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

Commit f3c7939e by Daniel Durante

Merge pull request #765 from durango/association-queries

Added smartWhere object, also treats where clauses as first class citizens for hasMany associations. Closes #757
2 parents d6705a3b 7a45d6db
......@@ -21,12 +21,26 @@ module.exports = (function() {
where[self.__factory.connectorDAO.tableName+"."+foreignKey] = {join: self.__factory.target.tableName+".id"}
if (options.where) {
Utils._.each(options.where, function(value, index) {
delete options.where[index];
options.where[self.__factory.target.tableName+"."+index] = value;
});
Utils._.extend(options.where, where)
if (Array.isArray(options.where)) {
smart = Utils.smartWhere([where, options.where], self.__factory.target.daoFactoryManager.sequelize.options.dialect)
smart = Utils.compileSmartWhere.call(self.__factory.target, smart, self.__factory.target.daoFactoryManager.sequelize.options.dialect)
if (smart.length > 0) {
options.where = smart
}
} else {
Utils._.each(options.where, function(value, index) {
delete options.where[index];
smart = Utils.smartWhere(value, self.__factory.target.daoFactoryManager.sequelize.options.dialect)
smart = Utils.compileSmartWhere.call(self.__factory.target, smart)
if (smart.length > 0) {
value = smart
}
options.where[self.__factory.target.tableName+"."+index] = value;
});
options.where = Utils._.extend(options.where, where)
}
} else {
options.where = where;
}
......@@ -51,10 +65,10 @@ module.exports = (function() {
return obj.id === old.id
})
})
, unassociatedObjects = newAssociations.filter(function (obj) {
, unassociatedObjects = newAssociations.filter(function (obj) {
return !Utils._.find(oldAssociations, function (old) {
return obj.id === old.id
})
})
})
if (obsoleteAssociations.length > 0) {
......@@ -81,7 +95,7 @@ module.exports = (function() {
})
chainer.add(self.__factory.connectorDAO.bulkCreate(bulk))
}
}
chainer
.run()
......
......@@ -12,42 +12,51 @@ module.exports = (function() {
where[this.__factory.identifier] = this.instance.id
options.where = options.where ? Utils._.extend(options.where, where) : where
return this.__factory.target.findAll(options)
if (options.where) {
smart = Utils.smartWhere([where, options.where], this.__factory.target.daoFactoryManager.sequelize.options.dialect)
smart = Utils.compileSmartWhere.call(this.__factory.target, smart)
if (smart.length > 0) {
options.where = smart
}
} else {
options.where = where
}
return this.__factory.target.all(options)
}
HasManySingleLinked.prototype.injectSetter = function(emitter, oldAssociations, newAssociations) {
var self = this
, chainer = new Utils.QueryChainer()
, obsoleteAssociations = oldAssociations.filter(function (old) {
, obsoleteAssociations = oldAssociations.filter(function (old) {
return !Utils._.find(newAssociations, function (obj) {
return obj.id === old.id
})
})
})
, unassociatedObjects = newAssociations.filter(function (obj) {
, unassociatedObjects = newAssociations.filter(function (obj) {
return !Utils._.find(oldAssociations, function (old) {
return obj.id === old.id
})
})
})
, update
if (obsoleteAssociations.length > 0) {
// clear the old associations
var obsoleteIds = obsoleteAssociations.map(function(associatedObject) {
var obsoleteIds = obsoleteAssociations.map(function(associatedObject) {
associatedObject[self.__factory.identifier] = null
return associatedObject.id
})
update = {}
update = {}
update[self.__factory.identifier] = null
chainer.add(this.__factory.target.update(update, { id: obsoleteIds }))
}
}
if (unassociatedObjects.length > 0) {
// set the new associations
var unassociatedIds = unassociatedObjects.map(function(associatedObject) {
var unassociatedIds = unassociatedObjects.map(function(associatedObject) {
associatedObject[self.__factory.identifier] = self.instance.id
return associatedObject.id
return associatedObject.id
})
update = {}
......
......@@ -56,6 +56,12 @@ module.exports = (function() {
params = {where: where}
}
smart = Utils.smartWhere([where, params.where || []], self.target.daoFactoryManager.sequelize.options.dialect)
smart = Utils.compileSmartWhere.call(self.target, smart)
if (smart.length > 0) {
params.where = smart
}
return self.target.find(params)
}
......
......@@ -289,7 +289,7 @@ module.exports = (function() {
Escape a value (e.g. a string, number or date)
*/
escape: function(value) {
throwMethodUndefined('quoteIdentifier')
throwMethodUndefined('escape')
}
}
......
var util = require("util")
, DataTypes = require("./data-types")
, SqlString = require("./sql-string")
, lodash = require("lodash")
, _string = require('underscore.string')
var Utils = module.exports = {
_: (function() {
var _ = require("lodash")
, _s = require('underscore.string')
var _ = lodash
, _s = _string
_.mixin(_s.exports())
_.mixin({
......@@ -39,6 +41,211 @@ var Utils = module.exports = {
var timeZone = null;
return SqlString.format(arr.shift(), arr, timeZone, dialect)
},
// smartWhere can accept an array of {where} objects, or a single {where} object.
// The smartWhere function breaks down the collection of where objects into a more
// centralized object for each column so we can avoid duplicates
// e.g. WHERE username='dan' AND username='dan' becomes WHERE username='dan'
// All of the INs, NOT INs, BETWEENS, etc. are compressed into one key for each column
// This function will hopefully provide more functionality to sequelize in the future.
// tl;dr It's a nice way to dissect a collection of where objects and compress them into one object
smartWhere: function(whereArg, dialect) {
var self = this
, _where = {}
, logic
, type
(Array.isArray(whereArg) ? whereArg : [whereArg]).forEach(function(where) {
// If it's an array we're already good... / it's in a format that can't be broken down further
// e.g. Util.format['SELECT * FROM world WHERE status=?', 'hello']
if (Array.isArray(where)) {
_where._ = where._ || {queries: [], bindings: []}
_where._.queries[_where._.queries.length] = where[0]
if (where.length > 1) {
var values = where.splice(1)
if (dialect === "sqlite") {
values.forEach(function(v, i) {
if (typeof v === "boolean") {
values[i] = (v === true ? 1 : 0)
}
})
}
_where._.bindings = _where._.bindings.concat(values)
}
}
else if (typeof where === "object") {
// First iteration is trying to compress IN and NOT IN as much as possible...
// .. reason being is that WHERE username IN (?) AND username IN (?) != WHERE username IN (?,?)
Object.keys(where).forEach(function(i) {
if (Array.isArray(where[i])) {
where[i] = {
in: where[i]
}
}
})
// Build our smart object
Object.keys(where).forEach(function(i) {
type = typeof where[i]
_where[i] = _where[i] || {}
if (Array.isArray(where[i])) {
_where[i].in = _where[i].in || []
_where[i].in.concat(where[i]);
}
else if (type === "object") {
Object.keys(where[i]).forEach(function(ii) {
logic = self.getWhereLogic(ii)
switch(logic) {
case 'IN':
_where[i].in = _where[i].in || []
_where[i].in = _where[i].in.concat(where[i][ii]);
break
case 'NOT':
_where[i].not = _where[i].not || []
_where[i].not = _where[i].not.concat(where[i][ii]);
break
case 'BETWEEN':
_where[i].between = _where[i].between || []
_where[i].between[_where[i].between.length] = [where[i][ii][0], where[i][ii][1]]
break
case 'NOT BETWEEN':
_where[i].nbetween = _where[i].nbetween || []
_where[i].nbetween[_where[i].nbetween.length] = [where[i][ii][0], where[i][ii][1]]
break
case 'JOIN':
_where[i].joined = _where[i].joined || []
_where[i].joined[_where[i].joined.length] = where[i][ii]
break
default:
_where[i].lazy = _where[i].lazy || {conditions: [], bindings: []}
_where[i].lazy.conditions[_where[i].lazy.conditions.length] = logic + ' ?'
_where[i].lazy.bindings = _where[i].lazy.bindings.concat(where[i][ii])
}
})
}
else if (type === "string" || type === "number" || type === "boolean") {
_where[i].lazy = _where[i].lazy || {conditions: [], bindings: []}
if (type === "boolean") {
_where[i].lazy.conditions[_where[i].lazy.conditions.length] = '= ' + SqlString.escape(where[i], false, null, dialect) // sqlite is special
} else {
_where[i].lazy.conditions[_where[i].lazy.conditions.length] = '= ?'
_where[i].lazy.bindings = _where[i].lazy.bindings.concat(where[i])
}
}
})
}
})
return _where
},
// Converts {smart where} object(s) into an array that's friendly for Utils.format()
// NOTE: Must be applied/called from the QueryInterface
compileSmartWhere: function(obj, dialect) {
var self = this
, whereArgs = []
, text = []
, columnName
if (typeof obj !== "object") {
return obj
}
for (var column in obj) {
if (column === "_") {
text[text.length] = obj[column].queries.join(' AND ')
if (obj[column].bindings.length > 0) {
whereArgs = whereArgs.concat(obj[column].bindings)
}
} else {
Object.keys(obj[column]).forEach(function(condition) {
columnName = self.QueryInterface.quoteIdentifiers(column)
switch(condition) {
case 'in':
text[text.length] = columnName + ' IN (' + obj[column][condition].map(function(){ return '?' }) + ')'
whereArgs = whereArgs.concat(obj[column][condition])
break
case 'not':
text[text.length] = columnName + ' NOT IN (' + obj[column][condition].map(function(){ return '?' }) + ')'
whereArgs = whereArgs.concat(obj[column][condition])
break
case 'between':
Object.keys(obj[column][condition]).forEach(function(row) {
text[text.length] = columnName + ' BETWEEN ? AND ?'
whereArgs = whereArgs.concat(obj[column][condition][row][0], obj[column][condition][row][1])
})
break
case 'nbetween':
Object.keys(obj[column][condition]).forEach(function(row) {
text[text.length] = columnName + ' BETWEEN ? AND ?'
whereArgs = whereArgs.concat(obj[column][condition][row][0], obj[column][condition][row][1])
})
break
case 'joined':
Object.keys(obj[column][condition]).forEach(function(row) {
text[text.length] = columnName + ' = ' + self.QueryInterface.quoteIdentifiers(obj[column][condition][row])
})
break
default: // lazy
text = text.concat(obj[column].lazy.conditions.map(function(val){ return columnName + ' ' + val }))
obj[column].lazy.bindings = obj[column].lazy.bindings.map(function(v) { return SqlString.escape(v, false, null, dialect) })
whereArgs = whereArgs.concat(obj[column].lazy.bindings)
}
})
}
}
return lodash.compact([text.join(' AND ')].concat(whereArgs))
},
getWhereLogic: function(logic) {
switch (logic) {
case 'join':
return 'JOIN'
case 'gte':
return '>='
break
case 'gt':
return '>'
break
case 'lte':
return '<='
break
case 'lt':
return '<'
break
case 'eq':
case 'join':
return '='
break
case 'ne':
return '!='
break
case 'between':
case '..':
return 'BETWEEN'
break
case 'nbetween':
case 'notbetween':
case '!..':
return 'NOT BETWEEN'
break
case 'in':
return 'IN'
break
case 'not':
return 'NOT IN'
break
case 'like':
return 'LIKE'
break
case 'nlike':
case 'notlike':
return 'NOT LIKE'
break
default:
return ''
}
},
isHash: function(obj) {
return Utils._.isObject(obj) && !Array.isArray(obj);
},
......
......@@ -4,6 +4,7 @@ if (typeof require === 'function') {
, Sequelize = require('../../index')
, dialect = Helpers.getTestDialect()
, _ = require('lodash')
, moment = require('moment')
}
buster.spec.expose()
......@@ -24,8 +25,12 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
describe('(1:N)', function() {
describe('hasSingle', function() {
before(function(done) {
this.Article = this.sequelize.define('Article', { 'title': Sequelize.STRING })
this.Label = this.sequelize.define('Label', { 'text': Sequelize.STRING })
this.Article = this.sequelize.define('Article', {
'title': Sequelize.STRING
})
this.Label = this.sequelize.define('Label', {
'text': Sequelize.STRING
})
this.Article.hasMany(this.Label)
......@@ -67,7 +72,8 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
[ article, 'hasLabel', [ label2 ]]
])
chainer.runSerially().success(function(_, label1, hasLabel1, hasLabel2) {
chainer.runSerially()
.success(function(_, label1, hasLabel1, hasLabel2) {
expect(hasLabel1).toBeTrue()
expect(hasLabel2).toBeFalse()
done()
......@@ -78,8 +84,12 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
describe('hasAll', function() {
before(function(done) {
this.Article = this.sequelize.define('Article', { 'title': Sequelize.STRING })
this.Label = this.sequelize.define('Label', { 'text': Sequelize.STRING })
this.Article = this.sequelize.define('Article', {
'title': Sequelize.STRING
})
this.Label = this.sequelize.define('Label', {
'text': Sequelize.STRING
})
this.Article.hasMany(this.Label)
......@@ -199,6 +209,38 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
})
})
it('should treat the where object of associations as a first class citizen', function(done) {
var self = this
this.Article = this.sequelize.define('Article', {
'title': Sequelize.STRING
})
this.Label = this.sequelize.define('Label', {
'text': Sequelize.STRING,
'until': Sequelize.DATE
})
this.Article.hasMany(this.Label)
this.sequelize.sync({ force: true }).success(function() {
var chainer = new Sequelize.Utils.QueryChainer([
self.Article.create({ title: 'Article' }),
self.Label.create({ text: 'Awesomeness', until: '2014-01-01 01:00:00' }),
self.Label.create({ text: 'Epicness', until: '2014-01-03 01:00:00' })
])
chainer.run().success(function(results, article, label1, label2) {
article.setLabels([label1, label2]).success(function() {
article.getLabels({where: ['until > ?', moment('2014-01-02').toDate()]}).success(function(labels) {
expect(labels).toBeArray()
expect(labels.length).toEqual(1)
expect(labels[0].text).toEqual('Epicness')
done()
})
})
})
})
})
it("gets all associated objects when no options are passed", function(done) {
this.User.find({where: {username: 'John'}}).success(function (john) {
john.getTasks().success(function (tasks) {
......@@ -309,6 +351,15 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
})
})
})
it("only gets objects that fulfill options with a formatted value", function(done) {
this.User.find({where: {username: 'John'}}).success(function (john) {
john.getTasks({where: ['active = ?', true]}).success(function (tasks) {
expect(tasks.length).toEqual(1)
done()
})
})
})
})
it("removes the reference id, which was added in the first place", function() {
......
......@@ -19,6 +19,27 @@ describe(Helpers.getTestDialectTeaser("HasOne"), function() {
})
})
describe('getAssocation', function() {
it('should be able to handle a where object that\'s a first class citizen.', function(done) {
var User = this.sequelize.define('UserXYZ', { username: Sequelize.STRING })
, Task = this.sequelize.define('TaskXYZ', { title: Sequelize.STRING, status: Sequelize.STRING })
User.hasOne(Task)
this.sequelize.sync({ force: true }).success(function() {
User.create({ username: 'foo' }).success(function(user) {
Task.create({ title: 'task', status: 'inactive' }).success(function(task) {
user.setTaskXYZ(task).success(function() {
user.getTaskXYZ({where: ['status = ?', 'active']}).success(function(task) {
expect(task).toEqual(null)
done()
})
})
})
})
})
})
})
describe('setAssociation', function() {
it('clears the association if null is passed', function(done) {
var User = this.sequelize.define('UserXYZ', { username: Sequelize.STRING })
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!