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

Commit d2428dd5 by Andy Edwards Committed by Sushant

feat(transaction): afterCommit hook (#10260)

1 parent 34e9fe16
......@@ -219,3 +219,45 @@ sequelize.transaction({
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.
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 {
constructor(sequelize, options) {
this.sequelize = sequelize;
this.savepoints = [];
this._afterCommitHooks = [];
// get dialect specific transaction options
const transactionOptions = sequelize.dialect.supports.transactionOptions || {};
......@@ -71,7 +72,11 @@ class Transaction {
return this.cleanup();
}
return null;
});
}).tap(
() => Utils.Promise.each(
this._afterCommitHooks,
hook => Promise.resolve(hook.apply(this, [this])))
);
}
/**
......@@ -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`.
* Default to `DEFERRED` but you can override the default type by passing `options.transactionType` in `new Sequelize`.
* Sqlite only.
......
......@@ -5,6 +5,7 @@ const chai = require('chai'),
Support = require(__dirname + '/support'),
dialect = Support.getTestDialect(),
Promise = require(__dirname + '/../../lib/promise'),
QueryTypes = require('../../lib/query-types'),
Transaction = require(__dirname + '/../../lib/transaction'),
sinon = require('sinon'),
current = Support.sequelize;
......@@ -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
if (dialect === 'postgres') {
it('do not rollback if already committed', function() {
......@@ -199,6 +225,105 @@ if (current.dialect.supports.transactions) {
).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() {
const self = this;
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!