Commit 0be34617 authored by Alex Kalderimis's avatar Alex Kalderimis

Rename and move writing-resolvers section

parent a6d0f1ba
......@@ -758,6 +758,141 @@ See the [Mutation arguments](#object-identifier-arguments) section.
To limit the amount of queries performed, we can use `BatchLoader`.
### Writing resolvers
Our code should aim to be thin declarative wrappers around finders and services. You can
repeat lists of arguments, or extract them to concerns. Composition is preferred over
inheritance in most cases. Treat resolvers like controllers: resolvers should be a DSL
that compose other application abstractions.
For example:
```ruby
class PostResolver < BaseResolver
type Post.connection_type, null: true
authorize :read_blog
description 'Blog posts, optionally filtered by name'
argument :name, [::GraphQL::STRING_TYPE], required: false, as: :slug
alias_method :blog, :object
def resolve(**args)
PostFinder.new(blog, current_user, args).execute
end
end
```
You should never re-use resolvers directly. Resolvers have a complex life-cycle, with
authorization, readiness and resolution orchestrated by the framework, and at
each stage lazy values can be returned to take advantage of batching
opportunities. Never instantiate a resolver or a mutation in application code.
Instead, the units of code reuse are much the same as in the rest of the
application:
- Finders in queries to look up data.
- Services in mutations to apply operations.
- Loaders (batch-aware finders) specific to queries.
Note that there is never any reason to use batching in a mutation. Mutations are
executed in series, so there are no batching opportunities. All values are
evaluated eagerly as soon as they are requested, so batching is unnecessary
overhead. If you are writing:
- A `Mutation`, feel free to lookup objects directly.
- A `Resolver` or methods on a `BaseObject`, then you want to allow for batching.
### Deriving resolvers (`BaseResolver.single` and `BaseResolver.last`)
For some simple use cases, we can derive resolvers from others.
The main use case for this is one resolver to find all items, and another to
find one specific one. For this, we supply convenience methods:
- `BaseResolver.single`, which constructs a new resolver that selects the first item.
- `BaseResolver.last`, with constructs a resolver that selects the last item.
The correct singular type is inferred from the collection type, so we don't have
to define the `type` here.
Before you make use of these methods, consider if it would be simpler to either:
- Write another resolver that defines its own arguments.
- Write a concern that abstracts out the query.
Using `BaseResolver.single` too freely is an anti-pattern. It can lead to
non-sensical fields, such as a `Project.mergeRequest` field that just returns
the first MR if no arguments are given. Whenever we derive a single resolver
from a collection resolver, it must have more restrictive arguments.
To make this possible, use the `when_single` block to customize the single
resolver. Every `when_single` block must:
- Define (or re-define) at least one argument.
- Make optional filters required.
For example, we can do this by redefining an existing optional argument,
changing its type and making it required:
```ruby
class JobsResolver < BaseResolver
type JobType.connection_type, null: true
authorize :read_pipeline
argument :name, [::GraphQL::STRING_TYPE], required: false
when_single do
argument :name, ::GraphQL::STRING_TYPE, required: true
end
def resolve(**args)
JobsFinder.new(pipeline, current_user, args.compact).execute
end
```
Here we have a simple resolver for getting pipeline jobs. The `name` argument is
optional when getting a list, but required when getting a single job.
If there are multiple arguments, and neither can be made required, we can use
the block to add a ready condition:
```ruby
class JobsResolver < BaseResolver
alias_method :pipeline, :object
type JobType.connection_type, null: true
authorize :read_pipeline
argument :name, [::GraphQL::STRING_TYPE], required: false
argument :id, [::Types::GlobalIDType[::Job]],
required: false,
prepare: ->(ids, ctx) { ids.map(&:model_id) }
when_single do
argument :name, ::GraphQL::STRING_TYPE, required: false
argument :id, ::Types::GlobalIDType[::Job],
required: false
prepare: ->(id, ctx) { id.model_id }
def ready?(**args)
raise ::Gitlab::Graphql::Errors::ArgumentError, 'Only one argument may be provided' unless args.size == 1
end
end
def resolve(**args)
JobsFinder.new(pipeline, current_user, args.compact).execute
end
```
And then we can use these resolver on fields:
```ruby
# In PipelineType
field :jobs, resolver: JobsResolver, description: 'All jobs'
field :job, resolver: JobsResolver.single, description: 'A single job'
```
### Correct use of `Resolver#ready?`
Resolvers have two public API methods as part of the framework: `#ready?(**args)` and `#resolve(**args)`.
......@@ -857,13 +992,13 @@ class, if the arguments are nested). Alternatively, you can consider to add a
### Metadata
When using resolvers, they can and should serve as the SSOT for field metadata.
When using resolvers, they can and should serve as the SSoT for field metadata.
All field options (apart from the field name) can be declared on the resolver.
These include:
- type (this is particularly important, and will soon be mandatory)
- extras
- description
- `type` (this is particularly important, and will soon be mandatory)
- `extras`
- `description`
Example:
......@@ -877,70 +1012,6 @@ module Resolvers
end
```
### Re-using resolvers
You should never re-use resolvers. Resolvers have a complex life-cycle, with
authorization, readiness and resolution orchestrated by the framework, and at
each stage lazy values can be returned to take advantage of batching
opportunities. Never instantiate a resolver or a mutation in application code.
Instead, the units of code reuse are much the same as in the rest of the
application:
- Finders in queries to lookup data
- Services in mutations to apply operations
- Loaders (batch-aware finders) - specific to queries
Note that there is never any reason to use batching in a mutation. Mutations are
executed in series, so there are no batching opportunities. All values are
evaluated eagerly as soon as they are requested, so batching is unnecessary
overhead.
Ideally our code should aim to be thin declarative wrappers around finders and
services. It is OK to repeat lists of arguments, or they can be extracted to
concerns - composition is to be preferred over inheritance in most cases.
For some simple use cases, however, we can derive some resolvers from others.
The main use case for this is one resolver to find all items, and another to
find one specific one. For this, we supply a convenience method
`BaseResolver.single` which constructs a new resolver that selects the first
item.
Using `BaseResolver.single` too freely is an anti-pattern. It can lead to
non-sensical fields, such as a `Project.mergeRequest` field that just returns
the first MR if no arguments are given. Whenever we derive a single resolver
from a collection resolver, it must have more restrictive arguments. For
example:
```ruby
class JobsResolver < BaseResolver
type JobType.connection_type, null: true
authorize :read_pipeline
argument :name, ::GraphQL::STRING_TYPE, required: false
when_single do
argument :name, ::GraphQL::STRING_TYPE, required: true
end
def resolve(name: :not_given)
jobs = object.jobs
jobs = jobs.where(name: name) unless name == :not_given
jobs
end
```
Here we have a simple resolver for getting pipeline jobs. The name argument is
optional when getting a list, but required when getting a single job. We can use
this as follows:
```ruby
# In PipelineType
field :jobs, resolver: JobsResolver, description: 'All jobs'
field :job, resolver: JobsResolver.single, description: 'A single job'
```
### Pass a parent object into a child Presenter
Sometimes you need to access the resolved query parent in a child context to compute fields. Usually the parent is only
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment