lib/waterline/utils/query/private/normalize-criteria.js
/**
* Module dependencies
*/
var util = require('util');
var _ = require('@sailshq/lodash');
var flaverr = require('flaverr');
var getModel = require('../../ontology/get-model');
var getAttribute = require('../../ontology/get-attribute');
var isSafeNaturalNumber = require('./is-safe-natural-number');
var isValidAttributeName = require('./is-valid-attribute-name');
var normalizeWhereClause = require('./normalize-where-clause');
var normalizeSortClause = require('./normalize-sort-clause');
/**
* Module constants
*/
var NAMES_OF_RECOGNIZED_CLAUSES = ['where', 'limit', 'skip', 'sort', 'select', 'omit'];
/**
* normalizeCriteria()
*
* Validate and normalize the provided value (`criteria`), hammering it destructively
* into the standardized format suitable to be part of a "stage 2 query" (see ARCHITECTURE.md).
* This allows us to present it in a normalized fashion to lifecycle callbacks, as well to
* other internal utilities within Waterline.
*
* Since the provided value _might_ be a string, number, or some other primitive that is
* NOT passed by reference, this function has a return value: a dictionary (plain JavaScript object).
* But realize that this is only to allow for a handful of edge cases. Most of the time, the
* provided value will be irreversibly mutated in-place, AS WELL AS returned.
*
* --
*
* There are many criteria normalization steps performed by Waterline.
* But this function only performs some of them.
*
* It DOES:
* (•) validate the criteria's format (particularly the `where` clause)
* (•) normalize the structure of the criteria (particularly the `where` clause)
* (•) ensure defaults exist for `limit`, `skip`, `sort`, `select`, and `omit`
* (•) apply (logical, not physical) schema-aware validations and normalizations
*
* It DOES NOT:
* (x) transform attribute names to column names
* (x) check that the criteria isn't trying to use features which are not supported by the adapter(s)
*
* --
*
* @param {Ref} criteria
* The original criteria (i.e. from a "stage 1 query").
* > WARNING:
* > IN SOME CASES (BUT NOT ALL!), THE PROVIDED CRITERIA WILL
* > UNDERGO DESTRUCTIVE, IN-PLACE CHANGES JUST BY PASSING IT
* > IN TO THIS UTILITY.
*
* @param {String} modelIdentity
* The identity of the model this criteria is referring to (e.g. "pet" or "user")
* > Useful for looking up the Waterline model and accessing its attribute definitions.
*
* @param {Ref} orm
* The Waterline ORM instance.
* > Useful for accessing the model definitions.
*
* @param {Dictionary?} meta
* The contents of the `meta` query key, if one was provided.
* > Useful for propagating query options to low-level utilities like this one.
*
* --
*
* @returns {Dictionary}
* The successfully-normalized criteria, ready for use in a stage 2 query.
*
*
* @throws {Error} If it encounters irrecoverable problems or unsupported usage in
* the provided criteria, including e.g. an invalid constraint is specified
* for an association.
* @property {String} code
* - E_HIGHLY_IRREGULAR
*
*
* @throws {Error} If the criteria indicates that it should never match anything.
* @property {String} code
* - E_WOULD_RESULT_IN_NOTHING
*
*
* @throws {Error} If anything else unexpected occurs.
*/
module.exports = function normalizeCriteria(criteria, modelIdentity, orm, meta) {
// Sanity checks.
// > These are just some basic, initial usage assertions to help catch
// > bugs during development of Waterline core.
//
// At this point, `criteria` MUST NOT be undefined.
// (Any defaulting related to that should be taken care of before calling this function.)
if (_.isUndefined(criteria)) {
throw new Error('Consistency violation: `criteria` should never be `undefined` when it is passed in to the normalizeCriteria() utility.');
}
// Look up the Waterline model for this query.
// > This is so that we can reference the original model definition.
var WLModel;
try {
WLModel = getModel(modelIdentity, orm);
} catch (e) {
switch (e.code) {
case 'E_MODEL_NOT_REGISTERED': throw new Error('Consistency violation: '+e.message);
default: throw e;
}
}//</catch>
// ████████╗ ██████╗ ██████╗ ██╗ ███████╗██╗ ██╗███████╗██╗
// ╚══██╔══╝██╔═══██╗██╔══██╗ ██║ ██╔════╝██║ ██║██╔════╝██║
// ██║ ██║ ██║██████╔╝█████╗██║ █████╗ ██║ ██║█████╗ ██║
// ██║ ██║ ██║██╔═══╝ ╚════╝██║ ██╔══╝ ╚██╗ ██╔╝██╔══╝ ██║
// ██║ ╚██████╔╝██║ ███████╗███████╗ ╚████╔╝ ███████╗███████╗
// ╚═╝ ╚═════╝ ╚═╝ ╚══════╝╚══════╝ ╚═══╝ ╚══════╝╚══════╝
//
// ███████╗ █████╗ ███╗ ██╗██╗████████╗██╗███████╗ █████╗ ████████╗██╗ ██████╗ ███╗ ██╗
// ██╔════╝██╔══██╗████╗ ██║██║╚══██╔══╝██║╚══███╔╝██╔══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║
// ███████╗███████║██╔██╗ ██║██║ ██║ ██║ ███╔╝ ███████║ ██║ ██║██║ ██║██╔██╗ ██║
// ╚════██║██╔══██║██║╚██╗██║██║ ██║ ██║ ███╔╝ ██╔══██║ ██║ ██║██║ ██║██║╚██╗██║
// ███████║██║ ██║██║ ╚████║██║ ██║ ██║███████╗██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║
// ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝
//
// ╔═╗╔═╗╔╦╗╔═╗╔═╗╔╦╗╦╔╗ ╦╦ ╦╔╦╗╦ ╦ (COMPATIBILITY)
// ║ ║ ║║║║╠═╝╠═╣ ║ ║╠╩╗║║ ║ ║ ╚╦╝
// ╚═╝╚═╝╩ ╩╩ ╩ ╩ ╩ ╩╚═╝╩╩═╝╩ ╩ ╩
// ┌─ ┌┬┐┌─┐┌─┐┬ ┬ ┬┬ ┌─┐┌─┐┬ ┌─┐┌─┐ ┬ ┬┌─┐ ┌┬┐┬┌─┐┌─┐ ┌─┐┌─┐┬ ┌─┐┌─┐┬ ┬ ─┐
// │─── │ │ │├─┘│ └┐┌┘│ ├┤ ├─┤│ └─┐├┤ └┐┌┘└─┐ ││││└─┐│ ├┤ ├─┤│ └─┐├┤ └┬┘ ───│
// └─ ┴ └─┘┴ ┴─┘└┘ ┴─┘ └ ┴ ┴┴─┘└─┘└─┘ └┘ └─┘o ┴ ┴┴└─┘└─┘ └ ┴ ┴┴─┘└─┘└─┘ ┴ ─┘
// If criteria is `false`, then we take that to mean that this is a special reserved
// criteria (Ø) that will never match any records.
if (criteria === false) {
throw flaverr('E_WOULD_RESULT_IN_NOTHING', new Error(
'In previous versions of Waterline, a criteria of `false` indicated that '+
'the specified query should simulate no matches. Now, it is up to the method. '+
'Be aware that support for using `false` in userland criterias may be completely '+
'removed in a future release of Sails/Waterline.'
));
}//-•
// If criteria is otherwise falsey (false, null, empty string, NaN, zero, negative zero)
// then understand it to mean the empty criteria (`{}`), which simulates ALL matches.
// Note that backwards-compatible support for this could be removed at any time!
if (!criteria) {
console.warn(
'Deprecated: In previous versions of Waterline, the specified criteria '+
'(`'+util.inspect(criteria,{depth:5})+'`) would match ALL records in '+
'this model. If that is what you are intending to happen, then please pass '+
'in `{}` instead, or simply omit the `criteria` dictionary altogether-- both of '+
'which are more explicit and future-proof ways of doing the same thing.\n'+
'> Warning: This backwards compatibility will be removed\n'+
'> in a future release of Sails/Waterline. If this usage\n'+
'> is left unchanged, then queries like this one will eventually \n'+
'> fail with an error.'
);
criteria = {};
}//>-
// ┌┐┌┌─┐┬─┐┌┬┐┌─┐┬ ┬┌─┐┌─┐ ╔═╗╦╔═╦ ╦ ┌─┐┬─┐ ╦╔╗╔ ┌─┐┬ ┬┌─┐┬─┐┌┬┐┬ ┬┌─┐┌┐┌┌┬┐
// ││││ │├┬┘│││├─┤│ │┌─┘├┤ ╠═╝╠╩╗╚╗╔╝ │ │├┬┘ ║║║║ └─┐├─┤│ │├┬┘ │ ├─┤├─┤│││ ││
// ┘└┘└─┘┴└─┴ ┴┴ ┴┴─┘┴└─┘└─┘ ╩ ╩ ╩ ╚╝ └─┘┴└─ ╩╝╚╝ └─┘┴ ┴└─┘┴└─ ┴ ┴ ┴┴ ┴┘└┘─┴┘
// ┌─ ┌┬┐┌─┐┌─┐┬ ┬ ┬┬ ┌─┐┌┬┐┬─┐ ┌┐┌┬ ┬┌┬┐ ┌─┐┬─┐ ┌─┐┬─┐┬─┐┌─┐┬ ┬ ─┐
// │─── │ │ │├─┘│ └┐┌┘│ └─┐ │ ├┬┘ ││││ ││││ │ │├┬┘ ├─┤├┬┘├┬┘├─┤└┬┘ ───│
// └─ ┴ └─┘┴ ┴─┘└┘ ┴─┘ └─┘ ┴ ┴└─┘ ┘└┘└─┘┴ ┴┘ └─┘┴└─ ┴ ┴┴└─┴└─┴ ┴ ┴ ─┘
//
// If the provided criteria is an array, string, or number, then we'll be able
// to understand it as a primary key, or as an array of primary key values.
if (_.isArray(criteria) || _.isNumber(criteria) || _.isString(criteria)) {
var topLvlPkValuesOrPkValue = criteria;
// So expand that into the beginnings of a proper criteria dictionary.
// (This will be further normalized throughout the rest of this file--
// this is just enough to get us to where we're working with a dictionary.)
criteria = {};
criteria.where = {};
criteria.where[WLModel.primaryKey] = topLvlPkValuesOrPkValue;
}//>-
// ┬ ┬┌─┐┬─┐┬┌─┐┬ ┬ ╔═╗╦╔╗╔╔═╗╦ ┌┬┐┌─┐┌─┐ ┬ ┬ ┬┬ ┌┬┐┌─┐┌┬┐┌─┐ ┌┬┐┬ ┬┌─┐┌─┐
// └┐┌┘├┤ ├┬┘│├┤ └┬┘ ╠╣ ║║║║╠═╣║ │ │ │├─┘───│ └┐┌┘│ ││├─┤ │ ├─┤ │ └┬┘├─┘├┤
// └┘ └─┘┴└─┴└ ┴ ╚ ╩╝╚╝╩ ╩╩═╝ ┴ └─┘┴ ┴─┘└┘ ┴─┘ ─┴┘┴ ┴ ┴ ┴ ┴ ┴ ┴ ┴ └─┘
//
// IWMIH and the provided criteria is anything OTHER than a proper dictionary,
// (e.g. if it's a function or regexp or something) then that means it is invalid.
if (!_.isObject(criteria) || _.isArray(criteria) || _.isFunction(criteria)){
throw flaverr('E_HIGHLY_IRREGULAR', new Error('The provided criteria is invalid. Should be a dictionary (plain JavaScript object), but instead got: '+util.inspect(criteria, {depth:5})+''));
}//-•
// ╔═╗╔═╗╔╦╗╔═╗╔═╗╔╦╗╦╔╗ ╦╦ ╦╔╦╗╦ ╦ (COMPATIBILITY)
// ║ ║ ║║║║╠═╝╠═╣ ║ ║╠╩╗║║ ║ ║ ╚╦╝
// ╚═╝╚═╝╩ ╩╩ ╩ ╩ ╩ ╩╚═╝╩╩═╝╩ ╩ ╩
// ┌─┐┌─┐┌─┐┬─┐┌─┐┌─┐┌─┐┌┬┐┬┌─┐┌┐┌┌─┐ ┬ ┬┌─┐┬─┐┬┌─ ┌┬┐┬┌─┐┌─┐┌─┐┬─┐┌─┐┌┐┌┌┬┐┬ ┬ ┬ ┌┐┌┌─┐┬ ┬
// ├─┤│ ┬│ ┬├┬┘├┤ │ ┬├─┤ │ ││ ││││└─┐ ││││ │├┬┘├┴┐ │││├┤ ├┤ ├┤ ├┬┘├┤ │││ │ │ └┬┘ ││││ ││││
// ┴ ┴└─┘└─┘┴└─└─┘└─┘┴ ┴ ┴ ┴└─┘┘└┘└─┘ └┴┘└─┘┴└─┴ ┴ ─┴┘┴└ └ └─┘┴└─└─┘┘└┘ ┴ ┴─┘┴ ┘└┘└─┘└┴┘
//
// If we see `sum`, `average`, `min`, `max`, or `groupBy`, throw a
// fatal error to explain what's up, and also to suggest a suitable
// alternative.
//
// > Support for basic aggregations via criteria clauses was removed
// > in favor of new model methods in Waterline v0.13. Specifically
// > for `min`, `max`, and `groupBy`, for which there are no new model
// > methods, we recommend using native queries (aka "stage 5 queries").
// > (Note that, in the future, you will also be able to do the same thing
// > using Waterline statements, aka "stage 4 queries". But as of Nov 2016,
// > they only support the basic aggregations: count, sum, and avg.)
if (!_.isUndefined(criteria.groupBy)) {
// ^^
// Note that `groupBy` comes first, since it might have been used in conjunction
// with the others (and if it was, you won't be able to do whatever it is you're
// trying to do using the approach suggested by the other compatibility errors
// below.)
throw new Error(
'The `groupBy` clause is no longer supported in Sails/Waterline.\n'+
'In previous versions, `groupBy` could be provided in a criteria '+
'to perform an aggregation query. But as of Sails v1.0/Waterline v0.13, the '+
'usage has changed. Now, to run aggregate queries using the `groupBy` operator, '+
'use a native query instead.\n'+
'\n'+
'Alternatively, if you are using `groupBy` as a column/attribute name then '+
'please be advised that some things won\'t work as expected.\n'+
'\n'+
'For more info, visit:\n'+
'http://sailsjs.com/docs/upgrading/to-v1.0'
);
}//-•
if (!_.isUndefined(criteria.sum)) {
throw new Error(
'The `sum` clause is no longer supported in Sails/Waterline.\n'+
'In previous versions, `sum` could be provided in a criteria '+
'to perform an aggregation query. But as of Sails v1.0/Waterline v0.13, the '+
'usage has changed. Now, to sum the value of an attribute across multiple '+
'records, use the `.sum()` model method.\n'+
'\n'+
'For example:\n'+
'```\n'+
'// Get the cumulative account balance of all bank accounts that '+'\n'+
'// have less than $32,000, or that are flagged as "suspended".'+'\n'+
'BankAccount.sum(\'balance\').where({'+'\n'+
' or: ['+'\n'+
' { balance: { \'<\': 32000 } },'+'\n'+
' { suspended: true }'+'\n'+
' ]'+'\n'+
'}).exec(function (err, total){'+'\n'+
' // ...'+'\n'+
'});'+'\n'+
'```\n'+
'Alternatively, if you are using `sum` as a column/attribute name then '+
'please be advised that some things won\'t work as expected.\n'+
'\n'+
'For more info, see:\n'+
'http://sailsjs.com/docs/reference/waterline-orm/models/sum'
);
}//-•
if (!_.isUndefined(criteria.average)) {
throw new Error(
'The `average` clause is no longer supported in Sails/Waterline.\n'+
'In previous versions, `average` could be provided in a criteria '+
'to perform an aggregation query. But as of Sails v1.0/Waterline v0.13, the '+
'usage has changed. Now, to calculate the mean value of an attribute across '+
'multiple records, use the `.avg()` model method.\n'+
'\n'+
'For example:\n'+
'```\n'+
'// Get the average balance of bank accounts owned by people between '+'\n'+
'// the ages of 35 and 45.'+'\n'+
'BankAccount.avg(\'balance\').where({'+'\n'+
' ownerAge: { \'>=\': 35, \'<=\': 45 }'+'\n'+
'}).exec(function (err, averageBalance){'+'\n'+
' // ...'+'\n'+
'});'+'\n'+
'```\n'+
'Alternatively, if you are using `average` as a column/attribute name then '+
'please be advised that some things won\'t work as expected.\n'+
'\n'+
'For more info, see:\n'+
'http://sailsjs.com/docs/reference/waterline-orm/models/avg'
);
}//-•
if (!_.isUndefined(criteria.min)) {
throw new Error(
'The `min` clause is no longer supported in Sails/Waterline.\n'+
'In previous versions, `min` could be provided in a criteria '+
'to perform an aggregation query. But as of Sails v1.0/Waterline v0.13, the '+
'usage has changed. Now, to calculate the minimum value of an attribute '+
'across multiple records, use the `.find()` model method.\n'+
'\n'+
'For example:\n'+
'```\n'+
'// Get the smallest account balance from amongst all account holders '+'\n'+
'// between the ages of 35 and 45.'+'\n'+
'BankAccount.find(\'balance\').where({'+'\n'+
' ownerAge: { \'>=\': 35, \'<=\': 45 }'+'\n'+
'})'+'\n'+
'.limit(1)'+'\n'+
'.select([\'balance\'])'+'\n'+
'.sort(\'balance ASC\')'+'\n'+
'}).exec(function (err, relevantAccounts){'+'\n'+
' // ...'+'\n'+
' var minBalance;'+'\n'+
' if (relevantAccounts[0]) {'+'\n'+
' minBalance = relevantAccounts[0].balance;'+'\n'+
' }'+'\n'+
' else {'+'\n'+
' minBalance = null;'+'\n'+
' }'+'\n'+
'});'+'\n'+
'```\n'+
'Alternatively, if you are using `min` as a column/attribute name then '+
'please be advised that some things won\'t work as expected.\n'+
'\n'+
'For more info, see:\n'+
'http://sailsjs.com/docs/reference/waterline-orm/models/find'
);
}//-•
if (!_.isUndefined(criteria.max)) {
throw new Error(
'The `max` clause is no longer supported in Sails/Waterline.\n'+
'In previous versions, `max` could be provided in a criteria '+
'to perform an aggregation query. But as of Sails v1.0/Waterline v0.13, the '+
'usage has changed. Now, to calculate the maximum value of an attribute '+
'across multiple records, use the `.find()` model method.\n'+
'\n'+
'For example:\n'+
'```\n'+
'// Get the largest account balance from amongst all account holders '+'\n'+
'// between the ages of 35 and 45.'+'\n'+
'BankAccount.find(\'balance\').where({'+'\n'+
' ownerAge: { \'>=\': 35, \'<=\': 45 }'+'\n'+
'})'+'\n'+
'.limit(1)'+'\n'+
'.select([\'balance\'])'+'\n'+
'.sort(\'balance DESC\')'+'\n'+
'}).exec(function (err, relevantAccounts){'+'\n'+
' // ...'+'\n'+
' var maxBalance;'+'\n'+
' if (relevantAccounts[0]) {'+'\n'+
' maxBalance = relevantAccounts[0].balance;'+'\n'+
' }'+'\n'+
' else {'+'\n'+
' maxBalance = null;'+'\n'+
' }'+'\n'+
'});'+'\n'+
'```\n'+
'Alternatively, if you are using `max` as a column/attribute name then '+
'please be advised that some things won\'t work as expected.\n'+
'\n'+
'For more info, see:\n'+
'http://sailsjs.com/docs/reference/waterline-orm/models/find'
);
}//-•
// ┬ ┬┌─┐┌┐┌┌┬┐┬ ┌─┐ ╦╔╦╗╔═╗╦ ╦╔═╗╦╔╦╗ ╦ ╦╦ ╦╔═╗╦═╗╔═╗ ╔═╗╦ ╔═╗╦ ╦╔═╗╔═╗
// ├─┤├─┤│││ │││ ├┤ ║║║║╠═╝║ ║║ ║ ║ ║║║╠═╣║╣ ╠╦╝║╣ ║ ║ ╠═╣║ ║╚═╗║╣
// ┴ ┴┴ ┴┘└┘─┴┘┴─┘└─┘ ╩╩ ╩╩ ╩═╝╩╚═╝╩ ╩ ╚╩╝╩ ╩╚═╝╩╚═╚═╝ ╚═╝╩═╝╩ ╩╚═╝╚═╝╚═╝
//
// Now, if the provided criteria dictionary DOES NOT contain the names of ANY
// known criteria clauses (like `where`, `limit`, etc.) as properties, then we
// can safely assume that it is relying on shorthand: i.e. simply specifying what
// would normally be the `where` clause, but at the top level.
var recognizedClauses = _.intersection(_.keys(criteria), NAMES_OF_RECOGNIZED_CLAUSES);
if (recognizedClauses.length === 0) {
criteria = {
where: criteria
};
}
// Otherwise, it DOES contain a recognized clause keyword.
else {
// In which case... well, there's nothing else to do just yet.
//
// > Note: a little ways down, we do a check for any extraneous properties.
// > That check is important, because mixed criterias like `{foo: 'bar', limit: 3}`
// > _were_ supported in previous versions of Waterline, but they are not anymore.
}//>-
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// ╔═╗╔═╗╔╦╗╔═╗╔═╗╔╦╗╦╔╗ ╦╦ ╦╔╦╗╦ ╦ (COMPATIBILITY)
// ║ ║ ║║║║╠═╝╠═╣ ║ ║╠╩╗║║ ║ ║ ╚╦╝
// ╚═╝╚═╝╩ ╩╩ ╩ ╩ ╩ ╩╚═╝╩╩═╝╩ ╩ ╩
// ┌─ ┌─┐┌─┐┬─┐┬ ┬┌┐ ╔═╗╔═╗╔═╗╦ ╦╦ ╔═╗╔╦╗╔═╗ ┬ ╔═╗╔═╗╔═╗╦ ╦╦ ╔═╗╔╦╗╔═╗╔═╗ ─┐
// │─── └─┐│ ├┬┘│ │├┴┐ ╠═╝║ ║╠═╝║ ║║ ╠═╣ ║ ║╣ ┌┼─ ╠═╝║ ║╠═╝║ ║║ ╠═╣ ║ ║╣ ╚═╗ ───│
// └─ └─┘└─┘┴└─└─┘└─┘ ╩ ╚═╝╩ ╚═╝╩═╝╩ ╩ ╩ ╚═╝ └┘ ╩ ╚═╝╩ ╚═╝╩═╝╩ ╩ ╩ ╚═╝╚═╝ ─┘
//
// - - - - - - - - - - - - -
// NOTE:
// Leaving this stuff commented out, because we should really just break
// backwards-compatibility here. If either of these properties are used,
// they are caught below by the unrecognized property check.
//
// This was not documented, and so hopefully was not widely used. If you've
// got feedback on that, hit up @particlebanana or @mikermcneil on Twitter.
// - - - - - - - - - - - - -
// ```
// // For compatibility, tolerate the presence of `.populate` or `.populates` on the
// // criteria dictionary (but scrub those suckers off right away).
// delete criteria.populate;
// delete criteria.populates;
// ```
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// ┌─┐┬─┐┌─┐┬ ┬┌─┐┌┐┌┌┬┐ ╔═╗═╗ ╦╔╦╗╦═╗╔═╗╔╗╔╔═╗╔═╗╦ ╦╔═╗ ╔═╗╦═╗╔═╗╔═╗╔═╗╦═╗╔╦╗╦╔═╗╔═╗
// ├─┘├┬┘├┤ └┐┌┘├┤ │││ │ ║╣ ╔╩╦╝ ║ ╠╦╝╠═╣║║║║╣ ║ ║║ ║╚═╗ ╠═╝╠╦╝║ ║╠═╝║╣ ╠╦╝ ║ ║║╣ ╚═╗
// ┴ ┴└─└─┘ └┘ └─┘┘└┘ ┴ ╚═╝╩ ╚═ ╩ ╩╚═╩ ╩╝╚╝╚═╝╚═╝╚═╝╚═╝ ╩ ╩╚═╚═╝╩ ╚═╝╩╚═ ╩ ╩╚═╝╚═╝
//
// Now that we've handled the "implicit `where`" case, make sure all remaining
// top-level keys on the criteria dictionary match up with recognized criteria
// clauses.
_.each(_.keys(criteria), function(clauseName) {
var clauseDef = criteria[clauseName];
// If this is NOT a recognized criteria clause...
var isRecognized = _.contains(NAMES_OF_RECOGNIZED_CLAUSES, clauseName);
if (!isRecognized) {
// Then, check to see if the RHS is `undefined`.
// If so, just strip it out and move on.
if (_.isUndefined(clauseDef)) {
delete criteria[clauseName];
return;
}//-•
// Otherwise, this smells like a mistake.
// It's at least highly irregular, that's for sure.
throw flaverr('E_HIGHLY_IRREGULAR', new Error(
'The provided criteria contains an unrecognized property: '+
util.inspect(clauseName, {depth:5})+'\n'+
'* * *\n'+
'In previous versions of Sails/Waterline, this criteria _may_ have worked, since '+
'keywords like `limit` were allowed to sit alongside attribute names that are '+
'really supposed to be wrapped inside of the `where` clause. But starting in '+
'Sails v1.0/Waterline 0.13, if a `limit`, `skip`, `sort`, etc is defined, then '+
'any <attribute name> vs. <constraint> pairs should be explicitly contained '+
'inside the `where` clause.\n'+
'* * *'
));
}//-•
// Otherwise, we know this must be a recognized criteria clause, so we're good.
// (We'll check it out more carefully in just a sec below.)
return;
});//</ _.each() :: each top-level property on the criteria >
// ██╗ ██╗██╗ ██╗███████╗██████╗ ███████╗
// ██║ ██║██║ ██║██╔════╝██╔══██╗██╔════╝
// ██║ █╗ ██║███████║█████╗ ██████╔╝█████╗
// ██║███╗██║██╔══██║██╔══╝ ██╔══██╗██╔══╝
// ╚███╔███╔╝██║ ██║███████╗██║ ██║███████╗
// ╚══╝╚══╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝
//
try {
criteria.where = normalizeWhereClause(criteria.where, modelIdentity, orm, meta);
} catch (e) {
switch (e.code) {
case 'E_WHERE_CLAUSE_UNUSABLE':
throw flaverr('E_HIGHLY_IRREGULAR', new Error(
'Could not use the provided `where` clause. '+ e.message
));
case 'E_WOULD_RESULT_IN_NOTHING':
throw e;
// 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: Unexpected error normalizing/validating the `where` clause: '+e.stack);
}
}//>-•
// ██╗ ██╗███╗ ███╗██╗████████╗
// ██║ ██║████╗ ████║██║╚══██╔══╝
// ██║ ██║██╔████╔██║██║ ██║
// ██║ ██║██║╚██╔╝██║██║ ██║
// ███████╗██║██║ ╚═╝ ██║██║ ██║
// ╚══════╝╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
// Validate/normalize `limit` clause.
// ╔╦╗╔═╗╔═╗╔═╗╦ ╦╦ ╔╦╗ ┬ ┬┌┬┐┬┌┬┐
// ║║║╣ ╠╣ ╠═╣║ ║║ ║ │ │││││ │
// ═╩╝╚═╝╚ ╩ ╩╚═╝╩═╝╩ ┴─┘┴┴ ┴┴ ┴
// If no `limit` clause was provided, give it a default value.
if (_.isUndefined(criteria.limit)) {
criteria.limit = (Number.MAX_SAFE_INTEGER||9007199254740991);
}//>-
// ╔═╗╔═╗╦═╗╔═╗╔═╗ ┌─┐┬─┐┌─┐┌┬┐ ╔═╗╔╦╗╦═╗╦╔╗╔╔═╗
// ╠═╝╠═╣╠╦╝╚═╗║╣ ├┤ ├┬┘│ ││││ ╚═╗ ║ ╠╦╝║║║║║ ╦
// ╩ ╩ ╩╩╚═╚═╝╚═╝ └ ┴└─└─┘┴ ┴ ╚═╝ ╩ ╩╚═╩╝╚╝╚═╝
// If the provided `limit` is a string, attempt to parse it into a number.
if (_.isString(criteria.limit)) {
criteria.limit = +criteria.limit;
}//>-•
// ╔═╗╔═╗╔╦╗╔═╗╔═╗╔╦╗╦╔╗ ╦╦ ╦╔╦╗╦ ╦ (COMPATIBILITY)
// ║ ║ ║║║║╠═╝╠═╣ ║ ║╠╩╗║║ ║ ║ ╚╦╝
// ╚═╝╚═╝╩ ╩╩ ╩ ╩ ╩ ╩╚═╝╩╩═╝╩ ╩ ╩
// ┌─ ┌┐┌┬ ┬┬ ┬ ┬┌┐┌┌─┐┬┌┐┌┬┌┬┐┬ ┬ ┌─┐┌─┐┬─┐┌─┐
// │─── ││││ ││ │ ││││├┤ │││││ │ └┬┘ ┌─┘├┤ ├┬┘│ │
// └─ ┘└┘└─┘┴─┘┴─┘┘ ┴┘└┘└ ┴┘└┘┴ ┴ ┴┘ └─┘└─┘┴└─└─┘┘
// ┬ ┌┐┌┌─┐┌─┐┌─┐┌┬┐┬┬ ┬┌─┐ ┌┐┌┬ ┬┌┬┐┌┐ ┌─┐┬─┐┌─┐ ─┐
// ┌┼─ │││├┤ │ ┬├─┤ │ │└┐┌┘├┤ ││││ ││││├┴┐├┤ ├┬┘└─┐ ───│
// └┘ ┘└┘└─┘└─┘┴ ┴ ┴ ┴ └┘ └─┘ ┘└┘└─┘┴ ┴└─┘└─┘┴└─└─┘ ─┘
// For convenience/compatibility, we also tolerate `null` and `Infinity`,
// and understand them to mean the same thing.
if (_.isNull(criteria.limit) || criteria.limit === Infinity) {
criteria.limit = (Number.MAX_SAFE_INTEGER||9007199254740991);
}//>-
// If limit is zero, then that means we'll be returning NO results.
if (criteria.limit === 0) {
throw flaverr('E_WOULD_RESULT_IN_NOTHING', new Error('A criteria with `limit: 0` will never actually match any records.'));
}//-•
// If limit is less than zero, then use the default limit.
// (But log a deprecation message.)
if (criteria.limit < 0) {
console.warn(
'Deprecated: In previous versions of Waterline, the specified `limit` '+
'(`'+util.inspect(criteria.limit,{depth:5})+'`) would work the same '+
'as if you had omitted the `limit` altogether-- i.e. defaulting to `Number.MAX_SAFE_INTEGER`. '+
'If that is what you are intending to happen, then please just omit `limit` instead, which is '+
'a more explicit and future-proof way of doing the same thing.\n'+
'> Warning: This backwards compatibility will be removed\n'+
'> in a future release of Sails/Waterline. If this usage\n'+
'> is left unchanged, then queries like this one will eventually \n'+
'> fail with an error.'
);
criteria.limit = (Number.MAX_SAFE_INTEGER||9007199254740991);
}//>-
// ┬ ┬┌─┐┬─┐┬┌─┐┬ ┬ ┌┬┐┬ ┬┌─┐┌┬┐ ┬ ┬┌┬┐┬┌┬┐ ┬┌─┐ ┌┐┌┌─┐┬ ┬
// └┐┌┘├┤ ├┬┘│├┤ └┬┘ │ ├─┤├─┤ │ │ │││││ │ │└─┐ ││││ ││││
// └┘ └─┘┴└─┴└ ┴ ┴ ┴ ┴┴ ┴ ┴ ┴─┘┴┴ ┴┴ ┴ ┴└─┘ ┘└┘└─┘└┴┘
// ┌─┐ ╔═╗╔═╗╔═╗╔═╗ ╔╗╔╔═╗╔╦╗╦ ╦╦═╗╔═╗╦ ╔╗╔╦ ╦╔╦╗╔╗ ╔═╗╦═╗
// ├─┤ ╚═╗╠═╣╠╣ ║╣ ║║║╠═╣ ║ ║ ║╠╦╝╠═╣║ ║║║║ ║║║║╠╩╗║╣ ╠╦╝
// ┴ ┴ ╚═╝╩ ╩╚ ╚═╝┘ ╝╚╝╩ ╩ ╩ ╚═╝╩╚═╩ ╩╩═╝ ╝╚╝╚═╝╩ ╩╚═╝╚═╝╩╚═
// At this point, the `limit` should be a safe, natural number.
// But if that's not the case, we say that this criteria is highly irregular.
//
// > Remember, if the limit happens to have been provided as `Infinity`, we
// > already handled that special case above, and changed it to be
// > `Number.MAX_SAFE_INTEGER` instead (which is a safe, natural number).
if (!isSafeNaturalNumber(criteria.limit)) {
throw flaverr('E_HIGHLY_IRREGULAR', new Error(
'The `limit` clause in the provided criteria is invalid. '+
'If provided, it should be a safe, natural number. '+
'But instead, got: '+
util.inspect(criteria.limit, {depth:5})+''
));
}//-•
// ███████╗██╗ ██╗██╗██████╗
// ██╔════╝██║ ██╔╝██║██╔══██╗
// ███████╗█████╔╝ ██║██████╔╝
// ╚════██║██╔═██╗ ██║██╔═══╝
// ███████║██║ ██╗██║██║
// ╚══════╝╚═╝ ╚═╝╚═╝╚═╝
//
// Validate/normalize `skip` clause.
// ╔╦╗╔═╗╔═╗╔═╗╦ ╦╦ ╔╦╗
// ║║║╣ ╠╣ ╠═╣║ ║║ ║
// ═╩╝╚═╝╚ ╩ ╩╚═╝╩═╝╩
// If no `skip` clause was provided, give it a default value.
if (_.isUndefined(criteria.skip)) {
criteria.skip = 0;
}//>-
// ╔═╗╔═╗╦═╗╔═╗╔═╗ ┌─┐┬─┐┌─┐┌┬┐ ╔═╗╔╦╗╦═╗╦╔╗╔╔═╗
// ╠═╝╠═╣╠╦╝╚═╗║╣ ├┤ ├┬┘│ ││││ ╚═╗ ║ ╠╦╝║║║║║ ╦
// ╩ ╩ ╩╩╚═╚═╝╚═╝ └ ┴└─└─┘┴ ┴ ╚═╝ ╩ ╩╚═╩╝╚╝╚═╝
// If the provided `skip` is a string, attempt to parse it into a number.
if (_.isString(criteria.skip)) {
criteria.skip = +criteria.skip;
}//>-•
// ┬ ┬┌─┐┬─┐┬┌─┐┬ ┬ ┌┬┐┬ ┬┌─┐┌┬┐ ___ ┬┌─┐ ┌┐┌┌─┐┬ ┬
// └┐┌┘├┤ ├┬┘│├┤ └┬┘ │ ├─┤├─┤ │ | | │└─┐ ││││ ││││
// └┘ └─┘┴└─┴└ ┴ ┴ ┴ ┴┴ ┴ ┴ | | ┴└─┘ ┘└┘└─┘└┴┘
// ┌─┐ ╔═╗╔═╗╔═╗╔═╗ ╔╗╔╔═╗╔╦╗╦ ╦╦═╗╔═╗╦ ╔╗╔╦ ╦╔╦╗╔╗ ╔═╗╦═╗
// ├─┤ ╚═╗╠═╣╠╣ ║╣ ║║║╠═╣ ║ ║ ║╠╦╝╠═╣║ ║║║║ ║║║║╠╩╗║╣ ╠╦╝ (OR zero)
// ┴ ┴ ╚═╝╩ ╩╚ ╚═╝┘ ╝╚╝╩ ╩ ╩ ╚═╝╩╚═╩ ╩╩═╝ ╝╚╝╚═╝╩ ╩╚═╝╚═╝╩╚═
// At this point, the `skip` should be either zero or a safe, natural number.
// But if that's not the case, we say that this criteria is highly irregular.
if (criteria.skip === 0) { /* skip: 0 is valid */ }
else if (isSafeNaturalNumber(criteria.skip)) { /* any safe, natural number is a valid `skip` */ }
else {
throw flaverr('E_HIGHLY_IRREGULAR', new Error(
'The `skip` clause in the provided criteria is invalid. If provided, it should be either zero (0), or a safe, natural number (e.g. 4). But instead, got: '+
util.inspect(criteria.skip, {depth:5})+''
));
}//-•
// ███████╗ ██████╗ ██████╗ ████████╗
// ██╔════╝██╔═══██╗██╔══██╗╚══██╔══╝
// ███████╗██║ ██║██████╔╝ ██║
// ╚════██║██║ ██║██╔══██╗ ██║
// ███████║╚██████╔╝██║ ██║ ██║
// ╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝
//
// Validate/normalize `sort` clause.
try {
criteria.sort = normalizeSortClause(criteria.sort, modelIdentity, orm, meta);
} catch (e) {
switch (e.code) {
case 'E_SORT_CLAUSE_UNUSABLE':
throw flaverr('E_HIGHLY_IRREGULAR', new Error(
'Could not use the provided `sort` clause: ' + e.message
));
// 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 a provided `sort` clause:\n```\n'+util.inspect(criteria.sort, {depth:5})+'```\nHere is the error:\n```'+e.stack+'\n```');
}
}//>-•
// ███████╗███████╗██╗ ███████╗ ██████╗████████╗
// ██╔════╝██╔════╝██║ ██╔════╝██╔════╝╚══██╔══╝
// ███████╗█████╗ ██║ █████╗ ██║ ██║
// ╚════██║██╔══╝ ██║ ██╔══╝ ██║ ██║
// ███████║███████╗███████╗███████╗╚██████╗ ██║
// ╚══════╝╚══════╝╚══════╝╚══════╝ ╚═════╝ ╚═╝
// Validate/normalize `select` clause.
// ╔╦╗╔═╗╔═╗╔═╗╦ ╦╦ ╔╦╗
// ║║║╣ ╠╣ ╠═╣║ ║║ ║
// ═╩╝╚═╝╚ ╩ ╩╚═╝╩═╝╩
// If no `select` clause was provided, give it a default value.
if (_.isUndefined(criteria.select)) {
criteria.select = ['*'];
}//>-
// If specified as a string, wrap it up in an array.
if (_.isString(criteria.select)) {
criteria.select = [
criteria.select
];
}//>-
// At this point, we should have an array.
// If not, then we'll bail with an error.
if (!_.isArray(criteria.select)) {
throw flaverr('E_HIGHLY_IRREGULAR', new Error(
'The `select` clause in the provided criteria is invalid. If provided, it should be an array of strings. But instead, got: '+
util.inspect(criteria.select, {depth:5})+''
));
}//-•
// Special handling of `['*']`.
//
// > In order for special meaning to take effect, array must have exactly one item (`*`).
// > (Also note that `*` is not a valid attribute name, so there's no chance of overlap there.)
if (_.isEqual(criteria.select, ['*'])) {
// ['*'] is always valid-- it is the default value for the `select` clause.
// So we don't have to do anything here.
}
// Otherwise, we must investigate further.
else {
// Ensure the primary key is included in the `select`.
// (If it is not, then add it automatically.)
//
// > Note that compatiblity with the `populates` query key is handled back in forgeStageTwoQuery().
if (!_.contains(criteria.select, WLModel.primaryKey)) {
criteria.select.push(WLModel.primaryKey);
}//>-
// If model is `schema: false`, then prevent using a custom `select` clause.
// (This is because doing so is not yet reliable.)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// FUTURE: Fix this & then thoroughly test with normal finds and populated finds,
// with the select clause in the main criteria and the subcriteria, using both native
// joins and polypopulates.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
if (WLModel.hasSchema === false) {
throw flaverr('E_HIGHLY_IRREGULAR', new Error(
'The provided criteria contains a custom `select` clause, but since this model (`'+modelIdentity+'`) '+
'is `schema: false`, this cannot be relied upon... yet. In the mean time, if you\'d like to use a '+
'custom `select`, configure this model to `schema: true`. Or, better yet, since this is usually an app-wide setting,'+
'configure all of your models to have `schema: true` -- e.g. in `config/models.js`. (Note that this WILL be supported in a '+
'future, minor version release of Sails/Waterline. Want to lend a hand? http://sailsjs.com/contribute)'
));
}//-•
// Loop through array and check each attribute name.
_.each(criteria.select, function (attrNameToKeep){
// Try to look up the attribute def.
var attrDef;
try {
attrDef = getAttribute(attrNameToKeep, modelIdentity, orm);
} catch (e){
switch (e.code) {
case 'E_ATTR_NOT_REGISTERED':
// If no matching attribute is found, `attrDef` just stays undefined
// and we keep going.
break;
default: throw e;
}
}//</catch>
// If model is `schema: true`...
if (WLModel.hasSchema === true) {
// Make sure this matched a recognized attribute name.
if (!attrDef) {
throw flaverr('E_HIGHLY_IRREGULAR', new Error(
'The `select` clause in the provided criteria contains an item (`'+attrNameToKeep+'`) which is '+
'not a recognized attribute in this model (`'+modelIdentity+'`).'
));
}//-•
}
// Else if model is `schema: false`...
else if (WLModel.hasSchema === false) {
// Make sure this is at least a valid name for a Waterline attribute.
if (!isValidAttributeName(attrNameToKeep)) {
throw flaverr('E_HIGHLY_IRREGULAR', new Error(
'The `select` clause in the provided criteria contains an item (`'+attrNameToKeep+'`) which is not '+
'a valid name for an attribute in Sails/Waterline.'
));
}//-•
} else { throw new Error('Consistency violation: Every instantiated Waterline model should always have a `hasSchema` property as either `true` or `false` (should have been derived from the `schema` model setting when Waterline was being initialized). But somehow, this model (`'+modelIdentity+'`) ended up with `hasSchema: '+util.inspect(WLModel.hasSchema, {depth:5})+'`'); }
// Ensure that we're not trying to `select` a plural association.
// > That's never allowed, because you can only populate a plural association-- it's a virtual attribute.
// > Note that we also do a related check when we normalize the `populates` query key back in forgeStageTwoQuery().
if (attrDef && attrDef.collection) {
throw flaverr('E_HIGHLY_IRREGULAR', new Error(
'The `select` clause in the provided criteria contains an item (`'+attrNameToKeep+'`) which is actually '+
'the name of a plural ("collection") association for this model (`'+modelIdentity+'`). But you cannot '+
'explicitly select plural association because they\'re virtual attributes (use `.populate()` instead.)'
));
}//-•
});//</ _.each() :: each attribute name >
// ┌─┐┬ ┬┌─┐┌─┐┬┌─ ┌─┐┌─┐┬─┐ ╔╦╗╦ ╦╔═╗╦ ╦╔═╗╔═╗╔╦╗╔═╗╔═╗
// │ ├─┤├┤ │ ├┴┐ ├┤ │ │├┬┘ ║║║ ║╠═╝║ ║║ ╠═╣ ║ ║╣ ╚═╗
// └─┘┴ ┴└─┘└─┘┴ ┴ └ └─┘┴└─ ═╩╝╚═╝╩ ╩═╝╩╚═╝╩ ╩ ╩ ╚═╝╚═╝
// Ensure that no two items refer to the same attribute.
criteria.select = _.uniq(criteria.select);
}//>-• </ else (this is something other than ['*']) >
// ██████╗ ███╗ ███╗██╗████████╗
// ██╔═══██╗████╗ ████║██║╚══██╔══╝
// ██║ ██║██╔████╔██║██║ ██║
// ██║ ██║██║╚██╔╝██║██║ ██║
// ╚██████╔╝██║ ╚═╝ ██║██║ ██║
// ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝
// ╔╦╗╔═╗╔═╗╔═╗╦ ╦╦ ╔╦╗
// ║║║╣ ╠╣ ╠═╣║ ║║ ║
// ═╩╝╚═╝╚ ╩ ╩╚═╝╩═╝╩
// If no `omit` clause was provided, give it a default value.
if (_.isUndefined(criteria.omit)) {
criteria.omit = [];
}//>-
// Verify that this is an array.
if (!_.isArray(criteria.omit)) {
throw flaverr('E_HIGHLY_IRREGULAR', new Error(
'The `omit` clause in the provided criteria is invalid. If provided, it should be an array of strings. But instead, got: '+
util.inspect(criteria.omit, {depth:5})+''
));
}//-•
// Loop through array and check each attribute name.
_.remove(criteria.omit, function (attrNameToOmit){
// Verify this is a string.
if (!_.isString(attrNameToOmit)) {
throw flaverr('E_HIGHLY_IRREGULAR', new Error(
'The `omit` clause in the provided criteria is invalid. If provided, it should be an array of strings (attribute names to omit. But one of the items is not a string: '+
util.inspect(attrNameToOmit, {depth:5})+''
));
}//-•
// If _explicitly_ trying to omit the primary key,
// then we say this is highly irregular.
//
// > Note that compatiblity with the `populates` query key is handled back in forgeStageTwoQuery().
if (attrNameToOmit === WLModel.primaryKey) {
throw flaverr('E_HIGHLY_IRREGULAR', new Error(
'The `omit` clause in the provided criteria explicitly attempts to omit the primary key (`'+WLModel.primaryKey+'`). But in the current version of Waterline, this is not possible.'
));
}//-•
// Try to look up the attribute def.
var attrDef;
try {
attrDef = getAttribute(attrNameToOmit, modelIdentity, orm);
} catch (e){
switch (e.code) {
case 'E_ATTR_NOT_REGISTERED':
// If no matching attribute is found, `attrDef` just stays undefined
// and we keep going.
break;
default: throw e;
}
}//</catch>
// If model is `schema: true`...
if (WLModel.hasSchema === true) {
// Make sure this matched a recognized attribute name.
if (!attrDef) {
throw flaverr('E_HIGHLY_IRREGULAR', new Error(
'The `omit` clause in the provided criteria contains an item (`'+attrNameToOmit+'`) which is not a recognized attribute in this model (`'+modelIdentity+'`).'
));
}//-•
}
// Else if model is `schema: false`...
else if (WLModel.hasSchema === false) {
// In this case, we just give up and throw an E_HIGHLY_IRREGULAR error here
// explaining what's up.
throw flaverr('E_HIGHLY_IRREGULAR', new Error(
'Cannot use `omit`, because the referenced model (`'+modelIdentity+'`) does not declare itself `schema: true`.'
));
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// FUTURE: double-check that there's not a reasonable way to do this in a way that
// supports both SQL and noSQL adapters.
//
// Best case, we get it to work for Mongo et al somehow, in which case we'd then
// also want to verify that each item is at least a valid Waterline attribute name here.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
} else { throw new Error('Consistency violation: Every instantiated Waterline model should always have a `hasSchema` property as either `true` or `false` (should have been derived from the `schema` model setting when Waterline was being initialized). But somehow, this model (`'+modelIdentity+'`) ended up with `hasSchema: '+util.inspect(WLModel.hasSchema, {depth:5})+'`'); }
// >-•
// Ensure that we're not trying to `omit` a plural association.
// If so, just strip it out.
//
// > Note that we also do a related check when we normalize the `populates` query key back in forgeStageTwoQuery().
if (attrDef && attrDef.collection) {
return true;
}//-•
// Otherwise, we'll keep this item in the `omit` clause.
return false;
});//</_.remove() :: each specified attr name to omit>
// ┌─┐┬ ┬┌─┐┌─┐┬┌─ ┌─┐┌─┐┬─┐ ╔╦╗╦ ╦╔═╗╦ ╦╔═╗╔═╗╔╦╗╔═╗╔═╗
// │ ├─┤├┤ │ ├┴┐ ├┤ │ │├┬┘ ║║║ ║╠═╝║ ║║ ╠═╣ ║ ║╣ ╚═╗
// └─┘┴ ┴└─┘└─┘┴ ┴ └ └─┘┴└─ ═╩╝╚═╝╩ ╩═╝╩╚═╝╩ ╩ ╩ ╚═╝╚═╝
// Ensure that no two items refer to the same attribute.
criteria.omit = _.uniq(criteria.omit);
// --• At this point, we know that both `select` AND `omit` are fully valid. So...
// ┌─┐┌┐┌┌─┐┬ ┬┬─┐┌─┐ ╔═╗╔╦╗╦╔╦╗ ┬ ╔═╗╔═╗╦ ╔═╗╔═╗╔╦╗ ┌┬┐┌─┐ ┌┐┌┌─┐┌┬┐ ┌─┐┬ ┌─┐┌─┐┬ ┬
// ├┤ │││└─┐│ │├┬┘├┤ ║ ║║║║║ ║ ┌┼─ ╚═╗║╣ ║ ║╣ ║ ║ │││ │ ││││ │ │ │ │ ├─┤└─┐├─┤
// └─┘┘└┘└─┘└─┘┴└─└─┘ ╚═╝╩ ╩╩ ╩ └┘ ╚═╝╚═╝╩═╝╚═╝╚═╝ ╩ ─┴┘└─┘ ┘└┘└─┘ ┴ └─┘┴─┘┴ ┴└─┘┴ ┴
// Make sure that `omit` and `select` are not BOTH specified as anything
// other than their default values. If so, then fail w/ an E_HIGHLY_IRREGULAR error.
var isNoopSelect = _.isEqual(criteria.select, ['*']);
var isNoopOmit = _.isEqual(criteria.omit, []);
if (!isNoopSelect && !isNoopOmit) {
throw flaverr('E_HIGHLY_IRREGULAR', new Error('Cannot specify both `omit` AND `select`. Please use one or the other.'));
}//-•
// IWMIH and the criteria is somehow no longer a dictionary, then freak out.
// (This is just to help us prevent present & future bugs in this utility itself.)
var isCriteriaNowValidDictionary = _.isObject(criteria) && !_.isArray(criteria) && !_.isFunction(criteria);
if (!isCriteriaNowValidDictionary) {
throw new Error('Consistency violation: At this point, the criteria should have already been normalized into a dictionary! But instead somehow it looks like this: '+util.inspect(criteria, {depth:5})+'');
}
// Return the normalized criteria dictionary.
return criteria;
};