Skip to content

Make Query::Partial able to run a fragment #5362

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

Merged
merged 3 commits into from
May 28, 2025
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
10 changes: 5 additions & 5 deletions lib/graphql/execution/interpreter/runtime.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,14 @@ def run_eager
case inner_type.kind.name
when "SCALAR", "ENUM"
result_name = ast_node.alias || ast_node.name
owner_type = query.field_definition.owner
field_defn = query.field_definition
owner_type = field_defn.owner
selection_result = GraphQLResultHash.new(nil, owner_type, nil, nil, false, EmptyObjects::EMPTY_ARRAY, false, ast_node, nil, nil)
selection_result.base_path = base_path
selection_result.ordered_result_keys = [result_name]
runtime_state = get_current_runtime_state
runtime_state.current_result = selection_result
runtime_state.current_result_name = result_name
field_defn = query.field_definition
continue_value = continue_value(object, field_defn, false, ast_node, result_name, selection_result)
if HALT != continue_value
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
Expand All @@ -156,14 +156,14 @@ def run_eager
end
when "SCALAR", "ENUM"
result_name = ast_node.alias || ast_node.name
owner_type = query.field_definition.owner
selection_result = GraphQLResultHash.new(nil, query.parent_type, nil, nil, false, EmptyObjects::EMPTY_ARRAY, false, ast_node, nil, nil)
field_defn = query.field_definition
owner_type = field_defn.owner
selection_result = GraphQLResultHash.new(nil, owner_type, nil, nil, false, EmptyObjects::EMPTY_ARRAY, false, ast_node, nil, nil)
selection_result.ordered_result_keys = [result_name]
selection_result.base_path = base_path
runtime_state = get_current_runtime_state
runtime_state.current_result = selection_result
runtime_state.current_result_name = result_name
field_defn = query.field_definition
continue_value = continue_value(object, field_defn, false, ast_node, result_name, selection_result)
if HALT != continue_value
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
Expand Down
121 changes: 70 additions & 51 deletions lib/graphql/query/partial.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class Partial
# @param object [Object] A starting object for execution
# @param query [GraphQL::Query] A full query instance that this partial is based on. Caches are shared.
# @param context [Hash] Extra context values to merge into `query.context`, if provided
def initialize(path:, object:, query:, context: nil)
# @param fragment_node [GraphQL::Language::Nodes::InlineFragment, GraphQL::Language::Nodes::FragmentDefinition]
def initialize(path: nil, object:, query:, context: nil, fragment_node: nil, type: nil)
@path = path
@object = object
@query = query
Expand All @@ -31,65 +32,26 @@ def initialize(path:, object:, query:, context: nil)
@multiplex = nil
@result_values = nil
@result = nil
selections = [@query.selected_operation]
type = @query.root_type
parent_type = nil
field_defn = nil
@path.each do |name_in_doc|
if name_in_doc.is_a?(Integer)
if type.list?
type = type.unwrap
next
else
raise ArgumentError, "Received path with index `#{name_in_doc}`, but type wasn't a list. Type: #{type.to_type_signature}, path: #{@path}"
end
end

next_selections = []
selections.each do |selection|
selections_to_check = []
selections_to_check.concat(selection.selections)
while (sel = selections_to_check.shift)
case sel
when GraphQL::Language::Nodes::InlineFragment
selections_to_check.concat(sel.selections)
when GraphQL::Language::Nodes::FragmentSpread
fragment = @query.fragments[sel.name]
selections_to_check.concat(fragment.selections)
when GraphQL::Language::Nodes::Field
if sel.alias == name_in_doc || sel.name == name_in_doc
next_selections << sel
end
else
raise "Unexpected selection in partial path: #{sel.class}, #{sel.inspect}"
end
end
end

if next_selections.empty?
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."
end
field_name = next_selections.first.name
field_defn = @schema.get_field(type, field_name, @query.context) || raise("Invariant: no field called #{field_name} on #{type.graphql_name}")
parent_type = type
type = field_defn.type
if type.non_null?
type = type.of_type
end
selections = next_selections
if fragment_node
@ast_nodes = [fragment_node]
@root_type = type || raise(ArgumentError, "Pass `type:` when using `node:`")
# This is only used when `@leaf`
@field_definition = nil
elsif path.nil?
raise ArgumentError, "`path:` is required if `node:` is not given; add `path:`"
else
set_type_info_from_path
end
@parent_type = parent_type
@ast_nodes = selections
@root_type = type
@field_definition = field_defn

