UNPKG

@pulzar/core

Version:

Next-generation Node.js framework for ultra-fast web applications with zero-reflection DI, GraphQL, WebSockets, events, and edge runtime support

405 lines 13.4 kB
import { z } from "zod"; import { logger } from "../utils/logger"; // Re-export zod for convenience export { z }; /** * Convert Zod errors to structured validation errors */ export function formatZodErrors(zodError) { return zodError.errors.map((error) => ({ field: error.path.join("."), message: error.message, code: error.code, value: error.code === "invalid_type" ? error.received : undefined, })); } /** * Convert Zod schema to JSON Schema for Fastify */ export function zodToJsonSchema(schema) { try { // Basic conversion - in production, use zod-to-json-schema library const def = schema._def; if (schema instanceof z.ZodString) { const stringSchema = { type: "string" }; if (def && def.checks && Array.isArray(def.checks)) { for (const check of def.checks) { if (!check || typeof check.kind !== "string") continue; switch (check.kind) { case "min": if (typeof check.value === "number") { stringSchema.minLength = check.value; } break; case "max": if (typeof check.value === "number") { stringSchema.maxLength = check.value; } break; case "email": stringSchema.format = "email"; break; case "url": stringSchema.format = "uri"; break; case "uuid": stringSchema.format = "uuid"; break; case "regex": if (check.regex && check.regex.source) { stringSchema.pattern = check.regex.source; } break; } } } return stringSchema; } if (schema instanceof z.ZodNumber) { const numberSchema = { type: "number" }; if (def && def.checks && Array.isArray(def.checks)) { for (const check of def.checks) { if (!check || typeof check.kind !== "string") continue; switch (check.kind) { case "min": if (typeof check.value === "number") { numberSchema.minimum = check.value; } break; case "max": if (typeof check.value === "number") { numberSchema.maximum = check.value; } break; case "int": numberSchema.type = "integer"; break; } } } return numberSchema; } if (schema instanceof z.ZodBoolean) { return { type: "boolean" }; } if (schema instanceof z.ZodArray) { if (def && def.type) { return { type: "array", items: zodToJsonSchema(def.type), }; } return { type: "array" }; } if (schema instanceof z.ZodObject) { const properties = {}; const required = []; if (def && typeof def.shape === "function") { const shape = def.shape(); if (shape && typeof shape === "object") { for (const [key, value] of Object.entries(shape)) { if (value && typeof value === "object") { properties[key] = zodToJsonSchema(value); // Check if field is required (not optional) if (!(value instanceof z.ZodOptional)) { required.push(key); } } } } } return { type: "object", properties, required: required.length > 0 ? required : undefined, additionalProperties: false, }; } if (schema instanceof z.ZodOptional) { if (def && def.innerType) { return zodToJsonSchema(def.innerType); } return { type: "string" }; } if (schema instanceof z.ZodNullable) { if (def && def.innerType) { const innerSchema = zodToJsonSchema(def.innerType); return { anyOf: [innerSchema, { type: "null" }], }; } return { anyOf: [{ type: "string" }, { type: "null" }] }; } if (schema instanceof z.ZodEnum) { if (def && def.values && Array.isArray(def.values)) { return { type: "string", enum: def.values, }; } return { type: "string" }; } if (schema instanceof z.ZodUnion) { if (def && def.options && Array.isArray(def.options)) { return { anyOf: def.options.map((option) => zodToJsonSchema(option)), }; } return { anyOf: [{ type: "string" }] }; } // Fallback for unsupported types return { type: "object", additionalProperties: true, }; } catch (error) { logger.warn("Failed to convert Zod schema to JSON Schema", { error }); return { type: "object", additionalProperties: true, }; } } /** * Validate data against Zod schema */ export function validateData(schema, data) { try { const result = schema.parse(data); return { success: true, data: result, }; } catch (error) { if (error instanceof z.ZodError) { return { success: false, errors: formatZodErrors(error), }; } return { success: false, errors: [ { field: "root", message: "Unknown validation error", code: "unknown", }, ], }; } } /** * Fastify plugin for Zod validation */ export const zodValidationPlugin = async (fastify, options) => { fastify.addHook("preHandler", async (request, reply) => { const errors = []; // Validate body if (options.body && request.body !== undefined) { const result = validateData(options.body, request.body); if (!result.success) { errors.push(...(result.errors || [])); } else { request.body = result.data; } } // Validate query parameters if (options.query && request.query !== undefined) { const result = validateData(options.query, request.query); if (!result.success) { errors.push(...(result.errors || [])); } else { request.query = result.data; } } // Validate route parameters if (options.params && request.params !== undefined) { const result = validateData(options.params, request.params); if (!result.success) { errors.push(...(result.errors || [])); } else { request.params = result.data; } } // Validate headers if (options.headers && request.headers !== undefined) { const result = validateData(options.headers, request.headers); if (!result.success) { errors.push(...(result.errors || [])); } } // If there are validation errors, return them if (errors.length > 0) { const response = { error: "ValidationError", message: "Request validation failed", statusCode: 400, details: errors, timestamp: new Date().toISOString(), }; logger.warn("Request validation failed", { url: request.url, method: request.method, errors, }); return reply.code(400).send(response); } }); }; /** * Create a validation schema decorator for route handlers */ export function validate(options) { return function (target, propertyKey, descriptor) { // Store validation metadata Reflect.defineMetadata("validation", options, target, propertyKey); return descriptor; }; } /** * Utility functions for common validation patterns */ export const CommonSchemas = { // Pagination pagination: z.object({ page: z.number().int().min(1).default(1), limit: z.number().int().min(1).max(100).default(20), sort: z.string().optional(), order: z.enum(["asc", "desc"]).default("asc"), }), // ID parameter id: z.object({ id: z.string().uuid(), }), // Numeric ID parameter numericId: z.object({ id: z.number().int().positive(), }), // Search query search: z.object({ q: z.string().min(1).max(255), filters: z.record(z.string()).optional(), }), // File upload fileUpload: z.object({ filename: z.string(), mimetype: z.string(), size: z.number().positive(), }), // Common response wrapper response: (dataSchema) => z.object({ success: z.boolean(), data: dataSchema, message: z.string().optional(), pagination: z .object({ page: z.number(), limit: z.number(), total: z.number(), pages: z.number(), }) .optional(), }), // Error response errorResponse: z.object({ error: z.string(), message: z.string(), statusCode: z.number(), details: z .array(z.object({ field: z.string(), message: z.string(), code: z.string(), })) .optional(), timestamp: z.string(), }), }; /** * Create typed request handler with validation */ export function createValidatedHandler(schema, handler) { return async (request, reply) => { const errors = []; let validatedBody = request.body; let validatedQuery = request.query; let validatedParams = request.params; let validatedHeaders = request.headers; // Validate each part if (schema.body) { const result = validateData(schema.body, request.body); if (!result.success) { errors.push(...(result.errors || [])); } else { validatedBody = result.data; } } if (schema.query) { const result = validateData(schema.query, request.query); if (!result.success) { errors.push(...(result.errors || [])); } else { validatedQuery = result.data; } } if (schema.params) { const result = validateData(schema.params, request.params); if (!result.success) { errors.push(...(result.errors || [])); } else { validatedParams = result.data; } } if (schema.headers) { const result = validateData(schema.headers, request.headers); if (!result.success) { errors.push(...(result.errors || [])); } else { validatedHeaders = result.data; } } // Return validation errors if any if (errors.length > 0) { return reply.code(400).send({ error: "ValidationError", message: "Request validation failed", statusCode: 400, details: errors, timestamp: new Date().toISOString(), }); } // Call handler with validated data return handler({ body: validatedBody, query: validatedQuery, params: validatedParams, headers: validatedHeaders, raw: request, }, reply); }; } /** * Response validation helper */ export function validateResponse(schema, data) { try { return schema.parse(data); } catch (error) { logger.error("Response validation failed", { error, data }); throw new Error("Internal server error: Invalid response format"); } } //# sourceMappingURL=zod-validator.js.map