Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-13 06:09:15 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-13 06:09:15 +0300
commita7364a04743746eab8b84c47900fbd04e1d45253 (patch)
tree6742fc3299a7f62f6c385fcd7edc428da93f3793 /doc/development/api_graphql_styleguide.md
parentcf58163b565da802f152cc8f2d635fde3ef001ab (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'doc/development/api_graphql_styleguide.md')
-rw-r--r--doc/development/api_graphql_styleguide.md175
1 files changed, 173 insertions, 2 deletions
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index 6e44e139e1e..6760a2efb7f 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -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
+```
+
+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)`.
@@ -855,7 +990,29 @@ To avoid duplicated argument definitions, you can place these arguments in a reu
class, if the arguments are nested). Alternatively, you can consider to add a
[helper resolver method](https://gitlab.com/gitlab-org/gitlab/-/issues/258969).
-## Pass a parent object into a child Presenter
+### 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`
+
+Example:
+
+```ruby
+module Resolvers
+ MyResolver < BaseResolver
+ type Types::MyType, null: true
+ extras [:lookahead]
+ description 'Retrieve a single MyType'
+ end
+end
+```
+
+### 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
available in the `Resolver` class as `parent`.
@@ -870,7 +1027,7 @@ To find the parent object in your `Presenter` class:
end
```
-1. Declare that your fields require the `parent` field context. For example:
+1. Declare that your resolver or fields require the `parent` field context. For example:
```ruby
# in ChildType
@@ -878,6 +1035,14 @@ To find the parent object in your `Presenter` class:
method: :my_computing_method,
extras: [:parent], # Necessary
description: 'My field description'
+
+ field :resolver_field, resolver: SomeTypeResolver
+
+ # In SomeTypeResolver
+
+ extras [:parent]
+ type SomeType, null: true
+ description 'My field description'
```
1. Declare your field's method in your Presenter class and have it accept the `parent` keyword argument.
@@ -889,6 +1054,12 @@ This argument contains the parent **GraphQL context**, so you have to access the
def my_computing_method(parent:)
# do something with `parent[:parent_object]` here
end
+
+ # In SomeTypeResolver
+
+ def resolve(parent:)
+ # ...
+ end
```
For an example of real-world use, check [this MR that added `scopedPath` and `scopedUrl` to `IterationPresenter`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39543)