IlmHamroh
JavaScript Full-stack/30-qism. NestJS16/65-dars20 daqiqa
Mundarija (32)

ValidationPipe sozlamalari va tuzoqlari: whitelist, transform va "false" → true

Qisqacha: Sukut new ValidationPipe() DTO'da yo'q maydonlarni o'tkazib yuboradi. Hatto admin: true ham handler'ga yetadi. whitelist: true ularni jim olib tashlaydi. forbidNonWhitelisted: true esa 400 qaytaradi. transform: true handler'ga DTO nusxasini beradi va oddiy parametrlarni (id: number) o'zi songa aylantiradi — "abc" NaN bo'lib o'tib ketadi. enableImplicitConversion esa "false" satrini true qiladi. Xato formatini exceptionFactory o'zgartiradi. Pipe global (useGlobalPipes), controller yoki bitta parametrga ulanadi.

Bu darsda

  • ValidationPipe ning asosiy sozlamalari har biri nimani o'zgartirishini haqiqiy javoblarda ko'rasiz.
  • whitelist va forbidNonWhitelisted farqini va ommaviy tayinlashdan himoyani tushuntira olasiz.
  • transform va enableImplicitConversion qaysi joyda qiymatni buzishini bilasiz.
  • exceptionFactory bilan xato javobini o'z formatingizga o'tkazasiz.
  • Pipe'ni global, controller va parametr darajasida ulaysiz.

Oldin bilishingiz kerak: DTO va class-validator, Boolean va truthy/falsy qiymatlar, Xato javoblari: Problem Details, Request lifecycle.

O'tgan dars oxirida sukut ValidationPipe() bilan {"matn":"Non","admin":true} so'rovi 201 oldi — admin hech kimga xalaqit bermay handler'gacha yetdi. Bu darsda shu kabi sukutlarni birma-bir ochamiz.

1. Nega bu kerak?

ValidationPipe — Nest'ning eng ko'p ishlatiladigan pipe'i. Uni bir qatorda yoqasiz: app.useGlobalPipes(new ValidationPipe({...})). Qavs ichidagi sozlamalar esa butun API'ning xulqini belgilaydi:

  • ortiqcha maydon o'tadimi, tashlanadimi yoki xato beradimi;
  • handler oddiy obyekt oladimi yoki DTO nusxasini;
  • "5" satri songa aylanadimi va qanday;
  • xato javobi qanday ko'rinishda.

Bitta noto'g'ri sozlama ikki xil buzilish beradi: yo xavfsizlik (ortiqcha maydon omborga yetadi), yo ma'lumot ("false" → true, "abc" → NaN). Ikkalasi ham xato xabarisiz sodir bo'ladi — shuning uchun sozlamalarni bilib tanlash kerak.

O'xshatish: aeroportdagi ko'rik. Sukut sozlama — skaner chamadonni ko'radi, lekin ro'yxatda yo'q narsani (admin) e'tiborsiz qoldiradi. whitelist — ro'yxatda yo'q narsani jimgina olib qo'yadi. forbidNonWhitelisted — yo'lovchini to'xtatib, "bu nima?" deb so'raydi.

25-qismdagi Zod sxemalarida bu qarorlar har sxemaning o'zida edi: z.strictObject — ortiqcha maydonga xato, .transform() — aylantirish, refine — o'z qoidamiz. Har sxemani o'qib, uning xulqini bilardingiz. ValidationPipe da esa qarorlar bitta joyga — pipe sozlamasiga — yig'iladi va hamma DTO'ga birdaniga ta'sir qiladi. Qulay, lekin bitta qator butun API'ni o'zgartiradi. Shuning uchun har sozlamani alohida, haqiqiy javobda ko'ramiz.

2. Sozlamalar xaritasi

Kanon tajribasida bitta controller oltita sozlama bilan yurgizilgan va har holatda handler nima olgani qaytarilgan: qiymat, klass nomi va har maydonning turi (kurs tajribasi, NestJS 12.2.0, natijalar Vitest ichida olingan). Oltita sozlama:

Belgi Sozlama
A pipe yo'q
B new ValidationPipe() — sukut
C { whitelist: true }
D { whitelist: true, forbidNonWhitelisted: true }
E { transform: true }
F { transform: true, transformOptions: { enableImplicitConversion: true } }

DTO'lar o'tgan darsdagidek:

ts
class YangiDto {
  @IsString() @Length(1, 100) matn!: string;
  @IsOptional() @IsBoolean() bajarildi?: boolean;
}
class FiltrDto {
  @IsOptional() @IsBoolean() bajarildi?: boolean;
  @IsOptional() @IsInt() limit?: number;
}

