Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Field Extensions (second try) #1758

Merged
merged 17 commits into from
Aug 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions guides/type_definitions/field_extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
layout: guide
doc_stub: false
search: true
section: Type Definitions
title: Field Extensions
desc: Programmatically modify field configuration and resolution
index: 10
class_based_api: true
---

{{ "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 }}).

### Making a new extension

Field extensions are subclasses of {{ "GraphQL::Schema::FieldExtension" | api_doc }}:

```ruby
class MyExtension < GraphQL::Schema::FieldExtension
end
```

### Using an extension

Defined extensions can be added to fields using the `extensions: [...]` option or the `extension(...)` method:

```ruby
field :name, String, null: false, extensions: [UpcaseExtension]
# or:
field :description, String, null: false do
extension(UpcaseExtension)
end
```

See below for how extensions may modify fields.

### Modifying field configuration

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:

```ruby
class SearchableExtension < GraphQL::Schema::FieldExtension
def apply
# add an argument to this field:
field.argument(:query, String, required: false, description: "A search query")
end
end
```

This way, an extension can encapsulate a behavior requiring several configuration options.

### Modifying field execution

Extensions have two hooks that wrap field resolution. Since GraphQL-Ruby supports deferred execution, these hooks _might not_ be called back-to-back.

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.

After resolution, {{ "GraphQL::Schema::FieldExtension#after_resolve" | api_doc }} is called. Whatever that method returns will be used as the field's return value.

See the linked API docs for the parameters of those methods.

#### Execution "memo"

One parameter to `after_resolve` deserves special attention: `memo:`. `before_resolve` _may_ yield a third value. For example:

```ruby
def before_resolve(object:, arguments:, **rest)
# yield the current time as `memo`
yield(object, arguments, Time.now.to_i)
end
```

If a third value is yielded, it will be passed to `after_resolve` as `memo:`, for example:

```ruby
def after_resolve(value:, memo:, **rest)
puts "Elapsed: #{Time.now.to_i - memo}"
# Return the original value
value
end
```

This allows the `before_resolve` hook to pass data to `after_resolve`.

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.)

### Extension options

The `extension(...)` method takes an optional second argument, for example:

```ruby
extension(LimitExtension, limit: 20)
```

In this case, `{limit: 20}` will be passed as `options:` to `#initialize` and `options[:limit]` will be `20`.

For example, options can be used for modifying execution:

```ruby
def after_resolve(value:, **rest)
# Apply the limit from the options
value.limit(options[:limit])
end
```
4 changes: 3 additions & 1 deletion lib/graphql/relay/connection_instrumentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ def self.default_arguments
# - Merging in the default arguments
# - Transforming its resolve function to return a connection object
def self.instrument(type, field)
if field.connection?
# Don't apply the wrapper to class-based fields, since they
# use Schema::Field::ConnectionFilter
if field.connection? && !field.metadata[:type_class]
connection_arguments = default_arguments.merge(field.arguments)
original_resolve = field.resolve_proc
original_lazy_resolve = field.lazy_resolve_proc
Expand Down
9 changes: 5 additions & 4 deletions lib/graphql/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,24 @@
require "graphql/schema/warden"
require "graphql/schema/build_from_definition"


require "graphql/schema/member"
require "graphql/schema/wrapper"
require "graphql/schema/list"
require "graphql/schema/non_null"
require "graphql/schema/argument"
require "graphql/schema/enum_value"
require "graphql/schema/enum"
require "graphql/schema/field_extension"
require "graphql/schema/field"
require "graphql/schema/input_object"
require "graphql/schema/interface"
require "graphql/schema/scalar"
require "graphql/schema/object"
require "graphql/schema/union"

require "graphql/schema/resolver"
require "graphql/schema/mutation"
require "graphql/schema/relay_classic_mutation"
require "graphql/schema/object"
require "graphql/schema/scalar"
require "graphql/schema/union"

module GraphQL
# A GraphQL schema which may be queried with {GraphQL::Query}.
Expand Down
Loading