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
).
Field extensions are subclasses of GraphQL::Schema::FieldExtension
:
class MyExtension < GraphQL::Schema::FieldExtension
end
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.
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.
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.)
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 Promise
s 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.
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.)
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 }]
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.)
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