Helpers for using graphql
with Relay.
gem "graphql-relay"
bundle install
graphql-relay
provides several helpers for making a Relay-compliant GraphQL endpoint in Ruby:
- global ids support Relay's UUID-based refetching
- connections implement Relay's pagination
- mutations allow Relay to mutate your system predictably
Global ids (or UUIDs) provide refetching & global identification for Relay.
Use GraphQL::Relay::GlobalNodeIdentification
helper by defining object_from_id(global_id, ctx)
& type_from_object(object)
. The resulting NodeIdentification
object is in your schema and internally by GraphQL::Relay
.
NodeIdentification = GraphQL::Relay::GlobalNodeIdentification.define do
# Given a UUID & the query context,
# return the corresponding application object
object_from_id -> (id, ctx) do
type_name, id = NodeIdentification.from_global_id(id)
# "Post" -> Post.find(id)
Object.const_get(type_name).find(id)
end
# Given an application object,
# return a GraphQL ObjectType to expose that object
type_from_object -> (object) do
if object.is_a?(Post)
PostType
else
CommentType
end
end
end
ObjectTypes in your schema should implement NodeIdentification.interface
with the global_id_field
helper, for example:
PostType = GraphQL::ObjectType.define do
name "Post"
interfaces [NodeIdentification.interface]
# `id` exposes the UUID
global_id_field :id
# ...
end
You should also add a field to your root query type for Relay to re-fetch objects:
QueryType = GraphQL::ObjectType.define do
name "Query"
# Used by Relay to lookup objects by UUID:
field :node, field: NodeIdentification.field
# ...
end
Connections provide pagination and pageInfo
for Array
s or ActiveRecord::Relation
s.
To define a connection field, use the connection
helper. For a return type, get a type's .connection_type
. For example:
PostType = GraphQL::ObjectType.define do
# `comments` field returns a CommentsConnection:
connection :comments, CommentType.connection_type
# To avoid circular dependencies, wrap the return type in a proc:
connection :similarPosts, -> { PostType.connection_type }
# ...
end
You can also define custom arguments and a custom resolve function for connections, just like other fields:
connection :featured_comments, CommentType.connection_type do
# Add an argument:
argument :since, types.String
# Return an Array or ActiveRecord::Relation
resolve -> (post, args, ctx) {
comments = post.comments.featured
if args[:since]
comments = comments.where("created_at >= ", since)
end
comments
}
end
You can customize a connection type with .define_connection
:
PostType.define_connection do
field :totalCount do
type types.Int
# `obj` is the Connection, `obj.object` is the collection of Posts
resolve -> (obj, args, ctx) { obj.object.count }
end
end
Now, PostType.connection_type
will include a totalCount
field.
Maybe you need to make a connection object yourself (for example, to return a connection type from a mutation). You can create a connection object like this:
items = ... # your collection objects
args = {} # stub out arguments for this connection object
connection_class = GraphQL::Relay::BaseConnection.connection_for_items(items)
connection_class.new(items, args)
.connection_for_items
will return RelationConnection or ArrayConnection depending on items
, then you can make a new connection
You can define a custom connection class and add it to GraphQL::Relay
.
First, define the custom connection:
class SetConnection < BaseConnection
# derive a cursor from `item`
# (it is used to find next & previous nodes,
# so it should include `order`)
def cursor_from_node(item)
# ...
end
private
# apply `#first` & `#last` to limit results
def paged_nodes
# ...
end
# apply cursor, order, filters, etc
# to get a subset of matching objects
def sliced_nodes
# ...
end
end
Then, register the new connection with GraphQL::Relay::BaseConnection
:
# When exposing a `Set`, use `SetConnection`:
GraphQL::Relay::BaseConnection.register_connection_implementation(Set, SetConnection)
At runtime, GraphQL::Relay
will use SetConnection
to expose Set
s.
If you need lower-level access to Connection fields, you can create them programmatically. Given a GraphQL::Field
which returns a collection of items, you can turn it into a connection field with ConnectionField.create
.
For example, to wrap a field with a connection field:
field = GraphQL::Field.new
# ... define the field
connection_field = GraphQL::Relay::ConnectionField.create(field)
Mutations allow Relay to mutate your system. They conform to a strict API which makes them predictable to the client.
To add mutations to your GraphQL schema, define a mutation type and pass it to your schema:
# Define the mutation type
MutationType = GraphQL::ObjectType.define do
name "Mutation"
# ...
end
# and pass it to the schema
MySchema = GraphQL::Schema.new(
query: QueryType,
mutation: MutationType
)
Like QueryType
, MutationType
is a root of the schema.
Members of MutationType
are mutation fields. For GraphQL in general, mutation fields are identical to query fields except that they have side-effects (which mutate application state, eg, update the database).
For Relay-compliant GraphQL, a mutation field must comply to a strict API. GraphQL::Relay
includes a mutation definition helper (see below) to make it simple.
After defining a mutation (see below), add it to your mutation type:
MutationType = GraphQL::ObjectType.define do
name "Mutation"
# Add the mutation's derived field to the mutation type
field :addComment, field: AddCommentMutation.field
# ...
end
To define a mutation, use GraphQL::Relay::Mutation.define
. Inside the block, you should configure:
name
, which will name the mutation field & derived typesinput_field
s, which will be applied to the derivedInputObjectType
return_field
s, which will be applied to the derivedObjectType
resolve(-> (inputs, ctx) { ... })
, the mutation which will actually happen
For example:
AddCommentMutation = GraphQL::Relay::Mutation.define do
# Used to name derived types:
name "AddComment"
# Accessible from `input` in the resolve function:
input_field :postId, !types.ID
input_field :authorId, !types.ID
input_field :content, !types.String
# The result has access to these fields,
# resolve must return a hash with these keys
return_field :post, PostType
return_field :comment, CommentType
# The resolve proc is where you alter the system state.
resolve -> (inputs, ctx) {
post = Post.find(inputs[:postId])
comment = post.comments.create!(author_id: inputs[:authorId], content: inputs[:content])
{comment: comment, post: post}
}
end
Under the hood, GraphQL creates:
- A field for your schema's
mutation
root - A derived
InputObjectType
for input values - A derived
ObjectType
for return values
The resolve proc:
- Takes
inputs
, which is a hash whose keys are the ones defined byinput_field
- Takes
ctx
, which is the query context you passed with thecontext:
keyword - Must return a hash with keys matching your defined
return_field
s
- Add a
max_page_size
config for connections? - Refactor some RelationConnection issues:
- GraphQL Slack, come join us in the
#ruby
channel! graphql
Ruby gemgraphql-relay-js
JavaScript helpers for GraphQL and Relay