UNPKG

fets

Version:

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

154 lines (153 loc) 6.92 kB
import { fetch, URLSearchParams } from '@whatwg-node/fetch'; 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 { async onResponse({ path, method, response }) { if (response.status === 400 && response.headers.get('x-error-type') === 'validation') { const resJson = await response.json(); if (resJson.errors) { throw new ClientValidationError(path, method, resJson.errors, response); } } }, }; } export function createClient({ endpoint, fetchFn = fetch, plugins = [] }) { 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({}, { get(_target, path) { return new Proxy({}, { get(_target, method) { return async function (requestParams = {}) { for (const pathParamKey in requestParams?.params || {}) { const value = requestParams?.params?.[pathParamKey]; if (value) { path = path.replace(`{${pathParamKey}}`, value).replace(`:${pathParamKey}`, value); } } if (!path.startsWith('/') && !path.startsWith('http')) { path = `/${path}`; } let searchParams; if (requestParams?.query) { searchParams = new URLSearchParams(); for (const queryParamKey in requestParams?.query || {}) { const value = requestParams?.query?.[queryParamKey]; if (value) { if (Array.isArray(value)) { for (const v of value) { searchParams.append(queryParamKey, v); } } else { searchParams.append(queryParamKey, value); } } } } const requestInit = { method, headers: requestParams?.headers || {}, }; if (requestParams?.json) { requestInit.body = JSON.stringify(requestParams.json); requestInit.headers['Content-Type'] = 'application/json'; } if (requestParams?.formData) { requestInit.body = requestParams.formData; } if (requestParams?.formUrlEncoded) { const urlSearchParams = new URLSearchParams(); for (const key in requestParams.formUrlEncoded) { const value = requestParams.formUrlEncoded[key]; if (value) { if (Array.isArray(value)) { for (const v of value) { urlSearchParams.append(key, v); } } else { urlSearchParams.append(key, value); } } } requestInit.body = urlSearchParams; requestInit.headers['Content-Type'] = 'application/x-www-form-urlencoded'; } let response; for (const onRequestParamsHook of onRequestInitHooks) { await onRequestParamsHook({ path, method, requestParams, requestInit, endResponse(res) { response = res; }, }); } let finalUrl = path; if (endpoint && !path.startsWith('http')) { finalUrl = `${endpoint}${path}`; } if (searchParams) { if (finalUrl.includes('?')) { finalUrl += '&' + searchParams.toString(); } else { finalUrl += '?' + searchParams.toString(); } } let currentFetchFn = fetchFn; for (const onFetchHook of onFetchHooks) { await onFetchHook({ url: finalUrl, init: requestInit, fetchFn: currentFetchFn, setFetchFn(newFetchFn) { currentFetchFn = newFetchFn; }, }); } response ||= await currentFetchFn(finalUrl, requestInit); for (const onResponseHook of onResponseHooks) { await onResponseHook({ path, method, requestParams, requestInit, response, }); } return response; }; }, }); }, }); }