From f28426455398f93f76947e6916b9411ca135c860 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 11 Aug 2022 23:30:13 +0200 Subject: [PATCH] Added initial end to end integration for the new gateway. (#5284) --- .../test/CookieCrumble.Tests/SnapshotTests.cs | 3 +- .../Execution/HotChocolate.Execution.csproj | 2 + .../Execution/Processing/Result/ListResult.cs | 20 + .../Processing/Result/ObjectResult.cs | 28 +- .../src/Execution/Processing/Selection.cs | 14 +- .../src/Execution/Processing/SelectionSet.cs | 4 +- .../Types.Mutations/Errors/ErrorMiddleware.cs | 6 +- .../MutationConventionMiddleware.cs | 2 +- .../OffsetPagingProvider.cs | 17 +- .../Types/Execution/Processing/ISelection.cs | 7 +- .../SchemaBuilderExtensions.Document.cs | 5 + .../QueryableFilterVisitorComparableTests.cs | 4 +- .../QueryableFilterVisitorIdTests.cs | 3 +- .../QueryableFilterVisitorObjectTests.cs | 2 +- .../QueryableFilterVisitorComparableTests.cs | 4 +- .../QueryableFilterVisitorObjectTests.cs | 2 +- .../test/Data.Filters.Tests/ExtensionsTest.cs | 4 +- .../Fusion/src/Core/Execution/Argument.cs | 16 + .../src/Core/Execution/ArgumentContext.cs | 65 +++ .../src/Core/Execution/ExecutionState.cs | 128 +++++ .../src/Core/Execution/HttpRequestExecutor.cs | 90 ++++ .../src/Core/Execution/IExecutionState.cs | 11 + .../Core/Execution/IRemoteRequestExecutor.cs | 8 + .../Core/Execution/JsonRequestFormatter.cs | 102 ++++ .../Fusion/src/Core/Execution/JsonResult.cs | 16 + .../Core/Execution/RemoteExecutorContext.cs | 35 ++ .../Core/Execution/RemoteQueryExecutor.txt | 142 +++++ .../Core/Execution/RemoteQueryExecutor2.cs | 390 ++++++++++++++ .../Execution/RemoteRequestExecutorFactory.cs | 14 + .../Fusion/src/Core/Execution/Request.cs | 26 + .../src/Core/Execution/RequestHandler.cs | 118 +++++ .../Fusion/src/Core/Execution/Response.cs | 25 + .../src/Core/Execution/SelectionResult.cs | 73 +++ .../Fusion/src/Core/Execution/WorkItem.cs | 36 ++ .../src/Core/HotChocolate.Fusion.csproj | 29 +- .../Metadata/ArgumentVariableDefinition.cs | 19 + .../ArgumentVariableDefinitionCollection.cs | 33 ++ .../src/Core/Metadata/FetchDefinition.cs | 159 ++++++ .../Metadata/FetchDefinitionCollection.cs | 41 ++ .../Core/Metadata/FieldVariableDefinition.cs | 23 + .../src/Core/Metadata/HttpClientConfig.cs | 14 + .../Fusion/src/Core/Metadata/IType.cs | 6 + .../src/Core/Metadata/IVariableDefinition.cs | 10 + .../Fusion/src/Core/Metadata/MemberBinding.cs | 32 ++ .../Core/Metadata/MemberBindingCollection.cs | 27 + .../Fusion/src/Core/Metadata/ObjectField.cs | 24 + .../Core/Metadata/ObjectFieldCollection.cs | 25 + .../Fusion/src/Core/Metadata/ObjectType.cs | 26 + .../src/Core/Metadata/ServiceConfiguration.cs | 486 ++++++++++++++++++ .../Metadata/VariableDefinitionCollection.cs | 28 + .../Fusion/src/Core/Planning/ExecutionNode.cs | 33 ++ .../src/Core/Planning/ExecutionPlanBuilder.cs | 308 +++++++++++ .../src/Core/Planning/ExportDefinition.cs | 27 + .../Core/Planning/ExportDefinitionRegistry.cs | 91 ++++ .../src/Core/Planning/IExecutionStep.cs | 36 ++ .../Fusion/src/Core/Planning/QueryPlan.cs | 39 ++ .../src/Core/Planning/QueryPlanContext.cs | 29 ++ .../Fusion/src/Core/Planning/RequestNode.cs | 13 + .../Fusion/src/Core/Planning/RequestPlaner.cs | 377 ++++++++++++++ .../src/Core/Planning/RequirementsPlaner.cs | 149 ++++++ .../Fusion/src/Core/Planning/RootSelection.cs | 17 + .../Core/Planning/SelectionExecutionStep.cs | 53 ++ .../Utilities/JsonQueryResultFormatter.cs | 447 ++++++++++++++++ .../JsonValueToGraphQLValueConverter.cs | 52 ++ .../Core.Tests/ExecutionPlanBuilderTests.cs | 407 +++++++++++++++ .../HotChocolate.Fusion.Tests.csproj | 25 +- .../Core.Tests/RemoteQueryExecutorTests.cs | 258 ++++++++++ .../test/Core.Tests/TestServerFactory.cs | 36 ++ .../Fusion/test/Core.Tests/UnitTest1.cs | 10 - .../Core.Tests/__resources__/dummy.graphql | 24 + ...anBuilderTests.GetPersonById_With_Bio.snap | 21 + ...ts.GetPersonById_With_Bio_Friends_Bio.snap | 44 ++ ...Tests.GetPersonById_With_Name_And_Bio.snap | 31 ++ ...PersonById_With_Name_Friends_Name_Bio.snap | 36 ++ .../RemoteQueryExecutorTests.Do.snap | 59 +++ .../Fusion/test/Directory.Build.props | 6 +- .../Language.Utf8/Utf8GraphQLParser.Syntax.cs | 37 ++ .../src/Language.Utf8/Utf8GraphQLParser.cs | 5 + .../MongoDbFilterVisitorComparableTests.cs | 4 +- .../MongoDbFilterVisitorObjectTests.cs | 2 +- .../Comparable/Neo4JFilterComparableTests.cs | 4 +- .../Utilities/HotChocolate.Utilities.csproj | 1 + 82 files changed, 5020 insertions(+), 65 deletions(-) create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/Argument.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/ArgumentContext.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/ExecutionState.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/HttpRequestExecutor.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/IExecutionState.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/IRemoteRequestExecutor.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/JsonRequestFormatter.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/JsonResult.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/RemoteExecutorContext.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/RemoteQueryExecutor.txt create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/RemoteQueryExecutor2.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/RemoteRequestExecutorFactory.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/Request.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/RequestHandler.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/Response.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/SelectionResult.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/WorkItem.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/ArgumentVariableDefinition.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/ArgumentVariableDefinitionCollection.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/FetchDefinition.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/FetchDefinitionCollection.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/FieldVariableDefinition.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/HttpClientConfig.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/IType.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/IVariableDefinition.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/MemberBinding.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/MemberBindingCollection.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/ObjectField.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/ObjectFieldCollection.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/ObjectType.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfiguration.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/VariableDefinitionCollection.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Planning/ExecutionNode.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Planning/ExecutionPlanBuilder.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Planning/ExportDefinition.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Planning/ExportDefinitionRegistry.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Planning/IExecutionStep.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Planning/QueryPlan.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Planning/QueryPlanContext.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Planning/RequestNode.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Planning/RequestPlaner.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Planning/RequirementsPlaner.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Planning/RootSelection.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Planning/SelectionExecutionStep.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Utilities/JsonQueryResultFormatter.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Utilities/JsonValueToGraphQLValueConverter.cs create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/ExecutionPlanBuilderTests.cs create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/TestServerFactory.cs delete mode 100644 src/HotChocolate/Fusion/test/Core.Tests/UnitTest1.cs create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__resources__/dummy.graphql create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Bio.snap create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Bio_Friends_Bio.snap create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_And_Bio.snap create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_Friends_Name_Bio.snap create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RemoteQueryExecutorTests.Do.snap diff --git a/src/CookieCrumble/test/CookieCrumble.Tests/SnapshotTests.cs b/src/CookieCrumble/test/CookieCrumble.Tests/SnapshotTests.cs index c74de68d105..5f29d469d52 100644 --- a/src/CookieCrumble/test/CookieCrumble.Tests/SnapshotTests.cs +++ b/src/CookieCrumble/test/CookieCrumble.Tests/SnapshotTests.cs @@ -88,7 +88,8 @@ public void SnapshotBuilder_Segment_Custom_Serializer_For_Segment() { var snapshot = new Snapshot(); snapshot.Add(new MyClass()); - snapshot.Add(new MyClass { Foo = "Bar" }, "Bar:", new CustomSerializer()); + snapshot.Add(new MyClass { Foo = "Baz" }, "Bar:", new CustomSerializer()); + snapshot.Add(new MyClass { Foo = "Baz" }); snapshot.Add(new MyClass { Foo = "Baz" }); snapshot.Match(); } diff --git a/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj b/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj index 1fd76b2af15..09e37d4b625 100644 --- a/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj +++ b/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj @@ -14,6 +14,8 @@ + + diff --git a/src/HotChocolate/Core/src/Execution/Processing/Result/ListResult.cs b/src/HotChocolate/Core/src/Execution/Processing/Result/ListResult.cs index a1a51f1f3a8..feda7651954 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/Result/ListResult.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/Result/ListResult.cs @@ -37,9 +37,29 @@ public sealed class ListResult : ResultData, IReadOnlyList internal void AddUnsafe(object? item) => _buffer[_count++] = item; + internal void AddUnsafe(ResultData? item) + { + if (item is not null) + { + item.Parent = this; + } + + _buffer[_count++] = item; + } + internal void SetUnsafe(int index, object? item) => _buffer[index] = item; + internal void SetUnsafe(int index, ResultData? item) + { + if (item is not null) + { + item.Parent = this; + } + + _buffer[index] = item; + } + /// /// Ensures that the result object has enough capacity on the buffer /// to store the expected fields. diff --git a/src/HotChocolate/Core/src/Execution/Processing/Result/ObjectResult.cs b/src/HotChocolate/Core/src/Execution/Processing/Result/ObjectResult.cs index 5f8e52f2f94..d88f0668699 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/Result/ObjectResult.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/Result/ObjectResult.cs @@ -56,6 +56,32 @@ public sealed class ObjectResult internal void SetValueUnsafe(int index, string name, object? value, bool isNullable = true) => _buffer[index].Set(name, value, isNullable); + /// + /// Sets a field value in the buffer. + /// Note: Set will not validate if the buffer has enough space. + /// + /// + /// The index in the buffer on which the value shall be stored. + /// + /// + /// The name of the field. + /// + /// + /// The field value. + /// + /// + /// Specifies if the value is allowed to be null. + /// + internal void SetValueUnsafe(int index, string name, ResultData? value, bool isNullable = true) + { + if (value is not null) + { + value.Parent = this; + } + + _buffer[index].Set(name, value, isNullable); + } + /// /// Removes a field value from the buffer. /// Note: Remove will not validate if the buffer has enough space. @@ -137,7 +163,7 @@ internal void Reset() { Unsafe.Add(ref searchSpace, i).Reset(); } - + _capacity = 0; } diff --git a/src/HotChocolate/Core/src/Execution/Processing/Selection.cs b/src/HotChocolate/Core/src/Execution/Processing/Selection.cs index 2618d12f4e5..ae9b373441b 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/Selection.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/Selection.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using HotChocolate.Execution.Properties; using HotChocolate.Language; @@ -96,6 +97,9 @@ protected Selection(Selection selection) /// public IObjectType DeclaringType { get; } + /// + public ISelectionSet DeclaringSelectionSet { get; private set; } = default!; + /// public IObjectField Field { get; } @@ -184,6 +188,9 @@ public bool IsIncluded(long includeFlags, bool allowInternals = false) return false; } + public override string ToString() + => SyntaxNode.ToString(); + internal void AddSelection(FieldNode selectionSyntax, long includeCondition = 0) { if ((_flags & Flags.Sealed) == Flags.Sealed) @@ -303,12 +310,17 @@ internal void MarkAsStream(long ifCondition) _flags |= Flags.Stream; } - internal void Seal() + internal void Seal(ISelectionSet declaringSelectionSet) { if ((_flags & Flags.Sealed) != Flags.Sealed) { + DeclaringSelectionSet = declaringSelectionSet; _flags |= Flags.Sealed; } + + Debug.Assert( + ReferenceEquals(declaringSelectionSet, DeclaringSelectionSet), + "Selections can only belong to a single selectionSet."); } private SelectionExecutionStrategy InferStrategy( diff --git a/src/HotChocolate/Core/src/Execution/Processing/SelectionSet.cs b/src/HotChocolate/Core/src/Execution/Processing/SelectionSet.cs index 2d8c7c0e0fe..774544cc2a8 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/SelectionSet.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/SelectionSet.cs @@ -72,7 +72,7 @@ internal void Seal() { for (var i = 0; i < _selections.Length; i++) { - _selections[i].Seal(); + _selections[i].Seal(this); } _flags |= Flags.Sealed; @@ -83,7 +83,7 @@ internal void Seal() /// Returns a reference to the 0th element of the underlying selections array. /// If the selections array is empty, returns a reference to the location where the 0th element /// would have been stored. Such a reference may or may not be null. - /// It can be used for pinning but must never be dereferenced. + /// It can be used for pinning but must never be de-referenced. /// This is only meant for use by the execution engine. /// internal ref Selection GetSelectionsReference() diff --git a/src/HotChocolate/Core/src/Types.Mutations/Errors/ErrorMiddleware.cs b/src/HotChocolate/Core/src/Types.Mutations/Errors/ErrorMiddleware.cs index e4fe2e74ea9..293d5286ad7 100644 --- a/src/HotChocolate/Core/src/Types.Mutations/Errors/ErrorMiddleware.cs +++ b/src/HotChocolate/Core/src/Types.Mutations/Errors/ErrorMiddleware.cs @@ -67,11 +67,7 @@ public async ValueTask InvokeAsync(IMiddlewareContext context) throw; } - context.SetScopedState(ErrorContextDataKeys.Errors, - new[] - { - error - }); + context.SetScopedState(ErrorContextDataKeys.Errors, new[] { error }); context.Result = ErrorObject; } } diff --git a/src/HotChocolate/Core/src/Types.Mutations/MutationConventionMiddleware.cs b/src/HotChocolate/Core/src/Types.Mutations/MutationConventionMiddleware.cs index bf13deabbe9..680303a1ab6 100644 --- a/src/HotChocolate/Core/src/Types.Mutations/MutationConventionMiddleware.cs +++ b/src/HotChocolate/Core/src/Types.Mutations/MutationConventionMiddleware.cs @@ -72,6 +72,6 @@ public async ValueTask InvokeAsync(IMiddlewareContext context) } } - internal static object Null { get; } = new object(); + internal static object Null { get; } = new(); } diff --git a/src/HotChocolate/Core/src/Types.OffsetPagination/OffsetPagingProvider.cs b/src/HotChocolate/Core/src/Types.OffsetPagination/OffsetPagingProvider.cs index cd8101cf557..d9a38c68ea8 100644 --- a/src/HotChocolate/Core/src/Types.OffsetPagination/OffsetPagingProvider.cs +++ b/src/HotChocolate/Core/src/Types.OffsetPagination/OffsetPagingProvider.cs @@ -3,20 +3,19 @@ namespace HotChocolate.Types.Pagination; /// -/// Represents an offset paging provider, which can be implemented to -/// create optimized pagination for data sources. -/// -/// The paging provider will be used by the configuration to choose +/// Represents an offset paging provider, which can be implemented to +/// create optimized pagination for data sources. +/// +/// The paging provider will be used by the configuration to choose /// the right paging handler for executing the field. /// -public abstract class OffsetPagingProvider - : IPagingProvider +public abstract class OffsetPagingProvider : IPagingProvider { /// - /// Specifies if this paging provider can handle the specified . + /// Specifies if this paging provider can handle the specified . /// /// - /// The source type represents the result of the field resolver and could be a collection, + /// The source type represents the result of the field resolver and could be a collection, /// a query builder or some other object representing the data set. /// public abstract bool CanHandle(IExtendedType source); @@ -30,7 +29,7 @@ public abstract class OffsetPagingProvider /// Creates the paging handler for offset pagination. /// /// - /// The source type represents the result of the field resolver and could be a collection, + /// The source type represents the result of the field resolver and could be a collection, /// a query builder or some other object representing the data set. /// /// diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/ISelection.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/ISelection.cs index 290baa2fe48..5a7031018ab 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/ISelection.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/ISelection.cs @@ -43,10 +43,15 @@ public interface ISelection : IOptionalSelection bool IsList { get; } /// - /// The type that declares the field that is selected by this selection. + /// Gets the type that declares the field that is selected by this selection. /// IObjectType DeclaringType { get; } + /// + /// Gets the selectionSet that declares this selection. + /// + ISelectionSet DeclaringSelectionSet { get; } + /// /// Gets the field selection syntax node. /// diff --git a/src/HotChocolate/Core/src/Types/Extensions/SchemaBuilderExtensions.Document.cs b/src/HotChocolate/Core/src/Types/Extensions/SchemaBuilderExtensions.Document.cs index 13d9758f5c9..34ef211c28e 100644 --- a/src/HotChocolate/Core/src/Types/Extensions/SchemaBuilderExtensions.Document.cs +++ b/src/HotChocolate/Core/src/Types/Extensions/SchemaBuilderExtensions.Document.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using HotChocolate.Language; using HotChocolate.Properties; @@ -9,7 +10,11 @@ public static partial class SchemaBuilderExtensions { public static ISchemaBuilder AddDocumentFromString( this ISchemaBuilder builder, +#if NET7_0_OR_GREATER + [StringSyntax("graphql")] string schema) +#else string schema) +#endif { if (builder is null) { diff --git a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorComparableTests.cs b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorComparableTests.cs index fd266ab5ac8..854e31ddc4f 100644 --- a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorComparableTests.cs +++ b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorComparableTests.cs @@ -389,7 +389,7 @@ public async Task Create_ShortIn_Expression() var res2 = await tester.ExecuteAsync( QueryRequestBuilder.New() - .SetQuery("{ root(where: { barShort: { in: [ null, 14 ]}}){ barShort}}") + .SetQuery("{ root(where: { barShort: { in: [ 13, 14 ]}}){ barShort}}") .Create()); var res3 = await tester.ExecuteAsync( @@ -419,7 +419,7 @@ public async Task Create_ShortNotIn_Expression() var res2 = await tester.ExecuteAsync( QueryRequestBuilder.New() - .SetQuery("{ root(where: { barShort: { nin: [ null, 14 ]}}){ barShort}}") + .SetQuery("{ root(where: { barShort: { nin: [ 13, 14 ]}}){ barShort}}") .Create()); var res3 = await tester.ExecuteAsync( diff --git a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorIdTests.cs b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorIdTests.cs index 1011a8b0a9f..ca998bf3399 100644 --- a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorIdTests.cs +++ b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorIdTests.cs @@ -557,7 +557,8 @@ public async Task Create_ShortNotIn_Expression() var res2 = await tester.ExecuteAsync( QueryRequestBuilder.New() .SetQuery( - "{ root(where: { barShort: { nin: [ null, \"Rm9vCnMxNA==\"]}}){ barShort}}") + "{ root(where: { barShort: { nin: " + + "[ \"Rm9vCnMxMg==\", \"Rm9vCnMxNA==\"]}}){ barShort}}") .Create()); var res3 = await tester.ExecuteAsync( diff --git a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorObjectTests.cs b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorObjectTests.cs index 85615b79902..abd9a02ead8 100644 --- a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorObjectTests.cs +++ b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorObjectTests.cs @@ -200,7 +200,7 @@ public async Task Create_ObjectShortIn_Expression() var res2 = await tester.ExecuteAsync( QueryRequestBuilder.New() .SetQuery( - "{ root(where: { foo: { barShort: { in: [ null, 14 ]}}}) " + + "{ root(where: { foo: { barShort: { in: [ 13, 14 ]}}}) " + "{ foo{ barShort}}}") .Create()); diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/QueryableFilterVisitorComparableTests.cs b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/QueryableFilterVisitorComparableTests.cs index 71cb27488ec..bb0e4e021b2 100644 --- a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/QueryableFilterVisitorComparableTests.cs +++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/QueryableFilterVisitorComparableTests.cs @@ -397,7 +397,7 @@ public async Task Create_ShortIn_Expression() var res2 = await tester.ExecuteAsync( QueryRequestBuilder.New() - .SetQuery("{ root(where: { barShort: { in: [ null, 14 ]}}){ barShort}}") + .SetQuery("{ root(where: { barShort: { in: [ 13, 14 ]}}){ barShort}}") .Create()); var res3 = await tester.ExecuteAsync( @@ -428,7 +428,7 @@ public async Task Create_ShortNotIn_Expression() var res2 = await tester.ExecuteAsync( QueryRequestBuilder.New() - .SetQuery("{ root(where: { barShort: { nin: [ null, 14 ]}}){ barShort}}") + .SetQuery("{ root(where: { barShort: { nin: [ 13, 14 ]}}){ barShort}}") .Create()); var res3 = await tester.ExecuteAsync( diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/QueryableFilterVisitorObjectTests.cs b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/QueryableFilterVisitorObjectTests.cs index 160f879b41f..fab8e3125c7 100644 --- a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/QueryableFilterVisitorObjectTests.cs +++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/QueryableFilterVisitorObjectTests.cs @@ -163,7 +163,7 @@ public async Task Create_ObjectShortIn_Expression() var res2 = await tester.ExecuteAsync( QueryRequestBuilder.New() .SetQuery( - "{ root(where: { foo: { barShort: { in: [ null, 14 ]}}}) " + + "{ root(where: { foo: { barShort: { in: [ 13, 14 ]}}}) " + "{ foo{ barShort}}}") .Create()); diff --git a/src/HotChocolate/Data/test/Data.Filters.Tests/ExtensionsTest.cs b/src/HotChocolate/Data/test/Data.Filters.Tests/ExtensionsTest.cs index 7f814032383..32a79d545e9 100644 --- a/src/HotChocolate/Data/test/Data.Filters.Tests/ExtensionsTest.cs +++ b/src/HotChocolate/Data/test/Data.Filters.Tests/ExtensionsTest.cs @@ -1,8 +1,8 @@ using System; using System.Linq; +using CookieCrumble; using HotChocolate.Data.Filters; using HotChocolate.Types; -using CookieCrumble; namespace HotChocolate.Data.Tests; @@ -41,13 +41,13 @@ public void Convention_DefaultScope_Extensions() public void ObjectField_UseFiltering() { // arrange - // act var builder = SchemaBuilder.New() .AddFiltering() .AddQueryType( c => c.Field(x => x.GetFoos()).UseFiltering()); + // act var schema = builder.Create(); // assert diff --git a/src/HotChocolate/Fusion/src/Core/Execution/Argument.cs b/src/HotChocolate/Fusion/src/Core/Execution/Argument.cs new file mode 100644 index 00000000000..3277edc5269 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/Argument.cs @@ -0,0 +1,16 @@ +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Execution; + +internal readonly struct Argument +{ + public Argument(string name, IValueNode value) + { + Name = name; + Value = value; + } + + public string Name { get; } + + public IValueNode Value { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/ArgumentContext.cs b/src/HotChocolate/Fusion/src/Core/Execution/ArgumentContext.cs new file mode 100644 index 00000000000..0cf3556e01f --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/ArgumentContext.cs @@ -0,0 +1,65 @@ +using HotChocolate.Utilities; + +namespace HotChocolate.Fusion.Execution; + +internal readonly struct ArgumentContext +{ + private readonly List _items; + + private ArgumentContext(List items) + { + _items = items; + } + + public ArgumentContext Push(IReadOnlyList arguments) + { + var items = new List(); + + foreach (var argument in arguments) + { + items.Add(new Item(0, argument)); + } + + if (_items is not null) + { + foreach (var currentItem in _items) + { + if (currentItem.Level > 0) + { + continue; + } + + var add = true; + + foreach (var newItem in items) + { + if (currentItem.Argument.Name.EqualsOrdinal(newItem.Argument.Name)) + { + add = false; + break; + } + } + + if (add) + { + items.Add(new Item(currentItem.Level + 1, currentItem.Argument)); + } + } + } + + return new ArgumentContext(items); + } + + private readonly struct Item + { + public Item(int level, Argument argument) + { + Level = level; + Argument = argument; + } + + public int Level { get; } + + public Argument Argument { get; } + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/ExecutionState.cs b/src/HotChocolate/Fusion/src/Core/Execution/ExecutionState.cs new file mode 100644 index 00000000000..9f4d212d928 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/ExecutionState.cs @@ -0,0 +1,128 @@ +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Text.Json; +using HotChocolate.Fusion.Utilities; +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Execution; + +internal sealed class ExecutionState : IExecutionState +{ + private static readonly ListValueNode _emptyList = new(Array.Empty()); + private readonly ConcurrentDictionary _store = new(); + + public void TryGetState(string key, ITypeNode expectedType, out IValueNode value) + => throw new NotImplementedException(); + + public IValueNode GetState(string key, ITypeNode expectedType) + { + if (_store.TryGetValue(key, out var state)) + { + var stateValue = state.Value; + + if (expectedType.Equals(state.Type, SyntaxComparison.Syntax)) + { + if (stateValue is IValueNode value) + { + return value; + } + + if(stateValue is ImmutableList list) + { + return list[0]; + } + + throw new InvalidOperationException("Unexpected State Value"); + } + + if (expectedType.IsListType() && + expectedType.InnerType().Equals(state.Type,SyntaxComparison.Syntax)) + { + if (stateValue is IValueNode value) + { + return new ListValueNode(value); + } + + if(stateValue is ImmutableList list) + { + return new ListValueNode(list); + } + + throw new InvalidOperationException("Unexpected State Value"); + } + } + + throw new ArgumentException("State Not Found"); + } + + public void AddState(string key, JsonElement value, ITypeNode type) + { + var literal = JsonValueToGraphQLValueConverter.Convert(value); + + if (_store.ContainsKey(key)) + { + _store.AddOrUpdate( + key, + static (_, _) => throw new InvalidOperationException("State is never removed"), + static (_, s, newValue) => + { + if (s.Value is IValueNode currentValue) + { + var builder = ImmutableList.CreateBuilder(); + builder.Add(currentValue); + builder.Add(newValue); + s.Value = builder.ToImmutable(); + } + else if (s.Value is ImmutableList list) + { + s.Value = list.Add(newValue); + } + else + { + throw new InvalidOperationException("Unexpected State Value"); + } + return s; + }, + literal); + } + else + { + var state = new State(type) { Value = literal }; + _store.AddOrUpdate( + key, + static (_, s) => s, + static (_, s, newState) => + { + if (s.Value is IValueNode currentValue) + { + var builder = ImmutableList.CreateBuilder(); + builder.Add(currentValue); + builder.Add((IValueNode)newState.Value!); + s.Value = builder.ToImmutable(); + } + else if (s.Value is ImmutableList list) + { + s.Value = list.Add((IValueNode)newState.Value!); + } + else + { + throw new InvalidOperationException("Unexpected State Value"); + } + return s; + }, + state); + } + } + + private sealed class State + { + public State(ITypeNode type) + { + Type = type; + } + + public ITypeNode Type { get; } + + public object? Value { get; set; } + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/HttpRequestExecutor.cs b/src/HotChocolate/Fusion/src/Core/Execution/HttpRequestExecutor.cs new file mode 100644 index 00000000000..57cb6ba2785 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/HttpRequestExecutor.cs @@ -0,0 +1,90 @@ +using System.Diagnostics; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using HotChocolate.Utilities; + +namespace HotChocolate.Fusion.Execution; + +public sealed class HttpRequestExecutor : IRemoteRequestExecutor +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly JsonRequestFormatter _formatter = new(); + + public HttpRequestExecutor(string schemaName, IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + SchemaName = schemaName; + } + + public string SchemaName { get; } + + public async Task ExecuteAsync(Request request, CancellationToken cancellationToken) + { + // todo : this is just a naive dummy implementation + using var writer = new ArrayWriter(); + using var client = _httpClientFactory.CreateClient(SchemaName); + using var requestMessage = CreateRequestMessage(writer, request); + using var responseMessage = await client.SendAsync(requestMessage, cancellationToken); + var s = await responseMessage.Content.ReadAsStringAsync(cancellationToken); + responseMessage.EnsureSuccessStatusCode(); // TODO : remove for production + + await using var contentStream = await responseMessage.Content.ReadAsStreamAsync(cancellationToken); + var stream = contentStream; + + var sourceEncoding = GetEncoding(responseMessage.Content.Headers.ContentType?.CharSet); + + if (sourceEncoding is not null && + !Equals(sourceEncoding.EncodingName, Encoding.UTF8.EncodingName)) + { + stream = GetTranscodingStream(contentStream, sourceEncoding); + } + + var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); + return new Response(document); + } + + private HttpRequestMessage CreateRequestMessage(ArrayWriter writer, Request request) + { + _formatter.Write(writer, request); + + var requestMessage = new HttpRequestMessage(HttpMethod.Post, default(Uri)); + requestMessage.Content = new ByteArrayContent(writer.GetInternalBuffer(), 0, writer.Length); + requestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + return requestMessage; + } + + private static Encoding? GetEncoding(string? charset) + { + Encoding? encoding = null; + + if (charset != null) + { + try + { + // Remove at most a single set of quotes. + if (charset.Length > 2 && charset[0] == '\"' && charset[^1] == '\"') + { + encoding = Encoding.GetEncoding(charset.Substring(1, charset.Length - 2)); + } + else + { + encoding = Encoding.GetEncoding(charset); + } + } + catch (ArgumentException e) + { + throw new InvalidOperationException("Invalid Charset", e); + } + + Debug.Assert(encoding != null); + } + + return encoding; + } + + private static Stream GetTranscodingStream(Stream contentStream, Encoding sourceEncoding) + { + return Encoding.CreateTranscodingStream(contentStream, innerStreamEncoding: sourceEncoding, outerStreamEncoding: Encoding.UTF8); + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/IExecutionState.cs b/src/HotChocolate/Fusion/src/Core/Execution/IExecutionState.cs new file mode 100644 index 00000000000..915cca38524 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/IExecutionState.cs @@ -0,0 +1,11 @@ +using System.Text.Json; +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Execution; + +internal interface IExecutionState +{ + IValueNode GetState(string key, ITypeNode expectedType); + + void AddState(string key, JsonElement value, ITypeNode type); +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/IRemoteRequestExecutor.cs b/src/HotChocolate/Fusion/src/Core/Execution/IRemoteRequestExecutor.cs new file mode 100644 index 00000000000..cf151405e65 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/IRemoteRequestExecutor.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.Fusion.Execution; + +public interface IRemoteRequestExecutor +{ + string SchemaName { get; } + + Task ExecuteAsync(Request request, CancellationToken cancellationToken); +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/JsonRequestFormatter.cs b/src/HotChocolate/Fusion/src/Core/Execution/JsonRequestFormatter.cs new file mode 100644 index 00000000000..9e51ebf0a64 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/JsonRequestFormatter.cs @@ -0,0 +1,102 @@ +using System.Buffers; +using System.Text.Encodings.Web; +using System.Text.Json; +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Execution; + +internal sealed class JsonRequestFormatter +{ + private readonly JsonWriterOptions _options = new() + { + Indented = true, + Encoder = JavaScriptEncoder.Default + }; + + public void Write(IBufferWriter bufferWriter, Request request) + { + using var jsonWriter = new Utf8JsonWriter(bufferWriter, _options); + jsonWriter.WriteStartObject(); + + WriteQuery(jsonWriter, request.Document); + + if (request.VariableValues is not null) + { + WriteVariableValues(jsonWriter, request.VariableValues); + } + + if (request.Extensions is not null) + { + WriteExtensions(jsonWriter, request.Extensions); + } + + jsonWriter.WriteEndObject(); + } + + private void WriteQuery(Utf8JsonWriter jsonWriter, DocumentNode documentNode) + => jsonWriter.WriteString("query", documentNode.ToString()); + + private void WriteVariableValues(Utf8JsonWriter jsonWriter, ObjectValueNode variableValues) + { + jsonWriter.WritePropertyName("variables"); + WriteValue(jsonWriter, variableValues); + } + + private void WriteExtensions(Utf8JsonWriter jsonWriter, ObjectValueNode extensions) + { + jsonWriter.WritePropertyName("extensions"); + WriteValue(jsonWriter, extensions); + } + + private void WriteValue(Utf8JsonWriter jsonWriter, IValueNode value) + { + switch (value) + { + case ObjectValueNode objectValueNode: + jsonWriter.WriteStartObject(); + foreach (var field in objectValueNode.Fields) + { + jsonWriter.WritePropertyName(field.Name.Value); + WriteValue(jsonWriter, field.Value); + } + jsonWriter.WriteEndObject(); + break; + + case ListValueNode listValueNode: + jsonWriter.WriteStartArray(); + foreach (var item in listValueNode.Items) + { + WriteValue(jsonWriter, item); + } + jsonWriter.WriteEndArray(); + break; + + case StringValueNode stringValueNode: + jsonWriter.WriteStringValue(stringValueNode.Value); + break; + + case FloatValueNode floatValueNode: + jsonWriter.WriteRawValue(floatValueNode.AsSpan()); + break; + + case IntValueNode intValueNode: + jsonWriter.WriteRawValue(intValueNode.AsSpan()); + break; + + case BooleanValueNode booleanValueNode: + jsonWriter.WriteBooleanValue(booleanValueNode.Value); + break; + + case EnumValueNode enumValueNode: + jsonWriter.WriteStringValue(enumValueNode.Value); + break; + + case NullValueNode: + jsonWriter.WriteNullValue(); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(value)); + } + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/JsonResult.cs b/src/HotChocolate/Fusion/src/Core/Execution/JsonResult.cs new file mode 100644 index 00000000000..55eb42eae5b --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/JsonResult.cs @@ -0,0 +1,16 @@ +using System.Text.Json; + +namespace HotChocolate.Fusion.Execution; + +internal readonly struct JsonResult +{ + public JsonResult(string schemaName, JsonElement element) + { + SchemaName = schemaName; + Element = element; + } + + public string SchemaName { get; } + + public JsonElement Element { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/RemoteExecutorContext.cs b/src/HotChocolate/Fusion/src/Core/Execution/RemoteExecutorContext.cs new file mode 100644 index 00000000000..7478bca6b52 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/RemoteExecutorContext.cs @@ -0,0 +1,35 @@ +using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Planning; + +namespace HotChocolate.Fusion.Execution; + +internal sealed class RemoteExecutorContext +{ + public RemoteExecutorContext( + ISchema schema, + ResultBuilder result, + IOperation operation, + QueryPlan plan, + IReadOnlySet requiresFetch) + { + Schema = schema; + Result = result; + Operation = operation; + Plan = plan; + RequiresFetch = requiresFetch; + } + + public ISchema Schema { get; } + + public ResultBuilder Result { get; } + + public IOperation Operation { get; } + + public QueryPlan Plan { get; } + + public IReadOnlySet RequiresFetch { get; } + + public List Fetch { get; } = new(); + + public Queue Compose { get; } = new(); +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/RemoteQueryExecutor.txt b/src/HotChocolate/Fusion/src/Core/Execution/RemoteQueryExecutor.txt new file mode 100644 index 00000000000..d2179f14451 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/RemoteQueryExecutor.txt @@ -0,0 +1,142 @@ +using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Planning; + +namespace HotChocolate.Fusion.Execution; + +internal sealed class RemoteQueryExecutor +{ + private readonly object _sync = new(); + private readonly RemoteRequestExecutorFactory _executorFactory; + private readonly List _completed = new(); + private readonly List _responses = new(); + private int _completedCount; + + public RemoteQueryExecutor(RemoteRequestExecutorFactory executorFactory) + { + _executorFactory = executorFactory; + } + + public async Task ExecuteAsync(/*OperationContext context,*/ QueryPlan plan, IExecutionState state) + { + using var semaphore = new SemaphoreSlim(0); + var backlog = new HashSet(plan.ExecutionNodes); + // var ct = context.RequestAborted; + var ct = new CancellationToken(); + + // var data = context.Result.RentObject(context.Operation.RootSelectionSet.Selections.Count); + + // enqueue root tasks + foreach (var root in plan.RootExecutionNodes) + { + if (root is RequestNode requestNode) + { + BeginExecuteNode(requestNode, state, semaphore, ct); + backlog.Remove(requestNode); + } + } + + await semaphore.WaitAsync(ct); + + if (backlog.Count > 0) + { + // process dependant tasks + var backlogCopy = new List(); + var completedCopy = new HashSet(); + var completedIndex = 0; + + do + { + // we use snapshots of backlog and completed to work without lock and + // be able to modify the collections. + backlogCopy.Clear(); + backlogCopy.AddRange(backlog); + + lock (_sync) + { + if (completedIndex < _completed.Count) + { + for (var i = completedIndex; i < _completed.Count; i++) + { + completedCopy.Add(_completed[i]); + } + + completedIndex = completedCopy.Count; + } + } + + foreach (var executionNode in backlogCopy) + { + if (DependenciesFulfilled(executionNode.DependsOn, completedCopy) && + executionNode is RequestNode requestNode) + { + BeginExecuteNode(requestNode, state, semaphore, ct); + backlog.Remove(requestNode); + } + } + + await semaphore.WaitAsync(ct); + } while (backlog.Count > 0); + } + + // wait for tasks to complete. + while (_completedCount < plan.ExecutionNodes.Count) + { + await semaphore.WaitAsync(ct); + } + + static bool DependenciesFulfilled( + IReadOnlyList dependsOn, + HashSet completed) + { + foreach (var dependency in dependsOn) + { + if (!completed.Contains(dependency)) + { + return false; + } + } + + return true; + } + } + +#pragma warning disable CS4014 + private void BeginExecuteNode( + RequestNode requestNode, + IExecutionState state, + SemaphoreSlim semaphore, + CancellationToken ct) + => ExecuteNode(requestNode, state, semaphore, ct); +#pragma warning restore CS4014 + + private async Task ExecuteNode( + RequestNode requestNode, + IExecutionState state, + SemaphoreSlim semaphore, + CancellationToken ct) + { + try + { + var request = requestNode.Handler.CreateRequest(state); + var executor = _executorFactory.Create(request.SchemaName); + var response = await executor.ExecuteAsync(request, ct); + + BuildResult(response, null); + + lock (_sync) + { + _completed.Add(requestNode); + } + } + finally + { + Interlocked.Increment(ref _completedCount); + semaphore.Release(); + } + } + + private void BuildResult(Response response, object responseNode) + { + + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/RemoteQueryExecutor2.cs b/src/HotChocolate/Fusion/src/Core/Execution/RemoteQueryExecutor2.cs new file mode 100644 index 00000000000..bc77ff9061f --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/RemoteQueryExecutor2.cs @@ -0,0 +1,390 @@ +using System.Diagnostics; +using System.Text.Json; +using HotChocolate.Execution; +using HotChocolate.Execution.Processing; +using HotChocolate.Language; +using HotChocolate.Types; +using static HotChocolate.Fusion.Utilities.JsonValueToGraphQLValueConverter; +using ObjectType = HotChocolate.Fusion.Metadata.ObjectType; + +namespace HotChocolate.Fusion.Execution; + +internal sealed class RemoteQueryExecutor2 +{ + private readonly Metadata.ServiceConfiguration _serviceConfiguration; + private readonly RemoteRequestExecutorFactory _executorFactory; + + public RemoteQueryExecutor2(Metadata.ServiceConfiguration serviceConfiguration, RemoteRequestExecutorFactory executorFactory) + { + _serviceConfiguration = serviceConfiguration; + _executorFactory = executorFactory; + } + + public async Task ExecuteAsync( + RemoteExecutorContext context, + CancellationToken cancellationToken = default) + { + var rootSelectionSet = context.Operation.RootSelectionSet; + var rootResult = context.Result.RentObject(rootSelectionSet.Selections.Count); + var rootWorkItem = new WorkItem(rootSelectionSet, new ArgumentContext(), rootResult); + context.Fetch.Add(rootWorkItem); + + while (context.Fetch.Count > 0) + { + await FetchAsync(context, cancellationToken).ConfigureAwait(false); + + while (context.Compose.TryDequeue(out var current)) + { + ComposeResult(context, current); + } + } + + return QueryResultBuilder.New().SetData(rootResult).Create(); + } + + // note: this is inefficient and we want to group here, for now we just want to get it working. + private async Task FetchAsync(RemoteExecutorContext context, CancellationToken ct) + { + foreach (var workItem in context.Fetch) + { + // todo: this is not really efficient + var variableValues = new Dictionary(StringComparer.Ordinal); + var selectionResults = workItem.SelectionResults; + var partialResult = selectionResults[0]; + var selections = workItem.SelectionSet.Selections; + var exportKeys = context.Plan.GetExports(workItem.SelectionSet); + + // if there was a partial result stored on the selection set then we will first unwrap + // it before starting to fetch. + if (partialResult.HasValue) + { + // first we need to erase the partial result from the array so that its not + // combined into the result creation. + selectionResults[0] = default; + + // next we will unwrap the results. + ExtractSelectionResults(partialResult, selections, selectionResults); + + // last we will check if there are any exports for this selection-set. + ExtractVariables(partialResult, exportKeys, variableValues); + } + + foreach (var requestNode in context.Plan.GetRequestNodes(workItem.SelectionSet)) + { + var executor = _executorFactory.Create(requestNode.Handler.SchemaName); + var request = requestNode.Handler.CreateRequest(variableValues); + var result = await executor.ExecuteAsync(request, ct).ConfigureAwait(false); + var data = requestNode.Handler.UnwrapResult(result); + + ExtractSelectionResults(selections, request.SchemaName, data, selectionResults); + ExtractVariables(data, exportKeys, variableValues); + } + + context.Compose.Enqueue(workItem); + } + + context.Fetch.Clear(); + } + + private void ComposeResult( + RemoteExecutorContext context, + WorkItem workItem) + => ComposeResult( + context, + workItem.SelectionSet.Selections, + workItem.SelectionResults, + workItem.Result, + workItem.Variables); + + private void ComposeResult( + RemoteExecutorContext context, + IReadOnlyList selections, + IReadOnlyList selectionResults, + ObjectResult selectionSetResult, + ArgumentContext variables) + { + for (var i = 0; i < selections.Count; i++) + { + var selection = selections[i]; + var selectionResult = selectionResults[i]; + var nullable = selection.TypeKind is not TypeKind.NonNull; + var namedType = selection.Type.NamedType(); + + if (selection.Type.IsScalarType() || namedType.IsScalarType()) + { + selectionSetResult.SetValueUnsafe( + i, + selection.ResponseName, + selectionResult.Single.Element, + nullable); + } + else if (selection.Type.IsEnumType() || namedType.IsEnumType()) + { + // we might need to map the enum value! + selectionSetResult.SetValueUnsafe( + i, + selection.ResponseName, + selectionResult.Single.Element, + nullable); + } + else if (selection.Type.IsCompositeType()) + { + selectionSetResult.SetValueUnsafe( + i, + selection.ResponseName, + ComposeObject(context, selection, selectionResult, variables)); + } + else + { + selectionSetResult.SetValueUnsafe( + i, + selection.ResponseName, + ComposeList(context, selection, selectionResult, variables, selection.Type)); + } + } + } + + private ListResult? ComposeList( + RemoteExecutorContext context, + ISelection selection, + SelectionResult selectionResult, + ArgumentContext variables, + Types.IType type) + { + if (selectionResult.IsNull()) + { + return null; + } + + var json = selectionResult.Single.Element; + var schemaName = selectionResult.Single.SchemaName; + Debug.Assert(selectionResult.Multiple is null, "selectionResult.Multiple is null"); + Debug.Assert(json.ValueKind is JsonValueKind.Array, "json.ValueKind is JsonValueKind.Array"); + + var elementType = type.ElementType(); + var result = context.Result.RentList(json.GetArrayLength()); + + if (!elementType.IsListType()) + { + foreach (var item in json.EnumerateArray()) + { + result.AddUnsafe( + ComposeObject( + context, + selection, + new SelectionResult(new JsonResult(schemaName, item)), + variables)); + } + } + else + { + foreach (var item in json.EnumerateArray()) + { + result.AddUnsafe( + ComposeList( + context, + selection, + new SelectionResult(new JsonResult(schemaName, item)), + variables, + elementType)); + } + } + + return result; + } + + private ObjectResult? ComposeObject( + RemoteExecutorContext context, + ISelection selection, + SelectionResult selectionResult, + ArgumentContext variables) + { + if (selectionResult.IsNull()) + { + return null; + } + + ObjectType typeMetadata; + Types.ObjectType type; + + if (selection.Type.NamedType() is Types.ObjectType ot) + { + type = ot; + typeMetadata = _serviceConfiguration.GetType(ot.Name); + } + else + { + var typeInfo = selectionResult.GetTypeInfo(); + typeMetadata = _serviceConfiguration.GetType(typeInfo); + type = context.Schema.GetType(typeMetadata.Name); + } + + var selectionSet = context.Operation.GetSelectionSet(selection, type); + var result = context.Result.RentObject(selectionSet.Selections.Count); + + if (context.RequiresFetch.Contains(selectionSet)) + { + var fetchArguments = CreateArguments( + context, + selection, + selectionResult, + typeMetadata); + + context.Fetch.Add( + new WorkItem(fetchArguments, selectionSet, variables, result) + { + SelectionResults = { [0] = selectionResult } + }); + } + else + { + var selections = selectionSet.Selections; + var childSelectionResults = new SelectionResult[selections.Count]; + ExtractSelectionResults(selectionResult, selections, childSelectionResults); + + /* + var childVariables = CreateVariables( + context, + selection, + selectionResult, + typeMetadata, + variables); + */ + + ComposeResult( + context, + selectionSet.Selections, + childSelectionResults, + result, + variables); + } + + return result; + } + + private IReadOnlyList CreateArguments( + RemoteExecutorContext context, + ISelection selection, + SelectionResult selectionResult, + ObjectType typeMetadata) + { + return new List(); + } + + private ArgumentContext CreateVariables( + RemoteExecutorContext context, + ISelection selection, + SelectionResult selectionResult, + ObjectType typeMetadata, + ArgumentContext variables) + => throw new NotImplementedException(); + + private static void ExtractSelectionResults( + SelectionResult parent, + IReadOnlyList selections, + SelectionResult[] selectionResults) + { + if (parent.Multiple is null) + { + var schemaName = parent.Single.SchemaName; + var data = parent.Single.Element; + + for (var i = 0; i < selections.Count; i++) + { + if (data.TryGetProperty(selections[i].ResponseName, out var property)) + { + var current = selectionResults[i]; + + selectionResults[i] = current.HasValue + ? current.AddResult(new JsonResult(schemaName, property)) + : new SelectionResult(new JsonResult(schemaName, property)); + } + } + } + else + { + foreach (var result in parent.Multiple) + { + var schemaName = result.SchemaName; + var data = result.Element; + + for (var i = 0; i < selections.Count; i++) + { + if (data.TryGetProperty(selections[i].ResponseName, out var property)) + { + var current = selectionResults[i]; + + selectionResults[i] = current.HasValue + ? current.AddResult(new JsonResult(schemaName, property)) + : new SelectionResult(new JsonResult(schemaName, property)); + } + } + } + } + } + + private static void ExtractSelectionResults( + IReadOnlyList selections, + string schemaName, + JsonElement data, + SelectionResult[] selectionResults) + { + for (var i = 0; i < selections.Count; i++) + { + if (data.TryGetProperty(selections[i].ResponseName, out var property)) + { + var selectionResult = selectionResults[i]; + if (selectionResult.HasValue) + { + selectionResults[i] = selectionResult.AddResult(new(schemaName, property)); + } + else + { + selectionResults[i] = new SelectionResult(new JsonResult(schemaName, property)); + } + } + } + } + + private static void ExtractVariables( + SelectionResult parent, + IReadOnlyList exportKeys, + Dictionary variableValues) + { + if (exportKeys.Count > 0) + { + if (parent.Multiple is null) + { + ExtractVariables(parent.Single.Element, exportKeys, variableValues); + } + else + { + foreach (var result in parent.Multiple) + { + ExtractVariables(result.Element, exportKeys, variableValues); + } + } + } + } + + private static void ExtractVariables( + JsonElement parent, + IReadOnlyList exportKeys, + Dictionary variableValues) + { + if (exportKeys.Count > 0 && + parent.ValueKind is not JsonValueKind.Null and not JsonValueKind.Undefined) + { + for (var i = 0; i < exportKeys.Count; i++) + { + var key = exportKeys[i]; + + if (!variableValues.ContainsKey(key) && + parent.TryGetProperty(key, out var property)) + { + variableValues.TryAdd(key, Convert(property)); + } + } + } + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/RemoteRequestExecutorFactory.cs b/src/HotChocolate/Fusion/src/Core/Execution/RemoteRequestExecutorFactory.cs new file mode 100644 index 00000000000..395084171ef --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/RemoteRequestExecutorFactory.cs @@ -0,0 +1,14 @@ +namespace HotChocolate.Fusion.Execution; + +internal sealed class RemoteRequestExecutorFactory +{ + private readonly Dictionary _executors; + + public RemoteRequestExecutorFactory(IEnumerable executors) + { + _executors = executors.ToDictionary(t => t.SchemaName); + } + + public IRemoteRequestExecutor Create(string schemaName) + => _executors[schemaName]; +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/Request.cs b/src/HotChocolate/Fusion/src/Core/Execution/Request.cs new file mode 100644 index 00000000000..71569a48670 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/Request.cs @@ -0,0 +1,26 @@ +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Execution; + +public readonly struct Request +{ + public Request( + string schemaName, + DocumentNode document, + ObjectValueNode? variableValues, + ObjectValueNode? extensions) + { + SchemaName = schemaName; + Document = document; + VariableValues = variableValues; + Extensions = extensions; + } + + public string SchemaName { get; } + + public DocumentNode Document { get; } + + public ObjectValueNode? VariableValues { get; } + + public ObjectValueNode? Extensions { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/RequestHandler.cs b/src/HotChocolate/Fusion/src/Core/Execution/RequestHandler.cs new file mode 100644 index 00000000000..44baff946b8 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/RequestHandler.cs @@ -0,0 +1,118 @@ +using System.Text.Json; +using HotChocolate.Execution.Processing; +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Execution; + +internal sealed class RequestHandler +{ + private readonly IReadOnlyList _path; + + internal RequestHandler( + string schemaName, + DocumentNode document, + ISelectionSet selectionSet, + IReadOnlyList requires, + IReadOnlyList path) + { + SchemaName = schemaName; + Document = document; + SelectionSet = selectionSet; + Requires = requires; + _path = path; + } + + /// + /// Gets the schema name on which this request handler executes. + /// + public string SchemaName { get; } + + /// + /// Gets the GraphQL request document. + /// + public DocumentNode Document { get; } + + /// + /// Gets the selection set for which this request provides a patch. + /// + public ISelectionSet SelectionSet { get; } + + /// + /// Gets the variables that this request handler requires to create a request. + /// + public IReadOnlyList Requires { get; } + + public Request CreateRequest(IReadOnlyDictionary variableValues) + { + ObjectValueNode? vars = null; + + if (Requires.Count > 0) + { + var fields = new List(); + + foreach (var required in Requires) + { + if (variableValues.TryGetValue(required.VariableName, out var value)) + { + fields.Add(new ObjectFieldNode(required.VariableName, value)); + } + else if (!required.IsOptional) + { + // TODO : error helper + throw new ArgumentException( + $"The variable value `{required.VariableName}` was not provided " + + "but is required.", + nameof(variableValues)); + } + } + + vars ??= new ObjectValueNode(fields); + } + + return new Request(SchemaName, Document, vars, null); + } + + public JsonElement UnwrapResult(Response response) + { + if (_path.Count == 0) + { + return response.Data; + } + + if (response.Data.ValueKind is not JsonValueKind.Undefined and not JsonValueKind.Null) + { + var current = response.Data; + + for (var i = 0; i < _path.Count; i++) + { + if (current.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null) + { + return current; + } + + current.TryGetProperty(_path[i], out var propertyValue); + current = propertyValue; + } + + return current; + } + + return response.Data; + } +} + +internal readonly struct RequiredState +{ + public RequiredState(string variableName, ITypeNode type, bool isOptional) + { + VariableName = variableName; + Type = type; + IsOptional = isOptional; + } + + public string VariableName { get; } + + public ITypeNode Type { get; } + + public bool IsOptional { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/Response.cs b/src/HotChocolate/Fusion/src/Core/Execution/Response.cs new file mode 100644 index 00000000000..ebbe60cac09 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/Response.cs @@ -0,0 +1,25 @@ +using System.Text.Json; + +namespace HotChocolate.Fusion.Execution; + +public sealed class Response : IDisposable +{ + private readonly JsonDocument? _document; + + public Response(JsonDocument? document) + { + _document = document; + + if (_document is not null && _document.RootElement.TryGetProperty("data", out var data)) + { + Data = data; + } + } + + public JsonElement Data { get; } + + public void Dispose() + { + _document?.Dispose(); + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/SelectionResult.cs b/src/HotChocolate/Fusion/src/Core/Execution/SelectionResult.cs new file mode 100644 index 00000000000..7fa15d2b848 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/SelectionResult.cs @@ -0,0 +1,73 @@ +using System.Text.Json; +using HotChocolate.Fusion.Metadata; + +namespace HotChocolate.Fusion.Execution; + +internal readonly struct SelectionResult +{ + public SelectionResult(JsonResult single) + { + Single = single; + Multiple = null; + HasValue = true; + } + + private SelectionResult(IReadOnlyList multiple) + { + Single = default; + Multiple = multiple; + HasValue = true; + } + + public bool HasValue { get; } + + public JsonResult Single { get; } + + public IReadOnlyList? Multiple { get; } + + public TypeInfo GetTypeInfo() + { + var result = Multiple is null ? Single : Multiple[0]; + + return new TypeInfo( + result.SchemaName, + result.Element.GetProperty("__typename").GetString()!); + } + + public bool IsNull() + { + if (Multiple is null) + { + return Single.Element.ValueKind is JsonValueKind.Null; + } + + for (var i = 0; i < Multiple.Count; i++) + { + if (Multiple[i].Element.ValueKind is not JsonValueKind.Null) + { + return false; + } + } + + return true; + + } + + public SelectionResult AddResult(JsonResult result) + { + if (Multiple is null) + { + return new SelectionResult(new[] { Single, result }); + } + + var array = new JsonResult[Multiple.Count + 1]; + + for (var i = 0; i < Multiple.Count; i++) + { + array[i] = Multiple[i]; + } + + array[Multiple.Count] = result; + return new SelectionResult(array); + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/WorkItem.cs b/src/HotChocolate/Fusion/src/Core/Execution/WorkItem.cs new file mode 100644 index 00000000000..3451b9bcf35 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/WorkItem.cs @@ -0,0 +1,36 @@ +using HotChocolate.Execution.Processing; + +namespace HotChocolate.Fusion.Execution; + +internal struct WorkItem +{ + public WorkItem( + ISelectionSet selectionSet, + ArgumentContext variables, + ObjectResult result) + : this(Array.Empty(), selectionSet, variables, result) { } + + public WorkItem( + IReadOnlyList arguments, + ISelectionSet selectionSet, + ArgumentContext variables, + ObjectResult result) + { + Arguments = arguments; + SelectionSet = selectionSet; + SelectionResults = Array.Empty(); + Variables = variables; + Result = result; + SelectionResults = new SelectionResult[selectionSet.Selections.Count]; + } + + public IReadOnlyList Arguments { get; } + + public ISelectionSet SelectionSet { get; } + + public SelectionResult[] SelectionResults { get; } + + public ArgumentContext Variables { get; set; } + + public ObjectResult Result { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/HotChocolate.Fusion.csproj b/src/HotChocolate/Fusion/src/Core/HotChocolate.Fusion.csproj index ec1fbff2edc..0db03610dd6 100644 --- a/src/HotChocolate/Fusion/src/Core/HotChocolate.Fusion.csproj +++ b/src/HotChocolate/Fusion/src/Core/HotChocolate.Fusion.csproj @@ -1,10 +1,19 @@ - - - - HotChocolate.Fusion - HotChocolate.Fusion - enable - enable - - - + + + + HotChocolate.Fusion + HotChocolate.Fusion + enable + enable + + + + + + + + + + + + diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ArgumentVariableDefinition.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ArgumentVariableDefinition.cs new file mode 100644 index 00000000000..69755b626aa --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ArgumentVariableDefinition.cs @@ -0,0 +1,19 @@ +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Metadata; + +internal sealed class ArgumentVariableDefinition : IVariableDefinition +{ + public ArgumentVariableDefinition(string name, ITypeNode type, string argumentName) + { + Name = name; + Type = type; + ArgumentName = argumentName; + } + + public string Name { get; } + + public ITypeNode Type { get; } + + public string ArgumentName { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ArgumentVariableDefinitionCollection.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ArgumentVariableDefinitionCollection.cs new file mode 100644 index 00000000000..4d0881859e1 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ArgumentVariableDefinitionCollection.cs @@ -0,0 +1,33 @@ +using System.Collections; +using System.Diagnostics.CodeAnalysis; + +namespace HotChocolate.Fusion.Metadata; + +internal sealed class ArgumentVariableDefinitionCollection : IEnumerable +{ + private readonly Dictionary _variableDefinitions; + + public ArgumentVariableDefinitionCollection( + IEnumerable variableDefinitions) + { + _variableDefinitions = variableDefinitions.ToDictionary(t => t.Name); + } + + public int Count => _variableDefinitions.Count; + + public ArgumentVariableDefinition this[string variableName] + => _variableDefinitions[variableName]; + + public bool TryGetValue( + string variableName, + [NotNullWhen(true)] out ArgumentVariableDefinition? value) + => _variableDefinitions.TryGetValue(variableName, out value); + + public bool ContainsVariable(string variableName) + => _variableDefinitions.ContainsKey(variableName); + + public IEnumerator GetEnumerator() + => _variableDefinitions.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/FetchDefinition.cs b/src/HotChocolate/Fusion/src/Core/Metadata/FetchDefinition.cs new file mode 100644 index 00000000000..bb1b0b87e9b --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/FetchDefinition.cs @@ -0,0 +1,159 @@ +using System.ComponentModel.Design; +using HotChocolate.Language; +using HotChocolate.Language.Visitors; + +namespace HotChocolate.Fusion.Metadata; + +internal sealed class FetchDefinition +{ + private static readonly FetchRewriter _rewriter = new(); + + public FetchDefinition( + string schemaName, + ISelectionNode select, + FragmentSpreadNode? placeholder, + IReadOnlyList requires) + { + SchemaName = schemaName; + Select = select; + Placeholder = placeholder; + Requires = requires; + } + + /// + /// Gets the schema to which the type system member is bound to. + /// + public string SchemaName { get; } + + public ISelectionNode Select { get; } + + public FragmentSpreadNode? Placeholder { get; } + + public IReadOnlyList Requires { get; } + + public (ISelectionNode selectionNode, IReadOnlyList Path) CreateSelection( + IReadOnlyDictionary variables, + SelectionSetNode? selectionSet) + { + var context = new FetchRewriterContext(Placeholder, variables, selectionSet); + var selection = _rewriter.Rewrite(Select, context); + + if (Placeholder is null && selectionSet is not null) + { + if (selection is not FieldNode fieldNode) + { + throw new InvalidOperationException( + "Either provide a placeholder or the select expression must be a FieldNode."); + } + + return (fieldNode.WithSelectionSet(selectionSet), new[] { fieldNode.Name.Value }); + } + + return ((ISelectionNode)selection!, context.SelectionPath); + } + + private class FetchRewriter : SyntaxRewriter + { + protected override SelectionSetNode? RewriteSelectionSet( + SelectionSetNode node, + FetchRewriterContext context) + { + var rewritten = base.RewriteSelectionSet(node, context); + + if (rewritten is not null && context.SelectionSet is not null) + { + List? rewrittenList = null; + for (var i = 0; i < rewritten.Selections.Count; i++) + { + var selectionNode = rewritten.Selections[i]; + + if (rewrittenList is null) + { + if (!selectionNode.Equals(context.Placeholder, SyntaxComparison.Syntax)) + { + continue; + } + + // preserve selection path, so we are later able to unwrap the result. + context.SelectionPath = context.Path.ToArray(); + rewrittenList = new List(); + + for (var j = 0; j < i; j++) + { + rewrittenList.Add(rewritten.Selections[j]); + } + } + + foreach (var selection in context.SelectionSet.Selections) + { + rewrittenList.Add(selection); + } + } + + return rewrittenList is null + ? rewritten + : rewritten.WithSelections(rewrittenList); + } + + return rewritten; + } + + protected override ISyntaxNode? OnRewrite(ISyntaxNode node, FetchRewriterContext context) + { + if (node is VariableNode variableNode && + context.Variables.TryGetValue(variableNode.Name.Value, out var valueNode)) + { + return valueNode; + } + + return base.OnRewrite(node, context); + } + + protected override FetchRewriterContext OnEnter( + ISyntaxNode node, + FetchRewriterContext context) + { + if (node is FieldNode field) + { + context.Path.Push(field.Name.Value); + } + + return base.OnEnter(node, context); + } + + protected override void OnLeave( + ISyntaxNode? node, + FetchRewriterContext context) + { + if (node is FieldNode) + { + context.Path.Pop(); + } + + base.OnLeave(node, context); + } + } + + private sealed class FetchRewriterContext : ISyntaxVisitorContext + { + public FetchRewriterContext( + FragmentSpreadNode? placeholder, + IReadOnlyDictionary variables, + SelectionSetNode? selectionSet) + { + Placeholder = placeholder; + Variables = variables; + SelectionSet = selectionSet; + } + + public Stack Path { get; } = new(); + + public FragmentSpreadNode? Placeholder { get; } + + public IReadOnlyDictionary Variables { get; } + + public SelectionSetNode? SelectionSet { get; } + + public IReadOnlyList SelectionPath { get; set; } = Array.Empty(); + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/FetchDefinitionCollection.cs b/src/HotChocolate/Fusion/src/Core/Metadata/FetchDefinitionCollection.cs new file mode 100644 index 00000000000..b78d233be83 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/FetchDefinitionCollection.cs @@ -0,0 +1,41 @@ +using System.Collections; +using System.Diagnostics.CodeAnalysis; + +namespace HotChocolate.Fusion.Metadata; + +internal sealed class FetchDefinitionCollection : IEnumerable +{ + private readonly Dictionary _fetchDefinitions; + + public FetchDefinitionCollection(IEnumerable fetchDefinitions) + { + _fetchDefinitions = fetchDefinitions + .GroupBy(t => t.SchemaName) + .ToDictionary(t => t.Key, t => t.ToArray(), StringComparer.Ordinal); + } + + public int Count => _fetchDefinitions.Count; + + // public IReadOnlyList this[string schemaName] => throw new NotImplementedException(); + + public bool TryGetValue( + string schemaName, + [NotNullWhen(true)] out IReadOnlyList? values) + { + if (_fetchDefinitions.TryGetValue(schemaName, out var temp)) + { + values = temp; + return true; + } + + values = null; + return false; + } + + public bool ContainsResolvers(string schemaName) => _fetchDefinitions.ContainsKey(schemaName); + + public IEnumerator GetEnumerator() + => _fetchDefinitions.Values.SelectMany(t => t).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/FieldVariableDefinition.cs b/src/HotChocolate/Fusion/src/Core/Metadata/FieldVariableDefinition.cs new file mode 100644 index 00000000000..5e130b1fab0 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/FieldVariableDefinition.cs @@ -0,0 +1,23 @@ +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Metadata; + +internal sealed class FieldVariableDefinition : IVariableDefinition +{ + public FieldVariableDefinition(string name, string schemaName, ITypeNode type, FieldNode select) + { + Name = name; + SchemaName = schemaName; + Type = type; + Select = select; + } + + public string Name { get; } + + public string SchemaName { get; } + + public ITypeNode Type { get; } + + // TODO : this probably should be a selection set ... + public FieldNode Select { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/HttpClientConfig.cs b/src/HotChocolate/Fusion/src/Core/Metadata/HttpClientConfig.cs new file mode 100644 index 00000000000..3b0284f6da9 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/HttpClientConfig.cs @@ -0,0 +1,14 @@ +namespace HotChocolate.Fusion.Metadata; + +internal sealed class HttpClientConfig +{ + public HttpClientConfig(string schemaName, Uri baseAddress) + { + SchemaName = schemaName; + BaseAddress = baseAddress; + } + + public string SchemaName { get; } + + public Uri BaseAddress { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/IType.cs b/src/HotChocolate/Fusion/src/Core/Metadata/IType.cs new file mode 100644 index 00000000000..36ebac2a88c --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/IType.cs @@ -0,0 +1,6 @@ +namespace HotChocolate.Fusion.Metadata; + +internal interface IType // TODO : should be called named type +{ + string Name { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/IVariableDefinition.cs b/src/HotChocolate/Fusion/src/Core/Metadata/IVariableDefinition.cs new file mode 100644 index 00000000000..6fc7fd4a2ab --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/IVariableDefinition.cs @@ -0,0 +1,10 @@ +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Metadata; + +internal interface IVariableDefinition +{ + string Name { get; } + + ITypeNode Type { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/MemberBinding.cs b/src/HotChocolate/Fusion/src/Core/Metadata/MemberBinding.cs new file mode 100644 index 00000000000..d9941518dad --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/MemberBinding.cs @@ -0,0 +1,32 @@ +namespace HotChocolate.Fusion.Metadata; + +/// +/// The type system member binding information. +/// +internal class MemberBinding +{ + /// + /// Initializes a new instance of . + /// + /// + /// The schema to which the type system member is bound to. + /// + /// + /// The name which the type system member has in the . + /// + public MemberBinding(string schemaName, string name) + { + SchemaName = schemaName; + Name = name; + } + + /// + /// Gets the schema to which the type system member is bound to. + /// + public string SchemaName { get; } + + /// + /// Gets the name which the type system member has in the . + /// + public string Name { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/MemberBindingCollection.cs b/src/HotChocolate/Fusion/src/Core/Metadata/MemberBindingCollection.cs new file mode 100644 index 00000000000..8ef4c691096 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/MemberBindingCollection.cs @@ -0,0 +1,27 @@ +using System.Collections; +using System.Diagnostics.CodeAnalysis; + +namespace HotChocolate.Fusion.Metadata; + +internal sealed class MemberBindingCollection : IEnumerable +{ + private readonly Dictionary _bindings; + + public MemberBindingCollection(IEnumerable bindings) + { + _bindings = bindings.ToDictionary(t => t.SchemaName, StringComparer.Ordinal); + } + + public int Count => _bindings.Count; + + public MemberBinding this[string schemaName] => _bindings[schemaName]; + + public bool TryGetValue(string schemaName, [NotNullWhen(true)] out MemberBinding? value) + => _bindings.TryGetValue(schemaName, out value); + + public bool ContainsSchema(string schemaName) => _bindings.ContainsKey(schemaName); + + public IEnumerator GetEnumerator() => _bindings.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ObjectField.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ObjectField.cs new file mode 100644 index 00000000000..d6a79a4ea35 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ObjectField.cs @@ -0,0 +1,24 @@ +namespace HotChocolate.Fusion.Metadata; + +internal sealed class ObjectField +{ + public ObjectField( + string name, + MemberBindingCollection bindings, + ArgumentVariableDefinitionCollection variables, + FetchDefinitionCollection resolvers) + { + Name = name; + Bindings = bindings; + Variables =variables; + Resolvers = resolvers; + } + + public string Name { get; } + + public MemberBindingCollection Bindings { get; } + + public ArgumentVariableDefinitionCollection Variables { get; } + + public FetchDefinitionCollection Resolvers { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ObjectFieldCollection.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ObjectFieldCollection.cs new file mode 100644 index 00000000000..b97c3c7d864 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ObjectFieldCollection.cs @@ -0,0 +1,25 @@ +using System.Collections; +using System.Diagnostics.CodeAnalysis; + +namespace HotChocolate.Fusion.Metadata; + +internal sealed class ObjectFieldCollection : IEnumerable +{ + private readonly Dictionary _fields; + + public ObjectFieldCollection(IEnumerable fields) + { + _fields = fields.ToDictionary(t => t.Name, StringComparer.Ordinal); + } + + public int Count => _fields.Count; + + public ObjectField this[string fieldName] => _fields[fieldName]; + + public bool TryGetValue(string fieldName, [NotNullWhen(true)] out ObjectField? value) + => _fields.TryGetValue(fieldName, out value); + + public IEnumerator GetEnumerator() => _fields.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ObjectType.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ObjectType.cs new file mode 100644 index 00000000000..6a8a982f72d --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ObjectType.cs @@ -0,0 +1,26 @@ +namespace HotChocolate.Fusion.Metadata; + +internal sealed class ObjectType : IType +{ + public ObjectType( + string name, + VariableDefinitionCollection variables, + FetchDefinitionCollection resolvers, + ObjectFieldCollection fields) + { + Name = name; + Variables = variables; + Resolvers = resolvers; + Fields = fields; + } + + public string Name { get; } + + public VariableDefinitionCollection Variables { get; } + + public FetchDefinitionCollection Resolvers { get; } + + public ObjectFieldCollection Fields { get; } + + public override string ToString() => Name; +} diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfiguration.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfiguration.cs new file mode 100644 index 00000000000..8dfe0ae3664 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfiguration.cs @@ -0,0 +1,486 @@ +using HotChocolate.Language; +using HotChocolate.Language.Visitors; +using HotChocolate.Utilities; +using static HotChocolate.Fusion.Metadata.FusionDirectiveNames; +using static HotChocolate.Language.Utf8GraphQLParser.Syntax; + +namespace HotChocolate.Fusion.Metadata; + +internal sealed class ServiceConfiguration +{ + private readonly string[] _bindings; + private readonly Dictionary _types; + private readonly Dictionary<(string, string), string> _typeNameLookup = new(); + + public ServiceConfiguration(IEnumerable bindings, IEnumerable types) + { + _bindings = bindings.ToArray(); + _types = types.ToDictionary(t => t.Name, StringComparer.Ordinal); + } + + public IReadOnlyList Bindings => _bindings; + + public T GetType(string typeName) where T : IType + { + if (_types.TryGetValue(typeName, out var type) && type is T casted) + { + return casted; + } + + throw new InvalidOperationException("Type not found."); + } + + public T GetType(string schemaName, string typeName) where T : IType + { + if (!_typeNameLookup.TryGetValue((schemaName, typeName), out var temp)) + { + temp = typeName; + } + + if (_types.TryGetValue(temp, out var type) && type is T casted) + { + return casted; + } + + throw new InvalidOperationException("Type not found."); + } + + public T GetType(TypeInfo typeInfo) where T : IType + { + throw new NotImplementedException(); + } + + public string GetTypeName(string schemaName, string typeName) + { + if (!_typeNameLookup.TryGetValue((schemaName, typeName), out var temp)) + { + temp = typeName; + } + + return temp; + } + + public string GetTypeName(TypeInfo typeInfo) + { + throw new NotImplementedException(); + } + + public static ServiceConfiguration Load(string sourceText) + => new SchemaReader().Read(sourceText); +} + +internal sealed class SchemaReader +{ + private readonly HashSet _assert = new(); + + public ServiceConfiguration Read(string sourceText) + => ReadServiceDefinition(Utf8GraphQLParser.Parse(sourceText)); + + private ServiceConfiguration ReadServiceDefinition(DocumentNode documentNode) + { + var types = new List(); + IReadOnlyList? httpClientConfigs = null; + + foreach (var definition in documentNode.Definitions) + { + switch (definition) + { + case ObjectTypeDefinitionNode node: + types.Add(ReadObjectType(node)); + break; + + case SchemaDefinitionNode node: + httpClientConfigs = ReadHttpClientConfigs(node.Directives); + break; + } + } + + if (httpClientConfigs is not { Count: > 0 }) + { + // TODO : EXCEPTION + throw new Exception("No clients configured"); + } + + if (types.Count == 0) + { + // TODO : EXCEPTION + throw new Exception("No types"); + } + + + return new ServiceConfiguration( + httpClientConfigs.Select(t => t.SchemaName), + types); + } + + private ObjectType ReadObjectType(ObjectTypeDefinitionNode typeDef) + { + var variables = ReadFieldVariableDefinitions(typeDef.Directives); + var resolvers = ReadFetchDefinitions(typeDef.Directives); + var fields = ReadObjectFields(typeDef.Fields); + return new ObjectType(typeDef.Name.Value, variables, resolvers, fields); + } + + private ObjectFieldCollection ReadObjectFields( + IReadOnlyList fieldDefinitionNodes) + { + var collection = new List(); + + foreach (var fieldDef in fieldDefinitionNodes) + { + var resolvers = ReadFetchDefinitions(fieldDef.Directives); + var bindings = ReadMemberBindings(fieldDef.Directives, fieldDef, resolvers); + var variables = ReadArgumentVariableDefinitions(fieldDef.Directives, fieldDef); + var field = new ObjectField(fieldDef.Name.Value, bindings, variables, resolvers); + collection.Add(field); + } + + return new ObjectFieldCollection(collection); + } + + private IReadOnlyList ReadHttpClientConfigs( + IReadOnlyList directiveNodes) + { + var configs = new List(); + + foreach (var directiveNode in directiveNodes) + { + if (directiveNode.Name.Value.EqualsOrdinal(HttpDirective)) + { + configs.Add(ReadHttpClientConfig(directiveNode)); + } + } + + return configs; + } + + private HttpClientConfig ReadHttpClientConfig( + DirectiveNode directiveNode) + { + AssertName(directiveNode, HttpDirective); + AssertArguments(directiveNode, NameArg, BaseAddressArg); + + string name = default!; ; + string baseAddress = default!; + + foreach (var argument in directiveNode.Arguments) + { + switch (argument.Name.Value) + { + case NameArg: + name = Expect(argument.Value).Value; + break; + + case BaseAddressArg: + baseAddress = Expect(argument.Value).Value; + break; + } + } + + return new HttpClientConfig(name, new Uri(baseAddress)); + } + + private VariableDefinitionCollection ReadFieldVariableDefinitions( + IReadOnlyList directiveNodes) + { + var definitions = new List(); + + foreach (var directiveNode in directiveNodes) + { + if (directiveNode.Name.Value.EqualsOrdinal(VariableDirective)) + { + definitions.Add(ReadFieldVariableDefinition(directiveNode)); + } + } + + return new VariableDefinitionCollection(definitions); + } + + private FieldVariableDefinition ReadFieldVariableDefinition(DirectiveNode directiveNode) + { + AssertName(directiveNode, VariableDirective); + AssertArguments(directiveNode, NameArg, SelectArg, TypeArg, FromArg); + + string name = default!; + FieldNode select = default!; + ITypeNode type = default!; + string schemaName = default!; + + foreach (var argument in directiveNode.Arguments) + { + switch (argument.Name.Value) + { + case NameArg: + name = Expect(argument.Value).Value; + break; + + case SelectArg: + select = ParseField(Expect(argument.Value).Value); + break; + + case TypeArg: + type = ParseTypeReference(Expect(argument.Value).Value); + break; + + case FromArg: + schemaName = Expect(argument.Value).Value; + break; + } + } + + return new FieldVariableDefinition(name, schemaName, type, select); + } + + private FetchDefinitionCollection ReadFetchDefinitions( + IReadOnlyList directiveNodes) + { + var definitions = new List(); + + foreach (var directiveNode in directiveNodes) + { + if (directiveNode.Name.Value.EqualsOrdinal(FetchDirective)) + { + definitions.Add(ReadFetchDefinition(directiveNode)); + } + } + + return new FetchDefinitionCollection(definitions); + } + + private FetchDefinition ReadFetchDefinition(DirectiveNode directiveNode) + { + AssertName(directiveNode, FetchDirective); + AssertArguments(directiveNode, SelectArg, FromArg); + + ISelectionNode select = default!; + string schemaName = default!; + + foreach (var argument in directiveNode.Arguments) + { + switch (argument.Name.Value) + { + case SelectArg: + select = ParseField(Expect(argument.Value).Value); + break; + + case FromArg: + schemaName = Expect(argument.Value).Value; + break; + } + } + + FragmentSpreadNode? placeholder = null; + _assert.Clear(); + + SyntaxVisitor + .Create( + enter: node => + { + if (node is FragmentSpreadNode p) + { + placeholder = p; + return SyntaxVisitor.Break; + } + + if (node is VariableNode v) + { + _assert.Add(v.Name.Value); + } + + return SyntaxVisitor.Continue; + }, + options: new() { VisitArguments = true }) + .Visit(select); + + return new FetchDefinition( + schemaName, + select, + placeholder, + _assert.Count == 0 + ? Array.Empty() + : _assert.ToArray()); + } + + private MemberBindingCollection ReadMemberBindings( + IReadOnlyList directiveNodes, + FieldDefinitionNode annotatedField, + FetchDefinitionCollection resolvers) + { + var definitions = new List(); + + foreach (var directiveNode in directiveNodes) + { + if (directiveNode.Name.Value.EqualsOrdinal(BindDirective)) + { + definitions.Add(ReadMemberBinding(directiveNode, annotatedField)); + } + } + + if (resolvers.Count > 0) + { + _assert.Clear(); + + foreach (var binding in definitions) + { + _assert.Add(binding.SchemaName); + } + + foreach (var resolver in resolvers) + { + if (_assert.Add(resolver.SchemaName)) + { + definitions.Add( + new MemberBinding(resolver.SchemaName, annotatedField.Name.Value)); + } + } + } + + return new MemberBindingCollection(definitions); + } + + private MemberBinding ReadMemberBinding( + DirectiveNode directiveNode, + FieldDefinitionNode annotatedField) + { + AssertName(directiveNode, BindDirective); + AssertArguments(directiveNode, ToArg, AsArg); + + string? name = null; + string schemaName = default!; + + foreach (var argument in directiveNode.Arguments) + { + switch (argument.Name.Value) + { + case AsArg: + name = Expect(argument.Value).Value; + break; + + case ToArg: + schemaName = Expect(argument.Value).Value; + break; + } + } + + return new MemberBinding(schemaName, name ?? annotatedField.Name.Value); + } + + private ArgumentVariableDefinitionCollection ReadArgumentVariableDefinitions( + IReadOnlyList directiveNodes, + FieldDefinitionNode annotatedField) + { + var definitions = new List(); + + foreach (var directiveNode in directiveNodes) + { + if (directiveNode.Name.Value.EqualsOrdinal(VariableDirective)) + { + definitions.Add(ReadArgumentVariableDefinition(directiveNode, annotatedField)); + } + } + + return new ArgumentVariableDefinitionCollection(definitions); + } + + private ArgumentVariableDefinition ReadArgumentVariableDefinition( + DirectiveNode directiveNode, + FieldDefinitionNode annotatedField) + { + AssertName(directiveNode, VariableDirective); + AssertArguments(directiveNode, NameArg, ArgumentArg); + + string name = default!; + string argumentName = default!; + + foreach (var argument in directiveNode.Arguments) + { + switch (argument.Name.Value) + { + case NameArg: + name = Expect(argument.Value).Value; + break; + + case ArgumentArg: + argumentName = Expect(argument.Value).Value; + break; + } + } + + var arg = annotatedField.Arguments.Single(t => t.Name.Value.EqualsOrdinal(argumentName)); + + return new ArgumentVariableDefinition(name, arg.Type, argumentName); + } + + private static T Expect(IValueNode valueNode) where T : IValueNode + { + if (valueNode is not T casted) + { + // TODO : EXCEPTION + throw new InvalidOperationException("Invalid value"); + } + + return casted; + } + + private void AssertName(DirectiveNode directive, string expectedName) + { + if (!directive.Name.Value.EqualsOrdinal(expectedName)) + { + // TODO : EXCEPTION + throw new InvalidOperationException("INVALID DIRECTIVE NAME"); + } + } + + private void AssertArguments(DirectiveNode directive, params string[] expectedArguments) + { + if (directive.Arguments.Count < 0) + { + // TODO : EXCEPTION + throw new InvalidOperationException("INVALID ARGS"); + } + + _assert.Clear(); + + foreach (var argument in directive.Arguments) + { + _assert.Add(argument.Name.Value); + } + + _assert.ExceptWith(expectedArguments); + + if (_assert.Count > 0) + { + // TODO : EXCEPTION + throw new InvalidOperationException("INVALID ARGS"); + } + } +} + +internal static class FusionDirectiveNames +{ + public const string VariableDirective = "variable"; + public const string FetchDirective = "fetch"; + public const string BindDirective = "bind"; + public const string HttpDirective = "httpClient"; + public const string NameArg = "name"; + public const string SelectArg = "select"; + public const string TypeArg = "type"; + public const string FromArg = "from"; + public const string ToArg = "to"; + public const string AsArg = "as"; + public const string ArgumentArg = "argument"; + public const string BaseAddressArg = "baseAddress"; +} + +public readonly struct TypeInfo +{ + public TypeInfo(string schemaName, string typeName) + { + SchemaName = schemaName; + TypeName = typeName; + } + + public string SchemaName { get; } + + public string TypeName { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/VariableDefinitionCollection.cs b/src/HotChocolate/Fusion/src/Core/Metadata/VariableDefinitionCollection.cs new file mode 100644 index 00000000000..af419248cad --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/VariableDefinitionCollection.cs @@ -0,0 +1,28 @@ +using System.Collections; + +namespace HotChocolate.Fusion.Metadata; + +internal sealed class VariableDefinitionCollection : IEnumerable +{ + private readonly FieldVariableDefinition[] _variables; + + public VariableDefinitionCollection(IEnumerable variables) + { + _variables = variables.ToArray(); + } + + public int Count => _variables.Length; + + public IEnumerator GetEnumerator() + { + foreach (var variable in _variables) + { + yield return variable; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Planning/ExecutionNode.cs b/src/HotChocolate/Fusion/src/Core/Planning/ExecutionNode.cs new file mode 100644 index 00000000000..ddba0377fdf --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Planning/ExecutionNode.cs @@ -0,0 +1,33 @@ +namespace HotChocolate.Fusion.Planning; + +internal abstract class ExecutionNode +{ + private readonly List _dependsOn = new(); + private bool _isReadOnly; + + public IReadOnlyList DependsOn => _dependsOn; + + internal void AddDependency(ExecutionNode node) + { + if (_isReadOnly) + { + throw new InvalidOperationException("The execution node is read-only."); + } + + if (!_dependsOn.Contains(node)) + { + _dependsOn.Add(node); + } + } + + internal void Seal() + { + if (!_isReadOnly) + { + OnSeal(); + _isReadOnly = true; + } + } + + protected virtual void OnSeal() { } +} diff --git a/src/HotChocolate/Fusion/src/Core/Planning/ExecutionPlanBuilder.cs b/src/HotChocolate/Fusion/src/Core/Planning/ExecutionPlanBuilder.cs new file mode 100644 index 00000000000..3bda68c98ae --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Planning/ExecutionPlanBuilder.cs @@ -0,0 +1,308 @@ +using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Execution; +using HotChocolate.Fusion.Metadata; +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Planning; + +internal sealed class ExecutionPlanBuilder +{ + private readonly ServiceConfiguration _serviceConfig; + private readonly ISchema _schema; + + public ExecutionPlanBuilder(ServiceConfiguration serviceConfig, ISchema schema) + { + _serviceConfig = serviceConfig ?? throw new ArgumentNullException(nameof(serviceConfig)); + _schema = schema ?? throw new ArgumentNullException(nameof(schema)); + } + + public QueryPlan Build(QueryPlanContext context) + { + foreach (var step in context.Steps) + { + if (step is SelectionExecutionStep executionStep) + { + var requestNode = CreateRequestNode(context, executionStep); + context.RequestNodes.Add(executionStep, requestNode); + } + } + + foreach (var (step, node) in context.RequestNodes) + { + if (step.DependsOn.Count > 0) + { + foreach (var dependency in step.DependsOn) + { + node.AddDependency(context.RequestNodes[dependency]); + } + } + } + + return new QueryPlan(context.RequestNodes.Values, context.Exports.All); + } + + private RequestNode CreateRequestNode( + QueryPlanContext context, + SelectionExecutionStep executionStep) + { + var selectionSet = executionStep.ParentSelection is null + ? context.Operation.RootSelectionSet + : context.Operation.GetSelectionSet( + executionStep.ParentSelection, + _schema.GetType(executionStep.SelectionSetType.Name)); + + var (requestDocument, path) = CreateRequestDocument(context, executionStep); + + var requestHandler = new RequestHandler( + executionStep.SchemaName, + requestDocument, + selectionSet, + // do we need the type? + executionStep.Variables.Values + .Select(t => new RequiredState(t, null!, false)) + .ToArray(), + path); + + return new RequestNode(requestHandler); + } + + private (DocumentNode Document, IReadOnlyList Path) CreateRequestDocument( + QueryPlanContext context, + SelectionExecutionStep executionStep) + { + var rootSelectionSetNode = CreateRootSelectionSetNode(context, executionStep); + IReadOnlyList path = Array.Empty(); + + if (executionStep.Resolver is not null && + executionStep.ParentSelection is not null) + { + ResolveRequirements( + context, + executionStep.ParentSelection, + executionStep.Resolver, + executionStep.Variables); + + var (rootResolver, p) = executionStep.Resolver.CreateSelection( + context.VariableValues, + rootSelectionSetNode); + + rootSelectionSetNode = new SelectionSetNode(new[] { rootResolver }); + path = p; + } + + var operationDefinitionNode = new OperationDefinitionNode( + null, + context.CreateRemoteOperationName(), + OperationType.Query, + context.Exports.CreateVariableDefinitions(executionStep.Variables.Values), + Array.Empty(), + rootSelectionSetNode); + + return (new DocumentNode(new[] { operationDefinitionNode }), path); + } + + private SelectionSetNode CreateRootSelectionSetNode( + QueryPlanContext context, + SelectionExecutionStep executionStep) + { + var selectionNodes = new List(); + var selectionSet = executionStep.RootSelections[0].Selection.DeclaringSelectionSet; + var selectionSetType = executionStep.SelectionSetType; + + // create + foreach (var rootSelection in executionStep.RootSelections) + { + ISelectionNode selectionNode; + var field = selectionSetType.Fields[rootSelection.Selection.Field.Name]; + + if (rootSelection.Resolver is null) + { + selectionNode = CreateSelectionNode( + context, + executionStep, + rootSelection.Selection, + field); + } + else + { + SelectionSetNode? selectionSetNode = null; + + if (rootSelection.Selection.SelectionSet is not null) + { + selectionSetNode = CreateSelectionSetNode( + context, + executionStep, + rootSelection.Selection); + } + + ResolveRequirements( + context, + rootSelection.Selection, + selectionSetType, + executionStep.ParentSelection, + rootSelection.Resolver, + executionStep.Variables); + + var (s, p) = rootSelection.Resolver.CreateSelection( + context.VariableValues, + selectionSetNode); + selectionNode = s; + } + + selectionNodes.Add(selectionNode); + } + + // append exports that were required by other execution steps. + foreach (var selection in context.Exports.GetExportSelections(executionStep, selectionSet)) + { + selectionNodes.Add(selection); + } + + return new SelectionSetNode(selectionNodes); + } + + private ISelectionNode CreateSelectionNode( + QueryPlanContext context, + SelectionExecutionStep executionStep, + ISelection selection, + ObjectField field) + { + SelectionSetNode? selectionSetNode = null; + + if (selection.SelectionSet is not null) + { + selectionSetNode = CreateSelectionSetNode(context, executionStep, selection); + } + + var binding = field.Bindings[executionStep.SchemaName]; + + var alias = !selection.ResponseName.Equals(binding.Name) + ? new NameNode(selection.ResponseName) + : null; + + return new FieldNode( + null, + new(binding.Name), + alias, + null, + Array.Empty(), + Array.Empty(), + selectionSetNode); + } + + private SelectionSetNode CreateSelectionSetNode( + QueryPlanContext context, + SelectionExecutionStep executionStep, + ISelection parentSelection) + { + // TODO : we need to spec inline fragments or a simple selectionsSet depending on pt + var selectionNodes = new List(); + var possibleTypes = context.Operation.GetPossibleTypes(parentSelection); + + foreach (var possibleType in possibleTypes) + { + var typeContext = _serviceConfig.GetType(possibleType.Name); + var selectionSet = context.Operation.GetSelectionSet(parentSelection, possibleType); + + foreach (var selection in selectionSet.Selections) + { + if (executionStep.AllSelections.Contains(selection)) + { + var field = typeContext.Fields[selection.Field.Name]; + var selectionNode = CreateSelectionNode( + context, + executionStep, + selection, + field); + selectionNodes.Add(selectionNode); + } + } + + // append exports that were required by other execution steps. + foreach (var selection in + context.Exports.GetExportSelections(executionStep, selectionSet)) + { + selectionNodes.Add(selection); + } + } + + return new SelectionSetNode(selectionNodes); + } + + private void ResolveRequirements( + QueryPlanContext context, + ISelection parent, + FetchDefinition resolver, + Dictionary variableStateLookup) + { + context.VariableValues.Clear(); + + var parentDeclaringType = _serviceConfig.GetType(parent.DeclaringType.Name); + var parentField = parentDeclaringType.Fields[parent.Field.Name]; + + foreach (var variable in parentField.Variables) + { + if (resolver.Requires.Contains(variable.Name)) + { + var argumentValue = parent.Arguments[variable.ArgumentName]; + context.VariableValues.Add(variable.Name, argumentValue.ValueLiteral!); + } + } + + foreach (var requirement in resolver.Requires) + { + if (!context.VariableValues.ContainsKey(requirement)) + { + var stateKey = variableStateLookup[requirement]; + context.VariableValues.Add(requirement, new VariableNode(stateKey)); + } + } + } + + private void ResolveRequirements( + QueryPlanContext context, + ISelection selection, + ObjectType declaringType, + ISelection? parent, + FetchDefinition resolver, + Dictionary variableStateLookup) + { + context.VariableValues.Clear(); + + var field = declaringType.Fields[selection.Field.Name]; + + foreach (var variable in field.Variables) + { + if (resolver.Requires.Contains(variable.Name)) + { + var argumentValue = selection.Arguments[variable.ArgumentName]; + context.VariableValues.Add(variable.Name, argumentValue.ValueLiteral!); + } + } + + if (parent is not null) + { + var parentDeclaringType = _serviceConfig.GetType(parent.DeclaringType.Name); + var parentField = parentDeclaringType.Fields[parent.Field.Name]; + + foreach (var variable in parentField.Variables) + { + if (!context.VariableValues.ContainsKey(variable.Name) && + resolver.Requires.Contains(variable.Name)) + { + var argumentValue = parent.Arguments[variable.ArgumentName]; + context.VariableValues.Add(variable.Name, argumentValue.ValueLiteral!); + } + } + } + + foreach (var requirement in resolver.Requires) + { + if (!context.VariableValues.ContainsKey(requirement)) + { + var stateKey = variableStateLookup[requirement]; + context.VariableValues.Add(requirement, new VariableNode(stateKey)); + } + } + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Planning/ExportDefinition.cs b/src/HotChocolate/Fusion/src/Core/Planning/ExportDefinition.cs new file mode 100644 index 00000000000..c954975d52f --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Planning/ExportDefinition.cs @@ -0,0 +1,27 @@ +using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Metadata; + +namespace HotChocolate.Fusion.Planning; + +internal readonly struct ExportDefinition +{ + public ExportDefinition( + string stateKey, + ISelectionSet selectionSet, + FieldVariableDefinition variableDefinition, + IExecutionStep executionStep) + { + StateKey = stateKey; + SelectionSet = selectionSet; + VariableDefinition = variableDefinition; + ExecutionStep = executionStep; + } + + public string StateKey { get; } + + public ISelectionSet SelectionSet { get; } + + public FieldVariableDefinition VariableDefinition { get; } + + public IExecutionStep ExecutionStep { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/Planning/ExportDefinitionRegistry.cs b/src/HotChocolate/Fusion/src/Core/Planning/ExportDefinitionRegistry.cs new file mode 100644 index 00000000000..f10c85297b3 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Planning/ExportDefinitionRegistry.cs @@ -0,0 +1,91 @@ +using System.Diagnostics.CodeAnalysis; +using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Metadata; +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Planning; + +internal sealed class ExportDefinitionRegistry +{ + private readonly Dictionary<(ISelectionSet, string), string> _stateKeyLookup = new(); + private readonly Dictionary _exportDefinitions = new(StringComparer.Ordinal); + private readonly string _groupKey = "_fusion_exports_"; + private int _stateId; + + public IReadOnlyCollection All => _exportDefinitions.Values; + + public string Register( + ISelectionSet selectionSet, + FieldVariableDefinition variableDefinition, + IExecutionStep executionStep) + { + var exportDefinition = new ExportDefinition( + $"_{_groupKey}_{++_stateId}", + selectionSet, + variableDefinition, + executionStep); + _exportDefinitions.Add(exportDefinition.StateKey, exportDefinition); + _stateKeyLookup.Add((selectionSet, variableDefinition.Name), exportDefinition.StateKey); + return exportDefinition.StateKey; + } + + public bool TryGetStateKey( + ISelectionSet selectionSet, + string variableName, + [NotNullWhen(true)] out string? stateKey, + [NotNullWhen(true)] out IExecutionStep? executionStep) + { + if (_stateKeyLookup.TryGetValue((selectionSet, variableName), out stateKey)) + { + executionStep = _exportDefinitions[stateKey].ExecutionStep; + return true; + } + + stateKey = null; + executionStep = null; + return false; + } + + public IReadOnlyList CreateVariableDefinitions( + IReadOnlyCollection stateKeys) + { + if (stateKeys.Count == 0) + { + return Array.Empty(); + } + + var definitions = new VariableDefinitionNode[stateKeys.Count]; + var index = 0; + + foreach (var stateKey in stateKeys) + { + var variableDefinition = _exportDefinitions[stateKey].VariableDefinition; + definitions[index++] = new VariableDefinitionNode( + null, + new VariableNode(stateKey), + variableDefinition.Type, + null, + Array.Empty()); + } + + return definitions; + } + + public IEnumerable GetExportSelections( + IExecutionStep executionStep, + ISelectionSet selectionSet) + { + foreach (var exportDefinition in _exportDefinitions.Values) + { + if (ReferenceEquals(exportDefinition.ExecutionStep, executionStep) && + ReferenceEquals(exportDefinition.SelectionSet, selectionSet)) + { + // TODO : we need to transform this for better selection during execution + var selection = exportDefinition.VariableDefinition.Select; + var stateKey = exportDefinition.StateKey; + yield return selection.WithAlias(new NameNode(stateKey)); + } + } + } + +} diff --git a/src/HotChocolate/Fusion/src/Core/Planning/IExecutionStep.cs b/src/HotChocolate/Fusion/src/Core/Planning/IExecutionStep.cs new file mode 100644 index 00000000000..8fe52f82158 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Planning/IExecutionStep.cs @@ -0,0 +1,36 @@ +using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Metadata; + +namespace HotChocolate.Fusion.Planning; + +/// +/// Represents a execution step within the execution plan while being in the planing phase. +/// After the planing phase execution steps are compiled into execution nodes. +/// +internal interface IExecutionStep +{ + /// + /// Gets the schema from which this execution step will fetch data. + /// + string SchemaName { get; } + + /// + /// Gets the declaring type of the root selection set of this execution step. + /// + ObjectType SelectionSetType { get; } + + /// + /// Gets the parent selection. + /// + ISelection? ParentSelection { get; } + + /// + /// Gets the resolver for this execution step. + /// + FetchDefinition? Resolver { get; } + + /// + /// Gets the execution steps this execution step is depending on. + /// + HashSet DependsOn { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlan.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlan.cs new file mode 100644 index 00000000000..45d571a62ae --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlan.cs @@ -0,0 +1,39 @@ +using HotChocolate.Execution.Processing; + +namespace HotChocolate.Fusion.Planning; + +internal sealed class QueryPlan +{ + private readonly ILookup _lookup; + private readonly Dictionary _exports; + + public QueryPlan( + IEnumerable executionNodes, + IEnumerable exportDefinitions) + { + ExecutionNodes = executionNodes.ToArray(); + RootExecutionNodes = ExecutionNodes.Where(t => t.DependsOn.Count == 0).ToArray(); + _lookup = ExecutionNodes.OfType().ToLookup(t => t.Handler.SelectionSet); + + _exports = exportDefinitions + .GroupBy(t => t.SelectionSet, t => t.StateKey) + .ToDictionary(t => t.Key, t => t.ToArray()); + } + + public IReadOnlyList RootExecutionNodes { get; } + + public IReadOnlyList ExecutionNodes { get; } + + public IEnumerable GetRequestNodes(ISelectionSet selectionSet) + => _lookup[selectionSet]; + + public IReadOnlyList GetExports(ISelectionSet selectionSet) + { + if (_exports.TryGetValue(selectionSet, out var exportKeys)) + { + return exportKeys; + } + + return Array.Empty(); + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlanContext.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlanContext.cs new file mode 100644 index 00000000000..612a5d4c4d3 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlanContext.cs @@ -0,0 +1,29 @@ +using HotChocolate.Execution.Processing; +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Planning; + +internal sealed class QueryPlanContext +{ + private readonly string _opName; + private int _opId; + + public QueryPlanContext(IOperation operation) + { + Operation = operation; + _opName = operation.Name ?? "Remote_" + Guid.NewGuid().ToString("N"); + } + + public IOperation Operation { get; } + + public ExportDefinitionRegistry Exports { get; } = new(); + + public List Steps { get; } = new(); + + public Dictionary VariableValues { get; } = new(); + + public Dictionary RequestNodes { get; } = new(); + + public NameNode CreateRemoteOperationName() + => new($"{_opName}_{++_opId}"); +} diff --git a/src/HotChocolate/Fusion/src/Core/Planning/RequestNode.cs b/src/HotChocolate/Fusion/src/Core/Planning/RequestNode.cs new file mode 100644 index 00000000000..606e40b62ea --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Planning/RequestNode.cs @@ -0,0 +1,13 @@ +using HotChocolate.Fusion.Execution; + +namespace HotChocolate.Fusion.Planning; + +internal sealed class RequestNode : ExecutionNode +{ + public RequestNode(RequestHandler handler) + { + Handler = handler; + } + + public RequestHandler Handler { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/Planning/RequestPlaner.cs b/src/HotChocolate/Fusion/src/Core/Planning/RequestPlaner.cs new file mode 100644 index 00000000000..f78a10fd5c1 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Planning/RequestPlaner.cs @@ -0,0 +1,377 @@ +using System.Diagnostics.CodeAnalysis; +using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Metadata; + +namespace HotChocolate.Fusion.Planning; + +/// +/// The request planer will rewrite the into +/// queries against the downstream services. +/// +internal sealed class RequestPlaner +{ + private readonly ServiceConfiguration _serviceConfig; + private readonly Queue _backlog = new(); // TODO: we should get rid of this, maybe put it on the context? + + public RequestPlaner(ServiceConfiguration serviceConfig) + { + _serviceConfig = serviceConfig ?? throw new ArgumentNullException(nameof(serviceConfig)); + } + + public void Plan(QueryPlanContext context) + { + var selectionSetType = _serviceConfig.GetType(context.Operation.RootType.Name); + var selections = context.Operation.RootSelectionSet.Selections; + + Plan(context, selectionSetType, selections, null); + + while (_backlog.TryDequeue(out var item)) + { + Plan(context, item.DeclaringType, item.Selections, item.ParentSelection); + } + } + + private void Plan( + QueryPlanContext context, + ObjectType selectionSetType, + IReadOnlyList selections, + ISelection? parentSelection) + { + var variablesInContext = new HashSet(); + List? leftovers = null; + + do + { + var current = (IReadOnlyList?)leftovers ?? selections; + var schemaName = ResolveBestMatchingSchema(context.Operation, current, selectionSetType); + var workItem = new SelectionExecutionStep(schemaName, selectionSetType, parentSelection); + context.Steps.Add(workItem); + leftovers = null; + FetchDefinition? resolver; + + if (parentSelection is not null && + selectionSetType.Resolvers.ContainsResolvers(schemaName)) + { + CalculateVariablesInContext(selectionSetType, parentSelection, variablesInContext); + if (TryGetResolver(selectionSetType, schemaName, variablesInContext, out resolver)) + { + workItem.Resolver = resolver; + CalculateRequirements(parentSelection, resolver, workItem.Requires); + } + } + + foreach (var selection in current) + { + var field = selectionSetType.Fields[selection.Field.Name]; + if (field.Bindings.TryGetValue(schemaName, out _)) + { + CalculateVariablesInContext( + selection, + selectionSetType, + parentSelection, + variablesInContext); + + resolver = null; + if (field.Resolvers.ContainsResolvers(schemaName)) + { + if (!TryGetResolver(field, schemaName, variablesInContext, out resolver)) + { + // todo : error message and type + throw new InvalidOperationException( + "There must be a field fetch definition valid in this context!"); + } + + CalculateRequirements( + selection, + selectionSetType, + parentSelection, + resolver, + workItem.Requires); + } + + workItem.AllSelections.Add(selection); + workItem.RootSelections.Add(new RootSelection(selection, resolver)); + + if (selection.SelectionSet is not null) + { + CollectChildSelections(context.Operation, selection, workItem); + } + } + else + { + (leftovers ??= new()).Add(selection); + } + } + } while (leftovers is not null); + } + + private void CollectChildSelections( + IOperation operation, + ISelection parentSelection, + SelectionExecutionStep executionStep) + { + foreach (var possibleType in operation.GetPossibleTypes(parentSelection)) + { + var declaringType = _serviceConfig.GetType(possibleType.Name); + var selectionSet = operation.GetSelectionSet(parentSelection, possibleType); + List? leftovers = null; + + executionStep.AllSelectionSets.Add(selectionSet); + + foreach (var selection in selectionSet.Selections) + { + var field = declaringType.Fields[selection.Field.Name]; + + if (field.Bindings.TryGetValue(executionStep.SchemaName, out _)) + { + executionStep.AllSelections.Add(selection); + + if (selection.SelectionSet is not null) + { + CollectChildSelections(operation, selection, executionStep); + } + } + else + { + (leftovers ??= new()).Add(selection); + } + } + + if (leftovers is not null) + { + _backlog.Enqueue(new BacklogItem(parentSelection, declaringType, leftovers)); + } + } + } + + private string ResolveBestMatchingSchema( + IOperation operation, + IReadOnlyList selections, + ObjectType typeContext) + { + var bestScore = 0; + var bestSchema = _serviceConfig.Bindings[0]; + + foreach (var schemaName in _serviceConfig.Bindings) + { + var score = CalculateSchemaScore(operation, selections, typeContext, schemaName); + + if (score > bestScore) + { + bestScore = score; + bestSchema = schemaName; + } + } + + return bestSchema; + } + + private int CalculateSchemaScore( + IOperation operation, + IReadOnlyList selections, + ObjectType typeContext, + string schemaName) + { + var score = 0; + + foreach (var selection in selections) + { + if (typeContext.Fields[selection.Field.Name].Bindings.ContainsSchema(schemaName)) + { + score++; + + if (selection.SelectionSet is not null) + { + foreach (var possibleType in operation.GetPossibleTypes(selection)) + { + var type = _serviceConfig.GetType(possibleType.Name); + var selectionSet = operation.GetSelectionSet(selection, possibleType); + score += CalculateSchemaScore( + operation, + selectionSet.Selections, + type, + schemaName); + } + } + } + } + + return score; + } + + private bool TryGetResolver( + ObjectField field, + string schemaName, + HashSet variablesInContext, + [NotNullWhen(true)] out FetchDefinition? resolver) + { + if (field.Resolvers.TryGetValue(schemaName, out var resolvers)) + { + foreach (var current in resolvers) + { + var canBeUsed = true; + + foreach (var requirement in current.Requires) + { + if (!variablesInContext.Contains(requirement)) + { + canBeUsed = false; + break; + } + } + + if (canBeUsed) + { + resolver = current; + return true; + } + } + } + + resolver = null; + return false; + } + + private bool TryGetResolver( + ObjectType declaringType, + string schemaName, + HashSet variablesInContext, + [NotNullWhen(true)] out FetchDefinition? resolver) + { + if (declaringType.Resolvers.TryGetValue(schemaName, out var resolvers)) + { + foreach (var current in resolvers) + { + var canBeUsed = true; + + foreach (var requirement in current.Requires) + { + if (!variablesInContext.Contains(requirement)) + { + canBeUsed = false; + break; + } + } + + if (canBeUsed) + { + resolver = current; + return true; + } + } + } + + resolver = null; + return false; + } + + private void CalculateVariablesInContext( + ISelection selection, + ObjectType declaringType, + ISelection? parent, + HashSet variablesInContext) + { + variablesInContext.Clear(); + + if (parent is not null) + { + var parentDeclaringType = _serviceConfig.GetType(parent.DeclaringType.Name); + var parentField = parentDeclaringType.Fields[parent.Field.Name]; + + foreach (var variable in parentField.Variables) + { + variablesInContext.Add(variable.Name); + } + } + + foreach (var variable in declaringType.Variables) + { + variablesInContext.Add(variable.Name); + } + + var field = declaringType.Fields[selection.Field.Name]; + + foreach (var variable in field.Variables) + { + variablesInContext.Add(variable.Name); + } + } + + private void CalculateVariablesInContext( + ObjectType declaringType, + ISelection parent, + HashSet variablesInContext) + { + variablesInContext.Clear(); + + var parentDeclaringType = _serviceConfig.GetType(parent.DeclaringType.Name); + var parentField = parentDeclaringType.Fields[parent.Field.Name]; + + foreach (var variable in parentField.Variables) + { + variablesInContext.Add(variable.Name); + } + + foreach (var variable in declaringType.Variables) + { + variablesInContext.Add(variable.Name); + } + } + + private void CalculateRequirements( + ISelection selection, + ObjectType declaringType, + ISelection? parent, + FetchDefinition resolver, + HashSet requirements) + { + var field = declaringType.Fields[selection.Field.Name]; + var inContext = field.Variables.Select(t => t.Name); + + if (parent is not null) + { + var parentDeclaringType = _serviceConfig.GetType(parent.DeclaringType.Name); + var parentField = parentDeclaringType.Fields[parent.Field.Name]; + inContext = inContext.Concat(parentField.Variables.Select(t => t.Name)); + } + + foreach (var requirement in resolver.Requires.Except(inContext)) + { + requirements.Add(requirement); + } + } + + private void CalculateRequirements( + ISelection parent, + FetchDefinition resolver, + HashSet requirements) + { + var parentDeclaringType = _serviceConfig.GetType(parent.DeclaringType.Name); + var parentField = parentDeclaringType.Fields[parent.Field.Name]; + + foreach (var requirement in + resolver.Requires.Except(parentField.Variables.Select(t => t.Name))) + { + requirements.Add(requirement); + } + } + + private readonly struct BacklogItem + { + public BacklogItem( + ISelection parentSelection, + ObjectType declaringType, + IReadOnlyList selections) + { + ParentSelection = parentSelection; + DeclaringType = declaringType; + Selections = selections; + } + + public ISelection ParentSelection { get; } + + public ObjectType DeclaringType { get; } + + public IReadOnlyList Selections { get; } + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Planning/RequirementsPlaner.cs b/src/HotChocolate/Fusion/src/Core/Planning/RequirementsPlaner.cs new file mode 100644 index 00000000000..0b163f95a94 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Planning/RequirementsPlaner.cs @@ -0,0 +1,149 @@ +using HotChocolate.Execution.Processing; +using static System.StringComparer; + +namespace HotChocolate.Fusion.Planning; + +/// +/// The requirements planer will analyze the requirements for each +/// request to a downstream service and enrich these so that all requirements for each requests +/// are fulfilled. +/// +internal sealed class RequirementsPlaner +{ + public void Plan(QueryPlanContext context) + { + var selectionLookup = CreateSelectionLookup(context.Steps); + var schemas = new Dictionary(Ordinal); + var requires = new HashSet(Ordinal); + + foreach (var step in context.Steps) + { + if (step is SelectionExecutionStep executionStep && + executionStep.ParentSelection is { } parent && + executionStep.Resolver is { }) + { + var declaringType = executionStep.RootSelections[0].Selection.DeclaringType; + var selectionSet = context.Operation.GetSelectionSet(parent, declaringType); + var siblingExecutionSteps = GetSiblingExecutionSteps(selectionLookup, selectionSet); + + // remove the execution step for which we try to resolve dependencies. + siblingExecutionSteps.Remove(executionStep); + + // clean and fill the schema execution step lookup + foreach (var siblingExecutionStep in siblingExecutionSteps) + { + schemas.TryAdd(siblingExecutionStep.SchemaName, siblingExecutionStep); + } + + // clean and fill requires set + InitializeSet(requires, executionStep.Requires); + + // first we need to check if the selectionSet from which we want to do the + // exports already is exporting the required variables + // if so we just need to refer to it. + foreach (var requirement in requires) + { + if (context.Exports.TryGetStateKey( + selectionSet, + requirement, + out var stateKey, + out var providingExecutionStep)) + { + executionStep.DependsOn.Add(providingExecutionStep); + executionStep.Variables.Add(requirement, stateKey); + } + } + + // if we still have requirements unfulfilled will try to resolve them + // from sibling execution steps. + foreach (var variable in step.SelectionSetType.Variables) + { + var schemaName = variable.SchemaName; + if (requires.Contains(variable.Name) && + schemas.TryGetValue(schemaName, out var providingExecutionStep)) + { + requires.Remove(variable.Name); + + var stateKey = context.Exports.Register( + selectionSet, + variable, + providingExecutionStep); + + executionStep.DependsOn.Add(providingExecutionStep); + executionStep.Variables.Add(variable.Name, stateKey); + } + } + + // it could happen that the existing execution steps cannot fulfill our needs + // and that we have to introduce a fetch to another remote schema to get the + // required value for the current execution step. In this case we will have + // to evaluate the schemas that we did skip for efficiency reasons. + // TODO: CODE + + if (requires.Count > 0) + { + // if the schema meta data are not consistent we could end up with no way to + // execute the current execution step. In this case we will fail here. + // TODO : NEEDS A PROPER EXCEPTION + throw new Exception("NEEDS A PROPER EXCEPTION"); + } + } + } + } + + private static HashSet GetSiblingExecutionSteps( + Dictionary selectionLookup, + ISelectionSet selectionSet) + { + var executionSteps = new HashSet(); + + if (selectionLookup.TryGetValue(selectionSet, out var executionStep)) + { + executionSteps.Add(executionStep); + } + + foreach (var sibling in selectionSet.Selections) + { + if (selectionLookup.TryGetValue(sibling, out executionStep)) + { + executionSteps.Add(executionStep); + } + } + + return executionSteps; + } + + private static Dictionary CreateSelectionLookup( + IReadOnlyList executionSteps) + { + var dictionary = new Dictionary(); + + foreach (var executionStep in executionSteps) + { + if (executionStep is SelectionExecutionStep ses) + { + foreach (var selection in ses.AllSelections) + { + dictionary.Add(selection, ses); + } + + foreach (var selectionSet in ses.AllSelectionSets) + { + dictionary.Add(selectionSet, ses); + } + } + } + + return dictionary; + } + + private static void InitializeSet(HashSet set, IEnumerable values) + { + set.Clear(); + + foreach (var value in values) + { + set.Add(value); + } + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Planning/RootSelection.cs b/src/HotChocolate/Fusion/src/Core/Planning/RootSelection.cs new file mode 100644 index 00000000000..4430d694679 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Planning/RootSelection.cs @@ -0,0 +1,17 @@ +using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Metadata; + +namespace HotChocolate.Fusion.Planning; + +internal readonly struct RootSelection +{ + public RootSelection(ISelection selection, FetchDefinition? resolver) + { + Selection = selection; + Resolver = resolver; + } + + public ISelection Selection { get; } + + public FetchDefinition? Resolver { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/Planning/SelectionExecutionStep.cs b/src/HotChocolate/Fusion/src/Core/Planning/SelectionExecutionStep.cs new file mode 100644 index 00000000000..9410d2a5c39 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Planning/SelectionExecutionStep.cs @@ -0,0 +1,53 @@ +using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Metadata; + +namespace HotChocolate.Fusion.Planning; + +internal class SelectionExecutionStep : IExecutionStep +{ + public SelectionExecutionStep( + string schemaNameName, + ObjectType selectionSetType, + ISelection? parentSelection) + { + SelectionSetType = selectionSetType; + ParentSelection = parentSelection; + SchemaName = schemaNameName; + } + + public string SchemaName { get; } + + /// + /// The type name of the root selection set of this execution step. + /// If is null then the selection set is the + /// operation root selection set, otherwise its the selection set resolved + /// by using the . + /// + public ObjectType SelectionSetType { get; } + + public ISelection? ParentSelection { get; } + + public FetchDefinition? Resolver { get; set; } + + public List RootSelections { get; } = new(); + + public HashSet AllSelections { get; } = new(); + + public HashSet AllSelectionSets { get; } = new(); + + /// + /// Gets the execution steps this execution step is depending on. + /// + public HashSet DependsOn { get; } = new(); + + /// + /// Gets a map for this execution task from the variable name + /// to the internal state key. + /// + public Dictionary Variables { get; } = new(); + + /// + /// The variable requirements by this task. + /// + public HashSet Requires { get; } = new(); +} diff --git a/src/HotChocolate/Fusion/src/Core/Utilities/JsonQueryResultFormatter.cs b/src/HotChocolate/Fusion/src/Core/Utilities/JsonQueryResultFormatter.cs new file mode 100644 index 00000000000..9a7ef81c2c3 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Utilities/JsonQueryResultFormatter.cs @@ -0,0 +1,447 @@ +using System.Buffers; +using System.Collections; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using HotChocolate.Execution; +using HotChocolate.Execution.Processing; +using HotChocolate.Execution.Serialization; +using HotChocolate.Utilities; +using static HotChocolate.Execution.Serialization.JsonConstants; + +namespace HotChocolate.Fusion.Utilities; + +public sealed class JsonQueryResultFormatter : IQueryResultFormatter +{ + private readonly JsonWriterOptions _options; + + /// + /// Creates a new instance of . + /// + /// + /// Defines whether the underlying + /// should pretty print the JSON which includes: + /// indenting nested JSON tokens, adding new lines, and adding + /// white space between property names and values. + /// By default, the JSON is written without any extra white space. + /// + /// + /// Gets or sets the encoder to use when escaping strings, or null to use the default encoder. + /// + public JsonQueryResultFormatter(bool indented = false, JavaScriptEncoder? encoder = null) + { + _options = new JsonWriterOptions { Indented = indented, Encoder = encoder }; + } + + public string Format(IQueryResult result) + { + if (result is null) + { + throw new ArgumentNullException(nameof(result)); + } + + using var arrayWriter = new ArrayWriter(); + Format(result, arrayWriter); + return Encoding.UTF8.GetString(arrayWriter.GetInternalBuffer(), 0, arrayWriter.Length); + } + + /// + public void Format(IQueryResult result, IBufferWriter writer) + { + if (result is null) + { + throw new ArgumentNullException(nameof(result)); + } + + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + using var jsonWriter = new Utf8JsonWriter(writer, _options); + WriteResult(jsonWriter, result); + jsonWriter.Flush(); + } + + /// + public async Task FormatAsync( + IQueryResult result, + Stream outputStream, + CancellationToken cancellationToken = default) + { + if (result is null) + { + throw new ArgumentNullException(nameof(result)); + } + + if (outputStream is null) + { + throw new ArgumentNullException(nameof(outputStream)); + } + + await using var writer = new Utf8JsonWriter(outputStream, _options); + + WriteResult(writer, result); + + await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + private void WriteResult(Utf8JsonWriter writer, IQueryResult result) + { + writer.WriteStartObject(); + + WritePatchInfo(writer, result); + WriteErrors(writer, result.Errors); + WriteData(writer, result.Data); + WriteExtensions(writer, result.Extensions); + WriteHasNext(writer, result); + + writer.WriteEndObject(); + } + + private static void WritePatchInfo( + Utf8JsonWriter writer, + IQueryResult result) + { + if (result.Label is not null) + { + writer.WriteString("label", result.Label); + } + + if (result.Path is not null) + { + WritePath(writer, result.Path); + } + } + + private static void WriteHasNext( + Utf8JsonWriter writer, + IQueryResult result) + { + if (result.HasNext.HasValue) + { + writer.WriteBoolean("hasNext", result.HasNext.Value); + } + } + + private void WriteData( + Utf8JsonWriter writer, + IReadOnlyDictionary? data) + { + if (data is not null) + { + writer.WritePropertyName(Data); + + if (data is ObjectResult resultMap) + { + WriteObjectResult(writer, resultMap); + } + else + { + WriteDictionary(writer, data); + } + } + } + + private void WriteErrors(Utf8JsonWriter writer, IReadOnlyList? errors) + { + if (errors is { Count: > 0 }) + { + writer.WritePropertyName(JsonConstants.Errors); + + writer.WriteStartArray(); + + for (var i = 0; i < errors.Count; i++) + { + WriteError(writer, errors[i]); + } + + writer.WriteEndArray(); + } + } + + private void WriteError(Utf8JsonWriter writer, IError error) + { + writer.WriteStartObject(); + + writer.WriteString(Message, error.Message); + + WriteLocations(writer, error.Locations); + WritePath(writer, error.Path); + WriteExtensions(writer, error.Extensions); + + writer.WriteEndObject(); + } + + private static void WriteLocations(Utf8JsonWriter writer, IReadOnlyList? locations) + { + if (locations is { Count: > 0 }) + { + writer.WritePropertyName(Locations); + + writer.WriteStartArray(); + + for (var i = 0; i < locations.Count; i++) + { + WriteLocation(writer, locations[i]); + } + + writer.WriteEndArray(); + } + } + + private static void WriteLocation(Utf8JsonWriter writer, Location location) + { + writer.WriteStartObject(); + writer.WriteNumber(Line, location.Line); + writer.WriteNumber(Column, location.Column); + writer.WriteEndObject(); + } + + private static void WritePath(Utf8JsonWriter writer, Path? path) + { + if (path is not null) + { + writer.WritePropertyName(JsonConstants.Path); + WritePathValue(writer, path); + } + } + + private static void WritePathValue(Utf8JsonWriter writer, Path path) + { + if (path.IsRoot) + { + writer.WriteStartArray(); + writer.WriteEndArray(); + return; + } + + writer.WriteStartArray(); + + var list = path.ToList(); + + for (var i = 0; i < list.Count; i++) + { + switch (list[i]) + { + case string s: + writer.WriteStringValue(s); + break; + + case int n: + writer.WriteNumberValue(n); + break; + + case short n: + writer.WriteNumberValue(n); + break; + + case long n: + writer.WriteNumberValue(n); + break; + + default: + writer.WriteStringValue(list[i].ToString()); + break; + } + } + + writer.WriteEndArray(); + } + + private void WriteExtensions( + Utf8JsonWriter writer, + IReadOnlyDictionary? dict) + { + if (dict is { Count: > 0 }) + { + writer.WritePropertyName(Extensions); + WriteDictionary(writer, dict); + } + } + + private void WriteDictionary( + Utf8JsonWriter writer, + IReadOnlyDictionary dict) + { + writer.WriteStartObject(); + + foreach (var item in dict) + { + writer.WritePropertyName(item.Key); + WriteFieldValue(writer, item.Value); + } + + writer.WriteEndObject(); + } + + private void WriteDictionary( + Utf8JsonWriter writer, + Dictionary dict) + { + writer.WriteStartObject(); + + foreach (var item in dict) + { + writer.WritePropertyName(item.Key); + WriteFieldValue(writer, item.Value); + } + + writer.WriteEndObject(); + } + + private void WriteObjectResult( + Utf8JsonWriter writer, + ObjectResult objectResult) + { + writer.WriteStartObject(); + + ref var searchSpace = ref objectResult.GetReference(); + + for(var i = 0; i < objectResult.Capacity; i++) + { + var field = Unsafe.Add(ref searchSpace, i); + if (field.IsInitialized) + { + writer.WritePropertyName(field.Name); + WriteFieldValue(writer, field.Value); + } + } + + writer.WriteEndObject(); + } + + private void WriteListResult( + Utf8JsonWriter writer, + ListResult list) + { + writer.WriteStartArray(); + + ref var searchSpace = ref list.GetReference(); + + for (var i = 0; i < list.Count; i++) + { + var element = Unsafe.Add(ref searchSpace, i); + WriteFieldValue(writer, element); + } + + writer.WriteEndArray(); + } + + private void WriteList( + Utf8JsonWriter writer, + IList list) + { + writer.WriteStartArray(); + + for (var i = 0; i < list.Count; i++) + { + WriteFieldValue(writer, list[i]); + } + + writer.WriteEndArray(); + } + + private void WriteFieldValue( + Utf8JsonWriter writer, + object? value) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + switch (value) + { + case ObjectResult resultMap: + WriteObjectResult(writer, resultMap); + break; + + case ListResult resultMapList: + WriteListResult(writer, resultMapList); + break; + + case JsonElement element: + writer.WriteRawValue(element.GetRawText()); + break; + + case Dictionary dict: + WriteDictionary(writer, dict); + break; + + case IReadOnlyDictionary dict: + WriteDictionary(writer, dict); + break; + + case IList list: + WriteList(writer, list); + break; + + case IError error: + WriteError(writer, error); + break; + + case string s: + writer.WriteStringValue(s); + break; + + case byte b: + writer.WriteNumberValue(b); + break; + + case short s: + writer.WriteNumberValue(s); + break; + + case ushort s: + writer.WriteNumberValue(s); + break; + + case int i: + writer.WriteNumberValue(i); + break; + + case uint i: + writer.WriteNumberValue(i); + break; + + case long l: + writer.WriteNumberValue(l); + break; + + case ulong l: + writer.WriteNumberValue(l); + break; + + case float f: + writer.WriteNumberValue(f); + break; + + case double d: + writer.WriteNumberValue(d); + break; + + case decimal d: + writer.WriteNumberValue(d); + break; + + case bool b: + writer.WriteBooleanValue(b); + break; + + case Uri u: + writer.WriteStringValue(u.ToString()); + break; + + case Path p: + WritePathValue(writer, p); + break; + + default: + writer.WriteStringValue(value.ToString()); + break; + } + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Utilities/JsonValueToGraphQLValueConverter.cs b/src/HotChocolate/Fusion/src/Core/Utilities/JsonValueToGraphQLValueConverter.cs new file mode 100644 index 00000000000..3295f86fba5 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Utilities/JsonValueToGraphQLValueConverter.cs @@ -0,0 +1,52 @@ +using System.Text.Json; +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Utilities; + +internal static class JsonValueToGraphQLValueConverter +{ + public static IValueNode Convert(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + var fields = new List(); + + foreach (var property in element.EnumerateObject()) + { + fields.Add(new ObjectFieldNode(property.Name, Convert(property.Value))); + } + + return new ObjectValueNode(fields); + + case JsonValueKind.Array: + var index = 0; + var items = new IValueNode[element.GetArrayLength()]; + + foreach (var item in element.EnumerateArray()) + { + items[index++] = Convert(item); + } + + return new ListValueNode(items); + + case JsonValueKind.String: + return new StringValueNode(element.GetString()!); + + case JsonValueKind.Number: + return Utf8GraphQLParser.Syntax.ParseValueLiteral(element.GetRawText()); + + case JsonValueKind.True: + return BooleanValueNode.True; + + case JsonValueKind.False: + return BooleanValueNode.False; + + case JsonValueKind.Null: + return NullValueNode.Default; + + default: + throw new ArgumentOutOfRangeException(); + } + } +} diff --git a/src/HotChocolate/Fusion/test/Core.Tests/ExecutionPlanBuilderTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/ExecutionPlanBuilderTests.cs new file mode 100644 index 00000000000..605796b40f1 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/ExecutionPlanBuilderTests.cs @@ -0,0 +1,407 @@ +using CookieCrumble; +using HotChocolate.Execution; +using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Planning; +using HotChocolate.Language; +using Microsoft.Extensions.DependencyInjection; +using static HotChocolate.Language.Utf8GraphQLParser; + +namespace HotChocolate.Fusion; + +public class ExecutionPlanBuilderTests +{ + [Fact] + public async Task GetPersonById_With_Name_And_Bio() + { + // arrange + const string sdl = @" + type Query { + personById(id: ID!) : Person + } + + type Person { + id: ID! + name: String! + bio: String + }"; + + const string serviceDefinition = @" + type Query { + personById(id: ID!): Person + @variable(name: ""personId"", argument: ""id"") + @bind(to: ""a"") + @fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") + } + + type Person + @variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"") + @variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"") + @fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") { + + id: ID! + @bind(to: ""a"") + @bind(to: ""b"") + name: String! + @bind(to: ""a"") + bio: String + @bind(to: ""b"") + } + + schema + @httpClient(name: ""a"" baseAddress: ""https://a/graphql"") + @httpClient(name: ""b"" baseAddress: ""https://b/graphql"") { + query: Query + }"; + + var schema = await new ServiceCollection() + .AddGraphQL() + .AddDocumentFromString(sdl) + .UseField(n => n) + .BuildSchemaAsync(); + + var serviceConfig = Metadata.ServiceConfiguration.Load(serviceDefinition); + + var request = + Parse( + @"query GetPersonById { + personById(id: 1) { + id + name + bio + } + }"); + + var operationCompiler = new OperationCompiler(new()); + var operation = operationCompiler.Compile( + "abc", + (OperationDefinitionNode)request.Definitions.First(), + schema.QueryType, + request, + schema); + + // act + var queryPlanContext = new QueryPlanContext(operation); + var requestPlaner = new RequestPlaner(serviceConfig); + var requirementsPlaner = new RequirementsPlaner(); + var executionPlanBuilder = new ExecutionPlanBuilder(serviceConfig, schema); + + requestPlaner.Plan(queryPlanContext); + requirementsPlaner.Plan(queryPlanContext); + var queryPlan = executionPlanBuilder.Build(queryPlanContext); + + // assert + var index = 0; + var snapshot = new Snapshot(); + snapshot.Add(request, "User Request"); + + foreach (var executionNode in queryPlan.ExecutionNodes) + { + if (executionNode is RequestNode rn) + { + snapshot.Add(rn.Handler.Document, $"Request {++index}"); + } + } + + await snapshot.MatchAsync(); + } + + [Fact] + public async Task GetPersonById_With_Bio() + { + // arrange + const string sdl = @" + type Query { + personById(id: ID!) : Person + } + + type Person { + id: ID! + name: String! + bio: String + }"; + + const string serviceDefinition = @" + type Query { + personById(id: ID!): Person + @variable(name: ""personId"", argument: ""id"") + @fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") + } + + type Person + @variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"") + @variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"") + @fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") { + + id: ID! + @bind(to: ""a"") + @bind(to: ""b"") + name: String! + @bind(to: ""a"") + bio: String + @bind(to: ""b"") + } + + schema + @httpClient(name: ""a"" baseAddress: ""https://a/graphql"") + @httpClient(name: ""b"" baseAddress: ""https://b/graphql"") { + query: Query + }"; + + var schema = await new ServiceCollection() + .AddGraphQL() + .AddDocumentFromString(sdl) + .UseField(n => n) + .BuildSchemaAsync(); + + var serviceConfig = Metadata.ServiceConfiguration.Load(serviceDefinition); + + var request = + Parse( + @"query GetPersonById { + personById(id: 1) { + id + bio + } + }"); + + var operationCompiler = new OperationCompiler(new()); + var operation = operationCompiler.Compile( + "abc", + (OperationDefinitionNode)request.Definitions.First(), + schema.QueryType, + request, + schema); + + // act + var queryPlanContext = new QueryPlanContext(operation); + var requestPlaner = new RequestPlaner(serviceConfig); + var requirementsPlaner = new RequirementsPlaner(); + var executionPlanBuilder = new ExecutionPlanBuilder(serviceConfig, schema); + + requestPlaner.Plan(queryPlanContext); + requirementsPlaner.Plan(queryPlanContext); + var queryPlan = executionPlanBuilder.Build(queryPlanContext); + + // assert + var index = 0; + var snapshot = new Snapshot(); + snapshot.Add(request, "User Request"); + + foreach (var executionNode in queryPlan.ExecutionNodes) + { + if (executionNode is RequestNode rn) + { + snapshot.Add(rn.Handler.Document, $"Request {++index}"); + } + } + + await snapshot.MatchAsync(); + } + + [Fact] + public async Task GetPersonById_With_Bio_Friends_Bio() + { + // arrange + const string sdl = @" + type Query { + personById(id: ID!) : Person + } + + type Person { + id: ID! + name: String! + bio: String + friends: [Person!] + }"; + + const string serviceDefinition = @" + type Query { + personById(id: ID!): Person + @variable(name: ""personId"", argument: ""id"") + @fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") + } + + type Person + @variable(name: ""personId"", select: ""id"" from: ""a"" type: ""ID!"") + @variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"") + @fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") { + + id: ID! + @bind(to: ""a"") + @bind(to: ""b"") + @bind(to: ""c"") + name: String! + @bind(to: ""a"") + bio: String + @bind(to: ""b"") + friends: [Person!] + @bind(to: ""a"") + } + + schema + @httpClient(name: ""a"" baseAddress: ""https://a/graphql"") + @httpClient(name: ""b"" baseAddress: ""https://b/graphql"") { + query: Query + }"; + + var schema = await new ServiceCollection() + .AddGraphQL() + .AddDocumentFromString(sdl) + .UseField(n => n) + .BuildSchemaAsync(); + + var serviceConfig = Metadata.ServiceConfiguration.Load(serviceDefinition); + + var request = + Parse( + @"query GetPersonById { + personById(id: 1) { + bio + friends { + bio + } + } + }"); + + var operationCompiler = new OperationCompiler(new()); + var operation = operationCompiler.Compile( + "abc", + (OperationDefinitionNode)request.Definitions[0], + schema.QueryType, + request, + schema); + + // act + var queryPlanContext = new QueryPlanContext(operation); + var requestPlaner = new RequestPlaner(serviceConfig); + var requirementsPlaner = new RequirementsPlaner(); + var executionPlanBuilder = new ExecutionPlanBuilder(serviceConfig, schema); + + requestPlaner.Plan(queryPlanContext); + requirementsPlaner.Plan(queryPlanContext); + var queryPlan = executionPlanBuilder.Build(queryPlanContext); + + // assert + var index = 0; + var snapshot = new Snapshot(); + snapshot.Add(request, "User Request"); + + foreach (var executionNode in queryPlan.ExecutionNodes) + { + if (executionNode is RequestNode rn) + { + snapshot.Add(rn.Handler.Document, $"Request {++index}"); + } + } + + await snapshot.MatchAsync(); + } + + [Fact] + public async Task GetPersonById_With_Name_Friends_Name_Bio() + { + // arrange + const string sdl = @" + type Query { + personById(id: ID!) : Person + } + + type Person { + id: ID! + name: String! + bio: String + friends: [Person!] + }"; + + const string serviceDefinition = @" + type Query { + personById(id: ID!): Person + @variable(name: ""personId"", argument: ""id"") + @fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") + } + + type Person + @variable(name: ""personId"", select: ""id"" from: ""a"" type: ""ID!"") + @variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"") + @fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") { + + id: ID! + @bind(to: ""a"") + @bind(to: ""b"") + @bind(to: ""c"") + name: String! + @bind(to: ""a"") + bio: String + @bind(to: ""b"") + friends: [Person!] + @bind(to: ""a"") + } + + schema + @httpClient(name: ""a"" baseAddress: ""https://a/graphql"") + @httpClient(name: ""b"" baseAddress: ""https://b/graphql"") { + query: Query + }"; + + var schema = await new ServiceCollection() + .AddGraphQL() + .AddDocumentFromString(sdl) + .UseField(n => n) + .BuildSchemaAsync(); + + var serviceConfig = Metadata.ServiceConfiguration.Load(serviceDefinition); + + var request = + Parse( + @"query GetPersonById { + personById(id: 1) { + name + friends { + name + bio + } + } + }"); + + var operationCompiler = new OperationCompiler(new()); + var operation = operationCompiler.Compile( + "abc", + (OperationDefinitionNode)request.Definitions[0], + schema.QueryType, + request, + schema); + + // act + var queryPlanContext = new QueryPlanContext(operation); + var requestPlaner = new RequestPlaner(serviceConfig); + var requirementsPlaner = new RequirementsPlaner(); + var executionPlanBuilder = new ExecutionPlanBuilder(serviceConfig, schema); + + requestPlaner.Plan(queryPlanContext); + requirementsPlaner.Plan(queryPlanContext); + var queryPlan = executionPlanBuilder.Build(queryPlanContext); + + // assert + var index = 0; + var snapshot = new Snapshot(); + snapshot.Add(request, "User Request"); + + foreach (var executionNode in queryPlan.ExecutionNodes) + { + if (executionNode is RequestNode rn) + { + snapshot.Add(rn.Handler.Document, $"Request {++index}"); + } + } + + await snapshot.MatchAsync(); + } +} diff --git a/src/HotChocolate/Fusion/test/Core.Tests/HotChocolate.Fusion.Tests.csproj b/src/HotChocolate/Fusion/test/Core.Tests/HotChocolate.Fusion.Tests.csproj index bc297ef8e2d..c9081e76296 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/HotChocolate.Fusion.Tests.csproj +++ b/src/HotChocolate/Fusion/test/Core.Tests/HotChocolate.Fusion.Tests.csproj @@ -1,8 +1,17 @@ - - - - HotChocolate.Fusion.Tests - HotChocolate.Fusion - - - + + + + HotChocolate.Fusion.Tests + HotChocolate.Fusion + + + + + + + + + + + + diff --git a/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs new file mode 100644 index 00000000000..36aeea914a0 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs @@ -0,0 +1,258 @@ +using CookieCrumble; +using HotChocolate.Execution; +using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Execution; +using HotChocolate.Fusion.Planning; +using HotChocolate.Fusion.Utilities; +using HotChocolate.Language; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using static HotChocolate.Language.Utf8GraphQLParser; + +namespace HotChocolate.Fusion; + +public class RemoteQueryExecutorTests +{ + private readonly TestServerFactory _testServerFactory = new(); + + [Fact] + public async Task Do() + { + using var server1 = _testServerFactory.Create( + s => s + .AddRouting() + .AddSingleton() + .AddGraphQLServer() + .AddQueryType(), + c => c + .UseRouting() + .UseEndpoints(endpoints => endpoints.MapGraphQL())); + + using var server2 = _testServerFactory.Create( + s => s + .AddRouting() + .AddSingleton() + .AddGraphQLServer() + .AddQueryType(), + c => c + .UseRouting() + .UseEndpoints(endpoints => endpoints.MapGraphQL())); + + // arrange + const string sdl = @" + type Query { + personById(id: ID!) : Person + } + + type Person { + id: ID! + name: String! + bio: String + friends: [Person!] + }"; + + const string serviceDefinition = @" + type Query { + personById(id: ID!): Person + @variable(name: ""personId"", argument: ""id"") + @fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(from: ""b"", select: ""personById(id: $personId) { ... Person }"") + } + + type Person + @variable(name: ""personId"", select: ""id"" from: ""a"" type: ""Int!"") + @variable(name: ""personId"", select: ""id"" from: ""b"" type: ""Int!"") + @fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(from: ""b"", select: ""personById(id: $personId) { ... Person }"") { + + id: ID! + @bind(to: ""a"") + @bind(to: ""b"") + @bind(to: ""c"") + name: String! + @bind(to: ""a"") + bio: String + @bind(to: ""b"") + + friends: [Person!] + @bind(to: ""a"") + } + + schema + @httpClient(name: ""a"" baseAddress: ""https://a/graphql"") + @httpClient(name: ""b"" baseAddress: ""https://b/graphql"") { + query: Query + }"; + + var schema = await new ServiceCollection() + .AddGraphQL() + .AddDocumentFromString(sdl) + .UseField(n => n) + .BuildSchemaAsync(); + + var serviceConfig = Metadata.ServiceConfiguration.Load(serviceDefinition); + + var request = + Parse( + @"query GetPersonById { + personById(id: 4) { + name + friends { + name + bio + } + } + }"); + + var operationCompiler = new OperationCompiler(new()); + var operation = operationCompiler.Compile( + "abc", + (OperationDefinitionNode)request.Definitions.First(), + schema.QueryType, + request, + schema); + + // act + var queryPlanContext = new QueryPlanContext(operation); + var requestPlaner = new RequestPlaner(serviceConfig); + var requirementsPlaner = new RequirementsPlaner(); + var executionPlanBuilder = new ExecutionPlanBuilder(serviceConfig, schema); + + requestPlaner.Plan(queryPlanContext); + requirementsPlaner.Plan(queryPlanContext); + var queryPlan = executionPlanBuilder.Build(queryPlanContext); + + var clients = new Dictionary> + { + { + "a", + () => + { + var httpClient = server1.CreateClient(); + httpClient.BaseAddress = new Uri("http://localhost:5000/graphql"); + return httpClient; + } + }, + { + "b", + () => + { + var httpClient = server2.CreateClient(); + httpClient.BaseAddress = new Uri("http://localhost:5000/graphql"); + return httpClient; + } + }, + }; + + var clientFactory = new MockHttpClientFactory(clients); + + var executor1 = new HttpRequestExecutor("a", clientFactory); + var executor2 = new HttpRequestExecutor("b", clientFactory); + + var executorFactory = new RemoteRequestExecutorFactory(new[] { executor1, executor2 }); + + var executor = new RemoteQueryExecutor2(serviceConfig, executorFactory); + var context = new RemoteExecutorContext( + schema, + new ResultBuilder( + new ResultPool( + new ObjectResultPool(32, 32), + new ListResultPool(32, 32))), + operation, + queryPlan, + new HashSet(queryPlan.ExecutionNodes.OfType().Select(t => t.Handler.SelectionSet))); + + + var result = await executor.ExecuteAsync(context); + + // assert + var index = 0; + var snapshot = new Snapshot(); + var formatter = new JsonQueryResultFormatter(indented: true); + + snapshot.Add(request, "User Request"); + + foreach (var executionNode in queryPlan.ExecutionNodes) + { + if (executionNode is RequestNode rn) + { + snapshot.Add(rn.Handler.Document, $"Request {++index}"); + } + } + + snapshot.Add(formatter.Format(result), "Result"); + + await snapshot.MatchAsync(); + } + + public class MockHttpClientFactory : IHttpClientFactory + { + private readonly Dictionary> _clients; + + public MockHttpClientFactory(Dictionary> clients) + { + _clients = clients; + } + + public HttpClient CreateClient(string name) + => _clients[name].Invoke(); + } + + public class Query1 + { + public Person1? GetPersonById(int id, [Service] Repository1 repository) + => repository.GetPersonById(id); + } + + public class Query2 + { + public Person2? GetPersonById(int id, [Service] Repository2 repository) + => repository.GetPersonById(id); + } + + public class Repository1 + { + private readonly Dictionary _store = new(); + + public Repository1() + { + var person1 = new Person1(1, "Pascal", Array.Empty()); + var person2 = new Person1(2, "Michael", Array.Empty()); + var person3 = new Person1(3, "Martin", Array.Empty()); + var person4 = new Person1(4, "Rafi", new[] { person1, person2, person3 }); + + _store.Add(person1.Id, person1); + _store.Add(person2.Id, person2); + _store.Add(person3.Id, person3); + _store.Add(person4.Id, person4); + } + + public Person1? GetPersonById(int id) + => _store.TryGetValue(id, out var p) ? p : null; + } + + public class Repository2 + { + private readonly Dictionary _store = new(); + + public Repository2() + { + var person1 = new Person2(1, "Foo"); + var person2 = new Person2(2, "Bar"); + var person3 = new Person2(3, "Baz"); + var person4 = new Person2(4, "Qux"); + + _store.Add(person1.Id, person1); + _store.Add(person2.Id, person2); + _store.Add(person3.Id, person3); + _store.Add(person4.Id, person4); + } + + public Person2? GetPersonById(int id) + => _store.TryGetValue(id, out var p) ? p : null; + } + + public record Person1(int Id, string Name, IReadOnlyList Friends); + + public record Person2(int Id, string Bio); +} diff --git a/src/HotChocolate/Fusion/test/Core.Tests/TestServerFactory.cs b/src/HotChocolate/Fusion/test/Core.Tests/TestServerFactory.cs new file mode 100644 index 00000000000..58ac13794f2 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/TestServerFactory.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Fusion; + +public sealed class TestServerFactory : IDisposable +{ + private readonly List _instances = new(); + + public TestServer Create( + Action configureServices, + Action configureApplication) + { + var builder = new WebHostBuilder() + .Configure(configureApplication) + .ConfigureServices(services => + { + services.AddHttpContextAccessor(); + configureServices.Invoke(services); + }); + + var server = new TestServer(builder); + _instances.Add(server); + return server; + } + + public void Dispose() + { + foreach (var testServer in _instances) + { + testServer.Dispose(); + } + } +} diff --git a/src/HotChocolate/Fusion/test/Core.Tests/UnitTest1.cs b/src/HotChocolate/Fusion/test/Core.Tests/UnitTest1.cs deleted file mode 100644 index 98d5655fa90..00000000000 --- a/src/HotChocolate/Fusion/test/Core.Tests/UnitTest1.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace test; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - - } -} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__resources__/dummy.graphql b/src/HotChocolate/Fusion/test/Core.Tests/__resources__/dummy.graphql new file mode 100644 index 00000000000..d648faa4dce --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__resources__/dummy.graphql @@ -0,0 +1,24 @@ +type Query { + personById(id: ID!): Person + @variable(name: "personId", argument: "id") + @fetch(from: "a", select: "personById(id: $personId) { ... Person }") + @fetch(from: "b", select: "personById(id: $personId) { ... Person }") +} + +type Person + @variable(name: "personId", select: "id", from: "a", type: "Int!") + @variable(name: "personId", select: "id", from: "b", type: "Int!") + @fetch(from: "a", select: "personById(id: $personId) { ... Person }") + @fetch(from: "b", select: "personById(id: $personId) { ... Person }") { + id: ID! @bind(to: "a") @bind(to: "b") @bind(to: "c") + name: String! @bind(to: "a") + bio: String @bind(to: "b") + + friends: [Person!] @bind(to: "a") +} + +schema + @httpClient(name: "a", baseAddress: "https://a/graphql") + @httpClient(name: "b", baseAddress: "https://b/graphql") { + query: Query +} diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Bio.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Bio.snap new file mode 100644 index 00000000000..76308701c73 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Bio.snap @@ -0,0 +1,21 @@ +User Request +--------------- +query GetPersonById { + personById(id: 1) { + id + bio + } +} +--------------- + +Request 1 +--------------- +query GetPersonById_1 { + node(id: 1) { + ... on Person { + id + bio + } + } +} +--------------- diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Bio_Friends_Bio.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Bio_Friends_Bio.snap new file mode 100644 index 00000000000..3ecde4c3961 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Bio_Friends_Bio.snap @@ -0,0 +1,44 @@ +User Request +--------------- +query GetPersonById { + personById(id: 1) { + bio + friends { + bio + } + } +} +--------------- + +Request 1 +--------------- +query GetPersonById_1 { + personById(id: 1) { + friends { + __fusion_exports__1: id + } + } +} +--------------- + +Request 2 +--------------- +query GetPersonById_2($__fusion_exports__1: ID!) { + node(id: $__fusion_exports__1) { + ... on Person { + bio + } + } +} +--------------- + +Request 3 +--------------- +query GetPersonById_3 { + node(id: 1) { + ... on Person { + bio + } + } +} +--------------- diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_And_Bio.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_And_Bio.snap new file mode 100644 index 00000000000..b6feca2de1c --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_And_Bio.snap @@ -0,0 +1,31 @@ +User Request +--------------- +query GetPersonById { + personById(id: 1) { + id + name + bio + } +} +--------------- + +Request 1 +--------------- +query GetPersonById_1 { + personById(id: 1) { + id + name + } +} +--------------- + +Request 2 +--------------- +query GetPersonById_2 { + node(id: 1) { + ... on Person { + bio + } + } +} +--------------- diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_Friends_Name_Bio.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_Friends_Name_Bio.snap new file mode 100644 index 00000000000..84e7108fc57 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_Friends_Name_Bio.snap @@ -0,0 +1,36 @@ +User Request +--------------- +query GetPersonById { + personById(id: 1) { + name + friends { + name + bio + } + } +} +--------------- + +Request 1 +--------------- +query GetPersonById_1 { + personById(id: 1) { + name + friends { + name + __fusion_exports__1: id + } + } +} +--------------- + +Request 2 +--------------- +query GetPersonById_2($__fusion_exports__1: ID!) { + node(id: $__fusion_exports__1) { + ... on Person { + bio + } + } +} +--------------- diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RemoteQueryExecutorTests.Do.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RemoteQueryExecutorTests.Do.snap new file mode 100644 index 00000000000..a9a8b481872 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RemoteQueryExecutorTests.Do.snap @@ -0,0 +1,59 @@ +User Request +--------------- +query GetPersonById { + personById(id: 4) { + name + friends { + name + bio + } + } +} +--------------- + +Request 1 +--------------- +query GetPersonById_1 { + personById(id: 4) { + name + friends { + name + __fusion_exports__1: id + } + } +} +--------------- + +Request 2 +--------------- +query GetPersonById_2($__fusion_exports__1: Int!) { + personById(id: $__fusion_exports__1) { + bio + } +} +--------------- + +Result +--------------- +{ + "data": { + "personById": { + "name": "Rafi", + "friends": [ + { + "name": "Pascal", + "bio": "Foo" + }, + { + "name": "Michael", + "bio": "Bar" + }, + { + "name": "Martin", + "bio": "Baz" + } + ] + } + } +} +--------------- diff --git a/src/HotChocolate/Fusion/test/Directory.Build.props b/src/HotChocolate/Fusion/test/Directory.Build.props index c99d170a8da..5d6ff988807 100644 --- a/src/HotChocolate/Fusion/test/Directory.Build.props +++ b/src/HotChocolate/Fusion/test/Directory.Build.props @@ -12,6 +12,10 @@ + + + + @@ -24,8 +28,6 @@ all - - diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Syntax.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Syntax.cs index b9628007e08..913e96a585d 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Syntax.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Syntax.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Diagnostics.CodeAnalysis; using static HotChocolate.Language.Properties.LangUtf8Resources; namespace HotChocolate.Language; @@ -12,7 +13,11 @@ public static class Syntax /// Parses a GraphQL object type definitions e.g. type Foo { bar: String } /// public static ObjectTypeDefinitionNode ParseObjectTypeDefinition( + #if NET7_0_OR_GREATER + [StringSyntax("graphql")] string sourceText) => + #else string sourceText) => + #endif Parse(sourceText, parser => parser.ParseObjectTypeDefinition()); /// @@ -33,7 +38,11 @@ public static class Syntax /// Parses a GraphQL object type definitions e.g. type Foo { bar: String } /// public static DirectiveDefinitionNode ParseDirectiveDefinition( +#if NET7_0_OR_GREATER + [StringSyntax("graphql")] string sourceText) => +#else string sourceText) => +#endif Parse(sourceText, parser => parser.ParseDirectiveDefinition()); /// @@ -54,7 +63,11 @@ public static class Syntax /// Parses a GraphQL field selection string e.g. field(arg: "abc") /// public static FieldDefinitionNode ParseFieldDefinition( +#if NET7_0_OR_GREATER + [StringSyntax("graphql")] string sourceText) => +#else string sourceText) => +#endif Parse(sourceText, parser => parser.ParseFieldDefinition()); /// @@ -75,7 +88,11 @@ public static class Syntax /// Parses a GraphQL field selection string e.g. field(arg: "abc") /// public static FieldNode ParseField( +#if NET7_0_OR_GREATER + [StringSyntax("graphql")] string sourceText) => +#else string sourceText) => +#endif Parse(sourceText, parser => parser.ParseField()); /// @@ -96,7 +113,11 @@ public static class Syntax /// Parses a GraphQL selection set string e.g. { field(arg: "abc") } /// public static SelectionSetNode ParseSelectionSet( +#if NET7_0_OR_GREATER + [StringSyntax("graphql")] string sourceText) => +#else string sourceText) => +#endif Parse(sourceText, parser => parser.ParseSelectionSet()); /// @@ -114,7 +135,11 @@ public static class Syntax new Utf8GraphQLParser(reader).ParseSelectionSet(); public static IValueNode ParseValueLiteral( +#if NET7_0_OR_GREATER + [StringSyntax("graphql")] string sourceText, +#else string sourceText, +#endif bool constant = true) => Parse(sourceText, parser => parser.ParseValueLiteral(constant)); @@ -129,7 +154,11 @@ public static class Syntax new Utf8GraphQLParser(reader).ParseValueLiteral(constant); public static ObjectValueNode ParseObjectLiteral( +#if NET7_0_OR_GREATER + [StringSyntax("graphql")] string sourceText, +#else string sourceText, +#endif bool constant = true) => Parse(sourceText, parser => parser.ParseObject(constant)); @@ -147,7 +176,11 @@ public static class Syntax /// Parses a GraphQL type reference e.g. [String!] /// public static ITypeNode ParseTypeReference( +#if NET7_0_OR_GREATER + [StringSyntax("graphql")] string sourceText) => +#else string sourceText) => +#endif Parse(sourceText, parser => parser.ParseTypeReference()); /// @@ -168,7 +201,11 @@ public static class Syntax /// Parses a GraphQL schema coordinate e.g. Query.userById(id:) /// public static SchemaCoordinateNode ParseSchemaCoordinate( +#if NET7_0_OR_GREATER + [StringSyntax("graphql")] string sourceText) => +#else string sourceText) => +#endif Parse(sourceText, parser => parser.ParseSingleSchemaCoordinate()); /// diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs index fbec433af62..b468287a766 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using static HotChocolate.Language.Properties.LangUtf8Resources; namespace HotChocolate.Language; @@ -150,7 +151,11 @@ private IDefinitionNode ParseDefinition() Parse(sourceText, ParserOptions.Default); public static unsafe DocumentNode Parse( +#if NET7_0_OR_GREATER + [StringSyntax("graphql")] string sourceText, +#else string sourceText, +#endif ParserOptions options) { if (string.IsNullOrEmpty(sourceText)) diff --git a/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/MongoDbFilterVisitorComparableTests.cs b/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/MongoDbFilterVisitorComparableTests.cs index 31e00e401e7..3b696576e26 100644 --- a/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/MongoDbFilterVisitorComparableTests.cs +++ b/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/MongoDbFilterVisitorComparableTests.cs @@ -497,7 +497,7 @@ public async Task Create_ShortIn_Expression() var res2 = await tester.ExecuteAsync( QueryRequestBuilder.New() - .SetQuery("{ root(where: { barShort: { in: [ null, 14 ]}}){ barShort}}") + .SetQuery("{ root(where: { barShort: { in: [ 13, 14 ]}}){ barShort}}") .Create()); var res3 = await tester.ExecuteAsync( @@ -528,7 +528,7 @@ public async Task Create_ShortNotIn_Expression() var res2 = await tester.ExecuteAsync( QueryRequestBuilder.New() - .SetQuery("{ root(where: { barShort: { nin: [ null, 14 ]}}){ barShort}}") + .SetQuery("{ root(where: { barShort: { nin: [ 13, 14 ]}}){ barShort}}") .Create()); var res3 = await tester.ExecuteAsync( diff --git a/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/MongoDbFilterVisitorObjectTests.cs b/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/MongoDbFilterVisitorObjectTests.cs index 06efbace8b9..e5e2d0aafea 100644 --- a/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/MongoDbFilterVisitorObjectTests.cs +++ b/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/MongoDbFilterVisitorObjectTests.cs @@ -170,7 +170,7 @@ public async Task Create_ObjectShortIn_Expression() var res2 = await tester.ExecuteAsync( QueryRequestBuilder.New() .SetQuery( - "{ root(where: { foo: { barShort: { in: [ null, 14 ]}}}) " + + "{ root(where: { foo: { barShort: { in: [ 13, 14 ]}}}) " + "{ foo{ barShort}}}") .Create()); diff --git a/src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/Comparable/Neo4JFilterComparableTests.cs b/src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/Comparable/Neo4JFilterComparableTests.cs index fcc0418b1c0..2ad006791d4 100644 --- a/src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/Comparable/Neo4JFilterComparableTests.cs +++ b/src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/Comparable/Neo4JFilterComparableTests.cs @@ -476,7 +476,7 @@ public async Task Create_ShortIn_Expression() .SetQuery(query1) .Create()); - const string query2 = "{ root(where: { barShort: { in: [ null, 14 ]}}){ barShort }}"; + const string query2 = "{ root(where: { barShort: { in: [ 13, 14 ]}}){ barShort }}"; var res2 = await tester.ExecuteAsync( QueryRequestBuilder.New() .SetQuery(query2) @@ -511,7 +511,7 @@ public async Task Create_ShortNotIn_Expression() .SetQuery(query1) .Create()); - const string query2 = "{ root(where: { barShort: { nin: [ null, 14 ]}}){ barShort }}"; + const string query2 = "{ root(where: { barShort: { nin: [ 13, 14 ]}}){ barShort }}"; var res2 = await tester.ExecuteAsync( QueryRequestBuilder.New() .SetQuery(query2) diff --git a/src/HotChocolate/Utilities/src/Utilities/HotChocolate.Utilities.csproj b/src/HotChocolate/Utilities/src/Utilities/HotChocolate.Utilities.csproj index 2d5cf84b153..8b4d8fb2e87 100644 --- a/src/HotChocolate/Utilities/src/Utilities/HotChocolate.Utilities.csproj +++ b/src/HotChocolate/Utilities/src/Utilities/HotChocolate.Utilities.csproj @@ -11,6 +11,7 @@ +