diff --git a/lib/app/Sails.js b/lib/app/Sails.js index 3cdc6237e..5627926af 100644 --- a/lib/app/Sails.js +++ b/lib/app/Sails.js @@ -36,6 +36,9 @@ function Sails() { // Keep a hash of loaded actions this._actions = {}; + // Keep a hash of loaded action middleware + this._actionMiddleware = {}; + // Build a Router instance (which will attach itself to the sails object) __Router(this); @@ -68,6 +71,7 @@ function Sails() { this['delete'] = _.bind(this['delete'], this); this.getActions = _.bind(this.getActions, this); this.registerAction = _.bind(this.registerAction, this); + this.registerActionMiddleware = _.bind(this.registerActionMiddleware, this); this.reloadModules = _.bind(this.reloadModules, this); this.controller = this.initializeController(this); } @@ -100,6 +104,7 @@ Sails.prototype.reloadModules = require('./reload-modules'); Sails.prototype.getActions = require('./get-actions'); Sails.prototype.registerAction = require('./register-action'); +Sails.prototype.registerActionMiddleware = require('./register-action-middleware'); diff --git a/lib/app/private/controller/bind-route.js b/lib/app/private/controller/bind-route.js index 638553916..616bcda4d 100644 --- a/lib/app/private/controller/bind-route.js +++ b/lib/app/private/controller/bind-route.js @@ -51,22 +51,7 @@ module.exports = function interpretRouteSyntax (route) { options = _.extend({}, options, _.omit(target, 'action')); } - // Attempt to find an action with that identity. - var action = sails._actions[actionIdentity]; - // If there is one, bind the given route address to the action. - if (action) { - // Make sure req.options.action is set to the identity of the action we're binding, - // for cases where the { action: 'foo.bar' } syntax wasn't used. - options.action = actionIdentity; - sails.router.bind(path, action, verb, options); - } - // Otherwise, log a message about the unknown action. - else { - sails.log.warn( - 'Ignored attempt to bind route (' + path + ') to unknown target ::', - target - ); - } + sails.router.bind(path, actionIdentity, verb, options); }; diff --git a/lib/app/register-action-middleware.js b/lib/app/register-action-middleware.js new file mode 100644 index 000000000..9699e6ed5 --- /dev/null +++ b/lib/app/register-action-middleware.js @@ -0,0 +1,34 @@ +/** + * Sails.prototype.registerActionMiddleware() + * + * Register an action middleware with Sails. + * + * Action middleware runs before the action or actions + * specified by the middleware key. + * + * @param {fn} middleware The function to register + * @param {string} actions The identity of the action or actions that this middleware should apply to. + * Use * at the end for a wildcard; e.g. `user.*` will apply to any actions + * whose identity begins with `user.`. + * + * + * @api public + */ +module.exports = function registerAction(middleware, actions) { + + // TODO -- update machine-as-action with a response type that calls `next`, + // so machine defs can be registered as middleware? + if (_.isFunction(middleware)) { + throw new Error('Attempted to register middleware for `' + actions + '` but the provided middleware was not a function (it was a ' + typeof(middleware) + ').'); + } + + // Get or create the array for this middleware key. + var middlewareForKey = this._actionMiddleware[actions] || []; + + // Add this middleware to the array. + middlewareForKey.push(middleware); + + // Assign the array back to the dictionary. + this._actionMiddleware[actions] = middlewareForKey; + +}; diff --git a/lib/router/bind.js b/lib/router/bind.js index c5d3b2e84..4197a85a5 100644 --- a/lib/router/bind.js +++ b/lib/router/bind.js @@ -4,7 +4,7 @@ var _ = require('lodash'); var sailsUtil = require('sails-util'); - +var async = require('async'); /** @@ -52,21 +52,12 @@ function bind( /* path, target, verb, options */ ) { else if (_.isString(target) && target.match(/^(https?:|\/)/)) { bindRedirect.apply(this, [path, target, verb, options]); } + // Otherwise if the target is a string, it must be an action. else if (_.isString(target)) { - // Get the action identity by replacing slashes with dots, removing `Controller` and lower-casing. - var actionIdentity = target.replace(/\//g, '.').replace(/Controller\./,'.').toLowerCase(); - // If there's no loaded action with that identity, log a warning and continue. - if (!sails._actions[actionIdentity]) { - sails.log.warn('Ignored attempt to bind route (' + path + ') to unknown target ::', target); - return; - } - // If the string has slashes in it, log a warning. - if (target.indexOf('/') > -1) { - sails.log.warn('Using string route target syntax with nested legacy controllers is deprecated. '+ - 'Binding `' + target + '` to `' + path + '` for now, but you should really change this to { action: \'' + actionIdentity + '\' }.'); - } - sails.controller.bindRoute({target: {action: actionIdentity}, path: path, verb: verb, options: options}); + bindAction.apply(this, [path, target, verb, options]); } + // If the target is a dictionary with `controller` or `action` properties, + // let the sails controller handle it. else if (_.isPlainObject(target) && (target.controller || target.action)) { sails.controller.bindRoute({path: path, target: target, verb: verb, options: options}); } @@ -125,6 +116,73 @@ function bindRedirect(path, redirectTo, verb, options) { }, verb, options]); } +/** + * Bind a previously-loaded action to a URL. + * (which should be a URL or redirectable path.) + * + * @api private + */ +function bindAction(path, target, verb, options) { + + var sails = this.sails; + + // Normalize the action identity by replacing slashes with dots, removing `Controller` and lower-casing. + var actionIdentity = target.replace(/\//g, '.').replace(/Controller\./,'.').toLowerCase(); + + // If there's no loaded action with that identity, log a warning and continue. + if (!sails._actions[actionIdentity]) { + sails.log.warn('Ignored attempt to bind route (' + path + ') to unknown target ::', target); + return; + } + + // If the string has slashes in it, log a warning. + if (target.indexOf('/') > -1) { + sails.log.warn('Using string route target syntax with nested legacy controllers is deprecated. '+ + 'Binding `' + target + '` to `' + path + '` for now, but you should really change this to { action: \'' + actionIdentity + '\' }.'); + } + + // Add "action" property to the route options. + _.extend(options || {}, {action: actionIdentity}); + + // Bind a function to the route which calls the specified action. + bind.apply(this,[path, function(req, res, next) { + // Loop through all of the registered action middleware, and find + // any that should apply to the action with the given identity. + var middlewareToRun = _.reduce(sails._actionMiddleware, function(memo, middlewareList, key) { + if ( + // If the registered action middleware key is '*'... + key === '*' || + // Or ends in '.*' so that the current action identity matches the wildcard... + (_.last(key) === '*' && (actionIdentity.indexOf(key.slice(0,-1)) === 0)) || + // Or matches the current action identity exactly... + (actionIdentity === key) + ) { + // Then add the action middleware from this key to the list of middleware + // to run before the action. + memo = memo.concat(middlewareList); + } + return memo; + }, []); + + // Run any action middleware we found first. + async.eachSeries(middlewareToRun, function(middleware, next) { + // The `next` argument will just run the next middleware + // when called. + // TODO -- support sending 'route' or errors through `next`? + middleware(req, res, function() { + return next(); + }); + }, + // Finally, after any action middleware has run, + // run the action itself. + function afterRunningActionMiddleware() { + return sails._actions[actionIdentity](req, res, function() { + throw new Error('`next` called in action function!'); + }); + }); + }, verb, options]); +} + /** * Recursively bind an array of targets in order