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

Commit e5b3c0cd by Mick Hansen

Merge pull request #3929 from sequelize/rewriteAssoc

Rewrite hasMany and belongsToMany
2 parents 6e899e27 350980c8
var Association = function () {};
'use strict';
var Association = function() {};
// Normalize input - may be array or single obj, instance or primary key - convert it to an array of built objects
Association.prototype.toInstanceArray = function (objs) {
if (!Array.isArray(objs)) {
objs = [objs];
}
return objs.map(function(obj) {
if (!(obj instanceof this.target.Instance)) {
var tmpInstance = {};
tmpInstance[this.target.primaryKeyAttribute] = obj;
return this.target.build(tmpInstance, {
isNewRecord: false
});
}
return obj;
}, this);
};
module.exports = Association;
......@@ -69,7 +69,7 @@ var BelongsToMany = function(source, target, options) {
/*
* Default/generated foreign/other keys
*/
if (Utils._.isObject(this.options.foreignKey)) {
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else {
......@@ -78,16 +78,16 @@ var BelongsToMany = function(source, target, options) {
}
this.foreignKeyAttribute = {};
this.foreignKey = this.options.foreignKey || Utils._.camelizeIf(
this.foreignKey = this.options.foreignKey || _.camelizeIf(
[
Utils._.underscoredIf(this.source.options.name.singular, this.source.options.underscored),
_.underscoredIf(this.source.options.name.singular, this.source.options.underscored),
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
);
}
if (Utils._.isObject(this.options.otherKey)) {
if (_.isObject(this.options.otherKey)) {
this.otherKeyAttribute = this.options.otherKey;
this.otherKey = this.otherKeyAttribute.name || this.otherKeyAttribute.fieldName;
} else {
......@@ -96,9 +96,9 @@ var BelongsToMany = function(source, target, options) {
}
this.otherKeyAttribute = {};
this.otherKey = this.options.otherKey || Utils._.camelizeIf(
this.otherKey = this.options.otherKey || _.camelizeIf(
[
Utils._.underscoredIf(
_.underscoredIf(
this.isSelfAssociation ?
Utils.singularize(this.as) :
this.target.options.name.singular,
......@@ -189,7 +189,7 @@ BelongsToMany.prototype.injectAttributes = function() {
this.foreignIdentifier = this.otherKey;
// remove any PKs previously defined by sequelize
Utils._.each(this.through.model.rawAttributes, function(attribute, attributeName) {
_.each(this.through.model.rawAttributes, function(attribute, attributeName) {
if (attribute.primaryKey === true && attribute._autoGenerated === true) {
delete self.through.model.rawAttributes[attributeName];
self.primaryKeyDeleted = true;
......@@ -202,8 +202,8 @@ BelongsToMany.prototype.injectAttributes = function() {
, targetKey = this.target.rawAttributes[this.target.primaryKeyAttribute]
, targetKeyType = targetKey.type
, targetKeyField = targetKey.field || this.target.primaryKeyAttribute
, sourceAttribute = Utils._.defaults(this.foreignKeyAttribute, { type: sourceKeyType })
, targetAttribute = Utils._.defaults(this.otherKeyAttribute, { type: targetKeyType });
, sourceAttribute = _.defaults(this.foreignKeyAttribute, { type: sourceKeyType })
, targetAttribute = _.defaults(this.otherKeyAttribute, { type: targetKeyType });
if (this.primaryKeyDeleted === true) {
targetAttribute.primaryKey = sourceAttribute.primaryKey = true;
......@@ -248,8 +248,8 @@ BelongsToMany.prototype.injectAttributes = function() {
if (!targetAttribute.onUpdate) targetAttribute.onUpdate = 'CASCADE';
}
this.through.model.rawAttributes[this.identifier] = Utils._.extend(this.through.model.rawAttributes[this.identifier], sourceAttribute);
this.through.model.rawAttributes[this.foreignIdentifier] = Utils._.extend(this.through.model.rawAttributes[this.foreignIdentifier], targetAttribute);
this.through.model.rawAttributes[this.identifier] = _.extend(this.through.model.rawAttributes[this.identifier], sourceAttribute);
this.through.model.rawAttributes[this.foreignIdentifier] = _.extend(this.through.model.rawAttributes[this.foreignIdentifier], targetAttribute);
this.identifierField = this.through.model.rawAttributes[this.identifier].field || this.identifier;
this.foreignIdentifierField = this.through.model.rawAttributes[this.foreignIdentifier].field || this.foreignIdentifier;
......@@ -367,66 +367,49 @@ BelongsToMany.prototype.injectGetter = function(obj) {
};
BelongsToMany.prototype.injectSetter = function(obj) {
var association = this
, primaryKeyAttribute = association.target.primaryKeyAttribute;
var association = this;
obj[this.accessors.set] = function(newAssociatedObjects, options) {
options = options || {};
var instance = this;
return instance[association.accessors.get]({
scope: false,
transaction: options.transaction,
logging: options.logging
}).then(function(oldAssociatedObjects) {
var foreignIdentifier = association.foreignIdentifier
, sourceKeys = Object.keys(association.source.primaryKeys)
, targetKeys = Object.keys(association.target.primaryKeys)
, obsoleteAssociations = []
, changedAssociations = []
, defaultAttributes = options
, promises = []
, unassociatedObjects;
var instance = this
, sourceKey = association.source.primaryKeyAttribute
, targetKey = association.target.primaryKeyAttribute
, identifier = association.identifier
, foreignIdentifier = association.foreignIdentifier
, where = {};
if (newAssociatedObjects === null) {
newAssociatedObjects = [];
} else {
if (!Array.isArray(newAssociatedObjects)) {
newAssociatedObjects = [newAssociatedObjects];
}
newAssociatedObjects = newAssociatedObjects.map(function(newAssociatedObject) {
if (!(newAssociatedObject instanceof association.target.Instance)) {
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = newAssociatedObject;
return association.target.build(tmpInstance, {
isNewRecord: false
});
}
return newAssociatedObject;
});
newAssociatedObjects = association.toInstanceArray(newAssociatedObjects);
}
if (options.remove) {
oldAssociatedObjects = newAssociatedObjects;
newAssociatedObjects = [];
}
where[identifier] = this.get(sourceKey);
return association.through.model.findAll(_.defaults({
where: where,
raw: true,
}, options)).then(function (currentRows) {
var obsoleteAssociations = []
, defaultAttributes = options
, promises = []
, unassociatedObjects;
// Don't try to insert the transaction as an attribute in the through table
defaultAttributes = Utils._.omit(defaultAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']);
defaultAttributes = _.omit(defaultAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']);
unassociatedObjects = newAssociatedObjects.filter(function(obj) {
return !Utils._.find(oldAssociatedObjects, function(old) {
return (!!obj[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : (!!obj[targetKeys[0]] ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id));
return !_.find(currentRows, function(currentRow) {
return currentRow[foreignIdentifier] === obj.get(targetKey);
});
});
oldAssociatedObjects.forEach(function(old) {
var newObj = Utils._.find(newAssociatedObjects, function(obj) {
return (!!obj[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : (!!obj[targetKeys[0]] ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id));
currentRows.forEach(function(currentRow) {
var newObj = _.find(newAssociatedObjects, function(obj) {
return currentRow[foreignIdentifier] === obj.get(targetKey);
});
if (!newObj) {
obsoleteAssociations.push(old);
obsoleteAssociations.push(currentRow);
} else {
var throughAttributes = newObj[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)
......@@ -434,48 +417,42 @@ BelongsToMany.prototype.injectSetter = function(obj) {
throughAttributes = {};
}
var changedAssociation = {
where: {},
attributes: Utils._.defaults({}, throughAttributes, defaultAttributes)
};
var where = {}
, attributes = _.defaults({}, throughAttributes, defaultAttributes);
changedAssociation.where[association.identifier] = instance[sourceKeys[0]] || instance.id;
changedAssociation.where[foreignIdentifier] = newObj[targetKeys[0]] || newObj.id;
where[identifier] = instance.get(sourceKey);
where[foreignIdentifier] = newObj.get(targetKey);
if (Object.keys(changedAssociation.attributes).length) {
changedAssociations.push(changedAssociation);
if (Object.keys(attributes).length) {
promises.push(association.through.model.update(attributes, _.extend(options, {
where: where
})));
}
}
});
if (obsoleteAssociations.length > 0) {
var foreignIds = obsoleteAssociations.map(function(associatedObject) {
return ((targetKeys.length === 1) ? associatedObject[targetKeys[0]] : associatedObject.id);
});
var where = {};
where[association.identifier] = ((sourceKeys.length === 1) ? instance[sourceKeys[0]] : instance.id);
where[foreignIdentifier] = foreignIds;
where[identifier] = instance.get(sourceKey);
where[foreignIdentifier] = obsoleteAssociations.map(function(obsoleteAssociation) {
return obsoleteAssociation[foreignIdentifier];
});
promises.push(association.through.model.destroy(Utils._.extend(options, {
promises.push(association.through.model.destroy(_.defaults({
where: where
})));
}, options)));
}
if (unassociatedObjects.length > 0) {
var bulk = unassociatedObjects.map(function(unassociatedObject) {
var attributes = {};
attributes[association.identifier] = ((sourceKeys.length === 1) ? instance[sourceKeys[0]] : instance.id);
attributes[foreignIdentifier] = ((targetKeys.length === 1) ? unassociatedObject[targetKeys[0]] : unassociatedObject.id);
attributes[identifier] = instance.get(sourceKey);
attributes[foreignIdentifier] = unassociatedObject.get(targetKey);
attributes = Utils._.defaults(attributes, unassociatedObject[association.through.model.name], defaultAttributes);
attributes = _.defaults(attributes, unassociatedObject[association.through.model.name], defaultAttributes);
if (association.through.scope) {
Object.keys(association.through.scope).forEach(function (attribute) {
attributes[attribute] = association.through.scope[attribute];
});
}
_.assign(attributes, association.through.scope);
return attributes;
}.bind(this));
......@@ -483,117 +460,67 @@ BelongsToMany.prototype.injectSetter = function(obj) {
promises.push(association.through.model.bulkCreate(bulk, options));
}
if (changedAssociations.length > 0) {
changedAssociations.forEach(function(assoc) {
promises.push(association.through.model.update(assoc.attributes, Utils._.extend(options, {
where: assoc.where
})));
});
}
return Utils.Promise.all(promises);
});
};
obj[this.accessors.addMultiple] = obj[this.accessors.add] = function(newInstance, additionalAttributes) {
// If newInstance is null or undefined, no-op
if (!newInstance) return Utils.Promise.resolve();
obj[this.accessors.addMultiple] = obj[this.accessors.add] = function(newInstances, additionalAttributes) {
// If newInstances is null or undefined, no-op
if (!newInstances) return Utils.Promise.resolve();
if (association.through && association.through.scope) {
_.assign(additionalAttributes, association.through.scope);
}
additionalAttributes = additionalAttributes || {};
var instance = this
, primaryKeyAttribute = association.target.primaryKeyAttribute
, options = additionalAttributes = additionalAttributes || {};
if (Array.isArray(newInstance)) {
var newInstances = newInstance.map(function(newInstance) {
if (!(newInstance instanceof association.target.Instance)) {
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = newInstance;
return association.target.build(tmpInstance, {
isNewRecord: false
});
}
return newInstance;
});
, defaultAttributes = _.omit(additionalAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging'])
, sourceKey = association.source.primaryKeyAttribute
, targetKey = association.target.primaryKeyAttribute
, identifier = association.identifier
, foreignIdentifier = association.foreignIdentifier
, options = additionalAttributes;
var foreignIdentifier = association.foreignIdentifier
, sourceKeys = Object.keys(association.source.primaryKeys)
, targetKeys = Object.keys(association.target.primaryKeys)
, obsoleteAssociations = []
, changedAssociations = []
, defaultAttributes = additionalAttributes
, promises = []
, oldAssociations = []
, unassociatedObjects;
newInstances = association.toInstanceArray(newInstances);
// Don't try to insert the transaction as an attribute in the through table
defaultAttributes = Utils._.omit(defaultAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']);
unassociatedObjects = newInstances.filter(function(obj) {
return !Utils._.find(oldAssociations, function(old) {
return (!!obj[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : (!!obj[targetKeys[0]] ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id));
});
});
var where = {};
where[identifier] = instance.get(sourceKey);
where[foreignIdentifier] = newInstances.map(function (newInstance) { return newInstance.get(targetKey); });
oldAssociations.forEach(function(old) {
var newObj = Utils._.find(newInstances, function(obj) {
return (!!obj[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : (!!obj[targetKeys[0]] ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id));
});
_.assign(where, association.through.scope);
if (!newObj) {
obsoleteAssociations.push(old);
} else if (Object(association.through.model) === association.through.model) {
var throughAttributes = newObj[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)
if (throughAttributes instanceof association.through.model.Instance) {
throughAttributes = {};
}
return association.through.model.findAll(_.defaults({
where: where,
raw: true,
}, options)).then(function (currentRows) {
var promises = [];
var changedAssociation = {
where: {},
attributes: Utils._.defaults({}, throughAttributes, defaultAttributes)
};
var unassociatedObjects = [], changedAssociations = [];
newInstances.forEach(function(obj) {
var existingAssociation = _.find(currentRows, function(current) {
return current[foreignIdentifier] === obj.get(targetKey);
});
changedAssociation.where[association.identifier] = instance[sourceKeys[0]] || instance.id;
changedAssociation.where[foreignIdentifier] = newObj[targetKeys[0]] || newObj.id;
if (!existingAssociation) {
unassociatedObjects.push(obj);
} else {
var throughAttributes = obj[association.through.model.name]
, attributes = _.defaults({}, throughAttributes, defaultAttributes);
if (Object.keys(changedAssociation.attributes).length) {
changedAssociations.push(changedAssociation);
if (_.any(Object.keys(attributes), function (attribute) {
return attributes[attribute] !== existingAssociation[attribute];
})) {
changedAssociations.push(obj);
}
}
});
if (obsoleteAssociations.length > 0) {
var foreignIds = obsoleteAssociations.map(function(associatedObject) {
return ((targetKeys.length === 1) ? associatedObject[targetKeys[0]] : associatedObject.id);
});
var where = {};
where[association.identifier] = ((sourceKeys.length === 1) ? instance[sourceKeys[0]] : instance.id);
where[association.foreignIdentifier] = foreignIds;
promises.push(association.through.model.destroy(Utils._.extend(options, {
where: where
})));
}
if (unassociatedObjects.length > 0) {
var bulk = unassociatedObjects.map(function(unassociatedObject) {
var attributes = {};
var throughAttributes = unassociatedObject[association.through.model.name]
, attributes = _.defaults({}, throughAttributes, defaultAttributes);
attributes[association.identifier] = ((sourceKeys.length === 1) ? instance[sourceKeys[0]] : instance.id);
attributes[association.foreignIdentifier] = ((targetKeys.length === 1) ? unassociatedObject[targetKeys[0]] : unassociatedObject.id);
attributes[identifier] = instance.get(sourceKey);
attributes[foreignIdentifier] = unassociatedObject.get(targetKey);
if (Object(association.through.model) === association.through.model) {
attributes = Utils._.defaults(attributes, unassociatedObject[association.through.model.name], defaultAttributes);
}
if (association.through.scope) {
_.assign(attributes, association.through.scope);
}
return attributes;
}.bind(this));
......@@ -601,70 +528,39 @@ BelongsToMany.prototype.injectSetter = function(obj) {
promises.push(association.through.model.bulkCreate(bulk, options));
}
if (changedAssociations.length > 0) {
changedAssociations.forEach(function(assoc) {
promises.push(association.through.model.update(assoc.attributes, Utils._.extend(options, {
where: assoc.where
var throughAttributes = assoc[association.through.model.name]
, attributes = _.defaults({}, throughAttributes, defaultAttributes)
, where = {};
// Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object)
if (throughAttributes instanceof association.through.model.Instance) {
throughAttributes = {};
}
where[identifier] = instance.get(sourceKey);
where[foreignIdentifier] = assoc.get(targetKey);
promises.push(association.through.model.update(attributes, _.extend(options, {
where: where
})));
});
}
return Utils.Promise.all(promises);
} else {
if (!(newInstance instanceof association.target.Instance)) {
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = newInstance;
newInstance = association.target.build(tmpInstance, {
isNewRecord: false
});
}
return instance[association.accessors.get]({
scope: false,
where: newInstance.where(),
transaction: (additionalAttributes || {}).transaction,
logging: options.logging
}).then(function(currentAssociatedObjects) {
var attributes = {}
, foreignIdentifier = association.foreignIdentifier;
var sourceKeys = Object.keys(association.source.primaryKeys);
var targetKeys = Object.keys(association.target.primaryKeys);
};
// Don't try to insert the transaction as an attribute in the through table
additionalAttributes = Utils._.omit(additionalAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']);
obj[this.accessors.removeMultiple] = obj[this.accessors.remove] = function(oldAssociatedObjects, options) {
options = options || {};
attributes[association.identifier] = ((sourceKeys.length === 1) ? instance[sourceKeys[0]] : instance.id);
attributes[foreignIdentifier] = ((targetKeys.length === 1) ? newInstance[targetKeys[0]] : newInstance.id);
oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects);
if (!!currentAssociatedObjects.length) {
var where = attributes;
attributes = Utils._.defaults({}, newInstance[association.through.model.name], additionalAttributes);
var where = {};
where[association.identifier] = this.get(association.source.primaryKeyAttribute);
where[association.foreignIdentifier] = oldAssociatedObjects.map(function (newInstance) { return newInstance.get(association.target.primaryKeyAttribute); });
if (Object.keys(attributes).length) {
return association.through.model.update(attributes, Utils._.extend(options, {
return association.through.model.destroy(_.defaults({
where: where
}));
} else {
return Utils.Promise.resolve();
}
} else {
attributes = Utils._.defaults(attributes, newInstance[association.through.model.name], additionalAttributes);
if (association.through.scope) {
_.assign(attributes, association.through.scope);
}
return association.through.model.create(attributes, options);
}
});
}
};
obj[this.accessors.removeMultiple] = obj[this.accessors.remove] = function(oldAssociatedObject, options) {
options = options || {};
options.remove = true;
return this[association.accessors.set](oldAssociatedObject, options);
}, options));
};
return this;
......@@ -676,6 +572,7 @@ BelongsToMany.prototype.injectCreator = function(obj) {
obj[this.accessors.create] = function(values, options) {
var instance = this;
options = options || {};
values = values || {};
if (Array.isArray(options)) {
options = {
......@@ -683,15 +580,11 @@ BelongsToMany.prototype.injectCreator = function(obj) {
};
}
if (values === undefined) {
values = {};
}
if (association.scope) {
Object.keys(association.scope).forEach(function (attribute) {
values[attribute] = association.scope[attribute];
if (options.fields) options.fields.push(attribute);
});
_.assign(values, association.scope);
if (options.fields) {
options.fields = options.fields.concat(Object.keys(association.scope));
}
}
// Create the related model instance
......
......@@ -2,6 +2,7 @@
var Utils = require('./../utils')
, Helpers = require('./helpers')
, _ = require('lodash')
, Transaction = require('../transaction')
, Association = require('./base')
, util = require('util');
......@@ -18,7 +19,7 @@ var BelongsTo = function(source, target, options) {
this.isSelfAssociation = (this.source === this.target);
this.as = this.options.as;
if (Utils._.isObject(this.options.foreignKey)) {
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else {
......@@ -37,18 +38,18 @@ var BelongsTo = function(source, target, options) {
}
if (!this.options.foreignKey) {
this.options.foreignKey = Utils._.camelizeIf(
this.options.foreignKey = _.camelizeIf(
[
Utils._.underscoredIf(this.as, this.source.options.underscored),
_.underscoredIf(this.as, this.source.options.underscored),
this.target.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
);
}
this.identifier = this.foreignKey || Utils._.camelizeIf(
this.identifier = this.foreignKey || _.camelizeIf(
[
Utils._.underscoredIf(this.options.name.singular, this.target.options.underscored),
_.underscoredIf(this.options.name.singular, this.target.options.underscored),
this.target.primaryKeyAttribute
].join('_'),
!this.target.options.underscored
......@@ -74,7 +75,7 @@ util.inherits(BelongsTo, Association);
BelongsTo.prototype.injectAttributes = function() {
var newAttributes = {};
newAttributes[this.identifier] = Utils._.defaults(this.foreignKeyAttribute, { type: this.options.keyType || this.target.rawAttributes[this.targetIdentifier].type });
newAttributes[this.identifier] = _.defaults(this.foreignKeyAttribute, { type: this.options.keyType || this.target.rawAttributes[this.targetIdentifier].type });
if (this.options.constraints !== false) {
this.options.onDelete = this.options.onDelete || 'SET NULL';
this.options.onUpdate = this.options.onUpdate || 'CASCADE';
......@@ -141,7 +142,7 @@ BelongsTo.prototype.injectSetter = function(instancePrototype) {
if (options.save === false) return;
options = Utils._.extend({
options = _.extend({
fields: [association.identifier],
allowNull: [association.identifier],
association: true
......
'use strict';
var Utils = require('./../utils')
, _ = require('lodash');
var HasManySingleLinked = function(association, instance) {
this.association = association;
this.instance = instance;
this.target = this.association.target;
this.source = this.association.source;
};
HasManySingleLinked.prototype.injectGetter = function(options) {
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 = {
$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);
};
HasManySingleLinked.prototype.injectSetter = function(oldAssociations, newAssociations, defaultAttributes) {
var self = this
, primaryKeys
, primaryKey
, updateWhere
, associationKeys = Object.keys((oldAssociations[0] || newAssociations[0] || {Model: {primaryKeys: {}}}).Model.primaryKeys || {})
, associationKey = (associationKeys.length === 1) ? associationKeys[0] : 'id'
, options = defaultAttributes
, promises = []
, obsoleteAssociations = oldAssociations.filter(function(old) {
return !Utils._.find(newAssociations, function(obj) {
return obj[associationKey] === old[associationKey];
});
})
, unassociatedObjects = newAssociations.filter(function(obj) {
return !Utils._.find(oldAssociations, function(old) {
return obj[associationKey] === old[associationKey];
});
})
, update;
if (obsoleteAssociations.length > 0) {
// clear the old associations
var obsoleteIds = obsoleteAssociations.map(function(associatedObject) {
associatedObject[self.association.identifier] = (newAssociations.length < 1 ? null : self.instance.id);
return associatedObject[associationKey];
});
update = {};
update[self.association.identifier] = null;
primaryKeys = Object.keys(this.association.target.primaryKeys);
primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id';
updateWhere = {};
updateWhere[primaryKey] = obsoleteIds;
promises.push(this.association.target.unscoped().update(
update,
Utils._.extend(options, {
allowNull: [self.association.identifier],
where: updateWhere
})
));
}
if (unassociatedObjects.length > 0) {
// For the self.instance
var pkeys = Object.keys(self.instance.Model.primaryKeys)
, pkey = pkeys.length === 1 ? pkeys[0] : 'id';
primaryKeys = Object.keys(this.association.target.primaryKeys);
primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id';
updateWhere = {};
// set the new associations
var unassociatedIds = unassociatedObjects.map(function(associatedObject) {
associatedObject[self.association.identifier] = self.instance[pkey] || self.instance.id;
return associatedObject[associationKey];
});
update = {};
update[self.association.identifier] = (newAssociations.length < 1 ? null : self.instance[pkey] || self.instance.id);
if (this.association.scope) {
_.assign(update, this.association.scope);
}
updateWhere[primaryKey] = unassociatedIds;
promises.push(this.association.target.unscoped().update(
update,
Utils._.extend(options, {
allowNull: [self.association.identifier],
where: updateWhere
})
));
}
return Utils.Promise.all(promises);
};
HasManySingleLinked.prototype.injectAdder = function(newAssociation, options) {
newAssociation.set(this.association.identifier, this.instance.get(this.instance.Model.primaryKeyAttribute));
if (this.association.scope) {
Object.keys(this.association.scope).forEach(function (attribute) {
newAssociation.set(attribute, this.association.scope[attribute]);
}.bind(this));
}
return newAssociation.save(options);
};
module.exports = HasManySingleLinked;
......@@ -5,8 +5,7 @@ var Utils = require('./../utils')
, _ = require('lodash')
, Association = require('./base')
, CounterCache = require('../plugins/counter-cache')
, util = require('util')
, HasManySingleLinked = require('./has-many-single-linked');
, util = require('util');
var HasMany = function(source, target, options) {
Association.call(this);
......@@ -27,7 +26,7 @@ var HasMany = function(source, target, options) {
throw new Error('N:M associations are not supported with hasMany. Use belongsToMany instead');
}
if (Utils._.isObject(this.options.foreignKey)) {
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else {
......@@ -45,7 +44,7 @@ var HasMany = function(source, target, options) {
if (this.as) {
this.isAliased = true;
if (Utils._.isPlainObject(this.as)) {
if (_.isPlainObject(this.as)) {
this.options.name = this.as;
this.as = this.as.plural;
} else {
......@@ -87,9 +86,9 @@ util.inherits(HasMany, Association);
// the id is in the target table
// or in an extra table which connects two tables
HasMany.prototype.injectAttributes = function() {
this.identifier = this.foreignKey || Utils._.camelizeIf(
this.identifier = this.foreignKey || _.camelizeIf(
[
Utils._.underscoredIf(this.source.options.name.singular, this.source.options.underscored),
_.underscoredIf(this.source.options.name.singular, this.source.options.underscored),
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
......@@ -190,152 +189,120 @@ HasMany.prototype.injectGetter = function(obj) {
};
HasMany.prototype.injectSetter = function(obj) {
var association = this
, primaryKeyAttribute = association.target.primaryKeyAttribute;
var association = this;
obj[this.accessors.set] = function(newAssociatedObjects, additionalAttributes) {
var options = additionalAttributes || {};
additionalAttributes = additionalAttributes || {};
if (newAssociatedObjects === null) {
newAssociatedObjects = [];
} else {
newAssociatedObjects = newAssociatedObjects.map(function(newAssociatedObject) {
if (!(newAssociatedObject instanceof association.target.Instance)) {
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = newAssociatedObject;
return association.target.build(tmpInstance, {
isNewRecord: false
});
}
return newAssociatedObject;
});
newAssociatedObjects = association.toInstanceArray(newAssociatedObjects);
}
var instance = this;
return instance[association.accessors.get]({
return instance[association.accessors.get](_.defaults({
scope: false,
transaction: (additionalAttributes || {}).transaction,
logging: (additionalAttributes || {}).logging
}).then(function(oldAssociatedObjects) {
return new HasManySingleLinked(association, instance).injectSetter(oldAssociatedObjects, newAssociatedObjects, additionalAttributes);
raw: true
}, options)).then(function(oldAssociations) {
var promises = []
, obsoleteAssociations = oldAssociations.filter(function(old) {
return !_.find(newAssociatedObjects, function(obj) {
return obj[association.target.primaryKeyAttribute] === old[association.target.primaryKeyAttribute];
});
};
obj[this.accessors.addMultiple] = obj[this.accessors.add] = function(newInstance, options) {
// If newInstance is null or undefined, no-op
if (!newInstance) return Utils.Promise.resolve();
})
, unassociatedObjects = newAssociatedObjects.filter(function(obj) {
return !_.find(oldAssociations, function(old) {
return obj[association.target.primaryKeyAttribute] === old[association.target.primaryKeyAttribute];
});
})
, updateWhere
, update;
var instance = this
, primaryKeyAttribute = association.target.primaryKeyAttribute;
if (obsoleteAssociations.length > 0) {
update = {};
update[association.identifier] = null;
options = options || {};
updateWhere = {};
if (Array.isArray(newInstance)) {
var newInstances = newInstance.map(function(newInstance) {
if (!(newInstance instanceof association.target.Instance)) {
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = newInstance;
return association.target.build(tmpInstance, {
isNewRecord: false
});
}
return newInstance;
updateWhere[association.target.primaryKeyAttribute] = obsoleteAssociations.map(function(associatedObject) {
return associatedObject[association.target.primaryKeyAttribute];
});
return new HasManySingleLinked(association, this).injectSetter([], newInstances, options);
} else {
if (!(newInstance instanceof association.target.Instance)) {
var values = {};
values[primaryKeyAttribute] = newInstance;
newInstance = association.target.build(values, {
isNewRecord: false
});
promises.push(association.target.unscoped().update(
update,
_.defaults({
where: updateWhere
}, options)
));
}
return instance[association.accessors.get]({
where: newInstance.where(),
scope: false,
transaction: options.transaction,
logging: options.logging
}).bind(this).then(function(currentAssociatedObjects) {
if (currentAssociatedObjects.length === 0) {
newInstance.set(association.identifier, instance.get(instance.Model.primaryKeyAttribute));
if (unassociatedObjects.length > 0) {
updateWhere = {};
if (association.scope) {
Object.keys(association.scope).forEach(function (attribute) {
newInstance.set(attribute, association.scope[attribute]);
update = {};
update[association.identifier] = instance.get(association.source.primaryKeyAttribute);
_.assign(update, association.scope);
updateWhere[association.target.primaryKeyAttribute] = unassociatedObjects.map(function(unassociatedObject) {
return unassociatedObject[association.target.primaryKeyAttribute];
});
}
return newInstance.save(options);
} else {
return Utils.Promise.resolve(currentAssociatedObjects[0]);
promises.push(association.target.unscoped().update(
update,
_.defaults({
where: updateWhere
}, options)
));
}
return Utils.Promise.all(promises).return(instance);
});
}
};
obj[this.accessors.remove] = function(oldAssociatedObject, options) {
var instance = this;
return instance[association.accessors.get](_.assign({
scope: false
}, options)).then(function(currentAssociatedObjects) {
var newAssociations = [];
if (!(oldAssociatedObject instanceof association.target.Instance)) {
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = oldAssociatedObject;
oldAssociatedObject = association.target.build(tmpInstance, {
isNewRecord: false
});
}
obj[this.accessors.addMultiple] = obj[this.accessors.add] = function(newInstances, options) {
// If newInstance is null or undefined, no-op
if (!newInstances) return Utils.Promise.resolve();
options = options || {};
currentAssociatedObjects.forEach(function(association) {
if (!Utils._.isEqual(oldAssociatedObject.where(), association.where())) {
newAssociations.push(association);
}
});
var instance = this, update = {}, where = {};
return instance[association.accessors.set](newAssociations, options);
});
};
newInstances = association.toInstanceArray(newInstances);
obj[this.accessors.removeMultiple] = function(oldAssociatedObjects, options) {
var instance = this;
return instance[association.accessors.get](_.assign({
scope: false
}, options)).then(function(currentAssociatedObjects) {
var newAssociations = [];
// Ensure the oldAssociatedObjects array is an array of target instances
oldAssociatedObjects = oldAssociatedObjects.map(function(oldAssociatedObject) {
if (!(oldAssociatedObject instanceof association.target.Instance)) {
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = oldAssociatedObject;
oldAssociatedObject = association.target.build(tmpInstance, {
isNewRecord: false
});
}
return oldAssociatedObject;
update[association.identifier] = instance.get(association.source.primaryKeyAttribute);
_.assign(update, association.scope);
where[association.target.primaryKeyAttribute] = newInstances.map(function (unassociatedObject) {
return unassociatedObject.get(association.target.primaryKeyAttribute);
});
currentAssociatedObjects.forEach(function(association) {
return association.target.unscoped().update(
update,
_.defaults({
where: where
}, options)
).return(instance);
};
// Determine is this is an association we want to remove
var obj = Utils._.find(oldAssociatedObjects, function(oldAssociatedObject) {
return Utils._.isEqual(oldAssociatedObject.where(), association.where());
});
obj[this.accessors.removeMultiple] = obj[this.accessors.remove] = function(oldAssociatedObjects, options) {
options = options || {};
oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects);
// This is not an association we want to remove. Add it back
// to the set of associations we will associate our instance with
if (!obj) {
newAssociations.push(association);
}
});
var update = {};
update[association.identifier] = null;
return instance[association.accessors.set](newAssociations, options);
});
var where = {};
where[association.identifier] = this.get(association.source.primaryKeyAttribute);
where[association.target.primaryKeyAttribute] = oldAssociatedObjects.map(function (oldAssociatedObject) { return oldAssociatedObject.get(association.target.primaryKeyAttribute); });
return association.target.unscoped().update(
update,
_.defaults({
where: where
}, options)
).return(this);
};
return this;
......
......@@ -2,6 +2,7 @@
var Utils = require('./../utils')
, Helpers = require('./helpers')
, _ = require('lodash')
, Association = require('./base')
, util = require('util');
......@@ -16,7 +17,7 @@ var HasOne = function(srcModel, targetModel, options) {
this.isSelfAssociation = (this.source === this.target);
this.as = this.options.as;
if (Utils._.isObject(this.options.foreignKey)) {
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else {
......@@ -35,18 +36,18 @@ var HasOne = function(srcModel, targetModel, options) {
}
if (!this.options.foreignKey) {
this.options.foreignKey = Utils._.camelizeIf(
this.options.foreignKey = _.camelizeIf(
[
Utils._.underscoredIf(Utils.singularize(this.source.name), this.target.options.underscored),
_.underscoredIf(Utils.singularize(this.source.name), this.target.options.underscored),
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
);
}
this.identifier = this.foreignKey || Utils._.camelizeIf(
this.identifier = this.foreignKey || _.camelizeIf(
[
Utils._.underscoredIf(this.source.options.name.singular, this.source.options.underscored),
_.underscoredIf(this.source.options.name.singular, this.source.options.underscored),
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
......@@ -73,7 +74,7 @@ HasOne.prototype.injectAttributes = function() {
var newAttributes = {}
, keyType = this.source.rawAttributes[this.sourceIdentifier].type;
newAttributes[this.identifier] = Utils._.defaults(this.foreignKeyAttribute, { type: this.options.keyType || keyType });
newAttributes[this.identifier] = _.defaults(this.foreignKeyAttribute, { type: this.options.keyType || keyType });
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.identifierField = this.target.rawAttributes[this.identifier].field || this.identifier;
......@@ -136,7 +137,7 @@ HasOne.prototype.injectSetter = function(instancePrototype) {
return instance[association.accessors.get](options).then(function(oldInstance) {
if (oldInstance) {
oldInstance[association.identifier] = null;
return oldInstance.save(Utils._.extend({}, options, {
return oldInstance.save(_.extend({}, options, {
fields: [association.identifier],
allowNull: [association.identifier],
association: true
......
'use strict';
var Utils = require('./../utils')
, _ = require('lodash')
, HasOne = require('./has-one')
, HasMany = require('./has-many')
, BelongsToMany = require('./belongs-to-many')
......@@ -109,7 +110,7 @@ var singleLinked = function (Type) {
options.useHooks = options.hooks;
// the id is in the foreign table
var association = new Type(sourceModel, targetModel, Utils._.extend(options, sourceModel.options));
var association = new Type(sourceModel, targetModel, _.extend(options, sourceModel.options));
sourceModel.associations[association.associationAccessor] = association.injectAttributes();
association.injectGetter(sourceModel.Instance.prototype);
......@@ -255,7 +256,7 @@ Mixin.hasMany = function(targetModel, options) {
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
options = Utils._.extend(options, Utils._.omit(sourceModel.options, ['hooks']));
options = _.extend(options, _.omit(sourceModel.options, ['hooks']));
// the id is in the foreign table or in a connecting table
var association = new HasMany(sourceModel, targetModel, options);
......@@ -349,7 +350,7 @@ Mixin.belongsToMany = function(targetModel, options) {
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
options = Utils._.extend(options, Utils._.omit(sourceModel.options, ['hooks']));
options = _.extend(options, _.omit(sourceModel.options, ['hooks']));
// the id is in the foreign table or in a connecting table
var association = new BelongsToMany(sourceModel, targetModel, options);
......
......@@ -844,6 +844,17 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
return this.task.getUsers();
}).then(function(users) {
expect(users).to.have.length(3);
// Re-add user 0's object, this should be harmless
// Re-add user 0's id, this should be harmless
return Promise.all([
expect(this.task.addUsers([this.users[0]])).not.to.be.rejected,
expect(this.task.addUsers([this.users[0].id])).not.to.be.rejected
]);
}).then(function() {
return this.task.getUsers();
}).then(function(users) {
expect(users).to.have.length(3);
});
});
});
......@@ -1065,7 +1076,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
logging: spy
}).return (user);
}).then(function(user) {
expect(spy.calledOnce).to.be.ok;
expect(spy).to.have.been.calledTwice;
spy.reset();
return Promise.join(
user,
......@@ -1119,8 +1130,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
logging: spy
}).return (project);
}).then(function(project) {
expect(spy.calledTwice).to.be.ok; // Once for SELECT, once for REMOVE
return self.user.setProjects([project]);
expect(spy).to.have.been.calledOnce;
});
});
......@@ -1762,8 +1772,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
if (current.dialect.supports.constraints.restrict) {
it('can restrict deletes both ways', function() {
var self = this
, spy = sinon.spy();
var self = this;
this.User.belongsToMany(this.Task, { onDelete: 'RESTRICT', through: 'tasksusers' });
this.Task.belongsToMany(this.User, { onDelete: 'RESTRICT', through: 'tasksusers' });
......@@ -1786,17 +1795,14 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
]);
}).then(function() {
return Promise.all([
this.user1.destroy().catch (self.sequelize.ForeignKeyConstraintError, spy), // Fails because of RESTRICT constraint
this.task2.destroy().catch (self.sequelize.ForeignKeyConstraintError, spy)
expect(this.user1.destroy()).to.have.been.rejectedWith(self.sequelize.ForeignKeyConstraintError), // Fails because of RESTRICT constraint
expect(this.task2.destroy()).to.have.been.rejectedWith(self.sequelize.ForeignKeyConstraintError)
]);
}).then(function() {
expect(spy).to.have.been.calledTwice;
});
});
it('can cascade and restrict deletes', function() {
var spy = sinon.spy()
, self = this;
var self = this;
self.User.belongsToMany(self.Task, { onDelete: 'RESTRICT', through: 'tasksusers' });
self.Task.belongsToMany(self.User, { onDelete: 'CASCADE', through: 'tasksusers' });
......@@ -1819,11 +1825,10 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
);
}).then(function() {
return Sequelize.Promise.join(
this.user1.destroy().catch(self.sequelize.ForeignKeyConstraintError, spy), // Fails because of RESTRICT constraint
expect(this.user1.destroy()).to.have.been.rejectedWith(self.sequelize.ForeignKeyConstraintError), // Fails because of RESTRICT constraint
this.task2.destroy()
);
}).then(function() {
expect(spy).to.have.been.calledOnce;
return self.sequelize.model('tasksusers').findAll({ where: { taskId: this.task2.id }});
}).then(function(usertasks) {
// This should not exist because deletes cascade
......
......@@ -634,54 +634,6 @@ describe(Support.getTestDialectTeaser('HasMany'), function() {
});
});
describe('optimizations using bulk create, destroy and update', function() {
beforeEach(function() {
this.User = this.sequelize.define('User', { username: DataTypes.STRING }, {timestamps: false});
this.Task = this.sequelize.define('Task', { title: DataTypes.STRING }, {timestamps: false});
this.User.hasMany(this.Task);
return this.sequelize.sync({ force: true });
});
it('uses one UPDATE statement', function() {
var self = this
, spy = sinon.spy();
return this.User.create({ username: 'foo' }).bind({}).then(function(user) {
this.user = user;
return self.Task.create({ title: 'task1' });
}).then(function(task1) {
this.task1 = task1;
return self.Task.create({ title: 'task2' });
}).then(function(task2) {
this.task2 = task2;
return this.user.setTasks([this.task1, this.task2], {logging: spy});
}).then(function() {
expect(spy).to.have.been.calledTwice; // Once for SELECT, once for UPDATE
});
});
it('uses one UPDATE statement', function() {
var self = this
, spy = sinon.spy();
return this.User.create({ username: 'foo' }).bind(this).then(function(user) {
this.user = user;
return self.Task.create({ title: 'task1' });
}).then(function(task1) {
this.task1 = task1;
return self.Task.create({ title: 'task2' });
}).then(function(task2) {
return this.user.setTasks([this.task1, task2]);
}).then(function() {
return this.user.setTasks(null, {logging: spy});
}).then(function() {
expect(spy).to.have.been.calledTwice; // Once for SELECT, once for UPDATE
});
});
}); // end optimization using bulk create, destroy and update
describe('selfAssociations', function() {
it('should work with alias', function() {
var Person = this.sequelize.define('Group', {});
......
......@@ -4606,39 +4606,24 @@ describe(Support.getTestDialectTeaser('Hooks'), function() {
describe('#remove', function() {
it('with no errors', function() {
var self = this
, beforeProject = false
, afterProject = false
, beforeTask = false
, afterTask = false;
this.Projects.beforeCreate(function(project, options, fn) {
beforeProject = true;
fn();
});
, beforeProject = sinon.spy()
, afterProject = sinon.spy()
, beforeTask = sinon.spy()
, afterTask = sinon.spy();
this.Projects.afterCreate(function(project, options, fn) {
afterProject = true;
fn();
});
this.Tasks.beforeUpdate(function(task, options, fn) {
beforeTask = true;
fn();
});
this.Tasks.afterUpdate(function(task, options, fn) {
afterTask = true;
fn();
});
this.Projects.beforeCreate(beforeProject);
this.Projects.afterCreate(afterProject);
this.Tasks.beforeUpdate(beforeTask);
this.Tasks.afterUpdate(afterTask);
return this.Projects.create({title: 'New Project'}).then(function(project) {
return self.Tasks.create({title: 'New Task'}).then(function(task) {
return project.addTask(task).then(function() {
return project.removeTask(task).then(function() {
expect(beforeProject).to.be.true;
expect(afterProject).to.be.true;
expect(beforeTask).to.be.true;
expect(afterTask).to.be.true;
expect(beforeProject).to.have.been.called;
expect(afterProject).to.have.been.called;
expect(beforeTask).not.to.have.been.called;
expect(afterTask).not.to.have.been.called;
});
});
});
......@@ -4813,39 +4798,24 @@ describe(Support.getTestDialectTeaser('Hooks'), function() {
describe('#remove', function() {
it('with no errors', function() {
var self = this
, beforeProject = false
, afterProject = false
, beforeTask = false
, afterTask = false;
this.Projects.beforeCreate(function(project, options, fn) {
beforeProject = true;
fn();
});
, beforeProject = sinon.spy()
, afterProject = sinon.spy()
, beforeTask = sinon.spy()
, afterTask = sinon.spy();
this.Projects.afterCreate(function(project, options, fn) {
afterProject = true;
fn();
});
this.Tasks.beforeUpdate(function(task, options, fn) {
beforeTask = true;
fn();
});
this.Tasks.afterUpdate(function(task, options, fn) {
afterTask = true;
fn();
});
this.Projects.beforeCreate(beforeProject);
this.Projects.afterCreate(afterProject);
this.Tasks.beforeUpdate(beforeTask);
this.Tasks.afterUpdate(afterTask);
return this.Projects.create({title: 'New Project'}).then(function(project) {
return self.Tasks.create({title: 'New Task'}).then(function(task) {
return project.addTask(task).then(function() {
return project.removeTask(task).then(function() {
expect(beforeProject).to.be.true;
expect(afterProject).to.be.true;
expect(beforeTask).to.be.true;
expect(afterTask).to.be.true;
expect(beforeProject).to.have.been.called;
expect(afterProject).to.have.been.called;
expect(beforeTask).not.to.have.been.called;
expect(afterTask).not.to.have.been.called;
});
});
});
......
......@@ -299,42 +299,6 @@ describe(Support.getTestDialectTeaser('Model'), function() {
});
});
it('should be able to return a record with primaryKey being null for new inserts', function() {
var Session = this.sequelize.define('Session', {
token: { type: DataTypes.TEXT, allowNull: false },
lastUpdate: { type: DataTypes.DATE, allowNull: false }
}, {
charset: 'utf8',
collate: 'utf8_general_ci',
omitNull: true
})
, User = this.sequelize.define('User', {
name: { type: DataTypes.STRING, allowNull: false, unique: true },
password: { type: DataTypes.STRING, allowNull: false },
isAdmin: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }
}, {
charset: 'utf8',
collate: 'utf8_general_ci'
});
User.hasMany(Session, { as: 'Sessions' });
Session.belongsTo(User);
return this.sequelize.sync({ force: true }).then(function() {
return User.create({name: 'Name1', password: '123', isAdmin: false}).then(function(user) {
var sess = Session.build({
lastUpdate: new Date(),
token: '123'
});
return user.addSession(sess).then(function(u) {
expect(u.token).to.equal('123');
});
});
});
});
it('should be able to find a row between a certain date', function() {
return this.User.findAll({
where: {
......
......@@ -2,11 +2,68 @@
/* jshint -W030 */
var chai = require('chai')
, sinon = require('sinon')
, expect = chai.expect
, stub = sinon.stub
, Support = require(__dirname + '/../support')
, current = Support.sequelize;
, DataTypes = require(__dirname + '/../../../lib/data-types')
, current = Support.sequelize
, Promise = current.Promise;
describe(Support.getTestDialectTeaser('belongsToMany'), function() {
describe('optimizations using bulk create, destroy and update', function() {
var User = current.define('User', { username: DataTypes.STRING })
, Task = current.define('Task', { title: DataTypes.STRING })
, UserTasks = current.define('UserTasks', {});
User.belongsToMany(Task, { through: UserTasks });
Task.belongsToMany(User, { through: UserTasks });
var user = User.build({
id: 42
}),
task1 = Task.build({
id: 15
}),
task2 = Task.build({
id: 16
});
beforeEach(function () {
this.findAll = stub(UserTasks, 'findAll').returns(Promise.resolve([]));
this.bulkCreate = stub(UserTasks, 'bulkCreate').returns(Promise.resolve([]));
this.destroy = stub(UserTasks, 'destroy').returns(Promise.resolve([]));
});
afterEach(function () {
this.findAll.restore();
this.bulkCreate.restore();
this.destroy.restore();
});
it('uses one insert into statement', function() {
return user.setTasks([task1, task2]).bind(this).then(function () {
expect(this.findAll).to.have.been.calledOnce;
expect(this.bulkCreate).to.have.been.calledOnce;
});
});
it('uses one delete from statement', function() {
this.findAll
.onFirstCall().returns(Promise.resolve([]))
.onSecondCall().returns(Promise.resolve([
{ userId: 42, taskId: 15 },
{ userId: 42, taskId: 16 }
]));
return user.setTasks([task1, task2]).bind(this).then(function () {
return user.setTasks(null);
}).then(function () {
expect(this.findAll).to.have.been.calledTwice;
expect(this.destroy).to.have.been.calledOnce;
});
});
describe(Support.getTestDialectTeaser('Associations'), function() {
describe('belongsToMany', function () {
it('works with singular and plural name for self-associations', function () {
// Models taken from https://github.com/sequelize/sequelize/issues/3796
......@@ -28,4 +85,5 @@ describe(Support.getTestDialectTeaser('Associations'), function() {
expect(Instance.prototype).not.to.have.property('addSupplementeds').which.is.a.function;
});
});
});
});
'use strict';
/* jshint -W030 */
var chai = require('chai')
, sinon = require('sinon')
, expect = chai.expect
, stub = sinon.stub
, Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + '/../../../lib/data-types')
, current = Support.sequelize
, Promise = current.Promise;
describe(Support.getTestDialectTeaser('hasMany'), function() {
describe('optimizations using bulk create, destroy and update', function() {
var User = current.define('User', { username: DataTypes.STRING })
, Task = current.define('Task', { title: DataTypes.STRING });
User.hasMany(Task);
var user = User.build({
id: 42
}),
task1 = Task.build({
id: 15
}),
task2 = Task.build({
id: 16
});
beforeEach(function () {
this.findAll = stub(Task, 'findAll').returns(Promise.resolve([]));
this.update = stub(Task, 'update').returns(Promise.resolve([]));
});
afterEach(function () {
this.findAll.restore();
this.update.restore();
});
it('uses one update statement for addition', function() {
return user.setTasks([task1, task2]).bind(this). then(function() {
expect(this.findAll).to.have.been.calledOnce;
expect(this.update).to.have.been.calledOnce;
});
});
it('uses one delete from statement', function() {
this.findAll
.onFirstCall().returns(Promise.resolve([]))
.onSecondCall().returns(Promise.resolve([
{ userId: 42, taskId: 15 },
{ userId: 42, taskId: 16 }
]));
return user.setTasks([task1, task2]).bind(this).then(function () {
this.update.reset();
return user.setTasks(null);
}).then(function () {
expect(this.findAll).to.have.been.calledTwice;
expect(this.update).to.have.been.calledOnce;
});
});
});
});
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!