lib/waterline/utils/query/private/normalize-value-to-set.js
/**
* 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;
};