Lighthouse GraphQL RailsLighthouse GraphQL Rails
Docs
Directives
Example app
Changelog
RubyGems
GitHub
Docs
Directives
Example app
Changelog
RubyGems
GitHub
  • Getting Started

    • Motivation
    • Getting Started
    • Configuration
  • The Basics

    • Resolvers, Mutations and Builders
    • Generators
    • Unions, Interfaces and Scalars
    • Relay: Global Object Identification
    • Relationships
    • Filtering & Ordering
    • Mutations
    • File Uploads
    • Validation
    • Subscriptions
  • Security

    • Authentication & Authorization
    • Query Complexity & Depth
  • Performance

    • Performance at scale
    • Caching
  • Digging Deeper

    • Schema Organisation
    • Custom Directives
    • Events
    • Apollo Federation
    • Best Practices
  • API Reference

    • Directives Reference

Events

Lighthouse dispatches events as a request goes by and as the schema is built, so an app can hook into them without patching either. They mirror PHP Lighthouse's events, adapted to Ruby: a listener is a block or anything that responds to call(event).

# config/initializers/lighthouse.rb
Lighthouse::Events.listen(Lighthouse::Events::StartExecution) do |event|
  Rails.logger.info("GraphQL #{event.operation_name} at #{event.moment}")
end

Listening

listener = Lighthouse::Events.listen(Lighthouse::Events::EndExecution) { |event| Metrics.record(event.result) }
Lighthouse::Events.listen(Lighthouse::Events::EndRequest, AuditLog)   # anything with #call
Lighthouse::Events.forget(listener)                                    # stop listening
  • Listeners run in registration order. A listener on an event class also hears its subclasses, so one on Lighthouse::Events::Event hears everything.
  • Lighthouse::Events.dispatch(event) returns the non-nil values the listeners returned. Some events give them a meaning (below); for the others they are ignored, so a listener's last expression does no harm.
  • Register in an initializer. Specs can call Lighthouse::Events.reset! to drop every listener.

ActiveSupport::Notifications

Every dispatch is also instrumented as <event name>.lighthouse, with the event as the :event payload, so Rails tooling observes them with no listener of ours:

ActiveSupport::Notifications.subscribe('end_execution.lighthouse') do |*, payload|
  Rails.logger.info(payload[:event].result['errors'])
end

The name is the class name underscored: start_request.lighthouse, build_schema_string.lighthouse, ...

Request lifecycle

In the order they happen. Events with a moment carry the Time they were created at (UTC).

EventWhenReaders
StartRequestThe controller action beginsrequest, moment
StartOperationOrOperationsThe operation(s) were read, before they runoperation_or_operations
StartExecutionA query is about to run (once per operation of a batch)schema, query, variables, operation_name, context, moment
BuildExtensionsResponseA query ranresult, query_complexity
ManipulateResultBefore the result is returnedresult (+ writer)
EndExecutionThe result is finalresult, moment
EndOperationOrOperationsThe operation(s) ranresult_or_results
EndRequestThe response is renderedresponse, moment

StartRequest and EndRequest are dispatched by execute, the action of the Lighthouse::GraphQL::Controller concern that rails g lighthouse:install generates a controller with (getting-started.md): the first thing it does, and right after it renders. An action of your own gets them by wrapping its body in lighthouse_request, which dispatches them once however it nests:

def search
  lighthouse_request do
    render json: Lighthouse::GraphQL::PersistedQueries.execute(AppSchema, query: params[:q])
  end
end

Without the concern, dispatch them yourself: Lighthouse::Events.dispatch(Lighthouse::Events::StartRequest.new(request)), and EndRequest.new(response) after rendering.

StartOperationOrOperations and EndOperationOrOperations are dispatched by Lighthouse::GraphQL::PersistedQueries.execute, which the generated controller calls. The first carries a Hash (query, variables, operation_name, extensions), as the request named them. For a batch, run the operations through PersistedQueries.multiplex(schema, operations, context: ...) instead (the concern does for a JSON array body): it takes an Array of such Hashes (each may also carry its own context), dispatches the Arrays, and answers one result per operation. Operations that run straight on schema.execute or schema.multiplex fire only the execution events below.

