import axios from "axios"; import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance, } from "axios"; import { ApiError } from "./ApiError"; import type { ApiRequestOptions } from "./ApiRequestOptions"; import type { ApiResult } from "./ApiResult"; import { CancelablePromise } from "./CancelablePromise"; import type { OnCancel } from "./CancelablePromise"; import type { OpenAPIConfig } from "./OpenAPI"; export const isString = (value: unknown): value is string => { return typeof value === "string"; }; export const isStringWithValue = (value: unknown): value is string => { return isString(value) && value !== ""; }; export const isBlob = (value: any): value is Blob => { return value instanceof Blob; }; export const isFormData = (value: unknown): value is FormData => { return value instanceof FormData; }; export const isSuccess = (status: number): boolean => { return status >= 200 && status < 300; }; export const base64 = (str: string): string => { try { return btoa(str); } catch (err) { // @ts-ignore return Buffer.from(str).toString("base64"); } }; export const getQueryString = (params: Record): string => { const qs: string[] = []; const append = (key: string, value: unknown) => { qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); }; const encodePair = (key: string, value: unknown) => { if (value === undefined || value === null) { return; } if (value instanceof Date) { append(key, value.toISOString()); } else if (Array.isArray(value)) { value.forEach((v) => encodePair(key, v)); } else if (typeof value === "object") { Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v)); } else { append(key, value); } }; Object.entries(params).forEach(([key, value]) => encodePair(key, value)); return qs.length ? `?${qs.join("&")}` : ""; }; const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { const encoder = config.ENCODE_PATH || encodeURI; const path = options.url .replace("{api-version}", config.VERSION) .replace(/{(.*?)}/g, (substring: string, group: string) => { if (options.path?.hasOwnProperty(group)) { return encoder(String(options.path[group])); } return substring; }); const url = config.BASE + path; return options.query ? url + getQueryString(options.query) : url; }; export const getFormData = ( options: ApiRequestOptions, ): FormData | undefined => { if (options.formData) { const formData = new FormData(); const process = (key: string, value: unknown) => { if (isString(value) || isBlob(value)) { formData.append(key, value); } else { formData.append(key, JSON.stringify(value)); } }; Object.entries(options.formData) .filter(([, value]) => value !== undefined && value !== null) .forEach(([key, value]) => { if (Array.isArray(value)) { value.forEach((v) => process(key, v)); } else { process(key, value); } }); return formData; } return undefined; }; type Resolver = (options: ApiRequestOptions) => Promise; export const resolve = async ( options: ApiRequestOptions, resolver?: T | Resolver, ): Promise => { if (typeof resolver === "function") { return (resolver as Resolver)(options); } return resolver; }; export const getHeaders = async ( config: OpenAPIConfig, options: ApiRequestOptions, ): Promise> => { const [token, username, password, additionalHeaders] = await Promise.all([ // @ts-ignore resolve(options, config.TOKEN), // @ts-ignore resolve(options, config.USERNAME), // @ts-ignore resolve(options, config.PASSWORD), // @ts-ignore resolve(options, config.HEADERS), ]); const headers = Object.entries({ Accept: "application/json", ...additionalHeaders, ...options.headers, }) .filter(([, value]) => value !== undefined && value !== null) .reduce( (headers, [key, value]) => ({ ...headers, [key]: String(value), }), {} as Record, ); if (isStringWithValue(token)) { headers["Authorization"] = `Bearer ${token}`; } if (isStringWithValue(username) && isStringWithValue(password)) { const credentials = base64(`${username}:${password}`); headers["Authorization"] = `Basic ${credentials}`; } if (options.body !== undefined) { if (options.mediaType) { headers["Content-Type"] = options.mediaType; } else if (isBlob(options.body)) { headers["Content-Type"] = options.body.type || "application/octet-stream"; } else if (isString(options.body)) { headers["Content-Type"] = "text/plain"; } else if (!isFormData(options.body)) { headers["Content-Type"] = "application/json"; } } else if (options.formData !== undefined) { if (options.mediaType) { headers["Content-Type"] = options.mediaType; } } return headers; }; export const getRequestBody = (options: ApiRequestOptions): unknown => { if (options.body) { return options.body; } return undefined; }; export const sendRequest = async ( config: OpenAPIConfig, options: ApiRequestOptions, url: string, body: unknown, formData: FormData | undefined, headers: Record, onCancel: OnCancel, axiosClient: AxiosInstance, ): Promise> => { const controller = new AbortController(); let requestConfig: AxiosRequestConfig = { data: body ?? formData, headers, method: options.method, signal: controller.signal, url, withCredentials: config.WITH_CREDENTIALS, }; onCancel(() => controller.abort()); for (const fn of config.interceptors.request._fns) { requestConfig = await fn(requestConfig); } try { return await axiosClient.request(requestConfig); } catch (error) { const axiosError = error as AxiosError; if (axiosError.response) { return axiosError.response; } throw error; } }; export const getResponseHeader = ( response: AxiosResponse, responseHeader?: string, ): string | undefined => { if (responseHeader) { const content = response.headers[responseHeader]; if (isString(content)) { return content; } } return undefined; }; export const getResponseBody = (response: AxiosResponse): unknown => { if (response.status !== 204) { return response.data; } return undefined; }; export const catchErrorCodes = ( options: ApiRequestOptions, result: ApiResult, ): void => { const errors: Record = { 400: "Bad Request", 401: "Unauthorized", 402: "Payment Required", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable", 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict", 410: "Gone", 411: "Length Required", 412: "Precondition Failed", 413: "Payload Too Large", 414: "URI Too Long", 415: "Unsupported Media Type", 416: "Range Not Satisfiable", 417: "Expectation Failed", 418: "Im a teapot", 421: "Misdirected Request", 422: "Unprocessable Content", 423: "Locked", 424: "Failed Dependency", 425: "Too Early", 426: "Upgrade Required", 428: "Precondition Required", 429: "Too Many Requests", 431: "Request Header Fields Too Large", 451: "Unavailable For Legal Reasons", 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", 505: "HTTP Version Not Supported", 506: "Variant Also Negotiates", 507: "Insufficient Storage", 508: "Loop Detected", 510: "Not Extended", 511: "Network Authentication Required", ...options.errors, }; const error = errors[result.status]; if (error) { throw new ApiError(options, result, error); } if (!result.ok) { const errorStatus = result.status ?? "unknown"; const errorStatusText = result.statusText ?? "unknown"; const errorBody = (() => { try { return JSON.stringify(result.body, null, 2); } catch (e) { return undefined; } })(); throw new ApiError( options, result, `Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`, ); } }; /** * Request method * @param config The OpenAPI configuration object * @param options The request options from the service * @param axiosClient The axios client instance to use * @returns CancelablePromise * @throws ApiError */ export const request = ( config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios, ): CancelablePromise => { return new CancelablePromise(async (resolve, reject, onCancel) => { try { const url = getUrl(config, options); const formData = getFormData(options); const body = getRequestBody(options); const headers = await getHeaders(config, options); if (!onCancel.isCancelled) { let response = await sendRequest( config, options, url, body, formData, headers, onCancel, axiosClient, ); for (const fn of config.interceptors.response._fns) { response = await fn(response); } const responseBody = getResponseBody(response); const responseHeader = getResponseHeader( response, options.responseHeader, ); let transformedBody = responseBody; if (options.responseTransformer && isSuccess(response.status)) { transformedBody = await options.responseTransformer(responseBody); } const result: ApiResult = { url, ok: isSuccess(response.status), status: response.status, statusText: response.statusText, body: responseHeader ?? transformedBody, }; catchErrorCodes(options, result); resolve(result.body); } } catch (error) { reject(error); } }); };