IlmHamroh
JavaScript Full-stack/30-qism. NestJS25/65-dars19 daqiqa
Mundarija (25)

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'ga req dan 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

  • createParamDecorator bilan o'z parametr dekoratoringizni yozasiz — data argumenti bilan va argumentsiz.
  • Parametr dekoratoriga pipe ulaysiz va ValidationPipe ning validateCustomDecorators sozlamasini bilasiz.
  • applyDecorators bilan takrorlanadigan dekorator to'plamini bitta dekoratorga aylantirasiz.
  • Reflector.createDecorator ni SetMetadata bilan solishtirasiz.
  • vazifalar-api controller'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:

ts
@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 faqat If-Match sarlavhasidan chiqarilgan "shart" kerak bo'lsa ham. Handler'ni o'qigan odam "u req dan yana nimani oladi?" deb qidiradi. Testda esa soxta req yasash 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

ts
/** 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'sha ExecutionContext (Guard'lar).

Demo natijasi (biz ishga tushirdik: NestJS 12.2.0, Node 24.21.0; so'rovda X-Foydalanuvchi: 42):

text
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:

ts
const Sahifa = createParamDecorator(
  (_: unknown, ctx: ExecutionContext) =>
    ctx.switchToHttp().getRequest<Request>().query['sahifa'],
);

@Get('sahifa')
sahifa(@Sahifa(ParseIntPipe) s: number) {
  return { s, turi: typeof s };
}
text
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:

ts
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; }
text
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: FoydalanuvchiDto va global ValidationPipe({ whitelist: true }). f ichida 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

ts
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:

text
  [RolGuard] kerak=["admin"]
POST /admin → 202 cache-control: no-store qabul

To'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):

js
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:

text
{ jsonTana: true, interceptor: 'VazifaSarlavhalari' }
undefined

Model 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:

ts
// dekorator
@Post('admin') @AdminAmali() admin() { … }   // Rollar(['admin'])

// guard ichida — kalit o'rniga dekoratorning o'zi
const kerak = this.r.get(Rollar, k.getHandler());
text
  [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

  1. 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.
  2. Tekshirilmaydigan parametr. validateCustomDecorators sukutda false — o'z dekoratoringiz tana yoki query'dan olsa, ValidationPipe uni tekshirmaydi. Himoya — mijoz ma'lumotini faqat @Body/@Query orqali olish yoki dekorator ichida tekshirish.
  3. To'plamdan himoya tushib qolishi. @AdminAmali() ichidagi UseGuards(RolGuard) ni kimdir "keraksiz" deb olib tashlasa — hamma admin marshrutlari ochiladi. Himoya — to'plam uchun test: "belgisiz foydalanuvchi — 403".
  4. Xatoni yashiradigan dekorator. Fabrika ichida xato tashlansa — u filtrga boradi (yaxshi); try/catch bilan undefined qaytarsa — handler "shart yo'q" deb o'ylaydi va himoya (If-Match) jim o'chadi. Kanon @IfMatchShart() yaroqsiz sarlavhada 412 tashlaydi, undefined qaytarmaydi.

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 —
  • ValidationPipe o'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() turi undefined ni 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
js
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:

text
undefined → undefined
* → undefined
"3" → {"versiya":3}
W/"3" → 412 If-Match yaroqsiz
3 → 412 If-Match yaroqsiz

Kanon 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
js
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:

text
/v1/vazifalar (versiya 3)
number

Modeldagi 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):

ts
// 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),
  );
  • ifMatch va asosYol — 25-qism va Controller'lar darsidagi funksiyalar, o'zgarmaydi. Dekorator — ularni handler'ga ulaydigan yupqa qatlam.
  • @IfMatchShart() yaroqsiz If-Match da ifMatch 412 tashlaydi — xato filtrga boradi, handler chaqirilmaydi.
  • @VazifaYozuvi() — JsonTana metama'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.

ts
// 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, yangila da; @JsonTana() + @UseInterceptors(...) juftligi o'rnida. almashtirHammasi va toplam da @JsonTana() yolg'iz qoladi.
  • @IfMatchShart() — almashtir, yangila, ochir da. Handler Shart | undefined oladi; ifMatch va asosYol importlari controller'dan yo'qoldi.
  • @ToplamYoli("vazifalar") — royxat va yarat da. keyingiHavola funksiyasi royxat ichiga ko'chdi (havola) — u endi asos ni yopilishda ishlatadi.
  • @Req() — faqat ikki xil joyda qoldi: uchta @All handler'i (405 xabari uchun req.method) va royxat (req.originalUrl — query nusxasi uchun).

8.3 eksport.controller.ts

Ikki o'zgarish: @ToplamYoli("eksportlar") va @Param("id") (avval String(req.params["id"])):

ts
@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-api da 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: "createParamDecorator va SetMetadata farqi?", "parametr dekoratoriga pipe qanday ulanadi?", "applyDecorators nima uchun?".

Xulosa

  • createParamDecorator((data, ctx) => qiymat) — handler argumentini tayyorlaydi; data — dekorator argumenti; pipe ulanadi.
  • ValidationPipe/StandardSchemaValidationPipe o'z dekoratorlarni sukutda tekshirmaydi (validateCustomDecorators).
  • applyDecorators(...) — guard, interceptor, metama'lumot, status, sarlavhalarni bitta nomga yig'adi.
  • Reflector.createDecorator<T>() — tur xavfsiz, lekin get() turida undefined ko'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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Custom dekoratorlar NestJS'da: createParamDecorator, applyDecorators va Reflector.createDecorator — IlmHamroh