Skip to main content

Overview

Rails GraphQL uses an event-driven architecture to handle requests and directives. Events are divided into two phases: definition (when components are defined) and execution (during request processing).
The event system uses SimpleDelegator and custom delegators extensively for flexible callback handling.

Event Phases

Definition Phase

Events that occur when components are being defined:
# attach event - when directive is attached to component
on(:attach) do
  # Manipulate the component being attached to
end
More definition events will be added as the gem evolves.

Execution Phase

Events during request processing. See Request Event Types for the full list.
field(:id).on(:finalize) do
  # Multiply the id by 10 before adding to response
  self.current_value = current_value * 10
end

Event Listeners

Add listeners using the on method:
field(:id).on(:organized) { do_something }

Event Shortcuts

Fields provide shortcut methods for common events:
field(:name)
  .organized { setup }
  .prepared { prepare }
  .before_resolve { load_data }  # alias for prepare
  .after_resolve { cleanup }     # alias for finalize
  .authorize { check_permissions }
resolve and perform are exclusive - fields can only have one, and they cannot be set with on.

Callback Arguments

Argument injection is controlled by two settings:
  • callback_inject_arguments
  • callback_inject_named_arguments

Basic Injection

# From lib/rails/graphql/event.rb
class Event
  attr_reader :source, :data, :event_name, :object, :last_result
  
  def parameter(name)
    respond_to?(name) ? public_send(name) : data[name]
  end
end
on(:finalize) { |event| event.inspect }

With Extra Arguments

Extra arguments have higher precedence:
on(:finalize, 123) { |event| event.inspect }
#                     ↳ This will be 123

on(:finalize, 123) { |value, event| event.inspect }
#                            ↳ Event injected second

on(:finalize, format: :string) { |direction:| ... }
#                                 ↳ Works with named args

on(:prepare, :sort_records, :name)
def sort_records(field)
  # Extra arg injected first
end

Calling Next

Capture and manipulate values from the event chain:
# From lib/rails/graphql/event.rb
def call_next
  trigger(@items.next)
rescue StopIteration
  # Do not do anything when missing next/super
end
on(:finalize) { 1 }                                                 # 1
on(:finalize) { |event| event.last_result * 10 }                    # 1 * 10
on(:finalize) { |event| event.last_result + 4 }                     # 10 + 4
on(:finalize) { |event| event.current_value = event.last_result }   # 14
# Result: 14

Practical Example

Extending ActiveRecord scopes:
# Field created by ActiveRecord source
field = field(:addresses, 'Address', full: true) do
  before_resolve(:preload_association, :addresses)
  before_resolve(:build_association_scope, :addresses)
  resolve(:parent_owned_records, true)
end

# Extend the scope
field.before_resolve do
  call_next.where(deleted_at: nil)
end
The prepare (before_resolve) event runs in reverse order, so the last listener added runs first.

Directive Events

Directive events have special characteristics:

Binding

Binding is always the directive instance:
# app/graphql/directives/awesome_directive.rb
on(:finalize) do
  self  # => directive instance
  event # => available as @event
end

Event Arguments

With injection disabled:
# From docs/guides/events.md
on(:finalize) { self.inspect }
#               ↳ No injection

on(:finalize) { |event| event.inspect }
#                ↳ Event as extra argument

on(:finalize, Rails.env) { |env, event| event.inspect }
#                                ↳ Event always last

on(:finalize, :inspect_event)
def inspect_event
  event.inspect  # @event reader available
end
Directives do not automatically delegate missing methods to @event.

Event Filters

Narrow event triggering with filters:
on :attach, during: :definition
on :finalize, during: :execution
during
:definition | :execution
Filter by event phase
More filters will be added in future versions.

Exclusiveness

By default, events only trigger when the source matches:
# From lib/rails/graphql/event.rb
def same_source?(other)
  if other.is_a?(Directive) || (other.is_a?(Module) && other < Directive)
    event_name == :attach || source.using?(other)
  else
    source == other
  end
end
field(:id).on(:finalize) do
  # Only triggers for :id field
end

Directive Example

# app/graphql/directives/logger_directive.rb
on(:finalize, exclusive_callback: false) do |source|
  puts "#{source.gql_name} has been resolved!"
end

# app/graphql/app_schema.rb
use :logger

# Logs every field resolution in the schema

Event Triggering

Multiple trigger modes:
# From lib/rails/graphql/event.rb
TRIGGER_TYPES = {
  all?: :trigger_all,
  stack?: :trigger_all,
  object?: :trigger_object,
  single?: :trigger,
}.freeze

def self.trigger(event_name, object, source, **xargs, &block)
  extra = xargs.slice!(*TRIGGER_TYPES.keys)
  fallback = extra.delete(:fallback_trigger!) || :trigger
  method_name = xargs.find { |k, v| break TRIGGER_TYPES[k] if v } || fallback
  
  instance = new(event_name, source, **extra)
  instance.instance_variable_set(:@object, object) if block.present?
  instance.public_send(method_name, block || object)
end
Event.trigger(:finalize, field, source, single?: true)

Stopping Events

Halt event execution:
# From lib/rails/graphql/event.rb
def stop(*result, layer: nil)
  layer = @layers[layer] if layer.is_a?(Numeric)
  throw(layer || @layers.first, *result)
end
on(:finalize) do |event|
  event.stop(42) if condition?
  # Subsequent listeners won't run
end

Callbacks

Event listeners are compiled into Callback objects:
callback = GraphQL::AppSchema[:query][:welcome].resolver
callback.source_location
# => ["(symbolized-callback/#<... AppSchema[:query] welcome: String>)", :welcome]

callback.call(event)
# => executes the callback

Types

Created with blocks:
on(:finalize) { do_something }
Binding is the event (or directive instance).

Available Event Data

What you can access from events:
# From docs/guides/events.md
def welcome
  data                      # Additional data from trigger
  event                     # Event instance
  event_name                # Name of the event
  last_result               # Last result in chain
  object                    # Object calling trigger
  source                    # Event source
  
  parameter(name)           # try(name) || data[name]
  [name]                    # Same as parameter
  parameter?(name)          # respond_to?(name) || data.key?(name)
  key?(name)                # Same as parameter?
  stop(*result)             # Stop and return result
end
Request events provide additional parameters. See Request Event documentation.

Best Practices

Use Shortcuts

Prefer shortcut methods for readability:
# Good
field(:user).before_resolve { load_user }

# Less clear
field(:user).on(:prepare) { load_user }

Chain call_next

Use call_next to extend behavior:
field.before_resolve do
  call_next.where(active: true)
end

Named Arguments

Use named arguments for clarity:
on(:prepare) { |id:, type:| Model.find_by(id: id, type: type) }

Request

Request event types and lifecycle

Directives

Using events in directives

Build docs developers (and LLMs) love