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

Commit 7175103a by Daniel Durante

Merge branch 'master' into forceparanoid

Conflicts:
	lib/dao-factory.js
	lib/dao.js
2 parents 7d96bb1e 1149f3ea
Showing with 2111 additions and 316 deletions
......@@ -10,6 +10,7 @@
"waitsFor": false,
"runs": false
},
"node": true,
"camelcase": true,
"curly": true,
"forin": true,
......@@ -18,5 +19,7 @@
"asi": true,
"evil": false,
"laxcomma": true,
"es5": true
"es5": true,
"quotmark": false,
"strict": false
}
\ No newline at end of file
......@@ -12,11 +12,12 @@ teaser:
echo ''
test:
@make teaser && \
./node_modules/mocha/bin/mocha \
--colors \
--reporter $(REPORTER) \
$(TESTS)
@if [ "$$GREP" ]; then \
make teaser && ./node_modules/mocha/bin/mocha --colors --reporter $(REPORTER) -g "$$GREP" $(TESTS); \
else \
make teaser && ./node_modules/mocha/bin/mocha --colors --reporter $(REPORTER) $(TESTS); \
fi
sqlite:
@DIALECT=sqlite make test
......
......@@ -33,6 +33,7 @@ changelog of the branch: https://github.com/sequelize/sequelize/blob/milestones/
- Associations
- Importing definitions from single files
- Promises
- Hooks/callbacks/lifecycle events
## Documentation and Updates ##
......@@ -62,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
- MariaDB support
- ~~Support for update and delete calls for whole tables without previous loading of instances~~ Implemented in [#569](https://github.com/sequelize/sequelize/pull/569) thanks to @optiltude
- Eager loading of nested associations [#388](https://github.com/sdepold/sequelize/issues/388#issuecomment-12019099)
- Eager loading of nested associations [#388](https://github.com/sequelize/sequelize/issues/388)
- ~~Model#delete~~ (renamed to [Model.destroy()](http://sequelizejs.com/documentation#instances-destroy))
- ~~Validate a model before it gets saved.~~ Implemented in [#601](https://github.com/sequelize/sequelize/pull/601), thanks to @durango
- Move validation of enum attribute value to validate method
- ~~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
- ~~Support for foreign keys~~ Implemented in [#595](https://github.com/sequelize/sequelize/pull/595), thanks to @optilude
......
......@@ -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] 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] 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] 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
......@@ -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] 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.
- [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] 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] 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
......
......@@ -14,6 +14,8 @@ module.exports = (function() {
this.options.foreignKey = Utils._.underscoredIf(Utils.singularize(this.options.as, this.source.options.language) + "Id", this.source.options.underscored)
}
this.options.useHooks = options.useHooks
this.associationAccessor = this.isSelfAssociation
? Utils.combineTableNames(this.target.tableName, this.options.as || this.target.tableName)
: this.options.as || this.target.tableName
......@@ -31,7 +33,7 @@ module.exports = (function() {
Utils._.defaults(this.source.rawAttributes, newAttributes)
// Sync attributes to DAO proto each time a new assoc is added
this.source.DAO.prototype.attributes = Object.keys(this.source.DAO.prototype.rawAttributes);
this.source.DAO.prototype.attributes = Object.keys(this.source.DAO.prototype.rawAttributes)
return this
}
......
......@@ -4,27 +4,61 @@ module.exports = (function() {
var HasManyDoubleLinked = function(definition, instance) {
this.__factory = definition
this.instance = instance
// Alias the quoting methods for code brevity
this.QueryInterface = instance.QueryInterface
}
HasManyDoubleLinked.prototype.injectGetter = function(options) {
var self = this, _options = options
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
var instancePrimaryKeys = Object.keys(self.instance.daoFactory.primaryKeys)
, 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]
, foreignPrimary = Object.keys(self.__factory.target.primaryKeys)
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 (Array.isArray(options.where)) {
......@@ -44,7 +78,7 @@ module.exports = (function() {
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('error', function(err){ customEventEmitter.emit('error', err) })
.on('sql', function(sql) { customEventEmitter.emit('sql', sql)})
......@@ -53,26 +87,41 @@ module.exports = (function() {
return customEventEmitter.run()
}
HasManyDoubleLinked.prototype.injectSetter = function(emitterProxy, oldAssociations, newAssociations) {
HasManyDoubleLinked.prototype.injectSetter = function(emitterProxy, oldAssociations, newAssociations, defaultAttributes) {
var self = this
, chainer = new Utils.QueryChainer()
, association = self.__factory.target.associations[self.__factory.associationAccessor]
, foreignIdentifier = association.isSelfAssociation ? association.foreignIdentifier : association.identifier
, sourceKeys = Object.keys(self.__factory.source.primaryKeys)
, targetKeys = Object.keys(self.__factory.target.primaryKeys)
, obsoleteAssociations = []
, changedAssociations = []
, unassociatedObjects;
var obsoleteAssociations = oldAssociations.filter(function (old) {
// Return only those old associations that are not found in new
return !Utils._.find(newAssociations, function (obj) {
return ((targetKeys.length === 1) ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id)
unassociatedObjects = newAssociations.filter(function (obj) {
return !Utils._.find(oldAssociations, function (old) {
return (!!obj[foreignIdentifier] && !!old[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : obj.id === old.id)
})
})
var unassociatedObjects = newAssociations.filter(function (obj) {
// Return only those associations that are new
return !Utils._.find(oldAssociations, function (old) {
return ((targetKeys.length === 1) ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id)
oldAssociations.forEach(function (old) {
var newObj = Utils._.find(newAssociations, function (obj) {
return (!!obj[foreignIdentifier] && !!old[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : 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) {
......@@ -93,12 +142,22 @@ module.exports = (function() {
attributes[self.__factory.identifier] = ((sourceKeys.length === 1) ? self.instance[sourceKeys[0]] : self.instance.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
})
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
.run()
.success(function() { emitterProxy.emit('success', newAssociations) })
......@@ -106,7 +165,7 @@ module.exports = (function() {
.on('sql', function(sql) { emitterProxy.emit('sql', sql) })
}
HasManyDoubleLinked.prototype.injectAdder = function(emitterProxy, newAssociation) {
HasManyDoubleLinked.prototype.injectAdder = function(emitterProxy, newAssociation, additionalAttributes, exists) {
var attributes = {}
, association = this.__factory.target.associations[this.__factory.associationAccessor]
, foreignIdentifier = association.isSelfAssociation ? association.foreignIdentifier : association.identifier;
......@@ -117,11 +176,22 @@ module.exports = (function() {
attributes[this.__factory.identifier] = ((sourceKeys.length === 1) ? this.instance[sourceKeys[0]] : this.instance.id)
attributes[foreignIdentifier] = ((targetKeys.length === 1) ? newAssociation[targetKeys[0]] : newAssociation.id)
if (exists) { // implies hasJoinTableModel === true
var where = attributes
attributes = Utils._.defaults({}, newAssociation[association.connectorDAO.name], additionalAttributes)
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
})()
......@@ -30,7 +30,7 @@ module.exports = (function() {
HasManySingleLinked.prototype.injectSetter = function(emitter, oldAssociations, newAssociations) {
var self = this
, associationKeys = Object.keys((oldAssociations[0] || newAssociations[0] || {}).daoFactory.primaryKeys || {})
, associationKeys = Object.keys((oldAssociations[0] || newAssociations[0] || {daoFactory: {primaryKeys: {}}}).daoFactory.primaryKeys || {})
, associationKey = associationKeys.length === 1 ? associationKeys[0] : 'id'
, chainer = new Utils.QueryChainer()
, obsoleteAssociations = oldAssociations.filter(function (old) {
......
......@@ -13,12 +13,22 @@ module.exports = (function() {
this.options = options
this.useJunctionTable = this.options.useJunctionTable === undefined ? true : this.options.useJunctionTable
this.isSelfAssociation = (this.source.tableName === this.target.tableName)
this.hasJoinTableModel = !!this.options.joinTableModel
var combinedTableName = Utils.combineTableNames(
var combinedTableName;
if (this.hasJoinTableModel) {
combinedTableName = this.options.joinTableModel.tableName
} else if (this.options.joinTableName) {
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
var as = (this.options.as || Utils.pluralize(this.target.tableName, this.target.options.language))
......@@ -37,6 +47,8 @@ module.exports = (function() {
// or in an extra table which connects two tables
HasMany.prototype.injectAttributes = function() {
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)
// is there already a single sided association between the source and the target?
......@@ -48,9 +60,15 @@ module.exports = (function() {
} else {
this.foreignIdentifier = this.target.associations[this.associationAccessor].identifier
this.target.associations[this.associationAccessor].foreignIdentifier = 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
var combinedTableAttributes = {}
......@@ -61,7 +79,21 @@ module.exports = (function() {
combinedTableAttributes[this.identifier] = {type: sourceKeyType, primaryKey: true}
combinedTableAttributes[this.foreignIdentifier] = {type: targetKeyType, primaryKey: true}
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) {
this.target.associations[this.associationAccessor].connectorDAO = this.connectorDAO
......@@ -135,7 +167,7 @@ module.exports = (function() {
HasMany.prototype.injectSetter = function(obj) {
var self = this
obj[this.accessors.set] = function(newAssociatedObjects) {
obj[this.accessors.set] = function(newAssociatedObjects, defaultAttributes) {
if (newAssociatedObjects === null) {
newAssociatedObjects = []
}
......@@ -147,7 +179,7 @@ module.exports = (function() {
instance[self.accessors.get]()
.success(function(oldAssociatedObjects) {
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) {
emitter.emit('error', err)
......@@ -158,20 +190,21 @@ module.exports = (function() {
}).run()
}
obj[this.accessors.add] = function(newAssociatedObject) {
obj[this.accessors.add] = function(newAssociatedObject, additionalAttributes) {
var instance = this
, primaryKeys = Object.keys(newAssociatedObject.daoFactory.primaryKeys || {})
, primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id'
, where = {}
where[newAssociatedObject.daoFactory.tableName+'.'+primaryKey] = newAssociatedObject[primaryKey]
return new Utils.CustomEventEmitter(function(emitter) {
instance[self.accessors.get]({ where: where })
.error(function(err){ emitter.emit('error', err)})
.success(function(currentAssociatedObjects) {
if (currentAssociatedObjects.length === 0) {
if (currentAssociatedObjects.length === 0 || self.hasJoinTableModel === true) {
var Class = self.connectorDAO ? HasManyMultiLinked : HasManySingleLinked
new Class(self, instance).injectAdder(emitter, newAssociatedObject)
new Class(self, instance).injectAdder(emitter, newAssociatedObject, additionalAttributes, !!currentAssociatedObjects.length)
} else {
emitter.emit('success', newAssociatedObject);
}
......@@ -184,15 +217,44 @@ module.exports = (function() {
var customEventEmitter = new Utils.CustomEventEmitter(function() {
instance[self.accessors.get]().success(function(currentAssociatedObjects) {
var newAssociations = []
, oldAssociations = []
currentAssociatedObjects.forEach(function(association) {
if (!Utils._.isEqual(oldAssociatedObject.identifiers, association.identifiers))
if (!Utils._.isEqual(oldAssociatedObject.identifiers, association.identifiers)) {
newAssociations.push(association)
}
})
var tick = 0
var next = function(err, i) {
if (!!err || i >= oldAssociations.length) {
return run(err)
}
oldAssociations[i].destroy().error(function(err) {
next(err)
})
.success(function() {
tick++
next(null, tick)
})
}
var run = function(err) {
if (!!err) {
return customEventEmitter.emit('error', err)
}
instance[self.accessors.set](newAssociations)
.success(function() { customEventEmitter.emit('success', null) })
.error(function(err) { customEventEmitter.emit('error', err) })
}
if (oldAssociations.length > 0) {
next(null, tick)
} else {
run()
}
})
})
return customEventEmitter.run()
......@@ -201,5 +263,27 @@ module.exports = (function() {
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
})()
......@@ -18,6 +18,8 @@ module.exports = (function() {
? Utils.combineTableNames(this.target.tableName, this.options.as || this.target.tableName)
: this.options.as || this.target.tableName
this.options.useHooks = options.useHooks
this.accessors = {
get: Utils._.camelize('get_' + (this.options.as || Utils.singularize(this.target.tableName, this.target.options.language))),
set: Utils._.camelize('set_' + (this.options.as || Utils.singularize(this.target.tableName, this.target.options.language)))
......
......@@ -13,10 +13,21 @@ module.exports = {
return source.rawAttributes[key].primaryKey
})
if(primaryKeys.length == 1) {
newAttribute.references = source.tableName,
if (primaryKeys.length === 1) {
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.onDelete = options.onDelete,
newAttribute.onDelete = options.onDelete
newAttribute.onUpdate = options.onUpdate
}
}
......
......@@ -7,6 +7,11 @@ var Utils = require("./../utils")
var Mixin = module.exports = function(){}
Mixin.hasOne = function(associatedDAO, options) {
// Since this is a mixin, we'll need a unique variable name for hooks (since DAOFactory will override our hooks option)
options = options || {}
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks)
options.useHooks = options.hooks
// the id is in the foreign table
var association = new HasOne(this, associatedDAO, Utils._.extend((options||{}), this.options))
this.associations[association.associationAccessor] = association.injectAttributes()
......@@ -18,8 +23,13 @@ Mixin.hasOne = function(associatedDAO, options) {
}
Mixin.belongsTo = function(associatedDAO, options) {
// Since this is a mixin, we'll need a unique variable name for hooks (since DAOFactory will override our hooks option)
options = options || {}
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks)
options.useHooks = options.hooks
// the id is in this table
var association = new BelongsTo(this, associatedDAO, Utils._.extend((options || {}), this.options))
var association = new BelongsTo(this, associatedDAO, Utils._.extend(options, this.options))
this.associations[association.associationAccessor] = association.injectAttributes()
association.injectGetter(this.DAO.prototype)
......@@ -29,6 +39,11 @@ Mixin.belongsTo = function(associatedDAO, options) {
}
Mixin.hasMany = function(associatedDAO, options) {
// Since this is a mixin, we'll need a unique variable name for hooks (since DAOFactory will override our hooks option)
options = options || {}
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks)
options.useHooks = options.hooks
// the id is in the foreign table or in a connecting table
var association = new HasMany(this, associatedDAO, Utils._.extend((options||{}), this.options))
this.associations[association.associationAccessor] = association.injectAttributes()
......
......@@ -25,7 +25,11 @@ module.exports = (function() {
schemaDelimiter: '',
language: 'en',
defaultScope: null,
scopes: null
scopes: null,
hooks: {
beforeCreate: [],
afterCreate: []
}
}, options || {})
// error check options
......@@ -339,6 +343,8 @@ module.exports = (function() {
this.options.whereCollection = options.where || null
}
options = paranoidClause.call(this, options)
return this.QueryInterface.select(this, this.tableName, options, Utils._.defaults({
type: 'SELECT',
hasJoin: hasJoin
......@@ -346,14 +352,16 @@ module.exports = (function() {
}
//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)
optcpy.attributes = optcpy.attributes || [this.QueryInterface.quoteIdentifier(this.tableName)+".*"]
// whereCollection is used for non-primary key updates
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))
}
/**
......@@ -430,6 +438,7 @@ module.exports = (function() {
}
}
options = paranoidClause.call(this, options)
options.limit = 1
return this.QueryInterface.select(this, this.getTableName(), options, Utils._.defaults({
......@@ -444,6 +453,8 @@ module.exports = (function() {
options.attributes.push(['count(*)', 'count'])
options.parseInt = true
options = paranoidClause.call(this, options)
return this.QueryInterface.rawSelect(this.getTableName(), options, 'count')
}
......@@ -563,7 +574,9 @@ module.exports = (function() {
*/
DAOFactory.prototype.bulkCreate = function(records, fields, options) {
options = options || {}
options.validate = options.validate || false
options.validate = options.validate === undefined ? false : Boolean(options.validate)
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks)
fields = fields || []
var self = this
......@@ -571,25 +584,62 @@ module.exports = (function() {
, createdAtAttr = Utils._.underscoredIf(self.options.createdAt, self.options.underscored)
, errors = []
, daos = records.map(function(v) {
var build = self.build(v)
if (options.validate === true) {
var valid = build.validate({skip: fields})
if (valid !== null) {
errors[errors.length] = {record: v, errors: valid}
return self.build(v)
})
return new Utils.CustomEventEmitter(function(emitter) {
var done = function() {
self.runHooks('afterBulkCreate', daos, fields, function(err, newRecords, newFields) {
if (!!err) {
return emitter.emit('error', err)
}
daos = newRecords || daos
fields = newFields || fields
emitter.emit('success', daos, fields)
})
}
var next = function() {
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)
}
daos[i] = newValues || daos[i]
daos[i].save().error(function(err) {
emitter.emit('error', err)
}).success(function() {
self.runHooks('afterCreate', daos[i], function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
daos[i] = newValues || daos[i]
i++
if (i >= daos.length) {
return done()
}
return build
iterate(i)
})
})
})
}
if (options.validate === true && errors.length > 0) {
return new Utils.CustomEventEmitter(function(emitter) {
emitter.emit('error', errors)
}).run()
iterate(i)
}
var runQuery = function() {
// we will re-create from DAOs, which may have set up default attributes
records = []
var found = false
daos.forEach(function(dao) {
var values = fields.length > 0 ? {} : dao.dataValues
......@@ -599,30 +649,72 @@ module.exports = (function() {
})
if (self.options.timestamps) {
values[createdAtAttr] = Utils.now()
values[updatedAtAttr] = Utils.now()
if (!values[createdAtAttr]) {
values[createdAtAttr] = Utils.now(self.daoFactoryManager.sequelize.options.dialect)
}
if (!values[updatedAtAttr]) {
values[updatedAtAttr] = Utils.now(self.daoFactoryManager.sequelize.options.dialect)
}
}
records.push(values)
})
// Validate enums
records.forEach(function(values) {
for (var attrName in self.rawAttributes) {
if (self.rawAttributes.hasOwnProperty(attrName)) {
var definition = self.rawAttributes[attrName]
, isEnum = (definition.type && (definition.type.toString() === DataTypes.ENUM.toString()))
, hasValue = (typeof values[attrName] !== 'undefined')
, valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1)
self.QueryInterface.bulkInsert(self.tableName, records)
.on('sql', function(sql) {
emitter.emit('sql', sql)
})
.error(function(err) {
emitter.emit('error', err)
}).success(function() {
done()
})
}
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)
}
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(', '))
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}
}
})
return self.QueryInterface.bulkInsert(self.tableName, records)
if (errors.length > 0) {
return emitter.emit('error', errors)
}
next()
}
} else {
next()
}
})
}).run()
}
/**
......@@ -630,6 +722,7 @@ module.exports = (function() {
*
* @param {Object} where Options to describe the scope of the search.
* @param {Object} options Possible options are:
- hooks: If set to true, destroy will find all records within the where parameter and will execute before/afterDestroy hooks on each row
- limit: How many rows to delete
- truncate: If set to true, dialects that support it will use TRUNCATE instead of DELETE FROM. If a table is truncated the where and limit options are ignored
* @return {Object} A promise which fires `success`, `error`, `complete` and `sql`.
......@@ -638,14 +731,111 @@ module.exports = (function() {
options = options || {}
options.force = options.force === undefined ? false : Boolean(options.force)
if (this.options.timestamps && this.options.paranoid && options.force === false) {
var attr = Utils._.underscoredIf(this.options.deletedAt, this.options.underscored)
var self = this
, query = null
, args = []
return new Utils.CustomEventEmitter(function(emitter) {
self.runHooks(self.options.hooks.beforeBulkDestroy, where, function(err, newWhere) {
if (!!err) {
return emitter.emit('error', err)
}
where = newWhere || where
if (self.options.timestamps && self.options.paranoid && options.force === false) {
var attr = Utils._.underscoredIf(self.options.deletedAt, self.options.underscored)
var attrValueHash = {}
attrValueHash[attr] = Utils.now()
return this.QueryInterface.bulkUpdate(this.tableName, attrValueHash, where)
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 {
return this.QueryInterface.bulkDelete(this.tableName, where, options)
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()
}
/**
......@@ -656,27 +846,129 @@ module.exports = (function() {
* @return {Object} A promise which fires `success`, `error`, `complete` and `sql`.
*/
DAOFactory.prototype.update = function(attrValueHash, where, options) {
var self = this
, query = null
, tick = 0
options = options || {}
options.validate = options.validate === undefined ? true : Boolean(options.validate)
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks)
if(this.options.timestamps) {
var attr = Utils._.underscoredIf(this.options.updatedAt, this.options.underscored)
if (self.options.timestamps) {
var attr = Utils._.underscoredIf(self.options.updatedAt, self.options.underscored)
attrValueHash[attr] = Utils.now()
}
if (options.validate === true) {
var build = this.build(attrValueHash)
, attrKeys = Object.keys(attrValueHash)
, validate = build.validate({skip: Object.keys(build.dataValues).filter(function(val) { return attrKeys.indexOf(val) !== -1 })})
if (validate !== null && Object.keys(validate).length > 0) {
return new Utils.CustomEventEmitter(function(emitter) {
emitter.emit('error', validate)
}).run()
var runSave = function() {
self.runHooks(self.options.hooks.beforeBulkUpdate, attrValueHash, where, function(err, attributes, _where) {
if (!!err) {
return emitter.emit('error', err)
}
where = _where || where
attrValueHash = attributes || attrValueHash
var runQuery = function(err, records) {
if (!!err) {
return emitter.emit('error', err)
}
query = self.QueryInterface.bulkUpdate(self.tableName, attrValueHash, where, options)
query.on('sql', function(sql) {
emitter.emit('sql', sql)
})
.error(function(err) {
emitter.emit('error', err)
})
.success(function(results) {
var finished = function(err, records) {
if (!!err) {
return emitter.emit('error', err)
}
self.runHooks(self.options.hooks.afterBulkUpdate, attrValueHash, where, function(err) {
if (!!err) {
return emitter.emit('error', err)
}
emitter.emit('success', records)
})
}
if (options && options.hooks === true && !!records && records.length > 0) {
var tick = 0
var next = function(i) {
self.runHooks(self.options.hooks.afterUpdate, records[i], function(err, newValues) {
if (!!err) {
return finished(err)
}
records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues
tick++
if (tick >= records.length) {
return finished(null, records)
}
return this.QueryInterface.bulkUpdate(this.tableName, attrValueHash, where, options)
next(tick)
})
}
next(tick)
} else {
finished(null, results)
}
})
}
if (options.hooks === true) {
self.all({where: where}).error(function(err) { emitter.emit('error', err) })
.success(function(records) {
if (records === null || records.length < 1) {
return runQuery(null)
}
var next = function(i) {
self.runHooks(self.options.hooks.beforeUpdate, records[i], function(err, newValues) {
if (!!err) {
return runQuery(err)
}
records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues
tick++
if (tick >= records.length) {
return runQuery(null, records)
}
next(tick)
})
}
next(tick)
})
} else {
runQuery()
}
})
}
if (options.validate === true) {
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) {
......@@ -700,6 +992,29 @@ module.exports = (function() {
// 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 self = this
Utils._.each(this.options.classMethods || {}, function(fct, name) { self[name] = fct })
......@@ -818,6 +1133,7 @@ module.exports = (function() {
}
Utils._.extend(DAOFactory.prototype, require("./associations/mixin"))
Utils._.extend(DAOFactory.prototype, require(__dirname + '/hooks'))
return DAOFactory
})()
......@@ -18,19 +18,50 @@ DaoValidator.prototype.validate = function() {
return errors
}
DaoValidator.prototype.hookValidate = function() {
var self = this
, errors = {}
return new Utils.CustomEventEmitter(function(emitter) {
self.model.daoFactory.runHooks('beforeValidate', self.model.dataValues, function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
self.model.dataValues = newValues || self.model.dataValues
errors = Utils._.extend(errors, validateAttributes.call(self))
errors = Utils._.extend(errors, validateModel.call(self))
if (Object.keys(errors).length > 0) {
return emitter.emit('error', errors)
}
self.model.daoFactory.runHooks('afterValidate', self.model.dataValues, function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
self.model.dataValues = newValues || self.model.dataValues
emitter.emit('success', self.model)
})
})
}).run()
}
// private
var validateModel = function() {
var errors = {}
var self = this
, errors = {}
// for each model validator for this DAO
Utils._.each(this.model.__options.validate, function(validator, validatorType) {
try {
validator.apply(this.model)
validator.apply(self.model)
} catch (err) {
errors[validatorType] = [err.message] // TODO: data structure needs to change for 2.0
}
}.bind(this))
})
return errors
}
......@@ -54,11 +85,12 @@ var validateAttributes = function() {
}
var validateAttribute = function(value, field) {
var errors = {}
var self = this
, errors = {}
// for each validator
Utils._.each(this.model.validators[field], function(details, validatorType) {
var validator = prepareValidationOfAttribute.call(this, value, details, validatorType)
var validator = prepareValidationOfAttribute.call(self, value, details, validatorType)
try {
validator.fn.apply(null, validator.args)
......@@ -74,7 +106,7 @@ var validateAttribute = function(value, field) {
errors[field] = errors[field] || []
errors[field].push(msg)
}
}.bind(this)) // for each validator for this field
})
return errors
}
......
......@@ -122,25 +122,20 @@ module.exports = (function() {
})
}
var errors = this.validate()
if (!!errors) {
return new Utils.CustomEventEmitter(function(emitter) {
emitter.emit('error', errors)
}).run()
}
for (var attrName in this.daoFactory.rawAttributes) {
if (this.daoFactory.rawAttributes.hasOwnProperty(attrName)) {
var definition = this.daoFactory.rawAttributes[attrName]
, isEnum = definition.type && (definition.type.toString() === DataTypes.ENUM.toString())
self.hookValidate().error(function(err) {
emitter.emit('error', err)
}).success(function() {
for (var attrName in self.daoFactory.rawAttributes) {
if (self.daoFactory.rawAttributes.hasOwnProperty(attrName)) {
var definition = self.daoFactory.rawAttributes[attrName]
, isHstore = !!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type
, hasValue = values[attrName] !== undefined
, isMySQL = this.daoFactory.daoFactoryManager.sequelize.options.dialect === "mysql"
, ciCollation = !!this.daoFactory.options.collate && this.daoFactory.options.collate.match(/_ci$/i)
, valueOutOfScope
, isEnum = definition.type && (definition.type.toString() === DataTypes.ENUM.toString())
, isMySQL = self.daoFactory.daoFactoryManager.sequelize.options.dialect === "mysql"
, ciCollation = !!self.daoFactory.options.collate && self.daoFactory.options.collate.match(/_ci$/i)
if (isEnum && isMySQL && ciCollation && hasValue) {
// Unfortunately for MySQL CI collation we need to map/lowercase values again
if (isEnum && isMySQL && ciCollation && (attrName in values)) {
var scopeIndex = (definition.values || []).map(function(d) { return d.toLowerCase() }).indexOf(values[attrName].toLowerCase())
valueOutOfScope = scopeIndex === -1
......@@ -148,12 +143,6 @@ module.exports = (function() {
if (!valueOutOfScope) {
values[attrName] = definition.values[scopeIndex]
}
} else {
valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1)
}
if (isEnum && hasValue && valueOutOfScope && !(definition.allowNull === true && values[attrName] === null)) {
throw new Error('Value "' + values[attrName] + '" for ENUM ' + attrName + ' is out of allowed scope. Allowed values: ' + definition.values.join(', '))
}
if (isHstore) {
......@@ -164,26 +153,67 @@ module.exports = (function() {
}
}
if (this.__options.timestamps && this.dataValues.hasOwnProperty(updatedAtAttr)) {
this.dataValues[updatedAtAttr] = values[updatedAtAttr] = Utils.now(this.sequelize.options.dialect)
if (self.__options.timestamps && self.dataValues.hasOwnProperty(updatedAtAttr)) {
self.dataValues[updatedAtAttr] = values[updatedAtAttr] = (
(
self.isNewRecord
&& !!self.daoFactory.rawAttributes[updatedAtAttr]
&& !!self.daoFactory.rawAttributes[updatedAtAttr].defaultValue
)
? self.daoFactory.rawAttributes[updatedAtAttr].defaultValue
: Utils.now(self.sequelize.options.dialect))
}
if (this.isNewRecord) {
this.isDirty = false
return this.QueryInterface.insert(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory), values)
var query = null
, args = []
, hook = ''
if (self.isNewRecord) {
self.isDirty = false
query = 'insert'
args = [self, self.QueryInterface.QueryGenerator.addSchema(self.__factory), values]
hook = 'Create'
} else {
var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : { id: this.id };
var identifier = self.__options.hasPrimaryKeys ? self.primaryKeyValues : { id: self.id }
if (identifier === null && this.__options.whereCollection !== null) {
identifier = this.__options.whereCollection;
if (identifier === null && self.__options.whereCollection !== null) {
identifier = self.__options.whereCollection;
}
this.isDirty = false
var tableName = this.QueryInterface.QueryGenerator.addSchema(this.__factory)
, query = this.QueryInterface.update(this, tableName, values, identifier, options)
self.isDirty = false
query = 'update'
args = [self, self.QueryInterface.QueryGenerator.addSchema(self.__factory), values, identifier, options]
hook = 'Update'
}
self.__factory.runHooks('before' + hook, values, function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
return query
// redeclare our new values
args[2] = newValues || args[2]
self.QueryInterface[query].apply(self.QueryInterface, args)
.on('sql', function(sql) {
emitter.emit('sql', sql)
})
.error(function(err) {
emitter.emit('error', err)
})
.success(function(result) {
self.__factory.runHooks('after' + hook, result.values, function(err, newValues) {
if (!!err) {
return emitter.emit('error', err)
}
result.dataValues = newValues
emitter.emit('success', result)
})
})
})
})
}).run()
}
/*
......@@ -231,6 +261,16 @@ module.exports = (function() {
return (Utils._.isEmpty(errors) ? null : errors)
}
/*
* Validate this dao's attribute values according to validation rules set in the dao definition.
*
* @return CustomEventEmitter with null if validation successful; otherwise an object containing { field name : [error msgs] } entries.
*/
DAO.prototype.hookValidate = function(object) {
var validator = new DaoValidator(this, object)
return validator.hookValidate()
}
DAO.prototype.updateAttributes = function(updates, fields) {
this.setAttributes(updates)
......@@ -280,14 +320,41 @@ module.exports = (function() {
options = options || {}
options.force = options.force === undefined ? false : Boolean(options.force)
if (this.__options.timestamps && this.__options.paranoid && options.force === false) {
var attr = Utils._.underscoredIf(this.__options.deletedAt, this.__options.underscored)
this.dataValues[attr] = new Date()
return this.save()
var self = this
, query = null
return new Utils.CustomEventEmitter(function(emitter) {
self.daoFactory.runHooks(self.daoFactory.options.hooks.beforeDestroy, self.dataValues, function(err) {
if (!!err) {
return emitter.emit('error', err)
}
if (self.__options.timestamps && self.__options.paranoid && options.force === false) {
var attr = Utils._.underscoredIf(self.__options.deletedAt, self.__options.underscored)
self.dataValues[attr] = new Date()
query = self.save()
} else {
var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : { id: this.id };
return this.QueryInterface.delete(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory.tableName, this.__factory.options.schema), identifier)
var 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) {
......@@ -422,10 +489,15 @@ module.exports = (function() {
}
if (this.__options.timestamps) {
if (!this.defaultValues[Utils._.underscoredIf(this.__options.createdAt, this.__options.underscored)]) {
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
}
}
......
......@@ -261,20 +261,6 @@ module.exports = (function() {
},
/*
Globally enable foreign key constraints
*/
enableForeignKeyConstraintsQuery: function() {
throwMethodUndefined('enableForeignKeyConstraintsQuery')
},
/*
Globally disable foreign key constraints
*/
disableForeignKeyConstraintsQuery: function() {
throwMethodUndefined('disableForeignKeyConstraintsQuery')
},
/*
Quote an object based on its type. This is a more general version of quoteIdentifiers
Strings: should proxy to quoteIdentifiers
Arrays: First argument should be qouted, second argument should be append without quoting
......@@ -303,6 +289,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)
*/
quoteIdentifier: function(identifier, force) {
......@@ -325,8 +354,29 @@ module.exports = (function() {
} else {
return SqlString.escape(value, false, null, this.dialect, field)
}
}
},
/**
* Generates an SQL query that returns all foreign keys of a table.
*
* @param {String} tableName The name of the table.
* @param {String} schemaName The name of the schema.
* @return {String} The generated sql query.
*/
getForeignKeysQuery: function(tableName, schemaName) {
throwMethodUndefined('getForeignKeysQuery')
},
/**
* Generates an SQL query that removes a foreign key from a table.
*
* @param {String} tableName The name of the table.
* @param {String} foreignKey The name of the foreign key constraint.
* @return {String} The generated sql query.
*/
dropForeignKeyQuery: function(tableName, foreignKey) {
throwMethodUndefined('dropForeignKeyQuery')
}
}
var throwMethodUndefined = function(methodName) {
......
......@@ -260,6 +260,21 @@ module.exports = (function() {
result = result.map(Dot.transform)
} else if (this.options.hasJoin === true) {
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 {
result = results.map(function(result) {
return this.callee.build(result, { isNewRecord: false, isDirty: false })
......
......@@ -253,17 +253,25 @@ module.exports = (function() {
}
var connect = function(done, config) {
var emitter = new (require('events').EventEmitter)()
config = config || this.config
var connection = mysql.createConnection({
var emitter = new (require('events').EventEmitter)()
var connectionConfig = {
host: config.host,
port: config.port,
user: config.username,
password: config.password,
database: config.database,
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) {
if (err) {
......
......@@ -404,10 +404,10 @@ module.exports = (function() {
},
showIndexQuery: function(tableName, options) {
var sql = "SHOW INDEX FROM <%= tableName %><%= options %>"
var sql = "SHOW INDEX FROM `<%= tableName %>`<%= options %>"
return Utils._.template(sql)({
tableName: tableName,
options: (options || {}).database ? ' FROM ' + options.database : ''
options: (options || {}).database ? ' FROM `' + options.database + '`' : ''
})
},
......@@ -589,22 +589,34 @@ module.exports = (function() {
return fields
},
enableForeignKeyConstraintsQuery: function() {
var sql = "SET FOREIGN_KEY_CHECKS = 1;"
return Utils._.template(sql, {})
},
disableForeignKeyConstraintsQuery: function() {
var sql = "SET FOREIGN_KEY_CHECKS = 0;"
return Utils._.template(sql, {})
},
quoteIdentifier: function(identifier, force) {
return Utils.addTicks(identifier, "`")
},
quoteIdentifiers: function(identifiers, force) {
return identifiers.split('.').map(function(v) { return this.quoteIdentifier(v, force) }.bind(this)).join('.')
},
/**
* Generates an SQL query that returns all foreign keys of a table.
*
* @param {String} tableName The name of the table.
* @param {String} schemaName The name of the schema.
* @return {String} The generated sql query.
*/
getForeignKeysQuery: function(tableName, schemaName) {
return "SELECT CONSTRAINT_NAME as constraint_name FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE where TABLE_NAME = '" + tableName + "' AND CONSTRAINT_NAME!='PRIMARY' AND CONSTRAINT_SCHEMA='" + schemaName + "' AND REFERENCED_TABLE_NAME IS NOT NULL;"
},
/**
* Generates an SQL query that removes a foreign key from a table.
*
* @param {String} tableName The name of the table.
* @param {String} foreignKey The name of the foreign key constraint.
* @return {String} The generated sql query.
*/
dropForeignKeyQuery: function(tableName, foreignKey) {
return 'ALTER TABLE ' + this.quoteIdentifier(tableName) + ' DROP FOREIGN KEY ' + this.quoteIdentifier(foreignKey) + ';'
}
}
......
......@@ -145,6 +145,7 @@ module.exports = (function() {
// Closes a client correctly even if we have backed up queries
// https://github.com/brianc/node-postgres/pull/346
this.client.on('drain', this.client.end.bind(this.client))
this.client = null
}
this.isConnecting = false
......
......@@ -15,7 +15,7 @@ module.exports = (function() {
var schema = (!!opts.options && !!opts.options.schema ? opts.options.schema : undefined)
var schemaDelimiter = (!!opts.options && !!opts.options.schemaDelimiter ? opts.options.schemaDelimiter : undefined)
if (!!opts.tableName) {
if (!!opts && !!opts.tableName) {
tableName = opts.tableName
}
else if (typeof opts === "string") {
......@@ -26,7 +26,7 @@ module.exports = (function() {
return tableName
}
return this.quoteIdentifier(schema) + '.' + this.quoteIdentifier(tableName)
return this.quoteIdentifiers((!!schema ? (schema + '.' + tableName) : tableName));
},
createSchema: function(schema) {
......@@ -177,7 +177,7 @@ module.exports = (function() {
if (definition.indexOf('DEFAULT') > 0) {
attrSql += Utils._.template(query)({
tableName: this.quoteIdentifiers(tableName),
query: this.quoteIdentifier(attributeName) + ' SET DEFAULT' + definition.match(/DEFAULT ([^;]+)/)[1]
query: this.quoteIdentifier(attributeName) + ' SET DEFAULT ' + definition.match(/DEFAULT ([^;]+)/)[1]
})
definition = definition.replace(/(DEFAULT[^;]+)/, '').trim()
......@@ -665,10 +665,10 @@ module.exports = (function() {
if(dataType.references) {
template += " REFERENCES <%= referencesTable %> (<%= referencesKey %>)"
replacements.referencesTable = this.quoteIdentifier(dataType.references)
replacements.referencesTable = this.quoteIdentifiers(dataType.references)
if(dataType.referencesKey) {
replacements.referencesKey = this.quoteIdentifier(dataType.referencesKey)
replacements.referencesKey = this.quoteIdentifiers(dataType.referencesKey)
} else {
replacements.referencesKey = this.quoteIdentifier('id')
}
......@@ -714,12 +714,79 @@ module.exports = (function() {
return fields
},
enableForeignKeyConstraintsQuery: function() {
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
})
},
disableForeignKeyConstraintsQuery: function() {
return false // not supported by dialect
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) {
......@@ -739,6 +806,77 @@ module.exports = (function() {
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) {
if (arguments.length === 1) {
options = tableName
......@@ -858,6 +996,28 @@ module.exports = (function() {
quoteIdentifiers: function(identifiers, force) {
return identifiers.split('.').map(function(t) { return this.quoteIdentifier(t, force) }.bind(this)).join('.')
},
/**
* Generates an SQL query that returns all foreign keys of a table.
*
* @param {String} tableName The name of the table.
* @param {String} schemaName The name of the schema.
* @return {String} The generated sql query.
*/
getForeignKeysQuery: function(tableName, schemaName) {
return "SELECT conname as constraint_name, pg_catalog.pg_get_constraintdef(r.oid, true) as condef FROM pg_catalog.pg_constraint r WHERE r.conrelid = (SELECT oid FROM pg_class WHERE relname = '" + tableName + "' LIMIT 1) AND r.contype = 'f' ORDER BY 1;"
},
/**
* Generates an SQL query that removes a foreign key from a table.
*
* @param {String} tableName The name of the table.
* @param {String} foreignKey The name of the foreign key constraint.
* @return {String} The generated sql query.
*/
dropForeignKeyQuery: function(tableName, foreignKey) {
return 'ALTER TABLE ' + this.quoteIdentifier(tableName) + ' DROP CONSTRAINT ' + this.quoteIdentifier(foreignKey) + ';'
}
}
......
......@@ -377,16 +377,6 @@ module.exports = (function() {
return fields
},
enableForeignKeyConstraintsQuery: function() {
var sql = "PRAGMA foreign_keys = ON;"
return Utils._.template(sql, {})
},
disableForeignKeyConstraintsQuery: function() {
var sql = "PRAGMA foreign_keys = OFF;"
return Utils._.template(sql, {})
},
hashToWhereConditions: function(hash) {
for (var key in hash) {
if (hash.hasOwnProperty(key)) {
......@@ -486,6 +476,17 @@ module.exports = (function() {
quoteIdentifiers: function(identifiers, force) {
return identifiers.split('.').map(function(v) { return this.quoteIdentifier(v, force) }.bind(this)).join('.')
},
/**
* Generates an SQL query that returns all foreign keys of a table.
*
* @param {String} tableName The name of the table.
* @param {String} schemaName The name of the schema.
* @return {String} The generated sql query.
*/
getForeignKeysQuery: function(tableName, schemaName) {
return "PRAGMA foreign_key_list(\"" + tableName + "\")"
}
}
......
......@@ -118,5 +118,60 @@ var QueryInterface = module.exports = {
queryAndEmit.call(this, queries.splice(queries.length - 1)[0], methodName, {}, emitter)
}
}.bind(this))
},
dropAllTables: function() {
var self = this
return new Utils.CustomEventEmitter(function(dropAllTablesEmitter) {
var events = []
, chainer = new Utils.QueryChainer()
, onError = function(err) {
self.emit('dropAllTables', err)
dropAllTablesEmitter.emit('error', err)
}
self
.showAllTables()
.error(onError)
.success(function(tableNames) {
self
.sequelize
.query('PRAGMA foreign_keys;')
.proxy(dropAllTablesEmitter, { events: ['sql'] })
.error(onError)
.success(function(result) {
var foreignKeysAreEnabled = result.foreign_keys === 1
if (foreignKeysAreEnabled) {
var queries = []
queries.push('PRAGMA foreign_keys = OFF')
tableNames.forEach(function(tableName) {
queries.push(self.QueryGenerator.dropTableQuery(tableName).replace(';', ''))
})
queries.push('PRAGMA foreign_keys = ON')
QueryInterface.execMultiQuery.call(self, queries, 'dropAllTables', dropAllTablesEmitter, self.queryAndEmit)
} else {
// add the table removal query to the chainer
tableNames.forEach(function(tableName) {
chainer.add(self, 'dropTable', [ tableName, { cascade: true } ])
})
chainer
.runSerially()
.proxy(dropAllTablesEmitter, { events: ['sql'] })
.error(onError)
.success(function() {
self.emit('dropAllTables', null)
dropAllTablesEmitter.emit('success', null)
})
}
})
})
}).run()
}
}
......@@ -139,6 +139,10 @@ module.exports = (function() {
result[_result.name].defaultValue = result[_result.name].defaultValue.replace(/'/g, "")
}
})
} else if (this.sql.indexOf('PRAGMA foreign_keys;') !== -1) {
result = results[0]
} else if (this.sql.indexOf('PRAGMA foreign_keys') !== -1) {
result = results
}
this.emit('success', result)
......
......@@ -2,26 +2,25 @@ var util = require("util")
, EventEmitter = require("events").EventEmitter
, Promise = require("promise")
, proxyEventKeys = ['success', 'error', 'sql']
, tick = (typeof setImmediate !== "undefined" ? setImmediate : process.nextTick)
, Utils = require('../utils')
var bindToProcess = function(fct) {
if (fct) {
if(process.domain) {
return process.domain.bind(fct);
}
if (fct && process.domain) {
return process.domain.bind(fct)
}
return fct;
return fct
};
module.exports = (function() {
var CustomEventEmitter = function(fct) {
this.fct = bindToProcess(fct);
this.fct = bindToProcess(fct)
}
util.inherits(CustomEventEmitter, EventEmitter)
CustomEventEmitter.prototype.run = function() {
process.nextTick(function() {
tick(function() {
if (this.fct) {
this.fct.call(this, this)
}
......@@ -54,25 +53,40 @@ module.exports = (function() {
return this
}
CustomEventEmitter.prototype.sql =
function(fct) {
CustomEventEmitter.prototype.sql = function(fct) {
this.on('sql', bindToProcess(fct))
return this;
}
CustomEventEmitter.prototype.proxy = function(emitter) {
proxyEventKeys.forEach(function (eventKey) {
/**
* Proxy every event of this custom event emitter to another one.
*
* @param {CustomEventEmitter} emitter The event emitter that should receive the events.
* @return {void}
*/
CustomEventEmitter.prototype.proxy = function(emitter, options) {
options = Utils._.extend({
events: proxyEventKeys,
skipEvents: []
}, options || {})
options.events = Utils._.difference(options.events, options.skipEvents)
options.events.forEach(function (eventKey) {
this.on(eventKey, function (result) {
emitter.emit(eventKey, result)
})
}.bind(this))
return this
}
CustomEventEmitter.prototype.then =
function (onFulfilled, onRejected) {
CustomEventEmitter.prototype.then = function(onFulfilled, onRejected) {
var self = this
onFulfilled = bindToProcess(onFulfilled)
onRejected = bindToProcess(onRejected)
return new Promise(function (resolve, reject) {
self.on('error', reject)
.on('success', resolve);
......
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)
}
......@@ -223,35 +223,46 @@ module.exports = (function() {
QueryInterface.prototype.dropAllTables = function() {
var self = this
return new Utils.CustomEventEmitter(function(emitter) {
var chainer = new Utils.QueryChainer()
if (this.sequelize.options.dialect === 'sqlite') {
// sqlite needs some special treatment as it cannot drop a column
return SQLiteQueryInterface.dropAllTables.call(this)
} else {
return new Utils.CustomEventEmitter(function(dropAllTablesEmitter) {
var events = []
, chainer = new Utils.QueryChainer()
, onError = function(err) {
self.emit('dropAllTables', err)
dropAllTablesEmitter.emit('error', err)
}
self.showAllTables().success(function(tableNames) {
chainer.add(self, 'disableForeignKeyConstraints', [])
self.getForeignKeysForTables(tableNames).success(function(foreignKeys) {
tableNames.forEach(function(tableName) {
chainer.add(self, 'dropTable', [tableName, {cascade: true}])
// add the foreign key removal query to the chainer
Object.keys(foreignKeys).forEach(function(tableName) {
foreignKeys[tableName].forEach(function(foreignKey) {
var sql = self.QueryGenerator.dropForeignKeyQuery(tableName, foreignKey)
chainer.add(self.sequelize, 'query', [ sql ])
})
})
chainer.add(self, 'enableForeignKeyConstraints', [])
// add the table removal query to the chainer
tableNames.forEach(function(tableName) {
chainer.add(self, 'dropTable', [ tableName, { cascade: true } ])
})
chainer
.runSerially()
.success(function() {
self.emit('dropAllTables', null)
emitter.emit('success', null)
})
.error(function(err) {
self.emit('dropAllTables', err)
emitter.emit('error', err)
})
}).error(function(err) {
self.emit('dropAllTables', err)
emitter.emit('error', err)
dropAllTablesEmitter.emit('success', null)
})
.error(onError)
}).error(onError)
}).error(onError)
}).run()
}
}
QueryInterface.prototype.renameTable = function(before, after) {
var sql = this.QueryGenerator.renameTableQuery(before, after)
......@@ -263,6 +274,7 @@ module.exports = (function() {
return new Utils.CustomEventEmitter(function(emitter) {
var showTablesSql = self.QueryGenerator.showTablesQuery()
self.sequelize.query(showTablesSql, null, { raw: true }).success(function(tableNames) {
self.emit('showAllTables', null)
emitter.emit('success', Utils._.flatten(tableNames))
......@@ -397,6 +409,35 @@ module.exports = (function() {
return queryAndEmit.call(this, sql, 'showIndex')
}
QueryInterface.prototype.getForeignKeysForTables = function(tableNames) {
var self = this
return new Utils.CustomEventEmitter(function(emitter) {
if (tableNames.length === 0) {
emitter.emit('success', {})
} else {
var chainer = new Utils.QueryChainer()
tableNames.forEach(function(tableName) {
var sql = self.QueryGenerator.getForeignKeysQuery(tableName, self.sequelize.config.database)
chainer.add(self.sequelize, 'query', [sql])
})
chainer.runSerially().proxy(emitter, {
skipEvents: ['success']
}).success(function(results) {
var result = {}
tableNames.forEach(function(tableName, i) {
result[tableName] = Utils._.compact(results[i]).map(function(r) { return r.constraint_name })
})
emitter.emit('success', result)
})
}
}).run()
}
QueryInterface.prototype.removeIndex = function(tableName, indexNameOrAttributes) {
var sql = this.QueryGenerator.removeIndexQuery(tableName, indexNameOrAttributes)
return queryAndEmit.call(this, sql, "removeIndex")
......@@ -434,13 +475,12 @@ module.exports = (function() {
return new Utils.CustomEventEmitter(function(emitter) {
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('success', results[1])
emitter.emit('success', results[0])
emitter.emit('sql', sql)
})
.error(function(err) {
......@@ -461,7 +501,6 @@ module.exports = (function() {
return new Utils.CustomEventEmitter(function(emitter) {
var chainer = new Utils.QueryChainer()
chainer.add(self, 'enableForeignKeyConstraints', [])
chainer.add(self, 'queryAndEmit', [sql, 'bulkUpdate'])
return chainer.runSerially()
......@@ -481,6 +520,7 @@ module.exports = (function() {
QueryInterface.prototype.delete = function(dao, tableName, identifier) {
var self = this
, restrict = false
, cascades = []
, sql = self.QueryGenerator.deleteQuery(tableName, identifier, null, dao.daoFactory)
// Check for a restrict field
......@@ -489,16 +529,63 @@ module.exports = (function() {
, length = keys.length
for (var i = 0; i < length; i++) {
if (dao.daoFactory.associations[keys[i]].options && dao.daoFactory.associations[keys[i]].options.onDelete && dao.daoFactory.associations[keys[i]].options.onDelete === "restrict") {
if (dao.daoFactory.associations[keys[i]].options && dao.daoFactory.associations[keys[i]].options.onDelete) {
if (dao.daoFactory.associations[keys[i]].options.onDelete === "restrict") {
restrict = true
}
else if (dao.daoFactory.associations[keys[i]].options.onDelete === "cascade" && dao.daoFactory.associations[keys[i]].options.useHooks === true) {
cascades[cascades.length] = dao.daoFactory.associations[keys[i]].accessors.get
}
}
}
}
return new Utils.CustomEventEmitter(function(emitter) {
var tick = 0
var iterate = function(err, i) {
if (!!err || i >= cascades.length) {
return run(err)
}
dao[cascades[i]]().success(function(tasks) {
if (tasks === null || tasks.length < 1) {
return run()
}
tasks = Array.isArray(tasks) ? tasks : [tasks]
var ii = 0
var next = function(err, ii) {
if (!!err || ii >= tasks.length) {
return iterate(err)
}
tasks[ii].destroy().error(function(err) {
return iterate(err)
})
.success(function() {
ii++
if (ii >= tasks.length) {
tick++
return iterate(null, tick)
}
next(null, ii)
})
}
next(null, ii)
})
}
var run = function(err) {
if (!!err) {
return emitter.emit('error', err)
}
var chainer = new Utils.QueryChainer()
chainer.add(self, 'enableForeignKeyConstraints', [])
chainer.add(self, 'queryAndEmit', [[sql, dao], 'delete'])
chainer.runSerially()
......@@ -512,6 +599,13 @@ module.exports = (function() {
emitter.emit('sql', sql)
emitter.emit('error', err)
})
}
if (cascades.length > 0) {
iterate(null, tick)
} else {
run()
}
}).run()
}
......@@ -522,7 +616,6 @@ module.exports = (function() {
return new Utils.CustomEventEmitter(function(emitter) {
var chainer = new Utils.QueryChainer()
chainer.add(self, 'enableForeignKeyConstraints', [])
chainer.add(self, 'queryAndEmit', [sql, 'bulkDelete', options])
chainer.runSerially()
......@@ -604,25 +697,75 @@ module.exports = (function() {
}).run()
}
QueryInterface.prototype.enableForeignKeyConstraints = function() {
var sql = this.QueryGenerator.enableForeignKeyConstraintsQuery()
if(sql) {
return queryAndEmit.call(this, sql, 'enableForeignKeyConstraints')
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('enableForeignKeyConstraints', null)
this.emit('dropFunction', null)
emitter.emit('success')
}).run()
}
}
QueryInterface.prototype.disableForeignKeyConstraints = function() {
var sql = this.QueryGenerator.disableForeignKeyConstraintsQuery()
if(sql){
return queryAndEmit.call(this, sql, 'disableForeignKeyConstraints')
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('disableForeignKeyConstraints', null)
this.emit('renameFunction', null)
emitter.emit('success')
}).run()
}
......
......@@ -103,7 +103,8 @@ module.exports = (function() {
native : this.options.native,
replication: this.options.replication,
dialectModulePath: this.options.dialectModulePath,
maxConcurrentQueries: this.options.maxConcurrentQueries
maxConcurrentQueries: this.options.maxConcurrentQueries,
dialectOptions: this.options.dialectOptions,
}
try {
......@@ -160,17 +161,8 @@ module.exports = (function() {
Sequelize.prototype.define = function(daoName, attributes, options) {
options = options || {}
var globalOptions = this.options
// If you don't specify a valid data type lets help you debug it
Utils._.each(attributes, function(dataType, name){
if (Utils.isHash(dataType)) {
dataType = dataType.type
}
if (dataType === undefined) {
throw new Error('Unrecognized data type for field '+ name)
}
})
var self = this
, globalOptions = this.options
if (globalOptions.define) {
options = Utils._.extend({}, globalOptions.define, options)
......@@ -185,6 +177,40 @@ module.exports = (function() {
options.omitNull = globalOptions.omitNull
options.language = globalOptions.language
// If you don't specify a valid data type lets help you debug it
Utils._.each(attributes, function(dataType, name) {
if (Utils.isHash(dataType)) {
dataType = dataType.type
}
if (dataType === undefined) {
throw new Error('Unrecognized data type for field '+ name)
}
if (dataType.toString() === "ENUM") {
attributes[name].validate = attributes[name].validate || {
_checkEnum: function(value) {
var hasValue = value !== undefined
, isMySQL = self.options.dialect === "mysql"
, ciCollation = !!options.collate && options.collate.match(/_ci$/i) !== null
, valueOutOfScope
if (isMySQL && ciCollation && hasValue) {
var scopeIndex = (attributes[name].values || []).map(function(d) { return d.toLowerCase() }).indexOf(value.toLowerCase())
valueOutOfScope = scopeIndex === -1
} else {
valueOutOfScope = ((attributes[name].values || []).indexOf(value) === -1)
}
if (hasValue && valueOutOfScope && !(attributes[name].allowNull === true && values[attrName] === null)) {
throw new Error('Value "' + value + '" for ENUM ' + name + ' is out of allowed scope. Allowed values: ' + attributes[name].values.join(', '))
}
}
}
}
})
// if you call "define" multiple times for the same daoName, do not clutter the factory
if(this.isDefined(daoName)) {
this.daoFactoryManager.removeDAO(this.daoFactoryManager.getDAO(daoName))
......
......@@ -36,7 +36,7 @@
"url": "https://github.com/sequelize/sequelize/issues"
},
"dependencies": {
"lodash": "~2.0.0",
"lodash": "~2.2.0",
"underscore.string": "~2.3.0",
"lingo": "~0.0.5",
"validator": "~1.5.0",
......@@ -46,15 +46,15 @@
"toposort-class": "~0.2.0",
"generic-pool": "2.0.4",
"promise": "~3.2.0",
"sql": "~0.26.0"
"sql": "~0.28.0"
},
"devDependencies": {
"sqlite3": "~2.1.12",
"mysql": "~2.0.0-alpha8",
"mysql": "~2.0.0-alpha9",
"pg": "~2.6.0",
"watchr": "~2.4.3",
"yuidocjs": "~0.3.36",
"chai": "~1.7.2",
"chai": "~1.8.0",
"mocha": "~1.13.0",
"chai-datetime": "~1.1.1",
"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);
}
}
......@@ -28,12 +28,9 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
describe('hasSingle', function() {
beforeEach(function(done) {
var self = this
this.Article = this.sequelize.define('Article', {
'title': DataTypes.STRING
})
this.Label = this.sequelize.define('Label', {
'text': DataTypes.STRING
})
this.Article = this.sequelize.define('Article', { 'title': DataTypes.STRING })
this.Label = this.sequelize.define('Label', { 'text': DataTypes.STRING })
this.Article.hasMany(this.Label)
......@@ -580,6 +577,201 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
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() {
......
module.exports = {
username: "root",
password: null,
database: 'sequelize_test',
host: '127.0.0.1',
pool: { maxConnections: 5, maxIdleTime: 30000},
username: process.env.SEQ_USER || "root",
password: process.env.SEQ_PW || null,
database: process.env.SEQ_DB || 'sequelize_test',
host: process.env.SEQ_HOST || '127.0.0.1',
pool: {
maxConnections: process.env.SEQ_POOL_MAX || 5,
maxIdleTime: process.env.SEQ_POOL_IDLE || 30000
},
rand: function() {
return parseInt(Math.random() * 999, 10)
......@@ -11,21 +14,29 @@ module.exports = {
//make maxIdleTime small so that tests exit promptly
mysql: {
username: "root",
password: null,
database: 'sequelize_test',
host: '127.0.0.1',
port: 3306,
pool: { maxConnections: 5, maxIdleTime: 30}
database: process.env.SEQ_MYSQL_DB || process.env.SEQ_DB || 'sequelize_test',
username: process.env.SEQ_MYSQL_USER || process.env.SEQ_USER || "root",
password: process.env.SEQ_MYSQL_PW || process.env.SEQ_PW || null,
host: process.env.SEQ_MYSQL_HOST || process.env.SEQ_HOST || '127.0.0.1',
port: process.env.SEQ_MYSQL_PORT || process.env.SEQ_PORT || 3306,
pool: {
maxConnections: process.env.SEQ_MYSQL_POOL_MAX || process.env.SEQ_POOL_MAX || 5,
maxIdleTime: process.env.SEQ_MYSQL_POOL_IDLE || process.env.SEQ_POOL_IDLE || 30
}
},
sqlite: {
},
postgres: {
database: 'sequelize_test',
username: "postgres",
port: 5432,
pool: { maxConnections: 5, maxIdleTime: 3000}
database: process.env.SEQ_PG_DB || process.env.SEQ_DB || 'sequelize_test',
username: process.env.SEQ_PG_USER || process.env.SEQ_USER || "postgres",
password: process.env.SEQ_PG_PW || process.env.SEQ_PW || null,
host: process.env.SEQ_PG_HOST || process.env.SEQ_HOST || '127.0.0.1',
port: process.env.SEQ_PG_PORT || process.env.SEQ_PORT || 5432,
pool: {
maxConnections: process.env.SEQ_PG_POOL_MAX || process.env.SEQ_POOL_MAX || 5,
maxIdleTime: process.env.SEQ_PG_POOL_IDLE || process.env.SEQ_POOL_IDLE || 3000
}
}
}
......@@ -101,14 +101,23 @@ describe(Support.getTestDialectTeaser("Configuration"), function() {
})
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
expect(config.database).to.equal('dbname')
expect(config.username).to.equal('root')
expect(config.password).to.equal('pass')
expect(config.port).to.equal(999)
expect(config.dialectOptions.supportBigNumbers).to.be.true
expect(config.dialectOptions.bigNumberStrings).to.be.true
done()
})
})
})
......@@ -69,6 +69,23 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
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) {
var self = this
expect(function() {
......@@ -108,6 +125,39 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
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) {
var UserTable = this.sequelize.define('UserCol', {
aNumber: Sequelize.INTEGER
......@@ -197,7 +247,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
done()
})
it("stores the the passed values in a special variable", function(done) {
it("stores the passed values in a special variable", function(done) {
var user = this.User.build({ username: 'John Wayne' })
expect(user.selectedValues).to.deep.equal({ username: 'John Wayne' })
done()
......@@ -279,7 +329,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
describe('findOrCreate', function () {
it("Returns instace if already existent. Single find field.", function(done) {
it("Returns instance if already existent. Single find field.", function(done) {
var self = this,
data = {
username: 'Username'
......@@ -297,7 +347,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
it("Returns instace if already existent. Multiple find fields.", function(done) {
it("Returns instance if already existent. Multiple find fields.", function(done) {
var self = this,
data = {
username: 'Username',
......@@ -385,7 +435,8 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
}
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()
})
})
......@@ -615,7 +666,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
it('can omitt autoincremental columns', function(done) {
it('can omit autoincremental columns', function(done) {
var self = this
, data = { title: 'Iliad' }
, dataTypes = [Sequelize.INTEGER, Sequelize.BIGINT]
......@@ -1059,7 +1110,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
describe('destroy', function() {
it('deletes a record from the database if dao is not paranoid', function(done) {
var UserDestroy = this.sequelize.define('UserDestory', {
var UserDestroy = this.sequelize.define('UserDestroy', {
name: Sequelize.STRING,
bio: Sequelize.TEXT
})
......@@ -1119,6 +1170,8 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
it('sets deletedAt to the current timestamp if paranoid is true', function(done) {
var self = this
, ident = self.sequelize.queryInterface.QueryGenerator.quoteIdentifier
, escape = self.sequelize.queryInterface.QueryGenerator.quote
, ParanoidUser = self.sequelize.define('ParanoidUser', {
username: Sequelize.STRING,
secretValue: Sequelize.STRING,
......@@ -1133,24 +1186,62 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
ParanoidUser.sync({ force: true }).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.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[2].username).to.equal("Bob")
expect(parseInt(+users[0].deletedAt/5000, 10)).to.equal(date)
expect(parseInt(+users[1].deletedAt/5000, 10)).to.equal(date)
if (dialect === "sqlite") {
expect(moment(users[0].deletedAt).format('YYYY-MM-DD h:mm')).to.equal(date)
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()
})
})
})
})
})
})
describe("can't find records marked as deleted with paranoid being true", function() {
it('with the DAOFactory', function(done) {
var User = this.sequelize.define('UserCol', {
username: Sequelize.STRING
}, { paranoid: true })
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()
})
})
})
})
})
})
})
})
})
it('should delete a paranoid record if I set force to true', function(done) {
var self = this
......@@ -1357,7 +1448,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
it('should be able to retun a record with primaryKey being null for new inserts', function(done) {
it('should be able to return a record with primaryKey being null for new inserts', function(done) {
var Session = this.sequelize.define('Session', {
token: { type: DataTypes.TEXT, allowNull: false },
lastUpdate: { type: DataTypes.DATE, allowNull: false }
......@@ -2736,6 +2827,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
it("handles offset and limit", function(done) {
var self = this
this.User.bulkCreate([{username: 'bobby'}, {username: 'tables'}]).success(function() {
self.User.findAll({ limit: 2, offset: 2 }).success(function(users) {
expect(users.length).to.equal(2)
......@@ -2823,6 +2915,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
done()
})
})
it("handles attributes", function(done) {
this.User.findAndCountAll({where: "id != " + this.users[0].id, attributes: ['data']}).success(function(info) {
expect(info.count).to.equal(2)
......@@ -3441,6 +3534,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){
var self = this
......@@ -3483,17 +3614,24 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
describe('references', function() {
this.timeout(3000)
beforeEach(function(done) {
this.Author = this.sequelize.define('author', { firstName: Sequelize.STRING })
this.Author.sync({ force: true }).success(function() {
var self = this
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().success(function() {
done()
})
})
})
})
afterEach(function(done) {
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() {
done()
})
......@@ -3514,14 +3652,13 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
this.Author.hasMany(Post)
Post.belongsTo(this.Author)
// The posts table gets dropped in the before filter.
Post.sync().on('sql', function(sql) {
if (dialect === 'postgres') {
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`\)/)
}
else if (dialect === 'sqlite') {
} else if (dialect === 'sqlite') {
expect(sql).to.match(/`authorId` INTEGER REFERENCES `authors` \(`id`\)/)
} else {
throw new Error('Undefined dialect!')
......@@ -3545,14 +3682,13 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
this.Author.hasMany(Post)
Post.belongsTo(this.Author)
// The posts table gets dropped in the before filter.
Post.sync().on('sql', function(sql) {
if (dialect === 'postgres') {
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`\)/)
}
else if (dialect === 'sqlite') {
} else if (dialect === 'sqlite') {
expect(sql).to.match(/`authorId` INTEGER REFERENCES `authors` \(`id`\)/)
} else {
throw new Error('Undefined dialect!')
......@@ -3576,6 +3712,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
this.Author.hasMany(Post)
Post.belongsTo(this.Author)
// The posts table gets dropped in the before filter.
Post.sync().success(function() {
if (dialect === 'sqlite') {
// sorry ... but sqlite is too stupid to understand whats going on ...
......@@ -3589,12 +3726,10 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
}).error(function(err) {
if (dialect === 'mysql') {
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
expect(1).to.equal(2)
}
else if (dialect === 'postgres') {
} else if (dialect === 'postgres') {
expect(err.message).to.match(/relation "4uth0r5" does not exist/)
} else {
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() {
logging: function(){}
}, options || {})
// this.sequelize.options.logging = console.log
//this.sequelize.options.logging = console.log
var migrator = new Migrator(this.sequelize, options)
migrator
......@@ -28,7 +28,7 @@ describe(Support.getTestDialectTeaser("Migrator"), function() {
describe('getUndoneMigrations', function() {
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) {
expect(err).to.be.null
expect(migrations.length).to.equal(0)
......@@ -86,7 +86,7 @@ describe(Support.getTestDialectTeaser("Migrator"), function() {
SequelizeMeta.create({ from: null, to: 20111117063700 }).success(function() {
migrator.getUndoneMigrations(function(err, migrations) {
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')
done()
})
......@@ -286,7 +286,8 @@ describe(Support.getTestDialectTeaser("Migrator"), function() {
})
})
})
})
})
describe('renameColumn', function() {
it("renames the signature column from user to sig", function(done) {
......@@ -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,19 +122,18 @@ if (dialect.match(/^mysql/)) {
})
User.sync({ force: true }).success(function() {
expect(function() {
User.create({mood: 'happy'})
}).to.throw(Error, 'Value "happy" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver')
expect(function() {
User.create({mood: 'happy'}).error(function(err) {
expect(err).to.deep.equal({ mood: [ 'Value "happy" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver' ] })
var u = User.build({mood: 'SAD'})
u.save()
}).to.throw(Error, 'Value "SAD" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver')
u.save().error(function(err) {
expect(err).to.deep.equal({ mood: [ 'Value "SAD" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver' ] })
done()
})
})
})
})
})
})
describe('primaryKeys', function() {
it("determines the correct primaryKeys", function(done) {
......
......@@ -450,10 +450,10 @@ if (dialect.match(/^mysql/)) {
showIndexQuery: [
{
arguments: ['User'],
expectation: 'SHOW INDEX FROM User'
expectation: 'SHOW INDEX FROM `User`'
}, {
arguments: ['User', { database: 'sequelize' }],
expectation: "SHOW INDEX FROM User FROM sequelize"
expectation: "SHOW INDEX FROM `User` FROM `sequelize`"
}
],
......
......@@ -435,14 +435,13 @@ describe(Support.getTestDialectTeaser("Sequelize"), function () {
})
it("doesn't save an instance if value is not in the range of enums", function(done) {
var self = this
expect(function() {
self.Review.create({ status: 'fnord' })
}).to.throw(Error, 'Value "fnord" for ENUM status is out of allowed scope. Allowed values: scheduled, active, finished')
this.Review.create({status: 'fnord'}).error(function(err) {
expect(err).to.deep.equal({ status: [ 'Value "fnord" for ENUM status is out of allowed scope. Allowed values: scheduled, active, finished' ] })
done()
})
})
})
})
describe('table', function() {
[
......
var fs = require('fs')
, Sequelize = require(__dirname + "/../index")
, DataTypes = require(__dirname + "/../lib/data-types")
, config = require(__dirname + "/config/config")
, Config = require(__dirname + "/config/config")
var Support = {
Sequelize: Sequelize,
......@@ -26,16 +26,19 @@ var Support = {
createSequelizeInstance: function(options) {
options = options || {}
options.dialect = options.dialect || 'mysql'
var config = Config[options.dialect]
options.logging = (options.hasOwnProperty('logging') ? options.logging : false)
options.pool = options.pool || config.pool
var sequelizeOptions = {
logging: options.logging,
dialect: options.dialect,
port: options.port || process.env.SEQ_PORT || config[options.dialect].port,
pool: options.pool
port: options.port || process.env.SEQ_PORT || config.port,
pool: options.pool,
dialectOptions: options.dialectOptions || {}
}
if (!!options.host) {
......@@ -50,16 +53,11 @@ var Support = {
sequelizeOptions.native = true
}
return this.getSequelizeInstance(
process.env.SEQ_DB || config[options.dialect].database,
process.env.SEQ_USER || process.env.SEQ_USERNAME || config[options.dialect].username,
process.env.SEQ_PW || process.env.SEQ_PASSWORD || config[options.dialect].password,
sequelizeOptions
)
return this.getSequelizeInstance(config.database, config.username, config.password, sequelizeOptions)
},
getSequelizeInstance: function(db, user, pass, options) {
options = options || {};
options = options || {}
options.dialect = options.dialect || this.getTestDialect()
return new Sequelize(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!