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 Events
Directive Events
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:
Block Listener
Method Listener
With Arguments
field ( :id ). on ( :organized ) { do_something }
Event Shortcuts
Fields provide shortcut methods for common events:
Shortcut Methods
Special 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
Event Injection
Request Injection
Multiple Parameters
Named Arguments
Argument Values
on ( :finalize ) { | event | event. inspect }
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
Using last_result
Using call_next
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
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
Exclusive (default)
Non-exclusive
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
Single Trigger
Object Trigger
Stack Trigger
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
Stop Current
Stop Specific Layer
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). Created with method references: on ( :finalize , :do_something )
def do_something
# @event available
# Normal binding maintained
end
Forces instance creation, attaches @event variable.
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