mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-19 17:52:45 +02:00
Merge pull request #687 from Unleash/feat-685-service-layer
Service layer
This commit is contained in:
commit
e9c233c188
@ -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');
|
|
||||||
try {
|
|
||||||
const { description, icon } = this.eventDataToRow(event);
|
|
||||||
await this.db(TABLE)
|
|
||||||
.where({ name: event.name })
|
|
||||||
.update({ description, icon });
|
|
||||||
stopTimer();
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error('Could not update tag type, error: ', err);
|
|
||||||
stopTimer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _deleteTagType(event) {
|
|
||||||
const stopTimer = this.timer('deleteTagType');
|
const stopTimer = this.timer('deleteTagType');
|
||||||
try {
|
|
||||||
const data = this.eventDataToRow(event);
|
|
||||||
await this.db(TABLE)
|
await this.db(TABLE)
|
||||||
.where({
|
.where({ name })
|
||||||
name: data.name,
|
|
||||||
})
|
|
||||||
.del();
|
.del();
|
||||||
await this.eventStore.store({
|
stopTimer();
|
||||||
type: TAG_TYPE_DELETED,
|
|
||||||
createdBy: event.createdBy || 'unleash-system',
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error('Could not delete tag, error: ', err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateTagType({ name, description, icon }) {
|
||||||
|
const stopTimer = this.timer('updateTagType');
|
||||||
|
await this.db(TABLE)
|
||||||
|
.where({ name })
|
||||||
|
.update({ description, icon });
|
||||||
stopTimer();
|
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,28 +15,19 @@ 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);
|
||||||
|
switch (e.name) {
|
||||||
|
case 'ValidationError':
|
||||||
|
return res.status(400).end();
|
||||||
|
default:
|
||||||
return res.status(500).end();
|
return res.status(500).end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = ClientMetricsController;
|
module.exports = ClientMetricsController;
|
||||||
|
@ -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,42 +1,31 @@
|
|||||||
'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);
|
||||||
|
switch (err.name) {
|
||||||
|
case 'ValidationError':
|
||||||
|
return res.status(400).end();
|
||||||
|
default:
|
||||||
return res.status(500).end();
|
return res.status(500).end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = RegisterController;
|
module.exports = RegisterController;
|
||||||
|
@ -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,12 +77,14 @@ class StateService {
|
|||||||
.filter(filterExisitng(keepExisting, oldToggles))
|
.filter(filterExisitng(keepExisting, oldToggles))
|
||||||
.filter(filterEqual(oldToggles))
|
.filter(filterEqual(oldToggles))
|
||||||
.map(feature =>
|
.map(feature =>
|
||||||
|
this.toggleStore.importFeature(feature).then(() =>
|
||||||
this.eventStore.store({
|
this.eventStore.store({
|
||||||
type: FEATURE_IMPORT,
|
type: FEATURE_IMPORT,
|
||||||
createdBy: userName,
|
createdBy: userName,
|
||||||
data: feature,
|
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.strategyStore.importStrategy(strategy).then(() => {
|
||||||
this.eventStore.store({
|
this.eventStore.store({
|
||||||
type: STRATEGY_IMPORT,
|
type: STRATEGY_IMPORT,
|
||||||
createdBy: userName,
|
createdBy: userName,
|
||||||
data: strategy,
|
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
|
||||||
|
.post('/api/admin/features')
|
||||||
|
.send({
|
||||||
name: 'com.test.Username',
|
name: 'com.test.Username',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
strategies: [{ name: 'default' }],
|
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
|
||||||
|
.post('/api/admin/features')
|
||||||
|
.send({
|
||||||
name: 'com.test.Username',
|
name: 'com.test.Username',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
strategies: [{ name: 'default' }],
|
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
|
||||||
|
.post('/api/admin/features')
|
||||||
|
.send({
|
||||||
name: 'com.test.Username',
|
name: 'com.test.Username',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
strategies: [{ name: 'default' }],
|
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(
|
||||||
|
'deleting an application should be idempotent, so expect 200',
|
||||||
|
async t => {
|
||||||
t.plan(1);
|
t.plan(1);
|
||||||
const request = await setupApp(stores);
|
const request = await setupApp(stores);
|
||||||
return request
|
return request
|
||||||
.delete('/api/admin/metrics/applications/unkown')
|
.delete('/api/admin/metrics/applications/unknown')
|
||||||
.expect(res => {
|
.expect(res => {
|
||||||
t.is(res.status, 409);
|
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