balderdashy/waterline

View on GitHub
lib/waterline/utils/query/private/normalize-new-record.js

Summary

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

var util = require('util');
var assert = require('assert');
var _ = require('@sailshq/lodash');
var flaverr = require('flaverr');
var rttc = require('rttc');
var getModel = require('../../ontology/get-model');
var getAttribute = require('../../ontology/get-attribute');
var normalizeValueToSet = require('./normalize-value-to-set');


/**
 * normalizeNewRecord()
 *
 * Validate and normalize the provided dictionary (`newRecord`), hammering it destructively
 * into the standardized format suitable to be part of a "stage 2 query" (see ARCHITECTURE.md).
 * This allows us to present it in a normalized fashion to lifecycle callbacks, as well to
 * other internal utilities within Waterline.
 *
 * This function has a return value.   But realize that this is only to allow for an
 * edge case: For convenience, the provided value is allowed to be `undefined`, in which
 * case it is automatically converted into a new, empty dictionary (plain JavaScript object).
 * But most of the time, the provided value will be irreversibly mutated in-place, AS WELL AS returned.
 *
 * --
 *
 * THIS UTILITY IS NOT CURRENTLY RESPONSIBLE FOR APPLYING HIGH-LEVEL ("anchor") VALIDATION RULES!
 * (but note that this could change at some point in the future)
 *
 * --
 *
 * @param  {Ref?} newRecord
 *         The original new record (i.e. from a "stage 1 query").
 *         (If provided as `undefined`, it will be understood as `{}`)
 *         > WARNING:
 *         > IN SOME CASES (BUT NOT ALL!), THE PROVIDED DICTIONARY WILL
 *         > UNDERGO DESTRUCTIVE, IN-PLACE CHANGES JUST BY PASSING IT
 *         > IN TO THIS UTILITY.
 *
 * @param {String} modelIdentity
 *        The identity of the model this record is for (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.
 *
 * @param {Number} currentTimestamp
 *        The current JS timestamp (epoch ms).
 *        > This is passed in so that it can be exactly the same in the situation where
 *        > this utility might be running multiple times for a given query.
 *
 * @param {Dictionary?} meta
 *        The contents of the `meta` query key, if one was provided.
 *        > Useful for propagating query options to low-level utilities like this one.
 *
 * --
 *
 * @returns {Dictionary}
 *          The successfully-normalized new record, ready for use in a stage 2 query.
 *
 * --
 *
 * @throws {Error} If it encounters incompatible usage in the provided `newRecord`,
 *   |             including e.g. the case where an invalid value is specified for
 *   |             an association.
 *   |     @property {String} code
 *   |               - E_HIGHLY_IRREGULAR
 *
 *
 * @throws {Error} If the provided `newRecord` is missing a value for a required attribute,
 *   |             or if it specifies `null` or empty string ("") for it.
 *   |     @property {String} code
 *   |               - E_REQUIRED
 *   |     @property {String} attrName
 *
 *
 * @throws {Error} If it encounters a value with an incompatible data type in the provided
 *   |             `newRecord`.  This is only versus the attribute's declared "type" --
 *   |             failed validation versus associations results in a different error code
 *   |             (see above).
 *   |     @property {String} code
 *   |               - E_TYPE
 *   |     @property {String} attrName
 *   |     @property {String} expectedType
 *   |               - string
 *   |               - number
 *   |               - boolean
 *   |               - json
 *   |
 *   | This is only versus the attribute's declared "type", or other similar type safety issues  --
 *   | certain failed checks for associations result in a different error code (see above).
 *   |
 *   | Remember:
 *   | This is the case where a _completely incorrect type of data_ was passed in.
 *   | This is NOT a high-level "anchor" validation failure! (see below for that)
 *   | > Unlike anchor validation errors, this exception should never be negotiated/parsed/used
 *   | > for delivering error messages to end users of an application-- it is carved out
 *   | > separately purely to make things easier to follow for the developer.
 *
 *
 * @throws {Error} If it encounters any values within the provided `newRecord` that violate
 *   |             high-level (anchor) validation rules.
 *   |     @property {String} code
 *   |      - E_VIOLATES_RULES
 *   |     @property {String} attrName
 *   |     @property {Array} ruleViolations
 *   |         [
 *   |           {
 *   |             rule: 'minLength',    //(isEmail/isNotEmptyString/max/isNumber/etc)
 *   |             message: 'Too few characters (max 30)'
 *   |           },
 *   |           ...
 *   |         ]
 *
 *
 * @throws {Error} If anything else unexpected occurs.
 */
