mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +01:00
feat: Added import & export through stateService #395
This commit is contained in:
parent
db3d88e6cd
commit
5e8059dcf1
@ -1,5 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const { DROP_FEATURES } = require('../event-type');
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
|
|
||||||
const EVENT_COLUMNS = ['id', 'type', 'created_by', 'created_at', 'data'];
|
const EVENT_COLUMNS = ['id', 'type', 'created_by', 'created_at', 'data'];
|
||||||
@ -35,6 +36,14 @@ class EventStore extends EventEmitter {
|
|||||||
.from('events')
|
.from('events')
|
||||||
.limit(100)
|
.limit(100)
|
||||||
.whereRaw("data ->> 'name' = ?", [name])
|
.whereRaw("data ->> 'name' = ?", [name])
|
||||||
|
.andWhere(
|
||||||
|
'id',
|
||||||
|
'>=',
|
||||||
|
this.db
|
||||||
|
.select(this.db.raw('coalesce(max(id),0) as id'))
|
||||||
|
.from('events')
|
||||||
|
.where({ type: DROP_FEATURES })
|
||||||
|
)
|
||||||
.orderBy('created_at', 'desc')
|
.orderBy('created_at', 'desc')
|
||||||
.map(this.rowToEvent);
|
.map(this.rowToEvent);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ const {
|
|||||||
FEATURE_UPDATED,
|
FEATURE_UPDATED,
|
||||||
FEATURE_ARCHIVED,
|
FEATURE_ARCHIVED,
|
||||||
FEATURE_REVIVED,
|
FEATURE_REVIVED,
|
||||||
|
FEATURE_IMPORT,
|
||||||
|
DROP_FEATURES,
|
||||||
} = require('../event-type');
|
} = require('../event-type');
|
||||||
const logger = require('../logger')('client-toggle-store.js');
|
const logger = require('../logger')('client-toggle-store.js');
|
||||||
const NotFoundError = require('../error/notfound-error');
|
const NotFoundError = require('../error/notfound-error');
|
||||||
@ -33,6 +35,8 @@ class FeatureToggleStore {
|
|||||||
eventStore.on(FEATURE_REVIVED, event =>
|
eventStore.on(FEATURE_REVIVED, event =>
|
||||||
this._reviveFeature(event.data)
|
this._reviveFeature(event.data)
|
||||||
);
|
);
|
||||||
|
eventStore.on(FEATURE_IMPORT, event => this._importFeature(event.data));
|
||||||
|
eventStore.on(DROP_FEATURES, () => this._dropFeatures());
|
||||||
}
|
}
|
||||||
|
|
||||||
getFeatures() {
|
getFeatures() {
|
||||||
@ -137,6 +141,29 @@ class FeatureToggleStore {
|
|||||||
logger.error('Could not archive feature, error was: ', err)
|
logger.error('Could not archive feature, error was: ', err)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_importFeature(data) {
|
||||||
|
data = this.eventDataToRow(data);
|
||||||
|
return this.db
|
||||||
|
.raw(`? ON CONFLICT (name) DO ?`, [
|
||||||
|
this.db(TABLE).insert(data),
|
||||||
|
this.db
|
||||||
|
.queryBuilder()
|
||||||
|
.update(data)
|
||||||
|
.update('archived', 0),
|
||||||
|
])
|
||||||
|
.catch(err =>
|
||||||
|
logger.error('Could not import feature, error was: ', err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_dropFeatures() {
|
||||||
|
return this.db(TABLE)
|
||||||
|
.delete()
|
||||||
|
.catch(err =>
|
||||||
|
logger.error('Could not drop features, error was: ', err)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = FeatureToggleStore;
|
module.exports = FeatureToggleStore;
|
||||||
|
@ -4,6 +4,8 @@ const {
|
|||||||
STRATEGY_CREATED,
|
STRATEGY_CREATED,
|
||||||
STRATEGY_DELETED,
|
STRATEGY_DELETED,
|
||||||
STRATEGY_UPDATED,
|
STRATEGY_UPDATED,
|
||||||
|
STRATEGY_IMPORT,
|
||||||
|
DROP_STRATEGIES,
|
||||||
} = require('../event-type');
|
} = require('../event-type');
|
||||||
const logger = require('../logger')('strategy-store.js');
|
const logger = require('../logger')('strategy-store.js');
|
||||||
const NotFoundError = require('../error/notfound-error');
|
const NotFoundError = require('../error/notfound-error');
|
||||||
@ -19,14 +21,13 @@ class StrategyStore {
|
|||||||
eventStore.on(STRATEGY_UPDATED, event =>
|
eventStore.on(STRATEGY_UPDATED, event =>
|
||||||
this._updateStrategy(event.data)
|
this._updateStrategy(event.data)
|
||||||
);
|
);
|
||||||
eventStore.on(STRATEGY_DELETED, event => {
|
eventStore.on(STRATEGY_DELETED, event =>
|
||||||
db(TABLE)
|
this._deleteStrategy(event.data)
|
||||||
.where('name', event.data.name)
|
);
|
||||||
.del()
|
eventStore.on(STRATEGY_IMPORT, event =>
|
||||||
.catch(err => {
|
this._importStrategy(event.data)
|
||||||
logger.error('Could not delete strategy, error was: ', err);
|
);
|
||||||
});
|
eventStore.on(DROP_STRATEGIES, () => this._dropStrategies());
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getStrategies() {
|
getStrategies() {
|
||||||
@ -81,6 +82,36 @@ class StrategyStore {
|
|||||||
logger.error('Could not update strategy, error was: ', err)
|
logger.error('Could not update strategy, error was: ', err)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_deleteStrategy({ name }) {
|
||||||
|
return this.db(TABLE)
|
||||||
|
.where({ name })
|
||||||
|
.del()
|
||||||
|
.catch(err => {
|
||||||
|
logger.error('Could not delete strategy, error was: ', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_importStrategy(data) {
|
||||||
|
data = this.eventDataToRow(data);
|
||||||
|
return this.db
|
||||||
|
.raw(`? ON CONFLICT (name) DO ?`, [
|
||||||
|
this.db(TABLE).insert(data),
|
||||||
|
this.db.queryBuilder().update(data).where(`${TABLE}.built_in`, 0), // eslint-disable-line
|
||||||
|
])
|
||||||
|
.catch(err =>
|
||||||
|
logger.error('Could not import strategy, error was: ', err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_dropStrategies() {
|
||||||
|
return this.db(TABLE)
|
||||||
|
.where({ built_in: 0 }) // eslint-disable-line
|
||||||
|
.delete()
|
||||||
|
.catch(err =>
|
||||||
|
logger.error('Could not drop strategies, error was: ', err)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = StrategyStore;
|
module.exports = StrategyStore;
|
||||||
|
@ -4,20 +4,32 @@ const {
|
|||||||
STRATEGY_CREATED,
|
STRATEGY_CREATED,
|
||||||
STRATEGY_DELETED,
|
STRATEGY_DELETED,
|
||||||
STRATEGY_UPDATED,
|
STRATEGY_UPDATED,
|
||||||
|
STRATEGY_IMPORT,
|
||||||
|
DROP_STRATEGIES,
|
||||||
FEATURE_CREATED,
|
FEATURE_CREATED,
|
||||||
FEATURE_UPDATED,
|
FEATURE_UPDATED,
|
||||||
FEATURE_ARCHIVED,
|
FEATURE_ARCHIVED,
|
||||||
FEATURE_REVIVED,
|
FEATURE_REVIVED,
|
||||||
|
FEATURE_IMPORT,
|
||||||
|
DROP_FEATURES,
|
||||||
} = require('./event-type');
|
} = require('./event-type');
|
||||||
const diff = require('deep-diff').diff;
|
const diff = require('deep-diff').diff;
|
||||||
|
|
||||||
const strategyTypes = [STRATEGY_CREATED, STRATEGY_DELETED, STRATEGY_UPDATED];
|
const strategyTypes = [
|
||||||
|
STRATEGY_CREATED,
|
||||||
|
STRATEGY_DELETED,
|
||||||
|
STRATEGY_UPDATED,
|
||||||
|
STRATEGY_IMPORT,
|
||||||
|
DROP_STRATEGIES,
|
||||||
|
];
|
||||||
|
|
||||||
const featureTypes = [
|
const featureTypes = [
|
||||||
FEATURE_CREATED,
|
FEATURE_CREATED,
|
||||||
FEATURE_UPDATED,
|
FEATURE_UPDATED,
|
||||||
FEATURE_ARCHIVED,
|
FEATURE_ARCHIVED,
|
||||||
FEATURE_REVIVED,
|
FEATURE_REVIVED,
|
||||||
|
FEATURE_IMPORT,
|
||||||
|
DROP_FEATURES,
|
||||||
];
|
];
|
||||||
|
|
||||||
function baseTypeFor(event) {
|
function baseTypeFor(event) {
|
||||||
|
@ -5,7 +5,11 @@ module.exports = {
|
|||||||
FEATURE_UPDATED: 'feature-updated',
|
FEATURE_UPDATED: 'feature-updated',
|
||||||
FEATURE_ARCHIVED: 'feature-archived',
|
FEATURE_ARCHIVED: 'feature-archived',
|
||||||
FEATURE_REVIVED: 'feature-revived',
|
FEATURE_REVIVED: 'feature-revived',
|
||||||
|
FEATURE_IMPORT: 'feature-import',
|
||||||
|
DROP_FEATURES: 'drop-features',
|
||||||
STRATEGY_CREATED: 'strategy-created',
|
STRATEGY_CREATED: 'strategy-created',
|
||||||
STRATEGY_DELETED: 'strategy-deleted',
|
STRATEGY_DELETED: 'strategy-deleted',
|
||||||
STRATEGY_UPDATED: 'strategy-updated',
|
STRATEGY_UPDATED: 'strategy-updated',
|
||||||
|
STRATEGY_IMPORT: 'strategy-import',
|
||||||
|
DROP_STRATEGIES: 'drop-strategies',
|
||||||
};
|
};
|
||||||
|
@ -20,6 +20,8 @@ const DEFAULT_OPTIONS = {
|
|||||||
sessionAge: THIRTY_DAYS,
|
sessionAge: THIRTY_DAYS,
|
||||||
adminAuthentication: 'unsecure',
|
adminAuthentication: 'unsecure',
|
||||||
ui: {},
|
ui: {},
|
||||||
|
importFile: undefined,
|
||||||
|
dropBeforeImport: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -15,6 +15,9 @@
|
|||||||
},
|
},
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"uri": "/api/admin/metrics"
|
"uri": "/api/admin/metrics"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"uri": "/api/admin/state"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ const StrategyController = require('./strategy');
|
|||||||
const MetricsController = require('./metrics');
|
const MetricsController = require('./metrics');
|
||||||
const UserController = require('./user');
|
const UserController = require('./user');
|
||||||
const ConfigController = require('./config');
|
const ConfigController = require('./config');
|
||||||
|
const StateController = require('./state');
|
||||||
const apiDef = require('./api-def.json');
|
const apiDef = require('./api-def.json');
|
||||||
|
|
||||||
class AdminApi extends Controller {
|
class AdminApi extends Controller {
|
||||||
@ -22,6 +23,7 @@ class AdminApi extends Controller {
|
|||||||
this.app.use('/metrics', new MetricsController(config).router);
|
this.app.use('/metrics', new MetricsController(config).router);
|
||||||
this.app.use('/user', new UserController(config).router);
|
this.app.use('/user', new UserController(config).router);
|
||||||
this.app.use('/ui-config', new ConfigController(config).router);
|
this.app.use('/ui-config', new ConfigController(config).router);
|
||||||
|
this.app.use('/state', new StateController(config).router);
|
||||||
}
|
}
|
||||||
|
|
||||||
index(req, res) {
|
index(req, res) {
|
||||||
|
80
lib/routes/admin-api/state.js
Normal file
80
lib/routes/admin-api/state.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const Controller = require('../controller');
|
||||||
|
const { ADMIN } = require('../../permissions');
|
||||||
|
const extractUser = require('../../extract-user');
|
||||||
|
const { handleErrors } = require('./util');
|
||||||
|
const YAML = require('js-yaml');
|
||||||
|
const moment = require('moment');
|
||||||
|
const multer = require('multer');
|
||||||
|
const upload = multer({ limits: { fileSize: 5242880 } });
|
||||||
|
|
||||||
|
class ImportController extends Controller {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.fileupload('/import', upload.single('file'), this.import, ADMIN);
|
||||||
|
this.get('/export', this.export, ADMIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
async import(req, res) {
|
||||||
|
const userName = extractUser(req);
|
||||||
|
const { drop } = req.query;
|
||||||
|
|
||||||
|
let data;
|
||||||
|
if (req.file) {
|
||||||
|
if (req.file.mimetype === 'text/yaml') {
|
||||||
|
data = YAML.safeLoad(req.file.buffer);
|
||||||
|
} else {
|
||||||
|
data = JSON.parse(req.file.buffer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data = req.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.config.stateService.import({
|
||||||
|
data,
|
||||||
|
userName,
|
||||||
|
dropBeforeImport: drop,
|
||||||
|
});
|
||||||
|
res.sendStatus(202);
|
||||||
|
} catch (err) {
|
||||||
|
handleErrors(res, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async export(req, res) {
|
||||||
|
const { format } = req.query;
|
||||||
|
|
||||||
|
let strategies = 'strategies' in req.query;
|
||||||
|
let featureToggles = 'features' in req.query;
|
||||||
|
|
||||||
|
if (!strategies && !featureToggles) {
|
||||||
|
strategies = true;
|
||||||
|
featureToggles = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.config.stateService.export({
|
||||||
|
strategies,
|
||||||
|
featureToggles,
|
||||||
|
});
|
||||||
|
const timestamp = moment().format('YYYY-MM-DD_HH-mm-ss');
|
||||||
|
if (format === 'yaml') {
|
||||||
|
if ('download' in req.query) {
|
||||||
|
res.attachment(`export-${timestamp}.yml`);
|
||||||
|
}
|
||||||
|
res.type('yaml').send(YAML.safeDump(data));
|
||||||
|
} else {
|
||||||
|
if ('download' in req.query) {
|
||||||
|
res.attachment(`export-${timestamp}.json`);
|
||||||
|
}
|
||||||
|
res.json(data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleErrors(res, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ImportController;
|
@ -44,6 +44,15 @@ class Controller {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileupload(path, filehandler, handler, permission) {
|
||||||
|
this.app.post(
|
||||||
|
path,
|
||||||
|
checkPermission(this.config, permission),
|
||||||
|
filehandler,
|
||||||
|
handler.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
use(path, router) {
|
use(path, router) {
|
||||||
this.app.use(path, router);
|
this.app.use(path, router);
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,11 @@ const getApp = require('./app');
|
|||||||
const { startMonitoring } = require('./metrics');
|
const { startMonitoring } = require('./metrics');
|
||||||
const { createStores } = require('./db');
|
const { createStores } = require('./db');
|
||||||
const { createOptions } = require('./options');
|
const { createOptions } = require('./options');
|
||||||
|
const StateService = require('./state-service');
|
||||||
const User = require('./user');
|
const User = require('./user');
|
||||||
const AuthenticationRequired = require('./authentication-required');
|
const AuthenticationRequired = require('./authentication-required');
|
||||||
|
|
||||||
function createApp(options) {
|
async function createApp(options) {
|
||||||
// Database dependecies (statefull)
|
// Database dependecies (statefull)
|
||||||
const stores = createStores(options);
|
const stores = createStores(options);
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
@ -35,12 +36,29 @@ function createApp(options) {
|
|||||||
stores.clientMetricsStore
|
stores.clientMetricsStore
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const stateService = new StateService(config);
|
||||||
|
config.stateService = stateService;
|
||||||
|
if (config.importFile) {
|
||||||
|
await stateService.importFile({
|
||||||
|
importFile: config.importFile,
|
||||||
|
dropBeforeImport: config.dropBeforeImport,
|
||||||
|
userName: 'importer',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const server = app.listen({ port: options.port, host: options.host }, () =>
|
const server = app.listen({ port: options.port, host: options.host }, () =>
|
||||||
logger.info(`Unleash started on port ${server.address().port}`)
|
logger.info(`Unleash started on port ${server.address().port}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
server.on('listening', () => resolve({ app, server, eventBus }));
|
server.on('listening', () =>
|
||||||
|
resolve({
|
||||||
|
app,
|
||||||
|
server,
|
||||||
|
eventBus,
|
||||||
|
stateService,
|
||||||
|
})
|
||||||
|
);
|
||||||
server.on('error', reject);
|
server.on('error', reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
122
lib/state-service.js
Normal file
122
lib/state-service.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const joi = require('joi');
|
||||||
|
const fs = require('fs');
|
||||||
|
const mime = require('mime');
|
||||||
|
const { featureShema } = require('./routes/admin-api/feature-schema');
|
||||||
|
const strategySchema = require('./routes/admin-api/strategy-schema');
|
||||||
|
const getLogger = require('./logger');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
const {
|
||||||
|
FEATURE_IMPORT,
|
||||||
|
DROP_FEATURES,
|
||||||
|
STRATEGY_IMPORT,
|
||||||
|
DROP_STRATEGIES,
|
||||||
|
} = require('./event-type');
|
||||||
|
|
||||||
|
const logger = getLogger('state-service.js');
|
||||||
|
|
||||||
|
const dataSchema = joi.object().keys({
|
||||||
|
features: joi
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
.items(featureShema),
|
||||||
|
strategies: joi
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
.items(strategySchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
class StateService {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
async importFile({ importFile, dropBeforeImport, userName }) {
|
||||||
|
let data = await new Promise((resolve, reject) =>
|
||||||
|
fs.readFile(importFile, (err, v) =>
|
||||||
|
err ? reject(err) : resolve(v)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (mime.lookup(importFile) === 'text/yaml') {
|
||||||
|
data = yaml.safeLoad(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.import({
|
||||||
|
data,
|
||||||
|
dropBeforeImport,
|
||||||
|
userName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async import({ data, userName, dropBeforeImport }) {
|
||||||
|
const { eventStore } = this.config.stores;
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
data = await joi.validate(data, dataSchema);
|
||||||
|
|
||||||
|
if (data.features) {
|
||||||
|
logger.info(`Importing ${data.features.length} features`);
|
||||||
|
if (dropBeforeImport) {
|
||||||
|
logger.info(`Dropping existing features`);
|
||||||
|
await eventStore.store({
|
||||||
|
type: DROP_FEATURES,
|
||||||
|
createdBy: userName,
|
||||||
|
data: { name: 'all-features' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const feature of data.features) {
|
||||||
|
await eventStore.store({
|
||||||
|
type: FEATURE_IMPORT,
|
||||||
|
createdBy: userName,
|
||||||
|
data: feature,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.strategies) {
|
||||||
|
logger.info(`Importing ${data.strategies.length} strategies`);
|
||||||
|
if (dropBeforeImport) {
|
||||||
|
logger.info(`Dropping existing strategies`);
|
||||||
|
await eventStore.store({
|
||||||
|
type: DROP_STRATEGIES,
|
||||||
|
createdBy: userName,
|
||||||
|
data: { name: 'all-strategies' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const strategy of data.strategies) {
|
||||||
|
await eventStore.store({
|
||||||
|
type: STRATEGY_IMPORT,
|
||||||
|
createdBy: userName,
|
||||||
|
data: strategy,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async export({ strategies, featureToggles }) {
|
||||||
|
const { featureToggleStore, strategyStore } = this.config.stores;
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
if (featureToggles) {
|
||||||
|
result.features = await featureToggleStore.getFeatures();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strategies) {
|
||||||
|
result.strategies = (await strategyStore.getStrategies())
|
||||||
|
.filter(strat => strat.editable)
|
||||||
|
.map(strat => {
|
||||||
|
strat = Object.assign({}, strat);
|
||||||
|
delete strat.editable;
|
||||||
|
return strat;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = StateService;
|
148
lib/state-service.test.js
Normal file
148
lib/state-service.test.js
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const test = require('ava');
|
||||||
|
|
||||||
|
const store = require('./../test/fixtures/store');
|
||||||
|
const StateService = require('./state-service');
|
||||||
|
const {
|
||||||
|
FEATURE_IMPORT,
|
||||||
|
DROP_FEATURES,
|
||||||
|
STRATEGY_IMPORT,
|
||||||
|
DROP_STRATEGIES,
|
||||||
|
} = require('./event-type');
|
||||||
|
|
||||||
|
function getSetup() {
|
||||||
|
const stores = store.createStores();
|
||||||
|
return { stateService: new StateService({ stores }), stores };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should import a feature', async t => {
|
||||||
|
const { stateService, stores } = getSetup();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
name: 'new-feature',
|
||||||
|
enabled: true,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await stateService.import({ data });
|
||||||
|
|
||||||
|
const events = await stores.eventStore.getEvents();
|
||||||
|
t.is(events.length, 1);
|
||||||
|
t.is(events[0].type, FEATURE_IMPORT);
|
||||||
|
t.is(events[0].data.name, 'new-feature');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should drop feature before import if specified', async t => {
|
||||||
|
const { stateService, stores } = getSetup();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
name: 'new-feature',
|
||||||
|
enabled: true,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await stateService.import({ data, dropBeforeImport: true });
|
||||||
|
|
||||||
|
const events = await stores.eventStore.getEvents();
|
||||||
|
t.is(events.length, 2);
|
||||||
|
t.is(events[0].type, DROP_FEATURES);
|
||||||
|
t.is(events[1].type, FEATURE_IMPORT);
|
||||||
|
t.is(events[1].data.name, 'new-feature');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should import a strategy', async t => {
|
||||||
|
const { stateService, stores } = getSetup();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
strategies: [
|
||||||
|
{
|
||||||
|
name: 'new-strategy',
|
||||||
|
parameters: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await stateService.import({ data });
|
||||||
|
|
||||||
|
const events = await stores.eventStore.getEvents();
|
||||||
|
t.is(events.length, 1);
|
||||||
|
t.is(events[0].type, STRATEGY_IMPORT);
|
||||||
|
t.is(events[0].data.name, 'new-strategy');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should drop strategies before import if specified', async t => {
|
||||||
|
const { stateService, stores } = getSetup();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
strategies: [
|
||||||
|
{
|
||||||
|
name: 'new-strategy',
|
||||||
|
parameters: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await stateService.import({ data, dropBeforeImport: true });
|
||||||
|
|
||||||
|
const events = await stores.eventStore.getEvents();
|
||||||
|
t.is(events.length, 2);
|
||||||
|
t.is(events[0].type, DROP_STRATEGIES);
|
||||||
|
t.is(events[1].type, STRATEGY_IMPORT);
|
||||||
|
t.is(events[1].data.name, 'new-strategy');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should drop neither features nor strategies when neither is imported', async t => {
|
||||||
|
const { stateService, stores } = getSetup();
|
||||||
|
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
await stateService.import({ data, dropBeforeImport: true });
|
||||||
|
|
||||||
|
const events = await stores.eventStore.getEvents();
|
||||||
|
t.is(events.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not accept gibberish', async t => {
|
||||||
|
const { stateService } = getSetup();
|
||||||
|
|
||||||
|
const data1 = {
|
||||||
|
type: 'gibberish',
|
||||||
|
flags: { evil: true },
|
||||||
|
};
|
||||||
|
const data2 = '{somerandomtext/';
|
||||||
|
|
||||||
|
await t.throwsAsync(stateService.import({ data: data1 }));
|
||||||
|
|
||||||
|
await t.throwsAsync(stateService.import({ data: data2 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should export featureToggles', async t => {
|
||||||
|
const { stateService, stores } = getSetup();
|
||||||
|
|
||||||
|
stores.featureToggleStore.addFeature({ name: 'a-feature' });
|
||||||
|
|
||||||
|
const data = await stateService.export({ featureToggles: true });
|
||||||
|
|
||||||
|
t.is(data.features.length, 1);
|
||||||
|
t.is(data.features[0].name, 'a-feature');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should export strategies', async t => {
|
||||||
|
const { stateService, stores } = getSetup();
|
||||||
|
|
||||||
|
stores.strategyStore.addStrategy({ name: 'a-strategy', editable: true });
|
||||||
|
|
||||||
|
const data = await stateService.export({ strategies: true });
|
||||||
|
|
||||||
|
t.is(data.strategies.length, 1);
|
||||||
|
t.is(data.strategies[0].name, 'a-strategy');
|
||||||
|
});
|
@ -69,9 +69,12 @@
|
|||||||
"gravatar": "^1.8.0",
|
"gravatar": "^1.8.0",
|
||||||
"install": "^0.12.2",
|
"install": "^0.12.2",
|
||||||
"joi": "^14.3.1",
|
"joi": "^14.3.1",
|
||||||
|
"js-yaml": "^3.12.2",
|
||||||
"knex": "^0.16.3",
|
"knex": "^0.16.3",
|
||||||
"log4js": "^4.0.0",
|
"log4js": "^4.0.0",
|
||||||
|
"mime": "^1.4.1",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
|
"multer": "^1.4.1",
|
||||||
"parse-database-url": "^0.3.0",
|
"parse-database-url": "^0.3.0",
|
||||||
"pg": "^7.8.1",
|
"pg": "^7.8.1",
|
||||||
"pkginfo": "^0.4.1",
|
"pkginfo": "^0.4.1",
|
||||||
|
2
test/fixtures/fake-event-store.js
vendored
2
test/fixtures/fake-event-store.js
vendored
@ -6,7 +6,7 @@ module.exports = () => {
|
|||||||
return {
|
return {
|
||||||
store: event => {
|
store: event => {
|
||||||
events.push(event);
|
events.push(event);
|
||||||
Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
getEvents: () => Promise.resolve(events),
|
getEvents: () => Promise.resolve(events),
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user