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

Commit 34d075e2 by Jochem Maas Committed by Sushant

feat(validation): new properties for ValidationErrorItem class (#7557)

1 parent eff8fb56
......@@ -293,7 +293,11 @@ class Query extends AbstractQuery {
_.forOwn(fields, (value, field) => {
errors.push(new sequelizeErrors.ValidationErrorItem(
this.getUniqueConstraintErrorMessage(field),
'unique violation', field, value
'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
field,
value,
this.instance,
'not_unique'
));
});
......
......@@ -196,7 +196,11 @@ class Query extends AbstractQuery {
_.forOwn(fields, (value, field) => {
errors.push(new sequelizeErrors.ValidationErrorItem(
this.getUniqueConstraintErrorMessage(field),
'unique violation', field, value
'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
field,
value,
this.instance,
'not_unique'
));
});
......@@ -206,15 +210,16 @@ class Query extends AbstractQuery {
case 1451:
case 1452: {
// e.g. CONSTRAINT `example_constraint_name` FOREIGN KEY (`example_id`) REFERENCES `examples` (`id`)
const match = err.message.match(/CONSTRAINT `(.*)` FOREIGN KEY \(`(.*)`\) REFERENCES `(.*)` \(`(.*)`\)/);
const fields = match ? match[2].split(/`, *`/) : undefined;
const match = err.message.match(/CONSTRAINT ([`"])(.*)\1 FOREIGN KEY \(\1(.*)\1\) REFERENCES \1(.*)\1 \(\1(.*)\1\)/);
const quoteChar = match ? match[1] : '`';
const fields = match ? match[3].split(new RegExp(`${quoteChar}, *${quoteChar}`)) : undefined;
return new sequelizeErrors.ForeignKeyConstraintError({
reltype: String(errCode) === '1451' ? 'parent' : 'child',
table: match ? match[3] : undefined,
table: match ? match[4] : undefined,
fields,
value: fields.length && this.instance && this.instance[fields[0]] || undefined,
index: match ? match[1] : undefined,
value: fields && fields.length && this.instance && this.instance[fields[0]] || undefined,
index: match ? match[2] : undefined,
parent: err
});
}
......
......@@ -298,7 +298,12 @@ class Query extends AbstractQuery {
_.forOwn(fields, (value, field) => {
errors.push(new sequelizeErrors.ValidationErrorItem(
this.getUniqueConstraintErrorMessage(field),
'unique violation', field, value));
'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
field,
value,
this.instance,
'not_unique'
));
});
if (this.model && this.model.uniqueKeys) {
......
......@@ -397,7 +397,12 @@ class Query extends AbstractQuery {
for (const field of fields) {
errors.push(new sequelizeErrors.ValidationErrorItem(
this.getUniqueConstraintErrorMessage(field),
'unique violation', field, this.instance && this.instance[field]));
'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
field,
this.instance && this.instance[field],
this.instance,
'not_unique'
));
}
if (this.model) {
......
'use strict';
const _ = require('lodash');
/**
* Sequelize provides a host of custom error classes, to allow you to do easier debugging. All of these errors are exposed on the sequelize object and the sequelize constructor.
* All sequelize errors inherit from the base JS error object.
......@@ -54,7 +56,7 @@ class ValidationError extends BaseError {
// ... otherwise create a concatenated message out of existing errors.
} else if (this.errors.length > 0 && this.errors[0].message) {
this.message = this.errors.map(err => err.type + ': ' + err.message).join(',\n');
this.message = this.errors.map(err => (err.type || err.origin) + ': ' + err.message).join(',\n');
}
Error.captureStackTrace(this, this.constructor);
}
......@@ -224,23 +226,110 @@ exports.UnknownConstraintError = UnknownConstraintError;
* Validation Error Item
* Instances of this class are included in the `ValidationError.errors` property.
*
* @param {string} message An error message
* @param {string} type The type of the validation error
* @param {string} path The field that triggered the validation error
* @param {String} message An error message
* @param {String} type The type/origin of the validation error
* @param {String} path The field that triggered the validation error
* @param {String} value The value that generated the error
* @param {Object} [inst] the DAO instance that caused the validation error
* @param {Object} [validatorKey] a validation "key", used for identification
* @param {String} [fnName] property name of the BUILT-IN validator function that caused the validation error (e.g. "in" or "len"), if applicable
* @param {String} [fnArgs] parameters used with the BUILT-IN validator function, if applicable
*
* @param {string} value The value that generated the error
*/
class ValidationErrorItem {
constructor(message, type, path, value) {
constructor(message, type, path, value, inst, validatorKey, fnName, fnArgs) {
this.message = message || '';
this.type = type || null;
this.type = null;
this.path = path || null;
this.value = value !== undefined ? value : null;
//This doesn't need captureStackTrace because it's not a subclass of Error
this.origin = null;
this.instance = inst || null;
this.validatorKey = validatorKey || null;
this.validatorName = fnName || null;
this.validatorArgs = fnArgs || [];
if (type) {
if (ValidationErrorItem.Origins[ type ]) {
this.origin = type;
} else {
const lowercaseType = _.toLower(type + '').trim();
const realType = ValidationErrorItem.TypeStringMap[ lowercaseType ];
if (realType && ValidationErrorItem.Origins[ realType ]) {
this.origin = realType;
this.type = type;
}
}
}
// This doesn't need captureStackTrace because it's not a subclass of Error
}
/**
* return a lowercase, trimmed string "key" that identifies the validator.
*
* Note: the string will be empty if the instance has neither a valid `validatorKey` property nor a valid `validatorName` property
*
* @param {Boolean} [useTypeAsNS=true] controls whether the returned value is "namespace",
* this parameter is ignored if the validator's `type` is not one of ValidationErrorItem.Origins
* @param {String} [NSSeparator='.'] a separator string for concatenating the namespace, must be not be empty,
* defaults to "." (fullstop). only used and validated if useTypeAsNS is TRUE.
* @throws {Error} thrown if NSSeparator is found to be invalid.
* @return {String}
*/
getValidatorKey(useTypeAsNS, NSSeparator) {
const useTANS = typeof useTypeAsNS === 'undefined' ? true : !!useTypeAsNS;
const NSSep = typeof NSSeparator === 'undefined' ? '.' : NSSeparator;
const type = this.origin;
const key = this.validatorKey || this.validatorName;
const useNS = useTANS && type && ValidationErrorItem.Origins[ type ];
if (useNS && (typeof NSSep !== 'string' || !NSSep.length)) {
throw new Error('Invalid namespace separator given, must be a non-empty string');
}
if (!(typeof key === 'string' && key.length)) {
return '';
}
return _.toLower(useNS ? [type, key].join(NSSep) : key).trim();
}
}
exports.ValidationErrorItem = ValidationErrorItem;
/**
* An enum that defines valid ValidationErrorItem `origin` values
*
* @type {Object}
* @property CORE {String} specifies errors that originate from the sequelize "core"
* @property DB {String} specifies validation errors that originate from the storage engine
* @property FUNCTION {String} specifies validation errors that originate from validator functions (both built-in and custom) defined for a given attribute
*/
ValidationErrorItem.Origins = {
CORE : 'CORE',
DB : 'DB',
FUNCTION : 'FUNCTION'
};
/**
* An object that is used internally by the `ValidationErrorItem` class
* that maps current `type` strings (as given to ValidationErrorItem.constructor()) to
* our new `origin` values.
*
* @type {Object}
*/
ValidationErrorItem.TypeStringMap = {
'notnull violation' : 'CORE',
'string violation' : 'CORE',
'unique violation' : 'DB',
'validation error' : 'FUNCTION'
};
/**
* A base class for all connection related errors.
*/
class ConnectionError extends BaseError {
......
......@@ -249,11 +249,11 @@ class InstanceValidator {
validatorFunction = Promise.promisify(validator.bind(this.modelInstance));
}
return validatorFunction()
.catch(e => this._pushError(false, errorKey, e, optValue));
.catch(e => this._pushError(false, errorKey, e, optValue, validatorType));
} else {
return Promise
.try(() => validator.call(this.modelInstance, invokeArgs))
.catch(e => this._pushError(false, errorKey, e, optValue));
.catch(e => this._pushError(false, errorKey, e, optValue, validatorType));
}
}
......@@ -275,10 +275,11 @@ class InstanceValidator {
if (typeof validator[validatorType] !== 'function') {
throw new Error('Invalid validator function: ' + validatorType);
}
const validatorArgs = this._extractValidatorArgs(test, validatorType, field);
if (!validator[validatorType].apply(validator, [valueString].concat(validatorArgs))) {
// extract the error msg
throw new Error(test.msg || `Validation ${validatorType} on ${field} failed`);
throw Object.assign(new Error(test.msg || `Validation ${validatorType} on ${field} failed`), { validatorName : validatorType, validatorArgs });
}
});
}
......@@ -318,19 +319,30 @@ class InstanceValidator {
* @private
*/
_validateSchema(rawAttribute, field, value) {
let error;
if (rawAttribute.allowNull === false && (value === null || value === undefined)) {
const validators = this.modelInstance.validators[field];
const errMsg = _.get(validators, 'notNull.msg', `${this.modelInstance.constructor.name}.${field} cannot be null`);
error = new sequelizeError.ValidationErrorItem(errMsg, 'notNull Violation', field, value);
this.errors.push(error);
this.errors.push(new sequelizeError.ValidationErrorItem(
errMsg,
'notNull Violation', // sequelizeError.ValidationErrorItem.Origins.CORE,
field,
value,
this.modelInstance,
'is_null'
));
}
if (rawAttribute.type === DataTypes.STRING || rawAttribute.type instanceof DataTypes.STRING || rawAttribute.type === DataTypes.TEXT || rawAttribute.type instanceof DataTypes.TEXT) {
if (Array.isArray(value) || _.isObject(value) && !(value instanceof Utils.SequelizeMethod) && !Buffer.isBuffer(value)) {
error = new sequelizeError.ValidationErrorItem(`${field} cannot be an array or an object`, 'string violation', field, value);
this.errors.push(error);
this.errors.push(new sequelizeError.ValidationErrorItem(
`${field} cannot be an array or an object`,
'string violation', // sequelizeError.ValidationErrorItem.Origins.CORE,
field,
value,
this.modelInstance,
'not_a_string'
));
}
}
}
......@@ -350,7 +362,9 @@ class InstanceValidator {
for (const promiseInspection of promiseInspections) {
if (promiseInspection.isRejected()) {
const rejection = promiseInspection.error();
this._pushError(true, field, rejection, value);
const isBuiltIn = !!rejection.validatorName;
this._pushError(isBuiltIn, field, rejection, value, rejection.validatorName, rejection.validatorArgs);
}
}
}
......@@ -358,15 +372,28 @@ class InstanceValidator {
/**
* Signs all errors retaining the original.
*
* @param {boolean} isBuiltin Determines if error is from builtin validator.
* @param {string} errorKey The error key to assign on this.errors object.
* @param {Error|string} rawError The original error.
* @param {string|number} value The data that triggered the error.
* @param {Boolean} isBuiltin - Determines if error is from builtin validator.
* @param {String} errorKey - name of invalid attribute.
* @param {Error|String} rawError - The original error.
* @param {String|Number} value - The data that triggered the error.
* @param {String} fnName - Name of the validator, if any
* @param {Array} fnArgs - Arguments for the validator [function], if any
*
* @private
*/
_pushError(isBuiltin, errorKey, rawError, value) {
_pushError(isBuiltin, errorKey, rawError, value, fnName, fnArgs) {
const message = rawError.message || rawError || 'Validation error';
const error = new sequelizeError.ValidationErrorItem(message, 'Validation error', errorKey, value);
const error = new sequelizeError.ValidationErrorItem(
message,
'Validation error', // sequelizeError.ValidationErrorItem.Origins.FUNCTION,
errorKey,
value,
this.modelInstance,
fnName,
isBuiltin ? fnName : undefined,
isBuiltin ? fnArgs : undefined
);
error[InstanceValidator.RAW_KEY_NAME] = rawError;
this.errors.push(error);
......
......@@ -84,8 +84,8 @@ describe(Support.getTestDialectTeaser('Sequelize Errors'), () => {
it('SequelizeValidationError should concatenate an error messages from given errors if no explicit message is defined', () => {
const errorItems = [
new errors.ValidationErrorItem('<field name> cannot be null', 'notNull Violation', '<field name>', null),
new errors.ValidationErrorItem('<field name> cannot be an array or an object', 'string violation', '<field name>', null)
new Sequelize.ValidationErrorItem('<field name> cannot be null', 'notNull Violation', '<field name>', null),
new Sequelize.ValidationErrorItem('<field name> cannot be an array or an object', 'string violation', '<field name>', null)
],
validationError = new Sequelize.ValidationError(null, errorItems);
......@@ -93,6 +93,92 @@ describe(Support.getTestDialectTeaser('Sequelize Errors'), () => {
expect(validationError.message).to.match(/notNull Violation: <field name> cannot be null,\nstring violation: <field name> cannot be an array or an object/);
});
it('SequelizeValidationErrorItem does not require instance & validator constructor parameters', () => {
const error = new Sequelize.ValidationErrorItem('error!', null, 'myfield');
expect(error).to.be.instanceOf(Sequelize.ValidationErrorItem);
});
it('SequelizeValidationErrorItem should have instance, key & validator properties when given to constructor', () => {
const inst = { foo : 'bar' };
const vargs = [4];
const error = new Sequelize.ValidationErrorItem('error!', 'FUNCTION', 'foo', 'bar', inst, 'klen', 'len', vargs);
expect(error).to.have.property('instance');
expect(error.instance).to.equal(inst);
expect(error).to.have.property('validatorKey', 'klen');
expect(error).to.have.property('validatorName', 'len');
expect(error).to.have.property('validatorArgs', vargs);
});
it('SequelizeValidationErrorItem.getValidatorKey() should return a string', () => {
const error = new Sequelize.ValidationErrorItem('error!', 'FUNCTION', 'foo', 'bar', null, 'klen', 'len', [4]);
expect(error).to.have.property('getValidatorKey');
expect(error.getValidatorKey).to.be.a('function');
expect(error.getValidatorKey()).to.equal('function.klen');
expect(error.getValidatorKey(false)).to.equal('klen');
expect(error.getValidatorKey(0)).to.equal('klen');
expect(error.getValidatorKey(1, ':')).to.equal('function:klen');
expect(error.getValidatorKey(true, '-:-')).to.equal('function-:-klen');
const empty = new Sequelize.ValidationErrorItem('error!', 'FUNCTION', 'foo', 'bar');
expect(empty.getValidatorKey()).to.equal('');
expect(empty.getValidatorKey(false)).to.equal('');
expect(empty.getValidatorKey(0)).to.equal('');
expect(empty.getValidatorKey(1, ':')).to.equal('');
expect(empty.getValidatorKey(true, '-:-')).to.equal('');
});
it('SequelizeValidationErrorItem.getValidatorKey() should throw if namespace separator is invalid (only if NS is used & available)', () => {
const error = new Sequelize.ValidationErrorItem('error!', 'FUNCTION', 'foo', 'bar', null, 'klen', 'len', [4]);
expect(() => error.getValidatorKey(false, {})).to.not.throw();
expect(() => error.getValidatorKey(false, [])).to.not.throw();
expect(() => error.getValidatorKey(false, null)).to.not.throw();
expect(() => error.getValidatorKey(false, '')).to.not.throw();
expect(() => error.getValidatorKey(false, false)).to.not.throw();
expect(() => error.getValidatorKey(false, true)).to.not.throw();
expect(() => error.getValidatorKey(false, undefined)).to.not.throw();
expect(() => error.getValidatorKey(true, undefined)).to.not.throw(); // undefined will trigger use of function parameter default
expect(() => error.getValidatorKey(true, {})).to.throw(Error);
expect(() => error.getValidatorKey(true, [])).to.throw(Error);
expect(() => error.getValidatorKey(true, null)).to.throw(Error);
expect(() => error.getValidatorKey(true, '')).to.throw(Error);
expect(() => error.getValidatorKey(true, false)).to.throw(Error);
expect(() => error.getValidatorKey(true, true)).to.throw(Error);
});
it('SequelizeValidationErrorItem should map deprecated "type" values to new "origin" values', () => {
const data = {
'notNull Violation' : 'CORE',
'string violation' : 'CORE',
'unique violation' : 'DB',
'Validation error' : 'FUNCTION'
};
Object.keys(data).forEach(k => {
const error = new Sequelize.ValidationErrorItem('error!', k, 'foo', null);
expect(error).to.have.property('origin', data[k]);
expect(error).to.have.property('type', k);
});
});
it('SequelizeValidationErrorItem.Origins is valid', () => {
const ORIGINS = errors.ValidationErrorItem.Origins;
expect(ORIGINS).to.have.property('CORE', 'CORE');
expect(ORIGINS).to.have.property('DB', 'DB');
expect(ORIGINS).to.have.property('FUNCTION', 'FUNCTION');
});
it('SequelizeDatabaseError should keep original message', () => {
const orig = new Error('original database error message');
const databaseError = new Sequelize.DatabaseError(orig);
......
......@@ -987,17 +987,11 @@ describe(Support.getTestDialectTeaser('Model'), () => {
return UserNull.sync({ force: true }).then(() => {
return UserNull.create({ username: 'foo2', smth: null }).catch(err => {
expect(err).to.exist;
expect(err.get('smth')[0].path).to.equal('smth');
if (dialect === 'mysql') {
// We need to allow two different errors for MySQL, see:
// http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_trans_tables
expect(err.get('smth')[0].type).to.match(/notNull Violation/);
}
else if (dialect === 'sqlite') {
expect(err.get('smth')[0].type).to.match(/notNull Violation/);
} else {
expect(err.get('smth')[0].type).to.match(/notNull Violation/);
}
const smth1 = err.get('smth')[0] || {};
expect(smth1.path).to.equal('smth');
expect(smth1.type || smth1.origin).to.match(/notNull Violation/);
});
});
});
......@@ -1670,8 +1664,12 @@ describe(Support.getTestDialectTeaser('Model'), () => {
], { validate: true }).catch(errors => {
expect(errors).to.be.instanceof(Promise.AggregateError);
expect(errors).to.have.length(2);
const e0name0 = errors[0].errors.get('name')[0];
expect(errors[0].record.code).to.equal('1234');
expect(errors[0].errors.get('name')[0].type).to.equal('notNull Violation');
expect(e0name0.type || e0name0.origin).to.equal('notNull Violation');
expect(errors[1].record.name).to.equal('bar');
expect(errors[1].record.code).to.equal('1');
expect(errors[1].errors.get('code')[0].message).to.equal('Validation len on code failed');
......
'use strict';
const chai = require('chai');
const expect = chai.expect;
const Support = require(__dirname + '/../../support');
const dialect = Support.getTestDialect();
const queryProto = Support.sequelize.dialect.Query.prototype;
if (dialect === 'mysql') {
describe('[MYSQL Specific] ForeignKeyConstraintError - error message parsing', () => {
it('FK Errors with ` quotation char are parsed correctly', () => {
const fakeErr = new Error('Cannot delete or update a parent row: a foreign key constraint fails (`table`.`brothers`, CONSTRAINT `brothers_ibfk_1` FOREIGN KEY (`personId`) REFERENCES `people` (`id`) ON UPDATE CASCADE).');
fakeErr.code = 1451;
const parsedErr = queryProto.formatError(fakeErr);
expect(parsedErr).to.be.instanceOf(Support.sequelize.ForeignKeyConstraintError);
expect(parsedErr.parent).to.equal(fakeErr);
expect(parsedErr.reltype).to.equal('parent');
expect(parsedErr.table).to.equal('people');
expect(parsedErr.fields).to.be.an('array').to.deep.equal(['personId']);
expect(parsedErr.value).to.be.undefined;
expect(parsedErr.index).to.equal('brothers_ibfk_1');
});
it('FK Errors with " quotation char are parsed correctly', () => {
const fakeErr = new Error('Cannot delete or update a parent row: a foreign key constraint fails ("table"."brothers", CONSTRAINT "brothers_ibfk_1" FOREIGN KEY ("personId") REFERENCES "people" ("id") ON UPDATE CASCADE).');
fakeErr.code = 1451;
const parsedErr = queryProto.formatError(fakeErr);
expect(parsedErr).to.be.instanceOf(Support.sequelize.ForeignKeyConstraintError);
expect(parsedErr.parent).to.equal(fakeErr);
expect(parsedErr.reltype).to.equal('parent');
expect(parsedErr.table).to.equal('people');
expect(parsedErr.fields).to.be.an('array').to.deep.equal(['personId']);
expect(parsedErr.value).to.be.undefined;
expect(parsedErr.index).to.equal('brothers_ibfk_1');
});
});
}
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!