From 2d8fa7ee6b1d64dfb3b44cf9a783676efad0e10b Mon Sep 17 00:00:00 2001 From: Ivar Date: Sun, 1 May 2016 22:53:09 +0200 Subject: [PATCH] Statefull modules should be injected from top --- app.js | 3 +- lib/databaseConfig.js | 29 +++++ lib/db/dbPool.js | 51 +-------- lib/db/event.js | 64 ++++++----- lib/db/feature.js | 208 +++++++++++++++++----------------- lib/db/strategy.js | 110 +++++++++--------- lib/eventStore.js | 14 +-- lib/routes/event.js | 5 +- lib/routes/feature-archive.js | 7 +- lib/routes/feature.js | 7 +- lib/routes/health-check.js | 5 +- lib/routes/index.js | 12 +- lib/routes/strategy.js | 7 +- server.js | 17 ++- test/databaseConfig.js | 11 ++ test/featureApiSpec.js | 6 + test/specHelper.js | 20 +++- 17 files changed, 305 insertions(+), 271 deletions(-) create mode 100644 lib/databaseConfig.js create mode 100644 test/databaseConfig.js diff --git a/app.js b/app.js index d6cb006f80..8f643b3b34 100644 --- a/app.js +++ b/app.js @@ -31,7 +31,8 @@ module.exports = function(config) { app.use(cookieParser()); - routes.create(router); + // Setup API routes + routes.create(router, config); app.use(baseUriPath, router); diff --git a/lib/databaseConfig.js b/lib/databaseConfig.js new file mode 100644 index 0000000000..70cb78e3c7 --- /dev/null +++ b/lib/databaseConfig.js @@ -0,0 +1,29 @@ +var nconf = require('nconf'); +var fs = require('fs'); +var ini = require('ini'); +var logger = require('./logger'); + +function getDatabaseIniUrl() { + // Finn specific way of delivering env variables + var databaseini = nconf.argv().get('databaseini'); + var config = ini.parse(fs.readFileSync(databaseini, 'utf-8')); + + logger.info('unleash started with databaseini: ' + databaseini); + + return config.DATABASE_URL; +} + +function getDatabaseUrl() { + if (process.env.DATABASE_URL) { + logger.info('unleash started with DATABASE_URL'); + return process.env.DATABASE_URL; + } else if (nconf.argv().get('databaseini') !== undefined) { + return getDatabaseIniUrl(); + } + + throw new Error('please set DATABASE_URL or pass --databaseini'); +} + +module.exports = { + getDatabaseUrl: getDatabaseUrl +}; diff --git a/lib/db/dbPool.js b/lib/db/dbPool.js index 640238a928..33a863e674 100644 --- a/lib/db/dbPool.js +++ b/lib/db/dbPool.js @@ -1,52 +1,9 @@ -var logger = require('../logger'); -var nconf = require('nconf'); -var fs = require('fs'); -var ini = require('ini'); var knex = require('knex'); -function isTestEnv() { - return process.env.NODE_ENV === 'test'; -} - -function getDatabaseIniUrl() { - // Finn specific way of delivering env variables - var databaseini = nconf.argv().get('databaseini'); - var config = ini.parse(fs.readFileSync(databaseini, 'utf-8')); - - logger.info('unleash started with databaseini: ' + databaseini); - - return config.DATABASE_URL; -} - -function getTestDatabaseUrl() { - if (process.env.TEST_DATABASE_URL) { - logger.info('unleash started with TEST_DATABASE_URL'); - return process.env.TEST_DATABASE_URL; - } else { - throw new Error('please set TEST_DATABASE_URL'); - } -} - -function getDatabaseUrl() { - if (process.env.DATABASE_URL) { - logger.info('unleash started with DATABASE_URL'); - return process.env.DATABASE_URL; - } else if (nconf.argv().get('databaseini') !== undefined) { - return getDatabaseIniUrl(); - } - - throw new Error('please set DATABASE_URL or pass --databaseini'); -} - -function createDbPool() { +module.exports = function(databaseConnection) { return knex({ client: 'pg', - connection: isTestEnv() ? getTestDatabaseUrl() : getDatabaseUrl(), - pool: { - min: 2, - max: 20 - } + connection: databaseConnection, + pool: { min: 2, max: 20 } }); -} - -module.exports = createDbPool(); +}; diff --git a/lib/db/event.js b/lib/db/event.js index 5dea161375..8d4d1f24f5 100644 --- a/lib/db/event.js +++ b/lib/db/event.js @@ -1,43 +1,45 @@ -var knex = require('./dbPool'); var EVENT_COLUMNS = ['id', 'type', 'created_by', 'created_at', 'data']; -function storeEvent(event) { - return knex('events').insert({ - type: event.type, - created_by: event.createdBy, // eslint-disable-line - data: event.data - }); -} +module.exports = function(db) { + function storeEvent(event) { + return db('events').insert({ + type: event.type, + created_by: event.createdBy, // eslint-disable-line + data: event.data + }); + } -function getEvents() { - return knex + function getEvents() { + return db + .select(EVENT_COLUMNS) + .from('events') + .orderBy('created_at', 'desc') + .map(rowToEvent); + } + + function getEventsFilterByName(name) { + return db .select(EVENT_COLUMNS) .from('events') + .whereRaw("data ->> 'name' = ?", [name]) .orderBy('created_at', 'desc') .map(rowToEvent); -} + } -function getEventsFilterByName(name) { - return knex - .select(EVENT_COLUMNS) - .from('events') - .whereRaw("data ->> 'name' = ?", [name]) - .orderBy('created_at', 'desc') - .map(rowToEvent); -} + function rowToEvent(row) { + return { + id: row.id, + type: row.type, + createdBy: row.created_by, + createdAt: row.created_at, + data: row.data + }; + } -function rowToEvent(row) { return { - id: row.id, - type: row.type, - createdBy: row.created_by, - createdAt: row.created_at, - data: row.data + store: storeEvent, + getEvents: getEvents, + getEventsFilterByName: getEventsFilterByName }; -} - -module.exports = { - store: storeEvent, - getEvents: getEvents, - getEventsFilterByName: getEventsFilterByName }; + diff --git a/lib/db/feature.js b/lib/db/feature.js index 9a7d2dad14..fe54a553c9 100644 --- a/lib/db/feature.js +++ b/lib/db/feature.js @@ -1,118 +1,118 @@ -var eventStore = require('../eventStore'); var eventType = require('../eventType'); var logger = require('../logger'); -var knex = require('./dbPool'); var NotFoundError = require('../error/NotFoundError'); var FEATURE_COLUMNS = ['name', 'description', 'enabled', 'strategy_name', 'parameters']; -eventStore.on(eventType.featureCreated, function (event) { - return createFeature(event.data); -}); +module.exports = function(db, eventStore) { + eventStore.on(eventType.featureCreated, function (event) { + return createFeature(event.data); + }); -eventStore.on(eventType.featureUpdated, function (event) { - return updateFeature(event.data); -}); + eventStore.on(eventType.featureUpdated, function (event) { + return updateFeature(event.data); + }); -eventStore.on(eventType.featureArchived, function (event) { - return archiveFeature(event.data); -}); + eventStore.on(eventType.featureArchived, function (event) { + return archiveFeature(event.data); + }); -eventStore.on(eventType.featureRevived, function (event) { - return reviveFeature(event.data); -}); + eventStore.on(eventType.featureRevived, function (event) { + return reviveFeature(event.data); + }); -function getFeatures() { - return knex - .select(FEATURE_COLUMNS) - .from('features') - .where({ archived: 0 }) - .orderBy('name', 'asc') - .map(rowToFeature); -} - -function getFeature(name) { - return knex - .first(FEATURE_COLUMNS) - .from('features') - .where({ name: name }) - .then(rowToFeature); -} - -function getArchivedFeatures() { - return knex - .select(FEATURE_COLUMNS) - .from('features') - .where({ archived: 1 }) - .orderBy('name', 'asc') - .map(rowToFeature); -} - - -function rowToFeature(row) { - if (!row) { - throw new NotFoundError('No feature toggle found'); + function getFeatures() { + return db + .select(FEATURE_COLUMNS) + .from('features') + .where({ archived: 0 }) + .orderBy('name', 'asc') + .map(rowToFeature); } + function getFeature(name) { + return db + .first(FEATURE_COLUMNS) + .from('features') + .where({ name: name }) + .then(rowToFeature); + } + + function getArchivedFeatures() { + return db + .select(FEATURE_COLUMNS) + .from('features') + .where({ archived: 1 }) + .orderBy('name', 'asc') + .map(rowToFeature); + } + + + function rowToFeature(row) { + if (!row) { + throw new NotFoundError('No feature toggle found'); + } + + return { + name: row.name, + description: row.description, + enabled: row.enabled > 0, + strategy: row.strategy_name, // eslint-disable-line + parameters: row.parameters + }; + } + + function eventDataToRow(data) { + return { + name: data.name, + description: data.description, + enabled: data.enabled ? 1 : 0, + archived: data.archived ? 1 :0, + strategy_name: data.strategy, // eslint-disable-line + parameters: data.parameters + }; + } + + function createFeature(data) { + return db('features') + .insert(eventDataToRow(data)) + .catch(function (err) { + logger.error('Could not insert feature, error was: ', err); + }); + } + + function updateFeature(data) { + return db('features') + .where({ name: data.name }) + .update(eventDataToRow(data)) + .catch(function (err) { + logger.error('Could not update feature, error was: ', err); + }); + } + + function archiveFeature(data) { + return db('features') + .where({ name: data.name }) + .update({ archived: 1 }) + .catch(function (err) { + logger.error('Could not archive feature, error was: ', err); + }); + } + + function reviveFeature(data) { + return db('features') + .where({ name: data.name }) + .update({ archived: 0, enabled: 0 }) + .catch(function (err) { + logger.error('Could not archive feature, error was: ', err); + }); + } + + return { - name: row.name, - description: row.description, - enabled: row.enabled > 0, - strategy: row.strategy_name, // eslint-disable-line - parameters: row.parameters + getFeatures: getFeatures, + getFeature: getFeature, + getArchivedFeatures: getArchivedFeatures, + _createFeature: createFeature, // visible for testing + _updateFeature: updateFeature // visible for testing }; -} - -function eventDataToRow(data) { - return { - name: data.name, - description: data.description || '', - enabled: data.enabled ? 1 : 0, - archived: data.archived ? 1 :0, - strategy_name: data.strategy || 'default', // eslint-disable-line - parameters: data.parameters || {} - }; -} - -function createFeature(data) { - return knex('features') - .insert(eventDataToRow(data)) - .catch(function (err) { - logger.error('Could not insert feature, error was: ', err); - }); -} - -function updateFeature(data) { - return knex('features') - .where({ name: data.name }) - .update(eventDataToRow(data)) - .catch(function (err) { - logger.error('Could not update feature, error was: ', err); - }); -} - -function archiveFeature(data) { - return knex('features') - .where({ name: data.name }) - .update({ archived: 1 }) - .catch(function (err) { - logger.error('Could not archive feature, error was: ', err); - }); -} - -function reviveFeature(data) { - return knex('features') - .where({ name: data.name }) - .update({ archived: 0, enabled: 0 }) - .catch(function (err) { - logger.error('Could not archive feature, error was: ', err); - }); -} - - -module.exports = { - getFeatures: getFeatures, - getFeature: getFeature, - getArchivedFeatures: getArchivedFeatures, - _createFeature: createFeature, // visible for testing - _updateFeature: updateFeature // visible for testing }; diff --git a/lib/db/strategy.js b/lib/db/strategy.js index faebcfb36b..9a19a9c503 100644 --- a/lib/db/strategy.js +++ b/lib/db/strategy.js @@ -1,70 +1,70 @@ -var eventStore = require('../eventStore'); var eventType = require('../eventType'); var logger = require('../logger'); -var knex = require('./dbPool'); var NotFoundError = require('../error/NotFoundError'); var STRATEGY_COLUMNS = ['name', 'description', 'parameters_template']; -eventStore.on(eventType.strategyCreated, function (event) { - return createStrategy(event.data); -}); +module.exports = function(db, eventStore) { + eventStore.on(eventType.strategyCreated, function (event) { + return createStrategy(event.data); + }); -eventStore.on(eventType.strategyDeleted, function (event) { - knex('strategies') - .where('name', event.data.name) - .del() - .catch(function (err) { - logger.error('Could not delete strategy, error was: ', err); - }); -}); + eventStore.on(eventType.strategyDeleted, function (event) { + db('strategies') + .where('name', event.data.name) + .del() + .catch(function (err) { + logger.error('Could not delete strategy, error was: ', err); + }); + }); -function getStrategies() { - return knex - .select(STRATEGY_COLUMNS) - .from('strategies') - .orderBy('created_at', 'asc') - .map(rowToStrategy); -} + function getStrategies() { + return db + .select(STRATEGY_COLUMNS) + .from('strategies') + .orderBy('created_at', 'asc') + .map(rowToStrategy); + } -function getStrategy(name) { - return knex - .first(STRATEGY_COLUMNS) - .from('strategies') - .where({ name: name }) - .then(rowToStrategy); -} + function getStrategy(name) { + return db + .first(STRATEGY_COLUMNS) + .from('strategies') + .where({ name: name }) + .then(rowToStrategy); + } -function rowToStrategy(row) { - if (!row) { - throw new NotFoundError('No strategy found'); + function rowToStrategy(row) { + if (!row) { + throw new NotFoundError('No strategy found'); + } + + return { + name: row.name, + description: row.description, + parametersTemplate: row.parameters_template + }; + } + + function eventDataToRow(data) { + return { + name: data.name, + description: data.description, + parameters_template: data.parametersTemplate // eslint-disable-line + }; + } + + function createStrategy(data) { + db('strategies') + .insert(eventDataToRow(data)) + .catch(function (err) { + logger.error('Could not insert strategy, error was: ', err); + }); } return { - name: row.name, - description: row.description, - parametersTemplate: row.parameters_template + getStrategies: getStrategies, + getStrategy: getStrategy, + _createStrategy: createStrategy // visible for testing }; -} - -function eventDataToRow(data) { - return { - name: data.name, - description: data.description, - parameters_template: data.parametersTemplate || {} // eslint-disable-line - }; -} - -function createStrategy(data) { - knex('strategies') - .insert(eventDataToRow(data)) - .catch(function (err) { - logger.error('Could not insert strategy, error was: ', err); - }); -} - -module.exports = { - getStrategies: getStrategies, - getStrategy: getStrategy, - _createStrategy: createStrategy // visible for testing }; diff --git a/lib/eventStore.js b/lib/eventStore.js index 8e71fcd61b..1f39dbdf2c 100644 --- a/lib/eventStore.js +++ b/lib/eventStore.js @@ -1,17 +1,17 @@ -var util = require('util'), - eventDb = require('./db/event'), - EventEmitter = require('events').EventEmitter; +var util = require('util'); +var EventEmitter = require('events').EventEmitter; -function EventStore() { +function EventStore(eventDb) { + this.eventDb = eventDb; EventEmitter.call(this); } util.inherits(EventStore, EventEmitter); EventStore.prototype.create = function (event) { var that = this; - return eventDb.store(event).then(function() { - return that.emit(event.type, event); + return this.eventDb.store(event).then(function() { + that.emit(event.type, event); }); }; -module.exports = new EventStore(); +module.exports = EventStore; diff --git a/lib/routes/event.js b/lib/routes/event.js index 6461b1f58a..b69c7f7f09 100644 --- a/lib/routes/event.js +++ b/lib/routes/event.js @@ -1,7 +1,8 @@ -var eventDb = require('../db/event'); var eventDiffer = require('../eventDiffer'); -module.exports = function (app) { +module.exports = function (app, config) { + var eventDb = config.eventDb; + app.get('/events', function (req, res) { eventDb.getEvents().then(function (events) { eventDiffer.addDiffs(events); diff --git a/lib/routes/feature-archive.js b/lib/routes/feature-archive.js index d9b260349f..29fc82448a 100644 --- a/lib/routes/feature-archive.js +++ b/lib/routes/feature-archive.js @@ -1,11 +1,12 @@ var logger = require('../logger'); -var eventStore = require('../eventStore'); var eventType = require('../eventType'); -var featureDb = require('../db/feature'); var ValidationError = require('../error/ValidationError'); var validateRequest = require('../error/validateRequest'); -module.exports = function (app) { +module.exports = function (app, config) { + var featureDb = config.featureDb; + var eventStore = config.eventStore; + app.get('/archive/features', function (req, res) { featureDb.getArchivedFeatures().then(function (archivedFeatures) { res.json({ 'features': archivedFeatures }); diff --git a/lib/routes/feature.js b/lib/routes/feature.js index 51ab2a8055..1f3c726eaf 100644 --- a/lib/routes/feature.js +++ b/lib/routes/feature.js @@ -1,15 +1,16 @@ var Promise = require("bluebird"); var logger = require('../logger'); -var eventStore = require('../eventStore'); var eventType = require('../eventType'); -var featureDb = require('../db/feature'); var NameExistsError = require('../error/NameExistsError'); var NotFoundError = require('../error/NotFoundError'); var ValidationError = require('../error/ValidationError'); var validateRequest = require('../error/validateRequest'); var extractUser = require('../extractUser'); -module.exports = function (app) { +module.exports = function (app, config) { + var featureDb = config.featureDb; + var eventStore = config.eventStore; + app.get('/features', function (req, res) { featureDb.getFeatures().then(function (features) { res.json({ features: features }); diff --git a/lib/routes/health-check.js b/lib/routes/health-check.js index 76b84afe1c..1343e81686 100644 --- a/lib/routes/health-check.js +++ b/lib/routes/health-check.js @@ -1,9 +1,8 @@ -var knex = require('../db/dbPool'); var logger = require('../logger'); -module.exports = function (app) { +module.exports = function (app, config) { app.get('/health', function (req, res) { - knex.select(1) + config.db.select(1) .from('features') .then(function() { res.json({ health: 'GOOD' }); diff --git a/lib/routes/index.js b/lib/routes/index.js index 0c25412d3d..6e3515fa64 100644 --- a/lib/routes/index.js +++ b/lib/routes/index.js @@ -2,10 +2,10 @@ * TODO: we should also inject config and * services to the routes to ease testing. **/ -exports.create = function (app) { - require('./event')(app); - require('./feature')(app); - require('./feature-archive')(app); - require('./strategy')(app); - require('./health-check')(app); +exports.create = function (app, config) { + require('./event')(app, config); + require('./feature')(app, config); + require('./feature-archive')(app, config); + require('./strategy')(app, config); + require('./health-check')(app, config); }; diff --git a/lib/routes/strategy.js b/lib/routes/strategy.js index 20e635d897..4176d215c4 100644 --- a/lib/routes/strategy.js +++ b/lib/routes/strategy.js @@ -1,7 +1,5 @@ var Promise = require("bluebird"); -var eventStore = require('../eventStore'); var eventType = require('../eventType'); -var strategyDb = require('../db/strategy'); var logger = require('../logger'); var NameExistsError = require('../error/NameExistsError'); var ValidationError = require('../error/ValidationError'); @@ -9,7 +7,10 @@ var NotFoundError = require('../error/NotFoundError'); var validateRequest = require('../error/validateRequest'); var extractUser = require('../extractUser'); -module.exports = function (app) { +module.exports = function (app, config) { + var strategyDb = config.strategyDb; + var eventStore = config.eventStore; + app.get('/strategies', function (req, res) { strategyDb.getStrategies().then(function (strategies) { res.json({ strategies: strategies }); diff --git a/server.js b/server.js index 8590411f1b..e02a37d17a 100644 --- a/server.js +++ b/server.js @@ -1,8 +1,22 @@ var logger = require('./lib/logger'); +var databaseUri = require('./lib/databaseConfig').getDatabaseUrl(); + +// Database dependecies (statefull) +var db = require('./lib/db/dbPool')(databaseUri); +var eventDb = require('./lib/db/event')(db); +var EventStore = require('./lib/eventStore'); +var eventStore = new EventStore(eventDb); +var featureDb = require('./lib/db/feature')(db, eventStore); +var strategyDb = require('./lib/db/strategy')(db, eventStore); var config = { baseUriPath: process.env.BASE_URI_PATH || '', - port: process.env.HTTP_PORT || process.env.PORT || 4242 + port: process.env.HTTP_PORT || process.env.PORT || 4242, + db: db, + eventDb: eventDb, + eventStore: eventStore, + featureDb: featureDb, + strategyDb: strategyDb }; var app = require('./app')(config); @@ -25,7 +39,6 @@ if (app.get('env') === 'development') { })); } - process.on('uncaughtException', function(err) { logger.error('Uncaught Exception:', err.message); logger.error(err.stack); diff --git a/test/databaseConfig.js b/test/databaseConfig.js new file mode 100644 index 0000000000..5f80b7f2c5 --- /dev/null +++ b/test/databaseConfig.js @@ -0,0 +1,11 @@ +'use strict'; +function getDatabaseUri() { + if (!process.env.TEST_DATABASE_URL) { + throw new Error('please set TEST_DATABASE_URL'); + } else { + return process.env.TEST_DATABASE_URL; + } +} +module.exports = { + getDatabaseUri: getDatabaseUri +}; diff --git a/test/featureApiSpec.js b/test/featureApiSpec.js index 39d473429d..0ba1b70310 100644 --- a/test/featureApiSpec.js +++ b/test/featureApiSpec.js @@ -1,4 +1,5 @@ 'use strict'; +var logger = require('../lib/logger'); var assert = require('assert'); var specHelper = require('./specHelper'); var request = specHelper.request; @@ -32,6 +33,7 @@ describe('The features api', function () { }); it('cant get feature that dose not exist', function (done) { + logger.setLevel('FATAL'); request .get('/features/myfeature') .expect('Content-Type', /json/) @@ -47,6 +49,7 @@ describe('The features api', function () { }); it('creates new feature toggle with createdBy', function (done) { + logger.setLevel('FATAL'); request .post('/features') .send({ name: 'com.test.Username', enabled: false }) @@ -63,6 +66,7 @@ describe('The features api', function () { }); it('require new feature toggle to have a name', function (done) { + logger.setLevel('FATAL'); request .post('/features') .send({ name: '' }) @@ -71,6 +75,7 @@ describe('The features api', function () { }); it('can not change status of feature toggle that does not exist', function (done) { + logger.setLevel('FATAL'); request .put('/features/should-not-exist') .send({ name: 'should-not-exist', enabled: false }) @@ -79,6 +84,7 @@ describe('The features api', function () { }); it('can change status of feature toggle that does exist', function (done) { + logger.setLevel('FATAL'); request .put('/features/featureY') .send({ name: 'featureY', enabled: true }) diff --git a/test/specHelper.js b/test/specHelper.js index 02a6563481..7ad8c98790 100644 --- a/test/specHelper.js +++ b/test/specHelper.js @@ -3,10 +3,22 @@ process.env.NODE_ENV = 'test'; var Promise = require('bluebird'); var request = require('supertest'); -var app = require('../app')({ baseUriPath: '' }); -var knex = require('../lib/db/dbPool'); -var featureDb = require('../lib/db/feature'); -var strategyDb = require('../lib/db/strategy'); +var databaseUri = require('./databaseConfig').getDatabaseUri(); +var knex = require('../lib/db/dbPool')(databaseUri); +var eventDb = require('../lib/db/event')(knex); +var EventStore = require('../lib/eventStore'); +var eventStore = new EventStore(eventDb); +var featureDb = require('../lib/db/feature')(knex, eventStore); +var strategyDb = require('../lib/db/strategy')(knex, eventStore); + +var app = require('../app')({ + baseUriPath: '', + db: knex, + eventDb: eventDb, + eventStore: eventStore, + featureDb: featureDb, + strategyDb: strategyDb +}); Promise.promisifyAll(request); request = request(app);