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

Commit bd751645 by Matt Broadstone

make UniqueConstraintError inherit ValidationError

Unique constraint errors are in fact a different type of validation
error, run on the database level. While important to make a destinction
internally when we have encountered a UniqueConstraintError vs a normal
ValidationError, on the user side they should be represented the same.
This patch allows these exceptions to provide data in the same fashion
making it easier to uniformly process the errors
1 parent d5cc32a2
...@@ -5,21 +5,6 @@ var Utils = require('../../utils'); ...@@ -5,21 +5,6 @@ var Utils = require('../../utils');
module.exports = (function() { module.exports = (function() {
var QueryGenerator = { var QueryGenerator = {
dialect: 'mariadb', dialect: 'mariadb',
uniqueConstraintMapping: {
code: 1062,
map: function(str) {
// we're manually remvoving uniq_ here for a future capability of defining column names explicitly
var match = str.replace('uniq_', '').match(/Duplicate entry .* for key '(.*?)'$/);
if (match === null || match.length < 2) {
return false;
}
return {
indexName: match[1],
fields: match[1].split('_')
};
}
}
}; };
// "MariaDB is a drop-in replacement for MySQL." - so thats exactly what we do, drop in the mysql query generator // "MariaDB is a drop-in replacement for MySQL." - so thats exactly what we do, drop in the mysql query generator
......
...@@ -581,23 +581,7 @@ module.exports = (function() { ...@@ -581,23 +581,7 @@ module.exports = (function() {
booleanValue: function(value) { booleanValue: function(value) {
return !!value ? 1 : 0; return !!value ? 1 : 0;
},
uniqueConstraintMapping: {
code: 'EREQUEST',
map: function(str) {
var match = str.match(/Violation of UNIQUE KEY constraint '(.*)'. Cannot insert duplicate key in object '?(.*?)$/);
match = match || str.match(/Cannot insert duplicate key row in object .* with unique index '(.*)'/);
if (match === null || match.length < 2) {
return false;
}
return {
indexName: match[1],
fields: match[1].split('_')
};
} }
},
}; };
// private methods // private methods
......
...@@ -163,14 +163,32 @@ module.exports = (function() { ...@@ -163,14 +163,32 @@ module.exports = (function() {
Query.prototype.formatError = function (err) { Query.prototype.formatError = function (err) {
var match; var match;
match = err.message.match(/Violation of UNIQUE KEY constraint '(.*)'. Cannot insert duplicate key in object '?(.*?)$/); match = err.message.match(/Violation of UNIQUE KEY constraint '(.*)'. Cannot insert duplicate key in object '.*'. The duplicate key value is \((.*)\)./);
match = match || err.message.match(/Cannot insert duplicate key row in object .* with unique index '(.*)'/); match = match || err.message.match(/Cannot insert duplicate key row in object .* with unique index '(.*)'/);
if (match && match.length > 1) { if (match && match.length > 1) {
var fields = {}
, message = 'Validation error'
, uniqueKey = this.callee.__options.uniqueKeys[match[1]];
if (!!uniqueKey.msg) message = uniqueKey.msg;
if (!!match[2]) {
var values = match[2].split(',').map(Function.prototype.call, String.prototype.trim);
if (!!uniqueKey) {
fields = Utils._.zipObject(uniqueKey.fields, values);
} else {
fields[match[1]] = match[2];
}
}
var errors = [];
Utils._.forOwn(fields, function(value, field) {
errors.push(new sequelizeErrors.ValidationErrorItem(
field + ' must be unique', 'unique violation', field, value));
});
return new sequelizeErrors.UniqueConstraintError({ return new sequelizeErrors.UniqueConstraintError({
name: 'SequelizeUniqueConstraintError', message: message,
fields: null, errors: errors,
index: 0,
value: match[2],
parent: err parent: err
}); });
} }
......
...@@ -99,22 +99,6 @@ module.exports = (function() { ...@@ -99,22 +99,6 @@ module.exports = (function() {
return 'SHOW TABLES;'; return 'SHOW TABLES;';
}, },
uniqueConstraintMapping: {
code: 'ER_DUP_ENTRY',
map: function(str) {
// we're manually remvoving uniq_ here for a future capability of defining column names explicitly
var match = str.replace('uniq_', '').match(/Duplicate entry .* for key '(.*?)'$/);
if (match === null || match.length < 2) {
return false;
}
return {
indexName: match[1],
fields: match[1].split('_')
};
}
},
addColumnQuery: function(table, key, dataType) { addColumnQuery: function(table, key, dataType) {
var query = 'ALTER TABLE <%= table %> ADD <%= attribute %>;' var query = 'ALTER TABLE <%= table %> ADD <%= attribute %>;'
, attribute = Utils._.template('<%= key %> <%= definition %>')({ , attribute = Utils._.template('<%= key %> <%= definition %>')({
......
...@@ -112,10 +112,27 @@ module.exports = (function() { ...@@ -112,10 +112,27 @@ module.exports = (function() {
case 1062: case 1062:
match = err.message.match(/Duplicate entry '(.*)' for key '?(.*?)'?$/); match = err.message.match(/Duplicate entry '(.*)' for key '?(.*?)'?$/);
var values = match[1].split('-')
, fields = {}
, message = 'Validation error'
, uniqueKey = this.callee.__options.uniqueKeys[match[2]];
if (!!uniqueKey) {
if (!!uniqueKey.msg) message = uniqueKey.msg;
fields = Utils._.zipObject(uniqueKey.fields, values);
} else {
fields[match[2]] = match[1];
}
var errors = [];
Utils._.forOwn(fields, function(value, field) {
errors.push(new sequelizeErrors.ValidationErrorItem(
field + ' must be unique', 'unique violation', field, value));
});
return new sequelizeErrors.UniqueConstraintError({ return new sequelizeErrors.UniqueConstraintError({
fields: null, message: message,
index: match[2], errors: errors,
value: match[1],
parent: err parent: err
}); });
......
...@@ -165,29 +165,6 @@ module.exports = (function() { ...@@ -165,29 +165,6 @@ module.exports = (function() {
} }
}, },
uniqueConstraintMapping: {
code: '23505',
map: function(str) {
var match = str.match(/duplicate key value violates unique constraint "(.*?)"/);
if (match === null || match.length < 2) {
return false;
}
var indexName = match[1];
var fields = [];
match = indexName.match(/(.*?)_key/);
if (!!match && match.length > 1) {
fields = match[1].split('_').splice(1);
}
return {
indexName: indexName,
fields: fields
};
}
},
addColumnQuery: function(table, key, dataType) { addColumnQuery: function(table, key, dataType) {
var query = 'ALTER TABLE <%= table %> ADD COLUMN <%= attribute %>;' var query = 'ALTER TABLE <%= table %> ADD COLUMN <%= attribute %>;'
, dbDataType = this.attributeToSQL(dataType, {context: 'addColumn'}) , dbDataType = this.attributeToSQL(dataType, {context: 'addColumn'})
......
...@@ -266,18 +266,34 @@ module.exports = (function() { ...@@ -266,18 +266,34 @@ module.exports = (function() {
match = err.detail.match(/Key \((.*?)\)=\((.*?)\)/); match = err.detail.match(/Key \((.*?)\)=\((.*?)\)/);
if (match) { if (match) {
var fields = Utils._.zipObject(match[1].split(', '), match[2].split(', '))
, errors = []
, message = 'Validation error';
Utils._.forOwn(fields, function(value, field) {
errors.push(new sequelizeErrors.ValidationErrorItem(
field + ' must be unique', 'unique violation', field, value));
});
Utils._.forOwn(this.callee.__options.uniqueKeys, function(constraint) {
if (Utils._.isEqual(constraint.fields, Object.keys(fields)) && !!constraint.msg) {
message = constraint.msg;
return false;
}
});
return new sequelizeErrors.UniqueConstraintError({ return new sequelizeErrors.UniqueConstraintError({
fields: match[1].split(', '), message: message,
value: match[2].split(', '), errors: errors,
index: null,
parent: err parent: err
}); });
} else { } else {
return new sequelizeErrors.UniqueConstraintError({ return new sequelizeErrors.UniqueConstraintError({
error: err, message: err.message,
message: err.message parent: err
}); });
} }
break; break;
default: default:
return new sequelizeErrors.DatabaseError(err); return new sequelizeErrors.DatabaseError(err);
......
...@@ -121,29 +121,6 @@ module.exports = (function() { ...@@ -121,29 +121,6 @@ module.exports = (function() {
return !!value ? 1 : 0; return !!value ? 1 : 0;
}, },
uniqueConstraintMapping: {
code: 'SQLITE_CONSTRAINT',
map: function(str) {
var match = str.match(/columns (.*?) are/); // Sqlite pre 2.2 behavior - Error: SQLITE_CONSTRAINT: columns x, y are not unique
if (match !== null && match.length >= 2) {
return {
fields: match[1].split(', ')
};
}
match = str.match(/UNIQUE constraint failed: (.*)/); // Sqlite 2.2 behavior - Error: SQLITE_CONSTRAINT: UNIQUE constraint failed: table.x, table.y
if (match !== null && match.length >= 2) {
return {
fields: match[1].split(', ').map(function (columnWithTable) {
return columnWithTable.split('.')[1];
})
};
}
return false;
}
},
addLimitAndOffset: function(options, model){ addLimitAndOffset: function(options, model){
var fragment = ''; var fragment = '';
if (options.offset && !options.limit) { if (options.offset && !options.limit) {
......
...@@ -189,33 +189,52 @@ module.exports = (function() { ...@@ -189,33 +189,52 @@ module.exports = (function() {
switch (err.code) { switch (err.code) {
case 'SQLITE_CONSTRAINT': case 'SQLITE_CONSTRAINT':
match = err.message.match(/columns (.*?) are/); // Sqlite pre 2.2 behavior - Error: SQLITE_CONSTRAINT: columns x, y are not unique match = err.message.match(/FOREIGN KEY constraint failed/);
if (match !== null && match.length >= 2) { if (match !== null) {
return new sequelizeErrors.UniqueConstraintError(match[1].split(', '),null, err); return new sequelizeErrors.ForeignKeyConstraintError({
parent :err
});
} }
match = err.message.match(/UNIQUE constraint failed: (.*)/); // Sqlite 2.2 behavior - Error: SQLITE_CONSTRAINT: UNIQUE constraint failed: table.x, table.y var fields = [];
// Sqlite pre 2.2 behavior - Error: SQLITE_CONSTRAINT: columns x, y are not unique
match = err.message.match(/columns (.*?) are/);
if (match !== null && match.length >= 2) {
fields = match[1].split(', ');
} else {
// Sqlite post 2.2 behavior - Error: SQLITE_CONSTRAINT: UNIQUE constraint failed: table.x, table.y
match = err.message.match(/UNIQUE constraint failed: (.*)/);
if (match !== null && match.length >= 2) { if (match !== null && match.length >= 2) {
var fields = match[1].split(', ').map(function (columnWithTable) { fields = match[1].split(', ').map(function (columnWithTable) {
return columnWithTable.split('.')[1]; return columnWithTable.split('.')[1];
}); });
}
}
return new sequelizeErrors.UniqueConstraintError({ var errors = []
fields: fields, , self = this
index: null, , message = 'Validation error';
value: null,
parent: err fields.forEach(function(field) {
errors.push(new sequelizeErrors.ValidationErrorItem(
field + ' must be unique', 'unique violation', field, self.callee[field]));
}); });
Utils._.forOwn(this.callee.__options.uniqueKeys, function(constraint) {
if (Utils._.isEqual(constraint.fields, fields) && !!constraint.msg) {
message = constraint.msg;
return false;
} }
});
match = err.message.match(/FOREIGN KEY constraint failed/); return new sequelizeErrors.UniqueConstraintError({
if (match !== null) { message: message,
return new sequelizeErrors.ForeignKeyConstraintError({ errors: errors,
parent :err parent: err
}); });
}
return err;
case 'SQLITE_BUSY': case 'SQLITE_BUSY':
return new sequelizeErrors.TimeoutError(err); return new sequelizeErrors.TimeoutError(err);
......
...@@ -105,7 +105,7 @@ error.TimeoutError = function (parent) { ...@@ -105,7 +105,7 @@ error.TimeoutError = function (parent) {
}; };
util.inherits(error.TimeoutError, error.BaseError); util.inherits(error.TimeoutError, error.BaseError);
/** /**
* Thrown when a unique constraint is violated in the database * Thrown when a unique constraint is violated in the database
* @extends DatabaseError * @extends DatabaseError
* @constructor * @constructor
...@@ -113,16 +113,15 @@ util.inherits(error.TimeoutError, error.BaseError); ...@@ -113,16 +113,15 @@ util.inherits(error.TimeoutError, error.BaseError);
error.UniqueConstraintError = function (options) { error.UniqueConstraintError = function (options) {
options = options || {}; options = options || {};
options.parent = options.parent || { sql: '' }; options.parent = options.parent || { sql: '' };
options.message = options.message || 'Validation error';
options.errors = options.errors || {};
error.DatabaseError.call(this, options.parent); error.ValidationError.call(this, options.message, options.errors);
this.name = 'SequelizeUniqueConstraintError'; this.name = 'SequelizeUniqueConstraintError';
this.message = options.message; this.message = options.message;
this.fields = options.fields; this.errors = options.errors;
this.value = options.value;
this.index = options.index;
}; };
util.inherits(error.UniqueConstraintError, error.DatabaseError); util.inherits(error.UniqueConstraintError, error.ValidationError);
/** /**
* Thrown when a foreign key constraint is violated in the database * Thrown when a foreign key constraint is violated in the database
......
...@@ -602,34 +602,13 @@ module.exports = (function() { ...@@ -602,34 +602,13 @@ module.exports = (function() {
}); });
} }
}).then(function() { }).then(function() {
return self.QueryInterface[query].apply(self.QueryInterface, args).catch(self.sequelize.UniqueConstraintError, function(err) { return self.QueryInterface[query].apply(self.QueryInterface, args)
if (!!self.__options.uniqueKeys && self.QueryInterface.QueryGenerator.uniqueConstraintMapping.code === err.parent.code) { .tap(function(result) {
var index = self.QueryInterface.QueryGenerator.uniqueConstraintMapping.map(err.parent.toString());
if (index !== false) {
var fields = index.fields.filter(function(f) { return f !== self.Model.tableName; });
Utils._.each(self.__options.uniqueKeys, function(uniqueKey) {
if (!!uniqueKey.msg) {
if (uniqueKey.name === index.indexName) {
fields = _.clone(uniqueKey.fields);
}
if (Utils._.isEqual(uniqueKey.fields, fields)) {
err = new self.sequelize.UniqueConstraintError({
message: uniqueKey.msg,
fields: fields,
index: index.indexName,
parent: err.parent
});
}
}
});
}
}
throw err;
}).tap(function(result) {
// Transfer database generated values (defaults, autoincrement, etc) // Transfer database generated values (defaults, autoincrement, etc)
Object.keys(self.Model.rawAttributes).forEach(function (attr) { Object.keys(self.Model.rawAttributes).forEach(function (attr) {
if (self.Model.rawAttributes[attr].field && values[self.Model.rawAttributes[attr].field] !== undefined && self.Model.rawAttributes[attr].field !== attr) { if (self.Model.rawAttributes[attr].field &&
values[self.Model.rawAttributes[attr].field] !== undefined &&
self.Model.rawAttributes[attr].field !== attr) {
values[attr] = values[self.Model.rawAttributes[attr].field]; values[attr] = values[self.Model.rawAttributes[attr].field];
delete values[self.Model.rawAttributes[attr].field]; delete values[self.Model.rawAttributes[attr].field];
} }
...@@ -641,12 +620,14 @@ module.exports = (function() { ...@@ -641,12 +620,14 @@ module.exports = (function() {
options.fields.forEach(function (field) { options.fields.forEach(function (field) {
result._previousDataValues[field] = result.dataValues[field]; result._previousDataValues[field] = result.dataValues[field];
}); });
}).tap(function(result) { })
.tap(function(result) {
// Run after hook // Run after hook
if (options.hooks) { if (options.hooks) {
return self.Model.runHooks('after' + hook, result, options); return self.Model.runHooks('after' + hook, result, options);
} }
}).then(function(result) { })
.then(function(result) {
return result; return result;
}); });
}); });
......
...@@ -129,7 +129,18 @@ describe(Support.getTestDialectTeaser('Sequelize Errors'), function () { ...@@ -129,7 +129,18 @@ describe(Support.getTestDialectTeaser('Sequelize Errors'), function () {
}); });
describe('Constraint error', function () { describe('Constraint error', function () {
it('Can be intercepted using .catch', function () { [
{
type: 'UniqueConstraintError',
exception: Sequelize.UniqueConstraintError
},
{
type: 'ValidationError',
exception: Sequelize.ValidationError
}
].forEach(function(constraintTest) {
it('Can be intercepted as ' + constraintTest.type + ' using .catch', function () {
var spy = sinon.spy() var spy = sinon.spy()
, User = this.sequelize.define('user', { , User = this.sequelize.define('user', {
first_name: { first_name: {
...@@ -142,13 +153,16 @@ describe(Support.getTestDialectTeaser('Sequelize Errors'), function () { ...@@ -142,13 +153,16 @@ describe(Support.getTestDialectTeaser('Sequelize Errors'), function () {
} }
}); });
var record = { first_name: 'jan', last_name: 'meier' };
return this.sequelize.sync({ force: true }).bind(this).then(function () { return this.sequelize.sync({ force: true }).bind(this).then(function () {
return User.create({ first_name: 'jan', last_name: 'meier' }); return User.create(record);
}).then(function () { }).then(function () {
return User.create({ first_name: 'jan', last_name: 'meier' }).catch(this.sequelize.UniqueConstraintError, spy); return User.create(record).catch(constraintTest.exception, spy);
}).then(function () { }).then(function () {
expect(spy).to.have.been.calledOnce; expect(spy).to.have.been.calledOnce;
}); });
}); });
});
}); });
}); });
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!