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

Custom Directives

Adding a directive to Lighthouse is "write one class and register it." There are no edits to Lighthouse internals — the generator discovers behavior through a set of contracts (marker modules) and a registry.

The architecture

Lighthouse builds a schema in two phases, mirroring PHP Lighthouse's manipulator/resolver split:

  1. Manipulation phase (before the schema is built) — directives that need to reshape the SDL/AST run here: @paginate injecting a Paginator type, @whereConditions generating input and enum types, etc.
  2. Resolution phase (after GraphQL::Schema.from_definition) — directives attach runtime behavior: resolving data, wrapping a resolver, or composing a query constraint.

A Lighthouse::DirectiveRegistry maps directive names to the classes that implement them. The generator iterates the registry rather than a hardcoded dispatch table.

The contracts

Include the marker module(s) your directive fulfils:

ContractRoleKey method
Contracts::FieldResolverprovide a field's resolver#define_resolver(type) (or #resolve_field)
Contracts::FieldMiddlewarewrap an existing resolver (e.g. @auth)#wrap_resolver(next)
Contracts::FieldManipulatorreshape the schema for a fieldmanipulate_field_definition(...)
Contracts::ArgManipulatorreshape the schema for an argument, as @orderBy and @whereConditions generate their types#manipulate_arg_definition(document_ast, field_node, type_node) returning the argument node
Contracts::FieldBuilderconstrain a field's query, whatever the client sends#handle_field_builder(relation)
Contracts::ArgBuildercompose a query constraint from an argument#handle_builder(relation, value)

A directive class also answers two class-level questions: its name and its SDL definition.

def self.directive_name = 'upcase'
def self.definition = 'directive @upcase on FIELD_DEFINITION'

If the definition refers to types of its own (an enum argument, say), return them from def self.type_definitions = { 'Mode' => 'enum Mode { A B }' }.

Lighthouse injects that definition into the schema automatically, so apps never hand-declare your directive, and bin/rails lighthouse:ide_helper writes it out for the IDE (see IDE support).

Example 1 — an argument filter

Argument-builder directives are the simplest. Suppose you want @startsWith:

module App
  module Graphql
    module Directives
      class StartsWith < Lighthouse::GraphQL::Directives::Arguments::Base
        def self.directive_name = 'startsWith'
        def self.definition = 'directive @startsWith(key: String) on ARGUMENT_DEFINITION'

        def handle_builder(relation, value)
          return relation if value.nil?

          relation.where(arel_column(relation).matches("#{value}%"))
        end
      end
    end
  end
end

Arguments::Base gives you column (the key: override or snake_cased arg name) and arel_column(relation).

Example 2 — a field resolver

A field-resolver directive attaches a resolver to a field. The simplest path is to subclass the base resolver and implement define_resolver:

module App
  module Graphql
    module Directives
      class UpcaseDirective < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
        def self.directive_name = 'upcase'
        def self.definition = 'directive @upcase on FIELD_DEFINITION'

        def define_resolver(target_type)
          name = @field_def.name
          target_type.define_singleton_method("resolve_field_#{name}") do |obj, _args, _ctx|
            value = obj.public_send(Lighthouse::Support::Naming.attribute(name))
            value&.upcase
          end
        end
      end
    end
  end
end

Registering

Register your directive once, from your Lighthouse initializer:

# config/initializers/lighthouse.rb
Lighthouse::DirectiveRegistry.register(App::Graphql::Directives::StartsWith)
Lighthouse::DirectiveRegistry.register(App::Graphql::Directives::UpcaseDirective)

To register every directive in a namespace at once, as PHP Lighthouse locates directives by namespace, listen to the RegisterDirectiveNamespaces event. Each class there with a directive_name that is a resolver or an argument builder gets registered when the schema is built:

Lighthouse::Events.listen(Lighthouse::Events::RegisterDirectiveNamespaces) { 'App::Graphql::Directives' }

Either way, it is now usable in your SDL — no other changes:

type Query {
  users(name: String @startsWith): [User!]! @all
}

type User {
  shoutName: String @upcase @rename(attribute: "name")
}

Naming helper

Whenever you map a GraphQL name to a database column or model attribute, route through Lighthouse::Support::Naming so your directive honors the same camelCase→snake_case rule as the rest of the library:

Lighthouse::Support::Naming.attribute('customAttributes') # => "custom_attributes"
Lighthouse::Support::Naming.column('CREATED_AT')          # => "created_at"
Edit this page on GitHub
Prev
Schema Organisation
Next
Events