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

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
......@@ -4,22 +4,7 @@ var Utils = require('../../utils');
module.exports = (function() {
var QueryGenerator = {
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('_')
};
}
}
dialect: 'mariadb',
};
// "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() {
booleanValue: function(value) {
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
......
......@@ -163,14 +163,32 @@ module.exports = (function() {
Query.prototype.formatError = function (err) {
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 '(.*)'/);
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({
name: 'SequelizeUniqueConstraintError',
fields: null,
index: 0,
value: match[2],
message: message,
errors: errors,
parent: err
});
}
......
......@@ -99,22 +99,6 @@ module.exports = (function() {
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) {
var query = 'ALTER TABLE <%= table %> ADD <%= attribute %>;'
, attribute = Utils._.template('<%= key %> <%= definition %>')({
......
......@@ -112,10 +112,27 @@ module.exports = (function() {
case 1062:
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({
fields: null,
index: match[2],
value: match[1],
message: message,
errors: errors,
parent: err
});
......
......@@ -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) {
var query = 'ALTER TABLE <%= table %> ADD COLUMN <%= attribute %>;'
, dbDataType = this.attributeToSQL(dataType, {context: 'addColumn'})
......
......@@ -266,18 +266,34 @@ module.exports = (function() {
match = err.detail.match(/Key \((.*?)\)=\((.*?)\)/);
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({
fields: match[1].split(', '),
value: match[2].split(', '),
index: null,
message: message,
errors: errors,
parent: err
});
} else {
return new sequelizeErrors.UniqueConstraintError({
error: err,
message: err.message
message: err.message,
parent: err
});
}
break;
default:
return new sequelizeErrors.DatabaseError(err);
......
......@@ -121,29 +121,6 @@ module.exports = (function() {
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){
var fragment = '';
if (options.offset && !options.limit) {
......
......@@ -189,25 +189,6 @@ module.exports = (function() {
switch (err.code) {
case 'SQLITE_CONSTRAINT':
match = err.message.match(/columns (.*?) are/); // Sqlite pre 2.2 behavior - Error: SQLITE_CONSTRAINT: columns x, y are not unique
if (match !== null && match.length >= 2) {
return new sequelizeErrors.UniqueConstraintError(match[1].split(', '),null, err);
}
match = err.message.match(/UNIQUE constraint failed: (.*)/); // Sqlite 2.2 behavior - Error: SQLITE_CONSTRAINT: UNIQUE constraint failed: table.x, table.y
if (match !== null && match.length >= 2) {
var fields = match[1].split(', ').map(function (columnWithTable) {
return columnWithTable.split('.')[1];
});
return new sequelizeErrors.UniqueConstraintError({
fields: fields,
index: null,
value: null,
parent: err
});
}
match = err.message.match(/FOREIGN KEY constraint failed/);
if (match !== null) {
return new sequelizeErrors.ForeignKeyConstraintError({
......@@ -215,7 +196,45 @@ module.exports = (function() {
});
}
return err;
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) {
fields = match[1].split(', ').map(function (columnWithTable) {
return columnWithTable.split('.')[1];
});
}
}
var errors = []
, self = this
, message = 'Validation error';
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;
}
});
return new sequelizeErrors.UniqueConstraintError({
message: message,
errors: errors,
parent: err
});
case 'SQLITE_BUSY':
return new sequelizeErrors.TimeoutError(err);
......
......@@ -105,7 +105,7 @@ error.TimeoutError = function (parent) {
};
util.inherits(error.TimeoutError, error.BaseError);
/**
/**
* Thrown when a unique constraint is violated in the database
* @extends DatabaseError
* @constructor
......@@ -113,16 +113,15 @@ util.inherits(error.TimeoutError, error.BaseError);
error.UniqueConstraintError = function (options) {
options = options || {};
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.message = options.message;
this.fields = options.fields;
this.value = options.value;
this.index = options.index;
this.errors = options.errors;
};
util.inherits(error.UniqueConstraintError, error.DatabaseError);
util.inherits(error.UniqueConstraintError, error.ValidationError);
/**
* Thrown when a foreign key constraint is violated in the database
......
......@@ -602,53 +602,34 @@ module.exports = (function() {
});
}
}).then(function() {
return self.QueryInterface[query].apply(self.QueryInterface, args).catch(self.sequelize.UniqueConstraintError, function(err) {
if (!!self.__options.uniqueKeys && self.QueryInterface.QueryGenerator.uniqueConstraintMapping.code === err.parent.code) {
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)
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) {
values[attr] = values[self.Model.rawAttributes[attr].field];
delete values[self.Model.rawAttributes[attr].field];
}
});
values = _.extend(values, result.dataValues);
return self.QueryInterface[query].apply(self.QueryInterface, args)
.tap(function(result) {
// Transfer database generated values (defaults, autoincrement, etc)
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) {
values[attr] = values[self.Model.rawAttributes[attr].field];
delete values[self.Model.rawAttributes[attr].field];
}
});
values = _.extend(values, result.dataValues);
// Ensure new values are on Instance, and reset previousDataValues
result.dataValues = _.extend(result.dataValues, values);
options.fields.forEach(function (field) {
result._previousDataValues[field] = result.dataValues[field];
// Ensure new values are on Instance, and reset previousDataValues
result.dataValues = _.extend(result.dataValues, values);
options.fields.forEach(function (field) {
result._previousDataValues[field] = result.dataValues[field];
});
})
.tap(function(result) {
// Run after hook
if (options.hooks) {
return self.Model.runHooks('after' + hook, result, options);
}
})
.then(function(result) {
return result;
});
}).tap(function(result) {
// Run after hook
if (options.hooks) {
return self.Model.runHooks('after' + hook, result, options);
}
}).then(function(result) {
return result;
});
});
});
};
......
......@@ -129,26 +129,40 @@ describe(Support.getTestDialectTeaser('Sequelize Errors'), function () {
});
describe('Constraint error', function () {
it('Can be intercepted using .catch', function () {
var spy = sinon.spy()
, User = this.sequelize.define('user', {
first_name: {
type: Sequelize.STRING,
unique: 'unique_name'
},
last_name: {
type: Sequelize.STRING,
unique: 'unique_name'
}
[
{
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()
, User = this.sequelize.define('user', {
first_name: {
type: Sequelize.STRING,
unique: 'unique_name'
},
last_name: {
type: Sequelize.STRING,
unique: 'unique_name'
}
});
var record = { first_name: 'jan', last_name: 'meier' };
return this.sequelize.sync({ force: true }).bind(this).then(function () {
return User.create(record);
}).then(function () {
return User.create(record).catch(constraintTest.exception, spy);
}).then(function () {
expect(spy).to.have.been.calledOnce;
});
return this.sequelize.sync({ force: true }).bind(this).then(function () {
return User.create({ first_name: 'jan', last_name: 'meier' });
}).then(function () {
return User.create({ first_name: 'jan', last_name: 'meier' }).catch(this.sequelize.UniqueConstraintError, spy);
}).then(function () {
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!