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

Commit d2428dd5 by Andy Edwards Committed by Sushant

feat(transaction): afterCommit hook (#10260)

1 parent 34e9fe16
...@@ -219,3 +219,45 @@ sequelize.transaction({ ...@@ -219,3 +219,45 @@ sequelize.transaction({
The `transaction` option goes with most other options, which are usually the first argument of a method. The `transaction` option goes with most other options, which are usually the first argument of a method.
For methods that take values, like `.create`, `.update()`, `.updateAttributes()` etc. `transaction` should be passed to the option in the second argument. For methods that take values, like `.create`, `.update()`, `.updateAttributes()` etc. `transaction` should be passed to the option in the second argument.
If unsure, refer to the API documentation for the method you are using to be sure of the signature. If unsure, refer to the API documentation for the method you are using to be sure of the signature.
## After commit hook
A `transaction` object allows tracking if and when it is committed.
An `afterCommit` hook can be added to both managed and unmanaged transaction objects:
```js
sequelize.transaction(t => {
t.afterCommit((transaction) => {
// Your logic
});
});
sequelize.transaction().then(t => {
t.afterCommit((transaction) => {
// Your logic
});
return t.commit();
})
```
The function passed to `afterCommit` can optionally return a promise that will resolve before the promise chain
that created the transaction resolves
`afterCommit` hooks are _not_ raised if a transaction is rolled back
`afterCommit` hooks do _not_ modify the return value of the transaction, unlike standard hooks
You can use the `afterCommit` hook in conjunction with model hooks to know when a instance is saved and available outside
of a transaction
```js
model.afterSave((instance, options) => {
if (options.transaction) {
// Save done within a transaction, wait until transaction is committed to
// notify listeners the instance has been saved
options.transaction.afterCommit(() => /* Notify */)
return;
}
// Save done outside a transaction, safe for callers to fetch the updated model
// Notify
...@@ -22,6 +22,7 @@ class Transaction { ...@@ -22,6 +22,7 @@ class Transaction {
constructor(sequelize, options) { constructor(sequelize, options) {
this.sequelize = sequelize; this.sequelize = sequelize;
this.savepoints = []; this.savepoints = [];
this._afterCommitHooks = [];
// get dialect specific transaction options // get dialect specific transaction options
const transactionOptions = sequelize.dialect.supports.transactionOptions || {}; const transactionOptions = sequelize.dialect.supports.transactionOptions || {};
...@@ -71,7 +72,11 @@ class Transaction { ...@@ -71,7 +72,11 @@ class Transaction {
return this.cleanup(); return this.cleanup();
} }
return null; return null;
}); }).tap(
() => Utils.Promise.each(
this._afterCommitHooks,
hook => Promise.resolve(hook.apply(this, [this])))
);
} }
/** /**
...@@ -189,6 +194,20 @@ class Transaction { ...@@ -189,6 +194,20 @@ class Transaction {
} }
/** /**
* A hook that is run after a transaction is committed
*
* @param {Function} fn A callback function that is called with the committed transaction
* @name afterCommit
* @memberof Sequelize.Transaction
*/
afterCommit(fn) {
if (!fn || typeof fn !== 'function') {
throw new Error('"fn" must be a function');
}
this._afterCommitHooks.push(fn);
}
/**
* Types can be set per-transaction by passing `options.type` to `sequelize.transaction`. * Types can be set per-transaction by passing `options.type` to `sequelize.transaction`.
* Default to `DEFERRED` but you can override the default type by passing `options.transactionType` in `new Sequelize`. * Default to `DEFERRED` but you can override the default type by passing `options.transactionType` in `new Sequelize`.
* Sqlite only. * Sqlite only.
......
...@@ -5,6 +5,7 @@ const chai = require('chai'), ...@@ -5,6 +5,7 @@ const chai = require('chai'),
Support = require(__dirname + '/support'), Support = require(__dirname + '/support'),
dialect = Support.getTestDialect(), dialect = Support.getTestDialect(),
Promise = require(__dirname + '/../../lib/promise'), Promise = require(__dirname + '/../../lib/promise'),
QueryTypes = require('../../lib/query-types'),
Transaction = require(__dirname + '/../../lib/transaction'), Transaction = require(__dirname + '/../../lib/transaction'),
sinon = require('sinon'), sinon = require('sinon'),
current = Support.sequelize; current = Support.sequelize;
...@@ -79,6 +80,31 @@ if (current.dialect.supports.transactions) { ...@@ -79,6 +80,31 @@ if (current.dialect.supports.transactions) {
}); });
}); });
it('supports running hooks when a transaction is commited', function() {
const hook = sinon.spy();
let transaction;
return expect(this.sequelize.transaction(t => {
transaction = t;
transaction.afterCommit(hook);
return this.sequelize.query('SELECT 1+1', { transaction, type: QueryTypes.SELECT });
}).then(() => {
expect(hook).to.have.been.calledOnce;
expect(hook).to.have.been.calledWith(transaction);
})
).to.eventually.be.fulfilled;
});
it('does not run hooks when a transaction is rolled back', function() {
const hook = sinon.spy();
return expect(this.sequelize.transaction(transaction => {
transaction.afterCommit(hook);
return Promise.reject(new Error('Rollback'));
})
).to.eventually.be.rejected.then(() => {
expect(hook).to.not.have.been.called;
});
});
//Promise rejection test is specifc to postgres //Promise rejection test is specifc to postgres
if (dialect === 'postgres') { if (dialect === 'postgres') {
it('do not rollback if already committed', function() { it('do not rollback if already committed', function() {
...@@ -199,6 +225,105 @@ if (current.dialect.supports.transactions) { ...@@ -199,6 +225,105 @@ if (current.dialect.supports.transactions) {
).to.be.rejectedWith('Transaction cannot be committed because it has been finished with state: commit'); ).to.be.rejectedWith('Transaction cannot be committed because it has been finished with state: commit');
}); });
it('should run hooks if a non-auto callback transaction is committed', function() {
const hook = sinon.spy();
let transaction;
return expect(
this.sequelize.transaction().then(t => {
transaction = t;
transaction.afterCommit(hook);
return t.commit().then(() => {
expect(hook).to.have.been.calledOnce;
expect(hook).to.have.been.calledWith(t);
});
}).catch(err => {
// Cleanup this transaction so other tests don't
// fail due to an open transaction
if (!transaction.finished) {
return transaction.rollback().then(() => {
throw err;
});
}
throw err;
})
).to.eventually.be.fulfilled;
});
it('should not run hooks if a non-auto callback transaction is rolled back', function() {
const hook = sinon.spy();
return expect(
this.sequelize.transaction().then(t => {
t.afterCommit(hook);
return t.rollback().then(() => {
expect(hook).to.not.have.been.called;
});
})
).to.eventually.be.fulfilled;
});
it('should throw an error if null is passed to afterCommit', function() {
const hook = null;
let transaction;
return expect(
this.sequelize.transaction().then(t => {
transaction = t;
transaction.afterCommit(hook);
return t.commit();
}).catch(err => {
// Cleanup this transaction so other tests don't
// fail due to an open transaction
if (!transaction.finished) {
return transaction.rollback().then(() => {
throw err;
});
}
throw err;
})
).to.eventually.be.rejectedWith('"fn" must be a function');
});
it('should throw an error if undefined is passed to afterCommit', function() {
const hook = undefined;
let transaction;
return expect(
this.sequelize.transaction().then(t => {
transaction = t;
transaction.afterCommit(hook);
return t.commit();
}).catch(err => {
// Cleanup this transaction so other tests don't
// fail due to an open transaction
if (!transaction.finished) {
return transaction.rollback().then(() => {
throw err;
});
}
throw err;
})
).to.eventually.be.rejectedWith('"fn" must be a function');
});
it('should throw an error if an object is passed to afterCommit', function() {
const hook = {};
let transaction;
return expect(
this.sequelize.transaction().then(t => {
transaction = t;
transaction.afterCommit(hook);
return t.commit();
}).catch(err => {
// Cleanup this transaction so other tests don't
// fail due to an open transaction
if (!transaction.finished) {
return transaction.rollback().then(() => {
throw err;
});
}
throw err;
})
).to.eventually.be.rejectedWith('"fn" must be a function');
});
it('does not allow commits after rollback', function() { it('does not allow commits after rollback', function() {
const self = this; const self = this;
return expect(self.sequelize.transaction().then(t => { return expect(self.sequelize.transaction().then(t => {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!