إنشاء أول واجهة API الخاصة بك في GraphQL

نشرت: 2022-03-11

مقدمة

قبل بضع سنوات ، قدم Facebook طريقة جديدة لبناء واجهات برمجة تطبيقات خلفية تسمى GraphQL ، وهي في الأساس لغة خاصة بالمجال لاستعلام البيانات ومعالجتها. في البداية ، لم أهتم به كثيرًا ، لكن في النهاية ، وجدت نفسي منخرطًا في مشروع في Toptal ، حيث كان عليّ تنفيذ واجهات برمجة التطبيقات الخلفية استنادًا إلى GraphQL. هذا عندما تقدمت وتعلمت كيفية تطبيق المعرفة التي تعلمتها من أجل REST على GraphQL.

لقد كانت تجربة ممتعة للغاية ، وخلال فترة التنفيذ ، كان علي إعادة التفكير في الأساليب والمنهجيات القياسية المستخدمة في واجهات برمجة تطبيقات REST بطريقة أكثر ملاءمة لـ GraphQL. في هذه المقالة ، أحاول تلخيص المشكلات الشائعة التي يجب مراعاتها عند تنفيذ واجهات برمجة تطبيقات GraphQL لأول مرة.

المكتبات المطلوبة

تم تطوير GraphQL داخليًا بواسطة Facebook وتم إصدارها للجمهور في عام 2015. في وقت لاحق من عام 2018 ، تم نقل مشروع GraphQL من Facebook إلى مؤسسة GraphQL التي تم إنشاؤها حديثًا ، والتي تستضيفها مؤسسة Linux غير الربحية ، والتي تحافظ وتطور مواصفات لغة استعلام GraphQL والمرجع تنفيذ جافا سكريبت.

نظرًا لأن GraphQL لا تزال تقنية حديثة وكان التنفيذ المرجعي الأولي متاحًا لـ JavaScript ، فإن معظم المكتبات الناضجة لها موجودة في النظام البيئي Node.js. هناك أيضًا شركتان أخريان ، Apollo و Prisma ، توفران أدوات ومكتبات مفتوحة المصدر لـ GraphQL. سيعتمد مثال المشروع في هذه المقالة على تنفيذ مرجعي لـ GraphQL لجافا سكريبت والمكتبات المقدمة من هاتين الشركتين:

  • Graphql-js - تنفيذ مرجعي لـ GraphQL لجافا سكريبت
  • خادم Apollo - خادم GraphQL لـ Express و Connect و Hapi و Koa والمزيد
  • Apollo-graphql-tools - إنشاء مخطط GraphQL واستهزائه ودرجه باستخدام SDL
  • البرامج الوسيطة Prisma-graphql - قسّم أدوات حل GraphQL في وظائف وسيطة

في عالم GraphQL ، أنت تصف واجهات برمجة التطبيقات الخاصة بك باستخدام مخططات GraphQL ، ولهذا تحدد المواصفات لغتها الخاصة التي تسمى لغة تعريف مخطط GraphQL (SDL). SDL بسيط جدًا وبديهي للاستخدام مع كونه قويًا للغاية ومعبّرًا في نفس الوقت.

توجد طريقتان لإنشاء مخططات GraphQL: أسلوب الكود أولاً ومنهج المخطط أولاً.

  • في نهج الكود أولاً ، تصف مخططات GraphQL الخاصة بك على أنها كائنات JavaScript استنادًا إلى مكتبة graphql-js ، ويتم إنشاء SDL تلقائيًا من التعليمات البرمجية المصدر.
  • في نهج المخطط أولاً ، يمكنك وصف مخططات GraphQL في SDL وربط منطق عملك باستخدام مكتبة أدوات Apollo Graphql.

