A GraphQL::Schema::Resolver
is a container for field signature and resolution logic. It can be attached to a field with the resolver:
keyword:
# Use the resolver class to execute this field
field :pending_orders, resolver: PendingOrders
Under the hood, GraphQL::Schema::Mutation
is a specialized subclass of Resolver
.
Do you really need a Resolver
? Putting logic in a Resolver has some downsides:
Here are a few alternatives to consider:
field :recommended_items, [Types::Item], null: false
def recommended_items
ItemRecommendation.new(user: context[:viewer]).items
end
# Generate a field which returns a filtered, sorted list of items
def self.items_field(name, override_options)
# Prepare options
default_field_options = { type: [Types::Item], null: false }
field_options = default_field_options.merge(override_options)
# Create the field
field(name, field_options) do
argument :order_by, Types::ItemOrder, required: false
argument :category, Types::ItemCategory, required: false
# Allow an override block to add more arguments
yield self if block_given?
end
end
# Then use the generator to create a field:
items_field(:recommended_items) do |field|
field.argument :similar_to_product_id, ID, required: false
end
# Implement the field
def recommended_items
# ...
end
As a matter of code organization, that class method could be put in a module and shared between different classes that need it.
self.included
hook, for example:module HasRecommendedItems
def self.included(child_class)
# attach the field here
child_class.field(:recommended_items, [Types::Item], null: false)
end
# then implement the field
def recommended_items
# ...
end
end
# Add the field to some objects:
class Types::User < BaseObject
include HasRecommendedItems # adds the field
end
So, if there are other, better options, why does Resolver
exist? Here are a few specific advantages:
Resolver
is instantiated for each call to the field, so its instance variables are private to that object. If you need to use instance variables for some reason, this helps. You have a guarantee that those values won’t hang around when the work is done.RelayClassicMutation
(which is a Resolver
subclass) generates input types and return types for each mutation. Using a Resolver
class makes it easier to implement, share and extend this code generation logic.resolver
To add resolvers to your project, make a base class:
# app/graphql/resolvers/base.rb
module Resolvers
class Base < GraphQL::Schema::Resolver
# if you have a custom argument class, you can attach it:
argument_class Arguments::Base
end
end
Then, extend it as needed:
module Resolvers
class RecommendedItems < Resolvers::Base
type [Types::Item], null: false
argument :order_by, Types::ItemOrder, required: false
argument :category, Types::ItemCategory, required: false
def resolve(order_by: nil, category: nil)
# call your application logic here:
recommendations = ItemRecommendation.new(
viewer: context[:viewer],
recommended_for: object,
order_by: order_by,
category: category,
)
# return the list of items
recommendations.items
end
end
end
And attach it to your field:
class Types::User < Types::BaseObject
field :recommended_items,
resolver: Resolvers::RecommendedItems,
description: "Items this user might like"
end
Since the Resolver
lifecycle is managed by the GraphQL runtime, the best way to test it is to execute GraphQL queries and check the results.
You may run into cyclical loading issues when using a resolver within the definition of the type the resolver returns e.g.
# app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
field :tasks, resolver: Resolvers::TasksResolver
end
end
# app/graphql/types/task_type.rb
module Types
class TaskType < Types::BaseObject
field :title, String, null: false
field :tasks, resolver: Resolvers::TasksResolver
end
end
# app/graphql/resolvers/tasks_resolver.rb
module Resolvers
class TasksResolver < GraphQL::Schema::Resolver
type [Types::TaskType], null: false
def resolve
[]
end
end
end
The above can produce the following error: Failed to build return type for Task.tasks from nil: Unexpected type input: (NilClass)
.
A simple solution is to express the type as a string in the resolver:
module Resolvers
class TasksResolver < GraphQL::Schema::Resolver
type "[Types::TaskType]", null: false
def resolve
[]
end
end
end
In doing so, you can defer the loading of the type class until the nested resolver has already been loaded.
In cases when you want your resolvers to add some extensions to the field they resolve, you can use extension
method, which accepts extension class and options. Multiple extensions can be configured for a single resolver.
class GreetingExtension < GraphQL::Schema::FieldExtension
def resolve(object:, arguments:, **rest)
name = yield(object, arguments)
"#{options[:greeting]}, #{name}!"
end
end
class ResolverWithExtension < BaseResolver
type String, null: false
extension GreetingExtension, greeting: "Hi"
def resolve
"Robert"
end
end