mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-18 00:19:49 +01:00
Add import/export for tags and projects (#754)
* Add import/export for tags and projects Tags includes (tags, tag-types and feature-tags) fixes: #752
This commit is contained in:
parent
e1fbe9d013
commit
289cf85a3c
@ -23,7 +23,7 @@ const unleash = require('unleash-server');
|
|||||||
const { services } = await unleash.start({...});
|
const { services } = await unleash.start({...});
|
||||||
const { stateService } = services;
|
const { stateService } = services;
|
||||||
|
|
||||||
const exportedData = await stateService.export({includeStrategies: false, includeFeatureToggles: true});
|
const exportedData = await stateService.export({includeStrategies: false, includeFeatureToggles: true, includeTags: true, includeProjects: true});
|
||||||
|
|
||||||
await stateService.import({data: exportedData, userName: 'import', dropBeforeImport: false});
|
await stateService.import({data: exportedData, userName: 'import', dropBeforeImport: false});
|
||||||
|
|
||||||
@ -45,11 +45,13 @@ You can customize the export with query parameters:
|
|||||||
| download | `false` | If the exported data should be downloaded as a file |
|
| download | `false` | If the exported data should be downloaded as a file |
|
||||||
| featureToggles | `true` | Include feature-toggles in the exported data |
|
| featureToggles | `true` | Include feature-toggles in the exported data |
|
||||||
| strategies | `true` | Include strategies in the exported data |
|
| strategies | `true` | Include strategies in the exported data |
|
||||||
|
| tags | `true` | Include tagtypes, tags and feature_tags in the exported data |
|
||||||
|
| projects | `true` | Include projects in the exported data |
|
||||||
|
|
||||||
For example if you want to download all feature-toggles as yaml:
|
For example if you want to download just feature-toggles as yaml:
|
||||||
|
|
||||||
```
|
```
|
||||||
/api/admin/state/export?format=yaml&featureToggles=1&download=1
|
/api/admin/state/export?format=yaml&featureToggles=1&strategies=0&tags=0&projects=0&download=1
|
||||||
```
|
```
|
||||||
|
|
||||||
### API Import
|
### API Import
|
||||||
|
@ -268,6 +268,37 @@ class FeatureToggleStore {
|
|||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAllFeatureTags() {
|
||||||
|
const rows = await this.db(FEATURE_TAG_TABLE).select(
|
||||||
|
FEATURE_TAG_COLUMNS,
|
||||||
|
);
|
||||||
|
return rows.map(row => {
|
||||||
|
return {
|
||||||
|
featureName: row.feature_name,
|
||||||
|
tagType: row.tag_type,
|
||||||
|
tagValue: row.tag_value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async dropFeatureTags() {
|
||||||
|
const stopTimer = this.timer('dropFeatureTags');
|
||||||
|
await this.db(FEATURE_TAG_TABLE).del();
|
||||||
|
stopTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async importFeatureTags(featureTags) {
|
||||||
|
const rows = await this.db(FEATURE_TAG_TABLE)
|
||||||
|
.insert(featureTags.map(this.importToRow))
|
||||||
|
.returning(FEATURE_TAG_COLUMNS)
|
||||||
|
.onConflict(FEATURE_TAG_COLUMNS)
|
||||||
|
.ignore();
|
||||||
|
if (rows) {
|
||||||
|
return rows.map(this.rowToFeatureAndTag);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
async untagFeature(featureName, tag) {
|
async untagFeature(featureName, tag) {
|
||||||
const stopTimer = this.timer('untagFeature');
|
const stopTimer = this.timer('untagFeature');
|
||||||
try {
|
try {
|
||||||
@ -300,6 +331,24 @@ class FeatureToggleStore {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rowToFeatureAndTag(row) {
|
||||||
|
return {
|
||||||
|
featureName: row.feature_name,
|
||||||
|
tag: {
|
||||||
|
type: row.tag_type,
|
||||||
|
value: row.tag_value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
importToRow({ featureName, tagType, tagValue }) {
|
||||||
|
return {
|
||||||
|
feature_name: featureName,
|
||||||
|
tag_type: tagType,
|
||||||
|
tag_value: tagValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
featureAndTagToRow(featureName, { type, value }) {
|
featureAndTagToRow(featureName, { type, value }) {
|
||||||
return {
|
return {
|
||||||
feature_name: featureName,
|
feature_name: featureName,
|
||||||
|
@ -67,6 +67,22 @@ class ProjectStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async importProjects(projects) {
|
||||||
|
const rows = await this.db(TABLE)
|
||||||
|
.insert(projects.map(this.fieldToRow))
|
||||||
|
.returning(COLUMNS)
|
||||||
|
.onConflict('id')
|
||||||
|
.ignore();
|
||||||
|
if (rows.length > 0) {
|
||||||
|
return rows.map(this.mapRow);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async dropProjects() {
|
||||||
|
await this.db(TABLE).del();
|
||||||
|
}
|
||||||
|
|
||||||
async delete(id) {
|
async delete(id) {
|
||||||
try {
|
try {
|
||||||
await this.db(TABLE)
|
await this.db(TABLE)
|
||||||
|
@ -63,6 +63,20 @@ class TagStore {
|
|||||||
stopTimer();
|
stopTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async dropTags() {
|
||||||
|
const stopTimer = this.timer('dropTags');
|
||||||
|
await this.db(TABLE).del();
|
||||||
|
stopTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkImport(tags) {
|
||||||
|
return this.db(TABLE)
|
||||||
|
.insert(tags)
|
||||||
|
.returning(COLUMNS)
|
||||||
|
.onConflict(['type', 'value'])
|
||||||
|
.ignore();
|
||||||
|
}
|
||||||
|
|
||||||
rowToTag(row) {
|
rowToTag(row) {
|
||||||
return {
|
return {
|
||||||
type: row.type,
|
type: row.type,
|
||||||
|
@ -65,6 +65,24 @@ class TagTypeStore {
|
|||||||
stopTimer();
|
stopTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async dropTagTypes() {
|
||||||
|
const stopTimer = this.timer('dropTagTypes');
|
||||||
|
await this.db(TABLE).del();
|
||||||
|
stopTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkImport(tagTypes) {
|
||||||
|
const rows = await this.db(TABLE)
|
||||||
|
.insert(tagTypes)
|
||||||
|
.returning(COLUMNS)
|
||||||
|
.onConflict('name')
|
||||||
|
.ignore();
|
||||||
|
if (rows.length > 0) {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
async updateTagType({ name, description, icon }) {
|
async updateTagType({ name, description, icon }) {
|
||||||
const stopTimer = this.timer('updateTagType');
|
const stopTimer = this.timer('updateTagType');
|
||||||
await this.db(TABLE)
|
await this.db(TABLE)
|
||||||
|
@ -8,6 +8,8 @@ module.exports = {
|
|||||||
FEATURE_REVIVED: 'feature-revived',
|
FEATURE_REVIVED: 'feature-revived',
|
||||||
FEATURE_IMPORT: 'feature-import',
|
FEATURE_IMPORT: 'feature-import',
|
||||||
FEATURE_TAGGED: 'feature-tagged',
|
FEATURE_TAGGED: 'feature-tagged',
|
||||||
|
FEATURE_TAG_IMPORT: 'feature-tag-import',
|
||||||
|
DROP_FEATURE_TAGS: 'drop-feature-tags',
|
||||||
FEATURE_UNTAGGED: 'feature-untagged',
|
FEATURE_UNTAGGED: 'feature-untagged',
|
||||||
FEATURE_STALE_ON: 'feature-stale-on',
|
FEATURE_STALE_ON: 'feature-stale-on',
|
||||||
FEATURE_STALE_OFF: 'feature-stale-off',
|
FEATURE_STALE_OFF: 'feature-stale-off',
|
||||||
@ -25,11 +27,17 @@ module.exports = {
|
|||||||
PROJECT_CREATED: 'project-created',
|
PROJECT_CREATED: 'project-created',
|
||||||
PROJECT_UPDATED: 'project-updated',
|
PROJECT_UPDATED: 'project-updated',
|
||||||
PROJECT_DELETED: 'project-deleted',
|
PROJECT_DELETED: 'project-deleted',
|
||||||
|
PROJECT_IMPORT: 'project-import',
|
||||||
|
DROP_PROJECTS: 'drop-projects',
|
||||||
TAG_CREATED: 'tag-created',
|
TAG_CREATED: 'tag-created',
|
||||||
TAG_DELETED: 'tag-deleted',
|
TAG_DELETED: 'tag-deleted',
|
||||||
|
TAG_IMPORT: 'tag-import',
|
||||||
|
DROP_TAGS: 'drop-tags',
|
||||||
TAG_TYPE_CREATED: 'tag-type-created',
|
TAG_TYPE_CREATED: 'tag-type-created',
|
||||||
TAG_TYPE_DELETED: 'tag-type-deleted',
|
TAG_TYPE_DELETED: 'tag-type-deleted',
|
||||||
TAG_TYPE_UPDATED: 'tag-type-updated',
|
TAG_TYPE_UPDATED: 'tag-type-updated',
|
||||||
|
TAG_TYPE_IMPORT: 'tag-type-import',
|
||||||
|
DROP_TAG_TYPES: 'drop-tag-types',
|
||||||
ADDON_CONFIG_CREATED: 'addon-config-created',
|
ADDON_CONFIG_CREATED: 'addon-config-created',
|
||||||
ADDON_CONFIG_UPDATED: 'addon-config-updated',
|
ADDON_CONFIG_UPDATED: 'addon-config-updated',
|
||||||
ADDON_CONFIG_DELETED: 'addon-config-deleted',
|
ADDON_CONFIG_DELETED: 'addon-config-deleted',
|
||||||
|
@ -10,7 +10,16 @@ const extractUser = require('../../extract-user');
|
|||||||
const { handleErrors } = require('./util');
|
const { handleErrors } = require('./util');
|
||||||
|
|
||||||
const upload = multer({ limits: { fileSize: 5242880 } });
|
const upload = multer({ limits: { fileSize: 5242880 } });
|
||||||
|
const paramToBool = (param, def) => {
|
||||||
|
if (param === null || param === undefined) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
const nu = Number.parseInt(param, 10);
|
||||||
|
if (Number.isNaN(nu)) {
|
||||||
|
return param.toLowerCase() === 'true';
|
||||||
|
}
|
||||||
|
return Boolean(nu);
|
||||||
|
};
|
||||||
class StateController extends Controller {
|
class StateController extends Controller {
|
||||||
constructor(config, services) {
|
constructor(config, services) {
|
||||||
super(config);
|
super(config);
|
||||||
@ -39,8 +48,8 @@ class StateController extends Controller {
|
|||||||
await this.stateService.import({
|
await this.stateService.import({
|
||||||
data,
|
data,
|
||||||
userName,
|
userName,
|
||||||
dropBeforeImport: drop,
|
dropBeforeImport: paramToBool(drop),
|
||||||
keepExisting: keep,
|
keepExisting: paramToBool(keep),
|
||||||
});
|
});
|
||||||
res.sendStatus(202);
|
res.sendStatus(202);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -51,20 +60,21 @@ class StateController extends Controller {
|
|||||||
async export(req, res) {
|
async export(req, res) {
|
||||||
const { format } = req.query;
|
const { format } = req.query;
|
||||||
|
|
||||||
const downloadFile = Boolean(req.query.download);
|
const downloadFile = paramToBool(req.query.download, false);
|
||||||
let includeStrategies = Boolean(req.query.strategies);
|
const includeStrategies = paramToBool(req.query.strategies, true);
|
||||||
let includeFeatureToggles = Boolean(req.query.featureToggles);
|
const includeFeatureToggles = paramToBool(
|
||||||
|
req.query.featureToggles,
|
||||||
// if neither is passed as query argument, export both
|
true,
|
||||||
if (!includeStrategies && !includeFeatureToggles) {
|
);
|
||||||
includeStrategies = true;
|
const includeProjects = paramToBool(req.query.projects, true);
|
||||||
includeFeatureToggles = true;
|
const includeTags = paramToBool(req.query.tags, true);
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await this.stateService.export({
|
const data = await this.stateService.export({
|
||||||
includeStrategies,
|
includeStrategies,
|
||||||
includeFeatureToggles,
|
includeFeatureToggles,
|
||||||
|
includeProjects,
|
||||||
|
includeTags,
|
||||||
});
|
});
|
||||||
const timestamp = moment().format('YYYY-MM-DD_HH-mm-ss');
|
const timestamp = moment().format('YYYY-MM-DD_HH-mm-ss');
|
||||||
if (format === 'yaml') {
|
if (format === 'yaml') {
|
||||||
|
@ -109,4 +109,15 @@ const querySchema = joi
|
|||||||
})
|
})
|
||||||
.options({ allowUnknown: false, stripUnknown: true, abortEarly: false });
|
.options({ allowUnknown: false, stripUnknown: true, abortEarly: false });
|
||||||
|
|
||||||
module.exports = { featureSchema, strategiesSchema, nameSchema, querySchema };
|
const featureTagSchema = joi.object().keys({
|
||||||
|
featureName: nameType,
|
||||||
|
tagType: nameType,
|
||||||
|
tagValue: joi.string(),
|
||||||
|
});
|
||||||
|
module.exports = {
|
||||||
|
featureSchema,
|
||||||
|
strategiesSchema,
|
||||||
|
nameSchema,
|
||||||
|
querySchema,
|
||||||
|
featureTagSchema,
|
||||||
|
};
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
const joi = require('joi');
|
const joi = require('joi');
|
||||||
const { featureSchema } = require('./feature-schema');
|
const { featureSchema, featureTagSchema } = require('./feature-schema');
|
||||||
const strategySchema = require('./strategy-schema');
|
const strategySchema = require('./strategy-schema');
|
||||||
|
const { tagSchema } = require('./tag-schema');
|
||||||
|
const { tagTypeSchema } = require('./tag-type-schema');
|
||||||
|
const projectSchema = require('./project-schema');
|
||||||
|
|
||||||
// TODO: Extract to seperate file
|
|
||||||
const stateSchema = joi.object().keys({
|
const stateSchema = joi.object().keys({
|
||||||
version: joi.number(),
|
version: joi.number(),
|
||||||
features: joi
|
features: joi
|
||||||
@ -13,6 +15,22 @@ const stateSchema = joi.object().keys({
|
|||||||
.array()
|
.array()
|
||||||
.optional()
|
.optional()
|
||||||
.items(strategySchema),
|
.items(strategySchema),
|
||||||
|
tags: joi
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
.items(tagSchema),
|
||||||
|
tagTypes: joi
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
.items(tagTypeSchema),
|
||||||
|
featureTags: joi
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
.items(featureTagSchema),
|
||||||
|
projects: joi
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
.items(projectSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -4,12 +4,20 @@ const {
|
|||||||
DROP_FEATURES,
|
DROP_FEATURES,
|
||||||
STRATEGY_IMPORT,
|
STRATEGY_IMPORT,
|
||||||
DROP_STRATEGIES,
|
DROP_STRATEGIES,
|
||||||
|
TAG_IMPORT,
|
||||||
|
DROP_TAGS,
|
||||||
|
FEATURE_TAG_IMPORT,
|
||||||
|
DROP_FEATURE_TAGS,
|
||||||
|
TAG_TYPE_IMPORT,
|
||||||
|
DROP_TAG_TYPES,
|
||||||
|
PROJECT_IMPORT,
|
||||||
|
DROP_PROJECTS,
|
||||||
} = require('../event-type');
|
} = require('../event-type');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
readFile,
|
readFile,
|
||||||
parseFile,
|
parseFile,
|
||||||
filterExisitng,
|
filterExisting,
|
||||||
filterEqual,
|
filterEqual,
|
||||||
} = require('./state-util');
|
} = require('./state-util');
|
||||||
|
|
||||||
@ -18,6 +26,9 @@ class StateService {
|
|||||||
this.eventStore = stores.eventStore;
|
this.eventStore = stores.eventStore;
|
||||||
this.toggleStore = stores.featureToggleStore;
|
this.toggleStore = stores.featureToggleStore;
|
||||||
this.strategyStore = stores.strategyStore;
|
this.strategyStore = stores.strategyStore;
|
||||||
|
this.tagStore = stores.tagStore;
|
||||||
|
this.tagTypeStore = stores.tagTypeStore;
|
||||||
|
this.projectStore = stores.projectStore;
|
||||||
this.logger = getLogger('services/state-service.js');
|
this.logger = getLogger('services/state-service.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +60,25 @@ class StateService {
|
|||||||
keepExisting,
|
keepExisting,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (importData.tagTypes && importData.tags) {
|
||||||
|
await this.importTagData({
|
||||||
|
tagTypes: data.tagTypes,
|
||||||
|
tags: data.tags,
|
||||||
|
featureTags: data.featureTags || [],
|
||||||
|
userName,
|
||||||
|
dropBeforeImport,
|
||||||
|
keepExisting,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (importData.projects) {
|
||||||
|
await this.importProjects({
|
||||||
|
projects: data.projects,
|
||||||
|
userName,
|
||||||
|
dropBeforeImport,
|
||||||
|
keepExisting,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async importFeatures({
|
async importFeatures({
|
||||||
@ -74,7 +104,7 @@ class StateService {
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
features
|
features
|
||||||
.filter(filterExisitng(keepExisting, oldToggles))
|
.filter(filterExisting(keepExisting, oldToggles))
|
||||||
.filter(filterEqual(oldToggles))
|
.filter(filterEqual(oldToggles))
|
||||||
.map(feature =>
|
.map(feature =>
|
||||||
this.toggleStore.importFeature(feature).then(() =>
|
this.toggleStore.importFeature(feature).then(() =>
|
||||||
@ -111,7 +141,7 @@ class StateService {
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
strategies
|
strategies
|
||||||
.filter(filterExisitng(keepExisting, oldStrategies))
|
.filter(filterExisting(keepExisting, oldStrategies))
|
||||||
.filter(filterEqual(oldStrategies))
|
.filter(filterEqual(oldStrategies))
|
||||||
.map(strategy =>
|
.map(strategy =>
|
||||||
this.strategyStore.importStrategy(strategy).then(() => {
|
this.strategyStore.importStrategy(strategy).then(() => {
|
||||||
@ -125,7 +155,183 @@ class StateService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async export({ includeFeatureToggles = true, includeStrategies = true }) {
|
async importProjects({
|
||||||
|
projects,
|
||||||
|
userName,
|
||||||
|
dropBeforeImport,
|
||||||
|
keepExisting,
|
||||||
|
}) {
|
||||||
|
this.logger.info(`Import ${projects.length} projects`);
|
||||||
|
const oldProjects = dropBeforeImport
|
||||||
|
? []
|
||||||
|
: await this.projectStore.getAll();
|
||||||
|
if (dropBeforeImport) {
|
||||||
|
this.logger.info('Dropping existing projects');
|
||||||
|
await this.projectStore.dropProjects();
|
||||||
|
await this.eventStore.store({
|
||||||
|
type: DROP_PROJECTS,
|
||||||
|
createdBy: userName,
|
||||||
|
data: { name: 'all-projects' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const projectsToImport = projects.filter(project =>
|
||||||
|
keepExisting
|
||||||
|
? !oldProjects.some(old => old.id === project.id)
|
||||||
|
: true,
|
||||||
|
);
|
||||||
|
if (projectsToImport.length > 0) {
|
||||||
|
const importedProjects = await this.projectStore.importProjects(
|
||||||
|
projectsToImport,
|
||||||
|
);
|
||||||
|
const importedProjectEvents = importedProjects.map(project => {
|
||||||
|
return {
|
||||||
|
type: PROJECT_IMPORT,
|
||||||
|
createdBy: userName,
|
||||||
|
data: project,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await this.eventStore.batchStore(importedProjectEvents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async importTagData({
|
||||||
|
tagTypes,
|
||||||
|
tags,
|
||||||
|
featureTags,
|
||||||
|
userName,
|
||||||
|
dropBeforeImport,
|
||||||
|
keepExisting,
|
||||||
|
}) {
|
||||||
|
this.logger.info(
|
||||||
|
`Importing ${tagTypes.length} tagtypes, ${tags.length} tags and ${featureTags.length} feature tags`,
|
||||||
|
);
|
||||||
|
const oldTagTypes = dropBeforeImport
|
||||||
|
? []
|
||||||
|
: await this.tagTypeStore.getAll();
|
||||||
|
const oldTags = dropBeforeImport ? [] : await this.tagStore.getAll();
|
||||||
|
const oldFeatureTags = dropBeforeImport
|
||||||
|
? []
|
||||||
|
: await this.toggleStore.getAllFeatureTags();
|
||||||
|
if (dropBeforeImport) {
|
||||||
|
this.logger.info(
|
||||||
|
'Dropping all existing featuretags, tags and tagtypes',
|
||||||
|
);
|
||||||
|
await this.toggleStore.dropFeatureTags();
|
||||||
|
await this.tagStore.dropTags();
|
||||||
|
await this.tagTypeStore.dropTagTypes();
|
||||||
|
await this.eventStore.batchStore([
|
||||||
|
{
|
||||||
|
type: DROP_FEATURE_TAGS,
|
||||||
|
createdBy: userName,
|
||||||
|
data: { name: 'all-feature-tags' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: DROP_TAGS,
|
||||||
|
createdBy: userName,
|
||||||
|
data: { name: 'all-tags' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: DROP_TAG_TYPES,
|
||||||
|
createdBy: userName,
|
||||||
|
data: { name: 'all-tag-types' },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await this.importTagTypes(
|
||||||
|
tagTypes,
|
||||||
|
keepExisting,
|
||||||
|
oldTagTypes,
|
||||||
|
userName,
|
||||||
|
);
|
||||||
|
await this.importTags(tags, keepExisting, oldTags, userName);
|
||||||
|
await this.importFeatureTags(
|
||||||
|
featureTags,
|
||||||
|
keepExisting,
|
||||||
|
oldFeatureTags,
|
||||||
|
userName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
compareFeatureTags = (old, tag) =>
|
||||||
|
old.featureName === tag.featureName &&
|
||||||
|
old.tagValue === tag.tagValue &&
|
||||||
|
old.tagType === tag.tagType;
|
||||||
|
|
||||||
|
async importFeatureTags(
|
||||||
|
featureTags,
|
||||||
|
keepExisting,
|
||||||
|
oldFeatureTags,
|
||||||
|
userName,
|
||||||
|
) {
|
||||||
|
const featureTagsToInsert = featureTags.filter(tag =>
|
||||||
|
keepExisting
|
||||||
|
? !oldFeatureTags.some(old => this.compareFeatureTags(old, tag))
|
||||||
|
: true,
|
||||||
|
);
|
||||||
|
if (featureTagsToInsert.length > 0) {
|
||||||
|
const importedFeatureTags = await this.toggleStore.importFeatureTags(
|
||||||
|
featureTagsToInsert,
|
||||||
|
);
|
||||||
|
const importedFeatureTagEvents = importedFeatureTags.map(tag => {
|
||||||
|
return {
|
||||||
|
type: FEATURE_TAG_IMPORT,
|
||||||
|
createdBy: userName,
|
||||||
|
data: tag,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await this.eventStore.batchStore(importedFeatureTagEvents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compareTags = (old, tag) =>
|
||||||
|
old.type === tag.type && old.value === tag.value;
|
||||||
|
|
||||||
|
async importTags(tags, keepExisting, oldTags, userName) {
|
||||||
|
const tagsToInsert = tags.filter(tag =>
|
||||||
|
keepExisting
|
||||||
|
? !oldTags.some(old => this.compareTags(old, tag))
|
||||||
|
: true,
|
||||||
|
);
|
||||||
|
if (tagsToInsert.length > 0) {
|
||||||
|
const importedTags = await this.tagStore.bulkImport(tagsToInsert);
|
||||||
|
const importedTagEvents = importedTags.map(tag => {
|
||||||
|
return {
|
||||||
|
type: TAG_IMPORT,
|
||||||
|
createdBy: userName,
|
||||||
|
data: tag,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await this.eventStore.batchStore(importedTagEvents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async importTagTypes(tagTypes, keepExisting, oldTagTypes = [], userName) {
|
||||||
|
const tagTypesToInsert = tagTypes.filter(tagType =>
|
||||||
|
keepExisting
|
||||||
|
? !oldTagTypes.some(t => t.name === tagType.name)
|
||||||
|
: true,
|
||||||
|
);
|
||||||
|
if (tagTypesToInsert.length > 0) {
|
||||||
|
const importedTagTypes = await this.tagTypeStore.bulkImport(
|
||||||
|
tagTypesToInsert,
|
||||||
|
);
|
||||||
|
const importedTagTypeEvents = importedTagTypes.map(tagType => {
|
||||||
|
return {
|
||||||
|
type: TAG_TYPE_IMPORT,
|
||||||
|
createdBy: userName,
|
||||||
|
data: tagType,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await this.eventStore.batchStore(importedTagTypeEvents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async export({
|
||||||
|
includeFeatureToggles = true,
|
||||||
|
includeStrategies = true,
|
||||||
|
includeProjects = true,
|
||||||
|
includeTags = true,
|
||||||
|
}) {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
includeFeatureToggles
|
includeFeatureToggles
|
||||||
? this.toggleStore.getFeatures()
|
? this.toggleStore.getFeatures()
|
||||||
@ -133,11 +339,32 @@ class StateService {
|
|||||||
includeStrategies
|
includeStrategies
|
||||||
? this.strategyStore.getEditableStrategies()
|
? this.strategyStore.getEditableStrategies()
|
||||||
: Promise.resolve(),
|
: Promise.resolve(),
|
||||||
]).then(([features, strategies]) => ({
|
this.projectStore && includeProjects
|
||||||
|
? this.projectStore.getAll()
|
||||||
|
: Promise.resolve(),
|
||||||
|
includeTags ? this.tagTypeStore.getAll() : Promise.resolve(),
|
||||||
|
includeTags ? this.tagStore.getAll() : Promise.resolve(),
|
||||||
|
includeTags
|
||||||
|
? this.toggleStore.getAllFeatureTags()
|
||||||
|
: Promise.resolve(),
|
||||||
|
]).then(
|
||||||
|
([
|
||||||
|
features,
|
||||||
|
strategies,
|
||||||
|
projects,
|
||||||
|
tagTypes,
|
||||||
|
tags,
|
||||||
|
featureTags,
|
||||||
|
]) => ({
|
||||||
version: 1,
|
version: 1,
|
||||||
features,
|
features,
|
||||||
strategies,
|
strategies,
|
||||||
}));
|
projects,
|
||||||
|
tagTypes,
|
||||||
|
tags,
|
||||||
|
featureTags,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,11 +6,16 @@ const store = require('../../test/fixtures/store');
|
|||||||
const getLogger = require('../../test/fixtures/no-logger');
|
const getLogger = require('../../test/fixtures/no-logger');
|
||||||
|
|
||||||
const StateService = require('./state-service');
|
const StateService = require('./state-service');
|
||||||
|
const NotFoundError = require('../error/notfound-error');
|
||||||
const {
|
const {
|
||||||
FEATURE_IMPORT,
|
FEATURE_IMPORT,
|
||||||
DROP_FEATURES,
|
DROP_FEATURES,
|
||||||
STRATEGY_IMPORT,
|
STRATEGY_IMPORT,
|
||||||
DROP_STRATEGIES,
|
DROP_STRATEGIES,
|
||||||
|
TAG_TYPE_IMPORT,
|
||||||
|
TAG_IMPORT,
|
||||||
|
FEATURE_TAG_IMPORT,
|
||||||
|
PROJECT_IMPORT,
|
||||||
} = require('../event-type');
|
} = require('../event-type');
|
||||||
|
|
||||||
function getSetup() {
|
function getSetup() {
|
||||||
@ -132,7 +137,7 @@ test('should import a strategy', async t => {
|
|||||||
t.is(events[0].data.name, 'new-strategy');
|
t.is(events[0].data.name, 'new-strategy');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not import an exiting strategy', async t => {
|
test('should not import an existing strategy', async t => {
|
||||||
const { stateService, stores } = getSetup();
|
const { stateService, stores } = getSetup();
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
@ -219,3 +224,217 @@ test('should export strategies', async t => {
|
|||||||
t.is(data.strategies.length, 1);
|
t.is(data.strategies.length, 1);
|
||||||
t.is(data.strategies[0].name, 'a-strategy');
|
t.is(data.strategies[0].name, 'a-strategy');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should import a tag and tag type', async t => {
|
||||||
|
const { stateService, stores } = getSetup();
|
||||||
|
const data = {
|
||||||
|
tagTypes: [
|
||||||
|
{ name: 'simple', description: 'some description', icon: '#' },
|
||||||
|
],
|
||||||
|
tags: [{ type: 'simple', value: 'test' }],
|
||||||
|
featureTags: [
|
||||||
|
{
|
||||||
|
featureName: 'demo-feature',
|
||||||
|
tagType: 'simple',
|
||||||
|
tagValue: 'test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
await stateService.import({ data });
|
||||||
|
const events = await stores.eventStore.getEvents();
|
||||||
|
t.is(events.length, 3);
|
||||||
|
t.is(events[0].type, TAG_TYPE_IMPORT);
|
||||||
|
t.is(events[0].data.name, 'simple');
|
||||||
|
t.is(events[1].type, TAG_IMPORT);
|
||||||
|
t.is(events[1].data.value, 'test');
|
||||||
|
t.is(events[2].type, FEATURE_TAG_IMPORT);
|
||||||
|
t.is(events[2].data.featureName, 'demo-feature');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should not import an existing tag', async t => {
|
||||||
|
const { stateService, stores } = getSetup();
|
||||||
|
const data = {
|
||||||
|
tagTypes: [
|
||||||
|
{ name: 'simple', description: 'some description', icon: '#' },
|
||||||
|
],
|
||||||
|
tags: [{ type: 'simple', value: 'test' }],
|
||||||
|
featureTags: [
|
||||||
|
{
|
||||||
|
featureName: 'demo-feature',
|
||||||
|
tagType: 'simple',
|
||||||
|
tagValue: 'test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
await stores.tagTypeStore.createTagType(data.tagTypes[0]);
|
||||||
|
await stores.tagStore.createTag(data.tags[0]);
|
||||||
|
await stores.featureToggleStore.tagFeature(
|
||||||
|
data.featureTags[0].featureName,
|
||||||
|
{
|
||||||
|
type: data.featureTags[0].tagType,
|
||||||
|
value: data.featureTags[0].tagValue,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await stateService.import({ data, keepExisting: true });
|
||||||
|
const events = await stores.eventStore.getEvents();
|
||||||
|
t.is(events.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should not keep existing tags if drop-before-import', async t => {
|
||||||
|
const { stateService, stores } = getSetup();
|
||||||
|
const notSoSimple = {
|
||||||
|
name: 'notsosimple',
|
||||||
|
description: 'some other description',
|
||||||
|
icon: '#',
|
||||||
|
};
|
||||||
|
const slack = {
|
||||||
|
name: 'slack',
|
||||||
|
description: 'slack tags',
|
||||||
|
icon: '#',
|
||||||
|
};
|
||||||
|
|
||||||
|
await stores.tagTypeStore.createTagType(notSoSimple);
|
||||||
|
await stores.tagTypeStore.createTagType(slack);
|
||||||
|
const data = {
|
||||||
|
tagTypes: [
|
||||||
|
{ name: 'simple', description: 'some description', icon: '#' },
|
||||||
|
],
|
||||||
|
tags: [{ type: 'simple', value: 'test' }],
|
||||||
|
featureTags: [
|
||||||
|
{
|
||||||
|
featureName: 'demo-feature',
|
||||||
|
tagType: 'simple',
|
||||||
|
tagValue: 'test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
await stateService.import({ data, dropBeforeImport: true });
|
||||||
|
const tagTypes = await stores.tagTypeStore.getAll();
|
||||||
|
t.is(tagTypes.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should export tag, tagtypes and feature tags', async t => {
|
||||||
|
const { stateService, stores } = getSetup();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
tagTypes: [
|
||||||
|
{ name: 'simple', description: 'some description', icon: '#' },
|
||||||
|
],
|
||||||
|
tags: [{ type: 'simple', value: 'test' }],
|
||||||
|
featureTags: [
|
||||||
|
{
|
||||||
|
featureName: 'demo-feature',
|
||||||
|
tagType: 'simple',
|
||||||
|
tagValue: 'test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
await stores.tagTypeStore.createTagType(data.tagTypes[0]);
|
||||||
|
await stores.tagStore.createTag(data.tags[0]);
|
||||||
|
await stores.featureToggleStore.tagFeature(
|
||||||
|
data.featureTags[0].featureName,
|
||||||
|
{
|
||||||
|
type: data.featureTags[0].tagType,
|
||||||
|
value: data.featureTags[0].tagValue,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const exported = await stateService.export({
|
||||||
|
includeFeatureToggles: false,
|
||||||
|
includeStrategies: false,
|
||||||
|
includeTags: true,
|
||||||
|
includeProjects: false,
|
||||||
|
});
|
||||||
|
t.is(exported.tags.length, 1);
|
||||||
|
t.is(exported.tags[0].type, data.tags[0].type);
|
||||||
|
t.is(exported.tags[0].value, data.tags[0].value);
|
||||||
|
t.is(exported.tagTypes.length, 1);
|
||||||
|
t.is(exported.tagTypes[0].name, data.tagTypes[0].name);
|
||||||
|
t.is(exported.featureTags.length, 1);
|
||||||
|
t.is(exported.featureTags[0].featureName, data.featureTags[0].featureName);
|
||||||
|
t.is(exported.featureTags[0].tagType, data.featureTags[0].tagType);
|
||||||
|
t.is(exported.featureTags[0].tagValue, data.featureTags[0].tagValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should import a project', async t => {
|
||||||
|
const { stateService, stores } = getSetup();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
id: 'default',
|
||||||
|
name: 'default',
|
||||||
|
description: 'Some fancy description for project',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await stateService.import({ data });
|
||||||
|
|
||||||
|
const events = await stores.eventStore.getEvents();
|
||||||
|
t.is(events.length, 1);
|
||||||
|
t.is(events[0].type, PROJECT_IMPORT);
|
||||||
|
t.is(events[0].data.name, 'default');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should not import an existing project', async t => {
|
||||||
|
const { stateService, stores } = getSetup();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
id: 'default',
|
||||||
|
name: 'default',
|
||||||
|
description: 'Some fancy description for project',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
await stores.projectStore.create(data.projects[0]);
|
||||||
|
|
||||||
|
await stateService.import({ data, keepExisting: true });
|
||||||
|
const events = await stores.eventStore.getEvents();
|
||||||
|
t.is(events.length, 0);
|
||||||
|
|
||||||
|
await stateService.import({ data });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should drop projects before import if specified', async t => {
|
||||||
|
const { stateService, stores } = getSetup();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
id: 'default',
|
||||||
|
name: 'default',
|
||||||
|
description: 'Some fancy description for project',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
await stores.projectStore.create({
|
||||||
|
id: 'fancy',
|
||||||
|
name: 'extra',
|
||||||
|
description: 'Not expected to be seen after import',
|
||||||
|
});
|
||||||
|
await stateService.import({ data, dropBeforeImport: true });
|
||||||
|
return t.throwsAsync(async () => stores.projectStore.hasProject('fancy'), {
|
||||||
|
instanceOf: NotFoundError,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should export projects', async t => {
|
||||||
|
const { stateService, stores } = getSetup();
|
||||||
|
await stores.projectStore.create({
|
||||||
|
id: 'fancy',
|
||||||
|
name: 'extra',
|
||||||
|
description: 'No surprises here',
|
||||||
|
});
|
||||||
|
const exported = await stateService.export({
|
||||||
|
includeFeatureToggles: false,
|
||||||
|
includeStrategies: false,
|
||||||
|
includeTags: false,
|
||||||
|
includeProjects: true,
|
||||||
|
});
|
||||||
|
t.is(exported.projects[0].id, 'fancy');
|
||||||
|
t.is(exported.projects[0].name, 'extra');
|
||||||
|
t.is(exported.projects[0].description, 'No surprises here');
|
||||||
|
});
|
||||||
|
@ -14,19 +14,19 @@ const parseFile = (file, data) => {
|
|||||||
: JSON.parse(data);
|
: JSON.parse(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterExisitng = (keepExisting, exitingArray) => {
|
const filterExisting = (keepExisting, existingArray = []) => {
|
||||||
return item => {
|
return item => {
|
||||||
if (keepExisting) {
|
if (keepExisting) {
|
||||||
const found = exitingArray.find(t => t.name === item.name);
|
const found = existingArray.find(t => t.name === item.name);
|
||||||
return !found;
|
return !found;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterEqual = exitingArray => {
|
const filterEqual = (existingArray = []) => {
|
||||||
return item => {
|
return item => {
|
||||||
const toggle = exitingArray.find(t => t.name === item.name);
|
const toggle = existingArray.find(t => t.name === item.name);
|
||||||
if (toggle) {
|
if (toggle) {
|
||||||
return JSON.stringify(toggle) !== JSON.stringify(item);
|
return JSON.stringify(toggle) !== JSON.stringify(item);
|
||||||
}
|
}
|
||||||
@ -37,6 +37,6 @@ const filterEqual = exitingArray => {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
readFile,
|
readFile,
|
||||||
parseFile,
|
parseFile,
|
||||||
filterExisitng,
|
filterExisting,
|
||||||
filterEqual,
|
filterEqual,
|
||||||
};
|
};
|
||||||
|
8
src/test/fixtures/fake-event-store.js
vendored
8
src/test/fixtures/fake-event-store.js
vendored
@ -15,6 +15,14 @@ class EventStore extends EventEmitter {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
batchStore(events) {
|
||||||
|
events.forEach(event => {
|
||||||
|
this.events.push(event);
|
||||||
|
this.emit(event.type, event);
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
getEvents() {
|
getEvents() {
|
||||||
return Promise.resolve(this.events);
|
return Promise.resolve(this.events);
|
||||||
}
|
}
|
||||||
|
59
src/test/fixtures/fake-feature-toggle-store.js
vendored
59
src/test/fixtures/fake-feature-toggle-store.js
vendored
@ -3,7 +3,7 @@
|
|||||||
module.exports = (databaseIsUp = true) => {
|
module.exports = (databaseIsUp = true) => {
|
||||||
const _features = [];
|
const _features = [];
|
||||||
const _archive = [];
|
const _archive = [];
|
||||||
const _featureTags = {};
|
const _featureTags = [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getFeature: name => {
|
getFeature: name => {
|
||||||
@ -79,15 +79,13 @@ module.exports = (databaseIsUp = true) => {
|
|||||||
return feature.name.indexOf(query[key]) > -1;
|
return feature.name.indexOf(query[key]) > -1;
|
||||||
}
|
}
|
||||||
if (key === 'tag') {
|
if (key === 'tag') {
|
||||||
return query[key].some(tag => {
|
return query[key].some(tagQuery => {
|
||||||
return (
|
return _featureTags
|
||||||
_featureTags[feature.name] &&
|
.filter(t => t.featureName === feature.name)
|
||||||
_featureTags[feature.name].some(t => {
|
.some(
|
||||||
return (
|
tag =>
|
||||||
t.type === tag[0] &&
|
tag.tagType === tagQuery[0] &&
|
||||||
t.value === tag[1]
|
tag.tagValue === tagQuery[1],
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -99,20 +97,43 @@ module.exports = (databaseIsUp = true) => {
|
|||||||
return Promise.resolve(_features);
|
return Promise.resolve(_features);
|
||||||
},
|
},
|
||||||
tagFeature: (featureName, tag) => {
|
tagFeature: (featureName, tag) => {
|
||||||
_featureTags[featureName] = _featureTags[featureName] || [];
|
_featureTags.push({
|
||||||
_featureTags[featureName].push(tag);
|
featureName,
|
||||||
|
tagType: tag.type,
|
||||||
|
tagValue: tag.value,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
untagFeature: event => {
|
untagFeature: event => {
|
||||||
const tags = _featureTags[event.featureName];
|
const index = _featureTags.findIndex(
|
||||||
_featureTags[event.featureName] = tags.splice(
|
f =>
|
||||||
tags.indexOf(
|
f.featureName === event.featureName &&
|
||||||
t => t.type === event.type && t.value === event.value,
|
f.tagType === event.type &&
|
||||||
),
|
f.tagValue === event.value,
|
||||||
1,
|
|
||||||
);
|
);
|
||||||
|
_featureTags.splice(index, 1);
|
||||||
},
|
},
|
||||||
getAllTagsForFeature: featureName => {
|
getAllTagsForFeature: featureName => {
|
||||||
return _featureTags[featureName] || [];
|
return Promise.resolve(
|
||||||
|
_featureTags
|
||||||
|
.filter(f => f.featureName === featureName)
|
||||||
|
.map(t => {
|
||||||
|
return {
|
||||||
|
type: t.tagType,
|
||||||
|
value: t.tagValue,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getAllFeatureTags: () => Promise.resolve(_featureTags),
|
||||||
|
importFeatureTags: tags => {
|
||||||
|
tags.forEach(tag => {
|
||||||
|
_featureTags.push(tag);
|
||||||
|
});
|
||||||
|
return Promise.resolve(_featureTags);
|
||||||
|
},
|
||||||
|
dropFeatureTags: () => {
|
||||||
|
_featureTags.splice(0, _featureTags.length);
|
||||||
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
35
src/test/fixtures/fake-project-store.js
vendored
Normal file
35
src/test/fixtures/fake-project-store.js
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
const NotFoundError = require('../../lib/error/notfound-error');
|
||||||
|
|
||||||
|
module.exports = (databaseIsUp = true) => {
|
||||||
|
const _projects = [];
|
||||||
|
return {
|
||||||
|
create: project => {
|
||||||
|
_projects.push(project);
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
getAll: () => {
|
||||||
|
if (databaseIsUp) {
|
||||||
|
return Promise.resolve(_projects);
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('Database is down'));
|
||||||
|
},
|
||||||
|
importProjects: projects => {
|
||||||
|
projects.forEach(project => {
|
||||||
|
_projects.push(project);
|
||||||
|
});
|
||||||
|
return Promise.resolve(_projects);
|
||||||
|
},
|
||||||
|
dropProjects: () => {
|
||||||
|
_projects.splice(0, _projects.length);
|
||||||
|
},
|
||||||
|
hasProject: id => {
|
||||||
|
const project = _projects.find(p => p.id === id);
|
||||||
|
if (project) {
|
||||||
|
return Promise.resolve(project);
|
||||||
|
}
|
||||||
|
return Promise.reject(
|
||||||
|
new NotFoundError(`Could not find project with id ${id}`),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
8
src/test/fixtures/fake-tag-store.js
vendored
8
src/test/fixtures/fake-tag-store.js
vendored
@ -34,5 +34,13 @@ module.exports = (databaseIsUp = true) => {
|
|||||||
}
|
}
|
||||||
return Promise.reject(new NotFoundError('Could not find tag'));
|
return Promise.reject(new NotFoundError('Could not find tag'));
|
||||||
},
|
},
|
||||||
|
bulkImport: tags => {
|
||||||
|
tags.forEach(tag => _tags.push(tag));
|
||||||
|
return Promise.resolve(_tags);
|
||||||
|
},
|
||||||
|
dropTags: () => {
|
||||||
|
_tags.splice(0, _tags.length);
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
15
src/test/fixtures/fake-tag-type-store.js
vendored
15
src/test/fixtures/fake-tag-type-store.js
vendored
@ -1,17 +1,26 @@
|
|||||||
const NotFoundError = require('../../lib/error/notfound-error');
|
const NotFoundError = require('../../lib/error/notfound-error');
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
const _tagTypes = {};
|
const _tagTypes = [];
|
||||||
return {
|
return {
|
||||||
getTagType: async name => {
|
getTagType: async name => {
|
||||||
const tag = _tagTypes[name];
|
const tag = _tagTypes.find(t => t.name === name);
|
||||||
if (tag) {
|
if (tag) {
|
||||||
return Promise.resolve(tag);
|
return Promise.resolve(tag);
|
||||||
}
|
}
|
||||||
return Promise.reject(new NotFoundError('Could not find tag type'));
|
return Promise.reject(new NotFoundError('Could not find tag type'));
|
||||||
},
|
},
|
||||||
createTagType: async tag => {
|
createTagType: async tag => {
|
||||||
_tagTypes[tag.name] = tag;
|
_tagTypes.push(tag);
|
||||||
|
},
|
||||||
|
getAll: () => Promise.resolve(_tagTypes),
|
||||||
|
bulkImport: tagTypes => {
|
||||||
|
tagTypes.forEach(tagType => _tagTypes.push(tagType));
|
||||||
|
return Promise.resolve(_tagTypes);
|
||||||
|
},
|
||||||
|
dropTagTypes: () => {
|
||||||
|
_tagTypes.splice(0, _tagTypes.length);
|
||||||
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
2
src/test/fixtures/store.js
vendored
2
src/test/fixtures/store.js
vendored
@ -11,6 +11,7 @@ const strategyStore = require('./fake-strategies-store');
|
|||||||
const contextFieldStore = require('./fake-context-store');
|
const contextFieldStore = require('./fake-context-store');
|
||||||
const settingStore = require('./fake-setting-store');
|
const settingStore = require('./fake-setting-store');
|
||||||
const addonStore = require('./fake-addon-store');
|
const addonStore = require('./fake-addon-store');
|
||||||
|
const projectStore = require('./fake-project-store');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createStores: (databaseIsUp = true) => {
|
createStores: (databaseIsUp = true) => {
|
||||||
@ -33,6 +34,7 @@ module.exports = {
|
|||||||
contextFieldStore: contextFieldStore(databaseIsUp),
|
contextFieldStore: contextFieldStore(databaseIsUp),
|
||||||
settingStore: settingStore(databaseIsUp),
|
settingStore: settingStore(databaseIsUp),
|
||||||
addonStore: addonStore(databaseIsUp),
|
addonStore: addonStore(databaseIsUp),
|
||||||
|
projectStore: projectStore(databaseIsUp),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user