GraphQL Connections

GraphQL is still rough around the edges. This post will try to explain why the previous sentence is a pun.

Types can be connected. For instance, in our dummy database example (see previous post), users may have playlists.

With sql-to-graphql, you can create connection between tables by following a simple naming convention.

For instance, this table:

+---------+-------------+------+-----+---------+----------------+
| Field   | Type        | Null | Key | Default | Extra          |
+---------+-------------+------+-----+---------+----------------+
| id      | int(11)     | NO   | PRI | NULL    | auto_increment |
| title   | varchar(50) | YES  |     | NULL    |                |
| user_id | int(11)     | YES  |     | NULL    |                |
+---------+-------------+------+-----+---------+----------------+

because of the user_id column, will create a link between Playlist and User.

If we populate this new table with those data:

+----+----------------------+---------+
| id | title                | user_id |
+----+----------------------+---------+
|  1 | Zidane's playlist #1 |       1 |
|  2 | Zidane's playlist #2 |       1 |
|  3 | Zidane's playlist #3 |       1 |
+----+----------------------+---------+

we now have this GraphQL schema:

type Playlist {
  id: Int!
  title: String
  userId: Int
  user: User
}

type RootQueryType {
  user(id: Int!): User
  playlist(id: Int!): Playlist
}

type User {
  id: Int!
  firstName: String
  lastName: String
  playlists(limit: Int, offset: Int): [Playlist]
}

Querying GraphQL relations

You can then query this relation in a natural way:

GraphQL query GraphQL response
{
 user(id: 1) {
  lastName
   playlists {
    title
  }
 }
}
{
  "data": {
    "user": {
      "lastName": "Zidane",
      "playlists": [
        {
          "title": "Zidane's playlist #1"
        },
        {
          "title": "Zidane's playlist #2"
        },
        {
          "title": "Zidane's playlist #3"
        }
      ]
    }
  }
}

You can also query the relation in the other direction, starting from the playlist:

GraphQL query GraphQL response
{
 playlist(id: 2) {
  title
   user{
    lastName
   }
 }
}
{
  "data": {
    "playlist": {
      "title": "Zidane's playlist #2",
      "user": {
        "lastName": "Zidane"
      }
    }
  }
}

connections attributes

The schema shows that the User to Playlist relation has some attributes :

playlists(limit: Int, offset: Int): [Playlist]

Those parameters are the underlying GraphQL mechanism to support pagination:

GraphQL query GraphQL response
{
  user(id: 1) {
    lastName
    playlists(limit:2, offset:1) {
      title
    }
  }
}
{
  "data": {
    "user": {
      "lastName": "Zidane",
      "playlists": [
        {
          "title": "Zidane's playlist #2"
        },
        {
          "title": "Zidane's playlist #3"
        }
      ]
    }
  }
}

GraphQL relations – relay mode

When we generate the schema in relay mode, the schema is once again very different:

