Skip to content

Commit 4675dab

Browse files
committed
put fomatting hooks onto client.
1 parent 506f14e commit 4675dab

19 files changed

+260
-157
lines changed

docs/composing_a_supergraph.md

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,7 @@ client = GraphQL::Stitching::Client.new(
4242
mutation_name: "Mutation",
4343
subscription_name: "Subscription",
4444
visibility_profiles: nil, # ["public", "private", ...]
45-
description_merger: ->(values_by_location, info) { values_by_location.values.join("\n") },
46-
deprecation_merger: ->(values_by_location, info) { values_by_location.values.first },
47-
default_value_merger: ->(values_by_location, info) { values_by_location.values.first },
48-
directive_kwarg_merger: ->(values_by_location, info) { values_by_location.values.last },
49-
root_entrypoints: {},
45+
root_entrypoints: { "Query.ping" => "pings" },
5046
},
5147
locations: {
5248
# ...
@@ -62,47 +58,49 @@ client = GraphQL::Stitching::Client.new(
6258

6359
- **`visibility_profiles:`**, an array of [visibility profiles](./visibility.md) that the supergraph responds to.
6460

65-
- **`description_merger:`**, a [value merger function](#value-merger-functions) for merging element description strings from across locations.
66-
67-
- **`deprecation_merger:`**, a [value merger function](#value-merger-functions) for merging element deprecation strings from across locations.
68-
69-
- **`default_value_merger:`**, a [value merger function](#value-merger-functions) for merging argument default values from across locations.
70-
71-
- **`directive_kwarg_merger:`**, a [value merger function](#value-merger-functions) for merging directive keyword arguments from across locations.
72-
7361
- **`root_entrypoints:`**, a hash of root field names mapped to their entrypoint locations, see [overlapping root fields](#overlapping-root-fields) below.
7462

75-
#### Value merger functions
63+
### Value mergers
7664

77-
Static data values such as element descriptions and directive arguments must also merge across locations. By default, the first non-null value encountered for a given element attribute is used. A value merger function may customize this process by selecting a different value or computing a new one:
65+
Static data values such as element descriptions and directive arguments must also merge across locations. By default, the first non-null value encountered for a given element property is used. Value merger methods can be customized by defining them on your own `Client` class:
7866

7967
```ruby
80-
join_values_merger = ->(values_by_location, info) { values_by_location.values.compact.join("\n") }
81-
82-
client = GraphQL::Stitching::Client.new(
83-
composer_options: {
84-
description_merger: join_values_merger,
85-
deprecation_merger: join_values_merger,
86-
default_value_merger: join_values_merger,
87-
directive_kwarg_merger: join_values_merger,
88-
},
89-
)
68+
class MyClient < GraphQL::Stitching::Client
69+
def merge_descriptions(values_by_location, info)
70+
# return a merged element description string from across locations...
71+
values_by_location.each_value.join("\n")
72+
end
73+
74+
def merge_deprecations(values_by_location, info)
75+
# return a merged element deprecation string from across locations...
76+
end
77+
78+
def merge_default_values(values_by_location, info)
79+
# return a merged argument default value from across locations...
80+
end
81+
82+
def merge_kwargs(values_by_location, info)
83+
# return a merged directive keyword argument from across locations...
84+
end
85+
end
86+
87+
client = MyClient.new(locations: ...)
9088
```
9189

92-
A merger function receives `values_by_location` and `info` arguments; these provide possible values keyed by location and info about where in the schema these values were encountered:
90+
All merge functions receive `values_by_location` and `info` arguments; these provide possible values keyed by location and info about where in the schema these values were encountered. For example:
9391

9492
```ruby
9593
values_by_location = {
96-
"users" => "A fabulous data type.",
97-
"products" => "An excellent data type.",
94+
"users" => "A fabulous data type description.",
95+
"products" => "An excellent data type description.",
9896
}
9997

100-
info = {
98+
info = GraphQL::Stitching::Formatter::Info.new(
10199
type_name: "Product",
102100
# field_name: ...,
103101
# argument_name: ...,
104102
# directive_name: ...,
105-
}
103+
)
106104
```
107105

108106
### Cached supergraphs

docs/error_handling.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,28 @@ Failed stitching requests can be tricky to debug because it's not always obvious
44

55
### Supergraph errors
66

7-
When exceptions happen while executing requests within the stitching layer, they will be rescued by the stitching client and trigger an `on_error` hook. You should add your stack's error reporting here and return a formatted error message to appear in [GraphQL errors](https://spec.graphql.org/June2018/#sec-Errors) for the request.
7+
When exceptions happen while executing requests within the stitching layer, they will be rescued by the stitching client and trigger an `on_error` hook. You can add your stack's error reporting here:
88

99
```ruby
1010
client = GraphQL::Stitching::Client.new(locations: { ... })
1111
client.on_error do |request, err|
1212
# log the error
1313
Bugsnag.notify(err)
14+
end
15+
```
1416

15-
# return a formatted message for the public response
16-
"Whoops, please contact support abount request '#{request.context[:request_id]}'"
17+
To modify the format of returned error messages that appear in [GraphQL errors](https://spec.graphql.org/June2018/#sec-Errors), extend `Client` and define your own `build_graphql_error` method:
18+
19+
```ruby
20+
class MyClient < GraphQL::Stitching::Client
21+
def build_graphql_error(request, err)
22+
graphql_error = super(request, err)
23+
graphql_error["message"] << " Contact support about Request ID #{request.context[:request_id]}"
24+
graphql_error
25+
end
1726
end
1827

19-
# Result:
20-
# { "errors" => [{ "message" => "Whoops, please contact support abount request '12345'" }] }
28+
client = MyClient.new(locations: { ... })
2129
```
2230

2331
### Subgraph errors

lib/graphql/stitching.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def supports_visibility?
7171
end
7272
end
7373

74+
require_relative "stitching/formatter"
7475
require_relative "stitching/directives"
7576
require_relative "stitching/supergraph"
7677
require_relative "stitching/client"

lib/graphql/stitching/client.rb

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ module Stitching
77
# Client is an out-of-the-box helper that assembles all
88
# stitching components into a workflow that executes requests.
99
class Client
10+
include Formatter
11+
1012
class << self
1113
def from_definition(schema, executables:)
1214
new(supergraph: Supergraph.from_definition(schema, executables: executables))
@@ -30,6 +32,7 @@ def initialize(locations: nil, supergraph: nil, composer_options: {})
3032
elsif supergraph
3133
supergraph
3234
else
35+
composer_options = { formatter: self }.merge!(composer_options)
3336
composer = Composer.new(**composer_options)
3437
composer.perform(locations)
3538
end
@@ -50,16 +53,16 @@ def execute(raw_query = nil, query: nil, variables: nil, operation_name: nil, co
5053

5154
if validate
5255
validation_errors = request.validate
53-
return error_result(request, validation_errors) unless validation_errors.empty?
56+
return error_result(request, validation_errors.map(&:to_h)) unless validation_errors.empty?
5457
end
5558

5659
load_plan(request)
5760
request.execute
5861
rescue GraphQL::ParseError, GraphQL::ExecutionError => e
59-
error_result(request, [e])
62+
error_result(request, [e.to_h])
6063
rescue StandardError => e
61-
custom_message = @on_error.call(request, e) if @on_error
62-
error_result(request, [{ "message" => custom_message || "An unexpected error occured." }])
64+
@on_error.call(request, e) if @on_error
65+
error_result(request, [build_graphql_error(request, e)])
6366
end
6467

6568
def on_cache_read(&block)
@@ -77,33 +80,33 @@ def on_error(&block)
7780
@on_error = block
7881
end
7982

83+
def read_cached_plan(request)
84+
if @on_cache_read && (plan_json = @on_cache_read.call(request))
85+
Plan.from_json(JSON.parse(plan_json))
86+
end
87+
end
88+
89+
def write_cached_plan(request, plan)
90+
@on_cache_write.call(request, JSON.generate(plan.as_json)) if @on_cache_write
91+
end
92+
8093
private
8194

8295
def load_plan(request)
83-
if @on_cache_read && plan_json = @on_cache_read.call(request)
84-
plan = Plan.from_json(JSON.parse(plan_json))
96+
plan = read_cached_plan(request)
8597

86-
# only use plans referencing current resolver versions
87-
if plan.ops.all? { |op| !op.resolver || @supergraph.resolvers_by_version[op.resolver] }
88-
return request.plan(plan)
89-
end
98+
# only use plans referencing current resolver versions
99+
if plan && plan.ops.all? { |op| !op.resolver || @supergraph.resolvers_by_version[op.resolver] }
100+
return request.plan(plan)
90101
end
91102

92-
plan = request.plan
93-
94-
if @on_cache_write
95-
@on_cache_write.call(request, JSON.generate(plan.as_json))
103+
request.plan.tap do |plan|
104+
write_cached_plan(request, plan)
96105
end
97-
98-
plan
99106
end
100107

101-
def error_result(request, errors)
102-
public_errors = errors.map! do |e|
103-
e.is_a?(Hash) ? e : e.to_h
104-
end
105-
106-
GraphQL::Query::Result.new(query: request, values: { "errors" => public_errors })
108+
def error_result(request, graphql_errors)
109+
GraphQL::Query::Result.new(query: request, values: { "errors" => graphql_errors })
107110
end
108111
end
109112
end

lib/graphql/stitching/composer.rb

Lines changed: 14 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,6 @@ class Composer
2020
t.get_field("f").get_argument("a").default_value
2121
end
2222

23-
# @api private
24-
BASIC_VALUE_MERGER = ->(values_by_location, _info) { values_by_location.values.find { !_1.nil? } }
25-
26-
# @api private
27-
VISIBILITY_PROFILES_MERGER = ->(values_by_location, _info) { values_by_location.values.reduce(:&) }
28-
2923
# @api private
3024
COMPOSITION_VALIDATORS = [
3125
ValidateInterfaces,
@@ -52,22 +46,14 @@ def initialize(
5246
mutation_name: "Mutation",
5347
subscription_name: "Subscription",
5448
visibility_profiles: [],
55-
description_merger: nil,
56-
deprecation_merger: nil,
57-
default_value_merger: nil,
58-
directive_kwarg_merger: nil,
59-
root_field_location_selector: nil,
60-
root_entrypoints: nil
49+
root_entrypoints: nil,
50+
formatter: nil
6151
)
6252
@query_name = query_name
6353
@mutation_name = mutation_name
6454
@subscription_name = subscription_name
65-
@description_merger = description_merger || BASIC_VALUE_MERGER
66-
@deprecation_merger = deprecation_merger || BASIC_VALUE_MERGER
67-
@default_value_merger = default_value_merger || BASIC_VALUE_MERGER
68-
@directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER
69-
@root_field_location_selector = root_field_location_selector
7055
@root_entrypoints = root_entrypoints || {}
56+
@formatter = formatter || Formatter::Default
7157

7258
@field_map = {}
7359
@resolver_map = {}
@@ -424,12 +410,12 @@ def build_merged_arguments(type_name, members_by_location, owner, field_name: ni
424410
end
425411

426412
unless default_values_by_location.empty?
427-
kwargs[:default_value] = @default_value_merger.call(default_values_by_location, {
413+
kwargs[:default_value] = @formatter.merge_default_values(default_values_by_location, Formatter::Info.new(
428414
type_name: type_name,
429415
field_name: field_name,
430416
argument_name: argument_name,
431417
directive_name: directive_name,
432-
}.tap(&:compact!))
418+
))
433419
end
434420

435421
type = merge_value_types(type_name, value_types, argument_name: argument_name, field_name: field_name)
@@ -458,7 +444,7 @@ def build_merged_directives(type_name, members_by_location, owner, field_name: n
458444
end
459445

460446
directives_by_name_location.each do |directive_name, directives_by_location|
461-
kwarg_merger = @directive_kwarg_merger
447+
kwarg_formatter = @formatter
462448
directive_class = @schema_directives[directive_name]
463449
next unless directive_class
464450

@@ -481,19 +467,19 @@ def build_merged_directives(type_name, members_by_location, owner, field_name: n
481467

482468
if (profiles = kwarg_values_by_name_location["profiles"])
483469
@visibility_profiles.merge(profiles.each_value.reduce(&:|))
484-
kwarg_merger = VISIBILITY_PROFILES_MERGER
470+
kwarg_formatter = Formatter::Default
485471
end
486472
end
487473

488474
kwargs = kwarg_values_by_name_location.each_with_object({}) do |(kwarg_name, kwarg_values_by_location), memo|
489-
memo[kwarg_name.to_sym] = kwarg_merger.call(kwarg_values_by_location, {
475+
memo[kwarg_name.to_sym] = kwarg_formatter.merge_kwargs(kwarg_values_by_location, Formatter::Info.new(
490476
type_name: type_name,
491477
field_name: field_name,
492478
argument_name: argument_name,
493479
enum_value: enum_value,
494480
directive_name: directive_name,
495481
kwarg_name: kwarg_name,
496-
}.tap(&:compact!))
482+
))
497483
end
498484

499485
owner.directive(directive_class, **kwargs)
@@ -537,24 +523,24 @@ def merge_value_types(type_name, subgraph_types, field_name: nil, argument_name:
537523
# @!visibility private
538524
def merge_descriptions(type_name, members_by_location, field_name: nil, argument_name: nil, enum_value: nil)
539525
strings_by_location = members_by_location.each_with_object({}) { |(l, m), memo| memo[l] = m.description }
540-
@description_merger.call(strings_by_location, {
526+
@formatter.merge_descriptions(strings_by_location, Formatter::Info.new(
541527
type_name: type_name,
542528
field_name: field_name,
543529
argument_name: argument_name,
544530
enum_value: enum_value,
545-
}.tap(&:compact!))
531+
))
546532
end
547533

548534
# @!scope class
549535
# @!visibility private
550536
def merge_deprecations(type_name, members_by_location, field_name: nil, argument_name: nil, enum_value: nil)
551537
strings_by_location = members_by_location.each_with_object({}) { |(l, m), memo| memo[l] = m.deprecation_reason }
552-
@deprecation_merger.call(strings_by_location, {
538+
@formatter.merge_deprecations(strings_by_location, Formatter::Info.new(
553539
type_name: type_name,
554540
field_name: field_name,
555541
argument_name: argument_name,
556542
enum_value: enum_value,
557-
}.tap(&:compact!))
543+
))
558544
end
559545

560546
# @!scope class
@@ -629,15 +615,7 @@ def select_root_field_locations(schema)
629615
next unless root_field_locations.length > 1
630616

631617
root_field_path = "#{root_type.graphql_name}.#{root_field_name}"
632-
target_location = if @root_field_location_selector && @root_entrypoints.empty?
633-
Warning.warn("Composer option `root_field_location_selector` is deprecated and will be removed.")
634-
@root_field_location_selector.call(root_field_locations, {
635-
type_name: root_type.graphql_name,
636-
field_name: root_field_name,
637-
})
638-
else
639-
@root_entrypoints[root_field_path] || root_field_locations.last
640-
end
618+
target_location = @root_entrypoints[root_field_path] || root_field_locations.last
641619

642620
unless root_field_locations.include?(target_location)
643621
raise CompositionError, "Invalid `root_entrypoints` configuration: `#{root_field_path}` has no `#{target_location}` location."

0 commit comments

Comments
 (0)