Skip to content

Commit

Permalink
Merge pull request #837 from strongloop/feature/scope-middleware-to-path
Browse files Browse the repository at this point in the history
#794 - Scope app middleware to a list of paths
  • Loading branch information
bajtos committed Nov 19, 2014
2 parents c411fb3 + 2baa4b0 commit 1c1e64c
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 7 deletions.
49 changes: 44 additions & 5 deletions lib/server-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ var express = require('express');
var merge = require('util')._extend;
var PhaseList = require('loopback-phase').PhaseList;
var debug = require('debug')('loopback:app');
var pathToRegexp = require('path-to-regexp');

var proto = {};

Expand Down Expand Up @@ -31,13 +32,15 @@ module.exports = function loopbackExpress() {
* @param {function} factory The factory function creating a middleware handler.
* Typically a result of `require()` call, e.g. `require('compression')`.
* @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.
* Default: `true`.
* @property {Array|*} [params] The arguments to pass to the factory
* function. Either an array of arguments,
* or the value of the first argument when the factory expects
* a single argument only.
* @property {Array|string|RegExp} [paths] Optional list of paths limiting
* the scope of the middleware.
*
* @returns {object} this (fluent API)
*
Expand All @@ -60,7 +63,7 @@ proto.middlewareFromConfig = function(factory, config) {
}

var handler = factory.apply(null, params);
this.middleware(config.phase, handler);
this.middleware(config.phase, config.paths || [], handler);

return this;
};
Expand Down Expand Up @@ -112,18 +115,32 @@ proto.defineMiddlewarePhases = function(nameOrArray) {
/**
* Register a middleware handler to be executed in a given phase.
* @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
* `function(req, res, next)` or
* `function(err, req, res, next)`
* @returns {object} this (fluent API)
*
* @header app.middleware(name, handler)
*/
proto.middleware = function(name, handler) {
proto.middleware = function(name, paths, handler) {
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 handler === 'function', '"handler" must be a function');
assert(Array.isArray(paths), '"paths" must be an array');

var fullName = name;
var handlerName = handler.name || '(anonymous)';
Expand All @@ -139,13 +156,15 @@ proto.middleware = function(name, handler) {
if (!phase)
throw new Error('Unknown middleware phase ' + name);

var matches = createRequestMatcher(paths);

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) {
if (ctx.err && matches(ctx.req)) {
var err = ctx.err;
ctx.err = undefined;
handler(err, ctx.req, ctx.res, storeErrorAndContinue(ctx, next));
Expand All @@ -157,7 +176,7 @@ proto.middleware = function(name, handler) {
// handler is function(req, res, next)
debug('Add middleware %j to phase %j', handlerName , fullName);
wrapper = function regularHandler(ctx, next) {
if (ctx.err) {
if (ctx.err || !matches(ctx.req)) {
next();
} else {
handler(ctx.req, ctx.res, storeErrorAndContinue(ctx, next));
Expand All @@ -169,6 +188,26 @@ proto.middleware = function(name, handler) {
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;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"loopback-phase": "^1.0.1",
"nodemailer": "~1.3.0",
"nodemailer-stub-transport": "~0.1.4",
"path-to-regexp": "^1.0.1",
"strong-remoting": "^2.4.0",
"uid2": "0.0.3",
"underscore": "~1.7.0",
Expand Down
80 changes: 78 additions & 2 deletions test/app.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
var async = require('async');
var path = require('path');

var http = require('http');
Expand Down Expand Up @@ -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) {
return function(req, res, next) {
steps.push(name);
next();
};
}

function pathSavingHandler() {
return function(req, res, next) {
steps.push(req.url);
next();
};
}
});

describe.onServer('.middlewareFromConfig', function() {
Expand Down Expand Up @@ -168,6 +215,30 @@ describe('app', function() {
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() {
Expand Down Expand Up @@ -600,13 +671,18 @@ describe('app', function() {
});
});

function executeMiddlewareHandlers(app, callback) {
function executeMiddlewareHandlers(app, urlPath, callback) {
var server = http.createServer(function(req, res) {
app.handle(req, res, callback);
});

if (callback === undefined && typeof urlPath === 'function') {
callback = urlPath;
urlPath = '/test/url';
}

request(server)
.get('/test/url')
.get(urlPath)
.end(function(err) {
if (err) return callback(err);
});
Expand Down

0 comments on commit 1c1e64c

Please sign in to comment.