From 69b85c372a685649f76d9111a749cd6d12e47d34 Mon Sep 17 00:00:00 2001 From: Sushant Date: Sun, 10 Mar 2019 13:27:47 +0530 Subject: [PATCH] refactor: instance tests --- test/integration/instance.test.js | 1385 +------------------ test/integration/instance/decrement.test.js | 222 +++ test/integration/instance/destroy.test.js | 164 +++ test/integration/instance/increment.test.js | 221 +++ test/integration/instance/reload.test.js | 308 +++++ test/integration/instance/save.test.js | 695 ++++++++++ 6 files changed, 1611 insertions(+), 1384 deletions(-) create mode 100644 test/integration/instance/decrement.test.js create mode 100644 test/integration/instance/destroy.test.js create mode 100644 test/integration/instance/increment.test.js create mode 100644 test/integration/instance/reload.test.js create mode 100644 test/integration/instance/save.test.js diff --git a/test/integration/instance.test.js b/test/integration/instance.test.js index fb94813b12e4..7089e1761378 100644 --- a/test/integration/instance.test.js +++ b/test/integration/instance.test.js @@ -2,14 +2,11 @@ const chai = require('chai'), expect = chai.expect, - Sequelize = require('../../index'), Support = require('./support'), DataTypes = require('../../lib/data-types'), dialect = Support.getTestDialect(), - config = require('../config/config'), sinon = require('sinon'), - isUUID = require('validator').isUUID, - current = Support.sequelize; + isUUID = require('validator').isUUID; describe(Support.getTestDialectTeaser('Instance'), () => { before(function() { @@ -119,596 +116,6 @@ describe(Support.getTestDialectTeaser('Instance'), () => { }); }); - describe('increment', () => { - beforeEach(function() { - return this.User.create({ id: 1, aNumber: 0, bNumber: 0 }); - }); - - if (current.dialect.supports.transactions) { - it('supports transactions', function() { - return Support.prepareTransactionTest(this.sequelize).then(sequelize => { - const User = sequelize.define('User', { number: Support.Sequelize.INTEGER }); - - return User.sync({ force: true }).then(() => { - return User.create({ number: 1 }).then(user => { - return sequelize.transaction().then(t => { - return user.increment('number', { by: 2, transaction: t }).then(() => { - return User.findAll().then(users1 => { - return User.findAll({ transaction: t }).then(users2 => { - expect(users1[0].number).to.equal(1); - expect(users2[0].number).to.equal(3); - return t.rollback(); - }); - }); - }); - }); - }); - }); - }); - }); - } - - if (current.dialect.supports.returnValues.returning) { - it('supports returning', function() { - return this.User.findByPk(1).then(user1 => { - return user1.increment('aNumber', { by: 2 }).then(() => { - expect(user1.aNumber).to.be.equal(2); - return user1.increment('bNumber', { by: 2, returning: false }).then(user3 => { - expect(user3.bNumber).to.be.equal(0); - }); - }); - }); - }); - } - - it('supports where conditions', function() { - return this.User.findByPk(1).then(user1 => { - return user1.increment(['aNumber'], { by: 2, where: { bNumber: 1 } }).then(() => { - return this.User.findByPk(1).then(user3 => { - expect(user3.aNumber).to.be.equal(0); - }); - }); - }); - }); - - it('with array', function() { - return this.User.findByPk(1).then(user1 => { - return user1.increment(['aNumber'], { by: 2 }).then(() => { - return this.User.findByPk(1).then(user3 => { - expect(user3.aNumber).to.be.equal(2); - }); - }); - }); - }); - - it('with single field', function() { - return this.User.findByPk(1).then(user1 => { - return user1.increment('aNumber', { by: 2 }).then(() => { - return this.User.findByPk(1).then(user3 => { - expect(user3.aNumber).to.be.equal(2); - }); - }); - }); - }); - - it('with single field and no value', function() { - return this.User.findByPk(1).then(user1 => { - return user1.increment('aNumber').then(() => { - return this.User.findByPk(1).then(user2 => { - expect(user2.aNumber).to.be.equal(1); - }); - }); - }); - }); - - it('should still work right with other concurrent updates', function() { - return this.User.findByPk(1).then(user1 => { - // Select the user again (simulating a concurrent query) - return this.User.findByPk(1).then(user2 => { - return user2.update({ - aNumber: user2.aNumber + 1 - }).then(() => { - return user1.increment(['aNumber'], { by: 2 }).then(() => { - return this.User.findByPk(1).then(user5 => { - expect(user5.aNumber).to.be.equal(3); - }); - }); - }); - }); - }); - }); - - it('should still work right with other concurrent increments', function() { - return this.User.findByPk(1).then(user1 => { - return Sequelize.Promise.all([ - user1.increment(['aNumber'], { by: 2 }), - user1.increment(['aNumber'], { by: 2 }), - user1.increment(['aNumber'], { by: 2 }) - ]).then(() => { - return this.User.findByPk(1).then(user2 => { - expect(user2.aNumber).to.equal(6); - }); - }); - }); - }); - - it('with key value pair', function() { - return this.User.findByPk(1).then(user1 => { - return user1.increment({ 'aNumber': 1, 'bNumber': 2 }).then(() => { - return this.User.findByPk(1).then(user3 => { - expect(user3.aNumber).to.be.equal(1); - expect(user3.bNumber).to.be.equal(2); - }); - }); - }); - }); - - it('with timestamps set to true', function() { - const User = this.sequelize.define('IncrementUser', { - aNumber: DataTypes.INTEGER - }, { timestamps: true }); - - let oldDate; - - return User.sync({ force: true }) - .then(() => User.create({ aNumber: 1 })) - .then(user => { - oldDate = user.get('updatedAt'); - - this.clock.tick(1000); - return user.increment('aNumber', { by: 1 }); - }) - .then(user => user.reload()) - .then(user => { - return expect(user).to.have.property('updatedAt').afterTime(oldDate); - }); - }); - - it('with timestamps set to true and options.silent set to true', function() { - const User = this.sequelize.define('IncrementUser', { - aNumber: DataTypes.INTEGER - }, { timestamps: true }); - let oldDate; - - return User.sync({ force: true }).then(() => { - return User.create({ aNumber: 1 }); - }).then(user => { - oldDate = user.updatedAt; - this.clock.tick(1000); - return user.increment('aNumber', { by: 1, silent: true }); - }).then(() => { - return expect(User.findByPk(1)).to.eventually.have.property('updatedAt').equalTime(oldDate); - }); - }); - }); - - describe('decrement', () => { - beforeEach(function() { - return this.User.create({ id: 1, aNumber: 0, bNumber: 0 }); - }); - - if (current.dialect.supports.transactions) { - it('supports transactions', function() { - return Support.prepareTransactionTest(this.sequelize).then(sequelize => { - const User = sequelize.define('User', { number: Support.Sequelize.INTEGER }); - - return User.sync({ force: true }).then(() => { - return User.create({ number: 3 }).then(user => { - return sequelize.transaction().then(t => { - return user.decrement('number', { by: 2, transaction: t }).then(() => { - return User.findAll().then(users1 => { - return User.findAll({ transaction: t }).then(users2 => { - expect(users1[0].number).to.equal(3); - expect(users2[0].number).to.equal(1); - return t.rollback(); - }); - }); - }); - }); - }); - }); - }); - }); - } - - if (current.dialect.supports.returnValues.returning) { - it('supports returning', function() { - return this.User.findByPk(1).then(user1 => { - return user1.decrement('aNumber', { by: 2 }).then(() => { - expect(user1.aNumber).to.be.equal(-2); - return user1.decrement('bNumber', { by: 2, returning: false }).then(user3 => { - expect(user3.bNumber).to.be.equal(0); - }); - }); - }); - }); - } - - it('with array', function() { - return this.User.findByPk(1).then(user1 => { - return user1.decrement(['aNumber'], { by: 2 }).then(() => { - return this.User.findByPk(1).then(user3 => { - expect(user3.aNumber).to.be.equal(-2); - }); - }); - }); - }); - - it('with single field', function() { - return this.User.findByPk(1).then(user1 => { - return user1.decrement('aNumber', { by: 2 }).then(() => { - return this.User.findByPk(1).then(user3 => { - expect(user3.aNumber).to.be.equal(-2); - }); - }); - }); - }); - - it('with single field and no value', function() { - return this.User.findByPk(1).then(user1 => { - return user1.decrement('aNumber').then(() => { - return this.User.findByPk(1).then(user2 => { - expect(user2.aNumber).to.be.equal(-1); - }); - }); - }); - }); - - it('should still work right with other concurrent updates', function() { - return this.User.findByPk(1).then(user1 => { - // Select the user again (simulating a concurrent query) - return this.User.findByPk(1).then(user2 => { - return user2.update({ - aNumber: user2.aNumber + 1 - }).then(() => { - return user1.decrement(['aNumber'], { by: 2 }).then(() => { - return this.User.findByPk(1).then(user5 => { - expect(user5.aNumber).to.be.equal(-1); - }); - }); - }); - }); - }); - }); - - it('should still work right with other concurrent increments', function() { - return this.User.findByPk(1).then(user1 => { - return Sequelize.Promise.all([ - user1.decrement(['aNumber'], { by: 2 }), - user1.decrement(['aNumber'], { by: 2 }), - user1.decrement(['aNumber'], { by: 2 }) - ]).then(() => { - return this.User.findByPk(1).then(user2 => { - expect(user2.aNumber).to.equal(-6); - }); - }); - }); - }); - - it('with key value pair', function() { - return this.User.findByPk(1).then(user1 => { - return user1.decrement({ 'aNumber': 1, 'bNumber': 2 }).then(() => { - return this.User.findByPk(1).then(user3 => { - expect(user3.aNumber).to.be.equal(-1); - expect(user3.bNumber).to.be.equal(-2); - }); - }); - }); - }); - - it('with negative value', function() { - return this.User.findByPk(1).then(user1 => { - return Sequelize.Promise.all([ - user1.decrement('aNumber', { by: -2 }), - user1.decrement(['aNumber', 'bNumber'], { by: -2 }), - user1.decrement({ 'aNumber': -1, 'bNumber': -2 }) - ]).then(() => { - return this.User.findByPk(1).then(user3 => { - expect(user3.aNumber).to.be.equal(+5); - expect(user3.bNumber).to.be.equal(+4); - }); - }); - }); - }); - - it('with timestamps set to true', function() { - const User = this.sequelize.define('IncrementUser', { - aNumber: DataTypes.INTEGER - }, { timestamps: true }); - let oldDate; - - return User.sync({ force: true }).then(() => { - return User.create({ aNumber: 1 }); - }).then(user => { - oldDate = user.updatedAt; - this.clock.tick(1000); - return user.decrement('aNumber', { by: 1 }); - }).then(() => { - return expect(User.findByPk(1)).to.eventually.have.property('updatedAt').afterTime(oldDate); - }); - }); - - it('with timestamps set to true and options.silent set to true', function() { - const User = this.sequelize.define('IncrementUser', { - aNumber: DataTypes.INTEGER - }, { timestamps: true }); - let oldDate; - - return User.sync({ force: true }).then(() => { - return User.create({ aNumber: 1 }); - }).then(user => { - oldDate = user.updatedAt; - this.clock.tick(1000); - return user.decrement('aNumber', { by: 1, silent: true }); - }).then(() => { - return expect(User.findByPk(1)).to.eventually.have.property('updatedAt').equalTime(oldDate); - }); - }); - }); - - describe('reload', () => { - if (current.dialect.supports.transactions) { - it('supports transactions', function() { - return Support.prepareTransactionTest(this.sequelize).then(sequelize => { - const User = sequelize.define('User', { username: Support.Sequelize.STRING }); - - return User.sync({ force: true }).then(() => { - return User.create({ username: 'foo' }).then(user => { - return sequelize.transaction().then(t => { - return User.update({ username: 'bar' }, { where: { username: 'foo' }, transaction: t }).then(() => { - return user.reload().then(user => { - expect(user.username).to.equal('foo'); - return user.reload({ transaction: t }).then(user => { - expect(user.username).to.equal('bar'); - return t.rollback(); - }); - }); - }); - }); - }); - }); - }); - }); - } - - it('should return a reference to the same DAO instead of creating a new one', function() { - return this.User.create({ username: 'John Doe' }).then(originalUser => { - return originalUser.update({ username: 'Doe John' }).then(() => { - return originalUser.reload().then(updatedUser => { - expect(originalUser === updatedUser).to.be.true; - }); - }); - }); - }); - - it('should update the values on all references to the DAO', function() { - return this.User.create({ username: 'John Doe' }).then(originalUser => { - return this.User.findByPk(originalUser.id).then(updater => { - return updater.update({ username: 'Doe John' }).then(() => { - // We used a different reference when calling update, so originalUser is now out of sync - expect(originalUser.username).to.equal('John Doe'); - return originalUser.reload().then(updatedUser => { - expect(originalUser.username).to.equal('Doe John'); - expect(updatedUser.username).to.equal('Doe John'); - }); - }); - }); - }); - }); - - it('should support updating a subset of attributes', function() { - return this.User.create({ - aNumber: 1, - bNumber: 1 - }).tap(user => { - return this.User.update({ - bNumber: 2 - }, { - where: { - id: user.get('id') - } - }); - }).then(user => { - return user.reload({ - attributes: ['bNumber'] - }); - }).then(user => { - expect(user.get('aNumber')).to.equal(1); - expect(user.get('bNumber')).to.equal(2); - }); - }); - - it('should update read only attributes as well (updatedAt)', function() { - return this.User.create({ username: 'John Doe' }).then(originalUser => { - this.originallyUpdatedAt = originalUser.updatedAt; - this.originalUser = originalUser; - - // Wait for a second, so updatedAt will actually be different - this.clock.tick(1000); - return this.User.findByPk(originalUser.id); - }).then(updater => { - return updater.update({ username: 'Doe John' }); - }).then(updatedUser => { - this.updatedUser = updatedUser; - return this.originalUser.reload(); - }).then(() => { - expect(this.originalUser.updatedAt).to.be.above(this.originallyUpdatedAt); - expect(this.updatedUser.updatedAt).to.be.above(this.originallyUpdatedAt); - }); - }); - - it('should update the associations as well', function() { - const Book = this.sequelize.define('Book', { title: DataTypes.STRING }), - Page = this.sequelize.define('Page', { content: DataTypes.TEXT }); - - Book.hasMany(Page); - Page.belongsTo(Book); - - return Book.sync({ force: true }).then(() => { - return Page.sync({ force: true }).then(() => { - return Book.create({ title: 'A very old book' }).then(book => { - return Page.create({ content: 'om nom nom' }).then(page => { - return book.setPages([page]).then(() => { - return Book.findOne({ - where: { id: book.id }, - include: [Page] - }).then(leBook => { - return page.update({ content: 'something totally different' }).then(page => { - expect(leBook.Pages.length).to.equal(1); - expect(leBook.Pages[0].content).to.equal('om nom nom'); - expect(page.content).to.equal('something totally different'); - return leBook.reload().then(leBook => { - expect(leBook.Pages.length).to.equal(1); - expect(leBook.Pages[0].content).to.equal('something totally different'); - expect(page.content).to.equal('something totally different'); - }); - }); - }); - }); - }); - }); - }); - }); - }); - - it('should update internal options of the instance', function() { - const Book = this.sequelize.define('Book', { title: DataTypes.STRING }), - Page = this.sequelize.define('Page', { content: DataTypes.TEXT }); - - Book.hasMany(Page); - Page.belongsTo(Book); - - return Book.sync({ force: true }).then(() => { - return Page.sync({ force: true }).then(() => { - return Book.create({ title: 'A very old book' }).then(book => { - return Page.create().then(page => { - return book.setPages([page]).then(() => { - return Book.findOne({ - where: { id: book.id } - }).then(leBook => { - const oldOptions = leBook._options; - return leBook.reload({ - include: [Page] - }).then(leBook => { - expect(oldOptions).not.to.equal(leBook._options); - expect(leBook._options.include.length).to.equal(1); - expect(leBook.Pages.length).to.equal(1); - expect(leBook.get({ plain: true }).Pages.length).to.equal(1); - }); - }); - }); - }); - }); - }); - }); - }); - - it('should return an error when reload fails', function() { - return this.User.create({ username: 'John Doe' }).then(user => { - return user.destroy().then(() => { - return expect(user.reload()).to.be.rejectedWith( - Sequelize.InstanceError, - 'Instance could not be reloaded because it does not exist anymore (find call returned null)' - ); - }); - }); - }); - - it('should set an association to null after deletion, 1-1', function() { - const Shoe = this.sequelize.define('Shoe', { brand: DataTypes.STRING }), - Player = this.sequelize.define('Player', { name: DataTypes.STRING }); - - Player.hasOne(Shoe); - Shoe.belongsTo(Player); - - return this.sequelize.sync({ force: true }).then(() => { - return Shoe.create({ - brand: 'the brand', - Player: { - name: 'the player' - } - }, { include: [Player] }); - }).then(shoe => { - return Player.findOne({ - where: { id: shoe.Player.id }, - include: [Shoe] - }).then(lePlayer => { - expect(lePlayer.Shoe).not.to.be.null; - return lePlayer.Shoe.destroy().return(lePlayer); - }).then(lePlayer => { - return lePlayer.reload(); - }).then(lePlayer => { - expect(lePlayer.Shoe).to.be.null; - }); - }); - }); - - it('should set an association to empty after all deletion, 1-N', function() { - const Team = this.sequelize.define('Team', { name: DataTypes.STRING }), - Player = this.sequelize.define('Player', { name: DataTypes.STRING }); - - Team.hasMany(Player); - Player.belongsTo(Team); - - return this.sequelize.sync({ force: true }).then(() => { - return Team.create({ - name: 'the team', - Players: [{ - name: 'the player1' - }, { - name: 'the player2' - }] - }, { include: [Player] }); - }).then(team => { - return Team.findOne({ - where: { id: team.id }, - include: [Player] - }).then(leTeam => { - expect(leTeam.Players).not.to.be.empty; - return leTeam.Players[1].destroy().then(() => { - return leTeam.Players[0].destroy(); - }).return(leTeam); - }).then(leTeam => { - return leTeam.reload(); - }).then(leTeam => { - expect(leTeam.Players).to.be.empty; - }); - }); - }); - - it('should update the associations after one element deleted', function() { - const Team = this.sequelize.define('Team', { name: DataTypes.STRING }), - Player = this.sequelize.define('Player', { name: DataTypes.STRING }); - - Team.hasMany(Player); - Player.belongsTo(Team); - - - return this.sequelize.sync({ force: true }).then(() => { - return Team.create({ - name: 'the team', - Players: [{ - name: 'the player1' - }, { - name: 'the player2' - }] - }, { include: [Player] }); - }).then(team => { - return Team.findOne({ - where: { id: team.id }, - include: [Player] - }).then(leTeam => { - expect(leTeam.Players).to.have.length(2); - return leTeam.Players[0].destroy().return(leTeam); - }).then(leTeam => { - return leTeam.reload(); - }).then(leTeam => { - expect(leTeam.Players).to.have.length(1); - }); - }); - }); - }); - describe('default values', () => { describe('uuid', () => { it('should store a string in uuidv1 and uuidv4', function() { @@ -884,642 +291,6 @@ describe(Support.getTestDialectTeaser('Instance'), () => { }); }); - describe('save', () => { - if (current.dialect.supports.transactions) { - it('supports transactions', function() { - return Support.prepareTransactionTest(this.sequelize).then(sequelize => { - const User = sequelize.define('User', { username: Support.Sequelize.STRING }); - return User.sync({ force: true }).then(() => { - return sequelize.transaction().then(t => { - return User.build({ username: 'foo' }).save({ transaction: t }).then(() => { - return User.count().then(count1 => { - return User.count({ transaction: t }).then(count2 => { - expect(count1).to.equal(0); - expect(count2).to.equal(1); - return t.rollback(); - }); - }); - }); - }); - }); - }); - }); - } - - it('only updates fields in passed array', function() { - const date = new Date(1990, 1, 1); - - return this.User.create({ - username: 'foo', - touchedAt: new Date() - }).then(user => { - user.username = 'fizz'; - user.touchedAt = date; - - return user.save({ fields: ['username'] }).then(() => { - // re-select user - return this.User.findByPk(user.id).then(user2 => { - // name should have changed - expect(user2.username).to.equal('fizz'); - // bio should be unchanged - expect(user2.birthDate).not.to.equal(date); - }); - }); - }); - }); - - it('should work on a model with an attribute named length', function() { - const Box = this.sequelize.define('box', { - length: DataTypes.INTEGER, - width: DataTypes.INTEGER, - height: DataTypes.INTEGER - }); - - return Box.sync({ force: true }).then(() => { - return Box.create({ - length: 1, - width: 2, - height: 3 - }).then(box => { - return box.update({ - length: 4, - width: 5, - height: 6 - }); - }).then(() => { - return Box.findOne({}).then(box => { - expect(box.get('length')).to.equal(4); - expect(box.get('width')).to.equal(5); - expect(box.get('height')).to.equal(6); - }); - }); - }); - }); - - it('only validates fields in passed array', function() { - return this.User.build({ - validateTest: 'cake', // invalid, but not saved - validateCustom: '1' - }).save({ - fields: ['validateCustom'] - }); - }); - - describe('hooks', () => { - it('should update attributes added in hooks when default fields are used', function() { - const User = this.sequelize.define(`User${config.rand()}`, { - name: DataTypes.STRING, - bio: DataTypes.TEXT, - email: DataTypes.STRING - }); - - User.beforeUpdate(instance => { - instance.set('email', 'B'); - }); - - return User.sync({ force: true }).then(() => { - return User.create({ - name: 'A', - bio: 'A', - email: 'A' - }).then(user => { - return user.set({ - name: 'B', - bio: 'B' - }).save(); - }).then(() => { - return User.findOne({}); - }).then(user => { - expect(user.get('name')).to.equal('B'); - expect(user.get('bio')).to.equal('B'); - expect(user.get('email')).to.equal('B'); - }); - }); - }); - - it('should update attributes changed in hooks when default fields are used', function() { - const User = this.sequelize.define(`User${config.rand()}`, { - name: DataTypes.STRING, - bio: DataTypes.TEXT, - email: DataTypes.STRING - }); - - User.beforeUpdate(instance => { - instance.set('email', 'C'); - }); - - return User.sync({ force: true }).then(() => { - return User.create({ - name: 'A', - bio: 'A', - email: 'A' - }).then(user => { - return user.set({ - name: 'B', - bio: 'B', - email: 'B' - }).save(); - }).then(() => { - return User.findOne({}); - }).then(user => { - expect(user.get('name')).to.equal('B'); - expect(user.get('bio')).to.equal('B'); - expect(user.get('email')).to.equal('C'); - }); - }); - }); - - it('should validate attributes added in hooks when default fields are used', function() { - const User = this.sequelize.define(`User${config.rand()}`, { - name: DataTypes.STRING, - bio: DataTypes.TEXT, - email: { - type: DataTypes.STRING, - validate: { - isEmail: true - } - } - }); - - User.beforeUpdate(instance => { - instance.set('email', 'B'); - }); - - return User.sync({ force: true }).then(() => { - return User.create({ - name: 'A', - bio: 'A', - email: 'valid.email@gmail.com' - }).then(user => { - return expect(user.set({ - name: 'B' - }).save()).to.be.rejectedWith(Sequelize.ValidationError); - }).then(() => { - return User.findOne({}).then(user => { - expect(user.get('email')).to.equal('valid.email@gmail.com'); - }); - }); - }); - }); - - it('should validate attributes changed in hooks when default fields are used', function() { - const User = this.sequelize.define(`User${config.rand()}`, { - name: DataTypes.STRING, - bio: DataTypes.TEXT, - email: { - type: DataTypes.STRING, - validate: { - isEmail: true - } - } - }); - - User.beforeUpdate(instance => { - instance.set('email', 'B'); - }); - - return User.sync({ force: true }).then(() => { - return User.create({ - name: 'A', - bio: 'A', - email: 'valid.email@gmail.com' - }).then(user => { - return expect(user.set({ - name: 'B', - email: 'still.valid.email@gmail.com' - }).save()).to.be.rejectedWith(Sequelize.ValidationError); - }).then(() => { - return User.findOne({}).then(user => { - expect(user.get('email')).to.equal('valid.email@gmail.com'); - }); - }); - }); - }); - }); - - it('stores an entry in the database', function() { - const username = 'user', - User = this.User, - user = this.User.build({ - username, - touchedAt: new Date(1984, 8, 23) - }); - - return User.findAll().then(users => { - expect(users).to.have.length(0); - return user.save().then(() => { - return User.findAll().then(users => { - expect(users).to.have.length(1); - expect(users[0].username).to.equal(username); - expect(users[0].touchedAt).to.be.instanceof(Date); - expect(users[0].touchedAt).to.equalDate(new Date(1984, 8, 23)); - }); - }); - }); - }); - - it('handles an entry with primaryKey of zero', function() { - const username = 'user', - newUsername = 'newUser', - User2 = this.sequelize.define('User2', - { - id: { - type: DataTypes.INTEGER.UNSIGNED, - autoIncrement: false, - primaryKey: true - }, - username: { type: DataTypes.STRING } - }); - - return User2.sync().then(() => { - return User2.create({ id: 0, username }).then(user => { - expect(user).to.be.ok; - expect(user.id).to.equal(0); - expect(user.username).to.equal(username); - return User2.findByPk(0).then(user => { - expect(user).to.be.ok; - expect(user.id).to.equal(0); - expect(user.username).to.equal(username); - return user.update({ username: newUsername }).then(user => { - expect(user).to.be.ok; - expect(user.id).to.equal(0); - expect(user.username).to.equal(newUsername); - }); - }); - }); - }); - }); - - it('updates the timestamps', function() { - const now = new Date(); - now.setMilliseconds(0); - - const user = this.User.build({ username: 'user' }); - this.clock.tick(1000); - - return user.save().then(savedUser => { - expect(savedUser).have.property('updatedAt').afterTime(now); - - this.clock.tick(1000); - return savedUser.save(); - }).then(updatedUser => { - expect(updatedUser).have.property('updatedAt').afterTime(now); - }); - }); - - it('does not update timestamps when passing silent=true', function() { - return this.User.create({ username: 'user' }).then(user => { - const updatedAt = user.updatedAt; - - this.clock.tick(1000); - return expect(user.update({ - username: 'userman' - }, { - silent: true - })).to.eventually.have.property('updatedAt').equalTime(updatedAt); - }); - }); - - it('does not update timestamps when passing silent=true in a bulk update', function() { - const data = [ - { username: 'Paul' }, - { username: 'Peter' } - ]; - let updatedAtPeter, - updatedAtPaul; - - return this.User.bulkCreate(data).then(() => { - return this.User.findAll(); - }).then(users => { - updatedAtPaul = users[0].updatedAt; - updatedAtPeter = users[1].updatedAt; - }) - .then(() => { - this.clock.tick(150); - return this.User.update( - { aNumber: 1 }, - { where: {}, silent: true } - ); - }).then(() => { - return this.User.findAll(); - }).then(users => { - expect(users[0].updatedAt).to.equalTime(updatedAtPeter); - expect(users[1].updatedAt).to.equalTime(updatedAtPaul); - }); - }); - - describe('when nothing changed', () => { - it('does not update timestamps', function() { - return this.User.create({ username: 'John' }).then(() => { - return this.User.findOne({ where: { username: 'John' } }).then(user => { - const updatedAt = user.updatedAt; - this.clock.tick(2000); - return user.save().then(newlySavedUser => { - expect(newlySavedUser.updatedAt).to.equalTime(updatedAt); - return this.User.findOne({ where: { username: 'John' } }).then(newlySavedUser => { - expect(newlySavedUser.updatedAt).to.equalTime(updatedAt); - }); - }); - }); - }); - }); - - it('should not throw ER_EMPTY_QUERY if changed only virtual fields', function() { - const User = this.sequelize.define(`User${config.rand()}`, { - name: DataTypes.STRING, - bio: { - type: DataTypes.VIRTUAL, - get: () => 'swag' - } - }, { - timestamps: false - }); - return User.sync({ force: true }).then(() => - User.create({ name: 'John', bio: 'swag 1' }).then(user => user.update({ bio: 'swag 2' }).should.be.fulfilled) - ); - }); - }); - - it('updates with function and column value', function() { - return this.User.create({ - aNumber: 42 - }).then(user => { - user.bNumber = this.sequelize.col('aNumber'); - user.username = this.sequelize.fn('upper', 'sequelize'); - return user.save().then(() => { - return this.User.findByPk(user.id).then(user2 => { - expect(user2.username).to.equal('SEQUELIZE'); - expect(user2.bNumber).to.equal(42); - }); - }); - }); - }); - - describe('without timestamps option', () => { - it("doesn't update the updatedAt column", function() { - const User2 = this.sequelize.define('User2', { - username: DataTypes.STRING, - updatedAt: DataTypes.DATE - }, { timestamps: false }); - return User2.sync().then(() => { - return User2.create({ username: 'john doe' }).then(johnDoe => { - // sqlite and mysql return undefined, whereas postgres returns null - expect([undefined, null]).to.include(johnDoe.updatedAt); - }); - }); - }); - }); - - describe('with custom timestamp options', () => { - it('updates the createdAt column if updatedAt is disabled', function() { - const now = new Date(); - this.clock.tick(1000); - - const User2 = this.sequelize.define('User2', { - username: DataTypes.STRING - }, { updatedAt: false }); - - return User2.sync().then(() => { - return User2.create({ username: 'john doe' }).then(johnDoe => { - expect(johnDoe.updatedAt).to.be.undefined; - expect(now).to.be.beforeTime(johnDoe.createdAt); - }); - }); - }); - - it('updates the updatedAt column if createdAt is disabled', function() { - const now = new Date(); - this.clock.tick(1000); - - const User2 = this.sequelize.define('User2', { - username: DataTypes.STRING - }, { createdAt: false }); - - return User2.sync().then(() => { - return User2.create({ username: 'john doe' }).then(johnDoe => { - expect(johnDoe.createdAt).to.be.undefined; - expect(now).to.be.beforeTime(johnDoe.updatedAt); - }); - }); - }); - - it('works with `allowNull: false` on createdAt and updatedAt columns', function() { - const User2 = this.sequelize.define('User2', { - username: DataTypes.STRING, - createdAt: { - type: DataTypes.DATE, - allowNull: false - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false - } - }, { timestamps: true }); - - return User2.sync().then(() => { - return User2.create({ username: 'john doe' }).then(johnDoe => { - expect(johnDoe.createdAt).to.be.an.instanceof(Date); - expect( ! isNaN(johnDoe.createdAt.valueOf()) ).to.be.ok; - expect(johnDoe.createdAt).to.equalTime(johnDoe.updatedAt); - }); - }); - }); - }); - - it('should fail a validation upon creating', function() { - return this.User.create({ aNumber: 0, validateTest: 'hello' }).catch(err => { - expect(err).to.exist; - expect(err).to.be.instanceof(Object); - expect(err.get('validateTest')).to.be.instanceof(Array); - expect(err.get('validateTest')[0]).to.exist; - expect(err.get('validateTest')[0].message).to.equal('Validation isInt on validateTest failed'); - }); - }); - - it('should fail a validation upon creating with hooks false', function() { - return this.User.create({ aNumber: 0, validateTest: 'hello' }, { hooks: false }).catch(err => { - expect(err).to.exist; - expect(err).to.be.instanceof(Object); - expect(err.get('validateTest')).to.be.instanceof(Array); - expect(err.get('validateTest')[0]).to.exist; - expect(err.get('validateTest')[0].message).to.equal('Validation isInt on validateTest failed'); - }); - }); - - it('should fail a validation upon building', function() { - return this.User.build({ aNumber: 0, validateCustom: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' }).save() - .catch(err => { - expect(err).to.exist; - expect(err).to.be.instanceof(Object); - expect(err.get('validateCustom')).to.exist; - expect(err.get('validateCustom')).to.be.instanceof(Array); - expect(err.get('validateCustom')[0]).to.exist; - expect(err.get('validateCustom')[0].message).to.equal('Length failed.'); - }); - }); - - it('should fail a validation when updating', function() { - return this.User.create({ aNumber: 0 }).then(user => { - return user.update({ validateTest: 'hello' }).catch(err => { - expect(err).to.exist; - expect(err).to.be.instanceof(Object); - expect(err.get('validateTest')).to.exist; - expect(err.get('validateTest')).to.be.instanceof(Array); - expect(err.get('validateTest')[0]).to.exist; - expect(err.get('validateTest')[0].message).to.equal('Validation isInt on validateTest failed'); - }); - }); - }); - - it('takes zero into account', function() { - return this.User.build({ aNumber: 0 }).save({ - fields: ['aNumber'] - }).then(user => { - expect(user.aNumber).to.equal(0); - }); - }); - - it('saves a record with no primary key', function() { - const HistoryLog = this.sequelize.define('HistoryLog', { - someText: { type: DataTypes.STRING }, - aNumber: { type: DataTypes.INTEGER }, - aRandomId: { type: DataTypes.INTEGER } - }); - return HistoryLog.sync().then(() => { - return HistoryLog.create({ someText: 'Some random text', aNumber: 3, aRandomId: 5 }).then(log => { - return log.update({ aNumber: 5 }).then(newLog => { - expect(newLog.aNumber).to.equal(5); - }); - }); - }); - }); - - describe('eagerly loaded objects', () => { - beforeEach(function() { - this.UserEager = this.sequelize.define('UserEagerLoadingSaves', { - username: DataTypes.STRING, - age: DataTypes.INTEGER - }, { timestamps: false }); - - this.ProjectEager = this.sequelize.define('ProjectEagerLoadingSaves', { - title: DataTypes.STRING, - overdue_days: DataTypes.INTEGER - }, { timestamps: false }); - - this.UserEager.hasMany(this.ProjectEager, { as: 'Projects', foreignKey: 'PoobahId' }); - this.ProjectEager.belongsTo(this.UserEager, { as: 'Poobah', foreignKey: 'PoobahId' }); - - return this.UserEager.sync({ force: true }).then(() => { - return this.ProjectEager.sync({ force: true }); - }); - }); - - it('saves one object that has a collection of eagerly loaded objects', function() { - return this.UserEager.create({ username: 'joe', age: 1 }).then(user => { - return this.ProjectEager.create({ title: 'project-joe1', overdue_days: 0 }).then(project1 => { - return this.ProjectEager.create({ title: 'project-joe2', overdue_days: 0 }).then(project2 => { - return user.setProjects([project1, project2]).then(() => { - return this.UserEager.findOne({ where: { age: 1 }, include: [{ model: this.ProjectEager, as: 'Projects' }] }).then(user => { - expect(user.username).to.equal('joe'); - expect(user.age).to.equal(1); - expect(user.Projects).to.exist; - expect(user.Projects.length).to.equal(2); - - user.age = user.age + 1; // happy birthday joe - return user.save().then(user => { - expect(user.username).to.equal('joe'); - expect(user.age).to.equal(2); - expect(user.Projects).to.exist; - expect(user.Projects.length).to.equal(2); - }); - }); - }); - }); - }); - }); - }); - - it('saves many objects that each a have collection of eagerly loaded objects', function() { - return this.UserEager.create({ username: 'bart', age: 20 }).then(bart => { - return this.UserEager.create({ username: 'lisa', age: 20 }).then(lisa => { - return this.ProjectEager.create({ title: 'detention1', overdue_days: 0 }).then(detention1 => { - return this.ProjectEager.create({ title: 'detention2', overdue_days: 0 }).then(detention2 => { - return this.ProjectEager.create({ title: 'exam1', overdue_days: 0 }).then(exam1 => { - return this.ProjectEager.create({ title: 'exam2', overdue_days: 0 }).then(exam2 => { - return bart.setProjects([detention1, detention2]).then(() => { - return lisa.setProjects([exam1, exam2]).then(() => { - return this.UserEager.findAll({ where: { age: 20 }, order: [['username', 'ASC']], include: [{ model: this.ProjectEager, as: 'Projects' }] }).then(simpsons => { - expect(simpsons.length).to.equal(2); - - const _bart = simpsons[0]; - const _lisa = simpsons[1]; - - expect(_bart.Projects).to.exist; - expect(_lisa.Projects).to.exist; - expect(_bart.Projects.length).to.equal(2); - expect(_lisa.Projects.length).to.equal(2); - - _bart.age = _bart.age + 1; // happy birthday bart - off to Moe's - - return _bart.save().then(savedbart => { - expect(savedbart.username).to.equal('bart'); - expect(savedbart.age).to.equal(21); - - _lisa.username = 'lsimpson'; - - return _lisa.save().then(savedlisa => { - expect(savedlisa.username).to.equal('lsimpson'); - expect(savedlisa.age).to.equal(20); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - - it('saves many objects that each has one eagerly loaded object (to which they belong)', function() { - return this.UserEager.create({ username: 'poobah', age: 18 }).then(user => { - return this.ProjectEager.create({ title: 'homework', overdue_days: 10 }).then(homework => { - return this.ProjectEager.create({ title: 'party', overdue_days: 2 }).then(party => { - return user.setProjects([homework, party]).then(() => { - return this.ProjectEager.findAll({ include: [{ model: this.UserEager, as: 'Poobah' }] }).then(projects => { - expect(projects.length).to.equal(2); - expect(projects[0].Poobah).to.exist; - expect(projects[1].Poobah).to.exist; - expect(projects[0].Poobah.username).to.equal('poobah'); - expect(projects[1].Poobah.username).to.equal('poobah'); - - projects[0].title = 'partymore'; - projects[1].title = 'partymore'; - projects[0].overdue_days = 0; - projects[1].overdue_days = 0; - - return projects[0].save().then(() => { - return projects[1].save().then(() => { - return this.ProjectEager.findAll({ where: { title: 'partymore', overdue_days: 0 }, include: [{ model: this.UserEager, as: 'Poobah' }] }).then(savedprojects => { - expect(savedprojects.length).to.equal(2); - expect(savedprojects[0].Poobah).to.exist; - expect(savedprojects[1].Poobah).to.exist; - expect(savedprojects[0].Poobah.username).to.equal('poobah'); - expect(savedprojects[1].Poobah.username).to.equal('poobah'); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - describe('findAll', () => { beforeEach(function() { this.ParanoidUser = this.sequelize.define('ParanoidUser', { @@ -1806,160 +577,6 @@ describe(Support.getTestDialectTeaser('Instance'), () => { }); }); - describe('destroy', () => { - if (current.dialect.supports.transactions) { - it('supports transactions', function() { - return Support.prepareTransactionTest(this.sequelize).then(sequelize => { - const User = sequelize.define('User', { username: Support.Sequelize.STRING }); - - return User.sync({ force: true }).then(() => { - return User.create({ username: 'foo' }).then(user => { - return sequelize.transaction().then(t => { - return user.destroy({ transaction: t }).then(() => { - return User.count().then(count1 => { - return User.count({ transaction: t }).then(count2 => { - expect(count1).to.equal(1); - expect(count2).to.equal(0); - return t.rollback(); - }); - }); - }); - }); - }); - }); - }); - }); - } - - it('does not set the deletedAt date in subsequent destroys if dao is paranoid', function() { - const UserDestroy = this.sequelize.define('UserDestroy', { - name: Support.Sequelize.STRING, - bio: Support.Sequelize.TEXT - }, { paranoid: true }); - - return UserDestroy.sync({ force: true }).then(() => { - return UserDestroy.create({ name: 'hallo', bio: 'welt' }).then(user => { - return user.destroy().then(() => { - return user.reload({ paranoid: false }).then(() => { - const deletedAt = user.deletedAt; - - return user.destroy().then(() => { - return user.reload({ paranoid: false }).then(() => { - expect(user.deletedAt).to.eql(deletedAt); - }); - }); - }); - }); - }); - }); - }); - - it('deletes a record from the database if dao is not paranoid', function() { - const UserDestroy = this.sequelize.define('UserDestroy', { - name: Support.Sequelize.STRING, - bio: Support.Sequelize.TEXT - }); - - return UserDestroy.sync({ force: true }).then(() => { - return UserDestroy.create({ name: 'hallo', bio: 'welt' }).then(u => { - return UserDestroy.findAll().then(users => { - expect(users.length).to.equal(1); - return u.destroy().then(() => { - return UserDestroy.findAll().then(users => { - expect(users.length).to.equal(0); - }); - }); - }); - }); - }); - }); - - it('allows sql logging of delete statements', function() { - const UserDelete = this.sequelize.define('UserDelete', { - name: Support.Sequelize.STRING, - bio: Support.Sequelize.TEXT - }); - - return UserDelete.sync({ force: true }).then(() => { - return UserDelete.create({ name: 'hallo', bio: 'welt' }).then(u => { - return UserDelete.findAll().then(users => { - expect(users.length).to.equal(1); - return u.destroy({ - logging(sql) { - expect(sql).to.exist; - expect(sql.toUpperCase()).to.include('DELETE'); - } - }); - }); - }); - }); - }); - - it('delete a record of multiple primary keys table', function() { - const MultiPrimary = this.sequelize.define('MultiPrimary', { - bilibili: { - type: Support.Sequelize.CHAR(2), - primaryKey: true - }, - - guruguru: { - type: Support.Sequelize.CHAR(2), - primaryKey: true - } - }); - - return MultiPrimary.sync({ force: true }).then(() => { - return MultiPrimary.create({ bilibili: 'bl', guruguru: 'gu' }).then(() => { - return MultiPrimary.create({ bilibili: 'bl', guruguru: 'ru' }).then(m2 => { - return MultiPrimary.findAll().then(ms => { - expect(ms.length).to.equal(2); - return m2.destroy({ - logging(sql) { - expect(sql).to.exist; - expect(sql.toUpperCase()).to.include('DELETE'); - expect(sql).to.include('ru'); - expect(sql).to.include('bl'); - } - }).then(() => { - return MultiPrimary.findAll().then(ms => { - expect(ms.length).to.equal(1); - expect(ms[0].bilibili).to.equal('bl'); - expect(ms[0].guruguru).to.equal('gu'); - }); - }); - }); - }); - }); - }); - }); - - if (dialect.match(/^postgres/)) { - it('converts Infinity in where clause to a timestamp', function() { - const Date = this.sequelize.define('Date', - { - date: { - type: DataTypes.DATE, - primaryKey: true - }, - deletedAt: { - type: DataTypes.DATE, - defaultValue: Infinity - } - }, - { paranoid: true }); - - return this.sequelize.sync({ force: true }) - .then(() => { - return Date.build({ date: Infinity }) - .save() - .then(date => { - return date.destroy(); - }); - }); - }); - } - }); - describe('isSoftDeleted', () => { beforeEach(function() { this.ParanoidUser = this.sequelize.define('ParanoidUser', { diff --git a/test/integration/instance/decrement.test.js b/test/integration/instance/decrement.test.js new file mode 100644 index 000000000000..185b99aecc13 --- /dev/null +++ b/test/integration/instance/decrement.test.js @@ -0,0 +1,222 @@ +'use strict'; + +const chai = require('chai'), + expect = chai.expect, + Sequelize = require('../../index'), + Support = require('./support'), + DataTypes = require('../../lib/data-types'), + sinon = require('sinon'), + current = Support.sequelize; + +describe(Support.getTestDialectTeaser('Instance'), () => { + before(function() { + this.clock = sinon.useFakeTimers(); + }); + + afterEach(function() { + this.clock.reset(); + }); + + after(function() { + this.clock.restore(); + }); + + beforeEach(function() { + this.User = this.sequelize.define('User', { + username: { type: DataTypes.STRING }, + uuidv1: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV1 }, + uuidv4: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4 }, + touchedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + aNumber: { type: DataTypes.INTEGER }, + bNumber: { type: DataTypes.INTEGER }, + aDate: { type: DataTypes.DATE }, + + validateTest: { + type: DataTypes.INTEGER, + allowNull: true, + validate: { isInt: true } + }, + validateCustom: { + type: DataTypes.STRING, + allowNull: true, + validate: { len: { msg: 'Length failed.', args: [1, 20] } } + }, + + dateAllowNullTrue: { + type: DataTypes.DATE, + allowNull: true + }, + + isSuperUser: { + type: DataTypes.BOOLEAN, + defaultValue: false + } + }); + + return this.User.sync({ force: true }); + }); + + describe('decrement', () => { + beforeEach(function() { + return this.User.create({ id: 1, aNumber: 0, bNumber: 0 }); + }); + + if (current.dialect.supports.transactions) { + it('supports transactions', function() { + return Support.prepareTransactionTest(this.sequelize).then(sequelize => { + const User = sequelize.define('User', { number: Support.Sequelize.INTEGER }); + + return User.sync({ force: true }).then(() => { + return User.create({ number: 3 }).then(user => { + return sequelize.transaction().then(t => { + return user.decrement('number', { by: 2, transaction: t }).then(() => { + return User.findAll().then(users1 => { + return User.findAll({ transaction: t }).then(users2 => { + expect(users1[0].number).to.equal(3); + expect(users2[0].number).to.equal(1); + return t.rollback(); + }); + }); + }); + }); + }); + }); + }); + }); + } + + if (current.dialect.supports.returnValues.returning) { + it('supports returning', function() { + return this.User.findByPk(1).then(user1 => { + return user1.decrement('aNumber', { by: 2 }).then(() => { + expect(user1.aNumber).to.be.equal(-2); + return user1.decrement('bNumber', { by: 2, returning: false }).then(user3 => { + expect(user3.bNumber).to.be.equal(0); + }); + }); + }); + }); + } + + it('with array', function() { + return this.User.findByPk(1).then(user1 => { + return user1.decrement(['aNumber'], { by: 2 }).then(() => { + return this.User.findByPk(1).then(user3 => { + expect(user3.aNumber).to.be.equal(-2); + }); + }); + }); + }); + + it('with single field', function() { + return this.User.findByPk(1).then(user1 => { + return user1.decrement('aNumber', { by: 2 }).then(() => { + return this.User.findByPk(1).then(user3 => { + expect(user3.aNumber).to.be.equal(-2); + }); + }); + }); + }); + + it('with single field and no value', function() { + return this.User.findByPk(1).then(user1 => { + return user1.decrement('aNumber').then(() => { + return this.User.findByPk(1).then(user2 => { + expect(user2.aNumber).to.be.equal(-1); + }); + }); + }); + }); + + it('should still work right with other concurrent updates', function() { + return this.User.findByPk(1).then(user1 => { + // Select the user again (simulating a concurrent query) + return this.User.findByPk(1).then(user2 => { + return user2.update({ + aNumber: user2.aNumber + 1 + }).then(() => { + return user1.decrement(['aNumber'], { by: 2 }).then(() => { + return this.User.findByPk(1).then(user5 => { + expect(user5.aNumber).to.be.equal(-1); + }); + }); + }); + }); + }); + }); + + it('should still work right with other concurrent increments', function() { + return this.User.findByPk(1).then(user1 => { + return Sequelize.Promise.all([ + user1.decrement(['aNumber'], { by: 2 }), + user1.decrement(['aNumber'], { by: 2 }), + user1.decrement(['aNumber'], { by: 2 }) + ]).then(() => { + return this.User.findByPk(1).then(user2 => { + expect(user2.aNumber).to.equal(-6); + }); + }); + }); + }); + + it('with key value pair', function() { + return this.User.findByPk(1).then(user1 => { + return user1.decrement({ 'aNumber': 1, 'bNumber': 2 }).then(() => { + return this.User.findByPk(1).then(user3 => { + expect(user3.aNumber).to.be.equal(-1); + expect(user3.bNumber).to.be.equal(-2); + }); + }); + }); + }); + + it('with negative value', function() { + return this.User.findByPk(1).then(user1 => { + return Sequelize.Promise.all([ + user1.decrement('aNumber', { by: -2 }), + user1.decrement(['aNumber', 'bNumber'], { by: -2 }), + user1.decrement({ 'aNumber': -1, 'bNumber': -2 }) + ]).then(() => { + return this.User.findByPk(1).then(user3 => { + expect(user3.aNumber).to.be.equal(+5); + expect(user3.bNumber).to.be.equal(+4); + }); + }); + }); + }); + + it('with timestamps set to true', function() { + const User = this.sequelize.define('IncrementUser', { + aNumber: DataTypes.INTEGER + }, { timestamps: true }); + let oldDate; + + return User.sync({ force: true }).then(() => { + return User.create({ aNumber: 1 }); + }).then(user => { + oldDate = user.updatedAt; + this.clock.tick(1000); + return user.decrement('aNumber', { by: 1 }); + }).then(() => { + return expect(User.findByPk(1)).to.eventually.have.property('updatedAt').afterTime(oldDate); + }); + }); + + it('with timestamps set to true and options.silent set to true', function() { + const User = this.sequelize.define('IncrementUser', { + aNumber: DataTypes.INTEGER + }, { timestamps: true }); + let oldDate; + + return User.sync({ force: true }).then(() => { + return User.create({ aNumber: 1 }); + }).then(user => { + oldDate = user.updatedAt; + this.clock.tick(1000); + return user.decrement('aNumber', { by: 1, silent: true }); + }).then(() => { + return expect(User.findByPk(1)).to.eventually.have.property('updatedAt').equalTime(oldDate); + }); + }); + }); +}); diff --git a/test/integration/instance/destroy.test.js b/test/integration/instance/destroy.test.js new file mode 100644 index 000000000000..8cae507704d5 --- /dev/null +++ b/test/integration/instance/destroy.test.js @@ -0,0 +1,164 @@ +'use strict'; + +const chai = require('chai'), + expect = chai.expect, + Support = require('../support'), + DataTypes = require('../../../lib/data-types'), + dialect = Support.getTestDialect(), + current = Support.sequelize; + +describe(Support.getTestDialectTeaser('Instance'), () => { + describe('destroy', () => { + if (current.dialect.supports.transactions) { + it('supports transactions', function() { + return Support.prepareTransactionTest(this.sequelize).then(sequelize => { + const User = sequelize.define('User', { username: Support.Sequelize.STRING }); + + return User.sync({ force: true }).then(() => { + return User.create({ username: 'foo' }).then(user => { + return sequelize.transaction().then(t => { + return user.destroy({ transaction: t }).then(() => { + return User.count().then(count1 => { + return User.count({ transaction: t }).then(count2 => { + expect(count1).to.equal(1); + expect(count2).to.equal(0); + return t.rollback(); + }); + }); + }); + }); + }); + }); + }); + }); + } + + it('does not set the deletedAt date in subsequent destroys if dao is paranoid', function() { + const UserDestroy = this.sequelize.define('UserDestroy', { + name: Support.Sequelize.STRING, + bio: Support.Sequelize.TEXT + }, { paranoid: true }); + + return UserDestroy.sync({ force: true }).then(() => { + return UserDestroy.create({ name: 'hallo', bio: 'welt' }).then(user => { + return user.destroy().then(() => { + return user.reload({ paranoid: false }).then(() => { + const deletedAt = user.deletedAt; + + return user.destroy().then(() => { + return user.reload({ paranoid: false }).then(() => { + expect(user.deletedAt).to.eql(deletedAt); + }); + }); + }); + }); + }); + }); + }); + + it('deletes a record from the database if dao is not paranoid', function() { + const UserDestroy = this.sequelize.define('UserDestroy', { + name: Support.Sequelize.STRING, + bio: Support.Sequelize.TEXT + }); + + return UserDestroy.sync({ force: true }).then(() => { + return UserDestroy.create({ name: 'hallo', bio: 'welt' }).then(u => { + return UserDestroy.findAll().then(users => { + expect(users.length).to.equal(1); + return u.destroy().then(() => { + return UserDestroy.findAll().then(users => { + expect(users.length).to.equal(0); + }); + }); + }); + }); + }); + }); + + it('allows sql logging of delete statements', function() { + const UserDelete = this.sequelize.define('UserDelete', { + name: Support.Sequelize.STRING, + bio: Support.Sequelize.TEXT + }); + + return UserDelete.sync({ force: true }).then(() => { + return UserDelete.create({ name: 'hallo', bio: 'welt' }).then(u => { + return UserDelete.findAll().then(users => { + expect(users.length).to.equal(1); + return u.destroy({ + logging(sql) { + expect(sql).to.exist; + expect(sql.toUpperCase()).to.include('DELETE'); + } + }); + }); + }); + }); + }); + + it('delete a record of multiple primary keys table', function() { + const MultiPrimary = this.sequelize.define('MultiPrimary', { + bilibili: { + type: Support.Sequelize.CHAR(2), + primaryKey: true + }, + + guruguru: { + type: Support.Sequelize.CHAR(2), + primaryKey: true + } + }); + + return MultiPrimary.sync({ force: true }).then(() => { + return MultiPrimary.create({ bilibili: 'bl', guruguru: 'gu' }).then(() => { + return MultiPrimary.create({ bilibili: 'bl', guruguru: 'ru' }).then(m2 => { + return MultiPrimary.findAll().then(ms => { + expect(ms.length).to.equal(2); + return m2.destroy({ + logging(sql) { + expect(sql).to.exist; + expect(sql.toUpperCase()).to.include('DELETE'); + expect(sql).to.include('ru'); + expect(sql).to.include('bl'); + } + }).then(() => { + return MultiPrimary.findAll().then(ms => { + expect(ms.length).to.equal(1); + expect(ms[0].bilibili).to.equal('bl'); + expect(ms[0].guruguru).to.equal('gu'); + }); + }); + }); + }); + }); + }); + }); + + if (dialect.match(/^postgres/)) { + it('converts Infinity in where clause to a timestamp', function() { + const Date = this.sequelize.define('Date', + { + date: { + type: DataTypes.DATE, + primaryKey: true + }, + deletedAt: { + type: DataTypes.DATE, + defaultValue: Infinity + } + }, + { paranoid: true }); + + return this.sequelize.sync({ force: true }) + .then(() => { + return Date.build({ date: Infinity }) + .save() + .then(date => { + return date.destroy(); + }); + }); + }); + } + }); +}); diff --git a/test/integration/instance/increment.test.js b/test/integration/instance/increment.test.js new file mode 100644 index 000000000000..7518b47a31b0 --- /dev/null +++ b/test/integration/instance/increment.test.js @@ -0,0 +1,221 @@ +'use strict'; + +const chai = require('chai'), + expect = chai.expect, + Sequelize = require('../../index'), + Support = require('./support'), + DataTypes = require('../../lib/data-types'), + sinon = require('sinon'), + current = Support.sequelize; + +describe(Support.getTestDialectTeaser('Instance'), () => { + before(function() { + this.clock = sinon.useFakeTimers(); + }); + + afterEach(function() { + this.clock.reset(); + }); + + after(function() { + this.clock.restore(); + }); + + beforeEach(function() { + this.User = this.sequelize.define('User', { + username: { type: DataTypes.STRING }, + uuidv1: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV1 }, + uuidv4: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4 }, + touchedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + aNumber: { type: DataTypes.INTEGER }, + bNumber: { type: DataTypes.INTEGER }, + aDate: { type: DataTypes.DATE }, + + validateTest: { + type: DataTypes.INTEGER, + allowNull: true, + validate: { isInt: true } + }, + validateCustom: { + type: DataTypes.STRING, + allowNull: true, + validate: { len: { msg: 'Length failed.', args: [1, 20] } } + }, + + dateAllowNullTrue: { + type: DataTypes.DATE, + allowNull: true + }, + + isSuperUser: { + type: DataTypes.BOOLEAN, + defaultValue: false + } + }); + + return this.User.sync({ force: true }); + }); + + describe('increment', () => { + beforeEach(function() { + return this.User.create({ id: 1, aNumber: 0, bNumber: 0 }); + }); + + if (current.dialect.supports.transactions) { + it('supports transactions', function() { + return Support.prepareTransactionTest(this.sequelize).then(sequelize => { + const User = sequelize.define('User', { number: Support.Sequelize.INTEGER }); + + return User.sync({ force: true }).then(() => { + return User.create({ number: 1 }).then(user => { + return sequelize.transaction().then(t => { + return user.increment('number', { by: 2, transaction: t }).then(() => { + return User.findAll().then(users1 => { + return User.findAll({ transaction: t }).then(users2 => { + expect(users1[0].number).to.equal(1); + expect(users2[0].number).to.equal(3); + return t.rollback(); + }); + }); + }); + }); + }); + }); + }); + }); + } + + if (current.dialect.supports.returnValues.returning) { + it('supports returning', function() { + return this.User.findByPk(1).then(user1 => { + return user1.increment('aNumber', { by: 2 }).then(() => { + expect(user1.aNumber).to.be.equal(2); + return user1.increment('bNumber', { by: 2, returning: false }).then(user3 => { + expect(user3.bNumber).to.be.equal(0); + }); + }); + }); + }); + } + + it('supports where conditions', function() { + return this.User.findByPk(1).then(user1 => { + return user1.increment(['aNumber'], { by: 2, where: { bNumber: 1 } }).then(() => { + return this.User.findByPk(1).then(user3 => { + expect(user3.aNumber).to.be.equal(0); + }); + }); + }); + }); + + it('with array', function() { + return this.User.findByPk(1).then(user1 => { + return user1.increment(['aNumber'], { by: 2 }).then(() => { + return this.User.findByPk(1).then(user3 => { + expect(user3.aNumber).to.be.equal(2); + }); + }); + }); + }); + + it('with single field', function() { + return this.User.findByPk(1).then(user1 => { + return user1.increment('aNumber', { by: 2 }).then(() => { + return this.User.findByPk(1).then(user3 => { + expect(user3.aNumber).to.be.equal(2); + }); + }); + }); + }); + + it('with single field and no value', function() { + return this.User.findByPk(1).then(user1 => { + return user1.increment('aNumber').then(() => { + return this.User.findByPk(1).then(user2 => { + expect(user2.aNumber).to.be.equal(1); + }); + }); + }); + }); + + it('should still work right with other concurrent updates', function() { + return this.User.findByPk(1).then(user1 => { + // Select the user again (simulating a concurrent query) + return this.User.findByPk(1).then(user2 => { + return user2.update({ + aNumber: user2.aNumber + 1 + }).then(() => { + return user1.increment(['aNumber'], { by: 2 }).then(() => { + return this.User.findByPk(1).then(user5 => { + expect(user5.aNumber).to.be.equal(3); + }); + }); + }); + }); + }); + }); + + it('should still work right with other concurrent increments', function() { + return this.User.findByPk(1).then(user1 => { + return Sequelize.Promise.all([ + user1.increment(['aNumber'], { by: 2 }), + user1.increment(['aNumber'], { by: 2 }), + user1.increment(['aNumber'], { by: 2 }) + ]).then(() => { + return this.User.findByPk(1).then(user2 => { + expect(user2.aNumber).to.equal(6); + }); + }); + }); + }); + + it('with key value pair', function() { + return this.User.findByPk(1).then(user1 => { + return user1.increment({ 'aNumber': 1, 'bNumber': 2 }).then(() => { + return this.User.findByPk(1).then(user3 => { + expect(user3.aNumber).to.be.equal(1); + expect(user3.bNumber).to.be.equal(2); + }); + }); + }); + }); + + it('with timestamps set to true', function() { + const User = this.sequelize.define('IncrementUser', { + aNumber: DataTypes.INTEGER + }, { timestamps: true }); + + let oldDate; + + return User.sync({ force: true }) + .then(() => User.create({ aNumber: 1 })) + .then(user => { + oldDate = user.get('updatedAt'); + + this.clock.tick(1000); + return user.increment('aNumber', { by: 1 }); + }) + .then(user => user.reload()) + .then(user => { + return expect(user).to.have.property('updatedAt').afterTime(oldDate); + }); + }); + + it('with timestamps set to true and options.silent set to true', function() { + const User = this.sequelize.define('IncrementUser', { + aNumber: DataTypes.INTEGER + }, { timestamps: true }); + let oldDate; + + return User.sync({ force: true }).then(() => { + return User.create({ aNumber: 1 }); + }).then(user => { + oldDate = user.updatedAt; + this.clock.tick(1000); + return user.increment('aNumber', { by: 1, silent: true }); + }).then(() => { + return expect(User.findByPk(1)).to.eventually.have.property('updatedAt').equalTime(oldDate); + }); + }); + }); +}); diff --git a/test/integration/instance/reload.test.js b/test/integration/instance/reload.test.js new file mode 100644 index 000000000000..d330aa25401b --- /dev/null +++ b/test/integration/instance/reload.test.js @@ -0,0 +1,308 @@ +'use strict'; + +const chai = require('chai'), + expect = chai.expect, + Sequelize = require('../../index'), + Support = require('./support'), + DataTypes = require('../../lib/data-types'), + current = Support.sequelize; + +describe(Support.getTestDialectTeaser('Instance'), () => { + beforeEach(function() { + this.User = this.sequelize.define('User', { + username: { type: DataTypes.STRING }, + uuidv1: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV1 }, + uuidv4: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4 }, + touchedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + aNumber: { type: DataTypes.INTEGER }, + bNumber: { type: DataTypes.INTEGER }, + aDate: { type: DataTypes.DATE }, + + validateTest: { + type: DataTypes.INTEGER, + allowNull: true, + validate: { isInt: true } + }, + validateCustom: { + type: DataTypes.STRING, + allowNull: true, + validate: { len: { msg: 'Length failed.', args: [1, 20] } } + }, + + dateAllowNullTrue: { + type: DataTypes.DATE, + allowNull: true + }, + + isSuperUser: { + type: DataTypes.BOOLEAN, + defaultValue: false + } + }); + + return this.User.sync({ force: true }); + }); + + describe('reload', () => { + if (current.dialect.supports.transactions) { + it('supports transactions', function() { + return Support.prepareTransactionTest(this.sequelize).then(sequelize => { + const User = sequelize.define('User', { username: Support.Sequelize.STRING }); + + return User.sync({ force: true }).then(() => { + return User.create({ username: 'foo' }).then(user => { + return sequelize.transaction().then(t => { + return User.update({ username: 'bar' }, { where: { username: 'foo' }, transaction: t }).then(() => { + return user.reload().then(user => { + expect(user.username).to.equal('foo'); + return user.reload({ transaction: t }).then(user => { + expect(user.username).to.equal('bar'); + return t.rollback(); + }); + }); + }); + }); + }); + }); + }); + }); + } + + it('should return a reference to the same DAO instead of creating a new one', function() { + return this.User.create({ username: 'John Doe' }).then(originalUser => { + return originalUser.update({ username: 'Doe John' }).then(() => { + return originalUser.reload().then(updatedUser => { + expect(originalUser === updatedUser).to.be.true; + }); + }); + }); + }); + + it('should update the values on all references to the DAO', function() { + return this.User.create({ username: 'John Doe' }).then(originalUser => { + return this.User.findByPk(originalUser.id).then(updater => { + return updater.update({ username: 'Doe John' }).then(() => { + // We used a different reference when calling update, so originalUser is now out of sync + expect(originalUser.username).to.equal('John Doe'); + return originalUser.reload().then(updatedUser => { + expect(originalUser.username).to.equal('Doe John'); + expect(updatedUser.username).to.equal('Doe John'); + }); + }); + }); + }); + }); + + it('should support updating a subset of attributes', function() { + return this.User.create({ + aNumber: 1, + bNumber: 1 + }).tap(user => { + return this.User.update({ + bNumber: 2 + }, { + where: { + id: user.get('id') + } + }); + }).then(user => { + return user.reload({ + attributes: ['bNumber'] + }); + }).then(user => { + expect(user.get('aNumber')).to.equal(1); + expect(user.get('bNumber')).to.equal(2); + }); + }); + + it('should update read only attributes as well (updatedAt)', function() { + return this.User.create({ username: 'John Doe' }).then(originalUser => { + this.originallyUpdatedAt = originalUser.updatedAt; + this.originalUser = originalUser; + + // Wait for a second, so updatedAt will actually be different + this.clock.tick(1000); + return this.User.findByPk(originalUser.id); + }).then(updater => { + return updater.update({ username: 'Doe John' }); + }).then(updatedUser => { + this.updatedUser = updatedUser; + return this.originalUser.reload(); + }).then(() => { + expect(this.originalUser.updatedAt).to.be.above(this.originallyUpdatedAt); + expect(this.updatedUser.updatedAt).to.be.above(this.originallyUpdatedAt); + }); + }); + + it('should update the associations as well', function() { + const Book = this.sequelize.define('Book', { title: DataTypes.STRING }), + Page = this.sequelize.define('Page', { content: DataTypes.TEXT }); + + Book.hasMany(Page); + Page.belongsTo(Book); + + return Book.sync({ force: true }).then(() => { + return Page.sync({ force: true }).then(() => { + return Book.create({ title: 'A very old book' }).then(book => { + return Page.create({ content: 'om nom nom' }).then(page => { + return book.setPages([page]).then(() => { + return Book.findOne({ + where: { id: book.id }, + include: [Page] + }).then(leBook => { + return page.update({ content: 'something totally different' }).then(page => { + expect(leBook.Pages.length).to.equal(1); + expect(leBook.Pages[0].content).to.equal('om nom nom'); + expect(page.content).to.equal('something totally different'); + return leBook.reload().then(leBook => { + expect(leBook.Pages.length).to.equal(1); + expect(leBook.Pages[0].content).to.equal('something totally different'); + expect(page.content).to.equal('something totally different'); + }); + }); + }); + }); + }); + }); + }); + }); + }); + + it('should update internal options of the instance', function() { + const Book = this.sequelize.define('Book', { title: DataTypes.STRING }), + Page = this.sequelize.define('Page', { content: DataTypes.TEXT }); + + Book.hasMany(Page); + Page.belongsTo(Book); + + return Book.sync({ force: true }).then(() => { + return Page.sync({ force: true }).then(() => { + return Book.create({ title: 'A very old book' }).then(book => { + return Page.create().then(page => { + return book.setPages([page]).then(() => { + return Book.findOne({ + where: { id: book.id } + }).then(leBook => { + const oldOptions = leBook._options; + return leBook.reload({ + include: [Page] + }).then(leBook => { + expect(oldOptions).not.to.equal(leBook._options); + expect(leBook._options.include.length).to.equal(1); + expect(leBook.Pages.length).to.equal(1); + expect(leBook.get({ plain: true }).Pages.length).to.equal(1); + }); + }); + }); + }); + }); + }); + }); + }); + + it('should return an error when reload fails', function() { + return this.User.create({ username: 'John Doe' }).then(user => { + return user.destroy().then(() => { + return expect(user.reload()).to.be.rejectedWith( + Sequelize.InstanceError, + 'Instance could not be reloaded because it does not exist anymore (find call returned null)' + ); + }); + }); + }); + + it('should set an association to null after deletion, 1-1', function() { + const Shoe = this.sequelize.define('Shoe', { brand: DataTypes.STRING }), + Player = this.sequelize.define('Player', { name: DataTypes.STRING }); + + Player.hasOne(Shoe); + Shoe.belongsTo(Player); + + return this.sequelize.sync({ force: true }).then(() => { + return Shoe.create({ + brand: 'the brand', + Player: { + name: 'the player' + } + }, { include: [Player] }); + }).then(shoe => { + return Player.findOne({ + where: { id: shoe.Player.id }, + include: [Shoe] + }).then(lePlayer => { + expect(lePlayer.Shoe).not.to.be.null; + return lePlayer.Shoe.destroy().return(lePlayer); + }).then(lePlayer => { + return lePlayer.reload(); + }).then(lePlayer => { + expect(lePlayer.Shoe).to.be.null; + }); + }); + }); + + it('should set an association to empty after all deletion, 1-N', function() { + const Team = this.sequelize.define('Team', { name: DataTypes.STRING }), + Player = this.sequelize.define('Player', { name: DataTypes.STRING }); + + Team.hasMany(Player); + Player.belongsTo(Team); + + return this.sequelize.sync({ force: true }).then(() => { + return Team.create({ + name: 'the team', + Players: [{ + name: 'the player1' + }, { + name: 'the player2' + }] + }, { include: [Player] }); + }).then(team => { + return Team.findOne({ + where: { id: team.id }, + include: [Player] + }).then(leTeam => { + expect(leTeam.Players).not.to.be.empty; + return leTeam.Players[1].destroy().then(() => { + return leTeam.Players[0].destroy(); + }).return(leTeam); + }).then(leTeam => { + return leTeam.reload(); + }).then(leTeam => { + expect(leTeam.Players).to.be.empty; + }); + }); + }); + + it('should update the associations after one element deleted', function() { + const Team = this.sequelize.define('Team', { name: DataTypes.STRING }), + Player = this.sequelize.define('Player', { name: DataTypes.STRING }); + + Team.hasMany(Player); + Player.belongsTo(Team); + + + return this.sequelize.sync({ force: true }).then(() => { + return Team.create({ + name: 'the team', + Players: [{ + name: 'the player1' + }, { + name: 'the player2' + }] + }, { include: [Player] }); + }).then(team => { + return Team.findOne({ + where: { id: team.id }, + include: [Player] + }).then(leTeam => { + expect(leTeam.Players).to.have.length(2); + return leTeam.Players[0].destroy().return(leTeam); + }).then(leTeam => { + return leTeam.reload(); + }).then(leTeam => { + expect(leTeam.Players).to.have.length(1); + }); + }); + }); + }); +}); diff --git a/test/integration/instance/save.test.js b/test/integration/instance/save.test.js new file mode 100644 index 000000000000..da19ea837a1c --- /dev/null +++ b/test/integration/instance/save.test.js @@ -0,0 +1,695 @@ +'use strict'; + +const chai = require('chai'), + expect = chai.expect, + Sequelize = require('../../index'), + Support = require('./support'), + DataTypes = require('../../lib/data-types'), + config = require('../config/config'), + sinon = require('sinon'), + current = Support.sequelize; + +describe(Support.getTestDialectTeaser('Instance'), () => { + before(function() { + this.clock = sinon.useFakeTimers(); + }); + + afterEach(function() { + this.clock.reset(); + }); + + after(function() { + this.clock.restore(); + }); + + beforeEach(function() { + this.User = this.sequelize.define('User', { + username: { type: DataTypes.STRING }, + uuidv1: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV1 }, + uuidv4: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4 }, + touchedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + aNumber: { type: DataTypes.INTEGER }, + bNumber: { type: DataTypes.INTEGER }, + aDate: { type: DataTypes.DATE }, + + validateTest: { + type: DataTypes.INTEGER, + allowNull: true, + validate: { isInt: true } + }, + validateCustom: { + type: DataTypes.STRING, + allowNull: true, + validate: { len: { msg: 'Length failed.', args: [1, 20] } } + }, + + dateAllowNullTrue: { + type: DataTypes.DATE, + allowNull: true + }, + + isSuperUser: { + type: DataTypes.BOOLEAN, + defaultValue: false + } + }); + + return this.User.sync({ force: true }); + }); + + describe('save', () => { + if (current.dialect.supports.transactions) { + it('supports transactions', function() { + return Support.prepareTransactionTest(this.sequelize).then(sequelize => { + const User = sequelize.define('User', { username: Support.Sequelize.STRING }); + return User.sync({ force: true }).then(() => { + return sequelize.transaction().then(t => { + return User.build({ username: 'foo' }).save({ transaction: t }).then(() => { + return User.count().then(count1 => { + return User.count({ transaction: t }).then(count2 => { + expect(count1).to.equal(0); + expect(count2).to.equal(1); + return t.rollback(); + }); + }); + }); + }); + }); + }); + }); + } + + it('only updates fields in passed array', function() { + const date = new Date(1990, 1, 1); + + return this.User.create({ + username: 'foo', + touchedAt: new Date() + }).then(user => { + user.username = 'fizz'; + user.touchedAt = date; + + return user.save({ fields: ['username'] }).then(() => { + // re-select user + return this.User.findByPk(user.id).then(user2 => { + // name should have changed + expect(user2.username).to.equal('fizz'); + // bio should be unchanged + expect(user2.birthDate).not.to.equal(date); + }); + }); + }); + }); + + it('should work on a model with an attribute named length', function() { + const Box = this.sequelize.define('box', { + length: DataTypes.INTEGER, + width: DataTypes.INTEGER, + height: DataTypes.INTEGER + }); + + return Box.sync({ force: true }).then(() => { + return Box.create({ + length: 1, + width: 2, + height: 3 + }).then(box => { + return box.update({ + length: 4, + width: 5, + height: 6 + }); + }).then(() => { + return Box.findOne({}).then(box => { + expect(box.get('length')).to.equal(4); + expect(box.get('width')).to.equal(5); + expect(box.get('height')).to.equal(6); + }); + }); + }); + }); + + it('only validates fields in passed array', function() { + return this.User.build({ + validateTest: 'cake', // invalid, but not saved + validateCustom: '1' + }).save({ + fields: ['validateCustom'] + }); + }); + + describe('hooks', () => { + it('should update attributes added in hooks when default fields are used', function() { + const User = this.sequelize.define(`User${config.rand()}`, { + name: DataTypes.STRING, + bio: DataTypes.TEXT, + email: DataTypes.STRING + }); + + User.beforeUpdate(instance => { + instance.set('email', 'B'); + }); + + return User.sync({ force: true }).then(() => { + return User.create({ + name: 'A', + bio: 'A', + email: 'A' + }).then(user => { + return user.set({ + name: 'B', + bio: 'B' + }).save(); + }).then(() => { + return User.findOne({}); + }).then(user => { + expect(user.get('name')).to.equal('B'); + expect(user.get('bio')).to.equal('B'); + expect(user.get('email')).to.equal('B'); + }); + }); + }); + + it('should update attributes changed in hooks when default fields are used', function() { + const User = this.sequelize.define(`User${config.rand()}`, { + name: DataTypes.STRING, + bio: DataTypes.TEXT, + email: DataTypes.STRING + }); + + User.beforeUpdate(instance => { + instance.set('email', 'C'); + }); + + return User.sync({ force: true }).then(() => { + return User.create({ + name: 'A', + bio: 'A', + email: 'A' + }).then(user => { + return user.set({ + name: 'B', + bio: 'B', + email: 'B' + }).save(); + }).then(() => { + return User.findOne({}); + }).then(user => { + expect(user.get('name')).to.equal('B'); + expect(user.get('bio')).to.equal('B'); + expect(user.get('email')).to.equal('C'); + }); + }); + }); + + it('should validate attributes added in hooks when default fields are used', function() { + const User = this.sequelize.define(`User${config.rand()}`, { + name: DataTypes.STRING, + bio: DataTypes.TEXT, + email: { + type: DataTypes.STRING, + validate: { + isEmail: true + } + } + }); + + User.beforeUpdate(instance => { + instance.set('email', 'B'); + }); + + return User.sync({ force: true }).then(() => { + return User.create({ + name: 'A', + bio: 'A', + email: 'valid.email@gmail.com' + }).then(user => { + return expect(user.set({ + name: 'B' + }).save()).to.be.rejectedWith(Sequelize.ValidationError); + }).then(() => { + return User.findOne({}).then(user => { + expect(user.get('email')).to.equal('valid.email@gmail.com'); + }); + }); + }); + }); + + it('should validate attributes changed in hooks when default fields are used', function() { + const User = this.sequelize.define(`User${config.rand()}`, { + name: DataTypes.STRING, + bio: DataTypes.TEXT, + email: { + type: DataTypes.STRING, + validate: { + isEmail: true + } + } + }); + + User.beforeUpdate(instance => { + instance.set('email', 'B'); + }); + + return User.sync({ force: true }).then(() => { + return User.create({ + name: 'A', + bio: 'A', + email: 'valid.email@gmail.com' + }).then(user => { + return expect(user.set({ + name: 'B', + email: 'still.valid.email@gmail.com' + }).save()).to.be.rejectedWith(Sequelize.ValidationError); + }).then(() => { + return User.findOne({}).then(user => { + expect(user.get('email')).to.equal('valid.email@gmail.com'); + }); + }); + }); + }); + }); + + it('stores an entry in the database', function() { + const username = 'user', + User = this.User, + user = this.User.build({ + username, + touchedAt: new Date(1984, 8, 23) + }); + + return User.findAll().then(users => { + expect(users).to.have.length(0); + return user.save().then(() => { + return User.findAll().then(users => { + expect(users).to.have.length(1); + expect(users[0].username).to.equal(username); + expect(users[0].touchedAt).to.be.instanceof(Date); + expect(users[0].touchedAt).to.equalDate(new Date(1984, 8, 23)); + }); + }); + }); + }); + + it('handles an entry with primaryKey of zero', function() { + const username = 'user', + newUsername = 'newUser', + User2 = this.sequelize.define('User2', + { + id: { + type: DataTypes.INTEGER.UNSIGNED, + autoIncrement: false, + primaryKey: true + }, + username: { type: DataTypes.STRING } + }); + + return User2.sync().then(() => { + return User2.create({ id: 0, username }).then(user => { + expect(user).to.be.ok; + expect(user.id).to.equal(0); + expect(user.username).to.equal(username); + return User2.findByPk(0).then(user => { + expect(user).to.be.ok; + expect(user.id).to.equal(0); + expect(user.username).to.equal(username); + return user.update({ username: newUsername }).then(user => { + expect(user).to.be.ok; + expect(user.id).to.equal(0); + expect(user.username).to.equal(newUsername); + }); + }); + }); + }); + }); + + it('updates the timestamps', function() { + const now = new Date(); + now.setMilliseconds(0); + + const user = this.User.build({ username: 'user' }); + this.clock.tick(1000); + + return user.save().then(savedUser => { + expect(savedUser).have.property('updatedAt').afterTime(now); + + this.clock.tick(1000); + return savedUser.save(); + }).then(updatedUser => { + expect(updatedUser).have.property('updatedAt').afterTime(now); + }); + }); + + it('does not update timestamps when passing silent=true', function() { + return this.User.create({ username: 'user' }).then(user => { + const updatedAt = user.updatedAt; + + this.clock.tick(1000); + return expect(user.update({ + username: 'userman' + }, { + silent: true + })).to.eventually.have.property('updatedAt').equalTime(updatedAt); + }); + }); + + it('does not update timestamps when passing silent=true in a bulk update', function() { + const data = [ + { username: 'Paul' }, + { username: 'Peter' } + ]; + let updatedAtPeter, + updatedAtPaul; + + return this.User.bulkCreate(data).then(() => { + return this.User.findAll(); + }).then(users => { + updatedAtPaul = users[0].updatedAt; + updatedAtPeter = users[1].updatedAt; + }) + .then(() => { + this.clock.tick(150); + return this.User.update( + { aNumber: 1 }, + { where: {}, silent: true } + ); + }).then(() => { + return this.User.findAll(); + }).then(users => { + expect(users[0].updatedAt).to.equalTime(updatedAtPeter); + expect(users[1].updatedAt).to.equalTime(updatedAtPaul); + }); + }); + + describe('when nothing changed', () => { + it('does not update timestamps', function() { + return this.User.create({ username: 'John' }).then(() => { + return this.User.findOne({ where: { username: 'John' } }).then(user => { + const updatedAt = user.updatedAt; + this.clock.tick(2000); + return user.save().then(newlySavedUser => { + expect(newlySavedUser.updatedAt).to.equalTime(updatedAt); + return this.User.findOne({ where: { username: 'John' } }).then(newlySavedUser => { + expect(newlySavedUser.updatedAt).to.equalTime(updatedAt); + }); + }); + }); + }); + }); + + it('should not throw ER_EMPTY_QUERY if changed only virtual fields', function() { + const User = this.sequelize.define(`User${config.rand()}`, { + name: DataTypes.STRING, + bio: { + type: DataTypes.VIRTUAL, + get: () => 'swag' + } + }, { + timestamps: false + }); + return User.sync({ force: true }).then(() => + User.create({ name: 'John', bio: 'swag 1' }).then(user => user.update({ bio: 'swag 2' }).should.be.fulfilled) + ); + }); + }); + + it('updates with function and column value', function() { + return this.User.create({ + aNumber: 42 + }).then(user => { + user.bNumber = this.sequelize.col('aNumber'); + user.username = this.sequelize.fn('upper', 'sequelize'); + return user.save().then(() => { + return this.User.findByPk(user.id).then(user2 => { + expect(user2.username).to.equal('SEQUELIZE'); + expect(user2.bNumber).to.equal(42); + }); + }); + }); + }); + + describe('without timestamps option', () => { + it("doesn't update the updatedAt column", function() { + const User2 = this.sequelize.define('User2', { + username: DataTypes.STRING, + updatedAt: DataTypes.DATE + }, { timestamps: false }); + return User2.sync().then(() => { + return User2.create({ username: 'john doe' }).then(johnDoe => { + // sqlite and mysql return undefined, whereas postgres returns null + expect([undefined, null]).to.include(johnDoe.updatedAt); + }); + }); + }); + }); + + describe('with custom timestamp options', () => { + it('updates the createdAt column if updatedAt is disabled', function() { + const now = new Date(); + this.clock.tick(1000); + + const User2 = this.sequelize.define('User2', { + username: DataTypes.STRING + }, { updatedAt: false }); + + return User2.sync().then(() => { + return User2.create({ username: 'john doe' }).then(johnDoe => { + expect(johnDoe.updatedAt).to.be.undefined; + expect(now).to.be.beforeTime(johnDoe.createdAt); + }); + }); + }); + + it('updates the updatedAt column if createdAt is disabled', function() { + const now = new Date(); + this.clock.tick(1000); + + const User2 = this.sequelize.define('User2', { + username: DataTypes.STRING + }, { createdAt: false }); + + return User2.sync().then(() => { + return User2.create({ username: 'john doe' }).then(johnDoe => { + expect(johnDoe.createdAt).to.be.undefined; + expect(now).to.be.beforeTime(johnDoe.updatedAt); + }); + }); + }); + + it('works with `allowNull: false` on createdAt and updatedAt columns', function() { + const User2 = this.sequelize.define('User2', { + username: DataTypes.STRING, + createdAt: { + type: DataTypes.DATE, + allowNull: false + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false + } + }, { timestamps: true }); + + return User2.sync().then(() => { + return User2.create({ username: 'john doe' }).then(johnDoe => { + expect(johnDoe.createdAt).to.be.an.instanceof(Date); + expect( ! isNaN(johnDoe.createdAt.valueOf()) ).to.be.ok; + expect(johnDoe.createdAt).to.equalTime(johnDoe.updatedAt); + }); + }); + }); + }); + + it('should fail a validation upon creating', function() { + return this.User.create({ aNumber: 0, validateTest: 'hello' }).catch(err => { + expect(err).to.exist; + expect(err).to.be.instanceof(Object); + expect(err.get('validateTest')).to.be.instanceof(Array); + expect(err.get('validateTest')[0]).to.exist; + expect(err.get('validateTest')[0].message).to.equal('Validation isInt on validateTest failed'); + }); + }); + + it('should fail a validation upon creating with hooks false', function() { + return this.User.create({ aNumber: 0, validateTest: 'hello' }, { hooks: false }).catch(err => { + expect(err).to.exist; + expect(err).to.be.instanceof(Object); + expect(err.get('validateTest')).to.be.instanceof(Array); + expect(err.get('validateTest')[0]).to.exist; + expect(err.get('validateTest')[0].message).to.equal('Validation isInt on validateTest failed'); + }); + }); + + it('should fail a validation upon building', function() { + return this.User.build({ aNumber: 0, validateCustom: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' }).save() + .catch(err => { + expect(err).to.exist; + expect(err).to.be.instanceof(Object); + expect(err.get('validateCustom')).to.exist; + expect(err.get('validateCustom')).to.be.instanceof(Array); + expect(err.get('validateCustom')[0]).to.exist; + expect(err.get('validateCustom')[0].message).to.equal('Length failed.'); + }); + }); + + it('should fail a validation when updating', function() { + return this.User.create({ aNumber: 0 }).then(user => { + return user.update({ validateTest: 'hello' }).catch(err => { + expect(err).to.exist; + expect(err).to.be.instanceof(Object); + expect(err.get('validateTest')).to.exist; + expect(err.get('validateTest')).to.be.instanceof(Array); + expect(err.get('validateTest')[0]).to.exist; + expect(err.get('validateTest')[0].message).to.equal('Validation isInt on validateTest failed'); + }); + }); + }); + + it('takes zero into account', function() { + return this.User.build({ aNumber: 0 }).save({ + fields: ['aNumber'] + }).then(user => { + expect(user.aNumber).to.equal(0); + }); + }); + + it('saves a record with no primary key', function() { + const HistoryLog = this.sequelize.define('HistoryLog', { + someText: { type: DataTypes.STRING }, + aNumber: { type: DataTypes.INTEGER }, + aRandomId: { type: DataTypes.INTEGER } + }); + return HistoryLog.sync().then(() => { + return HistoryLog.create({ someText: 'Some random text', aNumber: 3, aRandomId: 5 }).then(log => { + return log.update({ aNumber: 5 }).then(newLog => { + expect(newLog.aNumber).to.equal(5); + }); + }); + }); + }); + + describe('eagerly loaded objects', () => { + beforeEach(function() { + this.UserEager = this.sequelize.define('UserEagerLoadingSaves', { + username: DataTypes.STRING, + age: DataTypes.INTEGER + }, { timestamps: false }); + + this.ProjectEager = this.sequelize.define('ProjectEagerLoadingSaves', { + title: DataTypes.STRING, + overdue_days: DataTypes.INTEGER + }, { timestamps: false }); + + this.UserEager.hasMany(this.ProjectEager, { as: 'Projects', foreignKey: 'PoobahId' }); + this.ProjectEager.belongsTo(this.UserEager, { as: 'Poobah', foreignKey: 'PoobahId' }); + + return this.UserEager.sync({ force: true }).then(() => { + return this.ProjectEager.sync({ force: true }); + }); + }); + + it('saves one object that has a collection of eagerly loaded objects', function() { + return this.UserEager.create({ username: 'joe', age: 1 }).then(user => { + return this.ProjectEager.create({ title: 'project-joe1', overdue_days: 0 }).then(project1 => { + return this.ProjectEager.create({ title: 'project-joe2', overdue_days: 0 }).then(project2 => { + return user.setProjects([project1, project2]).then(() => { + return this.UserEager.findOne({ where: { age: 1 }, include: [{ model: this.ProjectEager, as: 'Projects' }] }).then(user => { + expect(user.username).to.equal('joe'); + expect(user.age).to.equal(1); + expect(user.Projects).to.exist; + expect(user.Projects.length).to.equal(2); + + user.age = user.age + 1; // happy birthday joe + return user.save().then(user => { + expect(user.username).to.equal('joe'); + expect(user.age).to.equal(2); + expect(user.Projects).to.exist; + expect(user.Projects.length).to.equal(2); + }); + }); + }); + }); + }); + }); + }); + + it('saves many objects that each a have collection of eagerly loaded objects', function() { + return this.UserEager.create({ username: 'bart', age: 20 }).then(bart => { + return this.UserEager.create({ username: 'lisa', age: 20 }).then(lisa => { + return this.ProjectEager.create({ title: 'detention1', overdue_days: 0 }).then(detention1 => { + return this.ProjectEager.create({ title: 'detention2', overdue_days: 0 }).then(detention2 => { + return this.ProjectEager.create({ title: 'exam1', overdue_days: 0 }).then(exam1 => { + return this.ProjectEager.create({ title: 'exam2', overdue_days: 0 }).then(exam2 => { + return bart.setProjects([detention1, detention2]).then(() => { + return lisa.setProjects([exam1, exam2]).then(() => { + return this.UserEager.findAll({ where: { age: 20 }, order: [['username', 'ASC']], include: [{ model: this.ProjectEager, as: 'Projects' }] }).then(simpsons => { + expect(simpsons.length).to.equal(2); + + const _bart = simpsons[0]; + const _lisa = simpsons[1]; + + expect(_bart.Projects).to.exist; + expect(_lisa.Projects).to.exist; + expect(_bart.Projects.length).to.equal(2); + expect(_lisa.Projects.length).to.equal(2); + + _bart.age = _bart.age + 1; // happy birthday bart - off to Moe's + + return _bart.save().then(savedbart => { + expect(savedbart.username).to.equal('bart'); + expect(savedbart.age).to.equal(21); + + _lisa.username = 'lsimpson'; + + return _lisa.save().then(savedlisa => { + expect(savedlisa.username).to.equal('lsimpson'); + expect(savedlisa.age).to.equal(20); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + + it('saves many objects that each has one eagerly loaded object (to which they belong)', function() { + return this.UserEager.create({ username: 'poobah', age: 18 }).then(user => { + return this.ProjectEager.create({ title: 'homework', overdue_days: 10 }).then(homework => { + return this.ProjectEager.create({ title: 'party', overdue_days: 2 }).then(party => { + return user.setProjects([homework, party]).then(() => { + return this.ProjectEager.findAll({ include: [{ model: this.UserEager, as: 'Poobah' }] }).then(projects => { + expect(projects.length).to.equal(2); + expect(projects[0].Poobah).to.exist; + expect(projects[1].Poobah).to.exist; + expect(projects[0].Poobah.username).to.equal('poobah'); + expect(projects[1].Poobah.username).to.equal('poobah'); + + projects[0].title = 'partymore'; + projects[1].title = 'partymore'; + projects[0].overdue_days = 0; + projects[1].overdue_days = 0; + + return projects[0].save().then(() => { + return projects[1].save().then(() => { + return this.ProjectEager.findAll({ where: { title: 'partymore', overdue_days: 0 }, include: [{ model: this.UserEager, as: 'Poobah' }] }).then(savedprojects => { + expect(savedprojects.length).to.equal(2); + expect(savedprojects[0].Poobah).to.exist; + expect(savedprojects[1].Poobah).to.exist; + expect(savedprojects[0].Poobah.username).to.equal('poobah'); + expect(savedprojects[1].Poobah.username).to.equal('poobah'); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); +});