lib/waterline/utils/query/get-query-modifier-methods.js
/**
* Module dependencies
*/
var assert = require('assert');
var _ = require('@sailshq/lodash');
var expandWhereShorthand = require('./private/expand-where-shorthand');
/**
* Module constants
*/
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// FUTURE: Consider pulling these out into their own files.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var BASELINE_Q_METHODS = {
/**
* Pass special metadata (a dictionary of "meta keys") down to Waterline core,
* and all the way to the adapter that won't be processed or touched by Waterline.
*
* > Note that we use `_wlQueryInfo.meta` internally because we're already using
* > `.meta()` as a method! In an actual S2Q, this key continues to be called `meta`.
*/
meta: function(metadata) {
// If meta already exists, merge on top of it.
// (this is important for when this method is combined with other things
// like .usingConnection() that mutate meta keys)
if (this._wlQueryInfo.meta) {
_.extend(this._wlQueryInfo.meta, metadata);
}
else {
this._wlQueryInfo.meta = metadata;
}
return this;
},
/**
* Pass an active database connection down to the query.
*/
usingConnection: function(db) {
this._wlQueryInfo.meta = this._wlQueryInfo.meta || {};
this._wlQueryInfo.meta.leasedConnection = db;
return this;
}
};
var STREAM_Q_METHODS = {
/**
* Add an iteratee to the query
*
* @param {Function} iteratee
* @returns {Query}
*/
eachRecord: function(iteratee) {
assert(this._wlQueryInfo.method === 'stream', 'Cannot chain `.eachRecord()` onto the `.'+this._wlQueryInfo.method+'()` method. The `.eachRecord()` method is only chainable to `.stream()`. (In fact, this shouldn\'t even be possible! So the fact that you are seeing this message at all is, itself, likely due to a bug in Waterline.)');
this._wlQueryInfo.eachRecordFn = iteratee;
return this;
},
/**
* Add an iteratee to the query
*
* @param {Number|Function} batchSizeOrIteratee
* @param {Function} iteratee
* @returns {Query}
*/
eachBatch: function(batchSizeOrIteratee, iteratee) {
assert(this._wlQueryInfo.method === 'stream', 'Cannot chain `.eachBatch()` onto the `.'+this._wlQueryInfo.method+'()` method. The `.eachBatch()` method is only chainable to `.stream()`. (In fact, this shouldn\'t even be possible! So the fact that you are seeing this message at all is, itself, likely due to a bug in Waterline.)');
if (arguments.length > 2) {
throw new Error('Invalid usage for `.eachBatch()` -- no more than 2 arguments should be passed in.');
}//•
if (iteratee === undefined) {
this._wlQueryInfo.eachBatchFn = batchSizeOrIteratee;
} else {
this._wlQueryInfo.eachBatchFn = iteratee;
// Apply custom batch size:
// > If meta already exists, merge on top of it.
// > (this is important for when this method is combined with .meta()/.usingConnection()/etc)
if (this._wlQueryInfo.meta) {
_.extend(this._wlQueryInfo.meta, { batchSize: batchSizeOrIteratee });
}
else {
this._wlQueryInfo.meta = { batchSize: batchSizeOrIteratee };
}
}
return this;
},
};
var SET_Q_METHODS = {
/**
* Add values to be used in update or create query
*
* @param {Dictionary} values
* @returns {Query}
*/
set: function(values) {
if (this._wlQueryInfo.method === 'create') {
console.warn(
'Deprecation warning: In future versions of Waterline, the use of .set() with .create()\n'+
'will no longer be supported. In the past, you could use .set() to provide the initial\n'+
'skeleton of a new record to create (like `.create().set({})`)-- but really .set() should\n'+
'only be used with .update(). So instead, please change this code so that it just passes in\n'+
'the initial new record as the first argument to `.create().`'
);
this._wlQueryInfo.newRecord = values;
}
else if (this._wlQueryInfo.method === 'createEach') {
console.warn(
'Deprecation warning: In future versions of Waterline, the use of .set() with .createEach()\n'+
'will no longer be supported. In the past, you could use .set() to provide an array of\n'+
'new records to create (like `.createEach().set([{}, {}])`)-- but really .set() was designed\n'+
'to be used with .update() only. So instead, please change this code so that it just\n'+
'passes in the initial new record as the first argument to `.createEach().`'
);
this._wlQueryInfo.newRecords = values;
}
else {
this._wlQueryInfo.valuesToSet = values;
}
return this;
},
};
var COLLECTION_Q_METHODS = {
/**
* Add associated IDs to the query
*
* @param {Array} associatedIds
* @returns {Query}
*/
members: function(associatedIds) {
this._wlQueryInfo.associatedIds = associatedIds;
return this;
},
};
var POPULATE_Q_METHODS = {
/**
* Modify this query so that it populates all associations (singular and plural).
*
* @returns {Query}
*/
populateAll: function() {
var pleaseDoNotUseThisArgument = arguments[0];
if (!_.isUndefined(pleaseDoNotUseThisArgument)) {
console.warn(
'Deprecation warning: Passing in an argument to `.populateAll()` is no longer supported.\n'+
'(But interpreting this usage the original way for you this time...)\n'+
'Note: If you really want to use the _exact same_ criteria for simultaneously populating multiple\n'+
'different plural ("collection") associations, please use separate calls to `.populate()` instead.\n'+
'Or, alternatively, instead of using `.populate()`, you can choose to call `.find()`, `.findOne()`,\n'+
'or `.stream()` with a dictionary (plain JS object) as the second argument, where each key is the\n'+
'name of an association, and each value is either:\n'+
' • true (for singular aka "model" associations), or\n'+
' • a criteria dictionary (for plural aka "collection" associations)\n'
);
}//>-
var self = this;
this._WLModel.associations.forEach(function (associationInfo) {
self.populate(associationInfo.alias, pleaseDoNotUseThisArgument);
});
return this;
},
/**
* .populate()
*
* Set the `populates` key for this query.
*
* > Used for populating associations.
*
* @param {String|Array} key, the key to populate or array of string keys
* @returns {Query}
*/
populate: function(keyName, subcriteria) {
assert(this._wlQueryInfo.method === 'find' || this._wlQueryInfo.method === 'findOne' || this._wlQueryInfo.method === 'stream', 'Cannot chain `.populate()` onto the `.'+this._wlQueryInfo.method+'()` method. (In fact, this shouldn\'t even be possible! So the fact that you are seeing this message at all is, itself, likely due to a bug in Waterline.)');
// Backwards compatibility for arrays passed in as `keyName`.
if (_.isArray(keyName)) {
console.warn(
'Deprecation warning: `.populate()` no longer accepts an array as its first argument.\n'+
'Please use separate calls to `.populate()` instead. Or, alternatively, instead of\n'+
'using `.populate()`, you can choose to call `.find()`, `.findOne()` or `.stream()`\n'+
'with a dictionary (plain JS object) as the second argument, where each key is the\n'+
'name of an association, and each value is either:\n'+
' • true (for singular aka "model" associations), or\n'+
' • a criteria dictionary (for plural aka "collection" associations)\n'+
'(Interpreting this usage the original way for you this time...)\n'
);
var self = this;
_.each(keyName, function(populate) {
self.populate(populate, subcriteria);
});
return this;
}//-•
// Verify that we're dealing with a semi-reasonable string.
// (This is futher validated)
if (!keyName || !_.isString(keyName)) {
throw new Error('Invalid usage for `.populate()` -- first argument should be the name of an assocation.');
}
// If this is the first time, make the `populates` query key an empty dictionary.
if (_.isUndefined(this._wlQueryInfo.populates)) {
this._wlQueryInfo.populates = {};
}
// Then, if subcriteria was specified, use it.
if (!_.isUndefined(subcriteria)){
this._wlQueryInfo.populates[keyName] = subcriteria;
}
else {
// (Note: even though we set {} regardless, even when it should really be `true`
// if it's a singular association, that's ok because it gets silently normalized
// in FS2Q.)
this._wlQueryInfo.populates[keyName] = {};
}
return this;
},
};
var PAGINATION_Q_METHODS = {
/**
* Add a `limit` clause to the query's criteria.
*
* @param {Number} number to limit
* @returns {Query}
*/
limit: function(limit) {
if (!this._alreadyInitiallyExpandedCriteria) {
this._wlQueryInfo.criteria = expandWhereShorthand(this._wlQueryInfo.criteria);
this._alreadyInitiallyExpandedCriteria = true;
}//>-
this._wlQueryInfo.criteria.limit = limit;
return this;
},
/**
* Add a `skip` clause to the query's criteria.
*
* @param {Number} number to skip
* @returns {Query}
*/
skip: function(skip) {
if (!this._alreadyInitiallyExpandedCriteria) {
this._wlQueryInfo.criteria = expandWhereShorthand(this._wlQueryInfo.criteria);
this._alreadyInitiallyExpandedCriteria = true;
}//>-
this._wlQueryInfo.criteria.skip = skip;
return this;
},
/**
* .paginate()
*
* Add a `skip`+`limit` clause to the query's criteria
* based on the specified page number (and optionally,
* the page size, which defaults to 30 otherwise.)
*
* > This method is really just a little dollop of syntactic sugar.
*
* ```
* Show.find({ category: 'home-and-garden' })
* .paginate(0)
* .exec(...)
* ```
*
* -OR- (for backwards compat.)
* ```
* Show.find({ category: 'home-and-garden' })
* .paginate({ page: 0, limit: 30 })
* .exec(...)
* ```
* - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* @param {Number} pageNumOrOpts
* @param {Number?} pageSize
*
* -OR-
*
* @param {Number|Dictionary} pageNumOrOpts
* @property {Number} page [the page num. (backwards compat.)]
* @property {Number?} limit [the page size (backwards compat.)]
* - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* @returns {Query}
* - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*/
paginate: function(pageNumOrOpts, pageSize) {
if (!this._alreadyInitiallyExpandedCriteria) {
this._wlQueryInfo.criteria = expandWhereShorthand(this._wlQueryInfo.criteria);
this._alreadyInitiallyExpandedCriteria = true;
}//>-
// Interpret page number.
var pageNum;
// If not specified...
if (_.isUndefined(pageNumOrOpts)) {
console.warn(
'Please always specify a `page` when calling .paginate() -- for example:\n'+
'```\n'+
'var first30Boats = await Boat.find()\n'+
'.sort(\'wetness DESC\')\n'+
'.paginate(0, 30)\n'+
'```\n'+
'(In the mean time, assuming the first page (#0)...)'
);
pageNum = 0;
}
// If dictionary... (temporary backwards-compat.)
else if (_.isObject(pageNumOrOpts)) {
pageNum = pageNumOrOpts.page || 0;
console.warn(
'Deprecation warning: Passing in a dictionary (plain JS object) to .paginate()\n'+
'is no longer supported -- instead, please use:\n'+
'```\n'+
'.paginate(pageNum, pageSize)\n'+
'```\n'+
'(In the mean time, interpreting this as page #'+pageNum+'...)'
);
}
// Otherwise, assume it's the proper usage.
else {
pageNum = pageNumOrOpts;
}
// Interpret the page size (number of records per page).
if (!_.isUndefined(pageSize)) {
if (!_.isNumber(pageSize)) {
console.warn(
'Unrecognized usage for .paginate() -- if specified, 2nd argument (page size)\n'+
'should be a number like 10 (otherwise, it defaults to 30).\n'+
'(Ignoring this and switching to a page size of 30 automatically...)'
);
pageSize = 30;
}
}
else if (_.isObject(pageNumOrOpts) && !_.isUndefined(pageNumOrOpts.limit)) {
// Note: IWMIH, then we must have already logged a deprecation warning above--
// so no need to do it again.
pageSize = pageNumOrOpts.limit || 30;
}
else {
// Note that this default is the same as the default batch size used by `.stream()`.
pageSize = 30;
}
// If page size is Infinity, then bail out now without doing anything.
// (Unless of course, this is a page other than the first-- that would be an error,
// because ordinals beyond infinity don't exist in real life)
if (pageSize === Infinity) {
if (pageNum !== 0) {
console.warn(
'Unrecognized usage for .paginate() -- if 2nd argument (page size) is Infinity,\n'+
'then the 1st argument (page num) must be zero, indicating the first page.\n'+
'(Ignoring this and using page zero w/ an infinite page size automatically...)'
);
}
return this;
}//-•
// Now, apply the page size as the limit, and compute & apply the appropriate `skip`.
// (REMEMBER: pages are now zero-indexed!)
this
.skip(pageNum * pageSize)
.limit(pageSize);
return this;
},
/**
* Add a `sort` clause to the criteria object
*
* @param {Ref} sortClause
* @returns {Query}
*/
sort: function(sortClause) {
if (!this._alreadyInitiallyExpandedCriteria) {
this._wlQueryInfo.criteria = expandWhereShorthand(this._wlQueryInfo.criteria);
this._alreadyInitiallyExpandedCriteria = true;
}//>-
this._wlQueryInfo.criteria.sort = sortClause;
return this;
},
};
var PROJECTION_Q_METHODS = {
/**
* Add projections to the query.
*
* @param {Array} attributes to select
* @returns {Query}
*/
select: function(selectAttributes) {
if (!this._alreadyInitiallyExpandedCriteria) {
this._wlQueryInfo.criteria = expandWhereShorthand(this._wlQueryInfo.criteria);
this._alreadyInitiallyExpandedCriteria = true;
}//>-
this._wlQueryInfo.criteria.select = selectAttributes;
return this;
},
/**
* Add an omit clause to the query's criteria.
*
* @param {Array} attributes to select
* @returns {Query}
*/
omit: function(omitAttributes) {
if (!this._alreadyInitiallyExpandedCriteria) {
this._wlQueryInfo.criteria = expandWhereShorthand(this._wlQueryInfo.criteria);
this._alreadyInitiallyExpandedCriteria = true;
}//>-
this._wlQueryInfo.criteria.omit = omitAttributes;
return this;
},
};
var FILTER_Q_METHODS = {
/**
* Add a `where` clause to the query's criteria.
*
* @param {Dictionary} criteria to append
* @returns {Query}
*/
where: function(whereCriteria) {
if (!this._alreadyInitiallyExpandedCriteria) {
this._wlQueryInfo.criteria = expandWhereShorthand(this._wlQueryInfo.criteria);
this._alreadyInitiallyExpandedCriteria = true;
}//>-
this._wlQueryInfo.criteria.where = whereCriteria;
return this;
},
};
var FETCH_Q_METHODS = {
/**
* Add `fetch: true` to the query's `meta`.
*
* @returns {Query}
*/
fetch: function() {
if (arguments.length > 0) {
throw new Error('Invalid usage for `.fetch()` -- no arguments should be passed in.');
}
// If meta already exists, merge on top of it.
// (this is important for when this method is combined with .meta()/.usingConnection()/etc)
if (this._wlQueryInfo.meta) {
_.extend(this._wlQueryInfo.meta, { fetch: true });
}
else {
this._wlQueryInfo.meta = { fetch: true };
}
return this;
},
};
var DECRYPT_Q_METHODS = {
/**
* Add `decrypt: true` to the query's `meta`.
*
* @returns {Query}
*/
decrypt: function() {
if (arguments.length > 0) {
throw new Error('Invalid usage for `.decrypt()` -- no arguments should be passed in.');
}
// If meta already exists, merge on top of it.
// (this is important for when this method is combined with .meta()/.usingConnection()/etc)
if (this._wlQueryInfo.meta) {
_.extend(this._wlQueryInfo.meta, { decrypt: true });
}
else {
this._wlQueryInfo.meta = { decrypt: true };
}
return this;
},
};
// ██╗ ██╗███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗ ██████╗ ██████╗ ████████╗███████╗██████╗
// ██║ ██║████╗ ██║██╔════╝██║ ██║██╔══██╗██╔══██╗██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝██╔══██╗
// ██║ ██║██╔██╗ ██║███████╗██║ ██║██████╔╝██████╔╝██║ ██║██████╔╝ ██║ █████╗ ██║ ██║
// ██║ ██║██║╚██╗██║╚════██║██║ ██║██╔═══╝ ██╔═══╝ ██║ ██║██╔══██╗ ██║ ██╔══╝ ██║ ██║
// ╚██████╔╝██║ ╚████║███████║╚██████╔╝██║ ██║ ╚██████╔╝██║ ██║ ██║ ███████╗██████╔╝
// ╚═════╝ ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═════╝
//
// ███╗ ███╗███████╗████████╗██╗ ██╗ ██████╗ ██████╗ ███████╗
// ████╗ ████║██╔════╝╚══██╔══╝██║ ██║██╔═══██╗██╔══██╗██╔════╝
// ██╔████╔██║█████╗ ██║ ███████║██║ ██║██║ ██║███████╗
// ██║╚██╔╝██║██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║╚════██║
// ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║╚██████╔╝██████╔╝███████║
// ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝
//
var OLD_AGGREGATION_Q_METHODS = {
/**
* Add the (NO LONGER SUPPORTED) `sum` clause to the criteria.
*
* > This is allowed through purposely, in order to trigger
* > the proper query error in FS2Q.
*
* @returns {Query}
*/
sum: function() {
if (!this._alreadyInitiallyExpandedCriteria) {
this._wlQueryInfo.criteria = expandWhereShorthand(this._wlQueryInfo.criteria);
this._alreadyInitiallyExpandedCriteria = true;
}//>-
this._wlQueryInfo.criteria.sum = arguments[0];
return this;
},
/**
* Add the (NO LONGER SUPPORTED) `avg` clause to the criteria.
*
* > This is allowed through purposely, in order to trigger
* > the proper query error in FS2Q.
*
* @returns {Query}
*/
avg: function() {
if (!this._alreadyInitiallyExpandedCriteria) {
this._wlQueryInfo.criteria = expandWhereShorthand(this._wlQueryInfo.criteria);
this._alreadyInitiallyExpandedCriteria = true;
}//>-
this._wlQueryInfo.criteria.avg = arguments[0];
return this;
},
/**
* Add the (NO LONGER SUPPORTED) `min` clause to the criteria.
*
* > This is allowed through purposely, in order to trigger
* > the proper query error in FS2Q.
*
* @returns {Query}
*/
min: function() {
if (!this._alreadyInitiallyExpandedCriteria) {
this._wlQueryInfo.criteria = expandWhereShorthand(this._wlQueryInfo.criteria);
this._alreadyInitiallyExpandedCriteria = true;
}//>-
this._wlQueryInfo.criteria.min = arguments[0];
return this;
},
/**
* Add the (NO LONGER SUPPORTED) `max` clause to the criteria.
*
* > This is allowed through purposely, in order to trigger
* > the proper query error in FS2Q.
*
* @returns {Query}
*/
max: function() {
if (!this._alreadyInitiallyExpandedCriteria) {
this._wlQueryInfo.criteria = expandWhereShorthand(this._wlQueryInfo.criteria);
this._alreadyInitiallyExpandedCriteria = true;
}//>-
this._wlQueryInfo.criteria.max = arguments[0];
return this;
},
/**
* Add the (NO LONGER SUPPORTED) `groupBy` clause to the criteria.
*
* > This is allowed through purposely, in order to trigger
* > the proper query error in FS2Q.
*/
groupBy: function() {
if (!this._alreadyInitiallyExpandedCriteria) {
this._wlQueryInfo.criteria = expandWhereShorthand(this._wlQueryInfo.criteria);
this._alreadyInitiallyExpandedCriteria = true;
}//>-
this._wlQueryInfo.criteria.groupBy = arguments[0];
return this;
},
};
/**
* getQueryModifierMethods()
*
* Return a dictionary containing the appropriate query (Deferred) methods
* for the specified category (i.e. model method name).
*
* > For example, calling `getQueryModifierMethods('find')` returns a dictionary
* > of methods like `where` and `select`, as well as the usual suspects
* > like `meta` and `usingConnection`.
* >
* > This never returns generic, universal Deferred methods; i.e. `exec`,
* > `then`, `catch`, and `toPromise`. Those are expected to be supplied
* > by parley.
*
*
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* @param {String} category
* The name of the model method this query is for.
*
* @returns {Dictionary}
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*/
module.exports = function getQueryModifierMethods(category){
assert(category && _.isString(category), 'A category must be provided as a valid string.');
// Set up the initial state of the dictionary that we'll be returning.
var queryMethods = {};
// No matter what category this is, we always begin with certain baseline methods.
_.extend(queryMethods, BASELINE_Q_METHODS);
// But from there, the methods become category specific:
switch (category) {
case 'find': _.extend(queryMethods, FILTER_Q_METHODS, PAGINATION_Q_METHODS, OLD_AGGREGATION_Q_METHODS, PROJECTION_Q_METHODS, POPULATE_Q_METHODS, DECRYPT_Q_METHODS); break;
case 'findOne': _.extend(queryMethods, FILTER_Q_METHODS, PROJECTION_Q_METHODS, POPULATE_Q_METHODS, DECRYPT_Q_METHODS); break;
case 'stream': _.extend(queryMethods, FILTER_Q_METHODS, PAGINATION_Q_METHODS, PROJECTION_Q_METHODS, POPULATE_Q_METHODS, STREAM_Q_METHODS, DECRYPT_Q_METHODS); break;
case 'count': _.extend(queryMethods, FILTER_Q_METHODS); break;
case 'sum': _.extend(queryMethods, FILTER_Q_METHODS); break;
case 'avg': _.extend(queryMethods, FILTER_Q_METHODS); break;
case 'create': _.extend(queryMethods, SET_Q_METHODS, FETCH_Q_METHODS, DECRYPT_Q_METHODS); break;
case 'createEach': _.extend(queryMethods, SET_Q_METHODS, FETCH_Q_METHODS, DECRYPT_Q_METHODS); break;
case 'findOrCreate': _.extend(queryMethods, FILTER_Q_METHODS, SET_Q_METHODS, FETCH_Q_METHODS, DECRYPT_Q_METHODS); break;
case 'update': _.extend(queryMethods, FILTER_Q_METHODS, SET_Q_METHODS, FETCH_Q_METHODS, DECRYPT_Q_METHODS); break;
case 'updateOne': _.extend(queryMethods, FILTER_Q_METHODS, SET_Q_METHODS, FETCH_Q_METHODS, DECRYPT_Q_METHODS); break;
case 'destroy': _.extend(queryMethods, FILTER_Q_METHODS, FETCH_Q_METHODS, DECRYPT_Q_METHODS); break;
case 'destroyOne': _.extend(queryMethods, FILTER_Q_METHODS, FETCH_Q_METHODS, DECRYPT_Q_METHODS); break;
case 'archive': _.extend(queryMethods, FILTER_Q_METHODS, FETCH_Q_METHODS, DECRYPT_Q_METHODS); break;
case 'archiveOne': _.extend(queryMethods, FILTER_Q_METHODS, FETCH_Q_METHODS, DECRYPT_Q_METHODS); break;
case 'addToCollection': _.extend(queryMethods, COLLECTION_Q_METHODS); break;
case 'removeFromCollection': _.extend(queryMethods, COLLECTION_Q_METHODS); break;
case 'replaceCollection': _.extend(queryMethods, COLLECTION_Q_METHODS); break;
default: throw new Error('Consistency violation: Unrecognized category (model method name): `'+category+'`');
}
// Now that we're done, return the new dictionary of methods.
return queryMethods;
};