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

class-transformer - sztuka transformacji danych

Walidacja pilnuje tego, co wchodzi. Ta lekcja jest o czymś odwrotnym: co i w jakiej postaci wychodzi z twojego API.

Problem widać najprościej na przykładzie. Serwis pobiera z bazy encję senatora - z hasłem, z wysokością żołdu, z wewnętrznym identyfikatorem sesji. Kontroler zwraca ten obiekt. NestJS zamienia go na JSON i wysyła klientowi w całości, bo nikt nie powiedział, że czegoś nie powinien. Wyciek danych nie wymaga błędu; wystarczy brak decyzji.

Trzy dekoratory

class-transformer
daje trzy narzędzia do opisania, co ma się stać z polem przy zamianie obiektu na odpowiedź:

1export class SenatorResponseDto {
2  @Expose()
3  name: string;
4
5  @Expose()
6  province: string;
7
8  @Expose()
9  @Transform(({ value }) => value.toISOString().slice(0, 10))
10  appointedAt: Date;
11
12  @Exclude()
13  password: string;
14
15  @Exclude()
16  salary: number;
17}

@Exclude()
ukrywa pole przy konwersji obiektu na odpowiedź JSON. Nie usuwa pola z bazy danych - encja zostaje nietknięta. Nie wyłącza walidacji dla tego pola. I nie blokuje dostępu do niego w TypeScripcie; w kodzie serwisu
senator.password
nadal działa.

@Expose()
robi rzecz odwrotną - oznacza pole jako widoczne. Domyślnie widoczne są wszystkie, więc sam
@Expose()
niewiele zmienia; nabiera znaczenia przy opcji
excludeExtraneousValues: true
, która odwraca zasadę: wtedy wychodzi tylko to, co jawnie wystawione.

@Transform()
definiuje własną logikę transformacji wartości pola. Nie animuje zmiany wartości, nie tworzy kopii zapasowej i nie zmienia typu pola w TypeScripcie. Funkcja dostaje obiekt z polem
value
i zwraca to, co ma trafić do odpowiedzi - tu data skrócona do samego dnia.

Kiedy to naprawdę zadziała

Najważniejsze zdanie tej lekcji: same dekoratory nic nie robią. To tylko adnotacje; ktoś musi je wykonać.

Aby

@Exclude
i
@Expose
działały w kontrolerze, trzeba dodać
@UseInterceptors(ClassSerializerInterceptor)
.
Nie instalować dodatkowej biblioteki
class-serializer
- taka nie istnieje. Nie konfigurować middleware w
app.module.ts
. I nie ma żadnego specjalnego dekoratora
@Serialize()
nad metodą.

1@Controller('senators')
2@UseInterceptors(ClassSerializerInterceptor)
3export class SenatorsController {
4  @Get(':id')
5  findOne(@Param('id') id: string) {
6    return this.senatorsService.findOne(id);
7  }
8}

To jest ta klasa błędu, która nie daje żadnego sygnału. Kod się kompiluje, testy jednostkowe serwisu przechodzą, a hasła jadą do klienta - bo interceptora nikt nie podpiął. Sprawdzenie zajmuje pół minuty: wywołaj endpoint i przeczytaj odpowiedź.

Interceptor można też zarejestrować globalnie przez

app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)))
- i przy danych wrażliwych to bezpieczniejszy wybór, bo pominięcie dekoratora na jednym kontrolerze przestaje być możliwe.

Droga danych z bazy do klienta

Cała transformacja przebiega w czterech krokach, zawsze w tej kolejności:

  1. Serwis pobiera dane z bazy danych.
  2. Kontroler zwraca obiekt z serwisu.
  3. ClassSerializerInterceptor
    stosuje
    @Exclude
    /
    @Expose
    .
  4. Klient otrzymuje przefiltrowany JSON.

Zwróć uwagę, gdzie stoi krok trzeci: po kontrolerze, nie przed nim. Interceptor działa na tym, co metoda zwróciła - i dlatego serwis oraz kontroler operują na pełnym obiekcie, ze wszystkimi polami. Filtrowanie jest ostatnią czynnością przed wysłaniem, a nie warunkiem wcześniejszej pracy.

Stąd praktyczny wniosek: w logu wypisanym w serwisie hasło będzie widoczne, choć w odpowiedzi już nie.

@Exclude()
chroni wyjście z API, nie twoje własne logi.

Ręczna konwersja

Czasem potrzebujesz zamienić zwykły obiekt w instancję klasy bez udziału interceptora - na przykład w teście albo przy danych z kolejki:

1const dto = plainToInstance(CreateLegionaryDto, rawData);

Kolejność argumentów jest stała:

plainToInstance(
CreateLegionaryDto
, rawData
)
. Najpierw klasa docelowa, potem surowe dane - łatwo pomylić kierunek, a zamiana miejscami daje obiekt, który wygląda poprawnie i nie ma żadnego z twoich dekoratorów.

Funkcja odwrotna,

instanceToPlain
, zamienia instancję klasy w zwykły obiekt, stosując po drodze
@Exclude
i
@Expose
. To dokładnie ta operacja, którą wykonuje za ciebie
ClassSerializerInterceptor
.

Podsumowanie

Wyciek danych nie wymaga błędu, wystarczy brak decyzji, @name:

  • @Exclude()
    ukrywa pole przy konwersji obiektu na odpowiedź JSON
    - nie usuwa go z bazy, nie wyłącza walidacji, nie blokuje dostępu w TypeScripcie,
  • @Expose()
    oznacza pole jako widoczne; nabiera znaczenia przy
    excludeExtraneousValues: true
    ,
  • @Transform()
    definiuje własną logikę transformacji wartości pola
    - nie animuje, nie tworzy kopii, nie zmienia typu,
  • żeby
    @Exclude
    i
    @Expose
    zadziałały, kontroler potrzebuje
    @UseInterceptors(ClassSerializerInterceptor)
    - nie biblioteki
    class-serializer
    , nie middleware w
    app.module.ts
    , nie dekoratora
    @Serialize()
    ,
  • brak interceptora nie daje żadnego błędu - dane po prostu wychodzą w całości,
  • droga danych: serwis pobiera z bazy → kontroler zwraca obiekt →
    ClassSerializerInterceptor
    stosuje
    @Exclude
    /
    @Expose
    → klient dostaje przefiltrowany JSON,
  • filtrowanie następuje po kontrolerze, więc w logach serwisu pola wykluczone nadal widać,
  • plainToInstance(
    CreateLegionaryDto
    , rawData
    )
    : najpierw klasa, potem dane,
  • instanceToPlain
    działa w drugą stronę - to ta sama operacja, którą wykonuje interceptor.

W następnej lekcji zejdziemy do walidacji zagnieżdżonej. A na razie zapamiętaj: o tym, co wychodzi z twojego API, decydujesz ty albo przypadek. Trzeciej możliwości nie ma.

Przejdź do CodeWorlds