الجزء 8: GraphQL

خادم GraphQL

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

في السنوات الأخيرة، أصبح GraphQL، الذي طوّرته Facebook، شائعاً في التواصل بين تطبيقات الويب والخوادم.

فلسفة GraphQL مختلفة جداً عن REST. فـ REST قائمة على الموارد. لكل مورد، مثلاً مستخدم، عنوانه الخاص الذي يعرّفه، مثلاً /users/10. وتُنفَّذ جميع العمليات على المورد عبر طلبات HTTP إلى عنوان URL الخاص به. ويعتمد الإجراء على طريقة HTTP المستخدمة.

إن اعتماد REST على الموارد يعمل جيداً في معظم الحالات. لكنه قد يكون مرهقاً بعض الشيء أحياناً.

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

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

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

خادم GraphQL مناسب تماماً لهذه الأنواع من الحالات.

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

يمكن جلب البيانات الموصوفة في السيناريو أعلاه باستعلام (تقريبي) كالتالي:

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

يمكن تفسير محتوى FetchBlogsQuery تقريباً كالتالي: ابحث عن مستخدم اسمه "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 بتنفيذ نسخة GraphQL من تطبيق دليل الهاتف من الجزأين 2 و3.

في قلب كل تطبيقات 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، وهو أحد الأنواع القياسية في GraphQL. يجب إعطاء قيمة لجميع حقول String، باستثناء phone. ويُشار إلى ذلك بعلامة التعجب في المخطط. نوع الحقل id هو ID. حقول ID نصوص، لكن GraphQL يضمن أنها فريدة.

النوع الثاني هو Query. عملياً، يصف كل مخطط GraphQL نوع Query، الذي يحدد أنواع الاستعلامات التي يمكن إجراؤها على الـ API.

يصف دليل الهاتف ثلاثة استعلامات مختلفة. يعيد personCount عدداً صحيحاً، ويعيد allPersons قائمة من كائنات Person، ويُعطى findPerson معاملاً نصياً ويعيد كائن Person.

مرة أخرى، تُستخدم علامات التعجب لتحديد القيم المُعادة والمعاملات التي هي غير قابلة للقيمة الفارغة (Non-Null). سيعيد personCount عدداً صحيحاً بالتأكيد. ويجب إعطاء الاستعلام findPerson نصاً كمعامل. ويعيد الاستعلام كائن Person أو null. ويعيد allPersons قائمة من كائنات Person، ولا تحتوي القائمة على أي قيم null.

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

أبسط الاستعلامات، personCount، يبدو كالتالي:

query {
  personCount
}

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

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

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

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"
    }
  }
}

تم تعليم القيمة المُعادة كقابلة للقيمة الفارغة (nullable)، لذا إذا بحثنا عن تفاصيل شخص غير معروف

query {
  findPerson(name: "Joe Biden") {
    phone
  }
}

فالقيمة المُعادة هي null.

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

كما ترى، هناك رابط مباشر بين استعلام GraphQL وكائن JSON المُعاد. ويمكن للمرء أن يفكر في أن الاستعلام يصف نوع البيانات التي يريدها كاستجابة. والفرق عن استعلامات REST صارخ. ففي REST، لا علاقة لعنوان URL ونوع الطلب بشكل البيانات المُعادة.

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

على الرغم من اسمه، لا علاقة لـ GraphQL فعلياً بقواعد البيانات. فهو لا يهتم بكيفية حفظ البيانات. يمكن حفظ البيانات التي يستخدمها GraphQL API في قاعدة بيانات علائقية، أو قاعدة بيانات مستندية، أو في خوادم أخرى يمكن لخادم GraphQL الوصول إليها باستخدام REST مثلاً.

Apollo Server

لننفّذ خادم GraphQL باستخدام المكتبة الرائدة اليوم: Apollo Server.

أنشئ مشروع npm جديد باستخدام npm init وثبّت الاعتماديات المطلوبة.

npm install @apollo/server graphql

أنشئ أيضاً ملف index.js في الدليل الجذري لمشروعك.

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

