fastify-file-router
Version:
A fastify plugin that automatically registers routes from files in a directory.
192 lines • 6.98 kB
JavaScript
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