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

Commit 78645c10 by Sascha Depold

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

Conflicts:
	lib/dao-factory.js
	lib/dao.js
	test/support.js
2 parents 04b5fe87 5ca77c33
......@@ -33,6 +33,7 @@ changelog of the branch: https://github.com/sequelize/sequelize/blob/milestones/
- Associations
- Importing definitions from single files
- Promises
- Hooks/callbacks/lifecycle events
## Documentation and Updates ##
......@@ -62,7 +63,7 @@ A very basic roadmap. Chances aren't too bad, that not mentioned things are impl
- Support for update of tables without primary key
- MariaDB support
- ~~Support for update and delete calls for whole tables without previous loading of instances~~ Implemented in [#569](https://github.com/sequelize/sequelize/pull/569) thanks to @optiltude
- Eager loading of nested associations [#388](https://github.com/sdepold/sequelize/issues/388#issuecomment-12019099)
- Eager loading of nested associations [#388](https://github.com/sequelize/sequelize/issues/388)
- ~~Model#delete~~ (renamed to [Model.destroy()](http://sequelizejs.com/documentation#instances-destroy))
- ~~Validate a model before it gets saved.~~ Implemented in [#601](https://github.com/sequelize/sequelize/pull/601), thanks to @durango
- Move validation of enum attribute value to validate method
......
......@@ -14,6 +14,8 @@ module.exports = (function() {
this.options.foreignKey = Utils._.underscoredIf(Utils.singularize(this.options.as, this.source.options.language) + "Id", this.source.options.underscored)
}
this.options.useHooks = options.useHooks
this.associationAccessor = this.isSelfAssociation
? Utils.combineTableNames(this.target.tableName, this.options.as || this.target.tableName)
: this.options.as || this.target.tableName
......@@ -31,7 +33,7 @@ module.exports = (function() {
Utils._.defaults(this.source.rawAttributes, newAttributes)
// Sync attributes to DAO proto each time a new assoc is added
this.source.DAO.prototype.attributes = Object.keys(this.source.DAO.prototype.rawAttributes);
this.source.DAO.prototype.attributes = Object.keys(this.source.DAO.prototype.rawAttributes)
return this
}
......
......@@ -30,7 +30,7 @@ module.exports = (function() {
HasManySingleLinked.prototype.injectSetter = function(emitter, oldAssociations, newAssociations) {
var self = this
, associationKeys = Object.keys((oldAssociations[0] || newAssociations[0] || {}).daoFactory.primaryKeys || {})
, associationKeys = Object.keys((oldAssociations[0] || newAssociations[0] || {daoFactory: {primaryKeys: {}}}).daoFactory.primaryKeys || {})
, associationKey = associationKeys.length === 1 ? associationKeys[0] : 'id'
, chainer = new Utils.QueryChainer()
, obsoleteAssociations = oldAssociations.filter(function (old) {
......
......@@ -21,6 +21,8 @@ module.exports = (function() {
this.options.tableName = this.combinedName = (this.options.joinTableName || combinedTableName)
this.associationAccessor = this.options.as || this.combinedName
this.options.useHooks = options.useHooks
var as = (this.options.as || Utils.pluralize(this.target.tableName, this.target.options.language))
this.accessors = {
......@@ -184,15 +186,46 @@ module.exports = (function() {
var customEventEmitter = new Utils.CustomEventEmitter(function() {
instance[self.accessors.get]().success(function(currentAssociatedObjects) {
var newAssociations = []
, oldAssociations = []
currentAssociatedObjects.forEach(function(association) {
if (!Utils._.isEqual(oldAssociatedObject.identifiers, association.identifiers))
newAssociations.push(association)
if (!Utils._.isEqual(oldAssociatedObject.identifiers, association.identifiers)) {
newAssociations[newAssociations.length] = association
} else {
oldAssociations[oldAssociations.length] = association
}
})
instance[self.accessors.set](newAssociations)
.success(function() { customEventEmitter.emit('success', null) })
.error(function(err) { customEventEmitter.emit('error', err) })
var tick = 0
var next = function(err, i) {
if (!!err || i >= oldAssociations.length) {
return run(err)
}
oldAssociations[i].destroy().error(function(err) {
next(err)
})
.success(function() {
tick++
next(null, tick)
})
}
var run = function(err) {
if (!!err) {
return customEventEmitter.emit('error', err)
}
instance[self.accessors.set](newAssociations)
.success(function() { customEventEmitter.emit('success', null) })
.error(function(err) { customEventEmitter.emit('error', err) })
}
if (oldAssociations.length > 0) {
next(null, tick)
} else {
run()
}
})
})
return customEventEmitter.run()
......
......@@ -18,6 +18,8 @@ module.exports = (function() {
? Utils.combineTableNames(this.target.tableName, this.options.as || this.target.tableName)
: this.options.as || this.target.tableName
this.options.useHooks = options.useHooks
this.accessors = {
get: Utils._.camelize('get_' + (this.options.as || Utils.singularize(this.target.tableName, this.target.options.language))),
set: Utils._.camelize('set_' + (this.options.as || Utils.singularize(this.target.tableName, this.target.options.language)))
......
......@@ -7,6 +7,11 @@ var Utils = require("./../utils")
var Mixin = module.exports = function(){}
Mixin.hasOne = function(associatedDAO, options) {
// Since this is a mixin, we'll need a unique variable name for hooks (since DAOFactory will override our hooks option)
options = options || {}
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks)
options.useHooks = options.hooks
// the id is in the foreign table
var association = new HasOne(this, associatedDAO, Utils._.extend((options||{}), this.options))
this.associations[association.associationAccessor] = association.injectAttributes()
......@@ -18,8 +23,13 @@ Mixin.hasOne = function(associatedDAO, options) {
}
Mixin.belongsTo = function(associatedDAO, options) {
// Since this is a mixin, we'll need a unique variable name for hooks (since DAOFactory will override our hooks option)
options = options || {}
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks)
options.useHooks = options.hooks
// the id is in this table
var association = new BelongsTo(this, associatedDAO, Utils._.extend((options || {}), this.options))
var association = new BelongsTo(this, associatedDAO, Utils._.extend(options, this.options))
this.associations[association.associationAccessor] = association.injectAttributes()
association.injectGetter(this.DAO.prototype)
......@@ -29,6 +39,11 @@ Mixin.belongsTo = function(associatedDAO, options) {
}
Mixin.hasMany = function(associatedDAO, options) {
// Since this is a mixin, we'll need a unique variable name for hooks (since DAOFactory will override our hooks option)
options = options || {}
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks)
options.useHooks = options.hooks
// the id is in the foreign table or in a connecting table
var association = new HasMany(this, associatedDAO, Utils._.extend((options||{}), this.options))
this.associations[association.associationAccessor] = association.injectAttributes()
......
......@@ -25,7 +25,11 @@ module.exports = (function() {
schemaDelimiter: '',
language: 'en',
defaultScope: null,
scopes: null
scopes: null,
hooks: {
beforeCreate: [],
afterCreate: []
}
}, options || {})
// error check options
......@@ -568,7 +572,9 @@ module.exports = (function() {
*/
DAOFactory.prototype.bulkCreate = function(records, fields, options) {
options = options || {}
options.validate = options.validate || false
options.validate = options.validate === undefined ? false : Boolean(options.validate)
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks)
fields = fields || []
var self = this
......@@ -576,63 +582,137 @@ module.exports = (function() {
, createdAtAttr = Utils._.underscoredIf(self.options.createdAt, self.options.underscored)
, errors = []
, daos = records.map(function(v) {
var build = self.build(v)
if (options.validate === true) {
var valid = build.validate({skip: fields})
if (valid !== null) {
errors[errors.length] = {record: v, errors: valid}
}
}
return build
return self.build(v)
})
if (options.validate === true && errors.length > 0) {
return new Utils.CustomEventEmitter(function(emitter) {
emitter.emit('error', errors)
}).run()
}
// we will re-create from DAOs, which may have set up default attributes
records = []
var found = false
return new Utils.CustomEventEmitter(function(emitter) {
var done = function() {
self.runHooks('afterBulkCreate', daos, fields, function(err, newRecords, newFields) {
if (!!err) {
return emitter.emit('error', err)
}
daos.forEach(function(dao) {
var values = fields.length > 0 ? {} : dao.dataValues
daos = newRecords || daos
fields = newFields || fields
fields.forEach(function(field) {
values[field] = dao.dataValues[field]
})
emitter.emit('success', daos, fields)
})
}
if (self.options.timestamps) {
if (!values[createdAtAttr]) {
values[createdAtAttr] = Utils.now(self.daoFactoryManager.sequelize.options.dialect)
var next = function() {
if (options.hooks === false) {
return runQuery()
}
if (!values[updatedAtAttr]) {
values[updatedAtAttr] = Utils.now(self.daoFactoryManager.sequelize.options.dialect)
var i = 0
var iterate = function(i) {
self.runHooks('beforeCreate', daos[i], function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
daos[i] = newValues || daos[i]
daos[i].save().error(function(err) {
emitter.emit('error', err)
}).success(function() {
self.runHooks('afterCreate', daos[i], function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
daos[i] = newValues || daos[i]
i++
if (i >= daos.length) {
return done()
}
iterate(i)
})
})
})
}
iterate(i)
}
records.push(values)
})
var runQuery = function() {
// we will re-create from DAOs, which may have set up default attributes
records = []
daos.forEach(function(dao) {
var values = fields.length > 0 ? {} : dao.dataValues
fields.forEach(function(field) {
values[field] = dao.dataValues[field]
})
// 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 && !(definition.allowNull === true && values[attrName] === null)) {
throw new Error('Value "' + values[attrName] + '" for ENUM ' + attrName + ' is out of allowed scope. Allowed values: ' + definition.values.join(', '))
if (self.options.timestamps) {
if (!values[createdAtAttr]) {
values[createdAtAttr] = Utils.now(self.daoFactoryManager.sequelize.options.dialect)
}
if (!values[updatedAtAttr]) {
values[updatedAtAttr] = Utils.now(self.daoFactoryManager.sequelize.options.dialect)
}
}
}
records.push(values)
})
self.QueryInterface.bulkInsert(self.tableName, records)
.on('sql', function(sql) {
emitter.emit('sql', sql)
})
.error(function(err) {
emitter.emit('error', err)
}).success(function() {
done()
})
}
})
return self.QueryInterface.bulkInsert(self.tableName, records)
self.runHooks('beforeBulkCreate', daos, fields, function(err, newRecords, newFields) {
if (!!err) {
return emitter.emit('error', err)
}
daos = newRecords || daos
fields = newFields || fields
if (options.validate === true) {
if (options.hooks === true) {
var iterate = function(i) {
daos[i].hookValidate({skip: fields}).error(function(err) {
errors[errors.length] = {record: v, errors: err}
i++
if (i > daos.length) {
if (errors.length > 0) {
return emitter.emit('error', errors)
}
return next()
}
iterate(i)
})
}
} else {
daos.forEach(function(v) {
var valid = v.validate({skip: fields})
if (valid !== null) {
errors[errors.length] = {record: v, errors: valid}
}
})
if (errors.length > 0) {
return emitter.emit('error', errors)
}
next()
}
} else {
next()
}
})
}).run()
}
/**
......@@ -640,19 +720,117 @@ module.exports = (function() {
*
* @param {Object} where Options to describe the scope of the search.
* @param {Object} options Possible options are:
- hooks: If set to true, destroy will find all records within the where parameter and will execute before/afterDestroy hooks on each row
- limit: How many rows to delete
- truncate: If set to true, dialects that support it will use TRUNCATE instead of DELETE FROM. If a table is truncated the where and limit options are ignored
* @return {Object} A promise which fires `success`, `error`, `complete` and `sql`.
*/
DAOFactory.prototype.destroy = function(where, options) {
if (this.options.timestamps && this.options.paranoid) {
var attr = Utils._.underscoredIf(this.options.deletedAt, this.options.underscored)
var attrValueHash = {}
attrValueHash[attr] = Utils.now()
return this.QueryInterface.bulkUpdate(this.tableName, attrValueHash, where)
} else {
return this.QueryInterface.bulkDelete(this.tableName, where, options)
}
var self = this
, query = null
, args = []
return new Utils.CustomEventEmitter(function(emitter) {
self.runHooks(self.options.hooks.beforeBulkDestroy, where, function(err, newWhere) {
if (!!err) {
return emitter.emit('error', err)
}
where = newWhere || where
if (self.options.timestamps && self.options.paranoid) {
var attr = Utils._.underscoredIf(self.options.deletedAt, self.options.underscored)
var attrValueHash = {}
attrValueHash[attr] = Utils.now()
query = 'bulkUpdate'
args = [self.tableName, attrValueHash, where]
} else {
query = 'bulkDelete'
args = [self.tableName, where, options]
}
var runQuery = function(err, records) {
if (!!err) {
return emitter.emit('error', err)
}
query = self.QueryInterface[query].apply(self.QueryInterface, args)
query.on('sql', function(sql) {
emitter.emit('sql', sql)
})
.error(function(err) {
emitter.emit('error', err)
})
.success(function(results) {
var finished = function(err) {
if (!!err) {
return emitter.emit('error', err)
}
self.runHooks(self.options.hooks.afterBulkDestroy, where, function(err) {
if (!!err) {
return emitter.emit('error', err)
}
emitter.emit('success', results)
})
}
if (options && options.hooks === true) {
var tick = 0
var next = function(i) {
self.runHooks(self.options.hooks.afterDestroy, records[i], function(err, newValues) {
if (!!err) {
return finished(err)
}
records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues
tick++
if (tick >= records.length) {
return finished()
}
next(tick)
})
}
next(tick)
} else {
finished()
}
})
}
if (options && options.hooks === true) {
var tick = 0
self.all({where: where}).error(function(err) { emitter.emit('error', err) })
.success(function(records) {
var next = function(i) {
self.runHooks(self.options.hooks.beforeDestroy, records[i], function(err, newValues) {
if (!!err) {
return runQuery(err)
}
records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues
tick++
if (tick >= records.length) {
return runQuery(null, records)
}
next(tick)
})
}
next(tick)
})
//
} else {
runQuery()
}
})
}).run()
}
/**
......@@ -663,27 +841,129 @@ module.exports = (function() {
* @return {Object} A promise which fires `success`, `error`, `complete` and `sql`.
*/
DAOFactory.prototype.update = function(attrValueHash, where, options) {
var self = this
, query = null
, tick = 0
options = options || {}
options.validate = options.validate === undefined ? true : Boolean(options.validate)
options.validate = options.validate === undefined ? true : Boolean(options.validate)
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks)
if(this.options.timestamps) {
var attr = Utils._.underscoredIf(this.options.updatedAt, this.options.underscored)
if (self.options.timestamps) {
var attr = Utils._.underscoredIf(self.options.updatedAt, self.options.underscored)
attrValueHash[attr] = Utils.now()
}
if (options.validate === true) {
var build = this.build(attrValueHash)
, attrKeys = Object.keys(attrValueHash)
, validate = build.validate({skip: Object.keys(build.dataValues).filter(function(val) { return attrKeys.indexOf(val) !== -1 })})
return new Utils.CustomEventEmitter(function(emitter) {
var runSave = function() {
self.runHooks(self.options.hooks.beforeBulkUpdate, attrValueHash, where, function(err, attributes, _where) {
if (!!err) {
return emitter.emit('error', err)
}
where = _where || where
attrValueHash = attributes || attrValueHash
var runQuery = function(err, records) {
if (!!err) {
return emitter.emit('error', err)
}
query = self.QueryInterface.bulkUpdate(self.tableName, attrValueHash, where, options)
query.on('sql', function(sql) {
emitter.emit('sql', sql)
})
.error(function(err) {
emitter.emit('error', err)
})
.success(function(results) {
var finished = function(err, records) {
if (!!err) {
return emitter.emit('error', err)
}
self.runHooks(self.options.hooks.afterBulkUpdate, attrValueHash, where, function(err) {
if (!!err) {
return emitter.emit('error', err)
}
emitter.emit('success', records)
})
}
if (options && options.hooks === true && !!records && records.length > 0) {
var tick = 0
var next = function(i) {
self.runHooks(self.options.hooks.afterUpdate, records[i], function(err, newValues) {
if (!!err) {
return finished(err)
}
records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues
tick++
if (tick >= records.length) {
return finished(null, records)
}
next(tick)
})
}
next(tick)
} else {
finished(null, results)
}
})
}
if (options.hooks === true) {
self.all({where: where}).error(function(err) { emitter.emit('error', err) })
.success(function(records) {
if (records === null || records.length < 1) {
return runQuery(null)
}
var next = function(i) {
self.runHooks(self.options.hooks.beforeUpdate, records[i], function(err, newValues) {
if (!!err) {
return runQuery(err)
}
records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues
tick++
if (validate !== null && Object.keys(validate).length > 0) {
return new Utils.CustomEventEmitter(function(emitter) {
emitter.emit('error', validate)
}).run()
if (tick >= records.length) {
return runQuery(null, records)
}
next(tick)
})
}
next(tick)
})
} else {
runQuery()
}
})
}
}
return this.QueryInterface.bulkUpdate(this.tableName, attrValueHash, where, options)
if (options.validate === true) {
var build = self.build(attrValueHash)
build.hookValidate({skip: Object.keys(attrValueHash)}).error(function(err) {
emitter.emit('error', err)
}).success(function(attributes) {
if (!!attributes && !!attributes.dataValues) {
attrValueHash = Utils._.pick.apply(Utils._, [].concat(attributes.dataValues).concat(Object.keys(attrValueHash)))
}
runSave()
})
} else {
runSave()
}
}).run()
}
DAOFactory.prototype.describe = function(schema) {
......@@ -848,6 +1128,7 @@ module.exports = (function() {
}
Utils._.extend(DAOFactory.prototype, require("./associations/mixin"))
Utils._.extend(DAOFactory.prototype, require(__dirname + '/hooks'))
return DAOFactory
})()
......@@ -18,19 +18,50 @@ DaoValidator.prototype.validate = function() {
return errors
}
DaoValidator.prototype.hookValidate = function() {
var self = this
, errors = {}
return new Utils.CustomEventEmitter(function(emitter) {
self.model.daoFactory.runHooks('beforeValidate', self.model.dataValues, function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
self.model.dataValues = newValues || self.model.dataValues
errors = Utils._.extend(errors, validateAttributes.call(self))
errors = Utils._.extend(errors, validateModel.call(self))
if (Object.keys(errors).length > 0) {
return emitter.emit('error', errors)
}
self.model.daoFactory.runHooks('afterValidate', self.model.dataValues, function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
self.model.dataValues = newValues || self.model.dataValues
emitter.emit('success', self.model)
})
})
}).run()
}
// private
var validateModel = function() {
var errors = {}
var self = this
, errors = {}
// for each model validator for this DAO
Utils._.each(this.model.__options.validate, function(validator, validatorType) {
try {
validator.apply(this.model)
validator.apply(self.model)
} catch (err) {
errors[validatorType] = [err.message] // TODO: data structure needs to change for 2.0
}
}.bind(this))
})
return errors
}
......@@ -54,11 +85,12 @@ var validateAttributes = function() {
}
var validateAttribute = function(value, field) {
var errors = {}
var self = this
, errors = {}
// for each validator
Utils._.each(this.model.validators[field], function(details, validatorType) {
var validator = prepareValidationOfAttribute.call(this, value, details, validatorType)
var validator = prepareValidationOfAttribute.call(self, value, details, validatorType)
try {
validator.fn.apply(null, validator.args)
......@@ -74,7 +106,7 @@ var validateAttribute = function(value, field) {
errors[field] = errors[field] || []
errors[field].push(msg)
}
}.bind(this)) // for each validator for this field
})
return errors
}
......
......@@ -122,68 +122,98 @@ module.exports = (function() {
})
}
var errors = this.validate()
if (!!errors) {
return new Utils.CustomEventEmitter(function(emitter) {
emitter.emit('error', errors)
}).run()
}
for (var attrName in this.daoFactory.rawAttributes) {
if (this.daoFactory.rawAttributes.hasOwnProperty(attrName)) {
var definition = this.daoFactory.rawAttributes[attrName]
, isEnum = definition.type && (definition.type.toString() === DataTypes.ENUM.toString())
, isHstore = !!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type
, hasValue = values[attrName] !== undefined
, 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]
return new Utils.CustomEventEmitter(function(emitter) {
self.hookValidate().error(function(err) {
emitter.emit('error', err)
}).success(function() {
for (var attrName in self.daoFactory.rawAttributes) {
if (self.daoFactory.rawAttributes.hasOwnProperty(attrName)) {
var definition = self.daoFactory.rawAttributes[attrName]
, isHstore = !!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type
, isEnum = definition.type && (definition.type.toString() === DataTypes.ENUM.toString())
, isMySQL = self.daoFactory.daoFactoryManager.sequelize.options.dialect === "mysql"
, ciCollation = !!self.daoFactory.options.collate && self.daoFactory.options.collate.match(/_ci$/i)
// Unfortunately for MySQL CI collation we need to map/lowercase values again
if (isEnum && isMySQL && ciCollation) {
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]
}
}
if (isHstore) {
if (typeof values[attrName] === "object") {
values[attrName] = hstore.stringify(values[attrName])
}
}
}
} else {
valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1)
}
if (isEnum && hasValue && valueOutOfScope && !(definition.allowNull === true && values[attrName] === null)) {
throw new Error('Value "' + values[attrName] + '" for ENUM ' + attrName + ' is out of allowed scope. Allowed values: ' + definition.values.join(', '))
if (self.__options.timestamps && self.dataValues.hasOwnProperty(updatedAtAttr)) {
self.dataValues[updatedAtAttr] = values[updatedAtAttr] = (
(
self.isNewRecord
&& !!self.daoFactory.rawAttributes[updatedAtAttr]
&& !!self.daoFactory.rawAttributes[updatedAtAttr].defaultValue
)
? self.daoFactory.rawAttributes[updatedAtAttr].defaultValue
: Utils.now(self.sequelize.options.dialect))
}
if (isHstore) {
if (typeof values[attrName] === "object") {
values[attrName] = hstore.stringify(values[attrName])
}
}
}
}
var query = null
, args = []
, hook = ''
if (this.__options.timestamps && this.dataValues.hasOwnProperty(updatedAtAttr)) {
this.dataValues[updatedAtAttr] = values[updatedAtAttr] = (this.isNewRecord && !!this.daoFactory.rawAttributes[updatedAtAttr] && !!this.daoFactory.rawAttributes[updatedAtAttr].defaultValue ? this.daoFactory.rawAttributes[updatedAtAttr].defaultValue : Utils.now(this.sequelize.options.dialect))
}
if (self.isNewRecord) {
self.isDirty = false
query = 'insert'
args = [self, self.QueryInterface.QueryGenerator.addSchema(self.__factory), values]
hook = 'Create'
} else {
var identifier = self.__options.hasPrimaryKeys ? self.primaryKeyValues : { id: self.id }
if (this.isNewRecord) {
this.isDirty = false
return this.QueryInterface.insert(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory), values)
} else {
var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : { id: this.id };
if (identifier === null && self.__options.whereCollection !== null) {
identifier = self.__options.whereCollection;
}
if (identifier === null && this.__options.whereCollection !== null) {
identifier = this.__options.whereCollection;
}
self.isDirty = false
query = 'update'
args = [self, self.QueryInterface.QueryGenerator.addSchema(self.__factory), values, identifier, options]
hook = 'Update'
}
this.isDirty = false
var tableName = this.QueryInterface.QueryGenerator.addSchema(this.__factory)
, query = this.QueryInterface.update(this, tableName, values, identifier, options)
self.__factory.runHooks('before' + hook, values, function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
return query
}
// redeclare our new values
args[2] = newValues || args[2]
self.QueryInterface[query].apply(self.QueryInterface, args)
.on('sql', function(sql) {
emitter.emit('sql', sql)
})
.error(function(err) {
emitter.emit('error', err)
})
.success(function(result) {
self.__factory.runHooks('after' + hook, result.values, function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
result.dataValues = newValues
emitter.emit('success', result)
})
})
})
})
}).run()
}
/*
......@@ -231,6 +261,16 @@ module.exports = (function() {
return (Utils._.isEmpty(errors) ? null : errors)
}
/*
* Validate this dao's attribute values according to validation rules set in the dao definition.
*
* @return CustomEventEmitter with null if validation successful; otherwise an object containing { field name : [error msgs] } entries.
*/
DAO.prototype.hookValidate = function(object) {
var validator = new DaoValidator(this, object)
return validator.hookValidate()
}
DAO.prototype.updateAttributes = function(updates, fields) {
this.setAttributes(updates)
......@@ -277,14 +317,41 @@ module.exports = (function() {
}
DAO.prototype.destroy = function() {
if (this.__options.timestamps && this.__options.paranoid) {
var attr = Utils._.underscoredIf(this.__options.deletedAt, this.__options.underscored)
this.dataValues[attr] = new Date()
return this.save()
} else {
var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : { id: this.id };
return this.QueryInterface.delete(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory.tableName, this.__factory.options.schema), identifier)
}
var self = this
, query = null
return new Utils.CustomEventEmitter(function(emitter) {
self.daoFactory.runHooks(self.daoFactory.options.hooks.beforeDestroy, self.dataValues, function(err) {
if (!!err) {
return emitter.emit('error', err)
}
if (self.__options.timestamps && self.__options.paranoid) {
var attr = Utils._.underscoredIf(self.__options.deletedAt, self.__options.underscored)
self.dataValues[attr] = new Date()
query = self.save()
} else {
var identifier = self.__options.hasPrimaryKeys ? self.primaryKeyValues : { id: self.id };
query = self.QueryInterface.delete(self, self.QueryInterface.QueryGenerator.addSchema(self.__factory.tableName, self.__factory.options.schema), identifier)
}
query.on('sql', function(sql) {
emitter.emit('sql', sql)
})
.error(function(err) {
emitter.emit('error', err)
})
.success(function(results) {
self.daoFactory.runHooks(self.daoFactory.options.hooks.afterDestroy, self.dataValues, function(err) {
if (!!err) {
return emitter.emit('error', err)
}
emitter.emit('success', results)
})
})
})
}).run()
}
DAO.prototype.increment = function(fields, count) {
......
var Hooks = module.exports = function(){}
Hooks.runHooks = function() {
var self = this
, tick = 0
, hooks = arguments[0]
, args = Array.prototype.slice.call(arguments, 1, arguments.length-1)
, fn = arguments[arguments.length-1]
if (typeof hooks === "string") {
hooks = this.options.hooks[hooks] || []
}
if (!Array.isArray(hooks)) {
hooks = hooks === undefined ? [] : [hooks]
}
if (hooks.length < 1) {
return fn.apply(this, [null].concat(args))
}
var run = function(hook) {
if (!hook) {
return fn.apply(this, [null].concat(args))
}
if (typeof hook === "object") {
hook = hook.fn
}
hook.apply(self, args.concat(function() {
tick++
if (!!arguments[0]) {
return fn(arguments[0])
}
// daoValues = newValues
return run(hooks[tick])
}))
}
run(hooks[tick])
}
Hooks.hook = function(hookType, name, fn) {
// For aliases, we may want to incorporate some sort of way to mitigate this
if (hookType === "beforeDelete") {
hookType = 'beforeDestroy'
}
else if (hookType === "afterDelete") {
hookType = 'afterDestroy'
}
Hooks.addHook.call(this, hookType, name, fn)
}
Hooks.addHook = function(hookType, name, fn) {
if (typeof name === "function") {
fn = name
name = null
}
var method = function() {
fn.apply(this, Array.prototype.slice.call(arguments, 0, arguments.length-1).concat(arguments[arguments.length-1]))
}
// Just in case if we override the default DAOFactory.options
this.options.hooks[hookType] = this.options.hooks[hookType] || []
this.options.hooks[hookType][this.options.hooks[hookType].length] = !!name ? {name: name, fn: method} : method
}
Hooks.beforeValidate = function(name, fn) {
Hooks.addHook.call(this, 'beforeValidate', name, fn)
}
Hooks.afterValidate = function(name, fn) {
Hooks.addHook.call(this, 'afterValidate', name, fn)
}
Hooks.beforeCreate = function(name, fn) {
Hooks.addHook.call(this, 'beforeCreate', name, fn)
}
Hooks.afterCreate = function(name, fn) {
Hooks.addHook.call(this, 'afterCreate', name, fn)
}
Hooks.beforeDestroy = function(name, fn) {
Hooks.addHook.call(this, 'beforeDestroy', name, fn)
}
Hooks.afterDestroy = function(name, fn) {
Hooks.addHook.call(this, 'afterDestroy', name, fn)
}
Hooks.beforeDelete = function(name, fn) {
Hooks.addHook.call(this, 'beforeDestroy', name, fn)
}
Hooks.afterDelete = function(name, fn) {
Hooks.addHook.call(this, 'afterDestroy', name, fn)
}
Hooks.beforeUpdate = function(name, fn) {
Hooks.addHook.call(this, 'beforeUpdate', name, fn)
}
Hooks.afterUpdate = function(name, fn) {
Hooks.addHook.call(this, 'afterUpdate', name, fn)
}
Hooks.beforeBulkCreate = function(name, fn) {
Hooks.addHook.call(this, 'beforeBulkCreate', name, fn)
}
Hooks.afterBulkCreate = function(name, fn) {
Hooks.addHook.call(this, 'afterBulkCreate', name, fn)
}
Hooks.beforeBulkDestroy = function(name, fn) {
Hooks.addHook.call(this, 'beforeBulkDestroy', name, fn)
}
Hooks.afterBulkDestroy = function(name, fn) {
Hooks.addHook.call(this, 'afterBulkDestroy', name, fn)
}
Hooks.beforeBulkUpdate = function(name, fn) {
Hooks.addHook.call(this, 'beforeBulkUpdate', name, fn)
}
Hooks.afterBulkUpdate = function(name, fn) {
Hooks.addHook.call(this, 'afterBulkUpdate', name, fn)
}
......@@ -479,9 +479,10 @@ module.exports = (function() {
}
QueryInterface.prototype.delete = function(dao, tableName, identifier) {
var self = this
, restrict = false
, sql = self.QueryGenerator.deleteQuery(tableName, identifier, null, dao.daoFactory)
var self = this
, restrict = false
, cascades = []
, sql = self.QueryGenerator.deleteQuery(tableName, identifier, null, dao.daoFactory)
// Check for a restrict field
if (!!dao.daoFactory && !!dao.daoFactory.associations) {
......@@ -489,29 +490,84 @@ module.exports = (function() {
, length = keys.length
for (var i = 0; i < length; i++) {
if (dao.daoFactory.associations[keys[i]].options && dao.daoFactory.associations[keys[i]].options.onDelete && dao.daoFactory.associations[keys[i]].options.onDelete === "restrict") {
restrict = true
if (dao.daoFactory.associations[keys[i]].options && dao.daoFactory.associations[keys[i]].options.onDelete) {
if (dao.daoFactory.associations[keys[i]].options.onDelete === "restrict") {
restrict = true
}
else if (dao.daoFactory.associations[keys[i]].options.onDelete === "cascade" && dao.daoFactory.associations[keys[i]].options.useHooks === true) {
cascades[cascades.length] = dao.daoFactory.associations[keys[i]].accessors.get
}
}
}
}
return new Utils.CustomEventEmitter(function(emitter) {
var chainer = new Utils.QueryChainer()
var tick = 0
var iterate = function(err, i) {
if (!!err || i >= cascades.length) {
return run(err)
}
chainer.add(self, 'enableForeignKeyConstraints', [])
chainer.add(self, 'queryAndEmit', [[sql, dao], 'delete'])
dao[cascades[i]]().success(function(tasks) {
if (tasks === null || tasks.length < 1) {
return run()
}
chainer.runSerially()
.success(function(results){
emitter.query = { sql: sql }
emitter.emit('sql', sql)
emitter.emit('success', results[1])
})
.error(function(err) {
emitter.query = { sql: sql }
emitter.emit('sql', sql)
emitter.emit('error', err)
})
tasks = Array.isArray(tasks) ? tasks : [tasks]
var ii = 0
var next = function(err, ii) {
if (!!err || ii >= tasks.length) {
return iterate(err)
}
tasks[ii].destroy().error(function(err) {
return iterate(err)
})
.success(function() {
ii++
if (ii >= tasks.length) {
tick++
return iterate(null, tick)
}
next(null, ii)
})
}
next(null, ii)
})
}
var run = function(err) {
if (!!err) {
return emitter.emit('error', err)
}
var chainer = new Utils.QueryChainer()
chainer.add(self, 'enableForeignKeyConstraints', [])
chainer.add(self, 'queryAndEmit', [[sql, dao], 'delete'])
chainer.runSerially()
.success(function(results){
emitter.query = { sql: sql }
emitter.emit('sql', sql)
emitter.emit('success', results[1])
})
.error(function(err) {
emitter.query = { sql: sql }
emitter.emit('sql', sql)
emitter.emit('error', err)
})
}
if (cascades.length > 0) {
iterate(null, tick)
} else {
run()
}
}).run()
}
......
......@@ -161,17 +161,8 @@ module.exports = (function() {
Sequelize.prototype.define = function(daoName, attributes, options) {
options = options || {}
var globalOptions = this.options
// If you don't specify a valid data type lets help you debug it
Utils._.each(attributes, function(dataType, name){
if (Utils.isHash(dataType)) {
dataType = dataType.type
}
if (dataType === undefined) {
throw new Error('Unrecognized data type for field '+ name)
}
})
var self = this
, globalOptions = this.options
if (globalOptions.define) {
options = Utils._.extend({}, globalOptions.define, options)
......@@ -186,6 +177,40 @@ module.exports = (function() {
options.omitNull = globalOptions.omitNull
options.language = globalOptions.language
// If you don't specify a valid data type lets help you debug it
Utils._.each(attributes, function(dataType, name) {
if (Utils.isHash(dataType)) {
dataType = dataType.type
}
if (dataType === undefined) {
throw new Error('Unrecognized data type for field '+ name)
}
if (dataType.toString() === "ENUM") {
attributes[name].validate = attributes[name].validate || {
_checkEnum: function(value) {
var hasValue = value !== undefined
, isMySQL = self.options.dialect === "mysql"
, ciCollation = !!options.collate && options.collate.match(/_ci$/i) !== null
, valueOutOfScope
if (isMySQL && ciCollation && hasValue) {
var scopeIndex = (attributes[name].values || []).map(function(d) { return d.toLowerCase() }).indexOf(value.toLowerCase())
valueOutOfScope = scopeIndex === -1
} else {
valueOutOfScope = ((attributes[name].values || []).indexOf(value) === -1)
}
if (hasValue && valueOutOfScope && !(attributes[name].allowNull === true && values[attrName] === null)) {
throw new Error('Value "' + value + '" for ENUM ' + name + ' is out of allowed scope. Allowed values: ' + attributes[name].values.join(', '))
}
}
}
}
})
// if you call "define" multiple times for the same daoName, do not clutter the factory
if(this.isDefined(daoName)) {
this.daoFactoryManager.removeDAO(this.daoFactoryManager.getDAO(daoName))
......
module.exports = {
username: "root",
password: null,
database: 'sequelize_test',
host: '127.0.0.1',
pool: { maxConnections: 5, maxIdleTime: 30000},
username: process.env.SEQ_USER || "root",
password: process.env.SEQ_PW || null,
database: process.env.SEQ_DB || 'sequelize_test',
host: process.env.SEQ_HOST || '127.0.0.1',
pool: {
maxConnections: process.env.SEQ_POOL_MAX || 5,
maxIdleTime: process.env.SEQ_POOL_IDLE || 30000
},
rand: function() {
return parseInt(Math.random() * 999, 10)
......@@ -11,21 +14,29 @@ module.exports = {
//make maxIdleTime small so that tests exit promptly
mysql: {
username: "root",
password: null,
database: 'sequelize_test',
host: '127.0.0.1',
port: 3306,
pool: { maxConnections: 5, maxIdleTime: 30}
database: process.env.SEQ_MYSQL_DB || process.env.SEQ_DB || 'sequelize_test',
username: process.env.SEQ_MYSQL_USER || process.env.SEQ_USER || "root",
password: process.env.SEQ_MYSQL_PW || process.env.SEQ_PW || null,
host: process.env.SEQ_MYSQL_HOST || process.env.SEQ_HOST || '127.0.0.1',
port: process.env.SEQ_MYSQL_PORT || process.env.SEQ_PORT || 3306,
pool: {
maxConnections: process.env.SEQ_MYSQL_POOL_MAX || process.env.SEQ_POOL_MAX || 5,
maxIdleTime: process.env.SEQ_MYSQL_POOL_IDLE || process.env.SEQ_POOL_IDLE || 30
}
},
sqlite: {
},
postgres: {
database: 'sequelize_test',
username: "postgres",
port: 5432,
pool: { maxConnections: 5, maxIdleTime: 3000}
database: process.env.SEQ_PG_DB || process.env.SEQ_DB || 'sequelize_test',
username: process.env.SEQ_PG_USER || process.env.SEQ_USER || "postgres",
password: process.env.SEQ_PG_PW || process.env.SEQ_PW || null,
host: process.env.SEQ_PG_HOST || process.env.SEQ_HOST || '127.0.0.1',
port: process.env.SEQ_PG_PORT || process.env.SEQ_PORT || 5432,
pool: {
maxConnections: process.env.SEQ_PG_POOL_MAX || process.env.SEQ_POOL_MAX || 5,
maxIdleTime: process.env.SEQ_PG_POOL_IDLE || process.env.SEQ_POOL_IDLE || 3000
}
}
}
This diff could not be displayed because it is too large.
......@@ -122,15 +122,14 @@ if (dialect.match(/^mysql/)) {
})
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() {
User.create({mood: 'happy'}).error(function(err) {
expect(err).to.deep.equal({ mood: [ 'Value "happy" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver' ] })
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()
u.save().error(function(err) {
expect(err).to.deep.equal({ mood: [ 'Value "SAD" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver' ] })
done()
})
})
})
})
})
......
......@@ -435,11 +435,10 @@ describe(Support.getTestDialectTeaser("Sequelize"), function () {
})
it("doesn't save an instance if value is not in the range of enums", function(done) {
var self = this
expect(function() {
self.Review.create({ status: 'fnord' })
}).to.throw(Error, 'Value "fnord" for ENUM status is out of allowed scope. Allowed values: scheduled, active, finished')
done()
this.Review.create({status: 'fnord'}).error(function(err) {
expect(err).to.deep.equal({ status: [ 'Value "fnord" for ENUM status is out of allowed scope. Allowed values: scheduled, active, finished' ] })
done()
})
})
})
})
......
var fs = require('fs')
, Sequelize = require(__dirname + "/../index")
, DataTypes = require(__dirname + "/../lib/data-types")
, config = require(__dirname + "/config/config")
, Config = require(__dirname + "/config/config")
var Support = {
Sequelize: Sequelize,
......@@ -26,15 +26,17 @@ var Support = {
createSequelizeInstance: function(options) {
options = options || {}
options.dialect = options.dialect || 'mysql'
var config = Config[options.dialect]
options.logging = (options.hasOwnProperty('logging') ? options.logging : false)
options.pool = options.pool || config.pool
var sequelizeOptions = {
logging: options.logging,
dialect: options.dialect,
port: options.port || process.env.SEQ_PORT || config[options.dialect].port,
port: options.port || process.env.SEQ_PORT || config.port,
pool: options.pool,
dialectOptions: options.dialectOptions || {}
}
......@@ -51,12 +53,7 @@ var Support = {
sequelizeOptions.native = true
}
return this.getSequelizeInstance(
process.env.SEQ_DB || config[options.dialect].database,
process.env.SEQ_USER || process.env.SEQ_USERNAME || config[options.dialect].username,
process.env.SEQ_PW || process.env.SEQ_PASSWORD || config[options.dialect].password,
sequelizeOptions
)
return this.getSequelizeInstance(config.database, config.username, config.password, sequelizeOptions)
},
getSequelizeInstance: function(db, user, pass, options) {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!