أنا شخصياً أفضل نهج المخطط أولاً وسأستخدمه لنموذج المشروع في هذه المقالة. سنقوم بتنفيذ مثال مكتبة كلاسيكية وإنشاء نهاية خلفية والتي ستوفر واجهات برمجة تطبيقات CRUD لإنشاء مؤلفين وكتب بالإضافة إلى واجهات برمجة تطبيقات لإدارة المستخدم والمصادقة.

إنشاء خادم GraphQL أساسي

لتشغيل خادم GraphQL أساسي ، يجب علينا إنشاء مشروع جديد ، وتهيئته باستخدام npm ، وتهيئة Babel. لتهيئة Babel ، قم أولاً بتثبيت المكتبات المطلوبة بالأمر التالي:

 npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/node

بعد تثبيت Babel ، أنشئ ملفًا باسم .babelrc في الدليل الجذر لمشروعنا وانسخ التكوين التالي هناك:

 { "presets": [ [ "@babel/env", { "targets": { "node": "current" } } ] ] }

قم أيضًا بتحرير ملف package.json وأضف الأمر التالي إلى قسم scripts :

 { ... "scripts": { "serve": "babel-node index.js" }, ... }

بمجرد تكوين Babel ، قم بتثبيت مكتبات GraphQL المطلوبة باستخدام الأمر التالي:

 npm install --save express apollo-server-express graphql graphql-tools graphql-tag

بعد تثبيت المكتبات المطلوبة ، لتشغيل خادم GraphQL مع الحد الأدنى من الإعداد ، انسخ مقتطف الشفرة هذا في ملف index.js بنا:

 import gql from 'graphql-tag'; import express from 'express'; import { ApolloServer, makeExecutableSchema } from 'apollo-server-express'; const port = process.env.PORT || 8080; // Define APIs using GraphQL SDL const typeDefs = gql` type Query { sayHello(name: String!): String! } type Mutation { sayHello(name: String!): String! } `; // Define resolvers map for API definitions in SDL const resolvers = { Query: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } }, Mutation: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } } }; // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolvers maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); });

بعد ذلك ، يمكننا تشغيل خادمنا باستخدام الأمر npm run serve ، وإذا انتقلنا في متصفح الويب إلى عنوان URL http://localhost:8080/graphql ، فسيتم فتح الهيكل المرئي التفاعلي لـ GraphQL ، والذي يسمى Playground ، حيث يمكننا ذلك تنفيذ استعلامات GraphQL والطفرات والاطلاع على بيانات النتائج.

في عالم GraphQL ، تنقسم وظائف واجهة برمجة التطبيقات إلى ثلاث مجموعات ، تسمى الاستعلامات والطفرات والاشتراكات:

  • يستخدم العميل الاستعلامات لطلب البيانات التي يحتاجها من الخادم.
  • يستخدم العميل الطفرات لإنشاء / تحديث / حذف البيانات على الخادم.
  • يستخدم العميل الاشتراكات لإنشاء اتصال في الوقت الفعلي بالخادم والحفاظ عليه. يتيح ذلك للعميل الحصول على الأحداث من الخادم والتصرف وفقًا لذلك.

في مقالتنا ، سنناقش فقط الاستفسارات والطفرات. تعتبر الاشتراكات موضوعًا ضخمًا - فهي تستحق مقالها الخاص وليست مطلوبة في كل تطبيق لواجهة برمجة التطبيقات.

أنواع البيانات العددية المتقدمة

بعد وقت قصير جدًا من اللعب باستخدام GraphQL ، ستكتشف أن SDL يوفر فقط أنواع البيانات الأولية وأنواع البيانات القياسية المتقدمة مثل التاريخ والوقت والتاريخ والوقت ، والتي تعد جزءًا مهمًا من كل واجهة برمجة تطبيقات ، مفقودة. لحسن الحظ ، لدينا مكتبة تساعدنا في حل هذه المشكلة ، وتسمى Graphql-iso-date. بعد تثبيته ، سنحتاج إلى تحديد أنواع بيانات قياسية متقدمة جديدة في مخططنا وربطها بالتطبيقات التي توفرها المكتبة:

 import { GraphQLDate, GraphQLDateTime, GraphQLTime } from 'graphql-iso-date'; // Define APIs using GraphQL SDL const typeDefs = gql` scalar Date scalar Time scalar DateTime type Query { sayHello(name: String!): String! } type Mutation { sayHello(name: String!): String! } `; // Define resolvers map for API definitions in SDL const resolvers = { Date: GraphQLDate, Time: GraphQLTime, DateTime: GraphQLDateTime, Query: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } }, Mutation: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } } };

إلى جانب التاريخ والوقت ، توجد أيضًا تطبيقات أخرى لنوع البيانات العددية الشيقة ، والتي يمكن أن تكون مفيدة لك اعتمادًا على حالة الاستخدام الخاصة بك. على سبيل المثال ، أحدها هو graphql-type-json ، والذي يمنحنا القدرة على استخدام الكتابة الديناميكية في مخطط GraphQL الخاص بنا وتمرير أو إرجاع كائنات JSON غير المكتوبة باستخدام واجهة برمجة التطبيقات الخاصة بنا. توجد أيضًا مكتبة Graphql-scalar ، والتي تمنحنا القدرة على تحديد مقاييس GraphQL المخصصة مع التعقيم المتقدم / التحقق من الصحة / التحويل.

إذا لزم الأمر ، يمكنك أيضًا تحديد نوع البيانات العددية المخصصة واستخدامها في مخططك ، كما هو موضح أعلاه. هذا ليس بالأمر الصعب ، لكن مناقشته خارج نطاق هذه المقالة - إذا كنت مهتمًا ، يمكنك العثور على مزيد من المعلومات المتقدمة في وثائق Apollo.

تقسيم المخطط

بعد إضافة المزيد من الوظائف إلى مخططك ، سيبدأ في النمو وسنفهم أنه من المستحيل الاحتفاظ بمجموعة كاملة من التعريفات في ملف واحد ، ونحن بحاجة إلى تقسيمها إلى أجزاء صغيرة لتنظيم الكود وجعله أكثر قابلية للتوسع من أجل حجم أكبر. لحسن الحظ ، فإن وظيفة منشئ المخطط makeExecutableSchema ، التي يوفرها Apollo ، تقبل أيضًا تعريفات المخطط وخرائط الحلول في شكل مصفوفة. يمنحنا هذا القدرة على تقسيم مخططنا وخريطة المحلل إلى أجزاء أصغر. هذا هو بالضبط ما فعلته في مشروع نموذجي ؛ لقد قسمت واجهة برمجة التطبيقات إلى الأجزاء التالية:

  • auth.api.graphql - واجهة برمجة تطبيقات لمصادقة المستخدم والتسجيل
  • author.api.graphql - واجهة برمجة تطبيقات CRUD لإدخالات المؤلف
  • book.api.graphql - واجهة برمجة تطبيقات CRUD لإدخالات الكتاب
  • root.api.graphql - جذر المخطط والتعاريف الشائعة (مثل الأنواع العددية المتقدمة)
  • user.api.graphql - واجهة برمجة تطبيقات CRUD لإدارة المستخدم

أثناء مخطط التقسيم ، هناك شيء واحد يجب علينا مراعاته. يجب أن يكون أحد الأجزاء هو مخطط الجذر بينما يجب أن توسع الأجزاء الأخرى مخطط الجذر. هذا يبدو معقدًا ، لكنه في الواقع بسيط للغاية. في مخطط الجذر ، يتم تعريف الاستعلامات والطفرات على النحو التالي:

 type Query { ... } type Mutation { ... }

وفي الحالات الأخرى ، يتم تعريفها على النحو التالي:

 extend type Query { ... } extend type Mutation { ... }

و هذا كل شيء.

المصادقة والتخويل