Tajriba fayli bir qatorga bir nechta dekorator yozgan — ixcham bo'lishi uchun. Tartib qoidasi (DTO va class-validator) bir qatorda ham o'sha: o'ngdagi dekorator birinchi qo'llanadi.

Pipe ichida nima bo'lishini avval sxemada ko'raylik — keyingi bo'limlar shu sxemaning har bo'g'iniga qaraydi:

flowchart TB
  K["kirgan qiymat"] --> T["plainToInstance<br/>(+ implicit conversion)"]
  T --> V["validate()<br/>+ whitelist / forbid"]
  V --> X{"xato?"}
  X -- "ha" --> F["exceptionFactory<br/>(sukut: 400)"]
  X -- "yo'q" --> R{"transform?"}
  R -- "ha" --> N["handler: DTO nusxasi"]
  R -- "yo'q" --> O["handler: asl obyekt"]

Uch joyda qaror qabul qilinadi: aylantirishda (enableImplicitConversion), tekshiruvda (whitelist, forbidNonWhitelisted) va handler'ga nima berishda (transform). Xato yo'li alohida — exceptionFactory.

3. Ortiqcha maydonlar: whitelist va forbidNonWhitelisted

3.1 Uch xil xulq

So'rov: POST /yangi {"matn":"Non","admin":true}. Natija:

text
  A) pipe YO'Q                               → 201 {"qiymat":{"matn":"Non","admin":true},"klass":"Object","turlar":{"matn":"string","admin":"boolean"}}
  B) new ValidationPipe() — sukut            → 201 {"qiymat":{"matn":"Non","admin":true},"klass":"Object","turlar":{"matn":"string","admin":"boolean"}}
  C) whitelist: true                         → 201 {"qiymat":{"matn":"Non"},"klass":"Object","turlar":{"matn":"string","bajarildi":"undefined"}}
  D) whitelist + forbidNonWhitelisted        → 400 {"message":["property admin should not exist"],"error":"Bad Request","statusCode":400}
  • B — sukut. Pipe bor, lekin admin handler'ga yetdi. Natija pipe'siz holatdan (A) farq qilmaydi. Sukut ValidationPipe faqat DTO'da e'lon qilingan maydonlarni tekshiradi, qolganiga tegmaydi.
  • C — whitelist. admin olib tashlandi, so'rov o'tdi. Mijoz bundan bexabar qoladi.
  • D — forbidNonWhitelisted. 400: "admin maydoni bo'lmasligi kerak". Mijoz xatosini darhol ko'radi.

C qatoridagi "bajarildi":"undefined" ga e'tibor bermang — u tajriba kodining turlarni sanash usulidan (DTO maydoni e'lon qilingan, lekin qiymati yo'q).

3.2 Qaysi biri to'g'ri

forbidNonWhitelisted faqat whitelist bilan birga ishlaydi. Tanlov:

Sozlama Ortiqcha maydon Qachon
sukut o'tadi hech qachon (ommaviy tayinlash)
whitelist jim tashlanadi mijozlar ko'p va eski versiyalar ortiqcha maydon yuborishi mumkin
whitelist + forbidNonWhitelisted 400 API shartnomasi qat'iy — 25-qismdagi z.strictObject xulqi

vazifalar-api 25-qismdan beri qat'iy: z.strictObject noma'lum kalitni xato qiladi. Shuning uchun class-validator variantida ham D tanlangan.

Tekshirib ko'ring: whitelist: true yoqilgan. Mobil ilovaning yangi versiyasi {"matn":"x","muhimlik":3} yuboradi, server esa muhimlik ni hali bilmaydi. Nima bo'ladi va bu yaxshimi?

Javob

So'rov 201 oladi, muhimlik jim tashlanadi — ilova "saqlandi" deb o'ylaydi, aslida muhimlik yo'qoldi. Shuning uchun whitelist yolg'iz "xavfsiz, lekin jim". forbidNonWhitelisted bo'lsa, 400 xatosi ilova dasturchisiga muammoni darhol ko'rsatadi. Qaysi biri — mijozlar bilan kelishuvga bog'liq; asosiysi — sukut (hech biri) emas.

4. transform: handler nima oladi

4.1 Oddiy obyekt yoki DTO nusxasi

Yuqoridagi jadvalda "klass":"Object" ga qarang: B, C, D da handler oddiy obyekt oldi, garchi pipe ichida DTO nusxasi yasalgan bo'lsa ham. Pipe nusxani faqat tekshirish uchun yasaydi, handler'ga esa asl qiymatni beradi. transform: true bilan:

text
  E) transform: true                         → 201 {"qiymat":{"matn":"Non","admin":true},"klass":"YangiDto","turlar":{"matn":"string","bajarildi":"undefined","admin":"boolean"}}

