Merge pull request #66 from strongloop/feature/middleware-mounting
Load middleware and phases from `middleware.json`
This commit is contained in:
commit
d25c64523d
2
index.js
2
index.js
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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: []
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"initial": {
|
||||
"../../helpers/push-name-middleware": {
|
||||
"params": "custom-middleware"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue