@balderdash/sails-edge
Version:
API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)
356 lines (288 loc) • 10.1 kB
JavaScript
/**
* Module dependencies.
*/
var _ = require('lodash');
var util = require('sails-util');
/**
* Expose `bind` method.
*/
module.exports = bind;
/**
* Bind new route(s)
*
* @param {String|RegExp} path
* @param {String|Object|Array|Function} target
* @param {String} verb (optional)
* @param {Object} options (optional)
*
* @this {SJSRouter}
* @return {SJSApp}
*
* @api private
*/
function bind( /* path, target, verb, options */ ) {
var sails = this.sails;
var args = sanitize.apply(this, Array.prototype.slice.call(arguments));
var path = args.path;
var target = args.target;
var verb = args.verb;
var options = args.options;
// Bind a list of multiple functions in order
if (util.isArray(target)) {
bindArray.apply(this, [path, target, verb, options]);
}
// Handle string redirects
// (to either public-facing URLs or internal routes)
else if (util.isString(target) && target.match(/^(https?:|\/)/)) {
bindRedirect.apply(this, [path, target, verb, options]);
}
// Bind a middleware function directly
else if (util.isFunction(target)) {
bindFunction.apply(this, [path, target, verb, options]);
}
// If target is an object with a `target`, pull out the rest
// of the keys as route options and then bind the target.
else if (util.isPlainObject(target) && target.target) {
var _target = _.cloneDeep(target.target);
delete target.target;
options = _.merge(options, target);
bind.apply(this, [path, _target, verb, options]);
}
else {
// If we make it here, the router doesn't know how to parse the target.
//
// This doesn't mean that it's necessarily invalid though--
// so we'll emit an event informing any listeners that an unrecognized route
// target was encountered. Then hooks can listen to this event and act
// accordingly. This makes it easier to add functionality to Sails.
sails.emit('route:typeUnknown', {
path: path,
target: target,
verb: verb,
options: options
});
// TODO: track emissions of "typeUnknown" to avoid logic errors that result in circular routes
// (part of the effort to make a more friendly environment for custom hook developers)
}
// Makes `.bind()` chainable (sort of)
return sails;
}
/**
* Requests will be redirected to the specified string
* (which should be a URL or redirectable path.)
*
* @api private
*/
function bindRedirect(path, redirectTo, verb, options) {
var sails = this.sails;
bind.apply(this,[path, function(req, res) {
sails.log.verbose('Redirecting request (`' + path + '`) to `' + redirectTo + '`...');
res.redirect(redirectTo);
}, verb, options]);
}
/**
* Recursively bind an array of targets in order
*
* TODO: Use a counter to prevent indefinite loops--
* only possible if a bad route is bound,
* but would still potentially be helpful.
*
* @api private
*/
function bindArray(path, target, verb, options) {
var self = this;
var sails = this.sails;
if (target.length === 0) {
sails.log.verbose('Ignoring empty array in `router.bind(' + path + ')`...');
} else {
// Bind each middleware fn
util.each(target, function(fn) {
bind.apply(self,[path, fn, verb, options]);
});
}
}
/**
* Attach middleware function to route.
*
* @api private
*/
function bindFunction(path, fn, verb, options) {
var sails = this.sails;
// Regex to check if a URL is an asset (something with a file extension)
var skipAssetsRegex = /^[^?]*\/[^?/]+\.[^?/]+(\?.*)?$/;
// Make sure (optional) options is a valid plain object ({})
options = util.isPlainObject(options) ? _.cloneDeep(options) : {};
var _middlewareType = options._middlewareType || fn._middlewareType || (fn.name && ('FUNCTION: ' + fn.name));
sails.log.silly('Binding route :: ', verb || '', path, _middlewareType?('('+_middlewareType+')'):'');
/**
* `router:route`
*
* Create a closure that emits the `router:route` event each time the route is hit
* before actually triggering the target function.
*
* NOTE: Modifications to route path parameters (i.e. `req.params`) or to `req.options`
* must be made here, since their values can change not only on a per-request, but
* also a per-route basis.
*/
var enhancedFn = function routeTargetFnWrapper(req, res, next) {
// Set req.options, using `options` to supply default values
req.options = _.merge(options || {}, req.options || {});
// This event can be tapped into to take control of logic
// that should be run before each middleware function
sails.emit('router:route', {
req: req,
res: res,
next: next,
options: options,
fn: fn
});
// INVESTIGATE: (this would allow `req.params` aka route params to be changed in policies)
// Apply any `req.params` that were added previously
// in user code.
// _.defaults(req.params, req._modifiedRouteParams);
// Trigger original middleware function
fn(req, res, function(err) {
// INVESTIGATE: (this would allow `req.params` aka route params to be changed in policies)
// Hold on to the current state of `req.params` after
// user code was run.
req._modifiedRouteParams = _.cloneDeep(req.params);
// Continue onwards
next(err);
});
};
/**
* Wrap a regex route in a helper function that pulls out regex params
*
* Example: for route: 'r|/\\d+/(.*)/(.*)$|foo,bar', the two parenthesized
* groups would be pulled out as req.params[0] and req.params[1] by Express;
* the regexRouteWrapper would then map them to req.params['foo'] and req.params['bar']
*
* @param {array} params List of params to apply to the req.params object
* @return {Function} A middleware function
*/
var regexRouteWrapper = function(params) {
return function(req, res, next) {
// Apply the regex route params
params.forEach(function(param, index) {
req.params[param] = req.params[index];
});
// Call enhancedFn
enhancedFn(req, res, next);
};
};
/**
* Wrap a route in a helper function that first checks whether the URL matches
* any of a set of regexes, and if so, skips the defined handler.
*
* @param {array} regexes Array of regexes to match the URL against
* @param {Function} fn Middleware function to run if URL does NOT match regexes
* @return {Function} A middleware function
*/
var skipRegexesWrapper = function(regexes, fn) {
// Remove anything that's not a regex
regexes = sails.util.compact(regexes.map(function(regex) {
if (regex instanceof RegExp) {
return regex;
}
sails.log.warn('Invalid regex "' + regex + "' supplied to skipRegexesWrapper; ignoring.");
return undefined;
}));
return function(req, res, next) {
// Check for matches
for (var i = 0; i < regexes.length; i++) {
if (req.url.match(regexes[i])) {
// If we find one, bail out
return next();
}
}
// Otherwise continue with the handler
return fn(req, res, next);
};
};
// If verb is not specified, default to CRUD methods.
// You can still explicitly route to "all /path" if you want ALLLLlllll the things.
var targetVerb = verb || ["get", "put", "post", "delete", "patch"];
// Function to actually bind
var targetFn;
// Regex to check if the route is...a regex.
var regExRoute = /^r\|(.*)\|(.*)$/;
// Perform the check
var matches = path.match(regExRoute);
// If it *is* a regex, create a RegExp object that Express can bind,
// pull out the params, and wrap the handler in regexRouteWrapper
if (matches) {
path = new RegExp(matches[1]);
var params = matches[2].split(',');
targetFn = regexRouteWrapper(params);
}
// Otherwise just bind enhancedFn
else {
targetFn = enhancedFn;
}
// If options.skipRegex is specified, make sure it's an array
if (options.skipRegex) {
if (!Array.isArray(options.skipRegex)) {
options.skipRegex = [options.skipRegex];
}
}
// Otherwise just make it an empty array
else {
options.skipRegex = [];
}
// If "skipAssets" option is true, add the skipAssets regex
// to the options.skipRegex array
if (options.skipAssets) {
options.skipRegex.push(skipAssetsRegex);
}
// If we have anything in the options.skipRegex array, wrap
// the target function again.
if (options.skipRegex.length) {
targetFn = skipRegexesWrapper(options.skipRegex, targetFn);
}
// Ensure targetVerb is an array
if (!Array.isArray(targetVerb)) {targetVerb = [targetVerb];}
// Loop through the verbs we want to bind
targetVerb.forEach(function(verb) {
verb = verb.toLowerCase();
// Bind the function to the private router
sails.router._privateRouter[verb](path, targetFn);
// Emit an event to make hooks aware that a route was bound
// This allows hooks to handle routes directly if they want to-
// e.g. with Express, the handler for this event looks like:
// sails.hooks.http.app[verb || 'all'](path, target);
sails.emit('router:bind', {
path: path,
target: util.clone(targetFn),
verb: verb,
originalFn: fn
});
});
}
/**
* Sanitize the arguments to `sails.router.bind()`
*
* @returns {Object} sanitized arguments
* @api private
*/
function sanitize(path, target, verb, options) {
options = options || {};
// If trying to bind '*', that's probably not what was intended, so fix it up
path = path === '*' ? '/*' : path;
// If route has an HTTP verb (e.g. `get /foo/bar`, `put /bar/foo`, etc.) parse it out,
var detectedVerb = util.detectVerb(path);
// then prune it from the path
path = detectedVerb.original;
// Keep track of parsed verb so we know if it was specified later
options.detectedVerb = detectedVerb;
// If a verb override was not specified,
// use the detected verb from the string route
if (!verb) {
verb = detectedVerb.verb;
}
return {
path: path,
target: target,
verb: verb,
options: options
};
}