UNPKG

waterline

Version:

An ORM for Node.js and the Sails framework

765 lines (596 loc) 25.5 kB
/** * 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; };