@leaf = @root_type.unwrap.kind.leaf?
end

def leaf?
@leaf
end

attr_reader :context, :query, :ast_nodes, :root_type, :object, :field_definition, :path, :parent_type, :schema
attr_reader :context, :query, :ast_nodes, :root_type, :object, :field_definition, :path, :schema

attr_accessor :multiplex, :result_values

Expand Down Expand Up @@ -155,6 +117,63 @@ def static_errors
def selected_operation_name
@query.selected_operation_name
end

private

def set_type_info_from_path
selections = [@query.selected_operation]
type = @query.root_type
parent_type = nil
field_defn = nil

@path.each do |name_in_doc|
if name_in_doc.is_a?(Integer)
if type.list?
type = type.unwrap
next
else
raise ArgumentError, "Received path with index `#{name_in_doc}`, but type wasn't a list. Type: #{type.to_type_signature}, path: #{@path}"
end
end

next_selections = []
selections.each do |selection|
selections_to_check = []
selections_to_check.concat(selection.selections)
while (sel = selections_to_check.shift)
case sel
when GraphQL::Language::Nodes::InlineFragment
selections_to_check.concat(sel.selections)
when GraphQL::Language::Nodes::FragmentSpread
fragment = @query.fragments[sel.name]
selections_to_check.concat(fragment.selections)
when GraphQL::Language::Nodes::Field
if sel.alias == name_in_doc || sel.name == name_in_doc
next_selections << sel
end
else
raise "Unexpected selection in partial path: #{sel.class}, #{sel.inspect}"
end
end
end

if next_selections.empty?
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."
end
field_name = next_selections.first.name
field_defn = @schema.get_field(type, field_name, @query.context) || raise("Invariant: no field called #{field_name} on #{type.graphql_name}")
parent_type = type
type = field_defn.type
if type.non_null?
type = type.of_type
end
selections = next_selections
end

@ast_nodes = selections
@root_type = type
@field_definition = field_defn
end
end
end
end
45 changes: 45 additions & 0 deletions spec/graphql/query/partial_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ def neighboring_farm
end
end

class UpcasedFarm < GraphQL::Schema::Object
field :name, String

def name
object[:name].upcase
end
end

class Market < GraphQL::Schema::Object
implements Entity
field :is_year_round, Boolean
Expand Down Expand Up @@ -174,6 +182,43 @@ def run_partials(string, partial_configs, **query_kwargs)
], results
end

it "runs inline fragments" do
str = "{
farm(id: \"1\") {
... on Farm {
name
... {
n2: name
}
}
}
}"

document = GraphQL.parse(str)
fragment_node = document.definitions.first.selections.first.selections.first
other_fragment_node = fragment_node.selections[1]
results = run_partials(str, [
{ fragment_node: fragment_node, type: PartialSchema::Farm, object: { name: "Belair Farm" } },
{ fragment_node: other_fragment_node, type: PartialSchema::UpcasedFarm, object: { name: "Free Union Grass Farm" } }
])
assert_equal({ "name" => "Belair Farm", "n2" => "Belair Farm" }, results[0]["data"])
assert_equal({ "n2" => "FREE UNION GRASS FARM" }, results[1]["data"])
end

it "runs fragment definitions" do
str = "{
farm(id: \"1\") { ... farmFields }
}

fragment farmFields on Farm {
farmName: name
}"

node = GraphQL.parse(str).definitions.last
results = run_partials(str, [{ fragment_node: node, type: PartialSchema::Farm, object: { name: "Clovertop Creamery" } }])
assert_equal({ "farmName" => "Clovertop Creamery" }, results[0]["data"])
end

it "works with GraphQL::Current" do
res = run_partials("query CheckCurrentValues { query { currentValues } }", [path: ["query"], object: nil])
assert_equal ["CheckCurrentValues", "Query.currentValues", "nil"], res[0]["data"]["currentValues"]
Expand Down