IlmHamroh
JavaScript Full-stack/30-qism. NestJS44/65-dars17 daqiqa
Mundarija (27)

GraphQL chuqur NestJS'da: @ResolveField, DataLoader, guard'lar va subscriptions

Qisqacha: @ResolveField har vazifa uchun alohida chaqiriladi — 3 vazifa, 3 ta baza so'rovi (N+1). Nest'da DataLoader har so'rov uchun yangi yaratiladi — odatda GraphQLModule ning context fabrikasida; shunda 3 so'rov bittaga yig'iladi va kesh boshqa foydalanuvchiga o'tmaydi. Guard'lar GraphQL'da ham ishlaydi, faqat so'rovni GqlExecutionContext orqali olasiz. Chuqurlik va murakkablik chegarasi — Apollo plagini (@Plugin()), real vaqt — @Subscription + graphql-ws.

Bu darsda

  • @ResolveField va @Parent bilan bog'liq maydonlarni yechasiz va N+1 ni o'lchaysiz.
  • DataLoader'ni so'rov doirasida (context fabrikasida) yaratasiz va global yuklovchi tuzog'ini ko'rasiz.
  • Guard'ni GraphQL resolver'iga qo'yasiz (GqlExecutionContext).
  • Apollo plagini bilan so'rov chuqurligini cheklaysiz.
  • Relay uslubidagi sahifalash va graphql-ws obunasini ishga tushirasiz; federation qachon kerakligini bilasiz.

Oldin bilishingiz kerak: GraphQL code-first, GraphQL chuqur, Guard'lar, Provider scope'lari.

Oldingi darsda code-first sxema yasadik. Kanon tajribasining birinchi qatorida izohlar resolver chaqiruvi: 2 (N+1) bor edi — bu darsni shu raqamdan boshlaymiz. N+1 va DataLoader g'oyasini 29-qismda o'rgangansiz. Bu yerda savol boshqa: Nest'da DataLoader qayerda yashashi kerak.

1. Nega bu kerak?

Frontend vazifalar ro'yxatini har bir vazifaning izohlari bilan so'raydi:

text
{ vazifalar { id izohlar { matn } } }

Vazifa turida izohlar maydoni yo'q — u boshqa jadvaldan keladi. Uni maydon resolver'i (field resolver) yechadi: har vazifa uchun alohida funksiya. 50 ta vazifa — 50 ta baza so'rovi. Bu N+1: ro'yxat uchun 1 ta, har element uchun yana N ta.

Yechimni bilasiz — DataLoader: bir tikdagi load(id) larni yig'ib, bitta WHERE id IN (…) qiladi. Lekin Nest'da yangi savol paydo bo'ladi. Resolver — singleton provider. DataLoader esa keshga ega. Uni resolver ichida bitta qilib yaratsangiz, kesh hamma foydalanuvchilar uchun umumiy bo'ladi. Bu tezlik muammosi emas — bu eskirgan va begona ma'lumot muammosi.

O'xshatish: oshxonada ofitsiant. N+1 — har stol uchun oshxonaga alohida yugurish. DataLoader — hamma stolning buyurtmasini yig'ib, bir marta borish. Global DataLoader esa — ofitsiant kechagi buyurtma daftarini bugun ham ishlatishi: "3-stolga osh" — lekin bugun u yerda boshqa mehmon o'tiribdi.

2. @ResolveField va N+1

2.1 Maydon resolver'i

ts
@Resolver(() => Vazifa)
class VazifaResolver {
  @Query(() => [Vazifa])
  vazifalar() {
    return vazifalar;
  }

  @ResolveField(() => [Izoh])
  izohlar(@Parent() v: Vazifa) {
    return ombor.izohlari(v.id);
  }
}

@ResolveField(() => [Izoh]) — Vazifa turiga izohlar maydonini qo'shadi va uni shu metod hisoblaydi. @Parent() — ota obyekt: hozir yechilayotgan vazifa. Maydon @ObjectType klassida yozilmagan bo'lsa ham, SDL'ga izohlar: [Izoh!]! tushadi.

