Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions lib/bundler.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions lib/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand All @@ -67,6 +75,7 @@ module.exports = function compile(options) {
config: appConfig,
dataSources: dataSourcesConfig,
models: modelInstructions,
middleware: middlewareInstructions,
files: {
boot: bootScripts
}
Expand Down Expand Up @@ -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
};
}
36 changes: 36 additions & 0 deletions lib/config-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 --*/

/**
Expand Down Expand Up @@ -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;
Expand Down
25 changes: 25 additions & 0 deletions lib/executor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
180 changes: 180 additions & 0 deletions test/compiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading