UNPKG

trpc-to-openapi

Version:
202 lines 8.21 kB
import { TRPCError } from '@trpc/server'; import { z } from 'zod'; import { HTTP_STATUS_TRPC_ERROR_CODE, TRPC_ERROR_CODE_HTTP_STATUS, TRPC_ERROR_CODE_MESSAGE, } from '../adapters/index.mjs'; import { instanceofZodType, instanceofZodTypeCoercible, instanceofZodTypeKind, instanceofZodTypeLikeString, instanceofZodTypeLikeVoid, instanceofZodTypeOptional, unwrapZodType, zodSupportsCoerce, } from '../utils/index.mjs'; import { HttpMethods } from './paths.mjs'; export const getParameterObjects = (schema, required, pathParameters, headersSchema, inType) => { const shape = schema.shape; const shapeKeys = Object.keys(shape); for (const pathParameter of pathParameters) { if (!shapeKeys.includes(pathParameter)) { throw new TRPCError({ message: `Input parser expects key from path: "${pathParameter}"`, code: 'INTERNAL_SERVER_ERROR', }); } } // @ts-expect-error fix later const { path, query } = shapeKeys .filter((shapeKey) => { const isPathParameter = pathParameters.includes(shapeKey); if (inType === 'path') { return isPathParameter; } else if (inType === 'query') { return !isPathParameter; } return true; }) .map((shapeKey) => { let shapeSchema = shape[shapeKey]; const isShapeRequired = !shapeSchema.safeParse(undefined).success; const isPathParameter = pathParameters.includes(shapeKey); if (!instanceofZodTypeLikeString(shapeSchema)) { if (zodSupportsCoerce) { if (!instanceofZodTypeCoercible(shapeSchema)) { throw new TRPCError({ message: `Input parser key: "${shapeKey}" must be ZodString, ZodNumber, ZodBoolean, ZodBigInt or ZodDate`, code: 'INTERNAL_SERVER_ERROR', }); } } else { throw new TRPCError({ message: `Input parser key: "${shapeKey}" must be ZodString`, code: 'INTERNAL_SERVER_ERROR', }); } } if (instanceofZodTypeOptional(shapeSchema)) { if (isPathParameter) { throw new TRPCError({ message: `Path parameter: "${shapeKey}" must not be optional`, code: 'INTERNAL_SERVER_ERROR', }); } shapeSchema = shapeSchema.unwrap(); } return { name: shapeKey, paramType: isPathParameter ? 'path' : 'query', required: isPathParameter || (required && isShapeRequired), schema: shapeSchema, }; }) .reduce(({ path, query }, { name, paramType, schema, required }) => // @ts-expect-error fix later paramType === 'path' ? { path: { ...path, [name]: required ? schema : schema.optional() }, query, } : { path, query: { ...query, [name]: required ? schema : schema.optional() }, }, { path: {}, query: {} }); let res = {}; if (headersSchema) { res.header = headersSchema; } res = { ...res, path: z.object(path), query: z.object(query), }; return res; }; export const getRequestBodyObject = (schema, required, pathParameters, contentTypes) => { // remove path parameters const mask = {}; pathParameters.forEach((pathParameter) => { mask[pathParameter] = true; }); const o = schema.meta(); const dedupedSchema = schema.omit(mask).meta({ ...(o?.title ? { title: o?.title } : {}), ...(o?.description ? { description: o?.description } : {}), }); // if all keys are path parameters if (pathParameters.length > 0 && Object.keys(dedupedSchema.shape).length === 0) { return undefined; } const content = {}; for (const contentType of contentTypes) { content[contentType] = { schema: dedupedSchema, }; } return { required, content, }; }; export const hasInputs = (schema) => instanceofZodType(schema) && !instanceofZodTypeLikeVoid(unwrapZodType(schema, true)); const errorResponseObjectByCode = {}; export const errorResponseObject = (code = 'INTERNAL_SERVER_ERROR', message, issues) => { if (!errorResponseObjectByCode[code]) { errorResponseObjectByCode[code] = { description: message ?? 'An error response', content: { 'application/json': { schema: z .object({ message: z.string().meta({ description: 'The error message', example: message ?? 'Internal server error', }), code: z.string().meta({ description: 'The error code', example: code ?? 'INTERNAL_SERVER_ERROR', }), issues: z .array(z.object({ message: z.string() })) .optional() .meta({ description: 'An array of issues that were responsible for the error', example: issues ?? [], }), }) .meta({ title: `${message ?? 'Internal server'} error (${TRPC_ERROR_CODE_HTTP_STATUS[code] ?? 500})`, description: 'The error information', example: { code: code ?? 'INTERNAL_SERVER_ERROR', message: message ?? 'Internal server error', issues: issues ?? [], }, id: `error.${code}`, }), }, }, }; } return errorResponseObjectByCode[code]; }; export const errorResponseFromStatusCode = (status) => { const code = HTTP_STATUS_TRPC_ERROR_CODE[status]; const message = code && TRPC_ERROR_CODE_MESSAGE[code]; return errorResponseObject(code, message ?? 'Unknown error'); }; export const errorResponseFromMessage = (status, message) => errorResponseObject(HTTP_STATUS_TRPC_ERROR_CODE[status], message); export const getResponsesObject = (schema, httpMethod, headers, isProtected, hasInputs, successDescription, errorResponses) => ({ 200: { description: successDescription ?? 'Successful response', headers: headers, content: { 'application/json': { schema: instanceofZodTypeKind(schema, 'void') ? {} : instanceofZodTypeKind(schema, 'never') || instanceofZodTypeKind(schema, 'undefined') ? { not: {} } : schema, }, }, }, ...(errorResponses !== undefined ? Object.fromEntries(Array.isArray(errorResponses) ? errorResponses.map((x) => [x, errorResponseFromStatusCode(x)]) : Object.entries(errorResponses).map(([k, v]) => [ k, errorResponseFromMessage(Number(k), v), ])) : { ...(isProtected ? { 401: errorResponseObject('UNAUTHORIZED', 'Authorization not provided'), 403: errorResponseObject('FORBIDDEN', 'Insufficient access'), } : {}), ...(hasInputs ? { 400: errorResponseObject('BAD_REQUEST', 'Invalid input data'), ...(httpMethod !== HttpMethods.POST ? { 404: errorResponseObject('NOT_FOUND', 'Not found'), } : {}), } : {}), 500: errorResponseObject('INTERNAL_SERVER_ERROR', 'Internal server error'), }), }); //# sourceMappingURL=schema.js.map