mirror of
https://github.com/hanzoai/trpc-openapi.git
synced 2026-10-30 18:18:55 +00:00
218 lines
8.6 KiB
JavaScript
218 lines
8.6 KiB
JavaScript
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 || 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) => {
|
|
const mask = {};
|
|
pathParameters.forEach((pathParameter) => {
|
|
mask[pathParameter] = true;
|
|
});
|
|
const o = schema.meta();
|
|
let dedupedSchema;
|
|
try {
|
|
dedupedSchema = schema.omit(mask).meta({
|
|
...(o?.title ? { title: o?.title } : {}),
|
|
...(o?.description ? { description: o?.description } : {}),
|
|
...(o?.examples ? { examples: o?.examples } : {}),
|
|
});
|
|
}
|
|
catch {
|
|
// Zod 4: .omit() throws on object schemas containing refinements; build body schema from shape
|
|
const shape = schema.shape;
|
|
const bodyShape = {};
|
|
for (const key of Object.keys(shape)) {
|
|
if (!mask[key] && shape[key])
|
|
bodyShape[key] = shape[key];
|
|
}
|
|
dedupedSchema = z.object(bodyShape).meta({
|
|
...(o?.title ? { title: o?.title } : {}),
|
|
...(o?.description ? { description: o?.description } : {}),
|
|
...(o?.examples ? { examples: o?.examples } : {}),
|
|
});
|
|
}
|
|
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
|