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

Commit 14e98ff2 by Mick Hansen

Merge pull request #3614 from sequelize/refactor-scopes

Refactor scopes
2 parents f970157b 6fd677ed
# Next # Next
- [FEATURE] Lock modes in Postgres now support `OF table` - [FEATURE] Lock modes in Postgres now support `OF table`
- [FEATURE] New transaction lock modes `FOR KEY SHARE` and `NO KEY UPDATE` for Postgres 9.3+ - [FEATURE] New transaction lock modes `FOR KEY SHARE` and `NO KEY UPDATE` for Postgres 9.3+
- [FEATURE/REFACTOR] Rewritten scopes with complete support for includes and scopes across associations
# 2.1.0 # 2.1.0
- [BUG] Enable standards conforming strings on connection in postgres. Adresses [#3545](https://github.com/sequelize/sequelize/issues/3545) - [BUG] Enable standards conforming strings on connection in postgres. Adresses [#3545](https://github.com/sequelize/sequelize/issues/3545)
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
#### Backwards compatibility changes #### Backwards compatibility changes
- Events support have been removed so using `.on('succes')` or `.succes()` is no longer supported. - Events support have been removed so using `.on('succes')` or `.succes()` is no longer supported.
- Trying to apply a scope that does not exist will always throw an error
# 2.0.6 # 2.0.6
- [BUG] Don't update virtual attributes in Model.update. Fixes [#2860](https://github.com/sequelize/sequelize/issues/2860) - [BUG] Don't update virtual attributes in Model.update. Fixes [#2860](https://github.com/sequelize/sequelize/issues/2860)
......
...@@ -188,14 +188,153 @@ UserProjects = sequelize.define('UserProjects', { ...@@ -188,14 +188,153 @@ UserProjects = sequelize.define('UserProjects', {
}) })
``` ```
## Scopes
This section concerns association scopes. For a definition of assocation scopes vs. scopes on associated models, see [Scopes](docs/scopes).
Association scopes allow you to place a scope (a set of default attributes for `get` and `create`) on the association. Scopes can be placed both on the associated model (the target of the association), and on the through table for n:m relations.
#### 1:m
Assume we have tables Comment, Post and Image. A comment can be associated to either an image or a post via `commentable_id` and `commentable` - we say that Post and Image are `Commentable`
```js
this.Comment = this.sequelize.define('comment', {
title: Sequelize.STRING,
commentable: Sequelize.STRING,
commentable_id: Sequelize.INTEGER
}, {
instanceMethods: {
getItem: function() {
return this['get' + this.get('commentable').substr(0, 1).toUpperCase() + this.get('commentable').substr(1)]();
}
}
});
this.Post.hasMany(this.Comment, {
foreignKey: 'commentable_id',
scope: {
commentable: 'post'
}
});
this.Comment.belongsTo(this.Post, {
foreignKey: 'commentable_id',
as: 'post'
});
this.Image.hasMany(this.Comment, {
foreignKey: 'commentable_id',
scope: {
commentable: 'image'
}
});
this.Comment.belongsTo(this.Image, {
foreignKey: 'commentable_id',
as: 'image'
});
```
Note that the Image -> Comment and Post -> Comment relations define a scope, `commentable: 'image'` and `commentable: 'post'` respectively. This scope is automatically applied when using the association functions:
```js
Image.getComments()
SELECT * FROM comments WHERE commentable_id = 42 AND commentable = 'image';
Image.createComment({
title: 'Awesome!'
})
INSERT INTO comments (title, commentable_id, commentable) VALUES ('Awesome!', 'image', 42);
Image.addComment(comment);
UPDATE comments SET commentable_id = 42, commentable = 'image'
```
The `getItem` utility function on `Comment` completes the picture - it simply converts the `commentable` string into a call to etiher `getImage` or `getPost`, providing an abstraction over whether a comment belongs to a post or an image.
#### n:m
Continuing with the idea of a polymorphic model, consider a tag table - an item can have multiple tags, and a tag can be related to several item
For brevity, the example only shows a Post model, but in reality Tag would be related to several other models.
```js
ItemTag = sequelize.define('item_tag', {
tag_id: {
type: DataTypes.INTEGER,
unique: 'item_tag_taggable'
},
taggable: {
type: DataTypes.STRING,
unique: 'item_tag_taggable'
},
taggable_id: {
type: DataTypes.INTEGER,
unique: 'item_tag_taggable',
references: null
}
});
Tag = sequelize.define('tag', {
name: DataTypes.STRING
});
Post.belongsToMany(Tag, {
through: {
model: ItemTag,
unique: false,
scope: {
taggable: 'post'
}
},
foreignKey: 'taggable_id',
constraints: false
});
Tag.belongsToMany(Post, {
through: {
model: ItemTag,
unique: false
},
foreignKey: 'tag_id'
});
```
Notice that the scoped column (`taggable`) is now on the through model (`ItemTag`).
We could also define a more restrictive association, for example to get all pending tags for a post by applying a scope of both the through model (`ItemTag`) and the target model (`Tag`):
```js
Post.hasMany(Tag, {
through: {
model: ItemTag,
unique: false,
scope: {
taggable: 'post'
}
},
scope: {
status: 'pending'
},
as: 'pendingTags',
foreignKey: 'taggable_id',
constraints: false
});
Post.getPendingTags();
```
```sql
SELECT `tag`.* INNER JOIN `item_tags` AS `item_tag`
ON `tag`.`id` = `item_tag`.`tagId`
AND `item_tag`.`taggable_id` = 42
AND `item_tag`.`taggable` = 'post'
WHERE (`tag`.`status` = 'pending');
```
`constraints: false` disables references constraints on the `taggable_id` column. Because the column is polymorphic, we cannot say that it `REFERENCES` a specific table.
## Naming strategy ## Naming strategy
By default sequelize will use the model name (the name passed to `sequelize.define`) to figure out the name of the model when used in associations. For example, a model named `user` will add the functions `get/set/add User` to instances of the associated model, and a property named `.user` in eager loading, while a model named `User` will add the same functions, but a property named `.User` (notice the upper case U) in eager loading. By default sequelize will use the model name (the name passed to `sequelize.define`) to figure out the name of the model when used in associations. For example, a model named `user` will add the functions `get/set/add User` to instances of the associated model, and a property named `.user` in eager loading, while a model named `User` will add the same functions, but a property named `.User` (notice the upper case U) in eager loading.
As we've already seen, you can alias models in associations using `as`. In single associations (has one and belongs to), the alias should be singular, while for many associations (has many) it should be plural. Sequelize then uses the [inflection ][0]library to convert the alias to its singular form. However, this might not always work for irregular or non-english words. In this case, you can provide both the plural and the singular form of the alias: As we've already seen, you can alias models in associations using `as`. In single associations (has one and belongs to), the alias should be singular, while for many associations (has many) it should be plural. Sequelize then uses the [inflection ][0]library to convert the alias to its singular form. However, this might not always work for irregular or non-english words. In this case, you can provide both the plural and the singular form of the alias:
```js ```js
User.belongsToMany(Project, { as: { singular: 'task', plural: 'tasks' }}) User.belongsToMany(Project, { as: { singular: 'task', plural: 'tasks' }})
// Notice that inflection has no problem singularizing tasks, this is just for illustrative purposes. // Notice that inflection has no problem singularizing tasks, this is just for illustrative purposes.
``` ```
......
## Data retrieval / Finders
Finder methods are designed to get data from the database. The returned data isn't just a plain object, but instances of one of the defined classes. Check the next major chapter about instances for further information. But as those things are instances, you can e.g. use the just describe expanded instance methods. So, here is what you can do:
### find - Search for one specific element in the database
```js
// search for known ids
Project.find(123).then(function(project) {
// project will be an instance of Project and stores the content of the table entry
// with id 123. if such an entry is not defined you will get null
})
// search for attributes
Project.find({ where: {title: 'aProject'} }).then(function(project) {
// project will be the first entry of the Projects table with the title 'aProject' || null
})
Project.find({
where: {title: 'aProject'},
attributes: ['id', ['name', 'title']]
}).then(function(project) {
// project will be the first entry of the Projects table with the title 'aProject' || null
// project.title will contain the name of the project
})
```
### findOrCreate - Search for a specific element or create it if not available
The method `findOrCreate` can be used to check if a certain element already exists in the database. If that is the case the method will result in a respective instance. If the element does not yet exist, it will be created.
Let's assume we have an empty database with a `User` model which has a `username` and a `job`.
```js
User
.findOrCreate({where: {username: 'sdepold'}, defaults: {job: 'Technical Lead JavaScript'}})
.spread(function(user, created) {
console.log(user.get({
plain: true
}))
console.log(created)
/*
{
username: 'sdepold',
job: 'Technical Lead JavaScript',
id: 1,
createdAt: Fri Mar 22 2013 21: 28: 34 GMT + 0100(CET),
updatedAt: Fri Mar 22 2013 21: 28: 34 GMT + 0100(CET)
}
created: true
*/
})
```
The code created a new instance. So when we already have an instance ...
```js
User
.create({ username: 'fnord', job: 'omnomnom' })
.then(function() {
User
.findOrCreate({where: {username: 'fnord'}, defaults: {job: 'something else'}})
.spread(function(user, created) {
console.log(user.get({
plain: true
}))
console.log(created)
/*
{
username: 'fnord',
job: 'omnomnom',
id: 2,
createdAt: Fri Mar 22 2013 21: 28: 34 GMT + 0100(CET),
updatedAt: Fri Mar 22 2013 21: 28: 34 GMT + 0100(CET)
}
created: false
*/
})
})
```
... the existing entry will not be changed. See the `job` of the second user, and the fact that created was false.
### findAndCountAll - Search for multiple elements in the database, returns both data and total count
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:
* `count` - an integer, total number records (matching the where clause)
* `rows` - an array of objects, the records (matching the where clause) within the limit/offset range
```js
Project
.findAndCountAll({
where: {
title: {
$like: 'foo%'
}
},
offset: 10,
limit: 2
})
.then(function(result) {
console.log(result.count);
console.log(result.rows);
});
```
The options [object] that you pass to`findAndCountAll`()is the same as for`findAll`()(described below).
### findAll - Search for multiple elements in the database
```js
// find multiple entries
Project.findAll().then(function(projects) {
// projects will be an array of all Project instances
})
// also possible:
Project.all().then(function(projects) {
// projects will be an array of all Project instances
})
// search for specific attributes - hash usage
Project.findAll({ where: { name: 'A Project' } }).then(function(projects) {
// projects will be an array of Project instances with the specified name
})
// search with string replacements
Project.findAll({ where: ["id > ?", 25] }).then(function(projects) {
// projects will be an array of Projects having a greater id than 25
})
// search within a specific range
Project.findAll({ where: { id: [1,2,3] } }).then(function(projects) {
// projects will be an array of Projects having the id 1, 2 or 3
// this is actually doing an IN query
})
Project.findAll({
where: {
id: {
$gt: 6, // id > 6
$gte: 6, // id >= 6
$lt: 10, // id < 10
$lte: 10, // id <= 10
$ne: 20, // id != 20
$between: [6, 10], // BETWEEN 6 AND 10
$notBetween: [11, 15], // NOT BETWEEN 11 AND 15
$in: [1, 2], // IN [1, 2]
$like: '%hat', // LIKE '%hat'
$notLike: '%hat' // NOT LIKE '%hat'
$iLike: '%hat' // ILIKE '%hat' (case insensitive)
$notILike: '%hat' // NOT ILIKE '%hat'
$overlap: [1, 2] // && [1, 2] (PG array overlap operator)
$contains: [1, 2] // @> [1, 2] (PG array contains operator)
$contained: [1, 2] // <@ [1, 2] (PG array contained by operator)
},
status: {
$not: false, // status NOT FALSE
}
}
})
```
### Complex filtering / OR / NOT queries
It's possible to do complex where queries with multiple levels of nested AND, OR and NOT conditions. In order to do that you can use `$or`, `$and` or `$not`:
```js
Project.find({
where: {
name: 'a project',
$or: [
{ id: [1,2,3] },
{ id: { $gt: 10 } }
]
}
})
Project.find({
where: {
name: 'a project',
id: {
$or: [
[1,2,3],
{ $gt: 10 }
]
}
}
})
```
Both pieces of code code will generate the following:
```sql
SELECT *
FROM `Projects`
WHERE (
`Projects`.`name` = 'a project'
AND (`Projects`.`id` IN (1,2,3) OR `Projects`.`id` > 10)
)
LIMIT 1;
```
`$not` example:
```js
Project.find({
where: {
name: 'a project',
$not: [
{ id: [1,2,3] },
{ array: { $contains: [3,4,5] } }
]
}
});
```
Will generate:
```sql
SELECT *
FROM `Projects`
WHERE (
`Projects`.`name` = 'a project'
AND NOT (`Projects`.`id` IN (1,2,3) OR `Projects`.`array` @> ARRAY[1,2,3]::INTEGER[])
)
LIMIT 1;
```
### Manipulating the dataset with limit&comma; offset&comma; order and group
To get more relevant data&comma; you can use limit&comma; offset&comma; order and grouping&colon;
```js
// limit the results of the query
Project.findAll({ limit: 10 })
// step over the first 10 elements
Project.findAll({ offset: 10 })
// step over the first 10 elements, and take 2
Project.findAll({ offset: 10, limit: 2 })
```
The syntax for grouping and ordering are equal&comma; so below it is only explained with a single example for group&comma; and the rest for order&period; Everything you see below can also be done for group
```js
Project.findAll({order: 'title DESC'})
// yields ORDER BY title DESC
Project.findAll({group: 'name'})
// yields GROUP BY name
```
Notice how in the two examples above&comma; the string provided is inserted verbatim into the query&comma; i&period;e&period; column names are not escaped&period; When you provide a string to order &sol; group&comma; this will always be the case. If you want to escape column names&comma; you should provide an array of arguments&comma; even though you only want to order &sol; group by a single column
```js
something.find({
order: [
'name',
// will return `name`
'username DESC',
// will return `username DESC` -- i.e. don't do it!
['username', 'DESC'],
// will return `username` DESC
sequelize.fn('max', sequelize.col('age')),
// will return max(`age`)
[sequelize.fn('max', sequelize.col('age')), 'DESC'],
// will return max(`age`) DESC
[sequelize.fn('otherfunction', sequelize.col('col1'), 12, 'lalala'), 'DESC'],
// will return otherfunction(`col1`, 12, 'lalala') DESC
[sequelize.fn('otherfunction', sequelize.fn('awesomefunction', sequelize.col('col'))), 'DESC']
// will return otherfunction(awesomefunction(`col`)) DESC, This nesting is potentially infinite!
[{ raw: 'otherfunction(awesomefunction(`col`))' }, 'DESC']
// This won't be quoted, but direction will be added
]
})
```
To recap&comma; the elements of the order &sol; group array can be the following&colon;
* String - will be quoted
* Array - first element will be qouted&comma; second will be appended verbatim
* Object -
* Raw will be added verbatim without quoting
* Everything else is ignored&comma; and if raw is not set&comma; the query will fail
* Sequelize&period;fn and Sequelize&period;col returns functions and quoted cools
### 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;
```js
// Are you expecting a masssive dataset from the DB,
// and don't want to spend the time building DAOs for each entry?
// You can pass an extra query option to get the raw data instead:
Project.findAll({ where: ... }, { raw: true })
```
### count - Count the occurences of elements in the database
There is also a method for counting database objects&colon;
```js
Project.count().then(function(c) {
console.log("There are " + c + " projects!")
})
Project.count({ where: ["id > ?", 25] }).then(function(c) {
console.log("There are " + c + " projects with an id greater than 25.")
})
```
### max - Get the greatest value of a specific attribute within a specific table
And here is a method for getting the max value of an attribute&colon;f
```js
/*
Let's assume 3 person objects with an attribute age.
The first one is 10 years old,
the second one is 5 years old,
the third one is 40 years old.
*/
Project.max('age').then(function(max) {
// this will return 40
})
Project.max('age', { where: { age: { lt: 20 } } }).then(function(max) {
// will be 10
})
```
### min - Get the least value of a specific attribute within a specific table
And here is a method for getting the min value of an attribute&colon;
```js
/*
Let's assume 3 person objects with an attribute age.
The first one is 10 years old,
the second one is 5 years old,
the third one is 40 years old.
*/
Project.min('age').then(function(min) {
// this will return 5
})
Project.min('age', { where: { age: { $gt: 5 } } }).then(function(min) {
// will be 10
})
```
### sum - Sum the value of specific attributes
In order to calculate the sum over a specific column of a table, you can
use the `sum` method.
```js
/*
Let's assume 3 person objects with an attribute age.
The first one is 10 years old,
the second one is 5 years old,
the third one is 40 years old.
*/
Project.sum('age').then(function(sum) {
// this will return 55
})
Project.sum('age', { where: { age: { $gt: 5 } } }).then(function(sum) {
// wil be 50
})
```
## Eager loading
When you are retrieving data from the database there is a fair chance that you also want to get associations with the same query - this is called eager loading. The basic idea behind that, is the use of the attribute `include` when you are calling `find` or `findAll`. Lets assume the following setup:
```js
var User = sequelize.define('User', { name: Sequelize.STRING })
, Task = sequelize.define('Task', { name: Sequelize.STRING })
, Tool = sequelize.define('Tool', { name: Sequelize.STRING })
Task.belongsTo(User)
User.hasMany(Task)
User.hasMany(Tool, { as: 'Instruments' })
sequelize.sync().done(function() {
// this is where we continue ...
})
```
OK&period; So&comma; first of all&comma; let's load all tasks with their associated user&period;
```js
Task.findAll({ include: [ User ] }).then(function(tasks) {
console.log(JSON.stringify(tasks))
/*
[{
"name": "A Task",
"id": 1,
"createdAt": "2013-03-20T20:31:40.000Z",
"updatedAt": "2013-03-20T20:31:40.000Z",
"UserId": 1,
"User": {
"name": "John Doe",
"id": 1,
"createdAt": "2013-03-20T20:31:45.000Z",
"updatedAt": "2013-03-20T20:31:45.000Z"
}
}]
*/
})
```
Notice that the accessor is singular as the association is one-to-something&period;
Next thing&colon; Loading of data with many-to-something associations&excl;
```js
User.findAll({ include: [ Task ] }).then(function(users) {
console.log(JSON.stringify(users))
/*
[{
"name": "John Doe",
"id": 1,
"createdAt": "2013-03-20T20:31:45.000Z",
"updatedAt": "2013-03-20T20:31:45.000Z",
"Tasks": [{
"name": "A Task",
"id": 1,
"createdAt": "2013-03-20T20:31:40.000Z",
"updatedAt": "2013-03-20T20:31:40.000Z",
"UserId": 1
}]
}]
*/
})
```
Notice that the accessor is plural&period; This is because the association is many-to-something&period;
If an association is aliased (using the `as` option), you must specify this alias when including the model&period; Notice how the user's `Tool`s are aliased as `Instruments` above&period; In order to get that right you have to specify the model you want to load&comma; as well as the alias&colon;
```js
User.findAll({ include: [{ model: Tool, as: 'Instruments' }] }).then(function(users) {
console.log(JSON.stringify(users))
/*
[{
"name": "John Doe",
"id": 1,
"createdAt": "2013-03-20T20:31:45.000Z",
"updatedAt": "2013-03-20T20:31:45.000Z",
"Instruments": [{
"name": "Toothpick",
"id": 1,
"createdAt": null,
"updatedAt": null,
"UserId": 1
}]
}]
*/
})
```
### Including everything
To include all attributes, you can pass a single object with `all: true`:
```js
User.findAll({ include: [{ all: true }]});
```
### Ordering Eager Loaded Associations
In the case of a one-to-many relationship.
```js
Company.findAll({ include: [ Division ], order: [ [ Division, 'name' ] ] });
Company.findAll({ include: [ Division ], order: [ [ Division, 'name', 'DESC' ] ] });
Company.findAll({
include: [ { model: Division, as: 'Div' } ],
order: [ [ { model: Division, as: 'Div' }, 'name' ] ]
});
Company.findAll({
include: [ { model: Division, include: [ Department ] } ],
order: [ [ Division, Department, 'name' ] ]
});
```
In the case of many-to-many joins, you are also able to sort by attributes in the through table.
```js
Company.findAll({
include: [ { model: Division, include: [ Department ] } ],
order: [ [ Division, DepartmentDivision, 'name' ] ]
});
```
### Nested eager loading
You can used nested eager loading to load all related models of a related model:
```js
User.findAll({
include: [
{model: Tool, as: 'Instruments', include: [
{model: Teacher, include: [ /* etc */]}
]}
]
}).then(function(users) {
console.log(JSON.stringify(users))
/*
[{
"name": "John Doe",
"id": 1,
"createdAt": "2013-03-20T20:31:45.000Z",
"updatedAt": "2013-03-20T20:31:45.000Z",
"Instruments": [{ // 1:M and N:M association
"name": "Toothpick",
"id": 1,
"createdAt": null,
"updatedAt": null,
"UserId": 1,
"Teacher": { // 1:1 association
"name": "Jimi Hendrix"
}
}]
}]
*/
})
```
Include all also supports nested loading:
```js
User.findAll({ include: [{ all: true, nested: true }]});
```
# Definition
Scoping allows you to define commonly used queries that you can easily use later. Scopes can include all the same attributes as regular finders, `where`, `include`, `limit` etc.
Scopes are defined in the model definition and can be finder objects, or functions returning finder objects - except for the default scope, which can only be an object:
```js
var Project = sequelize.define('project', {
// Attributes
}, {
defaultScope: {
where: {
active: true
}
},
scopes: {
deleted: {
where: {
deleted: true
}
},
activeUsers: {
include: [
{ model: User, where: { active: true }}
]
}
random: function () {
return {
where: {
someNumber: Math.random()
}
}
},
accessLevel: function (value) {
return {
where: {
accessLevel: {
$gte: value
}
}
}
}
}
});
```
The default scope is always applied. This means, that with the model definition above, `Project.findAll()` will create the following query:
```sql
SELECT * FROM projects WHERE active = true
```
The default scope can be removed by calling `.unscoped()`, `.scope(null)`, or by invoking another scope:
```js
Project.scope('deleted').findAll(); // Removes the default scope
```
```sql
SELECT * FROM projects WHERE deleted = true
```
# Usage
Scopes are applied by calling `.scope` on the model definition, passing the name of one or more scopes. `.scope` returns a fully functional model instance with all the regular methods: `.findAll`, `.update`, `.count`, `.destroy` etc. You can save this model instance and reuse it later:
```js
var DeletedProjects = Project.scope('deleted');
DeletedProjects.findAll();
// some time passes
// let's look for deleted projects again!
DeletedProjects.findAll();
```
Scopes apply to `.find`, `.findAll`, `.count`, `.update` and `.destroy`.
Scopes which are functions can be invoked in two ways. If the scope does not take any arguments it can be invoked as normally. If the scope takes arguments, pass an object:
```js
Project.scope('random', { method: ['accessLevel', 19]}).findAll();
```
```sql
SELECT * FROM projects WHERE someNumber = 42 AND accessLevel >= 19
```
## Merging
Several scopes can be applied simultaneously by passing an array of scopes to `.scope`, or by passing the scopes as consequtive arguments.
```js
// These two are equivalent
Project.scope('deleted', 'activeUsers').findAll();
Project.scope(['deleted', 'activeUsers']).findAll();
```
```sql
SELECT * FROM projects
INNER JOIN users ON projects.userId = users.id
AND users.active = true
```
If you want to apply another scope alongside the default scope, pass the key `defaultScope` to `.scope`:
```js
Project.scope('defaultScope', 'deleted').findAll();
```
```sql
SELECT * FROM projects WHERE active = true AND deleted = true
```
When invoking several scopes, keys from subsequent scopes will overwrite previous ones (similar to [_.assign](https://lodash.com/docs#assign)). Consider two scopes:
```js
{
scope1: {
where: {
firstName: 'bob',
age: {
$gt: 20
}
},
limit: 2
},
scope2: {
where: {
age: {
$gt: 30
}
},
limit: 10
}
}
```
Calling `.scope('scope1', 'scope2')` will yield the following query
```sql
WHERE firstName = 'bob' AND age > 30 LIMIT 10
```
Note how `limit` and `age` are overwritten by `scope2`, whíle `firstName` is preserved. `limit`, `offset`, `order`, `paranoid`, `lock` and `raw` are overwritten, while `where` and `include` are shallowly merged. This means that identical keys in the where objects, and subsequent includes of the same model will both overwrite each other.
The same merge logic applies when passing a find object directly to findAll on a scoped model:
```js
Project.scope('deleted').findAll({
where: {
firstName: 'john'
}
})
```
```sql
WHERE deleted = true AND firstName = 'john'
```
Here the `deleted` scope is merged with the finder. If we were to pass `where: { firstName: 'john', deleted: false }` to the finder, the `deleted` scope would be overwritten.
# Associations
Sequelize has two different but related scope concepts in relation to associations. The difference is subtle but important:
* **Assocation scopes** Allow you to specify default attributes when getting and setting associations - useful when implementing polymorphic associations. This scope is only invoked on the association between the two models, when using the `get`, `set`, `add` and `create` associated model functions
* **Scopes on associated models** Allows you to apply default and other scopes when fetching associations, and allows you to pass a scoped model when creating associtaions. These scopes both apply to regular finds on the model and to find through the association.
As an example, consider the models Post and Comment. Comment is associated to several other models (Image, Video etc.) and the association between Comment and other models is polymorphic, which means that Comment stores a `commentable` column, in addition to the foreign key `commentable_id`.
The polymorphic association can be implemented with an _association scope_ :
```js
this.Post.hasMany(this.Comment, {
foreignKey: 'commentable_id',
scope: {
commentable: 'post'
}
});
```
When calling `post.getComments()`, this will automatically add `WHERE commentable = 'post'`. Similarly, when adding new comments to a post, `commentable` will automagically be set to `'post'`. The association scope is meant to live in the background without the programmer having to worry about it - it cannot be disabled. For a more complete polymorphic example, see [Association scopes](docs/associations/#scopes)
Consider then, that Post has a default scope which only shows active posts: `where: { active: true }`. This scope lives on the associated model (Post), and not on the association like the `commentable` scope did. Just like the default scope is applied when calling `Post.findAll()`, it is also applied when calling `User.getPosts()` - this will only return the active posts for that user.
To disable the default scope, pass `scope: null` to the getter: `User.getPosts({ scope: null })`. Similarly, if you want to apply other scopes, pass an array like you would to `.scope`:
```js
User.getPosts({ scope: ['scope1', 'scope2']});
```
If you want to create a shortcut method to a scope on an associated model, you can pass the scoped model to the association. Consider a shortcut to get all deleted posts for a user:
```js
var Post = sequelize.define('post', attributes, {
defaultScope: {
where: {
active: true
}
},
scopes: {
deleted: {
where: {
deleted: true
}
}
}
});
User.hasMany(Post); // regular getPosts association
User.hasMany(Post.scope('deleted'), { as: 'deletedPosts' });
```
```js
User.getPosts(); // WHERE active = true
User.getDeletedPosts(); // WHERE deleted = true
```
...@@ -310,10 +310,7 @@ module.exports = (function() { ...@@ -310,10 +310,7 @@ module.exports = (function() {
, throughWhere; , throughWhere;
if (association.scope) { if (association.scope) {
scopeWhere = {}; scopeWhere = _.clone(association.scope);
Object.keys(association.scope).forEach(function (attribute) {
scopeWhere[attribute] = association.scope[attribute];
}.bind(this));
} }
options.where = { options.where = {
...@@ -351,7 +348,16 @@ module.exports = (function() { ...@@ -351,7 +348,16 @@ module.exports = (function() {
}); });
} }
return association.target.findAll(options, queryOptions); var model = association.target;
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
}
}
return model.findAll(options, queryOptions);
}; };
obj[this.accessors.hasAll] = function(instances, options) { obj[this.accessors.hasAll] = function(instances, options) {
...@@ -359,21 +365,24 @@ module.exports = (function() { ...@@ -359,21 +365,24 @@ module.exports = (function() {
, where; , where;
options = options || {}; options = options || {};
options.scope = false;
instances.forEach(function(instance) { instances.forEach(function(instance) {
if (instance instanceof association.target.Instance) { if (instance instanceof association.target.Instance) {
where = new Utils.or([where, instance.primaryKeyValues]); where = { $or: [where, instance.primaryKeyValues]};
} else { } else {
var _where = {}; var _where = {};
_where[association.target.primaryKeyAttribute] = instance; _where[association.target.primaryKeyAttribute] = instance;
where = new Utils.or([where, _where]); where = { $or: [where, _where]};
} }
}); });
options.where = new Utils.and([ options.where = {
where, $and: [
options.where where,
]); options.where
]
};
return instance[association.accessors.get]( return instance[association.accessors.get](
options, options,
...@@ -388,6 +397,7 @@ module.exports = (function() { ...@@ -388,6 +397,7 @@ module.exports = (function() {
, where; , where;
options = options || {}; options = options || {};
options.scope = false;
if (param instanceof association.target.Instance) { if (param instanceof association.target.Instance) {
where = param.primaryKeyValues; where = param.primaryKeyValues;
...@@ -396,10 +406,12 @@ module.exports = (function() { ...@@ -396,10 +406,12 @@ module.exports = (function() {
where[association.target.primaryKeyAttribute] = param; where[association.target.primaryKeyAttribute] = param;
} }
options.where = new Utils.and([ options.where = {
where, $and: [
options.where where,
]); options.where
]
};
return instance[association.accessors.get]( return instance[association.accessors.get](
options, options,
...@@ -419,7 +431,9 @@ module.exports = (function() { ...@@ -419,7 +431,9 @@ module.exports = (function() {
options = options || {}; options = options || {};
var instance = this; var instance = this;
return instance[association.accessors.get]({}, { return instance[association.accessors.get]({
scope: false
}, {
transaction: options.transaction, transaction: options.transaction,
logging: options.logging logging: options.logging
}).then(function(oldAssociatedObjects) { }).then(function(oldAssociatedObjects) {
...@@ -668,6 +682,7 @@ module.exports = (function() { ...@@ -668,6 +682,7 @@ module.exports = (function() {
} }
return instance[association.accessors.get]({ return instance[association.accessors.get]({
scope: false,
where: newInstance.primaryKeyValues where: newInstance.primaryKeyValues
}, { }, {
transaction: (additionalAttributes || {}).transaction, transaction: (additionalAttributes || {}).transaction,
......
...@@ -97,20 +97,31 @@ module.exports = (function() { ...@@ -97,20 +97,31 @@ module.exports = (function() {
BelongsTo.prototype.injectGetter = function(instancePrototype) { BelongsTo.prototype.injectGetter = function(instancePrototype) {
var association = this; var association = this;
instancePrototype[this.accessors.get] = function(params) { instancePrototype[this.accessors.get] = function(options) {
var where = {}; var where = {};
where[association.targetIdentifier] = this.get(association.identifier);
params = association.target.__optClone(params) || {}; options = association.target.__optClone(options) || {};
params.where = (params.where && [params.where]) || [];
where[association.targetIdentifier] = this.get(association.identifier); options.where = {
params.where.push(where); $and: [
options.where,
where
]
};
params.where = new Utils.and(params.where); if (options.limit === undefined) options.limit = null;
if (params.limit === undefined) params.limit = null; var model = association.target;
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
}
}
return association.target.find(params); return model.find(options);
}; };
return this; return this;
......
...@@ -18,16 +18,27 @@ module.exports = (function() { ...@@ -18,16 +18,27 @@ module.exports = (function() {
}.bind(this)); }.bind(this));
} }
options.where = new Utils.and([ options.where = {
new Utils.where( $and: [
this.target.rawAttributes[this.association.identifier], new Utils.where(
this.instance[this.source.primaryKeyAttribute] this.target.rawAttributes[this.association.identifier],
), this.instance[this.source.primaryKeyAttribute]
scopeWhere, ),
options.where scopeWhere,
]); options.where
]
return this.association.target.all(options, queryOptions); };
var model = this.association.target;
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
}
}
return model.all(options, queryOptions);
}; };
HasManySingleLinked.prototype.injectSetter = function(oldAssociations, newAssociations, defaultAttributes) { HasManySingleLinked.prototype.injectSetter = function(oldAssociations, newAssociations, defaultAttributes) {
...@@ -66,7 +77,7 @@ module.exports = (function() { ...@@ -66,7 +77,7 @@ module.exports = (function() {
updateWhere = {}; updateWhere = {};
updateWhere[primaryKey] = obsoleteIds; updateWhere[primaryKey] = obsoleteIds;
promises.push(this.association.target.update( promises.push(this.association.target.unscoped().update(
update, update,
Utils._.extend(options, { Utils._.extend(options, {
allowNull: [self.association.identifier], allowNull: [self.association.identifier],
...@@ -100,7 +111,7 @@ module.exports = (function() { ...@@ -100,7 +111,7 @@ module.exports = (function() {
updateWhere[primaryKey] = unassociatedIds; updateWhere[primaryKey] = unassociatedIds;
promises.push(this.association.target.update( promises.push(this.association.target.unscoped().update(
update, update,
Utils._.extend(options, { Utils._.extend(options, {
allowNull: [self.association.identifier], allowNull: [self.association.identifier],
......
...@@ -334,6 +334,7 @@ module.exports = (function() { ...@@ -334,6 +334,7 @@ module.exports = (function() {
obj[this.accessors.get] = function(options, queryOptions) { obj[this.accessors.get] = function(options, queryOptions) {
options = association.target.__optClone(options) || {}; options = association.target.__optClone(options) || {};
queryOptions = queryOptions || {}; queryOptions = queryOptions || {};
var Class = Object(association.through.model) === association.through.model ? HasManyDoubleLinked : HasManySingleLinked; var Class = Object(association.through.model) === association.through.model ? HasManyDoubleLinked : HasManySingleLinked;
return new Class(association, this).injectGetter(options, queryOptions); return new Class(association, this).injectGetter(options, queryOptions);
...@@ -344,21 +345,24 @@ module.exports = (function() { ...@@ -344,21 +345,24 @@ module.exports = (function() {
, where; , where;
options = options || {}; options = options || {};
options.scope = false;
instances.forEach(function(instance) { instances.forEach(function(instance) {
if (instance instanceof association.target.Instance) { if (instance instanceof association.target.Instance) {
where = new Utils.or([where, instance.primaryKeyValues]); where = { $or: [where, instance.primaryKeyValues]};
} else { } else {
var _where = {}; var _where = {};
_where[association.target.primaryKeyAttribute] = instance; _where[association.target.primaryKeyAttribute] = instance;
where = new Utils.or([where, _where]); where = { $or: [where, _where]};
} }
}); });
options.where = new Utils.and([ options.where = {
where, $and: [
options.where where,
]); options.where
]
};
return instance[association.accessors.get]( return instance[association.accessors.get](
options, options,
...@@ -373,6 +377,7 @@ module.exports = (function() { ...@@ -373,6 +377,7 @@ module.exports = (function() {
, where; , where;
options = options || {}; options = options || {};
options.scope = false;
if (param instanceof association.target.Instance) { if (param instanceof association.target.Instance) {
where = param.primaryKeyValues; where = param.primaryKeyValues;
...@@ -381,10 +386,12 @@ module.exports = (function() { ...@@ -381,10 +386,12 @@ module.exports = (function() {
where[association.target.primaryKeyAttribute] = param; where[association.target.primaryKeyAttribute] = param;
} }
options.where = new Utils.and([ options.where = {
where, $and: [
options.where where,
]); options.where
]
};
return instance[association.accessors.get]( return instance[association.accessors.get](
options, options,
...@@ -420,7 +427,9 @@ module.exports = (function() { ...@@ -420,7 +427,9 @@ module.exports = (function() {
var instance = this; var instance = this;
return instance[association.accessors.get]({}, { return instance[association.accessors.get]({
scope: false
}, {
transaction: (additionalAttributes || {}).transaction, transaction: (additionalAttributes || {}).transaction,
logging: (additionalAttributes || {}).logging logging: (additionalAttributes || {}).logging
}).then(function(oldAssociatedObjects) { }).then(function(oldAssociatedObjects) {
...@@ -467,7 +476,8 @@ module.exports = (function() { ...@@ -467,7 +476,8 @@ module.exports = (function() {
} }
return instance[association.accessors.get]({ return instance[association.accessors.get]({
where: newInstance.primaryKeyValues where: newInstance.primaryKeyValues,
scope: false
}, { }, {
transaction: (additionalAttributes || {}).transaction transaction: (additionalAttributes || {}).transaction
}).then(function(currentAssociatedObjects) { }).then(function(currentAssociatedObjects) {
...@@ -483,7 +493,9 @@ module.exports = (function() { ...@@ -483,7 +493,9 @@ module.exports = (function() {
obj[this.accessors.remove] = function(oldAssociatedObject, options) { obj[this.accessors.remove] = function(oldAssociatedObject, options) {
var instance = this; var instance = this;
return instance[association.accessors.get]({}, options).then(function(currentAssociatedObjects) { return instance[association.accessors.get]({
scope: false
}, options).then(function(currentAssociatedObjects) {
var newAssociations = []; var newAssociations = [];
if (!(oldAssociatedObject instanceof association.target.Instance)) { if (!(oldAssociatedObject instanceof association.target.Instance)) {
...@@ -506,7 +518,9 @@ module.exports = (function() { ...@@ -506,7 +518,9 @@ module.exports = (function() {
obj[this.accessors.removeMultiple] = function(oldAssociatedObjects, options) { obj[this.accessors.removeMultiple] = function(oldAssociatedObjects, options) {
var instance = this; var instance = this;
return instance[association.accessors.get]({}, options).then(function(currentAssociatedObjects) { return instance[association.accessors.get]({
scope: false
}, options).then(function(currentAssociatedObjects) {
var newAssociations = []; var newAssociations = [];
// Ensure the oldAssociatedObjects array is an array of target instances // Ensure the oldAssociatedObjects array is an array of target instances
......
...@@ -96,20 +96,31 @@ module.exports = (function() { ...@@ -96,20 +96,31 @@ module.exports = (function() {
HasOne.prototype.injectGetter = function(instancePrototype) { HasOne.prototype.injectGetter = function(instancePrototype) {
var association = this; var association = this;
instancePrototype[this.accessors.get] = function(params) { instancePrototype[this.accessors.get] = function(options) {
var where = {}; var where = {};
where[association.identifier] = this.get(association.sourceIdentifier);
params = association.target.__optClone(params) || {}; options = association.target.__optClone(options) || {};
params.where = (params.where && [params.where]) || [];
where[association.identifier] = this.get(association.sourceIdentifier); options.where = {
params.where.push(where); $and: [
options.where,
where
]
};
params.where = new Utils.and(params.where); if (options.limit === undefined) options.limit = null;
if (params.limit === undefined) params.limit = null; var model = association.target;
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
}
}
return association.target.find(params); return model.find(options);
}; };
return this; return this;
...@@ -121,6 +132,8 @@ module.exports = (function() { ...@@ -121,6 +132,8 @@ module.exports = (function() {
instancePrototype[this.accessors.set] = function(associatedInstance, options) { instancePrototype[this.accessors.set] = function(associatedInstance, options) {
var instance = this; var instance = this;
options = options || {};
options.scope = false;
return instance[association.accessors.get](options).then(function(oldInstance) { return instance[association.accessors.get](options).then(function(oldInstance) {
if (oldInstance) { if (oldInstance) {
oldInstance[association.identifier] = null; oldInstance[association.identifier] = null;
......
...@@ -366,7 +366,7 @@ Mixin.getAssociation = function(target, alias) { ...@@ -366,7 +366,7 @@ Mixin.getAssociation = function(target, alias) {
if (this.associations.hasOwnProperty(associationName)) { if (this.associations.hasOwnProperty(associationName)) {
var association = this.associations[associationName]; var association = this.associations[associationName];
if (association.target === target && (alias === undefined ? !association.isAliased : association.as === alias)) { if (association.target.name === target.name && (alias === undefined ? !association.isAliased : association.as === alias)) {
return association; return association;
} }
} }
......
...@@ -1223,7 +1223,7 @@ module.exports = (function() { ...@@ -1223,7 +1223,7 @@ module.exports = (function() {
} else if (Utils._.isPlainObject(options.where)) { } else if (Utils._.isPlainObject(options.where)) {
options.where['__' + as] = subQueryWhere; options.where['__' + as] = subQueryWhere;
} else { } else {
options.where = new Utils.and(options.where, subQueryWhere); options.where = { $and: [options.where, subQueryWhere] };
} }
} }
...@@ -1533,7 +1533,9 @@ module.exports = (function() { ...@@ -1533,7 +1533,9 @@ module.exports = (function() {
result = (value === 'NULL') ? key + ' IS NULL' : [key, value].join(smth.comparator); result = (value === 'NULL') ? key + ' IS NULL' : [key, value].join(smth.comparator);
} else if (_.isPlainObject(value)) { } else if (_.isPlainObject(value)) {
result = this.plainObjectToWhere(value, key, key, factory).join(' AND '); result = this.whereItemQuery(smth.attribute, value, {
model: factory
});
} else { } else {
if (typeof value === 'boolean') { if (typeof value === 'boolean') {
value = this.booleanValue(value); value = this.booleanValue(value);
...@@ -1726,7 +1728,6 @@ module.exports = (function() { ...@@ -1726,7 +1728,6 @@ module.exports = (function() {
outerBinding = ''; outerBinding = '';
if (key === '$not') outerBinding = 'NOT '; if (key === '$not') outerBinding = 'NOT ';
if (Array.isArray(value)) { if (Array.isArray(value)) {
value = value.map(function (item) { value = value.map(function (item) {
var itemQuery = self.whereItemsQuery(item, options, ' AND '); var itemQuery = self.whereItemsQuery(item, options, ' AND ');
...@@ -2166,83 +2167,6 @@ module.exports = (function() { ...@@ -2166,83 +2167,6 @@ module.exports = (function() {
return [_key, _value].join(' ' + logicResult + ' '); return [_key, _value].join(' ' + logicResult + ' ');
}, },
/*
Takes a hash and transforms it into a mysql where condition: {key: value, key2: value2} ==> key=value AND key2=value2
The values are transformed by the relevant datatype.
*/
hashToWhereConditions: function(hash, dao, options) {
var result = [];
options = options || {};
// Closures are nice
Utils._.each(hash, function(value, key) {
var _key
, _value = null;
if (value && value._isSequelizeMethod === true && (value instanceof Utils.literal)) {
result.push(value.val);
return;
}
if (options.keysEscaped) {
_key = key;
} else {
if (this.isAssociationFilter(key, dao)) {
_key = key = this.getAssociationFilterColumn(key, dao, options);
} else {
_key = this.quoteIdentifiers(key);
}
}
if (Array.isArray(value)) {
result.push(this.arrayValue(value, key, _key, dao, 'IN'));
} else if (value && Utils._.isPlainObject(value)) {
result = result.concat(this.plainObjectToWhere(value, key, _key, dao));
} else {
if (typeof value === 'boolean') {
_value = this.booleanValue(value);
} else {
_value = this.escape(value);
}
result.push((_value === 'NULL') ? _key + ' IS NULL' : [_key, _value].join('='));
}
}.bind(this));
return result.join(' AND ');
},
plainObjectToWhere: function (value, key, _key, dao) {
var _value
, result = [];
if (!!value.join) {
//using as sentinel for join column => value
_value = this.quoteIdentifiers(value.join);
result.push([_key, _value].join('='));
} else {
for (var logic in value) {
var logicResult = Utils.getWhereLogic(logic, value[logic]);
if (logicResult === 'BETWEEN' || logicResult === 'NOT BETWEEN') {
_value = this.escape(value[logic][0]);
var _value2 = this.escape(value[logic][1]);
result.push(' (' + _key + ' ' + logicResult + ' ' + _value + ' AND ' + _value2 + ') ');
} else if (logicResult === 'IN' || logicResult === 'NOT IN' || Array.isArray(value[logic])) {
var values = Array.isArray(value[logic]) ? value[logic] : [value[logic]];
result.push(this.arrayValue(values, key, _key, dao, logicResult));
} else {
_value = this.escape(value[logic]);
result.push([_key, _value].join(' ' + logicResult + ' '));
}
}
}
return result;
},
booleanValue: function(value) { booleanValue: function(value) {
return value; return value;
} }
......
...@@ -481,7 +481,7 @@ module.exports = (function() { ...@@ -481,7 +481,7 @@ module.exports = (function() {
if (!resultMap[itemHash]) { if (!resultMap[itemHash]) {
$parent = resultMap[parentHash]; $parent = resultMap[parentHash];
$lastKeyPrefix = lastKeyPrefix(prevKey); $lastKeyPrefix = lastKeyPrefix(prevKey);
//console.log($parent, prevKey, $lastKeyPrefix);
if (includeMap[prevKey].association.isSingleAssociation) { if (includeMap[prevKey].association.isSingleAssociation) {
$parent[$lastKeyPrefix] = resultMap[itemHash] = values; $parent[$lastKeyPrefix] = resultMap[itemHash] = values;
} else { } else {
...@@ -561,7 +561,7 @@ module.exports = (function() { ...@@ -561,7 +561,7 @@ module.exports = (function() {
if (!resultMap[itemHash]) { if (!resultMap[itemHash]) {
$parent = resultMap[parentHash]; $parent = resultMap[parentHash];
$lastKeyPrefix = lastKeyPrefix(prevKey); $lastKeyPrefix = lastKeyPrefix(prevKey);
//console.log($parent, prevKey, $lastKeyPrefix);
if (includeMap[prevKey].association.isSingleAssociation) { if (includeMap[prevKey].association.isSingleAssociation) {
$parent[$lastKeyPrefix] = resultMap[itemHash] = values; $parent[$lastKeyPrefix] = resultMap[itemHash] = values;
} else { } else {
......
...@@ -48,7 +48,6 @@ module.exports = (function() { ...@@ -48,7 +48,6 @@ module.exports = (function() {
return hooks; return hooks;
}); });
this.scopeObj = {};
this.sequelize = options.sequelize; this.sequelize = options.sequelize;
this.underscored = this.underscored || this.underscoredAll; this.underscored = this.underscored || this.underscoredAll;
...@@ -192,10 +191,13 @@ module.exports = (function() { ...@@ -192,10 +191,13 @@ module.exports = (function() {
return self.primaryKeyAttributes.indexOf(key) !== -1; return self.primaryKeyAttributes.indexOf(key) !== -1;
}); });
this.$scope = _.isPlainObject(this.options.defaultScope) ? this.options.defaultScope : {};
if (typeof this.options.defaultScope === 'object') { _.each(this.options.scopes, function (scope) {
Utils.injectScope.call(this, this.options.defaultScope); if (_.isPlainObject(scope) && scope.include) {
} conformOptions(scope);
}
});
// Instance prototype // Instance prototype
this.Instance = this.DAO = function() { this.Instance = this.DAO = function() {
...@@ -484,6 +486,13 @@ module.exports = (function() { ...@@ -484,6 +486,13 @@ module.exports = (function() {
}; };
/** /**
* @return {Model}
*/
Model.prototype.unscoped = function () {
return this.scope();
};
/**
* Apply a scope created in `define` to the model. First let's look at how to create scopes: * Apply a scope created in `define` to the model. First let's look at how to create scopes:
* ```js * ```js
* var Model = sequelize.define('model', attributes, { * var Model = sequelize.define('model', attributes, {
...@@ -501,9 +510,16 @@ module.exports = (function() { ...@@ -501,9 +510,16 @@ module.exports = (function() {
* }, * },
* complexFunction: function(email, accessLevel) { * complexFunction: function(email, accessLevel) {
* return { * return {
* where: ['email like ? AND access_level >= ?', email + '%', accessLevel] * where: {
* email: {
* $like: email
* },
* accesss_level {
* $gte: accessLevel
* }
* }
* } * }
* }, * }
* } * }
* }) * })
* ``` * ```
...@@ -519,94 +535,66 @@ module.exports = (function() { ...@@ -519,94 +535,66 @@ module.exports = (function() {
* // WHERE email like 'dan@sequelize.com%' AND access_level >= 42 * // WHERE email like 'dan@sequelize.com%' AND access_level >= 42
* ``` * ```
* *
* @param {Array|Object|String|null} options* The scope(s) to apply. Scopes can either be passed as consecutive arguments, or as an array of arguments. To apply simple scopes, pass them as strings. For scope function, pass an object, with a `method` property. The value can either be a string, if the method does not take any arguments, or an array, where the first element is the name of the method, and consecutive elements are arguments to that method. Pass null to remove all scopes, including the default. * @param {Array|Object|String|null} options* The scope(s) to apply. Scopes can either be passed as consecutive arguments, or as an array of arguments. To apply simple scopes and scope functions with no arguments, pass them as strings. For scope function, pass an object, with a `method` property. The value can either be a string, if the method does not take any arguments, or an array, where the first element is the name of the method, and consecutive elements are arguments to that method. Pass null to remove all scopes, including the default.
* @return {Model} A reference to the model, with the scope(s) applied. Calling scope again on the returned model will clear the previous scope. * @return {Model} A reference to the model, with the scope(s) applied. Calling scope again on the returned model will clear the previous scope.
*/ */
Model.prototype.scope = function(option) { Model.prototype.scope = function(option) {
var self = Object.create(this) var self = Object.create(this)
, type
, options , options
, merge
, i
, scope , scope
, scopeName , scopeName;
, scopeOptions
, argLength = arguments.length
, lastArg = arguments[argLength - 1];
self.scoped = true;
// Set defaults
scopeOptions = typeof lastArg === 'object' && !Array.isArray(lastArg) && lastArg || {};
scopeOptions.silent = (scopeOptions !== null && scopeOptions.hasOwnProperty('silent') ? scopeOptions.silent : true);
// Clear out any predefined scopes...
self.scopeObj = {};
// Possible formats for option: self.$scope = {};
// String of arguments: 'hello', 'world', 'etc'
// Array: ['hello', 'world', 'etc']
// Object: {merge: 'hello'}, {method: ['scopeName' [, args1, args2..]]}, {merge: true, method: ...}
if (argLength < 1 || !option) { if (!option) {
return self; return self;
} }
for (i = 0; i < argLength; i++) { options = _.flatten(arguments);
options = Array.isArray(arguments[i]) ? arguments[i] : [arguments[i]]; options.forEach(function(option) {
scope = null;
options.forEach(function(o) { scopeName = null;
type = typeof o;
scope = null;
merge = false;
scopeName = null;
if (type === 'object') {
// Right now we only support a merge functionality for objects
if (!!o.merge) {
merge = true;
scopeName = o.merge[0];
if (Array.isArray(o.merge) && !!self.options.scopes[scopeName]) {
scope = self.options.scopes[scopeName].apply(self, o.merge.splice(1));
}
else if (typeof o.merge === 'string') {
scopeName = o.merge;
scope = self.options.scopes[scopeName];
}
}
if (!!o.method) { if (_.isPlainObject(option)) {
if (Array.isArray(o.method) && !!self.options.scopes[o.method[0]]) { if (!!option.method) {
scopeName = o.method[0]; if (Array.isArray(option.method) && !!self.options.scopes[option.method[0]]) {
scope = self.options.scopes[scopeName].apply(self, o.method.splice(1)); scopeName = option.method[0];
merge = !!o.merge; scope = self.options.scopes[scopeName].apply(self, option.method.splice(1));
}
else if (!!self.options.scopes[o.method]) {
scopeName = o.method;
scope = self.options.scopes[scopeName].apply(self);
}
} else {
scopeName = o;
scope = self.options.scopes[scopeName];
} }
else if (!!self.options.scopes[option.method]) {
if (o.where) { scopeName = option.method;
scope = o; scope = self.options.scopes[scopeName].apply(self);
merge = true;
} }
} else { } else {
scopeName = o; scope = option;
scope = self.options.scopes[scopeName];
} }
} else {
if (option === 'defaultScope' && _.isPlainObject(self.options.defaultScope)) {
scope = self.options.defaultScope;
} else {
scopeName = option;
scope = self.options.scopes[scopeName];
if (!!scope) { if (_.isFunction(scope)) {
Utils.injectScope.call(self, scope, merge); scope = scope();
} }
else if (scopeOptions.silent !== true && !!scopeName) {
throw new Error('Invalid scope ' + scopeName + ' called.');
} }
}); }
}
if (!!scope) {
_.assign(self.$scope, scope, function scopeCustomizer(objectValue, sourceValue, key) {
if (key === 'where') {
return _.assign(objectValue || {}, sourceValue);
} else if (key === 'include' && Array.isArray(objectValue) && Array.isArray(sourceValue)) {
return objectValue.concat(sourceValue);
}
return objectValue ? objectValue : sourceValue;
});
} else {
throw new Error('Invalid scope ' + scopeName + ' called.');
}
});
return self; return self;
}; };
...@@ -704,14 +692,13 @@ module.exports = (function() { ...@@ -704,14 +692,13 @@ module.exports = (function() {
tableNames[this.getTableName(options)] = true; tableNames[this.getTableName(options)] = true;
options = optClone(options || {}); options = optClone(options || {});
options = Utils._.defaults(options, {
hooks: true
});
_.defaults(options, { hooks: true });
_.assign(options, queryOptions); _.assign(options, queryOptions);
return Promise.bind(this).then(function() { return Promise.bind(this).then(function() {
conformOptions(options); conformOptions(options, this);
Model.$injectScope(this.$scope, options);
if (options.hooks) { if (options.hooks) {
return this.runHooks('beforeFind', options); return this.runHooks('beforeFind', options);
...@@ -796,6 +783,8 @@ module.exports = (function() { ...@@ -796,6 +783,8 @@ module.exports = (function() {
options = optClone(param); options = optClone(param);
} }
Model.$injectScope(this.$scope, options);
if (options.limit === undefined && !(options.where && options.where[this.primaryKeyAttribute])) { if (options.limit === undefined && !(options.where && options.where[this.primaryKeyAttribute])) {
options.limit = 1; options.limit = 1;
} }
...@@ -862,7 +851,8 @@ module.exports = (function() { ...@@ -862,7 +851,8 @@ module.exports = (function() {
*/ */
Model.prototype.count = function(options) { Model.prototype.count = function(options) {
options = Utils._.clone(options || {}); options = Utils._.clone(options || {});
conformOptions(options); conformOptions(options, this);
Model.$injectScope(this.$scope, options);
var col = '*'; var col = '*';
if (options.include) { if (options.include) {
...@@ -908,7 +898,7 @@ module.exports = (function() { ...@@ -908,7 +898,7 @@ module.exports = (function() {
// 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(findOptions), ['offset', 'limit', 'order', 'attributes']);
conformOptions(countOptions); 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;
...@@ -1015,7 +1005,7 @@ module.exports = (function() { ...@@ -1015,7 +1005,7 @@ module.exports = (function() {
} }
if (!options.includeValidated) { if (!options.includeValidated) {
conformOptions(options); conformOptions(options, this);
if (options.include) { if (options.include) {
expandIncludeAll.call(this, options); expandIncludeAll.call(this, options);
validateIncludedElements.call(this, options); validateIncludedElements.call(this, options);
...@@ -1033,7 +1023,7 @@ module.exports = (function() { ...@@ -1033,7 +1023,7 @@ module.exports = (function() {
}, options || {}); }, options || {});
if (!options.includeValidated) { if (!options.includeValidated) {
conformOptions(options); conformOptions(options, this);
if (options.include) { if (options.include) {
expandIncludeAll.call(this, options); expandIncludeAll.call(this, options);
validateIncludedElements.call(this, options); validateIncludedElements.call(this, options);
...@@ -1482,6 +1472,7 @@ module.exports = (function() { ...@@ -1482,6 +1472,7 @@ module.exports = (function() {
}, options || {}); }, options || {});
options.type = QueryTypes.BULKDELETE; options.type = QueryTypes.BULKDELETE;
Model.$injectScope(this.$scope, options);
Utils.mapOptionFieldNames(options, this); Utils.mapOptionFieldNames(options, this);
...@@ -1629,6 +1620,8 @@ module.exports = (function() { ...@@ -1629,6 +1620,8 @@ module.exports = (function() {
options.type = QueryTypes.BULKUPDATE; options.type = QueryTypes.BULKUPDATE;
Model.$injectScope(this.$scope, options);
// Remove values that are not in the options.fields // Remove values that are not in the options.fields
if (options.fields && options.fields instanceof Array) { if (options.fields && options.fields instanceof Array) {
Object.keys(values).forEach(function(key) { Object.keys(values).forEach(function(key) {
...@@ -1794,6 +1787,26 @@ module.exports = (function() { ...@@ -1794,6 +1787,26 @@ module.exports = (function() {
} }
}; };
// Inject current scope into options. Includes should have been conformed (conformOptions) before calling this
Model.$injectScope = function (scope, options) {
var filteredScope = _.omit(scope, 'include'); // Includes need special treatment
_.defaults(options, filteredScope);
_.defaults(options.where, filteredScope.where);
if (scope.include) {
options.include = options.include || [];
// Reverse so we consider the latest include first.
// This is used if several scopes specify the same include - the last scope should take precendence
scope.include.reverse().forEach(function (scopeInclude) {
if (!_.any(options.include, _.matchesDots('model.name', scopeInclude.model.name))) {
options.include.push(scopeInclude);
}
});
}
};
// private // private
// validateIncludedElements should have been called before this method // validateIncludedElements should have been called before this method
...@@ -1908,11 +1921,10 @@ module.exports = (function() { ...@@ -1908,11 +1921,10 @@ module.exports = (function() {
}.bind(this)); }.bind(this));
}; };
var conformOptions = function(options) { var conformOptions = function(options, self) {
if (!options.include) { if (!options.include) {
return; return;
} }
// if include is not an array, wrap in an array // if include is not an array, wrap in an array
if (!Array.isArray(options.include)) { if (!Array.isArray(options.include)) {
options.include = [options.include]; options.include = [options.include];
...@@ -1923,20 +1935,45 @@ module.exports = (function() { ...@@ -1923,20 +1935,45 @@ module.exports = (function() {
// convert all included elements to { model: Model } form // convert all included elements to { model: Model } form
options.include = options.include.map(function(include) { options.include = options.include.map(function(include) {
var model;
if (include instanceof Association) { if (include instanceof Association) {
include = { association: include }; if (include.target.name === self.name) {
model = include.source;
} else {
model = include.target;
}
include = { model: model, association: include };
} else if (include instanceof Model) { } else if (include instanceof Model) {
model = include;
include = { model: include }; include = { model: include };
} else if (typeof include !== 'object') { } else if (_.isPlainObject(include)) {
throw new Error('Include unexpected. Element has to be either a Model, an Association or an object.');
} else {
// convert daoFactory to model (for backwards compatibility)
if (include.hasOwnProperty('daoFactory')) { if (include.hasOwnProperty('daoFactory')) {
include.model = include.daoFactory; throw new Error('include.daoFactory is deprecated, please use include.model instead');
delete include.daoFactory; }
if (include.association) {
if (include.association.target.name === self.name) {
model = include.association.source;
} else {
model = include.association.target;
}
if (!include.model) {
include.model = model;
}
} else {
model = include.model;
} }
conformOptions(include); conformOptions(include, model);
} else {
throw new Error('Include unexpected. Element has to be either a Model, an Association or an object.');
}
if (!include.all) {
_.defaults(include, model.$scope);
} }
return include; return include;
...@@ -1985,7 +2022,7 @@ module.exports = (function() { ...@@ -1985,7 +2022,7 @@ module.exports = (function() {
} }
if (include.association && !include._pseudo && !include.model) { if (include.association && !include._pseudo && !include.model) {
if (include.association.source === this) { if (include.association.source.name === this.name) {
include.model = include.association.target; include.model = include.association.target;
} else { } else {
include.model = include.association.source; include.model = include.association.source;
...@@ -2034,7 +2071,7 @@ module.exports = (function() { ...@@ -2034,7 +2071,7 @@ module.exports = (function() {
if (through.scope) { if (through.scope) {
include.through.where = include.through.where ? new Utils.and([include.through.where, through.scope]) : through.scope; include.through.where = include.through.where ? { $and: [include.through.where, through.scope]} : through.scope;
} }
include.include.push(include.through); include.include.push(include.through);
...@@ -2046,7 +2083,7 @@ module.exports = (function() { ...@@ -2046,7 +2083,7 @@ module.exports = (function() {
} }
if (include.association.scope) { if (include.association.scope) {
include.where = include.where ? new Utils.and([include.where, include.association.scope]) : include.association.scope; include.where = include.where ? { $and: [include.where, include.association.scope] }: include.association.scope;
} }
// Validate child includes // Validate child includes
...@@ -2074,7 +2111,7 @@ module.exports = (function() { ...@@ -2074,7 +2111,7 @@ module.exports = (function() {
return; return;
} }
for (var index = 0; index < includes.length; index++) { for (var index = 0; index < includes.length; index++) {
var include = includes[index]; var include = includes[index];
if (include.all) { if (include.all) {
......
...@@ -680,22 +680,6 @@ module.exports = (function() { ...@@ -680,22 +680,6 @@ module.exports = (function() {
options.type = QueryTypes.SELECT; options.type = QueryTypes.SELECT;
// See if we need to merge options and model.scopeObj
// we're doing this on the QueryInterface level because it's a bridge between
// sequelize and the databases
if (model.options.defaultScope && Object.keys(model.options.defaultScope).length > 0) {
if (!!options) {
Utils.injectScope.call(model, options, true);
}
var scopeObj = buildScope.call(model);
Object.keys(scopeObj).forEach(function(method) {
if (typeof scopeObj[method] === 'number' || !Utils._.isEmpty(scopeObj[method])) {
options[method] = scopeObj[method];
}
});
}
options.instance = model; options.instance = model;
return this.sequelize.query( return this.sequelize.query(
this.QueryGenerator.selectQuery(tableName, options, model), this.QueryGenerator.selectQuery(tableName, options, model),
...@@ -935,16 +919,5 @@ module.exports = (function() { ...@@ -935,16 +919,5 @@ module.exports = (function() {
return this.sequelize.query(sql, options); return this.sequelize.query(sql, options);
}; };
// private
var buildScope = function() {
var smart;
// Use smartWhere to convert several {where} objects into a single where object
smart = Utils.smartWhere(this.scopeObj.where || [], this.daoFactoryManager.sequelize.options.dialect);
smart = Utils.compileSmartWhere.call(this, smart, this.daoFactoryManager.sequelize.options.dialect);
return {limit: this.scopeObj.limit || null, offset: this.scopeObj.offset || null, where: smart, order: (this.scopeObj.order || []).join(', ')};
};
return QueryInterface; return QueryInterface;
})(); })();
...@@ -568,7 +568,8 @@ module.exports = (function() { ...@@ -568,7 +568,8 @@ module.exports = (function() {
delete options.modelName; delete options.modelName;
var factory = new Model(modelName, attributes, options); var factory = new Model(modelName, attributes, options);
this.modelManager.addDAO(factory.init(this.modelManager)); factory = factory.init(this.modelManager);
this.modelManager.addDAO(factory);
this.runHooks('afterDefine', factory); this.runHooks('afterDefine', factory);
......
...@@ -6,6 +6,7 @@ var DataTypes = require('./data-types') ...@@ -6,6 +6,7 @@ var DataTypes = require('./data-types')
, ParameterValidator = require('./utils/parameter-validator') , ParameterValidator = require('./utils/parameter-validator')
, inflection = require('inflection') , inflection = require('inflection')
, _ = require('lodash') , _ = require('lodash')
, dottie = require('dottie')
, uuid = require('node-uuid'); , uuid = require('node-uuid');
var Utils = module.exports = { var Utils = module.exports = {
...@@ -52,6 +53,11 @@ var Utils = module.exports = { ...@@ -52,6 +53,11 @@ var Utils = module.exports = {
} }
} }
return result; return result;
},
matchesDots: function (dots, value) {
return function (item) {
return dottie.get(item, dots) === value;
};
} }
}); });
...@@ -87,76 +93,6 @@ var Utils = module.exports = { ...@@ -87,76 +93,6 @@ var Utils = module.exports = {
var timeZone = null; var timeZone = null;
return SqlString.formatNamedParameters(sql, parameters, timeZone, dialect); return SqlString.formatNamedParameters(sql, parameters, timeZone, dialect);
}, },
injectScope: function(scope, merge) {
var self = this;
scope = scope || {};
if (!this.scoped && self.options.defaultScope) {
self.scopeObj = Utils._.clone(self.options.defaultScope);
if (!Array.isArray(self.scopeObj.where)) {
self.scopeObj.where = [self.scopeObj.where];
}
} else {
self.scopeObj = self.scopeObj || {};
}
if (Array.isArray(scope.where)) {
self.scopeObj.where = self.scopeObj.where || [];
self.scopeObj.where.push(scope.where);
return true;
}
if (typeof scope.order === 'string') {
self.scopeObj.order = self.scopeObj.order || [];
self.scopeObj.order[self.scopeObj.order.length] = scope.order;
}
// Limit and offset are *always* merged.
if (!!scope.limit) {
self.scopeObj.limit = scope.limit;
}
if (!!scope.offset) {
self.scopeObj.offset = scope.offset;
}
// Where objects are a mixed variable. Possible values are arrays, strings, and objects
if (!!scope.where) {
// Begin building our scopeObj
self.scopeObj.where = self.scopeObj.where || [];
// Reset if we're merging!
if (merge === true && !!scope.where && !!self.scopeObj.where) {
var scopeKeys = Object.keys(scope.where);
self.scopeObj.where = self.scopeObj.where.map(function(scopeObj) {
if (!Array.isArray(scopeObj) && typeof scopeObj === 'object' && scopeObj !== null) {
return lodash.omit.apply(undefined, [scopeObj].concat(scopeKeys));
} else {
return scopeObj;
}
}).filter(function(scopeObj) {
return !lodash.isEmpty(scopeObj);
});
self.scopeObj.where = self.scopeObj.where.concat(scope.where);
}
if (Array.isArray(scope.where)) {
self.scopeObj.where.push(scope.where);
}
else if (typeof scope.where === 'object') {
Object.keys(scope.where).forEach(function() {
self.scopeObj.where.push(scope.where);
});
} else { // Assume the value is a string
self.scopeObj.where.push([scope.where]);
}
}
if (!!self.scopeObj.where) {
self.scopeObj.where = lodash.uniq(self.scopeObj.where);
}
},
cloneDeep: function(obj, fn) { cloneDeep: function(obj, fn) {
return lodash.cloneDeep(obj, function (elem) { return lodash.cloneDeep(obj, function (elem) {
// Unfortunately, lodash.cloneDeep doesn't preserve Buffer.isBuffer, which we have to rely on for binary data // Unfortunately, lodash.cloneDeep doesn't preserve Buffer.isBuffer, which we have to rely on for binary data
...@@ -239,207 +175,6 @@ var Utils = module.exports = { ...@@ -239,207 +175,6 @@ var Utils = module.exports = {
return values; return values;
}, },
// smartWhere can accept an array of {where} objects, or a single {where} object.
// The smartWhere function breaks down the collection of where objects into a more
// centralized object for each column so we can avoid duplicates
// e.g. WHERE username='dan' AND username='dan' becomes WHERE username='dan'
// All of the INs, NOT INs, BETWEENS, etc. are compressed into one key for each column
// This function will hopefully provide more functionality to sequelize in the future.
// tl;dr It's a nice way to dissect a collection of where objects and compress them into one object
smartWhere: function(whereArg, dialect) {
var self = this
, _where = {}
, logic
, type;
(Array.isArray(whereArg) ? whereArg : [whereArg]).forEach(function(where) {
// If it's an array we're already good... / it's in a format that can't be broken down further
// e.g. Util.format['SELECT * FROM world WHERE status=?', 'hello']
if (Array.isArray(where)) {
_where._ = where._ || {queries: [], bindings: []};
_where._.queries[_where._.queries.length] = where[0];
if (where.length > 1) {
var values = where.splice(1);
if (dialect === 'sqlite') {
values.forEach(function(v, i) {
if (typeof v === 'boolean') {
values[i] = (v === true ? 1 : 0);
}
});
}
_where._.bindings = _where._.bindings.concat(values);
}
}
else if (typeof where === 'object' && where !== null) {
// First iteration is trying to compress IN and NOT IN as much as possible...
// .. reason being is that WHERE username IN (?) AND username IN (?) != WHERE username IN (?,?)
Object.keys(where).forEach(function(i) {
if (Array.isArray(where[i])) {
where[i] = {
in : where[i]
};
}
});
// Build our smart object
Object.keys(where).forEach(function(i) {
type = typeof where[i];
_where[i] = _where[i] || {};
if (where[i] === null) {
// skip nulls
}
else if (Array.isArray(where[i])) {
_where[i].in = _where[i]. in || [];
_where[i]. in .concat(where[i]);
}
else if (Utils._.isPlainObject(where[i])) {
Object.keys(where[i]).forEach(function(ii) {
logic = self.getWhereLogic(ii, where[i][ii]);
switch (logic) {
case 'IN':
_where[i].in = _where[i]. in || [];
_where[i].in = _where[i]. in .concat(where[i][ii]);
break;
case 'NOT':
_where[i].not = _where[i].not || [];
_where[i].not = _where[i].not.concat(where[i][ii]);
break;
case 'BETWEEN':
_where[i].between = _where[i].between || [];
_where[i].between[_where[i].between.length] = [where[i][ii][0], where[i][ii][1]];
break;
case 'NOT BETWEEN':
_where[i].nbetween = _where[i].nbetween || [];
_where[i].nbetween[_where[i].nbetween.length] = [where[i][ii][0], where[i][ii][1]];
break;
case 'JOIN':
_where[i].joined = _where[i].joined || [];
_where[i].joined[_where[i].joined.length] = where[i][ii];
break;
default:
_where[i].lazy = _where[i].lazy || {conditions: [], bindings: []};
_where[i].lazy.conditions[_where[i].lazy.conditions.length] = logic + ' ?';
_where[i].lazy.bindings = _where[i].lazy.bindings.concat(where[i][ii]);
}
});
}
else if (type === 'string' || type === 'number' || type === 'boolean' || Buffer.isBuffer(where[i])) {
_where[i].lazy = _where[i].lazy || {conditions: [], bindings: []};
if (type === 'boolean') {
_where[i].lazy.conditions[_where[i].lazy.conditions.length] = '= ' + SqlString.escape(where[i], false, null, dialect); // sqlite is special
} else {
_where[i].lazy.conditions[_where[i].lazy.conditions.length] = '= ?';
_where[i].lazy.bindings = _where[i].lazy.bindings.concat(where[i]);
}
}
});
}
});
return _where;
},
// Converts {smart where} object(s) into an array that's friendly for Utils.format()
// NOTE: Must be applied/called from the QueryInterface
compileSmartWhere: function(obj) {
var self = this
, whereArgs = []
, text = []
, columnName;
if (typeof obj !== 'object') {
return obj;
}
for (var column in obj) {
if (column === '_') {
text[text.length] = obj[column].queries.join(' AND ');
if (obj[column].bindings.length > 0) {
whereArgs = whereArgs.concat(obj[column].bindings);
}
} else {
Object.keys(obj[column]).forEach(function(condition) {
columnName = self.QueryInterface.quoteIdentifiers(column);
switch (condition) {
case 'in':
text[text.length] = columnName + ' IN (' + obj[column][condition].map(function() { return '?'; }) + ')';
whereArgs = whereArgs.concat(obj[column][condition]);
break;
case 'not':
text[text.length] = columnName + ' NOT IN (' + obj[column][condition].map(function() { return '?'; }) + ')';
whereArgs = whereArgs.concat(obj[column][condition]);
break;
case 'between':
Object.keys(obj[column][condition]).forEach(function(row) {
text[text.length] = columnName + ' BETWEEN ? AND ?';
whereArgs = whereArgs.concat(obj[column][condition][row][0], obj[column][condition][row][1]);
});
break;
case 'nbetween':
Object.keys(obj[column][condition]).forEach(function(row) {
text[text.length] = columnName + ' BETWEEN ? AND ?';
whereArgs = whereArgs.concat(obj[column][condition][row][0], obj[column][condition][row][1]);
});
break;
case 'joined':
Object.keys(obj[column][condition]).forEach(function(row) {
text[text.length] = columnName + ' = ' + self.QueryInterface.quoteIdentifiers(obj[column][condition][row]);
});
break;
default: // lazy
text = text.concat(obj[column].lazy.conditions.map(function(val) { return columnName + ' ' + val; }));
whereArgs = whereArgs.concat(obj[column].lazy.bindings);
}
});
}
}
return Utils._.compactLite([text.join(' AND ')].concat(whereArgs));
},
getWhereLogic: function(logic, val) {
switch (logic) {
case 'join':
return 'JOIN';
case 'gte':
return '>=';
case 'gt':
return '>';
case 'lte':
return '<=';
case 'lt':
return '<';
case 'eq':
return val === null ? 'IS' : '=';
case 'ne':
return val === null ? 'IS NOT' : '!=';
case 'between':
case '..':
return 'BETWEEN';
case 'nbetween':
case 'notbetween':
case '!..':
return 'NOT BETWEEN';
case 'in':
return 'IN';
case 'not':
return 'NOT IN';
case 'like':
return 'LIKE';
case 'nlike':
case 'notlike':
return 'NOT LIKE';
case 'ilike':
return 'ILIKE';
case 'nilike':
case 'notilike':
return 'NOT ILIKE';
case 'overlap':
return '&&';
default:
return '';
}
},
argsArePrimaryKeys: function(args, primaryKeys) { argsArePrimaryKeys: function(args, primaryKeys) {
var result = (args.length === Object.keys(primaryKeys).length); var result = (args.length === Object.keys(primaryKeys).length);
if (result) { if (result) {
......
...@@ -16,7 +16,9 @@ pages: ...@@ -16,7 +16,9 @@ pages:
- ['docs/getting-started.md', 'Documentation', 'Getting Started'] - ['docs/getting-started.md', 'Documentation', 'Getting Started']
- ['docs/schema.md', 'Documentation', 'Working with table schemas'] - ['docs/schema.md', 'Documentation', 'Working with table schemas']
#- ['docs/usage.md', 'Documentation', 'Usage'] #- ['docs/usage.md', 'Documentation', 'Usage']
- ['docs/models.md', 'Documentation', 'Models'] - ['docs/models-definition.md', 'Documentation', 'Models - Definition']
- ['docs/models-usage.md', 'Documentation', 'Models - Usage']
- ['docs/scopes.md', 'Documentation', 'Scopes']
- ['docs/instances.md', 'Documentation', 'Instances'] - ['docs/instances.md', 'Documentation', 'Instances']
- ['docs/associations.md', 'Documentation', 'Relations/Associations'] - ['docs/associations.md', 'Documentation', 'Relations/Associations']
- ['docs/hooks.md', 'Documentation', 'Hooks'] - ['docs/hooks.md', 'Documentation', 'Hooks']
......
...@@ -290,7 +290,7 @@ if (Support.dialectIsMySQL()) { ...@@ -290,7 +290,7 @@ if (Support.dialectIsMySQL()) {
) )
}; };
}], }],
expectation: "SELECT * FROM `myTable` WHERE (`myTable`.`archived` IS NULL AND COALESCE(`place_type_codename`, `announcement_type_codename`) IN ('Lost','Found'));", expectation: "SELECT * FROM `myTable` WHERE (`myTable`.`archived` IS NULL AND COALESCE(`place_type_codename`, `announcement_type_codename`) IN ('Lost', 'Found'));",
context: QueryGenerator, context: QueryGenerator,
needsSequelize: true needsSequelize: true
}, { }, {
...@@ -564,38 +564,6 @@ if (Support.dialectIsMySQL()) { ...@@ -564,38 +564,6 @@ if (Support.dialectIsMySQL()) {
arguments: ['User', ['foo', 'bar']], arguments: ['User', ['foo', 'bar']],
expectation: 'DROP INDEX user_foo_bar ON `User`' expectation: 'DROP INDEX user_foo_bar ON `User`'
} }
],
hashToWhereConditions: [
{
arguments: [{ id: [1, 2, 3] }],
expectation: '`id` IN (1,2,3)'
},
{
arguments: [{ id: [] }],
expectation: '`id` IN (NULL)'
},
{
arguments: [{ maple: false, bacon: true }],
expectation: '`maple`=false AND `bacon`=true'
},
{
arguments: [{ beaver: [false, true] }],
expectation: '`beaver` IN (false,true)'
},
{
arguments: [{birthday: new Date(Date.UTC(2011, 6, 1, 10, 1, 55))}],
expectation: "`birthday`='2011-07-01 10:01:55'"
},
{
arguments: [{ birthday: new Date(Date.UTC(2011, 6, 1, 10, 1, 55)),
otherday: new Date(Date.UTC(2013, 6, 2, 10, 1, 22)) }],
expectation: "`birthday`='2011-07-01 10:01:55' AND `otherday`='2013-07-02 10:01:22'"
},
{
arguments: [{ birthday: [new Date(Date.UTC(2011, 6, 1, 10, 1, 55)), new Date(Date.UTC(2013, 6, 2, 10, 1, 22))] }],
expectation: "`birthday` IN ('2011-07-01 10:01:55','2013-07-02 10:01:22')"
}
] ]
}; };
......
...@@ -315,7 +315,7 @@ if (dialect.match(/^postgres/)) { ...@@ -315,7 +315,7 @@ if (dialect.match(/^postgres/)) {
) )
}; };
}], }],
expectation: 'SELECT * FROM "myTable" WHERE ("myTable"."archived" IS NULL AND COALESCE("place_type_codename", "announcement_type_codename") IN (\'Lost\',\'Found\'));', expectation: 'SELECT * FROM "myTable" WHERE ("myTable"."archived" IS NULL AND COALESCE("place_type_codename", "announcement_type_codename") IN (\'Lost\', \'Found\'));',
context: QueryGenerator, context: QueryGenerator,
needsSequelize: true needsSequelize: true
}, { }, {
...@@ -939,47 +939,6 @@ if (dialect.match(/^postgres/)) { ...@@ -939,47 +939,6 @@ if (dialect.match(/^postgres/)) {
expectation: 'DROP INDEX IF EXISTS mySchema.user_foo_bar', expectation: 'DROP INDEX IF EXISTS mySchema.user_foo_bar',
context: {options: {quoteIdentifiers: false}} context: {options: {quoteIdentifiers: false}}
} }
],
hashToWhereConditions: [
{
arguments: [{ id: [1, 2, 3] }],
expectation: '\"id\" IN (1,2,3)'
},
{
arguments: [{ id: [] }],
expectation: '\"id\" IN (NULL)'
},
{
arguments: [{id: {not: [1, 2, 3] }}],
expectation: '\"id\" NOT IN (1,2,3)'
},
{
arguments: [{id: {not: [] }}],
expectation: '\"id\" NOT IN (NULL)'
},
// Variants when quoteIdentifiers is false
{
arguments: [{ id: [1, 2, 3] }],
expectation: 'id IN (1,2,3)',
context: {options: {quoteIdentifiers: false}}
},
{
arguments: [{ id: [] }],
expectation: 'id IN (NULL)',
context: {options: {quoteIdentifiers: false}}
},
{
arguments: [{ id: {not: [1, 2, 3] }}],
expectation: 'id NOT IN (1,2,3)',
context: {options: {quoteIdentifiers: false}}
},
{
arguments: [{ id: {not: [] }}],
expectation: 'id NOT IN (NULL)',
context: {options: {quoteIdentifiers: false}}
}
] ]
}; };
......
...@@ -368,8 +368,8 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -368,8 +368,8 @@ describe(Support.getTestDialectTeaser('Model'), function() {
return self.Environment.find({ return self.Environment.find({
where: { name: 'environment' }, where: { name: 'environment' },
include: [ include: [
{ daoFactory: self.Domain, as: 'PrivateDomain' }, { model: self.Domain, as: 'PrivateDomain' },
{ daoFactory: self.Domain, as: 'PublicDomain' } { model: self.Domain, as: 'PublicDomain' }
] ]
}).then(function(environment) { }).then(function(environment) {
expect(environment).to.exist; expect(environment).to.exist;
...@@ -559,7 +559,7 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -559,7 +559,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('throws an error if alias is not associated', function() { it('throws an error if alias is not associated', function() {
var self = this; var self = this;
return self.Worker.find({ include: [{ daoFactory: self.Task, as: 'Work' }] }).catch (function(err) { return self.Worker.find({ include: [{ model: self.Task, as: 'Work' }] }).catch (function(err) {
expect(err.message).to.equal('Task (Work) is not associated to Worker!'); expect(err.message).to.equal('Task (Work) is not associated to Worker!');
}); });
}); });
...@@ -567,7 +567,7 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -567,7 +567,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('returns the associated task via worker.task', function() { it('returns the associated task via worker.task', function() {
return this.Worker.find({ return this.Worker.find({
where: { name: 'worker' }, where: { name: 'worker' },
include: [{ daoFactory: this.Task, as: 'ToDo' }] include: [{ model: this.Task, as: 'ToDo' }]
}).then(function(worker) { }).then(function(worker) {
expect(worker).to.exist; expect(worker).to.exist;
expect(worker.ToDo).to.exist; expect(worker.ToDo).to.exist;
...@@ -648,7 +648,7 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -648,7 +648,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
where: { where: {
name: 'Boris' name: 'Boris'
}, },
include: [self.PhoneNumber, { daoFactory: self.Photo, as: 'Photos' }] include: [self.PhoneNumber, { model: self.Photo, as: 'Photos' }]
}).then(function(fetchedContact) { }).then(function(fetchedContact) {
expect(fetchedContact).to.exist; expect(fetchedContact).to.exist;
expect(fetchedContact.Photos.length).to.equal(1); expect(fetchedContact.Photos.length).to.equal(1);
...@@ -720,7 +720,7 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -720,7 +720,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('throws an error if alias is not associated', function() { it('throws an error if alias is not associated', function() {
var self = this; var self = this;
return self.Worker.find({ include: [{ daoFactory: self.Task, as: 'Work' }] }).catch (function(err) { return self.Worker.find({ include: [{ model: self.Task, as: 'Work' }] }).catch (function(err) {
expect(err.message).to.equal('Task (Work) is not associated to Worker!'); expect(err.message).to.equal('Task (Work) is not associated to Worker!');
}); });
}); });
...@@ -728,7 +728,7 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -728,7 +728,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('returns the associated task via worker.task', function() { it('returns the associated task via worker.task', function() {
return this.Worker.find({ return this.Worker.find({
where: { name: 'worker' }, where: { name: 'worker' },
include: [{ daoFactory: this.Task, as: 'ToDos' }] include: [{ model: this.Task, as: 'ToDos' }]
}).then(function(worker) { }).then(function(worker) {
expect(worker).to.exist; expect(worker).to.exist;
expect(worker.ToDos).to.exist; expect(worker.ToDos).to.exist;
......
...@@ -630,7 +630,7 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -630,7 +630,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('throws an error if alias is not associated', function() { it('throws an error if alias is not associated', function() {
var self = this; var self = this;
return self.Worker.findAll({ include: [{ daoFactory: self.Task, as: 'Work' }] }).catch (function(err) { return self.Worker.findAll({ include: [{ model: self.Task, as: 'Work' }] }).catch (function(err) {
expect(err.message).to.equal('Task (Work) is not associated to Worker!'); expect(err.message).to.equal('Task (Work) is not associated to Worker!');
}); });
}); });
...@@ -638,7 +638,7 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -638,7 +638,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('returns the associated task via worker.task', function() { it('returns the associated task via worker.task', function() {
return this.Worker.findAll({ return this.Worker.findAll({
where: { name: 'worker' }, where: { name: 'worker' },
include: [{ daoFactory: this.Task, as: 'ToDo' }] include: [{ model: this.Task, as: 'ToDo' }]
}).then(function(workers) { }).then(function(workers) {
expect(workers).to.exist; expect(workers).to.exist;
expect(workers[0].ToDo).to.exist; expect(workers[0].ToDo).to.exist;
...@@ -722,7 +722,7 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -722,7 +722,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('throws an error if alias is not associated', function() { it('throws an error if alias is not associated', function() {
var self = this; var self = this;
return self.Worker.findAll({ include: [{ daoFactory: self.Task, as: 'Work' }] }).catch (function(err) { return self.Worker.findAll({ include: [{ model: self.Task, as: 'Work' }] }).catch (function(err) {
expect(err.message).to.equal('Task (Work) is not associated to Worker!'); expect(err.message).to.equal('Task (Work) is not associated to Worker!');
}); });
}); });
...@@ -730,7 +730,7 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -730,7 +730,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('returns the associated task via worker.task', function() { it('returns the associated task via worker.task', function() {
return this.Worker.findAll({ return this.Worker.findAll({
where: { name: 'worker' }, where: { name: 'worker' },
include: [{ daoFactory: this.Task, as: 'ToDos' }] include: [{ model: this.Task, as: 'ToDos' }]
}).then(function(workers) { }).then(function(workers) {
expect(workers).to.exist; expect(workers).to.exist;
expect(workers[0].ToDos).to.exist; expect(workers[0].ToDos).to.exist;
...@@ -741,7 +741,7 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -741,7 +741,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('returns the associated task via worker.task when daoFactory is aliased with model', function() { it('returns the associated task via worker.task when daoFactory is aliased with model', function() {
return this.Worker.findAll({ return this.Worker.findAll({
where: { name: 'worker' }, where: { name: 'worker' },
include: [{ daoFactory: this.Task, as: 'ToDos' }] include: [{ model: this.Task, as: 'ToDos' }]
}).then(function(workers) { }).then(function(workers) {
expect(workers[0].ToDos[0].title).to.equal('homework'); expect(workers[0].ToDos[0].title).to.equal('homework');
}); });
......
'use strict';
/* jshint -W030 */
/* jshint -W110 */
var chai = require('chai')
, Sequelize = require('../../../../index')
, expect = chai.expect
, Promise = Sequelize.Promise
, Support = require(__dirname + '/../../support');
describe(Support.getTestDialectTeaser('Model'), function() {
describe('scope', function () {
describe('associations', function () {
beforeEach(function () {
this.ScopeMe = this.sequelize.define('ScopeMe', {
username: Sequelize.STRING,
email: Sequelize.STRING,
access_level: Sequelize.INTEGER,
other_value: Sequelize.INTEGER,
parent_id: Sequelize.INTEGER
}, {
defaultScope: {
where: {
access_level: {
gte: 5
}
}
},
scopes: {
isTony: {
where: {
username: 'tony'
}
},
}
});
this.Project = this.sequelize.define('project');
this.Company = this.sequelize.define('company', {
active: Sequelize.BOOLEAN
}, {
defaultScope: {
where: { active: true }
},
scopes: {
notActive: {
where: {
active: false
}
},
reversed: {
order: [['id', 'DESC']]
}
}
});
this.Profile = this.sequelize.define('profile', {
active: Sequelize.BOOLEAN
}, {
defaultScope: {
where: { active: true }
},
scopes: {
notActive: {
where: {
active: false
}
},
}
});
this.Project.belongsToMany(this.Company, { through: 'CompanyProjects' });
this.Company.belongsToMany(this.Project, { through: 'CompanyProjects' });
this.ScopeMe.hasOne(this.Profile, { foreignKey: 'userId' });
this.ScopeMe.belongsTo(this.Company);
this.UserAssociation = this.Company.hasMany(this.ScopeMe, { as: 'users'});
return this.sequelize.sync({force: true}).bind(this).then(function() {
return Promise.all([
this.ScopeMe.create({ id: 1, username: 'dan', email: 'dan@sequelizejs.com', access_level: 5, other_value: 10, parent_id: 1}),
this.ScopeMe.create({ id: 2, username: 'tobi', email: 'tobi@fakeemail.com', access_level: 10, other_value: 11, parent_id: 2}),
this.ScopeMe.create({ id: 3, username: 'tony', email: 'tony@sequelizejs.com', access_level: 3, other_value: 7, parent_id: 1}),
this.ScopeMe.create({ id: 4, username: 'fred', email: 'fred@foobar.com', access_level: 3, other_value: 7, parent_id: 1}),
this.Company.create({ id: 1, active: true}),
this.Company.create({ id: 2, active: false}),
this.ScopeMe.create({ id: 5, username: 'bob', email: 'bob@foobar.com', access_level: 1, other_value: 9, parent_id: 5}),
]);
}).spread(function (u1, u2, u3, u4, c1, c2, u5) {
return Promise.all([
c1.setUsers([u1, u2, u3, u4]),
c2.setUsers([u5]),
]);
});
});
describe('include', function () {
it('should scope columns properly', function () {
// Will error with ambigous column if id is not scoped properly to `Company`.`id`
return expect(this.Company.findAll({
where: { id: 1 },
include: [this.UserAssociation]
})).not.to.be.rejected;
});
it('should apply default scope when including an associations', function () {
return this.Company.findAll({
include: [this.UserAssociation]
}).get(0).then(function (company) {
expect(company.users).to.have.length(2);
});
});
it('should apply default scope when including a model', function () {
return this.Company.findAll({
include: [{ model: this.ScopeMe, as: 'users'}]
}).get(0).then(function (company) {
expect(company.users).to.have.length(2);
});
});
it('should be able to include a scoped model', function () {
return this.Company.findAll({
include: [{ model: this.ScopeMe.scope('isTony'), as: 'users'}]
}).get(0).then(function (company) {
expect(company.users).to.have.length(1);
expect(company.users[0].get('username')).to.equal('tony');
});
});
});
describe('get', function () {
beforeEach(function () {
return Promise.all([
this.Project.create(),
this.Company.unscoped().findAll()
]).spread(function (p, companies) {
return p.setCompanies(companies);
});
});
describe('it should be able to unscope', function () {
it('hasMany', function () {
return this.Company.find(1).then(function (company) {
return company.getUsers({ scope: false});
}).then(function (users) {
expect(users).to.have.length(4);
});
});
it('hasOne', function () {
return this.Profile.create({
active: false,
userId: 1
}).bind(this).then(function () {
return this.ScopeMe.find(1);
}).then(function (user) {
return user.getProfile({ scope: false });
}).then(function (project) {
expect(project).to.be.ok;
});
});
it('belongsTo', function () {
return this.ScopeMe.unscoped().find({ where: { username: 'bob' }}).then(function (user) {
return user.getCompany({ scope: false });
}).then(function (company) {
expect(company).to.be.ok;
});
});
it('belongsToMany', function () {
return this.Project.findAll().get(0).then(function (p) {
return p.getCompanies({ scope: false});
}).then(function (companies) {
expect(companies).to.have.length(2);
});
});
});
describe('it should apply default scope', function () {
it('hasMany', function () {
return this.Company.find(1).then(function (company) {
return company.getUsers();
}).then(function (users) {
expect(users).to.have.length(2);
});
});
it('hasOne', function () {
return this.Profile.create({
active: false,
userId: 1
}).bind(this).then(function () {
return this.ScopeMe.find(1);
}).then(function (user) {
return user.getProfile();
}).then(function (project) {
expect(project).not.to.be.ok;
});
});
it('belongsTo', function () {
return this.ScopeMe.unscoped().find({ where: { username: 'bob' }}).then(function (user) {
return user.getCompany();
}).then(function (company) {
expect(company).not.to.be.ok;
});
});
it('belongsToMany', function () {
return this.Project.findAll().get(0).then(function (p) {
return p.getCompanies();
}).then(function (companies) {
expect(companies).to.have.length(1);
expect(companies[0].get('active')).to.be.ok;
});
});
});
describe('it should be able to apply another scope', function () {
it('hasMany', function () {
return this.Company.find(1).then(function (company) {
return company.getUsers({ scope: 'isTony'});
}).then(function (users) {
expect(users).to.have.length(1);
expect(users[0].get('username')).to.equal('tony');
});
});
it('hasOne', function () {
return this.Profile.create({
active: true,
userId: 1
}).bind(this).then(function () {
return this.ScopeMe.find(1);
}).then(function (user) {
return user.getProfile({ scope: 'notActive' });
}).then(function (project) {
expect(project).not.to.be.ok;
});
});
it('belongsTo', function () {
return this.ScopeMe.unscoped().find({ where: { username: 'bob' }}).then(function (user) {
return user.getCompany({ scope: 'notActive' });
}).then(function (company) {
expect(company).to.be.ok;
});
});
it('belongsToMany', function () {
return this.Project.findAll().get(0).then(function (p) {
return p.getCompanies({ scope: 'reversed' });
}).then(function (companies) {
expect(companies).to.have.length(2);
expect(companies[0].id).to.equal(2);
expect(companies[1].id).to.equal(1);
});
});
});
});
});
});
});
'use strict';
/* jshint -W030 */
/* jshint -W110 */
var chai = require('chai')
, Sequelize = require('../../../../index')
, expect = chai.expect
, Support = require(__dirname + '/../../support');
describe(Support.getTestDialectTeaser('Model'), function() {
describe('scope', function () {
describe('count', function () {
beforeEach(function () {
this.ScopeMe = this.sequelize.define('ScopeMe', {
username: Sequelize.STRING,
email: Sequelize.STRING,
access_level: Sequelize.INTEGER,
other_value: Sequelize.INTEGER
}, {
defaultScope: {
where: {
access_level: {
gte: 5
}
}
},
scopes: {
lowAccess: {
where: {
access_level: {
lte: 5
}
}
}
}
});
return this.sequelize.sync({force: true}).then(function() {
var records = [
{username: 'tony', email: 'tony@sequelizejs.com', access_level: 3, other_value: 7},
{username: 'tobi', email: 'tobi@fakeemail.com', access_level: 10, other_value: 11},
{username: 'dan', email: 'dan@sequelizejs.com', access_level: 5, other_value: 10},
{username: 'fred', email: 'fred@foobar.com', access_level: 3, other_value: 7}
];
return this.ScopeMe.bulkCreate(records);
}.bind(this));
});
it('should apply defaultScope', function () {
return expect(this.ScopeMe.count()).to.eventually.equal(2);
});
it('should be able to override default scope', function () {
return expect(this.ScopeMe.count({ where: { access_level: { gt: 5 }}})).to.eventually.equal(1);
});
it('should be able to unscope', function () {
return expect(this.ScopeMe.unscoped().count()).to.eventually.equal(4);
});
it('should be able to apply other scopes', function () {
return expect(this.ScopeMe.scope('lowAccess').count()).to.eventually.equal(3);
});
it('should be able to merge scopes with where', function () {
return expect(this.ScopeMe.scope('lowAccess').count({ where: { username: 'dan'}})).to.eventually.equal(1);
});
});
});
});
'use strict';
/* jshint -W030 */
/* jshint -W110 */
var chai = require('chai')
, Sequelize = require('../../../../index')
, expect = chai.expect
, Support = require(__dirname + '/../../support');
describe(Support.getTestDialectTeaser('Model'), function() {
describe('scope', function () {
describe('destroy', function () {
beforeEach(function () {
this.ScopeMe = this.sequelize.define('ScopeMe', {
username: Sequelize.STRING,
email: Sequelize.STRING,
access_level: Sequelize.INTEGER,
other_value: Sequelize.INTEGER
}, {
defaultScope: {
where: {
access_level: {
gte: 5
}
}
},
scopes: {
lowAccess: {
where: {
access_level: {
lte: 5
}
}
}
}
});
return this.sequelize.sync({force: true}).then(function() {
var records = [
{username: 'tony', email: 'tony@sequelizejs.com', access_level: 3, other_value: 7},
{username: 'tobi', email: 'tobi@fakeemail.com', access_level: 10, other_value: 11},
{username: 'dan', email: 'dan@sequelizejs.com', access_level: 5, other_value: 10},
{username: 'fred', email: 'fred@foobar.com', access_level: 3, other_value: 7}
];
return this.ScopeMe.bulkCreate(records);
}.bind(this));
});
it('should apply defaultScope', function () {
return this.ScopeMe.destroy({ where: {}}).bind(this).then(function () {
return this.ScopeMe.unscoped().findAll();
}).then(function (users) {
expect(users).to.have.length(2);
expect(users[0].get('username')).to.equal('tony');
expect(users[1].get('username')).to.equal('fred');
});
});
it('should be able to override default scope', function () {
return this.ScopeMe.destroy({ where: { access_level: { lt: 5 }}}).bind(this).then(function () {
return this.ScopeMe.unscoped().findAll();
}).then(function (users) {
expect(users).to.have.length(2);
expect(users[0].get('username')).to.equal('tobi');
expect(users[1].get('username')).to.equal('dan');
});
});
it('should be able to unscope destroy', function () {
return this.ScopeMe.unscoped().destroy({ where: {}}).bind(this).then(function() {
return expect(this.ScopeMe.unscoped().findAll()).to.eventually.have.length(0);
});
});
it('should be able to apply other scopes', function () {
return this.ScopeMe.scope('lowAccess').destroy({ where: {}}).bind(this).then(function () {
return this.ScopeMe.unscoped().findAll();
}).then(function (users) {
expect(users).to.have.length(1);
expect(users[0].get('username')).to.equal('tobi');
});
});
it('should be able to merge scopes with where', function () {
return this.ScopeMe.scope('lowAccess').destroy({ where: { username: 'dan'}}).bind(this).then(function () {
return this.ScopeMe.unscoped().findAll();
}).then(function (users) {
expect(users).to.have.length(3);
expect(users[0].get('username')).to.equal('tony');
expect(users[1].get('username')).to.equal('tobi');
expect(users[2].get('username')).to.equal('fred');
});
});
});
});
});
'use strict';
/* jshint -W030 */
/* jshint -W110 */
var chai = require('chai')
, Sequelize = require('../../../../index')
, expect = chai.expect
, Support = require(__dirname + '/../../support');
describe(Support.getTestDialectTeaser('Model'), function() {
describe('scopes', function() {
beforeEach(function() {
this.ScopeMe = this.sequelize.define('ScopeMe', {
username: Sequelize.STRING,
email: Sequelize.STRING,
access_level: Sequelize.INTEGER,
other_value: Sequelize.INTEGER,
parent_id: Sequelize.INTEGER
}, {
defaultScope: {
where: {
access_level: {
gte: 5
}
}
},
scopes: {
highValue: {
where: {
other_value: {
gte: 10
}
}
},
andScope: {
where: {
$and: [
{
email: {
like: '%@sequelizejs.com'
}
},
{ access_level : 3 }
]
}
}
}
});
return this.sequelize.sync({force: true}).then(function() {
var records = [
{username: 'tony', email: 'tony@sequelizejs.com', access_level: 3, other_value: 7, parent_id: 1},
{username: 'tobi', email: 'tobi@fakeemail.com', access_level: 10, other_value: 11, parent_id: 2},
{username: 'dan', email: 'dan@sequelizejs.com', access_level: 5, other_value: 10, parent_id: 1},
{username: 'fred', email: 'fred@foobar.com', access_level: 3, other_value: 7, parent_id: 1}
];
return this.ScopeMe.bulkCreate(records);
}.bind(this));
});
it('should be able use where in scope', function() {
return this.ScopeMe.scope({where: { parent_id: 2 }}).findAll().then(function(users) {
expect(users).to.have.length(1);
expect(users[0].username).to.equal('tobi');
});
});
it('should be able to combine scope and findAll where clauses', function() {
return this.ScopeMe.scope({where: { parent_id: 1 }}).findAll({ where: {access_level: 3}}).then(function(users) {
expect(users).to.have.length(2);
expect(['tony', 'fred'].indexOf(users[0].username) !== -1).to.be.true;
expect(['tony', 'fred'].indexOf(users[1].username) !== -1).to.be.true;
});
});
it('should be able to use a defaultScope if declared', function() {
return this.ScopeMe.all().then(function(users) {
expect(users).to.have.length(2);
expect([10, 5].indexOf(users[0].access_level) !== -1).to.be.true;
expect([10, 5].indexOf(users[1].access_level) !== -1).to.be.true;
expect(['dan', 'tobi'].indexOf(users[0].username) !== -1).to.be.true;
expect(['dan', 'tobi'].indexOf(users[1].username) !== -1).to.be.true;
});
});
it('should be able to handle $and in scopes', function () {
return this.ScopeMe.scope('andScope').findAll().then(function(users) {
expect(users).to.have.length(1);
expect(users[0].username).to.equal('tony');
});
});
describe('should not overwrite', function() {
it('default scope with values from previous finds', function() {
return this.ScopeMe.findAll({ where: { other_value: 10 }}).bind(this).then(function(users) {
expect(users).to.have.length(1);
return this.ScopeMe.findAll();
}).then(function(users) {
// This should not have other_value: 10
expect(users).to.have.length(2);
});
});
it('other scopes with values from previous finds', function() {
return this.ScopeMe.scope('highValue').findAll({ where: { access_level: 10 }}).bind(this).then(function(users) {
expect(users).to.have.length(1);
return this.ScopeMe.scope('highValue').findAll();
}).then(function(users) {
// This should not have other_value: 10
expect(users).to.have.length(2);
});
});
});
it('should have no problem performing findOrCreate', function() {
return this.ScopeMe.findOrCreate({ where: {username: 'fake'}}).spread(function(user) {
expect(user.username).to.equal('fake');
});
});
});
});
'use strict';
/* jshint -W030 */
/* jshint -W110 */
var chai = require('chai')
, _ = require('lodash')
, Sequelize = require('../../../../index')
, expect = chai.expect
, Support = require(__dirname + '/../../support');
describe(Support.getTestDialectTeaser('Model'), function() {
describe('scope', function () {
describe('update', function () {
beforeEach(function () {
this.ScopeMe = this.sequelize.define('ScopeMe', {
username: Sequelize.STRING,
email: Sequelize.STRING,
access_level: Sequelize.INTEGER,
other_value: Sequelize.INTEGER
}, {
defaultScope: {
where: {
access_level: {
gte: 5
}
}
},
scopes: {
lowAccess: {
where: {
access_level: {
lte: 5
}
}
}
}
});
return this.sequelize.sync({force: true}).then(function() {
var records = [
{username: 'tony', email: 'tony@sequelizejs.com', access_level: 3, other_value: 7},
{username: 'tobi', email: 'tobi@fakeemail.com', access_level: 10, other_value: 11},
{username: 'dan', email: 'dan@sequelizejs.com', access_level: 5, other_value: 10},
{username: 'fred', email: 'fred@foobar.com', access_level: 3, other_value: 7}
];
return this.ScopeMe.bulkCreate(records);
}.bind(this));
});
it('should apply defaultScope', function () {
return this.ScopeMe.update({ username: 'ruben' }, { where: {}}).bind(this).then(function () {
return this.ScopeMe.unscoped().findAll({ where: { username: 'ruben' }});
}).then(function (users) {
expect(users).to.have.length(2);
expect(users[0].get('email')).to.equal('tobi@fakeemail.com');
expect(users[1].get('email')).to.equal('dan@sequelizejs.com');
});
});
it('should be able to override default scope', function () {
return this.ScopeMe.update({ username: 'ruben' }, { where: { access_level: { lt: 5 }}}).bind(this).then(function () {
return this.ScopeMe.unscoped().findAll({ where: { username: 'ruben' }});
}).then(function (users) {
expect(users).to.have.length(2);
expect(users[0].get('email')).to.equal('tony@sequelizejs.com');
expect(users[1].get('email')).to.equal('fred@foobar.com');
});
});
it('should be able to unscope destroy', function () {
return this.ScopeMe.unscoped().update({ username: 'ruben' }, { where: {}}).bind(this).then(function() {
return this.ScopeMe.unscoped().findAll();
}).then(function (rubens) {
expect(_.every(rubens, function (r) {
return r.get('username') === 'ruben';
})).to.be.true;
});
});
it('should be able to apply other scopes', function () {
return this.ScopeMe.scope('lowAccess').update({ username: 'ruben' }, { where: {}}).bind(this).then(function () {
return this.ScopeMe.unscoped().findAll({ where: { username: { $ne: 'ruben' }}});
}).then(function (users) {
expect(users).to.have.length(1);
expect(users[0].get('email')).to.equal('tobi@fakeemail.com');
});
});
it('should be able to merge scopes with where', function () {
return this.ScopeMe.scope('lowAccess').update({ username: 'ruben' }, { where: { username: 'dan'}}).bind(this).then(function () {
return this.ScopeMe.unscoped().findAll({ where: { username: 'ruben' }});
}).then(function (users) {
expect(users).to.have.length(1);
expect(users[0].get('email')).to.equal('dan@sequelizejs.com');
});
});
});
});
});
'use strict';
/* jshint -W030 */
/* jshint -W110 */
var chai = require('chai')
, Sequelize = require('../../../index')
, expect = chai.expect
, Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + '/../../../lib/data-types');
describe(Support.getTestDialectTeaser('Model'), function() {
beforeEach(function() {
return Support.prepareTransactionTest(this.sequelize).bind(this).then(function(sequelize) {
this.sequelize = sequelize;
this.User = this.sequelize.define('User', {
username: DataTypes.STRING,
secretValue: DataTypes.STRING,
data: DataTypes.STRING,
intVal: DataTypes.INTEGER,
theDate: DataTypes.DATE,
aBool: DataTypes.BOOLEAN
});
return this.User.sync({ force: true });
});
});
describe('scopes', function() {
beforeEach(function() {
this.ScopeMe = this.sequelize.define('ScopeMe', {
username: Sequelize.STRING,
email: Sequelize.STRING,
access_level: Sequelize.INTEGER,
other_value: Sequelize.INTEGER,
parent_id: Sequelize.INTEGER
}, {
defaultScope: {
where: {
access_level: {
gte: 5
}
}
},
scopes: {
orderScope: {
order: 'access_level DESC'
},
limitScope: {
limit: 2
},
sequelizeTeam: {
where: ['email LIKE \'%@sequelizejs.com\'']
},
fakeEmail: {
where: ['email LIKE \'%@fakeemail.com\'']
},
highValue: {
where: {
other_value: {
gte: 10
}
}
},
isTony: {
where: {
username: 'tony'
}
},
canBeTony: {
where: {
username: ['tony']
}
},
canBeDan: {
where: {
username: {
in : 'dan'
}
}
},
actualValue: function(value) {
return {
where: {
other_value: value
}
};
},
complexFunction: function(email, accessLevel) {
return {
where: ['email like ? AND access_level >= ?', email + '%', accessLevel]
};
},
lowAccess: {
where: {
access_level: {
lte: 5
}
}
},
escape: {
where: {
username: "escape'd"
}
}
}
});
return this.sequelize.sync({force: true}).then(function() {
var records = [
{username: 'dan', email: 'dan@sequelizejs.com', access_level: 5, other_value: 10, parent_id: 1},
{username: 'tobi', email: 'tobi@fakeemail.com', access_level: 10, other_value: 11, parent_id: 2},
{username: 'tony', email: 'tony@sequelizejs.com', access_level: 3, other_value: 7, parent_id: 1},
{username: 'fred', email: 'fred@foobar.com', access_level: 3, other_value: 7, parent_id: 1}
];
return this.ScopeMe.bulkCreate(records);
}.bind(this));
});
it('should be able use where in scope', function() {
return this.ScopeMe.scope({where: { parent_id: 2 }}).findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users[0].username).to.equal('tobi');
});
});
it('should be able to combine scope and findAll where clauses', function() {
return this.ScopeMe.scope({where: { parent_id: 1 }}).findAll({ where: {access_level: 3}}).then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(2);
expect(['tony', 'fred'].indexOf(users[0].username) !== -1).to.be.true;
expect(['tony', 'fred'].indexOf(users[1].username) !== -1).to.be.true;
});
});
it('should have no problems with escaping SQL', function() {
var self = this;
return this.ScopeMe.create({username: 'escape\'d', email: 'fake@fakemail.com'}).then(function() {
return self.ScopeMe.scope('escape').all().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users[0].username).to.equal('escape\'d');
});
});
});
it('should be able to use a defaultScope if declared', function() {
return this.ScopeMe.all().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(2);
expect([10, 5].indexOf(users[0].access_level) !== -1).to.be.true;
expect([10, 5].indexOf(users[1].access_level) !== -1).to.be.true;
expect(['dan', 'tobi'].indexOf(users[0].username) !== -1).to.be.true;
expect(['dan', 'tobi'].indexOf(users[1].username) !== -1).to.be.true;
});
});
it('should be able to amend the default scope with a find object', function() {
return this.ScopeMe.findAll({where: {username: 'dan'}}).then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users[0].username).to.equal('dan');
});
});
it('should be able to override the default scope', function() {
return this.ScopeMe.scope('fakeEmail').findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users[0].username).to.equal('tobi');
});
});
it('should be able to combine two scopes', function() {
return this.ScopeMe.scope(['sequelizeTeam', 'highValue']).findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users[0].username).to.equal('dan');
});
});
it("should be able to call a scope that's a function", function() {
return this.ScopeMe.scope({method: ['actualValue', 11]}).findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users[0].username).to.equal('tobi');
});
});
it('should be able to handle multiple function scopes', function() {
return this.ScopeMe.scope([{method: ['actualValue', 10]}, {method: ['complexFunction', 'dan', '5']}]).findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users[0].username).to.equal('dan');
});
});
it('should be able to stack the same field in the where clause', function() {
return this.ScopeMe.scope(['canBeDan', 'canBeTony']).findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(2);
expect(['dan', 'tony'].indexOf(users[0].username) !== -1).to.be.true;
expect(['dan', 'tony'].indexOf(users[1].username) !== -1).to.be.true;
});
});
it('should be able to merge scopes', function() {
return this.ScopeMe.scope(['highValue', 'isTony', {merge: true, method: ['actualValue', 7]}]).findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users[0].username).to.equal('tony');
});
});
describe('should not overwrite', function() {
it('default scope with values from previous finds', function() {
return this.ScopeMe.findAll({ where: { other_value: 10 }}).bind(this).then(function(users) {
expect(users).to.have.length(1);
return this.ScopeMe.findAll();
}).then(function(users) {
// This should not have other_value: 10
expect(users).to.have.length(2);
});
});
it('other scopes with values from previous finds', function() {
return this.ScopeMe.scope('highValue').findAll({ where: { access_level: 10 }}).bind(this).then(function(users) {
expect(users).to.have.length(1);
return this.ScopeMe.scope('highValue').findAll();
}).then(function(users) {
// This should not have other_value: 10
expect(users).to.have.length(2);
});
});
it('function scopes', function() {
return this.ScopeMe.scope({method: ['actualValue', 11]}).findAll().bind(this).then(function(users) {
expect(users).to.have.length(1);
expect(users[0].other_value).to.equal(11);
return this.ScopeMe.scope({method: ['actualValue', 10]}).findAll();
}).then(function(users) {
expect(users).to.have.length(1);
expect(users[0].other_value).to.equal(10);
});
});
});
it('should give us the correct order if we declare an order in our scope', function() {
return this.ScopeMe.scope('sequelizeTeam', 'orderScope').findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(2);
expect(users[0].username).to.equal('dan');
expect(users[1].username).to.equal('tony');
});
});
it('should give us the correct order as well as a limit if we declare such in our scope', function() {
return this.ScopeMe.scope(['orderScope', 'limitScope']).findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(2);
expect(users[0].username).to.equal('tobi');
expect(users[1].username).to.equal('dan');
});
});
it('should have no problems combining scopes and traditional where object', function() {
return this.ScopeMe.scope('sequelizeTeam').findAll({where: {other_value: 10}}).then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users[0].username).to.equal('dan');
expect(users[0].access_level).to.equal(5);
expect(users[0].other_value).to.equal(10);
});
});
it('should be able to remove all scopes', function() {
return this.ScopeMe.scope(null).findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(4);
});
});
it('should have no problem performing findOrCreate', function() {
return this.ScopeMe.findOrCreate({ where: {username: 'fake'}}).spread(function(user) {
expect(user.username).to.equal('fake');
});
});
it('should be able to hold multiple scope objects', function() {
var sequelizeTeam = this.ScopeMe.scope('sequelizeTeam', 'orderScope')
, tobi = this.ScopeMe.scope({method: ['actualValue', 11]});
return sequelizeTeam.all().then(function(team) {
return tobi.all().then(function(t) {
expect(team).to.be.an.instanceof(Array);
expect(team.length).to.equal(2);
expect(team[0].username).to.equal('dan');
expect(team[1].username).to.equal('tony');
expect(t).to.be.an.instanceof(Array);
expect(t.length).to.equal(1);
expect(t[0].username).to.equal('tobi');
});
});
});
it("should gracefully omit any scopes that don't exist", function() {
return this.ScopeMe.scope('sequelizeTeam', 'orderScope', 'doesntexist').all().then(function(team) {
expect(team).to.be.an.instanceof(Array);
expect(team.length).to.equal(2);
expect(team[0].username).to.equal('dan');
expect(team[1].username).to.equal('tony');
});
});
it("should gracefully omit any scopes that don't exist through an array", function() {
return this.ScopeMe.scope(['sequelizeTeam', 'orderScope', 'doesntexist']).all().then(function(team) {
expect(team).to.be.an.instanceof(Array);
expect(team.length).to.equal(2);
expect(team[0].username).to.equal('dan');
expect(team[1].username).to.equal('tony');
});
});
it("should gracefully omit any scopes that don't exist through an object", function() {
return this.ScopeMe.scope('sequelizeTeam', 'orderScope', {method: 'doesntexist'}).all().then(function(team) {
expect(team).to.be.an.instanceof(Array);
expect(team.length).to.equal(2);
expect(team[0].username).to.equal('dan');
expect(team[1].username).to.equal('tony');
});
});
it("should emit an error for scopes that don't exist with silent: false", function() {
expect(this.ScopeMe.scope.bind(this.ScopeMe, 'doesntexist', {silent: false})).to.throw('Invalid scope doesntexist called.');
});
});
});
'use strict';
/* jshint -W030 */
var chai = require('chai')
, expect = chai.expect
, Support = require(__dirname + '/../support')
, current = Support.sequelize;
describe(Support.getTestDialectTeaser('Model'), function() {
var Project = current.define('project')
, User = current.define('user')
, Company;
var scopes = {
somethingTrue: {
where: {
something: true,
somethingElse: 42
},
limit: 5
},
somethingFalse: {
where: {
something: false
}
},
users: {
include: [
{ model: User }
]
},
alsoUsers: {
include: [
{ model: User, where: { something: 42}}
]
},
projects: {
include: [Project]
},
noArgs: function () {
// This does not make much sense, since it does not actually need to be in a function,
// In reality it could be used to do for example new Date or random in the scope - but we want it deterministic
return {
where: {
other_value: 7
}
};
},
actualValue: function(value) {
return {
where: {
other_value: value
}
};
},
};
Company = current.define('company', {}, {
defaultScope: {
where: { active: true }
},
scopes: scopes
});
describe('.scope', function () {
it('should apply default scope', function () {
expect(Company.$scope).to.deep.equal({ where: { active: true }});
});
it('should be able to unscope', function () {
expect(Company.scope(null).$scope).to.be.empty;
expect(Company.unscoped().$scope).to.be.empty;
});
it('should be able to merge scopes', function() {
expect(Company.scope('somethingTrue', 'somethingFalse').$scope).to.deep.equal({
where: {
something: false,
somethingElse: 42
},
limit: 5
});
});
it('should support multiple, coexistent scoped models', function () {
var scoped1 = Company.scope('somethingTrue')
, scoped2 = Company.scope('somethingFalse');
expect(scoped1.$scope).to.deep.equal(scopes.somethingTrue);
expect(scoped2.$scope).to.deep.equal(scopes.somethingFalse);
});
it('should work with function scopes', function () {
expect(Company.scope({method: ['actualValue', 11]}).$scope).to.deep.equal({
where: {
other_value: 11
}
});
expect(Company.scope('noArgs').$scope).to.deep.equal({
where: {
other_value: 7
}
});
});
it('should be able to merge two scoped includes', function () {
expect(Company.scope('users', 'projects').$scope).to.deep.equal({
include: [
{ model: User },
{ model: Project }
]
});
});
it('should be able to override the default scope', function() {
expect(Company.scope('somethingTrue').$scope).to.deep.equal(scopes.somethingTrue);
});
it('should be able to combine default with another scope', function () {
expect(Company.scope(['defaultScope', {method: ['actualValue', 11]}]).$scope).to.deep.equal({
where: {
active: true,
other_value: 11
}
});
});
it('should emit an error for scopes that dont exist', function() {
expect(function () {
Company.scope('doesntexist');
}).to.throw('Invalid scope doesntexist called.');
});
});
describe('$injectScope', function () {
it('should be able to merge scope and where', function () {
var scope = {
where: {
something: true,
somethingElse: 42
},
limit: 15,
offset: 3
};
var options = {
where: {
something: false
},
limit: 9
};
current.Model.$injectScope(scope, options);
expect(options).to.deep.equal({
where: {
something: false,
somethingElse: 42
},
limit: 9,
offset: 3
});
});
it('should be able to overwrite multiple scopes with the same include', function () {
var scope = {
include: [
{ model: Project, where: { something: false }},
{ model: Project, where: { something: true }}
]
};
var options = {};
current.Model.$injectScope(scope, options);
expect(options.include).to.have.length(1);
expect(options.include[0]).to.deep.equal({ model: Project, where: { something: true }});
});
it('should be able to override scoped include', function () {
var scope = {
include: [{ model: Project, where: { something: false }}]
};
var options = {
include: [{ model: Project, where: { something: true }}]
};
current.Model.$injectScope(scope, options);
expect(options.include).to.have.length(1);
expect(options.include[0]).to.deep.equal({ model: Project, where: { something: true }});
});
it('should be able to merge scoped include with include in find', function () {
var scope = {
include: [
{ model: Project, where: { something: false }}
]
};
var options = {
include: [
{ model: User, where: { something: true }}
]
};
current.Model.$injectScope(scope, options);
expect(options.include).to.have.length(2);
expect(options.include[0]).to.deep.equal({ model: User, where: { something: true }});
expect(options.include[1]).to.deep.equal({ model: Project, where: { something: false }});
});
});
});
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!