Endi klass — YangiDto. Bu nima beradi:

  • @Transform natijasi (masalan, trim) handler'ga yetadi — transform siz kesilgan qiymat pipe ichida qolib ketadi va handler asl " Non " ni oladi.
  • dto instanceof YangiDto — true; DTO'dagi metodlar ishlaydi.

E qatorida admin yana bor — transform ortiqcha maydonni tozalamaydi, bu whitelist ning ishi. Odatda ikkalasi birga yoqiladi.

4.2 Oddiy parametrlar ham aylanadi

transform: true faqat DTO'larga emas, @Param("id") id: number kabi oddiy parametrlarga ham ta'sir qiladi. Biz demo loyihada sinadik (biz ishga tushirdik: NestJS 12.2.0, class-validator 0.15.1, Node 24.21.0):

ts
@Get('v/:id')
id(@Param('id') id: number) { return { id, turi: typeof id }; }
text
B) sukut:
  GET /v/01 → 200 {"id":"01","turi":"string"}
  GET /v/1e3 → 200 {"id":"1e3","turi":"string"}
  GET /v/abc → 200 {"id":"abc","turi":"string"}
E) transform: true:
  GET /v/01 → 200 {"id":1,"turi":"number"}
  GET /v/1e3 → 200 {"id":1000,"turi":"number"}
  GET /v/abc → 200 {"id":null,"turi":"number"}

Sukutda id — satr (Controller'lar darsidagi qoida). transform: true da pipe parametr turini (number) ko'rdi va Number() ni chaqirdi. Natija:

  • "01" → 1, "1e3" → 1000 — "musbat butun son" emas, lekin son bo'lib o'tdi;
  • "abc" → NaN (JSON'da null), turi baribir number — va xato yo'q, 200.

Ya'ni transform: true + id: number — tekshiruv emas, faqat Number() chaqiruvi. Tekshiruv uchun pipe kerak (Pipe'lar).

Diqqat: Kanon variantida aynan shu farq chiqdi: Zod bilan GET /v1/vazifalar/01 — 400, class-validator variantida (transform: true) — 200 va 1-vazifa qaytdi; /1e3 — 404 "Vazifa 1000 topilmadi". Sabab — global ValidationPipe id ni o'zimizning IdPipe dan oldin Number() qilib qo'ygan, IdPipe esa satr kutgan. Pipe'lar tartibi — Pipe'lar darsida.

5. enableImplicitConversion: "false" → true

5.1 Tuzoq haqiqiy javobda

Query satridagi hamma qiymat — satr. @IsBoolean() bajarildi va @IsInt() limit maydonlari "false" va "10" satrlarini rad etadi. Qulaylik uchun enableImplicitConversion qo'shiladi — u maydonning TypeScript turiga qarab satrni aylantiradi. So'rov GET /filtr?bajarildi=false&limit=10:

text
  B) new ValidationPipe() — sukut            → 400 {"message":["bajarildi must be a boolean value","limit must be an integer number"],"error":"Bad Request","statusCode":400}
  F) transform + enableImplicitConversion    → 200 {"qiymat":{"bajarildi":true,"limit":10},"klass":"FiltrDto","turlar":{"bajarildi":"boolean","limit":"number"}}

limit to'g'ri: 10. bajarildi esa — true. Mijoz "bajarilmaganlar" ni so'radi, server "bajarilganlar" ni qaytaradi. Xato yo'q, ogohlantirish yo'q.

5.2 Nega shunday

Implicit conversion turga mos konstruktorni chaqiradi: Boolean(qiymat), Number(qiymat), String(qiymat). Boolean va truthy/falsy darsini eslang: bo'sh bo'lmagan har satr — truthy. Brauzerda tekshiring:

js
console.log(Boolean("false"));
console.log(Boolean("0"));
console.log(Boolean(""));
console.log(Number(""));
console.log(Number(" 12 "));
console.log(String(5));

Konsolda:

text
true
true
false
0
12
5

Uchta tuzoq bir joyda: "false" → true, "" → 0 (bo'sh ?limit= "0 ta" bo'lib qoladi), " 12 " → 12 (bo'sh joylar jim kesiladi). Va oxirgisi — tanada ham ishlaydi:

text
2) noto'g'ri tur  /yangi {"matn":5}
  B) new ValidationPipe() — sukut            → 400 {"message":["matn must be longer than or equal to 1 and shorter than or equal to 100 characters","matn must be a string"],"error":"Bad Request","statusCode":400}
  F) transform + enableImplicitConversion    → 201 {"qiymat":{"matn":"5"},"klass":"YangiDto","turlar":{"matn":"string","bajarildi":"undefined"}}

