sails
Version:
API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)
570 lines (443 loc) • 28.1 kB
JavaScript
/**
* Module dependencies
*/
var util = require('util');
var _ = require('@sailshq/lodash');
var flaverr = require('flaverr');
/**
* parseBlueprintOptions()
*
* Parse information from the request for use in a blueprint action.
*
* > This is just the default implementation -- it can be overridden.
* > See http://sailsjs.com/config/blueprints for more information.
*
* | Term | Meaning
* |:----------------------|:----------------------------------------------------------------------------------------|
* | route option | e.g. `model`, `alias`, `parseBlueprintOptions`, `action`, etc. (+ non-standard options)
* | query key | e.g. `criteria`, `newRecord`, `valuesToSet`, `meta`, `using`, etc. (fully standardized)
* | blueprint option | e.g. `criteria`, `newRecord`, `valuesToSet`, `meta`, `using`, etc. (fully standardized)
*
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* @param {Request} req
*
* @returns {Dictionary}
* The final dict of "blueprint options"; special settings that
* tell a blueprint action what to do when it runs. (They are
* roughly equivalent to Waterline query keys.)
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*/
module.exports = function parseBlueprintOptions(req) {
// ███████╗███████╗████████╗██╗ ██╗██████╗
// ██╔════╝██╔════╝╚══██╔══╝██║ ██║██╔══██╗
// ███████╗█████╗ ██║ ██║ ██║██████╔╝
// ╚════██║██╔══╝ ██║ ██║ ██║██╔═══╝
// ███████║███████╗ ██║ ╚██████╔╝██║
// ╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝
// If you're copying code from one of the sections in the switch statement below,
// you'll probably also want to copy this setup code.
// Set some defaults.
var DEFAULT_LIMIT = 30;
var DEFAULT_POPULATE_LIMIT = 30;
// Get the name of the blueprint action being run.
var blueprint = req.options.blueprintAction;
// ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌┬┐┌─┐┌┬┐┌─┐┬
// ├─┘├─┤├┬┘└─┐├┤ ││││ │ ││├┤ │
// ┴ ┴ ┴┴└─└─┘└─┘ ┴ ┴└─┘─┴┘└─┘┴─┘
// Get the model identity from the action name (e.g. 'user/find').
var model = req.options.action.split('/')[0];
if (!model) { throw new Error(util.format('No "model" specified in route options.')); }
// Get the model class.
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)); }
// ┌┬┐┌─┐┌─┐┌─┐┬ ┬┬ ┌┬┐ ┌─┐┌─┐┌─┐┬ ┬┬ ┌─┐┌┬┐┌─┐┌─┐
// ││├┤ ├┤ ├─┤│ ││ │ ├─┘│ │├─┘│ ││ ├─┤ │ ├┤ └─┐
// ─┴┘└─┘└ ┴ ┴└─┘┴─┘┴ ┴ └─┘┴ └─┘┴─┘┴ ┴ ┴ └─┘└─┘
// Get the default populates array
var defaultPopulates = _.reduce(Model.associations, function(memo, association) {
if (association.type === 'collection') {
memo[association.alias] = {
where: {},
limit: DEFAULT_POPULATE_LIMIT,
skip: 0,
select: [ '*' ],
omit: []
};
} else {
memo[association.alias] = {};
}
return memo;
}, {});
// Initialize the queryOptions dictionary we'll be returning.
var queryOptions = {
using: model,
populates: defaultPopulates
};
switch (blueprint) {
// ███████╗██╗███╗ ██╗██████╗ ██╗
// ██╔════╝██║████╗ ██║██╔══██╗ ██╔╝
// █████╗ ██║██╔██╗ ██║██║ ██║ ██╔╝
// ██╔══╝ ██║██║╚██╗██║██║ ██║ ██╔╝
// ██║ ██║██║ ╚████║██████╔╝ ██╔╝
// ╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚═╝
//
// ███████╗██╗███╗ ██╗██████╗ ██████╗ ███╗ ██╗███████╗
// ██╔════╝██║████╗ ██║██╔══██╗██╔═══██╗████╗ ██║██╔════╝
// █████╗ ██║██╔██╗ ██║██║ ██║██║ ██║██╔██╗ ██║█████╗
// ██╔══╝ ██║██║╚██╗██║██║ ██║██║ ██║██║╚██╗██║██╔══╝
// ██║ ██║██║ ╚████║██████╔╝╚██████╔╝██║ ╚████║███████╗
// ╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝
case 'find':
case 'findOne':
queryOptions.criteria = {};
// ┌─┐┌─┐┬─┐┌─┐┌─┐ ╔═╗╦═╗╦╔╦╗╔═╗╦═╗╦╔═╗
// ├─┘├─┤├┬┘└─┐├┤ ║ ╠╦╝║ ║ ║╣ ╠╦╝║╠═╣
// ┴ ┴ ┴┴└─└─┘└─┘ ╚═╝╩╚═╩ ╩ ╚═╝╩╚═╩╩ ╩
queryOptions.criteria.where = (function getWhereCriteria(){
var where = {};
// For `findOne`, set "where" to just look at the primary key.
if (blueprint === 'findOne') {
where[Model.primaryKey] = req.param('id');
return where;
}
// Look for explicitly specified `where` parameter.
where = req.allParams().where;
// If `where` parameter is a string, try to interpret it as JSON.
// (If it cannot be parsed, throw a UsageError.)
if (_.isString(where)) {
try {
where = JSON.parse(where);
} catch (e) {
throw flaverr({ name: 'UsageError' }, new Error('Could not JSON.parse() the provided `where` clause. Here is the raw error: '+e.stack));
}
}//>-•
// 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.allParams();
// Omit built-in runtime config (like query modifiers)
where = _.omit(where, ['limit', 'skip', 'sort', 'populate', 'select', 'omit']);
// Omit any params that have `undefined` on the RHS.
where = _.omit(where, function(p) {
if (_.isUndefined(p)) { return true; }
});
}//>-
// Return final `where`.
return where;
})();
// ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┌─┐┬ ┌─┐┌─┐┌┬┐
// ├─┘├─┤├┬┘└─┐├┤ └─┐├┤ │ ├┤ │ │
// ┴ ┴ ┴┴└─└─┘└─┘ └─┘└─┘┴─┘└─┘└─┘ ┴
if (!_.isUndefined(req.param('select'))) {
queryOptions.criteria.select = req.param('select').split(',').map(function(attribute) {return attribute.trim();});
}
// ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┌┬┐┬┌┬┐
// ├─┘├─┤├┬┘└─┐├┤ │ │││││ │
// ┴ ┴ ┴┴└─└─┘└─┘ └─┘┴ ┴┴ ┴
else if (!_.isUndefined(req.param('omit'))) {
queryOptions.criteria.omit = req.param('omit').split(',').map(function(attribute) {return attribute.trim();});
}
// ┌─┐┌─┐┬─┐┌─┐┌─┐ ┬ ┬┌┬┐┬┌┬┐
// ├─┘├─┤├┬┘└─┐├┤ │ │││││ │
// ┴ ┴ ┴┴└─└─┘└─┘ ┴─┘┴┴ ┴┴ ┴
if (!_.isUndefined(req.param('limit'))) {
queryOptions.criteria.limit = req.param('limit');
} else {
queryOptions.criteria.limit = DEFAULT_LIMIT;
}
// ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┬┌─┬┌─┐
// ├─┘├─┤├┬┘└─┐├┤ └─┐├┴┐│├─┘
// ┴ ┴ ┴┴└─└─┘└─┘ └─┘┴ ┴┴┴
if (!_.isUndefined(req.param('skip'))) { queryOptions.criteria.skip = req.param('skip'); }
// ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┌─┐┬─┐┌┬┐
// ├─┘├─┤├┬┘└─┐├┤ └─┐│ │├┬┘ │
// ┴ ┴ ┴┴└─└─┘└─┘ └─┘└─┘┴└─ ┴
if (!_.isUndefined(req.param('sort'))) {
queryOptions.criteria.sort = (function getSortCriteria() {
var sort = req.param('sort');
if (_.isUndefined(sort)) {return undefined;}
// If `sort` is a string, attempt to JSON.parse() it.
// (e.g. `{"name": 1}`)
if (_.isString(sort)) {
try {
sort = JSON.parse(sort);
// If it is not valid JSON (e.g. because it's just some other string),
// then just fall back to interpreting it as-is (e.g. "name ASC")
} catch(unusedErr) {}
}
return sort;
})();
}
// ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┌─┐┌─┐┬ ┬┬ ┌─┐┌┬┐┌─┐
// ├─┘├─┤├┬┘└─┐├┤ ├─┘│ │├─┘│ ││ ├─┤ │ ├┤
// ┴ ┴ ┴┴└─└─┘└─┘ ┴ └─┘┴ └─┘┴─┘┴ ┴ ┴ └─┘
// If a `populate` param was sent, filter the attributes to populate
// against that value.
// e.g.:
// /model?populate=alias1,alias2,alias3
// /model?populate=[alias1,alias2,alias3]
if (req.param('populate')) {
queryOptions.populates = (function getPopulates() {
// Get the request param.
var attributes = req.param('populate');
// If it's `false`, populate nothing.
if (attributes === 'false') {
return {};
}
// Split the list on commas.
attributes = attributes.split(',');
// Trim whitespace off of the attributes.
attributes = _.reduce(attributes, function(memo, attribute) {
memo[attribute.trim()] = {};
return memo;
}, {});
return attributes;
})();
}
break;
// ██████╗██████╗ ███████╗ █████╗ ████████╗███████╗
// ██╔════╝██╔══██╗██╔════╝██╔══██╗╚══██╔══╝██╔════╝
// ██║ ██████╔╝█████╗ ███████║ ██║ █████╗
// ██║ ██╔══██╗██╔══╝ ██╔══██║ ██║ ██╔══╝
// ╚██████╗██║ ██║███████╗██║ ██║ ██║ ███████╗
// ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝
case 'create':
// Set `fetch: true`
queryOptions.meta = { fetch: true };
// ┌─┐┌─┐┬─┐┌─┐┌─┐ ┬ ┬┌─┐┬ ┬ ┬┌─┐┌─┐
// ├─┘├─┤├┬┘└─┐├┤ └┐┌┘├─┤│ │ │├┤ └─┐
// ┴ ┴ ┴┴└─└─┘└─┘ └┘ ┴ ┴┴─┘└─┘└─┘└─┘
queryOptions.newRecord = (function getNewRecord(){
// Use all of the request params as values for the new record.
var values = req.allParams();
// Attempt to JSON parse any collection attributes into arrays. This is to allow
// setting collections using the shortcut routes.
_.each(Model.attributes, function(attrDef, attrName) {
if (attrDef.collection && (!req.body || !req.body[attrName]) && (req.query && _.isString(req.query[attrName]))) {
try {
values[attrName] = JSON.parse(req.query[attrName]);
// If it is not valid JSON (e.g. because it's just a normal string),
// then fall back to interpreting it as-is
} catch(unusedErr) {}
}
});
return values;
})();
break;
// ██╗ ██╗██████╗ ██████╗ █████╗ ████████╗███████╗
// ██║ ██║██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝██╔════╝
// ██║ ██║██████╔╝██║ ██║███████║ ██║ █████╗
// ██║ ██║██╔═══╝ ██║ ██║██╔══██║ ██║ ██╔══╝
// ╚██████╔╝██║ ██████╔╝██║ ██║ ██║ ███████╗
// ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝
case 'update':
queryOptions.criteria = {
where: {}
};
queryOptions.criteria.where[Model.primaryKey] = req.param('id');
// Note that we do NOT set `fetch: true`, because if we do so, some versions
// of Waterline complain that `fetch` need not be included with .updateOne().
// (Now that we take advantage of .updateOne() in blueprints, this is a thing.)
queryOptions.meta = {};
// ┌─┐┌─┐┬─┐┌─┐┌─┐ ┬ ┬┌─┐┬ ┬ ┬┌─┐┌─┐
// ├─┘├─┤├┬┘└─┐├┤ └┐┌┘├─┤│ │ │├┤ └─┐
// ┴ ┴ ┴┴└─└─┘└─┘ └┘ ┴ ┴┴─┘└─┘└─┘└─┘
queryOptions.valuesToSet = (function getValuesToSet(){
// Use all of the request params as values for the new record, _except_ `id`.
var values = _.omit(req.allParams(), 'id');
// No matter what, don't allow changing the PK via the update blueprint
// (you should just drop and re-add the record if that's what you really want)
if (typeof values[Model.primaryKey] !== 'undefined' && values[Model.primaryKey] !== queryOptions.criteria.where[Model.primaryKey]) {
req._sails.log.warn('Cannot change primary key via update blueprint; ignoring value sent for `' + Model.primaryKey + '`');
}
// Make sure the primary key is unchanged
values[Model.primaryKey] = queryOptions.criteria.where[Model.primaryKey];
return values;
})();
break;
// ██████╗ ███████╗███████╗████████╗██████╗ ██████╗ ██╗ ██╗
// ██╔══██╗██╔════╝██╔════╝╚══██╔══╝██╔══██╗██╔═══██╗╚██╗ ██╔╝
// ██║ ██║█████╗ ███████╗ ██║ ██████╔╝██║ ██║ ╚████╔╝
// ██║ ██║██╔══╝ ╚════██║ ██║ ██╔══██╗██║ ██║ ╚██╔╝
// ██████╔╝███████╗███████║ ██║ ██║ ██║╚██████╔╝ ██║
// ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝
case 'destroy':
queryOptions.criteria = {};
queryOptions.criteria = {
where: {}
};
queryOptions.criteria.where[Model.primaryKey] = req.param('id');
// Set `fetch: true`
queryOptions.meta = { fetch: true };
break;
// █████╗ ██████╗ ██████╗
// ██╔══██╗██╔══██╗██╔══██╗
// ███████║██║ ██║██║ ██║
// ██╔══██║██║ ██║██║ ██║
// ██║ ██║██████╔╝██████╔╝
// ╚═╝ ╚═╝╚═════╝ ╚═════╝
case 'add':
if (!req.options.alias) {
throw new Error('Missing required route option, `req.options.alias`.');
}
queryOptions.alias = req.options.alias;
queryOptions.targetRecordId = req.param('parentid');
queryOptions.associatedIds = [req.param('childid')];
break;
// ██████╗ ███████╗███╗ ███╗ ██████╗ ██╗ ██╗███████╗
// ██╔══██╗██╔════╝████╗ ████║██╔═══██╗██║ ██║██╔════╝
// ██████╔╝█████╗ ██╔████╔██║██║ ██║██║ ██║█████╗
// ██╔══██╗██╔══╝ ██║╚██╔╝██║██║ ██║╚██╗ ██╔╝██╔══╝
// ██║ ██║███████╗██║ ╚═╝ ██║╚██████╔╝ ╚████╔╝ ███████╗
// ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ╚══════╝
case 'remove':
if (!req.options.alias) {
throw new Error('Missing required route option, `req.options.alias`.');
}
queryOptions.alias = req.options.alias;
queryOptions.targetRecordId = req.param('parentid');
queryOptions.associatedIds = [req.param('childid')];
break;
// ██████╗ ███████╗██████╗ ██╗ █████╗ ██████╗███████╗
// ██╔══██╗██╔════╝██╔══██╗██║ ██╔══██╗██╔════╝██╔════╝
// ██████╔╝█████╗ ██████╔╝██║ ███████║██║ █████╗
// ██╔══██╗██╔══╝ ██╔═══╝ ██║ ██╔══██║██║ ██╔══╝
// ██║ ██║███████╗██║ ███████╗██║ ██║╚██████╗███████╗
// ╚═╝ ╚═╝╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝╚══════╝
case 'replace':
if (!req.options.alias) {
throw new Error('Missing required route option, `req.options.alias`.');
}
queryOptions.alias = req.options.alias;
queryOptions.criteria = {};
queryOptions.criteria = {
where: {}
};
queryOptions.targetRecordId = req.param('parentid');
queryOptions.associatedIds = _.isArray(req.body) ? req.body : req.query[req.options.alias];
if (_.isString(queryOptions.associatedIds)) {
try {
queryOptions.associatedIds = JSON.parse(queryOptions.associatedIds);
} catch (e) {
throw flaverr({ name: 'UsageError', raw: e }, new Error(
'The associated ids provided in this request (for the `' + req.options.alias + '` collection) are not valid. '+
'If specified as a string, the associated ids provided to the "replace" blueprint action must be parseable as '+
'a JSON array, e.g. `[1, 2]`.'
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// FUTURE: Use smart example depending on the expected pk type (e.g. if string, show mongo ids instead)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
));
}//</catch>
}
break;
// ██████╗ ██████╗ ██████╗ ██╗ ██╗██╗ █████╗ ████████╗███████╗
// ██╔══██╗██╔═══██╗██╔══██╗██║ ██║██║ ██╔══██╗╚══██╔══╝██╔════╝
// ██████╔╝██║ ██║██████╔╝██║ ██║██║ ███████║ ██║ █████╗
// ██╔═══╝ ██║ ██║██╔═══╝ ██║ ██║██║ ██╔══██║ ██║ ██╔══╝
// ██║ ╚██████╔╝██║ ╚██████╔╝███████╗██║ ██║ ██║ ███████╗
// ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝
case 'populate':
if (!req.options.alias) {
throw new Error('Missing required route option, `req.options.alias`.');
}
var association = _.find(Model.associations, {alias: req.options.alias});
if (!association) {
throw new Error('Consistency violation: `populate` blueprint could not find association `' + req.options.alias + '` in model `' + Model.globalId + '`.');
}
queryOptions.alias = req.options.alias;
queryOptions.criteria = {};
// ┌─┐┌─┐┬─┐┌─┐┌─┐ ╔═╗╦═╗╦╔╦╗╔═╗╦═╗╦╔═╗
// ├─┘├─┤├┬┘└─┐├┤ ║ ╠╦╝║ ║ ║╣ ╠╦╝║╠═╣
// ┴ ┴ ┴┴└─└─┘└─┘ ╚═╝╩╚═╩ ╩ ╚═╝╩╚═╩╩ ╩
queryOptions.criteria = {};
queryOptions.criteria = {
where: {}
};
queryOptions.criteria.where[Model.primaryKey] = req.param('parentid');
queryOptions.populates = {};
queryOptions.populates[req.options.alias] = {};
// If this is a to-many association, add a `where` clause.
if (association.collection) {
queryOptions.populates[req.options.alias].where = (function getPopulateCriteria(){
var where = req.allParams().where;
// If `where` parameter is a string, try to interpret it as JSON.
// (If it cannot be parsed, throw a UsageError.)
if (_.isString(where)) {
try {
where = JSON.parse(where);
} catch (e) {
throw flaverr({ name: 'UsageError' }, new Error('Could not JSON.parse() the provided `where` clause. Here is the raw error: '+e.stack));
}
}//>-•
// 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.allParams();
// Omit built-in runtime config (like top-level criteria clauses)
where = _.omit(where, ['limit', 'skip', 'sort', 'populate', 'select', 'omit', 'parentid']);
// - - - - - - - - - - - - - - - - - - - - -
// ^^TODO: what about `where` itself?
// - - - - - - - - - - - - - - - - - - - - -
// Omit any params that have `undefined` on the RHS.
where = _.omit(where, function(p) {
if (_.isUndefined(p)) { return true; }
});
}//>-
// Return final `where`.
return where;
})();
}
// ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┌─┐┬ ┌─┐┌─┐┌┬┐
// ├─┘├─┤├┬┘└─┐├┤ └─┐├┤ │ ├┤ │ │
// ┴ ┴ ┴┴└─└─┘└─┘ └─┘└─┘┴─┘└─┘└─┘ ┴
if (!_.isUndefined(req.param('select'))) {
queryOptions.populates[req.options.alias].select = req.param('select').split(',').map(function(attribute) {return attribute.trim();});
}
// ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┌┬┐┬┌┬┐
// ├─┘├─┤├┬┘└─┐├┤ │ │││││ │
// ┴ ┴ ┴┴└─└─┘└─┘ └─┘┴ ┴┴ ┴
else if (!_.isUndefined(req.param('omit'))) {
queryOptions.populates[req.options.alias].omit = req.param('omit').split(',').map(function(attribute) {return attribute.trim();});
}
//
// ┌─┐┌─┐┬─┐┌─┐┌─┐ ┬ ┬┌┬┐┬┌┬┐
// ├─┘├─┤├┬┘└─┐├┤ │ │││││ │
// ┴ ┴ ┴┴└─└─┘└─┘ ┴─┘┴┴ ┴┴ ┴
if (!_.isUndefined(req.param('limit'))) {
queryOptions.populates[req.options.alias].limit = req.param('limit');
}
// If this is a to-many association, use the default limit if not was provided.
else if (association.collection) {
queryOptions.populates[req.options.alias].limit = DEFAULT_LIMIT;
}
// ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┬┌─┬┌─┐
// ├─┘├─┤├┬┘└─┐├┤ └─┐├┴┐│├─┘
// ┴ ┴ ┴┴└─└─┘└─┘ └─┘┴ ┴┴┴
if (!_.isUndefined(req.param('skip'))) { queryOptions.populates[req.options.alias].skip = req.param('skip'); }
// ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┌─┐┬─┐┌┬┐
// ├─┘├─┤├┬┘└─┐├┤ └─┐│ │├┬┘ │
// ┴ ┴ ┴┴└─└─┘└─┘ └─┘└─┘┴└─ ┴
if (!_.isUndefined(req.param('sort'))) {
queryOptions.populates[req.options.alias].sort = (function getSortCriteria() {
var sort = req.param('sort');
if (_.isUndefined(sort)) {return undefined;}
// If `sort` is a string, attempt to JSON.parse() it.
// (e.g. `{"name": 1}`)
if (_.isString(sort)) {
try {
sort = JSON.parse(sort);
// If it is not valid JSON (e.g. because it's just a normal string),
// then fall back to interpreting it as-is (e.g. "fullName ASC")
} catch(unusedErr) {}
}
return sort;
})();//ˆ
}
break;
}
return queryOptions;
};