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
end

Using 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)
end

See 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
end

This 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")
end

Additionally, 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 is called. Whatever that method returns will be used as the field’s return value.

See the linked API docs for the parameters of those methods.

Execution “memo”

One parameter to after_resolve deserves special attention: memo:. resolve may yield a third value. For example:

def resolve(object:, arguments:, **rest)
  # yield the current time as `memo`
  yield(object, arguments, Time.now.to_i)
end

If a third value is yielded, it will be passed to after_resolve as memo:, for example:

def after_resolve(value:, memo:, **rest)
  puts "Elapsed: #{Time.now.to_i - memo}"
  # Return the original value
  value
end

This allows the resolve hook to pass data to after_resolve.

Instance variables may not be used because, in a given GraphQL query, the same field may be resolved several times concurrently, and that would result in overriding the instance variable in an unpredictable way. (In fact, extensions are frozen to prevent instance variable writes.)

Extension options

The extension(...) method takes an optional second argument, for example:

extension(LimitExtension, limit: 20)

In this case, {limit: 20} will be passed as options: to #initialize and options[:limit] will be 20.

For example, options can be used for modifying execution:

def after_resolve(value:, **rest)
  # Apply the limit from the options, a readable attribute on the class
  value.limit(options[:limit])
end

If you use the extensions: [...] option, you can pass options using a hash:

field :name, String, null: false, extensions: [LimitExtension => { limit: 20 }]

Using extras

Extensions can have the same extras as fields (see Extra Field Metadata). Add them by calling extras in the class definition:

class MyExtension < GraphQL::Schema::FieldExtension
  extras [:ast_node, :errors, ...]
end

Any configured extras will be present in the given arguments, but removed before the field is resolved. (However, extras from any extension will be present in arguments for all extensions.)

Adding an extension by default

If you want to apply an extension to all your fields, you can do this in your BaseField’s def initialize, for example:

class Types::BaseField < GraphQL::Schema::Field
  def initialize(*args, **kwargs, &block)
    super
    # Add this to all fields based on this class:
    extension(MyDefaultExtension)
  end
end

You can also conditionally apply extensions in def initialize by adding keywords to the method definition, for example:

class Types::BaseField < GraphQL::Schema::Field
  # @param custom_extension [Boolean] if false, `MyCustomExtension` won't be added
  # @example skipping `MyCustomExtension`
  #   field :no_extension, String, custom_extension: false
  def initialize(*args, custom_extension: true, **kwargs, &block)
    super(*args, **kwargs, &block)
    # Don't apply this extension if the field is configured with `custom_extension: false`:
    if custom_extension
      extension(MyCustomExtensions)
    end
  end
end