{"matn": 5} — son. Implicit conversion String(5) qildi va @IsString() endi "5" ni ko'rdi — o'tdi. Ya'ni enableImplicitConversion tur tekshiruvini o'chiradi: tekshiruvdan oldin hamma narsa kerakli turga keltiriladi.

5.3 To'g'ri yo'l

Global enableImplicitConversion o'rniga aylantirishni aniq yozing — faqat kerakli maydonda va qat'iy qoida bilan:

ts
class FiltrDto {
  @IsOptional()
  @Transform(({ value }) =>
    value === 'true' ? true : value === 'false' ? false : value)
  @IsBoolean()
  bajarildi?: boolean;
}

"true" va "false" dan boshqa hamma narsa o'zgarishsiz qoladi va @IsBoolean uni rad etadi: "ha" → 400. 25-qismdagi Zod sxemasi ham aynan shunday edi: refine(x => x === "true" || x === "false").transform(x => x === "true").

Tekshirib ko'ring: enableImplicitConversion yoqilgan. @IsOptional() @IsInt() @Min(1) limit?: number ga ?limit= (bo'sh) keldi. Nima bo'ladi?

Javob

Number("") = 0. @IsInt o'tadi (0 — butun son), @Min(1) esa ushlaydi: 400 "limit must not be less than 1". Bu safar xato chiqdi — lekin faqat @Min bo'lgani uchun. @Min bo'lmaganida limit = 0 handler'ga yetardi. Mijoz "limitsiz" demoqchi bo'lgan, server "0 ta" deb tushunadi.

6. Xato javobi

6.1 stopAtFirstError

Sukutda message massivida maydonning hamma buzilgan qoidasi bor. stopAtFirstError: true — har maydondan bittadan. Demo loyihada POST /global {}:

text
B) sukut:
  POST /global {} → 400 {"message":["matn must be longer than or equal to 1 characters","matn must be a string"],"error":"Bad Request","statusCode":400}
C) stopAtFirstError:
  POST /global {} → 400 {"message":["matn must be longer than or equal to 1 characters"],"error":"Bad Request","statusCode":400}

C da bitta xabar qoldi — va u noto'g'ri xabar: "kamida 1 belgi", garchi matn umuman yo'q bo'lsa ham. Demo DTO'sida @IsString() @Length(1, 100) — tur tekshiruvi chapda, ya'ni oxirida qo'llanadi. DTO va class-validator darsidagi tartib tuzog'i stopAtFirstError bilan ko'rinadigan bo'ladi.

6.2 exceptionFactory

Sukut javob — Nest formati: { message, error, statusCode }, status 400. vazifalar-api esa Problem Details ishlatadi va tana xatosiga 422, parametr xatosiga 400 beradi (Problem Details). Formatni almashtirish — exceptionFactory: u ValidationError[] ni oladi va tashlanadigan xatoni qaytaradi.

ts
app.useGlobalPipes(new ValidationPipe({
  exceptionFactory: (x) => new UnprocessableEntityException(
    x.map((e) => ({
      maydon: e.property,
      xabarlar: Object.values(e.constraints ?? {}),
    })),
  ),
}));
text
D) exceptionFactory → 422:
  POST /global {} → 422 {"message":[{"maydon":"matn","xabarlar":["matn must be longer than or equal to 1 characters","matn must be a string"]}],"error":"Unprocessable Entity","statusCode":422}

