IlmHamroh
JavaScript Full-stack/30-qism. NestJS14/65-dars23 daqiqa
Mundarija (26)

NestJS Reflector va DiscoveryService: metadata, o'z dekoratoringiz va marshrut testi

Qisqacha: Nest'ning "sehri" — metama'lumot: dekorator klass yoki metodga yorliq yopishtiradi (SetMetadata(kalit, qiymat) yoki tipli Reflector.createDecorator<T>()), keyin guard, interceptor yoki xizmat uni Reflector bilan o'qiydi. Metod va klassda bir xil kalit bo'lsa, ikki yo'l bor. Birinchisi — getAllAndOverride: birinchi topilganini (odatda metodnikini) beradi. Ikkinchisi — getAllAndMerge: hammasini birlashtiradi. DiscoveryService esa ilovadagi hamma provider va controller'larni ro'yxat qilib beradi — o'z @OnX dekoratoringizni yozish yoki marshrutlar jadvalini test bilan tekshirish uchun.

Bu darsda

  • SetMetadata va Reflector.createDecorator bilan o'z dekoratoringizni yozasiz.
  • get, getAllAndOverride, getAllAndMerge farqini kanon tajribasida ko'rasiz va kichik nusxasini o'zingiz yasaysiz.
  • DiscoveryService va MetadataScanner bilan "dekorator belgilagan metodlarni" ishga tushishda topasiz.
  • vazifalar-api marshrutlari jadvalini Nest metama'lumotidan o'qib, 405 javoblarini avtomatik tekshiradigan test yozasiz.
  • Shu holatni commit qilasiz.

Oldin bilishingiz kerak: Lifecycle hook'lar, Dynamic modullar, Legacy dekoratorlar va reflect-metadata, Request lifecycle, Controller'lar va route dekoratorlari.

Bu holatning oxirgi darsi. Oldingi besh darsda konteyner ichini o'rgandik: DI, provider shakllari, scope, dinamik modul, hayot sikli. Endi Nest'ning o'zi foydalanadigan vositalarni olamiz — metama'lumotni o'qish va ilovani "ichidan ko'rish" — va ular bilan kanon bir eski kamchilikni yopadi.

1. Nega bu kerak?

Controller'lar darsida @All() handler'lari Allow sarlavhasini qo'lda yozardi: "GET, HEAD, PUT, PATCH, DELETE". O'sha darsning «Allow sarlavhasini hisoblash» mashqida bu qatorlarni hisoblaydigan funksiya yozgandik va oxirida aytgandik: haqiqiy loyihada bu tekshiruv testda, Nest'ning DiscoveryService i bilan bo'ladi.

Muammo oddiy. Ertaga kimdir @Patch() ni /vazifalar ga qo'shadi va Allow satrini yangilashni unutadi. Mijoz 405 javobida noto'g'ri ro'yxatni oladi. Hech qanday test buni ko'rmaydi — 25-qismdagi Express kodida ham shunday edi.

Express'da marshrutlar jadvali umuman yo'q edi. router.get(...), router.post(...) va .all(metodYoq("GET, HEAD, POST")) — har biri alohida qator. Express o'zi bu ro'yxatni "bilardi", lekin ichki obyektlarda, ommaviy API'siz. Test yozish uchun uni o'qib bo'lmasdi. Shuning uchun Allow satri bilan haqiqiy marshrutlar faqat dasturchining diqqatiga tayanib bir xil turardi.

Nest bu ma'lumotni allaqachon biladi: har @Get(), @Patch() dekoratori metodga yo'l va HTTP metodini metama'lumot sifatida yozgan. Uni o'qisak, haqiqiy marshrutlar jadvali qo'limizda. Bu darsda shu yo'lni ikki qadamda bosamiz: avval bitta metodning belgisini o'qishni (Reflector), keyin butun ilovani aylanib chiqishni (DiscoveryService) o'rganamiz.

O'xshatish: kutubxonadagi kitoblar. Har kitob ichiga kartochka qo'yilgan (dekorator — muqovadagi yorliq). Kutubxonachi (Reflector) kartochkani o'qiydi; inventarizatsiya (DiscoveryService) esa hamma javonni aylanib, kitoblar ro'yxatini tuzadi.

2. Metama'lumot yozish

2.1 SetMetadata

ts
const ROLLAR_KALIT = "rollar";
const Rollar = (...r: string[]) => SetMetadata(ROLLAR_KALIT, r);

@Controller("v")
@Rollar("foydalanuvchi")
class VController {
  @Get("b") @Rollar("admin") b() { return "b"; }
}

SetMetadata(kalit, qiymat) — tayyor dekorator yasaydi: u klassga yoki metodga kalit → qiymat yozadi. Rollar — uning ustiga qulay qobiq: @Rollar("admin"). Nest'ning o'z dekoratorlari ham shunday ishlaydi: @Get("b") metodga path va method kalitlarini yozadi.

Bu ikki kalit Nest'da konstanta sifatida bor: PATH_METADATA (qiymati — "path") va METHOD_METADATA ("method"). @Controller("v") esa klassga path yozadi. Ya'ni marshrut haqidagi hamma ma'lumot — yo'l va metod — aslida oddiy metama'lumot. Loyiha qadamidagi test aynan shu ikki kalitni o'qib, ilovaning marshrutlar jadvalini quradi.

Metama'lumot qachon yoziladi? Dekorator klass e'lon qilingan lahzada, fayl yuklanganda bir marta ishlaydi. So'rov kelganda hech narsa yozilmaydi — faqat o'qiladi. Reflector.get — oddiy qidiruv, u har so'rovda guard ichida chaqirilsa ham sezilarli vaqt olmaydi.

Tekshirib ko'ring: Bitta metodda @Rollar("admin") ikki marta yozildi: @Rollar("admin") @Rollar("moliya"). reflector.get nima qaytaradi?

Javob

Bitta qiymat — ikkalasi birlashmaydi. SetMetadata bir kalitga yozadi; ikkinchi dekorator birinchisining ustidan yozadi. Dekoratorlar pastdan yuqoriga qo'llanadi (Legacy dekoratorlar va reflect-metadata), shuning uchun oxirgi bo'lib tepadagisi yozadi. Biz NestJS 12.2.0 da tekshirdik — natija [ 'admin' ]. Bir nechta rol kerak bo'lsa — bitta chaqiruvda: @Rollar("admin", "moliya").

2.2 Reflector.createDecorator

SetMetadata ning kamchiligi — kalit satr va qiymat turi unknown. O'qishda turni qo'lda aytish kerak, kalitni noto'g'ri yozish oson. Nest 10.2 versiyasidan beri tipli muqobil bor:

ts
const Ochiq = Reflector.createDecorator<boolean>({
  transform: (v) => v ?? true,
});

@Get("c") @Ochiq() c() { return "c"; }

createDecorator<boolean>() — dekorator ham, kalit ham bir obyektda. O'qishda reflector.get(Ochiq, handler) — tur avtomatik boolean, qo'lda aytish shart emas. Lekin bitta kamchilik bor: turda undefined yo'q, garchi belgisiz handler'da aynan undefined kelsa ham. Bu kanon topgan haqiqiy tuzoq — uni Guard'lar darsida lint chiqishi bilan ko'ramiz. transform — argumentsiz @Ochiq() yozilsa true qo'yadi. Bunday dekorator keyinroq auth'da kerak bo'ladi: "bu yo'l login'siz ochiq" (Global auth guard, @Public va @Roles).

3. Metama'lumotni o'qish: Reflector

3.1 Kanon tajribasi

Kanon guard ichida uch metod natijasini chiqardi. Controller'da @Rollar("foydalanuvchi"), b metodida @Rollar("admin"), c da @Ochiq() (kanon tajribasi, Vitest muhitida):

text
1) Reflector — guard ichida (controller: @Rollar("foydalanuvchi")):
  VController.a: {"getAllAndOverride":["foydalanuvchi"],"getAllAndMerge":["foydalanuvchi"],"Ochiq (createDecorator, tipli)":false}
  VController.b: {"get(handler)":["admin"],"getAllAndOverride":["admin"],"getAllAndMerge":["admin","foydalanuvchi"],"Ochiq (createDecorator, tipli)":false}
  VController.c: {"getAllAndOverride":["foydalanuvchi"],"getAllAndMerge":["foydalanuvchi"],"Ochiq (createDecorator, tipli)":true}

Guard nishonlarni [c.getHandler(), c.getClass()] tartibida beradi — avval metod, keyin klass (Request lifecycle darsidagi ExecutionContext):

Metod a (metodda yo'q) b (metodda admin)
get(kalit, handler) undefined (JSON'da tushib qoldi) ["admin"]
getAllAndOverride ["foydalanuvchi"] — klassdan ["admin"] — metod ustun
getAllAndMerge ["foydalanuvchi"] ["admin", "foydalanuvchi"]

Qachon qaysi biri:

  • Override — "eng aniq qoida yutadi": controller hammaga foydalanuvchi desa, bitta metod admin talab qilsa — metodniki. Rollar va @Ochiq uchun deyarli doim shu.
  • Merge — "hammasi qo'shiladi": masalan, controller audit tegini qo'ysa, metod moliya tegini — log uchun ikkalasi kerak.

3.2 O'zimiz yozamiz

Reflector ning yuragi — Reflect.getMetadata ustidagi bir necha qator. Kichik nusxasi (TypeScript, dekoratorlar bilan; saytda ishga tushmaydi, tekshiruvimizda tsc va Node'da yurgizilgan):

ts
// @ts-options: {"experimentalDecorators": true}
import "reflect-metadata";

// Nest'ning SetMetadata va Reflector g'oyasi — kichik nusxa
const SetMetadata = (kalit: string, qiymat: unknown) =>
  (nishon: object, _k?: string | symbol, d?: PropertyDescriptor) => {
    // metod dekoratori — funksiyaga, klass dekoratori — klassga
    Reflect.defineMetadata(kalit, qiymat, d ? d.value : nishon);
  };
const Rollar = (...r: string[]) => SetMetadata("rollar", r);

const reflector = {
  get: (k: string, n: object): unknown => Reflect.getMetadata(k, n),
  getAllAndOverride(k: string, nishonlar: object[]): unknown {
    for (const n of nishonlar) {
      const q: unknown = Reflect.getMetadata(k, n);
      if (q !== undefined) return q;
    }
    return undefined;
  },
  getAllAndMerge(k: string, nishonlar: object[]): unknown[] {
    return nishonlar.flatMap(
      (n) =>
        (Reflect.getMetadata(k, n) as unknown[] | undefined) ?? [],
    );
  },
};

@Rollar("foydalanuvchi")
class VController {
  a() { return "a"; }
  @Rollar("admin")
  b() { return "b"; }
}

for (const nom of ["a", "b"] as const) {
  const handler = VController.prototype[nom];
  const nishonlar = [handler, VController];
  console.log(nom, JSON.stringify({
    handler: reflector.get("rollar", handler) ?? null,
    override: reflector.getAllAndOverride("rollar", nishonlar),
    merge: reflector.getAllAndMerge("rollar", nishonlar),
  }));
}

Konsolda:

text
a {"handler":null,"override":["foydalanuvchi"],"merge":["foydalanuvchi"]}
b {"handler":["admin"],"override":["admin"],"merge":["admin","foydalanuvchi"]}

Natija kanon tajribasidagi bilan bir xil. a qatorida handler — null: metodning o'zida belgi yo'q, shuning uchun override va merge klassdagi foydalanuvchi ni oldi. E'tibor bering: metod dekoratori metama'lumotni d.value ga — funksiyaning o'ziga — yozadi, klass dekoratori esa klassga. Shuning uchun guard getHandler() (funksiya) va getClass() (klass) ni beradi. Haqiqiy Reflector farqi: u Nest konteynerida provider (DI bilan olinadi) va createDecorator kalitlarini ham tushunadi.

Tekshirib ko'ring: Controller'da @Ochiq(), bitta metodda esa @Ochiq(false) — "bu metod ochiq emas". Guard getAllAndOverride(Ochiq, [handler, klass]) bilan nima oladi va bu to'g'rimi?

Javob

false — metodniki ustun, va bu to'g'ri: eng aniq qoida yutdi. Agar getAllAndMerge ishlatilsa, [false, true] kabi massiv chiqardi va "ochiqmi?" degan savolga bir ma'noli javob bo'lmasdi. Ha/yo'q qiymatlar uchun — override.

3.3 Qaysi metodni tanlash

Uch savol bilan tanlanadi:

  1. Belgi faqat metodda bo'lishi mumkinmi? Ha — get(kalit, getHandler()) yetadi. Masalan, @JsonTana() faqat metodga qo'yiladi; loyihadagi guard shunday o'qiydi.
  2. Klass va metod ikkalasida bo'lishi mumkin, qiymat — ha/yo'q yoki bitta tanlov? getAllAndOverride: eng aniq qoida yutadi. Rollar, @Ochiq, kesh muddati.
  3. Ikkala darajadagi qiymatlar birga kerakmi? getAllAndMerge: teglar, audit belgilari, log maydonlari.

Xato tanlov ikki xil zarar beradi. Override o'rniga merge — "faqat admin" qoidasi "admin yoki foydalanuvchi" ga aylanadi va ruxsat kengayadi. Merge o'rniga override — controller'dagi audit tegi metod tegiga almashadi va logdan yo'qoladi. Birinchisi xavfsizlik xatosi, ikkinchisi — ma'lumot yo'qolishi.

Tekshirib ko'ring: Controller'da @KeshMuddati(60), bitta metodda @KeshMuddati(5). Kesh interceptor'i qaysi metod bilan o'qishi kerak va natija nima?

Javob

getAllAndOverride — natija 5. Muddat bitta son: "60 va 5" dan iborat massiv ma'nosiz. Metod qoidasi aniqroq, u yutadi. Belgisiz metodlar esa controller'dagi 60 ni oladi.

4. DiscoveryService: ilovani ichidan ko'rish

4.1 O'z @Hodisada dekoratorimiz

Reflector bitta klass yoki metodni o'qiydi. Lekin "ilovadagi qaysi metodlarda @Hodisada bor?" degan savolga kim javob beradi? Buning uchun DiscoveryService — hamma provider va controller'lar ro'yxati — va MetadataScanner — klassning hamma metod nomlari.

Kanon kichik hodisa tizimini yozdi:

ts
const Hodisada = Reflector.createDecorator<string>();

@Injectable()
class Hodisalar implements OnApplicationBootstrap {
  readonly tinglovchilar =
    new Map<string, ((x: unknown) => unknown)[]>();
  constructor(
    private readonly discovery: DiscoveryService,
    private readonly scanner: MetadataScanner,
    private readonly reflector: Reflector,
  ) {}
  onApplicationBootstrap(): void {
    for (const w of this.discovery.getProviders()) {
      const obj = w.instance as Record<string, unknown> | undefined;
      if (!obj || !w.isDependencyTreeStatic()) continue;
      const proto = Object.getPrototypeOf(obj) as object;
      for (const nom of this.scanner.getAllMethodNames(proto)) {
        const fn = obj[nom] as (x: unknown) => unknown;
        const hodisa = this.reflector.get(Hodisada, fn);
        if (hodisa === undefined) continue;
        const r = this.tinglovchilar.get(hodisa) ?? [];
        r.push(fn.bind(obj));
        this.tinglovchilar.set(hodisa, r);
      }
    }
  }
  chiqar(hodisa: string, x: unknown): void {
    for (const f of this.tinglovchilar.get(hodisa) ?? []) f(x);
  }
}

@Injectable() class Bildirishnoma {
  @Hodisada("vazifa.yaratildi") yubor(x: unknown) { /* … */ }
}
@Injectable() class Statistika {
  @Hodisada("vazifa.yaratildi") sana() { /* … */ }
  @Hodisada("vazifa.ochirildi") ayir() { /* … */ }
}

(Kod qisqartirilgan; modulda imports: [DiscoveryModule] — DiscoveryService shu moduldan.)

text
2) Discovery — onApplicationBootstrap:
  topildi: Bildirishnoma.yubor → "vazifa.yaratildi"
  topildi: Statistika.sana → "vazifa.yaratildi"
  topildi: Statistika.ayir → "vazifa.ochirildi"
2b) chiqar("vazifa.yaratildi"):
  Bildirishnoma.yubor({"id":7})
  Statistika.sana()

Ishga tushishda Hodisalar hamma provider'ni aylanib chiqdi. U @Hodisada belgilangan uchta metodni topdi va ro'yxatga oldi. chiqar("vazifa.yaratildi") — ikkala tinglovchi chaqirildi. Bildirishnoma va Statistika bir-birini ham, Hodisalar ni ham bilmaydi — faqat dekorator yozilgan. Circular dependency darsidagi "halqani hodisa bilan buzish" g'oyasining Nest uslubi.

Uch tafsilot:

  • onApplicationBootstrap — hamma provider yaratilgan, ro'yxat to'liq (Lifecycle hook'lar).
  • isDependencyTreeStatic() — provider o'zi ham, bog'liqliklari ham REQUEST scope'da emas; nusxa ishga tushishda yaratilgan (Provider scope'lari). TRANSIENT uchun u true qaytaradi — bu nozik joy, pastda ko'ramiz.
  • fn.bind(obj) — metod keyinroq chaqirilganda this nusxaga bog'liq qolsin (bind).

@nestjs/event-emitter ning @OnEvent, @nestjs/schedule ning @Cron dekoratorlari aynan shu naqshda ishlaydi — Hodisalar va Rejalashtirish darslarida ularni ishlatamiz.

flowchart LR
  D["@Hodisada('vazifa.yaratildi')"] -->|"metadata yozadi"| M["metod funksiyasi"]
  B["onApplicationBootstrap"] --> DS["DiscoveryService.getProviders"]
  DS --> MS["MetadataScanner: metod nomlari"]
  MS --> R["Reflector.get(Hodisada, fn)"]
  R --> J["tinglovchilar jadvali"]
  C["chiqar(hodisa)"] --> J

Diagrammaning chap qismi — kompilyatsiya va yuklanish paytida (dekorator yozadi), o'ng qismi — ishga tushishda (Discovery o'qiydi), pastki — ish vaqtida.

