UNPKG

tsoa-zod-validator

Version:
205 lines (204 loc) 7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ZodValidate = ZodValidate; require("reflect-metadata"); const zod_1 = require("zod"); const ZodValidationError_1 = require("../errors/ZodValidationError"); const fileUtils_1 = require("../utils/fileUtils"); /** * Default validation options */ const DEFAULT_OPTIONS = { errorFormat: 'simple', }; /** * Validates request data using Zod schemas * * @param schemas The Zod schemas or a single schema for validation * @param options Validation options * @returns A decorator function for controller methods */ function ZodValidate(schemas, options = {}) { // Handle the case where a single schema is passed const validationSchemas = isZodType(schemas) ? { body: schemas } : schemas; // Merge options with defaults const validationOptions = { ...DEFAULT_OPTIONS, ...options, }; return (_target, _propertyKey, descriptor) => { const originalMethod = descriptor.value; descriptor.value = async function (req, res, next, ...args) { try { // Skip validation if the skipValidation function returns true if (validationOptions.skipValidation?.(req)) { return originalMethod.apply(this, [req, res, next, ...args]); } // Validate request data await validateRequest(req, validationSchemas); // If validation passes, call the original method return originalMethod.apply(this, [req, res, next, ...args]); } catch (error) { // Handle validation errors if (error instanceof ZodValidationError_1.ZodValidationError) { handleValidationError(error, req, res, validationOptions); } else { // Pass other errors to the next middleware next(error); } } }; return descriptor; }; } /** * Checks if a value is a Zod schema type * * @param value The value to check * @returns True if the value is a Zod schema, false otherwise */ function isZodType(value) { return (value && typeof value === 'object' && typeof value.parse === 'function'); } /** * Validates a request against the provided schemas * * @param req The Express request object with files * @param schemas The validation schemas */ async function validateRequest(req, schemas) { // Validate request body if (schemas.body && req.body) { try { const validatedBody = schemas.body.parse(req.body); req.body = validatedBody; } catch (error) { if (error instanceof zod_1.z.ZodError) { throw new ZodValidationError_1.ZodValidationError(error, 'body'); } throw error; } } // Validate query parameters if (schemas.query && req.query) { try { const validatedQuery = schemas.query.parse(req.query); // Safely assign validated query parameters Object.assign(req.query, validatedQuery); } catch (error) { if (error instanceof zod_1.z.ZodError) { throw new ZodValidationError_1.ZodValidationError(error, 'query'); } throw error; } } // Validate route parameters if (schemas.params && req.params) { try { const validatedParams = schemas.params.parse(req.params); // Safely assign validated route parameters Object.assign(req.params, validatedParams); } catch (error) { if (error instanceof zod_1.z.ZodError) { throw new ZodValidationError_1.ZodValidationError(error, 'params'); } throw error; } } // Validate request headers if (schemas.headers && req.headers) { try { // Headers validation doesn't modify the headers object schemas.headers.parse(req.headers); } catch (error) { if (error instanceof zod_1.z.ZodError) { throw new ZodValidationError_1.ZodValidationError(error, 'headers'); } throw error; } } // Validate files if (schemas.files && req.files) { validateFiles(req, schemas.files); } } /** * Validates file uploads * * @param req The Express request object with files * @param fileOptions File validation options */ function validateFiles(req, fileOptions) { // Skip if no files in request or files API not available if (!req.files) { if (fileOptions.required) { throw new Error('File upload is required'); } return; } const files = Array.isArray(req.files) ? req.files : Object.values(req.files).flat(); // Check if files are required if (fileOptions.required && (!files || files.length === 0)) { throw new Error('File upload is required'); } // Check minimum files requirement const minFiles = fileOptions.minFiles || 1; if (fileOptions.required && files.length < minFiles) { throw new Error(`At least ${minFiles} file(s) must be uploaded`); } // Check maximum files limit if (fileOptions.maxFiles && files.length > fileOptions.maxFiles) { throw new Error(`Maximum ${fileOptions.maxFiles} file(s) allowed`); } // Validate each file for (const file of files) { // Validate file size if (fileOptions.maxSize) { const maxSizeInBytes = (0, fileUtils_1.parseFileSize)(fileOptions.maxSize); if (file.size > maxSizeInBytes) { throw new Error(`File size exceeds the limit of ${fileOptions.maxSize}`); } } // Validate file type if (fileOptions.allowedTypes && fileOptions.allowedTypes.length > 0) { if (!(0, fileUtils_1.isValidFileType)(file.mimetype, fileOptions.allowedTypes)) { throw new Error(`File type ${file.mimetype} is not allowed`); } } } } /** * Handles a validation error based on the error format option * * @param error The validation error * @param req The Express request object * @param res The Express response object * @param options Validation options */ function handleValidationError(error, req, res, options) { if (options.customErrorHandler) { // Use custom error handler if provided const customResponse = options.customErrorHandler(error, req); res.status(400).json(customResponse); return; } // Use built-in error formats switch (options.errorFormat) { case 'detailed': res.status(400).json(error.toDetailedFormat()); break; default: res.status(400).json(error.toSimpleFormat()); break; } }