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

Commit 8e621213 by Sascha Depold

Merge branch 'scopes' of https://github.com/durango/sequelize into durango-scopes

Conflicts:
	lib/dialects/mysql/query-generator.js
	lib/query-interface.js
	lib/utils.js
	spec/dao-factory.spec.js
2 parents 042249b6 36546551
......@@ -19,7 +19,9 @@ module.exports = (function() {
whereCollection: null,
schema: null,
schemaDelimiter: '',
language: 'en'
language: 'en',
defaultScope: null,
scopes: null
}, options || {})
// error check options
......@@ -46,6 +48,7 @@ module.exports = (function() {
this.rawAttributes = attributes
this.daoFactoryManager = null // defined in init function
this.associations = {}
this.scopeObj = {}
}
Object.defineProperty(DAOFactory.prototype, 'attributes', {
......@@ -106,6 +109,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)
......@@ -222,6 +229,87 @@ module.exports = (function() {
return this.QueryGenerator.addSchema(this)
}
DAOFactory.prototype.scope = function(option) {
var self = Object.create(this)
, type
, options
, merge
, i
, scope
, scopeName
, argLength = arguments.length
, lastArg = arguments[argLength-1]
// Set defaults
scopeOptions = (typeof lastArg === "object" && !Array.isArray(lastArg) ? lastArg : {}) || {} // <-- for no arguments
scopeOptions.silent = (scopeOptions !== null && scopeOptions.hasOwnProperty('silent') ? scopeOptions.silent : true)
// 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 (i = 0; i < argLength; i++) {
options = Array.isArray(arguments[i]) ? arguments[i] : [arguments[i]]
options.forEach(function(o){
type = typeof o
scope = null
merge = false
scopeName = null
if (type === "object") {
// Right now we only support a merge functionality for objects
if (!!o.merge) {
merge = true
scopeName = o.merge[0]
if (Array.isArray(o.merge) && !!self.options.scopes[scopeName]) {
scope = self.options.scopes[scopeName].apply(self, o.merge.splice(1))
}
else if (typeof o.merge === "string") {
scopeName = o.merge
scope = self.options.scopes[scopeName]
}
}
if (!!o.method) {
if (Array.isArray(o.method) && !!self.options.scopes[o.method[0]]) {
scopeName = o.method[0]
scope = self.options.scopes[scopeName].apply(self, o.method.splice(1))
merge = !!o.merge
}
else if (!!self.options.scopes[o.method]) {
scopeName = o.method
scope = self.options.scopes[scopeName].apply(self)
}
} else {
scopeName = o
scope = self.options.scopes[scopeName]
}
} else {
scopeName = o
scope = self.options.scopes[scopeName]
}
if (!!scope) {
Utils.injectScope.call(self, scope, merge)
}
else if (scopeOptions.silent !== true && !!scopeName) {
throw new Error("Invalid scope " + scopeName + " called.")
}
})
}
return self
}
// alias for findAll
DAOFactory.prototype.all = function(options, queryOptions) {
return this.findAll(options, queryOptions)
......
......@@ -467,8 +467,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])
......@@ -488,33 +493,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 = {}
......
......@@ -572,8 +572,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])
......@@ -593,33 +598,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 = {}
......
......@@ -405,6 +405,22 @@ 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, factory)
queryOptions = Utils._.extend({}, queryOptions, { include: options.include })
return queryAndEmit.call(this, [sql, factory, queryOptions], 'select')
......@@ -502,6 +518,17 @@ module.exports = (function() {
return this.QueryGenerator.escape(value)
}
// private
var buildScope = function() {
var smart
// Use smartWhere to convert several {where} objects into a single where object
smart = Utils.smartWhere(this.scopeObj.where || [], this.daoFactoryManager.sequelize.options.dialect)
smart = Utils.compileSmartWhere.call(this, smart, this.daoFactoryManager.sequelize.options.dialect)
return {limit: this.scopeObj.limit || null, offset: this.scopeObj.offset || null, where: smart, order: (this.scopeObj.order || []).join(', ')}
}
var queryAndEmit = QueryInterface.prototype.queryAndEmit = function(sqlOrQueryParams, methodName, options, emitter) {
options = Utils._.extend({
success: function(){},
......
......@@ -61,6 +61,68 @@ var Utils = module.exports = {
var timeZone = null;
return SqlString.formatNamedParameters(sql, parameters, 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") {
Object.keys(scope.where).forEach(function(){
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
......@@ -225,45 +287,32 @@ var Utils = module.exports = {
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 ''
}
......
......@@ -995,14 +995,75 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
this.User.bulkCreate([
{username: 'boo', intVal: 5, theDate: '2013-01-01 12:00'},
{username: 'boo2', intVal: 10, theDate: '2013-01-10 12:00'}
]).success(function(user2){
done()
})
]).success(function(user2) {
done()
})
})
it('should be able to find a row using like', function(done) {
this.User.findAll({
where: {
username: {
like: '%2'
}
}
}).success(function(users) {
expect(users).to.be.an.instanceof(Array)
expect(users).to.have.length(1)
expect(users[0].username).to.equal('boo2')
expect(users[0].intVal).to.equal(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).to.be.an.instanceof(Array)
expect(users).to.have.length(1)
expect(users[0].username).to.equal('boo')
expect(users[0].intVal).to.equal(5)
done()
})
})
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).to.equal('boo2')
expect(users[0].intVal).to.equal(10)
done()
})
})
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).to.equal('boo')
expect(users[0].intVal).to.equal(5)
done()
})
})
it('should be able to handle false/true values just fine...', function(done) {
var User = this.User
, escapeChar = (dialect === "postgres" || dialect === "postgres-native") ? '"' : '`'
User.bulkCreate([
{username: 'boo5', aBool: false},
{username: 'boo6', aBool: true}
......@@ -1010,6 +1071,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
User.all({where: [escapeChar + 'aBool' + escapeChar + ' = ?', false]}).success(function(users) {
expect(users).to.have.length(1)
expect(users[0].username).to.equal('boo5')
User.all({where: [escapeChar + 'aBool' + escapeChar + ' = ?', true]}).success(function(_users) {
expect(_users).to.have.length(1)
expect(_users[0].username).to.equal('boo6')
......@@ -2555,6 +2617,290 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
describe('scopes', function() {
beforeEach(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\'']
},
fakeEmail: {
where: ['email LIKE \'%@fakeemail.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
}
}
},
escape: {
where: {
username: "escape'd"
}
}
}
})
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 have no problems with escaping SQL", function(done) {
var self = this
this.ScopeMe.create({username: 'escape\'d', email: 'fake@fakemail.com'}).success(function(){
self.ScopeMe.scope('escape').all().success(function(users){
expect(users).to.be.an.instanceof(Array)
expect(users.length).to.equal(1)
expect(users[0].username).to.equal('escape\'d');
done()
})
})
})
it("should be able to use a defaultScope if declared", function(done) {
this.ScopeMe.all().success(function(users) {
expect(users).to.be.an.instanceof(Array)
expect(users.length).to.equal(2)
expect([10,5].indexOf(users[0].access_level) !== -1).to.be.true
expect([10,5].indexOf(users[1].access_level) !== -1).to.be.true
expect(['dan', 'tobi'].indexOf(users[0].username) !== -1).to.be.true
expect(['dan', 'tobi'].indexOf(users[1].username) !== -1).to.be.true
done()
})
})
it("should be able to amend the default scope with a find object", function(done) {
this.ScopeMe.findAll({where: {username: 'dan'}}).success(function(users) {
expect(users).to.be.an.instanceof(Array)
expect(users.length).to.equal(1)
expect(users[0].username).to.equal('dan')
done()
})
})
it("should be able to override the default scope", function(done) {
this.ScopeMe.scope('fakeEmail').findAll().success(function(users) {
expect(users).to.be.an.instanceof(Array)
expect(users.length).to.equal(1)
expect(users[0].username).to.equal('tobi')
done()
})
})
it("should be able to combine two scopes", function(done) {
this.ScopeMe.scope(['sequelizeTeam', 'highValue']).findAll().success(function(users) {
expect(users).to.be.an.instanceof(Array)
expect(users.length).to.equal(1)
expect(users[0].username).to.equal('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).to.be.an.instanceof(Array)
expect(users.length).to.equal(1)
expect(users[0].username).to.equal('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).to.be.an.instanceof(Array)
expect(users.length).to.equal(1)
expect(users[0].username).to.equal('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).to.be.an.instanceof(Array)
expect(users.length).to.equal(2)
expect(['dan', 'tony'].indexOf(users[0].username) !== -1).to.be.true
expect(['dan', 'tony'].indexOf(users[1].username) !== -1).to.be.true
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).to.be.an.instanceof(Array)
expect(users.length).to.equal(1)
expect(users[0].username).to.equal('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).to.be.an.instanceof(Array)
expect(users.length).to.equal(2)
expect(users[0].username).to.equal('dan')
expect(users[1].username).to.equal('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).to.be.an.instanceof(Array)
expect(users.length).to.equal(2)
expect(users[0].username).to.equal('tobi')
expect(users[1].username).to.equal('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).to.be.an.instanceof(Array)
expect(users.length).to.equal(1)
expect(users[0].username).to.equal('dan')
expect(users[0].access_level).to.equal(5)
expect(users[0].other_value).to.equal(10)
done()
})
})
it("should be able to remove all scopes", function(done) {
this.ScopeMe.scope(null).findAll().success(function(users) {
expect(users).to.be.an.instanceof(Array)
expect(users.length).to.equal(3)
done()
})
})
it("should have no problem performing findOrCreate", function(done) {
this.ScopeMe.findOrCreate({username: 'fake'}).success(function(user) {
expect(user.username).to.equal('fake')
done()
})
})
it("should be able to hold multiple scope objects", function(done) {
var sequelizeTeam = this.ScopeMe.scope('sequelizeTeam', 'orderScope')
, tobi = this.ScopeMe.scope({method: ['actualValue', 11]})
sequelizeTeam.all().success(function(team) {
tobi.all().success(function(t) {
expect(team).to.be.an.instanceof(Array)
expect(team.length).to.equal(2)
expect(team[0].username).to.equal('dan')
expect(team[1].username).to.equal('tony')
expect(t).to.be.an.instanceof(Array)
expect(t.length).to.equal(1)
expect(t[0].username).to.equal('tobi')
done()
})
})
})
it("should gracefully omit any scopes that don't exist", function(done) {
this.ScopeMe.scope('sequelizeTeam', 'orderScope', 'doesntexist').all().success(function(team) {
expect(team).to.be.an.instanceof(Array)
expect(team.length).to.equal(2)
expect(team[0].username).to.equal('dan')
expect(team[1].username).to.equal('tony')
done()
})
})
it("should gracefully omit any scopes that don't exist through an array", function(done) {
this.ScopeMe.scope(['sequelizeTeam', 'orderScope', 'doesntexist']).all().success(function(team) {
expect(team).to.be.an.instanceof(Array)
expect(team.length).to.equal(2)
expect(team[0].username).to.equal('dan')
expect(team[1].username).to.equal('tony')
done()
})
})
it("should gracefully omit any scopes that don't exist through an object", function(done) {
this.ScopeMe.scope('sequelizeTeam', 'orderScope', {method: 'doesntexist'}).all().success(function(team) {
expect(team).to.be.an.instanceof(Array)
expect(team.length).to.equal(2)
expect(team[0].username).to.equal('dan')
expect(team[1].username).to.equal('tony')
done()
})
})
it("should emit an error for scopes that don't exist with silent: false", function(done) {
try {
this.ScopeMe.scope('doesntexist', {silent: false})
} catch (err) {
expect(err.message).to.equal('Invalid scope doesntexist called.')
done()
}
})
})
describe('schematic support', function() {
beforeEach(function(done){
var self = this;
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!