module.exports = function normalizeNewRecord(newRecord, modelIdentity, orm, currentTimestamp, meta) {

  // Tolerate this being left undefined by inferring a reasonable default.
  // Note that we can't bail early, because we need to check for more stuff
  // (there might be required attrs!)
  if (_.isUndefined(newRecord)){
    newRecord = {};
  }//>-

  // Verify that this is now a dictionary.
  if (!_.isObject(newRecord) || _.isFunction(newRecord) || _.isArray(newRecord)) {
    throw flaverr('E_HIGHLY_IRREGULAR', new Error(
      'Expecting new record to be provided as a dictionary (plain JavaScript object) but instead, got: '+util.inspect(newRecord,{depth:5})
    ));
  }//-•


  // Look up the Waterline model for this query.
  // > This is so that we can reference the original model definition.
  var WLModel;
  try {
    WLModel = getModel(modelIdentity, orm);
  } catch (e) {
    switch (e.code) {
      case 'E_MODEL_NOT_REGISTERED': throw new Error('Consistency violation: '+e.message);
      default: throw e;
    }
  }//</catch>



  //  ███╗   ██╗ ██████╗ ██████╗ ███╗   ███╗ █████╗ ██╗     ██╗███████╗███████╗
  //  ████╗  ██║██╔═══██╗██╔══██╗████╗ ████║██╔══██╗██║     ██║╚══███╔╝██╔════╝
  //  ██╔██╗ ██║██║   ██║██████╔╝██╔████╔██║███████║██║     ██║  ███╔╝ █████╗
  //  ██║╚██╗██║██║   ██║██╔══██╗██║╚██╔╝██║██╔══██║██║     ██║ ███╔╝  ██╔══╝
  //  ██║ ╚████║╚██████╔╝██║  ██║██║ ╚═╝ ██║██║  ██║███████╗██║███████╗███████╗
  //  ╚═╝  ╚═══╝ ╚═════╝ ╚═╝  ╚═╝╚═╝     ╚═╝╚═╝  ╚═╝╚══════╝╚═╝╚══════╝╚══════╝
  //
  //  ██████╗ ██████╗  ██████╗ ██╗   ██╗██╗██████╗ ███████╗██████╗
  //  ██╔══██╗██╔══██╗██╔═══██╗██║   ██║██║██╔══██╗██╔════╝██╔══██╗
  //  ██████╔╝██████╔╝██║   ██║██║   ██║██║██║  ██║█████╗  ██║  ██║
  //  ██╔═══╝ ██╔══██╗██║   ██║╚██╗ ██╔╝██║██║  ██║██╔══╝  ██║  ██║
  //  ██║     ██║  ██║╚██████╔╝ ╚████╔╝ ██║██████╔╝███████╗██████╔╝
  //  ╚═╝     ╚═╝  ╚═╝ ╚═════╝   ╚═══╝  ╚═╝╚═════╝ ╚══════╝╚═════╝
  //
  //  ██╗   ██╗ █████╗ ██╗     ██╗   ██╗███████╗███████╗
  //  ██║   ██║██╔══██╗██║     ██║   ██║██╔════╝██╔════╝
  //  ██║   ██║███████║██║     ██║   ██║█████╗  ███████╗
  //  ╚██╗ ██╔╝██╔══██║██║     ██║   ██║██╔══╝  ╚════██║
  //   ╚████╔╝ ██║  ██║███████╗╚██████╔╝███████╗███████║
  //    ╚═══╝  ╚═╝  ╚═╝╚══════╝ ╚═════╝ ╚══════╝╚══════╝
  //

  // Now loop over and check every key specified in this new record.
  _.each(_.keys(newRecord), function (supposedAttrName){

    // Validate & normalize this value.
    // > Note that we explicitly ALLOW values to be provided for plural associations by passing in `true`.
    try {
      newRecord[supposedAttrName] = normalizeValueToSet(newRecord[supposedAttrName], supposedAttrName, modelIdentity, orm, meta);
    } catch (e) {
      switch (e.code) {

        // If its RHS should be ignored (e.g. because it is `undefined`), then delete this key and bail early.
        case 'E_SHOULD_BE_IGNORED':
          delete newRecord[supposedAttrName];
          return;

        case 'E_HIGHLY_IRREGULAR':
          throw flaverr('E_HIGHLY_IRREGULAR', new Error(
            'Could not use specified `'+supposedAttrName+'`.  '+e.message
          ));

        case 'E_TYPE':
          throw flaverr({
            code: 'E_TYPE',
            attrName: supposedAttrName,
            expectedType: e.expectedType
          }, new Error(
            'New record contains the wrong type of data for property `'+supposedAttrName+'`.  '+e.message
          ));

        case 'E_REQUIRED':
          throw flaverr({
            code: 'E_REQUIRED',
            attrName: supposedAttrName
          }, new Error(
            'Could not use specified `'+supposedAttrName+'`.  '+e.message
          ));

        case 'E_VIOLATES_RULES':
          if (!_.isArray(e.ruleViolations) || e.ruleViolations.length === 0) {
            throw new Error('Consistency violation: This Error instance should ALWAYS have a non-empty array as its `ruleViolations` property.  But instead, its `ruleViolations` property is: '+util.inspect(e.ruleViolations, {depth: 5})+'\nAlso, for completeness/context, here is the error\'s complete stack: '+e.stack);
          }

          throw flaverr({
            code: 'E_VIOLATES_RULES',
            attrName: supposedAttrName,
            ruleViolations: e.ruleViolations
          }, new Error(
            'Could not use specified `'+supposedAttrName+'`.  '+e.message
          ));

        default:
          throw e;
      }
    }//</catch>

  });//</_.each() key in the new record>



  //  ┌─┐┬ ┬┌─┐┌─┐┬┌─  ┌─┐┌─┐┬─┐  ╔═╗╦═╗╦╔╦╗╔═╗╦═╗╦ ╦  ╦╔═╔═╗╦ ╦
  //  │  ├─┤├┤ │  ├┴┐  ├┤ │ │├┬┘  ╠═╝╠╦╝║║║║╠═╣╠╦╝╚╦╝  ╠╩╗║╣ ╚╦╝
  //  └─┘┴ ┴└─┘└─┘┴ ┴  └  └─┘┴└─  ╩  ╩╚═╩╩ ╩╩ ╩╩╚═ ╩   ╩ ╩╚═╝ ╩
  //
  // There will always be at least one required attribute: the primary key...
  // but, actually, we ALLOW it to be omitted since it might be (and usually is)
  // decided by the underlying database.
  //
  // That said, it must NEVER be `null`.
  if (_.isNull(newRecord[WLModel.primaryKey])) {
    throw flaverr('E_HIGHLY_IRREGULAR', new Error(
      'Could not use specified value (`null`) as the primary key value (`'+WLModel.primaryKey+'`) for a new record.  '+
      '(Try omitting it instead.)'
    ));
  }//-•

  // > Note that, if a non-null value WAS provided for the primary key, then it will have already
  // > been validated/normalized (if relevant) by the type safety check above.  So we don't need to
  // > worry about addressing any of that here-- doing so would be duplicative.



  //  ██╗      ██████╗  ██████╗ ██████╗      ██████╗ ██╗   ██╗███████╗██████╗
  //  ██║     ██╔═══██╗██╔═══██╗██╔══██╗    ██╔═══██╗██║   ██║██╔════╝██╔══██╗
  //  ██║     ██║   ██║██║   ██║██████╔╝    ██║   ██║██║   ██║█████╗  ██████╔╝
  //  ██║     ██║   ██║██║   ██║██╔═══╝     ██║   ██║╚██╗ ██╔╝██╔══╝  ██╔══██╗
  //  ███████╗╚██████╔╝╚██████╔╝██║         ╚██████╔╝ ╚████╔╝ ███████╗██║  ██║
  //  ╚══════╝ ╚═════╝  ╚═════╝ ╚═╝          ╚═════╝   ╚═══╝  ╚══════╝╚═╝  ╚═╝
  //
  //   █████╗ ██╗     ██╗          █████╗ ████████╗████████╗██████╗
  //  ██╔══██╗██║     ██║         ██╔══██╗╚══██╔══╝╚══██╔══╝██╔══██╗
  //  ███████║██║     ██║         ███████║   ██║      ██║   ██████╔╝
  //  ██╔══██║██║     ██║         ██╔══██║   ██║      ██║   ██╔══██╗
  //  ██║  ██║███████╗███████╗    ██║  ██║   ██║      ██║   ██║  ██║
  //  ╚═╝  ╚═╝╚══════╝╚══════╝    ╚═╝  ╚═╝   ╚═╝      ╚═╝   ╚═╝  ╚═╝
  //
  //  ██████╗ ███████╗███████╗██╗███╗   ██╗██╗████████╗██╗ ██████╗ ███╗   ██╗███████╗
  //  ██╔══██╗██╔════╝██╔════╝██║████╗  ██║██║╚══██╔══╝██║██╔═══██╗████╗  ██║██╔════╝
  //  ██║  ██║█████╗  █████╗  ██║██╔██╗ ██║██║   ██║   ██║██║   ██║██╔██╗ ██║███████╗
  //  ██║  ██║██╔══╝  ██╔══╝  ██║██║╚██╗██║██║   ██║   ██║██║   ██║██║╚██╗██║╚════██║
  //  ██████╔╝███████╗██║     ██║██║ ╚████║██║   ██║   ██║╚██████╔╝██║ ╚████║███████║
  //  ╚═════╝ ╚══════╝╚═╝     ╚═╝╚═╝  ╚═══╝╚═╝   ╚═╝   ╚═╝ ╚═════╝ ╚═╝  ╚═══╝╚══════╝
  //
  //     ██╗       ██████╗ ███████╗ █████╗ ██╗         ██╗    ██╗██╗████████╗██╗  ██╗
  //     ██║       ██╔══██╗██╔════╝██╔══██╗██║         ██║    ██║██║╚══██╔══╝██║  ██║
  //  ████████╗    ██║  ██║█████╗  ███████║██║         ██║ █╗ ██║██║   ██║   ███████║
  //  ██╔═██╔═╝    ██║  ██║██╔══╝  ██╔══██║██║         ██║███╗██║██║   ██║   ██╔══██║
  //  ██████║      ██████╔╝███████╗██║  ██║███████╗    ╚███╔███╔╝██║   ██║   ██║  ██║
  //  ╚═════╝      ╚═════╝ ╚══════╝╚═╝  ╚═╝╚══════╝     ╚══╝╚══╝ ╚═╝   ╚═╝   ╚═╝  ╚═╝
  //
  //   ██████╗ ███╗   ███╗██╗███████╗███████╗██╗ ██████╗ ███╗   ██╗███████╗
  //  ██╔═══██╗████╗ ████║██║██╔════╝██╔════╝██║██╔═══██╗████╗  ██║██╔════╝
  //  ██║   ██║██╔████╔██║██║███████╗███████╗██║██║   ██║██╔██╗ ██║███████╗
  //  ██║   ██║██║╚██╔╝██║██║╚════██║╚════██║██║██║   ██║██║╚██╗██║╚════██║
  //  ╚██████╔╝██║ ╚═╝ ██║██║███████║███████║██║╚██████╔╝██║ ╚████║███████║
  //   ╚═════╝ ╚═╝     ╚═╝╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝  ╚═══╝╚══════╝
  //

  //  ┌─┐┬ ┬┌─┐┌─┐┬┌─  ┌─┐┌─┐┬─┐  ╔═╗╔╦╗╦ ╦╔═╗╦═╗  ┬─┐┌─┐┌─┐ ┬ ┬┬┬─┐┌─┐┌┬┐  ┌─┐┌┬┐┌┬┐┬─┐┌─┐
  //  │  ├─┤├┤ │  ├┴┐  ├┤ │ │├┬┘  ║ ║ ║ ╠═╣║╣ ╠╦╝  ├┬┘├┤ │─┼┐│ ││├┬┘├┤  ││  ├─┤ │  │ ├┬┘└─┐
  //  └─┘┴ ┴└─┘└─┘┴ ┴  └  └─┘┴└─  ╚═╝ ╩ ╩ ╩╚═╝╩╚═  ┴└─└─┘└─┘└└─┘┴┴└─└─┘─┴┘  ┴ ┴ ┴  ┴ ┴└─└─┘
  //   ┬   ┌─┐┌─┐┌─┐┬ ┬ ┬  ╔╦╗╔═╗╔═╗╔═╗╦ ╦╦ ╔╦╗╔═╗
  //  ┌┼─  ├─┤├─┘├─┘│ └┬┘   ║║║╣ ╠╣ ╠═╣║ ║║  ║ ╚═╗
  //  └┘   ┴ ┴┴  ┴  ┴─┘┴   ═╩╝╚═╝╚  ╩ ╩╚═╝╩═╝╩ ╚═╝
  // Check that any OTHER required attributes are represented as keys, and neither `undefined` nor `null`.
  _.each(WLModel.attributes, function (attrDef, attrName) {

    // Quick sanity check.
    var isAssociation = attrDef.model || attrDef.collection;
    if (isAssociation && !_.isUndefined(attrDef.defaultsTo)) {
      throw new Error('Consistency violation: `defaultsTo` should never be defined for an association.  But `'+attrName+'` declares just such an inappropriate `defaultsTo`: '+util.inspect(attrDef.defaultsTo, {depth:5})+'');
    }

    // If the provided value is `undefined`, then it's considered an omission.
    // Otherwise, this is NOT an omission, so there's no way we'll need to mess
    // w/ any kind of requiredness check, or to apply a default value or a timestamp.
    // (i.e. in that case, we'll simply bail & skip ahead to the next attribute.)
    if (!_.isUndefined(newRecord[attrName])) {
      return;
    }//-•


    // IWMIH, we know the value is undefined, and thus we're dealing with an omission.


    // If this is for a required attribute...
    if (attrDef.required) {

      throw flaverr({
        code: 'E_REQUIRED',
        attrName: attrName
      }, new Error(
        'Missing value for required attribute `'+attrName+'`.  '+
        'Expected ' + (function _getExpectedNounPhrase (){
          if (!attrDef.model && !attrDef.collection) {
            return rttc.getNounPhrase(attrDef.type);
          }//-•
          var otherModelIdentity = attrDef.model ? attrDef.model : attrDef.collection;
          var OtherModel = getModel(otherModelIdentity, orm);
          var otherModelPkType = getAttribute(OtherModel.primaryKey, otherModelIdentity, orm).type;
          return rttc.getNounPhrase(otherModelPkType)+' (the '+OtherModel.primaryKey+' of a '+otherModelIdentity+')';
        })()+', '+
        'but instead, got: '+util.inspect(newRecord[attrName], {depth: 5})+''
      ));

    }//-•


    // IWMIH, this is for an optional attribute.


    // If this is the primary key attribute, then set it to `null`.
    // (https://docs.google.com/spreadsheets/d/1whV739iW6O9SxRZLCIe2lpvuAUqm-ie7j7tn_Pjir3s/edit#gid=1814738146)
    // (This gets dealt with in the adapter later!)
    if (attrName === WLModel.primaryKey) {
      newRecord[attrName] = null;
    }
    // Default singular associations to `null`.
    else if (attrDef.model) {
      newRecord[attrName] = null;
    }
    // Default plural associations to `[]`.
    else if (attrDef.collection) {
      newRecord[attrName] = [];
    }
    // Or apply the default if there is one.
    else if (attrDef.defaultsTo !== undefined) {

      // Deep clone the defaultsTo value.
      //
      // > FUTURE: eliminate the need for this deep clone by ensuring that we never mutate
      // > this value anywhere else in Waterline and in core adapters.
      // > (In the mean time, this behavior should not be relied on in any new code.)
      newRecord[attrName] = _.cloneDeep(attrDef.defaultsTo);

      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      // FUTURE: maybe support encryption of the default value here.
      // (See the related note in `waterline.js` for more information.)
      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    }
    // Or use the timestamp, if this is `autoCreatedAt` or `autoUpdatedAt`
    // (the latter because we set autoUpdatedAt to the same thing as `autoCreatedAt`
    // when initially creating a record)
    //
    // > Note that this other timestamp is passed in so that all new records share
    // > the exact same timestamp (in a `.createEach()` scenario, for example)
    else if (attrDef.autoCreatedAt || attrDef.autoUpdatedAt) {

      assert(attrDef.type === 'number' || attrDef.type === 'string' || attrDef.type === 'ref', 'If an attribute has `autoCreatedAt: true` or `autoUpdatedAt: true`, then it should always have either `type: \'string\'`, `type: \'number\'` or `type: \'ref\'`.  But the definition for attribute (`'+attrName+'`) has somehow gotten into an impossible state: It has `autoCreatedAt: '+attrDef.autoCreatedAt+'`, `autoUpdatedAt: '+attrDef.autoUpdatedAt+'`, and `type: \''+attrDef.type+'\'`');

      // Set the value equal to the current timestamp, using the appropriate format.
      if (attrDef.type === 'string') {
        newRecord[attrName] = (new Date(currentTimestamp)).toJSON();
      }
      else if (attrDef.type === 'ref') {
        newRecord[attrName] = new Date(currentTimestamp);
      }
      else {
        newRecord[attrName] = currentTimestamp;
      }

    }
    // Or use `null`, if this attribute specially expects/allows that as its base value.
    else if (attrDef.allowNull) {
      newRecord[attrName] = null;
    }
    // Or otherwise, just set it to the appropriate base value.
    else {
      newRecord[attrName] = rttc.coerce(attrDef.type);
    }//>-

  });//</_.each()>


  // Return the normalized dictionary.
  return newRecord;

};