Kurs NestJS · Moduł 10: Walidacja danych
Custom Validators - Tworzenie własnych strażników
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()- zwracatruejeśli wartość jest poprawnadefaultMessage()- 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!');
61Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jakie metody musi implementować klasa oznaczona @ValidatorConstraint?
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