lion/packages/ajax/src/AjaxClass.js

223 lines
7.7 KiB
JavaScript

import { axios } from '@bundled-es-modules/axios';
import { LionSingleton } from '@lion/core';
import {
cancelInterceptorFactory,
cancelPreviousOnNewRequestInterceptorFactory,
addAcceptLanguageHeaderInterceptorFactory,
} from './interceptors.js';
import { jsonPrefixTransformerFactory } from './transformers.js';
/**
* `AjaxClass` creates the singleton instance {@link:ajax}. It is a promise based system for
* fetching data, based on [axios](https://github.com/axios/axios).
*/
export class AjaxClass extends LionSingleton {
/**
* @property {Object} proxy the axios instance that is bound to the AjaxClass instance
*/
/**
* @param {Object} config configuration for the AjaxClass instance
* @param {string} config.jsonPrefix prefixing the JSON string in this manner is used to help
* prevent JSON Hijacking. The prefix renders the string syntactically invalid as a script so
* that it cannot be hijacked. This prefix should be stripped before parsing the string as JSON.
* @param {string} config.lang language
* @param {string} config.languageHeader the Accept-Language request HTTP header advertises
* which languages the client is able to understand, and which locale variant is preferred.
* @param {string} config.cancelable if request can be canceled
* @param {string} config.cancelPreviousOnNewRequest prevents concurrent requests
*/
constructor(config) {
super();
this.__config = {
lang: document.documentElement.getAttribute('lang'),
languageHeader: true,
cancelable: false,
cancelPreviousOnNewRequest: false,
...config,
};
this.proxy = axios.create(this.__config);
this.__setupInterceptors();
this.requestInterceptors = [];
this.requestErrorInterceptors = [];
this.requestDataTransformers = [];
this.requestDataErrorTransformers = [];
this.responseDataTransformers = [];
this.responseDataErrorTransformers = [];
this.responseInterceptors = [];
this.responseErrorInterceptors = [];
this.__isInterceptorsSetup = false;
if (this.__config.languageHeader) {
this.requestInterceptors.push(addAcceptLanguageHeaderInterceptorFactory(this.__config.lang));
}
if (this.__config.cancelable) {
this.requestInterceptors.push(cancelInterceptorFactory(this));
}
if (this.__config.cancelPreviousOnNewRequest) {
this.requestInterceptors.push(cancelPreviousOnNewRequestInterceptorFactory());
}
if (this.__config.jsonPrefix) {
const transformer = jsonPrefixTransformerFactory(this.__config.jsonPrefix);
this.responseDataTransformers.push(transformer);
}
}
/**
* Sets the config for the instance
* TODO: rename to 'config', because of conflict with options() request method on axios
*/
set options(config) {
this.__config = config;
}
get options() {
return this.__config;
}
/**
* Dispatches a request
* @see https://github.com/axios/axios
* @param {AxiosRequestConfig} config the config specific for this request
* @returns {AxiosResponseSchema}
*/
request(url, config) {
return this.proxy.request.apply(this, [url, { ...this.__config, ...config }]);
}
/**
* Dispatches a {@link AxiosRequestConfig} with method 'get' predefined
* @param {string} url the endpoint location
* @param {AxiosRequestConfig} config the config specific for this request
* @returns {AxiosResponseSchema}
*/
get(url, config) {
return this.proxy.get.apply(this, [url, { ...this.__config, ...config }]);
}
/**
* Dispatches a {@link AxiosRequestConfig} with method 'delete' predefined
* @param {string} url the endpoint location
* @param {AxiosRequestConfig} config the config specific for this request
* @returns {AxiosResponseSchema}
*/
delete(url, config) {
return this.proxy.delete.apply(this, [url, { ...this.__config, ...config }]);
}
/**
* Dispatches a {@link AxiosRequestConfig} with method 'head' predefined
* @param {string} url the endpoint location
* @param {AxiosRequestConfig} config the config specific for this request
* @returns {AxiosResponseSchema}
*/
head(url, config) {
return this.proxy.head.apply(this, [url, { ...this.__config, ...config }]);
}
/**
* Dispatches a {@link AxiosRequestConfig} with method 'options' predefined
* @param {string} url the endpoint location
* @param {AxiosRequestConfig} config the config specific for this request
* @returns {AxiosResponseSchema}
* TODO: consider reenable after rename of options to config
*/
// options(url, config) {
// return this.proxy.options.apply(this, [url, { ...this.__config, ...config }]);
// }
/**
* Dispatches a {@link AxiosRequestConfig} with method 'post' predefined
* @param {string} url the endpoint location
* @param {Object} data the data to be sent to the endpoint
* @param {AxiosRequestConfig} config the config specific for this request
* @returns {AxiosResponseSchema}
*/
post(url, data, config) {
return this.proxy.post.apply(this, [url, data, { ...this.__config, ...config }]);
}
/**
* Dispatches a {@link AxiosRequestConfig} with method 'put' predefined
* @param {string} url the endpoint location
* @param {Object} data the data to be sent to the endpoint
* @param {AxiosRequestConfig} config the config specific for this request
* @returns {AxiosResponseSchema}
*/
put(url, data, config) {
return this.proxy.put.apply(this, [url, data, { ...this.__config, ...config }]);
}
/**
* Dispatches a {@link AxiosRequestConfig} with method 'patch' predefined
* @see https://github.com/axios/axios (Request Config)
* @param {string} url the endpoint location
* @param {Object} data the data to be sent to the endpoint
* @param {Object} config the config specific for this request.
* @returns {AxiosResponseSchema}
*/
patch(url, data, config) {
return this.proxy.patch.apply(this, [url, data, { ...this.__config, ...config }]);
}
__setupInterceptors() {
this.proxy.interceptors.request.use(
config => {
const configWithTransformers = this.__setupTransformers(config);
return this.requestInterceptors.reduce((c, i) => i(c), configWithTransformers);
},
error => {
this.requestErrorInterceptors.forEach(i => i(error));
return Promise.reject(error);
},
);
this.proxy.interceptors.response.use(
response => this.responseInterceptors.reduce((r, i) => i(r), response),
error => {
this.responseErrorInterceptors.forEach(i => i(error));
return Promise.reject(error);
},
);
}
__setupTransformers(config) {
const axiosTransformRequest = config.transformRequest[0];
const axiosTransformResponse = config.transformResponse[0];
return {
...config,
transformRequest: (data, headers) => {
try {
const ourData = this.requestDataTransformers.reduce((d, t) => t(d, headers), data);
// axios does a lot of smart things with the request that people rely on
// and must be the last request data transformer to do this job
return axiosTransformRequest(ourData, headers);
} catch (error) {
this.requestDataErrorTransformers.forEach(t => t(error));
throw error;
}
},
transformResponse: data => {
try {
// axios does a lot of smart things with the response that people rely on
// and must be the first response data transformer to do this job
const axiosData = axiosTransformResponse(data);
return this.responseDataTransformers.reduce((d, t) => t(d), axiosData);
} catch (error) {
this.responseDataErrorTransformers.forEach(t => t(error));
throw error;
}
},
};
}
}