Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -213,16 +213,23 @@ internal static CompositionSettings CreateCompositionSettings(
{
CacheControlMergeBehavior = settings.CacheControlMergeBehavior,
EnableGlobalObjectIdentification = settings.EnableGlobalObjectIdentification,
EnumValuesMergeBehavior = settings.EnumValuesMergeBehavior,
NodeResolution = settings.NodeResolution,
TagMergeBehavior = settings.TagMergeBehavior
},
Satisfiability = { IncludeSatisfiabilityPaths = settings.IncludeSatisfiabilityPaths },
Satisfiability =
{
IncludeSatisfiabilityPaths = settings.IncludeSatisfiabilityPaths
},
ApolloFederationCompatibility =
{
AllowNonResolvableInterfaceObjects = settings.AllowNonResolvableInterfaceObjects,
ShareableFieldRuntimeTypeRouting = settings.ShareableFieldRuntimeTypeRouting
},
Preprocessor = { ExcludeByTag = settings.ExcludeByTag?.ToHashSet() }
Preprocessor =
{
ExcludeByTag = settings.ExcludeByTag?.ToHashSet()
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ public struct GraphQLCompositionSettings
/// </summary>
public bool? EnableGlobalObjectIdentification { get; set; }

/// <summary>
/// Gets or sets how enum values are merged when the same enum type is defined in multiple
/// source schemas.
/// </summary>
public EnumValuesMergeBehavior? EnumValuesMergeBehavior { get; set; }

/// <summary>
/// Gets or sets how the gateway resolves the <c>Query.node</c> field.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public static class LogEntryCodes
public const string InterfaceObjectKeyMismatch = "INTERFACE_OBJECT_KEY_MISMATCH";
public const string InterfaceObjectKeyMissing = "INTERFACE_OBJECT_KEY_MISSING";
public const string InterfaceObjectNoInterface = "INTERFACE_OBJECT_NO_INTERFACE";
public const string InvalidEnumValuesMergeBehavior = "INVALID_ENUM_VALUES_MERGE_BEHAVIOR";
public const string InvalidFieldSharing = "INVALID_FIELD_SHARING";
public const string InvalidGraphQL = "INVALID_GRAPHQL";
public const string InvalidApolloFederationSupportSettings =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace HotChocolate.Fusion.Options;

/// <summary>
/// Defines how enum values are merged when the same enum type is defined in multiple source
/// schemas.
/// </summary>
public enum EnumValuesMergeBehavior
{
/// <summary>
/// Applies <see cref="Strict"/>, unless at least one Apollo Federation connector source
/// schema is part of the composition, in which case <see cref="Union"/> applies.
/// </summary>
Auto = 0,

/// <summary>
/// Enum types with the same name must define identical value sets.
/// </summary>
Strict = 1,

/// <summary>
/// Values of enums used only in output positions are merged by union. Enums used in any
/// input position still require identical value sets.
/// </summary>
Union = 2
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ public sealed class SourceSchemaMergerOptions
/// </summary>
public bool EnableGlobalObjectIdentification { get; set; }

/// <summary>
/// Defines how enum values are merged when the same enum type is defined in multiple source
/// schemas.
/// </summary>
public EnumValuesMergeBehavior EnumValuesMergeBehavior { get; set; } = EnumValuesMergeBehavior.Auto;

/// <summary>
/// Defines how the gateway resolves the <c>Query.node</c> field.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
using System.Collections.Immutable;
using HotChocolate.Fusion.ApolloFederation;
using HotChocolate.Fusion.Events;
using HotChocolate.Fusion.Events.Contracts;
using HotChocolate.Fusion.Extensions;
using HotChocolate.Fusion.Info;
using HotChocolate.Fusion.Options;
using HotChocolate.Types;
using HotChocolate.Types.Mutable;
using static HotChocolate.Fusion.Logging.LogEntryHelper;

namespace HotChocolate.Fusion.PreMergeValidationRules;
Expand All @@ -19,21 +24,36 @@ namespace HotChocolate.Fusion.PreMergeValidationRules;
/// inconsistencies by enforcing that all instances of the same named enum across schemas have an
/// exact match in their values.
/// </para>
/// <para>
/// The configured merge behavior is provided at construction. Under
/// <see cref="EnumValuesMergeBehavior.Union"/>, values of enums used only in output positions
/// merge by union. <see cref="EnumValuesMergeBehavior.Auto"/> applies Union when at least one
/// Apollo Federation connector source schema is part of the composition, otherwise Strict.
/// </para>
/// </summary>
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Enum-Values-Mismatch">
/// Specification
/// </seealso>
internal sealed class EnumValuesMismatchRule : IEventHandler<EnumTypeGroupEvent>
internal sealed class EnumValuesMismatchRule(EnumValuesMergeBehavior enumValuesMergeBehavior)
: IEventHandler<EnumTypeGroupEvent>
{
private readonly EnumValuesMergeBehavior _enumValuesMergeBehavior = enumValuesMergeBehavior;

public void Handle(EnumTypeGroupEvent @event, CompositionContext context)
{
var (_, enumGroup) = @event;
var (typeName, enumGroup) = @event;

if (enumGroup.Length < 2)
{
return;
}

if (ResolveMergeBehavior(context) is EnumValuesMergeBehavior.Union
&& IsOutputOnlyEnum(typeName, enumGroup))
{
return;
}

var enumValues = enumGroup
.SelectMany(e => e.Type.Values.AsEnumerable())
.Where(v => !v.IsInaccessible)
Expand All @@ -51,4 +71,82 @@ public void Handle(EnumTypeGroupEvent @event, CompositionContext context)
}
}
}

private EnumValuesMergeBehavior ResolveMergeBehavior(CompositionContext context)
{
if (_enumValuesMergeBehavior is not EnumValuesMergeBehavior.Auto)
{
return _enumValuesMergeBehavior;
}

foreach (var schema in context.SchemaDefinitions)
{
if (schema.Features.Get<ConnectorKindMetadata>()?.Kind == "ApolloFederation")
{
return EnumValuesMergeBehavior.Union;
}
}

return EnumValuesMergeBehavior.Strict;
}

private static bool IsOutputOnlyEnum(string typeName, ImmutableArray<EnumTypeInfo> enumGroup)
{
foreach (var (_, schema) in enumGroup)
{
if (IsUsedInInputPosition(typeName, schema))
{
return false;
}
}

return true;
}

private static bool IsUsedInInputPosition(string typeName, MutableSchemaDefinition schema)
{
foreach (var type in schema.Types)
{
switch (type)
{
case MutableComplexTypeDefinition complexType:
foreach (var field in complexType.Fields)
{
foreach (var argument in field.Arguments)
{
if (argument.Type.NamedType().Name == typeName)
{
return true;
}
}
}

break;

case MutableInputObjectTypeDefinition inputType:
foreach (var field in inputType.Fields)
{
if (field.Type.NamedType().Name == typeName)
{
return true;
}
}

break;
}
}

foreach (var directiveDefinition in schema.DirectiveDefinitions)
{
foreach (var argument in directiveDefinition.Arguments)
{
if (argument.Type.NamedType().Name == typeName)
{
return true;
}
}
}

return false;
}
}
29 changes: 24 additions & 5 deletions src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ public SchemaComposer(

public CompositionResult<MutableSchemaDefinition> Compose()
{
if (!Enum.IsDefined(_schemaComposerOptions.Merger.EnumValuesMergeBehavior))
{
return InvalidEnumValuesMergeBehavior(
$"The enum values merge behavior '{_schemaComposerOptions.Merger.EnumValuesMergeBehavior}' is invalid.");
}

if (!Enum.IsDefined(_schemaComposerOptions.Merger.NodeResolution))
{
return InvalidNodeResolution(
Expand Down Expand Up @@ -159,7 +165,8 @@ public CompositionResult<MutableSchemaDefinition> Compose()

// Pre Merge Validation
var preMergeValidationResult =
new PreMergeValidator(schemas, PreMergeRules, _log).Validate();
new PreMergeValidator(schemas, CreatePreMergeRules(_schemaComposerOptions), _log)
.Validate();

if (preMergeValidationResult.IsFailure)
{
Expand Down Expand Up @@ -207,6 +214,18 @@ public CompositionResult<MutableSchemaDefinition> Compose()
return mergedSchema;
}

private CompositionError InvalidEnumValuesMergeBehavior(string message)
{
_log.Write(
LogEntryBuilder.New()
.SetMessage(message)
.SetCode(LogEntryCodes.InvalidEnumValuesMergeBehavior)
.SetSeverity(LogSeverity.Error)
.Build());

return new CompositionError(message);
}

private CompositionError InvalidNodeResolution(string message)
{
_log.Write(
Expand Down Expand Up @@ -276,12 +295,12 @@ private CompositionError InvalidShareableFieldRuntimeTypeRouting(string message)
];

/// <summary>
/// Gets the rules that run across the source schemas before the merge, in the order they are
/// applied.
/// Creates the rules that run across the source schemas before the merge, in the order they
/// are applied, configured from the given options.
/// </summary>
internal static ImmutableArray<object> PreMergeRules { get; } =
internal static ImmutableArray<object> CreatePreMergeRules(SchemaComposerOptions options) =>
[
new EnumValuesMismatchRule(),
new EnumValuesMismatchRule(options.Merger.EnumValuesMergeBehavior),
new ExternalArgumentDefaultMismatchRule(),
new ExternalArgumentMissingRule(),
Comment on lines +301 to 305
new ExternalArgumentTypeMismatchRule(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ internal sealed record MergerSettings

public bool? EnableGlobalObjectIdentification { get; set; }

public EnumValuesMergeBehavior? EnumValuesMergeBehavior { get; set; }

public NodeResolution? NodeResolution { get; set; }

public bool? RemoveUnreferencedDefinitions { get; init; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public CompositionSettings MergeInto(CompositionSettings settings)
EnableGlobalObjectIdentification =
compositionSettings.Merger.EnableGlobalObjectIdentification
?? settings.Merger.EnableGlobalObjectIdentification,
EnumValuesMergeBehavior =
compositionSettings.Merger.EnumValuesMergeBehavior
?? settings.Merger.EnumValuesMergeBehavior,
NodeResolution =
compositionSettings.Merger.NodeResolution
?? settings.Merger.NodeResolution,
Expand Down Expand Up @@ -113,6 +116,11 @@ public SourceSchemaMergerOptions ToOptions()
mergerOptions.EnableGlobalObjectIdentification = enableGlobalObjectIdentification;
}

if (mergerSettings.EnumValuesMergeBehavior is { } enumValuesMergeBehavior)
{
mergerOptions.EnumValuesMergeBehavior = enumValuesMergeBehavior;
}

if (mergerSettings.NodeResolution is { } nodeResolution)
{
mergerOptions.NodeResolution = nodeResolution;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace HotChocolate.Fusion;
Converters =
[
typeof(JsonStringEnumConverter<DirectiveMergeBehavior>),
typeof(JsonStringEnumConverter<EnumValuesMergeBehavior>),
typeof(JsonStringEnumConverter<NodeResolution>),
typeof(JsonStringEnumConverter<ShareableFieldRuntimeTypeRouting>)
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ public void CreateCompositionSettings_Should_MapAllUserFacingSettings()
AllowNonResolvableInterfaceObjects = true,
CacheControlMergeBehavior = DirectiveMergeBehavior.IncludePrivate,
EnableGlobalObjectIdentification = true,
EnumValuesMergeBehavior = EnumValuesMergeBehavior.Union,
ExcludeByTag = new HashSet<string> { "internal" },
IncludeSatisfiabilityPaths = false,
NodeResolution = NodeResolution.SourceSchema,
ShareableFieldRuntimeTypeRouting =
ShareableFieldRuntimeTypeRouting.CommonRuntimeTypes,
ShareableFieldRuntimeTypeRouting = ShareableFieldRuntimeTypeRouting.CommonRuntimeTypes,
TagMergeBehavior = DirectiveMergeBehavior.Include
};
var compositionSettings = AspireCompositionHelper.CreateCompositionSettings(settings);
Expand All @@ -102,6 +102,7 @@ public void CreateCompositionSettings_Should_MapAllUserFacingSettings()
"addFusionDefinitions": null,
"cacheControlMergeBehavior": "IncludePrivate",
"enableGlobalObjectIdentification": true,
"enumValuesMergeBehavior": "Union",
"nodeResolution": "SourceSchema",
"removeUnreferencedDefinitions": null,
"tagMergeBehavior": "Include"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ await harness.Composition.ComposeOnGatewayStartAsync(
"addFusionDefinitions": null,
"cacheControlMergeBehavior": "Include",
"enableGlobalObjectIdentification": false,
"enumValuesMergeBehavior": null,
"nodeResolution": "Gateway",
"removeUnreferencedDefinitions": false,
"tagMergeBehavior": "IncludePrivate"
Expand Down
Loading
Loading