import { HttpException, HttpStatus } from '@nestjs/common'; import type { ValidationError } from 'class-validator'; import { AppException } from './app.exception'; import { ErrorCode, type ErrorCodeValue, type ValidationErrorDetail } from './error-codes'; const KNOWN_VALIDATION_CODES = new Set(Object.values(ErrorCode)); function isKnownValidationCode(value: string): value is ErrorCodeValue { return KNOWN_VALIDATION_CODES.has(value); } function constraintToCode(constraintKey: string, message: string): ErrorCodeValue { if (isKnownValidationCode(message)) { return message; } switch (constraintKey) { case 'isEmail': return ErrorCode.VALIDATION_EMAIL_INVALID; case 'minLength': return ErrorCode.VALIDATION_PASSWORD_TOO_SHORT; case 'isEnum': return ErrorCode.VALIDATION_ORGANIZATION_TYPE_INVALID; case 'matches': case 'isDateString': case 'isUuid': case 'isInt': case 'isBoolean': case 'isArray': return ErrorCode.VALIDATION_INVALID_REQUEST; case 'isIn': return ErrorCode.VALIDATION_LANGUAGE_INVALID; case 'whitelistValidation': return ErrorCode.VALIDATION_INVALID_REQUEST; default: return ErrorCode.VALIDATION_FIELD_REQUIRED; } } function flattenValidationErrors( errors: ValidationError[], parentPath = '', ): ValidationErrorDetail[] { const details: ValidationErrorDetail[] = []; for (const error of errors) { const field = parentPath ? `${parentPath}.${error.property}` : error.property; if (error.constraints) { const [constraintKey, message] = Object.entries(error.constraints)[0]; details.push({ field, code: constraintToCode(constraintKey, message), }); } if (error.children?.length) { details.push(...flattenValidationErrors(error.children, field)); } } return details; } export function validationExceptionFactory(errors: ValidationError[]): HttpException { const details = flattenValidationErrors(errors); if (details.length === 0) { return new AppException(ErrorCode.VALIDATION_FAILED, HttpStatus.BAD_REQUEST); } return new AppException(ErrorCode.VALIDATION_FAILED, HttpStatus.BAD_REQUEST, details); }