mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +01:00
feat: Add support for toggle types (#618)
This commit is contained in:
parent
045a2dc621
commit
6568457ed8
@ -18,6 +18,7 @@ This endpoint is the one all admin ui should use to fetch all available feature
|
||||
{
|
||||
"name": "Feature.A",
|
||||
"description": "lorem ipsum",
|
||||
"type": "release",
|
||||
"enabled": false,
|
||||
"strategies": [
|
||||
{
|
||||
@ -68,6 +69,7 @@ Used to fetch details about a specific featureToggle. This is mostly provded to
|
||||
{
|
||||
"name": "Feature.A",
|
||||
"description": "lorem ipsum..",
|
||||
"type": "release",
|
||||
"enabled": false,
|
||||
"strategies": [
|
||||
{
|
||||
@ -89,6 +91,7 @@ Used to fetch details about a specific featureToggle. This is mostly provded to
|
||||
{
|
||||
"name": "Feature.A",
|
||||
"description": "lorem ipsum..",
|
||||
"type": "release",
|
||||
"enabled": false,
|
||||
"strategies": [
|
||||
{
|
||||
@ -99,7 +102,12 @@ Used to fetch details about a specific featureToggle. This is mostly provded to
|
||||
}
|
||||
```
|
||||
|
||||
Used by the admin-dashboard to create a new feature toggles. The name **must be unique**, otherwise you will get a _403-response_.
|
||||
Used by the admin-dashboard to create a new feature toggles.
|
||||
|
||||
**Notes:**
|
||||
|
||||
- _name_ **must be globally unique**, otherwise you will get a _403-response_.
|
||||
- _type_ is optional. If not defined it defaults to `release`
|
||||
|
||||
Returns 200-respose if the feature toggle was created successfully.
|
||||
|
||||
@ -113,6 +121,7 @@ Returns 200-respose if the feature toggle was created successfully.
|
||||
{
|
||||
"name": "Feature.A",
|
||||
"description": "lorem ipsum..",
|
||||
"type": "release",
|
||||
"enabled": false,
|
||||
"strategies": [
|
||||
{
|
||||
@ -150,6 +159,7 @@ None
|
||||
{
|
||||
"name": "Feature.A",
|
||||
"description": "lorem ipsum..",
|
||||
"type": "release",
|
||||
"enabled": true,
|
||||
"strategies": [
|
||||
{
|
||||
@ -177,6 +187,7 @@ None
|
||||
{
|
||||
"name": "Feature.A",
|
||||
"description": "lorem ipsum..",
|
||||
"type": "release",
|
||||
"enabled": false,
|
||||
"strategies": [
|
||||
{
|
||||
@ -205,6 +216,7 @@ Used to fetch list of archived feature toggles
|
||||
{
|
||||
"name": "Feature.A",
|
||||
"description": "lorem ipsum",
|
||||
"type": "release",
|
||||
"enabled": false,
|
||||
"strategies": [
|
||||
{
|
||||
|
50
docs/api/admin/feature-types-api.md
Normal file
50
docs/api/admin/feature-types-api.md
Normal file
@ -0,0 +1,50 @@
|
||||
---
|
||||
id: events
|
||||
title: /api/admin/feature-types
|
||||
---
|
||||
|
||||
# Feature Types API
|
||||
|
||||
`GET: http://unleash.host.com/api/admin/feature-types`
|
||||
|
||||
Used to fetch all feature types defined in the unleash system.
|
||||
|
||||
**Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"types": [
|
||||
{
|
||||
"id": "release",
|
||||
"name": "Release",
|
||||
"description": "Used to enable trunk-based development for teams practicing Continuous Delivery.",
|
||||
"lifetimeDays": 40
|
||||
},
|
||||
{
|
||||
"id": "experiment",
|
||||
"name": "Experiment",
|
||||
"description": "Used to perform multivariate or A/B testing.",
|
||||
"lifetimeDays": 40
|
||||
},
|
||||
{
|
||||
"id": "ops",
|
||||
"name": "Operational",
|
||||
"description": "Used to control operational aspects of the system behavior.",
|
||||
"lifetimeDays": 7
|
||||
},
|
||||
{
|
||||
"id": "killswitch",
|
||||
"name": "Kill switch",
|
||||
"description": "Used to to gracefully degrade system functionality.",
|
||||
"lifetimeDays": null
|
||||
},
|
||||
{
|
||||
"id": "permission",
|
||||
"name": "Permission",
|
||||
"description": "Used to change the features or product experience that certain users receive.",
|
||||
"lifetimeDays": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
@ -27,6 +27,7 @@ This endpoint should never return anything besides a valid _20X or 304-response_
|
||||
{
|
||||
"name": "Feature.A",
|
||||
"description": "lorem ipsum",
|
||||
"type": "release",
|
||||
"enabled": false,
|
||||
"strategies": [
|
||||
{
|
||||
@ -39,6 +40,7 @@ This endpoint should never return anything besides a valid _20X or 304-response_
|
||||
},
|
||||
{
|
||||
"name": "Feature.B",
|
||||
"type": "killswitch",
|
||||
"description": "lorem ipsum",
|
||||
"enabled": true,
|
||||
"strategies": [
|
||||
@ -76,6 +78,7 @@ Used to fetch details about a specific feature toggle. This is mainly provided t
|
||||
{
|
||||
"name": "Feature.A",
|
||||
"description": "lorem ipsum..",
|
||||
"type": "release",
|
||||
"enabled": false,
|
||||
"strategies": [
|
||||
{
|
||||
|
@ -9,21 +9,21 @@ This document describes our current database schema used in PostgreSQL. We use d
|
||||
|
||||
Used by db-migrate module to keep track of migrations.
|
||||
|
||||
| NAME | TYPE | SIZE | NULLABLE | COLUMN_DEF |
|
||||
| ------ | --------- | ---- | -------- | -------------------------------------- |
|
||||
| id | serial | 10 | 0 | nextval('migrations_id_seq'::regclass) |
|
||||
| name | varchar | 255 | 0 | (null) |
|
||||
| run_on | timestamp | 29 | 0 | (null) |
|
||||
| NAME | TYPE | SIZE | NULLABLE | COLUMN_DEF |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | serial | 10 | 0 | nextval('migrations_id_seq'::regclass) |
|
||||
| name | varchar | 255 | 0 | (null) |
|
||||
| run_on | timestamp | 29 | 0 | (null) |
|
||||
|
||||
## Table: _events_
|
||||
|
||||
| NAME | TYPE | SIZE | NULLABLE | COLUMN_DEF |
|
||||
| ---------- | --------- | ---------- | -------- | ---------------------------------- |
|
||||
| id | serial | 10 | 0 | nextval('events_id_seq'::regclass) |
|
||||
| created_at | timestamp | 29 | 1 | now() |
|
||||
| type | varchar | 255 | 0 | (null) |
|
||||
| created_by | varchar | 255 | 0 | (null) |
|
||||
| data | json | 2147483647 | 1 | (null) |
|
||||
| NAME | TYPE | SIZE | NULLABLE | COLUMN_DEF |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | serial | 10 | 0 | nextval('events_id_seq'::regclass) |
|
||||
| created_at | timestamp | 29 | 1 | now() |
|
||||
| type | varchar | 255 | 0 | (null) |
|
||||
| created_by | varchar | 255 | 0 | (null) |
|
||||
| data | json | 2147483647 | 1 | (null) |
|
||||
|
||||
## Table: _strategies_
|
||||
|
||||
@ -36,14 +36,15 @@ Used by db-migrate module to keep track of migrations.
|
||||
|
||||
## Table: _features_
|
||||
|
||||
| **NAME** | **TYPE** | **SIZE** | **NULLABLE** | **COLUMN_DEF** | **COMMENT** |
|
||||
| ----------- | --------- | ---------- | ------------ | -------------- | ----------- |
|
||||
| created_at | timestamp | 29 | 1 | now() | |
|
||||
| name | varchar | 255 | 0 | (null) | |
|
||||
| enabled | int4 | 10 | 1 | 0 | |
|
||||
| description | text | 2147483647 | 1 | (null) | |
|
||||
| archived | int4 | 10 | 1 | 0 | |
|
||||
| strategies | json | 2147483647 | 1 | (null) | |
|
||||
| **NAME** | **TYPE** | **SIZE** | **NULLABLE** | **COLUMN_DEF** | **COMMENT** |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| created_at | timestamp | 29 | 1 | now() | |
|
||||
| name | varchar | 255 | 0 | (null) | |
|
||||
| enabled | int4 | 10 | 1 | 0 | |
|
||||
| description | text | 2147483647 | 1 | (null) | |
|
||||
| archived | int4 | 10 | 1 | 0 | |
|
||||
| strategies | json | 2147483647 | 1 | (null) | |
|
||||
| type | varchar | 2147483647 | 1 | release | |
|
||||
|
||||
## Table: _client_strategies_
|
||||
|
||||
@ -65,8 +66,17 @@ Used by db-migrate module to keep track of migrations.
|
||||
|
||||
## Table: _client_metrics_
|
||||
|
||||
| COLUMN_NAME | TYPE_NAME | COLUMN_SIZE | NULLABLE | COLUMN_DEF |
|
||||
| ----------- | --------- | ----------- | -------- | ------------------------------------------ |
|
||||
| id | serial | 10 | 0 | nextval('client_metrics_id_seq'::regclass) |
|
||||
| created_at | timestamp | 29 | 1 | now() |
|
||||
| metrics | json | 2147483647 | 1 | (null) |
|
||||
| COLUMN_NAME | TYPE_NAME | COLUMN_SIZE | NULLABLE | COLUMN_DEF |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | serial | 10 | 0 | nextval('client_metrics_id_seq'::regclass) |
|
||||
| created_at | timestamp | 29 | 1 | now() |
|
||||
| metrics | json | 2147483647 | 1 | (null) |
|
||||
|
||||
## Table: _feature_types_
|
||||
|
||||
| COLUMN_NAME | TYPE_NAME | COLUMN_SIZE | NULLABLE | COLUMN_DEF |
|
||||
| ------------- | --------- | ----------- | -------- | ---------- |
|
||||
| id | varchar | 255 | 0 | (null) |
|
||||
| name | varchar | | 0 | (null) |
|
||||
| description | varchar | | 1 | (null) |
|
||||
| lifetime_days | integer | | 1 | (null) |
|
||||
|
@ -13,6 +13,7 @@ const NotFoundError = require('../error/notfound-error');
|
||||
const FEATURE_COLUMNS = [
|
||||
'name',
|
||||
'description',
|
||||
'type',
|
||||
'enabled',
|
||||
'strategies',
|
||||
'variants',
|
||||
@ -97,6 +98,7 @@ class FeatureToggleStore {
|
||||
return {
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
type: row.type,
|
||||
enabled: row.enabled > 0,
|
||||
strategies: row.strategies,
|
||||
variants: row.variants,
|
||||
@ -108,6 +110,7 @@ class FeatureToggleStore {
|
||||
return {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
type: data.type,
|
||||
enabled: data.enabled ? 1 : 0,
|
||||
archived: data.archived ? 1 : 0,
|
||||
strategies: JSON.stringify(data.strategies),
|
||||
|
29
lib/db/feature-type-store.js
Normal file
29
lib/db/feature-type-store.js
Normal file
@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
const COLUMNS = ['id', 'name', 'description', 'lifetime_days'];
|
||||
const TABLE = 'feature_types';
|
||||
|
||||
class FeatureToggleStore {
|
||||
constructor(db, getLogger) {
|
||||
this.db = db;
|
||||
this.getLogger = getLogger('feature-type-store.js');
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return this.db
|
||||
.select(COLUMNS)
|
||||
.from(TABLE)
|
||||
.map(this.rowToFeatureType);
|
||||
}
|
||||
|
||||
rowToFeatureType(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
lifetimeDays: row.lifetime_days,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FeatureToggleStore;
|
@ -3,6 +3,7 @@
|
||||
const { createDb } = require('./db-pool');
|
||||
const EventStore = require('./event-store');
|
||||
const FeatureToggleStore = require('./feature-toggle-store');
|
||||
const FeatureTypeStore = require('./feature-type-store');
|
||||
const StrategyStore = require('./strategy-store');
|
||||
const ClientInstanceStore = require('./client-instance-store');
|
||||
const ClientMetricsDb = require('./client-metrics-db');
|
||||
@ -22,6 +23,7 @@ module.exports.createStores = (config, eventBus) => {
|
||||
db,
|
||||
eventStore,
|
||||
featureToggleStore: new FeatureToggleStore(db, eventStore, getLogger),
|
||||
featureTypeStore: new FeatureTypeStore(db, getLogger),
|
||||
strategyStore: new StrategyStore(db, eventStore, getLogger),
|
||||
clientApplicationsStore: new ClientApplicationsStore(
|
||||
db,
|
||||
|
@ -54,6 +54,7 @@ const featureShema = joi
|
||||
.keys({
|
||||
name: nameType,
|
||||
enabled: joi.boolean().default(false),
|
||||
type: joi.string().default('release'),
|
||||
description: joi
|
||||
.string()
|
||||
.allow('')
|
||||
|
@ -17,6 +17,7 @@ test('should require URL firendly name', t => {
|
||||
test('should be valid toggle name', t => {
|
||||
const toggle = {
|
||||
name: 'app.name',
|
||||
type: 'release',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
};
|
||||
@ -28,6 +29,7 @@ test('should be valid toggle name', t => {
|
||||
test('should strip extra variant fields', t => {
|
||||
const toggle = {
|
||||
name: 'app.name',
|
||||
type: 'release',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
variants: [
|
||||
@ -47,6 +49,7 @@ test('should strip extra variant fields', t => {
|
||||
test('should allow weightType=fix', t => {
|
||||
const toggle = {
|
||||
name: 'app.name',
|
||||
type: 'release',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
variants: [
|
||||
@ -65,6 +68,7 @@ test('should allow weightType=fix', t => {
|
||||
test('should disallow weightType=unknown', t => {
|
||||
const toggle = {
|
||||
name: 'app.name',
|
||||
type: 'release',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
variants: [
|
||||
@ -86,6 +90,7 @@ test('should disallow weightType=unknown', t => {
|
||||
test('should be possible to define variant overrides', t => {
|
||||
const toggle = {
|
||||
name: 'app.name',
|
||||
type: 'release',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
variants: [
|
||||
@ -112,6 +117,7 @@ test('variant overrides must have corect shape', async t => {
|
||||
t.plan(1);
|
||||
const toggle = {
|
||||
name: 'app.name',
|
||||
type: 'release',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
variants: [
|
||||
@ -139,6 +145,7 @@ test('variant overrides must have corect shape', async t => {
|
||||
test('should keep constraints', t => {
|
||||
const toggle = {
|
||||
name: 'app.constraints',
|
||||
type: 'release',
|
||||
enabled: false,
|
||||
strategies: [
|
||||
{
|
||||
|
22
lib/routes/admin-api/feature-type.js
Normal file
22
lib/routes/admin-api/feature-type.js
Normal file
@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
const Controller = require('../controller');
|
||||
|
||||
const version = 1;
|
||||
|
||||
class FeatureTypeController extends Controller {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
this.featureTypeStore = config.stores.featureTypeStore;
|
||||
this.logger = config.getLogger('/admin-api/feature-type.js');
|
||||
|
||||
this.get('/', this.getAllFeatureTypes);
|
||||
}
|
||||
|
||||
async getAllFeatureTypes(req, res) {
|
||||
const types = await this.featureTypeStore.getAll();
|
||||
res.json({ version, types });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FeatureTypeController;
|
@ -2,6 +2,7 @@
|
||||
|
||||
const Controller = require('../controller');
|
||||
const FeatureController = require('./feature.js');
|
||||
const FeatureTypeController = require('./feature-type.js');
|
||||
const ArchiveController = require('./archive.js');
|
||||
const EventController = require('./event.js');
|
||||
const StrategyController = require('./strategy');
|
||||
@ -18,6 +19,10 @@ class AdminApi extends Controller {
|
||||
|
||||
this.app.get('/', this.index);
|
||||
this.app.use('/features', new FeatureController(config).router);
|
||||
this.app.use(
|
||||
'/feature-types',
|
||||
new FeatureTypeController(config).router,
|
||||
);
|
||||
this.app.use('/archive', new ArchiveController(config).router);
|
||||
this.app.use('/strategies', new StrategyController(config).router);
|
||||
this.app.use('/events', new EventController(config).router);
|
||||
|
38
migrations/20200805091409-add-feature-toggle-type.js
Normal file
38
migrations/20200805091409-add-feature-toggle-type.js
Normal file
@ -0,0 +1,38 @@
|
||||
/* eslint camelcase: "off" */
|
||||
|
||||
'use strict';
|
||||
|
||||
const async = require('async');
|
||||
|
||||
exports.up = function(db, cb) {
|
||||
async.series(
|
||||
[
|
||||
db.createTable.bind(db, 'feature_types', {
|
||||
id: {
|
||||
type: 'string',
|
||||
length: 255,
|
||||
primaryKey: true,
|
||||
notNull: true,
|
||||
},
|
||||
name: { type: 'string', notNull: true },
|
||||
description: { type: 'string' },
|
||||
lifetime_days: { type: 'int' },
|
||||
}),
|
||||
db.runSql.bind(
|
||||
db,
|
||||
`
|
||||
INSERT INTO feature_types(id, name, description, lifetime_days) VALUES('release', 'Release', 'Used to enable trunk-based development for teams practicing Continuous Delivery.', 40);
|
||||
INSERT INTO feature_types(id, name, description, lifetime_days) VALUES('experiment', 'Experiment', 'Used to perform multivariate or A/B testing.', 40);
|
||||
INSERT INTO feature_types(id, name, description, lifetime_days) VALUES('operational', 'Operational', 'Used to control operational aspects of the system behavior.', 7);
|
||||
INSERT INTO feature_types(id, name, description, lifetime_days) VALUES('kill-switch', 'Kill switch', 'Used to to gracefully degrade system functionality.', null);
|
||||
INSERT INTO feature_types(id, name, description, lifetime_days) VALUES('permission', 'Permission', 'Used to change the features or product experience that certain users receive.', null);
|
||||
`,
|
||||
),
|
||||
],
|
||||
cb,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function(db, cb) {
|
||||
return db.dropTable('feature_types', cb);
|
||||
};
|
17
migrations/20200805094311-add-feature-type-to-features.js
Normal file
17
migrations/20200805094311-add-feature-type-to-features.js
Normal file
@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, cb) {
|
||||
return db.addColumn(
|
||||
'features',
|
||||
'type',
|
||||
{
|
||||
type: 'string',
|
||||
defaultValue: 'release',
|
||||
},
|
||||
cb,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function(db, cb) {
|
||||
return db.removeColumn('features', 'type', cb);
|
||||
};
|
@ -87,7 +87,7 @@
|
||||
"prom-client": "^12.0.0",
|
||||
"response-time": "^2.3.2",
|
||||
"serve-favicon": "^2.5.0",
|
||||
"unleash-frontend": "3.4.0",
|
||||
"unleash-frontend": "3.4.1-0",
|
||||
"yargs": "^15.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -250,3 +250,30 @@ test.serial('creates new feature toggle with variant overrides', async t => {
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201);
|
||||
});
|
||||
|
||||
test.serial('creates new feature toggle without type', async t => {
|
||||
t.plan(1);
|
||||
const request = await setupApp(stores);
|
||||
await request.post('/api/admin/features').send({
|
||||
name: 'com.test.noType',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
});
|
||||
await request.get('/api/admin/features/com.test.noType').expect(res => {
|
||||
t.is(res.body.type, 'release');
|
||||
});
|
||||
});
|
||||
|
||||
test.serial('creates new feature toggle with type', async t => {
|
||||
t.plan(1);
|
||||
const request = await setupApp(stores);
|
||||
await request.post('/api/admin/features').send({
|
||||
name: 'com.test.withType',
|
||||
type: 'killswitch',
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
});
|
||||
await request.get('/api/admin/features/com.test.withType').expect(res => {
|
||||
t.is(res.body.type, 'killswitch');
|
||||
});
|
||||
});
|
||||
|
2
test/fixtures/fake-feature-toggle-store.js
vendored
2
test/fixtures/fake-feature-toggle-store.js
vendored
@ -9,7 +9,7 @@ module.exports = () => {
|
||||
if (toggle) {
|
||||
return Promise.resolve(toggle);
|
||||
}
|
||||
return Promise.reject();
|
||||
return Promise.reject(new Error('could not find toggle'));
|
||||
},
|
||||
hasFeature: name => {
|
||||
const toggle = _features.find(f => f.name === name);
|
||||
|
@ -5554,10 +5554,10 @@ universalify@^0.1.0:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
|
||||
unleash-frontend@3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-3.4.0.tgz#6df17f56904dab59e7c99765d5443e7716aa0734"
|
||||
integrity sha512-ZHzaPSoBKZGyp+Bneo2vBhTUbc34aOBh6hxyjZm3v/ol7PpTt6D3opoIVe18UElLOAZft2Kjq2Gtv/1gvJx6pg==
|
||||
unleash-frontend@3.4.1-0:
|
||||
version "3.4.1-0"
|
||||
resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-3.4.1-0.tgz#480731a753059ad4b38c98b9a5f014eaa44d912b"
|
||||
integrity sha512-skx+SlOFPHcrrQlY5UhNV6stxblUNaB0mGbZfbh7CdIuWMs1Y+KxVoorTqDaHV0zhyxKQvngzZOd696VnMxA4w==
|
||||
|
||||
unpipe@1.0.0, unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
|
Loading…
Reference in New Issue
Block a user