UNPKG

fastify-file-router

Version:

A fastify plugin that automatically registers routes from files in a directory.

192 lines 6.98 kB
import { z } from 'zod'; /** * Type helper to check if a value is a Zod schema * Exported for use in routeRegistration.ts */ export function isZodSchema(value) { return typeof value === 'object' && value !== null && '_def' in value && 'safeParse' in value; } /** * Converts a Zod schema to JSON Schema, removing the $schema property. * * Note: z.date() and z.coerce.date() are not supported. Use z.iso.datetime() instead * for date fields, which validates ISO 8601 datetime strings and is JSON-compatible. */ function zodToJsonSchema(schema) { const jsonSchema = z.toJSONSchema(schema, { target: 'draft-2020-12', unrepresentable: 'any', }); delete jsonSchema.$schema; return jsonSchema; } /** * Formats Zod validation errors into a single error message string. * Format: "Bad Request: [component] - [comma-separated list of issues]" * This function is exported so it can be used in routeRegistration.ts */ export function formatZodError(error, component) { const issueMessages = error.issues.map((issue) => { const path = issue.path.length > 0 ? `${issue.path.join('.')}: ` : ''; return `${path}${issue.message}`; }); return `Bad Request: ${component} - ${issueMessages.join(', ')}`; } /** * Formats JSON Schema validation errors into a single error message string. * Format: "Bad Request: [component] - [comma-separated list of issues]" * This function is exported so it can be used in routeRegistration.ts */ export function formatJsonSchemaError(errors, component) { const issueMessages = errors.map((error) => { const path = error.instancePath ? `${error.instancePath.substring(1)}: ` : ''; const message = error.message || 'validation failed'; return `${path}${message}`; }); return `Bad Request: ${component} - ${issueMessages.join(', ')}`; } /** * Helper to define a route using Zod schemas and/or JSON Schema. * This function extracts TypeScript types from Zod schemas using `z.infer` * and from JSON Schema using `FromSchema`, allowing you to mix validation approaches. * * @example * ```typescript * import { defineRouteZod } from 'fastify-file-router'; * import { z } from 'zod'; * * // Pure Zod example * export const route = defineRouteZod({ * schema: { * params: z.object({ * id: z.string().min(1) * }), * body: z.object({ * name: z.string().optional(), * email: z.email().optional() * }) * }, * handler: async (request, reply) => { * // request.params.id is correctly typed as string * // request.body.name and request.body.email are correctly typed * const { id } = request.params; * const { name, email } = request.body; * reply.status(200).send({ id, name, email }); * } * }); * ``` * * @example * ```typescript * // Mixed Zod and JSON Schema example * export const route = defineRouteZod({ * schema: { * params: z.object({ id: z.string() }), // Zod schema * body: { * type: 'object', * properties: { name: { type: 'string' } }, * required: ['name'] * } as const // JSON Schema * }, * handler: async (request, reply) => { * // Types are correctly inferred from both schema types * const { id } = request.params; // string (from Zod) * const { name } = request.body; // string (from JSON Schema) * reply.status(200).send({ id, name }); * } * }); * ``` */ export function defineRouteZod(route) { const inputSchema = route.schema; const fastifySchema = {}; const zodSchemas = {}; const schemaTypes = {}; // Process params if (inputSchema.params) { if (isZodSchema(inputSchema.params)) { fastifySchema.params = zodToJsonSchema(inputSchema.params); zodSchemas.params = inputSchema.params; schemaTypes.params = 'zod'; } else { fastifySchema.params = inputSchema.params; schemaTypes.params = 'json'; } } // Process querystring if (inputSchema.querystring) { if (isZodSchema(inputSchema.querystring)) { fastifySchema.querystring = zodToJsonSchema(inputSchema.querystring); zodSchemas.querystring = inputSchema.querystring; schemaTypes.querystring = 'zod'; } else { fastifySchema.querystring = inputSchema.querystring; schemaTypes.querystring = 'json'; } } // Process body if (inputSchema.body) { if (isZodSchema(inputSchema.body)) { fastifySchema.body = zodToJsonSchema(inputSchema.body); zodSchemas.body = inputSchema.body; schemaTypes.body = 'zod'; } else { fastifySchema.body = inputSchema.body; schemaTypes.body = 'json'; } } // Process headers if (inputSchema.headers) { if (isZodSchema(inputSchema.headers)) { fastifySchema.headers = zodToJsonSchema(inputSchema.headers); zodSchemas.headers = inputSchema.headers; schemaTypes.headers = 'zod'; } else { fastifySchema.headers = inputSchema.headers; schemaTypes.headers = 'json'; } } // Process response schemas (can mix Zod and JSON Schema) if (inputSchema.response) { const responseSchemas = {}; const zodResponseSchemas = {}; const responseSchemaTypes = {}; for (const [statusCode, responseSchema] of Object.entries(inputSchema.response)) { if (isZodSchema(responseSchema)) { responseSchemas[String(statusCode)] = zodToJsonSchema(responseSchema); zodResponseSchemas[statusCode] = responseSchema; responseSchemaTypes[statusCode] = 'zod'; } else { responseSchemas[String(statusCode)] = responseSchema; responseSchemaTypes[statusCode] = 'json'; } } fastifySchema.response = responseSchemas; if (Object.keys(zodResponseSchemas).length > 0) { zodSchemas.response = zodResponseSchemas; } schemaTypes.response = responseSchemaTypes; } // Copy any other properties (like description, tags, etc. for OpenAPI) for (const [key, value] of Object.entries(inputSchema)) { if (!['params', 'querystring', 'body', 'headers', 'response'].includes(key)) { fastifySchema[key] = value; } } // Store Zod schemas and schema type indicators on the schema object const schemaWithMetadata = fastifySchema; if (Object.keys(zodSchemas).length > 0) { schemaWithMetadata.__zodSchemas = zodSchemas; } schemaWithMetadata.__schemaTypes = schemaTypes; return { schema: schemaWithMetadata, handler: route.handler, // Don't wrap here, we'll use preValidation hook instead }; } //# sourceMappingURL=defineRouteZod.js.map