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

Commit 0299ce63 by Daniel Durante

Adds hooks / callbacks / lifecycle events

This commit adds the ability of hooks for the DAOFactory. The following
hooks (in their order of operations) are:

(1) beforeValidate(dao, fn)

(-) validate

(2) afterValidate(dao, fn)

(3) beforeBulkCreate(daos, fields, fn)
    beforeBulkDestroy(daos, fields, fn)
    beforeBulkUpdate(daos, fields, fn)

(4) beforeCreate(dao, fn)
    beforeDestroy(dao, fn)
    beforeUpdate(dao, fn)

(-) create / destroy / update

(5) afterCreate(dao, fn)
    aftreDestroy(dao, fn)
    afterUpdate(dao, fn)

(6) afterBulkCreate(daos, fields, fn)
    afterBulkDestory(daos, fields, fn)
    afterBulkUpdate(daos, fields, fn)

There's a new file called hooks.js which works very similar to
mixins.js which just extends a prototype.

You can add the hooks like so...

... via .define():

var User = sequelize.define('User', {
  username: DataTypes.STRING,
  mood: {
    type: DataTypes.ENUM,
    values: ['happy', 'sad', 'neutral']
  }
}, {
  hooks: {
    beforeValidate: function(user, fn) {
      user.mood = 'happy'
      fn(null, user)
    },
    afterValidate: function(user, fn) {
      user.username = 'Toni'
      fn(null, user)
    }
  }
})

... via .hook() method

var User = sequelize.define('User', {
  username: DataTypes.STRING,
  mood: {
    type: DataTypes.ENUM,
    values: ['happy', 'sad', 'neutral']
  }
})

User.hook('beforeValidate', function(user, fn) {
  user.mood = 'happy'
  fn(null, user)
})

User.hook('afterValidate', function(user, fn) {
  user.username = 'Toni'
  fn(null, user)
})

... via direct method:

var User = sequelize.define('User', {
  username: DataTypes.STRING,
  mood: {
    type: DataTypes.ENUM,
    values: ['happy', 'sad', 'neutral']
  }
})

User.beforeValidate(function(user, fn) {
  user.mood = 'happy'
  fn(null, user)
})

User.afterValidate(function(user, fn) {
  user.username = 'Toni'
  fn(null, user)
})

Quick example:

User.beforeCreate(function(user, fn) {
  if (user.accessLevel > 10 && user.username !== "Boss") {
    return fn("You can't grant this user that level!")
  }

  return fn()
})

User.create({
  username: 'Not a Boss',
  accessLevel: 20
}).error(function(err) {
  console.log(err) // You can't grant this user that level!
})

As of right now, each hook will process in the order they where
implemented / added to the factory.

To invoke the hooks simply run...

Model.runHooks.call(Model.options.hooks.<hook>, <args>, <callback>)

Some model hooks have two or three paramters sent to each hook
depending on it's type.

Model.beforeBulkCreate(function(records, fields, fn) {
  // records = the first argument sent to .bulkCreate
  // fields = the second argument sent to .bulkCreate
})

Model.bulkCreate([
  {username: 'Toni'}, // part of records argument
  {username: 'Tobi'} // part of records argument
], ['username'] /* part of fields argument */)

Model.beforeBulkUpdate(function(attributes, where, fn) {
  // attributes = first argument sent to Model.update
  // where = second argument sent to Model.update
})

Model.update(
  {gender: 'Male'} /*attribures argument*/,
  {username: 'Tom'} /*where argument*/
)

Model.beforeBulkDestroy(function(whereClause, fn) {
  // whereClause = first argument sent to Model.destroy
})

Model.destroy({username: 'Tom'} /*whereClause argument*/)

For 1.7.x backwards compatibility, I've added a new method called
.hookValidate() since .validate() is a synchronous function. All of
Sequelize's API functions will invoke .hookValidate(), but if you
utilize the .validate() function outside of Sequelize then you'll
need to update your code if you want to run before/afterValidate hooks.

Sequelzie 2.0.x will not need this change simply because it's
.validate() method is already asynchronous. However, it will have the
.hookValdate() function in order to make the transition from 1.7 to 2.0
smoother and easier. Eventually we'll want to deprecate this function.

In addition to this commit, I've also completed the following tasks:

Move validation of enum attribute value to validate method

I had to complete that task in order to get the validate hooks
working properly.

