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 diff --git a/lib/hooks.js b/lib/hooks.js index f8f25dd..6018dcb 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,15 @@ const start = (options = {}) => { return new Promise((resolve, reject) => { const transaction = {}; + if (parent) { + transaction.parent = parent; + transaction.committed = parent.committed; + } else { + transaction.committed = new Promise(resolve => { + transaction.resolve = resolve; + }); + } + transaction.starting = true; transaction.promise = knex.transaction(trx => { transaction.trx = trx; @@ -51,13 +61,14 @@ 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() .then(() => promise) + .then(() => transaction.resolve && transaction.resolve(true)) .then(() => debug('ended transaction %s', id)) .then(() => hook); }; @@ -71,13 +82,14 @@ 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) .then(() => promise) + .then(() => transaction.resolve && transaction.resolve(false)) .then(() => debug('rolled back transaction %s', id)) .then(() => hook); }; 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');