Maydon resolver'i faqat mijoz shu maydonni so'raganda chaqiriladi. { vazifalar { id } } so'rovida izohlar metodi umuman ishlamaydi. Bu GraphQL'ning REST'dan farqi: qimmat maydon "bepul" yashiringan.

2.2 O'lchov

Demoda omborga har chaqiruvni yozib bordik. Uch vazifa, so'rov { vazifalar { id izohlar { matn } } } (biz ishga tushirdik, NestJS 12.2.0, @nestjs/graphql 14.0.3):

text
1) oddiy: 200 {"data":{"vazifalar":[{"id":"1","izohlar":[{"matn":"Tez"},{"matn":"Bugun"}]},{"id":"2","izohlar":[{"matn":"Juma kuni"}]},{"id":"3","izohlar":[]}]}}
   ombor: izohlari(1) izohlari(2) izohlari(3)

Uch vazifa — uch so'rov. Kanon tajribasida ikki vazifa — ikki chaqiruv. Raqam vazifalar soni bilan o'sadi.

3. DataLoader Nest'da

3.1 So'rov doirasidagi yuklovchi

DataLoader'ni har GraphQL so'rovi uchun yangi yaratish kerak. Nest'da buning eng arzon joyi — GraphQLModule ning context fabrikasi. U har so'rovda chaqiriladi va natijasi har resolver'ga @Context() orqali keladi:

ts
import DataLoader from "dataloader";

const yuklovchiYarat = () =>
  new DataLoader<number, Izoh[]>(
    async (idlar) => ombor.izohlariKop(idlar),
  );

GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  autoSchemaFile: true,
  context: ({ req }) => ({ req, izohYuklovchi: yuklovchiYarat() }),
});

Maydon resolver'i endi omborni emas, yuklovchini chaqiradi:

ts
@ResolveField(() => [Izoh])
izohlar(@Parent() v: Vazifa, @Context() ctx: Kontekst) {
  return ctx.izohYuklovchi.load(v.id);
}

Xuddi shu so'rov:

text
1) dataloader: 200 {"data":{"vazifalar":[{"id":"1","izohlar":[{"matn":"Tez"},{"matn":"Bugun"}]},{"id":"2","izohlar":[{"matn":"Juma kuni"}]},{"id":"3","izohlar":[]}]}}
   ombor: izohlariKop([1,2,3])