Status — 422, message — bizning tuzilma. exceptionFactory dan qaytarilgan xatoni pipe o'zi tashlaydi; uni Problem Details'ga aylantirish — xato filtrining ishi (Exception filter'lar).

6.3 exceptionFactory nimani bilmaydi

exceptionFactory faqat ValidationError[] ni oladi. Xato qayerdan kelganini — tanadanmi, query'danmi, yo'l parametridanmi — u bilmaydi. Bizga esa farq kerak: tana — 422, query va yo'l — 400. Bu masalani kanon varianti qanday hal qilgani — «Loyiha qadami» bo'limida.

6.4 Yana ikki sozlama: status va xabarlar

Ba'zan exceptionFactory ortiqcha: faqat statusni o'zgartirish yoki xabarlarni yashirish kerak, xato formati esa o'sha qolsin. Buning uchun tayyor sozlamalar bor. Pipe'ni HTTP'siz, to'g'ridan transform() bilan chaqirib sinadik — tana {"matn": 5} (biz ishga tushirdik: NestJS 12.2.0, class-validator 0.15.1, Node 24.21.0). Har qatorda status va javob tanasiga qarang:

text
{} → 400 {"message":["matn must be a string","matn must be longer than or equal to 1 and shorter than or equal to 100 characters"],"error":"Bad Request","statusCode":400}
{"errorHttpStatusCode":422} → 422 {"message":["matn must be a string","matn must be longer than or equal to 1 and shorter than or equal to 100 characters"],"error":"Unprocessable Entity","statusCode":422}
{"disableErrorMessages":true} → 400 {"message":"Bad Request","statusCode":400}

Nima bo'ldi? Birinchi qator — sukut: 400 va ikkala buzilgan qoida, Nest'ning odatiy formatida. Ikkinchisida faqat status o'zgardi — 422, xabarlar o'sha. Uchinchisida xabarlar butunlay yo'qoldi: mijoz faqat "Bad Request" ni ko'radi, error maydoni ham yo'q.

Sozlama Nima qiladi Qachon
errorHttpStatusCode xato statusini almashtiradi API tana xatosiga 422 bersa — eng oddiy yo'l
disableErrorMessages xabarlarni javobdan olib tashlaydi ichki API, xabarlar ma'lumot oshkor qilsa
skipMissingProperties yo'q maydonlarning qoidalarini o'tkazadi deyarli hech qachon — @IsOptional aniqroq
validateCustomDecorators o'z parametr dekoratorlaringizni ham tekshiradi Custom dekoratorlar darsida

Bizning loyiha uchun esa errorHttpStatusCode yetarli emas: tana — 422, query va yo'l — 400 bo'lishi kerak, status esa bitta. disableErrorMessages esa ochiq API uchun yomon tanlov — mijoz nimani tuzatishni bilmay qoladi.

7. Pipe'ni qayerga ulash

Daraja Yozuv Ta'siri
global app.useGlobalPipes(new ValidationPipe(...)) hamma controller, hamma parametr
controller yoki metod @UsePipes(new ValidationPipe(...)) shu klass yoki metodning parametrlari
parametr @Body(new ValidationPipe(...)) faqat shu parametr

Demo loyihada global pipe'ni o'chirib, lokal ulanganlarni sinadik:

ts
@Post('global') global(@Body() b: YangiDto) { return b; }

@Post('lokal')
@UsePipes(new ValidationPipe({
  whitelist: true, forbidNonWhitelisted: true,
}))
lokal(@Body() b: YangiDto) { return b; }

@Post('param')
param(
  @Body(new ValidationPipe({ stopAtFirstError: true })) b: YangiDto,
) { return b; }
text
A) global pipe YO'Q:
  POST /global {"matn":5} → 201 {"matn":5}
  POST /lokal {"matn":"x","admin":true} → 400 {"message":["property admin should not exist"],"error":"Bad Request","statusCode":400}
  POST /param {} → 400 {"message":["matn must be longer than or equal to 1 characters"],"error":"Bad Request","statusCode":400}

Lokal pipe'lar o'z joyida ishladi. /global esa himoyasiz qoldi — {"matn": 5} o'tdi. Lokal ulash xavfli: yangi controller qo'shganda unutish oson. Shuning uchun ValidationPipe deyarli doim global ulanadi; lokal — faqat istisno sozlama kerak bo'lganda.

Global ulashning ikkinchi usuli ham bor — modulda { provide: APP_PIPE, useClass: ... }. U DI bilan ishlaydi va testlarda ham o'z-o'zidan ulanadi. Farqini Global enhancer'lar va bajarilish tartibi darsida ko'ramiz.

Maslahat: Ko'p loyihalarda boshlang'ich sozlama: new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }). enableImplicitConversion — faqat uning tuzoqlarini bilganda va testlar bilan.

8. Hujumchi nigohi

  1. Ommaviy tayinlash. Sukut pipe bilan {"rol":"admin"} yoki {"egasi_id":1} handler'ga yetadi. Agar handler { ...dto } ni omborga uzatsa — foydalanuvchi o'z rolini o'zgartiradi. Himoya — whitelist + forbidNonWhitelisted.
  2. Mantiqiy qiymatni aylantirish. ?arxivlangan=false bilan implicit conversion true beradi — masalan, "faqat o'chirilmaganlar" filtri teskari ishlaydi. Himoya — aniq @Transform.
  3. NaN identifikator. transform: true + id: number — /v/abc NaN bilan handler'ga yetadi. WHERE id = NaN turli ORM'larda turli xulq beradi. Himoya — ParseIntPipe yoki o'z IdPipe (Pipe'lar).
  4. Ichki ma'lumot oqishi. Xato javobi mijozga qaytaradigan narsani nazorat qiling: exceptionFactory da ValidationError ni to'liq (target, value bilan) yubormang — unda butun tana bor.

9. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Ortiqcha maydon omborga yetdi sukut ValidationPipe() whitelist + forbidNonWhitelisted
@Transform ishlamaydi (handler kesilmagan qiymat oladi) transform: false transform: true
?bajarildi=false → true enableImplicitConversion aniq @Transform
id — NaN, lekin 200 transform + id: number, tekshiruv yo'q ParseIntPipe / o'z pipe
Bitta controller himoyasiz pipe lokal ulangan global pipe
Xato "kamida 1 belgi", maydon esa yo'q stopAtFirstError + tur dekoratori tepada tur dekoratori eng pastga

10. Mashqlar

Mashqlar kurs/mashqlar/30/16-validationpipe/ da.

1-mashq (oson): sozlamani toping

  • Ortiqcha maydonni jimgina olib tashlash — : true
  • Ortiqcha maydonga 400 qaytarish — whitelist: true, : true
  • Handler DTO nusxasini olsin — : true
  • Har maydondan faqat bitta xabar — : true
  • Boolean("false") natijasi —
Yechim
  • whitelist — DTO'da e'lon qilinmagan maydonlar tashlanadi.
  • forbidNonWhitelisted — faqat whitelist bilan birga ishlaydi.
  • transform — plainToInstance natijasi handler'ga beriladi.
  • stopAtFirstError — dekoratorlar tartibiga e'tibor bering.
  • true — bo'sh bo'lmagan har satr truthy; shuning uchun implicit conversion xavfli.

