balderdashy/waterline

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

Summary

Maintainability
C
1 day
Test Coverage
/**
 * Module Dependencies
 */

var _ = require('@sailshq/lodash');
var flaverr = require('flaverr');
var parley = require('parley');
var buildOmen = require('../utils/query/build-omen');
var getModel = require('../utils/ontology/get-model');
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('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 (err) {
        switch (err.code) {
          case 'E_INVALID_CRITERIA':
            return done(
              flaverr({
                name: 'UsageError',
                code: err.code,
                details: err.details,
                message:
                'Invalid criteria.\n'+
                'Details:\n'+
                '  '+err.details+'\n'
              }, omen)
            );

          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(err);
        }
      }//fi

      // Bail now if archiving has been disabled.
      if (!WLModel.archiveModelIdentity) {
        return done(flaverr({
          name: 'UsageError',
          message: 'Since the `archiveModelIdentity` setting was explicitly disabled, .archive() cannot be used.'
        }, omen));
      }//•

      // Look up the Archive model.
      var Archive = WLModel.archiveModelIdentity;
      try {
        Archive = getModel(WLModel.archiveModelIdentity, orm);
      } catch (err) { return done(err); }//fi


      // - - - - - - - - - - - - - - - - - - - - - - - - - - -
      // FUTURE: pass through the `omen` in the metadata.
      // - - - - - - - - - - - - - - - - - - - - - - - - - - -

      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      // FUTURE: Maybe refactor this into more-generic `.move()` and/or
      // `.copy()` methods for migrating data between models/datastores.
      // Then just leverage those methods here in `.archive()`.
      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      //  ╔═╗═╗ ╦╔═╗╔═╗╦ ╦╔╦╗╔═╗  ┌─┐┬┌┐┌┌┬┐  ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬
      //  ║╣ ╔╩╦╝║╣ ║  ║ ║ ║ ║╣   ├┤ ││││ ││  │─┼┐│ │├┤ ├┬┘└┬┘
      //  ╚═╝╩ ╚═╚═╝╚═╝╚═╝ ╩ ╚═╝  └  ┴┘└┘─┴┘  └─┘└└─┘└─┘┴└─ ┴
      // Note that we pass in `meta` here, as well as in the other queries
      // below.  (This ensures we're on the same db connection, provided one
      // was explicitly passed in!)
      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      // WARNING:
      //
      // Before proceeding with calling an additional model method that relies
      // on criteria other than the primary .destroy(), we'll want to back up a
      // copy of our s2q's criteria (`query.criteria`).
      //
      // This is important because, in an effort to improve performance,
      // Waterline methods destructively mutate criteria when forging queries
      // for use in the adapter(s).  Since we'll be reusing criteria, we need
      // to insulate ourselves from those destructive changes in case there are
      // custom column names involved.  (e.g. Mongo's `_id``)
      //
      // > While the criteria might contain big crazy stuff for comparing with
      // > type:ref attributes, a deep clone is the best option we have.
      //
      // FUTURE: in s2q forge logic, for "archive" method, reject with an error
      // if deep refs (non-JSON-serializable data) are discovered in criteria.
      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      var s2qCriteriaForFind = _.cloneDeep(query.criteria);
      WLModel.find(s2qCriteriaForFind, function _afterFinding(err, foundRecords) {
        if (err) { return done(err); }

        // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        // FUTURE: as an optimization, fetch records batch-at-a-time
        // using .stream() instead of just doing a naïve `.find()`.
        // (This would allow you to potentially archive millions of records
        // at a time without overflowing RAM.)
        // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        var archives = [];
        _.each(foundRecords, function(record){
          archives.push({
            originalRecord: record,
            originalRecordId: record[WLModel.primaryKey],
            fromModel: WLModel.identity,
          });
        });//∞

        //  ╔═╗═╗ ╦╔═╗╔═╗╦ ╦╔╦╗╔═╗  ┌─┐┬─┐┌─┐┌─┐┌┬┐┌─┐┌─┐┌─┐┌─┐┬ ┬  ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬
        //  ║╣ ╔╩╦╝║╣ ║  ║ ║ ║ ║╣   │  ├┬┘├┤ ├─┤ │ ├┤ ├┤ ├─┤│  ├─┤  │─┼┐│ │├┤ ├┬┘└┬┘
        //  ╚═╝╩ ╚═╚═╝╚═╝╚═╝ ╩ ╚═╝  └─┘┴└─└─┘┴ ┴ ┴ └─┘└─┘┴ ┴└─┘┴ ┴  └─┘└└─┘└─┘┴└─ ┴
        Archive.createEach(archives, function _afterCreatingEach(err) {
          if (err) { return done(err); }

          // Remove the `limit`, `skip`, `sort`, `select`, and `omit` clauses so
          // that our `destroy` query is valid.
          // (This is because they were automatically attached above in the forging.)
          delete query.criteria.limit;
          delete query.criteria.skip;
          delete query.criteria.sort;
          delete query.criteria.select;
          delete query.criteria.omit;

          //  ╔═╗═╗ ╦╔═╗╔═╗╦ ╦╔╦╗╔═╗  ┌┬┐┌─┐┌─┐┌┬┐┬─┐┌─┐┬ ┬  ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬
          //  ║╣ ╔╩╦╝║╣ ║  ║ ║ ║ ║╣    ││├┤ └─┐ │ ├┬┘│ │└┬┘  │─┼┐│ │├┤ ├┬┘└┬┘
          //  ╚═╝╩ ╚═╚═╝╚═╝╚═╝ ╩ ╚═╝  ─┴┘└─┘└─┘ ┴ ┴└─└─┘ ┴   └─┘└└─┘└─┘┴└─ ┴
          WLModel.destroy(query.criteria, function _afterDestroying(err) {
            if (err) { return done(err); }

            if (query.meta&&query.meta.fetch){
              return done(undefined, foundRecords);
            }
            else {
              return done();
            }

          }, query.meta);//</.destroy()>
        }, query.meta);//</.createEach()>
      }, query.meta);//</.find()>

    },


    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>

};