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

Commit ae3785f1 by Mike Ringrose

Merge remote-tracking branch 'upstream/master'

2 parents 9bbe34b0 7888734e
...@@ -82,7 +82,7 @@ postgres-native: ...@@ -82,7 +82,7 @@ postgres-native:
# Coverage # Coverage
cover: cover:
rm -rf coverage \ rm -rf coverage \
make teaser && ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- -t 15000 --ui tdd $(TESTS); \ make teaser && COVERAGE=true ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- -t 15000 --ui tdd $(TESTS); \
mssql-cover: mssql-cover:
rm -rf coverage rm -rf coverage
......
# Next
- [FEATURE] Geometry support for postgres
- [BUG] Fix wrong count for `findAndCountAll` with required includes [#4016](https://github.com/sequelize/sequelize/pull/4016)
- [BUG] Fix problems related to parsing of unique constraint errors [#4017](https://github.com/sequelize/sequelize/issues/4017) and [#4012](https://github.com/sequelize/sequelize/issues/4012)
- [BUG] Fix postgres path variable being surrounded by quotes to often in unique constraint errors [#4034](https://github.com/sequelize/sequelize/pull/4034)
# 3.3.2 # 3.3.2
- [FIXED] upsert no longer updates with default values each time [#3994](https://github.com/sequelize/sequelize/pull/3994) - [FIXED] upsert no longer updates with default values each time [#3994](https://github.com/sequelize/sequelize/pull/3994)
...@@ -10,6 +16,7 @@ ...@@ -10,6 +16,7 @@
- [FIXED] $or/$and inside a where clause always expects the input to be an array [#3767](https://github.com/sequelize/sequelize/issues/3767) - [FIXED] $or/$and inside a where clause always expects the input to be an array [#3767](https://github.com/sequelize/sequelize/issues/3767)
- [ADDED] Unique constraints may now include custom error messages - [ADDED] Unique constraints may now include custom error messages
- [ADDED] It's possible now to remove a hook by name - [ADDED] It's possible now to remove a hook by name
- [ADDED] Hook name can be passed via the direct method [#3901](https://github.com/sequelize/sequelize/issues/3901)
# 3.2.0 # 3.2.0
- [FEATURE] Add support for new option `targetKey` in a belongs-to relationship for situations where the target key is not the id field. - [FEATURE] Add support for new option `targetKey` in a belongs-to relationship for situations where the target key is not the id field.
......
...@@ -111,7 +111,7 @@ A hook that is run after validation ...@@ -111,7 +111,7 @@ A hook that is run after validation
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with instance, options, callback(err) | | fn | Function | A callback function that is called with instance, options |
*** ***
...@@ -126,7 +126,7 @@ A hook that is run before creating a single instance ...@@ -126,7 +126,7 @@ A hook that is run before creating a single instance
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with attributes, options, callback(err) | | fn | Function | A callback function that is called with attributes, options |
*** ***
...@@ -141,7 +141,7 @@ A hook that is run after creating a single instance ...@@ -141,7 +141,7 @@ A hook that is run after creating a single instance
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with attributes, options, callback(err) | | fn | Function | A callback function that is called with attributes, options |
*** ***
...@@ -156,7 +156,7 @@ A hook that is run before destroying a single instance ...@@ -156,7 +156,7 @@ A hook that is run before destroying a single instance
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with instance, options, callback(err) | | fn | Function | A callback function that is called with instance, options |
__Aliases:__ beforeDelete __Aliases:__ beforeDelete
...@@ -172,7 +172,7 @@ A hook that is run after destroying a single instance ...@@ -172,7 +172,7 @@ A hook that is run after destroying a single instance
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with instance, options, callback(err) | | fn | Function | A callback function that is called with instance, options |
__Aliases:__ afterDelete __Aliases:__ afterDelete
...@@ -188,7 +188,7 @@ A hook that is run before updating a single instance ...@@ -188,7 +188,7 @@ A hook that is run before updating a single instance
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with instance, options, callback(err) | | fn | Function | A callback function that is called with instance, options |
*** ***
...@@ -203,7 +203,7 @@ A hook that is run after updating a single instance ...@@ -203,7 +203,7 @@ A hook that is run after updating a single instance
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with instance, options, callback(err) | | fn | Function | A callback function that is called with instance, options |
*** ***
...@@ -218,7 +218,7 @@ A hook that is run before creating instances in bulk ...@@ -218,7 +218,7 @@ A hook that is run before creating instances in bulk
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with instances, options, callback(err) | | fn | Function | A callback function that is called with instances, options |
*** ***
...@@ -233,7 +233,7 @@ A hook that is run after creating instances in bulk ...@@ -233,7 +233,7 @@ A hook that is run after creating instances in bulk
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with instances, options, callback(err) | | fn | Function | A callback function that is called with instances, options |
*** ***
...@@ -248,7 +248,7 @@ A hook that is run before destroying instances in bulk ...@@ -248,7 +248,7 @@ A hook that is run before destroying instances in bulk
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with options, callback(err) | | fn | Function | A callback function that is called with options |
__Aliases:__ beforeBulkDelete __Aliases:__ beforeBulkDelete
...@@ -264,7 +264,7 @@ A hook that is run after destroying instances in bulk ...@@ -264,7 +264,7 @@ A hook that is run after destroying instances in bulk
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with options, callback(err) | | fn | Function | A callback function that is called with options |
__Aliases:__ afterBulkDelete __Aliases:__ afterBulkDelete
...@@ -280,7 +280,7 @@ A hook that is run after updating instances in bulk ...@@ -280,7 +280,7 @@ A hook that is run after updating instances in bulk
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with options, callback(err) | | fn | Function | A callback function that is called with options |
*** ***
...@@ -295,7 +295,7 @@ A hook that is run after updating instances in bulk ...@@ -295,7 +295,7 @@ A hook that is run after updating instances in bulk
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with options, callback(err) | | fn | Function | A callback function that is called with options |
*** ***
...@@ -310,7 +310,7 @@ A hook that is run before a find (select) query ...@@ -310,7 +310,7 @@ A hook that is run before a find (select) query
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with options, callback(err) | | fn | Function | A callback function that is called with options |
*** ***
...@@ -325,7 +325,7 @@ A hook that is run before a find (select) query, after any { include: {all: ...} ...@@ -325,7 +325,7 @@ A hook that is run before a find (select) query, after any { include: {all: ...}
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with options, callback(err) | | fn | Function | A callback function that is called with options |
*** ***
...@@ -340,7 +340,7 @@ A hook that is run before a find (select) query, after all option parsing is com ...@@ -340,7 +340,7 @@ A hook that is run before a find (select) query, after all option parsing is com
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with options, callback(err) | | fn | Function | A callback function that is called with options |
*** ***
...@@ -355,7 +355,7 @@ A hook that is run after a find (select) query ...@@ -355,7 +355,7 @@ A hook that is run after a find (select) query
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with instance(s), options, callback(err) | | fn | Function | A callback function that is called with instance(s), option |
*** ***
...@@ -370,7 +370,7 @@ A hook that is run before a define call ...@@ -370,7 +370,7 @@ A hook that is run before a define call
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with attributes, options, callback(err) | | fn | Function | A callback function that is called with attributes, options |
*** ***
...@@ -385,7 +385,7 @@ A hook that is run after a define call ...@@ -385,7 +385,7 @@ A hook that is run after a define call
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with factory, callback(err) | | fn | Function | A callback function that is called with factory |
*** ***
...@@ -400,7 +400,7 @@ A hook that is run before Sequelize() call ...@@ -400,7 +400,7 @@ A hook that is run before Sequelize() call
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with config, options, callback(err) | | fn | Function | A callback function that is called with config, options |
*** ***
...@@ -415,7 +415,7 @@ A hook that is run after Sequelize() call ...@@ -415,7 +415,7 @@ A hook that is run after Sequelize() call
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| name | String | | | name | String | |
| fn | Function | A callback function that is called with sequelize, callback(err) | | fn | Function | A callback function that is called with sequelize |
*** ***
......
...@@ -53,7 +53,7 @@ Drop the table represented by this Model ...@@ -53,7 +53,7 @@ Drop the table represented by this Model
*** ***
<a name="schema"></a> <a name="schema"></a>
## `schema(schema, [options])` -> `hi` ## `schema(schema, [options])` -> `this`
[View code](https://github.com/sequelize/sequelize/blob/2c4a9f3cf9887fb33c31e397e758dd4aa3374d01/lib/model.js#L914) [View code](https://github.com/sequelize/sequelize/blob/2c4a9f3cf9887fb33c31e397e758dd4aa3374d01/lib/model.js#L914)
Apply a schema to this model. For postgres, this will actually place the schema in front of the table name - `"schema"."tableName"`, Apply a schema to this model. For postgres, this will actually place the schema in front of the table name - `"schema"."tableName"`,
while the schema will be prepended to the table name for mysql and sqlite - `'schema.tablename'`. while the schema will be prepended to the table name for mysql and sqlite - `'schema.tablename'`.
...@@ -240,7 +240,7 @@ The success listener is called with an array of instances if the query succeeds. ...@@ -240,7 +240,7 @@ The success listener is called with an array of instances if the query succeeds.
| [options.limit] | Number | | | [options.limit] | Number | |
| [options.offset] | Number | | | [options.offset] | Number | |
| [options.transaction] | Transaction | Transaction to run query under | | [options.transaction] | Transaction | Transaction to run query under |
| [options.lock] | String &#124; Object | Lock the selected rows. Possible options are transaction.LOCK.UPDATE and transaction.LOCK.SHARE. Postgres also supports transaction.LOCK.KEY_SHARE, transaction.LOCK.NO_KEY_UPDATE and specific model locks with joins. See [transaction.LOCK for an example](api/transaction#lock) | | [options.lock] | String &#124; Object | Lock the selected rows. Possible options are transaction.LOCK.UPDATE and transaction.LOCK.SHARE. Postgres also supports transaction.LOCK.KEY_SHARE, transaction.LOCK.NO_KEY_UPDATE and specific model locks with joins. See [transaction.LOCK for an example](transaction#lock) |
| [options.raw] | Boolean | Return raw result. See sequelize.query for more information. | | [options.raw] | Boolean | Return raw result. See sequelize.query for more information. |
| [options.logging=false] | Function | A function that gets executed while running the query to log the sql. | | [options.logging=false] | Function | A function that gets executed while running the query to log the sql. |
| [options.having] | Object | | | [options.having] | Object | |
......
Hooks (also known as callbacks or lifecycle events), are functions which are called before and after calls in sequelize are executed. For example, if you want to always set a value on a model before saving it, you can add a `beforeUpdate` hook. Hooks (also known as callbacks or lifecycle events), are functions which are called before and after calls in sequelize are executed. For example, if you want to always set a value on a model before saving it, you can add a `beforeUpdate` hook.
For a full list of hooks, see [Hooks API](/api/hooks).
## Order of Operations ## Order of Operations
``` ```
...@@ -32,9 +34,9 @@ Hooks (also known as callbacks or lifecycle events), are functions which are cal ...@@ -32,9 +34,9 @@ Hooks (also known as callbacks or lifecycle events), are functions which are cal
``` ```
## Declaring Hooks ## Declaring Hooks
Arguments to hooks are passed by reference. This means, that you can change the values, and this will be reflected in the insert / update statement. A hook may contain async actions - in this case the hook function should return a promise.
There are currently three ways to programmatically add hooks. A hook function always runs asynchronousĺy, and can be resolved either by calling a callback (passed as the last argument), There are currently three ways to programmatically add hooks:
or by returning a promise.
```js ```js
// Method 1 via the .define() method // Method 1 via the .define() method
...@@ -46,29 +48,18 @@ var User = sequelize.define('User', { ...@@ -46,29 +48,18 @@ var User = sequelize.define('User', {
} }
}, { }, {
hooks: { hooks: {
beforeValidate: function(user, options, fn) { beforeValidate: function(user, options) {
user.mood = 'happy' user.mood = 'happy'
fn(null, user)
}, },
afterValidate: function(user, options, fn) { afterValidate: function(user, options) {
user.username = 'Toni' user.username = 'Toni'
fn(null, user)
} }
} }
}) })
// Method 2 via the .hook() method // Method 2 via the .hook() method
var User = sequelize.define('User', { User.hook('beforeValidate', function(user, options) {
username: DataTypes.STRING,
mood: {
type: DataTypes.ENUM,
values: ['happy', 'sad', 'neutral']
}
})
User.hook('beforeValidate', function(user, options, fn) {
user.mood = 'happy' user.mood = 'happy'
fn(null, user)
}) })
User.hook('afterValidate', function(user, options) { User.hook('afterValidate', function(user, options) {
...@@ -76,22 +67,14 @@ User.hook('afterValidate', function(user, options) { ...@@ -76,22 +67,14 @@ User.hook('afterValidate', function(user, options) {
}) })
// Method 3 via the direct method // Method 3 via the direct method
var User = sequelize.define('User', { User.beforeCreate(function(user, options) {
username: DataTypes.STRING, return hashPassword(user.password).then(function (hashedPw) {
mood: { user.password = hashedPw;
type: DataTypes.ENUM, });
values: ['happy', 'sad', 'neutral']
}
})
User.beforeValidate(function(user, options) {
user.mood = 'happy'
return sequelize.Promise.resolve(user)
}) })
User.afterValidate(function(user, options, fn) { User.afterValidate('myHookAfter', function(user, options, fn) {
user.username = 'Toni' user.username = 'Toni'
fn(null, user)
}) })
``` ```
...@@ -111,9 +94,68 @@ Book.addHook('afterCreate', 'notifyUsers', function(book, options) { ...@@ -111,9 +94,68 @@ Book.addHook('afterCreate', 'notifyUsers', function(book, options) {
Book.removeHook('afterCreate', 'notifyUsers') Book.removeHook('afterCreate', 'notifyUsers')
``` ```
## Global / universal hooks
Global hooks are hooks which are run for all models. They can define behaviours that you want for all your models, and are especially useful for plugins. They can be defined in two ways, which have slightly different semantics:
### Sequelize.options.define (default hook)
```js
var sequelize = new Sequelize(..., {
define: {
hooks: {
beforeCreate: function () {
// Do stuff
}
}
}
});
```
This adds a default hook to all models, which is run if the model does not define its own `beforeCreate` hook:
```js
var User = sequelize.define('user');
var Project = sequelize.define('project', {}, {
hooks: {
beforeCreate: function () {
// Do other stuff
}
}
});
User.create() // Runs the global hook
Project.create() // Runs its own hook (because the global hook is overwritten)
```
### Sequelize.addHook (permanent hook)
```js
sequelize.addHook('beforeCreate', function () {
// Do stuff
});
```
This hooks is always run before create, regardless of whether the model specifies its own `beforeCreate` hook:
```js
var User = sequelize.define('user');
var Project = sequelize.define('project', {}, {
hooks: {
beforeCreate: function () {
// Do other stuff
}
}
});
User.create() // Runs the global hook
Project.create() // Runs its own hook, followed by the global hook
```
Local hooks are always run before global hooks.
### Instance hooks ### Instance hooks
The following hooks will emit whenever you're editing a single object... The following hooks will emit whenever you're editing a single object
``` ```
beforeValidate beforeValidate
...@@ -169,7 +211,7 @@ Model.update({username: 'Toni'}, { where: {accessLevel: 0}, individualHooks: tru ...@@ -169,7 +211,7 @@ Model.update({username: 'Toni'}, { where: {accessLevel: 0}, individualHooks: tru
Some model hooks have two or three parameters sent to each hook depending on it's type. Some model hooks have two or three parameters sent to each hook depending on it's type.
```js ```js
Model.beforeBulkCreate(function(records, fields, fn) { Model.beforeBulkCreate(function(records, fields) {
// records = the first argument sent to .bulkCreate // records = the first argument sent to .bulkCreate
// fields = the second argument sent to .bulkCreate // fields = the second argument sent to .bulkCreate
}) })
...@@ -179,14 +221,14 @@ Model.bulkCreate([ ...@@ -179,14 +221,14 @@ Model.bulkCreate([
{username: 'Tobi'} // part of records argument {username: 'Tobi'} // part of records argument
], ['username'] /* part of fields argument */) ], ['username'] /* part of fields argument */)
Model.beforeBulkUpdate(function(attributes, where, fn) { Model.beforeBulkUpdate(function(attributes, where) {
// attributes = first argument sent to Model.update // attributes = first argument sent to Model.update
// where = second argument sent to Model.update // where = second argument sent to Model.update
}) })
Model.update({gender: 'Male'} /*attributes argument*/, { where: {username: 'Tom'}} /*where argument*/) Model.update({gender: 'Male'} /*attributes argument*/, { where: {username: 'Tom'}} /*where argument*/)
Model.beforeBulkDestroy(function(whereClause, fn) { Model.beforeBulkDestroy(function(whereClause) {
// whereClause = first argument sent to Model.destroy // whereClause = first argument sent to Model.destroy
}) })
...@@ -197,8 +239,9 @@ Model.destroy({ where: {username: 'Tom'}} /*whereClause argument*/) ...@@ -197,8 +239,9 @@ Model.destroy({ where: {username: 'Tom'}} /*whereClause argument*/)
For the most part hooks will work the same for instances when being associated except a few things 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. 1. When using add/set functions the beforeUpdate/afterUpdate hooks will run.
2. The only way to call beforeDestroy/afterDestroy hooks are on associations with `onDelete: 'cascade'` and the option `hooks: true`. For instance: 2. The only way to call beforeDestroy/afterDestroy hooks are on associations with `onDelete: 'cascade'` and the option `hooks: true`. For instance:
```js ```js
var Projects = sequelize.define('Projects', { var Projects = sequelize.define('Projects', {
title: DataTypes.STRING title: DataTypes.STRING
...@@ -208,12 +251,11 @@ var Tasks = sequelize.define('Tasks', { ...@@ -208,12 +251,11 @@ var Tasks = sequelize.define('Tasks', {
title: DataTypes.STRING title: DataTypes.STRING
}) })
Projects.hasMany(Tasks, {onDelete: 'cascade', hooks: true}) Projects.hasMany(Tasks, { onDelete: 'cascade', hooks: true })
Tasks.belongsTo(Projects) Tasks.belongsTo(Projects)
``` ```
This code will run beforeDestroy/afterDestroy on the Tasks table. Sequelize, by default, will try to optimize your queries as much as possible. This code will run beforeDestroy/afterDestroy on the Tasks table. Sequelize, by default, will try to optimize your queries as much as possible. When calling cascade on delete, Sequelize will simply execute a
When calling cascade on delete, Sequelize will simply execute a
```sql ```sql
DELETE FROM `table` WHERE associatedIdentifiier = associatedIdentifier.primaryKey DELETE FROM `table` WHERE associatedIdentifiier = associatedIdentifier.primaryKey
...@@ -221,29 +263,6 @@ DELETE FROM `table` WHERE associatedIdentifiier = associatedIdentifier.primaryKe ...@@ -221,29 +263,6 @@ DELETE FROM `table` WHERE associatedIdentifiier = associatedIdentifier.primaryKe
However, adding `hooks: true` explicitly tells Sequelize that optimization is not of your concern and will perform a `SELECT` on the associated objects and destroy each instance one by one in order to be able to call the hooks with the right parameters. However, adding `hooks: true` explicitly tells Sequelize that optimization is not of your concern and will perform a `SELECT` on the associated objects and destroy each instance one by one in order to be able to call the hooks with the right parameters.
## Promises and callbacks
Sequelize will look at the function length of your hook callback to determine whether or not you're using callbacks or promises.
```js
// Will stall if the condition isn't met since the callback is never called
User.beforeCreate(function(user, options, callback) {
if (user.accessLevel > 10 && user.username !== "Boss") {
return callback("You can't grant this user an access level above 10!");
}
});
// Will never stall since returning undefined will act as a resolved promise with an undefined value
User.beforeCreate(function(user, options) {
if (user.accessLevel > 10 && user.username !== "Boss") {
return throw new Error("You can't grant this user an access level above 10!");
}
if (something) {
return Promise.reject();
}
});
```
## A Note About Transactions ## A Note About Transactions
Note that many model operations in Sequelize allow you to specify a transaction in the options parameter of the method. If a transaction _is_ specified in the original call, it will be present in the options parameter passed to the hook function. For example, consider the following snippet: Note that many model operations in Sequelize allow you to specify a transaction in the options parameter of the method. If a transaction _is_ specified in the original call, it will be present in the options parameter passed to the hook function. For example, consider the following snippet:
......
...@@ -72,7 +72,7 @@ The comment option can also be used on a table, see [model configuration][0] ...@@ -72,7 +72,7 @@ The comment option can also be used on a table, see [model configuration][0]
## Data types ## Data types
Below are some of the datatypes supported by sequelize. For a full and updated list, see [DataTypes](api/datatypes). Below are some of the datatypes supported by sequelize. For a full and updated list, see [DataTypes](../api/datatypes).
```js ```js
Sequelize.STRING // VARCHAR(255) Sequelize.STRING // VARCHAR(255)
...@@ -212,7 +212,7 @@ Employee ...@@ -212,7 +212,7 @@ Employee
### Defining as part of the model options ### Defining as part of the model options
Below is an example of defining the getters and setters in the model options. The `fullName` getter, is an example of how you can define pseudo properties on your models - attributes which are not actually part of your database schema. In fact, pseudo properties can be defined in two ways: using model getters, or by using a column with the [`VIRTUAL` datatype](api/datatypes#virtual). Virtual datatypes can have validations, while getters for virtual attributes cannot. Below is an example of defining the getters and setters in the model options. The `fullName` getter, is an example of how you can define pseudo properties on your models - attributes which are not actually part of your database schema. In fact, pseudo properties can be defined in two ways: using model getters, or by using a column with the [`VIRTUAL` datatype](../api/datatypes#virtual). Virtual datatypes can have validations, while getters for virtual attributes cannot.
Note that the `this.firstname` and `this.lastname` references in the `fullName` getter function will trigger a call to the respective getter functions. If you do not want that then use the `getDataValue()` method to access the raw value (see below). Note that the `this.firstname` and `this.lastname` references in the `fullName` getter function will trigger a call to the respective getter functions. If you do not want that then use the `getDataValue()` method to access the raw value (see below).
...@@ -264,7 +264,7 @@ Model validations, allow you to specify format&sol;content&sol;inheritance valid ...@@ -264,7 +264,7 @@ Model validations, allow you to specify format&sol;content&sol;inheritance valid
Validations are automatically run on `create`, `update` and `save`. You can also call `validate()` to manually validate an instance. Validations are automatically run on `create`, `update` and `save`. You can also call `validate()` to manually validate an instance.
The validations are implemented by [validator][3]. The validations are implemented by [validator.js][3].
```js ```js
var ValidateMe = sequelize.define('Foo', { var ValidateMe = sequelize.define('Foo', {
...@@ -320,7 +320,7 @@ var ValidateMe = sequelize.define('Foo', { ...@@ -320,7 +320,7 @@ var ValidateMe = sequelize.define('Foo', {
Note that where multiple arguments need to be passed to the built-in validation functions&comma; the arguments to be passed must be in an array&period; But if a single array argument is to be passed&comma; for instance an array of acceptable strings for `isIn`, this will be interpreted as multiple string arguments instead of one array argument&period; To work around this pass a single-length array of arguments&comma; such as `[['one', 'two']]` as shown above&period; Note that where multiple arguments need to be passed to the built-in validation functions&comma; the arguments to be passed must be in an array&period; But if a single array argument is to be passed&comma; for instance an array of acceptable strings for `isIn`, this will be interpreted as multiple string arguments instead of one array argument&period; To work around this pass a single-length array of arguments&comma; such as `[['one', 'two']]` as shown above&period;
To use a custom error message instead of that provided by node-validator&comma; use an object instead of the plain value or array of arguments&comma; for example a validator which needs no argument can be given a custom message with To use a custom error message instead of that provided by validator.js&comma; use an object instead of the plain value or array of arguments&comma; for example a validator which needs no argument can be given a custom message with
```js ```js
isInt: { isInt: {
...@@ -339,7 +339,7 @@ isIn: { ...@@ -339,7 +339,7 @@ isIn: {
When using custom validator functions the error message will be whatever message the thrown`Error`object holds&period; When using custom validator functions the error message will be whatever message the thrown`Error`object holds&period;
See [the node-validator project][4]for more details on the built in validation methods&period; See [the validator.js project][3] for more details on the built in validation methods&period;
**Hint&colon; **You can also define a custom function for the logging part&period; Just pass a function&period; The first parameter will be the string that is logged&period; **Hint&colon; **You can also define a custom function for the logging part&period; Just pass a function&period; The first parameter will be the string that is logged&period;
...@@ -642,6 +642,5 @@ sequelize.define('User', {}, { ...@@ -642,6 +642,5 @@ sequelize.define('User', {}, {
[0]: #configuration [0]: #configuration
[3]: https://github.com/chriso/validator.js [3]: https://github.com/chriso/validator.js
[4]: https://github.com/chriso/node-validator
[5]: /docs/latest/misc#asynchronicity [5]: /docs/latest/misc#asynchronicity
[6]: https://github.com/petkaantonov/bluebird/blob/master/API.md#spreadfunction-fulfilledhandler--function-rejectedhandler----promise [6]: https://github.com/petkaantonov/bluebird/blob/master/API.md#spreadfunction-fulfilledhandler--function-rejectedhandler----promise
...@@ -84,12 +84,12 @@ User ...@@ -84,12 +84,12 @@ User
### findAndCountAll - Search for multiple elements in the database&comma; returns both data and total count ### findAndCountAll - Search for multiple elements in the database&comma; returns both data and total count
This is a convienience method that combines`findAll`&lpar;&rpar;and `count`&lpar;&rpar;&lpar;see below&rpar;&comma; this is useful when dealing with queries related to pagination where you want to retrieve data with a `limit` and `offset` but also need to know the total number of records that match the query&period; This is a convienience method that combines`findAll` and `count` (see below) this is useful when dealing with queries related to pagination where you want to retrieve data with a `limit` and `offset` but also need to know the total number of records that match the query:
The success handler will always receive an object with two properties&colon; The success handler will always receive an object with two properties:
* `count` - an integer&comma; total number records &lpar;matching the where clause&rpar; * `count` - an integer, total number records matching the where clause
* `rows` - an array of objects&comma; the records &lpar;matching the where clause&rpar; within the limit&sol;offset range * `rows` - an array of objects, the records matching the where clause, within the limit and offset range
```js ```js
Project Project
.findAndCountAll({ .findAndCountAll({
...@@ -107,7 +107,33 @@ Project ...@@ -107,7 +107,33 @@ Project
}); });
``` ```
The options &lsqb;object&rsqb; that you pass to`findAndCountAll`&lpar;&rpar;is the same as for`findAll`&lpar;&rpar;&lpar;described below&rpar;&period; `findAndCountAll` also supports includes. Only the includes that are marked as `required` will be added to the count part:
Suppose you want to find all users who have a profile attached:
```js
User.findAndCountAll({
include: [
{ model: Profile, required: true}
],
limit 3
});
```
Because the include for `Profile` has `required` set it will result in an inner join, and only the users who have a profile will be counted. If we remove `required` from the include, both users with and without profiles will be counted. Adding a `where` clause to the include automatically makes it required:
```js
User.findAndCountAll({
include: [
{ model: Profile, where: { active: true }}
],
limit 3
});
```
The query above will only count users who have an active profile, because `required` is implicitly set to true when you add a where clause to the include.
The options object that you pass to `findAndCountAll` is the same as for `findAll` (described below).
### findAll - Search for multiple elements in the database ### findAll - Search for multiple elements in the database
```js ```js
...@@ -290,7 +316,7 @@ To recap&comma; the elements of the order &sol; group array can be the following ...@@ -290,7 +316,7 @@ To recap&comma; the elements of the order &sol; group array can be the following
### Raw queries ### Raw queries
Sometimes you might be expecting a massive dataset that you just want to display, without manipulation. For each row you select, Sequelize creates an instance with functions for updat, delete, get associations etc. If you have thousands of rows&comma; this might take some time&period; If you only need the raw data and don't want to update anything&comma; you can do like this to get the raw data&period; Sometimes you might be expecting a massive dataset that you just want to display, without manipulation. For each row you select, Sequelize creates an instance with functions for update, delete, get associations etc. If you have thousands of rows&comma; this might take some time&period; If you only need the raw data and don't want to update anything&comma; you can do like this to get the raw data&period;
```js ```js
// Are you expecting a masssive dataset from the DB, // Are you expecting a masssive dataset from the DB,
......
...@@ -7,7 +7,7 @@ Whether you are querying with findAll/find or doing bulk updates/destroys you ca ...@@ -7,7 +7,7 @@ Whether you are querying with findAll/find or doing bulk updates/destroys you ca
It's also possible to generate complex AND/OR conditions by nesting sets of `$or` and `$and`. It's also possible to generate complex AND/OR conditions by nesting sets of `$or` and `$and`.
### Basics ### Basics
``` ```js
Post.findAll({ Post.findAll({
where: { where: {
authorId: 2 authorId: 2
...@@ -48,7 +48,7 @@ Post.update({ ...@@ -48,7 +48,7 @@ Post.update({
$gt: 6, // id > 6 $gt: 6, // id > 6
$gte: 6, // id >= 6 $gte: 6, // id >= 6
$lt: 10, // id < 10 $lt: 10, // id < 10
$lte: 10, // id $lte: 10, // id <= 10
$ne: 20, // id != 20 $ne: 20, // id != 20
$between: [6, 10], // BETWEEN 6 AND 10 $between: [6, 10], // BETWEEN 6 AND 10
$notBetween: [11, 15], // NOT BETWEEN 11 AND 15 $notBetween: [11, 15], // NOT BETWEEN 11 AND 15
...@@ -141,6 +141,17 @@ JSONB can be queried in three different ways. ...@@ -141,6 +141,17 @@ JSONB can be queried in three different ways.
} }
``` ```
### Relations / Associations
```js
// Find all projects with a least one task where task.state === project.task
Project.findAll({
include: [{
model: Task,
where: { state: Sequelize.col('project.state') }
}]
})
```
## Pagination / Limiting ## Pagination / Limiting
```js ```js
// Fetch 10 instances/rows // Fetch 10 instances/rows
......
...@@ -97,7 +97,7 @@ var Mixin = module.exports = function() {}; ...@@ -97,7 +97,7 @@ var Mixin = module.exports = function() {};
// The logic for hasOne and belongsTo is exactly the same // The logic for hasOne and belongsTo is exactly the same
var singleLinked = function (Type) { var singleLinked = function (Type) {
return function(targetModel, options) { return function(targetModel, options) { // testhint options:none
if (!(targetModel instanceof this.sequelize.Model)) { if (!(targetModel instanceof this.sequelize.Model)) {
throw new Error(this.name + '.' + Utils.lowercaseFirst(Type.toString()) + ' called with something that\'s not an instance of Sequelize.Model'); throw new Error(this.name + '.' + Utils.lowercaseFirst(Type.toString()) + ' called with something that\'s not an instance of Sequelize.Model');
} }
...@@ -244,7 +244,7 @@ Mixin.belongsTo = singleLinked(BelongsTo); ...@@ -244,7 +244,7 @@ Mixin.belongsTo = singleLinked(BelongsTo);
* @param {string} [options.onUpdate='CASCADE'] * @param {string} [options.onUpdate='CASCADE']
* @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key. * @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key.
*/ */
Mixin.hasMany = function(targetModel, options) { Mixin.hasMany = function(targetModel, options) { // testhint options:none
if (!(targetModel instanceof this.sequelize.Model)) { if (!(targetModel instanceof this.sequelize.Model)) {
throw new Error(this.name + '.hasMany called with something that\'s not an instance of Sequelize.Model'); throw new Error(this.name + '.hasMany called with something that\'s not an instance of Sequelize.Model');
} }
...@@ -338,7 +338,7 @@ Mixin.hasMany = function(targetModel, options) { ...@@ -338,7 +338,7 @@ Mixin.hasMany = function(targetModel, options) {
* @param {string} [options.onUpdate='CASCADE'] * @param {string} [options.onUpdate='CASCADE']
* @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key. * @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key.
*/ */
Mixin.belongsToMany = function(targetModel, options) { Mixin.belongsToMany = function(targetModel, options) { // testhint options:none
if (!(targetModel instanceof this.sequelize.Model)) { if (!(targetModel instanceof this.sequelize.Model)) {
throw new Error(this.name + '.belongsToMany called with something that\'s not an instance of Sequelize.Model'); throw new Error(this.name + '.belongsToMany called with something that\'s not an instance of Sequelize.Model');
} }
......
...@@ -417,6 +417,8 @@ AbstractQuery.prototype.findTableNameInAttribute = function(attribute) { ...@@ -417,6 +417,8 @@ AbstractQuery.prototype.findTableNameInAttribute = function(attribute) {
AbstractQuery.prototype.getUniqueConstraintErrorMessage = function(field) { AbstractQuery.prototype.getUniqueConstraintErrorMessage = function(field) {
var message = field + ' must be unique'; var message = field + ' must be unique';
var self = this; var self = this;
if (self.model) {
Object.keys(self.model.uniqueKeys).forEach(function(key) { Object.keys(self.model.uniqueKeys).forEach(function(key) {
if (self.model.uniqueKeys[key].fields.indexOf(field.replace(/"/g, '')) >= 0) { if (self.model.uniqueKeys[key].fields.indexOf(field.replace(/"/g, '')) >= 0) {
if (self.model.uniqueKeys[key].hasOwnProperty('msg')) { if (self.model.uniqueKeys[key].hasOwnProperty('msg')) {
...@@ -424,6 +426,7 @@ AbstractQuery.prototype.getUniqueConstraintErrorMessage = function(field) { ...@@ -424,6 +426,7 @@ AbstractQuery.prototype.getUniqueConstraintErrorMessage = function(field) {
} }
} }
}); });
}
return message; return message;
}; };
......
...@@ -172,9 +172,11 @@ Query.prototype.formatError = function (err) { ...@@ -172,9 +172,11 @@ Query.prototype.formatError = function (err) {
if (match && match.length > 1) { if (match && match.length > 1) {
var fields = {} var fields = {}
, message = 'Validation error' , message = 'Validation error'
, uniqueKey = this.model.uniqueKeys[match[1]]; , uniqueKey = this.model && this.model.uniqueKeys[match[1]];
if (!!uniqueKey.msg) message = uniqueKey.msg; if (uniqueKey && !!uniqueKey.msg) {
message = uniqueKey.msg;
}
if (!!match[2]) { if (!!match[2]) {
var values = match[2].split(',').map(Function.prototype.call, String.prototype.trim); var values = match[2].split(',').map(Function.prototype.call, String.prototype.trim);
if (!!uniqueKey) { if (!!uniqueKey) {
......
...@@ -371,7 +371,7 @@ Query.prototype.formatError = function (err) { ...@@ -371,7 +371,7 @@ Query.prototype.formatError = function (err) {
case '23505': case '23505':
// there are multiple different formats of error messages for this error code // there are multiple different formats of error messages for this error code
// this regex should check at least two // this regex should check at least two
match = errDetail.match(/Key \((.*?)\)=\((.*?)\)/); match = errDetail.replace(/"/g, '').match(/Key \((.*?)\)=\((.*?)\)/);
if (match) { if (match) {
fields = Utils._.zipObject(match[1].split(', '), match[2].split(', ')); fields = Utils._.zipObject(match[1].split(', '), match[2].split(', '));
......
...@@ -204,35 +204,35 @@ Hooks.hasHooks = Hooks.hasHook; ...@@ -204,35 +204,35 @@ Hooks.hasHooks = Hooks.hasHook;
/** /**
* A hook that is run before validation * A hook that is run before validation
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with instance, options, callback(err) * @param {Function} fn A callback function that is called with instance, options
* @name beforeValidate * @name beforeValidate
*/ */
/** /**
* A hook that is run after validation * A hook that is run after validation
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with instance, options, callback(err) * @param {Function} fn A callback function that is called with instance, options
* @name afterValidate * @name afterValidate
*/ */
/** /**
* A hook that is run before creating a single instance * A hook that is run before creating a single instance
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with attributes, options, callback(err) * @param {Function} fn A callback function that is called with attributes, options
* @name beforeCreate * @name beforeCreate
*/ */
/** /**
* A hook that is run after creating a single instance * A hook that is run after creating a single instance
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with attributes, options, callback(err) * @param {Function} fn A callback function that is called with attributes, options
* @name afterCreate * @name afterCreate
*/ */
/** /**
* A hook that is run before destroying a single instance * A hook that is run before destroying a single instance
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with instance, options, callback(err) * @param {Function} fn A callback function that is called with instance, options
* *
* @name beforeDestroy * @name beforeDestroy
* @alias beforeDelete * @alias beforeDelete
...@@ -241,7 +241,7 @@ Hooks.hasHooks = Hooks.hasHook; ...@@ -241,7 +241,7 @@ Hooks.hasHooks = Hooks.hasHook;
/** /**
* A hook that is run after destroying a single instance * A hook that is run after destroying a single instance
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with instance, options, callback(err) * @param {Function} fn A callback function that is called with instance, options
* *
* @name afterDestroy * @name afterDestroy
* @alias afterDelete * @alias afterDelete
...@@ -250,35 +250,35 @@ Hooks.hasHooks = Hooks.hasHook; ...@@ -250,35 +250,35 @@ Hooks.hasHooks = Hooks.hasHook;
/** /**
* A hook that is run before updating a single instance * A hook that is run before updating a single instance
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with instance, options, callback(err) * @param {Function} fn A callback function that is called with instance, options
* @name beforeUpdate * @name beforeUpdate
*/ */
/** /**
* A hook that is run after updating a single instance * A hook that is run after updating a single instance
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with instance, options, callback(err) * @param {Function} fn A callback function that is called with instance, options
* @name afterUpdate * @name afterUpdate
*/ */
/** /**
* A hook that is run before creating instances in bulk * A hook that is run before creating instances in bulk
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with instances, options, callback(err) * @param {Function} fn A callback function that is called with instances, options
* @name beforeBulkCreate * @name beforeBulkCreate
*/ */
/** /**
* A hook that is run after creating instances in bulk * A hook that is run after creating instances in bulk
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with instances, options, callback(err) * @param {Function} fn A callback function that is called with instances, options
* @name afterBulkCreate * @name afterBulkCreate
*/ */
/** /**
* A hook that is run before destroying instances in bulk * A hook that is run before destroying instances in bulk
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with options, callback(err) * @param {Function} fn A callback function that is called with options
* *
* @name beforeBulkDestroy * @name beforeBulkDestroy
* @alias beforeBulkDelete * @alias beforeBulkDelete
...@@ -287,7 +287,7 @@ Hooks.hasHooks = Hooks.hasHook; ...@@ -287,7 +287,7 @@ Hooks.hasHooks = Hooks.hasHook;
/** /**
* A hook that is run after destroying instances in bulk * A hook that is run after destroying instances in bulk
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with options, callback(err) * @param {Function} fn A callback function that is called with options
* *
* @name afterBulkDestroy * @name afterBulkDestroy
* @alias afterBulkDelete * @alias afterBulkDelete
...@@ -296,70 +296,70 @@ Hooks.hasHooks = Hooks.hasHook; ...@@ -296,70 +296,70 @@ Hooks.hasHooks = Hooks.hasHook;
/** /**
* A hook that is run after updating instances in bulk * A hook that is run after updating instances in bulk
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with options, callback(err) * @param {Function} fn A callback function that is called with options
* @name beforeBulkUpdate * @name beforeBulkUpdate
*/ */
/** /**
* A hook that is run after updating instances in bulk * A hook that is run after updating instances in bulk
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with options, callback(err) * @param {Function} fn A callback function that is called with options
* @name afterBulkUpdate * @name afterBulkUpdate
*/ */
/** /**
* A hook that is run before a find (select) query * A hook that is run before a find (select) query
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with options, callback(err) * @param {Function} fn A callback function that is called with options
* @name beforeFind * @name beforeFind
*/ */
/** /**
* A hook that is run before a find (select) query, after any { include: {all: ...} } options are expanded * A hook that is run before a find (select) query, after any { include: {all: ...} } options are expanded
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with options, callback(err) * @param {Function} fn A callback function that is called with options
* @name beforeFindAfterExpandIncludeAll * @name beforeFindAfterExpandIncludeAll
*/ */
/** /**
* A hook that is run before a find (select) query, after all option parsing is complete * A hook that is run before a find (select) query, after all option parsing is complete
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with options, callback(err) * @param {Function} fn A callback function that is called with options
* @name beforeFindAfterOptions * @name beforeFindAfterOptions
*/ */
/** /**
* A hook that is run after a find (select) query * A hook that is run after a find (select) query
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with instance(s), options, callback(err) * @param {Function} fn A callback function that is called with instance(s), options
* @name afterFind * @name afterFind
*/ */
/** /**
* A hook that is run before a define call * A hook that is run before a define call
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with attributes, options, callback(err) * @param {Function} fn A callback function that is called with attributes, options
* @name beforeDefine * @name beforeDefine
*/ */
/** /**
* A hook that is run after a define call * A hook that is run after a define call
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with factory, callback(err) * @param {Function} fn A callback function that is called with factory
* @name afterDefine * @name afterDefine
*/ */
/** /**
* A hook that is run before Sequelize() call * A hook that is run before Sequelize() call
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with config, options, callback(err) * @param {Function} fn A callback function that is called with config, options
* @name beforeInit * @name beforeInit
*/ */
/** /**
* A hook that is run after Sequelize() call * A hook that is run after Sequelize() call
* @param {String} name * @param {String} name
* @param {Function} fn A callback function that is called with sequelize, callback(err) * @param {Function} fn A callback function that is called with sequelize
* @name afterInit * @name afterInit
*/ */
...@@ -373,8 +373,8 @@ module.exports = { ...@@ -373,8 +373,8 @@ module.exports = {
var allHooks = Object.keys(hookTypes).concat(Object.keys(hookAliases)); var allHooks = Object.keys(hookTypes).concat(Object.keys(hookAliases));
allHooks.forEach(function(hook) { allHooks.forEach(function(hook) {
Model.prototype[hook] = function(callback) { Model.prototype[hook] = function(name, callback) {
return this.addHook(hook, callback); return this.addHook(hook, name, callback);
}; };
}); });
} }
......
...@@ -152,10 +152,6 @@ InstanceValidator.prototype.validate = function() { ...@@ -152,10 +152,6 @@ InstanceValidator.prototype.validate = function() {
if (self.errors.length) { if (self.errors.length) {
return new sequelizeError.ValidationError(null, self.errors); return new sequelizeError.ValidationError(null, self.errors);
} }
return new Promise(function(resolve) {
resolve();
});
}); });
}; };
......
...@@ -176,7 +176,7 @@ Instance.prototype.setDataValue = function(key, value) { ...@@ -176,7 +176,7 @@ Instance.prototype.setDataValue = function(key, value) {
* @param {Boolean} [options.plain=false] If set to true, included instances will be returned as plain objects * @param {Boolean} [options.plain=false] If set to true, included instances will be returned as plain objects
* @return {Object|any} * @return {Object|any}
*/ */
Instance.prototype.get = function(key, options) { Instance.prototype.get = function(key, options) { // testhint options:none
if (options === undefined && typeof key === 'object') { if (options === undefined && typeof key === 'object') {
options = key; options = key;
key = undefined; key = undefined;
...@@ -246,7 +246,7 @@ Instance.prototype.get = function(key, options) { ...@@ -246,7 +246,7 @@ Instance.prototype.get = function(key, options) {
* @param {Boolean} [options.reset=false] Clear all previously set data values * @param {Boolean} [options.reset=false] Clear all previously set data values
* @alias setAttributes * @alias setAttributes
*/ */
Instance.prototype.set = function(key, value, options) { Instance.prototype.set = function(key, value, options) { // testhint options:none
var values var values
, originalValue , originalValue
, keys , keys
......
...@@ -437,7 +437,7 @@ var validateIncludedElements = function(options, tableNames) { ...@@ -437,7 +437,7 @@ var validateIncludedElements = function(options, tableNames) {
// validate all included elements // validate all included elements
var includes = options.include; var includes = options.include;
for (var index = 0; index < includes.length; index++) { for (var index = 0; index < includes.length; index++) {
var include = includes[index] = validateIncludedElement.call(this, includes[index], tableNames); var include = includes[index] = validateIncludedElement.call(this, includes[index], tableNames, options);
include.parent = options; include.parent = options;
// associations that are required or have a required child and is not a ?:M association are candidates for the subquery // associations that are required or have a required child and is not a ?:M association are candidates for the subquery
...@@ -461,7 +461,7 @@ var validateIncludedElements = function(options, tableNames) { ...@@ -461,7 +461,7 @@ var validateIncludedElements = function(options, tableNames) {
}; };
Model.$validateIncludedElements = validateIncludedElements; Model.$validateIncludedElements = validateIncludedElements;
validateIncludedElement = function(include, tableNames) { validateIncludedElement = function(include, tableNames, options) {
if (!include.hasOwnProperty('model') && !include.hasOwnProperty('association')) { if (!include.hasOwnProperty('model') && !include.hasOwnProperty('association')) {
throw new Error('Include malformed. Expected attributes: model or association'); throw new Error('Include malformed. Expected attributes: model or association');
} }
...@@ -476,17 +476,17 @@ validateIncludedElement = function(include, tableNames) { ...@@ -476,17 +476,17 @@ validateIncludedElement = function(include, tableNames) {
tableNames[include.model.getTableName()] = true; tableNames[include.model.getTableName()] = true;
if (include.attributes) { if (include.attributes && !options.raw) {
include.originalAttributes = include.attributes.slice(0); include.originalAttributes = include.attributes.slice(0);
if (include.attributes.length) { if (include.attributes.length) {
include.model.primaryKeyAttributes.forEach(function(attr) { include.model.primaryKeyAttributes.forEach(function (attr) {
if (include.attributes.indexOf(attr) === -1) { if (include.attributes.indexOf(attr) === -1) {
include.attributes.unshift(attr); include.attributes.unshift(attr);
} }
}); });
} }
} else { } else if (!include.attributes) {
include.attributes = Object.keys(include.model.tableAttributes); include.attributes = Object.keys(include.model.tableAttributes);
} }
...@@ -536,7 +536,7 @@ validateIncludedElement = function(include, tableNames) { ...@@ -536,7 +536,7 @@ validateIncludedElement = function(include, tableNames) {
// Validate child includes // Validate child includes
if (include.hasOwnProperty('include')) { if (include.hasOwnProperty('include')) {
validateIncludedElements.call(include.model, include, tableNames); validateIncludedElements.call(include.model, include, tableNames, options);
} }
return include; return include;
...@@ -917,9 +917,9 @@ Model.prototype.dropSchema = function(schema) { ...@@ -917,9 +917,9 @@ Model.prototype.dropSchema = function(schema) {
* @param {Object} [options] * @param {Object} [options]
* @param {String} [options.schemaDelimiter='.'] The character(s) that separates the schema name from the table name * @param {String} [options.schemaDelimiter='.'] The character(s) that separates the schema name from the table name
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql. * @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @return this * @return {this}
*/ */
Model.prototype.schema = function(schema, options) { Model.prototype.schema = function(schema, options) { // testhint options:none
this.options.schema = schema; this.options.schema = schema;
if (!!options) { if (!!options) {
...@@ -943,7 +943,7 @@ Model.prototype.schema = function(schema, options) { ...@@ -943,7 +943,7 @@ Model.prototype.schema = function(schema, options) {
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql. * @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @return {String|Object} * @return {String|Object}
*/ */
Model.prototype.getTableName = function(options) { Model.prototype.getTableName = function(options) { // testhint options:none
return this.QueryGenerator.addSchema(this); return this.QueryGenerator.addSchema(this);
}; };
...@@ -1142,7 +1142,7 @@ Model.prototype.all = function(options) { ...@@ -1142,7 +1142,7 @@ Model.prototype.all = function(options) {
* @param {Number} [options.limit] * @param {Number} [options.limit]
* @param {Number} [options.offset] * @param {Number} [options.offset]
* @param {Transaction} [options.transaction] Transaction to run query under * @param {Transaction} [options.transaction] Transaction to run query under
* @param {String|Object} [options.lock] Lock the selected rows. Possible options are transaction.LOCK.UPDATE and transaction.LOCK.SHARE. Postgres also supports transaction.LOCK.KEY_SHARE, transaction.LOCK.NO_KEY_UPDATE and specific model locks with joins. See [transaction.LOCK for an example](api/transaction#lock) * @param {String|Object} [options.lock] Lock the selected rows. Possible options are transaction.LOCK.UPDATE and transaction.LOCK.SHARE. Postgres also supports transaction.LOCK.KEY_SHARE, transaction.LOCK.NO_KEY_UPDATE and specific model locks with joins. See [transaction.LOCK for an example](transaction#lock)
* @param {Boolean} [options.raw] Return raw result. See sequelize.query for more information. * @param {Boolean} [options.raw] Return raw result. See sequelize.query for more information.
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql. * @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {Object} [options.having] * @param {Object} [options.having]
...@@ -1186,7 +1186,8 @@ Model.prototype.findAll = function(options) { ...@@ -1186,7 +1186,8 @@ Model.prototype.findAll = function(options) {
validateIncludedElements.call(this, options, tableNames); validateIncludedElements.call(this, options, tableNames);
if (options.attributes) { // If we're not raw, we have to make sure we include the primary key for deduplication
if (options.attributes && !options.raw) {
if (options.attributes.indexOf(this.primaryKeyAttribute) === -1) { if (options.attributes.indexOf(this.primaryKeyAttribute) === -1) {
options.originalAttributes = options.attributes; options.originalAttributes = options.attributes;
options.attributes = [this.primaryKeyAttribute].concat(options.attributes); options.attributes = [this.primaryKeyAttribute].concat(options.attributes);
...@@ -1235,7 +1236,7 @@ Model.prototype.findById = function(param, options) { ...@@ -1235,7 +1236,7 @@ Model.prototype.findById = function(param, options) {
return Promise.resolve(null); return Promise.resolve(null);
} }
options = options || {}; options = optClone(options) || {};
if (typeof param === 'number' || typeof param === 'string' || Buffer.isBuffer(param)) { if (typeof param === 'number' || typeof param === 'string' || Buffer.isBuffer(param)) {
options.where = {}; options.where = {};
...@@ -1341,6 +1342,7 @@ Model.prototype.count = function(options) { ...@@ -1341,6 +1342,7 @@ Model.prototype.count = function(options) {
Model.$injectScope(this.$scope, options); Model.$injectScope(this.$scope, options);
var col = '*'; var col = '*';
if (options.include) { if (options.include) {
col = this.name + '.' + this.primaryKeyField; col = this.name + '.' + this.primaryKeyField;
expandIncludeAll.call(this, options); expandIncludeAll.call(this, options);
...@@ -1353,6 +1355,8 @@ Model.prototype.count = function(options) { ...@@ -1353,6 +1355,8 @@ Model.prototype.count = function(options) {
options.dataType = new DataTypes.INTEGER(); options.dataType = new DataTypes.INTEGER();
options.includeIgnoreAttributes = false; options.includeIgnoreAttributes = false;
options.limit = null; options.limit = null;
options.offset = null;
options.order = null;
return this.aggregate(col, 'count', options); return this.aggregate(col, 'count', options);
}; };
...@@ -1371,22 +1375,36 @@ Model.prototype.count = function(options) { ...@@ -1371,22 +1375,36 @@ Model.prototype.count = function(options) {
* ``` * ```
* In the above example, `result.rows` will contain rows 13 through 24, while `result.count` will return the total number of rows that matched your query. * In the above example, `result.rows` will contain rows 13 through 24, while `result.count` will return the total number of rows that matched your query.
* *
* When you add includes, only those which are required (either because they have a where clause, or because `required` is explicitly set to true on the include) will be added to the count part.
*
* Suppose you want to find all users who have a profile attached:
* ```js
* User.findAndCountAll({
* include: [
* { model: Profile, required: true}
* ],
* limit 3
* });
* ```
* Because the include for `Profile` has `required` set it will result in an inner join, and only the users who have a profile will be counted. If we remove `required` from the include, both users with and without profiles will be counted
*
* @param {Object} [findOptions] See findAll * @param {Object} [findOptions] See findAll
* *
* @see {Model#findAll} for a specification of find and query options * @see {Model#findAll} for a specification of find and query options
* @return {Promise<Object>} * @return {Promise<Object>}
* @alias findAndCountAll * @alias findAndCountAll
*/ */
Model.prototype.findAndCount = function(findOptions) { Model.prototype.findAndCount = function(options) {
if (findOptions !== undefined && !_.isPlainObject(findOptions)) { if (options !== undefined && !_.isPlainObject(options)) {
throw new Error('The argument passed to findAndCount must be an options object, use findById if you wish to pass a single primary key value'); throw new Error('The argument passed to findAndCount must be an options object, use findById if you wish to pass a single primary key value');
} }
var self = this var self = this
// no limit, offset, order, attributes for the options given to count() // no limit, offset, order, attributes for the options given to count()
, countOptions = _.omit(_.clone(findOptions), ['offset', 'limit', 'order', 'attributes']); , countOptions = _.omit(_.clone(options), ['offset', 'limit', 'order', 'attributes']);
conformOptions(countOptions, this); conformOptions(countOptions, this);
if (countOptions.include) { if (countOptions.include) {
countOptions.include = _.cloneDeep(countOptions.include, function (element) { countOptions.include = _.cloneDeep(countOptions.include, function (element) {
if (element instanceof Model) return element; if (element instanceof Model) return element;
...@@ -1406,6 +1424,11 @@ Model.prototype.findAndCount = function(findOptions) { ...@@ -1406,6 +1424,11 @@ Model.prototype.findAndCount = function(findOptions) {
}); });
}; };
countOptions.include = keepNeeded(countOptions.include); countOptions.include = keepNeeded(countOptions.include);
if (countOptions.include.length) {
// Use distinct to count the number of parent rows, instead of the number of matched includes
countOptions.distinct = true;
}
} }
return self.count(countOptions).then(function(count) { return self.count(countOptions).then(function(count) {
...@@ -1415,7 +1438,7 @@ Model.prototype.findAndCount = function(findOptions) { ...@@ -1415,7 +1438,7 @@ Model.prototype.findAndCount = function(findOptions) {
rows: [] rows: []
}; };
} }
return self.findAll(findOptions).then(function(results) { return self.findAll(options).then(function(results) {
return { return {
count: count || 0, count: count || 0,
rows: (results && Array.isArray(results) ? results : []) rows: (results && Array.isArray(results) ? results : [])
...@@ -1476,7 +1499,7 @@ Model.prototype.sum = function(field, options) { ...@@ -1476,7 +1499,7 @@ Model.prototype.sum = function(field, options) {
* *
* @return {Instance} * @return {Instance}
*/ */
Model.prototype.build = function(values, options) { Model.prototype.build = function(values, options) { // testhint options:none
if (Array.isArray(values)) { if (Array.isArray(values)) {
return this.bulkBuild(values, options); return this.bulkBuild(values, options);
} }
...@@ -1502,7 +1525,7 @@ Model.prototype.build = function(values, options) { ...@@ -1502,7 +1525,7 @@ Model.prototype.build = function(values, options) {
}; };
Model.prototype.bulkBuild = function(valueSets, options) { Model.prototype.bulkBuild = function(valueSets, options) { // testhint options:none
options = _.extend({ options = _.extend({
isNewRecord: true isNewRecord: true
}, options || {}); }, options || {});
...@@ -1619,6 +1642,10 @@ Model.prototype.findOrCreate = function(options) { ...@@ -1619,6 +1642,10 @@ Model.prototype.findOrCreate = function(options) {
); );
} }
if (options.transaction === undefined && this.sequelize.constructor.cls) {
options.transaction = this.sequelize.constructor.cls.get('transaction');
}
var self = this var self = this
, internalTransaction = !options.transaction , internalTransaction = !options.transaction
, values , values
...@@ -1701,7 +1728,7 @@ Model.prototype.findOrCreate = function(options) { ...@@ -1701,7 +1728,7 @@ Model.prototype.findOrCreate = function(options) {
* @return {Promise<created>} Returns a boolean indicating whether the row was created or updated. * @return {Promise<created>} Returns a boolean indicating whether the row was created or updated.
*/ */
Model.prototype.upsert = function (values, options) { Model.prototype.upsert = function (values, options) {
options = options || {}; options = optClone(options) || {};
if (!options.fields) { if (!options.fields) {
options.fields = Object.keys(this.attributes); options.fields = Object.keys(this.attributes);
...@@ -1909,7 +1936,7 @@ Model.prototype.bulkCreate = function(records, options) { ...@@ -1909,7 +1936,7 @@ Model.prototype.bulkCreate = function(records, options) {
* @see {Model#destroy} for more information * @see {Model#destroy} for more information
*/ */
Model.prototype.truncate = function(options) { Model.prototype.truncate = function(options) {
options = options || {}; options = optClone(options) || {};
options.truncate = true; options.truncate = true;
return this.destroy(options); return this.destroy(options);
}; };
......
...@@ -90,7 +90,7 @@ QueryInterface.prototype.createTable = function(tableName, attributes, options) ...@@ -90,7 +90,7 @@ QueryInterface.prototype.createTable = function(tableName, attributes, options)
if (attribute.hasOwnProperty('defaultValue')) { if (attribute.hasOwnProperty('defaultValue')) {
if (typeof attribute.defaultValue === 'function' && ( if (typeof attribute.defaultValue === 'function' && (
attribute.defaultValue === DataTypes.NOW || attribute.defaultValue === DataTypes.NOW ||
attribute.defaultValue === DataTypes.UUIDV4 || attribute.defaultValue === DataTypes.UUIDV1 ||
attribute.defaultValue === DataTypes.UUIDV4 attribute.defaultValue === DataTypes.UUIDV4
)) { )) {
attribute.defaultValue = new attribute.defaultValue(); attribute.defaultValue = new attribute.defaultValue();
...@@ -437,6 +437,7 @@ QueryInterface.prototype.addIndex = function(tableName, attributes, options, raw ...@@ -437,6 +437,7 @@ QueryInterface.prototype.addIndex = function(tableName, attributes, options, raw
options = attributes; options = attributes;
attributes = options.fields; attributes = options.fields;
} }
// testhint argsConform.end
if (!rawTablename) { if (!rawTablename) {
// Map for backwards compat // Map for backwards compat
......
...@@ -67,7 +67,7 @@ var url = require('url') ...@@ -67,7 +67,7 @@ var url = require('url')
* @param {Object} [options.query={}] Default options for sequelize.query * @param {Object} [options.query={}] Default options for sequelize.query
* @param {Object} [options.set={}] Default options for sequelize.set * @param {Object} [options.set={}] Default options for sequelize.set
* @param {Object} [options.sync={}] Default options for sequelize.sync * @param {Object} [options.sync={}] Default options for sequelize.sync
* @param {String} [options.timezone='+00:00'] The timezone used when converting a date from the database into a JavaScript date. The timezone is also used to SET TIMEZONE when connecting to the server, to ensure that the result of NOW, CURRENT_TIMESTAMP and other time related functions have in the right timezone. For best cross platform performance use the format +/-HH:MM. * @param {String} [options.timezone='+00:00'] The timezone used when converting a date from the database into a JavaScript date. The timezone is also used to SET TIMEZONE when connecting to the server, to ensure that the result of NOW, CURRENT_TIMESTAMP and other time related functions have in the right timezone. For best cross platform performance use the format +/-HH:MM. Will also accept string versions of timezones used by moment.js (e.g. 'America/Los_Angeles'); this is useful to capture daylight savings time changes.
* @param {Function} [options.logging=console.log] A function that gets executed everytime Sequelize would log something. * @param {Function} [options.logging=console.log] A function that gets executed everytime Sequelize would log something.
* @param {Boolean} [options.omitNull=false] A flag that defines if null values should be passed to SQL queries or not. * @param {Boolean} [options.omitNull=false] A flag that defines if null values should be passed to SQL queries or not.
* @param {Boolean} [options.native=false] A flag that defines if native library shall be used or not. Currently only has an effect for postgres * @param {Boolean} [options.native=false] A flag that defines if native library shall be used or not. Currently only has an effect for postgres
...@@ -511,7 +511,7 @@ Sequelize.prototype.getQueryInterface = function() { ...@@ -511,7 +511,7 @@ Sequelize.prototype.getQueryInterface = function() {
* *
* @return {Model} * @return {Model}
*/ */
Sequelize.prototype.define = function(modelName, attributes, options) { Sequelize.prototype.define = function(modelName, attributes, options) { // testhint options:none
options = options || {}; options = options || {};
var globalOptions = this.options; var globalOptions = this.options;
...@@ -1123,6 +1123,7 @@ Sequelize.prototype.transaction = function(options, autoCallback) { ...@@ -1123,6 +1123,7 @@ Sequelize.prototype.transaction = function(options, autoCallback) {
autoCallback = options; autoCallback = options;
options = undefined; options = undefined;
} }
// testhint argsConform.end
var transaction = new Transaction(this, options) var transaction = new Transaction(this, options)
, ns = Sequelize.cls; , ns = Sequelize.cls;
......
...@@ -15,11 +15,6 @@ var Utils = module.exports = { ...@@ -15,11 +15,6 @@ var Utils = module.exports = {
var _ = lodash; var _ = lodash;
_.mixin({ _.mixin({
includes: function(str, needle){
if (needle === '') return true;
if (str === null) return false;
return String(str).indexOf(needle) !== -1;
},
camelizeIf: function(string, condition) { camelizeIf: function(string, condition) {
var result = string; var result = string;
......
...@@ -48,15 +48,16 @@ ...@@ -48,15 +48,16 @@
"wkx": "0.0.7" "wkx": "0.0.7"
}, },
"devDependencies": { "devDependencies": {
"chai": "^2.1.2", "chai": "^3.0.0",
"chai-as-promised": "^4.3.0", "chai-as-promised": "^5.1.0",
"chai-datetime": "~1.3.0", "chai-datetime": "~1.4.0",
"chai-spies": "~0.5.1", "chai-spies": "~0.6.0",
"coffee-script": "~1.9.1", "coffee-script": "~1.9.1",
"commander": "^2.6.0", "commander": "^2.6.0",
"continuation-local-storage": "^3.1.4", "continuation-local-storage": "^3.1.4",
"dox": "0.7.1", "dox": "0.7.1",
"git": "^0.1.5", "git": "^0.1.5",
"hints": "^0.2.0",
"istanbul": "~0.3.0", "istanbul": "~0.3.0",
"jshint": ">=2.4.2", "jshint": ">=2.4.2",
"lcov-result-merger": "~1.0.0", "lcov-result-merger": "~1.0.0",
......
...@@ -220,5 +220,29 @@ describe(Support.getTestDialectTeaser('Sequelize Errors'), function () { ...@@ -220,5 +220,29 @@ describe(Support.getTestDialectTeaser('Sequelize Errors'), function () {
expect(spy).to.have.been.calledOnce; expect(spy).to.have.been.calledOnce;
}); });
}); });
it('Works when unique keys are not defined in sequelize', function () {
var User = this.sequelize.define('user', {
name: {
type: Sequelize.STRING,
unique: 'unique \n unique',
}
}, { timestamps: false });
return this.sequelize.sync({ force: true }).bind(this).then(function () {
// Now let's pretend the index was created by someone else, and sequelize doesn't know about it
User = this.sequelize.define('user', {
name: Sequelize.STRING
}, { timestamps: false });
return User.create({ name: 'jan' });
}).then(function () {
// It should work even though the unique key is not defined in the model
return expect(User.create({ name: 'jan' })).to.be.rejectedWith(this.sequelize.UniqueConstraintError);
}).then(function () {
// And when the model is not passed at all
return expect(this.sequelize.query('INSERT INTO users (name) VALUES (\'jan\')')).to.be.rejectedWith(this.sequelize.UniqueConstraintError);
});
});
}); });
}); });
This diff could not be displayed because it is too large.
...@@ -1920,5 +1920,46 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -1920,5 +1920,46 @@ describe(Support.getTestDialectTeaser('Include'), function() {
expect(parseInt(post.get('commentCount'), 10)).to.equal(3); expect(parseInt(post.get('commentCount'), 10)).to.equal(3);
}); });
}); });
it('should not add primary key when including and aggregating with raw: true', function () {
var Post = this.sequelize.define('Post', {
title: DataTypes.STRING
})
, Comment = this.sequelize.define('Comment', {
content: DataTypes.TEXT
});
Post.Comments = Post.hasMany(Comment, {as: 'comments'});
return this.sequelize.sync({force: true}).bind(this).then(function () {
return Post.create({
title: Math.random().toString(),
comments: [
{content: Math.random().toString()},
{content: Math.random().toString()},
{content: Math.random().toString()},
]
}, {
include: [Post.Comments]
});
}).then(function () {
return Post.findAll({
attributes: [],
include: [
{
association: Post.Comments,
attributes: [[this.sequelize.fn('COUNT', this.sequelize.col('comments.id')), 'commentCount']]
}
],
raw: true
});
}).then(function (posts) {
expect(posts.length).to.equal(1);
var post = posts[0];
expect(post.id).not.to.be.ok;
expect(parseInt(post["comments.commentCount"], 10)).to.equal(3);
});
});
}); });
}); });
...@@ -180,5 +180,32 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -180,5 +180,32 @@ describe(Support.getTestDialectTeaser('Include'), function() {
expect(result.rows.length).to.equal(2); expect(result.rows.length).to.equal(2);
}); });
}); });
it('should return the correct count and rows when using a required belongsTo with a where condition and a limit', function() {
var Foo = this.sequelize.define('Foo', {})
, Bar = this.sequelize.define('Bar', {m: DataTypes.STRING(40)});
Foo.hasMany(Bar);
Bar.belongsTo(Foo);
return this.sequelize.sync({ force: true }).then(function() {
return Foo.bulkCreate([{id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}]);
}).then(function() {
// Make four instances of Bar, related to the first two instances of Foo
return Bar.bulkCreate([{'FooId': 1, m:'yes'}, {'FooId': 1, m:'yes'}, {'FooId': 1, m: 'no'}, {'FooId': 2, m: 'yes'}]);
}).then(function() {
// Query for the first instance of Foo which have related Bars with m === 'yes'
return Foo.findAndCountAll({
include: [{ model: Bar, where: { m: 'yes' } }],
limit: 1
});
}).then(function(result) {
// There should be 2 instances matching the query (Instances 1 and 2), see the findAll statement
expect(result.count).to.equal(2);
// The first one of those should be returned due to the limit (Foo instance 1)
expect(result.rows.length).to.equal(1);
});
});
}); });
}); });
...@@ -1641,6 +1641,7 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -1641,6 +1641,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
}); });
}); });
}); });
it('returns multiple rows when using group', function() { it('returns multiple rows when using group', function() {
var self = this; var self = this;
return this.User.bulkCreate([ return this.User.bulkCreate([
...@@ -1665,6 +1666,54 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -1665,6 +1666,54 @@ describe(Support.getTestDialectTeaser('Model'), function() {
}); });
}); });
describe("options sent to aggregate", function () {
var options, aggregateSpy;
beforeEach(function () {
options = { where: { username: 'user1'}};
aggregateSpy = sinon.spy(this.User, "aggregate");
});
afterEach(function () {
expect(aggregateSpy).to.have.been.calledWith(
sinon.match.any, sinon.match.any,
sinon.match.object.and(sinon.match.has('where', { username: 'user1'})));
aggregateSpy.restore();
});
it('modifies option "limit" by setting it to null', function() {
options.limit = 5;
return this.User.count(options).then(function() {
expect(aggregateSpy).to.have.been.calledWith(
sinon.match.any, sinon.match.any,
sinon.match.object.and(sinon.match.has('limit', null)));
});
});
it('modifies option "offset" by setting it to null', function() {
options.offset = 10;
return this.User.count(options).then(function() {
expect(aggregateSpy).to.have.been.calledWith(
sinon.match.any, sinon.match.any,
sinon.match.object.and(sinon.match.has('offset', null)));
});
});
it('modifies option "order" by setting it to null', function() {
options.order = "username";
return this.User.count(options).then(function() {
expect(aggregateSpy).to.have.been.calledWith(
sinon.match.any, sinon.match.any,
sinon.match.object.and(sinon.match.has('order', null)));
});
});
});
it('allows sql logging', function() { it('allows sql logging', function() {
var test = false; var test = false;
return this.User.count({ return this.User.count({
......
...@@ -31,6 +31,9 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -31,6 +31,9 @@ describe(Support.getTestDialectTeaser('Model'), function() {
lte: 5 lte: 5
} }
} }
},
withOrder: {
order: 'username'
} }
} }
}); });
...@@ -65,6 +68,10 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -65,6 +68,10 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('should be able to merge scopes with where', function () { it('should be able to merge scopes with where', function () {
return expect(this.ScopeMe.scope('lowAccess').count({ where: { username: 'dan'}})).to.eventually.equal(1); return expect(this.ScopeMe.scope('lowAccess').count({ where: { username: 'dan'}})).to.eventually.equal(1);
}); });
it('should ignore the order option if it is found within the scope', function () {
return expect(this.ScopeMe.scope('withOrder').count()).to.eventually.equal(4);
});
}); });
}); });
}); });
...@@ -6,6 +6,7 @@ var fs = require('fs') ...@@ -6,6 +6,7 @@ var fs = require('fs')
, Sequelize = require(__dirname + '/../index') , Sequelize = require(__dirname + '/../index')
, DataTypes = require(__dirname + '/../lib/data-types') , DataTypes = require(__dirname + '/../lib/data-types')
, Config = require(__dirname + '/config/config') , Config = require(__dirname + '/config/config')
, supportShim = require(__dirname + '/supportShim')
, chai = require('chai') , chai = require('chai')
, expect = chai.expect; , expect = chai.expect;
...@@ -27,6 +28,9 @@ Sequelize.Promise.onPossiblyUnhandledRejection(function(e, promise) { ...@@ -27,6 +28,9 @@ Sequelize.Promise.onPossiblyUnhandledRejection(function(e, promise) {
}); });
Sequelize.Promise.longStackTraces(); Sequelize.Promise.longStackTraces();
// shim all Sequelize methods for testing for correct `options.logging` passing
if (!process.env.COVERAGE) supportShim(Sequelize);
var Support = { var Support = {
Sequelize: Sequelize, Sequelize: Sequelize,
......
'use strict';
var QueryInterface = require(__dirname + '/../lib/query-interface')
, hintsModule = require('hints')
, _ = require('lodash');
/*
* Shims all Sequelize methods to test for logging passing.
* @param {Object} Sequelize Sequelize constructor
*/
module.exports = function(Sequelize) {
// Shim all Sequelize methods
shimAll(Sequelize.prototype);
shimAll(Sequelize.Model.prototype);
shimAll(Sequelize.Instance.prototype);
shimAll(QueryInterface.prototype);
// Shim Model.prototype to then shim getter/setter methods
['hasOne', 'belongsTo', 'hasMany', 'belongsToMany'].forEach(function(type) {
shimMethod(Sequelize.Model.prototype, type, function(original) {
return function(targetModel, options) {
var model = this,
association = original.apply(this, arguments);
_.forIn(association.accessors, function(accessor) {
shim(model.Instance.prototype, accessor, model.Instance.prototype[accessor].length);
});
return association;
};
});
});
// Support functions
/*
* Shims all shimmable methods on obj.
* @param {Object} obj
*/
function shimAll(obj) {
_.forIn(obj, function(method, name) {
var result = examine(method, name);
if (result) shim(obj, name, result.index, result.conform);
});
}
/*
* Given a function, checks whether is suitable for shimming to modify `options`
* and returns information about how to do that
*
* Returns an object in form:
* {
* index: [which argument of function is `options`],
* conform: [function for conforming the arguments if function accepts flexible options]
* }
*
* index is 1-based (i.e. 1st argument = 1)
*
* If method should not be shimmed, returns undefined
*
* It works out if a method can be shimmed based on:
* 1. If method name begins with lower case letter (skip classes and $/_ internals)
* 2. If one of function's arguments is called 'options'
* 3. Overiden by hints in function body
* `// testhint options:none` - skips shimming this function
* `// testhint options:2` - 2nd function argument is the `options` parameter (first arg = 1)
* `// testhint argsConform.start` & `// testhint argsConform.end`
* - this part of the function body deals with conforming flexible arguments
*
* @param {Function} method Function to examine
* @param {String} name Attribute name of this method on parent object
* @returns {Object}
*/
function examine(method, name) {
if (typeof method !== 'function') return;
// find test hints if provided
var obj = hintsModule.full(method.toString(), 'testhint', {function: true}),
hints = obj.hints,
tree = obj.tree;
var result = {};
// extract function arguments
var args = getFunctionArguments(tree);
// create args conform function
result.conform = getArgumentsConformFn(method, args, obj.hintsPos, tree);
// use hints to find index
var hint = hints.options;
if (hint === 'none') return;
if (hint && hint.match(/^\d+$/)) {
result.index = hint * 1;
return result;
}
// skip if function name does not start with lower case letter
if (!name.match(/^[a-z]/)) return;
// find 'options' argument - if none, then skip
var index = args.indexOf('options');
if (index === -1) return;
result.index = index + 1;
return result;
}
/*
* Shims a method to check for `options.logging`.
* The method then:
* Injects `options.logging` if called from within the tests.
* Throws if called from within Sequelize and not passed correct `options.logging`
*
* @param {Object} obj Object which is parent of this method
* @param {String} name Name of method on object to shim
* @param {Integer} index Index of argument which is `options` (1-based)
* @param {Function} conform Function to conform function arguments
*/
function shim(obj, name, index, conform) {
index--;
shimMethod(obj, name, function(original) {
return function() {
var args = Sequelize.Utils.sliceArgs(arguments),
fromTests = calledFromTests();
if (conform) args = conform.apply(this, arguments);
if (fromTests) {
args[index] = addLogger(args[index]);
} else {
testLogger(args[index]);
}
var result;
// NB next line written as a single statement to avoid bug with uncaught rejection
return (result = original.apply(this, args)) instanceof Sequelize.Promise ?
result.finally(finish) :
finish();
function finish() {
if (fromTests) removeLogger(args[index]);
return result;
}
};
});
}
/*
* Shims a method with given wrapper function
*
* @param {Object} obj Object which is parent of this method
* @param {String} name Name of method on object to shim
* @param {Function} wrapper Wrapper function
*/
function shimMethod(obj, name, wrapper) {
var original = obj[name];
if (original.__testShim) return;
if (original.__testShimmedTo) {
obj[name] = original.__testShimmedTo;
} else {
obj[name] = wrapper(original);
obj[name].__testShim = original;
original.__testShimmedTo = obj[name];
}
}
/*
* Adds `logging` function to `options`.
* If existing `logging` attribute, shims it.
*
* @param {Object} options
* @returns {Object} Options with `logging` attribute added
*/
function addLogger(options) {
if (!options) options = {};
var hadLogging = options.hasOwnProperty('logging'),
originalLogging = options.logging;
options.logging = function(msg) {
if (originalLogging) {
return originalLogging.apply(this, arguments);
} else {
logger(msg);
}
};
options.logging.__testLoggingFn = true;
if (hadLogging) options.logging.__originalLogging = originalLogging;
return options;
}
/*
* Revert `options.logging` to original value
*
* @param {Object} options
* @returns {Object} Options with `logging` attribute reverted to original value
*/
function removeLogger(options) {
if (options.logging && options.logging.__testLoggingFn) {
if (options.logging.hasOwnProperty('__originalLogging')) {
options.logging = options.logging.__originalLogging;
} else {
delete options.logging;
}
}
}
/*
* Checks if `options.logging` is an injected logging function
*
* @param {Object} options
* @throws {Error} Throws if `options.logging` is not a shimmed logging function
*/
function testLogger(options) {
if (!options || !options.logging || !options.logging.__testLoggingFn) throw new Error('options.logging has been lost');
}
/*
* Checks if this method called from the tests
* (as opposed to being called within Sequelize codebase).
*
* @returns {Boolean} true if this method called from within the tests
*/
var pathRegStr = _.escapeRegExp(__dirname + '/'),
regExp = new RegExp('^\\s+at\\s+(' + pathRegStr + '|.+ \\(' + pathRegStr + ')');
function calledFromTests() {
return !!((new Error()).stack.split(/[\r\n]+/)[3].match(regExp));
}
/*
* Logging function
*
* @param {String} msg Logging message
*/
function logger(msg) {
if (process.env.SEQ_LOG) console.log(msg);
}
};
// Helper functions for examining code for hints
/*
* Returns arguments of a function as an array, from it's AST
*
* @tree {Object} tree Abstract syntax tree of function's code
* @returns {Array} Array of names of `method`'s arguments
*/
function getFunctionArguments(tree) {
return tree.body[0].params.map(function(param) {return param.name;});
}
/*
* Extracts conform arguments section from function body and turns into function.
* That function is called with the same signature as the original function,
* conforms them into the standard order, and returns the arguments as an array.
*
* Returns undefined if no conform arguments hints.
*
* @param {Function} method Function to inspect
* @param {Array} args Array of names of `method`'s arguments
* @param {Object} hints Hints object containing code hints parsed from code
* @tree {Object} tree Abstract syntax tree of function's code
* @returns {Function} Function which will conform method's arguments and return as an array
*/
function getArgumentsConformFn(method, args, hints, tree) {
// check if argsConform hints present
hints = hints.argsConform;
if (!hints) return;
if (hints.start && !hints.end) throw new Error('Options conform section has no end');
if (!hints.end) return;
// extract
var start = hints.start ? hints.start.end : tree.body[0].body.start + 1,
body = method.toString().slice(start, hints.end.start);
// create function that conforms arguments
return new Function(args, body + ';return [' + args + '];'); // jshint ignore:line
}
'use strict';
/* jshint -W030 */
var chai = require('chai')
, sinon = require('sinon')
, expect = chai.expect
, Support = require(__dirname + '/support')
, _ = require('lodash')
, current = Support.sequelize
, Promise = current.Promise;
describe(Support.getTestDialectTeaser('Hooks'), function() {
beforeEach(function () {
this.Model = current.define('m');
});
describe('callback', function () {
// Legacy - remove at some point
it('success', function () {
this.Model.beforeCreate(function (attributes, options, fn) {
fn();
});
return expect(this.Model.runHooks('beforeCreate', {}, {})).to.be.resolved;
});
it('error', function () {
this.Model.beforeCreate(function (attributes, options, fn) {
fn('No!');
});
return expect(this.Model.runHooks('beforeCreate', {}, {})).to.be.rejectedWith('No!');
});
});
describe('arguments', function () {
it('hooks can modify passed arguments', function () {
this.Model.addHook('beforeCreate', function (options) {
options.answer = 41;
});
var options = {};
return this.Model.runHooks('beforeCreate', options).then(function () {
expect(options.answer).to.equal(41);
});
});
});
describe('multiple hooks', function () {
beforeEach(function () {
this.hook1 = sinon.spy();
this.hook2 = sinon.spy();
this.hook3 = sinon.spy();
});
describe('runs all hooks on success', function () {
afterEach(function () {
expect(this.hook1).to.have.been.calledOnce;
expect(this.hook2).to.have.been.calledOnce;
expect(this.hook3).to.have.been.calledOnce;
});
it('using addHook', function () {
this.Model.addHook('beforeCreate', this.hook1);
this.Model.addHook('beforeCreate', this.hook2);
this.Model.addHook('beforeCreate', this.hook3);
return this.Model.runHooks('beforeCreate');
});
it('using function', function () {
this.Model.beforeCreate(this.hook1);
this.Model.beforeCreate(this.hook2);
this.Model.beforeCreate(this.hook3);
return this.Model.runHooks('beforeCreate');
});
it('using define', function () {
return current.define('M', {}, {
hooks: {
beforeCreate: [this.hook1, this.hook2, this.hook3]
}
}).runHooks('beforeCreate');
});
it('using a mixture', function () {
var Model = current.define('M', {}, {
hooks: {
beforeCreate: this.hook1
}
});
Model.beforeCreate(this.hook2);
Model.addHook('beforeCreate', this.hook3);
return Model.runHooks('beforeCreate');
});
});
it('stops execution when a hook throws', function () {
this.Model.beforeCreate(function () {
this.hook1();
throw new Error('No!');
}.bind(this));
this.Model.beforeCreate(this.hook2);
return expect(this.Model.runHooks('beforeCreate')).to.be.rejected.then(function () {
expect(this.hook1).to.have.been.calledOnce;
expect(this.hook2).not.to.have.been.called;
}.bind(this));
});
it('stops execution when a hook rejects', function () {
this.Model.beforeCreate(function () {
this.hook1();
return Promise.reject('No!');
}.bind(this));
this.Model.beforeCreate(this.hook2);
return expect(this.Model.runHooks('beforeCreate')).to.be.rejected.then(function () {
expect(this.hook1).to.have.been.calledOnce;
expect(this.hook2).not.to.have.been.called;
}.bind(this));
});
});
describe('global hooks', function () {
describe('using addHook', function () {
it('invokes the global hook', function () {
var globalHook = sinon.spy();
current.addHook('beforeUpdate', globalHook);
return this.Model.runHooks('beforeUpdate').then(function () {
expect(globalHook).to.have.been.calledOnce;
});
});
it('invokes the global hook, when the model also has a hook', function () {
var globalHookBefore = sinon.spy()
, globalHookAfter = sinon.spy()
, localHook = sinon.spy();
current.addHook('beforeUpdate', globalHookBefore);
var Model = current.define('m', {}, {
hooks: {
beforeUpdate: localHook
}
});
current.addHook('beforeUpdate', globalHookAfter);
return Model.runHooks('beforeUpdate').then(function () {
expect(globalHookBefore).to.have.been.calledOnce;
expect(globalHookAfter).to.have.been.calledOnce;
expect(localHook).to.have.been.calledOnce;
expect(localHook).to.have.been.calledBefore(globalHookBefore);
expect(localHook).to.have.been.calledBefore(globalHookAfter);
});
});
});
describe('using define hooks', function () {
beforeEach(function () {
this.beforeCreate = sinon.spy();
this.sequelize = Support.createSequelizeInstance({
define: {
hooks: {
beforeCreate: this.beforeCreate
}
}
});
});
it('runs the global hook when no hook is passed', function () {
var Model = this.sequelize.define('M', {}, {
hooks: {
beforeUpdate: _.noop // Just to make sure we can define other hooks without overwriting the global one
}
});
return Model.runHooks('beforeCreate').bind(this).then(function () {
expect(this.beforeCreate).to.have.been.calledOnce;
});
});
it('does not run the global hook when the model specifies its own hook', function () {
var localHook = sinon.spy()
, Model = this.sequelize.define('M', {}, {
hooks: {
beforeCreate: localHook
}
});
return Model.runHooks('beforeCreate').bind(this).then(function () {
expect(this.beforeCreate).not.to.have.been.called;
expect(localHook).to.have.been.calledOnce;
});
});
});
});
describe('#removeHook', function() {
it('should remove hook', function() {
var hook1 = sinon.spy()
, hook2 = sinon.spy();
this.Model.addHook('beforeCreate', 'myHook', hook1);
this.Model.beforeCreate('myHook2', hook2);
return this.Model.runHooks('beforeCreate').bind(this).then(function() {
expect(hook1).to.have.been.calledOnce;
expect(hook2).to.have.been.calledOnce;
hook1.reset();
hook2.reset();
this.Model.removeHook('beforeCreate', 'myHook');
this.Model.removeHook('beforeCreate', 'myHook2');
return this.Model.runHooks('beforeCreate');
}).then(function() {
expect(hook1).not.to.have.been.called;
expect(hook2).not.to.have.been.called;
});
});
});
describe('#addHook', function() {
it('should add additional hook when previous exists', function() {
var hook1 = sinon.spy()
, hook2 = sinon.spy()
, Model;
Model = this.sequelize.define('Model', {}, {
hooks: { beforeCreate: hook1 }
});
Model.addHook('beforeCreate', hook2);
return Model.runHooks('beforeCreate').then(function() {
expect(hook1).to.have.been.calledOnce;
expect(hook2).to.have.been.calledOnce;
});
});
});
describe('aliases', function() {
beforeEach(function () {
this.beforeDelete = sinon.spy();
this.afterDelete = sinon.spy();
});
afterEach(function () {
expect(this.beforeDelete).to.have.been.calledOnce;
expect(this.afterDelete).to.have.been.calledOnce;
});
describe('direct method', function() {
it('#delete', function() {
this.Model.beforeDelete(this.beforeDelete);
this.Model.afterDelete(this.afterDelete);
return Promise.join(
this.Model.runHooks('beforeDestroy'),
this.Model.runHooks('afterDestroy')
);
});
});
describe('.hook() method', function() {
it('#delete', function() {
this.Model.hook('beforeDelete', this.beforeDelete);
this.Model.hook('afterDelete', this.afterDelete);
return Promise.join(
this.Model.runHooks('beforeDestroy'),
this.Model.runHooks('afterDestroy')
);
});
});
});
describe('promises', function() {
it('can return a promise', function() {
var self = this;
this.Model.beforeBulkCreate(function() {
return self.sequelize.Promise.resolve();
});
return expect(this.Model.runHooks('beforeBulkCreate')).to.be.resolved;
});
it('can return undefined', function() {
this.Model.beforeBulkCreate(function() {
// This space intentionally left blank
});
return expect(this.Model.runHooks('beforeBulkCreate')).to.be.resolved;
});
it('can return an error by rejecting', function() {
this.Model.beforeCreate(function() {
return Promise.reject(new Error('Forbidden'));
});
return expect(this.Model.runHooks('beforeCreate')).to.be.rejectedWith('Forbidden');
});
it('can return an error by throwing', function() {
this.Model.beforeCreate(function() {
throw (new Error('Forbidden'));
});
return expect(this.Model.runHooks('beforeCreate')).to.be.rejectedWith('Forbidden');
});
});
});
'use strict';
/* jshint -W030 */
var chai = require('chai')
, expect = chai.expect
, Support = require(__dirname + '/../support')
, current = Support.sequelize
, cls = require('continuation-local-storage')
, sinon = require('sinon')
, stub = sinon.stub
, Promise = require('bluebird');
describe(Support.getTestDialectTeaser('Model'), function() {
describe('method findOrCreate', function () {
before(function () {
current.constructor.cls = cls.createNamespace('sequelize');
});
after(function () {
delete current.constructor.cls;
});
beforeEach(function () {
this.User = current.define('User', {}, {
name: 'John'
});
this.transactionStub = stub(this.User.sequelize, 'transaction');
this.transactionStub.returns(new Promise(function () {}));
this.clsStub = stub(current.constructor.cls, 'get');
this.clsStub.returns({ id: 123 });
});
afterEach(function () {
this.transactionStub.restore();
this.clsStub.restore();
});
it('should use transaction from cls if available', function () {
var options = {
where : {
name : 'John'
}
};
this.User.findOrCreate(options);
expect(this.clsStub.calledOnce).to.equal(true, 'expected to ask for transaction');
});
it('should not use transaction from cls if provided as argument', function () {
var options = {
where : {
name : 'John'
},
transaction : { id : 123 }
};
this.User.findOrCreate(options);
expect(this.clsStub.called).to.equal(false);
});
});
});
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!