Asosiy tushunchalar8 min read

Execution context

Nest bir nechta ilova kontekstlari bo'ylab ishlaydigan ilovalarni yozishni osonlashtiradigan bir qator yordamchi sinflarni taqdim etadi (masalan, Nest HTTP serveriga asoslangan, mi

Nest bir nechta ilova kontekstlari bo'ylab ishlaydigan ilovalarni yozishni osonlashtiradigan bir qator yordamchi sinflarni taqdim etadi (masalan, Nest HTTP serveriga asoslangan, mikroservis va WebSockets ilova kontekstlari). Bu utilitalar joriy execution context haqida ma'lumot beradi va shu ma'lumotlar yordamida turli controllerlar, metodlar va execution contextlar bo'ylab ishlay oladigan umumiy guards, filters va interceptors yaratish mumkin.

Ushbu bobda shunday ikki sinfni ko'rib chiqamiz: ArgumentsHost va ExecutionContext.

ArgumentsHost sinfi

ArgumentsHost sinfi handlerga uzatilayotgan argumentlarni olish uchun metodlar taqdim etadi. U argumentlarni olish uchun mos kontekstni (masalan, HTTP, RPC (mikroservis) yoki WebSockets) tanlash imkonini beradi. Freymvork ArgumentsHost instansiyasini, odatda host parametri sifatida, unga kirish kerak bo'lishi mumkin bo'lgan joylarda taqdim etadi. Masalan, exception filter dagi catch() metodi ArgumentsHost instansiyasi bilan chaqiriladi.

ArgumentsHost shunchaki handler argumentlari ustidagi abstraksiyadir. Masalan, HTTP server ilovalari uchun (@nestjs/platform-express ishlatilganda) host obyekti Expressning [request, response, next] massivini kapsullaydi; bu yerda request - so'rov obyekti, response - javob obyekti, next esa ilovaning request-response siklini boshqaradigan funksiya. Boshqa tomondan, GraphQL ilovalari uchun host obyekti [root, args, context, info] massivini o'z ichiga oladi.

Joriy ilova konteksti

Bir nechta ilova kontekstlarida ishlashi kerak bo'lgan umumiy guards, filters va interceptors ni yaratishda, metodimiz hozir qaysi ilova turida ishlayotganini aniqlash usuliga muhtojmiz. Buni ArgumentsHost ning getType() metodi orqali qilamiz:

TypeScript
1if (host.getType() === 'http') {
2  // do something that is only important in the context of regular HTTP requests (REST)
3} else if (host.getType() === 'rpc') {
4  // do something that is only important in the context of Microservice requests
5} else if (host.getType<GqlContextType>() === 'graphql') {
6  // do something that is only important in the context of GraphQL requests
7}
Hint

GqlContextType@nestjs/graphql paketidan import qilinadi.

Ilova turi ma'lum bo'lganda, quyida ko'rsatilgandek, yanada umumiy komponentlarni yozishimiz mumkin.

Host handler argumentlari

Handlerga uzatilayotgan argumentlar massivini olish uchun bir yondashuv - host obyektining getArgs() metodidan foydalanish.

TypeScript
1const [req, res, next] = host.getArgs();

getArgByIndex() metodi yordamida indeks bo'yicha muayyan argumentni ajratib olish mumkin:

TypeScript
1const request = host.getArgByIndex(0);
2const response = host.getArgByIndex(1);

Bu misollarda biz request va response obyektlarini indeks bo'yicha oldik, bu odatda tavsiya etilmaydi, chunki u ilovani muayyan execution contextga bog'lab qo'yadi. Buning o'rniga, host obyektining mos ilova kontekstiga o'tuvchi utilita metodlaridan foydalanib kodingizni yanada mustahkam va qayta foydalaniladigan qilishingiz mumkin. Kontekstni almashtirish utilita metodlari quyida ko'rsatilgan.

TypeScript
1/**
2 * Switch context to RPC.
3 */
4switchToRpc(): RpcArgumentsHost;
5/**
6 * Switch context to HTTP.
7 */
8switchToHttp(): HttpArgumentsHost;
9/**
10 * Switch context to WebSockets.
11 */
12switchToWs(): WsArgumentsHost;

Keling, oldingi misolni switchToHttp() metodi yordamida qayta yozamiz. host.switchToHttp() yordamchi chaqiruv HTTP ilova kontekstiga mos bo'lgan HttpArgumentsHost obyektini qaytaradi. HttpArgumentsHost obyektida kerakli obyektlarni ajratib olish uchun ikkita foydali metod mavjud. Bu holatda native Express tiplangan obyektlarni qaytarish uchun Express type assertionsdan ham foydalanamiz:

TypeScript
1const ctx = host.switchToHttp();
2const request = ctx.getRequest<Request>();
3const response = ctx.getResponse<Response>();

Xuddi shunday, WsArgumentsHost va RpcArgumentsHost mikroservis va WebSockets kontekstlarida mos obyektlarni qaytaradigan metodlarga ega. Quyida WsArgumentsHost metodlari:

TypeScript
1export interface WsArgumentsHost {
2  /**
3   * Returns the data object.
4   */
5  getData<T>(): T;
6  /**
7   * Returns the client object.
8   */
9  getClient<T>(): T;
10}

Quyida RpcArgumentsHost metodlari:

TypeScript
1export interface RpcArgumentsHost {
2  /**
3   * Returns the data object.
4   */
5  getData<T>(): T;
6
7  /**
8   * Returns the context object.
9   */
10  getContext<T>(): T;
11}

ExecutionContext sinfi

ExecutionContext ArgumentsHost ni kengaytirib, joriy bajarilish jarayoni haqida qo'shimcha tafsilotlarni taqdim etadi. ArgumentsHost kabi, Nest ExecutionContext instansiyasini kerak bo'lishi mumkin bo'lgan joylarda taqdim etadi, masalan guard dagi canActivate() metodida va interceptor dagi intercept() metodida. U quyidagi metodlarni taqdim etadi:

TypeScript
1export interface ExecutionContext extends ArgumentsHost {
2  /**
3   * Returns the type of the controller class which the current handler belongs to.
4   */
5  getClass<T>(): Type<T>;
6  /**
7   * Returns a reference to the handler (method) that will be invoked next in the
8   * request pipeline.
9   */
10  getHandler(): Function;
11}

getHandler() metodi tez orada chaqiriladigan handlerga havola qaytaradi. getClass() metodi esa ushbu handler tegishli bo'lgan Controller sinfining turini qaytaradi. Masalan, HTTP kontekstida agar hozir qayta ishlanayotgan so'rov CatsController dagi create() metodiga bog'langan POST so'rov bo'lsa, getHandler() create() metodiga havola, getClass() esa CatsController sinfini (instansiyani emas) qaytaradi.

TypeScript
1const methodKey = ctx.getHandler().name; // "create"
2const className = ctx.getClass().name; // "CatsController"

Joriy sinf va handler metodiga havola olish imkoniyati katta moslashuvchanlik beradi. Eng muhimi, bu bizga guardlar yoki interceptorlar ichida Reflector#createDecorator orqali yaratilgan dekoratorlar yoki o'rnatilgan @SetMetadata() dekoratori orqali o'rnatilgan metadatalarga kirish imkonini beradi. Bu use-case'ni quyida ko'rib chiqamiz.

Reflection va metadata

Nest Reflector#createDecorator metodi orqali yaratilgan dekoratorlar va o'rnatilgan @SetMetadata() dekoratori yordamida route handlerlarga custom metadata biriktirish imkonini beradi. Bu bo'limda ikkala yondashuvni solishtiramiz va guard yoki interceptor ichidan metadatalarga qanday kirishni ko'ramiz.

Reflector#createDecorator yordamida kuchli tiplangan dekoratorlar yaratish uchun tip argumentini ko'rsatishimiz kerak. Masalan, Roles dekoratorini yaratamiz, u argument sifatida satrlar massivi oladi.

TypeScript
roles.decorator
1import { Reflector } from '@nestjs/core';
2
3export const Roles = Reflector.createDecorator<string[]>();

Bu yerda Roles dekoratori string[] tipidagi bitta argument qabul qiladigan funksiya.

Endi bu dekoratordan foydalanish uchun handlerni shunchaki unga annotatsiya qilamiz:

TypeScript
cats.controller
1@Post()
2@Roles(['admin'])
3async create(@Body() createCatDto: CreateCatDto) {
4  this.catsService.create(createCatDto);
5}

Bu yerda Roles dekoratori metadatasini create() metodiga biriktirdik, ya'ni faqat admin roliga ega foydalanuvchilar ushbu route'ga kirishi kerakligini ko'rsatdik.

Marshrutning roli(lar)i (custom metadata) ga kirish uchun yana Reflector yordamchi sinfidan foydalanamiz. Reflector odatiy tarzda sinfga in'eksiya qilinishi mumkin:

TypeScript
roles.guard
1@Injectable()
2export class RolesGuard {
3  constructor(private reflector: Reflector) {}
4}
Hint

Reflector sinfi @nestjs/core paketidan import qilinadi.

Endi handler metadatasini o'qish uchun get() metodidan foydalanamiz:

TypeScript
1const roles = this.reflector.get(Roles, context.getHandler());

Reflector#get metodi metadatalarga oson kirish imkonini beradi: unga ikkita argument uzatamiz - dekorator havolasi va metadata olinadigan kontekst (dekorator maqsadi). Bu misolda ko'rsatilgan dekorator Roles (yuqoridagi roles.decorator.ts fayliga qayting). Kontekst context.getHandler() chaqiruviga bog'liq, bu hozir qayta ishlanayotgan route handler uchun metadatalarni ajratib olishga olib keladi. Esda tuting, getHandler() route handler funksiyasiga havola beradi.

Muqobil ravishda, controller ichidagi barcha route'lar uchun metadata qo'llash maqsadida metadata'ni controller darajasida joylashtirishimiz mumkin.

TypeScript
cats.controller
1@Roles(['admin'])
2@Controller('cats')
3export class CatsController {}

Bu holatda controller metadatasini ajratib olish uchun ikkinchi argument sifatida context.getHandler() o'rniga context.getClass() uzatiladi (metadata ajratib olish uchun controller sinfini kontekst sifatida berish uchun):

TypeScript
roles.guard
1const roles = this.reflector.get(Roles, context.getClass());

Metadata'ni bir nechta darajada berish imkoniyati bo'lgani uchun, sizga bir nechta kontekstlardan metadata'ni ajratib olib, birlashtirish kerak bo'lishi mumkin. Reflector sinfi bunga yordam beradigan ikkita utilita metodni taqdim etadi. Bu metodlar ham controller, ham metod metadatasini bir vaqtda ajratib olib, ularni turli usullarda birlashtiradi.

Quyidagi scenariyni ko'rib chiqing, bunda Roles metadatasi ikkala darajada ham berilgan.

TypeScript
cats.controller
1@Roles(['user'])
2@Controller('cats')
3export class CatsController {
4  @Post()
5  @Roles(['admin'])
6  async create(@Body() createCatDto: CreateCatDto) {
7    this.catsService.create(createCatDto);
8  }
9}

Agar niyatingiz 'user' ni default rol sifatida berish va ayrim metodlar uchun uni selektiv tarzda override qilish bo'lsa, ehtimol getAllAndOverride() metodidan foydalanasiz.

TypeScript
1const roles = this.reflector.getAllAndOverride(Roles, [context.getHandler(), context.getClass()]);

Ushbu kodga ega guard create() metodi kontekstida yuqoridagi metadata bilan ishlaganda, roles ['admin'] ni o'z ichiga oladi.

Har ikkala metadatani olib, ularni birlashtirish uchun (bu metod massivlar va obyektlarning ikkalasini ham birlashtiradi) getAllAndMerge() metodidan foydalaning:

TypeScript
1const roles = this.reflector.getAllAndMerge(Roles, [context.getHandler(), context.getClass()]);

Bu holatda roles ['user', 'admin'] ni o'z ichiga oladi.

Bu ikki merge metodida ham birinchi argument sifatida metadata key, ikkinchi argument sifatida metadata target kontekstlari massivi (ya'ni getHandler() va/yoki getClass() chaqiruvlari) uzatiladi.

Past darajadagi yondashuv

Avval aytilganidek, Reflector#createDecorator o'rniga o'rnatilgan @SetMetadata() dekoratoridan ham foydalanib handlerga metadata biriktirishingiz mumkin.

TypeScript
cats.controller
1@Post()
2@SetMetadata('roles', ['admin'])
3async create(@Body() createCatDto: CreateCatDto) {
4  this.catsService.create(createCatDto);
5}
Hint

@SetMetadata() dekoratori @nestjs/common paketidan import qilinadi.

Yuqoridagi konstruktsiya bilan biz roles metadatasini (roles - metadata key, ['admin'] - tegishli qiymat) create() metodiga biriktirdik. Bu ishlaydi, ammo route'larda @SetMetadata() dan bevosita foydalanish yaxshi amaliyot emas. Buning o'rniga, quyida ko'rsatilganidek, o'z dekoratorlaringizni yarating:

TypeScript
roles.decorator
1import { SetMetadata } from '@nestjs/common';
2
3export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

Bu yondashuv ancha toza va o'qilishi oson, va qisman Reflector#createDecorator yondashuviga o'xshaydi. Farqi shundaki, @SetMetadata bilan metadata key va qiymatini ko'proq boshqarasiz, shuningdek bir nechta argument qabul qiladigan dekoratorlar yaratishingiz mumkin.

Endi bizda maxsus @Roles() dekoratori bor, uni create() metodini bezash uchun ishlatishimiz mumkin.

TypeScript
cats.controller
1@Post()
2@Roles('admin')
3async create(@Body() createCatDto: CreateCatDto) {
4  this.catsService.create(createCatDto);
5}

Marshrutning roli(lar)i (custom metadata) ga kirish uchun yana Reflector yordamchi sinfidan foydalanamiz:

TypeScript
roles.guard
1@Injectable()
2export class RolesGuard {
3  constructor(private reflector: Reflector) {}
4}
Hint

Reflector sinfi @nestjs/core paketidan import qilinadi.

Endi handler metadatasini o'qish uchun get() metodidan foydalanamiz.

TypeScript
1const roles = this.reflector.get<string[]>('roles', context.getHandler());

Bu yerda dekorator havolasi o'rniga metadata key ini birinchi argument sifatida uzatamiz (bizning holatda bu 'roles'). Qolgan hamma narsa Reflector#createDecorator misolidagi kabi.