UNPKG

fastify-zod-openapi

Version:
387 lines (386 loc) 15.3 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); //#region \0rolldown/runtime.js var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion let fastify_plugin = require("fastify-plugin"); fastify_plugin = __toESM(fastify_plugin); let zod_openapi_api = require("zod-openapi/api"); let fast_json_stringify = require("fast-json-stringify"); fast_json_stringify = __toESM(fast_json_stringify); let zod_openapi = require("zod-openapi"); let _fastify_error = require("@fastify/error"); let _fastify_swagger_lib_util_format_param_url_js = require("@fastify/swagger/lib/util/format-param-url.js"); //#region src/plugin.ts const FASTIFY_ZOD_OPENAPI_CONFIG = Symbol("fastify-zod-openapi-config"); const fastifyZodOpenApi = async (fastify, opts) => { const registry = (0, zod_openapi_api.createRegistry)(opts.components); fastify.addHook("onRoute", ({ schema }) => { if (!schema || schema.hide) return; schema[FASTIFY_ZOD_OPENAPI_CONFIG] ??= { registry, documentOpts: opts.documentOpts, fastifyComponents: { responses: /* @__PURE__ */ new Map(), requestBodies: /* @__PURE__ */ new Map() } }; }); }; const fastifyZodOpenApiPlugin = (0, fastify_plugin.default)(fastifyZodOpenApi, { name: "fastify-zod-openapi" }); //#endregion //#region src/validationError.ts var RequestValidationError = class extends Error { cause; constructor(keyword, instancePath, schemaPath, message, params) { super(message, { cause: params.issue }); this.keyword = keyword; this.instancePath = instancePath; this.schemaPath = schemaPath; this.message = message; this.params = params; } }; var ResponseSerializationError = class extends (0, _fastify_error.createError)("FST_ERR_RESPONSE_SERIALIZATION", "Response does not match the schema", 500) { cause; constructor(method, url, options) { super(); this.method = method; this.url = url; this.cause = options.cause; } }; //#endregion //#region src/serializerCompiler.ts const createSerializerCompiler = (opts) => (routeSchema) => { const { schema, url, method } = routeSchema; if (!(0, zod_openapi_api.isAnyZodType)(schema)) return opts?.fallbackSerializer ? opts.fallbackSerializer(routeSchema) : (0, fast_json_stringify.default)(schema); let stringify = opts?.stringify; if (!stringify) { const { schema: jsonSchema, components } = (0, zod_openapi.createSchema)(schema, { registry: (0, zod_openapi_api.createRegistry)({ schemas: opts?.components }), schemaRefPath: "#/definitions/" }); const maybeDefinitions = components ? { definitions: components } : void 0; stringify = (0, fast_json_stringify.default)({ ...jsonSchema, ...maybeDefinitions }); } return (value) => { const result = schema.safeParse(value); if (!result.success) throw new ResponseSerializationError(method, url, { cause: result.error }); return stringify(result.data); }; }; /** * Enables zod-openapi schema response validation * * @example * ```typescript * import Fastify from 'fastify' * * const server = Fastify().setSerializerCompiler(serializerCompiler) * ``` */ const serializerCompiler = createSerializerCompiler(); //#endregion //#region src/transformer.ts const createParams = (parameters, type, registry, path) => { const params = {}; for (const [key, value] of Object.entries(parameters._zod.def.shape)) { const parameter = registry.addParameter(value, path, { location: { in: type, name: key } }); if ("$ref" in parameter || !parameter.schema) throw new Error("References not supported"); const { in: inLocation, name, schema, ...rest } = parameter; params[key] = rest; } return params; }; const createResponse = (response, contentTypes, registry, responseComponents, path) => { if (typeof response !== "object" || response == null) return response; const responsesObject = {}; for (const [key, value] of Object.entries(response)) { const unknownValue = value; if ((0, zod_openapi_api.isAnyZodType)(unknownValue)) { if (!contentTypes?.length) { responsesObject[key] = registry.addSchema(unknownValue, [ ...path, key, "content", "application/json", "schema" ], { io: "output", source: { type: "mediaType" } }); continue; } responsesObject[key] = contentTypes.map((contentType) => registry.addSchema(unknownValue, [ ...path, key, "content", contentType, "schema" ], { io: "output", source: { type: "mediaType" } }))[0]; continue; } if (unknownValue && typeof unknownValue === "object" && !("content" in unknownValue)) { responsesObject[key] = { description: "description" in unknownValue ? unknownValue.description : void 0, type: "null" }; continue; } const responsePath = [...path, key]; const responseObject = registry.addResponse(unknownValue, responsePath); if ("$ref" in responseObject && typeof responseObject.$ref === "string") responseComponents.set(responseObject.$ref, { referenceObject: responseObject, path: responsePath }); responsesObject[key] = responseObject; } return responsesObject; }; const createBody = (body, contentTypes, routePath, registry, bodyComponents) => { if (!body) return; if ((0, zod_openapi_api.isAnyZodType)(body)) { if (!contentTypes?.length) { const bodySchema = registry.addSchema(body, [ ...routePath, "requestBody", "content", "application/json", "schema" ], { io: "input", source: { type: "mediaType" } }); bodySchema["x-fastify-zod-openapi-optional"] = body._zod.optin === "optional"; return bodySchema; } return contentTypes.map((contentType) => { const schema = registry.addSchema(body, [ ...routePath, "requestBody", "content", contentType, "schema" ], { io: "input", source: { type: "mediaType" } }); schema["x-fastify-zod-openapi-optional"] = body._zod.optin === "optional"; return schema; })[0]; } const requestBodyPath = [...routePath, "requestBody"]; const requestBodyObject = registry.addRequestBody(body, requestBodyPath); if ("$ref" in requestBodyObject && typeof requestBodyObject.$ref === "string") bodyComponents.set(requestBodyObject.$ref, { referenceObject: requestBodyObject, path: requestBodyPath }); return requestBodyObject; }; const fastifyZodOpenApiTransform = ({ schema, url, ...opts }) => { if (!schema || schema.hide) return { schema, url }; const { response, headers, querystring, body, params, ...rest } = schema; if (!("openapiObject" in opts)) throw new Error("openapiObject was not found in the options"); const config = schema[FASTIFY_ZOD_OPENAPI_CONFIG]; if (!config) throw new Error("Please register the fastify-zod-openapi plugin"); const { registry } = config; opts.openapiObject[FASTIFY_ZOD_OPENAPI_CONFIG] ??= config; const fastifySchema = rest; const routes = (typeof opts.route.method === "string" ? [opts.route.method] : opts.route.method).map((method) => { const routeObject = {}; let urlWithoutPrefix = url; const serverUrl = (opts?.openapiObject?.servers ?? []).find((server) => server.url.startsWith("/"))?.url || "/"; if (serverUrl !== "/") { const maybeSubPath = url.split(serverUrl)[1]; if (maybeSubPath) urlWithoutPrefix = (0, _fastify_swagger_lib_util_format_param_url_js.formatParamUrl)(maybeSubPath); } const routePath = [ "paths", (0, _fastify_swagger_lib_util_format_param_url_js.formatParamUrl)(urlWithoutPrefix), method.toLowerCase() ]; const parameterPath = [...routePath, "parameters"]; const maybeBody = createBody(body, rest.consumes, routePath, registry, config.fastifyComponents.requestBodies); if (maybeBody) routeObject.body = maybeBody; const maybeResponse = createResponse(response, rest.produces, registry, config.fastifyComponents.responses, [...routePath, "responses"]); if (maybeResponse) routeObject.response = maybeResponse; if ((0, zod_openapi_api.isAnyZodType)(querystring)) routeObject.querystring = createParams((0, zod_openapi_api.unwrapZodObject)(querystring, "input", [...parameterPath, "query"]), "query", registry, parameterPath); if ((0, zod_openapi_api.isAnyZodType)(params)) routeObject.params = createParams((0, zod_openapi_api.unwrapZodObject)(params, "input", [...parameterPath, "path"]), "path", registry, parameterPath); if ((0, zod_openapi_api.isAnyZodType)(headers)) routeObject.headers = createParams((0, zod_openapi_api.unwrapZodObject)(headers, "input", [...parameterPath, "header"]), "header", registry, parameterPath); return routeObject; }); Object.assign(fastifySchema, routes[0]); return { schema: fastifySchema, url }; }; const resolveSchemaComponent = (object, registry) => { if (typeof object.$ref === "string") { const id = object.$ref.replace("#/components/schemas/", ""); return registry.components.schemas.ids.get(id); } return object; }; const traverseObject = (openapiObject, source, schemaObject, registry) => { let index = 0; let current = openapiObject; while (index < source.path.length) { const key = source.path[index++]; if (typeof current !== "object" || current === null || !(key in current)) return; current = current[key]; if (typeof current === "object" && current !== null && "$ref" in current && typeof current.$ref === "string") return current; if (key === "requestBody" && typeof current === "object") { const requestBody = current; if (source.type === "requestBody") { delete requestBody.content; delete requestBody.required; delete requestBody.description; Object.assign(requestBody, schemaObject); return requestBody; } const contentType = source.path?.[index + 1]; if (!contentType) return; const schema = requestBody.content?.[contentType]?.schema; if (!schema) return; if ("$ref" in schema && schema.$ref) return schema; const resolved = resolveSchemaComponent(schemaObject, registry); const description = schemaObject.description ?? resolved.description; if (description) requestBody.description = description; const { examples, ...schemaWithoutExamples } = schemaObject; Object.assign(schema, schemaWithoutExamples); if (examples !== void 0 && Array.isArray(examples) && examples.length > 0) { const examplesAsRecord = examples.reduce((result, example, examplesIndex) => { result[`Example${examplesIndex + 1}`] = { value: example }; return result; }, {}); if (requestBody.content[contentType]) requestBody.content[contentType].examples = examplesAsRecord; } if (schema["x-fastify-zod-openapi-optional"] === false) { requestBody.required = true; delete schema["x-fastify-zod-openapi-optional"]; } return schema; } if (key === "parameters" && typeof current === "object" && Array.isArray(current) && source.type === "parameter") { const parameter = current.find((param) => param.name === source.location.name && param.in === source.location.in); if (parameter?.schema) return Object.assign(parameter.schema, schemaObject); return; } if (key === "responses" && typeof current === "object" && source.type === "response") { const responses = current; const statusCode = source.path?.[index]; if (!statusCode) return; responses[statusCode] = schemaObject; return responses[statusCode]; } } if (typeof current === "object" && current !== null && Object.keys(current).length !== 0) return current; return Object.assign(current, schemaObject); }; const combineComponents = (existingComponents, newComponents) => { for (const key of [ "schemas", "parameters", "responses", "requestBodies", "securitySchemes", "examples", "links", "headers", "callbacks" ]) if (existingComponents[key] || newComponents[key]) existingComponents[key] = { ...existingComponents[key], ...newComponents[key] }; return existingComponents; }; const fastifyZodOpenApiTransformObject = (opts) => { if ("swaggerObject" in opts) return opts.swaggerObject; const config = opts.openapiObject[FASTIFY_ZOD_OPENAPI_CONFIG]; if (!config) return opts.openapiObject; const components = (0, zod_openapi_api.createComponents)(config.registry, config.documentOpts ?? {}, opts.openapiObject.openapi); for (const [, value] of config.fastifyComponents.responses) traverseObject(opts.openapiObject, { type: "response", path: value.path }, value.referenceObject, config.registry); for (const [, value] of config.fastifyComponents.requestBodies) traverseObject(opts.openapiObject, { type: "requestBody", path: value.path }, value.referenceObject, config.registry); for (const [, value] of config.registry.components.schemas.input) traverseObject(opts.openapiObject, value.source, value.schemaObject, config.registry); for (const [, value] of config.registry.components.schemas.output) traverseObject(opts.openapiObject, value.source, value.schemaObject, config.registry); return { ...opts.openapiObject, components: combineComponents(opts.openapiObject.components, components) }; }; const fastifyZodOpenApiTransformers = { transform: fastifyZodOpenApiTransform, transformObject: fastifyZodOpenApiTransformObject }; //#endregion //#region src/validatorCompiler.ts /** * Enables zod-openapi schema validation * * @example * ```typescript * import Fastify from 'fastify' * * const server = Fastify().setValidatorCompiler(validatorCompiler) * ``` */ const validatorCompiler = ({ schema }) => { if (!(0, zod_openapi_api.isAnyZodType)(schema)) return (value) => ({ value }); return (value) => { const result = schema.safeParse(value); if (!result.success) return { error: result.error.issues.map((issue) => new RequestValidationError(issue.code, `/${issue.path.join("/")}`, `#/${issue.path.join("/")}/${issue.code}`, issue.message, { issue, error: result.error })) }; return { value: result.data }; }; }; //#endregion exports.FASTIFY_ZOD_OPENAPI_CONFIG = FASTIFY_ZOD_OPENAPI_CONFIG; exports.RequestValidationError = RequestValidationError; exports.ResponseSerializationError = ResponseSerializationError; exports.createSerializerCompiler = createSerializerCompiler; exports.fastifyZodOpenApiPlugin = fastifyZodOpenApiPlugin; exports.fastifyZodOpenApiTransform = fastifyZodOpenApiTransform; exports.fastifyZodOpenApiTransformObject = fastifyZodOpenApiTransformObject; exports.fastifyZodOpenApiTransformers = fastifyZodOpenApiTransformers; exports.serializerCompiler = serializerCompiler; exports.validatorCompiler = validatorCompiler;