Mutations et abonnements dans GraphQL
Dans cet article, nous examinerons l'utilisation des types Mutation
et Subscription
pour manipuler et surveiller nos données pour les modifications, au lieu de simplement interroger, dans GraphQL. N'hésitez pas à en découvrir plus dans les docs officiels.
Pour simplifier les choses, nous n'utiliserons aucune base de données ou requête HTTP, mais il est nécessaire de savoir comment configurer une API de base avec schémas et résolveurs.
Installation
Nous utiliserons la bibliothèque graphql-yoga pour configurer notre serveur et nodemon
pour qu'il se recharge automatiquement. Nous aurons également besoin d'un pré-processeur comme Prepros ou babel afin de pouvoir utiliser les dernières fonctionnalités de JavaScript.
$ npm i graphql-yoga nodemon
Configuration standard
Outre la configuration de notre serveur, nous avons juste un tableau users
vide et un schéma et un résolveur simples pour renvoyer tous nos utilisateurs.
serveur.js
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'));
Nous aurons besoin d'un script start
qui exécutera nodemon sur notre fichier de sortie :
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" }
Maintenant, dans le terminal, vous pouvez simplement exécuter npm run start
.
À localhost:4000
, nous devrions avoir GraphQL Playground opérationnel avec une requête pour user { name }
renvoyant notre tableau vide.
Créer une mutation
La syntaxe de nos mutations est presque la même que celle de notre requête. Nous avons juste besoin de déclarer les options que nous voulons, d'ajouter des arguments (le cas échéant) et de déclarer quel type doit être renvoyé une fois terminé.
Au lieu d'ajouter tous les arguments en ligne, il est assez courant de diviser les données en son propre type spécial appelé type input
pour des raisons d'organisation. C'est une convention de dénomination générale que vous verrez dans des outils comme Prisma pour nommer l'entrée quelle que soit la fin du résolveur par le mot input, donc addUser
obtient une entrée AddUserInput
.
serveur.js
const typeDefs = ` type Mutation { addUser(data: AddUserInput): User! } input AddUserInput { name: String!, age: Int! } `;
Tout comme avec les requêtes, nous pouvons accéder aux arguments sur args
et ajouter notre nouvel utilisateur à notre tableau, et les renvoyer.
const resolvers = { Query: {...}, Mutation: { addUser(parent, args, ctx, info) { const user = { ...args.data }; users.push(user); return user; } } }
Supprimer et mettre à jour les mutations
La syntaxe étant si simple, il est presque facile d'étoffer les autres opérations CRUD.
Nous saurons quel élément nous supprimons ou mettons à jour simplement en recherchant l'utilisateur par son nom.
serveur.js
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; } } }
Maintenant, à localhost:4000
, vous pouvez essayer cette mutation et interroger à nouveau notre tableau.
mutation { addUser(data: { name: "Alli", age: 48 }) { name age } }
Ou dans un autre onglet :
mutation { updateUser(name: "Alli", data: { name: "Crusher", age: 27 }) { name age } }
Abonnements
Nous pouvons utiliser le type spécial Subscription
pour nous permettre de surveiller toute modification de nos données. La syntaxe est très similaire à celle des requêtes et des mutations, ajoutez simplement le type Subscription
, ajoutez ce que vous voulez qu'il regarde et ce que vous voulez renvoyer. Nous allons renvoyer un type personnalisé qui nous renverra à la fois nos données modifiées et nous dira s'il s'agissait d'une opération de création, de suppression ou de mise à jour.
Pour utiliser les abonnements, nous devrons utiliser PubSub
de graphql-yoga et l'initialiser avant tout le reste. Dans notre résolveur d'abonnement, nous utiliserons une fonction appelée subscribe
qui devra renvoyer un événement asynchrone, que nous nommerons user
. Chaque fois que nous voulons connecter quelque chose à cet abonnement, nous utiliserons ce nom d'événement.
serveur.js
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'); } } } }
Notre abonnement lui-même est configuré et disponible dans les documents GraphQL générés, mais il ne sait pas quand se déclencher ni quoi renvoyer. De retour dans nos mutations, nous ajouterons pubsub.publish
pour établir un lien avec notre événement user
et retransmettre nos données.
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: { ... } };
Sur localhost:4000
, nous pouvons ouvrir un nouvel onglet et exécuter l'abonnement suivant. Vous devriez voir un message « écoute… » avec une petite roue qui tourne. Dans un autre onglet, nous pouvons maintenant exécuter n'importe laquelle de nos autres mutations passées et notre abonnement renverra automatiquement ce qui a été fait et ce qui a été modifié.
subscription { user { mutation data { name age } } }
Conclusion
J'espère que cela a été utile pour comprendre comment configurer vos API GraphQL avec des mutations et des abonnements. S'il y a eu des problèmes lors de la configuration, vous pouvez toujours consulter ce référentiel.