sequelize/sequelize — pull request #894 by durango (2013-09-10 05:57:35+00:00)
Title: Adds hooks / callbacks / lifecycle events

This commit incorporates lifecycle events, hooks, or otherwise known as callbacks for SequelizeJS which closes https://github.com/sequelize/sequelize/issues/873

# Hooks (aka callbacks or lifecycle events)

## Order of Operations

```js
// (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)
```

## Declaring Hooks

There are currently three ways on how to programatically add hooks...

```js
// Method 1 via the .define() method
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)
}
}
})

// Method 2 via the .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)
})

// Method 3 via the 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)
})
```

## How to Use

Simply type in which hook you want and return a normal NodeJS callback (err, newValues).

### Instance / DAO Hooks

The following hooks will emit whenever you're editing a single object...

```
beforeValidate
afterValidate
beforeCreate / beforeUpdate / beforeDestroy
afterCreate / afterUpdate / afterDestroy
```

This example will emit an error:

```js
// ...define ...
User.beforeCreate(function(user, fn) {
if (user.accessLevel > 10 && user.username !== "Boss") {
return fn("You can't grant this user an access level above 10!")
}

return fn()
})

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

The following example would emit a success event:

```js
// ...define ...
User.beforeCreate(function(user, fn) {
if (user.accessLevel > 10 && user.username !== "Boss") {
return fn("You can't grant this user an access level above 10!")
}

return fn()
})

User.create({username: 'Boss', accessLevel: 20}).success(function(user) {
console.log(user) // user object with username as Boss and accessLevel of 20
})
```

### Model / DAOFactory Callbacks

Sometimes you'll be editing more than one record at a time (by utilizing methods from the DAO Factory/Model directly)

The following will emit whenever you're using one of these methods...

```
beforeBulkCreate / beforeBulkUpdate / beforeBulkDestroy
afterBulkCreate / afterBulkUpdate / afterBulkDestroy
```

You can even emit direct hooks per instance when calling these events like so...

```js
Model.destroy({accessLevel: 0}, {hooks: true}) // Will select all records that are about to be deleted and emit beforeDestroy + afterDestroy on each instance
Model.update({username: 'Toni'}, {accessLevel: 0}, {hooks: true}) // Will select all records that are about to be updated and emit beforeUpdate + afterUpdate on each instance
Model.bulkCreate({accessLevel: 0}, null, {hooks: true}) // Will select all records that are about to be deleted and emit beforeDestroy + afterDestroy on each instance
```

**Note:** For ```update()``` and ```bulkCreate()``` when calling the ```{hooks: true}``` option, we'll return each DAO/Record on the ```success()``` call. The reason behind this is because we have to fetch all of the records *anyway*. For example:

```js
Model.bulkCreate([{accessLevel: 0}, {accessLevel: 5}], ['accessLevel'], {hooks: true}).success(function(records) {
console.log(records) // accessLevel 0 and accessLevel 5 records
})
```

#### Notes for Model Callbacks

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

```js
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*/)
```

## Associations

For the most part hooks will work the same for instances when being associated except a few things:

1. When using add/set[s] functions the beforeUpdate/afterUpdate hooks will run.

2. The only way to call beforeDestroy/afterDestroy hooks are on associations with ```onDelete: 'cascade'``` and a new option ```hooks: true```. For instance...

```js
var Projects = sequelize.define('Projects', {
title: DataTypes.STRING
})

var Tasks = sequelize.define('Tasks', {
title: DataTypes.STRING
})

Projects.hasMany(Tasks, {onDelete: 'cascade', hooks: true})
Tasks.belongsTo(Projects)
```

This code will run beforeDestroy/afterDestroy on the **Tasks** table. The reason behind this is twofold:

1. Backwards compatibility - We want to make sure your current codebase does not in anyway act differently. You should be able to upgrade without any problems.

2. Sequelize, by default, will try to optimize your queries as much as possible. When calling cascade on delete, Sequelize will simply execute a ```DELETE FROM table WHERE associatedIdentifiier = associatedIdentifier.primaryKey```. Calling ```hooks: true``` explicitly tells Sequelize that optimization is not of your concern and will perform a ```SELECT``` on the associated objects and destroying each one by one (in order to have the hooks).

## Aliases

As of right now, we only have one alias which is ```beforeDelete()``` and ```afterDelete()```. This works as-if you were typing ```beforeDestroy()``` and ```afterDestroy()``` respectively. Aliases does not work when using the ```define()``` method. You must use destroy for that particular case.

## Special Notes

### Omitting DAO / instances hook capbility

I've omitted the ability to add hooks to DAO instances specifically, only because ORMs traditionally *do not* have this capibility. I've made the system with adding this capbility in mind just in case if it's been requested enough times and/or the Sequelize team wants it.

### Ability to name hooks

As of right now, you have the ability to name hooks like so...

```js
User.afterCreate('AfterCreatePhase1', function(user, fn) {
// ...
})
```

As of right now, this serves no purpose other than the ability/capibility for the future to order hooks based on either a priority system or by before/after a specific hook (an example **could** be)...

```js
User.addBeforeHook('AfterCreatePhase1', function(user, fn) {
// will add this hook before 'AfterCreatePhase1'
fn()
})
```

This could be proven useful for some sort of plugin system in the future.

### Version Compatibility and Backwards-Compatibile issues

For 1.7.0+ you'll need to call ```.hookValidate()``` if you want to run validation hooks manually. For 2.0.0+ we've added a ```.hookValidate()``` alias for .validate() for each transition. This method will be deprecated within the future. In 1.7.0+ the ```.hookValidate()``` method mimmicks 2.0.0's ```.validate()``` method.

## Other things about this PR

I've moved the ENUM validation to the validate method (which closes a to-do item for 1.7.0's roadmap)

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.