diff --git a/src/HotChocolate/Fusion/src/Composition/FusionGraphComposer.cs b/src/HotChocolate/Fusion/src/Composition/FusionGraphComposer.cs index 4d87c747ab5..34855489646 100644 --- a/src/HotChocolate/Fusion/src/Composition/FusionGraphComposer.cs +++ b/src/HotChocolate/Fusion/src/Composition/FusionGraphComposer.cs @@ -73,6 +73,7 @@ internal FusionGraphComposer( .Use() .Use() .Use() + .Use() .Use() .Use() .Use() diff --git a/src/HotChocolate/Fusion/src/Composition/Pipeline/NodeMiddleware.cs b/src/HotChocolate/Fusion/src/Composition/Pipeline/NodeMiddleware.cs index 8a768b32f9f..f6e9b9ac710 100644 --- a/src/HotChocolate/Fusion/src/Composition/Pipeline/NodeMiddleware.cs +++ b/src/HotChocolate/Fusion/src/Composition/Pipeline/NodeMiddleware.cs @@ -13,10 +13,8 @@ public async ValueTask InvokeAsync(CompositionContext context, MergeDelegate nex if (fusionGraph.QueryType is not null && context.Features.IsNodeFieldSupported() && - fusionGraph.QueryType.Fields.TryGetField("node", out var nodeField)) + fusionGraph.QueryType.Fields.ContainsName("node")) { - fusionGraph.QueryType.Fields.TryGetField("nodes", out var nodesField); - var nodes = new HashSet(); foreach (var schema in context.Subgraphs) diff --git a/src/HotChocolate/Fusion/src/Composition/Pipeline/ViewerMiddleware.cs b/src/HotChocolate/Fusion/src/Composition/Pipeline/ViewerMiddleware.cs new file mode 100644 index 00000000000..b49013e1f56 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Composition/Pipeline/ViewerMiddleware.cs @@ -0,0 +1,35 @@ +using HotChocolate.Language; +using HotChocolate.Skimmed; + +namespace HotChocolate.Fusion.Composition.Pipeline; + +internal sealed class ViewerMiddleware : IMergeMiddleware +{ + public async ValueTask InvokeAsync(CompositionContext context, MergeDelegate next) + { + var fusionGraph = context.FusionGraph; + var fusionTypes = context.FusionTypes; + + if (fusionGraph.QueryType is not null && + fusionGraph.QueryType.Fields.TryGetField("viewer", out var viewerField) && + viewerField.Type.NamedType() is ObjectTypeDefinition { Name: "Viewer" } viewerType) + { + var viewerSelectionNode = new SelectionSetNode([new FieldNode("viewer")]); + + foreach (var subgraphSchema in context.Subgraphs) + { + if (subgraphSchema.QueryType is not null && + subgraphSchema.QueryType.Fields.TryGetField("viewer", out var subgraphViewerField) && + subgraphViewerField.Type.NamedType() is ObjectTypeDefinition { Name: "Viewer" }) + { + viewerType.Directives.Add(fusionTypes.CreateResolverDirective(subgraphSchema.Name, viewerSelectionNode)); + } + } + } + + if (!context.Log.HasErrors) + { + await next(context).ConfigureAwait(false); + } + } +} diff --git a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs index 1c27d956d6f..2c92c84cd54 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs @@ -2178,6 +2178,70 @@ type ResaleSurveyFeedback { await snapshot.MatchMarkdownAsync(); } + [Fact] + public async Task Viewer_Returned_From_Mutation_With_Selection_On_Another_Subgraph() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + viewer: Viewer + } + + type Mutation { + doSomething: DoSomethingPayload + } + + type DoSomethingPayload { + something: Int + viewer: Viewer + } + + type Viewer { + subgraphA: String + } + """); + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + viewer: Viewer + } + + type Viewer { + subgraphB: String + } + """); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + var executor = await subgraphs.GetExecutorAsync(); + + var request = Parse( + """ + mutation { + doSomething { + something + viewer { + subgraphA + subgraphB + } + } + } + + """); + + // act + await using var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + } + public sealed class HotReloadConfiguration : IObservable { private GatewayConfiguration _configuration; diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Viewer_Returned_From_Mutation_With_Selection_On_Another_Subgraph.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Viewer_Returned_From_Mutation_With_Selection_On_Another_Subgraph.md new file mode 100644 index 00000000000..be2ad1778ce --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Viewer_Returned_From_Mutation_With_Selection_On_Another_Subgraph.md @@ -0,0 +1,78 @@ +# Viewer_Returned_From_Mutation_With_Selection_On_Another_Subgraph + +## Result + +```json +{ + "data": { + "doSomething": { + "something": 123, + "viewer": { + "subgraphA": "string", + "subgraphB": "string" + } + } + } +} +``` + +## Request + +```graphql +mutation { + doSomething { + something + viewer { + subgraphA + subgraphB + } + } +} +``` + +## QueryPlan Hash + +```text +3BAD760CBB742706BF2DEDB9CA40F8DA880B22CC +``` + +## QueryPlan + +```json +{ + "document": "mutation { doSomething { something viewer { subgraphA subgraphB } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "mutation fetch_doSomething_1 { doSomething { something viewer { subgraphA } } }", + "selectionSetId": 0 + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Resolve", + "subgraph": "Subgraph_2", + "document": "query fetch_doSomething_2 { viewer { subgraphB } }", + "selectionSetId": 2, + "path": [ + "viewer" + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 2 + ] + } + ] + } +} +``` +