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

Commit 061004f0 by Mick Hansen

Merge pull request #1178 from mickhansen/nested-include

Support nested includes
2 parents cfc08647 e67f378e
...@@ -376,9 +376,7 @@ module.exports = (function() { ...@@ -376,9 +376,7 @@ module.exports = (function() {
if (options.hasOwnProperty('include')) { if (options.hasOwnProperty('include')) {
hasJoin = true hasJoin = true
options.include = options.include.map(function(include) { validateIncludedElements.call(this, options)
return validateIncludedElement.call(this, include)
}.bind(this))
} }
// whereCollection is used for non-primary key updates // whereCollection is used for non-primary key updates
...@@ -461,9 +459,7 @@ module.exports = (function() { ...@@ -461,9 +459,7 @@ module.exports = (function() {
if (options.hasOwnProperty('include')) { if (options.hasOwnProperty('include')) {
hasJoin = true hasJoin = true
options.include = options.include.map(function(include) { validateIncludedElements.call(this, options)
return validateIncludedElement.call(this, include)
}.bind(this))
} }
// whereCollection is used for non-primary key updates // whereCollection is used for non-primary key updates
...@@ -558,7 +554,7 @@ module.exports = (function() { ...@@ -558,7 +554,7 @@ module.exports = (function() {
DAOFactory.prototype.build = function(values, options) { DAOFactory.prototype.build = function(values, options) {
options = options || { isNewRecord: true, isDirty: true } options = options || { isNewRecord: true, isDirty: true }
if (options.hasOwnProperty('include') && (!options.includeValidated || !options.includeNames)) { if (options.hasOwnProperty('include') && options.include && (!options.includeValidated || !options.includeNames)) {
options.includeNames = [] options.includeNames = []
options.include = options.include.map(function(include) { options.include = options.include.map(function(include) {
include = validateIncludedElement.call(this, include) include = validateIncludedElement.call(this, include)
...@@ -790,7 +786,7 @@ module.exports = (function() { ...@@ -790,7 +786,7 @@ module.exports = (function() {
}) })
.error(function(err) { .error(function(err) {
emitter.emit('error', err) emitter.emit('error', err)
}).success(function() { }).success(function(rows) {
done() done()
}) })
} }
...@@ -1194,17 +1190,37 @@ module.exports = (function() { ...@@ -1194,17 +1190,37 @@ module.exports = (function() {
}.bind(this)) }.bind(this))
} }
var validateIncludedElement = function(include) { var validateIncludedElements = function(options) {
options.includeNames = []
options.includeMap = {}
options.include = options.include.map(function(include) {
include = validateIncludedElement.call(this, include, options.daoFactory)
options.includeMap[include.as] = include
options.includeNames.push(include.as)
return include
}.bind(this))
};
var validateIncludedElement = function(include, parent) {
if (include instanceof DAOFactory) { if (include instanceof DAOFactory) {
include = { daoFactory: include, as: include.tableName } include = { daoFactory: include, as: include.tableName }
} }
if (typeof parent === "undefined") {
parent = this
}
if (typeof include === 'object') { if (typeof include === 'object') {
if (include.hasOwnProperty('model')) { if (include.hasOwnProperty('model')) {
include.daoFactory = include.model include.daoFactory = include.model
delete include.model delete include.model
} }
if (!include.hasOwnProperty('as')) {
include.as = include.daoFactory.tableName
}
if (include.hasOwnProperty('attributes')) { if (include.hasOwnProperty('attributes')) {
var primaryKeys; var primaryKeys;
if (include.daoFactory.hasPrimaryKeys) { if (include.daoFactory.hasPrimaryKeys) {
...@@ -1222,7 +1238,7 @@ module.exports = (function() { ...@@ -1222,7 +1238,7 @@ module.exports = (function() {
if (include.hasOwnProperty('daoFactory') && (include.hasOwnProperty('as'))) { if (include.hasOwnProperty('daoFactory') && (include.hasOwnProperty('as'))) {
var usesAlias = (include.as !== include.daoFactory.tableName) var usesAlias = (include.as !== include.daoFactory.tableName)
, association = (usesAlias ? this.getAssociationByAlias(include.as) : this.getAssociation(include.daoFactory)) , association = (usesAlias ? parent.getAssociationByAlias(include.as) : parent.getAssociation(include.daoFactory))
// If single (1:1) association, we singularize the alias, so it will match the automatically generated alias of belongsTo/HasOne // If single (1:1) association, we singularize the alias, so it will match the automatically generated alias of belongsTo/HasOne
if (association && !usesAlias && association.isSingleAssociation) { if (association && !usesAlias && association.isSingleAssociation) {
...@@ -1233,6 +1249,10 @@ module.exports = (function() { ...@@ -1233,6 +1249,10 @@ module.exports = (function() {
if (!!association && (!association.options.as || (association.options.as === include.as))) { if (!!association && (!association.options.as || (association.options.as === include.as))) {
include.association = association include.association = association
if (include.hasOwnProperty('include')) {
validateIncludedElements(include)
}
return include return include
} else { } else {
var msg = include.daoFactory.name var msg = include.daoFactory.name
......
...@@ -543,7 +543,14 @@ module.exports = (function() { ...@@ -543,7 +543,14 @@ module.exports = (function() {
accessor = accessor.slice(0,1).toLowerCase() + accessor.slice(1) accessor = accessor.slice(0,1).toLowerCase() + accessor.slice(1)
attrs[key].forEach(function(data) { attrs[key].forEach(function(data) {
var daoInstance = include.daoFactory.build(data, { isNewRecord: false, isDirty: false }) var daoInstance = include.daoFactory.build(data, {
isNewRecord: false,
isDirty: false,
include: include.include,
includeNames: include.includeNames,
includeMap: include.includeMap,
includeValidated: true
})
, isEmpty = !Utils.firstValueOfHash(daoInstance.identifiers) , isEmpty = !Utils.firstValueOfHash(daoInstance.identifiers)
if (association.isSingleAssociation) { if (association.isSingleAssociation) {
......
...@@ -361,8 +361,9 @@ module.exports = (function() { ...@@ -361,8 +361,9 @@ module.exports = (function() {
- offset -> An offset value to start from. Only useable with limit! - offset -> An offset value to start from. Only useable with limit!
*/ */
selectQuery: function(tableName, options, factory) { selectQuery: function(tableName, options, factory) {
var table = null, var table = null
joinQuery = "" , self = this
, joinQuery = ""
options = options || {} options = options || {}
options.table = table = Array.isArray(tableName) ? tableName.map(function(t) { return this.quoteIdentifiers(t) }.bind(this)).join(", ") : this.quoteIdentifiers(tableName) options.table = table = Array.isArray(tableName) ? tableName.map(function(t) { return this.quoteIdentifiers(t) }.bind(this)).join(", ") : this.quoteIdentifiers(tableName)
...@@ -378,39 +379,59 @@ module.exports = (function() { ...@@ -378,39 +379,59 @@ module.exports = (function() {
if (options.include) { if (options.include) {
var optAttributes = options.attributes === '*' ? [options.table + '.*'] : [options.attributes] var optAttributes = options.attributes === '*' ? [options.table + '.*'] : [options.attributes]
options.include.forEach(function(include) { var generateJoinQuery = function(include, parentTable) {
var attributes = include.attributes.map(function(attr) {
return this.quoteIdentifier(include.as) + "." + this.quoteIdentifier(attr) + " AS " + this.quoteIdentifier(include.as + "." + attr)
}.bind(this))
optAttributes = optAttributes.concat(attributes)
var table = include.daoFactory.tableName var table = include.daoFactory.tableName
, as = include.as , as = include.as
, joinQueryItem = ""
, attributes
if (!(Object(include.association.through) === include.association.through)) { if (tableName !== parentTable) as = parentTable+'.'+include.as
var primaryKeysLeft = ((include.association.associationType === 'BelongsTo') ? Object.keys(include.association.target.primaryKeys) : Object.keys(include.association.source.primaryKeys)) attributes = include.attributes.map(function(attr) {
, tableLeft = ((include.association.associationType === 'BelongsTo') ? include.as : tableName) return self.quoteIdentifier(as) + "." + self.quoteIdentifier(attr) + " AS " + self.quoteIdentifier(as + "." + attr)
, attrLeft = ((primaryKeysLeft.length !== 1) ? 'id' : primaryKeysLeft[0]) })
, tableRight = ((include.association.associationType === 'BelongsTo') ? tableName : include.as)
, attrRight = include.association.identifier
joinQuery += " LEFT OUTER JOIN " + this.quoteIdentifier(table) + " AS " + this.quoteIdentifier(as) + " ON " + this.quoteIdentifier(tableLeft) + "." + this.quoteIdentifier(attrLeft) + " = " + this.quoteIdentifier(tableRight) + "." + this.quoteIdentifier(attrRight) optAttributes = optAttributes.concat(attributes)
} else {
if (include.association.doubleLinked) {
var primaryKeysSource = Object.keys(include.association.source.primaryKeys) var primaryKeysSource = Object.keys(include.association.source.primaryKeys)
, tableSource = tableName , tableSource = parentTable
, identSource = include.association.identifier , identSource = include.association.identifier
, attrSource = ((!include.association.source.hasPrimaryKeys || primaryKeysSource.length !== 1) ? 'id' : primaryKeysSource[0]) , attrSource = ((!include.association.source.hasPrimaryKeys || primaryKeysSource.length !== 1) ? 'id' : primaryKeysSource[0])
var primaryKeysTarget = Object.keys(include.association.target.primaryKeys) var primaryKeysTarget = Object.keys(include.association.target.primaryKeys)
, tableTarget = include.as , tableTarget = as
, identTarget = include.association.foreignIdentifier , identTarget = include.association.foreignIdentifier
, attrTarget = ((!include.association.target.hasPrimaryKeys || primaryKeysTarget.length !== 1) ? 'id' : primaryKeysTarget[0]) , attrTarget = ((!include.association.target.hasPrimaryKeys || primaryKeysTarget.length !== 1) ? 'id' : primaryKeysTarget[0])
var tableJunction = include.association.through.tableName var tableJunction = include.association.through.tableName
joinQuery += " LEFT OUTER JOIN " + this.quoteIdentifier(tableJunction) + " ON " + this.quoteIdentifier(tableSource) + "." + this.quoteIdentifier(attrSource) + " = " + this.quoteIdentifier(tableJunction) + "." + this.quoteIdentifier(identSource) joinQueryItem += " LEFT OUTER JOIN " + self.quoteIdentifier(tableJunction) + " ON "
joinQuery += " LEFT OUTER JOIN " + this.quoteIdentifier(table) + " AS " + this.quoteIdentifier(as) + " ON " + this.quoteIdentifier(tableTarget) + "." + this.quoteIdentifier(attrTarget) + " = " + this.quoteIdentifier(tableJunction) + "." + this.quoteIdentifier(identTarget) joinQueryItem += self.quoteIdentifier(tableSource) + "." + self.quoteIdentifier(attrSource) + " = "
joinQueryItem += self.quoteIdentifier(tableJunction) + "." + self.quoteIdentifier(identSource)
joinQueryItem += " LEFT OUTER JOIN " + self.quoteIdentifier(table) + " AS " + self.quoteIdentifier(as) + " ON "
joinQueryItem += self.quoteIdentifier(tableTarget) + "." + self.quoteIdentifier(attrTarget) + " = "
joinQueryItem += self.quoteIdentifier(tableJunction) + "." + self.quoteIdentifier(identTarget)
} else {
var primaryKeysLeft = ((include.association.associationType === 'BelongsTo') ? Object.keys(include.association.target.primaryKeys) : Object.keys(include.association.source.primaryKeys))
, tableLeft = ((include.association.associationType === 'BelongsTo') ? as : parentTable)
, attrLeft = ((primaryKeysLeft.length !== 1) ? 'id' : primaryKeysLeft[0])
, tableRight = ((include.association.associationType === 'BelongsTo') ? parentTable : as)
, attrRight = include.association.identifier
joinQueryItem += " LEFT OUTER JOIN " + self.quoteIdentifier(table) + " AS " + self.quoteIdentifier(as) + " ON " + self.quoteIdentifier(tableLeft) + "." + self.quoteIdentifier(attrLeft) + " = " + self.quoteIdentifier(tableRight) + "." + self.quoteIdentifier(attrRight)
} }
if (include.include) {
include.include.forEach(function(childInclude) {
joinQueryItem += generateJoinQuery(childInclude, as)
}.bind(this))
}
return joinQueryItem
}
options.include.forEach(function(include) {
joinQuery += generateJoinQuery(include, tableName)
}.bind(this)) }.bind(this))
options.attributes = optAttributes.join(', ') options.attributes = optAttributes.join(', ')
......
...@@ -191,7 +191,7 @@ module.exports = (function() { ...@@ -191,7 +191,7 @@ module.exports = (function() {
} }
var isSelectQuery = function() { var isSelectQuery = function() {
return this.options.type === 'SELECT'; return this.options.type === 'SELECT'
} }
var isUpdateQuery = function() { var isUpdateQuery = function() {
...@@ -219,16 +219,14 @@ module.exports = (function() { ...@@ -219,16 +219,14 @@ module.exports = (function() {
// Queries with include // Queries with include
} else if (this.options.hasJoin === true) { } else if (this.options.hasJoin === true) {
this.options.includeNames = this.options.include.map(function (include) { results = groupJoinData(results, this.options)
return include.as
})
results = groupJoinData.call(this, results)
result = results.map(function(result) { result = results.map(function(result) {
return this.callee.build(result, { return this.callee.build(result, {
isNewRecord: false, isNewRecord: false,
isDirty: false, isDirty: false,
include:this.options.include, include:this.options.include,
includeNames: this.options.includeNames, includeNames: this.options.includeNames,
includeMap: this.options.includeMap,
includeValidated: true includeValidated: true
}) })
}.bind(this)) }.bind(this))
...@@ -318,18 +316,21 @@ module.exports = (function() { ...@@ -318,18 +316,21 @@ module.exports = (function() {
] ]
*/ */
var groupJoinData = function(data) { var groupJoinData = function(data, options) {
var self = this var results = []
, results = []
, existingResult , existingResult
, calleeData , calleeData
, child
data.forEach(function (row) { data.forEach(function (row) {
row = Dot.transform(row) row = Dot.transform(row)
calleeData = _.omit(row, self.options.includeNames) calleeData = _.omit(row, options.includeNames)
existingResult = _.find(results, function (result) { existingResult = _.find(results, function (result) {
return Utils._.isEqual(_.omit(result, self.options.includeNames), calleeData) if (options.includeNames) {
return Utils._.isEqual(_.omit(result, options.includeNames.concat(['__children'])), calleeData)
}
return Utils._.isEqual(result, calleeData)
}) })
if (!existingResult) { if (!existingResult) {
...@@ -337,17 +338,24 @@ module.exports = (function() { ...@@ -337,17 +338,24 @@ module.exports = (function() {
} }
for (var attrName in row) { for (var attrName in row) {
if (row.hasOwnProperty(attrName) && Object(row[attrName]) === row[attrName] && self.options.includeNames.indexOf(attrName) !== -1) { if (row.hasOwnProperty(attrName)) {
existingResult[attrName] = existingResult[attrName] || [] child = Object(row[attrName]) === row[attrName] && options.includeMap && options.includeMap[attrName]
var attrRowExists = existingResult[attrName].some(function(attrRow) { if (child) {
return Utils._.isEqual(attrRow, row[attrName]) if (!existingResult.__children) existingResult.__children = {}
}) if (!existingResult.__children[attrName]) existingResult.__children[attrName] = []
if (!attrRowExists) {
existingResult[attrName].push(row[attrName]) existingResult.__children[attrName].push(row[attrName])
} }
} }
} }
}) })
results.forEach(function (result) {
_.each(result.__children, function (children, key) {
result[key] = groupJoinData(children, options.includeMap[key])
})
delete result.__children
})
return results return results
} }
......
...@@ -37,6 +37,7 @@ module.exports = (function() { ...@@ -37,6 +37,7 @@ module.exports = (function() {
query.on('error', function(err) { query.on('error', function(err) {
receivedError = true receivedError = true
err.sql = sql err.sql = sql
self.emit('sql', sql)
self.emit('error', err, self.callee) self.emit('error', err, self.callee)
}) })
...@@ -125,6 +126,7 @@ module.exports = (function() { ...@@ -125,6 +126,7 @@ module.exports = (function() {
}) })
}) })
} }
this.emit('success', this.send('handleSelectQuery', rows)) this.emit('success', this.send('handleSelectQuery', rows))
} }
} else if (this.send('isShowOrDescribeQuery')) { } else if (this.send('isShowOrDescribeQuery')) {
...@@ -141,7 +143,6 @@ module.exports = (function() { ...@@ -141,7 +143,6 @@ module.exports = (function() {
} }
} }
} }
this.emit('success', this.callee) this.emit('success', this.callee)
} else if (this.send('isUpdateQuery')) { } else if (this.send('isUpdateQuery')) {
if(this.callee !== null) { // may happen for bulk updates if(this.callee !== null) { // may happen for bulk updates
......
...@@ -659,7 +659,7 @@ module.exports = (function() { ...@@ -659,7 +659,7 @@ module.exports = (function() {
} }
var sql = this.QueryGenerator.selectQuery(tableName, options, factory) var sql = this.QueryGenerator.selectQuery(tableName, options, factory)
queryOptions = Utils._.extend({}, queryOptions, { include: options.include }) queryOptions = Utils._.extend({}, queryOptions, { include: options.include, includeNames: options.includeNames, includeMap: options.includeMap })
return queryAndEmit.call(this, [sql, factory, queryOptions], 'select') return queryAndEmit.call(this, [sql, factory, queryOptions], 'select')
} }
......
...@@ -367,14 +367,6 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -367,14 +367,6 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
done() done()
}) })
it('throws an error about missing attributes if include contains an object with daoFactory', function(done) {
var self = this
expect(function() {
self.Worker.find({ include: [ { daoFactory: self.Worker } ] })
}).to.throw(Error, 'Include malformed. Expected attributes: daoFactory, as!')
done()
})
it('throws an error if included DaoFactory is not associated', function(done) { it('throws an error if included DaoFactory is not associated', function(done) {
var self = this var self = this
expect(function() { expect(function() {
......
...@@ -482,14 +482,6 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -482,14 +482,6 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
done() done()
}) })
it('throws an error about missing attributes if include contains an object with daoFactory', function(done) {
var self = this
expect(function() {
self.Worker.all({ include: [ { daoFactory: self.Worker } ] })
}).to.throw(Error, 'Include malformed. Expected attributes: daoFactory, as!')
done()
})
it('throws an error if included DaoFactory is not associated', function(done) { it('throws an error if included DaoFactory is not associated', function(done) {
var self = this var self = this
expect(function() { expect(function() {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!