From c0e9d196f39490dd4c0843ba408ea241c0a4a85e Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Sun, 9 Mar 2025 19:36:47 +0100 Subject: [PATCH] Add failing test for same selection below field returning list of union --- .../test/Core.Tests/DemoIntegrationTests.cs | 148 +++++++ .../Fusion/test/Core.Tests/UnionTests.cs | 373 +++++++++++++++++- ...That_Require_Data_From_Another_Subgraph.md | 153 +++++++ ...That_Require_Data_From_Another_Subgraph.md | 125 ++++++ ..._With_Differing_Union_Item_Dependencies.md | 191 +++++++++ ..._Union_Item_Dependencies_SameSelections.md | 189 +++++++++ ...ches_With_Differing_Resolve_Nodes_Item1.md | 124 ++++++ ...ches_With_Differing_Resolve_Nodes_Item2.md | 124 ++++++ .../Fusion/test/Shared/TestSubgraph.cs | 8 +- 9 files changed, 1425 insertions(+), 10 deletions(-) create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_List_Fields_That_Require_Data_From_Another_Subgraph.md create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_Object_Types_That_Require_Data_From_Another_Subgraph.md create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies.md create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies_SameSelections.md create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item1.md create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item2.md diff --git a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs index 1c27d956d6f..5d7ae917c3c 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs @@ -15,6 +15,154 @@ public class DemoIntegrationTests(ITestOutputHelper output) { private readonly Func _logFactory = () => new TestCompositionLog(output); + [Fact] + public async Task Same_Selection_On_Two_Object_Types_That_Require_Data_From_Another_Subgraph() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + item1: Item1! + item2: Item2! + } + + type Item1 { + product: Product! + } + + type Item2 { + product: Product! + } + + type Product implements Node { + id: ID! + } + + interface Node { + id: ID! + } + """); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + node(id: ID!): Node + nodes(ids: [ID!]!): [Node]! + } + + type Product implements Node { + id: ID! + name: String! + } + + interface Node { + id: ID! + } + """); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + item1 { + product { + id + name + } + } + item2 { + product { + id + name + } + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + } + + [Fact] + public async Task Same_Selection_On_Two_List_Fields_That_Require_Data_From_Another_Subgraph() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + interface Node { + id: ID! + } + + type Query { + productsA: [Product] + productsB: [Product] + } + + type Product implements Node { + id: ID! + name: String! + } + """); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + interface Node { + id: ID! + } + + type Query { + node(id: ID!): Node + nodes(ids: [ID!]!): [Node]! + } + + type Product implements Node { + id: ID! + price: Float! + reviewCount: Int! + } + """); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + productsA { + id + name + price + reviewCount + } + productsB { + id + name + price + reviewCount + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + } + [Fact] public async Task Authors_And_Reviews_AutoCompose() { diff --git a/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs index 94eef46c543..a90249fc4dd 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs @@ -4,6 +4,7 @@ using HotChocolate.Fusion.Shared; using HotChocolate.Skimmed.Serialization; using HotChocolate.Types; +using HotChocolate.Types.Relay; using Microsoft.Extensions.DependencyInjection; using Xunit.Abstractions; using static HotChocolate.Fusion.Shared.DemoProjectSchemaExtensions; @@ -12,14 +13,9 @@ namespace HotChocolate.Fusion; -public class UnionTests +public class UnionTests(ITestOutputHelper output) { - private readonly Func _logFactory; - - public UnionTests(ITestOutputHelper output) - { - _logFactory = () => new TestCompositionLog(output); - } + private readonly Func _logFactory = () => new TestCompositionLog(output); [Fact] public async Task Error_Union_With_Inline_Fragment() @@ -263,6 +259,369 @@ mutation Upload($input: UploadProductPictureInput!) { await snapshot.MatchMarkdownAsync(cts.Token); } + [Fact] + public async Task Union_Two_Branches_With_Differing_Resolve_Nodes_Item1() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .ModifyOptions(o => o.EnsureAllNodesCanBeResolved = false) + .AddGlobalObjectIdentification(); + }); + + var subgraphB = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + var subgraphC = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB, subgraphC]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + union(item: 1) { + ... on Item1 { + something + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + Assert.False(subgraphC.HasReceivedRequest); + } + + [Fact] + public async Task Union_Two_Branches_With_Differing_Resolve_Nodes_Item2() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .ModifyOptions(o => o.EnsureAllNodesCanBeResolved = false) + .AddGlobalObjectIdentification(); + }); + + var subgraphB = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + var subgraphC = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB, subgraphC]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + union(item: 3) { + ... on Item1 { + something + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + Assert.False(subgraphB.HasReceivedRequest); + } + + [Fact(Skip = "Fix with new planner")] + public async Task Union_List_With_Differing_Union_Item_Dependencies_SameSelections() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .ModifyOptions(o => o.EnsureAllNodesCanBeResolved = false) + .AddGlobalObjectIdentification(); + }); + + var subgraphB = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + listOfUnion { + __typename + ... on Item1 { + something + product { + id + name + } + } + ... on Item2 { + other + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + // Ideally it would just be one request, but that's for another day... + Assert.Equal(3, subgraphB.NumberOfReceivedRequests); + } + + [Fact] + public async Task Union_List_With_Differing_Union_Item_Dependencies() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .ModifyOptions(o => o.EnsureAllNodesCanBeResolved = false) + .AddGlobalObjectIdentification(); + }); + + var subgraphB = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + listOfUnion { + __typename + ... on Item1 { + something + product { + id + name + } + } + ... on Item2 { + other + product { + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + // Ideally it would just be one request, but that's for another day... + Assert.Equal(3, subgraphB.NumberOfReceivedRequests); + } + + [ObjectType("Query")] + public class SubgraphA_Query + { + public ISomeUnion GetUnion(int item) + { + return item switch + { + 1 => new SubgraphA_Item1("Something", new SubgraphA_Product(1)), + _ => new SubgraphA_Item3(true, new SubgraphA_Review(2)) + }; + } + + public List GetListOfUnion() + { + return + [ + new SubgraphA_Item1("Something", new SubgraphA_Product(1)), + new SubgraphA_Item2(123, new SubgraphA_Product(2)), + new SubgraphA_Item3(true, new SubgraphA_Review(3)), + new SubgraphA_Item1("Something", new SubgraphA_Product(4)), + new SubgraphA_Item2(123, new SubgraphA_Product(5)), + new SubgraphA_Item3(true, new SubgraphA_Review(6)) + ]; + } + } + + [UnionType("SomeUnion")] + public interface ISomeUnion + { + } + + [ObjectType("Item1")] + public record SubgraphA_Item1(string Something, SubgraphA_Product Product) : ISomeUnion; + + [ObjectType("Item2")] + public record SubgraphA_Item2(int Other, SubgraphA_Product Product) : ISomeUnion; + + [ObjectType("Item3")] + public record SubgraphA_Item3(bool Another, SubgraphA_Review Review) : ISomeUnion; + + [Node] + [ObjectType("Product")] + public record SubgraphA_Product(int Id); + + [Node] + [ObjectType("Product")] + public record SubgraphB_Product(int Id, string Name) + { + [NodeResolver] + public static SubgraphB_Product Get(int id) + => new SubgraphB_Product(id, "Product_" + id); + } + + [Node] + [ObjectType("Review")] + public record SubgraphA_Review(int Id); + + [Node] + [ObjectType("Review")] + public record SubgraphB_Review(int Id, int Score) + { + [NodeResolver] + public static SubgraphB_Review Get(int id) + => new SubgraphB_Review(id, id % 5); + } + private sealed class NoWebSockets : IWebSocketConnectionFactory { public IWebSocketConnection CreateConnection(string name) diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_List_Fields_That_Require_Data_From_Another_Subgraph.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_List_Fields_That_Require_Data_From_Another_Subgraph.md new file mode 100644 index 00000000000..f8508e0e3be --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_List_Fields_That_Require_Data_From_Another_Subgraph.md @@ -0,0 +1,153 @@ +# Same_Selection_On_Two_List_Fields_That_Require_Data_From_Another_Subgraph + +## Result + +```json +{ + "data": { + "productsA": [ + { + "id": "1", + "name": "string", + "price": 123.456, + "reviewCount": 123 + }, + { + "id": "2", + "name": "string", + "price": 123.456, + "reviewCount": 123 + }, + { + "id": "3", + "name": "string", + "price": 123.456, + "reviewCount": 123 + } + ], + "productsB": [ + { + "id": "4", + "name": "string", + "price": 123.456, + "reviewCount": 123 + }, + { + "id": "5", + "name": "string", + "price": 123.456, + "reviewCount": 123 + }, + { + "id": "6", + "name": "string", + "price": 123.456, + "reviewCount": 123 + } + ] + } +} +``` + +## Request + +```graphql +{ + productsA { + id + name + price + reviewCount + } + productsB { + id + name + price + reviewCount + } +} +``` + +## QueryPlan Hash + +```text +57DE21D1B552226339A985FBFA65E0DA2E33703C +``` + +## QueryPlan + +```json +{ + "document": "{ productsA { id name price reviewCount } productsB { id name price reviewCount } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_productsA_productsB_1 { productsA { id name __fusion_exports__1: id } productsB { id name __fusion_exports__2: id } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_productsA_productsB_2($__fusion_exports__1: [ID!]!) { nodes(ids: $__fusion_exports__1) { ... on Product { price reviewCount __fusion_exports__1: id } } }", + "selectionSetId": 1, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_productsA_productsB_3($__fusion_exports__2: [ID!]!) { nodes(ids: $__fusion_exports__2) { ... on Product { price reviewCount __fusion_exports__2: id } } }", + "selectionSetId": 2, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 1, + 2 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Product_id", + "__fusion_exports__2": "Product_id" + } +} +``` + diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_Object_Types_That_Require_Data_From_Another_Subgraph.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_Object_Types_That_Require_Data_From_Another_Subgraph.md new file mode 100644 index 00000000000..bf981d0d007 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_Object_Types_That_Require_Data_From_Another_Subgraph.md @@ -0,0 +1,125 @@ +# Same_Selection_On_Two_Object_Types_That_Require_Data_From_Another_Subgraph + +## Result + +```json +{ + "data": { + "item1": { + "product": { + "id": "2", + "name": "string" + } + }, + "item2": { + "product": { + "id": "1", + "name": "string" + } + } + } +} +``` + +## Request + +```graphql +{ + item1 { + product { + id + name + } + } + item2 { + product { + id + name + } + } +} +``` + +## QueryPlan Hash + +```text +E78C88B24F708E43B14A881AD2A993668E068FE2 +``` + +## QueryPlan + +```json +{ + "document": "{ item1 { product { id name } } item2 { product { id name } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_item1_item2_1 { item1 { product { id __fusion_exports__1: id } } item2 { product { id __fusion_exports__2: id } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_2", + "document": "query fetch_item1_item2_2($__fusion_exports__1: ID!) { node(id: $__fusion_exports__1) { ... on Product { name } } }", + "selectionSetId": 3, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Resolve", + "subgraph": "Subgraph_2", + "document": "query fetch_item1_item2_3($__fusion_exports__2: ID!) { node(id: $__fusion_exports__2) { ... on Product { name } } }", + "selectionSetId": 4, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 3, + 4 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Product_id", + "__fusion_exports__2": "Product_id" + } +} +``` + diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies.md new file mode 100644 index 00000000000..182019a3c69 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies.md @@ -0,0 +1,191 @@ +# Union_List_With_Differing_Union_Item_Dependencies + +## Result + +```json +{ + "data": { + "listOfUnion": [ + { + "__typename": "Item1", + "something": "Something", + "product": { + "id": "UHJvZHVjdDox", + "name": "Product_1" + } + }, + { + "__typename": "Item2", + "other": 123, + "product": { + "name": "Product_2" + } + }, + { + "__typename": "Item3", + "another": true, + "review": { + "id": "UmV2aWV3OjM=", + "score": 3 + } + }, + { + "__typename": "Item1", + "something": "Something", + "product": { + "id": "UHJvZHVjdDo0", + "name": "Product_4" + } + }, + { + "__typename": "Item2", + "other": 123, + "product": { + "name": "Product_5" + } + }, + { + "__typename": "Item3", + "another": true, + "review": { + "id": "UmV2aWV3OjY=", + "score": 1 + } + } + ] + } +} +``` + +## Request + +```graphql +{ + listOfUnion { + __typename + ... on Item1 { + something + product { + id + name + } + } + ... on Item2 { + other + product { + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } +} +``` + +## QueryPlan Hash + +```text +6F3D15770F165F5A7166C5598F4B1A7D6910A88D +``` + +## QueryPlan + +```json +{ + "document": "{ listOfUnion { __typename ... on Item1 { something product { id name } } ... on Item2 { other product { name } } ... on Item3 { another review { id score } } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_listOfUnion_1 { listOfUnion { __typename ... on Item3 { __typename another review { id __fusion_exports__1: id } } ... on Item2 { __typename other product { __fusion_exports__2: id } } ... on Item1 { __typename something product { id __fusion_exports__3: id } } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + }, + { + "variable": "__fusion_exports__3" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_2($__fusion_exports__1: [ID!]!) { nodes(ids: $__fusion_exports__1) { ... on Review { score __fusion_exports__1: id } } }", + "selectionSetId": 4, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_3($__fusion_exports__2: [ID!]!) { nodes(ids: $__fusion_exports__2) { ... on Product { name __fusion_exports__2: id } } }", + "selectionSetId": 5, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_4($__fusion_exports__3: [ID!]!) { nodes(ids: $__fusion_exports__3) { ... on Product { name __fusion_exports__3: id } } }", + "selectionSetId": 6, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__3" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 4, + 5, + 6 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Review_id", + "__fusion_exports__2": "Product_id", + "__fusion_exports__3": "Product_id" + } +} +``` + diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies_SameSelections.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies_SameSelections.md new file mode 100644 index 00000000000..264d103c1e9 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies_SameSelections.md @@ -0,0 +1,189 @@ +# Union_List_With_Differing_Union_Item_Dependencies_SameSelections + +## Result + +```json +{ + "data": { + "listOfUnion": [ + { + "__typename": "Item1", + "something": "Something", + "product": { + "id": "UHJvZHVjdDox", + "name": "Product_1" + } + }, + { + "__typename": "Item2", + "other": 123, + "product": { + "id": "UHJvZHVjdDoy", + "name": "Product_2" + } + }, + { + "__typename": "Item3", + "another": true, + "review": { + "id": "UmV2aWV3OjM=", + "score": 3 + } + }, + { + "__typename": "Item1", + "something": "Something", + "product": { + "id": "UHJvZHVjdDo0", + "name": "Product_4" + } + }, + { + "__typename": "Item2", + "other": 123, + "product": { + "id": "UHJvZHVjdDo1", + "name": "Product_5" + } + }, + { + "__typename": "Item3", + "another": true, + "review": { + "id": "UmV2aWV3OjY=", + "score": 1 + } + } + ] + } +} +``` + +## Request + +```graphql +{ + listOfUnion { + __typename + ... on Item1 { + something + product { + id + name + } + } + ... on Item2 { + other + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } +} +``` + +## QueryPlan Hash + +```text +51CA519135EDC9C49C71D22D7EF0562D417753EA +``` + +## QueryPlan + +```json +{ + "document": "{ listOfUnion { __typename ... on Item1 { something product { id name } } ... on Item2 { other product { id name } } ... on Item3 { another review { id score } } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_listOfUnion_1 { listOfUnion { __typename ... on Item3 { __typename another review { id __fusion_exports__1: id } } ... on Item2 { __typename other product { id __fusion_exports__2: id } } ... on Item1 { __typename something product { id __fusion_exports__2: id } } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_2($__fusion_exports__1: [ID!]!) { nodes(ids: $__fusion_exports__1) { ... on Review { score __fusion_exports__1: id } } }", + "selectionSetId": 4, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_3($__fusion_exports__2: [ID!]!) { nodes(ids: $__fusion_exports__2) { ... on Product { name __fusion_exports__2: id } } }", + "selectionSetId": 5, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_4($__fusion_exports__2: [ID!]!) { nodes(ids: $__fusion_exports__2) { ... on Product { name __fusion_exports__2: id } } }", + "selectionSetId": 5, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 4, + 5 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Review_id", + "__fusion_exports__2": "Product_id" + } +} +``` + diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item1.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item1.md new file mode 100644 index 00000000000..3202ff88d96 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item1.md @@ -0,0 +1,124 @@ +# Union_Two_Branches_With_Differing_Resolve_Nodes_Item1 + +## Result + +```json +{ + "data": { + "union": { + "something": "Something", + "product": { + "id": "UHJvZHVjdDox", + "name": "Product_1" + } + } + } +} +``` + +## Request + +```graphql +{ + union(item: 1) { + ... on Item1 { + something + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } +} +``` + +## QueryPlan Hash + +```text +6162CA1B5815FE006F5BEC9B4A2F51035B1990DC +``` + +## QueryPlan + +```json +{ + "document": "{ union(item: 1) { ... on Item1 { something product { id name } } ... on Item3 { another review { id score } } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_union_1 { union(item: 1) { __typename ... on Item3 { another review { id __fusion_exports__1: id } } ... on Item2 { } ... on Item1 { something product { id __fusion_exports__2: id } } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_3", + "document": "query fetch_union_2($__fusion_exports__1: ID!) { node(id: $__fusion_exports__1) { ... on Review { score } } }", + "selectionSetId": 4, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Resolve", + "subgraph": "Subgraph_2", + "document": "query fetch_union_3($__fusion_exports__2: ID!) { node(id: $__fusion_exports__2) { ... on Product { name } } }", + "selectionSetId": 5, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 4, + 5 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Review_id", + "__fusion_exports__2": "Product_id" + } +} +``` + diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item2.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item2.md new file mode 100644 index 00000000000..3c7d274db82 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item2.md @@ -0,0 +1,124 @@ +# Union_Two_Branches_With_Differing_Resolve_Nodes_Item2 + +## Result + +```json +{ + "data": { + "union": { + "another": true, + "review": { + "id": "UmV2aWV3OjI=", + "score": 2 + } + } + } +} +``` + +## Request + +```graphql +{ + union(item: 3) { + ... on Item1 { + something + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } +} +``` + +## QueryPlan Hash + +```text +18FEEDD69A2D3612AD7DAAA5D221864EE5A17514 +``` + +## QueryPlan + +```json +{ + "document": "{ union(item: 3) { ... on Item1 { something product { id name } } ... on Item3 { another review { id score } } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_union_1 { union(item: 3) { __typename ... on Item3 { another review { id __fusion_exports__1: id } } ... on Item2 { } ... on Item1 { something product { id __fusion_exports__2: id } } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_3", + "document": "query fetch_union_2($__fusion_exports__1: ID!) { node(id: $__fusion_exports__1) { ... on Review { score } } }", + "selectionSetId": 4, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Resolve", + "subgraph": "Subgraph_2", + "document": "query fetch_union_3($__fusion_exports__2: ID!) { node(id: $__fusion_exports__2) { ... on Product { name } } }", + "selectionSetId": 5, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 4, + 5 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Review_id", + "__fusion_exports__2": "Product_id" + } +} +``` + diff --git a/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs b/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs index f2800c068a1..27f1fc9e5e9 100644 --- a/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs +++ b/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs @@ -48,7 +48,7 @@ public static async Task CreateAsync( { app.Use(next => context => { - testContext.HasReceivedRequest = true; + testContext.NumberOfReceivedRequests++; return next(context); }); @@ -60,10 +60,12 @@ public static async Task CreateAsync( return new TestSubgraph(testServer, schema, testContext, extensions, isOffline); } - public bool HasReceivedRequest => Context.HasReceivedRequest; + public int NumberOfReceivedRequests => Context.NumberOfReceivedRequests; + + public bool HasReceivedRequest => Context.NumberOfReceivedRequests > 0; } public class SubgraphTestContext { - public bool HasReceivedRequest { get; set; } + public int NumberOfReceivedRequests { get; set; } }