tsoa-zod-validator
Version:
Zod validation decorators for tsoa
205 lines (204 loc) • 7 kB
JavaScript
;
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;
}
}