@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
JavaScript
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