diff --git a/global.json b/global.json index 4d051c4aaca..5c393dc1ff6 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "7.0.100-preview.6.22352.1", + "version": "7.0.100-preview.7.22377.5", "rollForward": "latestMinor" } } \ No newline at end of file diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResultSerializer.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResultSerializer.cs index a0c3157b893..f9394335bb5 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResultSerializer.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResultSerializer.cs @@ -9,7 +9,7 @@ namespace HotChocolate.AspNetCore.Serialization; public class DefaultHttpResultSerializer : IHttpResultSerializer { - private readonly JsonQueryResultFormatter _jsonFormatter; + private readonly IQueryResultFormatter _jsonFormatter; private readonly string _deferContentType; private readonly IResponseStreamFormatter _deferFormatter; @@ -69,6 +69,41 @@ public class DefaultHttpResultSerializer : IHttpResultSerializer } } + /// + /// Creates a new instance of . + /// + protected DefaultHttpResultSerializer( + IQueryResultFormatter jsonFormatter, + HttpResultSerialization batchSerialization = HttpResultSerialization.MultiPartChunked, + HttpResultSerialization deferSerialization = HttpResultSerialization.MultiPartChunked) + { + _jsonFormatter = jsonFormatter; + var jsonArrayFormatter = new JsonArrayResponseStreamFormatter(_jsonFormatter); + var multiPartFormatter = new MultiPartResponseStreamFormatter(_jsonFormatter); + + if (deferSerialization is HttpResultSerialization.JsonArray) + { + _deferContentType = ContentType.Json; + _deferFormatter = jsonArrayFormatter; + } + else + { + _deferContentType = ContentType.MultiPart; + _deferFormatter = multiPartFormatter; + } + + if (batchSerialization is HttpResultSerialization.JsonArray) + { + _batchContentType = ContentType.Json; + _batchFormatter = jsonArrayFormatter; + } + else + { + _batchContentType = ContentType.MultiPart; + _batchFormatter = multiPartFormatter; + } + } + public virtual string GetContentType(IExecutionResult result) { if (result is null) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs index 95a3a55ac47..0ea238c63d6 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs @@ -225,7 +225,7 @@ public ApolloSubscriptionProtocolHandler(ISocketSessionInterceptor interceptor) jsonWriter.WriteString(Id, operationSessionId); jsonWriter.WriteString(MessageProperties.Type, Utf8Messages.Data); jsonWriter.WritePropertyName(Payload); - _formatter.Serialize(result, jsonWriter); + _formatter.Format(result, jsonWriter); jsonWriter.WriteEndObject(); await jsonWriter.FlushAsync(cancellationToken); await session.Connection.SendAsync(arrayWriter.Body, cancellationToken); @@ -243,7 +243,7 @@ public ApolloSubscriptionProtocolHandler(ISocketSessionInterceptor interceptor) jsonWriter.WriteString(Id, operationSessionId); jsonWriter.WriteString(MessageProperties.Type, Utf8Messages.Error); jsonWriter.WritePropertyName(Payload); - _formatter.Serialize(errors[0], jsonWriter); + _formatter.Format(errors[0], jsonWriter); jsonWriter.WriteEndObject(); await jsonWriter.FlushAsync(cancellationToken); await session.Connection.SendAsync(arrayWriter.Body, cancellationToken); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs index 66d6aebb9de..3a4c4309d05 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs @@ -204,7 +204,7 @@ public GraphQLOverWebSocketProtocolHandler(ISocketSessionInterceptor interceptor jsonWriter.WriteString(Id, operationSessionId); jsonWriter.WriteString(MessageProperties.Type, Utf8Messages.Next); jsonWriter.WritePropertyName(Payload); - _formatter.Serialize(result, jsonWriter); + _formatter.Format(result, jsonWriter); jsonWriter.WriteEndObject(); await jsonWriter.FlushAsync(cancellationToken); await session.Connection.SendAsync(arrayWriter.Body, cancellationToken); @@ -222,7 +222,7 @@ public GraphQLOverWebSocketProtocolHandler(ISocketSessionInterceptor interceptor jsonWriter.WriteString(Id, operationSessionId); jsonWriter.WriteString(MessageProperties.Type, Utf8Messages.Error); jsonWriter.WritePropertyName(Payload); - _formatter.Serialize(errors, jsonWriter); + _formatter.Format(errors, jsonWriter); jsonWriter.WriteEndObject(); await jsonWriter.FlushAsync(cancellationToken); await session.Connection.SendAsync(arrayWriter.Body, cancellationToken); diff --git a/src/HotChocolate/AspNetCore/test/Directory.Build.props b/src/HotChocolate/AspNetCore/test/Directory.Build.props index 92438c4b95d..f1f3b7fc9ce 100644 --- a/src/HotChocolate/AspNetCore/test/Directory.Build.props +++ b/src/HotChocolate/AspNetCore/test/Directory.Build.props @@ -21,7 +21,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/HotChocolate/Core/src/Execution/Extensions/ExecutionResultExtensions.cs b/src/HotChocolate/Core/src/Execution/Extensions/ExecutionResultExtensions.cs index d6ca876b3a1..06fc56ef400 100644 --- a/src/HotChocolate/Core/src/Execution/Extensions/ExecutionResultExtensions.cs +++ b/src/HotChocolate/Core/src/Execution/Extensions/ExecutionResultExtensions.cs @@ -49,8 +49,8 @@ public static class ExecutionResultExtensions if (result is IQueryResult queryResult) { return withIndentations - ? _formatterIndented.Serialize(queryResult) - : _formatter.Serialize(queryResult); + ? _formatterIndented.Format(queryResult) + : _formatter.Format(queryResult); } throw new NotSupportedException(ExecutionResultExtensions_OnlyQueryResults); diff --git a/src/HotChocolate/Core/src/Execution/Serialization/JsonArrayResponseStreamFormatter.cs b/src/HotChocolate/Core/src/Execution/Serialization/JsonArrayResponseStreamFormatter.cs index b2a073263a4..c3562ebee59 100644 --- a/src/HotChocolate/Core/src/Execution/Serialization/JsonArrayResponseStreamFormatter.cs +++ b/src/HotChocolate/Core/src/Execution/Serialization/JsonArrayResponseStreamFormatter.cs @@ -12,7 +12,7 @@ public sealed class JsonArrayResponseStreamFormatter : IResponseStreamFormatter private const byte _leftBracket = (byte)'['; private const byte _rightBracket = (byte)']'; private const byte _comma = (byte)','; - private readonly JsonQueryResultFormatter _formatter; + private readonly IQueryResultFormatter _formatter; /// /// Creates a new instance of . @@ -44,7 +44,7 @@ public sealed class JsonArrayResponseStreamFormatter : IResponseStreamFormatter /// is null. /// public JsonArrayResponseStreamFormatter( - JsonQueryResultFormatter formatter) + IQueryResultFormatter formatter) { _formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); diff --git a/src/HotChocolate/Core/src/Execution/Serialization/JsonQueryResultFormatter.cs b/src/HotChocolate/Core/src/Execution/Serialization/JsonQueryResultFormatter.cs index 90ff09a24a6..35f15acbc95 100644 --- a/src/HotChocolate/Core/src/Execution/Serialization/JsonQueryResultFormatter.cs +++ b/src/HotChocolate/Core/src/Execution/Serialization/JsonQueryResultFormatter.cs @@ -37,7 +37,7 @@ public JsonQueryResultFormatter(bool indented = false, JavaScriptEncoder? encode _options = new JsonWriterOptions { Indented = indented, Encoder = encoder }; } - public unsafe string Serialize(IQueryResult result) + public unsafe string Format(IQueryResult result) { if (result is null) { @@ -54,7 +54,7 @@ public unsafe string Serialize(IQueryResult result) } } - public void Serialize(IQueryResult result, Utf8JsonWriter writer) + public void Format(IQueryResult result, Utf8JsonWriter writer) { if (result is null) { @@ -69,7 +69,7 @@ public void Serialize(IQueryResult result, Utf8JsonWriter writer) WriteResult(writer, result); } - public void Serialize(IError error, Utf8JsonWriter writer) + public void Format(IError error, Utf8JsonWriter writer) { if (error is null) { @@ -84,7 +84,7 @@ public void Serialize(IError error, Utf8JsonWriter writer) WriteError(writer, error); } - public void Serialize(IReadOnlyList errors, Utf8JsonWriter writer) + public void Format(IReadOnlyList errors, Utf8JsonWriter writer) { if (errors is null) { @@ -359,7 +359,7 @@ private static void WritePathValue(Utf8JsonWriter writer, Path path) ref var searchSpace = ref objectResult.GetReference(); - for(var i = 0; i < objectResult.Capacity; i++) + for (var i = 0; i < objectResult.Capacity; i++) { var field = Unsafe.Add(ref searchSpace, i); if (field.IsInitialized) @@ -403,6 +403,76 @@ private static void WritePathValue(Utf8JsonWriter writer, Path path) writer.WriteEndArray(); } +#if NET5_0_OR_GREATER + private void WriteJsonElement( + Utf8JsonWriter writer, + JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + WriteJsonObject(writer, element); + break; + + case JsonValueKind.Array: + WriteJsonArray(writer, element); + break; + + case JsonValueKind.String: + writer.WriteStringValue(element.GetString()); + break; + + case JsonValueKind.Number: + writer.WriteRawValue(element.GetRawText()); + break; + + case JsonValueKind.True: + writer.WriteBooleanValue(true); + break; + + case JsonValueKind.False: + writer.WriteBooleanValue(false); + break; + + case JsonValueKind.Null: + writer.WriteNullValue(); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + private void WriteJsonObject( + Utf8JsonWriter writer, + JsonElement element) + { + writer.WriteStartObject(); + + foreach (var item in element.EnumerateObject()) + { + writer.WritePropertyName(item.Name); + WriteJsonElement(writer, item.Value); + } + + writer.WriteEndObject(); + } + + private void WriteJsonArray( + Utf8JsonWriter writer, + JsonElement element) + { + writer.WriteStartArray(); + + foreach (var item in element.EnumerateArray()) + { + WriteJsonElement(writer, item); + } + + writer.WriteEndArray(); + } + +#endif private void WriteFieldValue( Utf8JsonWriter writer, object? value) @@ -423,6 +493,11 @@ private static void WritePathValue(Utf8JsonWriter writer, Path path) WriteListResult(writer, resultMapList); break; +#if NET5_0_OR_GREATER + case JsonElement element: + WriteJsonElement(writer, element); + break; +#endif case Dictionary dict: WriteDictionary(writer, dict); break; diff --git a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj index 53080602b2b..d8643115285 100644 --- a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj +++ b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj @@ -38,7 +38,7 @@ - + diff --git a/src/HotChocolate/Data/src/EntityFramework/HotChocolate.Data.EntityFramework.csproj b/src/HotChocolate/Data/src/EntityFramework/HotChocolate.Data.EntityFramework.csproj index 6054899ad5f..d94e9c735cf 100644 --- a/src/HotChocolate/Data/src/EntityFramework/HotChocolate.Data.EntityFramework.csproj +++ b/src/HotChocolate/Data/src/EntityFramework/HotChocolate.Data.EntityFramework.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/HotChocolate/Data/test/Data.AutoMapper.Tests/HotChocolate.Data.AutoMapper.Tests.csproj b/src/HotChocolate/Data/test/Data.AutoMapper.Tests/HotChocolate.Data.AutoMapper.Tests.csproj index da562dc4853..e7298086e1b 100644 --- a/src/HotChocolate/Data/test/Data.AutoMapper.Tests/HotChocolate.Data.AutoMapper.Tests.csproj +++ b/src/HotChocolate/Data/test/Data.AutoMapper.Tests/HotChocolate.Data.AutoMapper.Tests.csproj @@ -22,10 +22,10 @@ - - - - + + + + diff --git a/src/HotChocolate/Data/test/Data.EntityFramework.Tests/HotChocolate.Data.EntityFramework.Tests.csproj b/src/HotChocolate/Data/test/Data.EntityFramework.Tests/HotChocolate.Data.EntityFramework.Tests.csproj index ce2c36a17c9..d0d5fcf20e5 100644 --- a/src/HotChocolate/Data/test/Data.EntityFramework.Tests/HotChocolate.Data.EntityFramework.Tests.csproj +++ b/src/HotChocolate/Data/test/Data.EntityFramework.Tests/HotChocolate.Data.EntityFramework.Tests.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/HotChocolate.Data.Filters.SqlServer.Tests.csproj b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/HotChocolate.Data.Filters.SqlServer.Tests.csproj index ba112b9c049..3444f7bbba1 100644 --- a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/HotChocolate.Data.Filters.SqlServer.Tests.csproj +++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/HotChocolate.Data.Filters.SqlServer.Tests.csproj @@ -16,9 +16,9 @@ - - - + + + diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.SqlServer.Tests.csproj b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.SqlServer.Tests.csproj index aacd0c98719..d08666fb6b7 100644 --- a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.SqlServer.Tests.csproj +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.SqlServer.Tests.csproj @@ -17,10 +17,10 @@ - - - - + + + + diff --git a/src/HotChocolate/Data/test/Data.Sorting.SqlLite.Tests/HotChocolate.Data.Sorting.SqlLite.Tests.csproj b/src/HotChocolate/Data/test/Data.Sorting.SqlLite.Tests/HotChocolate.Data.Sorting.SqlLite.Tests.csproj index bad1c7f13ab..f1b29a6e14f 100644 --- a/src/HotChocolate/Data/test/Data.Sorting.SqlLite.Tests/HotChocolate.Data.Sorting.SqlLite.Tests.csproj +++ b/src/HotChocolate/Data/test/Data.Sorting.SqlLite.Tests/HotChocolate.Data.Sorting.SqlLite.Tests.csproj @@ -16,9 +16,9 @@ - - - + + + diff --git a/src/HotChocolate/Fusion/src/Core/Clients/GraphQLHttpClient.cs b/src/HotChocolate/Fusion/src/Core/Clients/GraphQLHttpClient.cs index 25f8a373632..762b6a64a6a 100644 --- a/src/HotChocolate/Fusion/src/Core/Clients/GraphQLHttpClient.cs +++ b/src/HotChocolate/Fusion/src/Core/Clients/GraphQLHttpClient.cs @@ -7,6 +7,9 @@ namespace HotChocolate.Fusion.Clients; +// note: should the GraphQL client handle the capabilities? +// meaning the execution engine should just use batching and +// all and the client decides to batch if batching is available? public sealed class GraphQLHttpClient : IGraphQLClient { private readonly IHttpClientFactory _httpClientFactory; @@ -18,6 +21,7 @@ public GraphQLHttpClient(string schemaName, IHttpClientFactory httpClientFactory SchemaName = schemaName; } + // TODO: naming? SubGraphName? public string SchemaName { get; } public async Task ExecuteAsync(GraphQLRequest request, CancellationToken cancellationToken) diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/RequestExecutorBuilderExtensions.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/RequestExecutorBuilderExtensions.cs index 8358d3edcbb..c76fcd90ac8 100644 --- a/src/HotChocolate/Fusion/src/Core/DependencyInjection/RequestExecutorBuilderExtensions.cs +++ b/src/HotChocolate/Fusion/src/Core/DependencyInjection/RequestExecutorBuilderExtensions.cs @@ -4,6 +4,7 @@ using HotChocolate.Fusion.Metadata; using HotChocolate.Fusion.Pipeline; using HotChocolate.Fusion.Planning; +using HotChocolate.Language; using Microsoft.Extensions.DependencyInjection.Extensions; // ReSharper disable once CheckNamespace @@ -11,15 +12,53 @@ namespace Microsoft.Extensions.DependencyInjection; public static class RequestExecutorBuilderExtensions { - public static IRequestExecutorBuilder AddGraphQLGateway( - this IRequestExecutorBuilder builder, - string serviceConfig, - string sdl) + public static IRequestExecutorBuilder AddFusionGatewayServer( + this IServiceCollection services, + string serviceConfiguration) { - var configuration = ServiceConfiguration.Load(serviceConfig); + if (services is null) + { + throw new ArgumentNullException(nameof(services)); + } - return builder - .AddDocumentFromString(sdl) + if (string.IsNullOrEmpty(serviceConfiguration)) + { + throw new ArgumentNullException(nameof(serviceConfiguration)); + } + + var serviceConfDoc = Utf8GraphQLParser.Parse(serviceConfiguration); + return AddFusionGatewayServer(services, serviceConfDoc); + } + + public static IRequestExecutorBuilder AddFusionGatewayServer( + this IServiceCollection services, + DocumentNode serviceConfiguration) + { + if (services is null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (serviceConfiguration is null) + { + throw new ArgumentNullException(nameof(serviceConfiguration)); + } + + var configuration = ServiceConfiguration.Load(serviceConfiguration); + var context = ConfigurationDirectiveNamesContext.From(serviceConfiguration); + var rewriter = new ServiceConfigurationToSchemaRewriter(); + var schemaDoc = (DocumentNode?)rewriter.Rewrite(serviceConfiguration, context); + + if (schemaDoc is null) + { + // todo : exception. + throw new InvalidOperationException( + "A valid service configuration must always produce a schema document."); + } + + return services + .AddGraphQLServer() + .AddDocument(schemaDoc) .UseField(next => next) .UseDefaultGatewayPipeline() .ConfigureSchemaServices( @@ -42,7 +81,7 @@ public static class RequestExecutorBuilderExtensions }); } - public static IRequestExecutorBuilder UseDefaultGatewayPipeline( + private static IRequestExecutorBuilder UseDefaultGatewayPipeline( this IRequestExecutorBuilder builder) { return builder diff --git a/src/HotChocolate/Fusion/src/Core/HotChocolate.Fusion.csproj b/src/HotChocolate/Fusion/src/Core/HotChocolate.Fusion.csproj index 0db03610dd6..e0250468a41 100644 --- a/src/HotChocolate/Fusion/src/Core/HotChocolate.Fusion.csproj +++ b/src/HotChocolate/Fusion/src/Core/HotChocolate.Fusion.csproj @@ -8,12 +8,19 @@ + - - + + + + + + + + diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNames.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNames.cs new file mode 100644 index 00000000000..19d0be36614 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNames.cs @@ -0,0 +1,20 @@ +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 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 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 new file mode 100644 index 00000000000..e7f137d89db --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNamesContext.cs @@ -0,0 +1,151 @@ +using System.Diagnostics.CodeAnalysis; +using HotChocolate.Language; +using HotChocolate.Language.Visitors; +using HotChocolate.Utilities; + +namespace HotChocolate.Fusion.Metadata; + +internal class ConfigurationDirectiveNamesContext : ISyntaxVisitorContext +{ + private ConfigurationDirectiveNamesContext( + string variableDirective, + string fetchDirective, + string bindDirective, + string httpDirective, + string fusionDirective) + { + VariableDirective = variableDirective; + FetchDirective = fetchDirective; + BindDirective = bindDirective; + HttpDirective = httpDirective; + FusionDirective = fusionDirective; + } + + public string VariableDirective { get; } + public string FetchDirective { get; } + public string BindDirective { get; } + public string HttpDirective { get; } + public string FusionDirective { get; } + + public bool IsConfigurationDirective(string name) + => VariableDirective.EqualsOrdinal(name) || + FetchDirective.EqualsOrdinal(name) || + BindDirective.EqualsOrdinal(name) || + HttpDirective.EqualsOrdinal(name) || + FusionDirective.EqualsOrdinal(name); + + public static ConfigurationDirectiveNamesContext Create( + string? prefix = null, + bool prefixSelf = false) + { + if (prefix is not null) + { + return new ConfigurationDirectiveNamesContext( + $"{prefix}_{ConfigurationDirectiveNames.VariableDirective}", + $"{prefix}_{ConfigurationDirectiveNames.FetchDirective}", + $"{prefix}_{ConfigurationDirectiveNames.BindDirective}", + $"{prefix}_{ConfigurationDirectiveNames.HttpDirective}", + prefixSelf + ? $"{prefix}_{ConfigurationDirectiveNames.FusionDirective}" + : ConfigurationDirectiveNames.FusionDirective); + } + + return new ConfigurationDirectiveNamesContext( + ConfigurationDirectiveNames.VariableDirective, + ConfigurationDirectiveNames.FetchDirective, + ConfigurationDirectiveNames.BindDirective, + ConfigurationDirectiveNames.HttpDirective, + ConfigurationDirectiveNames.FusionDirective); + } + + public static ConfigurationDirectiveNamesContext From(DocumentNode document) + { + if (document is null) + { + throw new ArgumentNullException(nameof(document)); + } + + var schemaDef = document.Definitions.OfType().FirstOrDefault(); + + if (schemaDef is null) + { + // todo : exception + throw new ArgumentException( + "The provided document must at least contain a schema definition.", + nameof(document)); + } + + if (TryGetPrefix(schemaDef.Directives, out var prefixSelf, out var prefix)) + { + return new ConfigurationDirectiveNamesContext( + $"{prefix}_{ConfigurationDirectiveNames.VariableDirective}", + $"{prefix}_{ConfigurationDirectiveNames.FetchDirective}", + $"{prefix}_{ConfigurationDirectiveNames.BindDirective}", + $"{prefix}_{ConfigurationDirectiveNames.HttpDirective}", + prefixSelf + ? $"{prefix}_{ConfigurationDirectiveNames.FusionDirective}" + : ConfigurationDirectiveNames.FusionDirective); + } + + return new ConfigurationDirectiveNamesContext( + ConfigurationDirectiveNames.VariableDirective, + ConfigurationDirectiveNames.FetchDirective, + ConfigurationDirectiveNames.BindDirective, + ConfigurationDirectiveNames.HttpDirective, + ConfigurationDirectiveNames.FusionDirective); + } + + private static bool TryGetPrefix( + IReadOnlyList schemaDirectives, + out bool prefixSelf, + [NotNullWhen(true)] out string? prefix) + { + const string prefixedFusionDir = "_" + ConfigurationDirectiveNames.FusionDirective; + + foreach (var directive in schemaDirectives) + { + if (directive.Name.Value.EndsWith(prefixedFusionDir, StringComparison.Ordinal)) + { + var prefixSelfArg = + directive.Arguments.FirstOrDefault( + t => t.Name.Value.EqualsOrdinal("prefixSelf")); + + if (prefixSelfArg?.Value is BooleanValueNode { Value: true }) + { + var prefixArg = + directive.Arguments.FirstOrDefault( + t => t.Name.Value.EqualsOrdinal("prefix")); + + if (prefixArg?.Value is StringValueNode prefixVal && + directive.Name.Value.EqualsOrdinal($"{prefixVal.Value}{prefixedFusionDir}")) + { + prefixSelf = true; + prefix = prefixVal.Value; + return true; + } + } + } + } + + foreach (var directive in schemaDirectives) + { + if (directive.Name.Value.EqualsOrdinal(ConfigurationDirectiveNames.FusionDirective)) + { + var prefixArg = + directive.Arguments.FirstOrDefault( + t => t.Name.Value.EqualsOrdinal("prefix")); + + if (prefixArg?.Value is StringValueNode prefixVal) + { + prefixSelf = false; + prefix = prefixVal.Value; + return true; + } + } + } + + prefixSelf = false; + prefix = null; + return false; + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfiguration.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfiguration.cs index 8dfe0ae3664..37ee01a344d 100644 --- a/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfiguration.cs +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfiguration.cs @@ -1,8 +1,4 @@ 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; @@ -18,6 +14,7 @@ public ServiceConfiguration(IEnumerable bindings, IEnumerable typ _types = types.ToDictionary(t => t.Name, StringComparer.Ordinal); } + // todo: Should be named SchemaNames or maybe SubGraphNames? public IReadOnlyList Bindings => _bindings; public T GetType(string typeName) where T : IType @@ -66,410 +63,10 @@ public string GetTypeName(TypeInfo typeInfo) } 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(); + => new ServiceConfigurationReader().Read(sourceText); - 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 static ServiceConfiguration Load(DocumentNode document) + => new ServiceConfigurationReader().Read(document); } public readonly struct TypeInfo diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfigurationReader.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfigurationReader.cs new file mode 100644 index 00000000000..76f5ee2b55d --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfigurationReader.cs @@ -0,0 +1,418 @@ +using HotChocolate.Language; +using HotChocolate.Language.Visitors; +using HotChocolate.Utilities; +using static HotChocolate.Fusion.Metadata.ConfigurationDirectiveNames; +using static HotChocolate.Language.Utf8GraphQLParser.Syntax; + +namespace HotChocolate.Fusion.Metadata; + +internal sealed class ServiceConfigurationReader +{ + private readonly HashSet _assert = new(); + + public ServiceConfiguration Read(string sourceText) + => ReadServiceDefinition(Utf8GraphQLParser.Parse(sourceText)); + + public ServiceConfiguration Read(DocumentNode document) + => ReadServiceDefinition(document); + + private ServiceConfiguration ReadServiceDefinition(DocumentNode documentNode) + { + var context = ConfigurationDirectiveNamesContext.From(documentNode); + + var types = new List(); + IReadOnlyList? httpClientConfigs = null; + + foreach (var definition in documentNode.Definitions) + { + switch (definition) + { + case ObjectTypeDefinitionNode node: + types.Add(ReadObjectType(context, node)); + break; + + case SchemaDefinitionNode node: + httpClientConfigs = ReadHttpClientConfigs(context, 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( + ConfigurationDirectiveNamesContext context, + ObjectTypeDefinitionNode 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); + } + + private ObjectFieldCollection ReadObjectFields( + ConfigurationDirectiveNamesContext context, + IReadOnlyList fieldDefinitionNodes) + { + var collection = new List(); + + foreach (var fieldDef in fieldDefinitionNodes) + { + var resolvers = ReadFetchDefinitions(context, fieldDef.Directives); + var bindings = ReadMemberBindings(context, fieldDef.Directives, fieldDef, resolvers); + var variables = ReadArgumentVariableDefinitions(context, fieldDef.Directives, fieldDef); + var field = new ObjectField(fieldDef.Name.Value, bindings, variables, resolvers); + collection.Add(field); + } + + return new ObjectFieldCollection(collection); + } + + private IReadOnlyList ReadHttpClientConfigs( + ConfigurationDirectiveNamesContext context, + IReadOnlyList directiveNodes) + { + var configs = new List(); + + foreach (var directiveNode in directiveNodes) + { + if (directiveNode.Name.Value.EqualsOrdinal(context.HttpDirective)) + { + configs.Add(ReadHttpClientConfig(context, directiveNode)); + } + } + + return configs; + } + + private HttpClientConfig ReadHttpClientConfig( + ConfigurationDirectiveNamesContext context, + DirectiveNode directiveNode) + { + AssertName(directiveNode, context.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( + ConfigurationDirectiveNamesContext context, + IReadOnlyList directiveNodes) + { + var definitions = new List(); + + foreach (var directiveNode in directiveNodes) + { + if (directiveNode.Name.Value.EqualsOrdinal(context.VariableDirective)) + { + definitions.Add(ReadFieldVariableDefinition(context, directiveNode)); + } + } + + return new VariableDefinitionCollection(definitions); + } + + private FieldVariableDefinition ReadFieldVariableDefinition( + ConfigurationDirectiveNamesContext context, + DirectiveNode directiveNode) + { + AssertName(directiveNode, context.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( + ConfigurationDirectiveNamesContext context, + IReadOnlyList directiveNodes) + { + var definitions = new List(); + + foreach (var directiveNode in directiveNodes) + { + if (directiveNode.Name.Value.EqualsOrdinal(context.FetchDirective)) + { + definitions.Add(ReadFetchDefinition(context, directiveNode)); + } + } + + return new FetchDefinitionCollection(definitions); + } + + private FetchDefinition ReadFetchDefinition( + ConfigurationDirectiveNamesContext context, + DirectiveNode directiveNode) + { + AssertName(directiveNode, context.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( + ConfigurationDirectiveNamesContext context, + IReadOnlyList directiveNodes, + FieldDefinitionNode annotatedField, + FetchDefinitionCollection resolvers) + { + var definitions = new List(); + + foreach (var directiveNode in directiveNodes) + { + if (directiveNode.Name.Value.EqualsOrdinal(context.BindDirective)) + { + definitions.Add(ReadMemberBinding(context, 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( + ConfigurationDirectiveNamesContext context, + DirectiveNode directiveNode, + FieldDefinitionNode annotatedField) + { + AssertName(directiveNode, context.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( + ConfigurationDirectiveNamesContext context, + IReadOnlyList directiveNodes, + FieldDefinitionNode annotatedField) + { + var definitions = new List(); + + foreach (var directiveNode in directiveNodes) + { + if (directiveNode.Name.Value.EqualsOrdinal(context.VariableDirective)) + { + definitions.Add( + ReadArgumentVariableDefinition( + context, + directiveNode, + annotatedField)); + } + } + + return new ArgumentVariableDefinitionCollection(definitions); + } + + private ArgumentVariableDefinition ReadArgumentVariableDefinition( + ConfigurationDirectiveNamesContext context, + DirectiveNode directiveNode, + FieldDefinitionNode annotatedField) + { + AssertName(directiveNode, context.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"); + } + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfigurationToSchemaRewriter.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfigurationToSchemaRewriter.cs new file mode 100644 index 00000000000..ebab82f3532 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfigurationToSchemaRewriter.cs @@ -0,0 +1,20 @@ +using HotChocolate.Language; +using HotChocolate.Language.Visitors; + +namespace HotChocolate.Fusion.Metadata; + +internal sealed class ServiceConfigurationToSchemaRewriter + : SyntaxRewriter +{ + protected override DirectiveNode? RewriteDirective( + DirectiveNode node, + ConfigurationDirectiveNamesContext context) + { + if (context.IsConfigurationDirective(node.Name.Value)) + { + return null; + } + + return base.RewriteDirective(node, context); + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlan.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlan.cs index 45d571a62ae..62b9b20a842 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlan.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlan.cs @@ -13,17 +13,20 @@ internal sealed class QueryPlan { ExecutionNodes = executionNodes.ToArray(); RootExecutionNodes = ExecutionNodes.Where(t => t.DependsOn.Count == 0).ToArray(); - _lookup = ExecutionNodes.OfType().ToLookup(t => t.Handler.SelectionSet); + RequiresFetch = new HashSet(ExecutionNodes.OfType().Select(t => t.Handler.SelectionSet)); - _exports = exportDefinitions - .GroupBy(t => t.SelectionSet, t => t.StateKey) - .ToDictionary(t => t.Key, t => t.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; } + // name is not really good... the selection sets that require execution of request nodes. + public IReadOnlySet RequiresFetch { get; } + + // should we return a tree instead so that dependencies are correctly modeled? public IEnumerable GetRequestNodes(ISelectionSet selectionSet) => _lookup[selectionSet]; diff --git a/src/HotChocolate/Fusion/src/Core/Utilities/JsonQueryResultFormatter.cs b/src/HotChocolate/Fusion/src/Core/Utilities/JsonQueryResultFormatter.cs deleted file mode 100644 index 06d3f831135..00000000000 --- a/src/HotChocolate/Fusion/src/Core/Utilities/JsonQueryResultFormatter.cs +++ /dev/null @@ -1,447 +0,0 @@ -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; - -internal 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/JsonRequestFormatter.cs b/src/HotChocolate/Fusion/src/Core/Utilities/JsonRequestFormatter.cs index 52af944039f..650adc149e7 100644 --- a/src/HotChocolate/Fusion/src/Core/Utilities/JsonRequestFormatter.cs +++ b/src/HotChocolate/Fusion/src/Core/Utilities/JsonRequestFormatter.cs @@ -1,8 +1,6 @@ using System.Buffers; using System.Text.Encodings.Web; using System.Text.Json; -using HotChocolate.Fusion.Clients; -using HotChocolate.Fusion.Execution; using HotChocolate.Language; using GraphQLRequest = HotChocolate.Fusion.Clients.GraphQLRequest; diff --git a/src/HotChocolate/Fusion/test/Core.Tests/ExecutionPlanBuilderTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/ExecutionPlanBuilderTests.cs index 605796b40f1..546e3f81347 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/ExecutionPlanBuilderTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/ExecutionPlanBuilderTests.cs @@ -10,6 +10,202 @@ namespace HotChocolate.Fusion; public class ExecutionPlanBuilderTests { + [Fact] + public async Task GetPersonById_With_Name_And_Bio_With_Prefixed_Directives() + { + // 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 + @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 } }"") + } + + 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 } }"") { + + id: ID! + @abc_bind(to: ""a"") + @abc_bind(to: ""b"") + name: String! + @abc_bind(to: ""a"") + bio: String + @abc_bind(to: ""b"") + } + + schema + @fusion(prefix: ""abc"") + @abc_httpClient(name: ""a"" baseAddress: ""https://a/graphql"") + @abc_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_Name_And_Bio_With_Prefixed_Directives_PrefixedSelf() + { + // 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 + @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 } }"") + } + + 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 } }"") { + + id: ID! + @abc_bind(to: ""a"") + @abc_bind(to: ""b"") + name: String! + @abc_bind(to: ""a"") + bio: String + @abc_bind(to: ""b"") + } + + schema + @abc_fusion(prefix: ""abc"", prefixSelf: true) + @abc_httpClient(name: ""a"" baseAddress: ""https://a/graphql"") + @abc_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_Name_And_Bio() { diff --git a/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ConfigurationDirectiveNamesContextTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ConfigurationDirectiveNamesContextTests.cs new file mode 100644 index 00000000000..f4f7094dd31 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ConfigurationDirectiveNamesContextTests.cs @@ -0,0 +1,160 @@ +using CookieCrumble; +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Metadata; + +public class ConfigurationDirectiveNamesContextTests +{ + [Fact] + public void NewContext_DefaultDirectiveNames() + { + // act + var context = ConfigurationDirectiveNamesContext.Create(); + + // assert + Snapshot + .Create() + .Add(context) + .MatchInline( + @"{ + ""VariableDirective"": ""variable"", + ""FetchDirective"": ""fetch"", + ""BindDirective"": ""bind"", + ""HttpDirective"": ""httpClient"", + ""FusionDirective"": ""fusion"" + }"); + } + + [Fact] + public void NewContext_DirectiveNames_With_Prefix() + { + // act + var context = ConfigurationDirectiveNamesContext.Create(prefix: "def"); + + // assert + Snapshot + .Create() + .Add(context) + .MatchInline( + @"{ + ""VariableDirective"": ""def_variable"", + ""FetchDirective"": ""def_fetch"", + ""BindDirective"": ""def_bind"", + ""HttpDirective"": ""def_httpClient"", + ""FusionDirective"": ""fusion"" + }"); + } + + [Fact] + public void NewContext_DirectiveNames_With_Prefix_PrefixSelf() + { + // act + var context = ConfigurationDirectiveNamesContext.Create(prefix: "def", prefixSelf: true); + + // assert + Snapshot + .Create() + .Add(context) + .MatchInline( + @"{ + ""VariableDirective"": ""def_variable"", + ""FetchDirective"": ""def_fetch"", + ""BindDirective"": ""def_bind"", + ""HttpDirective"": ""def_httpClient"", + ""FusionDirective"": ""def_fusion"" + }"); + } + + [Fact] + public void From_Document_No_Fusion_Directive() + { + // arrange + var document = Utf8GraphQLParser.Parse(@"schema { }"); + + // act + var context = ConfigurationDirectiveNamesContext.From(document); + + // assert + Snapshot + .Create() + .Add(context) + .MatchInline( + @"{ + ""VariableDirective"": ""variable"", + ""FetchDirective"": ""fetch"", + ""BindDirective"": ""bind"", + ""HttpDirective"": ""httpClient"", + ""FusionDirective"": ""fusion"" + }"); + } + + [Fact] + public void From_Document_With_Fusion_Directive_No_Prefix() + { + // arrange + var document = Utf8GraphQLParser.Parse(@"schema @fusion(version: 1) { }"); + + // act + var context = ConfigurationDirectiveNamesContext.From(document); + + // assert + Snapshot + .Create() + .Add(context) + .MatchInline( + @"{ + ""VariableDirective"": ""variable"", + ""FetchDirective"": ""fetch"", + ""BindDirective"": ""bind"", + ""HttpDirective"": ""httpClient"", + ""FusionDirective"": ""fusion"" + }"); + } + + [Fact] + public void From_Document_With_Fusion_Directive_With_Prefix() + { + // arrange + var document = Utf8GraphQLParser.Parse(@"schema @fusion(prefix: ""abc"") { }"); + + // act + var context = ConfigurationDirectiveNamesContext.From(document); + + // assert + Snapshot + .Create() + .Add(context) + .MatchInline( + @"{ + ""VariableDirective"": ""abc_variable"", + ""FetchDirective"": ""abc_fetch"", + ""BindDirective"": ""abc_bind"", + ""HttpDirective"": ""abc_httpClient"", + ""FusionDirective"": ""fusion"" + }"); + } + + [Fact] + public void From_Document_With_Fusion_Directive_With_Prefix_PrefixSelf() + { + // arrange + var document = Utf8GraphQLParser.Parse( + @"schema @abc_fusion(prefix: ""abc"", prefixSelf: true) { }"); + + // act + var context = ConfigurationDirectiveNamesContext.From(document); + + // assert + Snapshot + .Create() + .Add(context) + .MatchInline( + @"{ + ""VariableDirective"": ""abc_variable"", + ""FetchDirective"": ""abc_fetch"", + ""BindDirective"": ""abc_bind"", + ""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 new file mode 100644 index 00000000000..dafe47a2d26 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ServiceConfigurationToSchemaRewriterTests.cs @@ -0,0 +1,69 @@ +using CookieCrumble; +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Metadata; + +public class ServiceConfigurationToSchemaRewriterTests +{ + [Fact] + public void Remove_Configuration_Directives() + { + // arrange + const string serviceDefinition = @" + 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 } }"") + } + + 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 } }"") { + + id: ID! + @abc_bind(to: ""a"") + @abc_bind(to: ""b"") + name: String! + @abc_bind(to: ""a"") + bio: String + @abc_bind(to: ""b"") + } + + schema + @fusion(prefix: ""abc"") + @abc_httpClient(name: ""a"" baseAddress: ""https://a/graphql"") + @abc_httpClient(name: ""b"" baseAddress: ""https://b/graphql"") { + query: Query + }"; + + var document = Utf8GraphQLParser.Parse(serviceDefinition); + + // act + var context = ConfigurationDirectiveNamesContext.From(document); + var rewriter = new ServiceConfigurationToSchemaRewriter(); + var rewritten = rewriter.Rewrite(document, context); + + // assert + Snapshot + .Create() + .Add(rewritten) + .MatchInline( + @"type Query { + personById(id: ID!): Person + } + + type Person { + id: ID! + name: String! + bio: String + } + + schema { + query: Query + }"); + } +} diff --git a/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs index 7a34a312fef..8fbb3486310 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs @@ -1,7 +1,7 @@ using CookieCrumble; using HotChocolate.Execution; using HotChocolate.Execution.Processing; -using HotChocolate.Fusion.Clients; +using HotChocolate.Execution.Serialization; using HotChocolate.Fusion.Execution; using HotChocolate.Fusion.Planning; using HotChocolate.Fusion.Utilities; @@ -64,18 +64,6 @@ public async Task Do() var clientFactory = new MockHttpClientFactory(clients); - const string sdl = @" - type Query { - personById(id: ID!) : Person - } - - type Person { - id: ID! - name: String! - bio: String - friends: [Person!] - }"; - const string serviceConfiguration = @" type Query { personById(id: ID!): Person @@ -120,10 +108,9 @@ type Person } }"); - var executor = await new ServiceCollection() + var executor = await new ServiceCollection() .AddSingleton(clientFactory) - .AddGraphQLServer() - .AddGraphQLGateway(serviceConfiguration, sdl) + .AddFusionGatewayServer(serviceConfiguration) .BuildRequestExecutorAsync(); // act diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__resources__/dummy.graphql b/src/HotChocolate/Fusion/test/Core.Tests/__resources__/dummy.graphql index d648faa4dce..2c388559021 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/__resources__/dummy.graphql +++ b/src/HotChocolate/Fusion/test/Core.Tests/__resources__/dummy.graphql @@ -1,24 +1,48 @@ 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(select: "personById(id: $personId) { ... Person }", from: "a") + @fetch(select: "personById(id: $personId) { ... Person }", from: "b") + persons(first: Int, last: Int) : [Person] + @variable(name: "first", argument: "first") + @variable(name: "last", argument: "last") + @fetch(select: "persons(first: $first, last: $last) { ... Person }", from: "a") + @fetch(select: "persons(first: $first, last: $last) { ... Person }", from: "b") } 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") + @variable(name: "personId", select: "id", from: "a", as: "Int!") + @variable(name: "personId", select: "id", from: "b", as: "Int!") + @fetch(select: "personById(id: $personId) { ... Person }", from: "a", ) + @fetch(select: "personById(id: $personId) { ... Person }", from: "b", ) { + 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 + @fusion(version: 1) @httpClient(name: "a", baseAddress: "https://a/graphql") @httpClient(name: "b", baseAddress: "https://b/graphql") { query: Query } + +directive @fusion(prefix: String, version: Int! = 1, prefixSelf: Boolean! = false) on SCHEMA +directive @fetch(from: _SchemaName, select: _FieldSyntax) repeatable on OBJECT | FIELD_DEFINITION +directive @variable(name: String!, argument: _ArgumentName, select: _FieldSyntax, from: _SchemaName, as: _TypeSyntax) repeatable on OBJECT | FIELD_DEFINITION +directive @bind(to: _SchemaName, as: _FieldName) repeatable on OBJECT | FIELD_DEFINITION +directive @httpClient(name: _SchemaName, baseAddress: _URI) repeatable on SCHEMA + +scalar _SchemaName +scalar _FieldName +scalar _ArgumentName +scalar _FieldSyntax +scalar _TypeSyntax +scalar _URI diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_And_Bio_With_Prefixed_Directives.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_And_Bio_With_Prefixed_Directives.snap new file mode 100644 index 00000000000..b6feca2de1c --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_And_Bio_With_Prefixed_Directives.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_And_Bio_With_Prefixed_Directives_PrefixedSelf.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_And_Bio_With_Prefixed_Directives_PrefixedSelf.snap new file mode 100644 index 00000000000..b6feca2de1c --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_And_Bio_With_Prefixed_Directives_PrefixedSelf.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/Spatial/test/Data.Filters.SqlServer.Tests/HotChocolate.Spatial.Data.Filters.SqlServer.Tests.csproj b/src/HotChocolate/Spatial/test/Data.Filters.SqlServer.Tests/HotChocolate.Spatial.Data.Filters.SqlServer.Tests.csproj index c22258eadb0..08eb8cd8e2d 100644 --- a/src/HotChocolate/Spatial/test/Data.Filters.SqlServer.Tests/HotChocolate.Spatial.Data.Filters.SqlServer.Tests.csproj +++ b/src/HotChocolate/Spatial/test/Data.Filters.SqlServer.Tests/HotChocolate.Spatial.Data.Filters.SqlServer.Tests.csproj @@ -21,8 +21,8 @@ - - + + diff --git a/src/HotChocolate/Spatial/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.Spatial.SqlServer.Tests.csproj b/src/HotChocolate/Spatial/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.Spatial.SqlServer.Tests.csproj index 24b1d332260..149990e4649 100644 --- a/src/HotChocolate/Spatial/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.Spatial.SqlServer.Tests.csproj +++ b/src/HotChocolate/Spatial/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.Spatial.SqlServer.Tests.csproj @@ -21,8 +21,8 @@ - - + +