Skip to content

Commit c9d2933

Browse files
committed
fix(InternalRepresentation) keep children separate by static type
1 parent f3171c1 commit c9d2933

File tree

8 files changed

+160
-48
lines changed

8 files changed

+160
-48
lines changed

lib/graphql/analysis/analyze_query.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ def analyze_query(query, analyzers)
3232
def reduce_node(irep_node, reducer_states)
3333
visit_analyzers(:enter, irep_node, reducer_states)
3434

35-
irep_node.children.each do |name, child_irep_node|
36-
reduce_node(child_irep_node, reducer_states)
35+
irep_node.typed_children.each do |type_defn, children|
36+
children.each do |name, child_irep_node|
37+
reduce_node(child_irep_node, reducer_states)
38+
end
3739
end
3840

3941
visit_analyzers(:leave, irep_node, reducer_states)

lib/graphql/internal_representation/node.rb

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,22 @@
33
module GraphQL
44
module InternalRepresentation
55
class Node
6-
def initialize(parent:, ast_node: nil, return_type: nil, name: nil, definition_name: nil, definitions: {}, children: {}, spreads: [], directives: Set.new, included: true)
7-
# Make sure these are kept in sync with #dup
6+
def initialize(parent:, ast_node: nil, return_type: nil, name: nil, definition_name: nil, definitions: {}, spreads: [], directives: Set.new, included: true, typed_children: Hash.new {|h, k| h[k] = {} })
87
@ast_node = ast_node
98
@return_type = return_type
109
@name = name
1110
@definition_name = definition_name
1211
@parent = parent
1312
@definitions = definitions
14-
@children = children
1513
@spreads = spreads
1614
@directives = directives
1715
@included = included
16+
@typed_children = typed_children
1817
end
1918

