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

Commit c20fa816 by Mick Hansen

Merge pull request #1744 from sequelize/addRelations

addRelations feature
2 parents 7e9679be b67058a6
var Utils = require("./../utils") var Utils = require("./../utils")
, DataTypes = require('./../data-types')
, Helpers = require('./helpers') , Helpers = require('./helpers')
, Transaction = require('../transaction') , Transaction = require('../transaction')
, _ = require('lodash')
module.exports = (function() { module.exports = (function() {
var BelongsTo = function(source, target, options) { var BelongsTo = function(source, target, options) {
this.associationType = 'BelongsTo' this.associationType = 'BelongsTo'
......
...@@ -12,76 +12,65 @@ module.exports = (function() { ...@@ -12,76 +12,65 @@ module.exports = (function() {
HasManyDoubleLinked.prototype.injectGetter = function(options) { HasManyDoubleLinked.prototype.injectGetter = function(options) {
var self = this var self = this
, _options = options , through = self.association.through
, smart , queryOptions = {}
, targetAssociation = self.association.targetAssociation
var customEventEmitter = new Utils.CustomEventEmitter(function() {
var where = [] options = options || {}
, through = self.association.through
, options = _options || {} //fully qualify
, queryOptions = {} var instancePrimaryKey = self.instance.Model.primaryKeyAttribute
, targetAssociation = self.association.targetAssociation , foreignPrimaryKey = self.association.target.primaryKeyAttribute
//fully qualify options.where = new Utils.and([
var instancePrimaryKey = self.instance.Model.primaryKeyAttribute new Utils.where(
, foreignPrimaryKey = self.association.target.primaryKeyAttribute through.rawAttributes[self.association.identifier],
self.instance[instancePrimaryKey]
options.where = new Utils.and([ ),
new Utils.where( new Utils.where(
through.rawAttributes[self.association.identifier], through.rawAttributes[self.association.foreignIdentifier],
self.instance[instancePrimaryKey] {
), join: new Utils.literal([
new Utils.where( self.QueryInterface.quoteTable(self.association.target.name),
through.rawAttributes[self.association.foreignIdentifier], self.QueryInterface.quoteIdentifier(foreignPrimaryKey)
{ ].join('.'))
join: new Utils.literal([
self.QueryInterface.quoteTable(self.association.target.name),
self.QueryInterface.quoteIdentifier(foreignPrimaryKey)
].join('.'))
}
),
options.where
])
if (Object(targetAssociation.through) === targetAssociation.through) {
queryOptions.hasJoinTableModel = true
queryOptions.joinTableModel = through
if (!options.attributes) {
options.attributes = [
self.QueryInterface.quoteTable(self.association.target.name)+".*"
]
}
if (options.joinTableAttributes) {
options.joinTableAttributes.forEach(function (elem) {
options.attributes.push(
self.QueryInterface.quoteTable(through.name) + '.' + self.QueryInterface.quoteIdentifier(elem) + ' as ' +
self.QueryInterface.quoteIdentifier(through.name + '.' + elem, true)
)
})
} else {
Utils._.forOwn(through.rawAttributes, function (elem, key) {
options.attributes.push(
self.QueryInterface.quoteTable(through.name) + '.' + self.QueryInterface.quoteIdentifier(key) + ' as ' +
self.QueryInterface.quoteIdentifier(through.name + '.' + key, true)
)
})
} }
),
options.where
])
if (Object(targetAssociation.through) === targetAssociation.through) {
queryOptions.hasJoinTableModel = true
queryOptions.joinTableModel = through
if (!options.attributes) {
options.attributes = [
self.QueryInterface.quoteTable(self.association.target.name)+".*"
]
} }
self.association.target.findAllJoin([through.getTableName(), through.name], options, queryOptions) if (options.joinTableAttributes) {
.on('success', function(objects) { customEventEmitter.emit('success', objects) }) options.joinTableAttributes.forEach(function (elem) {
.on('error', function(err){ customEventEmitter.emit('error', err) }) options.attributes.push(
.on('sql', function(sql) { customEventEmitter.emit('sql', sql)}) self.QueryInterface.quoteTable(through.name) + '.' + self.QueryInterface.quoteIdentifier(elem) + ' as ' +
}) self.QueryInterface.quoteIdentifier(through.name + '.' + elem, true)
)
})
} else {
Utils._.forOwn(through.rawAttributes, function (elem, key) {
options.attributes.push(
self.QueryInterface.quoteTable(through.name) + '.' + self.QueryInterface.quoteIdentifier(key) + ' as ' +
self.QueryInterface.quoteIdentifier(through.name + '.' + key, true)
)
})
}
}
return customEventEmitter.run() return self.association.target.findAllJoin([through.getTableName(), through.name], options, queryOptions)
} }
HasManyDoubleLinked.prototype.injectSetter = function(emitterProxy, oldAssociations, newAssociations, defaultAttributes) { HasManyDoubleLinked.prototype.injectSetter = function(oldAssociations, newAssociations, defaultAttributes) {
var self = this var self = this
, chainer = new Utils.QueryChainer()
, targetAssociation = self.association.targetAssociation , targetAssociation = self.association.targetAssociation
, foreignIdentifier = self.association.foreignIdentifier , foreignIdentifier = self.association.foreignIdentifier
, sourceKeys = Object.keys(self.association.source.primaryKeys) , sourceKeys = Object.keys(self.association.source.primaryKeys)
...@@ -89,6 +78,7 @@ module.exports = (function() { ...@@ -89,6 +78,7 @@ module.exports = (function() {
, obsoleteAssociations = [] , obsoleteAssociations = []
, changedAssociations = [] , changedAssociations = []
, options = {} , options = {}
, promises = []
, unassociatedObjects; , unassociatedObjects;
if ((defaultAttributes || {}).transaction instanceof Transaction) { if ((defaultAttributes || {}).transaction instanceof Transaction) {
...@@ -140,7 +130,7 @@ module.exports = (function() { ...@@ -140,7 +130,7 @@ module.exports = (function() {
where[self.association.identifier] = ((sourceKeys.length === 1) ? self.instance[sourceKeys[0]] : self.instance.id) where[self.association.identifier] = ((sourceKeys.length === 1) ? self.instance[sourceKeys[0]] : self.instance.id)
where[foreignIdentifier] = foreignIds where[foreignIdentifier] = foreignIds
chainer.add(self.association.through.destroy(where, options)) promises.push(self.association.through.destroy(where, options))
} }
if (unassociatedObjects.length > 0) { if (unassociatedObjects.length > 0) {
...@@ -157,23 +147,19 @@ module.exports = (function() { ...@@ -157,23 +147,19 @@ module.exports = (function() {
return attributes return attributes
}) })
chainer.add(self.association.through.bulkCreate(bulk, options)) promises.push(self.association.through.bulkCreate(bulk, options))
} }
if (changedAssociations.length > 0) { if (changedAssociations.length > 0) {
changedAssociations.forEach(function (assoc) { changedAssociations.forEach(function (assoc) {
chainer.add(self.association.through.update(assoc.attributes, assoc.where, options)) promises.push(self.association.through.update(assoc.attributes, assoc.where, options))
}) })
} }
chainer return Utils.Promise.all(promises)
.run()
.success(function() { emitterProxy.emit('success', newAssociations) })
.error(function(err) { emitterProxy.emit('error', err) })
.on('sql', function(sql) { emitterProxy.emit('sql', sql) })
} }
HasManyDoubleLinked.prototype.injectAdder = function(emitterProxy, newAssociation, additionalAttributes, exists) { HasManyDoubleLinked.prototype.injectAdder = function(newAssociation, additionalAttributes, exists) {
var attributes = {} var attributes = {}
, targetAssociation = this.association.targetAssociation , targetAssociation = this.association.targetAssociation
, foreignIdentifier = targetAssociation.identifier , foreignIdentifier = targetAssociation.identifier
...@@ -195,17 +181,14 @@ module.exports = (function() { ...@@ -195,17 +181,14 @@ module.exports = (function() {
attributes = Utils._.defaults({}, newAssociation[targetAssociation.through.name], additionalAttributes) attributes = Utils._.defaults({}, newAssociation[targetAssociation.through.name], additionalAttributes)
if (Object.keys(attributes).length) { if (Object.keys(attributes).length) {
targetAssociation.through.update(attributes, where).proxy(emitterProxy) return targetAssociation.through.update(attributes, where)
} else { } else {
emitterProxy.emit('success') return Utils.Promise.resolve()
} }
} else { } else {
attributes = Utils._.defaults(attributes, newAssociation[targetAssociation.through.name], additionalAttributes) attributes = Utils._.defaults(attributes, newAssociation[targetAssociation.through.name], additionalAttributes)
this.association.through.create(attributes, options) return this.association.through.create(attributes, options)
.success(function() { emitterProxy.emit('success', newAssociation) })
.error(function(err) { emitterProxy.emit('error', err) })
.on('sql', function(sql) { emitterProxy.emit('sql', sql) })
} }
} }
......
...@@ -24,12 +24,12 @@ module.exports = (function() { ...@@ -24,12 +24,12 @@ module.exports = (function() {
return this.association.target.all(options) return this.association.target.all(options)
} }
HasManySingleLinked.prototype.injectSetter = function(emitter, oldAssociations, newAssociations, defaultAttributes) { HasManySingleLinked.prototype.injectSetter = function(oldAssociations, newAssociations, defaultAttributes) {
var self = this var self = this
, associationKeys = Object.keys((oldAssociations[0] || newAssociations[0] || {Model: {primaryKeys: {}}}).Model.primaryKeys || {}) , associationKeys = Object.keys((oldAssociations[0] || newAssociations[0] || {Model: {primaryKeys: {}}}).Model.primaryKeys || {})
, associationKey = (associationKeys.length === 1) ? associationKeys[0] : 'id' , associationKey = (associationKeys.length === 1) ? associationKeys[0] : 'id'
, chainer = new Utils.QueryChainer()
, options = {} , options = {}
, promises = []
, obsoleteAssociations = oldAssociations.filter(function (old) { , obsoleteAssociations = oldAssociations.filter(function (old) {
return !Utils._.find(newAssociations, function (obj) { return !Utils._.find(newAssociations, function (obj) {
return obj[associationKey] === old[associationKey] return obj[associationKey] === old[associationKey]
...@@ -62,7 +62,7 @@ module.exports = (function() { ...@@ -62,7 +62,7 @@ module.exports = (function() {
, updateWhere = {} , updateWhere = {}
updateWhere[primaryKey] = obsoleteIds updateWhere[primaryKey] = obsoleteIds
chainer.add(this.__factory.target.update( promises.push(this.__factory.target.update(
update, update,
updateWhere, updateWhere,
Utils._.extend(options, { allowNull: [self.__factory.identifier] }) Utils._.extend(options, { allowNull: [self.__factory.identifier] })
...@@ -88,21 +88,17 @@ module.exports = (function() { ...@@ -88,21 +88,17 @@ module.exports = (function() {
update[self.__factory.identifier] = (newAssociations.length < 1 ? null : self.instance[pkey] || self.instance.id) update[self.__factory.identifier] = (newAssociations.length < 1 ? null : self.instance[pkey] || self.instance.id)
updateWhere[primaryKey] = unassociatedIds updateWhere[primaryKey] = unassociatedIds
chainer.add(this.__factory.target.update( promises.push(this.__factory.target.update(
update, update,
updateWhere, updateWhere,
Utils._.extend(options, { allowNull: [self.__factory.identifier] }) Utils._.extend(options, { allowNull: [self.__factory.identifier] })
)) ))
} }
chainer return Utils.Promise.all(promises)
.run()
.success(function() { emitter.emit('success', newAssociations) })
.error(function(err) { emitter.emit('error', err) })
.on('sql', function(sql) { emitter.emit('sql', sql) })
} }
HasManySingleLinked.prototype.injectAdder = function(emitterProxy, newAssociation, additionalAttributes) { HasManySingleLinked.prototype.injectAdder = function(newAssociation, additionalAttributes) {
var primaryKeys = Object.keys(this.instance.Model.primaryKeys) var primaryKeys = Object.keys(this.instance.Model.primaryKeys)
, primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id' , primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id'
, options = {} , options = {}
...@@ -114,10 +110,7 @@ module.exports = (function() { ...@@ -114,10 +110,7 @@ module.exports = (function() {
newAssociation[this.__factory.identifier] = this.instance[primaryKey] newAssociation[this.__factory.identifier] = this.instance[primaryKey]
newAssociation.save(options) return newAssociation.save(options)
.success(function() { emitterProxy.emit('success', newAssociation) })
.error(function(err) { emitterProxy.emit('error', err) })
.on('sql', function(sql) { emitterProxy.emit('sql', sql) })
} }
return HasManySingleLinked return HasManySingleLinked
......
var Utils = require("./../utils") var Utils = require("./../utils")
, DataTypes = require('./../data-types')
, Helpers = require('./helpers') , Helpers = require('./helpers')
, _ = require('lodash') , _ = require('lodash')
, Transaction = require('../transaction') , Transaction = require('../transaction')
...@@ -122,6 +121,7 @@ module.exports = (function() { ...@@ -122,6 +121,7 @@ module.exports = (function() {
this.accessors = { this.accessors = {
get: Utils._.camelize('get_' + this.as), get: Utils._.camelize('get_' + this.as),
set: Utils._.camelize('set_' + this.as), set: Utils._.camelize('set_' + this.as),
addMultiple: Utils._.camelize('add_' + this.as, this.target.options.language),
add: Utils._.camelize(Utils.singularize('add_' + this.as, this.target.options.language)), add: Utils._.camelize(Utils.singularize('add_' + this.as, this.target.options.language)),
create: Utils._.camelize(Utils.singularize('create_' + this.as, this.target.options.language)), create: Utils._.camelize(Utils.singularize('create_' + this.as, this.target.options.language)),
remove: Utils._.camelize(Utils.singularize('remove_' + this.as, this.target.options.language)), remove: Utils._.camelize(Utils.singularize('remove_' + this.as, this.target.options.language)),
...@@ -262,40 +262,26 @@ module.exports = (function() { ...@@ -262,40 +262,26 @@ module.exports = (function() {
obj[this.accessors.hasAll] = function(objects, options) { obj[this.accessors.hasAll] = function(objects, options) {
var instance = this; var instance = this;
var customEventEmitter = new Utils.CustomEventEmitter(function() { return instance[self.accessors.get](options).then(function(associatedObjects) {
instance[self.accessors.get](options) return Utils._.all(objects, function(o) {
.error(function(err) { customEventEmitter.emit('error', err) }) return Utils._.any(associatedObjects, function(associatedObject) {
.success(function(associatedObjects) { return Utils._.all(associatedObject.identifiers, function(key, identifier) {
customEventEmitter.emit('success', return o[identifier] == associatedObject[identifier];
Utils._.all(objects, function(o) { });
return Utils._.any(associatedObjects, function(associatedObject) {
return Utils._.all(associatedObject.identifiers, function(key, identifier) {
return o[identifier] == associatedObject[identifier];
});
})
})
)
}) })
})
}) })
return customEventEmitter.run()
} }
obj[this.accessors.hasSingle] = function(o, options) { obj[this.accessors.hasSingle] = function(o, options) {
var instance = this var instance = this
var customEventEmitter = new Utils.CustomEventEmitter(function() { return instance[self.accessors.get](options).then(function(associatedObjects) {
instance[self.accessors.get](options) return Utils._.any(associatedObjects, function(associatedObject) {
.error(function(err){ customEventEmitter.emit('error', err)}) return Utils._.all(associatedObject.identifiers, function(key, identifier) {
.success(function(associatedObjects) { return o[identifier] == associatedObject[identifier];
customEventEmitter.emit('success', });
Utils._.any(associatedObjects, function(associatedObject) {
return Utils._.all(associatedObject.identifiers, function(key, identifier) {
return o[identifier] == associatedObject[identifier];
});
})
)
}) })
}) })
return customEventEmitter.run()
} }
return this return this
} }
...@@ -303,93 +289,64 @@ module.exports = (function() { ...@@ -303,93 +289,64 @@ module.exports = (function() {
HasMany.prototype.injectSetter = function(obj) { HasMany.prototype.injectSetter = function(obj) {
var self = this var self = this
obj[this.accessors.set] = function(newAssociatedObjects, defaultAttributes) { obj[this.accessors.set] = function (newAssociatedObjects, additionalAttributes) {
if (newAssociatedObjects === null) { if (newAssociatedObjects === null) {
newAssociatedObjects = [] newAssociatedObjects = []
} }
var instance = this var instance = this
// define the returned customEventEmitter, which will emit the success event once everything is done return instance[self.accessors.get]({
return new Utils.CustomEventEmitter(function(emitter) { transaction: (additionalAttributes || {}).transaction
instance[self.accessors.get]({ }).then(function(oldAssociatedObjects) {
transaction: (defaultAttributes || {}).transaction var Class = Object(self.through) === self.through ? HasManyDoubleLinked : HasManySingleLinked
}) return new Class(self, instance).injectSetter(oldAssociatedObjects, newAssociatedObjects, additionalAttributes)
.success(function(oldAssociatedObjects) { })
var Class = Object(self.through) === self.through ? HasManyDoubleLinked : HasManySingleLinked
new Class(self, instance).injectSetter(emitter, oldAssociatedObjects, newAssociatedObjects, defaultAttributes)
})
.proxy(emitter, {events: ['error', 'sql']})
}).run()
} }
obj[this.accessors.add] = function(newInstance, additionalAttributes) { obj[this.accessors.add] = function (newInstance, additionalAttributes) {
var instance = this var instance = this
, primaryKey = newInstance.Model.primaryKeyAttribute , primaryKey = newInstance.Model.primaryKeyAttribute
, where = new Utils.where(self.target.rawAttributes[primaryKey], newInstance[primaryKey]) , where = new Utils.where(self.target.rawAttributes[primaryKey], newInstance[primaryKey])
return new Utils.CustomEventEmitter(function(emitter) { if (Array.isArray(newInstance)) {
instance[self.accessors.get]({ return obj[self.accessors.addMultiple](newInstance, additionalAttributes)
} else {
return instance[self.accessors.get]({
where: where, where: where,
transaction: (additionalAttributes || {}).transaction transaction: (additionalAttributes || {}).transaction
}).then(function(currentAssociatedObjects) {
if (currentAssociatedObjects.length === 0 || Object(self.through) === self.through) {
var Class = Object(self.through) === self.through ? HasManyDoubleLinked : HasManySingleLinked
return new Class(self, instance).injectAdder(newInstance, additionalAttributes, !!currentAssociatedObjects.length)
} else {
return Utils.Promise.resolve(newInstance)
}
}) })
.proxy(emitter, {events: ['error', 'sql']}) }
.success(function(currentAssociatedObjects) {
if (currentAssociatedObjects.length === 0 || Object(self.through) === self.through) {
var Class = Object(self.through) === self.through ? HasManyDoubleLinked : HasManySingleLinked
new Class(self, instance).injectAdder(emitter, newInstance, additionalAttributes, !!currentAssociatedObjects.length)
} else {
emitter.emit('success', newInstance);
}
})
}).run()
} }
obj[this.accessors.remove] = function(oldAssociatedObject, options) { obj[this.accessors.addMultiple] = function (newInstances, additionalAttributes) {
var instance = this var Class = Object(self.through) === self.through ? HasManyDoubleLinked : HasManySingleLinked
return new Utils.CustomEventEmitter(function(emitter) { return new Class(self, this).injectSetter([], newInstances, additionalAttributes)
instance[self.accessors.get]({ }
transaction: (options || {}).transaction
}).success(function(currentAssociatedObjects) {
var newAssociations = []
, oldAssociations = []
currentAssociatedObjects.forEach(function(association) {
if (!Utils._.isEqual(oldAssociatedObject.identifiers, association.identifiers)) {
newAssociations.push(association)
}
})
var tick = 0
var next = function(err, i) {
if (!!err || i >= oldAssociations.length) {
return run(err)
}
oldAssociations[i].destroy().error(function(err) {
next(err)
})
.success(function() {
tick++
next(null, tick)
})
}
var run = function(err) {
if (!!err) {
return emitter.emit('error', err)
}
instance[self.accessors.set](newAssociations).proxy(emitter)
}
if (oldAssociations.length > 0) { obj[this.accessors.remove] = function (oldAssociatedObject, options) {
next(null, tick) var instance = this
} else { return instance[self.accessors.get]({
run() transaction: (options || {}).transaction
}).then(function(currentAssociatedObjects) {
var newAssociations = []
currentAssociatedObjects.forEach(function(association) {
if (!Utils._.isEqual(oldAssociatedObject.identifiers, association.identifiers)) {
newAssociations.push(association)
} }
}) })
}).run()
return instance[self.accessors.set](newAssociations)
})
} }
return this return this
...@@ -407,16 +364,10 @@ module.exports = (function() { ...@@ -407,16 +364,10 @@ module.exports = (function() {
} }
if (Object(self.through) === self.through) { if (Object(self.through) === self.through) {
return new Utils.CustomEventEmitter(function(emitter) { // Create the related model instance
// Create the related model instance return self.target.create(values, fieldsOrOptions).then(function(newAssociatedObject) {
self.target return instance[self.accessors.add](newAssociatedObject, options)
.create(values, fieldsOrOptions) })
.proxy(emitter, { events: ['error', 'sql'] })
.success(function(newAssociatedObject) {
instance[self.accessors.add](newAssociatedObject, options)
.proxy(emitter)
})
}).run()
} else { } else {
values[self.identifier] = instance.get(self.source.primaryKeyAttribute); values[self.identifier] = instance.get(self.source.primaryKeyAttribute);
return self.target.create(values, fieldsOrOptions) return self.target.create(values, fieldsOrOptions)
......
var Utils = require("./../utils") var Utils = require("./../utils")
, DataTypes = require('./../data-types')
, Helpers = require("./helpers") , Helpers = require("./helpers")
, Transaction = require("../transaction") , Transaction = require("../transaction")
...@@ -118,15 +117,9 @@ module.exports = (function() { ...@@ -118,15 +117,9 @@ module.exports = (function() {
options.transaction = fieldsOrOptions.transaction options.transaction = fieldsOrOptions.transaction
} }
return new Utils.CustomEventEmitter(function(emitter) { return association.target.create(values, fieldsOrOptions).then(function(associationInstance) {
association.target return instance[association.accessors.set](associationInstance, options)
.create(values, fieldsOrOptions) })
.proxy(emitter, { events: ['error', 'sql'] })
.success(function(associationInstance) {
instance[association.accessors.set](associationInstance, options)
.proxy(emitter)
})
}).run()
} }
return this return this
......
...@@ -69,11 +69,11 @@ var Mixin = module.exports = function(){} ...@@ -69,11 +69,11 @@ var Mixin = module.exports = function(){}
* * set[AS] - for example setProfile(instance, options). Options are passed to `target.save` * * set[AS] - for example setProfile(instance, options). Options are passed to `target.save`
* * create[AS] - for example createProfile(value, options). Builds and saves a new instance of the associated model. Values and options are passed on to `target.create` * * create[AS] - for example createProfile(value, options). Builds and saves a new instance of the associated model. Values and options are passed on to `target.create`
* *
* All methods return an event emitter. * All methods return a promise
* *
* @param {Model} target * @param {Model} target
* @param {object} [options] * @param {object} [options]
* @param {boolean} [options.hooks=false] Set to true to run before-/afterDestroy hooks when an associated model is deleted because of a cascade. For example if `User.hasOne(Profile, {onDelete: 'cascade', hooks:true})`, the before-/afterDestroy hooks for profile will be called when a user is deleted. Otherwise the profile will be deleted without invoking any hooks * @param {boolean} [options.hooks=false] Set to true to run before-/afterDestroy hooks when an associated model is deleted because of a cascade. For example if `User.hasOne(Profile, {onDelete: 'cascade', hooks:true})`, the before-/afterDestroy hooks for profile will be called when a user is deleted. Otherwise the profile will be deleted without invoking any hooks
* @param {string} [options.as] The alias of this model. If you create multiple associations between the same tables, you should provide an alias to be able to distinguish between them. If you provide an alias when creating the assocition, you should provide the same alias when eager loading and when getting assocated models. Defaults to the singularized version of target.name * @param {string} [options.as] The alias of this model. If you create multiple associations between the same tables, you should provide an alias to be able to distinguish between them. If you provide an alias when creating the assocition, you should provide the same alias when eager loading and when getting assocated models. Defaults to the singularized version of target.name
* @param {string} [options.foreignKey] The name of the foreign key in the target table. Defaults to the name of source + primary key of source * @param {string} [options.foreignKey] The name of the foreign key in the target table. Defaults to the name of source + primary key of source
* @param {string} [options.onDelete='SET NULL'] * @param {string} [options.onDelete='SET NULL']
...@@ -110,11 +110,11 @@ Mixin.hasOne = function(targetModel, options) { ...@@ -110,11 +110,11 @@ Mixin.hasOne = function(targetModel, options) {
* * set[AS] - for example setUser(instance, options). Options are passed to this.save * * set[AS] - for example setUser(instance, options). Options are passed to this.save
* * create[AS] - for example createUser(value, options). Builds and saves a new instance of the associated model. Values and options are passed on to target.create * * create[AS] - for example createUser(value, options). Builds and saves a new instance of the associated model. Values and options are passed on to target.create
* *
* All methods return an event emitter. * All methods return a promise
* *
* @param {Model} target * @param {Model} target
* @param {object} [options] * @param {object} [options]
* @param {boolean} [options.hooks=false] Set to true to run before-/afterDestroy hooks when an associated model is deleted because of a cascade. For example if `User.hasOne(Profile, {onDelete: 'cascade', hooks:true})`, the before-/afterDestroy hooks for profile will be called when a user is deleted. Otherwise the profile will be deleted without invoking any hooks * @param {boolean} [options.hooks=false] Set to true to run before-/afterDestroy hooks when an associated model is deleted because of a cascade. For example if `User.hasOne(Profile, {onDelete: 'cascade', hooks:true})`, the before-/afterDestroy hooks for profile will be called when a user is deleted. Otherwise the profile will be deleted without invoking any hooks
* @param {string} [options.as] The alias of this model. If you create multiple associations between the same tables, you should provide an alias to be able to distinguish between them. If you provide an alias when creating the assocition, you should provide the same alias when eager loading and when getting assocated models. Defaults to the singularized version of target.name * @param {string} [options.as] The alias of this model. If you create multiple associations between the same tables, you should provide an alias to be able to distinguish between them. If you provide an alias when creating the assocition, you should provide the same alias when eager loading and when getting assocated models. Defaults to the singularized version of target.name
* @param {string} [options.foreignKey] The name of the foreign key in the source table. Defaults to the name of target + primary key of target * @param {string} [options.foreignKey] The name of the foreign key in the source table. Defaults to the name of target + primary key of target
* @param {string} [options.onDelete='SET&nbsp;NULL'] * @param {string} [options.onDelete='SET&nbsp;NULL']
...@@ -158,13 +158,14 @@ Mixin.belongsTo = function(targetModel, options) { ...@@ -158,13 +158,14 @@ Mixin.belongsTo = function(targetModel, options) {
* *
* * get[AS] - for example getPictures(). * * get[AS] - for example getPictures().
* * set[AS] - for example setPictures(instances, defaultAttributes|options). Update the associations. All currently associated models that are not in instances will be removed. * * set[AS] - for example setPictures(instances, defaultAttributes|options). Update the associations. All currently associated models that are not in instances will be removed.
* * add[AS] - for example addPicture(instance, defaultAttributes|options). Add another association. * * add[AS] - for example addPicture(instance, defaultAttributes|options). Add another associated object.
* * add[AS] [plural] - for example addPictures([instance1, instance2], defaultAttributes|options). Add some more associated objects.
* * create[AS] - for example createPicture(values, options). Build and save a new association. * * create[AS] - for example createPicture(values, options). Build and save a new association.
* * remove[AS] - for example removePicture(instance). Remove a single association * * remove[AS] - for example removePicture(instance). Remove a single association
* * has[AS] - for example hasPicture(instance). Is source associated to this target? * * has[AS] - for example hasPicture(instance). Is source associated to this target?
* * has[AS] [plural] - for example hasPictures(instances). Is source associated to all these targets? * * has[AS] [plural] - for example hasPictures(instances). Is source associated to all these targets?
* *
* All methods return an event emitter. * All methods return a promise
* *
* If you use a through model with custom attributes, these attributes can be set when adding / setting new associations in two ways. Consider users and projects from before * If you use a through model with custom attributes, these attributes can be set when adding / setting new associations in two ways. Consider users and projects from before
* with a join table that stores whether the project has been started yet: * with a join table that stores whether the project has been started yet:
...@@ -200,7 +201,7 @@ Mixin.belongsTo = function(targetModel, options) { ...@@ -200,7 +201,7 @@ Mixin.belongsTo = function(targetModel, options) {
* @param {Model} target * @param {Model} target
* @param {object} [options] * @param {object} [options]
* @param {boolean} [options.hooks=false] Set to true to run before-/afterDestroy hooks when an associated model is deleted because of a cascade. For example if `User.hasOne(Profile, {onDelete: 'cascade', hooks:true})`, the before-/afterDestroy hooks for profile will be called when a user is deleted. Otherwise the profile will be deleted without invoking any hooks * @param {boolean} [options.hooks=false] Set to true to run before-/afterDestroy hooks when an associated model is deleted because of a cascade. For example if `User.hasOne(Profile, {onDelete: 'cascade', hooks:true})`, the before-/afterDestroy hooks for profile will be called when a user is deleted. Otherwise the profile will be deleted without invoking any hooks
* @param {Model|string} [options.through] The name of the table that is used to join source and target in n:m associations. Can also be a sequelize model if you want to define the junction table yourself and add extra attributes to it. * @param {Model|string} [options.through] The name of the table that is used to join source and target in n:m associations. Can also be a sequelize model if you want to define the junction table yourself and add extra attributes to it.
* @param {string} [options.as] The alias of this model. If you create multiple associations between the same tables, you should provide an alias to be able to distinguish between them. If you provide an alias when creating the assocition, you should provide the same alias when eager loading and when getting assocated models. Defaults to the singularized version of target.name * @param {string} [options.as] The alias of this model. If you create multiple associations between the same tables, you should provide an alias to be able to distinguish between them. If you provide an alias when creating the assocition, you should provide the same alias when eager loading and when getting assocated models. Defaults to the singularized version of target.name
* @param {string} [options.foreignKey] The name of the foreign key in the source table. Defaults to the name of target + primary key of target * @param {string} [options.foreignKey] The name of the foreign key in the source table. Defaults to the name of target + primary key of target
* @param {string} [options.onDelete='SET&nbsp;NULL|CASCADE'] Cascade if this is a n:m, and set null if it is a 1:m * @param {string} [options.onDelete='SET&nbsp;NULL|CASCADE'] Cascade if this is a n:m, and set null if it is a 1:m
......
...@@ -92,7 +92,9 @@ Hooks.runHooks = function() { ...@@ -92,7 +92,9 @@ Hooks.runHooks = function() {
if (!!arguments[0]) { if (!!arguments[0]) {
return reject(arguments[0]) return reject(arguments[0])
} }
resolveArgs = Array.prototype.slice.call(arguments, 1) if (arguments.length) {
resolveArgs = Array.prototype.slice.call(arguments, 1)
}
return run(hooks[tick]) return run(hooks[tick])
})) }))
...@@ -101,7 +103,9 @@ Hooks.runHooks = function() { ...@@ -101,7 +103,9 @@ Hooks.runHooks = function() {
maybePromise.spread(function () { maybePromise.spread(function () {
tick++ tick++
resolveArgs = Array.prototype.slice.call(arguments) if (arguments.length) {
resolveArgs = Array.prototype.slice.call(arguments)
}
return run(hooks[tick]) return run(hooks[tick])
}, reject) }, reject)
......
...@@ -636,7 +636,7 @@ module.exports = (function() { ...@@ -636,7 +636,7 @@ module.exports = (function() {
* @param {Object} [options] A hash of options to describe the scope of the search * @param {Object} [options] A hash of options to describe the scope of the search
* @param {Object} [options.where] A hash of attributes to describe your search. See above for examples. * @param {Object} [options.where] A hash of attributes to describe your search. See above for examples.
* @param {Array<String>} [options.attributes] A list of the attributes that you want to select * @param {Array<String>} [options.attributes] A list of the attributes that you want to select
* @param {Array<Object|Model>} [options.include] A list of associations to eagerly load. Supported is either { include: [ Model1, Model2, ...] } or { include: [ { model: Model1, as: 'Alias' } ] }. If your association are set up with an `as` (eg. `X.hasMany(Y, { as: 'Z }`, you need to specify Z in the as attribute when eager loading Y). When using the object form, you can also specify `attributes` to specify what columns to load, `where` to limit the relations, and `include` to load further nested relations * @param {Array<Object|Model>} [options.include] A list of associations to eagerly load. Supported is either { include: [ Model1, Model2, ...] } or { include: [ { model: Model1, as: 'Alias' } ] }. If your association are set up with an `as` (eg. `X.hasMany(Y, { as: 'Z }`, you need to specify Z in the as attribute when eager loading Y). When using the object form, you can also specify `attributes` to specify what columns to load, `where` to limit the relations, and `include` to load further nested relations
* @param {String|Array|Sequelize.fn} [options.order] Specifies an ordering. If a string is provided, it will be esacped. Using an array, you can provide several columns / functions to order by. Each element can be further wrapped in a two-element array. The first element is the column / function to order by, the second is the direction. For example: `order: [['name', 'DESC']]`. In this way the column will be escaped, but the direction will not. * @param {String|Array|Sequelize.fn} [options.order] Specifies an ordering. If a string is provided, it will be esacped. Using an array, you can provide several columns / functions to order by. Each element can be further wrapped in a two-element array. The first element is the column / function to order by, the second is the direction. For example: `order: [['name', 'DESC']]`. In this way the column will be escaped, but the direction will not.
* @param {Number} [options.limit] * @param {Number} [options.limit]
* @param {Number} [options.offset] * @param {Number} [options.offset]
...@@ -1266,106 +1266,65 @@ module.exports = (function() { ...@@ -1266,106 +1266,65 @@ module.exports = (function() {
, query = null , query = null
, args = [] , args = []
return new Utils.CustomEventEmitter(function(emitter) { return self.runHooks(self.options.hooks.beforeBulkDestroy, where).then(function(newWhere) {
self.runHooks(self.options.hooks.beforeBulkDestroy, where, function(err, newWhere) { where = newWhere || where
if (!!err) {
return emitter.emit('error', err)
}
where = newWhere || where
if (self._timestampAttributes.deletedAt && options.force === false) { if (self._timestampAttributes.deletedAt && options.force === false) {
var attrValueHash = {} var attrValueHash = {}
attrValueHash[self._timestampAttributes.deletedAt] = Utils.now() attrValueHash[self._timestampAttributes.deletedAt] = Utils.now()
query = 'bulkUpdate' query = 'bulkUpdate'
args = [self.getTableName(), attrValueHash, where, self] args = [self.getTableName(), attrValueHash, where, self]
} else { } else {
query = 'bulkDelete' query = 'bulkDelete'
args = [self.getTableName(), where, options, self] args = [self.getTableName(), where, options, self]
} }
var runQuery = function(err, records) {
if (!!err) {
return emitter.emit('error', err)
}
query = self.QueryInterface[query].apply(self.QueryInterface, args) var runQuery = function(records) {
query.on('sql', function(sql) { return self.QueryInterface[query].apply(self.QueryInterface, args).then(function(results) {
emitter.emit('sql', sql) if (options && options.hooks === true) {
}) var tick = 0
.error(function(err) { var next = function(i) {
emitter.emit('error', err) return self.runHooks(self.options.hooks.afterDestroy, records[i]).then(function(newValues) {
}) records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues
.success(function(results) { tick++
var finished = function(err) {
if (!!err) {
return emitter.emit('error', err)
}
self.runHooks(self.options.hooks.afterBulkDestroy, where, function(err) { if (tick >= records.length) {
if (!!err) { return self.runHooks(self.options.hooks.afterBulkDestroy, where).return(results)
return emitter.emit('error', err)
} }
emitter.emit('success', results) return next(tick)
}) })
} }
if (options && options.hooks === true) { return next(tick)
var tick = 0 } else {
var next = function(i) { return self.runHooks(self.options.hooks.afterBulkDestroy, where).return(results)
self.runHooks(self.options.hooks.afterDestroy, records[i], function(err, newValues) { }
if (!!err) { })
return finished(err) }
}
records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues
tick++
if (tick >= records.length) { if (options && options.hooks === true) {
return finished() var tick = 0
} return self.all({where: where}).then(function(records) {
var next = function(i) {
return self.runHooks(self.options.hooks.beforeDestroy, records[i]).then(function(newValues) {
records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues
tick++
next(tick) if (tick >= records.length) {
}) return runQuery(records)
} }
next(tick) return next(tick)
} else { })
finished() }
}
})
}
if (options && options.hooks === true) {
var tick = 0
self.all({where: where}).error(function(err) { emitter.emit('error', err) })
.success(function(records) {
var next = function(i) {
self.runHooks(self.options.hooks.beforeDestroy, records[i], function(err, newValues) {
if (!!err) {
return runQuery(err)
}
records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues
tick++
if (tick >= records.length) {
return runQuery(null, records)
}
next(tick)
})
}
next(tick) return next(tick)
}) })
// } else {
} else { return runQuery()
runQuery() }
} })
})
}).run()
} }
/** /**
...@@ -1383,7 +1342,6 @@ module.exports = (function() { ...@@ -1383,7 +1342,6 @@ module.exports = (function() {
*/ */
Model.prototype.update = function(attrValueHash, where, options) { Model.prototype.update = function(attrValueHash, where, options) {
var self = this var self = this
, query = null
, tick = 0 , tick = 0
options = options || {} options = options || {}
...@@ -1395,121 +1353,79 @@ module.exports = (function() { ...@@ -1395,121 +1353,79 @@ module.exports = (function() {
attrValueHash[self._timestampAttributes.updatedAt] = Utils.now() attrValueHash[self._timestampAttributes.updatedAt] = Utils.now()
} }
return new Utils.CustomEventEmitter(function(emitter) { var runSave = function() {
var runSave = function() { return self.runHooks(self.options.hooks.beforeBulkUpdate, attrValueHash, where).spread(function(attributes, _where) {
self.runHooks(self.options.hooks.beforeBulkUpdate, attrValueHash, where, function(err, attributes, _where) { where = _where || where
if (!!err) { attrValueHash = attributes || attrValueHash
return emitter.emit('error', err)
}
where = _where || where
attrValueHash = attributes || attrValueHash
var runQuery = function(err, records) {
if (!!err) {
return emitter.emit('error', err)
}
query = self.QueryInterface.bulkUpdate(self.getTableName(), attrValueHash, where, options, self.rawAttributes) var runQuery = function(records) {
query.on('sql', function(sql) { return self.QueryInterface.bulkUpdate(self.getTableName(), attrValueHash, where, options, self.rawAttributes).then(function(results) {
emitter.emit('sql', sql) if (options && options.hooks === true && !!records && records.length > 0) {
}) var tick = 0
.error(function(err) { var next = function(i) {
emitter.emit('error', err) return self.runHooks(self.options.hooks.afterUpdate, records[i]).then(function(newValues) {
}) records[i].dataValues = (!!newValues && newValues.dataValues) ? newValues.dataValues : records[i].dataValues
.success(function(results) { tick++
var finished = function(err, records) {
if (!!err) {
return emitter.emit('error', err)
}
self.runHooks(self.options.hooks.afterBulkUpdate, attrValueHash, where, function(err) { if (tick >= records.length) {
if (!!err) { return self.runHooks(self.options.hooks.afterBulkUpdate, attrValueHash, where).return(records)
return emitter.emit('error', err)
} }
emitter.emit('success', records) return next(tick)
}) })
} }
if (options && options.hooks === true && !!records && records.length > 0) { return next(tick)
var tick = 0 } else {
var next = function(i) { return self.runHooks(self.options.hooks.afterBulkUpdate, attrValueHash, where).return(results)
self.runHooks(self.options.hooks.afterUpdate, records[i], function(err, newValues) { }
if (!!err) { })
return finished(err) }
}
records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues if (options.hooks === true) {
tick++ return self.all({where: where}).then(function(records) {
if (records === null || records.length < 1) {
return runQuery()
}
if (tick >= records.length) { var next = function(i) {
return finished(null, records) return self.runHooks(self.options.hooks.beforeUpdate, records[i]).then(function(newValues) {
} records[i].dataValues = (!!newValues && newValues.dataValues) ? newValues.dataValues : records[i].dataValues
tick++
next(tick) if (tick >= records.length) {
}) return runQuery(records)
} }
next(tick) return next(tick)
} else { })
finished(null, results) }
}
})
}
if (options.hooks === true) {
self.all({where: where}).error(function(err) { emitter.emit('error', err) })
.success(function(records) {
if (records === null || records.length < 1) {
return runQuery(null)
}
var next = function(i) {
self.runHooks(self.options.hooks.beforeUpdate, records[i], function(err, newValues) {
if (!!err) {
return runQuery(err)
}
records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues
tick++
if (tick >= records.length) {
return runQuery(null, records)
}
next(tick)
})
}
next(tick) return next(tick)
}) })
} else { } else {
runQuery() return runQuery()
} }
}) })
} }
if (options.validate === true) { if (options.validate === true) {
var build = self.build(attrValueHash) var build = self.build(attrValueHash)
// We want to skip validations for all other fields // We want to skip validations for all other fields
var updatedFields = Object.keys(attrValueHash) var updatedFields = Object.keys(attrValueHash)
var skippedFields = Utils._.difference(Object.keys(self.attributes), updatedFields) var skippedFields = Utils._.difference(Object.keys(self.attributes), updatedFields)
build.hookValidate({skip: skippedFields}).error(function(err) { return build.hookValidate({skip: skippedFields}).then(function(attributes) {
emitter.emit('error', err) if (!!attributes && !!attributes.dataValues) {
}).success(function(attributes) { attrValueHash = Utils._.pick.apply(Utils._, [].concat(attributes.dataValues).concat(Object.keys(attrValueHash)))
if (!!attributes && !!attributes.dataValues) { }
attrValueHash = Utils._.pick.apply(Utils._, [].concat(attributes.dataValues).concat(Object.keys(attrValueHash)))
}
runSave() return runSave()
}) })
} else { } else {
runSave() return runSave()
} }
}).run()
} }
/** /**
......
var util = require("util") var Promise
, Promise
, EventEmitter = require("events").EventEmitter , EventEmitter = require("events").EventEmitter
, proxyEventKeys = ['success', 'error', 'sql'] , proxyEventKeys = ['success', 'error', 'sql']
, Utils = require('./utils') , Utils = require('./utils')
...@@ -12,7 +11,7 @@ var util = require("util") ...@@ -12,7 +11,7 @@ var util = require("util")
* @mixes https://github.com/petkaantonov/bluebird/blob/master/API.md * @mixes https://github.com/petkaantonov/bluebird/blob/master/API.md
* @class Promise * @class Promise
*/ */
var SequelizePromise = Promise = require('bluebird') var SequelizePromise = Promise = require('sequelize-bluebird')
/** /**
* Listen for events, event emitter style. Mostly for backwards compat. with EventEmitter * Listen for events, event emitter style. Mostly for backwards compat. with EventEmitter
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
"generic-pool": "2.0.4", "generic-pool": "2.0.4",
"sql": "~0.35.0", "sql": "~0.35.0",
"circular-json": "~0.1.5", "circular-json": "~0.1.5",
"bluebird": "git://github.com/sequelize/bluebird.git", "sequelize-bluebird": "git://github.com/sequelize/bluebird.git",
"node-uuid": "~1.4.1" "node-uuid": "~1.4.1"
}, },
"devDependencies": { "devDependencies": {
......
...@@ -293,6 +293,35 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { ...@@ -293,6 +293,35 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
}) })
}) })
describe('addMultipleAssociations', function () {
it('adds associations without removing the current ones', function () {
var User = this.sequelize.define('User', { username: DataTypes.STRING })
, Task = this.sequelize.define('Task', { title: DataTypes.STRING })
Task.hasMany(User)
return this.sequelize.sync({ force: true }).then(function() {
return User.bulkCreate([
{ username: 'foo '},
{ username: 'bar '},
{ username: 'baz '}
]).then(function () {
return Task.create({ title: 'task' }).then(function (task) {
return User.findAll().then(function(users) {
return task.setUsers([users[0]]).then(function () {
return task.addUsers([users[1], users[2]]).then(function () {
return task.getUsers().then(function (users) {
expect(users).to.have.length(3)
})
})
})
})
})
})
})
})
})
it("clears associations when passing null to the set-method with omitNull set to true", function(done) { it("clears associations when passing null to the set-method with omitNull set to true", function(done) {
this.sequelize.options.omitNull = true this.sequelize.options.omitNull = true
...@@ -923,6 +952,36 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { ...@@ -923,6 +952,36 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
}) })
}) })
describe('addMultipleAssociations', function () {
it('adds associations without removing the current ones', function () {
var User = this.sequelize.define('User', { username: DataTypes.STRING })
, Task = this.sequelize.define('Task', { title: DataTypes.STRING })
User.hasMany(Task)
Task.hasMany(User)
return this.sequelize.sync({ force: true }).then(function() {
return User.bulkCreate([
{ username: 'foo '},
{ username: 'bar '},
{ username: 'baz '}
]).then(function () {
return Task.create({ title: 'task' }).then(function (task) {
return User.findAll().then(function(users) {
return task.setUsers([users[0]]).then(function () {
return task.addUsers([users[1], users[2]]).then(function () {
return task.getUsers().then(function (users) {
expect(users).to.have.length(3)
})
})
})
})
})
})
})
})
})
describe('optimizations using bulk create, destroy and update', function () { describe('optimizations using bulk create, destroy and update', function () {
beforeEach(function (done) { beforeEach(function (done) {
this.User = this.sequelize.define('User', { username: DataTypes.STRING }, {timestamps: false}) this.User = this.sequelize.define('User', { username: DataTypes.STRING }, {timestamps: false})
......
...@@ -3,7 +3,6 @@ var chai = require('chai') ...@@ -3,7 +3,6 @@ var chai = require('chai')
, Support = require(__dirname + '/support') , Support = require(__dirname + '/support')
, DataTypes = require(__dirname + "/../lib/data-types") , DataTypes = require(__dirname + "/../lib/data-types")
, SequelizePromise = require(__dirname + "/../lib/promise") , SequelizePromise = require(__dirname + "/../lib/promise")
, Promise = require('bluebird')
, dialect = Support.getTestDialect() , dialect = Support.getTestDialect()
, _ = require('lodash') , _ = require('lodash')
, sinon = require('sinon') , sinon = require('sinon')
...@@ -389,7 +388,7 @@ describe(Support.getTestDialectTeaser("Promise"), function () { ...@@ -389,7 +388,7 @@ describe(Support.getTestDialectTeaser("Promise"), function () {
it('should still work with .done() when resolving multiple results', function(done) { it('should still work with .done() when resolving multiple results', function(done) {
var spy = sinon.spy() var spy = sinon.spy()
, promise = new SequelizePromise(function (resolve, reject) { , promise = new SequelizePromise(function (resolve, reject) {
resolve(Promise.all(['MyModel', true])); resolve(SequelizePromise.all(['MyModel', true]));
}); });
promise.spread(spy); promise.spread(spy);
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!