في غالبية تطبيقات API ، هناك مطلب لتقييد الوصول العالمي وتقديم نوع من سياسات الوصول القائمة على القواعد. لهذا علينا أن نقدم في الكود الخاص بنا: المصادقة - لتأكيد هوية المستخدم - والتفويض ، لفرض سياسات الوصول المستندة إلى القواعد.

في عالم GraphQL ، مثل عالم REST ، بشكل عام للمصادقة نستخدم JSON Web Token. للتحقق من صحة رمز JWT الذي تم تمريره ، نحتاج إلى اعتراض جميع الطلبات الواردة والتحقق من رأس التفويض عليها. لهذا ، أثناء إنشاء خادم Apollo ، يمكننا تسجيل وظيفة كخطاف سياق ، والذي سيتم استدعاؤه بالطلب الحالي الذي ينشئ السياق المشترك عبر جميع أدوات الحل. يمكن القيام بذلك على النحو التالي:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, context: ({ req, res }) => { const context = {}; // Verify jwt token const parts = req.headers.authorization ? req.headers.authorization.split(' ') : ['']; const token = parts.length === 2 && parts[0].toLowerCase() === 'bearer' ? parts[1] : undefined; context.authUser = token ? verify(token) : undefined; return context; } }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); });

هنا ، إذا قام المستخدم بتمرير رمز JWT صحيح ، فإننا نتحقق منه ونخزن كائن المستخدم في السياق ، والذي سيكون متاحًا لجميع أدوات التحليل أثناء تنفيذ الطلب.

لقد تحققنا من هوية المستخدم ، ولكن لا يزال الوصول إلى واجهة برمجة التطبيقات الخاصة بنا متاحًا عالميًا ولا شيء يمنع مستخدمينا من الاتصال به دون إذن. تتمثل إحدى طرق منع ذلك في التحقق من كائن المستخدم في السياق مباشرةً في كل محلل ، ولكن هذا نهج معرض للخطأ للغاية لأنه يتعين علينا كتابة الكثير من التعليمات البرمجية المعيارية ويمكننا أن ننسى إضافة التحقق عند إضافة محلل جديد . إذا ألقينا نظرة على أطر عمل REST API ، فعادةً ما يتم حل هذه الأنواع من المشكلات باستخدام اعتراضات طلبات HTTP ، ولكن في حالة GraphQL ، لا يكون ذلك منطقيًا لأن طلب HTTP واحدًا يمكن أن يحتوي على استعلامات GraphQL متعددة ، وإذا كنا لا نزال نضيف نحن نحصل فقط على تمثيل السلسلة الأولية للاستعلام وعلينا تحليله يدويًا ، وهو بالتأكيد ليس أسلوبًا جيدًا. لا يُترجم هذا المفهوم جيدًا من REST إلى GraphQL.

لذلك نحن بحاجة إلى طريقة ما لاعتراض استعلامات GraphQL ، وهذه الطريقة تسمى برمجية prisma-graphql-middleware. تتيح لنا هذه المكتبة تشغيل تعليمات برمجية عشوائية قبل استدعاء المحلل أو بعده. إنه يحسن بنية الكود لدينا من خلال تمكين إعادة استخدام الكود والفصل الواضح للمخاوف.

أنشأ مجتمع GraphQL بالفعل مجموعة من البرامج الوسيطة الرائعة استنادًا إلى مكتبة البرامج الوسيطة Prisma ، والتي تحل بعض حالات الاستخدام المحددة ، ولإذن المستخدم ، توجد مكتبة تسمى Graphql-shield ، والتي تساعدنا في إنشاء طبقة إذن لواجهة برمجة التطبيقات الخاصة بنا.

بعد تثبيت graphql-shield ، يمكننا تقديم طبقة إذن لواجهة برمجة التطبيقات لدينا مثل هذا:

 import { allow } from 'graphql-shield'; const isAuthorized = rule()( (obj, args, { authUser }, info) => authUser && true ); export const permissions = { Query: { '*': isAuthorized, sayHello: allow }, Mutation: { '*': isAuthorized, sayHello: allow } }

ويمكننا تطبيق هذه الطبقة كبرنامج وسيط على مخططنا كما يلي:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); const schemaWithMiddleware = applyMiddleware(schema, shield(permissions, { allowExternalErrors: true })); // Build Apollo server const apolloServer = new ApolloServer({ schemaWithMiddleware }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })

هنا عند إنشاء كائن درع ، قمنا بتعيين allowExternalErrors على true ، لأنه افتراضيًا ، يكون سلوك الدرع هو التقاط الأخطاء التي تحدث داخل وحدات الحل ومعالجتها ، وهذا لم يكن سلوكًا مقبولًا لتطبيقي النموذجي.

في المثال أعلاه ، قمنا فقط بتقييد الوصول إلى واجهة برمجة التطبيقات الخاصة بنا للمستخدمين المصادق عليهم ، ولكن الدرع مرن للغاية ، وباستخدامه ، يمكننا تنفيذ مخطط تفويض غني جدًا لمستخدمينا. على سبيل المثال ، في نموذج التطبيق لدينا ، لدينا دورين: USER و USER_MANAGER ، ويمكن فقط للمستخدمين الذين لهم الدور USER_MANAGER استدعاء وظيفة إدارة المستخدم. يتم تنفيذ هذا على النحو التالي:

 export const isUserManager = rule()( (obj, args, { authUser }, info) => authUser && authUser.role === 'USER_MANAGER' ); export const permissions = { Query: { userById: isUserManager, users: isUserManager }, Mutation: { editUser: isUserManager, deleteUser: isUserManager } }

هناك شيء آخر أود ذكره وهو كيفية تنظيم وظائف البرامج الوسيطة في مشروعنا. كما هو الحال مع تعريفات المخططات وخرائط الحلول ، من الأفضل تقسيمها لكل مخطط والاحتفاظ بها في ملفات منفصلة ، ولكن على عكس خادم Apollo ، الذي يقبل مصفوفات من تعريفات المخطط وخرائط المحلل ويخيطها لنا ، فإن مكتبة البرامج الوسيطة Prisma لا تفعل ذلك و يقبل كائن خريطة وسيطة واحدًا فقط ، لذلك إذا قمنا بتقسيمها ، فسيتعين علينا إعادة تثبيتها يدويًا. لمعرفة الحل الخاص بي لهذه المشكلة ، يرجى الاطلاع على فئة ApiExplorer في نموذج المشروع.

تصديق

توفر GraphQL SDL وظائف محدودة للغاية للتحقق من صحة مدخلات المستخدم ؛ يمكننا فقط تحديد الحقل المطلوب وأيهما اختياري. أي متطلبات أخرى للتحقق من الصحة ، يجب أن ننفذها يدويًا. يمكننا تطبيق قواعد التحقق مباشرة في وظائف المحلل ، لكن هذه الوظيفة لا تنتمي هنا حقًا ، وهذه حالة استخدام أخرى رائعة لمستخدم GraphQL الوسيطة. على سبيل المثال ، دعنا نستخدم بيانات إدخال طلب تسجيل المستخدم ، حيث يتعين علينا التحقق مما إذا كان اسم المستخدم هو عنوان بريد إلكتروني صحيح ، وإذا كانت مدخلات كلمة المرور متطابقة ، وكانت كلمة المرور قوية بدرجة كافية. يمكن تنفيذ ذلك على النحو التالي:

 import { UserInputError } from 'apollo-server-express'; import passwordValidator from 'password-validator'; import { isEmail } from 'validator'; const passwordSchema = new passwordValidator() .is().min(8) .is().max(20) .has().letters() .has().digits() .has().symbols() .has().not().spaces(); export const validators = { Mutation: { signup: (resolve, parent, args, context) => { const { email, password, rePassword } = args.signupReq; if (!isEmail(email)) { throw new UserInputError('Invalid Email address!'); } if (password !== rePassword) { throw new UserInputError('Passwords don\'t match!'); } if (!passwordSchema.validate(password)) { throw new UserInputError('Password is not strong enough!'); } return resolve(parent, args, context); } } }