19+
# @return [Hash{GraphQL::BaseType => Hash{String => Node}] Children for each type condition
20+
attr_reader :typed_children
21+
2022
# Note: by the time this gets out of the Rewrite phase, this will be empty -- it's emptied out when fragments are merged back in
2123
# @return [Array<GraphQL::InternalRepresentation::Node>] Fragment names that were spread in this node
2224
attr_reader :spreads
@@ -56,8 +58,15 @@ def initialize(parent:, ast_node: nil, return_type: nil, name: nil, definition_n
5658
# @return [GraphQL::BaseType]
5759
attr_reader :return_type
5860

59-
# @return [Array<GraphQL::Query::Node>]
60-
attr_reader :children
61+
# @deprecated Use {#typed_children} instead
62+
# @return [Hash{String => Node}]
63+
def children
64+
if typed_children.size > 1
65+
raise("This node has children of different types, use #typed_children")
66+
else
67+
typed_children.values.first || {}
68+
end
69+
end
6170

6271
# @return [Boolean] false if every field for this node included `@skip(if: true)`
6372
attr_accessor :included
@@ -97,8 +106,12 @@ def path
97106
def inspect(indent = 0)
98107
own_indent = " " * indent
99108
self_inspect = "#{own_indent}<Node #{name} #{skipped? ? "(skipped)" : ""}(#{definition_name}: {#{definitions.keys.join("|")}} -> #{return_type})>"
100-
if children.any?
101-
self_inspect << " {\n#{children.values.map { |n| n.inspect(indent + 2)}.join("\n")}\n#{own_indent}}"
109+
if typed_children.any?
110+
self_inspect << " {"
111+
typed_children.each do |type_defn, children|
112+
self_inspect << "\n#{own_indent} #{type_defn} => (#{children.keys.join(",")})"
113+
end
114+
self_inspect << "\n#{own_indent}}"
102115
end
103116
self_inspect
104117
end

lib/graphql/internal_representation/rewrite.rb

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,11 @@ def validate(context)
4545
visitor[Nodes::Field].enter << ->(ast_node, prev_ast_node) {
4646
parent_node = @nodes.last
4747
node_name = ast_node.alias || ast_node.name
48+
owner_type = context.parent_type_definition.unwrap
4849
# This node might not be novel, eg inside an inline fragment
4950
# but it could contain new type information, which is captured below.
5051
# (StaticValidation ensures that merging fields is fair game)
51-
node = parent_node.children[node_name] ||= begin
52+
node = parent_node.typed_children[owner_type][node_name] ||= begin
5253
Node.new(
5354
return_type: context.type_definition && context.type_definition.unwrap,
5455
ast_node: ast_node,
@@ -58,8 +59,7 @@ def validate(context)
5859
included: false, # may be set to true on leaving the node
5960
)
6061
end
61-
object_type = context.parent_type_definition.unwrap
62-
node.definitions[object_type] = context.field_definition
62+
node.definitions[owner_type] = context.field_definition
6363
@nodes.push(node)
6464
@parent_directives.push([])
6565
}
@@ -176,8 +176,10 @@ def validate(context)
176176
# Merge the children from `fragment_node` into `parent_node`.
177177
# This is an implementation of "fragment inlining"
178178
def deep_merge(parent_node, fragment_node, included)
179-
fragment_node.children.each do |name, child_node|
180-
deep_merge_child(parent_node, name, child_node, included)
179+
fragment_node.typed_children.each do |type_defn, children|
180+
children.each do |name, child_node|
181+
deep_merge_child(parent_node, type_defn, name, child_node, included)
182+
end
181183
end
182184
end
183185

@@ -186,29 +188,29 @@ def deep_merge(parent_node, fragment_node, included)
186188
# - If the spread was included, first-level children should be included if _either_ node was included
187189
# - If the spread was _not_ included, first-level children should be included if _a pre-existing_ node was included
188190
# (A copied node should be excluded)
189-
def deep_merge_child(parent_node, name, node, extra_included)
190-
child_node = parent_node.children[name]
191+
def deep_merge_child(parent_node, type_defn, name, node, extra_included)
192+
child_node = parent_node.typed_children[type_defn][name]
191193
previously_included = child_node.nil? ? false : child_node.included?
192194
next_included = extra_included ? (previously_included || node.included?) : previously_included
193195

194196
if child_node.nil?
195-
child_node = parent_node.children[name] = node.dup
197+
child_node = parent_node.typed_children[type_defn][name] = node.dup
196198
end
197199

198200
child_node.definitions.merge!(node.definitions)
199201

200202
child_node.included = next_included
201203

202-
203-
204-
node.children.each do |merge_child_name, merge_child_node|
205-
deep_merge_child(child_node, merge_child_name, merge_child_node, node.included)
204+
node.typed_children.each do |type_defn, children|
205+
children.each do |merge_child_name, merge_child_node|
206+
deep_merge_child(child_node, type_defn, merge_child_name, merge_child_node, node.included)
207+
end
206208
end
207209
end
208210

209211
# return true if node or _any_ children have a fragment spread
210212
def any_fragment_spreads?(node)
211-
node.spreads.any? || node.children.any? { |name, node| any_fragment_spreads?(node) }
213+
node.spreads.any? || node.typed_children.any? { |type_defn, children| children.any? { |name, node| any_fragment_spreads?(node) } }
212214
end
213215

214216
# pop off own directives,

lib/graphql/query/serial_execution/selection_resolution.rb

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,36 @@ class Query
33
class SerialExecution
44
module SelectionResolution
55
def self.resolve(target, current_type, irep_node, execution_context)
6-
irep_node.children.each_with_object({}) do |(name, irep_node), memo|
7-
if irep_node.included? && irep_node.definitions.any? { |potential_type, field_defn| GraphQL::Execution::Typecast.compatible?(current_type, potential_type, execution_context.query.context) }
8-
field_result = execution_context.strategy.field_resolution.new(
9-
irep_node,
10-
current_type,
11-
target,
12-
execution_context
13-
).result
14-
memo.merge!(field_result)
6+
selection_result = {}
7+
irep_node.typed_children.each do |type_defn, typed_children|
8+
type_child_result = {}
9+
compat = GraphQL::Execution::Typecast.compatible?(current_type, type_defn, execution_context.query.context)
10+
if compat
11+
typed_children.each do |name, irep_node|
12+
applies = irep_node.included? && irep_node.definitions.any? { |potential_type, field_defn| GraphQL::Execution::Typecast.compatible?(current_type, potential_type, execution_context.query.context) }
13+
if applies
14+
field_result = execution_context.strategy.field_resolution.new(
15+
irep_node,
16+
current_type,
17+
target,
18+
execution_context
19+
).result
20+
type_child_result.merge!(field_result)
21+
end
22+
end
23+
end
24+
deeply_merge(selection_result, type_child_result)
25+
end
26+
selection_result
27+
end
28+
29+
def self.deeply_merge(complete_result, typed_result)
30+
typed_result.each do |key, value|
31+
if value.is_a?(Hash)
32+
(complete_result[key] ||= {}).merge!(value)
33+
else
34+
# TODO: we can avoid running this twice
35+
complete_result[key] = value
1536
end
1637
end
1738
end

spec/graphql/internal_representation/rewrite_spec.rb

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,20 @@
1919
}
2020
}
2121
|}
22+
2223
it "produces a tree of nodes" do
2324
op_node = rewrite_result["getCheeses"]
2425

