Relay: Global Object Identification
Relay clients refetch any object by one opaque id, through a root node field. Lighthouse gives a schema that, as PHP Lighthouse does, with two directives: @globalId encodes and decodes the ids, and @node makes a type findable by them. For Relay's cursor pagination, see @paginate(type: CONNECTION).
An app whose schema has graphql-ruby's Relay, as rails g graphql:install writes it by default, uses the same directives on graphql-ruby's ids and Node: see Relay through graphql-ruby. The sections before it describe a schema without it.
Global ids
@globalId on a field encodes its value with the type's name:
type User {
id: ID! @globalId
name: String
}
{ user(id: 1) { id } } answers "VXNlcjox", the base64 of User:1.
On an argument, or an input field, @globalId decodes what the client sent before the field resolves, so every resolver (a directive, @field, a Resolvers:: class) gets the decoded value. decode: says which part:
decode: | The resolver gets |
|---|---|
ARRAY (default) | ["User", "1"] |
TYPE | "User" |
ID | "1" |
type Query {
user(id: ID! @globalId(decode: ID) @eq): User @find
}
type Mutation {
deleteNode(id: ID! @globalId): Node
}
Lists of ids decode item by item. A value that is not a global id makes the field fail with "…" is not a valid global id.
Nodes
@node registers a type for refetching:
type User @node {
id: ID! @globalId
name: String
}
and adds what Relay expects, leaving alone whatever the SDL declares itself:
"Any object implementing this type can be found by ID through `Query.node`."
interface Node {
"Global identifier that can be used to resolve any Node implementation."
id: ID!
}
type User implements Node @node { ... }
type Query {
"Fetches an object given its globally unique `ID`."
node(id: ID! @globalId): Node
}
{
node(id: "VXNlcjox") {
id
... on User { name }
}
}
The type keeps its own id: ID! @globalId, which encodes the ids node decodes. node answers null for an id with no object, and an error for the id of a type without @node.
Where the object comes from
@node(resolver: "Countries@by_code")names a class, and a method after the@(callwithout one). It gets the decoded id and the context, and returns any object, a Hash included:type Country @node(resolver: "Countries@by_code") { id: ID! @globalId name: String! }class Resolvers::Countries COUNTRIES = { 'DE' => { id: 'DE', name: 'Germany' } }.freeze def self.by_code(id, _context) = COUNTRIES[id] endThe class is looked up as
@fieldlooks one up on a type: in the resolvers namespace, then inresolver_namespaces.@node(model: "Account")names the model.Otherwise, the type's
@model, or the model named after the type.
A model's record is found by its primary key, through graphql-ruby's Dataloader, so several node fields in one query load together.
To give Query.node your own description or middleware, declare it, with the registry Lighthouse uses:
type Query {
"This description is up to you."
node(id: ID! @globalId): Node
@field(resolver: "Lighthouse::GraphQL::NodeRegistry@resolve")
@guard
}
Relay through graphql-ruby
graphql-ruby's install (--relay, its default) writes Relay's side in Ruby: Types::NodeType, the node and nodes fields of Types::QueryType, and the schema's id_from_object / object_from_id, on Rails' GlobalID:
class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(
options: { base_types: { query_type: Types::QueryType, node: Types::NodeType, object: Types::BaseObject } }
)
def self.id_from_object(object, type_definition, query_ctx)
object.to_gid_param
end
def self.object_from_id(global_id, query_ctx)
GlobalID.find(global_id)
end
end
lighthouse:install names those classes in base_types, and Lighthouse's directives then work on them. The schema has one kind of global id and one Node:
@globalIdon a field that resolves an object's own id encodes it withid_from_object:Tag.idanswerstag.to_gid_param, as graphql-ruby'sNode.iddoes. Any other value is encoded as before.@globalIdon an argument decodes withobject_from_id, which loads the object.IDgives its id, andARRAY/TYPEits class name (["Tag", 1],"Tag").node:is the SDL'sNodeinterface:interface Node, which@nodeadds, andimplements Noderefer toTypes::NodeType. A@nodetype implements it.Query.nodeandnodesfind objects withobject_from_id, through the#nodeand#nodesmethods ofTypes::QueryType.lighthouse:installdeclares both fields in the startergraphql/schema.graphql, and removes thefield :node/field :nodesthat graphql-ruby wrote inTypes::QueryType, so the SDL is where they are declared. An SDL field without a resolving directive resolves with the root type class's method of its name. If the fields stay inTypes::QueryTypeinstead, they are part of the schema too, and thenodefield@nodewould add toQuerygives way to them.
The starter schema lighthouse:install writes on graphql-ruby's Relay:
type Query {
"Fetches an object given its ID. Resolved by Types::QueryType#node, with AppSchema.object_from_id."
node("ID of the object." id: ID!): Node
"Fetches a list of objects given a list of IDs. Resolved by Types::QueryType#nodes."
nodes("IDs of the objects." ids: [ID!]!): [Node]
user(id: ID! @globalId(decode: ID) @eq): User @find
users: [User!]! @paginate(defaultCount: 10)
}
interface Node {
"ID of the object."
id: ID!
}
type User @node {
id: ID! @globalId
name: String
email: String
}
module Types
class QueryType < Types::BaseObject
# Query.node and nodes are declared in graphql/schema.graphql; these methods resolve them.
def node(id:)
context.schema.object_from_id(id, context)
end
def nodes(ids:)
ids.map { |id| context.schema.object_from_id(id, context) }
end
end
end
lighthouse:install writes this only when the app has no schema file yet, and only on the node / nodes fields as graphql-ruby writes them.
type Tag @node {
id: ID! @globalId
name: String!
}
type Query {
tag(id: ID! @globalId(decode: ID) @eq): Tag @find
}
{
node(id: "Z2lkOi8vYXBwL1RhZy8x") { id ... on Tag { name } }
tag(id: "Z2lkOi8vYXBwL1RhZy8x") { name }
}
Tag.id and both arguments carry Tag 1's GlobalID (gid://app/Tag/1, in base64). Query.node finds every object with object_from_id, so @node(resolver:) and @node(model:) play no part in it, and global_id encodes only what id_from_object does not. A schema without id_from_object / object_from_id works as the sections above describe.
Your own encoding
global_id takes any object with encode(type, id) and decode(global_id), which returns [type, id]:
Lighthouse.configure do |config|
config.global_id = MyGlobalIds # e.g. signed, or a UUID lookup
end