feat(ajax): add interceptors for parsed response JSON objects
This commit is contained in:
parent
a4f654a32b
commit
8d178a548a
5 changed files with 98 additions and 3 deletions
5
.changeset/flat-fireants-hang.md
Normal file
5
.changeset/flat-fireants-hang.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/ajax': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
add interceptors for parsed response JSON objects
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
- Allows globally registering request and response interceptors
|
- Allows globally registering request and response interceptors
|
||||||
- Throws on 4xx and 5xx status codes
|
- 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 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 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.
|
- 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 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
|
## Ajax class options
|
||||||
|
|
||||||
| Property | Type | Default Value | Description |
|
| Property | Type | Default Value | Description |
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { AjaxFetchError } from './AjaxFetchError.js';
|
||||||
* @typedef {import('../types/types.js').CachedRequestInterceptor} CachedRequestInterceptor
|
* @typedef {import('../types/types.js').CachedRequestInterceptor} CachedRequestInterceptor
|
||||||
* @typedef {import('../types/types.js').ResponseInterceptor} ResponseInterceptor
|
* @typedef {import('../types/types.js').ResponseInterceptor} ResponseInterceptor
|
||||||
* @typedef {import('../types/types.js').CachedResponseInterceptor} CachedResponseInterceptor
|
* @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').AjaxConfig} AjaxConfig
|
||||||
* @typedef {import('../types/types.js').CacheRequest} CacheRequest
|
* @typedef {import('../types/types.js').CacheRequest} CacheRequest
|
||||||
* @typedef {import('../types/types.js').CacheResponse} CacheResponse
|
* @typedef {import('../types/types.js').CacheResponse} CacheResponse
|
||||||
|
|
@ -31,7 +32,7 @@ function isFailedResponse(response) {
|
||||||
- Allows globally registering request and response interceptors
|
- Allows globally registering request and response interceptors
|
||||||
- Throws on 4xx and 5xx status codes
|
- 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 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.
|
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 accept-language header to requests based on application language
|
||||||
- Adds XSRF header to request if the cookie is present
|
- Adds XSRF header to request if the cookie is present
|
||||||
|
|
@ -63,6 +64,8 @@ export class Ajax {
|
||||||
this._requestInterceptors = [];
|
this._requestInterceptors = [];
|
||||||
/** @type {Array.<ResponseInterceptor|CachedResponseInterceptor>} */
|
/** @type {Array.<ResponseInterceptor|CachedResponseInterceptor>} */
|
||||||
this._responseInterceptors = [];
|
this._responseInterceptors = [];
|
||||||
|
/** @type {Array.<ResponseJsonInterceptor>} */
|
||||||
|
this._responseJsonInterceptors = [];
|
||||||
|
|
||||||
if (this.__config.addAcceptLanguage) {
|
if (this.__config.addAcceptLanguage) {
|
||||||
this.addRequestInterceptor(acceptLanguageRequestInterceptor);
|
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.
|
* Fetch by calling the registered request and response interceptors.
|
||||||
*
|
*
|
||||||
|
|
@ -202,7 +210,10 @@ export class Ajax {
|
||||||
const jsonInit = /** @type {RequestInit} */ (lionInit);
|
const jsonInit = /** @type {RequestInit} */ (lionInit);
|
||||||
const response = await this.fetch(info, jsonInit, true);
|
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 };
|
return { response, body };
|
||||||
}
|
}
|
||||||
|
|
@ -287,4 +298,18 @@ export class Ajax {
|
||||||
}
|
}
|
||||||
return interceptedResponse;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,49 @@ describe('Ajax', () => {
|
||||||
const { response } = await ajax.fetchJson('/foo');
|
const { response } = await ajax.fetchJson('/foo');
|
||||||
expect(response.ok);
|
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', () => {
|
describe('request and response interceptors', () => {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export interface AjaxConfig {
|
||||||
|
|
||||||
export type RequestInterceptor = (request: Request) => Promise<Request | Response>;
|
export type RequestInterceptor = (request: Request) => Promise<Request | Response>;
|
||||||
export type ResponseInterceptor = (response: Response) => Promise<Response>;
|
export type ResponseInterceptor = (response: Response) => Promise<Response>;
|
||||||
|
export type ResponseJsonInterceptor = (jsonObject: object, response: Response) => Promise<object>;
|
||||||
|
|
||||||
export interface CacheConfig {
|
export interface CacheConfig {
|
||||||
expires: string;
|
expires: string;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue