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

Commit fe5a85b0 by Jan Aagaard Meier

refactor(scopes): Simplified implementation, added more tests, 💄 remove…

…d cruft from query generator
1 parent f970157b
......@@ -17,6 +17,7 @@
#### Backwards compatibility changes
- Events support have been removed so using `.on('succes')` or `.succes()` is no longer supported.
- Trying to apply a scope that does not exist will always throw an error
# 2.0.6
- [BUG] Don't update virtual attributes in Model.update. Fixes [#2860](https://github.com/sequelize/sequelize/issues/2860)
......
......@@ -351,7 +351,16 @@ module.exports = (function() {
});
}
return association.target.findAll(options, queryOptions);
var model = association.target;
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
}
}
return model.findAll(options, queryOptions);
};
obj[this.accessors.hasAll] = function(instances, options) {
......@@ -359,21 +368,24 @@ module.exports = (function() {
, where;
options = options || {};
options.scope = false;
instances.forEach(function(instance) {
if (instance instanceof association.target.Instance) {
where = new Utils.or([where, instance.primaryKeyValues]);
where = { $or: [where, instance.primaryKeyValues]};
} else {
var _where = {};
_where[association.target.primaryKeyAttribute] = instance;
where = new Utils.or([where, _where]);
where = { $or: [where, _where]};
}
});
options.where = new Utils.and([
where,
options.where
]);
options.where = {
$and: [
where,
options.where
]
};
return instance[association.accessors.get](
options,
......@@ -388,6 +400,7 @@ module.exports = (function() {
, where;
options = options || {};
options.scope = false;
if (param instanceof association.target.Instance) {
where = param.primaryKeyValues;
......@@ -396,10 +409,12 @@ module.exports = (function() {
where[association.target.primaryKeyAttribute] = param;
}
options.where = new Utils.and([
where,
options.where
]);
options.where = {
$and: [
where,
options.where
]
};
return instance[association.accessors.get](
options,
......@@ -419,7 +434,9 @@ module.exports = (function() {
options = options || {};
var instance = this;
return instance[association.accessors.get]({}, {
return instance[association.accessors.get]({
scope: false
}, {
transaction: options.transaction,
logging: options.logging
}).then(function(oldAssociatedObjects) {
......@@ -668,6 +685,7 @@ module.exports = (function() {
}
return instance[association.accessors.get]({
scope: false,
where: newInstance.primaryKeyValues
}, {
transaction: (additionalAttributes || {}).transaction,
......
......@@ -97,20 +97,31 @@ module.exports = (function() {
BelongsTo.prototype.injectGetter = function(instancePrototype) {
var association = this;
instancePrototype[this.accessors.get] = function(params) {
instancePrototype[this.accessors.get] = function(options) {
var where = {};
where[association.targetIdentifier] = this.get(association.identifier);
params = association.target.__optClone(params) || {};
params.where = (params.where && [params.where]) || [];
options = association.target.__optClone(options) || {};
where[association.targetIdentifier] = this.get(association.identifier);
params.where.push(where);
options.where = {
$and: [
options.where,
where
]
};
params.where = new Utils.and(params.where);
if (options.limit === undefined) options.limit = null;
if (params.limit === undefined) params.limit = null;
var model = association.target;
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
}
}
return association.target.find(params);
return model.find(options);
};
return this;
......
......@@ -18,16 +18,27 @@ module.exports = (function() {
}.bind(this));
}
options.where = new Utils.and([
new Utils.where(
this.target.rawAttributes[this.association.identifier],
this.instance[this.source.primaryKeyAttribute]
),
scopeWhere,
options.where
]);
return this.association.target.all(options, queryOptions);
options.where = {
$and: [
new Utils.where(
this.target.rawAttributes[this.association.identifier],
this.instance[this.source.primaryKeyAttribute]
),
scopeWhere,
options.where
]
};
var model = this.association.target;
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
}
}
return model.all(options, queryOptions);
};
HasManySingleLinked.prototype.injectSetter = function(oldAssociations, newAssociations, defaultAttributes) {
......@@ -100,7 +111,7 @@ module.exports = (function() {
updateWhere[primaryKey] = unassociatedIds;
promises.push(this.association.target.update(
promises.push(this.association.target.unscoped().update(
update,
Utils._.extend(options, {
allowNull: [self.association.identifier],
......
......@@ -334,6 +334,7 @@ module.exports = (function() {
obj[this.accessors.get] = function(options, queryOptions) {
options = association.target.__optClone(options) || {};
queryOptions = queryOptions || {};
var Class = Object(association.through.model) === association.through.model ? HasManyDoubleLinked : HasManySingleLinked;
return new Class(association, this).injectGetter(options, queryOptions);
......@@ -344,21 +345,24 @@ module.exports = (function() {
, where;
options = options || {};
options.scope = false;
instances.forEach(function(instance) {
if (instance instanceof association.target.Instance) {
where = new Utils.or([where, instance.primaryKeyValues]);
where = { $or: [where, instance.primaryKeyValues]};
} else {
var _where = {};
_where[association.target.primaryKeyAttribute] = instance;
where = new Utils.or([where, _where]);
where = { $or: [where, _where]};
}
});
options.where = new Utils.and([
where,
options.where
]);
options.where = {
$and: [
where,
options.where
]
};
return instance[association.accessors.get](
options,
......@@ -373,6 +377,7 @@ module.exports = (function() {
, where;
options = options || {};
options.scope = false;
if (param instanceof association.target.Instance) {
where = param.primaryKeyValues;
......@@ -381,10 +386,12 @@ module.exports = (function() {
where[association.target.primaryKeyAttribute] = param;
}
options.where = new Utils.and([
where,
options.where
]);
options.where = {
$and: [
where,
options.where
]
};
return instance[association.accessors.get](
options,
......@@ -420,7 +427,9 @@ module.exports = (function() {
var instance = this;
return instance[association.accessors.get]({}, {
return instance[association.accessors.get]({
scope: false
}, {
transaction: (additionalAttributes || {}).transaction,
logging: (additionalAttributes || {}).logging
}).then(function(oldAssociatedObjects) {
......@@ -467,7 +476,8 @@ module.exports = (function() {
}
return instance[association.accessors.get]({
where: newInstance.primaryKeyValues
where: newInstance.primaryKeyValues,
scope: false
}, {
transaction: (additionalAttributes || {}).transaction
}).then(function(currentAssociatedObjects) {
......@@ -483,7 +493,9 @@ module.exports = (function() {
obj[this.accessors.remove] = function(oldAssociatedObject, options) {
var instance = this;
return instance[association.accessors.get]({}, options).then(function(currentAssociatedObjects) {
return instance[association.accessors.get]({
scope: false
}, options).then(function(currentAssociatedObjects) {
var newAssociations = [];
if (!(oldAssociatedObject instanceof association.target.Instance)) {
......@@ -506,7 +518,9 @@ module.exports = (function() {
obj[this.accessors.removeMultiple] = function(oldAssociatedObjects, options) {
var instance = this;
return instance[association.accessors.get]({}, options).then(function(currentAssociatedObjects) {
return instance[association.accessors.get]({
scope: false
}, options).then(function(currentAssociatedObjects) {
var newAssociations = [];
// Ensure the oldAssociatedObjects array is an array of target instances
......
......@@ -96,20 +96,31 @@ module.exports = (function() {
HasOne.prototype.injectGetter = function(instancePrototype) {
var association = this;
instancePrototype[this.accessors.get] = function(params) {
instancePrototype[this.accessors.get] = function(options) {
var where = {};
where[association.identifier] = this.get(association.sourceIdentifier);
params = association.target.__optClone(params) || {};
params.where = (params.where && [params.where]) || [];
options = association.target.__optClone(options) || {};
where[association.identifier] = this.get(association.sourceIdentifier);
params.where.push(where);
options.where = {
$and: [
options.where,
where
]
};
params.where = new Utils.and(params.where);
if (options.limit === undefined) options.limit = null;
if (params.limit === undefined) params.limit = null;
var model = association.target;
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
}
}
return association.target.find(params);
return model.find(options);
};
return this;
......@@ -121,6 +132,8 @@ module.exports = (function() {
instancePrototype[this.accessors.set] = function(associatedInstance, options) {
var instance = this;
options = options || {};
options.scope = false;
return instance[association.accessors.get](options).then(function(oldInstance) {
if (oldInstance) {
oldInstance[association.identifier] = null;
......
......@@ -366,7 +366,7 @@ Mixin.getAssociation = function(target, alias) {
if (this.associations.hasOwnProperty(associationName)) {
var association = this.associations[associationName];
if (association.target === target && (alias === undefined ? !association.isAliased : association.as === alias)) {
if (association.target.name === target.name && (alias === undefined ? !association.isAliased : association.as === alias)) {
return association;
}
}
......
......@@ -1223,7 +1223,7 @@ module.exports = (function() {
} else if (Utils._.isPlainObject(options.where)) {
options.where['__' + as] = subQueryWhere;
} else {
options.where = new Utils.and(options.where, subQueryWhere);
options.where = { $and: [options.where, subQueryWhere] };
}
}
......@@ -1533,7 +1533,9 @@ module.exports = (function() {
result = (value === 'NULL') ? key + ' IS NULL' : [key, value].join(smth.comparator);
} else if (_.isPlainObject(value)) {
result = this.plainObjectToWhere(value, key, key, factory).join(' AND ');
result = this.whereItemQuery(smth.attribute, value, {
model: factory
});
} else {
if (typeof value === 'boolean') {
value = this.booleanValue(value);
......@@ -1726,7 +1728,6 @@ module.exports = (function() {
outerBinding = '';
if (key === '$not') outerBinding = 'NOT ';
if (Array.isArray(value)) {
value = value.map(function (item) {
var itemQuery = self.whereItemsQuery(item, options, ' AND ');
......@@ -2166,83 +2167,6 @@ module.exports = (function() {
return [_key, _value].join(' ' + logicResult + ' ');
},
/*
Takes a hash and transforms it into a mysql where condition: {key: value, key2: value2} ==> key=value AND key2=value2
The values are transformed by the relevant datatype.
*/
hashToWhereConditions: function(hash, dao, options) {
var result = [];
options = options || {};
// Closures are nice
Utils._.each(hash, function(value, key) {
var _key
, _value = null;
if (value && value._isSequelizeMethod === true && (value instanceof Utils.literal)) {
result.push(value.val);
return;
}
if (options.keysEscaped) {
_key = key;
} else {
if (this.isAssociationFilter(key, dao)) {
_key = key = this.getAssociationFilterColumn(key, dao, options);
} else {
_key = this.quoteIdentifiers(key);
}
}
if (Array.isArray(value)) {
result.push(this.arrayValue(value, key, _key, dao, 'IN'));
} else if (value && Utils._.isPlainObject(value)) {
result = result.concat(this.plainObjectToWhere(value, key, _key, dao));
} else {
if (typeof value === 'boolean') {
_value = this.booleanValue(value);
} else {
_value = this.escape(value);
}
result.push((_value === 'NULL') ? _key + ' IS NULL' : [_key, _value].join('='));
}
}.bind(this));
return result.join(' AND ');
},
plainObjectToWhere: function (value, key, _key, dao) {
var _value
, result = [];
if (!!value.join) {
//using as sentinel for join column => value
_value = this.quoteIdentifiers(value.join);
result.push([_key, _value].join('='));
} else {
for (var logic in value) {
var logicResult = Utils.getWhereLogic(logic, value[logic]);
if (logicResult === 'BETWEEN' || logicResult === 'NOT BETWEEN') {
_value = this.escape(value[logic][0]);
var _value2 = this.escape(value[logic][1]);
result.push(' (' + _key + ' ' + logicResult + ' ' + _value + ' AND ' + _value2 + ') ');
} else if (logicResult === 'IN' || logicResult === 'NOT IN' || Array.isArray(value[logic])) {
var values = Array.isArray(value[logic]) ? value[logic] : [value[logic]];
result.push(this.arrayValue(values, key, _key, dao, logicResult));
} else {
_value = this.escape(value[logic]);
result.push([_key, _value].join(' ' + logicResult + ' '));
}
}
}
return result;
},
booleanValue: function(value) {
return value;
}
......
......@@ -481,7 +481,7 @@ module.exports = (function() {
if (!resultMap[itemHash]) {
$parent = resultMap[parentHash];
$lastKeyPrefix = lastKeyPrefix(prevKey);
//console.log($parent, prevKey, $lastKeyPrefix);
if (includeMap[prevKey].association.isSingleAssociation) {
$parent[$lastKeyPrefix] = resultMap[itemHash] = values;
} else {
......@@ -561,7 +561,7 @@ module.exports = (function() {
if (!resultMap[itemHash]) {
$parent = resultMap[parentHash];
$lastKeyPrefix = lastKeyPrefix(prevKey);
//console.log($parent, prevKey, $lastKeyPrefix);
if (includeMap[prevKey].association.isSingleAssociation) {
$parent[$lastKeyPrefix] = resultMap[itemHash] = values;
} else {
......
......@@ -48,7 +48,6 @@ module.exports = (function() {
return hooks;
});
this.scopeObj = {};
this.sequelize = options.sequelize;
this.underscored = this.underscored || this.underscoredAll;
......@@ -192,10 +191,7 @@ module.exports = (function() {
return self.primaryKeyAttributes.indexOf(key) !== -1;
});
if (typeof this.options.defaultScope === 'object') {
Utils.injectScope.call(this, this.options.defaultScope);
}
this.__scope = _.isPlainObject(this.options.defaultScope) ? this.options.defaultScope : {};
// Instance prototype
this.Instance = this.DAO = function() {
......@@ -484,6 +480,13 @@ module.exports = (function() {
};
/**
* @return {Model}
*/
Model.prototype.unscoped = function () {
return this.scope();
};
/**
* Apply a scope created in `define` to the model. First let's look at how to create scopes:
* ```js
* var Model = sequelize.define('model', attributes, {
......@@ -524,89 +527,59 @@ module.exports = (function() {
*/
Model.prototype.scope = function(option) {
var self = Object.create(this)
, type
, options
, merge
, i
, scope
, scopeName
, scopeOptions
, argLength = arguments.length
, lastArg = arguments[argLength - 1];
self.scoped = true;
, scopeName;
// Set defaults
scopeOptions = typeof lastArg === 'object' && !Array.isArray(lastArg) && lastArg || {};
scopeOptions.silent = (scopeOptions !== null && scopeOptions.hasOwnProperty('silent') ? scopeOptions.silent : true);
self.__scope = {};
// Clear out any predefined scopes...
self.scopeObj = {};
// Possible formats for option:
// String of arguments: 'hello', 'world', 'etc'
// Array: ['hello', 'world', 'etc']
// Object: {merge: 'hello'}, {method: ['scopeName' [, args1, args2..]]}, {merge: true, method: ...}
if (argLength < 1 || !option) {
if (!option) {
return self;
}
for (i = 0; i < argLength; i++) {
options = Array.isArray(arguments[i]) ? arguments[i] : [arguments[i]];
options.forEach(function(o) {
type = typeof o;
scope = null;
merge = false;
scopeName = null;
if (type === 'object') {
// Right now we only support a merge functionality for objects
if (!!o.merge) {
merge = true;
scopeName = o.merge[0];
if (Array.isArray(o.merge) && !!self.options.scopes[scopeName]) {
scope = self.options.scopes[scopeName].apply(self, o.merge.splice(1));
}
else if (typeof o.merge === 'string') {
scopeName = o.merge;
scope = self.options.scopes[scopeName];
}
}
options = _.flatten(arguments);
options.forEach(function(option) {
scope = null;
scopeName = null;
if (!!o.method) {
if (Array.isArray(o.method) && !!self.options.scopes[o.method[0]]) {
scopeName = o.method[0];
scope = self.options.scopes[scopeName].apply(self, o.method.splice(1));
merge = !!o.merge;
}
else if (!!self.options.scopes[o.method]) {
scopeName = o.method;
scope = self.options.scopes[scopeName].apply(self);
}
} else {
scopeName = o;
scope = self.options.scopes[scopeName];
if (_.isPlainObject(option)) {
if (!!option.method) {
if (Array.isArray(option.method) && !!self.options.scopes[option.method[0]]) {
scopeName = option.method[0];
scope = self.options.scopes[scopeName].apply(self, option.method.splice(1));
}
if (o.where) {
scope = o;
merge = true;
else if (!!self.options.scopes[option.method]) {
scopeName = option.method;
scope = self.options.scopes[scopeName].apply(self);
}
} else {
scopeName = o;
scope = self.options.scopes[scopeName];
scope = option;
}
} else {
if (option === 'defaultScope') {
scope = self.options.defaultScope;
} else {
scopeName = option;
scope = self.options.scopes[scopeName];
if (!!scope) {
Utils.injectScope.call(self, scope, merge);
}
else if (scopeOptions.silent !== true && !!scopeName) {
throw new Error('Invalid scope ' + scopeName + ' called.');
if (_.isFunction(scope)) {
scope = scope();
}
}
});
}
}
if (!!scope) {
_.assign(self.__scope, scope, function scopeCustomizer(a, b, key) {
if (key === 'where') {
return _.assign(a || {}, b);
}
return a ? a : b;
});
} else {
throw new Error('Invalid scope ' + scopeName + ' called.');
}
});
return self;
};
......@@ -704,14 +677,13 @@ module.exports = (function() {
tableNames[this.getTableName(options)] = true;
options = optClone(options || {});
options = Utils._.defaults(options, {
hooks: true
});
_.defaults(options, { hooks: true });
_.assign(options, queryOptions);
this.__injectScope(options);
return Promise.bind(this).then(function() {
conformOptions(options);
conformOptions(options, this);
if (options.hooks) {
return this.runHooks('beforeFind', options);
......@@ -796,6 +768,8 @@ module.exports = (function() {
options = optClone(param);
}
this.__injectScope(options);
if (options.limit === undefined && !(options.where && options.where[this.primaryKeyAttribute])) {
options.limit = 1;
}
......@@ -862,7 +836,7 @@ module.exports = (function() {
*/
Model.prototype.count = function(options) {
options = Utils._.clone(options || {});
conformOptions(options);
conformOptions(options, this);
var col = '*';
if (options.include) {
......@@ -908,7 +882,7 @@ module.exports = (function() {
// no limit, offset, order, attributes for the options given to count()
, countOptions = _.omit(_.clone(findOptions), ['offset', 'limit', 'order', 'attributes']);
conformOptions(countOptions);
conformOptions(countOptions, this);
if (countOptions.include) {
countOptions.include = _.cloneDeep(countOptions.include, function (element) {
if (element instanceof Model) return element;
......@@ -1015,7 +989,7 @@ module.exports = (function() {
}
if (!options.includeValidated) {
conformOptions(options);
conformOptions(options, this);
if (options.include) {
expandIncludeAll.call(this, options);
validateIncludedElements.call(this, options);
......@@ -1033,7 +1007,7 @@ module.exports = (function() {
}, options || {});
if (!options.includeValidated) {
conformOptions(options);
conformOptions(options, this);
if (options.include) {
expandIncludeAll.call(this, options);
validateIncludedElements.call(this, options);
......@@ -1482,6 +1456,7 @@ module.exports = (function() {
}, options || {});
options.type = QueryTypes.BULKDELETE;
this.__injectScope(options);
Utils.mapOptionFieldNames(options, this);
......@@ -1629,6 +1604,8 @@ module.exports = (function() {
options.type = QueryTypes.BULKUPDATE;
this.__injectScope(options);
// Remove values that are not in the options.fields
if (options.fields && options.fields instanceof Array) {
Object.keys(values).forEach(function(key) {
......@@ -1794,6 +1771,11 @@ module.exports = (function() {
}
};
Model.prototype.__injectScope = function (options) {
_.defaults(options, this.__scope);
_.defaults(options.where, this.__scope.where);
};
// private
// validateIncludedElements should have been called before this method
......@@ -1908,11 +1890,10 @@ module.exports = (function() {
}.bind(this));
};
var conformOptions = function(options) {
var conformOptions = function(options, self) {
if (!options.include) {
return;
}
// if include is not an array, wrap in an array
if (!Array.isArray(options.include)) {
options.include = [options.include];
......@@ -1923,20 +1904,41 @@ module.exports = (function() {
// convert all included elements to { model: Model } form
options.include = options.include.map(function(include) {
var model;
if (include instanceof Association) {
include = { association: include };
if (include.target.name === self.name) {
model = include.source;
} else {
model = include.target;
}
include = { model: model, association: include };
} else if (include instanceof Model) {
model = include;
include = { model: include };
} else if (typeof include !== 'object') {
throw new Error('Include unexpected. Element has to be either a Model, an Association or an object.');
} else {
// convert daoFactory to model (for backwards compatibility)
} else if (_.isPlainObject(include)) {
if (include.hasOwnProperty('daoFactory')) {
include.model = include.daoFactory;
delete include.daoFactory;
throw new Error('include.daoFactory is deprecated, please use include.model instead');
}
conformOptions(include);
if (include.association) {
if (include.association.target.name === self.name) {
model = include.association.source;
} else {
model = include.association.target;
}
} else {
model = include.model;
}
conformOptions(include, model);
} else {
throw new Error('Include unexpected. Element has to be either a Model, an Association or an object.');
}
if (!include.all) {
_.defaults(include, model.__scope);
}
return include;
......@@ -1985,7 +1987,7 @@ module.exports = (function() {
}
if (include.association && !include._pseudo && !include.model) {
if (include.association.source === this) {
if (include.association.source.name === this.name) {
include.model = include.association.target;
} else {
include.model = include.association.source;
......@@ -2034,7 +2036,7 @@ module.exports = (function() {
if (through.scope) {
include.through.where = include.through.where ? new Utils.and([include.through.where, through.scope]) : through.scope;
include.through.where = include.through.where ? { $and: [include.through.where, through.scope]} : through.scope;
}
include.include.push(include.through);
......@@ -2046,7 +2048,7 @@ module.exports = (function() {
}
if (include.association.scope) {
include.where = include.where ? new Utils.and([include.where, include.association.scope]) : include.association.scope;
include.where = include.where ? { $and: [include.where, include.association.scope] }: include.association.scope;
}
// Validate child includes
......@@ -2074,7 +2076,7 @@ module.exports = (function() {
return;
}
for (var index = 0; index < includes.length; index++) {
for (var index = 0; index < includes.length; index++) {
var include = includes[index];
if (include.all) {
......
......@@ -680,22 +680,6 @@ module.exports = (function() {
options.type = QueryTypes.SELECT;
// See if we need to merge options and model.scopeObj
// we're doing this on the QueryInterface level because it's a bridge between
// sequelize and the databases
if (model.options.defaultScope && Object.keys(model.options.defaultScope).length > 0) {
if (!!options) {
Utils.injectScope.call(model, options, true);
}
var scopeObj = buildScope.call(model);
Object.keys(scopeObj).forEach(function(method) {
if (typeof scopeObj[method] === 'number' || !Utils._.isEmpty(scopeObj[method])) {
options[method] = scopeObj[method];
}
});
}
options.instance = model;
return this.sequelize.query(
this.QueryGenerator.selectQuery(tableName, options, model),
......@@ -935,16 +919,5 @@ module.exports = (function() {
return this.sequelize.query(sql, options);
};
// private
var buildScope = function() {
var smart;
// Use smartWhere to convert several {where} objects into a single where object
smart = Utils.smartWhere(this.scopeObj.where || [], this.daoFactoryManager.sequelize.options.dialect);
smart = Utils.compileSmartWhere.call(this, smart, this.daoFactoryManager.sequelize.options.dialect);
return {limit: this.scopeObj.limit || null, offset: this.scopeObj.offset || null, where: smart, order: (this.scopeObj.order || []).join(', ')};
};
return QueryInterface;
})();
......@@ -568,7 +568,8 @@ module.exports = (function() {
delete options.modelName;
var factory = new Model(modelName, attributes, options);
this.modelManager.addDAO(factory.init(this.modelManager));
factory = factory.init(this.modelManager);
this.modelManager.addDAO(factory);
this.runHooks('afterDefine', factory);
......
......@@ -87,76 +87,6 @@ var Utils = module.exports = {
var timeZone = null;
return SqlString.formatNamedParameters(sql, parameters, timeZone, dialect);
},
injectScope: function(scope, merge) {
var self = this;
scope = scope || {};
if (!this.scoped && self.options.defaultScope) {
self.scopeObj = Utils._.clone(self.options.defaultScope);
if (!Array.isArray(self.scopeObj.where)) {
self.scopeObj.where = [self.scopeObj.where];
}
} else {
self.scopeObj = self.scopeObj || {};
}
if (Array.isArray(scope.where)) {
self.scopeObj.where = self.scopeObj.where || [];
self.scopeObj.where.push(scope.where);
return true;
}
if (typeof scope.order === 'string') {
self.scopeObj.order = self.scopeObj.order || [];
self.scopeObj.order[self.scopeObj.order.length] = scope.order;
}
// Limit and offset are *always* merged.
if (!!scope.limit) {
self.scopeObj.limit = scope.limit;
}
if (!!scope.offset) {
self.scopeObj.offset = scope.offset;
}
// Where objects are a mixed variable. Possible values are arrays, strings, and objects
if (!!scope.where) {
// Begin building our scopeObj
self.scopeObj.where = self.scopeObj.where || [];
// Reset if we're merging!
if (merge === true && !!scope.where && !!self.scopeObj.where) {
var scopeKeys = Object.keys(scope.where);
self.scopeObj.where = self.scopeObj.where.map(function(scopeObj) {
if (!Array.isArray(scopeObj) && typeof scopeObj === 'object' && scopeObj !== null) {
return lodash.omit.apply(undefined, [scopeObj].concat(scopeKeys));
} else {
return scopeObj;
}
}).filter(function(scopeObj) {
return !lodash.isEmpty(scopeObj);
});
self.scopeObj.where = self.scopeObj.where.concat(scope.where);
}
if (Array.isArray(scope.where)) {
self.scopeObj.where.push(scope.where);
}
else if (typeof scope.where === 'object') {
Object.keys(scope.where).forEach(function() {
self.scopeObj.where.push(scope.where);
});
} else { // Assume the value is a string
self.scopeObj.where.push([scope.where]);
}
}
if (!!self.scopeObj.where) {
self.scopeObj.where = lodash.uniq(self.scopeObj.where);
}
},
cloneDeep: function(obj, fn) {
return lodash.cloneDeep(obj, function (elem) {
// Unfortunately, lodash.cloneDeep doesn't preserve Buffer.isBuffer, which we have to rely on for binary data
......@@ -239,207 +169,6 @@ var Utils = module.exports = {
return values;
},
// smartWhere can accept an array of {where} objects, or a single {where} object.
// The smartWhere function breaks down the collection of where objects into a more
// centralized object for each column so we can avoid duplicates
// e.g. WHERE username='dan' AND username='dan' becomes WHERE username='dan'
// All of the INs, NOT INs, BETWEENS, etc. are compressed into one key for each column
// This function will hopefully provide more functionality to sequelize in the future.
// tl;dr It's a nice way to dissect a collection of where objects and compress them into one object
smartWhere: function(whereArg, dialect) {
var self = this
, _where = {}
, logic
, type;
(Array.isArray(whereArg) ? whereArg : [whereArg]).forEach(function(where) {
// If it's an array we're already good... / it's in a format that can't be broken down further
// e.g. Util.format['SELECT * FROM world WHERE status=?', 'hello']
if (Array.isArray(where)) {
_where._ = where._ || {queries: [], bindings: []};
_where._.queries[_where._.queries.length] = where[0];
if (where.length > 1) {
var values = where.splice(1);
if (dialect === 'sqlite') {
values.forEach(function(v, i) {
if (typeof v === 'boolean') {
values[i] = (v === true ? 1 : 0);
}
});
}
_where._.bindings = _where._.bindings.concat(values);
}
}
else if (typeof where === 'object' && where !== null) {
// First iteration is trying to compress IN and NOT IN as much as possible...
// .. reason being is that WHERE username IN (?) AND username IN (?) != WHERE username IN (?,?)
Object.keys(where).forEach(function(i) {
if (Array.isArray(where[i])) {
where[i] = {
in : where[i]
};
}
});
// Build our smart object
Object.keys(where).forEach(function(i) {
type = typeof where[i];
_where[i] = _where[i] || {};
if (where[i] === null) {
// skip nulls
}
else if (Array.isArray(where[i])) {
_where[i].in = _where[i]. in || [];
_where[i]. in .concat(where[i]);
}
else if (Utils._.isPlainObject(where[i])) {
Object.keys(where[i]).forEach(function(ii) {
logic = self.getWhereLogic(ii, where[i][ii]);
switch (logic) {
case 'IN':
_where[i].in = _where[i]. in || [];
_where[i].in = _where[i]. in .concat(where[i][ii]);
break;
case 'NOT':
_where[i].not = _where[i].not || [];
_where[i].not = _where[i].not.concat(where[i][ii]);
break;
case 'BETWEEN':
_where[i].between = _where[i].between || [];
_where[i].between[_where[i].between.length] = [where[i][ii][0], where[i][ii][1]];
break;
case 'NOT BETWEEN':
_where[i].nbetween = _where[i].nbetween || [];
_where[i].nbetween[_where[i].nbetween.length] = [where[i][ii][0], where[i][ii][1]];
break;
case 'JOIN':
_where[i].joined = _where[i].joined || [];
_where[i].joined[_where[i].joined.length] = where[i][ii];
break;
default:
_where[i].lazy = _where[i].lazy || {conditions: [], bindings: []};
_where[i].lazy.conditions[_where[i].lazy.conditions.length] = logic + ' ?';
_where[i].lazy.bindings = _where[i].lazy.bindings.concat(where[i][ii]);
}
});
}
else if (type === 'string' || type === 'number' || type === 'boolean' || Buffer.isBuffer(where[i])) {
_where[i].lazy = _where[i].lazy || {conditions: [], bindings: []};
if (type === 'boolean') {
_where[i].lazy.conditions[_where[i].lazy.conditions.length] = '= ' + SqlString.escape(where[i], false, null, dialect); // sqlite is special
} else {
_where[i].lazy.conditions[_where[i].lazy.conditions.length] = '= ?';
_where[i].lazy.bindings = _where[i].lazy.bindings.concat(where[i]);
}
}
});
}
});
return _where;
},
// Converts {smart where} object(s) into an array that's friendly for Utils.format()
// NOTE: Must be applied/called from the QueryInterface
compileSmartWhere: function(obj) {
var self = this
, whereArgs = []
, text = []
, columnName;
if (typeof obj !== 'object') {
return obj;
}
for (var column in obj) {
if (column === '_') {
text[text.length] = obj[column].queries.join(' AND ');
if (obj[column].bindings.length > 0) {
whereArgs = whereArgs.concat(obj[column].bindings);
}
} else {
Object.keys(obj[column]).forEach(function(condition) {
columnName = self.QueryInterface.quoteIdentifiers(column);
switch (condition) {
case 'in':
text[text.length] = columnName + ' IN (' + obj[column][condition].map(function() { return '?'; }) + ')';
whereArgs = whereArgs.concat(obj[column][condition]);
break;
case 'not':
text[text.length] = columnName + ' NOT IN (' + obj[column][condition].map(function() { return '?'; }) + ')';
whereArgs = whereArgs.concat(obj[column][condition]);
break;
case 'between':
Object.keys(obj[column][condition]).forEach(function(row) {
text[text.length] = columnName + ' BETWEEN ? AND ?';
whereArgs = whereArgs.concat(obj[column][condition][row][0], obj[column][condition][row][1]);
});
break;
case 'nbetween':
Object.keys(obj[column][condition]).forEach(function(row) {
text[text.length] = columnName + ' BETWEEN ? AND ?';
whereArgs = whereArgs.concat(obj[column][condition][row][0], obj[column][condition][row][1]);
});
break;
case 'joined':
Object.keys(obj[column][condition]).forEach(function(row) {
text[text.length] = columnName + ' = ' + self.QueryInterface.quoteIdentifiers(obj[column][condition][row]);
});
break;
default: // lazy
text = text.concat(obj[column].lazy.conditions.map(function(val) { return columnName + ' ' + val; }));
whereArgs = whereArgs.concat(obj[column].lazy.bindings);
}
});
}
}
return Utils._.compactLite([text.join(' AND ')].concat(whereArgs));
},
getWhereLogic: function(logic, val) {
switch (logic) {
case 'join':
return 'JOIN';
case 'gte':
return '>=';
case 'gt':
return '>';
case 'lte':
return '<=';
case 'lt':
return '<';
case 'eq':
return val === null ? 'IS' : '=';
case 'ne':
return val === null ? 'IS NOT' : '!=';
case 'between':
case '..':
return 'BETWEEN';
case 'nbetween':
case 'notbetween':
case '!..':
return 'NOT BETWEEN';
case 'in':
return 'IN';
case 'not':
return 'NOT IN';
case 'like':
return 'LIKE';
case 'nlike':
case 'notlike':
return 'NOT LIKE';
case 'ilike':
return 'ILIKE';
case 'nilike':
case 'notilike':
return 'NOT ILIKE';
case 'overlap':
return '&&';
default:
return '';
}
},
argsArePrimaryKeys: function(args, primaryKeys) {
var result = (args.length === Object.keys(primaryKeys).length);
if (result) {
......
......@@ -290,7 +290,7 @@ if (Support.dialectIsMySQL()) {
)
};
}],
expectation: "SELECT * FROM `myTable` WHERE (`myTable`.`archived` IS NULL AND COALESCE(`place_type_codename`, `announcement_type_codename`) IN ('Lost','Found'));",
expectation: "SELECT * FROM `myTable` WHERE (`myTable`.`archived` IS NULL AND COALESCE(`place_type_codename`, `announcement_type_codename`) IN ('Lost', 'Found'));",
context: QueryGenerator,
needsSequelize: true
}, {
......@@ -564,38 +564,6 @@ if (Support.dialectIsMySQL()) {
arguments: ['User', ['foo', 'bar']],
expectation: 'DROP INDEX user_foo_bar ON `User`'
}
],
hashToWhereConditions: [
{
arguments: [{ id: [1, 2, 3] }],
expectation: '`id` IN (1,2,3)'
},
{
arguments: [{ id: [] }],
expectation: '`id` IN (NULL)'
},
{
arguments: [{ maple: false, bacon: true }],
expectation: '`maple`=false AND `bacon`=true'
},
{
arguments: [{ beaver: [false, true] }],
expectation: '`beaver` IN (false,true)'
},
{
arguments: [{birthday: new Date(Date.UTC(2011, 6, 1, 10, 1, 55))}],
expectation: "`birthday`='2011-07-01 10:01:55'"
},
{
arguments: [{ birthday: new Date(Date.UTC(2011, 6, 1, 10, 1, 55)),
otherday: new Date(Date.UTC(2013, 6, 2, 10, 1, 22)) }],
expectation: "`birthday`='2011-07-01 10:01:55' AND `otherday`='2013-07-02 10:01:22'"
},
{
arguments: [{ birthday: [new Date(Date.UTC(2011, 6, 1, 10, 1, 55)), new Date(Date.UTC(2013, 6, 2, 10, 1, 22))] }],
expectation: "`birthday` IN ('2011-07-01 10:01:55','2013-07-02 10:01:22')"
}
]
};
......
......@@ -368,8 +368,8 @@ describe(Support.getTestDialectTeaser('Model'), function() {
return self.Environment.find({
where: { name: 'environment' },
include: [
{ daoFactory: self.Domain, as: 'PrivateDomain' },
{ daoFactory: self.Domain, as: 'PublicDomain' }
{ model: self.Domain, as: 'PrivateDomain' },
{ model: self.Domain, as: 'PublicDomain' }
]
}).then(function(environment) {
expect(environment).to.exist;
......@@ -559,7 +559,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('throws an error if alias is not associated', function() {
var self = this;
return self.Worker.find({ include: [{ daoFactory: self.Task, as: 'Work' }] }).catch (function(err) {
return self.Worker.find({ include: [{ model: self.Task, as: 'Work' }] }).catch (function(err) {
expect(err.message).to.equal('Task (Work) is not associated to Worker!');
});
});
......@@ -567,7 +567,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('returns the associated task via worker.task', function() {
return this.Worker.find({
where: { name: 'worker' },
include: [{ daoFactory: this.Task, as: 'ToDo' }]
include: [{ model: this.Task, as: 'ToDo' }]
}).then(function(worker) {
expect(worker).to.exist;
expect(worker.ToDo).to.exist;
......@@ -648,7 +648,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
where: {
name: 'Boris'
},
include: [self.PhoneNumber, { daoFactory: self.Photo, as: 'Photos' }]
include: [self.PhoneNumber, { model: self.Photo, as: 'Photos' }]
}).then(function(fetchedContact) {
expect(fetchedContact).to.exist;
expect(fetchedContact.Photos.length).to.equal(1);
......@@ -720,7 +720,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('throws an error if alias is not associated', function() {
var self = this;
return self.Worker.find({ include: [{ daoFactory: self.Task, as: 'Work' }] }).catch (function(err) {
return self.Worker.find({ include: [{ model: self.Task, as: 'Work' }] }).catch (function(err) {
expect(err.message).to.equal('Task (Work) is not associated to Worker!');
});
});
......@@ -728,7 +728,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('returns the associated task via worker.task', function() {
return this.Worker.find({
where: { name: 'worker' },
include: [{ daoFactory: this.Task, as: 'ToDos' }]
include: [{ model: this.Task, as: 'ToDos' }]
}).then(function(worker) {
expect(worker).to.exist;
expect(worker.ToDos).to.exist;
......
......@@ -630,7 +630,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('throws an error if alias is not associated', function() {
var self = this;
return self.Worker.findAll({ include: [{ daoFactory: self.Task, as: 'Work' }] }).catch (function(err) {
return self.Worker.findAll({ include: [{ model: self.Task, as: 'Work' }] }).catch (function(err) {
expect(err.message).to.equal('Task (Work) is not associated to Worker!');
});
});
......@@ -638,7 +638,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('returns the associated task via worker.task', function() {
return this.Worker.findAll({
where: { name: 'worker' },
include: [{ daoFactory: this.Task, as: 'ToDo' }]
include: [{ model: this.Task, as: 'ToDo' }]
}).then(function(workers) {
expect(workers).to.exist;
expect(workers[0].ToDo).to.exist;
......@@ -722,7 +722,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('throws an error if alias is not associated', function() {
var self = this;
return self.Worker.findAll({ include: [{ daoFactory: self.Task, as: 'Work' }] }).catch (function(err) {
return self.Worker.findAll({ include: [{ model: self.Task, as: 'Work' }] }).catch (function(err) {
expect(err.message).to.equal('Task (Work) is not associated to Worker!');
});
});
......@@ -730,7 +730,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('returns the associated task via worker.task', function() {
return this.Worker.findAll({
where: { name: 'worker' },
include: [{ daoFactory: this.Task, as: 'ToDos' }]
include: [{ model: this.Task, as: 'ToDos' }]
}).then(function(workers) {
expect(workers).to.exist;
expect(workers[0].ToDos).to.exist;
......@@ -741,7 +741,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('returns the associated task via worker.task when daoFactory is aliased with model', function() {
return this.Worker.findAll({
where: { name: 'worker' },
include: [{ daoFactory: this.Task, as: 'ToDos' }]
include: [{ model: this.Task, as: 'ToDos' }]
}).then(function(workers) {
expect(workers[0].ToDos[0].title).to.equal('homework');
});
......
'use strict';
/* jshint -W030 */
/* jshint -W110 */
var chai = require('chai')
, Sequelize = require('../../../../index')
, expect = chai.expect
, Promise = Sequelize.Promise
, Support = require(__dirname + '/../../support');
describe(Support.getTestDialectTeaser('Model'), function() {
describe('scope', function () {
describe('associations', function () {
beforeEach(function () {
this.ScopeMe = this.sequelize.define('ScopeMe', {
username: Sequelize.STRING,
email: Sequelize.STRING,
access_level: Sequelize.INTEGER,
other_value: Sequelize.INTEGER,
parent_id: Sequelize.INTEGER
}, {
defaultScope: {
where: {
access_level: {
gte: 5
}
}
},
scopes: {
highValue: {
where: {
other_value: {
gte: 10
}
}
},
isTony: {
where: {
username: 'tony'
}
},
actualValue: function(value) {
return {
where: {
other_value: value
}
};
},
lowAccess: {
where: {
access_level: {
lte: 5
}
}
},
escape: {
where: {
username: "escape'd"
}
}
}
});
this.Company = this.sequelize.define('company', {
active: Sequelize.BOOLEAN
}, {
defaultScope: {
where: { active: true }
},
scopes: {
notActive: {
where: {
active: false
}
},
users: {
include: [
{ model: this.ScopeMe.unscoped(), as: 'users'}
]
},
reversed: {
order: [['id', 'DESC']]
}
}
});
this.Project = this.sequelize.define('Project');
this.Project.belongsToMany(this.Company, { through: 'CompanyProjects' });
this.Company.belongsToMany(this.Project, { through: 'CompanyProjects' });
this.ScopeMe.belongsTo(this.Company);
this.UserAssociation = this.Company.hasMany(this.ScopeMe, { as: 'users'});
return this.sequelize.sync({force: true}).bind(this).then(function() {
return Promise.all([
this.ScopeMe.create({username: 'dan', email: 'dan@sequelizejs.com', access_level: 5, other_value: 10, parent_id: 1}),
this.ScopeMe.create({username: 'tobi', email: 'tobi@fakeemail.com', access_level: 10, other_value: 11, parent_id: 2}),
this.ScopeMe.create({username: 'tony', email: 'tony@sequelizejs.com', access_level: 3, other_value: 7, parent_id: 1}),
this.ScopeMe.create({username: 'fred', email: 'fred@foobar.com', access_level: 3, other_value: 7, parent_id: 1}),
this.Company.create({active:true}),
this.Company.create({active:false}),
this.ScopeMe.create({username: 'bob', email: 'bob@foobar.com', access_level: 1, other_value: 9, parent_id: 5}),
]);
}).spread(function (u1, u2, u3, u4, c1, c2, u5) {
return Promise.all([
c1.setUsers([u1, u2, u3, u4]),
c2.setUsers([u5])
]);
});
});
describe('include', function () {
describe('target', function () {
it('should scope columns properly', function () {
// Will error with ambigous column if id is not scoped properly to `Company`.`id`
return expect(this.Company.findAll({
where: { id: 1},
include: [this.UserAssociation]
})).to.be.resolved;
});
it('should apply default scope when including an associations', function () {
return this.Company.findAll({
include: [this.UserAssociation]
}).get(0).then(function (company) {
expect(company.users).to.have.length(2);
});
});
it('should apply default scope when including a model', function () {
return this.Company.findAll({
include: [{ model: this.ScopeMe, as: 'users'}]
}).get(0).then(function (company) {
expect(company.users).to.have.length(2);
});
});
it('should be able to include a scoped model', function () {
return this.Company.findAll({
include: [{ model: this.ScopeMe.scope('isTony'), as: 'users'}]
}).get(0).then(function (company) {
expect(company.users).to.have.length(1);
expect(company.users[0].get('username')).to.equal('tony');
});
});
});
describe('source', function () {
it('should include model with scope', function () {
return this.Company.scope('users').findAll({ where: { id: 1 } }).get(0).then(function (company) {
expect(company.users).to.have.length(4);
});
});
it('should be able to override scoped include', function () {
return this.Company.scope('users').findAll({
include: [
{ association: this.UserAssociation, where: { username: 'dan'} }
]
}).get(0).then(function (company) {
expect(company.users).to.have.length(1);
expect(company.users[0].get('username')).to.equal('dan');
});
});
it('should be able to append to includes', function () {
// ? or should it
});
});
});
describe('get', function () {
beforeEach(function () {
return Promise.all([
this.Project.create(),
this.Company.unscoped().findAll()
]).spread(function (p, companies) {
return p.setCompanies(companies);
});
});
describe('it should be able to unscope', function () {
it('hasMany', function () {
return this.Company.find(1).then(function (company) {
return company.getUsers({ scope: false});
}).then(function (users) {
expect(users).to.have.length(4);
});
});
it('hasOne', function () {
});
it('belongsTo', function () {
return this.ScopeMe.unscoped().find({ where: { username: 'bob' }}).then(function (user) {
return user.getCompany({ scope: false });
}).then(function (company) {
expect(company).to.be.ok;
});
});
it('belongsToMany', function () {
return this.Project.findAll().get(0).then(function (p) {
return p.getCompanies({ scope: false});
}).then(function (companies) {
expect(companies).to.have.length(2);
});
});
});
describe('it should apply default scope', function () {
it('hasMany', function () {
return this.Company.find(1).then(function (company) {
return company.getUsers();
}).then(function (users) {
expect(users).to.have.length(2);
});
});
it('hasOne', function () {
});
it('hasMany double', function () {
// Do we need to - it's deprecated?
})
it('belongsTo', function () {
return this.ScopeMe.unscoped().find({ where: { username: 'bob' }}).then(function (user) {
return user.getCompany();
}).then(function (company) {
expect(company).not.to.be.ok;
});
});
it('belongsToMany', function () {
return this.Project.findAll().get(0).then(function (p) {
return p.getCompanies();
}).then(function (companies) {
expect(companies).to.have.length(1);
expect(companies[0].get('active')).to.be.ok;
});
});
});
describe('it should be able to apply another scope', function () {
it('hasMany', function () {
return this.Company.find(1).then(function (company) {
return company.getUsers({ scope: 'isTony'});
}).then(function (users) {
expect(users).to.have.length(1);
expect(users[0].get('username')).to.equal('tony');
});
});
it('hasOne', function () {
});
it('belongsTo', function () {
return this.ScopeMe.unscoped().find({ where: { username: 'bob' }}).then(function (user) {
return user.getCompany({ scope: 'notActive' });
}).then(function (company) {
expect(company).to.be.ok;
});
});
it('belongsToMany', function () {
return this.Project.findAll().get(0).then(function (p) {
return p.getCompanies({ scope: 'reversed' });
}).then(function (companies) {
expect(companies).to.have.length(2);
expect(companies[0].id).to.equal(2);
expect(companies[1].id).to.equal(1);
});
});
});
});
});
});
});
'use strict';
/* jshint -W030 */
/* jshint -W110 */
var chai = require('chai')
, Sequelize = require('../../../../index')
, expect = chai.expect
, Support = require(__dirname + '/../../support');
describe(Support.getTestDialectTeaser('Model'), function() {
describe('scope', function () {
describe('destroy', function () {
beforeEach(function () {
this.ScopeMe = this.sequelize.define('ScopeMe', {
username: Sequelize.STRING,
email: Sequelize.STRING,
access_level: Sequelize.INTEGER,
other_value: Sequelize.INTEGER
}, {
defaultScope: {
where: {
access_level: {
gte: 5
}
}
},
scopes: {
lowAccess: {
where: {
access_level: {
lte: 5
}
}
}
}
});
return this.sequelize.sync({force: true}).then(function() {
var records = [
{username: 'tony', email: 'tony@sequelizejs.com', access_level: 3, other_value: 7},
{username: 'tobi', email: 'tobi@fakeemail.com', access_level: 10, other_value: 11},
{username: 'dan', email: 'dan@sequelizejs.com', access_level: 5, other_value: 10},
{username: 'fred', email: 'fred@foobar.com', access_level: 3, other_value: 7}
];
return this.ScopeMe.bulkCreate(records);
}.bind(this));
});
it('should apply defaultScope', function () {
return this.ScopeMe.destroy({ where: {}}).bind(this).then(function () {
return this.ScopeMe.unscoped().findAll();
}).then(function (users) {
expect(users).to.have.length(2);
expect(users[0].get('username')).to.equal('tony');
expect(users[1].get('username')).to.equal('fred');
});
});
it('should be able to override default scope', function () {
return this.ScopeMe.destroy({ where: { access_level: { lt: 5 }}}).bind(this).then(function () {
return this.ScopeMe.unscoped().findAll();
}).then(function (users) {
expect(users).to.have.length(2);
expect(users[0].get('username')).to.equal('tobi');
expect(users[1].get('username')).to.equal('dan');
});
});
it('should be able to unscope destroy', function () {
return this.ScopeMe.unscoped().destroy({ where: {}}).bind(this).then(function() {
return expect(this.ScopeMe.unscoped().findAll()).to.eventually.have.length(0);
});
});
it('should be able to apply other scopes', function () {
return this.ScopeMe.scope('lowAccess').destroy({ where: {}}).bind(this).then(function () {
return this.ScopeMe.unscoped().findAll();
}).then(function (users) {
expect(users).to.have.length(1);
expect(users[0].get('username')).to.equal('tobi');
});
});
it('should be able to merge scopes with where', function () {
return this.ScopeMe.scope('lowAccess').destroy({ where: { username: 'dan'}}).bind(this).then(function () {
return this.ScopeMe.unscoped().findAll();
}).then(function (users) {
expect(users).to.have.length(3);
expect(users[0].get('username')).to.equal('tony');
expect(users[1].get('username')).to.equal('tobi');
expect(users[2].get('username')).to.equal('fred');
});
});
});
});
});
......@@ -3,28 +3,12 @@
/* jshint -W030 */
/* jshint -W110 */
var chai = require('chai')
, Sequelize = require('../../../index')
, Sequelize = require('../../../../index')
, expect = chai.expect
, Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + '/../../../lib/data-types');
, Promise = Sequelize.Promise
, Support = require(__dirname + '/../../support');
describe(Support.getTestDialectTeaser('Model'), function() {
beforeEach(function() {
return Support.prepareTransactionTest(this.sequelize).bind(this).then(function(sequelize) {
this.sequelize = sequelize;
this.User = this.sequelize.define('User', {
username: DataTypes.STRING,
secretValue: DataTypes.STRING,
data: DataTypes.STRING,
intVal: DataTypes.INTEGER,
theDate: DataTypes.DATE,
aBool: DataTypes.BOOLEAN
});
return this.User.sync({ force: true });
});
});
describe('scopes', function() {
beforeEach(function() {
this.ScopeMe = this.sequelize.define('ScopeMe', {
......@@ -48,12 +32,6 @@ describe(Support.getTestDialectTeaser('Model'), function() {
limitScope: {
limit: 2
},
sequelizeTeam: {
where: ['email LIKE \'%@sequelizejs.com\'']
},
fakeEmail: {
where: ['email LIKE \'%@fakeemail.com\'']
},
highValue: {
where: {
other_value: {
......@@ -66,17 +44,22 @@ describe(Support.getTestDialectTeaser('Model'), function() {
username: 'tony'
}
},
canBeTony: {
sequelizeTeam: {
where: {
username: ['tony']
email: {
like: '%@sequelizejs.com'
}
}
},
canBeDan: {
where: {
username: {
in : 'dan'
noArgs: function () {
// This does not make much sense, since it does not actually need to be in a function,
// In reality it could be used to do for example new Date or random in the scope - but we want it deterministic
return {
where: {
other_value: 7
}
}
};
},
actualValue: function(value) {
return {
......@@ -85,9 +68,16 @@ describe(Support.getTestDialectTeaser('Model'), function() {
}
};
},
complexFunction: function(email, accessLevel) {
complexFunction: function(username, accessLevel) {
return {
where: ['email like ? AND access_level >= ?', email + '%', accessLevel]
where: {
username: {
like: username
},
access_level: {
gte: accessLevel
}
}
};
},
lowAccess: {
......@@ -97,9 +87,16 @@ describe(Support.getTestDialectTeaser('Model'), function() {
}
}
},
escape: {
andScope: {
where: {
username: "escape'd"
$and: [
{
email: {
like: '%@sequelizejs.com'
}
},
{ access_level : 3 }
]
}
}
}
......@@ -107,9 +104,9 @@ describe(Support.getTestDialectTeaser('Model'), function() {
return this.sequelize.sync({force: true}).then(function() {
var records = [
{username: 'dan', email: 'dan@sequelizejs.com', access_level: 5, other_value: 10, parent_id: 1},
{username: 'tobi', email: 'tobi@fakeemail.com', access_level: 10, other_value: 11, parent_id: 2},
{username: 'tony', email: 'tony@sequelizejs.com', access_level: 3, other_value: 7, parent_id: 1},
{username: 'tobi', email: 'tobi@fakeemail.com', access_level: 10, other_value: 11, parent_id: 2},
{username: 'dan', email: 'dan@sequelizejs.com', access_level: 5, other_value: 10, parent_id: 1},
{username: 'fred', email: 'fred@foobar.com', access_level: 3, other_value: 7, parent_id: 1}
];
return this.ScopeMe.bulkCreate(records);
......@@ -118,36 +115,22 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('should be able use where in scope', function() {
return this.ScopeMe.scope({where: { parent_id: 2 }}).findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users).to.have.length(1);
expect(users[0].username).to.equal('tobi');
});
});
it('should be able to combine scope and findAll where clauses', function() {
return this.ScopeMe.scope({where: { parent_id: 1 }}).findAll({ where: {access_level: 3}}).then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(2);
expect(users).to.have.length(2);
expect(['tony', 'fred'].indexOf(users[0].username) !== -1).to.be.true;
expect(['tony', 'fred'].indexOf(users[1].username) !== -1).to.be.true;
});
});
it('should have no problems with escaping SQL', function() {
var self = this;
return this.ScopeMe.create({username: 'escape\'d', email: 'fake@fakemail.com'}).then(function() {
return self.ScopeMe.scope('escape').all().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users[0].username).to.equal('escape\'d');
});
});
});
it('should be able to use a defaultScope if declared', function() {
return this.ScopeMe.all().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(2);
expect(users).to.have.length(2);
expect([10, 5].indexOf(users[0].access_level) !== -1).to.be.true;
expect([10, 5].indexOf(users[1].access_level) !== -1).to.be.true;
expect(['dan', 'tobi'].indexOf(users[0].username) !== -1).to.be.true;
......@@ -157,57 +140,57 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('should be able to amend the default scope with a find object', function() {
return this.ScopeMe.findAll({where: {username: 'dan'}}).then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users).to.have.length(1);
expect(users[0].username).to.equal('dan');
});
});
it('should be able to override the default scope', function() {
return this.ScopeMe.scope('fakeEmail').findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users[0].username).to.equal('tobi');
return this.ScopeMe.scope('sequelizeTeam').findAll().then(function(users) {
expect(users).to.have.length(2);
expect(users[0].username).to.equal('tony');
expect(users[1].username).to.equal('dan');
});
});
it('should be able to combine two scopes', function() {
return this.ScopeMe.scope(['sequelizeTeam', 'highValue']).findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users).to.have.length(1);
expect(users[0].username).to.equal('dan');
});
});
it('should be able to combine default with another scope', function () {
return this.ScopeMe.scope(['defaultScope', {method: ['actualValue', 11]}]).findAll().then(function(users) {
expect(users).to.have.length(1);
expect(users[0].username).to.equal('tobi');
});
});
it("should be able to call a scope that's a function", function() {
return this.ScopeMe.scope({method: ['actualValue', 11]}).findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users).to.have.length(1);
expect(users[0].username).to.equal('tobi');
});
});
it('should be able to handle multiple function scopes', function() {
return this.ScopeMe.scope([{method: ['actualValue', 10]}, {method: ['complexFunction', 'dan', '5']}]).findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users).to.have.length(1);
expect(users[0].username).to.equal('dan');
});
});
it('should be able to stack the same field in the where clause', function() {
return this.ScopeMe.scope(['canBeDan', 'canBeTony']).findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(2);
expect(['dan', 'tony'].indexOf(users[0].username) !== -1).to.be.true;
expect(['dan', 'tony'].indexOf(users[1].username) !== -1).to.be.true;
it('should be able to handle $and in scopes', function () {
return this.ScopeMe.scope('andScope').findAll().then(function(users) {
expect(users).to.have.length(1);
expect(users[0].username).to.equal('tony');
});
});
it('should be able to merge scopes', function() {
return this.ScopeMe.scope(['highValue', 'isTony', {merge: true, method: ['actualValue', 7]}]).findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users).to.have.length(1);
expect(users[0].username).to.equal('tony');
});
});
......@@ -237,22 +220,27 @@ describe(Support.getTestDialectTeaser('Model'), function() {
});
it('function scopes', function() {
return this.ScopeMe.scope({method: ['actualValue', 11]}).findAll().bind(this).then(function(users) {
expect(users).to.have.length(1);
expect(users[0].other_value).to.equal(11);
return this.ScopeMe.scope({method: ['actualValue', 10]}).findAll();
}).then(function(users) {
expect(users).to.have.length(1);
expect(users[0].other_value).to.equal(10);
return Promise.all([
this.ScopeMe.scope({method: ['actualValue', 11]}).findAll(),
this.ScopeMe.scope({method: ['actualValue', 10]}).findAll(),
this.ScopeMe.scope('noArgs').findAll()
]).spread(function (users1, users2, users3) {
expect(users1).to.have.length(1);
expect(users1[0].other_value).to.equal(11);
expect(users2).to.have.length(1);
expect(users2[0].other_value).to.equal(10);
expect(users3).to.have.length(2);
expect(users3[0].other_value).to.equal(7);
expect(users3[1].other_value).to.equal(7);
});
});
});
it('should give us the correct order if we declare an order in our scope', function() {
return this.ScopeMe.scope('sequelizeTeam', 'orderScope').findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(2);
expect(users).to.have.length(2);
expect(users[0].username).to.equal('dan');
expect(users[1].username).to.equal('tony');
});
......@@ -260,28 +248,14 @@ describe(Support.getTestDialectTeaser('Model'), function() {
it('should give us the correct order as well as a limit if we declare such in our scope', function() {
return this.ScopeMe.scope(['orderScope', 'limitScope']).findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(2);
expect(users).to.have.length(2);
expect(users[0].username).to.equal('tobi');
expect(users[1].username).to.equal('dan');
});
});
it('should have no problems combining scopes and traditional where object', function() {
return this.ScopeMe.scope('sequelizeTeam').findAll({where: {other_value: 10}}).then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(1);
expect(users[0].username).to.equal('dan');
expect(users[0].access_level).to.equal(5);
expect(users[0].other_value).to.equal(10);
});
});
it('should be able to remove all scopes', function() {
return this.ScopeMe.scope(null).findAll().then(function(users) {
expect(users).to.be.an.instanceof(Array);
expect(users.length).to.equal(4);
});
return expect(this.ScopeMe.scope(null).findAll()).to.eventually.have.length(4);
});
it('should have no problem performing findOrCreate', function() {
......@@ -296,46 +270,17 @@ describe(Support.getTestDialectTeaser('Model'), function() {
return sequelizeTeam.all().then(function(team) {
return tobi.all().then(function(t) {
expect(team).to.be.an.instanceof(Array);
expect(team.length).to.equal(2);
expect(team).to.have.length(2);
expect(team[0].username).to.equal('dan');
expect(team[1].username).to.equal('tony');
expect(t).to.be.an.instanceof(Array);
expect(t.length).to.equal(1);
expect(t).to.have.length(1);
expect(t[0].username).to.equal('tobi');
});
});
});
it("should gracefully omit any scopes that don't exist", function() {
return this.ScopeMe.scope('sequelizeTeam', 'orderScope', 'doesntexist').all().then(function(team) {
expect(team).to.be.an.instanceof(Array);
expect(team.length).to.equal(2);
expect(team[0].username).to.equal('dan');
expect(team[1].username).to.equal('tony');
});
});
it("should gracefully omit any scopes that don't exist through an array", function() {
return this.ScopeMe.scope(['sequelizeTeam', 'orderScope', 'doesntexist']).all().then(function(team) {
expect(team).to.be.an.instanceof(Array);
expect(team.length).to.equal(2);
expect(team[0].username).to.equal('dan');
expect(team[1].username).to.equal('tony');
});
});
it("should gracefully omit any scopes that don't exist through an object", function() {
return this.ScopeMe.scope('sequelizeTeam', 'orderScope', {method: 'doesntexist'}).all().then(function(team) {
expect(team).to.be.an.instanceof(Array);
expect(team.length).to.equal(2);
expect(team[0].username).to.equal('dan');
expect(team[1].username).to.equal('tony');
});
});
it("should emit an error for scopes that don't exist with silent: false", function() {
it("should emit an error for scopes that don't exist", function() {
expect(this.ScopeMe.scope.bind(this.ScopeMe, 'doesntexist', {silent: false})).to.throw('Invalid scope doesntexist called.');
});
});
......
'use strict';
/* jshint -W030 */
/* jshint -W110 */
var chai = require('chai')
, _ = require('lodash')
, Sequelize = require('../../../../index')
, expect = chai.expect
, Support = require(__dirname + '/../../support');
describe(Support.getTestDialectTeaser('Model'), function() {
describe('scope', function () {
describe('update', function () {
beforeEach(function () {
this.ScopeMe = this.sequelize.define('ScopeMe', {
username: Sequelize.STRING,
email: Sequelize.STRING,
access_level: Sequelize.INTEGER,
other_value: Sequelize.INTEGER
}, {
defaultScope: {
where: {
access_level: {
gte: 5
}
}
},
scopes: {
lowAccess: {
where: {
access_level: {
lte: 5
}
}
}
}
});
return this.sequelize.sync({force: true}).then(function() {
var records = [
{username: 'tony', email: 'tony@sequelizejs.com', access_level: 3, other_value: 7},
{username: 'tobi', email: 'tobi@fakeemail.com', access_level: 10, other_value: 11},
{username: 'dan', email: 'dan@sequelizejs.com', access_level: 5, other_value: 10},
{username: 'fred', email: 'fred@foobar.com', access_level: 3, other_value: 7}
];
return this.ScopeMe.bulkCreate(records);
}.bind(this));
});
it('should apply defaultScope', function () {
return this.ScopeMe.update({ username: 'ruben' }, { where: {}}).bind(this).then(function () {
return this.ScopeMe.unscoped().findAll({ where: { username: 'ruben' }});
}).then(function (users) {
expect(users).to.have.length(2);
expect(users[0].get('email')).to.equal('tobi@fakeemail.com');
expect(users[1].get('email')).to.equal('dan@sequelizejs.com');
});
});
it('should be able to override default scope', function () {
return this.ScopeMe.update({ username: 'ruben' }, { where: { access_level: { lt: 5 }}}).bind(this).then(function () {
return this.ScopeMe.unscoped().findAll({ where: { username: 'ruben' }});
}).then(function (users) {
expect(users).to.have.length(2);
expect(users[0].get('email')).to.equal('tony@sequelizejs.com');
expect(users[1].get('email')).to.equal('fred@foobar.com');
});
});
it('should be able to unscope destroy', function () {
return this.ScopeMe.unscoped().update({ username: 'ruben' }, { where: {}}).bind(this).then(function() {
return this.ScopeMe.unscoped().findAll();
}).then(function (rubens) {
expect(_.every(rubens, function (r) {
return r.get('username') === 'ruben';
})).to.be.true;
});
});
it('should be able to apply other scopes', function () {
return this.ScopeMe.scope('lowAccess').update({ username: 'ruben' }, { where: {}}).bind(this).then(function () {
return this.ScopeMe.unscoped().findAll({ where: { username: { $ne: 'ruben' }}});
}).then(function (users) {
expect(users).to.have.length(1);
expect(users[0].get('email')).to.equal('tobi@fakeemail.com');
});
});
it('should be able to merge scopes with where', function () {
return this.ScopeMe.scope('lowAccess').update({ username: 'ruben' }, { where: { username: 'dan'}}).bind(this).then(function () {
return this.ScopeMe.unscoped().findAll({ where: { username: 'ruben' }});
}).then(function (users) {
expect(users).to.have.length(1);
expect(users[0].get('email')).to.equal('dan@sequelizejs.com');
});
});
});
});
});
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!