Implementing the Relay spec in a GraphQL server

I’ve seen some confusion on Relay and GraphQL lately. GraphQL is so often used with Relay that I think sometimes we forget what a GraphQL server is, and what Relay adds on top of it. The goal of the next 3 posts is to try to clearly see the line between the two, and how to implement the Relay part in an existing GraphQL Server.

Relay

From Facebook’s words Relay is simply a

Framework for building data-driven React applications.

It is a wrapper around Relay components, enabling them to live next to the GraphQL queries they need to fulfil their data requirements. On the client side, Relay’s strength is it’s amazing client side cache. On the server side, Relay expects the GraphQL server to implement a certain spec for things to work perfectly.

GraphQL & Relay

Let’s take a look at what a Relay app needs from a GraphQL server that is different from usual. First, the Global Object Identification. Relay introduces the need for objects with unique identifiers, called “nodes”. With that concept, Relay can refetch arbitrary objects from a GraphQL server using their GID. On the GraphQL server, each object must have an id field returning that GID, and the root query type must have a node field, used to query nodes globally.

Next, Connections. Connections are an Object that provide a standard way to slice and paginate results. Connections can provide PageInfo, a way of telling the client if there are more results to fetch still. The Connection object provides a cursor for every item in it’s result set.

The last thing that Relay adds to a GraphQL server is the Input Object Mutation. Compared to regular GraphQL mutations, Relay mutations are similar, but try to standardize the way they are exposed and called. Every mutation takes 1 argument, an Input Object. This input object must always contain a clientMutationId, and the GraphQL server must always return it in the response.

So as you see, 3 things are added on top of a vanila GraphQL server:

  • Global Object Identification
  • Connections
  • Input Object Mutations

Implementing the spec

Each of these require quite a bit of work to implement in a GraphQL server. I’ve done the exercise myself, and will share what I’ve done to implement the Relay spec in a Ruby on Rails GraphQL server, using the GraphQL gem as a base. I will divide the results in 3 blog posts, starting with this one. Let’s start by the Global Object Indentification.

Global Object Indentification

Let’s start by making our current Object Types compatible with Relay. If you remember from above, each object had to have an idfield. GraphQL has support for interface objects, this is a great use case for what we want to do. When using interfaces in the GraphQL gem, the object implementing it “inherits” from it’s field.

So the first step is to create that interface type, let’s call it Node.

# lib/graph/relay/node.rb
module Graph
  module Relay
    Node = GraphQL::InterfaceType.define do
      name "Node"
      field :id, !types.ID, property: :to_global_id

      resolve_type -> (object) do
        Graph::Relay::Node.possible_types.detect do |type|
          object.is_a?(type.model)
        end
      end
    end
  end
end

This should not look too unfamiliar to you. We’re simply defining an Interface type using the GraphQL gem. An interface must be able to resolve the type of an object. The code is pretty straight forward, we look through all of the possible_types and find one that matches our object using type.model.

Our field here, will simply resolve by calling object.to_global_id. We need Global Identification, so I used something that Rails has by default: GlobalID. Calling object.to_global_id transforms your ActiveRecord into a GlobalID oject, a unique identifier for that particular record.

=> User.find(1).to_global_id
=> #<GlobalID:0x007f9abca6ecb8 @uri=#<URI::GID gid://app-name/User/1>>

You might’ve noticed that type.model is not an attribute that types have using the GraphQL gem. Since we’re using rails, I’ve augmented the normal GraphQL object type with my own attributes, like this:

# lib/graph/active_record_object_type.rb
module Graph
  class ActiveRecordObjectType < GraphQL::ObjectType
    attr_accessor :model
    accepts_definitions(:model)
  end
end

accepts_definitions is a new addition to the GraphQL gem. It basically lets you add custom fields to an ObjectType, while still being able to use the DSL GraphQL-ruby provides.

With that being done, we can simply implement this interface in our existing fields, just like this:

# app/graph/user_type.rb
UserType = Graph::ActiveRecordObjectType.define do
  name "User"
  description "A User"
  interfaces [Graph::Relay::Node]
  model User

  field :email, !types.String, property: :email
end

The two lines that interest us here are model User which will indicate which ActiveRecord model our type is refering to, and interfaces [Graph::Relay::Node], which does exactly what we wanted to do!

There’s now only one thing left to do. Exposing a node field on the query root. We can do this just like defining any other field:

  field :node do
    type Graph::Relay::Node
    argument :id, !types.ID

    resolve -> (object, args, _) do
      gid = GlobalID.parse(args["id"])
      GlobalID::Locator.locate(gid)
    end
  end

Our field accepts one argument, id. And uses GlobalID::Locator to get the corresponding object, pretty simple!

Lets test this!

First, let’s see if the object we query have the id field available:

query getObjectId { viewer { currentUser { email } } }

screenshot 2016-03-27 19 07 34

Then, we should be able to query any object using the node field:

query getObjectById { node(id: "gid://review-buddy/User/1") { ... on User { email } } }

screenshot 2016-03-27 19 12 10

Next week I’ll have a post up about implementing the second thing Relay needs from a GraphQL server: Connections

As always you can find me on Twitter @__xuorig__ or Github

Go back to Recent Posts ✍️


READ THIS NEXT:

Relay's new applyUpdate function

I recently contributed to Relay and helped implementing a new function on the Relay Store, applyUpdate. It was recently merged into master and the standard update method was deprecated in v0.6....