const { ApolloServer } = require('@apollo/server')
const { startStandaloneServer } = require('@apollo/server/standalone')

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 = `
  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,
})

startStandaloneServer(server, {
  listen: { port: 4000 },
}).then(({ url }) => {
  console.log(`Server ready at ${url}`)
})

قلب الشيفرة هو ApolloServer، الذي يُعطى معاملين:

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

المعامل الأول، typeDefs، يحتوي على مخطط GraphQL.

المعامل الثاني كائن يحتوي على resolvers الخاصة بالخادم. وهي الشيفرة التي تحدد كيفية الاستجابة لاستعلامات GraphQL.

شيفرة الـ resolvers كالتالي:

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

كما ترى، تتوافق الـ resolvers مع الاستعلامات الموصوفة في المخطط.

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

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

الاستعلام

query {
  personCount
}

له الـ resolver

() => persons.length

إذن الاستجابة للاستعلام هي طول المصفوفة persons.

الاستعلام الذي يجلب جميع الأشخاص

query {
  allPersons {
    name
  }
}

له resolver يعيد جميع الكائنات من المصفوفة persons.

() => persons

Apollo Studio Explorer

لنضف السكربتات التالية إلى package.json لتشغيل التطبيق:

{
  //...
  "scripts": {
    "start": "node index.js",
    "dev": "node --watch index.js",
    // ...
  }
}

عند تشغيل خادم Apollo في وضع التطوير، تأخذنا الصفحة http://localhost:4000 إلى GraphOS Studio Explorer. وهذا مفيد جداً للمطوّر، ويمكن استخدامه لإجراء استعلامات على الخادم.

لنجربه:

استعلام مثال في Apollo Studio مع استجابة allPersons

على الجانب الأيسر، يعرض Explorer توثيق الـ API الذي أنشأه تلقائياً بناءً على المخطط.

إبراز صيغة المخطط في VS Code

يُعرَّف المخطط في شيفرتنا باستخدام صيغة القالب النصي (template literal):

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

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

يحتوي المخطط على معلومات بنيوية، لكن في محرر الشيفرة يظهر المحتوى كله باللون نفسه، ولا تستطيع أدوات التنسيق التلقائي مثل Prettier تنسيق محتوياته. يمكننا تفعيل إبراز صيغة مخطط GraphQL، والإكمال التلقائي مثلاً، في VS Code بتثبيت إضافة GraphQL: Language Feature Support.

نحتاج إلى إشارة ما للإضافة تدل على أن typeDefs يحتوي على GraphQL. وهناك عدة طرق للقيام بذلك. سنفعلها الآن بإضافة التعليق الدال على النوع /* GraphQL */ قبل نص القالب:

يستخدم VS Code إبراز الصيغة لمخطط GraphQL عند إضافة التعليق /* GraphQL */ قبل نص القالب

الآن يعمل إبراز الصيغة. يساعد التعليق الإضافة المثبَّتة على التعرف على النص كـ GraphQL وتقديم ميزات ذكية للمحرر، لكنه لا يؤثر على وقت تشغيل التطبيق. ويمكن لـ Prettier الآن تنسيق المخطط أيضاً.

معاملات الـ resolver

الاستعلام الذي يجلب شخصاً واحداً

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

له resolver يختلف عن السابق لأنه يُعطى معاملين:

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

المعامل الثاني، args، يحتوي على معاملات الاستعلام. ثم يعيد الـ resolver من المصفوفة persons الشخص الذي يتطابق اسمه مع قيمة args.name. ولا يحتاج الـ resolver إلى المعامل الأول root.

في الواقع، تُعطى جميع دوال الـ resolver أربعة معاملات. وفي JavaScript، لا يلزم تعريف المعاملات إذا لم تكن مطلوبة. وسنستخدم المعامل الأول والثالث للـ resolver لاحقاً في هذا الجزء.

الـ resolver الافتراضي

عندما نجري استعلاماً، على سبيل المثال

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

يعرف الخادم كيف يعيد بالضبط الحقول التي يتطلبها الاستعلام. كيف يحدث ذلك؟

يجب أن يعرّف خادم GraphQL resolvers لـكل حقل من كل نوع في المخطط. وقد عرّفنا حتى الآن resolvers فقط لحقول النوع Query، أي لكل استعلام في التطبيق.

ولأننا لم نعرّف resolvers لحقول النوع Person، فقد عرّف Apollo resolvers افتراضية لها. وهي تعمل مثل المبيّن أدناه:

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
  }

}

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

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

يمكننا مثلاً تعريف أن عنوان جميع الأشخاص هو Manhattan New York بكتابة ما يلي بشكل ثابت في resolvers حقلي 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، يحتوي على الشارع والمدينة.

ولأن الكائنات المحفوظة في المصفوفة لا تحتوي على حقل address، لا يكفي الـ resolver الافتراضي. لنضف resolver لحقل 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 و phone و id باستخدام resolvers الافتراضية، أما الحقل address فيُكوَّن باستخدام resolver معرَّف ذاتياً. المعامل root في دالة الـ resolver هو كائن الشخص، لذا يمكن أخذ الشارع والمدينة في العنوان من حقوله.

الاستعلامات التي تتطلب العنوان تصبح

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

والاستجابة الآن كائن شخص يحتوي على كائن عنوان.

{
  "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 الموصوفة في المخطط.

وخلافاً للنوع Person، لا يمتلك النوع Address حقلاً id، لأنها لا تُحفظ في بنية بيانات منفصلة خاصة بها في الخادم.

لنعدّل الـ resolver الخاص بحقل address بحيث يفكّك الحقول المطلوبة من المعامل الذي يستقبله:

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

يمكن العثور على الشيفرة الحالية للتطبيق على Github، في الفرع part8-1.

Mutations

لنضف وظيفة لإضافة أشخاص جدد إلى دليل الهاتف. في GraphQL، تُنفَّذ جميع العمليات التي تُحدث تغييراً عبر mutations. وتُوصف الـ mutations في المخطط كمفاتيح من النوع Mutation.

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

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

تُعطى الـ Mutation تفاصيل الشخص كمعاملات. والمعامل phone هو الوحيد القابل للقيمة الفارغة. وللـ Mutation أيضاً قيمة مُعادة. القيمة المُعادة من النوع Person، والفكرة أن تُعاد تفاصيل الشخص المضاف إذا نجحت العملية، وإلا فـ null. ولا تُعطى قيمة للحقل id كمعامل. فمن الأفضل ترك توليد المعرّف للخادم.

تتطلب الـ mutations أيضاً resolver:

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

// ...

const resolvers = {
  Query: {
    // ...
  },
  Person: {
    // ...
  },

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

}

// ...

تضيف الـ mutation الكائن المُعطى لها كمعامل args إلى المصفوفة persons، وتعيد الكائن الذي أضافته إلى المصفوفة.

يُعطى الحقل id قيمة فريدة باستخدام مكتبة uuid.

يمكن إضافة شخص جديد باستخدام الـ mutation التالية

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"
}

لكن الاستجابة للـ mutation هي

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

إذن يقوم resolver الحقل address من النوع Person بتنسيق كائن الاستجابة إلى الشكل الصحيح.

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

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

يعرض Apollo خطأً مع addPerson GRAPHQL VALIDATION FAILED

إذن يمكن تنفيذ بعض معالجة الأخطاء تلقائياً عبر التحقق في GraphQL.

لكن GraphQL لا يستطيع التعامل مع كل شيء تلقائياً. على سبيل المثال، يجب إضافة قواعد أكثر صرامة للبيانات المُرسلة إلى Mutation يدوياً. ويمكن التعامل مع خطأ برمي GraphQLError مع رمز خطأ مناسب.

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

const { GraphQLError } = require('graphql')

// ...

const resolvers = {
  // ..
  Mutation: {
    addPerson: (root, args) => {

      if (persons.find(p => p.name === args.name)) {
        throw new GraphQLError(`Name must be unique: ${args.name}`, {
          extensions: {
            code: 'BAD_USER_INPUT',
            invalidArgs: args.name
          }
        })
      }


      const person = { ...args, id: uuid() }
      persons = persons.concat(person)
      return person
    }
  }
}

إذن إذا كان الاسم المراد إضافته موجوداً بالفعل في دليل الهاتف، فارمِ خطأ GraphQLError.

يعرض Apollo خطأ BAD_USER_INPUT

يمكن العثور على الشيفرة الحالية للتطبيق على GitHub، في الفرع part8-2.

Enum

لنضف إمكانية ترشيح الاستعلام الذي يعيد جميع الأشخاص بالمعامل 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 هو enum في GraphQL، أو نوع قابل للتعداد، بقيمتين محتملتين: YES أو NO. في الاستعلام allPersons، يمتلك المعامل phone النوع YesNo، لكنه قابل للقيمة الفارغة.

يتغير الـ resolver كالتالي:

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)
},

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

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

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

  editNumber(
    name: String!
    phone: String!
  ): Person

}

وتُنفَّذ عبر resolver:

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
  }
}

تجد الـ mutation الشخص المراد تحديثه عبر الحقل name.

يمكن العثور على الشيفرة الحالية للتطبيق على Github، في الفرع part8-3.

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

مع GraphQL، يمكن دمج عدة حقول من النوع Query، أو «استعلامات منفصلة»، في استعلام واحد. على سبيل المثال، يعيد الاستعلام التالي كلاًّ من عدد الأشخاص في دليل الهاتف وأسمائهم:

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"
      }
    ]
  }
}

في بعض الحالات، قد يكون من المفيد تسمية الاستعلامات. ويحدث ذلك خاصة عندما تحتوي الاستعلامات أو الـ mutations على معاملات. وسنتناول المعاملات قريباً.

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

2. كل الكتب

3. كل المؤلفين

4. كتب مؤلف

5. الكتب حسب النوع الأدبي

6. إضافة كتاب

7. تحديث سنة ميلاد مؤلف