Как работают мутации и подписки в GraphQL
Development | Комментировать запись
В этой статье мы рассмотрим типы 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: GraphQL