UNPKG

waterline

Version:

An ORM for Node.js and the Sails framework

737 lines (651 loc) 39.4 kB
/** * Module dependencies */ var util = require('util'); var assert = require('assert'); var _ = require('@sailshq/lodash'); var anchor = require('anchor'); var flaverr = require('flaverr'); var rttc = require('rttc'); // var EA = require('encrypted-attr'); « this is required below for node compat. var getModel = require('../../ontology/get-model'); var getAttribute = require('../../ontology/get-attribute'); var isValidAttributeName = require('./is-valid-attribute-name'); var normalizePkValue = require('./normalize-pk-value'); var normalizePkValueOrValues = require('./normalize-pk-value-or-values'); /** * normalizeValueToSet() * * Validate and normalize the provided `value`, hammering it destructively into a format * that is compatible with the specified attribute. (Also take care of encrypting the `value`, * if configured to do so by the corresponding attribute definition.) * * This function has a return value. But realize that this is only because the provided value * _might_ be a string, number, or some other primitive that is NOT passed by reference, and thus * must be replaced, rather than modified. * * -- * * @param {Ref} value * The value to set (i.e. from the `valuesToSet` or `newRecord` query keys of a "stage 1 query"). * (If provided as `undefined`, it will be ignored) * > WARNING: * > IN SOME CASES (BUT NOT ALL!), THE PROVIDED VALUE WILL * > UNDERGO DESTRUCTIVE, IN-PLACE CHANGES JUST BY PASSING IT * > IN TO THIS UTILITY. * * @param {String} supposedAttrName * The "supposed attribute name"; i.e. the LHS the provided value came from (e.g. "id" or "favoriteBrands") * > Useful for looking up the appropriate attribute definition. * * @param {String} modelIdentity * The identity of the model this value 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 {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 {Ref} * The successfully-normalized value, ready for use within the `valuesToSet` or `newRecord` * query key of a stage 2 query. (May or may not be the original reference.) * * -- * * @throws {Error} If the value should be ignored/stripped (e.g. because it is `undefined`, or because it * does not correspond with a recognized attribute, and the model def has `schema: true`) * @property {String} code * - E_SHOULD_BE_IGNORED * * * @throws {Error} If it encounters incompatible usage in the provided `value`, * 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 `value` has an incompatible data type. * | @property {String} code * | - E_TYPE * | @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 the provided `value` fails the requiredness guarantee of the corresponding attribute. * | @property {String} code * | - E_REQUIRED * * * @throws {Error} If the provided `value` violates one or more of the high-level validation rules * | configured for the corresponding attribute. * | @property {String} code * | - E_VIOLATES_RULES * | @property {Array} ruleViolations * | e.g. * | ``` * | [ * | { * | rule: 'minLength', //(isEmail/isNotEmptyString/max/isNumber/etc) * | message: 'Too few characters (max 30)' * | } * | ] * | ``` * * @throws {Error} If anything else unexpected occurs. */ module.exports = function normalizeValueToSet(value, supposedAttrName, modelIdentity, orm, meta) { // ================================================================================================ assert(_.isString(supposedAttrName), '`supposedAttrName` must be a string.'); // (`modelIdentity` and `orm` will be automatically checked by calling `getModel()` below) // > Note that this attr name MIGHT be empty string -- although it should never be. // > (we check that below) // ================================================================================================ // ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗ ███╗ ███╗ ██████╗ ██████╗ ███████╗██╗ // ██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝ ████╗ ████║██╔═══██╗██╔══██╗██╔════╝██║ // ██║ ███████║█████╗ ██║ █████╔╝ ██╔████╔██║██║ ██║██║ ██║█████╗ ██║ // ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ██║╚██╔╝██║██║ ██║██║ ██║██╔══╝ ██║ // ╚██████╗██║ ██║███████╗╚██████╗██║ ██╗ ██║ ╚═╝ ██║╚██████╔╝██████╔╝███████╗███████╗ // ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ // // █████╗ ███╗ ██╗██████╗ █████╗ ████████╗████████╗██████╗ // ██╔══██╗████╗ ██║██╔══██╗ ██╔══██╗╚══██╔══╝╚══██╔══╝██╔══██╗ // ███████║██╔██╗ ██║██║ ██║ ███████║ ██║ ██║ ██████╔╝ // ██╔══██║██║╚██╗██║██║ ██║ ██╔══██║ ██║ ██║ ██╔══██╗ // ██║ ██║██║ ╚████║██████╔╝ ██║ ██║ ██║ ██║ ██║ ██║ // ╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ // // Look up the Waterline model. // > 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> // This local variable is used to hold a reference to the attribute def // that corresponds with this value (if there is one). var correspondingAttrDef; try { correspondingAttrDef = getAttribute(supposedAttrName, modelIdentity, orm); } catch (e) { switch (e.code) { case 'E_ATTR_NOT_REGISTERED': // If no matching attr def exists, then just leave `correspondingAttrDef` // undefined and continue... for now anyway. break; default: throw e; } }//</catch> // ┌─┐┬ ┬┌─┐┌─┐┬┌─ ┌─┐┌┬┐┌┬┐┬─┐┬┌┐ ┬ ┬┌┬┐┌─┐ ┌┐┌┌─┐┌┬┐┌─┐ // │ ├─┤├┤ │ ├┴┐ ├─┤ │ │ ├┬┘│├┴┐│ │ │ ├┤ │││├─┤│││├┤ // └─┘┴ ┴└─┘└─┘┴ ┴ ┴ ┴ ┴ ┴ ┴└─┴└─┘└─┘ ┴ └─┘ ┘└┘┴ ┴┴ ┴└─┘ // If this model declares `schema: true`... if (WLModel.hasSchema === true) { // Check that this key corresponded with a recognized attribute definition. // // > If no such attribute exists, then fail gracefully by bailing early, indicating // > that this value should be ignored (For example, this might cause this value to // > be stripped out of the `newRecord` or `valuesToSet` query keys.) if (!correspondingAttrDef) { throw flaverr('E_SHOULD_BE_IGNORED', new Error( 'This model declares itself `schema: true`, but this value does not match '+ 'any recognized attribute (thus it will be ignored).' )); }//-• }//</else if `hasSchema === true` > // ‡ // Else if this model declares `schema: false`... else if (WLModel.hasSchema === false) { // Check that this key is a valid Waterline attribute name, at least. if (!isValidAttributeName(supposedAttrName)) { if (supposedAttrName === '') { throw flaverr('E_HIGHLY_IRREGULAR', new Error('Empty string (\'\') is not a valid name for an attribute.')); } else { throw flaverr('E_HIGHLY_IRREGULAR', new Error('This is not a valid name for an attribute.')); } }//-• } // ‡ else { throw new Error( 'Consistency violation: Every live Waterline model should always have the `hasSchema` flag '+ 'as either `true` or `false` (should have been automatically derived from the `schema` model setting '+ 'shortly after construction. And `schema` should have been verified as existing by waterline-schema). '+ 'But somehow, this model\'s (`'+modelIdentity+'`) `hasSchema` property is as follows: '+ util.inspect(WLModel.hasSchema, {depth:5})+'' ); }//</ else > // ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗ ██╗ ██╗ █████╗ ██╗ ██╗ ██╗███████╗ // ██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝ ██║ ██║██╔══██╗██║ ██║ ██║██╔════╝ // ██║ ███████║█████╗ ██║ █████╔╝ ██║ ██║███████║██║ ██║ ██║█████╗ // ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ╚██╗ ██╔╝██╔══██║██║ ██║ ██║██╔══╝ // ╚██████╗██║ ██║███████╗╚██████╗██║ ██╗ ╚████╔╝ ██║ ██║███████╗╚██████╔╝███████╗ // ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚══════╝ // // Validate+lightly coerce this value, both as schema-agnostic data, // and vs. the corresponding attribute definition's declared `type`, // `model`, or `collection`. // Declare var to flag whether or not an attribute should have validation rules applied. // This will typically be the case for primary keys and generic attributes under certain conditions. var doCheckForRuleViolations = false; // If this value is `undefined`, then bail early, indicating that it should be ignored. if (_.isUndefined(value)) { throw flaverr('E_SHOULD_BE_IGNORED', new Error( 'This value is `undefined`. Remember: in Sails/Waterline, we always treat keys with '+ '`undefined` values as if they were never there in the first place.' )); }//-• // ┌─┐┌─┐┌─┐┌─┐┬┌─┐┬┌─┐┌┬┐ ┬ ┬┌─┐┬ ┬ ┬┌─┐ ┬┌─┐ ┌─┐┌─┐┬─┐ ┌─┐┌┐┌ // └─┐├─┘├┤ │ │├┤ │├┤ ││ └┐┌┘├─┤│ │ │├┤ │└─┐ ├┤ │ │├┬┘ ├─┤│││ // └─┘┴ └─┘└─┘┴└ ┴└─┘─┴┘ └┘ ┴ ┴┴─┘└─┘└─┘ ┴└─┘ └ └─┘┴└─ ┴ ┴┘└┘ // ╦ ╦╔╗╔╦═╗╔═╗╔═╗╔═╗╔═╗╔╗╔╦╔═╗╔═╗╔╦╗ ┌─┐┌┬┐┌┬┐┬─┐┬┌┐ ┬ ┬┌┬┐┌─┐ // ║ ║║║║╠╦╝║╣ ║ ║ ║║ ╦║║║║╔═╝║╣ ║║ ├─┤ │ │ ├┬┘│├┴┐│ │ │ ├┤ // ╚═╝╝╚╝╩╚═╚═╝╚═╝╚═╝╚═╝╝╚╝╩╚═╝╚═╝═╩╝ ┴ ┴ ┴ ┴ ┴└─┴└─┘└─┘ ┴ └─┘ // // If this value doesn't actually match an attribute definition... if (!correspondingAttrDef) { // IWMIH then we already know this model has `schema: false`. // So if this value doesn't match a recognized attribute def, // then we'll validate it as `type: json`. // // > This is because we don't want to send a potentially-circular/crazy // > value down to the adapter unless it corresponds w/ a `type: 'ref'` attribute. try { value = rttc.validate('json', value); } catch (e) { switch (e.code) { case 'E_INVALID': throw flaverr({ code: 'E_TYPE', expectedType: 'json' }, new Error( 'Invalid value for unrecognized attribute (must be JSON-compatible). To explicitly allow '+ 'non-JSON-compatible values like this, define a `'+supposedAttrName+'` attribute, and specify '+ '`type: ref`. More info on this error: '+e.message )); default: throw e; } } }//‡ // ┌─┐┌─┐┬─┐ ╔═╗╦═╗╦╔╦╗╔═╗╦═╗╦ ╦ ╦╔═╔═╗╦ ╦ ╔═╗╔╦╗╔╦╗╦═╗╦╔╗ ╦ ╦╔╦╗╔═╗ // ├┤ │ │├┬┘ ╠═╝╠╦╝║║║║╠═╣╠╦╝╚╦╝ ╠╩╗║╣ ╚╦╝ ╠═╣ ║ ║ ╠╦╝║╠╩╗║ ║ ║ ║╣ // └ └─┘┴└─ ╩ ╩╚═╩╩ ╩╩ ╩╩╚═ ╩ ╩ ╩╚═╝ ╩ ╩ ╩ ╩ ╩ ╩╚═╩╚═╝╚═╝ ╩ ╚═╝ else if (WLModel.primaryKey === supposedAttrName) { // Primary key attributes should have validation rules applied if they have any. if (!_.isUndefined(correspondingAttrDef.validations)) { doCheckForRuleViolations = true; } try { value = normalizePkValue(value, correspondingAttrDef.type); } catch (e) { switch (e.code) { case 'E_INVALID_PK_VALUE': throw flaverr('E_HIGHLY_IRREGULAR', new Error( 'Invalid primary key value. '+e.message )); default: throw e; } } }//‡ // ┌─┐┌─┐┬─┐ ╔═╗╦ ╦ ╦╦═╗╔═╗╦ ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔ // ├┤ │ │├┬┘ ╠═╝║ ║ ║╠╦╝╠═╣║ ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║ // └ └─┘┴└─ ╩ ╩═╝╚═╝╩╚═╩ ╩╩═╝ ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝ else if (correspondingAttrDef.collection) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // NOTE: For a brief period of time in the past, it was not permitted to call .update() or .validate() // using an array of ids for a collection. But prior to the stable release of Waterline v0.13, this // decision was reversed. The following commented-out code is left in Waterline to track what this // was about, for posterity: // ``` // // If properties are not allowed for plural ("collection") associations, // // then throw an error. // if (!allowCollectionAttrs) { // throw flaverr('E_HIGHLY_IRREGULAR', new Error( // 'As a precaution, prevented replacing entire plural ("collection") association (`'+supposedAttrName+'`). '+ // 'To do this, use `replaceCollection(...,\''+supposedAttrName+'\').members('+util.inspect(value, {depth:5})+')` '+ // 'instead.' // )); // }//-• // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Ensure that this is an array, and that each item in the array matches // the expected data type for a pk value of the associated model. try { value = normalizePkValueOrValues(value, getAttribute(getModel(correspondingAttrDef.collection, orm).primaryKey, correspondingAttrDef.collection, orm).type); } catch (e) { switch (e.code) { case 'E_INVALID_PK_VALUE': throw flaverr('E_HIGHLY_IRREGULAR', new Error( 'If specified, expected `'+supposedAttrName+'` to be an array of ids '+ '(representing the records to associate). But instead, got: '+ util.inspect(value, {depth:5})+'' // 'If specifying the value for a plural (`collection`) association, you must do so by '+ // 'providing an array of associated ids representing the associated records. But instead, '+ // 'for `'+supposedAttrName+'`, got: '+util.inspect(value, {depth:5})+'' )); default: throw e; } } }//‡ // ┌─┐┌─┐┬─┐ ╔═╗╦╔╗╔╔═╗╦ ╦╦ ╔═╗╦═╗ ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔ // ├┤ │ │├┬┘ ╚═╗║║║║║ ╦║ ║║ ╠═╣╠╦╝ ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║ // └ └─┘┴└─ ╚═╝╩╝╚╝╚═╝╚═╝╩═╝╩ ╩╩╚═ ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝ else if (correspondingAttrDef.model) { // If `null` was specified, then it _might_ be OK. if (_.isNull(value)) { // We allow `null` for singular associations UNLESS they are required. if (correspondingAttrDef.required) { throw flaverr('E_REQUIRED', new Error( 'Cannot set `null` for required association (`'+supposedAttrName+'`).' )); }//-• }//‡ // Otherwise, this value is NOT null. // So ensure that it matches the expected data type for a pk value // of the associated model (normalizing it, if appropriate/possible.) else { try { value = normalizePkValue(value, getAttribute(getModel(correspondingAttrDef.model, orm).primaryKey, correspondingAttrDef.model, orm).type); } catch (e) { switch (e.code) { case 'E_INVALID_PK_VALUE': throw flaverr('E_HIGHLY_IRREGULAR', new Error( 'Expecting an id representing the associated record, or `null` to indicate '+ 'there will be no associated record. But the specified value is not a valid '+ '`'+supposedAttrName+'`. '+e.message )); default: throw e; } }//</catch> }//</else (not null)> }//‡ // ┌─┐┌─┐┬─┐ ╔╦╗╦╔═╗╔═╗╔═╗╦ ╦ ╔═╗╔╗╔╔═╗╔═╗╦ ╦╔═╗ ╔═╗╔╦╗╔╦╗╦═╗╦╔╗ ╦ ╦╔╦╗╔═╗ // ├┤ │ │├┬┘ ║║║║╚═╗║ ║╣ ║ ║ ╠═╣║║║║╣ ║ ║║ ║╚═╗ ╠═╣ ║ ║ ╠╦╝║╠╩╗║ ║ ║ ║╣ // └ └─┘┴└─ ╩ ╩╩╚═╝╚═╝╚═╝╩═╝╩═╝╩ ╩╝╚╝╚═╝╚═╝╚═╝╚═╝ ╩ ╩ ╩ ╩ ╩╚═╩╚═╝╚═╝ ╩ ╚═╝ // Otherwise, the corresponding attr def is just a normal attr--not an association or primary key. // > We'll use loose validation (& thus also light coercion) on the value and see what happens. else { if (!_.isString(correspondingAttrDef.type) || correspondingAttrDef.type === '') { throw new Error('Consistency violation: There is no way this attribute (`'+supposedAttrName+'`) should have been allowed to be registered with neither a `type`, `model`, nor `collection`! Here is the attr def: '+util.inspect(correspondingAttrDef, {depth:5})+''); } // First, check if this is an auto-*-at timestamp, and if it is... if (correspondingAttrDef.autoCreatedAt || correspondingAttrDef.autoUpdatedAt) { // Ensure we are not trying to set it to empty string // (this would never make sense.) if (value === '') { throw flaverr('E_HIGHLY_IRREGULAR', new Error( 'If specified, should be a valid '+ ( correspondingAttrDef.type === 'number' ? 'JS timestamp (unix epoch ms)' : 'JSON timestamp (ISO 8601)' )+'. '+ 'But instead, it was empty string ("").' )); }//-• // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: If there is significant confusion being caused by allowing `autoUpdatedAt` // attrs to be set explicitly on .create() and .update() , then we should reevaluate // adding in the following code: // ``` // // And log a warning about how this auto-* timestamp is being set explicitly, // // whereas the generally expected behavior is to let it be set automatically. // var autoTSDisplayName; // if (correspondingAttrDef.autoCreatedAt) { // autoTSDisplayName = 'autoCreatedAt'; // } // else { // autoTSDisplayName = 'autoUpdatedAt'; // } // // console.warn('\n'+ // 'Warning: Explicitly overriding `'+supposedAttrName+'`...\n'+ // '(This attribute of the `'+modelIdentity+'` model is defined as '+ // '`'+autoTSDisplayName+': true`, meaning it is intended to be set '+ // 'automatically, except in special cases when debugging or migrating data.)\n' // ); // ``` // // But for now, leaving it (^^) out. // // > See https://github.com/balderdashy/waterline/pull/1440#issuecomment-275943205 // > for more information. Note that we'd need an extra meta key because of // > auto-migrations and other higher level tooling built on Waterline. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - }//>-• // Handle a special case where we want a more specific error: // // > Note: This is just like normal RTTC validation ("loose" mode), with one major exception: // > We handle `null` as a special case, regardless of the type being validated against; // > whether or not this attribute is `required: true`. That's because it's so easy to // > get confused about how `required` works in a given database vs. Waterline vs. JavaScript. // > (Especially when it comes to null vs. undefined vs. empty string, etc) // > // > In RTTC, `null` is only valid vs. `json` and `ref` types, for singular associations, // > and for completely unrecognized attributes -- and that's still true here. // > But most schemaful databases also support a configuration where `null` is ALSO allowed // > as an implicit base value for any type of data. This sorta serves the same purpose as // > `undefined`, or omission, in JavaScript or MongoDB. BUT that doesn't mean we necessarily // > allow `null` -- consistency of type safety rules is too important -- it just means that // > we give it its own special error message. // > // > BUT NOTE: if `allowNull` is enabled, we DO allow null. // > // > Review the "required"-ness checks in the `normalize-new-record.js` utility for examples // > of related behavior, and see the more detailed spec for more information: // > https://docs.google.com/spreadsheets/d/1whV739iW6O9SxRZLCIe2lpvuAUqm-ie7j7tn_Pjir3s/edit#gid=1814738146 var isProvidingNullForIncompatibleOptionalAttr = ( _.isNull(value) && correspondingAttrDef.type !== 'json' && correspondingAttrDef.type !== 'ref' && !correspondingAttrDef.allowNull && !correspondingAttrDef.required ); if (isProvidingNullForIncompatibleOptionalAttr) { throw flaverr({ code: 'E_TYPE', expectedType: correspondingAttrDef.type }, new Error( 'Specified value (`null`) is not a valid `'+supposedAttrName+'`. '+ 'Even though this attribute is optional, it still does not allow `null` to '+ 'be explicitly set, because `null` is not valid vs. the expected '+ 'type: \''+correspondingAttrDef.type+'\'. Instead, to indicate "voidness", '+ 'please set the value for this attribute to the base value for its type, '+ (function _getBaseValuePhrase(){ switch(correspondingAttrDef.type) { case 'string': return '`\'\'` (empty string)'; case 'number': return '`0` (zero)'; default: return '`'+rttc.coerce(correspondingAttrDef.type)+'`'; } })()+'. Or, if you specifically need to save `null`, then change this '+ 'attribute to either `type: \'json\'` or `type: \'ref\'`. '+ (function _getExtraPhrase(){ if (_.isUndefined(correspondingAttrDef.defaultsTo)) { return 'Also note: Since this attribute does not define a `defaultsTo`, '+ 'the base value will be used as an implicit default if `'+supposedAttrName+'` '+ 'is omitted when creating a record.'; } else { return ''; } })() )); }//-• // ┌─┐┬ ┬┌─┐┬─┐┌─┐┌┐┌┌┬┐┌─┐┌─┐ ╔╦╗╦ ╦╔═╗╔═╗ ╔═╗╔═╗╔═╗╔═╗╔╦╗╦ ╦ // │ ┬│ │├─┤├┬┘├─┤│││ │ ├┤ ├┤ ║ ╚╦╝╠═╝║╣ ╚═╗╠═╣╠╣ ║╣ ║ ╚╦╝ // └─┘└─┘┴ ┴┴└─┴ ┴┘└┘ ┴ └─┘└─┘ ╩ ╩ ╩ ╚═╝ ╚═╝╩ ╩╚ ╚═╝ ╩ ╩ // If the value is `null` and the attribute has allowNull set to true it's ok. if (correspondingAttrDef.allowNull && _.isNull(value)) { // Nothing else to validate here. } //‡ // Otherwise, verify that this value matches the expected type, and potentially // perform loose coercion on it at the same time. This throws an E_INVALID error // if validation fails. else { try { value = rttc.validate(correspondingAttrDef.type, value); } catch (e) { switch (e.code) { case 'E_INVALID': throw flaverr({ code: 'E_TYPE', expectedType: correspondingAttrDef.type }, new Error( 'Specified value is not a valid `'+supposedAttrName+'`. '+e.message )); default: throw e; } } } // ┬ ┬┌─┐┌┐┌┌┬┐┬ ┌─┐ ┌─┐┌─┐┌─┐┌─┐┬┌─┐┬ ┌─┐┌─┐┌─┐┌─┐┌─┐ // ├─┤├─┤│││ │││ ├┤ └─┐├─┘├┤ │ │├─┤│ │ ├─┤└─┐├┤ └─┐ // ┴ ┴┴ ┴┘└┘─┴┘┴─┘└─┘ └─┘┴ └─┘└─┘┴┴ ┴┴─┘ └─┘┴ ┴└─┘└─┘└─┘ // ┌─ ┌─┐┌─┐┬─┐ ╦═╗╔═╗╔═╗ ╦ ╦╦╦═╗╔═╗╔╦╗ ─┐ // │─── ├┤ │ │├┬┘ ╠╦╝║╣ ║═╬╗║ ║║╠╦╝║╣ ║║ ───│ // └─ └ └─┘┴└─ ╩╚═╚═╝╚═╝╚╚═╝╩╩╚═╚═╝═╩╝ ─┘ if (correspondingAttrDef.required) { // "" (empty string) is never allowed as a value for a required attribute. if (value === '') { throw flaverr('E_REQUIRED', new Error( 'Cannot set "" (empty string) for a required attribute.' )); }//>-• // `null` is never allowed as a value for a required attribute. if (_.isNull(value)) { throw flaverr('E_REQUIRED', new Error( 'Cannot set `null` for a required attribute.' )); }//-• }//>- </ if required > // Decide whether validation rules should be checked for this attribute. // // > • High-level validation rules are ALWAYS skipped for `null`. // > • If there is no `validations` attribute key, then there's nothing for us to check. doCheckForRuleViolations = !_.isNull(value) && !_.isUndefined(correspondingAttrDef.validations); }//</else (i.e. corresponding attr def is just a normal attr--not an association or primary key)> // ┌─┐┬ ┬┌─┐┌─┐┬┌─ ┌─┐┌─┐┬─┐ ╦═╗╦ ╦╦ ╔═╗ ╦ ╦╦╔═╗╦ ╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ // │ ├─┤├┤ │ ├┴┐ ├┤ │ │├┬┘ ╠╦╝║ ║║ ║╣ ╚╗╔╝║║ ║║ ╠═╣ ║ ║║ ║║║║╚═╗ // └─┘┴ ┴└─┘└─┘┴ ┴ └ └─┘┴└─ ╩╚═╚═╝╩═╝╚═╝ ╚╝ ╩╚═╝╩═╝╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝ // If appropriate, strictly enforce our (potentially-mildly-coerced) value // vs. the validation ruleset defined on the corresponding attribute. // Then, if there are any rule violations, stick them in an Error and throw it. if (doCheckForRuleViolations) { var ruleset = correspondingAttrDef.validations; var isRulesetDictionary = _.isObject(ruleset) && !_.isArray(ruleset) && !_.isFunction(ruleset); if (!isRulesetDictionary) { throw new Error('Consistency violation: If set, an attribute\'s validations ruleset (`validations`) should always be a dictionary (plain JavaScript object). But for the `'+modelIdentity+'` model\'s `'+supposedAttrName+'` attribute, it somehow ended up as this instead: '+util.inspect(correspondingAttrDef.validations,{depth:5})+''); } var ruleViolations; try { ruleViolations = anchor(value, ruleset); // e.g. // [ { rule: 'isEmail', message: 'Value was not a valid email address.' }, ... ] } catch (e) { throw new Error( 'Consistency violation: Unexpected error occurred when attempting to apply '+ 'high-level validation rules from `'+modelIdentity+'` model\'s `'+supposedAttrName+'` '+ 'attribute. '+e.stack ); }//</ catch > if (ruleViolations.length > 0) { // Format rolled-up summary for use in our error message. // e.g. // ``` // • Value was not in the configured whitelist (delinquent, new, paid) // • Value was an empty string. // ``` var summary = _.reduce(ruleViolations, function (memo, violation){ memo += ' • '+violation.message+'\n'; return memo; }, ''); throw flaverr({ code: 'E_VIOLATES_RULES', ruleViolations: ruleViolations }, new Error( 'Violated one or more validation rules:\n'+ summary )); }//-• }//>-• </if (doCheckForRuleViolations) > // ███████╗███╗ ██╗ ██████╗██████╗ ██╗ ██╗██████╗ ████████╗ ██████╗ █████╗ ████████╗ █████╗ // ██╔════╝████╗ ██║██╔════╝██╔══██╗╚██╗ ██╔╝██╔══██╗╚══██╔══╝ ██╔══██╗██╔══██╗╚══██╔══╝██╔══██╗ // █████╗ ██╔██╗ ██║██║ ██████╔╝ ╚████╔╝ ██████╔╝ ██║ ██║ ██║███████║ ██║ ███████║ // ██╔══╝ ██║╚██╗██║██║ ██╔══██╗ ╚██╔╝ ██╔═══╝ ██║ ██║ ██║██╔══██║ ██║ ██╔══██║ // ███████╗██║ ╚████║╚██████╗██║ ██║ ██║ ██║ ██║ ██████╔╝██║ ██║ ██║ ██║ ██║ // ╚══════╝╚═╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ // ╦╔═╗ ┬─┐┌─┐┬ ┌─┐┬ ┬┌─┐┌┐┌┌┬┐ // ║╠╣ ├┬┘├┤ │ ├┤ └┐┌┘├─┤│││ │ // ╩╚ ┴└─└─┘┴─┘└─┘ └┘ ┴ ┴┘└┘ ┴ooo if (correspondingAttrDef && correspondingAttrDef.encrypt) { if (correspondingAttrDef.encrypt !== true) { throw new Error( 'Consistency violation: `'+modelIdentity+'` model\'s `'+supposedAttrName+'` attribute '+ 'has a corrupted definition. Should not have been allowed to set `encrypt` to anything '+ 'other than `true` or `false`.' ); }//• if (correspondingAttrDef.type === 'ref') { throw new Error( 'Consistency violation: `'+modelIdentity+'` model\'s `'+supposedAttrName+'` attribute '+ 'has a corrupted definition. Should not have been allowed to be both `type: \'ref\' '+ 'AND `encrypt: true`.' ); }//• if (!_.isObject(WLModel.dataEncryptionKeys) || !WLModel.dataEncryptionKeys.default || !_.isString(WLModel.dataEncryptionKeys.default)) { throw new Error( 'Consistency violation: `'+modelIdentity+'` model has a corrupted definition. Should not '+ 'have been allowed to declare an attribute with `encrypt: true` without also specifying '+ 'the `dataEncryptionKeys` model setting as a valid dictionary (including a valid "default" '+ 'key).' ); }//• // Figure out what DEK to encrypt with. var idOfDekToEncryptWith; if (meta && meta.encryptWith) { idOfDekToEncryptWith = meta.encryptWith; } else { idOfDekToEncryptWith = 'default'; } if (!WLModel.dataEncryptionKeys[idOfDekToEncryptWith]) { throw new Error( 'There is no known data encryption key by that name (`'+idOfDekToEncryptWith+'`). '+ 'Please make sure a valid DEK (data encryption key) is configured under `dataEncryptionKeys`.' ); }//• try { // Never encrypt `''`(empty string), `0` (zero), `false`, or `null`, since these are possible // base values. (Note that the current code path only runs when a value is explicitly provided // for the attribute-- not when it is omitted. Thus these base values can get into the database // without being encrypted _anyway_.) if (value === '' || value === 0 || value === false || _.isNull(value)) { // Don't encrypt. } // Never encrypt if the (private/experimental) `skipEncryption` meta key is // set truthy. PLEASE DO NOT RELY ON THIS IN YOUR OWN CODE- IT COULD CHANGE // AT ANY TIME AND BREAK YOUR APP OR PLUGIN! // > (Useful for internal method calls-- e.g. the internal "create()" that // > Waterline uses to implement `findOrCreate()`. For more info on that, // > see https://github.com/balderdashy/sails/issues/4302#issuecomment-363883885) else if (meta && meta.skipEncryption) { // Don't encrypt. } else { // First, JSON-encode value, to allow for differentiating between strings/numbers/booleans/null. var jsonEncoded; try { jsonEncoded = JSON.stringify(value); } catch (err) { // Note: Stringification SHOULD always work, because we just checked all that out above. // But just in case it doesn't, or if this code gets moved elsewhere in the future, here // we include a reasonable error here as a backup. throw flaverr({ message: 'Before encrypting, Waterline attempted to JSON-stringify this value to ensure it '+ 'could be accurately decoded into the correct data type later (for example, `2` vs `\'2\'`). '+ 'But this time, JSON.stringify() failed with the following error: '+err.message }, err); } // Encrypt using the appropriate key from the configured DEKs. // console.log('•••••encrypting JSON-encoded value: `'+util.inspect(jsonEncoded, {depth:null})+'`'); // Require this down here for Node version compat. var EA = require('encrypted-attr'); value = EA([supposedAttrName], { keys: WLModel.dataEncryptionKeys, keyId: idOfDekToEncryptWith }) .encryptAttribute(undefined, jsonEncoded); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Alternative: (hack for testing) // ``` // if (value.match(/^ENCRYPTED:/)){ throw new Error('Unexpected behavior: Can\'t encrypt something already encrypted!!!'); } // value = 'ENCRYPTED:'+jsonEncoded; // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - }//fi } catch (err) { // console.log('•••••was attempting to encrypt this value: `'+util.inspect(value, {depth:null})+'`'); throw flaverr({ message: 'Encryption failed for `'+supposedAttrName+'`\n'+ 'Details:\n'+ ' '+err.message }, _.isError(err) ? err : new Error()); } }//fi // Return the normalized (and potentially encrypted) value. return value; };