اذهب إلى المحتوى

مدخل إلى المكتبة GraphQL واستعمالاتها في بناء تطبيقات الويب الحديثة


ابراهيم الخضور

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

وازدادت خلال السنوات الأخيرة شهرة المكتبة GraphQL التي طوّرتها فيسبوك Facebook للاتصال بين الخادم وتطبيق الويب.

تختلف فلسفة GraphQL عن REST بشكل واضح. حيث يعتمد المعيار REST على الموارد، ولكل مورد (كالمستخدم مثلًا) عنوانه الخاص الذي يعرّفه users/10/ مثلًا. وتجري كل العمليات على الموارد باستخدام طلب HTTP إلى عنوان موقعه. ويعتمد الفعل الذي سيُنفّذ على نوع الطلب HTTP.

تعمل آلية REST بشكل جيد في معظم الحالات. لكنها قد تتصرف بغرابة في بعض الأحيان.

لنفترض أن تطبيق المدونات سيمتلك وظيفة مشابهة لوسائل التواصل الاجتماعي، وأردنا تشكيل قائمة تضم أسماء كل المستخدمين الذين علّقوا على المدونات التي نتابعها.

فلو استعان الخادم بواجهة برمجية متوافقة مع REST، لكنا سنرسل عدة طلبات HTTP من المتصفح قبل أن نحصل على كل البيانات التي نحتاجها. كما ستعيد الطلبات الكثير من البيانات غير الضرورية، وربما ستكون الشيفرة معقدة على المتصفح.

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

يعتمد المبدأ العام للمكتبة GraphQL على حقيقة أن الشيفرة ستشكل على المتصفح استعلامًا يصف البيانات المطلوبة، ثم سترسلها إلى الواجهة البرمجية ضمن طلب HTTP-POST. وعلى خلاف REST، تُرسل كل الاستعلامات إلى نفس العنوان وستكون جميعها من النوع POST.

يمكن أن نحضر البيانات التي ناقشناها في السيناريو السابق كالتالي (تقريبيًا):

query FetchBlogsQuery {
  user(username: "mluukkai") {
    followedUsers {
      blogs {
        comments {
          user {
            blogs {
              title
            }
          }
        }
      }
    }
  }
}

سيستجيب الخادم بإعادته كائن JSON له تقريبًا البنية التالية:

{
  "data": {
    "followedUsers": [
      {
        "blogs": [
          {
            "comments": [
              {
                "user": {
                  "blogs": [
                    {
                      "title": "Goto considered harmful"
                    },
                    {
                      "title": "End to End Testing with Cypress is most enjoyable"
                    },
                    {
                      "title": "Navigating your transition to GraphQL"
                    },
                    {
                      "title": "From REST to GraphQL"
                    }
                  ]
                }
              }
            ]
          }
        ]
      }
    ]
  }
}

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

التخطيطات والاستعلامات

سنتعرف على أساسيات GraphQL، بإنجاز نسخة من تطبيق دليل الهاتف الذي عملنا عليه في القسمين 2و3، بالاعتماد على GraphQL.

في صميم كل تطبيقات GraphQL تخطيطًا يصف البيانات المرسلة بين العميل والخادم. سيكون التخطيط الأولي لتطبيق دليل الهاتف كالتالي:

type Person {
  name: String!
  phone: String
  street: String!
  city: String!
  id: ID! 
}

type Query {
  personCount: Int!
  allPersons: [Person!]!
  findPerson(name: String!): Person
}

يصف التخطيط نوعين. يحد أولهما Person أن للشخص خمسة حقول، أربعة منها "نصية" من النوع "String"، وهو أحد الأنماط السلّمية scalar types في GraphQL. ويجب أن تُعطى كل الحقول النصية قيمًا ما عدا الحقل phone. وستجد أنّ هذه الحقول مُعلّمة بإشارة تعجّب في التخطيط. أما نوع الحقل id فهو "ID". هذا النوع نصي أيضًا، لكن المكتبة GraphQL ستضمن أن قيمه فريدة.

أما النوع الثاني فهو Query أي استعلام. حيث يصف كل تخطيط في الواقع استعلامًا يحدد أنواع الاستعلامات التي يمكن طلبها من الواجهة البرمجية API.