Javob baytma-bayt bir xil, omborga esa bitta so'rov: izohlariKop([1,2,3]). Uchta load bitta tikda chaqirildi, DataLoader ularni yig'di. izohlariKop natijani idlar tartibida qaytarishi shart — bu DataLoader shartnomasi (GraphQL chuqur darsida ko'rgansiz).

3.2 Nega request-scoped provider emas?

Nest'da "har so'rovga yangi nusxa" deganda birinchi esga keladigani — Scope.REQUEST (Provider scope'lari). DataLoader'ni request-scoped provider qilish mumkin, Nest hujjatida ham shunday misol uchraydi. Lekin narxini eslang: request-scoped provider'ga bog'liq hamma klass — resolver ham — request-scoped bo'lib qoladi va har so'rovda qayta yaratiladi. context fabrikasi esa faqat bitta oddiy obyekt yaratadi, resolver singleton qoladi.

3.3 Tuzoq: global yuklovchi

Endi xato variant. Yuklovchini modul darajasida bitta yaratdik va hamma so'rovlar uni ishlatdi. Birinchi so'rov, keyin 3-vazifaga izoh qo'shuvchi mutation, keyin yana ikki so'rov — biri global yuklovchi bilan, biri so'rov doirasidagi bilan:

text
2) global DataLoader (so'rovlar orasida kesh)
    200 {"data":{"vazifalar":[…,{"id":"3","izohlar":[]}]}}
    200 {"data":{"izohQosh":{"id":"4"}}}
    200 {"data":{"vazifalar":[…,{"id":"3","izohlar":[]}]}}
    200 {"data":{"vazifalar":[…,{"id":"3","izohlar":[{"matn":"Yangi"}]}]}}
   ombor: izohlariKop([1,2,3]) izohlariKop([1,2,3])

(Takrorlanuvchi 1- va 2-vazifalarni … bilan qisqartirdik.) Uchinchi qatorga qarang: izoh qo'shildi, lekin global yuklovchi "izohlar":[] — eskirgan javob berdi. Omborga u umuman bormadi (ombor qatorida faqat ikkita so'rov: birinchi va to'rtinchi). To'rtinchi qator — har so'rovga yangi yuklovchi — to'g'ri ma'lumot.

Endi buni auth bilan tasavvur qiling: Ali o'z izohlarini so'radi, keyin Malika. Global keshda Ali'ning natijasi turibdi. Ma'lumot boshqa foydalanuvchiga sizadi. Shuning uchun qoida qat'iy: DataLoader — faqat so'rov doirasida.

Tekshirib ko'ring: Bitta GraphQL so'rovi ichida mutation izoh qo'shdi, keyin xuddi shu so'rovdagi query izohlarni yana o'qidi. So'rov doirasidagi yuklovchi ham eskirgan javob berishi mumkinmi?

Javob

Ha, agar o'sha vazifaning izohlari mutation'dan oldin shu so'rovda yuklangan bo'lsa. Kesh so'rov davomida yashaydi. Shuning uchun mutation'dan keyin loader.clear(id) qilinadi — bu 2-mashqning mavzusi. Amalda bitta so'rovda mutation va o'sha ma'lumotni qayta o'qish kam uchraydi, lekin bilib qo'ying.

4. Guard'lar GraphQL'da

4.1 GqlExecutionContext

REST'da guard context.switchToHttp().getRequest() bilan so'rovni olardi (Guard'lar). GraphQL'da bu ishlamaydi: Nest resolver'ni Express marshrutidan emas, Apollo ichidan chaqiradi. So'rov context fabrikasi qaytargan obyektda turadi:

ts
@Injectable()
class AdminGuard implements CanActivate {
  canActivate(ctx: ExecutionContext): boolean {
    const gql = GqlExecutionContext.create(ctx)
      .getContext<Kontekst>();
    return gql.req.headers["x-rol"] === "admin";
  }
}

// resolver ichida
@Query(() => Int)
@UseGuards(AdminGuard)
maxfiySon() {
  return 42;
}

GqlExecutionContext.create(ctx) — umumiy ExecutionContext ni GraphQL ko'rinishiga aylantiradi. Undan getContext() (bizning { req, izohYuklovchi }), getArgs(), getInfo() olinadi.

text
3) guard
    200 {"errors":[{"message":"Forbidden resource","locations":[{"line":1,"column":3}],"path":["maxfiySon"],"extensions":{"code":"FORBIDDEN","originalError":{"message":"Forbidden resource","error":"Forbidden","statusCode":403}}}],"data":null}
    200 {"data":{"maxfiySon":42}}

Sarlavhasiz — FORBIDDEN, x-rol: admin bilan — 42. Qiziq farq: oldingi darsda NotFoundException INTERNAL_SERVER_ERROR kodi bilan kelgan edi, guard'ning ForbiddenException i esa to'g'ri FORBIDDEN kodini oldi. Nest ba'zi HTTP xatolarini GraphQL kodlariga o'zi xaritalaydi, hammasini emas. Mijoz kodiga tayanishdan oldin har xatoni sinab ko'ring.

4.2 Pipe va interceptor

Pipe'lar ham ishlaydi: @Args() ga global ValidationPipe yoki Zod pipe'i qo'llanadi (@InputType klassi DTO o'rnida). Interceptor'larda esa ehtiyot bo'ling: REST uchun yozilgan interceptor switchToHttp().getResponse() ga tayansa, GraphQL'da u undefined qaytaradi. context.getType<GqlContextType>() === "graphql" bilan tekshiring. Bu ikkisini alohida sinamadik — hujjat bo'yicha.

REST'dagi so'rov yo'li (Request lifecycle) GraphQL'da ikki joyda o'zgaradi. Butun GraphQL so'rovi — bitta POST /graphql, ichida esa bir nechta resolver chaqiriladi:

Qatlam REST'da GraphQL'da
middleware har HTTP so'rovga bir marta POST /graphql ga bir marta — resolver'lar sonidan qat'i nazar
guard, interceptor, filter har handler'ga har @Query / @Mutation ga; @ResolveField ga — sukut bo'yicha yo'q
pipe har handler argumentiga har resolver argumentiga, @ResolveField ham

Ikkinchi qator — eng ko'p adashtiradigan joy. @nestjs/graphql 14.0.3 kodida @ResolveField uchun guard, interceptor va filter'lar faqat GraphQLModule sozlamasida fieldResolverEnhancers: ["guards", "interceptors", "filters"] berilsa ulanadi. Sabab — tezlik: 50 ta vazifaning har biri uchun izohlar maydoni chaqirilsa, har biriga guard qo'yish 50 marta qo'shimcha ish. Natija: guard'ni izohlar maydon resolver'iga qo'yib, "himoyalandi" deb o'ylasangiz — u jim ishlamaydi. Maxfiy maydonni himoyalash uchun yo sozlamani yoqing, yo tekshiruvni ota @Query darajasida qiling.

5. Chuqurlik va murakkablik chegarasi

GraphQL chuqur darsida mijoz so'rov shaklini o'zi tanlagani uchun server chegara qo'yishi shartligini ko'rdik. Nest'da chegara — Apollo plagini. @Plugin() dekoratorli klass Nest'ga provider sifatida qo'shiladi, Nest uni Apollo'ga uzatadi:

ts
import { Plugin } from "@nestjs/apollo";

const CHUQURLIK = 4;
function chuqurlik(s: SelectionSetNode | undefined): number {
  if (!s) return 0;
  return 1 + Math.max(0, ...s.selections.map((x) =>
    x.kind === Kind.FIELD ? chuqurlik(x.selectionSet) : 0));
}

@Plugin()
class ChuqurlikPlagini implements ApolloServerPlugin {
  async requestDidStart(): Promise<GraphQLRequestListener<object>> {
    return {
      async didResolveOperation({ operation }) {
        const d = chuqurlik(operation?.selectionSet);
        if (d > CHUQURLIK) {
          throw new GraphQLError(
            `So'rov chuqurligi ${d}, ruxsat ${CHUQURLIK}`,
            { extensions: {
              code: "CHUQURLIK_OSHDI", http: { status: 400 },
            } },
          );
        }
      },
    };
  }
}

didResolveOperation — so'rov tahlil qilinib, sxemaga tekshirilgandan keyin, lekin resolver'lardan oldin chaqiriladi. Xato shu yerda tashlansa, birorta resolver ishlamaydi. chuqurlik — so'rov daraxtini rekursiv aylanib, eng chuqur shoxni topadi (Rekursiya).

Sinov uchun o'z-o'ziga ishora qiluvchi tur yasadik: Tugun { id, ota: Tugun } — bunday tur bilan cheksiz chuqur so'rov yozish mumkin:

text
4) chuqurlik chegarasi
    200 {"data":{"tugun":{"id":"1","ota":{"id":"1"}}}}
    400 {"errors":[{"message":"So'rov chuqurligi 6, ruxsat 4","extensions":{"code":"CHUQURLIK_OSHDI"}}]}

Chuqurlik — sodda o'lchov. Aniqrog'i — murakkablik (complexity): har maydonning narxi, ro'yxatlar first ga ko'paytiriladi. Nest'da @Field({ complexity: 5 }) bilan maydon narxini belgilash va graphql-query-complexity kutubxonasi bilan plagin yozish mumkin (Nest hujjatida tayyor retsept bor). Biz bu kutubxonani sinamadik: uning peer talabi graphql 16, demo esa 17 da. G'oyasini 3-mashqda o'zimiz yozamiz.

6. Relay sahifalash

GraphQL chuqur darsidagi cursor connections Nest'da oddiy @ObjectType lar bilan yoziladi:

ts
@ObjectType()
class VazifaQirrasi {
  @Field() cursor!: string;
  @Field(() => Vazifa) node!: Vazifa;
}
@ObjectType()
class SahifaMalumoti {
  @Field() hasNextPage!: boolean;
  @Field({ nullable: true }) endCursor?: string;
}
@ObjectType()
class VazifaConnection {
  @Field(() => [VazifaQirrasi]) edges!: VazifaQirrasi[];
  @Field(() => SahifaMalumoti) pageInfo!: SahifaMalumoti;
}

edges (qirralar) va pageInfo — Relay spetsifikatsiyasidagi nomlar, ularni o'zgartirmang: Apollo Client va Relay kutubxonalari aynan shu nomlarni kutadi. Ikki sahifa so'radik (first: 2, keyin after: endCursor):

text
5) Relay sahifalash
    200 {"data":{"sahifa":{"edges":[{"cursor":"dmF6aWZhOjE","node":{"matn":"Non olish"}},{"cursor":"dmF6aWZhOjI","node":{"matn":"Hisobot yozish"}}],"pageInfo":{"hasNextPage":true,"endCursor":"dmF6aWZhOjI"}}}}
    200 {"data":{"sahifa":{"edges":[{"node":{"matn":"Sport"}}],"pageInfo":{"hasNextPage":false}}}}

Kursor — vazifa:2 ning base64url ko'rinishi (dmF6aWZhOjI). REST'dagi kursor (Pagination: offset, cursor, keyset) bilan bir xil g'oya. Har connection uchun shu uch klassni qayta yozmaslik uchun jamoalar generic yordamchi yasaydi — Paginated(Vazifa) kabi funksiya klass qaytaradi (Nest hujjatida "Generics" retsepti).

Yuqoridagi chuqurlik plagini bu so'rovni ham tekshirdi: sahifa → edges → node → matn — 4 daraja. Chegarani 3 qilganimizda Relay so'rovi ham rad etildi. Chegarani haqiqiy so'rovlaringizga qarab tanlang.

7. Subscriptions

Subscription (obuna) — GraphQL'ning real vaqt amali: mijoz bir marta obuna bo'ladi, server hodisa bo'lganda o'zi yuboradi. Transport — WebSocket, protokol — graphql-ws. Nest'da:

ts
import { PubSub } from "graphql-subscriptions";
const pubsub = new PubSub();

GraphQLModule.forRoot<ApolloDriverConfig>({
  // ...
  subscriptions: { "graphql-ws": true },
});

@Mutation(() => Izoh)
izohQosh(@Args("vazifaId", { type: () => ID }) vazifaId: string,
  @Args("matn") matn: string) {
  const iz = {
    id: izohlar.length + 1, matn, vazifaId: Number(vazifaId),
  };
  izohlar.push(iz);
  void pubsub.publish("izohQoshildi", { izohQoshildi: iz });
  return iz;
}

@Subscription(() => Izoh)
izohQoshildi() {
  return pubsub.asyncIterableIterator("izohQoshildi");
}

Mijoz sifatida graphql-ws paketining createClient ini ishlatdik (Node'da ws kutubxonasi bilan), obuna bo'ldik, keyin HTTP orqali mutation yubordik:

text
6) subscription (graphql-ws)
    200 {"data":{"izohQosh":{"id":"5"}}}
    obunachi oldi: {"data":{"izohQoshildi":{"id":"5","matn":"Shoshilinch"}}}

Mutation HTTP'da bajarildi, natija esa WebSocket orqali obunachiga keldi. Ichidagi zanjir: publish → PubSub → asyncIterableIterator → Apollo → graphql-ws → mijoz.

Uch eslatma:

  • PubSub (graphql-subscriptions 3.0.0) — xotirada, bitta jarayon ichida. Paket README'si ham uni demo va bitta nusxa uchun deb yozadi. Ikki nusxada — Redis asosidagi PubSubEngine kerak; bu oldingi darslardagi event-emitter cheklovining o'zi.
  • 3.0 versiyada metod nomi asyncIterableIterator (eski maqolalarda asyncIterator).
  • graphql-subscriptions 3.0.0 ning peer talabi ham graphql 16. Bizda 17 bilan ishladi, lekin bu kafolat emas.

vazifalar-api da real vaqt uchun SSE bor (Server-Sent Events NestJS'da). Subscription'ning foydasi — mijoz qaysi maydonlarni olishni o'zi tanlaydi. Narxi — WebSocket, alohida protokol va ikkinchi transport.

8. Federation — sinalmagan

Federation — bir nechta GraphQL xizmatni bitta umumiy sxemaga birlashtirish: "foydalanuvchilar" xizmati User ni, "vazifalar" xizmati Vazifa ni beradi, gateway (Apollo Router) ularni bitta grafga yig'adi. Nest'da ApolloFederationDriver va @Directive("@key(fields: \"id\")") bilan yoziladi. Bu katta tashkilotlar (o'nlab jamoa, o'nlab xizmat) uchun. Biz sinamadik: kamida uchta jarayon (ikki subgraph va router) va alohida router dasturi kerak. Mikroservis arxitekturasi haqida keyingi ikki darsda gaplashamiz.

9. JS modelida yuklovchi va kesh doirasi

29-qismdagi DataLoader'ning qisqa nusxasini olib, global va so'rov doirasidagi yuklovchini solishtiramiz. Blok brauzerda ishlaydi:

js
// 29-qismdagi DataLoader'ning qisqa nusxasi
class Yuklovchi {
  #kesh = new Map();
  #navbat = [];
  constructor(fn) {
    this.fn = fn;
  }
  load(id) {
    if (this.#kesh.has(id)) return this.#kesh.get(id);
    const p = new Promise((ok) => {
      this.#navbat.push({ id, ok });
      if (this.#navbat.length === 1) {
        queueMicrotask(() => this.#yubor());
      }
    });
    this.#kesh.set(id, p);
    return p;
  }
  async #yubor() {
    const n = this.#navbat;
    this.#navbat = [];
    const natija = await this.fn(n.map((x) => x.id));
    n.forEach((x, i) => x.ok(natija[i]));
  }
}

const izohlar = new Map([[1, ["Tez"]], [2, []]]);
let sorov = 0;
const ombordan = async (idlar) => {
  sorov++;
  return idlar.map((id) => [...(izohlar.get(id) ?? [])]);
};

// bitta GraphQL so'rovi: ikki vazifaning izohlari
const sora = (y) => Promise.all([1, 2].map((id) => y.load(id)));

const global = new Yuklovchi(ombordan); // XATO: hamma uchun bitta
console.log("1:", JSON.stringify(await sora(global)));
izohlar.get(2).push("Yangi izoh"); // mutation
console.log("2:", JSON.stringify(await sora(global)), "(eskirgan)");

// TO'G'RI: har so'rovga yangi yuklovchi (context fabrikasi)
const yangi = new Yuklovchi(ombordan);
console.log("3:", JSON.stringify(await sora(yangi)));
console.log(`omborga jami ${sorov} ta so'rov`);

Konsolda:

text
1: [["Tez"],[]]
2: [["Tez"],[]] (eskirgan)
3: [["Tez"],["Yangi izoh"]]
omborga jami 2 ta so'rov

Nest demosidagi manzaraning o'zi: global yuklovchi ikkinchi "so'rov"da omborga bormadi va eskirgan izohlarni berdi. Yangi yuklovchi omborga bordi va to'g'ri javob oldi. Jami ikki ombor so'rovi — har "so'rov"da bitta yig'ilgan so'rov (ikkinchisi umuman bormadi). queueMicrotask — bir tikdagi load larni yig'ish: birinchi load "yuborish"ni microtask'ka qo'yadi (Microtask va macrotask chuqur), qolganlari shu orada navbatga qo'shiladi.

sequenceDiagram
  participant M as Mijoz
  participant A as Apollo
  participant C as context()
  participant R as Resolver
  participant D as DataLoader
  M->>A: POST /graphql
  A->>C: har so'rovga
  C-->>A: yangi DataLoader
  A->>R: izohlar(v1), (v2), (v3)
  R->>D: load(1), load(2), load(3)
  D->>D: bitta tik — yig'ish
  D-->>R: ombor: [1,2,3] — 1 so'rov

Diagrammadagi context() qatori — butun darsning kaliti: yuklovchi so'rov bilan tug'iladi va so'rov bilan o'ladi.

10. Kanon tajribasi: nega loyihaga kirmaydi

Kanon tajribasi faqat N+1 ni qayd etdi (2 chaqiruv) va DataLoader, subscriptions, complexity, federation'ni sinamadi. Bu darsdagi qolgan chiqishlar — o'z demomiz. vazifalar-api ga GraphQL kirmagani sabablarini oldingi darsda aytdik. Bu dars ularga bittasini qo'shadi: GraphQL'ning chuqur imkoniyatlari (DataLoader, chegara, obuna) — har biri alohida infratuzilma va alohida xavfsizlik ishi. REST'da /v1/vazifalar?limit=50 ning narxi oldindan ma'lum, GraphQL'da esa uni plagin bilan o'zingiz hisoblashingiz kerak.

11. Ko'p uchraydigan xatolar

Xato Nima bo'ladi Tuzatish
@ResolveField da to'g'ridan baza N+1 DataLoader
Global DataLoader eskirgan va begona ma'lumot context fabrikasida yangi
Guard'da switchToHttp() so'rov undefined GqlExecutionContext.create(ctx)
Chegara yo'q bitta so'rov serverni to'ldiradi chuqurlik yoki murakkablik plagini
PubSub production'da boshqa nusxa hodisani eshitmaydi Redis PubSubEngine
idlar tartibi buzildi izohlar boshqa vazifaga tushadi natijani idlar tartibida qaytaring

12. Mashqlar

Mashqlar kurs/mashqlar/30/44-graphql-chuqur/ da.

1-mashq (oson): bo'shliqlarni to'ldiring

  • Ota obyektni maydon resolver'iga beradigan dekorator —
  • DataLoader qaysi joyda yaratilishi kerak (bir so'z)? — har uchun
  • Guard'da GraphQL kontekstini olish uchun klass —
  • Apollo plaginini Nest'ga tanitadigan dekorator —
  • Subscription transporti protokoli —
Yechim
  • @Parent — izohlar(@Parent() v: Vazifa).
  • so'rov — context: () => ({ izohYuklovchi: yuklovchiYarat() }).
  • GqlExecutionContext — .create(ctx).getContext().
  • @Plugin — @nestjs/apollo dan.
  • graphql-ws — subscriptions: { "graphql-ws": true }.

2-mashq (o'rta): clear bilan keshni tozalash

Darsdagi Yuklovchi ga clear(id) metodini qo'shing — keshdan shu kalitni o'chirsin va this ni qaytarsin (DataLoader'dagidek zanjir uchun). Ombor funksiyasi har chaqiruvda ombor: [...] deb yozsin. load(1), keyin izoh qo'shib clear(1) va yana load(1), oxirida yana load(1). Nechta ombor so'rovi bo'ladi? Ishora: Map.delete (Map).

Yechim
js
class Yuklovchi {
  #kesh = new Map();
  #navbat = [];
  constructor(fn) {
    this.fn = fn;
  }
  load(id) {
    if (this.#kesh.has(id)) return this.#kesh.get(id);
    const p = new Promise((ok) => {
      this.#navbat.push({ id, ok });
      if (this.#navbat.length === 1) {
        queueMicrotask(() => this.#yubor());
      }
    });
    this.#kesh.set(id, p);
    return p;
  }
  clear(id) {
    this.#kesh.delete(id);
    return this;
  }
  async #yubor() {
    const n = this.#navbat;
    this.#navbat = [];
    const natija = await this.fn(n.map((x) => x.id));
    n.forEach((x, i) => x.ok(natija[i]));
  }
}

const izohlar = new Map([[1, ["Tez"]]]);
const y = new Yuklovchi(async (idlar) => {
  console.log(`ombor: [${idlar}]`);
  return idlar.map((id) => [...(izohlar.get(id) ?? [])]);
});

console.log(await y.load(1));
izohlar.get(1).push("Bugun"); // shu so'rov ichida mutation
y.clear(1);
console.log(await y.load(1));
console.log(await y.load(1)); // keshdan, omborga bormaydi

Konsolda:

text
ombor: [1]
[ 'Tez' ]
ombor: [1]
[ 'Tez', 'Bugun' ]
[ 'Tez', 'Bugun' ]

Ikki ombor so'rovi: birinchi load va clear dan keyingisi. Uchinchi load keshdan keldi. Haqiqiy DataLoader'da ham clear(key), clearAll() va prime(key, value) bor — mutation resolver'i yangilangan yozuvni prime bilan keshga qo'yishi ham mumkin.

3-mashq (qiyin): so'rov narxi

Chuqurlik o'rniga murakkablikni hisoblang. So'rov — daraxt: { nom, args, bolalar }. Qoida: har maydon narxi 1; maydonda args.first bo'lsa, bolalarining jami narxi shu songa ko'paytiriladi (50 ta vazifaning har biri uchun izohlar). vazifalar(first: 50) { matn izohlar(first: 20) { matn muallif } } narxini hisoblang va 1000 chegarasi bilan solishtiring. Ishora: rekursiya va reduce.

Yechim
js
// So'rov "narxi": har maydon 1, ro'yxat maydoni esa
// first ga ko'paytiriladi
function narx(maydon) {
  const bolalar = (maydon.bolalar ?? [])
    .reduce((s, b) => s + narx(b), 0);
  const marta = maydon.args?.first ?? 1;
  return 1 + marta * bolalar;
}

const sorov = {
  nom: "vazifalar", args: { first: 50 },
  bolalar: [
    { nom: "matn" },
    { nom: "izohlar", args: { first: 20 },
      bolalar: [{ nom: "matn" }, { nom: "muallif" }] },
  ],
};
const n = narx(sorov);
console.log(`narx: ${n}`);
console.log(n > 1000 ? "rad etildi (limit 1000)" : "bajariladi");

Konsolda:

text
narx: 2101
rad etildi (limit 1000)

Hisob: izohlar = 1 + 20 × (1 + 1) = 41; vazifalar = 1 + 50 × (1 + 41) = 2 101. So'rov chuqurligi atigi 3 — chuqurlik chegarasi uni o'tkazib yuborardi. Murakkablik esa haqiqiy yukni ko'rsatadi: 50 vazifa × 20 izoh = 1 000 ta izoh qatori. graphql-query-complexity aynan shunday hisoblaydi (fieldExtensionsEstimator + simpleEstimator).

13. Loyiha qadami: kanon tajribasi, loyihaga kirmaydi

vazifalar-api o'zgarmaydi. Oldingi darsdagi demo loyihangiz bo'lsa, unga @ResolveField va context dagi DataLoader'ni qo'shing va ombor chaqiruvlarini sanab ko'ring: bitta so'rovda vazifalar soni ortsa ham, izohlar uchun ombor so'rovi bitta qolishi kerak.

14. Real ishda

  • DataLoader — Nest + GraphQL loyihalarining deyarli hammasida bor; review'da birinchi savol: "bu @ResolveField N+1 bermaydimi?".
  • Chegaralar — ochiq GraphQL API'lar (GitHub, Shopify) murakkablik "ballari" bilan ishlaydi va javobda qolgan limitni qaytaradi.
  • Subscriptions — chat, jonli kurs narxlari, buyurtma holati. Ko'p jamoalar esa oddiylik uchun SSE yoki WebSocket'ni GraphQL'dan alohida ishlatadi.
  • Intervyu savollari: "N+1 nima va DataLoader uni qanday hal qiladi?", "DataLoader nega har so'rovga yangi bo'lishi kerak?", "GraphQL'da guard qanday yoziladi?", "Federation qachon kerak?".

Xulosa

  • @ResolveField + @Parent — bog'liq maydon; faqat so'ralganda ishlaydi, lekin har ota uchun alohida — N+1.
  • DataLoader — GraphQLModule context fabrikasida, har so'rovga yangi; global yuklovchi eskirgan va begona ma'lumot beradi.
  • Guard, pipe GraphQL'da ishlaydi; so'rovni GqlExecutionContext.create(ctx).getContext() dan oling.
  • Chegaralar — @Plugin() Apollo plagini, didResolveOperation da; murakkablik chuqurlikdan aniqroq.
  • Relay — edges/node/cursor/pageInfo; subscriptions — graphql-ws + PubSub (xotirada, bitta nusxa).
  • Federation — ko'p jamoali katta tizimlar uchun; sinalmagan.

Keyingi dars: Mikroservis asoslari NestJS'da — createMicroservice, @MessagePattern va @EventPattern, ClientProxy — Nest ilovasi HTTP'siz xizmat sifatida.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
GraphQL chuqur NestJS'da: @ResolveField, DataLoader, guard'lar va subscriptions — IlmHamroh