Skip to content

Commit

Permalink
restore resolverless key matching for outbound types. (#149)
Browse files Browse the repository at this point in the history
Co-authored-by: Greg MacWilliam <[email protected]>
  • Loading branch information
gmac and Greg MacWilliam authored Jul 3, 2024
1 parent e9460f0 commit 2b2914a
Show file tree
Hide file tree
Showing 7 changed files with 29 additions and 9 deletions.
4 changes: 2 additions & 2 deletions docs/mechanics.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class GraphQlController
variables: params[:variables],
operation_name: params[:operation_name]
)
end
end
end
```

Expand Down Expand Up @@ -345,4 +345,4 @@ type Query {
}
```

In this graph, `Widget` is a merged type without a resolver query in location C. This works because all of its fields are resolvable in other locations; that means location C can provide outbound representations of this type without ever needing to resolve inbound requests for it. Outbound types do still require a shared key field (such as `id` above) that allow them to join with data in other resolver locations (such as `price` above).
In this graph, `Widget` is a merged type without a resolver query in location C. This works because all of its fields are resolvable in other locations; that means location C can provide outbound representations of this type without ever needing to resolve inbound requests for it. Outbound types do still require a shared key field (such as `id` above) that allow them to join with data in other resolver locations (such as `price` above). Support for this pattern is limited to single-field keys, [composite keys](../README.md#composite-type-keys) require a resolver definition.
4 changes: 2 additions & 2 deletions lib/graphql/stitching/composer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -557,15 +557,15 @@ def extract_resolvers(type_name, types_by_location)
argument = if subgraph_field.arguments.size == 1
subgraph_field.arguments.values.first
else
subgraph_field.arguments[key.default_argument_name]
subgraph_field.arguments[key.primitive_name]
end

unless argument
raise CompositionError, "No resolver argument matched for `#{type_name}.#{field_name}`." \
"An argument mapping is required for unmatched names and composite keys."
end

"#{argument.graphql_name}: $.#{key.default_argument_name}"
"#{argument.graphql_name}: $.#{key.primitive_name}"
end

arguments = Resolver.parse_arguments_with_field(arguments_format, subgraph_field)
Expand Down
6 changes: 4 additions & 2 deletions lib/graphql/stitching/composer/validate_resolvers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ def validate_as_resolver(supergraph, type, subgraph_types_by_location, resolvers
end

# All locations of a merged type must include at least one resolver key
supergraph.fields_by_type_and_location[type.graphql_name].each_key do |location|
if resolver_keys.none? { _1.locations.include?(location) }
supergraph.fields_by_type_and_location[type.graphql_name].each do |location, field_names|
has_resolver_key = resolver_keys.any? { _1.locations.include?(location) }
has_primitive_match = resolver_keys.any? { field_names.include?(_1.primitive_name) }
unless has_resolver_key || has_primitive_match
raise ValidationError, "A resolver key is required for `#{type.graphql_name}` in #{location} to join with other locations."
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/graphql/stitching/resolver/keys.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def ==(other)
to_definition == other.to_definition
end

def default_argument_name
def primitive_name
length == 1 ? first.name : nil
end

Expand Down
6 changes: 5 additions & 1 deletion lib/graphql/stitching/supergraph.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,11 @@ def possible_keys_for_type(type_name)
def possible_keys_for_type_and_location(type_name, location)
possible_keys_by_type = @possible_keys_by_type_and_location[type_name] ||= {}
possible_keys_by_type[location] ||= possible_keys_for_type(type_name).select do |key|
key.locations.include?(location)
next true if key.locations.include?(location)

# Outbound-only locations without resolver queries may dynamically match primitive keys
location_fields = fields_by_type_and_location[type_name][location] || GraphQL::Stitching::EMPTY_ARRAY
location_fields.include?(key.primitive_name)
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/graphql/stitching/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module GraphQL
module Stitching
VERSION = "1.4.1"
VERSION = "1.4.2"
end
end
14 changes: 14 additions & 0 deletions test/graphql/stitching/composer/validate_resolvers_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,18 @@ def test_validates_shared_types_across_locations_must_have_matching_compositions
assert compose_definitions({ "a" => a, "b" => b })
end
end

def test_permits_no_resolver_query_for_abstract_types_that_can_be_fully_resolved_elsewhere
a = %|
interface I { id:ID! }
type T implements I { id:ID! name:String }
type Query { a(id:ID!):I @stitch(key: "id") }
|
b = %|
type T { id:ID! }
type Query { b:T }
|

assert compose_definitions({ "a" => a, "b" => b })
end
end

0 comments on commit 2b2914a

Please sign in to comment.