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

Commit 1e3e71ca by Daniel Durante

Merge branch 'master' into sync-logging

2 parents 3dfa57ad 924085e4
...@@ -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
......
...@@ -39,6 +39,9 @@ ...@@ -39,6 +39,9 @@
- [BUG] Added tests & bugfixes for DAO-Factory.update and array of values in where clause [#880](https://github.com/sequelize/sequelize/pull/880). thanks to domasx2 - [BUG] Added tests & bugfixes for DAO-Factory.update and array of values in where clause [#880](https://github.com/sequelize/sequelize/pull/880). thanks to domasx2
- [BUG] sqlite no longer leaks a global `db` variable [#900](https://github.com/sequelize/sequelize/pull/900). thanks to xming - [BUG] sqlite no longer leaks a global `db` variable [#900](https://github.com/sequelize/sequelize/pull/900). thanks to xming
- [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 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
...@@ -72,8 +75,11 @@ ...@@ -72,8 +75,11 @@
- [FEATURE] Added Sequelize.fn() and Sequelize.col() to properly call columns and functions within Sequelize. [#882](https://github.com/sequelize/sequelize/pull/882). thanks to janmeier - [FEATURE] Added Sequelize.fn() and Sequelize.col() to properly call columns and functions within Sequelize. [#882](https://github.com/sequelize/sequelize/pull/882). thanks to janmeier
- [FEATURE] Sequelize.import supports relative paths. [#901](https://github.com/sequelize/sequelize/pull/901). thanks to accerqueira. - [FEATURE] Sequelize.import supports relative paths. [#901](https://github.com/sequelize/sequelize/pull/901). thanks to accerqueira.
- [FEATURE] Sequelize.import can now handle functions. [#911](https://github.com/sequelize/sequelize/pull/911). Thanks to davidrivera. - [FEATURE] Sequelize.import can now handle functions. [#911](https://github.com/sequelize/sequelize/pull/911). Thanks to davidrivera.
- [BUG] Allow include when the same table is referenced multiple times using hasMany [#913](https://github.com/sequelize/sequelize/pull/913). 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] 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] 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
...@@ -340,6 +344,8 @@ module.exports = (function() { ...@@ -340,6 +344,8 @@ module.exports = (function() {
this.options.whereCollection = options.where || null this.options.whereCollection = options.where || null
} }
options = paranoidClause.call(this, options)
return this.QueryInterface.select(this, this.tableName, options, Utils._.defaults({ return this.QueryInterface.select(this, this.tableName, options, Utils._.defaults({
type: 'SELECT', type: 'SELECT',
hasJoin: hasJoin hasJoin: hasJoin
...@@ -347,14 +353,16 @@ module.exports = (function() { ...@@ -347,14 +353,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))
} }
/** /**
...@@ -431,6 +439,7 @@ module.exports = (function() { ...@@ -431,6 +439,7 @@ module.exports = (function() {
} }
} }
options = paranoidClause.call(this, options)
options.limit = 1 options.limit = 1
return this.QueryInterface.select(this, this.getTableName(), options, Utils._.defaults({ return this.QueryInterface.select(this, this.getTableName(), options, Utils._.defaults({
...@@ -445,6 +454,8 @@ module.exports = (function() { ...@@ -445,6 +454,8 @@ module.exports = (function() {
options.attributes.push(['count(*)', 'count']) options.attributes.push(['count(*)', 'count'])
options.parseInt = true options.parseInt = true
options = paranoidClause.call(this, options)
return this.QueryInterface.rawSelect(this.getTableName(), options, 'count') return this.QueryInterface.rawSelect(this.getTableName(), options, 'count')
} }
...@@ -564,7 +575,9 @@ module.exports = (function() { ...@@ -564,7 +575,9 @@ module.exports = (function() {
*/ */
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
...@@ -572,58 +585,137 @@ module.exports = (function() { ...@@ -572,58 +585,137 @@ module.exports = (function() {
, createdAtAttr = Utils._.underscoredIf(self.options.createdAt, self.options.underscored) , createdAtAttr = Utils._.underscoredIf(self.options.createdAt, self.options.underscored)
, errors = [] , errors = []
, daos = records.map(function(v) { , daos = records.map(function(v) {
var build = self.build(v) return 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
}) })
if (options.validate === true && errors.length > 0) { return new Utils.CustomEventEmitter(function(emitter) {
return new Utils.CustomEventEmitter(function(emitter) { var done = function() {
emitter.emit('error', errors) self.runHooks('afterBulkCreate', daos, fields, function(err, newRecords, newFields) {
}).run() if (!!err) {
} return emitter.emit('error', err)
}
// we will re-create from DAOs, which may have set up default attributes daos = newRecords || daos
records = [] fields = newFields || fields
var found = false
daos.forEach(function(dao) { emitter.emit('success', daos, fields)
var values = fields.length > 0 ? {} : dao.dataValues })
}
fields.forEach(function(field) { var next = function() {
values[field] = dao.dataValues[field] if (options.hooks === false) {
}) return runQuery()
}
var i = 0
var iterate = function(i) {
self.runHooks('beforeCreate', daos[i], function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
if (self.options.timestamps) { daos[i] = newValues || daos[i]
values[createdAtAttr] = Utils.now() daos[i].save().error(function(err) {
values[updatedAtAttr] = Utils.now() 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]
})
if (self.options.timestamps) {
if (!values[createdAtAttr]) {
values[createdAtAttr] = Utils.now(self.daoFactoryManager.sequelize.options.dialect)
}
// Validate enums if (!values[updatedAtAttr]) {
records.forEach(function(values) { values[updatedAtAttr] = Utils.now(self.daoFactoryManager.sequelize.options.dialect)
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(', '))
} }
}
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()
} }
/** /**
...@@ -631,19 +723,117 @@ module.exports = (function() { ...@@ -631,19 +723,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()
} }
/** /**
...@@ -654,27 +844,129 @@ module.exports = (function() { ...@@ -654,27 +844,129 @@ 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()
} }
if (options.validate === true) { return new Utils.CustomEventEmitter(function(emitter) {
var build = this.build(attrValueHash) var runSave = function() {
, attrKeys = Object.keys(attrValueHash) self.runHooks(self.options.hooks.beforeBulkUpdate, attrValueHash, where, function(err, attributes, _where) {
, validate = build.validate({skip: Object.keys(build.dataValues).filter(function(val) { return attrKeys.indexOf(val) !== -1 })}) 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)
}
if (validate !== null && Object.keys(validate).length > 0) { query = self.QueryInterface.bulkUpdate(self.tableName, attrValueHash, where, options)
return new Utils.CustomEventEmitter(function(emitter) { query.on('sql', function(sql) {
emitter.emit('error', validate) emitter.emit('sql', sql)
}).run() })
.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()
}
})
} }
}
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) { DAOFactory.prototype.describe = function(schema) {
...@@ -698,6 +990,29 @@ module.exports = (function() { ...@@ -698,6 +990,29 @@ module.exports = (function() {
// private // private
var paranoidClause = function(options) {
if (this.options.paranoid === true) {
options = options || {}
options.where = options.where || {}
// Don't overwrite our explicit deletedAt search value if we provide one
if (!!options.where[this.options.deletedAt]) {
return options
}
if (typeof options.where === "string") {
options.where += ' AND ' + this.QueryInterface.quoteIdentifier(this.options.deletedAt) + ' IS NULL '
}
else if (Array.isArray(options.where)) {
options.where[0] += ' AND ' + this.QueryInterface.quoteIdentifier(this.options.deletedAt) + ' IS NULL '
} else {
options.where[this.options.deletedAt] = null
}
}
return options
}
var addOptionalClassMethods = function() { var addOptionalClassMethods = function() {
var self = this var self = this
Utils._.each(this.options.classMethods || {}, function(fct, name) { self[name] = fct }) Utils._.each(this.options.classMethods || {}, function(fct, name) { self[name] = fct })
...@@ -816,6 +1131,7 @@ module.exports = (function() { ...@@ -816,6 +1131,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
})() })()
...@@ -18,19 +18,50 @@ DaoValidator.prototype.validate = function() { ...@@ -18,19 +18,50 @@ DaoValidator.prototype.validate = function() {
return errors 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 // private
var validateModel = function() { var validateModel = function() {
var errors = {} var self = this
, errors = {}
// for each model validator for this DAO // for each model validator for this DAO
Utils._.each(this.model.__options.validate, function(validator, validatorType) { Utils._.each(this.model.__options.validate, function(validator, validatorType) {
try { try {
validator.apply(this.model) validator.apply(self.model)
} catch (err) { } catch (err) {
errors[validatorType] = [err.message] // TODO: data structure needs to change for 2.0 errors[validatorType] = [err.message] // TODO: data structure needs to change for 2.0
} }
}.bind(this)) })
return errors return errors
} }
...@@ -54,11 +85,12 @@ var validateAttributes = function() { ...@@ -54,11 +85,12 @@ var validateAttributes = function() {
} }
var validateAttribute = function(value, field) { var validateAttribute = function(value, field) {
var errors = {} var self = this
, errors = {}
// for each validator // for each validator
Utils._.each(this.model.validators[field], function(details, validatorType) { 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 { try {
validator.fn.apply(null, validator.args) validator.fn.apply(null, validator.args)
...@@ -74,7 +106,7 @@ var validateAttribute = function(value, field) { ...@@ -74,7 +106,7 @@ var validateAttribute = function(value, field) {
errors[field] = errors[field] || [] errors[field] = errors[field] || []
errors[field].push(msg) errors[field].push(msg)
} }
}.bind(this)) // for each validator for this field })
return errors return errors
} }
......
...@@ -122,68 +122,98 @@ module.exports = (function() { ...@@ -122,68 +122,98 @@ module.exports = (function() {
}) })
} }
var errors = this.validate() return new Utils.CustomEventEmitter(function(emitter) {
self.hookValidate().error(function(err) {
if (!!errors) { emitter.emit('error', err)
return new Utils.CustomEventEmitter(function(emitter) { }).success(function() {
emitter.emit('error', errors) for (var attrName in self.daoFactory.rawAttributes) {
}).run() if (self.daoFactory.rawAttributes.hasOwnProperty(attrName)) {
} var definition = self.daoFactory.rawAttributes[attrName]
, isHstore = !!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type
for (var attrName in this.daoFactory.rawAttributes) { , isEnum = definition.type && (definition.type.toString() === DataTypes.ENUM.toString())
if (this.daoFactory.rawAttributes.hasOwnProperty(attrName)) { , isMySQL = self.daoFactory.daoFactoryManager.sequelize.options.dialect === "mysql"
var definition = this.daoFactory.rawAttributes[attrName] , ciCollation = !!self.daoFactory.options.collate && self.daoFactory.options.collate.match(/_ci$/i)
, isEnum = definition.type && (definition.type.toString() === DataTypes.ENUM.toString())
, isHstore = !!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type // Unfortunately for MySQL CI collation we need to map/lowercase values again
, hasValue = values[attrName] !== undefined if (isEnum && isMySQL && ciCollation) {
, isMySQL = this.daoFactory.daoFactoryManager.sequelize.options.dialect === "mysql" var scopeIndex = (definition.values || []).map(function(d) { return d.toLowerCase() }).indexOf(values[attrName].toLowerCase())
, ciCollation = !!this.daoFactory.options.collate && this.daoFactory.options.collate.match(/_ci$/i) valueOutOfScope = scopeIndex === -1
, valueOutOfScope
// We'll return what the actual case will be, since a simple SELECT query would do the same...
if (isEnum && isMySQL && ciCollation && hasValue) { 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) { if (isHstore) {
values[attrName] = definition.values[scopeIndex] 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)) { if (self.isNewRecord) {
this.dataValues[updatedAtAttr] = values[updatedAtAttr] = Utils.now(this.sequelize.options.dialect) 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) { if (identifier === null && self.__options.whereCollection !== null) {
this.isDirty = false identifier = self.__options.whereCollection;
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 && this.__options.whereCollection !== null) { self.isDirty = false
identifier = this.__options.whereCollection; query = 'update'
} args = [self, self.QueryInterface.QueryGenerator.addSchema(self.__factory), values, identifier, options]
hook = 'Update'
}
this.isDirty = false self.__factory.runHooks('before' + hook, values, function(err, newValues) {
var tableName = this.QueryInterface.QueryGenerator.addSchema(this.__factory) if (!!err) {
, query = this.QueryInterface.update(this, tableName, values, identifier, options) 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() { ...@@ -231,6 +261,16 @@ module.exports = (function() {
return (Utils._.isEmpty(errors) ? null : errors) 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) { DAO.prototype.updateAttributes = function(updates, fields) {
this.setAttributes(updates) this.setAttributes(updates)
...@@ -277,14 +317,41 @@ module.exports = (function() { ...@@ -277,14 +317,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) {
...@@ -419,10 +486,15 @@ module.exports = (function() { ...@@ -419,10 +486,15 @@ module.exports = (function() {
} }
if (this.__options.timestamps) { if (this.__options.timestamps) {
defaults[Utils._.underscoredIf(this.__options.createdAt, this.__options.underscored)] = Utils.now(this.sequelize.options.dialect) if (!this.defaultValues[Utils._.underscoredIf(this.__options.createdAt, this.__options.underscored)]) {
defaults[Utils._.underscoredIf(this.__options.updatedAt, this.__options.underscored)] = Utils.now(this.sequelize.options.dialect) defaults[Utils._.underscoredIf(this.__options.createdAt, this.__options.underscored)] = Utils.now(this.sequelize.options.dialect)
}
if (!this.defaultValues[Utils._.underscoredIf(this.__options.updatedAt, this.__options.underscored)]) {
defaults[Utils._.underscoredIf(this.__options.updatedAt, this.__options.underscored)] = Utils.now(this.sequelize.options.dialect)
}
if (this.__options.paranoid) { if (this.__options.paranoid && !this.defaultValues[Utils._.underscoredIf(this.__options.deletedAt, this.__options.underscored)]) {
defaults[Utils._.underscoredIf(this.__options.deletedAt, this.__options.underscored)] = null defaults[Utils._.underscoredIf(this.__options.deletedAt, this.__options.underscored)] = null
} }
} }
......
...@@ -303,6 +303,49 @@ module.exports = (function() { ...@@ -303,6 +303,49 @@ module.exports = (function() {
}, },
/* /*
Create a trigger
*/
createTrigger: function(tableName, triggerName, timingType, fireOnArray, functionName, functionParams,
optionsArray) {
throwMethodUndefined('createTrigger')
},
/*
Drop a trigger
*/
dropTrigger: function(tableName, triggerName) {
throwMethodUndefined('dropTrigger')
},
/*
Rename a trigger
*/
renameTrigger: function(tableName, oldTriggerName, newTriggerName) {
throwMethodUndefined('renameTrigger')
},
/*
Create a function
*/
createFunction: function(functionName, params, returnType, language, body, options) {
throwMethodUndefined('createFunction')
},
/*
Drop a function
*/
dropFunction: function(functionName, params) {
throwMethodUndefined('dropFunction')
},
/*
Rename a function
*/
renameFunction: function(oldFunctionName, params, newFunctionName) {
throwMethodUndefined('renameFunction')
},
/*
Escape an identifier (e.g. a table or attribute name) Escape an identifier (e.g. a table or attribute name)
*/ */
quoteIdentifier: function(identifier, force) { quoteIdentifier: function(identifier, force) {
......
...@@ -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 })
......
...@@ -253,17 +253,25 @@ module.exports = (function() { ...@@ -253,17 +253,25 @@ module.exports = (function() {
} }
var connect = function(done, config) { var connect = function(done, config) {
var emitter = new (require('events').EventEmitter)()
config = config || this.config config = config || this.config
var connection = mysql.createConnection({ var emitter = new (require('events').EventEmitter)()
var connectionConfig = {
host: config.host, host: config.host,
port: config.port, port: config.port,
user: config.username, user: config.username,
password: config.password, password: config.password,
database: config.database, database: config.database,
timezone: 'Z' timezone: 'Z'
}) };
if (config.dialectOptions) {
Object.keys(config.dialectOptions).forEach(function (key) {
connectionConfig[key] = config.dialectOptions[key];
});
}
var connection = mysql.createConnection(connectionConfig);
connection.connect(function(err) { connection.connect(function(err) {
if (err) { if (err) {
...@@ -386,4 +394,4 @@ module.exports = (function() { ...@@ -386,4 +394,4 @@ module.exports = (function() {
} }
return ConnectorManager return ConnectorManager
})() })()
\ No newline at end of file
...@@ -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')
} }
...@@ -722,6 +722,81 @@ module.exports = (function() { ...@@ -722,6 +722,81 @@ module.exports = (function() {
return false // not supported by dialect return false // not supported by dialect
}, },
createTrigger: function(tableName, triggerName, eventType, fireOnSpec, functionName, functionParams, optionsArray) {
var sql = [
'CREATE <%= constraintVal %>TRIGGER <%= triggerName %>'
, '<%= eventType %> <%= eventSpec %>'
, 'ON <%= tableName %>'
, '<%= optionsSpec %>'
, 'EXECUTE PROCEDURE <%= functionName %>(<%= paramList %>);'
].join('\n\t')
return Utils._.template(sql)({
constraintVal: this.triggerEventTypeIsConstraint(eventType),
triggerName: triggerName,
eventType: this.decodeTriggerEventType(eventType),
eventSpec: this.expandTriggerEventSpec(fireOnSpec),
tableName: tableName,
optionsSpec: this.expandOptions(optionsArray),
functionName: functionName,
paramList: this.expandFunctionParamList(functionParams)
})
},
dropTrigger: function(tableName, triggerName) {
var sql = 'DROP TRIGGER <%= triggerName %> ON <%= tableName %> RESTRICT;'
return Utils._.template(sql)({
triggerName: triggerName,
tableName: tableName
})
},
renameTrigger: function(tableName, oldTriggerName, newTriggerName) {
var sql = 'ALTER TRIGGER <%= oldTriggerName %> ON <%= tableName %> RENAME TO <%= newTriggerName%>;'
return Utils._.template(sql)({
tableName: tableName,
oldTriggerName: oldTriggerName,
newTriggerName: newTriggerName
})
},
createFunction: function(functionName, params, returnType, language, body, options) {
var sql = [ "CREATE FUNCTION <%= functionName %>(<%= paramList %>)"
, "RETURNS <%= returnType %> AS $$"
, "BEGIN"
, "\t<%= body %>"
, "END;"
, "$$ language '<%= language %>'<%= options %>;"
].join('\n')
return Utils._.template(sql)({
functionName: functionName,
paramList: this.expandFunctionParamList(params),
returnType: returnType,
body: body.replace('\n', '\n\t'),
language: language,
options: this.expandOptions(options)
})
},
dropFunction: function(functionName, params) {
// RESTRICT is (currently, as of 9.2) default but we'll be explicit
var sql = 'DROP FUNCTION <%= functionName %>(<%= paramList %>) RESTRICT;'
return Utils._.template(sql)({
functionName: functionName,
paramList: this.expandFunctionParamList(params)
})
},
renameFunction: function(oldFunctionName, params, newFunctionName) {
var sql = 'ALTER FUNCTION <%= oldFunctionName %>(<%= paramList %>) RENAME TO <%= newFunctionName %>;'
return Utils._.template(sql)({
oldFunctionName: oldFunctionName,
paramList: this.expandFunctionParamList(params),
newFunctionName: newFunctionName
})
},
databaseConnectionUri: function(config) { databaseConnectionUri: function(config) {
var template = '<%= protocol %>://<%= user %>:<%= password %>@<%= host %><% if(port) { %>:<%= port %><% } %>/<%= database %>' var template = '<%= protocol %>://<%= user %>:<%= password %>@<%= host %><% if(port) { %>:<%= port %><% } %>/<%= database %>'
...@@ -739,6 +814,77 @@ module.exports = (function() { ...@@ -739,6 +814,77 @@ module.exports = (function() {
return this.quoteIdentifier(Utils.removeTicks(this.escape(val), "'")) return this.quoteIdentifier(Utils.removeTicks(this.escape(val), "'"))
}, },
expandFunctionParamList: function expandFunctionParamList(params) {
if (Utils._.isUndefined(params) || !Utils._.isArray(params)) {
throw new Error("expandFunctionParamList: function parameters array required, including an empty one for no arguments")
}
var paramList = Utils._.each(params, function expandParam(curParam){
paramDef = []
if (Utils._.has(curParam, 'type')) {
if (Utils._.has(curParam, 'direction')) { paramDef.push(curParam.direction) }
if (Utils._.has(curParam, 'name')) { paramDef.push(curParam.name) }
paramDef.push(curParam.type)
} else {
throw new Error('createFunction called with a parameter with no type')
}
return paramDef.join(' ')
})
return paramList.join(', ')
},
expandOptions: function expandOptions(options) {
return Utils._.isUndefined(options) || Utils._.isEmpty(options) ?
'' : '\n\t' + options.join('\n\t')
},
decodeTriggerEventType: function decodeTriggerEventType(eventSpecifier) {
var EVENT_DECODER = {
'after': 'AFTER',
'before': 'BEFORE',
'instead_of': 'INSTEAD OF',
'after_constraint': 'AFTER'
}
if (!Utils._.has(EVENT_DECODER, eventSpecifier)) {
throw new Error('Invalid trigger event specified: ' + eventSpecifier)
}
return EVENT_DECODER[eventSpecifier]
},
triggerEventTypeIsConstraint: function triggerEventTypeIsConstraint(eventSpecifier) {
return eventSpecifier === 'after_constrain' ? 'CONSTRAINT ' : ''
},
expandTriggerEventSpec: function expandTriggerEventSpec(fireOnSpec) {
if (Utils._.isEmpty(fireOnSpec)) {
throw new Error('no table change events specified to trigger on')
}
return Utils._.map(fireOnSpec, function parseTriggerEventSpec(fireValue, fireKey){
var EVENT_MAP = {
'insert': 'INSERT',
'update': 'UPDATE',
'delete': 'DELETE',
'truncate': 'TRUNCATE'
}
if (!Utils._.has(EVENT_MAP, fireKey)) {
throw new Error('parseTriggerEventSpec: undefined trigger event ' + fireKey)
}
var eventSpec = EVENT_MAP[fireKey]
if (eventSpec === 'UPDATE') {
if (Utils._.isArray(fireValue) && fireValue.length > 0) {
eventSpec += ' OF ' + fireValue.join(', ')
}
}
return eventSpec
}).join(' OR ')
},
pgListEnums: function(tableName, attrName, options) { pgListEnums: function(tableName, attrName, options) {
if (arguments.length === 1) { if (arguments.length === 1) {
options = tableName options = tableName
......
...@@ -2,13 +2,13 @@ var util = require("util") ...@@ -2,13 +2,13 @@ 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) {
if(process.domain) { if (process.domain) {
return process.domain.bind(fct); return process.domain.bind(fct);
} }
} }
return fct; return 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)
}
...@@ -484,9 +484,10 @@ module.exports = (function() { ...@@ -484,9 +484,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) {
...@@ -494,29 +495,84 @@ module.exports = (function() { ...@@ -494,29 +495,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()
} }
...@@ -633,6 +689,80 @@ module.exports = (function() { ...@@ -633,6 +689,80 @@ module.exports = (function() {
} }
} }
QueryInterface.prototype.createTrigger = function(tableName, triggerName, timingType, fireOnArray,
functionName, functionParams, optionsArray) {
var sql = this.QueryGenerator.createTrigger(tableName, triggerName, timingType, fireOnArray, functionName
, functionParams, optionsArray)
if (sql){
return queryAndEmit.call(this, sql, 'createTrigger')
} else {
return new Utils.CustomEventEmitter(function(emitter) {
this.emit('createTrigger', null)
emitter.emit('success')
}).run()
}
}
QueryInterface.prototype.dropTrigger = function(tableName, triggerName) {
var sql = this.QueryGenerator.dropTrigger(tableName, triggerName)
if (sql){
return queryAndEmit.call(this, sql, 'dropTrigger')
} else {
return new Utils.CustomEventEmitter(function(emitter) {
this.emit('dropTrigger', null)
emitter.emit('success')
}).run()
}
}
QueryInterface.prototype.renameTrigger = function(tableName, oldTriggerName, newTriggerName) {
var sql = this.QueryGenerator.renameTrigger(tableName, oldTriggerName, newTriggerName)
if (sql){
return queryAndEmit.call(this, sql, 'renameTrigger')
} else {
return new Utils.CustomEventEmitter(function(emitter) {
this.emit('renameTrigger', null)
emitter.emit('success')
}).run()
}
}
QueryInterface.prototype.createFunction = function(functionName, params, returnType, language, body, options) {
var sql = this.QueryGenerator.createFunction(functionName, params, returnType, language, body, options)
if (sql){
return queryAndEmit.call(this, sql, 'createFunction')
} else {
return new Utils.CustomEventEmitter(function(emitter) {
this.emit('createFunction', null)
emitter.emit('success')
}).run()
}
}
QueryInterface.prototype.dropFunction = function(functionName, params) {
var sql = this.QueryGenerator.dropFunction(functionName, params)
if (sql){
return queryAndEmit.call(this, sql, 'dropFunction')
} else {
return new Utils.CustomEventEmitter(function(emitter) {
this.emit('dropFunction', null)
emitter.emit('success')
}).run()
}
}
QueryInterface.prototype.renameFunction = function(oldFunctionName, params, newFunctionName) {
var sql = this.QueryGenerator.renameFunction(oldFunctionName, params, newFunctionName)
if (sql){
return queryAndEmit.call(this, sql, 'renameFunction')
} else {
return new Utils.CustomEventEmitter(function(emitter) {
this.emit('renameFunction', null)
emitter.emit('success')
}).run()
}
}
// Helper methods useful for querying // Helper methods useful for querying
/** /**
......
...@@ -103,7 +103,8 @@ module.exports = (function() { ...@@ -103,7 +103,8 @@ module.exports = (function() {
native : this.options.native, native : this.options.native,
replication: this.options.replication, replication: this.options.replication,
dialectModulePath: this.options.dialectModulePath, dialectModulePath: this.options.dialectModulePath,
maxConcurrentQueries: this.options.maxConcurrentQueries maxConcurrentQueries: this.options.maxConcurrentQueries,
dialectOptions: this.options.dialectOptions,
} }
try { try {
...@@ -160,17 +161,8 @@ module.exports = (function() { ...@@ -160,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)
...@@ -185,6 +177,40 @@ module.exports = (function() { ...@@ -185,6 +177,40 @@ 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) {
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 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.0.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",
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
"pg": "~2.6.0", "pg": "~2.6.0",
"watchr": "~2.4.3", "watchr": "~2.4.3",
"yuidocjs": "~0.3.36", "yuidocjs": "~0.3.36",
"chai": "~1.7.2", "chai": "~1.8.0",
"mocha": "~1.13.0", "mocha": "~1.13.0",
"chai-datetime": "~1.1.1", "chai-datetime": "~1.1.1",
"sinon": "~1.7.3" "sinon": "~1.7.3"
......
module.exports = {
up: function(migration, DataTypes, done) {
migration.createFunction('get_an_answer', [], 'int', 'plpgsql',
'RETURN 42;'
).complete(done);
},
down: function(migration, DataTypes, done) {
migration.dropFunction('get_an_answer', []).complete(done);
}
}
module.exports = {
up: function(migration, DataTypes, done) {
migration.renameFunction('get_an_answer', [], 'get_the_answer').complete(done);
},
down: function(migration, DataTypes, done) {
migration.renameFunction('get_the_answer', [], 'get_an_answer').complete(done);
}
}
module.exports = {
up: function(migration, DataTypes, done) {
migration.dropFunction('get_the_answer', []).complete(done);
},
down: function(migration, DataTypes, done) {
migration.createFunction('get_the_answer', 'int', 'plpgsql',
'RETURN 42;'
).complete(done);
}
}
module.exports = {
up: function(migration, DataTypes, done) {
migration
.createTable('trigger_test', {
name: DataTypes.STRING,
updated_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
allowNull: false
}
})
.complete(done)
},
down: function(migration, DataTypes, done) {
migration.dropTable('trigger_test').complete(done)
}
}
module.exports = {
up: function(migration, DataTypes, done) {
migration.createTrigger('trigger_test', 'updated_at', 'before', {update: true},
'bump_updated_at', []).complete(done);
},
down: function(migration, DataTypes, done) {
migration.dropTrigger('trigger_test', 'updated_at').complete(done);
}
}
module.exports = {
up: function(migration, DataTypes, done) {
migration.renameTrigger('trigger_test', 'updated_at', 'update_updated_at').complete(done);
},
down: function(migration, DataTypes, done) {
migration.renameTrigger('trigger_test', 'update_updated_at', 'updated_at').complete(done);
}
}
module.exports = {
up: function(migration, DataTypes, done) {
migration.dropTrigger('trigger_test', 'update_updated_at').complete(done);
},
down: function(migration, DataTypes, done) {
migration.createTrigger('trigger_test', 'update_updated_at', 'before', {update: true},
'bump_updated_at', []).complete(done);
}
}
...@@ -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
}
} }
} }
...@@ -101,14 +101,23 @@ describe(Support.getTestDialectTeaser("Configuration"), function() { ...@@ -101,14 +101,23 @@ describe(Support.getTestDialectTeaser("Configuration"), function() {
}) })
it('should accept four parameters (database, username, password, options)', function(done) { it('should accept four parameters (database, username, password, options)', function(done) {
var sequelize = new Sequelize('dbname', 'root', 'pass', { port: 999 }) var sequelize = new Sequelize('dbname', 'root', 'pass', {
port: 999,
dialectOptions: {
supportBigNumbers: true,
bigNumberStrings: true
}
})
var config = sequelize.config var config = sequelize.config
expect(config.database).to.equal('dbname') expect(config.database).to.equal('dbname')
expect(config.username).to.equal('root') expect(config.username).to.equal('root')
expect(config.password).to.equal('pass') expect(config.password).to.equal('pass')
expect(config.port).to.equal(999) expect(config.port).to.equal(999)
expect(config.dialectOptions.supportBigNumbers).to.be.true
expect(config.dialectOptions.bigNumberStrings).to.be.true
done() done()
}) })
}) })
}) })
...@@ -69,6 +69,23 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -69,6 +69,23 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
done() done()
}) })
it('allows us us to predefine the ID column with our own specs', function(done) {
var User = this.sequelize.define('UserCol', {
id: {
type: Sequelize.STRING,
defaultValue: 'User',
primaryKey: true
}
})
User.sync({ force: true }).success(function() {
User.create({id: 'My own ID!'}).success(function(user) {
expect(user.id).to.equal('My own ID!')
done()
})
})
})
it("throws an error if 2 autoIncrements are passed", function(done) { it("throws an error if 2 autoIncrements are passed", function(done) {
var self = this var self = this
expect(function() { expect(function() {
...@@ -108,6 +125,39 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -108,6 +125,39 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
done() done()
}) })
it('should allow me to set a default value for createdAt and updatedAt', function(done) {
var UserTable = this.sequelize.define('UserCol', {
aNumber: Sequelize.INTEGER,
createdAt: {
type: Sequelize.DATE,
defaultValue: moment('2012-01-01').toDate()
},
updatedAt: {
type: Sequelize.DATE,
defaultValue: moment('2012-01-02').toDate()
}
}, { timestamps: true })
UserTable.sync({ force: true }).success(function() {
UserTable.create({aNumber: 5}).success(function(user) {
UserTable.bulkCreate([
{aNumber: 10},
{aNumber: 12}
]).success(function() {
UserTable.all({where: {aNumber: { gte: 10 }}}).success(function(users) {
expect(moment(user.createdAt).format('YYYY-MM-DD')).to.equal('2012-01-01')
expect(moment(user.updatedAt).format('YYYY-MM-DD')).to.equal('2012-01-02')
users.forEach(function(u) {
expect(moment(u.createdAt).format('YYYY-MM-DD')).to.equal('2012-01-01')
expect(moment(u.updatedAt).format('YYYY-MM-DD')).to.equal('2012-01-02')
})
done()
})
})
})
})
})
it('should allow me to override updatedAt, createdAt, and deletedAt fields', function(done) { it('should allow me to override updatedAt, createdAt, and deletedAt fields', function(done) {
var UserTable = this.sequelize.define('UserCol', { var UserTable = this.sequelize.define('UserCol', {
aNumber: Sequelize.INTEGER aNumber: Sequelize.INTEGER
...@@ -382,10 +432,11 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -382,10 +432,11 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
, pad = function (number) { , pad = function (number) {
if (number > 9) { if (number > 9) {
return number return number
} }
return '0' + number return '0' + number
} }
expect(user.year).to.equal(now.getFullYear() + '-' + pad(now.getMonth() + 1) + '-' + pad(now.getDate()))
expect(user.year).to.equal(now.getUTCFullYear() + '-' + pad(now.getUTCMonth() + 1) + '-' + pad(now.getUTCDate()))
done() done()
}) })
}) })
...@@ -1119,6 +1170,8 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -1119,6 +1170,8 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
it('sets deletedAt to the current timestamp if paranoid is true', function(done) { it('sets deletedAt to the current timestamp if paranoid is true', function(done) {
var self = this var self = this
, ident = self.sequelize.queryInterface.QueryGenerator.quoteIdentifier
, escape = self.sequelize.queryInterface.QueryGenerator.quote
, ParanoidUser = self.sequelize.define('ParanoidUser', { , ParanoidUser = self.sequelize.define('ParanoidUser', {
username: Sequelize.STRING, username: Sequelize.STRING,
secretValue: Sequelize.STRING, secretValue: Sequelize.STRING,
...@@ -1133,19 +1186,57 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -1133,19 +1186,57 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
ParanoidUser.sync({ force: true }).success(function() { ParanoidUser.sync({ force: true }).success(function() {
ParanoidUser.bulkCreate(data).success(function() { ParanoidUser.bulkCreate(data).success(function() {
var date = parseInt(+new Date()/5000, 10) // since we save in UTC, let's format to UTC time
var date = moment().utc().format('YYYY-MM-DD h:mm')
ParanoidUser.destroy({secretValue: '42'}).success(function() { ParanoidUser.destroy({secretValue: '42'}).success(function() {
ParanoidUser.findAll({order: 'id'}).success(function(users) { ParanoidUser.findAll({order: 'id'}).success(function(users) {
expect(users.length).to.equal(3) expect(users.length).to.equal(1)
expect(users[0].username).to.equal("Bob")
self.sequelize.query('SELECT * FROM ' + ident('ParanoidUsers') + ' WHERE ' + ident('deletedAt') + ' IS NOT NULL ORDER BY ' + ident('id'), null, {raw: true}).success(function(users) {
expect(users[0].username).to.equal("Peter")
expect(users[1].username).to.equal("Paul")
expect(users[0].username).to.equal("Peter") if (dialect === "sqlite") {
expect(users[1].username).to.equal("Paul") expect(moment(users[0].deletedAt).format('YYYY-MM-DD h:mm')).to.equal(date)
expect(users[2].username).to.equal("Bob") expect(moment(users[1].deletedAt).format('YYYY-MM-DD h:mm')).to.equal(date)
} else {
expect(moment(users[0].deletedAt).utc().format('YYYY-MM-DD h:mm')).to.equal(date)
expect(moment(users[1].deletedAt).utc().format('YYYY-MM-DD h:mm')).to.equal(date)
}
done()
})
})
})
})
})
})
expect(parseInt(+users[0].deletedAt/5000, 10)).to.equal(date) describe("can't find records marked as deleted with paranoid being true", function() {
expect(parseInt(+users[1].deletedAt/5000, 10)).to.equal(date) it('with the DAOFactory', function(done) {
var User = this.sequelize.define('UserCol', {
username: Sequelize.STRING
}, { paranoid: true })
done() User.sync({ force: true }).success(function() {
User.bulkCreate([
{username: 'Toni'},
{username: 'Tobi'},
{username: 'Max'}
]).success(function() {
User.find(1).success(function(user) {
user.destroy().success(function() {
User.find(1).success(function(user) {
expect(user).to.be.null
User.count().success(function(cnt) {
expect(cnt).to.equal(2)
User.all().success(function(users) {
expect(users).to.have.length(2)
done()
})
})
})
})
}) })
}) })
}) })
...@@ -2693,6 +2784,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -2693,6 +2784,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
it("handles offset and limit", function(done) { it("handles offset and limit", function(done) {
var self = this var self = this
this.User.bulkCreate([{username: 'bobby'}, {username: 'tables'}]).success(function() { this.User.bulkCreate([{username: 'bobby'}, {username: 'tables'}]).success(function() {
self.User.findAll({ limit: 2, offset: 2 }).success(function(users) { self.User.findAll({ limit: 2, offset: 2 }).success(function(users) {
expect(users.length).to.equal(2) expect(users.length).to.equal(2)
...@@ -2780,6 +2872,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -2780,6 +2872,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
done() done()
}) })
}) })
it("handles attributes", function(done) { it("handles attributes", function(done) {
this.User.findAndCountAll({where: "id != " + this.users[0].id, attributes: ['data']}).success(function(info) { this.User.findAndCountAll({where: "id != " + this.users[0].id, attributes: ['data']}).success(function(info) {
expect(info.count).to.equal(2) expect(info.count).to.equal(2)
...@@ -3398,6 +3491,44 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3398,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
...@@ -3440,17 +3571,24 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3440,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()
}) })
...@@ -3474,11 +3612,9 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3474,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!')
...@@ -3505,11 +3641,9 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3505,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!')
...@@ -3546,12 +3680,10 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3546,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.
...@@ -14,7 +14,7 @@ describe(Support.getTestDialectTeaser("Migrator"), function() { ...@@ -14,7 +14,7 @@ describe(Support.getTestDialectTeaser("Migrator"), function() {
logging: function(){} logging: function(){}
}, options || {}) }, options || {})
// this.sequelize.options.logging = console.log //this.sequelize.options.logging = console.log
var migrator = new Migrator(this.sequelize, options) var migrator = new Migrator(this.sequelize, options)
migrator migrator
...@@ -28,7 +28,7 @@ describe(Support.getTestDialectTeaser("Migrator"), function() { ...@@ -28,7 +28,7 @@ describe(Support.getTestDialectTeaser("Migrator"), function() {
describe('getUndoneMigrations', function() { describe('getUndoneMigrations', function() {
it("returns no files if timestamps are after the files timestamp", function(done) { it("returns no files if timestamps are after the files timestamp", function(done) {
this.init({ from: 20120101010101 }, function(migrator) { this.init({ from: 20140101010101 }, function(migrator) {
migrator.getUndoneMigrations(function(err, migrations) { migrator.getUndoneMigrations(function(err, migrations) {
expect(err).to.be.null expect(err).to.be.null
expect(migrations.length).to.equal(0) expect(migrations.length).to.equal(0)
...@@ -86,7 +86,7 @@ describe(Support.getTestDialectTeaser("Migrator"), function() { ...@@ -86,7 +86,7 @@ describe(Support.getTestDialectTeaser("Migrator"), function() {
SequelizeMeta.create({ from: null, to: 20111117063700 }).success(function() { SequelizeMeta.create({ from: null, to: 20111117063700 }).success(function() {
migrator.getUndoneMigrations(function(err, migrations) { migrator.getUndoneMigrations(function(err, migrations) {
expect(err).to.be.null expect(err).to.be.null
expect(migrations).to.have.length(7) expect(migrations).to.have.length(14)
expect(migrations[0].filename).to.equal('20111130161100-emptyMigration.js') expect(migrations[0].filename).to.equal('20111130161100-emptyMigration.js')
done() done()
}) })
...@@ -286,7 +286,8 @@ describe(Support.getTestDialectTeaser("Migrator"), function() { ...@@ -286,7 +286,8 @@ describe(Support.getTestDialectTeaser("Migrator"), function() {
}) })
}) })
}) })
})
})
describe('renameColumn', function() { describe('renameColumn', function() {
it("renames the signature column from user to sig", function(done) { it("renames the signature column from user to sig", function(done) {
...@@ -307,5 +308,145 @@ describe(Support.getTestDialectTeaser("Migrator"), function() { ...@@ -307,5 +308,145 @@ describe(Support.getTestDialectTeaser("Migrator"), function() {
}) })
}) })
}) })
})
if (dialect.match(/^postgres/)) {
describe('function migrations', function() {
var generateFunctionCountQuery = function generateFunctionCountQuery(functionName, langName) {
return [
'SELECT * FROM pg_proc p LEFT OUTER JOIN pg_language l ON (l.oid = p.prolang)',
'WHERE p.proname = \'' + functionName + '\' AND l.lanname = \'' + langName + '\';'
].join('\n')
}
var FUNC_NAME = 'get_an_answer'
var RENAME_FUNC_NAME = 'get_the_answer'
// Set up the table and trigger
before(function(done){
this.init({ from: 20130909174103, to: 20130909174103}, function(migrator) {
migrator.migrate().success(function(){
done()
})
})
})
it("creates a function " + FUNC_NAME + "()", function(done) {
this.sequelize.query(generateFunctionCountQuery(FUNC_NAME, 'plpgsql')).success(function(rows){
expect(rows.length).to.equal(1)
done()
})
})
it("renames a function " + FUNC_NAME + "() to " + RENAME_FUNC_NAME + "()", function(done) {
var self = this
this.init({ from: 20130909174253, to: 20130909174253 }, function(migrator) {
migrator.migrate().success(function(){
self.sequelize.query(generateFunctionCountQuery(FUNC_NAME, 'plpgsql')).success(function(rows){
expect(rows.length).to.equal(0)
self.sequelize.query(generateFunctionCountQuery(RENAME_FUNC_NAME, 'plpgsql')).success(function(rows){
expect(rows.length).to.equal(1)
done()
})
})
})
})
})
it("deletes a function " + RENAME_FUNC_NAME + "()", function(done) {
var self = this
this.init({ from: 20130909175000, to: 20130909175000 }, function(migrator) {
migrator.migrate().success(function(){
self.sequelize.query(generateFunctionCountQuery(RENAME_FUNC_NAME, 'plpgsql')).success(function(rows){
expect(rows.length).to.equal(0)
done()
})
})
})
})
})
describe('test trigger migrations', function() {
var generateTriggerCountQuery = function generateTriggerCountQuery(triggerName) {
return 'SELECT * FROM pg_trigger where tgname = \'' + triggerName + '\''
}
var generateTableCountQuery = function generateTableCountQuery(functionName, schemaName) {
return 'SELECT * FROM pg_tables where tablename = \'' + functionName + '\' and schemaname = \'' + schemaName + '\''
}
var TRIGGER_NAME = 'updated_at'
var RENAME_TRIGGER_NAME = 'update_updated_at'
var TABLE_NAME = 'trigger_test'
var CATALOG_NAME = 'public'
// Make sure the function is present
before(function(done){
this.sequelize.query("CREATE FUNCTION bump_updated_at()\n" +
"RETURNS TRIGGER AS $$\n" +
"BEGIN\n" +
"NEW.updated_at = now();\n" +
"RETURN NEW;\n" +
"END;\n" +
"$$ language 'plpgsql';"
).success(function() {done()})
})
// Clean up the function
after(function(done){
this.sequelize.query("DROP FUNCTION IF EXISTS bump_updated_at()").success(function(){ done(); })
})
it("creates a trigger updated_at on trigger_test", function(done) {
var self = this
this.init({ from: 20130909175939, to: 20130909180846}, function(migrator) {
migrator.migrate().success(function(){
self.sequelize.query(generateTableCountQuery(TABLE_NAME, CATALOG_NAME)).success(function(rows){
expect(rows.length).to.equal(1)
self.sequelize.query(generateTriggerCountQuery(TRIGGER_NAME)).success(function(rows){
expect(rows.length).to.equal(1)
done()
})
})
})
})
})
it("renames a trigger on " + TABLE_NAME + " from " + TRIGGER_NAME + " to " + RENAME_TRIGGER_NAME, function(done){
var self = this
this.init({ from: 20130909175939, to: 20130909181148}, function(migrator) {
migrator.migrate().success(function(){
self.sequelize.query(generateTableCountQuery(TABLE_NAME, CATALOG_NAME)).success(function(rows){
expect(rows.length).to.equal(1)
self.sequelize.query(generateTriggerCountQuery(RENAME_TRIGGER_NAME)).success(function(rows){
expect(rows.length).to.equal(1)
self.sequelize.query(generateTriggerCountQuery(TRIGGER_NAME)).success(function(rows){
expect(rows.length).to.equal(0)
done()
})
})
})
})
})
})
it("deletes a trigger " + TRIGGER_NAME + " on trigger_test", function(done) {
var self = this
this.init({ from: 20130909175939, to: 20130909185621}, function(migrator) {
migrator.migrate().success(function(){
self.sequelize.query(generateTriggerCountQuery(TRIGGER_NAME)).success(function(rows){
expect(rows.length).to.equal(0)
migrator.migrate({method: 'down'}).success(function(){
self.sequelize.query(generateTableCountQuery(TABLE_NAME, CATALOG_NAME)).success(function(rows){
expect(rows.length).to.equal(0)
done()
})
})
})
})
})
})
})
} // if dialect postgres
})
...@@ -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()
})
})
}) })
}) })
}) })
......
...@@ -467,11 +467,10 @@ describe(Support.getTestDialectTeaser("Sequelize"), function () { ...@@ -467,11 +467,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,16 +26,19 @@ var Support = { ...@@ -26,16 +26,19 @@ 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 || {}
} }
if (!!options.host) { if (!!options.host) {
...@@ -50,12 +53,7 @@ var Support = { ...@@ -50,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!