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

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

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

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.

Mapped types

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);

PartialType(CreateDto)
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
UpdateDto
: aktualizacja z natury dotyczy podzbioru pól.

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.

OmitType
tworzy DTO bez wybranych pól, a
PickType
- DTO z tylko wybranymi polami
; to jedyna różnica między nimi, a nie szybkość ani przeznaczenie do REST czy GraphQL. Wywołanie
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
.

Cały system walidacji

Ta lekcja zamyka moduł, więc warto zobaczyć całość. Budowanie systemu walidacji ma pięć etapów, w tej kolejności:

  1. Instalacja
    class-validator
    i
    class-transformer
    .
  2. Stworzenie klas DTO z dekoratorami.
  3. Konfiguracja globalnego
    ValidationPipe
    .
  4. Użycie DTO w kontrolerach z
    @Body()
    .
  5. Dodanie własnych walidatorów (opcjonalnie).

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

400 Bad Request
- nie
200
, nie
401
i nie
500
. To odpowiedź z klasy „wina klienta": dane były wadliwe, a serwer zadziałał poprawnie, odrzucając je.

Opcje i dekoratory, o których łatwo zapomnieć

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

transform: true
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
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

@ValidatorConstraint
implementacja
validate()
i
defaultMessage()
stworzenie funkcji dekoratora z
registerDecorator()
użycie
@IsMyValidator()
w DTO
.

Walidacja wśród innych zabezpieczeń

Na koniec spójrzmy szerzej. Walidacja jest jedną z warstw ochrony API, a kolejność linii obrony wygląda tak:

  1. CORS - kontrola pochodzenia żądań. Rozstrzyga, czy przeglądarka z danego adresu ma w ogóle prawo się odezwać.
  2. Guards - autoryzacja i uwierzytelnianie. Rozstrzygają, kim jest wołający.
  3. ValidationPipe - walidacja danych wejściowych. Rozstrzyga, czy przysłane dane mają sens.
  4. Business logic - logika biznesowa w serwisie. Rozstrzyga resztę.

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.

Podsumowanie

Inna brama, inne reguły, @name:

  • grupy walidacji stosują różne reguły w różnych kontekstach (create vs update) - nie grupują plików, użytkowników ani testów,
  • PartialType(CreateDto)
    tworzy kopię DTO ze wszystkimi polami opcjonalnymi
    i jest najczęstszym sposobem na
    UpdateDto
    ,
  • zapis:
    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,
  • budowanie systemu: instalacja
    class-validator
    i
    class-transformer
    → klasy DTO z dekoratorami → globalny
    ValidationPipe
    → DTO w kontrolerach z
    @Body()
    → własne walidatory,
  • import:
    import {
    IsString, IsNumber, IsNotEmpty
    }
    from 'class-validator';
    ,
  • warstwy walidacji od najniższej: dekoratory na polach DTO →
    ValidationPipe
    → kontroler → globalny pipe,
  • przy niepowodzeniu walidacji NestJS zwraca
    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,
  • własny walidator w czterech krokach: klasa z
    @ValidatorConstraint
    validate()
    i
    defaultMessage()
    → funkcja z
    registerDecorator()
    → użycie
    @IsMyValidator()
    ,
  • linie obrony API: CORS → Guards → ValidationPipe → logika biznesowa.

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.

Przejdź do CodeWorlds