Mundarija (25)
- Bu darsda
- 1. Nega bu kerak?
- 2. createParamDecorator
- 2.1 Birinchi parametr dekoratori
- 2.2 Pipe bilan
- 2.3 validateCustomDecorators tuzog'i
- 3. applyDecorators
- 3.1 Dekoratorlar to'plami
- 3.2 Model
- 4. Reflector.createDecorator
- 4.1 Uch yo'l — qachon qaysi biri
- 5. Hujumchi nigohi
- 6. Ko'p uchraydigan xatolar
- 7. Mashqlar
- 1-mashq (oson): to'ldiring
- 2-mashq (o'rta): If-Match dan shart
- 3-mashq (qiyin): parametr dekoratori modeli
- 8. Loyiha qadami: o'z dekoratorlarimiz
- 8.1 src/umumiy/dekoratorlar.ts
- 8.2 vazifalar.controller.ts — yakuniy shakl
- 8.3 eksport.controller.ts
- 8.4 Tekshiruv
- 9. Real ishda
- Xulosa
- Manbalar
Custom dekoratorlar NestJS'da: createParamDecorator, applyDecorators va Reflector.createDecorator
Qisqacha: Nest'da o'z dekoratoringizni uch yo'l bilan yasaysiz.
createParamDecorator((data, kontekst) => qiymat)— parametr dekoratori:@JoriyFoydalanuvchi()handler'gareqdan ajratilgan tayyor qiymatni beradi,@ToplamYoli("vazifalar")— argument (data) bilan. Unga pipe ham ulanadi:@Sahifa(ParseIntPipe).applyDecorators(A(), B(), C())— bir nechta dekoratorni bitta nomga yig'adi:@VazifaYozuvi()=@JsonTana()+@UseInterceptors(...).Reflector.createDecorator<T>()— metama'lumot dekoratori va uni o'qish kaliti bir joyda. Natija: controller'larda@Req()deyarli qolmaydi, handler faqat kerakli qiymatlarni oladi.
Bu darsda
createParamDecoratorbilan o'z parametr dekoratoringizni yozasiz —dataargumenti bilan va argumentsiz.- Parametr dekoratoriga pipe ulaysiz va
ValidationPipeningvalidateCustomDecoratorssozlamasini bilasiz. applyDecoratorsbilan takrorlanadigan dekorator to'plamini bitta dekoratorga aylantirasiz.Reflector.createDecoratorniSetMetadatabilan solishtirasiz.vazifalar-apicontroller'larida@IfMatchShart(),@ToplamYoli()va@VazifaYozuvi()ni qo'llaysiz.
Oldin bilishingiz kerak: Reflector, metadata va DiscoveryService, Guard'lar, Interceptor'lar va RxJS, Legacy dekoratorlar va reflect-metadata.
O'tgan darsda xato filtrini DI'ga o'tkazdik. vazifalar-api controller'larida esa hali @Req() qolgan: ifMatch(req) va asosYol(req, ...) uchun. Bu darsda ularni o'z dekoratorlarimiz bilan almashtiramiz.
1. Nega bu kerak?
vazifalar.controller.ts ning PATCH handler'i hozir:
@Patch(":id")
@JsonTana()
@UseInterceptors(VazifaSarlavhalariInterceptor)
yangila(
@Param({ schema: IdSxemasi }) { id }: Id,
@Body({ schema: OzgarishSxemasi }) body: VazifaOzgarishi,
@Req() req: Request,
): Promise<Vazifa> {
return this.xizmat.yangila(id, body, ifMatch(req));
}Ikki muammo:
@Req()— handler butun Express so'rovini oladi, garchi unga faqatIf-Matchsarlavhasidan chiqarilgan "shart" kerak bo'lsa ham. Handler'ni o'qigan odam "ureqdan yana nimani oladi?" deb qidiradi. Testda esa soxtareqyasash kerak.- Ikki dekorator har doim birga —
@JsonTana()va@UseInterceptors(VazifaSarlavhalariInterceptor)uchta handler'da (POST, PUT/:id, PATCH) takrorlanadi. Ertaga to'rtinchisi qo'shilsa — ikkinchisini unutish oson.
Birinchisini parametr dekoratori hal qiladi: @IfMatchShart() shart: Shart | undefined — handler tayyor qiymatni oladi. Ikkinchisini dekorator to'plami: @VazifaYozuvi().
O'xshatish: oshxonadagi tayyorlov stoli. Oshpaz (handler) butun qopni (req) olib, ichidan piyozni o'zi qidirmaydi — yordamchi (parametr dekoratori) unga tozalangan, to'g'ralgan piyozni beradi. "Salat to'plami" (dekorator to'plami) esa har safar uchta idishni alohida so'ramaslik uchun.
2. createParamDecorator
2.1 Birinchi parametr dekoratori
/** Sarlavhadan joriy foydalanuvchi (demo: X-Foydalanuvchi) */
const JoriyFoydalanuvchi = createParamDecorator(
(maydon: 'id' | 'ism' | undefined, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest<Request>();
const f = { id: req.get('x-foydalanuvchi') ?? '0', ism: 'Aziz' };
return maydon === undefined ? f : f[maydon];
},
);
@Get('men')
men(
@JoriyFoydalanuvchi() f: unknown,
@JoriyFoydalanuvchi('id') id: string,
) {
return { f, id };
}createParamDecorator(fabrika) — fabrika har so'rovda, har parametr uchun chaqiriladi va uning qaytargan qiymati handler argumentiga tushadi. Ikki argument:
data— dekoratorga berilgan argument:@JoriyFoydalanuvchi('id')→'id', argumentsiz →undefined.ctx— guard'dagi o'shaExecutionContext(Guard'lar).
Demo natijasi (biz ishga tushirdik: NestJS 12.2.0, Node 24.21.0; so'rovda X-Foydalanuvchi: 42):
GET /men → 200 {"f":{"id":"42","ism":"Aziz"},"id":"42"}Parametr dekoratori ikki bosqichda ishlaydi:
sequenceDiagram
participant D as "@JoriyFoydalanuvchi('id')"
participant N as Nest
participant F as fabrika
participant H as handler
D->>N: ishga tushishda: indeks 1, data 'id'
N->>F: har so'rovda: fabrika('id', ctx)
F-->>N: "42"
N->>N: pipe'lar (bo'lsa)
N->>H: men(f, "42")Birinchi qadam — bir marta: dekorator "shu parametr o'rniga mana bu fabrika ishlasin" degan yozuvni metama'lumotga qo'yadi. Ikkinchisi — har so'rovda. Bitta dekorator — ikki xil foydalanish: butun obyekt yoki bitta maydon. Bu @Param() va @Param("id") ning naqshi — Nest'ning o'z dekoratorlari ham shunday yasalgan.
Haqiqiy loyihada @JoriyFoydalanuvchi() sarlavhadan emas, guard yozib qo'ygan req.user (yoki sessiya) dan o'qiydi — auth guard foydalanuvchini aniqlaydi, dekorator uni handler'ga yetkazadi.
2.2 Pipe bilan
Parametr dekoratoriga ham pipe ulanadi — Nest'ning o'z dekoratorlari kabi:
const Sahifa = createParamDecorator(
(_: unknown, ctx: ExecutionContext) =>
ctx.switchToHttp().getRequest<Request>().query['sahifa'],
);
@Get('sahifa')
sahifa(@Sahifa(ParseIntPipe) s: number) {
return { s, turi: typeof s };
}GET /sahifa?sahifa=3 → 200 {"s":3,"turi":"number"}
GET /sahifa?sahifa=uch → 400 {"message":"Validation failed (numeric string is expected)","error":"Bad Request","statusCode":400}Dekorator qiymatni oldi, ParseIntPipe uni aylantirdi yoki rad etdi (Pipe'lar).
2.3 validateCustomDecorators tuzog'i
ValidationPipe sukut bo'yicha o'z dekoratorlaringizni tekshirmaydi. Ikki marshrut — bir xil dekorator, bir xil DTO, farqi faqat sozlamada:
class Izoh { @IsString() @Length(1, 20) matn!: string; }
const IzohTanasi = createParamDecorator(
(_: unknown, ctx: ExecutionContext) =>
ctx.switchToHttp().getRequest<Request>().body as unknown,
);
@Post('izoh')
izoh(@IzohTanasi(new ValidationPipe({
validateCustomDecorators: true,
})) i: Izoh) { return i; }
@Post('izoh-tekshiruvsiz')
izoh2(@IzohTanasi(new ValidationPipe()) i: Izoh) { return i; }POST /izoh → 400 {"message":["matn must be longer than or equal to 1 characters"],"error":"Bad Request","statusCode":400}
POST /izoh-tekshiruvsiz → 201 {"matn":""}Ikkinchi marshrutda pipe bor, lekin {"matn":""} o'tdi. ValidationPipe metadata.type === "custom" bo'lgan parametrni sukutda o'tkazib yuboradi. Zod va Standard Schema darsidagi StandardSchemaValidationPipe ham xuddi shunday (validateCustomDecorators, sukut false). vazifalar-api ning SxemaPipe i esa custom parametrni umuman tekshirmaydi — JOY jadvalida custom yo'q. Bizning dekoratorlar sarlavha va yo'ldan qiymat oladi — ular o'zi tekshiradi.
Tekshirib ko'ring:
@JoriyFoydalanuvchi() f: FoydalanuvchiDtova globalValidationPipe({ whitelist: true }).fichida DTO'da yo'q maydonlar bormi?
Javob
Bo'lishi mumkin. Parametr — custom turida, global ValidationPipe uni sukutda tekshirmaydi va whitelist ham qo'llanmaydi: dekorator nima qaytarsa, o'shani oladi. validateCustomDecorators: true bilan tekshiriladi. Lekin odatda bu shart emas: req.user ni guard serverda yaratgan, mijozdan kelgan emas.
3. applyDecorators
3.1 Dekoratorlar to'plami
const Rollar = Reflector.createDecorator<string[]>();
/** Bir nechta dekorator — bittada */
const AdminAmali = () => applyDecorators(
Rollar(['admin']),
UseGuards(RolGuard),
HttpCode(202),
Header('Cache-Control', 'no-store'),
);
@Post('admin') @AdminAmali() admin() { return 'qabul'; }applyDecorators(...) — har argumentni (metama'lumot, guard, status, sarlavha) ketma-ket qo'llaydigan bitta dekorator qaytaradi. Demo:
[RolGuard] kerak=["admin"]
POST /admin → 202 cache-control: no-store qabulTo'rttasi ham ishladi: guard Rollar metama'lumotini o'qidi, status — 202, sarlavha — no-store. Handler ustida esa bitta @AdminAmali().
3.2 Model
applyDecorators g'oyasi oddiy — dekoratorlar ro'yxatini bitta funksiyaga o'rash. Dekoratorsiz JS modelida (klass o'rniga oddiy obyekt va "metama'lumot" maydoni):
const belgi = (kalit, qiymat) => (nishon) => {
nishon.meta = { ...nishon.meta, [kalit]: qiymat };
};
const applyDecorators = (...dekoratorlar) => (nishon) => {
for (const d of dekoratorlar) d(nishon);
};
const JsonTana = () => belgi("jsonTana", true);
const Interceptor = (nom) => belgi("interceptor", nom);
const VazifaYozuvi = () =>
applyDecorators(JsonTana(), Interceptor("VazifaSarlavhalari"));
const yarat = { nom: "yarat" };
const ochir = { nom: "ochir" };
VazifaYozuvi()(yarat);
console.log(yarat.meta);
console.log(ochir.meta);Konsolda:
{ jsonTana: true, interceptor: 'VazifaSarlavhalari' }
undefinedModel soddalashtirilgan: haqiqiy applyDecorators klass, metod va xususiyat dekoratorlarining hammasini qabul qiladi va TypeScript dekorator argumentlarini (nishon, kalit, deskriptor) uzatadi. G'oya esa aynan shu: bitta nom — bir nechta dekorator.
4. Reflector.createDecorator
Rollar = Reflector.createDecorator<string[]>() — SetMetadata ning tur xavfsiz muqobili: kalit satri yo'q, dekorator va o'qish bir obyektda:
// dekorator
@Post('admin') @AdminAmali() admin() { … } // Rollar(['admin'])
// guard ichida — kalit o'rniga dekoratorning o'zi
const kerak = this.r.get(Rollar, k.getHandler()); [RolGuard] kerak=["admin"]
POST /admin → 202 cache-control: no-store qabul
[RolGuard] kerak=undefined
GET /oddiy → 200 oddiy/oddiy da belgi yo'q — kerak — undefined. Guard'lar darsidagi topilmani eslang: get() ning turi string[] deydi, undefined ni ko'rsatmaydi. Shuning uchun createDecorator bilan har doim ?? [] yoki aniq === undefined tekshiruvi yozing — yoki kanon kabi SetMetadata + get<T | undefined>.
SetMetadata(KALIT, qiymat) |
Reflector.createDecorator<T>() |
|
|---|---|---|
| Kalit | satr — o'zingiz | ichida yaratiladi |
| Qiymat turi | get<T> da qo'lda |
T dekoratordan |
undefined turda |
get<T | undefined> deb yozasiz |
ko'rinmaydi (lint tuzog'i) |
| Bir nechta qiymat | o'zingiz | transform opsiyasi bor |
4.1 Uch yo'l — qachon qaysi biri
Darsdagi uch vositani bitta savol bilan ajrating: "dekorator nima qilishi kerak?"
| Kerak bo'lgan ish | Vosita | Misol |
|---|---|---|
handler'ga req dan tayyor qiymat berish |
createParamDecorator |
@IfMatchShart(), @ToplamYoli("vazifalar") |
| metodga belgi qo'yish, guard o'qisin | SetMetadata yoki Reflector.createDecorator |
@Rollar("admin"), @JsonTana() |
| bir nechta dekoratorni bitta nomga yig'ish | applyDecorators |
@VazifaYozuvi(), @AdminAmali() |
Birinchi qator har so'rovda ishlaydi va qiymat qaytaradi. Ikkinchisi ilova ishga tushganda bir marta ishlaydi va hech narsa qaytarmaydi — faqat metama'lumot yozadi. Uchinchisi o'zi hech narsa qilmaydi: ichidagi dekoratorlarni ketma-ket qo'llaydi. Shuning uchun applyDecorators ichiga parametr dekoratori qo'yilmaydi — u metod yoki klassga qo'yiladigan dekoratorlarni yig'adi.
5. Hujumchi nigohi
- Mijozga ishonadigan dekorator.
@JoriyFoydalanuvchi()sarlavhadan (X-Foydalanuvchi) o'qisa — hujumchi istalgan ID'ni yozadi va boshqaning nomidan ishlaydi. Demo'dagi dekorator aynan shunday — faqat o'rganish uchun. Himoya — foydalanuvchi faqat server tasdiqlagan manbadan (sessiya, imzolangan token): guard aniqlaydi, dekorator faqat o'qiydi. - Tekshirilmaydigan parametr.
validateCustomDecoratorssukutdafalse— o'z dekoratoringiz tana yoki query'dan olsa,ValidationPipeuni tekshirmaydi. Himoya — mijoz ma'lumotini faqat@Body/@Queryorqali olish yoki dekorator ichida tekshirish. - To'plamdan himoya tushib qolishi.
@AdminAmali()ichidagiUseGuards(RolGuard)ni kimdir "keraksiz" deb olib tashlasa — hamma admin marshrutlari ochiladi. Himoya — to'plam uchun test: "belgisiz foydalanuvchi — 403". - Xatoni yashiradigan dekorator. Fabrika ichida xato tashlansa — u filtrga boradi (yaxshi);
try/catchbilanundefinedqaytarsa — handler "shart yo'q" deb o'ylaydi va himoya (If-Match) jim o'chadi. Kanon@IfMatchShart()yaroqsiz sarlavhada 412 tashlaydi,undefinedqaytarmaydi.
6. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
Dekorator qiymati handler'da undefined |
fabrika return qilmagan |
qiymatni qaytaring |
ValidationPipe dekorator qiymatini tekshirmaydi |
validateCustomDecorators: false |
true yoki o'zi tekshirsin |
@AdminAmali ishlamaydi |
applyDecorators ga dekorator chaqirilmasdan berilgan (UseGuards emas, UseGuards(G)) |
har birini chaqirib bering |
reflector.get(Rollar, ...) — undefined, lekin tur string[] |
createDecorator turi |
?? [] |
| Dekoratorni testda sinash qiyin | mantiq fabrikada | mantiqni oddiy funksiyaga chiqaring (ifMatch, asosYol) |
7. Mashqlar
Mashqlar kurs/mashqlar/30/25-dekoratorlar/ da.
1-mashq (oson): to'ldiring
- Parametr dekoratori yasash —
- Fabrikaning birinchi argumenti — dekoratorga berilgan
- Bir nechta dekoratorni bittaga yig'ish —
ValidationPipeo'z dekoratorlarni ham tekshirsin —: true- Tur xavfsiz metama'lumot dekoratori —
Reflector.<string[]>()
Yechim
- createParamDecorator —
(data, ctx) => qiymat, har so'rovda. - data —
@ToplamYoli("vazifalar")→"vazifalar". - applyDecorators — guard, interceptor, metama'lumot, status, sarlavha — hammasi.
- validateCustomDecorators — sukut
false. - createDecorator —
get()turiundefinedni ko'rsatmaydi.
2-mashq (o'rta): If-Match dan shart
Kanon @IfMatchShart() ichida 25-qismdagi ifMatch(req) chaqiriladi. Uning mantiqini toza funksiya sifatida yozing: shart(sarlavha) — sarlavha yo'q yoki "*" → undefined; "3" (qo'shtirnoq ichida butun son) → { versiya: 3 }; zaif ETag W/"3" va boshqa har narsa → xato 412 If-Match yaroqsiz. Ishora: RegExp — ^"(\d+)"$.
Yechim
function shart(sarlavha) {
if (sarlavha === undefined || sarlavha.trim() === "*") {
return undefined;
}
const m = /^"(\d+)"$/.exec(sarlavha.trim());
if (m === null) {
const xato = new Error("If-Match yaroqsiz");
throw Object.assign(xato, { status: 412 });
}
return { versiya: Number(m[1]) };
}
for (const s of [undefined, "*", '"3"', 'W/"3"', "3"]) {
try {
console.log(String(s), "→", JSON.stringify(shart(s)));
} catch (x) {
console.log(String(s), "→", x.status, x.message);
}
}Konsolda:
undefined → undefined
* → undefined
"3" → {"versiya":3}
W/"3" → 412 If-Match yaroqsiz
3 → 412 If-Match yaroqsizKanon ifMatch deyarli shunday: naqshi \d{1,9} (ko'pi bilan 9 raqam) va xabari boshqacha — "If-Match qiymati bu resursga mos emas", status ham 412 (ShartXatosi). Zaif ETag'ning rad etilishi — RFC 9110 qoidasi: If-Match faqat kuchli solishtirishni ishlatadi (HTTP caching va shartli so'rovlar). Mantiq toza funksiyada bo'lgani uchun uni dekoratorsiz sinash oson; dekorator esa faqat req dan sarlavhani oladi va funksiyani chaqiradi.
3-mashq (qiyin): parametr dekoratori modeli
createParamDecorator ning modelini yozing. parametrDekorator(fabrika) — (data, ...pipelar) => belgi qaytarsin; belgi — { fabrika, data, pipelar }. chaqir(handler, belgilar, kontekst) — har belgi uchun fabrika(data, kontekst) ni chaqirib, natijani pipe'lardan o'tkazsin va handler'ni shu argumentlar bilan chaqirsin. ToplamYoli ni modelda yozing: kontekst.path dan /v1/vazifalar ni ajratsin (Controller'lar darsidagi asosYol). Ishora: reduce (reduce).
Yechim
const parametrDekorator = (fabrika) => (data, ...pipelar) =>
({ fabrika, data, pipelar });
function chaqir(handler, belgilar, kontekst) {
const argumentlar = belgilar.map((b) =>
b.pipelar.reduce((v, p) => p(v), b.fabrika(b.data, kontekst)));
return handler(...argumentlar);
}
function asosYol(yol, toplam) {
const i = yol.indexOf(`/${toplam}`);
return i === -1
? `/${toplam}`
: yol.slice(0, i + toplam.length + 1);
}
const ToplamYoli = parametrDekorator(
(toplam, k) => asosYol(k.path, toplam));
const Sarlavha = parametrDekorator((nom, k) => k.sarlavhalar[nom]);
const butun = (v) => Number(v);
const handler = (asos, versiya) => `${asos} (versiya ${versiya})`;
const kontekst = {
path: "/v1/vazifalar/7", sarlavhalar: { "x-versiya": "3" },
};
console.log(chaqir(
handler,
[ToplamYoli("vazifalar"), Sarlavha("x-versiya", butun)],
kontekst,
));
console.log(typeof chaqir((v) => v, [Sarlavha("x-versiya", butun)],
kontekst));Konsolda:
/v1/vazifalar (versiya 3)
numberModeldagi uch qism — Nest'dagi bilan bir xil: fabrika (data + kontekst), pipe zanjiri, handler argumentlari tartibi. Farq: Nest belgilarni ilova ishga tushganda dekoratorlardan (metama'lumotdan) o'qiydi va ularni parametr indeksi bo'yicha joylashtiradi.
8. Loyiha qadami: o'z dekoratorlarimiz
8.1 src/umumiy/dekoratorlar.ts
Kanon faylida qatorlar uzun edi — bo'ldik (ma'no aynan):
// dekoratorlar.ts — o'z parametr dekoratorlarimiz
// (createParamDecorator). Handler @Req() ni olib, ichida ajratish
// o'rniga — tayyor qiymat:
// @IfMatchShart() shart: Shart | undefined — If-Match sarlavhasi
// (yaroqsiz — 412)
// @ToplamYoli("vazifalar") asos: string — "/v1/vazifalar" yoki
// eskirgan "/vazifalar"
// @VazifaYozuvi() — dekoratorlar to'plami
import {
applyDecorators, createParamDecorator, UseInterceptors,
} from "@nestjs/common";
import type { ExecutionContext } from "@nestjs/common";
import type { Request } from "express";
import { ifMatch } from "./shartli.ts";
import { asosYol } from "./yol.ts";
import { JsonTana } from "./json-tana.guard.ts";
import {
VazifaSarlavhalariInterceptor,
} from "./vazifa-sarlavhalari.interceptor.ts";
export const IfMatchShart = createParamDecorator(
(_: unknown, kontekst: ExecutionContext) =>
ifMatch(kontekst.switchToHttp().getRequest<Request>()),
);
export const ToplamYoli = createParamDecorator(
(toplam: string, kontekst: ExecutionContext) =>
asosYol(kontekst.switchToHttp().getRequest<Request>(), toplam),
);
/** Bitta vazifani yozadigan endpoint (POST, PUT /:id, PATCH): tana
* JSON (415) + javobga ETag/Last-Modified — bitta dekorator */
export const VazifaYozuvi = () =>
applyDecorators(
JsonTana(),
UseInterceptors(VazifaSarlavhalariInterceptor),
);ifMatchvaasosYol— 25-qism va Controller'lar darsidagi funksiyalar, o'zgarmaydi. Dekorator — ularni handler'ga ulaydigan yupqa qatlam.@IfMatchShart()yaroqsizIf-MatchdaifMatch412 tashlaydi — xato filtrga boradi, handler chaqirilmaydi.@VazifaYozuvi()—JsonTanametama'lumoti va interceptor.PUT /v1/vazifalar(butun ro'yxat) uchun ishlatilmaydi: u bitta vazifa qaytarmaydi, interceptor unga mos emas.
8.2 vazifalar.controller.ts — yakuniy shakl
Faylning shu bosqichdagi to'liq holati. Kanon faylidagi uzun qatorlarni bo'ldik va izohlarni qisqartirdik (kod ma'nosi aynan); nusxani kanon nusxasida tsc --noEmit, ESLint va Vitest bilan tekshirdik.
// vazifalar.controller.ts — /v1/vazifalar: HTTP ↔ xizmat (25-qism
// marshrut.ts dan). Kirish: Nest 12 `@Body/@Query/@Param({ schema })`
// — 25-qism Zod sxemalari, tekshiruv — global SxemaPipe; tana JSON —
// @JsonTana() + global guard (415); ETag/Last-Modified —
// interceptor; ikkalasi birga — @VazifaYozuvi(); Cache-Control —
// @Header; If-Match va to'plam yo'li — o'z parametr
// dekoratorlarimiz. @Res({ passthrough: true }) faqat Link,
// Location va 207 uchun
import {
All, Body, Controller, Delete, Get, Header, HttpCode, Inject,
Param, Patch, Post, Put, Query, Req, Res, UseInterceptors,
} from "@nestjs/common";
import type { Request, Response } from "express";
import { MetodYoqXatosi } from "../umumiy/xatolar.ts";
import type { MaydonXatosi } from "../umumiy/xatolar.ts";
import { JsonTana } from "../umumiy/json-tana.guard.ts";
import {
IfMatchShart, ToplamYoli, VazifaYozuvi,
} from "../umumiy/dekoratorlar.ts";
import {
VazifaSarlavhalariInterceptor,
} from "../umumiy/vazifa-sarlavhalari.interceptor.ts";
import { kursorYoz } from "../umumiy/kursor.ts";
import { ILOVA_SOZLAMALARI } from "../tokenlar.ts";
import type { IlovaSozlamalari } from "../tokenlar.ts";
import {
BulkSxemasi, IdSxemasi, OzgarishSxemasi, RoyxatQuerySxemasi,
ToliqVazifaSxemasi, ToplamSxemasi, YangiVazifaSxemasi,
} from "./sxema.ts";
import type { RoyxatQuery } from "./sxema.ts";
import type {
ToliqVazifa, Vazifa, VazifaOzgarishi, YangiVazifa,
} from "./vazifa.ts";
import type { AlmashtirishVazifasi, Shart } from "./ombor.ts";
import { proyeksiya, royxatSorovi } from "./sorov.ts";
import { VazifalarService } from "./vazifalar.service.ts";
/** API javoblari: brauzer saqlashi mumkin, lekin har safar
* tekshirsin (private — umumiy keshlar saqlamasin) */
const KESH = ["Cache-Control", "private, no-cache"] as const;
type Natija = {
indeks: number;
holat: number;
vazifa?: Vazifa;
xato?: MaydonXatosi;
};
type Id = { id: number };
// Versiya — ilova.ts dagi enableVersioning (URI): /v1/vazifalar
// va eskirgan /vazifalar
@Controller("vazifalar")
export class VazifalarController {
constructor(
private readonly xizmat: VazifalarService,
@Inject(ILOVA_SOZLAMALARI)
private readonly sozlamalar: IlovaSozlamalari,
) {}
@Get()
@Header(...KESH)
async royxat(
@Query({ schema: RoyxatQuerySxemasi }) query: RoyxatQuery,
@ToplamYoli("vazifalar") asos: string,
@Req() req: Request,
@Res({ passthrough: true }) res: Response,
): Promise<object> {
const { sorov, maydonlar, imzo } =
royxatSorovi(query, this.sozlamalar.tz);
const natija = await this.xizmat.royxat(sorov);
const sahifa: Record<string, unknown> = { limit: sorov.limit };
// Keyingi sahifa havolasi: query nusxasi, offset/cursor yangi
const havola = (ozgartir: Record<string, string>): string => {
const p = new URLSearchParams(
req.originalUrl.split("?")[1] ?? "",
);
p.delete("offset");
p.delete("cursor");
for (const [k, v] of Object.entries(ozgartir)) p.set(k, v);
return `${asos}?${p.toString()}`;
};
let keyingi: string | undefined;
if (sorov.sahifa.tur === "offset") {
sahifa.offset = sorov.sahifa.offset;
if (natija.borKeyingi) {
const offset = String(sorov.sahifa.offset + sorov.limit);
keyingi = havola({ offset });
}
} else {
const kursor =
natija.keyingi && kursorYoz(natija.keyingi, imzo);
sahifa.keyingi = kursor ?? null;
if (kursor !== undefined) keyingi = havola({ cursor: kursor });
}
if (natija.jami !== undefined) sahifa.jami = natija.jami;
// Link sarlavhasi (RFC 8288) — mijoz URL'ni o'zi yig'masin
if (keyingi !== undefined) res.links({ next: keyingi });
// ro'yxat ETag'i — Express'niki (tana xeshi, W/"…"): Nest javobni
// res.json() bilan yuboradi → If-None-Match mos kelsa 304
return {
versiya: natija.versiya,
vazifalar: natija.vazifalar.map(
(v) => proyeksiya(v, maydonlar),
),
sahifa,
};
}
@Post()
@VazifaYozuvi()
async yarat(
@Body({ schema: YangiVazifaSxemasi }) body: YangiVazifa,
@ToplamYoli("vazifalar") asos: string,
@Res({ passthrough: true }) res: Response,
): Promise<Vazifa> {
const v = await this.xizmat.yarat(body);
res.location(`${asos}/${String(v.id)}`);
return v; // POST — sukut 201
}
/** PUT to'plamga — butun ro'yxatni almashtirish; atomik */
@Put()
@JsonTana()
@Header(...KESH)
async almashtirHammasi(
@Body({ schema: ToplamSxemasi })
body: { vazifalar: AlmashtirishVazifasi[] },
): Promise<object> {
const { vazifalar, versiya } =
await this.xizmat.almashtirHammasi(body.vazifalar);
return { versiya, vazifalar };
}
// Express metod mos kelmasa 404 beradi — 405 + Allow ni o'zimiz
// qo'yamiz (Allow ro'yxatini marshrutlar.test.ts tekshiradi)
@All()
toplamMetodYoq(@Req() req: Request): never {
throw new MetodYoqXatosi(req.method, "GET, HEAD, POST, PUT");
}
/** Bulk yaratish — qisman muvaffaqiyat: 207 Multi-Status
* (/:id dan OLDIN — aks holda "toplam" id deb olinadi) */
@Post("toplam")
@JsonTana()
async toplam(
@Body({ schema: BulkSxemasi }) body: { vazifalar: unknown[] },
@Res({ passthrough: true }) res: Response,
): Promise<{ natijalar: Natija[] }> {
// elementlar alohida tekshiriladi (qisman muvaffaqiyat) — pipe
// emas, sxema bevosita
const tekshirilgan = body.vazifalar.map(
(el) => YangiVazifaSxemasi.safeParse(el),
);
const yaroqli = tekshirilgan.flatMap(
(t) => (t.success ? [t.data] : []),
);
const yaratilgan = await this.xizmat.toplamYarat(yaroqli);
let k = 0;
const natijalar = tekshirilgan.map((t, indeks): Natija => {
if (!t.success) {
const xabar = t.error.issues.map((i) => i.message).join("; ");
const joy = `body.vazifalar[${String(indeks)}]`;
return { indeks, holat: 422, xato: { joy, xabar } };
}
const vazifa = yaratilgan[k++];
return vazifa === undefined
? { indeks, holat: 500 }
: { indeks, holat: 201, vazifa };
});
// dinamik status: passthrough bilan res.status() saqlanadi
res.status(natijalar.every((n) => n.holat === 201) ? 201 : 207);
return { natijalar };
}
@All("toplam")
toplamPostYoq(@Req() req: Request): never {
throw new MetodYoqXatosi(req.method, "POST");
}
@Get(":id")
@Header(...KESH)
@UseInterceptors(VazifaSarlavhalariInterceptor)
ol(@Param({ schema: IdSxemasi }) { id }: Id): Promise<Vazifa> {
return this.xizmat.ol(id);
}
/** PUT — to'liq almashtirish (idempotent: 2 marta = 1 marta) */
@Put(":id")
@VazifaYozuvi()
almashtir(
@Param({ schema: IdSxemasi }) { id }: Id,
@Body({ schema: ToliqVazifaSxemasi }) body: ToliqVazifa,
@IfMatchShart() shart: Shart | undefined,
): Promise<Vazifa> {
return this.xizmat.almashtir(id, body, shart);
}
/** PATCH — qisman (JSON Merge Patch). If-Match: "3" — faqat
* 3-versiya bo'lsa yoz */
@Patch(":id")
@VazifaYozuvi()
yangila(
@Param({ schema: IdSxemasi }) { id }: Id,
@Body({ schema: OzgarishSxemasi }) body: VazifaOzgarishi,
@IfMatchShart() shart: Shart | undefined,
): Promise<Vazifa> {
return this.xizmat.yangila(id, body, shart);
}
@Delete(":id")
@HttpCode(204)
ochir(
@Param({ schema: IdSxemasi }) { id }: Id,
@IfMatchShart() shart: Shart | undefined,
): Promise<void> {
return this.xizmat.ochir(id, shart);
}
@All(":id")
birMetodYoq(@Req() req: Request): never {
throw new MetodYoqXatosi(
req.method, "GET, HEAD, PUT, PATCH, DELETE",
);
}
}O'tgan holatdan farq:
@VazifaYozuvi()—yarat,almashtir,yangilada;@JsonTana()+@UseInterceptors(...)juftligi o'rnida.almashtirHammasivatoplamda@JsonTana()yolg'iz qoladi.@IfMatchShart()—almashtir,yangila,ochirda. HandlerShart | undefinedoladi;ifMatchvaasosYolimportlari controller'dan yo'qoldi.@ToplamYoli("vazifalar")—royxatvayaratda.keyingiHavolafunksiyasiroyxatichiga ko'chdi (havola) — u endiasosni yopilishda ishlatadi.@Req()— faqat ikki xil joyda qoldi: uchta@Allhandler'i (405 xabari uchunreq.method) varoyxat(req.originalUrl— query nusxasi uchun).
8.3 eksport.controller.ts
Ikki o'zgarish: @ToplamYoli("eksportlar") va @Param("id") (avval String(req.params["id"])):
@Post()
@HttpCode(202)
boshla(
@Body({ schema: EksportSxemasi }) body: EksportSorovi,
@ToplamYoli("eksportlar") asos: string,
@Res({ passthrough: true }) res: Response,
): Record<string, unknown> {
const ish = this.eksport.boshla(body);
res.location(`${asos}/${ish.id}`).set("Retry-After", "1");
return this.eksport.korinish(ish, asos);
}
@Get(":id")
holat(
@Param("id") id: string,
@ToplamYoli("eksportlar") asos: string,
@Res({ passthrough: true }) res: Response,
): Record<string, unknown> {
const ish = this.eksport.topilsin(id);
if (ish.holat === "kutilmoqda" || ish.holat === "bajarilmoqda") {
res.set("Retry-After", "1");
}
res.set("Cache-Control", "no-store");
return this.eksport.korinish(ish, asos);
}fayl metodida faqat birinchi qatorlar: fayl(@Param("id") id: string, @Res(...) res: Response) va this.eksport.topilsin(id). Importlar: Req va Request o'chadi, Param va ToplamYoli qo'shiladi, asosYol importi o'chadi. @Param("id") sxemasiz — eksport ID'si UUID satr, topilsin o'zi 404 beradi.
8.4 Tekshiruv
npm run tip, npm run lint, npm test — toza (Postgres'siz: Tests 75 passed | 6 skipped (81)). 25-qismdagi If-Match/412 testlari (shartli.test), Location va Link testlari o'zgarishsiz o'tadi — dekoratorlar xulqni o'zgartirmagani shundan ko'rinadi. Hali commit qilmang — keyingi dars shu bosqichni yopadi.
9. Real ishda
@CurrentUser()(yoki@User()) — deyarli har auth'li Nest loyihasida bor eng mashhur o'z dekorator.vazifalar-apida uning o'rnini@Egasi()egallaydi — so'rov egasining ID'sini auth qatlami yozgan joydan oladi (Fayl yuklash NestJS'da darsida).applyDecorators— ko'pincha Swagger bilan:@ApiOkResponse+@ApiBearerAuth+@UseGuards+@Roles— bitta@AdminEndpoint().- Intervyu: "
createParamDecoratorvaSetMetadatafarqi?", "parametr dekoratoriga pipe qanday ulanadi?", "applyDecoratorsnima uchun?".
Xulosa
createParamDecorator((data, ctx) => qiymat)— handler argumentini tayyorlaydi;data— dekorator argumenti; pipe ulanadi.ValidationPipe/StandardSchemaValidationPipeo'z dekoratorlarni sukutda tekshirmaydi (validateCustomDecorators).applyDecorators(...)— guard, interceptor, metama'lumot, status, sarlavhalarni bitta nomga yig'adi.Reflector.createDecorator<T>()— tur xavfsiz, lekinget()turidaundefinedko'rinmaydi.vazifalar-api:@IfMatchShart(),@ToplamYoli(),@VazifaYozuvi()— controller'larda@Req()faqat 405 va ro'yxatda qoldi.
Keyingi dars: Global enhancer'lar va bajarilish tartibi — APP_GUARD/APP_PIPE/APP_INTERCEPTOR/APP_FILTER va useGlobal* farqi, bir nechta enhancer tartibi; loyiha qadami commit bilan yopiladi.
Manbalar
- NestJS hujjati — Custom decorators (
createParamDecorator,applyDecorators, pipe bilan): https://docs.nestjs.com/custom-decorators - NestJS hujjati — Execution context (
Reflector.createDecorator): https://docs.nestjs.com/fundamentals/execution-context - RFC 9110 — If-Match (kuchli solishtirish): https://www.rfc-editor.org/rfc/rfc9110#name-if-match
- Kanon (IlmHamroh, 2026-10-27):
dekoratorlar.ts, controller'larning yakuniy shakli,vazifalar-apitekshiruvi. Demo (createParamDecoratorva pipe,validateCustomDecorators,applyDecorators,Reflector.createDecorator) — biz ishga tushirdik (2026-11-02, NestJS 12.2.0, class-validator 0.15.1, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!