Relay Guide
What is Relay?
The relay spec defines some interfaces that GraphQL servers can follow to allow clients to interact with them in a more efficient way. The spec makes two core assumptions about a GraphQL server:
- It provides a mechanism for refetching an object
- It provides a description of how to page through connections.
You can read more about the relay spec here .
Relay implementation example
Suppose we have the following type:
We want it to have a globally unique ID, a way to retrieve a paginated results
list of it and a way to refetch if if necessary. For that, we need to inherit it
from the Node
interface, annotate its attribute that will be used for
GlobalID
generation with relay.NodeID
and implement its resolve_nodes
abstract method.
Explaining what we did here:
-
We annotated
code
usingrelay.NodeID[int]
. This makescode
a Private type, which will not be exposed to the GraphQL API, and also tells theNode
interface that it should use its value to generate itsid: GlobalID!
for theFruit
type. -
We also implemented the
resolve_nodes
abstract method. This method is responsible for retrieving theFruit
instances given itsid
. Becausecode
is our id,node_ids
will be a list of codes as a string.
The GlobalID
gets generated by getting the base64 encoded version of the
string <TypeName>:<NodeID>
. In the example above, the Fruit
with a code of
1
would have its GlobalID
as base64("Fruit:1")
= RnJ1aXQ6MQ==
Now we can expose it in the schema for retrieval and pagination like:
This will generate a schema like this:
With only that we have a way to query node
to retrieve any Node
implemented
type in our schema (which includes our Fruit
type), and also a way to retrieve
a list of fruits with pagination.
For example, to retrieve a single fruit given its unique ID:
Or to retrieve the first 10 fruits available:
The connection resolver for relay.ListConnection
should return one of those:
-
List[<NodeType>]
-
Iterator[<NodeType>]
-
Iterable[<NodeType>]
-
AsyncIterator[<NodeType>]
-
AsyncIterable[<NodeType>]
-
Generator[<NodeType>, Any, Any]
-
AsyncGenerator[<NodeType>, Any]
The node field
As demonstrated above, the Node
field can be used to retrieve/refetch any
object in the schema that implements the Node
interface.
It can be defined in the Query
objects in 4 ways:
-
node: Node
: This will define a field that accepts aGlobalID!
and returns aNode
instance. This is the most basic way to define it. -
node: Optional[Node]
: The same asNode
, but if the given object doesn’t exist, it will returnnull
. -
node: List[Node]
: This will define a field that accepts[GlobalID!]!
and returns a list ofNode
instances. They can even be from different types. -
node: List[Optional[Node]]
: The same asList[Node]
, but the returned list can containnull
values if the given objects don’t exist.
Custom connection pagination
The default relay.Connection
class don’t implement any pagination logic, and
should be used as a base class to implement your own pagination logic. All you
need to do is implement the resolve_connection
classmethod.
The integration provides relay.ListConnection
, which implements a limit/offset
approach to paginate the results. This is a basic approach and might be enough
for most use cases.
relay.ListConnection
implementes the limit/offset by using slices. That means
that you can override what the slice does by customizing the __getitem__
method of the object returned by your nodes resolver.
For example, when working with Django
, resolve_nodes
can return a
QuerySet
, meaning that the slice on it will translate to a LIMIT
/OFFSET
in
the SQL query, which will fetch only the data that is needed from the database.
Also note that if that object doesn’t have a __getitem__
attribute, it will
use itertools.islice
to paginate it, meaning that when a generator is being
resolved it will only generate as much results as needed for the given
pagination, the worst case scenario being the last results needing to be
returned.
Now, suppose we want to implement a custom cursor-based pagination for our previous example. We can do something like this:
In the example above we specialized the FruitCustomPaginationConnection
by
inheriting it from relay.Connection[Fruit]
. We could still keep it generic by
inheriting it from relay.Connection[relay.NodeType]
and then specialize it
when defining the field, making it possible to use our custom pagination logic
with more than one type.
Custom connection arguments
By default the connection will automatically insert some arguments for it to be able to paginate the results. Those are:
-
before
: Returns the items in the list that come before the specified cursor -
after
: Returns the items in the list that come after the ” “specified cursor -
first
: Returns the first n items from the list -
last
: Returns the items in the list that come after the ” “specified cursor
You can still define extra arguments to be used by your own resolver or custom pagination logic. For example, suppose we want to return the pagination of all fruits whose name starts with a given string. We could do that like this:
This will generate a schema like this:
Convert the node to its proper type when resolving the connection
The connection expects that the resolver will return a list of objects that is a
subclass of its NodeType
. But there may be situations where you are resolving
something that needs to be converted to the proper type, like an ORM model.
In this case you can subclass the relay.Connection
/relay.ListConnection
and
provide a custom resolve_node
method to it, which by default returns the node
as is. For example:
The main advantage of this approach instead of converting it inside the custom
resolver is that the Connection
will paginate the QuerySet
first, which in
case of django will make sure that only the paginated results are fetched from
the database. After that, the resolve_node
function will be called for each
result to retrieve the correct object for it.
We used django for this example, but the same applies to any other other similar use case, like SQLAlchemy, etc.
The GlobalID scalar
The GlobalID
scalar is a special object that contains all the info necessary
to identify and retrieve a given object that implements the Node
interface.
It can for example be useful in a mutation, to receive and object and retrieve it in its resolver. For example:
In the example above, you can also access the type name directly with
id.type_name
, the raw node ID with id.id
, or even resolve the type itself
with id.resolve_type(info)
.