diff --git a/Readme.md b/Readme.md index ca673e5..1334429 100644 --- a/Readme.md +++ b/Readme.md @@ -157,6 +157,67 @@ Resources have the concept of "auto-loading" associated data. For example we can app.resource('users', actions, { format: 'json' }); +## Middleware + + This version supports attaching [route middleware](http://expressjs.com/2x/guide.html#route-middleware) to actions. + Every action can have its own different middleware attached (or no middleware at all). + The middleware can be specified in one of two ways: by passing a `middleware` option to `app.resource()` or by attaching it directly to the actions. + +### middleware option + + With the `middleware` option, it's possible to specify the middleware in a single place: + + var express = require('express') + , Resource = require('express-resource') + , app = express.createServer(); + + var forumsMiddleware = { + index: authMiddleware, + new: [authMiddleware, adminMiddleware], + create: [authMiddleware, adminMiddleware], + show: authMiddleware, + edit: [authMiddleware, adminMiddleware], + update: [authMiddleware, adminMiddleware], + destroy: [authMiddleware, adminMiddleware] + }; + app.resource('forums', require('./forum'), {middleware: forumsMiddleware}); + + It's also possible to specify a special "glob" key, that matches every action, and is considered when no specific middleware is specified: + + var forumsMiddleware = { + '*': authMiddleware, + new: [authMiddleware, adminMiddleware], + create: [authMiddleware, adminMiddleware], + edit: [authMiddleware, adminMiddleware], + update: [authMiddleware, adminMiddleware], + destroy: [authMiddleware, adminMiddleware] + }; + app.resource('forums', require('./forum'), {middleware: forumsMiddleware}); + +### inline middleware + + It's also possible to specify the route middleware directly with the actions. There are two styles, with a JavaScript object or with an array: + + exports.index = { + middleware: authMiddleware, + fn: function(req, res){ + res.send('forum index'); + } + }; + + exports.new = [ authMiddleware, adminMiddleware, function(req, res){ + res.send('new forum'); + } ]; + + exports.create = { + middleware: [ authMiddleware, adminMiddleware ], + fn: function(req, res){ + res.send('create forum'); + } + }; + + ... + ## Running Tests First make sure you have the submodules: diff --git a/index.js b/index.js index 6f58147..d6f3e73 100644 --- a/index.js +++ b/index.js @@ -27,7 +27,22 @@ var orderedActions = [ ,'update' // PUT /:id ,'destroy' // DEL /:id ]; - + +var defaultMiddleware = { + 'index': null + ,'new': null + ,'create': null + ,'show': null + ,'edit': null + ,'update': null + ,'destroy': null + ,'*': null +}; + +function isArray(obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; +} + /** * Initialize a new `Resource` with the given `name` and `actions`. * @@ -37,9 +52,11 @@ var orderedActions = [ * @api private */ -var Resource = module.exports = function Resource(name, actions, app) { +var Resource = module.exports = function Resource(name, actions, app, opts) { this.name = name; this.app = app; + this.options = opts || {}; + this.middleware = this.options.middleware || defaultMiddleware; this.routes = {}; actions = actions || {}; this.base = actions.base || '/'; @@ -115,11 +132,35 @@ Resource.prototype.__defineGetter__('defaultId', function(){ * @api public */ -Resource.prototype.map = function(method, path, fn){ +Resource.prototype.map = function(method, path, fnmap){ var self = this , orig = path; if (method instanceof Resource) return this.add(method); + + var middleware, fn; + if ('function' == typeof fnmap) { + middleware = []; + fn = fnmap; + } else if (isArray(fnmap) && (fnmap.length > 1) && ('0' in Object(fnmap))) { + middleware = fnmap.slice(0, fnmap.length-1); + fn = fnmap[fnmap.length-1]; + } else if (('object' == typeof fnmap) && fnmap.hasOwnProperty('fn')) { + middleware = fnmap.middleware + fn = fnmap.fn; + } else { + middleware = []; + fn = fnmap; + } + + if (isArray(fn) && (fn.length > 1) && ('0' in Object(fn)) && (middleware.length == 0)) { + middleware = middleware.concat(fn.slice(0, fn.length-1)); + fn = fn[fn.length-1]; + } else if (('object' == typeof fn) && fn.hasOwnProperty('fn') && fn.hasOwnProperty('middleware')) { + middleware = middleware.concat(fn.middleware); + fn = fn.fn; + } + if ('function' == typeof path) fn = path, path = ''; if ('object' == typeof path) fn = path, path = ''; if ('/' == path[0]) path = path.substr(1); @@ -132,16 +173,21 @@ Resource.prototype.map = function(method, path, fn){ route += path; route += '.:format?'; + if (middleware === undefined) + middleware = []; + // register the route so we may later remove it (this.routes[method] = this.routes[method] || {})[route] = { method: method , path: route , orig: orig + , middleware: middleware , fn: fn + , fnmap: fnmap }; // apply the route - this.app[method](route, function(req, res, next){ + this.app[method](route, middleware, function(req, res, next){ req.format = req.params.format || req.format || self.format; if (req.format) res.contentType(req.format); if ('object' == typeof fn) { @@ -186,7 +232,7 @@ Resource.prototype.add = function(resource){ route = routes[key]; delete routes[key]; app[method](key).remove(); - resource.map(route.method, route.orig, route.fn); + resource.map(route.method, route.orig, route.fnmap); } } @@ -202,27 +248,29 @@ Resource.prototype.add = function(resource){ */ Resource.prototype.mapDefaultAction = function(key, fn){ + var middleware = this.middleware[key] || this.middleware['*'] || []; + var fnmap = {'fn': fn, 'middleware': middleware}; switch (key) { case 'index': - this.get('/', fn); + this.get('/', fnmap); break; case 'new': - this.get('/new', fn); + this.get('/new', fnmap); break; case 'create': - this.post('/', fn); + this.post('/', fnmap); break; case 'show': - this.get(fn); + this.get(fnmap); break; case 'edit': - this.get('edit', fn); + this.get('edit', fnmap); break; case 'update': - this.put(fn); + this.put(fnmap); break; case 'destroy': - this.del(fn); + this.del(fnmap); break; } }; @@ -235,6 +283,7 @@ express.router.methods.concat(['del', 'all']).forEach(function(method){ Resource.prototype[method] = function(path, fn){ if ('function' == typeof path || 'object' == typeof path) fn = path, path = ''; + var middleware = this.middleware[method] || []; this.map(method, path, fn); return this; } @@ -255,8 +304,8 @@ express.HTTPSServer.prototype.resource = function(name, actions, opts){ if ('object' == typeof name) actions = name, name = null; if (options.id) actions.id = options.id; this.resources = this.resources || {}; - if (!actions) return this.resources[name] || new Resource(name, null, this); + if (!actions) return this.resources[name] || new Resource(name, null, this, opts); for (var key in opts) options[key] = opts[key]; - var res = this.resources[name] = new Resource(name, actions, this); + var res = this.resources[name] = new Resource(name, actions, this, opts); return res; }; diff --git a/test/resource.middleware.test.js b/test/resource.middleware.test.js new file mode 100644 index 0000000..c9bc1f7 --- /dev/null +++ b/test/resource.middleware.test.js @@ -0,0 +1,233 @@ + +/** + * Module dependencies. + */ +var assert = require('assert') + , express = require('express') + , should = require('should') + , Resource = require('../'); + +module.exports = { + 'test app.resource() with middleware': function(){ + var app = express.createServer(); + + var authMiddleware = function (req, res, next) { + if( req.auth ){ + next() + } else { + res.send('must auth') + } + }; + var setupMiddleware = function (req, res, next) { + req.values = []; + next() + }; + var append1Middleware = function (req, res, next) { + req.values.push(1); + next() + }; + var append2Middleware = function (req, res, next) { + req.values.push(2); + next() + }; + var finalMiddleware = function (req, res, next) { + res.json(req.values); + }; + var simpleMiddlewareObj = { + index: authMiddleware, + create: [setupMiddleware, append1Middleware, append2Middleware, finalMiddleware] + }; + + var ret = app.resource('forums', require('./fixtures/forum'), {middleware: simpleMiddlewareObj}); + ret.should.be.an.instanceof(Resource); + + assert.response(app, + { url: '/forums' }, + { body: 'must auth' }); + + assert.response(app, + { url: '/forums/new' }, + { body: 'new forum' }); + + assert.response(app, + { url: '/forums', method: 'POST' }, + { body: '[1,2]' }); + + assert.response(app, + { url: '/forums/5' }, + { body: 'show forum 5' }); + + assert.response(app, + { url: '/forums/5/edit' }, + { body: 'edit forum 5' }); + + assert.response(app, + { url: '/forums/5', method: 'PUT' }, + { body: 'update forum 5' }); + + assert.response(app, + { url: '/forums/5', method: 'DELETE' }, + { body: 'destroy forum 5' }); + }, + + + 'test app.resource() with global (*) middleware': function(){ + var app = express.createServer(); + + var authMiddleware = function (req, res, next) { + if( req.auth ){ + next() + } else { + res.send('must auth') + } + }; + var simpleMiddlewareObj = { + '*': authMiddleware, + }; + + var ret = app.resource('forums', require('./fixtures/forum'), {middleware: simpleMiddlewareObj}); + ret.should.be.an.instanceof(Resource); + + assert.response(app, + { url: '/forums' }, + { body: 'must auth' }); + + assert.response(app, + { url: '/forums/new' }, + { body: 'must auth' }); + + assert.response(app, + { url: '/forums', method: 'POST' }, + { body: 'must auth' }); + + assert.response(app, + { url: '/forums/5' }, + { body: 'must auth' }); + + assert.response(app, + { url: '/forums/5/edit' }, + { body: 'must auth' }); + + assert.response(app, + { url: '/forums/5', method: 'PUT' }, + { body: 'must auth' }); + + assert.response(app, + { url: '/forums/5', method: 'DELETE' }, + { body: 'must auth' }); + }, + + + 'test actions with inline middleware': function(){ + var app = express.createServer(); + + var authMiddleware = function (req, res, next) { + if( req.auth ){ + next() + } else { + res.send('must auth') + } + }; + var setupMiddleware = function (req, res, next) { + req.values = []; + next() + }; + var append1Middleware = function (req, res, next) { + req.values.push(1); + next() + }; + var append2Middleware = function (req, res, next) { + req.values.push(2); + next() + }; + var finalMiddleware = function (req, res, next) { + res.json(req.values); + }; + + var actions = { + index: { + middleware: authMiddleware, + fn: function(req, res){ + res.end('index'); + } + }, + 'new': { + middleware: [ setupMiddleware, append1Middleware, append2Middleware, finalMiddleware], + fn: function(req, res){ + res.end('new'); + } + }, + 'create': [ setupMiddleware, append1Middleware, append2Middleware, finalMiddleware, function(req, res){ + res.end('create'); + } ], + 'show': function(req, res){ + res.end('show'); + } + }; + + var ret = app.resource('forums', actions); + ret.should.be.an.instanceof(Resource); + + assert.response(app, + { url: '/forums' }, + { body: 'must auth' }); + + assert.response(app, + { url: '/forums/new' }, + { body: '[1,2]' }); + + assert.response(app, + { url: '/forums', method: 'POST' }, + { body: '[1,2]' }); + }, + + + 'test deep nesting with middleware': function(){ + var app = express.createServer(); + + var authMiddleware = function (req, res, next) { + if( req.auth ){ + next() + } else { + res.send('must auth') + } + }; + var simpleMiddlewareObj = { + index: authMiddleware, + }; + + var user = app.resource('users', { index: function(req, res){ res.end('users'); } }); + var forum = app.resource('forums', require('./fixtures/forum'), {middleware: simpleMiddlewareObj}); + var thread = app.resource('threads', require('./fixtures/thread')); + + var ret = user.add(forum); + ret.should.equal(user); + + var ret = forum.add(thread); + ret.should.equal(forum); + + assert.response(app, + { url: '/forums/20' }, + { status: 404 }); + + assert.response(app, + { url: '/users' }, + { body: 'users' }); + + assert.response(app, + { url: '/users/5/forums' }, + { body: 'must auth' }); + + assert.response(app, + { url: '/users/5/forums/12' }, + { body: 'show forum 12' }); + + assert.response(app, + { url: '/users/5/forums/12/threads' }, + { body: 'thread index of forum 12' }); + + assert.response(app, + { url: '/users/5/forums/1/threads/50' }, + { body: 'show thread 50 of forum 1' }); + } +}; \ No newline at end of file