UNPKG

fets

Version:

TypeScript HTTP Framework focusing on e2e type-safety, easy setup, performance & great developer experience

192 lines (191 loc) • 8.95 kB
import { stringify as qsStringify } from 'qs'; import { fetch, FormData } from '@whatwg-node/fetch'; import { iterateAsyncVoid } from '@whatwg-node/server'; import { EMPTY_OBJECT } from '../plugins/utils.js'; import { isBlob } from '../utils.js'; import { createClientTypedResponsePromise } from './clientResponse.js'; const qsOptions = { indices: false, arrayFormat: 'repeat', }; export class ClientValidationError extends Error { constructor(path, method, errors, response) { super(`Validation failed for ${method} ${path}`); this.path = path; this.method = method; this.errors = errors; this.response = response; } [Symbol.iterator]() { return this.errors[Symbol.iterator](); } } function useValidationErrors() { return { onResponse({ path, method, response }) { if (response.status === 400 && response.headers.get('x-error-type') === 'validation') { return response.json().then(resJson => { if (resJson.errors) { throw new ClientValidationError(path, method, resJson.errors, response); } }); } }, }; } export function createClient({ endpoint, fetchFn = fetch, plugins = [], globalParams, }) { plugins.unshift(useValidationErrors()); const onRequestInitHooks = []; const onFetchHooks = []; const onResponseHooks = []; for (const plugin of plugins) { if (plugin.onRequestInit) { onRequestInitHooks.push(plugin.onRequestInit); } if (plugin.onFetch) { onFetchHooks.push(plugin.onFetch); } if (plugin.onResponse) { onResponseHooks.push(plugin.onResponse); } } return new Proxy(EMPTY_OBJECT, { get(_target, path) { return new Proxy(EMPTY_OBJECT, { get(_target, method) { async function clientMethod(requestParams = {}) { // Merge globalParams with the current requestParams if (globalParams?.headers) { requestParams.headers = { ...globalParams.headers, ...requestParams.headers, }; } if (globalParams?.query) { requestParams.query = { ...globalParams.query, ...requestParams.query, }; } if (globalParams?.params) { requestParams.params = { ...globalParams.params, ...requestParams.params, }; } if (globalParams?.json) { requestParams.json = { ...globalParams.json, ...requestParams.json, }; } if (globalParams?.formData) { requestParams.formData = { ...globalParams.formData, ...requestParams.formData, }; } if (globalParams?.formUrlEncoded) { requestParams.formUrlEncoded = { ...globalParams.formUrlEncoded, ...requestParams.formUrlEncoded, }; } const { headers = {}, params: paramsBody, json: jsonBody, formData: formDataBody, formUrlEncoded: formUrlEncodedBody, query: queryBody, ...requestInitByUser } = requestParams; if (paramsBody) { for (const pathParamKey in paramsBody) { const value = paramsBody[pathParamKey]; if (value) { const encodedValue = encodeURIComponent(value); path = path .replace(`{${pathParamKey}}`, encodedValue) .replace(`:${pathParamKey}`, encodedValue); } } } if (!path.startsWith('/') && !path.startsWith('http')) { path = `/${path}`; } const requestInit = { ...requestInitByUser, method: method.toUpperCase(), headers, }; if (jsonBody) { requestInit.body = JSON.stringify(jsonBody); headers['Content-Type'] = 'application/json'; } if (formDataBody) { requestInit.body = new FormData(); for (const key in formDataBody) { const value = formDataBody[key]; if (value != null) { requestInit.body.append(key, isBlob(value) ? value : value.toString()); } } } if (formUrlEncodedBody) { requestInit.body = qsStringify(formUrlEncodedBody, qsOptions); requestInit.headers['Content-Type'] = 'application/x-www-form-urlencoded'; } let response; if (onRequestInitHooks.length) { const onRequestParamsHookPayload = { path, method, requestParams, requestInit, endResponse(res) { response = res; }, }; await iterateAsyncVoid(onRequestInitHooks, onRequestParamsHook => onRequestParamsHook(onRequestParamsHookPayload)); } if (response == null) { let finalUrl = path; if (endpoint && !path.startsWith('http')) { finalUrl = `${endpoint}${path}`; } if (queryBody) { const searchParams = qsStringify(queryBody, qsOptions); if (finalUrl.includes('?')) { finalUrl += '&' + searchParams; } else { finalUrl += '?' + searchParams; } } let currentFetchFn = fetchFn; if (onFetchHooks.length > 0) { const onFetchHookPayload = { url: finalUrl, init: requestInit, fetchFn: currentFetchFn, setFetchFn(newFetchFn) { currentFetchFn = newFetchFn; }, }; await iterateAsyncVoid(onFetchHooks, onFetchHook => onFetchHook(onFetchHookPayload)); } response = await currentFetchFn(finalUrl, requestInit); } if (onResponseHooks.length > 0) { const onResponsePayload = { path, method, requestParams, requestInit, response, }; await iterateAsyncVoid(onResponseHooks, onResponseHook => onResponseHook(onResponsePayload)); } return response; } return function wrappedClientMethod(requestParams = {}) { return createClientTypedResponsePromise(clientMethod(requestParams)); }; }, }); }, }); }