4.2 Scope'li provider'lar: instance — bo'sh qolip

w.instance doim haqiqiy nusxa emas. Biz demo loyihada uch provider'ni Discovery bilan ko'rdik (biz ishga tushirdik: NestJS 12.2.0, Node 24.21.0). Transient — TRANSIENT, ichida id maydoni; Sorovli — REQUEST, ichida x = 1; A — oddiy xizmat, constructor'da Transient ni oladi. Har biri uchun isDependencyTreeStatic() va instance ning JSON ko'rinishi (hook qatorlarini olib tashladik):

text
Transient static: true instance: {} Transient
Sorovli static: false instance: {} Sorovli
A static: true instance: {"t":{"id":1}} A

Nima bo'ldi? Uchinchi qator kutilganidek: A haqiqiy nusxa, ichida o'ziga berilgan Transient (id: 1). Birinchi ikkitasida esa instance — bo'sh obyekt {}. Klass to'g'ri, lekin constructor ishlamagan: na id, na x bor. Bu — Nest'ning ichki "qolipi". Haqiqiy nusxalar boshqa joyda: TRANSIENT'niki — har iste'molchida, REQUEST'niki — har so'rovda.

Eng nozik joy — birinchi qator: static: true. Ya'ni isDependencyTreeStatic() filtri TRANSIENT qolipini o'tkazib yuboradi. Agar TRANSIENT xizmatda @Hodisada metodi bo'lsa, Hodisalar uni qolipga bind qiladi — this ichida maydonlar yo'q bo'ladi. Ishonchli filtr: !w.isDependencyTreeStatic() || w.isTransient — ikkalasini ham o'tkazib yuborish. Kanon misolida TRANSIENT provider yo'q, shuning uchun u bu tekshiruvsiz ishladi.

4.3 Klass darajasidagi belgi: DiscoveryService.createDecorator

Ba'zan metod emas, butun klass belgilanadi: "bu xizmat — xabar yuboruvchi kanal". Buning uchun DiscoveryService ning o'z dekorator yasovchisi bor. U getProviders() ga filtr beradi — hamma provider'ni aylanib chiqish shart emas. Demo (biz ishga tushirdik, o'sha versiyalar):

ts
const Kanal = DiscoveryService.createDecorator<string>();

@Injectable() @Kanal("telegram")
class TelegramYuboruvchi {}
@Injectable() @Kanal("email")
class EmailYuboruvchi {}
@Injectable()
class Hisobchi {}

// ilova yig'ilgach:
const topilgan = d.getProviders({ metadataKey: Kanal.KEY });
for (const w of topilgan) {
  console.log(w.name, "→", d.getMetadataByDecorator(Kanal, w));
}
console.log("jami provider'lar:", d.getProviders().length,
  "| belgilangan:", topilgan.length);
text
TelegramYuboruvchi → telegram
EmailYuboruvchi → email
jami provider'lar: 23 | belgilangan: 2

Nima bo'ldi? Filtr faqat ikki belgili klassni qaytardi, Hisobchi tushib qoldi. "Jami 23" ga qarang: bizning modulda uchta xizmat bor, qolgan 20 tasi — Nest'ning ichki provider'lari (Reflector, ModuleRef, DiscoveryService …) va modul klasslarining o'zi. Filtrsiz getProviders() hammasini beradi — shuning uchun o'z kodingizda doim belgi bo'yicha tanlang.

Bir farq: getMetadataByDecorator ning turi string | undefined — undefined ni hisobga oladi. Reflector.createDecorator ning get() turi esa buni qilmaydi; bu tuzoqni Guard'lar darsida haqiqiy lint chiqishi bilan ko'ramiz.

5. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
reflector.get(...) doim undefined metama'lumot klassga yozilgan, metoddan o'qilyapti (yoki aksincha) getHandler() va getClass() ikkalasini bering
Metod qoidasi klassnikini bosmaydi nishonlar tartibi [klass, handler] [handler, klass]
Nest can't resolve dependencies of the Hodisalar (?) DiscoveryModule import qilinmagan imports: [DiscoveryModule]
Discovery'da instance bo'sh {}, maydonlari yo'q REQUEST yoki TRANSIENT provider'ning qolipi !w.isDependencyTreeStatic() || w.isTransient — o'tkazib yuboring
getProviders() 20+ begona provider beradi Nest'ning ichki provider'lari ham ro'yxatda belgi bo'yicha filtr (metadataKey)
Chaqirilgan metodda this undefined metod nusxadan ajratib saqlangan fn.bind(nusxa)

Uchinchi qatordagi xato eng ko'p uchraydi. Biz uni demo loyihada ataylab chiqardik: Hodisalar constructor'ida DiscoveryService bor, modulda esa DiscoveryModule yo'q (biz ishga tushirdik: NestJS 12.2.0):

text
Nest can't resolve dependencies of the Hodisalar (?). Please make sure that the argument DiscoveryService at index [0] is available in the HodisaModule module.

Potential solutions:
- Is HodisaModule a valid NestJS module?
- If DiscoveryService is a provider, is it part of the current HodisaModule?
- If DiscoveryService is exported from a separate @Module, is that module imported within HodisaModule?
  @Module({
    imports: [ /* the Module containing DiscoveryService */ ]
  })

For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors

Tarjimasi: "Nest Hodisalar ning bog'liqliklarini topa olmadi. 0-o'rindagi DiscoveryService argumenti HodisaModule modulida mavjudligiga ishonch hosil qiling." (?) — topilmagan argumentning o'rni. Uchinchi taklif to'g'ri yo'lni ko'rsatadi: DiscoveryService boshqa moduldan keladi, o'sha modulni (DiscoveryModule) import qiling. Bu — DI va IoC konteyner darsidagi o'sha xato; Reflector bilan esa u chiqmaydi, chunki Reflector har modulda o'z-o'zidan bor.

6. Mashqlar

Mashqlar kurs/mashqlar/30/14-reflector/ da.

1-mashq (oson): override yoki merge?

Controller'da @Rollar("foydalanuvchi"), metodda @Rollar("admin"). To'ldiring:

  • getAllAndOverride — ]
  • getAllAndMerge — ["admin", ]
  • Metodda dekorator yo'q bo'lsa getAllAndOverride — ]
  • Ilovadagi hamma provider'lar ro'yxatini beradigan xizmat —
  • Klassning metod nomlarini beradigan vosita —
Yechim
  • ["admin"] — metod birinchi nishon, u yutadi.
  • "foydalanuvchi" — merge ikkalasini qo'shadi, metodniki oldin.
  • ["foydalanuvchi"] — metodda yo'q, klassdan olindi.
  • DiscoveryService — DiscoveryModule dan.
  • MetadataScanner — getAllMethodNames(proto).

2-mashq (o'rta): JS'da @Hodisada modeli

Dekoratorsiz JS modeli: hodisada(Klass, metodNomi, hodisa) funksiyasi Map ga (klass.prototype[metod] → hodisa) yozsin. kashfEt(nusxalar) hamma nusxalarning prototip metodlarini aylanib (Object.getOwnPropertyNames), belgilanganlarini tinglovchilar ga (bind bilan) yig'sin. chiqar(hodisa, x) ularni chaqirsin. Darsdagi Bildirishnoma va Statistika misolini takrorlang. Ishora: constructor nomini o'tkazib yuboring.

Yechim
js
const metama = new Map(); // funksiya → hodisa nomi
function hodisada(Klass, metod, hodisa) {
  metama.set(Klass.prototype[metod], hodisa);
}

function kashfEt(nusxalar) {
  const tinglovchilar = new Map();
  for (const obj of nusxalar) {
    const proto = Object.getPrototypeOf(obj);
    for (const nom of Object.getOwnPropertyNames(proto)) {
      if (nom === "constructor") continue;
      const hodisa = metama.get(proto[nom]);
      if (hodisa === undefined) continue;
      const r = tinglovchilar.get(hodisa) ?? [];
      tinglovchilar.set(hodisa, [...r, proto[nom].bind(obj)]);
      const kim = `${proto.constructor.name}.${nom}`;
      console.log(`topildi: ${kim} → ${hodisa}`);
    }
  }
  return (hodisa, x) => {
    for (const f of tinglovchilar.get(hodisa) ?? []) f(x);
  };
}

class Bildirishnoma {
  yubor(x) {
    console.log(`Bildirishnoma.yubor(${JSON.stringify(x)})`);
  }
}
class Statistika {
  soni = 0;
  sana() {
    this.soni++;
    console.log(`Statistika.sana(): ${this.soni}`);
  }
  ayir() { this.soni--; }
}
hodisada(Bildirishnoma, "yubor", "vazifa.yaratildi");
hodisada(Statistika, "sana", "vazifa.yaratildi");
hodisada(Statistika, "ayir", "vazifa.ochirildi");

const chiqar = kashfEt([new Bildirishnoma(), new Statistika()]);
chiqar("vazifa.yaratildi", { id: 7 });
chiqar("vazifa.yaratildi", { id: 8 });

Konsolda:

text
topildi: Bildirishnoma.yubor → vazifa.yaratildi
topildi: Statistika.sana → vazifa.yaratildi
topildi: Statistika.ayir → vazifa.ochirildi
Bildirishnoma.yubor({"id":7})
Statistika.sana(): 1
Bildirishnoma.yubor({"id":8})
Statistika.sana(): 2

Statistika.sana(): 2 — bind tufayli this.soni haqiqiy nusxada o'sdi. bind siz this — undefined va TypeError bo'lardi. Model kalit sifatida funksiyaning o'zini ishlatadi — Reflect.defineMetadata(kalit, qiymat, d.value) ham aynan shunday.

3-mashq (qiyin): marshrut jadvali

Controller'lar ro'yxati beriladi: { asos: "vazifalar", metodlar: [{ yol: "/", metod: "GET" }, { yol: ":id", metod: "ALL" }, …] }. jadval(controllerlar) funksiyasini yozing: har to'liq yo'l uchun "GET, HEAD, POST | 405" ko'rinishidagi satr (tartib: GET, HEAD, POST, PUT, PATCH, DELETE; GET bo'lsa HEAD ham; ALL bo'lsa | 405). Darsdagi testning marshrutJadvali funksiyasining JS shakli. Ishora: /${asos} va yol === "/" ? "" : "/" + yol.

Yechim
js
const TARTIB = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"];

function jadval(controllerlar) {
  const j = new Map();
  for (const c of controllerlar) {
    for (const { yol, metod } of c.metodlar) {
      const toliq = `/${c.asos}${yol === "/" ? "" : `/${yol}`}`;
      const y = j.get(toliq) ??
        { metodlar: new Set(), allBor: false };
      if (metod === "ALL") y.allBor = true;
      else {
        y.metodlar.add(metod);
        if (metod === "GET") y.metodlar.add("HEAD");
      }
      j.set(toliq, y);
    }
  }
  return Object.fromEntries([...j].map(([k, v]) => [k,
    TARTIB.filter((m) => v.metodlar.has(m)).join(", ") +
      (v.allBor ? " | 405" : "")]));
}

console.log(jadval([
  {
    asos: "vazifalar",
    metodlar: [
      { yol: "/", metod: "GET" }, { yol: "/", metod: "POST" },
      { yol: "/", metod: "ALL" }, { yol: ":id", metod: "GET" },
      { yol: ":id", metod: "DELETE" }, { yol: ":id", metod: "ALL" },
    ],
  },
  { asos: "eksportlar", metodlar: [{ yol: "/", metod: "POST" }] },
]));

Konsolda:

text
{
  '/vazifalar': 'GET, HEAD, POST | 405',
  '/vazifalar/:id': 'GET, HEAD, DELETE | 405',
  '/eksportlar': 'POST'
}

Haqiqiy testda bu ro'yxat qo'lda yozilmaydi — DiscoveryService va Reflector uni Nest metama'lumotidan oladi. @Get() dekoratorining yo'li metama'lumotda "/" bo'lib saqlanadi — shuning uchun yol === "/" tekshiruvi.

7. Loyiha qadami: marshrutlar testi va commit

7.1 Test

test/marshrutlar.test.ts — yangi fayl. U ilovani tayyor XotiraOmbori bilan ko'taradi (AppModule.forRoot ning test yo'li), DiscoveryModule ni qo'shadi va Nest'ning o'z metama'lumot kalitlari (PATH_METADATA, METHOD_METADATA) bilan marshrutlar jadvalini quradi:

ts
// marshrutlar.test.ts — DiscoveryService + MetadataScanner +
// Reflector bilan ilovaning marshrut jadvali (Nest dekoratorlari
// yozgan metama'lumot) o'qiladi va har @All() (405) handler'ining
// Allow ro'yxati HAQIQIY marshrutlar bilan solishtiriladi. Yangi
// metod qo'shib Allow'ni yangilashni unutsangiz — shu test yiqiladi
// (Express'da .all(metodYoq("…")) satrlari qo'lda edi, tekshiruvsiz)
import { afterAll, beforeAll, expect, test } from "vitest";
import { Module, RequestMethod } from "@nestjs/common";
import {
  METHOD_METADATA, PATH_METADATA,
} from "@nestjs/common/constants.js";
import {
  DiscoveryModule, DiscoveryService, MetadataScanner, NestFactory,
  Reflector,
} from "@nestjs/core";
import type { INestApplication } from "@nestjs/common";
import request from "supertest";
import type { App } from "supertest/types.js";
import { AppModule } from "../src/app.module.ts";
import {
  ProblemDetailsFilter,
} from "../src/umumiy/problem.filter.ts";
import { XotiraOmbori } from "../src/vazifalar/xotira-ombor.ts";
import { logYarat } from "../src/umumiy/log.ts";

@Module({
  imports: [
    AppModule.forRoot({
      ombor: new XotiraOmbori(),
      log: logYarat("silent"),
      sozlamalar: { tz: "Asia/Tashkent" },
    }),
    DiscoveryModule,
  ],
})
class SinovModuli {}

let app: INestApplication<App>;
beforeAll(async () => {
  app = await NestFactory.create(SinovModuli, { logger: false });
  app.useGlobalFilters(new ProblemDetailsFilter(() => undefined));
  await app.init();
});
afterAll(() => app.close());

type Yol = { metodlar: Set<string>; allBor: boolean };
const TARTIB = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"];

function marshrutJadvali(): Map<string, Yol> {
  const discovery = app.get(DiscoveryService);
  const scanner = app.get(MetadataScanner);
  const reflector = app.get(Reflector);
  const jadval = new Map<string, Yol>();
  for (const w of discovery.getControllers()) {
    type Klass = new (...a: never[]) => object;
    const klass = w.metatype as Klass | null;
    if (klass === null || w.instance === undefined) continue;
    const asos = String(
      reflector.get<string | string[]>(PATH_METADATA, klass),
    );
    const proto = Object.getPrototypeOf(w.instance) as object;
    for (const nom of scanner.getAllMethodNames(proto)) {
      const handler =
        (proto as Record<string, unknown>)[nom] as () => unknown;
      const yol = reflector.get<string | undefined>(
        PATH_METADATA, handler,
      );
      const metod = reflector.get<RequestMethod | undefined>(
        METHOD_METADATA, handler,
      );
      if (yol === undefined || metod === undefined) continue;
      const toliq = `/${asos}${yol === "/" ? "" : `/${yol}`}`;
      const y = jadval.get(toliq) ??
        { metodlar: new Set<string>(), allBor: false };
      if (metod === RequestMethod.ALL) y.allBor = true;
      else {
        y.metodlar.add(RequestMethod[metod]);
        // Express GET marshruti HEAD'ga ham javob beradi
        if (metod === RequestMethod.GET) y.metodlar.add("HEAD");
      }
      jadval.set(toliq, y);
    }
  }
  return jadval;
}

const qator = (v: Yol) =>
  TARTIB.filter((m) => v.metodlar.has(m)).join(", ") +
  (v.allBor ? " | 405" : "");

test("DiscoveryService: marshrut jadvali (controller + handler " +
  "metama'lumoti)", () => {
  const jadval = Object.fromEntries(
    [...marshrutJadvali()].map(([k, v]) => [k, qator(v)]),
  );
  expect(jadval).toEqual({
    "/vazifalar": "GET, HEAD, POST, PUT | 405",
    "/vazifalar/toplam": "POST | 405",
    "/vazifalar/:id": "GET, HEAD, PUT, PATCH, DELETE | 405",
    "/teglar": "GET, HEAD | 405",
    "/eksportlar": "POST",
    "/eksportlar/:id": "GET, HEAD",
    "/eksportlar/:id/fayl": "GET, HEAD",
  });
});

test("har @All() handler'ining Allow sarlavhasi haqiqiy metodlar " +
  "bilan bir xil", async () => {
  for (const [yol, y] of marshrutJadvali()) {
    if (!y.allBor) continue;
    const kutilgan = TARTIB
      .filter((m) => y.metodlar.has(m)).join(", ");
    const yoq = ["DELETE", "POST", "PATCH", "PUT"]
      .find((m) => !y.metodlar.has(m)) ?? "OPTIONS";
    // sinov ilovasida versiyalash yoqilmagan — yo'l prefikssiz
    const r = await request(app.getHttpServer())[
      yoq.toLowerCase() as "delete"
    ](yol.replace(":id", "1"));
    expect([yol, r.status, r.headers["allow"]])
      .toEqual([yol, 405, kutilgan]);
  }
});

Ikki test:

  1. Jadval — ilovadagi hamma marshrut va ularning metodlari. Kimdir yangi yo'l qo'shsa yoki olib tashlasa — test buni ko'rsatadi va jadvalni ataylab yangilashga majbur qiladi. Bu — API shartnomasining "surati" (snapshot).
  2. Allow mosligi — har @All() yo'lga mavjud bo'lmagan metod bilan so'rov yuboriladi va 405 javobidagi Allow jadvaldagi haqiqiy metodlar bilan solishtiriladi.

Ikki kichik tafsilot. PATH_METADATA va METHOD_METADATA — Nest'ning ichki kalitlari (@nestjs/common/constants.js dan); ular ommaviy hujjatlashtirilmagan, versiya yangilanganda tekshirib turing. Sinov ilovasida enableVersioning yo'q — shuning uchun yo'llar /v1 siz (ilovaYarat emas, NestFactory.create to'g'ridan). qator — telefon uchun alohida yordamchi funksiyaga ajratdik; kanonda bu ifoda test ichida edi.

7.2 Test nimani ushlaydi — biz sinadik

teglar.controller.ts da Allow qiymatini ataylab "unutdik": "GET, HEAD" o'rniga "GET". Natija (biz ishga tushirdik):

text
 FAIL  test/marshrutlar.test.ts > har @All() handler'ining Allow sarlavhasi haqiqiy metodlar bilan bir xil
AssertionError: expected [ '/teglar', 405, 'GET' ] to deeply equal [ '/teglar', 405, 'GET, HEAD' ]

- Expected
+ Received

  [
    "/teglar",
    405,
-   "GET, HEAD",
+   "GET",
  ]

Xabar aniq: qaysi yo'l (/teglar), qaysi status va farq qayerda. 25-qismdagi Express kodi bunday xatoni hech qachon ushlamasdi. Qiymatni qaytardik — test yana yashil.

Bunday "surat" testining narxi ham bor. Har yangi marshrut qo'shilganda birinchi test yiqiladi va kutilgan jadvalni qo'lda yangilash kerak bo'ladi. Bu — ataylab qilingan to'siq: API shartnomasi o'zgarayotganini dasturchi va review qiluvchi ko'rib turadi. Jadval yangilanishi commit'da alohida qator bo'lib ko'rinadi — "bu yangi yo'l rejalashtirilganmi?" degan savolni berishga sabab.

Ikkinchi test esa qo'lda yangilanmaydi: u haqiqiy marshrutlardan kutilgan Allow ni o'zi hisoblaydi. Shuning uchun u yangi @Patch() qo'shilganda ham ishlayveradi va faqat Allow satri eskirganda yiqiladi.

7.3 Tekshiruv

bash
npm run check
npm run build

Kanon tekshiruvi (Node 24.21.0, NestJS 12.2.0, PostgreSQL 18.6):

text
--- npm run tip
exit 0 (5864 ms)
--- npm run lint
exit 0 (10040 ms)
--- npm run test
 ↓ test/konteyner.test.ts (1 test | 1 skipped)
 Test Files  22 passed | 1 skipped (23)
      Tests  117 passed | 1 skipped (118)
exit 0 (9971 ms)
--- npm run build
exit 0 (6087 ms)

Testlar soni oltitaga o'sdi (111 → 117): ombor-modul.test dan 4, marshrutlar.test dan 2. Testcontainers 10/10, jonli API 9 so'rov uch saqlash rejimida — o'zgarishsiz. Kanon yana bitta o'lchovni yozdi: yuklangan mashinada jarayon.test 17 s davom etdi (yolg'iz — 3 s) — bola jarayon Vite bilan ko'tariladi.

Biz ham kanon nusxasida yurgizdik: tip 0, lint 0, Vitest 77 o'tdi va 5 fayl SKIP (Redis va test bazalari yo'q; ombor-modul.test ning Postgres qismi o'z PostgreSQL 17 klasterimizda o'tdi), build 0. Jonli ishga tushirish SAQLASH=postgres bilan — ombor fabrikada yaratildi, GET /v1/teglar 200.

7.4 Commit

Bu holat oltita darsda yig'ildi: CONFIG tokeni (DI), ombor-yarat.ts va fabrika (custom provider'lar), OmborModule.forRoot/forRootAsync (dinamik modul), SaqlashYopuvchi va main.ts (lifecycle), marshrut testi (Discovery). Endi bitta commit:

bash
git add -A
M="refactor: ombor DI ichida — OmborModule.forRootAsync"
M+=" (config bo'yicha, fail-fast, yopishda pool/redis),"
M+=" Discovery bilan 405 tekshiruvi"
git commit -m "$M"

refactor — tashqi xulq (HTTP javoblari) o'zgarmadi, ichki tuzilma o'zgardi.

Commit'dan oldin git status bilan tarmoqni tekshiring: birinchi qatorda On branch nest bo'lishi kerak. Nest ko'chishi shu tarmoqda boradi; main esa 29-qismdagi ishlaydigan Express API bo'lib qoladi (Nest CLI va loyiha tuzilmasi darsidagi reja).

8. Real ishda

  • Auth guard'lar — Reflector ning eng ko'p ishlatiladigan joyi: @Public(), @Roles("admin") va guard ichida getAllAndOverride (Global auth guard, @Public va @Roles).
  • Kutubxonalar — @OnEvent, @Cron, @MessagePattern, @Process — hammasi "dekorator + Discovery" naqshi. Ichini bilsangiz, ularning xatolari tushunarli bo'ladi.
  • Arxitektura testlari — marshrut jadvali, "har controller'da auth bor", "har provider DEFAULT scope'da" kabi qoidalarni Discovery bilan test qilish. Masalan, "har yozuvchi marshrutda (POST, PUT, PATCH) @JsonTana() belgisi bormi" — o'sha marshrutJadvali funksiyasiga bitta reflector.get qo'shilsa bo'ldi.
  • Plaginlar tizimi — DiscoveryService.createDecorator bilan belgilangan klasslar (to'lov usullari, xabar kanallari) ishga tushishda yig'iladi. Yangi kanal qo'shish — yangi klass va bitta dekorator; markaziy ro'yxatni tahrirlash shart emas.
  • Kod review'da yangi dekorator ko'rsangiz: "bu belgini kim o'qiydi va qaysi metod bilan — get, override yoki merge?". Hech kim o'qimaydigan belgi — o'lik kod.
  • Intervyu: "SetMetadata va Reflector qanday ishlaydi?", "getAllAndOverride va getAllAndMerge farqi?", "O'z dekoratoringiz bilan metodlarni qanday topasiz?".

Xulosa

  • Dekorator metama'lumot yozadi: metod dekoratori — funksiyaga, klass dekoratori — klassga; SetMetadata — satr kalit, Reflector.createDecorator<T>() — tipli.
  • Reflector: get — bitta nishon, getAllAndOverride — birinchi topilgani (metod ustun), getAllAndMerge — hammasi birga.
  • DiscoveryService + MetadataScanner — hamma provider/controller va ularning metodlari; o'z @OnX dekoratoringiz shu naqshda. Klass belgisi — DiscoveryService.createDecorator va getProviders({ metadataKey }).
  • REQUEST va TRANSIENT provider'ning instance i — constructor'siz bo'sh qolip; Discovery'da ularni alohida hisobga oling.
  • vazifalar-api da marshrutlar jadvali va Allow sarlavhalari endi test bilan himoyalangan.
  • Holat commit qilindi (nest tarmog'ida): ombor DI ichida, config bo'yicha, ilova bilan birga yopiladi; marshrutlar jadvali test bilan qotirildi.

Keyingi dars: DTO va class-validator — Nest'ning standart validatsiya usuli: DTO klasslari va dekoratorlar bilan kirishni tekshirish; keyin kanon nega Zod'da qolganini o'lchov bilan ko'ramiz.

Manbalar

  • NestJS hujjati — Execution context, Reflection and metadata (Reflector, createDecorator, getAllAndOverride): https://docs.nestjs.com/fundamentals/execution-context
  • NestJS hujjati — Discovery service: https://docs.nestjs.com/fundamentals/discovery-service
  • Kanon tajribasi (IlmHamroh, 2026-10-27): SetMetadata va createDecorator, uch o'qish metodi, @Hodisada + Discovery, NestJS 12.2.0, Vitest muhitida; holat tekshiruvi (118 test). Mini Reflector (ts) bloki — tsc 7.0.2 + reflect-metadata 0.2.2 da; Discovery qoliplari, DiscoveryService.createDecorator, DiscoveryModule siz xato va ikki SetMetadata sinovi; Allow sabotaj sinovi va bazasiz tekshiruv — biz ishga tushirdik (2026-11-02, NestJS 12.2.0, Node 24.21.0, PostgreSQL 17).
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
NestJS Reflector va DiscoveryService: metadata, o'z dekoratoringiz va marshrut testi — IlmHamroh