mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
Add service layer
This simplifies stores to just be storage interaction, they no longer react to events. Controllers now call services and awaits the result from the call. When the service calls are returned the database is updated. This simplifies testing dramatically, cause you know that your state is updated when returned from a call, rather than hoping the store has picked up the event (which really was a command) and reacted to it. Events are still emitted from eventStore, so other parts of the app can react to events as they're being sent out. As part of the move to services, we now also emit an application-created event when we see a new client application. Fixes: #685 Fixes: #595
This commit is contained in:
parent
6ef2916200
commit
c17a1980a2
@ -24,6 +24,7 @@ Defined event types:
|
||||
- tag-type-created
|
||||
- tag-type-updated
|
||||
- tag-type-deleted
|
||||
- application-created
|
||||
|
||||
**Response**
|
||||
|
||||
|
@ -1,9 +0,0 @@
|
||||
module.exports = {
|
||||
TAG_FEATURE: 'tag-feature',
|
||||
UNTAG_FEATURE: 'untag-feature',
|
||||
CREATE_TAG: 'create-tag',
|
||||
DELETE_TAG: 'delete-tag',
|
||||
CREATE_TAG_TYPE: 'create-tag-type',
|
||||
DELETE_TAG_TYPE: 'delete-tag-type',
|
||||
UPDATE_TAG_TYPE: 'update-tag-type',
|
||||
};
|
@ -2,24 +2,13 @@
|
||||
|
||||
const metricsHelper = require('../metrics-helper');
|
||||
const { DB_TIME } = require('../events');
|
||||
const {
|
||||
TAG_CREATED,
|
||||
TAG_DELETED,
|
||||
FEATURE_TAGGED,
|
||||
FEATURE_UNTAGGED,
|
||||
} = require('../event-type');
|
||||
const { CREATE_TAG, DELETE_TAG } = require('../command-type');
|
||||
const NotFoundError = require('../error/notfound-error');
|
||||
|
||||
const COLUMNS = ['type', 'value'];
|
||||
const FEATURE_TAG_COLUMNS = ['feature_name', 'tag_type', 'tag_value'];
|
||||
const TABLE = 'tags';
|
||||
const FEATURE_TAG_TABLE = 'feature_tag';
|
||||
|
||||
class FeatureTagStore {
|
||||
constructor(db, eventStore, eventBus, getLogger) {
|
||||
constructor(db, eventBus, getLogger) {
|
||||
this.db = db;
|
||||
this.eventStore = eventStore;
|
||||
this.logger = getLogger('feature-tag-store.js');
|
||||
|
||||
this.timer = action =>
|
||||
@ -27,29 +16,6 @@ class FeatureTagStore {
|
||||
store: 'tag',
|
||||
action,
|
||||
});
|
||||
|
||||
eventStore.on(CREATE_TAG, event => this._createTag(event.data));
|
||||
eventStore.on(DELETE_TAG, event => this._deleteTag(event.data));
|
||||
}
|
||||
|
||||
async getTags() {
|
||||
const stopTimer = this.timer('getTags');
|
||||
const rows = await this.db.select(COLUMNS).from(TABLE);
|
||||
stopTimer();
|
||||
return rows.map(this.rowToTag);
|
||||
}
|
||||
|
||||
async getAllOfType(type) {
|
||||
const stopTimer = this.timer('getAllOfType');
|
||||
|
||||
const rows = await this.db
|
||||
.select(COLUMNS)
|
||||
.from(TABLE)
|
||||
.where({ type });
|
||||
|
||||
stopTimer();
|
||||
|
||||
return rows.map(this.rowToTag());
|
||||
}
|
||||
|
||||
async getAllTagsForFeature(featureName) {
|
||||
@ -62,131 +28,28 @@ class FeatureTagStore {
|
||||
return rows.map(this.featureTagRowToTag);
|
||||
}
|
||||
|
||||
async getTagByTypeAndValue(type, value) {
|
||||
const stopTimer = this.timer('getTagByTypeAndValue');
|
||||
const row = await this.db
|
||||
.first(COLUMNS)
|
||||
.from(TABLE)
|
||||
.where({ type, value });
|
||||
stopTimer();
|
||||
if (!row) {
|
||||
throw new NotFoundError('Could not find this tag');
|
||||
}
|
||||
return this.rowToTag(row);
|
||||
}
|
||||
|
||||
async hasTag(tag) {
|
||||
const stopTimer = this.timer('hasTag');
|
||||
const row = await this.db
|
||||
.first(COLUMNS)
|
||||
.from(TABLE)
|
||||
.where({
|
||||
type: tag.type,
|
||||
value: tag.value,
|
||||
});
|
||||
stopTimer();
|
||||
if (!row) {
|
||||
throw new NotFoundError('No tag found');
|
||||
}
|
||||
return this.rowToTag(row);
|
||||
}
|
||||
|
||||
async tagFeature(event) {
|
||||
async tagFeature(featureName, tag) {
|
||||
const stopTimer = this.timer('tagFeature');
|
||||
const tag = this.eventDataToRow(event);
|
||||
try {
|
||||
await this.hasTag(tag);
|
||||
} catch (err) {
|
||||
if (err instanceof NotFoundError) {
|
||||
this.logger.info(`Tag ${tag} did not exist. Creating.`);
|
||||
await this._createTag(tag);
|
||||
} else {
|
||||
this.logger.debug('Already existed');
|
||||
}
|
||||
}
|
||||
await this.db(FEATURE_TAG_TABLE)
|
||||
.insert({
|
||||
feature_name: event.featureName,
|
||||
tag_type: tag.type,
|
||||
tag_value: tag.value,
|
||||
})
|
||||
.insert(this.featureAndTagToRow(featureName, tag))
|
||||
.onConflict(['feature_name', 'tag_type', 'tag_value'])
|
||||
.ignore();
|
||||
stopTimer();
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_TAGGED,
|
||||
createdBy: event.createdBy || 'unleash-system',
|
||||
data: {
|
||||
featureName: event.featureName,
|
||||
tag,
|
||||
},
|
||||
});
|
||||
return tag;
|
||||
}
|
||||
|
||||
async untagFeature(event) {
|
||||
async untagFeature(featureName, tag) {
|
||||
const stopTimer = this.timer('untagFeature');
|
||||
try {
|
||||
await this.db(FEATURE_TAG_TABLE)
|
||||
.where({
|
||||
feature_name: event.featureName,
|
||||
tag_type: event.tagType,
|
||||
tag_value: event.tagValue,
|
||||
})
|
||||
.where(this.featureAndTagToRow(featureName, tag))
|
||||
.delete();
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_UNTAGGED,
|
||||
createdBy: event.createdBy || 'unleash-system',
|
||||
data: {
|
||||
featureName: event.featureName,
|
||||
tag: {
|
||||
value: event.tagValue,
|
||||
type: event.tagType,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
}
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
async _createTag(event) {
|
||||
const stopTimer = this.timer('createTag');
|
||||
await this.db(TABLE)
|
||||
.insert(this.eventDataToRow(event))
|
||||
.onConflict(['type', 'value'])
|
||||
.ignore();
|
||||
await this.eventStore.store({
|
||||
type: TAG_CREATED,
|
||||
createdBy: event.createdBy || 'unleash-system',
|
||||
data: {
|
||||
value: event.value,
|
||||
type: event.type,
|
||||
},
|
||||
});
|
||||
stopTimer();
|
||||
return { value: event.value, type: event.type };
|
||||
}
|
||||
|
||||
async _deleteTag(event) {
|
||||
const stopTimer = this.timer('deleteTag');
|
||||
const tag = this.eventDataToRow(event);
|
||||
try {
|
||||
await this.db(TABLE)
|
||||
.where(tag)
|
||||
.del();
|
||||
await this.eventStore.store({
|
||||
type: TAG_DELETED,
|
||||
createdBy: event.createdBy || 'unleash-system',
|
||||
data: tag,
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error('Could not delete tag, error: ', err);
|
||||
}
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
rowToTag(row) {
|
||||
if (row) {
|
||||
return {
|
||||
@ -207,10 +70,11 @@ class FeatureTagStore {
|
||||
return null;
|
||||
}
|
||||
|
||||
eventDataToRow(event) {
|
||||
featureAndTagToRow(featureName, { type, value }) {
|
||||
return {
|
||||
value: event.value,
|
||||
type: event.type,
|
||||
feature_name: featureName,
|
||||
tag_type: type,
|
||||
tag_value: value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -2,14 +2,6 @@
|
||||
|
||||
const metricsHelper = require('../metrics-helper');
|
||||
const { DB_TIME } = require('../events');
|
||||
const {
|
||||
FEATURE_CREATED,
|
||||
FEATURE_UPDATED,
|
||||
FEATURE_ARCHIVED,
|
||||
FEATURE_REVIVED,
|
||||
FEATURE_IMPORT,
|
||||
DROP_FEATURES,
|
||||
} = require('../event-type');
|
||||
const NotFoundError = require('../error/notfound-error');
|
||||
|
||||
const FEATURE_COLUMNS = [
|
||||
@ -27,7 +19,7 @@ const FEATURE_COLUMNS = [
|
||||
const TABLE = 'features';
|
||||
|
||||
class FeatureToggleStore {
|
||||
constructor(db, eventStore, eventBus, getLogger) {
|
||||
constructor(db, eventBus, getLogger) {
|
||||
this.db = db;
|
||||
this.logger = getLogger('feature-toggle-store.js');
|
||||
this.timer = action =>
|
||||
@ -35,21 +27,6 @@ class FeatureToggleStore {
|
||||
store: 'feature-toggle',
|
||||
action,
|
||||
});
|
||||
|
||||
eventStore.on(FEATURE_CREATED, event =>
|
||||
this._createFeature(event.data),
|
||||
);
|
||||
eventStore.on(FEATURE_UPDATED, event =>
|
||||
this._updateFeature(event.data),
|
||||
);
|
||||
eventStore.on(FEATURE_ARCHIVED, event =>
|
||||
this._archiveFeature(event.data),
|
||||
);
|
||||
eventStore.on(FEATURE_REVIVED, event =>
|
||||
this._reviveFeature(event.data),
|
||||
);
|
||||
eventStore.on(FEATURE_IMPORT, event => this._importFeature(event.data));
|
||||
eventStore.on(DROP_FEATURES, () => this._dropFeatures());
|
||||
}
|
||||
|
||||
async getFeatures() {
|
||||
@ -159,7 +136,7 @@ class FeatureToggleStore {
|
||||
};
|
||||
}
|
||||
|
||||
async _createFeature(data) {
|
||||
async createFeature(data) {
|
||||
try {
|
||||
await this.db(TABLE).insert(this.eventDataToRow(data));
|
||||
} catch (err) {
|
||||
@ -167,7 +144,7 @@ class FeatureToggleStore {
|
||||
}
|
||||
}
|
||||
|
||||
async _updateFeature(data) {
|
||||
async updateFeature(data) {
|
||||
try {
|
||||
await this.db(TABLE)
|
||||
.where({ name: data.name })
|
||||
@ -177,7 +154,7 @@ class FeatureToggleStore {
|
||||
}
|
||||
}
|
||||
|
||||
async _archiveFeature({ name }) {
|
||||
async archiveFeature(name) {
|
||||
try {
|
||||
await this.db(TABLE)
|
||||
.where({ name })
|
||||
@ -187,7 +164,7 @@ class FeatureToggleStore {
|
||||
}
|
||||
}
|
||||
|
||||
async _reviveFeature({ name }) {
|
||||
async reviveFeature({ name }) {
|
||||
try {
|
||||
await this.db(TABLE)
|
||||
.where({ name })
|
||||
@ -197,7 +174,7 @@ class FeatureToggleStore {
|
||||
}
|
||||
}
|
||||
|
||||
async _importFeature(data) {
|
||||
async importFeature(data) {
|
||||
const rowData = this.eventDataToRow(data);
|
||||
try {
|
||||
const result = await this.db(TABLE)
|
||||
@ -212,7 +189,7 @@ class FeatureToggleStore {
|
||||
}
|
||||
}
|
||||
|
||||
async _dropFeatures() {
|
||||
async dropFeatures() {
|
||||
try {
|
||||
await this.db(TABLE).delete();
|
||||
} catch (err) {
|
||||
|
@ -13,6 +13,7 @@ const ContextFieldStore = require('./context-field-store');
|
||||
const SettingStore = require('./setting-store');
|
||||
const UserStore = require('./user-store');
|
||||
const ProjectStore = require('./project-store');
|
||||
const TagStore = require('./tag-store');
|
||||
const FeatureTagStore = require('./feature-tag-store');
|
||||
const TagTypeStore = require('./tag-type-store');
|
||||
|
||||
@ -25,14 +26,9 @@ module.exports.createStores = (config, eventBus) => {
|
||||
return {
|
||||
db,
|
||||
eventStore,
|
||||
featureToggleStore: new FeatureToggleStore(
|
||||
db,
|
||||
eventStore,
|
||||
eventBus,
|
||||
getLogger,
|
||||
),
|
||||
featureToggleStore: new FeatureToggleStore(db, eventBus, getLogger),
|
||||
featureTypeStore: new FeatureTypeStore(db, getLogger),
|
||||
strategyStore: new StrategyStore(db, eventStore, getLogger),
|
||||
strategyStore: new StrategyStore(db, getLogger),
|
||||
clientApplicationsStore: new ClientApplicationsStore(
|
||||
db,
|
||||
eventBus,
|
||||
@ -53,12 +49,8 @@ module.exports.createStores = (config, eventBus) => {
|
||||
settingStore: new SettingStore(db, getLogger),
|
||||
userStore: new UserStore(db, getLogger),
|
||||
projectStore: new ProjectStore(db, getLogger),
|
||||
featureTagStore: new FeatureTagStore(
|
||||
db,
|
||||
eventStore,
|
||||
eventBus,
|
||||
getLogger,
|
||||
),
|
||||
tagTypeStore: new TagTypeStore(db, eventStore, eventBus, getLogger),
|
||||
tagStore: new TagStore(db, eventBus, getLogger),
|
||||
tagTypeStore: new TagTypeStore(db, eventBus, getLogger),
|
||||
featureTagStore: new FeatureTagStore(db, eventBus, getLogger),
|
||||
};
|
||||
};
|
||||
|
@ -1,34 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
STRATEGY_CREATED,
|
||||
STRATEGY_DELETED,
|
||||
STRATEGY_UPDATED,
|
||||
STRATEGY_IMPORT,
|
||||
DROP_STRATEGIES,
|
||||
} = require('../event-type');
|
||||
const NotFoundError = require('../error/notfound-error');
|
||||
|
||||
const STRATEGY_COLUMNS = ['name', 'description', 'parameters', 'built_in'];
|
||||
const TABLE = 'strategies';
|
||||
|
||||
class StrategyStore {
|
||||
constructor(db, eventStore, getLogger) {
|
||||
constructor(db, getLogger) {
|
||||
this.db = db;
|
||||
this.logger = getLogger('strategy-store.js');
|
||||
eventStore.on(STRATEGY_CREATED, event =>
|
||||
this._createStrategy(event.data),
|
||||
);
|
||||
eventStore.on(STRATEGY_UPDATED, event =>
|
||||
this._updateStrategy(event.data),
|
||||
);
|
||||
eventStore.on(STRATEGY_DELETED, event =>
|
||||
this._deleteStrategy(event.data),
|
||||
);
|
||||
eventStore.on(STRATEGY_IMPORT, event =>
|
||||
this._importStrategy(event.data),
|
||||
);
|
||||
eventStore.on(DROP_STRATEGIES, () => this._dropStrategies());
|
||||
}
|
||||
|
||||
async getStrategies() {
|
||||
@ -88,7 +68,7 @@ class StrategyStore {
|
||||
};
|
||||
}
|
||||
|
||||
async _createStrategy(data) {
|
||||
async createStrategy(data) {
|
||||
this.db(TABLE)
|
||||
.insert(this.eventDataToRow(data))
|
||||
.catch(err =>
|
||||
@ -96,7 +76,7 @@ class StrategyStore {
|
||||
);
|
||||
}
|
||||
|
||||
async _updateStrategy(data) {
|
||||
async updateStrategy(data) {
|
||||
this.db(TABLE)
|
||||
.where({ name: data.name })
|
||||
.update(this.eventDataToRow(data))
|
||||
@ -105,7 +85,7 @@ class StrategyStore {
|
||||
);
|
||||
}
|
||||
|
||||
async _deleteStrategy({ name }) {
|
||||
async deleteStrategy({ name }) {
|
||||
return this.db(TABLE)
|
||||
.where({ name })
|
||||
.del()
|
||||
@ -114,7 +94,7 @@ class StrategyStore {
|
||||
});
|
||||
}
|
||||
|
||||
async _importStrategy(data) {
|
||||
async importStrategy(data) {
|
||||
const rowData = this.eventDataToRow(data);
|
||||
return this.db(TABLE)
|
||||
.where({ name: rowData.name, built_in: 0 }) // eslint-disable-line
|
||||
@ -127,7 +107,7 @@ class StrategyStore {
|
||||
);
|
||||
}
|
||||
|
||||
async _dropStrategies() {
|
||||
async dropStrategies() {
|
||||
return this.db(TABLE)
|
||||
.where({ built_in: 0 }) // eslint-disable-line
|
||||
.delete()
|
||||
|
73
lib/db/tag-store.js
Normal file
73
lib/db/tag-store.js
Normal file
@ -0,0 +1,73 @@
|
||||
'use strict';
|
||||
|
||||
const metricsHelper = require('../metrics-helper');
|
||||
const { DB_TIME } = require('../events');
|
||||
const NotFoundError = require('../error/notfound-error');
|
||||
|
||||
const COLUMNS = ['type', 'value'];
|
||||
const TABLE = 'tags';
|
||||
class TagStore {
|
||||
constructor(db, eventBus, getLogger) {
|
||||
this.db = db;
|
||||
this.logger = getLogger('tag-store.js');
|
||||
this.timer = action =>
|
||||
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
||||
store: 'tag',
|
||||
action,
|
||||
});
|
||||
}
|
||||
|
||||
async getTagsByType(type) {
|
||||
const stopTimer = this.timer('getTagByType');
|
||||
const rows = await this.db
|
||||
.select(COLUMNS)
|
||||
.from(TABLE)
|
||||
.where({ type });
|
||||
stopTimer();
|
||||
return rows.map(this.rowToTag);
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
const stopTimer = this.timer('getAll');
|
||||
const rows = await this.db.select(COLUMNS).from(TABLE);
|
||||
stopTimer();
|
||||
return rows.map(this.rowToTag);
|
||||
}
|
||||
|
||||
async getTag(type, value) {
|
||||
const stopTimer = this.timer('getTag');
|
||||
const tag = await this.db
|
||||
.first(COLUMNS)
|
||||
.from(TABLE)
|
||||
.where({ type, value });
|
||||
stopTimer();
|
||||
if (!tag) {
|
||||
throw new NotFoundError(
|
||||
`No tag with type: [${type}] and value [${value}]`,
|
||||
);
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
async createTag(tag) {
|
||||
const stopTimer = this.timer('createTag');
|
||||
await this.db(TABLE).insert(tag);
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
async deleteTag(tag) {
|
||||
const stopTimer = this.timer('deleteTag');
|
||||
await this.db(TABLE)
|
||||
.where(tag)
|
||||
.del();
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
rowToTag(row) {
|
||||
return {
|
||||
type: row.type,
|
||||
value: row.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
module.exports = TagStore;
|
@ -1,12 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const metricsHelper = require('../metrics-helper');
|
||||
const {
|
||||
CREATE_TAG_TYPE,
|
||||
DELETE_TAG_TYPE,
|
||||
UPDATE_TAG_TYPE,
|
||||
} = require('../command-type');
|
||||
const { TAG_TYPE_CREATED, TAG_TYPE_DELETED } = require('../event-type');
|
||||
const { DB_TIME } = require('../events');
|
||||
const NotFoundError = require('../error/notfound-error');
|
||||
|
||||
@ -14,25 +8,14 @@ const COLUMNS = ['name', 'description', 'icon'];
|
||||
const TABLE = 'tag_types';
|
||||
|
||||
class TagTypeStore {
|
||||
constructor(db, eventStore, eventBus, getLogger) {
|
||||
constructor(db, eventBus, getLogger) {
|
||||
this.db = db;
|
||||
this.eventStore = eventStore;
|
||||
this.logger = getLogger('tag-type-store.js');
|
||||
this.timer = action =>
|
||||
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
||||
store: 'tag-type',
|
||||
action,
|
||||
});
|
||||
|
||||
eventStore.on(CREATE_TAG_TYPE, event =>
|
||||
this._createTagType(event.data),
|
||||
);
|
||||
eventStore.on(DELETE_TAG_TYPE, event =>
|
||||
this._deleteTagType(event.data),
|
||||
);
|
||||
eventStore.on(UPDATE_TAG_TYPE, event =>
|
||||
this._updateTagType(event.data),
|
||||
);
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
@ -58,53 +41,35 @@ class TagTypeStore {
|
||||
});
|
||||
}
|
||||
|
||||
async _createTagType(event) {
|
||||
async exists(name) {
|
||||
const stopTimer = this.timer('exists');
|
||||
const row = await this.db
|
||||
.first(COLUMNS)
|
||||
.from(TABLE)
|
||||
.where({ name });
|
||||
stopTimer();
|
||||
return row;
|
||||
}
|
||||
|
||||
async createTagType(newTagType) {
|
||||
const stopTimer = this.timer('createTagType');
|
||||
try {
|
||||
const data = this.eventDataToRow(event);
|
||||
await this.db(TABLE).insert(data);
|
||||
await this.eventStore.store({
|
||||
type: TAG_TYPE_CREATED,
|
||||
createdBy: event.createdBy || 'unleash-system',
|
||||
data,
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error('Could not insert tag type, error: ', err);
|
||||
}
|
||||
await this.db(TABLE).insert(newTagType);
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
async _updateTagType(event) {
|
||||
const stopTimer = this.timer('updateTagType');
|
||||
try {
|
||||
const { description, icon } = this.eventDataToRow(event);
|
||||
await this.db(TABLE)
|
||||
.where({ name: event.name })
|
||||
.update({ description, icon });
|
||||
stopTimer();
|
||||
} catch (err) {
|
||||
this.logger.error('Could not update tag type, error: ', err);
|
||||
stopTimer();
|
||||
}
|
||||
async deleteTagType(name) {
|
||||
const stopTimer = this.timer('deleteTagType');
|
||||
await this.db(TABLE)
|
||||
.where({ name })
|
||||
.del();
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
async _deleteTagType(event) {
|
||||
const stopTimer = this.timer('deleteTagType');
|
||||
try {
|
||||
const data = this.eventDataToRow(event);
|
||||
await this.db(TABLE)
|
||||
.where({
|
||||
name: data.name,
|
||||
})
|
||||
.del();
|
||||
await this.eventStore.store({
|
||||
type: TAG_TYPE_DELETED,
|
||||
createdBy: event.createdBy || 'unleash-system',
|
||||
data,
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error('Could not delete tag, error: ', err);
|
||||
}
|
||||
async updateTagType({ name, description, icon }) {
|
||||
const stopTimer = this.timer('updateTagType');
|
||||
await this.db(TABLE)
|
||||
.where({ name })
|
||||
.update({ description, icon });
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
@ -115,14 +80,6 @@ class TagTypeStore {
|
||||
icon: row.icon,
|
||||
};
|
||||
}
|
||||
|
||||
eventDataToRow(event) {
|
||||
return {
|
||||
name: event.name.toLowerCase(),
|
||||
description: event.description,
|
||||
icon: event.icon,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TagTypeStore;
|
||||
|
@ -27,6 +27,7 @@ const {
|
||||
TAG_DELETED,
|
||||
TAG_TYPE_CREATED,
|
||||
TAG_TYPE_DELETED,
|
||||
APPLICATION_CREATED,
|
||||
} = require('./event-type');
|
||||
|
||||
const strategyTypes = [
|
||||
@ -79,6 +80,9 @@ function baseTypeFor(event) {
|
||||
if (tagTypeTypes.indexOf(event.type) !== -1) {
|
||||
return 'tag-type';
|
||||
}
|
||||
if (event.type === APPLICATION_CREATED) {
|
||||
return 'application';
|
||||
}
|
||||
throw new Error(`unknown event type: ${JSON.stringify(event)}`);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
APPLICATION_CREATED: 'application-created',
|
||||
FEATURE_CREATED: 'feature-created',
|
||||
FEATURE_UPDATED: 'feature-updated',
|
||||
FEATURE_ARCHIVED: 'feature-archived',
|
||||
@ -24,4 +25,5 @@ module.exports = {
|
||||
TAG_DELETED: 'tag-deleted',
|
||||
TAG_TYPE_CREATED: 'tag-type-created',
|
||||
TAG_TYPE_DELETED: 'tag-type-deleted',
|
||||
TAG_TYPE_UPDATED: 'tag-type-updated',
|
||||
};
|
||||
|
@ -2,23 +2,21 @@
|
||||
|
||||
const Controller = require('../controller');
|
||||
|
||||
const { FEATURE_REVIVED } = require('../../event-type');
|
||||
const extractUser = require('../../extract-user');
|
||||
const { UPDATE_FEATURE } = require('../../permissions');
|
||||
|
||||
class ArchiveController extends Controller {
|
||||
constructor(config) {
|
||||
constructor(config, { featureToggleService }) {
|
||||
super(config);
|
||||
this.logger = config.getLogger('/admin-api/archive.js');
|
||||
this.featureToggleStore = config.stores.featureToggleStore;
|
||||
this.eventStore = config.stores.eventStore;
|
||||
this.featureService = featureToggleService;
|
||||
|
||||
this.get('/features', this.getArchivedFeatures);
|
||||
this.post('/revive/:name', this.reviveFeatureToggle, UPDATE_FEATURE);
|
||||
}
|
||||
|
||||
async getArchivedFeatures(req, res) {
|
||||
const features = await this.featureToggleStore.getArchivedFeatures();
|
||||
const features = await this.featureService.getArchivedFeatures();
|
||||
res.json({ features });
|
||||
}
|
||||
|
||||
@ -26,11 +24,7 @@ class ArchiveController extends Controller {
|
||||
const userName = extractUser(req);
|
||||
|
||||
try {
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_REVIVED,
|
||||
createdBy: userName,
|
||||
data: { name: req.params.name },
|
||||
});
|
||||
await this.featureService.reviveToggle(req.params.name, userName);
|
||||
return res.status(200).end();
|
||||
} catch (error) {
|
||||
this.logger.error('Server failed executing request', error);
|
||||
|
@ -7,6 +7,7 @@ const store = require('../../../test/fixtures/store');
|
||||
const permissions = require('../../../test/fixtures/permissions');
|
||||
const getLogger = require('../../../test/fixtures/no-logger');
|
||||
const getApp = require('../../app');
|
||||
const { createServices } = require('../../services');
|
||||
const { UPDATE_FEATURE } = require('../../permissions');
|
||||
|
||||
const eventBus = new EventEmitter();
|
||||
@ -15,20 +16,23 @@ function getSetup() {
|
||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||
const stores = store.createStores();
|
||||
const perms = permissions();
|
||||
const app = getApp({
|
||||
const config = {
|
||||
baseUriPath: base,
|
||||
stores,
|
||||
eventBus,
|
||||
extendedPermissions: true,
|
||||
preRouterHook: perms.hook,
|
||||
getLogger,
|
||||
});
|
||||
};
|
||||
const services = createServices(stores, config);
|
||||
const app = getApp(config, services);
|
||||
|
||||
return {
|
||||
base,
|
||||
perms,
|
||||
archiveStore: stores.featureToggleStore,
|
||||
eventStore: stores.eventStore,
|
||||
featureToggleService: services.featureToggleService,
|
||||
request: supertest(app),
|
||||
};
|
||||
}
|
||||
@ -81,9 +85,16 @@ test('should revive toggle', t => {
|
||||
test('should create event when reviving toggle', async t => {
|
||||
t.plan(4);
|
||||
const name = 'name1';
|
||||
const { request, base, archiveStore, eventStore, perms } = getSetup();
|
||||
const {
|
||||
request,
|
||||
base,
|
||||
featureToggleService,
|
||||
eventStore,
|
||||
perms,
|
||||
} = getSetup();
|
||||
perms.withPermissions(UPDATE_FEATURE);
|
||||
archiveStore.addArchivedFeature({
|
||||
|
||||
await featureToggleService.addArchivedFeature({
|
||||
name,
|
||||
strategies: [{ name: 'default' }],
|
||||
});
|
||||
|
@ -1,11 +1,5 @@
|
||||
const Controller = require('../controller');
|
||||
|
||||
const {
|
||||
FEATURE_CREATED,
|
||||
FEATURE_UPDATED,
|
||||
FEATURE_ARCHIVED,
|
||||
} = require('../../event-type');
|
||||
const NameExistsError = require('../../error/name-exists-error');
|
||||
const { handleErrors } = require('./util');
|
||||
const extractUser = require('../../extract-user');
|
||||
const {
|
||||
@ -13,17 +7,13 @@ const {
|
||||
DELETE_FEATURE,
|
||||
CREATE_FEATURE,
|
||||
} = require('../../permissions');
|
||||
const { featureShema, nameSchema } = require('./feature-schema');
|
||||
const { tagSchema } = require('./tag-schema');
|
||||
|
||||
const version = 1;
|
||||
|
||||
class FeatureController extends Controller {
|
||||
constructor(config) {
|
||||
constructor(config, { featureToggleService }) {
|
||||
super(config);
|
||||
this.featureToggleStore = config.stores.featureToggleStore;
|
||||
this.featureTagStore = config.stores.featureTagStore;
|
||||
this.eventStore = config.stores.eventStore;
|
||||
this.featureService = featureToggleService;
|
||||
this.logger = config.getLogger('/admin-api/feature.js');
|
||||
|
||||
this.get('/', this.getAllToggles);
|
||||
@ -40,21 +30,21 @@ class FeatureController extends Controller {
|
||||
this.get('/:featureName/tags', this.listTags, UPDATE_FEATURE);
|
||||
this.post('/:featureName/tags', this.addTag, UPDATE_FEATURE);
|
||||
this.delete(
|
||||
'/:featureName/tags/:tagType/:tagValue',
|
||||
'/:featureName/tags/:type/:value',
|
||||
this.removeTag,
|
||||
UPDATE_FEATURE,
|
||||
);
|
||||
}
|
||||
|
||||
async getAllToggles(req, res) {
|
||||
const features = await this.featureToggleStore.getFeatures();
|
||||
const features = await this.featureService.getFeatures();
|
||||
res.json({ version, features });
|
||||
}
|
||||
|
||||
async getToggle(req, res) {
|
||||
try {
|
||||
const name = req.params.featureName;
|
||||
const feature = await this.featureToggleStore.getFeature(name);
|
||||
const feature = await this.featureService.getFeature(name);
|
||||
res.json(feature).end();
|
||||
} catch (err) {
|
||||
res.status(404).json({ error: 'Could not find feature' });
|
||||
@ -62,8 +52,7 @@ class FeatureController extends Controller {
|
||||
}
|
||||
|
||||
async listTags(req, res) {
|
||||
const name = req.params.featureName;
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(name);
|
||||
const tags = await this.featureService.listTags(req.params.featureName);
|
||||
res.json({ version, tags });
|
||||
}
|
||||
|
||||
@ -71,14 +60,11 @@ class FeatureController extends Controller {
|
||||
const { featureName } = req.params;
|
||||
const userName = extractUser(req);
|
||||
try {
|
||||
await nameSchema.validateAsync({ name: featureName });
|
||||
const { value, type } = await tagSchema.validateAsync(req.body);
|
||||
const tag = await this.featureTagStore.tagFeature({
|
||||
value,
|
||||
type,
|
||||
const tag = await this.featureService.addTag(
|
||||
featureName,
|
||||
createdBy: userName,
|
||||
});
|
||||
req.body,
|
||||
userName,
|
||||
);
|
||||
res.status(201).json(tag);
|
||||
} catch (err) {
|
||||
handleErrors(res, this.logger, err);
|
||||
@ -86,14 +72,13 @@ class FeatureController extends Controller {
|
||||
}
|
||||
|
||||
async removeTag(req, res) {
|
||||
const { featureName, tagType, tagValue } = req.params;
|
||||
const { featureName, type, value } = req.params;
|
||||
const userName = extractUser(req);
|
||||
await this.featureTagStore.untagFeature({
|
||||
await this.featureService.removeTag(
|
||||
featureName,
|
||||
tagType,
|
||||
tagValue,
|
||||
createdBy: userName,
|
||||
});
|
||||
{ type, value },
|
||||
userName,
|
||||
);
|
||||
res.status(200).end();
|
||||
}
|
||||
|
||||
@ -101,44 +86,18 @@ class FeatureController extends Controller {
|
||||
const { name } = req.body;
|
||||
|
||||
try {
|
||||
await nameSchema.validateAsync({ name });
|
||||
await this.validateUniqueName(name);
|
||||
await this.featureService.validateName({ name });
|
||||
res.status(200).end();
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: cleanup this validation
|
||||
async validateUniqueName(name) {
|
||||
let msg;
|
||||
try {
|
||||
const definition = await this.featureToggleStore.hasFeature(name);
|
||||
msg = definition.archived
|
||||
? 'An archived toggle with that name already exist'
|
||||
: 'A toggle with that name already exist';
|
||||
} catch (error) {
|
||||
// No conflict, everything ok!
|
||||
return;
|
||||
}
|
||||
|
||||
// Interntional throw here!
|
||||
throw new NameExistsError(msg);
|
||||
}
|
||||
|
||||
async createToggle(req, res) {
|
||||
const toggleName = req.body.name;
|
||||
const userName = extractUser(req);
|
||||
|
||||
try {
|
||||
await this.validateUniqueName(toggleName);
|
||||
const value = await featureShema.validateAsync(req.body);
|
||||
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_CREATED,
|
||||
createdBy: userName,
|
||||
data: value,
|
||||
});
|
||||
await this.featureService.createFeatureToggle(req.body, userName);
|
||||
res.status(201).end();
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
@ -153,45 +112,39 @@ class FeatureController extends Controller {
|
||||
updatedFeature.name = featureName;
|
||||
|
||||
try {
|
||||
await this.featureToggleStore.getFeature(featureName);
|
||||
const value = await featureShema.validateAsync(updatedFeature);
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_UPDATED,
|
||||
createdBy: userName,
|
||||
data: value,
|
||||
});
|
||||
await this.featureService.updateToggle(updatedFeature, userName);
|
||||
res.status(200).end();
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Kept to keep backward compatability
|
||||
// Kept to keep backward compatibility
|
||||
async toggle(req, res) {
|
||||
const userName = extractUser(req);
|
||||
try {
|
||||
const name = req.params.featureName;
|
||||
const feature = await this.featureToggleStore.getFeature(name);
|
||||
const enabled = !feature.enabled;
|
||||
this._updateField('enabled', enabled, req, res);
|
||||
const feature = await this.featureService.toggle(name, userName);
|
||||
res.status(200).json(feature);
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
}
|
||||
}
|
||||
|
||||
async toggleOn(req, res) {
|
||||
this._updateField('enabled', true, req, res);
|
||||
await this._updateField('enabled', true, req, res);
|
||||
}
|
||||
|
||||
async toggleOff(req, res) {
|
||||
this._updateField('enabled', false, req, res);
|
||||
await this._updateField('enabled', false, req, res);
|
||||
}
|
||||
|
||||
async staleOn(req, res) {
|
||||
this._updateField('stale', true, req, res);
|
||||
await this._updateField('stale', true, req, res);
|
||||
}
|
||||
|
||||
async staleOff(req, res) {
|
||||
this._updateField('stale', false, req, res);
|
||||
await this._updateField('stale', false, req, res);
|
||||
}
|
||||
|
||||
async _updateField(field, value, req, res) {
|
||||
@ -199,16 +152,12 @@ class FeatureController extends Controller {
|
||||
const userName = extractUser(req);
|
||||
|
||||
try {
|
||||
const feature = await this.featureToggleStore.getFeature(
|
||||
const feature = await this.featureService.updateField(
|
||||
featureName,
|
||||
field,
|
||||
value,
|
||||
userName,
|
||||
);
|
||||
feature[field] = value;
|
||||
const validFeature = await featureShema.validateAsync(feature);
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_UPDATED,
|
||||
createdBy: userName,
|
||||
data: validFeature,
|
||||
});
|
||||
res.json(feature).end();
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
@ -220,14 +169,7 @@ class FeatureController extends Controller {
|
||||
const userName = extractUser(req);
|
||||
|
||||
try {
|
||||
await this.featureToggleStore.getFeature(featureName);
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_ARCHIVED,
|
||||
createdBy: userName,
|
||||
data: {
|
||||
name: featureName,
|
||||
},
|
||||
});
|
||||
await this.featureService.archiveToggle(featureName, userName);
|
||||
res.status(200).end();
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
|
@ -4,6 +4,7 @@ const test = require('ava');
|
||||
const supertest = require('supertest');
|
||||
const { EventEmitter } = require('events');
|
||||
const store = require('../../../test/fixtures/store');
|
||||
const { createServices } = require('../../services');
|
||||
const permissions = require('../../../test/fixtures/permissions');
|
||||
const getLogger = require('../../../test/fixtures/no-logger');
|
||||
const getApp = require('../../app');
|
||||
@ -15,14 +16,16 @@ function getSetup() {
|
||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||
const stores = store.createStores();
|
||||
const perms = permissions();
|
||||
const app = getApp({
|
||||
const config = {
|
||||
baseUriPath: base,
|
||||
stores,
|
||||
eventBus,
|
||||
extendedPermissions: true,
|
||||
preRouterHook: perms.hook,
|
||||
getLogger,
|
||||
});
|
||||
};
|
||||
const services = createServices(stores, config);
|
||||
const app = getApp(config, services);
|
||||
|
||||
return {
|
||||
base,
|
||||
@ -48,7 +51,7 @@ test('should get empty getFeatures via admin', t => {
|
||||
test('should get one getFeature', t => {
|
||||
t.plan(1);
|
||||
const { request, featureToggleStore, base } = getSetup();
|
||||
featureToggleStore.addFeature({
|
||||
featureToggleStore.createFeature({
|
||||
name: 'test_',
|
||||
strategies: [{ name: 'default_' }],
|
||||
});
|
||||
@ -65,7 +68,7 @@ test('should get one getFeature', t => {
|
||||
test('should add version numbers for /features', t => {
|
||||
t.plan(1);
|
||||
const { request, featureToggleStore, base } = getSetup();
|
||||
featureToggleStore.addFeature({
|
||||
featureToggleStore.createFeature({
|
||||
name: 'test2',
|
||||
strategies: [{ name: 'default' }],
|
||||
});
|
||||
@ -123,7 +126,7 @@ test('should be allowed to have variants="null"', t => {
|
||||
test('should not be allowed to reuse active toggle name', t => {
|
||||
t.plan(1);
|
||||
const { request, featureToggleStore, base } = getSetup();
|
||||
featureToggleStore.addFeature({
|
||||
featureToggleStore.createFeature({
|
||||
name: 'ts',
|
||||
strategies: [{ name: 'default' }],
|
||||
});
|
||||
@ -134,9 +137,9 @@ test('should not be allowed to reuse active toggle name', t => {
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(409)
|
||||
.expect(res => {
|
||||
t.true(
|
||||
res.body.details[0].message ===
|
||||
'A toggle with that name already exist',
|
||||
t.is(
|
||||
res.body.details[0].message,
|
||||
'A toggle with that name already exists',
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -155,9 +158,9 @@ test('should not be allowed to reuse archived toggle name', t => {
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(409)
|
||||
.expect(res => {
|
||||
t.true(
|
||||
res.body.details[0].message ===
|
||||
'An archived toggle with that name already exist',
|
||||
t.is(
|
||||
res.body.details[0].message,
|
||||
'An archived toggle with that name already exists',
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -166,7 +169,7 @@ test('should require at least one strategy when updating a feature toggle', t =>
|
||||
t.plan(0);
|
||||
const { request, featureToggleStore, base, perms } = getSetup();
|
||||
perms.withPermissions(UPDATE_FEATURE);
|
||||
featureToggleStore.addFeature({
|
||||
featureToggleStore.createFeature({
|
||||
name: 'ts',
|
||||
strategies: [{ name: 'default' }],
|
||||
});
|
||||
@ -284,7 +287,7 @@ test('should not allow variants with same name when updating feature flag', t =>
|
||||
const { request, featureToggleStore, base, perms } = getSetup();
|
||||
perms.withPermissions(UPDATE_FEATURE);
|
||||
|
||||
featureToggleStore.addFeature({
|
||||
featureToggleStore.createFeature({
|
||||
name: 'ts',
|
||||
strategies: [{ name: 'default' }],
|
||||
});
|
||||
@ -305,7 +308,7 @@ test('should toggle on', t => {
|
||||
const { request, featureToggleStore, base, perms } = getSetup();
|
||||
perms.withPermissions(UPDATE_FEATURE);
|
||||
|
||||
featureToggleStore.addFeature({
|
||||
featureToggleStore.createFeature({
|
||||
name: 'toggle.disabled',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
@ -325,7 +328,7 @@ test('should toggle off', t => {
|
||||
const { request, featureToggleStore, base, perms } = getSetup();
|
||||
perms.withPermissions(UPDATE_FEATURE);
|
||||
|
||||
featureToggleStore.addFeature({
|
||||
featureToggleStore.createFeature({
|
||||
name: 'toggle.enabled',
|
||||
enabled: true,
|
||||
strategies: [{ name: 'default' }],
|
||||
@ -345,7 +348,7 @@ test('should toggle', t => {
|
||||
const { request, featureToggleStore, base, perms } = getSetup();
|
||||
perms.withPermissions(UPDATE_FEATURE);
|
||||
|
||||
featureToggleStore.addFeature({
|
||||
featureToggleStore.createFeature({
|
||||
name: 'toggle.disabled',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
@ -361,10 +364,10 @@ test('should toggle', t => {
|
||||
});
|
||||
|
||||
test('should be able to add tag for feature', t => {
|
||||
t.plan(1);
|
||||
t.plan(0);
|
||||
const { request, featureToggleStore, base, perms } = getSetup();
|
||||
perms.withPermissions(UPDATE_FEATURE);
|
||||
featureToggleStore.addFeature({
|
||||
featureToggleStore.createFeature({
|
||||
name: 'toggle.disabled',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
@ -376,10 +379,7 @@ test('should be able to add tag for feature', t => {
|
||||
type: 'simple',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201)
|
||||
.expect(res => {
|
||||
t.is(res.body.value, 'TeamRed');
|
||||
});
|
||||
.expect(201);
|
||||
});
|
||||
test('should be able to get tags for feature', t => {
|
||||
t.plan(1);
|
||||
@ -392,14 +392,13 @@ test('should be able to get tags for feature', t => {
|
||||
} = getSetup();
|
||||
perms.withPermissions(UPDATE_FEATURE);
|
||||
|
||||
featureToggleStore.addFeature({
|
||||
featureToggleStore.createFeature({
|
||||
name: 'toggle.disabled',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
});
|
||||
|
||||
featureTagStore.tagFeature({
|
||||
featureName: 'toggle.disabled',
|
||||
featureTagStore.tagFeature('toggle.disabled', {
|
||||
value: 'TeamGreen',
|
||||
type: 'simple',
|
||||
});
|
||||
@ -417,7 +416,7 @@ test('Invalid tag for feature should be rejected', t => {
|
||||
const { request, featureToggleStore, base, perms } = getSetup();
|
||||
perms.withPermissions(UPDATE_FEATURE);
|
||||
|
||||
featureToggleStore.addFeature({
|
||||
featureToggleStore.createFeature({
|
||||
name: 'toggle.disabled',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
|
@ -1,23 +1,13 @@
|
||||
const Controller = require('../controller');
|
||||
const schema = require('./metrics-schema');
|
||||
const { handleErrors } = require('./util');
|
||||
const { UPDATE_APPLICATION } = require('../../permissions');
|
||||
|
||||
class MetricsController extends Controller {
|
||||
constructor(config, { clientMetricsService }) {
|
||||
super(config);
|
||||
this.logger = config.getLogger('/admin-api/metrics.js');
|
||||
const {
|
||||
clientInstanceStore,
|
||||
clientApplicationsStore,
|
||||
strategyStore,
|
||||
featureToggleStore,
|
||||
} = config.stores;
|
||||
|
||||
this.metrics = clientMetricsService;
|
||||
this.clientInstanceStore = clientInstanceStore;
|
||||
this.clientApplicationsStore = clientApplicationsStore;
|
||||
this.strategyStore = strategyStore;
|
||||
this.featureToggleStore = featureToggleStore;
|
||||
|
||||
this.get('/seen-toggles', this.getSeenToggles);
|
||||
this.get('/seen-apps', this.getSeenApps);
|
||||
@ -43,22 +33,7 @@ class MetricsController extends Controller {
|
||||
}
|
||||
|
||||
async getSeenApps(req, res) {
|
||||
const seenApps = this.metrics.getSeenAppsPerToggle();
|
||||
const applications = await this.clientApplicationsStore.getApplications();
|
||||
const metaData = applications.reduce((result, entry) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
result[entry.appName] = entry;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
Object.keys(seenApps).forEach(key => {
|
||||
seenApps[key] = seenApps[key].map(entry => {
|
||||
if (metaData[entry.appName]) {
|
||||
return { ...entry, ...metaData[entry.appName] };
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
});
|
||||
const seenApps = this.metrics.getSeenApps();
|
||||
res.json(seenApps);
|
||||
}
|
||||
|
||||
@ -81,94 +56,40 @@ class MetricsController extends Controller {
|
||||
const { appName } = req.params;
|
||||
|
||||
try {
|
||||
await this.clientApplicationsStore.getApplication(appName);
|
||||
} catch (e) {
|
||||
this.logger.warn(e);
|
||||
res.status(409).end();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.clientInstanceStore.deleteForApplication(appName);
|
||||
await this.clientApplicationsStore.deleteApplication(appName);
|
||||
await this.metrics.deleteApplication(appName);
|
||||
res.status(200).end();
|
||||
} catch (e) {
|
||||
this.logger.error(e);
|
||||
res.status(500).end();
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
}
|
||||
|
||||
async createApplication(req, res) {
|
||||
const input = { ...req.body, appName: req.params.appName };
|
||||
const { value: applicationData, error } = schema.validate(input);
|
||||
|
||||
if (error) {
|
||||
this.logger.warn('Invalid application data posted', error);
|
||||
return res.status(400).json(error);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.clientApplicationsStore.upsert(applicationData);
|
||||
return res.status(202).end();
|
||||
await this.metrics.createApplication(input);
|
||||
res.status(202).end();
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
return res.status(500).end();
|
||||
handleErrors(res, this.logger, err);
|
||||
}
|
||||
}
|
||||
|
||||
async getApplications(req, res) {
|
||||
try {
|
||||
const applications = await this.clientApplicationsStore.getApplications(
|
||||
req.query,
|
||||
);
|
||||
const applications = await this.metrics.getApplications(req.query);
|
||||
res.json({ applications });
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
res.status(500).end();
|
||||
handleErrors(res, this.logger, err);
|
||||
}
|
||||
}
|
||||
|
||||
async getApplication(req, res) {
|
||||
const { appName } = req.params;
|
||||
const seenToggles = this.metrics.getSeenTogglesByAppName(appName);
|
||||
|
||||
try {
|
||||
const [
|
||||
application,
|
||||
instances,
|
||||
strategies,
|
||||
features,
|
||||
] = await Promise.all([
|
||||
this.clientApplicationsStore.getApplication(appName),
|
||||
this.clientInstanceStore.getByAppName(appName),
|
||||
this.strategyStore.getStrategies(),
|
||||
this.featureToggleStore.getFeatures(),
|
||||
]);
|
||||
|
||||
const appDetails = {
|
||||
appName: application.appName,
|
||||
createdAt: application.createdAt,
|
||||
description: application.description,
|
||||
url: application.url,
|
||||
color: application.color,
|
||||
icon: application.icon,
|
||||
strategies: application.strategies.map(name => {
|
||||
const found = strategies.find(f => f.name === name);
|
||||
return found || { name, notFound: true };
|
||||
}),
|
||||
instances,
|
||||
seenToggles: seenToggles.map(name => {
|
||||
const found = features.find(f => f.name === name);
|
||||
return found || { name, notFound: true };
|
||||
}),
|
||||
links: {
|
||||
self: `/api/applications/${application.appName}`,
|
||||
},
|
||||
};
|
||||
const appDetails = await this.metrics.getApplication(appName);
|
||||
res.json(appDetails);
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
res.status(500).end();
|
||||
handleErrors(res, this.logger, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ test('should store application details wihtout strategies', t => {
|
||||
.expect(202);
|
||||
});
|
||||
|
||||
test('should not delete unknown application', t => {
|
||||
test('should accept a delete call to unknown application', t => {
|
||||
t.plan(0);
|
||||
const { request, perms } = getSetup();
|
||||
const appName = 'unknown';
|
||||
@ -165,7 +165,7 @@ test('should not delete unknown application', t => {
|
||||
|
||||
return request
|
||||
.delete(`/api/admin/metrics/applications/${appName}`)
|
||||
.expect(409);
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
test('should delete application', t => {
|
||||
|
@ -2,10 +2,7 @@
|
||||
|
||||
const Controller = require('../controller');
|
||||
|
||||
const eventType = require('../../event-type');
|
||||
const NameExistsError = require('../../error/name-exists-error');
|
||||
const extractUser = require('../../extract-user');
|
||||
const strategySchema = require('./strategy-schema');
|
||||
const { handleErrors } = require('./util');
|
||||
const {
|
||||
DELETE_STRATEGY,
|
||||
@ -16,11 +13,10 @@ const {
|
||||
const version = 1;
|
||||
|
||||
class StrategyController extends Controller {
|
||||
constructor(config) {
|
||||
constructor(config, { strategyService }) {
|
||||
super(config);
|
||||
this.logger = config.getLogger('/admin-api/strategy.js');
|
||||
this.strategyStore = config.stores.strategyStore;
|
||||
this.eventStore = config.stores.eventStore;
|
||||
this.strategyService = strategyService;
|
||||
|
||||
this.get('/', this.getAllStratgies);
|
||||
this.get('/:name', this.getStrategy);
|
||||
@ -30,14 +26,14 @@ class StrategyController extends Controller {
|
||||
}
|
||||
|
||||
async getAllStratgies(req, res) {
|
||||
const strategies = await this.strategyStore.getStrategies();
|
||||
const strategies = await this.strategyService.getStrategies();
|
||||
res.json({ version, strategies });
|
||||
}
|
||||
|
||||
async getStrategy(req, res) {
|
||||
try {
|
||||
const { name } = req.params;
|
||||
const strategy = await this.strategyStore.getStrategy(name);
|
||||
const strategy = await this.strategyService.getStrategy(name);
|
||||
res.json(strategy).end();
|
||||
} catch (err) {
|
||||
res.status(404).json({ error: 'Could not find strategy' });
|
||||
@ -46,17 +42,10 @@ class StrategyController extends Controller {
|
||||
|
||||
async removeStrategy(req, res) {
|
||||
const strategyName = req.params.name;
|
||||
const userName = extractUser(req);
|
||||
|
||||
try {
|
||||
const strategy = await this.strategyStore.getStrategy(strategyName);
|
||||
await this._validateEditable(strategy);
|
||||
await this.eventStore.store({
|
||||
type: eventType.STRATEGY_DELETED,
|
||||
createdBy: extractUser(req),
|
||||
data: {
|
||||
name: strategyName,
|
||||
},
|
||||
});
|
||||
await this.strategyService.removeStrategy(strategyName, userName);
|
||||
res.status(200).end();
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
@ -64,14 +53,9 @@ class StrategyController extends Controller {
|
||||
}
|
||||
|
||||
async createStrategy(req, res) {
|
||||
const userName = extractUser(req);
|
||||
try {
|
||||
const value = await strategySchema.validateAsync(req.body);
|
||||
await this._validateStrategyName(value);
|
||||
await this.eventStore.store({
|
||||
type: eventType.STRATEGY_CREATED,
|
||||
createdBy: extractUser(req),
|
||||
data: value,
|
||||
});
|
||||
await this.strategyService.createStrategy(req.body, userName);
|
||||
res.status(201).end();
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
@ -79,46 +63,14 @@ class StrategyController extends Controller {
|
||||
}
|
||||
|
||||
async updateStrategy(req, res) {
|
||||
const input = req.body;
|
||||
|
||||
const userName = extractUser(req);
|
||||
try {
|
||||
const value = await strategySchema.validateAsync(input);
|
||||
const strategy = await this.strategyStore.getStrategy(input.name);
|
||||
await this._validateEditable(strategy);
|
||||
|
||||
await this.eventStore.store({
|
||||
type: eventType.STRATEGY_UPDATED,
|
||||
createdBy: extractUser(req),
|
||||
data: value,
|
||||
});
|
||||
await this.strategyService.updateStrategy(req.body, userName);
|
||||
res.status(200).end();
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
}
|
||||
}
|
||||
|
||||
// This check belongs in the store.
|
||||
async _validateStrategyName(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.strategyStore
|
||||
.getStrategy(data.name)
|
||||
.then(() =>
|
||||
reject(
|
||||
new NameExistsError(
|
||||
`Strategy with name ${data.name} already exist!`,
|
||||
),
|
||||
),
|
||||
)
|
||||
.catch(() => resolve(data));
|
||||
});
|
||||
}
|
||||
|
||||
// This check belongs in the store.
|
||||
_validateEditable(strategy) {
|
||||
if (strategy.editable === false) {
|
||||
throw new Error(`Cannot edit strategy ${strategy.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StrategyController;
|
||||
|
@ -7,6 +7,7 @@ const store = require('../../../test/fixtures/store');
|
||||
const permissions = require('../../../test/fixtures/permissions');
|
||||
const getLogger = require('../../../test/fixtures/no-logger');
|
||||
const getApp = require('../../app');
|
||||
const { createServices } = require('../../services');
|
||||
const {
|
||||
DELETE_STRATEGY,
|
||||
CREATE_STRATEGY,
|
||||
@ -19,14 +20,16 @@ function getSetup() {
|
||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||
const perms = permissions();
|
||||
const stores = store.createStores();
|
||||
const app = getApp({
|
||||
const config = {
|
||||
baseUriPath: base,
|
||||
stores,
|
||||
eventBus,
|
||||
getLogger,
|
||||
extendedPermissions: true,
|
||||
preRouterHook: perms.hook,
|
||||
});
|
||||
};
|
||||
const services = createServices(stores, config);
|
||||
const app = getApp(config, services);
|
||||
|
||||
return {
|
||||
base,
|
||||
@ -99,7 +102,7 @@ test('not be possible to override name', t => {
|
||||
t.plan(0);
|
||||
const { request, base, strategyStore, perms } = getSetup();
|
||||
perms.withPermissions(CREATE_STRATEGY);
|
||||
strategyStore.addStrategy({ name: 'Testing', parameters: [] });
|
||||
strategyStore.createStrategy({ name: 'Testing', parameters: [] });
|
||||
|
||||
return request
|
||||
.post(`${base}/api/admin/strategies`)
|
||||
@ -112,7 +115,7 @@ test('update strategy', t => {
|
||||
const name = 'AnotherStrat';
|
||||
const { request, base, strategyStore, perms } = getSetup();
|
||||
perms.withPermissions(UPDATE_STRATEGY);
|
||||
strategyStore.addStrategy({ name, parameters: [] });
|
||||
strategyStore.createStrategy({ name, parameters: [] });
|
||||
|
||||
return request
|
||||
.put(`${base}/api/admin/strategies/${name}`)
|
||||
@ -137,7 +140,7 @@ test('validate format when updating strategy', t => {
|
||||
const name = 'AnotherStrat';
|
||||
const { request, base, strategyStore, perms } = getSetup();
|
||||
perms.withPermissions(UPDATE_STRATEGY);
|
||||
strategyStore.addStrategy({ name, parameters: [] });
|
||||
strategyStore.createStrategy({ name, parameters: [] });
|
||||
|
||||
return request
|
||||
.put(`${base}/api/admin/strategies/${name}`)
|
||||
@ -173,7 +176,7 @@ test('editable=true will allow delete request', t => {
|
||||
const name = 'deleteStrat';
|
||||
const { request, base, strategyStore, perms } = getSetup();
|
||||
perms.withPermissions(DELETE_STRATEGY);
|
||||
strategyStore.addStrategy({ name, parameters: [] });
|
||||
strategyStore.createStrategy({ name, parameters: [] });
|
||||
|
||||
return request
|
||||
.delete(`${base}/api/admin/strategies/${name}`)
|
||||
@ -186,7 +189,7 @@ test('editable=true will allow edit request', t => {
|
||||
const name = 'editStrat';
|
||||
const { request, base, strategyStore, perms } = getSetup();
|
||||
perms.withPermissions(UPDATE_STRATEGY);
|
||||
strategyStore.addStrategy({ name, parameters: [] });
|
||||
strategyStore.createStrategy({ name, parameters: [] });
|
||||
|
||||
return request
|
||||
.put(`${base}/api/admin/strategies/${name}`)
|
||||
|
@ -2,26 +2,17 @@
|
||||
|
||||
const Controller = require('../controller');
|
||||
|
||||
const { tagTypeSchema } = require('./tag-type-schema');
|
||||
const {
|
||||
UPDATE_TAG_TYPE,
|
||||
CREATE_TAG_TYPE,
|
||||
DELETE_TAG_TYPE,
|
||||
} = require('../../command-type');
|
||||
const { UPDATE_FEATURE } = require('../../permissions');
|
||||
const { handleErrors } = require('./util');
|
||||
const extractUsername = require('../../extract-user');
|
||||
const NameExistsError = require('../../error/name-exists-error');
|
||||
|
||||
const version = 1;
|
||||
|
||||
class TagTypeController extends Controller {
|
||||
constructor(config) {
|
||||
constructor(config, { tagTypeService }) {
|
||||
super(config);
|
||||
this.tagTypeStore = config.stores.tagTypeStore;
|
||||
this.eventStore = config.stores.eventStore;
|
||||
this.logger = config.getLogger('/admin-api/tag-type.js');
|
||||
|
||||
this.tagTypeService = tagTypeService;
|
||||
this.get('/', this.getTagTypes);
|
||||
this.post('/', this.createTagType, UPDATE_FEATURE);
|
||||
this.post('/validate', this.validate);
|
||||
@ -31,30 +22,14 @@ class TagTypeController extends Controller {
|
||||
}
|
||||
|
||||
async getTagTypes(req, res) {
|
||||
const tagTypes = await this.tagTypeStore.getAll();
|
||||
const tagTypes = await this.tagTypeService.getAll();
|
||||
res.json({ version, tagTypes });
|
||||
}
|
||||
|
||||
async validateUniqueName(name) {
|
||||
let msg;
|
||||
try {
|
||||
await this.tagTypeStore.getTagType(name);
|
||||
msg = `A Tag type with name: [${name}] already exist`;
|
||||
} catch (error) {
|
||||
// No conflict, everything ok!
|
||||
return;
|
||||
}
|
||||
|
||||
// Intentional throw here!
|
||||
throw new NameExistsError(msg);
|
||||
}
|
||||
|
||||
async validate(req, res) {
|
||||
const { name, description, icon } = req.body;
|
||||
try {
|
||||
await tagTypeSchema.validateAsync({ name, description, icon });
|
||||
await this.validateUniqueName(name);
|
||||
res.status(200).json({ valid: true });
|
||||
await this.tagTypeService.validate(req.body);
|
||||
res.status(200).json({ valid: true, tagType: req.body });
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
}
|
||||
@ -63,15 +38,11 @@ class TagTypeController extends Controller {
|
||||
async createTagType(req, res) {
|
||||
const userName = extractUsername(req);
|
||||
try {
|
||||
const data = await tagTypeSchema.validateAsync(req.body);
|
||||
data.name = data.name.toLowerCase();
|
||||
await this.validateUniqueName(data.name);
|
||||
await this.eventStore.store({
|
||||
type: CREATE_TAG_TYPE,
|
||||
createdBy: userName,
|
||||
data,
|
||||
});
|
||||
res.status(201).json(data);
|
||||
const tagType = await this.tagTypeService.createTagType(
|
||||
req.body,
|
||||
userName,
|
||||
);
|
||||
res.status(201).json(tagType);
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
}
|
||||
@ -82,16 +53,10 @@ class TagTypeController extends Controller {
|
||||
const { name } = req.params;
|
||||
const userName = extractUsername(req);
|
||||
try {
|
||||
const data = await tagTypeSchema.validateAsync({
|
||||
description,
|
||||
icon,
|
||||
name,
|
||||
});
|
||||
await this.eventStore.store({
|
||||
type: UPDATE_TAG_TYPE,
|
||||
createdBy: userName,
|
||||
data,
|
||||
});
|
||||
await this.tagTypeService.updateTagType(
|
||||
{ name, description, icon },
|
||||
userName,
|
||||
);
|
||||
res.status(200).end();
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
@ -101,7 +66,7 @@ class TagTypeController extends Controller {
|
||||
async getTagType(req, res) {
|
||||
const { name } = req.params;
|
||||
try {
|
||||
const tagType = await this.tagTypeStore.getTagType(name);
|
||||
const tagType = await this.tagTypeService.getTagType(name);
|
||||
res.json({ version, tagType });
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
@ -112,11 +77,7 @@ class TagTypeController extends Controller {
|
||||
const { name } = req.params;
|
||||
const userName = extractUsername(req);
|
||||
try {
|
||||
await this.eventStore.store({
|
||||
type: DELETE_TAG_TYPE,
|
||||
createdBy: userName,
|
||||
data: { name },
|
||||
});
|
||||
await this.tagTypeService.deleteTagType(name, userName);
|
||||
res.status(200).end();
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
const Controller = require('../controller');
|
||||
|
||||
const { tagSchema } = require('./tag-schema');
|
||||
const { CREATE_TAG, DELETE_TAG } = require('../../command-type');
|
||||
const { UPDATE_FEATURE } = require('../../permissions');
|
||||
const { handleErrors } = require('./util');
|
||||
const extractUsername = require('../../extract-user');
|
||||
@ -11,36 +9,32 @@ const extractUsername = require('../../extract-user');
|
||||
const version = 1;
|
||||
|
||||
class TagController extends Controller {
|
||||
constructor(config) {
|
||||
constructor(config, { tagService }) {
|
||||
super(config);
|
||||
this.featureTagStore = config.stores.featureTagStore;
|
||||
this.eventStore = config.stores.eventStore;
|
||||
this.tagService = tagService;
|
||||
this.logger = config.getLogger('/admin-api/tag.js');
|
||||
|
||||
this.get('/', this.getTags);
|
||||
this.post('/', this.createTag, UPDATE_FEATURE);
|
||||
this.get('/:type', this.getTagsByType);
|
||||
this.get('/:type/:value', this.getTagByTypeAndValue);
|
||||
this.get('/:type/:value', this.getTag);
|
||||
this.delete('/:type/:value', this.deleteTag, UPDATE_FEATURE);
|
||||
}
|
||||
|
||||
async getTags(req, res) {
|
||||
const tags = await this.featureTagStore.getTags();
|
||||
const tags = await this.tagService.getTags();
|
||||
res.json({ version, tags });
|
||||
}
|
||||
|
||||
async getTagsByType(req, res) {
|
||||
const tags = await this.featureTagStore.getAllOfType(req.params.type);
|
||||
const tags = await this.tagService.getTagsByType(req.params.type);
|
||||
res.json({ version, tags });
|
||||
}
|
||||
|
||||
async getTagByTypeAndValue(req, res) {
|
||||
async getTag(req, res) {
|
||||
const { type, value } = req.params;
|
||||
try {
|
||||
const tag = await this.featureTagStore.getTagByTypeAndValue(
|
||||
type,
|
||||
value,
|
||||
);
|
||||
const tag = await this.tagService.getTag({ type, value });
|
||||
res.json({ version, tag });
|
||||
} catch (err) {
|
||||
handleErrors(res, this.logger, err);
|
||||
@ -50,12 +44,7 @@ class TagController extends Controller {
|
||||
async createTag(req, res) {
|
||||
const userName = extractUsername(req);
|
||||
try {
|
||||
const data = await tagSchema.validateAsync(req.body);
|
||||
await this.eventStore.store({
|
||||
type: CREATE_TAG,
|
||||
createdBy: userName,
|
||||
data,
|
||||
});
|
||||
await this.tagService.createTag(req.body, userName);
|
||||
res.status(201).end();
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
@ -65,16 +54,8 @@ class TagController extends Controller {
|
||||
async deleteTag(req, res) {
|
||||
const { type, value } = req.params;
|
||||
const userName = extractUsername(req);
|
||||
|
||||
try {
|
||||
await this.eventStore.store({
|
||||
type: DELETE_TAG,
|
||||
createdBy: userName || 'unleash-system',
|
||||
data: {
|
||||
type,
|
||||
value,
|
||||
},
|
||||
});
|
||||
await this.tagService.deleteTag({ type, value }, userName);
|
||||
res.status(200).end();
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
|
@ -7,6 +7,8 @@ const store = require('../../../test/fixtures/store');
|
||||
const permissions = require('../../../test/fixtures/permissions');
|
||||
const getLogger = require('../../../test/fixtures/no-logger');
|
||||
const getApp = require('../../app');
|
||||
const { createServices } = require('../../services');
|
||||
const { UPDATE_FEATURE } = require('../../permissions');
|
||||
|
||||
const eventBus = new EventEmitter();
|
||||
|
||||
@ -14,19 +16,21 @@ function getSetup() {
|
||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||
const stores = store.createStores();
|
||||
const perms = permissions();
|
||||
const app = getApp({
|
||||
const config = {
|
||||
baseUriPath: base,
|
||||
stores,
|
||||
eventBus,
|
||||
extendedPermissions: true,
|
||||
preRouterHook: perms.hook,
|
||||
getLogger,
|
||||
});
|
||||
};
|
||||
const services = createServices(stores, config);
|
||||
const app = getApp(config, services);
|
||||
|
||||
return {
|
||||
base,
|
||||
perms,
|
||||
featureTagStore: stores.featureTagStore,
|
||||
tagStore: stores.tagStore,
|
||||
request: supertest(app),
|
||||
};
|
||||
}
|
||||
@ -45,8 +49,8 @@ test('should get empty getTags via admin', t => {
|
||||
|
||||
test('should get all tags added', t => {
|
||||
t.plan(1);
|
||||
const { request, featureTagStore, base } = getSetup();
|
||||
featureTagStore.addTag({
|
||||
const { request, tagStore, base } = getSetup();
|
||||
tagStore.createTag({
|
||||
type: 'simple',
|
||||
value: 'TeamGreen',
|
||||
});
|
||||
@ -62,12 +66,8 @@ test('should get all tags added', t => {
|
||||
|
||||
test('should be able to get single tag by type and value', t => {
|
||||
t.plan(1);
|
||||
const { request, featureTagStore, base } = getSetup();
|
||||
featureTagStore.addTag({
|
||||
id: 1,
|
||||
type: 'simple',
|
||||
value: 'TeamRed',
|
||||
});
|
||||
const { request, base, tagStore } = getSetup();
|
||||
tagStore.createTag({ value: 'TeamRed', type: 'simple' });
|
||||
return request
|
||||
.get(`${base}/api/admin/tags/simple/TeamRed`)
|
||||
.expect('Content-Type', /json/)
|
||||
@ -77,17 +77,6 @@ test('should be able to get single tag by type and value', t => {
|
||||
});
|
||||
});
|
||||
|
||||
test('trying to get non-existing tag should not be found', t => {
|
||||
const { request, featureTagStore, base } = getSetup();
|
||||
featureTagStore.addTag({
|
||||
id: 1,
|
||||
type: 'simple',
|
||||
value: 'TeamRed',
|
||||
});
|
||||
return request.get(`${base}/api/admin/tags/id/1125`).expect(res => {
|
||||
t.is(res.status, 404);
|
||||
});
|
||||
});
|
||||
test('trying to get non-existing tag by name and type should not be found', t => {
|
||||
const { request, base } = getSetup();
|
||||
return request.get(`${base}/api/admin/tags/simple/TeamRed`).expect(res => {
|
||||
@ -95,21 +84,13 @@ test('trying to get non-existing tag by name and type should not be found', t =>
|
||||
});
|
||||
});
|
||||
test('should be able to delete a tag', t => {
|
||||
t.plan(1);
|
||||
const { request, featureTagStore, base } = getSetup();
|
||||
featureTagStore.addTag({
|
||||
type: 'simple',
|
||||
value: 'TeamGreen',
|
||||
});
|
||||
|
||||
featureTagStore.removeTag({ type: 'simple', value: 'TeamGreen' });
|
||||
t.plan(0);
|
||||
const { request, base, tagStore, perms } = getSetup();
|
||||
perms.withPermissions(UPDATE_FEATURE);
|
||||
tagStore.createTag({ type: 'simple', value: 'TeamRed' });
|
||||
return request
|
||||
.get(`${base}/api/admin/tags`)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect(res => {
|
||||
t.true(res.body.tags.length === 0);
|
||||
});
|
||||
.delete(`${base}/api/admin/tags/simple/TeamGreen`)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
test('should get empty tags of type', t => {
|
||||
@ -125,17 +106,9 @@ test('should get empty tags of type', t => {
|
||||
});
|
||||
|
||||
test('should be able to filter by type', t => {
|
||||
const { request, base, featureTagStore } = getSetup();
|
||||
featureTagStore.addTag({
|
||||
id: 1,
|
||||
value: 'TeamRed',
|
||||
type: 'simple',
|
||||
});
|
||||
featureTagStore.addTag({
|
||||
id: 2,
|
||||
value: 'TeamGreen',
|
||||
type: 'slack',
|
||||
});
|
||||
const { request, base, tagStore } = getSetup();
|
||||
tagStore.createTag({ type: 'simple', value: 'TeamRed' });
|
||||
tagStore.createTag({ type: 'slack', value: 'TeamGreen' });
|
||||
return request
|
||||
.get(`${base}/api/admin/tags/simple`)
|
||||
.expect(200)
|
||||
|
@ -6,9 +6,9 @@ const { filter } = require('./util');
|
||||
const version = 1;
|
||||
|
||||
class FeatureController extends Controller {
|
||||
constructor({ featureToggleStore }) {
|
||||
constructor({ featureToggleService }) {
|
||||
super();
|
||||
this.toggleStore = featureToggleStore;
|
||||
this.toggleService = featureToggleService;
|
||||
|
||||
this.get('/', this.getAll);
|
||||
this.get('/:featureName', this.getFeatureToggle);
|
||||
@ -17,7 +17,7 @@ class FeatureController extends Controller {
|
||||
async getAll(req, res) {
|
||||
const nameFilter = filter('name', req.query.namePrefix);
|
||||
|
||||
const allFeatureToggles = await this.toggleStore.getFeatures();
|
||||
const allFeatureToggles = await this.toggleService.getFeatures();
|
||||
const features = nameFilter(allFeatureToggles);
|
||||
|
||||
res.json({ version, features });
|
||||
@ -26,7 +26,7 @@ class FeatureController extends Controller {
|
||||
async getFeatureToggle(req, res) {
|
||||
try {
|
||||
const name = req.params.featureName;
|
||||
const featureToggle = await this.toggleStore.getFeature(name);
|
||||
const featureToggle = await this.toggleService.getFeature(name);
|
||||
res.json(featureToggle).end();
|
||||
} catch (err) {
|
||||
res.status(404).json({ error: 'Could not find feature' });
|
||||
|
@ -6,18 +6,20 @@ const { EventEmitter } = require('events');
|
||||
const store = require('../../../test/fixtures/store');
|
||||
const getLogger = require('../../../test/fixtures/no-logger');
|
||||
const getApp = require('../../app');
|
||||
const { createServices } = require('../../services');
|
||||
|
||||
const eventBus = new EventEmitter();
|
||||
|
||||
function getSetup() {
|
||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||
const stores = store.createStores();
|
||||
const app = getApp({
|
||||
const config = {
|
||||
baseUriPath: base,
|
||||
stores,
|
||||
eventBus,
|
||||
getLogger,
|
||||
});
|
||||
};
|
||||
const app = getApp(config, createServices(stores, config));
|
||||
|
||||
return {
|
||||
base,
|
||||
@ -41,7 +43,7 @@ test('should get empty getFeatures via client', t => {
|
||||
test('fetch single feature', t => {
|
||||
t.plan(1);
|
||||
const { request, featureToggleStore, base } = getSetup();
|
||||
featureToggleStore.addFeature({
|
||||
featureToggleStore.createFeature({
|
||||
name: 'test_',
|
||||
strategies: [{ name: 'default' }],
|
||||
});
|
||||
@ -58,10 +60,10 @@ test('fetch single feature', t => {
|
||||
test('support name prefix', t => {
|
||||
t.plan(2);
|
||||
const { request, featureToggleStore, base } = getSetup();
|
||||
featureToggleStore.addFeature({ name: 'a_test1' });
|
||||
featureToggleStore.addFeature({ name: 'a_test2' });
|
||||
featureToggleStore.addFeature({ name: 'b_test1' });
|
||||
featureToggleStore.addFeature({ name: 'b_test2' });
|
||||
featureToggleStore.createFeature({ name: 'a_test1' });
|
||||
featureToggleStore.createFeature({ name: 'a_test2' });
|
||||
featureToggleStore.createFeature({ name: 'b_test1' });
|
||||
featureToggleStore.createFeature({ name: 'b_test2' });
|
||||
|
||||
const namePrefix = 'b_';
|
||||
|
||||
|
@ -7,16 +7,21 @@ const RegisterController = require('./register.js');
|
||||
const apiDef = require('./api-def.json');
|
||||
|
||||
class ClientApi extends Controller {
|
||||
constructor(config) {
|
||||
constructor(config, services = {}) {
|
||||
super();
|
||||
|
||||
const { stores } = config;
|
||||
const { getLogger } = config;
|
||||
|
||||
this.get('/', this.index);
|
||||
this.use('/features', new FeatureController(stores, getLogger).router);
|
||||
this.use('/metrics', new MetricsController(stores, getLogger).router);
|
||||
this.use('/register', new RegisterController(stores, getLogger).router);
|
||||
this.use(
|
||||
'/features',
|
||||
new FeatureController(services, getLogger).router,
|
||||
);
|
||||
this.use('/metrics', new MetricsController(services, getLogger).router);
|
||||
this.use(
|
||||
'/register',
|
||||
new RegisterController(services, getLogger).router,
|
||||
);
|
||||
}
|
||||
|
||||
index(req, res) {
|
||||
|
@ -1,18 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const Controller = require('../controller');
|
||||
const { clientMetricsSchema } = require('./metrics-schema');
|
||||
|
||||
class ClientMetricsController extends Controller {
|
||||
constructor(
|
||||
{ clientMetricsStore, clientInstanceStore, featureToggleStore },
|
||||
getLogger,
|
||||
) {
|
||||
constructor({ clientMetricsService }, getLogger) {
|
||||
super();
|
||||
this.logger = getLogger('/api/client/metrics');
|
||||
this.clientMetricsStore = clientMetricsStore;
|
||||
this.clientInstanceStore = clientInstanceStore;
|
||||
this.featureToggleStore = featureToggleStore;
|
||||
this.metrics = clientMetricsService;
|
||||
|
||||
this.post('/', this.registerMetrics);
|
||||
}
|
||||
@ -21,26 +15,17 @@ class ClientMetricsController extends Controller {
|
||||
const data = req.body;
|
||||
const clientIp = req.ip;
|
||||
|
||||
const { error, value } = clientMetricsSchema.validate(data);
|
||||
|
||||
if (error) {
|
||||
this.logger.warn('Invalid metrics posted', error);
|
||||
return res.status(400).json(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const toggleNames = Object.keys(value.bucket.toggles);
|
||||
await this.featureToggleStore.lastSeenToggles(toggleNames);
|
||||
await this.clientMetricsStore.insert(value);
|
||||
await this.clientInstanceStore.insert({
|
||||
appName: value.appName,
|
||||
instanceId: value.instanceId,
|
||||
clientIp,
|
||||
});
|
||||
await this.metrics.registerClientMetrics(data, clientIp);
|
||||
return res.status(202).end();
|
||||
} catch (e) {
|
||||
this.logger.error('failed to store metrics', e);
|
||||
return res.status(500).end();
|
||||
this.logger.error('Failed to store metrics', e);
|
||||
switch (e.name) {
|
||||
case 'ValidationError':
|
||||
return res.status(400).end();
|
||||
default:
|
||||
return res.status(500).end();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,9 @@ const { EventEmitter } = require('events');
|
||||
const store = require('../../../test/fixtures/store');
|
||||
const getLogger = require('../../../test/fixtures/no-logger');
|
||||
const getApp = require('../../app');
|
||||
const { clientMetricsSchema } = require('./metrics-schema');
|
||||
const {
|
||||
clientMetricsSchema,
|
||||
} = require('../../services/client-metrics/client-metrics-schema');
|
||||
const { createServices } = require('../../services');
|
||||
|
||||
const eventBus = new EventEmitter();
|
||||
@ -159,7 +161,7 @@ test('shema allow yes=<string nbr>', t => {
|
||||
test('should set lastSeen on toggle', async t => {
|
||||
t.plan(1);
|
||||
const { request, stores } = getSetup();
|
||||
stores.featureToggleStore.addFeature({ name: 'toggleLastSeen' });
|
||||
stores.featureToggleStore.createFeature({ name: 'toggleLastSeen' });
|
||||
await request
|
||||
.post('/api/client/metrics')
|
||||
.send({
|
||||
|
@ -1,40 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
const Controller = require('../controller');
|
||||
const { clientRegisterSchema: schema } = require('./register-schema');
|
||||
|
||||
class RegisterController extends Controller {
|
||||
constructor({ clientInstanceStore, clientApplicationsStore }, getLogger) {
|
||||
constructor({ clientMetricsService }, getLogger) {
|
||||
super();
|
||||
this.logger = getLogger('/api/client/register');
|
||||
this.clientInstanceStore = clientInstanceStore;
|
||||
this.clientApplicationsStore = clientApplicationsStore;
|
||||
|
||||
this.metrics = clientMetricsService;
|
||||
this.post('/', this.handleRegister);
|
||||
}
|
||||
|
||||
async handleRegister(req, res) {
|
||||
const data = req.body;
|
||||
const { value, error } = schema.validate(data);
|
||||
|
||||
if (error) {
|
||||
this.logger.warn('Invalid client data posted', error);
|
||||
return res.status(400).json(error);
|
||||
}
|
||||
|
||||
value.clientIp = req.ip;
|
||||
|
||||
try {
|
||||
await this.clientApplicationsStore.upsert(value);
|
||||
await this.clientInstanceStore.insert(value);
|
||||
const { appName, instanceId } = value;
|
||||
this.logger.info(
|
||||
`New client registration: appName=${appName}, instanceId=${instanceId}`,
|
||||
);
|
||||
const clientIp = req.ip;
|
||||
await this.metrics.registerClient(data, clientIp);
|
||||
return res.status(202).end();
|
||||
} catch (err) {
|
||||
this.logger.error('failed to register client', err);
|
||||
return res.status(500).end();
|
||||
switch (err.name) {
|
||||
case 'ValidationError':
|
||||
return res.status(400).end();
|
||||
default:
|
||||
return res.status(500).end();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,17 +6,20 @@ const { EventEmitter } = require('events');
|
||||
const store = require('../../../test/fixtures/store');
|
||||
const getLogger = require('../../../test/fixtures/no-logger');
|
||||
const getApp = require('../../app');
|
||||
const { createServices } = require('../../services');
|
||||
|
||||
const eventBus = new EventEmitter();
|
||||
|
||||
function getSetup() {
|
||||
const stores = store.createStores();
|
||||
const app = getApp({
|
||||
const config = {
|
||||
baseUriPath: '',
|
||||
stores,
|
||||
eventBus,
|
||||
getLogger,
|
||||
});
|
||||
};
|
||||
const services = createServices(stores, config);
|
||||
const app = getApp(config, services);
|
||||
|
||||
return {
|
||||
request: supertest(app),
|
||||
|
@ -22,7 +22,10 @@ class IndexRouter extends Controller {
|
||||
|
||||
// legacy support (remove in 4.x)
|
||||
if (config.enableLegacyRoutes) {
|
||||
const featureController = new FeatureController(config.stores);
|
||||
const featureController = new FeatureController(
|
||||
services,
|
||||
config.getLogger,
|
||||
);
|
||||
this.use('/api/features', featureController.router);
|
||||
}
|
||||
}
|
||||
|
@ -6,23 +6,25 @@ const { EventEmitter } = require('events');
|
||||
const store = require('../../test/fixtures/store');
|
||||
const getLogger = require('../../test/fixtures/no-logger');
|
||||
const getApp = require('../app');
|
||||
const { createServices } = require('../services');
|
||||
|
||||
const eventBus = new EventEmitter();
|
||||
|
||||
function getSetup() {
|
||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||
const stores = store.createStores();
|
||||
const app = getApp({
|
||||
const config = {
|
||||
baseUriPath: base,
|
||||
stores,
|
||||
eventBus,
|
||||
enableLegacyRoutes: true,
|
||||
getLogger,
|
||||
});
|
||||
};
|
||||
const services = createServices(stores, config);
|
||||
const app = getApp(config, services);
|
||||
|
||||
return {
|
||||
base,
|
||||
featureToggleStore: stores.featureToggleStore,
|
||||
request: supertest(app),
|
||||
};
|
||||
}
|
||||
|
@ -10,9 +10,14 @@ const UnleashClientMetrics = require('./index');
|
||||
const appName = 'appName';
|
||||
const instanceId = 'instanceId';
|
||||
|
||||
const getLogger = require('../../../test/fixtures/no-logger');
|
||||
|
||||
test('should work without state', t => {
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics({ clientMetricsStore });
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore },
|
||||
{ getLogger },
|
||||
);
|
||||
|
||||
t.truthy(metrics.getAppsWithToggles());
|
||||
t.truthy(metrics.getTogglesMetrics());
|
||||
@ -24,7 +29,10 @@ test.cb('data should expire', t => {
|
||||
const clock = lolex.install();
|
||||
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics({ clientMetricsStore });
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore },
|
||||
{ getLogger },
|
||||
);
|
||||
|
||||
metrics.addPayload({
|
||||
appName,
|
||||
@ -65,7 +73,10 @@ test.cb('data should expire', t => {
|
||||
|
||||
test('should listen to metrics from store', t => {
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics({ clientMetricsStore });
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore },
|
||||
{ getLogger },
|
||||
);
|
||||
clientMetricsStore.emit('metrics', {
|
||||
appName,
|
||||
instanceId,
|
||||
@ -123,7 +134,10 @@ test('should listen to metrics from store', t => {
|
||||
|
||||
test('should build up list of seend toggles when new metrics arrives', t => {
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics({ clientMetricsStore });
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore },
|
||||
{ getLogger },
|
||||
);
|
||||
clientMetricsStore.emit('metrics', {
|
||||
appName,
|
||||
instanceId,
|
||||
@ -159,7 +173,10 @@ test('should build up list of seend toggles when new metrics arrives', t => {
|
||||
|
||||
test('should handle a lot of toggles', t => {
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics({ clientMetricsStore });
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore },
|
||||
{ getLogger },
|
||||
);
|
||||
|
||||
const toggleCounts = {};
|
||||
for (let i = 0; i < 100; i++) {
|
||||
@ -186,7 +203,10 @@ test('should have correct values for lastMinute', t => {
|
||||
const clock = lolex.install();
|
||||
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics({ clientMetricsStore });
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore },
|
||||
{ getLogger },
|
||||
);
|
||||
|
||||
const now = new Date();
|
||||
const input = [
|
||||
@ -258,7 +278,10 @@ test('should have correct values for lastHour', t => {
|
||||
const clock = lolex.install();
|
||||
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics({ clientMetricsStore });
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore },
|
||||
{ getLogger },
|
||||
);
|
||||
|
||||
const now = new Date();
|
||||
const input = [
|
||||
@ -338,7 +361,10 @@ test('should have correct values for lastHour', t => {
|
||||
|
||||
test('should not fail when toggle metrics is missing yes/no field', t => {
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics({ clientMetricsStore });
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore },
|
||||
{ getLogger },
|
||||
);
|
||||
clientMetricsStore.emit('metrics', {
|
||||
appName,
|
||||
instanceId,
|
||||
|
@ -4,18 +4,39 @@
|
||||
|
||||
const Projection = require('./projection.js');
|
||||
const TTLList = require('./ttl-list.js');
|
||||
const appSchema = require('./metrics-schema');
|
||||
const NotFoundError = require('../../error/notfound-error');
|
||||
const { clientMetricsSchema } = require('./client-metrics-schema');
|
||||
const { clientRegisterSchema } = require('./register-schema');
|
||||
const { APPLICATION_CREATED } = require('../../event-type');
|
||||
|
||||
module.exports = class ClientMetricsService {
|
||||
constructor({ clientMetricsStore }) {
|
||||
constructor(
|
||||
{
|
||||
clientMetricsStore,
|
||||
strategyStore,
|
||||
featureToggleStore,
|
||||
clientApplicationsStore,
|
||||
clientInstanceStore,
|
||||
eventStore,
|
||||
},
|
||||
{ getLogger },
|
||||
) {
|
||||
this.globalCount = 0;
|
||||
this.apps = {};
|
||||
|
||||
this.strategyStore = strategyStore;
|
||||
this.toggleStore = featureToggleStore;
|
||||
this.clientAppStore = clientApplicationsStore;
|
||||
this.clientInstanceStore = clientInstanceStore;
|
||||
this.clientMetricsStore = clientMetricsStore;
|
||||
this.lastHourProjection = new Projection();
|
||||
this.lastMinuteProjection = new Projection();
|
||||
this.eventStore = eventStore;
|
||||
|
||||
this.lastHourList = new TTLList({
|
||||
interval: 10000,
|
||||
});
|
||||
this.logger = getLogger('services/client-metrics/index.js');
|
||||
|
||||
this.lastMinuteList = new TTLList({
|
||||
interval: 10000,
|
||||
@ -42,6 +63,44 @@ module.exports = class ClientMetricsService {
|
||||
clientMetricsStore.on('metrics', m => this.addPayload(m));
|
||||
}
|
||||
|
||||
async registerClientMetrics(data, clientIp) {
|
||||
const value = await clientMetricsSchema.validateAsync(data);
|
||||
const toggleNames = Object.keys(value.bucket.toggles);
|
||||
await this.toggleStore.lastSeenToggles(toggleNames);
|
||||
await this.clientMetricsStore.insert(value);
|
||||
await this.clientInstanceStore.insert({
|
||||
appName: value.appName,
|
||||
instanceId: value.instanceId,
|
||||
clientIp,
|
||||
});
|
||||
}
|
||||
|
||||
async upsertApp(value, clientIp) {
|
||||
try {
|
||||
const app = await this.clientAppStore.getApplication(value.appName);
|
||||
await this.updateRow(value, app);
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) {
|
||||
await this.clientAppStore.insertNewRow(value);
|
||||
await this.eventStore.store({
|
||||
type: APPLICATION_CREATED,
|
||||
createdBy: clientIp,
|
||||
data: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async registerClient(data, clientIp) {
|
||||
const value = await clientRegisterSchema.validateAsync(data);
|
||||
value.clientIp = clientIp;
|
||||
await this.upsertApp(value, clientIp);
|
||||
await this.clientInstanceStore.insert(value);
|
||||
this.logger.info(
|
||||
`New client registration: appName=${value.appName}, instanceId=${value.instanceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
getAppsWithToggles() {
|
||||
const apps = [];
|
||||
Object.keys(this.apps).forEach(appName => {
|
||||
@ -58,6 +117,66 @@ module.exports = class ClientMetricsService {
|
||||
: [];
|
||||
}
|
||||
|
||||
async getSeenApps() {
|
||||
const seenApps = this.getSeenAppsPerToggle();
|
||||
const applications = await this.clientAppStore.getApplications();
|
||||
const metaData = applications.reduce((result, entry) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
result[entry.appName] = entry;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
Object.keys(seenApps).forEach(key => {
|
||||
seenApps[key] = seenApps[key].map(entry => {
|
||||
if (metaData[entry.appName]) {
|
||||
return { ...entry, ...metaData[entry.appName] };
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
});
|
||||
return seenApps;
|
||||
}
|
||||
|
||||
async getApplications(query) {
|
||||
return this.clientAppStore.getApplications(query);
|
||||
}
|
||||
|
||||
async getApplication(appName) {
|
||||
const seenToggles = this.getSeenTogglesByAppName(appName);
|
||||
const [
|
||||
application,
|
||||
instances,
|
||||
strategies,
|
||||
features,
|
||||
] = await Promise.all([
|
||||
this.clientAppStore.getApplication(appName),
|
||||
this.clientInstanceStore.getByAppName(appName),
|
||||
this.strategyStore.getStrategies(),
|
||||
this.toggleStore.getFeatures(),
|
||||
]);
|
||||
|
||||
return {
|
||||
appName: application.appName,
|
||||
createdAt: application.createdAt,
|
||||
description: application.description,
|
||||
url: application.url,
|
||||
color: application.color,
|
||||
icon: application.icon,
|
||||
strategies: application.strategies.map(name => {
|
||||
const found = strategies.find(f => f.name === name);
|
||||
return found || { name, notFound: true };
|
||||
}),
|
||||
instances,
|
||||
seenToggles: seenToggles.map(name => {
|
||||
const found = features.find(f => f.name === name);
|
||||
return found || { name, notFound: true };
|
||||
}),
|
||||
links: {
|
||||
self: `/api/applications/${application.appName}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getSeenAppsPerToggle() {
|
||||
const toggles = {};
|
||||
Object.keys(this.apps).forEach(appName => {
|
||||
@ -139,6 +258,16 @@ module.exports = class ClientMetricsService {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteApplication(appName) {
|
||||
await this.clientInstanceStore.deleteForApplication(appName);
|
||||
await this.clientAppStore.deleteApplication(appName);
|
||||
}
|
||||
|
||||
async createApplication(input) {
|
||||
const applicationData = await appSchema.validateAsync(input);
|
||||
await this.clientAppStore.upsert(applicationData);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.lastHourList.destroy();
|
||||
this.lastMinuteList.destroy();
|
||||
|
@ -1,9 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const joi = require('joi');
|
||||
const { nameType } = require('./util');
|
||||
const { nameType } = require('../routes/admin-api/util');
|
||||
|
||||
const nameSchema = joi.object().keys({ name: nameType });
|
||||
const nameSchema = joi
|
||||
.object()
|
||||
.keys({ name: nameType })
|
||||
.options({ stripUnknown: true, allowUnknown: false, abortEarly: false });
|
||||
|
||||
const constraintSchema = joi.object().keys({
|
||||
contextName: joi.string(),
|
||||
@ -58,7 +61,7 @@ const variantsSchema = joi.object().keys({
|
||||
),
|
||||
});
|
||||
|
||||
const featureShema = joi
|
||||
const featureSchema = joi
|
||||
.object()
|
||||
.keys({
|
||||
name: nameType,
|
||||
@ -83,6 +86,6 @@ const featureShema = joi
|
||||
.optional()
|
||||
.items(variantsSchema),
|
||||
})
|
||||
.options({ allowUnknown: false, stripUnknown: true });
|
||||
.options({ allowUnknown: false, stripUnknown: true, abortEarly: false });
|
||||
|
||||
module.exports = { featureShema, strategiesSchema, nameSchema };
|
||||
module.exports = { featureSchema, strategiesSchema, nameSchema };
|
@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('ava');
|
||||
const { featureShema } = require('./feature-schema');
|
||||
const { featureSchema } = require('./feature-schema');
|
||||
|
||||
test('should require URL firendly name', t => {
|
||||
const toggle = {
|
||||
@ -10,7 +10,7 @@ test('should require URL firendly name', t => {
|
||||
strategies: [{ name: 'default' }],
|
||||
};
|
||||
|
||||
const { error } = featureShema.validate(toggle);
|
||||
const { error } = featureSchema.validate(toggle);
|
||||
t.deepEqual(error.details[0].message, '"name" must be URL friendly');
|
||||
});
|
||||
|
||||
@ -21,7 +21,7 @@ test('should be valid toggle name', t => {
|
||||
strategies: [{ name: 'default' }],
|
||||
};
|
||||
|
||||
const { value } = featureShema.validate(toggle);
|
||||
const { value } = featureSchema.validate(toggle);
|
||||
t.is(value.name, toggle.name);
|
||||
});
|
||||
|
||||
@ -41,7 +41,7 @@ test('should strip extra variant fields', t => {
|
||||
],
|
||||
};
|
||||
|
||||
const { value } = featureShema.validate(toggle);
|
||||
const { value } = featureSchema.validate(toggle);
|
||||
t.notDeepEqual(value, toggle);
|
||||
t.falsy(value.variants[0].unkown);
|
||||
});
|
||||
@ -63,7 +63,7 @@ test('should allow weightType=fix', t => {
|
||||
],
|
||||
};
|
||||
|
||||
const { value } = featureShema.validate(toggle);
|
||||
const { value } = featureSchema.validate(toggle);
|
||||
t.deepEqual(value, toggle);
|
||||
});
|
||||
|
||||
@ -83,7 +83,7 @@ test('should disallow weightType=unknown', t => {
|
||||
],
|
||||
};
|
||||
|
||||
const { error } = featureShema.validate(toggle);
|
||||
const { error } = featureSchema.validate(toggle);
|
||||
t.deepEqual(
|
||||
error.details[0].message,
|
||||
'"variants[0].weightType" must be one of [variable, fix]',
|
||||
@ -113,7 +113,7 @@ test('should be possible to define variant overrides', t => {
|
||||
],
|
||||
};
|
||||
|
||||
const { value, error } = featureShema.validate(toggle);
|
||||
const { value, error } = featureSchema.validate(toggle);
|
||||
t.deepEqual(value, toggle);
|
||||
t.falsy(error);
|
||||
});
|
||||
@ -139,7 +139,7 @@ test('variant overrides must have corect shape', async t => {
|
||||
};
|
||||
|
||||
try {
|
||||
await featureShema.validateAsync(toggle);
|
||||
await featureSchema.validateAsync(toggle);
|
||||
} catch (error) {
|
||||
t.is(
|
||||
error.details[0].message,
|
||||
@ -169,7 +169,7 @@ test('should keep constraints', t => {
|
||||
],
|
||||
};
|
||||
|
||||
const { value, error } = featureShema.validate(toggle);
|
||||
const { value, error } = featureSchema.validate(toggle);
|
||||
t.deepEqual(value, toggle);
|
||||
t.falsy(error);
|
||||
});
|
||||
@ -194,7 +194,7 @@ test('should not accept empty constraint values', t => {
|
||||
],
|
||||
};
|
||||
|
||||
const { error } = featureShema.validate(toggle);
|
||||
const { error } = featureSchema.validate(toggle);
|
||||
t.deepEqual(
|
||||
error.details[0].message,
|
||||
'"strategies[0].constraints[0].values[0]" is not allowed to be empty',
|
||||
@ -221,7 +221,7 @@ test('should not accept empty list of constraint values', t => {
|
||||
],
|
||||
};
|
||||
|
||||
const { error } = featureShema.validate(toggle);
|
||||
const { error } = featureSchema.validate(toggle);
|
||||
t.deepEqual(
|
||||
error.details[0].message,
|
||||
'"strategies[0].constraints[0].values" must contain at least 1 items',
|
172
lib/services/feature-toggle-service.js
Normal file
172
lib/services/feature-toggle-service.js
Normal file
@ -0,0 +1,172 @@
|
||||
const { FEATURE_TAGGED, FEATURE_UNTAGGED } = require('../event-type');
|
||||
const { featureSchema, nameSchema } = require('./feature-schema');
|
||||
const { tagSchema } = require('./tag-schema');
|
||||
const NameExistsError = require('../error/name-exists-error');
|
||||
const NotFoundError = require('../error/notfound-error');
|
||||
const {
|
||||
FEATURE_ARCHIVED,
|
||||
FEATURE_CREATED,
|
||||
FEATURE_REVIVED,
|
||||
FEATURE_UPDATED,
|
||||
TAG_CREATED,
|
||||
} = require('../event-type');
|
||||
|
||||
class FeatureToggleService {
|
||||
constructor(
|
||||
{ featureToggleStore, featureTagStore, tagStore, eventStore },
|
||||
{ getLogger },
|
||||
) {
|
||||
this.featureToggleStore = featureToggleStore;
|
||||
this.tagStore = tagStore;
|
||||
this.eventStore = eventStore;
|
||||
this.featureTagStore = featureTagStore;
|
||||
this.logger = getLogger('services/feature-toggle-service.js');
|
||||
}
|
||||
|
||||
async getFeatures() {
|
||||
return this.featureToggleStore.getFeatures();
|
||||
}
|
||||
|
||||
async getArchivedFeatures() {
|
||||
return this.featureToggleStore.getArchivedFeatures();
|
||||
}
|
||||
|
||||
async addArchivedFeature(feature) {
|
||||
await this.featureToggleStore.addArchivedFeature(feature);
|
||||
}
|
||||
|
||||
async getFeature(name) {
|
||||
return this.featureToggleStore.getFeature(name);
|
||||
}
|
||||
|
||||
async createFeatureToggle(value, userName) {
|
||||
await this.validateName(value);
|
||||
const feature = await featureSchema.validateAsync(value);
|
||||
await this.featureToggleStore.createFeature(feature);
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_CREATED,
|
||||
createdBy: userName,
|
||||
data: feature,
|
||||
});
|
||||
}
|
||||
|
||||
async updateToggle(updatedFeature, userName) {
|
||||
await this.featureToggleStore.getFeature(updatedFeature.name);
|
||||
const value = await featureSchema.validateAsync(updatedFeature);
|
||||
await this.featureToggleStore.updateFeature(value);
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_UPDATED,
|
||||
createdBy: userName,
|
||||
data: updatedFeature,
|
||||
});
|
||||
}
|
||||
|
||||
async archiveToggle(name, userName) {
|
||||
await this.featureToggleStore.getFeature(name);
|
||||
await this.featureToggleStore.archiveFeature(name);
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_ARCHIVED,
|
||||
createdBy: userName,
|
||||
data: { name },
|
||||
});
|
||||
}
|
||||
|
||||
async reviveToggle(name, userName) {
|
||||
await this.featureToggleStore.reviveFeature({ name });
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_REVIVED,
|
||||
createdBy: userName,
|
||||
data: { name },
|
||||
});
|
||||
}
|
||||
|
||||
async toggle(featureName, userName) {
|
||||
const feature = await this.featureToggleStore.getFeature(featureName);
|
||||
const toggle = !feature.enabled;
|
||||
return this.updateField(feature.name, 'enabled', toggle, userName);
|
||||
}
|
||||
|
||||
/** Tag releated */
|
||||
async listTags(featureName) {
|
||||
return this.featureTagStore.getAllTagsForFeature(featureName);
|
||||
}
|
||||
|
||||
async addTag(featureName, tag, userName) {
|
||||
await nameSchema.validateAsync({ name: featureName });
|
||||
const validatedTag = await tagSchema.validateAsync(tag);
|
||||
await this.createTagIfNeeded(validatedTag, userName);
|
||||
await this.featureTagStore.tagFeature(featureName, validatedTag);
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_TAGGED,
|
||||
createdBy: userName,
|
||||
data: {
|
||||
featureName,
|
||||
tag: validatedTag,
|
||||
},
|
||||
});
|
||||
return validatedTag;
|
||||
}
|
||||
|
||||
async createTagIfNeeded(tag, userName) {
|
||||
try {
|
||||
await this.tagStore.getTag(tag.type, tag.value);
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) {
|
||||
await this.tagStore.createTag(tag);
|
||||
await this.eventStore.store({
|
||||
type: TAG_CREATED,
|
||||
createdBy: userName,
|
||||
data: {
|
||||
tag,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async removeTag(featureName, tag, userName) {
|
||||
await this.featureTagStore.untagFeature(featureName, tag);
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_UNTAGGED,
|
||||
createdBy: userName,
|
||||
data: {
|
||||
featureName,
|
||||
tag,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Validations */
|
||||
async validateName({ name }) {
|
||||
await nameSchema.validateAsync({ name });
|
||||
await this.validateUniqueFeatureName(name);
|
||||
return name;
|
||||
}
|
||||
|
||||
async validateUniqueFeatureName(name) {
|
||||
let msg;
|
||||
try {
|
||||
const feature = await this.featureToggleStore.hasFeature(name);
|
||||
msg = feature.archived
|
||||
? 'An archived toggle with that name already exists'
|
||||
: 'A toggle with that name already exists';
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
throw new NameExistsError(msg);
|
||||
}
|
||||
|
||||
async updateField(featureName, field, value, userName) {
|
||||
const feature = await this.featureToggleStore.getFeature(featureName);
|
||||
feature[field] = value;
|
||||
await this.featureToggleStore.updateFeature(feature);
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_UPDATED,
|
||||
createdBy: userName,
|
||||
data: feature,
|
||||
});
|
||||
return feature;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FeatureToggleService;
|
@ -1,9 +1,17 @@
|
||||
const FeatureToggleService = require('./feature-toggle-service');
|
||||
const ProjectService = require('./project-service');
|
||||
const StateService = require('./state-service');
|
||||
const ClientMetricsService = require('./client-metrics');
|
||||
const TagTypeService = require('./tag-type-service');
|
||||
const TagService = require('./tag-service');
|
||||
const StrategyService = require('./strategy-service');
|
||||
|
||||
module.exports.createServices = (stores, config) => ({
|
||||
featureToggleService: new FeatureToggleService(stores, config),
|
||||
projectService: new ProjectService(stores, config),
|
||||
stateService: new StateService(stores, config),
|
||||
strategyService: new StrategyService(stores, config),
|
||||
tagTypeService: new TagTypeService(stores, config),
|
||||
tagService: new TagService(stores, config),
|
||||
clientMetricsService: new ClientMetricsService(stores, config),
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
const joi = require('joi');
|
||||
const { featureShema } = require('../routes/admin-api/feature-schema');
|
||||
const strategySchema = require('../routes/admin-api/strategy-schema');
|
||||
const { featureSchema } = require('./feature-schema');
|
||||
const strategySchema = require('./strategy-schema');
|
||||
|
||||
// TODO: Extract to seperate file
|
||||
const stateSchema = joi.object().keys({
|
||||
@ -8,7 +8,7 @@ const stateSchema = joi.object().keys({
|
||||
features: joi
|
||||
.array()
|
||||
.optional()
|
||||
.items(featureShema),
|
||||
.items(featureSchema),
|
||||
strategies: joi
|
||||
.array()
|
||||
.optional()
|
||||
|
@ -64,6 +64,7 @@ class StateService {
|
||||
|
||||
if (dropBeforeImport) {
|
||||
this.logger.info(`Dropping existing feature toggles`);
|
||||
await this.toggleStore.dropFeatures();
|
||||
await this.eventStore.store({
|
||||
type: DROP_FEATURES,
|
||||
createdBy: userName,
|
||||
@ -76,11 +77,13 @@ class StateService {
|
||||
.filter(filterExisitng(keepExisting, oldToggles))
|
||||
.filter(filterEqual(oldToggles))
|
||||
.map(feature =>
|
||||
this.eventStore.store({
|
||||
type: FEATURE_IMPORT,
|
||||
createdBy: userName,
|
||||
data: feature,
|
||||
}),
|
||||
this.toggleStore.importFeature(feature).then(() =>
|
||||
this.eventStore.store({
|
||||
type: FEATURE_IMPORT,
|
||||
createdBy: userName,
|
||||
data: feature,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -98,6 +101,7 @@ class StateService {
|
||||
|
||||
if (dropBeforeImport) {
|
||||
this.logger.info(`Dropping existing strategies`);
|
||||
await this.strategyStore.dropStrategies();
|
||||
await this.eventStore.store({
|
||||
type: DROP_STRATEGIES,
|
||||
createdBy: userName,
|
||||
@ -110,10 +114,12 @@ class StateService {
|
||||
.filter(filterExisitng(keepExisting, oldStrategies))
|
||||
.filter(filterEqual(oldStrategies))
|
||||
.map(strategy =>
|
||||
this.eventStore.store({
|
||||
type: STRATEGY_IMPORT,
|
||||
createdBy: userName,
|
||||
data: strategy,
|
||||
this.strategyStore.importStrategy(strategy).then(() => {
|
||||
this.eventStore.store({
|
||||
type: STRATEGY_IMPORT,
|
||||
createdBy: userName,
|
||||
data: strategy,
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
@ -55,7 +55,7 @@ test('should not import an existing feature', async t => {
|
||||
],
|
||||
};
|
||||
|
||||
await stores.featureToggleStore.addFeature(data.features[0]);
|
||||
await stores.featureToggleStore.createFeature(data.features[0]);
|
||||
|
||||
await stateService.import({ data, keepExisting: true });
|
||||
|
||||
@ -76,7 +76,7 @@ test('should not keep existing feature if drop-before-import', async t => {
|
||||
],
|
||||
};
|
||||
|
||||
await stores.featureToggleStore.addFeature(data.features[0]);
|
||||
await stores.featureToggleStore.createFeature(data.features[0]);
|
||||
|
||||
await stateService.import({
|
||||
data,
|
||||
@ -144,7 +144,7 @@ test('should not import an exiting strategy', async t => {
|
||||
],
|
||||
};
|
||||
|
||||
await stores.strategyStore.addStrategy(data.strategies[0]);
|
||||
await stores.strategyStore.createStrategy(data.strategies[0]);
|
||||
|
||||
await stateService.import({ data, keepExisting: true });
|
||||
|
||||
@ -201,7 +201,7 @@ test('should not accept gibberish', async t => {
|
||||
test('should export featureToggles', async t => {
|
||||
const { stateService, stores } = getSetup();
|
||||
|
||||
stores.featureToggleStore.addFeature({ name: 'a-feature' });
|
||||
stores.featureToggleStore.createFeature({ name: 'a-feature' });
|
||||
|
||||
const data = await stateService.export({ includeFeatureToggles: true });
|
||||
|
||||
@ -212,7 +212,7 @@ test('should export featureToggles', async t => {
|
||||
test('should export strategies', async t => {
|
||||
const { stateService, stores } = getSetup();
|
||||
|
||||
stores.strategyStore.addStrategy({ name: 'a-strategy', editable: true });
|
||||
stores.strategyStore.createStrategy({ name: 'a-strategy', editable: true });
|
||||
|
||||
const data = await stateService.export({ includeStrategies: true });
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const joi = require('joi');
|
||||
const { nameType } = require('./util');
|
||||
const { nameType } = require('../routes/admin-api/util');
|
||||
|
||||
const strategySchema = joi.object().keys({
|
||||
name: nameType,
|
83
lib/services/strategy-service.js
Normal file
83
lib/services/strategy-service.js
Normal file
@ -0,0 +1,83 @@
|
||||
const strategySchema = require('./strategy-schema');
|
||||
const NameExistsError = require('../error/name-exists-error');
|
||||
const {
|
||||
STRATEGY_CREATED,
|
||||
STRATEGY_DELETED,
|
||||
STRATEGY_UPDATED,
|
||||
} = require('../event-type');
|
||||
|
||||
class StrategyService {
|
||||
constructor({ strategyStore, eventStore }, { getLogger }) {
|
||||
this.strategyStore = strategyStore;
|
||||
this.eventStore = eventStore;
|
||||
this.logger = getLogger('services/strategy-service.js');
|
||||
}
|
||||
|
||||
async getStrategies() {
|
||||
return this.strategyStore.getStrategies();
|
||||
}
|
||||
|
||||
async getStrategy(name) {
|
||||
return this.strategyStore.getStrategy(name);
|
||||
}
|
||||
|
||||
async removeStrategy(strategyName, userName) {
|
||||
const strategy = await this.strategyStore.getStrategy(strategyName);
|
||||
await this._validateEditable(strategy);
|
||||
await this.strategyStore.deleteStrategy({ name: strategyName });
|
||||
await this.eventStore.store({
|
||||
type: STRATEGY_DELETED,
|
||||
createdBy: userName,
|
||||
data: {
|
||||
name: strategyName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createStrategy(value, userName) {
|
||||
const strategy = await strategySchema.validateAsync(value);
|
||||
await this._validateStrategyName(strategy);
|
||||
await this.strategyStore.createStrategy(strategy);
|
||||
await this.eventStore.store({
|
||||
type: STRATEGY_CREATED,
|
||||
createdBy: userName,
|
||||
data: strategy,
|
||||
});
|
||||
}
|
||||
|
||||
async updateStrategy(input, userName) {
|
||||
const value = await strategySchema.validateAsync(input);
|
||||
const strategy = await this.strategyStore.getStrategy(input.name);
|
||||
await this._validateEditable(strategy);
|
||||
await this.strategyStore.updateStrategy(value);
|
||||
await this.eventStore.store({
|
||||
type: STRATEGY_UPDATED,
|
||||
createdBy: userName,
|
||||
data: value,
|
||||
});
|
||||
}
|
||||
|
||||
async _validateStrategyName(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.strategyStore
|
||||
.getStrategy(data.name)
|
||||
.then(() =>
|
||||
reject(
|
||||
new NameExistsError(
|
||||
`Strategy with name ${data.name} already exist!`,
|
||||
),
|
||||
),
|
||||
)
|
||||
.catch(() => resolve(data));
|
||||
});
|
||||
}
|
||||
|
||||
// This check belongs in the store.
|
||||
_validateEditable(strategy) {
|
||||
if (strategy.editable === false) {
|
||||
throw new Error(`Cannot edit strategy ${strategy.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StrategyService;
|
@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const joi = require('joi');
|
||||
const { customJoi } = require('./util');
|
||||
const { customJoi } = require('../routes/admin-api/util');
|
||||
|
||||
const tagSchema = joi
|
||||
.object()
|
62
lib/services/tag-service.js
Normal file
62
lib/services/tag-service.js
Normal file
@ -0,0 +1,62 @@
|
||||
const { tagSchema } = require('./tag-schema');
|
||||
const NotFoundError = require('../error/notfound-error');
|
||||
const NameExistsError = require('../error/name-exists-error');
|
||||
const { TAG_CREATED, TAG_DELETED } = require('../event-type');
|
||||
|
||||
class TagService {
|
||||
constructor({ tagStore, eventStore }, { getLogger }) {
|
||||
this.tagStore = tagStore;
|
||||
this.eventStore = eventStore;
|
||||
this.logger = getLogger('services/tag-service.js');
|
||||
}
|
||||
|
||||
async getTags() {
|
||||
return this.tagStore.getAll();
|
||||
}
|
||||
|
||||
async getTagsByType(type) {
|
||||
return this.tagStore.getTagsByType(type);
|
||||
}
|
||||
|
||||
async getTag({ type, value }) {
|
||||
return this.tagStore.getTag(type, value);
|
||||
}
|
||||
|
||||
async validateUnique(tag) {
|
||||
try {
|
||||
await this.tagStore.getTag(tag.type, tag.value);
|
||||
} catch (err) {
|
||||
if (err instanceof NotFoundError) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new NameExistsError(`A tag of ${tag} already exists`);
|
||||
}
|
||||
|
||||
async validate(tag) {
|
||||
const data = await tagSchema.validateAsync(tag);
|
||||
await this.validateUnique(tag);
|
||||
return data;
|
||||
}
|
||||
|
||||
async createTag(tag, userName) {
|
||||
const data = await this.validate(tag);
|
||||
await this.tagStore.createTag(data);
|
||||
await this.eventStore.store({
|
||||
type: TAG_CREATED,
|
||||
createdBy: userName,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteTag(tag, userName) {
|
||||
await this.tagStore.deleteTag(tag);
|
||||
await this.eventStore.store({
|
||||
type: TAG_DELETED,
|
||||
createdBy: userName,
|
||||
data: tag,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TagService;
|
@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const joi = require('joi');
|
||||
const { customJoi } = require('./util');
|
||||
const { customJoi } = require('../routes/admin-api/util');
|
||||
|
||||
const tagTypeSchema = joi
|
||||
.object()
|
76
lib/services/tag-type-service.js
Normal file
76
lib/services/tag-type-service.js
Normal file
@ -0,0 +1,76 @@
|
||||
const NameExistsError = require('../error/name-exists-error');
|
||||
const NotFoundError = require('../error/notfound-error');
|
||||
const { tagTypeSchema } = require('./tag-type-schema');
|
||||
const {
|
||||
TAG_TYPE_CREATED,
|
||||
TAG_TYPE_DELETED,
|
||||
TAG_TYPE_UPDATED,
|
||||
} = require('../event-type');
|
||||
|
||||
class TagTypeService {
|
||||
constructor({ tagTypeStore, eventStore }, { getLogger }) {
|
||||
this.tagTypeStore = tagTypeStore;
|
||||
this.eventStore = eventStore;
|
||||
this.logger = getLogger('services/tag-type-service.js');
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
return this.tagTypeStore.getAll();
|
||||
}
|
||||
|
||||
async getTagType(name) {
|
||||
return this.tagTypeStore.getTagType(name);
|
||||
}
|
||||
|
||||
async createTagType(newTagType, userName) {
|
||||
const data = await tagTypeSchema.validateAsync(newTagType);
|
||||
await this.validateUnique(newTagType);
|
||||
await this.tagTypeStore.createTagType(data);
|
||||
await this.eventStore.store({
|
||||
type: TAG_TYPE_CREATED,
|
||||
createdBy: userName || 'unleash-system',
|
||||
data,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
async validateUnique({ name }) {
|
||||
try {
|
||||
await this.tagTypeStore.getTagType(name);
|
||||
} catch (err) {
|
||||
if (err instanceof NotFoundError) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new NameExistsError(
|
||||
`There already exists a tag-type with the name ${name}`,
|
||||
);
|
||||
}
|
||||
|
||||
async validate(tagType) {
|
||||
await tagTypeSchema.validateAsync(tagType);
|
||||
await this.validateUnique(tagType);
|
||||
}
|
||||
|
||||
async deleteTagType(name, userName) {
|
||||
await this.tagTypeStore.deleteTagType(name);
|
||||
await this.eventStore.store({
|
||||
type: TAG_TYPE_DELETED,
|
||||
createdBy: userName || 'unleash-system',
|
||||
data: { name },
|
||||
});
|
||||
}
|
||||
|
||||
async updateTagType(updatedTagType, userName) {
|
||||
const data = await tagTypeSchema.validateAsync(updatedTagType);
|
||||
await this.tagTypeStore.updateTagType(data);
|
||||
await this.eventStore.store({
|
||||
type: TAG_TYPE_UPDATED,
|
||||
createdBy: userName || 'unleash-system',
|
||||
data,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TagTypeService;
|
@ -25,14 +25,17 @@ test.serial('creates new feature toggle with createdBy', async t => {
|
||||
});
|
||||
|
||||
// create toggle
|
||||
await request.post('/api/admin/features').send({
|
||||
name: 'com.test.Username',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
});
|
||||
await request
|
||||
.post('/api/admin/features')
|
||||
.send({
|
||||
name: 'com.test.Username',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
await request.get('/api/admin/events').expect(res => {
|
||||
t.true(res.body.events[0].createdBy === 'user@mail.com');
|
||||
t.is(res.body.events[0].createdBy, 'user@mail.com');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -52,13 +52,16 @@ test.serial('creates new feature toggle with createdBy', async t => {
|
||||
const request = await setupAppWithCustomAuth(stores, preHook);
|
||||
|
||||
// create toggle
|
||||
await request.post('/api/admin/features').send({
|
||||
name: 'com.test.Username',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
});
|
||||
await request
|
||||
.post('/api/admin/features')
|
||||
.send({
|
||||
name: 'com.test.Username',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
await request.get('/api/admin/events').expect(res => {
|
||||
t.true(res.body.events[0].createdBy === user.email);
|
||||
t.is(res.body.events[0].createdBy, user.email);
|
||||
});
|
||||
});
|
||||
|
@ -90,11 +90,14 @@ test.serial('fetch feature toggle with variants', async t => {
|
||||
test.serial('creates new feature toggle with createdBy unknown', async t => {
|
||||
t.plan(1);
|
||||
const request = await setupApp(stores);
|
||||
await request.post('/api/admin/features').send({
|
||||
name: 'com.test.Username',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
});
|
||||
await request
|
||||
.post('/api/admin/features')
|
||||
.send({
|
||||
name: 'com.test.Username',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
})
|
||||
.expect(201);
|
||||
await request.get('/api/admin/events').expect(res => {
|
||||
t.is(res.body.events[0].createdBy, 'unknown');
|
||||
});
|
||||
@ -346,13 +349,11 @@ test.serial('can untag feature', async t => {
|
||||
.post('/api/admin/features/test.feature/tags')
|
||||
.send(tag)
|
||||
.expect(201);
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
await request
|
||||
.delete(
|
||||
`/api/admin/features/test.feature/tags/${tag.type}/${tag.value}`,
|
||||
)
|
||||
.expect(200);
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
return request
|
||||
.get('/api/admin/features/test.feature/tags')
|
||||
.expect('Content-Type', /json/)
|
||||
|
@ -63,12 +63,15 @@ test.serial('should delete application', async t => {
|
||||
});
|
||||
});
|
||||
|
||||
test.serial('should get 409 when deleting unknwn application', async t => {
|
||||
t.plan(1);
|
||||
const request = await setupApp(stores);
|
||||
return request
|
||||
.delete('/api/admin/metrics/applications/unkown')
|
||||
.expect(res => {
|
||||
t.is(res.status, 409);
|
||||
});
|
||||
});
|
||||
test.serial(
|
||||
'deleting an application should be idempotent, so expect 200',
|
||||
async t => {
|
||||
t.plan(1);
|
||||
const request = await setupApp(stores);
|
||||
return request
|
||||
.delete('/api/admin/metrics/applications/unknown')
|
||||
.expect(res => {
|
||||
t.is(res.status, 200);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
@ -55,7 +55,6 @@ test.serial('Can create a new tag type', async t => {
|
||||
icon:
|
||||
'http://icons.iconarchive.com/icons/papirus-team/papirus-apps/32/slack-icon.png',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
return request
|
||||
.get('/api/admin/tag-types/slack')
|
||||
.expect('Content-Type', /json/)
|
||||
@ -94,7 +93,6 @@ test.serial('Can update a tag types description and icon', async t => {
|
||||
icon: '$',
|
||||
})
|
||||
.expect(200);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
return request
|
||||
.get('/api/admin/tag-types/simple')
|
||||
.expect('Content-Type', /json/)
|
||||
@ -160,7 +158,6 @@ test.serial('Can delete tag type', async t => {
|
||||
.delete('/api/admin/tag-types/simple')
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(200);
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
return request.get('/api/admin/tag-types/simple').expect(404);
|
||||
});
|
||||
|
||||
@ -175,7 +172,6 @@ test.serial('Non unique tag-types gets rejected', async t => {
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201);
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
return request
|
||||
.post('/api/admin/tag-types')
|
||||
.send({
|
||||
|
@ -31,7 +31,7 @@ async function resetDatabase(stores) {
|
||||
}
|
||||
|
||||
function createStrategies(store) {
|
||||
return dbState.strategies.map(s => store._createStrategy(s));
|
||||
return dbState.strategies.map(s => store.createStrategy(s));
|
||||
}
|
||||
|
||||
function createContextFields(store) {
|
||||
@ -51,13 +51,13 @@ function createProjects(store) {
|
||||
}
|
||||
|
||||
function createFeatures(store) {
|
||||
return dbState.features.map(f => store._createFeature(f));
|
||||
return dbState.features.map(f => store.createFeature(f));
|
||||
}
|
||||
|
||||
function tagFeatures(store) {
|
||||
async function tagFeatures(tagStore, store) {
|
||||
await tagStore.createTag({ value: 'Tester', type: 'simple' });
|
||||
return dbState.features.map(f =>
|
||||
store.tagFeature({
|
||||
featureName: f.name,
|
||||
store.tagFeature(f.name, {
|
||||
value: 'Tester',
|
||||
type: 'simple',
|
||||
}),
|
||||
@ -65,7 +65,7 @@ function tagFeatures(store) {
|
||||
}
|
||||
|
||||
function createTagTypes(store) {
|
||||
return dbState.tag_types.map(t => store._createTagType(t));
|
||||
return dbState.tag_types.map(t => store.createTagType(t));
|
||||
}
|
||||
|
||||
async function setupDatabase(stores) {
|
||||
@ -76,7 +76,7 @@ async function setupDatabase(stores) {
|
||||
await Promise.all(createApplications(stores.clientApplicationsStore));
|
||||
await Promise.all(createProjects(stores.projectStore));
|
||||
await Promise.all(createTagTypes(stores.tagTypeStore));
|
||||
await Promise.all(tagFeatures(stores.featureTagStore));
|
||||
await tagFeatures(stores.tagStore, stores.featureTagStore);
|
||||
}
|
||||
|
||||
module.exports = async function init(databaseSchema = 'test', getLogger) {
|
||||
|
@ -72,7 +72,7 @@ test.serial('should not be able to delete project with toggles', async t => {
|
||||
description: 'Blah',
|
||||
};
|
||||
await projectService.createProject(project, 'some-user');
|
||||
await stores.featureToggleStore._createFeature({
|
||||
await stores.featureToggleStore.createFeature({
|
||||
name: 'test-project-delete',
|
||||
project: project.id,
|
||||
enabled: false,
|
@ -1,5 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const NotFoundError = require('../../lib/error/notfound-error');
|
||||
|
||||
module.exports = () => {
|
||||
let apps = [];
|
||||
|
||||
@ -8,11 +10,15 @@ module.exports = () => {
|
||||
apps.push(app);
|
||||
return Promise.resolve();
|
||||
},
|
||||
insertNewRow: value => {
|
||||
apps.push(value);
|
||||
return Promise.resolve();
|
||||
},
|
||||
getApplications: () => Promise.resolve(apps),
|
||||
getApplication: appName => {
|
||||
const app = apps.filter(a => a.appName === appName)[0];
|
||||
if (!app) {
|
||||
throw new Error(`Could not find app=${appName}`);
|
||||
throw new NotFoundError(`Could not find app=${appName}`);
|
||||
}
|
||||
return app;
|
||||
},
|
||||
|
38
test/fixtures/fake-feature-tag-store.js
vendored
38
test/fixtures/fake-feature-tag-store.js
vendored
@ -1,43 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const NotFoundError = require('../../lib/error/notfound-error');
|
||||
|
||||
module.exports = () => {
|
||||
const _tags = [];
|
||||
const _featureTags = {};
|
||||
return {
|
||||
getAllOfType: type => {
|
||||
const tags = _tags.filter(t => t.type === type);
|
||||
return Promise.resolve(tags);
|
||||
},
|
||||
addTag: tag => {
|
||||
_tags.push({ value: tag.value, type: tag.type });
|
||||
},
|
||||
removeTag: tag => {
|
||||
_tags.splice(
|
||||
_tags.indexOf(
|
||||
t => t.value === tag.value && t.type === tag.type,
|
||||
),
|
||||
1,
|
||||
);
|
||||
},
|
||||
getTags: () => Promise.resolve(_tags),
|
||||
getTagByTypeAndValue: (type, value) => {
|
||||
const tag = _tags.find(t => t.type === type && t.value === value);
|
||||
if (tag) {
|
||||
return Promise.resolve(tag);
|
||||
}
|
||||
return Promise.reject(new NotFoundError('Could not find tag'));
|
||||
},
|
||||
tagFeature: event => {
|
||||
_featureTags[event.featureName] =
|
||||
_featureTags[event.featureName] || [];
|
||||
const tag = {
|
||||
value: event.value,
|
||||
type: event.type,
|
||||
};
|
||||
_featureTags[event.featureName].push(tag);
|
||||
return tag;
|
||||
tagFeature: (featureName, tag) => {
|
||||
_featureTags[featureName] = _featureTags[featureName] || [];
|
||||
_featureTags[featureName].push(tag);
|
||||
},
|
||||
untagFeature: event => {
|
||||
const tags = _featureTags[event.featureName];
|
||||
|
22
test/fixtures/fake-feature-toggle-store.js
vendored
22
test/fixtures/fake-feature-toggle-store.js
vendored
@ -22,10 +22,25 @@ module.exports = () => {
|
||||
}
|
||||
return Promise.reject();
|
||||
},
|
||||
updateFeature: updatedFeature => {
|
||||
_features.splice(
|
||||
_features.indexOf(f => f.name === updatedFeature.name),
|
||||
1,
|
||||
);
|
||||
_features.push(updatedFeature);
|
||||
},
|
||||
getFeatures: () => Promise.resolve(_features),
|
||||
addFeature: feature => _features.push(feature),
|
||||
createFeature: feature => _features.push(feature),
|
||||
getArchivedFeatures: () => Promise.resolve(_archive),
|
||||
addArchivedFeature: feature => _archive.push(feature),
|
||||
reviveFeature: feature => {
|
||||
const revived = _archive.find(f => f.name === feature.name);
|
||||
_archive.splice(
|
||||
_archive.indexOf(f => f.name === feature.name),
|
||||
1,
|
||||
);
|
||||
_features.push(revived);
|
||||
},
|
||||
lastSeenToggles: (names = []) => {
|
||||
names.forEach(name => {
|
||||
const toggle = _features.find(f => f.name === name);
|
||||
@ -34,5 +49,10 @@ module.exports = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
dropFeatures: () => {
|
||||
_features.splice(0, _features.length);
|
||||
_archive.splice(0, _archive.length);
|
||||
},
|
||||
importFeature: feat => Promise.resolve(_features.push(feat)),
|
||||
};
|
||||
};
|
||||
|
16
test/fixtures/fake-strategies-store.js
vendored
16
test/fixtures/fake-strategies-store.js
vendored
@ -16,6 +16,20 @@ module.exports = () => {
|
||||
}
|
||||
return Promise.reject(new NotFoundError('Not found!'));
|
||||
},
|
||||
addStrategy: strat => _strategies.push(strat),
|
||||
createStrategy: strat => _strategies.push(strat),
|
||||
updateStrategy: strat => {
|
||||
_strategies.splice(
|
||||
_strategies.indexOf(({ name }) => name === strat.name),
|
||||
1,
|
||||
);
|
||||
_strategies.push(strat);
|
||||
},
|
||||
importStrategy: strat => Promise.resolve(_strategies.push(strat)),
|
||||
dropStrategies: () => _strategies.splice(0, _strategies.length),
|
||||
deleteStrategy: strat =>
|
||||
_strategies.splice(
|
||||
_strategies.indexOf(({ name }) => name === strat.name),
|
||||
1,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
30
test/fixtures/fake-tag-store.js
vendored
Normal file
30
test/fixtures/fake-tag-store.js
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
const NotFoundError = require('../../lib/error/notfound-error');
|
||||
|
||||
module.exports = () => {
|
||||
const _tags = [];
|
||||
return {
|
||||
getTagsByType: type => {
|
||||
const tags = _tags.filter(t => t.type === type);
|
||||
return Promise.resolve(tags);
|
||||
},
|
||||
createTag: tag => {
|
||||
_tags.push({ value: tag.value, type: tag.type });
|
||||
},
|
||||
deleteTag: tag => {
|
||||
_tags.splice(
|
||||
_tags.indexOf(
|
||||
t => t.value === tag.value && t.type === tag.type,
|
||||
),
|
||||
1,
|
||||
);
|
||||
},
|
||||
getAll: () => Promise.resolve(_tags),
|
||||
getTag: (type, value) => {
|
||||
const tag = _tags.find(t => t.type === type && t.value === value);
|
||||
if (tag) {
|
||||
return Promise.resolve(tag);
|
||||
}
|
||||
return Promise.reject(new NotFoundError('Could not find tag'));
|
||||
},
|
||||
};
|
||||
};
|
2
test/fixtures/store.js
vendored
2
test/fixtures/store.js
vendored
@ -5,6 +5,7 @@ const clientInstanceStore = require('./fake-client-instance-store');
|
||||
const clientApplicationsStore = require('./fake-client-applications-store');
|
||||
const featureToggleStore = require('./fake-feature-toggle-store');
|
||||
const featureTagStore = require('./fake-feature-tag-store');
|
||||
const tagStore = require('./fake-tag-store');
|
||||
const eventStore = require('./fake-event-store');
|
||||
const strategyStore = require('./fake-strategies-store');
|
||||
const contextFieldStore = require('./fake-context-store');
|
||||
@ -25,6 +26,7 @@ module.exports = {
|
||||
clientInstanceStore: clientInstanceStore(),
|
||||
featureToggleStore: featureToggleStore(),
|
||||
featureTagStore: featureTagStore(),
|
||||
tagStore: tagStore(),
|
||||
eventStore: eventStore(),
|
||||
strategyStore: strategyStore(),
|
||||
contextFieldStore: contextFieldStore(),
|
||||
|
Loading…
Reference in New Issue
Block a user