Skip to content

Commit d483c91

Browse files
author
Robert Mosolgo
authored
Merge pull request #1758 from rmosolgo/field-filter-instances
Field Extensions (second try)
2 parents 1b24d01 + 46db5a9 commit d483c91

File tree

15 files changed

+562
-139
lines changed

15 files changed

+562
-139
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
---
2+
layout: guide
3+
doc_stub: false
4+
search: true
5+
section: Type Definitions
6+
title: Field Extensions
7+
desc: Programmatically modify field configuration and resolution
8+
index: 10
9+
class_based_api: true
10+
---
11+
12+
{{ "GraphQL::Schema::FieldExtension" | api_doc }} 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" | api_doc }}).
13+
14+
### Making a new extension
15+
16+
Field extensions are subclasses of {{ "GraphQL::Schema::FieldExtension" | api_doc }}:
17+
18+
```ruby
19+
class MyExtension < GraphQL::Schema::FieldExtension
20+
end
21+
```
22+
23+
### Using an extension
24+
25+
Defined extensions can be added to fields using the `extensions: [...]` option or the `extension(...)` method:
26+
27+
```ruby
28+
field :name, String, null: false, extensions: [UpcaseExtension]
29+
# or:
30+
field :description, String, null: false do
31+
extension(UpcaseExtension)
32+
end
33+
```
34+
35+
See below for how extensions may modify fields.
36+
37+
### Modifying field configuration
38+
39+
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:
40+
41+
```ruby
42+
class SearchableExtension < GraphQL::Schema::FieldExtension
43+
def apply
44+
# add an argument to this field:
45+
field.argument(:query, String, required: false, description: "A search query")
46+
end
47+
end
48+
```
49+
50+
This way, an extension can encapsulate a behavior requiring several configuration options.
51+
52+
### Modifying field execution
53+
54+
Extensions have two hooks that wrap field resolution. Since GraphQL-Ruby supports deferred execution, these hooks _might not_ be called back-to-back.
55+
56+
First, {{ "GraphQL::Schema::FieldExtension#before_resolve" | api_doc }} is called. `before_resolve` should `yield(object, arguments)` to continue execution. If it doesn't `yield`, then the field won't resolve, and the methods return value will be returned to GraphQL instead.
57+
58+
After resolution, {{ "GraphQL::Schema::FieldExtension#after_resolve" | api_doc }} is called. Whatever that method returns will be used as the field's return value.
59+
60+
See the linked API docs for the parameters of those methods.
61+
62+
#### Execution "memo"
63+
64+
One parameter to `after_resolve` deserves special attention: `memo:`. `before_resolve` _may_ yield a third value. For example:
65+
66+
```ruby
67+
def before_resolve(object:, arguments:, **rest)
68+
# yield the current time as `memo`
69+
yield(object, arguments, Time.now.to_i)
70+
end
71+
```
72+
73+
If a third value is yielded, it will be passed to `after_resolve` as `memo:`, for example:
74+
75+
```ruby
76+
def after_resolve(value:, memo:, **rest)
77+
puts "Elapsed: #{Time.now.to_i - memo}"
78+
# Return the original value
79+
value
80+
end
81+
```
82+
83+
This allows the `before_resolve` hook to pass data to `after_resolve`.
84+
85+
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.)
86+
87+
### Extension options
88+
89+
The `extension(...)` method takes an optional second argument, for example:
90+
91+
```ruby
92+
extension(LimitExtension, limit: 20)
93+
```
94+
95+
In this case, `{limit: 20}` will be passed as `options:` to `#initialize` and `options[:limit]` will be `20`.
96+
97+
For example, options can be used for modifying execution:
98+
99+
```ruby
100+
def after_resolve(value:, **rest)
101+
# Apply the limit from the options
102+
value.limit(options[:limit])
103+
end
104+
```

lib/graphql/relay/connection_instrumentation.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ def self.default_arguments
3232
# - Merging in the default arguments
3333
# - Transforming its resolve function to return a connection object
3434
def self.instrument(type, field)
35-
if field.connection?
35+
# Don't apply the wrapper to class-based fields, since they
36+
# use Schema::Field::ConnectionFilter
37+
if field.connection? && !field.metadata[:type_class]
3638
connection_arguments = default_arguments.merge(field.arguments)
3739
original_resolve = field.resolve_proc
3840
original_lazy_resolve = field.lazy_resolve_proc

lib/graphql/schema.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,24 @@
1919
require "graphql/schema/warden"
2020
require "graphql/schema/build_from_definition"
2121

22-
2322
require "graphql/schema/member"
2423
require "graphql/schema/wrapper"
2524
require "graphql/schema/list"
2625
require "graphql/schema/non_null"
2726
require "graphql/schema/argument"
2827
require "graphql/schema/enum_value"
2928
require "graphql/schema/enum"
29+
require "graphql/schema/field_extension"
3030
require "graphql/schema/field"
3131
require "graphql/schema/input_object"
3232
require "graphql/schema/interface"
33+
require "graphql/schema/scalar"
34+
require "graphql/schema/object"
35+
require "graphql/schema/union"
36+
3337
require "graphql/schema/resolver"
3438
require "graphql/schema/mutation"
3539
require "graphql/schema/relay_classic_mutation"
36-
require "graphql/schema/object"
37-
require "graphql/schema/scalar"
38-
require "graphql/schema/union"
3940

4041
module GraphQL
4142
# A GraphQL schema which may be queried with {GraphQL::Query}.

0 commit comments

Comments
 (0)