ويمكننا تطبيق طبقة المدققين كبرنامج وسيط على مخططنا ، جنبًا إلى جنب مع طبقة أذونات مثل هذا:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); const schemaWithMiddleware = applyMiddleware(schema, validators, shield(permissions, { allowExternalErrors: true })); // Build Apollo server const apolloServer = new ApolloServer({ schemaWithMiddleware }); apolloServer.applyMiddleware({ app })

استعلامات N + 1

هناك مشكلة أخرى يجب مراعاتها ، والتي تحدث مع واجهات برمجة تطبيقات GraphQL وغالبًا ما يتم تجاهلها ، وهي استعلامات N + 1. تحدث هذه المشكلة عندما تكون لدينا علاقة رأس بأطراف بين الأنواع المحددة في مخططنا. لتوضيح ذلك ، على سبيل المثال ، دعنا نستخدم كتاب API لمشروعنا النموذجي:

 extend type Query { books: [Book!]! ... } extend type Mutation { ... } type Book { id: ID! creator: User! createdAt: DateTime! updatedAt: DateTime! authors: [Author!]! title: String! about: String language: String genre: String isbn13: String isbn10: String publisher: String publishDate: Date hardcover: Int } type User { id: ID! createdAt: DateTime! updatedAt: DateTime! fullName: String! email: String! }

هنا ، نرى أن نوع User له علاقة رأس بأطراف مع نوع Book ، ويتم تمثيل هذه العلاقة كحقل منشئ في Book . يتم تعريف خريطة المُحلل لهذا المخطط على النحو التالي:

 export const resolvers = { Query: { books: (obj, args, context, info) => { return bookService.findAll(); }, ... }, Mutation: { ... }, Book: { creator: ({ creatorId }, args, context, info) => { return userService.findById(creatorId); }, ... } }

إذا قمنا بتنفيذ استعلام كتب باستخدام واجهة برمجة التطبيقات هذه ، وألقينا نظرة على سجل عبارات SQL ، فسنرى شيئًا مثل هذا:

 select `books`.* from `books` select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? ...

من السهل التخمين - أثناء التنفيذ ، تم استدعاء المحلل أولاً لاستعلام الكتب ، والذي أعاد قائمة بالكتب ثم سُمي كل كائن كتاب بمحلل حقل المنشئ ، وقد تسبب هذا السلوك في استعلامات قاعدة بيانات N + 1. إذا كنا لا نريد تفجير قاعدة بياناتنا ، فإن هذا النوع من السلوك ليس رائعًا حقًا.

لحل مشكلة استعلامات N + 1 ، أنشأ مطورو Facebook حلاً مثيرًا للاهتمام يسمى DataLoader ، والذي تم وصفه في صفحة README الخاصة به مثل هذا:

"DataLoader هي أداة مساعدة عامة يتم استخدامها كجزء من طبقة جلب البيانات للتطبيق الخاص بك لتوفير واجهة برمجة تطبيقات مبسطة ومتسقة عبر مصادر البيانات البعيدة المختلفة مثل قواعد البيانات أو خدمات الويب عبر التجميع والتخزين المؤقت"

ليس من السهل جدًا فهم كيفية عمل DataLoader ، لذلك دعونا نرى أولاً المثال الذي يحل المشكلة الموضحة أعلاه ثم نشرح المنطق الكامن وراءها.

