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

Commit 73ece0f3 by Sascha Depold

Merge branch 'jointablemodel' of https://github.com/janmeier/sequelize into janmeier-jointablemodel

Conflicts:
	lib/associations/has-many.js
2 parents b606fd70 7b07cd00
......@@ -4,27 +4,61 @@ module.exports = (function() {
var HasManyDoubleLinked = function(definition, instance) {
this.__factory = definition
this.instance = instance
// Alias the quoting methods for code brevity
this.QueryInterface = instance.QueryInterface
}
HasManyDoubleLinked.prototype.injectGetter = function(options) {
var self = this, _options = options
var customEventEmitter = new Utils.CustomEventEmitter(function() {
var where = {}, options = _options || {}
var where = {}
, connectorDAO = self.__factory.connectorDAO
, options = _options || {}
, queryOptions = {}
, association = self.__factory.target.associations[self.__factory.associationAccessor]
//fully qualify
var instancePrimaryKeys = Object.keys(self.instance.daoFactory.primaryKeys)
, instancePrimaryKey = instancePrimaryKeys.length > 0 ? instancePrimaryKeys[0] : 'id'
where[self.__factory.connectorDAO.tableName+"."+self.__factory.identifier] = self.instance[instancePrimaryKey]
where[connectorDAO.tableName+"."+self.__factory.identifier] = self.instance[instancePrimaryKey]
var primaryKeys = Object.keys(self.__factory.connectorDAO.rawAttributes)
var primaryKeys = Object.keys(connectorDAO.primaryKeys)
, foreignKey = primaryKeys.filter(function(pk) { return pk != self.__factory.identifier })[0]
, foreignPrimary = Object.keys(self.__factory.target.primaryKeys)
foreignPrimary = foreignPrimary.length === 1 ? foreignPrimary[0] : 'id'
where[self.__factory.connectorDAO.tableName+"."+foreignKey] = {join: self.__factory.target.tableName+"."+foreignPrimary}
where[connectorDAO.tableName+"."+foreignKey] = {join: self.__factory.target.tableName+"."+foreignPrimary}
if (association.hasJoinTableModel) {
queryOptions.hasJoinTableModel = true
queryOptions.joinTableModel = connectorDAO
if (!options.attributes) {
options.attributes = [
self.QueryInterface.quoteIdentifier(self.__factory.target.tableName)+".*"
]
}
if (options.joinTableAttributes) {
options.joinTableAttributes.forEach(function (elem) {
options.attributes.push(
self.QueryInterface.quoteIdentifiers(connectorDAO.tableName + '.' + elem) + ' as ' +
self.QueryInterface.quoteIdentifier(connectorDAO.name + '.' + elem, true)
)
})
} else {
Utils._.forOwn(connectorDAO.rawAttributes, function (elem, key) {
options.attributes.push(
self.QueryInterface.quoteIdentifiers(connectorDAO.tableName + '.' + key) + ' as ' +
self.QueryInterface.quoteIdentifier(connectorDAO.name + '.' + key, true)
)
})
}
}
if (options.where) {
if (Array.isArray(options.where)) {
......@@ -44,7 +78,7 @@ module.exports = (function() {
options.where = where;
}
self.__factory.target.findAllJoin(self.__factory.connectorDAO.tableName, options)
self.__factory.target.findAllJoin(connectorDAO.tableName, options, queryOptions)
.on('success', function(objects) { customEventEmitter.emit('success', objects) })
.on('error', function(err){ customEventEmitter.emit('error', err) })
.on('sql', function(sql) { customEventEmitter.emit('sql', sql)})
......@@ -53,26 +87,41 @@ module.exports = (function() {
return customEventEmitter.run()
}
HasManyDoubleLinked.prototype.injectSetter = function(emitterProxy, oldAssociations, newAssociations) {
HasManyDoubleLinked.prototype.injectSetter = function(emitterProxy, oldAssociations, newAssociations, defaultAttributes) {
var self = this
, chainer = new Utils.QueryChainer()
, association = self.__factory.target.associations[self.__factory.associationAccessor]
, foreignIdentifier = association.isSelfAssociation ? association.foreignIdentifier : association.identifier
, sourceKeys = Object.keys(self.__factory.source.primaryKeys)
, targetKeys = Object.keys(self.__factory.target.primaryKeys)
, obsoleteAssociations = []
, changedAssociations = []
, unassociatedObjects;
var obsoleteAssociations = oldAssociations.filter(function (old) {
// Return only those old associations that are not found in new
return !Utils._.find(newAssociations, function (obj) {
return ((targetKeys.length === 1) ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id)
unassociatedObjects = newAssociations.filter(function (obj) {
return !Utils._.find(oldAssociations, function (old) {
return (!!obj[foreignIdentifier] && !!old[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : obj.id === old.id)
})
})
var unassociatedObjects = newAssociations.filter(function (obj) {
// Return only those associations that are new
return !Utils._.find(oldAssociations, function (old) {
return ((targetKeys.length === 1) ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id)
oldAssociations.forEach(function (old) {
var newObj = Utils._.find(newAssociations, function (obj) {
return (!!obj[foreignIdentifier] && !!old[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : obj.id === old.id)
})
if (!newObj) {
obsoleteAssociations.push(old)
} else if (association.hasJoinTableModel) {
var changedAssociation = {
where: {},
attributes: Utils._.defaults({}, newObj[self.__factory.connectorDAO.name], defaultAttributes)
}
changedAssociation.where[self.__factory.identifier] = self.instance[self.__factory.identifier] || self.instance.id
changedAssociation.where[foreignIdentifier] = newObj[foreignIdentifier] || newObj.id
changedAssociations.push(changedAssociation)
}
})
if (obsoleteAssociations.length > 0) {
......@@ -93,12 +142,22 @@ module.exports = (function() {
attributes[self.__factory.identifier] = ((sourceKeys.length === 1) ? self.instance[sourceKeys[0]] : self.instance.id)
attributes[foreignIdentifier] = ((targetKeys.length === 1) ? unassociatedObject[targetKeys[0]] : unassociatedObject.id)
if (association.hasJoinTableModel) {
attributes = Utils._.defaults(attributes, unassociatedObject[association.connectorDAO.name], defaultAttributes)
}
return attributes
})
chainer.add(self.__factory.connectorDAO.bulkCreate(bulk))
}
if (changedAssociations.length > 0) {
changedAssociations.forEach(function (assoc) {
chainer.add(self.__factory.connectorDAO.update(assoc.attributes, assoc.where))
})
}
chainer
.run()
.success(function() { emitterProxy.emit('success', newAssociations) })
......@@ -106,7 +165,7 @@ module.exports = (function() {
.on('sql', function(sql) { emitterProxy.emit('sql', sql) })
}
HasManyDoubleLinked.prototype.injectAdder = function(emitterProxy, newAssociation) {
HasManyDoubleLinked.prototype.injectAdder = function(emitterProxy, newAssociation, additionalAttributes, exists) {
var attributes = {}
, association = this.__factory.target.associations[this.__factory.associationAccessor]
, foreignIdentifier = association.isSelfAssociation ? association.foreignIdentifier : association.identifier;
......@@ -117,10 +176,21 @@ module.exports = (function() {
attributes[this.__factory.identifier] = ((sourceKeys.length === 1) ? this.instance[sourceKeys[0]] : this.instance.id)
attributes[foreignIdentifier] = ((targetKeys.length === 1) ? newAssociation[targetKeys[0]] : newAssociation.id)
this.__factory.connectorDAO.create(attributes)
.success(function() { emitterProxy.emit('success', newAssociation) })
.error(function(err) { emitterProxy.emit('error', err) })
.on('sql', function(sql) { emitterProxy.emit('sql', sql) })
if (exists) { // implies hasJoinTableModel === true
var where = attributes
attributes = Utils._.defaults({}, newAssociation[association.connectorDAO.name], additionalAttributes)
association.connectorDAO.update(attributes, where).proxy(emitterProxy)
} else {
if (association.hasJoinTableModel === true) {
attributes = Utils._.defaults(attributes, newAssociation[association.connectorDAO.name], additionalAttributes)
}
this.__factory.connectorDAO.create(attributes)
.success(function() { emitterProxy.emit('success', newAssociation) })
.error(function(err) { emitterProxy.emit('error', err) })
.on('sql', function(sql) { emitterProxy.emit('sql', sql) })
}
}
return HasManyDoubleLinked
......
......@@ -13,15 +13,23 @@ module.exports = (function() {
this.options = options
this.useJunctionTable = this.options.useJunctionTable === undefined ? true : this.options.useJunctionTable
this.isSelfAssociation = (this.source.tableName === this.target.tableName)
this.hasJoinTableModel = !!this.options.joinTableModel
var combinedTableName = Utils.combineTableNames(
this.source.tableName,
this.isSelfAssociation ? (this.options.as || this.target.tableName) : this.target.tableName
)
this.options.tableName = this.combinedName = (this.options.joinTableName || combinedTableName)
this.associationAccessor = this.options.as || this.combinedName
var combinedTableName;
if (this.hasJoinTableModel) {
combinedTableName = this.options.joinTableModel.tableName
} else if (this.options.joinTableName) {
combinedTableName = this.options.joinTableName
} else {
combinedTableName = Utils.combineTableNames(
this.source.tableName,
this.isSelfAssociation ? (this.options.as || this.target.tableName) : this.target.tableName
)
}
this.options.useHooks = options.useHooks
this.options.tableName = this.combinedName = (this.options.joinTableName || combinedTableName)
this.options.useHooks = options.useHooks
this.associationAccessor = this.options.as || this.combinedName
var as = (this.options.as || Utils.pluralize(this.target.tableName, this.target.options.language))
......@@ -39,6 +47,7 @@ module.exports = (function() {
// or in an extra table which connects two tables
HasMany.prototype.injectAttributes = function() {
var multiAssociation = this.target.associations.hasOwnProperty(this.associationAccessor)
, self = this
this.identifier = this.options.foreignKey || Utils._.underscoredIf(Utils.singularize(this.source.tableName, this.source.options.language) + "Id", this.options.underscored)
// is there already a single sided association between the source and the target?
......@@ -63,7 +72,21 @@ module.exports = (function() {
combinedTableAttributes[this.identifier] = {type: sourceKeyType, primaryKey: true}
combinedTableAttributes[this.foreignIdentifier] = {type: targetKeyType, primaryKey: true}
this.connectorDAO = this.source.daoFactoryManager.sequelize.define(this.combinedName, combinedTableAttributes, this.options)
if (this.hasJoinTableModel === true) {
this.connectorDAO = this.options.joinTableModel
// remove any previously defined PKs
Utils._.each(this.connectorDAO.attributes, function(dataTypeString, attributeName) {
if (dataTypeString.toString().indexOf('PRIMARY KEY') !== -1) {
delete self.connectorDAO.rawAttributes[attributeName]
}
})
this.connectorDAO.rawAttributes = Utils._.merge(this.connectorDAO.rawAttributes, combinedTableAttributes)
this.connectorDAO.init(this.connectorDAO.daoFactoryManager)
} else {
this.connectorDAO = this.source.daoFactoryManager.sequelize.define(this.combinedName, combinedTableAttributes, this.options)
}
if (!this.isSelfAssociation) {
this.target.associations[this.associationAccessor].connectorDAO = this.connectorDAO
......@@ -94,7 +117,7 @@ module.exports = (function() {
return new Class(self, this).injectGetter(options)
}
obj[this.accessors.hasAll] = function(objects) {
obj[this.accessors.hasAll] = function(objects) {
var instance = this;
var customEventEmitter = new Utils.CustomEventEmitter(function() {
instance[self.accessors.get]()
......@@ -137,7 +160,7 @@ module.exports = (function() {
HasMany.prototype.injectSetter = function(obj) {
var self = this
obj[this.accessors.set] = function(newAssociatedObjects) {
obj[this.accessors.set] = function(newAssociatedObjects, defaultAttributes) {
if (newAssociatedObjects === null) {
newAssociatedObjects = []
}
......@@ -149,7 +172,7 @@ module.exports = (function() {
instance[self.accessors.get]()
.success(function(oldAssociatedObjects) {
var Class = self.connectorDAO ? HasManyMultiLinked : HasManySingleLinked
new Class(self, instance).injectSetter(emitter, oldAssociatedObjects, newAssociatedObjects)
new Class(self, instance).injectSetter(emitter, oldAssociatedObjects, newAssociatedObjects, defaultAttributes)
})
.error(function(err) {
emitter.emit('error', err)
......@@ -160,20 +183,21 @@ module.exports = (function() {
}).run()
}
obj[this.accessors.add] = function(newAssociatedObject) {
obj[this.accessors.add] = function(newAssociatedObject, additionalAttributes) {
var instance = this
, primaryKeys = Object.keys(newAssociatedObject.daoFactory.primaryKeys || {})
, primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id'
, where = {}
where[newAssociatedObject.daoFactory.tableName+'.'+primaryKey] = newAssociatedObject[primaryKey]
return new Utils.CustomEventEmitter(function(emitter) {
instance[self.accessors.get]({ where: where })
.error(function(err){ emitter.emit('error', err)})
.success(function(currentAssociatedObjects) {
if (currentAssociatedObjects.length === 0) {
if (currentAssociatedObjects.length === 0 || self.hasJoinTableModel === true) {
var Class = self.connectorDAO ? HasManyMultiLinked : HasManySingleLinked
new Class(self, instance).injectAdder(emitter, newAssociatedObject)
new Class(self, instance).injectAdder(emitter, newAssociatedObject, additionalAttributes, !!currentAssociatedObjects.length)
} else {
emitter.emit('success', newAssociatedObject);
}
......@@ -190,9 +214,7 @@ module.exports = (function() {
currentAssociatedObjects.forEach(function(association) {
if (!Utils._.isEqual(oldAssociatedObject.identifiers, association.identifiers)) {
newAssociations[newAssociations.length] = association
} else {
oldAssociations[oldAssociations.length] = association
newAssociations.push(association)
}
})
......
......@@ -352,14 +352,16 @@ module.exports = (function() {
}
//right now, the caller (has-many-double-linked) is in charge of the where clause
DAOFactory.prototype.findAllJoin = function(joinTableName, options) {
DAOFactory.prototype.findAllJoin = function(joinTableName, options, queryOptions) {
var optcpy = Utils._.clone(options)
optcpy.attributes = optcpy.attributes || [this.QueryInterface.quoteIdentifier(this.tableName)+".*"]
// whereCollection is used for non-primary key updates
this.options.whereCollection = optcpy.where || null;
return this.QueryInterface.select(this, [this.getTableName(), joinTableName], optcpy, { type: 'SELECT' })
return this.QueryInterface.select(this, [this.getTableName(), joinTableName], optcpy, Utils._.defaults({
type: 'SELECT'
}, queryOptions))
}
/**
......
......@@ -260,6 +260,21 @@ module.exports = (function() {
result = result.map(Dot.transform)
} else if (this.options.hasJoin === true) {
result = transformRowsWithEagerLoadingIntoDaos.call(this, results)
} else if (this.options.hasJoinTableModel === true) {
result = results.map(function(result) {
result = Dot.transform(result)
var joinTableData = result[this.options.joinTableModel.name]
, joinTableDAO = this.options.joinTableModel.build(joinTableData, { isNewRecord: false, isDirty: false })
, mainDao
delete result[this.options.joinTableModel.name]
mainDao = this.callee.build(result, { isNewRecord: false, isDirty: false })
mainDao[this.options.joinTableModel.name] = joinTableDAO
return mainDao
}.bind(this))
} else {
result = results.map(function(result) {
return this.callee.build(result, { isNewRecord: false, isDirty: false })
......
......@@ -580,6 +580,136 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
done()
})
})
describe('join table model', function () {
beforeEach(function (done) {
this.User = this.sequelize.define('User', {})
this.Project = this.sequelize.define('Project', {})
this.UserProjects = this.sequelize.define('UserProjects', {
status: DataTypes.STRING,
data: DataTypes.INTEGER
})
this.User.hasMany(this.Project, { joinTableModel: this.UserProjects })
this.Project.hasMany(this.User, { joinTableModel: this.UserProjects })
this.sequelize.sync().success(function() { done() })
})
describe('fetching from join table', function () {
it('should contain the data from the join table on .UserProjects a DAO', function (done) {
var self = this
self.User.create().success(function (u) {
self.Project.create().success(function (p) {
u.addProject(p, { status: 'active', data: 42 }).success(function() {
u.getProjects().success(function(projects) {
var project = projects[0]
expect(project.UserProjects).to.be.defined
expect(project.status).not.to.exist
expect(project.UserProjects.status).to.equal('active')
expect(project.UserProjects.data).to.equal(42)
done()
})
})
})
})
})
it('should be able to limit the join table attributes returned', function (done) {
var self = this
self.User.create().success(function (u) {
self.Project.create().success(function (p) {
u.addProject(p, { status: 'active', data: 42 }).success(function() {
u.getProjects({ joinTableAttributes: ['status']}).success(function(projects) {
var project = projects[0]
expect(project.UserProjects).to.be.defined
expect(project.status).not.to.exist
expect(project.UserProjects.status).to.equal('active')
expect(project.UserProjects.data).not.to.exist
done()
})
})
})
})
})
})
describe('inserting in join table', function () {
describe('add', function () {
it('should insert data provided on the object into the join table', function (done) {
var self = this
self.User.create().success(function (u) {
self.Project.create().success(function (p) {
p.UserProjects = {
status: 'active'
}
u.addProject(p).success(function() {
self.UserProjects.find({ where: { UserId: u.id, ProjectId: p.id }}).success(function (up) {
expect(up.status).to.equal('active')
done()
})
})
})
})
})
it('should insert data provided as a second argument into the join table', function (done) {
var self = this
self.User.create().success(function (u) {
self.Project.create().success(function (p) {
u.addProject(p, { status: 'active' }).success(function() {
self.UserProjects.find({ where: { UserId: u.id, ProjectId: p.id }}).success(function (up) {
expect(up.status).to.equal('active')
done()
})
})
})
})
})
})
describe('set', function () {
it('should be able to combine properties on the associated objects, and default values', function (done) {
var self = this
, _done = _.after(2, done)
self.User.create().success(function (u) {
self.Project.bulkCreate([{}, {}]).success(function () {
self.Project.findAll().success(function (projects) {
var p1 = projects[0]
, p2 = projects[1]
p1.UserProjects = {
status: 'inactive'
}
u.setProjects([p1, p2], { status: 'active' }).success(function() {
self.UserProjects.find({ where: { UserId: u.id, ProjectId: p1.id }}).success(function (up) {
expect(up.status).to.equal('inactive')
_done()
})
self.UserProjects.find({ where: { UserId: u.id, ProjectId: p2.id }}).success(function (up) {
expect(up.status).to.equal('active')
_done()
})
})
})
})
})
})
})
})
})
})
describe("Foreign key constraints", function() {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!