Skip to content

Commit

Permalink
Merge pull request #2068 from rmosolgo/lookahead-fragment-fix
Browse files Browse the repository at this point in the history
Fix lookahead with typed fragments
  • Loading branch information
Robert Mosolgo authored Jan 22, 2019
2 parents 3642ee9 + 4b16af8 commit e66e7bc
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 14 deletions.
35 changes: 22 additions & 13 deletions lib/graphql/execution/lookahead.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def initialize(query:, ast_nodes:, field: nil, root_type: nil)
@field = field
@root_type = root_type
@query = query
@selected_type = @field ? @field.type.unwrap : root_type
end

# @return [Array<GraphQL::Language::Nodes::Field>]
Expand Down Expand Up @@ -72,16 +73,10 @@ def selected?
# Like {#selects?}, but can be used for chaining.
# It returns a null object (check with {#selected?})
# @return [GraphQL::Execution::Lookahead]
def selection(field_name, arguments: nil)
def selection(field_name, selected_type: @selected_type, arguments: nil)
next_field_name = normalize_name(field_name)

next_field_owner = if @field
@field.type.unwrap
else
@root_type
end

next_field_defn = FieldHelpers.get_field(@query.schema, next_field_owner, next_field_name)
next_field_defn = FieldHelpers.get_field(@query.schema, selected_type, next_field_name)
if next_field_defn
next_nodes = []
@ast_nodes.each do |ast_node|
Expand Down Expand Up @@ -118,7 +113,7 @@ def selection(field_name, arguments: nil)
def selections(arguments: nil)
subselections_by_name = {}
@ast_nodes.each do |node|
find_selections(subselections_by_name, node.selections, arguments)
find_selections(subselections_by_name, @selected_type, node.selections, arguments)
end

# Items may be filtered out if `arguments` doesn't match
Expand All @@ -139,6 +134,10 @@ def name
@field && @field.original_name
end

def inspect
"#<GraphQL::Execution::Lookahead #{@field ? "@field=#{@field.path.inspect}": "@root_type=#{@root_type}"} @ast_nodes.size=#{@ast_nodes.size}>"
end

# This is returned for {Lookahead#selection} when a non-existent field is passed
class NullLookahead < Lookahead
# No inputs required here.
Expand All @@ -160,6 +159,10 @@ def selection(*)
def selections(*)
[]
end

def inspect
"#<GraphQL::Execution::Lookahead::NullLookahead>"
end
end

# A singleton, so that misses don't come with overhead.
Expand All @@ -184,16 +187,22 @@ def normalize_keyword(keyword)
end
end

def find_selections(subselections_by_name, ast_selections, arguments)
def find_selections(subselections_by_name, selected_type, ast_selections, arguments)
ast_selections.each do |ast_selection|
case ast_selection
when GraphQL::Language::Nodes::Field
subselections_by_name[ast_selection.name] ||= selection(ast_selection.name, arguments: arguments)
subselections_by_name[ast_selection.name] ||= selection(ast_selection.name, selected_type: selected_type, arguments: arguments)
when GraphQL::Language::Nodes::InlineFragment
find_selections(subselections_by_name, ast_selection.selections, arguments)
if (t = ast_selection.type)
# Assuming this is valid, that `t` will be found.
selected_type = @query.schema.types[t.name].metadata[:type_class]
end
find_selections(subselections_by_name, selected_type, ast_selection.selections, arguments)
when GraphQL::Language::Nodes::FragmentSpread
frag_defn = @query.fragments[ast_selection.name] || raise("Invariant: Can't look ahead to nonexistent fragment #{ast_selection.name} (found: #{@query.fragments.keys})")
find_selections(subselections_by_name, frag_defn.selections, arguments)
# Again, assuming a valid AST
selected_type = @query.schema.types[frag_defn.type.name].metadata[:type_class]
find_selections(subselections_by_name, selected_type, frag_defn.selections, arguments)
else
raise "Invariant: Unexpected selection type: #{ast_selection.class}"
end
Expand Down
55 changes: 54 additions & 1 deletion spec/graphql/execution/lookahead_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,19 @@ def DATA.find_by_name(name)
DATA.find { |b| b.name == name }
end

module Node
include GraphQL::Schema::Interface
field :id, ID, null: false
end

class BirdGenus < GraphQL::Schema::Object
field :latin_name, String, null: false
field :id, ID, null: false, method: :latin_name
end

class BirdSpecies < GraphQL::Schema::Object
field :name, String, null: false
field :id, ID, null: false, method: :name
field :is_waterfowl, Boolean, null: false
field :similar_species, [BirdSpecies], null: false

Expand All @@ -46,6 +53,18 @@ class Query < GraphQL::Schema::Object
def find_bird_species(by_name:)
DATA.find_by_name(by_name)
end

field :node, Node, null: true do
argument :id, ID, required: true
end

def node(id:)
if (node = DATA.find_by_name(id))
node
else
DATA.map { |d| d.genus }.select { |g| g.name == id }
end
end
end

class LookaheadInstrumenter
Expand Down Expand Up @@ -101,6 +120,40 @@ class Schema < GraphQL::Schema
assert_equal true, query.lookahead.selects?("__typename")
end

describe "fields on interfaces" do
let(:document) {
GraphQL.parse <<-GRAPHQL
query {
node(id: "Cardinal") {
id
... on BirdSpecies {
name
}
...Other
}
}
fragment Other on BirdGenus {
latinName
}
GRAPHQL
}

it "finds fields on object types and interface types" do
node_lookahead = query.lookahead.selection("node")
assert_equal [:id, :name, :latin_name], node_lookahead.selections.map(&:name)
end
end

describe "inspect" do
it "works for root lookaheads" do
assert_includes query.lookahead.inspect, "#<GraphQL::Execution::Lookahead @root_type="
end

it "works for field lookaheads" do
assert_includes query.lookahead.selection(:find_bird_species).inspect, "#<GraphQL::Execution::Lookahead @field="
end
end

describe "constraints by arguments" do
let(:lookahead) do
query.lookahead
Expand Down Expand Up @@ -245,7 +298,7 @@ def query(doc = document)
ast_node = document.definitions.first.selections.first
field = LookaheadTest::Query.fields["findBirdSpecies"]
lookahead = GraphQL::Execution::Lookahead.new(query: query, ast_nodes: [ast_node], field: field)
assert_equal lookahead.selections.map(&:name), [:name, :similar_species]
assert_equal [:name, :similar_species], lookahead.selections.map(&:name)
end

it "filters outs selections which do not match arguments" do
Expand Down

0 comments on commit e66e7bc

Please sign in to comment.