UNPKG

trpc-to-openapi

Version:
166 lines 7.21 kB
import { TRPCError } from '@trpc/server'; import { getErrorShape } from '@trpc/server/unstable-core-do-not-import'; import { ZodArray } from 'zod'; import { generateOpenApiDocument } from '../../generator/index.mjs'; import { acceptsRequestBody, normalizePath, getInputOutputParsers, coerceSchema, instanceofZodTypeLikeVoid, instanceofZodTypeObject, instanceofZodTypeOptional, unwrapZodType, zodSupportsCoerce, getContentType, getRequestSignal, } from '../../utils/index.mjs'; import { TRPC_ERROR_CODE_HTTP_STATUS, getErrorFromUnknown } from './errors.mjs'; import { getBody, getQuery } from './input.mjs'; import { createProcedureCache } from './procedures.mjs'; export const createOpenApiNodeHttpHandler = (opts) => { const router = Object.assign({}, opts.router); // Validate router if (process.env.NODE_ENV !== 'production') { generateOpenApiDocument(router, { title: '', version: '', baseUrl: '' }); } const { createContext, responseMeta, onError, maxBodySize } = opts; const getProcedure = createProcedureCache(router); return async (req, res, next) => { const sendResponse = (statusCode, headers, body) => { res.statusCode = statusCode; res.setHeader('Content-Type', 'application/json'); for (const [key, value] of Object.entries(headers)) { if (typeof value !== 'undefined') { res.setHeader(key, value); } } res.end(JSON.stringify(body)); }; const method = req.method; const reqUrl = req.url; const url = new URL(reqUrl.startsWith('/') ? `http://127.0.0.1${reqUrl}` : reqUrl); const path = normalizePath(url.pathname); let input = undefined; let ctx = undefined; let info = undefined; let data = undefined; const { procedure, pathInput } = getProcedure(method, path) ?? {}; try { if (!procedure) { if (next) { return next(); } // Can be used for warmup if (method === 'HEAD') { sendResponse(204, {}, undefined); return; } throw new TRPCError({ message: 'Not found', code: 'NOT_FOUND', }); } const contentType = getContentType(req); const useBody = acceptsRequestBody(method); if (useBody && !contentType?.startsWith('application/json')) { throw new TRPCError({ code: 'UNSUPPORTED_MEDIA_TYPE', message: contentType ? `Unsupported content-type "${contentType}` : 'Missing content-type header', }); } const { inputParser } = getInputOutputParsers(procedure.procedure); const unwrappedSchema = unwrapZodType(inputParser, true); // input should stay undefined if z.void() if (!instanceofZodTypeLikeVoid(unwrappedSchema)) { input = { ...(useBody ? await getBody(req, maxBodySize) : getQuery(req, url)), ...pathInput, }; } // if supported, coerce all string values to correct types if (zodSupportsCoerce && instanceofZodTypeObject(unwrappedSchema)) { if (!useBody) { for (const [key, shape] of Object.entries(unwrappedSchema.shape)) { let isArray = false; // Check if it's a direct array if (shape instanceof ZodArray) { isArray = true; } // Check if it's an optional array else if (instanceofZodTypeOptional(shape)) { const innerType = shape.unwrap(); if (innerType instanceof ZodArray) { isArray = true; } } if (isArray && input[key] !== undefined && !Array.isArray(input[key])) { input[key] = [input[key]]; } } } coerceSchema(unwrappedSchema); } info = { isBatchCall: false, accept: null, calls: [], type: procedure.type, connectionParams: null, signal: getRequestSignal(req, res, maxBodySize), url, }; ctx = await createContext?.({ req, res, info }); const caller = router.createCaller(ctx); const segments = procedure.path.split('.'); const procedureFn = segments.reduce((acc, curr) => acc[curr], caller); data = await procedureFn(input); const meta = responseMeta?.({ type: procedure.type, paths: [procedure.path], ctx, data: [data], errors: [], info, eagerGeneration: true, }); const statusCode = meta?.status ?? 200; const headers = meta?.headers ?? {}; const body = data; sendResponse(statusCode, headers, body); } catch (cause) { const error = getErrorFromUnknown(cause); onError?.({ error, type: procedure?.type ?? 'unknown', path: procedure?.path, input, ctx, req, }); const meta = responseMeta?.({ type: procedure?.type ?? 'unknown', paths: procedure?.path ? [procedure?.path] : undefined, ctx, data: [data], errors: [error], info, eagerGeneration: true, }); const errorShape = getErrorShape({ config: router._def._config, error, type: procedure?.type ?? 'unknown', path: procedure?.path, input, ctx, }); const isInputValidationError = error.code === 'BAD_REQUEST' && error.cause instanceof Error && error.cause.name === 'ZodError'; const statusCode = meta?.status ?? TRPC_ERROR_CODE_HTTP_STATUS[error.code] ?? 500; const headers = meta?.headers ?? {}; const body = { ...errorShape, // Pass the error through message: isInputValidationError ? 'Input validation failed' : (errorShape?.message ?? error.message ?? 'An error occurred'), code: error.code, issues: isInputValidationError ? error.cause.issues : undefined, }; sendResponse(statusCode, headers, body); } }; }; //# sourceMappingURL=core.js.map