Skip to content

Commit 35bcd91

Browse files
authored
[Fusion] Check if type can be resolved when selecting best matching subgraph (#8497)
1 parent fdd5af3 commit 35bcd91

6 files changed

+390
-28
lines changed

src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,15 @@ parentSelectionPath is null
123123
var current = leftovers ?? selections;
124124
var subgraph = _config.GetBestMatchingSubgraph(
125125
operation,
126+
parentSelectionPath,
126127
current,
127128
selectionSetTypeMetadata);
129+
130+
if (subgraph is null)
131+
{
132+
throw ThrowHelper.NoResolverInContext();
133+
}
134+
128135
var executionStep = new SelectionExecutionStep(
129136
context.NextStepId(),
130137
subgraph,
@@ -232,14 +239,14 @@ parentSelectionPath is null
232239
path.RemoveAt(pathIndex);
233240
}
234241

235-
// if the current execution step has now way to resolve the data
242+
// if the current execution step has no way to resolve the data
236243
// we will try to resolve it from the root.
237244
if (executionStep.ParentSelection is not null
238245
&& executionStep.ParentSelectionPath is not null
239246
&& executionStep.Resolver is null
240247
&& executionStep.SelectionResolvers.Count == 0)
241248
{
242-
if (!EnsureStepCanBeResolvedFromRoot(
249+
if (!_config.EnsurePathCanBeResolvedFromRoot(
243250
executionStep.SubgraphName,
244251
executionStep.ParentSelectionPath))
245252
{
@@ -628,10 +635,16 @@ private SelectionExecutionStep CreateNodeNestedExecutionSteps(
628635
? availableSubgraphs[0]
629636
: _config.GetBestMatchingSubgraph(
630637
operation,
638+
null,
631639
entityTypeSelectionSet.Selections,
632640
entityTypeMetadata,
633641
availableSubgraphs);
634642

643+
if (subgraph is null)
644+
{
645+
throw ThrowHelper.NoResolverInContext();
646+
}
647+
635648
var field = nodeSelection.Field;
636649
var fieldInfo = queryTypeMetadata.Fields[field.Name];
637650
var executionStep = new SelectionExecutionStep(
@@ -914,27 +927,6 @@ private static bool IsNodeField(IObjectField field, IOperation operation)
914927
&& field.DeclaringType.Equals(operation.RootType)
915928
&& (field.Name.EqualsOrdinal("node") || field.Name.EqualsOrdinal("nodes"));
916929

917-
private bool EnsureStepCanBeResolvedFromRoot(
918-
string subgraphName,
919-
SelectionPath path)
920-
{
921-
var current = path;
922-
923-
while (current is not null)
924-
{
925-
var typeMetadata = _config.GetType<ObjectTypeMetadata>(current.Selection.DeclaringType.Name);
926-
927-
if (!typeMetadata.Fields[current.Selection.Field.Name].Bindings.ContainsSubgraph(subgraphName))
928-
{
929-
return false;
930-
}
931-
932-
current = current.Parent;
933-
}
934-
935-
return true;
936-
}
937-
938930
private readonly struct BacklogItem(
939931
ISelection parentSelection,
940932
SelectionPath? selectionPath,

src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,24 @@ namespace HotChocolate.Fusion.Planning;
55

66
internal static class QueryPlannerHelpers
77
{
8-
public static string GetBestMatchingSubgraph(
8+
public static string? GetBestMatchingSubgraph(
99
this FusionGraphConfiguration configuration,
1010
IOperation operation,
11+
SelectionPath? parentSelectionPath,
1112
IReadOnlyList<ISelection> selections,
1213
ObjectTypeMetadata typeMetadataContext,
1314
IReadOnlyList<string>? availableSubgraphs = null)
1415
{
1516
var bestScore = 0;
16-
var bestSubgraph = configuration.SubgraphNames[0];
17+
var bestSubgraph = default(string?);
1718

1819
foreach (var subgraphName in availableSubgraphs ?? configuration.SubgraphNames)
1920
{
2021
var score =
2122
EvaluateSubgraphCompatibilityScore(
2223
configuration,
2324
operation,
25+
parentSelectionPath,
2426
selections,
2527
typeMetadataContext,
2628
subgraphName);
@@ -38,23 +40,37 @@ public static string GetBestMatchingSubgraph(
3840
private static int EvaluateSubgraphCompatibilityScore(
3941
FusionGraphConfiguration configuration,
4042
IOperation operation,
43+
SelectionPath? parentSelectionPath,
4144
IReadOnlyList<ISelection> selections,
4245
ObjectTypeMetadata typeMetadataContext,
4346
string schemaName)
4447
{
4548
var score = 0;
49+
50+
var pathOrTypeCanBeResolvedFromRoot = EnsurePathOrTypeCanBeResolvedFromRoot(configuration, parentSelectionPath, typeMetadataContext, schemaName);
51+
4652
var stack = new Stack<(IReadOnlyList<ISelection> selections, ObjectTypeMetadata typeContext)>();
4753
stack.Push((selections, typeMetadataContext));
4854

4955
while (stack.Count > 0)
5056
{
5157
var (currentSelections, currentTypeContext) = stack.Pop();
5258

59+
// If there are no selections at the current node, it means the subgraph
60+
// can resolve the path up to this point without requiring any further fields, so we increase the score.
61+
if (currentSelections.Count == 0)
62+
{
63+
score++;
64+
}
65+
5366
foreach (var selection in currentSelections)
5467
{
5568
if (!selection.Field.IsIntrospectionField &&
5669
currentTypeContext.Fields[selection.Field.Name].Bindings
57-
.ContainsSubgraph(schemaName))
70+
.ContainsSubgraph(schemaName) &&
71+
(pathOrTypeCanBeResolvedFromRoot ||
72+
currentTypeContext.Fields[selection.Field.Name].Resolvers
73+
.ContainsResolvers(schemaName)))
5874
{
5975
score++;
6076

@@ -73,4 +89,36 @@ private static int EvaluateSubgraphCompatibilityScore(
7389

7490
return score;
7591
}
92+
93+
private static bool EnsurePathOrTypeCanBeResolvedFromRoot(
94+
FusionGraphConfiguration configuration,
95+
SelectionPath? parentSelectionPath,
96+
ObjectTypeMetadata typeMetadataContext,
97+
string schemaName)
98+
{
99+
return typeMetadataContext.Resolvers.ContainsResolvers(schemaName) ||
100+
configuration.EnsurePathCanBeResolvedFromRoot(schemaName, parentSelectionPath);
101+
}
102+
103+
public static bool EnsurePathCanBeResolvedFromRoot(
104+
this FusionGraphConfiguration configuration,
105+
string subgraphName,
106+
SelectionPath? path)
107+
{
108+
var current = path;
109+
110+
while (current is not null)
111+
{
112+
var typeMetadata = configuration.GetType<ObjectTypeMetadata>(current.Selection.DeclaringType.Name);
113+
114+
if (!typeMetadata.Fields[current.Selection.Field.Name].Bindings.ContainsSubgraph(subgraphName))
115+
{
116+
return false;
117+
}
118+
119+
current = current.Parent;
120+
}
121+
122+
return true;
123+
}
76124
}

src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2895,6 +2895,150 @@ query Test($number: Int!) {
28952895
Assert.Null(result.ExpectOperationResult().Errors);
28962896
}
28972897

2898+
[Fact]
2899+
public async Task Unresolvable_Subgraph_Is_Not_Chosen_If_Data_Is_Available_In_Resolvable_Subgraph()
2900+
{
2901+
// arrange
2902+
var subgraphA = await TestSubgraph.CreateAsync(
2903+
"""
2904+
type Query {
2905+
viewer: Viewer
2906+
}
2907+
2908+
type Viewer {
2909+
product: Product!
2910+
}
2911+
2912+
type Product implements Node {
2913+
id: ID!
2914+
}
2915+
2916+
interface Node {
2917+
id: ID!
2918+
}
2919+
""");
2920+
2921+
var subgraphB = await TestSubgraph.CreateAsync(
2922+
"""
2923+
type Query {
2924+
test: Test!
2925+
}
2926+
2927+
type Test {
2928+
id: ID!
2929+
}
2930+
2931+
type Product {
2932+
id: ID!
2933+
name: String!
2934+
}
2935+
""");
2936+
2937+
var subgraphC = await TestSubgraph.CreateAsync(
2938+
"""
2939+
type Query {
2940+
node(id: ID!): Node
2941+
}
2942+
2943+
type Product implements Node {
2944+
id: ID!
2945+
name: String!
2946+
}
2947+
2948+
interface Node {
2949+
id: ID!
2950+
}
2951+
""");
2952+
2953+
using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB, subgraphC]);
2954+
var executor = await subgraphs.GetExecutorAsync();
2955+
var request = """
2956+
query {
2957+
viewer {
2958+
product {
2959+
id
2960+
name
2961+
}
2962+
}
2963+
}
2964+
""";
2965+
2966+
// act
2967+
var result = await executor.ExecuteAsync(request);
2968+
2969+
// assert
2970+
MatchMarkdownSnapshot(request, result);
2971+
}
2972+
2973+
[Fact]
2974+
public async Task Subgraph_Containing_More_Selections_Is_Chosen()
2975+
{
2976+
// arrange
2977+
var subgraphA = await TestSubgraph.CreateAsync(
2978+
"""
2979+
type Query {
2980+
productBySlug: Product
2981+
}
2982+
2983+
type Product {
2984+
id: ID!
2985+
}
2986+
""");
2987+
2988+
var subgraphB = await TestSubgraph.CreateAsync(
2989+
"""
2990+
type Query {
2991+
productById(id: ID!): Product
2992+
}
2993+
2994+
type Product {
2995+
id: ID!
2996+
author: Author
2997+
}
2998+
2999+
type Author {
3000+
name: String!
3001+
}
3002+
""");
3003+
3004+
var subgraphC = await TestSubgraph.CreateAsync(
3005+
"""
3006+
type Query {
3007+
productById(id: ID!): Product
3008+
}
3009+
3010+
type Product {
3011+
id: ID!
3012+
author: Author
3013+
}
3014+
3015+
type Author {
3016+
name: String!
3017+
rating: Int!
3018+
}
3019+
""");
3020+
3021+
using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB, subgraphC]);
3022+
var executor = await subgraphs.GetExecutorAsync();
3023+
var request = """
3024+
query {
3025+
productBySlug {
3026+
author {
3027+
name
3028+
rating
3029+
}
3030+
}
3031+
}
3032+
""";
3033+
3034+
// act
3035+
var result = await executor.ExecuteAsync(request);
3036+
3037+
// assert
3038+
MatchMarkdownSnapshot(request, result);
3039+
}
3040+
3041+
28983042
public sealed class HotReloadConfiguration : IObservable<GatewayConfiguration>
28993043
{
29003044
private GatewayConfiguration _configuration;

src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Forward_Nested_Node_Variables.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ query ProductReviews($id: ID!, $first: Int!) {
2929
## QueryPlan Hash
3030

3131
```text
32-
3EB74A019DB95A4FA68CF9569951EA91659E7186
32+
90288E30B42792662254D8D4CF683A745331D191
3333
```
3434

3535
## QueryPlan
@@ -50,7 +50,7 @@ query ProductReviews($id: ID!, $first: Int!) {
5050
"type": "User",
5151
"node": {
5252
"type": "Resolve",
53-
"subgraph": "Accounts",
53+
"subgraph": "Reviews2",
5454
"document": "query ProductReviews_1($id: ID!) { node(id: $id) { ... on User { __typename } } }",
5555
"selectionSetId": 0,
5656
"forwardedVariables": [

0 commit comments

Comments
 (0)