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

Commit e15b14d2 by Daniel Durante

Merge branch 'master' into milestones/2.0.0

Conflicts:
	lib/dao-validator.js
	lib/dao.js
2 parents ab9a8364 44ac75c4
...@@ -28,6 +28,8 @@ ...@@ -28,6 +28,8 @@
- [BUG] Fixed SQL escaping with sqlite and unified escaping [#700](https://github.com/sequelize/sequelize/pull/700). thanks to PiPeep - [BUG] Fixed SQL escaping with sqlite and unified escaping [#700](https://github.com/sequelize/sequelize/pull/700). thanks to PiPeep
- [BUG] Fixed Postgres' pools [ff57af63](https://github.com/sequelize/sequelize/commit/ff57af63c2eb395b4828a5984a22984acdc2a5e1) - [BUG] Fixed Postgres' pools [ff57af63](https://github.com/sequelize/sequelize/commit/ff57af63c2eb395b4828a5984a22984acdc2a5e1)
- [BUG] Fixed BLOB/TEXT columns having a default value declared in MySQL [#793](https://github.com/sequelize/sequelize/pull/793). thanks to durango - [BUG] Fixed BLOB/TEXT columns having a default value declared in MySQL [#793](https://github.com/sequelize/sequelize/pull/793). thanks to durango
- [BUG] You can now use .find() on any single integer primary key when throwing just a number as an argument [#796](https://github.com/sequelize/sequelize/pull/796). thanks to durango
- [BUG] Adding unique to a column for Postgres in the migrator should be fixed [#795](https://github.com/sequelize/sequelize/pull/795). thanks to durango
- [FEATURE] Validate a model before it gets saved. [#601](https://github.com/sequelize/sequelize/pull/601). thanks to durango - [FEATURE] Validate a model before it gets saved. [#601](https://github.com/sequelize/sequelize/pull/601). thanks to durango
- [FEATURE] Schematics. [#564](https://github.com/sequelize/sequelize/pull/564). thanks to durango - [FEATURE] Schematics. [#564](https://github.com/sequelize/sequelize/pull/564). thanks to durango
- [FEATURE] Foreign key constraints. [#595](https://github.com/sequelize/sequelize/pull/595). thanks to optilude - [FEATURE] Foreign key constraints. [#595](https://github.com/sequelize/sequelize/pull/595). thanks to optilude
...@@ -50,6 +52,8 @@ ...@@ -50,6 +52,8 @@
- [FEATURE] Added support for model instances being referenced [#761](https://github.com/sequelize/sequelize/pull/761) thanks to sdepold - [FEATURE] Added support for model instances being referenced [#761](https://github.com/sequelize/sequelize/pull/761) thanks to sdepold
- [FEATURE] Added support for specifying the path to load a module for a dialect. [#766](https://github.com/sequelize/sequelize/pull/766) thanks to sonnym. - [FEATURE] Added support for specifying the path to load a module for a dialect. [#766](https://github.com/sequelize/sequelize/pull/766) thanks to sonnym.
- [FEATURE] Drop index if exists has been added to sqlite [#766](https://github.com/sequelize/sequelize/pull/776) thanks to coderbuzz - [FEATURE] Drop index if exists has been added to sqlite [#766](https://github.com/sequelize/sequelize/pull/776) thanks to coderbuzz
- [FEATURE] bulkCreate() now has a third argument which gives you the ability to validate each row before attempting to bulkInsert [#797](https://github.com/sequelize/sequelize/pull/797). thanks to durango
- [FEATURE] Added `isDirty` to model instances. [#798](https://github.com/sequelize/sequelize/pull/798). Thanks to mstorgaard
- [REFACTORING] hasMany now uses a single SQL statement when creating and destroying associations, instead of removing each association seperately [690](https://github.com/sequelize/sequelize/pull/690). Inspired by [#104](https://github.com/sequelize/sequelize/issues/104). janmeier - [REFACTORING] hasMany now uses a single SQL statement when creating and destroying associations, instead of removing each association seperately [690](https://github.com/sequelize/sequelize/pull/690). Inspired by [#104](https://github.com/sequelize/sequelize/issues/104). janmeier
- [REFACTORING] Consistent handling of offset across dialects. Offset is now always applied, and limit is set to max table size of not limit is given [#725](https://github.com/sequelize/sequelize/pull/725). janmeier - [REFACTORING] Consistent handling of offset across dialects. Offset is now always applied, and limit is set to max table size of not limit is given [#725](https://github.com/sequelize/sequelize/pull/725). janmeier
- [REFACTORING] Moved Jasmine to Buster and then Buster to Mocha + Chai. sdepold and durango - [REFACTORING] Moved Jasmine to Buster and then Buster to Mocha + Chai. sdepold and durango
......
...@@ -250,14 +250,20 @@ module.exports = (function() { ...@@ -250,14 +250,20 @@ module.exports = (function() {
} }
var primaryKeys = this.primaryKeys var primaryKeys = this.primaryKeys
, keys = Object.keys(primaryKeys)
, keysLength = keys.length
// options is not a hash but an id // options is not a hash but an id
if (typeof options === 'number') { if (typeof options === 'number') {
options = { where: options } var oldOption = options
options = { where: {} }
if (keysLength === 1) {
options.where[keys[0]] = oldOption
} else {
options.where.id = oldOption
}
} else if (Utils._.size(primaryKeys) && Utils.argsArePrimaryKeys(arguments, primaryKeys)) { } else if (Utils._.size(primaryKeys) && Utils.argsArePrimaryKeys(arguments, primaryKeys)) {
var where = {} var where = {}
, self = this
, keys = Object.keys(primaryKeys)
Utils._.each(arguments, function(arg, i) { Utils._.each(arguments, function(arg, i) {
var key = keys[i] var key = keys[i]
...@@ -288,7 +294,6 @@ module.exports = (function() { ...@@ -288,7 +294,6 @@ module.exports = (function() {
this.options.whereCollection = options.where || null this.options.whereCollection = options.where || null
} else if (typeof options === "string") { } else if (typeof options === "string") {
var where = {} var where = {}
, keys = Object.keys(primaryKeys)
if (this.primaryKeyCount === 1) { if (this.primaryKeyCount === 1) {
where[primaryKeys[keys[0]]] = options; where[primaryKeys[keys[0]]] = options;
...@@ -377,7 +382,7 @@ module.exports = (function() { ...@@ -377,7 +382,7 @@ module.exports = (function() {
} }
DAOFactory.prototype.build = function(values, options) { DAOFactory.prototype.build = function(values, options) {
options = options || { isNewRecord: true } options = options || { isNewRecord: true, isDirty: true }
var self = this var self = this
, instance = new this.DAO(values, this.options, options.isNewRecord) , instance = new this.DAO(values, this.options, options.isNewRecord)
...@@ -385,6 +390,7 @@ module.exports = (function() { ...@@ -385,6 +390,7 @@ module.exports = (function() {
instance.isNewRecord = options.isNewRecord instance.isNewRecord = options.isNewRecord
instance.daoFactoryName = this.name instance.daoFactoryName = this.name
instance.daoFactory = this instance.daoFactory = this
instance.isDirty = options.isDirty
return instance return instance
} }
...@@ -434,50 +440,80 @@ module.exports = (function() { ...@@ -434,50 +440,80 @@ module.exports = (function() {
* generated IDs and other default values in a way that can be mapped to * generated IDs and other default values in a way that can be mapped to
* multiple records * multiple records
*/ */
DAOFactory.prototype.bulkCreate = function(records, fields) { DAOFactory.prototype.bulkCreate = function(records, fields, options) {
options = options || {}
options.validate = options.validate || false
fields = fields || []
var self = this var self = this
, daos = records.map(function(v) { return self.build(v) })
, updatedAtAttr = self.options.underscored ? 'updated_at' : 'updatedAt' , updatedAtAttr = self.options.underscored ? 'updated_at' : 'updatedAt'
, createdAtAttr = self.options.underscored ? 'created_at' : 'createdAt' , createdAtAttr = self.options.underscored ? 'created_at' : 'createdAt'
, errors = []
, completed = 0
fields = fields || [] return new Utils.CustomEventEmitter(function(emitter) {
var daos = records.map(function(v) {
return self.build(v)
})
// we will re-create from DAOs, which may have set up default attributes var bulkCreateStep = function(i) {
records = [] daos[i].validate({skip: fields}).success(function(err) {
var found = false if (!!err) {
errors[errors.length] = {record: records[i], errors: err}
}
++completed
if (completed === records.length) {
bulkCreateFinal()
} else {
bulkCreateStep(completed)
}
})
}
daos.forEach(function(dao) { bulkCreateStep(0)
var values = fields.length > 0 ? {} : dao.dataValues
fields.forEach(function(field) { var bulkCreateFinal = function() {
values[field] = dao.dataValues[field] if (options.validate === true && errors.length > 0) {
}) return emitter.emit('error', errors)
}
if (self.options.timestamps) { // we will re-create from DAOs, which may have set up default attributes
values[createdAtAttr] = Utils.now() records = []
values[updatedAtAttr] = Utils.now()
}
records.push(values); daos.forEach(function(dao) {
}) var values = fields.length > 0 ? {} : dao.dataValues
fields.forEach(function(field) {
values[field] = dao.dataValues[field]
})
// Validate enums if (self.options.timestamps) {
records.forEach(function(values) { values[createdAtAttr] = Utils.now()
for (var attrName in self.rawAttributes) { values[updatedAtAttr] = Utils.now()
if (self.rawAttributes.hasOwnProperty(attrName)) {
var definition = self.rawAttributes[attrName]
, isEnum = (definition.type && (definition.type.toString() === DataTypes.ENUM.toString()))
, hasValue = (typeof values[attrName] !== 'undefined')
, valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1)
if (isEnum && hasValue && valueOutOfScope) {
throw new Error('Value "' + values[attrName] + '" for ENUM ' + attrName + ' is out of allowed scope. Allowed values: ' + definition.values.join(', '))
} }
}
}
})
return self.QueryInterface.bulkInsert(self.tableName, records) records.push(values)
})
// Validate enums
records.forEach(function(values) {
for (var attrName in self.rawAttributes) {
if (self.rawAttributes.hasOwnProperty(attrName)) {
var definition = self.rawAttributes[attrName]
, isEnum = (definition.type && (definition.type.toString() === DataTypes.ENUM.toString()))
, hasValue = (typeof values[attrName] !== 'undefined')
, valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1)
if (isEnum && hasValue && valueOutOfScope) {
throw new Error('Value "' + values[attrName] + '" for ENUM ' + attrName + ' is out of allowed scope. Allowed values: ' + definition.values.join(', '))
}
}
}
})
self.QueryInterface.bulkInsert(self.tableName, records).proxy(emitter)
}
}).run()
} }
/** /**
......
var Validator = require("validator") var Validator = require("validator")
, Utils = require("./utils") , Utils = require("./utils")
var DaoValidator = module.exports = function(model) { var DaoValidator = module.exports = function(model, options) {
options = options || {}
options.skip = options.skip || []
this.model = model this.model = model
this.chainer = new Utils.QueryChainer() this.chainer = new Utils.QueryChainer()
this.options = options
} }
DaoValidator.prototype.validate = function() { DaoValidator.prototype.validate = function() {
var self = this
return new Utils.CustomEventEmitter(function(emitter) { return new Utils.CustomEventEmitter(function(emitter) {
validateAttributes.call(this) validateAttributes.call(self)
validateModel.call(this) validateModel.call(self)
this self
.chainer .chainer
.run() .run()
.success(function () { .success(function () {
...@@ -26,7 +32,7 @@ DaoValidator.prototype.validate = function() { ...@@ -26,7 +32,7 @@ DaoValidator.prototype.validate = function() {
emitter.emit('success', errors) emitter.emit('success', errors)
}) })
}.bind(this)).run() }).run()
} }
// private // private
...@@ -52,17 +58,19 @@ var validateModel = function() { ...@@ -52,17 +58,19 @@ var validateModel = function() {
} }
var validateAttributes = function() { var validateAttributes = function() {
var errors = {} var self = this
, errors = {}
// for each field and value // for each field and value
Utils._.each(this.model.dataValues, function(value, field) { Utils._.each(this.model.dataValues, function(value, field) {
var rawAttribute = this.model.rawAttributes[field] var rawAttribute = self.model.rawAttributes[field]
, hasAllowedNull = ((rawAttribute === undefined || rawAttribute.allowNull === true) && ((value === null) || (value === undefined))) , hasAllowedNull = ((rawAttribute === undefined || rawAttribute.allowNull === true) && ((value === null) || (value === undefined)))
, isSkipped = self.options.skip.length > 0 && self.options.skip.indexOf(field) === -1
if (this.model.validators.hasOwnProperty(field) && !hasAllowedNull) { if (self.model.validators.hasOwnProperty(field) && !hasAllowedNull && !isSkipped) {
errors = Utils._.merge(errors, validateAttribute.call(this, value, field)) errors = Utils._.merge(errors, validateAttribute.call(self, value, field))
} }
}.bind(this)) // for each field })
return errors return errors
} }
...@@ -113,7 +121,7 @@ var prepareValidationOfAttribute = function(value, details, validatorType, optio ...@@ -113,7 +121,7 @@ var prepareValidationOfAttribute = function(value, details, validatorType, optio
} }
// extract the error msg // extract the error msg
errorMessage = details.hasOwnProperty("msg") ? details.msg : false errorMessage = details.hasOwnProperty("msg") ? details.msg : undefined
// check method exists // check method exists
var validator = Validator.check(value, errorMessage) var validator = Validator.check(value, errorMessage)
......
...@@ -84,10 +84,15 @@ module.exports = (function() { ...@@ -84,10 +84,15 @@ module.exports = (function() {
DAO.prototype.getDataValue = function(name) { DAO.prototype.getDataValue = function(name) {
return this.dataValues && this.dataValues.hasOwnProperty(name) ? this.dataValues[name] : this[name] return this.dataValues && this.dataValues.hasOwnProperty(name) ? this.dataValues[name] : this[name]
} }
DAO.prototype.get = DAO.prototype.getDataValue
DAO.prototype.setDataValue = function(name, value) { DAO.prototype.setDataValue = function(name, value) {
if (Utils.hasChanged(this.dataValues[name], value)) {
this.isDirty = true
}
this.dataValues[name] = value this.dataValues[name] = value
} }
DAO.prototype.set = DAO.prototype.setDataValue
// if an array with field names is passed to save() // if an array with field names is passed to save()
// only those fields will be updated // only those fields will be updated
...@@ -103,7 +108,7 @@ module.exports = (function() { ...@@ -103,7 +108,7 @@ module.exports = (function() {
fields.push(updatedAtAttr) fields.push(updatedAtAttr)
} }
if (fields.indexOf(createdAtAttr) === -1) { if (fields.indexOf(createdAtAttr) === -1 && this.isNewRecord === true) {
fields.push(createdAtAttr) fields.push(createdAtAttr)
} }
} }
...@@ -120,10 +125,24 @@ module.exports = (function() { ...@@ -120,10 +125,24 @@ module.exports = (function() {
for (var attrName in this.daoFactory.rawAttributes) { for (var attrName in this.daoFactory.rawAttributes) {
if (this.daoFactory.rawAttributes.hasOwnProperty(attrName)) { if (this.daoFactory.rawAttributes.hasOwnProperty(attrName)) {
var definition = this.daoFactory.rawAttributes[attrName] var definition = this.daoFactory.rawAttributes[attrName]
, isEnum = (definition.type && (definition.type.toString() === DataTypes.ENUM.toString())) , isEnum = definition.type && (definition.type.toString() === DataTypes.ENUM.toString())
, isHstore = (!!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type) , isHstore = !!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type
, hasValue = (typeof values[attrName] !== 'undefined') , hasValue = values[attrName] !== undefined
, valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1) , isMySQL = this.daoFactory.daoFactoryManager.sequelize.options.dialect === "mysql"
, ciCollation = !!this.daoFactory.options.collate && this.daoFactory.options.collate.match(/_ci$/i)
, valueOutOfScope
if (isEnum && isMySQL && ciCollation && hasValue) {
var scopeIndex = (definition.values || []).map(function(d) { return d.toLowerCase() }).indexOf(values[attrName].toLowerCase())
valueOutOfScope = scopeIndex === -1
// We'll return what the actual case will be, since a simple SELECT query would do the same...
if (!valueOutOfScope) {
values[attrName] = definition.values[scopeIndex]
}
} else {
valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1)
}
if (isEnum && hasValue && valueOutOfScope) { if (isEnum && hasValue && valueOutOfScope) {
throw new Error('Value "' + values[attrName] + '" for ENUM ' + attrName + ' is out of allowed scope. Allowed values: ' + definition.values.join(', ')) throw new Error('Value "' + values[attrName] + '" for ENUM ' + attrName + ' is out of allowed scope. Allowed values: ' + definition.values.join(', '))
...@@ -146,6 +165,7 @@ module.exports = (function() { ...@@ -146,6 +165,7 @@ module.exports = (function() {
if (!!errors) { if (!!errors) {
emitter.emit('error', errors) emitter.emit('error', errors)
} else if (this.isNewRecord) { } else if (this.isNewRecord) {
this.isDirty = false
this this
.QueryInterface .QueryInterface
.insert(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory), values) .insert(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory), values)
...@@ -157,6 +177,7 @@ module.exports = (function() { ...@@ -157,6 +177,7 @@ module.exports = (function() {
identifier = this.__options.whereCollection; identifier = this.__options.whereCollection;
} }
this.isDirty = false
var tableName = this.QueryInterface.QueryGenerator.addSchema(this.__factory) var tableName = this.QueryInterface.QueryGenerator.addSchema(this.__factory)
, query = this.QueryInterface.update(this, tableName, values, identifier) , query = this.QueryInterface.update(this, tableName, values, identifier)
...@@ -193,6 +214,7 @@ module.exports = (function() { ...@@ -193,6 +214,7 @@ module.exports = (function() {
this[valueName] = obj.values[valueName] this[valueName] = obj.values[valueName]
} }
} }
this.isDirty = false
emitter.emit('success', this) emitter.emit('success', this)
}.bind(this)) }.bind(this))
}.bind(this)).run() }.bind(this)).run()
...@@ -203,8 +225,8 @@ module.exports = (function() { ...@@ -203,8 +225,8 @@ module.exports = (function() {
* *
* @return null if and only if validation successful; otherwise an object containing { field name : [error msgs] } entries. * @return null if and only if validation successful; otherwise an object containing { field name : [error msgs] } entries.
*/ */
DAO.prototype.validate = function() { DAO.prototype.validate = function(options) {
return new DaoValidator(this).validate() return new DaoValidator(this, options).validate()
} }
DAO.prototype.updateAttributes = function(updates, fields) { DAO.prototype.updateAttributes = function(updates, fields) {
...@@ -216,11 +238,15 @@ module.exports = (function() { ...@@ -216,11 +238,15 @@ module.exports = (function() {
var self = this var self = this
var readOnlyAttributes = Object.keys(this.__factory.primaryKeys) var readOnlyAttributes = Object.keys(this.__factory.primaryKeys)
readOnlyAttributes.push('id') readOnlyAttributes.push('id')
readOnlyAttributes.push('createdAt')
readOnlyAttributes.push('updatedAt') if (this.isNewRecord !== true) {
readOnlyAttributes.push('deletedAt') readOnlyAttributes.push(this.daoFactory.options.underscored === true ? 'created_at' : 'createdAt')
}
// readOnlyAttributes.push(this.daoFactory.options.underscored === true ? 'updated_at' : 'updatedAt')
readOnlyAttributes.push(this.daoFactory.options.underscored === true ? 'deleted_at' : 'deletedAt')
var isDirty = this.isDirty
Utils._.each(updates, function(value, attr) { Utils._.each(updates, function(value, attr) {
var updateAllowed = ( var updateAllowed = (
...@@ -228,8 +254,23 @@ module.exports = (function() { ...@@ -228,8 +254,23 @@ module.exports = (function() {
(readOnlyAttributes.indexOf(Utils._.underscored(attr)) == -1) && (readOnlyAttributes.indexOf(Utils._.underscored(attr)) == -1) &&
(self.attributes.indexOf(attr) > -1) (self.attributes.indexOf(attr) > -1)
) )
updateAllowed && (self[attr] = value)
if (updateAllowed) {
if (Utils.hasChanged(self[attr], value)) {
isDirty = true
}
self[attr] = value
}
}) })
// since we're updating the record, we should be updating the updatedAt column..
if (this.daoFactory.options.timestamps === true) {
isDirty = true
self[this.daoFactory.options.underscored === true ? 'updated_at' : 'updatedAt'] = new Date()
}
this.isDirty = isDirty
} }
DAO.prototype.destroy = function() { DAO.prototype.destroy = function() {
...@@ -328,7 +369,19 @@ module.exports = (function() { ...@@ -328,7 +369,19 @@ module.exports = (function() {
// (the same is true for __defineSetter and 'prototype' getters) // (the same is true for __defineSetter and 'prototype' getters)
if (has !== true) { if (has !== true) {
this.__defineGetter__(attribute, has.get || function() { return this.dataValues[attribute]; }); this.__defineGetter__(attribute, has.get || function() { return this.dataValues[attribute]; });
this.__defineSetter__(attribute, has.set || function(v) { this.dataValues[attribute] = v; }); this.__defineSetter__(attribute, has.set || function(v) {
if (Utils.hasChanged(this.dataValues[attribute], v)) {
//Only dirty the object if the change is not due to id, touchedAt, createdAt or updatedAt being initiated
var updatedAtAttr = this.__options.underscored ? 'updated_at' : 'updatedAt'
, createdAtAttr = this.__options.underscored ? 'created_at' : 'createdAt'
, touchedAtAttr = this.__options.underscored ? 'touched_at' : 'touchedAt'
if (this.dataValues[attribute] || (attribute != 'id' && attribute != touchedAtAttr && attribute != createdAtAttr && attribute != updatedAtAttr)) {
this.isDirty = true
}
}
this.dataValues[attribute] = v
});
} }
this[attribute] = value; this[attribute] = value;
......
...@@ -262,7 +262,7 @@ module.exports = (function() { ...@@ -262,7 +262,7 @@ module.exports = (function() {
result = transformRowsWithEagerLoadingIntoDaos.call(this, results) result = transformRowsWithEagerLoadingIntoDaos.call(this, results)
} else { } else {
result = results.map(function(result) { result = results.map(function(result) {
return this.callee.build(result, { isNewRecord: false }) return this.callee.build(result, { isNewRecord: false, isDirty: false })
}.bind(this)) }.bind(this))
} }
...@@ -287,7 +287,7 @@ module.exports = (function() { ...@@ -287,7 +287,7 @@ module.exports = (function() {
var transformRowWithEagerLoadingIntoDao = function(result, dao) { var transformRowWithEagerLoadingIntoDao = function(result, dao) {
// let's build the actual dao instance first... // let's build the actual dao instance first...
dao = dao || this.callee.build(result[this.callee.tableName], { isNewRecord: false }) dao = dao || this.callee.build(result[this.callee.tableName], { isNewRecord: false, isDirty: false })
// ... and afterwards the prefetched associations // ... and afterwards the prefetched associations
for (var tableName in result) { for (var tableName in result) {
...@@ -323,7 +323,7 @@ module.exports = (function() { ...@@ -323,7 +323,7 @@ module.exports = (function() {
accessor = accessor.slice(0,1).toLowerCase() + accessor.slice(1) accessor = accessor.slice(0,1).toLowerCase() + accessor.slice(1)
associationData.forEach(function(data) { associationData.forEach(function(data) {
var daoInstance = associatedDaoFactory.build(data, { isNewRecord: false }) var daoInstance = associatedDaoFactory.build(data, { isNewRecord: false, isDirty: false })
, isEmpty = !Utils.firstValueOfHash(daoInstance.identifiers) , isEmpty = !Utils.firstValueOfHash(daoInstance.identifiers)
if (['BelongsTo', 'HasOne'].indexOf(association.associationType) > -1) { if (['BelongsTo', 'HasOne'].indexOf(association.associationType) > -1) {
......
...@@ -46,11 +46,15 @@ module.exports = (function() { ...@@ -46,11 +46,15 @@ module.exports = (function() {
ConnectorManager.prototype.query = function(sql, callee, options) { ConnectorManager.prototype.query = function(sql, callee, options) {
var self = this var self = this
// we really want pendingQueries to increment as fast as possible...
self.pendingQueries++ self.pendingQueries++
return new Utils.CustomEventEmitter(function(emitter) { return new Utils.CustomEventEmitter(function(emitter) {
self.connect() self.connect()
.on('error', function(err) { .on('error', function(err) {
// zero-out the previous increment
self.pendingQueries--
self.endQuery.call(self)
emitter.emit('error', err) emitter.emit('error', err)
}) })
.on('success', function(done) { .on('success', function(done) {
...@@ -58,14 +62,22 @@ module.exports = (function() { ...@@ -58,14 +62,22 @@ module.exports = (function() {
done = done || null done = done || null
query.run(sql, done) query.run(sql, done)
.success(function(results) { emitter.emit('success', results); self.endQuery.call(self) }) .success(function(results) {
.error(function(err) { emitter.emit('error', err); self.endQuery.call(self) }) self.pendingQueries--
emitter.emit('success', results)
self.endQuery.call(self)
})
.error(function(err) {
self.pendingQueries--
emitter.emit('error', err)
self.endQuery.call(self)
})
.on('sql', function(sql) { emitter.emit('sql', sql) }) .on('sql', function(sql) { emitter.emit('sql', sql) })
}) })
}).run().complete(function() { self.pendingQueries-- }) }).run()
} }
ConnectorManager.prototype.connect = function(callback) { ConnectorManager.prototype.connect = function() {
var self = this var self = this
var emitter = new (require('events').EventEmitter)() var emitter = new (require('events').EventEmitter)()
...@@ -119,9 +131,13 @@ module.exports = (function() { ...@@ -119,9 +131,13 @@ module.exports = (function() {
this.poolIdentifier = this.pg.pools.getOrCreate(this.sequelize.config) this.poolIdentifier = this.pg.pools.getOrCreate(this.sequelize.config)
this.poolIdentifier.connect(connectCallback) this.poolIdentifier.connect(connectCallback)
} else { } else {
//create one-off client if (this.client !== null) {
this.client = new this.pg.Client(uri) connectCallback(null, this.client)
this.client.connect(connectCallback) } else {
//create one-off client
this.client = new this.pg.Client(uri)
this.client.connect(connectCallback)
}
} }
return emitter return emitter
......
...@@ -189,6 +189,15 @@ module.exports = (function() { ...@@ -189,6 +189,15 @@ module.exports = (function() {
definition = definition.replace(/^ENUM\(.+\)/, this.quoteIdentifier("enum_" + tableName + "_" + attributeName)) definition = definition.replace(/^ENUM\(.+\)/, this.quoteIdentifier("enum_" + tableName + "_" + attributeName))
} }
if (definition.match(/UNIQUE;*$/)) {
definition = definition.replace(/UNIQUE;*$/, '')
attrSql += Utils._.template(query.replace('ALTER COLUMN', ''))({
tableName: this.quoteIdentifiers(tableName),
query: 'ADD CONSTRAINT ' + this.quoteIdentifier(attributeName + '_unique_idx') + ' UNIQUE (' + this.quoteIdentifier(attributeName) + ')'
})
}
attrSql += Utils._.template(query)({ attrSql += Utils._.template(query)({
tableName: this.quoteIdentifiers(tableName), tableName: this.quoteIdentifiers(tableName),
query: this.quoteIdentifier(attributeName) + ' TYPE ' + definition query: this.quoteIdentifier(attributeName) + ' TYPE ' + definition
......
...@@ -252,6 +252,23 @@ var Utils = module.exports = { ...@@ -252,6 +252,23 @@ var Utils = module.exports = {
isHash: function(obj) { isHash: function(obj) {
return Utils._.isObject(obj) && !Array.isArray(obj); return Utils._.isObject(obj) && !Array.isArray(obj);
}, },
hasChanged: function(attrValue, value) {
//If attribute value is Date, check value as a date
if (Utils._.isDate(attrValue) && !Utils._.isDate(value)) {
value = new Date(value)
}
if (Utils._.isDate(attrValue)) {
return attrValue.valueOf() !== value.valueOf()
}
//If both of them are empty, don't set as changed
if ((attrValue === undefined || attrValue === null || attrValue === '') && (value === undefined || value === null || value === '')) {
return false
}
return attrValue !== value
},
argsArePrimaryKeys: function(args, primaryKeys) { argsArePrimaryKeys: function(args, primaryKeys) {
var result = (args.length == Object.keys(primaryKeys).length) var result = (args.length == Object.keys(primaryKeys).length)
if (result) { if (result) {
......
module.exports = {
up: function(migration, DataTypes, done) {
migration.addColumn('User', 'uniqueName', { type: DataTypes.STRING }).complete(function() {
migration.changeColumn('User', 'uniqueName', { type: DataTypes.STRING, allowNull: false, unique: true }).complete(done)
})
},
down: function(migration, DataTypes, done) {
migration.removeColumn('User', 'uniqueName').complete(done)
}
}
...@@ -715,6 +715,68 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -715,6 +715,68 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
}) })
}) })
it('emits an error when validate is set to true', function(done) {
var Tasks = this.sequelize.define('Task', {
name: {
type: Sequelize.STRING,
validate: {
notNull: { args: true, msg: 'name cannot be null' }
}
},
code: {
type: Sequelize.STRING,
validate: {
len: [3, 10]
}
}
})
Tasks.sync({ force: true }).success(function() {
Tasks.bulkCreate([
{name: 'foo', code: '123'},
{code: '1234'},
{name: 'bar', code: '1'}
], null, {validate: true}).error(function(errors) {
expect(errors).to.not.be.null
expect(errors).to.be.instanceof(Array)
expect(errors).to.have.length(2)
expect(errors[0].record.code).to.equal('1234')
expect(errors[0].errors.name[0]).to.equal('name cannot be null')
expect(errors[1].record.name).to.equal('bar')
expect(errors[1].record.code).to.equal('1')
expect(errors[1].errors.code[0]).to.match(/String is not in range/)
done()
})
})
})
it("doesn't emit an error when validate is set to true but our selectedValues are fine", function(done) {
var Tasks = this.sequelize.define('Task', {
name: {
type: Sequelize.STRING,
validate: {
notNull: { args: true, msg: 'name cannot be null' }
}
},
code: {
type: Sequelize.STRING,
validate: {
len: [3, 10]
}
}
})
Tasks.sync({ force: true }).success(function() {
Tasks.bulkCreate([
{name: 'foo', code: '123'},
{code: '1234'}
], ['code'], {validate: true}).success(function() {
// we passed!
done()
})
})
})
describe('enums', function() { describe('enums', function() {
it('correctly restores enum values', function(done) { it('correctly restores enum values', function(done) {
var self = this var self = this
...@@ -736,6 +798,24 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -736,6 +798,24 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
}) })
describe('update', function() { describe('update', function() {
it('updates the attributes that we select only without updating createdAt', function(done) {
var User = this.sequelize.define('User1', {
username: Sequelize.STRING,
secretValue: Sequelize.STRING
}, {
paranoid:true
})
User.sync({ force: true }).success(function() {
User.create({username: 'Peter', secretValue: '42'}).success(function(user) {
user.updateAttributes({ secretValue: '43' }, ['secretValue']).on('sql', function(sql) {
expect(sql).to.match(/UPDATE\s+[`"]+User1s[`"]+\s+SET\s+[`"]+secretValue[`"]='43',[`"]+updatedAt[`"]+='[^`",]+'\s+WHERE [`"]+id[`"]+=1/)
done()
})
})
})
})
it('allows sql logging of updated statements', function(done) { it('allows sql logging of updated statements', function(done) {
var User = this.sequelize.define('User', { var User = this.sequelize.define('User', {
name: Sequelize.STRING, name: Sequelize.STRING,
...@@ -799,8 +879,8 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -799,8 +879,8 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
expect(users[1].username).to.equal("Bill") expect(users[1].username).to.equal("Bill")
expect(users[2].username).to.equal("Bob") expect(users[2].username).to.equal("Bob")
expect(parseInt(+users[0].updatedAt/5000, 10)).to.equal(parseInt(+new Date()/5000, 10)) expect(parseInt(+users[0].updatedAt/5000, 10)).to.be.closeTo(parseInt(+new Date()/5000, 10), 1)
expect(parseInt(+users[1].updatedAt/5000, 10)).to.equal(parseInt(+new Date()/5000, 10)) expect(parseInt(+users[1].updatedAt/5000, 10)).to.be.closeTo(parseInt(+new Date()/5000, 10), 1)
done() done()
}) })
...@@ -1330,17 +1410,34 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -1330,17 +1410,34 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
{where: {id: 0}}, {where: {id: 0}},
{where: {id: '0'}} {where: {id: '0'}}
] ]
, done = _.after(2 * permutations.length, _done); , done = _.after(2 * permutations.length, _done)
this.User.bulkCreate([{username: 'jack'}, {username: 'jack'}]).success(function() { this.User.bulkCreate([{username: 'jack'}, {username: 'jack'}]).success(function() {
permutations.forEach(function(perm) { permutations.forEach(function(perm) {
self.User.find(perm).done(function(err, user) { self.User.find(perm).done(function(err, user) {
expect(err).to.be.null; expect(err).to.be.null
expect(user).to.be.null; expect(user).to.be.null
done(); done()
}).on('sql', function(s) { }).on('sql', function(s) {
expect(s.indexOf(0)).not.to.equal(-1); expect(s.indexOf(0)).not.to.equal(-1)
done(); done()
})
})
})
})
it('should allow us to find IDs using capital letters', function(done) {
var User = this.sequelize.define('User' + config.rand(), {
ID: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
Login: { type: Sequelize.STRING }
})
User.sync({ force: true }).success(function() {
User.create({Login: 'foo'}).success(function() {
User.find(1).success(function(user) {
expect(user).to.exist
expect(user.ID).to.equal(1)
done()
}) })
}) })
}) })
...@@ -2053,6 +2150,23 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -2053,6 +2150,23 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
}) })
}) })
}) })
it('should allow us to find IDs using capital letters', function(done) {
var User = this.sequelize.define('User' + config.rand(), {
ID: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
Login: { type: Sequelize.STRING }
})
User.sync({ force: true }).success(function() {
User.create({Login: 'foo'}).success(function() {
User.findAll({ID: 1}).success(function(user) {
expect(user).to.be.instanceof(Array)
expect(user).to.have.length(1)
done()
})
})
})
})
}) })
}) })
......
...@@ -19,6 +19,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () { ...@@ -19,6 +19,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
touchedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, touchedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
aNumber: { type: DataTypes.INTEGER }, aNumber: { type: DataTypes.INTEGER },
bNumber: { type: DataTypes.INTEGER }, bNumber: { type: DataTypes.INTEGER },
aDate: { type: DataTypes.DATE },
validateTest: { validateTest: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
...@@ -110,6 +111,154 @@ describe(Support.getTestDialectTeaser("DAO"), function () { ...@@ -110,6 +111,154 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
}) })
}) })
describe('isDirty', function() {
it('returns true for non-saved objects', function(done) {
var user = this.User.build({ username: 'user' })
expect(user.id).to.be.null
expect(user.isDirty).to.be.true
done()
})
it("returns false for saved objects", function(done) {
this.User.build({ username: 'user' }).save().success(function(user) {
expect(user.isDirty).to.be.false
done()
})
})
it("returns true for changed attribute", function(done) {
this.User.create({ username: 'user' }).success(function(user) {
user.username = 'new'
expect(user.isDirty).to.be.true
done()
})
})
it("returns false for non-changed attribute", function(done) {
this.User.create({ username: 'user' }).success(function(user) {
user.username = 'user'
expect(user.isDirty).to.be.false
done()
})
})
it("returns false for non-changed date attribute", function(done) {
this.User.create({ aDate: new Date(2013, 6, 31, 14, 25, 21) }).success(function(user) {
user.aDate = '2013-07-31 14:25:21'
expect(user.isDirty).to.be.false
done()
})
})
it("returns false for two empty attributes", function(done) {
this.User.create({ username: null }).success(function(user) {
user.username = ''
expect(user.isDirty).to.be.false
done()
})
})
it("returns true for bulk changed attribute", function(done) {
this.User.create({ username: 'user' }).success(function(user) {
user.setAttributes({
username: 'new',
aNumber: 1
})
expect(user.isDirty).to.be.true
done()
})
})
it("returns true for bulk non-changed attribute + model with timestamps", function(done) {
this.User.create({ username: 'user' }).success(function(user) {
user.setAttributes({
username: 'user'
})
expect(user.isDirty).to.be.rue
done()
})
})
it("returns false for bulk non-changed attribute + model without timestamps", function(done) {
var User = this.sequelize.define('User' + parseInt(Math.random() * 10000000), {
username: DataTypes.STRING
}, {
timestamps: false
})
User
.sync({ force: true })
.then(function() {
return User.create({ username: "user" })
})
.then(function(user) {
return user.setAttributes({ username: "user" })
expect(user.isDirty).to.be.false
})
.then(function() {
done()
})
})
it("returns true for changed and bulk non-changed attribute", function(done) {
this.User.create({ username: 'user' }).success(function(user) {
user.aNumber = 23
user.setAttributes({
username: 'user'
})
expect(user.isDirty).to.be.true
done()
})
})
it("returns true for changed attribute and false for saved object", function(done) {
this.User.create({ username: 'user' }).success(function(user) {
user.username = 'new'
expect(user.isDirty).to.be.true
user.save().success(function() {
expect(user.isDirty).to.be.false
done()
})
})
})
it("returns false for created objects", function(done) {
this.User.create({ username: 'user' }).success(function(user) {
expect(user.isDirty).to.be.false
done()
})
})
it("returns false for objects found by find method", function(done) {
var self = this
this.User.create({ username: 'user' }).success(function(user) {
self.User.find(user.id).success(function(user) {
expect(user.isDirty).to.be.false
done()
})
})
})
it("returns false for objects found by findAll method", function(done) {
var self = this
, users = []
for (var i = 0; i < 10; i++) {
users[users.length] = {username: 'user'}
}
this.User.bulkCreate(users).success(function() {
self.User.findAll().success(function(users) {
users.forEach(function(u) {
expect(u.isDirty).to.be.false
})
done()
})
})
})
})
describe('increment', function () { describe('increment', function () {
beforeEach(function(done) { beforeEach(function(done) {
this.User.create({ id: 1, aNumber: 0, bNumber: 0 }).complete(function(){ this.User.create({ id: 1, aNumber: 0, bNumber: 0 }).complete(function(){
...@@ -985,17 +1134,21 @@ describe(Support.getTestDialectTeaser("DAO"), function () { ...@@ -985,17 +1134,21 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
identifier: 'identifier' identifier: 'identifier'
}).success(function(user) { }).success(function(user) {
var oldCreatedAt = user.createdAt var oldCreatedAt = user.createdAt
, oldUpdatedAt = user.updatedAt
, oldIdentifier = user.identifier , oldIdentifier = user.identifier
user.updateAttributes({ setTimeout(function () {
name: 'foobar', user.updateAttributes({
createdAt: new Date(2000, 1, 1), name: 'foobar',
identifier: 'another identifier' createdAt: new Date(2000, 1, 1),
}).success(function(user) { identifier: 'another identifier'
expect((new Date(user.createdAt)).getTime()).to.equal((new Date(oldCreatedAt)).getTime()) }).success(function(user) {
expect(user.identifier).to.equal(oldIdentifier) expect(new Date(user.createdAt)).to.equalDate(new Date(oldCreatedAt))
done() expect(new Date(user.updatedAt)).to.not.equalTime(new Date(oldUpdatedAt))
}) expect(user.identifier).to.equal(oldIdentifier)
done()
})
}, 1000)
}) })
}) })
}) })
......
var chai = require('chai') var chai = require('chai')
, expect = chai.expect , expect = chai.expect
, Support = require(__dirname + '/support') , Support = require(__dirname + '/support')
, DataTypes = require(__dirname + "/../lib/data-types")
, QueryChainer = require("../lib/query-chainer")
, Migrator = require("../lib/migrator") , Migrator = require("../lib/migrator")
, dialect = Support.getTestDialect() , dialect = Support.getTestDialect()
...@@ -16,6 +14,7 @@ describe(Support.getTestDialectTeaser("Migrator"), function() { ...@@ -16,6 +14,7 @@ describe(Support.getTestDialectTeaser("Migrator"), function() {
logging: function(){} logging: function(){}
}, options || {}) }, options || {})
// this.sequelize.options.logging = console.log
var migrator = new Migrator(this.sequelize, options) var migrator = new Migrator(this.sequelize, options)
migrator migrator
...@@ -87,7 +86,7 @@ describe(Support.getTestDialectTeaser("Migrator"), function() { ...@@ -87,7 +86,7 @@ describe(Support.getTestDialectTeaser("Migrator"), function() {
SequelizeMeta.create({ from: null, to: 20111117063700 }).success(function() { SequelizeMeta.create({ from: null, to: 20111117063700 }).success(function() {
migrator.getUndoneMigrations(function(err, migrations) { migrator.getUndoneMigrations(function(err, migrations) {
expect(err).to.be.null expect(err).to.be.null
expect(migrations).to.have.length(6) expect(migrations).to.have.length(7)
expect(migrations[0].filename).to.equal('20111130161100-emptyMigration.js') expect(migrations[0].filename).to.equal('20111130161100-emptyMigration.js')
done() done()
}) })
...@@ -186,6 +185,31 @@ describe(Support.getTestDialectTeaser("Migrator"), function() { ...@@ -186,6 +185,31 @@ describe(Support.getTestDialectTeaser("Migrator"), function() {
}) })
describe('addColumn', function() { describe('addColumn', function() {
it('adds a unique column to the user table', function(done) {
var self = this
this.init({ from: 20111117063700, to: 20111205167000 }, function(migrator) {
migrator.migrate().complete(function(err) {
self.sequelize.getQueryInterface().describeTable('User').complete(function(err, data) {
var signature = data.signature
, isAdmin = data.isAdmin
, shopId = data.shopId
expect(signature.allowNull).to.be.true
expect(isAdmin.allowNull).to.be.false
if (dialect === "postgres" || dialect === "postgres-native" || dialect === "sqlite") {
expect(isAdmin.defaultValue).to.be.false
} else {
expect(isAdmin.defaultValue).to.equal("0")
}
expect(shopId.allowNull).to.be.true
done()
})
})
})
})
it('adds a column to the user table', function(done) { it('adds a column to the user table', function(done) {
var self = this var self = this
......
...@@ -85,6 +85,57 @@ if (dialect.match(/^mysql/)) { ...@@ -85,6 +85,57 @@ if (dialect.match(/^mysql/)) {
}) })
}) })
describe('validations', function() {
describe('enums', function() {
it('enum data type should be case insensitive if my collation allows it', function(done) {
var User = this.sequelize.define('User' + config.rand(), {
mood: {
type: DataTypes.ENUM,
values: ['HAPPY', 'sad', 'WhatEver']
}
}, {
collate: 'utf8_general_ci'
})
User.sync({ force: true }).success(function() {
User.create({mood: 'happy'}).success(function(user) {
expect(user).to.exist
expect(user.mood).to.equal('HAPPY')
var u = User.build({mood: 'SAD'})
u.save().success(function(_user) {
expect(_user).to.exist
expect(_user.mood).to.equal('sad')
done()
})
})
})
})
it('enum data type should be case sensitive if my collation enforces it', function(done) {
var User = this.sequelize.define('User' + config.rand(), {
mood: {
type: DataTypes.ENUM,
values: ['HAPPY', 'sad', 'WhatEver']
}
}, {
collate: 'latin1_bin'
})
User.sync({ force: true }).success(function() {
expect(function() {
User.create({mood: 'happy'})
}).to.throw(Error, 'Value "happy" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver')
expect(function() {
var u = User.build({mood: 'SAD'})
u.save()
}).to.throw(Error, 'Value "SAD" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver')
done()
})
})
})
})
describe('primaryKeys', function() { describe('primaryKeys', function() {
it("determines the correct primaryKeys", function(done) { it("determines the correct primaryKeys", function(done) {
var User = this.sequelize.define('User' + config.rand(), { var User = this.sequelize.define('User' + config.rand(), {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!