diff --git a/lib/server-app.js b/lib/server-app.js index ac8e09ea..b75835da 100644 --- a/lib/server-app.js +++ b/lib/server-app.js @@ -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 = {}; @@ -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) * @@ -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; }; @@ -112,6 +115,10 @@ 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)` @@ -119,11 +126,21 @@ proto.defineMiddlewarePhases = function(nameOrArray) { * * @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)'; @@ -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)); @@ -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)); @@ -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; diff --git a/package.json b/package.json index 2c928bc0..5bce6742 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/app.test.js b/test/app.test.js index a5e02add..073bf149 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -1,3 +1,4 @@ +var async = require('async'); var path = require('path'); 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) { 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() { @@ -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() { @@ -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); });