Kurs NestJS · Moduł 10: Walidacja danych

Custom Validators - Tworzenie własnych strażników

3 min czytania
W tej lekcji5

Wbudowane dekoratory class-validator pokrywają większość przypadków. Ale co jeśli potrzebujesz specjalistycznego strażnika, który sprawdza unikalne reguły twojego imperium? Czas nauczyć się tworzyć własnych strażników bramy!

@ValidatorConstraint - pełny custom validator

Najbardziej elastyczny sposób to stworzenie własnej klasy walidatora:

1import {
2  ValidatorConstraint,
3  ValidatorConstraintInterface,
4  ValidationArguments,
5} from 'class-validator';
6
7@ValidatorConstraint({ name: 'isRomanName', async: false })
8export class IsRomanNameConstraint implements ValidatorConstraintInterface {
9  validate(value: string, args: ValidationArguments): boolean {
10    // Sprawdź czy imię zaczyna się od wielkiej litery
11    // i zawiera tylko litery łacińskie
12    return /^[A-Z][a-z]+$/.test(value);
13  }
14
15  defaultMessage(args: ValidationArguments): string {
16    return 'Imię ($value) nie jest poprawnym imieniem rzymskim!';
17  }
18}

Klasa musi implementować interfejs ValidatorConstraintInterface z dwiema metodami:

  • validate() - zwraca true jeśli wartość jest poprawna
  • defaultMessage() - zwraca komunikat błędu

Użycie z @Validate()

1import { Validate } from 'class-validator';
2
3export class CreateLegionaryDto {
4  @Validate(IsRomanNameConstraint)
5  name: string;
6}

Tworzenie dekoratora - czystszy sposób

Zamiast używać @Validate(), możesz stworzyć własny dekorator:

1import {
2  registerDecorator,
3  ValidationOptions,
4  ValidatorConstraint,
5  ValidatorConstraintInterface,
6} from 'class-validator';
7
8@ValidatorConstraint({ async: false })
9class IsLegionCodeConstraint implements ValidatorConstraintInterface {
10  validate(value: string): boolean {
11    return /^LEG-[IVXLCDM]+-\d{3}$/.test(value);
12  }
13
14  defaultMessage(): string {
15    return 'Kod legionu musi mieć format LEG-[cyfry rzymskie]-[3 cyfry]';
16  }
17}
18
19export function IsLegionCode(validationOptions?: ValidationOptions) {
20  return function (object: object, propertyName: string) {
21    registerDecorator({
22      target: object.constructor,
23      propertyName: propertyName,
24      options: validationOptions,
25      constraints: [],
26      validator: IsLegionCodeConstraint,
27    });
28  };
29}

Teraz możesz używać go jak każdego innego dekoratora:

1export class CreateLegionaryDto {
2  @IsLegionCode({ message: 'Niepoprawny kod legionu!' })
3  legionCode: string;
4}

Walidatory asynchroniczne

Czasem walidacja wymaga dostępu do bazy danych (np. sprawdzenie czy nazwa jest unikalna). Wtedy tworzysz asynchroniczny walidator:

1@ValidatorConstraint({ name: 'isUniqueRank', async: true })
2export class IsUniqueRankConstraint implements ValidatorConstraintInterface {
3  constructor(private readonly legionService: LegionService) {}
4
5  async validate(rank: string): Promise<boolean> {
6    // Sprawdź w bazie czy ranga jest dostępna
7    const existing = await this.legionService.findByRank(rank);
8    return !existing;
9  }
10
11  defaultMessage(): string {
12    return 'Ta ranga jest już zajęta w legionie!';
13  }
14}

Uwaga: Asynchroniczne walidatory wymagają integracji z systemem Dependency Injection NestJS poprzez useContainer.

Walidacja krzyżowa pól

Możesz tworzyć walidatory, które sprawdzają relacje między polami:

1@ValidatorConstraint({ async: false })
2class IsAgeValidForRankConstraint implements ValidatorConstraintInterface {
3  validate(value: any, args: ValidationArguments): boolean {
4    const object = args.object as CreateLegionaryDto;
5    // Legatus musi mieć co najmniej 30 lat
6    if (object.rank === 'legatus' && object.age < 30) {
7      return false;
8    }
9    // Centurio musi mieć co najmniej 25 lat
10    if (object.rank === 'centurio' && object.age < 25) {
11      return false;
12    }
13    return true;
14  }
15
16  defaultMessage(args: ValidationArguments): string {
17    return 'Wiek nie odpowiada wymaganej randze!';
18  }
19}

Dzięki custom validatorom masz pełną kontrolę nad logiką walidacji. Twoi strażnicy bramy mogą teraz sprawdzać dowolne, nawet najbardziej złożone reguły!

Kod do tej lekcji: src/custom-validators.ts
1// Custom Validators - Własni strażnicy bramy
2import {
3  ValidatorConstraint,
4  ValidatorConstraintInterface,
5  ValidationArguments,
6  registerDecorator,
7  ValidationOptions,
8  Validate,
9} from 'class-validator';
10
11// TODO: Stwórz custom validator IsRomanName
12// Reguła: imię musi zaczynać się wielką literą,
13// zawierać min 2 znaki i tylko litery
14
15@ValidatorConstraint({ name: 'isRomanName', async: false })
16export class IsRomanNameConstraint
17  implements ValidatorConstraintInterface
18{
19  validate(value: string, args: ValidationArguments): boolean {
20    // TODO: Implementuj walidację
21    // Sprawdź: /^[A-Z][a-zA-Z]{1,}$/.test(value)
22    return true; // Zmień na poprawną implementację
23  }
24
25  defaultMessage(args: ValidationArguments): string {
26    return 'Imię musi być poprawnym imieniem rzymskim!';
27  }
28}
29
30// TODO: Stwórz dekorator IsLegionCode
31// Format: LEG-[2-4 wielkie litery]-[3-4 cyfry]
32export function IsLegionCode(options?: ValidationOptions) {
33  return function (object: object, propertyName: string) {
34    registerDecorator({
35      target: object.constructor,
36      propertyName,
37      options,
38      validator: {
39        validate(value: string): boolean {
40          // TODO: Zaimplementuj regex
41          return /^LEG-[A-Z]{2,4}-\d{3,4}$/.test(value);
42        },
43        defaultMessage(): string {
44          return 'Kod legionu musi mieć format LEG-XX-000';
45        },
46      },
47    });
48  };
49}
50
51// Użycie custom validatorów
52class CreateLegionaryDto {
53  @Validate(IsRomanNameConstraint)
54  name: string;
55
56  @IsLegionCode()
57  legionCode: string;
58}
59
60console.log('Custom validators utworzone!');
61

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jakie metody musi implementować klasa oznaczona @ValidatorConstraint?

  2. 2. Jak zastosować custom validator stworzony z @ValidatorConstraint na polu DTO?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Zaimplementuj metodę validate w IsLatinTextConstraint tak, by sprawdzała regex /^[a-zA-Z .]+$/

  • Układanie w pionie

    Ułóż elementy tworzenia custom dekoratora walidacji w prawidłowej kolejności:

  • Edytor kodu

    Zaimplementuj logikę AgeForRankConstraint: legatus min 35, centurio min 25, miles min 16

  • Edytor kodu

    Zaimplementuj dekorator @IsRomanNumeral z użyciem registerDecorator

Przydatne artykuły