fastify-zod-openapi
Version:
Fastify plugin for zod-openapi
387 lines (386 loc) • 15.3 kB
JavaScript
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;