mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +01:00
Merge pull request #693 from Unleash/feat-690-toggle-filtering
feat: #690 toggle filtering
This commit is contained in:
commit
00c7d634d2
@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 3.11
|
||||||
|
|
||||||
|
- feat: Add support for filtering toggles on tags, projects or namePrefix (#690)
|
||||||
|
|
||||||
## 3.10.1
|
## 3.10.1
|
||||||
|
|
||||||
- fix: remove fields from /api/client/features respnse (#692)
|
- fix: remove fields from /api/client/features respnse (#692)
|
||||||
|
@ -73,6 +73,28 @@ This endpoint is the one all admin ui should use to fetch all available feature
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Filter feature toggles
|
||||||
|
|
||||||
|
Supports three params for now
|
||||||
|
|
||||||
|
- `tag` - filters for features tagged with tag
|
||||||
|
- `project` - filters for features belonging to project
|
||||||
|
- `namePrefix` - filters for features beginning with prefix
|
||||||
|
|
||||||
|
For `tag` and `project` performs OR filtering if multiple arguments
|
||||||
|
|
||||||
|
To filter for any feature tagged with a `simple` tag with value `taga` or a `simple` tag with value `tagb` use
|
||||||
|
|
||||||
|
`GET https://unleash.host.com/api/admin/features?tag[]=simple:taga&tag[]simple:tagb`
|
||||||
|
|
||||||
|
To filter for any feature belonging to project `myproject` use
|
||||||
|
|
||||||
|
`GET https://unleash.host.com/api/admin/features?project=myproject`
|
||||||
|
|
||||||
|
Response format is the same as `api/admin/features`
|
||||||
|
|
||||||
|
### Fetch specific feature toggle
|
||||||
|
|
||||||
`GET: http://unleash.host.com/api/admin/features/:featureName`
|
`GET: http://unleash.host.com/api/admin/features/:featureName`
|
||||||
|
|
||||||
Used to fetch details about a specific featureToggle. This is mostly provded to make it easy to debug the API and should not be used by the client implementations.
|
Used to fetch details about a specific featureToggle. This is mostly provded to make it easy to debug the API and should not be used by the client implementations.
|
||||||
|
@ -68,7 +68,27 @@ This endpoint should never return anything besides a valid _20X or 304-response_
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
You may limit the response by sending a `namePrefix` query-parameter.
|
#### Filter feature toggles
|
||||||
|
|
||||||
|
Supports three params for now
|
||||||
|
|
||||||
|
- `tag` - filters for features tagged with tag
|
||||||
|
- `project` - filters for features belonging to project
|
||||||
|
- `namePrefix` - filters for features beginning with prefix
|
||||||
|
|
||||||
|
For `tag` and `project` performs OR filtering if multiple arguments
|
||||||
|
|
||||||
|
To filter for any feature tagged with a `simple` tag with value `taga` or a `simple` tag with value `tagb` use
|
||||||
|
|
||||||
|
`GET https://unleash.host.com/api/client/features?tag[]=simple:taga&tag[]simple:tagb`
|
||||||
|
|
||||||
|
To filter for any feature belonging to project `myproject` use
|
||||||
|
|
||||||
|
`GET https://unleash.host.com/api/client/features?project=myproject`
|
||||||
|
|
||||||
|
Response format is the same as `api/client/features`
|
||||||
|
|
||||||
|
### Get specific feature toggle
|
||||||
|
|
||||||
`GET: http://unleash.host.com/api/client/features/:featureName`
|
`GET: http://unleash.host.com/api/client/features/:featureName`
|
||||||
|
|
||||||
|
@ -1,82 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const metricsHelper = require('../metrics-helper');
|
|
||||||
const { DB_TIME } = require('../events');
|
|
||||||
|
|
||||||
const FEATURE_TAG_COLUMNS = ['feature_name', 'tag_type', 'tag_value'];
|
|
||||||
const FEATURE_TAG_TABLE = 'feature_tag';
|
|
||||||
|
|
||||||
class FeatureTagStore {
|
|
||||||
constructor(db, eventBus, getLogger) {
|
|
||||||
this.db = db;
|
|
||||||
this.logger = getLogger('feature-tag-store.js');
|
|
||||||
|
|
||||||
this.timer = action =>
|
|
||||||
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
|
||||||
store: 'tag',
|
|
||||||
action,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAllTagsForFeature(featureName) {
|
|
||||||
const stopTimer = this.timer('getAllForFeature');
|
|
||||||
const rows = await this.db
|
|
||||||
.select(FEATURE_TAG_COLUMNS)
|
|
||||||
.from(FEATURE_TAG_TABLE)
|
|
||||||
.where({ feature_name: featureName });
|
|
||||||
stopTimer();
|
|
||||||
return rows.map(this.featureTagRowToTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
async tagFeature(featureName, tag) {
|
|
||||||
const stopTimer = this.timer('tagFeature');
|
|
||||||
await this.db(FEATURE_TAG_TABLE)
|
|
||||||
.insert(this.featureAndTagToRow(featureName, tag))
|
|
||||||
.onConflict(['feature_name', 'tag_type', 'tag_value'])
|
|
||||||
.ignore();
|
|
||||||
stopTimer();
|
|
||||||
return tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
async untagFeature(featureName, tag) {
|
|
||||||
const stopTimer = this.timer('untagFeature');
|
|
||||||
try {
|
|
||||||
await this.db(FEATURE_TAG_TABLE)
|
|
||||||
.where(this.featureAndTagToRow(featureName, tag))
|
|
||||||
.delete();
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(err);
|
|
||||||
}
|
|
||||||
stopTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
rowToTag(row) {
|
|
||||||
if (row) {
|
|
||||||
return {
|
|
||||||
value: row.value,
|
|
||||||
type: row.type,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
featureTagRowToTag(row) {
|
|
||||||
if (row) {
|
|
||||||
return {
|
|
||||||
value: row.tag_value,
|
|
||||||
type: row.tag_type,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
featureAndTagToRow(featureName, { type, value }) {
|
|
||||||
return {
|
|
||||||
feature_name: featureName,
|
|
||||||
tag_type: type,
|
|
||||||
tag_value: value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = FeatureTagStore;
|
|
@ -16,16 +16,15 @@ const FEATURE_COLUMNS = [
|
|||||||
'created_at',
|
'created_at',
|
||||||
'last_seen_at',
|
'last_seen_at',
|
||||||
];
|
];
|
||||||
const TABLE = 'features';
|
|
||||||
|
|
||||||
const FEATURE_COLUMNS_CLIENT = [
|
const mapperToColumnNames = {
|
||||||
'name',
|
createdAt: 'created_at',
|
||||||
'type',
|
lastSeenAt: 'last_seen_at',
|
||||||
'enabled',
|
};
|
||||||
'stale',
|
const TABLE = 'features';
|
||||||
'strategies',
|
const FEATURE_TAG_COLUMNS = ['feature_name', 'tag_type', 'tag_value'];
|
||||||
'variants',
|
const FEATURE_TAG_FILTER_COLUMNS = ['tag_type', 'tag_value'];
|
||||||
];
|
const FEATURE_TAG_TABLE = 'feature_tag';
|
||||||
|
|
||||||
class FeatureToggleStore {
|
class FeatureToggleStore {
|
||||||
constructor(db, eventBus, getLogger) {
|
constructor(db, eventBus, getLogger) {
|
||||||
@ -38,43 +37,36 @@ class FeatureToggleStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFeatures() {
|
async getFeatures(query = {}, fields = FEATURE_COLUMNS) {
|
||||||
const stopTimer = this.timer('getAll');
|
const stopTimer = this.timer('getAll');
|
||||||
|
const queryFields = fields.map(f =>
|
||||||
const rows = await this.db
|
mapperToColumnNames[f] ? mapperToColumnNames[f] : f,
|
||||||
.select(FEATURE_COLUMNS)
|
);
|
||||||
|
let baseQuery = this.db
|
||||||
|
.select(queryFields)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
.where({ archived: 0 })
|
.where({ archived: 0 })
|
||||||
.orderBy('name', 'asc');
|
.orderBy('name', 'asc');
|
||||||
|
if (query) {
|
||||||
stopTimer();
|
if (query.tag) {
|
||||||
|
const tagQuery = this.db
|
||||||
return rows.map(this.rowToFeature);
|
.from('feature_tag')
|
||||||
}
|
.select('feature_name')
|
||||||
|
.whereIn(FEATURE_TAG_FILTER_COLUMNS, query.tag);
|
||||||
async getFeaturesClient() {
|
baseQuery = baseQuery.whereIn('name', tagQuery);
|
||||||
const stopTimer = this.timer('getAllClient');
|
}
|
||||||
|
if (query.project) {
|
||||||
const rows = await this.db
|
baseQuery = baseQuery.whereIn('project', query.project);
|
||||||
.select(FEATURE_COLUMNS_CLIENT)
|
}
|
||||||
.from(TABLE)
|
if (query.namePrefix) {
|
||||||
.where({ archived: 0 })
|
baseQuery = baseQuery.where(
|
||||||
.orderBy('name', 'asc');
|
'name',
|
||||||
|
'like',
|
||||||
stopTimer();
|
`${query.namePrefix}%`,
|
||||||
|
);
|
||||||
return rows.map(this.rowToFeature);
|
}
|
||||||
}
|
}
|
||||||
|
const rows = await baseQuery;
|
||||||
async getClientFeatures() {
|
|
||||||
const stopTimer = this.timer('getAll');
|
|
||||||
|
|
||||||
const rows = await this.db
|
|
||||||
.select(FEATURE_COLUMNS)
|
|
||||||
.from(TABLE)
|
|
||||||
.where({ archived: 0 })
|
|
||||||
.orderBy('name', 'asc');
|
|
||||||
|
|
||||||
stopTimer();
|
stopTimer();
|
||||||
|
|
||||||
return rows.map(this.rowToFeature);
|
return rows.map(this.rowToFeature);
|
||||||
@ -233,6 +225,66 @@ class FeatureToggleStore {
|
|||||||
this.logger.error('Could not drop features, error: ', err);
|
this.logger.error('Could not drop features, error: ', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAllTagsForFeature(featureName) {
|
||||||
|
const stopTimer = this.timer('getAllForFeature');
|
||||||
|
const rows = await this.db
|
||||||
|
.select(FEATURE_TAG_COLUMNS)
|
||||||
|
.from(FEATURE_TAG_TABLE)
|
||||||
|
.where({ feature_name: featureName });
|
||||||
|
stopTimer();
|
||||||
|
return rows.map(this.featureTagRowToTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
async tagFeature(featureName, tag) {
|
||||||
|
const stopTimer = this.timer('tagFeature');
|
||||||
|
await this.db(FEATURE_TAG_TABLE)
|
||||||
|
.insert(this.featureAndTagToRow(featureName, tag))
|
||||||
|
.onConflict(['feature_name', 'tag_type', 'tag_value'])
|
||||||
|
.ignore();
|
||||||
|
stopTimer();
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
async untagFeature(featureName, tag) {
|
||||||
|
const stopTimer = this.timer('untagFeature');
|
||||||
|
try {
|
||||||
|
await this.db(FEATURE_TAG_TABLE)
|
||||||
|
.where(this.featureAndTagToRow(featureName, tag))
|
||||||
|
.delete();
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(err);
|
||||||
|
}
|
||||||
|
stopTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
rowToTag(row) {
|
||||||
|
if (row) {
|
||||||
|
return {
|
||||||
|
value: row.value,
|
||||||
|
type: row.type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
featureTagRowToTag(row) {
|
||||||
|
if (row) {
|
||||||
|
return {
|
||||||
|
value: row.tag_value,
|
||||||
|
type: row.tag_type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
featureAndTagToRow(featureName, { type, value }) {
|
||||||
|
return {
|
||||||
|
feature_name: featureName,
|
||||||
|
tag_type: type,
|
||||||
|
tag_value: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = FeatureToggleStore;
|
module.exports = FeatureToggleStore;
|
||||||
|
@ -14,7 +14,6 @@ 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 TagStore = require('./tag-store');
|
||||||
const FeatureTagStore = require('./feature-tag-store');
|
|
||||||
const TagTypeStore = require('./tag-type-store');
|
const TagTypeStore = require('./tag-type-store');
|
||||||
|
|
||||||
module.exports.createStores = (config, eventBus) => {
|
module.exports.createStores = (config, eventBus) => {
|
||||||
@ -51,6 +50,5 @@ module.exports.createStores = (config, eventBus) => {
|
|||||||
projectStore: new ProjectStore(db, getLogger),
|
projectStore: new ProjectStore(db, getLogger),
|
||||||
tagStore: new TagStore(db, eventBus, getLogger),
|
tagStore: new TagStore(db, eventBus, getLogger),
|
||||||
tagTypeStore: new TagTypeStore(db, eventBus, getLogger),
|
tagTypeStore: new TagTypeStore(db, eventBus, getLogger),
|
||||||
featureTagStore: new FeatureTagStore(db, eventBus, getLogger),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -9,6 +9,18 @@ const {
|
|||||||
} = require('../../permissions');
|
} = require('../../permissions');
|
||||||
|
|
||||||
const version = 1;
|
const version = 1;
|
||||||
|
const fields = [
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'type',
|
||||||
|
'project',
|
||||||
|
'enabled',
|
||||||
|
'stale',
|
||||||
|
'strategies',
|
||||||
|
'variants',
|
||||||
|
'createdAt',
|
||||||
|
'lastSeenAt',
|
||||||
|
];
|
||||||
|
|
||||||
class FeatureController extends Controller {
|
class FeatureController extends Controller {
|
||||||
constructor(config, { featureToggleService }) {
|
constructor(config, { featureToggleService }) {
|
||||||
@ -37,7 +49,10 @@ class FeatureController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAllToggles(req, res) {
|
async getAllToggles(req, res) {
|
||||||
const features = await this.featureService.getFeatures();
|
const features = await this.featureService.getFeatures(
|
||||||
|
req.query,
|
||||||
|
fields,
|
||||||
|
);
|
||||||
res.json({ version, features });
|
res.json({ version, features });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@ function getSetup() {
|
|||||||
base,
|
base,
|
||||||
perms,
|
perms,
|
||||||
featureToggleStore: stores.featureToggleStore,
|
featureToggleStore: stores.featureToggleStore,
|
||||||
featureTagStore: stores.featureTagStore,
|
|
||||||
request: supertest(app),
|
request: supertest(app),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -383,13 +382,7 @@ test('should be able to add tag for feature', t => {
|
|||||||
});
|
});
|
||||||
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);
|
||||||
const {
|
const { request, featureToggleStore, base, perms } = getSetup();
|
||||||
request,
|
|
||||||
featureToggleStore,
|
|
||||||
featureTagStore,
|
|
||||||
base,
|
|
||||||
perms,
|
|
||||||
} = getSetup();
|
|
||||||
perms.withPermissions(UPDATE_FEATURE);
|
perms.withPermissions(UPDATE_FEATURE);
|
||||||
|
|
||||||
featureToggleStore.createFeature({
|
featureToggleStore.createFeature({
|
||||||
@ -398,7 +391,7 @@ test('should be able to get tags for feature', t => {
|
|||||||
strategies: [{ name: 'default' }],
|
strategies: [{ name: 'default' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
featureTagStore.tagFeature('toggle.disabled', {
|
featureToggleStore.tagFeature('toggle.disabled', {
|
||||||
value: 'TeamGreen',
|
value: 'TeamGreen',
|
||||||
type: 'simple',
|
type: 'simple',
|
||||||
});
|
});
|
||||||
@ -434,3 +427,99 @@ test('Invalid tag for feature should be rejected', t => {
|
|||||||
t.is(res.body.details[0].message, '"type" must be URL friendly');
|
t.is(res.body.details[0].message, '"type" must be URL friendly');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Should be able to filter on tag', t => {
|
||||||
|
t.plan(2);
|
||||||
|
const { request, featureToggleStore, base, perms } = getSetup();
|
||||||
|
perms.withPermissions(UPDATE_FEATURE);
|
||||||
|
|
||||||
|
featureToggleStore.createFeature({
|
||||||
|
name: 'toggle.tagged',
|
||||||
|
enabled: false,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
featureToggleStore.createFeature({
|
||||||
|
name: 'toggle.untagged',
|
||||||
|
enabled: false,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
featureToggleStore.tagFeature('toggle.tagged', {
|
||||||
|
type: 'simple',
|
||||||
|
value: 'mytag',
|
||||||
|
});
|
||||||
|
return request
|
||||||
|
.get(`${base}/api/admin/features?tag=simple:mytag`)
|
||||||
|
.expect(200)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(res => {
|
||||||
|
t.is(res.body.features.length, 1);
|
||||||
|
t.is(res.body.features[0].name, 'toggle.tagged');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should be able to filter on name prefix', t => {
|
||||||
|
t.plan(3);
|
||||||
|
const { request, featureToggleStore, base, perms } = getSetup();
|
||||||
|
perms.withPermissions(UPDATE_FEATURE);
|
||||||
|
|
||||||
|
featureToggleStore.createFeature({
|
||||||
|
name: 'a_team.toggle',
|
||||||
|
enabled: false,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
featureToggleStore.createFeature({
|
||||||
|
name: 'a_tag.toggle',
|
||||||
|
enabled: false,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
featureToggleStore.createFeature({
|
||||||
|
name: 'b_tag.toggle',
|
||||||
|
enabled: false,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
return request
|
||||||
|
.get(`${base}/api/admin/features?namePrefix=a_`)
|
||||||
|
.expect(200)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(res => {
|
||||||
|
t.is(res.body.features.length, 2);
|
||||||
|
t.is(res.body.features[0].name, 'a_team.toggle');
|
||||||
|
t.is(res.body.features[1].name, 'a_tag.toggle');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should be able to filter on project', t => {
|
||||||
|
t.plan(3);
|
||||||
|
const { request, featureToggleStore, base, perms } = getSetup();
|
||||||
|
perms.withPermissions(UPDATE_FEATURE);
|
||||||
|
|
||||||
|
featureToggleStore.createFeature({
|
||||||
|
name: 'a_team.toggle',
|
||||||
|
enabled: false,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
project: 'projecta',
|
||||||
|
});
|
||||||
|
featureToggleStore.createFeature({
|
||||||
|
name: 'a_tag.toggle',
|
||||||
|
enabled: false,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
project: 'projecta',
|
||||||
|
});
|
||||||
|
featureToggleStore.createFeature({
|
||||||
|
name: 'b_tag.toggle',
|
||||||
|
enabled: false,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
project: 'projectb',
|
||||||
|
});
|
||||||
|
return request
|
||||||
|
.get(`${base}/api/admin/features?project=projecta`)
|
||||||
|
.expect(200)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(res => {
|
||||||
|
t.is(res.body.features.length, 2);
|
||||||
|
t.is(res.body.features[0].name, 'a_team.toggle');
|
||||||
|
t.is(res.body.features[1].name, 'a_tag.toggle');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const Controller = require('../controller');
|
const Controller = require('../controller');
|
||||||
const { filter } = require('./util');
|
|
||||||
|
|
||||||
const version = 1;
|
const version = 1;
|
||||||
|
|
||||||
|
const FEATURE_COLUMNS_CLIENT = [
|
||||||
|
'name',
|
||||||
|
'type',
|
||||||
|
'enabled',
|
||||||
|
'stale',
|
||||||
|
'strategies',
|
||||||
|
'variants',
|
||||||
|
];
|
||||||
|
|
||||||
class FeatureController extends Controller {
|
class FeatureController extends Controller {
|
||||||
constructor({ featureToggleService }, getLogger) {
|
constructor({ featureToggleService }, getLogger) {
|
||||||
super();
|
super();
|
||||||
@ -15,11 +23,10 @@ class FeatureController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAll(req, res) {
|
async getAll(req, res) {
|
||||||
const nameFilter = filter('name', req.query.namePrefix);
|
const features = await this.toggleService.getFeatures(
|
||||||
|
req.query,
|
||||||
const allFeatureToggles = await this.toggleService.getFeaturesClient();
|
FEATURE_COLUMNS_CLIENT,
|
||||||
const features = nameFilter(allFeatureToggles);
|
);
|
||||||
|
|
||||||
res.json({ version, features });
|
res.json({ version, features });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +72,22 @@ test('support name prefix', t => {
|
|||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect(res => {
|
.expect(res => {
|
||||||
t.true(res.body.features.length === 2);
|
t.is(res.body.features.length, 2);
|
||||||
t.true(res.body.features[1].name === 'b_test2');
|
t.is(res.body.features[1].name, 'b_test2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('support filtering on project', t => {
|
||||||
|
t.plan(2);
|
||||||
|
const { request, featureToggleStore, base } = getSetup();
|
||||||
|
featureToggleStore.createFeature({ name: 'a_test1', project: 'projecta' });
|
||||||
|
featureToggleStore.createFeature({ name: 'b_test2', project: 'projectb' });
|
||||||
|
return request
|
||||||
|
.get(`${base}/api/client/features?project=projecta`)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.is(res.body.features.length, 1);
|
||||||
|
t.is(res.body.features[0].name, 'a_test1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const filter = (key, value) => {
|
|
||||||
if (!key || !value) return array => array;
|
|
||||||
return array => array.filter(item => item[key].startsWith(value));
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
filter,
|
|
||||||
};
|
|
@ -88,4 +88,24 @@ const featureSchema = joi
|
|||||||
})
|
})
|
||||||
.options({ allowUnknown: false, stripUnknown: true, abortEarly: false });
|
.options({ allowUnknown: false, stripUnknown: true, abortEarly: false });
|
||||||
|
|
||||||
module.exports = { featureSchema, strategiesSchema, nameSchema };
|
const querySchema = joi
|
||||||
|
.object()
|
||||||
|
.keys({
|
||||||
|
tag: joi
|
||||||
|
.array()
|
||||||
|
.allow(null)
|
||||||
|
.items(joi.string().pattern(/\w+:.+/, { name: 'tag' }))
|
||||||
|
.optional(),
|
||||||
|
project: joi
|
||||||
|
.array()
|
||||||
|
.allow(null)
|
||||||
|
.items(joi.string().alphanum())
|
||||||
|
.optional(),
|
||||||
|
namePrefix: joi
|
||||||
|
.string()
|
||||||
|
.allow(null)
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.options({ allowUnknown: false, stripUnknown: true, abortEarly: false });
|
||||||
|
|
||||||
|
module.exports = { featureSchema, strategiesSchema, nameSchema, querySchema };
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const test = require('ava');
|
||||||
const { featureSchema } = require('./feature-schema');
|
const { featureSchema, querySchema } = require('./feature-schema');
|
||||||
|
|
||||||
test('should require URL firendly name', t => {
|
test('should require URL firendly name', t => {
|
||||||
const toggle = {
|
const toggle = {
|
||||||
@ -227,3 +227,41 @@ test('should not accept empty list of constraint values', t => {
|
|||||||
'"strategies[0].constraints[0].values" must contain at least 1 items',
|
'"strategies[0].constraints[0].values" must contain at least 1 items',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Filter queries should accept a list of tag values', t => {
|
||||||
|
const query = {
|
||||||
|
tag: ['simple:valuea', 'simple:valueb'],
|
||||||
|
};
|
||||||
|
const { value } = querySchema.validate(query);
|
||||||
|
t.deepEqual(value, { tag: ['simple:valuea', 'simple:valueb'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Filter queries should reject tag values with missing type prefix', t => {
|
||||||
|
const query = {
|
||||||
|
tag: ['simple', 'simple'],
|
||||||
|
};
|
||||||
|
const { error } = querySchema.validate(query);
|
||||||
|
t.deepEqual(
|
||||||
|
error.details[0].message,
|
||||||
|
'"tag[0]" with value "simple" fails to match the tag pattern',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Filter queries should allow project names', t => {
|
||||||
|
const query = {
|
||||||
|
project: ['projecta'],
|
||||||
|
};
|
||||||
|
const { value } = querySchema.validate(query);
|
||||||
|
t.deepEqual(value, { project: ['projecta'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Filter queries should reject project names that are not alphanum', t => {
|
||||||
|
const query = {
|
||||||
|
project: ['project name with space'],
|
||||||
|
};
|
||||||
|
const { error } = querySchema.validate(query);
|
||||||
|
t.deepEqual(
|
||||||
|
error.details[0].message,
|
||||||
|
'"project[0]" must only contain alpha-numeric characters',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const { FEATURE_TAGGED, FEATURE_UNTAGGED } = require('../event-type');
|
const { FEATURE_TAGGED, FEATURE_UNTAGGED } = require('../event-type');
|
||||||
const { featureSchema, nameSchema } = require('./feature-schema');
|
const { featureSchema, nameSchema, querySchema } = require('./feature-schema');
|
||||||
const { tagSchema } = require('./tag-schema');
|
const { tagSchema } = require('./tag-schema');
|
||||||
const NameExistsError = require('../error/name-exists-error');
|
const NameExistsError = require('../error/name-exists-error');
|
||||||
const NotFoundError = require('../error/notfound-error');
|
const NotFoundError = require('../error/notfound-error');
|
||||||
@ -12,23 +12,40 @@ const {
|
|||||||
} = require('../event-type');
|
} = require('../event-type');
|
||||||
|
|
||||||
class FeatureToggleService {
|
class FeatureToggleService {
|
||||||
constructor(
|
constructor({ featureToggleStore, tagStore, eventStore }, { getLogger }) {
|
||||||
{ featureToggleStore, featureTagStore, tagStore, eventStore },
|
|
||||||
{ getLogger },
|
|
||||||
) {
|
|
||||||
this.featureToggleStore = featureToggleStore;
|
this.featureToggleStore = featureToggleStore;
|
||||||
this.tagStore = tagStore;
|
this.tagStore = tagStore;
|
||||||
this.eventStore = eventStore;
|
this.eventStore = eventStore;
|
||||||
this.featureTagStore = featureTagStore;
|
|
||||||
this.logger = getLogger('services/feature-toggle-service.js');
|
this.logger = getLogger('services/feature-toggle-service.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFeatures() {
|
async getFeatures(query, fields) {
|
||||||
return this.featureToggleStore.getFeatures();
|
const preppedQuery = await this.prepQuery(query);
|
||||||
|
return this.featureToggleStore.getFeatures(preppedQuery, fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFeaturesClient() {
|
paramToArray(param) {
|
||||||
return this.featureToggleStore.getFeaturesClient();
|
if (!param) {
|
||||||
|
return param;
|
||||||
|
}
|
||||||
|
return Array.isArray(param) ? param : [param];
|
||||||
|
}
|
||||||
|
|
||||||
|
async prepQuery({ tag, project, namePrefix }) {
|
||||||
|
if (!tag && !project && !namePrefix) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const tagQuery = this.paramToArray(tag);
|
||||||
|
const projectQuery = this.paramToArray(project);
|
||||||
|
const query = await querySchema.validateAsync({
|
||||||
|
tag: tagQuery,
|
||||||
|
project: projectQuery,
|
||||||
|
namePrefix,
|
||||||
|
});
|
||||||
|
if (query.tag) {
|
||||||
|
query.tag = query.tag.map(q => q.split(':'));
|
||||||
|
}
|
||||||
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getArchivedFeatures() {
|
async getArchivedFeatures() {
|
||||||
@ -92,14 +109,14 @@ class FeatureToggleService {
|
|||||||
|
|
||||||
/** Tag releated */
|
/** Tag releated */
|
||||||
async listTags(featureName) {
|
async listTags(featureName) {
|
||||||
return this.featureTagStore.getAllTagsForFeature(featureName);
|
return this.featureToggleStore.getAllTagsForFeature(featureName);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addTag(featureName, tag, userName) {
|
async addTag(featureName, tag, userName) {
|
||||||
await nameSchema.validateAsync({ name: featureName });
|
await nameSchema.validateAsync({ name: featureName });
|
||||||
const validatedTag = await tagSchema.validateAsync(tag);
|
const validatedTag = await tagSchema.validateAsync(tag);
|
||||||
await this.createTagIfNeeded(validatedTag, userName);
|
await this.createTagIfNeeded(validatedTag, userName);
|
||||||
await this.featureTagStore.tagFeature(featureName, validatedTag);
|
await this.featureToggleStore.tagFeature(featureName, validatedTag);
|
||||||
await this.eventStore.store({
|
await this.eventStore.store({
|
||||||
type: FEATURE_TAGGED,
|
type: FEATURE_TAGGED,
|
||||||
createdBy: userName,
|
createdBy: userName,
|
||||||
@ -129,7 +146,7 @@ class FeatureToggleService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeTag(featureName, tag, userName) {
|
async removeTag(featureName, tag, userName) {
|
||||||
await this.featureTagStore.untagFeature(featureName, tag);
|
await this.featureToggleStore.untagFeature(featureName, tag);
|
||||||
await this.eventStore.store({
|
await this.eventStore.store({
|
||||||
type: FEATURE_UNTAGGED,
|
type: FEATURE_UNTAGGED,
|
||||||
createdBy: userName,
|
createdBy: userName,
|
||||||
|
@ -362,3 +362,115 @@ test.serial('can untag feature', async t => {
|
|||||||
t.is(res.body.tags.length, 0);
|
t.is(res.body.tags.length, 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.serial('Can get features tagged by tag', async t => {
|
||||||
|
t.plan(2);
|
||||||
|
const request = await setupApp(stores);
|
||||||
|
await request.post('/api/admin/features').send({
|
||||||
|
name: 'test.feature',
|
||||||
|
type: 'killswitch',
|
||||||
|
enabled: true,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
await request.post('/api/admin/features').send({
|
||||||
|
name: 'test.feature2',
|
||||||
|
type: 'killswitch',
|
||||||
|
enabled: true,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
const tag = { value: 'Crazy', type: 'simple' };
|
||||||
|
await request
|
||||||
|
.post('/api/admin/features/test.feature/tags')
|
||||||
|
.send(tag)
|
||||||
|
.expect(201);
|
||||||
|
return request
|
||||||
|
.get('/api/admin/features?tag=simple:Crazy')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.is(res.body.features.length, 1);
|
||||||
|
t.is(res.body.features[0].name, 'test.feature');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test.serial('Can query for multiple tags using OR', async t => {
|
||||||
|
t.plan(2);
|
||||||
|
const request = await setupApp(stores);
|
||||||
|
await request.post('/api/admin/features').send({
|
||||||
|
name: 'test.feature',
|
||||||
|
type: 'killswitch',
|
||||||
|
enabled: true,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
await request.post('/api/admin/features').send({
|
||||||
|
name: 'test.feature2',
|
||||||
|
type: 'killswitch',
|
||||||
|
enabled: true,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
const tag = { value: 'Crazy', type: 'simple' };
|
||||||
|
const tag2 = { value: 'tagb', type: 'simple' };
|
||||||
|
await request
|
||||||
|
.post('/api/admin/features/test.feature/tags')
|
||||||
|
.send(tag)
|
||||||
|
.expect(201);
|
||||||
|
await request
|
||||||
|
.post('/api/admin/features/test.feature2/tags')
|
||||||
|
.send(tag2)
|
||||||
|
.expect(201);
|
||||||
|
return request
|
||||||
|
.get('/api/admin/features?tag[]=simple:Crazy&tag[]=simple:tagb')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.is(res.body.features.length, 2);
|
||||||
|
t.is(res.body.features[0].name, 'test.feature');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test.serial('Querying with multiple filters ANDs the filters', async t => {
|
||||||
|
const request = await setupApp(stores);
|
||||||
|
await request.post('/api/admin/features').send({
|
||||||
|
name: 'test.feature',
|
||||||
|
type: 'killswitch',
|
||||||
|
enabled: true,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
await request.post('/api/admin/features').send({
|
||||||
|
name: 'test.feature2',
|
||||||
|
type: 'killswitch',
|
||||||
|
enabled: true,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
await request.post('/api/admin/features').send({
|
||||||
|
name: 'notestprefix.feature3',
|
||||||
|
type: 'release',
|
||||||
|
enabled: true,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
const tag = { value: 'Crazy', type: 'simple' };
|
||||||
|
const tag2 = { value: 'tagb', type: 'simple' };
|
||||||
|
await request
|
||||||
|
.post('/api/admin/features/test.feature/tags')
|
||||||
|
.send(tag)
|
||||||
|
.expect(201);
|
||||||
|
await request
|
||||||
|
.post('/api/admin/features/test.feature2/tags')
|
||||||
|
.send(tag2)
|
||||||
|
.expect(201);
|
||||||
|
await request
|
||||||
|
.post('/api/admin/features/notestprefix.feature3/tags')
|
||||||
|
.send(tag)
|
||||||
|
.expect(201);
|
||||||
|
await request
|
||||||
|
.get('/api/admin/features?tag=simple:Crazy')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => t.is(res.body.features.length, 2));
|
||||||
|
await request
|
||||||
|
.get('/api/admin/features?namePrefix=test&tag=simple:Crazy')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.is(res.body.features.length, 1);
|
||||||
|
t.is(res.body.features[0].name, 'test.feature');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -47,7 +47,7 @@ test.serial('gets a feature by name', async t => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('cant get feature that dose not exist', async t => {
|
test.serial('cant get feature that does not exist', async t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const request = await setupApp(stores);
|
const request = await setupApp(stores);
|
||||||
return request
|
return request
|
||||||
@ -55,3 +55,66 @@ test.serial('cant get feature that dose not exist', async t => {
|
|||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.serial('Can filter features by namePrefix', async t => {
|
||||||
|
t.plan(2);
|
||||||
|
const request = await setupApp(stores);
|
||||||
|
return request
|
||||||
|
.get('/api/client/features?namePrefix=feature.')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.is(res.body.features.length, 1);
|
||||||
|
t.is(res.body.features[0].name, 'feature.with.variants');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('Can use multiple filters', async t => {
|
||||||
|
t.plan(3);
|
||||||
|
const request = await setupApp(stores);
|
||||||
|
await request.post('/api/admin/features').send({
|
||||||
|
name: 'test.feature',
|
||||||
|
type: 'killswitch',
|
||||||
|
enabled: true,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
await request.post('/api/admin/features').send({
|
||||||
|
name: 'test.feature2',
|
||||||
|
type: 'killswitch',
|
||||||
|
enabled: true,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
await request.post('/api/admin/features').send({
|
||||||
|
name: 'notestprefix.feature3',
|
||||||
|
type: 'release',
|
||||||
|
enabled: true,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
});
|
||||||
|
const tag = { value: 'Crazy', type: 'simple' };
|
||||||
|
const tag2 = { value: 'tagb', type: 'simple' };
|
||||||
|
await request
|
||||||
|
.post('/api/admin/features/test.feature/tags')
|
||||||
|
.send(tag)
|
||||||
|
.expect(201);
|
||||||
|
await request
|
||||||
|
.post('/api/admin/features/test.feature2/tags')
|
||||||
|
.send(tag2)
|
||||||
|
.expect(201);
|
||||||
|
await request
|
||||||
|
.post('/api/admin/features/notestprefix.feature3/tags')
|
||||||
|
.send(tag)
|
||||||
|
.expect(201);
|
||||||
|
await request
|
||||||
|
.get('/api/client/features?tag=simple:Crazy')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => t.is(res.body.features.length, 2));
|
||||||
|
await request
|
||||||
|
.get('/api/client/features?namePrefix=test&tag=simple:Crazy')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.is(res.body.features.length, 1);
|
||||||
|
t.is(res.body.features[0].name, 'test.feature');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -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 tagFeatures(stores.tagStore, stores.featureTagStore);
|
await tagFeatures(stores.tagStore, stores.featureToggleStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = async function init(databaseSchema = 'test', getLogger) {
|
module.exports = async function init(databaseSchema = 'test', getLogger) {
|
||||||
|
23
test/fixtures/fake-feature-tag-store.js
vendored
23
test/fixtures/fake-feature-tag-store.js
vendored
@ -1,23 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
const _featureTags = {};
|
|
||||||
return {
|
|
||||||
tagFeature: (featureName, tag) => {
|
|
||||||
_featureTags[featureName] = _featureTags[featureName] || [];
|
|
||||||
_featureTags[featureName].push(tag);
|
|
||||||
},
|
|
||||||
untagFeature: event => {
|
|
||||||
const tags = _featureTags[event.featureName];
|
|
||||||
_featureTags[event.featureName] = tags.splice(
|
|
||||||
tags.indexOf(
|
|
||||||
t => t.type === event.type && t.value === event.value,
|
|
||||||
),
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
getAllTagsForFeature: featureName => {
|
|
||||||
return _featureTags[featureName] || [];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
50
test/fixtures/fake-feature-toggle-store.js
vendored
50
test/fixtures/fake-feature-toggle-store.js
vendored
@ -3,6 +3,8 @@
|
|||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
const _features = [];
|
const _features = [];
|
||||||
const _archive = [];
|
const _archive = [];
|
||||||
|
const _featureTags = {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getFeature: name => {
|
getFeature: name => {
|
||||||
const toggle = _features.find(f => f.name === name);
|
const toggle = _features.find(f => f.name === name);
|
||||||
@ -29,8 +31,6 @@ module.exports = () => {
|
|||||||
);
|
);
|
||||||
_features.push(updatedFeature);
|
_features.push(updatedFeature);
|
||||||
},
|
},
|
||||||
getFeatures: () => Promise.resolve(_features),
|
|
||||||
getFeaturesClient: () => Promise.resolve(_features),
|
|
||||||
createFeature: 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),
|
||||||
@ -55,5 +55,51 @@ module.exports = () => {
|
|||||||
_archive.splice(0, _archive.length);
|
_archive.splice(0, _archive.length);
|
||||||
},
|
},
|
||||||
importFeature: feat => Promise.resolve(_features.push(feat)),
|
importFeature: feat => Promise.resolve(_features.push(feat)),
|
||||||
|
getFeatures: query => {
|
||||||
|
if (query) {
|
||||||
|
const activeQueryKeys = Object.keys(query).filter(
|
||||||
|
t => query[t],
|
||||||
|
);
|
||||||
|
const filtered = _features.filter(feature => {
|
||||||
|
return activeQueryKeys.every(key => {
|
||||||
|
if (key === 'namePrefix') {
|
||||||
|
return feature.name.indexOf(query[key]) > -1;
|
||||||
|
}
|
||||||
|
if (key === 'tag') {
|
||||||
|
return query[key].some(tag => {
|
||||||
|
return (
|
||||||
|
_featureTags[feature.name] &&
|
||||||
|
_featureTags[feature.name].some(t => {
|
||||||
|
return (
|
||||||
|
t.type === tag[0] &&
|
||||||
|
t.value === tag[1]
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return query[key].some(v => v === feature[key]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Promise.resolve(filtered);
|
||||||
|
}
|
||||||
|
return Promise.resolve(_features);
|
||||||
|
},
|
||||||
|
tagFeature: (featureName, tag) => {
|
||||||
|
_featureTags[featureName] = _featureTags[featureName] || [];
|
||||||
|
_featureTags[featureName].push(tag);
|
||||||
|
},
|
||||||
|
untagFeature: event => {
|
||||||
|
const tags = _featureTags[event.featureName];
|
||||||
|
_featureTags[event.featureName] = tags.splice(
|
||||||
|
tags.indexOf(
|
||||||
|
t => t.type === event.type && t.value === event.value,
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getAllTagsForFeature: featureName => {
|
||||||
|
return _featureTags[featureName] || [];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
2
test/fixtures/store.js
vendored
2
test/fixtures/store.js
vendored
@ -4,7 +4,6 @@ const ClientMetricsStore = require('./fake-metrics-store');
|
|||||||
const clientInstanceStore = require('./fake-client-instance-store');
|
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 tagStore = require('./fake-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');
|
||||||
@ -25,7 +24,6 @@ module.exports = {
|
|||||||
clientMetricsStore: new ClientMetricsStore(),
|
clientMetricsStore: new ClientMetricsStore(),
|
||||||
clientInstanceStore: clientInstanceStore(),
|
clientInstanceStore: clientInstanceStore(),
|
||||||
featureToggleStore: featureToggleStore(),
|
featureToggleStore: featureToggleStore(),
|
||||||
featureTagStore: featureTagStore(),
|
|
||||||
tagStore: tagStore(),
|
tagStore: tagStore(),
|
||||||
eventStore: eventStore(),
|
eventStore: eventStore(),
|
||||||
strategyStore: strategyStore(),
|
strategyStore: strategyStore(),
|
||||||
|
Loading…
Reference in New Issue
Block a user