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

Commit ccb99dae by Yoni Jah Committed by Erik Seliger

feat(query-generator): change operators to be represented by symbols (#8240)

1 parent 9ca86af2
...@@ -102,7 +102,7 @@ Project ...@@ -102,7 +102,7 @@ Project
.findAndCountAll({ .findAndCountAll({
where: { where: {
title: { title: {
$like: 'foo%' [Op.like]: 'foo%'
} }
}, },
offset: 10, offset: 10,
...@@ -168,28 +168,28 @@ Project.findAll({ where: { id: [1,2,3] } }).then(projects => { ...@@ -168,28 +168,28 @@ Project.findAll({ where: { id: [1,2,3] } }).then(projects => {
Project.findAll({ Project.findAll({
where: { where: {
id: { id: {
$and: {a: 5} // AND (a = 5) [Op.and]: {a: 5} // AND (a = 5)
$or: [{a: 5}, {a: 6}] // (a = 5 OR a = 6) [Op.or]: [{a: 5}, {a: 6}] // (a = 5 OR a = 6)
$gt: 6, // id > 6 [Op.gt]: 6, // id > 6
$gte: 6, // id >= 6 [Op.gte]: 6, // id >= 6
$lt: 10, // id < 10 [Op.lt]: 10, // id < 10
$lte: 10, // id <= 10 [Op.lte]: 10, // id <= 10
$ne: 20, // id != 20 [Op.ne]: 20, // id != 20
$between: [6, 10], // BETWEEN 6 AND 10 [Op.between]: [6, 10], // BETWEEN 6 AND 10
$notBetween: [11, 15], // NOT BETWEEN 11 AND 15 [Op.notBetween]: [11, 15], // NOT BETWEEN 11 AND 15
$in: [1, 2], // IN [1, 2] [Op.in]: [1, 2], // IN [1, 2]
$notIn: [1, 2], // NOT IN [1, 2] [Op.notIn]: [1, 2], // NOT IN [1, 2]
$like: '%hat', // LIKE '%hat' [Op.like]: '%hat', // LIKE '%hat'
$notLike: '%hat' // NOT LIKE '%hat' [Op.notLike]: '%hat' // NOT LIKE '%hat'
$iLike: '%hat' // ILIKE '%hat' (case insensitive) (PG only) [Op.iLike]: '%hat' // ILIKE '%hat' (case insensitive) (PG only)
$notILike: '%hat' // NOT ILIKE '%hat' (PG only) [Op.notILike]: '%hat' // NOT ILIKE '%hat' (PG only)
$overlap: [1, 2] // && [1, 2] (PG array overlap operator) [Op.overlap]: [1, 2] // && [1, 2] (PG array overlap operator)
$contains: [1, 2] // @> [1, 2] (PG array contains operator) [Op.contains]: [1, 2] // @> [1, 2] (PG array contains operator)
$contained: [1, 2] // <@ [1, 2] (PG array contained by operator) [Op.contained]: [1, 2] // <@ [1, 2] (PG array contained by operator)
$any: [2,3] // ANY ARRAY[2, 3]::INTEGER (PG only) [Op.any]: [2,3] // ANY ARRAY[2, 3]::INTEGER (PG only)
}, },
status: { status: {
$not: false, // status NOT FALSE [Op.not]: false, // status NOT FALSE
} }
} }
}) })
...@@ -197,15 +197,15 @@ Project.findAll({ ...@@ -197,15 +197,15 @@ Project.findAll({
### Complex filtering / OR / NOT queries ### 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`: 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` `Operators`:
```js ```js
Project.findOne({ Project.findOne({
where: { where: {
name: 'a project', name: 'a project',
$or: [ [Op.or]: [
{ id: [1,2,3] }, { id: [1,2,3] },
{ id: { $gt: 10 } } { id: { [Op.gt]: 10 } }
] ]
} }
}) })
...@@ -214,9 +214,9 @@ Project.findOne({ ...@@ -214,9 +214,9 @@ Project.findOne({
where: { where: {
name: 'a project', name: 'a project',
id: { id: {
$or: [ [Op.or]: [
[1,2,3], [1,2,3],
{ $gt: 10 } { [Op.gt]: 10 }
] ]
} }
} }
...@@ -235,15 +235,15 @@ WHERE ( ...@@ -235,15 +235,15 @@ WHERE (
LIMIT 1; LIMIT 1;
``` ```
`$not` example: `not` example:
```js ```js
Project.findOne({ Project.findOne({
where: { where: {
name: 'a project', name: 'a project',
$not: [ [Op.not]: [
{ id: [1,2,3] }, { id: [1,2,3] },
{ array: { $contains: [3,4,5] } } { array: { [Op.contains]: [3,4,5] } }
] ]
} }
}); });
...@@ -338,7 +338,7 @@ Project.count().then(c => { ...@@ -338,7 +338,7 @@ Project.count().then(c => {
console.log("There are " + c + " projects!") console.log("There are " + c + " projects!")
}) })
Project.count({ where: {'id': {$gt: 25}} }).then(c => { Project.count({ where: {'id': {[Op.gt]: 25}} }).then(c => {
console.log("There are " + c + " projects with an id greater than 25.") console.log("There are " + c + " projects with an id greater than 25.")
}) })
``` ```
...@@ -358,7 +358,7 @@ Project.max('age').then(max => { ...@@ -358,7 +358,7 @@ Project.max('age').then(max => {
// this will return 40 // this will return 40
}) })
Project.max('age', { where: { age: { lt: 20 } } }).then(max => { Project.max('age', { where: { age: { [Op.lt]: 20 } } }).then(max => {
// will be 10 // will be 10
}) })
``` ```
...@@ -378,7 +378,7 @@ Project.min('age').then(min => { ...@@ -378,7 +378,7 @@ Project.min('age').then(min => {
// this will return 5 // this will return 5
}) })
Project.min('age', { where: { age: { $gt: 5 } } }).then(min => { Project.min('age', { where: { age: { [Op.gt]: 5 } } }).then(min => {
// will be 10 // will be 10
}) })
``` ```
...@@ -399,7 +399,7 @@ Project.sum('age').then(sum => { ...@@ -399,7 +399,7 @@ Project.sum('age').then(sum => {
// this will return 55 // this will return 55
}) })
Project.sum('age', { where: { age: { $gt: 5 } } }).then(sum => { Project.sum('age', { where: { age: { [Op.gt]: 5 } } }).then(sum => {
// will be 50 // will be 50
}) })
``` ```
...@@ -550,7 +550,7 @@ User.findAll({ ...@@ -550,7 +550,7 @@ User.findAll({
include: [{ include: [{
model: Tool, model: Tool,
as: 'Instruments', as: 'Instruments',
where: { name: { $like: '%ooth%' } } where: { name: { [Op.like]: '%ooth%' } }
}] }]
}).then(users => { }).then(users => {
console.log(JSON.stringify(users)) console.log(JSON.stringify(users))
...@@ -597,7 +597,7 @@ To move the where conditions from an included model from the `ON` condition to t ...@@ -597,7 +597,7 @@ To move the where conditions from an included model from the `ON` condition to t
```js ```js
User.findAll({ User.findAll({
where: { where: {
'$Instruments.name$': { $iLike: '%ooth%' } '$Instruments.name$': { [Op.iLike]: '%ooth%' }
}, },
include: [{ include: [{
model: Tool, model: Tool,
...@@ -653,7 +653,7 @@ In case you want to eager load soft deleted records you can do that by setting ` ...@@ -653,7 +653,7 @@ In case you want to eager load soft deleted records you can do that by setting `
User.findAll({ User.findAll({
include: [{ include: [{
model: Tool, model: Tool,
where: { name: { $like: '%ooth%' } }, where: { name: { [Op.like]: '%ooth%' } },
paranoid: false // query and loads the soft deleted records paranoid: false // query and loads the soft deleted records
}] }]
}); });
......
...@@ -72,10 +72,12 @@ Whether you are querying with findAll/find or doing bulk updates/destroys you ca ...@@ -72,10 +72,12 @@ Whether you are querying with findAll/find or doing bulk updates/destroys you ca
`where` generally takes an object from attribute:value pairs, where value can be primitives for equality matches or keyed objects for other operators. `where` generally takes an object from attribute:value pairs, where value can be primitives for equality matches or keyed objects for other operators.
It's also possible to generate complex AND/OR conditions by nesting sets of `$or` and `$and`. It's also possible to generate complex AND/OR conditions by nesting sets of `or` and `and` `Operators`.
### Basics ### Basics
```js ```js
const Op = Sequelize.Op;
Post.findAll({ Post.findAll({
where: { where: {
authorId: 2 authorId: 2
...@@ -103,7 +105,7 @@ Post.update({ ...@@ -103,7 +105,7 @@ Post.update({
}, { }, {
where: { where: {
deletedAt: { deletedAt: {
$ne: null [Op.ne]: null
} }
} }
}); });
...@@ -117,39 +119,42 @@ Post.findAll({ ...@@ -117,39 +119,42 @@ Post.findAll({
### Operators ### Operators
Sequelize exposes symbol operators that can be used for to create more complex comparisons -
```js ```js
$and: {a: 5} // AND (a = 5) const Op = Sequelize.Op
$or: [{a: 5}, {a: 6}] // (a = 5 OR a = 6)
$gt: 6, // > 6 [Op.and]: {a: 5} // AND (a = 5)
$gte: 6, // >= 6 [Op.or]: [{a: 5}, {a: 6}] // (a = 5 OR a = 6)
$lt: 10, // < 10 [Op.gt]: 6, // > 6
$lte: 10, // <= 10 [Op.gte]: 6, // >= 6
$ne: 20, // != 20 [Op.lt]: 10, // < 10
$eq: 3, // = 3 [Op.lte]: 10, // <= 10
$not: true, // IS NOT TRUE [Op.ne]: 20, // != 20
$between: [6, 10], // BETWEEN 6 AND 10 [Op.eq]: 3, // = 3
$notBetween: [11, 15], // NOT BETWEEN 11 AND 15 [Op.not]: true, // IS NOT TRUE
$in: [1, 2], // IN [1, 2] [Op.between]: [6, 10], // BETWEEN 6 AND 10
$notIn: [1, 2], // NOT IN [1, 2] [Op.notBetween]: [11, 15], // NOT BETWEEN 11 AND 15
$like: '%hat', // LIKE '%hat' [Op.in]: [1, 2], // IN [1, 2]
$notLike: '%hat' // NOT LIKE '%hat' [Op.notIn]: [1, 2], // NOT IN [1, 2]
$iLike: '%hat' // ILIKE '%hat' (case insensitive) (PG only) [Op.like]: '%hat', // LIKE '%hat'
$notILike: '%hat' // NOT ILIKE '%hat' (PG only) [Op.notLike]: '%hat' // NOT LIKE '%hat'
$regexp: '^[h|a|t]' // REGEXP/~ '^[h|a|t]' (MySQL/PG only) [Op.iLike]: '%hat' // ILIKE '%hat' (case insensitive) (PG only)
$notRegexp: '^[h|a|t]' // NOT REGEXP/!~ '^[h|a|t]' (MySQL/PG only) [Op.notILike]: '%hat' // NOT ILIKE '%hat' (PG only)
$iRegexp: '^[h|a|t]' // ~* '^[h|a|t]' (PG only) [Op.regexp]: '^[h|a|t]' // REGEXP/~ '^[h|a|t]' (MySQL/PG only)
$notIRegexp: '^[h|a|t]' // !~* '^[h|a|t]' (PG only) [Op.notRegexp]: '^[h|a|t]' // NOT REGEXP/!~ '^[h|a|t]' (MySQL/PG only)
$like: { $any: ['cat', 'hat']} [Op.iRegexp]: '^[h|a|t]' // ~* '^[h|a|t]' (PG only)
[Op.notIRegexp]: '^[h|a|t]' // !~* '^[h|a|t]' (PG only)
[Op.like]: { [Op.any]: ['cat', 'hat']}
// LIKE ANY ARRAY['cat', 'hat'] - also works for iLike and notLike // LIKE ANY ARRAY['cat', 'hat'] - also works for iLike and notLike
$overlap: [1, 2] // && [1, 2] (PG array overlap operator) [Op.overlap]: [1, 2] // && [1, 2] (PG array overlap operator)
$contains: [1, 2] // @> [1, 2] (PG array contains operator) [Op.contains]: [1, 2] // @> [1, 2] (PG array contains operator)
$contained: [1, 2] // <@ [1, 2] (PG array contained by operator) [Op.contained]: [1, 2] // <@ [1, 2] (PG array contained by operator)
$any: [2,3] // ANY ARRAY[2, 3]::INTEGER (PG only) [Op.any]: [2,3] // ANY ARRAY[2, 3]::INTEGER (PG only)
$col: 'user.organization_id' // = "user"."organization_id", with dialect specific column identifiers, PG in this example [Op.col]: 'user.organization_id' // = "user"."organization_id", with dialect specific column identifiers, PG in this example
``` ```
### Range Operators #### Range Operators
Range types can be queried with all supported operators. Range types can be queried with all supported operators.
...@@ -160,24 +165,26 @@ as well. ...@@ -160,24 +165,26 @@ as well.
```js ```js
// All the above equality and inequality operators plus the following: // All the above equality and inequality operators plus the following:
$contains: 2 // @> '2'::integer (PG range contains element operator) [Op.contains]: 2 // @> '2'::integer (PG range contains element operator)
$contains: [1, 2] // @> [1, 2) (PG range contains range operator) [Op.contains]: [1, 2] // @> [1, 2) (PG range contains range operator)
$contained: [1, 2] // <@ [1, 2) (PG range is contained by operator) [Op.contained]: [1, 2] // <@ [1, 2) (PG range is contained by operator)
$overlap: [1, 2] // && [1, 2) (PG range overlap (have points in common) operator) [Op.overlap]: [1, 2] // && [1, 2) (PG range overlap (have points in common) operator)
$adjacent: [1, 2] // -|- [1, 2) (PG range is adjacent to operator) [Op.adjacent]: [1, 2] // -|- [1, 2) (PG range is adjacent to operator)
$strictLeft: [1, 2] // << [1, 2) (PG range strictly left of operator) [Op.strictLeft]: [1, 2] // << [1, 2) (PG range strictly left of operator)
$strictRight: [1, 2] // >> [1, 2) (PG range strictly right of operator) [Op.strictRight]: [1, 2] // >> [1, 2) (PG range strictly right of operator)
$noExtendRight: [1, 2] // &< [1, 2) (PG range does not extend to the right of operator) [Op.noExtendRight]: [1, 2] // &< [1, 2) (PG range does not extend to the right of operator)
$noExtendLeft: [1, 2] // &> [1, 2) (PG range does not extend to the left of operator) [Op.noExtendLeft]: [1, 2] // &> [1, 2) (PG range does not extend to the left of operator)
``` ```
### Combinations #### Combinations
```js ```js
const Op = Sequelize.Op;
{ {
rank: { rank: {
$or: { [Op.or]: {
$lt: 1000, [Op.lt]: 1000,
$eq: null [Op.eq]: null
} }
} }
} }
...@@ -185,22 +192,22 @@ $noExtendLeft: [1, 2] // &> [1, 2) (PG range does not extend to the left of ope ...@@ -185,22 +192,22 @@ $noExtendLeft: [1, 2] // &> [1, 2) (PG range does not extend to the left of ope
{ {
createdAt: { createdAt: {
$lt: new Date(), [Op.lt]: new Date(),
$gt: new Date(new Date() - 24 * 60 * 60 * 1000) [Op.gt]: new Date(new Date() - 24 * 60 * 60 * 1000)
} }
} }
// createdAt < [timestamp] AND createdAt > [timestamp] // createdAt < [timestamp] AND createdAt > [timestamp]
{ {
$or: [ [Op.or]: [
{ {
title: { title: {
$like: 'Boat%' [Op.like]: 'Boat%'
} }
}, },
{ {
description: { description: {
$like: '%boat%' [Op.like]: '%boat%'
} }
} }
] ]
...@@ -208,6 +215,89 @@ $noExtendLeft: [1, 2] // &> [1, 2) (PG range does not extend to the left of ope ...@@ -208,6 +215,89 @@ $noExtendLeft: [1, 2] // &> [1, 2) (PG range does not extend to the left of ope
// title LIKE 'Boat%' OR description LIKE '%boat%' // title LIKE 'Boat%' OR description LIKE '%boat%'
``` ```
#### Operators Aliases
Sequelize allows setting specific strings as aliases for operators -
```js
const Op = Sequelize.Op;
const operatorsAliases = {
$gt: Op.gt
}
const connection = new Sequelize(db, user, pass, { operatorsAliases })
[Op.gt]: 6 // > 6
$gt: 6 // same as using Op.gt (> 6)
```
#### Operators security
Using Sequelize without any aliases improves security.
Some frameworks automatically parse user input into js objects and if you fail to sanitize your input it might be possible to inject an Object with string operators to Sequelize.
Not having any string aliases will make it extremely unlikely that operators could be injected but you should always properly validate and sanitize user input.
For backward compatibility reasons Sequelize sets the following aliases by default -
$eq, $ne, $gte, $gt, $lte, $lt, $not, $in, $notIn, $is, $like, $notLike, $iLike, $notILike, $regexp, $notRegexp, $iRegexp, $notIRegexp, $between, $notBetween, $overlap, $contains, $contained, $adjacent, $strictLeft, $strictRight, $noExtendRight, $noExtendLeft, $and, $or, $any, $all, $values, $col
Currently the following legacy aliases are also set but are planned to be fully removed in the near future -
ne, not, in, notIn, gte, gt, lte, lt, like, ilike, $ilike, nlike, $notlike, notilike, .., between, !.., notbetween, nbetween, overlap, &&, @>, <@
For better security it is highly advised to use `Sequelize.Op` and not depend on any string alias at all. You can limit alias your application will need by setting `operatorsAliases` option, remember to sanitize user input especially when you are directly passing them to Sequelize methods.
```js
const Op = Sequelize.Op;
//use sequelize without any operators aliases
const connection = new Sequelize(db, user, pass, { operatorsAliases: false });
//use sequelize with only alias for $and => Op.and
const connection2 = new Sequelize(db, user, pass, { operatorsAliases: { $and: Op.and } });
```
Sequelize will warn you if your using the default aliases and not limiting them
if you want to keep using all default aliases (excluding legacy ones) without the warning you can pass the following operatorsAliases option -
```js
const Op = Sequelize.Op;
const operatorsAliases = {
$eq: Op.eq,
$ne: Op.ne,
$gte: Op.gte,
$gt: Op.gt,
$lte: Op.lte,
$lt: Op.lt,
$not: Op.not,
$in: Op.in,
$notIn: Op.notIn,
$is: Op.is,
$like: Op.like,
$notLike: Op.notLike,
$iLike: Op.iLike,
$notILike: Op.notILike,
$regexp: Op.regexp,
$notRegexp: Op.notRegexp,
$iRegexp: Op.iRegexp,
$notIRegexp: Op.notIRegexp,
$between: Op.between,
$notBetween: Op.notBetween,
$overlap: Op.overlap,
$contains: Op.contains,
$contained: Op.contained,
$adjacent: Op.adjacent,
$strictLeft: Op.strictLeft,
$strictRight: Op.strictRight,
$noExtendRight: Op.noExtendRight,
$noExtendLeft: Op.noExtendLeft,
$and: Op.and,
$or: Op.or,
$any: Op.any,
$all: Op.all,
$values: Op.values,
$col: Op.col
};
const connection = new Sequelize(db, user, pass, { operatorsAliases });
```
### JSONB ### JSONB
JSONB can be queried in three different ways. JSONB can be queried in three different ways.
...@@ -218,7 +308,7 @@ JSONB can be queried in three different ways. ...@@ -218,7 +308,7 @@ JSONB can be queried in three different ways.
meta: { meta: {
video: { video: {
url: { url: {
$ne: null [Op.ne]: null
} }
} }
} }
...@@ -229,7 +319,7 @@ JSONB can be queried in three different ways. ...@@ -229,7 +319,7 @@ JSONB can be queried in three different ways.
```js ```js
{ {
"meta.audio.length": { "meta.audio.length": {
$gt: 20 [Op.gt]: 20
} }
} }
``` ```
...@@ -238,7 +328,7 @@ JSONB can be queried in three different ways. ...@@ -238,7 +328,7 @@ JSONB can be queried in three different ways.
```js ```js
{ {
"meta": { "meta": {
$contains: { [Op.contains]: {
site: { site: {
url: 'http://google.com' url: 'http://google.com'
} }
......
...@@ -36,7 +36,7 @@ const Project = sequelize.define('project', { ...@@ -36,7 +36,7 @@ const Project = sequelize.define('project', {
return { return {
where: { where: {
accessLevel: { accessLevel: {
$gte: value [Op.gte]: value
} }
} }
} }
...@@ -128,7 +128,7 @@ When invoking several scopes, keys from subsequent scopes will overwrite previou ...@@ -128,7 +128,7 @@ When invoking several scopes, keys from subsequent scopes will overwrite previou
where: { where: {
firstName: 'bob', firstName: 'bob',
age: { age: {
$gt: 20 [Op.gt]: 20
} }
}, },
limit: 2 limit: 2
...@@ -136,7 +136,7 @@ When invoking several scopes, keys from subsequent scopes will overwrite previou ...@@ -136,7 +136,7 @@ When invoking several scopes, keys from subsequent scopes will overwrite previou
scope2: { scope2: {
where: { where: {
age: { age: {
$gt: 30 [Op.gt]: 30
} }
}, },
limit: 10 limit: 10
......
...@@ -8,6 +8,7 @@ const BelongsTo = require('./belongs-to'); ...@@ -8,6 +8,7 @@ const BelongsTo = require('./belongs-to');
const HasMany = require('./has-many'); const HasMany = require('./has-many');
const HasOne = require('./has-one'); const HasOne = require('./has-one');
const AssociationError = require('../errors').AssociationError; const AssociationError = require('../errors').AssociationError;
const Op = require('../operators');
/** /**
* Many-to-many association with a join table. * Many-to-many association with a join table.
...@@ -383,7 +384,7 @@ class BelongsToMany extends Association { ...@@ -383,7 +384,7 @@ class BelongsToMany extends Association {
} }
options.where = { options.where = {
$and: [ [Op.and]: [
scopeWhere, scopeWhere,
options.where options.where
] ]
...@@ -400,7 +401,7 @@ class BelongsToMany extends Association { ...@@ -400,7 +401,7 @@ class BelongsToMany extends Association {
//If a user pass a where on the options through options, make an "and" with the current throughWhere //If a user pass a where on the options through options, make an "and" with the current throughWhere
if (options.through && options.through.where) { if (options.through && options.through.where) {
throughWhere = { throughWhere = {
$and: [throughWhere, options.through.where] [Op.and]: [throughWhere, options.through.where]
}; };
} }
...@@ -474,7 +475,7 @@ class BelongsToMany extends Association { ...@@ -474,7 +475,7 @@ class BelongsToMany extends Association {
scope: false scope: false
}); });
where.$or = instances.map(instance => { where[Op.or] = instances.map(instance => {
if (instance instanceof association.target) { if (instance instanceof association.target) {
return instance.where(); return instance.where();
} else { } else {
...@@ -485,7 +486,7 @@ class BelongsToMany extends Association { ...@@ -485,7 +486,7 @@ class BelongsToMany extends Association {
}); });
options.where = { options.where = {
$and: [ [Op.and]: [
where, where,
options.where options.where
] ]
......
...@@ -5,6 +5,8 @@ const Helpers = require('./helpers'); ...@@ -5,6 +5,8 @@ const Helpers = require('./helpers');
const _ = require('lodash'); const _ = require('lodash');
const Transaction = require('../transaction'); const Transaction = require('../transaction');
const Association = require('./base'); const Association = require('./base');
const Op = require('../operators');
/** /**
* One-to-one association * One-to-one association
...@@ -141,7 +143,7 @@ class BelongsTo extends Association { ...@@ -141,7 +143,7 @@ class BelongsTo extends Association {
if (instances) { if (instances) {
where[association.targetKey] = { where[association.targetKey] = {
$in: instances.map(instance => instance.get(association.foreignKey)) [Op.in]: instances.map(instance => instance.get(association.foreignKey))
}; };
} else { } else {
if (association.targetKeyIsPrimary && !options.where) { if (association.targetKeyIsPrimary && !options.where) {
...@@ -153,7 +155,7 @@ class BelongsTo extends Association { ...@@ -153,7 +155,7 @@ class BelongsTo extends Association {
} }
options.where = options.where ? options.where = options.where ?
{$and: [where, options.where]} : {[Op.and]: [where, options.where]} :
where; where;
if (instances) { if (instances) {
......
...@@ -4,6 +4,7 @@ const Utils = require('./../utils'); ...@@ -4,6 +4,7 @@ const Utils = require('./../utils');
const Helpers = require('./helpers'); const Helpers = require('./helpers');
const _ = require('lodash'); const _ = require('lodash');
const Association = require('./base'); const Association = require('./base');
const Op = require('../operators');
/** /**
* One-to-many association * One-to-many association
...@@ -192,7 +193,7 @@ class HasMany extends Association { ...@@ -192,7 +193,7 @@ class HasMany extends Association {
delete options.limit; delete options.limit;
} else { } else {
where[association.foreignKey] = { where[association.foreignKey] = {
$in: values [Op.in]: values
}; };
delete options.groupedLimit; delete options.groupedLimit;
} }
...@@ -202,7 +203,7 @@ class HasMany extends Association { ...@@ -202,7 +203,7 @@ class HasMany extends Association {
options.where = options.where ? options.where = options.where ?
{$and: [where, options.where]} : {[Op.and]: [where, options.where]} :
where; where;
if (options.hasOwnProperty('scope')) { if (options.hasOwnProperty('scope')) {
...@@ -277,7 +278,7 @@ class HasMany extends Association { ...@@ -277,7 +278,7 @@ class HasMany extends Association {
raw: true raw: true
}); });
where.$or = targetInstances.map(instance => { where[Op.or] = targetInstances.map(instance => {
if (instance instanceof association.target) { if (instance instanceof association.target) {
return instance.where(); return instance.where();
} else { } else {
...@@ -288,7 +289,7 @@ class HasMany extends Association { ...@@ -288,7 +289,7 @@ class HasMany extends Association {
}); });
options.where = { options.where = {
$and: [ [Op.and]: [
where, where,
options.where options.where
] ]
......
...@@ -4,6 +4,7 @@ const Utils = require('./../utils'); ...@@ -4,6 +4,7 @@ const Utils = require('./../utils');
const Helpers = require('./helpers'); const Helpers = require('./helpers');
const _ = require('lodash'); const _ = require('lodash');
const Association = require('./base'); const Association = require('./base');
const Op = require('../operators');
/** /**
* One-to-one association * One-to-one association
...@@ -140,7 +141,7 @@ class HasOne extends Association { ...@@ -140,7 +141,7 @@ class HasOne extends Association {
if (instances) { if (instances) {
where[association.foreignKey] = { where[association.foreignKey] = {
$in: instances.map(instance => instance.get(association.sourceKey)) [Op.in]: instances.map(instance => instance.get(association.sourceKey))
}; };
} else { } else {
where[association.foreignKey] = instance.get(association.sourceKey); where[association.foreignKey] = instance.get(association.sourceKey);
...@@ -151,7 +152,7 @@ class HasOne extends Association { ...@@ -151,7 +152,7 @@ class HasOne extends Association {
} }
options.where = options.where ? options.where = options.where ?
{$and: [where, options.where]} : {[Op.and]: [where, options.where]} :
where; where;
if (instances) { if (instances) {
......
...@@ -7,6 +7,8 @@ const AbstractQueryGenerator = require('../abstract/query-generator'); ...@@ -7,6 +7,8 @@ const AbstractQueryGenerator = require('../abstract/query-generator');
const randomBytes = require('crypto').randomBytes; const randomBytes = require('crypto').randomBytes;
const semver = require('semver'); const semver = require('semver');
const Op = require('../../operators');
/* istanbul ignore next */ /* istanbul ignore next */
const throwMethodUndefined = function(methodName) { const throwMethodUndefined = function(methodName) {
throw new Error('The method "' + methodName + '" is not defined! Please add it to your sql dialect.'); throw new Error('The method "' + methodName + '" is not defined! Please add it to your sql dialect.');
...@@ -377,7 +379,7 @@ const QueryGenerator = { ...@@ -377,7 +379,7 @@ const QueryGenerator = {
}); });
//Filter NULL Clauses //Filter NULL Clauses
const clauses = where.$or.filter(clause => { const clauses = where[Op.or].filter(clause => {
let valid = true; let valid = true;
/* /*
* Exclude NULL Composite PK/UK. Partial Composite clauses should also be excluded as it doesn't guarantee a single row * Exclude NULL Composite PK/UK. Partial Composite clauses should also be excluded as it doesn't guarantee a single row
......
...@@ -3,14 +3,15 @@ ...@@ -3,14 +3,15 @@
const _ = require('lodash'); const _ = require('lodash');
const Utils = require('../../utils'); const Utils = require('../../utils');
const AbstractQueryGenerator = require('../abstract/query-generator'); const AbstractQueryGenerator = require('../abstract/query-generator');
const Op = require('../../operators');
const QueryGenerator = { const QueryGenerator = {
__proto__: AbstractQueryGenerator, __proto__: AbstractQueryGenerator,
dialect: 'mysql', dialect: 'mysql',
OperatorMap: Object.assign({}, AbstractQueryGenerator.OperatorMap, { OperatorMap: Object.assign({}, AbstractQueryGenerator.OperatorMap, {
$regexp: 'REGEXP', [Op.regexp]: 'REGEXP',
$notRegexp: 'NOT REGEXP' [Op.notRegexp]: 'NOT REGEXP'
}), }),
createSchema() { createSchema() {
......
...@@ -170,16 +170,16 @@ const QueryGenerator = { ...@@ -170,16 +170,16 @@ const QueryGenerator = {
const pathKey = this.jsonPathExtractionQuery(baseKey, path); const pathKey = this.jsonPathExtractionQuery(baseKey, path);
if (_.isPlainObject(item)) { if (_.isPlainObject(item)) {
_.forOwn(item, (value, itemProp) => { Utils.getOperators(item).forEach(op => {
if (itemProp.indexOf('$') === 0) { let value = item[op];
if (value instanceof Date) { if (value instanceof Date) {
value = value.toISOString(); value = value.toISOString();
} else if (Array.isArray(value) && value[0] instanceof Date) { } else if (Array.isArray(value) && value[0] instanceof Date) {
value = value.map(val => val.toISOString()); value = value.map(val => val.toISOString());
}
items.push(this.whereItemQuery(this._castKey(pathKey, value, cast), {[itemProp]: value}));
return;
} }
items.push(this.whereItemQuery(this._castKey(pathKey, value, cast), {[op]: value}));
});
_.forOwn(item, (value, itemProp) => {
this._traverseJSON(items, baseKey, itemProp, value, path.concat([itemProp])); this._traverseJSON(items, baseKey, itemProp, value, path.concat([itemProp]));
}); });
......
...@@ -17,6 +17,8 @@ const Hooks = require('./hooks'); ...@@ -17,6 +17,8 @@ const Hooks = require('./hooks');
const associationsMixin = require('./associations/mixin'); const associationsMixin = require('./associations/mixin');
const defaultsOptions = { raw: true }; const defaultsOptions = { raw: true };
const assert = require('assert'); const assert = require('assert');
const Op = require('./operators');
/** /**
* A Model represents a table in the database. Instances of this class represent a database row. * A Model represents a table in the database. Instances of this class represent a database row.
...@@ -77,14 +79,14 @@ class Model { ...@@ -77,14 +79,14 @@ class Model {
const deletedAtObject = {}; const deletedAtObject = {};
let deletedAtDefaultValue = deletedAtAttribute.hasOwnProperty('defaultValue') ? deletedAtAttribute.defaultValue : null; let deletedAtDefaultValue = deletedAtAttribute.hasOwnProperty('defaultValue') ? deletedAtAttribute.defaultValue : null;
deletedAtDefaultValue = deletedAtDefaultValue || { $or: { $gt: model.sequelize.literal('CURRENT_TIMESTAMP'), $eq: null } }; deletedAtDefaultValue = deletedAtDefaultValue || { [Op.or]: { [Op.gt]: model.sequelize.literal('CURRENT_TIMESTAMP'), [Op.eq]: null } };
deletedAtObject[deletedAtAttribute.field || deletedAtCol] = deletedAtDefaultValue; deletedAtObject[deletedAtAttribute.field || deletedAtCol] = deletedAtDefaultValue;
if (_.isEmpty(options.where)) { if (_.isEmpty(options.where)) {
options.where = deletedAtObject; options.where = deletedAtObject;
} else { } else {
options.where = { $and: [deletedAtObject, options.where] }; options.where = { [Op.and]: [deletedAtObject, options.where] };
} }
return options; return options;
...@@ -508,7 +510,7 @@ class Model { ...@@ -508,7 +510,7 @@ class Model {
if (through.scope) { if (through.scope) {
include.through.where = include.through.where ? { $and: [include.through.where, through.scope]} : through.scope; include.through.where = include.through.where ? { [Op.and]: [include.through.where, through.scope]} : through.scope;
} }
include.include.push(include.through); include.include.push(include.through);
...@@ -539,7 +541,7 @@ class Model { ...@@ -539,7 +541,7 @@ class Model {
} }
if (include.association.scope) { if (include.association.scope) {
include.where = include.where ? { $and: [include.where, include.association.scope] }: include.association.scope; include.where = include.where ? { [Op.and]: [include.where, include.association.scope] }: include.association.scope;
} }
if (include.limit && include.separate === undefined) { if (include.limit && include.separate === undefined) {
...@@ -1287,10 +1289,10 @@ class Model { ...@@ -1287,10 +1289,10 @@ class Model {
* return { * return {
* where: { * where: {
* email: { * email: {
* $like: email * [Op.like]: email
* }, * },
* accesss_level { * accesss_level {
* $gte: accessLevel * [Op.gte]: accessLevel
* } * }
* } * }
* } * }
...@@ -1301,7 +1303,7 @@ class Model { ...@@ -1301,7 +1303,7 @@ class Model {
* Now, since you defined a default scope, every time you do Model.find, the default scope is appended to your query. Here's a couple of examples: * Now, since you defined a default scope, every time you do Model.find, the default scope is appended to your query. Here's a couple of examples:
* ```js * ```js
* Model.findAll() // WHERE username = 'dan' * Model.findAll() // WHERE username = 'dan'
* Model.findAll({ where: { age: { gt: 12 } } }) // WHERE age > 12 AND username = 'dan' * Model.findAll({ where: { age: { [Op.gt]: 12 } } }) // WHERE age > 12 AND username = 'dan'
* ``` * ```
* *
* To invoke scope functions you can do: * To invoke scope functions you can do:
...@@ -1398,20 +1400,20 @@ class Model { ...@@ -1398,20 +1400,20 @@ class Model {
* *
* __Using greater than, less than etc.__ * __Using greater than, less than etc.__
* ```js * ```js
* * const {gt, lte, ne, in: opIn} = Sequelize.Op;
* Model.findAll({ * Model.findAll({
* where: { * where: {
* attr1: { * attr1: {
* gt: 50 * [gt]: 50
* }, * },
* attr2: { * attr2: {
* lte: 45 * [lte]: 45
* }, * },
* attr3: { * attr3: {
* in: [1,2,3] * [opIn]: [1,2,3]
* }, * },
* attr4: { * attr4: {
* ne: 5 * [ne]: 5
* } * }
* } * }
* }) * })
...@@ -1419,19 +1421,20 @@ class Model { ...@@ -1419,19 +1421,20 @@ class Model {
* ```sql * ```sql
* WHERE attr1 > 50 AND attr2 <= 45 AND attr3 IN (1,2,3) AND attr4 != 5 * WHERE attr1 > 50 AND attr2 <= 45 AND attr3 IN (1,2,3) AND attr4 != 5
* ``` * ```
* Possible options are: `$ne, $in, $not, $notIn, $gte, $gt, $lte, $lt, $like, $ilike/$iLike, $notLike, $notILike, $regexp, $notRegexp, '..'/$between, '!..'/$notBetween, '&&'/$overlap, '@>'/$contains, '<@'/$contained` * See {@link Operators} for possible operators
* *
* __Queries using OR__ * __Queries using OR__
* ```js * ```js
* const {or, and, gt, lt} = Sequelize.Op;
* Model.findAll({ * Model.findAll({
* where: { * where: {
* name: 'a project', * name: 'a project',
* $or: [ * [or]: [
* {id: [1, 2, 3]}, * {id: [1, 2, 3]},
* { * {
* $and: [ * [and]: [
* {id: {gt: 10}}, * {id: {[gt]: 10}},
* {id: {lt: 100}} * {id: {[lt]: 100}}
* ] * ]
* } * }
* ] * ]
......
'use strict';
/**
* Operator symbols to be used when querying data
*
* @see {@link Model#where}
*/
const Op = {
eq: Symbol('eq'),
ne: Symbol('ne'),
gte: Symbol('gte'),
gt: Symbol('gt'),
lte: Symbol('lte'),
lt: Symbol('lt'),
not: Symbol('not'),
is: Symbol('is'),
in: Symbol('in'),
notIn: Symbol('notIn'),
like: Symbol('like'),
notLike: Symbol('notLike'),
iLike: Symbol('iLike'),
notILike: Symbol('notILike'),
regexp: Symbol('regexp'),
notRegexp: Symbol('notRegexp'),
iRegexp: Symbol('iRegexp'),
notIRegexp: Symbol('notIRegexp'),
between: Symbol('between'),
notBetween: Symbol('notBetween'),
overlap: Symbol('overlap'),
contains: Symbol('contains'),
contained: Symbol('contained'),
adjacent: Symbol('adjacent'),
strictLeft: Symbol('strictLeft'),
strictRight: Symbol('strictRight'),
noExtendRight: Symbol('noExtendRight'),
noExtendLeft: Symbol('noExtendLeft'),
and: Symbol('and'),
or: Symbol('or'),
any: Symbol('any'),
all: Symbol('all'),
values: Symbol('values'),
col: Symbol('col'),
placeholder: Symbol('placeholder'),
join: Symbol('join'),
raw: Symbol('raw') //deprecated remove by v5.0
};
const Aliases = {
$eq: Op.eq,
$ne: Op.ne,
$gte: Op.gte,
$gt: Op.gt,
$lte: Op.lte,
$lt: Op.lt,
$not: Op.not,
$in: Op.in,
$notIn: Op.notIn,
$is: Op.is,
$like: Op.like,
$notLike: Op.notLike,
$iLike: Op.iLike,
$notILike: Op.notILike,
$regexp: Op.regexp,
$notRegexp: Op.notRegexp,
$iRegexp: Op.iRegexp,
$notIRegexp: Op.notIRegexp,
$between: Op.between,
$notBetween: Op.notBetween,
$overlap: Op.overlap,
$contains: Op.contains,
$contained: Op.contained,
$adjacent: Op.adjacent,
$strictLeft: Op.strictLeft,
$strictRight: Op.strictRight,
$noExtendRight: Op.noExtendRight,
$noExtendLeft: Op.noExtendLeft,
$and: Op.and,
$or: Op.or,
$any: Op.any,
$all: Op.all,
$values: Op.values,
$col: Op.col,
$raw: Op.raw //deprecated remove by v5.0
};
const LegacyAliases = { //deprecated remove by v5.0
'ne': Op.ne,
'not': Op.not,
'in': Op.in,
'notIn': Op.notIn,
'gte': Op.gte,
'gt': Op.gt,
'lte': Op.lte,
'lt': Op.lt,
'like': Op.like,
'ilike': Op.iLike,
'$ilike': Op.iLike,
'nlike': Op.notLike,
'$notlike': Op.notLike,
'notilike': Op.notILike,
'..': Op.between,
'between': Op.between,
'!..': Op.notBetween,
'notbetween': Op.notBetween,
'nbetween': Op.notBetween,
'overlap': Op.overlap,
'&&': Op.overlap,
'@>': Op.contains,
'<@': Op.contained
};
Op.Aliases = Aliases;
Op.LegacyAliases = Object.assign({}, LegacyAliases, Aliases);
module.exports = Op;
\ No newline at end of file
...@@ -9,6 +9,7 @@ const MySQLQueryInterface = require('./dialects/mysql/query-interface'); ...@@ -9,6 +9,7 @@ const MySQLQueryInterface = require('./dialects/mysql/query-interface');
const Transaction = require('./transaction'); const Transaction = require('./transaction');
const Promise = require('./promise'); const Promise = require('./promise');
const QueryTypes = require('./query-types'); const QueryTypes = require('./query-types');
const Op = require('./operators');
/** /**
* The interface that Sequelize uses to talk to all databases * The interface that Sequelize uses to talk to all databases
...@@ -880,7 +881,7 @@ class QueryInterface { ...@@ -880,7 +881,7 @@ class QueryInterface {
} }
} }
where = { $or: wheres }; where = { [Op.or]: wheres };
options.type = QueryTypes.UPSERT; options.type = QueryTypes.UPSERT;
options.raw = true; options.raw = true;
......
...@@ -18,6 +18,7 @@ const Hooks = require('./hooks'); ...@@ -18,6 +18,7 @@ const Hooks = require('./hooks');
const Association = require('./associations/index'); const Association = require('./associations/index');
const Validator = require('./utils/validator-extras').validator; const Validator = require('./utils/validator-extras').validator;
const _ = require('lodash'); const _ = require('lodash');
const Op = require('./operators');
/** /**
* This is the main class, the entry point to sequelize. To use it, you just need to import sequelize: * This is the main class, the entry point to sequelize. To use it, you just need to import sequelize:
...@@ -94,6 +95,8 @@ class Sequelize { ...@@ -94,6 +95,8 @@ class Sequelize {
* @param {Array} [options.retry.match] Only retry a query if the error matches one of these strings. * @param {Array} [options.retry.match] Only retry a query if the error matches one of these strings.
* @param {Integer} [options.retry.max] How many times a failing query is automatically retried. Set to 0 to disable retrying on SQL_BUSY error. * @param {Integer} [options.retry.max] How many times a failing query is automatically retried. Set to 0 to disable retrying on SQL_BUSY error.
* @param {Boolean} [options.typeValidation=false] Run built in type validators on insert and update, e.g. validate that arguments passed to integer fields are integer-like. * @param {Boolean} [options.typeValidation=false] Run built in type validators on insert and update, e.g. validate that arguments passed to integer fields are integer-like.
* @param {Object|Boolean} [options.operatorsAliases=true] String based operator alias, default value is true which will enable all operators alias. Pass object to limit set of aliased operators or false to disable completely.
*
*/ */
constructor(database, username, password, options) { constructor(database, username, password, options) {
let config; let config;
...@@ -164,7 +167,8 @@ class Sequelize { ...@@ -164,7 +167,8 @@ class Sequelize {
isolationLevel: null, isolationLevel: null,
databaseVersion: 0, databaseVersion: 0,
typeValidation: false, typeValidation: false,
benchmark: false benchmark: false,
operatorsAliases: true
}, options || {}); }, options || {});
if (!this.options.dialect) { if (!this.options.dialect) {
...@@ -228,6 +232,12 @@ class Sequelize { ...@@ -228,6 +232,12 @@ class Sequelize {
this.dialect = new Dialect(this); this.dialect = new Dialect(this);
this.dialect.QueryGenerator.typeValidation = options.typeValidation; this.dialect.QueryGenerator.typeValidation = options.typeValidation;
if (this.options.operatorsAliases === true) {
Utils.deprecate('String based operators are now deprecated. Please use Symbol based operators for better security, read more at http://docs.sequelizejs.com/manual/tutorial/querying.html#operators');
this.dialect.QueryGenerator.setOperatorsAliases(Op.LegacyAliases); //Op.LegacyAliases should be removed and replaced by Op.Aliases by v5.0 use
} else {
this.dialect.QueryGenerator.setOperatorsAliases(this.options.operatorsAliases);
}
this.queryInterface = new QueryInterface(this); this.queryInterface = new QueryInterface(this);
...@@ -847,7 +857,7 @@ class Sequelize { ...@@ -847,7 +857,7 @@ class Sequelize {
* @return {Sequelize.and} * @return {Sequelize.and}
*/ */
static and() { static and() {
return { $and: Utils.sliceArgs(arguments) }; return { [Op.and]: Utils.sliceArgs(arguments) };
} }
/** /**
...@@ -861,7 +871,7 @@ class Sequelize { ...@@ -861,7 +871,7 @@ class Sequelize {
* @return {Sequelize.or} * @return {Sequelize.or}
*/ */
static or() { static or() {
return { $or: Utils.sliceArgs(arguments) }; return { [Op.or]: Utils.sliceArgs(arguments) };
} }
/** /**
...@@ -890,7 +900,7 @@ class Sequelize { ...@@ -890,7 +900,7 @@ class Sequelize {
* *
* @param {Object} attr The attribute, which can be either an attribute object from `Model.rawAttributes` or a sequelize object, for example an instance of `sequelize.fn`. For simple string attributes, use the POJO syntax * @param {Object} attr The attribute, which can be either an attribute object from `Model.rawAttributes` or a sequelize object, for example an instance of `sequelize.fn`. For simple string attributes, use the POJO syntax
* @param {string} [comparator='='] * @param {string} [comparator='=']
* @param {String|Object} logic The condition. Can be both a simply type, or a further condition (`$or`, `$and`, `.literal` etc.) * @param {String|Object} logic The condition. Can be both a simply type, or a further condition (`or`, `and`, `.literal` etc.)
* @alias condition * @alias condition
* @since v2.0.0-dev3 * @since v2.0.0-dev3
*/ */
...@@ -1155,6 +1165,13 @@ Sequelize.prototype.Promise = Sequelize.Promise = Promise; ...@@ -1155,6 +1165,13 @@ Sequelize.prototype.Promise = Sequelize.Promise = Promise;
*/ */
Sequelize.prototype.QueryTypes = Sequelize.QueryTypes = QueryTypes; Sequelize.prototype.QueryTypes = Sequelize.QueryTypes = QueryTypes;
/**
* Operators symbols to be used for querying data
* @see {@link Operators}
*/
Sequelize.prototype.Op = Sequelize.Op = Op;
/** /**
* Exposes the validator.js object, so you can extend it with custom validation functions. The validator is exposed both on the instance, and on the constructor. * Exposes the validator.js object, so you can extend it with custom validation functions. The validator is exposed both on the instance, and on the constructor.
* @see https://github.com/chriso/validator.js * @see https://github.com/chriso/validator.js
......
...@@ -7,6 +7,8 @@ const parameterValidator = require('./utils/parameter-validator'); ...@@ -7,6 +7,8 @@ const parameterValidator = require('./utils/parameter-validator');
const Logger = require('./utils/logger'); const Logger = require('./utils/logger');
const uuid = require('uuid'); const uuid = require('uuid');
const Promise = require('./promise'); const Promise = require('./promise');
const operators = require('./operators');
const operatorsArray = _.values(operators);
const primitives = ['string', 'number', 'boolean']; const primitives = ['string', 'number', 'boolean'];
let inflection = require('inflection'); let inflection = require('inflection');
...@@ -187,12 +189,9 @@ function mapOptionFieldNames(options, Model) { ...@@ -187,12 +189,9 @@ function mapOptionFieldNames(options, Model) {
exports.mapOptionFieldNames = mapOptionFieldNames; exports.mapOptionFieldNames = mapOptionFieldNames;
function mapWhereFieldNames(attributes, Model) { function mapWhereFieldNames(attributes, Model) {
let attribute;
let rawAttribute;
if (attributes) { if (attributes) {
for (attribute in attributes) { getComplexKeys(attributes).forEach(attribute => {
rawAttribute = Model.rawAttributes[attribute]; const rawAttribute = Model.rawAttributes[attribute];
if (rawAttribute && rawAttribute.field !== rawAttribute.fieldName) { if (rawAttribute && rawAttribute.field !== rawAttribute.fieldName) {
attributes[rawAttribute.field] = attributes[attribute]; attributes[rawAttribute.field] = attributes[attribute];
...@@ -217,7 +216,8 @@ function mapWhereFieldNames(attributes, Model) { ...@@ -217,7 +216,8 @@ function mapWhereFieldNames(attributes, Model) {
return where; return where;
}); });
} }
}
});
} }
return attributes; return attributes;
...@@ -564,3 +564,38 @@ exports.mapIsolationLevelStringToTedious = (isolationLevel, tedious) => { ...@@ -564,3 +564,38 @@ exports.mapIsolationLevelStringToTedious = (isolationLevel, tedious) => {
return tediousIsolationLevel.SNAPSHOT; return tediousIsolationLevel.SNAPSHOT;
} }
}; };
//Collection of helper methods to make it easier to work with symbol operators
/**
* getOperators
* @param {Object} obj
* @return {Array<Symbol>} All operators properties of obj
* @private
*/
function getOperators(obj) {
return _.intersection(Object.getOwnPropertySymbols(obj), operatorsArray);
}
exports.getOperators = getOperators;
/**
* getComplexKeys
* @param {Object} obj
* @return {Array<String|Symbol>} All keys including operators
* @private
*/
function getComplexKeys(obj) {
return getOperators(obj).concat(_.keys(obj));
}
exports.getComplexKeys = getComplexKeys;
/**
* getComplexSize
* @param {Object|Array} obj
* @return {Integer} Length of object properties including operators if obj is array returns its length
* @private
*/
function getComplexSize(obj) {
return Array.isArray(obj) ? obj.length : getComplexKeys(obj).length;
}
exports.getComplexSize = getComplexSize;
...@@ -11,6 +11,7 @@ const chai = require('chai'), ...@@ -11,6 +11,7 @@ const chai = require('chai'),
config = require(__dirname + '/../config/config'), config = require(__dirname + '/../config/config'),
moment = require('moment'), moment = require('moment'),
Transaction = require(__dirname + '/../../lib/transaction'), Transaction = require(__dirname + '/../../lib/transaction'),
Utils = require(__dirname + '/../../lib/utils'),
sinon = require('sinon'), sinon = require('sinon'),
semver = require('semver'), semver = require('semver'),
current = Support.sequelize; current = Support.sequelize;
...@@ -28,6 +29,10 @@ const qq = function(str) { ...@@ -28,6 +29,10 @@ const qq = function(str) {
describe(Support.getTestDialectTeaser('Sequelize'), () => { describe(Support.getTestDialectTeaser('Sequelize'), () => {
describe('constructor', () => { describe('constructor', () => {
afterEach(() => {
Utils.deprecate.restore && Utils.deprecate.restore();
});
if (dialect !== 'sqlite') { if (dialect !== 'sqlite') {
it.skip('should work with min connections', () => { it.skip('should work with min connections', () => {
const ConnectionManager = current.dialect.connectionManager, const ConnectionManager = current.dialect.connectionManager,
...@@ -55,6 +60,27 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { ...@@ -55,6 +60,27 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => {
expect(sequelize.config.host).to.equal('127.0.0.1'); expect(sequelize.config.host).to.equal('127.0.0.1');
}); });
it('should log deprecated warning if operators aliases were not set', () => {
sinon.stub(Utils, 'deprecate');
Support.createSequelizeInstance();
expect(Utils.deprecate.calledOnce).to.be.true;
expect(Utils.deprecate.args[0][0]).to.be.equal('String based operators are now deprecated. Please use Symbol based operators for better security, read more at http://docs.sequelizejs.com/manual/tutorial/querying.html#operators');
Utils.deprecate.reset();
Support.createSequelizeInstance({ operatorsAliases: {} });
expect(Utils.deprecate.called).to.be.false;
});
it('should set operators aliases on dialect QueryGenerator', () => {
const operatorsAliases = { fake: true };
const sequelize = Support.createSequelizeInstance({ operatorsAliases });
expect(sequelize).to.have.property('dialect');
expect(sequelize.dialect).to.have.property('QueryGenerator');
expect(sequelize.dialect.QueryGenerator).to.have.property('OperatorsAliasMap');
expect(sequelize.dialect.QueryGenerator.OperatorsAliasMap).to.be.eql(operatorsAliases);
});
if (dialect === 'sqlite') { if (dialect === 'sqlite') {
it('should work with connection strings (1)', () => { it('should work with connection strings (1)', () => {
const sequelize = new Sequelize('sqlite://test.sqlite'); // eslint-disable-line const sequelize = new Sequelize('sqlite://test.sqlite'); // eslint-disable-line
......
...@@ -8,7 +8,9 @@ const fs = require('fs'), ...@@ -8,7 +8,9 @@ const fs = require('fs'),
Config = require(__dirname + '/config/config'), Config = require(__dirname + '/config/config'),
supportShim = require(__dirname + '/supportShim'), supportShim = require(__dirname + '/supportShim'),
chai = require('chai'), chai = require('chai'),
expect = chai.expect; expect = chai.expect,
AbstractQueryGenerator = require('../lib/dialects/abstract/query-generator');
chai.use(require('chai-spies')); chai.use(require('chai-spies'));
chai.use(require('chai-datetime')); chai.use(require('chai-datetime'));
...@@ -153,6 +155,14 @@ const Support = { ...@@ -153,6 +155,14 @@ const Support = {
} }
}, },
getAbstractQueryGenerator(sequelize) {
return Object.assign(
{},
AbstractQueryGenerator,
{options: sequelize.options, _dialect: sequelize.dialect, sequelize, quoteIdentifier(identifier) { return identifier; }}
);
},
getTestDialect() { getTestDialect() {
let envDialect = process.env.DIALECT || 'mysql'; let envDialect = process.env.DIALECT || 'mysql';
......
...@@ -8,6 +8,7 @@ const chai = require('chai'), ...@@ -8,6 +8,7 @@ const chai = require('chai'),
Support = require(__dirname + '/../support'), Support = require(__dirname + '/../support'),
DataTypes = require(__dirname + '/../../../lib/data-types'), DataTypes = require(__dirname + '/../../../lib/data-types'),
HasMany = require(__dirname + '/../../../lib/associations/has-many'), HasMany = require(__dirname + '/../../../lib/associations/has-many'),
Op = require(__dirname + '/../../../lib/operators'),
current = Support.sequelize, current = Support.sequelize,
Promise = current.Promise; Promise = current.Promise;
...@@ -149,7 +150,8 @@ describe(Support.getTestDialectTeaser('hasMany'), () => { ...@@ -149,7 +150,8 @@ describe(Support.getTestDialectTeaser('hasMany'), () => {
}); });
it('should fetch associations for multiple source instances', () => { it('should fetch associations for multiple source instances', () => {
const findAll = stub(Task, 'findAll').returns(Promise.resolve([ const findAll = stub(Task, 'findAll').returns(
Promise.resolve([
Task.build({ Task.build({
'user_id': idA 'user_id': idA
}), }),
...@@ -162,8 +164,7 @@ describe(Support.getTestDialectTeaser('hasMany'), () => { ...@@ -162,8 +164,7 @@ describe(Support.getTestDialectTeaser('hasMany'), () => {
Task.build({ Task.build({
'user_id': idB 'user_id': idB
}) })
])), ]));
where = {};
User.Tasks = User.hasMany(Task, {foreignKey}); User.Tasks = User.hasMany(Task, {foreignKey});
const actual = User.Tasks.get([ const actual = User.Tasks.get([
...@@ -172,12 +173,10 @@ describe(Support.getTestDialectTeaser('hasMany'), () => { ...@@ -172,12 +173,10 @@ describe(Support.getTestDialectTeaser('hasMany'), () => {
User.build({id: idC}) User.build({id: idC})
]); ]);
where[foreignKey] = {
$in: [idA, idB, idC]
};
expect(findAll).to.have.been.calledOnce; expect(findAll).to.have.been.calledOnce;
expect(findAll.firstCall.args[0].where).to.deep.equal(where); expect(findAll.firstCall.args[0].where).to.have.property(foreignKey);
expect(findAll.firstCall.args[0].where[foreignKey]).to.have.property(Op.in);
expect(findAll.firstCall.args[0].where[foreignKey][Op.in]).to.deep.equal([idA, idB, idC]);
return actual.then(result => { return actual.then(result => {
expect(result).to.be.an('object'); expect(result).to.be.an('object');
......
'use strict';
const chai = require('chai'),
expect = chai.expect,
Op = require('../../../../lib/operators'),
getAbstractQueryGenerator = require(__dirname + '/../../support').getAbstractQueryGenerator;
describe('QueryGenerator', () => {
describe('whereItemQuery', () => {
it('should generate correct query for Symbol operators', function() {
const QG = getAbstractQueryGenerator(this.sequelize);
QG.whereItemQuery(Op.or, [{test: {[Op.gt]: 5}}, {test: {[Op.lt]: 3}}, {test: {[Op.in]: [4]}}])
.should.be.equal('(test > 5 OR test < 3 OR test IN (4))');
QG.whereItemQuery(Op.and, [{test: {[Op.between]: [2, 5]}}, {test: {[Op.ne]: 3}}, {test: {[Op.not]: 4}}])
.should.be.equal('(test BETWEEN 2 AND 5 AND test != 3 AND test != 4)');
});
it('should not parse any strings as aliases operators', function() {
const QG = getAbstractQueryGenerator(this.sequelize);
expect(() => QG.whereItemQuery('$or', [{test: 5}, {test: 3}]))
.to.throw('Invalid value [object Object]');
expect(() => QG.whereItemQuery('$and', [{test: 5}, {test: 3}]))
.to.throw('Invalid value [object Object]');
expect(() => QG.whereItemQuery('test', {$gt: 5}))
.to.throw('Invalid value [object Object]');
expect(() => QG.whereItemQuery('test', {$between: [2, 5]}))
.to.throw('Invalid value [object Object]');
expect(() => QG.whereItemQuery('test', {$ne: 3}))
.to.throw('Invalid value [object Object]');
expect(() => QG.whereItemQuery('test', {$not: 3}))
.to.throw('Invalid value [object Object]');
expect(() => QG.whereItemQuery('test', {$in: [4]}))
.to.throw('Invalid value [object Object]');
});
it('should parse set aliases strings as operators', function() {
const QG = getAbstractQueryGenerator(this.sequelize),
aliases = {
OR: Op.or,
'!': Op.not,
'^^': Op.gt
};
QG.setOperatorsAliases(aliases);
QG.whereItemQuery('OR', [{test: {'^^': 5}}, {test: {'!': 3}}, {test: {[Op.in]: [4]}}])
.should.be.equal('(test > 5 OR test != 3 OR test IN (4))');
QG.whereItemQuery(Op.and, [{test: {[Op.between]: [2, 5]}}, {test: {'!': 3}}, {test: {'^^': 4}}])
.should.be.equal('(test BETWEEN 2 AND 5 AND test != 3 AND test > 4)');
expect(() => QG.whereItemQuery('OR', [{test: {'^^': 5}}, {test: {$not: 3}}, {test: {[Op.in]: [4]}}]))
.to.throw('Invalid value [object Object]');
expect(() => QG.whereItemQuery('OR', [{test: {$gt: 5}}, {test: {'!': 3}}, {test: {[Op.in]: [4]}}]))
.to.throw('Invalid value [object Object]');
expect(() => QG.whereItemQuery('$or', [{test: 5}, {test: 3}]))
.to.throw('Invalid value [object Object]');
expect(() => QG.whereItemQuery('$and', [{test: 5}, {test: 3}]))
.to.throw('Invalid value [object Object]');
expect(() => QG.whereItemQuery('test', {$gt: 5}))
.to.throw('Invalid value [object Object]');
expect(() => QG.whereItemQuery('test', {$between: [2, 5]}))
.to.throw('Invalid value [object Object]');
expect(() => QG.whereItemQuery('test', {$ne: 3}))
.to.throw('Invalid value [object Object]');
expect(() => QG.whereItemQuery('test', {$not: 3}))
.to.throw('Invalid value [object Object]');
expect(() => QG.whereItemQuery('test', {$in: [4]}))
.to.throw('Invalid value [object Object]');
});
});
});
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
const Support = require(__dirname + '/../../support'); const Support = require(__dirname + '/../../support');
const expectsql = Support.expectsql; const expectsql = Support.expectsql;
const current = Support.sequelize; const current = Support.sequelize;
const Operators = require('../../../../lib/operators');
const QueryGenerator = require('../../../../lib/dialects/mssql/query-generator'); const QueryGenerator = require('../../../../lib/dialects/mssql/query-generator');
const _ = require('lodash'); const _ = require('lodash');
...@@ -10,6 +11,8 @@ if (current.dialect.name === 'mssql') { ...@@ -10,6 +11,8 @@ if (current.dialect.name === 'mssql') {
suite('[MSSQL Specific] QueryGenerator', () => { suite('[MSSQL Specific] QueryGenerator', () => {
// Dialect would normally be set by the query interface that instantiates the query-generator, but here we specify it explicitly // Dialect would normally be set by the query interface that instantiates the query-generator, but here we specify it explicitly
QueryGenerator._dialect = current.dialect; QueryGenerator._dialect = current.dialect;
//Aliases might not be needed here since it doesn't seem like this test uses any operators
QueryGenerator.setOperatorsAliases(Operators.Aliases);
test('getDefaultConstraintQuery', () => { test('getDefaultConstraintQuery', () => {
expectsql(QueryGenerator.getDefaultConstraintQuery({tableName: 'myTable', schema: 'mySchema'}, 'myColumn'), { expectsql(QueryGenerator.getDefaultConstraintQuery({tableName: 'myTable', schema: 'mySchema'}, 'myColumn'), {
......
...@@ -5,6 +5,7 @@ const chai = require('chai'), ...@@ -5,6 +5,7 @@ const chai = require('chai'),
Support = require(__dirname + '/../../support'), Support = require(__dirname + '/../../support'),
dialect = Support.getTestDialect(), dialect = Support.getTestDialect(),
_ = require('lodash'), _ = require('lodash'),
Operators = require('../../../../lib/operators'),
QueryGenerator = require('../../../../lib/dialects/mysql/query-generator'); QueryGenerator = require('../../../../lib/dialects/mysql/query-generator');
if (dialect === 'mysql') { if (dialect === 'mysql') {
...@@ -577,6 +578,7 @@ if (dialect === 'mysql') { ...@@ -577,6 +578,7 @@ if (dialect === 'mysql') {
QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' }); QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' });
QueryGenerator._dialect = this.sequelize.dialect; QueryGenerator._dialect = this.sequelize.dialect;
QueryGenerator.sequelize = this.sequelize; QueryGenerator.sequelize = this.sequelize;
QueryGenerator.setOperatorsAliases(Operators.LegacyAliases);
const conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments); const conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments);
expect(conditions).to.deep.equal(test.expectation); expect(conditions).to.deep.equal(test.expectation);
}); });
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
const chai = require('chai'), const chai = require('chai'),
expect = chai.expect, expect = chai.expect,
Operators = require('../../../../lib/operators'),
QueryGenerator = require('../../../../lib/dialects/postgres/query-generator'), QueryGenerator = require('../../../../lib/dialects/postgres/query-generator'),
Support = require(__dirname + '/../../support'), Support = require(__dirname + '/../../support'),
dialect = Support.getTestDialect(), dialect = Support.getTestDialect(),
...@@ -947,6 +948,7 @@ if (dialect.match(/^postgres/)) { ...@@ -947,6 +948,7 @@ if (dialect.match(/^postgres/)) {
QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' }); QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' });
QueryGenerator._dialect = this.sequelize.dialect; QueryGenerator._dialect = this.sequelize.dialect;
QueryGenerator.sequelize = this.sequelize; QueryGenerator.sequelize = this.sequelize;
QueryGenerator.setOperatorsAliases(Operators.LegacyAliases);
const conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments); const conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments);
expect(conditions).to.deep.equal(test.expectation); expect(conditions).to.deep.equal(test.expectation);
}); });
......
...@@ -7,6 +7,7 @@ const chai = require('chai'), ...@@ -7,6 +7,7 @@ const chai = require('chai'),
dialect = Support.getTestDialect(), dialect = Support.getTestDialect(),
_ = require('lodash'), _ = require('lodash'),
moment = require('moment'), moment = require('moment'),
Operators = require('../../../../lib/operators'),
QueryGenerator = require('../../../../lib/dialects/sqlite/query-generator'); QueryGenerator = require('../../../../lib/dialects/sqlite/query-generator');
if (dialect === 'sqlite') { if (dialect === 'sqlite') {
...@@ -550,6 +551,7 @@ if (dialect === 'sqlite') { ...@@ -550,6 +551,7 @@ if (dialect === 'sqlite') {
QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' }); QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' });
QueryGenerator._dialect = this.sequelize.dialect; QueryGenerator._dialect = this.sequelize.dialect;
QueryGenerator.sequelize = this.sequelize; QueryGenerator.sequelize = this.sequelize;
QueryGenerator.setOperatorsAliases(Operators.LegacyAliases);
const conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments); const conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments);
expect(conditions).to.deep.equal(test.expectation); expect(conditions).to.deep.equal(test.expectation);
}); });
......
...@@ -267,13 +267,13 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -267,13 +267,13 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
suite('$and', () => { suite('$and', () => {
testsql('$and', { testsql('$and', {
shared: 1,
$or: { $or: {
group_id: 1, group_id: 1,
user_id: 2 user_id: 2
} },
shared: 1
}, { }, {
default: '([shared] = 1 AND ([group_id] = 1 OR [user_id] = 2))' default: '(([group_id] = 1 OR [user_id] = 2) AND [shared] = 1)'
}); });
testsql('$and', [ testsql('$and', [
...@@ -320,13 +320,13 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -320,13 +320,13 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
suite('$not', () => { suite('$not', () => {
testsql('$not', { testsql('$not', {
shared: 1,
$or: { $or: {
group_id: 1, group_id: 1,
user_id: 2 user_id: 2
} },
shared: 1
}, { }, {
default: 'NOT ([shared] = 1 AND ([group_id] = 1 OR [user_id] = 2))' default: 'NOT (([group_id] = 1 OR [user_id] = 2) AND [shared] = 1)'
}); });
testsql('$not', [], { testsql('$not', [], {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!