diff --git a/.gitignore b/.gitignore index cc3ee3a..f77c400 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .test.sqlite node_modules +package-lock.json +server.js +.vscode diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..a2d6e71 --- /dev/null +++ b/.npmignore @@ -0,0 +1,7 @@ +.test.sqlite +node_modules +package-lock.json +server.js +.vscode +/test +.git \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index f2b2cf6..de89cef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,4 @@ language: node_js node_js: - - "9.0" - - "8.0" - - "7.0" - - "6.0" + - "11.14.0" + - "11.0" diff --git a/README.md b/README.md index c61e543..2768922 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Temporal tables maintain __historical versions__ of data. Modifying operations ( - undo functionalities - track interactions (customer support) -Under the hood a history table with the same structure, but without constraints is created. +Under the hood a history table with the same structure, but without constraints is created (unless option __addAssociation__ is set to __true__). The normal singular/plural naming scheme in Sequelize is used: @@ -75,12 +75,81 @@ whereas the options are listed here (with default value). /* runs the insert within the sequelize hook chain, disable for increased performance without warranties */ blocking: true, - /* By default sequelize-temporal persist only changes, and saves the previous state in the history table. + /* By default sequelize-temporal persist only changes, and saves the previous state in the history table. The "full" option saves all transactions into the temporal database (i.e. this includes the latest state.) - This allows to only query the hostory table to get the full history of an entity. + This allows to only query the history table to get the full history of an entity. */ - full: false + full: false, + /* + By default sequelize-temporal will add 'History' to the history Model name and 'Histories' to the history table. + By updating the modelSuffix value, you can decide what the naming will be. + The value will be appended to the history Model name and its plural will be appended to the history tablename. + + examples for table User: + modelSuffix: '_Hist' --> History Model Name: User_Hist --> History Table Name: User_Hists + modelSuffix: 'Memory' --> History Model Name: UserMemory --> History Table Name: UserMemories + modelSuffix: 'Pass' --> History Model Name: UserPass --> History Table Name: UserPasses + */ + modelSuffix: 'History', + /* + By default sequelize-temporal will create the history table without associations. + However, setting this flag to true, you can keep association between the history table and the table with the latest value (origin). + + NOTE: THIS DOES NOT WORK IF YOU ARE USING A SEPARATE DB FOR THE HISTORICAL TABLES. IN THAT CASE, KEEP THE VALUE TO FALSE OR YOU WILL GET AN ERROR. + + example for table User: + model: 'User' + history model: 'UserHistories' + --> This would add function User.getUserHistories() to return all history entries for that user entry. + --> This would add function UserHistories.getUser() to get the original user from an history. + + If a model has associations, those would be mirrored to the history table. + Origin model can only get its own histories. + Even if a history table is associated to another origin table thought a foreign key field, the history table is not accessible from that origin table + + Basically, what you can access in the origin table can be accessed from the history table. + + example: + model: User + history model: UserHistories + + model: Creation + history model: CreationHistories + + User <-> Creation: 1 to many + + User.getCreations() exists (1 to many) + Creation.getUser() exists (1 to 1) + + User <-> UserHistories: 1 to many + + User.getUserHistories() exists (1 to many) + UserHistories.getUser() exists (1 to 1) + + Creation <-> CreationHistories: 1 to many + + Creation.getCreationHistories() exists (1 to many) + CreationHistories.getCreation() exists (1 to 1) + + CreationHistories -> User: many to 1 + + CreationHistories.getUser() exists (1 to 1) (same as Creation.getUser()) + User.GetCreationHistories DOES NOT EXIST. THE ORIGIN TABLE IS NOT MODIFIED. + + UserHistories -> Creation: many to many + + UserHistories.getCreations() exists (1 to many) (same as User.getCreations()) + CreationHistories.getUser() DOES NOT EXIST. THE ORIGIN TABLE IS NOT MODIFIED. + + */ + addAssociations: false, + /* + By default, transactions are allowed but can be disabled with that flag for the historical tables (transactions on original tables should stay the same). It is useful in case you are using a separate DB than the one use by the original DB. + + NOTE: IF YOU USE A SEPARATE DB FOR HISTORICAL TABLE, SET THE VALUE TO FALSE OR YOU WILL GET AN ERROR. + */ + allowTransactions: true ``` Details diff --git a/index.d.ts b/index.d.ts index fc74696..7281eba 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,7 +1,9 @@ declare module 'sequelize-temporal' { interface Options { blocking?:boolean, - full?:boolean + full?:boolean, + modelSuffix?:string, + addAssociations?:boolean, } function output(define:T, sequelize:any, options?:Options): T diff --git a/index.js b/index.js index 2f7714b..c35ab17 100644 --- a/index.js +++ b/index.js @@ -4,22 +4,18 @@ var temporalDefaultOptions = { // runs the insert within the sequelize hook chain, disable // for increased performance blocking: true, - full: false + full: false, + modelSuffix: 'History', + addAssociations: false, + allowTransactions: true, }; -var excludeAttributes = function(obj, attrsToExclude){ - // fancy way to exclude attributes - return _.omit(obj, _.partial(_.rearg(_.contains,0,2,1), attrsToExclude)); -} - -var Temporal = function(model, sequelize, temporalOptions){ - temporalOptions = _.extend({},temporalDefaultOptions, temporalOptions); +var Temporal = function(model, sequelize, temporalOptions) { + temporalOptions = _.extend({},temporalDefaultOptions, temporalOptions); var Sequelize = sequelize.Sequelize; - var historyName = model.name + 'History'; - //var historyName = model.getTableName() + 'History'; - //var historyName = model.options.name.singular + 'History'; + var historyName = model.name + temporalOptions.modelSuffix; var historyOwnAttrs = { hid: { @@ -35,9 +31,9 @@ var Temporal = function(model, sequelize, temporalOptions){ } }; - var excludedAttributes = ["Model","unique","primaryKey","autoIncrement", "set", "get", "_modelAttribute"]; + var excludedAttributes = ["Model","unique","primaryKey","autoIncrement", "set", "get", "_modelAttribute","references","onDelete","onUpdate"]; var historyAttributes = _(model.rawAttributes).mapValues(function(v){ - v = excludeAttributes(v, excludedAttributes); + v = _.omit(v, excludedAttributes); // remove the "NOW" defaultValue for the default timestamps // we want to save them, but just a copy from our master record if(v.fieldName == "createdAt" || v.fieldName == "updatedAt"){ @@ -52,31 +48,34 @@ var Temporal = function(model, sequelize, temporalOptions){ timestamps: false }; var excludedNames = ["name", "tableName", "sequelize", "uniqueKeys", "hasPrimaryKey", "hooks", "scopes", "instanceMethods", "defaultScope"]; - var modelOptions = excludeAttributes(model.options, excludedNames); + var modelOptions = _.omit(model.options, excludedNames); var historyOptions = _.assign({}, modelOptions, historyOwnOptions); // We want to delete indexes that have unique constraint var indexes = historyOptions.indexes; if(Array.isArray(indexes)){ - historyOptions.indexes = indexes.filter(function(index){return !index.unique && index.type != 'UNIQUE';}); + historyOptions.indexes = indexes.filter(function(index){return !index.unique && index.type != 'UNIQUE';}); } var modelHistory = sequelize.define(historyName, historyAttributes, historyOptions); + modelHistory.originModel = model; + modelHistory.addAssociations = temporalOptions.addAssociations; // we already get the updatedAt timestamp from our models - var insertHook = function(obj, options){ - var dataValues = (!temporalOptions.full && obj._previousDataValues) || obj.dataValues; - var historyRecord = modelHistory.create(dataValues, {transaction: options.transaction}); + var insertHook = function(obj, options){ + var dataValues = (!temporalOptions.full && obj._previousDataValues) || obj.dataValues; + var historyRecord = modelHistory.create(dataValues, {transaction: temporalOptions.allowTransactions? options.transaction: null}); if(temporalOptions.blocking){ return historyRecord; } } + var insertBulkHook = function(options){ if(!options.individualHooks){ var queryAll = model.findAll({where: options.where, transaction: options.transaction}).then(function(hits){ if(hits){ - hits = _.pluck(hits, 'dataValues'); - return modelHistory.bulkCreate(hits, {transaction: options.transaction}); + hits = _.map(hits, 'dataValues'); + return modelHistory.bulkCreate(hits, {transaction: temporalOptions.allowTransactions? options.transaction: null}); } }); if(temporalOptions.blocking){ @@ -85,12 +84,48 @@ var Temporal = function(model, sequelize, temporalOptions){ } } + var beforeSync = function(options) { + const source = this.originModel; + const sourceHist = this; + + if(source && !source.name.endsWith(temporalOptions.modelSuffix) && source.associations && temporalOptions.addAssociations == true && sourceHist) { + const pkfield = source.primaryKeyField; + //adding associations from history model to origin model's association + Object.keys(source.associations).forEach(assokey => { + const association = source.associations[assokey]; + const associationOptions = _.cloneDeep(association.options); + const target = association.target; + const assocName = association.associationType.charAt(0).toLowerCase() + association.associationType.substr(1); + associationOptions.onDelete = 'NO ACTION'; + associationOptions.onUpdate = 'NO ACTION'; + + //handle primary keys for belongsToMany + if(assocName == 'belongsToMany') { + sourceHist.primaryKeys = _.forEach(source.primaryKeys, (x) => x.autoIncrement = false); + sourceHist.primaryKeyField = Object.keys(sourceHist.primaryKeys)[0]; + } + + sourceHist[assocName].apply(sourceHist, [target, associationOptions]); + }); + + //adding associations between origin model and history + source.hasMany(sourceHist, { foreignKey: pkfield }); + sourceHist.belongsTo(source, { foreignKey: pkfield }); + + sequelize.models[sourceHist.name] = sourceHist; + } + + return Promise.resolve('Temporal associations established'); + } + // use `after` to be nonBlocking // all hooks just create a copy if (temporalOptions.full) { model.addHook('afterCreate', insertHook); - model.addHook('afterUpdate', insertHook); - model.addHook('afterDestroy', insertHook); + model.addHook('afterUpdate', insertHook); + model.addHook('afterBulkUpdate', insertBulkHook); + model.addHook('afterDestroy', insertHook); + model.addHook('afterBulkDestroy', insertBulkHook); model.addHook('afterRestore', insertHook); } else { model.addHook('beforeUpdate', insertHook); @@ -106,6 +141,7 @@ var Temporal = function(model, sequelize, temporalOptions){ modelHistory.addHook('beforeUpdate', readOnlyHook); modelHistory.addHook('beforeDestroy', readOnlyHook); + modelHistory.addHook('beforeSync', 'HistoricalSyncHook', beforeSync); return model; }; diff --git a/package.json b/package.json index 61d97c7..49c3ad3 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { - "name": "sequelize-temporal", - "version": "1.0.6", + "name": "sequelize-temporal", + "version": "2.0.1", "description": "Temporal tables for Sequelize", "main": "index.js", "directories": { "test": "test" }, "scripts": { - "test": "./node_modules/mocha/bin/mocha" + "test": "mocha" }, "repository": { "type": "git", @@ -20,22 +20,22 @@ "sql11", "undo", "paranoid", - "historical" + "historical" ], "author": "greenify (https://github.com/greenify)", "license": "MIT", "bugs": { "url": "https://github.com/bonaval/sequelize-temporal/issues" }, - "homepage": "https://github.com/bonaval/sequelize-temporal#readme", + "homepage": "https://github.com/opencollective/sequelize-historical#readme", "dependencies": { - "lodash": "^3.10.1" + "lodash": "^4.17.11" }, "devDependencies": { - "chai": "^4.1.2", + "chai": "^4.2.0", "chai-as-promised": "^7.1.1", - "mocha": "^5.2.0", - "sequelize": "^4.38.0", - "sqlite3": "^4.0.1" + "mocha": "^6.1.4", + "sequelize": "^5.4.0", + "sqlite3": "^4.0.6" } } diff --git a/test/test.js b/test/test.js index 0a7bc5c..6bcf615 100644 --- a/test/test.js +++ b/test/test.js @@ -1,335 +1,1004 @@ -var Temporal = require('../'); -var Sequelize = require('sequelize'); -var chai = require("chai"); -var chaiAsPromised = require("chai-as-promised"); +const Historical = require('../'); +const Sequelize = require('sequelize'); +const chai = require("chai"); +const chaiAsPromised = require("chai-as-promised"); +const fs = require('fs'); chai.use(chaiAsPromised); -var assert = chai.assert; -var eventually = assert.eventually; +const assert = chai.assert; +const eventually = assert.eventually; describe('Read-only API', function(){ - var sequelize, User, UserHistory; - - function freshDB(){ - // overwrites the old SQLite DB - sequelize = new Sequelize('', '', '', { - dialect: 'sqlite', - storage: __dirname + '/.test.sqlite' - }); - User = Temporal(sequelize.define('User', { - name: Sequelize.TEXT - }), sequelize); - UserHistory = sequelize.models.UserHistory; - return sequelize.sync({ force: true }); - } - - function freshDBWithFullModeAndParanoid() { - sequelize = new Sequelize('', '', '', { - dialect: 'sqlite', - storage: __dirname + '/.test.sqlite' - }); - User = Temporal(sequelize.define('User', { - name: Sequelize.TEXT - }, { paranoid: true }), sequelize, { full: true }); - UserHistory = sequelize.models.UserHistory; - - return sequelize.sync({ force: true }); - } - - function assertCount(modelHistory, n, opts){ - // wrapped, chainable promise - return function(obj){ - return modelHistory.count(opts).then(function(count){ - assert.equal(n, count, "history entries") - return obj; - }); - } - } - - describe('hooks', function(){ - beforeEach(freshDB); - it('onCreate: should not store the new version in history db' , function(){ - return User.create({ name: 'test' }).then(assertCount(UserHistory, 0)); - }); - it('onUpdate/onDestroy: should save to the historyDB' , function(){ - return User.create() - .then(assertCount(UserHistory,0)) - .then(function(user){ - user.name = "foo"; - return user.save(); - }).then(assertCount(UserHistory,1)) - .then(function(user){ - return user.destroy(); - }).then(assertCount(UserHistory,2)) - }); - it('onUpdate: should store the previous version to the historyDB' , function(){ - return User.create({name: "foo"}) - .then(assertCount(UserHistory,0)) - .then(function(user){ - user.name = "bar"; - return user.save(); - }).then(assertCount(UserHistory,1)) - .then(function(){ - return UserHistory.findAll(); - }).then(function(users){ - assert.equal(users.length,1, "only one entry in DB"); - assert.equal(users[0].name, "foo", "previous entry saved"); - }).then(function(user){ - return User.findOne(); - }).then(function(user){ - return user.destroy(); - }).then(assertCount(UserHistory,2)) - }); - it('onDelete: should store the previous version to the historyDB' , function(){ - return User.create({name: "foo"}) - .then(assertCount(UserHistory,0)) - .then(function(user){ - return user.destroy(); - }).then(assertCount(UserHistory,1)) - .then(function(){ - return UserHistory.findAll(); - }).then(function(users){ - assert.equal(users.length,1, "only one entry in DB"); - assert.equal(users[0].name, "foo", "previous entry saved"); - }); - }); - }); - - describe('transactions', function(){ - beforeEach(freshDB); - it('revert on failed transactions' , function(){ - return sequelize.transaction().then(function(t){ - var opts = {transaction: t}; - return User.create(opts) - .then(assertCount(UserHistory,0, opts)) - .then(function(user){ - user.name = "foo"; - return user.save(opts); - }).then(assertCount(UserHistory,1, opts)) - .then(function(){ - t.rollback(); - }); - }).then(assertCount(UserHistory,0)); - }); - }); - - describe('bulk update', function(){ - beforeEach(freshDB); - it('should archive every entry' , function(){ - return User.bulkCreate([ - {name: "foo1"}, - {name: "foo2"}, - ]).then(assertCount(UserHistory,0)) - .then(function(){ - return User.update({ name: 'updated-foo' }, {where: {}}); - }).then(assertCount(UserHistory,2)) - }); - it('should revert under transactions' , function(){ - return sequelize.transaction().then(function(t){ - var opts = {transaction: t}; - return User.bulkCreate([ - {name: "foo1"}, - {name: "foo2"}, - ], opts).then(assertCount(UserHistory,0,opts)) - .then(function(){ - return User.update({ name: 'updated-foo' }, {where: {}, transaction: t}); - }).then(assertCount(UserHistory,2, opts)) - .then(function(){ - t.rollback(); - }); - }).then(assertCount(UserHistory,0)); - }); - - }); - - describe('bulk destroy/truncate', function(){ - beforeEach(freshDB); - it('should archive every entry' , function(){ - return User.bulkCreate([ - {name: "foo1"}, - {name: "foo2"}, - ]).then(assertCount(UserHistory,0)) - .then(function(){ - return User.destroy({ - where: {}, - truncate: true // truncate the entire table - }); - }).then(assertCount(UserHistory,2)) - }); - it('should revert under transactions' , function(){ - return sequelize.transaction().then(function(t){ - var opts = {transaction: t}; - return User.bulkCreate([ - {name: "foo1"}, - {name: "foo2"}, - ], opts).then(assertCount(UserHistory,0,opts)) - .then(function(){ - return User.destroy({ - where: {}, - truncate: true, // truncate the entire table - transaction: t - }); - }).then(assertCount(UserHistory,2, opts)) - .then(function(){ - t.rollback(); - }); - }).then(assertCount(UserHistory,0)); - }); - - - }); - - describe('read-only ', function(){ - it('should forbid updates' , function(){ - var userUpdate = UserHistory.create().then(function(uh){ - uh.update({name: 'bla'}); - }); - return assert.isRejected(userUpdate, Error, "Validation error"); - }); - it('should forbid deletes' , function(){ - var userUpdate = UserHistory.create().then(function(uh){ - uh.destroy(); - }); - return assert.isRejected(userUpdate, Error, "Validation error"); - }); - }); - - describe('interference with the original model', function(){ - - beforeEach(freshDB); - - it('shouldn\'t delete instance methods' , function(){ - Fruit = Temporal(sequelize.define('Fruit', { - name: Sequelize.TEXT - }), sequelize); - Fruit.prototype.sayHi = function(){ return 2;} - return sequelize.sync().then(function(){ - return Fruit.create(); - }).then(function(f){ - assert.isFunction(f.sayHi); - assert.equal(f.sayHi(), 2); - }); - }); - - it('shouldn\'t interfere with hooks of the model' , function(){ - var triggered = 0; - Fruit = Temporal(sequelize.define('Fruit', { - name: Sequelize.TEXT - }, { - hooks:{ - beforeCreate: function(){ triggered++;} - } - }), sequelize); - return sequelize.sync().then(function(){ - return Fruit.create(); - }).then(function(f){ - assert.equal(triggered, 1,"hook trigger count"); - }); - }); - - it('shouldn\'t interfere with setters' , function(){ - var triggered = 0; - Fruit = Temporal(sequelize.define('Fruit', { - name: { - type: Sequelize.TEXT, - set: function(){ - triggered++; - } - } - }), sequelize); - return sequelize.sync().then(function(){ - return Fruit.create({name: "apple"}); - }).then(function(f){ - assert.equal(triggered, 1,"hook trigger count"); - }); - }); - - }); - - describe('full mode', function() { - - beforeEach(freshDBWithFullModeAndParanoid); - - it('onCreate: should store the new version in history db' , function(){ - return User.create({ name: 'test' }) - .then(function() { - return UserHistory.findAll(); - }) - .then(function(histories) { - assert.equal(1, histories.length); - assert.equal('test', histories[0].name); - }); - }); - - it('onUpdate: should store the new version to the historyDB' , function(){ - return User.create({ name: 'test' }) - .then(function(user) { - return user.update({ name: 'renamed' }); - }) - .then(function() { - return UserHistory.findAll(); - }) - .then(function(histories) { - assert.equal(histories.length, 2, 'two entries in DB'); - assert.equal(histories[0].name, 'test', 'first version saved'); - assert.equal(histories[1].name, 'renamed', 'second version saved'); - }); - }); - - it('onDelete: should store the previous version to the historyDB' , function(){ - return User.create({ name: 'test' }) - .then(function(user) { - return user.update({ name: 'renamed' }); - }) - .then(function(user) { - return user.destroy(); - }) - .then(function() { - return UserHistory.findAll(); - }) - .then(function(histories) { - assert.equal(histories.length, 3, 'three entries in DB'); - assert.equal(histories[0].name, 'test', 'first version saved'); - assert.equal(histories[1].name, 'renamed', 'second version saved'); - assert.notEqual(histories[2].deletedAt, null, 'deleted version saved'); - }); - }); - - it('onRestore: should store the new version to the historyDB' , function(){ - return User.create({ name: 'test' }) - .then(function(user) { - return user.destroy(); - }) - .then(function(user) { - return user.restore(); - }) - .then(function() { - return UserHistory.findAll(); - }) - .then(function(histories) { - assert.equal(histories.length, 3, 'three entries in DB'); - assert.equal(histories[0].name, 'test', 'first version saved'); - assert.notEqual(histories[1].deletedAt, null, 'deleted version saved'); - assert.equal(histories[2].deletedAt, null, 'restored version saved'); - }); - }); - - it('should revert on failed transactions, even when using after hooks' , function(){ - return sequelize.transaction() - .then(function(transaction) { - var options = { transaction: transaction }; - - return User.create({ name: 'test' }, options) - .then(function(user) { - return user.destroy(options); - }) - .then(assertCount(UserHistory, 2, options)) - .then(function() { - return transaction.rollback() - }); - }) - .then(assertCount(UserHistory,0)); - }); - - }); + var sequelize; + var sequelizeHist; + + function newDB(paranoid, options){ + if(sequelize) { + sequelize.close(); + sequelize = null; + } + const separate = !(!options || !options.test || !options.test.separate || options.test.separate == false) + + const dbFile = __dirname + '/.test.sqlite'; + + try {fs.unlinkSync(dbFile);} catch {}; + + sequelize = new Sequelize('', '', '', { + dialect: 'sqlite', + storage: dbFile, + logging: false//console.log + }); + + if(separate == true) { + console.warn('Test is using separate DBs'); + + const dbFile2 = __dirname + '/.test2.sqlite'; + + try {fs.unlinkSync(dbFile2);} catch {}; + + sequelizeHist = new Sequelize('', '', '', { + dialect: 'sqlite', + storage: dbFile2, + logging: false//console.log + }); + } + + //Define origin models + const User = sequelize.define('User', { name: Sequelize.TEXT }, {paranoid: paranoid || false}); + const Creation = sequelize.define('Creation', { name: Sequelize.TEXT, user: Sequelize.INTEGER, user2: Sequelize.INTEGER }, {paranoid: paranoid || false}); + const Tag = sequelize.define('Tag', { name: Sequelize.TEXT }, {paranoid: paranoid || false}); + const Event = sequelize.define('Event', { name: Sequelize.TEXT, creation: Sequelize.INTEGER }, {paranoid: paranoid || false}); + const CreationTag = sequelize.define('CreationTag', { creation: Sequelize.INTEGER, tag: Sequelize.INTEGER }, {paranoid: paranoid || false}); + + //Associate models + + //1.* with 2 association to same table + User.hasMany(Creation, { foreignKey: 'user', as: 'creatorCreations' }); + User.hasMany(Creation, { foreignKey: 'user2', as: 'updatorCreations' }); + + Creation.belongsTo(User, { foreignKey: 'user', as: 'createUser' }); + Creation.belongsTo(User, { foreignKey: 'user2', as: 'updateUser' }); + + //1.1 + Event.belongsTo(Creation, { foreignKey: 'creation' }); + Creation.hasOne(Event, { foreignKey: 'creation' }); + + //*.* + Tag.belongsToMany(Creation, { through: CreationTag, foreignKey: 'tag', otherKey: 'creation' }); + Creation.belongsToMany(Tag, { through: CreationTag, foreignKey: 'creation', otherKey: 'tag' }); + + //Historyize + Historical(User, separate == true? sequelizeHist: sequelize, options); + Historical(Creation, separate == true? sequelizeHist: sequelize, options); + Historical(Tag, separate == true? sequelizeHist: sequelize, options); + Historical(Event, separate == true? sequelizeHist: sequelize, options); + Historical(CreationTag, separate == true? sequelizeHist: sequelize, options); + + return sequelize.sync({force:true}).then( s => separate == true ? sequelizeHist.sync({force:true}): s); + } + + //Adding 3 tags, 2 creations, 2 events, 2 user + //each creation has 3 tags + //user has 2 creations + //creation has 1 event + //tags,crestions,user,events are renamed 3 times to generate 3 history data + //1 tag is removed and re-added to a creation to create 1 history entry in the CreationTags table + function dataCreate() + { + const tag = sequelize.models.Tag.create({ name: 'tag01' }).then( t => { + t.name = 'tag01 renamed'; + t.save(); + t.name = 'tag01 renamed twice'; + t.save(); + t.name = 'tag01 renamed three times'; + t.save(); + return t; + }); + + const tag2 = sequelize.models.Tag.create({ name: 'tag02' }).then( t => { + t.name = 'tag02 renamed'; + t.save(); + t.name = 'tag02 renamed twice'; + t.save(); + t.name = 'tag02 renamed three times'; + t.save(); + return t; + }); + + const tag3 = sequelize.models.Tag.create({ name: 'tag03' }).then( t => { + t.name = 'tag03 renamed'; + t.save(); + t.name = 'tag03 renamed twice'; + t.save(); + t.name = 'tag03 renamed three times'; + t.save(); + return t; + }); + + const user = sequelize.models.User.create({ name: 'user01' }).then( u => { + u.name = 'user01 renamed'; + u.save(); + u.name = 'user01 renamed twice'; + u.save(); + u.name = 'user01 renamed three times'; + u.save(); + return u; + }); + + const user2 = sequelize.models.User.create({ name: 'user02' }).then( u => { + u.name = 'user02 renamed'; + u.save(); + u.name = 'user02 renamed twice'; + u.save(); + u.name = 'user02 renamed three times'; + u.save(); + return u; + }); + + const creation = Promise.all([user, user2]) + .then(allU => sequelize.models.Creation.create({ name: 'creation01', user: allU[0].id, user2: allU[1].id })) + .then( c => { + c.name = 'creation01 renamed'; + c.save(); + c.name = 'creation01 renamed twice'; + c.save(); + c.name = 'creation01 renamed three times'; + c.save(); + return c; + }); + + const creation2 = Promise.all([user, user2]) + .then(allU => sequelize.models.Creation.create({ name: 'creation02', user: allU[0].id, user2: allU[1].id })) + .then( c => { + c.name = 'creation02 renamed'; + c.save(); + c.name = 'creation02 renamed twice'; + c.save(); + c.name = 'creation02 renamed three times'; + c.save(); + return c; + }); + + const event = creation.then(c => sequelize.models.Event.create({ name: 'event01', creation: c.id })) + .then( e => { + e.name = 'event01 renamed'; + e.save(); + e.name = 'event01 renamed twice'; + e.save(); + e.name = 'event01 renamed three times'; + e.save(); + return e; + }); + + const event2 = creation2.then(c => sequelize.models.Event.create({ name: 'event02', creation: c.id })) + .then( e => { + e.name = 'event02 renamed'; + e.save(); + e.name = 'event02 renamed twice'; + e.save(); + e.name = 'event02 renamed three times'; + e.save(); + return e; + }); + + const creationTag1 = Promise.all([tag, creation]).then(models =>{ + const t = models[0]; + const c = models[1]; + + return c.addTag(t); + }); + + const creationTag1_rem = Promise.all([tag, creation,creationTag1]).then(models =>{ + const t = models[0]; + const c = models[1]; + + return c.removeTag(t); + }); + + const creationTag1_rea = Promise.all([tag, creation,creationTag1_rem]).then(models =>{ + const t = models[0]; + const c = models[1]; + + return c.addTag(t); + }); + + const creationTag2 = Promise.all([tag2, creation]).then(models =>{ + const t = models[0]; + const c = models[1]; + + return c.addTag(t); + }); + + const creationTag3 = Promise.all([tag3, creation]).then(models =>{ + const t = models[0]; + const c = models[1]; + + return c.addTag(t); + }); + + const creationTag4 = Promise.all([tag, creation2]).then(models =>{ + const t = models[0]; + const c = models[1]; + + return c.addTag(t); + }); + + const creationTag5 = Promise.all([tag2, creation2]).then(models =>{ + const t = models[0]; + const c = models[1]; + + return c.addTag(t); + }); + + const creationTag6 = Promise.all([tag3, creation2]).then(models =>{ + const t = models[0]; + const c = models[1]; + + return c.addTag(t); + }); + + return Promise.all([ + event, + event2, + tag, + tag2, + tag3, + user, + user2, + creation, + creation2, + creationTag1, + creationTag2, + creationTag3, + creationTag4, + creationTag5, + creationTag6, + creationTag1_rea, + creationTag1_rem + ]); + } + + function freshDB(){ + return newDB(); + } + + function freshDBWithSeparateHistoryDB(){ + return newDB(false, { allowTransactions: false , test: {separate: true}}); + } + + function freshDBWithAssociations(){ + return newDB(false, { addAssociations: true}); + } + + function freshDBWithFullModeAndParanoid(){ + return newDB(true,{ full: true }); + } + + function freshDBWithSuffixEndingWithT(){ + return newDB(false, { modelSuffix: '_Hist'}); + } + + function assertCount(modelHistory, n, opts) { + // wrapped, chainable promise + return function(obj) { + return modelHistory.count(opts).then((count) => { + assert.equal(n, count, "history entries") + return obj; + }); + } + } + + describe('Separate DB Tests', function() { + beforeEach(freshDBWithSeparateHistoryDB); + + it('onUpdate/onDestroy: should save to the historyDB' , function() { + return sequelize.models.User.create() + .then(assertCount(sequelizeHist.models.UserHistory,0)) + .then((user) => { + user.name = "foo"; + return user.save(); + }) + .then(assertCount(sequelizeHist.models.UserHistory,1)) + .then(user => user.destroy()) + .then(assertCount(sequelizeHist.models.UserHistory,2)) + }); + + it('revert on failed transactions' , function() { + return sequelize.transaction() + .then((t) => { + var opts = {transaction: t}; + return sequelize.models.User.create({name: "not foo"}) + .then(assertCount(sequelizeHist.models.UserHistory,0)) + .then((user) => { + user.name = "foo"; + user.save(opts); + }) + .then(assertCount(sequelizeHist.models.UserHistory,1)) + .then(() => t.rollback()); + }) + .then(assertCount(sequelizeHist.models.UserHistory,1)); + }); + + it('should archive every entry', function() { + return sequelize.models.User.bulkCreate([{name: "foo1"},{name: "foo2"}]) + .then(assertCount(sequelizeHist.models.UserHistory,0)) + .then(() => sequelize.models.User.update({ name: 'updated-foo' }, {where: {}})) + .then(assertCount(sequelizeHist.models.UserHistory,2)) + }); + + it('should revert under transactions', function() { + return sequelize.transaction() + .then(function(t) { + var opts = {transaction: t}; + return sequelize.models.User.bulkCreate([{name: "foo1"},{name: "foo2"}], opts) + .then(assertCount(sequelizeHist.models.UserHistory,0)) + .then(() => sequelize.models.User.update({ name: 'updated-foo' }, {where: {}, transaction: t})) + .then(assertCount(sequelizeHist.models.UserHistory,2)) + .then(() => t.rollback()); + }) + .then(assertCount(sequelizeHist.models.UserHistory,2)); + }); + + it('should revert on failed transactions, even when using after hooks' , function(){ + return sequelize.transaction() + .then((transaction) => { + var options = { transaction: transaction }; + + return sequelize.models.User.create({ name: 'test' }, options) + .then(user => user.destroy(options)) + .then(assertCount(sequelizeHist.models.UserHistory, 1)) + .then(() => transaction.rollback()); + }) + .then(assertCount(sequelizeHist.models.UserHistory,1)); + }); + }); + + describe('Association Tests', function() { + describe('test there are no history association', function(){ + beforeEach(freshDB); + it('Should have relations for origin models but not for history models' , function(){ + const init = dataCreate(); + + //Get User + const user = init.then(() => sequelize.models.User.findOne()); + + //User associations check + const userHistory = user.then(u =>{ + assert.notExists(u.getUserHistories, 'User: getUserHistories exists'); + return Promise.resolve('done'); + }); + + const creation = user.then(u =>{ + assert.exists(u.getCreatorCreations, 'User: getCreatorCreations does not exist'); + assert.exists(u.getUpdatorCreations, 'User: getUpdatorCreations does not exist'); + return u.getCreatorCreations(); + }); + + //Creation associations check + const creationHistory = creation.then(c =>{ + assert.equal(c.length, 2, 'User: should have found 2 creations'); + const first = c[0]; + assert.notExists(first.getCreationHistories, 'Creation: getCreationHistories exists'); + return Promise.resolve('done'); + }); + + const tag = creation.then(c =>{ + const first = c[0]; + assert.exists(first.getTags, 'Creation: getTags does not exist'); + return first.getTags(); + }); + + const event = creation.then(c =>{ + const first = c[0]; + assert.exists(first.getEvent, 'Creation: getEvent does not exist'); + return first.getEvent(); + }); + + const cUser = creation.then(c =>{ + const first = c[0]; + assert.exists(first.getCreateUser, 'Creation: getCreateUser does not exist'); + assert.exists(first.getUpdateUser, 'Creation: getUpdateUser does not exist'); + return first.getCreateUser(); + }).then(cu => { + assert.exists(cu, 'Creation: did not find CreateUser'); + return Promise.resolve('done'); + }); + + //Tag associations check + const tagHistory = tag.then(t =>{ + assert.equal(t.length, 3, 'Creation: should have found 3 tags'); + const first = t[0]; + assert.notExists(first.getTagHistories, 'Tag: getTagHistories exists'); + return Promise.resolve('done'); + }); + + const tCreation = tag.then(t =>{ + const first = t[0]; + assert.exists(first.getCreations, 'Tag: getCreations does not exist'); + return first.getCreations(); + }).then(tc => { + assert.equal(tc.length, 2, 'Tag: should have found 2 creations'); + return Promise.resolve('done'); + }); + + //Event associations check + const eventHistory = event.then(e =>{ + assert.exists(e, 'Creation: did not find event'); + assert.notExists(e.getEventHistories, 'Event: getEventHistories exist'); + return Promise.resolve('done'); + }); + + const eCreation = event.then(e =>{ + assert.exists(e.getCreation); + return e.getCreation(); + }).then(ec => { + assert.exists(ec); + return Promise.resolve('done'); + }); + + //Check history data + const userHistories = init.then(assertCount(sequelize.models.UserHistory, 6)); + const creationHistories = init.then(assertCount(sequelize.models.CreationHistory, 6)); + const tagHistories = init.then(assertCount(sequelize.models.TagHistory, 9)); + const eventHistories = init.then(assertCount(sequelize.models.EventHistory, 6)); + const creationTagHistories = init.then(assertCount(sequelize.models.CreationTagHistory, 1)); + + return Promise.all([ + creation, + creationHistories, + creationHistory, + creationTagHistories, + cUser, + eCreation, + event, + eventHistories, + eventHistory, + init, + tag, + tagHistories, + tagHistory, + tCreation, + user, + userHistories, + userHistory + ]); + }); + }); + + describe('test there are associations are created between origin and history', function(){ + beforeEach(freshDBWithAssociations); + it('Should have relations for origin models and for history models to origin' , function(){ + const init = dataCreate(); + + //Get User + const user = init.then(() => sequelize.models.User.findOne()); + + //User associations check + const userHistory = user.then(u =>{ + assert.exists(u.getUserHistories, 'User: getUserHistories does not exist'); + return u.getUserHistories(); + }); + + const creation = user.then(u =>{ + assert.exists(u.getCreatorCreations, 'User: getCreatorCreations does not exist'); + assert.exists(u.getUpdatorCreations, 'User: getUpdatorCreations does not exist'); + return u.getCreatorCreations(); + }); + + //UserHistories associations check + const uhCreation = userHistory.then(uh =>{ + assert.equal(uh.length, 3, 'User: should have found 3 UserHistories'); + const first = uh[0]; + assert.exists(first.getCreatorCreations, 'UserHistory: getCreatorCreations does not exist'); + assert.exists(first.getUpdatorCreations, 'UserHistory: getUpdatorCreations does not exist'); + return first.getCreatorCreations(); + }).then(uhc => { + assert.equal(uhc.length, 2, 'UserHistory: should have found 2 creations'); + return Promise.resolve('done'); + }); + + const uhUser = userHistory.then(uh =>{ + const first = uh[0]; + assert.exists(first.getUser, 'UserHistory: getUser does not exist'); + return first.getUser(); + }).then(uhu => { + assert.exists(uhu, 'UserHistory: did not find a user'); + return Promise.resolve('done'); + }); + + //Creation associations check + const creationHistory = creation.then(c =>{ + assert.equal(c.length, 2, 'User: should have found 2 creations'); + const first = c[0]; + assert.exists(first.getCreationHistories, 'Creation: getCreationHistories does not exist'); + return first.getCreationHistories(); + }); + + const tag = creation.then(c =>{ + const first = c[0]; + assert.exists(first.getTags, 'Creation: getTags does not exist'); + return first.getTags(); + }); + + const event = creation.then(c =>{ + const first = c[0]; + assert.exists(first.getEvent, 'Creation: getEvent does not exist'); + return first.getEvent(); + }); + + const cUser = creation.then(c =>{ + const first = c[0]; + assert.exists(first.getCreateUser, 'Creation: getCreateUser does not exist'); + assert.exists(first.getUpdateUser, 'Creation: getUpdateUser does not exist'); + return first.getCreateUser(); + }).then(cu => { + assert.exists(cu, 'Creation: did not find a create user'); + return Promise.resolve('done'); + }); + + //CreationHistories association check + const chCreation = creationHistory.then(ch =>{ + assert.equal(ch.length, 3, 'Creation: should have found 3 CreationHistories'); + const first = ch[0]; + assert.exists(first.getCreation, 'CreationHistory: getCreation does not exist'); + return first.getCreation(); + }).then(chc => { + assert.exists(chc, 'CreationHistory: did noy find a creation'); + return Promise.resolve('done'); + }); + + const chTag = creationHistory.then(ch =>{ + const first = ch[0]; + assert.exists(first.getTags, 'CreationHistory: getTags does not exist'); + return first.getTags(); + }).then(uht => { + assert.equal(uht.length, 3); + return Promise.resolve('done'); + }); + + const chUser = creationHistory.then(ch =>{ + const first = ch[0]; + assert.exists(first.getCreateUser, 'CreationHistory: getCreateUser does not exist'); + assert.exists(first.getUpdateUser, 'CreationHistory: getUpdateUser does not exist'); + return first.getCreateUser(); + }).then(chu => { + assert.exists(chu, 'CreationHistory: did not find a user'); + return Promise.resolve('done'); + }); + + const chEvent = creationHistory.then(ch =>{ + const first = ch[0]; + assert.exists(first.getEvent, 'CreationHistory: getEvent does not exist'); + return first.getEvent(); + }).then(che => { + assert.exists(che, 'CreationHistory: did not find an event'); + return Promise.resolve('done'); + }); + + + //Tag associations check + const tagHistory = tag.then(t =>{ + assert.equal(t.length, 3, 'Creation: should have found 3 tags'); + const first = t[0]; + assert.exists(first.getTagHistories, 'Tag: getTagHistories does not exist'); + return first.getTagHistories(); + }); + + const tCreation = tag.then(t =>{ + const first = t[0]; + assert.exists(first.getCreations, 'Tag: getCreations does not exist'); + return first.getCreations(); + }).then(tc => { + assert.equal(tc.length, 2, 'Tag: should have found 2 creations'); + return Promise.resolve('done'); + }); + + //TagHistories associations check + const thTag = tagHistory.then(th =>{ + assert.equal(th.length, 3, 'TagHistory: should have found 3 TagHistories'); + const first = th[0]; + assert.exists(first.getTag, 'TagHistory: getTag does not exist'); + return first.getTag(); + }).then(tht => { + assert.exists(tht, 'TagHistory: did not find a tag'); + return Promise.resolve('done'); + }); + + const thCreation = tagHistory.then(th =>{ + const first = th[0]; + assert.exists(first.getCreations, 'TagHistory: getCreations does not exist'); + return first.getCreations(); + }).then(thc => { + assert.equal(thc.length, 2, 'TagHistory: should have found 2 creations'); + return Promise.resolve('done'); + }); + + //Event associations check + const eventHistory = event.then(e =>{ + assert.exists(e, 'Creation: did not find an event'); + assert.exists(e.getEventHistories, 'Event: getEventHistories does not exist'); + return e.getEventHistories(); + }); + + const eCreation = event.then(e =>{ + assert.exists(e.getCreation, 'Event: getCreation does not exist'); + return e.getCreation(); + }).then(ec => { + assert.exists(ec, 'Event: did not find a creation'); + return Promise.resolve('done'); + }); + + //EventHistories associations check + const ehEvent = eventHistory.then(eh =>{ + assert.equal(eh.length, 3, 'Event: should have found 3 EventHistories'); + const first = eh[0]; + assert.exists(first.getEvent, 'EventHistories: getEvent does not exist'); + return first.getEvent(); + }).then(ehe => { + assert.exists(ehe, 'EventHistories: did not find an event'); + return Promise.resolve('done'); + }); + + const ehCreation = eventHistory.then(eh =>{ + const first = eh[0]; + assert.exists(first.getCreation, 'EventHistories: getCreation does not exist'); + return first.getCreation(); + }).then(ehc => { + assert.exists(ehc, 'EventHistories: did not find a creation'); + return Promise.resolve('done'); + }); + + //Check history data + const userHistories = init.then(assertCount(sequelize.models.UserHistory, 6)); + const creationHistories = init.then(assertCount(sequelize.models.CreationHistory, 6)); + const tagHistories = init.then(assertCount(sequelize.models.TagHistory, 9)); + const eventHistories = init.then(assertCount(sequelize.models.EventHistory, 6)); + const creationTagHistories = init.then(assertCount(sequelize.models.CreationTagHistory, 1)); + + + return Promise.all([ + chCreation, + chEvent, + chTag, + chUser, + creation, + creationHistories, + creationHistory, + creationTagHistories, + cUser, + eCreation, + event, + eventHistories, + eventHistory, + init, + tag, + tagHistories, + tagHistory, + tCreation, + thCreation, + thTag, + uhCreation, + uhUser, + user, + userHistories, + userHistory, + ehEvent, + ehCreation + ]); + }); + }); + }); + + //these tests are the same as hooks since the results should not change, even with a different model name + //Only added is to test for the model name + describe('test suffix ending in T', function() { + beforeEach(freshDBWithSuffixEndingWithT); + it('onCreate: should not store the new version in history db' , function() { + return sequelize.models.User.create({ name: 'test' }) + .then(assertCount(sequelize.models.User_Hist, 0)); + }); + it('onUpdate/onDestroy: should save to the historyDB' , function() { + return sequelize.models.User.create() + .then(assertCount(sequelize.models.User_Hist,0)) + .then((user) => { + user.name = "foo"; + return user.save(); + }) + .then(assertCount(sequelize.models.User_Hist,1)) + .then(user => user.destroy()) + .then(assertCount(sequelize.models.User_Hist,2)); + }); + it('onUpdate: should store the previous version to the historyDB' , function() { + return sequelize.models.User.create({name: "foo"}) + .then(assertCount(sequelize.models.User_Hist,0)) + .then((user) => { + user.name = "bar"; + return user.save(); + }) + .then(assertCount(sequelize.models.User_Hist,1)) + .then(() => sequelize.models.User_Hist.findAll()) + .then((users) => { + assert.equal(users.length,1, "only one entry in DB"); + assert.equal(users[0].name, "foo", "previous entry saved"); + }) + .then(() => sequelize.models.User.findOne()) + .then((user) => user.destroy()) + .then(assertCount(sequelize.models.User_Hist,2)) + }); + it('onDelete: should store the previous version to the historyDB' , function() { + return sequelize.models.User.create({name: "foo"}) + .then(assertCount(sequelize.models.User_Hist,0)) + .then(user => user.destroy()) + .then(assertCount(sequelize.models.User_Hist,1)) + .then(() => sequelize.models.User_Hist.findAll()) + .then((users) => { + assert.equal(users.length,1, "only one entry in DB"); + assert.equal(users[0].name, "foo", "previous entry saved"); + }); + }); + }); + + describe('hooks', function() { + beforeEach(freshDB); + it('onCreate: should not store the new version in history db' , function() { + return sequelize.models.User.create({ name: 'test' }) + .then(assertCount(sequelize.models.UserHistory, 0)); + }); + it('onUpdate/onDestroy: should save to the historyDB' , function() { + return sequelize.models.User.create() + .then(assertCount(sequelize.models.UserHistory,0)) + .then((user) => { + user.name = "foo"; + return user.save(); + }) + .then(assertCount(sequelize.models.UserHistory,1)) + .then(user => user.destroy()) + .then(assertCount(sequelize.models.UserHistory,2)) + }); + it('onUpdate: should store the previous version to the historyDB' , function() { + return sequelize.models.User.create({name: "foo"}) + .then(assertCount(sequelize.models.UserHistory,0)) + .then((user) => { + user.name = "bar"; + return user.save(); + }) + .then(assertCount(sequelize.models.UserHistory,1)) + .then(() => sequelize.models.UserHistory.findAll()) + .then((users) => { + assert.equal(users.length,1, "only one entry in DB"); + assert.equal(users[0].name, "foo", "previous entry saved"); + }).then(user => sequelize.models.User.findOne()) + .then(user => user.destroy()) + .then(assertCount(sequelize.models.UserHistory,2)) + }); + it('onDelete: should store the previous version to the historyDB' , function() { + return sequelize.models.User.create({name: "foo"}) + .then(assertCount(sequelize.models.UserHistory,0)) + .then(user => user.destroy()) + .then(assertCount(sequelize.models.UserHistory,1)) + .then(() => sequelize.models.UserHistory.findAll()) + .then((users) => { + assert.equal(users.length,1, "only one entry in DB"); + assert.equal(users[0].name, "foo", "previous entry saved"); + }); + }); + }); + + describe('transactions', function() { + beforeEach(freshDB); + it('revert on failed transactions' , function() { + return sequelize.transaction() + .then((t) => { + var opts = {transaction: t}; + return sequelize.models.User.create({name: "not foo"},opts) + .then(assertCount(sequelize.models.UserHistory,0, opts)) + .then((user) => { + user.name = "foo"; + user.save(opts); + }) + .then(assertCount(sequelize.models.UserHistory,1, opts)) + .then(() => t.rollback()); + }) + .then(assertCount(sequelize.models.UserHistory,0)); + }); + }); + + describe('bulk update', function() { + beforeEach(freshDB); + it('should archive every entry', function() { + return sequelize.models.User.bulkCreate([{name: "foo1"},{name: "foo2"}]) + .then(assertCount(sequelize.models.UserHistory,0)) + .then(() => sequelize.models.User.update({ name: 'updated-foo' }, {where: {}})) + .then(assertCount(sequelize.models.UserHistory,2)) + }); + it('should revert under transactions', function() { + return sequelize.transaction() + .then(function(t) { + var opts = {transaction: t}; + return sequelize.models.User.bulkCreate([{name: "foo1"},{name: "foo2"}], opts) + .then(assertCount(sequelize.models.UserHistory,0,opts)) + .then(() => sequelize.models.User.update({ name: 'updated-foo' }, {where: {}, transaction: t})) + .then(assertCount(sequelize.models.UserHistory,2, opts)) + .then(() => t.rollback()); + }) + .then(assertCount(sequelize.models.UserHistory,0)); + }); + }); + + describe('bulk destroy/truncate', function() { + beforeEach(freshDB); + it('should archive every entry', function() { + return sequelize.models.User.bulkCreate([{name: "foo1"},{name: "foo2"}]) + .then(assertCount(sequelize.models.UserHistory,0)) + .then(() => sequelize.models.User.destroy({ + where: {}, + truncate: true // truncate the entire table + })) + .then(assertCount(sequelize.models.UserHistory,2)) + }); + it('should revert under transactions', function() { + return sequelize.transaction() + .then((t) => { + var opts = {transaction: t}; + return sequelize.models.User.bulkCreate([{name: "foo1"},{name: "foo2"}], opts) + .then(assertCount(sequelize.models.UserHistory,0,opts)) + .then(() => sequelize.models.User.destroy({ + where: {}, + truncate: true, // truncate the entire table + transaction: t + })) + .then(assertCount(sequelize.models.UserHistory,2, opts)) + .then(() => t.rollback()); + }) + .then(assertCount(sequelize.models.UserHistory,0)); + }); + }); + + describe('bulk destroy/truncate with associations', function() { + beforeEach(freshDBWithAssociations); + it('should archive every entry', function() { + return dataCreate() + .then(assertCount(sequelize.models.UserHistory,3)) + .then(() => sequelize.models.User.destroy({ + where: {}, + truncate: true // truncate the entire table + })) + .then(assertCount(sequelize.models.UserHistory,6)) + .then(() => sequelize.models.User.findOne()) + .then(u => u.getUserHistories()) + .then(uh => assert.exists(uh, 'The truncation did not break the associations')) + .catch(err => assert.exists(err,'The truncation broke the associations')); + }); + it('should fail to truncate', function() { + return dataCreate() + .then(() => sequelize.transaction()) + .then((t) => { + var opts = {transaction: t}; + assertCount(sequelize.models.UserHistory,6,opts); + return sequelize.models.User.destroy({ + where: {}, + truncate: true, // truncate the entire table + transaction: t + }) + .then(assertCount(sequelize.models.UserHistory,3,opts)) + .then(() => t.rollback()) + .catch(err => assert.exists(err)); + }) + .then(assertCount(sequelize.models.UserHistory,6)); + }); + }); + + describe('read-only ', function() { + beforeEach(freshDB); + it('should forbid updates' , function() { + var userUpdate = sequelize.models.UserHistory.create({name: 'bla00'}) + .then((uh) => uh.update({name: 'bla'})); + + return assert.isRejected(userUpdate, Error, "Validation error"); + }); + it('should forbid deletes' , function() { + var userUpdate = sequelize.models.UserHistory.create({name: 'bla00'}) + .then(uh => uh.destroy()); + + return assert.isRejected(userUpdate, Error, "Validation error"); + }); + }); + + describe('interference with the original model', function() { + beforeEach(freshDB); + it('shouldn\'t delete instance methods' , function() { + Fruit = Historical(sequelize.define('Fruit', { name: Sequelize.TEXT }), sequelize); + Fruit.prototype.sayHi = () => { return 2; } + + return sequelize.sync() + .then(() => Fruit.create()) + .then((f) => { + assert.isFunction(f.sayHi); + assert.equal(f.sayHi(), 2); + }); + }); + + it('shouldn\'t interfere with hooks of the model' , function() { + var triggered = 0; + Fruit = Historical(sequelize.define('Fruit', { name: Sequelize.TEXT }, { hooks:{ beforeCreate: function(){ triggered++; }}}), sequelize); + return sequelize.sync() + .then(() => Fruit.create()) + .then((f) => assert.equal(triggered, 1,"hook trigger count")); + }); + + it('shouldn\'t interfere with setters' , function() { + var triggered = 0; + Fruit = Historical(sequelize.define('Fruit', { + name: { + type: Sequelize.TEXT, + set: function() { triggered++; } + } + }), sequelize); + return sequelize.sync() + .then(() => Fruit.create({name: "apple"})) + .then((f) => assert.equal(triggered, 1,"hook trigger count")); + }); + }); + + describe('full mode', function() { + beforeEach(freshDBWithFullModeAndParanoid); + it('onCreate: should store the new version in history db' , function() { + return sequelize.models.User.create({ name: 'test' }) + .then(() => sequelize.models.UserHistory.findAll()) + .then((histories) => { + assert.equal(1, histories.length); + assert.equal('test', histories[0].name); + }); + }); + + it('onUpdate: should store the new version to the historyDB' , function() { + return sequelize.models.User.create({ name: 'test' }) + .then(user => user.update({ name: 'renamed' })) + .then(() => sequelize.models.UserHistory.findAll()) + .then((histories) => { + assert.equal(histories.length, 2, 'two entries in DB'); + assert.equal(histories[0].name, 'test', 'first version saved'); + assert.equal(histories[1].name, 'renamed', 'second version saved'); + }); + }); + + it('onDelete: should store the previous version to the historyDB' , function() { + return sequelize.models.User.create({ name: 'test' }) + .then(user => user.update({ name: 'renamed' })) + .then(user=> user.destroy()) + .then(() => sequelize.models.UserHistory.findAll()) + .then((histories) => { + assert.equal(histories.length, 3, 'three entries in DB'); + assert.equal(histories[0].name, 'test', 'first version saved'); + assert.equal(histories[1].name, 'renamed', 'second version saved'); + assert.notEqual(histories[2].deletedAt, null, 'deleted version saved'); + }); + }); + + it('onRestore: should store the new version to the historyDB' , function() { + return sequelize.models.User.create({ name: 'test' }) + .then(user => user.destroy()) + .then(user => user.restore()) + .then(() => sequelize.models.UserHistory.findAll()) + .then((histories) => { + assert.equal(histories.length, 3, 'three entries in DB'); + assert.equal(histories[0].name, 'test', 'first version saved'); + assert.notEqual(histories[1].deletedAt, null, 'deleted version saved'); + assert.equal(histories[2].deletedAt, null, 'restored version saved'); + }); + }); + + it('should revert on failed transactions, even when using after hooks' , function(){ + return sequelize.transaction() + .then((transaction) => { + var options = { transaction: transaction }; + + return sequelize.models.User.create({ name: 'test' }, options) + .then(user => user.destroy(options)) + .then(assertCount(sequelize.models.UserHistory, 2, options)) + .then(() => transaction.rollback()); + }) + .then(assertCount(sequelize.models.UserHistory,0)); + }); + }); });