Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,15 @@ private void CollectSelections(
}
else
{
foreach (var possibleType in schema.GetPossibleTypes(type, includeInaccessible: true))
// The branches are limited to the object types the enclosing selection set can
// yield, as an interface type condition can be implemented by types that are not
// possible types of that selection set.
foreach (var possibleType in schema.GetPossibleTypes(context.SharedType, includeInaccessible: true))
{
AddSelectionsForConcreteType(context, possibleType, selectionsWithPath, cloneSelectionSets: true);
if (MatchesEnclosingTypeConditions(context, possibleType))
{
AddSelectionsForConcreteType(context, possibleType, selectionsWithPath, cloneSelectionSets: true);
}
}
}
}
Expand All @@ -137,6 +143,38 @@ private void CollectSelections(
}
}

/// <summary>
/// Determines whether the specified object type satisfies all type conditions
/// on the current type path.
/// </summary>
private bool MatchesEnclosingTypeConditions(Context context, FusionObjectTypeDefinition type)
{
foreach (var typeCondition in context.TypePath)
{
if (!ContainsType(schema.GetPossibleTypes(typeCondition, includeInaccessible: true), type))
{
return false;
}
}

return true;
}

private static bool ContainsType(
ImmutableArray<FusionObjectTypeDefinition> possibleTypes,
FusionObjectTypeDefinition type)
{
foreach (var possibleType in possibleTypes)
{
if (ReferenceEquals(possibleType, type))
{
return true;
}
}

return false;
}

private void AddSelectionsForConcreteType(
Context context,
FusionObjectTypeDefinition type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,53 @@ ... on Book { reviewsCount }
MatchSnapshot(plan);
}

[Fact]
public void Plan_Should_Prune_NonNode_Implementor_When_InterfaceFragment_Is_In_NodeSelectionSet()
{
// arrange
// The Manageable fragment fans out over all Manageable implementors, but the enclosing
// node selection set only ever yields Node implementors, so Draft must not get a branch.
var schema = CreateManageableNodeSchema();

// act
var plan = PlanOperation(
schema,
"""
query($id: ID!) {
node(id: $id) {
__typename
... on Node { id }
... on Manageable { canEdit }
}
}
""");

// assert
MatchSnapshot(plan);
}

[Fact]
public void Plan_Should_Prune_NonNode_Implementor_When_NodeSelectionSet_Has_No_Shared_Selections()
{
// arrange
var schema = CreateManageableNodeSchema();

// act
var plan = PlanOperation(
schema,
"""
query($id: ID!) {
node(id: $id) {
__typename
... on Manageable { canEdit }
}
}
""");

// assert
MatchSnapshot(plan);
}

// sku is co-located with the products root in "a", reviews in "r", Book-only title in "books".
private static FusionSchemaDefinition CreateProductTitleSchema()
=> ComposeSchema(
Expand Down Expand Up @@ -151,4 +198,23 @@ type Query {

type Magazine @key(fields: "id") { id: ID! title: String }
""");

// Ticket is both Manageable and a Node, Draft is only Manageable and has no id.
private static FusionSchemaDefinition CreateManageableNodeSchema()
=> ComposeSchema(
"""
# name: tickets
schema { query: Query }

type Query {
node(id: ID!): Node @lookup
draft: Draft
}

interface Node { id: ID! }
interface Manageable { canEdit: Boolean! }

type Ticket implements Node & Manageable { id: ID! canEdit: Boolean! }
type Draft implements Manageable { canEdit: Boolean! name: String }
""");
}
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,64 @@ ... on Votable {
""");
}

[Fact]
public void Selections_On_Interface_Skips_Implementors_That_Are_Not_Possible_Types()
{
// arrange
var source1 = new TestSourceSchema(
"""
type Query {
node(id: ID!): Node @lookup
draft: Draft
}

interface Node {
id: ID!
}

interface Votable {
viewerHasUpvoted: Boolean!
}

type Discussion implements Node & Votable {
id: ID!
title: String!
viewerHasUpvoted: Boolean!
}

type Draft implements Votable {
name: String!
viewerHasUpvoted: Boolean!
}
""");
var schema = ComposeSchema(source1);

var doc = Utf8GraphQLParser.Parse(
"""
query($id: ID!) {
node(id: $id) {
... on Votable {
viewerHasUpvoted
}
}
}
""");

// act
var result = Partition(schema, doc);

// assert
MatchInlineSnapshot(
result,
"""
Shared: null

Discussion: {
viewerHasUpvoted
}
""");
}

[Fact]
public void Concrete_Type_Selections_Within_Interface()
{
Expand Down Expand Up @@ -399,6 +457,71 @@ ... on Votable {
""");
}

[Fact]
public void Nested_Interface_Selections_Skip_Implementors_Outside_Outer_Interface()
{
// arrange
// Announcement is a Node and Commentable, but not Votable, so the nested Commentable
// fragment must not produce an Announcement branch.
var source1 = new TestSourceSchema(
"""
type Query {
node(id: ID!): Node @lookup
}

interface Node {
id: ID!
}

interface Votable {
viewerHasUpvoted: Boolean!
}

interface Commentable {
commentCount: Int!
}

type Discussion implements Node & Votable & Commentable {
id: ID!
viewerHasUpvoted: Boolean!
commentCount: Int!
}

type Announcement implements Node & Commentable {
id: ID!
commentCount: Int!
}
""");
var schema = ComposeSchema(source1);

var doc = Utf8GraphQLParser.Parse(
"""
query($id: ID!) {
node(id: $id) {
... on Votable {
... on Commentable {
commentCount
}
}
}
}
""");

// act
var result = Partition(schema, doc);

// assert
MatchInlineSnapshot(
result,
"""
Shared: null

Discussion: {
commentCount
}
""");
}

[Fact]
public void Spread_On_Type_Of_SelectionSet_Is_Part_Of_Shared_Selections()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
operation:
- document: |
query($id: ID!) {
node(id: $id) {
__typename
id
... on Manageable {
canEdit
}
}
}
hash: 123456789101112
searchSpace: 1
expandedNodes: 1
nodes:
- id: 1
type: Node
idValue: $id
responseName: node
branches:
- Ticket: 3
fallback: 2
- id: 2
type: Operation
operation: |
query Op_123456789101112_2($id: ID!) {
node(id: $id) {
__typename
id
}
}
forwardedVariables:
- id
dependencies:
- id: 1
- id: 3
type: Operation
schema: tickets
operation: |
query Op_123456789101112_3($id: ID!) {
node(id: $id) {
__typename
... on Ticket {
__typename
id
canEdit
}
}
}
forwardedVariables:
- id
dependencies:
- id: 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
operation:
- document: |
query($id: ID!) {
node(id: $id) {
__typename
... on Manageable {
canEdit
}
}
}
hash: 123456789101112
searchSpace: 1
expandedNodes: 1
nodes:
- id: 1
type: Node
idValue: $id
responseName: node
branches:
- Ticket: 3
fallback: 2
- id: 2
type: Operation
operation: |
query Op_123456789101112_2($id: ID!) {
node(id: $id) {
__typename
}
}
forwardedVariables:
- id
dependencies:
- id: 1
- id: 3
type: Operation
schema: tickets
operation: |
query Op_123456789101112_3($id: ID!) {
node(id: $id) {
__typename
... on Ticket {
__typename
canEdit
}
}
}
forwardedVariables:
- id
dependencies:
- id: 1
Loading