diff --git a/.gitignore b/.gitignore index 4bca10ce6f6b..ac8267131349 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ npm-debug.log *~ test/binary/tmp/* test/tmp/* +test/sqlite/test.sqlite coverage-* diff --git a/README.md b/README.md index 25615d4a5afd..fa1736f40c7f 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ A very basic roadmap. Chances aren't too bad, that not mentioned things are impl ### 1.7.0 - ~~Check if lodash is a proper alternative to current underscore usage.~~ -- Transactions +- ~~Transactions~~ - Associations of not yet saved objects: [#864](https://github.com/sequelize/sequelize/issues/864) - Support for update of tables without primary key - ~~MariaDB support~~ diff --git a/changelog.md b/changelog.md index 48ae95c1ca5a..fbb0c56e5777 100644 --- a/changelog.md +++ b/changelog.md @@ -86,6 +86,7 @@ - [FEATURE] Support for MariaDB. [#948](https://github.com/sequelize/sequelize/pull/948). Thanks to reedog117 and janmeier. - [FEATURE] Filter through associations. [#991](https://github.com/sequelize/sequelize/pull/991). Thanks to snit-ram. - [FEATURE] Possibility to disable loging for .sync [#937](https://github.com/sequelize/sequelize/pull/937). Thanks to durango +- [FEATURE] Support for transactions. [1062](https://github.com/sequelize/sequelize/pull/1062). - [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 diff --git a/lib/associations/belongs-to.js b/lib/associations/belongs-to.js index 2965ccfb2a66..4dc815d0afc8 100644 --- a/lib/associations/belongs-to.js +++ b/lib/associations/belongs-to.js @@ -45,8 +45,9 @@ module.exports = (function() { , primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id' obj[accessor] = function(params) { - var id = this[self.identifier] - , where = {} + var id = this[self.identifier] + , where = {} + , options = Utils._.pick(params || {}, 'transaction') where[primaryKey] = id @@ -60,7 +61,7 @@ module.exports = (function() { params = id } - return self.target.find(params) + return self.target.find(params, options) } return this @@ -70,14 +71,15 @@ module.exports = (function() { var self = this , accessor = Utils._.camelize('set_' + (this.options.as || Utils.singularize(this.target.tableName, this.target.options.language))) - obj[accessor] = function(associatedObject) { + obj[accessor] = function(associatedObject, options) { var primaryKeys = !!associatedObject && !!associatedObject.daoFactory ? Object.keys(associatedObject.daoFactory.primaryKeys) : [] , primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id' this[self.identifier] = associatedObject ? associatedObject[primaryKey] : null + options = Utils._.extend({ fields: [ self.identifier ], allowNull: [self.identifier] }, options) // passes the changed field to save, so only that field get updated. - return this.save([ self.identifier ], {allowNull: [self.identifier]}) + return this.save(options) } return this diff --git a/lib/associations/has-many-double-linked.js b/lib/associations/has-many-double-linked.js index 8398ed6b356c..9fc21900a970 100644 --- a/lib/associations/has-many-double-linked.js +++ b/lib/associations/has-many-double-linked.js @@ -1,4 +1,5 @@ -var Utils = require('./../utils') +var Utils = require('./../utils') + , Transaction = require('./../transaction') module.exports = (function() { var HasManyDoubleLinked = function(definition, instance) { @@ -48,14 +49,14 @@ module.exports = (function() { if (options.joinTableAttributes) { options.joinTableAttributes.forEach(function (elem) { options.attributes.push( - self.QueryInterface.quoteIdentifiers(connectorDAO.tableName + '.' + elem) + ' as ' + + 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.quoteIdentifiers(connectorDAO.tableName + '.' + key) + ' as ' + self.QueryInterface.quoteIdentifier(connectorDAO.name + '.' + key, true) ) }) @@ -90,16 +91,22 @@ module.exports = (function() { } 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) + 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 = [] + , changedAssociations = [] + , options = {} , unassociatedObjects; + if ((defaultAttributes || {}).transaction instanceof Transaction) { + options.transaction = defaultAttributes.transaction + delete defaultAttributes.transaction + } + unassociatedObjects = newAssociations.filter(function (obj) { return !Utils._.find(oldAssociations, function (old) { return (!!obj[foreignIdentifier] && !!old[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : obj.id === old.id) @@ -121,7 +128,7 @@ module.exports = (function() { changedAssociation.where[self.__factory.identifier] = self.instance[self.__factory.identifier] || self.instance.id changedAssociation.where[foreignIdentifier] = newObj[foreignIdentifier] || newObj.id - + changedAssociations.push(changedAssociation) } }) @@ -132,31 +139,33 @@ module.exports = (function() { }) var where = {} + where[self.__factory.identifier] = ((sourceKeys.length === 1) ? self.instance[sourceKeys[0]] : self.instance.id) where[foreignIdentifier] = foreignIds - chainer.add(self.__factory.connectorDAO.destroy(where)) + chainer.add(self.__factory.connectorDAO.destroy(where, options)) } if (unassociatedObjects.length > 0) { var bulk = unassociatedObjects.map(function(unassociatedObject) { var attributes = {} + 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)) + chainer.add(self.__factory.connectorDAO.bulkCreate(bulk, options)) } if (changedAssociations.length > 0) { changedAssociations.forEach(function (assoc) { - chainer.add(self.__factory.connectorDAO.update(assoc.attributes, assoc.where)) + chainer.add(self.__factory.connectorDAO.update(assoc.attributes, assoc.where, options)) }) } @@ -191,7 +200,7 @@ module.exports = (function() { 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) }) + .on('sql', function(sql) { emitterProxy.emit('sql', sql) }) } } diff --git a/lib/associations/has-many-single-linked.js b/lib/associations/has-many-single-linked.js index 1105f0d95466..7733ffa3b2fd 100644 --- a/lib/associations/has-many-single-linked.js +++ b/lib/associations/has-many-single-linked.js @@ -1,4 +1,5 @@ -var Utils = require('./../utils') +var Utils = require('./../utils') + , Transaction = require('./../transaction') module.exports = (function() { var HasManySingleLinked = function(definition, instance) { @@ -29,23 +30,29 @@ module.exports = (function() { return this.__factory.target.all(options) } - HasManySingleLinked.prototype.injectSetter = function(emitter, oldAssociations, newAssociations) { - var self = this - , associationKeys = Object.keys((oldAssociations[0] || newAssociations[0] || {daoFactory: {primaryKeys: {}}}).daoFactory.primaryKeys || {}) - , associationKey = associationKeys.length === 1 ? associationKeys[0] : 'id' - , chainer = new Utils.QueryChainer() + HasManySingleLinked.prototype.injectSetter = function(emitter, oldAssociations, newAssociations, defaultAttributes) { + var self = this + , associationKeys = Object.keys((oldAssociations[0] || newAssociations[0] || {daoFactory: {primaryKeys: {}}}).daoFactory.primaryKeys || {}) + , associationKey = (associationKeys.length === 1) ? associationKeys[0] : 'id' + , chainer = new Utils.QueryChainer() + , options = {} , obsoleteAssociations = oldAssociations.filter(function (old) { return !Utils._.find(newAssociations, function (obj) { return obj[associationKey] === old[associationKey] }) }) - , unassociatedObjects = newAssociations.filter(function (obj) { + , unassociatedObjects = newAssociations.filter(function (obj) { return !Utils._.find(oldAssociations, function (old) { return obj[associationKey] === old[associationKey] }) }) , update + if ((defaultAttributes || {}).transaction instanceof Transaction) { + options.transaction = defaultAttributes.transaction + delete defaultAttributes.transaction + } + if (obsoleteAssociations.length > 0) { // clear the old associations var obsoleteIds = obsoleteAssociations.map(function(associatedObject) { @@ -55,21 +62,26 @@ module.exports = (function() { update = {} update[self.__factory.identifier] = null + var primaryKeys = Object.keys(this.__factory.target.primaryKeys) - , primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id' + , primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id' , updateWhere = {} updateWhere[primaryKey] = obsoleteIds - chainer.add(this.__factory.target.update(update, updateWhere, {allowNull: [self.__factory.identifier]})) + chainer.add(this.__factory.target.update( + update, + updateWhere, + Utils._.extend(options, { allowNull: [self.__factory.identifier] }) + )) } if (unassociatedObjects.length > 0) { // For the self.instance - var pkeys = Object.keys(self.instance.daoFactory.primaryKeys) - , pkey = pkeys.length === 1 ? pkeys[0] : 'id' + var pkeys = Object.keys(self.instance.daoFactory.primaryKeys) + , pkey = pkeys.length === 1 ? pkeys[0] : 'id' // For chainer , primaryKeys = Object.keys(this.__factory.target.primaryKeys) - , primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id' + , primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id' , updateWhere = {} // set the new associations @@ -78,10 +90,15 @@ module.exports = (function() { return associatedObject[associationKey] }) - update = {} + update = {} update[self.__factory.identifier] = (newAssociations.length < 1 ? null : self.instance[pkey] || self.instance.id) - updateWhere[primaryKey] = unassociatedIds - chainer.add(this.__factory.target.update(update, updateWhere, {allowNull: [self.__factory.identifier]})) + updateWhere[primaryKey] = unassociatedIds + + chainer.add(this.__factory.target.update( + update, + updateWhere, + Utils._.extend(options, { allowNull: [self.__factory.identifier] }) + )) } chainer diff --git a/lib/associations/has-many.js b/lib/associations/has-many.js index 4b68f51d1288..50c6cadd0e9c 100644 --- a/lib/associations/has-many.js +++ b/lib/associations/has-many.js @@ -124,30 +124,30 @@ module.exports = (function() { return new Class(self, this).injectGetter(options) } - obj[this.accessors.hasAll] = function(objects) { + obj[this.accessors.hasAll] = function(objects, options) { var instance = this; var customEventEmitter = new Utils.CustomEventEmitter(function() { - instance[self.accessors.get]() - .error(function(err){ customEventEmitter.emit('error', err)}) - .success(function(associatedObjects) { - customEventEmitter.emit('success', - Utils._.all(objects, function(o) { - return Utils._.any(associatedObjects, function(associatedObject) { - return Utils._.all(associatedObject.identifiers, function(key, identifier) { - return o[identifier] == associatedObject[identifier]; - }); + instance[self.accessors.get](options) + .error(function(err) { customEventEmitter.emit('error', err) }) + .success(function(associatedObjects) { + customEventEmitter.emit('success', + Utils._.all(objects, function(o) { + return Utils._.any(associatedObjects, function(associatedObject) { + return Utils._.all(associatedObject.identifiers, function(key, identifier) { + return o[identifier] == associatedObject[identifier]; + }); + }) }) - }) - ) - }) + ) + }) }) return customEventEmitter.run() } - obj[this.accessors.hasSingle] = function(o) { - var instance = this; + obj[this.accessors.hasSingle] = function(o, options) { + var instance = this var customEventEmitter = new Utils.CustomEventEmitter(function() { - instance[self.accessors.get]() + instance[self.accessors.get](options) .error(function(err){ customEventEmitter.emit('error', err)}) .success(function(associatedObjects) { customEventEmitter.emit('success', diff --git a/lib/associations/has-one.js b/lib/associations/has-one.js index 9c53db0045ef..d90684b9e7fa 100644 --- a/lib/associations/has-one.js +++ b/lib/associations/has-one.js @@ -77,31 +77,38 @@ module.exports = (function() { HasOne.prototype.injectSetter = function(obj) { var self = this - obj[this.accessors.set] = function(associatedObject) { - var instance = this + obj[this.accessors.set] = function(associatedObject, options) { + var instance = this , instanceKeys = Object.keys(instance.daoFactory.primaryKeys) - , instanceKey = instanceKeys.length === 1 ? instanceKeys[0] : 'id' + , instanceKey = instanceKeys.length === 1 ? instanceKeys[0] : 'id' return new Utils.CustomEventEmitter(function(emitter) { instance[self.accessors.get]().success(function(oldObj) { if (oldObj) { oldObj[self.identifier] = null - oldObj.save([self.identifier], {allowNull: [self.identifier]}).success(function() { - if (associatedObject) { - associatedObject[self.identifier] = instance[instanceKey] - associatedObject - .save() - .success(function() { emitter.emit('success', associatedObject) }) - .error(function(err) { emitter.emit('error', err) }) - } else { - emitter.emit('success', null) - } - }) + oldObj + .save( + Utils._.extend({}, options, { + fields: [self.identifier], + allowNull: [self.identifier] + }) + ) + .success(function() { + if (associatedObject) { + associatedObject[self.identifier] = instance[instanceKey] + associatedObject + .save(options) + .success(function() { emitter.emit('success', associatedObject) }) + .error(function(err) { emitter.emit('error', err) }) + } else { + emitter.emit('success', null) + } + }) } else { if (associatedObject) { associatedObject[self.identifier] = instance[instanceKey] associatedObject - .save() + .save(options) .success(function() { emitter.emit('success', associatedObject) }) .error(function(err) { emitter.emit('error', err) }) } else { diff --git a/lib/dao-factory.js b/lib/dao-factory.js index 32a4e7e46f50..6db4869a58af 100644 --- a/lib/dao-factory.js +++ b/lib/dao-factory.js @@ -1,9 +1,10 @@ -var Utils = require("./utils") - , DAO = require("./dao") - , DataTypes = require("./data-types") - , Util = require('util') - , sql = require('sql') - , SqlString = require('./sql-string') +var Utils = require("./utils") + , DAO = require("./dao") + , DataTypes = require("./data-types") + , Util = require('util') + , sql = require('sql') + , SqlString = require('./sql-string') + , Transaction = require('./transaction') module.exports = (function() { var DAOFactory = function(name, attributes, options) { @@ -92,8 +93,13 @@ module.exports = (function() { return SqlString.format(query.text.replace(/(\$\d)/g, '?'), query.values, null, dialect) + ';' } - result.exec = function() { - return self.QueryInterface.queryAndEmit([result.toSql(), self, { type: 'SELECT' }], 'snafu') + result.exec = function(options) { + options = Utils._.extend({ + transaction: null, + type: 'SELECT' + }, options || {}) + + return self.QueryInterface.queryAndEmit([result.toSql(), self, options], 'snafu') } return result @@ -209,7 +215,7 @@ module.exports = (function() { attributeManipulation[name][type] = fct }) }) - + Object.defineProperties(this.DAO.prototype, attributeManipulation) this.DAO.prototype.attributes = Object.keys(this.DAO.prototype.rawAttributes) } @@ -218,9 +224,11 @@ module.exports = (function() { options = Utils._.extend({}, this.options, options || {}) var self = this + return new Utils.CustomEventEmitter(function(emitter) { var doQuery = function() { - self.QueryInterface + self + .QueryInterface .createTable(self.getTableName(), self.attributes, options) .success(function() { emitter.emit('success', self) }) .error(function(err) { emitter.emit('error', err) }) @@ -228,7 +236,10 @@ module.exports = (function() { } if (options.force) { - self.drop(options).success(doQuery).error(function(err) { emitter.emit('error', err) }) + self + .drop(options) + .success(doQuery) + .error(function(err) { emitter.emit('error', err) }) } else { doQuery() } @@ -376,7 +387,7 @@ module.exports = (function() { return this.QueryInterface.select(this, this.tableName, options, Utils._.defaults({ type: 'SELECT', hasJoin: hasJoin - }, queryOptions)) + }, queryOptions, { transaction: (options || {}).transaction })) } //right now, the caller (has-many-double-linked) is in charge of the where clause @@ -389,7 +400,7 @@ module.exports = (function() { return this.QueryInterface.select(this, [this.getTableName(), joinTableName], optcpy, Utils._.defaults({ type: 'SELECT' - }, queryOptions)) + }, queryOptions, { transaction: (options || {}).transaction })) } /** @@ -555,19 +566,38 @@ module.exports = (function() { return instance } - DAOFactory.prototype.create = function(values, fields) { - return this.build(values).save(fields) + DAOFactory.prototype.create = function(values, fieldsOrOptions) { + Utils.validateParameter(values, Object, { optional: true }) + Utils.validateParameter(fieldsOrOptions, Object, { deprecated: Array, optional: true, index: 2, method: 'DAOFactory#create' }) + + if (fieldsOrOptions instanceof Array) { + fieldsOrOptions = { fields: fieldsOrOptions } + } + + fieldsOrOptions = Utils._.extend({ + transaction: null + }, fieldsOrOptions || {}) + + return this.build(values).save(fieldsOrOptions) } - DAOFactory.prototype.findOrInitialize = DAOFactory.prototype.findOrBuild = function (params, defaults) { + DAOFactory.prototype.findOrInitialize = DAOFactory.prototype.findOrBuild = function (params, defaults, options) { + defaults = defaults || {} + options = options || {} + var self = this - , defaultKeys = Object.keys(defaults || {}) + , defaultKeys = Object.keys(defaults) , defaultLength = defaultKeys.length + if (!options.transaction && defaults.transaction && (defaults.transaction instanceof Transaction)) { + options.transaction = defaults.transaction + delete defaults.transaction + } + return new Utils.CustomEventEmitter(function (emitter) { self.find({ where: params - }).success(function (instance) { + }, options).success(function (instance) { if (instance === null) { var i = 0 @@ -592,10 +622,14 @@ module.exports = (function() { }).run() } - DAOFactory.prototype.findOrCreate = function (where, defaults) { - var self = this; + DAOFactory.prototype.findOrCreate = function (where, defaults, options) { + var self = this + , params = {} + + options = Utils._.extend({ + transaction: null + }, options || {}) - var params = {}; for (var attrname in where) { params[attrname] = where[attrname] } @@ -603,13 +637,16 @@ module.exports = (function() { return new Utils.CustomEventEmitter(function (emitter) { self.find({ where: params + }, { + transaction: options.transaction }).success(function (instance) { if (instance === null) { for (var attrname in defaults) { params[attrname] = defaults[attrname] } - self.create(params) + self + .create(params, options) .success(function (instance) { emitter.emit('success', instance, true) }) @@ -638,32 +675,39 @@ module.exports = (function() { * generated IDs and other default values in a way that can be mapped to * multiple records */ - DAOFactory.prototype.bulkCreate = function(records, fields, options) { - options = options || {} - options.validate = options.validate === undefined ? false : Boolean(options.validate) - options.hooks = options.hooks === undefined ? false : Boolean(options.hooks) + DAOFactory.prototype.bulkCreate = function(records, fieldsOrOptions, options) { + Utils.validateParameter(fieldsOrOptions, Object, { deprecated: Array, optional: true, index: 2, method: 'DAOFactory#bulkCreate' }) + Utils.validateParameter(options, 'undefined', { deprecated: Object, optional: true, index: 3, method: 'DAOFactory#bulkCreate' }) + + options = Utils._.extend({ + validate: false, + hooks: false + }, options || {}) - fields = fields || [] + if (fieldsOrOptions instanceof Array) { + options.fields = fieldsOrOptions + } else { + options.fields = options.fields || [] + options = Utils._.extend(options, fieldsOrOptions) + } var self = this , updatedAtAttr = Utils._.underscoredIf(self.options.updatedAt, self.options.underscored) , createdAtAttr = Utils._.underscoredIf(self.options.createdAt, self.options.underscored) , errors = [] - , daos = records.map(function(v) { - return self.build(v) - }) + , daos = records.map(function(v) { return self.build(v) }) return new Utils.CustomEventEmitter(function(emitter) { var done = function() { - self.runHooks('afterBulkCreate', daos, fields, function(err, newRecords, newFields) { + self.runHooks('afterBulkCreate', daos, options.fields, function(err, newRecords, newFields) { if (!!err) { return emitter.emit('error', err) } - daos = newRecords || daos - fields = newFields || fields + daos = newRecords || daos + options.fields = newFields || options.fields - emitter.emit('success', daos, fields) + emitter.emit('success', daos, options.fields) }) } @@ -680,7 +724,7 @@ module.exports = (function() { } daos[i] = newValues || daos[i] - daos[i].save().error(function(err) { + daos[i].save({ transaction: options.transaction }).error(function(err) { emitter.emit('error', err) }).success(function() { self.runHooks('afterCreate', daos[i], function(err, newValues) { @@ -708,9 +752,9 @@ module.exports = (function() { records = [] daos.forEach(function(dao) { - var values = fields.length > 0 ? {} : dao.dataValues + var values = options.fields.length > 0 ? {} : dao.dataValues - fields.forEach(function(field) { + options.fields.forEach(function(field) { values[field] = dao.dataValues[field] }) @@ -727,7 +771,7 @@ module.exports = (function() { records.push(values) }) - self.QueryInterface.bulkInsert(self.tableName, records) + self.QueryInterface.bulkInsert(self.tableName, records, options) .on('sql', function(sql) { emitter.emit('sql', sql) }) @@ -738,18 +782,18 @@ module.exports = (function() { }) } - self.runHooks('beforeBulkCreate', daos, fields, function(err, newRecords, newFields) { + self.runHooks('beforeBulkCreate', daos, options.fields, function(err, newRecords, newFields) { if (!!err) { return emitter.emit('error', err) } - daos = newRecords || daos - fields = newFields || fields + daos = newRecords || daos + options.fields = newFields || options.fields if (options.validate === true) { if (options.hooks === true) { var iterate = function(i) { - daos[i].hookValidate({skip: fields}).error(function(err) { + daos[i].hookValidate({skip: options.fields}).error(function(err) { errors[errors.length] = {record: v, errors: err} i++ if (i > daos.length) { @@ -764,7 +808,7 @@ module.exports = (function() { } } else { daos.forEach(function(v) { - var valid = v.validate({skip: fields}) + var valid = v.validate({skip: options.fields}) if (valid !== null) { errors[errors.length] = {record: v, errors: valid} } diff --git a/lib/dao.js b/lib/dao.js index a0350aaf8293..31d7c9de454c 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -96,26 +96,32 @@ module.exports = (function() { // if an array with field names is passed to save() // only those fields will be updated - DAO.prototype.save = function(fields, options) { + DAO.prototype.save = function(fieldsOrOptions, options) { + if (fieldsOrOptions instanceof Array) { + fieldsOrOptions = { fields: fieldsOrOptions } + } + + options = Utils._.extend({}, options, fieldsOrOptions) + var self = this - , values = fields ? {} : this.dataValues + , values = options.fields ? {} : this.dataValues , updatedAtAttr = Utils._.underscoredIf(this.__options.updatedAt, this.__options.underscored) , createdAtAttr = Utils._.underscoredIf(this.__options.createdAt, this.__options.underscored) - if (fields) { + if (options.fields) { if (self.__options.timestamps) { - if (fields.indexOf(updatedAtAttr) === -1) { - fields.push(updatedAtAttr) + if (options.fields.indexOf(updatedAtAttr) === -1) { + options.fields.push(updatedAtAttr) } - if (fields.indexOf(createdAtAttr) === -1 && this.isNewRecord === true) { - fields.push(createdAtAttr) + if (options.fields.indexOf(createdAtAttr) === -1 && this.isNewRecord === true) { + options.fields.push(createdAtAttr) } } var tmpVals = self.dataValues - fields.forEach(function(field) { + options.fields.forEach(function(field) { if (tmpVals[field] !== undefined) { values[field] = tmpVals[field] } @@ -172,7 +178,7 @@ module.exports = (function() { if (self.isNewRecord) { self.isDirty = false query = 'insert' - args = [self, self.QueryInterface.QueryGenerator.addSchema(self.__factory), values] + args = [self, self.QueryInterface.QueryGenerator.addSchema(self.__factory), values, options] hook = 'Create' } else { var identifier = self.__options.hasPrimaryKeys ? self.primaryKeyValues : { id: self.id } @@ -224,7 +230,7 @@ module.exports = (function() { * * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. */ - DAO.prototype.reload = function() { + DAO.prototype.reload = function(options) { var where = [ this.QueryInterface.quoteIdentifier(this.__factory.tableName) + '.' + this.QueryInterface.quoteIdentifier('id')+'=?', this.id @@ -235,7 +241,7 @@ module.exports = (function() { where: where, limit: 1, include: this.__eagerlyLoadedOptions || [] - }) + }, options) .on('sql', function(sql) { emitter.emit('sql', sql) }) .on('error', function(error) { emitter.emit('error', error) }) .on('success', function(obj) { @@ -273,9 +279,14 @@ module.exports = (function() { return validator.hookValidate() } - DAO.prototype.updateAttributes = function(updates, fields) { + DAO.prototype.updateAttributes = function(updates, fieldsOrOptions) { + if (fieldsOrOptions instanceof Array) { + fieldsOrOptions = { fields: fieldsOrOptions } + } + this.setAttributes(updates) - return this.save(fields) + + return this.save(fieldsOrOptions) } DAO.prototype.setAttributes = function(updates) { @@ -317,7 +328,7 @@ module.exports = (function() { this.isDirty = isDirty } - DAO.prototype.destroy = function() { + DAO.prototype.destroy = function(options) { var self = this , query = null @@ -330,10 +341,10 @@ module.exports = (function() { if (self.__options.timestamps && self.__options.paranoid) { var attr = Utils._.underscoredIf(self.__options.deletedAt, self.__options.underscored) self.dataValues[attr] = new Date() - query = self.save() + query = self.save(options) } else { var identifier = self.__options.hasPrimaryKeys ? self.primaryKeyValues : { id: self.id }; - query = self.QueryInterface.delete(self, self.QueryInterface.QueryGenerator.addSchema(self.__factory.tableName, self.__factory.options.schema), identifier) + query = self.QueryInterface.delete(self, self.QueryInterface.QueryGenerator.addSchema(self.__factory.tableName, self.__factory.options.schema), identifier, options) } query.on('sql', function(sql) { @@ -355,48 +366,73 @@ module.exports = (function() { }).run() } - DAO.prototype.increment = function(fields, count) { - var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : { id: this.id } - , updatedAtAttr = Utils._.underscoredIf(this.__options.updatedAt, this.__options.underscored) - , values = {} - , options = {} + DAO.prototype.increment = function(fields, countOrOptions) { + Utils.validateParameter(countOrOptions, Object, { + optional: true, + deprecated: 'number', + deprecationWarning: "Increment expects an object as second parameter. Please pass the incrementor as option! ~> instance.increment(" + JSON.stringify(fields) + ", { by: " + countOrOptions + " })" + }) + + var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : { id: this.id } + , updatedAtAttr = Utils._.underscoredIf(this.__options.updatedAt, this.__options.underscored) + , values = {} - if (count === undefined) { - count = 1; + if (countOrOptions === undefined) { + countOrOptions = { by: 1, transaction: null } + } else if (typeof countOrOptions === 'number') { + countOrOptions = { by: countOrOptions, transaction: null } } + countOrOptions = Utils._.extend({ + by: 1, + attributes: {} + }, countOrOptions) + if (Utils._.isString(fields)) { - values[fields] = count; + values[fields] = countOrOptions.by } else if (Utils._.isArray(fields)) { Utils._.each(fields, function (field) { - values[field] = count + values[field] = countOrOptions.by }) } else { // Assume fields is key-value pairs - values = fields; + values = fields } - if (this.__options.timestamps) { if (!values[updatedAtAttr]) { - options[updatedAtAttr] = Utils.now(this.daoFactory.daoFactoryManager.sequelize.options.dialect) + countOrOptions.attributes[updatedAtAttr] = Utils.now(this.daoFactory.daoFactoryManager.sequelize.options.dialect) } } - return this.QueryInterface.increment(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory.tableName, this.__factory.options.schema), values, identifier, options) + return this.QueryInterface.increment(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory.tableName, this.__factory.options.schema), values, identifier, countOrOptions) } - DAO.prototype.decrement = function (fields, count) { + DAO.prototype.decrement = function (fields, countOrOptions) { + Utils.validateParameter(countOrOptions, Object, { + optional: true, + deprecated: 'number', + deprecationWarning: "Decrement expects an object as second parameter. Please pass the decrementor as option! ~> instance.decrement(" + JSON.stringify(fields) + ", { by: " + countOrOptions + " })" + }) + + if (countOrOptions === undefined) { + countOrOptions = { by: 1, transaction: null } + } else if (typeof countOrOptions === 'number') { + countOrOptions = { by: countOrOptions, transaction: null } + } + + if (countOrOptions.by === undefined) { + countOrOptions.by = 1 + } + if (!Utils._.isString(fields) && !Utils._.isArray(fields)) { // Assume fields is key-value pairs Utils._.each(fields, function (value, field) { - fields[field] = -value; - }); + fields[field] = -value + }) } - if (count === undefined) { - count = 1; - } + countOrOptions.by = 0 - countOrOptions.by - return this.increment(fields, 0 - count); + return this.increment(fields, countOrOptions) } DAO.prototype.equals = function(other) { diff --git a/lib/dialects/abstract/query-generator.js b/lib/dialects/abstract/query-generator.js index b161a38ee3d7..d12611ccc262 100644 --- a/lib/dialects/abstract/query-generator.js +++ b/lib/dialects/abstract/query-generator.js @@ -454,6 +454,50 @@ module.exports = (function() { return query }, + /** + * Returns a query that starts a transaction. + * + * @param {Boolean} value A boolean that states whether autocommit shall be done or not. + * @return {String} The generated sql query. + */ + setAutocommitQuery: function(value) { + throwMethodUndefined('setAutocommitQuery') + }, + + setIsolationLevelQuery: function(value) { + throwMethodUndefined('setIsolationLevelQuery') + }, + + /** + * Returns a query that starts a transaction. + * + * @param {Object} options An object with options. + * @return {String} The generated sql query. + */ + startTransactionQuery: function(options) { + throwMethodUndefined('startTransactionQuery') + }, + + /** + * Returns a query that commits a transaction. + * + * @param {Object} options An object with options. + * @return {String} The generated sql query. + */ + commitTransactionQuery: function(options) { + throwMethodUndefined('commitTransactionQuery') + }, + + /** + * Returns a query that rollbacks a transaction. + * + * @param {Object} options An object with options. + * @return {String} The generated sql query. + */ + rollbackTransactionQuery: function(options) { + throwMethodUndefined('rollbackTransactionQuery') + }, + addLimitAndOffset: function(options, query){ if (options.offset && !options.limit) { query += " LIMIT " + options.offset + ", " + 10000000000000; diff --git a/lib/dialects/connector-manager.js b/lib/dialects/connector-manager.js index 83ac9f19b6bd..d36ae0018046 100644 --- a/lib/dialects/connector-manager.js +++ b/lib/dialects/connector-manager.js @@ -7,6 +7,10 @@ module.exports = (function(){ throw new Error('Define the query method!') } + ConnectorManager.prototype.afterTransactionSetup = function(callback) { + callback() + } + ConnectorManager.prototype.connect = function() { throw new Error('Define the connect method!') } diff --git a/lib/dialects/mariadb/query.js b/lib/dialects/mariadb/query.js index 35e463db739d..e3164dc2e870 100644 --- a/lib/dialects/mariadb/query.js +++ b/lib/dialects/mariadb/query.js @@ -20,7 +20,7 @@ module.exports = (function() { this.sql = sql if (this.options.logging !== false) { - this.options.logging('Executing: ' + this.sql) + this.options.logging('Executing (' + this.options.uuid + '): ' + this.sql) } var resultSet = [], @@ -67,7 +67,7 @@ module.exports = (function() { case "MEDIUMBLOB": case "LONGBLOB": if (metadata.charsetNrs[prop] === 63) { // binary - row[prop] = new Buffer(row[prop]) + row[prop] = new Buffer(row[prop]) } break case "TIME": @@ -104,7 +104,7 @@ module.exports = (function() { } else if( /^show/.test(self.sql.toLowerCase()) || /^select/.test(self.sql.toLowerCase()) || /^describe/.test(self.sql.toLowerCase())) { - self.emit('success', self.formatResults(resultSet)) + self.emit('success', self.formatResults(resultSet)) } else { self.emit('success', self.formatResults(info)) } diff --git a/lib/dialects/mysql/connector-manager.js b/lib/dialects/mysql/connector-manager.js index 6c358383c2b8..48f12cb3c13e 100644 --- a/lib/dialects/mysql/connector-manager.js +++ b/lib/dialects/mysql/connector-manager.js @@ -161,13 +161,15 @@ module.exports = (function() { sql: sql }; - enqueue.call(this, queueItem, options); - return queueItem.query; + queueItem.query.options.uuid = this.config.uuid + enqueue.call(this, queueItem, options) + return queueItem.query } var self = this, query = new Query(this.client, this.sequelize, callee, options || {}); this.pendingQueries++; + query.options.uuid = this.config.uuid query.done(function() { self.pendingQueries--; if (self.pool) { @@ -277,7 +279,7 @@ module.exports = (function() { if (err) { switch(err.code) { case 'ECONNREFUSED': - case 'ER_ACCESS_DENIED_ERROR': + case 'ER_ACCESS_D2ENIED_ERROR': emitter.emit('error', 'Failed to authenticate for MySQL. Please double check your settings.') break case 'ENOTFOUND': diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index b095e23437b2..2f97c6f8538d 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -333,6 +333,38 @@ module.exports = (function() { return Utils._.template(sql)({ tableName: tableName, indexName: indexName }) }, + /** + * Returns a query that starts a transaction. + * + * @param {Boolean} value A boolean that states whether autocommit shall be done or not. + * @return {String} The generated sql query. + */ + setAutocommitQuery: function(value) { + return "SET autocommit = " + (!!value ? 1 : 0) + ";" + }, + + setIsolationLevelQuery: function(value) { + return "SET SESSION TRANSACTION ISOLATION LEVEL " + value + ";" + }, + + /** + * Returns a query that starts a transaction. + * + * @param {Object} options An object with options. + * @return {String} The generated sql query. + */ + startTransactionQuery: function(options) { + return "START TRANSACTION;" + }, + + commitTransactionQuery: function(options) { + return "COMMIT;" + }, + + rollbackTransactionQuery: function(options) { + return "ROLLBACK;" + }, + attributesToSQL: function(attributes) { var result = {} diff --git a/lib/dialects/mysql/query.js b/lib/dialects/mysql/query.js index 41d113dd83ee..3a63b840c1ee 100644 --- a/lib/dialects/mysql/query.js +++ b/lib/dialects/mysql/query.js @@ -20,7 +20,7 @@ module.exports = (function() { this.sql = sql if (this.options.logging !== false) { - this.options.logging('Executing: ' + this.sql) + this.options.logging('Executing (' + this.options.uuid + '): ' + this.sql) } this.client.query(this.sql, function(err, results, fields) { diff --git a/lib/dialects/postgres/connector-manager.js b/lib/dialects/postgres/connector-manager.js index c4a03de06d37..37d6168c3ce0 100644 --- a/lib/dialects/postgres/connector-manager.js +++ b/lib/dialects/postgres/connector-manager.js @@ -1,3 +1,4 @@ + var Query = require("./query") , Utils = require("../../utils") @@ -70,6 +71,10 @@ module.exports = (function() { }).run() } + ConnectorManager.prototype.afterTransactionSetup = function(callback) { + this.setTimezone(this.client, 'UTC', callback) + } + ConnectorManager.prototype.connect = function(callback) { var self = this var emitter = new (require('events').EventEmitter)() @@ -108,15 +113,23 @@ module.exports = (function() { emitter.emit('error', err) break } + } else { + emitter.emit('error', new Error(err.message)) } } else if (client) { - client.query("SET TIME ZONE 'UTC'").on('end', function() { + var timezoneCallback = function() { self.isConnected = true self.client = client emitter.emit('success', done) - }) + } + + if (self.config.keepDefaultTimezone) { + Utils.tick(timezoneCallback) + } else { + self.setTimezone(client, 'UTC', timezoneCallback) + } } else if (self.config.native) { - self.client.query("SET TIME ZONE 'UTC'").on('end', function() { + self.setTimezone(self.client, 'UTC', function() { self.isConnected = true emitter.emit('success', done) }) @@ -136,13 +149,19 @@ module.exports = (function() { } else { //create one-off client this.client = new this.pg.Client(uri) - this.client.connect(connectCallback) + this.client.connect(function(err, client, done) { + connectCallback(err, client || self.client, done) + }) } } return emitter } + ConnectorManager.prototype.setTimezone = function(client, timezone, callback) { + client.query("SET TIME ZONE '" + (timezone || "UTC") + "'").on('end', callback) + } + ConnectorManager.prototype.disconnect = function() { if (this.poolIdentifier) { this.poolIdentifier.destroyAllNow() diff --git a/lib/dialects/postgres/query-generator.js b/lib/dialects/postgres/query-generator.js index 5299a76d95eb..4e6fbcd2cdbb 100644 --- a/lib/dialects/postgres/query-generator.js +++ b/lib/dialects/postgres/query-generator.js @@ -880,7 +880,39 @@ module.exports = (function() { */ dropForeignKeyQuery: function(tableName, foreignKey) { return 'ALTER TABLE ' + this.quoteIdentifier(tableName) + ' DROP CONSTRAINT ' + this.quoteIdentifier(foreignKey) + ';' - } + }, + + /** + * Returns a query that starts a transaction. + * + * @param {Boolean} value A boolean that states whether autocommit shall be done or not. + * @return {String} The generated sql query. + */ + setAutocommitQuery: function(value) { + return "SET autocommit = " + (!!value ? 1 : 0) + ";" + }, + + setIsolationLevelQuery: function(value) { + return "SET SESSION TRANSACTION ISOLATION LEVEL " + value + ";" + }, + + /** + * Returns a query that starts a transaction. + * + * @param {Object} options An object with options. + * @return {String} The generated sql query. + */ + startTransactionQuery: function(options) { + return "START TRANSACTION;" + }, + + commitTransactionQuery: function(options) { + return "COMMIT;" + }, + + rollbackTransactionQuery: function(options) { + return "ROLLBACK;" + }, } // Private diff --git a/lib/dialects/postgres/query.js b/lib/dialects/postgres/query.js index 06b08c32764d..adacacc749d1 100644 --- a/lib/dialects/postgres/query.js +++ b/lib/dialects/postgres/query.js @@ -21,13 +21,13 @@ module.exports = (function() { Query.prototype.run = function(sql) { this.sql = sql - var self = this + var self = this , receivedError = false , query = this.client.query(sql) , rows = [] if (this.options.logging !== false) { - this.options.logging('Executing: ' + this.sql) + this.options.logging('Executing (' + this.options.uuid + '): ' + this.sql) } query.on('row', function(row) { diff --git a/lib/dialects/sqlite/query-generator.js b/lib/dialects/sqlite/query-generator.js index ddd9157b8a12..8b8b8285e9ac 100644 --- a/lib/dialects/sqlite/query-generator.js +++ b/lib/dialects/sqlite/query-generator.js @@ -1,6 +1,7 @@ -var Utils = require("../../utils") - , DataTypes = require("../../data-types") - , SqlString = require("../../sql-string") +var Utils = require("../../utils") + , DataTypes = require("../../data-types") + , SqlString = require("../../sql-string") + , Transaction = require("../../transaction") var MySqlQueryGenerator = Utils._.extend( Utils._.clone(require("../abstract/query-generator")), @@ -409,6 +410,29 @@ module.exports = (function() { }) }, + startTransactionQuery: function(options) { + return "BEGIN TRANSACTION;" + }, + + setAutocommitQuery: function(value) { + return "-- SQLite does not support SET autocommit." + }, + + setIsolationLevelQuery: function(value) { + switch (value) { + case Transaction.ISOLATION_LEVELS.REPEATABLE_READ: + return "-- SQLite is not able to choose the isolation level REPEATABLE READ." + case Transaction.ISOLATION_LEVELS.READ_UNCOMMITTED: + return "PRAGMA read_uncommitted = ON;" + case Transaction.ISOLATION_LEVELS.READ_COMMITTED: + return "PRAGMA read_uncommitted = OFF;" + case Transaction.ISOLATION_LEVELS.SERIALIZABLE: + return "-- SQLite's default isolation level is SERIALIZABLE. Nothing to do." + default: + throw new Error('Unknown isolation level: ' + value) + } + }, + replaceBooleanDefaults: function(sql) { return sql.replace(/DEFAULT '?false'?/g, "DEFAULT 0").replace(/DEFAULT '?true'?/g, "DEFAULT 1") }, diff --git a/lib/dialects/sqlite/query.js b/lib/dialects/sqlite/query.js index d4a61da3b5bf..52b846829204 100644 --- a/lib/dialects/sqlite/query.js +++ b/lib/dialects/sqlite/query.js @@ -26,24 +26,32 @@ module.exports = (function() { this.sql = sql if (this.options.logging !== false) { - this.options.logging('Executing: ' + this.sql) + this.options.logging('Executing (' + this.options.uuid + '): ' + this.sql) } var columnTypes = {} this.database.serialize(function() { var executeSql = function() { - self.database[getDatabaseMethod.call(self)](self.sql, function(err, results) { - // allow clients to listen to sql to do their own logging or whatnot - self.emit('sql', self.sql) - - if (err) { - onFailure.call(self, err) - } else { - this.columnTypes = columnTypes - onSuccess.call(self, results, this) - } - }) - }; + if (self.sql.indexOf('-- ') === 0) { + // the sql query starts with a comment. don't bother the server with that ... + Utils.tick(function() { + self.emit('sql', self.sql) + self.emit('success', null) + }) + } else { + self.database[getDatabaseMethod.call(self)](self.sql, function(err, results) { + // allow clients to listen to sql to do their own logging or whatnot + self.emit('sql', self.sql) + + if (err) { + onFailure.call(self, err) + } else { + this.columnTypes = columnTypes + onSuccess.call(self, results, this) + } + }) + } + } if ((getDatabaseMethod.call(self) === 'all') && /select\s.*?\sfrom\s+([^ ;]+)/i.test(self.sql)) { var tableName = RegExp.$1; diff --git a/lib/emitters/custom-event-emitter.js b/lib/emitters/custom-event-emitter.js index fc108e1f7f9e..7531536aab34 100644 --- a/lib/emitters/custom-event-emitter.js +++ b/lib/emitters/custom-event-emitter.js @@ -2,7 +2,6 @@ var util = require("util") , EventEmitter = require("events").EventEmitter , Promise = require("bluebird") , proxyEventKeys = ['success', 'error', 'sql'] - , tick = (typeof setImmediate !== "undefined" ? setImmediate : process.nextTick) , Utils = require('../utils') var bindToProcess = function(fct) { @@ -20,7 +19,7 @@ module.exports = (function() { util.inherits(CustomEventEmitter, EventEmitter) CustomEventEmitter.prototype.run = function() { - tick(function() { + Utils.tick(function() { if (this.fct) { this.fct.call(this, this) } diff --git a/lib/query-interface.js b/lib/query-interface.js index 33505f7f1097..c7ed3ce73f08 100644 --- a/lib/query-interface.js +++ b/lib/query-interface.js @@ -1,6 +1,7 @@ var Utils = require(__dirname + '/utils') , DataTypes = require(__dirname + '/data-types') , SQLiteQueryInterface = require(__dirname + '/dialects/sqlite/query-interface') + , Transaction = require(__dirname + '/transaction') module.exports = (function() { var QueryInterface = function(sequelize) { @@ -112,8 +113,7 @@ module.exports = (function() { if (!results[enumIdx]) { sql = self.QueryGenerator.pgEnum(getTableName, keys[i], attributes[keys[i]], options) chainer2.add(self.sequelize.query(sql, null, { raw: true, logging: options.logging })) - } - else if (!!results[enumIdx] && !!daoTable) { + } else if (!!results[enumIdx] && !!daoTable) { var enumVals = self.QueryGenerator.fromArray(results[enumIdx].enum_value) , vals = daoTable.rawAttributes[keys[i]].values @@ -141,18 +141,19 @@ module.exports = (function() { sql = self.QueryGenerator.createTableQuery(tableName, attributes, options) chainer2.run().success(function() { - queryAndEmit.call(self, sql, 'createTable', options) - .success(function(res) { - self.emit('createTable', null) - emitter.emit('success', res) - }) - .error(function(err) { - self.emit('createTable', err) - emitter.emit('error', err) - }) - .on('sql', function(sql) { - emitter.emit('sql', sql) - }) + queryAndEmit + .call(self, sql, 'createTable', options) + .success(function(res) { + self.emit('createTable', null) + emitter.emit('success', res) + }) + .error(function(err) { + self.emit('createTable', err) + emitter.emit('error', err) + }) + .on('sql', function(sql) { + emitter.emit('sql', sql) + }) }).error(function(err) { emitter.emit('error', err) }).on('sql', function(sql) { @@ -449,16 +450,16 @@ module.exports = (function() { return queryAndEmit.call(this, sql, "removeIndex") } - QueryInterface.prototype.insert = function(dao, tableName, values) { + QueryInterface.prototype.insert = function(dao, tableName, values, options) { var sql = this.QueryGenerator.insertQuery(tableName, values, dao.daoFactory.rawAttributes) - return queryAndEmit.call(this, [sql, dao], 'insert', { + return queryAndEmit.call(this, [sql, dao, options], 'insert', { success: function(obj) { obj.isNewRecord = false } }) } - QueryInterface.prototype.bulkInsert = function(tableName, records) { + QueryInterface.prototype.bulkInsert = function(tableName, records, options) { var sql = this.QueryGenerator.bulkInsertQuery(tableName, records) - return queryAndEmit.call(this, sql, 'bulkInsert') + return queryAndEmit.call(this, [sql, null, options], 'bulkInsert') } QueryInterface.prototype.update = function(dao, tableName, values, identifier, options) { @@ -481,7 +482,7 @@ module.exports = (function() { return new Utils.CustomEventEmitter(function(emitter) { var chainer = new Utils.QueryChainer() - chainer.add(self, 'queryAndEmit', [[sql, dao], 'delete']) + chainer.add(self, 'queryAndEmit', [[sql, dao, options], 'delete']) chainer.runSerially() .success(function(results){ @@ -507,7 +508,7 @@ module.exports = (function() { return new Utils.CustomEventEmitter(function(emitter) { var chainer = new Utils.QueryChainer() - chainer.add(self, 'queryAndEmit', [sql, 'bulkUpdate']) + chainer.add(self, 'queryAndEmit', [[sql, null, options], 'bulkUpdate']) return chainer.runSerially() .success(function(results){ @@ -523,7 +524,7 @@ module.exports = (function() { }).run() } - QueryInterface.prototype.delete = function(dao, tableName, identifier) { + QueryInterface.prototype.delete = function(dao, tableName, identifier, options) { var self = this , restrict = false , cascades = [] @@ -592,7 +593,7 @@ module.exports = (function() { var chainer = new Utils.QueryChainer() - chainer.add(self, 'queryAndEmit', [[sql, dao], 'delete']) + chainer.add(self, 'queryAndEmit', [[sql, dao, options], 'delete']) chainer.runSerially() .success(function(results){ @@ -622,7 +623,7 @@ module.exports = (function() { return new Utils.CustomEventEmitter(function(emitter) { var chainer = new Utils.QueryChainer() - chainer.add(self, 'queryAndEmit', [sql, 'bulkDelete', options]) + chainer.add(self, 'queryAndEmit', [[sql, null, options], 'bulkDelete', options]) chainer.runSerially() .success(function(results){ @@ -663,8 +664,8 @@ module.exports = (function() { } QueryInterface.prototype.increment = function(dao, tableName, values, identifier, options) { - var sql = this.QueryGenerator.incrementQuery(tableName, values, identifier, options); - return queryAndEmit.call(this, [sql, dao], 'increment'); + var sql = this.QueryGenerator.incrementQuery(tableName, values, identifier, options.attributes) + return queryAndEmit.call(this, [sql, dao, options], 'increment') } QueryInterface.prototype.rawSelect = function(tableName, options, attributeSelector) { @@ -675,10 +676,11 @@ module.exports = (function() { } return new Utils.CustomEventEmitter(function(emitter) { - var sql = self.QueryGenerator.selectQuery(tableName, options) - , qry = self.sequelize.query(sql, null, { plain: true, raw: true, type: 'SELECT' }) + var sql = self.QueryGenerator.selectQuery(tableName, options) + , queryOptions = Utils._.extend({ transaction: options.transaction }, { plain: true, raw: true, type: 'SELECT' }) + , query = self.sequelize.query(sql, null, queryOptions) - qry + query .success(function(data) { var result = data ? data[attributeSelector] : null @@ -804,6 +806,63 @@ module.exports = (function() { return this.QueryGenerator.escape(value) } + QueryInterface.prototype.setAutocommit = function(transaction, value) { + if (!transaction || !(transaction instanceof Transaction)) { + throw new Error('Unable to set autocommit for a transaction without transaction object!') + } + + var sql = this.QueryGenerator.setAutocommitQuery(value) + return this.queryAndEmit([sql, null, { transaction: transaction }], 'setAutocommit') + } + + QueryInterface.prototype.setIsolationLevel = function(transaction, value) { + if (!transaction || !(transaction instanceof Transaction)) { + throw new Error('Unable to set isolation level for a transaction without transaction object!') + } + + var sql = this.QueryGenerator.setIsolationLevelQuery(value) + return this.queryAndEmit([sql, null, { transaction: transaction }], 'setIsolationLevel') + } + + QueryInterface.prototype.startTransaction = function(transaction, options) { + if (!transaction || !(transaction instanceof Transaction)) { + throw new Error('Unable to start a transaction without transaction object!') + } + + options = Utils._.extend({ + transaction: transaction + }, options || {}) + + var sql = this.QueryGenerator.startTransactionQuery(options) + return this.queryAndEmit([sql, null, options], 'startTransaction') + } + + QueryInterface.prototype.commitTransaction = function(transaction, options) { + if (!transaction || !(transaction instanceof Transaction)) { + throw new Error('Unable to commit a transaction without transaction object!') + } + + options = Utils._.extend({ + transaction: transaction + }, options || {}) + + var sql = this.QueryGenerator.commitTransactionQuery(options) + return this.queryAndEmit([sql, null, options], 'commitTransaction') + } + + QueryInterface.prototype.rollbackTransaction = function(transaction, options) { + if (!transaction || !(transaction instanceof Transaction)) { + throw new Error('Unable to rollback a transaction without transaction object!') + } + + options = Utils._.extend({ + transaction: transaction + }, options || {}) + + var sql = this.QueryGenerator.rollbackTransactionQuery(options) + return this.queryAndEmit([sql, null, options], 'rollbackTransaction') + } + // private var buildScope = function() { @@ -817,9 +876,10 @@ module.exports = (function() { var queryAndEmit = QueryInterface.prototype.queryAndEmit = function(sqlOrQueryParams, methodName, options, emitter) { options = Utils._.extend({ - success: function(){}, - error: function(){}, - logging: this.sequelize.options.logging + success: function(){}, + error: function(){}, + transaction: null, + logging: this.sequelize.options.logging }, options || {}) var execQuery = function(emitter) { @@ -834,27 +894,26 @@ module.exports = (function() { sqlOrQueryParams.push(typeof options === "object" ? options : {}) } - query = this.sequelize.query.apply(this.sequelize, sqlOrQueryParams) + emitter.query = this.sequelize.query.apply(this.sequelize, sqlOrQueryParams) } else { - query = this.sequelize.query(sqlOrQueryParams, null, options) + emitter.query = this.sequelize.query(sqlOrQueryParams, null, options) } - // append the query for better testing - emitter.query = query - - query.success(function(obj) { - options.success && options.success(obj) - this.emit(methodName, null) - emitter.emit('success', obj) - }.bind(this)).error(function(err) { - options.error && options.error(err) - this.emit(methodName, err) - emitter.emit('error', err) - }.bind(this)) - - query.on('sql', function(sql) { - emitter.emit('sql', sql) - }) + emitter + .query + .success(function(obj) { + options.success && options.success(obj) + this.emit(methodName, null) + emitter.emit('success', obj) + }.bind(this)) + .error(function(err) { + options.error && options.error(err) + this.emit(methodName, err) + emitter.emit('error', err) + }.bind(this)) + .on('sql', function(sql) { + emitter.emit('sql', sql) + }) }.bind(this) if (!!emitter) { diff --git a/lib/sequelize.js b/lib/sequelize.js index 9a49b0891267..56b5fd729345 100644 --- a/lib/sequelize.js +++ b/lib/sequelize.js @@ -1,10 +1,12 @@ -var url = require("url") - , Path = require("path") - , Utils = require("./utils") - , DAOFactory = require("./dao-factory") - , DataTypes = require('./data-types') - , DAOFactoryManager = require("./dao-factory-manager") - , QueryInterface = require("./query-interface") +var url = require("url") + , Path = require("path") + , Utils = require("./utils") + , DAOFactory = require("./dao-factory") + , DataTypes = require('./data-types') + , DAOFactoryManager = require("./dao-factory-manager") + , QueryInterface = require("./query-interface") + , Transaction = require("./transaction") + , TransactionManager = require('./transaction-manager') module.exports = (function() { /** @@ -72,6 +74,7 @@ module.exports = (function() { dialect: 'mysql', dialectModulePath: null, host: 'localhost', + port: 3306, protocol: 'tcp', define: {}, query: {}, @@ -108,15 +111,9 @@ module.exports = (function() { maxConcurrentQueries: this.options.maxConcurrentQueries, dialectOptions: this.options.dialectOptions, } - - try { - var ConnectorManager = require("./dialects/" + this.options.dialect + "/connector-manager") - } catch(err) { - throw new Error("The dialect " + this.options.dialect + " is not supported.") - } - this.daoFactoryManager = new DAOFactoryManager(this) - this.connectorManager = new ConnectorManager(this, this.config) + this.daoFactoryManager = new DAOFactoryManager(this) + this.transactionManager = new TransactionManager(this) this.importCache = {} } @@ -130,6 +127,24 @@ module.exports = (function() { Sequelize[dataType] = DataTypes[dataType] } + /** + * Polyfill for the default connector manager. + */ + Object.defineProperty(Sequelize.prototype, 'connectorManager', { + get: function() { + return this.transactionManager.getConnectorManager() + } + }) + + /** + * Returns the specified dialect. + * + * @return {String} The specified dialect. + */ + Sequelize.prototype.getDialect = function() { + return this.options.dialect + } + /** Returns an instance of QueryInterface. @@ -249,7 +264,7 @@ module.exports = (function() { // make path relative to the caller var callerFilename = Utils.stack()[1].getFileName() , callerPath = Path.dirname(callerFilename) - + path = Path.resolve(callerPath, path) } @@ -287,7 +302,7 @@ module.exports = (function() { type: (sql.toLowerCase().indexOf('select') === 0) ? 'SELECT' : false }) - return this.connectorManager.query(sql, callee, options) + return this.transactionManager.query(sql, callee, options) } Sequelize.prototype.createSchema = function(schema) { @@ -358,6 +373,22 @@ module.exports = (function() { }).run() } + Sequelize.prototype.authenticate = function() { + var self = this + + return new Utils.CustomEventEmitter(function(emitter) { + self + .query('SELECT 1+1 AS result', null, { raw: true, plain: true }) + .complete(function(err, result) { + if (!!err) { + emitter.emit('error', new Error('Invalid credentials.')) + } else { + emitter.emit('success') + } + }) + }).run() + } + Sequelize.prototype.fn = function (fn) { return new Utils.fn(fn, Array.prototype.slice.call(arguments, 1)) } @@ -374,5 +405,20 @@ module.exports = (function() { return new Utils.literal(val) } + Sequelize.prototype.transaction = function(_options, _callback) { + var options = (typeof _options === 'function') ? {} : _options + , callback = (typeof _options === 'function') ? _options : _callback + , transaction = new Transaction(this, options) + , self = this + + Utils.tick(function() { + transaction.prepareEnvironment(function() { + callback(transaction) + }) + }) + + return transaction + } + return Sequelize })() diff --git a/lib/transaction-manager.js b/lib/transaction-manager.js new file mode 100644 index 000000000000..1e9d088f99e9 --- /dev/null +++ b/lib/transaction-manager.js @@ -0,0 +1,40 @@ +Utils = require('./utils') + +var TransactionManager = module.exports = function(sequelize) { + this.sequelize = sequelize + this.connectorManagers = {} + + try { + this.ConnectorManager = require("./dialects/" + sequelize.getDialect() + "/connector-manager") + } catch(err) { + throw new Error("The dialect " + sequelize.getDialect() + " is not supported.") + } +} + +TransactionManager.prototype.getConnectorManager = function(uuid) { + uuid = uuid || 'default' + + if (!this.connectorManagers.hasOwnProperty(uuid)) { + var config = Utils._.extend({ uuid: uuid }, this.sequelize.config) + + if (uuid !== 'default') { + config.pool = { maxConnections: 0, useReplicaton: false } + config.keepDefaultTimezone = true + } + + this.connectorManagers[uuid] = new this.ConnectorManager(this.sequelize, config) + } + + return this.connectorManagers[uuid] +} + +TransactionManager.prototype.query = function(sql, callee, options) { + options = options || {} + options.uuid = 'default' + + if (options.transaction) { + options.uuid = options.transaction.id + } + + return this.getConnectorManager(options.uuid).query(sql, callee, options) +} diff --git a/lib/transaction.js b/lib/transaction.js new file mode 100644 index 000000000000..f9faf00a1325 --- /dev/null +++ b/lib/transaction.js @@ -0,0 +1,76 @@ +var Utils = require('./utils') + , util = require('util') + +var Transaction = module.exports = function(sequelize, options) { + this.sequelize = sequelize + this.id = Utils.generateUUID() + this.options = Utils._.extend({ + autocommit: true, + isolationLevel: Transaction.ISOLATION_LEVELS.REPEATABLE_READ + }, options || {}) +} + +util.inherits(Transaction, Utils.CustomEventEmitter) + +Transaction.ISOLATION_LEVELS = { + READ_UNCOMMITTED: "READ UNCOMMITTED", + READ_COMMITTED: "READ COMMITTED", + REPEATABLE_READ: "REPEATABLE READ", + SERIALIZABLE: "SERIALIZABLE" +} + +Transaction.prototype.commit = function() { + return this + .sequelize + .getQueryInterface() + .commitTransaction(this, {}) + .proxy(this) +} + + +Transaction.prototype.rollback = function() { + return this + .sequelize + .getQueryInterface() + .rollbackTransaction(this, {}) + .proxy(this) +} + +Transaction.prototype.prepareEnvironment = function(callback) { + var self = this + , connectorManager = self.sequelize.transactionManager.getConnectorManager(this.id) + + this.begin(function() { + self.setIsolationLevel(function() { + self.setAutocommit(function() { + connectorManager.afterTransactionSetup(callback) + }) + }) + }) +} + +Transaction.prototype.begin = function(callback) { + this + .sequelize + .getQueryInterface() + .startTransaction(this, {}) + .success(callback) + +} + +Transaction.prototype.setAutocommit = function(callback) { + this + .sequelize + .getQueryInterface() + .setAutocommit(this, this.options.autocommit) + .success(callback) +} + +Transaction.prototype.setIsolationLevel = function(callback) { + this + .sequelize + .getQueryInterface() + .setIsolationLevel(this, this.options.isolationLevel) + .success(callback) + .error(function(err) { console.log(err) }) +} diff --git a/lib/utils.js b/lib/utils.js index 2f88763dff23..7698ec31c683 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,9 +1,10 @@ -var util = require("util") - , DataTypes = require("./data-types") - , SqlString = require("./sql-string") - , lodash = require("lodash") - , _string = require('underscore.string') - , uuid = require('node-uuid') +var util = require("util") + , DataTypes = require("./data-types") + , SqlString = require("./sql-string") + , lodash = require("lodash") + , _string = require('underscore.string') + , ParameterValidator = require('./utils/parameter-validator') + , uuid = require('node-uuid') var Utils = module.exports = { _: (function() { @@ -503,6 +504,11 @@ var Utils = module.exports = { return now }, + tick: function(func) { + var tick = (typeof setImmediate !== "undefined" ? setImmediate : process.nextTick) + tick(func) + }, + // Note: Use the `quoteIdentifier()` and `escape()` methods on the // `QueryInterface` instead for more portable code. @@ -532,6 +538,17 @@ var Utils = module.exports = { }, literal: function (val) { this.val = val + }, + + generateUUID: function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8) + return v.toString(16) + }) + }, + + validateParameter: function(value, expectation, options) { + return ParameterValidator.check(value, expectation, options) } } diff --git a/lib/utils/parameter-validator.js b/lib/utils/parameter-validator.js new file mode 100644 index 000000000000..e48947a84c32 --- /dev/null +++ b/lib/utils/parameter-validator.js @@ -0,0 +1,90 @@ +var cJSON = require('circular-json') + +var ParameterValidator = module.exports = { + check: function(value, expectation, options) { + options = Utils._.extend({ + throwError: true, + deprecated: false, + deprecationWarning: generateDeprecationWarning(value, expectation, options), + onDeprecated: function(s) { console.log('DEPRECATION WARNING:', s) }, + index: null, + method: null, + optional: false + }, options || {}) + + if (options.optional && ((value === undefined) || (value === null)) ) { + return true + } + + if (value === undefined) { + throw new Error('No value has been passed.') + } + + if (expectation === undefined) { + throw new Error('No expectation has been passed.') + } + + return false + || validateDeprication(value, expectation, options) + || validate(value, expectation, options) + } +} + +var generateDeprecationWarning = function(value, expectation, options) { + options = options || {} + + if (options.method && options.index) { + return [ + 'The', + {1:'first',2:'second',3:'third',4:'fourth',5:'fifth'}[options.index], + 'parameter of', + options.method, + 'should be a', + extractClassName(expectation) + '!' + ].join(" ") + } else { + return ["Expected", cJSON.stringify(value), "to be", extractClassName(expectation) + '!'].join(" ") + } +} + +var matchesExpectation = function(value, expectation) { + if (typeof expectation === 'string') { + return (typeof value === expectation.toString()) + } else { + return (value instanceof expectation) + } +} + +var validateDeprication = function(value, expectation, options) { + if (options.deprecated) { + if (matchesExpectation(value, options.deprecated)) { + options.onDeprecated(options.deprecationWarning) + return true + } + } +} + +var validate = function(value, expectation, options) { + var result = matchesExpectation(value, expectation) + + if (result) { + return result + } else if (!options.throwError) { + return false + } else { + var _value = (value === null) ? 'null' : value.toString() + , _expectation = extractClassName(expectation) + + throw new Error('The parameter (value: ' + _value + ') is no ' + _expectation + '.') + } +} + +var extractClassName = function(o) { + if (typeof o === 'string') { + return o + } else if (!!o) { + return o.toString().match(/function ([^\(]+)/)[1] + } else { + return 'undefined' + } +} diff --git a/package.json b/package.json index 4e3e0e1deb40..c0ec534f1afb 100644 --- a/package.json +++ b/package.json @@ -40,26 +40,28 @@ "underscore.string": "~2.3.0", "lingo": "~0.0.5", "validator": "~1.5.0", - "moment": "~2.2.1", + "moment": "~2.4.0", "commander": "~2.0.0", "dottie": "0.0.8-0", "toposort-class": "~0.2.0", "generic-pool": "2.0.4", + "sql": "~0.31.0", + "circular-json": "~0.1.5", "bluebird": "~0.11.5", - "sql": "~0.28.0", "node-uuid": "~1.4.1" }, "devDependencies": { "sqlite3": "~2.1.12", "mysql": "~2.0.0-alpha9", - "pg": "~2.6.0", + "pg": "~2.8.1", "watchr": "~2.4.3", "yuidocjs": "~0.3.36", "chai": "~1.8.0", "mocha": "~1.13.0", "chai-datetime": "~1.1.1", "sinon": "~1.7.3", - "mariasql": "git://github.com/sequelize/node-mariasql.git", + "mariasql": "git://github.com/mscdex/node-mariasql.git", + "chai-spies": "~0.5.1", "lcov-result-merger": "0.0.2", "istanbul": "~0.1.45", "coveralls": "~2.5.0" diff --git a/test/associations/belongs-to.test.js b/test/associations/belongs-to.test.js index 1bb14c97f0d6..89d8d667378a 100644 --- a/test/associations/belongs-to.test.js +++ b/test/associations/belongs-to.test.js @@ -9,7 +9,7 @@ chai.Assertion.includeStack = true describe(Support.getTestDialectTeaser("BelongsTo"), function() { describe("Model.associations", function () { it("should store all assocations when associting to the same table multiple times", function () { - var User = this.sequelize.define('User', {}) + var User = this.sequelize.define('User', {}) , Group = this.sequelize.define('Group', {}) Group.belongsTo(User) @@ -20,7 +20,66 @@ describe(Support.getTestDialectTeaser("BelongsTo"), function() { }) }) + describe('getAssociation', function() { + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Support.Sequelize.STRING }) + , Group = sequelize.define('Group', { name: Support.Sequelize.STRING }) + + Group.belongsTo(User) + + sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Group.create({ name: 'bar' }).success(function(group) { + sequelize.transaction(function(t) { + group.setUser(user, { transaction: t }).success(function() { + Group.all().success(function(groups) { + groups[0].getUser().success(function(associatedUser) { + expect(associatedUser).to.be.null + Group.all({ transaction: t }).success(function(groups) { + groups[0].getUser({ transaction: t }).success(function(associatedUser) { + expect(associatedUser).to.be.not.null + t.rollback().success(function() { done() }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + describe('setAssociation', function() { + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Support.Sequelize.STRING }) + , Group = sequelize.define('Group', { name: Support.Sequelize.STRING }) + + Group.belongsTo(User) + + sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Group.create({ name: 'bar' }).success(function(group) { + sequelize.transaction(function(t) { + group.setUser(user, { transaction: t }).success(function() { + Group.all().success(function(groups) { + groups[0].getUser().success(function(associatedUser) { + expect(associatedUser).to.be.null + t.rollback().success(function() { done() }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + it('can set the association with declared primary keys...', function(done) { var User = this.sequelize.define('UserXYZ', { user_id: {type: DataTypes.INTEGER, primaryKey: true }, username: DataTypes.STRING }) , Task = this.sequelize.define('TaskXYZ', { task_id: {type: DataTypes.INTEGER, primaryKey: true }, title: DataTypes.STRING }) @@ -207,15 +266,15 @@ describe(Support.getTestDialectTeaser("BelongsTo"), function() { describe("Association column", function() { it('has correct type for non-id primary keys with non-integer type', function(done) { - var User = this.sequelize.define('UserPKBT', { - username: { + var User = this.sequelize.define('UserPKBT', { + username: { type: DataTypes.STRING } }) , self = this - var Group = this.sequelize.define('GroupPKBT', { - name: { + var Group = this.sequelize.define('GroupPKBT', { + name: { type: DataTypes.STRING, primaryKey: true } diff --git a/test/associations/has-many.test.js b/test/associations/has-many.test.js index 4fa91c4e545f..4ea3cbad79de 100644 --- a/test/associations/has-many.test.js +++ b/test/associations/has-many.test.js @@ -41,6 +41,37 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { }) }) + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var Article = sequelize.define('Article', { 'title': DataTypes.STRING }) + , Label = sequelize.define('Label', { 'text': DataTypes.STRING }) + + Article.hasMany(Label) + + sequelize.sync({ force: true }).success(function() { + Article.create({ title: 'foo' }).success(function(article) { + Label.create({ text: 'bar' }).success(function(label) { + sequelize.transaction(function(t) { + article.setLabels([ label ], { transaction: t }).success(function() { + Article.all({ transaction: t }).success(function(articles) { + articles[0].hasLabel(label).success(function(hasLabel) { + expect(hasLabel).to.be.false + Article.all({ transaction: t }).success(function(articles) { + articles[0].hasLabel(label, { transaction: t }).success(function(hasLabel) { + expect(hasLabel).to.be.true + t.rollback().success(function() { done() }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + it('does not have any labels assigned to it initially', function(done) { var chainer = new Sequelize.Utils.QueryChainer([ this.Article.create({ title: 'Articl2e' }), @@ -106,6 +137,37 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { }) }) + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var Article = sequelize.define('Article', { 'title': DataTypes.STRING }) + , Label = sequelize.define('Label', { 'text': DataTypes.STRING }) + + Article.hasMany(Label) + + sequelize.sync({ force: true }).success(function() { + Article.create({ title: 'foo' }).success(function(article) { + Label.create({ text: 'bar' }).success(function(label) { + sequelize.transaction(function(t) { + article.setLabels([ label ], { transaction: t }).success(function() { + Article.all({ transaction: t }).success(function(articles) { + articles[0].hasLabels([ label ]).success(function(hasLabel) { + expect(hasLabel).to.be.false + Article.all({ transaction: t }).success(function(articles) { + articles[0].hasLabels([ label ], { transaction: t }).success(function(hasLabel) { + expect(hasLabel).to.be.true + t.rollback().success(function() { done() }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + it('answers false if only some labels have been assigned', function(done) { var chainer = new Sequelize.Utils.QueryChainer([ this.Article.create({ title: 'Article' }), @@ -142,6 +204,38 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { }) describe('setAssociations', function() { + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var Article = sequelize.define('Article', { 'title': DataTypes.STRING }) + , Label = sequelize.define('Label', { 'text': DataTypes.STRING }) + + Article.hasMany(Label) + + sequelize.sync({ force: true }).success(function() { + Article.create({ title: 'foo' }).success(function(article) { + Label.create({ text: 'bar' }).success(function(label) { + sequelize.transaction(function(t) { + article.setLabels([ label ], { transaction: t }).success(function() { + Label + .findAll({ where: { ArticleId: article.id }, transaction: undefined }) + .success(function(labels) { + expect(labels.length).to.equal(0) + + Label + .findAll({ where: { ArticleId: article.id }, transaction: t }) + .success(function(labels) { + expect(labels.length).to.equal(1) + t.rollback().success(function() { done() }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + it("clears associations when passing null to the set-method", function(done) { var User = this.sequelize.define('User', { username: DataTypes.STRING }) , Task = this.sequelize.define('Task', { title: DataTypes.STRING }) @@ -365,6 +459,38 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { }) }) + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var Article = sequelize.define('Article', { 'title': DataTypes.STRING }) + , Label = sequelize.define('Label', { 'text': DataTypes.STRING }) + + Article.hasMany(Label) + Label.hasMany(Article) + + sequelize.sync({ force: true }).success(function() { + Article.create({ title: 'foo' }).success(function(article) { + Label.create({ text: 'bar' }).success(function(label) { + sequelize.transaction(function(t) { + article.setLabels([ label ], { transaction: t }).success(function() { + Article.all({ transaction: t }).success(function(articles) { + articles[0].getLabels().success(function(labels) { + expect(labels).to.have.length(0) + Article.all({ transaction: t }).success(function(articles) { + articles[0].getLabels({ transaction: t }).success(function(labels) { + expect(labels).to.have.length(1) + t.rollback().success(function() { done() }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + it("gets all associated objects when no options are passed", function(done) { this.User.find({where: {username: 'John'}}).success(function (john) { john.getTasks().success(function (tasks) { diff --git a/test/associations/has-one.test.js b/test/associations/has-one.test.js index 6869c7db63e8..9242a3bf1d16 100644 --- a/test/associations/has-one.test.js +++ b/test/associations/has-one.test.js @@ -21,6 +21,37 @@ describe(Support.getTestDialectTeaser("HasOne"), function() { }) describe('getAssocation', function() { + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Support.Sequelize.STRING }) + , Group = sequelize.define('Group', { name: Support.Sequelize.STRING }) + + Group.hasOne(User) + + sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Group.create({ name: 'bar' }).success(function(group) { + sequelize.transaction(function(t) { + group.setUser(user, { transaction: t }).success(function() { + Group.all().success(function(groups) { + groups[0].getUser().success(function(associatedUser) { + expect(associatedUser).to.be.null + Group.all({ transaction: t }).success(function(groups) { + groups[0].getUser({ transaction: t }).success(function(associatedUser) { + expect(associatedUser).to.be.not.null + t.rollback().success(function() { done() }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + it('should be able to handle a where object that\'s a first class citizen.', function(done) { var User = this.sequelize.define('UserXYZ', { username: Sequelize.STRING }) , Task = this.sequelize.define('TaskXYZ', { title: Sequelize.STRING, status: Sequelize.STRING }) @@ -44,9 +75,36 @@ describe(Support.getTestDialectTeaser("HasOne"), function() { }) describe('setAssociation', function() { + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Support.Sequelize.STRING }) + , Group = sequelize.define('Group', { name: Support.Sequelize.STRING }) + + Group.hasOne(User) + + sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Group.create({ name: 'bar' }).success(function(group) { + sequelize.transaction(function(t) { + group.setUser(user, { transaction: t }).success(function() { + Group.all().success(function(groups) { + groups[0].getUser().success(function(associatedUser) { + expect(associatedUser).to.be.null + t.rollback().success(function() { done() }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + it('can set an association with predefined primary keys', function(done) { var User = this.sequelize.define('UserXYZZ', { userCoolIdTag: { type: Sequelize.INTEGER, primaryKey: true }, username: Sequelize.STRING }) , Task = this.sequelize.define('TaskXYZZ', { taskOrSomething: { type: Sequelize.INTEGER, primaryKey: true }, title: Sequelize.STRING }) + , self = this User.hasOne(Task, {foreignKey: 'userCoolIdTag'}) @@ -244,15 +302,15 @@ describe(Support.getTestDialectTeaser("HasOne"), function() { describe("Association column", function() { it('has correct type for non-id primary keys with non-integer type', function(done) { - var User = this.sequelize.define('UserPKBT', { - username: { + var User = this.sequelize.define('UserPKBT', { + username: { type: Sequelize.STRING } }) , self = this - var Group = this.sequelize.define('GroupPKBT', { - name: { + var Group = this.sequelize.define('GroupPKBT', { + name: { type: Sequelize.STRING, primaryKey: true } @@ -293,4 +351,4 @@ describe(Support.getTestDialectTeaser("HasOne"), function() { }) }) -}) \ No newline at end of file +}) diff --git a/test/config/config.js b/test/config/config.js index a2e28c97c3b9..269a2e5ba608 100644 --- a/test/config/config.js +++ b/test/config/config.js @@ -31,7 +31,7 @@ module.exports = { postgres: { 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, + password: process.env.SEQ_PG_PW || process.env.SEQ_PW || "postgres", 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: { diff --git a/test/dao-factory.test.js b/test/dao-factory.test.js index f71bdcdbd3f1..088111d68e26 100644 --- a/test/dao-factory.test.js +++ b/test/dao-factory.test.js @@ -25,6 +25,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { theDate: DataTypes.DATE, aBool: DataTypes.BOOLEAN }) + this.User.sync({ force: true }).success(function() { done() }) @@ -351,6 +352,29 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) describe('findOrInitialize', function() { + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Sequelize.STRING, foo: Sequelize.STRING }) + + User.sync({ force: true }).success(function() { + sequelize.transaction(function(t) { + User.create({ username: 'foo' }, { transaction: t }).success(function() { + User.findOrInitialize({ username: 'foo' }).success(function(user1) { + User.findOrInitialize({ username: 'foo' }, { transaction: t }).success(function(user2) { + User.findOrInitialize({ username: 'foo' }, { foo: 'asd' }, { transaction: t }).success(function(user3) { + expect(user1.isNewRecord).to.be.true + expect(user2.isNewRecord).to.be.false + expect(user3.isNewRecord).to.be.false + t.commit().success(function() { done() }) + }) + }) + }) + }) + }) + }) + }) + }) + describe('returns an instance if it already exists', function() { it('with a single find field', function (done) { var self = this @@ -406,7 +430,35 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) describe('findOrCreate', function () { - it("Returns instance if already existent. Single find field.", function(done) { + it("supports transactions", function(done) { + var self = this + + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('user_with_transaction', { username: Sequelize.STRING, data: Sequelize.STRING }) + + User + .sync({ force: true }) + .success(function() { + sequelize.transaction(function(t) { + User.findOrCreate({ username: 'Username' }, { data: 'some data' }, { transaction: t }).complete(function(err) { + expect(err).to.be.null + + User.count().success(function(count) { + expect(count).to.equal(0) + t.commit().success(function() { + User.count().success(function(count) { + expect(count).to.equal(1) + done() + }) + }) + }) + }) + }) + }) + }) + }) + + it("returns instance if already existent. Single find field.", function(done) { var self = this, data = { username: 'Username' @@ -460,6 +512,30 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) describe('create', function() { + it('supports transactions', function(done) { + var self = this + + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('user_with_transaction', { username: Sequelize.STRING }) + + User.sync({ force: true }).success(function() { + sequelize.transaction(function(t) { + User.create({ username: 'user' }, { transaction: t }).success(function() { + User.count().success(function(count) { + expect(count).to.equal(0) + t.commit().success(function() { + User.count().success(function(count) { + expect(count).to.equal(1) + done() + }) + }) + }) + }) + }) + }) + }) + }) + it('is possible to use casting when creating an instance', function (done) { var self = this , type = Support.dialectIsMySQL() ? 'signed' : 'integer' @@ -620,6 +696,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) }) }) + it("casts empty array correct for postgres update", function(done) { if (dialect !== "postgres") { expect('').to.equal('') @@ -644,7 +721,6 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) }) - it("doesn't allow duplicated records with unique:true", function(done) { var User = this.sequelize.define('UserWithUniqueUsername', { username: { type: Sequelize.STRING, unique: true } @@ -798,7 +874,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { var self = this , data = { username: 'Peter', secretValue: '42' } - this.User.create(data, ['username']).success(function(user) { + this.User.create(data, { fields: ['username'] }).success(function(user) { self.User.find(user.id).success(function(_user) { expect(_user.username).to.equal(data.username) expect(_user.secretValue).not.to.equal(data.secretValue) @@ -988,6 +1064,28 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) describe('bulkCreate', function() { + it("supports transactions", function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Sequelize.STRING }) + + User.sync({ force: true }).success(function() { + sequelize.transaction(function(t) { + User + .bulkCreate([{ username: 'foo' }, { username: 'bar' }], { transaction: t }) + .success(function() { + User.count().success(function(count1) { + User.count({ transaction: t }).success(function(count2) { + expect(count1).to.equal(0) + expect(count2).to.equal(2) + t.rollback().success(function(){ done() }) + }) + }) + }) + }) + }) + }) + }) + it('properly handles disparate field lists', function(done) { var self = this , data = [{username: 'Peter', secretValue: '42' }, @@ -1009,7 +1107,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { , data = [{ username: 'Peter', secretValue: '42' }, { username: 'Paul', secretValue: '23'}] - this.User.bulkCreate(data, ['username']).success(function() { + this.User.bulkCreate(data, { fields: ['username'] }).success(function() { self.User.findAll({order: 'id'}).success(function(users) { expect(users.length).to.equal(2) expect(users[0].username).to.equal("Peter") @@ -1130,7 +1228,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { {name: 'foo', code: '123'}, {code: '1234'}, {name: 'bar', code: '1'} - ], null, {validate: true}).error(function(errors) { + ], { validate: true }).error(function(errors) { expect(errors).to.not.be.null expect(errors).to.be.instanceof(Array) expect(errors).to.have.length(2) @@ -1164,7 +1262,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { Tasks.bulkCreate([ {name: 'foo', code: '123'}, {code: '1234'} - ], ['code'], {validate: true}).success(function() { + ], { fields: ['code'], validate: true }).success(function() { // we passed! done() }) @@ -1192,6 +1290,28 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) describe('update', function() { + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Sequelize.STRING }) + + User.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function() { + sequelize.transaction(function(t) { + User.update({ username: 'bar' }, {}, { transaction: t }).success(function() { + User.all().success(function(users1) { + User.all({ transaction: t }).success(function(users2) { + expect(users1[0].username).to.equal('foo') + expect(users2[0].username).to.equal('bar') + t.rollback().success(function(){ done() }) + }) + }) + }) + }) + }) + }) + }) + }) + it('updates the attributes that we select only without updating createdAt', function(done) { var User = this.sequelize.define('User1', { username: Sequelize.STRING, @@ -1314,20 +1434,21 @@ 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('UserDestroy', { - name: Sequelize.STRING, - bio: Sequelize.TEXT - }) + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Sequelize.STRING }) - UserDestroy.sync({ force: true }).success(function() { - UserDestroy.create({name: 'hallo', bio: 'welt'}).success(function(u) { - UserDestroy.all().success(function(users) { - expect(users.length).to.equal(1) - u.destroy().success(function() { - UserDestroy.all().success(function(users) { - expect(users.length).to.equal(0) - done() + User.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function() { + sequelize.transaction(function(t) { + User.destroy({}, { transaction: t }).success(function() { + User.count().success(function(count1) { + User.count({ transaction: t }).success(function(count2) { + expect(count1).to.equal(1) + expect(count2).to.equal(0) + t.rollback().success(function(){ done() }) + }) + }) }) }) }) @@ -1335,26 +1456,6 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) }) - it('allows sql logging of delete statements', function(done) { - var UserDelete = this.sequelize.define('UserDelete', { - name: Sequelize.STRING, - bio: Sequelize.TEXT - }) - - UserDelete.sync({ force: true }).success(function() { - UserDelete.create({name: 'hallo', bio: 'welt'}).success(function(u) { - UserDelete.all().success(function(users) { - expect(users.length).to.equal(1) - u.destroy().on('sql', function(sql) { - expect(sql).to.exist - expect(sql.toUpperCase().indexOf("DELETE")).to.be.above(-1) - done() - }) - }) - }) - }) - }) - it('deletes values that match filter', function(done) { var self = this , data = [{ username: 'Peter', secretValue: '42' }, @@ -1449,544 +1550,147 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) }) - describe('special where conditions/smartWhere object', function() { - beforeEach(function(done) { - var self = this + describe('find', function() { + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Sequelize.STRING }) - this.User.bulkCreate([ - {username: 'boo', intVal: 5, theDate: '2013-01-01 12:00'}, - {username: 'boo2', intVal: 10, theDate: '2013-01-10 12:00'}, - {username: 'boo3', intVal: null, theDate: null} - ]).success(function(user2) { - done() + User.sync({ force: true }).success(function() { + sequelize.transaction(function(t) { + User.create({ username: 'foo' }, { transaction: t }).success(function() { + User.find({ username: 'foo' }).success(function(user1) { + User.find({ username: 'foo' }, { transaction: t }).success(function(user2) { + expect(user1).to.be.null + expect(user2).to.not.be.null + + t.rollback().success(function() { + done() + }) + }) + }) + }) + }) + }) }) }) - it('should be able to find rows where attribute is in a list of values', function (done) { - this.User.findAll({ - where: { - username: ['boo', 'boo2', 'boo3'] - } - }).success(function(users){ - expect(users).to.have.length(3); - done() - }); - }) - - it('should not break when trying to find rows using an array of primary keys', function (done) { - this.User.findAll({ - where: { - id: [1, 2, 3, 4] - } - }).success(function(users){ - expect(users).to.have.length(3) - done(); - }); - }) + describe('general / basic function', function() { + beforeEach(function(done) { + var self = this + this.User.create({username: 'barfooz'}).success(function(user) { + self.UserPrimary = self.sequelize.define('UserPrimary', { + specialkey: { + type: DataTypes.STRING, + primaryKey: true + } + }) - it('should be able to find a row using like', function(done) { - this.User.findAll({ - where: { - username: { - like: '%2' - } - } - }).success(function(users) { - expect(users).to.be.an.instanceof(Array) - expect(users).to.have.length(1) - expect(users[0].username).to.equal('boo2') - expect(users[0].intVal).to.equal(10) - done() + self.UserPrimary.sync({force: true}).success(function() { + self.UserPrimary.create({specialkey: 'a string'}).success(function() { + self.user = user + done() + }) + }) + }) }) - }) - it('should be able to find a row using not like', function(done) { - this.User.findAll({ - where: { - username: { - nlike: '%2' - } - } - }).success(function(users) { - expect(users).to.be.an.instanceof(Array) - expect(users).to.have.length(2) - expect(users[0].username).to.equal('boo') - expect(users[0].intVal).to.equal(5) - expect(users[1].username).to.equal('boo3') - expect(users[1].intVal).to.equal(null) - done() - }) - }) + it('does not modify the passed arguments', function (done) { + var options = { where: ['specialkey = ?', 'awesome']} - it('should be able to find a row between a certain date using the between shortcut', function(done) { - this.User.findAll({ - where: { - theDate: { - '..': ['2013-01-02', '2013-01-11'] - } - } - }).success(function(users) { - expect(users).to.have.length(1) - expect(users[0].username).to.equal('boo2') - expect(users[0].intVal).to.equal(10) - done() + this.UserPrimary.find(options).success(function(user) { + expect(options).to.deep.equal({ where: ['specialkey = ?', 'awesome']}) + done() + }) }) - }) - it('should be able to find a row not between a certain integer using the not between shortcut', function(done) { - this.User.findAll({ - where: { - intVal: { - '!..': [8, 10] - } - } - }).success(function(users) { - expect(users).to.have.length(1) - expect(users[0].username).to.equal('boo') - expect(users[0].intVal).to.equal(5) - done() + it('doesn\'t throw an error when entering in a non integer value for a specified primary field', function(done) { + this.UserPrimary.find('a string').success(function(user) { + expect(user.specialkey).to.equal('a string') + done() + }) }) - }) - - it('should be able to handle false/true values just fine...', function(done) { - var User = this.User - , escapeChar = (dialect === "postgres") ? '"' : '`' - - User.bulkCreate([ - {username: 'boo5', aBool: false}, - {username: 'boo6', aBool: true} - ]).success(function() { - User.all({where: [escapeChar + 'aBool' + escapeChar + ' = ?', false]}).success(function(users) { - expect(users).to.have.length(1) - expect(users[0].username).to.equal('boo5') - User.all({where: [escapeChar + 'aBool' + escapeChar + ' = ?', true]}).success(function(_users) { - expect(_users).to.have.length(1) - expect(_users[0].username).to.equal('boo6') - done() - }) + it('doesn\'t throw an error when entering in a non integer value', function(done) { + this.User.find('a string value').success(function(user) { + expect(user).to.be.null + done() }) }) - }) - it('should be able to handle false/true values through associations as well...', function(done) { - var User = this.User - , escapeChar = (dialect === "postgres") ? '"' : '`' - var Passports = this.sequelize.define('Passports', { - isActive: Sequelize.BOOLEAN + it('returns a single dao', function(done) { + var self = this + this.User.find(this.user.id).success(function(user) { + expect(Array.isArray(user)).to.not.be.ok + expect(user.id).to.equal(self.user.id) + expect(user.id).to.equal(1) + done() + }) }) - User.hasMany(Passports) - Passports.belongsTo(User) - - User.sync({ force: true }).success(function() { - Passports.sync({ force: true }).success(function() { - User.bulkCreate([ - {username: 'boo5', aBool: false}, - {username: 'boo6', aBool: true} - ]).success(function() { - Passports.bulkCreate([ - {isActive: true}, - {isActive: false} - ]).success(function() { - User.find(1).success(function(user) { - Passports.find(1).success(function(passport) { - user.setPassports([passport]).success(function() { - User.find(2).success(function(_user) { - Passports.find(2).success(function(_passport) { - _user.setPassports([_passport]).success(function() { - _user.getPassports({where: [escapeChar + 'isActive' + escapeChar + ' = ?', false]}).success(function(theFalsePassport) { - user.getPassports({where: [escapeChar + 'isActive' + escapeChar + ' = ?', true]}).success(function(theTruePassport) { - expect(theFalsePassport).to.have.length(1) - expect(theFalsePassport[0].isActive).to.be.false - expect(theTruePassport).to.have.length(1) - expect(theTruePassport[0].isActive).to.be.true - done() - }) - }) - }) - }) - }) - }) - }) - }) - }) - }) + it('returns a single dao given a string id', function(done) { + var self = this + this.User.find(this.user.id + '').success(function(user) { + expect(Array.isArray(user)).to.not.be.ok + expect(user.id).to.equal(self.user.id) + expect(user.id).to.equal(1) + 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 } - }, { - charset: 'utf8', - collate: 'utf8_general_ci', - omitNull: true - }) - - , User = this.sequelize.define('User', { - name: { type: DataTypes.STRING, allowNull: false, unique: true }, - password: { type: DataTypes.STRING, allowNull: false }, - isAdmin: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false } - }, { - charset: 'utf8', - collate: 'utf8_general_ci' - }) - - User.hasMany(Session, { as: 'Sessions' }) - Session.belongsTo(User) + it("should make aliased attributes available", function(done) { + this.User.find({ + where: { id: 1 }, + attributes: ['id', ['username', 'name']] + }).success(function(user) { + expect(user.name).to.equal('barfooz') + done() + }) + }) - Session.sync({ force: true }).success(function() { - User.sync({ force: true }).success(function() { - User.create({name: 'Name1', password: '123', isAdmin: false}).success(function(user) { - var sess = Session.build({ - lastUpdate: new Date(), - token: '123' - }) + it("should not try to convert boolean values if they are not selected", function(done) { + var UserWithBoolean = this.sequelize.define('UserBoolean', { + active: Sequelize.BOOLEAN + }) - user.addSession(sess).success(function(u) { - expect(u.token).to.equal('123') + UserWithBoolean.sync({force: true}).success(function () { + UserWithBoolean.create({ active: true }).success(function(user) { + UserWithBoolean.find({ where: { id: user.id }, attributes: [ 'id' ] }).success(function(user) { + expect(user.active).not.to.exist done() }) }) }) }) - }) - it('should be able to find a row between a certain date', function(done) { - this.User.findAll({ - where: { - theDate: { - between: ['2013-01-02', '2013-01-11'] - } - } - }).success(function(users) { - expect(users).to.have.length(1) - expect(users[0].username).to.equal('boo2') - expect(users[0].intVal).to.equal(10) - done() + it('finds a specific user via where option', function(done) { + this.User.find({ where: { username: 'barfooz' } }).success(function(user) { + expect(user.username).to.equal('barfooz') + done() + }) }) - }) - it('should be able to find a row between a certain date and an additional where clause', function(done) { - this.User.findAll({ - where: { - theDate: { - between: ['2013-01-02', '2013-01-11'] - }, - intVal: 10 - } - }).success(function(users) { - expect(users).to.have.length(1) - expect(users[0].username).to.equal('boo2') - expect(users[0].intVal).to.equal(10) - done() + it("doesn't find a user if conditions are not matching", function(done) { + this.User.find({ where: { username: 'foo' } }).success(function(user) { + expect(user).to.be.null + done() + }) }) - }) - it('should be able to find a row not between a certain integer', function(done) { - this.User.findAll({ - where: { - intVal: { - nbetween: [8, 10] - } - } - }).success(function(users) { - expect(users).to.have.length(1) - expect(users[0].username).to.equal('boo') - expect(users[0].intVal).to.equal(5) - done() + it('allows sql logging', function(done) { + this.User.find({ where: { username: 'foo' } }).on('sql', function(sql) { + expect(sql).to.exist + expect(sql.toUpperCase().indexOf("SELECT")).to.be.above(-1) + done() + }) }) - }) - it('should be able to find a row using not between and between logic', function(done) { - this.User.findAll({ - where: { - theDate: { - between: ['2012-12-10', '2013-01-02'], - nbetween: ['2013-01-04', '2013-01-20'] - } - } - }).success(function(users) { - expect(users).to.have.length(1) - expect(users[0].username).to.equal('boo') - expect(users[0].intVal).to.equal(5) - done() - }) - }) - - it('should be able to find a row using not between and between logic with dates', function(done) { - this.User.findAll({ - where: { - theDate: { - between: [new Date('2012-12-10'), new Date('2013-01-02')], - nbetween: [new Date('2013-01-04'), new Date('2013-01-20')] - } - } - }).success(function(users) { - expect(users).to.have.length(1) - expect(users[0].username).to.equal('boo') - expect(users[0].intVal).to.equal(5) - done() - }) - }) - - it('should be able to find a row using greater than or equal to logic with dates', function(done) { - this.User.findAll({ - where: { - theDate: { - gte: new Date('2013-01-09') - } - } - }).success(function(users) { - expect(users).to.have.length(1) - expect(users[0].username).to.equal('boo2') - expect(users[0].intVal).to.equal(10) - done() - }) - }) - - it('should be able to find a row using greater than or equal to', function(done) { - this.User.find({ - where: { - intVal: { - gte: 6 - } - } - }).success(function(user) { - expect(user.username).to.equal('boo2') - expect(user.intVal).to.equal(10) - done() - }) - }) - - it('should be able to find a row using greater than', function(done) { - this.User.find({ - where: { - intVal: { - gt: 5 - } - } - }).success(function(user) { - expect(user.username).to.equal('boo2') - expect(user.intVal).to.equal(10) - done() - }) - }) - - it('should be able to find a row using lesser than or equal to', function(done) { - this.User.find({ - where: { - intVal: { - lte: 5 - } - } - }).success(function(user) { - expect(user.username).to.equal('boo') - expect(user.intVal).to.equal(5) - done() - }) - }) - - it('should be able to find a row using lesser than', function(done) { - this.User.find({ - where: { - intVal: { - lt: 6 - } - } - }).success(function(user) { - expect(user.username).to.equal('boo') - expect(user.intVal).to.equal(5) - done() - }) - }) - - it('should have no problem finding a row using lesser and greater than', function(done) { - this.User.findAll({ - where: { - intVal: { - lt: 6, - gt: 4 - } - } - }).success(function(users) { - expect(users).to.have.length(1) - expect(users[0].username).to.equal('boo') - expect(users[0].intVal).to.equal(5) - done() - }) - }) - - it('should be able to find a row using not equal to logic', function(done) { - this.User.find({ - where: { - intVal: { - ne: 10 - } - } - }).success(function(user) { - expect(user.username).to.equal('boo') - expect(user.intVal).to.equal(5) - done() - }) - }) - - it('should be able to find a row using not equal to null logic', function(done) { - this.User.findAll({ - where: { - intVal: { - ne: null - } - } - }).success(function(users) { - expect(users).to.have.length(2) - expect(users[0].username).to.equal('boo') - expect(users[0].intVal).to.equal(5) - expect(users[1].username).to.equal('boo2') - expect(users[1].intVal).to.equal(10) - done() - }) - }) - - it('should be able to find multiple users with any of the special where logic properties', function(done) { - this.User.findAll({ - where: { - intVal: { - lte: 10 - } - } - }).success(function(users) { - expect(users).to.have.length(2) - expect(users[0].username).to.equal('boo') - expect(users[0].intVal).to.equal(5) - expect(users[1].username).to.equal('boo2') - expect(users[1].intVal).to.equal(10) - done() - }) - }) - }) - - describe('find', function() { - describe('general / basic function', function() { - beforeEach(function(done) { - var self = this - this.User.create({username: 'barfooz'}).success(function(user) { - self.UserPrimary = self.sequelize.define('UserPrimary', { - specialkey: { - type: DataTypes.STRING, - primaryKey: true - } - }) - - self.UserPrimary.sync({force: true}).success(function() { - self.UserPrimary.create({specialkey: 'a string'}).success(function() { - self.user = user - done() - }) - }) - }) - }) - - it('does not modify the passed arguments', function (done) { - var options = { where: ['specialkey = ?', 'awesome']} - - this.UserPrimary.find(options).success(function(user) { - expect(options).to.deep.equal({ where: ['specialkey = ?', 'awesome']}) - done() - }) - }) - - it('doesn\'t throw an error when entering in a non integer value for a specified primary field', function(done) { - this.UserPrimary.find('a string').success(function(user) { - expect(user.specialkey).to.equal('a string') - done() - }) - }) - - it('doesn\'t throw an error when entering in a non integer value', function(done) { - this.User.find('a string value').success(function(user) { - expect(user).to.be.null - done() - }) - }) - - it('returns a single dao', function(done) { - var self = this - this.User.find(this.user.id).success(function(user) { - expect(Array.isArray(user)).to.not.be.ok - expect(user.id).to.equal(self.user.id) - expect(user.id).to.equal(1) - done() - }) - }) - - it('returns a single dao given a string id', function(done) { - var self = this - this.User.find(this.user.id + '').success(function(user) { - expect(Array.isArray(user)).to.not.be.ok - expect(user.id).to.equal(self.user.id) - expect(user.id).to.equal(1) - done() - }) - }) - - it("should make aliased attributes available", function(done) { - this.User.find({ - where: { id: 1 }, - attributes: ['id', ['username', 'name']] - }).success(function(user) { - expect(user.name).to.equal('barfooz') - done() - }) - }) - - it("should not try to convert boolean values if they are not selected", function(done) { - var UserWithBoolean = this.sequelize.define('UserBoolean', { - active: Sequelize.BOOLEAN - }) - - UserWithBoolean.sync({force: true}).success(function () { - UserWithBoolean.create({ active: true }).success(function(user) { - UserWithBoolean.find({ where: { id: user.id }, attributes: [ 'id' ] }).success(function(user) { - expect(user.active).not.to.exist - done() - }) - }) - }) - }) - - it('finds a specific user via where option', function(done) { - this.User.find({ where: { username: 'barfooz' } }).success(function(user) { - expect(user.username).to.equal('barfooz') - done() - }) - }) - - it("doesn't find a user if conditions are not matching", function(done) { - this.User.find({ where: { username: 'foo' } }).success(function(user) { - expect(user).to.be.null - done() - }) - }) - - it('allows sql logging', function(done) { - this.User.find({ where: { username: 'foo' } }).on('sql', function(sql) { - expect(sql).to.exist - expect(sql.toUpperCase().indexOf("SELECT")).to.be.above(-1) - done() - }) - }) - - it('ignores passed limit option', function(done) { - this.User.find({ limit: 10 }).success(function(user) { - // it returns an object instead of an array - expect(Array.isArray(user)).to.not.be.ok - expect(user.dataValues.hasOwnProperty('username')).to.be.ok - done() - }) + it('ignores passed limit option', function(done) { + this.User.find({ limit: 10 }).success(function(user) { + // it returns an object instead of an array + expect(Array.isArray(user)).to.not.be.ok + expect(user.dataValues.hasOwnProperty('username')).to.be.ok + done() + }) }) it('finds entries via primary keys', function(done) { @@ -2725,6 +2429,423 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) describe('findAll', function() { + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Sequelize.STRING }) + + User.sync({ force: true }).success(function() { + sequelize.transaction(function(t) { + User.create({ username: 'foo' }, { transaction: t }).success(function() { + User.findAll({ username: 'foo' }).success(function(users1) { + User.findAll({ transaction: t }).success(function(users2) { + User.findAll({ username: 'foo' }, { transaction: t }).success(function(users3) { + expect(users1.length).to.equal(0) + expect(users2.length).to.equal(1) + expect(users3.length).to.equal(1) + + t.rollback().success(function() { + done() + }) + }) + }) + }) + }) + }) + }) + }) + }) + + describe('special where conditions/smartWhere object', function() { + beforeEach(function(done) { + var self = this + + this.User.bulkCreate([ + {username: 'boo', intVal: 5, theDate: '2013-01-01 12:00'}, + {username: 'boo2', intVal: 10, theDate: '2013-01-10 12:00'} + ]).success(function(user2) { + done() + }) + }) + + it('should be able to find rows where attribute is in a list of values', function (done) { + this.User.findAll({ + where: { + username: ['boo', 'boo2'] + } + }).success(function(users){ + expect(users).to.have.length(2); + done() + }); + }) + + it('should not break when trying to find rows using an array of primary keys', function (done) { + this.User.findAll({ + where: { + id: [1, 2, 3] + } + }).success(function(users){ + done(); + }); + }) + + it('should be able to find a row using like', function(done) { + this.User.findAll({ + where: { + username: { + like: '%2' + } + } + }).success(function(users) { + expect(users).to.be.an.instanceof(Array) + expect(users).to.have.length(1) + expect(users[0].username).to.equal('boo2') + expect(users[0].intVal).to.equal(10) + done() + }) + }) + + it('should be able to find a row using not like', function(done) { + this.User.findAll({ + where: { + username: { + nlike: '%2' + } + } + }).success(function(users) { + expect(users).to.be.an.instanceof(Array) + expect(users).to.have.length(1) + expect(users[0].username).to.equal('boo') + expect(users[0].intVal).to.equal(5) + done() + }) + }) + + it('should be able to find a row between a certain date using the between shortcut', function(done) { + this.User.findAll({ + where: { + theDate: { + '..': ['2013-01-02', '2013-01-11'] + } + } + }).success(function(users) { + expect(users[0].username).to.equal('boo2') + expect(users[0].intVal).to.equal(10) + done() + }) + }) + + it('should be able to find a row not between a certain integer using the not between shortcut', function(done) { + this.User.findAll({ + where: { + intVal: { + '!..': [8, 10] + } + } + }).success(function(users) { + expect(users[0].username).to.equal('boo') + expect(users[0].intVal).to.equal(5) + done() + }) + }) + + it('should be able to handle false/true values just fine...', function(done) { + var User = this.User + , escapeChar = (dialect === "postgres") ? '"' : '`' + + User.bulkCreate([ + {username: 'boo5', aBool: false}, + {username: 'boo6', aBool: true} + ]).success(function() { + User.all({where: [escapeChar + 'aBool' + escapeChar + ' = ?', false]}).success(function(users) { + expect(users).to.have.length(1) + expect(users[0].username).to.equal('boo5') + + User.all({where: [escapeChar + 'aBool' + escapeChar + ' = ?', true]}).success(function(_users) { + expect(_users).to.have.length(1) + expect(_users[0].username).to.equal('boo6') + done() + }) + }) + }) + }) + + it('should be able to handle false/true values through associations as well...', function(done) { + var User = this.User + , escapeChar = (dialect === "postgres") ? '"' : '`' + var Passports = this.sequelize.define('Passports', { + isActive: Sequelize.BOOLEAN + }) + + User.hasMany(Passports) + Passports.belongsTo(User) + + User.sync({ force: true }).success(function() { + Passports.sync({ force: true }).success(function() { + User.bulkCreate([ + {username: 'boo5', aBool: false}, + {username: 'boo6', aBool: true} + ]).success(function() { + Passports.bulkCreate([ + {isActive: true}, + {isActive: false} + ]).success(function() { + User.find(1).success(function(user) { + Passports.find(1).success(function(passport) { + user.setPassports([passport]).success(function() { + User.find(2).success(function(_user) { + Passports.find(2).success(function(_passport) { + _user.setPassports([_passport]).success(function() { + _user.getPassports({where: [escapeChar + 'isActive' + escapeChar + ' = ?', false]}).success(function(theFalsePassport) { + user.getPassports({where: [escapeChar + 'isActive' + escapeChar + ' = ?', true]}).success(function(theTruePassport) { + expect(theFalsePassport).to.have.length(1) + expect(theFalsePassport[0].isActive).to.be.false + expect(theTruePassport).to.have.length(1) + expect(theTruePassport[0].isActive).to.be.true + 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 } + }, { + charset: 'utf8', + collate: 'utf8_general_ci', + omitNull: true + }) + + , User = this.sequelize.define('User', { + name: { type: DataTypes.STRING, allowNull: false, unique: true }, + password: { type: DataTypes.STRING, allowNull: false }, + isAdmin: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false } + }, { + charset: 'utf8', + collate: 'utf8_general_ci' + }) + + User.hasMany(Session, { as: 'Sessions' }) + Session.belongsTo(User) + + Session.sync({ force: true }).success(function() { + User.sync({ force: true }).success(function() { + User.create({name: 'Name1', password: '123', isAdmin: false}).success(function(user) { + var sess = Session.build({ + lastUpdate: new Date(), + token: '123' + }) + + user.addSession(sess).success(function(u) { + expect(u.token).to.equal('123') + done() + }) + }) + }) + }) + }) + + it('should be able to find a row between a certain date', function(done) { + this.User.findAll({ + where: { + theDate: { + between: ['2013-01-02', '2013-01-11'] + } + } + }).success(function(users) { + expect(users[0].username).to.equal('boo2') + expect(users[0].intVal).to.equal(10) + done() + }) + }) + + it('should be able to find a row between a certain date and an additional where clause', function(done) { + this.User.findAll({ + where: { + theDate: { + between: ['2013-01-02', '2013-01-11'] + }, + intVal: 10 + } + }).success(function(users) { + expect(users[0].username).to.equal('boo2') + expect(users[0].intVal).to.equal(10) + done() + }) + }) + + it('should be able to find a row not between a certain integer', function(done) { + this.User.findAll({ + where: { + intVal: { + nbetween: [8, 10] + } + } + }).success(function(users) { + expect(users[0].username).to.equal('boo') + expect(users[0].intVal).to.equal(5) + done() + }) + }) + + it('should be able to find a row using not between and between logic', function(done) { + this.User.findAll({ + where: { + theDate: { + between: ['2012-12-10', '2013-01-02'], + nbetween: ['2013-01-04', '2013-01-20'] + } + } + }).success(function(users) { + expect(users[0].username).to.equal('boo') + expect(users[0].intVal).to.equal(5) + done() + }) + }) + + it('should be able to find a row using not between and between logic with dates', function(done) { + this.User.findAll({ + where: { + theDate: { + between: [new Date('2012-12-10'), new Date('2013-01-02')], + nbetween: [new Date('2013-01-04'), new Date('2013-01-20')] + } + } + }).success(function(users) { + expect(users[0].username).to.equal('boo') + expect(users[0].intVal).to.equal(5) + done() + }) + }) + + it('should be able to find a row using greater than or equal to logic with dates', function(done) { + this.User.findAll({ + where: { + theDate: { + gte: new Date('2013-01-09') + } + } + }).success(function(users) { + expect(users[0].username).to.equal('boo2') + expect(users[0].intVal).to.equal(10) + done() + }) + }) + + it('should be able to find a row using greater than or equal to', function(done) { + this.User.find({ + where: { + intVal: { + gte: 6 + } + } + }).success(function(user) { + expect(user.username).to.equal('boo2') + expect(user.intVal).to.equal(10) + done() + }) + }) + + it('should be able to find a row using greater than', function(done) { + this.User.find({ + where: { + intVal: { + gt: 5 + } + } + }).success(function(user) { + expect(user.username).to.equal('boo2') + expect(user.intVal).to.equal(10) + done() + }) + }) + + it('should be able to find a row using lesser than or equal to', function(done) { + this.User.find({ + where: { + intVal: { + lte: 5 + } + } + }).success(function(user) { + expect(user.username).to.equal('boo') + expect(user.intVal).to.equal(5) + done() + }) + }) + + it('should be able to find a row using lesser than', function(done) { + this.User.find({ + where: { + intVal: { + lt: 6 + } + } + }).success(function(user) { + expect(user.username).to.equal('boo') + expect(user.intVal).to.equal(5) + done() + }) + }) + + it('should have no problem finding a row using lesser and greater than', function(done) { + this.User.findAll({ + where: { + intVal: { + lt: 6, + gt: 4 + } + } + }).success(function(users) { + expect(users[0].username).to.equal('boo') + expect(users[0].intVal).to.equal(5) + done() + }) + }) + + it('should be able to find a row using not equal to logic', function(done) { + this.User.find({ + where: { + intVal: { + ne: 10 + } + } + }).success(function(user) { + expect(user.username).to.equal('boo') + expect(user.intVal).to.equal(5) + done() + }) + }) + + it('should be able to find multiple users with any of the special where logic properties', function(done) { + this.User.findAll({ + where: { + intVal: { + lte: 10 + } + } + }).success(function(users) { + expect(users[0].username).to.equal('boo') + expect(users[0].intVal).to.equal(5) + expect(users[1].username).to.equal('boo2') + expect(users[1].intVal).to.equal(10) + done() + }) + }) + }) + + + describe('eager loading', function() { describe('belongsTo', function() { beforeEach(function(done) { @@ -3158,6 +3279,27 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) }) + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Sequelize.STRING }) + + User.sync({ force: true }).success(function() { + sequelize.transaction(function(t) { + User.create({ username: 'foo' }, { transaction: t }).success(function() { + + User.findAndCountAll().success(function(info1) { + User.findAndCountAll({ transaction: t }).success(function(info2) { + expect(info1.count).to.equal(0) + expect(info2.count).to.equal(1) + t.rollback().success(function(){ done() }) + }) + }) + }) + }) + }) + }) + }) + it("handles where clause [only]", function(done) { this.User.findAndCountAll({where: "id != " + this.users[0].id}).success(function(info) { expect(info.count).to.equal(2) @@ -3225,6 +3367,26 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) }) + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Sequelize.STRING }) + + User.sync({ force: true }).success(function() { + sequelize.transaction(function(t) { + User.create({ username: 'foo' }, { transaction: t }).success(function() { + User.all().success(function(users1) { + User.all({ transaction: t }).success(function(users2) { + expect(users1.length).to.equal(0) + expect(users2.length).to.equal(1) + t.rollback().success(function(){ done() }) + }) + }) + }) + }) + }) + }) + }) + it("should return all users", function(done) { this.User.all().on('success', function(users) { expect(users.length).to.equal(2) @@ -3294,6 +3456,26 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) describe('count', function() { + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Sequelize.STRING }) + + User.sync({ force: true }).success(function() { + sequelize.transaction(function(t) { + User.create({ username: 'foo' }, { transaction: t }).success(function() { + User.count().success(function(count1) { + User.count({ transaction: t }).success(function(count2) { + expect(count1).to.equal(0) + expect(count2).to.equal(1) + t.rollback().success(function(){ done() }) + }) + }) + }) + }) + }) + }) + }) + it('counts all created objects', function(done) { var self = this this.User.bulkCreate([{username: 'user1'}, {username: 'user2'}]).success(function() { @@ -3352,6 +3534,26 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) }) + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { age: Sequelize.INTEGER }) + + User.sync({ force: true }).success(function() { + sequelize.transaction(function(t) { + User.bulkCreate([{ age: 2 }, { age: 5 }, { age: 3 }], { transaction: t }).success(function() { + User.min('age').success(function(min1) { + User.min('age', { transaction: t }).success(function(min2) { + expect(min1).to.be.not.ok + expect(min2).to.equal(2) + t.rollback().success(function(){ done() }) + }) + }) + }) + }) + }) + }) + }) + it("should return the min value", function(done) { var self = this this.UserWithAge.bulkCreate([{age: 3}, { age: 2 }]).success(function() { @@ -3400,6 +3602,26 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) }) + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { age: Sequelize.INTEGER }) + + User.sync({ force: true }).success(function() { + sequelize.transaction(function(t) { + User.bulkCreate([{ age: 2 }, { age: 5 }, { age: 3 }], { transaction: t }).success(function() { + User.max('age').success(function(min1) { + User.max('age', { transaction: t }).success(function(min2) { + expect(min1).to.be.not.ok + expect(min2).to.equal(5) + t.rollback().success(function(){ done() }) + }) + }) + }) + }) + }) + }) + }) + it("should return the max value for a field named the same as an SQL reserved keyword", function(done) { var self = this this.UserWithAge.bulkCreate([{age: 2, order: 3}, {age: 3, order: 5}]).success(function(){ @@ -4092,16 +4314,33 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { this .User - .create({ username: "foo" }) - .then(function() { - return self.User.create({ username: "bar" }) - }) - .then(function() { - return self.User.create({ username: "baz" }) - }) + .sync({ force: true }) + .then(function() { return self.User.create({ username: "foo" }) }) + .then(function() { return self.User.create({ username: "bar" }) }) + .then(function() { return self.User.create({ username: "baz" }) }) .then(function() { done() }) }) + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Sequelize.STRING }) + + User.sync({ force: true }).success(function() { + sequelize.transaction(function(t) { + User.create({ username: 'foo' }, { transaction: t }).success(function() { + User.where({ username: "foo" }).exec().success(function(users1) { + User.where({ username: "foo" }).exec({ transaction: t }).success(function(users2) { + expect(users1).to.have.length(0) + expect(users2).to.have.length(1) + t.rollback().success(function() { done() }) + }) + }) + }) + }) + }) + }) + }) + it("selects all users with name 'foo'", function(done) { this .User diff --git a/test/dao.test.js b/test/dao.test.js index 1eef7f932aa6..e8d129c346d4 100644 --- a/test/dao.test.js +++ b/test/dao.test.js @@ -269,10 +269,32 @@ describe(Support.getTestDialectTeaser("DAO"), function () { }) }) + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { number: Support.Sequelize.INTEGER }) + + User.sync({ force: true }).success(function() { + User.create({ number: 1 }).success(function(user) { + sequelize.transaction(function(t) { + user.increment('number', { by: 2, transaction: t }).success(function() { + User.all().success(function(users1) { + User.all({ transaction: t }).success(function(users2) { + expect(users1[0].number).to.equal(1) + expect(users2[0].number).to.equal(3) + t.rollback().success(function() { done() }) + }) + }) + }) + }) + }) + }) + }) + }) + it('with array', function(done) { var self = this this.User.find(1).complete(function(err, user1) { - user1.increment(['aNumber'], 2).complete(function() { + user1.increment(['aNumber'], { by: 2 }).complete(function() { self.User.find(1).complete(function(err, user3) { expect(user3.aNumber).to.be.equal(2) done() @@ -284,7 +306,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () { it('with single field', function(done) { var self = this this.User.find(1).complete(function(err, user1) { - user1.increment('aNumber', 2).complete(function() { + user1.increment('aNumber', { by: 2 }).complete(function() { self.User.find(1).complete(function(err, user3) { expect(user3.aNumber).to.be.equal(2) done() @@ -313,7 +335,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () { user2.updateAttributes({ aNumber: user2.aNumber + 1 }).complete(function () { - user1.increment(['aNumber'], 2).complete(function() { + user1.increment(['aNumber'], { by: 2 }).complete(function() { self.User.find(1).complete(function(err, user5) { expect(user5.aNumber).to.be.equal(3) done() @@ -334,16 +356,16 @@ describe(Support.getTestDialectTeaser("DAO"), function () { }) }) - user1.increment(['aNumber'], 2).complete(_done) - user1.increment(['aNumber'], 2).complete(_done) - user1.increment(['aNumber'], 2).complete(_done) + user1.increment(['aNumber'], { by: 2 }).complete(_done) + user1.increment(['aNumber'], { by: 2 }).complete(_done) + user1.increment(['aNumber'], { by: 2 }).complete(_done) }) }) it('with key value pair', function(done) { var self = this this.User.find(1).complete(function(err, user1) { - user1.increment({ 'aNumber': 1, 'bNumber': 2}).success(function() { + user1.increment({ 'aNumber': 1, 'bNumber': 2 }).success(function() { self.User.find(1).complete(function (err, user3) { expect(user3.aNumber).to.be.equal(1) expect(user3.bNumber).to.be.equal(2) @@ -362,7 +384,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () { User.create({aNumber: 1}).success(function (user) { var oldDate = user.updatedAt setTimeout(function () { - user.increment('aNumber', 1).success(function() { + user.increment('aNumber', { by: 1 }).success(function() { User.find(1).success(function (user) { expect(user.updatedAt).to.be.afterTime(oldDate) done() @@ -379,10 +401,32 @@ describe(Support.getTestDialectTeaser("DAO"), function () { this.User.create({ id: 1, aNumber: 0, bNumber: 0 }).complete(done) }) + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { number: Support.Sequelize.INTEGER }) + + User.sync({ force: true }).success(function() { + User.create({ number: 3 }).success(function(user) { + sequelize.transaction(function(t) { + user.decrement('number', { by: 2, transaction: t }).success(function() { + User.all().success(function(users1) { + User.all({ transaction: t }).success(function(users2) { + expect(users1[0].number).to.equal(3) + expect(users2[0].number).to.equal(1) + t.rollback().success(function() { done() }) + }) + }) + }) + }) + }) + }) + }) + }) + it('with array', function(done) { var self = this this.User.find(1).complete(function(err, user1) { - user1.decrement(['aNumber'], 2).complete(function() { + user1.decrement(['aNumber'], { by: 2 }).complete(function() { self.User.find(1).complete(function(err, user3) { expect(user3.aNumber).to.be.equal(-2) done() @@ -394,7 +438,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () { it('with single field', function(done) { var self = this this.User.find(1).complete(function(err, user1) { - user1.decrement('aNumber', 2).complete(function() { + user1.decrement('aNumber', { by: 2 }).complete(function() { self.User.find(1).complete(function(err, user3) { expect(user3.aNumber).to.be.equal(-2) done() @@ -423,7 +467,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () { user2.updateAttributes({ aNumber: user2.aNumber + 1 }).complete(function () { - user1.decrement(['aNumber'], 2).complete(function() { + user1.decrement(['aNumber'], { by: 2 }).complete(function() { self.User.find(1).complete(function(err, user5) { expect(user5.aNumber).to.be.equal(-1) done() @@ -444,9 +488,9 @@ describe(Support.getTestDialectTeaser("DAO"), function () { }) }) - user1.decrement(['aNumber'], 2).complete(_done) - user1.decrement(['aNumber'], 2).complete(_done) - user1.decrement(['aNumber'], 2).complete(_done) + user1.decrement(['aNumber'], { by: 2 }).complete(_done) + user1.decrement(['aNumber'], { by: 2 }).complete(_done) + user1.decrement(['aNumber'], { by: 2 }).complete(_done) }) }) @@ -472,7 +516,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () { User.create({aNumber: 1}).success(function (user) { var oldDate = user.updatedAt setTimeout(function () { - user.decrement('aNumber', 1).success(function() { + user.decrement('aNumber', { by: 1 }).success(function() { User.find(1).success(function (user) { expect(user.updatedAt).to.be.afterTime(oldDate) done() @@ -485,6 +529,28 @@ describe(Support.getTestDialectTeaser("DAO"), function () { }) describe('reload', function () { + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Support.Sequelize.STRING }) + + User.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + sequelize.transaction(function(t) { + User.update({ username: 'bar' }, {}, { transaction: t }).success(function() { + user.reload().success(function(user) { + expect(user.username).to.equal('foo') + user.reload({ transaction: t }).success(function(user) { + expect(user.username).to.equal('bar') + t.rollback().success(function() { done() }) + }) + }) + }) + }) + }) + }) + }) + }) + it("should return a reference to the same DAO instead of creating a new one", function(done) { this.User.create({ username: 'John Doe' }).complete(function(err, originalUser) { originalUser.updateAttributes({ username: 'Doe John' }).complete(function() { @@ -651,6 +717,26 @@ describe(Support.getTestDialectTeaser("DAO"), function () { }) describe('save', function() { + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Support.Sequelize.STRING }) + + User.sync({ force: true }).success(function() { + sequelize.transaction(function(t) { + User.build({ username: 'foo' }).save({ transaction: t }).success(function() { + User.count().success(function(count1) { + User.count({ transaction: t }).success(function(count2) { + expect(count1).to.equal(0) + expect(count2).to.equal(1) + t.rollback().success(function(){ done() }) + }) + }) + }) + }) + }) + }) + }) + it('only updates fields in passed array', function(done) { var self = this , userId = null @@ -1251,6 +1337,28 @@ describe(Support.getTestDialectTeaser("DAO"), function () { }) describe('updateAttributes', function() { + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Support.Sequelize.STRING }) + + User.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + sequelize.transaction(function(t) { + user.updateAttributes({ username: 'bar' }, { transaction: t }).success(function() { + User.all().success(function(users1) { + User.all({ transaction: t }).success(function(users2) { + expect(users1[0].username).to.equal('foo') + expect(users2[0].username).to.equal('bar') + t.rollback().success(function(){ done() }) + }) + }) + }) + }) + }) + }) + }) + }) + it("updates attributes in the database", function(done) { this.User.create({ username: 'user' }).success(function(user) { expect(user.username).to.equal('user') @@ -1361,4 +1469,69 @@ describe(Support.getTestDialectTeaser("DAO"), function () { }) }) }) + + describe('destroy', function() { + it('supports transactions', function(done) { + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + var User = sequelize.define('User', { username: Support.Sequelize.STRING }) + + User.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + sequelize.transaction(function(t) { + user.destroy({ transaction: t }).success(function() { + User.count().success(function(count1) { + User.count({ transaction: t }).success(function(count2) { + expect(count1).to.equal(1) + expect(count2).to.equal(0) + t.rollback().success(function() { done() }) + }) + }) + }) + }) + }) + }) + }) + }) + + it('deletes a record from the database if dao is not paranoid', function(done) { + var UserDestroy = this.sequelize.define('UserDestroy', { + name: Support.Sequelize.STRING, + bio: Support.Sequelize.TEXT + }) + + UserDestroy.sync({ force: true }).success(function() { + UserDestroy.create({name: 'hallo', bio: 'welt'}).success(function(u) { + UserDestroy.all().success(function(users) { + expect(users.length).to.equal(1) + u.destroy().success(function() { + UserDestroy.all().success(function(users) { + expect(users.length).to.equal(0) + done() + }) + }) + }) + }) + }) + }) + + it('allows sql logging of delete statements', function(done) { + var UserDelete = this.sequelize.define('UserDelete', { + name: Support.Sequelize.STRING, + bio: Support.Sequelize.TEXT + }) + + UserDelete.sync({ force: true }).success(function() { + UserDelete.create({name: 'hallo', bio: 'welt'}).success(function(u) { + UserDelete.all().success(function(users) { + expect(users.length).to.equal(1) + u.destroy().on('sql', function(sql) { + expect(sql).to.exist + expect(sql.toUpperCase().indexOf("DELETE")).to.be.above(-1) + done() + }) + }) + }) + }) + }) + }) }) diff --git a/test/hooks.test.js b/test/hooks.test.js index a3200bb5aa1c..ba312df726a8 100644 --- a/test/hooks.test.js +++ b/test/hooks.test.js @@ -101,7 +101,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { this.User.bulkCreate([ {username: 'Bob', mood: 'cold'}, {username: 'Tobi', mood: 'hot'} - ], [], {hooks: true}).success(function(bulkUsers) { + ], { fields: [], hooks: true }).success(function(bulkUsers) { expect(beforeBulkCreate).to.be.true expect(afterBulkCreate).to.be.true expect(bulkUsers).to.be.instanceof(Array) @@ -268,7 +268,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { this.User.bulkCreate([ {username: 'Bob', mood: 'cold'}, {username: 'Tobi', mood: 'hot'} - ], null, {hooks: true}).success(function(bulkUsers) { + ], { hooks: true }).success(function(bulkUsers) { expect(beforeBulkCreate).to.be.true expect(afterBulkCreate).to.be.true expect(bulkUsers).to.be.instanceof(Array) @@ -4320,7 +4320,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { fn() }) - this.User.bulkCreate([{aNumber: 5}, {aNumber: 7}, {aNumber: 3}], ['aNumber'], {hooks: true}).success(function(records) { + this.User.bulkCreate([{aNumber: 5}, {aNumber: 7}, {aNumber: 3}], { fields: ['aNumber'], hooks: true }).success(function(records) { records.forEach(function(record) { expect(record.username).to.equal('User' + record.id) expect(record.beforeHookTest).to.be.true @@ -4354,7 +4354,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { fn() }) - this.User.bulkCreate([{aNumber: 5}, {aNumber: 7}, {aNumber: 3}], ['aNumber'], {hooks: true}).error(function(err) { + this.User.bulkCreate([{aNumber: 5}, {aNumber: 7}, {aNumber: 3}], { fields: ['aNumber'], hooks: true }).error(function(err) { expect(err).to.equal('You shall not pass!') expect(beforeBulkCreate).to.be.true expect(afterBulkCreate).to.be.false @@ -5272,7 +5272,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { fn() }) - this.User.bulkCreate([{aNumber: 1}, {aNumber: 1}, {aNumber: 1}], ['aNumber']).success(function() { + this.User.bulkCreate([{aNumber: 1}, {aNumber: 1}, {aNumber: 1}], { fields: ['aNumber'] }).success(function() { self.User.update({aNumber: 10}, {aNumber: 1}, {hooks: true}).error(function(err) { expect(err).to.equal('You shall not pass!') expect(beforeBulk).to.be.true @@ -6059,7 +6059,7 @@ describe(Support.getTestDialectTeaser("Hooks"), function () { fn() }) - this.User.bulkCreate([{aNumber: 1}, {aNumber: 1}, {aNumber: 1}], ['aNumber']).success(function() { + this.User.bulkCreate([{aNumber: 1}, {aNumber: 1}, {aNumber: 1}], { fields: ['aNumber'] }).success(function() { self.User.destroy({aNumber: 1}, {hooks: true}).error(function(err) { expect(err).to.equal('You shall not pass!') expect(beforeBulk).to.be.true diff --git a/test/promise.test.js b/test/promise.test.js index 467c34cbf192..6353fa283ae6 100644 --- a/test/promise.test.js +++ b/test/promise.test.js @@ -47,7 +47,7 @@ describe(Support.getTestDialectTeaser("Promise"), function () { .find(1) .then(function(user) { expect(user.id).to.equal(1) - return user.increment(['aNumber'], 2) + return user.increment(['aNumber'], { by: 2 }) }) .then(function(user) { // The following assertion would rock hard, but it's not implemented :( @@ -72,7 +72,7 @@ describe(Support.getTestDialectTeaser("Promise"), function () { .then(function (user2) { return user2 .updateAttributes({ aNumber: user2.aNumber + 1 }) - .then(function() { return user1.increment(['aNumber'], 2) }) + .then(function() { return user1.increment(['aNumber'], { by: 2 }) }) .then(function() { return self.User.find(1) }) .then(function(user5) { expect(user5.aNumber).to.equal(3) @@ -112,7 +112,7 @@ describe(Support.getTestDialectTeaser("Promise"), function () { this.User .find(1) .then(function(user1) { - return user1.decrement(['aNumber'], 2) + return user1.decrement(['aNumber'], { by: 2 }) }) .then(function(user2) { return self.User.find(1) @@ -129,7 +129,7 @@ describe(Support.getTestDialectTeaser("Promise"), function () { this.User .find(1) .then(function(user1) { - return user1.decrement(['aNumber'], 2) + return user1.decrement(['aNumber'], { by: 2 }) }) .then(function(user3) { return self.User.find(1) @@ -155,9 +155,9 @@ describe(Support.getTestDialectTeaser("Promise"), function () { }) }) - user1.decrement(['aNumber'], 2).done(_done) - user1.decrement(['aNumber'], 2).done(_done) - user1.decrement(['aNumber'], 2).done(_done) + user1.decrement(['aNumber'], { by: 2 }).done(_done) + user1.decrement(['aNumber'], { by: 2 }).done(_done) + user1.decrement(['aNumber'], { by: 2 }).done(_done) }) }) }) diff --git a/test/sequelize.test.js b/test/sequelize.test.js index 829cc9797234..f5fd16e4d620 100644 --- a/test/sequelize.test.js +++ b/test/sequelize.test.js @@ -1,14 +1,16 @@ -var chai = require('chai') - , expect = chai.expect - , assert = chai.assert - , Support = require(__dirname + '/support') - , DataTypes = require(__dirname + "/../lib/data-types") - , dialect = Support.getTestDialect() - , _ = require('lodash') - , Sequelize = require(__dirname + '/../index') - , config = require(__dirname + "/config/config") - , moment = require('moment') - , sinon = require('sinon') +var chai = require('chai') + , expect = chai.expect + , assert = chai.assert + , Support = require(__dirname + '/support') + , DataTypes = require(__dirname + "/../lib/data-types") + , dialect = Support.getTestDialect() + , _ = require('lodash') + , Sequelize = require(__dirname + '/../index') + , config = require(__dirname + "/config/config") + , moment = require('moment') + , Transaction = require(__dirname + '/../lib/transaction') + , path = require('path') + , sinon = require('sinon') chai.Assertion.includeStack = true @@ -40,6 +42,38 @@ describe(Support.getTestDialectTeaser("Sequelize"), function () { }) }) + if (dialect !== 'sqlite') { + describe('authenticate', function() { + describe('with valid credentials', function() { + it('triggers the success event', function(done) { + this.sequelize.authenticate().success(done) + }) + }) + + describe('with invalid credentials', function() { + beforeEach(function() { + this.sequelizeWithInvalidCredentials = new Sequelize("omg", "wtf", "lol", this.sequelize.options) + }) + + it('triggers the error event', function(done) { + this + .sequelizeWithInvalidCredentials + .authenticate() + .complete(function(err, result) { + expect(err).to.not.be.null + done() + }) + }) + }) + }) + } + + describe('getDialect', function() { + it('returns the defined dialect', function() { + expect(this.sequelize.getDialect()).to.equal(dialect) + }) + }) + describe('isDefined', function() { it("returns false if the dao wasn't defined before", function() { expect(this.sequelize.isDefined('Project')).to.be.false @@ -362,28 +396,26 @@ describe(Support.getTestDialectTeaser("Sequelize"), function () { }) }) - it("fails with incorrect database credentials", function(done) { - // sqlite doesn't have a concept of database credentials - if (dialect === "sqlite") { - expect(true).to.be.true - return done() - } + if (dialect !== "sqlite") { + it("fails with incorrect database credentials", function(done) { + this.sequelizeWithInvalidCredentials = new Sequelize("omg", "bar", null, this.sequelize.options) - var sequelize2 = Support.getSequelizeInstance('foo', 'bar', null, { logging: false }) - , User2 = sequelize2.define('User', { name: DataTypes.STRING, bio: DataTypes.TEXT }) - - User2.sync().error(function(err) { - if (dialect === "postgres" || dialect === "postgres-native") { - assert([ - 'role "bar" does not exist', - 'password authentication failed for user "bar"' - ].indexOf(err.message) !== -1) - } else { - expect(err.message.toString()).to.match(/.*Access\ denied.*/) - } - done() + var User2 = this.sequelizeWithInvalidCredentials.define('User', { name: DataTypes.STRING, bio: DataTypes.TEXT }) + + User2.sync().error(function(err) { + if (dialect === "postgres" || dialect === "postgres-native") { + assert([ + 'role "bar" does not exist', + 'FATAL: role "bar" does not exist', + 'password authentication failed for user "bar"' + ].indexOf(err.message.trim()) !== -1) + } else { + expect(err.message.toString()).to.match(/.*Access\ denied.*/) + } + done() + }) }) - }) + } describe("doesn't emit logging when explicitly saying not to", function() { afterEach(function(done) { @@ -520,5 +552,133 @@ describe(Support.getTestDialectTeaser("Sequelize"), function () { }) }) + + describe('transaction', function() { + beforeEach(function(done) { + var self = this + + Support.prepareTransactionTest(this.sequelize, function(sequelize) { + self.sequelizeWithTransaction = sequelize + done() + }) + }) + + it('is a transaction method available', function() { + expect(Support.Sequelize).to.respondTo('transaction') + }) + + it('passes a transaction object to the callback', function(done) { + this.sequelizeWithTransaction.transaction(function(t) { + expect(t).to.be.instanceOf(Transaction) + done() + }) + }) + + it('returns a transaction object', function() { + expect(this.sequelizeWithTransaction.transaction(function(){})).to.be.instanceOf(Transaction) + }) + + it('allows me to define a callback on the result', function(done) { + this + .sequelizeWithTransaction + .transaction(function(t) { t.commit() }) + .done(done) + }) + + it('allows me to define a callback on the transaction object', function(done) { + this.sequelizeWithTransaction.transaction(function(t) { + t.done(done) + t.commit() + }) + }) + + if (dialect === 'sqlite') { + it("correctly scopes transaction from other connections", function(done) { + var TransactionTest = this.sequelizeWithTransaction.define('TransactionTest', { name: DataTypes.STRING }, { timestamps: false }) + , self = this + + var count = function(transaction, callback) { + var sql = self.sequelizeWithTransaction.getQueryInterface().QueryGenerator.selectQuery('TransactionTests', { attributes: [['count(*)', 'cnt']] }) + + self + .sequelizeWithTransaction + .query(sql, null, { plain: true, raw: true, transaction: transaction }) + .success(function(result) { callback(result.cnt) }) + } + + TransactionTest.sync({ force: true }).success(function() { + self.sequelizeWithTransaction.transaction(function(t1) { + self.sequelizeWithTransaction.query('INSERT INTO ' + qq('TransactionTests') + ' (' + qq('name') + ') VALUES (\'foo\');', null, { plain: true, raw: true, transaction: t1 }).success(function() { + count(null, function(cnt) { + expect(cnt).to.equal(0) + + count(t1, function(cnt) { + expect(cnt).to.equal(1) + + t1.commit().success(function() { + count(null, function(cnt) { + expect(cnt).to.equal(1) + done() + }) + }) + }) + }) + }) + }) + }) + }) + } else { + it("correctly handles multiple transactions", function(done) { + var TransactionTest = this.sequelizeWithTransaction.define('TransactionTest', { name: DataTypes.STRING }, { timestamps: false }) + , self = this + + var count = function(transaction, callback) { + var sql = self.sequelizeWithTransaction.getQueryInterface().QueryGenerator.selectQuery('TransactionTests', { attributes: [['count(*)', 'cnt']] }) + + self + .sequelizeWithTransaction + .query(sql, null, { plain: true, raw: true, transaction: transaction }) + .success(function(result) { callback(parseInt(result.cnt, 10)) }) + } + + TransactionTest.sync({ force: true }).success(function() { + self.sequelizeWithTransaction.transaction(function(t1) { + self.sequelizeWithTransaction.query('INSERT INTO ' + qq('TransactionTests') + ' (' + qq('name') + ') VALUES (\'foo\');', null, { plain: true, raw: true, transaction: t1 }).success(function() { + self.sequelizeWithTransaction.transaction(function(t2) { + self.sequelizeWithTransaction.query('INSERT INTO ' + qq('TransactionTests') + ' (' + qq('name') + ') VALUES (\'bar\');', null, { plain: true, raw: true, transaction: t2 }).success(function() { + count(null, function(cnt) { + expect(cnt).to.equal(0) + + count(t1, function(cnt) { + expect(cnt).to.equal(1) + + count(t2, function(cnt) { + expect(cnt).to.equal(1) + + t2.rollback().success(function() { + count(t2, function(cnt) { + expect(cnt).to.equal(0) + + t1.commit().success(function() { + count(null, function(cnt) { + expect(cnt).to.equal(1) + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + }).done(function() { + done() + }) + }) + }) + } + + }) }) }) diff --git a/test/sqlite/test.sqlite b/test/sqlite/test.sqlite deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/test/support.js b/test/support.js index 64bfa6ac01af..7ff6713aade3 100644 --- a/test/support.js +++ b/test/support.js @@ -1,4 +1,5 @@ var fs = require('fs') + , path = require('path') , Sequelize = require(__dirname + "/../index") , DataTypes = require(__dirname + "/../lib/data-types") , Config = require(__dirname + "/config/config") @@ -24,6 +25,19 @@ var Support = { }) }, + prepareTransactionTest: function(sequelize, callback) { + var dialect = Support.getTestDialect() + + if (dialect === 'sqlite') { + var options = Sequelize.Utils._.extend({}, sequelize.options, { storage: path.join(__dirname, 'tmp', 'db.sqlite') }) + , _sequelize = new Sequelize(sequelize.config.datase, null, null, options) + + _sequelize.sync({ force: true }).success(function() { callback(_sequelize) }) + } else { + callback(sequelize) + } + }, + createSequelizeInstance: function(options) { options = options || {} options.dialect = options.dialect || 'mysql' @@ -49,6 +63,10 @@ var Support = { sequelizeOptions.define = options.define } + if (!!config.storage) { + sequelizeOptions.storage = config.storage + } + if (process.env.DIALECT === 'postgres-native') { sequelizeOptions.native = true } diff --git a/test/transaction.test.js b/test/transaction.test.js new file mode 100644 index 000000000000..72333cbb7969 --- /dev/null +++ b/test/transaction.test.js @@ -0,0 +1,41 @@ +var chai = require('chai') + , expect = chai.expect + , Support = require(__dirname + '/support') + , Transaction = require(__dirname + '/../lib/transaction') + +describe(Support.getTestDialectTeaser("Transaction"), function () { + describe('constructor', function() { + it('stores options', function() { + var transaction = new Transaction(this.sequelize) + expect(transaction.options).to.be.an.instanceOf(Object) + }) + + it('generates an identifier', function() { + var transaction = new Transaction(this.sequelize) + expect(transaction.id).to.exist + }) + }) + + describe('commit', function() { + it('is a commit message available', function() { + expect(Transaction).to.respondTo('commit') + }) + }) + + describe('rollback', function() { + it('is a rollback message available', function() { + expect(Transaction).to.respondTo('rollback') + }) + }) + + describe('done', function() { + it('gets called when the transaction gets commited', function(done) { + var transaction = new Transaction(this.sequelize) + + transaction.done(done) + transaction.prepareEnvironment(function() { + transaction.commit() + }) + }) + }) +}) diff --git a/test/utils.test.js b/test/utils.test.js index b6888378eba5..6c527f60aa81 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -1,8 +1,10 @@ var chai = require('chai') + , spies = require('chai-spies') , expect = chai.expect , Utils = require(__dirname + '/../lib/utils') , Support = require(__dirname + '/support') +chai.use(spies) chai.Assertion.includeStack = true describe(Support.getTestDialectTeaser("Utils"), function() { @@ -158,4 +160,59 @@ describe(Support.getTestDialectTeaser("Utils"), function() { done() }) }) + + describe('validateParameter', function() { + describe('method signature', function() { + it('throws an error if the value is not defined', function() { + expect(function() { + Utils.validateParameter() + }).to.throw('No value has been passed.') + }) + + it('does not throw an error if the value is not defined and the parameter is optional', function() { + expect(function() { + Utils.validateParameter(undefined, Object, { optional: true }) + }).to.not.throw() + }) + + it('throws an error if the expectation is not defined', function() { + expect(function() { + Utils.validateParameter(1) + }).to.throw('No expectation has been passed.') + }) + }) + + describe('expectation', function() { + it('uses the typeof method if the expectation is a string', function() { + expect(Utils.validateParameter(1, 'number')).to.be.true + }) + + it('uses the instanceof method if the expectation is a class', function() { + expect(Utils.validateParameter(new Number(1), Number)).to.be.true + }) + }) + + describe('failing expectations', function() { + it('throws an error if the expectation does not match', function() { + expect(function() { + Utils.validateParameter(1, String) + }).to.throw(/The parameter.*is no.*/) + }) + + it('does not throw an error if throwError is false', function() { + expect(Utils.validateParameter(1, String, { throwError: false })).to.be.false + }) + }) + + describe('deprecation warning', function() { + it('uses the passed function', function() { + var spy = chai.spy(function(s){}) + Utils.validateParameter([], Object, { + deprecated: Array, + onDeprecated: spy + }) + expect(spy).to.have.been.called() + }) + }) + }) })