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

Commit df180435 by Mick Hansen

Merge pull request #1841 from overlookmotel/hooks2

Changes to hooks
2 parents 8df7e100 015aa1a5
...@@ -455,7 +455,7 @@ module.exports = (function() { ...@@ -455,7 +455,7 @@ module.exports = (function() {
if (Object(association.through) === association.through) { if (Object(association.through) === association.through) {
// Create the related model instance // Create the related model instance
return association.target.create(values, fieldsOrOptions).then(function(newAssociatedObject) { return association.target.create(values, fieldsOrOptions).then(function(newAssociatedObject) {
return instance[association.accessors.add](newAssociatedObject, options).return (newAssociatedObject); return instance[association.accessors.add](newAssociatedObject, options).return(newAssociatedObject);
}); });
} else { } else {
values[association.identifier] = instance.get(association.source.primaryKeyAttribute); values[association.identifier] = instance.get(association.source.primaryKeyAttribute);
......
...@@ -174,7 +174,7 @@ InstanceValidator.prototype.hookValidate = function() { ...@@ -174,7 +174,7 @@ InstanceValidator.prototype.hookValidate = function() {
}); });
}).then(function() { }).then(function() {
return self.modelInstance.Model.runHooks('afterValidate', self.modelInstance); return self.modelInstance.Model.runHooks('afterValidate', self.modelInstance);
}).return (self.modelInstance); }).return(self.modelInstance);
}; };
/** /**
...@@ -223,7 +223,7 @@ InstanceValidator.prototype._customValidators = function() { ...@@ -223,7 +223,7 @@ InstanceValidator.prototype._customValidators = function() {
var valprom = self._invokeCustomValidator(validator, validatorType) var valprom = self._invokeCustomValidator(validator, validatorType)
// errors are handled in settling, stub this // errors are handled in settling, stub this
.catch (noop); .catch(noop);
validators.push(valprom); validators.push(valprom);
}); });
...@@ -302,10 +302,10 @@ InstanceValidator.prototype._invokeCustomValidator = Promise.method(function(val ...@@ -302,10 +302,10 @@ InstanceValidator.prototype._invokeCustomValidator = Promise.method(function(val
validatorFunction = Promise.promisify(validator.bind(this.modelInstance)); validatorFunction = Promise.promisify(validator.bind(this.modelInstance));
} }
return validatorFunction() return validatorFunction()
.catch (this._pushError.bind(this, false, errorKey)); .catch(this._pushError.bind(this, false, errorKey));
} else { } else {
return Promise.try(validator.bind(this.modelInstance, invokeArgs)) return Promise.try(validator.bind(this.modelInstance, invokeArgs))
.catch (this._pushError.bind(this, false, errorKey)); .catch(this._pushError.bind(this, false, errorKey));
} }
}); });
......
...@@ -4,6 +4,7 @@ var Utils = require('./utils') ...@@ -4,6 +4,7 @@ var Utils = require('./utils')
, Mixin = require('./associations/mixin') , Mixin = require('./associations/mixin')
, InstanceValidator = require('./instance-validator') , InstanceValidator = require('./instance-validator')
, DataTypes = require('./data-types') , DataTypes = require('./data-types')
, Promise = require("./promise")
, _ = require('lodash') , _ = require('lodash')
, defaultsOptions = { raw: true }; , defaultsOptions = { raw: true };
...@@ -415,7 +416,9 @@ module.exports = (function() { ...@@ -415,7 +416,9 @@ module.exports = (function() {
fieldsOrOptions = { fields: fieldsOrOptions }; fieldsOrOptions = { fields: fieldsOrOptions };
} }
options = Utils._.extend({}, options, fieldsOrOptions); options = Utils._.extend({
hooks: true
}, options, fieldsOrOptions);
if (!options.fields) { if (!options.fields) {
options.fields = Object.keys(this.Model.attributes); options.fields = Object.keys(this.Model.attributes);
...@@ -450,8 +453,11 @@ module.exports = (function() { ...@@ -450,8 +453,11 @@ module.exports = (function() {
options.fields.push(createdAtAttr); options.fields.push(createdAtAttr);
} }
return self.hookValidate({ return Promise.try(function() {
skip: _.difference(Object.keys(self.rawAttributes), options.fields) // Validate
if (options.hooks) {
return self.hookValidate({skip: _.difference(Object.keys(self.rawAttributes), options.fields)});
}
}).then(function() { }).then(function() {
options.fields.forEach(function(field) { options.fields.forEach(function(field) {
if (self.dataValues[field] !== undefined) { if (self.dataValues[field] !== undefined) {
...@@ -488,7 +494,8 @@ module.exports = (function() { ...@@ -488,7 +494,8 @@ module.exports = (function() {
&& !!self.Model.rawAttributes[updatedAtAttr].defaultValue && !!self.Model.rawAttributes[updatedAtAttr].defaultValue
) )
? self.Model.rawAttributes[updatedAtAttr].defaultValue ? self.Model.rawAttributes[updatedAtAttr].defaultValue
: Utils.now(self.sequelize.options.dialect)); : Utils.now(self.sequelize.options.dialect)
);
} }
if (self.isNewRecord && createdAtAttr && !values[createdAtAttr]) { if (self.isNewRecord && createdAtAttr && !values[createdAtAttr]) {
...@@ -498,8 +505,9 @@ module.exports = (function() { ...@@ -498,8 +505,9 @@ module.exports = (function() {
&& !!self.Model.rawAttributes[createdAtAttr].defaultValue && !!self.Model.rawAttributes[createdAtAttr].defaultValue
) )
? self.Model.rawAttributes[createdAtAttr].defaultValue ? self.Model.rawAttributes[createdAtAttr].defaultValue
: Utils.now(self.sequelize.options.dialect)); : Utils.now(self.sequelize.options.dialect)
} );
}
var query = null var query = null
, args = [] , args = []
...@@ -534,26 +542,30 @@ module.exports = (function() { ...@@ -534,26 +542,30 @@ module.exports = (function() {
// Add the values to the Instance // Add the values to the Instance
self.dataValues = _.extend(self.dataValues, values); self.dataValues = _.extend(self.dataValues, values);
return self.Model.runHooks('before' + hook, self).then(function() { return Promise.try(function() {
// dataValues might have changed inside the hook, rebuild // Run before hook
// the values hash if (options.hooks) {
values = {}; return self.Model.runHooks('before' + hook, self).then(function() {
// dataValues might have changed inside the hook, rebuild the values hash
options.fields.forEach(function(attr) { values = {};
if (self.dataValues[attr] !== undefined) {
values[attr] = self.dataValues[attr]; options.fields.forEach(function(attr) {
} if (self.dataValues[attr] !== undefined) {
values[attr] = self.dataValues[attr];
// Field name mapping }
if (self.Model.rawAttributes[attr].field) {
values[self.Model.rawAttributes[attr].field] = values[attr]; // Field name mapping
delete values[attr]; if (self.Model.rawAttributes[attr].field) {
} values[self.Model.rawAttributes[attr].field] = values[attr];
}); delete values[attr];
}
args[2] = values; });
return self.QueryInterface[query].apply(self.QueryInterface, args).catch (function(err) { args[2] = values;
});
}
}).then(function() {
return self.QueryInterface[query].apply(self.QueryInterface, args).catch(function(err) {
if (!!self.__options.uniqueKeys && err.code && self.QueryInterface.QueryGenerator.uniqueConstraintMapping.code === err.code) { if (!!self.__options.uniqueKeys && err.code && self.QueryInterface.QueryGenerator.uniqueConstraintMapping.code === err.code) {
var fields = self.QueryInterface.QueryGenerator.uniqueConstraintMapping.map(err.toString()); var fields = self.QueryInterface.QueryGenerator.uniqueConstraintMapping.map(err.toString());
...@@ -568,15 +580,20 @@ module.exports = (function() { ...@@ -568,15 +580,20 @@ module.exports = (function() {
} }
throw err; throw err;
}).then(function(result) { }).tap(function(result) {
// Transfer database generated values (defaults, autoincrement, etc) // Transfer database generated values (defaults, autoincrement, etc)
values = _.extend(values, result.dataValues); values = _.extend(values, result.dataValues);
// Ensure new values are on Instance, and reset previousDataValues // Ensure new values are on Instance, and reset previousDataValues
result.dataValues = _.extend(result.dataValues, values); result.dataValues = _.extend(result.dataValues, values);
result._previousDataValues = _.clone(result.dataValues); result._previousDataValues = _.clone(result.dataValues);
}).tap(function(result) {
return self.Model.runHooks('after' + hook, result).return (result); // Run before hook
if (options.hooks) {
return self.Model.runHooks('after' + hook, result);
}
}).then(function(result) {
return result;
}); });
}); });
}); });
...@@ -604,7 +621,7 @@ module.exports = (function() { ...@@ -604,7 +621,7 @@ module.exports = (function() {
include: this.options.include || null include: this.options.include || null
}, options).then(function(reload) { }, options).then(function(reload) {
self.set(reload.dataValues, {raw: true, reset: true}); self.set(reload.dataValues, {raw: true, reset: true});
}).return (self); }).return(self);
}; };
/* /*
...@@ -660,24 +677,37 @@ module.exports = (function() { ...@@ -660,24 +677,37 @@ module.exports = (function() {
* @return {Promise<undefined>} * @return {Promise<undefined>}
*/ */
Instance.prototype.destroy = function(options) { Instance.prototype.destroy = function(options) {
options = options || {}; options = Utils._.extend({
options.force = options.force === undefined ? false : Boolean(options.force); hooks: true,
force: false
}, options || {});
var self = this; var self = this;
// This semi awkward syntax where we can't return the chain directly but have to return the last .then() call is to allow sql proxying // This semi awkward syntax where we can't return the chain directly but have to return the last .then() call is to allow sql proxying
return self.Model.runHooks(self.Model.options.hooks.beforeDestroy, self).then(function() { return Promise.try(function() {
// Run before hook
if (options.hooks) {
return self.Model.runHooks('beforeDestroy', self);
}
}).then(function() {
var identifier; var identifier;
if (self.Model._timestampAttributes.deletedAt && options.force === false) { if (self.Model._timestampAttributes.deletedAt && options.force === false) {
self.dataValues[self.Model._timestampAttributes.deletedAt] = new Date(); self.dataValues[self.Model._timestampAttributes.deletedAt] = new Date();
options.hooks = false;
return self.save(options); return self.save(options);
} else { } else {
identifier = self.__options.hasPrimaryKeys ? self.primaryKeyValues : { id: self.id }; identifier = self.__options.hasPrimaryKeys ? self.primaryKeyValues : { id: self.id };
return self.QueryInterface.delete(self, self.QueryInterface.QueryGenerator.addSchema(self.Model), identifier, options); return self.QueryInterface.delete(self, self.QueryInterface.QueryGenerator.addSchema(self.Model), identifier, options);
} }
}).then(function(results) { }).tap(function(result) {
return self.Model.runHooks(self.Model.options.hooks.afterDestroy, self).return (results); // Run after hook
if (options.hooks) {
return self.Model.runHooks('afterDestroy', self);
}
}).then(function(result) {
return result;
}); });
}; };
......
...@@ -8,6 +8,7 @@ var Utils = require('./utils') ...@@ -8,6 +8,7 @@ var Utils = require('./utils')
, sql = require('sql') , sql = require('sql')
, SqlString = require('./sql-string') , SqlString = require('./sql-string')
, Transaction = require('./transaction') , Transaction = require('./transaction')
, Promise = require("./promise")
, QueryTypes = require('./query-types'); , QueryTypes = require('./query-types');
module.exports = (function() { module.exports = (function() {
...@@ -387,10 +388,10 @@ module.exports = (function() { ...@@ -387,10 +388,10 @@ module.exports = (function() {
if (options.force) { if (options.force) {
return self.drop(options).then(function() { return self.drop(options).then(function() {
return doQuery().return (self); return doQuery().return(self);
}); });
} else { } else {
return doQuery().return (this); return doQuery().return(this);
} }
}; };
...@@ -708,7 +709,7 @@ module.exports = (function() { ...@@ -708,7 +709,7 @@ module.exports = (function() {
// no options defined? // no options defined?
// return an emitter which emits null // return an emitter which emits null
if ([null, undefined].indexOf(options) !== -1) { if ([null, undefined].indexOf(options) !== -1) {
return Utils.Promise.resolve(null); return Promise.resolve(null);
} }
var primaryKeys = this.primaryKeys var primaryKeys = this.primaryKeys
...@@ -1046,11 +1047,11 @@ module.exports = (function() { ...@@ -1046,11 +1047,11 @@ module.exports = (function() {
var build = self.build(params); var build = self.build(params);
return build.hookValidate({skip: Object.keys(params)}).then(function() { return build.hookValidate({skip: Object.keys(params)}).then(function() {
return Utils.Promise.resolve([build, true]); return Promise.resolve([build, true]);
}); });
} }
return Utils.Promise.resolve([instance, false]); return Promise.resolve([instance, false]);
}); });
}; };
...@@ -1090,11 +1091,11 @@ module.exports = (function() { ...@@ -1090,11 +1091,11 @@ module.exports = (function() {
} }
return self.create(values, options).then(function(instance) { return self.create(values, options).then(function(instance) {
return Utils.Promise.resolve([instance, true]); return Promise.resolve([instance, true]);
}); });
} }
return Utils.Promise.resolve([instance, false]); return Promise.resolve([instance, false]);
}); });
}; };
...@@ -1105,11 +1106,12 @@ module.exports = (function() { ...@@ -1105,11 +1106,12 @@ module.exports = (function() {
* and SQLite do not make it easy to obtain back automatically generated IDs and other default values in a way that can be mapped to multiple records. * and SQLite do not make it easy to obtain back automatically generated IDs and other default values in a way that can be mapped to multiple records.
* To obtain Instances for the newly created values, you will need to query for them again. * To obtain Instances for the newly created values, you will need to query for them again.
* *
* @param {Array} records List of objects (key/value pairs) to create instances from * @param {Array} records List of objects (key/value pairs) to create instances from
* @param {Object} [options] * @param {Object} [options]
* @param {Array} [options.fields] Fields to insert (defaults to all fields) * @param {Array} [options.fields] Fields to insert (defaults to all fields)
* @param {Boolean} [options.validate=false] Should each row be subject to validation before it is inserted. The whole insert will fail if one row fails validation * @param {Boolean} [options.validate=false] Should each row be subject to validation before it is inserted. The whole insert will fail if one row fails validation
* @param {Boolean} [options.hooks=false] Run before / after create hooks for each individual Instance? BulkCreate hooks will still be run. * @param {Boolean} [options.hooks=true] Run before / after bulk create hooks?
* @param {Boolean} [options.individualHooks=false] Run before / after create hooks for each individual Instance? BulkCreate hooks will still be run if options.hooks is true.
* @param {Boolean} [options.ignoreDuplicates=false] Ignore duplicate values for primary keys? (not supported by postgres) * @param {Boolean} [options.ignoreDuplicates=false] Ignore duplicate values for primary keys? (not supported by postgres)
* *
* @return {Promise<Array<Instance>>} * @return {Promise<Array<Instance>>}
...@@ -1119,16 +1121,15 @@ module.exports = (function() { ...@@ -1119,16 +1121,15 @@ module.exports = (function() {
Utils.validateParameter(options, 'undefined', { deprecated: Object, optional: true, index: 3, method: 'Model#bulkCreate' }); Utils.validateParameter(options, 'undefined', { deprecated: Object, optional: true, index: 3, method: 'Model#bulkCreate' });
if (!records.length) { if (!records.length) {
return new Utils.Promise(function(resolve) { return Promise.resolve([]);
resolve([]);
});
} }
options = Utils._.extend({ options = Utils._.extend({
validate: false, validate: false,
hooks: false, hooks: true,
individualHooks: false,
ignoreDuplicates: false ignoreDuplicates: false
}, options ||  {}); }, options || {});
if (fieldsOrOptions instanceof Array) { if (fieldsOrOptions instanceof Array) {
options.fields = fieldsOrOptions; options.fields = fieldsOrOptions;
...@@ -1138,298 +1139,330 @@ module.exports = (function() { ...@@ -1138,298 +1139,330 @@ module.exports = (function() {
} }
if (this.sequelize.options.dialect === 'postgres' && options.ignoreDuplicates) { if (this.sequelize.options.dialect === 'postgres' && options.ignoreDuplicates) {
return Utils.Promise.reject(new Error('Postgres does not support the \'ignoreDuplicates\' option.')); return Promise.reject(new Error('Postgres does not support the \'ignoreDuplicates\' option.'));
} }
var self = this var self = this
, updatedAtAttr = this._timestampAttributes.updatedAt
, createdAtAttr = this._timestampAttributes.createdAt , createdAtAttr = this._timestampAttributes.createdAt
, errors = [] , updatedAtAttr = this._timestampAttributes.updatedAt
, daoPromises = [] , now = Utils.now(self.modelManager.sequelize.options.dialect);
, daos = records.map(function(values) {
return self.build(values, { // build DAOs
isNewRecord: true var daos = records.map(function(values) {
return self.build(values, {isNewRecord: true});
});
return Promise.try(function() {
// Run before hook
if (options.hooks) {
return self.runHooks('beforeBulkCreate', daos, options.fields).spread(function(_daos, _fields) {
daos = _daos || daos;
options.fields = _fields || options.fields;
});
}
}).then(function() {
daos.forEach(function(dao) {
// Filter dataValues by options.fields
var values = {};
options.fields.forEach(function(field) {
values[field] = dao.dataValues[field];
}); });
});
if (options.validate && options.fields.length) { // set createdAt/updatedAt attributes
var skippedFields = Utils._.difference(Object.keys(self.attributes), options.fields); if (createdAtAttr && !values[createdAtAttr]) {
} values[createdAtAttr] = now;
}
if (updatedAtAttr && !values[updatedAtAttr]) {
values[updatedAtAttr] = now;
}
var runAfterCreate = function() { dao.dataValues = values;
return self.runHooks('afterBulkCreate', daos, options.fields).spread(function(newRecords) {
return new self.sequelize.Promise.resolve(newRecords || daos);
}); });
};
return self.runHooks('beforeBulkCreate', daos, options.fields).spread(function(newRecords, newFields) { // Validate
daos = newRecords || daos; if (options.validate) {
options.fields = newFields || options.fields; var skippedFields = Utils._.difference(Object.keys(self.attributes), options.fields);
var runHook = function(dao) { var errors = [];
if (options.hooks === false) { return Promise.map(daos, function(dao) {
var values = options.fields.length > 0 ? {} : dao.dataValues; var fn = options.individualHooks ? 'hookValidate' : 'validate';
return dao[fn]({skip: skippedFields}).then(function(err) {
options.fields.forEach(function(field) { if (!!err) {
values[field] = dao.dataValues[field]; errors.push({record: dao, errors: err});
}
}); });
}).then(function() {
if (createdAtAttr && !values[createdAtAttr]) { if (errors.length) {
values[createdAtAttr] = Utils.now(self.modelManager.sequelize.options.dialect); return Promise.reject(errors);
}
if (updatedAtAttr && !values[updatedAtAttr]) {
values[updatedAtAttr] = Utils.now(self.modelManager.sequelize.options.dialect);
} }
records.push(values);
return values;
}
return self.runHooks('beforeCreate', dao).spread(function(newValues) {
dao = newValues || dao;
return dao.save({ transaction: options.transaction }).then(function() {
return self.runHooks('afterCreate', dao);
});
}); });
}; }
}).then(function() {
var runValidation = function(dao) { if (options.individualHooks) {
if (options.validate === false) { // Create each dao individually
return dao; return Promise.map(daos, function(dao) {
} return dao.save({transaction: options.transaction});
}).then(function(_daos) {
var fn = options.hooks === true ? 'hookValidate' : 'validate'; daos = _daos;
return dao[fn]({skip: skippedFields}).then(function(err) { });
if (!!err) { } else {
errors.push({record: dao, errors: err}); // Create all in one query
} // Recreate records from daos to represent any changes made in hooks or validation
records = daos.map(function(dao) {
return dao.dataValues;
}); });
};
records = [];
daos.forEach(function(dao) {
daoPromises.push(runValidation(dao));
daoPromises.push(runHook(dao));
});
return self.sequelize.Promise.all(daoPromises).then(function() { // Map field names
if (errors.length) { records.forEach(function(values) {
// Validation or hooks failed for (var attr in values) {
return self.sequelize.Promise.reject(errors); if (values.hasOwnProperty(attr)) {
} else if (records.length) { if (self.rawAttributes[attr].field) {
// Map field names values[self.rawAttributes[attr].field] = values[attr];
records.forEach(function(values) { delete values[attr];
for (var attr in values) {
if (values.hasOwnProperty(attr)) {
if (self.rawAttributes[attr].field) {
values[self.rawAttributes[attr].field] = values[attr];
delete values[attr];
}
} }
} }
});
// Map attributes for serial identification
var attributes = {};
for (var attr in self.rawAttributes) {
attributes[attr] = self.rawAttributes[attr];
if (self.rawAttributes[attr].field) {
attributes[self.rawAttributes[attr].field] = self.rawAttributes[attr];
}
} }
});
// Insert all records at once // Map attributes for serial identification
return self.QueryInterface.bulkInsert(self.getTableName(), records, options, attributes).then(runAfterCreate); var attributes = {};
} else { for (var attr in self.rawAttributes) {
// Records were already saved while running create / update hooks attributes[attr] = self.rawAttributes[attr];
return runAfterCreate(); if (self.rawAttributes[attr].field) {
attributes[self.rawAttributes[attr].field] = self.rawAttributes[attr];
}
} }
});
}); // Insert all records at once
return self.QueryInterface.bulkInsert(self.getTableName(), records, options, attributes);
}
}).then(function() {
// Run after hook
if (options.hooks) {
return self.runHooks('afterBulkCreate', daos, options.fields).spread(function(_daos) {
if (_daos) daos = _daos;
});
}
}).then(function() {
return daos;
});
}; };
/** /**
* Delete multiple instances * Delete multiple instances
* *
* @param {Object} [where] Options to describe the scope of the search. * @param {Object} [where] Options to describe the scope of the search.
* @param {Object} [options] * @param {Object} [options]
* @param {Boolean} [options.hooks] If set to true, destroy will find all records within the where parameter and will execute before-/ after bulkDestroy hooks on each row * @param {Boolean} [options.hooks=true] Run before / after bulk destroy hooks?
* @param {Number} [options.limit] How many rows to delete * @param {Boolean} [options.individualHooks=false] If set to true, destroy will find all records within the where parameter and will execute before / after bulkDestroy hooks on each row
* @param {Boolean} [options.truncate] If set to true, dialects that support it will use TRUNCATE instead of DELETE FROM. If a table is truncated the where and limit options are ignored * @param {Number} [options.limit] How many rows to delete
* @param {Boolean} [options.truncate] If set to true, dialects that support it will use TRUNCATE instead of DELETE FROM. If a table is truncated the where and limit options are ignored
* *
* @return {Promise<undefined>} * @return {Promise<undefined>}
*/ */
Model.prototype.destroy = function(where, options) { Model.prototype.destroy = function(where, options) {
options = options || {}; options = Utils._.extend({
options.force = options.force === undefined ? false : Boolean(options.force); hooks: true,
individualHooks: false,
force: false
}, options || {});
options.type = QueryTypes.BULKDELETE; options.type = QueryTypes.BULKDELETE;
var self = this var self = this
, query = null , daos;
, args = [];
return self.runHooks(self.options.hooks.beforeBulkDestroy, where).then(function(newWhere) {
where = newWhere || where;
if (self._timestampAttributes.deletedAt && options.force === false) { return Promise.try(function() {
// Run before hook
if (options.hooks) {
return self.runHooks('beforeBulkDestroy', where).spread(function(_where) {
where = _where || where;
});
}
}).then(function() {
// Get daos and run beforeDestroy hook on each record individually
if (options.individualHooks) {
return self.all({where: where}, {transaction: options.transaction}).map(function(dao) {
return self.runHooks('beforeDestroy', dao).spread(function(_dao) {
return _dao || dao;
});
}).then(function(_daos) {
daos = _daos;
});
}
}).then(function() {
// Run delete query (or update if paranoid)
if (self._timestampAttributes.deletedAt && !options.force) {
var attrValueHash = {}; var attrValueHash = {};
attrValueHash[self._timestampAttributes.deletedAt] = Utils.now(); attrValueHash[self._timestampAttributes.deletedAt] = Utils.now(self.modelManager.sequelize.options.dialect);
query = 'bulkUpdate'; return self.QueryInterface.bulkUpdate(self.getTableName(), attrValueHash, where, options, self.rawAttributes);
args = [self.getTableName(), attrValueHash, where, self];
} else { } else {
query = 'bulkDelete'; return self.QueryInterface.bulkDelete(self.getTableName(), where, options, self);
args = [self.getTableName(), where, options, self];
} }
}).tap(function() {
var runQuery = function(records) { // Run afterDestroy hook on each record individually
return self.QueryInterface[query].apply(self.QueryInterface, args).then(function(results) { if (options.individualHooks) {
if (options && options.hooks === true) { return Promise.map(daos, function(dao) {
var tick = 0; return self.runHooks('afterDestroy', dao);
var next = function(i) {
return self.runHooks(self.options.hooks.afterDestroy, records[i]).then(function(newValues) {
records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues;
tick++;
if (tick >= records.length) {
return self.runHooks(self.options.hooks.afterBulkDestroy, where).return (results);
}
return next(tick);
});
};
return next(tick);
} else {
return self.runHooks(self.options.hooks.afterBulkDestroy, where).return (results);
}
});
};
if (options && options.hooks === true) {
var tick = 0;
return self.all({where: where}).then(function(records) {
var next = function(i) {
return self.runHooks(self.options.hooks.beforeDestroy, records[i]).then(function(newValues) {
records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues;
tick++;
if (tick >= records.length) {
return runQuery(records);
}
return next(tick);
});
};
return next(tick);
}); });
} else {
return runQuery();
} }
}).tap(function() {
// Run after hook
if (options.hooks) {
return self.runHooks('afterBulkDestroy', where);
}
}).then(function(affectedRows) {
return affectedRows;
}); });
}; };
/** /**
* Update multiple instances that match the where options. * Update multiple instances that match the where options.
* *
* @param {Object} attrValueHash A hash of fields to change and their new values * @param {Object} attrValueHash A hash of fields to change and their new values
* @param {Object where Options to describe the scope of the search. Note that these options are not wrapped in a { where: ... } is in find / findAll calls etc. This is probably due to change in 2.0 * @param {Object where Options to describe the scope of the search. Note that these options are not wrapped in a { where: ... } is in find / findAll calls etc. This is probably due to change in 2.0
* @param {Object} [options] * @param {Object} [options]
* @param {Boolean} [options.validate=true] Should each row be subject to validation before it is inserted. The whole insert will fail if one row fails validation * @param {Boolean} [options.validate=true] Should each row be subject to validation before it is inserted. The whole insert will fail if one row fails validation
* @param {Boolean} [options.hooks=false] Run before / after bulkUpdate hooks? * @param {Boolean} [options.hooks=true] Run before / after bulk update hooks?
* @param {Number} [options.limit] How many rows to update (only for mysql and mariadb) * @param {Boolean} [options.individualHooks=false] Run before / after update hooks?
* @param {Number} [options.limit] How many rows to update (only for mysql and mariadb)
* @deprecated The syntax is due for change, in order to make `where` more consistent with the rest of the API * @deprecated The syntax is due for change, in order to make `where` more consistent with the rest of the API
* *
* @return {Promise} * @return {Promise}
*/ */
Model.prototype.update = function(attrValueHash, where, options) { Model.prototype.update = function(attrValueHash, where, options) {
var self = this var self = this;
, tick = 0;
options = Utils._.extend({
validate: true,
hooks: true,
individualHooks: false,
force: false
}, options || {});
options = options || {};
options.validate = options.validate === undefined ? true : Boolean(options.validate);
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.type = QueryTypes.BULKUPDATE; options.type = QueryTypes.BULKUPDATE;
if (self._timestampAttributes.updatedAt) { if (self._timestampAttributes.updatedAt) {
attrValueHash[self._timestampAttributes.updatedAt] = Utils.now(); attrValueHash[self._timestampAttributes.updatedAt] = Utils.now(self.modelManager.sequelize.options.dialect);
} }
var runSave = function() { var daos
return self.runHooks(self.options.hooks.beforeBulkUpdate, attrValueHash, where).spread(function(attributes, _where) { , attrValueHashUse;
where = _where || where;
attrValueHash = attributes || attrValueHash;
var runQuery = function(records) {
return self.QueryInterface.bulkUpdate(self.getTableName(), attrValueHash, where, options, self.rawAttributes).then(function(results) {
if (options && options.hooks === true && !!records && records.length > 0) {
var tick = 0;
var next = function(i) {
return self.runHooks(self.options.hooks.afterUpdate, records[i]).then(function(newValues) {
records[i].dataValues = (!!newValues && newValues.dataValues) ? newValues.dataValues : records[i].dataValues;
tick++;
if (tick >= records.length) {
return self.runHooks(self.options.hooks.afterBulkUpdate, attrValueHash, where).return (records);
}
return next(tick);
});
};
return next(tick); return Promise.try(function() {
} else { // Validate
return self.runHooks(self.options.hooks.afterBulkUpdate, attrValueHash, where).return (results); if (options.validate) {
} var build = self.build(attrValueHash);
});
};
if (options.hooks === true) { // We want to skip validations for all other fields
return self.all({where: where}).then(function(records) { var skippedFields = Utils._.difference(Object.keys(self.attributes), Object.keys(attrValueHash));
if (records === null || records.length < 1) {
return runQuery();
}
var next = function(i) { return build.hookValidate({skip: skippedFields}).then(function(attributes) {
return self.runHooks(self.options.hooks.beforeUpdate, records[i]).then(function(newValues) { if (attributes && attributes.dataValues) {
records[i].dataValues = (!!newValues && newValues.dataValues) ? newValues.dataValues : records[i].dataValues; attrValueHash = Utils._.pick.apply(Utils._, [].concat(attributes.dataValues).concat(Object.keys(attrValueHash)));
tick++; }
});
}
}).then(function() {
// Run before hook
if (options.hooks) {
return self.runHooks('beforeBulkUpdate', attrValueHash, where).spread(function(_attrValueHash, _where) {
where = _where || where;
attrValueHash = _attrValueHash || attrValueHash;
});
}
}).then(function() {
attrValueHashUse = attrValueHash;
// Get daos and run beforeDestroy hook on each record individually
if (options.individualHooks) {
return self.all({where: where}, {transaction: options.transaction}).then(function(_daos) {
daos = _daos;
if (!daos.length) {
return [];
}
if (tick >= records.length) { // Run beforeUpdate hooks on each record and check whether beforeUpdate hook changes values uniformly
return runQuery(records); // i.e. whether they change values for each record in the same way
} var changedValues
, different = false;
return next(tick); return Promise.map(daos, function(dao) {
}); // Record updates in dao's dataValues
}; Utils._.extend(dao.dataValues, attrValueHash);
return next(tick); // Run beforeUpdate hook
}); return self.runHooks('beforeUpdate', dao).spread(function(_dao) {
} else { dao = _dao || dao;
return runQuery();
}
});
};
if (options.validate === true) { if (!different) {
var build = self.build(attrValueHash); var thisChangedValues = {};
Utils._.forIn(dao.dataValues, function(newValue, attr) {
if (newValue !== dao._previousDataValues[attr]) {
thisChangedValues[attr] = newValue;
}
});
// We want to skip validations for all other fields if (!changedValues) {
var updatedFields = Object.keys(attrValueHash); changedValues = thisChangedValues;
var skippedFields = Utils._.difference(Object.keys(self.attributes), updatedFields); } else {
different = !Utils._.isEqual(changedValues, thisChangedValues);
}
}
return build.hookValidate({skip: skippedFields}).then(function(attributes) { return dao;
if (!!attributes && !!attributes.dataValues) { });
attrValueHash = Utils._.pick.apply(Utils._, [].concat(attributes.dataValues).concat(Object.keys(attrValueHash))); }).then(function(_daos) {
} daos = _daos;
if (!different) {
// Hooks do not change values or change them uniformly
if (Object.keys(changedValues).length) {
// Hooks change values - record changes in attrValueHashUse so they are executed
attrValueHashUse = changedValues;
}
return;
} else {
// Hooks change values in a different way for each record
// Do not run original query but save each record individually
return Promise.map(daos, function(dao) {
return dao.save({transaction: options.transaction, hooks: false});
}).tap(function(_daos) {
daos = _daos;
});
}
});
});
}
}).then(function(results) {
if (results) {
// Update already done row-by-row - exit
return [results.length, results];
}
return runSave(); // Run query to update all rows
return self.QueryInterface.bulkUpdate(self.getTableName(), attrValueHashUse, where, options, self.rawAttributes).then(function(affectedRows) {
return [affectedRows];
}); });
} else { }).tap(function(result) {
return runSave(); if (options.individualHooks) {
} return Promise.map(daos, function(dao) {
return self.runHooks('afterUpdate', dao).spread(function(_dao) {
return _dao || dao;
});
}).then(function(_daos) {
result[1] = daos = _daos;
});
}
}).tap(function() {
// Run after hook
if (options.hooks) {
return self.runHooks('afterBulkUpdate', attrValueHash, where);
}
}).then(function(result) {
// Return result in form [affectedRows, daos] (daos missed off if options.individualHooks != true)
return result;
});
}; };
/** /**
......
...@@ -594,7 +594,7 @@ module.exports = (function() { ...@@ -594,7 +594,7 @@ module.exports = (function() {
* @return {Promise} * @return {Promise}
*/ */
Sequelize.prototype.authenticate = function() { Sequelize.prototype.authenticate = function() {
return this.query('SELECT 1+1 AS result', null, { raw: true, plain: true }).return ().catch (function(err) { return this.query('SELECT 1+1 AS result', null, { raw: true, plain: true }).return().catch(function(err) {
throw new Error(err); throw new Error(err);
}); });
}; };
......
...@@ -791,15 +791,13 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -791,15 +791,13 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
, done = _.after(2, _done) , done = _.after(2, _done)
this.User.bulkCreate(data).success(function() { this.User.bulkCreate(data).success(function() {
self.User.update({username: 'Bill'}, {secretValue: '42'}).done(function(err, affectedRows) { self.User.update({username: 'Bill'}, {secretValue: '42'}).spread(function(affectedRows) {
expect(err).not.to.be.ok
expect(affectedRows).to.equal(2) expect(affectedRows).to.equal(2)
done() done()
}) })
self.User.update({username: 'Bill'}, {secretValue: '44'}).done(function(err, affectedRows) { self.User.update({username: 'Bill'}, {secretValue: '44'}).spread(function(affectedRows) {
expect(err).not.to.be.ok
expect(affectedRows).to.equal(0) expect(affectedRows).to.equal(0)
done() done()
...@@ -815,8 +813,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -815,8 +813,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
{ username: 'Peter', secretValue: '42' }] { username: 'Peter', secretValue: '42' }]
this.User.bulkCreate(data).success(function () { this.User.bulkCreate(data).success(function () {
self.User.update({secretValue: '43'}, {username: 'Peter'}, {limit: 1}).done(function (err, affectedRows) { self.User.update({secretValue: '43'}, {username: 'Peter'}, {limit: 1}).spread(function(affectedRows) {
expect(err).not.to.be.ok
expect(affectedRows).to.equal(1) expect(affectedRows).to.equal(1)
done() done()
}) })
......
...@@ -103,7 +103,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { ...@@ -103,7 +103,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () {
this.User.bulkCreate([ this.User.bulkCreate([
{username: 'Bob', mood: 'cold'}, {username: 'Bob', mood: 'cold'},
{username: 'Tobi', mood: 'hot'} {username: 'Tobi', mood: 'hot'}
], { fields: [], hooks: true }).success(function(bulkUsers) { ], { fields: [], individualHooks: true }).success(function(bulkUsers) {
expect(beforeBulkCreate).to.be.true expect(beforeBulkCreate).to.be.true
expect(afterBulkCreate).to.be.true expect(afterBulkCreate).to.be.true
expect(bulkUsers).to.be.instanceof(Array) expect(bulkUsers).to.be.instanceof(Array)
...@@ -130,7 +130,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { ...@@ -130,7 +130,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () {
return this.User.bulkCreate([ return this.User.bulkCreate([
{username: 'Bob', mood: 'cold'}, {username: 'Bob', mood: 'cold'},
{username: 'Tobi', mood: 'hot'} {username: 'Tobi', mood: 'hot'}
], { fields: [], hooks: false }).success(function(bulkUsers) { ], { fields: [], individualHooks: false }).success(function(bulkUsers) {
return self.User.all().success(function(users) { return self.User.all().success(function(users) {
expect(users[0].mood).to.equal(null) expect(users[0].mood).to.equal(null)
expect(users[1].mood).to.equal(null) expect(users[1].mood).to.equal(null)
...@@ -288,7 +288,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { ...@@ -288,7 +288,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () {
this.User.bulkCreate([ this.User.bulkCreate([
{username: 'Bob', mood: 'cold'}, {username: 'Bob', mood: 'cold'},
{username: 'Tobi', mood: 'hot'} {username: 'Tobi', mood: 'hot'}
], { hooks: true }).success(function(bulkUsers) { ], { individualHooks: true }).success(function(bulkUsers) {
expect(beforeBulkCreate).to.be.true expect(beforeBulkCreate).to.be.true
expect(afterBulkCreate).to.be.true expect(afterBulkCreate).to.be.true
expect(bulkUsers).to.be.instanceof(Array) expect(bulkUsers).to.be.instanceof(Array)
...@@ -4343,7 +4343,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { ...@@ -4343,7 +4343,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () {
}) })
}) })
describe('with the {hooks: true} option', function() { describe('with the {individualHooks: true} option', function() {
beforeEach(function(done) { beforeEach(function(done) {
this.User = this.sequelize.define('User', { this.User = this.sequelize.define('User', {
username: { username: {
...@@ -4389,7 +4389,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { ...@@ -4389,7 +4389,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () {
fn() fn()
}) })
this.User.bulkCreate([{aNumber: 5}, {aNumber: 7}, {aNumber: 3}], { fields: ['aNumber'], hooks: true }).success(function(records) { this.User.bulkCreate([{aNumber: 5}, {aNumber: 7}, {aNumber: 3}], { fields: ['aNumber'], individualHooks: true }).success(function(records) {
records.forEach(function(record) { records.forEach(function(record) {
expect(record.username).to.equal('User' + record.id) expect(record.username).to.equal('User' + record.id)
expect(record.beforeHookTest).to.be.true expect(record.beforeHookTest).to.be.true
...@@ -4423,7 +4423,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { ...@@ -4423,7 +4423,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () {
fn() fn()
}) })
this.User.bulkCreate([{aNumber: 5}, {aNumber: 7}, {aNumber: 3}], { fields: ['aNumber'], hooks: true }).error(function(err) { this.User.bulkCreate([{aNumber: 5}, {aNumber: 7}, {aNumber: 3}], { fields: ['aNumber'], individualHooks: true }).error(function(err) {
expect(err).to.equal('You shall not pass!') expect(err).to.equal('You shall not pass!')
expect(beforeBulkCreate).to.be.true expect(beforeBulkCreate).to.be.true
expect(afterBulkCreate).to.be.false expect(afterBulkCreate).to.be.false
...@@ -5254,7 +5254,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { ...@@ -5254,7 +5254,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () {
}) })
}) })
describe('with the {hooks: true} option', function() { describe('with the {individualHooks: true} option', function() {
beforeEach(function(done) { beforeEach(function(done) {
this.User = this.sequelize.define('User', { this.User = this.sequelize.define('User', {
username: { username: {
...@@ -5301,11 +5301,10 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { ...@@ -5301,11 +5301,10 @@ describe(Support.getTestDialectTeaser("Hooks"), function () {
fn() fn()
}) })
this.User.bulkCreate([ this.User.bulkCreate([
{aNumber: 1}, {aNumber: 1}, {aNumber: 1} {aNumber: 1}, {aNumber: 1}, {aNumber: 1}
]).success(function() { ]).success(function() {
self.User.update({aNumber: 10}, {aNumber: 1}, {hooks: true}).success(function(records) { self.User.update({aNumber: 10}, {aNumber: 1}, {individualHooks: true}).spread(function(affectedRows, records) {
records.forEach(function(record) { records.forEach(function(record) {
expect(record.username).to.equal('User' + record.id) expect(record.username).to.equal('User' + record.id)
expect(record.beforeHookTest).to.be.true expect(record.beforeHookTest).to.be.true
...@@ -5342,7 +5341,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { ...@@ -5342,7 +5341,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () {
}) })
this.User.bulkCreate([{aNumber: 1}, {aNumber: 1}, {aNumber: 1}], { fields: ['aNumber'] }).success(function() { this.User.bulkCreate([{aNumber: 1}, {aNumber: 1}, {aNumber: 1}], { fields: ['aNumber'] }).success(function() {
self.User.update({aNumber: 10}, {aNumber: 1}, {hooks: true}).error(function(err) { self.User.update({aNumber: 10}, {aNumber: 1}, {individualHooks: true}).error(function(err) {
expect(err).to.equal('You shall not pass!') expect(err).to.equal('You shall not pass!')
expect(beforeBulk).to.be.true expect(beforeBulk).to.be.true
expect(afterBulk).to.be.false expect(afterBulk).to.be.false
...@@ -6038,7 +6037,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { ...@@ -6038,7 +6037,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () {
}) })
}) })
describe('with the {hooks: true} option', function() { describe('with the {individualHooks: true} option', function() {
beforeEach(function(done) { beforeEach(function(done) {
this.User = this.sequelize.define('User', { this.User = this.sequelize.define('User', {
username: { username: {
...@@ -6091,7 +6090,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { ...@@ -6091,7 +6090,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () {
this.User.bulkCreate([ this.User.bulkCreate([
{aNumber: 1}, {aNumber: 1}, {aNumber: 1} {aNumber: 1}, {aNumber: 1}, {aNumber: 1}
]).success(function() { ]).success(function() {
self.User.destroy({aNumber: 1}, {hooks: true}).success(function() { self.User.destroy({aNumber: 1}, {individualHooks: true}).success(function() {
expect(beforeBulk).to.be.true expect(beforeBulk).to.be.true
expect(afterBulk).to.be.true expect(afterBulk).to.be.true
expect(beforeHook).to.be.true expect(beforeHook).to.be.true
...@@ -6129,7 +6128,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { ...@@ -6129,7 +6128,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () {
}) })
this.User.bulkCreate([{aNumber: 1}, {aNumber: 1}, {aNumber: 1}], { fields: ['aNumber'] }).success(function() { this.User.bulkCreate([{aNumber: 1}, {aNumber: 1}, {aNumber: 1}], { fields: ['aNumber'] }).success(function() {
self.User.destroy({aNumber: 1}, {hooks: true}).error(function(err) { self.User.destroy({aNumber: 1}, {individualHooks: true}).error(function(err) {
expect(err).to.equal('You shall not pass!') expect(err).to.equal('You shall not pass!')
expect(beforeBulk).to.be.true expect(beforeBulk).to.be.true
expect(beforeHook).to.be.true expect(beforeHook).to.be.true
...@@ -7243,7 +7242,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { ...@@ -7243,7 +7242,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () {
return this.User.bulkCreate([ return this.User.bulkCreate([
{username: 'Bob', mood: 'cold'}, {username: 'Bob', mood: 'cold'},
{username: 'Tobi', mood: 'hot'} {username: 'Tobi', mood: 'hot'}
], { fields: [], hooks: false }).success(function(bulkUsers) { ], { fields: [], individualHooks: false }).success(function(bulkUsers) {
return self.User.all().success(function(users) { return self.User.all().success(function(users) {
expect(users[0].mood).to.equal(null) expect(users[0].mood).to.equal(null)
expect(users[1].mood).to.equal(null) expect(users[1].mood).to.equal(null)
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!