UNPKG

@balderdash/sails-edge

Version:

API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)

371 lines (295 loc) 11.3 kB
/** * Module dependencies */ var _ = require('lodash'), mergeDefaults = require('merge-defaults'), util = require('util'); // Parameter used for jsonp callback is constant, as far as // blueprints are concerned (for now.) var JSONP_CALLBACK_PARAM = 'callback'; /** * Utility methods used in built-in blueprint actions. * * @type {Object} */ var actionUtil = { /** * Given a Waterline query and an express request, populate * the appropriate/specified association attributes and * return it so it can be chained further ( i.e. so you can * .exec() it ) * * @param {Query} query [waterline query object] * @param {Request} req * @return {Query} */ populateRequest: function(query, req) { var DEFAULT_POPULATE_LIMIT = req._sails.config.blueprints.defaultLimit || 30; var _options = req.options; var aliasFilter = req.param('populate'); var shouldPopulate = _options.populate; // Convert the string representation of the filter list to an Array. We // need this to provide flexibility in the request param. This way both // list string representations are supported: // /model?populate=alias1,alias2,alias3 // /model?populate=[alias1,alias2,alias3] if (typeof aliasFilter === 'string') { aliasFilter = aliasFilter.replace(/\[|\]/g, ''); aliasFilter = (aliasFilter) ? aliasFilter.split(',') : []; } var associations = []; _.each(_options.associations, function(association) { // If an alias filter was provided, override the blueprint config. if (aliasFilter) { shouldPopulate = _.contains(aliasFilter, association.alias); } // Only populate associations if a population filter has been supplied // with the request or if `populate` is set within the blueprint config. // Population filters will override any value stored in the config. // // Additionally, allow an object to be specified, where the key is the // name of the association attribute, and value is true/false // (true to populate, false to not) if (shouldPopulate) { var populationLimit = _options['populate_' + association.alias + '_limit'] || _options.populate_limit || _options.limit || DEFAULT_POPULATE_LIMIT; associations.push({ alias: association.alias, limit: populationLimit }); } }); return actionUtil.populateQuery(query, associations, req._sails); }, /** * Given a Waterline query and Waterline model, populate the * appropriate/specified association attributes and return it * so it can be chained further ( i.e. so you can .exec() it ) * * @param {Query} query [waterline query object] * @param {Model} model [waterline model object] * @return {Query} */ populateModel: function(query, model) { return actionUtil.populateQuery(query, model.associations); }, /** * Given a Waterline query, populate the appropriate/specified * association attributes and return it so it can be chained * further ( i.e. so you can .exec() it ) * * @param {Query} query [waterline query object] * @param {Array} associations [array of objects with an alias * and (optional) limit key] * @return {Query} */ populateQuery: function(query, associations, sails) { var DEFAULT_POPULATE_LIMIT = (sails && sails.config.blueprints.defaultLimit) || 30; return _.reduce(associations, function(query, association) { return query.populate(association.alias, { limit: association.limit || DEFAULT_POPULATE_LIMIT }); }, query); }, /** * Subscribe deep (associations) * * @param {[type]} associations [description] * @param {[type]} record [description] * @return {[type]} [description] */ subscribeDeep: function ( req, record ) { _.each(req.options.associations, function (assoc) { // Look up identity of associated model var ident = assoc[assoc.type]; var AssociatedModel = req._sails.models[ident]; if (req.options.autoWatch) { AssociatedModel.watch(req); } // Subscribe to each associated model instance in a collection if (assoc.type === 'collection') { _.each(record[assoc.alias], function (associatedInstance) { AssociatedModel.subscribe(req, associatedInstance); }); } // If there is an associated to-one model instance, subscribe to it else if (assoc.type === 'model' && record[assoc.alias]) { AssociatedModel.subscribe(req, record[assoc.alias]); } }); }, /** * Parse primary key value for use in a Waterline criteria * (e.g. for `find`, `update`, or `destroy`) * * @param {Request} req * @return {Integer|String} */ parsePk: function ( req ) { var pk = req.options.id || (req.options.where && req.options.where.id) || req.param('id'); // TODO: make this smarter... // (e.g. look for actual primary key of model and look for it // in the absence of `id`.) // See coercePK for reference (although be aware it is not currently in use) // exclude criteria on id field pk = _.isPlainObject(pk) ? undefined : pk; return pk; }, /** * Parse primary key value from parameters. * Throw an error if it cannot be retrieved. * * @param {Request} req * @return {Integer|String} */ requirePk: function (req) { var pk = module.exports.parsePk(req); // Validate the required `id` parameter if ( !pk ) { var err = new Error( 'No `id` parameter provided.'+ '(Note: even if the model\'s primary key is not named `id`- '+ '`id` should be used as the name of the parameter- it will be '+ 'mapped to the proper primary key name)' ); err.status = 400; throw err; } return pk; }, /** * Parse `criteria` for a Waterline `find` or `update` from all * request parameters. * * @param {Request} req * @return {Object} the WHERE criteria object */ parseCriteria: function ( req ) { // Allow customizable blacklist for params NOT to include as criteria. req.options.criteria = req.options.criteria || {}; req.options.criteria.blacklist = req.options.criteria.blacklist || ['limit', 'skip', 'sort', 'populate']; // Validate blacklist to provide a more helpful error msg. var blacklist = req.options.criteria && req.options.criteria.blacklist; if (blacklist && !_.isArray(blacklist)) { throw new Error('Invalid `req.options.criteria.blacklist`. Should be an array of strings (parameter names.)'); } // Look for explicitly specified `where` parameter. var where = req.params.all().where; // If `where` parameter is a string, try to interpret it as JSON if (_.isString(where)) { where = tryToParseJSON(where); } // If `where` has not been specified, but other unbound parameter variables // **ARE** specified, build the `where` option using them. if (!where) { // Prune params which aren't fit to be used as `where` criteria // to build a proper where query where = req.params.all(); // Omit built-in runtime config (like query modifiers) where = _.omit(where, blacklist || ['limit', 'skip', 'sort']); // Omit any params w/ undefined values where = _.omit(where, function (p){ if (_.isUndefined(p)) return true; }); // Omit jsonp callback param (but only if jsonp is enabled) var jsonpOpts = req.options.jsonp && !req.isSocket; jsonpOpts = _.isObject(jsonpOpts) ? jsonpOpts : { callback: JSONP_CALLBACK_PARAM }; if (jsonpOpts) { where = _.omit(where, [jsonpOpts.callback]); } } // Merge w/ req.options.where and return where = _.merge({}, req.options.where || {}, where) || undefined; return where; }, /** * Parse `values` for a Waterline `create` or `update` from all * request parameters. * * @param {Request} req * @return {Object} */ parseValues: function (req) { // Allow customizable blacklist for params NOT to include as values. req.options.values = req.options.values || {}; req.options.values.blacklist = req.options.values.blacklist; // Validate blacklist to provide a more helpful error msg. var blacklist = req.options.values.blacklist; if (blacklist && !_.isArray(blacklist)) { throw new Error('Invalid `req.options.values.blacklist`. Should be an array of strings (parameter names.)'); } // Merge params into req.options.values, omitting the blacklist. var values = mergeDefaults(req.params.all(), _.omit(req.options.values, 'blacklist')); // Omit values that are in the blacklist (like query modifiers) values = _.omit(values, blacklist || []); // Omit any values w/ undefined values values = _.omit(values, function (p){ if (_.isUndefined(p)) return true; }); // Omit jsonp callback param (but only if jsonp is enabled) var jsonpOpts = req.options.jsonp && !req.isSocket; jsonpOpts = _.isObject(jsonpOpts) ? jsonpOpts : { callback: JSONP_CALLBACK_PARAM }; if (jsonpOpts) { values = _.omit(values, [jsonpOpts.callback]); } return values; }, /** * Determine the model class to use w/ this blueprint action. * @param {Request} req * @return {WLCollection} */ parseModel: function (req) { // Ensure a model can be deduced from the request options. var model = req.options.model || req.options.controller; if (!model) throw new Error(util.format('No "model" specified in route options.')); var Model = req._sails.models[model]; if ( !Model ) throw new Error(util.format('Invalid route option, "model".\nI don\'t know about any models named: `%s`',model)); return Model; }, /** * @param {Request} req */ parseSort: function (req) { var sort = req.param('sort') || req.options.sort; if (typeof sort == 'undefined') {return undefined;} if (typeof sort == 'string') { try { sort = JSON.parse(sort); } catch(e) {} } return sort; }, /** * @param {Request} req */ parseLimit: function (req) { var DEFAULT_LIMIT = req._sails.config.blueprints.defaultLimit || 30; var limit = req.param('limit') || (typeof req.options.limit !== 'undefined' ? req.options.limit : DEFAULT_LIMIT); if (limit) { limit = +limit; } return limit; }, /** * @param {Request} req */ parseSkip: function (req) { var DEFAULT_SKIP = 0; var skip = req.param('skip') || (typeof req.options.skip !== 'undefined' ? req.options.skip : DEFAULT_SKIP); if (skip) { skip = +skip; } return skip; } }; // TODO: // // Replace the following helper with the version in sails.util: // Attempt to parse JSON // If the parse fails, return the error object // If JSON is falsey, return null // (this is so that it will be ignored if not specified) function tryToParseJSON (json) { if (!_.isString(json)) return null; try { return JSON.parse(json); } catch (e) { return e; } } module.exports = actionUtil;