mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-26 01:17:00 +02:00
Add support for aggregate_strategies in the API.
This commit changes the features-tables: - drop columns 'strategy' and 'parameters' - add column 'strategies' of type json. - migrates existing strategy-mappings in to the new format. The idea is that the 'strategies' column should contain a json-array of strategy-configuration for the toggle: ``` [{ "name" : "strategy1", "parameters": { "name": "vale" } }] ``` To make sure to not break exiting clients the api is extended with a mapping layer (adding old fields to the json-respons, and mapping to the new format on create/update a feature toggle. this commit is first step in solving #102
This commit is contained in:
parent
611942551d
commit
0cb45f110c
@ -2,7 +2,7 @@
|
|||||||
const eventType = require('../eventType');
|
const eventType = require('../eventType');
|
||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
const NotFoundError = require('../error/NotFoundError');
|
const NotFoundError = require('../error/NotFoundError');
|
||||||
const FEATURE_COLUMNS = ['name', 'description', 'enabled', 'strategy_name', 'parameters'];
|
const FEATURE_COLUMNS = ['name', 'description', 'enabled', 'strategies'];
|
||||||
|
|
||||||
module.exports = function (db, eventStore) {
|
module.exports = function (db, eventStore) {
|
||||||
eventStore.on(eventType.featureCreated, event => createFeature(event.data));
|
eventStore.on(eventType.featureCreated, event => createFeature(event.data));
|
||||||
@ -39,18 +39,15 @@ module.exports = function (db, eventStore) {
|
|||||||
.map(rowToFeature);
|
.map(rowToFeature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function rowToFeature (row) {
|
function rowToFeature (row) {
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new NotFoundError('No feature toggle found');
|
throw new NotFoundError('No feature toggle found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: row.name,
|
name: row.name,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
enabled: row.enabled > 0,
|
enabled: row.enabled > 0,
|
||||||
strategy: row.strategy_name, // eslint-disable-line
|
strategies: row.strategies,
|
||||||
parameters: row.parameters,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,8 +57,7 @@ module.exports = function (db, eventStore) {
|
|||||||
description: data.description,
|
description: data.description,
|
||||||
enabled: data.enabled ? 1 : 0,
|
enabled: data.enabled ? 1 : 0,
|
||||||
archived: data.archived ? 1 : 0,
|
archived: data.archived ? 1 : 0,
|
||||||
strategy_name: data.strategy, // eslint-disable-line
|
strategies: JSON.stringify(data.strategies),
|
||||||
parameters: data.parameters,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,6 +70,7 @@ module.exports = function (db, eventStore) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateFeature (data) {
|
function updateFeature (data) {
|
||||||
|
console.log(data);
|
||||||
return db('features')
|
return db('features')
|
||||||
.where({ name: data.name })
|
.where({ name: data.name })
|
||||||
.update(eventDataToRow(data))
|
.update(eventDataToRow(data))
|
||||||
|
31
packages/unleash-api/lib/helper/legacy-feature-mapper.js
Normal file
31
packages/unleash-api/lib/helper/legacy-feature-mapper.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
function addOldFields (feature) {
|
||||||
|
let modifiedFeature = Object.assign({}, feature);
|
||||||
|
modifiedFeature.strategy = feature.strategies[0].name;
|
||||||
|
modifiedFeature.parameters = Object.assign({}, feature.strategies[0].parameters);
|
||||||
|
return modifiedFeature;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOldFomrat (feature) {
|
||||||
|
return feature.strategy && !feature.strategies;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNewFormat (feature) {
|
||||||
|
if (isOldFomrat(feature)) {
|
||||||
|
return {
|
||||||
|
name: feature.name,
|
||||||
|
description: feature.description,
|
||||||
|
enabled: feature.enabled,
|
||||||
|
strategies: [
|
||||||
|
{
|
||||||
|
name: feature.strategy,
|
||||||
|
parameters: Object.assign({}, feature.parameters),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return feature;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { addOldFields, toNewFormat };
|
@ -8,24 +8,23 @@ const ValidationError = require('../error/ValidationError');
|
|||||||
const validateRequest = require('../error/validateRequest');
|
const validateRequest = require('../error/validateRequest');
|
||||||
const extractUser = require('../extractUser');
|
const extractUser = require('../extractUser');
|
||||||
|
|
||||||
|
const legacyFeatureMapper = require('../helper/legacy-feature-mapper');
|
||||||
|
|
||||||
module.exports = function (app, config) {
|
module.exports = function (app, config) {
|
||||||
const featureDb = config.featureDb;
|
const featureDb = config.featureDb;
|
||||||
const eventStore = config.eventStore;
|
const eventStore = config.eventStore;
|
||||||
|
|
||||||
app.get('/features', (req, res) => {
|
app.get('/features', (req, res) => {
|
||||||
featureDb.getFeatures().then(features => {
|
featureDb.getFeatures()
|
||||||
res.json({ features });
|
.then((features) => features.map(legacyFeatureMapper.addOldFields))
|
||||||
});
|
.then(features => res.json({ features }));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/features/:featureName', (req, res) => {
|
app.get('/features/:featureName', (req, res) => {
|
||||||
featureDb.getFeature(req.params.featureName)
|
featureDb.getFeature(req.params.featureName)
|
||||||
.then(feature => {
|
.then(legacyFeatureMapper.addOldFields)
|
||||||
res.json(feature);
|
.then(feature => res.json(feature).end())
|
||||||
})
|
.catch(() => res.status(404).json({ error: 'Could not find feature' }));
|
||||||
.catch(() => {
|
|
||||||
res.status(404).json({ error: 'Could not find feature' });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/features', (req, res) => {
|
app.post('/features', (req, res) => {
|
||||||
@ -37,20 +36,15 @@ module.exports = function (app, config) {
|
|||||||
.then(() => eventStore.create({
|
.then(() => eventStore.create({
|
||||||
type: eventType.featureCreated,
|
type: eventType.featureCreated,
|
||||||
createdBy: extractUser(req),
|
createdBy: extractUser(req),
|
||||||
data: req.body,
|
data: legacyFeatureMapper.toNewFormat(req.body),
|
||||||
}))
|
}))
|
||||||
.then(() => {
|
.then(() => res.status(201).end())
|
||||||
res.status(201).end();
|
|
||||||
})
|
|
||||||
.catch(NameExistsError, () => {
|
.catch(NameExistsError, () => {
|
||||||
res.status(403).json([{
|
res.status(403)
|
||||||
msg: `A feature named '${req.body.name}' already exists. It could be archived.`,
|
.json([{ msg: `A feature named '${req.body.name}' already exists.` }])
|
||||||
}])
|
.end();
|
||||||
.end();
|
|
||||||
})
|
|
||||||
.catch(ValidationError, () => {
|
|
||||||
res.status(400).json(req.validationErrors());
|
|
||||||
})
|
})
|
||||||
|
.catch(ValidationError, () => res.status(400).json(req.validationErrors()))
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
logger.error('Could not create feature toggle', err);
|
logger.error('Could not create feature toggle', err);
|
||||||
res.status(500).end();
|
res.status(500).end();
|
||||||
@ -60,7 +54,7 @@ module.exports = function (app, config) {
|
|||||||
app.put('/features/:featureName', (req, res) => {
|
app.put('/features/:featureName', (req, res) => {
|
||||||
const featureName = req.params.featureName;
|
const featureName = req.params.featureName;
|
||||||
const userName = extractUser(req);
|
const userName = extractUser(req);
|
||||||
const updatedFeature = req.body;
|
const updatedFeature = legacyFeatureMapper.toNewFormat(req.body);
|
||||||
|
|
||||||
updatedFeature.name = featureName;
|
updatedFeature.name = featureName;
|
||||||
|
|
||||||
@ -70,12 +64,8 @@ module.exports = function (app, config) {
|
|||||||
createdBy: userName,
|
createdBy: userName,
|
||||||
data: updatedFeature,
|
data: updatedFeature,
|
||||||
}))
|
}))
|
||||||
.then(() => {
|
.then(() => res.status(200).end())
|
||||||
res.status(200).end();
|
.catch(NotFoundError, () => res.status(404).end())
|
||||||
})
|
|
||||||
.catch(NotFoundError, () => {
|
|
||||||
res.status(404).end();
|
|
||||||
})
|
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
logger.error(`Could not update feature toggle=${featureName}`, err);
|
logger.error(`Could not update feature toggle=${featureName}`, err);
|
||||||
res.status(500).end();
|
res.status(500).end();
|
||||||
@ -94,12 +84,8 @@ module.exports = function (app, config) {
|
|||||||
name: featureName,
|
name: featureName,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
.then(() => {
|
.then(() => res.status(200).end())
|
||||||
res.status(200).end();
|
.catch(NotFoundError, () => res.status(404).end())
|
||||||
})
|
|
||||||
.catch(NotFoundError, () => {
|
|
||||||
res.status(404).end();
|
|
||||||
})
|
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
logger.error(`Could not archive feature=${featureName}`, err);
|
logger.error(`Could not archive feature=${featureName}`, err);
|
||||||
res.status(500).end();
|
res.status(500).end();
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
'use strict';
|
||||||
|
module.exports = require('../scripts/migration-runner').create('007-add-strategies-to-features');
|
@ -0,0 +1,13 @@
|
|||||||
|
--create old columns
|
||||||
|
ALTER TABLE features ADD "parameters" json;
|
||||||
|
ALTER TABLE features ADD "strategy_name" varchar(255);
|
||||||
|
|
||||||
|
--populate old columns
|
||||||
|
UPDATE features
|
||||||
|
SET strategy_name = f.strategies->0->>'name',
|
||||||
|
parameters = f.strategies->0->'parameters'
|
||||||
|
FROM features as f
|
||||||
|
WHERE f.name = features.name;
|
||||||
|
|
||||||
|
--drop new column
|
||||||
|
ALTER TABLE features DROP COLUMN "strategies";
|
@ -0,0 +1,12 @@
|
|||||||
|
--create new strategies-column
|
||||||
|
ALTER TABLE features ADD "strategies" json;
|
||||||
|
|
||||||
|
--populate the strategies column
|
||||||
|
UPDATE features
|
||||||
|
SET strategies = ('[{"name":"'||f.strategy_name||'","parameters":'||f.parameters||'}]')::json
|
||||||
|
FROM features as f
|
||||||
|
WHERE f.name = features.name;
|
||||||
|
|
||||||
|
--delete old strategy-columns
|
||||||
|
ALTER TABLE features DROP COLUMN "strategy_name";
|
||||||
|
ALTER TABLE features DROP COLUMN "parameters";
|
71
packages/unleash-api/notes/schema.md
Normal file
71
packages/unleash-api/notes/schema.md
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# Schema
|
||||||
|
|
||||||
|
## Table: _migrations_
|
||||||
|
|
||||||
|
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) |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 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) |
|
||||||
|
|
||||||
|
|
||||||
|
## Table: _strategies_loc
|
||||||
|
| NAME | TYPE | SIZE | NULLABLE | COLUMN_DEF |
|
||||||
|
| ------------------- | --------- | ----------- | -------- | ---------- |
|
||||||
|
| created_at | timestamp | 29 | 1 | now() |
|
||||||
|
| name | varchar | 255 | 0 | (null) |
|
||||||
|
| description | text | 2147483647 | 1 | (null) |
|
||||||
|
| parameters_template | json | 2147483647 | 1 | (null) |
|
||||||
|
|
||||||
|
|
||||||
|
## 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 | |
|
||||||
|
| parameters | json | 2147483647 | 1 | (null) | deprecated (*) |
|
||||||
|
| strategy_name | varchar | 255 | 1 | (null) | deprecated (*) |
|
||||||
|
| strategies | json | 2147483647 | 1 | (null) | |
|
||||||
|
|
||||||
|
(*) we migrated from `parmaters` and `strategy_name` to `strategies` which should contain an array of these.
|
||||||
|
|
||||||
|
For [aggregate strategies](https://github.com/finn-no/unleash/issues/102) we had the following sql to migrate to the strategies column:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE features ADD "strategies" json;
|
||||||
|
|
||||||
|
--populate the strategies column
|
||||||
|
UPDATE features
|
||||||
|
SET strategies = ('[{"name":"'||f.strategy_name||'","parameters":'||f.parameters||'}]')::json
|
||||||
|
FROM features as f
|
||||||
|
WHERE f.name = features.name;
|
||||||
|
```
|
||||||
|
|
||||||
|
In order to migrate back, one can use the following sql (it will loose all, but the first activation strategy):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE features
|
||||||
|
SET strategy_name = f.strategies->0->>'name',
|
||||||
|
parameters = f.strategies->0->'parameters'
|
||||||
|
FROM features as f
|
||||||
|
WHERE f.name = features.name;
|
||||||
|
|
||||||
|
ALTER TABLE features DROP COLUMN "strategies";
|
||||||
|
```
|
@ -111,4 +111,35 @@ describe('The features api', () => {
|
|||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(403, done);
|
.expect(403, done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('new strategies api', function () {
|
||||||
|
it('automatically map existing strategy to strategies array', function (done) {
|
||||||
|
request
|
||||||
|
.get('/features/featureY')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.end(function (err, res) {
|
||||||
|
assert.equal(res.body.strategies.length, 1, 'expected strategy added to strategies');
|
||||||
|
assert.equal(res.body.strategy, res.body.strategies[0].name);
|
||||||
|
assert.deepEqual(res.body.parameters, res.body.strategies[0].parameters);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can add two strategies to a feature toggle', function (done) {
|
||||||
|
request
|
||||||
|
.put('/features/featureY')
|
||||||
|
.send({
|
||||||
|
name: 'featureY',
|
||||||
|
description: 'soon to be the #14 feature',
|
||||||
|
enabled: false,
|
||||||
|
strategies: [
|
||||||
|
{
|
||||||
|
name: 'baz',
|
||||||
|
parameters: { foo: 'bar' },
|
||||||
|
},
|
||||||
|
] })
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(200, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -46,52 +46,60 @@ function createFeatures () {
|
|||||||
name: 'featureX',
|
name: 'featureX',
|
||||||
description: 'the #1 feature',
|
description: 'the #1 feature',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
strategy: 'default',
|
strategies: [{ name: 'default', parameters: {} }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'featureY',
|
name: 'featureY',
|
||||||
description: 'soon to be the #1 feature',
|
description: 'soon to be the #1 feature',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
strategy: 'baz',
|
strategies: [{
|
||||||
parameters: {
|
name: 'baz',
|
||||||
foo: 'bar',
|
parameters: {
|
||||||
},
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'featureZ',
|
name: 'featureZ',
|
||||||
description: 'terrible feature',
|
description: 'terrible feature',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
strategy: 'baz',
|
strategies: [{
|
||||||
parameters: {
|
name: 'baz',
|
||||||
foo: 'rab',
|
parameters: {
|
||||||
},
|
foo: 'rab',
|
||||||
|
},
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'featureArchivedX',
|
name: 'featureArchivedX',
|
||||||
description: 'the #1 feature',
|
description: 'the #1 feature',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
archived: true,
|
archived: true,
|
||||||
strategy: 'default',
|
strategies: [{ name: 'default', parameters: {} }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'featureArchivedY',
|
name: 'featureArchivedY',
|
||||||
description: 'soon to be the #1 feature',
|
description: 'soon to be the #1 feature',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
archived: true,
|
archived: true,
|
||||||
strategy: 'baz',
|
strategies: [{
|
||||||
parameters: {
|
name: 'baz',
|
||||||
foo: 'bar',
|
parameters: {
|
||||||
},
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'featureArchivedZ',
|
name: 'featureArchivedZ',
|
||||||
description: 'terrible feature',
|
description: 'terrible feature',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
archived: true,
|
archived: true,
|
||||||
strategy: 'baz',
|
strategies: [{
|
||||||
parameters: {
|
name: 'baz',
|
||||||
foo: 'rab',
|
parameters: {
|
||||||
},
|
foo: 'rab',
|
||||||
|
},
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
], feature => featureDb._createFeature(feature));
|
], feature => featureDb._createFeature(feature));
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
'use strict';
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
const mapper = require('../../../lib/helper/legacy-feature-mapper');
|
||||||
|
|
||||||
|
describe('legacy-feature-mapper', () => {
|
||||||
|
it('adds old fields to feature', () => {
|
||||||
|
const feature = {
|
||||||
|
name: 'test',
|
||||||
|
enabled: 0,
|
||||||
|
strategies: [{
|
||||||
|
name: 'default',
|
||||||
|
parameters: {
|
||||||
|
val: 'bar',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mappedFeature = mapper.addOldFields(feature);
|
||||||
|
|
||||||
|
assert.equal(mappedFeature.name, feature.name);
|
||||||
|
assert.equal(mappedFeature.enabled, feature.enabled);
|
||||||
|
assert.equal(mappedFeature.strategy, feature.strategies[0].name);
|
||||||
|
assert.notEqual(mappedFeature.parameters, feature.strategies[0].parameters);
|
||||||
|
assert.deepEqual(mappedFeature.parameters, feature.strategies[0].parameters);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('transforms fields to new format', () => {
|
||||||
|
const feature = {
|
||||||
|
name: 'test',
|
||||||
|
enabled: 0,
|
||||||
|
strategy: 'default',
|
||||||
|
parameters: {
|
||||||
|
val: 'bar',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mappedFeature = mapper.toNewFormat(feature);
|
||||||
|
|
||||||
|
assert.equal(mappedFeature.name, feature.name);
|
||||||
|
assert.equal(mappedFeature.enabled, feature.enabled);
|
||||||
|
assert.equal(mappedFeature.strategies.length, 1);
|
||||||
|
assert.equal(mappedFeature.strategies[0].name, feature.strategy);
|
||||||
|
assert.deepEqual(mappedFeature.strategies[0].parameters, feature.parameters);
|
||||||
|
assert(mappedFeature.strategy === undefined);
|
||||||
|
assert(mappedFeature.parameters === undefined);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user