2014-11-11 16:51:50 +00:00
|
|
|
var assert = require('assert');
|
Middleware phases - initial implementation
Modify the app and router implementation, so that the middleware is
executed in order defined by phases.
Predefined phases:
'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final'
Methods defined via `app.use`, `app.route` and friends are executed
as the first thing in 'routes' phase.
API usage:
app.middleware('initial', compression());
app.middleware('initial:before', serveFavicon());
app.middleware('files:after', loopback.urlNotFound());
app.middleware('final:after', errorHandler());
Middleware flavours:
// regular handler
function handler(req, res, next) {
// do stuff
next();
}
// error handler
function errorHandler(err, req, res, next) {
// handle error and/or call next
next(err);
}
2014-11-05 19:07:58 +00:00
|
|
|
var express = require('express');
|
|
|
|
var merge = require('util')._extend;
|
|
|
|
var PhaseList = require('loopback-phase').PhaseList;
|
|
|
|
var debug = require('debug')('loopback:app');
|
|
|
|
|
|
|
|
var proto = {};
|
|
|
|
|
|
|
|
module.exports = function loopbackExpress() {
|
|
|
|
var app = express();
|
|
|
|
app.__expressLazyRouter = app.lazyrouter;
|
|
|
|
merge(app, proto);
|
|
|
|
return app;
|
|
|
|
};
|
|
|
|
|
2014-11-11 16:51:50 +00:00
|
|
|
/**
|
|
|
|
* Register a middleware using a factory function and a JSON config.
|
|
|
|
*
|
|
|
|
* **Example**
|
|
|
|
*
|
|
|
|
* ```js
|
|
|
|
* app.middlewareFromConfig(compression, {
|
|
|
|
* enabled: true,
|
|
|
|
* phase: 'initial',
|
2014-11-13 12:22:25 +00:00
|
|
|
* params: {
|
2014-11-11 16:51:50 +00:00
|
|
|
* threshold: 128
|
|
|
|
* }
|
|
|
|
* });
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param {function} factory The factory function creating a middleware handler.
|
|
|
|
* Typically a result of `require()` call, e.g. `require('compression')`.
|
|
|
|
* @param {Array|*} config The configuration. Either an array of arguments
|
|
|
|
* to pass to the factory function, or the value of the first argument
|
|
|
|
* when the factory expects a single argument only.
|
2014-11-13 12:22:25 +00:00
|
|
|
*
|
|
|
|
* @returns {object} this (fluent API)
|
2014-11-11 16:51:50 +00:00
|
|
|
*/
|
|
|
|
proto.middlewareFromConfig = function(factory, config) {
|
|
|
|
assert(typeof factory === 'function', '"factory" must be a function');
|
|
|
|
assert(typeof config === 'object', '"config" must be an object');
|
|
|
|
assert(typeof config.phase === 'string' && config.phase,
|
|
|
|
'"config.phase" must be a non-empty string');
|
|
|
|
|
|
|
|
if (config.enabled === false)
|
|
|
|
return;
|
|
|
|
|
2014-11-13 12:22:25 +00:00
|
|
|
var params = config.params;
|
|
|
|
if (params === undefined) {
|
|
|
|
params = [];
|
|
|
|
} else if (!Array.isArray(params)) {
|
|
|
|
params = [params];
|
2014-11-11 16:51:50 +00:00
|
|
|
}
|
|
|
|
|
2014-11-13 12:22:25 +00:00
|
|
|
var handler = factory.apply(null, params);
|
2014-11-11 16:51:50 +00:00
|
|
|
this.middleware(config.phase, handler);
|
2014-11-13 12:22:25 +00:00
|
|
|
|
|
|
|
return this;
|
2014-11-11 16:51:50 +00:00
|
|
|
};
|
|
|
|
|
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
2014-11-11 16:13:44 +00:00
|
|
|
/**
|
|
|
|
* 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.<string>} nameOrArray A phase name or a list of phase
|
|
|
|
* names to add.
|
2014-11-13 12:22:25 +00:00
|
|
|
*
|
|
|
|
* @returns {object} this (fluent API)
|
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
2014-11-11 16:13:44 +00:00
|
|
|
*/
|
|
|
|
proto.defineMiddlewarePhases = function(nameOrArray) {
|
|
|
|
this.lazyrouter();
|
|
|
|
|
Simplify `app.defineMiddlewarePhases`
Refactor the implementation to use the new method `phaseList.zipMerge`.
This is commit is changing the behaviour in the case when
the first new phase does not exist in the current list.
Before the change, all new phases were added just before the "routes"
phase.
After this change, new phases are added to the head of the list,
until an existing phase is encountered, at which point the regular
merge algorithm kicks in.
Example:
app.defineMiddlewarePhases(['first', 'routes', 'subapps']);
Before the change: code throws an error - 'routes' already exists.
After the change: phases are merged with the following result:
'first', 'initial', ..., 'routes', 'subapps', ...
2014-11-12 07:59:56 +00:00
|
|
|
if (Array.isArray(nameOrArray)) {
|
|
|
|
this._requestHandlingPhases.zipMerge(nameOrArray);
|
|
|
|
} else {
|
|
|
|
this._requestHandlingPhases.addBefore('routes', nameOrArray);
|
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
2014-11-11 16:13:44 +00:00
|
|
|
}
|
2014-11-13 12:22:25 +00:00
|
|
|
|
|
|
|
return this;
|
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
2014-11-11 16:13:44 +00:00
|
|
|
};
|
|
|
|
|
Middleware phases - initial implementation
Modify the app and router implementation, so that the middleware is
executed in order defined by phases.
Predefined phases:
'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final'
Methods defined via `app.use`, `app.route` and friends are executed
as the first thing in 'routes' phase.
API usage:
app.middleware('initial', compression());
app.middleware('initial:before', serveFavicon());
app.middleware('files:after', loopback.urlNotFound());
app.middleware('final:after', errorHandler());
Middleware flavours:
// regular handler
function handler(req, res, next) {
// do stuff
next();
}
// error handler
function errorHandler(err, req, res, next) {
// handle error and/or call next
next(err);
}
2014-11-05 19:07:58 +00:00
|
|
|
/**
|
|
|
|
* Register a middleware handler to be executed in a given phase.
|
|
|
|
* @param {string} name The phase name, e.g. "init" or "routes".
|
|
|
|
* @param {function} handler The middleware handler, one of
|
|
|
|
* `function(req, res, next)` or
|
|
|
|
* `function(err, req, res, next)`
|
|
|
|
* @returns {object} this (fluent API)
|
|
|
|
*/
|
|
|
|
proto.middleware = function(name, handler) {
|
|
|
|
this.lazyrouter();
|
|
|
|
|
2014-11-11 16:51:50 +00:00
|
|
|
assert(typeof name === 'string' && name, '"name" must be a non-empty string');
|
|
|
|
assert(typeof handler === 'function', '"handler" must be a function');
|
|
|
|
|
Middleware phases - initial implementation
Modify the app and router implementation, so that the middleware is
executed in order defined by phases.
Predefined phases:
'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final'
Methods defined via `app.use`, `app.route` and friends are executed
as the first thing in 'routes' phase.
API usage:
app.middleware('initial', compression());
app.middleware('initial:before', serveFavicon());
app.middleware('files:after', loopback.urlNotFound());
app.middleware('final:after', errorHandler());
Middleware flavours:
// regular handler
function handler(req, res, next) {
// do stuff
next();
}
// error handler
function errorHandler(err, req, res, next) {
// handle error and/or call next
next(err);
}
2014-11-05 19:07:58 +00:00
|
|
|
var fullName = name;
|
|
|
|
var handlerName = handler.name || '(anonymous)';
|
|
|
|
|
|
|
|
var hook = 'use';
|
|
|
|
var m = name.match(/^(.+):(before|after)$/);
|
|
|
|
if (m) {
|
|
|
|
name = m[1];
|
|
|
|
hook = m[2];
|
|
|
|
}
|
|
|
|
|
|
|
|
var phase = this._requestHandlingPhases.find(name);
|
|
|
|
if (!phase)
|
|
|
|
throw new Error('Unknown middleware phase ' + name);
|
|
|
|
|
|
|
|
var wrapper;
|
|
|
|
if (handler.length === 4) {
|
|
|
|
// handler is function(err, req, res, next)
|
|
|
|
debug('Add error handler %j to phase %j', handlerName, fullName);
|
|
|
|
|
|
|
|
wrapper = function errorHandler(ctx, next) {
|
|
|
|
if (ctx.err) {
|
|
|
|
var err = ctx.err;
|
|
|
|
ctx.err = undefined;
|
|
|
|
handler(err, ctx.req, ctx.res, storeErrorAndContinue(ctx, next));
|
|
|
|
} else {
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
// handler is function(req, res, next)
|
|
|
|
debug('Add middleware %j to phase %j', handlerName , fullName);
|
|
|
|
wrapper = function regularHandler(ctx, next) {
|
|
|
|
if (ctx.err) {
|
|
|
|
next();
|
|
|
|
} else {
|
|
|
|
handler(ctx.req, ctx.res, storeErrorAndContinue(ctx, next));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
phase[hook](wrapper);
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
|
|
|
|
function storeErrorAndContinue(ctx, next) {
|
|
|
|
return function(err) {
|
|
|
|
if (err) ctx.err = err;
|
|
|
|
next();
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Install our custom PhaseList-based handler into the app
|
|
|
|
proto.lazyrouter = function() {
|
|
|
|
var self = this;
|
|
|
|
if (self._router) return;
|
|
|
|
|
|
|
|
self.__expressLazyRouter();
|
|
|
|
|
|
|
|
// Storing the fn in another property of the router object
|
|
|
|
// allows us to call the method with the router as `this`
|
|
|
|
// without the need to use slow `call` or `apply`.
|
|
|
|
self._router.__expressHandle = self._router.handle;
|
|
|
|
|
|
|
|
self._requestHandlingPhases = new PhaseList();
|
|
|
|
self._requestHandlingPhases.add([
|
|
|
|
'initial', 'session', 'auth', 'parse',
|
|
|
|
'routes', 'files', 'final'
|
|
|
|
]);
|
|
|
|
|
|
|
|
// In order to pass error into express router, we have
|
|
|
|
// to pass it to a middleware executed from within the router.
|
|
|
|
// This is achieved by adding a phase-handler that wraps the error
|
|
|
|
// into `req` object and then a router-handler that unwraps the error
|
|
|
|
// and calls `next(err)`.
|
|
|
|
// It is important to register these two handlers at the very beginning,
|
|
|
|
// before any other handlers are added.
|
|
|
|
self.middleware('routes', function wrapError(err, req, res, next) {
|
|
|
|
req.__err = err;
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
|
|
|
|
self.use(function unwrapError(req, res, next) {
|
|
|
|
var err = req.__err;
|
|
|
|
req.__err = undefined;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
|
|
|
|
self.middleware('routes', function runRootHandlers(req, res, next) {
|
|
|
|
self._router.__expressHandle(req, res, next);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Overwrite the original handle() function provided by express,
|
|
|
|
// replace it with our implementation based on PhaseList
|
|
|
|
self._router.handle = function(req, res, next) {
|
|
|
|
var ctx = { req: req, res: res };
|
|
|
|
self._requestHandlingPhases.run(ctx, function(err) {
|
|
|
|
next(err || ctx.err);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
};
|