feat(ajax): add interceptors for parsed response JSON objects

This commit is contained in:
Mikhail Bashkirov 2024-08-20 18:59:33 +02:00 committed by Thijs Louisse
parent a4f654a32b
commit 8d178a548a
5 changed files with 98 additions and 3 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/ajax': minor
---
add interceptors for parsed response JSON objects

View file

@ -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 |

View file

@ -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.<ResponseInterceptor|CachedResponseInterceptor>} */
this._responseInterceptors = [];
/** @type {Array.<ResponseJsonInterceptor>} */
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<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;
}
}

View file

@ -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', () => {

View file

@ -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;