Merge branch 'release/2.8.6' into production
This commit is contained in:
commit
779f187fb9
19
CHANGES.md
19
CHANGES.md
|
@ -1,3 +1,11 @@
|
|||
2014-12-15, Version 2.8.6
|
||||
=========================
|
||||
|
||||
* server-app: make _sortLayersByPhase stable (Miroslav Bajtoš)
|
||||
|
||||
* Rework phased middleware, fix several bugs (Miroslav Bajtoš)
|
||||
|
||||
|
||||
2014-12-12, Version 2.8.5
|
||||
=========================
|
||||
|
||||
|
@ -9,16 +17,19 @@
|
|||
|
||||
* Fix bcrypt issues for browserify (Raymond Feng)
|
||||
|
||||
|
||||
2014-12-08, Version 2.8.4
|
||||
=========================
|
||||
|
||||
* Allow native bcrypt for performance (Raymond Feng)
|
||||
|
||||
|
||||
2014-12-08, Version 2.8.3
|
||||
=========================
|
||||
|
||||
|
||||
|
||||
2014-12-08, Version 2.8.4
|
||||
=========================
|
||||
|
||||
* Allow native bcrypt for performance (Raymond Feng)
|
||||
|
||||
* Remove unused underscore dependency (Ryan Graham)
|
||||
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
var assert = require('assert');
|
||||
var express = require('express');
|
||||
var merge = require('util')._extend;
|
||||
var PhaseList = require('loopback-phase').PhaseList;
|
||||
var mergePhaseNameLists = require('loopback-phase').mergePhaseNameLists;
|
||||
var debug = require('debug')('loopback:app');
|
||||
var pathToRegexp = require('path-to-regexp');
|
||||
var stableSortInPlace = require('stable').inplace;
|
||||
|
||||
var BUILTIN_MIDDLEWARE = { builtin: true };
|
||||
|
||||
var proto = {};
|
||||
|
||||
|
@ -104,9 +106,12 @@ proto.defineMiddlewarePhases = function(nameOrArray) {
|
|||
this.lazyrouter();
|
||||
|
||||
if (Array.isArray(nameOrArray)) {
|
||||
this._requestHandlingPhases.zipMerge(nameOrArray);
|
||||
this._requestHandlingPhases =
|
||||
mergePhaseNameLists(this._requestHandlingPhases, nameOrArray);
|
||||
} else {
|
||||
this._requestHandlingPhases.addBefore('routes', nameOrArray);
|
||||
// add the new phase before 'routes'
|
||||
var routesIx = this._requestHandlingPhases.indexOf('routes');
|
||||
this._requestHandlingPhases.splice(routesIx - 1, 0, nameOrArray);
|
||||
}
|
||||
|
||||
return this;
|
||||
|
@ -131,90 +136,39 @@ proto.middleware = function(name, paths, handler) {
|
|||
|
||||
if (handler === undefined && typeof paths === 'function') {
|
||||
handler = paths;
|
||||
paths = [];
|
||||
}
|
||||
|
||||
if (typeof paths === 'string' || paths instanceof RegExp) {
|
||||
paths = [paths];
|
||||
paths = undefined;
|
||||
}
|
||||
|
||||
assert(typeof name === 'string' && name, '"name" must be a non-empty string');
|
||||
assert(typeof handler === 'function', '"handler" must be a function');
|
||||
assert(Array.isArray(paths), '"paths" must be an array');
|
||||
|
||||
var fullName = name;
|
||||
var handlerName = handler.name || '(anonymous)';
|
||||
if (paths === undefined) {
|
||||
paths = '/';
|
||||
}
|
||||
|
||||
var fullPhaseName = 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)
|
||||
if (this._requestHandlingPhases.indexOf(name) === -1)
|
||||
throw new Error('Unknown middleware phase ' + name);
|
||||
|
||||
var matches = createRequestMatcher(paths);
|
||||
debug('use %s %s %s', fullPhaseName, paths, handlerName);
|
||||
|
||||
var wrapper;
|
||||
if (handler.length === 4) {
|
||||
// handler is function(err, req, res, next)
|
||||
debug('Add error handler %j to phase %j', handlerName, fullName);
|
||||
this._skipLayerSorting = true;
|
||||
this.use(paths, handler);
|
||||
this._router.stack[this._router.stack.length - 1].phase = fullPhaseName;
|
||||
this._skipLayerSorting = false;
|
||||
|
||||
wrapper = function errorHandler(ctx, next) {
|
||||
if (ctx.err && matches(ctx.req)) {
|
||||
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 || !matches(ctx.req)) {
|
||||
next();
|
||||
} else {
|
||||
handler(ctx.req, ctx.res, storeErrorAndContinue(ctx, next));
|
||||
}
|
||||
};
|
||||
}
|
||||
this._sortLayersByPhase();
|
||||
|
||||
phase[hook](wrapper);
|
||||
return this;
|
||||
};
|
||||
|
||||
function createRequestMatcher(paths) {
|
||||
if (!paths.length) {
|
||||
return function requestMatcher(req) { return true; };
|
||||
}
|
||||
|
||||
var checks = paths.map(function(p) {
|
||||
return pathToRegexp(p, {
|
||||
sensitive: true,
|
||||
strict: false,
|
||||
end: false
|
||||
});
|
||||
});
|
||||
|
||||
return function requestMatcher(req) {
|
||||
return checks.some(function(regex) {
|
||||
return regex.test(req.url);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -222,45 +176,70 @@ proto.lazyrouter = function() {
|
|||
|
||||
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;
|
||||
var router = self._router;
|
||||
|
||||
self._requestHandlingPhases = new PhaseList();
|
||||
self._requestHandlingPhases.add([
|
||||
// Mark all middleware added by Router ctor as builtin
|
||||
// The sorting algo will keep them at beginning of the list
|
||||
router.stack.forEach(function(layer) {
|
||||
layer.phase = BUILTIN_MIDDLEWARE;
|
||||
});
|
||||
|
||||
router.__expressUse = router.use;
|
||||
router.use = function useAndSort() {
|
||||
var retval = this.__expressUse.apply(this, arguments);
|
||||
self._sortLayersByPhase();
|
||||
return retval;
|
||||
};
|
||||
|
||||
router.__expressRoute = router.route;
|
||||
router.route = function routeAndSort() {
|
||||
var retval = this.__expressRoute.apply(this, arguments);
|
||||
self._sortLayersByPhase();
|
||||
return retval;
|
||||
};
|
||||
|
||||
self._requestHandlingPhases = [
|
||||
'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);
|
||||
});
|
||||
};
|
||||
];
|
||||
};
|
||||
|
||||
proto._sortLayersByPhase = function() {
|
||||
if (this._skipLayerSorting) return;
|
||||
|
||||
var phaseOrder = {};
|
||||
this._requestHandlingPhases.forEach(function(name, ix) {
|
||||
phaseOrder[name + ':before'] = ix * 3;
|
||||
phaseOrder[name] = ix * 3 + 1;
|
||||
phaseOrder[name + ':after'] = ix * 3 + 2;
|
||||
});
|
||||
|
||||
var router = this._router;
|
||||
stableSortInPlace(router.stack, compareLayers);
|
||||
|
||||
function compareLayers(left, right) {
|
||||
var leftPhase = left.phase;
|
||||
var rightPhase = right.phase;
|
||||
|
||||
if (leftPhase === rightPhase) return 0;
|
||||
|
||||
// Builtin middleware is always first
|
||||
if (leftPhase === BUILTIN_MIDDLEWARE) return -1;
|
||||
if (rightPhase === BUILTIN_MIDDLEWARE) return 1;
|
||||
|
||||
// Layers registered via app.use and app.route
|
||||
// are executed as the first items in `routes` phase
|
||||
if (leftPhase === undefined) {
|
||||
if (rightPhase === 'routes')
|
||||
return -1;
|
||||
|
||||
return phaseOrder['routes'] - phaseOrder[rightPhase];
|
||||
}
|
||||
|
||||
if (rightPhase === undefined)
|
||||
return -compareLayers(right, left);
|
||||
|
||||
// Layers registered via `app.middleware` are compared via phase & hook
|
||||
return phaseOrder[leftPhase] - phaseOrder[rightPhase];
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "loopback",
|
||||
"version": "2.8.5",
|
||||
"version": "2.8.6",
|
||||
"description": "LoopBack: Open Source Framework for Node.js",
|
||||
"homepage": "http://loopback.io",
|
||||
"keywords": [
|
||||
|
@ -42,11 +42,11 @@
|
|||
"express": "^4.10.2",
|
||||
"inflection": "~1.4.2",
|
||||
"loopback-connector-remote": "^1.0.1",
|
||||
"loopback-phase": "^1.0.1",
|
||||
"loopback-phase": "^1.2.0",
|
||||
"nodemailer": "~1.3.0",
|
||||
"nodemailer-stub-transport": "~0.1.4",
|
||||
"path-to-regexp": "^1.0.1",
|
||||
"serve-favicon": "^2.1.6",
|
||||
"stable": "^0.1.5",
|
||||
"strong-remoting": "^2.4.0",
|
||||
"uid2": "0.0.3",
|
||||
"underscore.string": "~2.3.3"
|
||||
|
|
181
test/app.test.js
181
test/app.test.js
|
@ -4,6 +4,7 @@ var async = require('async');
|
|||
var path = require('path');
|
||||
|
||||
var http = require('http');
|
||||
var express = require('express');
|
||||
var loopback = require('../');
|
||||
var PersistedModel = loopback.PersistedModel;
|
||||
|
||||
|
@ -41,6 +42,17 @@ describe('app', function() {
|
|||
});
|
||||
});
|
||||
|
||||
it('preserves order of handlers in the same phase', function(done) {
|
||||
app.middleware('initial', namedHandler('first'));
|
||||
app.middleware('initial', namedHandler('second'));
|
||||
|
||||
executeMiddlewareHandlers(app, function(err) {
|
||||
if (err) return done(err);
|
||||
expect(steps).to.eql(['first', 'second']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('supports `before:` and `after:` prefixes', function(done) {
|
||||
app.middleware('routes:before', namedHandler('routes:before'));
|
||||
app.middleware('routes:after', namedHandler('routes:after'));
|
||||
|
@ -151,6 +163,160 @@ describe('app', function() {
|
|||
});
|
||||
});
|
||||
|
||||
it('sets req.url to a sub-path', function(done) {
|
||||
app.middleware('initial', ['/scope'], function(req, res, next) {
|
||||
steps.push(req.url);
|
||||
next();
|
||||
});
|
||||
|
||||
executeMiddlewareHandlers(app, '/scope/id', function(err) {
|
||||
if (err) return done(err);
|
||||
expect(steps).to.eql(['/id']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('exposes express helpers on req and res objects', function(done) {
|
||||
var req;
|
||||
var res;
|
||||
|
||||
app.middleware('initial', function(rq, rs, next) {
|
||||
req = rq;
|
||||
res = rs;
|
||||
next();
|
||||
});
|
||||
|
||||
executeMiddlewareHandlers(app, function(err) {
|
||||
if (err) return done(err);
|
||||
expect(getObjectAndPrototypeKeys(req), 'request').to.include.members([
|
||||
'accepts',
|
||||
'get',
|
||||
'param',
|
||||
'params',
|
||||
'query',
|
||||
'res'
|
||||
]);
|
||||
|
||||
expect(getObjectAndPrototypeKeys(res), 'response').to.include.members([
|
||||
'cookie',
|
||||
'download',
|
||||
'json',
|
||||
'jsonp',
|
||||
'redirect',
|
||||
'req',
|
||||
'send',
|
||||
'sendFile',
|
||||
'set'
|
||||
]);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('sets req.baseUrl and req.originalUrl', function(done) {
|
||||
var reqProps;
|
||||
app.middleware('initial', function(req, res, next) {
|
||||
reqProps = { baseUrl: req.baseUrl, originalUrl: req.originalUrl };
|
||||
next();
|
||||
});
|
||||
|
||||
executeMiddlewareHandlers(app, '/test/url', function(err) {
|
||||
if (err) return done(err);
|
||||
expect(reqProps).to.eql({ baseUrl: '', originalUrl: '/test/url' });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves correct order of routes vs. middleware', function(done) {
|
||||
// This test verifies that `app.route` triggers sort of layers
|
||||
app.middleware('files', namedHandler('files'));
|
||||
app.get('/test', namedHandler('route'));
|
||||
|
||||
executeMiddlewareHandlers(app, '/test', function(err) {
|
||||
if (err) return done(err);
|
||||
expect(steps).to.eql(['route', 'files']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves order of middleware in the same phase', function(done) {
|
||||
// while we are discouraging developers from depending on
|
||||
// the registration order of middleware in the same phase,
|
||||
// we must preserve the order for compatibility with `app.use`
|
||||
// and `app.route`.
|
||||
|
||||
// we need at least 9 elements to expose non-stability
|
||||
// of the built-in sort function
|
||||
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
numbers.forEach(function(n) {
|
||||
app.middleware('routes', namedHandler(n));
|
||||
});
|
||||
|
||||
executeMiddlewareHandlers(app, function(err) {
|
||||
if (err) return done;
|
||||
expect(steps).to.eql(numbers);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('correctly mounts express apps', function(done) {
|
||||
var data;
|
||||
var mountWasEmitted;
|
||||
var subapp = express();
|
||||
subapp.use(function(req, res, next) {
|
||||
data = {
|
||||
mountpath: req.app.mountpath,
|
||||
parent: req.app.parent
|
||||
};
|
||||
next();
|
||||
});
|
||||
subapp.on('mount', function() { mountWasEmitted = true; });
|
||||
|
||||
app.middleware('routes', '/mountpath', subapp);
|
||||
|
||||
executeMiddlewareHandlers(app, '/mountpath/test', function(err) {
|
||||
if (err) return done(err);
|
||||
expect(mountWasEmitted, 'mountWasEmitted').to.be.true();
|
||||
expect(data).to.eql({
|
||||
mountpath: '/mountpath',
|
||||
parent: app
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('restores req & res on return from mounted express app', function(done) {
|
||||
// jshint proto:true
|
||||
var expected = {};
|
||||
var actual = {};
|
||||
|
||||
var subapp = express();
|
||||
subapp.use(function verifyTestAssumptions(req, res, next) {
|
||||
expect(req.__proto__).to.not.equal(expected.req);
|
||||
expect(res.__proto__).to.not.equal(expected.res);
|
||||
next();
|
||||
});
|
||||
|
||||
app.middleware('initial', function saveOriginalValues(req, res, next) {
|
||||
expected.req = req.__proto__;
|
||||
expected.res = res.__proto__;
|
||||
next();
|
||||
});
|
||||
app.middleware('routes', subapp);
|
||||
app.middleware('final', function saveActualValues(req, res, next) {
|
||||
actual.req = req.__proto__;
|
||||
actual.res = res.__proto__;
|
||||
next();
|
||||
});
|
||||
|
||||
executeMiddlewareHandlers(app, function(err) {
|
||||
if (err) return done(err);
|
||||
expect(actual.req, 'req').to.equal(expected.req);
|
||||
expect(actual.res, 'res').to.equal(expected.res);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
function namedHandler(name) {
|
||||
return function(req, res, next) {
|
||||
steps.push(name);
|
||||
|
@ -160,10 +326,19 @@ describe('app', function() {
|
|||
|
||||
function pathSavingHandler() {
|
||||
return function(req, res, next) {
|
||||
steps.push(req.url);
|
||||
steps.push(req.originalUrl);
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function getObjectAndPrototypeKeys(obj) {
|
||||
var result = [];
|
||||
for (var k in obj) {
|
||||
result.push(k);
|
||||
}
|
||||
result.sort();
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
describe.onServer('.middlewareFromConfig', function() {
|
||||
|
@ -223,7 +398,7 @@ describe('app', function() {
|
|||
app.middlewareFromConfig(
|
||||
function factory() {
|
||||
return function(req, res, next) {
|
||||
steps.push(req.url);
|
||||
steps.push(req.originalUrl);
|
||||
next();
|
||||
};
|
||||
},
|
||||
|
@ -286,7 +461,7 @@ describe('app', function() {
|
|||
it('throws helpful error on ordering conflict', function() {
|
||||
app.defineMiddlewarePhases(['first', 'second']);
|
||||
expect(function() { app.defineMiddlewarePhases(['second', 'first']); })
|
||||
.to.throw(/ordering conflict.*first.*second/);
|
||||
.to.throw(/Ordering conflict.*first.*second/);
|
||||
});
|
||||
|
||||
function verifyMiddlewarePhases(names, done) {
|
||||
|
|
Loading…
Reference in New Issue