From 5662f9bdbb01914f2afdbd07704953b15f7bfabe Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:53:32 +0100 Subject: [PATCH 1/2] Introduce centralized composition helper --- .../Fusion.Aspire/AspireCompositionHelper.cs | 82 +++++++ .../src/Fusion.Aspire/CompositionHelper.cs | 175 -------------- .../GraphQLCompositionSettings.cs | 2 +- .../src/Fusion.Aspire/SchemaComposition.cs | 27 ++- .../src/Fusion.Aspire/SourceSchemaInfo.cs | 2 +- .../Fusion.Composition/CompositionHelper.cs | 203 +++++++++++++++++ .../HotChocolate.Fusion.Composition.csproj | 3 + .../Settings/CompositionSettings.cs | 2 +- .../Settings}/SettingsExtensions.cs | 3 +- .../Settings/SettingsJsonSerializerContext.cs | 2 +- .../Settings/SourceSchemaSettings.cs | 2 +- .../SourceSchemaEnricher.cs | 8 +- .../SourceSchemaPreprocessor.cs | 6 +- .../Commands/Fusion/FusionComposeCommand.cs | 214 +----------------- .../Commands/Fusion/FusionPublishCommand.cs | 1 - .../Commands/Fusion/FusionPublishHelpers.cs | 3 +- .../Fusion/FusionSettingsSetCommand.cs | 2 +- .../CommandLineResources.Designer.cs | 6 - .../Properties/CommandLineResources.resx | 3 - 19 files changed, 323 insertions(+), 423 deletions(-) create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/AspireCompositionHelper.cs delete mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/CompositionHelper.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/CompositionHelper.cs rename src/{Nitro/CommandLine/src/CommandLine => HotChocolate/Fusion-vnext/src/Fusion.Composition}/Settings/CompositionSettings.cs (96%) rename src/{Nitro/CommandLine/src/CommandLine/Extensions => HotChocolate/Fusion-vnext/src/Fusion.Composition/Settings}/SettingsExtensions.cs (98%) rename src/{Nitro/CommandLine/src/CommandLine => HotChocolate/Fusion-vnext/src/Fusion.Composition}/Settings/SettingsJsonSerializerContext.cs (94%) rename src/{Nitro/CommandLine/src/CommandLine => HotChocolate/Fusion-vnext/src/Fusion.Composition}/Settings/SourceSchemaSettings.cs (94%) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/AspireCompositionHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/AspireCompositionHelper.cs new file mode 100644 index 00000000000..9aff6b2e124 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/AspireCompositionHelper.cs @@ -0,0 +1,82 @@ +using System.Collections.Immutable; +using System.Text; +using HotChocolate.Fusion.Logging; +using HotChocolate.Fusion.Packaging; +using Microsoft.Extensions.Logging; + +namespace HotChocolate.Fusion.Aspire; + +internal static class AspireCompositionHelper +{ + public static async Task TryComposeAsync( + string fusionArchivePath, + ImmutableArray newSourceSchemas, + GraphQLCompositionSettings settings, + ILogger logger, + CancellationToken cancellationToken) + { + using var archive = File.Exists(fusionArchivePath) + ? FusionArchive.Open(fusionArchivePath, FusionArchiveMode.Update) + : FusionArchive.Create(fusionArchivePath); + + var compositionLog = new CompositionLog(); + var environment = settings.EnvironmentName ?? "Aspire"; + var compositionSettings = new CompositionSettings + { + Merger = { EnableGlobalObjectIdentification = settings.EnableGlobalObjectIdentification } + }; + var sourceSchemas = newSourceSchemas.ToDictionary( + s => s.Name, + s => (s.Schema, s.SchemaSettings)); + + var result = await CompositionHelper.ComposeAsync( + compositionLog, + sourceSchemas, + archive, + environment, + compositionSettings, + cancellationToken); + + var output = new StringBuilder(); + + foreach (var entry in compositionLog) + { + if (entry.Severity is LogSeverity.Error) + { + output.AppendLine($"‼️ {FormatMultilineMessage(entry.Message)}"); + } + else + { + output.AppendLine(entry.Message); + } + } + + if (result.IsFailure) + { + output.Append("❌ Composition failed:"); + logger.LogError("{Message}", output.ToString()); + return false; + } + + output.Append("✅ Composition completed successfully."); + logger.LogInformation("{Message}", output.ToString()); + + return true; + } + + /// + /// Since we're prefixing the message with an emoji and space before printing, + /// we need to also indent each line of a multiline message by three spaces to fix the alignment. + /// + private static string FormatMultilineMessage(string message) + { + var lines = message.Split(Environment.NewLine); + + if (lines.Length <= 1) + { + return message; + } + + return string.Join(Environment.NewLine + " ", lines); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/CompositionHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/CompositionHelper.cs deleted file mode 100644 index aa35ff62b5a..00000000000 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/CompositionHelper.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System.Collections.Immutable; -using System.Text; -using System.Text.Json; -using HotChocolate.Buffers; -using HotChocolate.Fusion.Logging; -using HotChocolate.Fusion.Options; -using HotChocolate.Fusion.Packaging; -using Microsoft.Extensions.Logging; - -namespace HotChocolate.Fusion.Aspire; - -internal static class CompositionHelper -{ - public static async Task TryComposeAsync( - string fusionArchivePath, - ImmutableArray newSourceSchemas, - GraphQLCompositionSettings settings, - ILogger logger, - CancellationToken cancellationToken) - { - using var archive = File.Exists(fusionArchivePath) - ? FusionArchive.Open(fusionArchivePath, FusionArchiveMode.Update) - : FusionArchive.Create(fusionArchivePath); - - var existingSourceSchemaName = new SortedSet( - await archive.GetSourceSchemaNamesAsync(cancellationToken), - StringComparer.Ordinal); - - var normalizedToRealExistingSchemaNameLookup = - existingSourceSchemaName.ToDictionary(StringUtilities.ToConstantCase, s => s); - - // During the schema merging process, schema names are converted to upper-case, - // before being inserted into the fusion__Schema enum. - // This means two different schema names, like some-service and SomeService, - // could be uppercased to a conflicting SOME_SERVICE. - // To avoid weird errors for the user down the line, - // we already validate for collisions here. - foreach (var newSourceSchema in newSourceSchemas) - { - var normalizedSchemaName = StringUtilities.ToConstantCase(newSourceSchema.Name); - - if (normalizedToRealExistingSchemaNameLookup.TryGetValue(normalizedSchemaName, out var existingSchemaName) - && existingSchemaName != newSourceSchema.Name) - { - logger.LogError( - $"❌ '{newSourceSchema.Name}' conflicts with the existing source schema name '{existingSchemaName}'. " - + $"Either rename '{newSourceSchema.Name}' to '{existingSchemaName}' if they're the same, or " - + $"rename '{newSourceSchema.Name}' to something else if they're different."); - return false; - } - } - - var newSourceSchemaLookup = newSourceSchemas.ToDictionary(s => s.Name, StringComparer.Ordinal); - - foreach (var existingSchemaName in existingSourceSchemaName) - { - if (newSourceSchemaLookup.ContainsKey(existingSchemaName)) - { - continue; - } - - var configuration = await archive.TryGetSourceSchemaConfigurationAsync(existingSchemaName, cancellationToken); - if (configuration is null) - { - continue; - } - - var sourceText = await ReadSchemaSourceTextAsync(configuration, cancellationToken); - - newSourceSchemaLookup.Add(existingSchemaName, new SourceSchemaInfo - { - Name = existingSchemaName, - Schema = new SourceSchemaText(existingSchemaName, sourceText), - SchemaSettings = configuration.Settings.RootElement.Clone() - }); - } - - var newSourceSchemaNames = new SortedSet(newSourceSchemaLookup.Keys); - - var compositionLog = new CompositionLog(); - var schemaComposer = new SchemaComposer( - newSourceSchemaLookup.Values.Select(t => t.Schema), - new SchemaComposerOptions - { - Merger = - { - EnableGlobalObjectIdentification = settings.EnableGlobalObjectIdentification - } - }, - compositionLog); - var result = schemaComposer.Compose(); - - var output = new StringBuilder(); - - foreach (var entry in compositionLog) - { - if (entry.Severity is LogSeverity.Error) - { - output.AppendLine($"‼️ {FormatMultilineMessage(entry.Message)}"); - } - else - { - output.AppendLine(entry.Message); - } - } - - if (result.IsSuccess) - { - using var buffer = new PooledArrayWriter(4096); - var settingsComposer = new SettingsComposer(); - settingsComposer.Compose( - buffer, - newSourceSchemaLookup.Values.Select(t => t.SchemaSettings).ToArray(), - settings.EnvironmentName ?? "Aspire"); - - var metadata = new ArchiveMetadata - { - SupportedGatewayFormats = [WellKnownVersions.LatestGatewayFormatVersion], - SourceSchemas = [.. newSourceSchemaNames] - }; - - await archive.SetArchiveMetadataAsync(metadata, cancellationToken); - - foreach (var sourceSchema in newSourceSchemaLookup.Values) - { - await archive.SetSourceSchemaConfigurationAsync( - sourceSchema.Name, - Encoding.UTF8.GetBytes(sourceSchema.Schema.SourceText), - JsonDocument.Parse(sourceSchema.SchemaSettings.GetRawText()), - cancellationToken); - } - - await archive.SetGatewayConfigurationAsync( - result.Value.ToString(), - JsonDocument.Parse(buffer.WrittenMemory), - WellKnownVersions.LatestGatewayFormatVersion, - cancellationToken); - - await archive.CommitAsync(cancellationToken); - - output.Append("✅ Composition completed successfully."); - logger.LogInformation("{Message}", output.ToString()); - return true; - } - - output.Append("❌ Composition failed:"); - logger.LogError("{Message}", output.ToString()); - return false; - } - - private static async Task ReadSchemaSourceTextAsync( - SourceSchemaConfiguration configuration, - CancellationToken cancellationToken) - { - await using var stream = await configuration.OpenReadSchemaAsync(cancellationToken); - using var reader = new StreamReader(stream, Encoding.UTF8); - return await reader.ReadToEndAsync(cancellationToken); - } - - /// - /// Since we're prefixing the message with an emoji and space before printing, - /// we need to also indent each line of a multiline message by three spaces to fix the alignment. - /// - private static string FormatMultilineMessage(string message) - { - var lines = message.Split(Environment.NewLine); - - if (lines.Length <= 1) - { - return message; - } - - return string.Join(Environment.NewLine + " ", lines); - } -} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/GraphQLCompositionSettings.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/GraphQLCompositionSettings.cs index 5334e08f662..f930bec881b 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/GraphQLCompositionSettings.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/GraphQLCompositionSettings.cs @@ -8,7 +8,7 @@ public struct GraphQLCompositionSettings /// /// Gets or sets a value indicating whether Global Object Identification should be enabled. /// - public bool EnableGlobalObjectIdentification { get; set; } + public bool? EnableGlobalObjectIdentification { get; set; } /// /// Gets or sets the environment name that shall be used for composition. diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/SchemaComposition.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/SchemaComposition.cs index 90a730dff4b..e490475a394 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/SchemaComposition.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/SchemaComposition.cs @@ -91,9 +91,19 @@ private async Task ComposeSchemaAsync( return true; } - var gatewayDirectory = GetProjectPath(compositionResource)!; - var archivePath = Path.Combine(Path.GetDirectoryName(gatewayDirectory)!, settings.OutputFileName); - return await ComposeSchemaAsync(archivePath, sourceSchemas, settings, cancellationToken); + try + { + var gatewayDirectory = GetProjectPath(compositionResource)!; + var archivePath = Path.Combine(Path.GetDirectoryName(gatewayDirectory)!, settings.OutputFileName); + return await ComposeSchemaAsync(archivePath, sourceSchemas, settings, cancellationToken); + } + finally + { + foreach (var sourceSchema in sourceSchemas) + { + sourceSchema.SchemaSettings.Dispose(); + } + } } catch (OperationCanceledException) { @@ -247,7 +257,7 @@ private List GetReferencedResources( ResourceName = resource.Name, HttpEndpointUrl = new Uri(schemaUrl), Schema = new SourceSchemaText(sourceSchemaName, schemaText), - SchemaSettings = schemaSettings.Value + SchemaSettings = schemaSettings }; } @@ -281,11 +291,11 @@ private List GetReferencedResources( ResourceName = resource.Name, HttpEndpointUrl = null, // No HTTP endpoint for file-based schemas Schema = new SourceSchemaText(sourceSchemaName, schemaFromFile), - SchemaSettings = schemaSettings.Value + SchemaSettings = schemaSettings }; } - private async Task GetSourceSchemaSettingsAsync( + private async Task GetSourceSchemaSettingsAsync( IResourceWithEndpoints resource, string settingsFileName, CancellationToken cancellationToken) @@ -309,8 +319,7 @@ private List GetReferencedResources( } var settingsJson = await File.ReadAllTextAsync(settingsFile, cancellationToken); - using var document = JsonDocument.Parse(settingsJson); - return document.RootElement.Clone(); + return JsonDocument.Parse(settingsJson); } catch (Exception ex) { @@ -487,7 +496,7 @@ private async Task ComposeSchemaAsync( File.Copy(archivePath, tempArchivePath); } - if (await CompositionHelper.TryComposeAsync( + if (await AspireCompositionHelper.TryComposeAsync( tempArchivePath, [.. sourceSchemas], settings.Settings, diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/SourceSchemaInfo.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/SourceSchemaInfo.cs index 14dc784b7c0..22953fd177c 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/SourceSchemaInfo.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Aspire/SourceSchemaInfo.cs @@ -8,5 +8,5 @@ internal sealed record SourceSchemaInfo public string? ResourceName { get; init; } public Uri? HttpEndpointUrl { get; init; } public required SourceSchemaText Schema { get; init; } - public required JsonElement SchemaSettings { get; init; } + public required JsonDocument SchemaSettings { get; init; } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/CompositionHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/CompositionHelper.cs new file mode 100644 index 00000000000..8397ccdcf87 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/CompositionHelper.cs @@ -0,0 +1,203 @@ +using System.Collections.Immutable; +using System.Text; +using System.Text.Json; +using HotChocolate.Buffers; +using HotChocolate.Fusion.Errors; +using HotChocolate.Fusion.Logging; +using HotChocolate.Fusion.Logging.Contracts; +using HotChocolate.Fusion.Options; +using HotChocolate.Fusion.Packaging; +using HotChocolate.Fusion.Results; +using HotChocolate.Types.Mutable; + +namespace HotChocolate.Fusion; + +internal static class CompositionHelper +{ + public static async Task> ComposeAsync( + ICompositionLog compositionLog, + Dictionary sourceSchemas, + FusionArchive archive, + string environment, + CompositionSettings? compositionSettings, + CancellationToken cancellationToken) + { + var existingSourceSchemaNames = new SortedSet( + await archive.GetSourceSchemaNamesAsync(cancellationToken), + StringComparer.Ordinal); + + var normalizedToRealExistingSchemaNameLookup = + existingSourceSchemaNames.ToDictionary(StringUtilities.ToConstantCase, s => s); + + // During the schema merging process, schema names are converted to upper-case, + // before being inserted into the fusion__Schema enum. + // This means two different schema names, like some-service and SomeService, + // could be uppercased to a conflicting SOME_SERVICE. + // To avoid weird errors for the user down the line, + // we already validate for collisions here. + foreach (var (newSourceSchemaName, _) in sourceSchemas) + { + var normalizedSchemaName = StringUtilities.ToConstantCase(newSourceSchemaName); + + if (normalizedToRealExistingSchemaNameLookup.TryGetValue(normalizedSchemaName, out var existingSchemaName) + && existingSchemaName != newSourceSchemaName) + { + compositionLog.Write( + LogEntryBuilder.New() + .SetMessage( + "'{0}' conflicts with the existing source schema name '{1}'. Either rename '{0}' to '{1}' if they're the same, or rename '{0}' to something else if they're different.", + newSourceSchemaName, + existingSchemaName) + .SetCode(LogEntryCodes.ConflictingSourceSchemaName) + .SetSeverity(LogSeverity.Error) + .Build()); + + ImmutableArray errors = [new("❌ Composition failed")]; + return errors; + } + } + + foreach (var schemaName in existingSourceSchemaNames) + { + if (sourceSchemas.ContainsKey(schemaName)) + { + // We have a new configuration for the schema, so we'll take that + // instead of the one in the gateway package. + continue; + } + + var configuration = await archive.TryGetSourceSchemaConfigurationAsync(schemaName, cancellationToken); + + if (configuration is null) + { + continue; + } + + var sourceText = await ReadSchemaSourceTextAsync(configuration, cancellationToken); + + sourceSchemas[schemaName] = (new SourceSchemaText(schemaName, sourceText), configuration.Settings); + } + + var existingCompositionSettings = await GetCompositionSettingsAsync(archive, cancellationToken); + var mergedCompositionSettings = + compositionSettings?.MergeInto(existingCompositionSettings) ?? existingCompositionSettings; + + var sourceSchemaOptionsMap = new Dictionary(); + var mergerOptions = mergedCompositionSettings.Merger.ToOptions(); + var satisfiabilityOptions = mergedCompositionSettings.Satisfiability.ToOptions(); + + foreach (var (sourceSchemaName, (_, sourceSchemaSettings)) in sourceSchemas) + { + var schemaSettings = + sourceSchemaSettings.Deserialize(SettingsJsonSerializerContext.Default.SourceSchemaSettings)!; + + var sourceSchemaOptions = schemaSettings.ToOptions(); + + mergedCompositionSettings.Preprocessor?.MergeInto(sourceSchemaOptions.Preprocessor); + sourceSchemaOptionsMap.Add(sourceSchemaName, sourceSchemaOptions); + schemaSettings.Satisfiability?.MergeInto(satisfiabilityOptions); + } + + var schemaComposerOptions = new SchemaComposerOptions + { + SourceSchemas = sourceSchemaOptionsMap, + Merger = mergerOptions, + Satisfiability = satisfiabilityOptions + }; + + var schemaComposer = new SchemaComposer( + sourceSchemas.Select(s => s.Value.Item1), + schemaComposerOptions, + compositionLog); + + var result = schemaComposer.Compose(); + + if (result.IsFailure) + { + return result; + } + + using var bufferWriter = new PooledArrayWriter(); + new SettingsComposer().Compose( + bufferWriter, + sourceSchemas.Select(s => s.Value.Item2.RootElement).ToArray(), + environment); + + var metadata = new ArchiveMetadata + { + SupportedGatewayFormats = [WellKnownVersions.LatestGatewayFormatVersion], + SourceSchemas = [.. sourceSchemas.Keys] + }; + + await archive.SetArchiveMetadataAsync(metadata, cancellationToken); + + foreach (var (schemaName, (schema, settings)) in sourceSchemas) + { + await archive.SetSourceSchemaConfigurationAsync( + schemaName, + Encoding.UTF8.GetBytes(schema.SourceText), + settings, + cancellationToken); + } + + await archive.SetGatewayConfigurationAsync( + result.Value + Environment.NewLine, + JsonDocument.Parse(bufferWriter.WrittenMemory), + WellKnownVersions.LatestGatewayFormatVersion, + cancellationToken); + + await SaveCompositionSettingsAsync(archive, schemaComposerOptions, cancellationToken); + + await archive.CommitAsync(cancellationToken); + + return result; + } + + private static async Task GetCompositionSettingsAsync( + FusionArchive archive, + CancellationToken cancellationToken) + { + var compositionSettings = await archive.GetCompositionSettingsAsync(cancellationToken); + + return compositionSettings?.Deserialize(SettingsJsonSerializerContext.Default.CompositionSettings) + ?? new CompositionSettings + { + Merger = new CompositionSettings.MergerSettings + { + EnableGlobalObjectIdentification = false + } + }; + } + + private static async Task SaveCompositionSettingsAsync( + FusionArchive archive, + SchemaComposerOptions options, + CancellationToken cancellationToken) + { + var settings = new CompositionSettings + { + Merger = new CompositionSettings.MergerSettings + { + EnableGlobalObjectIdentification = options.Merger.EnableGlobalObjectIdentification + }, + Satisfiability = new CompositionSettings.SatisfiabilitySettings + { + IncludeSatisfiabilityPaths = options.Satisfiability.IncludeSatisfiabilityPaths + } + }; + var settingsJson = JsonSerializer.SerializeToDocument( + settings, + SettingsJsonSerializerContext.Default.CompositionSettings); + + await archive.SetCompositionSettingsAsync(settingsJson, cancellationToken); + } + + private static async Task ReadSchemaSourceTextAsync( + SourceSchemaConfiguration configuration, + CancellationToken cancellationToken) + { + await using var stream = await configuration.OpenReadSchemaAsync(cancellationToken); + using var reader = new StreamReader(stream, Encoding.UTF8); + return await reader.ReadToEndAsync(cancellationToken); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj index e2f7e11d936..6b598fd38ca 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj @@ -7,6 +7,8 @@ + + @@ -16,6 +18,7 @@ + diff --git a/src/Nitro/CommandLine/src/CommandLine/Settings/CompositionSettings.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Settings/CompositionSettings.cs similarity index 96% rename from src/Nitro/CommandLine/src/CommandLine/Settings/CompositionSettings.cs rename to src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Settings/CompositionSettings.cs index f4b9eaedbf6..8f0b804af74 100644 --- a/src/Nitro/CommandLine/src/CommandLine/Settings/CompositionSettings.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Settings/CompositionSettings.cs @@ -1,7 +1,7 @@ using System.Text.Json.Serialization; using HotChocolate.Fusion.Options; -namespace ChilliCream.Nitro.CommandLine.Settings; +namespace HotChocolate.Fusion; internal sealed record CompositionSettings { diff --git a/src/Nitro/CommandLine/src/CommandLine/Extensions/SettingsExtensions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Settings/SettingsExtensions.cs similarity index 98% rename from src/Nitro/CommandLine/src/CommandLine/Extensions/SettingsExtensions.cs rename to src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Settings/SettingsExtensions.cs index b5834987c6c..607a23892c4 100644 --- a/src/Nitro/CommandLine/src/CommandLine/Extensions/SettingsExtensions.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Settings/SettingsExtensions.cs @@ -1,7 +1,6 @@ -using ChilliCream.Nitro.CommandLine.Settings; using HotChocolate.Fusion.Options; -namespace ChilliCream.Nitro.CommandLine; +namespace HotChocolate.Fusion; internal static class SettingsExtensions { diff --git a/src/Nitro/CommandLine/src/CommandLine/Settings/SettingsJsonSerializerContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Settings/SettingsJsonSerializerContext.cs similarity index 94% rename from src/Nitro/CommandLine/src/CommandLine/Settings/SettingsJsonSerializerContext.cs rename to src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Settings/SettingsJsonSerializerContext.cs index 5a883cb1812..7041c1fd068 100644 --- a/src/Nitro/CommandLine/src/CommandLine/Settings/SettingsJsonSerializerContext.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Settings/SettingsJsonSerializerContext.cs @@ -1,7 +1,7 @@ using System.Text.Json.Serialization; using HotChocolate.Fusion.Options; -namespace ChilliCream.Nitro.CommandLine.Settings; +namespace HotChocolate.Fusion; [JsonSerializable(typeof(CompositionSettings))] [JsonSerializable(typeof(CompositionSettings.PreprocessorSettings), TypeInfoPropertyName = "CompositionPreprocessorSettings")] diff --git a/src/Nitro/CommandLine/src/CommandLine/Settings/SourceSchemaSettings.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Settings/SourceSchemaSettings.cs similarity index 94% rename from src/Nitro/CommandLine/src/CommandLine/Settings/SourceSchemaSettings.cs rename to src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Settings/SourceSchemaSettings.cs index 49c1b2b34eb..ce6a47f1e6a 100644 --- a/src/Nitro/CommandLine/src/CommandLine/Settings/SourceSchemaSettings.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Settings/SourceSchemaSettings.cs @@ -1,4 +1,4 @@ -namespace ChilliCream.Nitro.CommandLine.Settings; +namespace HotChocolate.Fusion; internal sealed record SourceSchemaSettings { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaEnricher.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaEnricher.cs index b23021840dc..0ef6b1d6db1 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaEnricher.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaEnricher.cs @@ -57,7 +57,7 @@ private static void EnrichObjectType(MutableObjectTypeDefinition objectType) { var sourceMetadata = objectType.Features.GetOrSet(); sourceMetadata.HasShareableDirective = objectType.Directives.ContainsName(Shareable); - sourceMetadata.IsInternal = objectType.Directives.ContainsName(Internal); + sourceMetadata.IsInternal = objectType.Directives.ContainsName(WellKnownDirectiveNames.Internal); } private static void EnrichEnumType(MutableEnumTypeDefinition enumType) @@ -80,11 +80,11 @@ private void EnrichOutputField(MutableOutputFieldDefinition outputField) outputField.Directives.ContainsName(Inaccessible) || declaringType.Directives.ContainsName(Inaccessible); sourceMetadata.IsInternal = - outputField.Directives.ContainsName(Internal) - || declaringType.Directives.ContainsName(Internal); + outputField.Directives.ContainsName(WellKnownDirectiveNames.Internal) + || declaringType.Directives.ContainsName(WellKnownDirectiveNames.Internal); sourceMetadata.IsLookup = outputField.Directives.ContainsName(Lookup); sourceMetadata.HasExternalDirective = outputField.Directives.ContainsName(External); - sourceMetadata.HasInternalDirective = outputField.Directives.ContainsName(Internal); + sourceMetadata.HasInternalDirective = outputField.Directives.ContainsName(WellKnownDirectiveNames.Internal); sourceMetadata.HasOverrideDirective = outputField.Directives.ContainsName(Override); sourceMetadata.HasProvidesDirective = outputField.Directives.ContainsName(Provides); sourceMetadata.HasShareableDirective = outputField.Directives.ContainsName(Shareable); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaPreprocessor.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaPreprocessor.cs index fe835c9622b..24cf5ab4cc6 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaPreprocessor.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaPreprocessor.cs @@ -199,13 +199,13 @@ private void ApplyShareableDirectives() continue; } - if (field.Directives.ContainsName(Internal) || field.Directives.ContainsName(Inaccessible)) + if (field.Directives.ContainsName(WellKnownDirectiveNames.Internal) || field.Directives.ContainsName(Inaccessible)) { continue; } if (!otherType.Fields.TryGetField(field.Name, out var otherField) - || otherField.Directives.ContainsName(Internal) + || otherField.Directives.ContainsName(WellKnownDirectiveNames.Internal) || otherField.Directives.ContainsName(Inaccessible)) { continue; @@ -240,7 +240,7 @@ private void RemoveDirectivesFromBatchFields() continue; } - if (field.Directives.ContainsName(Internal)) + if (field.Directives.ContainsName(WellKnownDirectiveNames.Internal)) { queryType.Fields.Remove(field); } diff --git a/src/Nitro/CommandLine/src/CommandLine/Commands/Fusion/FusionComposeCommand.cs b/src/Nitro/CommandLine/src/CommandLine/Commands/Fusion/FusionComposeCommand.cs index c0e7fb3fcfa..aaa5135de7d 100644 --- a/src/Nitro/CommandLine/src/CommandLine/Commands/Fusion/FusionComposeCommand.cs +++ b/src/Nitro/CommandLine/src/CommandLine/Commands/Fusion/FusionComposeCommand.cs @@ -1,19 +1,11 @@ -using System.Collections.Immutable; using System.CommandLine.IO; using System.Text; using System.Text.Json; using System.Threading.Channels; using ChilliCream.Nitro.CommandLine.Options; -using ChilliCream.Nitro.CommandLine.Settings; -using HotChocolate.Buffers; using HotChocolate.Fusion; -using HotChocolate.Fusion.Errors; using HotChocolate.Fusion.Logging; -using HotChocolate.Fusion.Logging.Contracts; -using HotChocolate.Fusion.Options; using HotChocolate.Fusion.Packaging; -using HotChocolate.Fusion.Results; -using HotChocolate.Types.Mutable; using static ChilliCream.Nitro.CommandLine.CommandLineResources; namespace ChilliCream.Nitro.CommandLine.Commands.Fusion; @@ -407,8 +399,9 @@ private static async Task ComposeAsync( var sourceSchemas = await ReadSourceSchemasAsync(sourceSchemaFiles, cancellationToken); var compositionLog = new CompositionLog(); + environment ??= Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development"; - var result = await ComposeAsync( + var result = await CompositionHelper.ComposeAsync( compositionLog, sourceSchemas, archive, @@ -451,161 +444,6 @@ private static async Task ComposeAsync( } } - public static async Task> ComposeAsync( - ICompositionLog compositionLog, - Dictionary sourceSchemas, - FusionArchive archive, - string? environment, - CompositionSettings? compositionSettings, - CancellationToken cancellationToken) - { - environment ??= Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development"; - - var existingSourceSchemaNames = new SortedSet( - await archive.GetSourceSchemaNamesAsync(cancellationToken), - StringComparer.Ordinal); - - var normalizedToRealExistingSchemaNameLookup = - existingSourceSchemaNames.ToDictionary(StringUtilities.ToConstantCase, s => s); - - // During the schema merging process, schema names are converted to upper-case, - // before being inserted into the fusion__Schema enum. - // This means two different schema names, like some-service and SomeService, - // could be uppercased to a conflicting SOME_SERVICE. - // To avoid weird errors for the user down the line, - // we already validate for collisions here. - foreach (var (newSourceSchemaName, _) in sourceSchemas) - { - var normalizedSchemaName = StringUtilities.ToConstantCase(newSourceSchemaName); - - if (normalizedToRealExistingSchemaNameLookup.TryGetValue(normalizedSchemaName, out var existingSchemaName) - && existingSchemaName != newSourceSchemaName) - { - compositionLog.Write( - LogEntryBuilder.New() - .SetMessage( - ComposeCommand_Error_ConflictingSchemaName, - newSourceSchemaName, - existingSchemaName) - .SetCode(LogEntryCodes.ConflictingSourceSchemaName) - .SetSeverity(LogSeverity.Error) - .Build()); - - ImmutableArray errors = [new("❌ Composition failed")]; - return errors; - } - } - - foreach (var schemaName in existingSourceSchemaNames) - { - if (sourceSchemas.ContainsKey(schemaName)) - { - // We have a new configuration for the schema, so we'll take that - // instead of the one in the gateway package. - continue; - } - - var configuration = await archive.TryGetSourceSchemaConfigurationAsync(schemaName, cancellationToken); - - if (configuration is null) - { - continue; - } - - var sourceText = await ReadSchemaSourceTextAsync(configuration, cancellationToken); - - sourceSchemas[schemaName] = (new SourceSchemaText(schemaName, sourceText), configuration.Settings); - } - - var existingCompositionSettings = await GetCompositionSettingsAsync(archive, cancellationToken); - var mergedCompositionSettings = - compositionSettings?.MergeInto(existingCompositionSettings) ?? existingCompositionSettings; - - var sourceSchemaOptionsMap = new Dictionary(); - var mergerOptions = mergedCompositionSettings.Merger.ToOptions(); - var satisfiabilityOptions = mergedCompositionSettings.Satisfiability.ToOptions(); - - foreach (var (sourceSchemaName, (_, sourceSchemaSettings)) in sourceSchemas) - { - var schemaSettings = - sourceSchemaSettings.Deserialize(SettingsJsonSerializerContext.Default.SourceSchemaSettings)!; - - var sourceSchemaOptions = schemaSettings.ToOptions(); - - mergedCompositionSettings.Preprocessor?.MergeInto(sourceSchemaOptions.Preprocessor); - sourceSchemaOptionsMap.Add(sourceSchemaName, sourceSchemaOptions); - schemaSettings.Satisfiability?.MergeInto(satisfiabilityOptions); - } - - var schemaComposerOptions = new SchemaComposerOptions - { - SourceSchemas = sourceSchemaOptionsMap, - Merger = mergerOptions, - Satisfiability = satisfiabilityOptions - }; - - if (existingCompositionSettings.Merger.EnableGlobalObjectIdentification - != schemaComposerOptions.Merger.EnableGlobalObjectIdentification) - { - compositionLog.Write( - LogEntryBuilder.New() - .SetMessage( - schemaComposerOptions.Merger.EnableGlobalObjectIdentification - ? ComposeCommand_GlobalObjectIdentification_Enabled - : ComposeCommand_GlobalObjectIdentification_Disabled) - .SetCode(LogEntryCodes.ModifiedCompositionSetting) - .SetSeverity(LogSeverity.Info) - .Build()); - } - - var schemaComposer = new SchemaComposer( - sourceSchemas.Select(s => s.Value.Item1), - schemaComposerOptions, - compositionLog); - - var result = schemaComposer.Compose(); - - if (result.IsFailure) - { - return result; - } - - using var bufferWriter = new PooledArrayWriter(); - new SettingsComposer().Compose( - bufferWriter, - sourceSchemas.Select(s => s.Value.Item2.RootElement).ToArray(), - environment); - - var metadata = new ArchiveMetadata - { - SupportedGatewayFormats = [WellKnownVersions.LatestGatewayFormatVersion], - SourceSchemas = [.. sourceSchemas.Keys] - }; - - await archive.SetArchiveMetadataAsync(metadata, cancellationToken); - - foreach (var (schemaName, (schema, settings)) in sourceSchemas) - { - await archive.SetSourceSchemaConfigurationAsync( - schemaName, - Encoding.UTF8.GetBytes(schema.SourceText), - settings, - cancellationToken); - } - - await archive.SetGatewayConfigurationAsync( - result.Value + Environment.NewLine, - JsonDocument.Parse(bufferWriter.WrittenMemory), - WellKnownVersions.LatestGatewayFormatVersion, - cancellationToken); - - await SaveCompositionSettingsAsync(archive, schemaComposerOptions, cancellationToken); - - await archive.CommitAsync(cancellationToken); - - return result; - } - public static void WriteCompositionLog( CompositionLog compositionLog, IStandardStreamWriter writer, @@ -715,54 +553,6 @@ public static void WriteCompositionLog( return (schemaName, new SourceSchemaText(schemaName, sourceText), settings); } - private static async Task GetCompositionSettingsAsync( - FusionArchive archive, - CancellationToken cancellationToken) - { - var compositionSettings = await archive.GetCompositionSettingsAsync(cancellationToken); - - return compositionSettings?.Deserialize(SettingsJsonSerializerContext.Default.CompositionSettings) - ?? new CompositionSettings - { - Merger = new CompositionSettings.MergerSettings - { - EnableGlobalObjectIdentification = false - } - }; - } - - private static async Task SaveCompositionSettingsAsync( - FusionArchive archive, - SchemaComposerOptions options, - CancellationToken cancellationToken) - { - var settings = new CompositionSettings - { - Merger = new CompositionSettings.MergerSettings - { - EnableGlobalObjectIdentification = options.Merger.EnableGlobalObjectIdentification - }, - Satisfiability = new CompositionSettings.SatisfiabilitySettings - { - IncludeSatisfiabilityPaths = options.Satisfiability.IncludeSatisfiabilityPaths - } - }; - var settingsJson = JsonSerializer.SerializeToDocument( - settings, - SettingsJsonSerializerContext.Default.CompositionSettings); - - await archive.SetCompositionSettingsAsync(settingsJson, cancellationToken); - } - - private static async Task ReadSchemaSourceTextAsync( - SourceSchemaConfiguration configuration, - CancellationToken cancellationToken) - { - await using var stream = await configuration.OpenReadSchemaAsync(cancellationToken); - using var reader = new StreamReader(stream, Encoding.UTF8); - return await reader.ReadToEndAsync(cancellationToken); - } - /// /// Since we're prefixing the message with an emoji and space before printing, /// we need to also indent each line of a multiline message by three spaces to fix the alignment. diff --git a/src/Nitro/CommandLine/src/CommandLine/Commands/Fusion/FusionPublishCommand.cs b/src/Nitro/CommandLine/src/CommandLine/Commands/Fusion/FusionPublishCommand.cs index 430bc9f63fb..5034bc73281 100644 --- a/src/Nitro/CommandLine/src/CommandLine/Commands/Fusion/FusionPublishCommand.cs +++ b/src/Nitro/CommandLine/src/CommandLine/Commands/Fusion/FusionPublishCommand.cs @@ -4,7 +4,6 @@ using ChilliCream.Nitro.CommandLine.Commands.Fusion.PublishCommand; using ChilliCream.Nitro.CommandLine.Helpers; using ChilliCream.Nitro.CommandLine.Options; -using ChilliCream.Nitro.CommandLine.Settings; using HotChocolate.Fusion; namespace ChilliCream.Nitro.CommandLine.Commands.Fusion; diff --git a/src/Nitro/CommandLine/src/CommandLine/Commands/Fusion/FusionPublishHelpers.cs b/src/Nitro/CommandLine/src/CommandLine/Commands/Fusion/FusionPublishHelpers.cs index bacf54621ee..f1debe01947 100644 --- a/src/Nitro/CommandLine/src/CommandLine/Commands/Fusion/FusionPublishHelpers.cs +++ b/src/Nitro/CommandLine/src/CommandLine/Commands/Fusion/FusionPublishHelpers.cs @@ -6,7 +6,6 @@ using System.Text.Json; using ChilliCream.Nitro.CommandLine.Client; using ChilliCream.Nitro.CommandLine.Helpers; -using ChilliCream.Nitro.CommandLine.Settings; using HotChocolate.Fusion; using HotChocolate.Fusion.Logging; using HotChocolate.Fusion.Packaging; @@ -334,7 +333,7 @@ public static async Task ComposeAsync( var compositionLog = new CompositionLog(); - var result = await FusionComposeCommand.ComposeAsync( + var result = await CompositionHelper.ComposeAsync( compositionLog, newSourceSchemas, archive, diff --git a/src/Nitro/CommandLine/src/CommandLine/Commands/Fusion/FusionSettingsSetCommand.cs b/src/Nitro/CommandLine/src/CommandLine/Commands/Fusion/FusionSettingsSetCommand.cs index 7f8cdc4686c..859ac69ad8b 100644 --- a/src/Nitro/CommandLine/src/CommandLine/Commands/Fusion/FusionSettingsSetCommand.cs +++ b/src/Nitro/CommandLine/src/CommandLine/Commands/Fusion/FusionSettingsSetCommand.cs @@ -1,7 +1,7 @@ using ChilliCream.Nitro.CommandLine.Client; using ChilliCream.Nitro.CommandLine.Helpers; using ChilliCream.Nitro.CommandLine.Options; -using ChilliCream.Nitro.CommandLine.Settings; +using HotChocolate.Fusion; namespace ChilliCream.Nitro.CommandLine.Commands.Fusion; diff --git a/src/Nitro/CommandLine/src/CommandLine/Properties/CommandLineResources.Designer.cs b/src/Nitro/CommandLine/src/CommandLine/Properties/CommandLineResources.Designer.cs index d38d37bc715..e87aa8ab423 100644 --- a/src/Nitro/CommandLine/src/CommandLine/Properties/CommandLineResources.Designer.cs +++ b/src/Nitro/CommandLine/src/CommandLine/Properties/CommandLineResources.Designer.cs @@ -81,12 +81,6 @@ internal static string ComposeCommand_Description { } } - internal static string ComposeCommand_GlobalObjectIdentification_Disabled { - get { - return ResourceManager.GetString("ComposeCommand_GlobalObjectIdentification_Disabled", resourceCulture); - } - } - internal static string ComposeCommand_GlobalObjectIdentification_Enabled { get { return ResourceManager.GetString("ComposeCommand_GlobalObjectIdentification_Enabled", resourceCulture); diff --git a/src/Nitro/CommandLine/src/CommandLine/Properties/CommandLineResources.resx b/src/Nitro/CommandLine/src/CommandLine/Properties/CommandLineResources.resx index 44d36bf4ab6..c55d4145d8b 100644 --- a/src/Nitro/CommandLine/src/CommandLine/Properties/CommandLineResources.resx +++ b/src/Nitro/CommandLine/src/CommandLine/Properties/CommandLineResources.resx @@ -36,9 +36,6 @@ Composes multiple source schemas into a single composite schema. - - Disabled global object identification. - Enabled global object identification. From 1e952c600199177bc8545e7e2fe3e3d12c8e45cf Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:44:33 +0100 Subject: [PATCH 2/2] Cleanup --- .../CommandLine/Properties/CommandLineResources.Designer.cs | 6 ------ .../src/CommandLine/Properties/CommandLineResources.resx | 3 --- 2 files changed, 9 deletions(-) diff --git a/src/Nitro/CommandLine/src/CommandLine/Properties/CommandLineResources.Designer.cs b/src/Nitro/CommandLine/src/CommandLine/Properties/CommandLineResources.Designer.cs index e87aa8ab423..823f9c03254 100644 --- a/src/Nitro/CommandLine/src/CommandLine/Properties/CommandLineResources.Designer.cs +++ b/src/Nitro/CommandLine/src/CommandLine/Properties/CommandLineResources.Designer.cs @@ -81,12 +81,6 @@ internal static string ComposeCommand_Description { } } - internal static string ComposeCommand_GlobalObjectIdentification_Enabled { - get { - return ResourceManager.GetString("ComposeCommand_GlobalObjectIdentification_Enabled", resourceCulture); - } - } - internal static string ComposeCommand_EnableGlobalObjectIdentification_Description { get { return ResourceManager.GetString("ComposeCommand_EnableGlobalObjectIdentification_Description", resourceCulture); diff --git a/src/Nitro/CommandLine/src/CommandLine/Properties/CommandLineResources.resx b/src/Nitro/CommandLine/src/CommandLine/Properties/CommandLineResources.resx index c55d4145d8b..2e2487cb402 100644 --- a/src/Nitro/CommandLine/src/CommandLine/Properties/CommandLineResources.resx +++ b/src/Nitro/CommandLine/src/CommandLine/Properties/CommandLineResources.resx @@ -36,9 +36,6 @@ Composes multiple source schemas into a single composite schema. - - Enabled global object identification. - Determines whether the 'Query.node' field shall be added.