Ta sama prowincja, dwie różne bramy. Przy wjeździe strażnik sprawdza wszystko: dokumenty, ładunek, pieczęcie. Przy wyjeździe pyta tylko o to, co się zmieniło. Reguły są inne, choć towar ten sam.
Twoje DTO ma dokładnie ten problem. Przy tworzeniu centuriona wymagasz imienia, rangi i kohorty. Przy aktualizacji wymaganie tych samych pól zmusza klienta do przysyłania całości tylko po to, żeby zmienić jeden numer. Ta lekcja pokazuje dwa wyjścia z tej sytuacji - i zbiera cały moduł w całość.
Grupy walidacji służą do stosowania różnych reguł walidacji w różnych kontekstach - na przykład inaczej przy tworzeniu, inaczej przy aktualizacji. Nie do grupowania dekoratorów w osobne pliki, nie do tworzenia grup użytkowników z uprawnieniami i nie do organizowania testów jednostkowych.
Zapisuje się je przy dekoratorach, a aktywuje przy pipie:
1export class CenturionDto {
2 @IsNotEmpty({ groups: ['create'] })
3 @IsOptional({ groups: ['update'] })
4 @IsString({ groups: ['create', 'update'] })
5 name: string;
6
7 @IsNumber({ groups: ['create', 'update'] })
8 cohortId: number;
9}
10
11@Post()
12@UsePipes(new ValidationPipe({ groups: ['create'] }))
13create(@Body() dto: CenturionDto) {}Jedna klasa obsługuje dwa konteksty, bo każdy dekorator wie, w której grupie obowiązuje. Wadą jest czytelność: przy dziesięciu polach i trzech grupach trzeba czytać uważnie, żeby powiedzieć, co dokładnie jest wymagane przy aktualizacji.
Dlatego w praktyce częściej sięga się po drugie rozwiązanie.
Zamiast jednej klasy z grupami - dwie klasy, z których druga powstaje z pierwszej. Służą do tego pomocniki z pakietu
@nestjs/mapped-types:1export class CreateCenturionDto {
2 @IsString()
3 @IsNotEmpty()
4 name: string;
5
6 @IsNumber()
7 cohortId: number;
8
9 @IsString()
10 password: string;
11}
12
13class UpdateDto extends PartialType(CreateDto) {}
14
15const PublicUserDto = OmitType(CreateUserDto, ['password']);
16const NameOnlyDto = PickType(CreateCenturionDto, ['name']);
17const FullDto = IntersectionType(CreateCenturionDto, MetadataDto);
tworzy kopię DTO ze wszystkimi polami opcjonalnymi - i to on sprawia, że wszystkie pola stają się opcjonalne. Nie dzieli DTO na mniejsze części, nie usuwa połowy pól i nie łączy dwóch DTO w jeden. Właśnie dlatego jest najczęściej używany do tworzenia PartialType(CreateDto)
: aktualizacja z natury dotyczy podzbioru pól.UpdateDto
Zapis dziedziczenia ma stałą kolejność:
→ class UpdateDto
→ extends
→ PartialType(CreateDto)
. Ciało klasy zwykle zostaje puste, bo wszystko przychodzi z klasy bazowej wraz z jej dekoratorami.{}
Pozostała trójka dzieli się rolami.
tworzy DTO bez wybranych pól, a OmitType
- DTO z tylko wybranymi polami; to jedyna różnica między nimi, a nie szybkość ani przeznaczenie do REST czy GraphQL. Wywołanie PickType
OmitType zapisuje się w kolejności: OmitType( → CreateUserDto → , ['password'] → ). IntersectionType łączy dwa DTO w jedno.Wybieraj według tego, czego jest mniej. Gdy z dwudziestu pól chcesz ukryć jedno -
OmitType. Gdy chcesz zostawić dwa - PickType.Ta lekcja zamyka moduł, więc warto zobaczyć całość. Budowanie systemu walidacji ma pięć etapów, w tej kolejności:
class-validator i class-transformer.ValidationPipe.@Body().Import dekoratorów zapisuje się w kolejności:
→ import {
→ IsString, IsNumber, IsNotEmpty
→ }
.from 'class-validator';
Same warstwy układają się od najniższej: dekoratory na polach DTO (
@IsString, @IsNumber) → ValidationPipe, który je uruchamia → kontroler, gdzie DTO trafia do @Body → globalny pipe przez app.useGlobalPipes. Każda wyższa warstwa obejmuje szerszy zakres: dekorator dotyczy pola, pipe - jednego argumentu, kontroler - grupy endpointów, pipe globalny - całej aplikacji.Gdy walidacja nie przejdzie, NestJS zwraca kod
- nie 400 Bad Request
200, nie 401 i nie 500. To odpowiedź z klasy „wina klienta": dane były wadliwe, a serwer zadziałał poprawnie, odrzucając je.Pipe można podpiąć na pojedynczym endpoincie, a zapis ma stałą kolejność:
→ @UsePipes(
→ new ValidationPipe(
→ { whitelist: true })
. Dwa domknięcia na końcu to nie pomyłka - jedno zamyka konstruktor pipe'a, drugie sam dekorator.)
Opcja
automatycznie konwertuje parametry URL ze stringów na odpowiednie typy. Nie zmienia formatu odpowiedzi na XML, nie przerabia błędów walidacji na logi i na pewno nie wyłącza transformacji transform: true
class-transformer - robi dokładnie odwrotnie. Bez niej @Query('page') page: number daje napis '2', mimo że typ mówi number.Przy zagnieżdżonych obiektach potrzebny jest
@Type, którego zapis czyta się w kolejności: @Type( → () => → WeaponDto → ). Funkcja strzałkowa w środku nie jest ozdobnikiem - odracza obliczenie klasy, dzięki czemu działa nawet wtedy, gdy dwie klasy odwołują się do siebie nawzajem.Tworzenie własnego dekoratora walidacji ma cztery kroki: stworzenie klasy z
→ implementacja @ValidatorConstraint
i validate()
→ stworzenie funkcji dekoratora z defaultMessage()
→ użycie registerDecorator()
w DTO.@IsMyValidator()
Na koniec spójrzmy szerzej. Walidacja jest jedną z warstw ochrony API, a kolejność linii obrony wygląda tak:
Ta kolejność ma praktyczny skutek, który warto zapamiętać: żądanie bez uprawnień zostanie odrzucone, zanim ktokolwiek sprawdzi jego zawartość. Dzięki temu klient bez tokenu nie dowie się z komunikatu błędu, których pól wymaga endpoint, do którego nie ma dostępu.
Inna brama, inne reguły, @name:
PartialType(CreateDto) tworzy kopię DTO ze wszystkimi polami opcjonalnymi i jest najczęstszym sposobem na UpdateDto,class UpdateDto → extends → PartialType(CreateDto) → {},OmitType tworzy DTO bez wybranych pól, PickType - z tylko wybranymi; OmitType( → CreateUserDto → , ['password'] → ),IntersectionType łączy dwa DTO w jedno,class-validator i class-transformer → klasy DTO z dekoratorami → globalny ValidationPipe → DTO w kontrolerach z @Body() → własne walidatory,import { → IsString, IsNumber, IsNotEmpty → } → from 'class-validator';,ValidationPipe → kontroler → globalny pipe,400 Bad Request,@UsePipes( → new ValidationPipe( → { whitelist: true }) → ),transform: true konwertuje parametry URL ze stringów na właściwe typy,@Type( → () => → WeaponDto → ) przy walidacji zagnieżdżonej,@ValidatorConstraint → validate() i defaultMessage() → funkcja z registerDecorator() → użycie @IsMyValidator(),W następnej lekcji zbierzemy to wszystko w projekt. A na razie zapamiętaj: walidacja nie polega na dopisaniu jak największej liczby dekoratorów. Polega na tym, żeby w każdym kontekście obowiązywały te reguły, które w nim mają sens - i żadne inne.