water-orm
Version:
A monolith version of Standalone waterline ORM
292 lines (249 loc) • 7.85 kB
JavaScript
/**
* Handles validation on a model
*
* Uses Anchor for validating
* https://github.com/balderdashy/anchor
*/
var _ = require('lodash');
var anchor = require('../../anchor');
var async = require('async');
var utils = require('../utils/helpers');
var hasOwnProperty = utils.object.hasOwnProperty;
var WLValidationError = require('../error/WLValidationError');
/**
* Build up validations using the Anchor module.
*
* @param {String} adapter
*/
var Validator = module.exports = function(adapter) {
this.validations = {};
};
/**
* Builds a Validation Object from a normalized attributes
* object.
*
* Loops through an attributes object to build a validation object
* containing attribute name as key and a series of validations that
* are run on each model. Skips over type and defaultsTo as they are
* schema properties.
*
* Example:
*
* attributes: {
* name: {
* type: 'string',
* length: { min: 2, max: 5 }
* }
* email: {
* type: 'string',
* required: true
* }
* }
*
* Returns: {
* name: { length: { min:2, max: 5 }},
* email: { required: true }
* }
*/
Validator.prototype.initialize = function(attrs, types, defaults) {
var self = this;
defaults = defaults || {};
// These properties are reserved and may not be used as validations
this.reservedProperties = [
'defaultsTo',
'primaryKey',
'autoIncrement',
'unique',
'index',
'collection',
'dominant',
'through',
'columnName',
'foreignKey',
'references',
'on',
'groupKey',
'model',
'via',
'size',
'example',
'validationMessage',
'validations',
'populateSettings',
'onKey',
'protected',
'meta'
];
if (defaults.ignoreProperties && Array.isArray(defaults.ignoreProperties)) {
this.reservedProperties = this.reservedProperties.concat(defaults.ignoreProperties);
}
// Add custom type definitions to anchor
types = types || {};
anchor.define(types);
Object.keys(attrs).forEach(function(attr) {
self.validations[attr] = {};
Object.keys(attrs[attr]).forEach(function(prop) {
// Ignore null values
if (attrs[attr][prop] === null) { return; }
// If property is reserved don't do anything with it
if (self.reservedProperties.indexOf(prop) > -1) { return; }
// use the Anchor `in` method for enums
if (prop === 'enum') {
self.validations[attr]['in'] = attrs[attr][prop];
return;
}
self.validations[attr][prop] = attrs[attr][prop];
});
});
};
/**
* Validator.prototype.validate()
*
* Accepts a dictionary of values and validates them against
* the validation rules expected by this schema (`this.validations`).
* Validation is performed using Anchor.
*
*
* @param {Dictionary} values
* The dictionary of values to validate.
*
* @param {Boolean|String|String[]} presentOnly
* only validate present values (if `true`) or validate the
* specified attribute(s).
*
* @param {Function} callback
* @param {Error} err - a fatal error, if relevant.
* @param {Array} invalidAttributes - an array of errors
*/
Validator.prototype.validate = function(values, presentOnly, cb) {
var self = this;
var errors = {};
var validations = Object.keys(this.validations);
// Handle optional second arg AND Use present values only, specified values, or all validations
/* eslint-disable no-fallthrough */
switch (typeof presentOnly) {
case 'function':
cb = presentOnly;
break;
case 'string':
validations = [presentOnly];
break;
case 'object':
if (Array.isArray(presentOnly)) {
validations = presentOnly;
break;
} // Fall through to the default if the object is not an array
default:
// Any other truthy value.
if (presentOnly) {
validations = _.intersection(validations, Object.keys(values));
}
/* eslint-enable no-fallthrough */
}
// Validate all validations in parallel
async.each(validations, function _eachValidation(validation, cb) {
var curValidation = self.validations[validation];
// Build Requirements
var requirements;
try {
requirements = anchor(curValidation);
}
catch (e) {
// Handle fatal error:
return cb(e);
}
requirements = _.cloneDeep(requirements);
// Grab value and set to null if undefined
var value = values[validation];
if (typeof value == 'undefined') {
value = null;
}
// If value is not required and empty then don't
// try and validate it
if (!curValidation.required) {
if (value === null || value === '') {
return cb();
}
}
// If Boolean and required manually check
if (curValidation.required && curValidation.type === 'boolean' && (typeof value !== 'undefined' && value !== null)) {
if (value.toString() === 'true' || value.toString() === 'false') {
return cb();
}
}
// If type is integer and the value matches a mongoID let it validate
if (hasOwnProperty(self.validations[validation], 'type') && self.validations[validation].type === 'integer') {
if (utils.matchMongoId(value)) {
return cb();
}
}
// Rule values may be specified as sync or async functions.
// Call them and replace the rule value with the function's result
// before running validations.
async.each(Object.keys(requirements.data), function _eachKey(key, next) {
try {
if (typeof requirements.data[key] !== 'function') {
return next();
}
// Run synchronous function
if (requirements.data[key].length < 1) {
requirements.data[key] = requirements.data[key].apply(values, []);
return next();
}
// Run async function
requirements.data[key].call(values, function(result) {
requirements.data[key] = result;
next();
});
}
catch (e) {
return next(e);
}
}, function afterwards(unexpectedErr) {
if (unexpectedErr) {
// Handle fatal error
return cb(unexpectedErr);
}
// If the value has a dynamic required function and it evaluates to false lets look and see
// if the value supplied is null or undefined. If so then we don't need to check anything. This
// prevents type errors like `undefined` should be a string.
// if required is set to 'false', don't enforce as required rule
if (requirements.data.hasOwnProperty('required') && !requirements.data.required) {
if (_.isNull(value)) {
return cb();
}
}
// Now run the validations using Anchor.
var validationError;
try {
validationError = anchor(value).to(requirements.data, values);
}
catch (e) {
// Handle fatal error:
return cb(e);
}
// If no validation errors, bail.
if (!validationError) {
return cb();
}
// Build an array of errors.
errors[validation] = [];
validationError.forEach(function(obj) {
if (obj.property) {
delete obj.property;
}
errors[validation].push({ rule: obj.rule, message: obj.message });
});
return cb();
});
}, function allValidationsChecked(err) {
// Handle fatal error:
if (err) {
return cb(err);
}
if (Object.keys(errors).length === 0) {
return cb();
}
return cb(undefined, errors);
});
};