@balderdash/sails-edge
Version:
API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)
452 lines (356 loc) • 17.2 kB
JavaScript
/**
* Module dependencies
*/
var _ = require('lodash')
, util = require('util')
, pluralize = require('pluralize')
, BlueprintController = {
create : require('./actions/create')
, find : require('./actions/find')
, findone : require('./actions/findOne')
, update : require('./actions/update')
, destroy : require('./actions/destroy')
, populate: require('./actions/populate')
, add : require('./actions/add')
, remove : require('./actions/remove')
}
, STRINGFILE = require('sails-stringfile');
/**
* Blueprints (Core Hook)
*
* Stability: 1 - Experimental
* (see http://nodejs.org/api/documentation.html#documentation_stability_index)
*/
module.exports = function(sails) {
/**
* Private dependencies.
* (need access to `sails`)
*/
var onRoute = require('./onRoute')(sails);
var hook;
/**
* Expose blueprint hook definition
*/
return {
/**
* Default configuration to merge w/ top-level `sails.config`
* @type {Object}
*/
defaults: {
// These config options are mixed into the route options (req.options)
// and made accessible from the blueprint actions. Most of them currently
// relate to the shadow (i.e. implicit) routes which are created, and are
// interpreted by this hook.
blueprints: {
// Blueprint/Shadow-Routes Enabled
//
// e.g. '/frog/jump': 'FrogController.jump'
actions: true,
// e.g. '/frog': 'FrogController.index'
index: true,
// e.g. '/frog/find/:id?': 'FrogController.find'
shortcuts: true,
// e.g. 'get /frog/:id?': 'FrogController.find'
rest: true,
// Blueprint/Shadow-Route Modifiers
//
// e.g. 'get /api/v2/frog/:id?': 'FrogController.find'
prefix: '',
// Blueprint/REST-Route Modifiers
// Will work only for REST and will extend `prefix` option
//
// e.g. 'get /api/v2/frog/:id?': 'FrogController.find'
restPrefix: '',
// e.g. 'get /frogs': 'FrogController.find'
pluralize: false,
// Configuration of the blueprint actions themselves:
// Whether to populate all association attributes in the `find`
// blueprint action.
populate: true,
// Whether to run `Model.watch()` in the `find` blueprint action.
autoWatch: true,
// (TODO: generated comments for jsonp configuration needs to be updated w/ new options)
// (TODO: need to mention new `req.options` stuff in generated comments)
// // Enable JSONP callbacks.
// jsonp: false
// Deprecated:
// Skip blueprint if `:id?` is NOT an integer.
// expectIntegerId: false,
}
},
/**
* Initialize is fired first thing when the hook is loaded.
*
* @param {Function} cb
*/
initialize: function (cb) {
// Provide hook context to closures
hook = this;
////////////////////////////////////////////////////////////////////////
// TODO:
// Provide deprecation notice letting 0.9 users know that they need to
// move their blueprint configuration to `config.blueprints` instead of
// `config.controllers.blueprints`. Similarly, need a message to let
// folks know to move their controller-specific blueprint config from
// `SomeController._config.blueprints` to `SomeController._config`.
// In both cases, we can "fix" the configuration in-memory, avoiding
// allowing the app to "still work". This can be done the same way we're
// doing it for adapter config.
////////////////////////////////////////////////////////////////////////
// Register route syntax for binding blueprints directly.
sails.on('route:typeUnknown', onRoute);
// Set up listener to bind shadow routes when the time is right.
//
// Always wait until after router has bound static routes.
// If policies hook is enabled, also wait until policies are bound.
// If orm hook is enabled, also wait until models are known.
// If controllers hook is enabled, also wait until controllers are known.
var eventsToWaitFor = [];
eventsToWaitFor.push('router:after');
if (sails.hooks.policies) {
eventsToWaitFor.push('hook:policies:bound');
}
if (sails.hooks.orm) {
eventsToWaitFor.push('hook:orm:loaded');
}
if (sails.hooks.controllers) {
eventsToWaitFor.push('hook:controllers:loaded');
}
sails.after(eventsToWaitFor, hook.bindShadowRoutes);
// Load blueprint middleware and continue.
loadMiddleware(cb);
},
extendControllerMiddleware: function() {
_.each(sails.middleware.controllers, function (controller) {
_.defaults(controller, hook.middleware);
});
},
bindShadowRoutes: function() {
var logWarns = function(warns) {
sails.log.blank();
_.each(warns, function (warn) {
sails.log.warn(warn);
});
STRINGFILE.logMoreInfoLink(STRINGFILE.get('links.docs.config.blueprints'), sails.log.warn);
}
_.each(sails.middleware.controllers, function eachController (controller, controllerId) {
if ( !_.isObject(controller) || _.isArray(controller) ) return;
// Get globalId for use in errors/warnings
var globalId = sails.controllers[controllerId].globalId;
// Determine blueprint configuration for this controller
var config = _.merge({},
sails.config.blueprints,
controller._config || {});
// Validate blueprint config for this controller
if ( config.prefix ) {
if ( !_(config.prefix).isString() ) {
sails.after('lifted', function () {
logWarns([
util.format('Ignoring invalid blueprint prefix configured for controller `%s`.', globalId),
'`prefix` should be a string, e.g. "/api/v1".'
]);
});
return;
}
if ( !config.prefix.match(/^\//) ) {
var originalPrefix = config.prefix;
sails.after('lifted', function () {
logWarns([
util.format('Invalid blueprint prefix ("%s") configured for controller `%s` (should start with a `/`).', originalPrefix, globalId),
util.format('For now, assuming you meant: "%s".', config.prefix)
]);
});
config.prefix = '/' + config.prefix;
}
}
// Validate REST route blueprint config for this controller
if ( config.restPrefix ) {
if ( !_(config.restPrefix).isString() ) {
sails.after('lifted', function () {
logWarns([
util.format('Ignoring invalid blueprint rest prefix configured for controller `%s`.', globalId),
'`restPrefix` should be a string, e.g. "/api/v1".'
]);
});
return;
}
if ( !config.restPrefix.match(/^\//) ) {
var originalRestPrefix = config.restPrefix;
sails.after('lifted', function () {
logWarns([
util.format('Invalid blueprint restPrefix ("%s") configured for controller `%s` (should start with a `/`).', originalRestPrefix, globalId),
util.format('For now, assuming you meant: "%s".', config.restPrefix)
]);
});
config.restPrefix = '/' + config.restPrefix;
}
}
// Determine the names of the controller's user-defined actions
// IMPORTANT: Use `sails.controllers` instead of `sails.middleware.controllers`
// (since `sails.middleware.controllers` will have blueprints already mixed-in,
// and we want the explicit actions defined in the app)
var actions = Object.keys(sails.controllers[controllerId]);
// Determine base route
var baseRoute = config.prefix + '/' + controllerId;
// Determine base route for RESTful service
// Note that restPrefix will always start with /
var baseRestRoute = config.prefix + config.restPrefix + '/' + controllerId;
if (config.pluralize) {
baseRoute = pluralize(baseRoute);
baseRestRoute = pluralize(baseRestRoute);
}
// Build route options for blueprint
var routeOpts = config;
// Bind "actions" and "index" shadow routes for each action
_.each(actions, function eachActionID (actionId) {
var opts = _.merge({
action: actionId,
controller: controllerId
}, routeOpts);
// Bind a route based on the action name, if `actions` shadows enabled
if (config.actions) {
var actionRoute = baseRoute + '/' + actionId.toLowerCase() + '/:id?';
sails.log.silly('Binding action ('+actionId.toLowerCase()+') blueprint/shadow route for controller:',controllerId);
sails.router.bind(actionRoute, controller[actionId.toLowerCase()], null, opts);
}
// Bind base route to index action, if `index` shadows are not disabled
if (config.index !== false && actionId.match(/^index$/i)) {
sails.log.silly('Binding index blueprint/shadow route for controller:',controllerId);
sails.router.bind(baseRoute, controller.index, null, opts);
}
});
// Determine the model connected to this controller either by:
// -> explicit configuration
// -> on the controller
// -> on the routes config
// -> or implicitly by globalId
// -> or implicitly by controller id
var routeConfig = sails.router.explicitRoutes[controllerId] || {};
var modelFromGlobalId = sails.util.findWhere(sails.models, {globalId: globalId});
var modelId = config.model || routeConfig.model || (modelFromGlobalId && modelFromGlobalId.identity) || controllerId;
// If the orm hook is enabled, it has already been loaded by this time,
// so just double-check to see if the attached model exists in `sails.models`
// before trying to attach any CRUD blueprint actions to the controller.
if (sails.hooks.orm && sails.models && sails.models[modelId]) {
// If a model with matching identity exists,
// extend route options with the id of the model.
routeOpts.model = modelId;
var Model = sails.models[modelId];
// Bind convenience functions for readability below:
// Given an action id like "find" or "create", returns the appropriate
// blueprint action (or explicit controller action if the controller
// overrode the blueprint CRUD action.)
var _getAction = _.partial(_getMiddlewareForShadowRoute, controllerId);
// Returns a customized version of the route template as a string.
var _getRoute = _.partialRight(util.format, baseRoute);
var _getRestRoute = _getRoute;
if (config.restPrefix) {
// Returns a customized version of the route template as a string for REST
_getRestRoute = _.partialRight(util.format, baseRestRoute);
}
// Mix in the known associations for this model to the route options.
routeOpts = _.merge({ associations: _.cloneDeep(Model.associations) }, routeOpts);
// Binds a route to the specifed action using _getAction, and sets the action and controller
// options for req.options
var _bindRoute = function (path, action, options) {
options = options || routeOpts;
options = _.extend({}, options, {action: action, controller: controllerId});
sails.router.bind ( path, _getAction(action), null, options);
};
// Bind URL-bar "shortcuts"
// (NOTE: in a future release, these may be superceded by embedding actions in generated controllers
// and relying on action blueprints instead.)
if ( config.shortcuts ) {
sails.log.silly('Binding shortcut blueprint/shadow routes for model ', modelId, ' on controller:', controllerId);
_bindRoute(_getRoute('%s/find'), 'find');
_bindRoute(_getRoute('%s/find/:id'), 'findOne');
_bindRoute(_getRoute('%s/create'), 'create');
_bindRoute(_getRoute('%s/update/:id'), 'update');
_bindRoute(_getRoute('%s/destroy/:id?'), 'destroy');
// Bind add/remove "shortcuts" for each `collection` associations
_(Model.associations).where({type: 'collection'}).forEach(function (association) {
var alias = association.alias;
var _getAssocRoute = _.partialRight(util.format, baseRoute, alias);
var opts = _.merge({ alias: alias }, routeOpts);
sails.log.silly('Binding "shortcuts" to association blueprint `'+alias+'` for',controllerId);
_bindRoute( _getAssocRoute('%s/:parentid/%s/add/:id?'), 'add' , opts );
_bindRoute( _getAssocRoute('%s/:parentid/%s/remove/:id?'), 'remove', opts );
});
}
// Bind "rest" blueprint/shadow routes
if ( config.rest ) {
sails.log.silly('Binding RESTful blueprint/shadow routes for model+controller:',controllerId);
_bindRoute(_getRestRoute('get %s'), 'find');
_bindRoute(_getRestRoute('get %s/:id'), 'findOne');
_bindRoute(_getRestRoute('post %s'), 'create');
_bindRoute(_getRestRoute('put %s/:id'), 'update');
_bindRoute(_getRestRoute('post %s/:id'), 'update');
_bindRoute(_getRestRoute('delete %s/:id?'), 'destroy');
// Bind "rest" blueprint/shadow routes based on known associations in our model's schema
// Bind add/remove for each `collection` associations
_(Model.associations).where({type: 'collection'}).forEach(function (association) {
var alias = association.alias;
var _getAssocRoute = _.partialRight(util.format, baseRestRoute, alias);
var opts = _.merge({ alias: alias }, routeOpts);
sails.log.silly('Binding RESTful association blueprint `'+alias+'` for',controllerId);
_bindRoute( _getAssocRoute('post %s/:parentid/%s/:id?'), 'add', opts );
_bindRoute( _getAssocRoute('delete %s/:parentid/%s/:id?'), 'remove', opts );
});
// and populate for both `collection` and `model` associations
_(Model.associations).forEach(function (association) {
var alias = association.alias;
var _getAssocRoute = _.partialRight(util.format, baseRestRoute, alias);
var opts = _.merge({ alias: alias }, routeOpts);
sails.log.silly('Binding RESTful association blueprint `'+alias+'` for',controllerId);
_bindRoute( _getAssocRoute('get %s/:parentid/%s/:id?'), 'populate', opts );
});
}
}
});
/**
* Return the middleware function that should be bound for a shadow route
* pointing to the specified blueprintId. Will use the explicit controller
* action if it exists, otherwise the blueprint action.
*
* @param {String} controllerId
* @param {String} blueprintId [find, create, etc.]
* @return {Function} [middleware]
*/
function _getMiddlewareForShadowRoute (controllerId, blueprintId) {
// Allow custom actions defined in controller to override blueprint actions.
return sails.middleware.controllers[controllerId][blueprintId.toLowerCase()] || hook.middleware[blueprintId.toLowerCase()];
}
}
};
/**
* (Re)load middleware.
*
* First, built-in blueprint actions in core Sails will be loaded.
* Then, we'll attempt to load any custom blueprint definitions from
* the user app using moduleloader.
*
* @api private
*/
function loadMiddleware (cb) {
sails.log.verbose('Loading blueprint middleware...');
// Start off w/ the built-in blueprint actions (generic CRUD logic)
BlueprintController;
// Get custom blueprint definitions
sails.modules.loadBlueprints(function modulesLoaded (err, modules) {
if (err) return cb(err);
// Merge custom overrides from our app into the BlueprintController
// in Sails core.
_.extend(BlueprintController, modules);
// Add _middlewareType keys to the functions, for debugging
_.each(BlueprintController, function(fn, key) {
fn._middlewareType = 'BLUEPRINT: '+fn.name || key;
});
// Save reference to blueprints middleware in hook.
hook.middleware = BlueprintController;
// When our app's controllers are finished loading,
// merge the blueprint actions into each of them as defaults.
sails.once('middleware:registered', hook.extendControllerMiddleware);
return cb(err);
});
}
};