Content-Length: 736966 | pFad | https://snyk.io/redirect/github/sequelize/sequelize/commit/d2428dd580c1adb0a4c763a30499da2d87b19b3d

FD7E feat(transaction): afterCommit hook (#10260) · sequelize/sequelize@d2428dd · GitHub
Skip to content

Commit d2428dd

Browse files
jedwards1211sushantdhiman
authored andcommitted
feat(transaction): afterCommit hook (#10260)
1 parent 34e9fe1 commit d2428dd

File tree

3 files changed

+187
-1
lines changed

3 files changed

+187
-1
lines changed

docs/transactions.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,45 @@ sequelize.transaction({
219219
The `transaction` option goes with most other options, which are usually the first argument of a method.
220220
For methods that take values, like `.create`, `.update()`, `.updateAttributes()` etc. `transaction` should be passed to the option in the second argument.
221221
If unsure, refer to the API documentation for the method you are using to be sure of the signature.
222+
223+
## After commit hook
224+
225+
A `transaction` object allows tracking if and when it is committed.
226+
227+
An `afterCommit` hook can be added to both managed and unmanaged transaction objects:
228+
```js
229+
sequelize.transaction(t => {
230+
t.afterCommit((transaction) => {
231+
// Your logic
232+
});
233+
});
234+
235+
sequelize.transaction().then(t => {
236+
t.afterCommit((transaction) => {
237+
// Your logic
238+
});
239+
240+
return t.commit();
241+
})
242+
```
243+
244+
The function passed to `afterCommit` can optionally return a promise that will resolve before the promise chain
245+
that created the transaction resolves
246+
247+
`afterCommit` hooks are _not_ raised if a transaction is rolled back
248+
249+
`afterCommit` hooks do _not_ modify the return value of the transaction, unlike standard hooks
250+
251+
You can use the `afterCommit` hook in conjunction with model hooks to know when a instance is saved and available outside
252+
of a transaction
253+
254+
```js
255+
model.afterSave((instance, options) => {
256+
if (options.transaction) {
257+
// Save done within a transaction, wait until transaction is committed to
258+
// notify listeners the instance has been saved
259+
options.transaction.afterCommit(() => /* Notify */)
260+
return;
261+
}
262+
// Save done outside a transaction, safe for callers to fetch the updated model
263+
// Notify

lib/transaction.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class Transaction {
2222
constructor(sequelize, options) {
2323
this.sequelize = sequelize;
2424
this.savepoints = [];
25+
this._afterCommitHooks = [];
2526

2627
// get dialect specific transaction options
2728
const transactionOptions = sequelize.dialect.supports.transactionOptions || {};
@@ -71,7 +72,11 @@ class Transaction {
7172
return this.cleanup();
7273
}
7374
return null;
74-
});
75+
}).tap(
76+
() => Utils.Promise.each(
77+
this._afterCommitHooks,
78+
hook => Promise.resolve(hook.apply(this, [this])))
79+
);
7580
}
7681

7782
/**
@@ -188,6 +193,20 @@ class Transaction {
188193
}
189194
}
190195

196+
/**
197+
* A hook that is run after a transaction is committed
198+
*
199+
* @param {Function} fn A callback function that is called with the committed transaction
200+
* @name afterCommit
201+
* @memberof Sequelize.Transaction
202+
*/
203+
afterCommit(fn) {
204+
if (!fn || typeof fn !== 'function') {
205+
throw new Error('"fn" must be a function');
206+
}
207+
this._afterCommitHooks.push(fn);
208+
}
209+
191210
/**
192211
* Types can be set per-transaction by passing `options.type` to `sequelize.transaction`.
193212
* Default to `DEFERRED` but you can override the default type by passing `options.transactionType` in `new Sequelize`.

test/integration/transaction.test.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const chai = require('chai'),
55
Support = require(__dirname + '/support'),
66
dialect = Support.getTestDialect(),
77
Promise = require(__dirname + '/../../lib/promise'),
8+
QueryTypes = require('../../lib/query-types'),
89
Transaction = require(__dirname + '/../../lib/transaction'),
910
sinon = require('sinon'),
1011
current = Support.sequelize;
@@ -79,6 +80,31 @@ if (current.dialect.supports.transactions) {
7980
});
8081
});
8182

83+
it('supports running hooks when a transaction is commited', function() {
84+
const hook = sinon.spy();
85+
let transaction;
86+
return expect(this.sequelize.transaction(t => {
87+
transaction = t;
88+
transaction.afterCommit(hook);
89+
return this.sequelize.query('SELECT 1+1', { transaction, type: QueryTypes.SELECT });
90+
}).then(() => {
91+
expect(hook).to.have.been.calledOnce;
92+
expect(hook).to.have.been.calledWith(transaction);
93+
})
94+
).to.eventually.be.fulfilled;
95+
});
96+
97+
it('does not run hooks when a transaction is rolled back', function() {
98+
const hook = sinon.spy();
99+
return expect(this.sequelize.transaction(transaction => {
100+
transaction.afterCommit(hook);
101+
return Promise.reject(new Error('Rollback'));
102+
})
103+
).to.eventually.be.rejected.then(() => {
104+
expect(hook).to.not.have.been.called;
105+
});
106+
});
107+
82108
//Promise rejection test is specifc to postgres
83109
if (dialect === 'postgres') {
84110
it('do not rollback if already committed', function() {
@@ -199,6 +225,105 @@ if (current.dialect.supports.transactions) {
199225
).to.be.rejectedWith('Transaction cannot be committed because it has been finished with state: commit');
200226
});
201227

228+
it('should run hooks if a non-auto callback transaction is committed', function() {
229+
const hook = sinon.spy();
230+
let transaction;
231+
return expect(
232+
this.sequelize.transaction().then(t => {
233+
transaction = t;
234+
transaction.afterCommit(hook);
235+
return t.commit().then(() => {
236+
expect(hook).to.have.been.calledOnce;
237+
expect(hook).to.have.been.calledWith(t);
238+
});
239+
}).catch(err => {
240+
// Cleanup this transaction so other tests don't
241+
// fail due to an open transaction
242+
if (!transaction.finished) {
243+
return transaction.rollback().then(() => {
244+
throw err;
245+
});
246+
}
247+
throw err;
248+
})
249+
).to.eventually.be.fulfilled;
250+
});
251+
252+
it('should not run hooks if a non-auto callback transaction is rolled back', function() {
253+
const hook = sinon.spy();
254+
return expect(
255+
this.sequelize.transaction().then(t => {
256+
t.afterCommit(hook);
257+
return t.rollback().then(() => {
258+
expect(hook).to.not.have.been.called;
259+
});
260+
})
261+
).to.eventually.be.fulfilled;
262+
});
263+
264+
it('should throw an error if null is passed to afterCommit', function() {
265+
const hook = null;
266+
let transaction;
267+
return expect(
268+
this.sequelize.transaction().then(t => {
269+
transaction = t;
270+
transaction.afterCommit(hook);
271+
return t.commit();
272+
}).catch(err => {
273+
// Cleanup this transaction so other tests don't
274+
// fail due to an open transaction
275+
if (!transaction.finished) {
276+
return transaction.rollback().then(() => {
277+
throw err;
278+
});
279+
}
280+
throw err;
281+
})
282+
).to.eventually.be.rejectedWith('"fn" must be a function');
283+
});
284+
285+
it('should throw an error if undefined is passed to afterCommit', function() {
286+
const hook = undefined;
287+
let transaction;
288+
return expect(
289+
this.sequelize.transaction().then(t => {
290+
transaction = t;
291+
transaction.afterCommit(hook);
292+
return t.commit();
293+
}).catch(err => {
294+
// Cleanup this transaction so other tests don't
295+
// fail due to an open transaction
296+
if (!transaction.finished) {
297+
return transaction.rollback().then(() => {
298+
throw err;
299+
});
300+
}
301+
throw err;
302+
})
303+
).to.eventually.be.rejectedWith('"fn" must be a function');
304+
});
305+
306+
it('should throw an error if an object is passed to afterCommit', function() {
307+
const hook = {};
308+
let transaction;
309+
return expect(
310+
this.sequelize.transaction().then(t => {
311+
transaction = t;
312+
transaction.afterCommit(hook);
313+
return t.commit();
314+
}).catch(err => {
315+
// Cleanup this transaction so other tests don't
316+
// fail due to an open transaction
317+
if (!transaction.finished) {
318+
return transaction.rollback().then(() => {
319+
throw err;
320+
});
321+
}
322+
throw err;
323+
})
324+
).to.eventually.be.rejectedWith('"fn" must be a function');
325+
});
326+
202327
it('does not allow commits after rollback', function() {
203328
const self = this;
204329
return expect(self.sequelize.transaction().then(t => {

0 commit comments

Comments
 (0)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: https://snyk.io/redirect/github/sequelize/sequelize/commit/d2428dd580c1adb0a4c763a30499da2d87b19b3d

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy