balderdashy/waterline

View on GitHub
lib/waterline/methods/destroy.js

Summary

Maintainability
F
6 days
Test Coverage
/**
 * 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('destroy');



/**
 * destroy()
 *
 * Destroy records that match the specified criteria.
 *
 * ```
 * // Destroy all bank accounts with more than $32,000 in them.
 * BankAccount.destroy().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 destroy(/* 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(destroy);

  // Build initial query.
  var query = {
    method: 'destroy',
    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:
  //
  // • destroy(criteria, ...)
  if (!_.isFunction(arguments[0])) {
    query.criteria = arguments[0];
    explicitCbMaybe = arguments[1];
    query.meta = arguments[2];
  }
  // • destroy(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',
                code: e.code,
                details: e.details,
                message:
                'Invalid criteria.\n'+
                'Details:\n'+
                '  '+e.details+'\n'
              }, omen)
            );

          case 'E_NOOP':
            // Determine the appropriate no-op result.
            // If `fetch` meta key is set, use `[]`-- otherwise use `undefined`.
            //
            // > Note that future versions might simulate output from the raw driver.
            // > (e.g. `{ numRecordsDestroyed: 0 }`)
            // >  See: https://github.com/treelinehq/waterline-query-docs/blob/master/docs/results.md#destroy
            var noopResult = undefined;
            if (query.meta && query.meta.fetch) {
              noopResult = [];
            }//>-
            return done(undefined, noopResult);

          default:
            return done(e);
        }
      }

      //  ╦ ╦╔═╗╔╗╔╔╦╗╦  ╔═╗        ┬  ┬┌─┐┌─┐┌─┐┬ ┬┌─┐┬  ┌─┐  ┌─┐┌─┐┬  ┬  ┌┐ ┌─┐┌─┐┬┌─
      //  ╠═╣╠═╣║║║ ║║║  ║╣  BEFORE │  │├┤ ├┤ │  └┬┘│  │  ├┤   │  ├─┤│  │  ├┴┐├─┤│  ├┴┐
      //  ╩ ╩╩ ╩╝╚╝═╩╝╩═╝╚═╝        ┴─┘┴└  └─┘└─┘ ┴ └─┘┴─┘└─┘  └─┘┴ ┴┴─┘┴─┘└─┘┴ ┴└─┘┴ ┴
      // Determine what to do about running any lifecycle callback.
      (function _runBeforeLC(proceed) {
        // If the `skipAllLifecycleCallbacks` meta flag was set, don't run the lifecycle callback.
        if (query.meta && query.meta.skipAllLifecycleCallbacks) {
          return proceed(undefined, query);
        }

        // If there is no relevant LC, then just proceed.
        if (!_.has(WLModel._callbacks, 'beforeDestroy')) {
          return proceed(undefined, query);
        }

        // But otherwise, run it.
        WLModel._callbacks.beforeDestroy(query.criteria, function (err){
          if (err) { return proceed(err); }
          return proceed(undefined, query);
        });

      })(function _afterRunningBeforeLC(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 a `destroy` method.
        if (!adapter.destroy) {
          return done(new Error('The adapter used by this model (`' + modelIdentity + '`) doesn\'t support the `destroy` method.'));
        }

        // If `cascade` is enabled, do an extra assertion...
        if (query.meta && query.meta.cascade){

          // First, a sanity check to ensure the adapter has a `find` method too.
          if (!adapter.find) {
            return done(new Error('The adapter used by this model (`' + modelIdentity + '`) doesn\'t support the `find` method, but that method is mandatory to be able to use `cascade: true`.'));
          }

        }//>-



        // ================================================================================
        // FUTURE: potentially bring this back (but also would need the `omit clause`)
        // ================================================================================
        // // Before we get to forging again, save a copy of the stage 2 query's
        // // `select` clause.  We'll need this later on when processing the resulting
        // // records, and if we don't copy it now, it might be damaged by the forging.
        // //
        // // > Note that we don't need a deep clone.
        // // > (That's because the `select` clause is only 1 level deep.)
        // var s2QSelectClause = _.clone(query.criteria.select);
        // ================================================================================


        //  ╔═╗╔═╗╦═╗╔═╗╔═╗  ┌─┐┌┬┐┌─┐┌─┐┌─┐  ┌┬┐┬ ┬┬─┐┌─┐┌─┐  ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬
        //  ╠╣ ║ ║╠╦╝║ ╦║╣   └─┐ │ ├─┤│ ┬├┤    │ ├─┤├┬┘├┤ ├┤   │─┼┐│ │├┤ ├┬┘└┬┘
        //  ╚  ╚═╝╩╚═╚═╝╚═╝  └─┘ ┴ ┴ ┴└─┘└─┘   ┴ ┴ ┴┴└─└─┘└─┘  └─┘└└─┘└─┘┴└─ ┴
        // 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);

          });//</adapter.find()>

        })(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) {
                    if (err.name === 'PropagationError') {
                      return next(flaverr({
                        name: err.name,
                        code: err.code,
                        message: 'Failed to run the "cascade" polyfill.  Could not propagate the potential '+
                        'destruction of '+(idsOfRecordsBeingDestroyedMaybe.length===1?'this '+WLModel.identity+' record':('these '+idsOfRecordsBeingDestroyedMaybe.length+' '+WLModel.identity+' records'))+'.\n'+
                        'Details:\n'+
                        '  '+err.message+'\n'+
                        '\n'+
                        'This error originated from the fact that the "cascade" polyfill was enabled for this query.\n'+
                        'Tip: Try reordering your .destroy() calls.\n'+
                        ' [?] See https://sailsjs.com/support for more help.\n'
                      }, omen));
                    }//•
                    else { return next(err); }
                  }//•

                  return next();

                }, query.meta);//</.replaceCollection()>

              },// ~∞%°
              function _afterwards(err) {
                if (err) { return proceed(err); }

                return proceed();

              });//</ async.each >

            })(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 `destroy` 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);
                }

                //  ╔═╗╔═╗╔╦╗╔═╗╦═╗  ┌┬┐┌─┐┌─┐┌┬┐┬─┐┌─┐┬ ┬  ┌─┐┌─┐┬  ┬  ┌┐ ┌─┐┌─┐┬┌─
                //  ╠═╣╠╣  ║ ║╣ ╠╦╝   ││├┤ └─┐ │ ├┬┘│ │└┬┘  │  ├─┤│  │  ├┴┐├─┤│  ├┴┐
                //  ╩ ╩╚   ╩ ╚═╝╩╚═  ─┴┘└─┘└─┘ ┴ ┴└─└─┘ ┴   └─┘┴ ┴┴─┘┴─┘└─┘┴ ┴└─┘┴ ┴
                // Run "after" lifecycle callback AGAIN and AGAIN- once for each record.
                // ============================================================
                async.each(transformedRecordsMaybe, function _eachRecord(record, next) {

                  // If the `skipAllLifecycleCallbacks` meta flag was set, don't run any of
                  // the methods.
                  if (_.has(query.meta, 'skipAllLifecycleCallbacks') && query.meta.skipAllLifecycleCallbacks) {
                    return next();
                  }

                  // Skip "after" lifecycle callback, if not defined.
                  if (!_.has(WLModel._callbacks, 'afterDestroy')) {
                    return next();
                  }

                  // Otherwise run it.
                  WLModel._callbacks.afterDestroy(record, function _afterMaybeRunningAfterDestroyForThisRecord(err) {
                    if (err) {
                      return next(err);
                    }

                    return next();
                  });

                },// ~∞%°
                function _afterIteratingOverRecords(err) {
                  if (err) {
                    return done(err);
                  }

                  return done(undefined, transformedRecordsMaybe);
                });//_∏_   (†: async.each() -- ran "after" lifecycle callback on each record)
              });//_∏_   (†: 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,

    })


  );//</parley>

};