1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
|
---
layout: guide
doc_stub: false
search: true
section: Fields
title: Resolvers
desc: Reusable, extendable resolution logic for complex fields
index: 2
redirect_from:
- /fields/functions
---
A {{ "GraphQL::Schema::Resolver" | api_doc }} is a container for field signature and resolution logic. It can be attached to a field with the `resolver:` keyword:
```ruby
# Use the resolver class to execute this field
field :pending_orders, resolver: PendingOrders
```
Under the hood, {{ "GraphQL::Schema::Mutation" | api_doc }} is a specialized subclass of `Resolver`.
## First, ask yourself ...
Do you really need a `Resolver`? Putting logic in a Resolver has some downsides:
- Since it's coupled to GraphQL, it's harder to test than a plain ol' Ruby object in your app
- Since the base class comes from GraphQL-Ruby, it's subject to upstream changes which may require updates in your code
Here are a few alternatives to consider:
- Put display logic (sorting, filtering, etc.) into a plain ol' Ruby class in your app, and test that class
- Hook up that object with a method, for example:
```ruby
field :recommended_items, [Types::Item], null: false
def recommended_items
ItemRecommendation.new(user: context[:viewer]).items
end
```
- If you have lots of arguments to share, use a class method to generate fields, for example:
```ruby
# Generate a field which returns a filtered, sorted list of items
def self.items_field(name, override_options, &block)
# 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
instance_eval(&block) 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.
- If you need the _same_ logic shared between several objects, consider using a Ruby module and its `self.included` hook, for example:
```ruby
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
```
- If the module approach looks good to you, also consider {% internal_link "Interfaces", "/type_definitions/interfaces" %}. They also share behavior between objects (since they're just modules that get included, after all), and they expose that commonality to clients via introspection.
## When do you really need a resolver?
So, if there are other, better options, why does `Resolver` exist? Here are a few specific advantages:
- __Isolation__. A `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.
- __Complex Schema Generation__. `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.
## Using `resolver`
Use the base resolver class:
```ruby
module Resolvers
class RecommendedItems < BaseResolver
type [Types::Item], null: false
description "Items this user might like"
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:
```ruby
class Types::User < Types::BaseObject
field :recommended_items, resolver: Resolvers::RecommendedItems
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.
### Nesting resolvers of the same type
You may run into cyclical loading issues when using a resolver within the definition of the type the resolver returns e.g.
```ruby
# 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 < BaseResolver
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:
```ruby
module Resolvers
class TasksResolver < BaseResolver
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.
# Extensions
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.
```ruby
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
```
|