1
0
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:
Christopher Kolstad 2021-03-12 11:08:10 +01:00 committed by GitHub
parent e1fbe9d013
commit 289cf85a3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 731 additions and 56 deletions

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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',

View File

@ -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') {

View File

@ -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,
};

View File

@ -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 = {

View File

@ -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,
}),
);
}
}

View File

@ -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');
});

View File

@ -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,
};

View File

@ -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);
}

View File

@ -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
View 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}`),
);
},
};
};

View File

@ -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();
},
};
};

View File

@ -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();
},
};
};

View File

@ -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),
};
},
};