1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-31 01:16:01 +02:00

Prettier/lint

This commit is contained in:
sveisvei 2017-06-28 10:17:14 +02:00 committed by Ivar Conradi Østhus
parent 0ea10bc9b2
commit 375a8b1d4f
34 changed files with 440 additions and 274 deletions

View File

@ -1,12 +1,22 @@
{ {
"extends": [ "extends": [
"finn", "finn",
"finn/node" "finn/node",
"finn-prettier"
], ],
"parserOptions": { "parserOptions": {
"ecmaVersion": "2017" "ecmaVersion": "2017"
}, },
"rules": { "rules": {
"max-nested-callbacks": "off" "max-nested-callbacks": "off",
"new-cap": [
"error",
{
"capIsNewExceptions": [
"Router",
"Mitm"
]
}
]
} }
} }

View File

@ -14,7 +14,7 @@ const errorHandler = require('errorhandler');
const { REQUEST_TIME } = require('./events'); const { REQUEST_TIME } = require('./events');
module.exports = function (config) { module.exports = function(config) {
const app = express(); const app = express();
const baseUriPath = config.baseUriPath || ''; const baseUriPath = config.baseUriPath || '';
@ -34,10 +34,17 @@ module.exports = function (config) {
app.use(favicon(path.join(publicFolder, 'favicon.ico'))); app.use(favicon(path.join(publicFolder, 'favicon.ico')));
} }
app.use(responseTime((req, res, time) => { app.use(
const timingInfo = { path: req.path, method: req.method, statusCode: res.statusCode, time }; responseTime((req, res, time) => {
config.eventBus.emit(REQUEST_TIME, timingInfo); const timingInfo = {
})); path: req.path,
method: req.method,
statusCode: res.statusCode,
time,
};
config.eventBus.emit(REQUEST_TIME, timingInfo);
})
);
app.use(validator([])); app.use(validator([]));
@ -48,10 +55,12 @@ module.exports = function (config) {
app.use(bodyParser.json({ strict: false })); app.use(bodyParser.json({ strict: false }));
if (config.enableRequestLogger) { if (config.enableRequestLogger) {
app.use(log4js.connectLogger(logger, { app.use(
format: ':status :method :url :response-timems', log4js.connectLogger(logger, {
level: 'auto', // 3XX=WARN, 4xx/5xx=ERROR format: ':status :method :url :response-timems',
})); level: 'auto', // 3XX=WARN, 4xx/5xx=ERROR
})
);
} }
if (typeof config.preRouterHook === 'function') { if (typeof config.preRouterHook === 'function') {

View File

@ -16,16 +16,20 @@ test('should not throw when valid config', t => {
test('should call preHook', t => { test('should call preHook', t => {
let called = 0; let called = 0;
getApp({ preHook: () => { getApp({
called++; preHook: () => {
} }); called++;
},
});
t.true(called === 1); t.true(called === 1);
}); });
test('should call preRouterHook', t => { test('should call preRouterHook', t => {
let called = 0; let called = 0;
getApp({ preRouterHook: () => { getApp({
called++; preRouterHook: () => {
} }); called++;
},
});
t.true(called === 1); t.true(called === 1);
}); });

View File

@ -10,7 +10,7 @@ const { EventEmitter } = require('events');
const appName = 'appName'; const appName = 'appName';
const instanceId = 'instanceId'; const instanceId = 'instanceId';
test('should work without state', (t) => { test('should work without state', t => {
const store = new EventEmitter(); const store = new EventEmitter();
const metrics = new UnleashClientMetrics(store); const metrics = new UnleashClientMetrics(store);
@ -20,7 +20,7 @@ test('should work without state', (t) => {
metrics.destroy(); metrics.destroy();
}); });
test.cb('data should expire', (t) => { test.cb('data should expire', t => {
const clock = sinon.useFakeTimers(); const clock = sinon.useFakeTimers();
const store = new EventEmitter(); const store = new EventEmitter();
@ -84,9 +84,14 @@ test('should listen to metrics from store', t => {
t.truthy(metrics.apps[appName].count === 123); t.truthy(metrics.apps[appName].count === 123);
t.truthy(metrics.globalCount === 123); t.truthy(metrics.globalCount === 123);
t.deepEqual(metrics.getTogglesMetrics().lastHour.toggleX, { yes: 123, no: 0 }); t.deepEqual(metrics.getTogglesMetrics().lastHour.toggleX, {
t.deepEqual(metrics.getTogglesMetrics().lastMinute.toggleX, { yes: 123, no: 0 }); yes: 123,
no: 0,
});
t.deepEqual(metrics.getTogglesMetrics().lastMinute.toggleX, {
yes: 123,
no: 0,
});
metrics.addPayload({ metrics.addPayload({
appName, appName,
@ -104,8 +109,14 @@ test('should listen to metrics from store', t => {
}); });
t.truthy(metrics.globalCount === 143); t.truthy(metrics.globalCount === 143);
t.deepEqual(metrics.getTogglesMetrics().lastHour.toggleX, { yes: 133, no: 10 }); t.deepEqual(metrics.getTogglesMetrics().lastHour.toggleX, {
t.deepEqual(metrics.getTogglesMetrics().lastMinute.toggleX, { yes: 133, no: 10 }); yes: 133,
no: 10,
});
t.deepEqual(metrics.getTogglesMetrics().lastMinute.toggleX, {
yes: 133,
no: 10,
});
metrics.destroy(); metrics.destroy();
}); });
@ -146,7 +157,6 @@ test('should build up list of seend toggles when new metrics arrives', t => {
metrics.destroy(); metrics.destroy();
}); });
test('should handle a lot of toggles', t => { test('should handle a lot of toggles', t => {
const store = new EventEmitter(); const store = new EventEmitter();
const metrics = new UnleashClientMetrics(store); const metrics = new UnleashClientMetrics(store);
@ -244,7 +254,6 @@ test('should have correct values for lastMinute', t => {
clock.restore(); clock.restore();
}); });
test('should have correct values for lastHour', t => { test('should have correct values for lastHour', t => {
const clock = sinon.useFakeTimers(); const clock = sinon.useFakeTimers();

View File

@ -4,7 +4,7 @@ const Projection = require('./projection.js');
const TTLList = require('./ttl-list.js'); const TTLList = require('./ttl-list.js');
module.exports = class UnleashClientMetrics { module.exports = class UnleashClientMetrics {
constructor (clientMetricsStore) { constructor(clientMetricsStore) {
this.globalCount = 0; this.globalCount = 0;
this.apps = {}; this.apps = {};
@ -21,20 +21,26 @@ module.exports = class UnleashClientMetrics {
expireAmount: 1, expireAmount: 1,
}); });
this.lastHourList.on('expire', (toggles) => { this.lastHourList.on('expire', toggles => {
Object.keys(toggles).forEach(toggleName => { Object.keys(toggles).forEach(toggleName => {
this.lastHourProjection.substract(toggleName, toggles[toggleName]); this.lastHourProjection.substract(
toggleName,
toggles[toggleName]
);
}); });
}); });
this.lastMinuteList.on('expire', (toggles) => { this.lastMinuteList.on('expire', toggles => {
Object.keys(toggles).forEach(toggleName => { Object.keys(toggles).forEach(toggleName => {
this.lastMinuteProjection.substract(toggleName, toggles[toggleName]); this.lastMinuteProjection.substract(
toggleName,
toggles[toggleName]
);
}); });
}); });
clientMetricsStore.on('metrics', (m) => this.addPayload(m)); clientMetricsStore.on('metrics', m => this.addPayload(m));
} }
getAppsWithToggles () { getAppsWithToggles() {
const apps = []; const apps = [];
Object.keys(this.apps).forEach(appName => { Object.keys(this.apps).forEach(appName => {
const seenToggles = Object.keys(this.apps[appName].seenToggles); const seenToggles = Object.keys(this.apps[appName].seenToggles);
@ -43,14 +49,18 @@ module.exports = class UnleashClientMetrics {
}); });
return apps; return apps;
} }
getSeenTogglesByAppName (appName) { getSeenTogglesByAppName(appName) {
return this.apps[appName] ? Object.keys(this.apps[appName].seenToggles) : []; return this.apps[appName]
? Object.keys(this.apps[appName].seenToggles)
: [];
} }
getSeenAppsPerToggle () { getSeenAppsPerToggle() {
const toggles = {}; const toggles = {};
Object.keys(this.apps).forEach(appName => { Object.keys(this.apps).forEach(appName => {
Object.keys(this.apps[appName].seenToggles).forEach((seenToggleName) => { Object.keys(
this.apps[appName].seenToggles
).forEach(seenToggleName => {
if (!toggles[seenToggleName]) { if (!toggles[seenToggleName]) {
toggles[seenToggleName] = []; toggles[seenToggleName] = [];
} }
@ -60,36 +70,39 @@ module.exports = class UnleashClientMetrics {
return toggles; return toggles;
} }
getTogglesMetrics () { getTogglesMetrics() {
return { return {
lastHour: this.lastHourProjection.getProjection(), lastHour: this.lastHourProjection.getProjection(),
lastMinute: this.lastMinuteProjection.getProjection(), lastMinute: this.lastMinuteProjection.getProjection(),
}; };
} }
addPayload (data) { addPayload(data) {
const { appName, bucket } = data; const { appName, bucket } = data;
const app = this.getApp(appName); const app = this.getApp(appName);
this.addBucket(app, bucket); this.addBucket(app, bucket);
} }
getApp (appName) { getApp(appName) {
this.apps[appName] = this.apps[appName] || { seenToggles: {}, count: 0 }; this.apps[appName] = this.apps[appName] || {
seenToggles: {},
count: 0,
};
return this.apps[appName]; return this.apps[appName];
} }
addBucket (app, bucket) { addBucket(app, bucket) {
let count = 0; let count = 0;
// TODO stop should be createdAt // TODO stop should be createdAt
const { stop, toggles } = bucket; const { stop, toggles } = bucket;
const toggleNames = Object.keys(toggles); const toggleNames = Object.keys(toggles);
toggleNames.forEach((n) => { toggleNames.forEach(n => {
const entry = toggles[n]; const entry = toggles[n];
this.lastHourProjection.add(n, entry); this.lastHourProjection.add(n, entry);
this.lastMinuteProjection.add(n, entry); this.lastMinuteProjection.add(n, entry);
count += (entry.yes + entry.no); count += entry.yes + entry.no;
}); });
this.lastHourList.add(toggles, stop); this.lastHourList.add(toggles, stop);
@ -100,13 +113,13 @@ module.exports = class UnleashClientMetrics {
this.addSeenToggles(app, toggleNames); this.addSeenToggles(app, toggleNames);
} }
addSeenToggles (app, toggleNames) { addSeenToggles(app, toggleNames) {
toggleNames.forEach(t => { toggleNames.forEach(t => {
app.seenToggles[t] = true; app.seenToggles[t] = true;
}); });
} }
destroy () { destroy() {
this.lastHourList.destroy(); this.lastHourList.destroy();
this.lastMinuteList.destroy(); this.lastMinuteList.destroy();
} }

View File

@ -3,12 +3,12 @@
const { EventEmitter } = require('events'); const { EventEmitter } = require('events');
class Node { class Node {
constructor (value) { constructor(value) {
this.value = value; this.value = value;
this.next = null; this.next = null;
} }
link (next) { link(next) {
this.next = next; this.next = next;
next.prev = this; next.prev = this;
return this; return this;
@ -16,13 +16,13 @@ class Node {
} }
module.exports = class List extends EventEmitter { module.exports = class List extends EventEmitter {
constructor () { constructor() {
super(); super();
this.start = null; this.start = null;
this.tail = null; this.tail = null;
} }
add (obj) { add(obj) {
const node = new Node(obj); const node = new Node(obj);
if (this.start) { if (this.start) {
this.start = node.link(this.start); this.start = node.link(this.start);
@ -33,7 +33,7 @@ module.exports = class List extends EventEmitter {
return node; return node;
} }
iterate (fn) { iterate(fn) {
if (!this.start) { if (!this.start) {
return; return;
} }
@ -48,7 +48,7 @@ module.exports = class List extends EventEmitter {
} }
} }
iterateReverse (fn) { iterateReverse(fn) {
if (!this.tail) { if (!this.tail) {
return; return;
} }
@ -63,7 +63,7 @@ module.exports = class List extends EventEmitter {
} }
} }
reverseRemoveUntilTrue (fn) { reverseRemoveUntilTrue(fn) {
if (!this.tail) { if (!this.tail) {
return; return;
} }
@ -95,7 +95,7 @@ module.exports = class List extends EventEmitter {
} }
} }
toArray () { toArray() {
const result = []; const result = [];
if (this.start) { if (this.start) {

View File

@ -1,9 +1,9 @@
'use strict'; 'use strict';
const test = require('ava'); const { test } = require('ava');
const List = require('./list'); const List = require('./list');
function getList () { function getList() {
const list = new List(); const list = new List();
list.add(1); list.add(1);
list.add(2); list.add(2);
@ -15,10 +15,10 @@ function getList () {
return list; return list;
} }
test('should emit "evicted" events for objects leaving list', (t) => { test('should emit "evicted" events for objects leaving list', t => {
const list = getList(); const list = getList();
const evictedList = []; const evictedList = [];
list.on('evicted', (value) => { list.on('evicted', value => {
evictedList.push(value); evictedList.push(value);
}); });
@ -43,7 +43,7 @@ test('should emit "evicted" events for objects leaving list', (t) => {
t.true(evictedList.length === 8); t.true(evictedList.length === 8);
}); });
test('list should be able remove until given value', (t) => { test('list should be able remove until given value', t => {
const list = getList(); const list = getList();
t.true(list.toArray().length === 7); t.true(list.toArray().length === 7);
@ -58,7 +58,7 @@ test('list should be able remove until given value', (t) => {
t.true(list.toArray().length === 3); t.true(list.toArray().length === 3);
}); });
test('list can be cleared and re-add entries', (t) => { test('list can be cleared and re-add entries', t => {
const list = getList(); const list = getList();
list.add(8); list.add(8);
@ -77,7 +77,7 @@ test('list can be cleared and re-add entries', (t) => {
t.true(list.toArray().length === 3); t.true(list.toArray().length === 3);
}); });
test('should not iterate empty list ', (t) => { test('should not iterate empty list ', t => {
const list = new List(); const list = new List();
let iterateCount = 0; let iterateCount = 0;
@ -87,8 +87,7 @@ test('should not iterate empty list ', (t) => {
t.true(iterateCount === 0); t.true(iterateCount === 0);
}); });
test('should iterate', t => {
test('should iterate', (t) => {
const list = getList(); const list = getList();
let iterateCount = 0; let iterateCount = 0;
@ -102,7 +101,7 @@ test('should iterate', (t) => {
t.true(iterateCount === 4); t.true(iterateCount === 4);
}); });
test('should reverse iterate', (t) => { test('should reverse iterate', t => {
const list = getList(); const list = getList();
let iterateCount = 0; let iterateCount = 0;
@ -116,7 +115,7 @@ test('should reverse iterate', (t) => {
t.true(iterateCount === 5); t.true(iterateCount === 5);
}); });
test('should not reverse iterate empty list', (t) => { test('should not reverse iterate empty list', t => {
const list = new List(); const list = new List();
let iterateCount = 0; let iterateCount = 0;

View File

@ -1,15 +1,15 @@
'use strict'; 'use strict';
module.exports = class Projection { module.exports = class Projection {
constructor () { constructor() {
this.store = {}; this.store = {};
} }
getProjection () { getProjection() {
return this.store; return this.store;
} }
add (name, countObj) { add(name, countObj) {
if (this.store[name]) { if (this.store[name]) {
this.store[name].yes += countObj.yes; this.store[name].yes += countObj.yes;
this.store[name].no += countObj.no; this.store[name].no += countObj.no;
@ -21,7 +21,7 @@ module.exports = class Projection {
} }
} }
substract (name, countObj) { substract(name, countObj) {
if (this.store[name]) { if (this.store[name]) {
this.store[name].yes -= countObj.yes; this.store[name].yes -= countObj.yes;
this.store[name].no -= countObj.no; this.store[name].no -= countObj.no;

View File

@ -6,11 +6,9 @@ const moment = require('moment');
// this list must have entries with sorted ttl range // this list must have entries with sorted ttl range
module.exports = class TTLList extends EventEmitter { module.exports = class TTLList extends EventEmitter {
constructor ({ constructor(
interval = 1000, { interval = 1000, expireAmount = 1, expireType = 'hours' } = {}
expireAmount = 1, ) {
expireType = 'hours',
} = {}) {
super(); super();
this.interval = interval; this.interval = interval;
this.expireAmount = expireAmount; this.expireAmount = expireAmount;
@ -24,7 +22,7 @@ module.exports = class TTLList extends EventEmitter {
this.startTimer(); this.startTimer();
} }
startTimer () { startTimer() {
if (this.list) { if (this.list) {
this.timer = setTimeout(() => { this.timer = setTimeout(() => {
if (this.list) { if (this.list) {
@ -35,7 +33,7 @@ module.exports = class TTLList extends EventEmitter {
} }
} }
add (value, timestamp = new Date()) { add(value, timestamp = new Date()) {
const ttl = moment(timestamp).add(this.expireAmount, this.expireType); const ttl = moment(timestamp).add(this.expireAmount, this.expireType);
if (moment().isBefore(ttl)) { if (moment().isBefore(ttl)) {
this.list.add({ ttl, value }); this.list.add({ ttl, value });
@ -44,13 +42,15 @@ module.exports = class TTLList extends EventEmitter {
} }
} }
timedCheck () { timedCheck() {
const now = moment(); const now = moment();
this.list.reverseRemoveUntilTrue(({ value }) => now.isBefore(value.ttl)); this.list.reverseRemoveUntilTrue(({ value }) =>
now.isBefore(value.ttl)
);
this.startTimer(); this.startTimer();
} }
destroy () { destroy() {
// https://github.com/nodejs/node/issues/9561 // https://github.com/nodejs/node/issues/9561
// clearTimeout(this.timer); // clearTimeout(this.timer);
// this.timer = null; // this.timer = null;

View File

@ -1,18 +1,18 @@
'use strict'; 'use strict';
const test = require('ava'); const { test } = require('ava');
const TTLList = require('./ttl-list'); const TTLList = require('./ttl-list');
const moment = require('moment'); const moment = require('moment');
const sinon = require('sinon'); const sinon = require('sinon');
test.cb('should emit expire', (t) => { test.cb('should emit expire', t => {
const list = new TTLList({ const list = new TTLList({
interval: 20, interval: 20,
expireAmount: 10, expireAmount: 10,
expireType: 'milliseconds', expireType: 'milliseconds',
}); });
list.on('expire', (entry) => { list.on('expire', entry => {
list.destroy(); list.destroy();
t.true(entry.n === 1); t.true(entry.n === 1);
t.end(); t.end();
@ -21,7 +21,7 @@ test.cb('should emit expire', (t) => {
list.add({ n: 1 }); list.add({ n: 1 });
}); });
test.cb('should slice off list', (t) => { test.cb('should slice off list', t => {
const clock = sinon.useFakeTimers(); const clock = sinon.useFakeTimers();
const list = new TTLList({ const list = new TTLList({
@ -30,7 +30,6 @@ test.cb('should slice off list', (t) => {
expireType: 'milliseconds', expireType: 'milliseconds',
}); });
list.add({ n: '1' }, moment().add(1, 'milliseconds')); list.add({ n: '1' }, moment().add(1, 'milliseconds'));
list.add({ n: '2' }, moment().add(50, 'milliseconds')); list.add({ n: '2' }, moment().add(50, 'milliseconds'));
list.add({ n: '3' }, moment().add(200, 'milliseconds')); list.add({ n: '3' }, moment().add(200, 'milliseconds'));
@ -38,7 +37,7 @@ test.cb('should slice off list', (t) => {
const expired = []; const expired = [];
list.on('expire', (entry) => { list.on('expire', entry => {
// console.timeEnd(entry.n); // console.timeEnd(entry.n);
expired.push(entry); expired.push(entry);
}); });

View File

@ -1,10 +1,19 @@
/* eslint camelcase:off */ /* eslint camelcase:off */
'use strict'; 'use strict';
const COLUMNS = ['app_name', 'created_at', 'updated_at', 'description', 'strategies', 'url', 'color', 'icon']; const COLUMNS = [
'app_name',
'created_at',
'updated_at',
'description',
'strategies',
'url',
'color',
'icon',
];
const TABLE = 'client_applications'; const TABLE = 'client_applications';
const mapRow = (row) => ({ const mapRow = row => ({
appName: row.app_name, appName: row.app_name,
createdAt: row.created_at, createdAt: row.created_at,
updatedAt: row.updated_at, updatedAt: row.updated_at,
@ -25,24 +34,23 @@ const remapRow = (input, old = {}) => ({
strategies: JSON.stringify(input.strategies || old.strategies), strategies: JSON.stringify(input.strategies || old.strategies),
}); });
class ClientApplicationsDb { class ClientApplicationsDb {
constructor (db) { constructor(db) {
this.db = db; this.db = db;
} }
updateRow (details, prev) { updateRow(details, prev) {
details.updatedAt = 'now()'; details.updatedAt = 'now()';
return this.db(TABLE) return this.db(TABLE)
.where('app_name', details.appName) .where('app_name', details.appName)
.update(remapRow(details, prev)); .update(remapRow(details, prev));
} }
insertNewRow (details) { insertNewRow(details) {
return this.db(TABLE).insert(remapRow(details)); return this.db(TABLE).insert(remapRow(details));
} }
upsert (data) { upsert(data) {
if (!data) { if (!data) {
throw new Error('Missing data to add / update'); throw new Error('Missing data to add / update');
} }
@ -58,20 +66,17 @@ class ClientApplicationsDb {
}); });
} }
getAll () { getAll() {
return this.db return this.db.select(COLUMNS).from(TABLE).map(mapRow);
.select(COLUMNS)
.from(TABLE)
.map(mapRow);
} }
getApplication (appName) { getApplication(appName) {
return this.db return this.db
.select(COLUMNS) .select(COLUMNS)
.where('app_name', appName) .where('app_name', appName)
.from(TABLE) .from(TABLE)
.map(mapRow) .map(mapRow)
.then(list => list[0]); .then(list => list[0]);
} }
/** /**
@ -83,19 +88,21 @@ class ClientApplicationsDb {
* ) as foo * ) as foo
* WHERE foo.strategyName = '"other"'; * WHERE foo.strategyName = '"other"';
*/ */
getAppsForStrategy (strategyName) { getAppsForStrategy(strategyName) {
return this.db return this.db
.select(COLUMNS) .select(COLUMNS)
.from(TABLE) .from(TABLE)
.map(mapRow) .map(mapRow)
.then(apps => apps .then(apps =>
.filter(app => app.strategies.includes(strategyName))); apps.filter(app => app.strategies.includes(strategyName))
);
} }
getApplications (filter) { getApplications(filter) {
return filter && filter.strategyName ? return filter && filter.strategyName
this.getAppsForStrategy(filter.strategyName) : this.getAll(); ? this.getAppsForStrategy(filter.strategyName)
: this.getAll();
} }
}; }
module.exports = ClientApplicationsDb; module.exports = ClientApplicationsDb;

View File

@ -2,10 +2,16 @@
'use strict'; 'use strict';
const logger = require('../logger'); const logger = require('../logger');
const COLUMNS = ['app_name', 'instance_id', 'client_ip', 'last_seen', 'created_at']; const COLUMNS = [
'app_name',
'instance_id',
'client_ip',
'last_seen',
'created_at',
];
const TABLE = 'client_instances'; const TABLE = 'client_instances';
const mapRow = (row) => ({ const mapRow = row => ({
appName: row.app_name, appName: row.app_name,
instanceId: row.instance_id, instanceId: row.instance_id,
clientIp: row.client_ip, clientIp: row.client_ip,
@ -19,21 +25,23 @@ const mapRow = (row) => ({
// }); // });
class ClientInstanceStore { class ClientInstanceStore {
constructor(db) {
constructor (db) {
this.db = db; this.db = db;
setTimeout(() => this._removeInstancesOlderThanTwoDays(), 10).unref(); setTimeout(() => this._removeInstancesOlderThanTwoDays(), 10).unref();
setInterval(() => this._removeInstancesOlderThanTwoDays(), 24 * 61 * 60 * 1000).unref(); setInterval(
() => this._removeInstancesOlderThanTwoDays(),
24 * 61 * 60 * 1000
).unref();
} }
_removeInstancesOlderThanTwoDays () { _removeInstancesOlderThanTwoDays() {
this.db(TABLE) this.db(TABLE)
.whereRaw('created_at < now() - interval \'2 days\'') .whereRaw("created_at < now() - interval '2 days'")
.del() .del()
.then((res) => res > 0 && logger.info(`Deleted ${res} instances`)); .then(res => res > 0 && logger.info(`Deleted ${res} instances`));
} }
updateRow (details) { updateRow(details) {
return this.db(TABLE) return this.db(TABLE)
.where('app_name', details.appName) .where('app_name', details.appName)
.where('instance_id', details.instanceId) .where('instance_id', details.instanceId)
@ -43,7 +51,7 @@ class ClientInstanceStore {
}); });
} }
insertNewRow (details) { insertNewRow(details) {
return this.db(TABLE).insert({ return this.db(TABLE).insert({
app_name: details.appName, app_name: details.appName,
instance_id: details.instanceId, instance_id: details.instanceId,
@ -51,7 +59,7 @@ class ClientInstanceStore {
}); });
} }
insert (details) { insert(details) {
return this.db(TABLE) return this.db(TABLE)
.count('*') .count('*')
.where('app_name', details.appName) .where('app_name', details.appName)
@ -66,7 +74,7 @@ class ClientInstanceStore {
}); });
} }
getAll () { getAll() {
return this.db return this.db
.select(COLUMNS) .select(COLUMNS)
.from(TABLE) .from(TABLE)
@ -74,7 +82,7 @@ class ClientInstanceStore {
.map(mapRow); .map(mapRow);
} }
getByAppName (appName) { getByAppName(appName) {
return this.db return this.db
.select() .select()
.from(TABLE) .from(TABLE)
@ -83,7 +91,7 @@ class ClientInstanceStore {
.map(mapRow); .map(mapRow);
} }
getApplications () { getApplications() {
return this.db return this.db
.distinct('app_name') .distinct('app_name')
.select(['app_name']) .select(['app_name'])
@ -91,6 +99,6 @@ class ClientInstanceStore {
.orderBy('app_name', 'desc') .orderBy('app_name', 'desc')
.map(mapRow); .map(mapRow);
} }
}; }
module.exports = ClientInstanceStore; module.exports = ClientInstanceStore;

View File

@ -5,46 +5,49 @@ const logger = require('../logger');
const METRICS_COLUMNS = ['id', 'created_at', 'metrics']; const METRICS_COLUMNS = ['id', 'created_at', 'metrics'];
const TABLE = 'client_metrics'; const TABLE = 'client_metrics';
const mapRow = (row) => ({ const mapRow = row => ({
id: row.id, id: row.id,
createdAt: row.created_at, createdAt: row.created_at,
metrics: row.metrics, metrics: row.metrics,
}); });
class ClientMetricsDb { class ClientMetricsDb {
constructor (db) { constructor(db) {
this.db = db; this.db = db;
// Clear old metrics regulary // Clear old metrics regulary
setTimeout(() => this.removeMetricsOlderThanOneHour(), 10).unref(); setTimeout(() => this.removeMetricsOlderThanOneHour(), 10).unref();
setInterval(() => this.removeMetricsOlderThanOneHour(), 60 * 1000).unref(); setInterval(
() => this.removeMetricsOlderThanOneHour(),
60 * 1000
).unref();
} }
removeMetricsOlderThanOneHour () { removeMetricsOlderThanOneHour() {
this.db(TABLE) this.db(TABLE)
.whereRaw('created_at < now() - interval \'1 hour\'') .whereRaw("created_at < now() - interval '1 hour'")
.del() .del()
.then((res) => res > 0 && logger.info(`Deleted ${res} metrics`)); .then(res => res > 0 && logger.info(`Deleted ${res} metrics`));
} }
// Insert new client metrics // Insert new client metrics
insert (metrics) { insert(metrics) {
return this.db(TABLE).insert({ metrics }); return this.db(TABLE).insert({ metrics });
} }
// Used at startup to load all metrics last week into memory! // Used at startup to load all metrics last week into memory!
getMetricsLastHour () { getMetricsLastHour() {
return this.db return this.db
.select(METRICS_COLUMNS) .select(METRICS_COLUMNS)
.from(TABLE) .from(TABLE)
.limit(2000) .limit(2000)
.whereRaw('created_at > now() - interval \'1 hour\'') .whereRaw("created_at > now() - interval '1 hour'")
.orderBy('created_at', 'asc') .orderBy('created_at', 'asc')
.map(mapRow); .map(mapRow);
} }
// Used to poll for new metrics // Used to poll for new metrics
getNewMetrics (lastKnownId) { getNewMetrics(lastKnownId) {
return this.db return this.db
.select(METRICS_COLUMNS) .select(METRICS_COLUMNS)
.from(TABLE) .from(TABLE)
@ -53,6 +56,6 @@ class ClientMetricsDb {
.orderBy('created_at', 'asc') .orderBy('created_at', 'asc')
.map(mapRow); .map(mapRow);
} }
}; }
module.exports = ClientMetricsDb; module.exports = ClientMetricsDb;

View File

@ -7,31 +7,32 @@ const { EventEmitter } = require('events');
const TEN_SECONDS = 10 * 1000; const TEN_SECONDS = 10 * 1000;
class ClientMetricsStore extends EventEmitter { class ClientMetricsStore extends EventEmitter {
constructor(metricsDb, pollInterval = TEN_SECONDS) {
constructor (metricsDb, pollInterval = TEN_SECONDS) {
super(); super();
this.metricsDb = metricsDb; this.metricsDb = metricsDb;
this.highestIdSeen = 0; this.highestIdSeen = 0;
// Build internal state // Build internal state
metricsDb.getMetricsLastHour() metricsDb
.then((metrics) => this._emitMetrics(metrics)) .getMetricsLastHour()
.then(metrics => this._emitMetrics(metrics))
.then(() => this._startPoller(pollInterval)) .then(() => this._startPoller(pollInterval))
.then(() => this.emit('ready')) .then(() => this.emit('ready'))
.catch((err) => logger.error(err)); .catch(err => logger.error(err));
} }
_startPoller (pollInterval) { _startPoller(pollInterval) {
this.timer = setInterval(() => this._fetchNewAndEmit(), pollInterval); this.timer = setInterval(() => this._fetchNewAndEmit(), pollInterval);
this.timer.unref(); this.timer.unref();
} }
_fetchNewAndEmit () { _fetchNewAndEmit() {
this.metricsDb.getNewMetrics(this.highestIdSeen) this.metricsDb
.then((metrics) => this._emitMetrics(metrics)); .getNewMetrics(this.highestIdSeen)
.then(metrics => this._emitMetrics(metrics));
} }
_emitMetrics (metrics) { _emitMetrics(metrics) {
if (metrics && metrics.length > 0) { if (metrics && metrics.length > 0) {
this.highestIdSeen = metrics[metrics.length - 1].id; this.highestIdSeen = metrics[metrics.length - 1].id;
metrics.forEach(m => this.emit('metrics', m.metrics)); metrics.forEach(m => this.emit('metrics', m.metrics));
@ -39,15 +40,15 @@ class ClientMetricsStore extends EventEmitter {
} }
// Insert new client metrics // Insert new client metrics
insert (metrics) { insert(metrics) {
return this.metricsDb.insert(metrics); return this.metricsDb.insert(metrics);
} }
destroy () { destroy() {
try { try {
clearInterval(this.timer); clearInterval(this.timer);
} catch (e) {} } catch (e) {}
} }
}; }
module.exports = ClientMetricsStore; module.exports = ClientMetricsStore;

View File

@ -4,33 +4,31 @@ const { test } = require('ava');
const ClientMetricStore = require('./client-metrics-store'); const ClientMetricStore = require('./client-metrics-store');
const sinon = require('sinon'); const sinon = require('sinon');
function getMockDb () { function getMockDb() {
const list = [ const list = [
{ id: 4, metrics: { appName: 'test' } }, { id: 4, metrics: { appName: 'test' } },
{ id: 3, metrics: { appName: 'test' } }, { id: 3, metrics: { appName: 'test' } },
{ id: 2, metrics: { appName: 'test' } }, { id: 2, metrics: { appName: 'test' } },
]; ];
return { return {
getMetricsLastHour () { getMetricsLastHour() {
return Promise.resolve([{ id: 1, metrics: { appName: 'test' } }]); return Promise.resolve([{ id: 1, metrics: { appName: 'test' } }]);
}, },
getNewMetrics () { getNewMetrics() {
return Promise.resolve([list.pop() || { id: 0 }]); return Promise.resolve([list.pop() || { id: 0 }]);
}, },
}; };
} }
test.cb('should call database on startup', t => {
test.cb('should call database on startup', (t) => {
const mock = getMockDb(); const mock = getMockDb();
const store = new ClientMetricStore(mock); const store = new ClientMetricStore(mock);
t.plan(2); t.plan(2);
store.on('metrics', metrics => {
store.on('metrics', (metrics) => {
t.true(store.highestIdSeen === 1); t.true(store.highestIdSeen === 1);
t.true(metrics.appName === 'test'); t.true(metrics.appName === 'test');
store.destroy(); store.destroy();
@ -39,15 +37,14 @@ test.cb('should call database on startup', (t) => {
}); });
}); });
test.cb('should poll for updates', t => {
test.cb('should poll for updates', (t) => {
const clock = sinon.useFakeTimers(); const clock = sinon.useFakeTimers();
const mock = getMockDb(); const mock = getMockDb();
const store = new ClientMetricStore(mock, 100); const store = new ClientMetricStore(mock, 100);
const metrics = []; const metrics = [];
store.on('metrics', (m) => metrics.push(m)); store.on('metrics', m => metrics.push(m));
t.true(metrics.length === 0); t.true(metrics.length === 0);
@ -63,4 +60,3 @@ test.cb('should poll for updates', (t) => {
}); });
}); });
}); });

View File

@ -2,7 +2,12 @@
const knex = require('knex'); const knex = require('knex');
module.exports.createDb = function ({ databaseUrl, poolMin = 2, poolMax = 20, databaseSchema = 'public' }) { module.exports.createDb = function({
databaseUrl,
poolMin = 2,
poolMax = 20,
databaseSchema = 'public',
}) {
const db = knex({ const db = knex({
client: 'pg', client: 'pg',
connection: databaseUrl, connection: databaseUrl,

View File

@ -5,22 +5,22 @@ const { EventEmitter } = require('events');
const EVENT_COLUMNS = ['id', 'type', 'created_by', 'created_at', 'data']; const EVENT_COLUMNS = ['id', 'type', 'created_by', 'created_at', 'data'];
class EventStore extends EventEmitter { class EventStore extends EventEmitter {
constructor(db) {
constructor (db) {
super(); super();
this.db = db; this.db = db;
} }
store (event) { store(event) {
return this.db('events').insert({ return this.db('events')
type: event.type, .insert({
type: event.type,
created_by: event.createdBy, // eslint-disable-line created_by: event.createdBy, // eslint-disable-line
data: event.data, data: event.data,
}) })
.then(() => this.emit(event.type, event)); .then(() => this.emit(event.type, event));
} }
getEvents () { getEvents() {
return this.db return this.db
.select(EVENT_COLUMNS) .select(EVENT_COLUMNS)
.from('events') .from('events')
@ -29,17 +29,17 @@ class EventStore extends EventEmitter {
.map(this.rowToEvent); .map(this.rowToEvent);
} }
getEventsFilterByName (name) { getEventsFilterByName(name) {
return this.db return this.db
.select(EVENT_COLUMNS) .select(EVENT_COLUMNS)
.from('events') .from('events')
.limit(100) .limit(100)
.whereRaw('data ->> \'name\' = ?', [name]) .whereRaw("data ->> 'name' = ?", [name])
.orderBy('created_at', 'desc') .orderBy('created_at', 'desc')
.map(this.rowToEvent); .map(this.rowToEvent);
} }
rowToEvent (row) { rowToEvent(row) {
return { return {
id: row.id, id: row.id,
type: row.type, type: row.type,
@ -48,7 +48,6 @@ class EventStore extends EventEmitter {
data: row.data, data: row.data,
}; };
} }
}; }
module.exports = EventStore; module.exports = EventStore;

View File

@ -1,21 +1,40 @@
'use strict'; 'use strict';
const { FEATURE_CREATED, FEATURE_UPDATED, FEATURE_ARCHIVED, FEATURE_REVIVED } = require('../event-type'); const {
FEATURE_CREATED,
FEATURE_UPDATED,
FEATURE_ARCHIVED,
FEATURE_REVIVED,
} = require('../event-type');
const logger = require('../logger'); const logger = require('../logger');
const NotFoundError = require('../error/notfound-error'); const NotFoundError = require('../error/notfound-error');
const FEATURE_COLUMNS = ['name', 'description', 'enabled', 'strategies', 'created_at']; const FEATURE_COLUMNS = [
'name',
'description',
'enabled',
'strategies',
'created_at',
];
const TABLE = 'features'; const TABLE = 'features';
class FeatureToggleStore { class FeatureToggleStore {
constructor (db, eventStore) { constructor(db, eventStore) {
this.db = db; this.db = db;
eventStore.on(FEATURE_CREATED, event => this._createFeature(event.data)); eventStore.on(FEATURE_CREATED, event =>
eventStore.on(FEATURE_UPDATED, event => this._updateFeature(event.data)); this._createFeature(event.data)
eventStore.on(FEATURE_ARCHIVED, event => this._archiveFeature(event.data)); );
eventStore.on(FEATURE_REVIVED, event => this._reviveFeature(event.data)); eventStore.on(FEATURE_UPDATED, event =>
this._updateFeature(event.data)
);
eventStore.on(FEATURE_ARCHIVED, event =>
this._archiveFeature(event.data)
);
eventStore.on(FEATURE_REVIVED, event =>
this._reviveFeature(event.data)
);
} }
getFeatures () { getFeatures() {
return this.db return this.db
.select(FEATURE_COLUMNS) .select(FEATURE_COLUMNS)
.from(TABLE) .from(TABLE)
@ -24,7 +43,7 @@ class FeatureToggleStore {
.map(this.rowToFeature); .map(this.rowToFeature);
} }
getFeature (name) { getFeature(name) {
return this.db return this.db
.first(FEATURE_COLUMNS) .first(FEATURE_COLUMNS)
.from(TABLE) .from(TABLE)
@ -32,7 +51,7 @@ class FeatureToggleStore {
.then(this.rowToFeature); .then(this.rowToFeature);
} }
getArchivedFeatures () { getArchivedFeatures() {
return this.db return this.db
.select(FEATURE_COLUMNS) .select(FEATURE_COLUMNS)
.from(TABLE) .from(TABLE)
@ -41,7 +60,7 @@ class FeatureToggleStore {
.map(this.rowToFeature); .map(this.rowToFeature);
} }
rowToFeature (row) { rowToFeature(row) {
if (!row) { if (!row) {
throw new NotFoundError('No feature toggle found'); throw new NotFoundError('No feature toggle found');
} }
@ -54,7 +73,7 @@ class FeatureToggleStore {
}; };
} }
eventDataToRow (data) { eventDataToRow(data) {
return { return {
name: data.name, name: data.name,
description: data.description, description: data.description,
@ -64,32 +83,40 @@ class FeatureToggleStore {
}; };
} }
_createFeature (data) { _createFeature(data) {
return this.db(TABLE) return this.db(TABLE)
.insert(this.eventDataToRow(data)) .insert(this.eventDataToRow(data))
.catch(err => logger.error('Could not insert feature, error was: ', err)); .catch(err =>
logger.error('Could not insert feature, error was: ', err)
);
} }
_updateFeature (data) { _updateFeature(data) {
return this.db(TABLE) return this.db(TABLE)
.where({ name: data.name }) .where({ name: data.name })
.update(this.eventDataToRow(data)) .update(this.eventDataToRow(data))
.catch(err => logger.error('Could not update feature, error was: ', err)); .catch(err =>
logger.error('Could not update feature, error was: ', err)
);
} }
_archiveFeature ({ name }) { _archiveFeature({ name }) {
return this.db(TABLE) return this.db(TABLE)
.where({ name }) .where({ name })
.update({ archived: 1 }) .update({ archived: 1 })
.catch(err => logger.error('Could not archive feature, error was: ', err)); .catch(err =>
logger.error('Could not archive feature, error was: ', err)
);
} }
_reviveFeature ({ name }) { _reviveFeature({ name }) {
return this.db(TABLE) return this.db(TABLE)
.where({ name }) .where({ name })
.update({ archived: 0, enabled: 0 }) .update({ archived: 0, enabled: 0 })
.catch(err => logger.error('Could not archive feature, error was: ', err)); .catch(err =>
logger.error('Could not archive feature, error was: ', err)
);
} }
}; }
module.exports = FeatureToggleStore; module.exports = FeatureToggleStore;

View File

@ -9,7 +9,7 @@ const ClientMetricsDb = require('./client-metrics-db');
const ClientMetricsStore = require('./client-metrics-store'); const ClientMetricsStore = require('./client-metrics-store');
const ClientApplicationsStore = require('./client-applications-store'); const ClientApplicationsStore = require('./client-applications-store');
module.exports.createStores = (config) => { module.exports.createStores = config => {
const db = createDb(config); const db = createDb(config);
const eventStore = new EventStore(db); const eventStore = new EventStore(db);
const clientMetricsDb = new ClientMetricsDb(db); const clientMetricsDb = new ClientMetricsDb(db);

View File

@ -1,27 +1,32 @@
'use strict'; 'use strict';
const { STRATEGY_CREATED, STRATEGY_DELETED, STRATEGY_UPDATED } = require('../event-type'); const {
STRATEGY_CREATED,
STRATEGY_DELETED,
STRATEGY_UPDATED,
} = require('../event-type');
const logger = require('../logger'); const logger = require('../logger');
const NotFoundError = require('../error/notfound-error'); const NotFoundError = require('../error/notfound-error');
const STRATEGY_COLUMNS = ['name', 'description', 'parameters']; const STRATEGY_COLUMNS = ['name', 'description', 'parameters'];
const TABLE = 'strategies'; const TABLE = 'strategies';
class StrategyStore { class StrategyStore {
constructor (db, eventStore) { constructor(db, eventStore) {
this.db = db; this.db = db;
eventStore.on(STRATEGY_CREATED, event => this._createStrategy(event.data)); eventStore.on(STRATEGY_CREATED, event =>
eventStore.on(STRATEGY_UPDATED, event => this._updateStrategy(event.data)); this._createStrategy(event.data)
);
eventStore.on(STRATEGY_UPDATED, event =>
this._updateStrategy(event.data)
);
eventStore.on(STRATEGY_DELETED, event => { eventStore.on(STRATEGY_DELETED, event => {
db(TABLE) db(TABLE).where('name', event.data.name).del().catch(err => {
.where('name', event.data.name) logger.error('Could not delete strategy, error was: ', err);
.del() });
.catch(err => {
logger.error('Could not delete strategy, error was: ', err);
});
}); });
} }
getStrategies () { getStrategies() {
return this.db return this.db
.select(STRATEGY_COLUMNS) .select(STRATEGY_COLUMNS)
.from(TABLE) .from(TABLE)
@ -29,7 +34,7 @@ class StrategyStore {
.map(this.rowToStrategy); .map(this.rowToStrategy);
} }
getStrategy (name) { getStrategy(name) {
return this.db return this.db
.first(STRATEGY_COLUMNS) .first(STRATEGY_COLUMNS)
.from(TABLE) .from(TABLE)
@ -37,7 +42,7 @@ class StrategyStore {
.then(this.rowToStrategy); .then(this.rowToStrategy);
} }
rowToStrategy (row) { rowToStrategy(row) {
if (!row) { if (!row) {
throw new NotFoundError('No strategy found'); throw new NotFoundError('No strategy found');
} }
@ -49,7 +54,7 @@ class StrategyStore {
}; };
} }
eventDataToRow (data) { eventDataToRow(data) {
return { return {
name: data.name, name: data.name,
description: data.description, description: data.description,
@ -57,19 +62,22 @@ class StrategyStore {
}; };
} }
_createStrategy (data) { _createStrategy(data) {
this.db(TABLE) this.db(TABLE)
.insert(this.eventDataToRow(data)) .insert(this.eventDataToRow(data))
.catch(err => logger.error('Could not insert strategy, error was: ', err)); .catch(err =>
logger.error('Could not insert strategy, error was: ', err)
);
} }
_updateStrategy (data) { _updateStrategy(data) {
this.db(TABLE) this.db(TABLE)
.where({ name: data.name }) .where({ name: data.name })
.update(this.eventDataToRow(data)) .update(this.eventDataToRow(data))
.catch(err => logger.error('Could not update strategy, error was: ', err)); .catch(err =>
logger.error('Could not update strategy, error was: ', err)
);
} }
}; }
module.exports = StrategyStore; module.exports = StrategyStore;

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
class NameExistsError extends Error { class NameExistsError extends Error {
constructor (message) { constructor(message) {
super(); super();
Error.captureStackTrace(this, this.constructor); Error.captureStackTrace(this, this.constructor);

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
class NotFoundError extends Error { class NotFoundError extends Error {
constructor (message) { constructor(message) {
super(); super();
Error.captureStackTrace(this, this.constructor); Error.captureStackTrace(this, this.constructor);

View File

@ -2,7 +2,7 @@
const ValidationError = require('./validation-error'); const ValidationError = require('./validation-error');
function validateRequest (req) { function validateRequest(req) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (req.validationErrors()) { if (req.validationErrors()) {
reject(new ValidationError('Invalid syntax')); reject(new ValidationError('Invalid syntax'));

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
class ValidationError extends Error { class ValidationError extends Error {
constructor (message) { constructor(message) {
super(); super();
Error.captureStackTrace(this, this.constructor); Error.captureStackTrace(this, this.constructor);

View File

@ -11,11 +11,7 @@ const {
} = require('./event-type'); } = require('./event-type');
const diff = require('deep-diff').diff; const diff = require('deep-diff').diff;
const strategyTypes = [ const strategyTypes = [STRATEGY_CREATED, STRATEGY_DELETED, STRATEGY_UPDATED];
STRATEGY_CREATED,
STRATEGY_DELETED,
STRATEGY_UPDATED,
];
const featureTypes = [ const featureTypes = [
FEATURE_CREATED, FEATURE_CREATED,
@ -24,7 +20,7 @@ const featureTypes = [
FEATURE_REVIVED, FEATURE_REVIVED,
]; ];
function baseTypeFor (event) { function baseTypeFor(event) {
if (featureTypes.indexOf(event.type) !== -1) { if (featureTypes.indexOf(event.type) !== -1) {
return 'features'; return 'features';
} else if (strategyTypes.indexOf(event.type) !== -1) { } else if (strategyTypes.indexOf(event.type) !== -1) {
@ -33,14 +29,15 @@ function baseTypeFor (event) {
throw new Error(`unknown event type: ${JSON.stringify(event)}`); throw new Error(`unknown event type: ${JSON.stringify(event)}`);
} }
function groupByBaseTypeAndName (events) { function groupByBaseTypeAndName(events) {
const groups = {}; const groups = {};
events.forEach(event => { events.forEach(event => {
const baseType = baseTypeFor(event); const baseType = baseTypeFor(event);
groups[baseType] = groups[baseType] || {}; groups[baseType] = groups[baseType] || {};
groups[baseType][event.data.name] = groups[baseType][event.data.name] || []; groups[baseType][event.data.name] =
groups[baseType][event.data.name] || [];
groups[baseType][event.data.name].push(event); groups[baseType][event.data.name].push(event);
}); });
@ -48,7 +45,7 @@ function groupByBaseTypeAndName (events) {
return groups; return groups;
} }
function eachConsecutiveEvent (events, callback) { function eachConsecutiveEvent(events, callback) {
const groups = groupByBaseTypeAndName(events); const groups = groupByBaseTypeAndName(events);
Object.keys(groups).forEach(baseType => { Object.keys(groups).forEach(baseType => {
@ -70,7 +67,7 @@ function eachConsecutiveEvent (events, callback) {
}); });
} }
function addDiffs (events) { function addDiffs(events) {
eachConsecutiveEvent(events, (left, right) => { eachConsecutiveEvent(events, (left, right) => {
if (right) { if (right) {
left.diffs = diff(right.data, left.data); left.diffs = diff(right.data, left.data);
@ -81,7 +78,6 @@ function addDiffs (events) {
}); });
} }
module.exports = { module.exports = {
addDiffs, addDiffs,
}; };

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const test = require('ava'); const { test } = require('ava');
const eventDiffer = require('./event-differ'); const eventDiffer = require('./event-differ');
const { FEATURE_CREATED, FEATURE_UPDATED } = require('./event-type'); const { FEATURE_CREATED, FEATURE_UPDATED } = require('./event-type');
const logger = require('./logger'); const logger = require('./logger');
@ -27,11 +27,23 @@ test('diffs a feature-update event', t => {
const events = [ const events = [
{ {
type: FEATURE_UPDATED, type: FEATURE_UPDATED,
data: { name: feature, description: desc, strategy: 'default', enabled: true, parameters: { value: 2 } }, data: {
name: feature,
description: desc,
strategy: 'default',
enabled: true,
parameters: { value: 2 },
},
}, },
{ {
type: FEATURE_CREATED, type: FEATURE_CREATED,
data: { name: feature, description: desc, strategy: 'default', enabled: false, parameters: { value: 1 } }, data: {
name: feature,
description: desc,
strategy: 'default',
enabled: false,
parameters: { value: 1 },
},
}, },
]; ];
@ -57,19 +69,43 @@ test('diffs only against features with the same name', t => {
const events = [ const events = [
{ {
type: FEATURE_UPDATED, type: FEATURE_UPDATED,
data: { name: 'bar', description: 'desc', strategy: 'default', enabled: true, parameters: {} }, data: {
name: 'bar',
description: 'desc',
strategy: 'default',
enabled: true,
parameters: {},
},
}, },
{ {
type: FEATURE_UPDATED, type: FEATURE_UPDATED,
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: false, parameters: {} }, data: {
name: 'foo',
description: 'desc',
strategy: 'default',
enabled: false,
parameters: {},
},
}, },
{ {
type: FEATURE_CREATED, type: FEATURE_CREATED,
data: { name: 'bar', description: 'desc', strategy: 'default', enabled: false, parameters: {} }, data: {
name: 'bar',
description: 'desc',
strategy: 'default',
enabled: false,
parameters: {},
},
}, },
{ {
type: FEATURE_CREATED, type: FEATURE_CREATED,
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {} }, data: {
name: 'foo',
description: 'desc',
strategy: 'default',
enabled: true,
parameters: {},
},
}, },
]; ];
@ -85,11 +121,23 @@ test('sets an empty array of diffs if nothing was changed', t => {
const events = [ const events = [
{ {
type: FEATURE_UPDATED, type: FEATURE_UPDATED,
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {} }, data: {
name: 'foo',
description: 'desc',
strategy: 'default',
enabled: true,
parameters: {},
},
}, },
{ {
type: FEATURE_CREATED, type: FEATURE_CREATED,
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {} }, data: {
name: 'foo',
description: 'desc',
strategy: 'default',
enabled: true,
parameters: {},
},
}, },
]; ];
@ -101,11 +149,16 @@ test('sets diffs to null if there was nothing to diff against', t => {
const events = [ const events = [
{ {
type: FEATURE_UPDATED, type: FEATURE_UPDATED,
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {} }, data: {
name: 'foo',
description: 'desc',
strategy: 'default',
enabled: true,
parameters: {},
},
}, },
]; ];
eventDiffer.addDiffs(events); eventDiffer.addDiffs(events);
t.true(events[0].diffs === null); t.true(events[0].diffs === null);
}); });

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
function extractUsername (req) { function extractUsername(req) {
return req.cookies.username || 'unknown'; return req.cookies.username || 'unknown';
} }
module.exports = extractUsername; module.exports = extractUsername;

View File

@ -2,9 +2,7 @@
const log4js = require('log4js'); const log4js = require('log4js');
log4js.configure({ log4js.configure({
appenders: [ appenders: [{ type: 'console' }],
{ type: 'console' },
],
}); });
const logger = log4js.getLogger('unleash'); const logger = log4js.getLogger('unleash');

View File

@ -9,9 +9,14 @@ exports.startMonitoring = (enable, eventBus) => {
const client = require('prom-client'); const client = require('prom-client');
const requestDuration = new client.Summary('http_request_duration_milliseconds', 'App response time', ['path', 'method', 'status'], { const requestDuration = new client.Summary(
percentiles: [0.1, 0.5, 0.9, 0.99], 'http_request_duration_milliseconds',
}); 'App response time',
['path', 'method', 'status'],
{
percentiles: [0.1, 0.5, 0.9, 0.99],
}
);
eventBus.on(events.REQUEST_TIME, ({ path, method, time, statusCode }) => { eventBus.on(events.REQUEST_TIME, ({ path, method, time, statusCode }) => {
requestDuration.labels(path, method, statusCode).observe(time); requestDuration.labels(path, method, statusCode).observe(time);

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const test = require('ava'); const { test } = require('ava');
const { EventEmitter } = require('events'); const { EventEmitter } = require('events');
const eventBus = new EventEmitter(); const eventBus = new EventEmitter();
const { REQUEST_TIME } = require('./events'); const { REQUEST_TIME } = require('./events');
@ -9,8 +9,16 @@ const prometheusRegister = require('prom-client/lib/register');
test('should collect metrics for requests', t => { test('should collect metrics for requests', t => {
startMonitoring(true, eventBus); startMonitoring(true, eventBus);
eventBus.emit(REQUEST_TIME, { path: 'somePath', method: 'GET', statusCode: 200, time: 1337 }); eventBus.emit(REQUEST_TIME, {
path: 'somePath',
method: 'GET',
statusCode: 200,
time: 1337,
});
const metrics = prometheusRegister.metrics(); const metrics = prometheusRegister.metrics();
t.regex(metrics, /http_request_duration_milliseconds{quantile="0.99",status="200",method="GET",path="somePath"} 1337/); t.regex(
metrics,
/http_request_duration_milliseconds{quantile="0.99",status="200",method="GET",path="somePath"} 1337/
);
}); });

View File

@ -14,16 +14,19 @@ const DEFAULT_OPTIONS = {
}; };
module.exports = { module.exports = {
createOptions: (opts) => { createOptions: opts => {
const options = Object.assign({}, DEFAULT_OPTIONS, opts); const options = Object.assign({}, DEFAULT_OPTIONS, opts);
// If we are running in development we should assume local db // If we are running in development we should assume local db
if (isDev() && !options.databaseUrl) { if (isDev() && !options.databaseUrl) {
options.databaseUrl = 'postgres://unleash_user:passord@localhost:5432/unleash'; options.databaseUrl =
'postgres://unleash_user:passord@localhost:5432/unleash';
} }
if (!options.databaseUrl) { if (!options.databaseUrl) {
throw new Error('You must either pass databaseUrl option or set environemnt variable DATABASE_URL'); throw new Error(
'You must either pass databaseUrl option or set environemnt variable DATABASE_URL'
);
} }
return options; return options;
}, },

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const test = require('ava'); const { test } = require('ava');
delete process.env.DATABASE_URL; delete process.env.DATABASE_URL;
@ -18,7 +18,10 @@ test('should set default databaseUrl for develpment', t => {
const options = createOptions({}); const options = createOptions({});
t.true(options.databaseUrl === 'postgres://unleash_user:passord@localhost:5432/unleash'); t.true(
options.databaseUrl ===
'postgres://unleash_user:passord@localhost:5432/unleash'
);
}); });
test('should not override provided options', t => { test('should not override provided options', t => {

View File

@ -10,15 +10,18 @@ const { startMonitoring } = require('./metrics');
const { createStores } = require('./db'); const { createStores } = require('./db');
const { createOptions } = require('./options'); const { createOptions } = require('./options');
function createApp (options) { 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();
const config = Object.assign({ const config = Object.assign(
stores, {
eventBus, stores,
}, options); eventBus,
},
options
);
const app = getApp(config); const app = getApp(config);
const server = app.listen(app.get('port'), () => { const server = app.listen(app.get('port'), () => {
@ -30,7 +33,7 @@ function createApp (options) {
return { app, server, eventBus }; return { app, server, eventBus };
} }
function start (opts) { function start(opts) {
const options = createOptions(opts); const options = createOptions(opts);
return migrator({ databaseUrl: options.databaseUrl }) return migrator({ databaseUrl: options.databaseUrl })

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const test = require('ava'); const { test } = require('ava');
const proxyquire = require('proxyquire'); const proxyquire = require('proxyquire');
const getApp = proxyquire('./app', { const getApp = proxyquire('./app', {
@ -13,21 +13,21 @@ const getApp = proxyquire('./app', {
const serverImpl = proxyquire('./server-impl', { const serverImpl = proxyquire('./server-impl', {
'./app': getApp, './app': getApp,
'./metrics': { './metrics': {
startMonitoring (o) { startMonitoring(o) {
return o; return o;
}, },
}, },
'./db': { './db': {
createStores (o) { createStores(o) {
return o; return o;
}, },
}, },
'./options': { './options': {
createOptions (o) { createOptions(o) {
return o; return o;
}, },
}, },
'../migrator' () { '../migrator'() {
return Promise.resolve(); return Promise.resolve();
}, },
}); });