UNPKG

fets

Version:

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

211 lines (210 loc) • 8.89 kB
import Ajv from 'ajv'; import addFormats from 'ajv-formats'; import jsonSerializerFactory from '@ardatan/fast-json-stringify'; import { URL } from '@whatwg-node/fetch'; import { getHeadersObj } from '@whatwg-node/server'; import { Response } from '../Response.js'; import { isZodSchema } from '../zod/types.js'; export function useAjv({ components = {}, } = {}) { const ajv = new Ajv({ strict: false, strictSchema: false, validateSchema: false, allowUnionTypes: true, uriResolver: { parse(uri) { const url = new URL(uri); return { scheme: url.protocol, userinfo: url.username + (url.password ? ':' + url.password : ''), host: url.hostname, port: url.port, path: url.pathname, query: url.search, fragment: url.hash, }; }, resolve(base, ref) { return new URL(ref, base).toString(); }, serialize(components) { return (components.scheme + '://' + components.userinfo + components.host + components.port + components.path + components.query + components.fragment); }, }, }); addFormats(ajv); // Required for fast-json-stringify ajv.addKeyword({ keyword: 'fjs_type', type: 'object', errors: false, validate: (_type, date) => { return date instanceof Date; }, }); const serializersByCtx = new WeakMap(); return { onRoute({ schemas, handlers }) { const validationMiddlewares = new Map(); if (schemas?.request?.headers && !isZodSchema(schemas.request.headers)) { const validateFn = ajv.compile({ ...schemas.request.headers, components, }); validationMiddlewares.set('headers', request => { const headersObj = getHeadersObj(request.headers); const isValid = validateFn(headersObj); if (!isValid) { return validateFn.errors; } return []; }); } if (schemas?.request?.params && !isZodSchema(schemas.request.params)) { const validateFn = ajv.compile({ ...schemas.request.params, components, }); validationMiddlewares.set('params', request => { const isValid = validateFn(request.params); if (!isValid) { return validateFn.errors; } return []; }); } if (schemas?.request?.query && !isZodSchema(schemas.request.query)) { const validateFn = ajv.compile({ ...schemas.request.query, components, }); validationMiddlewares.set('query', request => { const isValid = validateFn(request.query); if (!isValid) { return validateFn.errors; } return []; }); } if (schemas?.request?.json && !isZodSchema(schemas.request.json)) { const validateFn = ajv.compile({ ...schemas.request.json, components, }); validationMiddlewares.set('json', async (request) => { const contentType = request.headers.get('content-type'); if (contentType?.includes('json')) { const jsonObj = await request.json(); Object.defineProperty(request, 'json', { value: async () => jsonObj, configurable: true, }); const isValid = validateFn(jsonObj); if (!isValid) { return validateFn.errors; } } return []; }); } if (schemas?.request?.formData && !isZodSchema(schemas.request.formData)) { const validateFn = ajv.compile({ ...schemas.request.formData, components, }); validationMiddlewares.set('formData', async (request) => { const contentType = request.headers.get('content-type'); if (contentType?.includes('multipart/form-data') || contentType?.includes('application/x-www-form-urlencoded')) { const formData = await request.formData(); const formDataObj = {}; const jobs = []; formData.forEach((value, key) => { if (typeof value === 'string') { formDataObj[key] = value; } else { jobs.push(value.arrayBuffer().then(buffer => { const typedArray = new Uint8Array(buffer); const binaryStrParts = []; typedArray.forEach((byte, index) => { binaryStrParts[index] = String.fromCharCode(byte); }); formDataObj[key] = binaryStrParts.join(''); })); } }); await Promise.all(jobs); Object.defineProperty(request, 'formData', { value: async () => formData, configurable: true, }); const isValid = validateFn(formDataObj); if (!isValid) { return validateFn.errors; } } return []; }); } if (jsonSerializerFactory && schemas?.responses) { const serializerByStatusCode = new Map(); for (const statusCode in schemas.responses) { const schema = schemas.responses[statusCode]; if (!isZodSchema(schema)) { const serializer = jsonSerializerFactory({ ...schema, components, }, { ajv, }); serializerByStatusCode.set(Number(statusCode), serializer); } } handlers.unshift((_request, ctx) => { serializersByCtx.set(ctx, serializerByStatusCode); }); } if (validationMiddlewares.size > 0) { handlers.unshift(async (request) => { const validationErrorsNonFlat = await Promise.all([...validationMiddlewares.entries()].map(async ([name, fn]) => { const errors = await fn(request); if (errors.length > 0) { return errors.map(error => ({ name, ...error, })); } })); const validationErrors = validationErrorsNonFlat.flat().filter(Boolean); if (validationErrors.length > 0) { return Response.json({ errors: validationErrors, }, { status: 400, headers: { 'x-error-type': 'validation', }, }); } }); } }, onSerializeResponse({ serverContext, lazyResponse }) { const serializers = serializersByCtx.get(serverContext); if (serializers) { const serializer = serializers.get(lazyResponse.init?.status || 200); if (serializer) { lazyResponse.resolveWithSerializer(serializer); } } }, }; }