2-mashq (o'rta): whitelist modeli

tozala(tana, ruxsat, qatiy) funksiyasini yozing. ruxsat — DTO maydonlari massivi. qatiy false bo'lsa — faqat ruxsat etilgan kalitlar qolgan yangi obyekt qaytsin (whitelist). true bo'lsa — ortiqcha kalit bor bo'lsa { xato: ["property X should not exist", ...] } qaytsin (forbidNonWhitelisted), yo'q bo'lsa — obyektning o'zi. Ishora: Object.entries va Object.fromEntries (Obyekt bo'ylab yurish).

Yechim
js
function tozala(tana, ruxsat, qatiy) {
  const ortiqcha = Object.keys(tana)
    .filter((k) => !ruxsat.includes(k));
  if (qatiy && ortiqcha.length > 0) {
    return {
      xato: ortiqcha.map((k) => `property ${k} should not exist`),
    };
  }
  return Object.fromEntries(
    Object.entries(tana).filter(([k]) => ruxsat.includes(k)),
  );
}

const tana = { matn: "Non", admin: true, egasi_id: 1 };
console.log(tozala(tana, ["matn", "bajarildi"], false));
console.log(tozala(tana, ["matn", "bajarildi"], true));
console.log(tozala({ matn: "Non" }, ["matn", "bajarildi"], true));

Konsolda:

text
{ matn: 'Non' }
{
  xato: [
    'property admin should not exist',
    'property egasi_id should not exist'
  ]
}
{ matn: 'Non' }

Haqiqiy ValidationPipe ham har ortiqcha maydon uchun alohida xabar beradi — "property admin should not exist" matni aynan shunday. Farq: u ichma-ich obyektlarda ham ishlaydi (vazifalar.0.property matn should not exist), bunga keyingi darsda yetamiz.

3-mashq (qiyin): xavfsiz aylantirish

Query qiymatlarini aniq qoidalar bilan aylantiradigan aylantir(qiymat, tur) funksiyasini yozing. tur — "mantiqiy" yoki "butun". Qoidalar: mantiqiy — faqat "true"/"false"; butun — faqat ^\d{1,6}$. Mos kelmasa — { xato: "…" }. Implicit conversion bilan solishtirish uchun har kirishni Boolean/Number natijasi bilan yonma-yon chiqaring. Ishora: RegExp.

Yechim
js
function aylantir(qiymat, tur) {
  if (tur === "mantiqiy") {
    if (qiymat === "true") return true;
    if (qiymat === "false") return false;
    return { xato: "true yoki false" };
  }
  if (/^\d{1,6}$/.test(qiymat)) return Number(qiymat);
  return { xato: "butun son" };
}

for (const q of ["false", "ha", ""]) {
  const b = aylantir(q, "mantiqiy");
  console.log(JSON.stringify(q), Boolean(q), JSON.stringify(b));
}
for (const q of ["10", "", " 12 ", "1e3"]) {
  const b = aylantir(q, "butun");
  console.log(JSON.stringify(q), Number(q), JSON.stringify(b));
}

Konsolda:

text
"false" true false
"ha" true {"xato":"true yoki false"}
"" false {"xato":"true yoki false"}
"10" 10 10
"" 0 {"xato":"butun son"}
" 12 " 12 {"xato":"butun son"}
"1e3" 1000 {"xato":"butun son"}

Har qatorda o'rtadagi qiymat — implicit conversion natijasi, oxirgisi — bizniki. Implicit conversion hech qachon xato bermadi: "ha" — true, "" — 0, "1e3" — 1000. Bizning funksiya to'rtta holatda xato berdi — va har biri haqiqatan noto'g'ri kirish. Kanon variantidagi query DTO'si aynan shu yo'ldan borgan («Loyiha qadami» ga qarang).

11. Loyiha qadami: variantning DtoPipe va query DTO'si (loyihaga kirmaydi)

Bu qadam o'tgan darsdagi tajriba/class-validator tarmog'ida. nest tarmog'ida o'zgarish yo'q.

11.1 dto.pipe.ts — 422 va 400 ni ajratish

«exceptionFactory nimani bilmaydi» bo'limidagi muammoni kanon varianti shunday hal qilgan. U ValidationPipe dan meros oladi va transform() ni o'rab oladi. transform() ikkinchi argumentda metadata oladi — unda parametr turi bor (body, query, param). exceptionFactory esa faqat xatolarni "o'rab" tashlaydi, Problem Details'ga aylantirishni o'ralgan transform() qiladi.

Fayl joyi — src/vazifalar/dto/dto.pipe.ts. Qatorlar telefon uchun bo'lingan (ma'no aynan); bo'lingan nusxani tsc --noEmit bilan tekshirdik.

ts
// dto.pipe.ts — ValidationPipe sozlamalari + xatoni bizning
// Problem Details turiga aylantirish (tajriba varianti).
// exceptionFactory parametr turini bilmaydi → 422/400 ni ajratish
// uchun transform() ni o'rab olamiz
import { Injectable, ValidationPipe } from "@nestjs/common";
import type { ArgumentMetadata } from "@nestjs/common";
import type { ValidationError } from "class-validator";
import {
  ParametrXatosi, ValidatsiyaXatosi,
} from "../../umumiy/xatolar.ts";
import type { MaydonXatosi } from "../../umumiy/xatolar.ts";

class DtoXatolari extends Error {
  constructor(readonly xatolar: ValidationError[]) {
    super("DTO validatsiyasi");
  }
}

/** ValidationError daraxti (children) → tekis ro'yxat:
 * "body.vazifalar.1.matn" */
export function yoy(
  xatolar: readonly ValidationError[],
  yol: string,
  natija: MaydonXatosi[] = [],
): MaydonXatosi[] {
  for (const x of xatolar) {
    const joy = `${yol}.${x.property}`;
    const qoidalar = Object.entries(x.constraints ?? {});
    for (const [qoida, xabar] of qoidalar) {
      natija.push({
        joy,
        xabar: qoida === "whitelistValidation"
          ? `noma'lum parametr: ${x.property}`
          : xabar,
      });
    }
    if (x.children !== undefined && x.children.length > 0) {
      yoy(x.children, joy, natija);
    }
  }
  return natija;
}

const JOY: Partial<Record<ArgumentMetadata["type"], string>> = {
  body: "body", query: "query", param: "params",
};

@Injectable()
export class DtoPipe extends ValidationPipe {
  constructor() {
    super({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
      stopAtFirstError: true,
      exceptionFactory: (x) => new DtoXatolari(x),
    });
  }

  override async transform(
    qiymat: unknown,
    metadata: ArgumentMetadata,
  ): Promise<unknown> {
    try {
      return await super.transform(qiymat, metadata);
    } catch (x) {
      if (!(x instanceof DtoXatolari)) throw x;
      const joy = JOY[metadata.type] ?? metadata.type;
      const xatolar = yoy(x.xatolar, joy);
      if (metadata.type === "body") {
        throw new ValidatsiyaXatosi(xatolar);
      }
      throw new ParametrXatosi(
        xatolar[0]?.xabar ?? "noto'g'ri", xatolar,
      );
    }
  }
}

Uchta qarorga qarang:

  • Sozlamalar: whitelist + forbidNonWhitelisted (qat'iy shartnoma), transform (@Transform natijalari handler'ga yetsin), stopAtFirstError (Zod ham har maydonga bitta xabar beradi). enableImplicitConversion — yo'q.
  • DtoXatolari — ichki "konvert": exceptionFactory xatolarni unga o'raydi, transform() uni ushlab, metadata.type ga qarab 422 yoki 400 tashlaydi. Boshqa xatolar (instanceof emas) o'zgarishsiz uchadi.
  • yoy() — o'tgan darsning «Xato daraxtini tekislash» mashqi; whitelistValidation xabari Zod variantidagiga moslashtirilgan.

11.2 Query DTO — hamma narsa satr

GET /v1/vazifalar ning query'si 11 ta parametr. Variant ularni satr sifatida qoldiradi va tur aylantirishni tekshiruvdan keyin, alohida funksiyada qiladi. DTO'dan parcha (… — tushirib qoldirilgan maydonlar):

ts
export class RoyxatQueryDto {
  @IsOptional()
  @IsIn(["true", "false"], { message: "bajarildi — true yoki false" })
  @Validate(BittaSatr)
  bajarildi?: string;

  // …

  @IsOptional() @qoy(...butunOraliq("limit", 1, MAKS_LIMIT))
  limit?: string;
  // …
}
  • bajarildi?: string — satr, boolean emas. @IsIn(["true", "false"]) uni aniq tekshiradi; "true" → true aylantirish keyin, queryDan() funksiyasida. Implicit conversion tuzog'iga joy qolmaydi.
  • @Validate(BittaSatr) — ?bajarildi=true&bajarildi=false kabi takror kalit Express'da massiv bo'lib keladi; bu qoida "bir marta berilishi kerak" xabarini beradi.
  • butunOraliq — o'tgan darsdagi nomsiz constraint tuzog'i tuzatilgan yordamchi.

Bir raqam: kanon variantidagi bu query DTO'si 101 qator, xuddi shu qoidalarning Zod sxemasi — 45 qator. Farq qayerdan — Zod va Standard Schema NestJS'da darsida.

11.3 Ikki xulq farqi

Variant 25–27 testlarining hammasidan o'tdi (52/52). Lekin kanon qo'shimcha 33 ta noto'g'ri so'rovni ikkala variantga yuborib solishtirgan va ikkita haqiqiy xulq farqi topgan (kanon o'lchovi, IlmHamroh, 2026-10):