يصف دليل الهاتف ثلاث استعلامات مختلفة: يعيد الأول personCount عددًا صحيحًا، ويعيد الثاني allPersons قائمة من الكائنات Person، أما الثالث findPerson فيتلقى معاملًا نصيًا ويعيد كائنًا من النوع Person. استخدمنا هنا أيضًا إشارة التعجّب لتحديد أي استعلام سيعيد قيمة، وأية معاملات ينبغي أن تحمل قيمة.

سيعيد الاستعلام personCount قيمة صحيحة، ويجب أن يتلقى الاستعلام findPerson معاملًا نصيًا وأن يعيد كائن من النوع Person أو "Null- لاشيء". كما سيعيد الاستعلام allPersons قائمة كائنات من النوع Person بحيث لا تحتوي القائمة على أية قيم من النوع "Null- لاشيء".

إذًا سيصف التخطيط الاستعلامات التي يمكن للمستخدم إرسالها إلى الخادم، والمعاملات التي تأخذها، وأنواع المعطيات التي تعيدها.

إن أبسط الاستعلامات هو personCount والذي يبدو كالتالي:

query {
  personCount
}

فلو افترضنا أن تطبيقنا قد حفظ معلومات عن ثلاث أشخاص، ستكون نتيجة الاستعلام كالتالي:

{
  "data": {
    "personCount": 3
  }
}

أما الاستعلام allPersons الذي يحضر معلومات كل الأشخاص، فهو أعقد بقليل. وطالما أنه سيعيد قائمة من الكائنات، فعليه أن يحدد أية حقول من الكائن سوف يعيدها:

query {
  allPersons {
    name
    phone
  }
}

يمكن أن تكون الاستجابة على النحو التالي:

{
  "data": {
    "allPersons": [
      {
        "name": "Arto Hellas",
        "phone": "040-123543"
      },
      {
        "name": "Matti Luukkainen",
        "phone": "040-432342"
      },
      {
        "name": "Venla Ruuska",
        "phone": null
      }
    ]
  }
}

يمكن أن ننفذ الاستعلام لإعادة إي حقل يصفه التخطيط. فالاستعلام التالي على سبيل المثال صحيح:

query {
  allPersons{
    name
    city
    street
  }
}

يظهر المثال السابق استعلامًا يتطلب معاملًا، ويعيد تفاصيل شخص واحد.

query {
  findPerson(name: "Arto Hellas") {
    phone 
    city 
    street
    id
  }
}

لاحظ كيف نضع المعامل بين قوسين، ثم ترتب حقول القيمة التي ستعاد ضمن قوسين معقوصين. ستكون الاستجابة كالتالي:

{
  "data": {
    "findPerson": {
      "phone": "040-123543",
      "city": "Espoo",
      "street": "Tapiolankatu 5 A"
      "id": "3d594650-3436-11e9-bc57-8b80ba54c431"
    }
  }
}

يمكن أن يعيد الاستعلام القيمة "Null"، فلو حاولنا البحث عن شخص غير موجود:

query {
  findPerson(name: "Donald Trump") {
    phone 
  }
}

ستكون النتيجة Null

{
  "data": {
    "findPerson": null
  }
}

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

يصف استعلام GraphQL البيانات المنتقلة بين الخادم والعميل فقط. بينما يمكننا تصنيف البيانات وحفظها كما نريد على الخادم.

وعلى الرغم من اسمها، لا تتعلق GraphQL فعليًا بقواعد البيانات، فهي لا تهتم بالطريقة التي تُخزّن فيها البيانات. فقد تُخزّن البيانات التي تستخدمها واجهة GraphQL البرمجية في قاعدة بيانات علاقيّة أو مستنديّة Document database، أو ضمن خوادم أخرى يمكن لخادم GraphQL الوصول إليها مع REST على سبيل المثال.

خادم Apollo

لننجز خادم GraphQL بمساعدة المكتبة الرائدة في هذا المجال Apollo -server. سننشئ مشروع npm جديد بتنفيذ الأمر npm init وسنثبت اعتمادياته اللازمة:

npm install apollo-server graphql

ستكون الشيفرة الأولية كالتالي:

const { ApolloServer, gql } = require('apollo-server')

let persons = [
  {
    name: "Arto Hellas",
    phone: "040-123543",
    street: "Tapiolankatu 5 A",
    city: "Espoo",
    id: "3d594650-3436-11e9-bc57-8b80ba54c431"
  },
  {
    name: "Matti Luukkainen",
    phone: "040-432342",
    street: "Malminkaari 10 A",
    city: "Helsinki",
    id: '3d599470-3436-11e9-bc57-8b80ba54c431'
  },
  {
    name: "Venla Ruuska",
    street: "Nallemäentie 22 C",
    city: "Helsinki",
    id: '3d599471-3436-11e9-bc57-8b80ba54c431'
  },
]

