graphql-ruby Documentation

Ruby implementation of GraphQL

5,445 stars Ruby Ruby 121 files · ~125,857 tokens #graphql#graphql-ruby#hacktoberfest#rails#relay#ruby
RAW Doc

Technical Documentation: rmosolgo/graphql-ruby

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (`readme.md`)

graphql


A Ruby implementation of GraphQL.

Installation

Install from RubyGems by adding it to your Gemfile, then bundling.

ruby
# Gemfile
gem 'graphql'
text
$ bundle install

Getting Started

text
$ rails generate graphql:install

After this, you may need to run bundle install again, as by default graphiql-rails is added on installation.

Or, see "Getting Started".

Upgrade

I also sell GraphQL::Pro which provides several features on top of the GraphQL runtime, including:

Besides that, Pro customers get email support and an opportunity to support graphql-ruby's development!

Goals

  • Implement the GraphQL spec & support a Relay front end
  • Provide idiomatic, plain-Ruby API with similarities to reference implementation where possible
  • Support Ruby on Rails and Relay

Getting Involved

  • Say hi & ask questions in the #graphql-ruby channel on Discord.
  • Report bugs by posting a description, full stack trace, and all relevant code in a GitHub issue.
  • Start hacking with the Development guide.

Overview (`javascript_client/readme.md`)

Find the graphql-ruby-client docs on the GraphQL-Ruby website.

License

graphql-ruby-client is available under the LGPLv3 license;
graphql-pro customers are granted a special commercial license.

Development

  • With GraphQL-Ruby:
    • Install the dependencies with rake js:install
    • Run the tests with rake js:test
  • Stand-alone:
    • Install dependencies yarn install
    • Run the tests yarn run test
  • Run the TypeScript compiler: yarn tsc -w
  • Install for local development with npm link .

Guides Authorization Overview (`guides/authorization/overview.md`)

Overview

Here's a conceptual approach to GraphQL authorization, followed by an introduction to the built-in authorization framework. Each part of the framework is described in detail in its own guide.

Authorization: GraphQL vs REST

In a REST API, the common authorization pattern is fairly simple. Before performing the requested action, the server asserts that the current client has the required permissions for that action. For example:

ruby
class PostsController < ApiController
  def create
    # First, check the client's permission level:
    if current_user.can?(:create_posts)
      # If the user is permitted, then perform the action:
      post = Post.create(params)
      render json: post
    else
      # Otherwise, return an error:
      render nothing: true, status: 403
    end
  end
end

However, this request-by-request mindset doesn't map well to GraphQL because there's only one controller and the requests that come to it may be very different. To illustrate the problem:

ruby
class GraphqlController < ApplicationController
  def execute
    # What permission is required for `query_str`?
    # It depends on the string! So, you can't generalize at this level.
    if current_user.can?(:"???")
      MySchema.execute(query_str, context: ctx, variables: variables)
    end
  end
end

So, what new mindset will work with a GraphQL API?

For mutations, remember that each mutation is like an API request in itself. For example, Posts#create above would map to the createPost(...) mutation in GraphQL. So, each mutation should be authorized in its own right.

For queries, you can think of each individual object like a GET request to a REST API. So, each object should be authorized for reading in its own right.

By applying this mindset, each part of the GraphQL query will be properly authorized before it is executed. Also, since the different units of code are each authorized on their own, you can be sure that each incoming query will be properly authorized, even if it's a brand new query that the server has never seen before.

What About Authentication?

As a reminder:

  • Authentication is the process of determining what user is making the current request, for example, accepting a username and password, or finding a User in the database from session[:current_user_id].
  • Authorization is the process of verifying that the current user has permission to do something (or see something), for example, checking admin? status or looking up permission groups from the database.

In general, authentication is not addressed in GraphQL at all. Instead, your controller should get the current user based on the HTTP request (eg, an HTTP header or a cookie) and provide that information to the GraphQL query. For example:

ruby
class GraphqlController < ApplicationController
  def execute
    # Somehow get the current `User` from this HTTP request.
    current_user = get_logged_in_user(request)
    # Provide the current user in `context` for use during the query
    context = { current_user: current_user }
    MySchema.execute(query_str, context: context, ...)
  end
end

After your HTTP handler has loaded the current user, you can access it via context[:current_user] in your GraphQL code.

Authorization in Your Business Logic

Before introducing GraphQL-specific authorization, consider the advantages of application-level authorization. (See the GraphQL.org post on the same topic.) For example, here's authorization mixed into the GraphQL API layer:

ruby
field :posts, [Types::Post], null: false

def posts
  # Perform an auth check in the GraphQL field code:
  if context[:current_user].admin?
    Post.all
  else
    Post.published
  end
end

The downside of this is that, when Types::Post is queried in other contexts, the same authorization check may not be applied. Additionally, since the authorization code is coupled with the GraphQL API, the only way to test it is via GraphQL queries, which adds some complexity to tests.

Alternatively, you could move the authorization to your business logic, the Post class:

ruby
class Post < ActiveRecord::Base
  # Return the list of posts which `user` may see
  def self.posts_for(user)
    if user.admin?
      self.all
    else
      self.published
    end
  end
end

Then, use this application method in your GraphQL code:

ruby
field :posts, [Types::Post], null: false

def posts
  # Fetch the posts this user can see:
  Post.posts_for(context[:current_user])
end

In this case, Post.posts_for(user) could be tested independently from GraphQL. Then, you have less to worry about in your GraphQL tests. As a bonus, you can use Post.posts_for(user) in other parts of the app, too, such as the web UI or REST API.

GraphQL-Ruby's Authorization Framework

Despite the advantages of authorization at the application layer, as described above, there might be some reasons to authorize in the API layer:

  • Have an extra assurance that your API layer is secure
  • Authorize the API request before running it (see "visibility" below)
  • Integrate with code that doesn't have authorization built-in

To accomplish these, you can use GraphQL-Ruby's authorization framework. The framework has three levels, each of which is described in its own guide:

  • Visibility hides parts of the GraphQL schema from users who don't have full permission.
  • Authorization checks application objects during execution to be sure the user has permission to access them.

Also, GraphQL::Pro has integrations for CanCan and Pundit.


Guides Changesets Overview (`guides/changesets/overview.md`)

API Versioning for GraphQL-Ruby

Out-of-the-box, GraphQL is versionless by design. GraphQL's openness to extension paves the way for continuously expanding and improving an API. You can always add new fields, new arguments, and new types to implement new features and customize existing behavior.

However, sometimes a business case may call for a different versioning scheme. GraphQL-Enterprise's "Changesets" enable schemas to release any change -- even breaking changes -- to clients, depending on what version of the schema they're using. With Changesets, you can redefine existing fields, define new types using old names, add or remove enum values -- anything, really -- while maintaining compatibility for existing clients.

Why Changesets?

Changesets are a complementary evolution technique to continuous additions. In general, additive changes (new fields, new arguments, new types) are best added right to the existing schema. But if you need to remove something from the schema or redefine existing parts of the schema in non-backwards-compatible ways, Changesets provide a handy way of doing so.

For example, if you add a values to an Enum, you can just add it to the existing schema:

diff
class Types::RecipeTag < Types::BaseEnum
    value "LOW_FAT"
    value "LOW_CARB"
+   value "VEGAN"
+   value "KETO"
+   value "GRAPEFRUIT_DIET"
  end

However, if you want to change the schema in ways that would break previous queries, you can do that with a Changeset:

ruby
class Types::RecipeTag < Types::BaseEnum
  # Turns out this makes you sick:
  value "GRAPEFRUIT_DIET", removed_in: Changesets::RemoveLegacyDiets
end

Then, only clients requesting API versions before this changeset would be able to use GRAPEFRUIT_DIET; clients requesting newer versions could not send it as input and would not receive it in responses.

(Changesets also support additive changes, if you prefer to make them that way.)

Getting Started

To start using Changesets, read on:


Guides Dataloader Overview (`guides/dataloader/overview.md`)

Overview

GraphQL::Dataloader provides efficient, batched access to external services, backed by Ruby's Fiber concurrency primitive. It has a per-query result cache and AsyncDataloader supports truly parallel execution out-of-the-box.

GraphQL::Dataloader is inspired by @bessey's proof-of-concept and shopify/graphql-batch.

Batch Loading

GraphQL::Dataloader facilitates a two-stage approach to fetching data from external sources (like databases or APIs):

  • First, GraphQL fields register their data requirements (eg, object IDs or query parameters)
  • Then, after as many requirements have been gathered as possible, GraphQL::Dataloader initiates actual fetches to external services

That cycle is repeated during execution: data requirements are gathered until no further GraphQL fields can be executed, then GraphQL::Dataloader triggers external calls based on those requirements and GraphQL execution resumes.

Fibers

GraphQL::Dataloader uses Ruby's Fiber, a lightweight concurrency primitive which supports application-level scheduling within a Thread. By using Fiber, GraphQL::Dataloader can pause GraphQL execution when data is requested, then resume execution after the data is fetched.

At a high level, GraphQL::Dataloader's usage of Fiber looks like this:

  • GraphQL execution is run inside a Fiber.
  • When that Fiber returns, if the Fiber was paused to wait for data, then GraphQL execution resumes with the next (sibling) GraphQL field inside a new Fiber.
  • That cycle continues until no further sibling fields are available and all known Fibers are paused.
  • GraphQL::Dataloader takes the first paused Fiber and resumes it, causing the GraphQL::Dataloader::Source to execute its #fetch(...) call. That Fiber continues execution as far as it can.
  • Likewise, paused Fibers are resumed, causing GraphQL execution to continue, until all paused Fibers are evaluated completely.

Whenever GraphQL::Dataloader creates a new Fiber, it copies each pair from Thread.current[...] and reassigns them inside the new Fiber.

AsyncDataloader, built on top of the async gem, supports parallel I/O operations (like network and database communication) via Ruby's non-blocking Fiber.schedule API. Learn more →.

Getting Started

To install GraphQL::Dataloader, add it to your schema with use ..., for example:

ruby
class MySchema < GraphQL::Schema
  # ...
  use GraphQL::Dataloader
end

Then, inside your schema, you can request batch-loaded objects by their lookup key with dataloader.with(...).load(...):

ruby
field :user, Types::User do
  argument :handle, String
end

def user(handle:)
  dataloader.with(Sources::UserByHandle).load(handle)
end

Or, load several objects by passing an array of lookup keys to .load_all(...):

ruby
field :is_following, Boolean, null: false do
  argument :follower_handle, String
  argument :followed_handle, String
end

def is_following(follower_handle:, followed_handle:)
  follower, followed = dataloader
    .with(Sources::UserByHandle)
    .load_all([follower_handle, followed_handle])

  followed && follower && follower.follows?(followed)
end

To prepare requests from several sources, use .request(...), then call .load after all requests are registered:

ruby
class AddToList < GraphQL::Schema::Mutation
  argument :handle, String
  argument :list, String, as: :list_name

  field :list, Types::UserList

  def resolve(handle:, list_name:)
    # first, register the requests:
    user_request = dataloader.with(Sources::UserByHandle).request(handle)
    list_request = dataloader.with(Sources::ListByName, context[:viewer]).request(list_name)
    # then, use `.load` to wait for the external call and return the object:
    user = user_request.load
    list = list_request.load
    # Now, all objects are ready.
    list.add_user!(user)
    { list: list }
  end
end

`loads:` and `object_from_id`

dataloader is also available as context.dataloader, so you can use it to implement MySchema.object_from_id. For example:

ruby
class MySchema < GraphQL::Schema
  def self.object_from_id(id, ctx)
    model_class, database_id = IdDecoder.decode(id)
    ctx.dataloader.with(Sources::RecordById, model_class).load(database_id)
  end
end

Then, any arguments with loads: will use that method to fetch objects. For example:

ruby
class FollowUser < GraphQL::Schema::Mutation
  argument :follow_id, ID, loads: Types::User

  field :followed, Types::User

  def resolve(follow:)
    # `follow` was fetched using the Schema's `object_from_id` hook
    context[:viewer].follow!(follow)
    { followed: follow }
  end
end

Data Sources

To implement batch-loading data sources, see the Sources guide.

Parallelism

You can run I/O operations in parallel with GraphQL::Dataloader. There are two approaches:

  • AsyncDataloader uses the async gem to automatically background I/O from Dataloader::Source#fetch calls. Read More
  • You can manually call dataloader.yield after starting work in the background. Read More

Guides Defer Overview (`guides/defer/overview.md`)

Overview

@defer is a directive for streaming GraphQL responses from the server to the client.

By streaming the response, the server can send the most critical (or most available) data first, following up with secondary data shortly afterward.

@defer was first described by Lee Byron at React Europe 2015 and got experimental support in Apollo in 2018.

@stream is like @defer, but it returns list items one at a time. Find details in the Stream guide.

Example

GraphQL queries can be large and complex, requiring lots of computation or dependencies on slow external services.

In this example, the local server maintains an index of items ("decks"), but the item data ("cards") is actually hosted on a remote server. So, GraphQL queries must make remote calls in order to serve that data.

Without @defer, the whole query is blocked until the last field is done resolving:

But, we can add @defer to slow fields:

diff
deck {
    slots {
      quantity
-     card
+     card @defer {
        name
        price
      }
    }
  }

Then, the response will stream to the client bit by bit, so the page can load progressively:

This way, clients get a snappy feel from the app even while data is still loading.

View the full demo at https://github.com/rmosolgo/graphql_defer_example.

Considerations

  • @defer adds some overhead to the response, so only apply it judiciously.
  • @defer is single-threaded. @defered fields are still evaluated in sequence, but in a chunk-by-chunk way.

Next Steps

Set up your server to support @defer or read about client usage of it.


Guides Errors Overview (`guides/errors/overview.md`)

Errors in GraphQL

There are a lot of different kinds of errors in GraphQL! In this guide, we'll discuss some of the main categories and learn when they apply.

Validation Errors

Because GraphQL is strongly typed, it performs validation of all queries before executing them. If an incoming query is invalid, it isn't executed. Instead, a response is sent back with "errors":

ruby
{
  "errors" => [ ... ]
}

Each error has a message, line, column and path.

The validation rules are part of the GraphQL specification and built into GraphQL-Ruby, so there's not really a way to customize this behavior, except to pass validate: false when executing a query, which skips validation altogether.

You can configure your schema to stop validating after a certain number of errors by setting Schema.validate_max_errors. Also, you can add a timeout to this step with Schema.validate_timeout.

Analysis Errors

GraphQL-Ruby supports pre-execution analysis, which may return "errors" instead of running a query. You can find details in the Analysis guide.

GraphQL Invariants

While GraphQL-Ruby is executing a query, some constraints must be satisfied. For example:

  • Non-null fields may not return nil.
  • Interface and union types must resolve objects to types that belong to that interface/union.

These constraints are part of the GraphQL specification, and when they are violated, it must be addressed somehow. Read more in Type Errors.

Top-level `"errors"`

The GraphQL specification provides for a top-level "errors" key which may include information about errors during query execution. "errors" and "data" may both be present in the case of a partial success.

In your own schema, you can add to the "errors" key by raising GraphQL::ExecutionError (or subclasses of it) in your code. Read more in the Execution Errors guide.

Handled Errors

A schema can be configured to handle certain errors during field execution with handlers that you give it, using rescue_from. Read more in the Error Handling guide.

Unhandled Errors (Crashes)

When a raised error is not rescued, the GraphQL query crashes entirely and the surrounding code (like a Rails controller) must handle the exception.

For example, Rails will probably return a generic 500 page.

Errors as Data

When you want end users (human beings) to read error messages, you can express errors in the schema, using normal GraphQL fields and types. In this approach, errors are strongly-typed data, queryable in the schema, like any other application data.

For more about this approach, see Mutation Errors


Guides Javascript Client Overview (`guides/javascript_client/overview.md`)

Overview

There is a JavaScript client for GraphQL-Ruby, graphql-ruby-client.

You can install it from NPM or Yarn:

sh
yarn add graphql-ruby-client
# Or:
npm install graphql-ruby-client

The source code is in the graphql-ruby repository.

See detailed guides for more info about its features:


Guides Limiters Overview (`guides/limiters/overview.md`)

Rate Limiters for GraphQL

GraphQL::Enterprise includes rate limiters built especially for GraphQL.

For REST APIs, rate limiters often count requests and block clients when they exceed their limit over a certain period of time. However, this paradigm doesn't translate well to GraphQL because the cost of serving a request may vary dramatically depending on the GraphQL query contained in the request. Instead, GraphQL::Enterprise implements two other kinds of limiters:

  • An active operation limiter which allows clients to run a certain number of operations at a time. For example, if the limit is five concurrent operations, and a client sends six requests simultaneously, then only five of those incoming operations will be executed; the sixth will be returned with an error and it may be retried when one of the five others finishes.
  • A runtime limiter which limits the amount of processing time a client may consume during a given window. For example, a limit of 120 seconds per minute would allow two concurrent requests on average -- although in practice, it might be spiky: perhaps five concurrent, 20-second-long requests, followed by 40 seconds of no requests.

There's some overlap in these limiters; both of them constrain the amount of time a client may force the server to spend in handling requests. The active operation limiter puts an upper bound on how many processes a client may occupy while the runtime limiter puts a bound on total processing time (regardless of the number of concurrent operations at a given moment).

To get started, read on:


Guides Object Cache Overview (`guides/object_cache/overview.md`)

GraphQL ObjectCache

GraphQL::Enterprise::ObjectCache is an application-level cache for GraphQL-Ruby servers. It works by storing a cache fingerprint for each object in a query, then serving a cached response as long as those fingerprints don't change. The cache can also be customized with TTLs.

Why?

ObjectCache can greatly reduce GraphQL response times by serving cached responses when the underlying data for a query hasn't changed.

Usually, a GraphQL query alternates between data fetching and calling application logic:

But with ObjectCache, it checks the cache first, returning a cached response if possible:

This reduces latency for clients and reduces the load on your database and application server.

How

Before running a query, ObjectCache creates a fingerprint for the query using GraphQL::Query#fingerprint and Schema.context_fingerprint_for(ctx). Then, it checks the backend for a cached response which matches the fingerprint.

If a match is found, the ObjectCache fetches the objects previously visited by this query. Then, it compares the current fingerprint of each object ot the one in the cache and checks .authorized? for that object. If the fingerprints all match and all objects pass authorization checks, then the cached response returned. (Authorization checks can be disabled.)

If there is no cached response or if the fingerprints don't match, then the incoming query is re-evaluated. While it's executed, ObjectCache gathers the IDs and fingerprints of each object it encounters. When the query is done, the result and the new object fingerprints are written to the cache.

Setup

To get started with the object cache:


Guides Operation Store Overview (`guides/operation_store/overview.md`)

Overview

GraphQL::Pro::OperationStore uses Rack and a storage backend (ActiveRecord or Redis) to maintain a normalized, deduplicated database of persisted queries for your GraphQL system.

In this guide, you'll find:

In other guides, you can read more about:

Also, you can find a demo app on GitHub.

What are Persisted Queries?

Persisted queries are GraphQL queries (query, mutation, or subscription) that are saved on the server and invoked by clients by reference. In this arrangement, clients don't send GraphQL queries over the network. Instead, clients send:

  • Client name, to identify the client who is making the request
  • Query alias, to specify which stored operation to run
  • Query variables, to provide values for the stored operation

Then, the server uses the identifier to fetch the full GraphQL document from the database.

Without persisted queries, clients send the whole document:

ruby
# Before, without persisted queries
query_string = "query GetUserDetails($userId: ID!) { ... }"

MyGraphQLEndpoint.post({
  query: query_string,
  operationName: "GetUserDetails",
  variables: { userId: "100" },
})

But with persisted queries, the full document isn't sent because the server already has a copy of it:

ruby
# After, with persisted queries:
MyGraphQLEndpoint.post({
  operationId: { "relay-app-v1/fc84dbba3623383fdc",
  #               client name / query alias (eg, @relayHash)
  variables: { userId: "100" },
})

Why Persisted Queries?

Using persisted queries improves the security, efficiency and visibility of your GraphQL system.

Security

Persisted queries improve security because you can reject arbitrary GraphQL queries, removing an attack vector from your system. The query database serves a whitelist, so you can be sure that no unexpected queries will hit your system.

For example, after all clients have migrated to persisted queries, you can reject arbitrary GraphQL in production:

ruby
# app/controllers/graphql_controller.rb
if Rails.env.production? && params[:query].present?
  # Reject arbitrary GraphQL in production:
  render json: { errors: [{ message: "Raw GraphQL is not accepted" }]}
else
  # ...
end

Efficiency

Persisted queries improve the efficiency of your system by reducing HTTP traffic. Instead of repeatedly sending GraphQL over the wire, queries are fetched from the database, so your requests require less bandwidth.

For example, before using persisted queries, the entire query is sent to the server:

But after using persisted queries, only the query identification info is sent to the server:

Visibility

Persisted queries improve visibility because you can track GraphQL usage from a single location. OperationStore maintains an index of type, field and argument usage so that you can analyze your traffic.

How it Works

OperationStore uses tables in your database to store normalized, deduplicated GraphQL strings. The database is immutable: new operations may be added, but operations are never modified or removed.

When clients sync their operations, requests are authenticated, then the incoming GraphQL is validated, normalized, and added to the database if needed. Also, the incoming client name is associated with all operations in the payload.

Then, at runtime, clients send an operation ID to run a persisted query. It looks like this in params:

ruby
params[:operationId] # => "relay-app-v1/810c97f6631001..."

OperationStore uses this to fetch the matching operation from the database. From there, the query is evaluated normally.

Getting Started

See the getting started guide to add OperationStore to your app.


Guides Pagination Overview (`guides/pagination/overview.md`)

Overview

GraphQL-Ruby ships with several implementations of Relay's "connection"-style pagination. You can familiarize yourself with connections in Connection Concepts and see how to use them in Using Connections. It also supports custom connection implementations and type definitions, which you can explore in the Custom Connections guide.

GraphQL has its own great pagination docs for further reading.


Guides Subscriptions Overview (`guides/subscriptions/overview.md`)

Overview

Subscriptions allow GraphQL clients to observe specific events and receive updates from the server when those events occur. This supports live updates, such as websocket pushes. Subscriptions introduce several new concepts:

  • The Subscription type is the entry point for subscription queries
  • Subscription classes are resolvers for handing initial subscription requests and subsequent updates
  • Triggers begin the update process
  • The Implementation provides application-specific methods for executing & delivering updates.
  • Broadcasts can send the same GraphQL result to any number of subscribers.

Subscription Type

subscription is an entry point to your GraphQL schema, like query or mutation. It is defined by your SubscriptionType, a root-level GraphQL::Schema::Object.

Read more in the Subscription Type guide.

Subscription Classes

GraphQL::Schema::Subscription is a resolver class with subscription-specific behaviors. Each subscription field should be implemented by a subscription class.

Read more in the Subscription Classes guide

Triggers

After an event occurs in our application, triggers begin the update process by sending a name and payload to GraphQL.

Read more in the Triggers guide.

Implementation

Besides the GraphQL component, your application must provide some subscription-related plumbing, for example:

  • state management: How does your application keep track of who is subscribed to what?
  • transport: How does your application deliver payloads to clients?
  • queueing: How does your application distribute the work of re-running subscription queries?

Read more in the Implementation guide or check out the ActionCable implementation, Pusher implementation or Ably implementation.

Broadcasts

By default, the subscription implementations listed above handle each subscription in total isolation. However, this behavior can be optimized by setting up broadcasts. Read more in the Broadcast guide.

Multi-Tenant

See the Multi-tenant guide for supporting multi-tenancy in GraphQL subscriptions.


Guides Testing Overview (`guides/testing/overview.md`)

Overview

So, you've spiked a GraphQL API, and now you're ready to tighten things up and add some proper tests. These guides will help you think about how to ensure stability and compatibility for your GraphQL system.

  • Structure testing verifies that schema changes are backwards-compatible. This way, you don't break existing clients.
  • Integration testing exercises the various behaviors of the GraphQL system, making sure that it returns the right data to the right clients.
  • Testing helpers for running GraphQL fields without writing a whole query

Guide Metadata.Yml (`docs/guide_metadata.yml`)

version: 1

order:
authorization:
- overview
- visibility
- authorization
- can_can_integration
- pundit_integration
- scoping
changesets:
- overview
- installation
- definition
- releases
dataloader:
- overview
- sources
- dataloader
- adopting
- testing
- async_dataloader
- parallelism
defer:
- overview
- setup
- usage
- stream
- graphiql
development:
- getting_started
- development
- faq
- related_projects
- documentation_maintenance
errors:
- overview
- execution_errors
- error_handling
- type_errors
execution:
- next
- migration
fields:
- introduction
- arguments
- resolvers
- validation
- limits
javascript_client:
- overview
- sync
- apollo_subscriptions
- relay_subscriptions
- urql_subscriptions
- graphiql_subscriptions
language_tools:
- visitor
- c_parser
limiters:
- overview
- redis
- active_operations
- runtime
- deployment
mutations:
- mutation_root
- mutation_classes
- mutation_errors
- mutation_authorization
object_cache:
- overview
- schema_setup
- caching
- memcached
- redis
- runtime_considerations
operation_store:
- overview
- getting_started
- active_record_backend
- redis_backend
- client_workflow
- server_management
- access_control
pagination:
- overview
- connection_concepts
- using_connections
- custom_connections
- cursors
- stable_relation_connections
pro:
- home
- installation
- dashboard
- encoders
- privacy
queries:
- executing_queries
- ast_analysis
- phases_of_execution
- complexity_and_depth
- timeout
- multiplex
- lookahead
- tracing
- backtrace_annotations
- logging
- response_extensions
relay:
- range_add
schema:
- definition
- root_types
- generators
- introspection
- lazy_execution
- sdl
- dynamic_types
- object_identification
subscriptions:
- overview
- subscription_classes
- subscription_type
- triggers
- broadcast
- implementation
- action_cable_implementation
- pusher_implementation
- ably_implementation
- multi_tenant
testing:
- overview
- schema_structure
- integration_tests
- helpers
- profiling
type_definitions:
- objects
- scalars
- enums
- input_objects
- interfaces
- unions
- lists
- non_nulls
- extensions
- directives
- field_extensions

Product callouts migrated from the Jekyll `pro` and `enterprise` frontmatter.

products:
pro:
- authorization/can_can_integration
- authorization/pundit_integration
- defer/*
- operation_store/*
- pagination/stable_relation_connections
- pro/dashboard
- pro/encoders
- pro/home
- pro/installation
- subscriptions/ably_implementation
- subscriptions/pusher_implementation
enterprise:
- changesets/*
- limiters/*
- object_cache/*


Redirects.Yml (`docs/redirects.yml`)

Guide URLs are generated from the source tree so every existing guide has a

concrete redirect. Add explicit entries for pages that move into an API ref.

guide_source: guides
redirects:

  • old_path: /docs/maintenance
    destination:
    kind: page
    value: guides/development/documentation_maintenance.md
  • old_path: /guides
    destination:
    kind: page
    value: guides/development/getting_started.md
  • old_path: /queries/executing_queries
    destination:
    kind: rdoc_ref
    value: GraphQL::Schema.execute
  • old_path: /getting_started
    destination:
    kind: page
    value: guides/development/getting_started.md

Guides Authorization Authorization (`guides/authorization/authorization.md`)

Authorization

While a query is running, you can check each object to see whether the current user is authorized to interact with that object. If the user is not authorized, you can handle the case with an error.

Adding Authorization Checks

Schema members have authorized? methods which will be called during execution:

  • Type classes have .authorized?(object, context) class methods
  • Fields have #authorized?(object, args, context) instance methods
  • Arguments have #authorized?(object, arg_value, context) instance methods
  • Mutations and Resolvers have .authorized?(object, context) class methods and #authorized?(args) instance methods
  • Enum values have #authorized?(context) instance methods

These methods are called with:

  • object: the object from your application which was returned from a field
  • args/arg_value: The arguments for a field, or the value of an argument
  • context: the query context, based on the hash passed as context:

Object Authorization

When you implement this method to return false, the query will be halted, for example:

ruby
class Types::Friendship < Types::BaseObject
  # You can only see the details on a `Friendship`
  # if you're one of the people involved in it.
  def self.authorized?(object, context)
    super && (object.to_friend == context[:viewer] || object.from_friend == context[:viewer])
  end
end

(Always call super to get the default checks, too.)

Now, whenever an object of type Friendship is going to be returned to the client, it will first go through the .authorized? method. If that method returns false, the field will get nil instead of the original object, and you may handle that case with an error (see below).

Field Authorization

Field #authorized? methods are called before resolving a field, for example:

ruby
class Types::BaseField < GraphQL::Schema::Field
  # Pass `field ..., require_admin: true` to reject non-admin users from a given field
  def initialize(*args, require_admin: false, **kwargs, &block)
    @require_admin = require_admin
    super(*args, **kwargs, &block)
  end

  def authorized?(obj, args, ctx)
    # if `require_admin:` was given, then require the current user to be an admin
    super && (@require_admin ? ctx[:viewer]&.admin? : true)
  end
end

For this to work, the base field class must be configured with other GraphQL types.

Argument Authorization

Argument #authorized? hooks are called before resolving the field that the argument belongs to. For example:

ruby
class Types::BaseArgument < GraphQL::Schema::Argument
  def initialize(*args, require_logged_in: false, **kwargs, &block)
    @require_logged_in = require_logged_in
    super(*args, **kwargs, &block)
  end

  def authorized?(obj, arg_value, ctx)
    super && if @require_logged_in
      ctx[:viewer].present?
    else
      true
    end
  end
end

For this to work, the base argument class must be configured with other GraphQL types.

Mutation Authorization

See Mutation Authorization in the Mutation Guides.

Enum Value Authorization

GraphQL::Schema::EnumValue#authorized? is called when client input is received and when the schema returns values to the client.

For authorizing input, if a value's #authorized? method returns false, then a GraphQL::UnauthorizedEnumValueError is raised. It passed to your schema's .unauthorized_object hook, where you can handle it another way if you want.

For authorizing return values, if an outgoing value's #authorized? method returns false, then a GraphQL::Schema::Enum::UnresolvedValueError is raised, which crashes the query. In this case, you should modify your field or resolver to not return this value to an unauthorized viewer. (In this case, the error isn't returned to the viewer because the viewer can't do anything about it -- it's a developer-facing issue instead.)

Handling Unauthorized Objects

By default, GraphQL-Ruby silently replaces unauthorized objects with nil, as if they didn't exist. You can customize this behavior by implementing Schema.unauthorized_object in your schema class, for example:

ruby
class MySchema < GraphQL::Schema
  # Override this hook to handle cases when `authorized?` returns false for an object:
  def self.unauthorized_object(error)
    # Add a top-level error to the response instead of returning nil:
    raise GraphQL::ExecutionError, "An object of type #{error.type.graphql_name} was hidden due to permissions"
  end
end

Now, the custom hook will be called instead of the default one.

If .unauthorized_object returns a non-nil object (and doesn't raise an error), then that object will be used in place of the unauthorized object.

A similar hook is available for unauthorized fields:

ruby
class MySchema < GraphQL::Schema
  # Override this hook to handle cases when `authorized?` returns false for a field:
  def self.unauthorized_field(error)
    # Add a top-level error to the response instead of returning nil:
    raise GraphQL::ExecutionError, "The field #{error.field.graphql_name} on an object of type #{error.type.graphql_name} was hidden due to permissions"
  end
end

Guides Authorization Can Can Integration (`guides/authorization/can_can_integration.md`)

CanCan Integration

GraphQL::Pro includes an integration for powering GraphQL authorization with CanCan.

Why bother? You could put your authorization code in your GraphQL types themselves, but writing a separate authorization layer gives you a few advantages:

  • Since the authorization code isn't embedded in GraphQL, you can use the same logic in non-GraphQL (or legacy) parts of the app.
  • The authorization logic can be tested in isolation, so your end-to-end GraphQL tests don't have to cover as many possibilities.

Getting Started

NOTE: Requires the latest gems, so make sure your Gemfile has:

ruby
# For CanCanIntegration:
gem "graphql-pro", ">=1.7.11"
# For list scoping:
gem "graphql", ">=1.8.7"

Then, bundle install.

Whenever you run queries, include :current_user in the context:

ruby
context = {
  current_user: current_user,
  # ...
}
MySchema.execute(..., context: context)

Rails Generator

If your schema files follow the same convention as rails generate graphql:install, then you can install the CanCan integration with a Rails generator:

bash
$ rails generate graphql:cancan:install

This will insert all the necessary include ...s described below. Alternatively, check the docs below to mix in CanCanIntegration's modules.

Authorizing Objects

For each object type, you can assign a required action for Ruby objects of that type. To get started, include the ObjectIntegration in your base object class:

ruby
# app/graphql/types/base_object.rb
class Types::BaseObject < GraphQL::Schema::Object
  # Add the CanCan integration:
  include GraphQL::Pro::CanCanIntegration::ObjectIntegration
  # By default, require `can :read, ...`
  can_can_action(:read)
  # Or, to require no permissions by default:
  # can_can_action(nil)
end

Now, anyone fetching an object will need can :read, ... for that object.

CanCan configurations are inherited, and can be overridden in subclasses. For example, to allow all viewers to see the Query root type:

ruby
class Types::Query < Types::BaseObject
  # Allow anyone to see the query root
  can_can_action nil
end

Bypassing CanCan

can_can_action(nil) will override any inherited configuration and skip CanCan checks for an object, field, argument or mutation.

Handling Unauthorized Objects

When any CanCan check returns false, the unauthorized object is passed to Schema.unauthorized_object, as described in Handling unauthorized objects.

Scopes

ActiveRecord::Relation

The CanCan integration adds CanCan's .accessible_by to GraphQL-Ruby's list scoping

To scope lists of interface or union type, include the integration in your base union class and base interface module and set a base can_can_action, if desired:

ruby
class BaseUnion < GraphQL::Schema::Union
  include GraphQL::Pro::CanCanIntegration::UnionIntegration
  # To provide a default action for scoping lists:
  can_can_action :read
end

module BaseInterface
  include GraphQL::Schema::Interface
  include GraphQL::Pro::CanCanIntegration::InterfaceIntegration
  # To provide a default action for scoping lists:
  can_can_action :read
end

Array

For Arrays, the CanCan integration will use .select { ... } to filter items using the can_can_action from the lists's type.

Bypassing scopes

To allow an unscoped relation to be returned from a field, disable scoping with scope: false, for example:

ruby
# Allow anyone to browse the job postings
field :job_postings, [Types::JobPosting], null: false,
  scope: false

Authorizing Fields

You can also require certain checks on a field-by-field basis. First, include the integration in your base field class:

ruby
# app/graphql/types/base_field.rb
class Types::BaseField < GraphQL::Schema::Field
  # Add the CanCan integration:
  include GraphQL::Pro::CanCanIntegration::FieldIntegration
  # By default, don't require a role at field-level:
  can_can_action nil
end

If you haven't already done so, you should also hook up your base field class to your base object and base interface:

ruby
# app/graphql/types/base_object.rb
class Types::BaseObject < GraphQL::Schema::Object
  field_class Types::BaseField
end
# app/graphql/types/base_interface.rb
module Types::BaseInterface
  # ...
  field_class Types::BaseField
end
# app/graphql/mutations/base_mutation.rb
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  field_class Types::BaseField
end

Then, you can add can_can_action: options to your fields:

ruby
class Types::JobPosting < Types::BaseObject
  # Only allow `can :review_applications, JobPosting` users
  # to see who has applied
  field :applicants, [Types::User],
    can_can_action: :review_applicants
end

It will require the named action (:review_applicants) for the object being viewed (a JobPosting).

Authorizing by attribute

CanCan 3.0 added attribute-level authorization (pull request). You can leverage this in your field definitions with the can_can_attribute: configuration:

ruby
# This will call `.can?(:read, user, :email_address)`
field :email_address, String,
  can_can_action: :read,
  can_can_attribute: :email_address

You could also provide a default value for can_can_attribute in your base field class:

ruby
class Types::BaseField
  def initialize(*args, **kwargs, &block)
    # pass all configs to the super class:
    super
    # Then set a new `can_can_attribute` value, if applicable
    if can_can_attribute.nil? && can_can_action.present?
      # `method_sym` is the thing GraphQL-Ruby will use to resolve this field
      can_can_attribute(method_sym)
    end
  end
end

(See GraphQL::Schema::Field for the different values available for defaults.)

Providing a Custom CanCan Subject

Authorization checks are skipped whenever the underlying object is nil. This can happen in root query fields, for example, when no root_value: ... is given. To provide a can_can_subject in this case, you can add it as a field configuration:

ruby
field :users, Types::User.connection_type, null: false,
  can_can_action: :manage,
  # `:all` will be used instead of `object` (which is `nil`)
  can_can_subject: :all

The configuration above will call can?(:manage, :all) whenever that field is requested.

Authorizing Arguments

Similar to field-level checks, you can require certain permissions to use certain arguments. To do this, add the integration to your base argument class:

ruby
class Types::BaseArgument < GraphQL::Schema::Argument
  # Include the integration and default to no permissions required
  include GraphQL::Pro::CanCanIntegration::ArgumentIntegration
  can_can_action nil
end

Then, make sure your base argument is hooked up to your base field and base input object:

ruby
class Types::BaseField < GraphQL::Schema::Field
  argument_class Types::BaseArgument
  # PS: see "Authorizing Fields" to make sure your base field is hooked up to objects, interfaces and mutations
end

class Types::BaseInputObject < GraphQL::Schema::InputObject
  argument_class Types::BaseArgument
end

class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  argument_class Types::BaseArgument
end

Now, arguments accept a can_can_action: option, for example:

ruby
class Types::Company < Types::BaseObject
  field :employees, Types::Employee.connection_type do
    # Only admins can filter employees by email:
    argument :email, String, required: false, can_can_action: :admin
  end
end

This will check for can :admin, Company (or a similar rule for the company being queried) for the current user.

Authorizing Mutations

There are a few ways to authorize GraphQL mutations with the CanCan integration:

Also, you can configure unauthorized object handling

Setup

Add MutationIntegration to your base mutation, for example:

ruby
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  include GraphQL::Pro::CanCanIntegration::MutationIntegration

  # Also, to use argument-level authorization:
  argument_class Types::BaseArgument
end

Also, you'll probably want a BaseMutationPayload where you can set a default role:

ruby
class Types::BaseMutationPayload < Types::BaseObject
  # If `BaseObject` requires some permissions, override that for mutation results.
  # Assume that anyone who can run a mutation can read their generated result types.
  can_can_action nil
end

And hook it up to your base mutation:

ruby
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  object_class Types::BaseMutationPayload
  field_class Types::BaseField
end

Mutation-level roles

Each mutation can have a class-level can_can_action which will be checked before loading objects or resolving, for example:

ruby
class Mutations::PromoteEmployee < Mutations::BaseMutation
  can_can_action :run_mutation
end

In the example above, can :run_mutation, Mutations::PromoteEmployee will be checked before running the mutation. (The currently-running instance of Mutations::PromoteEmployee is passed to the ability checker.)

Authorizing Loaded Objects

Mutations can automatically load and authorize objects by ID using the loads: option.

Beyond the normal object reading permissions, you can add an additional role for the specific mutation input using a can_can_action: option:

ruby
class Mutations::FireEmployee < Mutations::BaseMutation
  argument :employee_id, ID,
    loads: Types::Employee,
    can_can_action: :supervise,
end

In the case above, the mutation will halt unless the can :supervise, ... check returns true. (The fetched instance of Employee is passed to the ability checker.)

Unauthorized Mutations

By default, an authorization failure in a mutation will raise a Ruby exception. You can customize this by implementing #unauthorized_by_can_can(owner, value) in your base mutation, for example:

ruby
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  def unauthorized_by_can_can(owner, value)
    # No error, just return nil:
    nil
  end
end

The method is called with:

  • owner: the GraphQL::Schema::Argument instance or mutation class whose role was not satisfied
  • value: the object which didn't pass for context[:current_user]

Since it's a mutation method, you can also access context in that method.

Whatever that method returns will be treated as an early return value for the mutation, so for example, you could return errors as data:

ruby
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  field :errors, [String]

  def unauthorized_by_can_can(owner, value)
    # Return errors as data:
    { errors: ["Missing required permission: #{owner.can_can_action}, can't access #{value.inspect}"] }
  end
end

Authorizing Resolvers

Resolvers are authorized just like mutations, and require similar setup:

ruby
# app/graphql/resolvers/base_resolver.rb
class Resolvers::BaseResolver < GraphQL::Schema::Resolver
  include GraphQL::Pro::CanCanIntegration::ResolverIntegration
  argument_class BaseArgument
  # can_can_action(nil) # to disable authorization by default
end

Beyond that, see Authorizing Mutations above for further details.

Custom Abilities Class

By default, the integration will look for a top-level ::Ability class.

If you're using a different class, provide an instance ahead-of-time as context[:can_can_ability]

For example, you could always add one in your schema's #execute method:

ruby
class MySchema < GraphQL::Schema
  # Override `execute` to provide a custom Abilities instance for the CanCan integration
  def self.execute(*args, context: {}, **kwargs)
    # Assign `context[:can_can_ability]` to an instance of our custom class
    context[:can_can_ability] = MyAuthorization::CustomAbilitiesClass.new(context[:current_user])
    super
  end
end

Guides Authorization Pundit Integration (`guides/authorization/pundit_integration.md`)

Pundit Integration

GraphQL::Pro includes an integration for powering GraphQL authorization with Pundit policies.

Why bother? You could put your authorization code in your GraphQL types themselves, but writing a separate authorization layer gives you a few advantages:

  • Since the authorization code isn't embedded in GraphQL, you can use the same logic in non-GraphQL (or legacy) parts of the app.
  • The authorization logic can be tested in isolation, so your end-to-end GraphQL tests don't have to cover as many possibilities.

Getting Started

NOTE: Requires the latest gems, so make sure your Gemfile has:

ruby
# For PunditIntegration:
gem "graphql-pro", ">=1.7.9"
# For list scoping:
gem "graphql", ">=1.8.7"

Then, bundle install.

Whenever you run queries, include :current_user in the context:

ruby
context = {
  current_user: current_user,
  # ...
}
MySchema.execute(..., context: context)

Rails Generator

If your schema files follow the same convention as rails generate graphql:install, then you can install the Pundit integration with a Rails generator:

bash
$ rails generate graphql:pundit:install

This will insert all the necessary include ...s described below. Alternatively, check the docs below to mix in PunditIntegration's modules.

Authorizing Objects

You can specify Pundit roles that must be satisfied in order for viewers to see objects of a certain type. To get started, include the ObjectIntegration in your base object class:

ruby
# app/graphql/types/base_object.rb
class Types::BaseObject < GraphQL::Schema::Object
  # Add the Pundit integration:
  include GraphQL::Pro::PunditIntegration::ObjectIntegration
  # By default, require staff:
  pundit_role :staff
  # Or, to require no permissions by default:
  # pundit_role nil
end

Now, anyone trying to read a GraphQL object will have to pass the #staff? check on that object's policy.

Then, each child class can override that parent configuration. For example, allow all viewers to read the Query root:

ruby
class Types::Query < Types::BaseObject
  # Allow anyone to see the query root
  pundit_role nil
end

Policies and Methods

For each object returned by GraphQL, the integration matches it to a policy and method.

The policy is found using Pundit.policy!, which looks up a policy using the object's class name. (This can be customized, see below.)

Then, GraphQL will call a method on the policy to see whether the object is permitted or not. This method is assigned in the object class, for example:

ruby
class Types::Employee < Types::BaseObject
  # Only show employee objects to their bosses,
  # or when that employee is the current viewer
  pundit_role :employer_or_self
  # ...
end

That configuration will call #employer_or_self? on the corresponding Pundit policy.

Custom Policy Class

By default, the integration uses Pundit.policy!(current_user, object) to find a policy. You can specify a policy class using pundit_policy_class(...):

ruby
class Types::Employee < Types::BaseObject
  pundit_policy_class(Policies::CustomEmployeePolicy)
  # Or, you could use a string:
  # pundit_policy_class("Policies::CustomEmployeePolicy")
end

For really custom policy lookup, see Custom Policy Lookup below.

Bypassing Policies

The integration requires that every object with a pundit_role has a corresponding policy class. To allow objects to skip authorization, you can pass nil as the role:

ruby
class Types::PublicProfile < Types::BaseObject
  # Anyone can see this
  pundit_role nil
end

Handling Unauthorized Objects

When any Policy method returns false, the unauthorized object is passed to Schema.unauthorized_object, as described in Handling unauthorized objects.

Scopes

The Pundit integration adds Pundit scopes to GraphQL-Ruby's list scoping feature. Any list or connection will be scoped. If a scope is missing, the query will crash rather than risk leaking unfiltered data.

To scope lists of interface or union type, include the integration in your base union class and base interface module:

ruby
class BaseUnion < GraphQL::Schema::Union
  include GraphQL::Pro::PunditIntegration::UnionIntegration
end

module BaseInterface
  include GraphQL::Schema::Interface
  include GraphQL::Pro::PunditIntegration::InterfaceIntegration
end

Bypassing scopes

To allow an unscoped relation to be returned from a field, disable scoping with scope: false, for example:

ruby
# Allow anyone to browse the job postings
field :job_postings, [Types::JobPosting], null: false,
  scope: false

Authorizing Fields

You can also require certain checks on a field-by-field basis. First, include the integration in your base field class:

ruby
# app/graphql/types/base_field.rb
class Types::BaseField < GraphQL::Schema::Field
  # Add the Pundit integration:
  include GraphQL::Pro::PunditIntegration::FieldIntegration
  # By default, don't require a role at field-level:
  pundit_role nil
end

If you haven't already done so, you should also hook up your base field class to your base object and base interface:

ruby
# app/graphql/types/base_object.rb
class Types::BaseObject < GraphQL::Schema::Object
  field_class Types::BaseField
end
# app/graphql/types/base_interface.rb
module Types::BaseInterface
  # ...
  field_class Types::BaseField
end
# app/graphql/mutations/base_mutation.rb
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  field_class Types::BaseField
end

Then, you can add pundit_role: options to your fields:

ruby
class Types::JobPosting < Types::BaseObject
  # Allow signed-in users to browse listings
  pundit_role :signed_in

  # But, only allow `JobPostingPolicy#staff?` users to see
  # who has applied
  field :applicants, [Types::User],
    pundit_role: :staff
end

It will call the named role (eg, #staff?) on the parent object's policy (eg JobPostingPolicy).

Custom Policy Class

You can override the policy class for a field using pundit_policy_class:, for example:

ruby
class Types::JobPosting < Types::BaseObject
  # Only allow `ApplicantsPolicy#staff?` users to see
  # who has applied
  field :applicants, [Types::User],
    pundit_role: :staff,
    pundit_policy_class: ApplicantsPolicy
    # Or with a string:
    # pundit_policy_class: "ApplicantsPolicy"
end

This will initialize an ApplicantsPolicy with the parent object (a Job) and call #staff? on it.

For really custom policy lookup, see Custom Policy Lookup below.

Authorizing Arguments

Similar to field-level checks, you can require certain permissions to use certain arguments. To do this, add the integration to your base argument class:

ruby
class Types::BaseArgument < GraphQL::Schema::Argument
  # Include the integration and default to no permissions required
  include GraphQL::Pro::PunditIntegration::ArgumentIntegration
  pundit_role nil
end

Then, make sure your base argument is hooked up to your base field and base input object:

ruby
class Types::BaseField < GraphQL::Schema::Field
  argument_class Types::BaseArgument
  # PS: see "Authorizing Fields" to make sure your base field is hooked up to objects, interfaces and mutations
end

class Types::BaseInputObject < GraphQL::Schema::InputObject
  argument_class Types::BaseArgument
end

class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  argument_class Types::BaseArgument
end

Now, arguments accept a pundit_role: option, for example:

ruby
class Types::Company < Types::BaseObject
  field :employees, Types::Employee.connection_type do
    # Only admins can filter employees by email:
    argument :email, String, required: false, pundit_role: :admin
  end
end

The role will be called on the parent object's policy, for example CompanyPolicy#admin? in the case above.

Authorizing Mutations

There are a few ways to authorize GraphQL mutations with the Pundit integration:

Also, you can configure unauthorized object handling

Setup

Add MutationIntegration to your base mutation, for example:

ruby
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  include GraphQL::Pro::PunditIntegration::MutationIntegration

  # Also, to use argument-level authorization:
  argument_class Types::BaseArgument
end

Also, you'll probably want a BaseMutationPayload where you can set a default role:

ruby
class Types::BaseMutationPayload < Types::BaseObject
  # If `BaseObject` requires some permissions, override that for mutation results.
  # Assume that anyone who can run a mutation can read their generated result types.
  pundit_role nil
end

And hook it up to your base mutation:

ruby
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  object_class Types::BaseMutationPayload
  field_class Types::BaseField
end

Mutation-level roles

Each mutation can have a class-level pundit_role which will be checked before loading objects or resolving, for example:

ruby
class Mutations::PromoteEmployee < Mutations::BaseMutation
  pundit_role :admin
end

In the example above, PromoteEmployeePolicy#admin? will be checked before running the mutation.

Custom Policy Class

By default, Pundit uses the mutation's class name to look up a policy. You can override this by defining pundit_policy_class on your mutation:

ruby
class Mutations::PromoteEmployee < Mutations::BaseMutation
  pundit_policy_class ::UserPolicy
  pundit_role :admin
end

Now, the mutation will check UserPolicy#admin? before running.

For really custom policy lookup, see Custom Policy Lookup below.

Authorizing Loaded Objects

Mutations can automatically load and authorize objects by ID using the loads: option.

Beyond the normal object reading permissions, you can add an additional role for the specific mutation input using a pundit_role: option:

ruby
class Mutations::FireEmployee < Mutations::BaseMutation
  argument :employee_id, ID,
    loads: Types::Employee,
    pundit_role: :supervisor,
end

In the case above, the mutation will halt unless the EmployeePolicy#supervisor? method returns true.

Unauthorized Mutations

By default, an authorization failure in a mutation will raise a Ruby exception. You can customize this by implementing #unauthorized_by_pundit(owner, value) in your base mutation, for example:

ruby
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  def unauthorized_by_pundit(owner, value)
    # No error, just return nil:
    nil
  end
end

The method is called with:

  • owner: the GraphQL::Schema::Argument or mutation class whose role was not satisfied
  • value: the object which didn't pass for context[:current_user]

Since it's a mutation method, you can also access context in that method.

Whatever that method returns will be treated as an early return value for the mutation, so for example, you could return errors as data:

ruby
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  field :errors, [String]

  def unauthorized_by_pundit(owner, value)
    # Return errors as data:
    { errors: ["Missing required permission: #{owner.pundit_role}, can't access #{value.inspect}"] }
  end
end

Authorizing Resolvers

Resolvers are authorized just like mutations, and require similar setup:

ruby
# app/graphql/resolvers/base_resolver.rb
class Resolvers::BaseResolver < GraphQL::Schema::Resolver
  include GraphQL::Pro::PunditIntegration::ResolverIntegration
  argument_class BaseArgument
  # pundit_role nil # to disable authorization by default
end

Beyond that, see Authorizing Mutations above for further details.

Custom Policy Lookup

By default, the integration uses Pundit's top-level methods to interact with policies:

  • Pundit.policy!(context[:current_user], object) is called to find a policy instance
  • Pundit.policy_scope!(context[:current_user], items) is called to filter items

Custom Policy Methods

You can implement a custom lookup by defining the following methods in your schema:

  • pundit_policy_class_for(object, context) to return a policy class (or raise an error if one isn't found)
  • pundit_role_for(object, context) to return a role method (Symbol), or nil to bypass authorization
  • scope_by_pundit_policy(context, items) to apply a scope to items (or raise an error if one isn't found)

Since different objects have different lifecycles, the hooks are installed slightly different ways:

  • Your base argument, field, and mutation classes should have instance methods with those names
  • Your base type classes should have class methods with that name

Here's an example of how the custom hooks can be installed:

ruby
module CustomPolicyLookup
  # Lookup policies in the `SystemAdmin::` namespace for system_admin users
  # @return [Class]
  def pundit_policy_class_for(object, context)
    current_user = context[:current_user]
    if current_user.system_admin?
      SystemAdmin.const_get("#{object.class.name}Policy")
    else
      super
    end
  end

  # Require admin permissions if the object is pending_approval
  def pundit_role_for(object, context)
    if object.pending_approval?
      :admin
    else
      super # fall back to the normally-configured role
    end
  end
end

# Add policy hooks as class methods
class Types::BaseObject < GraphQL::Schema::Object
  extend CustomPolicyLookup
end
class Types::BaseUnion < GraphQL::Schema::Union
  extend CustomPolicyLookup
end
module Types::BaseInterface
  include GraphQL::Schema::Interface
  # Add this as a class method that will be "inherited" by other interfaces:
  definition_methods do
    include CustomPolicyLookup
  end
end

# Add policy hooks as instance methods
class Types::BaseField < GraphQL::Schema::Field
  include CustomPolicyLookup
end
class Types::BaseArgument < GraphQL::Schema::Argument
  include CustomPolicyLookup
end
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  include CustomPolicyLookup
end

One Policy Per Class

Another good approach is to have one policy per class. You can implement policy_class_for(object, context) to look up a policy within the class, for example:

ruby
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  def policy_class_for(_object, _context)
    # Look up a nested `Policy` constant:
    self.class.const_get(:Policy)
  end
end

Then, each mutation can define its policy inline, for example:

ruby
class Mutations::PromoteEmployee < Mutations::BaseMutation
  # This will be found by `BaseMutation.policy_class`, defined above:
  class Policy
    # ...
  end

  pundit_role :admin
end

Now, Mutations::PromoteEmployee::Policy#admin? will be checked before running the mutation.

Custom User Lookup

By default, the Pundit integration looks for the current user in context[:current_user]. You can override this by implementing #pundit_user on your custom query context class. For example:

ruby
# app/graphql/query_context.rb
class QueryContext < GraphQL::Query::Context
  def pundit_user
    # Lookup `context[:viewer]` instead:
    self[:viewer]
  end
end

Then be sure to hook up your custom class in the schema:

ruby
class MySchema < GraphQL::Schema
  context_class(QueryContext)
end

Then, the Pundit integration will use your def pundit_user to get the current user at runtime.


Guides Authorization Scoping (`guides/authorization/scoping.md`)

Scoping

Scoping is a complementary consideration to authorization. Rather than checking "can this user see this thing?", scoping takes a list of items filters it to the subset which is appropriate for the current viewer and context.

For similar features, see Pundit scopes and Cancan's .accessible_by.

`scope:` option

Fields accept a scope: option to enable (or disable) scoping, for example:

ruby
field :products, [Types::Product], scope: true
# Or
field :all_products, [Types::Product], scope: false

For list and connection fields, scope: true is the default. For all other fields, scope: false is the default. You can override this by using the scope: option.

`.scope_items(items, ctx)` method

Type classes may implement .scope_items(items, ctx). This method is called when a field has scope: true. For example,

ruby
field :products, [Types::Product] # has `scope: true` by default

Will call:

ruby
class Types::Product < Types::BaseObject
  def self.scope_items(items, context)
    # filter items here
  end
end

The method should return a new list with only the appropriate items for the current context.

Bypassing object-level authorization

If you know that any items returned from .scope_items should be visible to the current client, you can skip the normal .authorized?(obj, ctx) checks by configuring reauthorize_scoped_objects(false) in your type definition. For example:

ruby
class Types::Product < Types::BaseObject
  # Check that singly-loaded objects are visible to the current viewer
  def self.authorized?(object, context)
    super && object.visible_to?(context[:viewer])
  end

  # Filter any list to only include objects that are visible to the current viewer
  def self.scope_items(items, context)
    items = super(items, context)
    items.visible_for(context[:viewer])
  end

  # If an object of this type was returned from `.scope_items`,
  # don't call `.authorized?` with it.
  reauthorize_scoped_objects(false)
end

Guides Authorization Visibility (`guides/authorization/visibility.md`)

Visibility

With GraphQL-Ruby, it's possible to hide parts of your schema from some users. This isn't exactly part of the GraphQL spec, but it's roughly within the bounds of the spec.

Here are some reasons you might want to hide parts of your schema:

  • You don't want non-admin users to know about administration functions of the schema.
  • You're developing a new feature and want to make a gradual release to only a few users first.

Hiding Parts of the Schema

To start limiting visibility of your schema, add the plugin:

ruby
class MySchema < GraphQL::Schema
  # ...
  use GraphQL::Schema::Visibility # see below for options
end

Then, you can customize the visibility of parts of your schema by reimplementing various visible? methods:

  • Type classes have a .visible?(context) class method
  • Fields and arguments have a #visible?(context) instance method
  • Enum values have #visible?(context) instance method
  • Mutation classes have a .visible?(context) class method

These methods are called with the query context, based on the hash you pass as context:. If the method returns false, then that member of the schema will be treated as though it doesn't exist for the entirety of the query. That is:

  • In introspection, the member will not be included in the result
  • In normal queries, if a query references that member, it will return a validation error, since that member doesn't exist

Visibility Profiles

You can use named profiles to cache your schema's visibility modes. For example:

ruby
use GraphQL::Schema::Visibility, profiles: {
  # mode_name => example_context_hash
  public: { public: true },
  beta: { public: true, beta: true },
  internal_admin: { internal_admin: true }
}

Then, you can run queries with context[:visibility_profile] equal to one of the pre-defined profiles. When you do, GraphQL-Ruby will create a cached set of types for named profile. .visible? will only be called with the context hash passed to profiles: ....

The profile contexts passed to profiles will have visibility_profile: ... added to them, then they're frozen by GraphQL-Ruby.

Preloading profiles

By default, GraphQL-Ruby will preload all named visibility profiles when Rails.env.production? is present and true. You can manually set this option by passing use ... preload: true (or false). Enable preloading in production to reduce latency of the first request to each visibility profile. Disable preloading in development to speed up application boot.

Dynamic profiles

When you provide named visibility profiles, context[:visibility_profile] is required for query execution. You can also permit dynamic visibility for queries which don't have that key set by passing use ..., dynamic: true. You could use this to support backwards compatibility or when visibility calculations are too complex to predefine.

When no named profiles are defined, all queries use dynamic visibility.

Object Visibility

Let's say you're working on a new feature which should remain secret for a while. You can implement .visible? in a type:

ruby
class Types::SecretFeature < Types::BaseObject
  def self.visible?(context)
    # only show it to users with the secret_feature enabled
    super && context[:viewer].feature_enabled?(:secret_feature)
  end
end

(Always call super to inherit the default behavior.)

Now, the following bits of GraphQL will return validation errors:

  • Fields that return SecretFeature, eg query { findSecretFeature { ... } }
  • Fragments on SecretFeature, eg Fragment SF on SecretFeature

And in introspection:

  • __schema { types { ... } } will not include SecretFeature
  • __type(name: "SecretFeature") will return nil
  • Any interfaces or unions which normally include SecretFeature will not include it
  • Any fields that return SecretFeature will be excluded from introspection

Field Visibility

ruby
class Types::BaseField < GraphQL::Schema::Field
  # Pass `field ..., require_admin: true` to hide this field from non-admin users
  def initialize(*args, require_admin: false, **kwargs, &block)
    @require_admin = require_admin
    super(*args, **kwargs, &block)
  end

  def visible?(ctx)
    # if `require_admin:` was given, then require the current user to be an admin
    super && (@require_admin ? ctx[:viewer]&.admin? : true)
  end
end

For this to work, the base field class must be configured with other GraphQL types.

Argument Visibility

ruby
class Types::BaseArgument < GraphQL::Schema::Argument
  # If `require_logged_in: true` is given, then this argument will be hidden from logged-out viewers
  def initialize(*args, require_logged_in: false, **kwargs, &block)
    @require_logged_in = require_logged_in
    super(*args, **kwargs, &block)
  end

  def visible?(ctx)
    super && (@require_logged_in ? ctx[:viewer].present? : true)
  end
end

For this to work, the base argument class must be configured with other GraphQL types.

Opting Out

By default, GraphQL-Ruby always runs visibility checks. You can opt out of this by adding to your schema class:

ruby
class MySchema < GraphQL::Schema
  # ...
  # Opt out of GraphQL-Ruby's visibility feature:
  use GraphQL::Schema::AlwaysVisible
end

For big schemas, this can be a worthwhile speed-up.

Migration Notes

GraphQL::Schema::Visibility is a new implementation of visibility in GraphQL-Ruby. It has some slight differences from the previous implementation (GraphQL::Schema::Warden):

  • Visibility speeds up Rails app boot because it doesn't require all types to be loaded during boot and only loads types as they are used by queries.
  • Visibility supports predefined, reusable visibility profiles which speeds up queries using complicated visible? checks.
  • Visibility hides types differently in a few edge cases:
    • Previously, Warden hid interface and union types which had no possible types. Visibility doesn't check possible types (in order to support performance improvements), so those types must return false for visible? in the same cases where all possible types were hidden. Otherwise, that interface or union type will be visible but have no possible types.
    • When an object type is connected to the schema as a field return type or a union member, and also implements and interface, if the object type's other connection(s) to the schema are hidden, then it won't appear as an implementer of that interface unless it's registered with orphan_types (either by the schema or interface). Warden used a "global" map of types so it could discover object types in this case, but Visibility doesn't have that global map. (Since time of writing, Visibility does have some global type tracking, so maybe this could be fixed.)
  • When Visibility is used, several (Ruby-level) Schema introspection methods don't work because the caches they draw on haven't been calculated (Schema.references_to, Schema.union_memberships). If you're using these, please get in touch so that we can find a way forward.
  • If you programmatically generate your GraphQL schema (instead of using plain-Ruby .rb files), you must ensure that generation is threadsafe. Otherwise, definitions may be malformed in Rails development mode when multithreaded. (Rails eager-loads all files in production mode, so this concern doesn't exist there.)

Migration Mode

You can use use GraphQL::Schema::Visibility, ... migration_errors: true to enable migration mode. In this mode, GraphQL-Ruby will make visibility checks with both Visibility and Warden and compare the result, raising a descriptive error when the two systems return different results. As you migrate to Visibility, enable this mode in test to find any unexpected discrepancies.

Sometimes, there's a discrepancy that is hard to resolve but doesn't make any real difference in application behavior. To address these cases, you can use these flags in context:

  • context[:visibility_migration_running] = true is set in the main query context.
  • context[:visibility_migration_warden_running] = true is set in the duplicate context which is passed to a Warden instance.
  • If you set context[:skip_migration_error] = true, then no migration error will be raised for that query.

You can use these flags to conditionally handle edge cases that should be ignored in testing.


Guides Changesets Definition (`guides/changesets/definition.md`)

Defining Changesets

After installing Changeset integrations in your schema, you can create Changesets which modify parts of the schema. Changesets extend GraphQL::Enterprise::Changeset and include a release string. Once a Changeset class is defined, it can be referenced with added_in: ... or removed_in: ... configurations in the schema.

Note: Before GraphQL-Enterprise 1.3.0, Changesets were configured with modifies ... blocks. These blocks are still supported and you can find the documentation for that API on GitHub.

Changeset Classes

This Changeset will be available to any client whose context[:changeset_version] is on or after 2020-12-01:

ruby
# app/graphql/changesets/deprecate_recipe_flag.rb
class Changesets::DeprecateRecipeTags < GraphQL::Enterprise::Changeset
  release "2020-12-01"
end

Additionally, Changesets must be released for their changes to be published.

Publishing with `added_in:`

New things can be published in a changeset by adding added_in: SomeChangeset to their configuration. For example, to add a new argument to a field:

ruby
field :search_recipes, [Types::Recipe] do
  argument :query, String
  argument :tags, [Types::RecipeTag], required: false, added_in: Changesets::AddRecipeTags
end

You can also provide a replacement implementation by using added_in:. When a new definition has the same name as an existing definition, it implicitly replaces the previous definition in new versions of the API. For example:

ruby
field :rating, Integer, "A 1-5 score for this recipe" # This definition will be superseded by the following one
field :rating, Float, "A 1.0-5.0 score for this recipe", added_in: Changesets::FloatingPointRatings

Here, a new implementation for rating will be used when clients requests an API version that includes Changesets::FloatingPointRatings. (If the client requests a version before that changeset, then the preceding implementation would be used instead.)

Removing with `removed_in:`

A removed_in: configuration removes something in the named changeset. For example, these enum values are replaced with more clearly-named ones:

ruby
class Types::RecipeTag < Types::BaseEnum
  # These are replaced by *_HEAT below:
  value :SPICY, removed_in: Changesets::ClarifyHeatTags
  value :MEDIUM, removed_in: Changesets::ClarifyHeatTags
  value :MILD, removed_in: Changesets::ClarifyHeatTags
  # These new tags are more clear:
  value :SPICY_HEAT, added_in: Changesets::ClarifyHeatTags
  value :MEDIUM_HEAT, added_in: Changesets::ClarifyHeatTags
  value :MILD_HEAT, added_in: Changesets::ClarifyHeatTags
end

If something has been defined several times, a removed_in: configuration removes all definitions:

ruby
class Mutations::SubmitRecipeRating < Mutations::BaseMutation
  # This is replaced in future API versions by the following argument
  argument :rating, Integer
  # This replaces the previous, but in another future version,
  # it is removed completely (and so is the previous one)
  argument :rating, Float, added_in: Changesets::FloatingPointRatings, removed_in: Changesets::RemoveRatingsCompletely
end

Examples

See below for the different kind of modifications you can make in a changeset:

  • Fields: adding, modifying, and removing fields
  • Arguments: adding, modifying, and removing arguments
  • Enum values: adding, modifying, and removing arguments
  • Unions: adding or removing object types from a union
  • Interfaces: adding or removing interface implementations from object types
  • Types: changing one type definition for another
  • Runtime: choosing a behavior at runtime based on the current request and changeset

Fields

To add or redefine a field, use field(..., added_in: ...), including all configuration values for the new implementation (see the GraphQL::Schema::Field API). The definition given here will override the previous definition (if there was one) whenever this Changeset applies.

ruby
class Types::Recipe < Types::BaseObject
  # This new field is available when `context[:changeset_version]`
  # is on or after the release date of `AddRecipeTags`
  field :tags, [Types::RecipeTag], added_in: Changeset::AddRecipeTags
end

To remove a field, add a removed_in: ... configuration to the last definition of the field:

ruby
class Types::Recipe < Types::BaseObject
  # Even after migrating to floating point values,
  # the "rating" feature never took off,
  # so we removed it entirely eventually.
  field :rating, Integer
  field :rating, Float, added_in: Changeset::FloatingPointRatings,
    removed_in: Changeset::RemoveRatings
end

When a field is removed, queries that request that field will be invalid, unless the client has requested a previous API version where the field is still available.

Arguments

You can add, redefine, or remove arguments that belong to fields, input objects, or resolvers. Use added_in: ... to provide a new (or updated) definition for an argument, for example:

ruby
class Types::RecipesFilter < Types::BaseInputObject
  argument :rating, Integer
  # This new definition is available when
  # the client's `context[:changeset_version]` includes `FloatingPointRatings`
  argument :rating, Float, added_in: Changesets::FloatingPointRatings
end

To remove an argument entirely, add a removed_in: ... configuration to the last definition. It will remove all implementations for that argument. For example:

ruby
class Mutations::SubmitRating < Mutations::BaseMutation
  # Remove this because it's irrelevant:
  argument :phone_number, String, removed_in: Changesets::StopCollectingPersonalInformation
end

When arguments are removed, the schema will reject any queries which use them unless the client has requested a previous API version where the argument is still allowed.

Enum Values

With Changesets, you can add, redefine, or remove enum values. To add a new value (or provide a new implementation for a value), include added_in: in the value(...) configuration:

ruby
class Types::RecipeTag < Types::BaseEnum
  # This enum will accept and return `KETO` only when the client's API version
  # includes `AddKetoDietSupport`'s release date.
  value :KETO, added_in: Changesets::AddKetoDietSupport
end

Values can be removed with removed_in:, for example:

ruby
class Types::RecipeTag < Types::BaseEnum
  # Old API versions will serve this value;
  # new versions won't accept it or return it.
  value :GRAPEFRUIT_DIET, removed_in: Changesets::RemoveLegacyDiets
end

When enum values are removed, they won't be accepted as input and they won't be allowed as return values from fields unless the client has requested a previous API version where those values are still allowed.

Unions

You can add to or remove from a union's possible types. To release a new union member, include added_in: in the possible_types configuration:

ruby
class Types::Cookable < Types::BaseUnion
 possible_types Types::Recipe, Types::Ingredient
 # Add this to the union when clients opt in to our new feature:
 possible_types Types::Cuisine, added_in: Changeset::ReleaseCuisines

To remove a member from a union, move it to a possible_types call with removed_in: ...:

ruby
# Stop including this in the union in new API versions:
possible_types Types::Chef, removed_in: Changeset::LessChefHype

When a possible type is removed, it will not be associated with the union type in introspection queries or schema dumps.

Interfaces

You can add to or remove from an object type's interface definitions. To add one or more interface implementations, use implements(..., added_in:). This will add the interface and its fields to the object whenever this Changeset is active, for example:

ruby
class Types::Recipe < Types::BaseObject
  # Add this new implementation in new API versions only:
  implements Types::RssSubject, added_in: Changesets::AddRssSupport
end

To remove one or more more interface implementations, add removed_in: to the implements ... configuration, for example:

ruby
implements Types::RssSubject,
    added_in: Changesets::AddRssSupport,
    # Sadly, nobody seems to want to use this,
    # so we removed it all:
    removed_in: Changesets::RemoveRssSupport

When an interface implementation is removed, then the interface will not be associated with the object in introspection queries or schema dumps. Also, any fields inherited from the interface will be hidden from clients. (If the object defines the field itself, it will still be visible.)

Types

Using Changesets, it's possible to define a new type using the same name as an old type. (Only one type per name is allowed for each query, but different queries can use different types for the same name.)

First, to define two types with the same name, make two different type definitions. One of them will have to use graphql_name(...) to specify the conflicting type name. For example, to migrate an enum type to an object type, define two types:

ruby
# app/graphql/types/legacy_recipe_flag.rb

# In the old version of the schema, "recipe tags" were limited to defined set of values.
# This enum was renamed from `Types::RecipeTag`, then `graphql_name("RecipeTag")`
# was added for GraphQL.
class Types::LegacyRecipeTag < Types::BaseEnum
  graphql_name "RecipeTag"
  # ...
end
ruby
# app/graphql/types/recipe_flag.rb

# But in the new schema, each tag is a full-fledged object with fields of its own
class Types::RecipeTag < Types::BaseObject
  field :name, String, null: false
  field :is_vegetarian, Boolean, null: false
  # ...
end

Then, add or update fields or arguments to use the new type instead of the old one. For example:

diff
class Types::Recipe < Types::BaseObject

# Change this definition to point at the newly-renamed _legacy_ type
# (It's the same type definition, but the Ruby class has a new name)
-   field :tags, [Types::RecipeTag]
+   field :tags, [Types::LegacyRecipeTag]

# And add a new field for the new type:
+   field :tags, [Types::RecipeTag], added_in: Changesets::MigrateRecipeTagToObject
  end

With that Changeset, Recipe.tags will return an object type instead of an enum type. Clients requesting older versions will still receive enum values from that field.

The resolver will probably need an update, too, for example:

ruby
class Types::Recipe < Types::BaseObject
  # Here's the original definition which returns enum values:
  field :tags, [Types::LegacyRecipeTag], null: false
  # Here's the new definition which replaces the previous one on new API versions:
  field :tags, [Types::RecipeTag], null: false, added_in: Changesets::MigrateRecipeTagToObject

  def flags
    all_flag_objects = object.flag_objects
    if Changesets::MigrateRecipeTagToObject.active?(context)
      # Here's the new behavior, returning full objects:
      all_flag_objects
    else
      # Convert this to enum values, for legacy behavior:
      all_flag_objects.map { |f| f.name.upcase }
    end
  end
end

That way, legacy clients will continue to receive enum values while new clients will receive objects.

Runtime

While a query is running, you can check if a changeset applies by using its .active?(context) method. For example:

ruby
class Types::Recipe
  field :flag, Types::RecipeFlag, null: true

  def flag
    # Check if this changeset applies to the current request:
    if Changesets::DeprecateRecipeFlag.active?(context)
      Stats.count(:deprecated_recipe_flag, context[:viewer])
    end
    # ...
  end
end

Besides observability, you can use a runtime check when a resolver needs to pick a different behavior depending on the API version.

After defining a changeset, add it to the schema to release it.


Guides Changesets Installation (`guides/changesets/installation.md`)

Installing Changesets

Changesets require some updates to the schema (to define changesets) and some updates to your controller (to receive version headers from clients).

Schema Setup

To get started with GraphQL-Enterprise Changesets, you have to add them to your schema. They're added in several places:

  • To support versioning arguments, add the ArgumentIntegration to your base argument:

    ruby
    # app/graphql/types/base_argument.rb
    class Types::BaseArgument < GraphQL::Schema::Argument
      include GraphQL::Enterprise::Changeset::ArgumentIntegration
    end

    Also, make sure that your BaseField, BaseInputObject, BaseResolver, and BaseMutation have argument_class(Types::BaseArgument) configured in them.

  • To support versioning fields, add the FieldIntegration to your base field:

    ruby
    # app/graphql/types/base_field.rb
    class Types::BaseField < GraphQL::Schema::Field
      include GraphQL::Enterprise::Changeset::FieldIntegration
      argument_class(Types::BaseArgument)
    end

    Also, make sure that your BaseObject, BaseInterface, and BaseMutation have field_class(Types::BaseField) configured in them.

  • To support versioning enum values, add the EnumValueIntegration to your base enum value:

    ruby
    # app/graphql/types/base_enum_value.rb
    class Types::BaseEnumValue < GraphQL::Schema::EnumValue
      include GraphQL::Enterprise::Changeset::EnumValueIntegration
    end

    Also, make sure that your BaseEnum has enum_value_class(Types::BaseEnumValue) configured in it.

  • To support versioning union memberships and interface implementations, add the TypeMembershipIntegration to your base type membership:

    ruby
    # app/graphql/types/base_type_membership.rb
    class Types::BaseTypeMembership < GraphQL::Schema::TypeMembership
      include GraphQL::Enterprise::Changeset::TypeMembershipIntegration
    end

    Also, make sure that your BaseUnion and BaseInterface have type_membership_class(Types::BaseTypeMembership) configured in it. (TypeMemberships are used by GraphQL-Ruby to link object types to the union types they belong to and the interfaces they implement. By using a custom type membership class, you can make objects belong (or not belong) to unions or interfaces, depending on the API version.)

Once those integrations are set up, you're ready to write a changeset and start releasing API versions!

Controller Setup

Additionally, your controller must pass context[:changeset_version] when running queries. To provide this, update your controller:

ruby
class GraphqlController < ApplicationController
  def execute
    context = {
      # ...
      changeset_version: request.headers["API-Version"], # <- Your header here. Choose something for API clients to pass.
    }
    result = MyAppSchema.execute(..., context: context)
    # ...
  end
end

In the example above, API-Version: ... will be parsed from the incoming request and used as context[:changeset_version].

If context[:changeset_version] is nil, then no changesets will apply to that request.

Now that Changesets are installed, read on to define some changesets.


Guides Changesets Releases (`guides/changesets/releases.md`)

Releasing Changesets

To be available to clients, Changesets added to the schema with use GraphQL::Enterprise::Changeset::Release changesets_dir: "...":

ruby
class MyAppSchema < GraphQL::Schema
  # Add this before root types so that newly-added types are also added to the schema
  use GraphQL::Enterprise::Changeset::Release, changesets_dir: "app/graphql/changesets"

  query(...)
  mutation(...)
  subscription(...)
end

This attaches each Changeset defined in app/graphql/changesets/*.rb to the schema. (It assumes Rails conventions, where an underscored file like app/graphql/changesets/add_some_feature.rb contains a class like Changesets::AddSomeFeature.)

Warning:

Add GraphQL::Enterprise::Changeset::Release before hooking up your root query(...), mutation(...), and subscription(...) types. Otherwise, the schema may not find links to types in new schema versions.

Alternatively, Changesets can be explicitly attached using changesets: [...], for example:

ruby
class MyAppSchema < GraphQL::Schema
  use GraphQL::Enterprise::Changeset::Release, changesets: [
    Changesets::DeprecateRecipeFlag,
    Changesets::RemoveRecipeFlag,
  ]
  # ...
end

Only changesets in the directory (or in the array) will be shown to clients. The release ... configuration in the changeset will be compared to context[:changeset_version] to determine if the changeset applies to the current request.

Inspecting Releases

To preview releases, you can create schema dumps by passing context: { changeset_version: ... } to Schema.to_definition.

For example, to see how the schema looks with API-Version: 2021-06-01:

ruby
schema_sdl = MyAppSchema.to_definition(context: { changeset_version: "2021-06-01"})
# The GraphQL schema definition for the schema at version "2021-06-01":
puts schema_sdl

To make sure schema versions don't change unexpectedly, use the techniques described in the Schema structure guide.

Introspection Methods

You can also inspect a schema's changesets programmatically. GraphQL::Enterprise adds a Schema.changesets method which returns a Set of changeset classes:

ruby
MySchema.changesets
# #<Set: {AddNewFeature, RemoveOldFeature}>

Additionally, each changeset has a .changes method describing its modifications:

ruby
AddNewFeature.changes
# [
#   #<GraphQL::Enterprise::Changeset::Change: ...>,
#   #<GraphQL::Enterprise::Changeset::Change: ...>,
#   #<GraphQL::Enterprise::Changeset::Change: ...>,
#   ...
# ]

Each Change object responds to:

  • .member, the part of the schema that was modified
  • .type, the kind of modification (:addition when something new is added, :removal when a member is removed or replaced with a new definition)

Guides Dataloader Adopting (`guides/dataloader/adopting.md`)

Dataloader vs. GraphQL-Batch

GraphQL::Dataloader solves the same problem as GraphQL::Batch. There are a few major differences between the modules:

  • Concurrency Primitive: GraphQL-Batch uses Promises from promise.rb; GraphQL::Dataloader uses Ruby's Fiber API. These primitives dictate how batch loading code is written (see below for comparisons).
  • Maturity: Frankly, GraphQL-Batch is about as old as GraphQL-Ruby, and it's been in production at Shopify, GitHub, and others for many years. GraphQL::Dataloader is new, and although Ruby has supported Fibers since 1.9, they still aren't widely used.
  • Scope: It's not currently possible to use GraphQL::Dataloader outside GraphQL.

The incentive in writing GraphQL::Dataloader was to leverage Fiber's ability to transparently pause and resume work, which removes the need for Promises (and removes the resulting complexity in the code). Additionally, GraphQL::Dataloader should eventually support Ruby 3.0's Fiber.scheduler API, which runs I/O in the background by default.

Comparison: Fetching a single object

In this example, a single object is batch-loaded to satisfy a GraphQL field.

  • With GraphQL-Batch, you call a loader, which returns a Promise:

    ruby
    record_promise = Loaders::Record.load(1)

    Then, under the hood, GraphQL-Ruby manages the promise (using its lazy_resolve feature, upstreamed from GraphQL-Batch many years ago). GraphQL-Ruby will call .sync on it when no further execution is possible; promise.rb implements Promise#sync to execute the pending work.

  • With GraphQL::Dataloader, you get a source, then call .load on it, which may pause the current Fiber, but it returns the requested object.

    ruby
    dataloader.with(Sources::Record).load(1)

    Since the requested object is (eventually) returned from .load, Nothing else is required.

Comparison: Fetching objects in sequence (dependent)

In this example, one object is loaded, then another object is loaded based on the first one.

  • With GraphQL-Batch, .then { ... } is used to join dependent code blocks:

    ruby
    Loaders::Record.load(1).then do |record|
      Loaders::OtherRecord.load(record.other_record_id)
    end

    That call returns a Promise, which is stored by GraphQL-Ruby, and finally .synced.

  • With GraphQL-Dataloader, .load(...) returns the requested object (after a potential Fiber pause), so no other method calls are necessary:

    ruby
    record = dataloader.with(Sources::Record).load(1)
    dataloader.with(Sources::OtherRecord).load(record.other_record_id)

Comparison: Fetching objects concurrently (independent)

Sometimes, you need multiple independent records to perform a calculation. Each record is loaded, then they're combined in some bit of work.

  • With GraphQL-Batch, Promise.all(...) is used to wait for several pending loads:

    ruby
    promise_1 = Loaders::Record.load(1)
    promise_2 = Loaders::OtherRecord.load(2)
    Promise.all([promise_1, promise_2]).then do |record, other_record|
      do_something(record, other_record)
    end

    If the objects are loaded from the same loader, then .load_many also works:

    ruby
    Loaders::Record.load_many([1, 2]).then do |record, other_record|
      do_something(record, other_record)
    end
  • With GraphQL::Dataloader, each request is registered with .request(...) (which never pauses the Fiber), then data is loaded with .load (which will pause the Fiber as needed):

    ruby
    # first, make some requests
    request_1 = dataloader.with(Sources::Record).request(1)
    request_2 = dataloader.with(Sources::OtherRecord).request(2)
    # then, load the objects and do something
    record = request_1.load
    other_record = request_2.load
    do_something(record, other_record)

    If the objects come from the same Source, then .load_all will return the objects directly:

    ruby
    record, other_record = dataloader.with(Sources::Record).load_all([1, 2])
    do_something(record, other_record)

Guides Dataloader Async Dataloader (`guides/dataloader/async_dataloader.md`)

Async Source Execution

AsyncDataloader will run GraphQL::Dataloader::Source#fetch calls in parallel, so that external service calls (like database queries or network calls) don't have to wait in a queue.

To use AsyncDataloader, hook it up in your schema instead of GraphQL::Dataloader:

diff
- use GraphQL::Dataloader
+ use GraphQL::Dataloader::AsyncDataloader

Also, add the async gem to your project, for example:

text
bundle add async

Now, GraphQL::Dataloader::AsyncDataloader will create Async::Task instances instead of plain Fibers and the async gem will manage parallelism.

For a demonstration of this behavior, see: https://github.com/rmosolgo/rails-graphql-async-demo

You can also implement manual parallelism using dataloader.yield.

Rails

For Rails, you'll need Rails 7.1, which properly supports fiber-based concurrency, and you'll also want to configure Rails to use Fibers for isolation:

ruby
class Application < Rails::Application
  # ...
  config.active_support.isolation_level = :fiber
end

ActiveRecord Connections

You can use Dataloader's Fiber lifecycle hooks to improve ActiveRecord connection handling:

  • In Rails < 7.2, connections are not reused when a Fiber exits; instead, they're only reused when a request or background job finishes. You can add manual release_connection calls to improve this.
  • With isolation_level = :fiber, new Fibers don't inherit connected_to ... settings from their parent fibers.

Altogether, it can be improved like this:

ruby
def get_fiber_variables
  vars = super
  # Collect the current connection config to pass on:
  vars[:connected_to] = {
    role: ActiveRecord::Base.current_role,
    shard: ActiveRecord::Base.current_shard,
    prevent_writes: ActiveRecord::Base.current_preventing_writes
  }
  vars
end

def set_fiber_variables(vars)
  connection_config = vars.delete(:connected_to)
  # Reset connection config from the parent fiber:
  ActiveRecord::Base.connecting_to(**connection_config)
  super(vars)
end

def cleanup_fiber
  super
  # Release the current connection
  ActiveRecord::Base.connection_pool.release_connection
end

Modify the example according to your database configuration and abstract class hierarchy.

Other Options

You can also manually implement parallelism with Dataloader. See the Dataloader Parallelism guide for details.


Guides Dataloader Dataloader (`guides/dataloader/dataloader.md`)

Dataloader

GraphQL::Dataloader instances are created for each query (or multiplex) and they:

  • Cache Source instances for the duration of GraphQL execution
  • Run pending Fibers to resolve data requirements and continue GraphQL execution

During a query, you can access the dataloader instance with:

Fiber Lifecycle Hooks

Under the hood, Dataloader creates Fibers as-needed and uses them to run GraphQL and load data from Source classes. You can hook into these Fibers through several lifecycle hooks. To implement these hooks, create a custom subclass and provide new implementation for these methods:

ruby
class MyDataloader < GraphQL::Dataloader # or GraphQL::Dataloader::AsyncDataloader
  # ...
end

Then, use your customized dataloader instead of the built-in one:

diff
class MySchema < GraphQL::Schema
-   use GraphQL::Dataloader
+   use MyDataloader
  end
  • GraphQL::Dataloader#get_fiber_variables is called before creating a Fiber. By default, it returns a hash containing the parent Fiber's variables (from Thread.current[...]). You can add to this hash in your own implementation of this method.
  • GraphQL::Dataloader#set_fiber_variables is called inside the new Fiber. It's passed the hash returned from get_fiber_variables. You can use this method to initialize "global" state inside the new Fiber.
  • GraphQL::Dataloader#cleanup_fiber is called just before a Dataloader Fiber exits. You can use this methods to teardown any state that you prepared in set_fiber_variables.

Guides Dataloader Parallelism (`guides/dataloader/parallelism.md`)

Manual Parallelism

You can coordinate with GraphQL::Dataloader to run tasks in the background. To do this, call dataloader.yield inside Source#fetch after kicking off your task. For example:

ruby
def fetch(ids)
  # somehow queue up a background query,
  # see examples below
  future_result = async_query_for(ids)
  # return control to the dataloader
  dataloader.yield
  # dataloader will come back here
  # after calling other sources,
  # now wait for the value
  future_result.value
end

Alternatively, you can use AsyncDataloader to automatically background I/O inside Source#fetch calls.

Example: Rails load_async

You can use Rails's load_async method to load ActiveRecord::Relations in the background. For example:

ruby
class Sources::AsyncRelationSource < GraphQL::Dataloader::Source
  def fetch(relations)
    relations.each(&:load_async) # start loading them in the background
    dataloader.yield # hand back to GraphQL::Dataloader
    relations.each(&:load) # now, wait for the result, returning the now-loaded relation
  end
end

You could call that source from a GraphQL field method:

ruby
field :direct_reports, [Person]

def direct_reports
  # prepare an ActiveRecord::Relation:
  direct_reports = Person.where(manager: object)
  # pass it off to the source:
  dataloader
    .with(Sources::AsyncRelationSource)
    .load(direct_reports)
end

Example: Rails async calculations

In a Dataloader source, you can run Rails async calculations in the background while other work continues. For example:

ruby
class Sources::DirectReportsCount < GraphQL::Dataloader::Source
  def fetch(users)
    # Start the queries in the background:
    promises = users.map { |u| u.direct_reports.async_count }
    # Return to GraphQL::Dataloader:
    dataloader.yield
    # Now return the results, waiting if necessary:
    promises.map(&:value)
  end
end

Which could be used in a GraphQL field:

ruby
field :direct_reports_count, Int

def direct_reports_count
  dataloader.with(Sources::DirectReportsCount).load(object)
end

Example: Concurrent::Future

You could use concurrent-ruby to put work in a background thread. For example, using Concurrent::Future:

ruby
class Sources::ExternalDataSource < GraphQL::Dataloader::Source
  def fetch(urls)
    # Start some I/O-intensive work:
    futures = urls.map do |url|
      Concurrent::Future.execute {
        # Somehow fetch and parse data:
        get_remote_json(url)
      }
    end
    # Yield back to GraphQL::Dataloader:
    dataloader.yield
    # Dataloader has done what it can,
    # so now return the value, waiting if necessary:
    futures.map(&:value)
  end
end

Guides Dataloader Sources (`guides/dataloader/sources.md`)

Sources

Sources are what GraphQL::Dataloader uses to fetch data from external services.

Source Concepts

Sources are classes that inherit from GraphQL::Dataloader::Source. A Source must implement def fetch(keys) to return a list of objects, one for each of the given keys. A source may implement def initialize(...) to accept other batching parameters.

Sources will receive two kinds of inputs from GraphQL::Dataloader:

  • keys, which correspond to objects requested by the application.

    Keys are passed to def fetch(keys), which must return an object (or nil) for each of keys, in the same order as keys.

    Under the hood, each Source instance maintains a key => object cache.

  • batch parameters, which are the basis of batched groups. For example, if you're loading records from different database tables, the table name would be a batch parameter.

    Batch parameters are given to dataloader.with(source_class, *batch_parameters), and the default is no batch parameters. When you define a source, you should add the batch parameters to def initialize(...) and store them in instance variables.

    (dataloader.with(source_class, *batch_parameters) returns an instance of source_class with the given batch parameters -- but it might be an instance which was cached by dataloader.)

    Additionally, batch parameters are used to de-duplicate Source initializations during a query run. .with(...) calls that have the same batch parameters will use the same Source instance under the hood. To customize how Sources are de-duplicated, see GraphQL::Dataloader::Source.batch_key_for.

Example: Loading Strings from Redis by Key

The simplest source might fetch values based on their keys. For example:

ruby
# app/graphql/sources/redis_string.rb
class Sources::RedisString < GraphQL::Dataloader::Source
  REDIS = Redis.new
  def fetch(keys)
    # Redis's `mget` will return a value for each key with a `nil` for any not-found key.
    REDIS.mget(*keys)
  end
end

This loader could be used in GraphQL like this:

ruby
some_string = dataloader.with(Sources::RedisString).load("some_key")

Calls to .load(key) will be batched, and when GraphQL::Dataloader can't go any further, it will dispatch a call to def fetch(keys) above.

Example: Loading ActiveRecord Objects by ID

To fetch ActiveRecord objects by ID, the source should also accept the model class as a batching parameter. For example:

ruby
# app/graphql/sources/active_record_object.rb
class Sources::ActiveRecordObject < GraphQL::Dataloader::Source
  def initialize(model_class)
    @model_class = model_class
  end

  def fetch(ids)
    records = @model_class.where(id: ids)
    # return a list with `nil` for any ID that wasn't found
    ids.map { |id| records.find { |r| r.id == id.to_i } }
  end
end

This source could be used for any model_class, for example:

ruby
author = dataloader.with(Sources::ActiveRecordObject, ::User).load(1)
post = dataloader.with(Sources::ActiveRecordObject, ::Post).load(1)

Example: Batched Calculations

Besides fetching objects, Sources can return values from batched calculations. For example, a system could batch up checks for who a user follows:

ruby
# for a given user, batch checks to see whether this user follows another user.
# (The default `user.followings.where(followed_user_id: followed).exists?` would cause N+1 queries.)
class Sources::UserFollowingExists < GraphQL::Dataloader::Source
  def initialize(user)
    @user = user
  end

  def fetch(handles)
    # Prepare a `SELECT id FROM users WHERE handle IN(...)` statement
    user_ids = ::User.where(handle: handles).select(:id)
    # And use it to filter this user's followings:
    followings = @user.followings.where(followed_user_id: user_ids)
    # Now, for followings that _actually_ hit a user, get the handles for those users:
    followed_users = ::User.where(id: followings.select(:followed_user_id))
    # Finally, return a result set, with one entry (true or false) for each of the given `handles`
    handles.map { |h| !!followed_users.find { |u| u.handle == h }}
  end
end

It could be used like this:

ruby
is_following = dataloader.with(Sources::UserFollowingExists, context[:viewer]).load(handle)

After all requests were batched, #fetch will return a Boolean result to is_following.

Example: Loading in a background thread

Inside Source#fetch(keys), you can call dataloader.yield to return control to the Dataloader. This way, it will proceed loading other Sources (if there are any), then return the source that yielded.

A simple example, spinning up a new Thread:

ruby
def fetch(keys)
  # spin up some work in a background thread
  thread = Thread.new {
    fetch_external_data(keys)
  }
  # return control to the dataloader
  dataloader.yield
  # at this point,
  # the dataloader has tried everything else and come back to this source,
  # so block if necessary:
  thread.value
end

See the parallelism guide for details about this approach.

Filling the Dataloader Cache

If you load records from the database, you can use them to populate a source's cache by using Dataloader::Source#merge. For example:

ruby
# Build a `{ key => value }` map to populate the cache
comments_by_id = post.comments.each_with_object({}) { |comment, hash| hash[comment.id] = comment }
# Merge the map into the source's cache
dataloader.with(Sources::ActiveRecordObject, Comment).merge(comments_by_id)

After that, any calls to .load(id) will use those already-loaded records if they're available.

De-duplicating equivalent objects

Sometimes, different objects in the application should load the same object from fetch. You can customize this behavior by implementing def result_key_for(key) in your application. For example, to map records from your ORM to their database ID:

ruby
# Load the `created_by` person for a record from our database
class CreatedBySource < GraphQL::Dataloader::Source
  def result_key_for(key)
    key.id # Use the record's ID to deduplicate different `.load` calls
  end

  # Fetch a `person` for each of `records`, based on their created_by_id
  def fetch(records)
    PersonService.find_each(records.map(&:created_by_id))
  end
end

In this case, records will include the first object for each unique record.id -- subsequent records with the same .id will be assumed to be duplicates. Under the hood, the Source will cache the result based on the record's id.

Alternatively, you could use this to make the Source retain each incoming object, even when they would otherwise be treated as duplicates. (This would come in handy when you need def fetch to mutate each object). For example, to treat every incoming object as distinct:

ruby
def result_key_for(record)
  record.object_id # even if the records are equivalent, handle each distinct Ruby object separately
end

Guides Dataloader Testing (`guides/dataloader/testing.md`)

Testing

There are a few techniques for testing your GraphQL::Dataloader setup.

Integration Tests

One important feature of Dataloader is how it manages database access while GraphQL runs queries. You can test that by listening for database queries while running queries, for example, with ActiveRecord:

ruby
def test_active_record_queries_are_batched_and_cached
  # set up a listener function
  database_queries = 0
  callback = lambda {|_name, _started, _finished, _unique_id, _payload| database_queries += 1 }

  query_str = <<-GRAPHQL
  {
    a1: author(id: 1) { name }
    a2: author(id: 2) { name }
    b1: book(id: 1) { author { name } }
    b2: book(id: 2) { author { name } }
  }
  GRAPHQL

  # Run the query with the listener
  ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
    MySchema.execute(query_str)
  end

  # One query for authors, one query for books
  assert_equal 2, database_queries
end

You could also make specific assertions on the queries that are run (see the sql.active_record docs). For other frameworks and databases, check your ORM or library for instrumentation options.

Testing Dataloader Sources

You can also test Dataloader behavior outside of GraphQL using GraphQL::Dataloader.with_dataloading. For example, let's say you have a Sources::ActiveRecord source defined like so:

ruby
module Sources
  class User < GraphQL::Dataloader::Source
    def fetch(ids)
      records = User.where(id: ids)
      # return a list with `nil` for any ID that wasn't found, so the shape matches
      ids.map { |id| records.find { |r| r.id == id.to_i } }
    end
  end
end

You can test it like so:

ruby
def test_it_fetches_objects_by_id
  user_1, user_2, user_3 = 3.times.map { User.create! }

  database_queries = 0
  callback = lambda {|_name, _started, _finished, _unique_id, _payload| database_queries += 1 }

  ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
    GraphQL::Dataloader.with_dataloading do |dataloader|
      req1 = dataloader.with(Sources::ActiveRecord).request(user_1.id)
      req2 = dataloader.with(Sources::ActiveRecord).request(user_2.id)
      req3 = dataloader.with(Sources::ActiveRecord).request(user_3.id)
      req4 = dataloader.with(Sources::ActiveRecord).request(-1)

      # Validate source's matching up of records
      expect(req1.load).to eq(user_1)
      expect(req2.load).to eq(user_2)
      expect(req3.load).to eq(user_3)
      expect(req4.load).to be_nil
    end
  end

  assert_equal 1, database_queries, "All users were looked up at once"
end

Guides Defer Graphiql (`guides/defer/graphiql.md`)

Use with GraphiQL

You can use @defer and @stream with GraphiQL, an in-browser IDE.

Incremental responses

If you're using the proposed incremental: ... response syntax (proposal, Ruby support), you'll need a custom "fetcher" function to handle the incremental: ... part of the response. For example:

js
import { meros } from "meros"; // for handling multipart responses

const customFetcher = async function* (graphqlParams, fetcherOpts) {
  // Make the initial fetch
  var result = await fetch("/graphql", {
    method: "POST",
    body: JSON.stringify(graphqlParams),
    headers: {
      'content-type': 'application/json',
    }
  }).then((r) => {
    // Use meros to turn multipart responses into streams
    return meros(r, { multiple: true })
  })

  if (!isAsyncIterable(result)) {
    // Return plain responses as promises
    return result.json()
  } else {
    // Handle multipart responses one chunk at a time
    for await (const chunk of result) {
      yield chunk.map(part => {
        // Move the incremental part of the response into top-level
        // This assumes there's only one `incremental` entry
        // which is currently true for GraphQL-Pro's @defer implementation
        var newJson = {...part.body}
        if (newJson.incremental) {
          newJson.data = newJson.incremental[0].data
          newJson.path = newJson.incremental[0].path
          delete newJson.incremental
        }
        return newJson
      });
    }
  }
}

// Helper for checking for a multipart response:
function isAsyncIterable(input) {
  return (
      typeof input === "object" &&
      input !== null &&
      (
        input[Symbol.toStringTag] === "AsyncGenerator" ||
        (Symbol.asyncIterator && Symbol.asyncIterator in input)
      )
    );
}

Hopefully a new GraphiQL version will support this out of the box; follow the issue on GitHub.


Guides Defer Setup (`guides/defer/setup.md`)

Server Setup

Before using @defer in queries, you have to:

  • Update graphql and graphql-pro gems
  • Add @defer to your GraphQL schema
  • Update your HTTP handlers (eg, Rails controllers) to send streaming responses
  • Optionally, customize @defer to work with GraphQL-Batch

You can also see a full Rails & Apollo-Client demo.

Updating the gems

GraphQL-Ruby 1.9+ and GraphQL-Pro 1.10+ are required:

ruby
gem "graphql", "~>1.9"
gem "graphql-pro", "~>1.10"

And then install them:

text
$ bundle update graphql graphql-pro

Adding `@defer` to your schema

Then, add GraphQL::Pro::Defer to your schema as a plugin:

ruby
class MySchema < GraphQL::Schema
  use GraphQL::Pro::Defer
end

This will:

  • Attach a custom directive called @defer
  • Add instrumentation to queries to track deferred work and execute it later

Sending streaming responses

Many web frameworks have support for streaming responses, for example:

See below for how to integrate GraphQL's deferred patches with a streaming response API.

To investigate support with a web framework, please open an issue or email [email protected].

Checking for deferrals

When a query has any @defered fields, you can check for context[:defer]:

ruby
if context[:defer]
  # some fields were `@defer`ed
else
  # normal GraphQL, no `@defer`
end

Working with deferrals

To handle deferrals, you can enumerate over context[:defer], for example:

ruby
context[:defer].each do |deferral|
  # do something with the `deferral`, eg
  # stream_to_client(deferral.to_h)
end

The initial result is also present in the deferrals, so you can treat it just like a patch.

Each deferred patch has a few methods for building a response:

  • .to_h returns a hash with path:, data:, and/or errors:. (There is no path: for the root result.)
  • .to_http_multipart(incremental: true) returns a string which works with Apollo client's @defer support. (Use incremental: true to format patches for the forthcoming spec.)
  • .path returns the path to this patch in the response
  • .data returns successfully-resolved results of the patch
  • .errors returns an array of errors, if there were any

Calling .data or .errors on a deferral will resume GraphQL execution until the patch is complete.

Example: Rails with Apollo Client

In this example, a Rails controller will stream HTTP Multipart patches to the client, in Apollo Client's supported format.

ruby
class GraphqlController < ApplicationController
  # Support `response.stream` below:
  include ActionController::Live

  def execute
    # ...
    result = MySchema.execute(query, variables: variables, context: context, operation_name: operation_name)

    # Check if this is a deferred query:
    if (deferred = result.context[:defer])
      # Required for Rack 2.2+, see https://github.com/rack/rack/issues/1619
      response.headers['Last-Modified'] = Time.now.httpdate
      # Use built-in `stream_http_multipart` with Apollo-Client & ActionController::Live
      deferred.stream_http_multipart(response, incremental: true)
    else
      # Return a plain, non-deferred result
      render json: result
    end
  ensure
    # Always make sure to close the stream
    response.stream.close
  end
end

You can also investigate a full Rails & Apollo-Client demo

With GraphQL-Batch

GraphQL-Batch is a third-party data loading library that wraps GraphQL-Ruby execution. Deferred resolution happens outside the normal execution flow, so to work with GraphQL-Batch, you have to customize GraphQL::Pro::Defer a bit. Also, you'll need GraphQL-Pro v1.24.6 or later. Here's a custom Defer implementation:

ruby
# app/graphql/directives/defer.rb
module Directives
  # Modify the library's `@defer` implementation to work with GraphQL-Batch
  class Defer < GraphQL::Pro::Defer
    def self.resolve(obj, arguments, context, &block)
      # While the query is running, store the batch executor to re-use later
      context[:graphql_batch_executor] ||= GraphQL::Batch::Executor.current
      super
    end

    class Deferral < GraphQL::Pro::Defer::Deferral
      def resolve
        # Before calling the deferred execution,
        # set GraphQL-Batch back up:
        prev_executor = GraphQL::Batch::Executor.current
        GraphQL::Batch::Executor.current ||= @context[:graphql_batch_executor]
        super
      ensure
        # Clean up afterward:
        GraphQL::Batch::Executor.current = prev_executor
      end
    end
  end
end

And update your schema to use your custom defer implementation:

ruby
# Use our GraphQL-Batch-compatible defer:
use Directives::Defer

Next Steps

Read about client usage of @defer.


Guides Defer Stream (`guides/defer/stream.md`)

Stream

@stream works very much like @defer, except it only applies to list fields. When a field has @stream and it returns a list, then each item in the list is returned to the client as a patch. @stream is described in a proposal to the GraphQL specification.

Note: @stream was added in GraphQL-Pro 1.21.0 and requires GraphQL-Ruby 1.13.6+.

Installation

To support @stream in your schema, add it with use GraphQL::Pro::Stream:

ruby
class MySchema < GraphQL::Schema
  # ...
  use GraphQL::Pro::Stream
end

Additionally, you should update your controller to handle deferred parts of the response. See the @defer setup guide for details. (@stream uses the same deferral pipeline as @defer, so the same setup instructions apply.)

Usage

After that, you can include @stream in your queries, for example:

ruby
{
  # Send each movie in its own patch:
  nowPlaying @stream {
    title
    director { name }
  }
}

If @stream is applied to non-list fields, it's ignored.

@stream supports several arguments:

  • if: Boolean = true: when false, the list is not streamed. Instead, all items are returned synchronously.
  • label: String: if present, the given string is returned in patches as "label": "..."
  • initialCount: Int = 0: this number of list items are returned synchronously. (If the list is shorter than initialCount, then the whole list is returned synchronously.)

Guides Defer Usage (`guides/defer/usage.md`)

Usage

@defer is a GraphQL directive which instructs the server to execute the field in a special way:

graphql
query GetPlayerInfo($handle: String!){
  player(handle: $handle) {
    name
    # Send this field later, to avoid slowing down the initial response:
    topScore(from: 2000, to: 2020) @defer
  }
}

The directives @skip and @include are built into any GraphQL server and client, but @defer requires special attention.

Apollo-Client currently supports the @defer directive.

@defer also accepts a label: option which will be included in outgoing patches when it's present in the query (eg, @defer(label: "patch1")).

Want to use @defer with another client? Please open an issue or email [email protected] and we'll dig in!

Next Steps

Set up your server to support @defer.


Guides Development Development (`guides/development/development.md`)

Development

So, you want to hack on GraphQL Ruby! Here are some tips for getting started.

Setup

Get your own copy of graphql-ruby by forking rmosolgo/graphql-ruby on GitHub and cloning your fork.

Then, install the dependencies:

  • Install SQLite3 and MongoDB (eg, brew install sqlite && brew tap mongodb/brew && brew install mongodb-community)
  • bundle install
  • rake compile # If you get warnings at this step, you can ignore them.
  • Optional: Ragel is required to build the lexer

Running the Tests

Unit tests

You can run the tests with

text
bundle exec rake        # tests & Rubocop
bundle exec rake test   # tests only

You can run a specific file with TEST=:

text
bundle exec rake test TEST=spec/graphql/query_spec.rb
# run tests in `query_spec.rb` only

You can focus on a specific example with focus:

ruby
focus
it "does something cool" do
  # ...
end

Then, only focused tests will run:

text
bundle exec rake test
# only the focused test will be run

(This is provided by minitest-focus.)

Integration tests

You need to pick a specific gemfile from gemfiles/ to run integration tests. For example:

text
BUNDLE_GEMFILE=gemfiles/rails_6.1.gemfile bundle install
BUNDLE_GEMFILE=gemfiles/rails_6.1.gemfile bundle exec rake test TEST=spec/integration/rails/graphql/relay/array_connection_spec.rb

GraphQL-CParser tests

To test the graphql_cparser gem, you have to build the binary first:

text
bundle exec rake build_ext

Then, run the test suite with GRAPHQL_CPARSER=1:

text
GRAPHQL_CPARSER=1 bundle exec rake test

(Add TEST= to pick a certain file.)

Other tests

There are system tests for checking ActionCable behavior, use:

text
bundle exec rake test:system

And JavaScript tests:

text
bundle exec rake test:js

Gemfiles, Gemfiles, Gemfiles

graphql-ruby has several gemfiles to ensure support for various Rails versions. You can specify a gemfile with BUNDLE_GEMFILE, eg:

text
BUNDLE_GEMFILE=gemfiles/rails_5.gemfile bundle exec rake test

Debugging with Pry

pry is included with GraphQL-Ruby's development setup to help with debugging.

To pause execution in Ruby code, add:

ruby
binding.pry

Then, the program will pause and your terminal will become a Ruby REPL. Feel free to use pry in your development process!

Running the Benchmarks

This project includes some Rake tasks to record benchmarks:

sh
$ bundle exec rake -T | grep bench:
rake bench:profile         # Generate a profile of the introspection query
rake bench:query           # Benchmark the introspection query
rake bench:validate        # Benchmark validation of several queries

You can save results by sending the output into a file:

sh
$ bundle exec rake bench:validate > before.txt
$ cat before.txt
# ...
# --> benchmark output here

If you want to check performance, create a baseline by running these tasks before your changes. Then, make your changes and run the tasks again and compare your results.

Keep these points in mind when using benchmarks:

  • The results are hardware-specific: computers with different hardware will have different results. So don't compare your results to results from other computers.
  • The results are environment-specific: CPU and memory availability are affected by other processes on your computer. So try to create similar environments for your before-and-after testing.

Coding Guidelines

GraphQL-Ruby uses a thorough test suite to make sure things work reliably day-after-day. Please include tests that describe your changes, for example:

  • If you contribute a bug fix, include a test for the code that was broken (and is now fixed)
  • If you contribute a feature, include tests for all intended uses of that feature
  • If you modify existing behavior, update the tests to cover all intended behaviors for that code

Don't fret about coding style or organization. There's a minimal Rubocop config in .rubocop.yml which runs during CI. You can run it manually with bundle exec rake rubocop.

Website

To update the website, update the .md files in guides/.

Install the optional documentation dependencies and build the site locally:

sh
BUNDLE_WITH=docs bundle install
bundle exec rake docs:build
bundle exec rake docs:rdoc:serve

Then visit http://127.0.0.1:8808. Run the documentation checks before submitting a change:

sh
bundle exec rake docs:check
bundle exec rake docs:build_twice

See the documentation maintenance guide for API links, GraphQL code blocks, redirects, and release documentation.

API Docs

The GraphQL-Ruby website has a rendered version of each published gem's API docs. Generate a local copy with:

text
$ bundle exec rake "docs:rdoc:build_version[1.8.0]"

The output is written to tmp/rdoc-api/1.8.0/; the release workflow publishes it under /api-doc/1.8.0/ while preserving older versions.

Versioning

GraphQL-Ruby does not attempt to deliver "semantic versioning" for the reasons described in jashkenas'
s post, "Why Semantic Versioning Isn't". Instead, the following scheme is used as a guideline:

  • Version numbers consist of three parts, MAJOR.MINOR.PATCH
  • PATCH version indicates bug fixes or small features for specific use cases. Ideally, you can upgrade patch versions with only a quick skim of the changelog.
  • MINOR version indicates significant additions, internal refactors, or small breaking changes. When upgrading a minor version, check the changelog for any new features or breaking changes which apply to your system. The changelog will always include an upgrade path for any breaking changes. Minor versions may also include deprecation warnings to warn about upcoming breaking changes.
  • MAJOR version indicates significant breaking changes. Do not expect code to run without some modification, especially if the code yielded deprecation warnings.

This policy is inspired by the Ruby 2.1.0+ version policy.

Pull requests and issues may be tagged with a GitHub milestone to denote when they'll be released.

The changelog should always contain accurate and thorough information so that users can upgrade. If you have trouble upgrading based on the changelog, please open an issue on GitHub.

Releasing

GraphQL-Ruby doesn't have a strict release schedule. If you think it should, consider opening an issue to share your thoughts.

To cut a release:

  • Update CHANGELOG.md for the new version:
    • Add a new heading for the new version, and paste the four categories of changes into the new section
    • Open the GitHub milestone corresponding to the new version
    • Check each pull request and put it in the category (or categories) that it belongs in
      • If a change affects the default behavior of GraphQL-Ruby in a disruptive way, add it to ## Breaking Changes and include migration notes if possible
      • Include the PR number beside the change description for future reference
  • Update lib/graphql/version.rb with the new version number
  • Commit changes to master
  • Push changes to GitHub: git push origin master. GitHub Actions will update the website.
  • Release to RubyGems: bundle exec rake release. This will also push the tag to GitHub which will kick off a GitHub Actions job to update the API docs.
  • Celebrate 🎊 !

Guides Development Documentation Maintenance (`guides/development/documentation_maintenance.md`)

Documentation maintenance

GraphQL-Ruby documentation is generated with RDoc and the Aliki generator. API comments live beside the Ruby code, while tutorials and cross-cutting explanations remain Markdown pages in guides/.

Install the optional documentation dependencies and build a local site with:

sh
BUNDLE_WITH=docs bundle install
bundle exec rake docs:build

The generated site is written to tmp/rdoc-site. To preview it, run bundle exec rake docs:rdoc:serve and open http://127.0.0.1:8808.

When adding or moving a guide, update docs/guide_metadata.yml; it controls sidebar order and the GraphQL-Pro and GraphQL-Enterprise callouts.

Run the documentation checks before submitting a change:

sh
bundle exec rake docs:check
bundle exec rake docs:build_twice

Use rdoc-ref:GraphQL::Schema#execute (or the corresponding class-method form) for links from comments and guides. Add a short API comment when a public method is missing from the generated index. Dynamic methods should use RDoc directives such as :method: and :call-seq: rather than introducing new YARD tags.

Use a fenced graphql block for GraphQL examples:

markdown
```graphql
query Viewer {
  viewer { id }
}
```

When a page or API moves, add an entry to docs/redirects.yml so the old URL remains usable. Run bundle exec ruby tool/docs/link_checker.rb --root tmp/rdoc-site --strict to verify the redirect and its fragment.

Release builds use bundle exec rake "docs:rdoc:build_version[VERSION]" to generate a versioned API site under tmp/rdoc-api/VERSION. The publish workflow copies that directory into gh-pages/api-doc/VERSION without removing older versions.


Guides Development Faq (`guides/development/faq.md`)

FAQ

Returning Route URLs

With GraphQL there is less of a need to include resource URLs to other REST resources, however sometimes you want to use Rails routing to include a URL as one of your fields. A common use case would be to build HTML format URLs to render a link in your React UI. In that case you can pass the request to your context, so that the helpers are able to build full URLs based on the incoming host, port and protocol.

Example

ruby
class Types::UserType < Types::BaseObject
  include ActionController::UrlFor
  include Rails.application.routes.url_helpers
  # Needed by ActionController::UrlFor to extract the host, port, protocol etc. from the current request
  def request
    context[:request]
  end
  # Needed by Rails.application.routes.url_helpers, it will then use the url_options defined by ActionController::UrlFor
  def default_url_options
    {}
  end
  
  field :profile_url, String, null: false
  def profile_url
    user_url(object)
  end
end

# In your GraphQL controller, add the request to `context`:
MySchema.execute(
  params[:query],
  variables: params[:variables],
  context: {
    request: request
  },
)

Returning ActiveStorage blob URLs

If you are using ActiveStorage and need to return a URL to an attachment blob, you will find that using Rails.application.routes.url_helpers.rails_blob_url alone will throw an exception since Rails won't know what host, port or protocol to use in it.
You can include ActiveStorage::SetCurrent in your GraphQL controller to pass on this information into your resolvers.

Example

ruby
class GraphqlController < ApplicationController
  include ActiveStorage::SetCurrent
  ...
end

class Types::UserType < Types::BaseObject
  field :picture_url, String, null: false
  def picture_url
    Rails.application.routes.url_helpers.rails_blob_url(
      object.picture,
      protocol: ActiveStorage::Current.url_options[:protocol],
      host: ActiveStorage::Current.url_options[:host],
      port: ActiveStorage::Current.url_options[:port]
    )
  end
end

Guides Development Getting Started (`guides/development/getting_started.md`)

Getting Started

Installation

You can install graphql from RubyGems by adding to your application's Gemfile:

ruby
# Gemfile
gem "graphql"

Then, running bundle install:

sh
$ bundle install

Getting Started

On Rails, you can get started with a few GraphQL generators:

sh
# Add graphql-ruby boilerplate and mount graphiql in development
$ rails g graphql:install
# You may need to run bundle install again, as by default graphiql-rails is added on installation.
$ bundle install
# Make your first object type
$ rails g graphql:object Post title:String rating:Int comments:[Comment]

Or, you can build a GraphQL server by hand:

  • Define some types
  • Connect them to a schema
  • Execute queries with your schema

Declare types

Types describe objects in your application and form the basis for GraphQL's type system.

ruby
# app/graphql/types/post_type.rb
module Types
  class PostType < Types::BaseObject
    description "A blog post"
    field :id, ID, null: false
    field :title, String, null: false
    # fields should be queried in camel-case (this will be `truncatedPreview`)
    field :truncated_preview, String, null: false
    # Fields can return lists of other objects:
    field :comments, [Types::CommentType],
      # And fields can have their own descriptions:
      description: "This post's comments, or null if this post has comments disabled."
  end
end

# app/graphql/types/comment_type.rb
module Types
  class CommentType < Types::BaseObject
    field :id, ID, null: false
    field :post, PostType, null: false
  end
end

Build a Schema

Before building a schema, you have to define an entry point to your system, the "query root":

ruby
class QueryType < GraphQL::Schema::Object
  description "The query root of this schema"

  field :post, resolver: Resolvers::PostResolver
end

Define how this field is resolved by creating a resolver class:

ruby
# app/graphql/resolvers/post_resolver.rb
module Resolvers
  class PostResolver < BaseResolver
    type Types::PostType, null: false
    argument :id, ID

    def resolve(id:)
      ::Post.find(id)
    end
  end
end

Then, build a schema with QueryType as the query entry point:

ruby
class Schema < GraphQL::Schema
  query Types::QueryType
end

This schema is ready to serve GraphQL queries! Browse the guides to learn about other GraphQL Ruby features.

Execute queries

You can execute queries from a query string:

ruby
query_string = "
{
  post(id: 1) {
    id
    title
    truncatedPreview
  }
}"
result_hash = Schema.execute(query_string)
# {
#   "data" => {
#     "post" => {
#        "id" => 1,
#        "title" => "GraphQL is nice"
#        "truncatedPreview" => "GraphQL is..."
#     }
#   }
# }

See Executing Queries for more information about running queries on your schema.

Use with Relay

If you're building a backend for Relay, you'll need:

Use with Apollo Client

Apollo Client is a full featured, simple to use GraphQL client with convenient integrations for popular view layers. You don't need to do anything special to connect Apollo Client to a graphql-ruby server.

Use with GraphQL.js Client

GraphQL.js Client is a tiny client that is platform- and framework-agnostic. It works well with graphql-ruby servers, since GraphQL requests are simple query strings transported over HTTP.


Want to add something? Please open a pull request on GitHub!

Code

Blog Posts

Screencasts

Presentations

Tutorials


Guides Errors Error Handling (`guides/errors/error_handling.md`)

Error Handling

You can configure your schema to rescue application errors during field resolution. Errors during batch loading will also be rescued.

Thanks to @exAspArk for the graphql-errors gem which inspired this behavior and @thiago-sydow who suggested an implementation like this.

Add error handlers

Handlers are added with rescue_from configurations in the schema:

ruby
class MySchema < GraphQL::Schema
  # ...

  rescue_from(ActiveRecord::RecordNotFound) do |err, obj, args, ctx, field|
    # Raise a graphql-friendly error with a custom message
    raise GraphQL::ExecutionError, "#{field.type.unwrap.graphql_name} not found"
  end

  rescue_from(SearchIndex::UnavailableError) do |err, obj, args, ctx, field|
    # Log the error
    Bugsnag.notify(err)
    # replace it with nil
    nil
  end
end

The handler is called with several arguments:

  • err is the error that was raised during field execution, then rescued
  • obj is the object which was having a field resolved against it
  • args is the Hash of arguments passed to the resolver
  • ctx is the query context
  • field is the GraphQL::Schema::Field instance for the field where the error was rescued

Inside the handler, you can:

  • Raise a GraphQL-friendly GraphQL::ExecutionError to return to the user
  • Re-raise the given err to crash the query and halt execution. (The error will propagate to your application, eg, the controller.)
  • Report some metrics from the error, if applicable
  • Return a new value to be used for the error case (if not raising another error)

Guides Errors Execution Errors (`guides/errors/execution_errors.md`)

The GraphQL specification allows for a top-level "errors" key in the response which may contain information about what went wrong during execution. For example:

ruby
{
  "errors" => [ ... ]
}

The response may include both "data" and "errors" in the case of a partial success:

ruby
{
  "data" => { ... } # parts of the query that ran successfully
  "errors" => [ ... ] # errors that prevented some parts of the query from running
}

When to Use Top-Level Errors

In general, top-level errors should only be used for exceptional circumstances when a developer should be made aware that the system had some kind of problem.

For example, the GraphQL specification says that when a non-null field returns nil, an error should be added to the "errors" key. This kind of error is not recoverable by the client. Instead, something on the server should be fixed to handle this case.

When you want to notify a client some kind of recoverable issue, consider making error messages part of the schema, for example, as in mutation errors.

Adding Errors to the Array

In GraphQL-Ruby, you can add entries to this array by raising GraphQL::ExecutionError (or a subclass of it), for example:

ruby
raise GraphQL::ExecutionError, "Can't continue with this query"

When this error is raised, its message will be added to the "errors" key and GraphQL-Ruby will automatically add the line, column and path to it. So, the above error might be:

ruby
{
  "errors" => [
    {
      "message" => "Can't continue with this query",
      "locations" => [
        {
          "line" => 2,
          "column" => 10,
        }
      ],
      "path" => ["user", "login"],
    }
  ]
}

Customizing Error JSON

The default error JSON includes "message", "locations" and "path". The forthcoming version of the GraphQL spec recommends putting custom data in the "extensions" key of the error JSON.

You can customize this in two ways:

  • Pass extensions: when raising an error, for example:

    ruby
    raise GraphQL::ExecutionError.new("Something went wrong", extensions: { "code" => "BROKEN" })

    In this case, "extensions" => { "code" => "BROKEN" } will be added to the error JSON.

  • Override #to_h in a subclass of GraphQL::ExecutionError, for example:

    ruby
    class ServiceUnavailableError < GraphQL::ExecutionError
      def to_h
        super.merge({ "extensions" => {"code" => "SERVICE_UNAVAILABLE"} })
      end
    end

    Now, "extensions" => { "code" => "SERVICE_UNAVAILABLE" } will be added to the error JSON.


Guides Errors Type Errors (`guides/errors/type_errors.md`)

Type Errors

The GraphQL specification requires certain assumptions to hold true when executing a query. However, it's possible that some code would violate that assumption, resulting in a type error.

Here are two type errors that you can customize in GraphQL-Ruby:

  • A field with null: false returned nil
  • A field returned a value as a union or interface, but that value couldn't be resolved to a member of that union or interface.

You can specify behavior in these cases by defining a Schema.type_error hook:

ruby
class MySchema < GraphQL::Schema
  def self.type_error(err, query_ctx)
    # Handle a failed runtime type coercion
  end
end

It is called with an instance of GraphQL::UnresolvedTypeError or GraphQL::InvalidNullError and the query context (a GraphQL::Query::Context).

If you don't specify a hook, you get the default behavior:

An object that fails type resolution is treated as nil.


Guides Execution Migration (`guides/execution/migration.md`)

Migrating to Execution::Next

This guide includes tips for migrating your schema configuration and production traffic to the new engine.

Migration Philosophy

Execution::Next is designed to run alongside the previous engine so that the same schema can run queries both ways. This supports an incremental migration and live toggling in production.

First, update your schema to include the necessary field configurations. If you implement new class methods in your Object type classes, you can also migrate instance methods to call "up" to those class methods, preserving a single source of truth:

ruby
field :unpublished_posts, [Types::Post], resolve_each: true

# Support batching:
def self.unpublished_posts(object, context)
  object.posts.where(published: false).order("created_at DESC")
end

# Support legacy in a DRY way by calling the class method:
def unpublished_posts
  self.class.unpublished_posts(object, context)
end

Test your new configurations in CI by running a new build which calls execution_next instead of execution, for example:

ruby
# test_helpers.rb
def run_graphql(...)
  if ENV["GRAPHQL_EXECUTION_NEXT"]
    MyAppSchema.execute_next(...)
  else
    MyAppSchema.execute(...)
  end
end

Adopting a feature flag system (described below) can also make this easier.

When all tests pass on .execute_next, you're ready to try it out in production.

Migration and Clean-Up Script

graphql_migrate_execution is a command-line development tool that can automate many common GraphQL-Ruby field resolver patterns.

Check out its docs and try out: https://rmosolgo.github.io/graphql_migrate_execution/

Production Considerations

There are two categories of problems when migrating:

  • Some schema misconfigurations may only be detected at runtime.
  • The engine may have bugs. (It's brand-new code trying to emulate 10 years of incremental development!)

When migrating, these possibilities should be considered from three different angles. Using the new engine may...

  • ...raise errors in new ways.
  • ...return a different result than the old engine.
  • ...perform worse than the old engine, especially because of different database access patterns.

To mitigate these possibilities, use dynamic release tools in production like feature flags and experiments.

Feature Flags

You should use a feature flagging system so that you can shift traffic between old and new runtime engines without redeploying. A good feature-flagging system supports percentage-based flags, so that you can send 1% of traffic to new code while the other 99% uses existing code. After it runs without issues, you can increase the percentage. Or, if you discover issues in production (errors or performance), you can turn it back to 0% while you troubleshoot the problem.

For example:

ruby
# app/controllers/graphql_controller.rb
exec_method = use_graphql_next? ? :execute_next : :execute
result = MyAppSchema.public_send(exec_method, query_string, context: { ... }, variables: { ... })
render json: result

Flipper is a great gem for feature flags. You could also roll your own or pick a third-party service.

Before using .execute_next to produce results for production traffic, you might want to run an experiment as described below.

Experiments

While the two runtime engines should return identical responses, it's possible that .execute_next will return a different result than .execute due to gem bugs or schema misconfigurations. You can check for this using an "experiment" system in your application which runs both execution engines and compares the result (for queries only!).

You'll want to use feature flagging to run the experiment on a subset of traffic, since it comes with performance overhead.

Here's some example code for a setup like this:

ruby
# app/controllers/graphql_controller.rb
result = MySchema.execute(...)

# Use a dynamic flag, eg Flipper. This should always be true in development and test.
if use_graphql_next_experiment?
  if !query_string.include?("mutation") && !query_string.include?("subscription") # easy way of checking for queries, could possibly have false negatives
    batched_result = MySchema.execute_next(...)
    if batched_result.to_h != result.to_h
      # Log this mismatch somehow here, avoiding potential PII/passwords:
      BugTracker.report <<~TXT
        A GraphQL query returned a non-identical response. Sanitized query string:

        #{result.query.sanitized_query_string}

        User: #{current_user.id}
        # Other context info here...
      TXT
    end
  end
end

See Scientist for a full-blown production experimentation system.

Combining feature flags and experiments

A fully-managed rollout would include two flags:

  • use_graphql_next_experiment?: when true, build an .execute_next response and compare it to the .execute response. But always return the .execute response.
  • use_graphql_next?: when true, use .execute_next and don't call .execute at all

This gives you full control over how production traffic is executed without needing to redeploy. You can always turn them down to 0% to get the current behavior.

Here's some example code:

ruby
if use_graphql_next? # again, use a dynamic feature flag
  result = MySchema.execute_next(...)
else
  result = MySchema.execute(...)
  if use_graphql_next_experiment?
    # Continue running the comparison experiment
  end
end

render json: result.to_h

Compatibility Notes

Execution::Next's new structure means that some GraphQL-Ruby features behave differently (or aren't supported at all, at least not yet). They are discussed one-by-one below.

Implicit Field Resolution

The default, implicit field resolution behavior has changed. Previously, when a field didn't have a specified method or hash key, GraphQL-Ruby would try a combination of object.public_send(...) and object[...] to resolve it. In Execution::Next, GraphQL-Ruby tries object.public_send(field_sym) unless another configuration is provided. This removes a lot of overhead from field execution.

Consider a field like this:

ruby
field :title, String

Previously, GraphQL-Ruby would check type_object.respond_to?(:title), object.respond_to?(:title), object.is_a?(Hash). object.key?(:title) and object.key?("title").

Now, GraphQL-Ruby simply calls object.title and allows the NoMethodError to bubble up if one is raised.

Interface Resolver Methods

Resolver methods are now class methods instead of instance methods. In order to make this work in interface modules, they must be defined in a resolver_methods do ... end block, for example:

ruby
module Node
  include BaseInterface

  field :id, ID, resolve_each: true

  resolver_methods do
    # This will define `def self.id` on Object types that implement this interface
    def id(object, context)
      GlobalId.new(object).to_s
    end
  end

  # Backwards compat instance method:
  def id
    self.class.id(object, context)
  end
end

Methods defined in resolver_methods { ... } will be copied into Object type classes as class methods, so they'll be available for resolve_{each|static|batch} fields.

Query Analyzers, including complexity 🟡

Support is identical; this runs before execution using the exact same code.

TODO: accessing loaded arguments inside analyzers may turn out to be slightly different; it still calls legacy code.

Authorization, Scoping

def (self.)authorized? and def self.scope_items will be called as needed during execution.

One incompatibility:

  • Argument #authorized? will be called if the argument wasn't present in the query but a default value is used. Execution::Next doesn't create the metadata necessary to skip authorization in that case. A work-around might be to check if the value is equal to the default value in def authorized? and permit it if it is. If this is a blocker for you, please open an issue on GitHub and we can check it out.

Visibility, including Changesets

Visibility works exactly as before; both runtime modules call the same methods to get type information from the schema.

Dataloader

Dataloader runs with new execution, but when migrating from instance methods to batch-level class methods, you may need to use Schema::Member::HasDataloader#dataload_all instead of .dataload.

Tracing

Fully supported, but some legacy hooks are not called. Implement the new hooks instead (existing runtime already calls these new hooks). Not called are:

  • execute_field, execute_field_lazy: use begin_execute_field, end_execute_field instead. (These may be called multiple times when Dataloader pauses or a GraphQL-Batch promise is returned)
  • execute_query, execute_query_lazy: use execute_multiplex for a top-level hook instead. (Single queries are always executed in a multiplex of size = 1.)
  • resolve_type, authorized: use {begin,end}_resolve_type and {begin,end}_authorized instead. (May be called multiple times for Dataloader etc.)

Additionally, object parameters to those methods will receive an Array of objects instead.

Lazy resolution (GraphQL-Batch)

Lazy resolution runs in the new execution (GraphQL-Batch is supported). When migrating to class methods, you may need to update your library method calls to work on a set of inputs rather than a single input.

`current_path` ❌

This is not supported because the new runtime doesn't actually produce current_path.

It is theoretically possible to support this but it will be a ton of work. If you use this for core runtime functions, please share your use case in a GitHub issue and we can investigate future options.

Scoped context ❌

This is currently implemented with current_path. Another implementation is probably possible but not implemented yet. Please open an issue to discuss.

`@defer` 🟡

@defer is supported with an implementation difference that probably doesn't affect your application: previously, @defer worked by pausing and resuming the same GraphQL::Query instance. However, with Execution::Next, @defer takes a different approach. Instead, when a GraphQL::Query encounters @defer, it notes the location in the document and stops executing that branch. Later, when you request the deferred result, that branch of the query is resumed using a new instance of GraphQL::Query::Partial.

This might matter if you're modifying context at runtime because those new instances also have fresh Query::Context instances. The original query context will get copied into the @defer branches using Query::Context.new(**original_query.context.to_h), so any custom values will be available. But if you assign new keys after the context is copied, those keys won't appear when running later @defered branches.

To handle this, you can refactor how you accumulate data during execution. Instead of ||='ing into context[...] during execution, assign a new accumulator object before starting the query, then call methods on that object to make any necessary state changes. That new object will be copied into @defer partials, and since the object is shared between the different branches, any necessary state changes will still be "seen" everywhere.

If this gives you trouble, please feel free to email me or open an issue on GitHub to discuss a migration strategy.

GraphQL-Batch support

When using Execution::Next, no custom code is required to support graphql-batch -- support is built-in.

`@stream`

@stream is supported.

See the not above about how @defer no longer resumes the original, top-level query. The same thing applies to @stream.

GraphQL::Pro::Stream now lazily streams Enumerators. If you were using the (undocumented) GraphQL::Pro::FutureStream, you can switch to GraphQL::Pro::Stream after migrating to Execution::Next. (Once all your traffic uses the new execution module, you'll get the same runtime behavior from GraphQL::Pro::Stream.)

ObjectCache

Supported completely.

Custom Directives ❌

There is some implementation in the code right now but it's not stable. Please open an issue to discuss.

Query-level directives are not implemented yet, but will be. Please open an issue if you have a use case for this.

`as:`

as: is applied: arguments are passed into Ruby methods by their as: names instead of their GraphQL names.

`loads:` 🟡

loads: is handled as previously, except that custom def load_... methods are not called.

`prepare:` 🟡

Procs are called as before.

Methods that depend on a runtime object (such as a type instance or Mutation class) are not called, because arguments are prepared before objects are ready.

`validates:` 🟡

Built-in validators are supported. Custom validators will always receive nil as the object. (object is no longer available; this API will probably change before this is fully released.)

Field Extensions 🟡

Field extension methods are called with new arguments:

  • objects: instead of object:, with an Array
  • values: instead of value:, with an Array

You can support both types of calls in your methods by changing the signature to object: nil, objects: nil (and value: nil, values: nil), then checking which argument was passed.

Resolver classes (including Mutations and Subscriptions) 🟡

Resolver classes are called, but with slightly different semantics:

  • #ready? is still called, but after arguments are loaded. It's now a useless method and will probably be deprecated.
  • def load_... methods are not called; instead, arguments are passed to the top-level Schema.object_from_id hook.

Field `extras:`, including `lookahead`

:ast_node and :lookahead are already implemented. Others are possible -- please raise an issue if you need one. extras: [:current_path] is not possible.

`raw_value` 🟡

Supported, but the raw_value call must be made on context, for example:

ruby
field :values, SomeObjectType, resolve_static: true

def self.values(context)
  context.raw_value(...)
end

Errors and `rescue_from` 🟡

Raising GraphQL::ExecutionError and adding rescue_from handlers are supported

Returning an array of GraphQL::ExecutionError instances is not supported anymore.

extras: [:execution_errors] and context.add_error are not supported anymore.

Connection fields

Connection arguments are automatically handled and connection wrapper objects are automatically applied to arrays and relations.

Custom Introspection

This works but if you want custom authorization or any lazy values, see notes about that compatibility.

If you're reimplementing default values, you'll need to add the corresponding resolve_static: true or resolve_each: true configurations. See the built-in type definitions under GraphQL::Introspection to get these configurations.

Multiplex

To use the new engine to run a multiplex, use MyAppSchema.multiplex_next(...) with the same arguments.

GraphQL::Current 🟡

current_field doesn't work; dataloader_source works. current_operation_name doesn't work.

This will be fixed soon but may require opt-in to avoid needless overhead.

`fallback_value:` ❌

fallback_value: is not supported in Execution::Next. It's not implemented because of the overhead it adds to resultion. You'll have to implement it by hand in resolvers.

graphql_migrate_execution creates a resolver that always returns the fallback_value. This might be right in some cases, but you'll probably have to implement your own method, like:

ruby
field :name, String, fallback_value: "Anonymous", resolve_each: :resolve_name

def resolve_name(object, context)
  if object.respond_to?(:name)
    object.name
  elsif (is_h = object.is_a?(Hash)) && object.key?(:name)
    object[:name]
  elsif is_h && object.key?("name")
    object["name"]
  else
    "Anonymous"
  end
end

GraphQL::Backtrace

Doesn't support Execution::Next, but it's probably not necessary. Execution::Next includes the field name in error messages and doesn't generate crazy-long stack traces because of its design.


Guides Execution Next (`guides/execution/next.md`)

New Execution Module

GraphQL-Ruby has a new execution engine, GraphQL::Execution::Next. It's much faster and less memory-consuming than the existing execution engine, but requires some care in migrating.

This feature is in heavy development, so if you give it a try and run into any problems, please open an issue on GitHub!

Background

Breadth-first GraphQL execution (or, "execution batching") is an algorithmic paradigm developed by Shopify to address problems of scale when resolving large lists and nested sets. Rather than paying field-level overhead costs (resolver calls, instrumentation, lazy promises, etc) for every field of every resolved object, the pattern instead incurs these costs only once per field selection and runs the corresponding breadth of objects with no additional overhead.

The original proof-of-concept of Shopify's core algorithm and white paper notes can be found in graphql-breadth-exec. That prototype matured into Shopify's proprietary GraphQL Cardinal execution engine that now runs much of their core traffic.

GraphQL-Ruby brings these breadth-first design principles to the open-source community with several novel techniques for implementing GraphQL:

  • Fields are resolved breadth-first using implicitly batched resolvers. These run longer and hotter on application logic with no execution overhead.
  • Batched resolvers may bind entire load sets to a single lazy promise to dramatically reduce promise bloat.
  • Error handling is optimized into a second pass that only runs when errors actually occur.
  • Stack profiling becomes much more organized with a linear flow and aggregate field spans, rather than fields getting split up across subtree repetitions.
  • The engine is driven by enqueuing rather than recursion, which shrinks stack traces and reduces memory usage.

Breadth-first patterns can produce dramatic results in responses with a high degree of repetition: it's not uncommon to see breadth batching run 15x faster and use 75% less memory than classic GraphQL Ruby execution. However – gains are relative. A flat tree with no lists will see little difference. A list of 2 resolving one field each will see a small gain, while a list of 100 resolving ten fields each will likely see dramatic results.

The downside is that many of GraphQL-Ruby's "bonus features" -- those that go beyond the behavior described in the GraphQL Specification -- add non-trivial overhead when used. So, the task ahead is to "lift the ceiling" of performance in GraphQL-Ruby while retaining as much compatibility as possible and supporting a gradual transition to this new runtime engine.

Enabling Execution::Next

The new execution engine is enabled with two steps:

  • Add the plugin to your schema with use GraphQL::Execution::Next
  • Call MySchema.execute_next(...) instead of MySchema.execute(...). It takes the same arguments.

See compatibility notes for updating your schema to run queries with the new engine.

You can also add ..., as_default: true to use execute_next by default. In that case, call execute_legacy if you need the old runtime.

Field configurations

The new runtime engine supports several field resolution configurations out of the box.

Method calls (default, `method:`)

These fields call object.#{field_name}. This is the default, and the method name can be overridden with method: ...:

ruby
field :title, String # calls object.title
field :title, String, method: :get_title_somehow # calls object.get_title_somehow

Hash keys (`hash_key:`)

These fields call object[hash_key], configured with hash_key: ....

ruby
field :title, String, hash_key: :title # calls object[:title]
field :title, String, hash_key: "title" # calls object["title"]

Note: new execution doesn't "fall back" to hash key lookups, and it doesn't try strings when Symbols are given. The existing runtime engine does that, but it has been excluded for performance reasons. To get the old resolution behavior, you can code it like:

ruby
field :title, String, resolve_each: true

def self.title(object, context)
  # For example, try a symbol key first, then a string:
  object[:title] || object["title"]
end

Per-object (`resolve_each:`)

These fields use a class method to produce a result for each object, configured with resolve_each:.

ruby
field :title, String, resolve_each: true do # calls `self.title(...)` below
  argument :language, Types::Language, required: false, default_value: "EN"
end

def self.title(object, context, language:)
  # Assuming this makes no database lookups or other external service calls:
  object.localization.get(:title, language:)
end

The default method is the same as the field name symbol. You can also provide a custom method:

ruby
# Avoid a conflict with Ruby's built-in `Class#name`:
field :name, String, resolve_each: :get_name

def self.get_name(object, context)
  # ...
end

Under the hood, GraphQL-Ruby calls objects.map { ... }, calling this class method.

‼️ Don't use this if your logic calls external services or databases (including with Dataloader). If you do, your I/O will be sequential instead of batched. Use resolve_batch: or resolve_static: instead, see below.

Global (`resolve_static:`)

Fields that use a class method to produce a single result shared by all objects, configured with resolve_static:. The method does not receive any object, only context:

ruby
field :posts_count, Integer, resolve_static: :count_all_posts do
  argument :include_unpublished, Boolean, required: false, default_value: false
end

def self.count_all_posts(context, include_unpublished:)
  posts = Post.all
  if !include_unpublished
    posts = posts.published
  end
  posts.count
end

Under the hood, GraphQL-Ruby calls Array.new(objects.size, static_result).

Batch resolvers (`resolve_batch:`)

This is a high-performance option for when you need to do I/O to generate results. By working with a batch of objects, you can greatly reduce the framework overhead in preparing a result.

These fields use a class method to map parent objects to field results, configured with resolve_batch::

ruby
field :title, String, resolve_batch: true do # calls self.title below
  argument :language, Types::Language, required: false, default_value: "EN"
end

def self.title(objects, context, language:)
  # This is equivalent to plain `field :title, ...`, but for example:
  objects.map { |obj| obj.title(language:) }
end

This is especially useful when batching Dataloader calls:

ruby
class Types::Comment < BaseObject
  field :author_rating, Integer, resolve_batch: true

  def self.author_rating(objects, context)
    authors = context.dataload_all_records(objects, :author)
    context.dataload_all(Sources::AuthorRating, authors)
  end
end

By default, it calls a class method matching the field name. You can customize this configuration, too:

ruby
field :author_rating, Integer, resolve_batch: :calculate_rating # calls `self.calculate_rating(objects, context)`

def self.calculate_rating(objects, context)
  # ...
end

Dataloader

Execution::Next supports field configuration shorthands for common dataloader usage. Under the hood, these make sure data fetching is batched and cached.

method: can be added to any hash-style dataload: config to call a public method on the loaded value.

Sources

Use a custom dataloader source from your application:

ruby
class Types::CommentType
  # Equivalent to `dataload(Sources::CommentRating, object)`
  field :rating, Integer, dataload: Sources::CommentRating

  # `using:`: A method to call to get a value to pass to dataloader
  # `by: [...]`: An array of arguments to pass on to dataloader
  #
  # Equivalent to `dataload(Sources::ReadingDuration, :comment, object.body)
  field :reading_duration, Integer, dataload: { with: Sources::ReadingDuration, using: :body, by: [:comment] }

  # `method:`: A method to call on the loaded value
  #
  # Equivalent to `dataload(Sources::CommentRating, object).to_s`
  field :rating_string, String, dataload: { with: Sources::CommentRating, method: :to_s }

Rails Associations

Load ActiveRecord associations using GraphQL::Dataloader::ActiveRecordAssociationSource:

ruby
class Types::CommentType < Types::BaseObject
  # Equivalent to `dataload_association(:post)`
  field :post, Types::Post, dataload: { association: true }
  # Equivalent to `dataload_association(:user)
  field :author, Types::Post, dataload: { association: :user }
  # Equivalent to `dataload_association(:author)&.name`
  field :author_name, String, null: true, dataload: { association: :author, method: :name }
end

Rails Records

Load ActiveRecord associations using GraphQL::Dataloader::ActiveRecordSource.

ruby
class Types::SearchResult < Types::BaseObject
  # Equivalent to `dataload_record(Post, object.post_id)`
  field :post, Types::Post, dataload: { model: Post, using: :post_id }
  # Equivalent to `dataload_record(User, object.created_by_handle, find_by: :handle)`
  field :author, Types::User, dataload: { model: User, using: :created_by_handle, find_by: :handle }
  # Equivalent to `dataload_record(Post, object.post_id)&.title`
  field :post_title, String, null: true, dataload: { model: Post, using: :post_id, method: :title }
end

Legacy instance methods

resolve_legacy_instance_method:

There is partial support for instance methods on Object type classes, for now. It will be deprecated and removed soon.

‼️ Don't use legacy instance methods with Dataloader. It will be sequential, not batched. ‼️

ruby
field :title, String, resolve_legacy_instance_method: true do
  argument :language, Types::Language, required: false, default_value: "EN"
end

def title(language:)
  # Assuming this makes no database lookups or other external service calls:
  object.localization.get(:title, language:)
end

Under the hood, GraphQL-Ruby calls objects.map { ... }, calling this instance method. It adds significant overhead because GraphQL-Ruby initializes the object type class.

`true` shorthand

There is also a true shorthand: when one of the resolve_...: configurations is passed as true (ie, resolve_batch: true, resolve_each: true, resolve_static: true, or resolve_legacy_instance_method: true), then the Symbol field name is used as the class method. For example:

ruby
field :posts_count, Integer, resolve_static: true

def self.posts_count(context)
  Post.all.count
end

Migration

Read about migrating in the Migration Doc.


Guides Fields Arguments (`guides/fields/arguments.md`)

Arguments

Arguments are part of the GraphQL::Schema::Argument API. The complete
reference, including nullability, defaults, deprecation, aliasing,
preprocessing, camelization, and supported input types, is maintained with
the implementation:

GraphQL::Schema::Argument

Use that API reference for the exact argument(...) parameters and runtime
behavior. This guide remains the entry point for field arguments and connects
the topic to the fields and
validation guides.


Guides Fields Introduction (`guides/fields/introduction.md`)

Field definitions

Field definitions are part of the GraphQL::Schema::Field API. The complete
reference, including names, return types, descriptions, resolution behavior,
arguments, extras, and default options, is maintained with the implementation:

GraphQL::Schema::Field

Use that API reference when you need the exact field(...) parameters and
runtime behavior. This guide remains the entry point for the fields section
and links to the related objects,
interfaces, and
arguments guides.

Field Names

See the Field API reference.

Field Return Type

See the Field API reference.

Field Documentation

See the Field API reference.

Field Resolution

See the Field API reference.

Field Arguments

See the Field API reference.

Extra Field Metadata

See the Field API reference.

Field Parameter Default Values

See the Field API reference.


Guides Fields Limits (`guides/fields/limits.md`)

Limits

List Fields

Always limit the number of items which can be returned from a list field. For example, use a limit: argument and make sure it's not too big. The prepare: function provides a convenient place to cap the number of items:

ruby
field :items, [Types::ItemType] do
  # Cap the number of items at 30
  argument :limit, Integer, default_value: 20, prepare: ->(limit, ctx) {[limit, 30].min}
end

def items(limit:)
  object.items.limit(limit)
end

This way, you won't hit your database for 1000 items!

Connections

Connections accept a max_page_size option which limits the number of nodes.


Guides Fields Resolvers (`guides/fields/resolvers.md`)

Resolvers

A GraphQL::Schema::Resolver is a container for field signature and resolution logic. It can be attached to a field with the resolver: keyword:

ruby
# Use the resolver class to execute this field
field :pending_orders, resolver: PendingOrders

Under the hood, GraphQL::Schema::Mutation is a specialized subclass of Resolver.

First, ask yourself ...

Do you really need a Resolver? Putting logic in a Resolver has some downsides:

  • Since it's coupled to GraphQL, it's harder to test than a plain ol' Ruby object in your app
  • Since the base class comes from GraphQL-Ruby, it's subject to upstream changes which may require updates in your code

Here are a few alternatives to consider:

  • Put display logic (sorting, filtering, etc.) into a plain ol' Ruby class in your app, and test that class
  • Hook up that object with a method, for example:
ruby
field :recommended_items, [Types::Item], null: false
def recommended_items
  ItemRecommendation.new(user: context[:viewer]).items
end
  • If you have lots of arguments to share, use a class method to generate fields, for example:
ruby
# Generate a field which returns a filtered, sorted list of items
def self.items_field(name, override_options, &block)
  # Prepare options
  default_field_options = { type: [Types::Item], null: false }
  field_options = default_field_options.merge(override_options)
  # Create the field
  field(name, **field_options) do
    argument :order_by, Types::ItemOrder, required: false
    argument :category, Types::ItemCategory, required: false
    # Allow an override block to add more arguments
    instance_eval(&block) if block_given?
  end
end

# Then use the generator to create a field:
items_field(:recommended_items) do |field|
  field.argument :similar_to_product_id, ID, required: false
end
# Implement the field
def recommended_items
  # ...
end

As a matter of code organization, that class method could be put in a module and shared between different classes that need it.

  • If you need the same logic shared between several objects, consider using a Ruby module and its self.included hook, for example:
ruby
module HasRecommendedItems
  def self.included(child_class)
    # attach the field here
    child_class.field(:recommended_items, [Types::Item], null: false)
  end

  # then implement the field
  def recommended_items
    # ...
  end
end

# Add the field to some objects:
class Types::User < BaseObject
  include HasRecommendedItems # adds the field
end
  • If the module approach looks good to you, also consider Interfaces. They also share behavior between objects (since they're just modules that get included, after all), and they expose that commonality to clients via introspection.

When do you really need a resolver?

So, if there are other, better options, why does Resolver exist? Here are a few specific advantages:

  • Isolation. A Resolver is instantiated for each call to the field, so its instance variables are private to that object. If you need to use instance variables for some reason, this helps. You have a guarantee that those values won't hang around when the work is done.
  • Complex Schema Generation. RelayClassicMutation (which is a Resolver subclass) generates input types and return types for each mutation. Using a Resolver class makes it easier to implement, share and extend this code generation logic.

Using `resolver`

Use the base resolver class:

ruby
module Resolvers
  class RecommendedItems < BaseResolver
    type [Types::Item], null: false
    description "Items this user might like"

    argument :order_by, Types::ItemOrder, required: false
    argument :category, Types::ItemCategory, required: false

    def resolve(order_by: nil, category: nil)
      # call your application logic here:
      recommendations = ItemRecommendation.new(
        viewer: context[:viewer],
        recommended_for: object,
        order_by: order_by,
        category: category,
      )
      # return the list of items
      recommendations.items
    end
  end
end

And attach it to your field:

ruby
class Types::User < Types::BaseObject
  field :recommended_items, resolver: Resolvers::RecommendedItems
end

Since the Resolver lifecycle is managed by the GraphQL runtime, the best way to test it is to execute GraphQL queries and check the results.

Nesting resolvers of the same type

You may run into cyclical loading issues when using a resolver within the definition of the type the resolver returns e.g.

ruby
# app/graphql/types/query_type.rb

module Types
  class QueryType < Types::BaseObject
    field :tasks, resolver: Resolvers::TasksResolver
  end
end

# app/graphql/types/task_type.rb

module Types
  class TaskType < Types::BaseObject
    field :title, String, null: false
    field :tasks, resolver: Resolvers::TasksResolver
  end
end

# app/graphql/resolvers/tasks_resolver.rb

module Resolvers
  class TasksResolver < BaseResolver
    type [Types::TaskType], null: false

    def resolve
      []
    end
  end
end

The above can produce the following error: Failed to build return type for Task.tasks from nil: Unexpected type input: (NilClass).

A simple solution is to express the type as a string in the resolver:

ruby
module Resolvers
  class TasksResolver < BaseResolver
    type "[Types::TaskType]", null: false

    def resolve
      []
    end
  end
end

In doing so, you can defer the loading of the type class until the nested resolver has already been loaded.

Extensions

In cases when you want your resolvers to add some extensions to the field they resolve, you can use extension method, which accepts extension class and options. Multiple extensions can be configured for a single resolver.

ruby
class GreetingExtension < GraphQL::Schema::FieldExtension
  def resolve(object:, arguments:, **rest)
    name = yield(object, arguments)
    "#{options[:greeting]}, #{name}!"
  end
end

class ResolverWithExtension < BaseResolver
  type String, null: false

  extension GreetingExtension, greeting: "Hi"

  def resolve
    "Robert"
  end
end

Guides Fields Validation (`guides/fields/validation.md`)

Validation

Arguments can be validated at runtime using built-in or custom validators.

Validations are configured in argument(...) calls on fields or input objects:

ruby
argument :home_phone, String,
  description: "A US phone number",
  validates: { format: { with: /\d{3}-\d{3}-\d{4}/ } }

or, validates required: { ... } inside a field ... do ... end block:

ruby
field :comments, [Comment],
  description: "Find comments by author ID or author name" do
  argument :author_id, ID, required: false
  argument :author_name, String, required: false
  # Either `authorId` or `authorName` must be provided by the client, but not both:
  validates required: { one_of: [:author_id, :author_name] }
end

Validations can be provided with a keyword (validates: { ... }) or with a method inside the configuration block (validates ...).

Built-In Validations

See each validator's API docs for details:

Some of the validators accept customizable messages for certain validation failures; see the API docs for examples.

allow_blank: and allow_null: may affect other validations, for example:

ruby
validates: { format: { with: /\A\d{4}\Z/ }, allow_blank: true }

Will permit any String containing four digits, or the empty string ("") if Rails is loaded. (GraphQL-Ruby checks for .blank?, which is usually defined by Rails.)

Alternatively, they can be used alone, for example:

ruby
argument :id, ID, required: false, validates: { allow_null: true }

Will permit any query that passes id: null.

Validation options may also be passed as procs that accept no parameters, for example:

ruby
validates :title, String, validates: { format: { with: -> { Flipper.enabled?(:new_title_format) ? NEW_TITLE_FORMAT : /.*/ }}

Custom Validators

You can write custom validators, too. A validator is a class that extends GraphQL::Schema::Validator. It should implement:

  • def initialize(..., **default_options) to accept any validator-specific options and pass along the defaults to super(**default_options)
  • def validate(_empty, context, value) which is called at runtime to validate value. It may return a String error message or an Array of Strings. GraphQL-Ruby will add those messages to the top-level "errors" array along with runtime context information.

Then, custom validators can be attached either:

  • directly, passed to validates, like validates: { MyCustomValidator => { some: :options }
  • by keyword, if the keyword is registered with GraphQL::Schema::Validator.install(:custom, MyCustomValidator). (That would support validates: { custom: { some: :options }}).)

Validators are initialized when the schema is constructed (at application boot), and validate(...) is called while executing the query. There's one Validator instance for each configuration on each field, argument, or input object. (Validator instances aren't shared.)


Guides Javascript Client Apollo Subscriptions (`guides/javascript_client/apollo_subscriptions.md`)

Apollo Subscriptions

GraphQL-Ruby's JavaScript client includes several kinds of support for Apollo Client:

Apollo Links are used by Apollo client 2.x and 3.x.

graphql-ruby-client includes support for subscriptions with Pusher and ApolloLink.

To use it, add PusherLink before your HttpLink.

For example:

js
// Load Apollo stuff
import { ApolloClient, HttpLink, ApolloLink, InMemoryCache } from "@apollo/client";
// Load PusherLink from graphql-ruby-client
import PusherLink from 'graphql-ruby-client/subscriptions/PusherLink';

// Load Pusher and create a client
import Pusher from "pusher-js"
var pusherClient = new Pusher("your-app-key", { cluster: "us2" })

// Make the HTTP link which actually sends the queries
const httpLink = new HttpLink({
  uri: '/graphql',
  credentials: 'include'
});

// Make the Pusher link which will pick up on subscriptions
const pusherLink = new PusherLink({pusher: pusherClient})

// Combine the two links to work together
const link = ApolloLink.from([pusherLink, httpLink])

// Initialize the client
const client = new ApolloClient({
  link: link,
  cache: new InMemoryCache()
});

This link will check responses for the X-Subscription-ID header, and if it's present, it will use that value to subscribe to Pusher for future updates.

If you're using compressed payloads, configure a decompress: function, too:

javascript
// Add `pako` to the project for gunzipping
import pako from "pako"

const pusherLink = new PusherLink({
  pusher: pusherClient,
  decompress: function(compressed) {
    // Decode base64
    const data = atob(compressed)
      .split('')
      .map(x => x.charCodeAt(0));
    // Decompress
    const payloadString = pako.inflate(new Uint8Array(data), { to: 'string' });
    // Parse into an object
    return JSON.parse(payloadString);
  }
})

graphql-ruby-client includes support for subscriptions with Ably and ApolloLink.

To use it, add AblyLink before your HttpLink.

For example:

js
// Load Apollo stuff
import { ApolloClient, HttpLink, ApolloLink, InMemoryCache } from '@apollo/client';
// Load Ably subscriptions link
import AblyLink from 'graphql-ruby-client/subscriptions/AblyLink'
// Load Ably and create a client
const Ably = require("ably")
const ablyClient = new Ably.Realtime({ key: "your-app-key" })

// Make the HTTP link which actually sends the queries
const httpLink = new HttpLink({
  uri: '/graphql',
  credentials: 'include'
});

// Make the Ably link which will pick up on subscriptions
const ablyLink = new AblyLink({ably: ablyClient})

// Combine the two links to work together
const link = ApolloLink.from([ablyLink, httpLink])

// Initialize the client
const client = new ApolloClient({
  link: link,
  cache: new InMemoryCache()
});

This link will check responses for the X-Subscription-ID header, and if it's present, it will use that value to subscribe to Ably for future updates.

For your app key, make a key with "Subscribe" and "Presence" privileges and use that:

graphql-ruby-client includes support for subscriptions with ActionCable and ApolloLink.

To use it, construct a split link that routes:

  • subscription queries to an ActionCableLink; and
  • other queries to an HttpLink

For example:

js
import { ApolloClient, HttpLink, ApolloLink, InMemoryCache } from '@apollo/client';
import { createConsumer } from '@rails/actioncable';
import ActionCableLink from 'graphql-ruby-client/subscriptions/ActionCableLink';

const cable = createConsumer()

const httpLink = new HttpLink({
  uri: '/graphql',
  credentials: 'include'
});

const hasSubscriptionOperation = ({ query: { definitions } }) => {
  return definitions.some(
    ({ kind, operation }) => kind === 'OperationDefinition' && operation === 'subscription'
  )
}

const link = ApolloLink.split(
  hasSubscriptionOperation,
  new ActionCableLink({cable}),
  httpLink
);

const client = new ApolloClient({
  link: link,
  cache: new InMemoryCache()
});

Note that for Rails 5, the ActionCable client package is actioncable, not @rails/actioncable.

Apollo 1

graphql-ruby-client includes support for Apollo 1 client subscriptions over Pusher or ActionCable.

To use it, require subscriptions/addGraphQLSubscriptions and call the function with your network interface and transport client (example below).

See the Subscriptions guide for information about server-side setup.

Apollo 1 -- Pusher

Pass {pusher: pusherClient} to use Pusher:

js
// Load Pusher and create a client
var Pusher = require("pusher-js")
var pusherClient = new Pusher(appKey, options)

// Add subscriptions to the network interface with the `pusher:` options
import addGraphQLSubscriptions from "graphql-ruby-client/subscriptions/addGraphQLSubscriptions"
addGraphQLSubscriptions(myNetworkInterface, {pusher: pusherClient})

// Optionally, add persisted query support:
var OperationStoreClient = require("./OperationStoreClient")
RailsNetworkInterface.use([OperationStoreClient.apolloMiddleware])

If you're using compressed payloads, configure a decompress: function, too:

javascript
// Add `pako` to the project for gunzipping
import pako from "pako"

addGraphQLSubscriptions(myNetworkInterface, {
  pusher: pusherClient,
  decompress: function(compressed) {
    // Decode base64
    const data = btoa(compressed)
    // Decompress
    const payloadString = pako.inflate(data, { to: 'string' })
    // Parse into an object
    return JSON.parse(payloadString);
  }
})

Apollo 1 -- ActionCable

By passing {cable: cable}, all subscription queries will be routed to ActionCable.

For example:

js
// Load ActionCable and create a consumer
var ActionCable = require('@rails/actioncable')
var cable = ActionCable.createConsumer()
window.cable = cable

// Load ApolloClient and create a network interface
var apollo = require('apollo-client')
var RailsNetworkInterface = apollo.createNetworkInterface({
 uri: '/graphql',
 opts: {
   credentials: 'include',
 },
 headers: {
   'X-CSRF-Token': $("meta[name=csrf-token]").attr("content"),
 }
});

// Add subscriptions to the network interface
import addGraphQLSubscriptions from "graphql-ruby-client/subscriptions/addGraphQLSubscriptions"
addGraphQLSubscriptions(RailsNetworkInterface, {cable: cable})

// Optionally, add persisted query support:
var OperationStoreClient = require("./OperationStoreClient")
RailsNetworkInterface.use([OperationStoreClient.apolloMiddleware])

Guides Javascript Client Graphiql Subscriptions (`guides/javascript_client/graphiql_subscriptions.md`)

GraphiQL Subscriptions

After setting up your server, you can integrate subscriptions into GraphiQL, the in-browser GraphQL IDE.

Adding GraphiQL to your app

To get started, make a page for rendering GraphiQL, for example:

html
<div id="root" style="height: 100vh;"></div>

Then, install GraphiQL (eg, yarn add graphiql) and add JavaScript code to import GraphiQL and render it on your page:

js
import { GraphiQL } from 'graphiql'
import React from 'react'
import { createRoot } from 'react-dom/client'
import 'graphiql/graphiql.css'
import { createGraphiQLFetcher } from '@graphiql/toolkit'

const fetcher = createGraphiQLFetcher({ url: '/graphql' })
const root = createRoot(document.getElementById('root'))
root.render(<GraphiQL fetcher={fetcher}/>)

After that, you should be able to load the page in your app and see the GraphiQL editor.

Ably

To integrate Ably subscriptions, use createAblyFetcher, for example:

js
import Ably from "ably"
import createAblyFetcher from 'graphql-ruby-client/subscriptions/createAblyFetcher'

// Initialize a client
// the key must have "subscribe" and "presence" permissions
const ably = new Ably.Realtime({ key: "your.application.key" })

// Initialize a new fetcher and pass it to GraphiQL below
var fetcher = createAblyFetcher({ ably: ably, url: "/graphql" })
const root = createRoot(document.getElementById('root'))
root.render(<GraphiQL fetcher={fetcher} />)

Under the hood, it will use window.fetch to send GraphQL operations to the server, then listen for X-Subscription-ID headers in responses. To customize its HTTP requests, you can pass a fetchOptions: object or a custom fetch: function to createAblyFetcher({ ... }).

Pusher

To integrate Pusher subscriptions, use createPusherFetcher, for example:

js
import Pusher from "pusher-js"
import createPusherFetcher from 'graphql-ruby-client/subscriptions/createPusherFetcher'

// Initialize a client
const pusher = new Pusher("your-app-key", { cluster: "your-cluster" })

// Initialize a new fetcher and pass it to GraphiQL below
var fetcher = createPusherFetcher({ pusher: pusher, url: "/graphql" })
const root = createRoot(document.getElementById('root'))
root.render(<GraphiQL fetcher={fetcher} />)

Under the hood, it will use window.fetch to send GraphQL operations to the server, then listen for X-Subscription-ID headers in responses. To customize its HTTP requests, you can pass a fetchOptions: object or a custom fetch: function to createPusherFetcher({ ... }).

ActionCable

To integrate ActionCable subscriptions, use createActionCableFetcher, for example:

js
import { createConsumer } from "@rails/actioncable"
import createActionCableFetcher from 'graphql-ruby-client/subscriptions/createActionCableFetcher';

// Initialize a client
const actionCable = createConsumer()

// Initialize a new fetcher and pass it to GraphiQL below
var fetcher = createActionCableFetcher({ consumer: actionCable, url: "/graphql" })
const root = createRoot(document.getElementById('root'))
root.render(<GraphiQL fetcher={fetcher} />)

Under the hood, it will split traffic: it will send subscription { ... } operations via ActionCable and send queries and mutations via HTTP POST using window.fetch. To customize its HTTP requests, you can pass a fetchOptions: object or a custom fetch: function to createActionCableFetcher({ ... }).


Guides Javascript Client Relay Subscriptions (`guides/javascript_client/relay_subscriptions.md`)

Relay Subscriptions

graphql-ruby-client includes three kinds of support for subscriptions with Relay Modern:

To use it, require graphql-ruby-client/subscriptions/createRelaySubscriptionHandler and call the function with your client and optionally, your OperationStoreClient.

Note: For Relay <11, use import { createLegacyRelaySubscriptionHandler } from "graphql-ruby-client/subscriptions/createRelaySubscriptionHandler" instead; the signature changed in Relay 11.

See the Subscriptions guide for information about server-side setup.

Pusher

Subscriptions with Pusher require two things:

Pusher client

Pass pusher: to get Subscription updates over Pusher:

js
// Load the helper function
import createRelaySubscriptionHandler from "graphql-ruby-client/subscriptions/createRelaySubscriptionHandler"

// Prepare a Pusher client
var Pusher = require("pusher-js")
var pusherClient = new Pusher(appKey, options)

// Create a fetchOperation, see below for more details
function fetchOperation(operation, variables, cacheConfig) {
  return fetch(...)
}

// Create a Relay Modern-compatible handler
var subscriptionHandler = createRelaySubscriptionHandler({
  pusher: pusherClient,
  fetchOperation: fetchOperation
})

// Create a Relay Modern network with the handler
var network = Network.create(fetchQuery, subscriptionHandler)

Compressed Payloads

If you're using compressed payloads, configure a decompress: function, too:

javascript
// Add `pako` to the project for gunzipping
import pako from "pako"

var subscriptionHandler = createRelaySubscriptionHandler({
  pusher: pusherClient,
  fetchOperation: fetchOperation,
  decompress: function(compressed) {
    // Decode base64
    const data = btoa(compressed)
    // Decompress
    const payloadString = pako.inflate(data, { to: 'string' })
    // Parse into an object
    return JSON.parse(payloadString);
  }
})

Ably

Subscriptions with Ably require two things:

Ably client

Pass ably: to get Subscription updates over Ably:

js
// Load the helper function
import createRelaySubscriptionHandler from "graphql-ruby-client/subscriptions/createRelaySubscriptionHandler"

// Load Ably and create a client
const Ably = require("ably")
const ablyClient = new Ably.Realtime({ key: "your-app-key" })

// create a fetchOperation, see below for more details
function fetchOperation(operation, variables, cacheConfig) {
  return fetch(...)
}

// Create a Relay Modern-compatible handler
var subscriptionHandler = createRelaySubscriptionHandler({
  ably: ablyClient,
  fetchOperation: fetchOperation
})

// Create a Relay Modern network with the handler
var network = Network.create(fetchQuery, subscriptionHandler)

ActionCable

With this configuration, subscription queries will be routed to ActionCable.

For example:

js
// Require the helper function
import createRelaySubscriptionHandler from "graphql-ruby-client/subscriptions/createRelaySubscriptionHandler")
// Optionally, load your OperationStoreClient
var OperationStoreClient = require("./OperationStoreClient")

// Create a Relay Modern-compatible handler
var subscriptionHandler = createRelaySubscriptionHandler({
  cable: createConsumer(...),
  operations: OperationStoreClient,
})

// Create a Relay Modern network with the handler
var network = Network.create(fetchQuery, subscriptionHandler)

With Relay Persisted Queries

If you're using Relay's built-in persisted query support, you can pass clientName: to the handler in order to build IDs that work with the OperationStore. For example:

js
var subscriptionHandler = createRelaySubscriptionHandler({
  cable: createConsumer(...),
  clientName: "web-frontend", // This should match the one you use for `sync`
})

// Create a Relay Modern network with the handler
var network = Network.create(fetchQuery, subscriptionHandler)

Then, the ActionCable handler will use Relay's provided operation IDs to interact with the OperationStore.

fetchOperation function

The fetchOperation function can be extracted from your fetchQuery function. Its signature is:

js
// Returns a promise from `fetch`
function fetchOperation(operation, variables, cacheConfig) {
  return fetch(...)
}
  • operation, variables, and cacheConfig are the first three arguments to the fetchQuery function.
  • The function should call fetch and return the result (a Promise of a Response).

For example, Environment.js may look like:

js
// This function sends a GraphQL query to the server
const fetchOperation = function(operation, variables, cacheConfig) {
  const bodyValues = {
    variables,
    operationName: operation.name,
  }
  const useStoredOperations = process.env.NODE_ENV === "production"
  if (useStoredOperations) {
    // In production, use the stored operation
    bodyValues.operationId = OperationStoreClient.getOperationId(operation.name)
  } else {
    // In development, use the query text
    bodyValues.query = operation.text
  }
  return fetch('http://localhost:3000/graphql', {
    method: 'POST',
    opts: {
      credentials: 'include',
    },
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(bodyValues),
  })
}

// `fetchQuery` uses `fetchOperation`, but returns a Promise of JSON
const fetchQuery = (operation, variables, cacheConfig, uploadables) => {
  return fetchOperation(operation, variables, cacheConfig).then(response => {
    return response.json()
  })
}

// Subscriptions uses the same `fetchOperation` function for initial subscription requests
const subscriptionHandler = createRelaySubscriptionHandler({pusher: pusherClient, fetchOperation: fetchOperation})
// Combine them into a `Network`
const network = Network.create(fetchQuery, subscriptionHandler)

Since OperationStoreClient is in the fetchOperation function, it will apply to all GraphQL operations.


Guides Javascript Client Sync (`guides/javascript_client/sync.md`)

OperationStore Sync

JavaScript support for GraphQL projects using graphql-pro's OperationStore for persisted queries.

See the OperationStore guide for server-side setup.

`sync` utility

This package contains a command line utility, graphql-ruby-client sync:

text
$ graphql-ruby-client sync # ...
Authorizing with HMAC
Syncing 4 operations to http://myapp.com/graphql/operations...
  3 added
  1 not modified
  0 failed
Generating client module in app/javascript/graphql/OperationStoreClient.js...
✓ Done!

sync Takes several options:

option description
--url Sync API url
--path Local directory to search for .graphql / .graphql.js files
--relay-persisted-output Path to a .json file from relay-compiler ... --persist-output
--apollo-codegen-json-output Path to a .json file from apollo client:codegen ... --target json
--apollo-android-operation-output Path to an OperationOutput.json file from Apollo Android
--client Client ID (created on server)
--secret Client Secret (created on server)
--outfile Destination for generated code
--outfile-type What kind of code to generate (js or json)
--header={key}:{value} Add a header to the outgoing HTTP request (may be repeated)
--add-typename Add __typename to all selection sets (for use with Apollo Client)
--verbose Output some debug information
--changeset-version Set a Changeset Version when syncing these queries. (context[:changeset_version] will also be required at runtime, when running these stored operations.)
--dump-payload A file to write the HTTP Post payload into, or if no filename is passed, then the payload will be written to stdout.

You can see these and a few others with graphql-ruby-client sync --help.

Use with Relay <2

graphql-ruby-client can persist queries from relay-compiler using the embedded @relayHash value. (This was created in Relay before 2.0.0. See below for Relay 2.0+.)

To sync your queries with the server, use the --path option to point to your __generated__ directory, for example:

bash
# sync a Relay project
$ graphql-ruby-client sync --path=src/__generated__  --outfile=src/OperationStoreClient.js --url=...

Then, the generated code may be integrated with Relay's Network Layer:

js
// ...
// require the generated module:
const OperationStoreClient = require('./OperationStoreClient')

// ...
function fetchQuery(operation, variables, cacheConfig, uploadables) {
  const requestParams = {
    variables,
    operationName: operation.name,
  }

  if (process.env.NODE_ENV === "production")
    // In production, use the stored operation
    requestParams.operationId = OperationStoreClient.getOperationId(operation.name)
  } else {
    // In development, use the query text
    requestParams.query = operation.text,
  }

  return fetch('/graphql', {
    method: 'POST',
    headers: { /*...*/ },
    body: JSON.stringify(requestParams),
  }).then(/* ... */);
}

// ...

(Only Relay Modern is supported. Legacy Relay can't generate static queries.)

Use With Relay Persisted Output

To use Relay's persisted output, add a "file": ... to your project's persistConfig object. For example:

json
"relay": {
    ...
    "persistConfig": {
      "file": "./persisted-queries.json"
    }
  },

Then, push Relay's generated queries to your OperationStore server with --relay-persisted-output:

text
$ graphql-ruby-client sync --relay-persisted-output=path/to/persisted-queries.json --url=...

In this case, sync won't generate a JavaScript module because relay-compiler has already prepared its queries for persisted use. Instead, update your network layer to include the client name and operation id in the HTTP params:

js
const operationStoreClientName = "MyRelayApp";

function fetchQuery(operation, variables,) {
  return fetch('/graphql', {
    method: 'POST',
    headers: {
      'content-type': 'application/json'
    },
    body: JSON.stringify({
      // Pass the client name and the operation ID, joined by `/`
      documentId: operationStoreClientName + "/" + operation.id,
      // query: operation.text, // this is now obsolete because text is null
      variables,
    }),
  }).then(response => {
    return response.json();
  });
}

(Inspired by https://relay.dev/docs/guides/persisted-queries/#network-layer-changes.)

Now, your Relay app will only send operation IDs over the wire to the server.

Use with Apollo Client

Use the --path option to point at your .graphql files:

text
$ graphql-ruby-client sync --path=src/graphql/ --url=...

Then, load the generated module and add its .apolloMiddleware to your network interface with .use([...]):

js
// load the generated module
var OperationStoreClient = require("./OperationStoreClient")

// attach it as middleware in production
// (in development, send queries to the server as normal)
if (process.env.NODE_ENV === "production") {
  MyNetworkInterface.use([OperationStoreClient.apolloMiddleware])
}

Now, the middleware will replace query strings with operationIds.

Use the --path option to point at your .graphql files:

text
$ graphql-ruby-client sync --path=src/graphql/ --url=...

Then, load the generated module and add its .apolloLink to your Apollo Link:

js
// load the generated module
var OperationStoreClient = require("./OperationStoreClient")

// Integrate the link to another link:
const link = ApolloLink.from([
  authLink,
  OperationStoreClient.apolloLink,
  httpLink,
])

// Create a client
const client = new ApolloClient({
  link: link,
  cache: new InMemoryCache(),
});

Update the controller: Apollo Link supports extra parameters nested as params[:extensions][:operationId], so update your controller to add that param to context:

ruby
# app/controllers/graphql_controller.rb
context = {
  # ...
  # Support Apollo Link:
  operation_id: params[:extensions][:operationId]
}

Now, context[:operation_id] will be used to fetch a query from the database.

Use with Apollo Codegen

Use apollo client:codegen ... --target json to build a JSON artifact containing your app's queries. Then, pass the path to that artifact to graphql-ruby-client sync --apollo-codegen-json-output path/to/output.json .... sync will use Apollo-generated operationIds to populate the OperationStore.

Then, to use Apollo-style persisted query IDs, hook up the Persisted Queries Link as described in Apollo's documentation

Finally, update the controller to pass the Apollo-style persisted query ID as the operation ID:

ruby
# app/controllers/graphql_controller.rb
context = {
  # ...
  # Support already-synced Apollo Persisted Queries:
  operation_id: params[:extensions][:operationId]
}

Now, Apollo-style persisted query IDs will be used to fetch operations from the server's OperationStore.

Use with Apollo Android

Apollo Android's generateOperationOutput option builds an OperationOutput.json file which works with the OperationStore. To sync those queries, use the --apollo-android-operation-output option:

sh
graphql-ruby-client sync --apollo-android-operation-output=path/to/OperationOutput.json --url=...

That way, the OperationStore will use the query IDs generated by Apollo Android.

On the server, you'll have to update your controller to receive the client name and the operation ID. For example:

ruby
# app/controllers/graphql_controller.rb
context = { ... }

# Check for an incoming operation ID from Apollo Client:
apollo_android_operation_id = request.headers["X-APOLLO-OPERATION-ID"]
if apollo_android_operation_id.present?
  # Check the incoming request to confirm that
  # it's your first-party client with stored operations
  client_name = # ...
  if client_name.present?
    # If we received an incoming operation ID
    # _and_ identified the client, run a persisted operation.
    context[:operation_id] = "#{client_name}/#{apollo_android_operation_id}"
  end
end

You may also have to update your app to send an identifier, so that the server can determine the "client name" used with the operation store. (Apollo Android sends a query hash, but the operation store expects IDs in the form #{client_name}/#{query_hash}.)

Use with Apollo Persisted Queries

Apollo client has a Persisted Queries Link. You can use that link with GraphQL-Pro's OperationStore. First, create a manifest with generate-persisted-query-manifest, then, pass the path to that file to sync:

sh
$ graphql-ruby-client sync --apollo-persisted-query-manifest=path/to/manifest.json ...

Then, configure Apollo Client to use your persisted query manifest.

Finally, update your controller to receive the operation ID and pass it as context[:operation_id]:

ruby
client_name = "..." # TODO: send the client name as a query param or header
persisted_query_hash = params[:extensions][:persistedQuery][:sha256Hash]
context = {
  # ...
  operation_id: "#{client_name}/#{persisted_query_hash}"
}

The operation_id will also need your client name. Using Apollo Client, you could send this as a custom header or another way that works for your application (eg, session or user agent).

Use with plain JavaScript

OperationStoreClient.getOperationId takes an operation name as input and returns the server-side alias for that operation:

js
var OperationStoreClient = require("./OperationStoreClient")

OperationStoreClient.getOperationId("AppHomeQuery")       // => "my-frontend-app/7a8078c7555e20744cb1ff5a62e44aa92c6e0f02554868a15b8a1cbf2e776b6f"
OperationStoreClient.getOperationId("ProductDetailQuery") // => "my-frontend-app/6726a3b816e99b9971a1d25a1205ca81ecadc6eb1d5dd3a71028c4b01cc254c1"

Post the operationId in your GraphQL requests:

js
// Lookup the operation name:
var operationId = OperationStoreClient.getOperationId(operationName)

// Include it in the params:
$.post("/graphql", {
  operationId: operationId,
  variables: queryVariables,
}, function(response) {
  // ...
})

Authorization

OperationStore uses HMAC-SHA256 to authenticate requests.

Pass the key to graphql-ruby-client sync as --secret to authenticate it:

bash
$ export MY_SECRET_KEY= "abcdefg..."
$ graphql-ruby-client sync ... --secret=$MY_SECRET_KEY
# ...
Authenticating with HMAC
# ...

Guides Javascript Client Urql Subscriptions (`guides/javascript_client/urql_subscriptions.md`)

urql Subscriptions

GraphQL-Ruby currently supports using urql with the ActionCable and Pusher implementation.

Pusher

js
import SubscriptionExchange from "graphql-ruby-client/subscriptions/SubscriptionExchange"
import Pusher from "pusher"
import { Client, defaultExchanges, subscriptionExchange } from 'urql'

const pusherClient = new Pusher("your-app-key", { cluster: "us2" })
const forwardToPusher = SubscriptionExchange.create({ pusher: pusherClient })

const client = new Client({
  url: '/graphql',
  exchanges: [
    ...defaultExchanges,
    subscriptionExchange({
      forwardSubscription: forwardToPusher
    }),
  ],
});

ActionCable

js
import { createConsumer } from "@rails/actioncable";
import SubscriptionExchange from "graphql-ruby-client/subscriptions/SubscriptionExchange"

const actionCable = createConsumer('ws://127.0.0.1:3000/cable');
const forwardToActionCable = SubscriptionExchange.create({ consumer: actionCable })

const client = new Client({
  url: '/graphql',
  exchanges: [
    ...defaultExchanges,
    subscriptionExchange({
      forwardSubscription: forwardToActionCable
    }),
  ],
});

Want to use urql with another subscription backend? Please open an issue.


Guides Language Tools C Parser (`guides/language_tools/c_parser.md`)

C-based Parser

GraphQL-Ruby includes a plain-Ruby parser, but a faster parser is available as a C extension. To use it, add the graphql-c_parser gem to your project, for example:

ruby
bundle add graphql-c_parser

When graphql-c_parser is required by your app, the C-based parser is installed as the default parser (as GraphQL.default_parser). Bundler requires the library automatically, but you can also require it manually:

ruby
require "graphql/c_parser"

This alternative parser is faster and uses less memory.

The library also adds GraphQL.scan_with_c and GraphQL.parse_with_c for calling the C-based parser directly.


Guides Language Tools Visitor (`guides/language_tools/visitor.md`)

AST Visitor

GraphQL code is usually contained in a string, for example:

ruby
query_string = "query { user(id: \"1\") { userName } }"

You can perform programmatic analysis and modifications to GraphQL code using a three-step process:

  • Parse the code into an abstract syntax tree
  • Analyze/Modify the code with a visitor
  • Print the code back to a string

Parse

GraphQL.parse turns a string into a GraphQL document:

ruby
parsed_doc = GraphQL.parse("{ user(id: \"1\") { userName } }")
# => #<GraphQL::Language::Nodes::Document ...>

Also, GraphQL.parse_file parses the contents of the named file and includes a filename in the parsed document.

AST Nodes

The parsed document is a tree of nodes, called an abstract syntax tree (AST). This tree is immutable: once a document has been parsed, those Ruby objects can't be changed. Modifications are performed by copying existing nodes, applying changes to the copy, then making a new tree to hold the copied node. Where possible, unmodified nodes are retained in the new tree (it's persistent).

The copy-and-modify workflow is supported by a few methods on the AST nodes:

  • .merge(new_attrs) returns a copy of the node with new_attrs applied. This new copy can replace the original node.
  • .add_{child}(new_child_attrs) makes a new node with new_child_attrs, adds it to the array specified by {child}, and returns a copy whose {children} array contains the newly created node.

For example, to rename a field and add an argument to it, you could:

ruby
modified_node = field_node
  # Apply a new name
  .merge(name: "newName")
  # Add an argument to this field's arguments
  .add_argument(name: "newArgument", value: "newValue")

Above, field_node is unmodified, but modified_node reflects the new name and new argument.

Analyze/Modify

To inspect or modify a parsed document, extend GraphQL::Language::Visitor and implement its various hooks. It's an implementation of the visitor pattern. In short, each node of the tree will be "visited" by calling a method, and those methods can gather information and perform modifications.

In the visitor, each node class has a hook, for example:

See the GraphQL::Language::Visitor API docs for a full list of methods.

Each method is called with (node, parent), where:

  • node is the AST node currently visited
  • parent is the AST node above this node in the tree

The method has a few options for analyzing or modifying the AST:

Continue/Halt

To continue visiting, the hook should call super. This allows the visit to continue to node's children in the tree, for example:

ruby
def on_field(_node, _parent)
  # Do nothing, this is the default behavior:
  super
end

To halt the visit, a method may skip the call to super. For example, if the visitor encountered an error, it might want to return early instead of continuing to visit.

Modify a Node

Visitor hooks are expected to return the (node, parent) they are called with. If they return a different node, then that node will replace the original node. When you call super(node, parent), the node is returned. So, to modify a node and continue visiting:

  • Make a modified copy of node
  • Pass the modified copy to super(new_node, parent)

For example, to rename an argument:

ruby
def on_argument(node, parent)
  # make a copy of `node` with a new name
  modified_node = node.merge(name: "renamed")
  # continue visiting with the modified node and parent
  super(modified_node, parent)
end

Delete a Node

To delete the currently-visited node, don't pass node to super(...). Instead, pass a magic constant, DELETE_NODE, in place of node.

For example, to delete a directive:

ruby
def on_directive(node, parent)
  # Don't pass `node` to `super`,
  # instead, pass `DELETE_NODE`
  super(DELETE_NODE, parent)
end

Insert a Node

Inserting nodes is similar to modifying nodes. To insert a new child into node, call one of its .add_ helpers. This returns a copied node with a new child added. For example, to add a selection to a field's selection set:

ruby
def on_field(node, parent)
  node_with_selection = node.add_selection(name: "emailAddress")
  super(node_with_selection, parent)
end

This will add emailAddress the fields selection on node.

(These .add_* helpers are wrappers around GraphQL::Language::Nodes::AbstractNode#merge.)

Print

The easiest way to turn an AST back into a string of GraphQL is GraphQL::Language::Nodes::AbstractNode#to_query_string, for example:

ruby
parsed_doc.to_query_string
# => '{ user(id: "1") { userName } }'

You can also create a subclass of GraphQL::Language::Printer to customize how nodes are printed.


Guides Limiters Active Operations (`guides/limiters/active_operations.md`)

Active Operation Limiter

GraphQL::Enterprise::ActiveOperationLimiter prevents clients from running too many GraphQL operations at the same time. It uses Redis to track currently-running operations.

Why?

Some clients may suddently swamp a server with tons of requests, occupying all available Ruby processes and therefore interrupting service for other clients. This limiter aims to prevent that at the GraphQL level by halting queries when a client already has lots of queries running. That way, server processes will remain available for other clients' requests.

Setup

To use this limiter, update the schema configuration and include context[:limiter_key] in your queries.

Schema Setup

To setup the schema, add use GraphQL::Enterprise::ActiveOperationLimiter with a default limit: value:

ruby
class MySchema < GraphQL::Schema
  # ...
  use GraphQL::Enterprise::ActiveOperationLimiter,
    redis: Redis.new(...),
    # Or:
    # connection_pool: ...
    # redis_cluster: ...
    limit: 5
end

limit: false may also be given, which defaults to no limit for this limiter.

It also accepts a stale_request_seconds: option. The limiter uses that value to clean up request data in case of a crash or other unexpected scenario.

Before requests will actually be halted, soft mode must be disabled.

Query Setup

In order to limit clients, the limiter needs a client identifier for each GraphQL operation. By default, it checks context[:limiter_key] to find it:

ruby
context = {
  viewer: current_user,
  # for example:
  limiter_key: logged_in? ? "user:#{current_user.id}" : "anon-ip:#{request.remote_ip}",
  # ...
}

result = MySchema.execute(query_str, context: context)

Operations with the same context[:limiter_key] will rate limited in the same buckets. A limiter key is required; if a query is run without one, the limiter will raise an error.

To provide a client identifier another way, see Customization.

Customization

GraphQL::Enterprise::ActiveOperationLimiter provides several hooks for customizing its behavior. To use these, make a subclass of the limiter and override methods as described:

ruby
# app/graphql/limiters/active_operations.rb
class Limiters::ActiveOperations < GraphQL::Enterprise::ActiveOperationsLimiter
  # override methods here
end

The hooks are:

  • def limiter_key(query) should return a string which identifies the current client for query.
  • def limit_for(key, query) should return an integer or nil. If an integer is returned, that limit is applied for the current query. If nil is returned, no limit is applied to the current query.
  • def soft_limit?(key, query) can be implemented to customize the application of "soft mode". By default, it checks a setting in redis.
  • def handle_redis_error(err) is called when the limit rescues an error from Redis. By default, it's passed to warn and the query is not halted.

Instrumentation

While the limiter is installed, it adds some information to the query context about its operation. It can be accessed at context[:active_operation_limiter]:

ruby
result = MySchema.execute(...)

pp result.context[:active_operation_limiter]
# {:key=>"user:123", :limit=>2, :soft=>false, :limited=>true}

It returns a Hash containing:

  • key: [String], the limiter key used for this query
  • limit: [Integer, nil], the limit applied to this query
  • soft: [Boolean], true if the query was run in "soft mode"
  • limited: [Boolean], true if the query exceeded the rate limit (but if soft: was also true, then the query was not halted)

You could use this to add detailed metrics to your application monitoring system, for example:

ruby
MyMetrics.increment("graphql.active_operation_limiter", tags: result.context[:active_operation_limiter])

Guides Limiters Deployment (`guides/limiters/deployment.md`)

Deploying Rate Limiters

Here are a few options for deploying GraphQL-Enterprise's rate limiters:

  • The Dashboard shows some basic metrics about the limiter.
  • Soft limits start logging over-limit requests to the dashboard but don't actually halt traffic.
  • Subscriptions need extra consideration

Dashboard

Once installed, your GraphQL-Pro dashboard will include a simple metrics view:

To disable dashboard charts, add use(... dashboard_charts: false) to your configuration.

Also, the dashboard includes a link to enable or disable "soft mode":

When "soft mode" is enabled, limited requests are not actually halted (although they are counted). When "soft mode" is disabled, any over-limit requests are halted.

For more detailed metrics, see the "Instrumentation" section of the documentation for each limiter.

Soft Limits

By default, limiters don't actually halt queries; instead, they start out in "soft mode". In this mode:

  • limited/unlimited requests are counted in the Dashboard
  • but, no requests are actually halted

This mode is for assessing the impact of the limiter before it's applied to production traffic. Additionally, if you release the limiter but find that it's affecting production traffic adversely, you can re-enable "soft mode" to stop blocking traffic.

To disable "soft mode" and start limiting, use the Dashboard or re-implement some of the customization methods of the limiter.

You can also disable "soft mode" in Ruby:

ruby
# Turn "soft mode" off for the ActiveOperationLimiter
MySchema.enterprise_active_operation_limiter.set_soft_limit(false)
# or, for RuntimeLimiter
MySchema.enterprise_runtime_limiter.set_soft_limit(false)

Subscriptions

If you're using PusherSubscriptions or AblySubscriptions, then you'll need to accomodate subscriptions that were created before you deployed the rate limiter. Those subscriptions are already stored in Redis and their contexts don't include the required limiter_key: value.

To address this, you can customize the limiter(s) you're using to provide a default value in this case. For example:

ruby
class CustomRuntimeLimiter < GraphQL::Enterprise::RuntimeLimiter
  def limiter_key(query)
    if query.subscription_update? && query.context[:limiter_key].nil?
      # This subscription was created before limiter_key was required,
      # so provide a value for it.
      # If `context` includes enough information to create a
      # "real" limiter key, you could also do that here.
      # In this case, we're providing a default flag:
      "legacy-subscription-update"
    else
      super
    end
  end

  def limit_for(key, query)
    if key == "legacy-subscription-update"
      nil # no limit in this case
    else
      super
    end
  end
end

With methods like that, any subscriptions created before limiter_key: was required will not be subject to rate limits. Adjust those methods as needed for your application. Finally, be sure to attach your custom limiter in your schema, for example:

ruby
# Use a custom subclass of GraphQL::Enterprise::RuntimeLimiter:
use CustomRuntimeLimiter, ...

Guides Limiters Redis (`guides/limiters/redis.md`)

Configuring Redis

Rate limiting requires a persistent Redis instance, just like Sidekiq or the Operation Store. Set maxmemory-policy noeviction in redis.conf to ensure that Redis doesn't silently drop keys when it reaches its memory limit.

Memory Usage

Estimating memory usage depends on the string used to identify clients, since those are used in the Redis keys. Using 100-character client keys, the runtime limiter uses 400 bytes per client (two keys). Memory usage by the active operation limiter depends on the limit because each concurrent operation uses some memory; a higher limit permits more concurrent operations. With 10 active operations and a 100-character client key, the active operation limiter uses 350 bytes per client. Additionally, the limiters use up to 35kb for dashboards (2 limiters, for each one: 2x 60 per-minute keys, 24 hourly keys, and 30 daily keys @ 72 bytes per key).

By those estimates, 1 gigabyte of memory would support both rate limiters for over 1.4 million active clients.

Connection Pool

ActiveOperationsLimiter and RuntimeLimiter support ConnectionPool. To use it, pass connection_pool::

ruby
use GraphQL::Enterprise::RuntimeLimiter, # or ActiveOperationLimiter
  connection_pool: ConnectionPool.new(...) { ... }
  # ...

Redis Cluster

ActiveOperationsLimiter and RuntimeLimiter support redis-cluster. To use it, pass redis_cluster::

ruby
use GraphQL::Enterprise::RuntimeLimiter, # or ActiveOperationLimiter
  redis_cluster: Redis::Cluster.new(...)
  # ...

Guides Limiters Runtime (`guides/limiters/runtime.md`)

Runtime Limiter

GraphQL::Enterprise::RuntimeLimiter applies an upper bound to processing time consumed by a single client. It uses Redis track time with a token bucket algorithm.

Why?

This limiter prevents a single client from consuming too much processing time, regardless of whether it comes a burst of short-lived queries (which the Active Operation Limiter can prevent) or a small number of long-running queries. Unlike request counters or complexity calculations, the runtime limiter pays no attention to the structure of the incoming request. Instead, it simply measures the time spent on the request as a whole and halts queries when a client consumes more than the limit.

Setup

To use this limiter, update the schema configuration and include context[:limiter_key] in your queries.

Schema Setup

To setup the schema, add use GraphQL::Enterprise::RuntimeLimiter with a default limit_ms: value:

ruby
class MySchema < GraphQL::Schema
  # ...
  use GraphQL::Enterprise::RuntimeLimiter,
    redis: Redis.new(...),
    # Or:
    # connection_pool: ...
    # redis_cluster: ...
    limit_ms: 90 * 1000 # 90 seconds per minute
end

limit_ms: false may also be given, which defaults to no limit for this limiter.

It also accepts a window_ms: option, which is the duration over which limit_ms: is added to a client's bucket. It defaults to 60_000 (one minute).

Before requests will actually be halted, soft mode must be disabled.

Query Setup

In order to limit clients, the limiter needs a client identifier for each GraphQL operation. By default, it checks context[:limiter_key] to find it:

ruby
context = {
  viewer: current_user,
  # for example:
  limiter_key: logged_in? ? "user:#{current_user.id}" : "anon-ip:#{request.remote_ip}",
  # ...
}

result = MySchema.execute(query_str, context: context)

Operations with the same context[:limiter_key] will rate limited in the same buckets. A limiter key is required; if a query is run without one, the limiter will raise an error.

To provide a client identifier another way, see Customization.

Customization

GraphQL::Enterprise::RuntimeLimiter provides several hooks for customizing its behavior. To use these, make a subclass of the limiter and override methods as described:

ruby
# app/graphql/limiters/runtime.rb
class Limiters::Runtime < GraphQL::Enterprise::RuntimeLimiter
  # override methods here
end

The hooks are:

  • def limiter_key(query) should return a string which identifies the current client for query.
  • def limit_for(key, query) should return an integer or nil. If an integer is returned, that limit is applied for the current query. If nil is returned, no limit is applied to the current query.
  • def soft_limit?(key, query) can be implemented to customize the application of "soft mode". By default, it checks a setting in redis.
  • def handle_redis_error(err) is called when the limit rescues an error from Redis. By default, it's passed to warn and the query is not halted.

Instrumentation

While the limiter is installed, it adds some information to the query context about its operation. It can be accessed at context[:runtime_limiter]:

ruby
result = MySchema.execute(...)

pp result.context[:runtime_limiter]
# {:key=>"custom-key-9",
#  :limit_ms=>800,
#  :remaining_ms=>0,
#  :soft=>true,
#  :limited=>true,
#  :window_ms=>60_000}

It returns a Hash containing:

  • key: [String], the limiter key used for this query
  • limit_ms: [Integer, nil], the limit applied to this query
  • remaining_ms: [Integer, nil], the amount of time remaining in this client's bucket
  • soft: [Boolean], true if the query was run in "soft mode"
  • limited: [Boolean], true if the query exceeded the rate limit (but if soft: was also true, then the query was not halted)
  • window_ms: [Integer] the configured window_ms: for the limiter

You could use this to add detailed metrics to your application monitoring system, for example:

ruby
MyMetrics.increment("graphql.runtime_limiter", tags: result.context[:runtime_limiter])

Some Caveats

The limiter will not interrupt a long-running field. Instead, it stops executing new fields after a client exceeds its allowed processing time. This is because interrupting arbitrary code may have unintended consequences for I/O operations, see "Timeout: Ruby's most dangerous API".

Also, the limiter only checks remaining time at the start of a query and it only decreases the remaining time at the end of a query. This means that simulaneous queries may consume the remainder at the same time. Use the Active Operation Limiter to limit behavior in this regard. This implementation is basically a trade-off: more granular updates would require more communication with Redis which would add overhead to each request.


Guides Mutations Mutation Authorization (`guides/mutations/mutation_authorization.md`)

Mutation authorization

Before running a mutation, you probably want to do a few things:

  • Make sure the current user has permission to try this mutation
  • Load some objects from the database, using some ID inputs
  • Check if the user has permission to modify those loaded objects

This guide describes how to accomplish that workflow with GraphQL-Ruby.

Checking conditions before instantiating the mutation

ruby
class UpdateUserMutation < BaseMutation
  # ...

  def resolve(update_user_input:, user:)
    # ...
  end

  def self.authorized?(obj, ctx)
    super && ctx[:viewer].present?
  end
end

Checking the user permissions

Before loading any data from the database, you might want to see if the user has a certain permission level. For example, maybe only .admin? users can run Mutation.promoteEmployee.

This check can be implemented using the #ready? method in a mutation:

ruby
class Mutations::PromoteEmployee < Mutations::BaseMutation
  def ready?(**args)
    # Called with mutation args.
    # Use keyword args such as employee_id: or **args to collect them
    if !context[:current_user].admin?
      raise GraphQL::ExecutionError, "Only admins can run this mutation"
    else
      # Return true to continue the mutation:
      true
    end
  end

  # ...
end

Now, when any non-admin user tries to run the mutation, it won't run. Instead, they'll get an error in the response.

Additionally, #ready? may return false, { ... } to return errors as data:

ruby
def ready?
  if !context[:current_user].allowed?
    return false, { errors: ["You don't have permission to do this"]}
  else
    true
  end
end

Loading and authorizing objects

Often, mutations take IDs as input and use them to load records from the database. GraphQL-Ruby can load IDs for you when you provide a loads: option.

In short, here's an example:

ruby
class Mutations::PromoteEmployee < Mutations::BaseMutation
  # `employeeId` is an ID, Types::Employee is an _Object_ type
  argument :employee_id, ID, loads: Types::Employee

  # Behind the scenes, `:employee_id` is used to fetch an object from the database,
  # then the object is authorized with `Employee.authorized?`, then
  # if all is well, the object is injected here:
  def resolve(employee:)
    employee.promote!
  end
end

It works like this: if you pass a loads: option, it will:

  • Automatically remove _id from the name and pass that name for the as: option
  • Add a prepare hook to fetch an object with the given ID (using Schema.object_from_id)
  • Check that the fetched object's type matches the loads: type (using Schema.resolve_type)
  • Run the fetched object through its type's .authorized? hook (see Authorization)
  • Inject it into #resolve using the object-style name (employee:)

In this case, if the argument value is provided by object_from_id doesn't return a value, the mutation will fail with an error.

Alternatively if your ID doesn't specify both class and id, resolvers have a load_#{argument} method that can be overridden.

ruby
argument :employee_id, ID, loads: Types::Employee

def load_employee(id)
  ::Employee.find(id)
end

If you don't want this behavior, don't use it. Instead, create arguments with type ID and use them your own way, for example:

ruby
# No special loading behavior:
argument :employee_id, ID

Can _this user_ perform _this action_?

Sometimes you need to authorize a specific user-object(s)-action combination. For example, .admin? users can't promote all employees! They can only promote employees which they manage.

You can add this check by implementing a #authorized? method, for example:

ruby
def authorized?(employee:)
  super && context[:current_user].manager_of?(employee)
end

When #authorized? returns false (or something falsey), the mutation will be halted. If it returns true (or something truthy), the mutation will continue.

Adding errors

To add errors as data (as described in Mutation errors), return a value along with false, for example:

ruby
def authorized?(employee:)
  super && if context[:current_user].manager_of?(employee)
    true
  else
    return false, { errors: ["Can't promote an employee you don't manage"] }
  end
end

Alternatively, you can add top-level errors by raising GraphQL::ExecutionError, for example:

ruby
def authorized?(employee:)
  super && if context[:current_user].manager_of?(employee)
    true
  else
    raise GraphQL::ExecutionError, "You can only promote your _own_ employees"
  end
end

In either case (returning [false, data] or raising an error), the mutation will be halted.

Finally, doing the work

Now that the user has been authorized in general, data has been loaded, and objects have been validated in particular, you can modify the database using #resolve:

ruby
def resolve(employee:)
  if employee.promote
    {
      employee: employee,
      errors: [],
    }
  else
    # See "Mutation Errors" for more:
    {
      errors: employee.errors.full_messages
    }
  end
end

Guides Mutations Mutation Classes (`guides/mutations/mutation_classes.md`)

Mutation Classes

GraphQL mutations are special fields: instead of reading data or performing calculations, they may modify the application state. For example, mutation fields may:

  • Create, update or destroy records in the database
  • Establish associations between already-existing records in the database
  • Increment counters
  • Create, modify or delete files
  • Clear caches

These actions are called side effects.

Like all GraphQL fields, mutation fields:

  • Accept inputs, called arguments
  • Return values via fields

GraphQL-Ruby includes two classes to help you write mutations:

Besides those, you can also use the plain field API to write mutation fields.

Example mutation class

If you used the install generator, a base mutation class will already have been generated for you. If that's not the case, you should add a base class to your application, for example:

ruby
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  # Add your custom classes if you have them:
  # This is used for generating payload types
  object_class Types::BaseObject
  # This is used for return fields on the mutation's payload
  field_class Types::BaseField
  # This is used for generating the `input: { ... }` object type
  input_object_class Types::BaseInputObject
end

Then extend it for your mutations:

ruby
class Mutations::CreateComment < Mutations::BaseMutation
  null true
  argument :body, String
  argument :post_id, ID

  field :comment, Types::Comment
  field :errors, [String], null: false

  def resolve(body:, post_id:)
    post = Post.find(post_id)
    comment = post.comments.build(body: body, author: context[:current_user])
    if comment.save
      # Successful creation, return the created object with no errors
      {
        comment: comment,
        errors: [],
      }
    else
      # Failed save, return the errors to the client
      {
        comment: nil,
        errors: comment.errors.full_messages
      }
    end
  end
end

The #resolve method should return a hash whose symbols match the field names.

(See Mutation Errors for more information about returning errors.)

Also, you can configure null(false) in your mutation class to make the generated payload class non-null.

Hooking up mutations

Mutations must be attached to the mutation root using the mutation: keyword, for example:

ruby
class Types::Mutation < Types::BaseObject
  field :create_comment, mutation: Mutations::CreateComment
end

Auto-loading arguments

In most cases, a GraphQL mutation will act against a given global relay ID. Loading objects from these global relay IDs can require a lot of boilerplate code in the mutation's resolver.

An alternative approach is to use the loads: argument when defining the argument:

ruby
class Mutations::AddStar < Mutations::BaseMutation
  argument :post_id, ID, loads: Types::Post

  field :post, Types::Post

  def resolve(post:)
    post.star

    {
      post: post,
    }
  end
end

By specifying that the post_id argument loads a Types::Post object type, a Post object will be loaded via Schema.object_from_id with the provided post_id.

All arguments that end in _id and use the loads: method will have their _id suffix removed. For example, the mutation resolver above receives a post argument which contains the loaded object, instead of a post_id argument.

The loads: option also works with list of IDs, for example:

ruby
class Mutations::AddStars < Mutations::BaseMutation
  argument :post_ids, [ID], loads: Types::Post

  field :posts, [Types::Post]

  def resolve(posts:)
    posts.map(&:star)

    {
      posts: posts,
    }
  end
end

All arguments that end in _ids and use the loads: method will have their _ids suffix removed and an s appended to their name. For example, the mutation resolver above receives a posts argument which contains all the loaded objects, instead of a post_ids argument.

In some cases, you may want to control the resulting argument name. This can be done using the as: argument, for example:

ruby
class Mutations::AddStar < Mutations::BaseMutation
  argument :post_id, ID, loads: Types::Post, as: :something

  field :post, Types::Post

  def resolve(something:)
    something.star

    {
      post: something
    }
  end
end

In the above examples, loads: is provided a concrete type, but it also supports abstract types (i.e. interfaces and unions).

Resolving the type of loaded objects

When loads: gets an object from Schema.object_from_id, it passes that object to Schema.resolve_type to confirm that it resolves to the same type originally configured with loads:.

Handling failed loads

If loads: fails to find an object or if the loaded object isn't resolved to the specified loads: type (using Schema.resolve_type), a GraphQL::LoadApplicationObjectFailedError is raised and returned to the client.

You can customize this behavior by implementing def load_application_object_failed in your mutation class, for example:

ruby
def load_application_object_failed(error)
  raise GraphQL::ExecutionError, "Couldn't find an object for ID: `#{error.id}`"
end

Or, if load_application_object_failed returns a new object, that object will be used as the loads: result.

Handling unauthorized loaded objects

When an object is loaded but fails its .authorized? check, a GraphQL::UnauthorizedError is raised. By default, it's passed to Schema.unauthorized_object (see Handling Unauthorized Objects). You can customize this behavior by implementing def unauthorized_object(err) in your mutation, for example:

ruby
def unauthorized_object(error)
  # Raise a nice user-facing error instead
  raise GraphQL::ExecutionError, "You don't have permission to modify the loaded #{error.type.graphql_name}."
end

Guides Mutations Mutation Errors (`guides/mutations/mutation_errors.md`)

Mutation errors

How can you handle errors inside mutations? Let's explore a couple of options.

Raising Errors

One way to handle an error is by raising, for example:

ruby
def resolve(id:, attributes:)
  # Will crash the query if the data is invalid:
  Post.find(id).update!(attributes.to_h)
  # ...
end

Or:

ruby
def resolve(id:, attributes:)
  if post.update(attributes)
    { post: post }
  else
    raise GraphQL::ExecutionError, post.errors.full_messages.join(", ")
  end
end

This kind of error handling does express error state (either via HTTP 500 or by the top-level "errors" key), but it doesn't take advantage of GraphQL's type system and can only express one error at a time. It works, but a stronger solution is to treat errors as data.

Errors as Data

Another way to handle rich error information is to add error types to your schema, for example:

ruby
class Types::UserError < Types::BaseObject
  description "A user-readable error"

  field :message, String, null: false,
    description: "A description of the error"
  field :path, [String],
    description: "Which input value this error came from"
end

Then, add a field to your mutation which uses this error type:

ruby
class Mutations::UpdatePost < Mutations::BaseMutation
  # ...
  field :errors, [Types::UserError], null: false
end

And in the mutation's resolve method, be sure to return errors: in the hash:

ruby
def resolve(id:, attributes:)
  post = Post.find(id)
  if post.update(attributes)
    {
      post: post,
      errors: [],
    }
  else
    # Convert Rails model errors into GraphQL-ready error hashes
    user_errors = post.errors.map do |error|
      # This is the GraphQL argument which corresponds to the validation error:
      path = ["attributes", error.attribute.to_s.camelize(:lower)]
      {
        path: path,
        message: error.message,
      }
    end
    {
      post: post,
      errors: user_errors,
    }
  end
end

Now that the field returns errors in its payload, it supports errors as part of the incoming mutations, for example:

graphql
mutation($postId: ID!, $postAttributes: PostAttributes!) {
  updatePost(id: $postId, attributes: $postAttributes) {
    # This will be present in case of success or failure:
    post {
      title
      comments {
        body
      }
    }
    # In case of failure, there will be errors in this list:
    errors {
      path
      message
    }
  }
}

In case of a failure, you might get a response like:

ruby
{
  "data" => {
    "createPost" => {
      "post" => nil,
      "errors" => [
        { "message" => "Title can't be blank", "path" => ["attributes", "title"] },
        { "message" => "Body can't be blank", "path" => ["attributes", "body"] }
      ]
    }
  }
}

Then, client apps can show the error messages to end users, so they might correct the right fields in a form, for example.

Nullable Mutation Payload Fields

To benefit from "Errors as Data" described above, mutation fields must not have null: false. Why?

Well, for non-null fields (which have null: false), if they return nil, then GraphQL aborts the query and removes those fields from the response altogether.

In mutations, when errors happen, the other fields may return nil. So, if those other fields have null: false, but they return nil, the GraphQL will panic and remove the whole mutation from the response, including the errors!

In order to have the rich error data, even when other fields are nil, those fields must have null: true (which is the default) so that the type system can be obeyed when errors happen.

Here's an example of a nullable field (good!):

ruby
class Mutations::UpdatePost < Mutations::BaseMutation
  # Use the default `null: true` to support rich errors:
  field :post, Types::Post
  # ...
end

Guides Mutations Mutation Root (`guides/mutations/mutation_root.md`)

Mutation Root

GraphQL mutations all begin with the mutation keyword:

graphql
mutation($accountNumber: ID!, $newBalance: Int!) {
# ^^^^ here
  setAccountBalance(accountNumber: $accountNumber, newBalance: $newBalance) {
    # ...
  }
}

Operations that begin with mutation get special treatment by the GraphQL runtime: root fields are guaranteed
to be executed sequentially. This way, the effect of a series of mutations is predictable.

Mutations are executed by a specific GraphQL object, Mutation. This object is defined like any other GraphQL object:

ruby
class Types::Mutation < Types::BaseObject
  # ...
end

Then, it must be attached to your schema with the mutation(...) configuration:

ruby
class Schema < GraphQL::Schema
  # ...
  mutation(Types::Mutation)
end

Now, whenever an incoming request uses the mutation keyword, it will go to Mutation.

See Mutation Classes for some helpers to define mutation fields.


Guides Object Cache Caching (`guides/object_cache/caching.md`)

Caching Results

GraphQL::Enterprise::ObjectCache supports several different caching configurations for objects and fields. To get started, include the extension in your base object class and base field class and use cacheable(...) to set up the default cache behavior:

ruby
# app/graphql/types/base_object.rb
class Types::BaseObject < GraphQL::Schema::Object
  include GraphQL::Enterprise::ObjectCache::ObjectIntegration
  field_class Types::BaseField
  cacheable(...) # see below
  # ...
end
ruby
# app/graphql/types/base_field.rb
class Types::BaseField < GraphQL::Schema::Field
  include GraphQL::Enterprise::ObjectCache::FieldIntegration
  cacheable(...) # see below
  # ...
end

Also, make sure your base interface module is using your field class:

ruby
# app/graphql/types/base_interface.md
module Types::BaseInterface
 field_class Types::BaseField
end

Field caching can be configured per-field, too, for example:

ruby
field :latest_update, Types::Update, null: false, cacheable: { ttl: 60 }

field :random_number, Int, null: false, cacheable: false

Only queries are cached. ObjectCache skips mutations and subscriptions altogether.

`cacheable(true|false)`

cacheable(true) means that the configured type or field may be stored in the cache until its cache fingerprint changes. It also defaults to public: false, meaning that clients will not share cached responses. See public: below for more about this option.

cacheable(false) disables caching for the configured type or field. Any query that includes this type or field will neither check for an already-cached value nor update the cache with its result.

`public:`

cacheable(public: false) means that a type or field may be cached, but Schema.private_context_fingerprint_for(ctx) should be included in its cache key. In practice, this means that each client can have its own cached responses. Any query that contains a cacheable(public: false) type or field will use a private cache key.

cacheable(public: true) means that cached values from this type or field may be shared by all clients. Use this for public-facing data which is the same for all viewers. Queries that include only public: true types and fields will not include Schema.private_context_fingerprint_for(ctx) in their cache keys. That way their responses will be shared by all clients who request them.

`ttl:`

cacheable(ttl: seconds) expires any cached value after the given number of seconds, regardless of cache fingerprint. ttl: shines in a few cases:

  • Objects that can't reliably generate a fingerprint value (for example, they have no .updated_at timestamp). In this case, a conservative ttl may be the only option for cache expiration.
  • Or, root-level fields that should be expired after a certain amount of time. The root-level Query often has no backing object, so it won't have a cache fingerprint, either. Adding cacheable: { ttl: ... } to root level fields will provide some caching along with a guarantee about when they'll be expired.
  • Or, list responses that may be difficult to invalidate properly (see below).

Under the hood, ttl: is implemented with Redis's EXPIRE.

Caching lists and connections

Lists and connections require a little extra consideration. By default, each item in a list is registered with the cache, but when new items are created, they are unknown to the cache and therefore don't invalidate the cached result. There are two main approaches to address this.

`has_many` lists

In order to effectively bust the cache, items that belong to the list of "parent" object should update the parent (eg, Rails .touch) whenever they're created, destroyed, or updated. For example, if there's a list of players on a team:

graphql
{
  team { players { totalCount } }
}

None of the specific Players will be part of the cached response, but the Team will be. To properly invalidate the cache, the Team's updated_at (or other cache key) should be updated whenever a Player is added or removed from the Team.

If a list may be sorted, then updates to Players should also update the Team so that any sorted results in the cache are invalidated, too. Alternatively (or additionally), you could use a ttl: to expire cached results after a certain duration, just to be sure that results are eventually expired.

With Rails, you can accomplish this with:

ruby
# update the team whenever a player is saved or destroyed:
  belongs_to :team, touch: true

Top-level lists

For ActiveRecord::Relations without a "parent" object, you can use GraphQL::Enterprise::ObjectCache::CacheableRelation to make a synthetic cache entry for the whole relation. To use this class, make a subclass and implement def items, for example:

ruby
class AllTeams < GraphQL::Enterprise::ObjectCache::CacheableRelation
  def items(division: nil)
    teams = Team.all
    if division
      teams = teams.where(division: division)
    end
    teams
  end
end

Then, in your resolver, use your new class to retrieve the items:

ruby
class Query < GraphQL::Schema::Object
  field :teams, Team.connection_type do
    argument :division, Division, required: false
  end

  def teams(division: nil)
    AllTeams.items_for(self, division: division)
  end
end

If you're using GraphQL::Schema::Resolver, you'd call .items_for slightly differently:

ruby
def resolve(division: nil)
  # use `context[:current_object]` to get the GraphQL::Schema::Object instance whose field is being resolved
  AllTeams.items_for(context[:current_object], division: division)
end

Finally, you'll need to handle CacheableRelations in your object identification methods, for example:

ruby
class MySchema < GraphQL::Schema
  # ...
  def self.id_from_object(object, type, ctx)
    if object.is_a?(GraphQL::Enterprise::ObjectCache::CacheableRelation)
      object.id
    else
      # The rest of your id_from_object logic here...
    end
  end

  def self.object_from_id(id, ctx)
    if (cacheable_rel = GraphQL::Enterprise::ObjectCache::CacheableRelation.find?(id))
      cacheable_rel
    else
      # The rest of your object_from_id logic here...
    end
  end
end

In this example, AllTeams implements several methods to support caching:

  • #id creates a cache-friendly, stable global ID
  • #to_param creates a cache fingerprint (using Rails's #cache_key under the hood)
  • .find? retrieves the list based on its ID

This way, if a Team is created, the cached result will be invalidated and a fresh result will be created.

Alternatively (or additionally), you could use a ttl: to expire cached results after a certain duration, just to be sure that results are eventually expired.

Connections

By default, connection-related objects (like *Connection and *Edge types) "inherit" cacheability from their node types. You can override this in your base classes as long as GraphQL::Enterprise::ObjectCache::ObjectIntegration is included in the inheritance chain somewhere.

Caching Introspection

By default, introspection fields are considered public for all queries. This means that they are considered cacheable and their results will be reused for any clients who request them. When adding the ObjectCache to your schema, you can provide some options to customize this behavior:

  • cache_introspection: { public: false, ... } to use public: false for all introspection fields. Use this if you hide schema members for some clients.
  • cache_introspection: false to completely disable caching on introspection fields.
  • cache_introspection: { ttl: ..., ... } to set a ttl (in seconds) for introspection fields.

Object Dependencies

By default, the object of a GraphQL Object type is used for caching the fields selected on that object. But, you can specify what object (or objects) should be used to check the cache by implementing def self.cache_dependencies_for(object, context) in your type definition. For example:

ruby
class Types::Player
  def self.cache_dependencies_for(player, context)
    # we update the team's timestamp whenever player details change,
    # so ignore the `player` for caching purposes
    player.team
  end
end

Use this to:

  • improve performance when caching lists of children that belong to a parent object
  • register other objects with the ObjectCache when running a query. (cacheable_object(obj) or def self.object_fingerprint_for can also be used in this case.)

If this method returns an Array, each object in the array will be registered with the cache.


Guides Object Cache Memcached (`guides/object_cache/memcached.md`)

Dalli Configuration

GraphQL::Enterprise::ObjectCache can also run with a Memcached backend via the Dalli client gem.

Set it up by passing a Dalli::Client instance as dalli: ..., for example:

ruby
use GraphQL::Enterprise::OperationStore, dalli: Dalli::Client.new(...)

Guides Object Cache Redis (`guides/object_cache/redis.md`)

Redis Configuration

GraphQL::Enterprise::ObjectCache requires a Redis connection to store cached responses. Unlike OperationStore or rate limiters, this Redis instance should be configured to evict keys as needed.

Memory Management

Memory consumption is hard to estimate since it depends on how many queries the cache receives, how many objects those queries reference, how big the response is for those queries, and how long the fingerprints are for each object and query. To manage memory, configure the Redis instance with a maxmemory and maxmemory-policy directive, for example:

text
maxmemory 1gb
maxmemory-policy allkeys-lfu

Additionally, consider conditionally skipping the cache to prioritize your most critical GraphQL traffic.

Redis Cluster

ObjectCache also supports Redis Cluster. To use, pass redis_cluster::

ruby
use GraphQL::Enterprise::ObjectCache, redis_cluster: Redis::Cluster.new(...)

Under the hood, it uses query fingerprints as hash tags and each cached result has its own set of object metadata.

Connection Pool

ObjectCache also supports ConnectionPool. To use it, pass connection_pool::

ruby
use GraphQL::Enterprise::ObjectCache, connection_pool: ConnectionPool.new(...) { ... }

Data Structure

Under the hood, ObjectCache stores a mapping of queries and objects. Additionally, there are back-references from objects to queries that reference them. In general, like this:

text
"query1:result" => '{"data":{...}}'
"query1:objects" => ["obj1:v1", "obj2:v2"]

"query2:result" => '{"data":{...}}'
"query2:objects" => ["obj2:v2", "obj3:v1"]

"obj1:v1" => { "fingerprint" => "...", "id" => "...", "type_name" => "..." }
"obj2:v2" => { "fingerprint" => "...", "id" => "...", "type_name" => "..." }
"obj3:v1" => { "fingerprint" => "...", "id" => "...", "type_name" => "..." }

"obj1:v1:queries" => ["query1"]
"obj2:v2:queries" => ["query1", "query2"]
"obj3:v1:queries" => ["query2"]

These mappings enable proper clean-up when queries or objects are expired from the cache. Additionally, whenever ObjectCache finds incomplete data in storage (for example, a necessary key was evicted), then it invalidates the whole query and re-runs it.


Guides Object Cache Runtime Considerations (`guides/object_cache/runtime_considerations.md`)

Runtime Considerations

With caching configured, here are a few more things to keep in mind while queries are running.

Skipping the cache

You can set skip_object_cache: true in your query context: { ... } to disable ObjectCache for a given query.

Manually adding an object to caching

By default, ObjectCache gathers the objects "behind" each GraphQL object in the result, then uses their fingerprints as cache keys. To manually register another object with the cache while a query is running, call Schema::Object.cacheable_object(...), passing the object and context. For example:

ruby
field :team_member_count, Integer, null: true do
  argument :name, String, required: true
end

def team_member_count(name:)
  team = Team.find_by(name: name)
  if team
    # Register this object so that the cached result
    # will be invalidated when the team is updated:
    Types::Team.cacheable_object(team, context)
    team.members.count
  else
    nil
  end
end

(When the cache is disabled, cacheable_object(...) is a no-op.)

Measuring the cache

While the cache is running, it logs some data in a Hash as context[:object_cache]. For example:

ruby
result = MySchema.execute(...)
pp result.context[:object_cache]
{
  key: "...",                 # the cache key used for this query
  write: true,                # if this query caused an update to the cache
  ttl: 15,                    # the smallest `ttl:` value encountered in this query (used for this query's result)
  hit: true,                  # if this query returned a cached result
  public: false,              # true or false, whether this query used a public cache key or a private one
  messages: ["...", "..."],   # status messages about the cache's behavior
  objects: Set(...),          # application objects encountered during the query
  uncacheable: true,          # if ObjectCache found a reason that this query couldn't be cached (see `messages: ...` for reason)
  reauthorized_cached_objects: true,
                              # if `.authorized?` was checked for cached objects, see "Disabling Reauthorization"
}

Manually refreshing the cache

If you need to manually clear the cache for a query, pass context: { refresh_object_cache: true, ... }. This will cause the ObjectCache to remove the already-cached result (if there was one), reassess the query for cache validity, and return a freshly-executed result.

Usually, this shouldn't be necessary; making sure objects update their cache fingerprints will cause entries to expire when they should be re-executed. See also Schema fingerprint for expiring all results in the cache.


Guides Object Cache Schema Setup (`guides/object_cache/schema_setup.md`)

Schema Setup

To prepare the schema to serve cached responses, you have to add GraphQL::Enterprise::ObjectCache and implement a few hooks.

Add the Cache

In your schema, add use GraphQL::Enterprise::ObjectCache, redis: ...:

ruby
class MySchema < GraphQL::Schema
  use GraphQL::Enterprise::ObjectCache, redis: CACHE_REDIS
end

See the Redis guide or Memcached guide for details about configuring cache storage.

Additionally, it accepts some options for customizing how introspection is cached, see Caching Introspection

Context Fingerprint

Additionally, you should implement def self.private_context_fingerprint_for(context) to return a string identifying the private scope of the given context. This method will be called whenever a query includes a public: false type or field. For example:

ruby
class MySchema < GraphQL::Schema
  # ...
  def self.private_context_fingerprint_for(context)
    viewer = context[:viewer]
    if viewer.nil?
      # This should never happen, but just in case:
      raise("Invariant: No viewer in context! Can't create a private context fingerprint" )
    end

    # include permissions in the fingerprint so that if the viewer's permissions change, the cache will be invalidated
    permission_fingerprint = viewer.team_memberships.map { |tm| "#{tm.team_id}/#{tm.permission}" }.join(":")

    "user:#{viewer.id}:#{permission_fingerprint}"
  end
end

Whenever queries including public: false are cached, the private context fingerprint will be part of the cache key, preventing responses from being shared between different viewers.

The returned String should reflect any aspects of context that, if changed, should invalidate the cache. For example, if a user's permission level or team memberships change, then any previously-cached responses should be ignored.

Object Fingerprint

In order to determine whether cached results should be returned or invalidated, GraphQL needs a way to determine the "version" of each object in the query. It uses Schema.object_fingerprint_for(object) to do this. By default, it checks .cache_key_with_version (implemented by Rails), then .to_param, then it returns nil. Returning nil tells the cache not to use the cache at all. To customize this behavior, you can implement def self.object_fingerprint_for(object) in your schema:

ruby
class MySchema < GraphQL::Schema
  # ...

  # For example, if you defined `.custom_cache_key` and `.uncacheable?`
  # on objects in your application:
  def self.object_fingerprint_for(object)
    if object.respond_to?(:custom_cache_key)
      object.custom_cache_key
    elsif object.respond_to?(:uncacheable?) && object.uncacheable?
      nil # don't cache queries containing this object
    else
      super
    end
  end
end

The returned strings are used as cache keys in the database -- whenever they change, stale data is left to be cleaned up by Redis.

Object Identification

ObjectCache depends on object identification hooks used elsewhere in GraphQL-Ruby:

  • def self.id_from_object(object, type, context) which returns a globally-unique String id for object
  • def self.object_from_id(id, context) which returns the application object for the given globally-unique id
  • def self.resolve_type(abstract_type, object, context) which returns a GraphQL object type definition to use for object

After your schema is setup, you can configure caching on your types and fields.

Schema Fingerprint

ObjectCache will also call .fingerprint on your Schema class. You can implement this method to return a new string if you make breaking changes to your schema, for example:

ruby
class MySchema < GraphQL::Schema
 def self.fingerprint
   "v2" # increment this if there are breaking changes to the schema
 end
end

By returning a new MySchema.fingerprint, all previously-cached results will be expired.

Disabling Reauthorization

By default, ObjectCache checks .authorized? on each object before returning a cached result. However, if all authorization-related considerations are present in the object's cache fingerprint, then you can disable this check in two ways:

  • per-query, by passing context: { reauthorize_cached_objects: false }
  • globally, by configuring use GraphQL::Enterprise::ObjectCache, ... reauthorize_cached_objects: false

Guides Operation Store Access Control (`guides/operation_store/access_control.md`)

Access Control

The OperationStore has a built-in mechanism for authenticating incoming sync requests. This way, you can be sure that all registered queries came from legitimate sources.

Authentication

When you add a client, you also associate a secret with that client. You can use the default or provide your own and you can update a client secret at any time. By updating a secret, old secrets become invalid.

This secret is used to add an authorization header, generated with HMAC-SHA256. With this header, the server can assert:

  • The request came from an authorized client
  • The request was not corrupted in transit

For more info about HMAC, see Wikipedia or Ruby's OpenSSL::HMAC support.

The Authorization header takes the form:

ruby
"GraphQL::Pro #{client_name} #{hmac}"

graphql-ruby-client adds this header to outgoing requests by using the provided --client and --secret values.


Guides Operation Store Active Record Backend (`guides/operation_store/active_record_backend.md`)

ActiveRecord Backend

GraphQL-Pro's OperationStore can use ActiveRecord to store persisted queries. After setting up the database, it will read and write using those tables as needed.

Database Setup

To use ActiveRecord, GraphQL::Pro::OperationStore requires some database tables.

Rails Generator

With Rails, you can generate the required migration then run it:

bash
$ rails generate graphql:operation_store:create
$ rails db:migrate

(You'll have to run that migration on any staging or production servers, too.)

Now, OperationStore has what it needs to save queries using ActiveRecord!

Manual Setup

You can also create the required migration by manually by generating an empty migration:

bash
$ rails generate migration SetupOperationStore

Then open the migration file and add:

text
/* Detailed source-code truncated for AI context efficiency. */

Then run the migration:

text
$ bundle exec rake db:migrate

(You'll have to run that migration on any staging or production servers, too.)

Now, OperationStore has what it needs to save queries using ActiveRecord!

Database Update

GraphQL-Pro 1.15.0 introduced new features for the OperationStore. To enable them, add some columns to your database:

ruby
add_column :graphql_client_operations, :is_archived, :boolean, default: false
add_column :graphql_client_operations, :last_used_at, :datetime

Updating `last_used_at`

By default, GraphQL-Pro updates last_used_at values in a background thread every 5 seconds. You can customize this by passing a number of seconds to update_last_used_at_every: when installing the OperationStore:

ruby
use GraphQL::Pro::OperationStore, update_last_used_at_every: 1 # seconds

To update that column inline each time an operation is accessed, pass 0.

Note: It is recommended to set this to 0 in test environments, to avoid delayed updates in another thread that can cause intermittent test hangs and failures. For example:

ruby
# Update immediately in Test, wait 5 seconds in other environments:
use GraphQL::Pro::OperationStore, update_last_used_at_every: Rails.env.test? ? 0 : 5

Guides Operation Store Client Workflow (`guides/operation_store/client_workflow.md`)

Client Workflow

To use persisted queries with your client application, you must:

This documentation also touches on graphql-ruby-client sync, a JavaScript client library for using OperationStore.

Add a Client

Clients are registered via the dashboard:

A default secret is provided for you, but you can also enter your own. The secret is used for HMAC authentication.

(Are you interested in a Ruby API for this? Please open an issue or email [email protected].)

Syncing

Once a client is registered, it can push queries to the server via the Sync API.

The easiest way to sync is with graphql-ruby-client sync, a command-line tool written in JavaScript (Sync Guide)

In short, it:

  • Finds GraphQL queries from .graphql files or relay-compiler output in the provided --path
  • Adds an Authentication header based on the provided --client and --secret
  • Sends the operations to the provided --url
  • Generates a JavaScript module into the provided --outfile

For example:

For help syncing in another language, you can take inspiration from the JavaScript implementation, open an issue, or email [email protected].

Client Usage

See the Sync Guide for using OperationStore with Relay Modern, Apollo 1.x, Apollo Link, or plain JavaScript.

To run stored operations from another client, send a param called operationId which is composed of:

ruby
{
   # ...
   operationId: "my-relay-app/ce79aa2784fc..."
   #            ^ client id  / ^ operation id
 }

The server will use those values to fetch an operation from the database.

Next Steps

Learn more about OperationStore's authentication or read some tips for server management.


Guides Operation Store Getting Started (`guides/operation_store/getting_started.md`)

Getting Started

To use GraphQL::Pro::OperationStore with your app, follow these steps:

Dependencies

OperationStore requires two gems in your application environment:

  • ActiveRecord or Redis for persistence. (Using another ORM or backend? Please open an issue to request support!)
  • Rack: to serve the Dashboard and Sync API. (In Rails, this is provided by config/routes.rb.)

These are bundled with Rails by default.

Prepare the Database

If you're going to store data with ActiveRecord, migrate the database to prepare tables for it.

Add `OperationStore`

To hook up the storage to your schema, add the plugin:

ruby
class MySchema < GraphQL::Schema
  # Add it _after_ other tracing-related features, for example:
  # use GraphQL::Tracing::DataDogTracing
  # ...
  use GraphQL::Pro::OperationStore
end

Make sure to add this feature after other Tracing-based features so that those other features will have access to the loaded query string. Otherwise, you may get "No query string was present" errors.

By default, it uses ActiveRecord. It also accepts:

  • redis:, for using a Redis backend; OR
  • backend_class:, for implementing custom persistence.

Also, you can disable updates to "last used at" with default_touch_last_used_at: false. (This can also be configured per-query with context[:operation_store_touch_last_used_at] = true|false.)

Add Routes

To use OperationStore, add two routes to your app:

ruby
# config/routes.rb

# Include GraphQL::Pro's routing extensions:
using GraphQL::Pro::Routes

Rails.application.routes.draw do
  # ...
  # Add the Dashboard
  # TODO: authorize, see the dashboard guide
  mount MySchema.dashboard, at: "/graphql/dashboard"
  # Add the Sync API (authorization built-in)
  mount MySchema.operation_store_sync, at: "/graphql/sync"
end

MySchema.operation_store_sync receives pushes from clients. See Client Workflow for more info on how this endpoint is used.

MySchema.dashboard includes a web view to the OperationStore, visible at /graphql/dashboard. See the Dashboard guide for more details, including authorization.

operation_store_sync and dashboard are both Rack apps, so you can mount them in Rails, Sinatra, or any other Rack app.

Alternatively, you can configure the routes to load your schema lazily, during the first request:

ruby
# Provide the fully-qualified class name of your schema:
lazy_routes = GraphQL::Pro::Routes::Lazy.new("MySchema")
mount lazy_routes.dashboard, at: "/graphql/dashboard"
mount lazy_routes.operation_store_sync, at: "/graphql/sync"

With Visibility Profiles

You can apply a visibility profile to incoming operations by passing the profile name to operation_store_sync, for example:

ruby
mount MySchema.operation_store_sync(visibility_profile: :public_api), at: "/graphql/sync"
# or:
mount lazy_routes.operation_store_sync(visibility_profile: :public_api), at: "/graphql/sync"

This will apply that profile to all newly-synced operations. (It doesn't affect operations that were already synced.)

Update the Controller

Add operation_id: to your GraphQL context:

ruby
# app/controllers/graphql_controller.rb
context = {
  # Relay / Apollo 1.x:
  operation_id: params[:operationId]
  # Or, Apollo Link:
  # operation_id: params[:extensions][:operationId]
}

MySchema.execute(
  # ...
  context: context,
)

OperationStore will use operation_id to fetch the operation from the database.

See Server Management for details about rejecting GraphQL from params[:query].

Next Steps

Sync your operations with the Client Workflow.


Guides Operation Store Redis Backend (`guides/operation_store/redis_backend.md`)

Redis Backend

OperationStore can use Redis to store persisted queries. Pass a redis: option when adding the plugin:

ruby
class MySchema < GraphQL::Schema
  use GraphQL::Pro::OperationStore, redis: Redis.new
end

(You can initialize Redis with any options you need.)

Note: Be sure that this Redis instance is configured as a persistent database, not as a cache. You don't want it to throw away old keys!


Guides Operation Store Server Management (`guides/operation_store/server_management.md`)

Server Management

After getting started, here some things to keep in mind.

Rejecting Arbitrary Queries

With persisted queries, you can stop accepting arbitrary GraphQL input. This way, malicious users can't run large or inappropriate queries on your server.

In short, you can ignore arbitrary GraphQL by skipping the first argument of MySchema.execute:

ruby
# app/controllers/graphql.rb

# Don't pass a query string; ignore `params[:query]`
MySchema.execute(
  context: context,
  variables: params[:variables],
  operation_name: params[:operationName],
)

However, take these points into consideration:

  • Are any previous clients using arbitrary GraphQL? (For example, old versions of native apps or old web pages may still be sending GraphQL.)
  • Should some users still be allowed to send custom strings? (For example, do staff members use GraphiQL to develop new features or debug issues?)

If those apply to you, you can apply some logic to query_string:

ruby
# Allow arbitrary GraphQL:
# - from staff users
# - in development
query_string = if current_user.staff? || Rails.env.development?
  params[:query]
else
  nil
end

MySchema.execute(
  query_string, # maybe nil, that's OK.
  context: context,
  variables: params[:variables],
  operation_name: params[:operationName],
)

Archiving and Deleting Data

Clients can only add to the database, but as an administrator, you can also archive or delete entries from the database. (Make sure you authorize access to the Dashboard.) This is a dangerous operation: by archiving or deleting something, any clients who depend on that data will crash.

Some reasons to archive or delete from the database are:

  • Data was pushed in error; the data is not used
  • The queries are invalid or unsafe; it's better to remove them than to keep them

If this is true, you can use "Archive" or "Delete" buttons to remove things from production.

When an operation is archived, it's no longer available to clients, but it's still in the database. It can be unarchived later, so this is lower-risk than full deletion.

Integration with Your Application

It's on the road map to add a Ruby API to OperationStore so that you can integrate it with your application. For example, you might:

  • Create clients that correspond to users in your system
  • Show client secrets via the Dashboard so that users can save them
  • Render your own administration dashboards with OperationStore data

If this interests you, please open an issue or email [email protected].


Guides Pagination Connection Concepts (`guides/pagination/connection_concepts.md`)

Connection Concepts

Connections are a pagination solution which started with Relay JS, but now it's used for almost any GraphQL API.

Connections are composed of a few kinds of objects:

  • Connection types are generics that expose pagination-related metadata and access to the items
  • Edge types are also generics. They represent the relationship between the parent and child (eg, a PostEdge represents the link from Blog to Post).
  • nodes are actual list items. In a PostsConnection, each node is a Post.

Connections have some advantages over offset-based pagination:

  • First-class support for relationship metadata
  • Cursor implementations can support efficient, stable pagination

Connections, Edges and Nodes

Connection pagination has three core objects: connections, edges, and nodes.

Nodes

Nodes are items in a list. A node is usually an object in your schema. For example, a node in a posts connection is a Post:

ruby
{
  posts(first: 5) {
    edges {
      node {
        # This is a `Post` object:
        title
        body
        publishedAt
      }
    }
  }
}

Connections

Connections are objects that represent a one-to-many relation. They contain metadata about the list of items and access to the items.

Connections are often generated from object types. Their list items, called nodes, are members of that object type. Connections can also be generated from union types and interface types.

Connection metadata

Connections can tell you about the list in general. For example, if you add a total count field, they can tell you the count:

ruby
{
  posts {
    # This is a PostsConnection
    totalCount
  }
}

Connection items

The list items in a connection are called nodes. They can generally be accessed two ways:

  • via edges: posts { edges { node { ... } } }
  • via nodes: posts { nodes { ... } }

The differences is that edges { node { ... } } has more room for relationship metadata. For example, when listing the members of a team, you might include when someone joined the team as edge metadata:

ruby
team {
  members(first: 10) {
    edges {
      # when did this person join the team?
      joinedAt
      # information about the person:
      node {
        name
      }
    }
  }
}

Alternatively, nodes provides easier access to the items, but can't expose relationship metadata:

ruby
team {
  members(first: 10) {
    nodes {
      # shows the team members' names
      name
    }
  }
}

There's no way to show joinedAt above without using edges { ... }.

Edges

Edges are like join tables in that they can expose relationship metadata between a parent object and its children.

For example, let's say that someone may be the member of several teams. You would make a join table in the database (eg, team_memberships) which connect people to each of the teams they're on. This join table could also include information about how that person is related to the team: when they joined, what role they have, etc.

Edges can reveal this information about the relationship, for example:

ruby
team {
  # this is the team name
  name

  members(first: 10) {
    edges {
      # this is a team membership
      joinedAt
      role

      node {
        # this is the person on the team
        name
      }
    }
  }
}

So, edges really help when the relationship between two objects has special data associated with it. If you use a join table, that's a clue that you might use a custom edge to model the relationship.


Guides Pagination Cursors (`guides/pagination/cursors.md`)

Cursors

Connections use cursors to advance through paginated lists. A cursor is an opaque string that indicates a specific point in this.

Here, opaque means that the string has no meaning except its value. cursors shouldn't be decoded, reverse-engineered, or generated ad-hoc. The only guarantee of a cursor is that, after you retrieve one, you can use it to request subsequent or preceding items in the list.

Although cursors can be tricky, they were chosen for Relay-style connections because they can be implemented in stable and high-performing ways.

Customizing Cursors

By default, cursors are encoded in base64 to make them opaque to a human client. You can specify a custom encoder with Schema.cursor_encoder. The value should be an object which responds to .encode(plain_text, nonce:) and .decode(encoded_text, nonce: false).

For example, to use URL-safe base-64 encoding:

ruby
module URLSafeBase64Encoder
  def self.encode(txt, nonce: false)
    Base64.urlsafe_encode64(txt)
  end

  def self.decode(txt, nonce: false)
    Base64.urlsafe_decode64(txt)
  end
end

class MySchema < GraphQL::Schema
  # ...
  cursor_encoder(URLSafeBase64Encoder)
end

Now, all connections will use URL-safe base-64 encoding.

From a connection instance, the cursor_encoder methods are available via GraphQL::Pagination::Connection #encode and #decode


Guides Pagination Custom Connections (`guides/pagination/custom_connections.md`)

Custom Connections

GraphQL-Ruby ships with built-in connection support for ActiveRecord, Sequel, Mongoid, and Ruby Arrays. You can read more in the Using Connections guide.

When you want to serve a connection based on your own data object, you can create a custom connection. The implementation will have several components:

  • The application object -- the list of items that you want to paginate in GraphQL
  • The connection wrapper which wraps the application object and implements methods used by GraphQL
  • The connection type, a GraphQL object type which implements the connection contract

For this example, we'll imagine that your application communicates with an external search engine, and expresses all search results with a SearchEngine::Result class. (There isn't really any class like this; it's an arbitrary example of an application-specific collection of items.)

Application Object

In Ruby, everything is an object, and that includes lists of objects. For example, we think of an Array as a list of objects, but Arrays are also objects in their own right.

Some list objects have very fancy implementations. Think of an ActiveRecord::Relation: it gathers up the parts of a SQL query, and at the right moment, it dispatches a call to your database to fetch the objects in the list. An ActiveRecord::Relation is also a list object.

Your application probably has other list objects that you want to paginate via GraphQL connections. For example, you might show a user some search results, or a list of files from a fileserver. Those lists are modeled with list objects, and those list objects can be wrapped with connection wrappers.

Connection Wrapper

A connection wrapper is an adapter between a plain-Ruby list object (like an Array, Relation, or something application-specific, like SearchEngine::Result) and a GraphQL connection type. The connection wrapper implements methods which the GraphQL connection type requires, and it implements those methods based on the underlying list object.

You can extend GraphQL::Pagination::Connection to get started on a custom connection wrapper, for example:

ruby
# app/graphql/connections/search_results_connection.rb
class Connections::SearchResultsConnection < GraphQL::Pagination::Connection
  # implementation here ...
end

The methods you must implement are:

  • #nodes, which returns a paginated slice of @items based on the given arguments
  • #has_next_page, which returns true if there are items after the ones in #nodes
  • #has_previous_page, which returns true if there are items before the ones in #nodes
  • #cursor_for(item), which returns a String to serve as the cursor for item

How to implement these methods (efficiently!) depends on your backend and how you communicate with it. For inspiration, you can see the built-in connections:

Using a Custom Connection

To integrate your custom connection wrapper with GraphQL, you have two options:

  • Map the wrapper to a list object at the schema level, so that those list objects are always automatically wrapped by your wrapper; OR
  • Use the wrapper manually in resolve methods, to override any automatic mapping

The first case is very convenient, and the second case makes it possible to customize connections for specific situations.

To map the wrapper to a class of objects, add it to your schema:

ruby
class MySchema < GraphQL::Schema
  # Hook up a custom wrapper
  connections.add(SearchEngine::Result, Connections::SearchResultsConnection)
end

Now, any time a field returns an instance of SearchEngine::Result, it will be wrapped with Connections::SearchResultsConnection

Alternatively, you can apply a connection wrapper on a case-by-case basis by applying it during the resolver (method or GraphQL::Schema::Resolver):

ruby
field :search, Types::SearchResult.connection_type, null: false do
  argument :query, String
end

def search(query:)
  search = SearchEngine::Search.new(query: query, viewer: context[:current_user])
  results = search.results
  # Apply the connection wrapper and return it
  Connections::SearchResultsConnection.new(results)
end

GraphQL-Ruby will use the provided connection wrapper in that case. You can use this fine-grained approach to handle special cases or implement performance optimizations.

Connection Type

Connection types are GraphQL object types which comply to the Relay connection specification. GraphQL-Ruby ships with some tools to help you create those object types:

  • GraphQL::Types::Relay::BaseConnection and GraphQL::Types::Relay::BaseEdge are example implementations of the spec. They don't inherit from your application's base object class though, so you might not be able to use them out of the box.
  • Type classes respond to .connection_type which returns a generated connection type based on that class. By default, it inherits from the provided GraphQL::Types::Relay::BaseConnection, but you can override that by setting connection_type_class(Types::MyBaseConnectionObject) in your base classes.

For example, you could implement a base connection class:

ruby
class Types::BaseConnectionObject < Types::BaseObject
  # implement based on `GraphQL::Types::Relay::BaseConnection`, etc
end

Then hook it up to your base classes:

ruby
class Types::BaseObject < GraphQL::Schema::Object
  # ...
  connection_type_class(Types::BaseConnectionObject)
end

class Types::BaseUnion < GraphQL::Schema::Union
  connection_type_class(Types::BaseConnectionObject)
end

module Types::BaseInterface
  include GraphQL::Schema::Interface

  connection_type_class(Types::BaseConnectionObject)
end

Then, when defining fields, you could use .connection_type to use your connection class hierarchy:

ruby
field :posts, Types::Post.connection_type, null: false

(Those fields get connection: true by default, because the generated connection type's name ends in *Connection.)


Guides Pagination Stable Relation Connections (`guides/pagination/stable_relation_connections.md`)

Stable Relation Connections

GraphQL::Pro includes a mechanism for serving stable connections for ActiveRecord::Relations based on column values. If objects are created or destroyed during pagination, the list of items won't be disrupted.

These connection implementations are database-specific so that they can build proper queries with regard to NULL handling. (Postgres treats nulls as larger than other values while MySQL and SQLite treat them as smaller than other values.)

What's the difference?

The default GraphQL::Pagination::ActiveRecordRelationConnection (which turns an ActiveRecord::Relation into a GraphQL-ready connection) uses offset as a cursor. This naive approach is sufficient for many cases, but it's subject to a specific set of bugs.

Let's say you're looking at the second page of 10 items (LIMIT 10 OFFSET 10). During that time, one of the items on page 1 is deleted. When you navigate to page 3 (LIMIT 10 OFFSET 20), you'll actually miss one item. The entire list shifted "up" one position when a previous item was deleted.

To solve this bug, we should use a value to page through items (instead of offset). For example, if items are ordered by id, use the id for pagination:

sql
LIMIT 10                      -- page 1
WHERE id > :last_id LIMIT 10  -- page 2

This way, even when items are added or removed, pagination will continue without interruption.

For more information about this issue, see "Pagination: You're (Probably) Doing It Wrong".

Installation

You can use a stable connection for all ActiveRecord::Relations by installing it at the schema level:

ruby
class MyAppSchema < GraphQL::Schema
  # Hook up the stable connection that matches your database
  connections.add(ActiveRecord::Relation, GraphQL::Pro::PostgresStableRelationConnection)
  # Or...
  # connections.add(ActiveRecord::Relation, GraphQL::Pro::MySQLStableRelationConnection)
  # connections.add(ActiveRecord::Relation, GraphQL::Pro::SqliteStableRelationConnection)
end

Alternatively, you can apply the stable connection wrapper on a field-by-field basis. For example:

ruby
field :items, Types::ItemType.connection_type, null: false

def items
  # Build an ActiveRecord::Relation
  relation = Item.all
  # And wrap it with a connection implementation, then return the connection
  GraphQL::Pro::MySQLStableRelationConnection.new(relation)
end

That way, you can adopt stable cursors bit-by-bit. (See below for backwards compatibility notes.)

Similarly, if you enable stable connections for the whole schema, you can wrap specific relations with GraphQL::Pagination::ActiveRecordRelationConnection when you want to use index-based cursors. (This is handy for relations whose ordering is too complicated for cursor generation.)

Implementation Notes

Keep these points in mind when using value-based cursors:

  • For a given ActiveRecord::Relation, only columns of that specific model can be used in pagination. (This is because column names are turned into WHERE conditions.)
  • The connection may add an additional primary_key ordering to ensure that the cursor value is unique. This behavior is inspired by Relation#reverse_order which also assumes that primary_key is the default sort.
  • The connection will add fields to the relation's SELECT clause, so that cursors can be reliably constructed from the database results.

Grouped Relations

When using a grouped ActiveRecord::Relation, include a unique ID in your sort to ensure that each row in the result has a unique cursor. For example:

ruby
# Bad: If two results have the same `max(price)`,
# they will be identical from a pagination perspective:
Products.select("max(price) as price").group("category_id").order("price")

# Good: `category_id` is used to disambiguate any results with the same price:
Products.select("max(price) as price").group("category_id").order("price, category_id")

For ungrouped relations, this issue is handled automatically by adding the model's primary_key to the order values.

If you provide an unordered, grouped relation, GraphQL::Pro::RelationConnection::InvalidRelationError will be raised because an unordered relation cannot be paginated in a stable way.

Backwards Compatibility

GraphQL::Pro's stable relation connection is backwards-compatible. If it receives an offset-based cursor, it uses that cursor for the next resolution, then returns value-based cursors in the next result.

ActiveRecord Versions

Stable relation connections support ActiveRecord >= 4.1.0.


Guides Pagination Using Connections (`guides/pagination/using_connections.md`)

Using Connections

GraphQL-Ruby ships with a few implementations of the connection pattern that you can use out of the box. They support Ruby Arrays, Mongoid, Sequel, and ActiveRecord.

Additionally, connections allow you to limit the number of items returned with max_page_size and set the default number of items returned with default_page_size.

Make Connection Fields

Use .connection_type to generate a connection type for paginating over objects of a given type:

ruby
field :items, Types::ItemType.connection_type, null: false

The generated return type will be called ItemConnection. Since it ends in *Connection, the field(...) will automatically be configured with connection: true. If the connection type's name doesn't end in Connection, you have to add that configuration yourself:

ruby
# here's a custom type whose name doesn't end in "Connection", so `connection: true` is required:
field :items, Types::ItemConnectionPage, null: false, connection: true

The field will be given some arguments by default: first, last, after, and before.

Opting out of default connection handling

To opt out of GraphQL-Ruby's default connection handling, add connection: false to the field definition:

diff
- field :items, Types::ItemType.connection_type, null: false
+ field :items, Types::ItemType.connection_type, null: false, connection: false

Then, add any arguments you want (first, last, after, before) and make sure that your resolver returns an object that can fulfill the fields of the configured return type.

Return Collections

With connection fields, you can return collection objects from fields or resolvers:

ruby
def items
  object.items # => eg, returns an ActiveRecord Relation
end

The collection object (Array, Mongoid relation, Sequel dataset, ActiveRecord relation) will be automatically paginated with the provided arguments. Cursors will be generated based on the offset of nodes in the collection.

Make Custom Connections

If you want to paginate something that isn't supported out-of-the-box, you can implement your own pagination wrapper and hook it up to GraphQL-Ruby. Read more in Custom Connections.

Special Cases

Sometimes, you have one collection that needs special handling, unlike other instances of its class. For cases like this, you can manually apply the connection wrapper in the resolver. For example:

ruby
def items
  # Get the ActiveRecord relation to paginate
  relation = object.items
  # Apply a custom wrapper
  Connections::ItemsConnection.new(relation)
end

This way, you can handle this particular relation with custom code.

Max Page Size

You can apply max_page_size to limit the number of items returned and queried from the database, regardless of what the client requests.

  • For the whole schema, you can add it to your schema definition:
ruby
class MyAppSchema < GraphQL::Schema
  default_max_page_size 50
end

At runtime, that value will be applied to every connection, unless an override is provided as described below.

  • For a given field, add it to the field definition with a keyword:
ruby
field :items, Item.connection_type, null: false,
  max_page_size: 25
  • Dynamically, you can add max_page_size: when you apply custom connection wrappers:
ruby
def items
  relation = object.items
  Connections::ItemsConnection.new(relation, max_page_size: 10)
end

To remove a max_page_size setting, you can pass nil. That will allow unbounded collections to be returned to clients.

Default Page Size

You can apply default_page_size to limit the number of items returned and queried from the database when no first or last is provided.

  • For the whole schema, you can add it to your schema definition:
ruby
class MyAppSchema < GraphQL::Schema
  default_page_size 50
end

At runtime, that value will be applied to every connection, unless an override is provided as described below.

  • For a given field, add it to the field definition with a keyword:
ruby
field :items, Item.connection_type, null: false,
  default_page_size: 25
  • Dynamically, you can add default_page_size: when you apply custom connection wrappers:
ruby
def items
  relation = object.items
  Connections::ItemsConnection.new(relation, default_page_size: 10)
end

If max_page_size is set and default_page_size is higher than it, the default_page_size will be clamped down to match max_page_size. If both default_page_size and max_page_size are set to nil, unbounded collections will be returned.


Guides Pro Dashboard (`guides/pro/dashboard.md`)

Dashboard

GraphQL-Pro includes a web dashboard for monitoring Operation Store and subscriptions.

Installation

To hook up the Dashboard, add it to routes.rb

ruby
# config/routes.rb

# Include GraphQL::Pro's routing extensions:
using GraphQL::Pro::Routes

Rails.application.routes.draw do
  # ...
  # Add the GraphQL::Pro Dashboard
  # TODO: authorize, see below
  mount MySchema.dashboard, at: "/graphql/dashboard"
end

With this configuration, it will be available at /graphql/dashboard.

The dashboard is a Rack app, so you can mount it in Sinatra or any other Rack app.

Lazy-loading the schema

Alternatively, you can set up the dashboard to load the schema during the first request. To do that, initialize GraphQL::Pro::Routes::Lazy with a string that gives the fully-qualified name of your schema class, for example:

ruby
Rails.application.routes.draw do
  # ...
  # Add the GraphQL::Pro Dashboard
  # TODO: authorize, see below
  lazy_routes = GraphQL::Pro::Routes::Lazy.new("MySchema")
  mount lazy_routes.dashboard, at: "/graphql/dashboard"
end

With this setup, MySchema will be loaded when the dashboard serves its first request. This can speed up your application's boot in development since it doesn't load the whole GraphQL schema when building the routes.

Authorizing the Dashboard

You should only allow admin users to see /graphql/dashboard because it allows viewers to delete stored operations.

Rails Routing Constraints

Use Rails routing constraints to restrict access to authorized users, for example:

ruby
# Check the secure session for a staff flag:
STAFF_ONLY = ->(request) { request.session["staff"] == true }
# Only serve the GraphQL Dashboard to staff users:
constraints(STAFF_ONLY) do
  mount MySchema.dashboard, at: "/graphql/dashboard"
end

Rack Basic Authentication

Insert the Rack::Auth::Basic middleware, before the web view. This prompts for a username and password when visiting the dashboard.

ruby
graphql_dashboard = Rack::Builder.new do
  use(Rack::Auth::Basic) do |username, password|
    username == ENV.fetch("GRAPHQL_USERNAME") && password == ENV.fetch("GRAPHQL_PASSWORD")
  end

  run MySchema.dashboard
end
mount graphql_dashboard, at: "/graphql/dashboard"

Guides Pro Encoders (`guides/pro/encoders.md`)

Encrypted, Versioned Cursors and IDs

GraphQL::Pro includes a mechanism for serving encrypted, versioned cursors and IDs. This provides some benefits:

  • Users can't reverse-engineer node IDs or connection cursors, removing a possible attack vector.
  • You can gradually transition between cursor strategies, adding encrypting while supporting any "stale" encoders which clients already have.

GraphQL::Pro's encrypted encoders provide a few security features:

  • Key-based encryption by aes-128-gcm by default
  • Authentication
  • Nonces for cursors (but not IDs, that would be silly)

Defining an Encoder

Encoders can be created by subclassing GraphQL::Pro::Encoder:

ruby
class MyEncoder < GraphQL::Pro::Encoder
  key("f411f30...")
  # optional:
  tag("81ce51c307")
end
  • key is the encryption key for this encoder. You can generate one with: require "securerandom"; SecureRandom.bytes(16)
  • tag, if provided, is used as authentication data or for disambiguating versioned encoders

Encrypting Cursors

Encrypt cursors by attaching an encrypted encoder to Schema#cursor_encoder:

ruby
class MySchema GraphQL::Schema
  cursor_encoder(MyCursorEncoder)
end

Now, built-in connection implementations will use that encoder for cursors.

If you implement your own connections, you can access the encoder's encryption methods via GraphQL::Pagination::Connection#encode and GraphQL::Pagination::Connection#decode.

Encrypting IDs

Encrypt IDs by using encoders in Schema.id_from_object and Schema.object_from_id:

ruby
class MySchema < GraphQL::Schema
  def self.id_from_object(object, type, ctx)
    id_data = "#{object.class.name}/#{object.id}"
    MyIDEncoder.encode(id_data)
  end

  def self.object_from_id(id, ctx)
    id_data = MyIDEncoder.decode(id)
    class_name, id = id_data.split("/")
    class_name.constantize.find(id)
  end
end

Note that IDs are not encrypted with nonces. This means that if someone can guess how IDs are constructed, they can determine the encryption key (a kind of known-plaintext attack). To reduce this risk, make your plaintext IDs unpredictable, for example, by appending a salt or obfuscating their content.

Versioning

You can combine several encoders into a single chain of versioned encoders. Pass them to .versioned, newest-to-oldest:

ruby
# Define some encoders ...
class NewSecureEncoder < GraphQL::Pro::Encoder
  # ...
end

class OldSecureEncoder < GraphQL::Pro::Encoder
  # ...
end

class LegacyInsecureEncoder < GraphQL::Pro::Encoder
  # ...
end

# Then order them by priority:
VersionedEncoder = GraphQL::Pro::Encoder.versioned(
  # Newest:
  NewSecureEncoder,
  OldSecureEncoder,
  # Oldest:
  LegacyInsecureEncoder
)

When receiving an ID or cursor, a versioned encoders tries each encoder in sequence. When creating a new ID or cursor, the encoder always uses the first encoder. This way, clients will receiving new encoders, but the server will still accept old encoders (until the old one is removed from the list).

VersionedEncoder#decode_versioned returns two values: the decoded data and the encoder which successfully decoded it. You can use this to determine how to process decoded data. For example, you can switch on the encoder:

ruby
data, encoder = VersionedEncoder.decode_versioned(id)
case encoder
when UUIDEncoder
  find_by_uuid(data)
when SQLPrimaryKeyEncoder
  find_by_pk(data)
when nil
  # `id` could not be decoded
  nil
end

Encoding

By default, encrypted bytes is stringified as base-64. You can specific a custom encoder with the Encoder#encoder definition. For example, you could define an encode which uses URL-safe base-64 functions:

ruby
module URLSafeEncoder
  def self.encode(str)
    Base64.urlsafe_encode64(str)
  end
  def self.decode(str)
    Base64.urlsafe_decode64(str)
  end
end

Then attach it to your encoder:

ruby
class MyURLSafeEncoder < GraphQL::Pro::Encoder
  encoder URLSafeEncoder
end

Now, these node IDs and cursors will be URL-safe!


Guides Pro Installation (`guides/pro/installation.md`)

Installation

GraphQL::Pro is distributed as a Ruby gem. When you buy GraphQL::Pro, you'll receive credentials, which you can register with bundler:

sh
bundle config gems.graphql.pro #{YOUR_CREDENTIALS}

Then, you can add graphql-pro to your Gemfile, which a custom source:

ruby
source "https://gems.graphql.pro" do
  gem "graphql-pro"
end

Then, install the gem with Bundler:

sh
bundle install

Then, check out some of GraphQL::Pro's features!

Updates

To update GraphQL::Pro, use Bundler:

sh
bundle update graphql-pro

Be sure to check the changelog between versions!

Dependencies

graphql-pro 1.0.0 requires graphql ~>1.4. The latest version requires graphql =>1.7.6.

Verifying Integrity

You can verify the integrity of graphql-pro by getting its checksum and comparing it to the published checksums.

Include the graphql:pro:validate task in your Rakefile:

ruby
# Rakefile
require "graphql/rake_task/validate"

Then invoke it with a version:

text
$ bundle exec rake graphql:pro:validate[1.0.0]
Validating graphql-pro v1.0.0
  - Checking for graphql-pro credentials...
    ✓ found
  - Fetching the gem...
    ✓ fetched
  - Validating digest...
    ✓ validated from GitHub
    ✓ validated from graphql-ruby.org
✔ graphql-pro 1.0.0 validated successfully!

In case of a failure, please open an issue:

text
Validating graphql-pro v1.4.800
  - Checking for graphql-pro credentials...
    ✓ found
  - Fetching the gem...
    ✓ fetched
  - Validating digest...
    ✘ SHA mismatch:
      Downloaded:       c9cab2619aa6540605ce7922784fc84dbba3623383fdce6b17fde01d8da0aff49d666810c97f66310013c030e3ab7712094ee2d8f1ea9ce79aaf65c1684d992a
      GitHub:           404: Not Found
      graphql-ruby.org: 404: Not Found

      This download of graphql-pro is invalid, please open an issue:
      https://github.com/rmosolgo/graphql-ruby/issues/new?title=graphql-pro%20digest%20mismatch%20(1.4.800)

Guides Pro Privacy (`guides/pro/privacy.md`)

Privacy

The following statement describes what data GraphQL::Pro collects during normal operation and how that data is used.

What Data We Collect And How It's Used

GraphQL::Pro collects the following kinds of data:

Kind of Data Use
Cookies User authentication
HTTP traffic information, such as IP address and bundler credential Authentication, system operation
User-provided profile information, such as contact email address and company name Business communications, newsletter delivery (if signed up)
Billing information Recurring payment for GraphQL::Pro license

The graphql-pro Ruby gem collects no data and never "phones home" for any purpose.

GraphQL::Pro is not directed at children under the age of 13. If you are under age 13, please do not use GraphQL::Pro.

Third-Party Services

Use of GraphQL::Pro includes the following third-party services:

Name of Service Use
GitHub Pages Website Hosting
Digital Ocean Cloud Application Hosting
Heroku Cloud Application Hosting
Bugsnag Application Monitoring
New Relic APM Application Monitoring
Papertrail Application Monitoring
Stripe Payment Processing
Buttondown Newsletter Management
Google Apps Company Infrastructure

Data Security

GraphQL::Pro does not collect "Sensitive Personal Information". GraphQL::Pro's systems are secured with strong, unique passwords and two-factor authentication is enabled wherever possible.

You are responsible for using a strong, unique password to log into https://billing.graphql.pro.

More Information

This document is managed on GitHub. You can use a GitHub account to watch for changes or subscribe to a public RSS feed.

For questions, comments, or requests regarding this policy, please contact [email protected]


Guides Queries Ast Analysis (`guides/queries/ast_analysis.md`)

Ahead-of-Time AST Analysis

You can do ahead-of-time analysis for your queries.

The primitive for analysis is GraphQL::Analysis::Analyzer. Analyzers must inherit from this base class and implement the desired methods for analysis.

Using Analyzers

Query analyzers are added to the schema with query_analyzer, for example:

ruby
class MySchema < GraphQL::Schema
  query_analyzer MyQueryAnalyzer
end

Pass the class (and not an instance) of your analyzer. The analysis engine will take care of instantiating your analyzers with the query.

Analyzer API

Analyzers respond to methods similar to AST visitors. They're named like on_enter_#{ast_node} and on_leave_#{ast_node}. Methods are called with three arguments:

  • node: The current AST node (being entered or left)
  • parent: The AST node which precedes this one in the tree
  • visitor: A GraphQL::Analysis::Visitor which is managing this analysis run

For example:

ruby
class BasicCounterAnalyzer < GraphQL::Analysis::Analyzer
  def initialize(query_or_multiplex)
    super
    @fields = Set.new
    @arguments = Set.new
  end

  # Visitors are all defined on the Analyzer base class
  # We override them for custom analyzers.
  def on_leave_field(node, _parent, _visitor)
    @fields.add(node.name)
  end

  def result
    # Do something with the gathered result.
    Analytics.log(@fields)
  end
end

In this example, we counted every field, no matter if it was on fragment definitions
or if it was skipped by directives. If we want to detect those contexts, we can use helper
methods:

ruby
class BasicFieldAnalyzer < GraphQL::Analysis::Analyzer
  def initialize(query_or_multiplex)
    super
    @fields = Set.new
  end

  # Visitors are all defined on the Analyzer base class
  # We override them for custom analyzers.
  def on_leave_field(node, _parent, visitor)
    if visitor.skipping? || visitor.visiting_fragment_definition?
      # We don't want to count skipped fields or fields
      # inside fragment definitions
    else
      @fields.add(node.name)
    end
  end

  def result
    Analytics.log(@fields)
  end
end

See GraphQL::Analysis::Visitor for more information about the visitor object.

Field Arguments

Usually, analyzers will use on_enter_field and on_leave_field to process queries. To get a field's arguments during analysis, use visitor.query.arguments_for(node, visitor.field_definition) (GraphQL::Query#arguments_for). That method returns coerced argument values and normalizes argument literals and variable values.

Errors

It is still possible to return errors from an analyzer. To reject a query and halt its execution, you may return GraphQL::AnalysisError in the result method:

ruby
class NoFieldsCalledHello < GraphQL::Analysis::Analyzer
  def on_leave_field(node, _parent, visitor)
    if node.name == "hello"
      @field_called_hello = true
    end
  end

  def result
    GraphQL::AnalysisError.new("A field called `hello` was found.") if @field_called_hello
  end
end

Conditional Analysis

Some analyzers might only make sense in certain context, or some might be too expensive to run for every query. To handle these scenarios, your analyzers may answer to an analyze? method:

ruby
class BasicFieldAnalyzer < GraphQL::Analysis::Analyzer
  # Use the analyze? method to enable or disable a certain analyzer
  # at query time.
  def analyze?
    !!subject.context[:should_analyze]
  end

  def on_leave_field(node, _parent, visitor)
    # ...
  end

  def result
    # ...
  end
end

Analyzing Multiplexes

Analyzers are initialized with the unit of analysis, available as subject.

When analyzers are hooked up to multiplexes, query is nil, but multiplex returns the subject of analysis. You can use visitor.query inside visit methods to reference the query that owns the current AST node.

Note that some built-in analyzers (eg AST::MaxQueryDepth) support multiplexes even though Query is in their name.


Guides Queries Backtrace Annotations (`guides/queries/backtrace_annotations.md`)

Backtrace Annotations

context objects have a backtrace which shows its GraphQL context. You can print the backtrace during query execution:

ruby
puts context.backtrace
# Loc  | Field                         | Object       | Arguments             | Result
# 3:13 | User.login                    | #<User id=1> | {"message"=>"Boom"}   | #<RuntimeError: This is broken: Boom>
# 2:11 | Query.user                    | nil          | {"login"=>"rmosolgo"} | {}
# 1:9  | query                         | nil          | {"msg"=>"Boom"}       |

The backtrace contains some execution data:

  • Loc is the line:column of the field in the query string
  • Field is the TypeName.fieldName of the fields in the backtrace
  • Object is the obj for query resolution (used for resolving the given field), equivalent to obj.inspect
  • Arguments are the GraphQL arguments for field resolution (including any default values and variable values)
  • Result is the GraphQL-ready result which is being constructed (it may be incomplete if the query is still in-progress)

Wrapping Errors

You can wrap unhandled errors with a GraphQL error with GraphQL::Backtrace.

To enable this feature for a query, add backtrace: true to your context, for example:

ruby
# Wrap this query with backtrace annotation
MySchema.execute(query_string, context: { backtrace: true })

Or, to always wrap backtraces, add it to your schema definition with use, for example:

ruby
class MySchema < GraphQL::Schema
  # Always wrap backtraces with GraphQL annotation
  use GraphQL::Backtrace
end

Now, any unhandled errors will be wrapped by GraphQL::Backtrace::TracedError, which prints out the GraphQL backtrace, too. For example:

text
/* Detailed source-code truncated for AI context efficiency. */

Guides Queries Complexity And Depth (`guides/queries/complexity_and_depth.md`)

Complexity & Depth

GraphQL-Ruby ships with some validations based on query analysis. You can customize them as-needed, too.

Prevent deeply-nested queries

You can also reject queries based on the depth of their nesting. You can define max_depth at schema-level or query-level:

ruby
# Schema-level:
class MySchema < GraphQL::Schema
  # ...
  max_depth 15
end

# Query-level, which overrides the schema-level setting:
MySchema.execute(query_string, max_depth: 20)

By default, introspection fields are counted. The default introspection query requires at least max_depth 13. You can also configure your schema not to count introspection fields with max_depth ..., count_introspection_fields: false.

You can use nil to disable the validation:

ruby
# This query won't be validated:
MySchema.execute(query_string, max_depth: nil)

To get a feeling for depth of queries in your system, you can extend GraphQL::Analysis::QueryDepth. Hook it up to log out values from each query:

ruby
class LogQueryDepth < GraphQL::Analysis::QueryDepth
  def result
    query_depth = super
    message = "[GraphQL Query Depth] #{query_depth} || staff?  #{query.context[:current_user].staff?}"
    Rails.logger.info(message)
  end
end

class MySchema < GraphQL::Schema
  query_analyzer(LogQueryDepth)
end

Prevent complex queries

Fields have a "complexity" value which can be configured in their definition. It can be a constant (numeric) value, or a proc. If no complexity is defined for a field, it will default to a value of 1. It can be defined as a keyword or inside the configuration block. For example:

ruby
# Constant complexity:
field :top_score, Integer, null: false, complexity: 10

# Dynamic complexity:
field :top_scorers, [PlayerType], null: false do
  argument :limit, Integer, limit: false, default_value: 5
  complexity ->(ctx, args, child_complexity) {
    if ctx[:current_user].staff?
      # no limit for staff users
      0
    else
      # `child_complexity` is the value for selections
      # which were made on the items of this list.
      #
      # We don't know how many items will be fetched because
      # we haven't run the query yet, but we can estimate by
      # using the `limit` argument which we defined above.
      args[:limit] * child_complexity
    end
  }
end

Then, define your max_complexity at the schema-level:

ruby
class MySchema < GraphQL::Schema
  # ...
  max_complexity 100
end

Or, at the query-level, which overrides the schema-level setting:

ruby
MySchema.execute(query_string, max_complexity: 100)

Using nil will disable the validation:

ruby
# 😧 Anything goes!
MySchema.execute(query_string, max_complexity: nil)

To get a feeling for complexity of queries in your system, you can extend GraphQL::Analysis::QueryComplexity. Hook it up to log out values from each query:

ruby
class LogQueryComplexityAnalyzer < GraphQL::Analysis::QueryComplexity
  # Override this method to _do something_ with the calculated complexity value
  def result
    complexity = super
    message = "[GraphQL Query Complexity] #{complexity} | staff? #{query.context[:current_user].staff?}"
    Rails.logger.info(message)
  end
end

class MySchema < GraphQL::Schema
  query_analyzer(LogQueryComplexityAnalyzer)
end

By default, introspection fields are counted. You can also configure your schema not to count introspection fields with max_complexity ..., count_introspection_fields: false.

Connection fields

By default, GraphQL-Ruby calculates a complexity value for connection fields by:

  • adding 1 for pageInfo and each of its subselections

  • adding 1 for count, totalCount, or total

  • adding 1 for the connection field itself

  • multiplying the complexity of other fields by the largest possible page size, which is the greater of first: or last:, or if neither of those are given it will go through each of default_page_size, the schema's default_page_size, max_page_size, and then the schema's default_max_page_size.

    (If no default page size or max page size can be determined, then the analysis crashes with an internal error -- set default_page_size or default_max_page_size in your schema to prevent this.)

For example, this query has complexity 26:

graphql
query {
  author {              # +1
    name                # +1
    books(first: 10) {  # +1
      nodes {           # +10 (+1, multiplied by `first:` above)
        title           # +10 (ditto)
      }
      pageInfo {        # +1
        endCursor       # +1
      }
      totalCount        # +1
    }
  }
}

To customize this behavior, implement def calculate_complexity(query:, nodes:, child_complexity:) in your base field class, handling the case where self.connection? is true:

ruby
class Types::BaseField < GraphQL::Schema::Field
  def calculate_complexity(query:, nodes:, child_complexity:)
    if connection?
      # Custom connection calculation goes here
    else
      super
    end
  end
end

How complexity scoring works

GraphQL Ruby's complexity scoring algorithm is biased towards selection fairness. While highly accurate, its results are not always intuitive. Here's an example query performed on the Shopify Admin API:

graphql
query {
  node(id: "123") { # interface Node
    id
    ...on HasMetafields { # interface HasMetafields
      metafield(key: "a") {
        value
      }
      metafields(first: 10) {
        nodes {
          value
        }
      }
    }
    ...on Product { # implements HasMetafields
      title
      metafield(key: "a") {
        definition {
          description
        }
      }
    }
    ...on PriceList {
      name
      catalog {
        id
      }
    }
  }
}

First, GraphQL Ruby allows field definitions to specify a complexity attribute that provides a complexity score (or a proc that computes a score) for each field. Let's say that this schema defines a system where:

  • Leaf fields cost 0
  • Composite fields cost 1
  • Connection fields cost children * input size

Given these parameters, we get an itemized scoring distribution of:

graphql
query {
  node(id: "123") { # 1, composite
    id # 0, leaf
    ...on HasMetafields {
      metafield(key: "a") { # 1, composite
        value # 0, leaf
      }
      metafields(first: 10) { # 1 * 10, connection
        nodes { # 1, composite
          value # 0, leaf
        }
      }
    }
    ...on Product {
      title # 0, leaf
      metafield(key: "a") { # 1, composite
        definition { # 1, composite
          description # 0, leaf
        }
      }
    }
    ...on PriceList {
      name # 0, leaf
      catalog { # 1, composite
        id # 0, leaf
      }
    }
  }
}

However, we cannot naively tally these itemized scores without over-costing the query. Consider:

  • The node scope makes many possible selections on an abstract type, so we need the maximum among concrete possibilities for a fair representation.
  • A node.metafield selection path is duplicated across the HasMetafields and Product selection scopes. This path will only resolve once, so should also only cost once.

To reconcile these possibilities, the complexity algorithm breaks the selection down into a tree of types mapped to possible selections, across which lexical selections can be coalesced and deduplicated (pseudocode):

ruby
{
  Schema::Query => {
    "node" => {
      Schema::Node => {
        "id" => nil,
      },
      Schema::HasMetafields => {
        "metafield" => {
          Schema::Metafield => {
            "value" => nil,
          },
        },
        "metafields" => {
          Schema::Metafield => {
            "nodes" => { ... },
          },
        },
      },
      Schema::Product => {
        "title" => nil,
        "metafield" => {
          Schema::Metafield => {
            "definition" => { ... },
          },
        },
      },
      Schema::PriceList => {
        "name" => nil,
        "catalog" => {
          Schema::Catalog => {
            "id" => nil,
          },
        },
      },
    },
  },
}

This aggregation provides a new perspective on the scoring where possible typed selections have costs rather than individual fields. In this normalized view, Product acquires the HasMetafields interface costs, and ignores a duplicated path. Ultimately the maximum of possible typed costs is used, making this query cost 12:

graphql
query {
  node(id: "123") { # max(11, 12, 1) = 12
    id
    ...on HasMetafields { # 1 + 10 = 11
      metafield(key: "a") { # 1
        value
      }
      metafields(first: 10) { # 10
        nodes {
          value
        }
      }
    }
    ...on Product { # 1 + 11 from HasMetafields = 12
      title
      metafield(key: "a") { # duplicated in HasMetafields
        definition { # 1
          description
        }
      }
    }
    ...on PriceList { # 1 = 1
      name
      catalog { # 1
        id
      }
    }
  }
}

Guides Queries Executing Queries (`guides/queries/executing_queries.md`)

Executing Queries

You can execute queries with your GraphQL::Schema and get a Ruby Hash as a result. For example, to execute a query from a string:

ruby
query_string = "{ ... }"
MySchema.execute(query_string)
# {
#   "data" => { ... }
# }

Or, you can execute multiple queries at once:

ruby
MySchema.multiplex([
  {query: query_string_1},
  {query: query_string_2},
  {query: query_string_3},
])
# [
#   { "data" => { ... } },
#   { "data" => { ... } },
#   { "data" => { ... } },
# ]

The complete option contract for variables:, context:, root_value:,
operation_name:, document:, validation, and query limits is maintained in the
GraphQL::Query and
GraphQL::Schema API references. The examples below
focus on the application-level patterns built on those options.

The API-specific portions of this page were migrated to the
GraphQL::Query and
GraphQL::Schema source comments. This page keeps
the variables, context, scoped-context, and root-value walkthroughs.

Variables

GraphQL provides query variables as a way to parameterize query strings. If your query string contains variables, you can provide values in a hash of { String => value } pairs. The keys should not contain "$".

For example, to provide variables to a query:

ruby
query_string = "
  query getPost($postId: ID!) {
    post(id: $postId) {
      title
    }
  }"

variables = { "postId" => "1" }

MySchema.execute(query_string, variables: variables)

If the variable is a GraphQL::Schema::InputObject, you can provide a nested hash, for example:

ruby
query_string = "
mutation createPost($postParams: PostInput!, $createdById: ID!){
  createPost(params: $postParams, createdById: $createdById) {
    id
    title
    createdBy { name }
  }
}
"

variables = {
  "postParams" => {
    "title" => "...",
    "body" => "..."
  },
  "createdById" => "5",
}

MySchema.execute(query_string, variables: variables)

Context

You can provide application-specific values to GraphQL as context:. This is available in many places:

  • resolve functions
  • Schema#resolve_type hook
  • ID generation & fetching

Common uses for context: include the current user or auth token. To provide a context: value, pass a hash to Schema#execute:

ruby
context = {
  current_user: session[:current_user],
  current_organization: session[:current_organization],
}

MySchema.execute(query_string, context: context)

Then, you can access those values during execution:

ruby
field :post, Post do
  argument :id, ID
end

def post(id:)
  context[:current_user] # => #<User id=123 ... >
  # ...
end

Note that context is not the hash that you passed it. It's an instance of GraphQL::Query::Context, but it delegates #[], #[]=, and a few other methods to the hash you provide.

Scoped Context

context is shared by the whole query. Anything you add to context will be accessible by any other field in the query (although GraphQL-Ruby's order of execution can vary).

However, "scoped context" can be used to assign values into context that are only available in the current field and the children of the current field. For example, in this query:

graphql
{
  posts {
    comments {
      author {
        isOriginalPoster
      }
    }
  }
}

You could use "scoped context" to implement isOriginalPoster, based on the parent comments field.

Warning:

Using scoped context may result in a violation of the GraphQL specification and
break normalized client stores, which assume that a given object always
has the same values for its fields.

See "Referencing ancestors breaks normalized stores"
for details about this pitfall and alternative approaches which avoid it.

In def comments, add :current_post to scoped context using context.scoped_set!:

ruby
class Types::Post < Types::BaseObject
  # ...
  def comments
    context.scoped_set!(:current_post, object)
    object.comments
  end
end

Then, inside User (assuming author resolves to Types::User), you can check context[:current_post]:

ruby
class Types::User < Types::BaseObject
  # ...
  def is_original_poster
    current_post = context[:current_post]
    current_post && current_post.author == object
  end
end

context[:current_post] will be present if an "upstream" field assigned it with scoped_set!.

context.scoped_merge!({ ... }) is also available for setting multiple keys at once.

Note: With batched data loading (eg, GraphQL-Batch), scoped context might not work because of GraphQL-Ruby's control flow jumps from one field to the next. In that case, use scoped_ctx = context.scoped to grab a scoped context reference before calling a loader, then used scoped_ctx.set! or scoped_ctx.merge! to modify scoped context inside the promise body. For example:

ruby
# For use with GraphQL-Batch promises:
scoped_ctx = context.scoped
SomethingLoader.load(:something).then do |thing|
  scoped_ctx.set!(:thing_name, thing.name)
end

Root Value

You can provide a root object value with root_value:. For example, to base the query off of the current organization:

ruby
current_org = session[:current_organization]
MySchema.execute(query_string, root_value: current_org)

That value will be provided to root-level fields, such as mutation fields. For example:

ruby
class Types::MutationType < GraphQL::Schema::Object
  field :create_post, Post

  def create_post(**args)
    object # => #<Organization id=456 ...>
    # ...
  end
end

GraphQL::Schema::Mutation fields will also receive root_value: as obj (assuming they're attached directly to your MutationType).


Guides Queries Logging (`guides/queries/logging.md`)

Logging

At runtime, GraphQL-Ruby will output debug information using GraphQL::Query#logger. By default, this uses Rails.logger. To see output, make sure config.log_level = :debug is set. (This information isn't meant for production logs.)

You can configure a custom logger with GraphQL::Schema.default_logger, for example:

ruby
class MySchema < GraphQL::Schema
  # This logger will be used by queries during execution:
  default_logger MyCustomLogger.new
end

You can also pass context[:logger] to provide a logger during execution.


Guides Queries Lookahead (`guides/queries/lookahead.md`)

Lookahead

GraphQL-Ruby 1.9+ includes GraphQL::Execution::Lookahead for checking whether child fields are selected. You can use this to optimize database access, for example, selecting only the needed fields from the database.

Getting a Lookahead

Add extras: [:lookahead] to your field configuration to receive an injected lookahead:

ruby
field :files, [Types::File], null: false, extras: [:lookahead]

Then, update your resolver method to accept a lookahead: argument:

ruby
def files(lookahead:)
  # ...
end

That argument will be injected by the GraphQL runtime.

Using a lookahead

Inside your field resolver, you can use the lookahead to check for child fields. For example, you can check for a specific selection:

ruby
def files(lookahead:)
  if lookahead.selects?(:full_path)
    # This is a query like `files { fullPath ... }`
  else
    # This query doesn't have `fullPath`
  end
end

Or, you can list all the selected fields:

ruby
def files(lookahead:)
  all_selections = lookahead.selections.map(&:name)
  if all_selections == [:name]
    # Only `files { name }` was selected, use a fast cached value:
    object.file_names.map { |n| { name: n }}
  else
    # Lots of fields were selected, fall back to a more resource-intensive approach
    FileSystemHelper.load_files_for(object)
  end
end

Lookaheads are chainable, so you can use them to check nested selections too:

ruby
def files(lookahead:)
  if lookahead.selection(:history).selects?(:author)
    # For example, `files { history { author { ... } } }`
    # We're checking for commit authors, so load those objects appropriately ...
  else
    # Not selecting commit authors ...
  end
end

Nested lookaheads return empty objects when there's no selection (not nil), so the code above will never have a "no method error on nil".

Lookaheads with connections

If you want to see what selections were made on the items in a connection, you can use nested lookaheads. However, don't forget to check for edges { node } and nodes { ... }, if you support that shortcut field. For example:

ruby
field :products, Types::Product.connection_type, null: false, extras: [:lookahead]

def products(lookahead:)
  selects_quantity_available = lookahead.selection(:nodes).selects?(:quantity_available) ||
                               # ^^ check for `products { nodes { quantityAvailable } }`
    lookahead.selection(:edges).selection(:node).selects?(:quantity_available)
    # ^^ check for `products { edges { node { quantityAvailable } } }`

  if selects_quantity_available
    # ...
  else
    # ...
  end
end

That way, you can check for specific selections on the nodes in a connection.

Lookaheads with aliases

If you want to find selection by its alias, you can use #alias_selection(...) or check if it exists with #selects_alias?. In this case, the lookahead will check if there is a field with the provided alias.

For example, this query can find a bird species by its name:

graphql
query {
  gull: findBirdSpecies(byName: "Laughing Gull") {
    name
  }

  tanager: findBirdSpecies(byName: "Scarlet Tanager") {
    name
  }
}

You can get the lookahead for each selection in a following way:

ruby
def find_bird_species(by_name:, lookahead:)
  if lookahead.selects_alias?("gull")
    lookahead.alias_selection("gull")
  end
end

Guides Queries Multiplex (`guides/queries/multiplex.md`)

Multiplex

Some clients may send several queries to the server at once (for example, Apollo Client's query batching). You can execute them concurrently with Schema.multiplex.

Multiplex runs have their own context, analyzers and instrumentation.

NOTE: As an implementation detail, all queries run inside multiplexes. That is, a stand-alone query is executed as a "multiplex of one", so instrumentation and multiplex analyzers and tracers will apply to standalone queries run with MySchema.execute(...).

Concurrent Execution

To run queries concurrently, build an array of query options, using query: for the query string. For example:

ruby
# Prepare the context for each query:
context = {
  current_user: current_user,
}

# Prepare the query options:
queries = [
  {
   query: "query Query1 { someField }",
   variables: {},
   operation_name: 'Query1',
   context: context,
 },
 {
   query: "query Query2 ($num: Int){ plusOne(num: $num) }",
   variables: { num: 3 },
   operation_name: 'Query2',
   context: context,
 }
]

Then, pass them to Schema.multiplex:

ruby
results = MySchema.multiplex(queries)

results will contain the result for each query in queries. NOTE: The results will always be in the same order that their respective requests were sent in.

Apollo Query Batching

Apollo sends batches of queries as an array of queries. Rails' ActionDispatch will parse the request and put the result into the _json field of the params variable. You also need to ensure that your schema can handle both batched and non-batched queries, below is an example of the default GraphqlController rewritten to handle Apollo batches:

ruby
def execute
  context = {}

  # Apollo sends the queries in an array when batching is enabled. The data ends up in the _json field of the params variable.
  # see the Apollo Documentation about query batching: https://www.apollographql.com/docs/react/api/link/apollo-link-batch-http/
  result = if params[:_json]
    queries = params[:_json].map do |param|
      {
        query: param[:query],
        operation_name: param[:operationName],
        variables: ensure_hash(param[:variables]),
        context: context
      }
    end
    MySchema.multiplex(queries)
  else
    MySchema.execute(
      params[:query],
      operation_name: params[:operationName],
      variables: ensure_hash(params[:variables]),
      context: context
    )
  end

  render json: result, root: false
end

Validation and Error Handling

Each query is validated and analyzed independently. The results array may include a mix of successful results and failed results.

Multiplex-Level Context

You can add values to Execution::Multiplex#context by providing a context: hash:

ruby
MySchema.multiplex(queries, context: { current_user: current_user })

This will be available to instrumentation as multiplex.context[:current_user] (see below).

Multiplex-Level Analysis

You can analyze all queries in a multiplex by adding a multiplex analyzer. For example:

ruby
class MySchema < GraphQL::Schema
  # ...
  multiplex_analyzer(MyAnalyzer)
end

The API is the same as query analyzers.

Multiplex analyzers may return GraphQL::AnalysisError to halt execution of the whole multiplex.

Multiplex Tracing

You can add hooks for each multiplex run with trace modules.

The trace module may implement def execute_multiplex(multiplex:) which calls super to allow the multiplex to execute. See Execution::Multiplex for available methods.

For example:

ruby
# Count how many queries are in the multiplex run:
module MultiplexCounter
  def execute_multiplex(multiplex:)
    Rails.logger.info("Multiplex size: #{multiplex.queries.length}")
    super
  end
end

# ...

class MySchema < GraphQL::Schema
  # ...
  trace_with(MultiplexCounter )
end

Now, MultiplexCounter#execute_multiplex will be called for each execution, logging the size of each multiplex.


Guides Queries Phases Of Execution (`guides/queries/phases_of_execution.md`)

Phases of Execution

When GraphQL receives a query string, it goes through these steps:


Guides Queries Response Extensions (`guides/queries/response_extensions.md`)

Response Extensions

During query execution, you can add to the response's "extensions" => { ... } Hash. By default, no "extensions" key is present in the result, but if you call the method below, it will be present with the given values.

To add to "extensions", call context.response_extensions[key] = value during execution. For example:

ruby
field :to_dos, [ToDo]

def to_dos
  warnings = context.response_extensions["warnings"] ||= []
  warnings << "To-Dos will be disabled on Jan. 31, 2022."
  context[:current_user].deprecated_to_dos
end

That would add to the final query response:

ruby
{
  "data" => { ... },
  "extensions" => {
    "warnings" => ["To-Dos will be disabled on Jan. 31, 2022"],
  },
}

Values written to context.response_extensions are added to the GraphQL response verbatim.


Guides Queries Timeout (`guides/queries/timeout.md`)

Timeout

You can apply a timeout to query execution with the GraphQL::Schema::Timeout plugin. For example:

ruby
class MySchema < GraphQL::Schema
  use GraphQL::Schema::Timeout, max_seconds: 2
end

After max_seconds, no new fields will be resolved. Instead, errors will be added to the errors key for fields that weren't resolved.

Note that this does not interrupt field execution (doing so is buggy). If you're making external calls (eg, HTTP requests or database queries), make sure to use a library-specific timeout for that operation (eg, Redis timeout, Net::HTTP's ssl_timeout, open_timeout, and read_timeout).

Custom Error Handling

To log the error, provide a subclass of GraphQL::Schema::Timeout with an overridden handle_timeout method:

ruby
class MyTimeout < GraphQL::Schema::Timeout
  def handle_timeout(error, query)
    Rails.logger.warn("GraphQL Timeout: #{error.message}: #{query.query_string}")
  end
end

class MySchema < GraphQL::Schema
  use MyTimeout, max_seconds: 2
end

Customizing the Timeout Window

To dynamically pick a timeout duration (or bypass it), override GraphQL::Schema::Timeout#max_seconds in your subclass. To bypass the timeout altogether, max_seconds can return false.

For example:

ruby
class MyTimeout < GraphQL::Schema::Timeout
  # Allow 10s for an incoming mutation, but don't apply any timeout for an admin user.
  def max_seconds(query)
    if query.context[:current_user]&.admin?
      false
    elsif query.mutation?
      10
    else
      super
    end
  end
end

# ...

class MySchema < GraphQL::Schema
  use MyTimeout, max_seconds: 5
end

Validation and Analysis

Queries can originate from a user, and may be crafted in a manner to take a long time to validate against the schema.

It is possible to limit how many seconds the static validation rules and analysers are allowed to run before returning a validation timeout error. By default, validation and query analysis have a 3-second timeout. You can customize this timeout or disable it completely:

For example:

ruby
# Customize timeout (in seconds)
class MySchema < GraphQL::Schema
  # Applies to static validation and query analysis
  validate_timeout 10
end

# OR disable timeout completely
class MySchema < GraphQL::Schema
  validate_timeout nil
end

Note: This configuration uses Ruby's built-in Timeout API, which can interrupt IO calls mid-flight, resulting in very weird bugs. None of GraphQL-Ruby's validators make IO calls but if you want to use this configuration and you have custom static validators that make IO calls, open an issue to discuss implementing this in an IO-safe way.


Guides Queries Tracing (`guides/queries/tracing.md`)

Tracing

GraphQL::Tracing::Trace provides hooks to observe and modify events during runtime. Tracing hooks are methods, defined in modules and mixed in with Schema.trace_with.

ruby
module CustomTrace
  def parse(query_string:)
    # measure, log, etc
    super
  end

  # ...
end

To include a trace module when running queries, add it to the schema with trace_with:

ruby
# Run `MyCustomTrace` for all queries
class MySchema < GraphQL::Schema
  trace_with(MyCustomTrace)
end

For a full list of methods and their arguments, see GraphQL::Tracing::Trace.

By default, GraphQL-Ruby makes a new trace instance when it runs a query. You can pass an existing instance as context: { trace: ... }. Also, GraphQL.parse( ..., trace: ...) accepts a trace instance.

Detailed Traces

You can capture detailed traces of query execution with Tracing::DetailedTrace. They can be viewed in Google's Perfetto Trace Viewer. They include a per-Fiber breakdown with links between fields and Dataloader sources.

Learn how to set it up in the Tracing::DetailedTrace docs.

External Monitoring Platforms

There integrations for GraphQL-Ruby with several other monitoring systems:


Guides Relay Range Add (`guides/relay/range_add.md`)

RangeAdd helper for mutations

Relay specifies RANGE_ADD operations for adding items to connections. GraphQL-Ruby ships with GraphQL::Relay::RangeAdd to help implement this. Check the API docs for a usage example.


Guides Schema Definition (`guides/schema/definition.md`)

Definition

A GraphQL system is called a schema. The schema contains all the types and fields in the system. The schema executes queries and publishes an introspection system.

Your GraphQL schema is a class that extends GraphQL::Schema, for example:

ruby
class MyAppSchema < GraphQL::Schema
  max_complexity 400
  query Types::Query
  use GraphQL::Dataloader

  # Define hooks as class methods:
  def self.resolve_type(type, obj, ctx)
    # ...
  end

  def self.object_from_id(node_id, ctx)
    # ...
  end

  def self.id_from_object(object, type, ctx)
    # ...
  end
end

There are lots of schema configuration methods. The complete reference is
maintained with the implementation in GraphQL::Schema,
including root types, object identification, error hooks, limits, introspection,
authorization, tracing, analyzers, and plugins.

The API-specific portions of this page were migrated to the
GraphQL::Schema source comments. This page keeps the
setup, lazy-loading, and production walkthroughs.

For defining GraphQL types, see the guides for those types: object types, interface types, union types, input object types, enum types, and scalar types.

Types in the Schema

See the GraphQL::Schema API reference for
root types, orphan types, and extra types. For defining GraphQL types, see the
guides for object types, interface types,
union types, input objects,
enums, and scalars.

Lazy-loading types

In development, GraphQL-Ruby can defer loading your type definitions until they're needed. This requires some configuration to opt in:

  • Add use GraphQL::Schema::Visibility to your schema. (GraphQL::Schema::Visibility supports lazy loading and will be the default in a future GraphQL-Ruby version. See Migration Notes if you have an existing visibility implementation.)

  • Move your entry-point type definitions into a block, for example:

    diff
    - query Types::Query
    + query { Types::Query }
  • Optionally, move field types into blocks, too:

    diff
    - field :posts, [Types::Post] # Loads `types/post.rb` immediately
    + field :posts do
    +   type([Types::Post]) # Loads `types/post.rb` when this field is used in a query
    + end

To enforce these patterns, you can enable two Rubocop rules that ship with GraphQL-Ruby:

  • GraphQL/RootTypesInBlock will make sure that query, mutation, and subscription are all defined in a block.
  • GraphQL/FieldTypeInBlock will make sure that non-built-in field return types are defined in blocks.

Object Identification

The GraphQL::Schema API reference
documents object_from_id, id_from_object, and resolve_type. For Relay-style
IDs, see Object Identification; for loads:, see
auto-loading arguments.

Error Handling

The GraphQL::Schema API reference
describes the error hooks. See the Type errors guide and
error handling guide for application-level examples.

Default Limits

See the GraphQL::Schema API reference
for depth, complexity, page-size, timeout, error-count, and query-token limits.
The Complexity and Depth and Timeout
guides show deployment-oriented examples.

Introspection

The GraphQL::Schema API reference
documents extra_types and custom introspection namespaces. See the
introspection guide for a custom introspection system.

Authorization

The GraphQL::Schema API reference
documents the unauthorized object and field hooks. See the
authorization guide for the complete authorization flow.

Execution Configuration

The GraphQL::Schema API reference
documents tracing, analyzers, logging, custom execution classes, lazy values, and
plugins. See Tracing, Analysis,
Logging, and lazy execution for
cross-cutting examples.

Plugins

Use Schema.use to install plugins such as
GraphQL::Dataloader and
GraphQL::Schema::Visibility. The plugin
contract is documented in the GraphQL::Schema API reference.

Production Considerations

  • Parser caching: if your application parses GraphQL files (queries or schema definition), it may benefit from enabling GraphQL::Language::Cache.

  • Eager loading the library: by default, GraphQL-Ruby autoloads its constants as-needed. In production, they should be eager loaded instead, using GraphQL.eager_load!.

    • Rails: enabled automatically. (ActiveSupport calls .eager_load!.)
    • Sinatra: add configure(:production) { GraphQL.eager_load! } to your application file.
    • Hanami: add environment(:production) { GraphQL.eager_load! } to your application file.
    • Other frameworks: call GraphQL.eager_load! when your application is booting in production mode.

    See GraphQL::Autoload#eager_load! for more details.


Guides Schema Dynamic Types (`guides/schema/dynamic_types.md`)

Dynamic types and fields

You can use different versions of your GraphQL schema for each operation. To do this, add use GraphQL::Schema::Visibility and implement visible?(context) on the parts of your schema that will be conditionally accessible. Additionally, many schema elements have definition methods which are called at runtime by GraphQL-Ruby. You can re-implement those to return any valid schema objects.

GraphQL-Ruby caches schema elements for the duration of the operation, but if you're making external service calls to implement the methods below, consider adding a cache layer to improve the client experience and reduce load on your backend.

At runtime, ensure that only one object is visible per name (type name, field name, etc.). (If .visible?(context) returns false, then that part of the schema will be hidden for the current operation.)

When using dynamic schema members, be sure to include the relevant context: ... when generating schema definition files.

Different fields

You can customize which field definitions are used for each operation.

Using `#visible?(context)`

To serve different fields to different clients, implement def visible?(context) in your base field class:

ruby
class Types::BaseField < GraphQL::Schema::Field
  def initialize(*args, for_staff: false, **kwargs, &block)
    super(*args, **kwargs, &block)
    @for_staff = for_staff
  end

  def visible?(context)
    super && case @for_staff
    when true
      !!context[:current_user]&.staff?
    when false
      !context[:current_user]&.staff?
    else
      true
    end
  end
end

Then, you can configure fields with for_staff: true|false:

ruby
field :comments, Types::Comment.connection_type, null: false,
  description: "Comments on this blog post",
  resolver_method: :moderated_comments,
  for_staff: false

field :comments, Types::Comment.connection_type, null: false,
  description: "Comments on this blog post, including unmoderated comments",
  resolver_method: :all_comments,
  for_staff: true

With that configuration, post { comments { ... } } will use def moderated_comments when context[:current_user] is nil or is not .staff?, but when context[:current_user].staff? is true, it will use def all_comments instead.

Using `.fields(context)` and `.get_field(name, context)`

To customize the set of fields used at runtime, you can implement def self.fields(context) in your type classes. It should return a Hash of { String => GraphQL::Schema::Field }.

Along with this, you should implement .get_field(name, context) to return a field for name, if it should exist. For example:

ruby
class Types::User < Types::BaseObject
  def self.fields(context)
    all_fields = super
    if !context[:current_user]&.staff?
      all_fields.delete("isSpammy") # this is staff-only
    end
    all_fields
  end

  def self.get_field(name, context)
    field = super
    if field.graphql_name == "isSpammy" && !context[:current_user]&.staff?
      nil # don't show this field to non-staff
    else
      field
    end
  end
end

Hidden Return Types

Besides field visibility described above, if an field's return type is hidden (that is, it implements self.visible?(context) to return false), then the field will be hidden too.

Different arguments

As with fields, you can use different sets of argument definitions for different GraphQL operations.

Using `#visible?(context)`

To serve different arguments to different clients, implement def visible?(context) in your base argument class:

ruby
class Types::BaseArgument < GraphQL::Schema::Argument
  def initialize(*args, for_staff: false, **kwargs, &block)
    super(*args, **kwargs, &block)
    @for_staff = for_staff
  end

  def visible?(context)
    super && case @for_staff
    when true
      !!context[:current_user]&.staff?
    when false
      !context[:current_user]&.staff?
    else
      true
    end
  end
end

Then, you can configure arguments with for_staff: true|false:

ruby
field :user, Types::User, null: true, description: "Look up a user" do
  # Require a UUID-style ID from non-staff clients:
  argument :id, ID, required: true, for_staff: false
  # Support database primary key lookups for staff clients:
  argument :id, ID, required: false, for_staff: true
  argument :database_id, Int, required: false, for_staff: true
end

def user(id: nil, database_id: nil)
  # ...
end

That way, any staff client will have the option of id or databaseId while non-staff clients must use id.

Using `def arguments(context)` and `def get_argument(name, context)`

Also, you can implement def arguments(context) on your base field class to return a Hash of { String => GraphQL::Schema::Argument } and def get_argument(name, context) to return a GraphQL::Schema::Argument or nil. . If you take this approach, you might want some custom field classes for any types or resolvers that use these methods. That way, you don't have to reimplement the method for all the fields in the schema.

Hidden Input Types

Besides argument visibility described above, if an argument's input type is hidden (that is, it implements self.visible?(context) to return false), then the argument will be hidden too.

Different enum values

Using `#visible?(context)`

You can implement def visible?(context) in your base enum value class to hide some enum values from some clients. For example:

ruby
class BaseEnumValue < GraphQL::Schema::EnumValue
  def initialize(*args, for_staff: false, **kwargs, &block)
    super(*args, **kwargs, &block)
    @for_staff = for_staff
  end

  def visible?(context)
    super && case @for_staff
    when true
      !!context[:current_user]&.staff?
    when false
      !context[:current_user]&.staff?
    else
      true
    end
  end
end

With this base class, you can configure some enum values to be just for staff or non-staff viewers:

ruby
class AccountStatus < Types::BaseEnum
  value "ACTIVE"
  value "INACTIVE"
  # Use this for sensitive account statuses when the viewer is public:
  value "OTHER", for_staff: false
  # Staff-only sensitive account statuses:
  value "BANNED", for_staff: true
  value "PAYMENT_FAILED", for_staff: true
  value "PENDING_VERIFICATION", for_staff: true
end

Using `.enum_values(context)`

Alternatively, you can implement def self.enum_values(context) in your enum types to return an Array of GraphQL::Schema::EnumValues. For example, to return a dynamic set of enum values:

ruby
class ProjectStatus < Types::BaseEnum
  def self.enum_values(context = {})
    # Fetch the values from the database
    status_names = context[:tenant].project_statuses.pluck("name")

    # Then build an Array of Enum values
    status_names.map do |name|
      # Be sure to include `owner: self`, the back-reference from the EnumValue to its parent Enum
      GraphQL::Schema::EnumValue.new(name, owner: self)
    end
  end
end

Different types

You can also use different types for each query. A few behaviors depend on the methods defined above:

  • If a type is not used as a return type, an argument type, or as a member of a union or implementer of an interface, it will be hidden
  • If an interface or union has members, it will be hidden
  • If a field's return type is hidden, the field will be hidden
  • If an argument's input type is hidden, the argument will be hidden

As you can imagine, these different hiding behaviors influence one another and they can cause some real head-scratchers when used simultaneously.

Using `.visible?(context)`

Type classes can implement def self.visible?(context) to hide themselves at runtime:

ruby
class Types::BanReason < Types::BaseEnum
  # Hide any arguments or fields that use this enum
  # unless the current user is staff
  def self.visible?(context)
    super && !!context[:current_user]&.staff?
  end

  # ...
end

Different definitions for the same type

You can provide different implementations of the same type by:

  • Implementing def self.visible?(context) to return true and false in complementary contexts. (They should never both be .visible? => true).
  • Hooking the types up to the schema with different field or argument definitions, as described above

For example, to migrate your Money scalar to a Money object type:

ruby
# Previously, we used a simple string to describe money:
class Types::LegacyMoney < Types::BaseScalar
  # This graphql name will conflict with `Types::Money`,
  # so we have to be careful not to use them at the same time.
  # (GraphQL-Ruby will raise an error if it finds two definitions with the same name at runtime.)
  graphql_name "Money"
  describe "A string describing an amount of money."

  # Use this type definition if the current request
  # explicitly opted in to the legacy money representation:
  def self.visible?(context)
    !!context[:requests_legacy_money]
  end
end

# But we want to improve the client experience with a dedicated object type:
class Types::Money < Types::BaseObject
  field :amount, Integer, null: false
  field :currency, Types::Currency, null: false

  # Use this new definition if the client
  # didn't explicitly ask for the legacy definition:
  def self.visible?(context)
    !context[:requests_legacy_money]
  end
end

Then, hook the definitions up to the schema using field definitions:

ruby
class Types::BaseField < GraphQL::Schema::Field
  def initialize(*args, legacy_money: false, **kwargs, &block)
    super(*args, **kwargs, &block)
    @legacy_money = legacy_money
  end

  def visible?(context)
    super && (@legacy_money ? !!context[:requests_legacy_money] : !context[:requests_legacy_money])
  end
end

class Types::Invoice < Types::BaseObject
  # Add one definition for each possible return type
  # (one definition will be hidden at runtime)
  field :amount, Types::LegacyMoney, null: false, legacy_money: true
  field :amount, Types::Money, null: false, legacy_money: false
end

Input types (like input objects, scalars, and enums) work the same way with argument definitions.

Schema Dumps

To dump a certain version of the schema, provide the applicable context: ... to Schema.to_definition. For example:

ruby
# Legacy money schema:
MySchema.to_definition(context: { requests_legacy_money: true })

or

ruby
# Staff-only schema:
MySchema.to_definition(context: { current_user: OpenStruct.new(staff?: true) })

That way, the given context will be passed to visible?(context) calls and other relevant methods.


Guides Schema Generators (`guides/schema/generators.md`)

Generators

If you're using GraphQL with Ruby on Rails, you can use generators to:

graphql:install

You can add GraphQL to a Rails app with graphql:install:

text
rails generate graphql:install

This will:

  • Set up a folder structure in app/graphql/
  • Add schema definition
  • Add base type classes
  • Add a Query type definition
  • Add a Mutation type definition with a base mutation class
  • Add a route and controller for executing queries
  • Install graphiql-rails
  • Enable ActiveRecord::QueryLogs and add GraphQL-related metadata (using GraphQL::Current)

After installing you can see your new schema by:

  • bundle install
  • rails server
  • Open localhost:3000/graphiql

Options

  • --directory=DIRECTORY will specify the directory where generated files should be saved (default is app/graphql)
  • --schema=MySchemaName will be used for naming the schema (default is #{app_name}Schema)
  • --skip-graphiql will exclude graphiql-rails from the setup
  • --skip-mutation-root-type will not create of the mutation root type
  • --skip-query-logs will skip the QueryLogs setup
  • --relay will add Relay-specific code to your schema
  • --batch will add GraphQL::Batch to your gemfile and include the setup in your schema
  • --playground will include graphql_playground-rails in the setup (mounted at /playground)
  • --api will create smaller stack for API only apps

Scaffolding Types

Several generators will add GraphQL types to your project. Run them with -h to see the options:

  • rails g graphql:object
  • rails g graphql:input
  • rails g graphql:interface
  • rails g graphql:union
  • rails g graphql:enum
  • rails g graphql:scalar

ActiveRecord columns auto-extraction

The graphql:object and graphql:input generators can detect the existence of an ActiveRecord class with the same name, and scaffold all database columns as fields/arguments using appropriate GraphQL types and nullability detection

Options

  • --namespaced-types will generate each one of the object/input/interface/... types under separate Types::Objects::*/Types::Inputs::*/Types::Interfaces::*/... namespaces and folders

Scaffolding Mutations

You can prepare a Relay Classic mutation with

text
rails g graphql:mutation #{mutation_name}

Scaffolding ActiveRecord Mutations

You can generate a Relay Classic create, update or delete mutation for a given model with

text
rails g graphql:mutation_create #{model_class_name}
rails g graphql:mutation_update #{model_class_name}
rails g graphql:mutation_delete #{model_class_name}

model_class_name accepts both namespace/class_type and Namespace::ClassType formats.
This mutation also accepts the --namespaced-types flag, to keep it consistent with the scaffolded Object and Input classes from the type generators

Scaffolding Loaders

You can prepare a GraphQL::Batch loader with

text
rails g graphql:loader

Guides Schema Introspection (`guides/schema/introspection.md`)

Introspection

A GraphQL schema has a built-in introspection system that publishes the schema's structure. In fact, the introspection system can be queried using GraphQL, for example:

graphql
{
  __schema {
    queryType {
      name
    }
  }
}
# Returns:
# {
#   "data": {
#     "__schema": {
#       "queryType": {
#         "name": "Query"
#       }
#     }
#   }
# }

This system is used for GraphQL tooling like the GraphiQL editor.

Here are the default parts of the introspection system:

  • __schema is a root-level field that contains data about the schema: its entry points, types, and directives.
  • __type(name: String!) is a root-level field that returns data about a type with the given name, if there is a type with that name.
  • __typename works a bit differently: it can be added to any selection, and it will return the type of object being queried.

Here are some __typename examples:

graphql
{
  user(id: "1") {
    handle
    __typename
  }
}
# Returns:
# {
#   "data": {
#     "user": {
#       "handle": "rmosolgo",
#       "__typename": "User"
#     }
#   }
# }

For unions and interfaces, __typename returns the object type for the current object, for example:

graphql
{
  search(term: "puppies") {
    title
    __typename
  }
}
# Returns:
# {
#   "data": {
#     "search": [
#       {
#         "title": "Sound of Dogs Barking",
#         "__typename": "AudioClip",
#       },
#       {
#         "title": "Cute puppies playing with a stick",
#         "__typename": "VideoClip",
#       },
#       {
#         "title": "The names of my favorite pets",
#         "__typename": "TextSnippet"
#       },
#     ]
#   }
# }

Customizing Introspection

You can use custom introspection types.

ruby
# create a module namespace for your custom types:
module Introspection
  # described below ...
end

class MySchema < GraphQL::Schema
  # ...
  # then pass the module as `introspection`
  introspection Introspection
end

Keep in mind that off-the-shelf tooling may not support your custom introspection fields. You may have to modify existing tooling or create your own tools to make use of your extensions.

Introspection Namespace

The introspection namespace may contain a few different customizations:

  • Class-based object definitions which replace the built-in introspection types (such as __Schema and __Type)
  • EntryPoints, A class-based object definition containing introspection entry points (like __schema and __type(name:)).
  • DynamicFields, A class-based object definition containing dynamic, globally-available fields (like __typename.)

Custom Introspection Types

The module passed as introspection may contain classes with the following names, which replace the built-in introspection types:

Custom class name GraphQL type Built-in class name
SchemaType __Schema GraphQL::Introspection::SchemaType
TypeType __Type GraphQL::Introspection::TypeType
DirectiveType __Directive GraphQL::Introspection::DirectiveType
DirectiveLocationType __DirectiveLocation GraphQL::Introspection::DirectiveLocationEnum
EnumValueType __EnumValue GraphQL::Introspection::EnumValueType
FieldType __Field GraphQL::Introspection::FieldType
InputValueType __InputValue GraphQL::Introspection::InputValueType
TypeKindType __TypeKind GraphQL::Introspection::TypeKindEnum

The class-based definitions' names must match the names of the types they replace.

Extending a Built-in Type

The built-in classes listed above may be extended:

ruby
module Introspection
  class SchemaType < GraphQL::Introspection::SchemaType
    # ...
  end
end

Inside the class definition, you may:

  • add new fields by calling field(...) and providing implementations
  • redefine field structure by calling field(...)
  • provide new field implementations by defining methods
  • provide new descriptions by calling description(...)

Introspection Entry Points

The GraphQL spec describes two entry points to the introspection system:

  • __schema returns data about the schema (as type __Schema)
  • __type(name:) returns data about a type, if one is found by name (as type __Type)

You can re-implement these fields or create new ones by creating a custom EntryPoints class in your introspection namespace:

ruby
module Introspection
  class EntryPoints < GraphQL::Introspection::EntryPoints
    # ...
  end
end

This class is an object type definition, so you can override existing fields or add new ones here. They'll be available on the root query object, but ignored in introspection (just like __schema and __type).

Dynamic Fields

The GraphQL spec describes a field which may be added to any selection: __typename. It returns the name of the current GraphQL type.

You can add fields like this (or override __typename) by creating a custom DynamicFields definition:

ruby
module Introspection
  class DynamicFields < GraphQL::Introspection::DynamicFields
    # ...
  end
end

Any fields defined there will be available in any selection, but ignored in introspection (just like __typename).

Disabling Introspection

In case you want to turn off introspection entry points __schema and __type (for instance in the production environment) you can use a #disable_introspection_entry_points shorthand method:

ruby
class MySchema < GraphQL::Schema
  disable_introspection_entry_points if Rails.env.production?
end

Where disable_introspection_entry_points will disable both the __schema and __type introspection entry points, you can also individually disable the introspection entry points using the disable_schema_introspection_entry_point and disable_type_introspection_entry_point shorthand methods:

ruby
class MySchema < GraphQL::Schema
  disable_schema_introspection_entry_point
  disable_type_introspection_entry_point
end

Guides Schema Lazy Execution (`guides/schema/lazy_execution.md`)

Lazy Execution

With lazy execution, you can optimize access to external services (such as databases) by making batched calls. Building a lazy loader has three steps:

  • Define a lazy-loading class with one method for loading & returning a value
  • Connect it to your schema with GraphQL::Schema.lazy_resolve
  • In resolve methods, return instances of the lazy-loading class

Example: Batched Find

Here's a way to find many objects by ID using one database call, preventing N+1 queries.

  1. Lazy-loading class which finds models by ID.
ruby
class LazyFindPerson
  def initialize(query_ctx, person_id)
    @person_id = person_id
    # Initialize the loading state for this query,
    # or get the previously-initiated state
    @lazy_state = query_ctx[:lazy_find_person] ||= {
      pending_ids: Set.new,
      loaded_ids: {},
    }
    # Register this ID to be loaded later:
    @lazy_state[:pending_ids] << person_id
  end

  # Return the loaded record, hitting the database if needed
  def person
    # Check if the record was already loaded:
    loaded_record = @lazy_state[:loaded_ids][@person_id]
    if loaded_record
      # The pending IDs were already loaded,
      # so return the result of that previous load
      loaded_record
    else
      # The record hasn't been loaded yet, so
      # hit the database with all pending IDs
      pending_ids = @lazy_state[:pending_ids].to_a
      people = Person.where(id: pending_ids)
      people.each { |person| @lazy_state[:loaded_ids][person.id] = person }
      @lazy_state[:pending_ids].clear
      # Now, get the matching person from the loaded result:
      @lazy_state[:loaded_ids][@person_id]
    end
  end
  1. Connect the lazy resolve method
ruby
class MySchema < GraphQL::Schema
  # ...
  lazy_resolve(LazyFindPerson, :person)
end
  1. Return lazy objects from resolve
ruby
field :author, PersonType

def author
  LazyFindPerson.new(context, object.author_id)
end

Now, calls to author will use batched database access. For example, this query:

graphql
{
  p1: post(id: 1) { author { name } }
  p2: post(id: 2) { author { name } }
  p3: post(id: 3) { author { name } }
}

Will only make one query to load the author values.

Gems for batching

The example above is simple and has some shortcomings. Consider the following gems for a robust solution to batched resolution:

  • GraphQL::Dataloader is a built-in, Fiber-based approach to batching. See the Dataloader guide for more information.
  • graphql-batch provides a powerful, flexible toolkit for lazy resolution with GraphQL.
  • dataloader is more general promise-based utility for batching queries within the same thread.
  • batch-loader works with any Ruby code including GraphQL, no extra dependencies or primitives.

Guides Schema Object Identification (`guides/schema/object_identification.md`)

Object Identification

GraphQL-Ruby ships with some helpers to implement Relay-style object identification.

Schema methods

See the Schema definition guide for required top-level hooks.

Node interface

One requirement for Relay's object management is implementing the "Node" interface.

To implement the node interface, add GraphQL::Types::Relay::Node to your definition:

ruby
class Types::PostType < GraphQL::Schema::Object
  # Implement the "Node" interface for Relay
  implements GraphQL::Types::Relay::Node
  # ...
end

To tell GraphQL how to resolve members of the Node interface, you must also define Schema.resolve_type:

ruby
class MySchema < GraphQL::Schema
  # You'll also need to define `resolve_type` for
  # telling the schema what type Relay `Node` objects are
  def self.resolve_type(type, obj, ctx)
    case obj
    when Post
      Types::PostType
    when Comment
      Types::CommentType
    else
      raise("Unexpected object: #{obj}")
    end
  end
end

UUID fields

Nodes must have a field named "id" which returns a globally unique ID.

To add a UUID field named "id", implement the GraphQL::Types::Relay::Node interface::

ruby
class Types::PostType < GraphQL::Schema::Object
  implements GraphQL::Types::Relay::Node
end

This field will call the previously-defined id_from_object class method.

`node` field (find-by-UUID)

You should also provide a root-level node field so that Relay can refetch objects from your schema. You can attach it like this:

ruby
class Types::QueryType < GraphQL::Schema::Object
  # Used by Relay to lookup objects by UUID:
  # Add `node(id: ID!)
  include GraphQL::Types::Relay::HasNodeField
  # ...
end

`nodes` field

You can also provide a root-level nodes field so that Relay can refetch objects by IDs:

ruby
class Types::QueryType < GraphQL::Schema::Object
  # Fetches a list of objects given a list of IDs
  # Add `nodes(ids: [ID!]!)`
  include GraphQL::Types::Relay::HasNodesField
  # ...
end

Guides Schema Root Types (`guides/schema/root_types.md`)

Root Types

GraphQL queries begin from root types: query, mutation, and subscription.

Attach these to your schema using methods with the same name:

ruby
class MySchema < GraphQL::Schema
  # required
  query Types::QueryType
  # optional
  mutation Types::MutationType
  subscription Types::SubscriptionType
end

The types are GraphQL::Schema::Object classes, for example:

ruby
# app/graphql/types/query_type.rb
class Types::QueryType < GraphQL::Schema::Object
  field :posts, [PostType], 'Returns all blog posts', null: false
end

# Similarly:
class Types::MutationType < GraphQL::Schema::Object
  field :create_post, mutation: Mutations::AddPost
end
# and
class Types::SubscriptionType < GraphQL::Schema::Object
  field :comment_added, subscription: Subscriptions::CommentAdded
end

Each type is the entry point for the corresponding GraphQL query:

ruby
query Posts {
  # `Query.posts`
  posts { ... }
}

mutation AddPost($postAttrs: PostInput!){
  # `Mutation.createPost`
  createPost(attrs: $postAttrs)
}

subscription CommentAdded {
  # `Subscription.commentAdded`
  commentAdded(postId: 1)
}

Guides Schema Sdl (`guides/schema/sdl.md`)

Parsing GraphQL Schema Definition Language into a Ruby Schema

GraphQL-Ruby includes a way to build a runable schema from the GraphQL Schema Definition Language (SDL). GraphQL::Schema.from_definition returns a schema class based on a filename or string containing GraphQL SDL. For example:

ruby
# From a file:
MySchema = GraphQL::Schema.from_definition("path/to/schema.graphql")
# Or, from a string:
MySchema = GraphQL::Schema.from_definition(<<~GRAPHQL)
  type Query {
    # ...
  }
  # ...
GRAPHQL

Definitions from the SDL are converted into Ruby classes, similar to those defined in plain Ruby code.

Execution

You can provide execution behaviors to a generated schema as default_resolve:, which accepts two kinds of values:

  • Implementation Object: an object that implements several methods used at runtime
  • Implementation Hash: keys and nested hashes provide procs for execution

Implementation Object

By providing an object that implements several runtime methods, you can define the execution behaviors of a schema loaded from SDL:

ruby
class SchemaImplementation
  # see below for methods
end

# Pass the object as `default_resolve:`
MySchema = GraphQL::Schema.from_definition(
  "path/to/schema.graphql",
  default_resolve: SchemaImplementation.new
)

The default_resolve: object may implement:

  • #call(type, field, obj, args, ctx) for resolving fields
  • #resolve_type(abstract_type, obj, ctx) for resolving obj as one of abstract_type's possible object types
  • #coerce_input(type, value, ctx) for coercing scalar input
  • #coerce_result(type, value, ctx) for coercing scalar return values

Implementation Hash

Alternatively, you can provide a Hash containing callable behaviors, for example:

ruby
schema_implementation = {
  # ... see below for hash structure
}

# Pass the hash as `default_resolve:`
MySchema = GraphQL::Schema.from_definition(
  "path/to/schema.graphql",
  default_resolve: schema_implementation
)

The hash may contain:

  • A key for each object type name, containing a sub-hash of { field_name => ->(obj, args, ctx) { ... } } for resolving object fields
  • A key for each scalar type name, containing a sub-hash with keys "coerce_result" and "coerce_input", each pointing to a ->(value, ctx) { ... } for handling scalar values at runtime
  • A "resolve_type" key pointing to a ->(abstract_type, object, ctx) { ... } callable, used for resolving object to one of abstract_type's possible types

Plugins

GraphQL::Schema.from_definition accepts a using: argument, which may be given as a map of plugin => args pairs. For example:

ruby
MySchema = GraphQL::Schema.from_definition("path/to/schema.graphql", using: {
  GraphQL::Pro::PusherSubscriptions => { redis: $redis },
  GraphQL::Pro::OperationStore => nil, # no options here
})

Directives

Although GraphQL-Ruby doesn't have special handling for directives in the SDL, you can build custom behavior in your own app. If part of the schema had a directive, you can access it using .ast_node.directives. For example:

ruby
schema = GraphQL::Schema.from_definition <<-GRAPHQL
type Query @flagged {
  secret: Boolean @privacy(secret: true)
}
GRAPHQL

pp schema.query.ast_node.directives.map(&:to_query_string)
# => ["@flagged"]
pp schema.get_field("Query", "secret").ast_node.directives.map(&:to_query_string)
# => ["@privacy(secret: true)"]

See GraphQL::Language::Nodes::Directive for available methods.


Guides Subscriptions Ably Implementation (`guides/subscriptions/ably_implementation.md`)

Ably Implementation

GraphQL Pro includes a subscription system based on Redis and Ably which works with any Ruby web framework.

After creating an app on Ably, you can hook it up to your GraphQL schema.

How it Works

This subscription implementation uses a hybrid approach:

  • Your app takes GraphQL queries an runs them
  • Redis stores subscription data for later updates
  • Ably sends updates to subscribed clients

So, the lifecycle goes like this:

  • A subscription query is sent by HTTP Post to your server (just like a query or mutation)
  • The response contains an Ably channel ID (as an HTTP header) which the client may subscribe to
  • The client opens that Ably channel
  • When the server triggers updates, they're delivered over the Ably channel
  • When the client unsubscribes, the server receives a webhook and responds by removing its subscription data

Here's another look:

text
1. Subscription is created in your app

          HTTP POST
        .---------->   write to Redis
      📱            ⚙️ -----> 💾
        <---------'
        X-Subscription-ID: 1234


2. Client opens a connection to Ably

          websocket
      📱 <---------> ☁️


3. The app sends updates via Ably

      ⚙️ ---------> ☁️ ------> 📱
        POST           update
      (via gem)   (via websocket)


4. When the client unsubscribes, Ably notifies the app

          webhook
      ⚙️ <-------- ☁️  (disconnect) 📱

By using this configuration, you can use GraphQL subscriptions without hosting a push server yourself!

Ably setup

Add ably-rest to your Gemfile:

ruby
gem 'ably-rest'

and bundle install.

Database setup

Subscriptions require a persistent Redis database, configured with:

sh
maxmemory-policy noeviction
# optional, more durable persistence:
appendonly yes

Otherwise, Redis will drop data that doesn't fit in memory (read more in "Redis persistence").

If you're already using Redis in your application, see "Storing Data in Redis" for options to isolate data and tune your configuration.

Schema configuration

Add redis to your Gemfile:

ruby
gem 'redis'

and bundle install. Then create a Redis instance:

ruby
# for example, in an initializer:
$graphql_subscriptions_redis = Redis.new # default connection

Then, that Redis client is passed to the Subscription configuration:

ruby
class MySchema < GraphQL::Schema
  use GraphQL::Pro::AblySubscriptions,
    redis: $graphql_subscriptions_redis,
    ably: Ably::Rest.new(key: ABLY_API_KEY)
end

That connection will be used for managing subscription state. All writes to Redis are prefixed with graphql:sub:.

There are also two configurations for managing persistence:

  • stale_ttl_s: expires subscription data after the given number of seconds without any update. After stale_ttl_s has passed, the data will expire from Redis. Each time a subscription receives an update, its TTL is refreshed. (Generally, this isn't required because the backend is built to clean itself up. But, if you find that Redis is collecting stale queries, you can set them to expire after some very long time as a safeguard.)
  • cleanup_delay_s: (default: 5) prevents deleting a subscription during those first seconds after it's created. Usually, a longer delay isn't necessary, but if you observe latency between the subscription's initial response and the client's subscription to the delivery channel, you can set this configuration to account for it.

Connection Pool

For better performance reading and writing to Redis, you can pass a connection_pool: instead of redis:, using the connection_pool gem:

ruby
use GraphQL::Pro::AblySubscriptions,
    connection_pool: ConnectionPool.new(size: 5, timeout: 5) { Redis.new },
    ably: Ably::Rest.new(key: ABLY_API_KEY)

Broadcasts

If you set up Broadcasts, then you can update many clients over a single Ably channel.

Broadcast channels have stable, predictable IDs. To prevent unauthorized clients from "listening in," use token authorization for transport. Broadcasts channels use the namespace gqlbdcst:, so you can provide capabilities to receive them using "gqlbdcst:*" => [ ... ] in your authorization code. (If you're using encryption, the prefix will be ablyencr-gqlbdcst: instead.)

Execution configuration

During execution, GraphQL will assign a subscription_id to the context hash. The client will use that ID to listen for updates, so you must return the subscription_id in the response headers.

Return result.context[:subscription_id] as the X-Subscription-ID header. For example:

ruby
result = MySchema.execute(...)
# For subscriptions, return the subscription_id as a header
if result.subscription?
  response.headers["X-Subscription-ID"] = result.context[:subscription_id]
end
render json: result

This way, the client can use that ID as a Ably channel.

For CORS requests, you need a special header so that clients can read the custom header:

ruby
if result.subscription?
  response.headers["X-Subscription-ID"] = result.context[:subscription_id]
  # Required for CORS requests:
  response.headers["Access-Control-Expose-Headers"] = "X-Subscription-ID"
end

Read more here: "Using CORS".

Webhook configuration

Your server needs to receive webhooks from Ably when clients disconnect. This keeps your local subscription database in sync with Ably.

Server

Note: if you're setting up in a development environment you should follow the Developing with webhooks section first

Mount the Rack app for handling webhooks from Ably. For example, on Rails:

ruby
# config/routes.rb

# Include GraphQL::Pro's routing extensions:
using GraphQL::Pro::Routes

Rails.application.routes.draw do
  # ...
  # Handle webhooks for subscriptions:
  mount MySchema.ably_webhooks_client, at: "/ably_webhooks"
end

Alternatively, you can configure the routes to load your schema lazily, during the first request:

ruby
# Provide the fully-qualified class name of your schema:
lazy_routes = GraphQL::Pro::Routes::Lazy.new("MySchema")
mount lazy_routes.ably_webhooks_client, at: "/ably_webhooks"

Ably

  1. Go to the Ably dashboard
  2. Click on your application
  3. Select the "Integrations" tab
  4. Click on the "+ New Integration Rule" button
  5. Click on the "Choose" button for "Webhook"
  6. Click on the "Choose" button for "Webhook" (again)
  7. Enter your URL (including the webhooks path from above) in the URL field.
  8. Select "Batch request" for "Request Mode"
  9. Under "Source", select "Presence"
  10. Under "Sign with key", select the API Key prefix that matches the prefix of the ABLY_API_KEY you provided
  11. Click "Create"

Authorization

You can use Ably's token authentication by implementing an endpoint in your app, for example:

ruby
class AblyController < ActionController::Base
  def auth
    render status: 201, json: ably_rest_client.auth.create_token_request(
      capability: { '*' => ['presence', 'subscribe'] },
      client_id: 'graphql-subscriber',
    )
  end
end

Ably's tutorial also demonstrates some of the setup for this.

Encryption

You can use Ably's end-to-end encryption with GraphQL subscriptions. To enable it, add cipher_base: to your setup:

ruby
use GraphQL::Pro::AblySubscriptions,
    redis: $graphql_subscriptions_redis,
    ably: Ably::Rest.new(key: ABLY_API_KEY),
    # Add `cipher_base:` to enable end-to-end encryption
    cipher_base: "ff16381ae2f2b6c6de6ff696226009f3"

(Any random string will do, eg ruby -e "require 'securerandom'; puts SecureRandom.hex".)

Also, return a header to client so that it can decrypt subscription updates. The key is put in context[:ably_cipher_base64], and graphql-ruby-client expects to find it in the X-Subscription-Key header:

ruby
result = MySchema.execute(...)
# For subscriptions, return the subscription_id as a header
if result.subscription?
  response.headers["X-Subscription-ID"] = result.context[:subscription_id]
  # Also return the encryption key so that clients
  # can decode subscription updates
  response.headers["X-Subscription-Key"] = result.context[:ably_cipher_base64]
end

(Also, if you're using CORS requests, update Access-Control-Expose-Headers to include X-Subscription-Key)

With this setup,

  • GraphQL::Pro::AblySubscriptions will generate per-subscription keys (using cipher_base and the subscription ID) and use them to encrypt Ably payloads
  • Those keys will be returned to clients in X-Subscription-Key
  • Clients will use those keys to decrypt incoming messages

Backwards compatibility: GraphQL::Pro::AblySubscriptions will only encrypt payloads whose query.context[:ably_cipher_base64] is present. Any subscriptions created before cipher_base: was added to the setup will not be encrypted. (There was no key to encrypt them, and clients don't have a key to decrypt them!)

Serializing Context

Since subscription state is stored in the database, then reloaded for pushing updates, you have to serialize and reload your query context.

By default, this is done with GraphQL::Subscriptions::Serialize's dump and load methods, but you can provide custom implementations as well. To customize the serialization logic, create a subclass of GraphQL::Pro::AblySubscriptions and override #dump_context(ctx) and #load_context(ctx_string):

ruby
class CustomSubscriptions < GraphQL::Pro::AblySubscriptions
  def dump_context(ctx)
    context_hash = ctx.to_h
    # somehow convert this hash to a string, return the string
  end

  def load_context(ctx_string)
    # Given the string from the DB, create a new hash
    # to use as `context:`
  end
end

Then, use your custom subscriptions class instead of the built-in one for your schema:

ruby
class MySchema < GraphQL::Schema
  # Use custom subscriptions instead of GraphQL::Pro::AblySubscriptions
  # to get custom serialization logic
  use CustomSubscriptions, ...
end

That gives you fine-grained control of context reloading.

Dashboard

You can monitor subscription state in the GraphQL-Pro Dashboard:

Development Tips

Clear subscription data

At any time, you can reset your subscription database with the "Reset" button in the GraphQL-Pro Dashboard, or in Ruby:

ruby
# Wipe all subscription data from the DB:
MySchema.subscriptions.clear

Developing with webhooks

To receive webhooks in development, you can use ngrok. It gives you a public URL which you can setup with Ably, then any hooks delivered to that URL will be forwarded to your development environment.

Client configuration

Install the Ably JS client then see docs for:


Guides Subscriptions Action Cable Implementation (`guides/subscriptions/action_cable_implementation.md`)

Action Cable Implementation

ActionCable is a great platform for delivering GraphQL subscriptions on Rails 5+. It handles message passing (via broadcast) and transport (via transmit over a websocket).

To get started, see examples in the API docs: GraphQL::Subscriptions::ActionCableSubscriptions. GraphQL-Ruby also includes a mock ActionCable implementation for testing: GraphQL::Testing::MockActionCable.

See client usage for:


Guides Subscriptions Broadcast (`guides/subscriptions/broadcast.md`)

Broadcasts

GraphQL subscription updates may broadcast data to multiple subscribers.

A broadcast is a subscription update which is executed once, then delivered to any number of subscribers. This reduces the time your server spends running GraphQL queries, since it doesn't have to re-run the query for every subscriber.

But, take care: this approach risks leaking information to subscribers who shouldn't receive it.

Setup

To enable broadcasts, add broadcast: true to your subscription setup:

ruby
class MyAppSchema < GraphQL::Schema
  # ...
  use SomeSubscriptionImplementation,
    broadcast: true # <----
end

Then, any broadcastable field can be configured with broadcastable: true:

ruby
field :name, String, null: false,
  broadcastable: true

When a subscription comes in where all of its fields are broadcastable: true, then it will be handled as a broadcast.

Additionally, you can set default_broadcastable: true:

ruby
class MyAppSchema < GraphQL::Schema
  # ...
  use SomeSubscriptionImplementation,
    broadcast: true,
    default_broadcastable: true # <----
end

With this setting, fields are broadcastable by default. Only a field with broadcastable: false in its configuration will cause a subscription to be handled on a subscriber-by-subscriber basis.

What fields are broadcastable?

GraphQL-Ruby can't infer whether a field is broadcastable or not. You must configure it explicitly with broadcastable: true or broadcastable: false. (The subscription plugin also accepts default_broadcastable: true|false.)

A field is broadcastable if all clients who request the field will see the same value. For example:

  • General facts: celebrity names, laws of physics, historical dates
  • Public information: object names, document updated-at timestamps, boilerplate info

For fields like this, you can add broadcastable: true.

A field is not broadcastable if its value is different for different clients. For example:

  • Viewer-specific information: if a field is specifically viewer-based, then it can't be broadcasted to other viewers. For example, discussion { viewerCanModerate } might be true for a moderator, but it shouldn't be broadcasted to other viewers.
  • Context-specific information: if a field's value takes the request context into consideration, it shouldn't be broadcasted. For example, IP addresses or HTTP header values probably can't be broadcasted. If a field reflects the viewer's timezone, it can't be broadcasted.
  • Restricted information: if some viewers see one value, while other viewers see a different value, then it's not broadcastable. Broadcasting this data might leak private information to unauthorized clients. (This includes filtered lists: if the filtering is viewer-by-viewer, it's not broadcastable.)
  • Fields with side effects: if the system requires a side effect (eg, logging a metric, updating a database, incrementing a counter) whenever a resolver is executed, it's not a good candidate for broadcasting because some executions will be optimized away.

These fields can be tagged with broadcastable: false so that GraphQL-Ruby will handle them on a subscriber-by-subscriber basis.

If you want to use subscriptions but have a lot of non-broadcastable fields in your schema, consider building a new set of subscription fields with limited access to other schema objects. Instead, optimize those subscriptions for broacastability.

Under the Hood

GraphQL-Ruby determines which subscribers can receive a broadcast by inspecting:

  • Query string. Only exactly-matching query strings will receive the same broadcast.
  • Variables. Only exactly-matching variable values will receive the same broadcast.
  • Field and Arguments given to .trigger. They must match the ones initially sent when subscribing. (Subscriptions always worked this way.)
  • Subscription scope. Only clients with exactly-matching subscription scope can receive the same broadcasts.

So, take care to set subscription_scope whenever a subscription should be implicitly scoped!

(See GraphQL::Subscriptions::Event#fingerprint for the implementation of broadcast fingerprints.)

Checking for Broadcastable

For testing purposes, you can confirm that a GraphQL query string is broadcastable by using Subscriptions#broadcastable?:

ruby
subscription_string = "subscription { ... }"
MySchema.subscriptions.broadcastable?(subscription_string)
# => true or false

Use this in your application's tests to make sure that broadcastable fields aren't accidentally made non-broadcastable.

Connections and Edges

You can configure your generated Connection and Edge types to be broadcastable by setting default_broadcastable(true) in their definition:

ruby
# app/types/base_connection.rb
class Types::BaseConnection < Types::BaseObject
  include GraphQL::Types::Relay::ConnectionBehaviors
  default_broadcastable(true)
end

# app/types/base_edge.rb
class Types::BaseEdge < Types::BaseObject
  include GraphQL::Types::Relay::EdgeBehaviors
  default_broadcastable(true)
end

(In your BaseObject, you should also have connection_type_class(Types::BaseConnection) and edge_type_class(Types::BaseEdge).)

PageInfo is broadcastable by default.


Guides Subscriptions Implementation (`guides/subscriptions/implementation.md`)

Implementation

The GraphQL::Subscriptions plugin is a base class for implementing subscriptions.

Each method corresponds to a step in the subscription lifecycle. See the API docs for method-by-method documentation: GraphQL::Subscriptions.

Also, see the Pusher implementation guide, the Ably implementation guide, the ActionCable implementation guide or GraphQL::Subscriptions::ActionCableSubscriptions docs for an example implementation.

Considerations

Every Ruby application is different, so consider these points when implementing subscriptions:

  • Is your application single-process or multiprocess? Single-process applications can store state in memory while multiprocess applications need a message broker to keep all processes up-to-date.
  • What components of your application can be used for persistence and message passing?
  • How will you deliver push updates to subscribed clients? (For example, websockets, ActionCable, Pusher, webhooks, or something else?)
  • How will you handle thundering herds? When an event is triggered, how will you manage database access to update clients without swamping your system?

Broadcasts

Broadcasting updates to multiple subscribers is supported by GraphQL-Ruby, but requires implementation-specific work, see more in the Broadcast guide.


Guides Subscriptions Multi Tenant (`guides/subscriptions/multi_tenant.md`)

Multi-Tenant

In a multi-tenant system, data from many different accounts is stored on the same server. (An account might be an organization, a customer, a namespace, a domain, etc -- these are all tenants.) Gems like Apartment assist with this arrangement, but it can also be implemented in the application. Here are a few considerations for this architecture when using GraphQL subscriptions.

Add Tenant to `context`

All the approaches below will use context[:tenant] to identify the tenant during GraphQL execution, so make sure to assign it before executing a query:

ruby
context = {
  viewer: current_user,
  tenant: current_user.tenant,
  # ...
}

MySchema.execute(query_str, context: context, ...)

Tenant-based `subscription_scope`

When subscriptions are delivered, subscription_scope is one element used to route data to the right subscriber. In short, it's the implicit identifier for the receiver. In a multi-tenant architecture, subscription_scope should reference the context key that names the tenant, for example:

ruby
class BudgetWasApproved < GraphQL::Schema::Subscription
  subscription_scope :tenant # This would work with `context[:tenant] => "acme-corp"`
  # ...
end

# Include the scope when `.trigger`ing:
BudgetSchema.subscriptions.trigger(:budget_was_approved, {}, { ... }, scope: "acme-corp")

Alternatively, subscription_scope might name something that belongs to the tenant:

ruby
class BudgetWasApproved < GraphQL::Schema::Subscription
  subscription_scope :project_id # This would work with `context[:project_id] = 1234`
end

# Include the scope when `.trigger`ing:
BudgetSchema.subscriptions.trigger(:budget_was_approved, {}, { ... }, scope: 1234)

As long as project_id is unique among all tenants, that would work fine too. But some scope is required so that subscriptions can be disambiguated between tenants.

Choosing a tenant for execution

There are a few places where subscriptions might need to load data:

  • When building the payload for the subscription (fetching data to prepare the result)
  • ActionCableSubscriptions: when deserializing the JSON string broadcasted by ActionCable
  • PusherSubscriptions and AblySubscriptions: when deserializing query context

Each of these operations will need to select the right tenant in order to load data properly.

For building the payload, use a Trace module:

ruby
module TenantSelectionTrace
  def execute_multiplex(multiplex:) # this is the top-level, umbrella event
    context = data[:multiplex].queries.first.context # This assumes that all queries in a multiplex have the same tenant
    MultiTenancy.select_tenant(context[:tenant]) do
      # ^^ your multi-tenancy implementation here
      super # Call through to the rest of execution
    end
  end
end

# ...
class MySchema < GraphQL::Schema
  trace_with(TenantSelectionTrace)
end

The tracer above will use context[:tenant] to select a tenant for the duration of execution for all queries, mutations, and subscriptions.

For deserializing ActionCable messages, provide a serializer: object that implements .dump(obj) and .load(string, context):

ruby
class MultiTenantSerializer
  def self.dump(obj)
    GraphQL::Subscriptions::Serialize.dump(obj)
  end

  def self.load(string, context)
    MultiTenancy.select_tenant(context[:tenant]) do
      GraphQL::Subscriptions::Serialize.load(string)
    end
  end
end

# ...
class MySchema < GraphQL::Schema
  # ...
  use GraphQL::Subscriptions::ActionCableSubscriptions, serializer: MultiTenantSerializer
end

The implementation above will use the built-in serialization algorithms, but it will do so in the context of the selected tenant.

For loading query context in Pusher and Ably, add tenant selection to your load_context method, if required:

ruby
class CustomSubscriptions < GraphQL::Pro::PusherSubscriptions # or `GraphQL::Pro::AblySubscriptions`
  def dump_context(ctx)
    JSON.dump(ctx.to_h)
  end

  def load_context(ctx_string)
    ctx_data = JSON.parse(ctx_string)
    MultiTenancy.select_tenant(ctx_data["tenant"]) do
      # Build a symbol-keyed hash, loading objects from the database if necessary
      # to use a `context: ...`
    end
  end
end

With that approach, the selected tenant will be active when building the context hash, in case any objects need to be loaded from the database.


Guides Subscriptions Pusher Implementation (`guides/subscriptions/pusher_implementation.md`)

Pusher Implementation

GraphQL Pro includes a subscription system based on Redis and Pusher which works with any Ruby web framework.

After creating an app on Pusher and configuring the Ruby gem, you can hook it up to your GraphQL schema.

How it Works

This subscription implementation uses a hybrid approach:

  • Your app takes GraphQL queries an runs them
  • Redis stores subscription data for later updates
  • Pusher sends updates to subscribed clients

So, the lifecycle goes like this:

  • A subscription query is sent by HTTP Post to your server (just like a query or mutation)
  • The response contains a Pusher channel ID (as an HTTP header) which the client may subscribe to
  • The client opens that Pusher channel
  • When the server triggers updates, they're delivered over the Pusher channel
  • When the client unsubscribes, the server receives a webhook and responds by removing its subscription data

Here's another look:

text
1. Subscription is created in your app

          HTTP POST
        .---------->   write to Redis
      📱            ⚙️ -----> 💾
        <---------'
        X-Subscription-ID: 1234


2. Client opens a connection to Pusher

          websocket
      📱 <---------> ☁️


3. The app sends updates via Pusher

      ⚙️ ---------> ☁️ ------> 📱
        POST           update
      (via gem)   (via websocket)


4. When the client unsubscribes, Pusher notifies the app

          webhook
      ⚙️ <-------- ☁️  (disconnect) 📱

By using this configuration, you can use GraphQL subscriptions without hosting a push server yourself!

Database setup

Subscriptions require a persistent Redis database, configured with:

sh
maxmemory-policy noeviction
# optional, more durable persistence:
appendonly yes

Otherwise, Redis will drop data that doesn't fit in memory (read more in "Redis persistence").

If you're already using Redis in your application, see "Storing Data in Redis" for options to isolate data and tune your configuration.

Schema configuration

Add redis to your Gemfile:

ruby
gem 'redis'

and bundle install. Then create a Redis instance:

ruby
# for example, in an initializer:
$graphql_subscriptions_redis = Redis.new # default connection

Then, that Redis client is passed to the Subscription configuration:

ruby
class MySchema < GraphQL::Schema
  use GraphQL::Pro::PusherSubscriptions, redis: $graphql_subscriptions_redis
end

That connection will be used for managing subscription state. All writes to Redis are prefixed with graphql:sub:.

There are two configurations for managing persistence:

  • stale_ttl_s: expires subscription data after the given number of seconds without any update. After stale_ttl_s has passed, the data will expire from Redis. Each time a subscription receives an update, its TTL is refreshed. (Generally, this isn't required because the backend is built to clean itself up. But, if you find that Redis is collecting stale queries, you can set them to expire after some very long time as a safeguard.)
  • cleanup_delay_s: (default: 5) prevents deleting a subscription during those first seconds after it's created. Usually, a longer delay isn't necessary, but if you observe latency between the subscription's initial response and the client's subscription to the delivery channel, you can set this configuration to account for it.

Also, you can use extra_webhook_tokens: [{ key: "...", secret: "..." }, ...] when you need to roll Pusher keys. Pusher uses the oldest active token, so you can pass the old credentials there while you're rolling over to the new one.

Connection Pool

For better performance reading and writing to Redis, you can pass a connection_pool: instead of redis:, using the connection_pool gem:

ruby
use GraphQL::Pro::PusherSubscriptions,
    connection_pool: ConnectionPool.new(size: 5, timeout: 5) { Redis.new },

Broadcasts

If you set up Broadcasts, then you can update many clients over a single Pusher channel.

Broadcast channels have stable, predictable IDs. To prevent unauthorized clients from "listening in," use an authorized Pusher channel for transport. In your authorization code, you can check for a broadcast using .broadcast_subscription_id?:

ruby
# In your Pusher authorization endpoint:
channel_name = params[:channel_name]
MySchema.subscriptions.broadcast_subscription_id?(channel_name)
# => true | false

Execution configuration

During execution, GraphQL will assign a subscription_id to the context hash. The client will use that ID to listen for updates, so you must return the subscription_id in the response headers.

Return result.context[:subscription_id] as the X-Subscription-ID header. For example:

ruby
result = MySchema.execute(...)
# For subscriptions, return the subscription_id as a header
if result.subscription?
  response.headers["X-Subscription-ID"] = result.context[:subscription_id]
end
render json: result

This way, the client can use that ID as a Pusher channel.

For CORS requests, you need a special header so that clients can read the custom header:

ruby
if result.subscription?
  response.headers["X-Subscription-ID"] = result.context[:subscription_id]
  # Required for CORS requests:
  response.headers["Access-Control-Expose-Headers"] = "X-Subscription-ID"
end

Read more here: "Using CORS".

Payload Compression

To mitigate problems with Pusher's 10kb message limit, you can specify compress_pusher_payload: true in the context of your subscription. For example:

ruby
# app/controllers/graphql_controller.rb
def execute
  # ...
  # Somehow detect whether the client supports compressed payloads,
  # for example, User-Agent, query param, or request header:
  if client_supports_compressed_payloads?
    context[:compress_pusher_payload] = true
  end
  # ...
end

This will cause subscription payloads to include compressed_result: "..." instead of result: "..." when they're sent over Pusher. See docs for Apollo Client or Relay Modern to read about preparing clients for compressed payloads.

By configuring compress_pusher_payload: true on a query-by-query basis, the subscription backend can continue to support clients running old client code (by not compressing) while upgrading new clients to compressed payloads.

Batched Deliveries

By default, PusherSubscriptions sends updates in batches of up to 10 at a time, using batch triggers. You can customize the batch size by passing batch_size: when installing it, for example:

ruby
use GraphQL::Pro::PusherSubscriptions, batch_size: 1, ...

batch_size: 1 will make PusherSubscriptions use the single trigger API instead of batch triggers.

Webhook configuration

Your server needs to receive webhooks from Pusher when clients disconnect. This keeps your local subscription database in sync with Pusher.

In the Pusher web UI, Add a webhook for "Channel existence"

Then, mount the Rack app for handling webhooks from Pusher. For example, on Rails:

ruby
# config/routes.rb

# Include GraphQL::Pro's routing extensions:
using GraphQL::Pro::Routes

Rails.application.routes.draw do
  # ...
  # Handle Pusher webhooks for subscriptions:
  mount MySchema.pusher_webhooks_client, at: "/pusher_webhooks"
end

This way, we'll be kept up-to-date with Pusher's unsubscribe events.

Alternatively, you can configure the routes to load your schema lazily, during the first request:

ruby
# Provide the fully-qualified class name of your schema:
lazy_routes = GraphQL::Pro::Routes::Lazy.new("MySchema")
mount lazy_routes.pusher_webhooks_client, at: "/pusher_webhooks"

Authorization

To ensure the privacy of subscription updates, you should use a private channel for transport.

To use a private channel, add a channel_prefix: key to your query context:

ruby
MySchema.execute(
  query_string,
  context: {
    # If this query is a subscription, use this prefix for the Pusher channel:
    channel_prefix: "private-user-#{current_user.id}-",
    # ...
  },
  # ...
)

That prefix will be applied to GraphQL-related Pusher channel names. (The prefix should begin with private-, as required by Pusher.)

Then, in your auth endpoint, you can assert that the logged-in user matches the channel name:

ruby
if params[:channel_name].start_with?("private-user-#{current_user.id}-")
  # success, render the auth token
else
  # failure, render unauthorized
end

Serializing Context

Since subscription state is stored in the database, then reloaded for pushing updates, you have to serialize and reload your query context.

By default, this is done with GraphQL::Subscriptions::Serialize's dump and load methods, but you can provide custom implementations as well. To customize the serialization logic, create a subclass of GraphQL::Pro::PusherSubscriptions and override #dump_context(ctx) and #load_context(ctx_string):

ruby
class CustomSubscriptions < GraphQL::Pro::PusherSubscriptions
  def dump_context(ctx)
    context_hash = ctx.to_h
    # somehow convert this hash to a string, return the string
  end

  def load_context(ctx_string)
    # Given the string from the DB, create a new hash
    # to use as `context:`
  end
end

Then, use your custom subscriptions class instead of the built-in one for your schema:

ruby
class MySchema < GraphQL::Schema
  # Use custom subscriptions instead of GraphQL::Pro::PusherSubscriptions
  # to get custom serialization logic
  use CustomSubscriptions, redis: $redis
end

That gives you fine-grained control of context reloading.

Dashboard

You can monitor subscription state in the GraphQL-Pro Dashboard:

Development Tips

Clear subscription data

At any time, you can reset your subscription database with the "Reset" button in the GraphQL-Pro Dashboard, or in Ruby:

ruby
# Wipe all subscription data from the DB:
MySchema.subscriptions.clear

Developing with Pusher webhooks

To receive Pusher's webhooks in development, Pusher suggests using ngrok. It gives you a public URL which you can setup with Pusher, then any hooks delivered to that URL will be forwarded to your development environment.

Client configuration

Install the Pusher JS client then see docs for:


Guides Subscriptions Subscription Classes (`guides/subscriptions/subscription_classes.md`)

Subscription Classes

You can extend GraphQL::Schema::Subscription to create fields that can be subscribed to.

These classes support several behaviors:

Continue reading to set up subscription classes.

Add a base class

First, add a base class for your application. You can hook up your base classes there:

ruby
# app/graphql/subscriptions/base_subscription.rb
class Subscriptions::BaseSubscription < GraphQL::Schema::Subscription
  # Hook up base classes
  object_class Types::BaseObject
  field_class Types::BaseField
  argument_class Types::BaseArgument
end

(This base class is a lot like the mutation base class. They're both subclasses of GraphQL::Schema::Resolver.)

Extend the base class and hook it up

Define a class for each subscribable event in your system. For example, if you run a chat room, you might publish events whenever messages are posted in a room:

ruby
# app/graphql/subscriptions/message_was_posted.rb
class Subscriptions::MessageWasPosted < Subscriptions::BaseSubscription
end

Then, hook up the new class to the Subscription root type with the subscription: option:

ruby
class Types::SubscriptionType < Types::BaseObject
  field :message_was_posted, subscription: Subscriptions::MessageWasPosted
end

Now, it will be accessible as:

graphql
subscription {
  messageWasPosted(roomId: "abcd") {
    # ...
  }
}

Arguments

Subscription fields take arguments just like normal fields. They also accept a loads: option just like mutations. For example:

ruby
class Subscriptions::MessageWasPosted < Subscriptions::BaseSubscription
  # `room_id` loads a `room`
  argument :room_id, ID, loads: Types::RoomType

  # It's passed to other methods as `room`
  def subscribe(room:)
    # ...
  end

  def update(room:)
    # ...
  end
end

This can be invoked as

graphql
subscription($roomId: ID!) {
  messageWasPosted(roomId: $roomId) {
    # ...
  }
}

If the ID doesn't find an object, then the subscription will be unsubscribed (with #unsubscribe, see below).

Fields

Like mutations, you can use a generated return type for subscriptions. When you add field(...)s to a subscription, they'll be added to the subscription's generated return type. For example:

ruby
class Subscriptions::MessageWasPosted < Subscriptions::BaseSubscription
  field :room, Types::RoomType, null: false
  field :message, Types::MessageType, null: false
end

will generate:

graphql
type MessageWasPostedPayload {
  room: Room!
  message: Message!
}

Which you can use in queries like:

graphql
subscription($roomId: ID!) {
  messageWasPosted(roomId: $roomId) {
    room {
      name
    }
    message {
      author {
        handle
      }
      body
      postedAt
    }
  }
}

If you remove null: false, then you can return different data in the initial subscription and the subsequent updates. (See lifecycle methods below.)

Instead of a generated type, you can provide an already-configured type with payload_type:

ruby
# Just return a message
payload_type Types::MessageType

(In that case, don't return a hash from #subscribe or #update, return a message object instead.)

Scope

Usually, GraphQL-Ruby uses explicitly-passed arguments to determine when a trigger applies to an active subscription. But, you can use subscription_scope to configure implicit conditions on updates. When subscription_scope is configured, only triggers with a matching scope: value will cause clients to receive updates.

subscription_scope accepts a symbol and the given symbol will be looked up in context to find a scope value.

For example, this subscription will use context[:current_organization_id] as a scope:

ruby
class Subscriptions::EmployeeHired < Subscriptions::BaseSubscription
  # ...
  subscription_scope :current_organization_id
end

Clients subscribe without any arguments:

graphql
subscription {
  employeeHired {
    hireDate
    employee {
      name
      department
    }
  }
}

But .triggers are routed using scope:. So, if the subscriber's context includes current_organization_id: 100, then the trigger must include the same scope: value:

ruby
MyAppSchema.subscriptions.trigger(
  # Field name
  :employee_hired,
  # Arguments
  {},
  # Object
  { hire_date: Time.now, employee: new_employee },
  # This corresponds to `context[:current_organization_id]`
  # in the original subscription:
  scope: 100
 )

Scope is also used for determining whether subscribers can receive the same broadcast.

Check Permissions with #authorized?

Suppose a client is subscribing to messages in a chat room:

graphql
subscription($roomId: ID!) {
  messageWasPosted(roomId: $roomId) {
    message {
      author { handle }
      body
      postedAt
    }
  }
}

You can implement #authorized? to check that the user has permission to subscribe to these arguments (and receive updates for these arguments), for example:

ruby
def authorized?(room:)
  super && context[:viewer].can_read_messages?(room)
end

The method may return false or raise a GraphQL::ExecutionError to halt execution.

This method is called before #subscribe and #update, described below. This way, if a user's permissions have changed since they subscribed, they won't receive updates unauthorized updates.

Also, if this method fails before calling #update, then the client will be automatically unsubscribed (with #unsubscribe).

Initial Subscription with #subscribe

def subscribe(**args) is called when a client first sends a subscription { ... } request. In this method, you can do a few things:

  • Raise GraphQL::ExecutionError to halt and return an error
  • Return a value to give the client an initial response
  • Return :no_response to skip the initial response
  • Return super to fall back to the default behavior (which is :no_response).

You can define this method to add initial responses or perform other logic before subscribing.

Adding an Initial Response

By default, GraphQL-Ruby returns nothing (:no_response) on an initial subscription. But, you may choose to override this and return a value in def subscribe. For example:

ruby
class Subscriptions::MessageWasPosted < Subscriptions::BaseSubscription
  # ...
  field :room, Types::RoomType

  def subscribe(room:)
    # authorize, etc ...
    # Return the room in the initial response
    {
      room: room
    }
  end
end

Now, a client can get some initial data with:

graphql
subscription($roomId: ID!) {
  messageWasPosted(roomId: $roomId) {
    room {
      name
      messages(last: 40) {
        # ...
      }
    }
  }
}

Subsequent Updates with #update

After a client has registered a subscription, the application may trigger subscription updates with MySchema.subscriptions.trigger(...) (see the Triggers guide for more). Then, def update will be called for each client's subscription. In this method you can:

  • Unsubscribe the client with unsubscribe
  • Return a value with super (which returns object) or by returning a different value.
  • Return NO_UPDATE to skip this update

Skipping subscription updates

Perhaps you don't want to send updates to a certain subscriber. For example, if someone leaves a comment, you might want to push the new comment to other subscribers, but not the commenter, who already has that comment data. You can accomplish this by returning NO_UPDATE.

ruby
class Subscriptions::CommentWasAdded < Subscriptions::BaseSubscription
  def update(post_id:)
    comment = object # #<Comment ...>
    if comment.author == context[:viewer]
      NO_UPDATE
    else
      # Continue updating this client, since it's not the commenter
      super
    end
  end
end

Returning a different object for subscription updates

By default, whatever object you pass to .trigger(event_name, args, object) will be used for responding to subscription fields. But, you can return a different object from #update to override this:

ruby
field :queue, Types::QueueType, null: false

# eg, `MySchema.subscriptions.trigger("queueWasUpdated", {name: "low-priority"}, :low_priority)`
def update(name:)
  # Make a Queue object which _represents_ the queue with this name
  queue = JobQueue.new(name)

  # This object was passed to `.trigger`, but we're ignoring it:
  object # => :low_priority

  # return the queue instead:
  { queue: queue }
end

Terminating the subscription with #unsubscribe

Within a subscription method, you may call unsubscribe to terminate the client's subscription, for example:

ruby
def update(room:)
  if room.archived?
    # Don't let anyone subscribe to messages on an archived room
    unsubscribe
  else
    super
  end
end

#unsubscribe has the following effects:

  • The subscription is unregistered from the backend (this is backend-specific)
  • The client is told to unsubscribe (this is transport-specific)

Arguments with loads: configurations will call unsubscribe if they are required: true (which is the default) and their ID doesn't return a value. (It's assumed that the subscribed object was deleted.)

You can provide a final update value with unsubscribe by passing a value to the method:

ruby
def update(room:)
  if room.archived?
    # Don't let anyone subscribe to messages on an archived room
    unsubscribe({message: "This room has been archived"})
  else
    super
  end
end

Extras

Subscription methods can access query-related metadata by configuring extras [...] in the class definition. For example, to use a lookahead and the ast_node:

ruby
class Subscriptions::JobFinished < GraphQL::Schema::Subscription
  # ...
  extras [:lookahead, :ast_node]

  def subscribe(lookahead:, ast_node:)
    # ...
  end

  def update(lookahead:, ast_node:)
    # ...
  end
end

See the Extra Field Metadata for more information about available metadata.


Guides Subscriptions Subscription Type (`guides/subscriptions/subscription_type.md`)

Subscription Type

Subscription is the entry point for all subscriptions in a GraphQL system. Each field corresponds to an event which may be subscribed to:

graphql
type Subscription {
  # Triggered whenever a post is added
  postWasPublished: Post
  # Triggered whenever a comment is added;
  # to watch a certain post, provide a `postId`
  commentWasPublished(postId: ID): Comment
}

This type is the root for subscription operations, for example:

graphql
subscription {
  postWasPublished {
    # This data will be delivered whenever `postWasPublished`
    # is triggered by the server:
    title
    author {
      name
    }
  }
}

To add subscriptions to your system, define an ObjectType named Subscription:

ruby
# app/graphql/types/subscription_type.rb
class Types::SubscriptionType < GraphQL::Schema::Object
  field :post_was_published, subscription: Subscriptions::PostWasPublished
  # ...
end

Then, add it as the subscription root with subscription(...):

ruby
# app/graphql/my_schema.rb
class MySchema < GraphQL::Schema
  query(Types::QueryType)
  # ...
  # Add Subscription to
  subscription(Types::SubscriptionType)
end

See Implementing Subscriptions for more about actually delivering updates.

See Subscription Classes for more about implementing subscription root fields.


Guides Subscriptions Triggers (`guides/subscriptions/triggers.md`)

Triggers

From your application, you can push updates to GraphQL clients with .trigger.

Events are triggered by name, and the name must match fields on your Subscription Type

ruby
# Update the system with the new blog post:
MySchema.subscriptions.trigger(:post_added, {}, new_post)

The arguments are:

  • name, which corresponds to the field on subscription type
  • arguments, which corresponds to the arguments on subscription type (for example, if you subscribe to comments on a certain post, the arguments would be {post_id: comment.post_id}.)
  • object, which will be the root object of the subscription update
  • scope: (shown below) for implicitly scoping the clients who will receive updates.

Scope

To send updates to certain clients only, you can use scope: to narrow the trigger's reach.

Scopes are based on query context: a value in context: is used as the scope; an equivalent value must be passed with .trigger(... scope:) to update that client. (The value is serialized with GraphQL::Subscriptions::Serialize.)

To specify that a topic is scoped, add a subscription_scope option to the Subscription class:

ruby
class Subscriptions::CommentAdded < Subscription::BaseSubscription
  description "A comment was added to one of the viewer's posts"
  # For a given viewer, this will be triggered
  # whenever one of their posts gets a new comment
  subscription_scope :current_user_id
  # ...
end

(Read more in the Subscription Classes guide.)

Then, subscription operations should have a context: { current_user_id: ... } value, for example:

ruby
# current_user_id will be the scope for some subscriptions:
MySchema.execute(query_string, context: { current_user_id: current_user.id })

Finally, when events happen in your app, you should provide the scoping value as scope:, for example:

ruby
# A new comment is added
comment = post.comments.create!(attrs)
# notify the author
author_id = post.author.id
MySchema.subscriptions.trigger(:comment_added, {}, comment, scope: author_id)

Since this trigger has a scope:, only subscribers with a matching scope value will be updated.

Validation

By default, subscriptions are re-validated when a trigger causes them to send updates. To disable this, you can pass validate_update: false when hooking up subscriptions to your schema. For example:

ruby
use SomeSubscriptions, validate_update: false

If you're sure you won't be releasing breaking changes to your schema, this setting can reduce overhead in evaluating updates.


Guides Testing Helpers (`guides/testing/helpers.md`)

Helpers

GraphQL-Ruby ships with a test helper method, run_graphql_field, that can execute a GraphQL field in isolation. To use it in your test suite, include the module with your schema class:

ruby
# Mix in `run_graphql_field(...)` to run on `MySchema`
include GraphQL::Testing::Helpers.for(MySchema)

Then, you can run fields using Testing::Helpers#run_graphql_field:

ruby
post = Post.first
graphql_post_title = run_graphql_field("Post.title", post)
assert_equal "100 Great Ideas", graphql_post_title

run_graphql_field accepts two required arguments:

  • Field path, in Type.field format
  • Runtime object: some non-nil object to resolve the field on.

Additionally, it accepts some keyword arguments:

  • arguments:, GraphQL arguments to the field, in Ruby-style (underscore, symbol) or GraphQL-style (camel-case, string)
  • context:, the GraphQL context to use for this query

run_graphql_field performs several GraphQL-related steps:

  • Checks .visible? on the named Object Type, raising an error if it isn't visible
  • Wraps the given runtime object in the GraphQL Object Type
  • Checks .authorized? on the type, calling Schema.unauthorized_object if authorization fails
  • Prepares arguments for field resolution
  • Checks #visible? on the field, raising an error if the field isn't visible
  • Checks #authorized? on the field, calling Schema.unauthorized_field if it fails
  • Calls any field extensions
  • Runs Dataloader and/or GraphQL-Batch, as needed

Resolving fields on the same object

You can use Testing::Helpers#with_resolution_context to use the same type, runtime object, and GraphQL context for multiple field resolutions. For example:

ruby
# Assuming `include GraphQL::Testing::Helpers.for(MySchema)`
# was used above ...
with_resolution_context(type: "Post", object: example_post, context: { current_user: author }) do |rc|
  assert_equal "100 Great Ideas", rc.run_graphql_field("title")
  assert_equal true, rc.run_graphql_field("viewerIsAuthor")
  assert_equal 5, rc.run_graphql_field("commentsCount")
  # Optionally, pass `arguments:` for the field:
  assert_equal 9, rc.run_graphql_field("commentsCount", arguments: { include_unmoderated: true })
end

The method yields a resolution context (rc, above) which responds to run_graphql_field.


Guides Testing Integration Tests (`guides/testing/integration_tests.md`)

Integration Tests

Besides testing schema structure, you should also test your GraphQL system's behavior. There are really a few levels to this:

  • Application-level behaviors, like business logic, permissions, and persistence. These behaviors may be shared by your API and user interface.
  • Interface-level behaviors, like GraphQL fields, mutations, error scenarios, and HTTP-specific behaviors. These are unique to your GraphQL system.
  • Transport-level behaviors, like HTTP headers, parameters and status codes

Testing Application-Level Behaviors

When it comes to how your application behaves, you should lean on unit tests which exercise application primitives directly. For example, if postings require a title and a body, you should write a test for Post which asserts that invalid Posts fail to save. Several other components of the application may be tested this way:

  • Permissions: test your authorization system using example resources and actors. Dedicated, high-level frameworks like Pundit make it easy to test authorization in isolation.
  • Business logic: What are the operations that a user can perform in your system? For example, on a blog, they might be: drafting and publishing posts, moderating comments, filtering posts by category, or blocking users. Test these operations in isolation so you can be confident that the core code is correct.
  • Persistence (and other external services): how does your app interact with the "outside world", like databases, files, and third-party APIs? These interactions also deserve specific tests.

By testing these (and other) application-level behaviors without GraphQL, you can reduce the overhead of your test suite and simplify your testing scenarios.

Testing Interface-Level Behaviors

After building your application, you give it an interface so that people (or other software) can interact with it. Sometimes the interface is a website, other times it's a GraphQL API. The interface has transport-specific primitives that map to your application's primitives. For example, a React app might have components that correspond to Post, Comment, and a Moderation operation. (These components might even be context-specific, like ThreadComment or DraftPost.) Similarly, a GraphQL interface has types and fields that correspond to the underlying application primitives (like Post and Comment types, a Post.isDraft field, or a ModerateComment mutation).

The best way to test a GraphQL interface is with integration tests which run the whole GraphQL system (using MySchema.execute(...)). By using an integration test, you can be sure that all of GraphQL-Ruby's internal systems are engaged (validation, analysis, authorization, data loading, response type-checking, etc.).

A basic integration test might look like:

ruby
it "loads posts by ID" do
  query_string = <<-GRAPHQL
    query($id: ID!){
      node(id: $id) {
        ... on Post {
          title
          id
          isDraft
          comments(first: 5) {
            nodes {
              body
            }
          }
        }
      }
    }
  GRAPHQL

  post = create(:post_with_comments, title: "My Cool Thoughts")
  post_id = MySchema.id_from_object(post, Types::Post, {})
  result = MySchema.execute(query_string, variables: { id: post_id })

  post_result = result["data"]["node"]
  # Make sure the query worked
  assert_equal post_id, post_result["id"]
  assert_equal "My Cool Thoughts", post_result["title"]
end

To make sure that different parts of the system are properly engaged, you can add integration tests for specific scenarios, too. For example, you could add a test to make sure that data is hidden from some users:

ruby
it "doesn't show draft posts to anyone except their author" do
  author = create(:user)
  non_author = create(:non_user)
  draft_post = create(:post, draft: true, author: author)

  query_string = <<-GRAPHQL
  query($id: ID!) {
    node(id: $id) {
      ... on Post {
        isDraft
      }
    }
  }
  GRAPHQL

  post_id = MySchema.id_from_object(draft_post, Types::Post, {})

  # Authors can see their drafts:
  author_result = MySchema.execute(query_string, context: { viewer: author }, variables: { id: post_id })
  assert_equal true, author_result["data"]["node"]["isDraft"]

  # Other users can't see others' drafts
  non_author_result = MySchema.execute(query_string, context: { viewer: non_author }, variables: { id: post_id })
  assert_nil author_result["data"]["node"]
end

This test engages the underlying authorization and business logic, and provides a sanity check at the GraphQL interface layer.

Testing Transport-Level Behaviors

GraphQL is usually served over HTTP. You probably want tests that make sure that HTTP inputs are correctly prepared for GraphQL. For example, you might test that:

  • POST data is correctly turned into query variables
  • Authentication headers are used to load a context[:viewer]

In Rails, you might use a functional test for this, for example:

ruby
it "loads user token into the viewer" do
  query_string = "{ viewer { username } }"
  post graphql_path, params: { query: query_string }
  json_response = JSON.parse(@response.body)
  assert_nil json_response["data"]["viewer"], "Unauthenticated requests have no viewer"

  # This time, add some authentication to the HTTP request
  user = create(:user)
  post graphql_path,
    params: { query: query_string },
    headers: { "Authorization" => "Bearer #{user.auth_token}" }

  json_response = JSON.parse(@response.body)
  assert_equal user.username, json_response["data"]["viewer"]["username"], "Authenticated requests load the viewer"
end

Guides Testing Profiling (`guides/testing/profiling.md`)

Profiling

If you want to know more about how time is spent during GraphQL queries, including GraphQL-Ruby internals, you can use Ruby profiling tools to take a closer look.

If you want to investigate GraphQL-Ruby performance together, prepare a runtime profile and memory profile as described below and open an issue on GitHub, including those files.

StackProf

StackProf is a Ruby library for figuring out where an operation's time is spent. To capture a profile, surround a block with StackProf.run { ... }.

ruby
require "stackprof"

# Prepare any GraphQL-related data or context:
query_string = "{ someGraphQL ... }"
context = { ... }

# This will dump a profile in `tmp/graphql-prof.dump`
StackProf.run(mode: :wall, interval: 10, out: "tmp/graphql-prof.dump") do
  # Execute the query inside the block:
  MySchema.execute(query_string, context: context)
end

The out: option tells StackProf to create a "dump" at the given location. Then, anyone who has that file can investigate the profile using the stackprof command, for example:

text
$ stackprof tmp/graphql-prof.dump
==================================
  Mode: wall(1)
  Samples: 2492 (58.06% miss rate)
  GC: 0 (0.00%)
==================================
     TOTAL    (pct)     SAMPLES    (pct)     FRAME
       902  (36.2%)          94   (3.8%)     GraphQL::Execution::Interpreter::Runtime#evaluate_selection_with_resolved_keyword_args
      1283  (51.5%)          87   (3.5%)     GraphQL::Execution::Interpreter::Runtime#continue_field
       274  (11.0%)          78   (3.1%)     GraphQL::Schema::Field#resolve
      1068  (42.9%)          73   (2.9%)     GraphQL::Execution::Interpreter::Runtime#evaluate_selection
      # ...

Additionally, stackprof accepts a --method argument which provides details about the performance and usage of a specific method, for example:

text
$ stackprof tmp/small.dump --method #gather_selections
GraphQL::Execution::Interpreter::Runtime#gather_selections (/Users/rmosolgo/code/graphql-ruby/lib/graphql/execution/interpreter/runtime.rb:305)
  samples:    17 self (0.7%)  /     17 total (0.7%)
  callers:
      16  (   94.1%)  GraphQL::Execution::Interpreter::Runtime#continue_field
       6  (   35.3%)  Array#each
       1  (    5.9%)  GraphQL::Execution::Interpreter::Runtime#run_eager
  callees (0 total):
       6  (    Inf%)  Array#each
  code:
    1    (0.0%) /     1   (0.0%)  |   305  |                 when :lookahead
    6    (0.2%) /     6   (0.2%)  |   306  |                   if !field_ast_nodes
    3    (0.1%) /     3   (0.1%)  |   307  |                     field_ast_nodes = [ast_node]
                                  |   308  |                   end

Anyone with the .dump file can perform this analysis -- it's a really useful file! If you want to investigate GraphQL-Ruby performance together, please share a runtime profile.

MemoryProfiler

MemoryProfiler provides insight into where an operation interacts with system memory and the Ruby heap. This is helpful because memory usage problems cause code to run slowly; fixing them can make code run fast.

To produce a report, wrap a block in MemoryProfiler.report { ... } and then call .pretty_print on the result. For example, to create a report on a GraphQL query:

ruby
require 'memory_profiler'

# Prepare any GraphQL-related data or context:
query_string = "{ someGraphQL ... }"
context = { ... }

report = MemoryProfiler.report do
  # Execute the query inside the block:
  MySchema.execute(query_string, context: context)
end

# Write the result to a file
report.pretty_print(to_file: "tmp/graphql-memory.txt")

The report will include many interesting sections including:

  • Total memory and objects allocated
  • Objects allocated by location and by class
  • String allocations, including the number of times a string with the same value was allocated

All of these can indicate "hot spots" in the code and inform refactors to reduce memory use. In turn, this reduces time spent in Ruby GC.

If you want to investigate GraphQL-Ruby performance together, please share a memory profile.


Guides Testing Schema Structure (`guides/testing/schema_structure.md`)

Schema Structure

Structural changes to a GraphQL schema come in two categories:

  • Breaking changes may cause previously-valid queries to become invalid. For example, if you remove the title field, anyone who tries to query that field will get a validation error instead of response data.
  • Non-breaking changes add options to a schema without breaking previously-valid queries.

Making a breaking change can be bad news for your API clients, since their applications may break. But, sometimes they're required. Non-breaking changes don't affect existing queries, since they just add new parts to the schema.

Here are few tips for managing schema structure changes.

Maintain a `.graphql` schema dump

Make structure changes part of the normal code review process by adding a schema.graphql artifact to your project. This way, any changes to schema structure will show up clearly in a pull request as a diff to that file.

You can read about this approach in "Tracking Schema Changes with GraphQL-Ruby" or the built-in GraphQL::RakeTask for generating schema dumps.

Automatically check for breaking changes

You can use GraphQL::SchemaComparator to check for breaking changes during development or CI. If you maintain a dump of queries that typically run against your server, you may also utilize GraphQL::StaticValidation to validate these queries directly. A Rake task such as the one below can be used to identify changes that are incompatible with existing queries.

ruby
namespace :graphql do
  namespace :queries do
    desc 'Validates GraphQL queries against the current schema'
    task validate: [:environment] do
      queries_file = 'test/fixtures/files/queries.json'
      queries = Oj.load(File.read(queries_file))

      Validate.run_validate(queries, MySchema)
    end

    module Validate
      def self.run_validate(queries, schema)
        puts '⏳  Validating queries...'
        puts "\n"

        results = queries.map { |query| schema.validate(query) }
        errors = results.flatten

        if errors.empty?
          puts '✅  All queries are valid'
        else
          print_errors(errors)
        end
      end

      def self.print_errors(errors)
        puts 'Detected the following errors:'
        puts "\n"

        errors.each do |error|
          path = error.path.join(', ')
          puts "❌  #{path}: #{error.message}"
        end
      end
    end
  end
end

Guides Type Definitions Directives (`guides/type_definitions/directives.md`)

Directives

Directives are system-defined keywords with two kinds of uses:

  • runtime directives modify execution, so that when they are present, the GraphQL runtime does something different;
  • schema directives annotate schema definitions, indicating different configurations or metadata about schemas and types.

Runtime Directives

Runtime directives are server-defined keywords that modify GraphQL execution. All GraphQL systems have at least two directives, @skip and @include. For example:

ruby
query ProfileView($renderingDetailedProfile: Boolean!){
  viewer {
    handle
    # These fields will be included only if the check passes:
    ... @include(if: $renderingDetailedProfile) {
      location
      homepageUrl
    }
  }
}

Here's how the two built-in directives work:

Custom Runtime Directives

Custom directives extend GraphQL::Schema::Directive:

ruby
# app/graphql/directives/my_directive.rb
class Directives::MyDirective < GraphQL::Schema::Directive
  description "A nice runtime customization"
  location FIELD
end

Then, they're hooked up to the schema using directive(...):

ruby
class MySchema < GraphQL::Schema
  # Attach the custom directive to the schema
  directive(Directives::MyDirective)
end

And you can reference them in the query with @myDirective(...):

ruby
query {
  field @myDirective {
    id
  }
}

GraphQL::Schema::Directive::Feature and GraphQL::Schema::Directive::Transform are included in the library as examples.

Runtime hooks

Directive classes may implement the following class methods to interact with the runtime:

  • def self.include?(obj, args, ctx): If this hook returns false, the nodes flagged by this directive will be skipped at runtime.
  • def self.resolve(obj, args, ctx): Wraps the resolution of flagged nodes. Resolution is passed as a block, so yield will continue resolution.

Looking for a runtime hook that isn't listed here? open an issue to start the conversation!

Schema Directives

Schema directives are used in GraphQL's interface definition language (IDL). For example, @deprecated is built in to GraphQL-Ruby:

ruby
type User {
  firstName @deprecated(reason: "Use `name` instead")
  lastName @deprecated(reason: "Use `name` instead")
  name
}

In the schema definition, directives express metadata about types, fields, and arguments.

Custom Schema Directives

To make a custom schema directive, extend GraphQL::Schema::Directive:

ruby
# app/graphql/directives/permission.rb
class Directives::Permission < GraphQL::Schema::Directive
  argument :level, String
  locations FIELD_DEFINITION, OBJECT
end

Then, attach it to parts of your schema with directive(...):

ruby
class Types::JobPosting < Types::BaseObject
  directive Directives::Permission, level: "manager"
end

Arguments and fields also accept a directives: keyword:

ruby
field :salary, Integer, null: false,
  directives: { Directives::Permission => { level: "manager" } }

After that:

  • the configured object's .directives method will return an array containing an instance of the specified directive
  • IDL dumps (from Schema.to_definition) will include the configured directives

Similarly, Schema.from_definition parses directives from IDL strings.

For a couple of built-in examples, check out:

Custom Name

By default, the directive's name is taken from the class name. You can override this with graphql_name, for example:

ruby
class Directives::IsPrivate < GraphQL::Schema::Directive
  graphql_name "someOtherName"
end

Arguments

Like fields, directives may have arguments :

ruby
argument :if, Boolean,
  description: "Skips the selection if this condition is true"

Guides Type Definitions Enums (`guides/type_definitions/enums.md`)

Enums

Enum types are sets of discrete values. An enum field must return one of the possible values of the enum. In the GraphQL Schema Definition Language (SDL), enums are described like this:

ruby
enum MediaCategory {
  AUDIO
  IMAGE
  TEXT
  VIDEO
}

So, a MediaCategory value is one of: AUDIO, IMAGE, TEXT, or VIDEO. This is similar to ActiveRecord enums.

In a GraphQL query, enums are written as identifiers (not strings), for example:

ruby
search(term: "puppies", mediaType: IMAGE) { ... }

(Notice that IMAGE doesn't have quotes.)

But, when GraphQL responses or variables are transported using JSON, enum values are expressed as strings, for example:

ruby
# in a graphql controller:
params["variables"]
# { "mediaType" => "IMAGE" }

Defining Enum Types

In your application, enums extend GraphQL::Schema::Enum and define values with the value(...) method:

ruby
# First, a base class
# app/graphql/types/base_enum.rb
class Types::BaseEnum < GraphQL::Schema::Enum
end

# app/graphql/types/media_category.rb
class Types::MediaCategory < Types::BaseEnum
  value "AUDIO", "An audio file, such as music or spoken word"
  value "IMAGE", "A still image, such as a photo or graphic"
  value "TEXT", "Written words"
  value "VIDEO", "Motion picture, may have audio"
end

Each value may have:

  • A description (as the second argument or description: keyword)
  • A comment (as a comment: keyword)
  • A deprecation reason (as deprecation_reason:), marking this value as deprecated
  • A corresponding Ruby value (as value:), see below

By default, Ruby strings correspond to GraphQL enum values. But, you can provide value: options to specify a different mapping. For example, if you use symbols instead of strings, you can say:

ruby
value "AUDIO", value: :audio

Then, GraphQL inputs of AUDIO will be converted to :audio and Ruby values of :audio will be converted to "AUDIO" in GraphQL responses.

Enum classes are never instantiated and their methods are never called.

You can get the GraphQL name of the enum value using the method matching its downcased name:

ruby
Types::MediaCategory.audio # => "AUDIO"

You can pass a value_method: to override the value of the generated method:

ruby
value "AUDIO", value: :audio, value_method: :lo_fi_audio

# ...

Types::MediaCategory.lo_fi_audio # => "AUDIO"

Also, you can completely skip the method generation by setting value_method to false

ruby
value "AUDIO", value: :audio, value_method: false

Guides Type Definitions Extensions (`guides/type_definitions/extensions.md`)

Extending the GraphQL-Ruby Type Definition System

While integrating GraphQL into your app, you can customize the definition DSL. For example, you might:

  • Assign "area of responsibility" to different types and fields
  • DRY up shared logic between types and fields
  • Attach metadata for use during authorization

This guide describes various options for extending the class-based definition API. Keep in mind that these approaches may change as the API matures. If you're having trouble, consider opening an issue on GitHub to get help.

Note: This document describes best practice with GraphQL-Ruby 1.10+. For customizing schemas on older versions, use GitHub to browse older versions of this page.

Customization Overview

In general, the schema definition process goes like this:

  • The application defines lots of classes for the GraphQL types
  • Starting from root types (query, mutation, and subscription) and any defined orphan_types, the schema discovers all types, fields, arguments, enum values, and directives in the schema
  • Non-type objects (fields, arguments, enum values) are initialized when they're attached to the classes or instances they belong to.

Customizing type definitions

In your custom classes, you can add class-level instance variables that hold configuration. For example:

ruby
class Types::BaseObject < GraphQL::Schema::Object
  # Call this method in an Object class to get or set the permission level:
  def self.required_permission(permission_level = nil)
    if permission_level.nil?
      # return the configured value
      @required_permission
    else
      @required_permission = permission_level
    end
  end
end

# Then, in concrete classes
class Dossier < BaseObject
  # The Dossier object type will have `.metadata[:required_permission] # => :admin`
  required_permission :admin
end

# Now, the type responds to that method:
Dossier.required_permission
# => :admin

Now, any runtime code which calls type.required_permission will get the configured value.

Customizing fields

Fields are generated in a different way. Instead of using classes, they are generated with instances of GraphQL::Schema::Field (or a subclass). In short, the definition process works like this:

ruby
# This is what happens under the hood, roughly:
# In an object class:
field :name, String, null: false
# ...
# Leads to:
field_config = GraphQL::Schema::Field.new(name: :name, type: String, null: false)

So, you can customize this process by:

  • creating a custom class which extends GraphQL::Schema::Field
  • overriding #initialize on that class (instance methods)
  • registering that class as the field_class on Objects and Interfaces which should use the customized field.

For example, you can create a custom class which accepts a new parameter to initialize:

ruby
class Types::BaseField < GraphQL::Schema::Field
  # Override #initialize to take a new argument:
  def initialize(*args, required_permission: nil, **kwargs, &block)
    @required_permission = required_permission
    # Pass on the default args:
    super(*args, **kwargs, &block)
  end

  attr_reader :required_permission
end

Then, pass the field class as field_class(...) wherever it should be used:

ruby
class Types::BaseObject < GraphQL::Schema::Object
  # Use this class for defining fields
  field_class BaseField
end

# And....
class Types::BaseInterface < GraphQL::Schema::Interface
  field_class BaseField
end

class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  field_class BaseField
end

Now, BaseField.new(*args, &block) will be used to create GraphQL::Schema::Fields on those types. At runtime field.required_permission will return the configured value.

Customizing Connections

Connections may be customized in a similar way to Fields.

  • Create a new class extending 'GraphQL::Types::Relay::BaseConnection'
  • Assign it to your object/interface type with connection_type_class(MyCustomConnection)

For example, you can create a custom connection:

ruby
class Types::MyCustomConnection < GraphQL::Types::Relay::BaseConnection
  # BaseConnection has these nullable configurations
  # and the nodes field by default, but you can change
  # these options if you want
  edges_nullable(true)
  edge_nullable(true)
  node_nullable(true)
  has_nodes_field(true)

  field :total_count, Integer, null: false

  def total_count
    object.items.size
  end
end

Then, pass the field class as connection_type_class(...) wherever it should be used:

ruby
module Types
  class Types::BaseObject < GraphQL::Schema::Object
    # Use this class for defining connections
    connection_type_class MyCustomConnection
  end
end

Now, all type classes that extend BaseObject will have a connection_type with the additional field totalCount.

Customizing Edges

Edges may be customized in a similar way to Connections.

  • Create a new class extending 'GraphQL::Types::Relay::BaseEdge'
  • Assign it to your object/interface type with edge_type_class(MyCustomEdge)

Customizing Arguments

Arguments may be customized in a similar way to Fields.

  • Create a new class extending GraphQL::Schema::Argument
  • Use argument_class(MyArgClass) to assign it to your base field class, base resolver class, and base mutation class

Then, in your custom argument class, you can use #initialize(name, type, desc = nil, **kwargs) to take input from the DSL.

Customizing Enum Values

Enum values may be customized in a similar way to Fields.

  • Create a new class extending GraphQL::Schema::EnumValue
  • Assign it to your base Enum class with enum_value_class(MyEnumValueClass)

Then, in your custom enum class, you can use #initialize(name, desc = nil, **kwargs) to take input from the DSL.


Guides Type Definitions Field Extensions (`guides/type_definitions/field_extensions.md`)

Field Extensions

GraphQL::Schema::FieldExtension provides a way to modify user-defined fields in a programmatic way. For example, Relay connections are implemented as a field extension (GraphQL::Schema::Field::ConnectionExtension).

Making a new extension

Field extensions are subclasses of GraphQL::Schema::FieldExtension:

ruby
class MyExtension < GraphQL::Schema::FieldExtension
end

Using an extension

Defined extensions can be added to fields using the extensions: [...] option or the extension(...) method:

ruby
field :name, String, null: false, extensions: [UpcaseExtension]
# or:
field :description, String, null: false do
  extension(UpcaseExtension)
end

See below for how extensions may modify fields.

Modifying field configuration

When extensions are attached, they are initialized with a field: and options:. Then, #apply is called, when they may extend the field they're attached to. For example:

ruby
class SearchableExtension < GraphQL::Schema::FieldExtension
  def apply
    # add an argument to this field:
    field.argument(:query, String, required: false, description: "A search query")
  end
end

This way, an extension can encapsulate a behavior requiring several configuration options.

Adding default argument configurations

Extensions may provide default argument configurations which are applied if the field doesn't define the argument for itself. The configuration is passed to Schema::FieldExtension.default_argument. For example, to define a :query argument if the field doesn't already have one:

ruby
class SearchableExtension < GraphQL::Schema::FieldExtension
  # Any field which uses this extension and _doesn't_ define
  # its own `:query` argument will get an argument configured with this:
  default_argument(:query, String, required: false, description: "A search query")
end

Additionally, extensions may implement def after_define which is called after the field's do .. . end block. This is helpful when an extension should provide default configurations without overriding anything in the field definition. (When extensions are added by calling field.extension(...) on an already-defined field def after_define is called immediately.)

Modifying field execution

Extensions have two hooks that wrap field resolution. Since GraphQL-Ruby supports deferred execution, these hooks might not be called back-to-back.

First, GraphQL::Schema::FieldExtension#resolve is called. resolve should yield(object, arguments) to continue execution. If it doesn't yield, then the underlying field won't be called. Whatever #resolve returns will be used for continuing execution.

After resolution and after syncing lazy values (like Promises from graphql-batch), [GraphQL::Schema::FieldExtension#after_resolve](rdoc-r