Merge branch 'release/2.8.6' into production

This commit is contained in:
Miroslav Bajtoš 2014-12-15 19:29:18 +01:00
commit 779f187fb9
4 changed files with 282 additions and 117 deletions

View File

@ -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)

View File

@ -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];
}
};

View File

@ -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"

View File

@ -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) {