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 { 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});
|
||||
|
||||
@ -45,11 +45,13 @@ You can customize the export with query parameters:
|
||||
| download | `false` | If the exported data should be downloaded as a file |
|
||||
| featureToggles | `true` | Include feature-toggles 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
|
||||
|
@ -268,6 +268,37 @@ class FeatureToggleStore {
|
||||
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) {
|
||||
const stopTimer = this.timer('untagFeature');
|
||||
try {
|
||||
@ -300,6 +331,24 @@ class FeatureToggleStore {
|
||||
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 }) {
|
||||
return {
|
||||
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) {
|
||||
try {
|
||||
await this.db(TABLE)
|
||||
|
@ -63,6 +63,20 @@ class TagStore {
|
||||
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) {
|
||||
return {
|
||||
type: row.type,
|
||||
|
@ -65,6 +65,24 @@ class TagTypeStore {
|
||||
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 }) {
|
||||
const stopTimer = this.timer('updateTagType');
|
||||
await this.db(TABLE)
|
||||
|
@ -8,6 +8,8 @@ module.exports = {
|
||||
FEATURE_REVIVED: 'feature-revived',
|
||||
FEATURE_IMPORT: 'feature-import',
|
||||
FEATURE_TAGGED: 'feature-tagged',
|
||||
FEATURE_TAG_IMPORT: 'feature-tag-import',
|
||||
DROP_FEATURE_TAGS: 'drop-feature-tags',
|
||||
FEATURE_UNTAGGED: 'feature-untagged',
|
||||
FEATURE_STALE_ON: 'feature-stale-on',
|
||||
FEATURE_STALE_OFF: 'feature-stale-off',
|
||||
@ -25,11 +27,17 @@ module.exports = {
|
||||
PROJECT_CREATED: 'project-created',
|
||||
PROJECT_UPDATED: 'project-updated',
|
||||
PROJECT_DELETED: 'project-deleted',
|
||||
PROJECT_IMPORT: 'project-import',
|
||||
DROP_PROJECTS: 'drop-projects',
|
||||
TAG_CREATED: 'tag-created',
|
||||
TAG_DELETED: 'tag-deleted',
|
||||
TAG_IMPORT: 'tag-import',
|
||||
DROP_TAGS: 'drop-tags',
|
||||
TAG_TYPE_CREATED: 'tag-type-created',
|
||||
TAG_TYPE_DELETED: 'tag-type-deleted',
|
||||
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_UPDATED: 'addon-config-updated',
|
||||
ADDON_CONFIG_DELETED: 'addon-config-deleted',
|
||||
|
@ -10,7 +10,16 @@ const extractUser = require('../../extract-user');
|
||||
const { handleErrors } = require('./util');
|
||||
|
||||
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 {
|
||||
constructor(config, services) {
|
||||
super(config);
|
||||
@ -39,8 +48,8 @@ class StateController extends Controller {
|
||||
await this.stateService.import({
|
||||
data,
|
||||
userName,
|
||||
dropBeforeImport: drop,
|
||||
keepExisting: keep,
|
||||
dropBeforeImport: paramToBool(drop),
|
||||
keepExisting: paramToBool(keep),
|
||||
});
|
||||
res.sendStatus(202);
|
||||
} catch (err) {
|
||||
@ -51,20 +60,21 @@ class StateController extends Controller {
|
||||
async export(req, res) {
|
||||
const { format } = req.query;
|
||||
|
||||
const downloadFile = Boolean(req.query.download);
|
||||
let includeStrategies = Boolean(req.query.strategies);
|
||||
let includeFeatureToggles = Boolean(req.query.featureToggles);
|
||||
|
||||
// if neither is passed as query argument, export both
|
||||
if (!includeStrategies && !includeFeatureToggles) {
|
||||
includeStrategies = true;
|
||||
includeFeatureToggles = true;
|
||||
}
|
||||
const downloadFile = paramToBool(req.query.download, false);
|
||||
const includeStrategies = paramToBool(req.query.strategies, true);
|
||||
const includeFeatureToggles = paramToBool(
|
||||
req.query.featureToggles,
|
||||
true,
|
||||
);
|
||||
const includeProjects = paramToBool(req.query.projects, true);
|
||||
const includeTags = paramToBool(req.query.tags, true);
|
||||
|
||||
try {
|
||||
const data = await this.stateService.export({
|
||||
includeStrategies,
|
||||
includeFeatureToggles,
|
||||
includeProjects,
|
||||
includeTags,
|
||||
});
|
||||
const timestamp = moment().format('YYYY-MM-DD_HH-mm-ss');
|
||||
if (format === 'yaml') {
|
||||
|
@ -109,4 +109,15 @@ const querySchema = joi
|
||||
})
|
||||
.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 { featureSchema } = require('./feature-schema');
|
||||
const { featureSchema, featureTagSchema } = require('./feature-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({
|
||||
version: joi.number(),
|
||||
features: joi
|
||||
@ -13,6 +15,22 @@ const stateSchema = joi.object().keys({
|
||||
.array()
|
||||
.optional()
|
||||
.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 = {
|
||||
|
@ -4,12 +4,20 @@ const {
|
||||
DROP_FEATURES,
|
||||
STRATEGY_IMPORT,
|
||||
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');
|
||||
|
||||
const {
|
||||
readFile,
|
||||
parseFile,
|
||||
filterExisitng,
|
||||
filterExisting,
|
||||
filterEqual,
|
||||
} = require('./state-util');
|
||||
|
||||
@ -18,6 +26,9 @@ class StateService {
|
||||
this.eventStore = stores.eventStore;
|
||||
this.toggleStore = stores.featureToggleStore;
|
||||
this.strategyStore = stores.strategyStore;
|
||||
this.tagStore = stores.tagStore;
|
||||
this.tagTypeStore = stores.tagTypeStore;
|
||||
this.projectStore = stores.projectStore;
|
||||
this.logger = getLogger('services/state-service.js');
|
||||
}
|
||||
|
||||
@ -49,6 +60,25 @@ class StateService {
|
||||
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({
|
||||
@ -74,7 +104,7 @@ class StateService {
|
||||
|
||||
await Promise.all(
|
||||
features
|
||||
.filter(filterExisitng(keepExisting, oldToggles))
|
||||
.filter(filterExisting(keepExisting, oldToggles))
|
||||
.filter(filterEqual(oldToggles))
|
||||
.map(feature =>
|
||||
this.toggleStore.importFeature(feature).then(() =>
|
||||
@ -111,7 +141,7 @@ class StateService {
|
||||
|
||||
await Promise.all(
|
||||
strategies
|
||||
.filter(filterExisitng(keepExisting, oldStrategies))
|
||||
.filter(filterExisting(keepExisting, oldStrategies))
|
||||
.filter(filterEqual(oldStrategies))
|
||||
.map(strategy =>
|
||||
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([
|
||||
includeFeatureToggles
|
||||
? this.toggleStore.getFeatures()
|
||||
@ -133,11 +339,32 @@ class StateService {
|
||||
includeStrategies
|
||||
? this.strategyStore.getEditableStrategies()
|
||||
: 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,
|
||||
features,
|
||||
strategies,
|
||||
}));
|
||||
projects,
|
||||
tagTypes,
|
||||
tags,
|
||||
featureTags,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,11 +6,16 @@ const store = require('../../test/fixtures/store');
|
||||
const getLogger = require('../../test/fixtures/no-logger');
|
||||
|
||||
const StateService = require('./state-service');
|
||||
const NotFoundError = require('../error/notfound-error');
|
||||
const {
|
||||
FEATURE_IMPORT,
|
||||
DROP_FEATURES,
|
||||
STRATEGY_IMPORT,
|
||||
DROP_STRATEGIES,
|
||||
TAG_TYPE_IMPORT,
|
||||
TAG_IMPORT,
|
||||
FEATURE_TAG_IMPORT,
|
||||
PROJECT_IMPORT,
|
||||
} = require('../event-type');
|
||||
|
||||
function getSetup() {
|
||||
@ -132,7 +137,7 @@ test('should import a strategy', async t => {
|
||||
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 data = {
|
||||
@ -219,3 +224,217 @@ test('should export strategies', async t => {
|
||||
t.is(data.strategies.length, 1);
|
||||
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);
|
||||
};
|
||||
|
||||
const filterExisitng = (keepExisting, exitingArray) => {
|
||||
const filterExisting = (keepExisting, existingArray = []) => {
|
||||
return item => {
|
||||
if (keepExisting) {
|
||||
const found = exitingArray.find(t => t.name === item.name);
|
||||
const found = existingArray.find(t => t.name === item.name);
|
||||
return !found;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
};
|
||||
|
||||
const filterEqual = exitingArray => {
|
||||
const filterEqual = (existingArray = []) => {
|
||||
return item => {
|
||||
const toggle = exitingArray.find(t => t.name === item.name);
|
||||
const toggle = existingArray.find(t => t.name === item.name);
|
||||
if (toggle) {
|
||||
return JSON.stringify(toggle) !== JSON.stringify(item);
|
||||
}
|
||||
@ -37,6 +37,6 @@ const filterEqual = exitingArray => {
|
||||
module.exports = {
|
||||
readFile,
|
||||
parseFile,
|
||||
filterExisitng,
|
||||
filterExisting,
|
||||
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();
|
||||
}
|
||||
|
||||
batchStore(events) {
|
||||
events.forEach(event => {
|
||||
this.events.push(event);
|
||||
this.emit(event.type, event);
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getEvents() {
|
||||
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) => {
|
||||
const _features = [];
|
||||
const _archive = [];
|
||||
const _featureTags = {};
|
||||
const _featureTags = [];
|
||||
|
||||
return {
|
||||
getFeature: name => {
|
||||
@ -79,15 +79,13 @@ module.exports = (databaseIsUp = true) => {
|
||||
return feature.name.indexOf(query[key]) > -1;
|
||||
}
|
||||
if (key === 'tag') {
|
||||
return query[key].some(tag => {
|
||||
return (
|
||||
_featureTags[feature.name] &&
|
||||
_featureTags[feature.name].some(t => {
|
||||
return (
|
||||
t.type === tag[0] &&
|
||||
t.value === tag[1]
|
||||
);
|
||||
})
|
||||
return query[key].some(tagQuery => {
|
||||
return _featureTags
|
||||
.filter(t => t.featureName === feature.name)
|
||||
.some(
|
||||
tag =>
|
||||
tag.tagType === tagQuery[0] &&
|
||||
tag.tagValue === tagQuery[1],
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -99,20 +97,43 @@ module.exports = (databaseIsUp = true) => {
|
||||
return Promise.resolve(_features);
|
||||
},
|
||||
tagFeature: (featureName, tag) => {
|
||||
_featureTags[featureName] = _featureTags[featureName] || [];
|
||||
_featureTags[featureName].push(tag);
|
||||
_featureTags.push({
|
||||
featureName,
|
||||
tagType: tag.type,
|
||||
tagValue: tag.value,
|
||||
});
|
||||
},
|
||||
untagFeature: event => {
|
||||
const tags = _featureTags[event.featureName];
|
||||
_featureTags[event.featureName] = tags.splice(
|
||||
tags.indexOf(
|
||||
t => t.type === event.type && t.value === event.value,
|
||||
),
|
||||
1,
|
||||
const index = _featureTags.findIndex(
|
||||
f =>
|
||||
f.featureName === event.featureName &&
|
||||
f.tagType === event.type &&
|
||||
f.tagValue === event.value,
|
||||
);
|
||||
_featureTags.splice(index, 1);
|
||||
},
|
||||
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'));
|
||||
},
|
||||
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');
|
||||
|
||||
module.exports = () => {
|
||||
const _tagTypes = {};
|
||||
const _tagTypes = [];
|
||||
return {
|
||||
getTagType: async name => {
|
||||
const tag = _tagTypes[name];
|
||||
const tag = _tagTypes.find(t => t.name === name);
|
||||
if (tag) {
|
||||
return Promise.resolve(tag);
|
||||
}
|
||||
return Promise.reject(new NotFoundError('Could not find tag type'));
|
||||
},
|
||||
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 settingStore = require('./fake-setting-store');
|
||||
const addonStore = require('./fake-addon-store');
|
||||
const projectStore = require('./fake-project-store');
|
||||
|
||||
module.exports = {
|
||||
createStores: (databaseIsUp = true) => {
|
||||
@ -33,6 +34,7 @@ module.exports = {
|
||||
contextFieldStore: contextFieldStore(databaseIsUp),
|
||||
settingStore: settingStore(databaseIsUp),
|
||||
addonStore: addonStore(databaseIsUp),
|
||||
projectStore: projectStore(databaseIsUp),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user