diff --git a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs index c0d49b3fd93..48be962571d 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs @@ -2,6 +2,7 @@ using HotChocolate.Execution; using HotChocolate.Fusion.Composition; using HotChocolate.Fusion.Composition.Features; +using HotChocolate.Fusion.Metadata; using HotChocolate.Fusion.Shared; using HotChocolate.Skimmed.Serialization; using HotChocolate.Types.Relay; @@ -17,6 +18,116 @@ public class DemoIntegrationTests(ITestOutputHelper output) { private readonly Func _logFactory = () => new TestCompositionLog(output); + [Fact] + public async Task Fetch_Product_With_Entity_Resolver_From_Two_Subgraphs_One_Subgraph_Fails_To_Resolve() + { + // arrange + using var demoProject = await DemoProject.CreateAsync(); + + var fusionGraph = await new FusionGraphComposer(logFactory: _logFactory).ComposeAsync( + new[] + { + demoProject.Products.ToConfiguration(ProductsExtensionSdl), + demoProject.Reviews.ToConfiguration(ReviewsExtensionSdl), + }, + new FusionFeatureCollection(FusionFeatures.NodeField)); + + var executor = await new ServiceCollection() + .AddSingleton(demoProject.HttpClientFactory) + .AddSingleton(demoProject.WebSocketConnectionFactory) + .AddSingleton(new UnreachableSubgraphConfigurationRewriter("Reviews")) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) + .BuildRequestExecutorAsync(); + + var request = Parse( + """ + query Product { + productById(id: "UHJvZHVjdAppMQ==") { + # Subgraph: Product + name + # Subgraph: Reviews + reviews { + body + } + } + } + """); + + // act + await using var result = await executor.ExecuteAsync( + QueryRequestBuilder + .New() + .SetQuery(request) + .Create()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result, fusionGraph); + await snapshot.MatchAsync(); + + Assert.Null(result.ExpectQueryResult().Errors); + } + + [Fact] + public async Task Fetch_User_With_Node_Field_From_Two_Subgraphs_One_Subgraph_Fails_To_Resolve() + { + // arrange + using var demoProject = await DemoProject.CreateAsync(); + + // act + var fusionGraph = await new FusionGraphComposer(logFactory: _logFactory).ComposeAsync( + new[] + { + demoProject.Reviews2.ToConfiguration(Reviews2ExtensionSdl), + demoProject.Accounts.ToConfiguration(AccountsExtensionSdl), + demoProject.Products.ToConfiguration(ProductsExtensionSdl) + }, + new FusionFeatureCollection(FusionFeatures.NodeField)); + + var executor = await new ServiceCollection() + .AddSingleton(demoProject.HttpClientFactory) + .AddSingleton(demoProject.WebSocketConnectionFactory) + .AddSingleton(new UnreachableSubgraphConfigurationRewriter("Reviews")) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) + .BuildRequestExecutorAsync(); + + var request = Parse( + """ + query FetchNode($id: ID!) { + node(id: $id) { + ... on User { + # Subgraph: Account + birthdate + # Subgraph: Reviews + reviews { + body + } + } + } + } + """); + + var idSerializer = new IdSerializer(); + var id = idSerializer.Serialize("User", 1); + + // act + await using var result = await executor.ExecuteAsync( + QueryRequestBuilder + .New() + .SetQuery(request) + .SetVariableValue("id", id) + .Create()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result, fusionGraph); + await snapshot.MatchAsync(); + + Assert.Null(result.ExpectQueryResult().Errors); + } + [Fact] public async Task Authors_And_Reviews_AutoCompose() { @@ -1667,6 +1778,31 @@ query TopProducts { Assert.Null(result.ExpectQueryResult().Errors); } + private class UnreachableSubgraphConfigurationRewriter : ConfigurationRewriter + { + private readonly string[] _unreachableSubgraphs; + + public UnreachableSubgraphConfigurationRewriter(params string[] unreachableSubgraphs) + { + _unreachableSubgraphs = unreachableSubgraphs; + } + + protected override ValueTask RewriteAsync( + Metadata.HttpClientConfiguration configuration, + CancellationToken cancellationToken) + { + if (_unreachableSubgraphs.Contains(configuration.ClientName)) + { + // This simulates the subgraph not being reachable. + return base.RewriteAsync( + configuration with { EndpointUri = new Uri("http://client") }, + cancellationToken); + } + + return base.RewriteAsync(configuration, cancellationToken); + } + } + public sealed class HotReloadConfiguration : IObservable { private GatewayConfiguration _configuration; @@ -1710,7 +1846,6 @@ public void Update() public void Dispose() { - } } } diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Fetch_Product_With_Entity_Resolver_From_Two_Subgraphs_One_Subgraph_Fails_To_Resolve.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Fetch_Product_With_Entity_Resolver_From_Two_Subgraphs_One_Subgraph_Fails_To_Resolve.snap new file mode 100644 index 00000000000..3c3b0b8bf19 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Fetch_Product_With_Entity_Resolver_From_Two_Subgraphs_One_Subgraph_Fails_To_Resolve.snap @@ -0,0 +1,266 @@ +User Request +--------------- +query Product { + productById(id: "UHJvZHVjdAppMQ==") { + name + reviews { + body + } + } +} +--------------- + +QueryPlan +--------------- +{ + "document": "query Product { productById(id: \u0022UHJvZHVjdAppMQ==\u0022) { name reviews { body } } }", + "operation": "Product", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Reviews", + "document": "query Product_1 { productById(id: \u0022UHJvZHVjdAppMQ==\u0022) { reviews { body } __fusion_exports__1: id } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Resolve", + "subgraph": "Products", + "document": "query Product_2($__fusion_exports__1: ID!) { productById(id: $__fusion_exports__1) { name } }", + "selectionSetId": 1, + "path": [ + "productById" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 1 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Product_id" + } +} +--------------- + +QueryPlan Hash +--------------- +B61955FA3636E3CD1D730D1CA24D72B0AEBFB447 +--------------- + +Result +--------------- +{ + "errors": [ + { + "message": "Internal Execution Error" + } + ], + "data": { + "productById": null + } +} +--------------- + +Fusion Graph +--------------- +schema + @fusion(version: 1) + @transport(subgraph: "Products", location: "http:\/\/localhost:5000\/graphql", kind: "HTTP") + @transport(subgraph: "Products", location: "ws:\/\/localhost:5000\/graphql", kind: "WebSocket") + @transport(subgraph: "Reviews", location: "http:\/\/localhost:5000\/graphql", kind: "HTTP") + @transport(subgraph: "Reviews", location: "ws:\/\/localhost:5000\/graphql", kind: "WebSocket") + @node(subgraph: "Products", types: [ "Product" ]) + @node(subgraph: "Reviews", types: [ "User", "Review" ]) { + query: Query + mutation: Mutation + subscription: Subscription +} + +type Query { + "Fetches an object given its ID." + node("ID of the object." id: ID!): Node + @variable(subgraph: "Products", name: "id", argument: "id") + @resolver(subgraph: "Products", select: "{ node(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) + @variable(subgraph: "Reviews", name: "id", argument: "id") + @resolver(subgraph: "Reviews", select: "{ node(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) + "Lookup nodes by a list of IDs." + nodes("The list of node IDs." ids: [ID!]!): [Node]! + @variable(subgraph: "Products", name: "ids", argument: "ids") + @resolver(subgraph: "Products", select: "{ nodes(ids: $ids) }", arguments: [ { name: "ids", type: "[ID!]!" } ]) + @variable(subgraph: "Reviews", name: "ids", argument: "ids") + @resolver(subgraph: "Reviews", select: "{ nodes(ids: $ids) }", arguments: [ { name: "ids", type: "[ID!]!" } ]) + productById(id: ID!): Product + @variable(subgraph: "Products", name: "id", argument: "id") + @resolver(subgraph: "Products", select: "{ productById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) + @variable(subgraph: "Reviews", name: "id", argument: "id") + @resolver(subgraph: "Reviews", select: "{ productById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) + reviewById(id: ID!): Review + @variable(subgraph: "Reviews", name: "id", argument: "id") + @resolver(subgraph: "Reviews", select: "{ reviewById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) + reviewOrAuthor: ReviewOrAuthor! + @resolver(subgraph: "Reviews", select: "{ reviewOrAuthor }") + reviews: [Review!]! + @resolver(subgraph: "Reviews", select: "{ reviews }") + topProducts(first: Int!): [Product!]! + @variable(subgraph: "Products", name: "first", argument: "first") + @resolver(subgraph: "Products", select: "{ topProducts(first: $first) }", arguments: [ { name: "first", type: "Int!" } ]) + userById(id: ID!): User + @variable(subgraph: "Reviews", name: "id", argument: "id") + @resolver(subgraph: "Reviews", select: "{ authorById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) +} + +type Mutation { + addReview(input: AddReviewInput!): AddReviewPayload! + @variable(subgraph: "Reviews", name: "input", argument: "input") + @resolver(subgraph: "Reviews", select: "{ addReview(input: $input) }", arguments: [ { name: "input", type: "AddReviewInput!" } ]) + uploadProductPicture(input: UploadProductPictureInput!): UploadProductPicturePayload! + @variable(subgraph: "Products", name: "input", argument: "input") + @resolver(subgraph: "Products", select: "{ uploadProductPicture(input: $input) }", arguments: [ { name: "input", type: "UploadProductPictureInput!" } ]) +} + +type Subscription { + onNewReview: Review! + @resolver(subgraph: "Reviews", select: "{ onNewReview }", kind: "SUBSCRIBE") +} + +type AddReviewPayload { + review: Review + @source(subgraph: "Reviews") +} + +type Product implements Node + @variable(subgraph: "Products", name: "Product_id", select: "id") + @variable(subgraph: "Reviews", name: "Product_id", select: "id") + @resolver(subgraph: "Products", select: "{ productById(id: $Product_id) }", arguments: [ { name: "Product_id", type: "ID!" } ]) + @resolver(subgraph: "Reviews", select: "{ productById(id: $Product_id) }", arguments: [ { name: "Product_id", type: "ID!" } ]) + @resolver(subgraph: "Products", select: "{ nodes(ids: $Product_id) { ... on Product { ... Product } } }", arguments: [ { name: "Product_id", type: "[ID!]!" } ], kind: "BATCH") { + dimension: ProductDimension! + @source(subgraph: "Products") + id: ID! + @source(subgraph: "Products") + @source(subgraph: "Reviews") + name: String! + @source(subgraph: "Products") + price: Int! + @source(subgraph: "Products") + repeat(num: Int!): Int! + @source(subgraph: "Products") + @variable(subgraph: "Products", name: "num", argument: "num") + repeatData(data: SomeDataInput!): SomeData! + @source(subgraph: "Products") + @variable(subgraph: "Products", name: "data", argument: "data") + reviews: [Review!]! + @source(subgraph: "Reviews") + weight: Int! + @source(subgraph: "Products") +} + +type ProductDimension { + size: Int! + @source(subgraph: "Products") + weight: Int! + @source(subgraph: "Products") +} + +type ProductNotFoundError implements Error { + message: String! + @source(subgraph: "Products") + productId: Int! + @source(subgraph: "Products") +} + +type Review implements Node + @variable(subgraph: "Reviews", name: "Review_id", select: "id") + @resolver(subgraph: "Reviews", select: "{ reviewById(id: $Review_id) }", arguments: [ { name: "Review_id", type: "ID!" } ]) + @resolver(subgraph: "Reviews", select: "{ nodes(ids: $Review_id) { ... on Review { ... Review } } }", arguments: [ { name: "Review_id", type: "[ID!]!" } ], kind: "BATCH") { + author: User! + @source(subgraph: "Reviews") + body: String! + @source(subgraph: "Reviews") + id: ID! + @source(subgraph: "Reviews") + product: Product! + @source(subgraph: "Reviews") +} + +type SomeData { + data: SomeData + @source(subgraph: "Products") + num: Int + @source(subgraph: "Products") +} + +type UploadProductPicturePayload { + boolean: Boolean + @source(subgraph: "Products") + errors: [UploadProductPictureError!] + @source(subgraph: "Products") +} + +type User implements Node + @source(subgraph: "Reviews", name: "Author") + @variable(subgraph: "Reviews", name: "User_id", select: "id") + @resolver(subgraph: "Reviews", select: "{ authorById(id: $User_id) }", arguments: [ { name: "User_id", type: "ID!" } ]) + @resolver(subgraph: "Reviews", select: "{ nodes(ids: $User_id) { ... on User { ... User } } }", arguments: [ { name: "User_id", type: "[ID!]!" } ], kind: "BATCH") { + id: ID! + @source(subgraph: "Reviews") + @reEncodeId + name: String! + @source(subgraph: "Reviews") + reviews: [Review!]! + @source(subgraph: "Reviews") +} + +interface Error { + message: String! +} + +"The node interface is implemented by entities that have a global unique identifier." +interface Node { + id: ID! +} + +union ReviewOrAuthor = User | Review + +union UploadProductPictureError = ProductNotFoundError + +input AddReviewInput { + authorId: Int! + body: String! + upc: Int! +} + +input SomeDataInput { + data: SomeDataInput + num: Int +} + +input UploadProductPictureInput { + file: Upload! + productId: Int! +} + +"The `Upload` scalar type represents a file upload." +scalar Upload +--------------- diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Fetch_User_With_Node_Field_From_Two_Subgraphs_One_Subgraph_Fails_To_Resolve.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Fetch_User_With_Node_Field_From_Two_Subgraphs_One_Subgraph_Fails_To_Resolve.snap new file mode 100644 index 00000000000..7940fb16db4 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Fetch_User_With_Node_Field_From_Two_Subgraphs_One_Subgraph_Fails_To_Resolve.snap @@ -0,0 +1,367 @@ +User Request +--------------- +query FetchNode($id: ID!) { + node(id: $id) { + ... on User { + birthdate + reviews { + body + } + } + } +} +--------------- + +QueryPlan +--------------- +{ + "document": "query FetchNode($id: ID!) { node(id: $id) { ... on User { birthdate reviews { body } } } }", + "operation": "FetchNode", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "ResolveNode", + "selectionId": 0, + "responseName": "node", + "branches": [ + { + "type": "Product", + "node": { + "type": "Resolve", + "subgraph": "Products", + "document": "query FetchNode_1($id: ID!) { node(id: $id) { ... on Product { __typename } } }", + "selectionSetId": 0, + "forwardedVariables": [ + { + "variable": "id" + } + ] + } + }, + { + "type": "Review", + "node": { + "type": "Resolve", + "subgraph": "Reviews2", + "document": "query FetchNode_2($id: ID!) { node(id: $id) { ... on Review { __typename } } }", + "selectionSetId": 0, + "forwardedVariables": [ + { + "variable": "id" + } + ] + } + }, + { + "type": "User", + "node": { + "type": "Resolve", + "subgraph": "Reviews2", + "document": "query FetchNode_3($id: ID!) { node(id: $id) { ... on User { reviews { body } __fusion_exports__1: id __typename } } }", + "selectionSetId": 0, + "forwardedVariables": [ + { + "variable": "id" + } + ] + } + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Resolve", + "subgraph": "Accounts", + "document": "query FetchNode_4($__fusion_exports__1: ID!) { userById(id: $__fusion_exports__1) { birthdate } }", + "selectionSetId": 3, + "path": [ + "userById" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 3 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "User_id" + } +} +--------------- + +QueryPlan Hash +--------------- +CF2FA54AB2C2B903352C6B622DDDE63DD34CE7A4 +--------------- + +Result +--------------- +{ + "data": { + "node": { + "birthdate": "1815-12-10", + "reviews": [ + { + "body": "Love it!" + }, + { + "body": "Could be better." + } + ] + } + } +} +--------------- + +Fusion Graph +--------------- +schema + @fusion(version: 1) + @transport(subgraph: "Reviews2", location: "http:\/\/localhost:5000\/graphql", kind: "HTTP") + @transport(subgraph: "Reviews2", location: "ws:\/\/localhost:5000\/graphql", kind: "WebSocket") + @transport(subgraph: "Accounts", location: "http:\/\/localhost:5000\/graphql", kind: "HTTP") + @transport(subgraph: "Accounts", location: "ws:\/\/localhost:5000\/graphql", kind: "WebSocket") + @transport(subgraph: "Products", location: "http:\/\/localhost:5000\/graphql", kind: "HTTP") + @transport(subgraph: "Products", location: "ws:\/\/localhost:5000\/graphql", kind: "WebSocket") + @node(subgraph: "Reviews2", types: [ "User", "Review" ]) + @node(subgraph: "Accounts", types: [ "User" ]) + @node(subgraph: "Products", types: [ "Product" ]) { + query: Query + mutation: Mutation + subscription: Subscription +} + +type Query { + "Fetches an object given its ID." + node("ID of the object." id: ID!): Node + @variable(subgraph: "Reviews2", name: "id", argument: "id") + @resolver(subgraph: "Reviews2", select: "{ node(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) + @variable(subgraph: "Accounts", name: "id", argument: "id") + @resolver(subgraph: "Accounts", select: "{ node(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) + @variable(subgraph: "Products", name: "id", argument: "id") + @resolver(subgraph: "Products", select: "{ node(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) + "Lookup nodes by a list of IDs." + nodes("The list of node IDs." ids: [ID!]!): [Node]! + @variable(subgraph: "Reviews2", name: "ids", argument: "ids") + @resolver(subgraph: "Reviews2", select: "{ nodes(ids: $ids) }", arguments: [ { name: "ids", type: "[ID!]!" } ]) + @variable(subgraph: "Accounts", name: "ids", argument: "ids") + @resolver(subgraph: "Accounts", select: "{ nodes(ids: $ids) }", arguments: [ { name: "ids", type: "[ID!]!" } ]) + @variable(subgraph: "Products", name: "ids", argument: "ids") + @resolver(subgraph: "Products", select: "{ nodes(ids: $ids) }", arguments: [ { name: "ids", type: "[ID!]!" } ]) + productById(id: ID!): Product + @variable(subgraph: "Reviews2", name: "id", argument: "id") + @resolver(subgraph: "Reviews2", select: "{ productById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) + @variable(subgraph: "Products", name: "id", argument: "id") + @resolver(subgraph: "Products", select: "{ productById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) + reviewById(id: ID!): Review + @variable(subgraph: "Reviews2", name: "id", argument: "id") + @resolver(subgraph: "Reviews2", select: "{ reviewById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) + reviewOrAuthor: ReviewOrAuthor! + @resolver(subgraph: "Reviews2", select: "{ reviewOrAuthor }") + reviews: [Review!]! + @resolver(subgraph: "Reviews2", select: "{ reviews }") + topProducts(first: Int!): [Product!]! + @variable(subgraph: "Products", name: "first", argument: "first") + @resolver(subgraph: "Products", select: "{ topProducts(first: $first) }", arguments: [ { name: "first", type: "Int!" } ]) + userById(id: ID!): User + @variable(subgraph: "Reviews2", name: "id", argument: "id") + @resolver(subgraph: "Reviews2", select: "{ authorById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) + @variable(subgraph: "Accounts", name: "id", argument: "id") + @resolver(subgraph: "Accounts", select: "{ userById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) + users: [User!]! + @resolver(subgraph: "Accounts", select: "{ users }") + usersById(ids: [ID!]!): [User!]! + @variable(subgraph: "Accounts", name: "ids", argument: "ids") + @resolver(subgraph: "Accounts", select: "{ usersById(ids: $ids) }", arguments: [ { name: "ids", type: "[ID!]!" } ]) + viewer: Viewer! + @resolver(subgraph: "Reviews2", select: "{ viewer }") + @resolver(subgraph: "Accounts", select: "{ viewer }") +} + +type Mutation { + addReview(input: AddReviewInput!): AddReviewPayload! + @variable(subgraph: "Reviews2", name: "input", argument: "input") + @resolver(subgraph: "Reviews2", select: "{ addReview(input: $input) }", arguments: [ { name: "input", type: "AddReviewInput!" } ]) + addUser(input: AddUserInput!): AddUserPayload! + @variable(subgraph: "Accounts", name: "input", argument: "input") + @resolver(subgraph: "Accounts", select: "{ addUser(input: $input) }", arguments: [ { name: "input", type: "AddUserInput!" } ]) + uploadProductPicture(input: UploadProductPictureInput!): UploadProductPicturePayload! + @variable(subgraph: "Products", name: "input", argument: "input") + @resolver(subgraph: "Products", select: "{ uploadProductPicture(input: $input) }", arguments: [ { name: "input", type: "UploadProductPictureInput!" } ]) +} + +type Subscription { + onNewReview: Review! + @resolver(subgraph: "Reviews2", select: "{ onNewReview }", kind: "SUBSCRIBE") +} + +type AddReviewPayload { + review: Review + @source(subgraph: "Reviews2") +} + +type AddUserPayload { + user: User + @source(subgraph: "Accounts") +} + +type Product implements Node + @variable(subgraph: "Reviews2", name: "Product_id", select: "id") + @variable(subgraph: "Products", name: "Product_id", select: "id") + @resolver(subgraph: "Reviews2", select: "{ productById(id: $Product_id) }", arguments: [ { name: "Product_id", type: "ID!" } ]) + @resolver(subgraph: "Products", select: "{ productById(id: $Product_id) }", arguments: [ { name: "Product_id", type: "ID!" } ]) + @resolver(subgraph: "Products", select: "{ nodes(ids: $Product_id) { ... on Product { ... Product } } }", arguments: [ { name: "Product_id", type: "[ID!]!" } ], kind: "BATCH") { + dimension: ProductDimension! + @source(subgraph: "Products") + id: ID! + @source(subgraph: "Reviews2") + @source(subgraph: "Products") + name: String! + @source(subgraph: "Products") + price: Int! + @source(subgraph: "Products") + repeat(num: Int!): Int! + @source(subgraph: "Products") + @variable(subgraph: "Products", name: "num", argument: "num") + repeatData(data: SomeDataInput!): SomeData! + @source(subgraph: "Products") + @variable(subgraph: "Products", name: "data", argument: "data") + reviews: [Review!]! + @source(subgraph: "Reviews2") + weight: Int! + @source(subgraph: "Products") +} + +type ProductDimension { + size: Int! + @source(subgraph: "Products") + weight: Int! + @source(subgraph: "Products") +} + +type ProductNotFoundError implements Error { + message: String! + @source(subgraph: "Products") + productId: Int! + @source(subgraph: "Products") +} + +type Review implements Node + @variable(subgraph: "Reviews2", name: "Review_id", select: "id") + @resolver(subgraph: "Reviews2", select: "{ reviewById(id: $Review_id) }", arguments: [ { name: "Review_id", type: "ID!" } ]) + @resolver(subgraph: "Reviews2", select: "{ nodes(ids: $Review_id) { ... on Review { ... Review } } }", arguments: [ { name: "Review_id", type: "[ID!]!" } ], kind: "BATCH") { + author: User! + @source(subgraph: "Reviews2") + body: String! + @source(subgraph: "Reviews2") + id: ID! + @source(subgraph: "Reviews2") + product: Product! + @source(subgraph: "Reviews2") +} + +type SomeData { + accountValue: String! + @source(subgraph: "Accounts") + data: SomeData + @source(subgraph: "Products") + num: Int + @source(subgraph: "Products") + reviewsValue: String! + @source(subgraph: "Reviews2") +} + +type UploadProductPicturePayload { + boolean: Boolean + @source(subgraph: "Products") + errors: [UploadProductPictureError!] + @source(subgraph: "Products") +} + +"The user who wrote the review." +type User implements Node + @variable(subgraph: "Reviews2", name: "User_id", select: "id") + @variable(subgraph: "Accounts", name: "User_id", select: "id") + @resolver(subgraph: "Reviews2", select: "{ authorById(id: $User_id) }", arguments: [ { name: "User_id", type: "ID!" } ]) + @resolver(subgraph: "Accounts", select: "{ userById(id: $User_id) }", arguments: [ { name: "User_id", type: "ID!" } ]) + @resolver(subgraph: "Accounts", select: "{ usersById(ids: $User_id) }", arguments: [ { name: "User_id", type: "[ID!]!" } ], kind: "BATCH") + @resolver(subgraph: "Reviews2", select: "{ nodes(ids: $User_id) { ... on User { ... User } } }", arguments: [ { name: "User_id", type: "[ID!]!" } ], kind: "BATCH") { + birthdate: Date! + @source(subgraph: "Accounts") + id: ID! + @source(subgraph: "Reviews2") + @source(subgraph: "Accounts") + name: String! + @source(subgraph: "Reviews2") + @source(subgraph: "Accounts") + reviews: [Review!]! + @source(subgraph: "Reviews2") + username: String! + @source(subgraph: "Accounts") +} + +type Viewer { + data: SomeData! + @source(subgraph: "Reviews2") + @source(subgraph: "Accounts") + latestReview: Review + @source(subgraph: "Reviews2") + user: User + @source(subgraph: "Accounts") +} + +interface Error { + message: String! +} + +"The node interface is implemented by entities that have a global unique identifier." +interface Node { + id: ID! +} + +union ReviewOrAuthor = User | Review + +union UploadProductPictureError = ProductNotFoundError + +input AddReviewInput { + authorId: Int! + body: String! + upc: Int! +} + +input AddUserInput { + birthdate: Date! + name: String! + username: String! +} + +input SomeDataInput { + data: SomeDataInput + num: Int +} + +input UploadProductPictureInput { + file: Upload! + productId: Int! +} + +"The `Date` scalar represents an ISO-8601 compliant date type." +scalar Date + +"The `Upload` scalar type represents a file upload." +scalar Upload +---------------