UNPKG

waterline

Version:

An ORM for Node.js and the Sails framework

734 lines (623 loc) 40.1 kB
/** * Module dependencies */ var assert = require('assert'); var util = require('util'); var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); var getModel = require('../../ontology/get-model'); var normalizeConstraint = require('./normalize-constraint'); /** * Module constants */ // Predicate operators var PREDICATE_OPERATOR_KINDS = [ 'or', 'and' ]; /** * normalizeWhereClause() * * Validate and normalize the `where` clause, rejecting any obviously-unsupported * usage, and tolerating certain backwards-compatible things. * * ------------------------------------------------------------------------------------------ * @param {Ref} whereClause * A hypothetically well-formed `where` clause from a Waterline criteria. * (i.e. in a "stage 1 query") * > WARNING: * > IN SOME CASES (BUT NOT ALL!), THE PROVIDED VALUE WILL * > UNDERGO DESTRUCTIVE, IN-PLACE CHANGES JUST BY PASSING IT * > IN TO THIS UTILITY. * * @param {String} modelIdentity * The identity of the model this `where` clause 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 `where` clause, ready for use in a stage 2 query. * > Note that the originally provided `where` clause MAY ALSO HAVE BEEN * > MUTATED IN PLACE! * ------------------------------------------------------------------------------------------ * @throws {Error} If it encounters irrecoverable problems or unsupported usage in * the provided `where` clause. * @property {String} code * - E_WHERE_CLAUSE_UNUSABLE * * * @throws {Error} If the `where` clause 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 normalizeWhereClause(whereClause, modelIdentity, orm, meta) { // Look up the Waterline model for this query. // > This is so that we can reference the original model definition. var WLModel = getModel(modelIdentity, orm); // ┌─┐┬ ┬┌─┐┌─┐┌─┐┬─┐┌┬┐ ╔╦╗╦ ╦╔╦╗╔═╗╔╦╗╔═╗ ╔═╗╦═╗╔═╗╔═╗ ┌┬┐┌─┐┌┬┐┌─┐ ┬┌─┌─┐┬ ┬ // └─┐│ │├─┘├─┘│ │├┬┘ │ ║║║║ ║ ║ ╠═╣ ║ ║╣ ╠═╣╠╦╝║ ╦╚═╗ │││├┤ │ ├─┤ ├┴┐├┤ └┬┘ // └─┘└─┘┴ ┴ └─┘┴└─ ┴ ╩ ╩╚═╝ ╩ ╩ ╩ ╩ ╚═╝ ╩ ╩╩╚═╚═╝╚═╝ ┴ ┴└─┘ ┴ ┴ ┴ ┴ ┴└─┘ ┴ // Unless the `mutateArgs` meta key is enabled, deep-clone the entire `where` clause. if (!meta || !meta.mutateArgs) { whereClause = _.cloneDeep(whereClause); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Replace this naive implementation with something better. // (This isn't great because it can mess up things like Buffers... which you // shouldn't really be using in a `where` clause anyway, but still-- it makes // it way harder to figure out what's wrong when folks find themselves in that // situation. It could also affect any weird custom constraints for `type:'ref'` // attrs. And if the current approach were also used in valuesToSet, newRecord, // newRecords etc, it would matter even more.) // // The full list of query keys that need to be carefully checked: // • criteria // • populates // • newRecord // • newRecords // • valuesToSet // • targetRecordIds // • associatedIds // // The solution will probably mean distributing this deep clone behavior out // to the various places it's liable to come up. In reality, this will be // more performant anyway, since we won't be unnecessarily cloning things like // big JSON values, etc. // // The important thing is that this should do shallow clones of deeply-nested // control structures like top level query key dictionaries, criteria clauses, // predicates/constraints/modifiers in `where` clauses, etc. // // > And remember: Don't deep-clone functions. // > Note that, weirdly, it's fine to deep-clone dictionaries/arrays // > that contain nested functions (they just don't get cloned-- they're // > the same reference). But if you try to deep-clone a function at the // > top level, it gets messed up. // > // > More background on this: https://trello.com/c/VLXPvyN5 // > (Note that this behavior maintains backwards compatibility with Waterline <=0.12.) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - }//fi // ╔╦╗╔═╗╔═╗╔═╗╦ ╦╦ ╔╦╗ // ║║║╣ ╠╣ ╠═╣║ ║║ ║ // ═╩╝╚═╝╚ ╩ ╩╚═╝╩═╝╩ // If no `where` clause was provided, give it a default value. if (_.isUndefined(whereClause)) { whereClause = {}; }//>- // ╔═╗╔═╗╔╦╗╔═╗╔═╗╔╦╗╦╔╗ ╦╦ ╦╔╦╗╦ ╦ (COMPATIBILITY) // ║ ║ ║║║║╠═╝╠═╣ ║ ║╠╩╗║║ ║ ║ ╚╦╝ // ╚═╝╚═╝╩ ╩╩ ╩ ╩ ╩ ╩╚═╝╩╩═╝╩ ╩ ╩ // COMPATIBILITY // If where is `null`, turn it into an empty dictionary. if (_.isNull(whereClause)) { console.warn(); console.warn( 'Deprecated: In previous versions of Waterline, the specified `where` '+'\n'+ 'clause (`null`) would match ALL records in this model (`'+modelIdentity+'`). '+'\n'+ 'So for compatibility, that\'s what just happened. If that is what you intended '+'\n'+ 'then, in the future, please pass in `{}` instead, or simply omit the `where` '+'\n'+ 'clause altogether-- both of which are more explicit and future-proof ways of '+'\n'+ 'doing the same thing.\n'+ '\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.' ); console.warn(); whereClause = {}; }//>- // ┌┐┌┌─┐┬─┐┌┬┐┌─┐┬ ┬┌─┐┌─┐ ╔═╗╦╔═╦ ╦ ┌─┐┬─┐ ╦╔╗╔ ┌─┐┬ ┬┌─┐┬─┐┌┬┐┬ ┬┌─┐┌┐┌┌┬┐ // ││││ │├┬┘│││├─┤│ │┌─┘├┤ ╠═╝╠╩╗╚╗╔╝ │ │├┬┘ ║║║║ └─┐├─┤│ │├┬┘ │ ├─┤├─┤│││ ││ // ┘└┘└─┘┴└─┴ ┴┴ ┴┴─┘┴└─┘└─┘ ╩ ╩ ╩ ╚╝ └─┘┴└─ ╩╝╚╝ └─┘┴ ┴└─┘┴└─ ┴ ┴ ┴┴ ┴┘└┘─┴┘ // ┌─ ┌─┐┌┬┐ ┌┬┐┬ ┬┌─┐ ┌┬┐┌─┐┌─┐ ┬ ┌─┐┬ ┬┌─┐┬ ┌─┐┌─┐ ╦ ╦╦ ╦╔═╗╦═╗╔═╗ ─┐ // │─── ├─┤ │ │ ├─┤├┤ │ │ │├─┘ │ ├┤ └┐┌┘├┤ │ │ │├┤ ║║║╠═╣║╣ ╠╦╝║╣ ───│ // └─ ┴ ┴ ┴ ┴ ┴ ┴└─┘ ┴ └─┘┴ ┴─┘└─┘ └┘ └─┘┴─┘ └─┘└ ╚╩╝╩ ╩╚═╝╩╚═╚═╝ ─┘ // // If the `where` clause itself 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. // // ``` // where: [...] // ``` // // ``` // where: 'bar' // ``` // // ``` // where: 29 // ``` if (_.isArray(whereClause) || _.isNumber(whereClause) || _.isString(whereClause)) { var topLvlPkValuesOrPkValueInWhere = whereClause; // So expand that into the beginnings of a proper `where` 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.) whereClause = {}; whereClause[WLModel.primaryKey] = topLvlPkValuesOrPkValueInWhere; }//>- // ┬ ┬┌─┐┬─┐┬┌─┐┬ ┬ ┌┬┐┬ ┬┌─┐┌┬┐ ┌┬┐┬ ┬┌─┐ ╦ ╦╦ ╦╔═╗╦═╗╔═╗ ┌─┐┬ ┌─┐┬ ┬┌─┐┌─┐ // └┐┌┘├┤ ├┬┘│├┤ └┬┘ │ ├─┤├─┤ │ │ ├─┤├┤ ║║║╠═╣║╣ ╠╦╝║╣ │ │ ├─┤│ │└─┐├┤ // └┘ └─┘┴└─┴└ ┴ ┴ ┴ ┴┴ ┴ ┴ ┴ ┴ ┴└─┘ ╚╩╝╩ ╩╚═╝╩╚═╚═╝ └─┘┴─┘┴ ┴└─┘└─┘└─┘ // ┬┌─┐ ┌┐┌┌─┐┬ ┬ ┌─┐ ╔╦╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗╦═╗╦ ╦ // │└─┐ ││││ ││││ ├─┤ ║║║║ ║ ║║ ║║║║╠═╣╠╦╝╚╦╝ // ┴└─┘ ┘└┘└─┘└┴┘ ┴ ┴ ═╩╝╩╚═╝ ╩ ╩╚═╝╝╚╝╩ ╩╩╚═ ╩ // At this point, the `where` clause should be a dictionary. if (!_.isObject(whereClause) || _.isArray(whereClause) || _.isFunction(whereClause)) { throw flaverr('E_WHERE_CLAUSE_UNUSABLE', new Error( 'If provided, `where` clause should be a dictionary. But instead, got: '+ util.inspect(whereClause, {depth:5})+'' )); }//-• // ██╗ ██████╗ ███████╗ ██████╗██╗ ██╗██████╗ ███████╗██╗ ██████╗ ███╗ ██╗ ██╗ // ██╔╝ ██╔══██╗██╔════╝██╔════╝██║ ██║██╔══██╗██╔════╝██║██╔═══██╗████╗ ██║ ╚██╗ // ██╔╝ ██████╔╝█████╗ ██║ ██║ ██║██████╔╝███████╗██║██║ ██║██╔██╗ ██║ ╚██╗ // ╚██╗ ██╔══██╗██╔══╝ ██║ ██║ ██║██╔══██╗╚════██║██║██║ ██║██║╚██╗██║ ██╔╝ // ╚██╗ ██║ ██║███████╗╚██████╗╚██████╔╝██║ ██║███████║██║╚██████╔╝██║ ╚████║ ██╔╝ // ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ // ███████╗███████╗███████╗███████╗███████╗███████╗███████╗███████╗███████╗███████╗ // ╚══════╝╚══════╝╚══════╝╚══════╝╚══════╝╚══════╝╚══════╝╚══════╝╚══════╝╚══════╝ // ┌┬┐┌─┐ ┌┬┐┬ ┬┌─┐ ╦═╗╔═╗╔═╗╦ ╦╦═╗╔═╗╦╦ ╦╔═╗ ╔═╗╦═╗╔═╗╦ ╦╦ // │││ │ │ ├─┤├┤ ╠╦╝║╣ ║ ║ ║╠╦╝╚═╗║╚╗╔╝║╣ ║ ╠╦╝╠═╣║║║║ // ─┴┘└─┘ ┴ ┴ ┴└─┘ ╩╚═╚═╝╚═╝╚═╝╩╚═╚═╝╩ ╚╝ ╚═╝ ╚═╝╩╚═╩ ╩╚╩╝╩═╝ // Recursively iterate through the provided `where` clause, starting with the top level. // // > Note that we mutate the `where` clause IN PLACE here-- there is no return value // > from this self-calling recursive function. // // // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // EDGE CASES INVOLVING "VOID" AND "UNIVERSAL" // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // In order to provide the simplest possible interface for adapter implementors // (i.e. fully-normalized stage 2&3 queries, w/ the fewest possible numbers of // extraneous symbols) we need to handle certain edge cases in a special way. // // For example, an empty array of conjuncts/disjuncts is not EXACTLY invalid, per se. // Instead, what exactly it means depends on the circumstances: // // |-------------------------|-------------------|-------------------|-------------------| // | || Parent branch => | Parent is `and` | Parent is `or` | No parent | // | \/ This branch | (conjunct, `∩`) | (disjunct, `∪`) | (at top level) | // |-------------------------|===================|===================|===================| // | | | | | // | `{ and: [] }` | Rip out this | Throw to indicate | Replace entire | // | `{ ??: { nin: [] } }` | conjunct. | parent will match | `where` clause | // | `{}` | | EVERYTHING. | with `{}`. | // | | | | | // | Ξ : universal | x ∩ Ξ = x | x ∪ Ξ = Ξ | Ξ | // | ("matches everything") | <<identity>> | <<annihilator>> | (universal) | // |-------------------------|-------------------|-------------------|-------------------| // | | | | | // | `{ or: [] }` | Throw to indicate | Rip out this | Throw E_WOULD_... | // | `{ ??: { in: [] } }` | parent will NEVER | disjunct. | RESULT_IN_NOTHING | // | | match anything. | | error to indicate | // | | | | that this query | // | | | | is a no-op. | // | | | | | // | Ø : void | x ∩ Ø = Ø | x ∪ Ø = x | Ø | // | ("matches nothing") | <<annihilator>> | <<identity>> | (void) | // |-------------------------|-------------------|-------------------|-------------------| // // > For deeper reference, here are the boolean monotone laws: // > https://en.wikipedia.org/wiki/Boolean_algebra#Monotone_laws // > // > See also the "identity" and "domination" laws from fundamental set algebra: // > (the latter of which is roughly equivalent to the "annihilator" law from boolean algebra) // > https://en.wikipedia.org/wiki/Algebra_of_sets#Fundamentals // // Anyways, as it turns out, this is exactly how it should work for ANY universal/void // branch in the `where` clause. So as you can see below, we use this strategy to handle // various edge cases involving `and`, `or`, `nin`, `in`, and `{}`. // // **There are some particular bits to notice in the implementation below:** // • If removing this conjunct/disjunct would cause the parent predicate operator to have // NO items, then we recursively apply the normalization all the way back up the tree, // until we hit the root. That's taken care of above (in the place in the code where we // make the recursive call). // • If there is no containing conjunct/disjunct (i.e. because we're at the top-level), // then we'll either throw a E_WOULD_RESULT_IN_NOTHING error (if this is an `or`), // or revert the criteria to `{}` so it matches everything (if this is an `and`). // That gets taken care of below. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // // With that, let's begin. try { // Initially invoke our self-calling, recursive function. (function _recursiveStep(branch, recursionDepth, parent, indexInParent){ var MAX_RECURSION_DEPTH = 25; if (recursionDepth > MAX_RECURSION_DEPTH) { throw flaverr('E_WHERE_CLAUSE_UNUSABLE', new Error('This `where` clause seems to have a circular reference. Aborted automatically after reaching maximum recursion depth ('+MAX_RECURSION_DEPTH+').')); }//-• //-• IWMIH, we know that `branch` is a dictionary. // But that's about all we can trust. // // > In an already-fully-normalized `where` clause, we'd know that this dictionary // > would ALWAYS be a valid conjunct/disjunct. But since we're doing the normalizing // > here, we have to be more forgiving-- both for usability and backwards-compatibility. // ╔═╗╔╦╗╦═╗╦╔═╗ ╦╔═╔═╗╦ ╦╔═╗ ┬ ┬┬┌┬┐┬ ┬ ╦ ╦╔╗╔╔╦╗╔═╗╔═╗╦╔╗╔╔═╗╔╦╗ ┬─┐┬ ┬┌─┐ // ╚═╗ ║ ╠╦╝║╠═╝ ╠╩╗║╣ ╚╦╝╚═╗ ││││ │ ├─┤ ║ ║║║║ ║║║╣ ╠╣ ║║║║║╣ ║║ ├┬┘├─┤└─┐ // ╚═╝ ╩ ╩╚═╩╩ ╩ ╩╚═╝ ╩ ╚═╝ └┴┘┴ ┴ ┴ ┴ ╚═╝╝╚╝═╩╝╚═╝╚ ╩╝╚╝╚═╝═╩╝ ┴└─┴ ┴└─┘ // Strip out any keys with undefined values. _.each(_.keys(branch), function (key){ if (_.isUndefined(branch[key])) { delete branch[key]; } }); // Now count the keys. var origBranchKeys = _.keys(branch); // ┬ ┬┌─┐┌┐┌┌┬┐┬ ┌─┐ ╔═╗╔╦╗╔═╗╔╦╗╦ ╦ ┬ ┬┬ ┬┌─┐┬─┐┌─┐ ┌─┐┬ ┌─┐┬ ┬┌─┐┌─┐ // ├─┤├─┤│││ │││ ├┤ ║╣ ║║║╠═╝ ║ ╚╦╝ │││├─┤├┤ ├┬┘├┤ │ │ ├─┤│ │└─┐├┤ // ┴ ┴┴ ┴┘└┘─┴┘┴─┘└─┘ ╚═╝╩ ╩╩ ╩ ╩ └┴┘┴ ┴└─┘┴└─└─┘ └─┘┴─┘┴ ┴└─┘└─┘└─┘ // If there are 0 keys... if (origBranchKeys.length === 0) { // An empty dictionary means that this branch is universal (Ξ). // That is, that it would match _everything_. // // So we'll throw a special signal indicating that to the previous recursive step. // (or to our `catch` statement below, if we're at the top level-- i.e. an empty `where` clause.) // // > Note that we could just `return` instead of throwing if we're at the top level. // > That's because it's a no-op and throwing would achieve exactly the same thing. // > Since this is a hot code path, we might consider doing that as a future optimization. throw flaverr('E_UNIVERSAL', new Error('`{}` would match everything')); }//-• // ╔═╗╦═╗╔═╗╔═╗╔╦╗╦ ╦╦═╗╔═╗ ┌┐ ┬─┐┌─┐┌┐┌┌─┐┬ ┬ // ╠╣ ╠╦╝╠═╣║ ║ ║ ║╠╦╝║╣ ├┴┐├┬┘├─┤││││ ├─┤ // ╚ ╩╚═╩ ╩╚═╝ ╩ ╚═╝╩╚═╚═╝ └─┘┴└─┴ ┴┘└┘└─┘┴ ┴ // Now we may need to denormalize (or "fracture") this branch. // This is to normalize it such that it has only one key, with a // predicate operator on the RHS. // // For example: // ``` // { // name: 'foo', // age: 2323, // createdAt: 23238828382, // hobby: { contains: 'ball' } // } // ``` // ==> // ``` // { // and: [ // { name: 'foo' }, // { age: 2323 } // { createdAt: 23238828382 }, // { hobby: { contains: 'ball' } } // ] // } // ``` if (origBranchKeys.length > 1) { // Loop over each key in the original branch and build an array of conjuncts. var fracturedConjuncts = []; _.each(origBranchKeys, function (origKey){ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // For now, we don't log this warning. // It is still convenient to write criteria this way, and it's still // a bit too soon to determine whether we should be recommending a change. // // > NOTE: There are two sides to this, for sure. // > If you like this usage the way it is, please let @mikermcneil or // > @particlebanana know. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // // Check if this is a key for a predicate operator. // // e.g. the `or` in this example: // // ``` // // { // // age: { '>': 28 }, // // or: [ // // { name: { 'startsWith': 'Jon' } }, // // { name: { 'endsWith': 'Snow' } } // // ] // // } // // ``` // // // // If so, we'll still automatically map it. // // But also log a deprecation warning here, since it's more explicit to avoid // // using predicates within multi-facet shorthand (i.e. could have used an additional // // `and` predicate instead.) // // // if (_.contains(PREDICATE_OPERATOR_KINDS, origKey)) { // // // console.warn(); // // console.warn( // // 'Deprecated: Within a `where` clause, it tends to be better (and certainly '+'\n'+ // // 'more explicit) to use an `and` predicate when you need to group together '+'\n'+ // // 'constraints side by side with additional predicates (like `or`). This was '+'\n'+ // // 'automatically normalized on your behalf for compatibility\'s sake, but please '+'\n'+ // // 'consider changing your usage in the future:'+'\n'+ // // '```'+'\n'+ // // util.inspect(branch, {depth:5})+'\n'+ // // '```'+'\n'+ // // '> Warning: This backwards compatibility may be removed\n'+ // // '> in a future release of Sails/Waterline. If this usage\n'+ // // '> is left unchanged, then queries like this one may eventually \n'+ // // '> fail with an error.' // // ); // // console.warn(); // // }//>- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var conjunct = {}; conjunct[origKey] = branch[origKey]; fracturedConjuncts.push(conjunct); });//</ _.each() > // Change this branch so that it now contains a predicate consisting of // the conjuncts we built above. // // > Note that we change the branch in-place (on its parent) AND update // > our `branch` variable. If the branch has no parent (i.e. top lvl), // > then we change the actual variable we're using instead. This will // > change the return value from this utility. branch = { and: fracturedConjuncts }; if (parent) { parent[indexInParent] = branch; } else { whereClause = branch; } }//>- </if this branch was a multi-key dictionary> // --• IWMIH, then we know there is EXACTLY one key. var soleBranchKey = _.keys(branch)[0]; // ┬ ┬┌─┐┌┐┌┌┬┐┬ ┌─┐ ╔═╗╔═╗╔╗╔╔═╗╔╦╗╦═╗╔═╗╦╔╗╔╔╦╗ // ├─┤├─┤│││ │││ ├┤ ║ ║ ║║║║╚═╗ ║ ╠╦╝╠═╣║║║║ ║ // ┴ ┴┴ ┴┘└┘─┴┘┴─┘└─┘ ╚═╝╚═╝╝╚╝╚═╝ ╩ ╩╚═╩ ╩╩╝╚╝ ╩ // If this key is NOT a predicate (`and`/`or`)... if (!_.contains(PREDICATE_OPERATOR_KINDS, soleBranchKey)) { // ...then we know we're dealing with a constraint. // ╔═╗╦═╗╔═╗╔═╗╔╦╗╦ ╦╦═╗╔═╗ ┌─┐┌─┐┌┬┐┌─┐┬ ┌─┐─┐ ┬ ┌─┐┌─┐┌┐┌┌─┐┌┬┐┬─┐┌─┐┬┌┐┌┌┬┐ // ╠╣ ╠╦╝╠═╣║ ║ ║ ║╠╦╝║╣ │ │ ││││├─┘│ ├┤ ┌┴┬┘ │ │ ││││└─┐ │ ├┬┘├─┤││││ │ // ╚ ╩╚═╩ ╩╚═╝ ╩ ╚═╝╩╚═╚═╝ └─┘└─┘┴ ┴┴ ┴─┘└─┘┴ └─ └─┘└─┘┘└┘└─┘ ┴ ┴└─┴ ┴┴┘└┘ ┴ // ┌─ ┬┌─┐ ┬┌┬┐ ┬┌─┐ ┌┬┐┬ ┬┬ ┌┬┐┬ ┬┌─┌─┐┬ ┬ ─┐ // │ │├┤ │ │ │└─┐ ││││ ││ │ │───├┴┐├┤ └┬┘ │ // └─ ┴└ ┴ ┴ ┴└─┘ ┴ ┴└─┘┴─┘┴ ┴ ┴ ┴└─┘ ┴ ─┘ // Before proceeding, we may need to fracture the RHS of this key. // (if it is a complex constraint w/ multiple keys-- like a "range" constraint) // // > This is to normalize it such that every complex constraint ONLY EVER has one key. // > In order to do this, we may need to reach up to our highest ancestral predicate. var isComplexConstraint = _.isObject(branch[soleBranchKey]) && !_.isArray(branch[soleBranchKey]) && !_.isFunction(branch[soleBranchKey]); // If this complex constraint has multiple keys... if (isComplexConstraint && _.keys(branch[soleBranchKey]).length > 1){ // Then fracture it before proceeding. var complexConstraint = branch[soleBranchKey]; // Loop over each modifier in the complex constraint and build an array of conjuncts. var fracturedModifierConjuncts = []; _.each(complexConstraint, function (modifier, modifierKind){ var conjunct = {}; conjunct[soleBranchKey] = {}; conjunct[soleBranchKey][modifierKind] = modifier; fracturedModifierConjuncts.push(conjunct); });//</ _.each() key in the complex constraint> // Change this branch so that it now contains a predicate consisting of // the new conjuncts we just built for these modifiers. // // > Note that we change the branch in-place (on its parent) AND update // > our `branch` variable. If the branch has no parent (i.e. top lvl), // > then we change the actual variable we're using instead. This will // > change the return value from this utility. // branch = { and: fracturedModifierConjuncts }; if (parent) { parent[indexInParent] = branch; } else { whereClause = branch; } // > Also note that we update the sole branch key variable. soleBranchKey = _.keys(branch)[0]; // Now, since we know our branch is a predicate, we'll continue on. // (see predicate handling code below) } // Otherwise, we can go ahead and normalize the constraint, then bail. else { // ╔╗╔╔═╗╦═╗╔╦╗╔═╗╦ ╦╔═╗╔═╗ ╔═╗╔═╗╔╗╔╔═╗╔╦╗╦═╗╔═╗╦╔╗╔╔╦╗ // ║║║║ ║╠╦╝║║║╠═╣║ ║╔═╝║╣ ║ ║ ║║║║╚═╗ ║ ╠╦╝╠═╣║║║║ ║ // ╝╚╝╚═╝╩╚═╩ ╩╩ ╩╩═╝╩╚═╝╚═╝ ╚═╝╚═╝╝╚╝╚═╝ ╩ ╩╚═╩ ╩╩╝╚╝ ╩ // Normalize the constraint itself. // (note that this checks the RHS, but it also checks the key aka constraint target -- i.e. the attr name) try { branch[soleBranchKey] = normalizeConstraint(branch[soleBranchKey], soleBranchKey, modelIdentity, orm, meta); } catch (e) { switch (e.code) { case 'E_CONSTRAINT_NOT_USABLE': throw flaverr('E_WHERE_CLAUSE_UNUSABLE', new Error( 'Could not filter by `'+soleBranchKey+'`: '+ e.message )); case 'E_CONSTRAINT_WOULD_MATCH_EVERYTHING': throw flaverr('E_UNIVERSAL', e); case 'E_CONSTRAINT_WOULD_MATCH_NOTHING': throw flaverr('E_VOID', e); default: throw e; } }//</catch> // Then bail early. return; }//</ else (i.e. case where the constraint was good to go w/o needing any fracturing)> }//</ if the sole branch key is NOT a predicate > // >-• IWMIH, then we know that this branch's sole key is a predicate (`and`/`or`). // (If it isn't, then our code above has a bug.) assert(soleBranchKey === 'and' || soleBranchKey === 'or', 'Should never have made it here if the sole branch key is not `and` or `or`!'); // ██╗ ██╗ █████╗ ███╗ ██╗██████╗ ██╗ ███████╗ // ██║ ██║██╔══██╗████╗ ██║██╔══██╗██║ ██╔════╝ // ███████║███████║██╔██╗ ██║██║ ██║██║ █████╗ // ██╔══██║██╔══██║██║╚██╗██║██║ ██║██║ ██╔══╝ // ██║ ██║██║ ██║██║ ╚████║██████╔╝███████╗███████╗ // ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚══════╝ // // ██████╗ ██████╗ ███████╗██████╗ ██╗ ██████╗ █████╗ ████████╗███████╗ // ██╔══██╗██╔══██╗██╔════╝██╔══██╗██║██╔════╝██╔══██╗╚══██╔══╝██╔════╝ // ██████╔╝██████╔╝█████╗ ██║ ██║██║██║ ███████║ ██║ █████╗ // ██╔═══╝ ██╔══██╗██╔══╝ ██║ ██║██║██║ ██╔══██║ ██║ ██╔══╝ // ██║ ██║ ██║███████╗██████╔╝██║╚██████╗██║ ██║ ██║ ███████╗ // ╚═╝ ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ // // // ``` ``` // { { // or: [...] and: [...] // } } // ``` ``` var conjunctsOrDisjuncts = branch[soleBranchKey]; // RHS of a predicate must always be an array. if (!_.isArray(conjunctsOrDisjuncts)) { throw flaverr('E_WHERE_CLAUSE_UNUSABLE', new Error('Expected an array at `'+soleBranchKey+'`, but instead got: '+util.inspect(conjunctsOrDisjuncts,{depth: 5})+'\n(`and`/`or` should always be provided with an array on the right-hand side.)')); }//-• // Now loop over each conjunct or disjunct within this AND/OR predicate. // Along the way, track any that will need to be trimmed out. var indexesToRemove = []; _.each(conjunctsOrDisjuncts, function (conjunctOrDisjunct, i){ // If conjunct/disjunct is `undefined`, trim it out and bail to the next one. if (conjunctsOrDisjuncts[i] === undefined) { indexesToRemove.push(i); return; }//• // Check that each conjunct/disjunct is a plain dictionary, no funny business. if (!_.isObject(conjunctOrDisjunct) || _.isArray(conjunctOrDisjunct) || _.isFunction(conjunctOrDisjunct)) { throw flaverr('E_WHERE_CLAUSE_UNUSABLE', new Error('Expected each item within an `and`/`or` predicate\'s array to be a dictionary (plain JavaScript object). But instead, got: `'+util.inspect(conjunctOrDisjunct,{depth: 5})+'`')); } // Recursive call try { _recursiveStep(conjunctOrDisjunct, recursionDepth+1, conjunctsOrDisjuncts, i); } catch (e) { switch (e.code) { // If this conjunct or disjunct is universal (Ξ)... case 'E_UNIVERSAL': // If this item is a disjunct, then annihilate our branch by throwing this error // on up for the previous recursive step to take care of. // ``` // x ∪ Ξ = Ξ // ``` if (soleBranchKey === 'or') { throw e; }//-• // Otherwise, rip it out of the array. // ``` // x ∩ Ξ = x // ``` indexesToRemove.push(i); break; // If this conjunct or disjunct is void (Ø)... case 'E_VOID': // If this item is a conjunct, then annihilate our branch by throwing this error // on up for the previous recursive step to take care of. // ``` // x ∩ Ø = Ø // ``` if (soleBranchKey === 'and') { throw e; }//-• // Otherwise, rip it out of the array. // ``` // x ∪ Ø = x // ``` indexesToRemove.push(i); break; default: throw e; } }//</catch> });//</each conjunct or disjunct inside of predicate operator> // If any conjuncts/disjuncts were scheduled for removal above, // go ahead and take care of that now. if (indexesToRemove.length > 0) { for (var i = 0; i < indexesToRemove.length; i++) { var indexToRemove = indexesToRemove[i] - i; conjunctsOrDisjuncts.splice(indexToRemove, 1); }//</for> }//>- // If the array is NOT EMPTY, then this is the normal case, and we can go ahead and bail. if (conjunctsOrDisjuncts.length > 0) { return; }//-• // Otherwise, the predicate array is empty (e.g. `{ or: [] }` / `{ and: [] }`) // // For our purposes here, we just need to worry about signaling either "universal" or "void". // (see table above for more information). // If this branch is universal (i.e. matches everything / `{and: []}`) // ``` // Ξ // ``` if (soleBranchKey === 'and') { throw flaverr('E_UNIVERSAL', new Error('`{and: []}` with an empty array would match everything.')); } // Otherwise, this branch is void (i.e. matches nothing / `{or: []}`) // ``` // Ø // ``` else { throw flaverr('E_VOID', new Error('`{or: []}` with an empty array would match nothing.')); } })(whereClause, 0, undefined, undefined); //</self-invoking recursive function that kicked off our recursion with the `where` clause> } catch (e) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Note: // This `catch` block exists to handle top-level E_UNIVERSAL and E_VOID exceptions. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - switch (e.code) { // If an E_UNIVERSAL exception made it all the way up here, then we know that // this `where` clause should match EVERYTHING. So we set it to `{}`. case 'E_UNIVERSAL': whereClause = {}; break; // If an E_VOID exception made it all the way up here, then we know that // this `where` clause would match NOTHING. So we throw `E_WOULD_RESULT_IN_NOTHING` // to pass that message along. case 'E_VOID': throw flaverr('E_WOULD_RESULT_IN_NOTHING', new Error('Would match nothing')); default: throw e; } }//</catch> // ███████╗███████╗███████╗███████╗███████╗███████╗███████╗███████╗███████╗███████╗ // ╚══════╝╚══════╝╚══════╝╚══════╝╚══════╝╚══════╝╚══════╝╚══════╝╚══════╝╚══════╝ // // ██╗ ██╗ ██████╗ ███████╗ ██████╗██╗ ██╗██████╗ ███████╗██╗ ██████╗ ███╗ ██╗ ██╗ // ██╔╝ ██╔╝ ██╔══██╗██╔════╝██╔════╝██║ ██║██╔══██╗██╔════╝██║██╔═══██╗████╗ ██║ ╚██╗ // ██╔╝ ██╔╝ ██████╔╝█████╗ ██║ ██║ ██║██████╔╝███████╗██║██║ ██║██╔██╗ ██║ ╚██╗ // ╚██╗ ██╔╝ ██╔══██╗██╔══╝ ██║ ██║ ██║██╔══██╗╚════██║██║██║ ██║██║╚██╗██║ ██╔╝ // ╚██╗██╔╝ ██║ ██║███████╗╚██████╗╚██████╔╝██║ ██║███████║██║╚██████╔╝██║ ╚████║ ██╔╝ // ╚═╝╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ // // Return the modified `where` clause. return whereClause; };