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

Commit 3d353789 by Sascha Depold

Merge pull request #807 from durango/milestones/2.0.0

Milestones/2.0.0 (big merge)
2 parents ab9a8364 e15b14d2
......@@ -28,6 +28,8 @@
- [BUG] Fixed SQL escaping with sqlite and unified escaping [#700](https://github.com/sequelize/sequelize/pull/700). thanks to PiPeep
- [BUG] Fixed Postgres' pools [ff57af63](https://github.com/sequelize/sequelize/commit/ff57af63c2eb395b4828a5984a22984acdc2a5e1)
- [BUG] Fixed BLOB/TEXT columns having a default value declared in MySQL [#793](https://github.com/sequelize/sequelize/pull/793). thanks to durango
- [BUG] You can now use .find() on any single integer primary key when throwing just a number as an argument [#796](https://github.com/sequelize/sequelize/pull/796). thanks to durango
- [BUG] Adding unique to a column for Postgres in the migrator should be fixed [#795](https://github.com/sequelize/sequelize/pull/795). thanks to durango
- [FEATURE] Validate a model before it gets saved. [#601](https://github.com/sequelize/sequelize/pull/601). thanks to durango
- [FEATURE] Schematics. [#564](https://github.com/sequelize/sequelize/pull/564). thanks to durango
- [FEATURE] Foreign key constraints. [#595](https://github.com/sequelize/sequelize/pull/595). thanks to optilude
......@@ -50,6 +52,8 @@
- [FEATURE] Added support for model instances being referenced [#761](https://github.com/sequelize/sequelize/pull/761) thanks to sdepold
- [FEATURE] Added support for specifying the path to load a module for a dialect. [#766](https://github.com/sequelize/sequelize/pull/766) thanks to sonnym.
- [FEATURE] Drop index if exists has been added to sqlite [#766](https://github.com/sequelize/sequelize/pull/776) thanks to coderbuzz
- [FEATURE] bulkCreate() now has a third argument which gives you the ability to validate each row before attempting to bulkInsert [#797](https://github.com/sequelize/sequelize/pull/797). thanks to durango
- [FEATURE] Added `isDirty` to model instances. [#798](https://github.com/sequelize/sequelize/pull/798). Thanks to mstorgaard
- [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
......
......@@ -250,14 +250,20 @@ module.exports = (function() {
}
var primaryKeys = this.primaryKeys
, keys = Object.keys(primaryKeys)
, keysLength = keys.length
// options is not a hash but an id
if (typeof options === 'number') {
options = { where: options }
var oldOption = options
options = { where: {} }
if (keysLength === 1) {
options.where[keys[0]] = oldOption
} else {
options.where.id = oldOption
}
} else if (Utils._.size(primaryKeys) && Utils.argsArePrimaryKeys(arguments, primaryKeys)) {
var where = {}
, self = this
, keys = Object.keys(primaryKeys)
Utils._.each(arguments, function(arg, i) {
var key = keys[i]
......@@ -288,7 +294,6 @@ module.exports = (function() {
this.options.whereCollection = options.where || null
} else if (typeof options === "string") {
var where = {}
, keys = Object.keys(primaryKeys)
if (this.primaryKeyCount === 1) {
where[primaryKeys[keys[0]]] = options;
......@@ -377,7 +382,7 @@ module.exports = (function() {
}
DAOFactory.prototype.build = function(values, options) {
options = options || { isNewRecord: true }
options = options || { isNewRecord: true, isDirty: true }
var self = this
, instance = new this.DAO(values, this.options, options.isNewRecord)
......@@ -385,6 +390,7 @@ module.exports = (function() {
instance.isNewRecord = options.isNewRecord
instance.daoFactoryName = this.name
instance.daoFactory = this
instance.isDirty = options.isDirty
return instance
}
......@@ -434,50 +440,80 @@ 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) {
DAOFactory.prototype.bulkCreate = function(records, fields, options) {
options = options || {}
options.validate = options.validate || false
fields = fields || []
var self = this
, daos = records.map(function(v) { return self.build(v) })
, updatedAtAttr = self.options.underscored ? 'updated_at' : 'updatedAt'
, createdAtAttr = self.options.underscored ? 'created_at' : 'createdAt'
, errors = []
, completed = 0
fields = fields || []
return new Utils.CustomEventEmitter(function(emitter) {
var daos = records.map(function(v) {
return self.build(v)
})
// we will re-create from DAOs, which may have set up default attributes
records = []
var found = false
var bulkCreateStep = function(i) {
daos[i].validate({skip: fields}).success(function(err) {
if (!!err) {
errors[errors.length] = {record: records[i], errors: err}
}
++completed
if (completed === records.length) {
bulkCreateFinal()
} else {
bulkCreateStep(completed)
}
})
}
daos.forEach(function(dao) {
var values = fields.length > 0 ? {} : dao.dataValues
bulkCreateStep(0)
fields.forEach(function(field) {
values[field] = dao.dataValues[field]
})
var bulkCreateFinal = function() {
if (options.validate === true && errors.length > 0) {
return emitter.emit('error', errors)
}
if (self.options.timestamps) {
values[createdAtAttr] = Utils.now()
values[updatedAtAttr] = Utils.now()
}
// we will re-create from DAOs, which may have set up default attributes
records = []
records.push(values);
})
daos.forEach(function(dao) {
var values = fields.length > 0 ? {} : dao.dataValues
fields.forEach(function(field) {
values[field] = dao.dataValues[field]
})
// Validate enums
records.forEach(function(values) {
for (var attrName in self.rawAttributes) {
if (self.rawAttributes.hasOwnProperty(attrName)) {
var definition = self.rawAttributes[attrName]
, isEnum = (definition.type && (definition.type.toString() === DataTypes.ENUM.toString()))
, hasValue = (typeof values[attrName] !== 'undefined')
, valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1)
if (isEnum && hasValue && valueOutOfScope) {
throw new Error('Value "' + values[attrName] + '" for ENUM ' + attrName + ' is out of allowed scope. Allowed values: ' + definition.values.join(', '))
if (self.options.timestamps) {
values[createdAtAttr] = Utils.now()
values[updatedAtAttr] = Utils.now()
}
}
}
})
return self.QueryInterface.bulkInsert(self.tableName, records)
records.push(values)
})
// Validate enums
records.forEach(function(values) {
for (var attrName in self.rawAttributes) {
if (self.rawAttributes.hasOwnProperty(attrName)) {
var definition = self.rawAttributes[attrName]
, isEnum = (definition.type && (definition.type.toString() === DataTypes.ENUM.toString()))
, hasValue = (typeof values[attrName] !== 'undefined')
, valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1)
if (isEnum && hasValue && valueOutOfScope) {
throw new Error('Value "' + values[attrName] + '" for ENUM ' + attrName + ' is out of allowed scope. Allowed values: ' + definition.values.join(', '))
}
}
}
})
self.QueryInterface.bulkInsert(self.tableName, records).proxy(emitter)
}
}).run()
}
/**
......
var Validator = require("validator")
, Utils = require("./utils")
var DaoValidator = module.exports = function(model) {
var DaoValidator = module.exports = function(model, options) {
options = options || {}
options.skip = options.skip || []
this.model = model
this.chainer = new Utils.QueryChainer()
this.options = options
}
DaoValidator.prototype.validate = function() {
var self = this
return new Utils.CustomEventEmitter(function(emitter) {
validateAttributes.call(this)
validateModel.call(this)
validateAttributes.call(self)
validateModel.call(self)
this
self
.chainer
.run()
.success(function () {
......@@ -26,7 +32,7 @@ DaoValidator.prototype.validate = function() {
emitter.emit('success', errors)
})
}.bind(this)).run()
}).run()
}
// private
......@@ -52,17 +58,19 @@ var validateModel = function() {
}
var validateAttributes = function() {
var errors = {}
var self = this
, errors = {}
// for each field and value
Utils._.each(this.model.dataValues, function(value, field) {
var rawAttribute = this.model.rawAttributes[field]
var rawAttribute = self.model.rawAttributes[field]
, hasAllowedNull = ((rawAttribute === undefined || rawAttribute.allowNull === true) && ((value === null) || (value === undefined)))
, isSkipped = self.options.skip.length > 0 && self.options.skip.indexOf(field) === -1
if (this.model.validators.hasOwnProperty(field) && !hasAllowedNull) {
errors = Utils._.merge(errors, validateAttribute.call(this, value, field))
if (self.model.validators.hasOwnProperty(field) && !hasAllowedNull && !isSkipped) {
errors = Utils._.merge(errors, validateAttribute.call(self, value, field))
}
}.bind(this)) // for each field
})
return errors
}
......@@ -113,7 +121,7 @@ var prepareValidationOfAttribute = function(value, details, validatorType, optio
}
// extract the error msg
errorMessage = details.hasOwnProperty("msg") ? details.msg : false
errorMessage = details.hasOwnProperty("msg") ? details.msg : undefined
// check method exists
var validator = Validator.check(value, errorMessage)
......
......@@ -84,10 +84,15 @@ module.exports = (function() {
DAO.prototype.getDataValue = function(name) {
return this.dataValues && this.dataValues.hasOwnProperty(name) ? this.dataValues[name] : this[name]
}
DAO.prototype.get = DAO.prototype.getDataValue
DAO.prototype.setDataValue = function(name, value) {
if (Utils.hasChanged(this.dataValues[name], value)) {
this.isDirty = true
}
this.dataValues[name] = value
}
DAO.prototype.set = DAO.prototype.setDataValue
// if an array with field names is passed to save()
// only those fields will be updated
......@@ -103,7 +108,7 @@ module.exports = (function() {
fields.push(updatedAtAttr)
}
if (fields.indexOf(createdAtAttr) === -1) {
if (fields.indexOf(createdAtAttr) === -1 && this.isNewRecord === true) {
fields.push(createdAtAttr)
}
}
......@@ -120,10 +125,24 @@ module.exports = (function() {
for (var attrName in this.daoFactory.rawAttributes) {
if (this.daoFactory.rawAttributes.hasOwnProperty(attrName)) {
var definition = this.daoFactory.rawAttributes[attrName]
, isEnum = (definition.type && (definition.type.toString() === DataTypes.ENUM.toString()))
, isHstore = (!!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type)
, hasValue = (typeof values[attrName] !== 'undefined')
, valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1)
, isEnum = definition.type && (definition.type.toString() === DataTypes.ENUM.toString())
, isHstore = !!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type
, hasValue = values[attrName] !== undefined
, isMySQL = this.daoFactory.daoFactoryManager.sequelize.options.dialect === "mysql"
, ciCollation = !!this.daoFactory.options.collate && this.daoFactory.options.collate.match(/_ci$/i)
, valueOutOfScope
if (isEnum && isMySQL && ciCollation && hasValue) {
var scopeIndex = (definition.values || []).map(function(d) { return d.toLowerCase() }).indexOf(values[attrName].toLowerCase())
valueOutOfScope = scopeIndex === -1
// We'll return what the actual case will be, since a simple SELECT query would do the same...
if (!valueOutOfScope) {
values[attrName] = definition.values[scopeIndex]
}
} else {
valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1)
}
if (isEnum && hasValue && valueOutOfScope) {
throw new Error('Value "' + values[attrName] + '" for ENUM ' + attrName + ' is out of allowed scope. Allowed values: ' + definition.values.join(', '))
......@@ -146,6 +165,7 @@ module.exports = (function() {
if (!!errors) {
emitter.emit('error', errors)
} else if (this.isNewRecord) {
this.isDirty = false
this
.QueryInterface
.insert(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory), values)
......@@ -157,6 +177,7 @@ module.exports = (function() {
identifier = this.__options.whereCollection;
}
this.isDirty = false
var tableName = this.QueryInterface.QueryGenerator.addSchema(this.__factory)
, query = this.QueryInterface.update(this, tableName, values, identifier)
......@@ -193,6 +214,7 @@ module.exports = (function() {
this[valueName] = obj.values[valueName]
}
}
this.isDirty = false
emitter.emit('success', this)
}.bind(this))
}.bind(this)).run()
......@@ -203,8 +225,8 @@ module.exports = (function() {
*
* @return null if and only if validation successful; otherwise an object containing { field name : [error msgs] } entries.
*/
DAO.prototype.validate = function() {
return new DaoValidator(this).validate()
DAO.prototype.validate = function(options) {
return new DaoValidator(this, options).validate()
}
DAO.prototype.updateAttributes = function(updates, fields) {
......@@ -216,11 +238,15 @@ module.exports = (function() {
var self = this
var readOnlyAttributes = Object.keys(this.__factory.primaryKeys)
readOnlyAttributes.push('id')
readOnlyAttributes.push('createdAt')
readOnlyAttributes.push('updatedAt')
readOnlyAttributes.push('deletedAt')
if (this.isNewRecord !== true) {
readOnlyAttributes.push(this.daoFactory.options.underscored === true ? 'created_at' : 'createdAt')
}
// readOnlyAttributes.push(this.daoFactory.options.underscored === true ? 'updated_at' : 'updatedAt')
readOnlyAttributes.push(this.daoFactory.options.underscored === true ? 'deleted_at' : 'deletedAt')
var isDirty = this.isDirty
Utils._.each(updates, function(value, attr) {
var updateAllowed = (
......@@ -228,8 +254,23 @@ module.exports = (function() {
(readOnlyAttributes.indexOf(Utils._.underscored(attr)) == -1) &&
(self.attributes.indexOf(attr) > -1)
)
updateAllowed && (self[attr] = value)
if (updateAllowed) {
if (Utils.hasChanged(self[attr], value)) {
isDirty = true
}
self[attr] = value
}
})
// since we're updating the record, we should be updating the updatedAt column..
if (this.daoFactory.options.timestamps === true) {
isDirty = true
self[this.daoFactory.options.underscored === true ? 'updated_at' : 'updatedAt'] = new Date()
}
this.isDirty = isDirty
}
DAO.prototype.destroy = function() {
......@@ -328,7 +369,19 @@ module.exports = (function() {
// (the same is true for __defineSetter and 'prototype' getters)
if (has !== true) {
this.__defineGetter__(attribute, has.get || function() { return this.dataValues[attribute]; });
this.__defineSetter__(attribute, has.set || function(v) { this.dataValues[attribute] = v; });
this.__defineSetter__(attribute, has.set || function(v) {
if (Utils.hasChanged(this.dataValues[attribute], v)) {
//Only dirty the object if the change is not due to id, touchedAt, createdAt or updatedAt being initiated
var updatedAtAttr = this.__options.underscored ? 'updated_at' : 'updatedAt'
, createdAtAttr = this.__options.underscored ? 'created_at' : 'createdAt'
, touchedAtAttr = this.__options.underscored ? 'touched_at' : 'touchedAt'
if (this.dataValues[attribute] || (attribute != 'id' && attribute != touchedAtAttr && attribute != createdAtAttr && attribute != updatedAtAttr)) {
this.isDirty = true
}
}
this.dataValues[attribute] = v
});
}
this[attribute] = value;
......
......@@ -262,7 +262,7 @@ module.exports = (function() {
result = transformRowsWithEagerLoadingIntoDaos.call(this, results)
} else {
result = results.map(function(result) {
return this.callee.build(result, { isNewRecord: false })
return this.callee.build(result, { isNewRecord: false, isDirty: false })
}.bind(this))
}
......@@ -287,7 +287,7 @@ module.exports = (function() {
var transformRowWithEagerLoadingIntoDao = function(result, dao) {
// let's build the actual dao instance first...
dao = dao || this.callee.build(result[this.callee.tableName], { isNewRecord: false })
dao = dao || this.callee.build(result[this.callee.tableName], { isNewRecord: false, isDirty: false })
// ... and afterwards the prefetched associations
for (var tableName in result) {
......@@ -323,7 +323,7 @@ module.exports = (function() {
accessor = accessor.slice(0,1).toLowerCase() + accessor.slice(1)
associationData.forEach(function(data) {
var daoInstance = associatedDaoFactory.build(data, { isNewRecord: false })
var daoInstance = associatedDaoFactory.build(data, { isNewRecord: false, isDirty: false })
, isEmpty = !Utils.firstValueOfHash(daoInstance.identifiers)
if (['BelongsTo', 'HasOne'].indexOf(association.associationType) > -1) {
......
......@@ -46,11 +46,15 @@ module.exports = (function() {
ConnectorManager.prototype.query = function(sql, callee, options) {
var self = this
// we really want pendingQueries to increment as fast as possible...
self.pendingQueries++
return new Utils.CustomEventEmitter(function(emitter) {
self.connect()
.on('error', function(err) {
// zero-out the previous increment
self.pendingQueries--
self.endQuery.call(self)
emitter.emit('error', err)
})
.on('success', function(done) {
......@@ -58,14 +62,22 @@ module.exports = (function() {
done = done || null
query.run(sql, done)
.success(function(results) { emitter.emit('success', results); self.endQuery.call(self) })
.error(function(err) { emitter.emit('error', err); self.endQuery.call(self) })
.success(function(results) {
self.pendingQueries--
emitter.emit('success', results)
self.endQuery.call(self)
})
.error(function(err) {
self.pendingQueries--
emitter.emit('error', err)
self.endQuery.call(self)
})
.on('sql', function(sql) { emitter.emit('sql', sql) })
})
}).run().complete(function() { self.pendingQueries-- })
}).run()
}
ConnectorManager.prototype.connect = function(callback) {
ConnectorManager.prototype.connect = function() {
var self = this
var emitter = new (require('events').EventEmitter)()
......@@ -119,9 +131,13 @@ module.exports = (function() {
this.poolIdentifier = this.pg.pools.getOrCreate(this.sequelize.config)
this.poolIdentifier.connect(connectCallback)
} else {
//create one-off client
this.client = new this.pg.Client(uri)
this.client.connect(connectCallback)
if (this.client !== null) {
connectCallback(null, this.client)
} else {
//create one-off client
this.client = new this.pg.Client(uri)
this.client.connect(connectCallback)
}
}
return emitter
......
......@@ -189,6 +189,15 @@ module.exports = (function() {
definition = definition.replace(/^ENUM\(.+\)/, this.quoteIdentifier("enum_" + tableName + "_" + attributeName))
}
if (definition.match(/UNIQUE;*$/)) {
definition = definition.replace(/UNIQUE;*$/, '')
attrSql += Utils._.template(query.replace('ALTER COLUMN', ''))({
tableName: this.quoteIdentifiers(tableName),
query: 'ADD CONSTRAINT ' + this.quoteIdentifier(attributeName + '_unique_idx') + ' UNIQUE (' + this.quoteIdentifier(attributeName) + ')'
})
}
attrSql += Utils._.template(query)({
tableName: this.quoteIdentifiers(tableName),
query: this.quoteIdentifier(attributeName) + ' TYPE ' + definition
......
......@@ -252,6 +252,23 @@ var Utils = module.exports = {
isHash: function(obj) {
return Utils._.isObject(obj) && !Array.isArray(obj);
},
hasChanged: function(attrValue, value) {
//If attribute value is Date, check value as a date
if (Utils._.isDate(attrValue) && !Utils._.isDate(value)) {
value = new Date(value)
}
if (Utils._.isDate(attrValue)) {
return attrValue.valueOf() !== value.valueOf()
}
//If both of them are empty, don't set as changed
if ((attrValue === undefined || attrValue === null || attrValue === '') && (value === undefined || value === null || value === '')) {
return false
}
return attrValue !== value
},
argsArePrimaryKeys: function(args, primaryKeys) {
var result = (args.length == Object.keys(primaryKeys).length)
if (result) {
......
module.exports = {
up: function(migration, DataTypes, done) {
migration.addColumn('User', 'uniqueName', { type: DataTypes.STRING }).complete(function() {
migration.changeColumn('User', 'uniqueName', { type: DataTypes.STRING, allowNull: false, unique: true }).complete(done)
})
},
down: function(migration, DataTypes, done) {
migration.removeColumn('User', 'uniqueName').complete(done)
}
}
......@@ -715,6 +715,68 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
it('emits an error when validate is set to true', function(done) {
var Tasks = this.sequelize.define('Task', {
name: {
type: Sequelize.STRING,
validate: {
notNull: { args: true, msg: 'name cannot be null' }
}
},
code: {
type: Sequelize.STRING,
validate: {
len: [3, 10]
}
}
})
Tasks.sync({ force: true }).success(function() {
Tasks.bulkCreate([
{name: 'foo', code: '123'},
{code: '1234'},
{name: 'bar', code: '1'}
], null, {validate: true}).error(function(errors) {
expect(errors).to.not.be.null
expect(errors).to.be.instanceof(Array)
expect(errors).to.have.length(2)
expect(errors[0].record.code).to.equal('1234')
expect(errors[0].errors.name[0]).to.equal('name cannot be null')
expect(errors[1].record.name).to.equal('bar')
expect(errors[1].record.code).to.equal('1')
expect(errors[1].errors.code[0]).to.match(/String is not in range/)
done()
})
})
})
it("doesn't emit an error when validate is set to true but our selectedValues are fine", function(done) {
var Tasks = this.sequelize.define('Task', {
name: {
type: Sequelize.STRING,
validate: {
notNull: { args: true, msg: 'name cannot be null' }
}
},
code: {
type: Sequelize.STRING,
validate: {
len: [3, 10]
}
}
})
Tasks.sync({ force: true }).success(function() {
Tasks.bulkCreate([
{name: 'foo', code: '123'},
{code: '1234'}
], ['code'], {validate: true}).success(function() {
// we passed!
done()
})
})
})
describe('enums', function() {
it('correctly restores enum values', function(done) {
var self = this
......@@ -736,6 +798,24 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
describe('update', function() {
it('updates the attributes that we select only without updating createdAt', function(done) {
var User = this.sequelize.define('User1', {
username: Sequelize.STRING,
secretValue: Sequelize.STRING
}, {
paranoid:true
})
User.sync({ force: true }).success(function() {
User.create({username: 'Peter', secretValue: '42'}).success(function(user) {
user.updateAttributes({ secretValue: '43' }, ['secretValue']).on('sql', function(sql) {
expect(sql).to.match(/UPDATE\s+[`"]+User1s[`"]+\s+SET\s+[`"]+secretValue[`"]='43',[`"]+updatedAt[`"]+='[^`",]+'\s+WHERE [`"]+id[`"]+=1/)
done()
})
})
})
})
it('allows sql logging of updated statements', function(done) {
var User = this.sequelize.define('User', {
name: Sequelize.STRING,
......@@ -799,8 +879,8 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
expect(users[1].username).to.equal("Bill")
expect(users[2].username).to.equal("Bob")
expect(parseInt(+users[0].updatedAt/5000, 10)).to.equal(parseInt(+new Date()/5000, 10))
expect(parseInt(+users[1].updatedAt/5000, 10)).to.equal(parseInt(+new Date()/5000, 10))
expect(parseInt(+users[0].updatedAt/5000, 10)).to.be.closeTo(parseInt(+new Date()/5000, 10), 1)
expect(parseInt(+users[1].updatedAt/5000, 10)).to.be.closeTo(parseInt(+new Date()/5000, 10), 1)
done()
})
......@@ -1330,17 +1410,34 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
{where: {id: 0}},
{where: {id: '0'}}
]
, done = _.after(2 * permutations.length, _done);
, done = _.after(2 * permutations.length, _done)
this.User.bulkCreate([{username: 'jack'}, {username: 'jack'}]).success(function() {
permutations.forEach(function(perm) {
self.User.find(perm).done(function(err, user) {
expect(err).to.be.null;
expect(user).to.be.null;
done();
expect(err).to.be.null
expect(user).to.be.null
done()
}).on('sql', function(s) {
expect(s.indexOf(0)).not.to.equal(-1);
done();
expect(s.indexOf(0)).not.to.equal(-1)
done()
})
})
})
})
it('should allow us to find IDs using capital letters', function(done) {
var User = this.sequelize.define('User' + config.rand(), {
ID: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
Login: { type: Sequelize.STRING }
})
User.sync({ force: true }).success(function() {
User.create({Login: 'foo'}).success(function() {
User.find(1).success(function(user) {
expect(user).to.exist
expect(user.ID).to.equal(1)
done()
})
})
})
......@@ -2053,6 +2150,23 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
it('should allow us to find IDs using capital letters', function(done) {
var User = this.sequelize.define('User' + config.rand(), {
ID: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
Login: { type: Sequelize.STRING }
})
User.sync({ force: true }).success(function() {
User.create({Login: 'foo'}).success(function() {
User.findAll({ID: 1}).success(function(user) {
expect(user).to.be.instanceof(Array)
expect(user).to.have.length(1)
done()
})
})
})
})
})
})
......
......@@ -19,6 +19,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
touchedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
aNumber: { type: DataTypes.INTEGER },
bNumber: { type: DataTypes.INTEGER },
aDate: { type: DataTypes.DATE },
validateTest: {
type: DataTypes.INTEGER,
......@@ -110,6 +111,154 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
})
})
describe('isDirty', function() {
it('returns true for non-saved objects', function(done) {
var user = this.User.build({ username: 'user' })
expect(user.id).to.be.null
expect(user.isDirty).to.be.true
done()
})
it("returns false for saved objects", function(done) {
this.User.build({ username: 'user' }).save().success(function(user) {
expect(user.isDirty).to.be.false
done()
})
})
it("returns true for changed attribute", function(done) {
this.User.create({ username: 'user' }).success(function(user) {
user.username = 'new'
expect(user.isDirty).to.be.true
done()
})
})
it("returns false for non-changed attribute", function(done) {
this.User.create({ username: 'user' }).success(function(user) {
user.username = 'user'
expect(user.isDirty).to.be.false
done()
})
})
it("returns false for non-changed date attribute", function(done) {
this.User.create({ aDate: new Date(2013, 6, 31, 14, 25, 21) }).success(function(user) {
user.aDate = '2013-07-31 14:25:21'
expect(user.isDirty).to.be.false
done()
})
})
it("returns false for two empty attributes", function(done) {
this.User.create({ username: null }).success(function(user) {
user.username = ''
expect(user.isDirty).to.be.false
done()
})
})
it("returns true for bulk changed attribute", function(done) {
this.User.create({ username: 'user' }).success(function(user) {
user.setAttributes({
username: 'new',
aNumber: 1
})
expect(user.isDirty).to.be.true
done()
})
})
it("returns true for bulk non-changed attribute + model with timestamps", function(done) {
this.User.create({ username: 'user' }).success(function(user) {
user.setAttributes({
username: 'user'
})
expect(user.isDirty).to.be.rue
done()
})
})
it("returns false for bulk non-changed attribute + model without timestamps", function(done) {
var User = this.sequelize.define('User' + parseInt(Math.random() * 10000000), {
username: DataTypes.STRING
}, {
timestamps: false
})
User
.sync({ force: true })
.then(function() {
return User.create({ username: "user" })
})
.then(function(user) {
return user.setAttributes({ username: "user" })
expect(user.isDirty).to.be.false
})
.then(function() {
done()
})
})
it("returns true for changed and bulk non-changed attribute", function(done) {
this.User.create({ username: 'user' }).success(function(user) {
user.aNumber = 23
user.setAttributes({
username: 'user'
})
expect(user.isDirty).to.be.true
done()
})
})
it("returns true for changed attribute and false for saved object", function(done) {
this.User.create({ username: 'user' }).success(function(user) {
user.username = 'new'
expect(user.isDirty).to.be.true
user.save().success(function() {
expect(user.isDirty).to.be.false
done()
})
})
})
it("returns false for created objects", function(done) {
this.User.create({ username: 'user' }).success(function(user) {
expect(user.isDirty).to.be.false
done()
})
})
it("returns false for objects found by find method", function(done) {
var self = this
this.User.create({ username: 'user' }).success(function(user) {
self.User.find(user.id).success(function(user) {
expect(user.isDirty).to.be.false
done()
})
})
})
it("returns false for objects found by findAll method", function(done) {
var self = this
, users = []
for (var i = 0; i < 10; i++) {
users[users.length] = {username: 'user'}
}
this.User.bulkCreate(users).success(function() {
self.User.findAll().success(function(users) {
users.forEach(function(u) {
expect(u.isDirty).to.be.false
})
done()
})
})
})
})
describe('increment', function () {
beforeEach(function(done) {
this.User.create({ id: 1, aNumber: 0, bNumber: 0 }).complete(function(){
......@@ -985,17 +1134,21 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
identifier: 'identifier'
}).success(function(user) {
var oldCreatedAt = user.createdAt
, oldUpdatedAt = user.updatedAt
, oldIdentifier = user.identifier
user.updateAttributes({
name: 'foobar',
createdAt: new Date(2000, 1, 1),
identifier: 'another identifier'
}).success(function(user) {
expect((new Date(user.createdAt)).getTime()).to.equal((new Date(oldCreatedAt)).getTime())
expect(user.identifier).to.equal(oldIdentifier)
done()
})
setTimeout(function () {
user.updateAttributes({
name: 'foobar',
createdAt: new Date(2000, 1, 1),
identifier: 'another identifier'
}).success(function(user) {
expect(new Date(user.createdAt)).to.equalDate(new Date(oldCreatedAt))
expect(new Date(user.updatedAt)).to.not.equalTime(new Date(oldUpdatedAt))
expect(user.identifier).to.equal(oldIdentifier)
done()
})
}, 1000)
})
})
})
......
var chai = require('chai')
, expect = chai.expect
, Support = require(__dirname + '/support')
, DataTypes = require(__dirname + "/../lib/data-types")
, QueryChainer = require("../lib/query-chainer")
, Migrator = require("../lib/migrator")
, dialect = Support.getTestDialect()
......@@ -16,6 +14,7 @@ describe(Support.getTestDialectTeaser("Migrator"), function() {
logging: function(){}
}, options || {})
// this.sequelize.options.logging = console.log
var migrator = new Migrator(this.sequelize, options)
migrator
......@@ -87,7 +86,7 @@ describe(Support.getTestDialectTeaser("Migrator"), function() {
SequelizeMeta.create({ from: null, to: 20111117063700 }).success(function() {
migrator.getUndoneMigrations(function(err, migrations) {
expect(err).to.be.null
expect(migrations).to.have.length(6)
expect(migrations).to.have.length(7)
expect(migrations[0].filename).to.equal('20111130161100-emptyMigration.js')
done()
})
......@@ -186,6 +185,31 @@ describe(Support.getTestDialectTeaser("Migrator"), function() {
})
describe('addColumn', function() {
it('adds a unique column to the user table', function(done) {
var self = this
this.init({ from: 20111117063700, to: 20111205167000 }, function(migrator) {
migrator.migrate().complete(function(err) {
self.sequelize.getQueryInterface().describeTable('User').complete(function(err, data) {
var signature = data.signature
, isAdmin = data.isAdmin
, shopId = data.shopId
expect(signature.allowNull).to.be.true
expect(isAdmin.allowNull).to.be.false
if (dialect === "postgres" || dialect === "postgres-native" || dialect === "sqlite") {
expect(isAdmin.defaultValue).to.be.false
} else {
expect(isAdmin.defaultValue).to.equal("0")
}
expect(shopId.allowNull).to.be.true
done()
})
})
})
})
it('adds a column to the user table', function(done) {
var self = this
......
......@@ -85,6 +85,57 @@ if (dialect.match(/^mysql/)) {
})
})
describe('validations', function() {
describe('enums', function() {
it('enum data type should be case insensitive if my collation allows it', function(done) {
var User = this.sequelize.define('User' + config.rand(), {
mood: {
type: DataTypes.ENUM,
values: ['HAPPY', 'sad', 'WhatEver']
}
}, {
collate: 'utf8_general_ci'
})
User.sync({ force: true }).success(function() {
User.create({mood: 'happy'}).success(function(user) {
expect(user).to.exist
expect(user.mood).to.equal('HAPPY')
var u = User.build({mood: 'SAD'})
u.save().success(function(_user) {
expect(_user).to.exist
expect(_user.mood).to.equal('sad')
done()
})
})
})
})
it('enum data type should be case sensitive if my collation enforces it', function(done) {
var User = this.sequelize.define('User' + config.rand(), {
mood: {
type: DataTypes.ENUM,
values: ['HAPPY', 'sad', 'WhatEver']
}
}, {
collate: 'latin1_bin'
})
User.sync({ force: true }).success(function() {
expect(function() {
User.create({mood: 'happy'})
}).to.throw(Error, 'Value "happy" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver')
expect(function() {
var u = User.build({mood: 'SAD'})
u.save()
}).to.throw(Error, 'Value "SAD" for ENUM mood is out of allowed scope. Allowed values: HAPPY, sad, WhatEver')
done()
})
})
})
})
describe('primaryKeys', function() {
it("determines the correct primaryKeys", function(done) {
var User = this.sequelize.define('User' + config.rand(), {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!