UNPKG

waterline

Version:

An ORM for Node.js and the Sails framework

1,045 lines (856 loc) 117 kB
/** * Module dependencies */ var assert = require('assert'); var util = require('util'); var _ = require('@sailshq/lodash'); var getModel = require('../ontology/get-model'); var getAttribute = require('../ontology/get-attribute'); var isCapableOfOptimizedPopulate = require('../ontology/is-capable-of-optimized-populate'); var isExclusive = require('../ontology/is-exclusive'); var normalizePkValueOrValues = require('./private/normalize-pk-value-or-values'); var normalizeCriteria = require('./private/normalize-criteria'); var normalizeNewRecord = require('./private/normalize-new-record'); var normalizeValueToSet = require('./private/normalize-value-to-set'); var buildUsageError = require('./private/build-usage-error'); var isSafeNaturalNumber = require('./private/is-safe-natural-number'); /** * forgeStageTwoQuery() * * Normalize and validate userland query keys (called a "stage 1 query" -- see `ARCHITECTURE.md`) * i.e. these are things like `criteria` or `populates` that are passed in, either explicitly or * implicitly, to a static model method (fka "collection method") such as `.find()`. * * > This DOES NOT RETURN ANYTHING! Instead, it modifies the provided "stage 1 query" in-place. * > And when this is finished, the provided "stage 1 query" will be a normalized, validated * > "stage 2 query" - aka logical protostatement. * > * > ALSO NOTE THAT THIS IS NOT ALWAYS IDEMPOTENT!! (Consider encryption.) * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * * @param {Dictionary} query [A stage 1 query to destructively mutate into a stage 2 query.] * | @property {String} method * | @property {Dictionary} meta * | @property {String} using * | * |...PLUS a number of other potential properties, depending on the "method". (see below) * * * @param {Ref} orm * The Waterline ORM instance. * > Useful for accessing the model definitions, datastore configurations, etc. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * * @throws {Error} If it encounters irrecoverable problems or unsupported usage in the provided query keys. * @property {String} name (Always "UsageError") * @property {String} code * One of: * - E_INVALID_META (universal) * - E_INVALID_CRITERIA * - E_INVALID_POPULATES * - E_INVALID_NUMERIC_ATTR_NAME * - E_INVALID_STREAM_ITERATEE (for `eachBatchFn` & `eachRecordFn`) * - E_INVALID_NEW_RECORD * - E_INVALID_NEW_RECORDS * - E_INVALID_VALUES_TO_SET * - E_INVALID_TARGET_RECORD_IDS * - E_INVALID_COLLECTION_ATTR_NAME * - E_INVALID_ASSOCIATED_IDS * - E_NOOP (relevant for various different methods, like find/count/addToCollection/etc.) * @property {String} details * The lower-level, original error message, without any sort of "Invalid yada yada. Details: ..." wrapping. * Use this property to create custom messages -- for example: * ``` * new Error(e.details); * ``` * @property {String} message * The standard `message` property of any Error-- just note that this Error's `message` is composed * from an original, lower-level error plus a template (see buildUsageError() for details.) * @property {String} stack * The standard `stack` property, like any Error. Combines name + message + stack trace. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * * @throws {Error} If anything else unexpected occurs * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ module.exports = function forgeStageTwoQuery(query, orm) { // if (process.env.NODE_ENV !== 'production') { // console.time('forgeStageTwoQuery'); // } // Create a JS timestamp to represent the current (timezone-agnostic) date+time. var theMomentBeforeFS2Q = Date.now(); // ^^ -- -- -- -- -- -- -- -- -- -- -- -- -- // Since Date.now() has trivial performance impact, we generate our // JS timestamp up here no matter what, just in case we end up needing // it later for `autoCreatedAt` or `autoUpdatedAt`, in situations where // we might need to automatically add it in multiple spots (such as // in `newRecords`, when processing a `.createEach()`.) // // > Benchmark: // > • Absolute: ~0.021ms // > • Relative: http://jsben.ch/#/TOF9y (vs. `(new Date()).getTime()`) // -- -- -- -- -- -- -- -- -- -- -- -- -- -- // ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗ ████████╗██╗ ██╗███████╗ // ██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝ ╚══██╔══╝██║ ██║██╔════╝ // ██║ ███████║█████╗ ██║ █████╔╝ ██║ ███████║█████╗ // ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ██║ ██╔══██║██╔══╝ // ╚██████╗██║ ██║███████╗╚██████╗██║ ██╗ ██║ ██║ ██║███████╗ // ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ // // ███████╗███████╗███████╗███████╗███╗ ██╗████████╗██╗ █████╗ ██╗ ███████╗ // ██╔════╝██╔════╝██╔════╝██╔════╝████╗ ██║╚══██╔══╝██║██╔══██╗██║ ██╔════╝ // █████╗ ███████╗███████╗█████╗ ██╔██╗ ██║ ██║ ██║███████║██║ ███████╗ // ██╔══╝ ╚════██║╚════██║██╔══╝ ██║╚██╗██║ ██║ ██║██╔══██║██║ ╚════██║ // ███████╗███████║███████║███████╗██║ ╚████║ ██║ ██║██║ ██║███████╗███████║ // ╚══════╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝ // ┌─┐┬ ┬┌─┐┌─┐┬┌─ ╦ ╦╔═╗╦╔╗╔╔═╗ // │ ├─┤├┤ │ ├┴┐ ║ ║╚═╗║║║║║ ╦ // └─┘┴ ┴└─┘└─┘┴ ┴ ╚═╝╚═╝╩╝╚╝╚═╝ // Always check `using`. if (!_.isString(query.using) || query.using === '') { throw new Error( 'Consistency violation: Every stage 1 query should include a property called `using` as a non-empty string.'+ ' But instead, got: ' + util.inspect(query.using, {depth:5}) ); }//-• // Look up the Waterline model for this query. // > This is so that we can reference the original model definition. var WLModel; try { WLModel = getModel(query.using, orm); } catch (e) { switch (e.code) { case 'E_MODEL_NOT_REGISTERED': throw new Error('Consistency violation: The specified `using` ("'+query.using+'") does not match the identity of any registered model.'); default: throw e; } }//</catch> // ┌─┐┬ ┬┌─┐┌─┐┬┌─ ╔╦╗╔═╗╔╦╗╦ ╦╔═╗╔╦╗ // │ ├─┤├┤ │ ├┴┐ ║║║║╣ ║ ╠═╣║ ║ ║║ // └─┘┴ ┴└─┘└─┘┴ ┴ ╩ ╩╚═╝ ╩ ╩ ╩╚═╝═╩╝ // ┬ ┌─┐┬ ┬┌─┐┌─┐┬┌─ ┌─┐┌─┐┬─┐ ┌─┐─┐ ┬┌┬┐┬─┐┌─┐┌┐┌┌─┐┌─┐┬ ┬┌─┐ ┬┌─┌─┐┬ ┬┌─┐ // ┌┼─ │ ├─┤├┤ │ ├┴┐ ├┤ │ │├┬┘ ├┤ ┌┴┬┘ │ ├┬┘├─┤│││├┤ │ ││ │└─┐ ├┴┐├┤ └┬┘└─┐ // └┘ └─┘┴ ┴└─┘└─┘┴ ┴ └ └─┘┴└─ └─┘┴ └─ ┴ ┴└─┴ ┴┘└┘└─┘└─┘└─┘└─┘ ┴ ┴└─┘ ┴ └─┘┘ // ┬ ┌┬┐┌─┐┌┬┐┌─┐┬─┐┌┬┐┬┌┐┌┌─┐ ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬ ┬┌─┌─┐┬ ┬┌─┐ // ┌┼─ ││├┤ │ ├┤ ├┬┘│││││││├┤ │─┼┐│ │├┤ ├┬┘└┬┘ ├┴┐├┤ └┬┘└─┐ // └┘ ─┴┘└─┘ ┴ └─┘┴└─┴ ┴┴┘└┘└─┘ └─┘└└─┘└─┘┴└─ ┴ ┴ ┴└─┘ ┴ └─┘ // Always check `method`. if (!_.isString(query.method) || query.method === '') { throw new Error( 'Consistency violation: Every stage 1 query should include a property called `method` as a non-empty string.'+ ' But instead, got: ' + util.inspect(query.method, {depth:5}) ); }//-• // Determine the set of acceptable query keys for the specified `method`. // (and, in the process, verify that we recognize this method in the first place) var queryKeys = (function _getQueryKeys (){ switch(query.method) { case 'find': return [ 'criteria', 'populates' ]; case 'findOne': return [ 'criteria', 'populates' ]; case 'stream': return [ 'criteria', 'populates', 'eachRecordFn', 'eachBatchFn' ]; case 'count': return [ 'criteria' ]; case 'sum': return [ 'numericAttrName', 'criteria' ]; case 'avg': return [ 'numericAttrName', 'criteria' ]; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: consider renaming "numericAttrName" to something like "targetField" // so that it's more descriptive even after being forged as part of a s3q. // But note that this would be a pretty big change throughout waterline core, // possibly other utilities, as well as being a breaking change to the spec // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case 'create': return [ 'newRecord' ]; case 'createEach': return [ 'newRecords' ]; case 'findOrCreate': return [ 'criteria', 'newRecord' ]; case 'update': return [ 'criteria', 'valuesToSet' ]; case 'updateOne': return [ 'criteria', 'valuesToSet' ]; case 'destroy': return [ 'criteria' ]; case 'destroyOne': return [ 'criteria' ]; case 'archive': return [ 'criteria' ]; case 'archiveOne': return [ 'criteria' ]; case 'addToCollection': return [ 'targetRecordIds', 'collectionAttrName', 'associatedIds' ]; case 'removeFromCollection': return [ 'targetRecordIds', 'collectionAttrName', 'associatedIds' ]; case 'replaceCollection': return [ 'targetRecordIds', 'collectionAttrName', 'associatedIds' ]; default: throw new Error('Consistency violation: Unrecognized `method` ("'+query.method+'")'); } })();//</self-calling function :: _getQueryKeys()> // > Note: // > // > It's OK if keys are missing at this point. We'll do our best to // > infer a reasonable default, when possible. In some cases, it'll // > still fail validation later, but in other cases, it'll pass. // > // > Anyway, that's all handled below. // Now check that we see ONLY the expected keys for that method. // (i.e. there should never be any miscellaneous stuff hanging out on the stage1 query dictionary) // We start off by building up an array of legal keys, starting with the universally-legal ones. var allowedKeys = [ 'meta', 'using', 'method' ].concat(queryKeys); // Then finally, we check that no extraneous keys are present. var extraneousKeys = _.difference(_.keys(query), allowedKeys); if (extraneousKeys.length > 0) { throw new Error('Consistency violation: Provided "stage 1 query" contains extraneous top-level keys: '+extraneousKeys); } // ███╗ ███╗███████╗████████╗ █████╗ // ████╗ ████║██╔════╝╚══██╔══╝██╔══██╗ // ██╔████╔██║█████╗ ██║ ███████║ // ██║╚██╔╝██║██╔══╝ ██║ ██╔══██║ // ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║ // ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ // // ┌─┐┬ ┬┌─┐┌─┐┬┌─ ╔╦╗╔═╗╔╦╗╔═╗ ┌─ ┬┌─┐ ┌─┐┬─┐┌─┐┬ ┬┬┌┬┐┌─┐┌┬┐ ─┐ // │ ├─┤├┤ │ ├┴┐ ║║║║╣ ║ ╠═╣ │ │├┤ ├─┘├┬┘│ │└┐┌┘│ ││├┤ ││ │ // └─┘┴ ┴└─┘└─┘┴ ┴ ╩ ╩╚═╝ ╩ ╩ ╩ └─ ┴└ ┴ ┴└─└─┘ └┘ ┴─┴┘└─┘─┴┘ ─┘ // If specified, check that `meta` is a dictionary. if (!_.isUndefined(query.meta)) { if (!_.isObject(query.meta) || _.isArray(query.meta) || _.isFunction(query.meta)) { throw buildUsageError( 'E_INVALID_META', 'If `meta` is provided, it should be a dictionary (i.e. a plain JavaScript object). '+ 'But instead, got: ' + util.inspect(query.meta, {depth:5})+'', query.using ); }//-• }//>-• // Now check a few different model settings that correspond with `meta` keys, // and set the relevant `meta` keys accordingly. // // > Remember, we rely on waterline-schema to have already validated // > these model settings when the ORM was first initialized. // ┌─┐┌─┐┌─┐┌─┐┌─┐┌┬┐┌─┐ ┌─┐┌┐┌ ┌┬┐┌─┐┌─┐┌┬┐┬─┐┌─┐┬ ┬┌─┐ // │ ├─┤└─┐│ ├─┤ ││├┤ │ ││││ ││├┤ └─┐ │ ├┬┘│ │└┬┘ ┌┘ // └─┘┴ ┴└─┘└─┘┴ ┴─┴┘└─┘ └─┘┘└┘ ─┴┘└─┘└─┘ ┴ ┴└─└─┘ ┴ o if (query.method === 'destroy' && !_.isUndefined(WLModel.cascadeOnDestroy)) { if (!_.isBoolean(WLModel.cascadeOnDestroy)) { throw new Error('Consistency violation: If specified, expecting `cascadeOnDestroy` model setting to be `true` or `false`. But instead, got: '+util.inspect(WLModel.cascadeOnDestroy, {depth:5})+''); } if (!query.meta || query.meta.cascade === undefined) { // Only bother setting the `cascade` meta key if the model setting is `true`. // (because otherwise it's `false`, which is the default anyway) if (WLModel.cascadeOnDestroy) { query.meta = query.meta || {}; query.meta.cascade = WLModel.cascadeOnDestroy; } }//fi }//>- // ┌─┐┌─┐┌┬┐┌─┐┬ ┬ ┬─┐┌─┐┌─┐┌─┐┬─┐┌┬┐┌─┐ ┌─┐┌┐┌ ┬ ┬┌─┐┌┬┐┌─┐┌┬┐┌─┐┌─┐ // ├┤ ├┤ │ │ ├─┤ ├┬┘├┤ │ │ │├┬┘ ││└─┐ │ ││││ │ │├─┘ ││├─┤ │ ├┤ ┌┘ // └ └─┘ ┴ └─┘┴ ┴ ┴└─└─┘└─┘└─┘┴└──┴┘└─┘ └─┘┘└┘ └─┘┴ ─┴┘┴ ┴ ┴ └─┘ o if (query.method === 'update' && !_.isUndefined(WLModel.fetchRecordsOnUpdate)) { if (!_.isBoolean(WLModel.fetchRecordsOnUpdate)) { throw new Error('Consistency violation: If specified, expecting `fetchRecordsOnUpdate` model setting to be `true` or `false`. But instead, got: '+util.inspect(WLModel.fetchRecordsOnUpdate, {depth:5})+''); } if (!query.meta || query.meta.fetch === undefined) { // Only bother setting the `fetch` meta key if the model setting is `true`. // (because otherwise it's `false`, which is the default anyway) if (WLModel.fetchRecordsOnUpdate) { query.meta = query.meta || {}; query.meta.fetch = WLModel.fetchRecordsOnUpdate; } }//fi }//>- // ┌─┐┌─┐┌┬┐┌─┐┬ ┬ ┬─┐┌─┐┌─┐┌─┐┬─┐┌┬┐┌─┐ ┌─┐┌┐┌ ┌┬┐┌─┐┌─┐┌┬┐┬─┐┌─┐┬ ┬┌─┐ // ├┤ ├┤ │ │ ├─┤ ├┬┘├┤ │ │ │├┬┘ ││└─┐ │ ││││ ││├┤ └─┐ │ ├┬┘│ │└┬┘ ┌┘ // └ └─┘ ┴ └─┘┴ ┴ ┴└─└─┘└─┘└─┘┴└──┴┘└─┘ └─┘┘└┘ ─┴┘└─┘└─┘ ┴ ┴└─└─┘ ┴ o if (query.method === 'destroy' && !_.isUndefined(WLModel.fetchRecordsOnDestroy)) { if (!_.isBoolean(WLModel.fetchRecordsOnDestroy)) { throw new Error('Consistency violation: If specified, expecting `fetchRecordsOnDestroy` model setting to be `true` or `false`. But instead, got: '+util.inspect(WLModel.fetchRecordsOnDestroy, {depth:5})+''); } if (!query.meta || query.meta.fetch === undefined) { // Only bother setting the `fetch` meta key if the model setting is `true`. // (because otherwise it's `false`, which is the default anyway) if (WLModel.fetchRecordsOnDestroy) { query.meta = query.meta || {}; query.meta.fetch = WLModel.fetchRecordsOnDestroy; } }//fi }//>- // ┌─┐┌─┐┌┬┐┌─┐┬ ┬ ┬─┐┌─┐┌─┐┌─┐┬─┐┌┬┐┌─┐ ┌─┐┌┐┌ ┌─┐┬─┐┌─┐┌─┐┌┬┐┌─┐┌─┐ // ├┤ ├┤ │ │ ├─┤ ├┬┘├┤ │ │ │├┬┘ ││└─┐ │ ││││ │ ├┬┘├┤ ├─┤ │ ├┤ ┌┘ // └ └─┘ ┴ └─┘┴ ┴ ┴└─└─┘└─┘└─┘┴└──┴┘└─┘ └─┘┘└┘ └─┘┴└─└─┘┴ ┴ ┴ └─┘ o if (query.method === 'create' && !_.isUndefined(WLModel.fetchRecordsOnCreate)) { if (!_.isBoolean(WLModel.fetchRecordsOnCreate)) { throw new Error('Consistency violation: If specified, expecting `fetchRecordsOnCreate` model setting to be `true` or `false`. But instead, got: '+util.inspect(WLModel.fetchRecordsOnCreate, {depth:5})+''); } if (!query.meta || query.meta.fetch === undefined) { // Only bother setting the `fetch` meta key if the model setting is `true`. // (because otherwise it's `false`, which is the default anyway) if (WLModel.fetchRecordsOnCreate) { query.meta = query.meta || {}; query.meta.fetch = WLModel.fetchRecordsOnCreate; } }//fi }//>- // ┌─┐┌─┐┌┬┐┌─┐┬ ┬ ┬─┐┌─┐┌─┐┌─┐┬─┐┌┬┐┌─┐ ┌─┐┌┐┌ ┌─┐┬─┐┌─┐┌─┐┌┬┐┌─┐ ┌─┐┌─┐┌─┐┬ ┬┌─┐ // ├┤ ├┤ │ │ ├─┤ ├┬┘├┤ │ │ │├┬┘ ││└─┐ │ ││││ │ ├┬┘├┤ ├─┤ │ ├┤ ├┤ ├─┤│ ├─┤ ┌┘ // └ └─┘ ┴ └─┘┴ ┴ ┴└─└─┘└─┘└─┘┴└──┴┘└─┘ └─┘┘└┘ └─┘┴└─└─┘┴ ┴ ┴ └─┘ └─┘┴ ┴└─┘┴ ┴ o if (query.method === 'createEach' && !_.isUndefined(WLModel.fetchRecordsOnCreateEach)) { if (!_.isBoolean(WLModel.fetchRecordsOnCreateEach)) { throw new Error('Consistency violation: If specified, expecting `fetchRecordsOnCreateEach` model setting to be `true` or `false`. But instead, got: '+util.inspect(WLModel.fetchRecordsOnCreateEach, {depth:5})+''); } if (!query.meta || query.meta.fetch === undefined) { // Only bother setting the `fetch` meta key if the model setting is `true`. // (because otherwise it's `false`, which is the default anyway) if (WLModel.fetchRecordsOnCreateEach) { query.meta = query.meta || {}; query.meta.fetch = WLModel.fetchRecordsOnCreateEach; } } }//>- // ┌─┐┬─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌┬┐┌─┐ ┌┐┌┌─┐┌┐┌ ┌─┐┌┐ ┬┌─┐┌─┐┌┬┐ ┬┌┬┐ ┌┬┐┌─┐┬ ┌─┐┬─┐┌─┐┌┐┌┌─┐┌─┐ // ├─┘├┬┘│ │├─┘├─┤│ ┬├─┤ │ ├┤ ││││ ││││───│ │├┴┐ │├┤ │ │───│ ││ │ │ ││ ├┤ ├┬┘├─┤││││ ├┤ // ┴ ┴└─└─┘┴ ┴ ┴└─┘┴ ┴ ┴ └─┘ ┘└┘└─┘┘└┘ └─┘└─┘└┘└─┘└─┘ ┴ ┴─┴┘ ┴ └─┘┴─┘└─┘┴└─┴ ┴┘└┘└─┘└─┘ // ┌┬┐┌─┐┌┬┐┌─┐┬ ┌─┐┌─┐┌┬┐┌┬┐┬┌┐┌┌─┐ ┌┬┐┌─┐ ┌┬┐┬ ┬┌─┐ ┌─┐┌─┐┌─┐┬─┐┌─┐┌─┐┬─┐┬┌─┐┌┬┐┌─┐ // ││││ │ ││├┤ │ └─┐├┤ │ │ │││││ ┬ │ │ │ │ ├─┤├┤ ├─┤├─┘├─┘├┬┘│ │├─┘├┬┘│├─┤ │ ├┤ // ┴ ┴└─┘─┴┘└─┘┴─┘ └─┘└─┘ ┴ ┴ ┴┘└┘└─┘ ┴ └─┘ ┴ ┴ ┴└─┘ ┴ ┴┴ ┴ ┴└─└─┘┴ ┴└─┴┴ ┴ ┴ └─┘ // ┌┬┐┌─┐┌┬┐┌─┐ ┬┌─┌─┐┬ ┬ ┌─ ┌─┐┌─┐┬─┐ ┌┬┐┌─┐┌┐┌┌─┐┌─┐ ─┐ // │││├┤ │ ├─┤ ├┴┐├┤ └┬┘ │ ├┤ │ │├┬┘ ││││ │││││ ┬│ │ │ // ┴ ┴└─┘ ┴ ┴ ┴ ┴ ┴└─┘ ┴ └─ └ └─┘┴└─ ┴ ┴└─┘┘└┘└─┘└─┘ ─┘ // Set the `modelsNotUsingObjectIds` meta key of the query based on // the `dontUseObjectIds` model setting of relevant models. // // Note that if no models have this flag set, the meta key won't be set at all. // This avoids the weirdness of seeing this key pop up in a query for a non-mongo adapter. // // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Remove the need for this mongo-specific code by respecting this model setting // in the adapter itself. (To do that, Waterline needs to be sending down actual WL models // though. See the waterline.js file in this repo for notes about that.) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (function() { var modelsNotUsingObjectIds = _.reduce(orm.collections, function(memo, WLModel) { if (WLModel.dontUseObjectIds === true) { memo.push(WLModel.identity); } return memo; }, []); if (modelsNotUsingObjectIds.length > 0) { query.meta = query.meta || {}; query.meta.modelsNotUsingObjectIds = modelsNotUsingObjectIds; } })(); // Next, check specific, common `meta` keys, to make sure they're valid. // > (Not all `meta` keys can be checked, obviously, because there could be **anything** // > in there, such as meta keys proprietary to particular adapters. But certain core // > `meta` keys can be properly verified. Currently, we only validate _some_ of the // > ones that are more commonly used.) if (query.meta !== undefined) { // ┌─┐┌─┐┌┬┐┌─┐┬ ┬ // ├┤ ├┤ │ │ ├─┤ // └ └─┘ ┴ └─┘┴ ┴ if (query.meta.fetch !== undefined) { if (!_.isBoolean(query.meta.fetch)) { throw buildUsageError( 'E_INVALID_META', 'If provided, `fetch` should be either `true` or `false`.', query.using ); }//• // If this is a findOrCreate/updateOne/destroyOne/archiveOne query, // make sure that the `fetch` meta key hasn't been explicitly set // (because that wouldn't make any sense). if (_.contains(['findOrCreate', 'updateOne', 'destroyOne', 'archiveOne'], query.method)) { console.warn( 'warn: `fetch` is unnecessary when calling .'+query.method+'(). '+ 'If successful, .'+query.method+'() *always* returns the affected record.' ); }//fi }//fi // ┌┬┐┬ ┬┌┬┐┌─┐┌┬┐┌─┐ ┌─┐┬─┐┌─┐┌─┐ // ││││ │ │ ├─┤ │ ├┤ ├─┤├┬┘│ ┬└─┐ // ┴ ┴└─┘ ┴ ┴ ┴ ┴ └─┘ ┴ ┴┴└─└─┘└─┘ // // EXPERIMENTAL: The `mutateArgs` meta key enabled optimizations by preventing // unnecessary cloning of arguments. // // > Note that this is ONLY respected at the stage 2 level! // > That is, it doesn't matter if this meta key is set or not when you call adapters. // // > PLEASE DO NOT RELY ON `mutateArgs` IN YOUR OWN CODE- IT COULD CHANGE // > AT ANY TIME AND BREAK YOUR APP OR PLUGIN! if (query.meta.mutateArgs !== undefined) { if (!_.isBoolean(query.meta.mutateArgs)) { throw buildUsageError( 'E_INVALID_META', 'If provided, `mutateArgs` should be either `true` or `false`.', query.using ); }//• }//fi // ┌┬┐┌─┐┌─┐┬─┐┬ ┬┌─┐┌┬┐ // ││├┤ │ ├┬┘└┬┘├─┘ │ // ─┴┘└─┘└─┘┴└─ ┴ ┴ ┴ if (query.meta.decrypt !== undefined) { if (!_.isBoolean(query.meta.decrypt)) { throw buildUsageError( 'E_INVALID_META', 'If provided, `decrypt` should be either `true` or `false`.', query.using ); }//• }//fi // ┌─┐┌┐┌┌─┐┬─┐┬ ┬┌─┐┌┬┐┬ ┬┬┌┬┐┬ ┬ // ├┤ ││││ ├┬┘└┬┘├─┘ │ ││││ │ ├─┤ // └─┘┘└┘└─┘┴└─ ┴ ┴ ┴ └┴┘┴ ┴ ┴ ┴ if (query.meta.encryptWith !== undefined) { if (!query.meta.encryptWith || !_.isString(query.meta.encryptWith)) { throw buildUsageError( 'E_INVALID_META', 'If provided, `encryptWith` should be a non-empty string (the name of '+ 'one of the configured data encryption keys).', query.using ); }//• }//fi // ┌─┐┬┌─┬┌─┐┌─┐┌┐┌┌─┐┬─┐┬ ┬┌─┐┌┬┐┬┌─┐┌┐┌ // └─┐├┴┐│├─┘├┤ ││││ ├┬┘└┬┘├─┘ │ ││ ││││ // └─┘┴ ┴┴┴ └─┘┘└┘└─┘┴└─ ┴ ┴ ┴ ┴└─┘┘└┘ // // EXPERIMENTAL: The `skipEncryption` meta key prevents encryption. // (see the implementation of findOrCreate() for more information) // // > PLEASE DO NOT RELY ON `skipEncryption` IN YOUR OWN CODE- IT COULD // > CHANGE AT ANY TIME AND BREAK YOUR APP OR PLUGIN! if (query.meta.skipEncryption !== undefined) { if (!_.isBoolean(query.meta.skipEncryption)) { throw buildUsageError( 'E_INVALID_META', 'If provided, `skipEncryption` should be true or false.', query.using ); }//• }//fi // ┌┐ ┌─┐┌┬┐┌─┐┬ ┬┌─┐┬┌─┐┌─┐ // ├┴┐├─┤ │ │ ├─┤└─┐│┌─┘├┤ // └─┘┴ ┴ ┴ └─┘┴ ┴└─┘┴└─┘└─┘ if (query.meta.batchSize !== undefined) { if (!_.isNumber(query.meta.batchSize) || !isSafeNaturalNumber(query.meta.batchSize)) { throw buildUsageError( 'E_INVALID_META', 'If provided, `batchSize` should be a whole, positive, safe, and natural integer. '+ 'Instead, got '+util.inspect(query.meta.batchSize, {depth: null})+'.', query.using ); }//• if (query.method !== 'stream') { // FUTURE: consider changing this usage error to a warning instead. throw buildUsageError( 'E_INVALID_META', '`batchSize` cannot be used with .'+query.method+'() -- it is only compatible '+ 'with the .stream() model method.', query.using ); }//• }//fi // … }//fi //-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - // -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- //- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- //-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - // -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- //- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- //-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - // ██████╗██████╗ ██╗████████╗███████╗██████╗ ██╗ █████╗ // ██╔════╝██╔══██╗██║╚══██╔══╝██╔════╝██╔══██╗██║██╔══██╗ // ██║ ██████╔╝██║ ██║ █████╗ ██████╔╝██║███████║ // ██║ ██╔══██╗██║ ██║ ██╔══╝ ██╔══██╗██║██╔══██║ // ╚██████╗██║ ██║██║ ██║ ███████╗██║ ██║██║██║ ██║ // ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ // if (_.contains(queryKeys, 'criteria')) { // ╔═╗╔═╗╔═╗╔═╗╦╔═╗╦ ╔═╗╔═╗╔═╗╔═╗╔═╗ // ╚═╗╠═╝║╣ ║ ║╠═╣║ ║ ╠═╣╚═╗║╣ ╚═╗ // ╚═╝╩ ╚═╝╚═╝╩╩ ╩╩═╝ ╚═╝╩ ╩╚═╝╚═╝╚═╝ // ┌─ ┬ ┌─┐ ┬ ┬┌┐┌┌─┐┬ ┬┌─┐┌─┐┌─┐┬─┐┌┬┐┌─┐┌┬┐ ┌─┐┌─┐┌┬┐┌┐ ┬┌┐┌┌─┐┌┬┐┬┌─┐┌┐┌┌─┐ ┌─┐┌─┐ // │─── │ ├┤ │ ││││└─┐│ │├─┘├─┘│ │├┬┘ │ ├┤ ││ │ │ ││││├┴┐││││├─┤ │ ││ ││││└─┐ │ │├┤ // └─ ┴o└─┘o └─┘┘└┘└─┘└─┘┴ ┴ └─┘┴└─ ┴ └─┘─┴┘ └─┘└─┘┴ ┴└─┘┴┘└┘┴ ┴ ┴ ┴└─┘┘└┘└─┘ └─┘└ // ┌─┐┌─┐┬─┐┌┬┐┌─┐┬┌┐┌ ┌─┐┬─┐┬┌┬┐┌─┐┬─┐┬┌─┐ ┌─┐┬ ┌─┐┬ ┬┌─┐┌─┐┌─┐ ┌─┐┌─┐┬─┐ // │ ├┤ ├┬┘ │ ├─┤││││ │ ├┬┘│ │ ├┤ ├┬┘│├─┤ │ │ ├─┤│ │└─┐├┤ └─┐ ├┤ │ │├┬┘ // └─┘└─┘┴└─ ┴ ┴ ┴┴┘└┘ └─┘┴└─┴ ┴ └─┘┴└─┴┴ ┴ └─┘┴─┘┴ ┴└─┘└─┘└─┘└─┘ └ └─┘┴└─ // ┌─┐┌─┐┌─┐┌─┐┬┌─┐┬┌─┐ ┌┬┐┌─┐┌┬┐┌─┐┬ ┌┬┐┌─┐┌┬┐┬ ┬┌─┐┌┬┐┌─┐ ─┐ // └─┐├─┘├┤ │ │├┤ ││ ││││ │ ││├┤ │ │││├┤ │ ├─┤│ │ ││└─┐ ───│ // └─┘┴ └─┘└─┘┴└ ┴└─┘ ┴ ┴└─┘─┴┘└─┘┴─┘ ┴ ┴└─┘ ┴ ┴ ┴└─┘─┴┘└─┘ ─┘ // // Next, handle a few special cases that we are careful to fail loudly about. // // > Because if we don't, it can cause major confusion. Think about it: in some cases, // > certain usage can seem intuitive, and like a reasonable enough thing to try out... // > ...but it might actually be unsupported. // > // > When you do try it out, unless it fails LOUDLY, then you could easily end // > up believing that it is actually doing something. And then, as is true when // > working w/ any library or framework, you end up with all sorts of weird superstitions // > and false assumptions that take a long time to wring out of your code base. // > So let's do our best to prevent that. // // > WARNING: // > It is really important that we do this BEFORE we normalize the criteria! // > (Because by then, it'll be too late to tell what was and wasn't included // > in the original, unnormalized criteria dictionary.) // // If the criteria explicitly specifies `select` or `omit`, then make sure the query method // is actually compatible with those clauses. if (_.isObject(query.criteria) && !_.isArray(query.criteria) && (!_.isUndefined(query.criteria.select) || !_.isUndefined(query.criteria.omit))) { var PROJECTION_COMPATIBLE_METHODS = ['find', 'findOne', 'stream']; var isCompatibleWithProjections = _.contains(PROJECTION_COMPATIBLE_METHODS, query.method); if (!isCompatibleWithProjections) { throw buildUsageError('E_INVALID_CRITERIA', 'Cannot use `select`/`omit` with this method (`'+query.method+'`).', query.using); } }//>-• // If the criteria explicitly specifies `limit`, `skip`, or `sort`, then make sure // the query method is actually compatible with those clauses. if (_.isObject(query.criteria) && !_.isArray(query.criteria) && (!_.isUndefined(query.criteria.limit) || !_.isUndefined(query.criteria.skip) || !_.isUndefined(query.criteria.sort))) { var PAGINATION_COMPATIBLE_METHODS = ['find', 'stream']; var isCompatibleWithLimit = _.contains(PAGINATION_COMPATIBLE_METHODS, query.method); if (!isCompatibleWithLimit) { throw buildUsageError('E_INVALID_CRITERIA', 'Cannot use `limit`, `skip`, or `sort` with this method (`'+query.method+'`).', query.using); } }//>-• // If the criteria is not defined, then in most cases, we treat it like `{}`. // BUT if this query will be running as a result of an `update()`, or a `destroy()`, // or an `.archive()`, then we'll be a bit more picky in order to prevent accidents. if (_.isUndefined(query.criteria) && (query.method === 'update' || query.method === 'destroy' || query.method === 'archive')) { throw buildUsageError('E_INVALID_CRITERIA', 'Cannot use this method (`'+query.method+'`) with a criteria of `undefined`. (This is just a simple failsafe to help protect your data: if you really want to '+query.method+' ALL records, no problem-- please just be explicit and provide a criteria of `{}`.)', query.using); }//>-• // ╔╦╗╔═╗╔═╗╔═╗╦ ╦╦ ╔╦╗ // ║║║╣ ╠╣ ╠═╣║ ║║ ║ // ═╩╝╚═╝╚ ╩ ╩╚═╝╩═╝╩ // Tolerate this being left undefined by inferring a reasonable default. // (This will be further processed below.) if (_.isUndefined(query.criteria)) { query.criteria = {}; }//>- // ╔╗╔╔═╗╦═╗╔╦╗╔═╗╦ ╦╔═╗╔═╗ ┬ ╦ ╦╔═╗╦ ╦╔╦╗╔═╗╔╦╗╔═╗ // ║║║║ ║╠╦╝║║║╠═╣║ ║╔═╝║╣ ┌┼─ ╚╗╔╝╠═╣║ ║ ║║╠═╣ ║ ║╣ // ╝╚╝╚═╝╩╚═╩ ╩╩ ╩╩═╝╩╚═╝╚═╝ └┘ ╚╝ ╩ ╩╩═╝╩═╩╝╩ ╩ ╩ ╚═╝ // Validate and normalize the provided `criteria`. try { query.criteria = normalizeCriteria(query.criteria, query.using, orm, query.meta); } catch (e) { switch (e.code) { case 'E_HIGHLY_IRREGULAR': throw buildUsageError('E_INVALID_CRITERIA', e.message, query.using); case 'E_WOULD_RESULT_IN_NOTHING': throw buildUsageError('E_NOOP', 'The provided criteria would not match any records. '+e.message, query.using); // If no error code (or an unrecognized error code) was specified, // then we assume that this was a spectacular failure do to some // kind of unexpected, internal error on our part. default: throw new Error('Consistency violation: Encountered unexpected internal error when attempting to normalize/validate the provided criteria:\n```\n'+util.inspect(query.criteria, {depth:5})+'\n```\nAnd here is the actual error itself:\n```\n'+e.stack+'\n```'); } }//>-• // ┌─┐┬ ┬ ┬┌─┐┬ ┬┌─┐ ┌─┐┌─┐┬─┐┌─┐┌─┐ ╦ ╦╔╦╗╦╔╦╗ ┌┬┐┌─┐ ╔╦╗╦ ╦╔═╗ // ├─┤│ │││├─┤└┬┘└─┐ ├┤ │ │├┬┘│ ├┤ ║ ║║║║║ ║ │ │ │ ║ ║║║║ ║ // ┴ ┴┴─┘└┴┘┴ ┴ ┴ └─┘ └ └─┘┴└─└─┘└─┘ ╩═╝╩╩ ╩╩ ╩ ┴ └─┘ ╩ ╚╩╝╚═╝ // ┌─ ┬┌─┐ ┌┬┐┬ ┬┬┌─┐ ┬┌─┐ ┌─┐ ╔═╗╦╔╗╔╔╦╗ ╔═╗╔╗╔╔═╗ ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬ ─┐ // │─── │├┤ │ ├─┤│└─┐ │└─┐ ├─┤ ╠╣ ║║║║ ║║ ║ ║║║║║╣ │─┼┐│ │├┤ ├┬┘└┬┘ ───│ // └─ ┴└ ┴ ┴ ┴┴└─┘ ┴└─┘ ┴ ┴ ╚ ╩╝╚╝═╩╝ ╚═╝╝╚╝╚═╝ └─┘└└─┘└─┘┴└─ ┴ ─┘ // Last but not least, if the current method is `findOne`, then set `limit: 2`. // // > This is a performance/stability check that prevents accidentally fetching the entire database // > with queries like `.findOne({})`. If > 1 record is found, the findOne will fail w/ an error // > anyway, so it only makes sense to fetch _just enough_. if (query.method === 'findOne') { query.criteria.limit = 2; }//>- // ┌─┐┌┐┌┌─┐┬ ┬┬─┐┌─┐ ╦ ╦╦ ╦╔═╗╦═╗╔═╗ ┌─┐┬ ┌─┐┬ ┬┌─┐┌─┐ ┬┌─┐ ┌─┐┌─┐┌─┐┌─┐┬┌─┐┬┌─┐ // ├┤ │││└─┐│ │├┬┘├┤ ║║║╠═╣║╣ ╠╦╝║╣ │ │ ├─┤│ │└─┐├┤ │└─┐ └─┐├─┘├┤ │ │├┤ ││ // └─┘┘└┘└─┘└─┘┴└─└─┘ ╚╩╝╩ ╩╚═╝╩╚═╚═╝ └─┘┴─┘┴ ┴└─┘└─┘└─┘ ┴└─┘ └─┘┴ └─┘└─┘┴└ ┴└─┘ // ┌─ ┬┌─┐ ┌┬┐┬ ┬┬┌─┐ ┬┌─┐ ┌─┐ \│/╔═╗╔╗╔╔═╗ ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬ ─┐ // │─── │├┤ │ ├─┤│└─┐ │└─┐ ├─┤ ─ ─║ ║║║║║╣ │─┼┐│ │├┤ ├┬┘└┬┘ ───│ // └─ ┴└ ┴ ┴ ┴┴└─┘ ┴└─┘ ┴ ┴ o/│\╚═╝╝╚╝╚═╝ └─┘└└─┘└─┘┴└─ ┴ ─┘ // If this is a `findOne`/`updateOne`/`destroyOne`/`archiveOne` query, // and the `where` clause is not defined, or if it is `{}`, then fail // with a usage error (for clarity's sake). if (_.contains(['findOne','updateOne','destroyOne','archiveOne'], query.method) && _.isEqual(query.criteria.where, {})) { throw buildUsageError( 'E_INVALID_CRITERIA', 'Cannot `'+query.method+'()` without specifying a more specific `where` clause (the provided `where` clause, `{}`, is too broad).'+ (query.method === 'findOne' ? ' (If you want to work around this, use `.find().limit(1)`.)' : ''), query.using ); }//>-• }// >-• // ██████╗ ██████╗ ██████╗ ██╗ ██╗██╗ █████╗ ████████╗███████╗███████╗ // ██╔══██╗██╔═══██╗██╔══██╗██║ ██║██║ ██╔══██╗╚══██╔══╝██╔════╝██╔════╝ // ██████╔╝██║ ██║██████╔╝██║ ██║██║ ███████║ ██║ █████╗ ███████╗ // ██╔═══╝ ██║ ██║██╔═══╝ ██║ ██║██║ ██╔══██║ ██║ ██╔══╝ ╚════██║ // ██║ ╚██████╔╝██║ ╚██████╔╝███████╗██║ ██║ ██║ ███████╗███████║ // ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝ // // Validate/normalize the `populates` query key. // // > NOTE: At this point, we know that the `criteria` query key has already been checked/normalized. if (_.contains(queryKeys, 'populates')) { // Tolerate this being left undefined by inferring a reasonable default. if (_.isUndefined(query.populates)) { query.populates = {}; }//>- // Verify that `populates` is a dictionary. if (!_.isObject(query.populates) || _.isArray(query.populates) || _.isFunction(query.populates)) { throw buildUsageError( 'E_INVALID_POPULATES', '`populates` must be a dictionary. But instead, got: '+util.inspect(query.populates, {depth: 1}), query.using ); }//-• // For each key in our `populates` dictionary... _.each(_.keys(query.populates), function (populateAttrName) { // For convenience/consistency, if the RHS of this "populate" directive was set // to `false`/`undefined`, understand it to mean the same thing as if this particular // populate directive wasn't included in the first place. In other words, strip // this key from the `populates` dictionary and just return early. if (query.populates[populateAttrName] === false || _.isUndefined(query.populates[populateAttrName])) { delete query.populates[populateAttrName]; return; }//-• // ┬ ┌─┐┌─┐┬┌─ ┬ ┬┌─┐ ╔═╗╔╦╗╔╦╗╦═╗ ╔╦╗╔═╗╔═╗ ┌─┐┌─┐┬─┐ ┌─┐┌─┐┌─┐┌─┐┌─┐┬┌─┐┌┬┐┬┌─┐┌┐┌ // │ │ ││ │├┴┐ │ │├─┘ ╠═╣ ║ ║ ╠╦╝ ║║║╣ ╠╣ ├┤ │ │├┬┘ ├─┤└─┐└─┐│ ││ │├─┤ │ ││ ││││ // ┴─┘└─┘└─┘┴ ┴ └─┘┴ ╩ ╩ ╩ ╩ ╩╚═ ═╩╝╚═╝╚ └ └─┘┴└─ ┴ ┴└─┘└─┘└─┘└─┘┴┴ ┴ ┴ ┴└─┘┘└┘ // Look up the attribute definition for the association being populated. // (at the same time, validating that an association by this name actually exists in this model definition.) var populateAttrDef; try { populateAttrDef = getAttribute(populateAttrName, query.using, orm); } catch (e) { switch (e.code) { case 'E_ATTR_NOT_REGISTERED': throw buildUsageError( 'E_INVALID_POPULATES', 'Could not populate `'+populateAttrName+'`. '+ 'There is no attribute named `'+populateAttrName+'` defined in this model.', query.using ); default: throw new Error('Consistency violation: When attempting to populate `'+populateAttrName+'` for this model (`'+query.using+'`), an unexpected error occurred looking up the association\'s definition. This SHOULD never happen. Here is the original error:\n```\n'+e.stack+'\n```'); } }//</catch> // ┬ ┌─┐┌─┐┬┌─ ┬ ┬┌─┐ ┬┌┐┌┌─┐┌─┐ ┌─┐┌┐┌ ┌┬┐┬ ┬┌─┐ ╔═╗╔╦╗╦ ╦╔═╗╦═╗ ╔╦╗╔═╗╔╦╗╔═╗╦ // │ │ ││ │├┴┐ │ │├─┘ ││││├┤ │ │ │ ││││ │ ├─┤├┤ ║ ║ ║ ╠═╣║╣ ╠╦╝ ║║║║ ║ ║║║╣ ║ // ┴─┘└─┘└─┘┴ ┴ └─┘┴ ┴┘└┘└ └─┘ └─┘┘└┘ ┴ ┴ ┴└─┘ ╚═╝ ╩ ╩ ╩╚═╝╩╚═ ╩ ╩╚═╝═╩╝╚═╝╩═╝ // Determine the identity of the other (associated) model, then use that to make // sure that the other model's definition is actually registered in our `orm`. var otherModelIdentity; if (populateAttrDef.model) { otherModelIdentity = populateAttrDef.model; }//‡ else if (populateAttrDef.collection) { otherModelIdentity = populateAttrDef.collection; }//‡ // Otherwise, this query is invalid, since the attribute with this name is // neither a "collection" nor a "model" association. else { throw buildUsageError( 'E_INVALID_POPULATES', 'Could not populate `'+populateAttrName+'`. '+ 'The attribute named `'+populateAttrName+'` defined in this model (`'+query.using+'`) '+ 'is not defined as a "collection" or "model" association, and thus cannot '+ 'be populated. Instead, its definition looks like this:\n'+ util.inspect(populateAttrDef, {depth: 1}), query.using ); }//>-• // ┬ ┬┌─┐ ╔═╗╦═╗╦╔╦╗╔═╗╦═╗╦ ╦ ╔═╗╦═╗╦╔╦╗╔═╗╦═╗╦╔═╗ // └┐┌┘└─┐ ╠═╝╠╦╝║║║║╠═╣╠╦╝╚╦╝ ║ ╠╦╝║ ║ ║╣ ╠╦╝║╠═╣ // └┘ └─┘o ╩ ╩╚═╩╩ ╩╩ ╩╩╚═ ╩ ╚═╝╩╚═╩ ╩ ╚═╝╩╚═╩╩ ╩ // If trying to populate an association that is ALSO being omitted (in the primary criteria), // then we say this is invalid. // // > We know that the primary criteria has been normalized already at this point. // > Note: You can NEVER `select` or `omit` plural associations anyway, but that's // > already been dealt with above from when we normalized the criteria. if (_.contains(query.criteria.omit, populateAttrName)) { throw buildUsageError( 'E_INVALID_POPULATES', 'Could not populate `'+populateAttrName+'`. '+ 'This query also indicates that this attribute should be omitted. '+ 'Cannot populate AND omit an association at the same time!', query.using ); }//-• // If trying to populate an association that was included in an explicit `select` clause // in the primary criteria, then gracefully modify that select clause so that it is NOT included. // (An explicit `select` clause is only used for singular associations that AREN'T populated.) // // > We know that the primary criteria has been normalized already at this point. if (query.criteria.select[0] !== '*' && _.contains(query.criteria.select, populateAttrName)) { _.remove(query.criteria.select, populateAttrName); }//>- // If trying to populate an association that was ALSO included in an explicit // `sort` clause in the primary criteria, then don't allow this to be populated. // // > We know that the primary criteria has been normalized already at this point. var isMentionedInPrimarySort = _.any(query.criteria.sort, function (comparatorDirective){ var sortBy = _.keys(comparatorDirective)[0]; return (sortBy === populateAttrName); }); if (isMentionedInPrimarySort) { throw buildUsageError( 'E_INVALID_POPULATES', 'Could not populate `'+populateAttrName+'`. '+ 'Cannot populate AND sort by an association at the same time!', query.using ); }//>- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Similar to the above... // // FUTURE: Verify that trying to populate a association that was ALSO referenced somewhere // from within the `where` clause in the primary criteria (i.e. as an fk) works properly. // (This is an uncommon use case, and is not currently officially supported.) // // > Note that we already throw out any attempts to filter based on a plural ("collection") // > association, whether it's populated or not-- but that's taken care of separately in // > normalizeCriteria(). // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // ┌─┐┬ ┬┌─┐┌─┐┬┌─ ┌┬┐┬ ┬┌─┐ ╦═╗╦ ╦╔═╗ // │ ├─┤├┤ │ ├┴┐ │ ├─┤├┤ ╠╦╝╠═╣╚═╗ // └─┘┴ ┴└─┘└─┘┴ ┴ ┴ ┴ ┴└─┘ ╩╚═╩ ╩╚═╝ // If this is a singular ("model") association, then it should always have // an empty dictionary on the RHS. (For this type of association, there is // always either exactly one associated record, or none of them.) if (populateAttrDef.model) { // Tolerate a subcriteria of `{}`, interpreting it to mean that there is // really no criteria at all, and that we should just use `true` (the // default "enabled" value for singular "model" associations.) if (_.isEqual(query.populates[populateAttrName], {})) { query.populates[populateAttrName] = true; } // Otherwise, this simply must be `true`. Otherwise it's invalid. else { if (query.populates[populateAttrName] !== true) { throw buildUsageError( 'E_INVALID_POPULATES', 'Could not populate `'+populateAttrName+'`. '+ 'This is a singular ("model") association, which means it never refers to '+ 'more than _one_ associated record. So passing in subcriteria (i.e. as '+ 'the second argument to `.populate()`) is not supported for this association, '+ 'since it generally wouldn\'t make any sense. But that\'s the trouble-- it '+ 'looks like some sort of a subcriteria (or something) _was_ provided!\n'+ '(Note that subcriterias consisting ONLY of `omit` or `select` are a special '+ 'case that _does_ make sense. This usage will be supported in a future version '+ 'of Waterline.)\n'+ '\n'+ 'Here\'s what was passed in:\n'+ util.inspect(query.populates[populateAttrName], {depth: 5}), query.using ); }//-• }//>-• } // Otherwise, this is a plural ("collection") association, so we'll need to // validate and fully-normalize the provided subcriteria. else { // For compatibility, interpet a subcriteria of `true` to mean that there // is really no subcriteria at all, and that we should just use the default (`{}`). // > This will be further expanded into a fully-formed criteria dictionary shortly. if (query.populates[populateAttrName] === true) { query.populates[populateAttrName] = {}; }//>- // Track whether `sort` was effectively omitted from the subcriteria. // (this is used just a little ways down below.) // // > Be sure to see "FUTURE (1)" for details about how we might improve this in // > the future-- it's not a 100% accurate or clean check right now!! var isUsingDefaultSort = ( !_.isObject(query.populates[populateAttrName]) || _.isUndefined(query.populates[populateAttrName].sort) || _.isEqual(query.populates[populateAttrName].sort, []) ); // Validate and normalize the provided subcriteria. try { query.populates[populateAttrName] = normalizeCriteria(query.populates[populateAttrName], otherModelIdentity, orm, query.meta); } catch (e) { switch (e.code) { case 'E_HIGHLY_IRREGULAR': throw buildUsageError( 'E_INVALID_POPULATES', 'Could not use the specified subcriteria for populating `'+populateAttrName+'`: '+e.message, // (Tip: Instead of that ^^^, when debugging Waterline itself, replace `e.message` with `e.stack`) query.using ); case 'E_WOULD_RESULT_IN_NOTHING': // If the criteria indicates this populate would result in nothing, then set it to // `false` - a special value indicating that it is a no-op. // > • In Waterline's operation builder, whenever we see a subcriteria of `false`, // > we simply skip the populate (i.e. don't factor it in to our stage 3 queries) // > • And in the transformer, whenever we're putting back together a result set, // > and we see a subcriteria of `false` from the original stage 2 query, then // > we ensure that the virtual attributes comes back set to `[]` in the resulting // > record. query.populates[populateAttrName] = false; // And then return early from this iteration of our loop to skip further checks // for this populate (since they won't be relevant anyway) return; // If no error code (or an unrecognized error code) was specified, // then we assume that this was a spectacular failure do to some // kind of unexpected, internal error on our part. default: throw new Error('Consistency violation: Encountered unexpected internal error when attempting to normalize/validate the provided criteria for populating `'+populateAttrName+'`:\n```\n'+util.inspect(query.populates[populateAttrName], {depth:5})+'\n```\nThe following error occurred:\n```\n'+e.stack+'\n```'); } }//>-• // ┌─┐┬─┐┌─┐┌┬┐┬ ┬┌─┐┌┬┐┬┌─┐┌┐┌ ┌─┐┬ ┬┌─┐┌─┐┬┌─ // ├─┘├┬┘│ │ │││ ││ │ ││ ││││ │ ├─┤├┤ │ ├┴┐ // ┴ ┴└─└─┘─┴┘└─┘└─┘ ┴ ┴└─┘┘└┘ └─┘┴ ┴└─┘└─┘┴ ┴ // ┌─┐┌─┐┬─┐ ╔╗╔╔═╗╔╗╔ ╔═╗╔═╗╔╦╗╦╔╦╗╦╔═╗╔═╗╔╦╗ ┌─┐┌─┐┌─┐┬ ┬┬ ┌─┐┌┬┐┌─┐┌─┐ // ├┤ │ │├┬┘ ║║║║ ║║║║───║ ║╠═╝ ║ ║║║║║╔═╝║╣ ║║ ├─┘│ │├─┘│ ││ ├─┤ │ ├┤ └─┐ // └ └─┘┴└─ ╝╚╝╚═╝╝╚╝ ╚═╝╩ ╩ ╩╩ ╩╩╚═╝╚═╝═╩╝ ┴ └─┘┴ └─┘┴─┘┴ ┴ ┴ └─┘└─┘ // ┌┬┐┬ ┬┌─┐┌┬┐ ╔═╗╦ ╔═╗╔═╗ ╦ ╦╔═╗╔═╗ ╔═╗╦ ╦╔╗ ╔═╗╦═╗╦╔╦╗╔═╗╦═╗╦╔═╗ // │ ├─┤├─┤ │ ╠═╣║ ╚═╗║ ║ ║ ║╚═╗║╣ ╚═╗║ ║╠╩╗║ ╠╦╝║ ║ ║╣ ╠╦╝║╠═╣ // ┴ ┴ ┴┴ ┴ ┴ ╩ ╩╩═╝╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝╚═╝╚═╝╚═╝╩╚═╩ ╩ ╚═╝╩╚═╩╩ ╩ // In production, if this check fails, a warning will be logged. // Determine if we are populating an association that does not support a fully-optimized populate. var isAssociationFullyCapable = isCapableOfOptimizedPopulate(populateAttrName, query.using, orm); // If so, then make sure we are not attempting to perform a "dangerous" populate-- // that is, one that is not currently safe using our built-in joining shim. // (This is related to memory usage, and is a result of the shim's implementation.) if (!isAssociationFullyCapable) { var subcriteria = query.populates[populateAttrName]; var isPotentiallyDangerous = ( subcriteria.skip !== 0 || subcriteria.limit !== (Number.MAX_SAFE_INTEGER||9007199254740991) || !isUsingDefaultSort ); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // > FUTURE (1): make this check more restrictive-- not EVERYTHING it prevents is actually // > dangerous given the current implementation of the shim. But in the mean time, // > better to err on the safe side. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // > FUTURE (2): overcome this by implementing a more complicated batching strategy-- however, // > this is not a priority right now, since this is only an issue for xD/A associations, // > which will likely never come up for the majority of applications. Our focus is on the // > much more common real-world scenario of populating across associations in the same database. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if (isPotentiallyDangerous) { if (process.env.NODE_ENV === 'production') { console.warn('\n'+ 'Warning: Attempting to populate `'+populateAttrName+'` with the specified subcriteria,\n'+ 'but this MAY NOT BE SAFE, depending on the number of records stored in your models.\n'+ 'Since this association does not support optimized populates (i.e. it spans multiple '+'\n'+ 'datastores, or uses an adapter that does not support native joins), it is not a good '+'\n'+ 'idea to populate it along with a subcriteria that uses `limit`, `skip`, and/or `sort`-- '+'\n'+ 'at least not in a production environment.\n'+ '\n'+ 'This is because, to satisfy the specified `limit`/`skip`/`sort`, many additional records\n'+ 'may need to be fetched along the way -- perhaps enough of them to overflow RAM on your server.\n'+ '\n'+ 'If you are just using sails-disk during development, or are certain this is not a problem\n'+ 'based on your application\'s requirements, then you can safely ignore this message.\n'+ 'But otherwise, to overcome this, either (A) remove or change this subcriteria and approach\n'+ 'this query a different way (such as multiple separate queries or a native query), or\n'+ '(B) configure all involved models to use the same datastore, and/or switch to an adapter\n'+ 'like sails-mysql or sails-postgresql that supports native joins.\n'+ ' [?] See https://sailsjs.com/support for help.\n' ); }//fi </ if production > }//fi </ if populating would be potentially- dangerous as far as process memory consumption > }//fi </ if association is NOT fully capable of being populated in a fully-optimized way > }//</else :: this is a plural ("collection") association> });//</_.each() key in the `populates` dictionary> }//>-• // ███╗ ██╗██╗ ██╗███╗ ███╗███████╗██████╗ ██╗ ██████╗ // ████╗ ██║██║ ██║████╗ ████║██╔════╝██╔══██╗██║██╔════╝ // ██