В этой статье мы рассмотрим типы 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. Если при настройке возникли какие-либо проблемы, вы всегда можете обратиться к этому репозиторию.
