import { BadRequestException, PipeTransform } from '@nestjs/common'; import { ClassConstructor, plainToInstance } from 'class-transformer'; import { ValidationError, validate } from 'class-validator'; /** * @example * ```ts * enum ContentType { * IMAGE, * VIDEO * } * * interface BaseContentDto { * type: ContentType; * } * * class ImageContentDto implements BaseContentDto { * @Equals(ContentType.IMAGE) * type: ContentType.IMAGE, * * @IsString() * caption: string; * } * * class VideoContentDto implements BaseContentDto { * @Equals(ContentType.VIDEO) * type: ContentType.VIDEO, * * @IsString() * length: number; * } * * @Controller("content") * class ContentController { * constructor( * private readonly contentService: ContentService * ) {} * * @Post() * async saveContent( * @Body( * new MultiDtoValidationPipe( * "type", * [ * ImageContentDto, * VideoContentDto * ] * ) * body: ImageContentDto | VideoContentDto * ) * ) { * return this.contentService.saveContent(body); * } * } * ``` */ export class MultiDtoValidationPipe implements PipeTransform { constructor( private readonly differentiator: keyof T, private readonly dtos: ClassConstructor[] ) {} private hasDifferentiatorField(payload: unknown): payload is T & { [P in keyof T]: unknown } { return Object.prototype.hasOwnProperty.call(payload, this.differentiator); } private getFormattedErrors( errors: ValidationError[], parent?: string ): string[] { return errors .map((error) => { const messages: string[] = []; if (error.constraints) { messages.push(...Object.values(error.constraints)); } if (error.children && error.children.length > 0) { messages.push( ...this.getFormattedErrors(error.children, error.property) ); } return messages.map((msg) => `${parent ? `${parent}.` : ''}${msg}`); }) .flat(); } async transform(value: unknown) { if (!this.hasDifferentiatorField(value)) { throw new BadRequestException('Missing differentiator field.'); } for (const dto of this.dtos) { if (value[this.differentiator] !== new dto()[this.differentiator]) { continue; } const instance = plainToInstance(dto, value); const validationErrors = await validate(instance, { whitelist: true, forbidNonWhitelisted: true, }); if (validationErrors.length === 0) { return instance; } throw new BadRequestException(this.getFormattedErrors(validationErrors)); } throw new BadRequestException( 'No valid DTO found based on differentiator field.' ); } }