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

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
.findAndCountAll({
where: {
title: {
$like: 'foo%'
[Op.like]: 'foo%'
}
},
offset: 10,
......@@ -168,28 +168,28 @@ Project.findAll({ where: { id: [1,2,3] } }).then(projects => {
Project.findAll({
where: {
id: {
$and: {a: 5} // AND (a = 5)
$or: [{a: 5}, {a: 6}] // (a = 5 OR a = 6)
$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]
$notIn: [1, 2], // NOT IN [1, 2]
$like: '%hat', // LIKE '%hat'
$notLike: '%hat' // NOT LIKE '%hat'
$iLike: '%hat' // ILIKE '%hat' (case insensitive) (PG only)
$notILike: '%hat' // NOT ILIKE '%hat' (PG only)
$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)
$any: [2,3] // ANY ARRAY[2, 3]::INTEGER (PG only)
[Op.and]: {a: 5} // AND (a = 5)
[Op.or]: [{a: 5}, {a: 6}] // (a = 5 OR a = 6)
[Op.gt]: 6, // id > 6
[Op.gte]: 6, // id >= 6
[Op.lt]: 10, // id < 10
[Op.lte]: 10, // id <= 10
[Op.ne]: 20, // id != 20
[Op.between]: [6, 10], // BETWEEN 6 AND 10
[Op.notBetween]: [11, 15], // NOT BETWEEN 11 AND 15
[Op.in]: [1, 2], // IN [1, 2]
[Op.notIn]: [1, 2], // NOT IN [1, 2]
[Op.like]: '%hat', // LIKE '%hat'
[Op.notLike]: '%hat' // NOT LIKE '%hat'
[Op.iLike]: '%hat' // ILIKE '%hat' (case insensitive) (PG only)
[Op.notILike]: '%hat' // NOT ILIKE '%hat' (PG only)
[Op.overlap]: [1, 2] // && [1, 2] (PG array overlap operator)
[Op.contains]: [1, 2] // @> [1, 2] (PG array contains operator)
[Op.contained]: [1, 2] // <@ [1, 2] (PG array contained by operator)
[Op.any]: [2,3] // ANY ARRAY[2, 3]::INTEGER (PG only)
},
status: {
$not: false, // status NOT FALSE
[Op.not]: false, // status NOT FALSE
}
}
})
......@@ -197,15 +197,15 @@ Project.findAll({
### 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
Project.findOne({
where: {
name: 'a project',
$or: [
[Op.or]: [
{ id: [1,2,3] },
{ id: { $gt: 10 } }
{ id: { [Op.gt]: 10 } }
]
}
})
......@@ -214,9 +214,9 @@ Project.findOne({
where: {
name: 'a project',
id: {
$or: [
[Op.or]: [
[1,2,3],
{ $gt: 10 }
{ [Op.gt]: 10 }
]
}
}
......@@ -235,15 +235,15 @@ WHERE (
LIMIT 1;
```
`$not` example:
`not` example:
```js
Project.findOne({
where: {
name: 'a project',
$not: [
[Op.not]: [
{ id: [1,2,3] },
{ array: { $contains: [3,4,5] } }
{ array: { [Op.contains]: [3,4,5] } }
]
}
});
......@@ -338,7 +338,7 @@ Project.count().then(c => {
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.")
})
```
......@@ -358,7 +358,7 @@ Project.max('age').then(max => {
// 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
})
```
......@@ -378,7 +378,7 @@ Project.min('age').then(min => {
// 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
})
```
......@@ -399,7 +399,7 @@ Project.sum('age').then(sum => {
// 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
})
```
......@@ -550,7 +550,7 @@ User.findAll({
include: [{
model: Tool,
as: 'Instruments',
where: { name: { $like: '%ooth%' } }
where: { name: { [Op.like]: '%ooth%' } }
}]
}).then(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
```js
User.findAll({
where: {
'$Instruments.name$': { $iLike: '%ooth%' }
'$Instruments.name$': { [Op.iLike]: '%ooth%' }
},
include: [{
model: Tool,
......@@ -653,7 +653,7 @@ In case you want to eager load soft deleted records you can do that by setting `
User.findAll({
include: [{
model: Tool,
where: { name: { $like: '%ooth%' } },
where: { name: { [Op.like]: '%ooth%' } },
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
`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
```js
const Op = Sequelize.Op;
Post.findAll({
where: {
authorId: 2
......@@ -103,7 +105,7 @@ Post.update({
}, {
where: {
deletedAt: {
$ne: null
[Op.ne]: null
}
}
});
......@@ -117,39 +119,42 @@ Post.findAll({
### Operators
Sequelize exposes symbol operators that can be used for to create more complex comparisons -
```js
$and: {a: 5} // AND (a = 5)
$or: [{a: 5}, {a: 6}] // (a = 5 OR a = 6)
$gt: 6, // > 6
$gte: 6, // >= 6
$lt: 10, // < 10
$lte: 10, // <= 10
$ne: 20, // != 20
$eq: 3, // = 3
$not: true, // IS NOT TRUE
$between: [6, 10], // BETWEEN 6 AND 10
$notBetween: [11, 15], // NOT BETWEEN 11 AND 15
$in: [1, 2], // IN [1, 2]
$notIn: [1, 2], // NOT IN [1, 2]
$like: '%hat', // LIKE '%hat'
$notLike: '%hat' // NOT LIKE '%hat'
$iLike: '%hat' // ILIKE '%hat' (case insensitive) (PG only)
$notILike: '%hat' // NOT ILIKE '%hat' (PG only)
$regexp: '^[h|a|t]' // REGEXP/~ '^[h|a|t]' (MySQL/PG only)
$notRegexp: '^[h|a|t]' // NOT REGEXP/!~ '^[h|a|t]' (MySQL/PG only)
$iRegexp: '^[h|a|t]' // ~* '^[h|a|t]' (PG only)
$notIRegexp: '^[h|a|t]' // !~* '^[h|a|t]' (PG only)
$like: { $any: ['cat', 'hat']}
const Op = Sequelize.Op
[Op.and]: {a: 5} // AND (a = 5)
[Op.or]: [{a: 5}, {a: 6}] // (a = 5 OR a = 6)
[Op.gt]: 6, // > 6
[Op.gte]: 6, // >= 6
[Op.lt]: 10, // < 10
[Op.lte]: 10, // <= 10
[Op.ne]: 20, // != 20
[Op.eq]: 3, // = 3
[Op.not]: true, // IS NOT TRUE
[Op.between]: [6, 10], // BETWEEN 6 AND 10
[Op.notBetween]: [11, 15], // NOT BETWEEN 11 AND 15
[Op.in]: [1, 2], // IN [1, 2]
[Op.notIn]: [1, 2], // NOT IN [1, 2]
[Op.like]: '%hat', // LIKE '%hat'
[Op.notLike]: '%hat' // NOT LIKE '%hat'
[Op.iLike]: '%hat' // ILIKE '%hat' (case insensitive) (PG only)
[Op.notILike]: '%hat' // NOT ILIKE '%hat' (PG only)
[Op.regexp]: '^[h|a|t]' // REGEXP/~ '^[h|a|t]' (MySQL/PG only)
[Op.notRegexp]: '^[h|a|t]' // NOT REGEXP/!~ '^[h|a|t]' (MySQL/PG only)
[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
$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)
$any: [2,3] // ANY ARRAY[2, 3]::INTEGER (PG only)
[Op.overlap]: [1, 2] // && [1, 2] (PG array overlap operator)
[Op.contains]: [1, 2] // @> [1, 2] (PG array contains operator)
[Op.contained]: [1, 2] // <@ [1, 2] (PG array contained by operator)
[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.
......@@ -160,24 +165,26 @@ as well.
```js
// All the above equality and inequality operators plus the following:
$contains: 2 // @> '2'::integer (PG range contains element operator)
$contains: [1, 2] // @> [1, 2) (PG range contains range operator)
$contained: [1, 2] // <@ [1, 2) (PG range is contained by operator)
$overlap: [1, 2] // && [1, 2) (PG range overlap (have points in common) operator)
$adjacent: [1, 2] // -|- [1, 2) (PG range is adjacent to operator)
$strictLeft: [1, 2] // << [1, 2) (PG range strictly left of operator)
$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)
$noExtendLeft: [1, 2] // &> [1, 2) (PG range does not extend to the left of operator)
[Op.contains]: 2 // @> '2'::integer (PG range contains element operator)
[Op.contains]: [1, 2] // @> [1, 2) (PG range contains range operator)
[Op.contained]: [1, 2] // <@ [1, 2) (PG range is contained by operator)
[Op.overlap]: [1, 2] // && [1, 2) (PG range overlap (have points in common) operator)
[Op.adjacent]: [1, 2] // -|- [1, 2) (PG range is adjacent to operator)
[Op.strictLeft]: [1, 2] // << [1, 2) (PG range strictly left of operator)
[Op.strictRight]: [1, 2] // >> [1, 2) (PG range strictly right of operator)
[Op.noExtendRight]: [1, 2] // &< [1, 2) (PG range does not extend to the right of operator)
[Op.noExtendLeft]: [1, 2] // &> [1, 2) (PG range does not extend to the left of operator)
```
### Combinations
#### Combinations
```js
const Op = Sequelize.Op;
{
rank: {
$or: {
$lt: 1000,
$eq: null
[Op.or]: {
[Op.lt]: 1000,
[Op.eq]: null
}
}
}
......@@ -185,22 +192,22 @@ $noExtendLeft: [1, 2] // &> [1, 2) (PG range does not extend to the left of ope
{
createdAt: {
$lt: new Date(),
$gt: new Date(new Date() - 24 * 60 * 60 * 1000)
[Op.lt]: new Date(),
[Op.gt]: new Date(new Date() - 24 * 60 * 60 * 1000)
}
}
// createdAt < [timestamp] AND createdAt > [timestamp]
{
$or: [
[Op.or]: [
{
title: {
$like: 'Boat%'
[Op.like]: 'Boat%'
}
},
{
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
// 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 can be queried in three different ways.
......@@ -218,7 +308,7 @@ JSONB can be queried in three different ways.
meta: {
video: {
url: {
$ne: null
[Op.ne]: null
}
}
}
......@@ -229,7 +319,7 @@ JSONB can be queried in three different ways.
```js
{
"meta.audio.length": {
$gt: 20
[Op.gt]: 20
}
}
```
......@@ -238,7 +328,7 @@ JSONB can be queried in three different ways.
```js
{
"meta": {
$contains: {
[Op.contains]: {
site: {
url: 'http://google.com'
}
......
......@@ -36,7 +36,7 @@ const Project = sequelize.define('project', {
return {
where: {
accessLevel: {
$gte: value
[Op.gte]: value
}
}
}
......@@ -128,7 +128,7 @@ When invoking several scopes, keys from subsequent scopes will overwrite previou
where: {
firstName: 'bob',
age: {
$gt: 20
[Op.gt]: 20
}
},
limit: 2
......@@ -136,7 +136,7 @@ When invoking several scopes, keys from subsequent scopes will overwrite previou
scope2: {
where: {
age: {
$gt: 30
[Op.gt]: 30
}
},
limit: 10
......
......@@ -8,6 +8,7 @@ const BelongsTo = require('./belongs-to');
const HasMany = require('./has-many');
const HasOne = require('./has-one');
const AssociationError = require('../errors').AssociationError;
const Op = require('../operators');
/**
* Many-to-many association with a join table.
......@@ -383,7 +384,7 @@ class BelongsToMany extends Association {
}
options.where = {
$and: [
[Op.and]: [
scopeWhere,
options.where
]
......@@ -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 (options.through && options.through.where) {
throughWhere = {
$and: [throughWhere, options.through.where]
[Op.and]: [throughWhere, options.through.where]
};
}
......@@ -474,7 +475,7 @@ class BelongsToMany extends Association {
scope: false
});
where.$or = instances.map(instance => {
where[Op.or] = instances.map(instance => {
if (instance instanceof association.target) {
return instance.where();
} else {
......@@ -485,7 +486,7 @@ class BelongsToMany extends Association {
});
options.where = {
$and: [
[Op.and]: [
where,
options.where
]
......
......@@ -5,6 +5,8 @@ const Helpers = require('./helpers');
const _ = require('lodash');
const Transaction = require('../transaction');
const Association = require('./base');
const Op = require('../operators');
/**
* One-to-one association
......@@ -141,7 +143,7 @@ class BelongsTo extends Association {
if (instances) {
where[association.targetKey] = {
$in: instances.map(instance => instance.get(association.foreignKey))
[Op.in]: instances.map(instance => instance.get(association.foreignKey))
};
} else {
if (association.targetKeyIsPrimary && !options.where) {
......@@ -153,7 +155,7 @@ class BelongsTo extends Association {
}
options.where = options.where ?
{$and: [where, options.where]} :
{[Op.and]: [where, options.where]} :
where;
if (instances) {
......
......@@ -4,6 +4,7 @@ const Utils = require('./../utils');
const Helpers = require('./helpers');
const _ = require('lodash');
const Association = require('./base');
const Op = require('../operators');
/**
* One-to-many association
......@@ -192,7 +193,7 @@ class HasMany extends Association {
delete options.limit;
} else {
where[association.foreignKey] = {
$in: values
[Op.in]: values
};
delete options.groupedLimit;
}
......@@ -202,7 +203,7 @@ class HasMany extends Association {
options.where = options.where ?
{$and: [where, options.where]} :
{[Op.and]: [where, options.where]} :
where;
if (options.hasOwnProperty('scope')) {
......@@ -277,7 +278,7 @@ class HasMany extends Association {
raw: true
});
where.$or = targetInstances.map(instance => {
where[Op.or] = targetInstances.map(instance => {
if (instance instanceof association.target) {
return instance.where();
} else {
......@@ -288,7 +289,7 @@ class HasMany extends Association {
});
options.where = {
$and: [
[Op.and]: [
where,
options.where
]
......
......@@ -4,6 +4,7 @@ const Utils = require('./../utils');
const Helpers = require('./helpers');
const _ = require('lodash');
const Association = require('./base');
const Op = require('../operators');
/**
* One-to-one association
......@@ -140,7 +141,7 @@ class HasOne extends Association {
if (instances) {
where[association.foreignKey] = {
$in: instances.map(instance => instance.get(association.sourceKey))
[Op.in]: instances.map(instance => instance.get(association.sourceKey))
};
} else {
where[association.foreignKey] = instance.get(association.sourceKey);
......@@ -151,7 +152,7 @@ class HasOne extends Association {
}
options.where = options.where ?
{$and: [where, options.where]} :
{[Op.and]: [where, options.where]} :
where;
if (instances) {
......
......@@ -11,6 +11,7 @@ const Association = require('../../associations/base');
const BelongsTo = require('../../associations/belongs-to');
const BelongsToMany = require('../../associations/belongs-to-many');
const HasMany = require('../../associations/has-many');
const Op = require('../../operators');
const uuid = require('uuid');
const semver = require('semver');
......@@ -1035,7 +1036,7 @@ const QueryGenerator = {
duplicating: false, // The UNION'ed query may contain duplicates, but each sub-query cannot
required: true,
where: Object.assign({
'$$PLACEHOLDER$$': true
[Op.placeholder]: true
}, options.groupedLimit.through && options.groupedLimit.through.where)
}],
model
......@@ -1073,7 +1074,7 @@ const QueryGenerator = {
// Ordering is handled by the subqueries, so ordering the UNION'ed result is not needed
groupedLimitOrder = options.order;
delete options.order;
where.$$PLACEHOLDER$$ = true;
where[Op.placeholder] = true;
}
// Caching the base query and splicing the where part into it is consistently > twice
......@@ -1090,7 +1091,7 @@ const QueryGenerator = {
},
model
).replace(/;$/, '') + ')';
const placeHolder = this.whereItemQuery('$$PLACEHOLDER$$', true, { model });
const placeHolder = this.whereItemQuery(Op.placeholder, true, { model });
const splicePos = baseQuery.indexOf(placeHolder);
mainQueryItems.push(this.selectFromTableFragment(options, mainTable.model, attributes.main, '(' +
......@@ -1316,7 +1317,7 @@ const QueryGenerator = {
const associationWhere = {};
associationWhere[association.identifierField] = {
$eq: this.sequelize.literal(`${this.quoteTable(parentTableName.internalAs)}.${this.quoteIdentifier(association.sourceKeyField || association.source.primaryKeyField)}`)
[Op.eq]: this.sequelize.literal(`${this.quoteTable(parentTableName.internalAs)}.${this.quoteIdentifier(association.sourceKeyField || association.source.primaryKeyField)}`)
};
if (!topLevelInfo.options.where) {
......@@ -1327,7 +1328,7 @@ const QueryGenerator = {
const $query = this.selectQuery(include.model.getTableName(), {
attributes: [association.identifierField],
where: {
$and: [
[Op.and]: [
associationWhere,
include.where || {}
]
......@@ -1346,7 +1347,7 @@ const QueryGenerator = {
if (_.isPlainObject(topLevelInfo.options.where)) {
topLevelInfo.options.where['__' + includeAs.internalAs] = subQueryWhere;
} else {
topLevelInfo.options.where = { $and: [topLevelInfo.options.where, subQueryWhere] };
topLevelInfo.options.where = { [Op.and]: [topLevelInfo.options.where, subQueryWhere] };
}
}
joinQuery = this.generateJoin(include, topLevelInfo);
......@@ -1616,7 +1617,7 @@ const QueryGenerator = {
}).include,
model: topInclude.through.model,
where: {
$and: [
[Op.and]: [
this.sequelize.asIs([
this.quoteTable(topParent.model.name) + '.' + this.quoteIdentifier(topParent.model.primaryKeyField),
this.quoteIdentifier(topInclude.through.model.name) + '.' + this.quoteIdentifier(topInclude.association.identifierField)
......@@ -1637,7 +1638,7 @@ const QueryGenerator = {
attributes: [topInclude.model.primaryKeyAttributes[0]],
include: topInclude.include,
where: {
$join: this.sequelize.asIs(join)
[Op.join]: this.sequelize.asIs(join)
},
limit: 1,
includeIgnoreAttributes: false
......@@ -1915,10 +1916,9 @@ const QueryGenerator = {
whereItemsQuery(where, options, binding) {
if (
Array.isArray(where) && where.length === 0 ||
_.isPlainObject(where) && _.isEmpty(where) ||
where === null ||
where === undefined
where === undefined ||
Utils.getComplexSize(where) === 0
) {
// NO OP
return '';
......@@ -1934,8 +1934,9 @@ const QueryGenerator = {
if (binding.substr(0, 1) !== ' ') binding = ' '+binding+' ';
if (_.isPlainObject(where)) {
_.forOwn(where, (value, key) => {
items.push(this.whereItemQuery(key, value, options));
Utils.getComplexKeys(where).forEach(prop => {
const item = where[prop];
items.push(this.whereItemQuery(prop, item, options));
});
} else {
items.push(this.whereItemQuery(undefined, where, options));
......@@ -1944,68 +1945,52 @@ const QueryGenerator = {
return items.length && items.filter(item => item && item.length).join(binding) || '';
},
OperatorsAliasMap: {
'ne': '$ne',
'in': '$in',
'not': '$not',
'notIn': '$notIn',
'gte': '$gte',
'gt': '$gt',
'lte': '$lte',
'lt': '$lt',
'like': '$like',
'ilike': '$iLike',
'$ilike': '$iLike',
'nlike': '$notLike',
'$notlike': '$notLike',
'notilike': '$notILike',
'..': '$between',
'between': '$between',
'!..': '$notBetween',
'notbetween': '$notBetween',
'nbetween': '$notBetween',
'overlap': '$overlap',
'&&': '$overlap',
'@>': '$contains',
'<@': '$contained'
OperatorMap: {
[Op.eq]: '=',
[Op.ne]: '!=',
[Op.gte]: '>=',
[Op.gt]: '>',
[Op.lte]: '<=',
[Op.lt]: '<',
[Op.not]: 'IS NOT',
[Op.is]: 'IS',
[Op.in]: 'IN',
[Op.notIn]: 'NOT IN',
[Op.like]: 'LIKE',
[Op.notLike]: 'NOT LIKE',
[Op.iLike]: 'ILIKE',
[Op.notILike]: 'NOT ILIKE',
[Op.regexp]: '~',
[Op.notRegexp]: '!~',
[Op.iRegexp]: '~*',
[Op.notIRegexp]: '!~*',
[Op.between]: 'BETWEEN',
[Op.notBetween]: 'NOT BETWEEN',
[Op.overlap]: '&&',
[Op.contains]: '@>',
[Op.contained]: '<@',
[Op.adjacent]: '-|-',
[Op.strictLeft]: '<<',
[Op.strictRight]: '>>',
[Op.noExtendRight]: '&<',
[Op.noExtendLeft]: '&>',
[Op.any]: 'ANY',
[Op.all]: 'ALL',
[Op.and]: ' AND ',
[Op.or]: ' OR ',
[Op.col]: 'COL',
[Op.placeholder]: '$$PLACEHOLDER$$',
[Op.raw]: 'DEPRECATED' //kept here since we still throw an explicit error if operator being used remove by v5,
},
OperatorMap: {
$eq: '=',
$ne: '!=',
$gte: '>=',
$gt: '>',
$lte: '<=',
$lt: '<',
$not: 'IS NOT',
$is: 'IS',
$like: 'LIKE',
$notLike: 'NOT LIKE',
$iLike: 'ILIKE',
$notILike: 'NOT ILIKE',
$regexp: '~',
$notRegexp: '!~',
$iRegexp: '~*',
$notIRegexp: '!~*',
$between: 'BETWEEN',
$notBetween: 'NOT BETWEEN',
$overlap: '&&',
$contains: '@>',
$contained: '<@',
$adjacent: '-|-',
$strictLeft: '<<',
$strictRight: '>>',
$noExtendRight: '&<',
$noExtendLeft: '&>',
$in : 'IN',
$notIn: 'NOT IN',
$any: 'ANY',
$all: 'ALL',
$and: ' AND ',
$or: ' OR ',
$col: 'COL',
$raw: 'DEPRECATED' //kept here since we still throw an explicit error if operator being used
OperatorsAliasMap: {},
setOperatorsAliases(aliases) {
if (!aliases || _.isEmpty(aliases)) {
this.OperatorsAliasMap = false;
} else {
this.OperatorsAliasMap = _.assign({}, aliases);
}
},
whereItemQuery(key, value, options) {
......@@ -2025,11 +2010,11 @@ const QueryGenerator = {
const isPlainObject = _.isPlainObject(value);
const isArray = !isPlainObject && Array.isArray(value);
key = this.OperatorsAliasMap[key] || key;
key = this.OperatorsAliasMap && this.OperatorsAliasMap[key] || key;
if (isPlainObject) {
this._replaceAliases(value);
value = this._replaceAliases(value);
}
const valueKeys = isPlainObject && _.keys(value);
const valueKeys = isPlainObject && Utils.getComplexKeys(value);
if (key === undefined) {
if (typeof value === 'string') {
......@@ -2042,36 +2027,37 @@ const QueryGenerator = {
}
if (!value) {
return this._joinKeyValue(key, this.escape(value, field), value === null ? this.OperatorMap.$is : this.OperatorMap.$eq, options.prefix);
return this._joinKeyValue(key, this.escape(value, field), value === null ? this.OperatorMap[Op.is] : this.OperatorMap[Op.eq], options.prefix);
}
if (value instanceof Utils.SequelizeMethod && !(key !== undefined && value instanceof Utils.Fn)) {
return this.handleSequelizeMethod(value);
}
// Convert where: [] to $and if possible, else treat as literal/replacements
// Convert where: [] to Op.and if possible, else treat as literal/replacements
if (key === undefined && isArray) {
if (Utils.canTreatArrayAsAnd(value)) {
key = '$and';
key = Op.and;
} else {
throw new Error('Support for literal replacements in the `where` object has been removed.');
}
}
if (key === '$or' || key === '$and' || key === '$not') {
if (key === Op.or || key === Op.and || key === Op.not) {
return this._whereGroupBind(key, value, options);
}
if (value.$or) {
return this._whereBind(this.OperatorMap.$or, key, value.$or, options);
if (value[Op.or]) {
return this._whereBind(this.OperatorMap[Op.or], key, value[Op.or], options);
}
if (value.$and) {
return this._whereBind(this.OperatorMap.$and, key, value.$and, options);
if (value[Op.and]) {
return this._whereBind(this.OperatorMap[Op.and], key, value[Op.and], options);
}
if (isArray && fieldType instanceof DataTypes.ARRAY) {
return this._joinKeyValue(key, this.escape(value, field), this.OperatorMap.$eq, options.prefix);
return this._joinKeyValue(key, this.escape(value, field), this.OperatorMap[Op.eq], options.prefix);
}
if (isPlainObject && fieldType instanceof DataTypes.JSON && options.json !== false) {
......@@ -2079,21 +2065,25 @@ const QueryGenerator = {
}
// If multiple keys we combine the different logic conditions
if (isPlainObject && valueKeys.length > 1) {
return this._whereBind(this.OperatorMap.$and, key, value, options);
return this._whereBind(this.OperatorMap[Op.and], key, value, options);
}
if (isArray) {
return this._whereParseSingleValueObject(key, field, '$in', value, options);
return this._whereParseSingleValueObject(key, field, Op.in, value, options);
}
if (isPlainObject && this.OperatorMap[valueKeys[0]]) {
if (isPlainObject) {
if (this.OperatorMap[valueKeys[0]]) {
return this._whereParseSingleValueObject(key, field, valueKeys[0], value[valueKeys[0]], options);
} else {
return this._whereParseSingleValueObject(key, field, this.OperatorMap.$eq, value, options);
return this._whereParseSingleValueObject(key, field, this.OperatorMap[Op.eq], value, options);
}
}
return this._joinKeyValue(key, this.escape(value, field), this.OperatorMap.$eq, options.prefix);
if (key === Op.placeholder) {
return this._joinKeyValue(this.OperatorMap[key], this.escape(value, field), this.OperatorMap[Op.eq], options.prefix);
}
return this._joinKeyValue(key, this.escape(value, field), this.OperatorMap[Op.eq], options.prefix);
},
_findField(key, options) {
......@@ -2110,24 +2100,40 @@ const QueryGenerator = {
}
},
_replaceAliases(obj) {
_.forOwn(obj, (item, prop) => {
if (this.OperatorsAliasMap[prop]) {
obj[this.OperatorsAliasMap[prop]] = item;
delete obj[prop];
_replaceAliases(orig) {
const obj = {};
if (!this.OperatorsAliasMap) {
return orig;
}
Utils.getOperators(orig).forEach(op => {
const item = orig[op];
if (_.isPlainObject(item)) {
obj[op] = this._replaceAliases(item);
} else {
obj[op] = item;
}
});
_.forOwn(orig, (item, prop) => {
prop = this.OperatorsAliasMap[prop] || prop;
if (_.isPlainObject(item)) {
item = this._replaceAliases(item);
}
obj[prop] = item;
});
return obj;
},
// OR/AND/NOT grouping logic
_whereGroupBind(key, value, options) {
const binding = key === '$or' ? this.OperatorMap.$or : this.OperatorMap.$and;
const outerBinding = key === '$not' ? 'NOT ': '';
const binding = key === Op.or ? this.OperatorMap[Op.or] : this.OperatorMap[Op.and];
const outerBinding = key === Op.not ? 'NOT ': '';
if (Array.isArray(value)) {
value = value.map(item => {
let itemQuery = this.whereItemsQuery(item, options, this.OperatorMap.$and);
if (itemQuery && itemQuery.length && (Array.isArray(item) || _.isPlainObject(item)) && _.size(item) > 1) {
let itemQuery = this.whereItemsQuery(item, options, this.OperatorMap[Op.and]);
if (itemQuery && itemQuery.length && (Array.isArray(item) || _.isPlainObject(item)) && Utils.getComplexSize(item) > 1) {
itemQuery = '('+itemQuery+')';
}
return itemQuery;
......@@ -2139,7 +2145,7 @@ const QueryGenerator = {
}
// Op.or: [] should return no data.
// Op.not of no restriction should also return no data
if ((key === '$or' || key === '$not') && !value) {
if ((key === Op.or || key === Op.not) && !value) {
return '0 = 1';
}
......@@ -2148,7 +2154,10 @@ const QueryGenerator = {
_whereBind(binding, key, value, options) {
if (_.isPlainObject(value)) {
value = _.map(value, (item, prop) => this.whereItemQuery(key, {[prop] : item}, options));
value = Utils.getComplexKeys(value).map(prop => {
const item = value[prop];
return this.whereItemQuery(key, {[prop] : item}, options);
});
} else {
value = value.map(item => this.whereItemQuery(key, item, options));
}
......@@ -2168,20 +2177,22 @@ const QueryGenerator = {
baseKey = `${this.quoteTable(options.prefix)}.${baseKey}`;
}
}
_.forOwn(value, (item, prop) => {
if (prop.indexOf('$') === 0) {
Utils.getOperators(value).forEach(op => {
const where = {};
where[prop] = value[prop];
where[op] = value[op];
items.push(this.whereItemQuery(key, where, _.assign({}, options, {json: false})));
return;
}
});
_.forOwn(value, (item, prop) => {
this._traverseJSON(items, baseKey, prop, item, [prop]);
});
const result = items.join(this.OperatorMap.$and);
const result = items.join(this.OperatorMap[Op.and]);
return items.length > 1 ? '('+result+')' : result;
},
_traverseJSON(items, baseKey, prop, item, path) {
let cast;
......@@ -2194,18 +2205,18 @@ const QueryGenerator = {
const pathKey = this.jsonPathExtractionQuery(baseKey, path);
if (_.isPlainObject(item)) {
Utils.getOperators(item).forEach(op => {
const value = item[op];
items.push(this.whereItemQuery(this._castKey(pathKey, value, cast), {[op]: value}));
});
_.forOwn(item, (value, itemProp) => {
if (itemProp.indexOf('$') === 0) {
items.push(this.whereItemQuery(this._castKey(pathKey, value, cast), {[itemProp]: value}));
return;
}
this._traverseJSON(items, baseKey, itemProp, value, path.concat([itemProp]));
});
return;
}
items.push(this.whereItemQuery(this._castKey(pathKey, item, cast), {$eq: item}));
items.push(this.whereItemQuery(this._castKey(pathKey, item, cast), {[Op.eq]: item}));
},
_castKey(key, value, cast) {
......@@ -2276,20 +2287,20 @@ const QueryGenerator = {
return key;
},
_whereParseSingleValueObject(key, field, prop, value, options) {
if (prop === '$not') {
_whereParseSingleValueObject (key, field, prop, value, options) {
if (prop === Op.not) {
if (Array.isArray(value)) {
prop = '$notIn';
prop = Op.notIn;
} else if ([null, true, false].indexOf(value) < 0) {
prop = '$ne';
prop = Op.ne;
}
}
let comparator = this.OperatorMap[prop] || this.OperatorMap.$eq;
let comparator = this.OperatorMap[prop] || this.OperatorMap[Op.eq];
switch (prop) {
case '$in':
case '$notIn':
case Op.in:
case Op.notIn:
if (value instanceof Utils.Literal) {
return this._joinKeyValue(key, value.val, comparator, options.prefix);
}
......@@ -2298,26 +2309,26 @@ const QueryGenerator = {
return this._joinKeyValue(key, `(${value.map(item => this.escape(item)).join(', ')})`, comparator, options.prefix);
}
if (comparator === this.OperatorMap.$in) {
if (comparator === this.OperatorMap[Op.in]) {
return this._joinKeyValue(key, '(NULL)', comparator, options.prefix);
}
return '';
case '$any':
case '$all':
comparator = `${this.OperatorMap.$eq} ${comparator}`;
if (value.$values) {
return this._joinKeyValue(key, `(VALUES ${value.$values.map(item => `(${this.escape(item)})`).join(', ')})`, comparator, options.prefix);
case Op.any:
case Op.all:
comparator = `${this.OperatorMap[Op.eq]} ${comparator}`;
if (value[Op.values]) {
return this._joinKeyValue(key, `(VALUES ${value[Op.values].map(item => `(${this.escape(item)})`).join(', ')})`, comparator, options.prefix);
}
return this._joinKeyValue(key, `(${this.escape(value, field)})`, comparator, options.prefix);
case '$between':
case '$notBetween':
case Op.between:
case Op.notBetween:
return this._joinKeyValue(key, `${this.escape(value[0])} AND ${this.escape(value[1])}`, comparator, options.prefix);
case '$raw':
case Op.raw:
throw new Error('The `$raw` where property is no longer supported. Use `sequelize.literal` instead.');
case '$col':
comparator = this.OperatorMap.$eq;
case Op.col:
comparator = this.OperatorMap[Op.eq];
value = value.split('.');
if (value.length > 2) {
......@@ -2332,31 +2343,31 @@ const QueryGenerator = {
}
const escapeOptions = {
acceptStrings: comparator.indexOf(this.OperatorMap.$like) !== -1
acceptStrings: comparator.indexOf(this.OperatorMap[Op.like]) !== -1
};
if (_.isPlainObject(value)) {
if (value.$col) {
if (value[Op.col]) {
return this._joinKeyValue(key, this.whereItemQuery(null, value), comparator, options.prefix);
}
if (value.$any) {
if (value[Op.any]) {
escapeOptions.isList = true;
return this._joinKeyValue(key, `(${this.escape(value.$any, field, escapeOptions)})`, `${comparator} ${this.OperatorMap.$any}`, options.prefix);
return this._joinKeyValue(key, `(${this.escape(value[Op.any], field, escapeOptions)})`, `${comparator} ${this.OperatorMap[Op.any]}`, options.prefix);
}
if (value.$all) {
if (value[Op.all]) {
escapeOptions.isList = true;
return this._joinKeyValue(key, `(${this.escape(value.$all, field, escapeOptions)})`, `${comparator} ${this.OperatorMap.$all}`, options.prefix);
return this._joinKeyValue(key, `(${this.escape(value[Op.all], field, escapeOptions)})`, `${comparator} ${this.OperatorMap[Op.all]}`, options.prefix);
}
}
if (comparator.indexOf(this.OperatorMap.$regexp) !== -1) {
if (comparator.indexOf(this.OperatorMap[Op.regexp]) !== -1) {
return this._joinKeyValue(key, `'${value}'`, comparator, options.prefix);
}
if (value === null && comparator === this.OperatorMap.$eq) {
return this._joinKeyValue(key, this.escape(value, field, escapeOptions), this.OperatorMap.$is, options.prefix);
} else if (value === null && this.OperatorMap.$ne) {
return this._joinKeyValue(key, this.escape(value, field, escapeOptions), this.OperatorMap.$not, options.prefix);
if (value === null && comparator === this.OperatorMap[Op.eq]) {
return this._joinKeyValue(key, this.escape(value, field, escapeOptions), this.OperatorMap[Op.is], options.prefix);
} else if (value === null && this.OperatorMap[Op.ne]) {
return this._joinKeyValue(key, this.escape(value, field, escapeOptions), this.OperatorMap[Op.not], options.prefix);
}
return this._joinKeyValue(key, this.escape(value, field, escapeOptions), comparator, options.prefix);
......@@ -2416,7 +2427,7 @@ const QueryGenerator = {
} else if (Array.isArray(smth)) {
if (smth.length === 0 || smth.length > 0 && smth[0].length === 0) return '1=1';
if (Utils.canTreatArrayAsAnd(smth)) {
const _smth = { $and: smth };
const _smth = { [Op.and]: smth };
result = this.getWhereConditions(_smth, tableName, factory, options, prepend);
} else {
throw new Error('Support for literal replacements in the `where` object has been removed.');
......
......@@ -7,6 +7,8 @@ const AbstractQueryGenerator = require('../abstract/query-generator');
const randomBytes = require('crypto').randomBytes;
const semver = require('semver');
const Op = require('../../operators');
/* istanbul ignore next */
const throwMethodUndefined = function(methodName) {
throw new Error('The method "' + methodName + '" is not defined! Please add it to your sql dialect.');
......@@ -377,7 +379,7 @@ const QueryGenerator = {
});
//Filter NULL Clauses
const clauses = where.$or.filter(clause => {
const clauses = where[Op.or].filter(clause => {
let valid = true;
/*
* Exclude NULL Composite PK/UK. Partial Composite clauses should also be excluded as it doesn't guarantee a single row
......
......@@ -3,14 +3,15 @@
const _ = require('lodash');
const Utils = require('../../utils');
const AbstractQueryGenerator = require('../abstract/query-generator');
const Op = require('../../operators');
const QueryGenerator = {
__proto__: AbstractQueryGenerator,
dialect: 'mysql',
OperatorMap: Object.assign({}, AbstractQueryGenerator.OperatorMap, {
$regexp: 'REGEXP',
$notRegexp: 'NOT REGEXP'
[Op.regexp]: 'REGEXP',
[Op.notRegexp]: 'NOT REGEXP'
}),
createSchema() {
......
......@@ -170,16 +170,16 @@ const QueryGenerator = {
const pathKey = this.jsonPathExtractionQuery(baseKey, path);
if (_.isPlainObject(item)) {
_.forOwn(item, (value, itemProp) => {
if (itemProp.indexOf('$') === 0) {
Utils.getOperators(item).forEach(op => {
let value = item[op];
if (value instanceof Date) {
value = value.toISOString();
} else if (Array.isArray(value) && value[0] instanceof Date) {
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]));
});
......
......@@ -17,6 +17,8 @@ const Hooks = require('./hooks');
const associationsMixin = require('./associations/mixin');
const defaultsOptions = { raw: true };
const assert = require('assert');
const Op = require('./operators');
/**
* A Model represents a table in the database. Instances of this class represent a database row.
......@@ -77,14 +79,14 @@ class Model {
const deletedAtObject = {};
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;
if (_.isEmpty(options.where)) {
options.where = deletedAtObject;
} else {
options.where = { $and: [deletedAtObject, options.where] };
options.where = { [Op.and]: [deletedAtObject, options.where] };
}
return options;
......@@ -508,7 +510,7 @@ class Model {
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);
......@@ -539,7 +541,7 @@ class Model {
}
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) {
......@@ -1287,10 +1289,10 @@ class Model {
* return {
* where: {
* email: {
* $like: email
* [Op.like]: email
* },
* accesss_level {
* $gte: accessLevel
* [Op.gte]: accessLevel
* }
* }
* }
......@@ -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:
* ```js
* 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:
......@@ -1398,20 +1400,20 @@ class Model {
*
* __Using greater than, less than etc.__
* ```js
*
* const {gt, lte, ne, in: opIn} = Sequelize.Op;
* Model.findAll({
* where: {
* attr1: {
* gt: 50
* [gt]: 50
* },
* attr2: {
* lte: 45
* [lte]: 45
* },
* attr3: {
* in: [1,2,3]
* [opIn]: [1,2,3]
* },
* attr4: {
* ne: 5
* [ne]: 5
* }
* }
* })
......@@ -1419,19 +1421,20 @@ class Model {
* ```sql
* 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__
* ```js
* const {or, and, gt, lt} = Sequelize.Op;
* Model.findAll({
* where: {
* name: 'a project',
* $or: [
* [or]: [
* {id: [1, 2, 3]},
* {
* $and: [
* {id: {gt: 10}},
* {id: {lt: 100}}
* [and]: [
* {id: {[gt]: 10}},
* {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');
const Transaction = require('./transaction');
const Promise = require('./promise');
const QueryTypes = require('./query-types');
const Op = require('./operators');
/**
* The interface that Sequelize uses to talk to all databases
......@@ -880,7 +881,7 @@ class QueryInterface {
}
}
where = { $or: wheres };
where = { [Op.or]: wheres };
options.type = QueryTypes.UPSERT;
options.raw = true;
......
......@@ -18,6 +18,7 @@ const Hooks = require('./hooks');
const Association = require('./associations/index');
const Validator = require('./utils/validator-extras').validator;
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:
......@@ -94,6 +95,8 @@ class Sequelize {
* @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 {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) {
let config;
......@@ -164,7 +167,8 @@ class Sequelize {
isolationLevel: null,
databaseVersion: 0,
typeValidation: false,
benchmark: false
benchmark: false,
operatorsAliases: true
}, options || {});
if (!this.options.dialect) {
......@@ -228,6 +232,12 @@ class Sequelize {
this.dialect = new Dialect(this);
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);
......@@ -847,7 +857,7 @@ class Sequelize {
* @return {Sequelize.and}
*/
static and() {
return { $and: Utils.sliceArgs(arguments) };
return { [Op.and]: Utils.sliceArgs(arguments) };
}
/**
......@@ -861,7 +871,7 @@ class Sequelize {
* @return {Sequelize.or}
*/
static or() {
return { $or: Utils.sliceArgs(arguments) };
return { [Op.or]: Utils.sliceArgs(arguments) };
}
/**
......@@ -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 {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
* @since v2.0.0-dev3
*/
......@@ -1155,6 +1165,13 @@ Sequelize.prototype.Promise = Sequelize.Promise = Promise;
*/
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.
* @see https://github.com/chriso/validator.js
......
......@@ -7,6 +7,8 @@ const parameterValidator = require('./utils/parameter-validator');
const Logger = require('./utils/logger');
const uuid = require('uuid');
const Promise = require('./promise');
const operators = require('./operators');
const operatorsArray = _.values(operators);
const primitives = ['string', 'number', 'boolean'];
let inflection = require('inflection');
......@@ -187,12 +189,9 @@ function mapOptionFieldNames(options, Model) {
exports.mapOptionFieldNames = mapOptionFieldNames;
function mapWhereFieldNames(attributes, Model) {
let attribute;
let rawAttribute;
if (attributes) {
for (attribute in attributes) {
rawAttribute = Model.rawAttributes[attribute];
getComplexKeys(attributes).forEach(attribute => {
const rawAttribute = Model.rawAttributes[attribute];
if (rawAttribute && rawAttribute.field !== rawAttribute.fieldName) {
attributes[rawAttribute.field] = attributes[attribute];
......@@ -217,7 +216,8 @@ function mapWhereFieldNames(attributes, Model) {
return where;
});
}
}
});
}
return attributes;
......@@ -564,3 +564,38 @@ exports.mapIsolationLevelStringToTedious = (isolationLevel, tedious) => {
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'),
config = require(__dirname + '/../config/config'),
moment = require('moment'),
Transaction = require(__dirname + '/../../lib/transaction'),
Utils = require(__dirname + '/../../lib/utils'),
sinon = require('sinon'),
semver = require('semver'),
current = Support.sequelize;
......@@ -28,6 +29,10 @@ const qq = function(str) {
describe(Support.getTestDialectTeaser('Sequelize'), () => {
describe('constructor', () => {
afterEach(() => {
Utils.deprecate.restore && Utils.deprecate.restore();
});
if (dialect !== 'sqlite') {
it.skip('should work with min connections', () => {
const ConnectionManager = current.dialect.connectionManager,
......@@ -55,6 +60,27 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => {
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') {
it('should work with connection strings (1)', () => {
const sequelize = new Sequelize('sqlite://test.sqlite'); // eslint-disable-line
......
......@@ -8,7 +8,9 @@ const fs = require('fs'),
Config = require(__dirname + '/config/config'),
supportShim = require(__dirname + '/supportShim'),
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-datetime'));
......@@ -153,6 +155,14 @@ const Support = {
}
},
getAbstractQueryGenerator(sequelize) {
return Object.assign(
{},
AbstractQueryGenerator,
{options: sequelize.options, _dialect: sequelize.dialect, sequelize, quoteIdentifier(identifier) { return identifier; }}
);
},
getTestDialect() {
let envDialect = process.env.DIALECT || 'mysql';
......
......@@ -8,6 +8,7 @@ const chai = require('chai'),
Support = require(__dirname + '/../support'),
DataTypes = require(__dirname + '/../../../lib/data-types'),
HasMany = require(__dirname + '/../../../lib/associations/has-many'),
Op = require(__dirname + '/../../../lib/operators'),
current = Support.sequelize,
Promise = current.Promise;
......@@ -149,7 +150,8 @@ describe(Support.getTestDialectTeaser('hasMany'), () => {
});
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({
'user_id': idA
}),
......@@ -162,8 +164,7 @@ describe(Support.getTestDialectTeaser('hasMany'), () => {
Task.build({
'user_id': idB
})
])),
where = {};
]));
User.Tasks = User.hasMany(Task, {foreignKey});
const actual = User.Tasks.get([
......@@ -172,12 +173,10 @@ describe(Support.getTestDialectTeaser('hasMany'), () => {
User.build({id: idC})
]);
where[foreignKey] = {
$in: [idA, idB, idC]
};
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 => {
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 @@
const Support = require(__dirname + '/../../support');
const expectsql = Support.expectsql;
const current = Support.sequelize;
const Operators = require('../../../../lib/operators');
const QueryGenerator = require('../../../../lib/dialects/mssql/query-generator');
const _ = require('lodash');
......@@ -10,6 +11,8 @@ if (current.dialect.name === 'mssql') {
suite('[MSSQL Specific] QueryGenerator', () => {
// Dialect would normally be set by the query interface that instantiates the query-generator, but here we specify it explicitly
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', () => {
expectsql(QueryGenerator.getDefaultConstraintQuery({tableName: 'myTable', schema: 'mySchema'}, 'myColumn'), {
......
......@@ -5,6 +5,7 @@ const chai = require('chai'),
Support = require(__dirname + '/../../support'),
dialect = Support.getTestDialect(),
_ = require('lodash'),
Operators = require('../../../../lib/operators'),
QueryGenerator = require('../../../../lib/dialects/mysql/query-generator');
if (dialect === 'mysql') {
......@@ -577,6 +578,7 @@ if (dialect === 'mysql') {
QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' });
QueryGenerator._dialect = this.sequelize.dialect;
QueryGenerator.sequelize = this.sequelize;
QueryGenerator.setOperatorsAliases(Operators.LegacyAliases);
const conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments);
expect(conditions).to.deep.equal(test.expectation);
});
......
......@@ -2,6 +2,7 @@
const chai = require('chai'),
expect = chai.expect,
Operators = require('../../../../lib/operators'),
QueryGenerator = require('../../../../lib/dialects/postgres/query-generator'),
Support = require(__dirname + '/../../support'),
dialect = Support.getTestDialect(),
......@@ -947,6 +948,7 @@ if (dialect.match(/^postgres/)) {
QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' });
QueryGenerator._dialect = this.sequelize.dialect;
QueryGenerator.sequelize = this.sequelize;
QueryGenerator.setOperatorsAliases(Operators.LegacyAliases);
const conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments);
expect(conditions).to.deep.equal(test.expectation);
});
......
......@@ -7,6 +7,7 @@ const chai = require('chai'),
dialect = Support.getTestDialect(),
_ = require('lodash'),
moment = require('moment'),
Operators = require('../../../../lib/operators'),
QueryGenerator = require('../../../../lib/dialects/sqlite/query-generator');
if (dialect === 'sqlite') {
......@@ -550,6 +551,7 @@ if (dialect === 'sqlite') {
QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' });
QueryGenerator._dialect = this.sequelize.dialect;
QueryGenerator.sequelize = this.sequelize;
QueryGenerator.setOperatorsAliases(Operators.LegacyAliases);
const conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments);
expect(conditions).to.deep.equal(test.expectation);
});
......
......@@ -267,13 +267,13 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
suite('$and', () => {
testsql('$and', {
shared: 1,
$or: {
group_id: 1,
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', [
......@@ -320,13 +320,13 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
suite('$not', () => {
testsql('$not', {
shared: 1,
$or: {
group_id: 1,
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', [], {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!