From 8d178a548a5b02907bb0b9ad724d1d55d32d5eb2 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Tue, 20 Aug 2024 18:59:33 +0200 Subject: [PATCH] feat(ajax): add interceptors for parsed response JSON objects --- .changeset/flat-fireants-hang.md | 5 +++ docs/fundamentals/tools/ajax/overview.md | 23 ++++++++++++- packages/ajax/src/Ajax.js | 29 ++++++++++++++-- packages/ajax/test/Ajax.test.js | 43 ++++++++++++++++++++++++ packages/ajax/types/types.ts | 1 + 5 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 .changeset/flat-fireants-hang.md diff --git a/.changeset/flat-fireants-hang.md b/.changeset/flat-fireants-hang.md new file mode 100644 index 000000000..a929ed661 --- /dev/null +++ b/.changeset/flat-fireants-hang.md @@ -0,0 +1,5 @@ +--- +'@lion/ajax': minor +--- + +add interceptors for parsed response JSON objects diff --git a/docs/fundamentals/tools/ajax/overview.md b/docs/fundamentals/tools/ajax/overview.md index 9fb4ff121..0226d9618 100644 --- a/docs/fundamentals/tools/ajax/overview.md +++ b/docs/fundamentals/tools/ajax/overview.md @@ -5,7 +5,7 @@ - Allows globally registering request and response interceptors - Throws on 4xx and 5xx status codes - Supports caching, so a request can be prevented from reaching to network, by returning the cached response. -- Supports JSON with `ajax.fetchJSON` by automatically serializing request body and deserializing response payload as JSON, and adding the correct Content-Type and Accept headers. +- Supports JSON with `ajax.fetchJson` by automatically serializing request body and deserializing response payload as JSON, and adding the correct Content-Type and Accept headers. - Adds accept-language header to requests based on application language - Adds XSRF header to request if the cookie is present and the request is for a mutable action (POST/PUT/PATCH/DELETE) and if the origin is the same as current origin or the request origin is in the xsrfTrustedOrigins list. @@ -124,6 +124,27 @@ ajax.addResponseInterceptor(rewriteFoo); Response interceptors can be async and will be awaited. +### Response JSON object interceptors + +A response JSON object interceptor is a function that takes a successfully parsed response JSON object and `response` object and returns a new response JSON object. +It's used only when the request is made with the `fetchJson` method, providing a convenience API to directly modify or inspect the parsed JSON without the need to parse it and handle errors manually. + +```js +async function interceptJson(jsonObject, response) { + if (response.url === '/my-api') { + return { + ...jsonObject, + changed: true, + }; + } + return jsonObject; +} + +ajax.addResponseJsonInterceptor(interceptJson); +``` + +Response JSON object interceptors can be async and will be awaited. + ## Ajax class options | Property | Type | Default Value | Description | diff --git a/packages/ajax/src/Ajax.js b/packages/ajax/src/Ajax.js index f79da2516..cdd236dad 100644 --- a/packages/ajax/src/Ajax.js +++ b/packages/ajax/src/Ajax.js @@ -11,6 +11,7 @@ import { AjaxFetchError } from './AjaxFetchError.js'; * @typedef {import('../types/types.js').CachedRequestInterceptor} CachedRequestInterceptor * @typedef {import('../types/types.js').ResponseInterceptor} ResponseInterceptor * @typedef {import('../types/types.js').CachedResponseInterceptor} CachedResponseInterceptor + * @typedef {import('../types/types.js').ResponseJsonInterceptor} ResponseJsonInterceptor * @typedef {import('../types/types.js').AjaxConfig} AjaxConfig * @typedef {import('../types/types.js').CacheRequest} CacheRequest * @typedef {import('../types/types.js').CacheResponse} CacheResponse @@ -31,7 +32,7 @@ function isFailedResponse(response) { - Allows globally registering request and response interceptors - Throws on 4xx and 5xx status codes - Supports caching, so a request can be prevented from reaching to network, by returning the cached response. -- Supports JSON with `ajax.fetchJSON` by automatically serializing request body and +- Supports JSON with `ajax.fetchJson` by automatically serializing request body and deserializing response payload as JSON, and adding the correct Content-Type and Accept headers. - Adds accept-language header to requests based on application language - Adds XSRF header to request if the cookie is present @@ -63,6 +64,8 @@ export class Ajax { this._requestInterceptors = []; /** @type {Array.} */ this._responseInterceptors = []; + /** @type {Array.} */ + this._responseJsonInterceptors = []; if (this.__config.addAcceptLanguage) { this.addRequestInterceptor(acceptLanguageRequestInterceptor); @@ -127,6 +130,11 @@ export class Ajax { ); } + /** @param {ResponseJsonInterceptor} responseJsonInterceptor */ + addResponseJsonInterceptor(responseJsonInterceptor) { + this._responseJsonInterceptors.push(responseJsonInterceptor); + } + /** * Fetch by calling the registered request and response interceptors. * @@ -202,7 +210,10 @@ export class Ajax { const jsonInit = /** @type {RequestInit} */ (lionInit); const response = await this.fetch(info, jsonInit, true); - const body = await this.__parseBody(response); + let body = await this.__parseBody(response); + if (typeof body === 'object') { + body = await this.__interceptResponseJson(body, response); + } return { response, body }; } @@ -287,4 +298,18 @@ export class Ajax { } return interceptedResponse; } + + /** + * @param {object} jsonObject + * @param {Response} response + * @returns {Promise} + */ + async __interceptResponseJson(jsonObject, response) { + let interceptedJsonObject = jsonObject; + for (const intercept of this._responseJsonInterceptors) { + // eslint-disable-next-line no-await-in-loop + interceptedJsonObject = await intercept(interceptedJsonObject, response); + } + return interceptedJsonObject; + } } diff --git a/packages/ajax/test/Ajax.test.js b/packages/ajax/test/Ajax.test.js index 0a6aa0499..1aaa51732 100644 --- a/packages/ajax/test/Ajax.test.js +++ b/packages/ajax/test/Ajax.test.js @@ -271,6 +271,49 @@ describe('Ajax', () => { const { response } = await ajax.fetchJson('/foo'); expect(response.ok); }); + + describe('addResponseJsonInterceptor', () => { + it('adds a function which intercepts the parsed response body JSON object', async () => { + ajax.addResponseJsonInterceptor(async jsonObject => ({ + ...jsonObject, + intercepted: true, + })); + fetchStub.returns(Promise.resolve(new Response('{"a":1,"b":2}', responseInit()))); + + const response = await ajax.fetchJson('/foo'); + + expect(response.body).to.eql({ a: 1, b: 2, intercepted: true }); + }); + + it('does not serialize/deserialize the JSON object after intercepting', async () => { + let interceptorJsonObject; + ajax.addResponseJsonInterceptor(async jsonObject => { + interceptorJsonObject = { + ...jsonObject, + }; + return interceptorJsonObject; + }); + fetchStub.returns(Promise.resolve(new Response('{"a":1,"b":2}', responseInit()))); + + const response = await ajax.fetchJson('/foo'); + + expect(response.body).to.equal(interceptorJsonObject); + }); + + it('provides response object to the interceptor', async () => { + let interceptorResponse; + ajax.addResponseJsonInterceptor(async (jsonObject, response) => { + interceptorResponse = response; + return jsonObject; + }); + const mockedResponse = new Response('{"a":1,"b":2}', responseInit()); + fetchStub.returns(Promise.resolve(mockedResponse)); + + await ajax.fetchJson('/foo'); + + expect(interceptorResponse).to.equal(mockedResponse); + }); + }); }); describe('request and response interceptors', () => { diff --git a/packages/ajax/types/types.ts b/packages/ajax/types/types.ts index 228ccef51..b30e53802 100644 --- a/packages/ajax/types/types.ts +++ b/packages/ajax/types/types.ts @@ -22,6 +22,7 @@ export interface AjaxConfig { export type RequestInterceptor = (request: Request) => Promise; export type ResponseInterceptor = (response: Response) => Promise; +export type ResponseJsonInterceptor = (jsonObject: object, response: Response) => Promise; export interface CacheConfig { expires: string;