From fc20f4806c8e91d8152ada0ddc0eb06e5d56611b Mon Sep 17 00:00:00 2001 From: Karolis Narkevicius Date: Fri, 20 Nov 2020 16:13:32 +0000 Subject: [PATCH 1/4] Put the caller's transaction back in the params on end/rollback This allows to wait for that trancsaction's promise to fulfil --- lib/hooks.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/hooks.js b/lib/hooks.js index f8f25dd..ead0cf6 100644 --- a/lib/hooks.js +++ b/lib/hooks.js @@ -13,6 +13,7 @@ const start = (options = {}) => { return hook => { const { transaction } = hook.params; + const parent = transaction const knex = transaction ? transaction.trx : options.getKnex(hook); if (!knex) { @@ -22,6 +23,10 @@ const start = (options = {}) => { return new Promise((resolve, reject) => { const transaction = {}; + if (parent) { + transaction.parent = parent + } + transaction.starting = true; transaction.promise = knex.transaction(trx => { transaction.trx = trx; @@ -51,9 +56,9 @@ const end = () => { return; } - const { trx, id, promise } = transaction; + const { trx, id, promise, parent } = transaction; - hook.params = { ...hook.params, transaction: undefined }; + hook.params = { ...hook.params, transaction: parent }; transaction.starting = false; return trx.commit() @@ -71,9 +76,9 @@ const rollback = () => { return; } - const { trx, id, promise } = transaction; + const { trx, id, promise, parent } = transaction; - hook.params = { ...hook.params, transaction: undefined }; + hook.params = { ...hook.params, transaction: parent }; transaction.starting = false; return trx.rollback(ROLLBACK) From 06d8449ab57753890fe8a9488ef8d0ecc92e1291 Mon Sep 17 00:00:00 2001 From: Karolis Narkevicius Date: Thu, 17 Dec 2020 11:41:13 +0000 Subject: [PATCH 2/4] Introduce a transaction.committed promise to be able to wait for end of trx --- lib/hooks.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/hooks.js b/lib/hooks.js index ead0cf6..6018dcb 100644 --- a/lib/hooks.js +++ b/lib/hooks.js @@ -13,7 +13,7 @@ const start = (options = {}) => { return hook => { const { transaction } = hook.params; - const parent = transaction + const parent = transaction; const knex = transaction ? transaction.trx : options.getKnex(hook); if (!knex) { @@ -24,7 +24,12 @@ const start = (options = {}) => { const transaction = {}; if (parent) { - transaction.parent = parent + transaction.parent = parent; + transaction.committed = parent.committed; + } else { + transaction.committed = new Promise(resolve => { + transaction.resolve = resolve; + }); } transaction.starting = true; @@ -63,6 +68,7 @@ const end = () => { return trx.commit() .then(() => promise) + .then(() => transaction.resolve && transaction.resolve(true)) .then(() => debug('ended transaction %s', id)) .then(() => hook); }; @@ -83,6 +89,7 @@ const rollback = () => { return trx.rollback(ROLLBACK) .then(() => promise) + .then(() => transaction.resolve && transaction.resolve(false)) .then(() => debug('rolled back transaction %s', id)) .then(() => hook); }; From e4a4eaff7f1bfe393d96a82a990b6d697d196ff4 Mon Sep 17 00:00:00 2001 From: Karolis Narkevicius Date: Thu, 17 Dec 2020 12:13:35 +0000 Subject: [PATCH 3/4] Add test for the new transaction.committed feature --- test/index.test.js | 94 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/test/index.test.js b/test/index.test.js index 171a932..c4afb59 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -494,6 +494,100 @@ describe('Feathers Knex Service', () => { expect(created).to.have.length(1); expect(created[0]).to.have.property('name', 'Success'); }); + + it('allows waiting for transaction to complete', async () => { + const app = feathers(); + + let seq = [] + + app.hooks({ + before: [ + transaction.start({ getKnex: () => db }), + context => { + seq.push(`${context.path}: waiting for trx to be committed`) + context.params.transaction.committed.then((success) => { + seq.push(`${context.path}: committed ${success}`) + }) + }, + async context => { + seq.push(`${context.path}: another hook`) + } + ], + after: [ + transaction.end(), + context => { seq.push(`${context.path}: trx ended`) } + ], + error: [ + transaction.rollback(), + context => { seq.push(`${context.path}: trx rolled back`) } + ] + }); + + app.use('/people', people); + + app.use('/test', { + create: async (data, params) => { + await app.service('/people').create({ name: 'Foo' }, { ...params }); + + if (data.throw) { + throw new TypeError('Deliberate'); + } + } + }); + + expect(seq).to.eql([]) + + await expect(app.service('/test').create({ throw: true })).to.eventually.be.rejectedWith(TypeError, 'Deliberate'); + + expect(seq).to.eql([ + 'test: waiting for trx to be committed', + 'test: another hook', + 'people: waiting for trx to be committed', + 'people: another hook', + 'people: trx ended', + 'test: committed false', + 'people: committed false', + 'test: trx rolled back', + ]) + + seq = [] + + expect(await app.service('/people').find()).to.have.length(0); + + expect(seq).to.eql([ + 'people: waiting for trx to be committed', + 'people: another hook', + 'people: committed true', + 'people: trx ended' + ]) + + seq = [] + + await expect(app.service('/test').create({})) + .to.eventually.be.fulfilled; + + expect(seq).to.eql([ + 'test: waiting for trx to be committed', + 'test: another hook', + 'people: waiting for trx to be committed', + 'people: another hook', + 'people: trx ended', + 'test: committed true', + 'people: committed true', + 'test: trx ended', + ]) + + seq = [] + + expect(await app.service('/people').find()).to.have.length(1); + + expect(seq).to.eql([ + 'people: waiting for trx to be committed', + 'people: another hook', + 'people: committed true', + 'people: trx ended' + ]) + }); }); testSuite(app, errors, 'users'); From 4f0485c2e34dcd7bc8cc885c6ff175b60985f52d Mon Sep 17 00:00:00 2001 From: Karolis Narkevicius Date: Thu, 17 Dec 2020 12:18:52 +0000 Subject: [PATCH 4/4] Add README for the new transaction.committed feature --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 8c6557a..35481e2 100644 --- a/README.md +++ b/README.md @@ -355,6 +355,27 @@ try { } ``` +### Waiting for transactions to complete + +Sometimes it can be important to know when the transaction has been completed (committed or rolled back). For example, we might want to wait for transaction to complete before we send out any realtime events. This can be done by awaiting on the `transaction.committed` promise which will always resolve to either `true` in case the transaction has been committed, or `false` in case the transaction has been rejected. + +```js +app.service('messages').publish((data, context) => { + const { transaction } = context.params + + if (transaction) { + const success = await transaction.committed + if (!success) { + return [] + } + } + + return app.channel(`rooms/${data.roomId}`) +}) +``` + +This also works with nested service calls and nested transactions. For example, if a service calls `transaction.start()` and passes the transaction param to a nested service call, which also calls `transaction.start()` in it's own hooks, they will share the top most `committed` promise that will resolve once all of the transactions have succesfully committed. + ## License Copyright (c) 2019