# GraphQL Subscriptions with Graphql-Yoga

# Goals and Introduction

See the tutorial GraphQL subscriptions with Node.js and Express (opens new window) at https://blog.logrocket.com/graphql-subscriptions-nodejs-express (opens new window)

We’ll use predefined post data stored inside a JSON file postdata.js (opens new window) to perform the following operations:

  • getPosts: read all posts
  • getPost: read a specific post by ID
  • updatePost: update a post and triggers an event to channel post
  • deletePost: delete a post and triggers an event to channel post
  • createPost: create a post and triggers an event to channel post

In addition to the normal schema definitions for queries and mutations, we have a type called Subscription that is added on the post object via SubscriptionPayload, a custom type.

Post objects have the following structure:

    type Post{
        id:ID
        title:String
        subtitle:String
        body:String
        published:Boolean
        author: String
        upvotes: Int
        downvotes: Int
        commentCount: Int
    }
1
2
3
4
5
6
7
8
9
10
11

Each time a change is made to a post object, an event will be triggered that returns the name of the mutation performed , either update, delete, or create a post.

# App structure

Here is the structure of the app:

✗ tree -I node_modules
.
├── README.md   # Write your report here
├── index.js    # Starts the GraphQL server
├── package-lock.json
├── package.json
├── postData.js  # Exports the json. Mimicks the database
├── resolvers.js # Has the logic to resolve for all queries, mutations, and subscriptions 
└── typeDefs.gql # the GraphQL Schema
1
2
3
4
5
6
7
8
9

# GraphQL Yoga

We’ll use graphql-yoga (opens new window). You can follow this tutorial (opens new window) to learn more about graphql-yoga.

✗ jq '.dependencies' package.json 
{
  "graphql-yoga": "^1.18.3"
}
1
2
3
4

GraphQL Yoga uses server-sent-events[1] for the subscription protocol.

Server-Sent Events (SSE) is a server push (opens new window) technology enabling a client to receive automatic updates from a server via an HTTP connection, and describes how servers can initiate data transmission towards clients once an initial client connection has been established.

/images/server-sent-events.png

SSE are commonly used to send message updates or continuous data streams to a browser client and designed to enhance native, cross-browser streaming through a JavaScript API called EventSource[2], through which a client requests a particular URL in order to receive an event stream. The EventSource API is standardized as part of HTML5. The mime type for SSE is text/event-stream.

# Resolvers Structure

Notice how resolvers (opens new window) is an object with three properties Query, Mutation, and Subscription whose values are objects.

