Skip to content

Commit 0e8e332

Browse files
authored
Merge pull request #5362 from rmosolgo/partial-node
Make Query::Partial able to run a fragment
2 parents fb9d48c + 185000e commit 0e8e332

File tree

3 files changed

+120
-56
lines changed

3 files changed

+120
-56
lines changed

lib/graphql/execution/interpreter/runtime.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,14 @@ def run_eager
123123
case inner_type.kind.name
124124
when "SCALAR", "ENUM"
125125
result_name = ast_node.alias || ast_node.name
126-
owner_type = query.field_definition.owner
126+
field_defn = query.field_definition
127+
owner_type = field_defn.owner
127128
selection_result = GraphQLResultHash.new(nil, owner_type, nil, nil, false, EmptyObjects::EMPTY_ARRAY, false, ast_node, nil, nil)
128129
selection_result.base_path = base_path
129130
selection_result.ordered_result_keys = [result_name]
130131
runtime_state = get_current_runtime_state
131132
runtime_state.current_result = selection_result
132133
runtime_state.current_result_name = result_name
133-
field_defn = query.field_definition
134134
continue_value = continue_value(object, field_defn, false, ast_node, result_name, selection_result)
135135
if HALT != continue_value
136136
continue_field(continue_value, owner_type, field_defn, root_type, ast_node, nil, false, nil, nil, result_name, selection_result, false, runtime_state) # rubocop:disable Metrics/ParameterLists
@@ -156,14 +156,14 @@ def run_eager
156156
end
157157
when "SCALAR", "ENUM"
158158
result_name = ast_node.alias || ast_node.name
159-
owner_type = query.field_definition.owner
160-
selection_result = GraphQLResultHash.new(nil, query.parent_type, nil, nil, false, EmptyObjects::EMPTY_ARRAY, false, ast_node, nil, nil)
159+
field_defn = query.field_definition
160+
owner_type = field_defn.owner
161+
selection_result = GraphQLResultHash.new(nil, owner_type, nil, nil, false, EmptyObjects::EMPTY_ARRAY, false, ast_node, nil, nil)
161162
selection_result.ordered_result_keys = [result_name]
162163
selection_result.base_path = base_path
163164
runtime_state = get_current_runtime_state
164165
runtime_state.current_result = selection_result
165166
runtime_state.current_result_name = result_name
166-
field_defn = query.field_definition
167167
continue_value = continue_value(object, field_defn, false, ast_node, result_name, selection_result)
168168
if HALT != continue_value
169169
continue_field(continue_value, owner_type, field_defn, query.root_type, ast_node, nil, false, nil, nil, result_name, selection_result, false, runtime_state) # rubocop:disable Metrics/ParameterLists

lib/graphql/query/partial.rb

Lines changed: 70 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ class Partial
1818
# @param object [Object] A starting object for execution
1919
# @param query [GraphQL::Query] A full query instance that this partial is based on. Caches are shared.
2020
# @param context [Hash] Extra context values to merge into `query.context`, if provided
21-
def initialize(path:, object:, query:, context: nil)
21+
# @param fragment_node [GraphQL::Language::Nodes::InlineFragment, GraphQL::Language::Nodes::FragmentDefinition]
22+
def initialize(path: nil, object:, query:, context: nil, fragment_node: nil, type: nil)
2223
@path = path
2324
@object = object
2425
@query = query
@@ -31,65 +32,26 @@ def initialize(path:, object:, query:, context: nil)
3132
@multiplex = nil
3233
@result_values = nil
3334
@result = nil
34-
selections = [@query.selected_operation]
35-
type = @query.root_type
36-
parent_type = nil
37-
field_defn = nil
38-
@path.each do |name_in_doc|
39-
if name_in_doc.is_a?(Integer)
40-
if type.list?
41-
type = type.unwrap
42-
next
43-
else
44-
raise ArgumentError, "Received path with index `#{name_in_doc}`, but type wasn't a list. Type: #{type.to_type_signature}, path: #{@path}"
45-
end
46-
end
47-
48-
next_selections = []
49-
selections.each do |selection|
50-
selections_to_check = []
51-
selections_to_check.concat(selection.selections)
52-
while (sel = selections_to_check.shift)
53-
case sel
54-
when GraphQL::Language::Nodes::InlineFragment
55-
selections_to_check.concat(sel.selections)
56-
when GraphQL::Language::Nodes::FragmentSpread
57-
fragment = @query.fragments[sel.name]
58-
selections_to_check.concat(fragment.selections)
59-
when GraphQL::Language::Nodes::Field
60-
if sel.alias == name_in_doc || sel.name == name_in_doc
61-
next_selections << sel
62-
end
63-
else
64-
raise "Unexpected selection in partial path: #{sel.class}, #{sel.inspect}"
65-
end
66-
end
67-
end
6835

69-
if next_selections.empty?
70-
raise ArgumentError, "Path `#{@path.inspect}` is not present in this query. `#{name_in_doc.inspect}` was not found. Try a different path or rewrite the query to include it."
71-
end
72-
field_name = next_selections.first.name
73-
field_defn = @schema.get_field(type, field_name, @query.context) || raise("Invariant: no field called #{field_name} on #{type.graphql_name}")
74-
parent_type = type
75-
type = field_defn.type
76-
if type.non_null?
77-
type = type.of_type
78-
end
79-
selections = next_selections
36+
if fragment_node
37+
@ast_nodes = [fragment_node]
38+
@root_type = type || raise(ArgumentError, "Pass `type:` when using `node:`")
39+
# This is only used when `@leaf`
40+
@field_definition = nil
41+
elsif path.nil?
42+
raise ArgumentError, "`path:` is required if `node:` is not given; add `path:`"
43+
else
44+
set_type_info_from_path
8045
end
81-
@parent_type = parent_type
82-
@ast_nodes = selections
83-
@root_type = type
84-
@field_definition = field_defn
46+
8547
@leaf = @root_type.unwrap.kind.leaf?
8648
end
8749

