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

Commit b3f038b4 by Daniel Durante

Merge branch 'master' into milestones/2.0.0

Conflicts:
	lib/dao-factory.js
	lib/dao-validator.js
	lib/dao.js
	lib/emitters/custom-event-emitter.js
2 parents 43c0b0c1 77377566
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
"waitsFor": false, "waitsFor": false,
"runs": false "runs": false
}, },
"node": true,
"camelcase": true, "camelcase": true,
"curly": true, "curly": true,
"forin": true, "forin": true,
...@@ -18,5 +19,7 @@ ...@@ -18,5 +19,7 @@
"asi": true, "asi": true,
"evil": false, "evil": false,
"laxcomma": true, "laxcomma": true,
"es5": true "es5": true,
"quotmark": false,
"strict": false
} }
\ No newline at end of file
...@@ -12,11 +12,12 @@ teaser: ...@@ -12,11 +12,12 @@ teaser:
echo '' echo ''
test: test:
@make teaser && \ @if [ "$$GREP" ]; then \
./node_modules/mocha/bin/mocha \ make teaser && ./node_modules/mocha/bin/mocha --colors --reporter $(REPORTER) -g "$$GREP" $(TESTS); \
--colors \ else \
--reporter $(REPORTER) \ make teaser && ./node_modules/mocha/bin/mocha --colors --reporter $(REPORTER) $(TESTS); \
$(TESTS) fi
sqlite: sqlite:
@DIALECT=sqlite make test @DIALECT=sqlite make test
......
...@@ -33,6 +33,7 @@ changelog of the branch: https://github.com/sequelize/sequelize/blob/milestones/ ...@@ -33,6 +33,7 @@ changelog of the branch: https://github.com/sequelize/sequelize/blob/milestones/
- Associations - Associations
- Importing definitions from single files - Importing definitions from single files
- Promises - Promises
- Hooks/callbacks/lifecycle events
## Documentation and Updates ## ## Documentation and Updates ##
...@@ -62,10 +63,10 @@ A very basic roadmap. Chances aren't too bad, that not mentioned things are impl ...@@ -62,10 +63,10 @@ A very basic roadmap. Chances aren't too bad, that not mentioned things are impl
- Support for update of tables without primary key - Support for update of tables without primary key
- MariaDB support - 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 - ~~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)) - ~~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 - ~~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 - ~~Move validation of enum attribute value to validate method~~ Implemented in [#894](https://github.com/sequelize/sequelize/pull/894) thanks to @durango
- ~~BLOB~~ [#842](https://github.com/sequelize/sequelize/pull/842), thanks to @janmeier - ~~BLOB~~ [#842](https://github.com/sequelize/sequelize/pull/842), thanks to @janmeier
- ~~Support for foreign keys~~ Implemented in [#595](https://github.com/sequelize/sequelize/pull/595), thanks to @optilude - ~~Support for foreign keys~~ Implemented in [#595](https://github.com/sequelize/sequelize/pull/595), thanks to @optilude
......
...@@ -44,6 +44,7 @@ ...@@ -44,6 +44,7 @@
- [BUG] Fix for counts queries with no result [#906](https://github.com/sequelize/sequelize/pull/906). thanks to iamjochem - [BUG] Fix for counts queries with no result [#906](https://github.com/sequelize/sequelize/pull/906). thanks to iamjochem
- [BUG] Allow include when the same table is referenced multiple times using hasMany [#913](https://github.com/sequelize/sequelize/pull/913). thanks to janmeier - [BUG] Allow include when the same table is referenced multiple times using hasMany [#913](https://github.com/sequelize/sequelize/pull/913). thanks to janmeier
- [BUG] Allow definition of defaultValue for the timestamp columns (createdAt, updatedAt, deletedAt) [#930](https://github.com/sequelize/sequelize/pull/930). Thank to durango - [BUG] Allow definition of defaultValue for the timestamp columns (createdAt, updatedAt, deletedAt) [#930](https://github.com/sequelize/sequelize/pull/930). Thank to durango
- [BUG] Don't delete foreign keys of many-to-many associations, if still needed. [#961](https://github.com/sequelize/sequelize/pull/961). thanks to sdepold
- [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
...@@ -80,6 +81,8 @@ ...@@ -80,6 +81,8 @@
- [FEATURE] Uses sequelize.fn and sequelize.col functionality to allow you to use the value of another column or a function when updating. It also allows you to use a function as a default value when supported (in sqlite and postgres). [#928](https://github.com/sequelize/sequelize/pull/928). thanks to janmeier - [FEATURE] Uses sequelize.fn and sequelize.col functionality to allow you to use the value of another column or a function when updating. It also allows you to use a function as a default value when supported (in sqlite and postgres). [#928](https://github.com/sequelize/sequelize/pull/928). thanks to janmeier
- [FEATURE] Added possibility to pass options to node-mysql. [#929](https://github.com/sequelize/sequelize/pull/929). thanks to poying - [FEATURE] Added possibility to pass options to node-mysql. [#929](https://github.com/sequelize/sequelize/pull/929). thanks to poying
- [FEATURE] Triggers for Postgres. [#915](https://github.com/sequelize/sequelize/pull/915). Thanks to jonathana. - [FEATURE] Triggers for Postgres. [#915](https://github.com/sequelize/sequelize/pull/915). Thanks to jonathana.
- [FEATURE] Support for join tables. [#877](https://github.com/sequelize/sequelize/pull/877). Thanks to janmeier.
- [FEATURE] Support for hooks. [#894](https://github.com/sequelize/sequelize/pull/894). Thanks to durango.
- [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
......
...@@ -14,6 +14,8 @@ module.exports = (function() { ...@@ -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.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 this.associationAccessor = this.isSelfAssociation
? Utils.combineTableNames(this.target.tableName, this.options.as || this.target.tableName) ? Utils.combineTableNames(this.target.tableName, this.options.as || this.target.tableName)
: this.options.as || this.target.tableName : this.options.as || this.target.tableName
...@@ -31,7 +33,7 @@ module.exports = (function() { ...@@ -31,7 +33,7 @@ module.exports = (function() {
Utils._.defaults(this.source.rawAttributes, newAttributes) Utils._.defaults(this.source.rawAttributes, newAttributes)
// Sync attributes to DAO proto each time a new assoc is added // 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 return this
} }
......
...@@ -4,27 +4,61 @@ module.exports = (function() { ...@@ -4,27 +4,61 @@ module.exports = (function() {
var HasManyDoubleLinked = function(definition, instance) { var HasManyDoubleLinked = function(definition, instance) {
this.__factory = definition this.__factory = definition
this.instance = instance this.instance = instance
// Alias the quoting methods for code brevity
this.QueryInterface = instance.QueryInterface
} }
HasManyDoubleLinked.prototype.injectGetter = function(options) { HasManyDoubleLinked.prototype.injectGetter = function(options) {
var self = this, _options = options var self = this, _options = options
var customEventEmitter = new Utils.CustomEventEmitter(function() { var customEventEmitter = new Utils.CustomEventEmitter(function() {
var where = {}, options = _options || {} var where = {}
, connectorDAO = self.__factory.connectorDAO
, options = _options || {}
, queryOptions = {}
, association = self.__factory.target.associations[self.__factory.associationAccessor]
//fully qualify //fully qualify
var instancePrimaryKeys = Object.keys(self.instance.daoFactory.primaryKeys) var instancePrimaryKeys = Object.keys(self.instance.daoFactory.primaryKeys)
, instancePrimaryKey = instancePrimaryKeys.length > 0 ? instancePrimaryKeys[0] : 'id' , instancePrimaryKey = instancePrimaryKeys.length > 0 ? instancePrimaryKeys[0] : 'id'
where[self.__factory.connectorDAO.tableName+"."+self.__factory.identifier] = self.instance[instancePrimaryKey] where[connectorDAO.tableName+"."+self.__factory.identifier] = self.instance[instancePrimaryKey]
var primaryKeys = Object.keys(self.__factory.connectorDAO.rawAttributes) var primaryKeys = Object.keys(connectorDAO.primaryKeys)
, foreignKey = primaryKeys.filter(function(pk) { return pk != self.__factory.identifier })[0] , foreignKey = primaryKeys.filter(function(pk) { return pk != self.__factory.identifier })[0]
, foreignPrimary = Object.keys(self.__factory.target.primaryKeys) , foreignPrimary = Object.keys(self.__factory.target.primaryKeys)
foreignPrimary = foreignPrimary.length === 1 ? foreignPrimary[0] : 'id' foreignPrimary = foreignPrimary.length === 1 ? foreignPrimary[0] : 'id'
where[self.__factory.connectorDAO.tableName+"."+foreignKey] = {join: self.__factory.target.tableName+"."+foreignPrimary} where[connectorDAO.tableName+"."+foreignKey] = {join: self.__factory.target.tableName+"."+foreignPrimary}
if (association.hasJoinTableModel) {
queryOptions.hasJoinTableModel = true
queryOptions.joinTableModel = connectorDAO
if (!options.attributes) {
options.attributes = [
self.QueryInterface.quoteIdentifier(self.__factory.target.tableName)+".*"
]
}
if (options.joinTableAttributes) {
options.joinTableAttributes.forEach(function (elem) {
options.attributes.push(
self.QueryInterface.quoteIdentifiers(connectorDAO.tableName + '.' + elem) + ' as ' +
self.QueryInterface.quoteIdentifier(connectorDAO.name + '.' + elem, true)
)
})
} else {
Utils._.forOwn(connectorDAO.rawAttributes, function (elem, key) {
options.attributes.push(
self.QueryInterface.quoteIdentifiers(connectorDAO.tableName + '.' + key) + ' as ' +
self.QueryInterface.quoteIdentifier(connectorDAO.name + '.' + key, true)
)
})
}
}
if (options.where) { if (options.where) {
if (Array.isArray(options.where)) { if (Array.isArray(options.where)) {
...@@ -44,7 +78,7 @@ module.exports = (function() { ...@@ -44,7 +78,7 @@ module.exports = (function() {
options.where = where; options.where = where;
} }
self.__factory.target.findAllJoin(self.__factory.connectorDAO.tableName, options) self.__factory.target.findAllJoin(connectorDAO.tableName, options, queryOptions)
.on('success', function(objects) { customEventEmitter.emit('success', objects) }) .on('success', function(objects) { customEventEmitter.emit('success', objects) })
.on('error', function(err){ customEventEmitter.emit('error', err) }) .on('error', function(err){ customEventEmitter.emit('error', err) })
.on('sql', function(sql) { customEventEmitter.emit('sql', sql)}) .on('sql', function(sql) { customEventEmitter.emit('sql', sql)})
...@@ -53,26 +87,41 @@ module.exports = (function() { ...@@ -53,26 +87,41 @@ module.exports = (function() {
return customEventEmitter.run() return customEventEmitter.run()
} }
HasManyDoubleLinked.prototype.injectSetter = function(emitterProxy, oldAssociations, newAssociations) { HasManyDoubleLinked.prototype.injectSetter = function(emitterProxy, oldAssociations, newAssociations, defaultAttributes) {
var self = this var self = this
, chainer = new Utils.QueryChainer() , chainer = new Utils.QueryChainer()
, association = self.__factory.target.associations[self.__factory.associationAccessor] , association = self.__factory.target.associations[self.__factory.associationAccessor]
, foreignIdentifier = association.isSelfAssociation ? association.foreignIdentifier : association.identifier , foreignIdentifier = association.isSelfAssociation ? association.foreignIdentifier : association.identifier
, sourceKeys = Object.keys(self.__factory.source.primaryKeys) , sourceKeys = Object.keys(self.__factory.source.primaryKeys)
, targetKeys = Object.keys(self.__factory.target.primaryKeys) , targetKeys = Object.keys(self.__factory.target.primaryKeys)
, obsoleteAssociations = []
, changedAssociations = []
, unassociatedObjects;
var obsoleteAssociations = oldAssociations.filter(function (old) { unassociatedObjects = newAssociations.filter(function (obj) {
// Return only those old associations that are not found in new return !Utils._.find(oldAssociations, function (old) {
return !Utils._.find(newAssociations, function (obj) { return (!!obj[foreignIdentifier] && !!old[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : obj.id === old.id)
return ((targetKeys.length === 1) ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id)
}) })
}) })
var unassociatedObjects = newAssociations.filter(function (obj) { oldAssociations.forEach(function (old) {
// Return only those associations that are new var newObj = Utils._.find(newAssociations, function (obj) {
return !Utils._.find(oldAssociations, function (old) { return (!!obj[foreignIdentifier] && !!old[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : obj.id === old.id)
return ((targetKeys.length === 1) ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id)
}) })
if (!newObj) {
obsoleteAssociations.push(old)
} else if (association.hasJoinTableModel) {
var changedAssociation = {
where: {},
attributes: Utils._.defaults({}, newObj[self.__factory.connectorDAO.name], defaultAttributes)
}
changedAssociation.where[self.__factory.identifier] = self.instance[self.__factory.identifier] || self.instance.id
changedAssociation.where[foreignIdentifier] = newObj[foreignIdentifier] || newObj.id
changedAssociations.push(changedAssociation)
}
}) })
if (obsoleteAssociations.length > 0) { if (obsoleteAssociations.length > 0) {
...@@ -93,12 +142,22 @@ module.exports = (function() { ...@@ -93,12 +142,22 @@ module.exports = (function() {
attributes[self.__factory.identifier] = ((sourceKeys.length === 1) ? self.instance[sourceKeys[0]] : self.instance.id) attributes[self.__factory.identifier] = ((sourceKeys.length === 1) ? self.instance[sourceKeys[0]] : self.instance.id)
attributes[foreignIdentifier] = ((targetKeys.length === 1) ? unassociatedObject[targetKeys[0]] : unassociatedObject.id) attributes[foreignIdentifier] = ((targetKeys.length === 1) ? unassociatedObject[targetKeys[0]] : unassociatedObject.id)
if (association.hasJoinTableModel) {
attributes = Utils._.defaults(attributes, unassociatedObject[association.connectorDAO.name], defaultAttributes)
}
return attributes return attributes
}) })
chainer.add(self.__factory.connectorDAO.bulkCreate(bulk)) chainer.add(self.__factory.connectorDAO.bulkCreate(bulk))
} }
if (changedAssociations.length > 0) {
changedAssociations.forEach(function (assoc) {
chainer.add(self.__factory.connectorDAO.update(assoc.attributes, assoc.where))
})
}
chainer chainer
.run() .run()
.success(function() { emitterProxy.emit('success', newAssociations) }) .success(function() { emitterProxy.emit('success', newAssociations) })
...@@ -106,7 +165,7 @@ module.exports = (function() { ...@@ -106,7 +165,7 @@ module.exports = (function() {
.on('sql', function(sql) { emitterProxy.emit('sql', sql) }) .on('sql', function(sql) { emitterProxy.emit('sql', sql) })
} }
HasManyDoubleLinked.prototype.injectAdder = function(emitterProxy, newAssociation) { HasManyDoubleLinked.prototype.injectAdder = function(emitterProxy, newAssociation, additionalAttributes, exists) {
var attributes = {} var attributes = {}
, association = this.__factory.target.associations[this.__factory.associationAccessor] , association = this.__factory.target.associations[this.__factory.associationAccessor]
, foreignIdentifier = association.isSelfAssociation ? association.foreignIdentifier : association.identifier; , foreignIdentifier = association.isSelfAssociation ? association.foreignIdentifier : association.identifier;
...@@ -117,10 +176,21 @@ module.exports = (function() { ...@@ -117,10 +176,21 @@ module.exports = (function() {
attributes[this.__factory.identifier] = ((sourceKeys.length === 1) ? this.instance[sourceKeys[0]] : this.instance.id) attributes[this.__factory.identifier] = ((sourceKeys.length === 1) ? this.instance[sourceKeys[0]] : this.instance.id)
attributes[foreignIdentifier] = ((targetKeys.length === 1) ? newAssociation[targetKeys[0]] : newAssociation.id) attributes[foreignIdentifier] = ((targetKeys.length === 1) ? newAssociation[targetKeys[0]] : newAssociation.id)
this.__factory.connectorDAO.create(attributes) if (exists) { // implies hasJoinTableModel === true
.success(function() { emitterProxy.emit('success', newAssociation) }) var where = attributes
.error(function(err) { emitterProxy.emit('error', err) }) attributes = Utils._.defaults({}, newAssociation[association.connectorDAO.name], additionalAttributes)
.on('sql', function(sql) { emitterProxy.emit('sql', sql) })
association.connectorDAO.update(attributes, where).proxy(emitterProxy)
} else {
if (association.hasJoinTableModel === true) {
attributes = Utils._.defaults(attributes, newAssociation[association.connectorDAO.name], additionalAttributes)
}
this.__factory.connectorDAO.create(attributes)
.success(function() { emitterProxy.emit('success', newAssociation) })
.error(function(err) { emitterProxy.emit('error', err) })
.on('sql', function(sql) { emitterProxy.emit('sql', sql) })
}
} }
return HasManyDoubleLinked return HasManyDoubleLinked
......
...@@ -30,7 +30,7 @@ module.exports = (function() { ...@@ -30,7 +30,7 @@ module.exports = (function() {
HasManySingleLinked.prototype.injectSetter = function(emitter, oldAssociations, newAssociations) { HasManySingleLinked.prototype.injectSetter = function(emitter, oldAssociations, newAssociations) {
var self = this 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' , associationKey = associationKeys.length === 1 ? associationKeys[0] : 'id'
, chainer = new Utils.QueryChainer() , chainer = new Utils.QueryChainer()
, obsoleteAssociations = oldAssociations.filter(function (old) { , obsoleteAssociations = oldAssociations.filter(function (old) {
......
...@@ -13,12 +13,22 @@ module.exports = (function() { ...@@ -13,12 +13,22 @@ module.exports = (function() {
this.options = options this.options = options
this.useJunctionTable = this.options.useJunctionTable === undefined ? true : this.options.useJunctionTable this.useJunctionTable = this.options.useJunctionTable === undefined ? true : this.options.useJunctionTable
this.isSelfAssociation = (this.source.tableName === this.target.tableName) this.isSelfAssociation = (this.source.tableName === this.target.tableName)
this.hasJoinTableModel = !!this.options.joinTableModel
var combinedTableName = Utils.combineTableNames( var combinedTableName;
this.source.tableName, if (this.hasJoinTableModel) {
this.isSelfAssociation ? (this.options.as || this.target.tableName) : this.target.tableName combinedTableName = this.options.joinTableModel.tableName
) } else if (this.options.joinTableName) {
this.options.tableName = this.combinedName = (this.options.joinTableName || combinedTableName) combinedTableName = this.options.joinTableName
} else {
combinedTableName = Utils.combineTableNames(
this.source.tableName,
this.isSelfAssociation ? (this.options.as || this.target.tableName) : this.target.tableName
)
}
this.options.tableName = this.combinedName = (this.options.joinTableName || combinedTableName)
this.options.useHooks = options.useHooks
this.associationAccessor = this.options.as || this.combinedName this.associationAccessor = this.options.as || this.combinedName
var as = (this.options.as || Utils.pluralize(this.target.tableName, this.target.options.language)) var as = (this.options.as || Utils.pluralize(this.target.tableName, this.target.options.language))
...@@ -37,6 +47,8 @@ module.exports = (function() { ...@@ -37,6 +47,8 @@ module.exports = (function() {
// or in an extra table which connects two tables // or in an extra table which connects two tables
HasMany.prototype.injectAttributes = function() { HasMany.prototype.injectAttributes = function() {
var multiAssociation = this.target.associations.hasOwnProperty(this.associationAccessor) var multiAssociation = this.target.associations.hasOwnProperty(this.associationAccessor)
, self = this
this.identifier = this.options.foreignKey || Utils._.underscoredIf(Utils.singularize(this.source.tableName, this.source.options.language) + "Id", this.options.underscored) this.identifier = this.options.foreignKey || Utils._.underscoredIf(Utils.singularize(this.source.tableName, this.source.options.language) + "Id", this.options.underscored)
// is there already a single sided association between the source and the target? // is there already a single sided association between the source and the target?
...@@ -48,8 +60,14 @@ module.exports = (function() { ...@@ -48,8 +60,14 @@ module.exports = (function() {
} else { } else {
this.foreignIdentifier = this.target.associations[this.associationAccessor].identifier this.foreignIdentifier = this.target.associations[this.associationAccessor].identifier
this.target.associations[this.associationAccessor].foreignIdentifier = this.identifier this.target.associations[this.associationAccessor].foreignIdentifier = this.identifier
delete this.source.rawAttributes[this.foreignIdentifier]
delete this.target.associations[this.associationAccessor].source.rawAttributes[this.identifier] if (isForeignKeyDeletionAllowedFor.call(this, this.source, this.foreignIdentifier)) {
delete this.source.rawAttributes[this.foreignIdentifier]
}
if (isForeignKeyDeletionAllowedFor.call(this, this.target, this.identifier)) {
delete this.target.associations[this.associationAccessor].source.rawAttributes[this.identifier]
}
} }
// define a new model, which connects the models // define a new model, which connects the models
...@@ -61,7 +79,21 @@ module.exports = (function() { ...@@ -61,7 +79,21 @@ module.exports = (function() {
combinedTableAttributes[this.identifier] = {type: sourceKeyType, primaryKey: true} combinedTableAttributes[this.identifier] = {type: sourceKeyType, primaryKey: true}
combinedTableAttributes[this.foreignIdentifier] = {type: targetKeyType, primaryKey: true} combinedTableAttributes[this.foreignIdentifier] = {type: targetKeyType, primaryKey: true}
this.connectorDAO = this.source.daoFactoryManager.sequelize.define(this.combinedName, combinedTableAttributes, this.options) if (this.hasJoinTableModel === true) {
this.connectorDAO = this.options.joinTableModel
// remove any previously defined PKs
Utils._.each(this.connectorDAO.attributes, function(dataTypeString, attributeName) {
if (dataTypeString.toString().indexOf('PRIMARY KEY') !== -1) {
delete self.connectorDAO.rawAttributes[attributeName]
}
})
this.connectorDAO.rawAttributes = Utils._.merge(this.connectorDAO.rawAttributes, combinedTableAttributes)
this.connectorDAO.init(this.connectorDAO.daoFactoryManager)
} else {
this.connectorDAO = this.source.daoFactoryManager.sequelize.define(this.combinedName, combinedTableAttributes, this.options)
}
if (!this.isSelfAssociation) { if (!this.isSelfAssociation) {
this.target.associations[this.associationAccessor].connectorDAO = this.connectorDAO this.target.associations[this.associationAccessor].connectorDAO = this.connectorDAO
...@@ -92,7 +124,7 @@ module.exports = (function() { ...@@ -92,7 +124,7 @@ module.exports = (function() {
return new Class(self, this).injectGetter(options) return new Class(self, this).injectGetter(options)
} }
obj[this.accessors.hasAll] = function(objects) { obj[this.accessors.hasAll] = function(objects) {
var instance = this; var instance = this;
var customEventEmitter = new Utils.CustomEventEmitter(function() { var customEventEmitter = new Utils.CustomEventEmitter(function() {
instance[self.accessors.get]() instance[self.accessors.get]()
...@@ -135,7 +167,7 @@ module.exports = (function() { ...@@ -135,7 +167,7 @@ module.exports = (function() {
HasMany.prototype.injectSetter = function(obj) { HasMany.prototype.injectSetter = function(obj) {
var self = this var self = this
obj[this.accessors.set] = function(newAssociatedObjects) { obj[this.accessors.set] = function(newAssociatedObjects, defaultAttributes) {
if (newAssociatedObjects === null) { if (newAssociatedObjects === null) {
newAssociatedObjects = [] newAssociatedObjects = []
} }
...@@ -147,7 +179,7 @@ module.exports = (function() { ...@@ -147,7 +179,7 @@ module.exports = (function() {
instance[self.accessors.get]() instance[self.accessors.get]()
.success(function(oldAssociatedObjects) { .success(function(oldAssociatedObjects) {
var Class = self.connectorDAO ? HasManyMultiLinked : HasManySingleLinked var Class = self.connectorDAO ? HasManyMultiLinked : HasManySingleLinked
new Class(self, instance).injectSetter(emitter, oldAssociatedObjects, newAssociatedObjects) new Class(self, instance).injectSetter(emitter, oldAssociatedObjects, newAssociatedObjects, defaultAttributes)
}) })
.error(function(err) { .error(function(err) {
emitter.emit('error', err) emitter.emit('error', err)
...@@ -158,20 +190,21 @@ module.exports = (function() { ...@@ -158,20 +190,21 @@ module.exports = (function() {
}).run() }).run()
} }
obj[this.accessors.add] = function(newAssociatedObject) { obj[this.accessors.add] = function(newAssociatedObject, additionalAttributes) {
var instance = this var instance = this
, primaryKeys = Object.keys(newAssociatedObject.daoFactory.primaryKeys || {}) , primaryKeys = Object.keys(newAssociatedObject.daoFactory.primaryKeys || {})
, primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id' , primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id'
, where = {} , where = {}
where[newAssociatedObject.daoFactory.tableName+'.'+primaryKey] = newAssociatedObject[primaryKey] where[newAssociatedObject.daoFactory.tableName+'.'+primaryKey] = newAssociatedObject[primaryKey]
return new Utils.CustomEventEmitter(function(emitter) { return new Utils.CustomEventEmitter(function(emitter) {
instance[self.accessors.get]({ where: where }) instance[self.accessors.get]({ where: where })
.error(function(err){ emitter.emit('error', err)}) .error(function(err){ emitter.emit('error', err)})
.success(function(currentAssociatedObjects) { .success(function(currentAssociatedObjects) {
if (currentAssociatedObjects.length === 0) { if (currentAssociatedObjects.length === 0 || self.hasJoinTableModel === true) {
var Class = self.connectorDAO ? HasManyMultiLinked : HasManySingleLinked var Class = self.connectorDAO ? HasManyMultiLinked : HasManySingleLinked
new Class(self, instance).injectAdder(emitter, newAssociatedObject) new Class(self, instance).injectAdder(emitter, newAssociatedObject, additionalAttributes, !!currentAssociatedObjects.length)
} else { } else {
emitter.emit('success', newAssociatedObject); emitter.emit('success', newAssociatedObject);
} }
...@@ -184,15 +217,44 @@ module.exports = (function() { ...@@ -184,15 +217,44 @@ module.exports = (function() {
var customEventEmitter = new Utils.CustomEventEmitter(function() { var customEventEmitter = new Utils.CustomEventEmitter(function() {
instance[self.accessors.get]().success(function(currentAssociatedObjects) { instance[self.accessors.get]().success(function(currentAssociatedObjects) {
var newAssociations = [] var newAssociations = []
, oldAssociations = []
currentAssociatedObjects.forEach(function(association) { currentAssociatedObjects.forEach(function(association) {
if (!Utils._.isEqual(oldAssociatedObject.identifiers, association.identifiers)) if (!Utils._.isEqual(oldAssociatedObject.identifiers, association.identifiers)) {
newAssociations.push(association) newAssociations.push(association)
}
}) })
instance[self.accessors.set](newAssociations) var tick = 0
.success(function() { customEventEmitter.emit('success', null) }) var next = function(err, i) {
.error(function(err) { customEventEmitter.emit('error', err) }) 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() return customEventEmitter.run()
...@@ -201,5 +263,27 @@ module.exports = (function() { ...@@ -201,5 +263,27 @@ module.exports = (function() {
return this return this
} }
/**
* The method checks if it is ok to delete the previously defined foreign key.
* This is done because we need to keep the foreign key if another association
* is depending on it.
*
* @param {DaoFactory} daoFactory The source or target DaoFactory of this assocation
* @param {[type]} identifier The name of the foreign key identifier
* @return {Boolean} Whether or not the deletion of the foreign key is ok.
*/
var isForeignKeyDeletionAllowedFor = function(daoFactory, identifier) {
var isAllowed = true
, associationNames = Utils._.without(Object.keys(daoFactory.associations), this.associationAccessor)
associationNames.forEach(function(associationName) {
if (daoFactory.associations[associationName].identifier === identifier) {
isAllowed = false
}
})
return isAllowed
}
return HasMany return HasMany
})() })()
...@@ -18,6 +18,8 @@ module.exports = (function() { ...@@ -18,6 +18,8 @@ module.exports = (function() {
? Utils.combineTableNames(this.target.tableName, this.options.as || this.target.tableName) ? Utils.combineTableNames(this.target.tableName, this.options.as || this.target.tableName)
: this.options.as || this.target.tableName : this.options.as || this.target.tableName
this.options.useHooks = options.useHooks
this.accessors = { this.accessors = {
get: Utils._.camelize('get_' + (this.options.as || Utils.singularize(this.target.tableName, this.target.options.language))), 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))) set: Utils._.camelize('set_' + (this.options.as || Utils.singularize(this.target.tableName, this.target.options.language)))
......
...@@ -13,10 +13,21 @@ module.exports = { ...@@ -13,10 +13,21 @@ module.exports = {
return source.rawAttributes[key].primaryKey return source.rawAttributes[key].primaryKey
}) })
if(primaryKeys.length == 1) { if (primaryKeys.length === 1) {
newAttribute.references = source.tableName, if (!!source.options.schema) {
newAttribute.references = source.daoFactoryManager.sequelize.queryInterface.QueryGenerator.addSchema({
tableName: source.tableName,
options: {
schema: source.options.schema,
schemaDelimiter: source.options.schemaDelimiter
}
})
} else {
newAttribute.references = source.tableName
}
newAttribute.referencesKey = primaryKeys[0] newAttribute.referencesKey = primaryKeys[0]
newAttribute.onDelete = options.onDelete, newAttribute.onDelete = options.onDelete
newAttribute.onUpdate = options.onUpdate newAttribute.onUpdate = options.onUpdate
} }
} }
......
...@@ -7,6 +7,11 @@ var Utils = require("./../utils") ...@@ -7,6 +7,11 @@ var Utils = require("./../utils")
var Mixin = module.exports = function(){} var Mixin = module.exports = function(){}
Mixin.hasOne = function(associatedDAO, options) { 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 // the id is in the foreign table
var association = new HasOne(this, associatedDAO, Utils._.extend((options||{}), this.options)) var association = new HasOne(this, associatedDAO, Utils._.extend((options||{}), this.options))
this.associations[association.associationAccessor] = association.injectAttributes() this.associations[association.associationAccessor] = association.injectAttributes()
...@@ -18,8 +23,13 @@ Mixin.hasOne = function(associatedDAO, options) { ...@@ -18,8 +23,13 @@ Mixin.hasOne = function(associatedDAO, options) {
} }
Mixin.belongsTo = 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 // 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() this.associations[association.associationAccessor] = association.injectAttributes()
association.injectGetter(this.DAO.prototype) association.injectGetter(this.DAO.prototype)
...@@ -29,6 +39,11 @@ Mixin.belongsTo = function(associatedDAO, options) { ...@@ -29,6 +39,11 @@ Mixin.belongsTo = function(associatedDAO, options) {
} }
Mixin.hasMany = 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 // the id is in the foreign table or in a connecting table
var association = new HasMany(this, associatedDAO, Utils._.extend((options||{}), this.options)) var association = new HasMany(this, associatedDAO, Utils._.extend((options||{}), this.options))
this.associations[association.associationAccessor] = association.injectAttributes() this.associations[association.associationAccessor] = association.injectAttributes()
......
...@@ -25,7 +25,11 @@ module.exports = (function() { ...@@ -25,7 +25,11 @@ module.exports = (function() {
schemaDelimiter: '', schemaDelimiter: '',
language: 'en', language: 'en',
defaultScope: null, defaultScope: null,
scopes: null scopes: null,
hooks: {
beforeCreate: [],
afterCreate: []
}
}, options || {}) }, options || {})
// error check options // error check options
...@@ -348,14 +352,16 @@ module.exports = (function() { ...@@ -348,14 +352,16 @@ module.exports = (function() {
} }
//right now, the caller (has-many-double-linked) is in charge of the where clause //right now, the caller (has-many-double-linked) is in charge of the where clause
DAOFactory.prototype.findAllJoin = function(joinTableName, options) { DAOFactory.prototype.findAllJoin = function(joinTableName, options, queryOptions) {
var optcpy = Utils._.clone(options) var optcpy = Utils._.clone(options)
optcpy.attributes = optcpy.attributes || [this.QueryInterface.quoteIdentifier(this.tableName)+".*"] optcpy.attributes = optcpy.attributes || [this.QueryInterface.quoteIdentifier(this.tableName)+".*"]
// whereCollection is used for non-primary key updates // whereCollection is used for non-primary key updates
this.options.whereCollection = optcpy.where || null; this.options.whereCollection = optcpy.where || null;
return this.QueryInterface.select(this, [this.getTableName(), joinTableName], optcpy, { type: 'SELECT' }) return this.QueryInterface.select(this, [this.getTableName(), joinTableName], optcpy, Utils._.defaults({
type: 'SELECT'
}, queryOptions))
} }
/** /**
...@@ -566,43 +572,77 @@ module.exports = (function() { ...@@ -566,43 +572,77 @@ 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, options) { DAOFactory.prototype.bulkCreate = function(records, fields, options) {
options = 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 || [] fields = fields || []
var self = this var self = this
, updatedAtAttr = self.options.underscored ? 'updated_at' : 'updatedAt' , updatedAtAttr = Utils._.underscoredIf(self.options.updatedAt, self.options.underscored)
, createdAtAttr = self.options.underscored ? 'created_at' : 'createdAt' , createdAtAttr = Utils._.underscoredIf(self.options.createdAt, self.options.underscored)
, errors = [] , errors = []
, completed = 0 , daos = records.map(function(v) {
return self.build(v)
})
return new Utils.CustomEventEmitter(function(emitter) { return new Utils.CustomEventEmitter(function(emitter) {
var daos = records.map(function(v) { var done = function() {
return self.build(v) self.runHooks('afterBulkCreate', daos, fields, function(err, newRecords, newFields) {
}) if (!!err) {
return emitter.emit('error', err)
}
var bulkCreateStep = function(i) { daos = newRecords || daos
daos[i].validate({skip: fields}).success(function(err) { fields = newFields || fields
if (!!err) {
errors[errors.length] = {record: records[i], errors: err} emitter.emit('success', daos, fields)
}
++completed
if (completed === records.length) {
bulkCreateFinal()
} else {
bulkCreateStep(completed)
}
}) })
} }
bulkCreateStep(0) var next = function(err) {
if (err !== undefined && err !== null) {
return emitter.emit('error', err)
}
var bulkCreateFinal = function() { if (options.hooks === false) {
if (options.validate === true && errors.length > 0) { return runQuery()
return emitter.emit('error', errors)
} }
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)
}
var runQuery = function() {
// we will re-create from DAOs, which may have set up default attributes // we will re-create from DAOs, which may have set up default attributes
records = [] records = []
...@@ -626,24 +666,59 @@ module.exports = (function() { ...@@ -626,24 +666,59 @@ module.exports = (function() {
records.push(values) records.push(values)
}) })
// Validate enums self.QueryInterface.bulkInsert(self.tableName, records)
records.forEach(function(values) { .on('sql', function(sql) {
for (var attrName in self.rawAttributes) { emitter.emit('sql', sql)
if (self.rawAttributes.hasOwnProperty(attrName)) { })
var definition = self.rawAttributes[attrName] .error(function(err) {
, isEnum = (definition.type && (definition.type.toString() === DataTypes.ENUM.toString())) emitter.emit('error', err)
, hasValue = (typeof values[attrName] !== 'undefined') }).success(function() {
, valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1) done()
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(', '))
}
}
}
}) })
self.QueryInterface.bulkInsert(self.tableName, records).proxy(emitter)
} }
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}).complete(function (err) {
if (!!err) {
errors.push({record: v, errors: err})
}
i++
if (i > daos.length) {
next(errors.length > 0 ? errors : null)
}
iterate(i)
})
}
} else {
var afterDaos = Utils._.after(daos.length, function() {
next(errors.length > 0 ? errors : null)
})
daos.forEach(function(v) {
v.validate({skip: fields}).success(function(err) {
if (!!err) {
errors.push({record: v, errors: err})
}
afterDaos()
})
})
}
} else {
next()
}
})
}).run() }).run()
} }
...@@ -652,19 +727,117 @@ module.exports = (function() { ...@@ -652,19 +727,117 @@ module.exports = (function() {
* *
* @param {Object} where Options to describe the scope of the search. * @param {Object} where Options to describe the scope of the search.
* @param {Object} options Possible options are: * @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 - 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 - 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`. * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`.
*/ */
DAOFactory.prototype.destroy = function(where, options) { DAOFactory.prototype.destroy = function(where, options) {
if (this.options.timestamps && this.options.paranoid) { var self = this
var attr = Utils._.underscoredIf(this.options.deletedAt, this.options.underscored) , query = null
var attrValueHash = {} , args = []
attrValueHash[attr] = Utils.now()
return this.QueryInterface.bulkUpdate(this.tableName, attrValueHash, where) return new Utils.CustomEventEmitter(function(emitter) {
} else { self.runHooks(self.options.hooks.beforeBulkDestroy, where, function(err, newWhere) {
return this.QueryInterface.bulkDelete(this.tableName, where, options) 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()
} }
/** /**
...@@ -675,40 +848,133 @@ module.exports = (function() { ...@@ -675,40 +848,133 @@ module.exports = (function() {
* @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`.
*/ */
DAOFactory.prototype.update = function(attrValueHash, where, options) { DAOFactory.prototype.update = function(attrValueHash, where, options) {
var self = this
, query = null
, tick = 0
options = options || {} 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) { if (self.options.timestamps) {
var attr = Utils._.underscoredIf(this.options.updatedAt, this.options.underscored) var attr = Utils._.underscoredIf(self.options.updatedAt, self.options.underscored)
attrValueHash[attr] = Utils.now() attrValueHash[attr] = Utils.now()
} }
return new Utils.CustomEventEmitter(function(emitter) { 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 (tick >= records.length) {
return runQuery(null, records)
}
next(tick)
})
}
next(tick)
})
} else {
runQuery()
}
})
}
if (options.validate === true) { if (options.validate === true) {
var build = this.build(attrValueHash) var build = self.build(attrValueHash)
, attrKeys = Object.keys(attrValueHash) , attrKeys = Object.keys(attrValueHash)
build build.hookValidate({
.validate({ skip: Object.keys(build.dataValues).filter(function(val) { return attrKeys.indexOf(val) !== -1 })
skip: Object.keys(build.dataValues).filter(function(val) { return attrKeys.indexOf(val) !== -1 }) }).error(function(err) {
}) emitter.emit('error', err)
.success(function(errors) { }).success(function(attributes) {
if (!!errors) { if (!!attributes && !!attributes.dataValues) {
emitter.emit('error', errors) attrValueHash = Utils._.pick.apply(Utils._, [].concat(attributes.dataValues).concat(Object.keys(attrValueHash)))
} else { }
this
.QueryInterface runSave()
.bulkUpdate(this.tableName, attrValueHash, where, options) })
.proxy(emitter)
}
}.bind(this))
} else { } else {
this runSave()
.QueryInterface
.bulkUpdate(this.tableName, attrValueHash, where, options)
.proxy(emitter)
} }
}.bind(this)).run() }).run()
} }
DAOFactory.prototype.describe = function(schema) { DAOFactory.prototype.describe = function(schema) {
...@@ -873,6 +1139,7 @@ module.exports = (function() { ...@@ -873,6 +1139,7 @@ module.exports = (function() {
} }
Utils._.extend(DAOFactory.prototype, require("./associations/mixin")) Utils._.extend(DAOFactory.prototype, require("./associations/mixin"))
Utils._.extend(DAOFactory.prototype, require(__dirname + '/hooks'))
return DAOFactory return DAOFactory
})() })()
...@@ -35,6 +35,36 @@ DaoValidator.prototype.validate = function() { ...@@ -35,6 +35,36 @@ DaoValidator.prototype.validate = function() {
}).run() }).run()
} }
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
self.validate().success(function (error) {
if (!!error) {
return emitter.emit('error', error)
}
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 // private
var validateModel = function() { var validateModel = function() {
...@@ -51,6 +81,7 @@ var validateModel = function() { ...@@ -51,6 +81,7 @@ var validateModel = function() {
emitter.emit('success') emitter.emit('success')
} }
} }
validator.args.unshift(next); validator.args.unshift(next);
validator.fn.apply(null, validator.args) validator.fn.apply(null, validator.args)
}.bind(this)).run()) }.bind(this)).run())
...@@ -90,6 +121,7 @@ var validateAttribute = function(value, field) { ...@@ -90,6 +121,7 @@ var validateAttribute = function(value, field) {
emitter.emit('success') emitter.emit('success')
} }
} }
validator.args.unshift(next); validator.args.unshift(next);
validator.fn.apply(null, validator.args) validator.fn.apply(null, validator.args)
}.bind(this)).run()) }.bind(this)).run())
......
...@@ -122,69 +122,99 @@ module.exports = (function() { ...@@ -122,69 +122,99 @@ module.exports = (function() {
}) })
} }
for (var attrName in this.daoFactory.rawAttributes) { return new Utils.CustomEventEmitter(function(emitter) {
if (this.daoFactory.rawAttributes.hasOwnProperty(attrName)) { self.hookValidate().error(function (err) {
var definition = this.daoFactory.rawAttributes[attrName] return emitter.emit('error', err)
, isEnum = definition.type && (definition.type.toString() === DataTypes.ENUM.toString()) }).success(function() {
, isHstore = !!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type
, hasValue = values[attrName] !== undefined for (var attrName in self.daoFactory.rawAttributes) {
, isMySQL = this.daoFactory.daoFactoryManager.sequelize.options.dialect === "mysql" if (self.daoFactory.rawAttributes.hasOwnProperty(attrName)) {
, ciCollation = !!this.daoFactory.options.collate && this.daoFactory.options.collate.match(/_ci$/i) var definition = self.daoFactory.rawAttributes[attrName]
, valueOutOfScope , isHstore = !!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type
, isEnum = definition.type && (definition.type.toString() === DataTypes.ENUM.toString())
if (isEnum && isMySQL && ciCollation && hasValue) { , isMySQL = self.daoFactory.daoFactoryManager.sequelize.options.dialect === "mysql"
var scopeIndex = (definition.values || []).map(function(d) { return d.toLowerCase() }).indexOf(values[attrName].toLowerCase()) , ciCollation = !!self.daoFactory.options.collate && self.daoFactory.options.collate.match(/_ci$/i)
valueOutOfScope = scopeIndex === -1
// Unfortunately for MySQL CI collation we need to map/lowercase values again
// We'll return what the actual case will be, since a simple SELECT query would do the same... if (isEnum && isMySQL && ciCollation) {
if (!valueOutOfScope) { var scopeIndex = (definition.values || []).map(function(d) { return d.toLowerCase() }).indexOf(values[attrName].toLowerCase())
values[attrName] = definition.values[scopeIndex] 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)) { if (self.__options.timestamps && self.dataValues.hasOwnProperty(updatedAtAttr)) {
throw new Error('Value "' + values[attrName] + '" for ENUM ' + attrName + ' is out of allowed scope. Allowed values: ' + definition.values.join(', ')) 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) { var query = null
if (typeof values[attrName] === "object") { , args = []
values[attrName] = hstore.stringify(values[attrName]) , 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))
}
return new Utils.CustomEventEmitter(function(emitter) { if (self.isNewRecord) {
this.validate().success(function(errors) { self.isDirty = false
if (!!errors) { query = 'insert'
emitter.emit('error', errors) args = [self, self.QueryInterface.QueryGenerator.addSchema(self.__factory), values]
} else if (this.isNewRecord) { hook = 'Create'
this.isDirty = false
this
.QueryInterface
.insert(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory), values)
.proxy(emitter)
} else { } else {
var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : { id: this.id }; var identifier = self.__options.hasPrimaryKeys ? self.primaryKeyValues : { id: self.id }
if (identifier === null && this.__options.whereCollection !== null) { if (identifier === null && self.__options.whereCollection !== null) {
identifier = this.__options.whereCollection; identifier = self.__options.whereCollection;
} }
this.isDirty = false self.isDirty = false
var tableName = this.QueryInterface.QueryGenerator.addSchema(this.__factory) query = 'update'
, query = this.QueryInterface.update(this, tableName, values, identifier, options) args = [self, self.QueryInterface.QueryGenerator.addSchema(self.__factory), values, identifier, options]
hook = 'Update'
query.proxy(emitter)
} }
}.bind(this))
}.bind(this)).run() self.__factory.runHooks('before' + hook, values, function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
// 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()
} }
/* /*
...@@ -229,6 +259,17 @@ module.exports = (function() { ...@@ -229,6 +259,17 @@ module.exports = (function() {
return new DaoValidator(this, options).validate() return new DaoValidator(this, options).validate()
} }
/*
* 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) { DAO.prototype.updateAttributes = function(updates, fields) {
this.setAttributes(updates) this.setAttributes(updates)
return this.save(fields) return this.save(fields)
...@@ -274,14 +315,41 @@ module.exports = (function() { ...@@ -274,14 +315,41 @@ module.exports = (function() {
} }
DAO.prototype.destroy = function() { DAO.prototype.destroy = function() {
if (this.__options.timestamps && this.__options.paranoid) { var self = this
var attr = Utils._.underscoredIf(this.__options.deletedAt, this.__options.underscored) , query = null
this.dataValues[attr] = new Date()
return this.save() return new Utils.CustomEventEmitter(function(emitter) {
} else { self.daoFactory.runHooks(self.daoFactory.options.hooks.beforeDestroy, self.dataValues, function(err) {
var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : { id: this.id }; if (!!err) {
return this.QueryInterface.delete(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory.tableName, this.__factory.options.schema), identifier) 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) { DAO.prototype.increment = function(fields, count) {
......
...@@ -260,6 +260,21 @@ module.exports = (function() { ...@@ -260,6 +260,21 @@ module.exports = (function() {
result = result.map(Dot.transform) result = result.map(Dot.transform)
} else if (this.options.hasJoin === true) { } else if (this.options.hasJoin === true) {
result = transformRowsWithEagerLoadingIntoDaos.call(this, results) result = transformRowsWithEagerLoadingIntoDaos.call(this, results)
} else if (this.options.hasJoinTableModel === true) {
result = results.map(function(result) {
result = Dot.transform(result)
var joinTableData = result[this.options.joinTableModel.name]
, joinTableDAO = this.options.joinTableModel.build(joinTableData, { isNewRecord: false, isDirty: false })
, mainDao
delete result[this.options.joinTableModel.name]
mainDao = this.callee.build(result, { isNewRecord: false, isDirty: false })
mainDao[this.options.joinTableModel.name] = joinTableDAO
return mainDao
}.bind(this))
} else { } else {
result = results.map(function(result) { result = results.map(function(result) {
return this.callee.build(result, { isNewRecord: false, isDirty: false }) return this.callee.build(result, { isNewRecord: false, isDirty: false })
......
...@@ -15,7 +15,7 @@ module.exports = (function() { ...@@ -15,7 +15,7 @@ module.exports = (function() {
var schema = (!!opts.options && !!opts.options.schema ? opts.options.schema : undefined) var schema = (!!opts.options && !!opts.options.schema ? opts.options.schema : undefined)
var schemaDelimiter = (!!opts.options && !!opts.options.schemaDelimiter ? opts.options.schemaDelimiter : undefined) var schemaDelimiter = (!!opts.options && !!opts.options.schemaDelimiter ? opts.options.schemaDelimiter : undefined)
if (!!opts.tableName) { if (!!opts && !!opts.tableName) {
tableName = opts.tableName tableName = opts.tableName
} }
else if (typeof opts === "string") { else if (typeof opts === "string") {
...@@ -26,7 +26,7 @@ module.exports = (function() { ...@@ -26,7 +26,7 @@ module.exports = (function() {
return tableName return tableName
} }
return this.quoteIdentifier(schema) + '.' + this.quoteIdentifier(tableName) return this.quoteIdentifiers((!!schema ? (schema + '.' + tableName) : tableName));
}, },
createSchema: function(schema) { createSchema: function(schema) {
...@@ -665,10 +665,10 @@ module.exports = (function() { ...@@ -665,10 +665,10 @@ module.exports = (function() {
if(dataType.references) { if(dataType.references) {
template += " REFERENCES <%= referencesTable %> (<%= referencesKey %>)" template += " REFERENCES <%= referencesTable %> (<%= referencesKey %>)"
replacements.referencesTable = this.quoteIdentifier(dataType.references) replacements.referencesTable = this.quoteIdentifiers(dataType.references)
if(dataType.referencesKey) { if(dataType.referencesKey) {
replacements.referencesKey = this.quoteIdentifier(dataType.referencesKey) replacements.referencesKey = this.quoteIdentifiers(dataType.referencesKey)
} else { } else {
replacements.referencesKey = this.quoteIdentifier('id') replacements.referencesKey = this.quoteIdentifier('id')
} }
......
...@@ -2,7 +2,7 @@ var util = require("util") ...@@ -2,7 +2,7 @@ var util = require("util")
, EventEmitter = require("events").EventEmitter , EventEmitter = require("events").EventEmitter
, Promise = require("promise") , Promise = require("promise")
, proxyEventKeys = ['success', 'error', 'sql'] , proxyEventKeys = ['success', 'error', 'sql']
, tick = (typeof setImmediate !== "undefined" ? setImmediate : process.nextTick)
var bindToProcess = function(fct) { var bindToProcess = function(fct) {
if (fct) { if (fct) {
...@@ -21,7 +21,7 @@ module.exports = (function() { ...@@ -21,7 +21,7 @@ module.exports = (function() {
util.inherits(CustomEventEmitter, EventEmitter) util.inherits(CustomEventEmitter, EventEmitter)
CustomEventEmitter.prototype.run = function() { CustomEventEmitter.prototype.run = function() {
process.nextTick(function() { tick(function() {
if (this.fct) { if (this.fct) {
this.fct.call(this, this) this.fct.call(this, this)
} }
......
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() { ...@@ -479,9 +479,10 @@ module.exports = (function() {
} }
QueryInterface.prototype.delete = function(dao, tableName, identifier) { QueryInterface.prototype.delete = function(dao, tableName, identifier) {
var self = this var self = this
, restrict = false , restrict = false
, sql = self.QueryGenerator.deleteQuery(tableName, identifier, null, dao.daoFactory) , cascades = []
, sql = self.QueryGenerator.deleteQuery(tableName, identifier, null, dao.daoFactory)
// Check for a restrict field // Check for a restrict field
if (!!dao.daoFactory && !!dao.daoFactory.associations) { if (!!dao.daoFactory && !!dao.daoFactory.associations) {
...@@ -489,29 +490,84 @@ module.exports = (function() { ...@@ -489,29 +490,84 @@ module.exports = (function() {
, length = keys.length , length = keys.length
for (var i = 0; i < length; i++) { 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") { if (dao.daoFactory.associations[keys[i]].options && dao.daoFactory.associations[keys[i]].options.onDelete) {
restrict = true 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) { 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', []) dao[cascades[i]]().success(function(tasks) {
chainer.add(self, 'queryAndEmit', [[sql, dao], 'delete']) if (tasks === null || tasks.length < 1) {
return run()
}
chainer.runSerially() tasks = Array.isArray(tasks) ? tasks : [tasks]
.success(function(results){
emitter.query = { sql: sql } var ii = 0
emitter.emit('sql', sql) var next = function(err, ii) {
emitter.emit('success', results[1]) if (!!err || ii >= tasks.length) {
}) return iterate(err)
.error(function(err) { }
emitter.query = { sql: sql }
emitter.emit('sql', sql) tasks[ii].destroy().error(function(err) {
emitter.emit('error', 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() }).run()
} }
......
...@@ -161,17 +161,8 @@ module.exports = (function() { ...@@ -161,17 +161,8 @@ module.exports = (function() {
Sequelize.prototype.define = function(daoName, attributes, options) { Sequelize.prototype.define = function(daoName, attributes, options) {
options = options || {} options = options || {}
var globalOptions = this.options var self = this
, 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)
}
})
if (globalOptions.define) { if (globalOptions.define) {
options = Utils._.extend({}, globalOptions.define, options) options = Utils._.extend({}, globalOptions.define, options)
...@@ -186,6 +177,41 @@ module.exports = (function() { ...@@ -186,6 +177,41 @@ module.exports = (function() {
options.omitNull = globalOptions.omitNull options.omitNull = globalOptions.omitNull
options.language = globalOptions.language 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, next) {
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)) {
return next('Value "' + value + '" for ENUM ' + name + ' is out of allowed scope. Allowed values: ' + attributes[name].values.join(', '))
}
next()
}
}
}
})
// if you call "define" multiple times for the same daoName, do not clutter the factory // if you call "define" multiple times for the same daoName, do not clutter the factory
if(this.isDefined(daoName)) { if(this.isDefined(daoName)) {
this.daoFactoryManager.removeDAO(this.daoFactoryManager.getDAO(daoName)) this.daoFactoryManager.removeDAO(this.daoFactoryManager.getDAO(daoName))
......
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
"url": "https://github.com/sequelize/sequelize/issues" "url": "https://github.com/sequelize/sequelize/issues"
}, },
"dependencies": { "dependencies": {
"lodash": "~2.1.0", "lodash": "~2.2.0",
"underscore.string": "~2.3.0", "underscore.string": "~2.3.0",
"lingo": "~0.0.5", "lingo": "~0.0.5",
"validator": "~1.5.0", "validator": "~1.5.0",
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
"toposort-class": "~0.2.0", "toposort-class": "~0.2.0",
"generic-pool": "2.0.4", "generic-pool": "2.0.4",
"promise": "~3.2.0", "promise": "~3.2.0",
"sql": "~0.26.0" "sql": "~0.28.0"
}, },
"devDependencies": { "devDependencies": {
"sqlite3": "~2.1.12", "sqlite3": "~2.1.12",
......
...@@ -580,6 +580,201 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { ...@@ -580,6 +580,201 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
done() done()
}) })
}) })
describe('join table model', function () {
beforeEach(function (done) {
this.User = this.sequelize.define('User', {})
this.Project = this.sequelize.define('Project', {})
this.UserProjects = this.sequelize.define('UserProjects', {
status: DataTypes.STRING,
data: DataTypes.INTEGER
})
this.User.hasMany(this.Project, { joinTableModel: this.UserProjects })
this.Project.hasMany(this.User, { joinTableModel: this.UserProjects })
this.sequelize.sync().success(function() { done() })
})
describe('fetching from join table', function () {
it('should contain the data from the join table on .UserProjects a DAO', function (done) {
var self = this
self.User.create().success(function (u) {
self.Project.create().success(function (p) {
u.addProject(p, { status: 'active', data: 42 }).success(function() {
u.getProjects().success(function(projects) {
var project = projects[0]
expect(project.UserProjects).to.be.defined
expect(project.status).not.to.exist
expect(project.UserProjects.status).to.equal('active')
expect(project.UserProjects.data).to.equal(42)
done()
})
})
})
})
})
it('should be able to limit the join table attributes returned', function (done) {
var self = this
self.User.create().success(function (u) {
self.Project.create().success(function (p) {
u.addProject(p, { status: 'active', data: 42 }).success(function() {
u.getProjects({ joinTableAttributes: ['status']}).success(function(projects) {
var project = projects[0]
expect(project.UserProjects).to.be.defined
expect(project.status).not.to.exist
expect(project.UserProjects.status).to.equal('active')
expect(project.UserProjects.data).not.to.exist
done()
})
})
})
})
})
})
describe('inserting in join table', function () {
describe('add', function () {
it('should insert data provided on the object into the join table', function (done) {
var self = this
self.User.create().success(function (u) {
self.Project.create().success(function (p) {
p.UserProjects = {
status: 'active'
}
u.addProject(p).success(function() {
self.UserProjects.find({ where: { UserId: u.id, ProjectId: p.id }}).success(function (up) {
expect(up.status).to.equal('active')
done()
})
})
})
})
})
it('should insert data provided as a second argument into the join table', function (done) {
var self = this
self.User.create().success(function (u) {
self.Project.create().success(function (p) {
u.addProject(p, { status: 'active' }).success(function() {
self.UserProjects.find({ where: { UserId: u.id, ProjectId: p.id }}).success(function (up) {
expect(up.status).to.equal('active')
done()
})
})
})
})
})
})
describe('set', function () {
it('should be able to combine properties on the associated objects, and default values', function (done) {
var self = this
, _done = _.after(2, done)
self.User.create().success(function (u) {
self.Project.bulkCreate([{}, {}]).success(function () {
self.Project.findAll().success(function (projects) {
var p1 = projects[0]
, p2 = projects[1]
p1.UserProjects = {
status: 'inactive'
}
u.setProjects([p1, p2], { status: 'active' }).success(function() {
self.UserProjects.find({ where: { UserId: u.id, ProjectId: p1.id }}).success(function (up) {
expect(up.status).to.equal('inactive')
_done()
})
self.UserProjects.find({ where: { UserId: u.id, ProjectId: p2.id }}).success(function (up) {
expect(up.status).to.equal('active')
_done()
})
})
})
})
})
})
})
})
})
describe('belongsTo and hasMany at once', function() {
beforeEach(function() {
this.A = this.sequelize.define('a', { name: Sequelize.STRING })
this.B = this.sequelize.define('b', { name: Sequelize.STRING })
})
describe('source belongs to target', function() {
beforeEach(function(done) {
this.A.belongsTo(this.B, { as: 'relation1' })
this.A.hasMany(this.B, { as: 'relation2' })
this.B.hasMany(this.A, { as: 'relation2' })
this.sequelize.sync({ force: true }).success(function() {
done()
})
})
it('correctly uses bId in A', function(done) {
var self = this
var a1 = this.A.build({ name: 'a1' })
, b1 = this.B.build({ name: 'b1' })
a1
.save()
.then(function() { return b1.save() })
.then(function() { return a1.setRelation1(b1) })
.then(function() { return self.A.find({ where: { name: 'a1' } }) })
.done(function(a) {
expect(a.bId).to.be.eq(b1.id)
done()
})
})
})
describe('target belongs to source', function() {
beforeEach(function(done) {
this.B.belongsTo(this.A, { as: 'relation1' })
this.A.hasMany(this.B, { as: 'relation2' })
this.B.hasMany(this.A, { as: 'relation2' })
this.sequelize.sync({ force: true }).success(function() {
done()
})
})
it('correctly uses bId in A', function(done) {
var self = this
var a1 = this.A.build({ name: 'a1' })
, b1 = this.B.build({ name: 'b1' })
a1
.save()
.then(function() { return b1.save() })
.then(function() { return b1.setRelation1(a1) })
.then(function() { return self.B.find({ where: { name: 'b1' } }) })
.done(function(b) {
expect(b.aId).to.be.eq(a1.id)
done()
})
})
})
})
}) })
describe("Foreign key constraints", function() { describe("Foreign key constraints", function() {
...@@ -746,4 +941,4 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { ...@@ -746,4 +941,4 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
}) })
}) })
}) })
}) })
\ No newline at end of file
module.exports = { module.exports = {
username: "root", username: process.env.SEQ_USER || "root",
password: null, password: process.env.SEQ_PW || null,
database: 'sequelize_test', database: process.env.SEQ_DB || 'sequelize_test',
host: '127.0.0.1', host: process.env.SEQ_HOST || '127.0.0.1',
pool: { maxConnections: 5, maxIdleTime: 30000}, pool: {
maxConnections: process.env.SEQ_POOL_MAX || 5,
maxIdleTime: process.env.SEQ_POOL_IDLE || 30000
},
rand: function() { rand: function() {
return parseInt(Math.random() * 999, 10) return parseInt(Math.random() * 999, 10)
...@@ -11,21 +14,29 @@ module.exports = { ...@@ -11,21 +14,29 @@ module.exports = {
//make maxIdleTime small so that tests exit promptly //make maxIdleTime small so that tests exit promptly
mysql: { mysql: {
username: "root", database: process.env.SEQ_MYSQL_DB || process.env.SEQ_DB || 'sequelize_test',
password: null, username: process.env.SEQ_MYSQL_USER || process.env.SEQ_USER || "root",
database: 'sequelize_test', password: process.env.SEQ_MYSQL_PW || process.env.SEQ_PW || null,
host: '127.0.0.1', host: process.env.SEQ_MYSQL_HOST || process.env.SEQ_HOST || '127.0.0.1',
port: 3306, port: process.env.SEQ_MYSQL_PORT || process.env.SEQ_PORT || 3306,
pool: { maxConnections: 5, maxIdleTime: 30} 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: { sqlite: {
}, },
postgres: { postgres: {
database: 'sequelize_test', database: process.env.SEQ_PG_DB || process.env.SEQ_DB || 'sequelize_test',
username: "postgres", username: process.env.SEQ_PG_USER || process.env.SEQ_USER || "postgres",
port: 5432, password: process.env.SEQ_PG_PW || process.env.SEQ_PW || null,
pool: { maxConnections: 5, maxIdleTime: 3000} 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
}
} }
} }
...@@ -3491,6 +3491,44 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3491,6 +3491,44 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
}) })
}) })
it('should be able to reference a table with a schema set', function(done) {
var self = this
var sequelize = this.sequelize
var UserPub = this.sequelize.define('UserPub', {
username: Sequelize.STRING
}, { schema: 'prefix' })
var ItemPub = this.sequelize.define('ItemPub', {
name: Sequelize.STRING
}, { schema: 'prefix' })
UserPub.hasMany(ItemPub, {
foreignKeyConstraint: true
})
var run = function() {
UserPub.sync({ force: true }).success(function() {
ItemPub.sync({ force: true }).on('sql', function(sql) {
if (dialect === "postgres") {
expect(sql).to.match(/REFERENCES\s+"prefix"\."UserPubs" \("id"\)/)
} else {
expect(sql).to.match(/REFERENCES\s+`prefix\.UserPubs` \(`id`\)/)
}
done()
})
})
}
if (dialect === "postgres") {
this.sequelize.queryInterface.createSchema('prefix').success(function() {
run.call(self)
})
} else {
run.call(self)
}
})
it("should be able to create and update records under any valid schematic", function(done){ it("should be able to create and update records under any valid schematic", function(done){
var self = this var self = this
...@@ -3533,17 +3571,24 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3533,17 +3571,24 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
describe('references', function() { describe('references', function() {
this.timeout(3000) this.timeout(3000)
beforeEach(function(done) { beforeEach(function(done) {
this.Author = this.sequelize.define('author', { firstName: Sequelize.STRING }) var self = this
this.Author.sync({ force: true }).success(function() {
done() this.sequelize.getQueryInterface().dropTable('posts', { force: true }).success(function() {
self.sequelize.getQueryInterface().dropTable('authors', { force: true }).success(function() {
self.Author = self.sequelize.define('author', { firstName: Sequelize.STRING })
self.Author.sync({ force: true }).success(function() {
done()
})
})
}) })
}) })
afterEach(function(done) { afterEach(function(done) {
var self = this var self = this
self.sequelize.getQueryInterface().dropTable('posts', { force: true }).success(function() { this.sequelize.getQueryInterface().dropTable('posts', { force: true }).success(function() {
self.sequelize.getQueryInterface().dropTable('authors', { force: true }).success(function() { self.sequelize.getQueryInterface().dropTable('authors', { force: true }).success(function() {
done() done()
}) })
...@@ -3567,11 +3612,9 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3567,11 +3612,9 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
Post.sync().on('sql', function(sql) { Post.sync().on('sql', function(sql) {
if (dialect === 'postgres') { if (dialect === 'postgres') {
expect(sql).to.match(/"authorId" INTEGER REFERENCES "authors" \("id"\)/) expect(sql).to.match(/"authorId" INTEGER REFERENCES "authors" \("id"\)/)
} } else if (dialect === 'mysql') {
else if (dialect === 'mysql') {
expect(sql).to.match(/FOREIGN KEY \(`authorId`\) REFERENCES `authors` \(`id`\)/) expect(sql).to.match(/FOREIGN KEY \(`authorId`\) REFERENCES `authors` \(`id`\)/)
} } else if (dialect === 'sqlite') {
else if (dialect === 'sqlite') {
expect(sql).to.match(/`authorId` INTEGER REFERENCES `authors` \(`id`\)/) expect(sql).to.match(/`authorId` INTEGER REFERENCES `authors` \(`id`\)/)
} else { } else {
throw new Error('Undefined dialect!') throw new Error('Undefined dialect!')
...@@ -3598,11 +3641,9 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3598,11 +3641,9 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
Post.sync().on('sql', function(sql) { Post.sync().on('sql', function(sql) {
if (dialect === 'postgres') { if (dialect === 'postgres') {
expect(sql).to.match(/"authorId" INTEGER REFERENCES "authors" \("id"\)/) expect(sql).to.match(/"authorId" INTEGER REFERENCES "authors" \("id"\)/)
} } else if (dialect === 'mysql') {
else if (dialect === 'mysql') {
expect(sql).to.match(/FOREIGN KEY \(`authorId`\) REFERENCES `authors` \(`id`\)/) expect(sql).to.match(/FOREIGN KEY \(`authorId`\) REFERENCES `authors` \(`id`\)/)
} } else if (dialect === 'sqlite') {
else if (dialect === 'sqlite') {
expect(sql).to.match(/`authorId` INTEGER REFERENCES `authors` \(`id`\)/) expect(sql).to.match(/`authorId` INTEGER REFERENCES `authors` \(`id`\)/)
} else { } else {
throw new Error('Undefined dialect!') throw new Error('Undefined dialect!')
...@@ -3639,12 +3680,10 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3639,12 +3680,10 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
}).error(function(err) { }).error(function(err) {
if (dialect === 'mysql') { if (dialect === 'mysql') {
expect(err.message).to.match(/ER_CANNOT_ADD_FOREIGN|ER_CANT_CREATE_TABLE/) expect(err.message).to.match(/ER_CANNOT_ADD_FOREIGN|ER_CANT_CREATE_TABLE/)
} } else if (dialect === 'sqlite') {
else if (dialect === 'sqlite') {
// the parser should not end up here ... see above // the parser should not end up here ... see above
expect(1).to.equal(2) expect(1).to.equal(2)
} } else if (dialect === 'postgres') {
else if (dialect === 'postgres') {
expect(err.message).to.match(/relation "4uth0r5" does not exist/) expect(err.message).to.match(/relation "4uth0r5" does not exist/)
} else { } else {
throw new Error('Undefined dialect!') throw new Error('Undefined dialect!')
......
This diff could not be displayed because it is too large.
...@@ -122,15 +122,14 @@ if (dialect.match(/^mysql/)) { ...@@ -122,15 +122,14 @@ if (dialect.match(/^mysql/)) {
}) })
User.sync({ force: true }).success(function() { User.sync({ force: true }).success(function() {
expect(function() { User.create({mood: 'happy'}).error(function(err) {
User.create({mood: 'happy'}) expect(err).to.deep.equal({ mood: [ 'Value "happy" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver' ] })
}).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'}) var u = User.build({mood: 'SAD'})
u.save() u.save().error(function(err) {
}).to.throw(Error, 'Value "SAD" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver') expect(err).to.deep.equal({ mood: [ 'Value "SAD" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver' ] })
done() done()
})
})
}) })
}) })
}) })
......
...@@ -435,11 +435,10 @@ describe(Support.getTestDialectTeaser("Sequelize"), function () { ...@@ -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) { it("doesn't save an instance if value is not in the range of enums", function(done) {
var self = this this.Review.create({status: 'fnord'}).error(function(err) {
expect(function() { expect(err).to.deep.equal({ status: [ 'Value "fnord" for ENUM status is out of allowed scope. Allowed values: scheduled, active, finished' ] })
self.Review.create({ status: 'fnord' }) done()
}).to.throw(Error, 'Value "fnord" for ENUM status is out of allowed scope. Allowed values: scheduled, active, finished') })
done()
}) })
}) })
}) })
......
var fs = require('fs') var fs = require('fs')
, Sequelize = require(__dirname + "/../index") , Sequelize = require(__dirname + "/../index")
, DataTypes = require(__dirname + "/../lib/data-types") , DataTypes = require(__dirname + "/../lib/data-types")
, config = require(__dirname + "/config/config") , Config = require(__dirname + "/config/config")
var Support = { var Support = {
Sequelize: Sequelize, Sequelize: Sequelize,
...@@ -26,15 +26,17 @@ var Support = { ...@@ -26,15 +26,17 @@ var Support = {
createSequelizeInstance: function(options) { createSequelizeInstance: function(options) {
options = options || {} options = options || {}
options.dialect = options.dialect || 'mysql' options.dialect = options.dialect || 'mysql'
var config = Config[options.dialect]
options.logging = (options.hasOwnProperty('logging') ? options.logging : false) options.logging = (options.hasOwnProperty('logging') ? options.logging : false)
options.pool = options.pool || config.pool options.pool = options.pool || config.pool
var sequelizeOptions = { var sequelizeOptions = {
logging: options.logging, logging: options.logging,
dialect: options.dialect, 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, pool: options.pool,
dialectOptions: options.dialectOptions || {} dialectOptions: options.dialectOptions || {}
} }
...@@ -51,12 +53,7 @@ var Support = { ...@@ -51,12 +53,7 @@ var Support = {
sequelizeOptions.native = true sequelizeOptions.native = true
} }
return this.getSequelizeInstance( return this.getSequelizeInstance(config.database, config.username, config.password, sequelizeOptions)
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
)
}, },
getSequelizeInstance: function(db, user, pass, options) { 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!