Skip to content

Commit

Permalink
feat(ajax): add interceptors for parsed response JSON objects
Browse files Browse the repository at this point in the history
  • Loading branch information
bashmish committed Aug 20, 2024
1 parent 95b7460 commit 3e8c21f
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/flat-fireants-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lion/ajax': minor
---

add interceptors for parsed response JSON objects
23 changes: 22 additions & 1 deletion docs/fundamentals/tools/ajax/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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);
```

JSON response interceptors can be async and will be awaited.

## Ajax class options

| Property | Type | Default Value | Description |
Expand Down
29 changes: 27 additions & 2 deletions packages/ajax/src/Ajax.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -63,6 +64,8 @@ export class Ajax {
this._requestInterceptors = [];
/** @type {Array.<ResponseInterceptor|CachedResponseInterceptor>} */
this._responseInterceptors = [];
/** @type {Array.<ResponseJsonInterceptor>} */
this._responseJsonInterceptors = [];

if (this.__config.addAcceptLanguage) {
this.addRequestInterceptor(acceptLanguageRequestInterceptor);
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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 };
}
Expand Down Expand Up @@ -287,4 +298,18 @@ export class Ajax {
}
return interceptedResponse;
}

/**
* @param {object} jsonObject
* @param {Response} response
* @returns {Promise<object>}
*/
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;
}
}
43 changes: 43 additions & 0 deletions packages/ajax/test/Ajax.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/ajax/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface AjaxConfig {

export type RequestInterceptor = (request: Request) => Promise<Request | Response>;
export type ResponseInterceptor = (response: Response) => Promise<Response>;
export type ResponseJsonInterceptor = (jsonObject: object, response: Response) => Promise<object>;

export interface CacheConfig {
expires: string;
Expand Down

0 comments on commit 3e8c21f

Please sign in to comment.