GraphQL, Rails and Global IDs

If you ever play with GraphQL, you’ll sooner or later need to load an arbitrary resource from the root query object. As an example, the Relay.js specification asks that each identifiable resource expose a globally unique, standardized identifier. Such objects are referred to as nodes.

query {
node(id: "globally unique id") {
id
}
}
The Node interface contains a single field, id, which is a ID!. The node root field takes a single argument, a ID!, and returns a Node. These two work in concert to allow refetching; if we pass the id returned in that field to the node field, we get the object back.

Although the specification is clear on the intent, it doesn’t enforce nor provide any guidance as to how those identifiers should be created. In Rails, record IDs are scoped per table, or type-specific, hence aren’t considered globally unique. Given a single ID of 1, you wouldn’t know if you need to load #<Post id: 1 ...> or #<Comment id: 1 ...>.

Remember polymorphic associations? The problem was similar, in a sense that an association record could be of any type. Rails solved this by adding two columns, object_id and object_type, making it trivial to know to whom the ID belongs. However, the specification is clear here; only a single ID must be provided.

One interesting solution could be to reuse the same idea and combine the object type with the ID, producing a string similar to "Post/1".

query {
node(id: "Post/1") {
id
}
}

We’d then be able to split the string server side and extract both the class name and the ID. The problem? We’d be reinventing the wheel.

Turns out, it isn’t the first time Rails has a need for a globally unique resource identifier. Back in 4.2, it introduced ActiveJob, a standardized interface for Sidekiq, Resque, and other queuing systems. One particularly interesting aspect of this interface is how jobs are queued, or should I say, what exactly get queued.

$ PostTaggerJob.perform_later(Post.first)
$ YAML.load(Delayed::Job.first.handler).args
> [PostTaggerJob, "abc-1-xyz-2", "gid://myApp/Post/1"]

These params represent the job class name, the job ID, and, you guessed it, a global identifier for the provided resource. In 4.2, alongside ActiveJob, Rails also introduced a gem called GlobalID, included by default in ActiveRecord.

A Global ID is an app wide URI that uniquely identifies a model instance.
$ gid = Post.first.to_global_id
> #<GlobalID:0x007fa9add041f8 ...>
$ gid.app
> "myApp"
$ gid.model_name
> "Post"
$ gid.model_class
> Post(id: integer, name: string...)
$ gid.model_id
> "1"
$ gid.to_s
> "gid://myApp/Post/1"

Obviously, the gem wouldn’t be complete without a way to retrieve our resources.

$ GlobalID::Locator.locate("gid://myApp/Post/1")
> #<Post id: 1, name: "GraphQL, Rails and Global IDs" ...>

This is all we needed for GraphQL, as we’re now able to define unique resource identifiers in a standardized way.

query {
node(id: "gid://myApp/Post/1") {
id
}
}
IDs are designed to be opaque (the only thing that should be passed to the id argument on node is the unaltered result of querying id on some object in the system), and base64ing a string is a useful convention in GraphQL to remind viewers that the string is an opaque identifier.

Global IDs aren’t only useful for GraphQL and ActiveJobs. Since we can now uniquely identify resources everywhere in our application, it opens the door to better polymorphic drop-down list of options, better one-off access granting, resource sharing, and so on.

Have fun!