Created
October 1, 2021 15:27
-
-
Save Keireira/bf932bcea1a8afd7508aa68167fe4ee1 to your computer and use it in GitHub Desktop.
API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | |
| }; | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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