GraphQL 3- Simple subscription and check when data gets changed


In the last entry, I went over the basics of mutations in GraphQL- create, update, or delete data (GraphQL 2 — Mutation with Basic GraphQL). When these updates happen, the client side should also reflect these changes. To do so, there needs to be a system to notify the client side.

Subscription is a GraphQL feature that allows a server to send data to its clients when these updates happen. As the client side initiates the subscription, the server maintains a connection to its subscribed client, and it returns the updated information as a mutation takes place.

An overall system diagram of GraphQL subscriptions with Redis. (Image source: GraphQL subscriptions with Redis Pub Sub)

Basic subscription for when data is created

In this entry, I will setup a subscription that listens to any changes to the list of movies created from the last entry. To set it up, the new Type Definition for subscription has to be created.

Create a custom type, Subscription

Just like the Type Definitions for query and mutations from the previous entry, this operation should return the Movieinstance just as shown below. This is pretty much it for the setup for the Type Definition.

shema.graphql

type Subscription {
movie: Movie!
}

Create a Subscription resolver that listens to updates

The next step is to prepare a new Subscription resolver, which is essentially a function that runs in the new Subscription.js file. The basis for the resolver is similar to the one used for mutations. The new resolver file has to be imported into the index.js, and added to the resolvers object of the server instance.

Subscription.js
const Subscription = {
movie: {
subscribe(parent, args, ctx, info){

}
}
}
export { Subscription as default }

While in the index.js, there is an extremely important library, PubSub (publish-subscription), that needs to be imported for the subscription. The publish-subscription system is one of the asynchronous communication models where a publisher sends messages NOT by identifying a specific subscriber, but by categorizing the messages into classes. To prepare, we need to import the PubSub package in index.js, and make the instance accessible from all resolvers, hence add it to the context object.

index.js
import { GraphQLServer, PubSub } from 'graphql-yoga'
import Subscription from './resolvers/Subscription'
const pubsub = new PubSub()
const server = new GraphQLServer({

...

resolvers: {
      ...
      Subscription,
      ...
   },
   context: {
db,
pubsub
}
})

Going back to the Subscription resolver file, we need to add one crucial method to the Subscription instance, which is to initiate listening to the server. PubSub library provides the light-weight asyncIterator. The asyncIterator is not for clients to push anything, but instead for pulling items. The setup is simple; we just need to return pubsub.asyncIterator with a channel name, in this case “movie”.

Subscription.js
const Subscription = {
movie: {
subscribe(parent, args, { pubsub }, info){
return pubsub.asyncIterator("movie")
}
}
}
export { Subscription as default }

Use PubSub to trigger the message to subscribers – Publish

As a movie is created in the mutation, it should trigger the subscriber to pull this information. To do that, we need to add a line in the mutation instance, createMovie. In this createMovie method, we add the pubsub.publish method with the proper channel name “movie” and the return value, in this case the movie instance.

createMovie(parent, { data }, { db, pubsub }, info) {
   ...
   db.movies.push(movie)
pubsub.publish("movie", { movie })
return movie
}

Now it’s ready to initiate a subscription, and it should respond as if a new movie has been created.

Subscriptions for when data is updated or deleted.

Add a mutation message to the Create operation

Steps similar to those that we took for a create mutation can be applied for an update or a delete mutation. Before moving forward, we have to consider communicating back to the client in which mutation took place in the subscription method. In the createMovie example, the operation simply returns a Movie instance, but the client side would not know if the instance was sent because it was createdupdated, or deleted. In this section, we will add a message that tells whether the operation was createupdate, or delete into the subscription model.

The first step is to add this message into the createMovie operation that we just created. To do so, we need to add a new message into the type definition that we created. In order to organize it better, we will have this new message and the Movie instance that gets returned outside of the Subscription type. For this, we will create a new type, MovieSubscriptionPayload to store just as shown below. Adding the suffix, SubscriptionPayload, is the common naming convention used in GraphQL.

shema.graphql
type Subscription {

   movie: MovieSubscriptionPayload!

}

type MovieSubscriptionPayload {

   mutation: String!
   data: Movie!

}

This added mutation message will also be reflected to the pubsub.publish operation in the Mutation.js file. In this case, the message ‘CREATED’ will be sent as the Create operation runs. This is pretty much it for the changes needed for the createMovie operation.

pubsub.publish("movie", {
   
   movie: {

      mutation: 'CREATED',
      data: movie

   }

})

Extend the method to Delete and Update operations

Essentially the same operations will happen for Delete and Update as well.

deleteMovie(parent, { id }, { db, pubsub }, info){

   ...

   const [deletedMovie] = db.movies.splice(movieIndex, 1)

   ...

   pubsub.publish("movie", {

      movie: {
         mutation: 'DELETED',
         data: deletedMovie
      }

   })

   return deletedMovie

}

For updates, there are three possible conditions that may affect the client side differently.

1) The movie was released originally but it changes to false, in other words, it becomes unreleased.

2) The movie was not released originally, but it is switched to true, or released.

3) There was no released boolean passed, which means that the movie data simply got updated without any changes to release conditions.

The first and second cases could influence how the list might be shown on the client side. For instance, if a movie on a list got unreleased, this movie would be removed from view. Therefore, mutation messages for the first and second cases would be DELETED and CREATED respectively, rather than UPDATED, to clarify different cases to the client side.

updateMovie(parent, { id, data }, { db }, info){

   ...

   // Update the movie release, if the new released is not null

   if(typeof data.released === 'boolean'){
       
       movie.released = data.released
    
       if(originalMovie.released && !movie.released){

           // The movie was just unreleased
           pubsub.publish("movie", {
              movie: {
                 mutation: 'DELETED',
                 data: movie
              }
           })

        } else if (!originalMovie.released && movie.released ){

           // The movie was just released
           pubsub.publish("movie", {
              movie: {
                  mutation: 'CREATED',
                  data: movie
               }
           })

        }) else if ( movie.released ) {

           // No new boolean was passed, so only other information was updated.
           pubsub.publish("movie", {
              movie: {
                 mutation: 'UPDATED',
                    data: movie
                 }
              })
           }
           
           ...
        
       }
   }

})
   }
})

Source codes

Some of main files are listed below. The full project is hosted here.

index.js

schema.graphql

Subscription.js

Mutation.js

Follow on Medium: https://medium.com/@takuma.kakehi/graphql-3-simple-subscription-and-check-when-data-gets-changed-2680fcad484c

Reference

The Modern GraphQL Bootcamp (with Node.js and Apollo) | Udemy

Execution | GraphQL

GraphQL subscriptions with Redis Pub Sub | Apollo

GraphQL: Query vs Mutation | GitHub Training & Guides

Leave a comment

Your email address will not be published. Required fields are marked *