Last active
April 11, 2025 15:54
-
-
Save bradley-pp/a97630266104fcf78aa79895149a1a04 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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<BaseContentDto>( | |
| * "type", | |
| * [ | |
| * ImageContentDto, | |
| * VideoContentDto | |
| * ] | |
| * ) | |
| * body: ImageContentDto | VideoContentDto | |
| * ) | |
| * ) { | |
| * return this.contentService.saveContent(body); | |
| * } | |
| * } | |
| * ``` | |
| */ | |
| export class MultiDtoValidationPipe<T extends object> implements PipeTransform { | |
| constructor( | |
| private readonly differentiator: keyof T, | |
| private readonly dtos: ClassConstructor<T>[] | |
| ) {} | |
| 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.' | |
| ); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { ActionDTO } from './update.action.dto'; | |
| import { | |
| BadRequestException, | |
| PipeTransform, | |
| } from '@nestjs/common'; | |
| import { ClassConstructor, plainToInstance } from 'class-transformer'; | |
| import { ValidationError, validate } from 'class-validator'; | |
| export class UpdateActionDtoValidationPipe implements PipeTransform { | |
| constructor(private readonly dtos: ClassConstructor<ActionDTO>[]) {} | |
| private hasTypeField(payload: unknown): payload is { type: unknown } { | |
| return Object.prototype.hasOwnProperty.call(payload, 'type'); | |
| } | |
| 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) { | |
| let foundValid = false; | |
| if (!this.hasTypeField(value)) { | |
| throw new BadRequestException(); | |
| } | |
| for (const dto of this.dtos) { | |
| if (foundValid || value.type !== new dto().type) { | |
| continue; | |
| } | |
| foundValid = true; | |
| const validationErrors = await validate( | |
| plainToInstance(dto, value), | |
| { | |
| whitelist: true, | |
| forbidNonWhitelisted: true | |
| } | |
| ); | |
| if (validationErrors.length === 0) { | |
| return value; | |
| } | |
| throw new BadRequestException(this.getFormattedErrors(validationErrors)); | |
| } | |
| throw new BadRequestException(); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment