NestJS — Node.js للمؤسسات

اشتراكات GraphQL والأنماط المتقدمة

16 دقيقة الدرس 34 من 48

اشتراكات GraphQL والأنماط المتقدمة

تتناول هذه الدرس أربع قدرات GraphQL على مستوى الإنتاج: البيانات الفورية عبر @Subscription وPubSub، وحلّ العلاقات المتداخلة بـ @ResolveField، والقضاء على مشكلة N+1 الشهيرة بـ DataLoader، وإعادة الأخطاء المنظّمة عبر معالجة أخطاء GraphQL. تجتمع هذه المفاهيم لترفع الواجهة البرمجية من مستوى أساسي إلى جودة المؤسسات.

البث الفوري بـ @Subscription وPubSub

تدفع الاشتراكات البيانات إلى العملاء عبر اتصال WebSocket دائم. يربطها NestJS بواسطة @Subscription() على دالة المُحلِّل وكائن PubSub الذي يعمل كحافلة أحداث داخل العملية. لعمليات النشر ذات الخادم الواحد، تكفي PubSub المدمجة من graphql-subscriptions؛ أما في البيئات متعددة العُقد فاستبدلها بـ pub/sub مدعوم بـ Redis.

// posts.resolver.ts import { Resolver, Mutation, Subscription, Args } from '@nestjs/graphql'; import { PubSub } from 'graphql-subscriptions'; import { Post } from './post.model'; import { CreatePostInput } from './dto/create-post.input'; const pubSub = new PubSub(); const POST_ADDED_EVENT = 'postAdded'; @Resolver(() => Post) export class PostsResolver { @Mutation(() => Post) async createPost(@Args('input') input: CreatePostInput): Promise<Post> { const post = await this.postsService.create(input); await pubSub.publish(POST_ADDED_EVENT, { postAdded: post }); return post; } @Subscription(() => Post) postAdded() { return pubSub.asyncIterator(POST_ADDED_EVENT); } }
يُشترط نقل WebSocket. تحتاج الاشتراكات إلى installSubscriptionHandlers: true (Apollo Server 2) أو إضافة subscriptions-transport-ws أو graphql-ws مُهيَّأة على GraphQLModule. بروتوكول HTTP وحده لا يستطيع دفع البيانات إلى العملاء.

حلّ الأنواع المتداخلة بـ @ResolveField

يعلّم @ResolveField() نظامَ NestJS كيفية ملء حقل لا توجد قيمته مباشرةً على الكيان الأصل — مثل جلب كائن author كلّما أُعيد Post. تتلقّى الدالة الكائنَ الأصلَ كوسيط أول (يُحقَن بـ @Parent()) لتنفّذ بحثًا مستهدفًا:

import { Resolver, ResolveField, Parent } from '@nestjs/graphql'; import { Post } from './post.model'; import { User } from '../users/user.model'; @Resolver(() => Post) export class PostsResolver { @ResolveField(() => User) async author(@Parent() post: Post): Promise<User> { return this.usersService.findOne(post.authorId); } }

مشكلة N+1 وDataLoader

تنشأ مشكلة N+1 حين يُطلق جلب قائمة من N منشورات استعلامًا منفصلًا عن المؤلّف لكلّ منشور، ما يُنتج استعلام قائمة واحدًا + N استعلامًا للمؤلّفين. يحلّ DataLoader هذا بـالتجميع: يجمع كل معرّفات المؤلّفين المُجمَّعة خلال دورة تنفيذ GraphQL واحدة في استعلام واحد، ثم يوزّع النتائج على كلّ مُحلِّل.

  • التثبيت: npm install dataloader
  • أنشئ مُحمِّلًا يقبل مصفوفة من المعرّفات ويُعيد مصفوفة كيانات بالترتيب ذاته.
  • اجعل نطاق المُحمِّل الطلب (استخدم مزوّدًا بنطاق REQUEST أو DataLoaderInterceptor في NestJS) ليكون التجميع لكل طلب وليس عالميًا.
import DataLoader from 'dataloader'; import { Injectable, Scope } from '@nestjs/common'; @Injectable({ scope: Scope.REQUEST }) export class UserLoader { private loader = new DataLoader<number, User>(async (ids) => { const users = await this.usersService.findByIds(ids as number[]); // أعِد النتائج بالترتيب ذاته للمعرّفات return ids.map(id => users.find(u => u.id === id) ?? new Error(`User ${id} not found`)); }); load(id: number): Promise<User> { return this.loader.load(id); } } // في PostsResolver — استبدل استدعاء الخدمة المباشر بالمُحمِّل: @ResolveField(() => User) async author(@Parent() post: Post): Promise<User> { return this.userLoader.load(post.authorId); // مُجمَّع تلقائيًا }
استخدم دائمًا نطاق REQUEST مع DataLoader. مُحمِّل مُفرد (singleton) سيُسرِّب البيانات المُجمَّعة عبر طلبات غير مترابطة ويُخزّن نتائج قديمة طوال عمر العملية.

معالجة أخطاء GraphQL

ينبغي أن تكون أخطاء GraphQL ذات معنى ومُصنَّفة. يُمرّر NestJS استثناءاته (مثل NotFoundException) إلى مصفوفة errors في الاستجابة، لكن يمكنك أيضًا رمي GraphQLError مباشرةً للحصول على رموز خطأ أكثر ثراءً للعميل:

  • رمي استثناءات NestJS — تُترجَم تلقائيًا إلى أخطاء GraphQL مع الحفاظ على الرسالة.
  • رموز خطأ مخصّصة — استخدم extensions.code على GraphQLError ليتمكّن العملاء من التمييز بين أنواع الأخطاء برمجيًا.
  • تنسيق الأخطاء عالميًا — هيّئ formatError على GraphQLModule لحذف تتبّع المكدّس في الإنتاج.
لا تكشف أبدًا تتبّعات المكدّس الداخلية أو تفاصيل أخطاء قاعدة البيانات في استجابات GraphQL الإنتاجية. استخدم formatError لإعادة رسالة آمنة فقط ورمز قابل للقراءة آليًا. سجّل التفاصيل الكاملة على جانب الخادم.

الخلاصة

يُرسل @Subscription + PubSub الأحداث الفورية عبر WebSockets؛ استبدل بـ Redis PubSub للنشر متعدد العُقد. يملأ @ResolveField الأنواع المتداخلة بنظافة. يُجمِّع DataLoader (بنطاق REQUEST) الاستعلامات المتداخلة ويُزيل مشكلة N+1. تحمي معالجة الأخطاء المنظَّمة بـ formatError العملاءَ من تسرّب التفاصيل الداخلية مع توفير رموز قابلة للتنفيذ. هذه الأنماط الأربعة هي أساس واجهات NestJS GraphQL الجاهزة للإنتاج.