Skip to content

Instantly share code, notes, and snippets.

@Keireira
Created October 1, 2021 15:27
Show Gist options
  • Select an option

  • Save Keireira/bf932bcea1a8afd7508aa68167fe4ee1 to your computer and use it in GitHub Desktop.

Select an option

Save Keireira/bf932bcea1a8afd7508aa68167fe4ee1 to your computer and use it in GitHub Desktop.
API
import type { ObjectStr } from '@src/types';
import { ALLOWED_METHODS } from './requestKeys';
export type BackendError = {
status: number
error: string
name?: string
attributes?: ObjectStr
}
export type RequestParams = {
[key: string]: any
}
export type RequestConfig = {
[key: string]: any,
headers?: ObjectStr
}
export type Method = typeof ALLOWED_METHODS[number]
export type ApiResponse = {
request: Promise<any>,
abort: () => void
}
type RequestHandler = (
endpoint: string,
params?: RequestParams,
reqConfig?: RequestConfig
) => ApiResponse
export type ApiRequestsProxy = {
get: RequestHandler,
post: RequestHandler,
put: RequestHandler,
patch: RequestHandler,
delete: RequestHandler,
}
export type ApiArgs = {
method: Method,
endpoint: string
params: RequestParams
reqConfig: RequestConfig
controller: AbortController
}
export type ThrowError = {
status: string
raw: BackendError,
}
import { ALLOWED_METHODS } from '@api/requestKeys';
import { buildUrl, buildConfig } from '@api/requestHelpers';
import { onSuccessHandler, onErrorHandler } from '@api/requestHandlers';
import type { ApiArgs, Method, RequestParams, RequestConfig, ApiResponse, ApiRequestsProxy } from '@api/api.d';
const createRequest = async ({ method, endpoint, params, reqConfig, controller }: ApiArgs) => {
const { responseType = 'json', ...restConfig } = reqConfig;
const url = buildUrl(method, endpoint, params);
const config = await buildConfig(method, params, restConfig);
const request = new Request(url, {
...config,
signal: controller.signal,
});
const promise = fetch(request, config.params)
.then((response) => onSuccessHandler(response, responseType))
.catch(onErrorHandler);
return promise;
};
const sendRequest = (
method: Method,
endpoint: string = '',
params: RequestParams = {},
reqConfig: RequestConfig = {},
): ApiResponse => {
const controller = new AbortController();
return {
request: createRequest({
method, endpoint, params, reqConfig, controller,
}),
abort: () => controller.abort(),
};
};
// @ts-ignore
const api: ApiRequestsProxy = new Proxy({}, {
get(_, key: Method) {
const formattedMethod = String(key).toUpperCase();
// @ts-ignore
if (!ALLOWED_METHODS.includes(formattedMethod)) {
return () => {
throw new Error(`[API] Unknown method ${formattedMethod}!\nYou can use a ${ALLOWED_METHODS.join(', ')} methods only`);
};
}
// @ts-ignore
return sendRequest.bind(null, formattedMethod);
},
});
export * from './requestKeys';
export default api;
import ROUTES from '@routes';
import history from '@core/store/history';
import { getHeaders } from './requestHelpers';
import { netErrorsMap, FETCH_ABORTED } from './requestKeys';
import type { BackendError } from './api.d';
const parseResponse = async (response: Response) => {
try {
const data = await response.clone().json();
return data;
} catch (err) {
const text = await response.clone().text();
return JSON.parse(text || '{}');
}
};
export const onSuccessHandler = async (response: Response, responseType?: 'json' | 'blob') => {
// 403 block
if (response.status === 403 && responseType !== 'blob') {
history.push({
pathname: ROUTES.FORBIDDEN,
});
} else if (responseType === 'blob') {
const blob = await response.clone().blob();
return Promise.resolve(blob);
} else if (response.status === 403) {
return Promise.resolve();
}
// 401 block
if (response.status === 401 && !response.url.includes(ROUTES.AUTH.ROOT)) {
history.push({
pathname: ROUTES.AUTH.ROOT,
state: { reason: '401' },
});
}
// Parse block
const data = await parseResponse(response);
const headers = await getHeaders(response.headers);
// 204 (No Content) block
if (response.ok && response.status === 204) {
return Promise.resolve({
headers,
data: {},
meta: data.meta,
status: response.status,
});
}
// Generate a successful finality for 200/201/304 etc codes
if (response.ok) {
return Promise.resolve({
headers,
data: data.data || data,
meta: data.meta,
status: response.status,
});
}
// Generate an error
return Promise.reject({
status: response.status,
error: data.error || data.errors,
});
};
export const onErrorHandler = async (error: BackendError) => {
if (error.name === 'AbortError') {
throw {
status: FETCH_ABORTED,
raw: error,
};
}
throw {
status: netErrorsMap[error.status] || error.status,
raw: error,
};
};
import QS from 'qs';
import { get } from 'idb-keyval';
import { AUTH_TOKEN } from './requestKeys';
import type { Method, RequestParams, RequestConfig } from './api.d';
const buildHeaders = async (externalHeaders = {}, isFormData: boolean = false): Promise<Headers> => {
const authToken = await get(AUTH_TOKEN);
const headers = new Headers(externalHeaders);
if (!isFormData) {
headers.append('Content-Type', 'application/json');
}
if (authToken) {
headers.append('Authorization', authToken);
}
return headers;
};
export const getHeaders = async (headers: Headers) => {
const entries: RequestParams = {};
for (let header of headers.entries()) {
const [key, value] = header;
entries[key] = value;
}
return entries;
};
export const buildUrl = (method: Method, endpoint: string, params: RequestParams): string => {
let queryString = '';
if (method === 'GET') {
queryString = QS.stringify(params, { arrayFormat: 'brackets', addQueryPrefix: true });
}
return `${endpoint}${queryString}`;
};
export const buildConfig = async (method: Method, params: RequestParams, config: RequestConfig) => {
const isFormData = params instanceof FormData;
const headers = await buildHeaders(config.headers, isFormData);
const requestParams: RequestParams = { ...config, headers, method };
if (method !== 'GET' && !isFormData) {
requestParams.body = JSON.stringify(params);
} else if (isFormData) {
requestParams.body = params;
}
return requestParams;
};
import type { ObjectNumber } from '@src/types/common';
export const AUTH_TOKEN = '_TENEBRIA_AUTH_TOKEN' as const;
export const ALLOWED_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;
export const FETCH_ABORTED = 'fetch_aborted';
export const BAD_REQUEST = 'bad_request';
export const UNAUTHORIZED = 'unauthorized';
export const FORBIDDEN = 'forbidden';
export const NOT_FOUND = 'not_found';
export const REQUEST_TIMEOUT = 'request_timeout';
export const INTERNAL_ERROR = 'internal_error';
export const BAD_GATEWAY = 'bad_gateway';
export const UNAVAILABLE = 'unavailable';
export const GATEWAY_TIMEOUT = 'gateway_timeout';
export const UNPROCESSABLE = 'unprocessable';
export const netErrorsMap: ObjectNumber = {
[400]: BAD_REQUEST,
[401]: UNAUTHORIZED,
[403]: FORBIDDEN,
[404]: NOT_FOUND,
[408]: REQUEST_TIMEOUT,
[422]: UNPROCESSABLE,
[500]: INTERNAL_ERROR,
[502]: BAD_GATEWAY,
[503]: UNAVAILABLE,
[504]: GATEWAY_TIMEOUT,
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment