fets
Version:
TypeScript HTTP Framework focusing on e2e type-safety, easy setup, performance & great developer experience
192 lines (191 loc) • 8.95 kB
JavaScript
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));
};
},
});
},
});
}