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

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