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.
# Gemfile
gem 'graphql'$ bundle installGetting Started
$ rails generate graphql:installAfter 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:
- Persisted queries
- API versioning
- Streaming payloads
- Server-side caching
- Rate limiters
- Subscriptions backends for Pusher and Ably
- Authorization plugins for Pundit and CanCan
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
- Install the dependencies with
- Stand-alone:
- Install dependencies
yarn install - Run the tests
yarn run test
- Install dependencies
- 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:
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
endHowever, 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:
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
endSo, 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
Userin the database fromsession[: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:
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
endAfter 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:
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
endThe 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:
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
endThen, use this application method in your GraphQL code:
field :posts, [Types::Post], null: false
def posts
# Fetch the posts this user can see:
Post.posts_for(context[:current_user])
endIn 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:
class Types::RecipeTag < Types::BaseEnum
value "LOW_FAT"
value "LOW_CARB"
+ value "VEGAN"
+ value "KETO"
+ value "GRAPEFRUIT_DIET"
endHowever, if you want to change the schema in ways that would break previous queries, you can do that with a Changeset:
class Types::RecipeTag < Types::BaseEnum
# Turns out this makes you sick:
value "GRAPEFRUIT_DIET", removed_in: Changesets::RemoveLegacyDiets
endThen, 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::Dataloaderinitiates 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::Dataloadertakes the first paused Fiber and resumes it, causing theGraphQL::Dataloader::Sourceto 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:
class MySchema < GraphQL::Schema
# ...
use GraphQL::Dataloader
endThen, inside your schema, you can request batch-loaded objects by their lookup key with dataloader.with(...).load(...):
field :user, Types::User do
argument :handle, String
end
def user(handle:)
dataloader.with(Sources::UserByHandle).load(handle)
endOr, load several objects by passing an array of lookup keys to .load_all(...):
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)
endTo prepare requests from several sources, use .request(...), then call .load after all requests are registered:
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:
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
endThen, any arguments with loads: will use that method to fetch objects. For example:
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
endData 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:
AsyncDataloaderuses theasyncgem to automatically background I/O fromDataloader::Source#fetchcalls. Read More- You can manually call
dataloader.yieldafter 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:
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
@deferadds some overhead to the response, so only apply it judiciously.@deferis 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":
{
"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:
yarn add graphql-ruby-client
# Or:
npm install graphql-ruby-clientThe source code is in the graphql-ruby repository.
See detailed guides for more info about its features:
- sync CLI for use with graphql-pro's persisted query backend
- Subscription support:
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:
- Configure Redis for the limiters' backend
- Active Operation Limiter
- Runtime Limiter
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:
- Prepare the schema
- Set up a Redis backend or Memcached backend
- Configure types and fields for caching
- Check out the runtime considerations
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:
- Description of persisted queries
- The rationale behind them
- How
OperationStoreworks, in brief
In other guides, you can read more about:
- Getting Started installing
OperationStorein your app - Workflow and usage for client apps
- Authentication for the sync API
- Server Management after your system is running
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:
# 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:
# 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:
# 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
# ...
endEfficiency
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:
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
Navigation order migrated from the Jekyll `index` frontmatter.
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 fieldargs/arg_value: The arguments for a field, or the value of an argumentcontext: the query context, based on the hash passed ascontext:
Object Authorization
When you implement this method to return false, the query will be halted, for example:
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:
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
endFor 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:
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
endFor 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:
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
endNow, 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:
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
endGuides 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:
# 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:
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:
$ rails generate graphql:cancan:installThis 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:
# 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)
endNow, 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:
class Types::Query < Types::BaseObject
# Allow anyone to see the query root
can_can_action nil
endBypassing 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:
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
endArray
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:
# Allow anyone to browse the job postings
field :job_postings, [Types::JobPosting], null: false,
scope: falseAuthorizing Fields
You can also require certain checks on a field-by-field basis. First, include the integration in your base field class:
# 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
endIf you haven't already done so, you should also hook up your base field class to your base object and base interface:
# 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
endThen, you can add can_can_action: options to your fields:
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
endIt 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:
# This will call `.can?(:read, user, :email_address)`
field :email_address, String,
can_can_action: :read,
can_can_attribute: :email_addressYou could also provide a default value for can_can_attribute in your base field class:
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:
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: :allThe 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:
class Types::BaseArgument < GraphQL::Schema::Argument
# Include the integration and default to no permissions required
include GraphQL::Pro::CanCanIntegration::ArgumentIntegration
can_can_action nil
endThen, make sure your base argument is hooked up to your base field and base input object:
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
endNow, arguments accept a can_can_action: option, for example:
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
endThis 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:
- Add a mutation-level roles
- Run checks on objects loaded by ID
Also, you can configure unauthorized object handling
Setup
Add MutationIntegration to your base mutation, for example:
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
include GraphQL::Pro::CanCanIntegration::MutationIntegration
# Also, to use argument-level authorization:
argument_class Types::BaseArgument
endAlso, you'll probably want a BaseMutationPayload where you can set a default role:
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
endAnd hook it up to your base mutation:
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
object_class Types::BaseMutationPayload
field_class Types::BaseField
endMutation-level roles
Each mutation can have a class-level can_can_action which will be checked before loading objects or resolving, for example:
class Mutations::PromoteEmployee < Mutations::BaseMutation
can_can_action :run_mutation
endIn 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:
class Mutations::FireEmployee < Mutations::BaseMutation
argument :employee_id, ID,
loads: Types::Employee,
can_can_action: :supervise,
endIn 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:
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
def unauthorized_by_can_can(owner, value)
# No error, just return nil:
nil
end
endThe method is called with:
owner: theGraphQL::Schema::Argumentinstance or mutation class whose role was not satisfiedvalue: the object which didn't pass forcontext[: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:
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
endAuthorizing Resolvers
Resolvers are authorized just like mutations, and require similar setup:
# 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
endBeyond 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:
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
endGuides 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:
# 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:
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:
$ rails generate graphql:pundit:installThis 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:
# 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
endNow, 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:
class Types::Query < Types::BaseObject
# Allow anyone to see the query root
pundit_role nil
endPolicies 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:
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
# ...
endThat 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(...):
class Types::Employee < Types::BaseObject
pundit_policy_class(Policies::CustomEmployeePolicy)
# Or, you could use a string:
# pundit_policy_class("Policies::CustomEmployeePolicy")
endFor 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:
class Types::PublicProfile < Types::BaseObject
# Anyone can see this
pundit_role nil
endHandling 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:
class BaseUnion < GraphQL::Schema::Union
include GraphQL::Pro::PunditIntegration::UnionIntegration
end
module BaseInterface
include GraphQL::Schema::Interface
include GraphQL::Pro::PunditIntegration::InterfaceIntegration
endBypassing scopes
To allow an unscoped relation to be returned from a field, disable scoping with scope: false, for example:
# Allow anyone to browse the job postings
field :job_postings, [Types::JobPosting], null: false,
scope: falseAuthorizing Fields
You can also require certain checks on a field-by-field basis. First, include the integration in your base field class:
# 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
endIf you haven't already done so, you should also hook up your base field class to your base object and base interface:
# 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
endThen, you can add pundit_role: options to your fields:
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
endIt 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:
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"
endThis 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:
class Types::BaseArgument < GraphQL::Schema::Argument
# Include the integration and default to no permissions required
include GraphQL::Pro::PunditIntegration::ArgumentIntegration
pundit_role nil
endThen, make sure your base argument is hooked up to your base field and base input object:
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
endNow, arguments accept a pundit_role: option, for example:
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
endThe 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:
- Add a mutation-level roles
- Run checks on objects loaded by ID
Also, you can configure unauthorized object handling
Setup
Add MutationIntegration to your base mutation, for example:
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
include GraphQL::Pro::PunditIntegration::MutationIntegration
# Also, to use argument-level authorization:
argument_class Types::BaseArgument
endAlso, you'll probably want a BaseMutationPayload where you can set a default role:
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
endAnd hook it up to your base mutation:
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
object_class Types::BaseMutationPayload
field_class Types::BaseField
endMutation-level roles
Each mutation can have a class-level pundit_role which will be checked before loading objects or resolving, for example:
class Mutations::PromoteEmployee < Mutations::BaseMutation
pundit_role :admin
endIn 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:
class Mutations::PromoteEmployee < Mutations::BaseMutation
pundit_policy_class ::UserPolicy
pundit_role :admin
endNow, 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:
class Mutations::FireEmployee < Mutations::BaseMutation
argument :employee_id, ID,
loads: Types::Employee,
pundit_role: :supervisor,
endIn 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:
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
def unauthorized_by_pundit(owner, value)
# No error, just return nil:
nil
end
endThe method is called with:
owner: theGraphQL::Schema::Argumentor mutation class whose role was not satisfiedvalue: the object which didn't pass forcontext[: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:
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
endAuthorizing Resolvers
Resolvers are authorized just like mutations, and require similar setup:
# 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
endBeyond 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 instancePundit.policy_scope!(context[:current_user], items)is called to filteritems
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), ornilto bypass authorizationscope_by_pundit_policy(context, items)to apply a scope toitems(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:
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
endOne 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:
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
def policy_class_for(_object, _context)
# Look up a nested `Policy` constant:
self.class.const_get(:Policy)
end
endThen, each mutation can define its policy inline, for example:
class Mutations::PromoteEmployee < Mutations::BaseMutation
# This will be found by `BaseMutation.policy_class`, defined above:
class Policy
# ...
end
pundit_role :admin
endNow, 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:
# app/graphql/query_context.rb
class QueryContext < GraphQL::Query::Context
def pundit_user
# Lookup `context[:viewer]` instead:
self[:viewer]
end
endThen be sure to hook up your custom class in the schema:
class MySchema < GraphQL::Schema
context_class(QueryContext)
endThen, 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:
field :products, [Types::Product], scope: true
# Or
field :all_products, [Types::Product], scope: falseFor 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,
field :products, [Types::Product] # has `scope: true` by defaultWill call:
class Types::Product < Types::BaseObject
def self.scope_items(items, context)
# filter items here
end
endThe 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:
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)
endGuides 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:
class MySchema < GraphQL::Schema
# ...
use GraphQL::Schema::Visibility # see below for options
endThen, 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:
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:
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, egquery { findSecretFeature { ... } } - Fragments on
SecretFeature, egFragment SF on SecretFeature
And in introspection:
__schema { types { ... } }will not includeSecretFeature__type(name: "SecretFeature")will returnnil- Any interfaces or unions which normally include
SecretFeaturewill not include it - Any fields that return
SecretFeaturewill be excluded from introspection
Field Visibility
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
endFor this to work, the base field class must be configured with other GraphQL types.
Argument Visibility
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
endFor 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:
class MySchema < GraphQL::Schema
# ...
# Opt out of GraphQL-Ruby's visibility feature:
use GraphQL::Schema::AlwaysVisible
endFor 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):
Visibilityspeeds 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.Visibilitysupports predefined, reusable visibility profiles which speeds up queries using complicatedvisible?checks.Visibilityhides types differently in a few edge cases:- Previously,
Wardenhid interface and union types which had no possible types.Visibilitydoesn't check possible types (in order to support performance improvements), so those types must returnfalseforvisible?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).Wardenused a "global" map of types so it could discover object types in this case, butVisibilitydoesn't have that global map. (Since time of writing,Visibilitydoes have some global type tracking, so maybe this could be fixed.)
- Previously,
- When
Visibilityis 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
.rbfiles), 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] = trueis set in the main query context.context[:visibility_migration_warden_running] = trueis set in the duplicate context which is passed to aWardeninstance.- 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:
# app/graphql/changesets/deprecate_recipe_flag.rb
class Changesets::DeprecateRecipeTags < GraphQL::Enterprise::Changeset
release "2020-12-01"
endAdditionally, 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:
field :search_recipes, [Types::Recipe] do
argument :query, String
argument :tags, [Types::RecipeTag], required: false, added_in: Changesets::AddRecipeTags
endYou 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:
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::FloatingPointRatingsHere, 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:
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
endIf something has been defined several times, a removed_in: configuration removes all definitions:
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
endExamples
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.
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
endTo remove a field, add a removed_in: ... configuration to the last definition of the field:
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
endWhen 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:
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
endTo remove an argument entirely, add a removed_in: ... configuration to the last definition. It will remove all implementations for that argument. For example:
class Mutations::SubmitRating < Mutations::BaseMutation
# Remove this because it's irrelevant:
argument :phone_number, String, removed_in: Changesets::StopCollectingPersonalInformation
endWhen 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:
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
endValues can be removed with removed_in:, for example:
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
endWhen 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:
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::ReleaseCuisinesTo remove a member from a union, move it to a possible_types call with removed_in: ...:
# Stop including this in the union in new API versions:
possible_types Types::Chef, removed_in: Changeset::LessChefHypeWhen 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:
class Types::Recipe < Types::BaseObject
# Add this new implementation in new API versions only:
implements Types::RssSubject, added_in: Changesets::AddRssSupport
endTo remove one or more more interface implementations, add removed_in: to the implements ... configuration, for example:
implements Types::RssSubject,
added_in: Changesets::AddRssSupport,
# Sadly, nobody seems to want to use this,
# so we removed it all:
removed_in: Changesets::RemoveRssSupportWhen 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:
# 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# 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
# ...
endThen, add or update fields or arguments to use the new type instead of the old one. For example:
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
endWith 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:
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
endThat 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:
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
endBesides 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
ArgumentIntegrationto your base argument:ruby# app/graphql/types/base_argument.rb class Types::BaseArgument < GraphQL::Schema::Argument include GraphQL::Enterprise::Changeset::ArgumentIntegration endAlso, make sure that your
BaseField,BaseInputObject,BaseResolver, andBaseMutationhaveargument_class(Types::BaseArgument)configured in them.To support versioning fields, add the
FieldIntegrationto 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) endAlso, make sure that your
BaseObject,BaseInterface, andBaseMutationhavefield_class(Types::BaseField)configured in them.To support versioning enum values, add the
EnumValueIntegrationto your base enum value:ruby# app/graphql/types/base_enum_value.rb class Types::BaseEnumValue < GraphQL::Schema::EnumValue include GraphQL::Enterprise::Changeset::EnumValueIntegration endAlso, make sure that your
BaseEnumhasenum_value_class(Types::BaseEnumValue)configured in it.To support versioning union memberships and interface implementations, add the
TypeMembershipIntegrationto your base type membership:ruby# app/graphql/types/base_type_membership.rb class Types::BaseTypeMembership < GraphQL::Schema::TypeMembership include GraphQL::Enterprise::Changeset::TypeMembershipIntegration endAlso, make sure that your
BaseUnionandBaseInterfacehavetype_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:
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
endIn 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: "...":
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(...)
endThis 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::Releasebefore hooking up your rootquery(...),mutation(...), andsubscription(...)types. Otherwise, the schema may not find links to types in new schema versions.
Alternatively, Changesets can be explicitly attached using changesets: [...], for example:
class MyAppSchema < GraphQL::Schema
use GraphQL::Enterprise::Changeset::Release, changesets: [
Changesets::DeprecateRecipeFlag,
Changesets::RemoveRecipeFlag,
]
# ...
endOnly 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:
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_sdlTo 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:
MySchema.changesets
# #<Set: {AddNewFeature, RemoveOldFeature}>Additionally, each changeset has a .changes method describing its modifications:
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 (:additionwhen something new is added,:removalwhen 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 frompromise.rb; GraphQL::Dataloader uses Ruby'sFiberAPI. 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::Dataloaderoutside 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:rubyrecord_promise = Loaders::Record.load(1)Then, under the hood, GraphQL-Ruby manages the promise (using its
lazy_resolvefeature, upstreamed from GraphQL-Batch many years ago). GraphQL-Ruby will call.syncon it when no further execution is possible;promise.rbimplementsPromise#syncto execute the pending work.With GraphQL::Dataloader, you get a source, then call
.loadon it, which may pause the current Fiber, but it returns the requested object.rubydataloader.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:rubyLoaders::Record.load(1).then do |record| Loaders::OtherRecord.load(record.other_record_id) endThat call returns a
Promise, which is stored by GraphQL-Ruby, and finally.synced.With GraphQL-Dataloader,
.load(...)returns the requested object (after a potentialFiberpause), so no other method calls are necessary:rubyrecord = 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:rubypromise_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) endIf the objects are loaded from the same loader, then
.load_manyalso works:rubyLoaders::Record.load_many([1, 2]).then do |record, other_record| do_something(record, other_record) endWith 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_allwill return the objects directly:rubyrecord, 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:
- use GraphQL::Dataloader
+ use GraphQL::Dataloader::AsyncDataloaderAlso, add the async gem to your project, for example:
bundle add asyncNow, 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:
class Application < Rails::Application
# ...
config.active_support.isolation_level = :fiber
endActiveRecord 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_connectioncalls to improve this. - With
isolation_level = :fiber, new Fibers don't inheritconnected_to ...settings from their parent fibers.
Altogether, it can be improved like this:
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
endModify 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:
- GraphQL::Query::Context#dataloader (
context.dataloader, anywhere that query context is available) - GraphQL::Schema::Object#dataloader (
dataloaderinside a resolver method) - GraphQL::Schema::Resolver#dataloader (
dataloaderinsidedef resolveof a Resolver, Mutation, or Subscription class.)
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:
class MyDataloader < GraphQL::Dataloader # or GraphQL::Dataloader::AsyncDataloader
# ...
endThen, use your customized dataloader instead of the built-in one:
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:
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
endAlternatively, 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:
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
endYou could call that source from a GraphQL field method:
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)
endExample: Rails async calculations
In a Dataloader source, you can run Rails async calculations in the background while other work continues. For example:
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
endWhich could be used in a GraphQL field:
field :direct_reports_count, Int
def direct_reports_count
dataloader.with(Sources::DirectReportsCount).load(object)
endExample: Concurrent::Future
You could use concurrent-ruby to put work in a background thread. For example, using Concurrent::Future:
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
endGuides 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 (ornil) for each ofkeys, in the same order askeys.Under the hood, each Source instance maintains a
key => objectcache.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 todef initialize(...)and store them in instance variables.(
dataloader.with(source_class, *batch_parameters)returns an instance ofsource_classwith the given batch parameters -- but it might be an instance which was cached bydataloader.)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:
# 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
endThis loader could be used in GraphQL like this:
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:
# 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
endThis source could be used for any model_class, for example:
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:
# 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
endIt could be used like this:
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:
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
endSee 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:
# 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:
# 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
endIn 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:
def result_key_for(record)
record.object_id # even if the records are equivalent, handle each distinct Ruby object separately
endGuides 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:
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
endYou 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:
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
endYou can test it like so:
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"
endGuides 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:
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
graphqlandgraphql-progems - Add
@deferto your GraphQL schema - Update your HTTP handlers (eg, Rails controllers) to send streaming responses
- Optionally, customize
@deferto 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:
gem "graphql", "~>1.9"
gem "graphql-pro", "~>1.10"And then install them:
$ bundle update graphql graphql-proAdding `@defer` to your schema
Then, add GraphQL::Pro::Defer to your schema as a plugin:
class MySchema < GraphQL::Schema
use GraphQL::Pro::Defer
endThis 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:
- Rails has ActionController::Live
- Sinatra has Sinatra::Streaming
- Hanami::Controller can stream responses
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]:
if context[:defer]
# some fields were `@defer`ed
else
# normal GraphQL, no `@defer`
endWorking with deferrals
To handle deferrals, you can enumerate over context[:defer], for example:
context[:defer].each do |deferral|
# do something with the `deferral`, eg
# stream_to_client(deferral.to_h)
endThe 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_hreturns a hash withpath:,data:, and/orerrors:. (There is nopath:for the root result.).to_http_multipart(incremental: true)returns a string which works with Apollo client's@defersupport. (Useincremental: trueto format patches for the forthcoming spec.).pathreturns the path to this patch in the response.datareturns successfully-resolved results of the patch.errorsreturns 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.
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
endYou 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:
# 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
endAnd update your schema to use your custom defer implementation:
# Use our GraphQL-Batch-compatible defer:
use Directives::DeferNext 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:
class MySchema < GraphQL::Schema
# ...
use GraphQL::Pro::Stream
endAdditionally, 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:
{
# 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: whenfalse, 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 thaninitialCount, 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:
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 your development environment
- Run the tests to verify your setup
- Debug with pry
- Run the benchmarks to test performance in your environment
- Coding guidelines for working on your contribution
- Special tools for building the lexer and parser
- Building and publishing the GraphQL Ruby website
- Versioning describes how changes are managed and released
- Releasing Gem versions
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 installrake 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
bundle exec rake # tests & Rubocop
bundle exec rake test # tests onlyYou can run a specific file with TEST=:
bundle exec rake test TEST=spec/graphql/query_spec.rb
# run tests in `query_spec.rb` onlyYou can focus on a specific example with focus:
focus
it "does something cool" do
# ...
endThen, only focused tests will run:
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:
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.rbGraphQL-CParser tests
To test the graphql_cparser gem, you have to build the binary first:
bundle exec rake build_extThen, run the test suite with GRAPHQL_CPARSER=1:
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:
bundle exec rake test:systemAnd JavaScript tests:
bundle exec rake test:jsGemfiles, Gemfiles, Gemfiles
graphql-ruby has several gemfiles to ensure support for various Rails versions. You can specify a gemfile with BUNDLE_GEMFILE, eg:
BUNDLE_GEMFILE=gemfiles/rails_5.gemfile bundle exec rake testDebugging with Pry
pry is included with GraphQL-Ruby's development setup to help with debugging.
To pause execution in Ruby code, add:
binding.pryThen, 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:
$ 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 queriesYou can save results by sending the output into a file:
$ bundle exec rake bench:validate > before.txt
$ cat before.txt
# ...
# --> benchmark output hereIf 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:
BUNDLE_WITH=docs bundle install
bundle exec rake docs:build
bundle exec rake docs:rdoc:serveThen visit http://127.0.0.1:8808. Run the documentation checks before submitting a change:
bundle exec rake docs:check
bundle exec rake docs:build_twiceSee 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:
$ 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 PATCHversion indicates bug fixes or small features for specific use cases. Ideally, you can upgrade patch versions with only a quick skim of the changelog.MINORversion 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.MAJORversion 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.mdfor 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 Changesand include migration notes if possible - Include the PR number beside the change description for future reference
- If a change affects the default behavior of GraphQL-Ruby in a disruptive way, add it to
- Update
lib/graphql/version.rbwith 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:
BUNDLE_WITH=docs bundle install
bundle exec rake docs:buildThe 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:
bundle exec rake docs:check
bundle exec rake docs:build_twiceUse 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:
```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
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
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
endGuides Development Getting Started (`guides/development/getting_started.md`)
Getting Started
Installation
You can install graphql from RubyGems by adding to your application's Gemfile:
# Gemfile
gem "graphql"Then, running bundle install:
$ bundle installGetting Started
On Rails, you can get started with a few GraphQL generators:
# 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.
# 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
endBuild a Schema
Before building a schema, you have to define an entry point to your system, the "query root":
class QueryType < GraphQL::Schema::Object
description "The query root of this schema"
field :post, resolver: Resolvers::PostResolver
endDefine how this field is resolved by creating a resolver class:
# 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
endThen, build a schema with QueryType as the query entry point:
class Schema < GraphQL::Schema
query Types::QueryType
endThis 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:
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:
- A JSON dump of the schema, which you can get by sending
GraphQL::Introspection::INTROSPECTION_QUERY - Relay-specific helpers for GraphQL, see the Connection guide, Mutation guide, and Object Identification guide.
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.
Guides Development Related Projects (`guides/development/related_projects.md`)
Related Projects
Want to add something? Please open a pull request on GitHub!
Code
graphql-ruby+ Rails demo (src / heroku)graphql-ruby+ Sinatra demo (src / heroku)graphql-batch, a batched query execution strategygraphql-cache, a resolver-level caching solutiongraphql-devise, a gql interface to handle authentication with Devisegraphql-docs, a tool to automatically generate static HTML documentation from your GraphQL implementationgraphql-metrics, a plugin to extract fine-grain metrics of GraphQL queries received by your servergraphql-stitching, tools to combine multiple local and remote schemas into a single graph that queries as onegraphql-groups, a DSL to define group- and aggregation queries with graphql-ruby- Rails Helpers:
graphql-activerecordgraphql-rails-resolvegraphql-query-resolver, a graphql-ruby add-on to minimize N+1 queries.graphql-rails_logger, a logger which allows you to inspect GraphQL queries in a more readable format.apollo_upload_server-ruby, a middleware which allows you to upload files with GraphQL and multipart/form-data usingapollo-upload-clientlibrary on front-end.graphql-sourcesa collection of common GraphQL sources to simplify usingActiveRecord,ActiveStorage,Rails.cache, and more.graphql-filters, a DSL to define fully typed filters for list fields.
search_object_graphql, a DSL for defining search resolvers for GraphQL.action_policy-graphql, an integration for usingaction_policyas an authorization framework for GraphQL applications.graphql_rails, Rails way GraphQL build toolgraphql-rails-generators, Generate graphql-ruby mutations, types and input types from your ActiveRecord models.graphql-ruby-fragment_cache, a tool for caching response fragments.graphql-ruby-persisted_queries, the implementation of Apollo persisted queries.rubocop-graphql, rubocop extension for enforcing best practices.apollo-federation-ruby, a Ruby implementation of the Apollo Federation subgraph spec.
Blog Posts
- Building a blog in GraphQL and Relay on Rails Introduction, Part 1, Part 2
- https://medium.com/@khor/relay-facebook-on-rails-8b4af2057152
- https://blog.jacobwgillespie.com/from-rest-to-graphql-b4e95e94c26b#.4cjtklrwt
- https://jonsimpson.ca/parallel-graphql-resolvers-with-futures/
- Active Storage meets GraphQL: Direct uploads and Exposing attachment URLs
- Exposing permissions in GraphQL APIs with Action Policy
- Reporting non-nullable violations in graphql-ruby properly
- How to GraphQL with Ruby, Rails, Active Record, and no N+1
Screencasts
Presentations
Tutorials
Rails/GraphQL + React/Apollo Tutorial (Part 1, Part 2, Part 3) by @evilmartians
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:
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
endThe handler is called with several arguments:
erris the error that was raised during field execution, then rescuedobjis the object which was having a field resolved against itargsis the Hash of arguments passed to the resolverctxis the query contextfieldis 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
errto 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:
{
"errors" => [ ... ]
}The response may include both "data" and "errors" in the case of a partial success:
{
"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:
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:
{
"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:rubyraise GraphQL::ExecutionError.new("Something went wrong", extensions: { "code" => "BROKEN" })In this case,
"extensions" => { "code" => "BROKEN" }will be added to the error JSON.Override
#to_hin a subclass ofGraphQL::ExecutionError, for example:rubyclass ServiceUnavailableError < GraphQL::ExecutionError def to_h super.merge({ "extensions" => {"code" => "SERVICE_UNAVAILABLE"} }) end endNow,
"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: falsereturnednil - 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:
class MySchema < GraphQL::Schema
def self.type_error(err, query_ctx)
# Handle a failed runtime type coercion
end
endIt 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:
- Unexpected
nils add an error the response's"errors"key - Unresolved Union / Interface types raise GraphQL::UnresolvedTypeError
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:
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)
endTest your new configurations in CI by running a new build which calls execution_next instead of execution, for example:
# test_helpers.rb
def run_graphql(...)
if ENV["GRAPHQL_EXECUTION_NEXT"]
MyAppSchema.execute_next(...)
else
MyAppSchema.execute(...)
end
endAdopting 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:
# 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: resultFlipper 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:
# 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
endSee 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_nextresponse and compare it to the.executeresponse. But always return the.executeresponse.use_graphql_next?: when true, use.execute_nextand don't call.executeat 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:
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_hCompatibility 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:
field :title, StringPreviously, 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:
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
endMethods 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::Nextdoesn'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 indef 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: usebegin_execute_field,end_execute_fieldinstead. (These may be called multiple times when Dataloader pauses or a GraphQL-Batch promise is returned)execute_query,execute_query_lazy: useexecute_multiplexfor a top-level hook instead. (Single queries are always executed in a multiplex of size = 1.)resolve_type,authorized: use{begin,end}_resolve_typeand{begin,end}_authorizedinstead. (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 ofobject:, with an Arrayvalues:instead ofvalue:, 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-levelSchema.object_from_idhook.
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:
field :values, SomeObjectType, resolve_static: true
def self.values(context)
context.raw_value(...)
endErrors 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:
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
endGraphQL::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 ofMySchema.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: ...:
field :title, String # calls object.title
field :title, String, method: :get_title_somehow # calls object.get_title_somehowHash keys (`hash_key:`)
These fields call object[hash_key], configured with hash_key: ....
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:
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"]
endPer-object (`resolve_each:`)
These fields use a class method to produce a result for each object, configured with resolve_each:.
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:)
endThe default method is the same as the field name symbol. You can also provide a custom method:
# Avoid a conflict with Ruby's built-in `Class#name`:
field :name, String, resolve_each: :get_name
def self.get_name(object, context)
# ...
endUnder 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:
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
endUnder 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::
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:) }
endThis is especially useful when batching Dataloader calls:
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
endBy default, it calls a class method matching the field name. You can customize this configuration, too:
field :author_rating, Integer, resolve_batch: :calculate_rating # calls `self.calculate_rating(objects, context)`
def self.calculate_rating(objects, context)
# ...
endDataloader
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:
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:
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 }
endRails Records
Load ActiveRecord associations using GraphQL::Dataloader::ActiveRecordSource.
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 }
endLegacy 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. ‼️
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:)
endUnder 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:
field :posts_count, Integer, resolve_static: true
def self.posts_count(context)
Post.all.count
endMigration
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:
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:
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:
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)
endThis 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:
# Use the resolver class to execute this field
field :pending_orders, resolver: PendingOrdersUnder 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:
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:
# 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
# ...
endAs 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.includedhook, for example:
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
Resolveris 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 aResolversubclass) generates input types and return types for each mutation. Using aResolverclass makes it easier to implement, share and extend this code generation logic.
Using `resolver`
Use the base resolver class:
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
endAnd attach it to your field:
class Types::User < Types::BaseObject
field :recommended_items, resolver: Resolvers::RecommendedItems
endSince 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.
# 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
endThe 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:
module Resolvers
class TasksResolver < BaseResolver
type "[Types::TaskType]", null: false
def resolve
[]
end
end
endIn 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.
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
endGuides 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:
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:
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] }
endValidations 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:
length: { maximum: ..., minimum: ..., is: ..., within: ... }Schema::Validator::LengthValidatorformat: { with: /.../, without: /.../ }Schema::Validator::FormatValidatornumericality: { greater_than:, greater_than_or_equal_to:, less_than:, less_than_or_equal_to:, other_than:, odd:, even: }Schema::Validator::NumericalityValidatorinclusion: { in: [...] }Schema::Validator::InclusionValidatorexclusion: { in: [...] }Schema::Validator::ExclusionValidatorrequired: { one_of: [...] }Schema::Validator::RequiredValidatorallow_blank: true|falseSchema::Validator::AllowBlankValidatorallow_null: true|falseSchema::Validator::AllowNullValidatorall: { ... }Schema::Validator::AllValidator
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:
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:
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:
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 tosuper(**default_options)def validate(_empty, context, value)which is called at runtime to validatevalue. 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, likevalidates: { MyCustomValidator => { some: :options } - by keyword, if the keyword is registered with
GraphQL::Schema::Validator.install(:custom, MyCustomValidator). (That would supportvalidates: { 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 Link (2.x, 3.x):
- Apollo 1.x:
Apollo Link
Apollo Links are used by Apollo client 2.x and 3.x.
Apollo Link -- Pusher
graphql-ruby-client includes support for subscriptions with Pusher and ApolloLink.
To use it, add PusherLink before your HttpLink.
For example:
// 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:
// 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);
}
})Apollo Link -- Ably
graphql-ruby-client includes support for subscriptions with Ably and ApolloLink.
To use it, add AblyLink before your HttpLink.
For example:
// 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:
Apollo Link -- ActionCable
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:
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:
// 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:
// 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:
// 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:
<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:
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:
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:
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:
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:
- A client from the
pusher-jslibrary - A
fetchOperationfunction for sending thesubscriptionoperation to the server
Pusher client
Pass pusher: to get Subscription updates over Pusher:
// 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:
// 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:
- A client from the
ably-jslibrary - A
fetchOperationfunction for sending thesubscriptionoperation to the server
Ably client
Pass ably: to get Subscription updates over Ably:
// 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:
// 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:
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:
// Returns a promise from `fetch`
function fetchOperation(operation, variables, cacheConfig) {
return fetch(...)
}operation,variables, andcacheConfigare the first three arguments to thefetchQueryfunction.- The function should call
fetchand return the result (a Promise of aResponse).
For example, Environment.js may look like:
// 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.
syncCLI- Relay <2 support
- Relay 2+ support
- Apollo Client support
- Apollo Link support
- Apollo Codegen Support
- Apollo Android support
- Apollo Persisted Queries Support
- Plain JS support
- Authorization
See the OperationStore guide for server-side setup.
`sync` utility
This package contains a command line utility, graphql-ruby-client sync:
$ 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:
# 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:
// ...
// 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:
"relay": {
...
"persistConfig": {
"file": "./persisted-queries.json"
}
},Then, push Relay's generated queries to your OperationStore server with --relay-persisted-output:
$ 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:
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:
$ graphql-ruby-client sync --path=src/graphql/ --url=...Then, load the generated module and add its .apolloMiddleware to your network interface with .use([...]):
// 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 with Apollo Link
Use the --path option to point at your .graphql files:
$ graphql-ruby-client sync --path=src/graphql/ --url=...Then, load the generated module and add its .apolloLink to your Apollo Link:
// 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:
# 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:
# 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:
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:
# 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
endYou 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:
$ 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]:
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:
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:
// 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:
$ 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
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
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:
bundle add graphql-c_parserWhen 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:
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:
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:
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 withnew_attrsapplied. This new copy can replace the original node..add_{child}(new_child_attrs)makes a new node withnew_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:
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:
- GraphQL::Language::Nodes::Fields are routed to
#on_field - GraphQL::Language::Nodes::Arguments are routed to
#on_argument
See the GraphQL::Language::Visitor API docs for a full list of methods.
Each method is called with (node, parent), where:
nodeis the AST node currently visitedparentis 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:
def on_field(_node, _parent)
# Do nothing, this is the default behavior:
super
endTo 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:
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)
endDelete 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:
def on_directive(node, parent)
# Don't pass `node` to `super`,
# instead, pass `DELETE_NODE`
super(DELETE_NODE, parent)
endInsert 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:
def on_field(node, parent)
node_with_selection = node.add_selection(name: "emailAddress")
super(node_with_selection, parent)
endThis will add emailAddress the fields selection on node.
(These .add_* helpers are wrappers around GraphQL::Language::Nodes::AbstractNode#merge.)
The easiest way to turn an AST back into a string of GraphQL is GraphQL::Language::Nodes::AbstractNode#to_query_string, for example:
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:
class MySchema < GraphQL::Schema
# ...
use GraphQL::Enterprise::ActiveOperationLimiter,
redis: Redis.new(...),
# Or:
# connection_pool: ...
# redis_cluster: ...
limit: 5
endlimit: 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:
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:
# app/graphql/limiters/active_operations.rb
class Limiters::ActiveOperations < GraphQL::Enterprise::ActiveOperationsLimiter
# override methods here
endThe hooks are:
def limiter_key(query)should return a string which identifies the current client forquery.def limit_for(key, query)should return an integer ornil. If an integer is returned, that limit is applied for the current query. Ifnilis 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 towarnand 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]:
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 querylimit: [Integer, nil], the limit applied to this querysoft: [Boolean],trueif the query was run in "soft mode"limited: [Boolean],trueif the query exceeded the rate limit (but ifsoft:was alsotrue, then the query was not halted)
You could use this to add detailed metrics to your application monitoring system, for example:
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:
# 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:
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
endWith 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:
# 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::
use GraphQL::Enterprise::RuntimeLimiter, # or ActiveOperationLimiter
connection_pool: ConnectionPool.new(...) { ... }
# ...Redis Cluster
ActiveOperationsLimiter and RuntimeLimiter support redis-cluster. To use it, pass redis_cluster::
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:
class MySchema < GraphQL::Schema
# ...
use GraphQL::Enterprise::RuntimeLimiter,
redis: Redis.new(...),
# Or:
# connection_pool: ...
# redis_cluster: ...
limit_ms: 90 * 1000 # 90 seconds per minute
endlimit_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:
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:
# app/graphql/limiters/runtime.rb
class Limiters::Runtime < GraphQL::Enterprise::RuntimeLimiter
# override methods here
endThe hooks are:
def limiter_key(query)should return a string which identifies the current client forquery.def limit_for(key, query)should return an integer ornil. If an integer is returned, that limit is applied for the current query. Ifnilis 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 towarnand 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]:
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 querylimit_ms: [Integer, nil], the limit applied to this queryremaining_ms: [Integer, nil], the amount of time remaining in this client's bucketsoft: [Boolean],trueif the query was run in "soft mode"limited: [Boolean],trueif the query exceeded the rate limit (but ifsoft:was alsotrue, then the query was not halted)window_ms: [Integer]the configuredwindow_ms:for the limiter
You could use this to add detailed metrics to your application monitoring system, for example:
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
IDinputs - 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
class UpdateUserMutation < BaseMutation
# ...
def resolve(update_user_input:, user:)
# ...
end
def self.authorized?(obj, ctx)
super && ctx[:viewer].present?
end
endChecking 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:
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
# ...
endNow, 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:
def ready?
if !context[:current_user].allowed?
return false, { errors: ["You don't have permission to do this"]}
else
true
end
endLoading 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:
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
endIt works like this: if you pass a loads: option, it will:
- Automatically remove
_idfrom the name and pass that name for theas: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
#resolveusing 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.
argument :employee_id, ID, loads: Types::Employee
def load_employee(id)
::Employee.find(id)
endIf you don't want this behavior, don't use it. Instead, create arguments with type ID and use them your own way, for example:
# No special loading behavior:
argument :employee_id, IDCan _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:
def authorized?(employee:)
super && context[:current_user].manager_of?(employee)
endWhen #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:
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
endAlternatively, you can add top-level errors by raising GraphQL::ExecutionError, for example:
def authorized?(employee:)
super && if context[:current_user].manager_of?(employee)
true
else
raise GraphQL::ExecutionError, "You can only promote your _own_ employees"
end
endIn 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:
def resolve(employee:)
if employee.promote
{
employee: employee,
errors: [],
}
else
# See "Mutation Errors" for more:
{
errors: employee.errors.full_messages
}
end
endGuides 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:
- GraphQL::Schema::Mutation, a bare-bones base class
- GraphQL::Schema::RelayClassicMutation, a base class with a set of nice conventions that also supports the Relay Classic mutation specification.
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:
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
endThen extend it for your mutations:
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
endThe #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:
class Types::Mutation < Types::BaseObject
field :create_comment, mutation: Mutations::CreateComment
endAuto-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:
class Mutations::AddStar < Mutations::BaseMutation
argument :post_id, ID, loads: Types::Post
field :post, Types::Post
def resolve(post:)
post.star
{
post: post,
}
end
endBy 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:
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
endAll 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:
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
endIn 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:
def load_application_object_failed(error)
raise GraphQL::ExecutionError, "Couldn't find an object for ID: `#{error.id}`"
endOr, 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:
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}."
endGuides 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:
def resolve(id:, attributes:)
# Will crash the query if the data is invalid:
Post.find(id).update!(attributes.to_h)
# ...
endOr:
def resolve(id:, attributes:)
if post.update(attributes)
{ post: post }
else
raise GraphQL::ExecutionError, post.errors.full_messages.join(", ")
end
endThis 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:
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"
endThen, add a field to your mutation which uses this error type:
class Mutations::UpdatePost < Mutations::BaseMutation
# ...
field :errors, [Types::UserError], null: false
endAnd in the mutation's resolve method, be sure to return errors: in the hash:
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
endNow that the field returns errors in its payload, it supports errors as part of the incoming mutations, for example:
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:
{
"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!):
class Mutations::UpdatePost < Mutations::BaseMutation
# Use the default `null: true` to support rich errors:
field :post, Types::Post
# ...
endGuides Mutations Mutation Root (`guides/mutations/mutation_root.md`)
Mutation Root
GraphQL mutations all begin with the mutation keyword:
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:
class Types::Mutation < Types::BaseObject
# ...
endThen, it must be attached to your schema with the mutation(...) configuration:
class Schema < GraphQL::Schema
# ...
mutation(Types::Mutation)
endNow, 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:
# 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# app/graphql/types/base_field.rb
class Types::BaseField < GraphQL::Schema::Field
include GraphQL::Enterprise::ObjectCache::FieldIntegration
cacheable(...) # see below
# ...
endAlso, make sure your base interface module is using your field class:
# app/graphql/types/base_interface.md
module Types::BaseInterface
field_class Types::BaseField
endField caching can be configured per-field, too, for example:
field :latest_update, Types::Update, null: false, cacheable: { ttl: 60 }
field :random_number, Int, null: false, cacheable: falseOnly 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_attimestamp). In this case, a conservativettlmay be the only option for cache expiration. - Or, root-level fields that should be expired after a certain amount of time. The root-level
Queryoften has no backing object, so it won't have a cache fingerprint, either. Addingcacheable: { 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:
{
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:
# update the team whenever a player is saved or destroyed:
belongs_to :team, touch: trueTop-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:
class AllTeams < GraphQL::Enterprise::ObjectCache::CacheableRelation
def items(division: nil)
teams = Team.all
if division
teams = teams.where(division: division)
end
teams
end
endThen, in your resolver, use your new class to retrieve the items:
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
endIf you're using GraphQL::Schema::Resolver, you'd call .items_for slightly differently:
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)
endFinally, you'll need to handle CacheableRelations in your object identification methods, for example:
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
endIn this example, AllTeams implements several methods to support caching:
#idcreates a cache-friendly, stable global ID#to_paramcreates a cache fingerprint (using Rails's#cache_keyunder 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 usepublic: falsefor all introspection fields. Use this if you hide schema members for some clients.cache_introspection: falseto 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:
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
endUse 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)ordef self.object_fingerprint_forcan 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:
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:
maxmemory 1gb
maxmemory-policy allkeys-lfuAdditionally, consider conditionally skipping the cache to prioritize your most critical GraphQL traffic.
Redis Cluster
ObjectCache also supports Redis Cluster. To use, pass redis_cluster::
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::
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:
"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:
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:
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: ...:
class MySchema < GraphQL::Schema
use GraphQL::Enterprise::ObjectCache, redis: CACHE_REDIS
endSee 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:
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
endWhenever 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:
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
endThe 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 forobjectdef self.object_from_id(id, context)which returns the application object for the given globally-uniqueiddef self.resolve_type(abstract_type, object, context)which returns a GraphQL object type definition to use forobject
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:
class MySchema < GraphQL::Schema
def self.fingerprint
"v2" # increment this if there are breaking changes to the schema
end
endBy 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:
"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:
$ 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:
$ rails generate migration SetupOperationStoreThen open the migration file and add:
/* Detailed source-code truncated for AI context efficiency. */Then run the migration:
$ 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:
add_column :graphql_client_operations, :is_archived, :boolean, default: false
add_column :graphql_client_operations, :last_used_at, :datetimeUpdating `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:
use GraphQL::Pro::OperationStore, update_last_used_at_every: 1 # secondsTo 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:
# Update immediately in Test, wait 5 seconds in other environments:
use GraphQL::Pro::OperationStore, update_last_used_at_every: Rails.env.test? ? 0 : 5Guides Operation Store Client Workflow (`guides/operation_store/client_workflow.md`)
Client Workflow
To use persisted queries with your client application, you must:
- Set up
OperationStore, as described in Getting Started - Add the client to the system
- Sync operations from the client to the server
- Send
params[:operationId]from the client app
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
.graphqlfiles orrelay-compileroutput in the provided--path - Adds an Authentication header based on the provided
--clientand--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:
{
# ...
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:
- Check the dependencies to make sure
OperationStoreis supported - Prepare the database for
OperationStore's data - Add
OperationStoreto your GraphQL schema - Add routes for the Dashboard and sync API
- Update your controller to support persisted queries
- Add a client to start syncing queries
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 byconfig/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:
class MySchema < GraphQL::Schema
# Add it _after_ other tracing-related features, for example:
# use GraphQL::Tracing::DataDogTracing
# ...
use GraphQL::Pro::OperationStore
endMake 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; ORbackend_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:
# 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"
endMySchema.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:
# 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:
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:
# 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:
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:
# 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:
# 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
OperationStoredata
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:
Connectiontypes are generics that expose pagination-related metadata and access to the itemsEdgetypes are also generics. They represent the relationship between the parent and child (eg, aPostEdgerepresents the link fromBlogtoPost).- nodes are actual list items. In a
PostsConnection, each node is aPost.
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:
{
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:
{
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:
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:
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:
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:
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)
endNow, 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:
# app/graphql/connections/search_results_connection.rb
class Connections::SearchResultsConnection < GraphQL::Pagination::Connection
# implementation here ...
endThe methods you must implement are:
#nodes, which returns a paginated slice of@itemsbased on the given arguments#has_next_page, which returnstrueif there are items after the ones in#nodes#has_previous_page, which returnstrueif there are items before the ones in#nodes#cursor_for(item), which returns a String to serve as the cursor foritem
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:
- GraphQL::Pagination::ArrayConnection
- GraphQL::Pagination::ActiveRecordRelationConnection
- GraphQL::Pagination::SequelDatasetConnection
- GraphQL::Pagination::MongoidRelationConnection
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:
class MySchema < GraphQL::Schema
# Hook up a custom wrapper
connections.add(SearchEngine::Result, Connections::SearchResultsConnection)
endNow, 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):
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)
endGraphQL-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_typewhich returns a generated connection type based on that class. By default, it inherits from the providedGraphQL::Types::Relay::BaseConnection, but you can override that by settingconnection_type_class(Types::MyBaseConnectionObject)in your base classes.
For example, you could implement a base connection class:
class Types::BaseConnectionObject < Types::BaseObject
# implement based on `GraphQL::Types::Relay::BaseConnection`, etc
endThen hook it up to your base classes:
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)
endThen, when defining fields, you could use .connection_type to use your connection class hierarchy:
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:
LIMIT 10 -- page 1
WHERE id > :last_id LIMIT 10 -- page 2This 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:
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)
endAlternatively, you can apply the stable connection wrapper on a field-by-field basis. For example:
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)
endThat 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 intoWHEREconditions.) - The connection may add an additional
primary_keyordering to ensure that the cursor value is unique. This behavior is inspired byRelation#reverse_orderwhich also assumes thatprimary_keyis the default sort. - The connection will add fields to the relation's
SELECTclause, 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:
# 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:
field :items, Types::ItemType.connection_type, null: falseThe 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:
# here's a custom type whose name doesn't end in "Connection", so `connection: true` is required:
field :items, Types::ItemConnectionPage, null: false, connection: trueThe 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:
- field :items, Types::ItemType.connection_type, null: false
+ field :items, Types::ItemType.connection_type, null: false, connection: falseThen, 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:
def items
object.items # => eg, returns an ActiveRecord Relation
endThe 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:
def items
# Get the ActiveRecord relation to paginate
relation = object.items
# Apply a custom wrapper
Connections::ItemsConnection.new(relation)
endThis 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:
class MyAppSchema < GraphQL::Schema
default_max_page_size 50
endAt 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:
field :items, Item.connection_type, null: false,
max_page_size: 25- Dynamically, you can add
max_page_size:when you apply custom connection wrappers:
def items
relation = object.items
Connections::ItemsConnection.new(relation, max_page_size: 10)
endTo 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:
class MyAppSchema < GraphQL::Schema
default_page_size 50
endAt 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:
field :items, Item.connection_type, null: false,
default_page_size: 25- Dynamically, you can add
default_page_size:when you apply custom connection wrappers:
def items
relation = object.items
Connections::ItemsConnection.new(relation, default_page_size: 10)
endIf 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
# 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"
endWith 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:
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"
endWith 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:
# 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"
endRack Basic Authentication
Insert the Rack::Auth::Basic middleware, before the web view. This prompts for a username and password when visiting the dashboard.
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-gcmby default - Authentication
- Nonces for cursors (but not IDs, that would be silly)
Defining an Encoder
Encoders can be created by subclassing GraphQL::Pro::Encoder:
class MyEncoder < GraphQL::Pro::Encoder
key("f411f30...")
# optional:
tag("81ce51c307")
endkeyis 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:
class MySchema GraphQL::Schema
cursor_encoder(MyCursorEncoder)
endNow, 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:
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
endNote 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:
# 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:
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
endEncoding
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:
module URLSafeEncoder
def self.encode(str)
Base64.urlsafe_encode64(str)
end
def self.decode(str)
Base64.urlsafe_decode64(str)
end
endThen attach it to your encoder:
class MyURLSafeEncoder < GraphQL::Pro::Encoder
encoder URLSafeEncoder
endNow, 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:
bundle config gems.graphql.pro #{YOUR_CREDENTIALS}Then, you can add graphql-pro to your Gemfile, which a custom source:
source "https://gems.graphql.pro" do
gem "graphql-pro"
endThen, install the gem with Bundler:
bundle installThen, check out some of GraphQL::Pro's features!
Updates
To update GraphQL::Pro, use Bundler:
bundle update graphql-proBe 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:
# Rakefile
require "graphql/rake_task/validate"Then invoke it with a version:
$ 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:
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:
class MySchema < GraphQL::Schema
query_analyzer MyQueryAnalyzer
endPass 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 treevisitor: A GraphQL::Analysis::Visitor which is managing this analysis run
For example:
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
endIn 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:
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
endSee 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:
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
endConditional 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:
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
endAnalyzing 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:
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:
Locis theline:columnof the field in the query stringFieldis theTypeName.fieldNameof the fields in the backtraceObjectis theobjfor query resolution (used for resolving the given field), equivalent toobj.inspectArgumentsare the GraphQL arguments for field resolution (including any default values and variable values)Resultis 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:
# 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:
class MySchema < GraphQL::Schema
# Always wrap backtraces with GraphQL annotation
use GraphQL::Backtrace
endNow, any unhandled errors will be wrapped by GraphQL::Backtrace::TracedError, which prints out the GraphQL backtrace, too. For example:
/* 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:
# 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:
# 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:
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)
endPrevent 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:
# 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
}
endThen, define your max_complexity at the schema-level:
class MySchema < GraphQL::Schema
# ...
max_complexity 100
endOr, at the query-level, which overrides the schema-level setting:
MySchema.execute(query_string, max_complexity: 100)Using nil will disable the validation:
# 😧 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:
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)
endBy 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
1forpageInfoand each of its subselectionsadding
1forcount,totalCount, ortotaladding
1for the connection field itselfmultiplying the complexity of other fields by the largest possible page size, which is the greater of
first:orlast:, or if neither of those are given it will go through each ofdefault_page_size, the schema'sdefault_page_size,max_page_size, and then the schema'sdefault_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_sizeordefault_max_page_sizein your schema to prevent this.)
For example, this query has complexity 26:
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:
class Types::BaseField < GraphQL::Schema::Field
def calculate_complexity(query:, nodes:, child_complexity:)
if connection?
# Custom connection calculation goes here
else
super
end
end
endHow 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:
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:
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
nodescope makes many possible selections on an abstract type, so we need the maximum among concrete possibilities for a fair representation. - A
node.metafieldselection path is duplicated across theHasMetafieldsandProductselection 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):
{
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:
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:
query_string = "{ ... }"
MySchema.execute(query_string)
# {
# "data" => { ... }
# }Or, you can execute multiple queries at once:
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:
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:
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:
resolvefunctionsSchema#resolve_typehook- 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:
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:
field :post, Post do
argument :id, ID
end
def post(id:)
context[:current_user] # => #<User id=123 ... >
# ...
endNote 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:
{
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!:
class Types::Post < Types::BaseObject
# ...
def comments
context.scoped_set!(:current_post, object)
object.comments
end
endThen, inside User (assuming author resolves to Types::User), you can check context[:current_post]:
class Types::User < Types::BaseObject
# ...
def is_original_poster
current_post = context[:current_post]
current_post && current_post.author == object
end
endcontext[: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:
# For use with GraphQL-Batch promises:
scoped_ctx = context.scoped
SomethingLoader.load(:something).then do |thing|
scoped_ctx.set!(:thing_name, thing.name)
endRoot Value
You can provide a root object value with root_value:. For example, to base the query off of the current organization:
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:
class Types::MutationType < GraphQL::Schema::Object
field :create_post, Post
def create_post(**args)
object # => #<Organization id=456 ...>
# ...
end
endGraphQL::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:
class MySchema < GraphQL::Schema
# This logger will be used by queries during execution:
default_logger MyCustomLogger.new
endYou 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:
field :files, [Types::File], null: false, extras: [:lookahead]Then, update your resolver method to accept a lookahead: argument:
def files(lookahead:)
# ...
endThat 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:
def files(lookahead:)
if lookahead.selects?(:full_path)
# This is a query like `files { fullPath ... }`
else
# This query doesn't have `fullPath`
end
endOr, you can list all the selected fields:
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
endLookaheads are chainable, so you can use them to check nested selections too:
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
endNested 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:
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
endThat 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:
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:
def find_bird_species(by_name:, lookahead:)
if lookahead.selects_alias?("gull")
lookahead.alias_selection("gull")
end
endGuides 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:
# 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:
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:
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
endValidation 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:
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:
class MySchema < GraphQL::Schema
# ...
multiplex_analyzer(MyAnalyzer)
endThe 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:
# 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 )
endNow, 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:
- Tokenize: GraphQL::Language::Lexer splits the string into a stream of tokens
- Parse: GraphQL::Language::Parser builds an abstract syntax tree (AST) out of the stream of tokens
- Validate: GraphQL::StaticValidation::Validator validates the incoming AST as a valid query for the schema
- Analyze: If there are any query analyzers, they are run with GraphQL::Analysis.analyze_query
- Execute: The query is traversed,
resolvefunctions are called and the response is built - Respond: The response is returned as a GraphQL::Query::Result
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:
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
endThat would add to the final query response:
{
"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:
class MySchema < GraphQL::Schema
use GraphQL::Schema::Timeout, max_seconds: 2
endAfter 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:
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
endCustomizing 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:
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
endValidation 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:
# 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
endNote: 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.
module CustomTrace
def parse(query_string:)
# measure, log, etc
super
end
# ...
endTo include a trace module when running queries, add it to the schema with trace_with:
# Run `MyCustomTrace` for all queries
class MySchema < GraphQL::Schema
trace_with(MyCustomTrace)
endFor 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:
ActiveSupport::Notifications: See Tracing::ActiveSupportNotificationsTrace.- AppOptics instrumentation is automatic in
appoptics_apmv4.11.0+. - AppSignal: See Tracing::AppsignalTrace.
- Datadog: See Tracing::DataDogTrace.
- NewRelic: See Tracing::NewRelicTrace.
- Prometheus: See Tracing::PrometheusTrace.
- Scout APM: See Tracing::ScoutTrace.
- Sentry: See Tracing::SentryTrace.
- Skylight: either enable the GraphQL probe or use Tracing::ActiveSupportNotificationsTrace.
- Statsd: See Tracing::StatsdTrace.
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:
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
endThere 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::Visibilityto 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/RootTypesInBlockwill make sure thatquery,mutation, andsubscriptionare all defined in a block.GraphQL/FieldTypeInBlockwill 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.
- Rails: enabled automatically. (ActiveSupport calls
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:
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
endThen, you can configure fields with for_staff: true|false:
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: trueWith 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:
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
endHidden 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:
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
endThen, you can configure arguments with for_staff: true|false:
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)
# ...
endThat 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:
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
endWith this base class, you can configure some enum values to be just for staff or non-staff viewers:
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
endUsing `.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:
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
endDifferent 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:
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
# ...
endDifferent definitions for the same type
You can provide different implementations of the same type by:
- Implementing
def self.visible?(context)to returntrueandfalsein 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:
# 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
endThen, hook the definitions up to the schema using field definitions:
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
endInput 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:
# Legacy money schema:
MySchema.to_definition(context: { requests_legacy_money: true })or
# 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:
- setup GraphQL, including GraphiQL, GraphQL::Batch, and Relay
- scaffold types
- scaffold Relay mutations
- scaffold ActiveRecord create/update/delete mutations
- scaffold GraphQL::Batch loaders
graphql:install
You can add GraphQL to a Rails app with graphql:install:
rails generate graphql:installThis will:
- Set up a folder structure in
app/graphql/ - Add schema definition
- Add base type classes
- Add a
Querytype definition - Add a
Mutationtype definition with a base mutation class - Add a route and controller for executing queries
- Install
graphiql-rails - Enable
ActiveRecord::QueryLogsand add GraphQL-related metadata (using GraphQL::Current)
After installing you can see your new schema by:
bundle installrails server- Open
localhost:3000/graphiql
Options
--directory=DIRECTORYwill specify the directory where generated files should be saved (default isapp/graphql)--schema=MySchemaNamewill be used for naming the schema (default is#{app_name}Schema)--skip-graphiqlwill excludegraphiql-railsfrom the setup--skip-mutation-root-typewill not create of the mutation root type--skip-query-logswill skip the QueryLogs setup--relaywill add Relay-specific code to your schema--batchwill add GraphQL::Batch to your gemfile and include the setup in your schema--playgroundwill includegraphql_playground-railsin the setup (mounted at/playground)--apiwill 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:objectrails g graphql:inputrails g graphql:interfacerails g graphql:unionrails g graphql:enumrails 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-typeswill generate each one of theobject/input/interface/... types under separateTypes::Objects::*/Types::Inputs::*/Types::Interfaces::*/... namespaces and folders
Scaffolding Mutations
You can prepare a Relay Classic mutation with
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
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
rails g graphql:loaderGuides 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:
{
__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:
__schemais 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 givenname, if there is a type with that name.__typenameworks 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:
{
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:
{
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.
# 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
endKeep 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
__Schemaand__Type) EntryPoints, A class-based object definition containing introspection entry points (like__schemaand__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:
module Introspection
class SchemaType < GraphQL::Introspection::SchemaType
# ...
end
endInside 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:
__schemareturns 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:
module Introspection
class EntryPoints < GraphQL::Introspection::EntryPoints
# ...
end
endThis 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:
module Introspection
class DynamicFields < GraphQL::Introspection::DynamicFields
# ...
end
endAny 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:
class MySchema < GraphQL::Schema
disable_introspection_entry_points if Rails.env.production?
endWhere 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:
class MySchema < GraphQL::Schema
disable_schema_introspection_entry_point
disable_type_introspection_entry_point
endGuides 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
resolvemethods, 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.
- Lazy-loading class which finds models by ID.
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- Connect the lazy resolve method
class MySchema < GraphQL::Schema
# ...
lazy_resolve(LazyFindPerson, :person)
end- Return lazy objects from
resolve
field :author, PersonType
def author
LazyFindPerson.new(context, object.author_id)
endNow, calls to author will use batched database access. For example, this query:
{
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-batchprovides a powerful, flexible toolkit for lazy resolution with GraphQL.dataloaderis more general promise-based utility for batching queries within the same thread.batch-loaderworks 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:
class Types::PostType < GraphQL::Schema::Object
# Implement the "Node" interface for Relay
implements GraphQL::Types::Relay::Node
# ...
endTo tell GraphQL how to resolve members of the Node interface, you must also define Schema.resolve_type:
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
endUUID 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::
class Types::PostType < GraphQL::Schema::Object
implements GraphQL::Types::Relay::Node
endThis 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:
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:
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
# ...
endGuides 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:
class MySchema < GraphQL::Schema
# required
query Types::QueryType
# optional
mutation Types::MutationType
subscription Types::SubscriptionType
endThe types are GraphQL::Schema::Object classes, for example:
# 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
endEach type is the entry point for the corresponding GraphQL query:
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:
# From a file:
MySchema = GraphQL::Schema.from_definition("path/to/schema.graphql")
# Or, from a string:
MySchema = GraphQL::Schema.from_definition(<<~GRAPHQL)
type Query {
# ...
}
# ...
GRAPHQLDefinitions 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:
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 resolvingobjas one ofabstract_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:
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 resolvingobjectto one ofabstract_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:
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:
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
subscriptionquery is sent by HTTP Post to your server (just like aqueryormutation) - 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:
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:
gem 'ably-rest'and bundle install.
Database setup
Subscriptions require a persistent Redis database, configured with:
maxmemory-policy noeviction
# optional, more durable persistence:
appendonly yesOtherwise, 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:
gem 'redis'and bundle install. Then create a Redis instance:
# for example, in an initializer:
$graphql_subscriptions_redis = Redis.new # default connectionThen, that Redis client is passed to the Subscription configuration:
class MySchema < GraphQL::Schema
use GraphQL::Pro::AblySubscriptions,
redis: $graphql_subscriptions_redis,
ably: Ably::Rest.new(key: ABLY_API_KEY)
endThat 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. Afterstale_ttl_shas 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:
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:
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: resultThis 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:
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"
endRead 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:
# 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"
endAlternatively, you can configure the routes to load your schema lazily, during the first request:
# 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
- Go to the Ably dashboard
- Click on your application
- Select the "Integrations" tab
- Click on the "+ New Integration Rule" button
- Click on the "Choose" button for "Webhook"
- Click on the "Choose" button for "Webhook" (again)
- Enter your URL (including the webhooks path from above) in the URL field.
- Select "Batch request" for "Request Mode"
- Under "Source", select "Presence"
- Under "Sign with key", select the API Key prefix that matches the prefix of the
ABLY_API_KEYyou provided - Click "Create"
Authorization
You can use Ably's token authentication by implementing an endpoint in your app, for example:
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
endAbly'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:
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:
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::AblySubscriptionswill generate per-subscription keys (usingcipher_baseand 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):
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
endThen, use your custom subscriptions class instead of the built-in one for your schema:
class MySchema < GraphQL::Schema
# Use custom subscriptions instead of GraphQL::Pro::AblySubscriptions
# to get custom serialization logic
use CustomSubscriptions, ...
endThat 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:
# Wipe all subscription data from the DB:
MySchema.subscriptions.clearDeveloping 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:
class MyAppSchema < GraphQL::Schema
# ...
use SomeSubscriptionImplementation,
broadcast: true # <----
endThen, any broadcastable field can be configured with broadcastable: true:
field :name, String, null: false,
broadcastable: trueWhen 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:
class MyAppSchema < GraphQL::Schema
# ...
use SomeSubscriptionImplementation,
broadcast: true,
default_broadcastable: true # <----
endWith 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?:
subscription_string = "subscription { ... }"
MySchema.subscriptions.broadcastable?(subscription_string)
# => true or falseUse 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:
# 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:
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:
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:
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 byActionCablePusherSubscriptionsandAblySubscriptions: 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:
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)
endThe 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):
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
endThe 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:
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
endWith 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
subscriptionquery is sent by HTTP Post to your server (just like aqueryormutation) - 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:
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:
maxmemory-policy noeviction
# optional, more durable persistence:
appendonly yesOtherwise, 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:
gem 'redis'and bundle install. Then create a Redis instance:
# for example, in an initializer:
$graphql_subscriptions_redis = Redis.new # default connectionThen, that Redis client is passed to the Subscription configuration:
class MySchema < GraphQL::Schema
use GraphQL::Pro::PusherSubscriptions, redis: $graphql_subscriptions_redis
endThat 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. Afterstale_ttl_shas 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:
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?:
# In your Pusher authorization endpoint:
channel_name = params[:channel_name]
MySchema.subscriptions.broadcast_subscription_id?(channel_name)
# => true | falseExecution 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:
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: resultThis 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:
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"
endRead 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:
# 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
# ...
endThis 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:
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:
# 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"
endThis 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:
# 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:
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:
if params[:channel_name].start_with?("private-user-#{current_user.id}-")
# success, render the auth token
else
# failure, render unauthorized
endSerializing 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):
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
endThen, use your custom subscriptions class instead of the built-in one for your schema:
class MySchema < GraphQL::Schema
# Use custom subscriptions instead of GraphQL::Pro::PusherSubscriptions
# to get custom serialization logic
use CustomSubscriptions, redis: $redis
endThat 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:
# Wipe all subscription data from the DB:
MySchema.subscriptions.clearDeveloping 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:
- Authorizing (or rejecting) initial subscription requests and subsequent updates
- Returning values for initial subscription requests
- Unsubscribing from the server
- Implicitly scoping updates, to direct data to the right subscriber
- Skipping updates for certain clients (eg, don't send updates to the person who triggered the event)
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:
# 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:
# app/graphql/subscriptions/message_was_posted.rb
class Subscriptions::MessageWasPosted < Subscriptions::BaseSubscription
endThen, hook up the new class to the Subscription root type with the subscription: option:
class Types::SubscriptionType < Types::BaseObject
field :message_was_posted, subscription: Subscriptions::MessageWasPosted
endNow, it will be accessible as:
subscription {
messageWasPosted(roomId: "abcd") {
# ...
}
}Arguments
Subscription fields take arguments just like normal fields. They also accept a loads: option just like mutations. For example:
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
endThis can be invoked as
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:
class Subscriptions::MessageWasPosted < Subscriptions::BaseSubscription
field :room, Types::RoomType, null: false
field :message, Types::MessageType, null: false
endwill generate:
type MessageWasPostedPayload {
room: Room!
message: Message!
}Which you can use in queries like:
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:
# 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:
class Subscriptions::EmployeeHired < Subscriptions::BaseSubscription
# ...
subscription_scope :current_organization_id
endClients subscribe without any arguments:
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:
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:
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:
def authorized?(room:)
super && context[:viewer].can_read_messages?(room)
endThe 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::ExecutionErrorto halt and return an error - Return a value to give the client an initial response
- Return
:no_responseto skip the initial response - Return
superto 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:
class Subscriptions::MessageWasPosted < Subscriptions::BaseSubscription
# ...
field :room, Types::RoomType
def subscribe(room:)
# authorize, etc ...
# Return the room in the initial response
{
room: room
}
end
endNow, a client can get some initial data with:
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 returnsobject) or by returning a different value. - Return
NO_UPDATEto 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.
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
endReturning 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:
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 }
endTerminating the subscription with #unsubscribe
Within a subscription method, you may call unsubscribe to terminate the client's subscription, for example:
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:
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
endExtras
Subscription methods can access query-related metadata by configuring extras [...] in the class definition. For example, to use a lookahead and the ast_node:
class Subscriptions::JobFinished < GraphQL::Schema::Subscription
# ...
extras [:lookahead, :ast_node]
def subscribe(lookahead:, ast_node:)
# ...
end
def update(lookahead:, ast_node:)
# ...
end
endSee 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:
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:
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:
# app/graphql/types/subscription_type.rb
class Types::SubscriptionType < GraphQL::Schema::Object
field :post_was_published, subscription: Subscriptions::PostWasPublished
# ...
endThen, add it as the subscription root with subscription(...):
# app/graphql/my_schema.rb
class MySchema < GraphQL::Schema
query(Types::QueryType)
# ...
# Add Subscription to
subscription(Types::SubscriptionType)
endSee 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
# 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 typearguments, 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 updatescope:(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:
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:
# 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:
# 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:
use SomeSubscriptions, validate_update: falseIf 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:
# 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:
post = Post.first
graphql_post_title = run_graphql_field("Post.title", post)
assert_equal "100 Great Ideas", graphql_post_titlerun_graphql_field accepts two required arguments:
- Field path, in
Type.fieldformat - Runtime object: some non-
nilobject 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:
# 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 })
endThe 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:
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"]
endTo 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:
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"]
endThis 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:
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"
endGuides 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 { ... }.
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)
endThe 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:
$ 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:
$ 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 | endAnyone 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:
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
titlefield, 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.
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
endGuides 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:
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:
@skip(if: ...)skips the selection if theif: ...value is truthy (GraphQL::Schema::Directive::Skip)@include(if: ...)includes the selection if theif: ...value is truthy (GraphQL::Schema::Directive::Include)
Custom Runtime Directives
Custom directives extend GraphQL::Schema::Directive:
# app/graphql/directives/my_directive.rb
class Directives::MyDirective < GraphQL::Schema::Directive
description "A nice runtime customization"
location FIELD
endThen, they're hooked up to the schema using directive(...):
class MySchema < GraphQL::Schema
# Attach the custom directive to the schema
directive(Directives::MyDirective)
endAnd you can reference them in the query with @myDirective(...):
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 returnsfalse, 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, soyieldwill 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:
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:
# app/graphql/directives/permission.rb
class Directives::Permission < GraphQL::Schema::Directive
argument :level, String
locations FIELD_DEFINITION, OBJECT
endThen, attach it to parts of your schema with directive(...):
class Types::JobPosting < Types::BaseObject
directive Directives::Permission, level: "manager"
endArguments and fields also accept a directives: keyword:
field :salary, Integer, null: false,
directives: { Directives::Permission => { level: "manager" } }After that:
- the configured object's
.directivesmethod 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:
- GraphQL::Schema::Directive::Deprecated which implements
deprecation_reason(via GraphQL::Schema::Member::HasDeprecationReason) - GraphQL::Schema::Directive::Flagged, which is an example of using schema directives to implement visibility
Custom Name
By default, the directive's name is taken from the class name. You can override this with graphql_name, for example:
class Directives::IsPrivate < GraphQL::Schema::Directive
graphql_name "someOtherName"
endArguments
Like fields, directives may have arguments :
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:
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:
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:
# 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:
# 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"
endEach 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:
value "AUDIO", value: :audioThen, 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:
Types::MediaCategory.audio # => "AUDIO"You can pass a value_method: to override the value of the generated method:
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
value "AUDIO", value: :audio, value_method: falseGuides 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, andsubscription) and any definedorphan_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:
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
# => :adminNow, 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:
# 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
#initializeon that class (instance methods) - registering that class as the
field_classon Objects and Interfaces which should use the customized field.
For example, you can create a custom class which accepts a new parameter to initialize:
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
endThen, pass the field class as field_class(...) wherever it should be used:
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
endNow, 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:
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
endThen, pass the field class as connection_type_class(...) wherever it should be used:
module Types
class Types::BaseObject < GraphQL::Schema::Object
# Use this class for defining connections
connection_type_class MyCustomConnection
end
endNow, 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
Enumclass withenum_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:
class MyExtension < GraphQL::Schema::FieldExtension
endUsing an extension
Defined extensions can be added to fields using the extensions: [...] option or the extension(...) method:
field :name, String, null: false, extensions: [UpcaseExtension]
# or:
field :description, String, null: false do
extension(UpcaseExtension)
endSee 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:
class SearchableExtension < GraphQL::Schema::FieldExtension
def apply
# add an argument to this field:
field.argument(:query, String, required: false, description: "A search query")
end
endThis 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:
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")
endAdditionally, 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