interface Node {
  id: ID!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type Playlist implements Node {
  id: ID!
  title: String
  userId: Int
  user: User
}

type PlaylistConnection {
  pageInfo: PageInfo!
  edges: [PlaylistEdge]
}

type PlaylistEdge {
  node: Playlist
  cursor: String!
}

type RootQueryType {
  node(id: ID!): Node
}

type User implements Node {
  id: ID!
  firstName: String
  lastName: String
  playlists(before: String, after: String, first: Int, last: Int): PlaylistConnection
}

The relay schema adds 3 new concepts:

A diagram will show the difference between those 2 GraphQL schemas:

GraphQL relations - simple mode (no relay)

without relay

GraphQL relations - relay mode

with relay

GraphQL relaty relation navigation

Using the schema we know how to query this new structure (using the base64 id we computed in the previous post) :

{
  node(id: "VXNlcjox") {
    id
    ... on User {
      firstName
    	lastName
      playlists {
        pageInfo {
          hasNextPage
          hasPreviousPage
          startCursor
          endCursor
        }
        edges {
          cursor
          node {
            title
          }
        }
      }
    }
  }
}

This is the response:

{
  "data": {
    "node": {
      "id": "VXNlcjox",
      "firstName": "Zinedine",
      "lastName": "Zidane",
      "playlists": {
        "pageInfo": {
          "hasNextPage": false,
          "hasPreviousPage": false,
          "startCursor": "Q29ubmVjdGlvbjow",
          "endCursor": "Q29ubmVjdGlvbjoy"
        },
        "edges": [
          {
            "cursor": "Q29ubmVjdGlvbjow",
            "node": {
              "title": "Zidane's playlist #1"
            }
          },
          {
            "cursor": "Q29ubmVjdGlvbjox",
            "node": {
              "title": "Zidane's playlist #2"
            }
          },
          {
            "cursor": "Q29ubmVjdGlvbjoy",
            "node": {
              "title": "Zidane's playlist #3"
            }
          }
        ]
      }
    }
  }
}

Let’s get the cursor out of the way: they appear to also be base64 encoded string and after decoding we have :

encoded Decoded
Q29ubmVjdGlvbjow Connection:0
Q29ubmVjdGlvbjox Connection:1
Q29ubmVjdGlvbjoy Connection:2

We can conclude that the cursor is used for the pagination by keeping track of the ids of the items currently being presented.

Pagination size

The first question is how do we define the pagination strategy, like the size of the page?

The schema shows that attribute to query the relation takes some parameters :

playlists(before: String, after: String, first: Int, last: Int): PlaylistConnection

Documentation for the meaning of those parameters are sparse.

A good description was found in one of the GraphQL issue discussion thread by Lee Byron:

Pagination in GraphQL at Facebook looks something like:

type User {
…
friends(after: String, first: Int, before: String, last: Int): UserConnection
…
}
So here, the friends field on a User accepts a couple arguments. after accepts a value that cursor gave you in a prior query, and first limits how many items are returned. before and last are the same ideas, but for paginating from the back of a list towards the front.

So again, this is just a convention that Facebook uses. GraphQL itself doesn’t know what pagination is, it just enables patterns like these. Underlying code is responsible for actually reading these arguments and applying them in a sensible way.

So, it appears that you either use after and first for a forward navigation or before and last for a backward navigation.

The values of first and last define the number of items you want and after and before are cursors values for the starting point of your navigation.

It is also clear that:

Pagination with sql-to-graphql

When using the implementation of sql-to-graphql, it appears that when you set the value of first, this will define the size of your pagnination window but the values of hasNextPage and hasPreviousPage are not set properly (always set to false).

When calling with:

playlists(first: 1)

we get back:

“edges”: [
  {
    “cursor”: “Q29ubmVjdGlvbjow”,
    “node”: {
      “title”: “Zidane’s playlist #1”
    }
  }
]

As the comment above says, the actual mechanism to handle the pagination is implementation specific but, I was expecting that I could then use the cursor value to continue my pagination:

but:

playlists(first: 1, after: “Q29ubmVjdGlvbjow”)

returns:

“edges”: [
  {
    “cursor”: “Q29ubmVjdGlvbjox”,
    “node”: {
      “title”: “Zidane’s playlist #1”
    }
  }
]

A bit confusing as it seems that we have then 2 cursors for the playlist #1.

From there, the navigation works though:

playlists(first: 1, after: "Q29ubmVjdGlvbjox")

returns:

“edges”: [
  {
    “cursor”: “Q29ubmVjdGlvbjoy”,
    “node”: {
      “title”: “Zidane’s playlist #2”
    }
  }
]

and

playlists(first: 1, after: “Q29ubmVjdGlvbjoy”)

returns the last one:

“edges”: [
  {
    “cursor”: “Q29ubmVjdGlvbjoz”,
    “node”: {
      “title”: “Zidane’s playlist #3”
    }
  }
]

I guess this is an implementation ssue with the first page where the cursor is not properly managed and the value hasPreviousPage is also flawed.

Introduction material

For a good introduction to GraphQL, I suggest the video by Lee Byron at the React Rally conference last month in Salt Lake City :