في مشروعنا النموذجي ، يتم تعريف DataLoader على هذا النحو لحقل المنشئ:

 export class UserDataLoader extends DataLoader { constructor() { const batchLoader = userIds => { return userService .findByIds(userIds) .then( users => userIds.map( userId => users.filter(user => user.id === userId)[0] ) ); }; super(batchLoader); } static getInstance(context) { if (!context.userDataLoader) { context.userDataLoader = new UserDataLoader(); } return context.userDataLoader; } }

بمجرد تحديد UserDataLoader ، يمكننا تغيير محلل حقل المنشئ مثل هذا:

 export const resolvers = { Query: { ... }, Mutation: { ... }, Book: { creator: ({ creatorId }, args, context, info) => { const userDataLoader = UserDataLoader.getInstance(context); return userDataLoader.load(creatorId); }, ... } }

بعد التغييرات المطبقة ، إذا قمنا بتنفيذ استعلام الكتب مرة أخرى ونظرنا إلى سجل عبارات SQL ، فسنرى شيئًا كالتالي:

 select `books`.* from `books` select `users`.* from `users` where `id` in (?)

هنا ، يمكننا أن نرى أن استعلامات قاعدة بيانات N + 1 قد تم تقليصها إلى استعلامين ، حيث يختار الأول قائمة الكتب والثاني يختار قائمة المستخدمين المقدمين كمبدعين في قائمة الكتب. الآن دعنا نشرح كيف يحقق DataLoader هذه النتيجة.

الميزة الأساسية لبرنامج DataLoader هي التجميع. أثناء مرحلة التنفيذ الفردية ، سيقوم DataLoader بجمع جميع المعرفات المميزة لجميع استدعاءات وظيفة التحميل الفردية ثم استدعاء وظيفة الدُفعة مع جميع المعرفات المطلوبة. أحد الأشياء المهمة التي يجب تذكرها هو أنه لا يمكن إعادة استخدام مثيلات DataLoaders ، بمجرد استدعاء وظيفة الدُفعة ، سيتم تخزين القيم التي تم إرجاعها مؤقتًا على سبيل المثال إلى الأبد. بسبب هذا السلوك ، يجب علينا إنشاء مثيل جديد من DataLoader لكل مرحلة تنفيذ. لتحقيق ذلك ، أنشأنا وظيفة getInstance ثابتة ، والتي تتحقق مما إذا كان مثيل DataLoader مقدمًا في كائن سياق ، وإذا لم يتم العثور عليه ، يقوم بإنشاء واحد. تذكر أنه يتم إنشاء كائن سياق جديد لكل مرحلة تنفيذ وتتم مشاركته عبر جميع أدوات الحل.

تقبل وظيفة تحميل الدُفعات الخاصة بـ DataLoader مصفوفة من المعرفات المطلوبة المميزة وترجع وعدًا يحل إلى مصفوفة من الكائنات المقابلة. عند كتابة دالة تحميل دفعة ، يجب أن نتذكر شيئين مهمين:

  1. يجب أن يكون صفيف النتائج بنفس طول صفيف المعرفات المطلوبة. على سبيل المثال ، إذا طلبنا المعرفات [1, 2, 3] ، يجب أن تحتوي مصفوفة النتائج التي تم إرجاعها على ثلاثة كائنات بالضبط: [{ "id": 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }, { “id”: 3, “fullName”: “user3” }]
  2. يجب أن يتوافق كل فهرس في مصفوفة النتائج مع نفس الفهرس في مصفوفة المعرفات المطلوبة. على سبيل المثال ، إذا كانت مصفوفة المعرفات المطلوبة لها الترتيب التالي: [3, 1, 2] ، فيجب أن تحتوي مصفوفة النتائج التي تم إرجاعها على كائنات بنفس الترتيب تمامًا: [{ "id": 3, “fullName”: “user3” }, { “id”: 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }]

في مثالنا ، نتأكد من أن ترتيب النتائج يطابق ترتيب المعرفات المطلوبة مع الكود التالي:

 then( users => userIds.map( userId => users.filter(user => user.id === userId)[0] ) )

حماية

