fets
Version:
TypeScript HTTP Framework focusing on e2e type-safety, easy setup, performance & great developer experience
144 lines (143 loc) • 6.3 kB
JavaScript
import { TypeGuard } from '@sinclair/typebox';
import { TypeCompiler } from '@sinclair/typebox/compiler';
import { Value } from '@sinclair/typebox/value';
import { HTTPError } from '@whatwg-node/server';
import { getHeadersObj } from './utils.js';
function createValidateFn(schema) {
try {
const validator = TypeCompiler.Compile(schema);
return function getErrors(data) {
return validator.Errors(data);
};
}
catch (e) {
return function getErrors(data) {
return Value.Errors(schema, data);
};
}
}
function sanitizeError({ schema, type, ...error }, name) {
return {
...error,
name,
};
}
export function useTypeBox({ components = {}, } = {}) {
const validateFnBySchema = new WeakMap();
function getValidateFn(schema) {
let validateFn = validateFnBySchema.get(schema);
if (!validateFn) {
validateFn = createValidateFn({
...schema,
components,
});
validateFnBySchema.set(schema, validateFn);
}
return validateFn;
}
return {
onRouteHandle({ route: { schemas }, request }) {
if (schemas?.request?.headers && TypeGuard.IsSchema(schemas.request.headers)) {
const validateFn = getValidateFn(schemas.request.headers);
const headersObj = getHeadersObj(request.headers);
const errors = [...validateFn(headersObj)].map(error => sanitizeError(error, 'headers'));
if (errors.length) {
throw new HTTPError(400, 'Bad Request', {
'x-error-type': 'validation',
}, {
errors,
});
}
}
if (schemas?.request?.params && TypeGuard.IsSchema(schemas.request.params)) {
const validateFn = getValidateFn(schemas.request.params);
const errors = [...validateFn(request.params)].map(error => sanitizeError(error, 'params'));
if (errors.length) {
throw new HTTPError(400, 'Bad Request', {
'x-error-type': 'validation',
}, {
errors,
});
}
}
if (schemas?.request?.query && TypeGuard.IsSchema(schemas.request.query)) {
const validateFn = getValidateFn(schemas.request.query);
const errors = [...validateFn(request.query)].map(error => sanitizeError(error, 'query'));
if (errors.length) {
throw new HTTPError(400, 'Bad Request', {
'x-error-type': 'validation',
}, {
errors,
});
}
}
if (schemas?.request?.json && TypeGuard.IsSchema(schemas.request.json)) {
const validateFn = getValidateFn(schemas.request.json);
const origReqJsonMethod = request.json.bind(request);
Object.defineProperty(request, 'json', {
value: () => origReqJsonMethod().then(jsonObj => {
const errors = [...validateFn(jsonObj)].map(({ schema, type, ...error }) => ({
name: 'json',
...error,
}));
if (errors.length) {
throw new HTTPError(400, 'Bad Request', {
'x-error-type': 'validation',
}, {
errors,
});
}
return jsonObj;
}),
configurable: true,
});
}
if (schemas?.request?.formData && TypeGuard.IsSchema(schemas.request.formData)) {
const validateFn = getValidateFn(schemas.request.formData);
const origMethod = request.formData.bind(request);
Object.defineProperty(request, 'formData', {
configurable: true,
value: () => origMethod().then(formData => {
const formDataObj = {};
const jobs = [];
formData.forEach((value, key) => {
if (value != null) {
if (typeof value === 'string') {
formDataObj[key] = value;
}
else {
jobs.push(value.arrayBuffer().then(buffer => {
const typedArray = isByteArray(buffer) ? buffer : new Uint8Array(buffer);
const binaryStrParts = [];
typedArray.forEach((byte, index) => {
binaryStrParts[index] = String.fromCharCode(byte);
});
formDataObj[key] = binaryStrParts.join('');
}));
}
}
});
function handleErrors() {
const errors = [...validateFn(formDataObj)].map(error => sanitizeError(error, 'formData'));
if (errors.length) {
throw new HTTPError(400, 'Bad Request', {
'x-error-type': 'validation',
}, {
errors,
});
}
return formDataObj;
}
if (jobs.length) {
return Promise.all(jobs).then(handleErrors);
}
return handleErrors();
}),
});
}
},
};
}
function isByteArray(value) {
return value?.forEach != null;
}