const typeDefs = gql`
  type Person {
    name: String!
    phone: String
    street: String!
    city: String! 
    id: ID!
  }

  type Query {
    personCount: Int!
    allPersons: [Person!]!
    findPerson(name: String!): Person
  }
`

const resolvers = {
  Query: {
    personCount: () => persons.length,
    allPersons: () => persons,
    findPerson: (root, args) =>
      persons.find(p => p.name === args.name)
  }
}

const server = new ApolloServer({
  typeDefs,
  resolvers,
})

server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`)
})

إن جوهر الشيفرة السابقة هو الكائن ApolloServer الذي يتلقى معاملين:

const server = new ApolloServer({
  typeDefs,
  resolvers,
})

الأول هو typeDefs ويتضمن تخطيط GraphQL، والثاني كائن يحتوي على محللات Resolvers لاستجابة الخادم، وهي شيفرة تحدد كيف سيستجيب الخادم لاستعلامات GraphQL. لشيفرة المحللات resolvers الشكل التالي:

وكما نرى، تتعلق المحللات بوصف الاستعلامات الوارد في التخطيط:

type Query {
  personCount: Int!
  allPersons: [Person!]!
  findPerson(name: String!): Person
}

إذًا، هناك حقل لكل استعلام يصفه التخطيط داخل النوع Query. فالاستعلام التالي:

query {
  personCount
}

سيمتلك المحلل:

() => persons.length

وبالتالي سيكون طول المصفوفة persons هو الرد على الاستعلام.

أما الاستعلام الذي سيحضر بيانات كل الأشخاص:

query {
  allPersons {
    name
  }
}

سيمتلك محللًا يعيد كل الكائنات الموجودة في المصفوفة persons.

() => persons

أرضية عمل GraphQL

عندما يعمل خادم Apollo في وضع التطوير node filename.js، سيُشغّل أرضية عمل(playground (GraphQL على العنوان http://localhost:4000/graphql. إنّ هذه الميزة مفيدة جدًا للمطورين، ويمكن استخدامها لإنشاء استعلامات إلى الخادم.

لنجرّب ذلك:

Gql-playground_01.png

يتطلب منك العمل مع الأرضية بعض الدقة. فلو أخطأت في صياغة الاستعلام، فلن تلاحظ رسالة الخطأ، ولن يحدث شيء على الإطلاق عند النقر على الزر "go".

gql-playground_query_error_02.png

ستبقى نتيجة الاستعلام السابق ظاهرة في الجهة اليمنى من أرضية العمل حتى لو كان الاستعلام الحالي خاطئًا. وبوضع مؤشر الفأرة على المكان الصحيح من السطر الذي ارتكبنا فيه الخطأ، ستظهر رسالة الخطأ.

gql-playground_error_message_03.png

إن بدا لك أن أرضية العمل لا تستجيب، فقد يساعدك تحديث الصفحة refresh.

سيظهر لك بالنقر على النص DOCS على يمين أرضية العمل تخطيط GraphQL على الخادم.

gql_schema_04.png

معاملات المحلل

يمتلك الاستعلام التالي الذي يحضر بيانات شخص واحد:

query {
  findPerson(name: "Arto Hellas") {
    phone 
    city 
    street
  }
}

محللًا يختلف عن سابقاته بامتلاكه معاملين:

(root, args) => persons.find(p => p.name === args.name)

يحتوي المعامل الثاني args معاملات الاستعلام. حيث يعيد المحلل عندها شخصًا من قائمة الأشخاص عندما يتطابق اسمه مع القيمة args.name. ولا يحتاج المحلل إلى المعامل الأول root.

تعطى كل المحللات عمليًا أربع معاملات. لكن ليس من الضروري في تعريف المعاملات إن لم نحتاجها. وسنرى كيف سنستعمل المعاملين الأول والثالث فقط لاحقًا في هذا القسم.

المحلل الافتراضي

عندما نرسل الاستعلام التالي على سبيل المثال:

query {
  findPerson(name: "Arto Hellas") {
    phone 
    city 
    street
  }
}

كيف يمكن للخادم أن يعيد الحقول التي يطلبها الاستعلام تمامًا؟

ينبغي على خادم GraphQL أن يحدد محللًا لكل حقل من كل نوع في التخطيط. لقد عرفنا حاليًا محللات للحقول من النوع "Query" فقط وذلك من أجل كل استعلام في التطبيق.

ولأننا لم نعرف محللات لحقول النوع "Person"، يحدد لها Apollo محللات افتراضية. تعمل هذه المحللات كالمثال التالي:

const resolvers = {
  Query: {
    personCount: () => persons.length,
    allPersons: () => persons,
    findPerson: (root, args) => persons.find(p => p.name === args.name)
  },
  Person: {
      name: (root) => root.name,
      phone: (root) => root.phone,
      street: (root) => root.street,
      city: (root) => root.city,
      id: (root) => root.id
  }}

يعيد المحلل الافتراضي القيمة المرتبطة بالحقل المحدد من الكائن. ويمكن الوصول إلى الكائن نفسه عبر معامل المحلل الأول "root".

لا يتوجب عليك تعريف محللات خاصة إن كانت المحللات الافتراضية كافية لتنفيذ المطلوب. كما يمكنك تعريف محللات ترتبط ببعض حقول نوع معين، وترك المحللات الافتراضية تتعامل مع بقية الحقول.

يمكنك على سبيل المثال تحديد عنوان واحد لكل الأشخاص وليكن Manhattan New York بكتابته مباشرة ضمن الشيفرة، وكتابة التالي لمحللات الحقلين street وcity العائدين للنوع "Person":

Person: {
  street: (root) => "Manhattan",
  city: (root) => "New York"
}

كائنات ضمن الكائنات

لنعدّل التخطيط السابق قليلًا:

type Address {  street: String!  city: String! }
type Person {
  name: String!
  phone: String
  address: Address!
  id: ID!
}

type Query {
  personCount: Int!
  allPersons: [Person!]!
  findPerson(name: String!): Person
}

سيمتلك الشخص حقلًا من النوع "Address" الذي يحتوي على المدينة والشارع. سيتغير الاستعلام الذي يتطلب العنوان إلى الشكل:

query {
  findPerson(name: "Arto Hellas") {
    phone 
    address {
      city 
      street
    }
  }
}

وستكون الاستجابة الآن على شكل كائن من النوع "person" يحتوي على كائن من النوع "Address".

{
  "data": {
    "findPerson": {
      "phone": "040-123543",
      "address":  {
        "city": "Espoo",
        "street": "Tapiolankatu 5 A"
      }
    }
  }
}

لكن طريقة تخزين بيانات الأشخاص على الخادم بقيت كما هي.

let persons = [
  {
    name: "Arto Hellas",
    phone: "040-123543",
    street: "Tapiolankatu 5 A",
    city: "Espoo",
    id: "3d594650-3436-11e9-bc57-8b80ba54c431"
  },
  // ...
]

إذًا، فالكائن "person" الذي يُخزَّن في الخادم ليس مطابقًا لكائن GraphQL من النوع "person" والموصوف في التخطيط.

وعلى النقيض، لا يمتلك النوع "Address" الحقل id لأنه لا يُخزَّن ضمن بنية خاصة به على الخادم. وطالما أنّ الكائنات التي خُزّنت داخل المصفوفة لا تمتلك الحقل address، لن يكون المحلل الافتراضي كافيًا. لنضيف إذًا محللًا للحقل address العائد للنوع "Person".

const resolvers = {
  Query: {
    personCount: () => persons.length,
    allPersons: () => persons,
    findPerson: (root, args) =>
      persons.find(p => p.name === args.name)
  },
  Person: {
      address: (root) => {
          return {
              street: root.street,
              city: root.city
          }
      }
  }}

وهكذا وفي كل مرة يُعاد فيها كائن من النوع "Person"، ستُعاد الحقول name وid وphone باستخدام محللاتها الخاصة أما الحقل address فسيتشكل باستخدام المحلل الذي عرّفناه. وطالما أنّ المعامل root لدالة المحلل هو كائن من النوع "Person"، فيمكن الوصول إلى الشارع والمدينة من حقوله.

ستجد شيفرة التطبيق بوضعه الحالي ضمن الفرع part8-1 في المستودع المخصص للتطبيق على GitHub

الطفرات

لنضف وظيفة إدخال مستخدم جديد إلى دليل الهاتف. تُنفَّذ جميع العمليات التي تسبب تغييرات في GraphQL باستخدام الطفرات (mutations). تُوصف الطفرات كمفاتيح من النوع "’Mutation" ضمن تخطيط GraphQL.

سيبدو تخطيط إضافة شخص جديد كالتالي:

type Mutation {
  addPerson(
    name: String!
    phone: String
    street: String!
    city: String!
  ): Person
}

تُعطى الطفرة تفاصيل الشخص كمعاملات. يمكن للمعامل phone فقط أن لا يعيد شيئًا. كما نلاحظ أن الطفرة ستعيد قيمة من النوع "Person"، والغرض من ذلك إعادة بيانات الشخص إن نجحت العملية و"NULL" إن فشلت. لا تمرر قيمة الحقل id كمعامل، فمن الأفضل ترك ذلك للخادم.

تتطلب الطفرات محللات أيضًا:

const { v1: uuid } = require('uuid')

// ...

const resolvers = {
  // ...
  Mutation: {
    addPerson: (root, args) => {
      const person = { ...args, id: uuid() }
      persons = persons.concat(person)
      return person
    }
  }
}

تضيف الطفرة الكائن الذي يمرر إليها عبر الوسيط args إلى المصفوفة persons، وتعيد الكائن الذي أضافته إلى المصفوفة. يُعطى الحقل id قيمة فريدة بالاستعانة بالمكتبة uuid.

يمكن أن يُضاف شخص جديد من خلال الطفرة التالية:

mutation {
  addPerson(
    name: "Pekka Mikkola"
    phone: "045-2374321"
    street: "Vilppulantie 25"
    city: "Helsinki"
  ) {
    name
    phone
    address{
      city
      street
    }
    id
  }
}

لاحظ أن الشخص الجديد سيوضع ضمن المصفوفة persons على الشكل التالي:

{
  name: "Pekka Mikkola",
  phone: "045-2374321",
  street: "Vilppulantie 25",
  city: "Helsinki",
  id: "2b24e0b0-343c-11e9-8c2a-cb57c2bf804f"
}

لكن استجابة الخادم على الطفرة ستكون بالشكل:

{
  "data": {
    "addPerson": {
      "name": "Pekka Mikkola",
      "phone": "045-2374321",
      "address": {
        "city": "Helsinki",
        "street": "Vilppulantie 25"
      },
      "id": "2b24e0b0-343c-11e9-8c2a-cb57c2bf804f"
    }
  }
}

إذًا، سيُنسّق محلل الحقل address للنوع "Person " الكائن الذي يُعيده الخادم بالشكل الصحيح.

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

إذا لم تتوافق المعاملات عند إنشاء شخص جديد مع الوصف المرافق للتخطيط، سيعطينا الخادم رسالة الخطأ التالية:

server_error_message_05.png

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

لنمنع إضافة نفس الاسم عدة مرات إلى دليل الهاتف:

const { ApolloServer, UserInputError, gql } = require('apollo-server')
// ...

const resolvers = {
  // ..
  Mutation: {
    addPerson: (root, args) => {
      if (persons.find(p => p.name === args.name)) {
          throw new UserInputError('Name must be unique', { 
                              invalidArgs: args.name,
         })
      }
      const person = { ...args, id: uuid() }
      persons = persons.concat(person)
      return person
    }
  }
}

فلو كان الاسم موجودًا مسبقًا، فسيرمي التطبيق الخطأ UserInputError.

user_input_error_06.png

ستجد شيفرة التطبيق بوضعه الحالي ضمن الفرع part8-2 في المستودع المخصص للتطبيق على GitHub.

التعداد Enum في GraphQL

لنضف إلى التطبيق إمكانية ترشيح الاستعلام الذي يعيد كل الأشخاص وفقًا للمعامل phone، وبالتالي: إما أن يعيد الاستعلام الشخص الذي يمتلك رقم هاتف فقط.

query {
  allPersons(phone: YES) {
    name
    phone 
  }
}

أو الأشخاص الذين لا يمتلكون رقم هاتف.

query {
  allPersons(phone: NO) {
    name
  }
}

ستغير التخطيط على النحو التالي:

enum YesNo { YES  NO}
type Query {
  personCount: Int!
  allPersons(phone: YesNo): [Person!]!  findPerson(name: String!): Person
}

يمثل النوع YesNo تعدادًا GraphQL، أو معدِّدًا يأخذ إحدى قيمتين YES أو NO. يعتبر المعامل phone في الاستعلام allPerson من النوع "YesNO"، لكنه قد يحمل القيمة "null".

سيتغير المحلل كالتالي:

Query: {
  personCount: () => persons.length,
  allPersons: (root, args) => {
      if (!args.phone) {
          return persons
      }
      const byPhone = (person) =>
      args.phone === 'YES' ? person.phone : !person.phone
      return persons.filter(byPhone)  },
          findPerson: (root, args) =>
    persons.find(p => p.name === args.name)
},

تغيير رقم الهاتف

لنضف طفرة لتغيير رقم هاتف شخص محدد. سيبدو تخطيط هذه الطفرة كالتالي:

type Mutation {
  addPerson(
    name: String!
    phone: String
    street: String!
    city: String!
  ): Person
  editNumber(    name: String!    phone: String!  ): Person}

وتنفذ هذه الطفرة باستخدام المحلل:

Mutation: {
  // ...
  editNumber: (root, args) => {
    const person = persons.find(p => p.name === args.name)
    if (!person) {
      return null
    }

    const updatedPerson = { ...person, phone: args.phone }
    persons = persons.map(p => p.name === args.name ? updatedPerson : p)
    return updatedPerson
  }   
}

ستجد الطفرة الشخص الذي ستُحدَّث بياناته من خلال الحقل name.

ستجد شيفرة التطبيق بوضعه الحالي ضمن الفرع part8-3 في المستودع المخصص للتطبيق على GitHub.

المزيد عن الاستعلامات

يمكن في GraphQL أن ندمج بين عدة حقول من استعلام، أو بين استعلامات مختلفة ضمن استعلام واحد. فالاستعلام التالي على سبيل المثال سيعيد عدد الأشخاص في دليل الهاتف وأسماءهم:

query {
  personCount
  allPersons {
    name
  }
}

وستظهر الإجابة على الشكل التالي:

{
  "data": {
    "personCount": 3,
    "allPersons": [
      {
        "name": "Arto Hellas"
      },
      {
        "name": "Matti Luukkainen"
      },
      {
        "name": "Venla Ruuska"
      }
    ]
  }
}

ويمكن للاستعلامات المشتركة أن تستخدم نفس بنية الاستعلام مرات عدة بشرط أن تعطي كلًا منها اسمًا مختلفًا كالتالي:

query {
  havePhone: allPersons(phone: YES){
    name
  }
  phoneless: allPersons(phone: NO){
    name
  }
}

وستكون الاستجابة كالتالي:

{
  "data": {
    "havePhone": [
      {
        "name": "Arto Hellas"
      },
      {
        "name": "Matti Luukkainen"
      }
    ],
    "phoneless": [
      {
        "name": "Venla Ruuska"
      }
    ]
  }
}

ومن المفيد أن نعطي الاستعلام اسمًا في بعض الأحيان. وخاصة عندما تمتلك الاستعلامات أو الطفرات معاملات. وسنتطرق إلى هذا الموضوع قريبًا.

إن ظهرت في شيفرتك استعلامات متعددة، ستسألك أرضية العمل أن تحدد الاستعلام الذي ستنفّذه:

playground_query_to_run_07.png

التمارين

سننجز خلال هذه التمارين واجهة خلفية باستخدام GraphQL لمكتبة صغيرة. استخدم الملف المخصص للتمرين والموجود على GitHub كنقطة انطلاق، وتذكر أن تستخدم أمر التهيئة npm init وأن تُثبّت الاعتماديات.

انتبه لفكرة أن الشيفرة لن تعمل بشكلها الحالي لأن تعريف التخطيط الخاص بالواجهة غير مكتمل بعد.

1. عدد الكتب المؤلفين

أنجز الاستعلامين bookCount وauthorCount اللذين يعيدان عدد الكتب وعدد المؤلفين.

ينبغي للاستعلام التالي:

query {
  bookCount
  authorCount
}

أن يعيد النتيجة التالية:

{
  "data": {
    "bookCount": 7,
    "authorCount": 5
  }
}

2. جميع الكتب

أنجز الاستعلام allBooks الذي يعيد تفاصيل جميع الكتب. على المستخدم في نهاية المطاف أن يكون قادرًا على تنفيذ الاستعلام التالي:

query {
  allBooks { 
    title 
    author
    published 
    genres
  }
}

3. جميع المؤلفين

أنجز الاستعلام allAuthors الذي يعيد تفاصيل جميع المؤلفين. يجب أن تتضمن الاستجابة الحقل bookCount الذي يضم عدد الكتب التي ألّفها الكاتب. فسيعيد الاستعلام التالي على سبيل المثال:

query {
  allAuthors {
    name
    bookCount
  }
}

النتيجة التالية:

{
  "data": {
    "allAuthors": [
      {
        "name": "Robert Martin",
        "bookCount": 2
      },
      {
        "name": "Martin Fowler",
        "bookCount": 1
      },
      {
        "name": "Fyodor Dostoevsky",
        "bookCount": 2
      },
      {
        "name": "Joshua Kerievsky",
        "bookCount": 1
      },
      {
        "name": "Sandi Metz",
        "bookCount": 1
      }
    ]
  }
}

4. الكتب التي أنجزها مؤلف

عدل الاستعلام allBooks لكي يتمكن المستخدم من تمرير المعامل الاختياري author إلى الاستعلام. يجب أن تحتوي الاستجابة على الكتب التي أنجزها المؤلف الذي مُرِّر من خلال المعامل.

سيعيد مثلًا الاستعلام التالي:

query {
  allBooks(author: "Robert Martin") {
    title
  }
}

النتيجة التالية:

{
  "data": {
    "allBooks": [
      {
        "title": "Clean Code"
      },
      {
        "title": "Agile software development"
      }
    ]
  }
}

5. الحصول على الكتب من خلال نوعها

عدل الاستعلام allBooks لكي يتمكن المستخدم من تمرير المعامل الاختياري genre إلى الاستعلام. ستحتوي الاستجابة على الكتب التي لها نفس النوع فقط.

سيعطي الاستعلام التالي على سبيل المثال:

query {
  allBooks(genre: "refactoring") {
    title
    author
  }
}

النتيجة التالية:

{
  "data": {
    "allBooks": [
      {
        "title": "Clean Code",
        "author": "Robert Martin"
      },
      {
        "title": "Refactoring, edition 2",
        "author": "Martin Fowler"
      },
      {
        "title": "Refactoring to patterns",
        "author": "Joshua Kerievsky"
      },
      {
        "title": "Practical Object-Oriented Design, An Agile Primer Using Ruby",
        "author": "Sandi Metz"
      }
    ]
  }
}

يجب أن يعمل الاستعلام أيضًا عند تمرير المعاملين السابقين معًا:

query {
  allBooks(author: "Robert Martin", genre: "refactoring") {
    title
    author
  }
}

6. إضافة كتاب

أنجز الطفرة addBook التي ستستخدم كالتالي:

mutation {
  addBook(
    title: "NoSQL Distilled",
    author: "Martin Fowler",
    published: 2012,
    genres: ["database", "nosql"]
  ) {
    title,
    author
  }
}

من المفترض أن تعمل الطفرة mutation حتى لو لم يُخزَّن اسم المؤلف على الخادم بعد:

mutation {
  addBook(
    title: "Pimeyden tango",
    author: "Reijo Mäki",
    published: 1997,
    genres: ["crime"]
  ) {
    title,
    author
  }
}

إن لم يُخزَّن اسم المؤلف على الخادم بعد، سيُضاف المؤلف الجديد إلى المنظومة. وطالما أن عام ولادة المؤلف لم يُخزَّن على الخادم بعد، فسيعيد الاستعلام التالي:

query {
  allAuthors {
    name
    born
    bookCount
  }
}

هذه النتيجة:

{
  "data": {
    "allAuthors": [
      // ...
      {
        "name": "Reijo Mäki",
        "born": null,
        "bookCount": 1
      }
    ]
  }
}

7. تحديث عام ولادة المؤلف

أنجز الطفرة editAuthor التي ستستخدم لتحديد عام ولادة المؤلف. يمكن استخدام الطفرة على النحو التالي:

mutation {
  editAuthor(name: "Reijo Mäki", setBornTo: 1958) {
    name
    born
  }
}

إن عُثر على المؤلف المطلوب، ستعيد العملية تفاصيل المؤلف وقد عُدِّل عام ولادته:

{
  "data": {
    "editAuthor": {
      "name": "Reijo Mäki",
      "born": 1958
    }
  }
}

إن لم يكن المؤلف موجودًا، ستعيد العملية القيمة "null".

{
  "data": {
    "editAuthor": null
  }
}

ترجمة -وبتصرف- للفصل GraphQl-Server من سلسلة Deep Dive Into Modern Web Development


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...