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