@hyper-fetch/core
Version:
Cache, Queue and Persist your requests no matter if you are online or offline!
196 lines (168 loc) • 6 kB
text/typescript
import {
QueryParamType,
QueryParamsType,
QueryParamValuesType,
ResponseReturnType,
QueryStringifyOptionsType,
AdapterInstance,
} from "adapter";
import { RequestInstance } from "request";
import { stringifyDefaultOptions } from "client";
import { NegativeTypes } from "types";
import { RequestInterceptorType, ResponseInterceptorType } from "./client.types";
import { hasWindow } from "managers";
// Utils
export const stringifyValue = (response: string | unknown): string => {
try {
return JSON.stringify(response as string);
} catch (err) {
return "";
}
};
export const interceptRequest = async (interceptors: RequestInterceptorType[], request: RequestInstance) => {
let newRequest = request;
if (!request.requestOptions.disableRequestInterceptors) {
// eslint-disable-next-line no-restricted-syntax
for (const interceptor of interceptors) {
// eslint-disable-next-line no-await-in-loop
newRequest = (await interceptor(request)) as RequestInstance;
if (!newRequest) throw new Error("Request modifier must return request");
}
}
return newRequest;
};
export const interceptResponse = async <GlobalErrorType, Adapter extends AdapterInstance>(
interceptors: ResponseInterceptorType[],
response: ResponseReturnType<any, GlobalErrorType, Adapter>,
request: RequestInstance,
) => {
let newResponse = response;
if (!request.requestOptions.disableResponseInterceptors) {
// eslint-disable-next-line no-restricted-syntax
for (const interceptor of interceptors) {
// eslint-disable-next-line no-await-in-loop
newResponse = await interceptor(response, request);
if (!newResponse) throw new Error("Response modifier must return data");
}
}
return newResponse;
};
// Mappers
export const getAdapterHeaders = (request: RequestInstance) => {
const isFormData = hasWindow() && request.data instanceof FormData;
const headers: HeadersInit = {};
if (!isFormData) headers["Content-Type"] = "application/json";
Object.assign(headers, request.headers);
return headers as HeadersInit;
};
export const getAdapterPayload = (data: unknown): string | FormData => {
const isFormData = hasWindow() && data instanceof FormData;
if (isFormData) return data;
return stringifyValue(data);
};
// Stringify
const isValidValue = (options: QueryStringifyOptionsType) => {
return (value: QueryParamType) => {
const { skipNull, skipEmptyString } = options;
if (skipEmptyString && value === undefined) {
return false;
}
if (skipEmptyString && value === "") {
return false;
}
if (skipNull && value === null) {
return false;
}
return true;
};
};
const encodeValue = (
value: string,
{ encode, strict }: Pick<QueryStringifyOptionsType, "encode" | "strict">,
): string => {
if (encode && strict) {
return encodeURIComponent(value).replace(/[!'()*]/g, (s) => `%${s.charCodeAt(0).toString(16).toUpperCase()}`);
}
if (encode) {
return encodeURIComponent(value);
}
return value;
};
const encodeParams = (key: string, value: QueryParamType, options: QueryStringifyOptionsType) => {
const shouldSkip = !isValidValue(options)(value);
if (!key || shouldSkip) {
return "";
}
const parsedValue = () => {
if (value instanceof Date) {
return options.dateParser?.(value) || value.toISOString();
}
if (typeof value === "object" && !Array.isArray(value)) {
return options.objectParser?.(value) || JSON.stringify(value);
}
return String(value);
};
return `${encodeValue(key, options)}=${encodeValue(parsedValue(), options)}`;
};
const encodeArray = (key: string, array: Array<QueryParamValuesType>, options: QueryStringifyOptionsType): string => {
const { arrayFormat, arraySeparator } = options;
return array
.filter(isValidValue(options))
.reduce<string[]>((acc, value, index) => {
switch (arrayFormat) {
case "index": {
const keyValue = `${encodeValue(key, options)}[${encodeValue(String(index), options)}]=`;
acc.push(`${keyValue}${encodeValue(String(value), options)}`);
break;
}
case "bracket": {
const keyValue = `${encodeValue(key, options)}[]=`;
acc.push(`${keyValue}${encodeValue(String(value), options)}`);
break;
}
case "comma": {
const keyValue = (!acc.length && `${encodeValue(key, options)}=`) || "";
return [[...acc, `${keyValue}${encodeValue(String(value), options)}`].join(",")];
}
case "separator": {
const keyValue = (!acc.length && `${encodeValue(key, options)}=`) || "";
return [[...acc, `${keyValue}${encodeValue(String(value), options)}`].join(arraySeparator || "|")];
}
case "bracket-separator": {
const keyValue = (!acc.length && `${encodeValue(key, options)}[]=`) || "";
return [[...acc, `${keyValue}${encodeValue(String(value), options)}`].join(arraySeparator || "|")];
}
default: {
const keyValue = `${encodeValue(key, options)}=`;
acc.push(`${keyValue}${encodeValue(String(value), options)}`);
}
}
return acc;
}, [])
.join("&");
};
export const stringifyQueryParams = (
queryParams: QueryParamsType | string | NegativeTypes,
options: QueryStringifyOptionsType = stringifyDefaultOptions,
): string => {
if (!queryParams || !Object.keys(queryParams).length) {
return "";
}
if (typeof queryParams === "string") {
const hasQuestionMark = queryParams[0] === "?";
return hasQuestionMark ? queryParams : `?${queryParams}`;
}
const stringified = Object.entries(queryParams)
.map(([key, value]): string => {
if (Array.isArray(value)) {
return encodeArray(key, value, options);
}
return encodeParams(key, value, options);
})
.filter(Boolean)
.join("&");
if (stringified) {
return `?${stringified}`;
}
return "";
};