UNPKG

waterline

Version:

An ORM for Node.js and the Sails framework

624 lines (516 loc) 35.9 kB
/** * Module dependencies */ var assert = require('assert'); var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); var parley = require('parley'); var buildOmen = require('../utils/query/build-omen'); var forgeStageTwoQuery = require('../utils/query/forge-stage-two-query'); var getQueryModifierMethods = require('../utils/query/get-query-modifier-methods'); var verifyModelMethodContext = require('../utils/query/verify-model-method-context'); /** * Module constants */ var DEFERRED_METHODS = getQueryModifierMethods('replaceCollection'); /** * replaceCollection() * * Replace all members of the specified collection in each of the target record(s). * * ``` * // For users 3 and 4, change their "pets" collection to contain ONLY pets 99 and 98. * User.replaceCollection([3,4], 'pets', [99,98]).exec(...); * ``` * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * * Usage without deferred object: * ================================================ * * @param {Array?|String?|Number?} targetRecordIds * * @param {String?} collectionAttrName * * @param {Array?} associatedIds * * @param {Function?} explicitCbMaybe * Callback function to run when query has either finished successfully or errored. * (If unspecified, will return a Deferred object instead of actually doing anything.) * * @param {Ref?} meta * For internal use. * * @returns {Ref?} Deferred object if no `explicitCbMaybe` callback was provided * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * * The underlying query keys: * ============================== * * @qkey {Array|String|Number} targetRecordIds * The primary key value(s) (i.e. ids) for the parent record(s). * Must be a number or string; e.g. '507f191e810c19729de860ea' or 49 * Or an array of numbers or strings; e.g. ['507f191e810c19729de860ea', '14832ace0c179de897'] or [49, 32, 37] * If an empty array (`[]`) is specified, then this is a no-op. * * @qkey {String} collectionAttrName * The name of the collection association (e.g. "pets") * * @qkey {Array} associatedIds * The primary key values (i.e. ids) for the child records that will be the new members of the association. * Must be an array of numbers or strings; e.g. ['334724948aca33ea0f13', '913303583e0af031358bac931'] or [18, 19] * Specify an empty array (`[]`) to completely wipe out the collection's contents. * * @qkey {Dictionary?} meta * @qkey {String} using * @qkey {String} method * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ module.exports = function replaceCollection(/* targetRecordIds?, collectionAttrName?, associatedIds?, explicitCbMaybe?, meta? */) { // Verify `this` refers to an actual Sails/Waterline model. verifyModelMethodContext(this); // Set up a few, common local vars for convenience / familiarity. var WLModel = this; var orm = this.waterline; var modelIdentity = this.identity; // Build an omen for potential use in the asynchronous callback below. var omen = buildOmen(replaceCollection); // Build query w/ initial, universal keys. var query = { method: 'replaceCollection', using: modelIdentity }; // ██╗ ██╗ █████╗ ██████╗ ██╗ █████╗ ██████╗ ██╗ ██████╗███████╗ // ██║ ██║██╔══██╗██╔══██╗██║██╔══██╗██╔══██╗██║██╔════╝██╔════╝ // ██║ ██║███████║██████╔╝██║███████║██║ ██║██║██║ ███████╗ // ╚██╗ ██╔╝██╔══██║██╔══██╗██║██╔══██║██║ ██║██║██║ ╚════██║ // ╚████╔╝ ██║ ██║██║ ██║██║██║ ██║██████╔╝██║╚██████╗███████║ // ╚═══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═════╝╚══════╝ // // Handle the various supported usage possibilities // (locate the `explicitCbMaybe` callback, and extend the `query` dictionary) // The `explicitCbMaybe` callback, if one was provided. var explicitCbMaybe; // Handle the various supported usage possibilities // (locate the `explicitCbMaybe` callback) // // > Note that we define `args` so that we can insulate access // > to the arguments provided to this function. var args = arguments; (function _handleVariadicUsage(){ // The metadata container, if one was provided. var _meta; // Handle first two arguments: // (both of which always have exactly one meaning) // // • replaceCollection(targetRecordIds, collectionAttrName, ...) query.targetRecordIds = args[0]; query.collectionAttrName = args[1]; // Handle double meaning of third argument, & then handle the rest: // // • replaceCollection(____, ____, associatedIds, explicitCbMaybe, _meta) var is3rdArgArray = !_.isUndefined(args[2]); if (is3rdArgArray) { query.associatedIds = args[2]; explicitCbMaybe = args[3]; _meta = args[4]; } // • replaceCollection(____, ____, explicitCbMaybe, _meta) else { explicitCbMaybe = args[2]; _meta = args[3]; } // Fold in `_meta`, if relevant. if (!_.isUndefined(_meta)) { query.meta = _meta; } // >- })(); // ██████╗ ███████╗███████╗███████╗██████╗ // ██╔══██╗██╔════╝██╔════╝██╔════╝██╔══██╗ // ██║ ██║█████╗ █████╗ █████╗ ██████╔╝ // ██║ ██║██╔══╝ ██╔══╝ ██╔══╝ ██╔══██╗ // ██████╔╝███████╗██║ ███████╗██║ ██║ // ╚═════╝ ╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝ // // ██╗███╗ ███╗ █████╗ ██╗ ██╗██████╗ ███████╗██╗ // ██╔╝████╗ ████║██╔══██╗╚██╗ ██╔╝██╔══██╗██╔════╝╚██╗ // ██║ ██╔████╔██║███████║ ╚████╔╝ ██████╔╝█████╗ ██║ // ██║ ██║╚██╔╝██║██╔══██║ ╚██╔╝ ██╔══██╗██╔══╝ ██║ // ╚██╗██║ ╚═╝ ██║██║ ██║ ██║ ██████╔╝███████╗██╔╝ // ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ // // ┌┐ ┬ ┬┬┬ ┌┬┐ ┬ ┬─┐┌─┐┌┬┐┬ ┬┬─┐┌┐┌ ┌┐┌┌─┐┬ ┬ ┌┬┐┌─┐┌─┐┌─┐┬─┐┬─┐┌─┐┌┬┐ // ├┴┐│ │││ ││ ┌┼─ ├┬┘├┤ │ │ │├┬┘│││ │││├┤ │││ ││├┤ ├┤ ├┤ ├┬┘├┬┘├┤ ││ // └─┘└─┘┴┴─┘─┴┘ └┘ ┴└─└─┘ ┴ └─┘┴└─┘└┘ ┘└┘└─┘└┴┘ ─┴┘└─┘└ └─┘┴└─┴└─└─┘─┴┘ // ┌─ ┬┌─┐ ┬─┐┌─┐┬ ┌─┐┬ ┬┌─┐┌┐┌┌┬┐ ─┐ // │─── │├┤ ├┬┘├┤ │ ├┤ └┐┌┘├─┤│││ │ ───│ // └─ ┴└ ┴└─└─┘┴─┘└─┘ └┘ ┴ ┴┘└┘ ┴ ─┘ // If an explicit callback function was specified, then immediately run the logic below // and trigger the explicit callback when the time comes. Otherwise, build and return // a new Deferred now. (If/when the Deferred is executed, the logic below will run.) return parley( function (done){ // Otherwise, IWMIH, we know that it's time to actually do some stuff. // So... // // ███████╗██╗ ██╗███████╗ ██████╗██╗ ██╗████████╗███████╗ // ██╔════╝╚██╗██╔╝██╔════╝██╔════╝██║ ██║╚══██╔══╝██╔════╝ // █████╗ ╚███╔╝ █████╗ ██║ ██║ ██║ ██║ █████╗ // ██╔══╝ ██╔██╗ ██╔══╝ ██║ ██║ ██║ ██║ ██╔══╝ // ███████╗██╔╝ ██╗███████╗╚██████╗╚██████╔╝ ██║ ███████╗ // ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝ // ╔═╗╔═╗╦═╗╔═╗╔═╗ ┌─┐┌┬┐┌─┐┌─┐┌─┐ ┌┬┐┬ ┬┌─┐ ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬ // ╠╣ ║ ║╠╦╝║ ╦║╣ └─┐ │ ├─┤│ ┬├┤ │ ││││ │ │─┼┐│ │├┤ ├┬┘└┬┘ // ╚ ╚═╝╩╚═╚═╝╚═╝ └─┘ ┴ ┴ ┴└─┘└─┘ ┴ └┴┘└─┘ └─┘└└─┘└─┘┴└─ ┴ // // Forge a stage 2 query (aka logical protostatement) try { forgeStageTwoQuery(query, orm); } catch (e) { switch (e.code) { case 'E_INVALID_TARGET_RECORD_IDS': return done( flaverr({ name: 'UsageError', code: e.code, details: e.details, message: 'The target record ids (i.e. first argument) passed to `.replaceCollection()` '+ 'should be the ID (or IDs) of compatible target records whose collection will '+ 'be modified.\n'+ 'Details:\n'+ ' ' + e.details + '\n' }, omen) ); case 'E_INVALID_COLLECTION_ATTR_NAME': return done( flaverr({ name: 'UsageError', code: e.code, details: e.details, message: 'The collection attr name (i.e. second argument) to `.replaceCollection()` should '+ 'be the name of a collection association from this model.\n'+ 'Details:\n'+ ' ' + e.details + '\n' }, omen) ); case 'E_INVALID_ASSOCIATED_IDS': return done( flaverr({ name: 'UsageError', code: e.code, details: e.details, message: 'The associated ids (i.e. using `.members()`, or the third argument) passed to `.replaceCollection()` should be '+ 'the ID (or IDs) of associated records to use.\n'+ 'Details:\n'+ ' ' + e.details + '\n' }, omen) ); case 'E_NOOP': return done(); // ^ tolerate no-ops -- i.e. empty array of target record ids case 'E_INVALID_META': return done( flaverr({ name: 'UsageError', code: e.code, details: e.details, message: e.message }, omen) ); // ^ when the standard usage error message is good enough as-is, without any further customization default: return done(e); // ^ when an internal, miscellaneous, or unexpected error occurs } } // >-• // ┌┐┌┌─┐┬ ┬ ╔═╗╔═╗╔╦╗╦ ╦╔═╗╦ ╦ ╦ ╦ ┌┬┐┌─┐┬ ┬┌─ ┌┬┐┌─┐ ┌┬┐┬ ┬┌─┐ ┌┬┐┌┐ ┌─┐ // ││││ ││││ ╠═╣║ ║ ║ ║╠═╣║ ║ ╚╦╝ │ ├─┤│ ├┴┐ │ │ │ │ ├─┤├┤ ││├┴┐└─┐ // ┘└┘└─┘└┴┘ ╩ ╩╚═╝ ╩ ╚═╝╩ ╩╩═╝╩═╝╩ ┴ ┴ ┴┴─┘┴ ┴ ┴ └─┘ ┴ ┴ ┴└─┘ ─┴┘└─┘└─┘ (function (proceed){ // Get the model being used as the parent var WLModel = orm.collections[query.using]; try { assert.equal(query.using.toLowerCase(), query.using, '`query.using` (identity) should have already been normalized before getting here! But it was not: '+query.using); } catch (e) { return proceed(e); } // Look up the association by name in the schema definition. var schemaDef = WLModel.schema[query.collectionAttrName]; // Look up the associated collection using the schema def which should have // join tables normalized var WLChild = orm.collections[schemaDef.collection]; try { assert.equal(schemaDef.collection.toLowerCase(), schemaDef.collection, '`schemaDef.collection` (identity) should have already been normalized before getting here! But it was not: '+schemaDef.collection); assert.equal(schemaDef.referenceIdentity.toLowerCase(), schemaDef.referenceIdentity, '`schemaDef.referenceIdentity` (identity) should have already been normalized before getting here! But it was not: '+schemaDef.referenceIdentity); assert.equal(Object.getPrototypeOf(WLChild).identity.toLowerCase(), Object.getPrototypeOf(WLChild).identity, '`Object.getPrototypeOf(WLChild).identity` (identity) should have already been normalized before getting here! But it was not: '+Object.getPrototypeOf(WLChild).identity); } catch (e) { return proceed(e); } // Flag to determine if the WLChild is a manyToMany relation var manyToMany = false; // Check if the schema references something other than the WLChild if (schemaDef.referenceIdentity !== Object.getPrototypeOf(WLChild).identity) { manyToMany = true; WLChild = orm.collections[schemaDef.referenceIdentity]; } // Check if the child is a join table if (_.has(Object.getPrototypeOf(WLChild), 'junctionTable') && WLChild.junctionTable) { manyToMany = true; } // Check if the child is a through table if (_.has(Object.getPrototypeOf(WLChild), 'throughTable') && _.keys(WLChild.throughTable).length) { manyToMany = true; } // Ensure the query skips lifecycle callbacks // Build a modified shallow clone of the originally-provided `meta` var modifiedMeta = _.extend({}, query.meta || {}, { skipAllLifecycleCallbacks: true }); // ██╗███╗ ██╗ ███╗ ███╗██╗ // ██╔╝████╗ ██║ ████╗ ████║╚██╗ // ██║ ██╔██╗ ██║ ██╔████╔██║ ██║ // ██║ ██║╚██╗██║ ██║╚██╔╝██║ ██║ // ╚██╗██║ ╚████║██╗██╗██║ ╚═╝ ██║██╔╝ // ╚═╝╚═╝ ╚═══╝╚═╝╚═╝╚═╝ ╚═╝╚═╝ // // ███╗ ███╗ █████╗ ███╗ ██╗██╗ ██╗ ████████╗ ██████╗ ███╗ ███╗ █████╗ ███╗ ██╗██╗ ██╗ // ████╗ ████║██╔══██╗████╗ ██║╚██╗ ██╔╝ ╚══██╔══╝██╔═══██╗ ████╗ ████║██╔══██╗████╗ ██║╚██╗ ██╔╝ // ██╔████╔██║███████║██╔██╗ ██║ ╚████╔╝ ██║ ██║ ██║ ██╔████╔██║███████║██╔██╗ ██║ ╚████╔╝ // ██║╚██╔╝██║██╔══██║██║╚██╗██║ ╚██╔╝ ██║ ██║ ██║ ██║╚██╔╝██║██╔══██║██║╚██╗██║ ╚██╔╝ // ██║ ╚═╝ ██║██║ ██║██║ ╚████║ ██║ ██║ ╚██████╔╝ ██║ ╚═╝ ██║██║ ██║██║ ╚████║ ██║ // ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ // // If the collection uses a join table, build a query that removes the records // from the table. if (manyToMany) { // ╔╗ ╦ ╦╦╦ ╔╦╗ ┬─┐┌─┐┌─┐┌─┐┬─┐┌─┐┌┐┌┌─┐┌─┐ ┌┬┐┌─┐┌─┐┌─┐┬┌┐┌┌─┐ // ╠╩╗║ ║║║ ║║ ├┬┘├┤ ├┤ ├┤ ├┬┘├┤ ││││ ├┤ │││├─┤├─┘├─┘│││││ ┬ // ╚═╝╚═╝╩╩═╝═╩╝ ┴└─└─┘└ └─┘┴└─└─┘┘└┘└─┘└─┘ ┴ ┴┴ ┴┴ ┴ ┴┘└┘└─┘ // // Maps out the parent and child attribute names to use for the query. var parentReference; var childReference; // Find the parent reference if (_.has(Object.getPrototypeOf(WLChild), 'junctionTable') && WLChild.junctionTable) { // Assumes the generated junction table will only ever have two foreign key // values. Should be safe for now and any changes would need to be made in // Waterline-Schema where a map could be formed anyway. _.each(WLChild.schema, function(wlsAttrDef, key) { if (!_.has(wlsAttrDef, 'references')) { return; } // If this is the piece of the join table, set the parent reference. if (_.has(wlsAttrDef, 'columnName') && wlsAttrDef.columnName === schemaDef.on) { parentReference = key; } }); } // If it's a through table, grab the parent and child reference from the // through table mapping that was generated by Waterline-Schema. else if (_.has(Object.getPrototypeOf(WLChild), 'throughTable')) { childReference = WLChild.throughTable[WLModel.identity + '.' + query.collectionAttrName]; _.each(WLChild.throughTable, function(rhs, key) { if (key !== WLModel.identity + '.' + query.collectionAttrName) { parentReference = rhs; } }); }//>- // Find the child reference in a junction table if (_.has(Object.getPrototypeOf(WLChild), 'junctionTable') && WLChild.junctionTable) { // Assumes the generated junction table will only ever have two foreign key // values. Should be safe for now and any changes would need to be made in // Waterline-Schema where a map could be formed anyway. _.each(WLChild.schema, function(wlsAttrDef, key) { if (!_.has(wlsAttrDef, 'references')) { return; } // If this is the other piece of the join table, set the child reference. if (_.has(wlsAttrDef, 'columnName') && wlsAttrDef.columnName !== schemaDef.on) { childReference = key; } }); } // ╔╗ ╦ ╦╦╦ ╔╦╗ ┌┬┐┌─┐┌─┐┌┬┐┬─┐┌─┐┬ ┬ ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬ // ╠╩╗║ ║║║ ║║ ││├┤ └─┐ │ ├┬┘│ │└┬┘ │─┼┐│ │├┤ ├┬┘└┬┘ // ╚═╝╚═╝╩╩═╝═╩╝ ─┴┘└─┘└─┘ ┴ ┴└─└─┘ ┴ └─┘└└─┘└─┘┴└─ ┴ // // When replacing a collection, the first step is to remove all the records // for the target id's in the join table. var criteriaOfDestruction = { where: {} }; criteriaOfDestruction.where[parentReference] = { in: query.targetRecordIds }; // Don't worry about fetching modifiedMeta.fetch = false; // ╔╗ ╦ ╦╦╦ ╔╦╗ ┬┌┐┌┌─┐┌─┐┬─┐┌┬┐ ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬ // ╠╩╗║ ║║║ ║║ ││││└─┐├┤ ├┬┘ │ │─┼┐│ │├┤ ├┬┘└┬┘ // ╚═╝╚═╝╩╩═╝═╩╝ ┴┘└┘└─┘└─┘┴└─ ┴ └─┘└└─┘└─┘┴└─ ┴ // // Then build up an insert query for creating the new join table records. var insertRecords = []; // For each target record, build an insert query for the associated records. _.each(query.targetRecordIds, function(targetId) { _.each(query.associatedIds, function(associatedId) { var record = {}; record[parentReference] = targetId; record[childReference] = associatedId; insertRecords.push(record); }); }); // ╦═╗╦ ╦╔╗╔ ┌┬┐┌─┐┌─┐┌┬┐┬─┐┌─┐┬ ┬ ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬ // ╠╦╝║ ║║║║ ││├┤ └─┐ │ ├┬┘│ │└┬┘ │─┼┐│ │├┤ ├┬┘└┬┘ // ╩╚═╚═╝╝╚╝ ─┴┘└─┘└─┘ ┴ ┴└─└─┘ ┴ └─┘└└─┘└─┘┴└─ ┴ WLChild.destroy(criteriaOfDestruction, function $afterDestroyingChildRecords(err) { if (err) { return proceed(err); } // If there were no associated id's to insert, exit out if (!query.associatedIds.length) { return proceed(); } // ╦═╗╦ ╦╔╗╔ ┌─┐┬─┐┌─┐┌─┐┌┬┐┌─┐ ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬ // ╠╦╝║ ║║║║ │ ├┬┘├┤ ├─┤ │ ├┤ │─┼┐│ │├┤ ├┬┘└┬┘ // ╩╚═╚═╝╝╚╝ └─┘┴└─└─┘┴ ┴ ┴ └─┘ └─┘└└─┘└─┘┴└─ ┴ WLChild.createEach(insertRecords, proceed, modifiedMeta); }, modifiedMeta); return; }//-• // ██╗███╗ ██╗ ██╗██╗ // ██╔╝████╗ ██║ ███║╚██╗ // ██║ ██╔██╗ ██║ ╚██║ ██║ // ██║ ██║╚██╗██║ ██║ ██║ // ╚██╗██║ ╚████║██╗██╗██║██╔╝ // ╚═╝╚═╝ ╚═══╝╚═╝╚═╝╚═╝╚═╝ // // ██████╗ ███████╗██╗ ██████╗ ███╗ ██╗ ██████╗ ███████╗ ████████╗ ██████╗ // ██╔══██╗██╔════╝██║ ██╔═══██╗████╗ ██║██╔════╝ ██╔════╝ ╚══██╔══╝██╔═══██╗ // ██████╔╝█████╗ ██║ ██║ ██║██╔██╗ ██║██║ ███╗███████╗ ██║ ██║ ██║ // ██╔══██╗██╔══╝ ██║ ██║ ██║██║╚██╗██║██║ ██║╚════██║ ██║ ██║ ██║ // ██████╔╝███████╗███████╗╚██████╔╝██║ ╚████║╚██████╔╝███████║ ██║ ╚██████╔╝ // ╚═════╝ ╚══════╝╚══════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ // // Otherwise the child records need to have their foreign keys updated to reflect the // new realities of the association. We'll either (A) set the new child records to // have the same fk and null out any other existing child records or (B) just null out // all existing child records. That's because there should only ever be either (A) exactly // one target record with >=1 new child records to associate or (B) >=1 target records with // zero new child records to associate (i.e. a null-out) if (query.targetRecordIds.length >= 2 && query.associatedIds.length > 0) { return proceed(new Error('Consistency violation: Too many target record ids and associated ids-- should never have been possible, because this query should have been halted when it was being forged at stage 2.')); } if (query.targetRecordIds.length === 0) { return proceed(new Error('Consistency violation: No target record ids-- should never have been possible, because this query should have been halted when it was being forged at stage 2.')); } // First, check whether the foreign key attribute is required/optional so that we know whether // it's safe to null things out without checking for collisions beforehand. var isFkAttributeOptional = !WLChild.attributes[schemaDef.via].required; (function(proceed){ if (isFkAttributeOptional) { return proceed(undefined, 0); }//• var potentialCollisionCriteria = { where: {} }; potentialCollisionCriteria.where[schemaDef.via] = { in: query.targetRecordIds }; potentialCollisionCriteria.where[WLChild.primaryKey] = { nin: query.associatedIds }; WLChild.count(potentialCollisionCriteria, function(err, total) { if (err) { return proceed(err); } return proceed(undefined, total); });//_∏_ </WLChild.count()> })(function (err, numCollisions) { if (err) { return proceed(err); } if (!isFkAttributeOptional && numCollisions > 0) { return proceed(flaverr({ name: 'PropagationError', code: 'E_COLLISIONS_WHEN_NULLING_OUT_REQUIRED_FK', message: 'Cannot '+(query.associatedIds.length===0?'wipe':'replace')+' the contents of '+ 'association (`'+query.collectionAttrName+'`) because there '+ (numCollisions===1?('is one conflicting '+WLChild.identity+' record'):('are '+numCollisions+' conflicting '+WLChild.identity+' records'))+' '+ 'whose `'+schemaDef.via+'` cannot be set to `null`. (That attribute is required.)' // For example, if you have a car with four tires, and you set out // to replace the four old tires with only three new ones, then you'll need to // destroy the spare tire before attempting to call `Car.replaceCollection()`) // ^^ Actually maybe just do that last bit in FS2Q (see other note there) }, omen)); }//• // So to recap: IWMIH we know that one of two things is true. // // Either: // (A) there are >=1 associated record ids, but EXACTLY ONE target record id (**null out fks for existing associated records except for the new ones, then set all the new ones to the same value**), or // (B) there is >=1 target record id, but ZERO associated record ids (**just null out fks for all existing associated records**) // // ╦═╗╦ ╦╔╗╔ ┌─┐┌─┐┬─┐┌┬┐┬┌─┐┬ ┌┐┌┬ ┬┬ ┬ ┌─┐┬ ┬┌┬┐ ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬ ┌┬┐┬ ┬┌─┐┌┐┌ // ╠╦╝║ ║║║║ ├─┘├─┤├┬┘ │ │├─┤│ ││││ ││ │───│ ││ │ │ │─┼┐│ │├┤ ├┬┘└┬┘ │ ├─┤├┤ │││ // ╩╚═╚═╝╝╚╝ ┴ ┴ ┴┴└─ ┴ ┴┴ ┴┴─┘ ┘└┘└─┘┴─┘┴─┘ └─┘└─┘ ┴ └─┘└└─┘└─┘┴└─ ┴┘ ┴ ┴ ┴└─┘┘└┘ // ┌─┐┌┐┌┌─┐┌┬┐┬ ┬┌─┐┬─┐ ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬ ┌┬┐┌─┐ ┌─┐┌─┐┌┬┐ ┌─┐┌─┐┬─┐┌─┐┬┌─┐┌┐┌ ┬┌─┌─┐┬ ┬┌─┐ // ├─┤││││ │ │ ├─┤├┤ ├┬┘ │─┼┐│ │├┤ ├┬┘└┬┘ │ │ │ └─┐├┤ │ ├┤ │ │├┬┘├┤ ││ ┬│││ ├┴┐├┤ └┬┘└─┐ // ┴ ┴┘└┘└─┘ ┴ ┴ ┴└─┘┴└─ └─┘└└─┘└─┘┴└─ ┴ ┴ └─┘ └─┘└─┘ ┴ └ └─┘┴└─└─┘┴└─┘┘└┘ ┴ ┴└─┘ ┴ └─┘ // We'll start with scenario A, where we first null out the fk on any existing records // other than the new ones, then update all the foreign key values for new associated // records to point to one particular parent record (aka target record). if (query.associatedIds.length > 0) { // console.log('** partial null-out ** # collisions:', numCollisions); var partialNullOutCriteria = { where: {} }; partialNullOutCriteria.where[WLChild.primaryKey] = { nin: query.associatedIds }; partialNullOutCriteria.where[schemaDef.via] = query.targetRecordIds[0]; // ^^ we know there has to be exactly one target record id at this point // (see assertions above) so this is safe. var partialNullOutVts = {}; partialNullOutVts[schemaDef.via] = null; // If the FK attribute is required, then we've already looked up the # of collisions, // so we can use that as an optimization to decide whether we can skip past this query // altogether. (If we already know there are no collisions, there's nothing to "null out"!) if (!isFkAttributeOptional && numCollisions === 0) { // > To accomplish this, we just use an empty "values to set" query key to make // > this first query into a no-op. This saves us doing yet another self-calling // > function. (One day, when the world has entirely switched to Node >= 7.9, // > we could just use `await` for all this exciting stuff.) partialNullOutVts = {}; }//fi WLChild.update(partialNullOutCriteria, partialNullOutVts, function(err) { if (err) { return proceed(err); } var newFkUpdateCriteria = { where: {} }; newFkUpdateCriteria.where[WLChild.primaryKey] = { in: query.associatedIds }; var newFkUpdateVts = {}; newFkUpdateVts[schemaDef.via] = query.targetRecordIds[0]; // ^^ we know there has to be exactly one target record id at this point // (see assertions above) so this is safe. WLChild.update(newFkUpdateCriteria, newFkUpdateVts, function(err) { if (err) { return proceed(err); } return proceed(); }, modifiedMeta);//_∏_ </WLChild.update() -- the "new fk update" query> }, modifiedMeta);//_∏_ </WLChild.update() -- the "partial null-out" query> }//‡ // ╦═╗╦ ╦╔╗╔ ┌┐ ┬ ┌─┐┌┐┌┬┌─┌─┐┌┬┐ ┌┐┌┬ ┬┬ ┬ ┌─┐┬ ┬┌┬┐ ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬ // ╠╦╝║ ║║║║ ├┴┐│ ├─┤│││├┴┐├┤ │ ││││ ││ │ │ ││ │ │ │─┼┐│ │├┤ ├┬┘└┬┘ // ╩╚═╚═╝╝╚╝ └─┘┴─┘┴ ┴┘└┘┴ ┴└─┘ ┴ ┘└┘└─┘┴─┘┴─┘ └─┘└─┘ ┴ └─┘└└─┘└─┘┴└─ ┴ // Alternatively, we'll go with scenario B, where we potentially null all the fks out. else { // console.log('** BLANKET NULL-OUT ** # collisions:', numCollisions); // If the FK attribute is required, then we've already looked up the # of collisions, // so we can use that as an optimization to decide whether we can skip past this query // altogether. (If we already know there are no collisions, there's nothing to "null out"!) if (!isFkAttributeOptional && numCollisions === 0) { return proceed(); }//• // Otherwise, proceed with the "null out" var nullOutCriteria = { where: {} }; nullOutCriteria.where[schemaDef.via] = { in: query.targetRecordIds }; var blanketNullOutVts = {}; blanketNullOutVts[schemaDef.via] = null; WLChild.update(nullOutCriteria, blanketNullOutVts, function(err) { if (err) { return proceed(err); } return proceed(); }, modifiedMeta);//_∏_ </WLChild.update() -- the "blanket null-out" query> }//fi // (Reminder: don't put any code down here!) });//_∏_ </†> })(function (err) { if (err) { return done(err); } // IWMIH, everything worked! // > Note that we do not send back a result of any kind-- this it to reduce the likelihood // > writing userland code that relies undocumented/experimental output. return done(); });//</ self-calling function (actually talk to the dbs) > }, explicitCbMaybe, _.extend(DEFERRED_METHODS, { // Provide access to this model for use in query modifier methods. _WLModel: WLModel, // Set up initial query metadata. _wlQueryInfo: query, }) );//</parley> };