balderdashy/waterline

View on GitHub
lib/waterline/utils/query/process-all-records.js

Summary

Maintainability
F
3 days
Test Coverage
/**
 * Module dependencies
 */

var assert = require('assert');
var util = require('util');
var _ = require('@sailshq/lodash');
// var EA = require('encrypted-attr'); « this is required below for node compat.
var flaverr = require('flaverr');
var rttc = require('rttc');
var eachRecordDeep = require('waterline-utils').eachRecordDeep;

/**
 * Module constants
 */

var WARNING_SUFFIXES = {

  MIGHT_BE_YOUR_FAULT:
  '\n'+
  '> You are seeing this warning because there are records in your database that don\'t\n'+
  '> match up with your models.  This is often the result of a model definition being\n'+
  '> changed without also migrating leftover data.  But it could also be because records\n'+
  '> were added or modified in your database from somewhere outside of Sails/Waterline\n'+
  '> (e.g. phpmyadmin, or another app).  In either case, to make this warning go away,\n'+
  '> you have a few options.  First of all, you could change your model definition so\n'+
  '> that it matches the existing records in your database.  Or you could update/destroy\n'+
  '> the old records in your database; either by hand, or using a migration script.\n'+
  '> \n'+
  (process.env.NODE_ENV !== 'production' ? '> (For example, to wipe all data, you might just use `migrate: drop`.)\n' : '')+
  '> \n'+
  '> More rarely, this warning could mean there is a bug in the adapter itself.  If you\n'+
  '> believe that is the case, then please contact the maintainer of this adapter by opening\n'+
  '> an issue, or visit http://sailsjs.com/support for help.\n',

  HARD_TO_SEE_HOW_THIS_COULD_BE_YOUR_FAULT:
  '\n'+
  '> This is usally caused by a bug in the adapter itself.  If you believe that\n'+
  '> might be the case here, then please contact the maintainer of this adapter by\n'+
  '> opening an issue, or visit http://sailsjs.com/support for help.\n'

};


/**
 * processAllRecords()
 *
 * Process potentially-populated records coming back from the adapter, AFTER they've already had
 * their keys transformed from column names back to attribute names and had populated data reintegrated.
 * To reiterate that: this function takes logical records, **NOT physical records**.
 *
 * `processAllRecords()` has 3 responsibilities:
 *
 * (1) Verify the integrity of the provided records, and any populated child records
 *     (Note: If present, child records only ever go 1 level deep in Waterline currently.)
 *      > At the moment, this serves primarily as a way to check for stale, unmigrated data that
 *      > might exist in the database, as well as any unexpected adapter compatibility problems.
 *      > For the full specification and expected behavior, see:
 *      > https://docs.google.com/spreadsheets/d/1whV739iW6O9SxRZLCIe2lpvuAUqm-ie7j7tn_Pjir3s/edit#gid=1927470769
 *
 * (2) Attach custom toJSON() functions to records, if the model says to do so.
 *
 * (3) Decrypt any data that was encrypted at rest.
 *
 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 *
 * @param  {Array} records
 *         An array of records.  (These are logical records -- NOT physical records!!)
 *         (WARNING: This array and its deeply-nested contents might be mutated in-place!!!)
 *
 * @param {Ref?} meta
 *        The `meta` query key for the query.
 *
 * @param {String} modelIdentity
 *        The identity of the model these records came from (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.
 *
 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 */
