Automatic ActiveRecord column selection for GraphQL (Ruby) fields.
This library was developed for- and extracted from HireFire.
The documentation can be found on RubyDoc.
- Ruby 2.7+
- ActiveRecord 6.0+
- GraphQL (Ruby) 1.9+
Add the gem to your Gemfile and run bundle
.
gem "graphql_activerecord_autoselect", "~> 2"
class Types::Actor < Types::Base::Object
using GraphQLActiveRecordAutoSelect
field :organizations, Types::Organization, null: false, extras: [:lookahead]
def organizations(lookahead:)
object.organizations.autoselect(lookahead)
end
field :organization, Types::Organization, null: false, extras: [:lookahead] do
argument :id, ID, required: true
end
def organization(id:, lookahead:)
object.organizations.autoselect(lookahead).find(id)
end
end
In this example an actor has many organizations. We allow the client to query all of the actor's organizations, or a specific one by id. We acquire the lookahead functionality provided by GraphQL Ruby and pass it to autoselect
.
The autoselect
method is made available to ActiveRecord::Base at the class level, as well as to all instances of ActiveRecord::Relations, in classes where the GraphQLActiveRecordAutoSelect refinement is used (using GraphQLActiveRecordAutoSelect
).
If you submit the following query to the server:
query {
actor {
organizations {
name
time_zone
}
}
}
It'll translate this:
object.organizations.autoselect(lookahead)
Into this:
object.organizations.select(:id, :actor_id, :name, :time_zone)
Notice that it didn't only select the :name
and :time_zone
fields, but :id
and :actor_id
as well. These kinds of identifiers are always selected in order to avoid potential lookup issues on other relations where they're mandatory.
The following fields are always selected:
- The primary key field (typically id)
- The type field
- Fields that end in _id
- Fields that end in _type
The example above builds a select on a has many relation, but the same works for belongs to.
class Types::Organization < Types::Base::Object
using GraphQLActiveRecordAutoSelect
field :actor, Types::Actor, null: false, extras: [:lookahead]
def actor(lookahead:)
Actor.autoselect(lookahead).find(object.actor_id)
end
end
And with this query:
query {
organization {
actor {
email
}
}
}
It'll translate this:
Actor.autoselect(lookahead).find(object.actor_id)
Into this:
Actor.select(:id, :email).find(object.actor_id)
You might have fields or methods that depend on the presence of one or more fields. You can specify dependents to ensure that the requested field or method will be able to successfully compute:
class Actor < ActiveRecord::Base
def secret
decrypt(secret_digest)
end
private
def decrypt(column)
# ...
end
end
class Types::Actor < Types::Base::Object
# ...
field :email, String, null: false
field :full_name, String, null: false
field :secret, String, null: false
def self.dependents
{
:full_name => [:first_name, :last_name],
:secret => [:secret_digest],
}
end
def full_name
"#{object.first_name} #{object.last_name}"
end
# ...
end
class Types::Organization < Types::Base::Object
using GraphQLActiveRecordAutoSelect
field :actor, Types::Actor, null: false, extras: [:lookahead]
def actor(lookahead:)
Actor.autoselect(lookahead, Types::Actor.dependents).find(object.actor_id)
end
end
There's a few things to note here.
- We've defined the
secret
method in the Actor model which decrypts and returns the encrypted data stored in thesecret_digest
column. - We've defined
self.dependents
on theTypes::Actor
class which describes which fields depend on what columns. - We've passed in that dependent configuration to the second argument of
autoselect
.
We're essentially telling autoselect that if the client selects the full_name
field, that it has to select the first_name
and last_name
columns on the model. Likewise, when selecting the secret
field, it'll select the secret_digest
column on the model so that it has the encrypted data it needs to decrypt and return.
query {
organization {
actor {
email
full_name
secret
}
}
}
It'll translates this:
Actor.autoselect(lookahead, Actor.dependents).find(object.actor_id)
Into this:
Actor.select(:id, :email, :first_name, :last_name, :secret_digest).find(object.actor_id)
Since we've selected the full_name
and secret
fields, autoselect selects the first_name
, last_name
, and secret_digest
columns, as these are necessary in order to compute full_name
and secret
.
Bug reports and pull requests are welcome on GitHub at:
https://github.com/mrrooijen/graphql_activerecord_autoselect
To install the dependencies:
$ bundle
To open an interactive console:
$ bundle console
To run the tests:
$ bundle exec rake
To view the code coverage (generated after each test run):
$ open coverage/index.html
To run the local documentation server:
$ bundle exec rake doc
To build a gem:
$ bundle exec rake build
To build and install a gem on your local machine:
$ bundle exec rake install
For a list of available tasks:
$ bundle exec rake --tasks
Released under the MIT License by Michael van Rooijen.