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

Commit c58bc413 by Mick Hansen

Merge pull request #2268 from sequelize/feat-association-scope

[WIP] Feature: Association scopes
2 parents 90965ba3 e07ef3cf
...@@ -2,9 +2,11 @@ ...@@ -2,9 +2,11 @@
- [BUG] Fixed an issue with foreign key object syntax for hasOne and belongsTo - [BUG] Fixed an issue with foreign key object syntax for hasOne and belongsTo
- [FEATURE] Added `field` and `name` to the object form of foreign key definitions - [FEATURE] Added `field` and `name` to the object form of foreign key definitions
- [FEATURE] Added support for calling `Promise.done`, thus explicitly ending the promise chain by calling done with no arguments. Done with a function argument still continues the promise chain, to maintain BC. - [FEATURE] Added support for calling `Promise.done`, thus explicitly ending the promise chain by calling done with no arguments. Done with a function argument still continues the promise chain, to maintain BC.
- [FEATURE] Added `scope` to hasMany association definitions, provides default values to association setters/finders [#2268](https://github.com/sequelize/sequelize/pull/2268)
#### Backwards compatability changes #### Backwards compatability changes
- The `fieldName` property, used in associations with a foreign key object `(A.hasMany(B, { foreignKey: { ... }})`, has been renamed to `name` to avoid confusion with `field`. - The `fieldName` property, used in associations with a foreign key object `(A.hasMany(B, { foreignKey: { ... }})`, has been renamed to `name` to avoid confusion with `field`.
- The naming of the join table entry for N:M association getters is now singular (like includes)
# v2.0.0-dev13 # v2.0.0-dev13
We are working our way to the first 2.0.0 release candidate. We are working our way to the first 2.0.0 release candidate.
......
...@@ -14,6 +14,7 @@ module.exports = (function() { ...@@ -14,6 +14,7 @@ module.exports = (function() {
this.source = source; this.source = source;
this.target = target; this.target = target;
this.options = options; this.options = options;
this.scope = options.scope;
this.isSingleAssociation = true; this.isSingleAssociation = true;
this.isSelfAssociation = (this.source === this.target); this.isSelfAssociation = (this.source === this.target);
this.as = this.options.as; this.as = this.options.as;
......
...@@ -15,57 +15,48 @@ module.exports = (function() { ...@@ -15,57 +15,48 @@ module.exports = (function() {
HasManyDoubleLinked.prototype.injectGetter = function(options, queryOptions) { HasManyDoubleLinked.prototype.injectGetter = function(options, queryOptions) {
var self = this var self = this
, through = self.association.through , through = self.association.through
, targetAssociation = self.association.targetAssociation; , scopeWhere
, throughWhere;
//fully qualify
var instancePrimaryKey = self.instance.Model.primaryKeyAttribute if (this.association.scope) {
, foreignPrimaryKey = self.association.target.primaryKeyAttribute; scopeWhere = {};
Object.keys(this.association.scope).forEach(function (attribute) {
scopeWhere[attribute] = this.association.scope[attribute];
}.bind(this));
}
options.where = new Utils.and([ options.where = new Utils.and([
new Utils.where( scopeWhere,
through.rawAttributes[self.association.identifier],
self.instance[instancePrimaryKey]
),
new Utils.where(
through.rawAttributes[self.association.foreignIdentifier],
{
join: new Utils.literal([
self.QueryInterface.quoteTable(self.association.target.name),
self.QueryInterface.quoteIdentifier(foreignPrimaryKey)
].join('.'))
}
),
options.where options.where
]); ]);
if (Object(targetAssociation.through) === targetAssociation.through) { if (Object(through.model) === through.model) {
queryOptions.hasJoinTableModel = true; throughWhere = {};
queryOptions.joinTableModel = through; throughWhere[self.association.identifier] = self.instance.get(self.association.source.primaryKeyAttribute);
if (!options.attributes) { if (through && through.scope) {
options.attributes = [ Object.keys(through.scope).forEach(function (attribute) {
self.QueryInterface.quoteTable(self.association.target.name) + '.*' throughWhere[attribute] = through.scope[attribute];
]; }.bind(this));
} }
if (options.joinTableAttributes) { options.include = options.include || [];
options.joinTableAttributes.forEach(function(elem) { options.include.push({
options.attributes.push( model: through.model,
self.QueryInterface.quoteTable(through.name) + '.' + self.QueryInterface.quoteIdentifier(elem) + ' as ' + as: Utils.singularize(through.model.tableName),
self.QueryInterface.quoteIdentifier(through.name + '.' + elem, true) attributes: options.joinTableAttributes,
); association: {
isSingleAssociation: true,
source: self.association.target,
target: self.association.source,
identifier: self.association.foreignIdentifier
},
required: true,
where: throughWhere,
_pseudo: true
}); });
} else {
Utils._.forOwn(through.rawAttributes, function(elem, key) {
options.attributes.push(
self.QueryInterface.quoteTable(through.name) + '.' + self.QueryInterface.quoteIdentifier(key) + ' as ' +
self.QueryInterface.quoteIdentifier(through.name + '.' + key, true)
);
});
}
} }
return self.association.target.findAll(options, queryOptions);
return self.association.target.findAllJoin([through.getTableName(), through.name], options, queryOptions);
}; };
HasManyDoubleLinked.prototype.injectSetter = function(oldAssociations, newAssociations, defaultAttributes) { HasManyDoubleLinked.prototype.injectSetter = function(oldAssociations, newAssociations, defaultAttributes) {
...@@ -97,10 +88,10 @@ module.exports = (function() { ...@@ -97,10 +88,10 @@ module.exports = (function() {
if (!newObj) { if (!newObj) {
obsoleteAssociations.push(old); obsoleteAssociations.push(old);
} else if (Object(targetAssociation.through) === targetAssociation.through) { } else if (Object(targetAssociation.through.model) === targetAssociation.through.model) {
var throughAttributes = newObj[self.association.through.name]; var throughAttributes = newObj[self.association.through.model.name];
// Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object) // Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object)
if (throughAttributes instanceof self.association.through.DAO) { if (throughAttributes instanceof self.association.through.model.Instance) {
throughAttributes = {}; throughAttributes = {};
} }
...@@ -128,7 +119,7 @@ module.exports = (function() { ...@@ -128,7 +119,7 @@ module.exports = (function() {
where[self.association.identifier] = ((sourceKeys.length === 1) ? self.instance[sourceKeys[0]] : self.instance.id); where[self.association.identifier] = ((sourceKeys.length === 1) ? self.instance[sourceKeys[0]] : self.instance.id);
where[foreignIdentifier] = foreignIds; where[foreignIdentifier] = foreignIds;
promises.push(self.association.through.destroy(where, options)); promises.push(self.association.through.model.destroy(where, options));
} }
if (unassociatedObjects.length > 0) { if (unassociatedObjects.length > 0) {
...@@ -138,19 +129,25 @@ module.exports = (function() { ...@@ -138,19 +129,25 @@ module.exports = (function() {
attributes[self.association.identifier] = ((sourceKeys.length === 1) ? self.instance[sourceKeys[0]] : self.instance.id); attributes[self.association.identifier] = ((sourceKeys.length === 1) ? self.instance[sourceKeys[0]] : self.instance.id);
attributes[foreignIdentifier] = ((targetKeys.length === 1) ? unassociatedObject[targetKeys[0]] : unassociatedObject.id); attributes[foreignIdentifier] = ((targetKeys.length === 1) ? unassociatedObject[targetKeys[0]] : unassociatedObject.id);
if (Object(targetAssociation.through) === targetAssociation.through) { if (Object(targetAssociation.through.model) === targetAssociation.through.model) {
attributes = Utils._.defaults(attributes, unassociatedObject[targetAssociation.through.name], defaultAttributes); attributes = Utils._.defaults(attributes, unassociatedObject[targetAssociation.through.model.name], defaultAttributes);
}
if (this.association.through.scope) {
Object.keys(this.association.through.scope).forEach(function (attribute) {
attributes[attribute] = this.association.through.scope[attribute];
}.bind(this));
} }
return attributes; return attributes;
}); }.bind(this));
promises.push(self.association.through.bulkCreate(bulk, options)); promises.push(self.association.through.model.bulkCreate(bulk, options));
} }
if (changedAssociations.length > 0) { if (changedAssociations.length > 0) {
changedAssociations.forEach(function(assoc) { changedAssociations.forEach(function(assoc) {
promises.push(self.association.through.update(assoc.attributes, assoc.where, options)); promises.push(self.association.through.model.update(assoc.attributes, assoc.where, options));
}); });
} }
...@@ -175,17 +172,22 @@ module.exports = (function() { ...@@ -175,17 +172,22 @@ module.exports = (function() {
if (exists) { if (exists) {
var where = attributes; var where = attributes;
attributes = Utils._.defaults({}, newAssociation[targetAssociation.through.name], additionalAttributes); attributes = Utils._.defaults({}, newAssociation[targetAssociation.through.model.name], additionalAttributes);
if (Object.keys(attributes).length) { if (Object.keys(attributes).length) {
return targetAssociation.through.update(attributes, where, options); return targetAssociation.through.model.update(attributes, where, options);
} else { } else {
return Utils.Promise.resolve(); return Utils.Promise.resolve();
} }
} else { } else {
attributes = Utils._.defaults(attributes, newAssociation[targetAssociation.through.name], additionalAttributes); attributes = Utils._.defaults(attributes, newAssociation[targetAssociation.through.model.name], additionalAttributes);
if (this.association.through.scope) {
Object.keys(this.association.through.scope).forEach(function (attribute) {
attributes[attribute] = this.association.through.scope[attribute];
}.bind(this));
}
return this.association.through.create(attributes, options); return this.association.through.model.create(attributes, options);
} }
}; };
......
...@@ -5,7 +5,6 @@ var Utils = require('./../utils') ...@@ -5,7 +5,6 @@ var Utils = require('./../utils')
module.exports = (function() { module.exports = (function() {
var HasManySingleLinked = function(association, instance) { var HasManySingleLinked = function(association, instance) {
this.__factory = association;
this.association = association; this.association = association;
this.instance = instance; this.instance = instance;
this.target = this.association.target; this.target = this.association.target;
...@@ -13,11 +12,19 @@ module.exports = (function() { ...@@ -13,11 +12,19 @@ module.exports = (function() {
}; };
HasManySingleLinked.prototype.injectGetter = function(options, queryOptions) { HasManySingleLinked.prototype.injectGetter = function(options, queryOptions) {
var scopeWhere = this.association.scope ? {} : null;
if (this.association.scope) {
Object.keys(this.association.scope).forEach(function (attribute) {
scopeWhere[attribute] = this.association.scope[attribute];
}.bind(this));
}
options.where = new Utils.and([ options.where = new Utils.and([
new Utils.where( new Utils.where(
this.target.rawAttributes[this.association.identifier], this.target.rawAttributes[this.association.identifier],
this.instance[this.source.primaryKeyAttribute]) this.instance[this.source.primaryKeyAttribute]
, ),
scopeWhere,
options.where options.where
]); ]);
...@@ -52,22 +59,22 @@ module.exports = (function() { ...@@ -52,22 +59,22 @@ module.exports = (function() {
if (obsoleteAssociations.length > 0) { if (obsoleteAssociations.length > 0) {
// clear the old associations // clear the old associations
var obsoleteIds = obsoleteAssociations.map(function(associatedObject) { var obsoleteIds = obsoleteAssociations.map(function(associatedObject) {
associatedObject[self.__factory.identifier] = (newAssociations.length < 1 ? null : self.instance.id); associatedObject[self.association.identifier] = (newAssociations.length < 1 ? null : self.instance.id);
return associatedObject[associationKey]; return associatedObject[associationKey];
}); });
update = {}; update = {};
update[self.__factory.identifier] = null; update[self.association.identifier] = null;
primaryKeys = Object.keys(this.__factory.target.primaryKeys); primaryKeys = Object.keys(this.association.target.primaryKeys);
primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id'; primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id';
updateWhere = {}; updateWhere = {};
updateWhere[primaryKey] = obsoleteIds; updateWhere[primaryKey] = obsoleteIds;
promises.push(this.__factory.target.update( promises.push(this.association.target.update(
update, update,
updateWhere, updateWhere,
Utils._.extend(options, { allowNull: [self.__factory.identifier] }) Utils._.extend(options, { allowNull: [self.association.identifier] })
)); ));
} }
...@@ -76,36 +83,43 @@ module.exports = (function() { ...@@ -76,36 +83,43 @@ module.exports = (function() {
var pkeys = Object.keys(self.instance.Model.primaryKeys) var pkeys = Object.keys(self.instance.Model.primaryKeys)
, pkey = pkeys.length === 1 ? pkeys[0] : 'id'; , pkey = pkeys.length === 1 ? pkeys[0] : 'id';
primaryKeys = Object.keys(this.__factory.target.primaryKeys); primaryKeys = Object.keys(this.association.target.primaryKeys);
primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id'; primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id';
updateWhere = {}; updateWhere = {};
// set the new associations // set the new associations
var unassociatedIds = unassociatedObjects.map(function(associatedObject) { var unassociatedIds = unassociatedObjects.map(function(associatedObject) {
associatedObject[self.__factory.identifier] = self.instance[pkey] || self.instance.id; associatedObject[self.association.identifier] = self.instance[pkey] || self.instance.id;
return associatedObject[associationKey]; return associatedObject[associationKey];
}); });
update = {}; update = {};
update[self.__factory.identifier] = (newAssociations.length < 1 ? null : self.instance[pkey] || self.instance.id); update[self.association.identifier] = (newAssociations.length < 1 ? null : self.instance[pkey] || self.instance.id);
if (this.association.scope) {
Object.keys(this.association.scope).forEach(function (attribute) {
update[attribute] = this.association.scope[attribute];
}.bind(this));
}
updateWhere[primaryKey] = unassociatedIds; updateWhere[primaryKey] = unassociatedIds;
promises.push(this.__factory.target.update( promises.push(this.association.target.update(
update, update,
updateWhere, updateWhere,
Utils._.extend(options, { allowNull: [self.__factory.identifier] }) Utils._.extend(options, { allowNull: [self.association.identifier] })
)); ));
} }
return Utils.Promise.all(promises); return Utils.Promise.all(promises);
}; };
HasManySingleLinked.prototype.injectAdder = function(newAssociation, additionalAttributes) { HasManySingleLinked.prototype.injectAdder = function(newAssociation, options) {
var primaryKeys = Object.keys(this.instance.Model.primaryKeys) newAssociation.set(this.association.identifier, this.instance.get(this.instance.Model.primaryKeyAttribute));
, primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id' if (this.association.scope) {
, options = additionalAttributes; Object.keys(this.association.scope).forEach(function (attribute) {
newAssociation.set(attribute, this.association.scope[attribute]);
newAssociation[this.__factory.identifier] = this.instance[primaryKey]; }.bind(this));
}
return newAssociation.save(options); return newAssociation.save(options);
}; };
......
...@@ -3,22 +3,26 @@ ...@@ -3,22 +3,26 @@
var Utils = require('./../utils') var Utils = require('./../utils')
, Helpers = require('./helpers') , Helpers = require('./helpers')
, _ = require('lodash') , _ = require('lodash')
, Transaction = require('../transaction'); , Association = require('./base')
, Transaction = require('../transaction')
, Model = require('../model');
var HasManySingleLinked = require('./has-many-single-linked') var HasManySingleLinked = require('./has-many-single-linked')
, HasManyDoubleLinked = require('./has-many-double-linked'); , HasManyDoubleLinked = require('./has-many-double-linked');
module.exports = (function() { module.exports = (function() {
var HasMany = function(source, target, options) { var HasMany = function(source, target, options) {
Association.call(this);
var self = this; var self = this;
this.associationType = 'HasMany'; this.associationType = 'HasMany';
this.source = source; this.source = source;
this.target = target; this.target = target;
this.targetAssociation = null; this.targetAssociation = null;
this.options = options; this.options = options || {};
this.sequelize = source.daoFactoryManager.sequelize; this.sequelize = source.daoFactoryManager.sequelize;
this.through = options.through; this.through = options.through;
this.scope = options.scope;
this.isMultiAssociation = true; this.isMultiAssociation = true;
this.isSelfAssociation = this.source === this.target; this.isSelfAssociation = this.source === this.target;
this.doubleLinked = false; this.doubleLinked = false;
...@@ -54,16 +58,22 @@ module.exports = (function() { ...@@ -54,16 +58,22 @@ module.exports = (function() {
} }
} }
if (this.through !== null && !this.through.model) {
this.through = {
model: this.through
};
}
/* /*
* Determine associationAccessor, especially for include options to identify the correct model * Determine associationAccessor, especially for include options to identify the correct model
*/ */
this.associationAccessor = this.as; this.associationAccessor = this.as;
if (!this.associationAccessor) { if (!this.associationAccessor) {
if (typeof this.through === 'string') { if (typeof this.through.model === 'string') {
this.associationAccessor = this.through; this.associationAccessor = this.through.model;
} else if (Object(this.through) === this.through) { } else if (Object(this.through.model) === this.through.model) {
this.associationAccessor = this.through.tableName; this.associationAccessor = this.through.model.tableName;
} else { } else {
this.associationAccessor = this.combinedTableName; this.associationAccessor = this.combinedTableName;
} }
...@@ -74,7 +84,7 @@ module.exports = (function() { ...@@ -74,7 +84,7 @@ module.exports = (function() {
*/ */
if (this.isSelfAssociation) { if (this.isSelfAssociation) {
// check 'as' is defined for many-to-many self-association // check 'as' is defined for many-to-many self-association
if (this.through && this.through !== true && !this.as) { if (this.through && this.through.model !== true && !this.as) {
throw new Error('\'as\' must be defined for many-to-many self-associations'); throw new Error('\'as\' must be defined for many-to-many self-associations');
} }
...@@ -90,12 +100,15 @@ module.exports = (function() { ...@@ -90,12 +100,15 @@ module.exports = (function() {
var paired; var paired;
// If through is default, we determine pairing by the accesor value (i.e. DAOFactory's using as won't pair, but regular ones will) // If through is default, we determine pairing by the accesor value (i.e. DAOFactory's using as won't pair, but regular ones will)
if (self.through === true) { if (self.through.model === true) {
paired = accessor === self.associationAccessor; paired = accessor === self.associationAccessor;
} }
// If through is not default, determine pairing by through value (model/string) // If through is not default, determine pairing by through value (model/string)
else { else {
paired = self.options.through === association.options.through; paired = self.options.through === association.options.through ||
self.options.through === (association.options.through && association.options.through.model) ||
(self.options.through && self.options.through.model) === (association.options.through && association.options.through.model) ||
(self.options.through && self.options.through.model) === association.options.through;
} }
// If paired, set properties identifying both associations as double linked, and allow them to each eachtoerh // If paired, set properties identifying both associations as double linked, and allow them to each eachtoerh
if (paired) { if (paired) {
...@@ -112,22 +125,26 @@ module.exports = (function() { ...@@ -112,22 +125,26 @@ module.exports = (function() {
/* /*
* If we are double linked, and through is either default or a string, we create the through model and set it on both associations * If we are double linked, and through is either default or a string, we create the through model and set it on both associations
*/ */
if (this.doubleLinked && this.through === true) { if (this.doubleLinked && this.through.model === true) {
this.through = this.combinedTableName; this.through.model = this.combinedTableName;
} }
if (typeof this.through === 'string') { if (typeof this.through.model === 'string') {
this.through = this.sequelize.define(this.through, {}, _.extend(this.options, { this.through.model = this.sequelize.define(this.through.model, {}, _.extend(this.options, {
tableName: this.through, tableName: this.through.model,
paranoid: false // A paranoid join table does not make sense paranoid: false // A paranoid join table does not make sense
})); }));
if (this.targetAssociation) { if (this.targetAssociation) {
this.targetAssociation.through = this.through; this.targetAssociation.through.model = this.through.model;
} }
} }
this.options.tableName = this.combinedName = (this.through === Object(this.through) ? this.through.tableName : this.through); if (this.through) {
this.throughModel = this.through.model;
}
this.options.tableName = this.combinedName = (this.through.model === Object(this.through.model) ? this.through.model.tableName : this.through.model);
if (this.as) { if (this.as) {
this.isAliased = true; this.isAliased = true;
...@@ -178,16 +195,16 @@ module.exports = (function() { ...@@ -178,16 +195,16 @@ module.exports = (function() {
// is there already a single sided association between the source and the target? // is there already a single sided association between the source and the target?
// or is the association on the model itself? // or is the association on the model itself?
if ((this.isSelfAssociation && Object(this.through) === this.through) || doubleLinked) { if ((this.isSelfAssociation && Object(this.through.model) === this.through.model) || doubleLinked) {
// We need to remove the keys that 1:M have added // We need to remove the keys that 1:M have added
if (this.isSelfAssociation && doubleLinked) { if (this.isSelfAssociation && doubleLinked) {
if (self.through.rawAttributes[this.targetAssociation.identifier] if (self.through.model.rawAttributes[this.targetAssociation.identifier]
&& self.through.rawAttributes[this.targetAssociation.identifier]._autoGenerated) { && self.through.model.rawAttributes[this.targetAssociation.identifier]._autoGenerated) {
delete self.through.rawAttributes[this.targetAssociation.identifier]; delete self.through.model.rawAttributes[this.targetAssociation.identifier];
} }
if (self.through.rawAttributes[this.targetAssociation.foreignIdentifier] if (self.through.model.rawAttributes[this.targetAssociation.foreignIdentifier]
&& self.through.rawAttributes[this.targetAssociation.foreignIdentifier]._autoGenerated) { && self.through.model.rawAttributes[this.targetAssociation.foreignIdentifier]._autoGenerated) {
delete self.through.rawAttributes[this.targetAssociation.foreignIdentifier]; delete self.through.model.rawAttributes[this.targetAssociation.foreignIdentifier];
} }
} }
...@@ -210,9 +227,9 @@ module.exports = (function() { ...@@ -210,9 +227,9 @@ module.exports = (function() {
} }
// remove any PKs previously defined by sequelize // remove any PKs previously defined by sequelize
Utils._.each(this.through.rawAttributes, function(attribute, attributeName) { Utils._.each(this.through.model.rawAttributes, function(attribute, attributeName) {
if (attribute.primaryKey === true && attribute._autoGenerated === true) { if (attribute.primaryKey === true && attribute._autoGenerated === true) {
delete self.through.rawAttributes[attributeName]; delete self.through.model.rawAttributes[attributeName];
self.targetAssociation.primaryKeyDeleted = true; self.targetAssociation.primaryKeyDeleted = true;
} }
}); });
...@@ -242,27 +259,27 @@ module.exports = (function() { ...@@ -242,27 +259,27 @@ module.exports = (function() {
if (this.targetAssociation.primaryKeyDeleted === true) { if (this.targetAssociation.primaryKeyDeleted === true) {
targetAttribute.primaryKey = sourceAttribute.primaryKey = true; targetAttribute.primaryKey = sourceAttribute.primaryKey = true;
} else { } else if (this.through.unique !== false) {
var uniqueKey = [this.through.tableName, this.identifier, this.foreignIdentifier, 'unique'].join('_'); var uniqueKey = [this.through.model.tableName, this.identifier, this.foreignIdentifier, 'unique'].join('_');
targetAttribute.unique = sourceAttribute.unique = uniqueKey; targetAttribute.unique = sourceAttribute.unique = uniqueKey;
} }
if (!this.through.rawAttributes[this.identifier]) { if (!this.through.model.rawAttributes[this.identifier]) {
this.through.rawAttributes[this.identifier] = { this.through.model.rawAttributes[this.identifier] = {
_autoGenerated: true _autoGenerated: true
}; };
} }
if (!this.through.rawAttributes[this.foreignIdentifier]) { if (!this.through.model.rawAttributes[this.foreignIdentifier]) {
this.through.rawAttributes[this.foreignIdentifier] = { this.through.model.rawAttributes[this.foreignIdentifier] = {
_autoGenerated: true _autoGenerated: true
}; };
} }
this.through.rawAttributes[this.identifier] = Utils._.extend(this.through.rawAttributes[this.identifier], sourceAttribute); this.through.model.rawAttributes[this.identifier] = Utils._.extend(this.through.model.rawAttributes[this.identifier], sourceAttribute);
this.through.rawAttributes[this.foreignIdentifier] = Utils._.extend(this.through.rawAttributes[this.foreignIdentifier], targetAttribute); this.through.model.rawAttributes[this.foreignIdentifier] = Utils._.extend(this.through.model.rawAttributes[this.foreignIdentifier], targetAttribute);
this.through.init(this.through.daoFactoryManager); this.through.model.init(this.through.model.daoFactoryManager);
} else { } else {
var newAttributes = {}; var newAttributes = {};
var constraintOptions = _.clone(this.options); // Create a new options object for use with addForeignKeyConstraints, to avoid polluting this.options in case it is later used for a n:m var constraintOptions = _.clone(this.options); // Create a new options object for use with addForeignKeyConstraints, to avoid polluting this.options in case it is later used for a n:m
...@@ -291,7 +308,7 @@ module.exports = (function() { ...@@ -291,7 +308,7 @@ module.exports = (function() {
obj[this.accessors.get] = function(options, queryOptions) { obj[this.accessors.get] = function(options, queryOptions) {
options = options || {}; options = options || {};
queryOptions = queryOptions || {}; queryOptions = queryOptions || {};
var Class = Object(association.through) === association.through ? HasManyDoubleLinked : HasManySingleLinked; var Class = Object(association.through.model) === association.through.model ? HasManyDoubleLinked : HasManySingleLinked;
return new Class(association, this).injectGetter(options, queryOptions); return new Class(association, this).injectGetter(options, queryOptions);
}; };
...@@ -377,7 +394,7 @@ module.exports = (function() { ...@@ -377,7 +394,7 @@ module.exports = (function() {
return instance[association.accessors.get]({}, { return instance[association.accessors.get]({}, {
transaction: (additionalAttributes || {}).transaction transaction: (additionalAttributes || {}).transaction
}).then(function(oldAssociatedObjects) { }).then(function(oldAssociatedObjects) {
var Class = Object(association.through) === association.through ? HasManyDoubleLinked : HasManySingleLinked; var Class = Object(association.through.model) === association.through.model ? HasManyDoubleLinked : HasManySingleLinked;
return new Class(association, instance).injectSetter(oldAssociatedObjects, newAssociatedObjects, additionalAttributes); return new Class(association, instance).injectSetter(oldAssociatedObjects, newAssociatedObjects, additionalAttributes);
}); });
}; };
...@@ -389,6 +406,13 @@ module.exports = (function() { ...@@ -389,6 +406,13 @@ module.exports = (function() {
var instance = this var instance = this
, primaryKeyAttribute = association.target.primaryKeyAttribute; , primaryKeyAttribute = association.target.primaryKeyAttribute;
additionalAttributes = additionalAttributes || {};
if (association.through && association.through.scope) {
Object.keys(association.through.scope).forEach(function (attribute) {
additionalAttributes[attribute] = association.through.scope[attribute];
});
}
if (Array.isArray(newInstance)) { if (Array.isArray(newInstance)) {
var newInstances = newInstance.map(function(newInstance) { var newInstances = newInstance.map(function(newInstance) {
if (!(newInstance instanceof association.target.Instance)) { if (!(newInstance instanceof association.target.Instance)) {
...@@ -401,7 +425,7 @@ module.exports = (function() { ...@@ -401,7 +425,7 @@ module.exports = (function() {
return newInstance; return newInstance;
}); });
var Class = Object(association.through) === association.through ? HasManyDoubleLinked : HasManySingleLinked; var Class = Object(association.through.model) === association.through.model ? HasManyDoubleLinked : HasManySingleLinked;
return new Class(association, this).injectSetter([], newInstances, additionalAttributes); return new Class(association, this).injectSetter([], newInstances, additionalAttributes);
} else { } else {
if (!(newInstance instanceof association.target.Instance)) { if (!(newInstance instanceof association.target.Instance)) {
...@@ -417,8 +441,8 @@ module.exports = (function() { ...@@ -417,8 +441,8 @@ module.exports = (function() {
}, { }, {
transaction: (additionalAttributes || {}).transaction transaction: (additionalAttributes || {}).transaction
}).then(function(currentAssociatedObjects) { }).then(function(currentAssociatedObjects) {
if (currentAssociatedObjects.length === 0 || Object(association.through) === association.through) { if (currentAssociatedObjects.length === 0 || Object(association.through.model) === association.through.model) {
var Class = Object(association.through) === association.through ? HasManyDoubleLinked : HasManySingleLinked; var Class = Object(association.through.model) === association.through.model ? HasManyDoubleLinked : HasManySingleLinked;
return new Class(association, instance).injectAdder(newInstance, additionalAttributes, !!currentAssociatedObjects.length); return new Class(association, instance).injectAdder(newInstance, additionalAttributes, !!currentAssociatedObjects.length);
} else { } else {
return Utils.Promise.resolve(currentAssociatedObjects[0]); return Utils.Promise.resolve(currentAssociatedObjects[0]);
...@@ -455,7 +479,13 @@ module.exports = (function() { ...@@ -455,7 +479,13 @@ module.exports = (function() {
values = {}; values = {};
} }
if (Object(association.through) === association.through) { if (association.scope) {
Object.keys(association.scope).forEach(function (attribute) {
values[attribute] = association.scope[attribute];
});
}
if (Object(association.through.model) === association.through.model) {
// Create the related model instance // Create the related model instance
return association.target.create(values, fieldsOrOptions).then(function(newAssociatedObject) { return association.target.create(values, fieldsOrOptions).then(function(newAssociatedObject) {
return instance[association.accessors.add](newAssociatedObject, fieldsOrOptions).return(newAssociatedObject); return instance[association.accessors.add](newAssociatedObject, fieldsOrOptions).return(newAssociatedObject);
......
...@@ -227,9 +227,13 @@ Mixin.belongsTo = singleLinked(BelongsTo); ...@@ -227,9 +227,13 @@ Mixin.belongsTo = singleLinked(BelongsTo);
* @param {Model} target * @param {Model} target
* @param {object} [options] * @param {object} [options]
* @param {boolean} [options.hooks=false] Set to true to run before-/afterDestroy hooks when an associated model is deleted because of a cascade. For example if `User.hasOne(Profile, {onDelete: 'cascade', hooks:true})`, the before-/afterDestroy hooks for profile will be called when a user is deleted. Otherwise the profile will be deleted without invoking any hooks * @param {boolean} [options.hooks=false] Set to true to run before-/afterDestroy hooks when an associated model is deleted because of a cascade. For example if `User.hasOne(Profile, {onDelete: 'cascade', hooks:true})`, the before-/afterDestroy hooks for profile will be called when a user is deleted. Otherwise the profile will be deleted without invoking any hooks
* @param {Model|string} [options.through] The name of the table that is used to join source and target in n:m associations. Can also be a sequelize model if you want to define the junction table yourself and add extra attributes to it. * @param {Model|string|object} [options.through] The name of the table that is used to join source and target in n:m associations. Can also be a sequelize model if you want to define the junction table yourself and add extra attributes to it.
* @param {Model} [options.through.model] The model used to join both sides of the N:M association.
* @param {object} [options.through.scope] A key/value set that will be used for association create and find defaults on the through model. (Remember to add the attributes to the through model)
* @param {boolean} [options.through.unique=true] If true a unique key will be generated from the foreign keys used (might want to turn this off and create specific unique keys when using scopes)
* @param {string|object} [options.as] The alias of this model. If you provide a string, it should be plural, and will be singularized using node.inflection. If you want to control the singular version yourself, provide an object with `plural` and `singular` keys. See also the `name` option passed to `sequelize.define`. If you create multiple associations between the same tables, you should provide an alias to be able to distinguish between them. If you provide an alias when creating the assocition, you should provide the same alias when eager loading and when getting assocated models. Defaults to the pluralized name of target * @param {string|object} [options.as] The alias of this model. If you provide a string, it should be plural, and will be singularized using node.inflection. If you want to control the singular version yourself, provide an object with `plural` and `singular` keys. See also the `name` option passed to `sequelize.define`. If you create multiple associations between the same tables, you should provide an alias to be able to distinguish between them. If you provide an alias when creating the assocition, you should provide the same alias when eager loading and when getting assocated models. Defaults to the pluralized name of target
* @param {string|object} [options.foreignKey] The name of the foreign key in the target table / join table or an object representing the type definition for the foreign column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property to set the name of the colum. Defaults to the name of source + primary key of source * @param {string|object} [options.foreignKey] The name of the foreign key in the target table / join table or an object representing the type definition for the foreign column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property to set the name of the colum. Defaults to the name of source + primary key of source
* @param {object} [options.scope] A key/value set that will be used for association create and find defaults on the target. (sqlite not supported for N:M)
* @param {string} [options.onDelete='SET&nbsp;NULL|CASCADE'] Cascade if this is a n:m, and set null if it is a 1:m * @param {string} [options.onDelete='SET&nbsp;NULL|CASCADE'] Cascade if this is a n:m, and set null if it is a 1:m
* @param {string} [options.onUpdate='CASCADE'] * @param {string} [options.onUpdate='CASCADE']
* @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key. * @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key.
......
...@@ -19,6 +19,7 @@ AbstractDialect.prototype.supports = { ...@@ -19,6 +19,7 @@ AbstractDialect.prototype.supports = {
type: false, type: false,
using: true, using: true,
}, },
joinTableDependent: true
}; };
module.exports = AbstractDialect; module.exports = AbstractDialect;
...@@ -536,7 +536,7 @@ module.exports = (function() { ...@@ -536,7 +536,7 @@ module.exports = (function() {
// check if model provided is through table // check if model provided is through table
var association; var association;
if (!as && parentAssociation && parentAssociation.through === model) { if (!as && parentAssociation && parentAssociation.through.model === model) {
association = {as: Utils.singularize(model.tableName, model.options.language)}; association = {as: Utils.singularize(model.tableName, model.options.language)};
} else { } else {
// find applicable association for linking parent to this model // find applicable association for linking parent to this model
...@@ -874,6 +874,14 @@ module.exports = (function() { ...@@ -874,6 +874,14 @@ module.exports = (function() {
targetJoinOn = self.quoteIdentifier(tableTarget) + '.' + self.quoteIdentifier(attrTarget) + ' = '; targetJoinOn = self.quoteIdentifier(tableTarget) + '.' + self.quoteIdentifier(attrTarget) + ' = ';
targetJoinOn += self.quoteIdentifier(throughAs) + '.' + self.quoteIdentifier(identTarget); targetJoinOn += self.quoteIdentifier(throughAs) + '.' + self.quoteIdentifier(identTarget);
if (self._dialect.supports.joinTableDependent) {
// Generate a wrapped join so that the through table join can be dependent on the target join
joinQueryItem += joinType + '(';
joinQueryItem += self.quoteTable(throughTable, throughAs);
joinQueryItem += joinType + self.quoteTable(table, as) + ' ON ';
joinQueryItem += targetJoinOn;
joinQueryItem += ') ON '+sourceJoinOn;
} else {
// Generate join SQL for left side of through // Generate join SQL for left side of through
joinQueryItem += joinType + self.quoteTable(throughTable, throughAs) + ' ON '; joinQueryItem += joinType + self.quoteTable(throughTable, throughAs) + ' ON ';
joinQueryItem += sourceJoinOn; joinQueryItem += sourceJoinOn;
...@@ -881,6 +889,7 @@ module.exports = (function() { ...@@ -881,6 +889,7 @@ module.exports = (function() {
// Generate join SQL for right side of through // Generate join SQL for right side of through
joinQueryItem += joinType + self.quoteTable(table, as) + ' ON '; joinQueryItem += joinType + self.quoteTable(table, as) + ' ON ';
joinQueryItem += targetJoinOn; joinQueryItem += targetJoinOn;
}
if (include.where) { if (include.where) {
targetWhere = self.getWhereConditions(include.where, self.sequelize.literal(self.quoteIdentifier(as)), include.model, whereOptions); targetWhere = self.getWhereConditions(include.where, self.sequelize.literal(self.quoteIdentifier(as)), include.model, whereOptions);
...@@ -901,8 +910,8 @@ module.exports = (function() { ...@@ -901,8 +910,8 @@ module.exports = (function() {
} }
} }
} else { } else {
var left = association.associationType === 'BelongsTo' ? association.target : include.association.source var left = association.associationType === 'BelongsTo' ? association.target : association.source
, primaryKeysLeft = association.associationType === 'BelongsTo' ? left.primaryKeyAttributes : left.primaryKeyAttributes , primaryKeysLeft = left.primaryKeyAttributes
, tableLeft = association.associationType === 'BelongsTo' ? as : parentTable , tableLeft = association.associationType === 'BelongsTo' ? as : parentTable
, attrLeft = primaryKeysLeft[0] , attrLeft = primaryKeysLeft[0]
, tableRight = association.associationType === 'BelongsTo' ? parentTable : as , tableRight = association.associationType === 'BelongsTo' ? parentTable : as
...@@ -1012,12 +1021,14 @@ module.exports = (function() { ...@@ -1012,12 +1021,14 @@ module.exports = (function() {
// Add WHERE to sub or main query // Add WHERE to sub or main query
if (options.hasOwnProperty('where')) { if (options.hasOwnProperty('where')) {
options.where = this.getWhereConditions(options.where, mainTableAs || tableName, model, options); options.where = this.getWhereConditions(options.where, mainTableAs || tableName, model, options);
if (options.where) {
if (subQuery) { if (subQuery) {
subQueryItems.push(' WHERE ' + options.where); subQueryItems.push(' WHERE ' + options.where);
} else { } else {
mainQueryItems.push(' WHERE ' + options.where); mainQueryItems.push(' WHERE ' + options.where);
} }
} }
}
// Add GROUP BY to sub or main query // Add GROUP BY to sub or main query
if (options.group) { if (options.group) {
...@@ -1211,7 +1222,7 @@ module.exports = (function() { ...@@ -1211,7 +1222,7 @@ module.exports = (function() {
return self.getWhereConditions(arg, tableName, factory, options, prepend); return self.getWhereConditions(arg, tableName, factory, options, prepend);
}).join(connector); }).join(connector);
result = '(' + result + ')'; result = result.length && '(' + result + ')' || undefined;
} else if (smth instanceof Utils.where) { } else if (smth instanceof Utils.where) {
var value = smth.logic var value = smth.logic
, key = this.quoteTable(smth.attribute.Model.name) + '.' + this.quoteIdentifier(smth.attribute.fieldName) , key = this.quoteTable(smth.attribute.Model.name) + '.' + this.quoteIdentifier(smth.attribute.fieldName)
...@@ -1272,6 +1283,8 @@ module.exports = (function() { ...@@ -1272,6 +1283,8 @@ module.exports = (function() {
} else { } else {
result = Utils.format(smth, this.dialect); result = Utils.format(smth, this.dialect);
} }
} else if (smth === null) {
result = '1=1';
} }
return result ? result : '1=1'; return result ? result : '1=1';
...@@ -1395,14 +1408,14 @@ module.exports = (function() { ...@@ -1395,14 +1408,14 @@ module.exports = (function() {
joins += ' LEFT JOIN ' + self.quoteIdentifiers(association.target.tableName); joins += ' LEFT JOIN ' + self.quoteIdentifiers(association.target.tableName);
joins += ' ON ' + self.quoteIdentifiers(association.source.tableName + '.' + association.identifier); joins += ' ON ' + self.quoteIdentifiers(association.source.tableName + '.' + association.identifier);
joins += ' = ' + self.quoteIdentifiers(association.target.tableName + '.' + association.target.autoIncrementField); joins += ' = ' + self.quoteIdentifiers(association.target.tableName + '.' + association.target.autoIncrementField);
} else if (Object(association.through) === association.through) { } else if (Object(association.through.model) === association.through.model) {
joinedTables[association.through.tableName] = true; joinedTables[association.through.model.tableName] = true;
joins += ' LEFT JOIN ' + self.quoteIdentifiers(association.through.tableName); joins += ' LEFT JOIN ' + self.quoteIdentifiers(association.through.model.tableName);
joins += ' ON ' + self.quoteIdentifiers(association.source.tableName + '.' + association.source.autoIncrementField); joins += ' ON ' + self.quoteIdentifiers(association.source.tableName + '.' + association.source.autoIncrementField);
joins += ' = ' + self.quoteIdentifiers(association.through.tableName + '.' + association.identifier); joins += ' = ' + self.quoteIdentifiers(association.through.model.tableName + '.' + association.identifier);
joins += ' LEFT JOIN ' + self.quoteIdentifiers(association.target.tableName); joins += ' LEFT JOIN ' + self.quoteIdentifiers(association.target.tableName);
joins += ' ON ' + self.quoteIdentifiers(association.through.tableName + '.' + association.foreignIdentifier); joins += ' ON ' + self.quoteIdentifiers(association.through.model.tableName + '.' + association.foreignIdentifier);
joins += ' = ' + self.quoteIdentifiers(association.target.tableName + '.' + association.target.autoIncrementField); joins += ' = ' + self.quoteIdentifiers(association.target.tableName + '.' + association.target.autoIncrementField);
} else { } else {
joins += ' LEFT JOIN ' + self.quoteIdentifiers(association.target.tableName); joins += ' LEFT JOIN ' + self.quoteIdentifiers(association.target.tableName);
......
...@@ -341,7 +341,7 @@ module.exports = (function() { ...@@ -341,7 +341,7 @@ module.exports = (function() {
var replacements = { var replacements = {
table: this.quoteIdentifiers(tableName), table: this.quoteIdentifiers(tableName),
where: this.getWhereConditions(where), where: this.getWhereConditions(where) || '1=1',
limit: !!options.limit ? ' LIMIT ' + this.escape(options.limit) : '', limit: !!options.limit ? ' LIMIT ' + this.escape(options.limit) : '',
primaryKeys: primaryKeys[tableName].length > 1 ? '(' + pks + ')' : pks, primaryKeys: primaryKeys[tableName].length > 1 ? '(' + pks + ')' : pks,
primaryKeysSelection: pks primaryKeysSelection: pks
......
...@@ -15,7 +15,8 @@ SqliteDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.suppor ...@@ -15,7 +15,8 @@ SqliteDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.suppor
'DEFAULT VALUES': true, 'DEFAULT VALUES': true,
index: { index: {
using: false using: false
} },
joinTableDependent: false
}); });
SqliteDialect.prototype.Query = Query; SqliteDialect.prototype.Query = Query;
......
...@@ -716,12 +716,12 @@ module.exports = (function() { ...@@ -716,12 +716,12 @@ module.exports = (function() {
//right now, the caller (has-many-double-linked) is in charge of the where clause //right now, the caller (has-many-double-linked) is in charge of the where clause
Model.prototype.findAllJoin = function(joinTableName, options, queryOptions) { Model.prototype.findAllJoin = function(joinTableName, options, queryOptions) {
var optcpy = Utils._.clone(options); options = optClone(options || {});
optcpy.attributes = optcpy.attributes || [this.QueryInterface.quoteTable(this.name) + '.*']; options.attributes = options.attributes || [this.QueryInterface.quoteTable(this.name) + '.*'];
// whereCollection is used for non-primary key updates // whereCollection is used for non-primary key updates
this.options.whereCollection = optcpy.where || null; this.options.whereCollection = options.where || null;
return this.QueryInterface.select(this, [[this.getTableName(), this.name], joinTableName], optcpy, Utils._.defaults({ return this.QueryInterface.select(this, [[this.getTableName(), this.name], joinTableName], options, Utils._.defaults({
type: QueryTypes.SELECT type: QueryTypes.SELECT
}, queryOptions, { transaction: (options || {}).transaction })); }, queryOptions, { transaction: (options || {}).transaction }));
}; };
...@@ -1742,7 +1742,7 @@ module.exports = (function() { ...@@ -1742,7 +1742,7 @@ module.exports = (function() {
tableNames[include.model.getTableName()] = true; tableNames[include.model.getTableName()] = true;
if (include.hasOwnProperty('attributes')) { if (include.attributes) {
include.originalAttributes = include.attributes.slice(0); include.originalAttributes = include.attributes.slice(0);
include.model.primaryKeyAttributes.forEach(function(attr) { include.model.primaryKeyAttributes.forEach(function(attr) {
if (include.attributes.indexOf(attr) === -1) { if (include.attributes.indexOf(attr) === -1) {
...@@ -1767,13 +1767,13 @@ module.exports = (function() { ...@@ -1767,13 +1767,13 @@ module.exports = (function() {
include.as = association.as; include.as = association.as;
// If through, we create a pseudo child include, to ease our parsing later on // If through, we create a pseudo child include, to ease our parsing later on
if (Object(include.association.through) === include.association.through) { if (include.association.through && Object(include.association.through.model) === include.association.through.model) {
if (!include.include) include.include = []; if (!include.include) include.include = [];
var through = include.association.through; var through = include.association.through;
include.through = Utils._.defaults(include.through || {}, { include.through = Utils._.defaults(include.through || {}, {
model: through, model: through.model,
as: Utils.singularize(through.tableName), as: Utils.singularize(through.model.tableName),
association: { association: {
isSingleAssociation: true isSingleAssociation: true
}, },
...@@ -1788,6 +1788,10 @@ module.exports = (function() { ...@@ -1788,6 +1788,10 @@ module.exports = (function() {
include.required = !!include.where; include.required = !!include.where;
} }
if (include.association.scope) {
include.where = include.where ? new Util.and(include.where, include.association.scope) : include.association.scope;
}
// Validate child includes // Validate child includes
if (include.hasOwnProperty('include')) { if (include.hasOwnProperty('include')) {
validateIncludedElements.call(include.model, include, tableNames); validateIncludedElements.call(include.model, include, tableNames);
......
...@@ -894,9 +894,9 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { ...@@ -894,9 +894,9 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
}).then(function(projects) { }).then(function(projects) {
expect(projects).to.have.length(1); expect(projects).to.have.length(1);
var project = projects[0]; var project = projects[0];
expect(project.ProjectUsers).to.be.defined; expect(project.ProjectUser).to.be.defined;
expect(project.status).not.to.exist; expect(project.status).not.to.exist;
expect(project.ProjectUsers.status).to.equal('active'); expect(project.ProjectUser.status).to.equal('active');
}); });
}); });
}); });
...@@ -1101,6 +1101,9 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { ...@@ -1101,6 +1101,9 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
} }
); );
}).then(function (userGroups) { }).then(function (userGroups) {
userGroups.sort(function (a, b) {
return a.userId < b.userId ? - 1 : 1;
});
expect(userGroups[0].userId).to.equal(1); expect(userGroups[0].userId).to.equal(1);
expect(userGroups[0].isAdmin).to.be.ok; expect(userGroups[0].isAdmin).to.be.ok;
expect(userGroups[1].userId).to.equal(2); expect(userGroups[1].userId).to.equal(2);
...@@ -1363,7 +1366,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { ...@@ -1363,7 +1366,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
expect(associationName).not.to.equal(this.User.tableName); expect(associationName).not.to.equal(this.User.tableName);
expect(associationName).not.to.equal(this.Task.tableName); expect(associationName).not.to.equal(this.Task.tableName);
var through = this.User.associations[associationName].through; var through = this.User.associations[associationName].through.model;
if (typeof through !== 'undefined') { if (typeof through !== 'undefined') {
expect(through.tableName).to.equal(associationName); expect(through.tableName).to.equal(associationName);
} }
...@@ -1390,7 +1393,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { ...@@ -1390,7 +1393,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
expect(ParanoidTask.options.paranoid).to.be.ok; expect(ParanoidTask.options.paranoid).to.be.ok;
_.forEach(ParanoidUser.associations, function (association) { _.forEach(ParanoidUser.associations, function (association) {
expect(association.through.options.paranoid).not.to.be.ok; expect(association.through.model.options.paranoid).not.to.be.ok;
}); });
}); });
}); });
...@@ -1472,7 +1475,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { ...@@ -1472,7 +1475,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
_.each([this.UserTasks, this.UserTasks2], function (model) { _.each([this.UserTasks, this.UserTasks2], function (model) {
fk = Object.keys(model.options.uniqueKeys)[0]; fk = Object.keys(model.options.uniqueKeys)[0];
expect(model.options.uniqueKeys[fk].fields).to.deep.equal([ 'TaskId', 'UserId' ]); expect(model.options.uniqueKeys[fk].fields.sort()).to.deep.equal([ 'TaskId', 'UserId' ]);
}); });
}); });
...@@ -1538,8 +1541,8 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { ...@@ -1538,8 +1541,8 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
expect(project.UserProjects).to.be.defined; expect(project.UserProjects).to.be.defined;
expect(project.status).not.to.exist; expect(project.status).not.to.exist;
expect(project.UserProjects.status).to.equal('active'); expect(project.UserProject.status).to.equal('active');
expect(project.UserProjects.data).to.equal(42); expect(project.UserProject.data).to.equal(42);
}); });
}); });
...@@ -1556,8 +1559,8 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { ...@@ -1556,8 +1559,8 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
expect(project.UserProjects).to.be.defined; expect(project.UserProjects).to.be.defined;
expect(project.status).not.to.exist; expect(project.status).not.to.exist;
expect(project.UserProjects.status).to.equal('active'); expect(project.UserProject.status).to.equal('active');
expect(project.UserProjects.data).not.to.exist; expect(project.UserProject.data).not.to.exist;
}); });
}); });
}); });
...@@ -1841,9 +1844,9 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { ...@@ -1841,9 +1844,9 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
User.hasMany(Group, { as: 'MyGroups', through: 'group_user'}); User.hasMany(Group, { as: 'MyGroups', through: 'group_user'});
Group.hasMany(User, { as: 'MyUsers', through: 'group_user'}); Group.hasMany(User, { as: 'MyUsers', through: 'group_user'});
expect(Group.associations.MyUsers.through === User.associations.MyGroups.through); expect(Group.associations.MyUsers.through.model === User.associations.MyGroups.through.model);
expect(Group.associations.MyUsers.through.rawAttributes.UserId).to.exist; expect(Group.associations.MyUsers.through.model.rawAttributes.UserId).to.exist;
expect(Group.associations.MyUsers.through.rawAttributes.GroupId).to.exist; expect(Group.associations.MyUsers.through.model.rawAttributes.GroupId).to.exist;
}); });
it("correctly identifies its counterpart when through is a model", function () { it("correctly identifies its counterpart when through is a model", function () {
...@@ -1854,10 +1857,10 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { ...@@ -1854,10 +1857,10 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
User.hasMany(Group, { as: 'MyGroups', through: UserGroup}); User.hasMany(Group, { as: 'MyGroups', through: UserGroup});
Group.hasMany(User, { as: 'MyUsers', through: UserGroup}); Group.hasMany(User, { as: 'MyUsers', through: UserGroup});
expect(Group.associations.MyUsers.through === User.associations.MyGroups.through); expect(Group.associations.MyUsers.through.model === User.associations.MyGroups.through.model);
expect(Group.associations.MyUsers.through.rawAttributes.UserId).to.exist; expect(Group.associations.MyUsers.through.model.rawAttributes.UserId).to.exist;
expect(Group.associations.MyUsers.through.rawAttributes.GroupId).to.exist; expect(Group.associations.MyUsers.through.model.rawAttributes.GroupId).to.exist;
}); });
}); });
}); });
...@@ -2234,15 +2237,15 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { ...@@ -2234,15 +2237,15 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
expect(Task.rawAttributes.uid).not.to.be.defined; expect(Task.rawAttributes.uid).not.to.be.defined;
expect(Task.associations.tasksusers.through.rawAttributes.taskId).to.be.defined; expect(Task.associations.tasksusers.through.model.rawAttributes.taskId).to.be.defined;
expect(Task.associations.tasksusers.through.rawAttributes.taskId.allowNull).to.be.false; expect(Task.associations.tasksusers.through.model.rawAttributes.taskId.allowNull).to.be.false;
expect(Task.associations.tasksusers.through.rawAttributes.taskId.references).to.equal(Task.getTableName()); expect(Task.associations.tasksusers.through.model.rawAttributes.taskId.references).to.equal(Task.getTableName());
expect(Task.associations.tasksusers.through.rawAttributes.taskId.referencesKey).to.equal('id'); expect(Task.associations.tasksusers.through.model.rawAttributes.taskId.referencesKey).to.equal('id');
expect(Task.associations.tasksusers.through.rawAttributes.uid).to.be.defined; expect(Task.associations.tasksusers.through.model.rawAttributes.uid).to.be.defined;
expect(Task.associations.tasksusers.through.rawAttributes.uid.allowNull).to.be.false; expect(Task.associations.tasksusers.through.model.rawAttributes.uid.allowNull).to.be.false;
expect(Task.associations.tasksusers.through.rawAttributes.uid.references).to.equal(User.getTableName()); expect(Task.associations.tasksusers.through.model.rawAttributes.uid.references).to.equal(User.getTableName());
expect(Task.associations.tasksusers.through.rawAttributes.uid.referencesKey).to.equal('id'); expect(Task.associations.tasksusers.through.model.rawAttributes.uid.referencesKey).to.equal('id');
}); });
it('works when taking a column directly from the object', function () { it('works when taking a column directly from the object', function () {
......
...@@ -217,7 +217,6 @@ describe(Support.getTestDialectTeaser("Multiple Level Filters"), function() { ...@@ -217,7 +217,6 @@ describe(Support.getTestDialectTeaser("Multiple Level Filters"), function() {
User.find(1).success(function(user){ User.find(1).success(function(user){
Project.find(1).success(function(project){ Project.find(1).success(function(project){
user.setProjects([project]).success(function(){ user.setProjects([project]).success(function(){
User.find(2).success(function(user){ User.find(2).success(function(user){
Project.find(2).success(function(project){ Project.find(2).success(function(project){
user.setProjects([project]).success(function(){ user.setProjects([project]).success(function(){
...@@ -246,6 +245,6 @@ describe(Support.getTestDialectTeaser("Multiple Level Filters"), function() { ...@@ -246,6 +245,6 @@ describe(Support.getTestDialectTeaser("Multiple Level Filters"), function() {
}); });
}); });
}); });
}) });
}) });
}) });
"use strict";
/* jshint camelcase: false, expr: true */
var chai = require('chai')
, expect = chai.expect
, Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + "/../../lib/data-types")
, Sequelize = require('../../index')
, Promise = Sequelize.Promise
, assert = require('assert');
chai.config.includeStack = true;
describe(Support.getTestDialectTeaser("associations"), function() {
describe('scope', function () {
beforeEach(function () {
this.Post = this.sequelize.define('post', {});
this.Image = this.sequelize.define('image', {});
this.Question = this.sequelize.define('question', {});
this.Comment = this.sequelize.define('comment', {
title: Sequelize.STRING,
commentable: Sequelize.STRING,
commentable_id: Sequelize.INTEGER
}, {
instanceMethods: {
getItem: function() {
return this['get'+this.get('commentable').substr(0, 1).toUpperCase()+this.get('commentable').substr(1)]();
}
}
});
this.Post.hasMany(this.Comment, {
foreignKey: 'commentable_id',
scope: {
commentable: 'post'
}
});
this.Comment.belongsTo(this.Post, {
foreignKey: 'commentable_id',
as: 'post'
});
this.Image.hasMany(this.Comment, {
foreignKey: 'commentable_id',
scope: {
commentable: 'image'
}
});
this.Comment.belongsTo(this.Image, {
foreignKey: 'commentable_id',
as: 'image'
});
this.Question.hasMany(this.Comment, {
foreignKey: 'commentable_id',
scope: {
commentable: 'question'
}
});
this.Comment.belongsTo(this.Question, {
foreignKey: 'commentable_id',
as: 'question'
});
});
describe('1:M', function () {
it('should create, find and include associations with scope values', function () {
var self = this;
return this.sequelize.sync({force: true}).then(function () {
return Promise.join(
self.Post.create(),
self.Image.create(),
self.Question.create(),
self.Comment.create({
title: 'I am a image comment'
}),
self.Comment.create({
title: 'I am a question comment'
})
);
}).bind(this).spread(function (post, image, question, commentA, commentB) {
this.post = post;
this.image = image;
this.question = question;
return Promise.join(
post.createComment({
title: 'I am a post comment'
}),
image.addComment(commentA),
question.setComments([commentB])
);
}).then(function () {
return self.Comment.findAll();
}).then(function (comments) {
comments.forEach(function (comment) {
expect(comment.get('commentable')).to.be.ok;
});
expect(comments.map(function (comment) {
return comment.get('commentable');
}).sort()).to.deep.equal(['image', 'post', 'question']);
}).then(function () {
return Promise.join(
this.post.getComments(),
this.image.getComments(),
this.question.getComments()
);
}).spread(function (postComments, imageComments, questionComments) {
expect(postComments.length).to.equal(1);
expect(postComments[0].get('title')).to.equal('I am a post comment');
expect(imageComments.length).to.equal(1);
expect(imageComments[0].get('title')).to.equal('I am a image comment');
expect(questionComments.length).to.equal(1);
expect(questionComments[0].get('title')).to.equal('I am a question comment');
return [postComments[0], imageComments[0], questionComments[0]];
}).spread(function (postComment, imageComment, questionComment) {
return Promise.join(
postComment.getItem(),
imageComment.getItem(),
questionComment.getItem()
);
}).spread(function (post, image, question) {
expect(post.Model).to.equal(self.Post);
expect(image.Model).to.equal(self.Image);
expect(question.Model).to.equal(self.Question);
}).then(function () {
return Promise.join(
self.Post.find({
include: [self.Comment]
}),
self.Image.find({
include: [self.Comment]
}),
self.Question.find({
include: [self.Comment]
})
);
}).spread(function (post, image, question) {
expect(post.comments.length).to.equal(1);
expect(post.comments[0].get('title')).to.equal('I am a post comment');
expect(image.comments.length).to.equal(1);
expect(image.comments[0].get('title')).to.equal('I am a image comment');
expect(question.comments.length).to.equal(1);
expect(question.comments[0].get('title')).to.equal('I am a question comment');
});
});
});
if (Support.getTestDialect() !== 'sqlite') {
describe('N:M', function () {
describe('on the target', function () {
beforeEach(function () {
this.Post = this.sequelize.define('post', {});
this.Tag = this.sequelize.define('tag', {
type: DataTypes.STRING
});
this.PostTag = this.sequelize.define('post_tag');
this.Tag.hasMany(this.Post, {through: this.PostTag});
this.Post.hasMany(this.Tag, {as: 'categories', through: this.PostTag, scope: { type: 'category' }});
this.Post.hasMany(this.Tag, {as: 'tags', through: this.PostTag, scope: { type: 'tag' }});
});
it('should create, find and include associations with scope values', function () {
var self = this;
return Promise.join(
self.Post.sync({force: true}),
self.Tag.sync({force: true})
).bind(this).then(function () {
return self.PostTag.sync({force: true});
}).then(function () {
return Promise.join(
self.Post.create(),
self.Post.create(),
self.Post.create(),
self.Tag.create({type: 'category'}),
self.Tag.create({type: 'category'}),
self.Tag.create({type: 'tag'}),
self.Tag.create({type: 'tag'})
);
}).spread(function (postA, postB, postC, categoryA, categoryB, tagA, tagB) {
this.postA = postA;
this.postB = postB;
this.postC = postC;
return Promise.join(
postA.addCategory(categoryA),
postB.setCategories([categoryB]),
postC.createCategory(),
postA.createTag(),
postB.addTag(tagA),
postC.setTags([tagB])
);
}).then(function () {
return Promise.join(
this.postA.getCategories(),
this.postA.getTags(),
this.postB.getCategories(),
this.postB.getTags(),
this.postC.getCategories(),
this.postC.getTags()
);
}).spread(function (postACategories, postATags, postBCategories, postBTags, postCCategories, postCTags) {
expect(postACategories.length).to.equal(1);
expect(postATags.length).to.equal(1);
expect(postBCategories.length).to.equal(1);
expect(postBTags.length).to.equal(1);
expect(postCCategories.length).to.equal(1);
expect(postCTags.length).to.equal(1);
expect(postACategories[0].get('type')).to.equal('category');
expect(postATags[0].get('type')).to.equal('tag');
expect(postBCategories[0].get('type')).to.equal('category');
expect(postBTags[0].get('type')).to.equal('tag');
expect(postCCategories[0].get('type')).to.equal('category');
expect(postCTags[0].get('type')).to.equal('tag');
}).then(function () {
return Promise.join(
self.Post.find({
where: {
id: self.postA.get('id')
},
include: [
{model: self.Tag, as: 'tags'},
{model: self.Tag, as: 'categories'}
]
}),
self.Post.find({
where: {
id: self.postB.get('id')
},
include: [
{model: self.Tag, as: 'tags'},
{model: self.Tag, as: 'categories'}
]
}),
self.Post.find({
where: {
id: self.postC.get('id')
},
include: [
{model: self.Tag, as: 'tags'},
{model: self.Tag, as: 'categories'}
]
})
);
}).spread(function (postA, postB, postC) {
expect(postA.get('categories').length).to.equal(1);
expect(postA.get('tags').length).to.equal(1);
expect(postB.get('categories').length).to.equal(1);
expect(postB.get('tags').length).to.equal(1);
expect(postC.get('categories').length).to.equal(1);
expect(postC.get('tags').length).to.equal(1);
expect(postA.get('categories')[0].get('type')).to.equal('category');
expect(postA.get('tags')[0].get('type')).to.equal('tag');
expect(postB.get('categories')[0].get('type')).to.equal('category');
expect(postB.get('tags')[0].get('type')).to.equal('tag');
expect(postC.get('categories')[0].get('type')).to.equal('category');
expect(postC.get('tags')[0].get('type')).to.equal('tag');
});
});
});
describe('on the through model', function () {
beforeEach(function () {
this.Post = this.sequelize.define('post', {});
this.Image = this.sequelize.define('image', {});
this.Question = this.sequelize.define('question', {});
this.ItemTag = this.sequelize.define('item_tag', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
tag_id: {
type: DataTypes.INTEGER,
unique: 'item_tag_taggable'
},
taggable: {
type: DataTypes.STRING,
unique: 'item_tag_taggable'
},
taggable_id: {
type: DataTypes.INTEGER,
unique: 'item_tag_taggable',
references: null
}
});
this.Tag = this.sequelize.define('tag', {
name: DataTypes.STRING
});
this.Post.hasMany(this.Tag, {
through: {
model: this.ItemTag,
unique: false,
scope: {
taggable: 'post'
}
},
foreignKey: 'taggable_id',
constraints: false
});
this.Tag.hasMany(this.Post, {
through: {
model: this.ItemTag,
unique: false
},
foreignKey: 'tag_id'
});
this.Image.hasMany(this.Tag, {
through: {
model: this.ItemTag,
unique: false,
scope: {
taggable: 'image'
}
},
foreignKey: 'taggable_id',
constraints: false
});
this.Tag.hasMany(this.Image, {
through: {
model: this.ItemTag,
unique: false
},
foreignKey: 'tag_id'
});
this.Question.hasMany(this.Tag, {
through: {
model: this.ItemTag,
unique: false,
scope: {
taggable: 'question'
}
},
foreignKey: 'taggable_id',
constraints: false
});
this.Tag.hasMany(this.Question, {
through: {
model: this.ItemTag,
unique: false
},
foreignKey: 'tag_id'
});
});
it('should create, find and include associations with scope values', function () {
var self = this;
return Promise.join(
this.Post.sync({force: true}),
this.Image.sync({force: true}),
this.Question.sync({force: true}),
this.Tag.sync({force: true})
).bind(this).then(function () {
return this.ItemTag.sync({force: true});
}).then(function () {
return Promise.join(
this.Post.create(),
this.Image.create(),
this.Question.create(),
this.Tag.create({name: 'tagA'}),
this.Tag.create({name: 'tagB'}),
this.Tag.create({name: 'tagC'})
);
}).spread(function (post, image, question, tagA, tagB, tagC) {
this.post = post;
this.image = image;
this.question = question;
return Promise.join(
post.setTags([tagA]).then(function () {
return Promise.join(
post.createTag({name: 'postTag'}),
post.addTag(tagB)
);
}),
image.setTags([tagB]).then(function () {
return Promise.join(
image.createTag({name: 'imageTag'}),
image.addTag(tagC)
);
}),
question.setTags([tagC]).then(function () {
return Promise.join(
question.createTag({name: 'questionTag'}),
question.addTag(tagA)
);
})
);
}).then(function () {
return Promise.join(
this.post.getTags(),
this.image.getTags(),
this.question.getTags()
).spread(function (postTags, imageTags, questionTags) {
expect(postTags.length).to.equal(3);
expect(imageTags.length).to.equal(3);
expect(questionTags.length).to.equal(3);
expect(postTags.map(function (tag) {
return tag.name;
}).sort()).to.deep.equal(['postTag', 'tagA', 'tagB']);
expect(imageTags.map(function (tag) {
return tag.name;
}).sort()).to.deep.equal(['imageTag', 'tagB', 'tagC']);
expect(questionTags.map(function (tag) {
return tag.name;
}).sort()).to.deep.equal(['questionTag', 'tagA', 'tagC']);
});
});
});
});
});
}
});
});
\ No newline at end of file
...@@ -133,7 +133,7 @@ describe(Support.getTestDialectTeaser("Self"), function() { ...@@ -133,7 +133,7 @@ describe(Support.getTestDialectTeaser("Self"), function() {
}); });
}).then(function () { }).then(function () {
return this.john.getChildren().on('sql', function(sql) { return this.john.getChildren().on('sql', function(sql) {
var whereClause = sql.split('WHERE')[1]; // look only in the whereClause var whereClause = sql.split('FROM')[1]; // look only in the whereClause
expect(whereClause).to.have.string('preexisting_child'); expect(whereClause).to.have.string('preexisting_child');
expect(whereClause).to.have.string('preexisting_parent'); expect(whereClause).to.have.string('preexisting_parent');
}); });
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!