And the last thing, I fixed executables.test.js if your DB didn't use
the default values for config/config.js, this was causing errors for me
on my local machine.
1 parent 4339799d
......@@ -33,6 +33,7 @@ changelog of the branch: https://github.com/sequelize/sequelize/blob/milestones/
- Associations
- Importing definitions from single files
- Promises
- Hooks/callbacks/lifecycle events
## Documentation and Updates ##
......@@ -62,7 +63,7 @@ A very basic roadmap. Chances aren't too bad, that not mentioned things are impl
- Support for update of tables without primary key
- MariaDB support
- ~~Support for update and delete calls for whole tables without previous loading of instances~~ Implemented in [#569](https://github.com/sequelize/sequelize/pull/569) thanks to @optiltude
- Eager loading of nested associations [#388](https://github.com/sdepold/sequelize/issues/388#issuecomment-12019099)
- Eager loading of nested associations [#388](https://github.com/sequelize/sequelize/issues/388)
- ~~Model#delete~~ (renamed to [Model.destroy()](http://sequelizejs.com/documentation#instances-destroy))
- ~~Validate a model before it gets saved.~~ Implemented in [#601](https://github.com/sequelize/sequelize/pull/601), thanks to @durango
- Move validation of enum attribute value to validate method
......
......@@ -14,6 +14,8 @@ module.exports = (function() {
this.options.foreignKey = Utils._.underscoredIf(Utils.singularize(this.options.as, this.source.options.language) + "Id", this.source.options.underscored)
}
this.options.useHooks = options.useHooks
this.associationAccessor = this.isSelfAssociation
? Utils.combineTableNames(this.target.tableName, this.options.as || this.target.tableName)
: this.options.as || this.target.tableName
......@@ -31,7 +33,7 @@ module.exports = (function() {
Utils._.defaults(this.source.rawAttributes, newAttributes)
// Sync attributes to DAO proto each time a new assoc is added
this.source.DAO.prototype.attributes = Object.keys(this.source.DAO.prototype.rawAttributes);
this.source.DAO.prototype.attributes = Object.keys(this.source.DAO.prototype.rawAttributes)
return this
}
......
......@@ -30,7 +30,7 @@ module.exports = (function() {
HasManySingleLinked.prototype.injectSetter = function(emitter, oldAssociations, newAssociations) {
var self = this
, associationKeys = Object.keys((oldAssociations[0] || newAssociations[0] || {}).daoFactory.primaryKeys || {})
, associationKeys = Object.keys((oldAssociations[0] || newAssociations[0] || {daoFactory: {primaryKeys: {}}}).daoFactory.primaryKeys || {})
, associationKey = associationKeys.length === 1 ? associationKeys[0] : 'id'
, chainer = new Utils.QueryChainer()
, obsoleteAssociations = oldAssociations.filter(function (old) {
......
......@@ -21,6 +21,8 @@ module.exports = (function() {
this.associationAccessor = this.combinedName = (this.options.joinTableName || combinedTableName)
this.options.tableName = this.combinedName
this.options.useHooks = options.useHooks
var as = (this.options.as || Utils.pluralize(this.target.tableName, this.target.options.language))
this.accessors = {
......@@ -184,15 +186,46 @@ module.exports = (function() {
var customEventEmitter = new Utils.CustomEventEmitter(function() {
instance[self.accessors.get]().success(function(currentAssociatedObjects) {
var newAssociations = []
, oldAssociations = []
currentAssociatedObjects.forEach(function(association) {
if (!Utils._.isEqual(oldAssociatedObject.identifiers, association.identifiers))
newAssociations.push(association)
if (!Utils._.isEqual(oldAssociatedObject.identifiers, association.identifiers)) {
newAssociations[newAssociations.length] = association
} else {
oldAssociations[oldAssociations.length] = association
}
})
instance[self.accessors.set](newAssociations)
.success(function() { customEventEmitter.emit('success', null) })
.error(function(err) { customEventEmitter.emit('error', err) })
var tick = 0
var next = function(err, i) {
if (!!err || i >= oldAssociations.length) {
return run(err)
}
oldAssociations[i].destroy().error(function(err) {
next(err)
})
.success(function() {
tick++
next(null, tick)
})
}
var run = function(err) {
if (!!err) {
return customEventEmitter.emit('error', err)
}
instance[self.accessors.set](newAssociations)
.success(function() { customEventEmitter.emit('success', null) })
.error(function(err) { customEventEmitter.emit('error', err) })
}
if (oldAssociations.length > 0) {
next(null, tick)
} else {
run()
}
})
})
return customEventEmitter.run()
......
......@@ -18,6 +18,8 @@ module.exports = (function() {
? Utils.combineTableNames(this.target.tableName, this.options.as || this.target.tableName)
: this.options.as || this.target.tableName
this.options.useHooks = options.useHooks
this.accessors = {
get: Utils._.camelize('get_' + (this.options.as || Utils.singularize(this.target.tableName, this.target.options.language))),
set: Utils._.camelize('set_' + (this.options.as || Utils.singularize(this.target.tableName, this.target.options.language)))
......
......@@ -7,6 +7,11 @@ var Utils = require("./../utils")
var Mixin = module.exports = function(){}
Mixin.hasOne = function(associatedDAO, options) {
// Since this is a mixin, we'll need a unique variable name for hooks (since DAOFactory will override our hooks option)
options = options || {}
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks)
options.useHooks = options.hooks
// the id is in the foreign table
var association = new HasOne(this, associatedDAO, Utils._.extend((options||{}), this.options))
this.associations[association.associationAccessor] = association.injectAttributes()
......@@ -18,8 +23,13 @@ Mixin.hasOne = function(associatedDAO, options) {
}
Mixin.belongsTo = function(associatedDAO, options) {
// Since this is a mixin, we'll need a unique variable name for hooks (since DAOFactory will override our hooks option)
options = options || {}
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks)
options.useHooks = options.hooks
// the id is in this table
var association = new BelongsTo(this, associatedDAO, Utils._.extend((options || {}), this.options))
var association = new BelongsTo(this, associatedDAO, Utils._.extend(options, this.options))
this.associations[association.associationAccessor] = association.injectAttributes()
association.injectGetter(this.DAO.prototype)
......@@ -29,6 +39,11 @@ Mixin.belongsTo = function(associatedDAO, options) {
}
Mixin.hasMany = function(associatedDAO, options) {
// Since this is a mixin, we'll need a unique variable name for hooks (since DAOFactory will override our hooks option)
options = options || {}
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks)
options.useHooks = options.hooks
// the id is in the foreign table or in a connecting table
var association = new HasMany(this, associatedDAO, Utils._.extend((options||{}), this.options))
this.associations[association.associationAccessor] = association.injectAttributes()
......
......@@ -18,19 +18,50 @@ DaoValidator.prototype.validate = function() {
return errors
}
DaoValidator.prototype.hookValidate = function() {
var self = this
, errors = {}
return new Utils.CustomEventEmitter(function(emitter) {
self.model.daoFactory.runHooks('beforeValidate', self.model.dataValues, function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
self.model.dataValues = newValues || self.model.dataValues
errors = Utils._.extend(errors, validateAttributes.call(self))
errors = Utils._.extend(errors, validateModel.call(self))
if (Object.keys(errors).length > 0) {
return emitter.emit('error', errors)
}
self.model.daoFactory.runHooks('afterValidate', self.model.dataValues, function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
self.model.dataValues = newValues || self.model.dataValues
emitter.emit('success', self.model)
})
})
}).run()
}
// private
var validateModel = function() {
var errors = {}
var self = this
, errors = {}
// for each model validator for this DAO
Utils._.each(this.model.__options.validate, function(validator, validatorType) {
try {
validator.apply(this.model)
validator.apply(self.model)
} catch (err) {
errors[validatorType] = [err.message] // TODO: data structure needs to change for 2.0
}
}.bind(this))
})
return errors
}
......@@ -54,11 +85,12 @@ var validateAttributes = function() {
}
var validateAttribute = function(value, field) {
var errors = {}
var self = this
, errors = {}
// for each validator
Utils._.each(this.model.validators[field], function(details, validatorType) {
var validator = prepareValidationOfAttribute.call(this, value, details, validatorType)
var validator = prepareValidationOfAttribute.call(self, value, details, validatorType)
try {
validator.fn.apply(null, validator.args)
......@@ -74,7 +106,7 @@ var validateAttribute = function(value, field) {
errors[field] = errors[field] || []
errors[field].push(msg)
}
}.bind(this)) // for each validator for this field
})
return errors
}
......
......@@ -105,11 +105,11 @@ module.exports = (function() {
if (fields) {
if (self.__options.timestamps) {
if (fields.indexOf(updatedAtAttr) === -1) {
fields.push(updatedAtAttr)
fields[fields.length] = updatedAtAttr
}
if (fields.indexOf(createdAtAttr) === -1 && this.isNewRecord === true) {
fields.push(createdAtAttr)
fields[fields.length] = createdAtAttr
}
}
......@@ -122,100 +122,89 @@ module.exports = (function() {
})
}
var errors = this.validate()
if (!!errors) {
return new Utils.CustomEventEmitter(function(emitter) {
emitter.emit('error', errors)
}).run()
}
return new Utils.CustomEventEmitter(function(emitter) {
self.hookValidate().error(function(err) {
emitter.emit('error', err)
}).success(function() {
for (var attrName in self.daoFactory.rawAttributes) {
if (self.daoFactory.rawAttributes.hasOwnProperty(attrName)) {
var definition = self.daoFactory.rawAttributes[attrName]
, isHstore = !!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type
, isEnum = definition.type && (definition.type.toString() === DataTypes.ENUM.toString())
, isMySQL = self.daoFactory.daoFactoryManager.sequelize.options.dialect === "mysql"
, ciCollation = !!self.daoFactory.options.collate && self.daoFactory.options.collate.match(/_ci$/i)
// Unfortunately for MySQL CI collation we need to map/lowercase values again
if (isEnum && isMySQL && ciCollation) {
var scopeIndex = (definition.values || []).map(function(d) { return d.toLowerCase() }).indexOf(values[attrName].toLowerCase())
valueOutOfScope = scopeIndex === -1
// We'll return what the actual case will be, since a simple SELECT query would do the same...
if (!valueOutOfScope) {
values[attrName] = definition.values[scopeIndex]
}
}
for (var attrName in this.daoFactory.rawAttributes) {
if (this.daoFactory.rawAttributes.hasOwnProperty(attrName)) {
var definition = this.daoFactory.rawAttributes[attrName]
, isEnum = definition.type && (definition.type.toString() === DataTypes.ENUM.toString())
, isHstore = !!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type
, hasValue = values[attrName] !== undefined
, isMySQL = this.daoFactory.daoFactoryManager.sequelize.options.dialect === "mysql"
, ciCollation = !!this.daoFactory.options.collate && this.daoFactory.options.collate.match(/_ci$/i)
, valueOutOfScope
if (isEnum && isMySQL && ciCollation && hasValue) {
var scopeIndex = (definition.values || []).map(function(d) { return d.toLowerCase() }).indexOf(values[attrName].toLowerCase())
valueOutOfScope = scopeIndex === -1
// We'll return what the actual case will be, since a simple SELECT query would do the same...
if (!valueOutOfScope) {
values[attrName] = definition.values[scopeIndex]
if (isHstore) {
if (typeof values[attrName] === "object") {
values[attrName] = hstore.stringify(values[attrName])
}
}
}
} else {
valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1)
}
if (isEnum && hasValue && valueOutOfScope && !(definition.allowNull === true && values[attrName] === null)) {
throw new Error('Value "' + values[attrName] + '" for ENUM ' + attrName + ' is out of allowed scope. Allowed values: ' + definition.values.join(', '))
}
if (isHstore) {
if (typeof values[attrName] === "object") {
values[attrName] = hstore.stringify(values[attrName])
}
if (self.__options.timestamps && self.dataValues.hasOwnProperty(updatedAtAttr)) {
self.dataValues[updatedAtAttr] = values[updatedAtAttr] = Utils.now(self.sequelize.options.dialect)
}
}
}
if (this.__options.timestamps && this.dataValues.hasOwnProperty(updatedAtAttr)) {
this.dataValues[updatedAtAttr] = values[updatedAtAttr] = Utils.now(this.sequelize.options.dialect)
}
var query = null
, args = []
, hook = ''
var query = null
, args = []
, hook = ''
if (this.isNewRecord) {
this.isDirty = false
query = 'insert'
args = [this, this.QueryInterface.QueryGenerator.addSchema(this.__factory), values]
hook = 'Create'
} else {
var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : { id: this.id };
if (identifier === null && this.__options.whereCollection !== null) {
identifier = this.__options.whereCollection;
}
if (self.isNewRecord) {
self.isDirty = false
query = 'insert'
args = [self, self.QueryInterface.QueryGenerator.addSchema(self.__factory), values]
hook = 'Create'
} else {
var identifier = self.__options.hasPrimaryKeys ? self.primaryKeyValues : { id: self.id }
this.isDirty = false
query = 'update'
args = [this, this.QueryInterface.QueryGenerator.addSchema(this.__factory), values, identifier, options]
hook = 'Update'
}
if (identifier === null && self.__options.whereCollection !== null) {
identifier = self.__options.whereCollection;
}
return new Utils.CustomEventEmitter(function(saveEmitter) {
self.__factory.runHooks.call(self, self.__factory.options.hooks['before' + hook], values, function(err, newValues) {
if (!!err) {
return saveEmitter.emit('error', err)
self.isDirty = false
query = 'update'
args = [self, self.QueryInterface.QueryGenerator.addSchema(self.__factory), values, identifier, options]
hook = 'Update'
}
// redeclare our new values
args[2] = newValues
self.__factory.runHooks('before' + hook, values, function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
self.QueryInterface[query].apply(self.QueryInterface, args)
.on('sql', function(sql) {
saveEmitter.emit('sql', sql)
})
.error(function(err) {
saveEmitter.emit('err', err)
})
.success(function(result) {
self.__factory.runHooks.call(self, self.__factory.options.hooks['after' + hook], result.values, function(err, newValues) {
if (!!err) {
return saveEmitter.emit('error', err)
}
// redeclare our new values
args[2] = newValues || args[2]
result.dataValues = newValues
saveEmitter.emit('success', result)
self.QueryInterface[query].apply(self.QueryInterface, args)
.on('sql', function(sql) {
emitter.emit('sql', sql)
})
})
.error(function(err) {
emitter.emit('error', err)
})
.success(function(result) {
self.__factory.runHooks('after' + hook, result.values, function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
result.dataValues = newValues
emitter.emit('success', result)
})
})
})
})
}).run()
}
......@@ -259,14 +248,22 @@ module.exports = (function() {
* @return null if and only if validation successful; otherwise an object containing { field name : [error msgs] } entries.
*/
DAO.prototype.validate = function(object) {
var self = this
var validator = new DaoValidator(this, object)
, errors = validator.validate()
return (Utils._.isEmpty(errors) ? null : errors)
}
/*
* Validate this dao's attribute values according to validation rules set in the dao definition.
*
* @return CustomEventEmitter with null if validation successful; otherwise an object containing { field name : [error msgs] } entries.
*/
DAO.prototype.hookValidate = function(object) {
var validator = new DaoValidator(this, object)
return validator.hookValidate()
}
DAO.prototype.updateAttributes = function(updates, fields) {
this.setAttributes(updates)
......@@ -317,13 +314,11 @@ module.exports = (function() {
, query = null
return new Utils.CustomEventEmitter(function(emitter) {
self.daoFactory.runHooks.call(self, self.daoFactory.options.hooks.beforeDestroy, self.dataValues, function(err, newValues) {
self.daoFactory.runHooks(self.daoFactory.options.hooks.beforeDestroy, self.dataValues, function(err) {
if (!!err) {
return emitter.emit('error', err)
}
self.dataValues = newValues
if (self.__options.timestamps && self.__options.paranoid) {
var attr = Utils._.underscoredIf(self.__options.deletedAt, self.__options.underscored)
self.dataValues[attr] = new Date()
......@@ -340,12 +335,11 @@ module.exports = (function() {
emitter.emit('error', err)
})
.success(function(results) {
self.daoFactory.runHooks.call(self, self.daoFactory.options.hooks.afterDestroy, self.dataValues, function(err, newValues) {
self.daoFactory.runHooks(self.daoFactory.options.hooks.afterDestroy, self.dataValues, function(err) {
if (!!err) {
return emitter.emit('error', err)
}
self.dataValues = newValues
emitter.emit('success', results)
})
})
......
var Utils = require("./utils")
var Hooks = module.exports = function(){}
Hooks.runHooks = function(hooks, daoValues, fn) {
var self = this
, tick = 0
Hooks.runHooks = function() {
var self = this
, tick = 0
, hooks = arguments[0]
, args = Array.prototype.slice.call(arguments, 1, arguments.length-1)
, fn = arguments[arguments.length-1]
if (typeof hooks === "string") {
hooks = this.options.hooks[hooks] || []
}
if (!Array.isArray(hooks)) {
hooks = [hooks]
}
if (hooks === undefined || hooks.length < 1) {
return fn.apply(this, [null].concat(args))
}
var run = function(hook) {
if (!hook) {
return fn(null, daoValues);
return fn.apply(this, [null].concat(args))
}
if (typeof hook === "object") {
hook = hook.fn
}
hook.call(self, daoValues, function(err, newValues) {
hook.apply(self, args.concat(function() {
tick++
if (!!err) {
return fn(err)
if (!!arguments[0]) {
return fn(arguments[0])
}
daoValues = newValues
// daoValues = newValues
return run(hooks[tick])
})
}))
}
run(hooks[tick])
}
Hooks.hook = function(hookType, name, fn) {
// For aliases, we may want to incorporate some sort of way to mitigate this
if (hookType === "beforeDelete") {
hookType = 'beforeDestroy'
}
else if (hookType === "afterDelete") {
hookType = 'afterDestroy'
}
Hooks.addHook.call(this, hookType, name, fn)
}
......@@ -39,8 +61,8 @@ Hooks.addHook = function(hookType, name, fn) {
name = null
}
var method = function(daoValues, callback) {
fn.call(this, daoValues, callback)
var method = function() {
fn.apply(this, Array.prototype.slice.call(arguments, 0, arguments.length-1).concat(arguments[arguments.length-1]))
}
// Just in case if we override the default DAOFactory.options
......@@ -72,6 +94,30 @@ Hooks.afterDestroy = function(name, fn) {
Hooks.addHook.call(this, 'afterDestroy', name, fn)
}
Hooks.beforeDelete = function(name, fn) {
Hooks.addHook.call(this, 'beforeDestroy', name, fn)
}
Hooks.afterDelete = function(name, fn) {
Hooks.addHook.call(this, 'afterDestroy', name, fn)
}
Hooks.beforeUpdate = function(name, fn) {
Hooks.addHook.call(this, 'beforeUpdate', name, fn)
}
Hooks.afterUpdate = function(name, fn) {
Hooks.addHook.call(this, 'afterUpdate', name, fn)
}
Hooks.beforeBulkCreate = function(name, fn) {
Hooks.addHook.call(this, 'beforeBulkCreate', name, fn)
}
Hooks.afterBulkCreate = function(name, fn) {
Hooks.addHook.call(this, 'afterBulkCreate', name, fn)
}
Hooks.beforeBulkDestroy = function(name, fn) {
Hooks.addHook.call(this, 'beforeBulkDestroy', name, fn)
}
......@@ -80,40 +126,10 @@ Hooks.afterBulkDestroy = function(name, fn) {
Hooks.addHook.call(this, 'afterBulkDestroy', name, fn)
}
// - beforeSave
// - afterSave
// - beforeUpdate
// - afterUpdate
// - beforeDestroy
// - afterDestroy
// - beforeValidate
// - afterValidate
// user.save(callback); // If Model.id isn't set, save will invoke Model.create() instead
// // beforeValidate
// // afterValidate
// // beforeSave
// // beforeUpdate
// // afterUpdate
// // afterSave
// // callback
// user.updateAttribute('email', 'email@example.com', callback);
// // beforeValidate
// // afterValidate
// // beforeSave
// // beforeUpdate
// // afterUpdate
// // afterSave
// // callback
// user.destroy(callback);
// // beforeDestroy
// // afterDestroy
// // callback
// User.create(data, callback);
// // beforeValidate
// // afterValidate
// // beforeCreate
// // beforeSave
// // afterSave
// // afterCreate
// // callback
Hooks.beforeBulkUpdate = function(name, fn) {
Hooks.addHook.call(this, 'beforeBulkUpdate', name, fn)
}
Hooks.afterBulkUpdate = function(name, fn) {
Hooks.addHook.call(this, 'afterBulkUpdate', name, fn)
}
......@@ -479,9 +479,10 @@ module.exports = (function() {
}
QueryInterface.prototype.delete = function(dao, tableName, identifier) {
var self = this
, restrict = false
, sql = self.QueryGenerator.deleteQuery(tableName, identifier, null, dao.daoFactory)
var self = this
, restrict = false
, cascades = []
, sql = self.QueryGenerator.deleteQuery(tableName, identifier, null, dao.daoFactory)
// Check for a restrict field
if (!!dao.daoFactory && !!dao.daoFactory.associations) {
......@@ -489,29 +490,84 @@ module.exports = (function() {
, length = keys.length
for (var i = 0; i < length; i++) {
if (dao.daoFactory.associations[keys[i]].options && dao.daoFactory.associations[keys[i]].options.onDelete && dao.daoFactory.associations[keys[i]].options.onDelete === "restrict") {
restrict = true
if (dao.daoFactory.associations[keys[i]].options && dao.daoFactory.associations[keys[i]].options.onDelete) {
if (dao.daoFactory.associations[keys[i]].options.onDelete === "restrict") {
restrict = true
}
else if (dao.daoFactory.associations[keys[i]].options.onDelete === "cascade" && dao.daoFactory.associations[keys[i]].options.useHooks === true) {
cascades[cascades.length] = dao.daoFactory.associations[keys[i]].accessors.get
}
}
}
}
return new Utils.CustomEventEmitter(function(emitter) {
var chainer = new Utils.QueryChainer()
var tick = 0
var iterate = function(err, i) {
if (!!err || i >= cascades.length) {
return run(err)
}
chainer.add(self, 'enableForeignKeyConstraints', [])
chainer.add(self, 'queryAndEmit', [[sql, dao], 'delete'])
dao[cascades[i]]().success(function(tasks) {
if (tasks === null || tasks.length < 1) {
return run()
}
chainer.runSerially()
.success(function(results){
emitter.query = { sql: sql }
emitter.emit('sql', sql)
emitter.emit('success', results[1])
})
.error(function(err) {
emitter.query = { sql: sql }
emitter.emit('sql', sql)
emitter.emit('error', err)
})
tasks = Array.isArray(tasks) ? tasks : [tasks]
var ii = 0
var next = function(err, ii) {
if (!!err || ii >= tasks.length) {
return iterate(err)
}
tasks[ii].destroy().error(function(err) {
return iterate(err)
})
.success(function() {
ii++
if (ii >= tasks.length) {
tick++
return iterate(null, tick)
}
next(null, ii)
})
}
next(null, ii)
})
}
var run = function(err) {
if (!!err) {
return emitter.emit('error', err)
}
var chainer = new Utils.QueryChainer()
chainer.add(self, 'enableForeignKeyConstraints', [])
chainer.add(self, 'queryAndEmit', [[sql, dao], 'delete'])
chainer.runSerially()
.success(function(results){
emitter.query = { sql: sql }
emitter.emit('sql', sql)
emitter.emit('success', results[1])
})
.error(function(err) {
emitter.query = { sql: sql }
emitter.emit('sql', sql)
emitter.emit('error', err)
})
}
if (cascades.length > 0) {
iterate(null, tick)
} else {
run()
}
}).run()
}
......
......@@ -159,17 +159,8 @@ module.exports = (function() {
Sequelize.prototype.define = function(daoName, attributes, options) {
options = options || {}
var globalOptions = this.options
// If you don't specify a valid data type lets help you debug it
Utils._.each(attributes, function(dataType, name){
if (Utils.isHash(dataType)) {
dataType = dataType.type
}
if (dataType === undefined) {
throw new Error('Unrecognized data type for field '+ name)
}
})
var self = this
, globalOptions = this.options
if (globalOptions.define) {
options = Utils._.extend({}, globalOptions.define, options)
......@@ -184,6 +175,40 @@ module.exports = (function() {
options.omitNull = globalOptions.omitNull
options.language = globalOptions.language
// If you don't specify a valid data type lets help you debug it
Utils._.each(attributes, function(dataType, name) {
if (Utils.isHash(dataType)) {
dataType = dataType.type
}
if (dataType === undefined) {
throw new Error('Unrecognized data type for field '+ name)
}
if (dataType.toString() === "ENUM") {
attributes[name].validate = attributes[name].validate || {
_checkEnum: function(value) {
var hasValue = value !== undefined
, isMySQL = self.options.dialect === "mysql"
, ciCollation = !!options.collate && options.collate.match(/_ci$/i) !== null
, valueOutOfScope
if (isMySQL && ciCollation && hasValue) {
var scopeIndex = (attributes[name].values || []).map(function(d) { return d.toLowerCase() }).indexOf(value.toLowerCase())
valueOutOfScope = scopeIndex === -1
} else {
valueOutOfScope = ((attributes[name].values || []).indexOf(value) === -1)
}
if (hasValue && valueOutOfScope && !(attributes[name].allowNull === true && values[attrName] === null)) {
throw new Error('Value "' + value + '" for ENUM ' + name + ' is out of allowed scope. Allowed values: ' + attributes[name].values.join(', '))
}
}
}
}
})
// if you call "define" multiple times for the same daoName, do not clutter the factory
if(this.isDefined(daoName)) {
this.daoFactoryManager.removeDAO(this.daoFactoryManager.getDAO(daoName))
......
module.exports = {
username: "root",
password: null,
database: 'sequelize_test',
host: '127.0.0.1',
pool: { maxConnections: 5, maxIdleTime: 30000},
username: process.env.SEQ_USER || "root",
password: process.env.SEQ_PW || null,
database: process.env.SEQ_DB || 'sequelize_test',
host: process.env.SEQ_HOST || '127.0.0.1',
pool: {
maxConnections: process.env.SEQ_POOL_MAX || 5,
maxIdleTime: process.env.SEQ_POOL_IDLE || 30000
},
rand: function() {
return parseInt(Math.random() * 999, 10)
......@@ -11,21 +14,29 @@ module.exports = {
//make maxIdleTime small so that tests exit promptly
mysql: {
username: "root",
password: null,
database: 'sequelize_test',
host: '127.0.0.1',
port: 3306,
pool: { maxConnections: 5, maxIdleTime: 30}
database: process.env.SEQ_MYSQL_DB || process.env.SEQ_DB || 'sequelize_test',
username: process.env.SEQ_MYSQL_USER || process.env.SEQ_USER || "root",
password: process.env.SEQ_MYSQL_PW || process.env.SEQ_PW || null,
host: process.env.SEQ_MYSQL_HOST || process.env.SEQ_HOST || '127.0.0.1',
port: process.env.SEQ_MYSQL_PORT || process.env.SEQ_PORT || 3306,
pool: {
maxConnections: process.env.SEQ_MYSQL_POOL_MAX || process.env.SEQ_POOL_MAX || 5,
maxIdleTime: process.env.SEQ_MYSQL_POOL_IDLE || process.env.SEQ_POOL_IDLE || 30
}
},
sqlite: {
},
postgres: {
database: 'sequelize_test',
username: "postgres",
port: 5432,
pool: { maxConnections: 5, maxIdleTime: 3000}
database: process.env.SEQ_PG_DB || process.env.SEQ_DB || 'sequelize_test',
username: process.env.SEQ_PG_USER || process.env.SEQ_USER || "postgres",
password: process.env.SEQ_PG_PW || process.env.SEQ_PW || null,
host: process.env.SEQ_PG_HOST || process.env.SEQ_HOST || '127.0.0.1',
port: process.env.SEQ_PG_PORT || process.env.SEQ_PORT || 5432,
pool: {
maxConnections: process.env.SEQ_PG_POOL_MAX || process.env.SEQ_POOL_MAX || 5,
maxIdleTime: process.env.SEQ_PG_POOL_IDLE || process.env.SEQ_POOL_IDLE || 3000
}
}
}
......@@ -1809,17 +1809,17 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
it('eager loads with non-id primary keys', function(done) {
var self = this
self.User = self.sequelize.define('UserPKeagerbelong', {
username: {
self.User = self.sequelize.define('UserPKeagerbelong', {
username: {
type: Sequelize.STRING,
primaryKey: true
}
}
})
self.Group = self.sequelize.define('GroupPKeagerbelong', {
name: {
self.Group = self.sequelize.define('GroupPKeagerbelong', {
name: {
type: Sequelize.STRING,
primaryKey: true
}
}
})
self.User.belongsTo(self.Group)
......@@ -1878,17 +1878,17 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
it('eager loads with non-id primary keys', function(done) {
var self = this
self.User = self.sequelize.define('UserPKeagerone', {
username: {
self.User = self.sequelize.define('UserPKeagerone', {
username: {
type: Sequelize.STRING,
primaryKey: true
}
}
})
self.Group = self.sequelize.define('GroupPKeagerone', {
name: {
self.Group = self.sequelize.define('GroupPKeagerone', {
name: {
type: Sequelize.STRING,
primaryKey: true
}
}
})
self.Group.hasOne(self.User)
......@@ -2000,17 +2000,17 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
it('eager loads with non-id primary keys', function(done) {
var self = this
self.User = self.sequelize.define('UserPKeagerone', {
username: {
self.User = self.sequelize.define('UserPKeagerone', {
username: {
type: Sequelize.STRING,
primaryKey: true
}
}
})
self.Group = self.sequelize.define('GroupPKeagerone', {
name: {
self.Group = self.sequelize.define('GroupPKeagerone', {
name: {
type: Sequelize.STRING,
primaryKey: true
}
}
})
self.Group.hasMany(self.User)
self.User.hasMany(self.Group)
......@@ -2032,7 +2032,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
expect(someUser.groupPKeagerones[0].name).to.equal('people')
done()
})
})
})
})
})
})
......
This diff could not be displayed because it is too large.
......@@ -122,15 +122,14 @@ if (dialect.match(/^mysql/)) {
})
User.sync({ force: true }).success(function() {
expect(function() {
User.create({mood: 'happy'})
}).to.throw(Error, 'Value "happy" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver')
expect(function() {
User.create({mood: 'happy'}).error(function(err) {
expect(err).to.deep.equal({ mood: [ 'Value "happy" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver' ] })
var u = User.build({mood: 'SAD'})
u.save()
}).to.throw(Error, 'Value "SAD" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver')
done()
u.save().error(function(err) {
expect(err).to.deep.equal({ mood: [ 'Value "SAD" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver' ] })
done()
})
})
})
})
})
......
......@@ -423,11 +423,10 @@ describe(Support.getTestDialectTeaser("Sequelize"), function () {
})
it("doesn't save an instance if value is not in the range of enums", function(done) {
var self = this
expect(function() {
self.Review.create({ status: 'fnord' })
}).to.throw(Error, 'Value "fnord" for ENUM status is out of allowed scope. Allowed values: scheduled, active, finished')
done()
this.Review.create({status: 'fnord'}).error(function(err) {
expect(err).to.deep.equal({ status: [ 'Value "fnord" for ENUM status is out of allowed scope. Allowed values: scheduled, active, finished' ] })
done()
})
})
})
})
......
var fs = require('fs')
, Sequelize = require(__dirname + "/../index")
, DataTypes = require(__dirname + "/../lib/data-types")
, config = require(__dirname + "/config/config")
, Config = require(__dirname + "/config/config")
var Support = {
Sequelize: Sequelize,
......@@ -26,15 +26,17 @@ var Support = {
createSequelizeInstance: function(options) {
options = options || {}
options.dialect = options.dialect || 'mysql'
var config = Config[options.dialect]
options.logging = (options.hasOwnProperty('logging') ? options.logging : false)
options.pool = options.pool || config.pool
var sequelizeOptions = {
logging: options.logging,
dialect: options.dialect,
port: options.port || process.env.SEQ_PORT || config[options.dialect].port,
port: options.port || config.port,
pool: options.pool
}
......@@ -50,12 +52,7 @@ var Support = {
sequelizeOptions.native = true
}
return this.getSequelizeInstance(
process.env.SEQ_DB || config[options.dialect].database,
process.env.SEQ_USER || process.env.SEQ_USERNAME || config[options.dialect].username,
process.env.SEQ_PW || process.env.SEQ_PASSWORD || config[options.dialect].password,
sequelizeOptions
)
return this.getSequelizeInstance(config.database, config.username, config.password, sequelizeOptions)
},
getSequelizeInstance: function(db, user, pass, options) {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!