8850
def leaf?
8951
@leaf
9052
end
9153

92-
attr_reader :context, :query, :ast_nodes, :root_type, :object, :field_definition, :path, :parent_type, :schema
54+
attr_reader :context, :query, :ast_nodes, :root_type, :object, :field_definition, :path, :schema
9355

9456
attr_accessor :multiplex, :result_values
9557

@@ -155,6 +117,63 @@ def static_errors
155117
def selected_operation_name
156118
@query.selected_operation_name
157119
end
120+
121+
private
122+
123+
def set_type_info_from_path
124+
selections = [@query.selected_operation]
125+
type = @query.root_type
126+
parent_type = nil
127+
field_defn = nil
128+
129+
@path.each do |name_in_doc|
130+
if name_in_doc.is_a?(Integer)
131+
if type.list?
132+
type = type.unwrap
133+
next
134+
else
135+
raise ArgumentError, "Received path with index `#{name_in_doc}`, but type wasn't a list. Type: #{type.to_type_signature}, path: #{@path}"
136+
end
137+
end
138+
139+
next_selections = []
140+
selections.each do |selection|
141+
selections_to_check = []
142+
selections_to_check.concat(selection.selections)
143+
while (sel = selections_to_check.shift)
144+
case sel
145+
when GraphQL::Language::Nodes::InlineFragment
146+
selections_to_check.concat(sel.selections)
147+
when GraphQL::Language::Nodes::FragmentSpread
148+
fragment = @query.fragments[sel.name]
149+
selections_to_check.concat(fragment.selections)
150+
when GraphQL::Language::Nodes::Field
151+
if sel.alias == name_in_doc || sel.name == name_in_doc
152+
next_selections << sel
153+
end
154+
else
155+
raise "Unexpected selection in partial path: #{sel.class}, #{sel.inspect}"
156+
end
157+
end
158+
end
159+
160+
if next_selections.empty?
161+
raise ArgumentError, "Path `#{@path.inspect}` is not present in this query. `#{name_in_doc.inspect}` was not found. Try a different path or rewrite the query to include it."
162+
end
163+
field_name = next_selections.first.name
164+
field_defn = @schema.get_field(type, field_name, @query.context) || raise("Invariant: no field called #{field_name} on #{type.graphql_name}")
165+
parent_type = type
166+
type = field_defn.type
167+
if type.non_null?
168+
type = type.of_type
169+
end
170+
selections = next_selections
171+
end
172+
173+
@ast_nodes = selections
174+
@root_type = type
175+
@field_definition = field_defn
176+
end
158177
end
159178
end
160179
end

spec/graphql/query/partial_spec.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ def neighboring_farm
6565
end
6666
end
6767

68+
class UpcasedFarm < GraphQL::Schema::Object
69+
field :name, String
70+
71+
def name
72+
object[:name].upcase
73+
end
74+
end
75+
6876
class Market < GraphQL::Schema::Object
6977
implements Entity
7078
field :is_year_round, Boolean
@@ -174,6 +182,43 @@ def run_partials(string, partial_configs, **query_kwargs)
174182
], results
175183
end
176184

185+
it "runs inline fragments" do
186+
str = "{
187+
farm(id: \"1\") {
188+
... on Farm {
189+
name
190+
... {
191+
n2: name
192+
}
193+
}
194+
}
195+
}"
196+
197+
document = GraphQL.parse(str)
198+
fragment_node = document.definitions.first.selections.first.selections.first
199+
other_fragment_node = fragment_node.selections[1]
200+
results = run_partials(str, [
201+
{ fragment_node: fragment_node, type: PartialSchema::Farm, object: { name: "Belair Farm" } },
202+
{ fragment_node: other_fragment_node, type: PartialSchema::UpcasedFarm, object: { name: "Free Union Grass Farm" } }
203+
])
204+
assert_equal({ "name" => "Belair Farm", "n2" => "Belair Farm" }, results[0]["data"])
205+
assert_equal({ "n2" => "FREE UNION GRASS FARM" }, results[1]["data"])
206+
end
207+
208+
it "runs fragment definitions" do
209+
str = "{
210+
farm(id: \"1\") { ... farmFields }
211+
}
212+
213+
fragment farmFields on Farm {
214+
farmName: name
215+
}"
216+
217+
node = GraphQL.parse(str).definitions.last
218+
results = run_partials(str, [{ fragment_node: node, type: PartialSchema::Farm, object: { name: "Clovertop Creamery" } }])
219+
assert_equal({ "farmName" => "Clovertop Creamery" }, results[0]["data"])
220+
end
221+
177222
it "works with GraphQL::Current" do
178223
res = run_partials("query CheckCurrentValues { query { currentValues } }", [path: ["query"], object: nil])
179224
assert_equal ["CheckCurrentValues", "Query.currentValues", "nil"], res[0]["data"]["currentValues"]

0 commit comments

Comments
 (0)