Как работают мутации и подписки в GraphQL

В этой статье мы рассмотрим типы Mutation и Subscription в GraphQL, которые можно использовать вместо обычных запросов для управления и отслеживания данных на предмет изменений.

Узнать больше об этом можно в официальной документации.

Для простоты здесь мы не будем использовать никаких баз данных или HTTP-запросов, но в целом для работы вам необходимо знать, как настроить базовый API со схемами и распознаватели.

Читайте также: Схемы и распознаватели GraphQL

Установка сервера GraphQL

Мы будем использовать библиотеку graphql-yoga для установки сервера и nodemon для автоматической перезагрузки. Также нам понадобится препроцессор, например Prepros и babel, чтобы использовать новейшие функции JavaScript.

$ npm i graphql-yoga nodemon

Стандартная настройка

Откройте файл server.js. Помимо настроек сервера, все, что у нас есть сейчас – это пустой массив users, простая схема и распознаватель для возврата пользователей.

import { GraphQLServer } from 'graphql-yoga'

const users = [];

const typeDefs = `
  type Query {
    users: [User!]!
  }

  type User {
    name: String!
    age: Int!
  }
`;

const resolvers = {
  Query: {
    user() {
      return users;
    }
  }
}

const server = new GraphQLServer({ typeDefs, resolvers });

server.start(() => console.log('server running'));

Нам понадобится стартовый скрипт, который запустит nodemon. Откройте файл package.json и поместите в него:

{
  "name": "graphql-api",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "dependencies": {
    "graphql-yoga": "^1.16.7"
  },
  "devDependencies": {
    "nodemon": "^1.19.1"
  },
  "scripts": {
    "start": "nodemon server-dist.js"
  },
  "author": "",
  "license": "ISC"
}

Теперь можно просто запустить в терминале:

npm run start

После этого на localhost:4000 вы найдете GraphQL Playground, что при запросе user { name } вернет пустой массив.

Создание мутации

Синтаксис мутаций почти такой же, как и запросов. Нужно просто объявить необходимые параметры, добавить любые аргументы (если есть) и объявить, какой тип должен быть возвращен в итоге.

Вместо того чтобы добавлять все аргументы в строку, довольно часто для лучшей организации данные выделяют в отдельный специальный тип, называемый типом ввода. Это общее соглашение об именах, которое встречается в таких инструментах, как Prisma. Такой подход позволяет называть ввод независимо от того, заканчивается распознаватель словом input или нет (поэтому addUser получает ввод AddUserInput). Вернитесь в server.js:

const typeDefs = `
  type Mutation {
    addUser(data: AddUserInput): User!
  }

  input AddUserInput {
    name: String!,
    age: Int!
  }
`;

Как и в случае с запросами, мы можем получить доступ к аргументам в args, добавить нового пользователя в массив и вернуть их.

const resolvers = {
  Query: {...},
  Mutation: {
    addUser(parent, args, ctx, info) {
      const user = { ...args.data };

      users.push(user);
      return user;
    }
  }
}

Мутации удаления и обновления

Благодаря такому простому синтаксису мы можем практически без труда добавить другие операции CRUD.

Чтоб узнать, какой элемент мы удаляем или обновляем, просто выполните поиск пользователя по имени.

const typeDefs = `
  type Mutation {
    deleteUser(name: String!): User!
    updateUser(name: String!, data: UpdateUserInput): User!
  }

  input UpdateUserInput {
    name: String
    age: Int
  }
`

const resolvers = {
  Query: { ... },
  Mutation: {
    deleteUser(parent, args, ctx, info) {
      // We're just finding the index of the user with a matching name,
      // checking if it exists, and removing that section of the array.
      const userIndex = users.findIndex(user => user.name.toLowerCase() === args.name.toLowerCase());
      if (userIndex === -1) throw new Error('User not found');

      const user = users.splice(userIndex, 1);
      return user[0];
    },
    updateUser(parent, args, ctx, info) {
      const user = users.find(user => user.name.toLowerCase() === args.who.toLowerCase());
      if (!user) throw new Error('User not found');

      // This way, only the fields that are passed-in will be changed.
      if (typeof args.data.name === "string") user.name = args.data.name;
      if (typeof args.data.age !== "undefined") user.age = args.data.age;

      return user;
    }
  }
}

