Merge pull request #66 from strongloop/feature/middleware-mounting

Load middleware and phases from `middleware.json`
This commit is contained in:
Miroslav Bajtoš 2014-11-19 09:58:38 +01:00
commit d25c64523d
9 changed files with 393 additions and 10 deletions

View File

@ -107,6 +107,8 @@ var addInstructionsToBrowserify = require('./lib/bundler');
* `production`; however the applications are free to use any names.
* @property {Array.<String>} [modelSources] List of directories where to look
* for files containing model definitions.
* @property {Object} [middleware] Middleware configuration to use instead
* of `{appRootDir}/middleware.json`
* @property {Array.<String>} [bootDirs] List of directories where to look
* for boot scripts.
* @property {Array.<String>} [bootScripts] List of script files to execute

View File

@ -1,6 +1,7 @@
var fs = require('fs');
var path = require('path');
var commondir = require('commondir');
var cloneDeep = require('lodash.clonedeep');
/**
* Add boot instructions to a browserify bundler.
@ -60,6 +61,17 @@ function addScriptsToBundle(name, list, bundler) {
}
function bundleInstructions(instructions, bundler) {
instructions = cloneDeep(instructions);
var hasMiddleware = instructions.middleware.phases.length ||
instructions.middleware.middleware.length;
if (hasMiddleware) {
console.warn(
'Discarding middleware instructions,' +
' loopback client does not support middleware.');
}
delete instructions.middleware;
var instructionsString = JSON.stringify(instructions, null, 2);
/* The following code does not work due to a bug in browserify

View File

@ -44,6 +44,14 @@ module.exports = function compile(options) {
ConfigLoader.loadDataSources(dsRootDir, env);
assertIsValidConfig('data source', dataSourcesConfig);
// not configurable yet
var middlewareRootDir = appRootDir;
var middlewareConfig = options.middleware ||
ConfigLoader.loadMiddleware(middlewareRootDir, env);
var middlewareInstructions =
buildMiddlewareInstructions(middlewareRootDir, middlewareConfig);
// require directories
var bootDirs = options.bootDirs || []; // precedence
bootDirs = bootDirs.concat(path.join(appRootDir, 'boot'));
@ -67,6 +75,7 @@ module.exports = function compile(options) {
config: appConfig,
dataSources: dataSourcesConfig,
models: modelInstructions,
middleware: middlewareInstructions,
files: {
boot: bootScripts
}
@ -338,3 +347,46 @@ function loadModelDefinition(rootDir, jsonFile, allFiles) {
sourceFile: sourceFile
};
}
function buildMiddlewareInstructions(rootDir, config) {
var phasesNames = Object.keys(config);
var middlewareList = [];
phasesNames.forEach(function(phase) {
var phaseConfig = config[phase];
Object.keys(phaseConfig).forEach(function(middleware) {
var start = middleware.substring(0, 2);
var sourceFile = start !== './' && start !== '..' ?
middleware :
path.resolve(rootDir, middleware);
var allConfigs = phaseConfig[middleware];
if (!Array.isArray(allConfigs))
allConfigs = [allConfigs];
allConfigs.forEach(function(config) {
var middlewareConfig = cloneDeep(config);
middlewareConfig.phase = phase;
middlewareList.push({
sourceFile: require.resolve(sourceFile),
config: middlewareConfig
});
});
});
});
var flattenedPhaseNames = phasesNames
.map(function getBaseName(name) {
return name.replace(/:[^:]+$/, '');
})
.filter(function differsFromPreviousItem(value, ix, source) {
// Skip duplicate entries. That happens when
// `name:before` and `name:after` are both translated to `name`
return ix === 0 || value !== source[ix - 1];
});
return {
phases: flattenedPhaseNames,
middleware: middlewareList
};
}

View File

@ -34,6 +34,16 @@ ConfigLoader.loadModels = function(rootDir, env) {
return tryReadJsonConfig(rootDir, 'model-config') || {};
};
/**
* Load middleware config from `middleware.json` and friends.
* @param {String} rootDir Directory where to look for files.
* @param {String} env Environment, usually `process.env.NODE_ENV`
* @returns {Object}
*/
ConfigLoader.loadMiddleware = function(rootDir, env) {
return loadNamed(rootDir, env, 'middleware', mergeMiddlewareConfig);
};
/*-- Implementation --*/
/**
@ -126,6 +136,32 @@ function mergeAppConfig(target, config, fileName) {
}
}
function mergeMiddlewareConfig(target, config, fileName) {
var err;
for (var phase in config) {
if (phase in target) {
err = mergePhaseConfig(target[phase], config[phase], phase);
} else {
err = 'The phase "' + phase + '" is not defined in the main config.';
}
if (err)
throw new Error('Cannot apply ' + fileName + ': ' + err);
}
}
function mergePhaseConfig(target, config, phase) {
var err;
for (var middleware in config) {
if (middleware in target) {
err = mergeObjects(target[middleware], config[middleware]);
} else {
err = 'The middleware "' + middleware + '" in phase "' + phase + '"' +
'is not defined in the main config.';
}
if (err) return err;
}
}
function mergeObjects(target, config, keyPrefix) {
for (var key in config) {
var fullKey = keyPrefix ? keyPrefix + '.' + key : key;

View File

@ -26,6 +26,7 @@ module.exports = function execute(app, instructions, callback) {
setupDataSources(app, instructions);
setupModels(app, instructions);
setupMiddleware(app, instructions);
// Run the boot scripts in series synchronously or asynchronously
// Please note async supports both styles
@ -250,6 +251,30 @@ function tryRequire(modulePath) {
}
}
function setupMiddleware(app, instructions) {
if (!instructions.middleware) {
// the browserified client does not support middleware
return;
}
var phases = instructions.middleware.phases;
assert(Array.isArray(phases),
'instructions.middleware.phases must be an array');
var middleware = instructions.middleware.middleware;
assert(Array.isArray(middleware),
'instructions.middleware.middleware must be an object');
debug('Defining middleware phases %j', phases);
app.defineMiddlewarePhases(phases);
middleware.forEach(function(data) {
debug('Configuring middleware %j', data.sourceFile);
var factory = require(data.sourceFile);
app.middlewareFromConfig(factory, data.config);
});
}
function runBootScripts(app, instructions, callback) {
runScripts(app, instructions.files.boot, callback);
}

View File

@ -673,6 +673,186 @@ describe('compiler', function() {
expect(instructions.config).to.not.have.property('modified');
});
});
describe('for middleware', function() {
beforeEach(function() {
appdir.createConfigFilesSync();
});
it('emits middleware instructions', function() {
appdir.writeConfigFileSync('middleware.json', {
initial: {
},
custom: {
'loopback/server/middleware/url-not-found': {
params: 'some-config-data'
}
},
});
var instructions = boot.compile(appdir.PATH);
expect(instructions.middleware).to.eql({
phases: ['initial', 'custom'],
middleware: [{
sourceFile:
require.resolve('loopback/server/middleware/url-not-found'),
config: {
phase: 'custom',
params: 'some-config-data'
}
}]
});
});
it('fails when a module middleware cannot be resolved', function() {
appdir.writeConfigFileSync('middleware.json', {
final: {
'loopback/path-does-not-exist': { }
}
});
expect(function() { boot.compile(appdir.PATH); })
.to.throw(/path-does-not-exist/);
});
it('resolves paths relatively to appRootDir', function() {
appdir.writeConfigFileSync('./middleware.json', {
routes: {
// resolves to ./middleware.json
'./middleware': { }
}
});
var instructions = boot.compile(appdir.PATH);
expect(instructions.middleware).to.eql({
phases: ['routes'],
middleware: [{
sourceFile: path.resolve(appdir.PATH, 'middleware.json'),
config: { phase: 'routes' }
}]
});
});
it('merges config.params', function() {
appdir.writeConfigFileSync('./middleware.json', {
routes: {
'./middleware': {
params: {
key: 'initial value'
}
}
}
});
appdir.writeConfigFileSync('./middleware.local.json', {
routes: {
'./middleware': {
params: {
key: 'custom value',
}
}
}
});
var instructions = boot.compile(appdir.PATH);
expect(instructions.middleware.middleware[0].config.params).to.eql({
key: 'custom value'
});
});
it('merges config.enabled', function() {
appdir.writeConfigFileSync('./middleware.json', {
routes: {
'./middleware': {
params: {
key: 'initial value'
}
}
}
});
appdir.writeConfigFileSync('./middleware.local.json', {
routes: {
'./middleware': {
enabled: false
}
}
});
var instructions = boot.compile(appdir.PATH);
expect(instructions.middleware.middleware[0].config)
.to.have.property('enabled', false);
});
it('flattens sub-phases', function() {
appdir.writeConfigFileSync('middleware.json', {
'initial:after': {
},
'custom:before': {
'loopback/server/middleware/url-not-found': {
params: 'some-config-data'
}
},
'custom:after': {
}
});
var instructions = boot.compile(appdir.PATH);
expect(instructions.middleware.phases, 'phases')
.to.eql(['initial', 'custom']);
expect(instructions.middleware.middleware, 'middleware')
.to.eql([{
sourceFile:
require.resolve('loopback/server/middleware/url-not-found'),
config: {
phase: 'custom:before',
params: 'some-config-data'
}
}]);
});
it('supports multiple instances of the same middleware', function() {
appdir.writeConfigFileSync('middleware.json', {
'final': {
'./middleware': [
{
params: 'first'
},
{
params: 'second'
}
]
},
});
var instructions = boot.compile(appdir.PATH);
expect(instructions.middleware.middleware)
.to.eql([
{
sourceFile: path.resolve(appdir.PATH, 'middleware.json'),
config: {
phase: 'final',
params: 'first'
}
},
{
sourceFile: path.resolve(appdir.PATH, 'middleware.json'),
config: {
phase: 'final',
params: 'second'
}
},
]);
});
});
});
function getNameProperty(obj) {

View File

@ -6,6 +6,7 @@ var expect = require('chai').expect;
var fs = require('fs-extra');
var sandbox = require('./helpers/sandbox');
var appdir = require('./helpers/appdir');
var supertest = require('supertest');
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app');
@ -18,6 +19,13 @@ describe('executor', function() {
beforeEach(function() {
app = loopback();
// process.bootFlags is used by simple-app/boot/*.js scripts
process.bootFlags = [];
});
afterEach(function() {
delete process.bootFlags;
});
var dummyInstructions = someInstructions({
@ -178,7 +186,6 @@ describe('executor', function() {
describe('with boot and models files', function() {
beforeEach(function() {
process.bootFlags = process.bootFlags || [];
boot.execute(app, simpleAppInstructions());
});
@ -212,14 +219,6 @@ describe('executor', function() {
});
describe('with boot with callback', function() {
beforeEach(function() {
process.bootFlags = process.bootFlags || [];
});
afterEach(function() {
delete process.bootFlags;
});
it('should run `boot/*` files asynchronously', function(done) {
boot.execute(app, simpleAppInstructions(), function() {
expect(process.bootFlags).to.eql([
@ -233,7 +232,6 @@ describe('executor', function() {
done();
});
});
});
describe('with PaaS and npm env variables', function() {
@ -326,6 +324,68 @@ describe('executor', function() {
boot.execute(app, someInstructions({ files: { boot: [file] } }));
expect(app.fnCalled, 'exported fn was called').to.be.true();
});
it('configures middleware', function(done) {
var pushNamePath = require.resolve('./helpers/push-name-middleware');
boot.execute(app, someInstructions({
middleware: {
phases: ['initial', 'custom'],
middleware: [
{
sourceFile: pushNamePath,
config: {
phase: 'initial',
params: 'initial'
}
},
{
sourceFile: pushNamePath,
config: {
phase: 'custom',
params: 'custom'
}
},
{
sourceFile: pushNamePath,
config: {
phase: 'routes',
params: 'routes'
}
},
{
sourceFile: pushNamePath,
config: {
phase: 'routes',
enabled: false,
params: 'disabled'
}
}
]
}
}));
supertest(app)
.get('/')
.end(function(err, res) {
if (err) return done(err);
var names = (res.headers.names || '').split(',');
expect(names).to.eql(['initial', 'custom', 'routes']);
done();
});
});
it('configures middleware (end-to-end)', function(done) {
boot.execute(app, simpleAppInstructions());
supertest(app)
.get('/')
.end(function(err, res) {
if (err) return done(err);
expect(res.headers.names).to.equal('custom-middleware');
done();
});
});
});
function assertValidDataSource(dataSource) {
@ -350,6 +410,7 @@ function someInstructions(values) {
config: values.config || {},
models: values.models || [],
dataSources: values.dataSources || { db: { connector: 'memory' } },
middleware: values.middleware || { phases: [], middleware: [] },
files: {
boot: []
}

View File

@ -0,0 +1,7 @@
{
"initial": {
"../../helpers/push-name-middleware": {
"params": "custom-middleware"
}
}
}

View File

@ -0,0 +1,8 @@
module.exports = function(name) {
return function(req, res, next) {
req._names = req._names || [];
req._names.push(name);
res.setHeader('names', req._names.join(','));
next();
};
};