وأخيراً وليس آخراً ، أود أن أذكر الأمن. باستخدام GraphQL ، يمكننا إنشاء واجهات برمجة تطبيقات مرنة جدًا ومنح المستخدم إمكانات غنية لكيفية الاستعلام عن البيانات. يمنح هذا قدرًا كبيرًا من القوة إلى جانب العميل في التطبيق ، وكما قال العم بن ، "مع القوة الكبيرة ، تأتي مسؤولية كبيرة". بدون الأمان المناسب ، يمكن للمستخدم الضار إرسال استعلام باهظ الثمن والتسبب في هجوم DoS (رفض الخدمة) على خادمنا.

أول شيء يمكننا القيام به لحماية واجهة برمجة التطبيقات الخاصة بنا هو تعطيل التأمل في مخطط GraphQL. بشكل افتراضي ، يكشف خادم واجهة برمجة تطبيقات GraphQL عن القدرة على التأمل في مخططه بالكامل ، والذي يتم استخدامه عمومًا بواسطة القذائف المرئية التفاعلية مثل GraphiQL و Apollo Playground ، ولكنه أيضًا يمكن أن يكون مفيدًا جدًا للمستخدم الضار في إنشاء استعلام معقد استنادًا إلى واجهة برمجة التطبيقات الخاصة بنا . يمكننا تعطيل هذا عن طريق ضبط معلمة introspection على false عند إنشاء خادم Apollo:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, introspection: false }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })

الشيء التالي الذي يمكننا القيام به لحماية API الخاص بنا هو الحد من عمق الاستعلام. هذا مهم بشكل خاص إذا كانت لدينا علاقة دورية بين أنواع البيانات لدينا. على سبيل المثال ، في العينة الخاصة بنا ، يوجد نوع المشروع Author لديه كتب ميدانية ، ونوع Book به مؤلفون ميدانيون. من الواضح أن هذه علاقة دورية ، ولا شيء يمنع المستخدم الضار من كتابة استعلام مثل هذا:

 query { authors { id, fullName books { id, title authors { id, fullName books { id, title, authors { id, fullName books { id, title authors { ... } } } } } } } }

من الواضح أنه مع وجود تداخل كافٍ ، يمكن لمثل هذا الاستعلام أن يؤدي إلى تفجير خادمنا بسهولة. للحد من عمق الاستعلامات ، يمكننا استخدام مكتبة تسمى Graphql-deep-limit. بمجرد التثبيت ، يمكننا تطبيق قيود العمق عند إنشاء خادم Apollo ، على النحو التالي:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, introspection: false, validationRules: [ depthLimit(5) ] }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })

هنا ، قمنا بتحديد أقصى عمق للاستعلامات بخمسة.

بعد البرنامج النصي: يعد الانتقال من REST إلى GraphQL أمرًا ممتعًا

في هذا البرنامج التعليمي ، حاولت توضيح المشكلات الشائعة التي ستواجهها عند بدء تنفيذ واجهات برمجة تطبيقات GraphQL. ومع ذلك ، تقدم بعض أجزاء منه أمثلة التعليمات البرمجية الضحلة جدًا ولا تخدش سوى سطح المشكلة التي تمت مناقشتها ، نظرًا لحجمها. لهذا السبب ، للاطلاع على المزيد من أمثلة التعليمات البرمجية الكاملة ، يرجى الرجوع إلى مستودع Git لنموذج مشروع GraphQL API الخاص بي: graphql-example.

في النهاية ، أود أن أقول إن GraphQL هي تقنية مثيرة للاهتمام حقًا. هل ستحل محل REST؟ لا أحد يعرف ، ربما غدًا في عالم تكنولوجيا المعلومات سريع التغير ، سيظهر نهج أفضل لتطوير واجهات برمجة التطبيقات ، لكن GraphQL تندرج حقًا في فئة التقنيات المثيرة للاهتمام التي تستحق التعلم بالتأكيد.