Попробуйте снова открыть localhost:4000 и при помощи этой мутации снова запросить наш массив.

mutation {
  addUser(data: {
    name: "Alli",
    age: 48
  }) {
    name
    age
  }
}

Откройте его в другой вкладке:

mutation {
  updateUser(name: "Alli", data: {
    name: "Crusher",
    age: 27
  }) {
    name
    age
  }
}

Подписки

Мы можем использовать специальный тип Subscription (подписку), чтобы отслеживать любые изменения в наших данных. Синтаксис подписки очень похож на синтаксис запросов и мутаций, только здесь нужно указывать тип Subscription, а также все данные, которые нужно отслеживать, и то, что вы хотите вернуть. Давайте попробуем вернуть пользовательский тип, который отправит нам измененные данные и сообщит, какая именно из CRUD-операций имела место.

Прежде всего используем PubSub из graphql-yoga и инициализируем его. В распознавателе подписки мы будем использовать функцию по имени subscribe, которая будет возвращать асинхронное событие (назовем его user). Каждый раз, когда мы хотим подключить что-то к этой подписке, мы будем использовать это имя события.

import { GraphQLServer, PubSub } from 'graphql-yoga';

const pubsub = new PubSub();

const typeDefs = `
type Subscription {
  user: UserSubscription!
}

type UserSubscription {
  mutation: String!
  data: User!
}
`

const resolvers = {
  Query: { ... },
  Mutation: { ... },
  Subscription: {
    user: {
      subscribe() {
        return pubsub.asyncIterator('user');
      }
    }
}
}

Итак, наша подписка настроена и доступна в сгенерированных документах GraphQL, но она не знает, когда активироваться или что возвращать. Вернемся к нашим мутациям и добавим pubsub.publish, чтобы связать с нашим пользовательским событием и передать наши данные обратно.

const resolvers = {
  Query: { ... },
  Mutation: {
    addUser(parent, args, ctx, info) {
      const user = { ...args.data };

      users.push(user);

      // We'll just link it to our user event,
      // and return what type of mutation this is and our new user.
      pubsub.publish("user", {
        user: {
          mutation: "Added",
          data: user
        }
      });

      return user;
    },
    deleteUser(parent, args, ctx, info) {
      const userIndex = users.findIndex(
        user => user.name.toLowerCase() === args.who.toLowerCase()
      );
      if (userIndex === -1) throw new Error("User not found");

      const user = users.splice(userIndex, 1);

      pubsub.publish("user", {
        user: {
          mutation: "Deleted",
          data: user[0]
        }
      });

      return user[0];
    },
    updateUser(parent, args, ctx, info) {
      const user = users.find(
        user => user.name.toLowerCase() === args.who.toLowerCase()
      );
      if (!user) throw new Error("User not found");

      if (typeof args.data.name === "string") user.name = args.data.name;
      if (typeof args.data.age !== "undefined") user.age = args.data.age;

      pubsub.publish("user", {
        user: {
          mutation: "Updated",
          data: user
        }
      });

      return user;
    }
  },
  Subscription: { ... }
};

Откроем localhost:4000 в новой вкладке и запустим подписку. Вы должны увидеть сообщение «listening…» с маленьким вращающимся колесиком загрузки. В другой вкладке мы теперь можем запустить любую из наших предыдущих мутаций, и наша подписка автоматически вернет то, что было сделано и что было изменено.

subscription {
  user {
    mutation
    data {
      name
      age
    }
  }
}

Заключение

В этом материале мы обсудили, как настроить мутации и подписки для API GraphQL. Если при настройке возникли какие-либо проблемы, вы всегда можете обратиться к этому репозиторию.

Tags:

Добавить комментарий