graphql-relay

Helpers for using graphql
with Relay. Includes support for serving Relay connections from Array
s, ActiveRecord::Relation
s and Sequel::Dataset
s.
API Documentation
Installation
gem "graphql-relay"
bundle install
Usage
graphql-relay
provides several helpers for making a Relay-compliant GraphQL endpoint in Ruby:
Global Ids
Global ids (or UUIDs) provide refetching & global identification for Relay.
UUID Lookup
Use GraphQL::Relay::GlobalNodeIdentification
helper by defining object_from_id(global_id, ctx)
& type_from_object(object)
. Then, assign the result to Schema#node_identification
so that it can be used for query execution.
For example, define a node identification helper:
NodeIdentification = GraphQL::Relay::GlobalNodeIdentification.define do
object_from_id -> (id, ctx) do
type_name, id = NodeIdentification.from_global_id(id)
Object.const_get(type_name).find(id)
end
type_from_object -> (object) do
if object.is_a?(Post)
PostType
else
CommentType
end
end
end
Then assign it to the schema:
MySchema = GraphQL::Schema.new(...)
MySchema.node_identification = NodeIdentification
UUID fields
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]
global_id_field :id
end
node
field (find-by-UUID)
You should also add a field to your root query type for Relay to re-fetch objects:
QueryType = GraphQL::ObjectType.define do
name "Query"
field :node, field: NodeIdentification.field
end
Custom UUID Generation
By default, graphql-relay
uses Base64.strict_encode64
to generate opaque global ids. You can modify this behavior by providing two configurations. They work together to encode and decode ids:
NodeIdentification = GraphQL::Relay::GlobalNodeIdentification.define do
to_global_id -> (type_name, id) {
"#{type_name}/#{id}"
}
from_global_id -> (global_id) {
id_parts = global_id.split("/")
type_name = id_parts[0]
id = id_parts[1]
[type_name, id]
}
end
MySchema.node_identification = NodeIdentification
graphql-relay
will use those procs for interacting with global ids.
Connections
Connections provide pagination and pageInfo
for Array
s, ActiveRecord::Relation
s or Sequel::Dataset
s.
Connection fields
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
connection :comments, CommentType.connection_type
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
name "CommentConnectionWithSince"
argument :since, types.String
resolve -> (post, args, ctx) {
comments = post.comments.featured
if args[:since]
comments = comments.where("created_at >= ", since)
end
comments
}
end
Maximum Page Size
You can limit the number of results with max_page_size:
:
connection :featured_comments, CommentType.connection_type, max_page_size: 50
Connection types
You can customize a connection type with .define_connection
:
PostConnectionWithTotalCountType = PostType.define_connection do
field :totalCount do
type types.Int
resolve -> (obj, args, ctx) { obj.object.count }
end
end
Now, you can use PostConnectionWithTotalCountType
to define a connection with the "totalCount" field:
AuthorType = GraphQL::ObjectType.define do
connection :posts, PostConnectionWithTotalCountType
end
Custom edge types
If you need custom fields on edge
s, you can define an edge type and pass it to a connection:
MembershipSinceEdgeType = BaseType.define_edge do
name "MembershipSinceEdge"
field :memberSince, types.Int, "The date that this person joined this team" do
resolve -> (obj, args, ctx) {
obj
person = obj.parent
team = obj.node
membership = Membership.where(person: person, team: team).first
membership.created_at.to_i
}
end
end
Then, pass the edge type when defining the connection type:
TeamMembershipsConnectionType = TeamType.define_connection(edge_type: MembershipSinceEdgeType) do
name "TeamMembershipsConnection"
end
Now, you can query custom fields on the edge
:
{
me {
teams {
edge {
memberSince
node {
teamName: name
}
}
}
}
}
Custom Edge classes
For more robust custom edges, you can define a custom edge class. It will be obj
in the edge type's resolve function. For example, to define a membership edge:
class MembershipSinceEdge < GraphQL::Relay::Edge
def membership
@membership ||= begin
person = self.parent
team = self.node
Membership.where(person: person, team: team).first
end
end
def member_since
membership.created_at.to_i
end
def leader?
membership.leader?
end
end
Then, hook it up with custom edge type and custom connection type:
MembershipSinceEdgeType = BaseType.define_edge do
name "MembershipSinceEdge"
field :memberSince, types.Int, "The date that this person joined this team", property: :member_since
field :isPrimary, types.Boolean, "Is this person the team leader?". property: :primary?
end
end
TeamMembershipsConnectionType = TeamType.define_connection(
edge_class: MembershipSinceEdge,
edge_type: MembershipSinceEdgeType,
) do
name "TeamMembershipsConnection"
end
Connection objects
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 = [...]
args = {}
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
Custom connections
You can define a custom connection class and add it to GraphQL::Relay
.
First, define the custom connection:
class SetConnection < BaseConnection
def cursor_from_node(item)
end
private
def paged_nodes
end
def sliced_nodes
end
end
Then, register the new connection with GraphQL::Relay::BaseConnection
:
GraphQL::Relay::BaseConnection.register_connection_implementation(Set, SetConnection)
At runtime, GraphQL::Relay
will use SetConnection
to expose Set
s.
Creating connection fields by hand
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
connection_field = GraphQL::Relay::ConnectionField.create(field)
Mutations
Mutations allow Relay to mutate your system. They conform to a strict API which makes them predictable to the client.
Mutation root
To add mutations to your GraphQL schema, define a mutation type and pass it to your schema:
MutationType = GraphQL::ObjectType.define do
name "Mutation"
end
MySchema = GraphQL::Schema.new(
query: QueryType,
mutation: MutationType
)
Like QueryType
, MutationType
is a root of the schema.
Mutation fields
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"
field :addComment, field: AddCommentMutation.field
end
Relay mutations
To define a mutation, use GraphQL::Relay::Mutation.define
. Inside the block, you should configure:
name
, which will name the mutation field & derived types
input_field
s, which will be applied to the derived InputObjectType
return_field
s, which will be applied to the derived ObjectType
resolve(-> (inputs, ctx) { ... })
, the mutation which will actually happen
For example:
AddCommentMutation = GraphQL::Relay::Mutation.define do
name "AddComment"
input_field :postId, !types.ID
input_field :authorId, !types.ID
input_field :content, !types.String
return_field :post, PostType
return_field :comment, CommentType
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 by input_field
- Takes
ctx
, which is the query context you passed with the context:
keyword
- Must return a hash with keys matching your defined
return_field
s
Tutorials
Todo
GlobalNodeIdentification.to_global_id
should receive the type name and object, not id
. (Or, maintain the "type_name, id
in, type_name, id
out" pattern?)
- Reduce duplication in ArrayConnection / RelationConnection
- Improve API for creating edges (better RANGE_ADD support)
- If the new edge isn't a member of the connection's objects, raise a nice error
- Rename
Connection#object
=> Connection#collection
with deprecation
More Resources