diff --git a/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.cs b/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.cs index 7287d8bbd6c..d92d0fb0b53 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.cs @@ -166,9 +166,13 @@ public OperationCompiler(InputParser parser) // more mutations on the compiled selection variants. // after we have executed all optimizers we will seal the selection variants. var context = new OperationOptimizerContext( + operationId, + document, + operationDefinition, schema, operationType, variants, + _includeConditions, _contextData); foreach (var item in _selectionVariants) diff --git a/src/HotChocolate/Core/src/Execution/Processing/OperationOptimizerContext.cs b/src/HotChocolate/Core/src/Execution/Processing/OperationOptimizerContext.cs index 73e11ef579a..b24c9f404b4 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/OperationOptimizerContext.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/OperationOptimizerContext.cs @@ -11,23 +11,50 @@ namespace HotChocolate.Execution.Processing; /// public readonly ref struct OperationOptimizerContext { - private readonly IReadOnlyList _variants; + private readonly SelectionVariants[] _variants; + private readonly IncludeCondition[] _includeConditions; + private readonly ObjectType _rootType; + private readonly Dictionary _contextData; /// /// Initializes a new instance of /// internal OperationOptimizerContext( + string id, + DocumentNode document, + OperationDefinitionNode definition, ISchema schema, - IObjectType rootType, + ObjectType rootType, SelectionVariants[] variants, + IncludeCondition[] includeConditions, Dictionary contextData) { - _variants = variants; + Id = id; + Document = document; + Definition = definition; Schema = schema; - RootType = rootType; - ContextData = contextData; + _rootType = rootType; + _variants = variants; + _includeConditions = includeConditions; + _contextData = contextData; } + /// + /// Gets the internal unique identifier for this operation. + /// + public string Id { get; } + + /// + /// Gets the parsed query document that contains the + /// operation-. + /// + public DocumentNode Document { get; } + + /// + /// Gets the syntax node representing the operation definition. + /// + public OperationDefinitionNode Definition { get; } + /// /// Gets the schema for which the query is compiled. /// @@ -36,7 +63,7 @@ namespace HotChocolate.Execution.Processing; /// /// Gets the root type on which the operation is executed. /// - public IObjectType RootType { get; } + public IObjectType RootType => _rootType; /// /// Gets the prepared root selections for this operation. @@ -52,7 +79,7 @@ namespace HotChocolate.Execution.Processing; /// The context data dictionary can be used by middleware components and /// resolvers to store and retrieve data during execution. /// - public IDictionary ContextData { get; } + public IDictionary ContextData => _contextData; /// /// Sets the resolvers on the specified . @@ -82,4 +109,17 @@ namespace HotChocolate.Execution.Processing; /// public FieldDelegate CompileResolverPipeline(IObjectField field, FieldNode selection) => OperationCompiler.CreateFieldMiddleware(Schema, field, selection); + + /// + /// Creates a temporary operation object for the optimizer. + /// + public IOperation CreateOperation() + => new Operation( + Id, + Document, + Definition, + _rootType, + _variants, + _includeConditions, + _contextData); } diff --git a/src/HotChocolate/Core/src/Subscriptions.Redis/RedisEventStream.cs b/src/HotChocolate/Core/src/Subscriptions.Redis/RedisEventStream.cs index 5da70c8df87..bd2c8cf9837 100644 --- a/src/HotChocolate/Core/src/Subscriptions.Redis/RedisEventStream.cs +++ b/src/HotChocolate/Core/src/Subscriptions.Redis/RedisEventStream.cs @@ -7,8 +7,7 @@ namespace HotChocolate.Subscriptions.Redis; -public class RedisEventStream - : ISourceStream +public class RedisEventStream : ISourceStream { private readonly ChannelMessageQueue _channel; private readonly IMessageSerializer _messageSerializer; @@ -66,8 +65,7 @@ public async ValueTask DisposeAsync() } } - private sealed class EnumerateMessages - : IAsyncEnumerable + private sealed class EnumerateMessages : IAsyncEnumerable { private readonly ChannelMessageQueue _channel; private readonly Func _messageSerializer; @@ -85,8 +83,7 @@ private sealed class EnumerateMessages { while (!cancellationToken.IsCancellationRequested) { - ChannelMessage message = await _channel.ReadAsync(cancellationToken) - .ConfigureAwait(false); + var message = await _channel.ReadAsync(cancellationToken).ConfigureAwait(false); string body = message.Message; if (body.Equals(RedisPubSub.Completed, StringComparison.Ordinal)) diff --git a/src/HotChocolate/Core/src/Subscriptions.Redis/RedisPubSub.cs b/src/HotChocolate/Core/src/Subscriptions.Redis/RedisPubSub.cs index 7d063cec346..f29e855f0a0 100644 --- a/src/HotChocolate/Core/src/Subscriptions.Redis/RedisPubSub.cs +++ b/src/HotChocolate/Core/src/Subscriptions.Redis/RedisPubSub.cs @@ -29,8 +29,8 @@ public RedisPubSub(IConnectionMultiplexer connection, IMessageSerializer message CancellationToken cancellationToken = default) where TTopic : notnull { - ISubscriber subscriber = _connection.GetSubscriber(); - var serializedTopic = topic is string s ? s : _messageSerializer.Serialize(topic); + var subscriber = _connection.GetSubscriber(); + var serializedTopic = topic as string ?? _messageSerializer.Serialize(topic); var serializedMessage = _messageSerializer.Serialize(message); await subscriber.PublishAsync(serializedTopic, serializedMessage).ConfigureAwait(false); } @@ -38,8 +38,8 @@ public RedisPubSub(IConnectionMultiplexer connection, IMessageSerializer message public async ValueTask CompleteAsync(TTopic topic) where TTopic : notnull { - ISubscriber subscriber = _connection.GetSubscriber(); - var serializedTopic = topic is string s ? s : _messageSerializer.Serialize(topic); + var subscriber = _connection.GetSubscriber(); + var serializedTopic = topic as string ?? _messageSerializer.Serialize(topic); await subscriber.PublishAsync(serializedTopic, Completed).ConfigureAwait(false); } @@ -48,10 +48,10 @@ public async ValueTask CompleteAsync(TTopic topic) CancellationToken cancellationToken = default) where TTopic : notnull { - ISubscriber subscriber = _connection.GetSubscriber(); - var serializedTopic = topic is string s ? s : _messageSerializer.Serialize(topic); + var subscriber = _connection.GetSubscriber(); + var serializedTopic = topic as string ?? _messageSerializer.Serialize(topic); - ChannelMessageQueue channel = await subscriber + var channel = await subscriber .SubscribeAsync(serializedTopic) .ConfigureAwait(false); diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/IOperation.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/IOperation.cs index 2b5eccd3699..b44190b5757 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/IOperation.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/IOperation.cs @@ -7,6 +7,9 @@ namespace HotChocolate.Execution.Processing; +/// +/// Represents a compiled GraphQL operation. +/// public interface IOperation : IHasReadOnlyContextData { /// diff --git a/src/HotChocolate/Fusion/src/Core/Clients/GraphQLHttpClient.cs b/src/HotChocolate/Fusion/src/Core/Clients/GraphQLHttpClient.cs index 762b6a64a6a..82104c87cdc 100644 --- a/src/HotChocolate/Fusion/src/Core/Clients/GraphQLHttpClient.cs +++ b/src/HotChocolate/Fusion/src/Core/Clients/GraphQLHttpClient.cs @@ -14,11 +14,13 @@ public sealed class GraphQLHttpClient : IGraphQLClient { private readonly IHttpClientFactory _httpClientFactory; private readonly JsonRequestFormatter _formatter = new(); + private readonly HttpClient _client; public GraphQLHttpClient(string schemaName, IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; SchemaName = schemaName; + _client =_httpClientFactory.CreateClient(SchemaName); } // TODO: naming? SubGraphName? @@ -28,10 +30,8 @@ public async Task ExecuteAsync(GraphQLRequest request, Cancella { // 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); + using var responseMessage = await _client.SendAsync(requestMessage, cancellationToken); responseMessage.EnsureSuccessStatusCode(); // TODO : remove for production await using var contentStream = await responseMessage.Content.ReadAsStreamAsync(cancellationToken); diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/RequestExecutorBuilderExtensions.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/RequestExecutorBuilderExtensions.cs index c76fcd90ac8..aea1e4c4c8c 100644 --- a/src/HotChocolate/Fusion/src/Core/DependencyInjection/RequestExecutorBuilderExtensions.cs +++ b/src/HotChocolate/Fusion/src/Core/DependencyInjection/RequestExecutorBuilderExtensions.cs @@ -61,10 +61,11 @@ public static class RequestExecutorBuilderExtensions .AddDocument(schemaDoc) .UseField(next => next) .UseDefaultGatewayPipeline() + .AddOperationCompilerOptimizer() .ConfigureSchemaServices( sc => { - foreach (var schemaName in configuration.Bindings) + foreach (var schemaName in configuration.SchemaNames) { sc.AddSingleton( sp => new GraphQLHttpClient( @@ -73,8 +74,8 @@ public static class RequestExecutorBuilderExtensions } sc.TryAddSingleton(configuration); - sc.TryAddSingleton(); - sc.TryAddSingleton(); + sc.TryAddSingleton(); + sc.TryAddSingleton(); sc.TryAddSingleton(); sc.TryAddSingleton(); sc.TryAddSingleton(); @@ -95,7 +96,6 @@ public static class RequestExecutorBuilderExtensions .UseOperationComplexityAnalyzer() .UseOperationResolver() .UseOperationVariableCoercion() - .UseRequest() .UseRequest(); } } diff --git a/src/HotChocolate/Fusion/src/Core/Execution/FederatedQueryContext.cs b/src/HotChocolate/Fusion/src/Core/Execution/FederatedQueryContext.cs index 8d5f6e7c245..8f815ac71aa 100644 --- a/src/HotChocolate/Fusion/src/Core/Execution/FederatedQueryContext.cs +++ b/src/HotChocolate/Fusion/src/Core/Execution/FederatedQueryContext.cs @@ -6,24 +6,22 @@ namespace HotChocolate.Fusion.Execution; internal sealed class FederatedQueryContext { public FederatedQueryContext( - ISchema schema, - ResultBuilder result, - IOperation operation, + OperationContext operationContext, QueryPlan plan, IReadOnlySet requiresFetch) { - Schema = schema; - Result = result; - Operation = operation; + OperationContext = operationContext; Plan = plan; RequiresFetch = requiresFetch; } - public ISchema Schema { get; } + public OperationContext OperationContext { get; } - public ResultBuilder Result { get; } + public ISchema Schema => OperationContext.Schema; - public IOperation Operation { get; } + public ResultBuilder Result => OperationContext.Result; + + public IOperation Operation => OperationContext.Operation; public QueryPlan Plan { get; } diff --git a/src/HotChocolate/Fusion/src/Core/Execution/FederatedQueryExecutor.cs b/src/HotChocolate/Fusion/src/Core/Execution/FederatedQueryExecutor.cs index 49bdacdd7c2..c5309a1fe24 100644 --- a/src/HotChocolate/Fusion/src/Core/Execution/FederatedQueryExecutor.cs +++ b/src/HotChocolate/Fusion/src/Core/Execution/FederatedQueryExecutor.cs @@ -1,14 +1,18 @@ +using System.Collections.Immutable; using System.Diagnostics; using System.Text.Json; using HotChocolate.Execution; using HotChocolate.Execution.Processing; +using HotChocolate.Execution.Processing.Tasks; using HotChocolate.Fusion.Clients; using HotChocolate.Fusion.Metadata; using HotChocolate.Language; using HotChocolate.Types; +using HotChocolate.Types.Introspection; +using HotChocolate.Utilities; +using static HotChocolate.Fusion.Utilities.JsonValueToGraphQLValueConverter; using IType = HotChocolate.Types.IType; using ObjectType = HotChocolate.Fusion.Metadata.ObjectType; -using static HotChocolate.Fusion.Utilities.JsonValueToGraphQLValueConverter; namespace HotChocolate.Fusion.Execution; @@ -31,11 +35,39 @@ internal sealed class FederatedQueryExecutor FederatedQueryContext context, CancellationToken cancellationToken = default) { + var scopedContext = ImmutableDictionary.Empty; var rootSelectionSet = context.Operation.RootSelectionSet; var rootResult = context.Result.RentObject(rootSelectionSet.Selections.Count); var rootWorkItem = new WorkItem(rootSelectionSet, rootResult); context.Fetch.Add(rootWorkItem); + // introspection + if (context.Plan.HasIntrospectionSelections) + { + var rootSelections = rootSelectionSet.Selections; + var operationContext = context.OperationContext; + + for (var i = 0; i < rootSelections.Count; i++) + { + var selection = rootSelections[i]; + if (selection.Field.IsIntrospectionField) + { + var resolverTask = operationContext.CreateResolverTask( + selection, + operationContext.RootValue, + rootResult, + i, + operationContext.PathFactory.Append(Path.Root, selection.ResponseName), + scopedContext); + resolverTask.BeginExecute(cancellationToken); + + // todo : this is just temporary + await resolverTask.WaitForCompletionAsync(cancellationToken); + } + } + } + + // federated stuff while (context.Fetch.Count > 0) { await FetchAsync(context, cancellationToken).ConfigureAwait(false); @@ -46,7 +78,8 @@ internal sealed class FederatedQueryExecutor } } - return QueryResultBuilder.New().SetData(rootResult).Create(); + context.Result.SetData(rootResult); + return context.Result.BuildResult(); } // note: this is inefficient and we want to group here, for now we just want to get it working. @@ -80,11 +113,17 @@ private async Task FetchAsync(FederatedQueryContext context, CancellationToken c { 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); + var response = await executor.ExecuteAsync(request, ct).ConfigureAwait(false); + var data = requestNode.Handler.UnwrapResult(response); ExtractSelectionResults(selections, request.SchemaName, data, selectionResults); ExtractVariables(data, exportKeys, variableValues); + + context.Result.RegisterForCleanup(() => + { + response.Dispose(); + return default; + }); } context.Compose.Enqueue(workItem); @@ -111,6 +150,14 @@ private async Task FetchAsync(FederatedQueryContext context, CancellationToken c for (var i = 0; i < selections.Count; i++) { var selection = selections[i]; + + if (selection.Field.IsIntrospectionField && + (selection.Field.Name.EqualsOrdinal(IntrospectionFields.Schema) || + selection.Field.Name.EqualsOrdinal(IntrospectionFields.Type))) + { + continue; + } + var selectionResult = selectionResults[i]; var nullable = selection.TypeKind is not TypeKind.NonNull; var namedType = selection.Type.NamedType(); diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNames.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNames.cs index 19d0be36614..77cc6b46b5d 100644 --- a/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNames.cs +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNames.cs @@ -1,20 +1,16 @@ -using static HotChocolate.Fusion.Metadata.ConfigurationDirectiveNames; - namespace HotChocolate.Fusion.Metadata; internal static class ConfigurationDirectiveNames { public const string VariableDirective = "variable"; public const string FetchDirective = "fetch"; - public const string BindDirective = "bind"; + public const string SourceDirective = "source"; public const string HttpDirective = "httpClient"; public const string FusionDirective = "fusion"; 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 SchemaArg = "schema"; public const string ArgumentArg = "argument"; public const string BaseAddressArg = "baseAddress"; } diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNamesContext.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNamesContext.cs index e7f137d89db..de1d5c1e748 100644 --- a/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNamesContext.cs +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNamesContext.cs @@ -10,27 +10,27 @@ internal class ConfigurationDirectiveNamesContext : ISyntaxVisitorContext private ConfigurationDirectiveNamesContext( string variableDirective, string fetchDirective, - string bindDirective, + string sourceDirective, string httpDirective, string fusionDirective) { VariableDirective = variableDirective; FetchDirective = fetchDirective; - BindDirective = bindDirective; + SourceDirective = sourceDirective; HttpDirective = httpDirective; FusionDirective = fusionDirective; } public string VariableDirective { get; } public string FetchDirective { get; } - public string BindDirective { get; } + public string SourceDirective { get; } public string HttpDirective { get; } public string FusionDirective { get; } public bool IsConfigurationDirective(string name) => VariableDirective.EqualsOrdinal(name) || FetchDirective.EqualsOrdinal(name) || - BindDirective.EqualsOrdinal(name) || + SourceDirective.EqualsOrdinal(name) || HttpDirective.EqualsOrdinal(name) || FusionDirective.EqualsOrdinal(name); @@ -43,7 +43,7 @@ public bool IsConfigurationDirective(string name) return new ConfigurationDirectiveNamesContext( $"{prefix}_{ConfigurationDirectiveNames.VariableDirective}", $"{prefix}_{ConfigurationDirectiveNames.FetchDirective}", - $"{prefix}_{ConfigurationDirectiveNames.BindDirective}", + $"{prefix}_{ConfigurationDirectiveNames.SourceDirective}", $"{prefix}_{ConfigurationDirectiveNames.HttpDirective}", prefixSelf ? $"{prefix}_{ConfigurationDirectiveNames.FusionDirective}" @@ -53,7 +53,7 @@ public bool IsConfigurationDirective(string name) return new ConfigurationDirectiveNamesContext( ConfigurationDirectiveNames.VariableDirective, ConfigurationDirectiveNames.FetchDirective, - ConfigurationDirectiveNames.BindDirective, + ConfigurationDirectiveNames.SourceDirective, ConfigurationDirectiveNames.HttpDirective, ConfigurationDirectiveNames.FusionDirective); } @@ -80,7 +80,7 @@ public static ConfigurationDirectiveNamesContext From(DocumentNode document) return new ConfigurationDirectiveNamesContext( $"{prefix}_{ConfigurationDirectiveNames.VariableDirective}", $"{prefix}_{ConfigurationDirectiveNames.FetchDirective}", - $"{prefix}_{ConfigurationDirectiveNames.BindDirective}", + $"{prefix}_{ConfigurationDirectiveNames.SourceDirective}", $"{prefix}_{ConfigurationDirectiveNames.HttpDirective}", prefixSelf ? $"{prefix}_{ConfigurationDirectiveNames.FusionDirective}" @@ -90,7 +90,7 @@ public static ConfigurationDirectiveNamesContext From(DocumentNode document) return new ConfigurationDirectiveNamesContext( ConfigurationDirectiveNames.VariableDirective, ConfigurationDirectiveNames.FetchDirective, - ConfigurationDirectiveNames.BindDirective, + ConfigurationDirectiveNames.SourceDirective, ConfigurationDirectiveNames.HttpDirective, ConfigurationDirectiveNames.FusionDirective); } diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/FetchDefinition.cs b/src/HotChocolate/Fusion/src/Core/Metadata/FetchDefinition.cs index bb1b0b87e9b..40beca4f6c0 100644 --- a/src/HotChocolate/Fusion/src/Core/Metadata/FetchDefinition.cs +++ b/src/HotChocolate/Fusion/src/Core/Metadata/FetchDefinition.cs @@ -1,6 +1,6 @@ -using System.ComponentModel.Design; using HotChocolate.Language; using HotChocolate.Language.Visitors; +using HotChocolate.Utilities; namespace HotChocolate.Fusion.Metadata; @@ -33,9 +33,10 @@ internal sealed class FetchDefinition public (ISelectionNode selectionNode, IReadOnlyList Path) CreateSelection( IReadOnlyDictionary variables, - SelectionSetNode? selectionSet) + SelectionSetNode? selectionSet, + string? responseName) { - var context = new FetchRewriterContext(Placeholder, variables, selectionSet); + var context = new FetchRewriterContext(Placeholder, variables, selectionSet, responseName); var selection = _rewriter.Rewrite(Select, context); if (Placeholder is null && selectionSet is not null) @@ -54,6 +55,24 @@ internal sealed class FetchDefinition private class FetchRewriter : SyntaxRewriter { + protected override FieldNode? RewriteField(FieldNode node, FetchRewriterContext context) + { + var result = base.RewriteField(node, context); + + if (result is not null && context.PlaceholderFound) + { + context.PlaceholderFound = false; + + if (context.ResponseName is not null && + !node.Name.Value.EqualsOrdinal(context.ResponseName)) + { + return result.WithAlias(new NameNode(context.ResponseName)); + } + } + + return result; + } + protected override SelectionSetNode? RewriteSelectionSet( SelectionSetNode node, FetchRewriterContext context) @@ -75,9 +94,16 @@ private class FetchRewriter : SyntaxRewriter } // preserve selection path, so we are later able to unwrap the result. - context.SelectionPath = context.Path.ToArray(); + var path = context.Path.ToArray(); + context.SelectionPath = path; + context.PlaceholderFound = true; rewrittenList = new List(); + if (context.ResponseName is not null) + { + path[^1] = context.ResponseName; + } + for (var j = 0; j < i; j++) { rewrittenList.Add(rewritten.Selections[j]); @@ -139,17 +165,23 @@ private sealed class FetchRewriterContext : ISyntaxVisitorContext public FetchRewriterContext( FragmentSpreadNode? placeholder, IReadOnlyDictionary variables, - SelectionSetNode? selectionSet) + SelectionSetNode? selectionSet, + string? responseName) { Placeholder = placeholder; Variables = variables; SelectionSet = selectionSet; + ResponseName = responseName; } + public string? ResponseName { get; } + public Stack Path { get; } = new(); public FragmentSpreadNode? Placeholder { get; } + public bool PlaceholderFound { get; set; } + public IReadOnlyDictionary Variables { get; } public SelectionSetNode? SelectionSet { get; } diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/IType.cs b/src/HotChocolate/Fusion/src/Core/Metadata/IType.cs index 36ebac2a88c..e6228609c6e 100644 --- a/src/HotChocolate/Fusion/src/Core/Metadata/IType.cs +++ b/src/HotChocolate/Fusion/src/Core/Metadata/IType.cs @@ -3,4 +3,6 @@ namespace HotChocolate.Fusion.Metadata; internal interface IType // TODO : should be called named type { string Name { get; } + + MemberBindingCollection Bindings { get; } } diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ObjectType.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ObjectType.cs index 6a8a982f72d..84d5c305bd3 100644 --- a/src/HotChocolate/Fusion/src/Core/Metadata/ObjectType.cs +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ObjectType.cs @@ -4,11 +4,13 @@ internal sealed class ObjectType : IType { public ObjectType( string name, + MemberBindingCollection bindings, VariableDefinitionCollection variables, FetchDefinitionCollection resolvers, ObjectFieldCollection fields) { Name = name; + Bindings = bindings; Variables = variables; Resolvers = resolvers; Fields = fields; @@ -16,6 +18,8 @@ internal sealed class ObjectType : IType public string Name { get; } + public MemberBindingCollection Bindings { get; } + public VariableDefinitionCollection Variables { get; } public FetchDefinitionCollection Resolvers { get; } diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfiguration.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfiguration.cs index 37ee01a344d..8530d7afa52 100644 --- a/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfiguration.cs +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfiguration.cs @@ -1,21 +1,37 @@ using HotChocolate.Language; +using HotChocolate.Utilities; namespace HotChocolate.Fusion.Metadata; internal sealed class ServiceConfiguration { - private readonly string[] _bindings; private readonly Dictionary _types; - private readonly Dictionary<(string, string), string> _typeNameLookup = new(); + private readonly Dictionary<(string Schema, string Type), string> _typeNameLookup = new(); - public ServiceConfiguration(IEnumerable bindings, IEnumerable types) + public ServiceConfiguration( + IReadOnlyList types, + IReadOnlyList httpClientConfigs) { - _bindings = bindings.ToArray(); _types = types.ToDictionary(t => t.Name, StringComparer.Ordinal); + HttpClientConfigs = httpClientConfigs; + SchemaNames = httpClientConfigs.Select(t => t.SchemaName).ToArray(); + + foreach (var type in types) + { + foreach (var binding in type.Bindings) + { + if (!binding.Name.EqualsOrdinal(type.Name)) + { + _typeNameLookup.Add((binding.SchemaName, binding.Name), type.Name); + } + } + } } // todo: Should be named SchemaNames or maybe SubGraphNames? - public IReadOnlyList Bindings => _bindings; + public IReadOnlyList SchemaNames { get; } + + public IReadOnlyList HttpClientConfigs { get; } public T GetType(string typeName) where T : IType { diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfigurationReader.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfigurationReader.cs index 76f5ee2b55d..575ff27724b 100644 --- a/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfigurationReader.cs +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfigurationReader.cs @@ -49,20 +49,18 @@ private ServiceConfiguration ReadServiceDefinition(DocumentNode documentNode) throw new Exception("No types"); } - - return new ServiceConfiguration( - httpClientConfigs.Select(t => t.SchemaName), - types); + return new ServiceConfiguration(types, httpClientConfigs); } private ObjectType ReadObjectType( ConfigurationDirectiveNamesContext context, ObjectTypeDefinitionNode typeDef) { + var bindings = ReadMemberBindings(context, typeDef.Directives, typeDef); var variables = ReadFieldVariableDefinitions(context, typeDef.Directives); var resolvers = ReadFetchDefinitions(context, typeDef.Directives); var fields = ReadObjectFields(context, typeDef.Fields); - return new ObjectType(typeDef.Name.Value, variables, resolvers, fields); + return new ObjectType(typeDef.Name.Value, bindings, variables, resolvers, fields); } private ObjectFieldCollection ReadObjectFields( @@ -105,7 +103,7 @@ private ServiceConfiguration ReadServiceDefinition(DocumentNode documentNode) DirectiveNode directiveNode) { AssertName(directiveNode, context.HttpDirective); - AssertArguments(directiveNode, NameArg, BaseAddressArg); + AssertArguments(directiveNode, SchemaArg, BaseAddressArg); string name = default!; string baseAddress = default!; @@ -114,7 +112,7 @@ private ServiceConfiguration ReadServiceDefinition(DocumentNode documentNode) { switch (argument.Name.Value) { - case NameArg: + case SchemaArg: name = Expect(argument.Value).Value; break; @@ -149,7 +147,7 @@ private ServiceConfiguration ReadServiceDefinition(DocumentNode documentNode) DirectiveNode directiveNode) { AssertName(directiveNode, context.VariableDirective); - AssertArguments(directiveNode, NameArg, SelectArg, TypeArg, FromArg); + AssertArguments(directiveNode, NameArg, SelectArg, TypeArg, SchemaArg); string name = default!; FieldNode select = default!; @@ -172,7 +170,7 @@ private ServiceConfiguration ReadServiceDefinition(DocumentNode documentNode) type = ParseTypeReference(Expect(argument.Value).Value); break; - case FromArg: + case SchemaArg: schemaName = Expect(argument.Value).Value; break; } @@ -203,7 +201,7 @@ private ServiceConfiguration ReadServiceDefinition(DocumentNode documentNode) DirectiveNode directiveNode) { AssertName(directiveNode, context.FetchDirective); - AssertArguments(directiveNode, SelectArg, FromArg); + AssertArguments(directiveNode, SelectArg, SchemaArg); ISelectionNode select = default!; string schemaName = default!; @@ -216,7 +214,7 @@ private ServiceConfiguration ReadServiceDefinition(DocumentNode documentNode) select = ParseField(Expect(argument.Value).Value); break; - case FromArg: + case SchemaArg: schemaName = Expect(argument.Value).Value; break; } @@ -254,6 +252,24 @@ private ServiceConfiguration ReadServiceDefinition(DocumentNode documentNode) : _assert.ToArray()); } + private MemberBindingCollection ReadMemberBindings( + ConfigurationDirectiveNamesContext context, + IReadOnlyList directiveNodes, + NamedSyntaxNode annotatedMember) + { + var definitions = new List(); + + foreach (var directiveNode in directiveNodes) + { + if (directiveNode.Name.Value.EqualsOrdinal(context.SourceDirective)) + { + definitions.Add(ReadMemberBinding(context, directiveNode, annotatedMember)); + } + } + + return new MemberBindingCollection(definitions); + } + private MemberBindingCollection ReadMemberBindings( ConfigurationDirectiveNamesContext context, IReadOnlyList directiveNodes, @@ -264,7 +280,7 @@ private ServiceConfiguration ReadServiceDefinition(DocumentNode documentNode) foreach (var directiveNode in directiveNodes) { - if (directiveNode.Name.Value.EqualsOrdinal(context.BindDirective)) + if (directiveNode.Name.Value.EqualsOrdinal(context.SourceDirective)) { definitions.Add(ReadMemberBinding(context, directiveNode, annotatedField)); } @@ -295,10 +311,10 @@ private ServiceConfiguration ReadServiceDefinition(DocumentNode documentNode) private MemberBinding ReadMemberBinding( ConfigurationDirectiveNamesContext context, DirectiveNode directiveNode, - FieldDefinitionNode annotatedField) + NamedSyntaxNode annotatedField) { - AssertName(directiveNode, context.BindDirective); - AssertArguments(directiveNode, ToArg, AsArg); + AssertName(directiveNode, context.SourceDirective); + AssertArguments(directiveNode, SchemaArg, NameArg); string? name = null; string schemaName = default!; @@ -307,11 +323,11 @@ private ServiceConfiguration ReadServiceDefinition(DocumentNode documentNode) { switch (argument.Name.Value) { - case AsArg: + case NameArg: name = Expect(argument.Value).Value; break; - case ToArg: + case SchemaArg: schemaName = Expect(argument.Value).Value; break; } @@ -394,10 +410,12 @@ private void AssertName(DirectiveNode directive, string expectedName) private void AssertArguments(DirectiveNode directive, params string[] expectedArguments) { - if (directive.Arguments.Count < 0) + if (directive.Arguments.Count == 0) { // TODO : EXCEPTION - throw new InvalidOperationException("INVALID ARGS"); + throw new InvalidOperationException( + $"The directive `{directive.Name.Value}` has required arguments " + + "but non were provided."); } _assert.Clear(); @@ -411,8 +429,12 @@ private void AssertArguments(DirectiveNode directive, params string[] expectedAr if (_assert.Count > 0) { - // TODO : EXCEPTION - throw new InvalidOperationException("INVALID ARGS"); + throw new InvalidOperationException( + $"Expected arguments for the directive `{directive.Name.Value}` are " + + $"`{string.Join(", ", expectedArguments)}`. " + + "The service configuration reader found the following arguments " + + $"`{string.Join(", ", _assert)}` on the directive in line number " + + $"{directive.Location!.Line}."); } } } diff --git a/src/HotChocolate/Fusion/src/Core/Pipeline/OperationExecutionMiddleware.cs b/src/HotChocolate/Fusion/src/Core/Pipeline/OperationExecutionMiddleware.cs index 5a0b5e933cc..2c9e336c2bd 100644 --- a/src/HotChocolate/Fusion/src/Core/Pipeline/OperationExecutionMiddleware.cs +++ b/src/HotChocolate/Fusion/src/Core/Pipeline/OperationExecutionMiddleware.cs @@ -1,7 +1,9 @@ using HotChocolate.Execution; using HotChocolate.Execution.Processing; +using HotChocolate.Fetching; using HotChocolate.Fusion.Execution; using HotChocolate.Fusion.Planning; +using Microsoft.Extensions.ObjectPool; namespace HotChocolate.Fusion.Pipeline; @@ -10,46 +12,84 @@ internal sealed class OperationExecutionMiddleware private readonly RequestDelegate _next; private readonly FederatedQueryExecutor _executor; private readonly ISchema _schema; + private readonly ObjectPool _operationContextPool; public OperationExecutionMiddleware( RequestDelegate next, + ObjectPool operationContextPool, [SchemaService] FederatedQueryExecutor executor, [SchemaService] ISchema schema) { _next = next ?? throw new ArgumentNullException(nameof(next)); + _operationContextPool = operationContextPool ?? + throw new ArgumentNullException(nameof(operationContextPool)); _executor = executor ?? throw new ArgumentNullException(nameof(executor)); - _schema = schema; + _schema = schema ?? + throw new ArgumentNullException(nameof(schema)); } - public async ValueTask InvokeAsync(IRequestContext context, ResultBuilder resultBuilder) + public async ValueTask InvokeAsync( + IRequestContext context, + IBatchDispatcher batchDispatcher) { if (context.Operation is not null && context.Variables is not null && - context.ContextData.TryGetValue(PipelineProperties.QueryPlan, out var value) && + context.Operation.ContextData.TryGetValue(PipelineProps.QueryPlan, out var value) && value is QueryPlan queryPlan) { - resultBuilder.Initialize( + var operationContext = _operationContextPool.Get(); + + operationContext.Initialize( + context, + context.Services, + batchDispatcher, context.Operation, - context.ErrorHandler, - context.DiagnosticEvents); + context.Variables, + new object(), // todo: we can use static representations for these + () => new object()); // todo: we can use static representations for these var federatedQueryContext = new FederatedQueryContext( - _schema, - resultBuilder, - context.Operation, + operationContext, queryPlan, new HashSet( queryPlan.ExecutionNodes .OfType() .Select(t => t.Handler.SelectionSet))); + // TODO : just for debug + if (context.ContextData.ContainsKey(WellKnownContextData.IncludeQueryPlan)) + { + var subGraphRequests = new OrderedDictionary(); + var plan = new OrderedDictionary(); + plan.Add("userRequest", context.Document?.ToString()); + plan.Add("subGraphRequests", subGraphRequests); + + var index = 0; + foreach (var executionNode in queryPlan.ExecutionNodes) + { + if (executionNode is RequestNode rn) + { + subGraphRequests.Add( + $"subGraphRequest{++index}", + rn.Handler.Document.ToString()); + } + } + + operationContext.Result.SetExtension("queryPlan", plan); + } + + // we store the context on the result for unit tests. + operationContext.Result.SetContextData("queryPlan", queryPlan); + context.Result = await _executor.ExecuteAsync( federatedQueryContext, context.RequestAborted) .ConfigureAwait(false); + _operationContextPool.Return(operationContext); + await _next(context).ConfigureAwait(false); } else diff --git a/src/HotChocolate/Fusion/src/Core/Pipeline/OperationQueryPlanCompiler.cs b/src/HotChocolate/Fusion/src/Core/Pipeline/OperationQueryPlanCompiler.cs new file mode 100644 index 00000000000..c6237c5fba4 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Pipeline/OperationQueryPlanCompiler.cs @@ -0,0 +1,31 @@ +using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Planning; + +namespace HotChocolate.Fusion.Pipeline; + +internal sealed class OperationQueryPlanCompiler : IOperationOptimizer +{ + private readonly RequestPlanner _requestPlanner; + private readonly RequirementsPlanner _requirementsPlanner; + private readonly ExecutionPlanBuilder _executionPlanBuilder; + + public OperationQueryPlanCompiler( + RequestPlanner requestPlanner, + RequirementsPlanner requirementsPlanner, + ExecutionPlanBuilder executionPlanBuilder) + { + _requestPlanner = requestPlanner; + _requirementsPlanner = requirementsPlanner; + _executionPlanBuilder = executionPlanBuilder; + } + + public void OptimizeOperation(OperationOptimizerContext context) + { + var temporaryOperation = context.CreateOperation(); + var queryPlanContext = new QueryPlanContext(temporaryOperation); + _requestPlanner.Plan(queryPlanContext); + _requirementsPlanner.Plan(queryPlanContext); + var queryPlan = _executionPlanBuilder.Build(queryPlanContext); + context.ContextData[PipelineProps.QueryPlan] = queryPlan; + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Pipeline/PipelineProperties.cs b/src/HotChocolate/Fusion/src/Core/Pipeline/PipelineProps.cs similarity index 75% rename from src/HotChocolate/Fusion/src/Core/Pipeline/PipelineProperties.cs rename to src/HotChocolate/Fusion/src/Core/Pipeline/PipelineProps.cs index ef227d7134c..fb3b3ceb314 100644 --- a/src/HotChocolate/Fusion/src/Core/Pipeline/PipelineProperties.cs +++ b/src/HotChocolate/Fusion/src/Core/Pipeline/PipelineProps.cs @@ -1,6 +1,6 @@ namespace HotChocolate.Fusion.Pipeline; -internal static class PipelineProperties +internal static class PipelineProps { public const string QueryPlan = "HotChocolate.Fusion.Pipeline.QueryPlan"; } diff --git a/src/HotChocolate/Fusion/src/Core/Pipeline/QueryPlanMiddleware.cs b/src/HotChocolate/Fusion/src/Core/Pipeline/QueryPlanMiddleware.cs deleted file mode 100644 index 0ee1d5e0ede..00000000000 --- a/src/HotChocolate/Fusion/src/Core/Pipeline/QueryPlanMiddleware.cs +++ /dev/null @@ -1,48 +0,0 @@ -using HotChocolate.Execution; -using HotChocolate.Fusion.Planning; - -namespace HotChocolate.Fusion.Pipeline; - -/// -/// Creates the query plan for the federated request. -/// -internal sealed class QueryPlanMiddleware -{ - private readonly RequestDelegate _next; - private readonly RequestPlaner _requestPlaner; - private readonly RequirementsPlaner _requirementsPlaner; - private readonly ExecutionPlanBuilder _executionPlanBuilder; - - public QueryPlanMiddleware( - RequestDelegate next, - [SchemaService] RequestPlaner requestPlaner, - [SchemaService] RequirementsPlaner requirementsPlaner, - [SchemaService] ExecutionPlanBuilder executionPlanBuilder) - { - _next = next ?? - throw new ArgumentNullException(nameof(next)); - _requestPlaner = requestPlaner ?? - throw new ArgumentNullException(nameof(requestPlaner)); - _requirementsPlaner = requirementsPlaner ?? - throw new ArgumentNullException(nameof(requirementsPlaner)); - _executionPlanBuilder = executionPlanBuilder ?? - throw new ArgumentNullException(nameof(executionPlanBuilder)); - } - - public async ValueTask InvokeAsync(IRequestContext context) - { - if (context.Operation is not null && context.Variables is not null) - { - var queryPlanContext = new QueryPlanContext(context.Operation); - _requestPlaner.Plan(queryPlanContext); - _requirementsPlaner.Plan(queryPlanContext); - var queryPlan = _executionPlanBuilder.Build(queryPlanContext); - context.ContextData[PipelineProperties.QueryPlan] = queryPlan; - await _next(context).ConfigureAwait(false); - } - else - { - context.Result = ErrorHelper.StateInvalidForOperationExecution(); - } - } -} diff --git a/src/HotChocolate/Fusion/src/Core/Planning/ExecutionPlanBuilder.cs b/src/HotChocolate/Fusion/src/Core/Planning/ExecutionPlanBuilder.cs index 3bda68c98ae..ca4bd2fda30 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/ExecutionPlanBuilder.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/ExecutionPlanBuilder.cs @@ -38,7 +38,10 @@ public QueryPlan Build(QueryPlanContext context) } } - return new QueryPlan(context.RequestNodes.Values, context.Exports.All); + return new QueryPlan( + context.RequestNodes.Values, + context.Exports.All, + context.HasIntrospectionSelections); } private RequestNode CreateRequestNode( @@ -84,7 +87,8 @@ public QueryPlan Build(QueryPlanContext context) var (rootResolver, p) = executionStep.Resolver.CreateSelection( context.VariableValues, - rootSelectionSetNode); + rootSelectionSetNode, + null); rootSelectionSetNode = new SelectionSetNode(new[] { rootResolver }); path = p; @@ -143,9 +147,10 @@ public QueryPlan Build(QueryPlanContext context) rootSelection.Resolver, executionStep.Variables); - var (s, p) = rootSelection.Resolver.CreateSelection( + var (s, _) = rootSelection.Resolver.CreateSelection( context.VariableValues, - selectionSetNode); + selectionSetNode, + rootSelection.Selection.ResponseName); selectionNode = s; } diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlan.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlan.cs index 62b9b20a842..5ac10595d03 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlan.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlan.cs @@ -9,16 +9,20 @@ internal sealed class QueryPlan public QueryPlan( IEnumerable executionNodes, - IEnumerable exportDefinitions) + IEnumerable exportDefinitions, + bool hasIntrospectionSelections) { ExecutionNodes = executionNodes.ToArray(); RootExecutionNodes = ExecutionNodes.Where(t => t.DependsOn.Count == 0).ToArray(); RequiresFetch = new HashSet(ExecutionNodes.OfType().Select(t => t.Handler.SelectionSet)); + HasIntrospectionSelections = hasIntrospectionSelections; _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 bool HasIntrospectionSelections { get; set; } + public IReadOnlyList RootExecutionNodes { get; } public IReadOnlyList ExecutionNodes { get; } diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlanContext.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlanContext.cs index 612a5d4c4d3..4260c5390ae 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlanContext.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlanContext.cs @@ -24,6 +24,8 @@ public QueryPlanContext(IOperation operation) public Dictionary RequestNodes { get; } = new(); + public bool HasIntrospectionSelections { get; set; } + public NameNode CreateRemoteOperationName() => new($"{_opName}_{++_opId}"); } diff --git a/src/HotChocolate/Fusion/src/Core/Planning/RequestPlaner.cs b/src/HotChocolate/Fusion/src/Core/Planning/RequestPlanner.cs similarity index 91% rename from src/HotChocolate/Fusion/src/Core/Planning/RequestPlaner.cs rename to src/HotChocolate/Fusion/src/Core/Planning/RequestPlanner.cs index f78a10fd5c1..18f2537aef8 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/RequestPlaner.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/RequestPlanner.cs @@ -1,6 +1,8 @@ using System.Diagnostics.CodeAnalysis; using HotChocolate.Execution.Processing; using HotChocolate.Fusion.Metadata; +using HotChocolate.Types.Introspection; +using HotChocolate.Utilities; namespace HotChocolate.Fusion.Planning; @@ -8,12 +10,12 @@ namespace HotChocolate.Fusion.Planning; /// The request planer will rewrite the into /// queries against the downstream services. /// -internal sealed class RequestPlaner +internal sealed class RequestPlanner { 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) + public RequestPlanner(ServiceConfiguration serviceConfig) { _serviceConfig = serviceConfig ?? throw new ArgumentNullException(nameof(serviceConfig)); } @@ -45,7 +47,6 @@ public void Plan(QueryPlanContext context) 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; @@ -62,6 +63,18 @@ public void Plan(QueryPlanContext context) foreach (var selection in current) { + if (selection.Field.IsIntrospectionField) + { + if (!context.HasIntrospectionSelections && + (selection.Field.Name.EqualsOrdinal(IntrospectionFields.Schema) || + selection.Field.Name.EqualsOrdinal(IntrospectionFields.Type))) + { + context.HasIntrospectionSelections = true; + } + + continue; + } + var field = selectionSetType.Fields[selection.Field.Name]; if (field.Bindings.TryGetValue(schemaName, out _)) { @@ -102,6 +115,12 @@ public void Plan(QueryPlanContext context) (leftovers ??= new()).Add(selection); } } + + if (workItem.RootSelections.Count > 0) + { + context.Steps.Add(workItem); + } + } while (leftovers is not null); } @@ -150,9 +169,9 @@ public void Plan(QueryPlanContext context) ObjectType typeContext) { var bestScore = 0; - var bestSchema = _serviceConfig.Bindings[0]; + var bestSchema = _serviceConfig.SchemaNames[0]; - foreach (var schemaName in _serviceConfig.Bindings) + foreach (var schemaName in _serviceConfig.SchemaNames) { var score = CalculateSchemaScore(operation, selections, typeContext, schemaName); @@ -176,7 +195,8 @@ public void Plan(QueryPlanContext context) foreach (var selection in selections) { - if (typeContext.Fields[selection.Field.Name].Bindings.ContainsSchema(schemaName)) + if (!selection.Field.IsIntrospectionField && + typeContext.Fields[selection.Field.Name].Bindings.ContainsSchema(schemaName)) { score++; @@ -199,7 +219,7 @@ public void Plan(QueryPlanContext context) return score; } - private bool TryGetResolver( + private static bool TryGetResolver( ObjectField field, string schemaName, HashSet variablesInContext, @@ -232,7 +252,7 @@ public void Plan(QueryPlanContext context) return false; } - private bool TryGetResolver( + private static bool TryGetResolver( ObjectType declaringType, string schemaName, HashSet variablesInContext, diff --git a/src/HotChocolate/Fusion/src/Core/Planning/RequirementsPlaner.cs b/src/HotChocolate/Fusion/src/Core/Planning/RequirementsPlanner.cs similarity index 99% rename from src/HotChocolate/Fusion/src/Core/Planning/RequirementsPlaner.cs rename to src/HotChocolate/Fusion/src/Core/Planning/RequirementsPlanner.cs index 0b163f95a94..5552e5826d2 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/RequirementsPlaner.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/RequirementsPlanner.cs @@ -8,7 +8,7 @@ namespace HotChocolate.Fusion.Planning; /// request to a downstream service and enrich these so that all requirements for each requests /// are fulfilled. /// -internal sealed class RequirementsPlaner +internal sealed class RequirementsPlanner { public void Plan(QueryPlanContext context) { diff --git a/src/HotChocolate/Fusion/test/Core.Tests/ExecutionPlanBuilderTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/ExecutionPlanBuilderTests.cs index 546e3f81347..0afb2f81578 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/ExecutionPlanBuilderTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/ExecutionPlanBuilderTests.cs @@ -1,6 +1,7 @@ using CookieCrumble; using HotChocolate.Execution; using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Metadata; using HotChocolate.Fusion.Planning; using HotChocolate.Language; using Microsoft.Extensions.DependencyInjection; @@ -29,30 +30,30 @@ public async Task GetPersonById_With_Name_And_Bio_With_Prefixed_Directives() type Query { personById(id: ID!): Person @abc_variable(name: ""personId"", argument: ""id"") - @abc_bind(to: ""a"") - @abc_fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"") - @abc_fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") + @abc_source(schema: ""a"") + @abc_fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @abc_fetch(schema: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") } type Person - @abc_variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"") - @abc_variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"") - @abc_fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"") - @abc_fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") { + @abc_variable(name: ""personId"", select: ""id"" schema: ""b"" type: ""ID!"") + @abc_variable(name: ""personId"", select: ""id"" schema: ""b"" type: ""ID!"") + @abc_fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @abc_fetch(schema: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") { id: ID! - @abc_bind(to: ""a"") - @abc_bind(to: ""b"") + @abc_source(schema: ""a"") + @abc_source(schema: ""b"") name: String! - @abc_bind(to: ""a"") + @abc_source(schema: ""a"") bio: String - @abc_bind(to: ""b"") + @abc_source(schema: ""b"") } schema @fusion(prefix: ""abc"") - @abc_httpClient(name: ""a"" baseAddress: ""https://a/graphql"") - @abc_httpClient(name: ""b"" baseAddress: ""https://b/graphql"") { + @abc_httpClient(schema: ""a"" baseAddress: ""https://a/graphql"") + @abc_httpClient(schema: ""b"" baseAddress: ""https://b/graphql"") { query: Query }"; @@ -62,7 +63,7 @@ type Person .UseField(n => n) .BuildSchemaAsync(); - var serviceConfig = Metadata.ServiceConfiguration.Load(serviceDefinition); + var serviceConfig = ServiceConfiguration.Load(serviceDefinition); var request = Parse( @@ -84,8 +85,8 @@ type Person // act var queryPlanContext = new QueryPlanContext(operation); - var requestPlaner = new RequestPlaner(serviceConfig); - var requirementsPlaner = new RequirementsPlaner(); + var requestPlaner = new RequestPlanner(serviceConfig); + var requirementsPlaner = new RequirementsPlanner(); var executionPlanBuilder = new ExecutionPlanBuilder(serviceConfig, schema); requestPlaner.Plan(queryPlanContext); @@ -127,30 +128,30 @@ public async Task GetPersonById_With_Name_And_Bio_With_Prefixed_Directives_Prefi type Query { personById(id: ID!): Person @abc_variable(name: ""personId"", argument: ""id"") - @abc_bind(to: ""a"") - @abc_fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"") - @abc_fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") + @abc_source(schema: ""a"") + @abc_fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @abc_fetch(schema: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") } type Person - @abc_variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"") - @abc_variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"") - @abc_fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"") - @abc_fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") { + @abc_variable(name: ""personId"", select: ""id"" schema: ""b"" type: ""ID!"") + @abc_variable(name: ""personId"", select: ""id"" schema: ""b"" type: ""ID!"") + @abc_fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @abc_fetch(schema: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") { id: ID! - @abc_bind(to: ""a"") - @abc_bind(to: ""b"") + @abc_source(schema: ""a"") + @abc_source(schema: ""b"") name: String! - @abc_bind(to: ""a"") + @abc_source(schema: ""a"") bio: String - @abc_bind(to: ""b"") + @abc_source(schema: ""b"") } schema @abc_fusion(prefix: ""abc"", prefixSelf: true) - @abc_httpClient(name: ""a"" baseAddress: ""https://a/graphql"") - @abc_httpClient(name: ""b"" baseAddress: ""https://b/graphql"") { + @abc_httpClient(schema: ""a"" baseAddress: ""https://a/graphql"") + @abc_httpClient(schema: ""b"" baseAddress: ""https://b/graphql"") { query: Query }"; @@ -160,7 +161,7 @@ type Person .UseField(n => n) .BuildSchemaAsync(); - var serviceConfig = Metadata.ServiceConfiguration.Load(serviceDefinition); + var serviceConfig = ServiceConfiguration.Load(serviceDefinition); var request = Parse( @@ -182,8 +183,8 @@ type Person // act var queryPlanContext = new QueryPlanContext(operation); - var requestPlaner = new RequestPlaner(serviceConfig); - var requirementsPlaner = new RequirementsPlaner(); + var requestPlaner = new RequestPlanner(serviceConfig); + var requirementsPlaner = new RequirementsPlanner(); var executionPlanBuilder = new ExecutionPlanBuilder(serviceConfig, schema); requestPlaner.Plan(queryPlanContext); @@ -225,29 +226,29 @@ public async Task GetPersonById_With_Name_And_Bio() 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 } }"") + @source(schema: ""a"") + @fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(schema: ""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 } }"") { + @variable(name: ""personId"", select: ""id"" schema: ""b"" type: ""ID!"") + @variable(name: ""personId"", select: ""id"" schema: ""b"" type: ""ID!"") + @fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(schema: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") { id: ID! - @bind(to: ""a"") - @bind(to: ""b"") + @source(schema: ""a"") + @source(schema: ""b"") name: String! - @bind(to: ""a"") + @source(schema: ""a"") bio: String - @bind(to: ""b"") + @source(schema: ""b"") } schema - @httpClient(name: ""a"" baseAddress: ""https://a/graphql"") - @httpClient(name: ""b"" baseAddress: ""https://b/graphql"") { + @httpClient(schema: ""a"" baseAddress: ""https://a/graphql"") + @httpClient(schema: ""b"" baseAddress: ""https://b/graphql"") { query: Query }"; @@ -257,7 +258,7 @@ type Person .UseField(n => n) .BuildSchemaAsync(); - var serviceConfig = Metadata.ServiceConfiguration.Load(serviceDefinition); + var serviceConfig = ServiceConfiguration.Load(serviceDefinition); var request = Parse( @@ -279,8 +280,105 @@ type Person // act var queryPlanContext = new QueryPlanContext(operation); - var requestPlaner = new RequestPlaner(serviceConfig); - var requirementsPlaner = new RequirementsPlaner(); + var requestPlaner = new RequestPlanner(serviceConfig); + var requirementsPlaner = new RequirementsPlanner(); + 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 Use_Alias_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"") + @source(schema: ""a"") + @fetch(schema: ""a"", select: ""personByIdFoo(id: $personId) { ... Person }"") + @fetch(schema: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") + } + + type Person + @variable(name: ""personId"", select: ""id"" schema: ""b"" type: ""ID!"") + @variable(name: ""personId"", select: ""id"" schema: ""b"" type: ""ID!"") + @fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(schema: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") { + + id: ID! + @source(schema: ""a"") + @source(schema: ""b"") + name: String! + @source(schema: ""a"") + bio: String + @source(schema: ""b"") + } + + schema + @httpClient(schema: ""a"" baseAddress: ""https://a/graphql"") + @httpClient(schema: ""b"" baseAddress: ""https://b/graphql"") { + query: Query + }"; + + var schema = await new ServiceCollection() + .AddGraphQL() + .AddDocumentFromString(sdl) + .UseField(n => n) + .BuildSchemaAsync(); + + var serviceConfig = 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 RequestPlanner(serviceConfig); + var requirementsPlaner = new RequirementsPlanner(); var executionPlanBuilder = new ExecutionPlanBuilder(serviceConfig, schema); requestPlaner.Plan(queryPlanContext); @@ -322,28 +420,28 @@ public async Task GetPersonById_With_Bio() 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 } }"") + @fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(schema: ""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 } }"") { + @variable(name: ""personId"", select: ""id"" schema: ""b"" type: ""ID!"") + @variable(name: ""personId"", select: ""id"" schema: ""b"" type: ""ID!"") + @fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(schema: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") { id: ID! - @bind(to: ""a"") - @bind(to: ""b"") + @source(schema: ""a"") + @source(schema: ""b"") name: String! - @bind(to: ""a"") + @source(schema: ""a"") bio: String - @bind(to: ""b"") + @source(schema: ""b"") } schema - @httpClient(name: ""a"" baseAddress: ""https://a/graphql"") - @httpClient(name: ""b"" baseAddress: ""https://b/graphql"") { + @httpClient(schema: ""a"" baseAddress: ""https://a/graphql"") + @httpClient(schema: ""b"" baseAddress: ""https://b/graphql"") { query: Query }"; @@ -353,7 +451,7 @@ type Person .UseField(n => n) .BuildSchemaAsync(); - var serviceConfig = Metadata.ServiceConfiguration.Load(serviceDefinition); + var serviceConfig = ServiceConfiguration.Load(serviceDefinition); var request = Parse( @@ -374,8 +472,8 @@ type Person // act var queryPlanContext = new QueryPlanContext(operation); - var requestPlaner = new RequestPlaner(serviceConfig); - var requirementsPlaner = new RequirementsPlaner(); + var requestPlaner = new RequestPlanner(serviceConfig); + var requirementsPlaner = new RequirementsPlanner(); var executionPlanBuilder = new ExecutionPlanBuilder(serviceConfig, schema); requestPlaner.Plan(queryPlanContext); @@ -418,31 +516,31 @@ public async Task GetPersonById_With_Bio_Friends_Bio() 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 } }"") + @fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(schema: ""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 } }"") { + @variable(name: ""personId"", select: ""id"" schema: ""a"" type: ""ID!"") + @variable(name: ""personId"", select: ""id"" schema: ""b"" type: ""ID!"") + @fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(schema: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") { id: ID! - @bind(to: ""a"") - @bind(to: ""b"") - @bind(to: ""c"") + @source(schema: ""a"") + @source(schema: ""b"") + @source(schema: ""c"") name: String! - @bind(to: ""a"") + @source(schema: ""a"") bio: String - @bind(to: ""b"") + @source(schema: ""b"") friends: [Person!] - @bind(to: ""a"") + @source(schema: ""a"") } schema - @httpClient(name: ""a"" baseAddress: ""https://a/graphql"") - @httpClient(name: ""b"" baseAddress: ""https://b/graphql"") { + @httpClient(schema: ""a"" baseAddress: ""https://a/graphql"") + @httpClient(schema: ""b"" baseAddress: ""https://b/graphql"") { query: Query }"; @@ -452,7 +550,7 @@ type Person .UseField(n => n) .BuildSchemaAsync(); - var serviceConfig = Metadata.ServiceConfiguration.Load(serviceDefinition); + var serviceConfig = ServiceConfiguration.Load(serviceDefinition); var request = Parse( @@ -475,8 +573,8 @@ type Person // act var queryPlanContext = new QueryPlanContext(operation); - var requestPlaner = new RequestPlaner(serviceConfig); - var requirementsPlaner = new RequirementsPlaner(); + var requestPlaner = new RequestPlanner(serviceConfig); + var requirementsPlaner = new RequirementsPlanner(); var executionPlanBuilder = new ExecutionPlanBuilder(serviceConfig, schema); requestPlaner.Plan(queryPlanContext); @@ -519,31 +617,31 @@ public async Task GetPersonById_With_Name_Friends_Name_Bio() 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 } }"") + @fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(schema: ""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 } }"") { + @variable(name: ""personId"", select: ""id"" schema: ""a"" type: ""ID!"") + @variable(name: ""personId"", select: ""id"" schema: ""b"" type: ""ID!"") + @fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(schema: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") { id: ID! - @bind(to: ""a"") - @bind(to: ""b"") - @bind(to: ""c"") + @source(schema: ""a"") + @source(schema: ""b"") + @source(schema: ""c"") name: String! - @bind(to: ""a"") + @source(schema: ""a"") bio: String - @bind(to: ""b"") + @source(schema: ""b"") friends: [Person!] - @bind(to: ""a"") + @source(schema: ""a"") } schema - @httpClient(name: ""a"" baseAddress: ""https://a/graphql"") - @httpClient(name: ""b"" baseAddress: ""https://b/graphql"") { + @httpClient(schema: ""a"" baseAddress: ""https://a/graphql"") + @httpClient(schema: ""b"" baseAddress: ""https://b/graphql"") { query: Query }"; @@ -553,7 +651,7 @@ type Person .UseField(n => n) .BuildSchemaAsync(); - var serviceConfig = Metadata.ServiceConfiguration.Load(serviceDefinition); + var serviceConfig = ServiceConfiguration.Load(serviceDefinition); var request = Parse( @@ -577,8 +675,8 @@ type Person // act var queryPlanContext = new QueryPlanContext(operation); - var requestPlaner = new RequestPlaner(serviceConfig); - var requirementsPlaner = new RequirementsPlaner(); + var requestPlaner = new RequestPlanner(serviceConfig); + var requirementsPlaner = new RequirementsPlanner(); var executionPlanBuilder = new ExecutionPlanBuilder(serviceConfig, schema); requestPlaner.Plan(queryPlanContext); @@ -600,4 +698,106 @@ type Person await snapshot.MatchAsync(); } + + [Fact] + public async Task StoreService_Me_Name_Reviews_Upc() + { + // arrange + var request = Parse( + @"query Me { + me { + name + reviews { + product { + upc + } + } + } + }"); + + // act + var queryPlan = await BuildStoreServiceQueryPlanAsync(request); + + // 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 StoreService_Introspection() + { + // arrange + var request = Parse( + @"query Intro { + __schema { + types { + name + } + } + }"); + + // act + var queryPlan = await BuildStoreServiceQueryPlanAsync(request); + + // 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(); + } + + private static async Task BuildStoreServiceQueryPlanAsync(DocumentNode request) + { + // arrange + var serviceConfigDoc = Parse(FileResource.Open("StoreServiceConfig.graphql")!); + var serviceConfig = ServiceConfiguration.Load(serviceConfigDoc); + var context = ConfigurationDirectiveNamesContext.From(serviceConfigDoc); + var rewriter = new ServiceConfigurationToSchemaRewriter(); + var rewritten = rewriter.Rewrite(serviceConfigDoc, context); + var sdl = rewritten!.ToString(); + + var schema = await new ServiceCollection() + .AddGraphQL() + .AddDocumentFromString(sdl) + .UseField(n => n) + .BuildSchemaAsync(); + + 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 RequestPlanner(serviceConfig); + var requirementsPlaner = new RequirementsPlanner(); + var executionPlanBuilder = new ExecutionPlanBuilder(serviceConfig, schema); + + requestPlaner.Plan(queryPlanContext); + requirementsPlaner.Plan(queryPlanContext); + return executionPlanBuilder.Build(queryPlanContext); + } } 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 c9081e76296..ae67e204dc4 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/HotChocolate.Fusion.Tests.csproj +++ b/src/HotChocolate/Fusion/test/Core.Tests/HotChocolate.Fusion.Tests.csproj @@ -14,4 +14,13 @@ + + + Always + + + Always + + + diff --git a/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ConfigurationDirectiveNamesContextTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ConfigurationDirectiveNamesContextTests.cs index f4f7094dd31..02ede1969b2 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ConfigurationDirectiveNamesContextTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ConfigurationDirectiveNamesContextTests.cs @@ -19,7 +19,7 @@ public void NewContext_DefaultDirectiveNames() @"{ ""VariableDirective"": ""variable"", ""FetchDirective"": ""fetch"", - ""BindDirective"": ""bind"", + ""SourceDirective"": ""source"", ""HttpDirective"": ""httpClient"", ""FusionDirective"": ""fusion"" }"); @@ -39,7 +39,7 @@ public void NewContext_DirectiveNames_With_Prefix() @"{ ""VariableDirective"": ""def_variable"", ""FetchDirective"": ""def_fetch"", - ""BindDirective"": ""def_bind"", + ""SourceDirective"": ""def_source"", ""HttpDirective"": ""def_httpClient"", ""FusionDirective"": ""fusion"" }"); @@ -59,7 +59,7 @@ public void NewContext_DirectiveNames_With_Prefix_PrefixSelf() @"{ ""VariableDirective"": ""def_variable"", ""FetchDirective"": ""def_fetch"", - ""BindDirective"": ""def_bind"", + ""SourceDirective"": ""def_source"", ""HttpDirective"": ""def_httpClient"", ""FusionDirective"": ""def_fusion"" }"); @@ -82,7 +82,7 @@ public void From_Document_No_Fusion_Directive() @"{ ""VariableDirective"": ""variable"", ""FetchDirective"": ""fetch"", - ""BindDirective"": ""bind"", + ""SourceDirective"": ""source"", ""HttpDirective"": ""httpClient"", ""FusionDirective"": ""fusion"" }"); @@ -105,7 +105,7 @@ public void From_Document_With_Fusion_Directive_No_Prefix() @"{ ""VariableDirective"": ""variable"", ""FetchDirective"": ""fetch"", - ""BindDirective"": ""bind"", + ""SourceDirective"": ""source"", ""HttpDirective"": ""httpClient"", ""FusionDirective"": ""fusion"" }"); @@ -128,7 +128,7 @@ public void From_Document_With_Fusion_Directive_With_Prefix() @"{ ""VariableDirective"": ""abc_variable"", ""FetchDirective"": ""abc_fetch"", - ""BindDirective"": ""abc_bind"", + ""SourceDirective"": ""abc_source"", ""HttpDirective"": ""abc_httpClient"", ""FusionDirective"": ""fusion"" }"); @@ -152,7 +152,7 @@ public void From_Document_With_Fusion_Directive_With_Prefix_PrefixSelf() @"{ ""VariableDirective"": ""abc_variable"", ""FetchDirective"": ""abc_fetch"", - ""BindDirective"": ""abc_bind"", + ""SourceDirective"": ""abc_source"", ""HttpDirective"": ""abc_httpClient"", ""FusionDirective"": ""abc_fusion"" }"); diff --git a/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ServiceConfigurationToSchemaRewriterTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ServiceConfigurationToSchemaRewriterTests.cs index dafe47a2d26..3fa3592efb2 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ServiceConfigurationToSchemaRewriterTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ServiceConfigurationToSchemaRewriterTests.cs @@ -13,30 +13,30 @@ public void Remove_Configuration_Directives() type Query { personById(id: ID!): Person @abc_variable(name: ""personId"", argument: ""id"") - @abc_bind(to: ""a"") - @abc_fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"") - @abc_fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") + @abc_source(schema: ""a"") + @abc_fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @abc_fetch(schema: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") } type Person - @abc_variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"") - @abc_variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"") - @abc_fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"") - @abc_fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") { + @abc_variable(name: ""personId"", select: ""id"" schema: ""b"" type: ""ID!"") + @abc_variable(name: ""personId"", select: ""id"" schema: ""b"" type: ""ID!"") + @abc_fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @abc_fetch(schema: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") { id: ID! - @abc_bind(to: ""a"") - @abc_bind(to: ""b"") + @abc_source(schema: ""a"") + @abc_source(schema: ""b"") name: String! - @abc_bind(to: ""a"") + @abc_source(schema: ""a"") bio: String - @abc_bind(to: ""b"") + @abc_source(schema: ""b"") } schema @fusion(prefix: ""abc"") - @abc_httpClient(name: ""a"" baseAddress: ""https://a/graphql"") - @abc_httpClient(name: ""b"" baseAddress: ""https://b/graphql"") { + @abc_httpClient(schema: ""a"" baseAddress: ""https://a/graphql"") + @abc_httpClient(schema: ""b"" baseAddress: ""https://b/graphql"") { query: Query }"; diff --git a/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs index 8fbb3486310..b3d487425eb 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs @@ -68,32 +68,32 @@ public async Task Do() 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 }"") + @fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(schema: ""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 }"") { + @variable(name: ""personId"", select: ""id"" schema: ""a"" type: ""Int!"") + @variable(name: ""personId"", select: ""id"" schema: ""b"" type: ""Int!"") + @fetch(schema: ""a"", select: ""personById(id: $personId) { ... Person }"") + @fetch(schema: ""b"", select: ""personById(id: $personId) { ... Person }"") { id: ID! - @bind(to: ""a"") - @bind(to: ""b"") - @bind(to: ""c"") + @source(schema: ""a"") + @source(schema: ""b"") + @source(schema: ""c"") name: String! - @bind(to: ""a"") + @source(schema: ""a"") bio: String - @bind(to: ""b"") + @source(schema: ""b"") friends: [Person!] - @bind(to: ""a"") + @source(schema: ""a"") } schema - @httpClient(name: ""a"" baseAddress: ""https://a/graphql"") - @httpClient(name: ""b"" baseAddress: ""https://b/graphql"") { + @httpClient(schema: ""a"" baseAddress: ""https://a/graphql"") + @httpClient(schema: ""b"" baseAddress: ""https://b/graphql"") { query: Query }"; @@ -127,15 +127,18 @@ type Person snapshot.Add(request, "User Request"); - /* - foreach (var executionNode in queryPlan.ExecutionNodes) + if (result.ContextData is not null && + result.ContextData.TryGetValue("queryPlan", out var value) && + value is QueryPlan queryPlan) { - if (executionNode is RequestNode rn) + foreach (var executionNode in queryPlan.ExecutionNodes) { - snapshot.Add(rn.Handler.Document, $"Request {++index}"); + if (executionNode is RequestNode rn) + { + snapshot.Add(rn.Handler.Document, $"Request {++index}"); + } } } - */ snapshot.Add(formatter.Format((QueryResult)result), "Result"); diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__resources__/StoreServiceConfig.graphql b/src/HotChocolate/Fusion/test/Core.Tests/__resources__/StoreServiceConfig.graphql new file mode 100644 index 00000000000..a8ea063149e --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__resources__/StoreServiceConfig.graphql @@ -0,0 +1,56 @@ +type Query { + me: User! + @fetch(select: "user(id: 1) { ... User }", schema: "accounts") + @fetch(select: "authorById(id: 1) { ... User }", schema: "reviews") + @source(schema: "accounts") + @source(schema: "reviews") +} + +type User + @source(schema: "reviews", name: "Author") + @variable(name: "userId", select: "id", schema: "accounts", type: "Int!") + @variable(name: "userId", select: "id", schema: "reviews", type: "Int!") + @fetch(select: "user(id: $userId) { ... User }", schema: "accounts") + @fetch(select: "authorById(id: $userId) { ... User }", schema: "reviews") { + id: Int! + @source(schema: "accounts") + @source(schema: "reviews") + name: String! + @source(schema: "accounts") + birthdate: DateTime! + @source(schema: "accounts") + username: String! + @source(schema: "accounts") + @source(schema: "reviews") + reviews: [Review] + @source(schema: "reviews") +} + +type Review + @variable(name: "reviewId", select: "id", schema: "reviews", type: "Int!") + @fetch(select: "reviewById(id: $reviewId) { ... Review }", schema: "reviews") { + id: Int! + @source(schema: "reviews") + body: String! + @source(schema: "reviews") + author: User! + @source(schema: "reviews") + product: Product! + @source(schema: "reviews") +} + +type Product + @variable(name: "productId", select: "upc", schema: "reviews", type: "Int!") + @fetch(select: "productById(upc: $productId) { ... Product }", schema: "reviews") { + upc: Int! + @source(schema: "reviews") + reviews: [Review!]! + @source(schema: "reviews") +} + + +schema + @httpClient(schema: "accounts", baseAddress: "http://localhost:5051") + @httpClient(schema: "reviews", baseAddress: "http://localhost:5054") { + 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 index 76308701c73..d9f619c35d1 100644 --- 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 @@ -11,7 +11,7 @@ query GetPersonById { Request 1 --------------- query GetPersonById_1 { - node(id: 1) { + personById: node(id: 1) { ... on Person { id bio diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.StoreService_Introspection.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.StoreService_Introspection.snap new file mode 100644 index 00000000000..8bd0904df85 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.StoreService_Introspection.snap @@ -0,0 +1,7 @@ +query Intro { + __schema { + types { + name + } + } +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.StoreService_Me_Name_Reviews_Upc.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.StoreService_Me_Name_Reviews_Upc.snap new file mode 100644 index 00000000000..84ced272b66 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.StoreService_Me_Name_Reviews_Upc.snap @@ -0,0 +1,36 @@ +User Request +--------------- +query Me { + me { + name + reviews { + product { + upc + } + } + } +} +--------------- + +Request 1 +--------------- +query Me_1 { + me: authorById(id: 1) { + reviews { + product { + upc + } + } + __fusion_exports__1: id + } +} +--------------- + +Request 2 +--------------- +query Me_2($__fusion_exports__1: Int!) { + user(id: $__fusion_exports__1) { + name + } +} +--------------- diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.Use_Alias_GetPersonById_With_Name_And_Bio.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.Use_Alias_GetPersonById_With_Name_And_Bio.snap new file mode 100644 index 00000000000..b0e2b4d940c --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.Use_Alias_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: personByIdFoo(id: 1) { + id + name + } +} +--------------- + +Request 2 +--------------- +query GetPersonById_2 { + node(id: 1) { + ... on Person { + bio + } + } +} +---------------