diff --git a/CHANGELOG.md b/CHANGELOG.md index a694366..9293f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +#### Feature + +* add support for a read hook ([#63](https://github.com/ExpediaGroup/service-client/pull/63)) + ## [3.1.0](https://github.com/expediagroup/service-client/compare/v3.0.1...v3.1.0) (2021-09-09) #### Feature diff --git a/README.md b/README.md index d1185b5..610f06e 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ Service-Client plugins provide the ability to hook into different spots of a req Plugins are registered with `ServiceClient.mergeConfig({plugins: []})` or `ServiceClient.use([])` and affect all Service-Client instances created thereafter. ### Overview -A plugin exports a function which, when executed, returns an object containing a set of hook functions to be ran during a request's lifecycle. See all available hooks [below](#available-plugins). +A plugin exports a function which, when executed, returns an object containing a set of hook functions to be run during a request's lifecycle. See all available hooks [below](#available-plugins). Example: ```js @@ -322,7 +322,7 @@ async function plugin({client, context, plugins}) { * This hook is special. The only value that should be returned here is * a response object. This is helpful for authentication plugins that * want to retry a request if an invalid response was received. A request - * made within this hook could return it's response object here. + * made within this hook could return its response object here. * * There's a catch however. Since response objects from multiple hooks * cannot be merged, only the first response object will be taken. All @@ -335,6 +335,24 @@ async function plugin({client, context, plugins}) { }, + /** + * This hook is special. This hook behaves just like response but can be used to + * validate data after being read. The only value that should be returned here is + * a response object. This is helpful for authentication plugins that + * want to retry a request if an invalid response was received. A request + * made within this hook could return its response object here. + * + * There's a catch however. Since response objects from multiple hooks + * cannot be merged, only the first response object will be taken. All + * other returned responses are discarded. + * + * @param {object} data - data provided to the hook on every request + * @param {object} data.response - the response received from Wreck after being read + */ + async read(data) { + + }, + /** * @param {object} data - data provided to the hook on every request * @param {Error} data.error - the error object resulting from timeouts, read errors, etc diff --git a/lib/client.js b/lib/client.js index c87502b..ab8e4cc 100644 --- a/lib/client.js +++ b/lib/client.js @@ -163,6 +163,11 @@ class ServiceClient { await hooks.error({ context, clientId, requestId, ts, servicename, operation, error }) } } + if (hooks.read) { + reqHooks.read = async (id, response, ts = Date.now()) => { + return hooks.read({ context, clientId, requestId, ts, servicename, operation, response }) + } + } const doRequest = async function () { if (self._circuitState.open) { diff --git a/lib/hooks.js b/lib/hooks.js index 7e5a772..43df2ef 100644 --- a/lib/hooks.js +++ b/lib/hooks.js @@ -115,13 +115,20 @@ async function init (options) { return finalData } else - // for the `response` hook, return the first non-falsy value if (hookName === 'response') { const response = results.find((result) => result) debug('%s(): first result: %j', hookName, response) + return response + } else + // for the `read` hook, return the first non-falsy value + if (hookName === 'read') { + const response = results.find((result) => result) + + debug('%s(): first result: %j', hookName, response) + return response } } diff --git a/lib/http/request.js b/lib/http/request.js index 646334c..64e0de6 100644 --- a/lib/http/request.js +++ b/lib/http/request.js @@ -133,7 +133,12 @@ const makeRequest = async function (method, path, options, hooks) { return response } - response.payload = await Read(response, options.readOptions) + if (response.payload === undefined) { + response.payload = await Read(response, options.readOptions) + if (hooks.read) { + response = await hooks.read(options.id, response) || response + } + } debug('makeRequest(): payload read') oncomplete() diff --git a/test/unit/hooks/standalone.test.js b/test/unit/hooks/standalone.test.js index ccd7091..ca1ed63 100644 --- a/test/unit/hooks/standalone.test.js +++ b/test/unit/hooks/standalone.test.js @@ -43,7 +43,8 @@ describe('Using ServiceClient in a standalone context', () => { response: suite.sandbox.spy(), error: suite.sandbox.spy(), stats: suite.sandbox.spy(), - end: suite.sandbox.spy() + end: suite.sandbox.spy(), + read: suite.sandbox.spy() } ServiceClient.use(() => { return spies @@ -65,8 +66,9 @@ describe('Using ServiceClient in a standalone context', () => { Sinon.assert.notCalled(spies.error) // no error for this test Sinon.assert.calledOnce(spies.stats) Sinon.assert.calledOnce(spies.end) + Sinon.assert.calledOnce(spies.read) - Sinon.assert.callOrder(spies.request, spies.init, spies.socket, spies.response, spies.stats, spies.end) + Sinon.assert.callOrder(spies.request, spies.init, spies.socket, spies.response, spies.read, spies.stats, spies.end) }) it('should call hooks (error thrown in plugin initialization)', async function () {