waterline
Version:
An ORM for Node.js and the Sails framework
1,045 lines (856 loc) • 117 kB
JavaScript
/**
* 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>
}//>-•
// ███╗ ██╗██╗ ██╗███╗ ███╗███████╗██████╗ ██╗ ██████╗
// ████╗ ██║██║ ██║████╗ ████║██╔════╝██╔══██╗██║██╔════╝
// ██