diff --git a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs index d6a7be5e7e9..9413161336d 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs @@ -16,6 +16,79 @@ public class DemoIntegrationTests(ITestOutputHelper output) { private readonly Func _logFactory = () => new TestCompositionLog(output); + [Fact] + public async Task Test() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + interface Node { + id: ID! + } + + type Query { + productsA: [Product] + productsB: [Product] + node(id: ID!): Node + nodes(ids: [ID!]!): [Node]! + } + + 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 60392133e65..8d03ebabc05 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs @@ -5,6 +5,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; @@ -13,14 +14,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() @@ -264,6 +260,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] + 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.Test.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Test.md new file mode 100644 index 00000000000..a00e2d961fc --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Test.md @@ -0,0 +1,191 @@ +# Test + +## Result + +```json +{ + "errors": [ + { + "message": "Cannot return null for non-nullable field.", + "locations": [ + { + "line": 11, + "column": 5 + } + ], + "path": [ + "productsB", + 2, + "price" + ], + "extensions": { + "code": "HC0018" + } + }, + { + "message": "Cannot return null for non-nullable field.", + "locations": [ + { + "line": 11, + "column": 5 + } + ], + "path": [ + "productsB", + 1, + "price" + ], + "extensions": { + "code": "HC0018" + } + }, + { + "message": "Cannot return null for non-nullable field.", + "locations": [ + { + "line": 11, + "column": 5 + } + ], + "path": [ + "productsB", + 0, + "price" + ], + "extensions": { + "code": "HC0018" + } + } + ], + "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": [ + null, + null, + null + ] + } +} +``` + +## 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__/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..a4f8111a225 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies_SameSelections.md @@ -0,0 +1,212 @@ +# Union_List_With_Differing_Union_Item_Dependencies_SameSelections + +## Result + +```json +{ + "errors": [ + { + "message": "Cannot return null for non-nullable field.", + "locations": [ + { + "line": 15, + "column": 9 + } + ], + "path": [ + "listOfUnion", + 4, + "product", + "name" + ], + "extensions": { + "code": "HC0018" + } + }, + { + "message": "Cannot return null for non-nullable field.", + "locations": [ + { + "line": 15, + "column": 9 + } + ], + "path": [ + "listOfUnion", + 3, + "product", + "name" + ], + "extensions": { + "code": "HC0018" + } + }, + { + "message": "Cannot return null for non-nullable field.", + "locations": [ + { + "line": 15, + "column": 9 + } + ], + "path": [ + "listOfUnion", + 1, + "product", + "name" + ], + "extensions": { + "code": "HC0018" + } + }, + { + "message": "Cannot return null for non-nullable field.", + "locations": [ + { + "line": 15, + "column": 9 + } + ], + "path": [ + "listOfUnion", + 0, + "product", + "name" + ], + "extensions": { + "code": "HC0018" + } + } + ], + "data": null +} +``` + +## 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 f23d4b0f40b..5406d830b90 100644 --- a/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs +++ b/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs @@ -46,7 +46,7 @@ public static async Task CreateAsync( { app.Use(next => context => { - testContext.HasReceivedRequest = true; + testContext.NumberOfReceivedRequests++; return next(context); }); @@ -58,10 +58,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; } }