Execution events

StartExecution, BuildExtensionsResponse, ManipulateResult and EndExecution come from a graphql-ruby trace Lighthouse installs on every schema it builds, so they fire however the query was started: the controller, schema.execute in a job, or a spec. A multiplex fires them once per query. With no listener, and no ActiveSupport::Notifications subscriber, they cost one lookup per request.

StartExecution#query is the parsed GraphQL::Language::Nodes::Document.

BuildExtensionsResponse: return an ExtensionsResponse (or an Array of them) to add to the result's extensions:

Lighthouse::Events.listen(Lighthouse::Events::BuildExtensionsResponse) do |event|
  Lighthouse::Events::ExtensionsResponse.new('queries', Query.count_for(event.result))
end
# => { "data" => {...}, "extensions" => { "queries" => 3 } }

query_complexity is the query's complexity score, the number graphql-ruby checks against max_complexity (see security.md). It is nil for a query that failed validation.

ManipulateResult: change event.result (the Hash with data, errors and extensions) in place, or assign a new Hash to event.result=:

Lighthouse::Events.listen(Lighthouse::Events::ManipulateResult) do |event|
  event.result['extensions'] = (event.result['extensions'] || {}).merge('served_by' => Socket.gethostname)
  nil
end

Both also fire for a query that fails validation; the result then holds errors and no data.

Schema build events

These fire while the schema is built, once per build. Listen from an initializer that runs before the schema is first used (the schema constant autoloads when something first refers to it).

RegisterDirectiveNamespaces: return a namespace (a Module, or its name) or an Array of them. Every directive class under them (a constant that responds to directive_name: a field resolver or an argument builder) is registered, as if with Lighthouse::DirectiveRegistry.register:

Lighthouse::Events.listen(Lighthouse::Events::RegisterDirectiveNamespaces) do
  %w[App::Graphql::Directives MyEngine::Directives]
end

A name that is no constant fails the build with Lighthouse::DefinitionError. It fires before the directive definitions are injected, on every build, schema cache or not.

BuildSchemaString: return SDL to append to the app's schema, before the manipulation phase (so it can use any directive):

Lighthouse::Events.listen(Lighthouse::Events::BuildSchemaString) do |event|
  'extend type Query { version: String @field(resolver: "Version") }'
end

ManipulateAST: the document after manipulation, right before the schema is built from it. graphql-ruby's nodes are immutable, so replace the document: event.document_ast = GraphQL.parse(...).

With schema_cache on, these two fire only when the document is actually built, on a cache miss, and the cache holds what they made. The cache key does not cover them: after you change what a listener adds, run bin/rails lighthouse:clear_cache.

ValidateSchema

bin/rails lighthouse:validate_schema (SCHEMA=AppSchema names another schema) dispatches ValidateSchema with the schema, then has graphql-ruby build and print every type. A listener that raises, or a schema graphql-ruby rejects, aborts the task with a message, so it can run in CI:

Lighthouse::Events.listen(Lighthouse::Events::ValidateSchema) do |event|
  raise 'The schema has no mutations' unless event.schema.mutation
end

Dispatching your own: @event

The @event directive dispatches an event of yours once a field has resolved:

type Mutation {
  publishPost(id: ID!): Post! @update @event(dispatch: "PostPublished")
}
# app/events/post_published.rb
class PostPublished
  attr_reader :post

  def initialize(post) = @post = post
end

Lighthouse::Events.listen(PostPublished) { |event| Mailer.published(event.post).deliver_later }

Any class works as an event. Lighthouse looks the name up in Config.namespaces[:events] (default ['Events'], so Events::PostPublished, then ::PostPublished).

Edit this page on GitHub
Prev
Custom Directives
Next
Apollo Federation