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

Commit 2a5336af by Daniel Durante

Several things in this commit: 1. New scope() functionality, works very similar …

…to ActiveRecord's scope. 2. Big internal changes (but as of right now only related to scope()) made for the {where} object when using finders. Could be useful for some, it basically breaks down several where objects into one concise object. 3. New where finders.. 'eq', 'like', 'nlike', 'notlike', and 'not' (not in). 4. New finder shortcuts, '..' for between and '..' for not between
1 parent 366685e5
......@@ -5,8 +5,6 @@ var Utils = require("./utils")
module.exports = (function() {
var DAOFactory = function(name, attributes, options) {
var self = this
this.options = Utils._.extend({
timestamps: true,
instanceMethods: {},
......@@ -19,7 +17,9 @@ module.exports = (function() {
whereCollection: null,
schema: null,
schemaDelimiter: '',
language: 'en'
language: 'en',
defaultScope: null,
scopes: null
}, options || {})
// error check options
......@@ -39,6 +39,7 @@ module.exports = (function() {
this.rawAttributes = attributes
this.daoFactoryManager = null // defined in init function
this.associations = {}
this.scopeObj = {}
}
Object.defineProperty(DAOFactory.prototype, 'attributes', {
......@@ -70,6 +71,10 @@ module.exports = (function() {
this.primaryKeyCount = Object.keys(this.primaryKeys).length;
this.options.hasPrimaryKeys = this.hasPrimaryKeys = this.primaryKeyCount > 0;
if (typeof this.options.defaultScope === "object") {
Utils.injectScope.call(this, this.options.defaultScope)
}
addDefaultAttributes.call(this)
addOptionalClassMethods.call(this)
findAutoIncrementField.call(this)
......@@ -183,6 +188,62 @@ module.exports = (function() {
return this.QueryGenerator.addSchema(this)
}
DAOFactory.prototype.scope = function(option) {
var self = this
, type
, options
, merge
, argLength = arguments.length
// Clear out any predefined scopes...
self.scopeObj = {}
// Possible formats for option:
// String of arguments: 'hello', 'world', 'etc'
// Array: ['hello', 'world', 'etc']
// Object: {merge: 'hello'}, {method: ['scopeName' [, args1, args2..]]}, {merge: true, method: ...}
if (argLength < 1 || !option) {
return self
}
for (var i = 0; i < argLength; i++) {
options = Array.isArray(arguments[i]) ? arguments[i] : [arguments[i]]
options.forEach(function(o){
type = typeof o
if (type === "object") {
// Right now we only support a merge functionality for objects
if (!!o.merge) {
if (Array.isArray(o.merge)) {
merge = self.options.scopes[o.merge[0]].apply(self, o.merge.splice(1))
Utils.injectScope.call(self, merge, true)
}
else if (typeof o.merge === "string") {
Utils.injectScope.call(self, self.options.scopes[o.merge], true)
}
}
if (!!o.method) {
if (Array.isArray(o.method) && !!self.options.scopes[o.method[0]]) {
merge = self.options.scopes[o.method[0]].apply(self, o.method.splice(1))
Utils.injectScope.call(self, merge, (!!o.merge))
} else {
Utils.injectScope.call(self, self.options.scopes[o.method].apply(self))
}
} else {
Utils.injectScope.call(self, self.options.scopes[o])
}
}
else if (!!self.options.scopes[o]) {
Utils.injectScope.call(self, self.options.scopes[o])
}
})
}
return self
}
// alias for findAll
DAOFactory.prototype.all = function(options, queryOptions) {
return this.findAll(options, queryOptions)
......
......@@ -220,7 +220,7 @@ module.exports = (function() {
if (options.offset && !options.limit) {
/*
* If no limit is defined, our best bet is to use the max number of rows in a table. From the MySQL docs:
* There is a limit of 2^32 (~4.295E+09) rows in a MyISAM table. If you build MySQL with the --with-big-tables option,
* There is a limit of 2^32 (~4.295E+09) rows in a MyISAM table. If you build MySQL with the --with-big-tables option,
* the row limitation is increased to (2^32)^2 (1.844E+19) rows.
*/
query += " LIMIT " + options.offset + ", " + 18440000000000000000;
......@@ -443,8 +443,13 @@ module.exports = (function() {
result.push([_key, _value].join("="))
} else {
for (var logic in value) {
var logicResult = this.getWhereLogic(logic)
if (logicResult === "BETWEEN" || logicResult === "NOT BETWEEN") {
var logicResult = Utils.getWhereLogic(logic)
if (logic === "IN" || logic === "NOT IN") {
var values = Array.isArray(where[i][ii]) ? where[i][ii] : [where[i][ii]]
_where[_where.length] = i + ' ' + logic + ' (' + values.map(function(){ return '?' }).join(',') + ')'
_whereArgs = _whereArgs.concat(values)
}
else if (logicResult === "BETWEEN" || logicResult === "NOT BETWEEN") {
_value = this.escape(value[logic][0])
var _value2 = this.escape(value[logic][1])
......@@ -464,33 +469,6 @@ module.exports = (function() {
return result.join(" AND ")
},
getWhereLogic: function(logic) {
switch (logic) {
case 'gte':
return '>='
break
case 'gt':
return '>'
break
case 'lte':
return '<='
break
case 'lt':
return '<'
break
case 'ne':
return '!='
break
case 'between':
return 'BETWEEN'
break
case 'nbetween':
case 'notbetween':
return 'NOT BETWEEN'
break
}
},
attributesToSQL: function(attributes) {
var result = {}
......
......@@ -527,8 +527,13 @@ module.exports = (function() {
result.push([_key, _value].join("="))
} else {
for (var logic in value) {
var logicResult = this.getWhereLogic(logic)
if (logicResult === "BETWEEN" || logicResult === "NOT BETWEEN") {
var logicResult = Utils.getWhereLogic(logic)
if (logic === "IN" || logic === "NOT IN") {
var values = Array.isArray(where[i][ii]) ? where[i][ii] : [where[i][ii]]
_where[_where.length] = i + ' ' + logic + ' (' + values.map(function(){ return '?' }).join(',') + ')'
_whereArgs = _whereArgs.concat(values)
}
else if (logicResult === "BETWEEN" || logicResult === "NOT BETWEEN") {
_value = this.escape(value[logic][0])
var _value2 = this.escape(value[logic][1])
......@@ -548,33 +553,6 @@ module.exports = (function() {
return result.join(' AND ')
},
getWhereLogic: function(logic) {
switch (logic) {
case 'gte':
return '>='
break
case 'gt':
return '>'
break
case 'lte':
return '<='
break
case 'lt':
return '<'
break
case 'ne':
return '!='
break
case 'between':
return 'BETWEEN'
break
case 'nbetween':
case 'notbetween':
return 'NOT BETWEEN'
break
}
},
attributesToSQL: function(attributes) {
var result = {}
......
......@@ -288,6 +288,23 @@ module.exports = (function() {
QueryInterface.prototype.select = function(factory, tableName, options, queryOptions) {
options = options || {}
// See if we need to merge options and factory.scopeObj
// we're doing this on the QueryInterface level because it's a bridge between
// sequelize and the databases
if (Object.keys(factory.scopeObj).length > 0) {
if (!!options) {
Utils.injectScope.call(factory, options, true)
}
var scopeObj = buildScope.call(factory)
Object.keys(scopeObj).forEach(function(method) {
if (typeof scopeObj[method] === "number" || !Utils._.isEmpty(scopeObj[method])) {
options[method] = scopeObj[method]
}
})
}
var sql = this.QueryGenerator.selectQuery(tableName, options)
queryOptions = Utils._.extend({}, queryOptions, { include: options.include })
return queryAndEmit.call(this, [sql, factory, queryOptions], 'select')
......@@ -387,6 +404,19 @@ module.exports = (function() {
// private
var buildScope = function() {
var where = []
, format
, smart
, arr = this.scopeObj.where || []
// Use smartWhere to convert several {where} objects into a single where object
smart = Utils.smartWhere(this.scopeObj.where || [])
smart = Utils.compileSmartWhere.call(this, smart)
return {limit: this.scopeObj.limit || null, offset: this.scopeObj.offset || null, where: smart, order: (this.scopeObj.order || []).join(', ')}
}
var queryAndEmit = function(sqlOrQueryParams, methodName, options, emitter) {
options = Utils._.extend({
success: function(){},
......
var util = require("util")
, DataTypes = require("./data-types")
, SqlString = require("./sql-string")
, lodash = require("lodash")
var Utils = module.exports = {
_: (function() {
var _ = require("lodash")
var _ = lodash
, _s = require('underscore.string')
_.mixin(_s.exports())
......@@ -39,6 +40,248 @@ var Utils = module.exports = {
var timeZone = null;
return SqlString.format(arr.shift(), arr, timeZone, dialect)
},
injectScope: function(scope, merge) {
var self = this
scope = scope || {}
self.scopeObj = self.scopeObj || {}
if (Array.isArray(scope.where)) {
self.scopeObj.where = self.scopeObj.where || []
self.scopeObj.where.push(scope.where)
return true
}
if (typeof scope.order === "string") {
self.scopeObj.order = self.scopeObj.order || []
self.scopeObj.order[self.scopeObj.order.length] = scope.order
}
// Limit and offset are *always* merged.
if (!!scope.limit) {
self.scopeObj.limit = scope.limit
}
if (!!scope.offset) {
self.scopeObj.offset = scope.offset
}
// Where objects are a mixed variable. Possible values are arrays, strings, and objects
if (!!scope.where) {
// Begin building our scopeObj
self.scopeObj.where = self.scopeObj.where || []
// Reset if we're merging!
if (merge === true && !!scope.where && !!self.scopeObj.where) {
var scopeKeys = Object.keys(scope.where)
self.scopeObj.where = self.scopeObj.where.map(function(scopeObj) {
if (!Array.isArray(scopeObj) && typeof scopeObj === "object") {
return lodash.omit.apply(undefined, [scopeObj].concat(scopeKeys))
} else {
return scopeObj
}
}).filter(function(scopeObj) {
return !lodash.isEmpty(scopeObj)
})
self.scopeObj.where = self.scopeObj.where.concat(scope.where)
}
if (Array.isArray(scope.where)) {
self.scopeObj.where.push(scope.where)
}
else if (typeof scope.where === "object") {
for (var i in scope.where) {
self.scopeObj.where.push(scope.where)
}
} else { // Assume the value is a string
self.scopeObj.where.push([scope.where])
}
}
if (!!self.scopeObj.where) {
self.scopeObj.where = lodash.uniq(self.scopeObj.where)
}
},
// 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) {
var self = this
, _where = {}
, logic
, type
whereArg = Array.isArray(whereArg) ? 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) {
_where._.bindings = _where._.bindings.concat(where.splice(1))
}
}
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 (?,?)
for (var i in where) {
if (Array.isArray(where[i])) {
where[i] = {
in: where[i]
}
}
}
// Build our smart object
for (var i in where) {
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") {
for (var ii in where[i]) {
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;
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") {
_where[i].lazy = _where[i].lazy || {conditions: [], bindings: []}
_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) {
var 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 {
for (var condition in obj[column]) {
columnName = this.QueryInterface.quoteIdentifiers(column)
switch(condition) {
case 'in':
text[text.length] = column + ' IN (' + obj[column][condition].map(function(){ return '?'; }) + ')'
whereArgs = whereArgs.concat(obj[column][condition])
break
case 'not':
text[text.length] = column + ' NOT IN (' + obj[column][condition].map(function(){ return '?'; }) + ')'
whereArgs = whereArgs.concat(obj[column][condition])
break
case 'between':
for (var row in obj[column][condition]) {
text[text.length] = column + ' BETWEEN ? AND ?'
whereArgs = whereArgs.concat(obj[column][condition][row][0], obj[column][condition][row][1])
}
break
case 'nbetween':
for (var row in obj[column][condition]) {
text[text.length] = column + ' BETWEEN ? AND ?'
whereArgs = whereArgs.concat(obj[column][condition][row][0], obj[column][condition][row][1])
}
break
default: // lazy
text = text.concat(obj[column].lazy.conditions.map(function(val){ return column + ' ' + val }))
whereArgs = whereArgs.concat(obj[column].lazy.bindings)
}
}
}
}
return lodash.compact([text.join(' AND ')].concat(whereArgs))
},
getWhereLogic: function(logic) {
switch (logic) {
case 'gte':
return '>='
break
case 'gt':
return '>'
break
case 'lte':
return '<='
break
case 'lt':
return '<'
break
case 'eq':
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);
},
......
/* jshint camelcase: false */
if(typeof require === 'function') {
const buster = require("buster")
, Sequelize = require("../index")
......@@ -866,6 +868,38 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
})
})
it('should be able to find a row using like', function(done) {
this.User.findAll({
where: {
username: {
like: '%2'
}
}
}).success(function(users) {
expect(users).toBeArray()
expect(users.length).toEqual(1)
expect(users[0].username).toEqual('boo2')
expect(users[0].intVal).toEqual(10)
done()
})
})
it('should be able to find a row using not like', function(done) {
this.User.findAll({
where: {
username: {
nlike: '%2'
}
}
}).success(function(users) {
expect(users).toBeArray()
expect(users.length).toEqual(1)
expect(users[0].username).toEqual('boo')
expect(users[0].intVal).toEqual(5)
done()
})
})
it('should be able to find a row between a certain date', function(done) {
this.User.findAll({
where: {
......@@ -880,6 +914,20 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
})
})
it('should be able to find a row between a certain date using the between shortcut', function(done) {
this.User.findAll({
where: {
theDate: {
'..': ['2013-01-02', '2013-01-11']
}
}
}).success(function(users) {
expect(users[0].username).toEqual('boo2')
expect(users[0].intVal).toEqual(10)
done()
})
})
it('should be able to find a row between a certain date and an additional where clause', function(done) {
this.User.findAll({
where: {
......@@ -909,6 +957,20 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
})
})
it('should be able to find a row not between a certain integer using the not between shortcut', function(done) {
this.User.findAll({
where: {
intVal: {
'!..': [8, 10]
}
}
}).success(function(users) {
expect(users[0].username).toEqual('boo')
expect(users[0].intVal).toEqual(5)
done()
})
})
it('should be able to find a row using not between and between logic', function(done) {
this.User.findAll({
where: {
......@@ -1921,9 +1983,203 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
})
}) //- describe: max
describe('scopes', function() {
before(function(done) {
this.ScopeMe = this.sequelize.define('ScopeMe', {
username: Sequelize.STRING,
email: Sequelize.STRING,
access_level: Sequelize.INTEGER,
other_value: Sequelize.INTEGER
}, {
defaultScope: {
where: {
access_level: {
gte: 5
}
}
},
scopes: {
orderScope: {
order: 'access_level DESC'
},
limitScope: {
limit: 2
},
sequelizeTeam: {
where: ['email LIKE \'%@sequelizejs.com\'']
},
highValue: {
where: {
other_value: {
gte: 10
}
}
},
isTony: {
where: {
username: 'tony'
}
},
canBeTony: {
where: {
username: ['tony']
}
},
canBeDan: {
where: {
username: {
in: 'dan'
}
}
},
actualValue: function(value) {
return {
where: {
other_value: value
}
}
},
complexFunction: function(email, accessLevel) {
return {
where: ['email like ? AND access_level >= ?', email + '%', accessLevel]
}
},
lowAccess: {
where: {
access_level: {
lte: 5
}
}
}
}
})
this.sequelize.sync({force: true}).success(function() {
var records = [
{username: 'dan', email: 'dan@sequelizejs.com', access_level: 5, other_value: 10},
{username: 'tobi', email: 'tobi@fakeemail.com', access_level: 10, other_value: 11},
{username: 'tony', email: 'tony@sequelizejs.com', access_level: 3, other_value: 7}
];
this.ScopeMe.bulkCreate(records).success(function() {
done()
})
}.bind(this))
})
it("should be able to use a defaultScope if declared", function(done) {
this.ScopeMe.findAll().success(function(users) {
expect(users).toBeArray()
expect(users.length).toEqual(2)
expect(['dan', 'tobi'].indexOf(users[0].username) !== -1).toBeTrue()
expect(['dan', 'tobi'].indexOf(users[1].username) !== -1).toBeTrue()
done()
})
})
it("should be able to override the default scope", function(done) {
this.ScopeMe.scope('highValue').findAll().success(function(users) {
expect(users).toBeArray()
expect(users.length).toEqual(2)
expect(['dan', 'tobi'].indexOf(users[0].username) !== -1).toBeTrue()
expect(['dan', 'tobi'].indexOf(users[1].username) !== -1).toBeTrue()
done()
})
})
it("should be able to combine two scopes", function(done) {
this.ScopeMe.scope(['sequelizeTeam', 'highValue']).findAll().success(function(users) {
expect(users).toBeArray()
expect(users.length).toEqual(1)
expect(users[0].username).toEqual('dan')
done()
})
})
it("should be able to call a scope that's a function", function(done) {
this.ScopeMe.scope({method: ['actualValue', 11]}).findAll().success(function(users) {
expect(users).toBeArray()
expect(users.length).toEqual(1)
expect(users[0].username).toEqual('tobi')
done()
})
})
it("should be able to handle multiple function scopes", function(done) {
this.ScopeMe.scope([{method: ['actualValue', 10]}, {method: ['complexFunction', 'dan', '5']}]).findAll().success(function(users) {
expect(users).toBeArray()
expect(users.length).toEqual(1)
expect(users[0].username).toEqual('dan')
done()
})
})
it("should be able to stack the same field in the where clause", function(done) {
this.ScopeMe.scope(['canBeDan', 'canBeTony']).findAll().success(function(users) {
expect(users).toBeArray()
expect(users.length).toEqual(2)
expect(['dan', 'tony'].indexOf(users[0].username) !== -1).toBeTrue()
expect(['dan', 'tony'].indexOf(users[1].username) !== -1).toBeTrue()
done()
})
})
it("should be able to merge scopes", function(done) {
this.ScopeMe.scope(['highValue', 'isTony', {merge: true, method: ['actualValue', 7]}]).findAll().success(function(users) {
expect(users).toBeArray()
expect(users.length).toEqual(1)
expect(users[0].username).toEqual('tony')
done()
})
})
it("should give us the correct order if we declare an order in our scope", function(done) {
this.ScopeMe.scope('sequelizeTeam', 'orderScope').findAll().success(function(users) {
expect(users).toBeArray()
expect(users.length).toEqual(2)
expect(users[0].username).toEqual('dan')
expect(users[1].username).toEqual('tony')
done()
})
})
it("should give us the correct order as well as a limit if we declare such in our scope", function(done) {
this.ScopeMe.scope(['orderScope', 'limitScope']).findAll().success(function(users) {
expect(users).toBeArray()
expect(users.length).toEqual(2)
expect(users[0].username).toEqual('tobi')
expect(users[1].username).toEqual('dan')
done()
})
})
it("should have no problems combining scopes and traditional where object", function(done) {
this.ScopeMe.scope('sequelizeTeam').findAll({where: {other_value: 10}}).success(function(users) {
expect(users).toBeArray()
expect(users.length).toEqual(1)
expect(users[0].username).toEqual('dan')
done()
})
})
it("should be able to remove all scopes", function(done) {
this.ScopeMe.scope(null).findAll().success(function(users) {
expect(users).toBeArray()
expect(users.length).toEqual(3)
done()
})
})
it("should have no problem performing findOrCreate", function(done) {
this.ScopeMe.findOrCreate({username: 'fake'}).success(function(user) {
expect(user.username).toEqual('fake')
done()
})
})
})
describe('schematic support', function() {
before(function(done){
var self = this;
var self = this
this.UserPublic = this.sequelize.define('UserPublic', {
age: Sequelize.INTEGER
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!