25-
assert_equal 2, op_node.children.length
26+
root_children = op_node.typed_children[DairyAppQueryType]
27+
assert_equal 2, root_children.length
2628
assert_equal DairyAppQueryType, op_node.return_type
27-
first_field = op_node.children.values.first
28-
assert_equal 3, first_field.children.length
29+
first_field = root_children.values.first
30+
assert_equal 3, first_field.typed_children[CheeseType].length
2931
assert_equal [DairyAppQueryType], first_field.definitions.keys
3032
assert_equal CheeseType, first_field.return_type
3133

32-
second_field = op_node.children.values.last
33-
assert_equal 1, second_field.children.length
34+
second_field = root_children.values.last
35+
assert_equal 1, second_field.typed_children[CheeseType].length
3436
assert_equal [DairyAppQueryType], second_field.definitions.keys
3537
assert_equal CheeseType, second_field.return_type
3638
assert second_field.inspect.is_a?(String)
@@ -47,8 +49,8 @@
4749
|}
4850

4951
it "gets dynamic field definitions" do
50-
cheese_field = rewrite_result[nil].children["cheese"]
51-
typename_field = cheese_field.children["typename"]
52+
cheese_field = rewrite_result[nil].typed_children[DairyAppQueryType]["cheese"]
53+
typename_field = cheese_field.typed_children[CheeseType]["typename"]
5254
assert_equal "__typename", typename_field.definitions.values.first.name
5355
assert_equal "__typename", typename_field.definition_name
5456
end
@@ -125,22 +127,91 @@
125127
it "puts all fragment members as children" do
126128
op_node = rewrite_result[nil]
127129

128-
cheese_field = op_node.children["cheese"]
129-
assert_equal ["id1", "id2", "fatContent", "origin", "similarCow", "flavor"], cheese_field.children.keys
130+
cheese_field = op_node.typed_children[DairyAppQueryType]["cheese"]
131+
assert_equal ["id1", "id2", "fatContent", "similarCow", "flavor"], cheese_field.typed_children[CheeseType].keys
132+
assert_equal ["fatContent", "origin"], cheese_field.typed_children[EdibleInterface].keys
130133
# Merge:
131-
similar_cow_field = cheese_field.children["similarCow"]
132-
assert_equal ["similarCowSource", "fatContent", "similarCheese", "id"], similar_cow_field.children.keys
134+
similar_cow_field = cheese_field.typed_children[CheeseType]["similarCow"]
135+
assert_equal ["similarCowSource", "fatContent", "similarCheese", "id"], similar_cow_field.typed_children[CheeseType].keys
133136
# Deep merge:
134-
similar_sheep_field = similar_cow_field.children["similarCheese"]
135-
assert_equal ["flavor", "source"], similar_sheep_field.children.keys
137+
similar_sheep_field = similar_cow_field.typed_children[CheeseType]["similarCheese"]
138+
assert_equal ["flavor", "source"], similar_sheep_field.typed_children[CheeseType].keys
136139

137-
assert_equal [EdibleInterface], cheese_field.children["origin"].definitions.keys
138-
assert_equal [CheeseType, EdibleInterface], cheese_field.children["fatContent"].definitions.keys
139-
assert_equal [CheeseType], cheese_field.children["flavor"].definitions.keys
140+
assert_equal [EdibleInterface], cheese_field.typed_children[EdibleInterface]["origin"].definitions.keys
141+
assert_equal [EdibleInterface], cheese_field.typed_children[EdibleInterface]["fatContent"].definitions.keys
142+
assert_equal [CheeseType], cheese_field.typed_children[CheeseType]["fatContent"].definitions.keys
143+
assert_equal [CheeseType], cheese_field.typed_children[CheeseType]["flavor"].definitions.keys
140144

141145
# nested spread inside fragment definition:
142146
cheese_2_field = op_node.children["cheese2"].children["similarCheese"]
143147
assert_equal ["id", "fatContent"], cheese_2_field.children.keys
144148
end
145149
end
150+
151+
describe "nested fields on typed fragments" do
152+
let(:result) { DummySchema.execute(query_string) }
153+
let(:query_string) {%|
154+
{
155+
allDairy {
156+
__typename
157+
158+
... on Milk {
159+
selfAsEdible {
160+
milkInlineOrigin: origin
161+
}
162+
}
163+
164+
... on Cheese {
165+
selfAsEdible {
166+
cheeseInlineOrigin: origin
167+
}
168+
}
169+
170+
... on Edible {
171+
selfAsEdible {
172+
edibleInlineOrigin: origin
173+
}
174+
}
175+
176+
... {
177+
... on Edible {
178+
selfAsEdible {
179+
untypedInlineOrigin: origin
180+
}
181+
}
182+
}
183+
...milkFields
184+
...cheeseFields
185+
}
186+
}
187+
188+
fragment cheeseFields on Cheese {
189+
selfAsEdible {
190+
cheeseFragmentOrigin: origin
191+
}
192+
}
193+
fragment milkFields on Milk {
194+
selfAsEdible {
195+
milkFragmentOrigin: origin
196+
}
197+
}
198+
|}
199+
200+
it "distinguishes between nested fields with the same name on different typed fragments" do
201+
all_dairy = result["data"]["allDairy"]
202+
cheeses = all_dairy.select { |d| d["__typename"] == "Cheese" }
203+
milks = all_dairy.select { |d| d["__typename"] == "Milk" }
204+
205+
# Make sure all the data is there:
206+
assert_equal 3, cheeses.length
207+
assert_equal 1, milks.length
208+
209+
cheeses.each do |cheese|
210+
assert_equal ["cheeseInlineOrigin", "cheeseFragmentOrigin", "edibleInlineOrigin", "untypedInlineOrigin"], cheese["selfAsEdible"].keys
211+
end
212+
milks.each do |milk|
213+
assert_equal ["milkInlineOrigin", "milkFragmentOrigin", "edibleInlineOrigin", "untypedInlineOrigin"], milk["selfAsEdible"].keys
214+
end
215+
end
216+
end
146217
end

spec/graphql/introspection/type_type_spec.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
{"name"=>"id", "isDeprecated" => false, "type" => { "kind" => "NON_NULL", "name" => nil, "ofType" => { "name" => "Int"}}},
1919
{"name"=>"nullableCheese", "isDeprecated"=>false, "type"=>{ "kind" => "OBJECT", "name" => "Cheese", "ofType"=>nil}},
2020
{"name"=>"origin", "isDeprecated" => false, "type" => { "kind" => "NON_NULL", "name" => nil, "ofType" => { "name" => "String"}}},
21+
{"name"=>"selfAsEdible", "isDeprecated"=>false, "type"=>{"kind"=>"INTERFACE", "name"=>"Edible", "ofType"=>nil}},
2122
{"name"=>"similarCheese", "isDeprecated"=>false, "type"=>{ "kind" => "OBJECT", "name"=>"Cheese", "ofType"=>nil}},
2223
{"name"=>"source", "isDeprecated" => false, "type" => { "kind" => "NON_NULL", "name" => nil, "ofType" => { "name" => "DairyAnimal"}}},
2324
]}
@@ -49,6 +50,7 @@
4950
{"type"=>{"kind"=>"LIST","name"=>nil, "ofType"=>{"name"=>"String"}}},
5051
{"type"=>{"kind"=>"NON_NULL","name"=>nil, "ofType"=>{"name"=>"ID"}}},
5152
{"type"=>{"kind"=>"NON_NULL","name"=>nil, "ofType"=>{"name"=>"String"}}},
53+
{"type"=>{"kind"=>"INTERFACE", "name"=>"Edible", "ofType"=>nil}},
5254
{"type"=>{"kind"=>"ENUM","name"=>"DairyAnimal", "ofType"=>nil}},
5355
]
5456
},

spec/graphql/schema/reduce_types_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ def reduce_types(types)
1010
"Cheese" => CheeseType,
1111
"Float" => GraphQL::FLOAT_TYPE,
1212
"String" => GraphQL::STRING_TYPE,
13+
"Edible" => EdibleInterface,
1314
"DairyAnimal" => DairyAnimalEnum,
1415
"Int" => GraphQL::INT_TYPE,
15-
"Edible" => EdibleInterface,
1616
"AnimalProduct" => AnimalProductInterface,
1717
"LocalProduct" => LocalProductInterface,
1818
}

spec/support/dairy_app.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class NoSuchDairyError < StandardError; end
1616
description "Something you can eat, yum"
1717
field :fatContent, !types.Float, "Percentage which is fat"
1818
field :origin, !types.String, "Place the edible comes from"
19+
field :selfAsEdible, EdibleInterface, resolve: ->(o, a, c) { o }
1920
end
2021

2122
AnimalProductInterface = GraphQL::InterfaceType.define do

0 commit comments

Comments
 (0)