Merge pull request #837 from strongloop/feature/scope-middleware-to-path
#794 - Scope app middleware to a list of paths
This commit is contained in:
commit
1c1e64c09e
|
@ -3,6 +3,7 @@ var express = require('express');
|
||||||
var merge = require('util')._extend;
|
var merge = require('util')._extend;
|
||||||
var PhaseList = require('loopback-phase').PhaseList;
|
var PhaseList = require('loopback-phase').PhaseList;
|
||||||
var debug = require('debug')('loopback:app');
|
var debug = require('debug')('loopback:app');
|
||||||
|
var pathToRegexp = require('path-to-regexp');
|
||||||
|
|
||||||
var proto = {};
|
var proto = {};
|
||||||
|
|
||||||
|
@ -31,13 +32,15 @@ module.exports = function loopbackExpress() {
|
||||||
* @param {function} factory The factory function creating a middleware handler.
|
* @param {function} factory The factory function creating a middleware handler.
|
||||||
* Typically a result of `require()` call, e.g. `require('compression')`.
|
* Typically a result of `require()` call, e.g. `require('compression')`.
|
||||||
* @options {Object} config The configuration.
|
* @options {Object} config The configuration.
|
||||||
* @property {String} phase The phase to register the middelware in.
|
* @property {String} phase The phase to register the middleware in.
|
||||||
* @property {Boolean} [enabled] Whether the middleware is enabled.
|
* @property {Boolean} [enabled] Whether the middleware is enabled.
|
||||||
* Default: `true`.
|
* Default: `true`.
|
||||||
* @property {Array|*} [params] The arguments to pass to the factory
|
* @property {Array|*} [params] The arguments to pass to the factory
|
||||||
* function. Either an array of arguments,
|
* function. Either an array of arguments,
|
||||||
* or the value of the first argument when the factory expects
|
* or the value of the first argument when the factory expects
|
||||||
* a single argument only.
|
* a single argument only.
|
||||||
|
* @property {Array|string|RegExp} [paths] Optional list of paths limiting
|
||||||
|
* the scope of the middleware.
|
||||||
*
|
*
|
||||||
* @returns {object} this (fluent API)
|
* @returns {object} this (fluent API)
|
||||||
*
|
*
|
||||||
|
@ -60,7 +63,7 @@ proto.middlewareFromConfig = function(factory, config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var handler = factory.apply(null, params);
|
var handler = factory.apply(null, params);
|
||||||
this.middleware(config.phase, handler);
|
this.middleware(config.phase, config.paths || [], handler);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
@ -112,6 +115,10 @@ proto.defineMiddlewarePhases = function(nameOrArray) {
|
||||||
/**
|
/**
|
||||||
* Register a middleware handler to be executed in a given phase.
|
* Register a middleware handler to be executed in a given phase.
|
||||||
* @param {string} name The phase name, e.g. "init" or "routes".
|
* @param {string} name The phase name, e.g. "init" or "routes".
|
||||||
|
* @param {Array|string|RegExp} [paths] Optional list of paths limiting
|
||||||
|
* the scope of the middleware.
|
||||||
|
* String paths are interpreted as expressjs path patterns,
|
||||||
|
* regular expressions are used as-is.
|
||||||
* @param {function} handler The middleware handler, one of
|
* @param {function} handler The middleware handler, one of
|
||||||
* `function(req, res, next)` or
|
* `function(req, res, next)` or
|
||||||
* `function(err, req, res, next)`
|
* `function(err, req, res, next)`
|
||||||
|
@ -119,11 +126,21 @@ proto.defineMiddlewarePhases = function(nameOrArray) {
|
||||||
*
|
*
|
||||||
* @header app.middleware(name, handler)
|
* @header app.middleware(name, handler)
|
||||||
*/
|
*/
|
||||||
proto.middleware = function(name, handler) {
|
proto.middleware = function(name, paths, handler) {
|
||||||
this.lazyrouter();
|
this.lazyrouter();
|
||||||
|
|
||||||
|
if (handler === undefined && typeof paths === 'function') {
|
||||||
|
handler = paths;
|
||||||
|
paths = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof paths === 'string' || paths instanceof RegExp) {
|
||||||
|
paths = [paths];
|
||||||
|
}
|
||||||
|
|
||||||
assert(typeof name === 'string' && name, '"name" must be a non-empty string');
|
assert(typeof name === 'string' && name, '"name" must be a non-empty string');
|
||||||
assert(typeof handler === 'function', '"handler" must be a function');
|
assert(typeof handler === 'function', '"handler" must be a function');
|
||||||
|
assert(Array.isArray(paths), '"paths" must be an array');
|
||||||
|
|
||||||
var fullName = name;
|
var fullName = name;
|
||||||
var handlerName = handler.name || '(anonymous)';
|
var handlerName = handler.name || '(anonymous)';
|
||||||
|
@ -139,13 +156,15 @@ proto.middleware = function(name, handler) {
|
||||||
if (!phase)
|
if (!phase)
|
||||||
throw new Error('Unknown middleware phase ' + name);
|
throw new Error('Unknown middleware phase ' + name);
|
||||||
|
|
||||||
|
var matches = createRequestMatcher(paths);
|
||||||
|
|
||||||
var wrapper;
|
var wrapper;
|
||||||
if (handler.length === 4) {
|
if (handler.length === 4) {
|
||||||
// handler is function(err, req, res, next)
|
// handler is function(err, req, res, next)
|
||||||
debug('Add error handler %j to phase %j', handlerName, fullName);
|
debug('Add error handler %j to phase %j', handlerName, fullName);
|
||||||
|
|
||||||
wrapper = function errorHandler(ctx, next) {
|
wrapper = function errorHandler(ctx, next) {
|
||||||
if (ctx.err) {
|
if (ctx.err && matches(ctx.req)) {
|
||||||
var err = ctx.err;
|
var err = ctx.err;
|
||||||
ctx.err = undefined;
|
ctx.err = undefined;
|
||||||
handler(err, ctx.req, ctx.res, storeErrorAndContinue(ctx, next));
|
handler(err, ctx.req, ctx.res, storeErrorAndContinue(ctx, next));
|
||||||
|
@ -157,7 +176,7 @@ proto.middleware = function(name, handler) {
|
||||||
// handler is function(req, res, next)
|
// handler is function(req, res, next)
|
||||||
debug('Add middleware %j to phase %j', handlerName , fullName);
|
debug('Add middleware %j to phase %j', handlerName , fullName);
|
||||||
wrapper = function regularHandler(ctx, next) {
|
wrapper = function regularHandler(ctx, next) {
|
||||||
if (ctx.err) {
|
if (ctx.err || !matches(ctx.req)) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
handler(ctx.req, ctx.res, storeErrorAndContinue(ctx, next));
|
handler(ctx.req, ctx.res, storeErrorAndContinue(ctx, next));
|
||||||
|
@ -169,6 +188,26 @@ proto.middleware = function(name, handler) {
|
||||||
return this;
|
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) {
|
function storeErrorAndContinue(ctx, next) {
|
||||||
return function(err) {
|
return function(err) {
|
||||||
if (err) ctx.err = err;
|
if (err) ctx.err = err;
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
"loopback-phase": "^1.0.1",
|
"loopback-phase": "^1.0.1",
|
||||||
"nodemailer": "~1.3.0",
|
"nodemailer": "~1.3.0",
|
||||||
"nodemailer-stub-transport": "~0.1.4",
|
"nodemailer-stub-transport": "~0.1.4",
|
||||||
|
"path-to-regexp": "^1.0.1",
|
||||||
"strong-remoting": "^2.4.0",
|
"strong-remoting": "^2.4.0",
|
||||||
"uid2": "0.0.3",
|
"uid2": "0.0.3",
|
||||||
"underscore": "~1.7.0",
|
"underscore": "~1.7.0",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
var async = require('async');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
|
|
||||||
var http = require('http');
|
var http = require('http');
|
||||||
|
@ -109,12 +110,58 @@ describe('app', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('scopes middleware to a string path', function(done) {
|
||||||
|
app.middleware('initial', '/scope', pathSavingHandler());
|
||||||
|
|
||||||
|
async.eachSeries(
|
||||||
|
['/', '/scope', '/scope/item', '/other'],
|
||||||
|
function(url, next) { executeMiddlewareHandlers(app, url, next); },
|
||||||
|
function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
expect(steps).to.eql(['/scope', '/scope/item']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scopes middleware to a regex path', function(done) {
|
||||||
|
app.middleware('initial', /^\/(a|b)/, pathSavingHandler());
|
||||||
|
|
||||||
|
async.eachSeries(
|
||||||
|
['/', '/a', '/b', '/c'],
|
||||||
|
function(url, next) { executeMiddlewareHandlers(app, url, next); },
|
||||||
|
function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
expect(steps).to.eql(['/a', '/b']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scopes middleware to a list of scopes', function(done) {
|
||||||
|
app.middleware('initial', ['/scope', /^\/(a|b)/], pathSavingHandler());
|
||||||
|
|
||||||
|
async.eachSeries(
|
||||||
|
['/', '/a', '/b', '/c', '/scope', '/other'],
|
||||||
|
function(url, next) { executeMiddlewareHandlers(app, url, next); },
|
||||||
|
function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
expect(steps).to.eql(['/a', '/b', '/scope']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function namedHandler(name) {
|
function namedHandler(name) {
|
||||||
return function(req, res, next) {
|
return function(req, res, next) {
|
||||||
steps.push(name);
|
steps.push(name);
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pathSavingHandler() {
|
||||||
|
return function(req, res, next) {
|
||||||
|
steps.push(req.url);
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.onServer('.middlewareFromConfig', function() {
|
describe.onServer('.middlewareFromConfig', function() {
|
||||||
|
@ -168,6 +215,30 @@ describe('app', function() {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('scopes middleware to a list of scopes', function(done) {
|
||||||
|
var steps = [];
|
||||||
|
app.middlewareFromConfig(
|
||||||
|
function factory() {
|
||||||
|
return function(req, res, next) {
|
||||||
|
steps.push(req.url);
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
phase: 'initial',
|
||||||
|
paths: ['/scope', /^\/(a|b)/]
|
||||||
|
});
|
||||||
|
|
||||||
|
async.eachSeries(
|
||||||
|
['/', '/a', '/b', '/c', '/scope', '/other'],
|
||||||
|
function(url, next) { executeMiddlewareHandlers(app, url, next); },
|
||||||
|
function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
expect(steps).to.eql(['/a', '/b', '/scope']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.onServer('.defineMiddlewarePhases(nameOrArray)', function() {
|
describe.onServer('.defineMiddlewarePhases(nameOrArray)', function() {
|
||||||
|
@ -600,13 +671,18 @@ describe('app', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function executeMiddlewareHandlers(app, callback) {
|
function executeMiddlewareHandlers(app, urlPath, callback) {
|
||||||
var server = http.createServer(function(req, res) {
|
var server = http.createServer(function(req, res) {
|
||||||
app.handle(req, res, callback);
|
app.handle(req, res, callback);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (callback === undefined && typeof urlPath === 'function') {
|
||||||
|
callback = urlPath;
|
||||||
|
urlPath = '/test/url';
|
||||||
|
}
|
||||||
|
|
||||||
request(server)
|
request(server)
|
||||||
.get('/test/url')
|
.get(urlPath)
|
||||||
.end(function(err) {
|
.end(function(err) {
|
||||||
if (err) return callback(err);
|
if (err) return callback(err);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue