From 98d439050a0d721a09c381cbdd47966f2449dda7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 11 Nov 2014 17:13:44 +0100 Subject: [PATCH] Implement `app.defineMiddlewarePhases` Implement method for registering (new) middleware phases. - If all names are new, then the phases are added just before the "routes" phase. - Otherwise the provided list of names is merged with the existing phases in such way that the order of phases is preserved. Example // built-in phases: // initial, session, auth, parse, routes, files, final app.defineMiddlewarePhases('custom'); // new list of phases // initial, session, auth, parse, // custom, // routes, files, final app.defineMiddlewarePhases([ 'initial', 'postinit', 'preauth', 'routes', 'subapps' ]); // new list of phases // initial, // postinit, preauth, // session, auth, parse, custom, // routes, // subapps, // files, final --- lib/server-app.js | 71 +++++++++++++++++++++++++++++++++++++++++++++++ test/app.test.js | 58 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/lib/server-app.js b/lib/server-app.js index 7f7cda16..71ea7b0c 100644 --- a/lib/server-app.js +++ b/lib/server-app.js @@ -54,6 +54,77 @@ proto.middlewareFromConfig = function(factory, config) { this.middleware(config.phase, handler); }; +/** + * Register (new) middleware phases. + * + * If all names are new, then the phases are added just before "routes" phase. + * Otherwise the provided list of names is merged with the existing phases + * in such way that the order of phases is preserved. + * + * **Examples** + * + * ```js + * // built-in phases: + * // initial, session, auth, parse, routes, files, final + * + * app.defineMiddlewarePhases('custom'); + * // new list of phases + * // initial, session, auth, parse, custom, routes, files, final + * + * app.defineMiddlewarePhases([ + * 'initial', 'postinit', 'preauth', 'routes', 'subapps' + * ]); + * // new list of phases + * // initial, postinit, preauth, session, auth, parse, custom, + * // routes, subapps, files, final + * ``` + * + * @param {string|Array.} nameOrArray A phase name or a list of phase + * names to add. + */ +proto.defineMiddlewarePhases = function(nameOrArray) { + this.lazyrouter(); + if (!Array.isArray(nameOrArray)) { + this._requestHandlingPhases.addBefore('routes', nameOrArray); + return; + } + + var sourcePhases = nameOrArray; + if (!sourcePhases.length) return; + + var targetPhases = this._requestHandlingPhases.getPhaseNames(); + var targetIx = targetPhases.indexOf(nameOrArray[0]); + + if (targetIx === -1) { + // the first new phase does not match any existing one + // add all phases before "routes" + sourcePhases.forEach(function(it) { + this._requestHandlingPhases.addBefore('routes', it); + }, this); + return; + } + + // merge (zip) two arrays of phases + for (var sourceIx = 1; sourceIx < sourcePhases.length; sourceIx++) { + var nameToAdd = sourcePhases[sourceIx]; + var previousPhase = sourcePhases[sourceIx - 1]; + var existingIx = targetPhases.indexOf(nameToAdd, targetIx); + if (existingIx === -1) { + // A new phase - try to add it after the last one, + // unless it was already registered + if (targetPhases.indexOf(nameToAdd) !== -1) { + throw new Error('Phase ordering conflict: cannot add "' + nameToAdd + + '" after "' + previousPhase + '", because the opposite order was ' + + ' already specified'); + } + this._requestHandlingPhases.addAfter(previousPhase, nameToAdd); + } else { + // An existing phase - move the pointer + targetIx = existingIx; + } + } +}; + /** * Register a middleware handler to be executed in a given phase. * @param {string} name The phase name, e.g. "init" or "routes". diff --git a/test/app.test.js b/test/app.test.js index 9c29e706..18ab3cbc 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -146,6 +146,64 @@ describe('app', function() { }); }); + describe.onServer('.defineMiddlewarePhases(nameOrArray)', function() { + var app; + beforeEach(function() { + app = loopback(); + }); + + it('adds the phase just before "routes" by default', function(done) { + app.defineMiddlewarePhases('custom'); + verifyMiddlewarePhases(['custom', 'routes'], done); + }); + + it('adds an array of phases just before "routes"', function(done) { + app.defineMiddlewarePhases(['custom1', 'custom2']); + verifyMiddlewarePhases(['custom1', 'custom2', 'routes'], done); + }); + + it('merges phases preserving the order', function(done) { + app.defineMiddlewarePhases([ + 'initial', + 'postinit', 'preauth', // add + 'auth', 'routes', + 'subapps', // add + 'final', + 'last' // add + ]); + verifyMiddlewarePhases([ + 'initial', + 'postinit', 'preauth', // new + 'auth', 'routes', + 'subapps', // new + 'files', 'final', + 'last' // new + ], done); + }); + + it('throws helpful error on ordering conflict', function() { + app.defineMiddlewarePhases(['first', 'second']); + expect(function() { app.defineMiddlewarePhases(['second', 'first']); }) + .to.throw(/ordering conflict.*first.*second/); + }); + + function verifyMiddlewarePhases(names, done) { + var steps = []; + names.forEach(function(it) { + app.middleware(it, function(req, res, next) { + steps.push(it); + next(); + }); + }); + + executeMiddlewareHandlers(app, function(err) { + if (err) return done(err); + expect(steps).to.eql(names); + done(); + }); + } + }); + describe('app.model(Model)', function() { var app, db; beforeEach(function() {