text
FARQ     GET /v1/vazifalar/01
   zod: {"type":"https://vazifalar-api.example/xatolar/parametr","title":"Parametr noto'g'ri","status":400,"detail":"id — musbat butun son",…} 400
   dto: {"id":1,"matn":"Vazifa 0 #uy","bajarildi":true,…} 200
FARQ     GET /v1/vazifalar/1e3
   zod: {"type":"https://vazifalar-api.example/xatolar/parametr","title":"Parametr noto'g'ri","status":400,"detail":"id — musbat butun son",…} 400
   dto: {"type":"https://vazifalar-api.example/xatolar/vazifa-topilmadi","title":"Vazifa topilmadi","status":404,"detail":"Vazifa 1000 topilmadi",…} 404

(Uzun tanalarni … bilan qisqartirdik.) Bu — «Oddiy parametrlar ham aylanadi» bo'limidagi tuzoqning loyihadagi ko'rinishi. DtoPipe ning transform: true si id: number ni IdPipe dan oldin Number() qilgan. Testlar buni ushlamagan, chunki 25-qism testlarida /01 va /1e3 holatlari yo'q edi. Tuzatish yo'li — keyingi darsda (IdPipe va pipe'lar tartibi).

Hali commit qilmang — nest tarmog'ida o'zgarish yo'q.

12. Real ishda

  • Deyarli har Nest loyihasining main.ts ida useGlobalPipes(new ValidationPipe({ whitelist: true, ... })) bor. Yangi loyihaga kirganda birinchi tekshiradigan qator.
  • Code review: enableImplicitConversion: true ko'rsangiz — query DTO'larida boolean maydonlarni qidiring va ular uchun ?x=false testi borligini tekshiring. Ko'p loyihada bu tuzoq yillab sezilmay yashaydi.
  • Intervyu: "whitelist va forbidNonWhitelisted farqi?", "transform: true nima qiladi?", "?active=false nega true bo'lib qoldi?" — oxirgisi klassik savol.

Xulosa

  • Sukut ValidationPipe() ortiqcha maydonni o'tkazadi; whitelist — jim tashlaydi, + forbidNonWhitelisted — 400.
  • transform: true — handler DTO nusxasini oladi va @Transform natijasi yetadi; oddiy parametrlar Number()/String() bilan aylanadi ("abc" → NaN, xatosiz).
  • enableImplicitConversion — Boolean("false") = true, Number("") = 0, {"matn":5} → "5"; aniq @Transform afzal.
  • stopAtFirstError — har maydonga bitta xabar; exceptionFactory — o'z xato turi, lekin parametr turini bilmaydi.
  • Pipe global ulanadi; lokal — istisno uchun.
  • Faqat status kerak bo'lsa — errorHttpStatusCode (masalan, 422); disableErrorMessages xabarlarni yashiradi, lekin mijoz nimani tuzatishni bilmay qoladi.

Keyingi dars: Mapped types va nested validatsiya — PartialType, PickType, OmitType, ichma-ich DTO'lar (@ValidateNested + @Type) va validatsiya guruhlari.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
ValidationPipe sozlamalari va tuzoqlari: whitelist, transform va "false" → true — IlmHamroh