不要怂,就是干,撸起袖子干!

Major refactoring of dao-validator, faster, leaner, meaner

1 parent 04b237e7
Showing with 169 additions and 127 deletions
var Validator = require("validator") var Validator = require("validator")
, Utils = require("./utils") , Utils = require("./utils")
, sequelizeError = require("./errors") , sequelizeError = require("./errors")
, Promise = require("bluebird")
// Backwards compat for people using old validation function // Backwards compat for people using old validation function
// We cannot use .extend, since it coerces the first arg to string // We cannot use .extend, since it coerces the first arg to string
...@@ -76,7 +77,13 @@ Validator.extend('is', function(str, pattern, modifiers) { ...@@ -76,7 +77,13 @@ Validator.extend('is', function(str, pattern, modifiers) {
return this.regex(str, pattern, modifiers); return this.regex(str, pattern, modifiers);
}) })
/**
* The Main DAO Validator.
*
* @param {sequelize.Model} modelInstance The model instance.
* @param {Object} options A dict with options.
* @constructor
*/
var DaoValidator = module.exports = function(modelInstance, options) { var DaoValidator = module.exports = function(modelInstance, options) {
options = options || {} options = options || {}
options.skip = options.skip || [] options.skip = options.skip || []
...@@ -90,30 +97,47 @@ var DaoValidator = module.exports = function(modelInstance, options) { ...@@ -90,30 +97,47 @@ var DaoValidator = module.exports = function(modelInstance, options) {
* @name Validator * @name Validator
*/ */
this.Validator = Validator this.Validator = Validator
/**
* All errors will be stored here from the validations.
*
* @type {Object} Will contain keys that correspond to attributes which will
* be Arrays of Errors.
*/
this.errors = {};
/** @type {boolean} Indicates if validations are in progress */
this.inProgress = false;
} }
/** @define {string} The error key for arguments as passed by custom validators */ /** @define {string} The error key for arguments as passed by custom validators */
DaoValidator.RAW_KEY_NAME = '__raw' DaoValidator.RAW_KEY_NAME = '__raw'
/**
* The main entry point for the Validation module, invoke to start the dance.
*
* @return {sequelize.Utils.CustomEventEmitter} That thing...
*/
DaoValidator.prototype.validate = function() { DaoValidator.prototype.validate = function() {
var self = this if (this.inProgress) {
throw new Error('Validations already in progress.');
}
this.inProgress = true;
this.errors = [];
var self = this
return new Utils.CustomEventEmitter(function(emitter) { return new Utils.CustomEventEmitter(function(emitter) {
validateAttributes.call(self) Promise.settle([
validateModel.call(self) self._validateAttributes(),
self._validateSchema(),
self ]).then(function () {
.chainer emitter.emit('success')
.run() }).catch(function(err) {
.success(function () {
emitter.emit('success')
})
.error(function(err) {
var error = new sequelizeError.ValidationError('Validation error') var error = new sequelizeError.ValidationError('Validation error')
error[DaoValidator.RAW_KEY_NAME] = [] error[DaoValidator.RAW_KEY_NAME] = []
Utils._.each(err, function (value) { Utils._.each(err, function (value) {
error[DaoValidator.RAW_KEY_NAME].push(value[DaoValidator.RAW_KEY_NAME]) error[DaoValidator.RAW_KEY_NAME].push(value[DaoValidator.RAW_KEY_NAME]);
delete value[DaoValidator.RAW_KEY_NAME] delete value[DaoValidator.RAW_KEY_NAME]
Utils._.extend(error, value) Utils._.extend(error, value)
}) })
...@@ -123,6 +147,14 @@ DaoValidator.prototype.validate = function() { ...@@ -123,6 +147,14 @@ DaoValidator.prototype.validate = function() {
}).run() }).run()
} }
/**
* Invoke the Validation sequence:
* - Before Validation Model Hooks
* - Validation
* - After Validation Model Hooks
*
* @return {sequelize.Utils.CustomEventEmitter} An eventemitter.
*/
DaoValidator.prototype.hookValidate = function() { DaoValidator.prototype.hookValidate = function() {
var self = this var self = this
...@@ -149,138 +181,125 @@ DaoValidator.prototype.hookValidate = function() { ...@@ -149,138 +181,125 @@ DaoValidator.prototype.hookValidate = function() {
}).run() }).run()
} }
// private /**
var validateModel = function() { * Will validate all attributes based on their schema rules and defined validators.
Utils._.each(this.modelInstance.__options.validate, function(_validator, validatorType) { *
var validator = prepareValidationOfAttribute.call(this, undefined, _validator, validatorType, { omitValue: true }) * @return {Promise(Array.<Promise.PromiseInspection>)} A promise from .settle().
* @private
this.chainer.add(new Utils.CustomEventEmitter(function(emitter) { */
var next = function(err) { DaoValidator.prototype._validateAttributes = function() {
var self = this
if (err) {
var error = {};
error[DaoValidator.RAW_KEY_NAME] = err
var msg = ((err instanceof Error) ? err.message : err)
error[validatorType] = [msg]
emitter.emit('error', error)
} else {
emitter.emit('success')
}
}
validator.args.unshift(next);
validator.fn.apply(null, validator.args)
}.bind(this)).run())
}.bind(this))
}
var validateAttributes = function() { // promisify all attribute invocations
var self = this var validators = [];
, errors = {} _.forIn(this.modelInstance.rawAttributes, function(rawAttribute, field) {
var value = self.modelInstance.dataValues[field]
if (self.modelInstance.validators.hasOwnProperty(field)) {
Utils._.each(this.modelInstance.rawAttributes, function(rawAttribute, field) { validators.push(self._validateAttribute(value, field))
var value = self.modelInstance.dataValues[field]
, hasAllowedNull = ((rawAttribute === undefined || rawAttribute.allowNull === true) && ((value === null) || (value === undefined)))
, isSkipped = self.options.skip.length > 0 && self.options.skip.indexOf(field) !== -1
if (self.modelInstance.validators.hasOwnProperty(field) && !hasAllowedNull && !isSkipped) {
errors = Utils._.merge(errors, validateAttribute.call(self, value, field))
} }
}) })
return errors return Promise.settle(validators)
} }
var validateAttribute = function(value, field) { /**
// for each validator * Will validate a single field against its schema definition (isnull).
Utils._.each(this.modelInstance.validators[field], function(details, validatorType) { *
var validator = prepareValidationOfAttribute.call(this, value, details, validatorType) * @param {string} field The field name.
* @param {*} value anything.
this.chainer.add(new Utils.CustomEventEmitter(function(emitter) { throw error;
var next = function(err) { * @return {Promise} A promise, will always resolve,
if (err) { * auto populates error on this.error local object.
var error = {} * @private
error[field] = [err] */
emitter.emit('error', error) DaoValidator.prototype._validateSchema = function(field, value) {
} else { var self = this;
emitter.emit('success') return new Promise(function(resolve) {
}
var hasAllowedNull = ((rawAttribute === undefined ||
rawAttribute.allowNull === true) && ((value === null) ||
(value === undefined)))
var isSkipped = self.options.skip.length > 0 &&
self.options.skip.indexOf(field) !== -1
if (!hasAllowedNull && !isSkipped) {
var error = new sequelizeError.ValidationError(field + ' cannot be null')
error.path = field
error.value = value
error.type = 'notNull Violation'
if (!self.errors[field]) {
self.errors[field] = [];
} }
self.errors[field].push(error);
}
validator.args.unshift(next); resolve();
validator.fn.apply(null, validator.args) });
}.bind(this)).run()) };
}.bind(this)) // for each validator for this field
} /**
* Validate a single attribute with all the defined validators.
var prepareValidationOfAttribute = function(value, details, validatorType, options) { *
* @param {*} value Anything.
* @param {string} field The field name.
* @return {Promise} A promise, will always resolve,
* auto populates error on this.error local object.
* @private
*/
DaoValidator.prototype._validateAttribute = function(value, field) {
var self = this;
// Promisify each validator
var validators = [];
Utils._.forIn(this.modelInstance.validators[field], function(details,
validatorType) {
var validator = self._prepareValidationOfAttribute.call(self, value, details,
validatorType);
validators.push(Promise.nodeify(validator));
});
return Promise.settle(validators)
.then(this._handleSettledResult.bind(this, field));
};
/**
* Prepare Attribute for validation.
*
* @param {*} value Anything
* @param {Function} validator The validator.
* @param {string} validatorType One of known to Sequelize validators.
* @param {Object=} optOptions Options
* @return {Object} An object with specific keys to invoke the validator.
* @private
*/
DaoValidator.prototype._prepareValidationOfAttribute = function(value, validator,
validatorType, optOptions) {
var isCustomValidator = false // if true then it's a custom validation method var isCustomValidator = false // if true then it's a custom validation method
, validatorFunction = null // the validation function to call , validatorFunction = null // the validation function to call
, validatorArgs = [] // extra arguments to pass to validation function
, errorMessage = "" // the error message to return if validation fails
if (typeof details === 'function') {
// it is a custom validator function?
isCustomValidator = true
var callArgs = []
var validatorArity = details.length
var omitValue = !!(options || {}).omitValue
if (!omitValue) {
callArgs.push(value)
}
// check if validator is async and requires a callback
var isAsync = omitValue && validatorArity === 1 ||
!omitValue && validatorArity === 2
validatorFunction = function(next) {
if (isAsync) {
callArgs.push(next)
}
try {
details.apply(this.modelInstance, callArgs)
} catch(ex) {
return next(ex)
}
if (!isAsync) { // it is a custom validator function?
next() isCustomValidator = true
}
}.bind(this)
} else {
// extract extra arguments for the validator
validatorArgs = details.hasOwnProperty("args") ? details.args : details
if (!Array.isArray(validatorArgs)) {
validatorArgs = [validatorArgs]
} else {
validatorArgs = validatorArgs.slice(0);
}
// extract the error msg var validatorArity = validator.length;
errorMessage = details.hasOwnProperty("msg") ? details.msg : 'Validation ' + validatorType + ' failed' var callArgs = []
var options = optOptions || {}
var omitValue = !!options.omitValue
// check if Validator knows that kind of validation test if (!omitValue) {
if (!Utils._.isFunction(Validator[validatorType])) { callArgs.push(value)
throw new Error("Invalid validator function: " + validatorType) }
}
// bind to validator obj
validatorFunction = function(next) {
var args = Array.prototype.slice.call(arguments, 1)
if (Validator[validatorType].apply(Validator, [value].concat(args))) { // check if validator is async and requires a callback
next() var isAsync = false
} else { if (omitValue && validatorArity === 1 || !omitValue && validatorArity === 2) {
next(errorMessage) isAsync = true;
}
}
} }
validatorFunction = Promise.nodeify(validator.bind(this.modelInstance))
return { return {
fn: validatorFunction, fn: validatorFunction,
msg: errorMessage, msg: errorMessage,
...@@ -289,3 +308,26 @@ var prepareValidationOfAttribute = function(value, details, validatorType, optio ...@@ -289,3 +308,26 @@ var prepareValidationOfAttribute = function(value, details, validatorType, optio
} }
} }
/**
* Handles the returned result of a Promise.settle.
*
* If errors are found it populates this.error and throws an Array of the errors.
*
* @param {string} field The attribute name.
* @param {Array.<Promise.PromiseInspection>} Promise inspection objects.
* @private
*/
DaoValidator.prototype._handleSettleResult = function(field, promiseInspections) {
var self = this;
promiseInspections.forEach(function(promiseInspection) {
if (promiseInspection.isRejected) {
var rejection = promiseInspection.error();
var error = new sequelizeError.ValidationError('Validation error')
error[DaoValidator.RAW_KEY_NAME] = rejection
if (!self.errors[field]) {
self.errors[field] = [];
}
self.errors[field].push(error);
}
});
};
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!