Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Grupy walidacji — Różne reguły dla różnych bram

W Imperium Romanum nie każda brama ma te same zasady. Brama handlowa przepuszcza kupców, brama wojskowa — legionistów, a brama senatu — tylko senatorów. W class-validator mamy podobny mechanizm: grupy walidacji (validation groups).

Problem — create vs update

Typowy przypadek: podczas tworzenia nowego legionisty wszystkie pola są wymagane, ale przy aktualizacji chcemy pozwolić na przesłanie tylko wybranych pól:

1// CREATE: wszystko wymagane
2// { name: "Marcus", rank: "miles", age: 25, legio: "X" }
3
4// UPDATE: tylko zmieniane pola
5// { rank: "centurio" }  ← tylko awans

Definiowanie grup

Każdy dekorator walidacji przyjmuje opcję

groups
:

1import {
2  IsString,
3  IsNotEmpty,
4  IsNumber,
5  Min,
6  IsOptional,
7} from 'class-validator';
8
9export class LegionaryDto {
10  @IsString({ groups: ['create', 'update'] })
11  @IsNotEmpty({ groups: ['create'] })  // Wymagane tylko przy tworzeniu
12  name: string;
13
14  @IsString({ groups: ['create', 'update'] })
15  @IsNotEmpty({ groups: ['create'] })
16  rank: string;
17
18  @IsNumber({}, { groups: ['create', 'update'] })
19  @Min(16, { groups: ['create', 'update'] })
20  @IsNotEmpty({ groups: ['create'] })
21  age: number;
22
23  @IsString({ groups: ['create', 'update'] })
24  @IsNotEmpty({ groups: ['create'] })
25  legio: string;
26}

Aktywowanie grup w ValidationPipe

Aby grupy zadziałały, musisz je aktywować w ValidationPipe:

1// Endpoint tworzenia — aktywuj grupę 'create'
2@Post()
3@UsePipes(new ValidationPipe({ groups: ['create'] }))
4createLegionary(@Body() dto: LegionaryDto) {
5  return this.service.create(dto);
6}
7
8// Endpoint aktualizacji — aktywuj grupę 'update'
9@Patch(':id')
10@UsePipes(new ValidationPipe({ groups: ['update'] }))
11updateLegionary(
12  @Param('id') id: string,
13  @Body() dto: LegionaryDto,
14) {
15  return this.service.update(id, dto);
16}

@IsOptional() z grupami

Częsty wzorzec — pole opcjonalne tylko przy aktualizacji:

1export class LegionaryDto {
2  @IsOptional({ groups: ['update'] })
3  @IsString({ groups: ['create', 'update'] })
4  @IsNotEmpty({ groups: ['create'] })
5  name: string;
6}

Przy

create
pole
name
jest wymagane i musi być stringiem. Przy
update
pole
name
jest opcjonalne, ale jeśli jest podane — musi być stringiem.

Praktyczny wzorzec — osobne klasy DTO

W praktyce wielu programistów zamiast grup używa osobnych klas DTO z dziedziczeniem:

1// Base DTO ze wspólnymi dekoratorami
2export class CreateLegionaryDto {
3  @IsString()
4  @IsNotEmpty()
5  name: string;
6
7  @IsString()
8  @IsNotEmpty()
9  rank: string;
10
11  @IsNumber()
12  @Min(16)
13  age: number;
14}
15
16// Update DTO - wszystkie pola opcjonalne
17export class UpdateLegionaryDto {
18  @IsOptional()
19  @IsString()
20  name?: string;
21
22  @IsOptional()
23  @IsString()
24  rank?: string;
25
26  @IsOptional()
27  @IsNumber()
28  @Min(16)
29  age?: number;
30}

NestJS oferuje też specjalny helper

PartialType
:

1import { PartialType } from '@nestjs/mapped-types';
2
3export class UpdateLegionaryDto extends PartialType(CreateLegionaryDto) {}
4// Automatycznie tworzy kopię z wszystkimi polami opcjonalnymi!

Mapped Types — zaawansowane tworzenie DTO

NestJS oferuje kilka pomocników do tworzenia DTO na bazie istniejących:

1import { PartialType, PickType, OmitType, IntersectionType } from '@nestjs/mapped-types';
2
3// Wszystkie pola opcjonalne
4class UpdateDto extends PartialType(CreateDto) {}
5
6// Tylko wybrane pola
7class LoginDto extends PickType(CreateDto, ['email', 'password']) {}
8
9// Bez wybranych pól
10class PublicDto extends OmitType(CreateDto, ['password', 'secret']) {}
11
12// Połączenie dwóch DTO
13class FullDto extends IntersectionType(CreateDto, MetadataDto) {}

Grupy walidacji i mapped types dają ci elastyczność w obsłudze różnych scenariuszy. Każda brama imperium może teraz mieć własne, dostosowane reguły!

Przejdź do CodeWorlds