module.exports = function processAllRecords(records, meta, modelIdentity, orm) {
  // console.time('processAllRecords');


  if (!_.isArray(records)) {
    throw new Error('Consistency violation: Expected `records` to be an array.  But instead, got: '+util.inspect(records,{depth:5})+'');
  }

  if (!_.isUndefined(meta) && !_.isObject(meta)) {
    throw new Error('Consistency violation: Expected `meta` to be a dictionary, or undefined.  But instead, got: '+util.inspect(meta,{depth:5})+'');
  }

  if (!_.isString(modelIdentity) || modelIdentity === '') {
    throw new Error('Consistency violation: Expected `modelIdentity` to be a non-empty string.  But instead, got: '+util.inspect(modelIdentity,{depth:5})+'');
  }


  // Determine whether to skip record verification below.
  // (we always do it unless the `skipRecordVerification` meta key is explicitly truthy,)
  var skippingRecordVerification = meta && meta.skipRecordVerification;


  // Iterate over each parent record and any nested arrays/dictionaries that
  // appear to be populated child records.
  eachRecordDeep(records, function _eachParentOrChildRecord(record, WLModel){

    // First, check the results 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).
    if (!skippingRecordVerification) {


      //  ███╗   ██╗ ██████╗ ███╗   ██╗       █████╗ ████████╗████████╗██████╗ ██╗██████╗ ██╗   ██╗████████╗███████╗
      //  ████╗  ██║██╔═══██╗████╗  ██║      ██╔══██╗╚══██╔══╝╚══██╔══╝██╔══██╗██║██╔══██╗██║   ██║╚══██╔══╝██╔════╝
      //  ██╔██╗ ██║██║   ██║██╔██╗ ██║█████╗███████║   ██║      ██║   ██████╔╝██║██████╔╝██║   ██║   ██║   █████╗
      //  ██║╚██╗██║██║   ██║██║╚██╗██║╚════╝██╔══██║   ██║      ██║   ██╔══██╗██║██╔══██╗██║   ██║   ██║   ██╔══╝
      //  ██║ ╚████║╚██████╔╝██║ ╚████║      ██║  ██║   ██║      ██║   ██║  ██║██║██████╔╝╚██████╔╝   ██║   ███████╗
      //  ╚═╝  ╚═══╝ ╚═════╝ ╚═╝  ╚═══╝      ╚═╝  ╚═╝   ╚═╝      ╚═╝   ╚═╝  ╚═╝╚═╝╚═════╝  ╚═════╝    ╚═╝   ╚══════╝
      //
      //  ██╗  ██╗███████╗██╗   ██╗███████╗
      //  ██║ ██╔╝██╔════╝╚██╗ ██╔╝██╔════╝
      //  █████╔╝ █████╗   ╚████╔╝ ███████╗
      //  ██╔═██╗ ██╔══╝    ╚██╔╝  ╚════██║
      //  ██║  ██╗███████╗   ██║   ███████║
      //  ╚═╝  ╚═╝╚══════╝   ╚═╝   ╚══════╝
      //
      // If this model is defined as `schema: true`, then check the returned record
      // for any extraneous keys which do not correspond with declared attributes.
      // If any are found, then log a warning.
      if (WLModel.hasSchema) {

        var nonAttrKeys = _.difference(_.keys(record), _.keys(WLModel.attributes));
        if (nonAttrKeys > 0) {

          // Since this is `schema: true`, the adapter method should have
          // received an explicit `select` clause in the S3Q `criteria`
          // query key, and thus it should not have sent back any unrecognized
          // attributes (or in cases where there is no `criteria` query key, e.g.
          // a create(), the adapter should never send back extraneous properties
          // anyways, because Waterline core should have stripped any such extra
          // properties off on the way _in_ to the adapter).
          //
          // So if we made it here, we can safely assume that this is due
          // to an issue in the _adapter_ -- not some problem with unmigrated
          // data.
          console.warn('\n'+
            'Warning: A record in this result set has extraneous properties ('+nonAttrKeys+')\n'+
            'that, after adjusting for any custom columnNames, still do not correspond\n'+
            'any recognized attributes of this model (`'+WLModel.identity+'`).\n'+
            'Since this model is defined as `schema: true`, this behavior is unexpected.\n'+
            // ====================================================================================
            // Removed this for the sake of brevity-- could bring it back if deemed helpful.
            // ====================================================================================
            // 'This problem could be the result of an adapter method not properly observing\n'+
            // 'the `select` clause it receives in the incoming criteria (or otherwise sending\n'+
            // 'extra, unexpected properties on records that were left over from old data).\n'+
            // ====================================================================================
            WARNING_SUFFIXES.MIGHT_BE_YOUR_FAULT
          );

        }//</if>

      }//</if>



      //  ██╗  ██╗███████╗██╗   ██╗███████╗    ██╗    ██╗    ██╗    ██████╗ ██╗  ██╗███████╗
      //  ██║ ██╔╝██╔════╝╚██╗ ██╔╝██╔════╝    ██║    ██║   ██╔╝    ██╔══██╗██║  ██║██╔════╝
      //  █████╔╝ █████╗   ╚████╔╝ ███████╗    ██║ █╗ ██║  ██╔╝     ██████╔╝███████║███████╗
      //  ██╔═██╗ ██╔══╝    ╚██╔╝  ╚════██║    ██║███╗██║ ██╔╝      ██╔══██╗██╔══██║╚════██║
      //  ██║  ██╗███████╗   ██║   ███████║    ╚███╔███╔╝██╔╝       ██║  ██║██║  ██║███████║
      //  ╚═╝  ╚═╝╚══════╝   ╚═╝   ╚══════╝     ╚══╝╚══╝ ╚═╝        ╚═╝  ╚═╝╚═╝  ╚═╝╚══════╝
      //
      //   ██████╗ ███████╗    ██╗   ██╗███╗   ██╗██████╗ ███████╗███████╗██╗███╗   ██╗███████╗██████╗
      //  ██╔═══██╗██╔════╝    ██║   ██║████╗  ██║██╔══██╗██╔════╝██╔════╝██║████╗  ██║██╔════╝██╔══██╗
      //  ██║   ██║█████╗      ██║   ██║██╔██╗ ██║██║  ██║█████╗  █████╗  ██║██╔██╗ ██║█████╗  ██║  ██║
      //  ██║   ██║██╔══╝      ██║   ██║██║╚██╗██║██║  ██║██╔══╝  ██╔══╝  ██║██║╚██╗██║██╔══╝  ██║  ██║
      //  ╚██████╔╝██║         ╚██████╔╝██║ ╚████║██████╔╝███████╗██║     ██║██║ ╚████║███████╗██████╔╝
      //   ╚═════╝ ╚═╝          ╚═════╝ ╚═╝  ╚═══╝╚═════╝ ╚══════╝╚═╝     ╚═╝╚═╝  ╚═══╝╚══════╝╚═════╝
      //
      // Loop over the properties of the record.
      _.each(_.keys(record), function (key){

        // Ensure that the value was not explicitly sent back as `undefined`.
        // (but if it was, log a warning.  Note that we don't strip it out like
        // we would normally, because we're careful not to munge data in this utility.)
        if(_.isUndefined(record[key])){
          console.warn('\n'+
            'Warning: A database adapter should never send back records that have `undefined`\n'+
            'on the RHS of any property (e.g. `foo: undefined`).  But after transforming\n'+
            'columnNames back to attribute names for the model `' + modelIdentity + '`, one\n'+
            'of the records sent back from this adapter has a property (`'+key+'`) with\n'+
            '`undefined` on the right-hand side.\n' +
            WARNING_SUFFIXES.HARD_TO_SEE_HOW_THIS_COULD_BE_YOUR_FAULT
          );
        }//>-

      });



      // Now, loop over each attribute in the model.
      _.each(WLModel.attributes, function (attrDef, attrName){


        //  ██████╗ ██████╗ ██╗███╗   ███╗ █████╗ ██████╗ ██╗   ██╗    ██╗  ██╗███████╗██╗   ██╗
        //  ██╔══██╗██╔══██╗██║████╗ ████║██╔══██╗██╔══██╗╚██╗ ██╔╝    ██║ ██╔╝██╔════╝╚██╗ ██╔╝
        //  ██████╔╝██████╔╝██║██╔████╔██║███████║██████╔╝ ╚████╔╝     █████╔╝ █████╗   ╚████╔╝
        //  ██╔═══╝ ██╔══██╗██║██║╚██╔╝██║██╔══██║██╔══██╗  ╚██╔╝      ██╔═██╗ ██╔══╝    ╚██╔╝
        //  ██║     ██║  ██║██║██║ ╚═╝ ██║██║  ██║██║  ██║   ██║       ██║  ██╗███████╗   ██║
        //  ╚═╝     ╚═╝  ╚═╝╚═╝╚═╝     ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝   ╚═╝       ╚═╝  ╚═╝╚══════╝   ╚═╝
        //
        if (attrName === WLModel.primaryKey) {

          assert(!attrDef.allowNull, 'The primary key attribute should never be defined with `allowNull:true`.  (This should have already been caught in wl-schema during ORM initialization!  Please report this at http://sailsjs.com/bugs)');

          // Do quick, incomplete verification that a valid primary key value was sent back.
          var isProbablyValidPkValue = (
            record[attrName] !== '' &&
            record[attrName] !== 0 &&
            (
              _.isString(record[attrName]) || _.isNumber(record[attrName])
            )
          );

          if (!isProbablyValidPkValue) {
            console.warn('\n'+
              'Warning: Records sent back from a database adapter should always have a valid property\n'+
              'that corresponds with the primary key attribute (`'+WLModel.primaryKey+'`).  But in this result set,\n'+
              'after transforming columnNames back to attribute names for model `' + modelIdentity + '`,\n'+
              'there is a record with a missing or invalid `'+WLModel.primaryKey+'`.\n'+
              'Record:\n'+
              '```\n'+
              util.inspect(record, {depth:5})+'\n'+
              '```\n'+
              WARNING_SUFFIXES.MIGHT_BE_YOUR_FAULT
            );
          }

        }
        //  ███████╗██╗███╗   ██╗ ██████╗ ██╗   ██╗██╗      █████╗ ██████╗
        //  ██╔════╝██║████╗  ██║██╔════╝ ██║   ██║██║     ██╔══██╗██╔══██╗
        //  ███████╗██║██╔██╗ ██║██║  ███╗██║   ██║██║     ███████║██████╔╝
        //  ╚════██║██║██║╚██╗██║██║   ██║██║   ██║██║     ██╔══██║██╔══██╗
        //  ███████║██║██║ ╚████║╚██████╔╝╚██████╔╝███████╗██║  ██║██║  ██║
        //  ╚══════╝╚═╝╚═╝  ╚═══╝ ╚═════╝  ╚═════╝ ╚══════╝╚═╝  ╚═╝╚═╝  ╚═╝
        //
        else if (attrDef.model) {

          assert(!attrDef.allowNull, 'Singular ("model") association attributes should never be defined with `allowNull:true` (they always allow null, by nature!).  (This should have already been caught in wl-schema during ORM initialization!  Please report this at http://sailsjs.com/bugs)');

          // If record does not define a value for a singular association, that's ok.
          // It may have been deliberately excluded by the `select` or `omit` clause.
          if (_.isUndefined(record[attrName])) {
          }
          // If the value for this singular association came back as `null`, then that
          // might be ok too-- it could mean that the association is empty.
          // (Note that it might also mean that it is set, and that population was attempted,
          // but that it failed; presumably because the associated child record no longer exists)
          else if (_.isNull(record[attrName])) {
          }
          // If the value came back as something that looks vaguely like a valid primary key value,
          // then that's probably ok--  it could mean that the association was set, but not populated.
          else if ((_.isString(record[attrName]) || _.isNumber(record[attrName])) && record[attrName] !== '' && record[attrName] !== 0 && !_.isNaN(record[attrName])) {
          }
          // If the value came back as a dictionary, then that might be ok-- it could mean
          // the association was successfully populated.
          else if (_.isObject(record[attrName]) && !_.isArray(record[attrName]) && !_.isFunction(record[attrName])) {
            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
            // FUTURE: we could check this more carefully in the future by providing more
            // information to this utility-- specifically, the `populates` key from the S2Q.
            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
          }
          // Otherwise, the value is invalid.
          else {
            console.warn('\n'+
              'An association in a result record has an unexpected data type.  Since `'+attrName+'` is\n'+
              'a singular (association), it should come back from Waterline as either:\n'+
              '• `null` (if not populated and set to null explicitly, or populated but orphaned)\n'+
              '• a dictionary (if successfully populated), or\n'+
              '• a valid primary key value for the associated model (if set + not populated)\n'+
              'But for this record, after converting column names back into attribute names, it\n'+
              'wasn\'t any of those things.\n'+
              'Record:\n'+
              '```\n'+
              util.inspect(record, {depth:5})+'\n'+
              '```\n'+
              WARNING_SUFFIXES.MIGHT_BE_YOUR_FAULT
            );
          }

        }
        //  ██████╗ ██╗     ██╗   ██╗██████╗  █████╗ ██╗
        //  ██╔══██╗██║     ██║   ██║██╔══██╗██╔══██╗██║
        //  ██████╔╝██║     ██║   ██║██████╔╝███████║██║
        //  ██╔═══╝ ██║     ██║   ██║██╔══██╗██╔══██║██║
        //  ██║     ███████╗╚██████╔╝██║  ██║██║  ██║███████╗
        //  ╚═╝     ╚══════╝ ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚══════╝
        //
        else if (attrDef.collection) {
          assert(!attrDef.allowNull, 'Plural ("collection") association attributes should never be defined with `allowNull:true`.  (This should have already been caught in wl-schema during ORM initialization!  Please report this at http://sailsjs.com/bugs)');

          // If record does not define a value for a plural association, that's ok.
          // That probably just means it was not populated.
          if (_.isUndefined(record[attrName])) {
          }
          // If the value for this singular association came back as an array, then
          // that might be ok too-- it probably means that the association was populated.
          else if (_.isArray(record[attrName])) {
            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
            // FUTURE: we could check that it is an array of valid child records,
            // instead of just verifying that it is an array of _some kind_.
            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
          }
          // Otherwise, the value is invalid.
          else {
            console.warn('\n'+
              'An association in a result record has an unexpected data type.  Since `'+attrName+'` is\n'+
              'a plural (association), it should come back from Waterline as either:\n'+
              '• `undefined` (if not populated), or\n'+
              '• an array of child records (if populated)\n'+
              'But for this record, it wasn\'t any of those things.\n'+
              // Note that this could mean there was something else already there
              // (imagine changing your model to use a plural association instead
              // of an embedded array from a `type: 'json'` attribute)
              'Record:\n'+
              '```\n'+
              util.inspect(record, {depth:5})+'\n'+
              '```\n'+
              WARNING_SUFFIXES.MIGHT_BE_YOUR_FAULT
            );
          }

        }
        //  ███████╗████████╗ █████╗ ███╗   ███╗██████╗ ███████╗
        //  ██╔════╝╚══██╔══╝██╔══██╗████╗ ████║██╔══██╗██╔════╝
        //  ███████╗   ██║   ███████║██╔████╔██║██████╔╝███████╗
        //  ╚════██║   ██║   ██╔══██║██║╚██╔╝██║██╔═══╝ ╚════██║
        //  ███████║   ██║   ██║  ██║██║ ╚═╝ ██║██║     ███████║
        //  ╚══════╝   ╚═╝   ╚═╝  ╚═╝╚═╝     ╚═╝╚═╝     ╚══════╝
        //
        else if (attrDef.autoCreatedAt || attrDef.autoUpdatedAt) {

          assert(!attrDef.allowNull, 'Timestamp attributes should never be defined with `allowNull:true`.  (This should have already been caught in wl-schema during ORM initialization!  Please report this at http://sailsjs.com/bugs)');

          // If there is no value defined on the record for this attribute...
          if (_.isUndefined(record[attrName])) {

            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
            // FUTURE: Log a warning (but note that, to really get this right, we'd need access to
            // a clone of the `omit` and `select` clauses from the s2q criteria, plus the `populates`
            // query key from the s2q criteria -- probably also a clone of that)
            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

          }
          // Otherwise, we know there's SOMETHING there at least.
          else {

            // Do quick, very incomplete verification that a valid timestamp was sent back.
            var isProbablyValidTimestamp = (
              record[attrName] !== '' &&
              record[attrName] !== 0 &&
              (
                _.isString(record[attrName]) || _.isNumber(record[attrName]) || _.isDate(record[attrName])
              )
            );

            if (!isProbablyValidTimestamp) {
              console.warn('\n'+
                'Warning: After transforming columnNames back to attribute names for model `' + modelIdentity + '`,\n'+
                ' a record in the result has a value with an unexpected data type for property `'+attrName+'`.\n'+
                'The model\'s `'+attrName+'` attribute declares itself an auto timestamp with\n'+
                '`type: \''+attrDef.type+'\'`, but instead of a valid timestamp, the actual value\n'+
                'in the record is:\n'+
                '```\n'+
                util.inspect(record[attrName],{depth:5})+'\n'+
                '```\n'+
                WARNING_SUFFIXES.MIGHT_BE_YOUR_FAULT
              );
            }

          }//</else>

        }
        //  ███╗   ███╗██╗███████╗ ██████╗        ██╗████████╗██╗   ██╗██████╗ ███████╗██╗
        //  ████╗ ████║██║██╔════╝██╔════╝       ██╔╝╚══██╔══╝╚██╗ ██╔╝██╔══██╗██╔════╝╚██╗
        //  ██╔████╔██║██║███████╗██║            ██║    ██║    ╚████╔╝ ██████╔╝█████╗   ██║
        //  ██║╚██╔╝██║██║╚════██║██║            ██║    ██║     ╚██╔╝  ██╔═══╝ ██╔══╝   ██║
        //  ██║ ╚═╝ ██║██║███████║╚██████╗██╗    ╚██╗   ██║      ██║   ██║     ███████╗██╔╝
        //  ╚═╝     ╚═╝╚═╝╚══════╝ ╚═════╝╚═╝     ╚═╝   ╚═╝      ╚═╝   ╚═╝     ╚══════╝╚═╝
        //
        else {

          // Sanity check:
          if (attrDef.type === 'json' || attrDef.type === 'ref') {
            assert(!attrDef.allowNull, '`type:\'json\'` and `type:\'ref\'` attributes should never be defined with `allowNull:true`.  (This should have already been caught in wl-schema during ORM initialization!  Please report this at http://sailsjs.com/bugs)');
          }

          // If there is no value defined on the record for this attribute...
          if (_.isUndefined(record[attrName])) {

            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
            // FUTURE: Log a warning (but note that, to really get this right, we'd need access to
            // a clone of the `omit` and `select` clauses from the s2q criteria, plus the `populates`
            // query key from the s2q criteria -- probably also a clone of that)
            // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

          }
          // If the value is `null`, and the attribute has `allowNull:true`, then its ok.
          else if (_.isNull(record[attrName]) && attrDef.allowNull === true) {
            // Nothing to validate here.
          }
          // Otherwise, we'll need to validate the value.
          else {

            // Strictly validate the value vs. the attribute's `type`, and if it is
            // obviously incorrect, then log a warning (but don't actually coerce it.)
            try {
              rttc.validateStrict(attrDef.type, record[attrName]);
            } catch (e) {
              switch (e.code) {
                case 'E_INVALID':

                  if (_.isNull(record[attrName])) {
                    console.warn('\n'+
                      'Warning: After transforming columnNames back to attribute names for model `' + modelIdentity + '`,\n'+
                      ' a record in the result has a value of `null` for property `'+attrName+'`.\n'+
                      'Since the `'+attrName+'` attribute declares `type: \''+attrDef.type+'\'`,\n'+
                      'without ALSO declaring `allowNull: true`, this `null` value is unexpected.\n'+
                      '(To resolve, either change this attribute to `allowNull: true` or update\n'+
                      'existing records in the database accordingly.)\n'+
                      WARNING_SUFFIXES.MIGHT_BE_YOUR_FAULT
                    );
                  }
                  else {
                    console.warn('\n'+
                      'Warning: After transforming columnNames back to attribute names for model `' + modelIdentity + '`,\n'+
                      ' a record in the result has a value with an unexpected data type for property `'+attrName+'`.\n'+
                      'The corresponding attribute declares `type: \''+attrDef.type+'\'` but instead\n'+
                      'of that, the actual value is:\n'+
                      '```\n'+
                      util.inspect(record[attrName],{depth:5})+'\n'+
                      '```\n'+
                      WARNING_SUFFIXES.MIGHT_BE_YOUR_FAULT
                    );
                  }
                  break;
                default: throw e;
              }
            }//>-•

          }

        }


        //>-

        //   ██████╗██╗  ██╗███████╗ ██████╗██╗  ██╗
        //  ██╔════╝██║  ██║██╔════╝██╔════╝██║ ██╔╝
        //  ██║     ███████║█████╗  ██║     █████╔╝
        //  ██║     ██╔══██║██╔══╝  ██║     ██╔═██╗
        //  ╚██████╗██║  ██║███████╗╚██████╗██║  ██╗
        //   ╚═════╝╚═╝  ╚═╝╚══════╝ ╚═════╝╚═╝  ╚═╝
        //
        //  ██████╗ ███████╗ ██████╗ ██╗   ██╗██╗██████╗ ███████╗██████╗ ███╗   ██╗███████╗███████╗███████╗
        //  ██╔══██╗██╔════╝██╔═══██╗██║   ██║██║██╔══██╗██╔════╝██╔══██╗████╗  ██║██╔════╝██╔════╝██╔════╝
        //  ██████╔╝█████╗  ██║   ██║██║   ██║██║██████╔╝█████╗  ██║  ██║██╔██╗ ██║█████╗  ███████╗███████╗
        //  ██╔══██╗██╔══╝  ██║▄▄ ██║██║   ██║██║██╔══██╗██╔══╝  ██║  ██║██║╚██╗██║██╔══╝  ╚════██║╚════██║
        //  ██║  ██║███████╗╚██████╔╝╚██████╔╝██║██║  ██║███████╗██████╔╝██║ ╚████║███████╗███████║███████║
        //  ╚═╝  ╚═╝╚══════╝ ╚══▀▀═╝  ╚═════╝ ╚═╝╚═╝  ╚═╝╚══════╝╚═════╝ ╚═╝  ╚═══╝╚══════╝╚══════╝╚══════╝
        //
        // If attribute is required, check that the value returned in this record
        // is neither `null` nor empty string ('') nor `undefined`.
        if (attrDef.required) {

          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
          // FUTURE: Log a warning (but note that, to really get this right, we'd need access to
          // a clone of the `omit` and `select` clauses from the s2q criteria, plus the `populates`
          // query key from the s2q criteria -- probably also a clone of that)
          //
          // ```
          // if (_.isUndefined(record[attrName]) || _.isNull(record[attrName]) || record[attrName] === '') {
          //   // (We'd also need to make sure this wasn't deliberately exluded by custom projections
          //   //  before logging this warning.)
          //   console.warn('\n'+
          //     'Warning: After transforming columnNames back to attribute names for model `' + modelIdentity + '`,\n'+
          //     'a record in the result contains an unexpected value (`'+util.inspect(record[attrName],{depth:1})+'`)`\n'+
          //     'for its `'+attrName+'` property.  Since `'+attrName+'` is a required attribute,\n'+
          //     'it should never be returned as `null` or empty string.  This usually means there\n'+
          //     'is existing data that was persisted some time before the `'+attrName+'` attribute\n'+
          //     'was set to `required: true`.  To make this warning go away, either remove\n'+
          //     '`required: true` from this attribute, or update the existing, already-stored data\n'+
          //     'so that the `'+attrName+'` of all records is set to some value other than null or\n'+
          //     'empty string.\n'+
          //     WARNING_SUFFIXES.MIGHT_BE_YOUR_FAULT
          //   );
          // }
          // ```
          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

        }

      });//</_.each>

    }//fi    (verify records)


    //   █████╗ ████████╗████████╗ █████╗  ██████╗██╗  ██╗
    //  ██╔══██╗╚══██╔══╝╚══██╔══╝██╔══██╗██╔════╝██║  ██║
    //  ███████║   ██║      ██║   ███████║██║     ███████║
    //  ██╔══██║   ██║      ██║   ██╔══██║██║     ██╔══██║
    //  ██║  ██║   ██║      ██║   ██║  ██║╚██████╗██║  ██║
    //  ╚═╝  ╚═╝   ╚═╝      ╚═╝   ╚═╝  ╚═╝ ╚═════╝╚═╝  ╚═╝
    //
    //   ██████╗██╗   ██╗███████╗████████╗ ██████╗ ███╗   ███╗
    //  ██╔════╝██║   ██║██╔════╝╚══██╔══╝██╔═══██╗████╗ ████║
    //  ██║     ██║   ██║███████╗   ██║   ██║   ██║██╔████╔██║
    //  ██║     ██║   ██║╚════██║   ██║   ██║   ██║██║╚██╔╝██║
    //  ╚██████╗╚██████╔╝███████║   ██║   ╚██████╔╝██║ ╚═╝ ██║
    //   ╚═════╝ ╚═════╝ ╚══════╝   ╚═╝    ╚═════╝ ╚═╝     ╚═╝
    //
    //  ████████╗ ██████╗      ██╗███████╗ ██████╗ ███╗   ██╗ ██╗██╗
    //  ╚══██╔══╝██╔═══██╗     ██║██╔════╝██╔═══██╗████╗  ██║██╔╝╚██╗
    //     ██║   ██║   ██║     ██║███████╗██║   ██║██╔██╗ ██║██║  ██║
    //     ██║   ██║   ██║██   ██║╚════██║██║   ██║██║╚██╗██║██║  ██║
    //  ██╗██║   ╚██████╔╝╚█████╔╝███████║╚██████╔╝██║ ╚████║╚██╗██╔╝
    //  ╚═╝╚═╝    ╚═════╝  ╚════╝ ╚══════╝ ╚═════╝ ╚═╝  ╚═══╝ ╚═╝╚═╝
    //  ╦╔═╗  ┬─┐┌─┐┬  ┌─┐┬  ┬┌─┐┌┐┌┌┬┐
    //  ║╠╣   ├┬┘├┤ │  ├┤ └┐┌┘├─┤│││ │
    //  ╩╚    ┴└─└─┘┴─┘└─┘ └┘ ┴ ┴┘└┘ ┴ooo
    if (WLModel.customToJSON) {
      Object.defineProperty(record, 'toJSON', {
        writable: true,
        value: WLModel.customToJSON
      });
    }//>-


    //  ██████╗ ███████╗ ██████╗██████╗ ██╗   ██╗██████╗ ████████╗    ██████╗  █████╗ ████████╗ █████╗
    //  ██╔══██╗██╔════╝██╔════╝██╔══██╗╚██╗ ██╔╝██╔══██╗╚══██╔══╝    ██╔══██╗██╔══██╗╚══██╔══╝██╔══██╗
    //  ██║  ██║█████╗  ██║     ██████╔╝ ╚████╔╝ ██████╔╝   ██║       ██║  ██║███████║   ██║   ███████║
    //  ██║  ██║██╔══╝  ██║     ██╔══██╗  ╚██╔╝  ██╔═══╝    ██║       ██║  ██║██╔══██║   ██║   ██╔══██║
    //  ██████╔╝███████╗╚██████╗██║  ██║   ██║   ██║        ██║       ██████╔╝██║  ██║   ██║   ██║  ██║
    //  ╚═════╝ ╚══════╝ ╚═════╝╚═╝  ╚═╝   ╚═╝   ╚═╝        ╚═╝       ╚═════╝ ╚═╝  ╚═╝   ╚═╝   ╚═╝  ╚═╝
    //  ╦╔═╗  ┬─┐┌─┐┬  ┌─┐┬  ┬┌─┐┌┐┌┌┬┐
    //  ║╠╣   ├┬┘├┤ │  ├┤ └┐┌┘├─┤│││ │
    //  ╩╚    ┴└─└─┘┴─┘└─┘ └┘ ┴ ┴┘└┘ ┴ooo
    var willDecrypt = meta && meta.decrypt;
    if (willDecrypt) {
      _.each(WLModel.attributes, function (attrDef, attrName){
        try {
          if (attrDef.encrypt) {

            // Never try to decrypt `''`(empty string), `0` (zero), `false`, or `null`, since these are
            // possible base values, which might end up in the database.  (Note that if this is a required
            // attribute, we could probably be more picky-- but it seems unlikely that encrypting these base
            // values at rest will ever be a priority, since they don't contain any sensitive information.
            // Arguably, there are edge cases where knowing _whether_ a particular field is at its base value
            // could be deemed sensitive info, but building around that extreme edge case seems like a bad idea
            // that probably isn't worth the extra headache and complexity in core.)
            if (record[attrName] === '' || record[attrName] === 0 || record[attrName] === false || _.isNull(record[attrName])) {
              // Don't try to decrypt these.
            }
            else {

              // Decrypt using the appropriate key from the configured DEKs.
              var decryptedButStillJsonEncoded;

              // console.log('•••••decrypting: `'+util.inspect(record[attrName], {depth:null})+'`');

              // Require this down here for Node version compat.
              var EA = require('encrypted-attr');
              decryptedButStillJsonEncoded = EA([attrName], {
                keys: WLModel.dataEncryptionKeys
              })
              .decryptAttribute(undefined, record[attrName]);
              // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
              // Alternative: (hack for testing)
              // ```
              // if (!record[attrName].match(/^ENCRYPTED:/)){ throw new Error('Unexpected behavior: Can\'t decrypt something already decrypted!!!'); }
              // decryptedButStillJsonEncoded = record[attrName].replace(/^ENCRYPTED:/, '');
              // ```
              // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

              // Finally, JSON-decode the value, to allow for differentiating between strings/numbers/booleans/null.
              try {
                record[attrName] = JSON.parse(decryptedButStillJsonEncoded);
              } catch (err) {
                throw flaverr({
                  message: 'After initially decrypting the raw data, Waterline attempted to JSON-parse the data '+
                  'to ensure it was accurately decoded into the correct data type (for example, `2` vs `\'2\'`).  '+
                  'But this time, JSON.parse() failed with the following error:  '+err.message
                }, err);
              }

            }//fi

          }//fi
        } catch (err) {
          // console.log('•••••was attempting to decrypt this value: `'+util.inspect(record[attrName], {depth:null})+'`');

          // Note: Decryption might not work, because there's no way of knowing what could have gotten into
          // the database  (e.g. from other processes, apps, maybe not even Node.js, etc.)
          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
          // FUTURE: Instead of failing with an error, consider logging a warning and
          // sending back the data as-is. (e.g. and attach MIGHT_BE_YOUR_FAULT suffix.)
          // But remember: this is potentially sensitive data we're talking about, so being
          // a little over-strict seems like the right idea.  Maybe the right answer is to
          // still log the warning, but instead of sending back the potentially-sensitive data,
          // log it as part of the warning and send back whatever the appropriate base value is
          // instead.
          //
          // Regardless, for now we use an actual error to be on the safe side.
          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
          throw flaverr({
            message: 'Decryption failed for `'+attrName+'` (in a `'+WLModel.identity+'` record).\n'+
            'The actual value in the record that could not be decrypted is:\n'+
            '```\n'+
            util.inspect(record[attrName],{depth:5})+'\n'+
            '```\n'+
            'Error details:\n'+
            '  '+err.message
          }, _.isError(err) ? err : new Error());
        }
      });//∞
    }//fi



  }, false, modelIdentity, orm);//</eachRecordDeep>


  //
  // Records are modified in-place above, so there is no return value.
  //

  // console.timeEnd('processAllRecords');

};