diff --git a/lib/waterline/methods/archive.js b/lib/waterline/methods/archive.js new file mode 100644 index 000000000..27e530016 --- /dev/null +++ b/lib/waterline/methods/archive.js @@ -0,0 +1,493 @@ +/** + * Module Dependencies + */ + +var util = require('util'); +var async = require('async'); +var _ = require('@sailshq/lodash'); +var flaverr = require('flaverr'); +var parley = require('parley'); +var buildOmen = require('../utils/query/build-omen'); +var forgeAdapterError = require('../utils/query/forge-adapter-error'); +var forgeStageTwoQuery = require('../utils/query/forge-stage-two-query'); +var forgeStageThreeQuery = require('../utils/query/forge-stage-three-query'); +var getQueryModifierMethods = require('../utils/query/get-query-modifier-methods'); +var processAllRecords = require('../utils/query/process-all-records'); +var verifyModelMethodContext = require('../utils/query/verify-model-method-context'); + + +/** + * Module constants + */ + +var DEFERRED_METHODS = getQueryModifierMethods('archive'); + + + +/** + * archive() + * + * Archive (s.k.a. "soft-delete") records that match the specified criteria, + * saving them as new records in the built-in Archive model, then destroying + * the originals. + * + * ``` + * // Archive all bank accounts with more than $32,000 in them. + * BankAccount.archive().where({ + * balance: { '>': 32000 } + * }).exec(function(err) { + * // ... + * }); + * ``` + * + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * + * Usage without deferred object: + * ================================================ + * + * @param {Dictionary?} criteria + * + * @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 {Dictionary?} criteria + * + * @qkey {Dictionary?} meta + * @qkey {String} using + * @qkey {String} method + * + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + +module.exports = function archive(/* criteria, explicitCbMaybe, metaContainer */) { + + // 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(archive); + + // Build initial query. + var query = { + method: 'archive', + using: modelIdentity, + criteria: undefined, + meta: undefined + }; + + // ██╗ ██╗ █████╗ ██████╗ ██╗ █████╗ ██████╗ ██╗ ██████╗███████╗ + // ██║ ██║██╔══██╗██╔══██╗██║██╔══██╗██╔══██╗██║██╔════╝██╔════╝ + // ██║ ██║███████║██████╔╝██║███████║██║ ██║██║██║ ███████╗ + // ╚██╗ ██╔╝██╔══██║██╔══██╗██║██╔══██║██║ ██║██║██║ ╚════██║ + // ╚████╔╝ ██║ ██║██║ ██║██║██║ ██║██████╔╝██║╚██████╗███████║ + // ╚═══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═════╝╚══════╝ + // + // FUTURE: when time allows, update this to match the "VARIADICS" format + // used in the other model methods. + + // The explicit callback, if one was provided. + var explicitCbMaybe; + + // Handle double meaning of first argument: + // + // • archive(criteria, ...) + if (!_.isFunction(arguments[0])) { + query.criteria = arguments[0]; + explicitCbMaybe = arguments[1]; + query.meta = arguments[2]; + } + // • archive(explicitCbMaybe, ...) + else { + explicitCbMaybe = arguments[0]; + query.meta = arguments[1]; + } + + + + // ██████╗ ███████╗███████╗███████╗██████╗ + // ██╔══██╗██╔════╝██╔════╝██╔════╝██╔══██╗ + // ██║ ██║█████╗ █████╗ █████╗ ██████╔╝ + // ██║ ██║██╔══╝ ██╔══╝ ██╔══╝ ██╔══██╗ + // ██████╔╝███████╗██║ ███████╗██║ ██║ + // ╚═════╝ ╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝ + // + // ██╗███╗ ███╗ █████╗ ██╗ ██╗██████╗ ███████╗██╗ + // ██╔╝████╗ ████║██╔══██╗╚██╗ ██╔╝██╔══██╗██╔════╝╚██╗ + // ██║ ██╔████╔██║███████║ ╚████╔╝ ██████╔╝█████╗ ██║ + // ██║ ██║╚██╔╝██║██╔══██║ ╚██╔╝ ██╔══██╗██╔══╝ ██║ + // ╚██╗██║ ╚═╝ ██║██║ ██║ ██║ ██████╔╝███████╗██╔╝ + // ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ + // + // ┌┐ ┬ ┬┬┬ ┌┬┐ ┬ ┬─┐┌─┐┌┬┐┬ ┬┬─┐┌┐┌ ┌┐┌┌─┐┬ ┬ ┌┬┐┌─┐┌─┐┌─┐┬─┐┬─┐┌─┐┌┬┐ + // ├┴┐│ │││ ││ ┌┼─ ├┬┘├┤ │ │ │├┬┘│││ │││├┤ │││ ││├┤ ├┤ ├┤ ├┬┘├┬┘├┤ ││ + // └─┘└─┘┴┴─┘─┴┘ └┘ ┴└─└─┘ ┴ └─┘┴└─┘└┘ ┘└┘└─┘└┴┘ ─┴┘└─┘└ └─┘┴└─┴└─└─┘─┴┘ + // ┌─ ┬┌─┐ ┬─┐┌─┐┬ ┌─┐┬ ┬┌─┐┌┐┌┌┬┐ ─┐ + // │─── │├┤ ├┬┘├┤ │ ├┤ └┐┌┘├─┤│││ │ ───│ + // └─ ┴└ ┴└─└─┘┴─┘└─┘ └┘ ┴ ┴┘└┘ ┴ ─┘ + // If a callback function was not specified, then build a new Deferred and bail now. + // + // > This method will be called AGAIN automatically when the Deferred is executed. + // > and next time, it'll have a callback. + return parley( + + function (done){ + + // Otherwise, IWMIH, we know that a callback was specified. + // So... + + // ███████╗██╗ ██╗███████╗ ██████╗██╗ ██╗████████╗███████╗ + // ██╔════╝╚██╗██╔╝██╔════╝██╔════╝██║ ██║╚══██╔══╝██╔════╝ + // █████╗ ╚███╔╝ █████╗ ██║ ██║ ██║ ██║ █████╗ + // ██╔══╝ ██╔██╗ ██╔══╝ ██║ ██║ ██║ ██║ ██╔══╝ + // ███████╗██╔╝ ██╗███████╗╚██████╗╚██████╔╝ ██║ ███████╗ + // ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝ + // + // ╔═╗╔═╗╦═╗╔═╗╔═╗ ┌─┐┌┬┐┌─┐┌─┐┌─┐ ┌┬┐┬ ┬┌─┐ ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬ + // ╠╣ ║ ║╠╦╝║ ╦║╣ └─┐ │ ├─┤│ ┬├┤ │ ││││ │ │─┼┐│ │├┤ ├┬┘└┬┘ + // ╚ ╚═╝╩╚═╚═╝╚═╝ └─┘ ┴ ┴ ┴└─┘└─┘ ┴ └┴┘└─┘ └─┘└└─┘└─┘┴└─ ┴ + // + // Forge a stage 2 query (aka logical protostatement) + // This ensures a normalized format. + try { + forgeStageTwoQuery(query, orm); + } catch (e) { + switch (e.code) { + case 'E_INVALID_CRITERIA': + return done( + flaverr( + { name: 'UsageError' }, + new Error( + 'Invalid criteria.\n'+ + 'Details:\n'+ + ' '+e.details+'\n' + ) + ) + ); + + case 'E_NOOP': + // Determine the appropriate no-op result. + // If `fetch` meta key is set, use `[]`-- otherwise use `undefined`. + var noopResult = undefined; + if (query.meta && query.meta.fetch) { + noopResult = []; + }//>- + return done(undefined, noopResult); + + default: + return done(e); + } + } + + + // ╦ ╦╔═╗╔╗╔╔╦╗╦ ╔═╗ ┬ ┬┌─┐┌─┐┌─┐┬ ┬┌─┐┬ ┌─┐ ┌─┐┌─┐┬ ┬ ┌┐ ┌─┐┌─┐┬┌─ + // ╠═╣╠═╣║║║ ║║║ ║╣ BEFORE │ │├┤ ├┤ │ └┬┘│ │ ├┤ │ ├─┤│ │ ├┴┐├─┤│ ├┴┐ + // ╩ ╩╩ ╩╝╚╝═╩╝╩═╝╚═╝ ┴─┘┴└ └─┘└─┘ ┴ └─┘┴─┘└─┘ └─┘┴ ┴┴─┘┴─┘└─┘┴ ┴└─┘┴ ┴ + (function (proceed) { + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // FUTURE: `beforeArchive` lifecycle callback? + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + return proceed(); + })(function (err, query) { + if (err) { return done(err); } + + // ┬ ┌─┐┌─┐┬┌─┬ ┬┌─┐ ┌─┐┌┬┐┌─┐┌─┐┌┬┐┌─┐┬─┐ + // │ │ ││ │├┴┐│ │├─┘ ├─┤ ││├─┤├─┘ │ ├┤ ├┬┘ + // ┴─┘└─┘└─┘┴ ┴└─┘┴ ┴ ┴─┴┘┴ ┴┴ ┴ └─┘┴└─ + // Look up the appropriate adapter to use for this model. + + // Get a reference to the adapter. + var adapter = WLModel._adapter; + if (!adapter) { + // ^^One last sanity check to make sure the adapter exists-- again, for compatibility's sake. + return done(new Error('Consistency violation: Cannot find adapter for model (`' + modelIdentity + '`). This model appears to be using datastore `'+WLModel.datastore+'`, but the adapter for that datastore cannot be located.')); + } + + // Verify the adapter has `destroy` & `find` methods. + if (!adapter.destroy || !adapter.find) { + return done(new Error('The adapter used by this model (`' + modelIdentity + '`) doesn\'t support the `destroy`+`find` methods.')); + } + + // ╔═╗╔═╗╦═╗╔═╗╔═╗ ┌─┐┌┬┐┌─┐┌─┐┌─┐ ┌┬┐┬ ┬┬─┐┌─┐┌─┐ ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬ + // ╠╣ ║ ║╠╦╝║ ╦║╣ └─┐ │ ├─┤│ ┬├┤ │ ├─┤├┬┘├┤ ├┤ │─┼┐│ │├┤ ├┬┘└┬┘ + // ╚ ╚═╝╩╚═╚═╝╚═╝ └─┘ ┴ ┴ ┴└─┘└─┘ ┴ ┴ ┴┴└─└─┘└─┘ └─┘└└─┘└─┘┴└─ ┴ + // Now, destructively forge this S2Q into a S3Q. + try { + query = forgeStageThreeQuery({ + stageTwoQuery: query, + identity: modelIdentity, + transformer: WLModel._transformer, + originalModels: orm.collections + }); + } catch (e) { return done(e); } + + + // ┬┌─┐ ╔═╗╔═╗╔═╗╔═╗╔═╗╔╦╗╔═╗ ┌─┐┌┐┌┌─┐┌┐ ┬ ┌─┐┌┬┐ ┌┬┐┬ ┬┌─┐┌┐┌ + // │├┤ ║ ╠═╣╚═╗║ ╠═╣ ║║║╣ ├┤ │││├─┤├┴┐│ ├┤ ││ │ ├─┤├┤ │││ + // ┴└ ╚═╝╩ ╩╚═╝╚═╝╩ ╩═╩╝╚═╝ └─┘┘└┘┴ ┴└─┘┴─┘└─┘─┴┘┘ ┴ ┴ ┴└─┘┘└┘ + // ┌─┐┬┌┐┌┌┬┐ ╦╔╦╗╔═╗ ┌┬┐┌─┐ ┌┬┐┌─┐┌─┐┌┬┐┬─┐┌─┐┬ ┬ + // ├┤ ││││ ││ ║ ║║╚═╗ │ │ │ ││├┤ └─┐ │ ├┬┘│ │└┬┘ + // └ ┴┘└┘─┴┘ ╩═╩╝╚═╝ ┴ └─┘ ─┴┘└─┘└─┘ ┴ ┴└─└─┘ ┴ + (function _maybeFindIdsToDestroy(proceed) { + + // If `cascade` meta key is NOT enabled, then just proceed. + if (!query.meta || !query.meta.cascade) { + return proceed(); + } + + // Look up the ids of records that will be destroyed. + // (We need these because, later, since `cascade` is enabled, we'll need + // to empty out all of their associated collections.) + // + // > FUTURE: instead of doing this, consider forcing `fetch: true` in the + // > implementation of `.destroy()` when `cascade` meta key is enabled (mainly + // > for consistency w/ the approach used in createEach()/create()) + + // To do this, we'll grab the appropriate adapter method and call it with a stage 3 + // "find" query, using almost exactly the same QKs as in the incoming "destroy". + // The only tangible difference is that its criteria has a `select` clause so that + // records only contain the primary key field (by column name, of course.) + var pkColumnName = WLModel.schema[WLModel.primaryKey].columnName; + if (!pkColumnName) { + return done(new Error('Consistency violation: model `' + WLModel.identity + '` schema has no primary key column name!')); + } + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // > Note: We have to look up the column name this way (instead of simply using the + // > getAttribute() utility) because it is currently only fully normalized on the + // > `schema` dictionary-- the model's attributes don't necessarily have valid, + // > normalized column names. For more context, see: + // > https://github.com/balderdashy/waterline/commit/19889b7ee265e9850657ec2b4c7f3012f213a0ae#commitcomment-20668097 + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + adapter.find(WLModel.datastore, { + method: 'find', + using: query.using, + criteria: { + where: query.criteria.where, + skip: query.criteria.skip, + limit: query.criteria.limit, + sort: query.criteria.sort, + select: [ pkColumnName ] + }, + meta: query.meta //<< this is how we know that the same db connection will be used + }, function _afterPotentiallyFindingIdsToDestroy(err, pRecords) { + if (err) { + err = forgeAdapterError(err, omen, 'find', modelIdentity, orm); + return proceed(err); + } + + // Slurp out just the array of ids (pk values), and send that back. + var ids = _.pluck(pRecords, pkColumnName); + return proceed(undefined, ids); + + });// + + })(function _afterPotentiallyLookingUpRecordsToCascade(err, idsOfRecordsBeingDestroyedMaybe) { + if (err) { return done(err); } + + + // Now we'll actually perform the `destroy`. + + // ┌─┐┌─┐┌┐┌┌┬┐ ┌┬┐┌─┐ ╔═╗╔╦╗╔═╗╔═╗╔╦╗╔═╗╦═╗ + // └─┐├┤ │││ ││ │ │ │ ╠═╣ ║║╠═╣╠═╝ ║ ║╣ ╠╦╝ + // └─┘└─┘┘└┘─┴┘ ┴ └─┘ ╩ ╩═╩╝╩ ╩╩ ╩ ╚═╝╩╚═ + // Call the `destroy` adapter method. + adapter.destroy(WLModel.datastore, query, function _afterTalkingToAdapter(err, rawAdapterResult) { + if (err) { + err = forgeAdapterError(err, omen, 'destroy', modelIdentity, orm); + return done(err); + }//-• + + + // ╦═╗╔═╗╦╔╗╔ ╔╦╗╔═╗╦ ╦╔╗╔ ╔╦╗╔═╗╔═╗╔╦╗╦═╗╦ ╦╔═╗╔╦╗╦╔═╗╔╗╔ ┌─┐┌┐┌┌┬┐┌─┐ + // ╠╦╝╠═╣║║║║ ║║║ ║║║║║║║ ║║║╣ ╚═╗ ║ ╠╦╝║ ║║ ║ ║║ ║║║║ │ ││││ │ │ │ + // ╩╚═╩ ╩╩╝╚╝ ═╩╝╚═╝╚╩╝╝╚╝ ═╩╝╚═╝╚═╝ ╩ ╩╚═╚═╝╚═╝ ╩ ╩╚═╝╝╚╝ └─┘┘└┘ ┴ └─┘ + // ┌─┐┌─┐┌─┐┌─┐┌─┐┬┌─┐┌┬┐┬┌─┐┌┐┌┌─┐ ┌─ ┬ ┌─┐ ┌─┐┌─┐┌─┐┌─┐┌─┐┌┬┐┌─┐ ─┐ + // ├─┤└─┐└─┐│ ││ │├─┤ │ ││ ││││└─┐ │ │ ├┤ │ ├─┤└─┐│ ├─┤ ││├┤ │ + // ┴ ┴└─┘└─┘└─┘└─┘┴┴ ┴ ┴ ┴└─┘┘└┘└─┘ └─ ┴o└─┘o └─┘┴ ┴└─┘└─┘┴ ┴─┴┘└─┘ ─┘ + (function _maybeWipeAssociatedCollections(proceed) { + + // If `cascade` meta key is NOT enabled, then just proceed. + if (!query.meta || !query.meta.cascade) { + return proceed(); + } + + // Otherwise, then we should have the records we looked up before. + // (Here we do a quick sanity check.) + if (!_.isArray(idsOfRecordsBeingDestroyedMaybe)) { + return proceed(new Error('Consistency violation: Should have an array of records looked up before! But instead, got: '+util.inspect(idsOfRecordsBeingDestroyedMaybe, {depth: 5})+'')); + } + + // --• + // Now we'll clear out collections belonging to the specified records. + // (i.e. use `replaceCollection` to wipe them all out to be `[]`) + + + // First, if there are no target records, then gracefully bail without complaint. + // (i.e. this is a no-op) + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // FUTURE: Revisit this and verify that it's unnecessary. While this isn't a bad micro-optimization, + // its existence makes it seem like this wouldn't work or would cause a warning or something. And it + // really shouldn't be necessary. (It's doubtful that it adds any real tangible performance benefit anyway.) + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + if (idsOfRecordsBeingDestroyedMaybe.length === 0) { + return proceed(); + }//-• + + // Otherwise, we have work to do. + // + // Run .replaceCollection() for each associated collection of the targets, wiping them all out. + // (if n..m, this destroys junction records; otherwise, it's n..1, so this just nulls out the other side) + // + // > Note that we pass through `meta` here, ensuring that the same db connection is used, if possible. + async.each(_.keys(WLModel.attributes), function _eachAttribute(attrName, next) { + + var attrDef = WLModel.attributes[attrName]; + + // Skip everything other than collection attributes. + if (!attrDef.collection){ return next(); } + + // But otherwise, this is a collection attribute. So wipe it. + WLModel.replaceCollection(idsOfRecordsBeingDestroyedMaybe, attrName, [], function (err) { + if (err) { return next(err); } + + return next(); + + }, query.meta);// + + },// ~∞%° + function _afterwards(err) { + if (err) { return proceed(err); } + + return proceed(); + + });// + + })(function _afterPotentiallyWipingCollections(err) {// ~∞%° + if (err) { + return done(err); + } + + // ╔╦╗╦═╗╔═╗╔╗╔╔═╗╔═╗╔═╗╦═╗╔╦╗ ┬─┐┌─┐┌─┐┌─┐┬─┐┌┬┐┌─┐ ┌┐ ┬ ┬┌┬┐ ┌─┐┌┐┌┬ ┬ ┬ ┬┌─┐ + // ║ ╠╦╝╠═╣║║║╚═╗╠╣ ║ ║╠╦╝║║║ ├┬┘├┤ │ │ │├┬┘ ││└─┐ ├┴┐│ │ │ │ │││││ └┬┘ │├┤ + // ╩ ╩╚═╩ ╩╝╚╝╚═╝╚ ╚═╝╩╚═╩ ╩ ┴└─└─┘└─┘└─┘┴└──┴┘└─┘ooo└─┘└─┘ ┴ └─┘┘└┘┴─┘┴ ┴└ + // ╔═╗╔═╗╔╦╗╔═╗╦ ╦ ┌┬┐┌─┐┌┬┐┌─┐ ┬┌─┌─┐┬ ┬ ┬ ┬┌─┐┌─┐ ┌─┐┌─┐┌┬┐ ┌┬┐┌─┐ ┌┬┐┬─┐┬ ┬┌─┐ + // ╠╣ ║╣ ║ ║ ╠═╣ │││├┤ │ ├─┤ ├┴┐├┤ └┬┘ │││├─┤└─┐ └─┐├┤ │ │ │ │ │ ├┬┘│ │├┤ + // ╚ ╚═╝ ╩ ╚═╝╩ ╩ ┴ ┴└─┘ ┴ ┴ ┴ ┴ ┴└─┘ ┴ └┴┘┴ ┴└─┘ └─┘└─┘ ┴ ┴ └─┘ ┴ ┴└─└─┘└─┘ + (function _maybeTransformRecords(proceed){ + + // If `fetch` was not enabled, return. + if (!_.has(query.meta, 'fetch') || query.meta.fetch === false) { + + // > Note: This `if` statement is a convenience, for cases where the result from + // > the adapter may have been coerced from `undefined` to `null` automatically. + // > (we want it to be `undefined` still, for consistency) + if (_.isNull(rawAdapterResult)) { + return proceed(); + }//-• + + if (!_.isUndefined(rawAdapterResult)) { + console.warn('\n'+ + 'Warning: Unexpected behavior in database adapter:\n'+ + 'Since `fetch` is NOT enabled, this adapter (for datastore `'+WLModel.datastore+'`)\n'+ + 'should NOT have sent back anything as the 2nd argument when triggering the callback\n'+ + 'from its `destroy` method. But it did!\n'+ + '\n'+ + '(Displaying this warning to help avoid confusion and draw attention to the bug.\n'+ + 'Specifically, got:\n'+ + util.inspect(rawAdapterResult, {depth:5})+'\n'+ + '(Ignoring it and proceeding anyway...)'+'\n' + ); + }//>- + + // Continue on. + return proceed(); + + }//-• + + // IWMIH then we know that `fetch: true` meta key was set, and so the + // adapter should have sent back an array. + + // Verify that the raw result from the adapter is an array. + if (!_.isArray(rawAdapterResult)) { + return proceed(new Error( + 'Unexpected behavior in database adapter: Since `fetch: true` was enabled, this adapter '+ + '(for datastore `'+WLModel.datastore+'`) should have sent back an array of records as the 2nd argument when triggering '+ + 'the callback from its `archive` method. But instead, got: '+util.inspect(rawAdapterResult, {depth:5})+'' + )); + }//-• + + // Attempt to convert the column names in each record back into attribute names. + var transformedRecords; + try { + transformedRecords = rawAdapterResult.map(function(record) { + return WLModel._transformer.unserialize(record); + }); + } catch (e) { return proceed(e); } + + // Check the records to verify compliance with the adapter spec, + // as well as any issues related to stale data that might not have been + // been migrated to keep up with the logical schema (`type`, etc. in + // attribute definitions). + try { + processAllRecords(transformedRecords, query.meta, modelIdentity, orm); + } catch (e) { return proceed(e); } + + // Now continue on. + return proceed(undefined, transformedRecords); + + })(function (err, transformedRecordsMaybe){ + if (err) { return done(err); } + + // ╔═╗╔═╗╔╦╗╔═╗╦═╗ ┬ ┬┌─┐┌─┐┌─┐┬ ┬┌─┐┬ ┌─┐ ┌─┐┌─┐┬ ┬ ┌┐ ┌─┐┌─┐┬┌─ + // ╠═╣╠╣ ║ ║╣ ╠╦╝ │ │├┤ ├┤ │ └┬┘│ │ ├┤ │ ├─┤│ │ ├┴┐├─┤│ ├┴┐ + // ╩ ╩╚ ╩ ╚═╝╩╚═ ┴─┘┴└ └─┘└─┘ ┴ └─┘┴─┘└─┘ └─┘┴ ┴┴─┘┴─┘└─┘┴ ┴└─┘┴ ┴ + (function (proceed){ + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // FUTURE: `afterArchive` lifecycle callback? + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + return proceed(); + })(function(err){ + if (err){ return done(err); } + return done(undefined, transformedRecordsMaybe); + });//_∏_ (†: "after" LC) + });//_∏_ (†: after determining (and potentially transforming) the result from the adapter) + });//_∏_ (†: _afterPotentiallyWipingCollections) + });//_∏_ (adapter.destroy) + }); //_∏_ (†: after potentially looking up records to cascade) + }); //_∏_ (†: "before" LC) + }, + + + explicitCbMaybe, + + + _.extend(DEFERRED_METHODS, { + + // Provide access to this model for use in query modifier methods. + _WLModel: WLModel, + + // Set up initial query metadata. + _wlQueryInfo: query, + + }) + + + );// + +};