const resolvers = {
    Query: {
        getPosts() { ... },
        getPost(parent, args) { ... }
    },
    Mutation: {
        createPost(parent, args, { pubsub }) { ...  },
        updatePost(parent, args, { pubsub }) { ... },
        deletePost(parent, args, { pubsub }) { ... },
    },

    Subscription: {
        post: { ... }
    },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Each entry in the Query object corresponds to the function resolver of the corresponding query.

# Queries

We’ll define two queries, getPost and getPosts. Here is the schema:

  type Query {
        getPosts: [Post!]!
        getPost(query: String!): [Post]
    }
1
2
3
4

and here are the resolvers:

const resolvers = {
    Query: {
        getPosts() {
          return posts;
        },
        getPost(parent, args) {
                return posts.filter((post) => {
                const inBody = post.body.toLowerCase().includes(args.query.toLowerCase())
                const inTitle = post.title.toLowerCase().includes(args.query.toLowerCase())
                return inBody || inTitle;
            });
        }
    },
    Mutation: {
        createPost(parent, args, { pubsub }) { ... },
        updatePost(parent, args, { pubsub }) { ... },
        deletePost(parent, args, { pubsub }) { ... },
    },

    Subscription: {
        post: { ... }
    },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Testing the queries

This is an example of a query to get a post:

query {
  getPost(query: "recusandae") {
    id
    title
    body
    author
    published
  }
}
1
2
3
4
5
6
7
8
9

The answer is:

{
  "data": {
    "getPost": [
      {
        "id": "7",
        "title": "Et sunt in error et recusandae ut animi ut.",
        "body": "magni adipisci voluptatibus",
        "author": "Anabelle Sipes",
        "published": true
      }
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# Mutations

# Schema

This is the schema for the mutation createPost to create a Post:

    type Post{
        id:ID
        title:String
        subtitle:String
        body:String
        published:Boolean
        author: String
        upvotes: Int
        downvotes: Int
        commentCount: Int
    }

    type Mutation{
        createPost(
          id:ID!
          title:String
          subtitle:String
          body:String
          published:Boolean
          author: String!
          upvotes: Int
          downvotes: Int
          commentCount: Int
        ): Post!

        updatePost( ... ): Post!
        deletePost(id: ID!): Post!
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# Resolvers

Here is the correspondent resolver for createPost:










 
 
 
 
 
 









    Mutation: {
        createPost(parent, args, { pubsub }) {
            const id = parseInt(args.id, 10);
            const postIndex = posts.findIndex((post) => post.id === id);
            if (postIndex === -1) {
                posts.push({
                    ...args
                });

                pubsub.publish('post', {
                    post: {
                        mutation: 'CREATED',
                        data: { ...args }
                    }
                });

                return { ...args };
            };
            throw new Error('Post with same id already exist!');
        },
        updatePost(parent, args, { pubsub }) { ... },
        deletePost(parent, args, { pubsub }) { ... },
    },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

Once the post is created, we’ll publish an event with the field mutation set to CREATED to all subscribers of the post channel with the newly created post in the field data.

pubsub.publish('post', {
    post: {
        mutation: 'CREATED',
        data: { ...args }
    }
});
1
2
3
4
5
6

Notice how we have got the pubsub object from the context.

createPost(parent, args, { pubsub }) { ... }
1

# The pubsub Object

The pubsub object was created in the index.js file:






 
 
 


const { GraphQLServer, PubSub } = require('graphql-yoga');
const pubsub = new PubSub()
const server  = new GraphQLServer({
  typeDefs,
  resolvers,
  context:{
    pubsub
  }
})
1
2
3
4
5
6
7
8
9

Observe how is passed inside the context argument of the GraphQLServer constructor:

# Testing the mutations

Let us test the createPost mutation. We'll send first an Invalid example:

mutation withError{
  createPost(
    id: 3, title: "hello", body: "world", 
    author: "Casiano") {
    id
    title
    body
    author
  }
}
1
2
3
4
5
6
7
8
9
10

the answer is:

{
  "data": null,
  "errors": [
    {
      "message": "Post with same id already exist!",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "createPost"
      ]
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Notice how data is null and errors is an array of errors.

Here is a second test, containing a valid mutation

mutation ok {
  createPost(
    id: 11, title: "hello", body: "world", 
    author: "Casiano") {
    id
    title
    body
    author
  }
}
1
2
3
4
5
6
7
8
9
10

The answer is:

{
  "data": {
    "createPost": {
      "id": "11",
      "title": "hello",
      "body": "world",
      "author": "Casiano"
    }
  }
}
1
2
3
4
5
6
7
8
9
10

The code for the other mutations is similar to the one for createPost.

# Subscriptions

Here is the schema for the subscription:

enum Crud {
  CREATE
  UPDATE
  DELETE
}

type Subscription {
  post: SubscriptionPayload
}

type SubscriptionPayload {
  mutation: Crud
  data: Post
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

The objects returned by the subscription will be of type SubscriptionPayload. The SubscriptionPayload object has two fields: mutation and data.

  • The mutation field will contain the type of mutation that triggered the subscription, in the case of a createPost is CREATED.
  • The data field will contain the data of the newly created post.

The resolver for post uses thepubsub.asyncIterator method to map the event underlying the source stream to a returned response stream. The asyncIterator takes the channel name through which the event across the app will be mapped out:

    Subscription: {
        post: {
            subscribe(parent, args, { pubsub }) {
                return pubsub.asyncIterator('post');
            }
        }
    }
1
2
3
4
5
6
7

Subscription resolvers are wrapped inside an object and need to be provided as the value for a subscribe field. You also need to provide another field called resolve that actually returns the data from the data emitted by the AsyncIterator.

# Testing the subscription

Here follow two captures of the subscription in action. The first one is the mutation,

the second one shows the notification received by the subscription:

# Exercise

Update the example to use the latest version of GraphQL-yoga (v3). See Migration from Yoga V1 (opens new window)

# References

# FootNotes


  1. See Server Sent Events (opens new window) ↩︎

  2. The EventSource interface (opens new window) is web content's interface to server-sent events. An EventSource instance opens a persistent connection to an HTTP server, which sends events in text/event-stream format. The connection remains open until closed by calling EventSource.10 oct ↩︎

Grading Rubric#

Comments#

Last Updated: 2 months ago