Skip to content
Draft
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 @@ -23,6 +23,11 @@ internal record InputTypeGroupEvent(
string InputTypeName,
ImmutableArray<InputTypeInfo> InputTypeGroup) : IEvent;

internal record ObjectFieldGroupEvent(
string FieldName,
ImmutableArray<ObjectFieldInfo> FieldGroup,
string TypeName) : IEvent;

internal record OutputFieldGroupEvent(
string FieldName,
ImmutableArray<OutputFieldInfo> FieldGroup,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ public static void ApplyKeyDirective(this MutableComplexTypeDefinition type, str
new ArgumentAssignment(ArgumentNames.Fields, keyFields)));
}
}

public static IEnumerable<Directive> GetKeyDirectives(this MutableComplexTypeDefinition type)
{
return type.Directives.AsEnumerable().Where(d => d.Name == DirectiveNames.Key);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ public static string GetKeyFields(
return mergedSelectionSet.ToString(indented: false).AsSpan()[2..^2].ToString();
}

public static string? GetOverrideFrom(this MutableOutputFieldDefinition field)
{
var overrideDirective =
field.Directives.AsEnumerable().SingleOrDefault(d => d.Name == DirectiveNames.Override);

return (string?)overrideDirective?.Arguments[ArgumentNames.From].Value;
}

public static bool HasInternalDirective(this MutableOutputFieldDefinition field)
{
return field.Directives.ContainsName(DirectiveNames.Internal);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using HotChocolate.Features;
using HotChocolate.Fusion.Features;
using HotChocolate.Types;
using static HotChocolate.Fusion.WellKnownArgumentNames;
Expand Down Expand Up @@ -26,8 +27,9 @@ public static ImmutableArray<string> GetSchemaNames(
return [.. schemaNames];
}

public static SourceFieldMetadata? GetSourceFieldMetadata(this IOutputFieldDefinition field)
public static SourceFieldMetadata GetRequiredSourceFieldMetadata(
this IOutputFieldDefinition field)
{
return field.Features.Get<SourceFieldMetadata>();
return field.Features.GetRequired<SourceFieldMetadata>();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ namespace HotChocolate.Fusion.Features;

internal sealed class SourceFieldMetadata
{
public bool HasShareableDirective { get; set; }

public bool IsExternal { get; set; }

public bool IsInternal { get; set; }

public bool IsKeyField { get; set; }

public bool IsOverridden { get; set; }

public bool IsShareable { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using HotChocolate.Types.Mutable;

namespace HotChocolate.Fusion.Info;

internal record ObjectFieldInfo(
MutableOutputFieldDefinition Field,
MutableObjectTypeDefinition Type,
MutableSchemaDefinition Schema);
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public static class LogEntryCodes
public const string InputFieldTypesNotMergeable = "INPUT_FIELD_TYPES_NOT_MERGEABLE";
public const string InputWithMissingRequiredFields = "INPUT_WITH_MISSING_REQUIRED_FIELDS";
public const string InterfaceFieldNoImplementation = "INTERFACE_FIELD_NO_IMPLEMENTATION";
public const string InvalidFieldSharing = "INVALID_FIELD_SHARING";
public const string InvalidGraphQL = "INVALID_GRAPHQL";
public const string InvalidShareableUsage = "INVALID_SHAREABLE_USAGE";
public const string IsInvalidFields = "IS_INVALID_FIELDS";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,22 @@ public static LogEntry InterfaceFieldNoImplementation(
schema);
}

public static LogEntry InvalidFieldSharing(
MutableOutputFieldDefinition field,
string typeName,
MutableSchemaDefinition schema)
{
var coordinate = new SchemaCoordinate(typeName, field.Name);

return new LogEntry(
string.Format(LogEntryHelper_InvalidFieldSharing, coordinate, schema.Name),
LogEntryCodes.InvalidFieldSharing,
LogSeverity.Error,
coordinate,
field,
schema);
}

public static LogEntry InvalidGraphQL(string exceptionMessage)
{
return new LogEntry(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ public sealed class SchemaComposerOptions
{
public required bool EnableGlobalObjectIdentification { get; init; }

public SourceSchemaPreprocessorOptions Preprocessor { get; init; } = new();
public Dictionary<string, SourceSchemaPreprocessorOptions> PreprocessorOptions { get; init; } = [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@ public sealed class SourceSchemaPreprocessorOptions
/// Applies inferred key directives to types that are returned by lookup fields.
/// </summary>
public bool ApplyInferredKeyDirectives { get; set; } = true;

/// <summary>
/// Applies key directives to types based on the keys defined on the interfaces that they
/// implement.
/// </summary>
public bool InheritInterfaceKeys { get; set; } = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Collections.Immutable;
using HotChocolate.Fusion.Events;
using HotChocolate.Fusion.Events.Contracts;
using HotChocolate.Fusion.Extensions;
using static HotChocolate.Fusion.Logging.LogEntryHelper;

namespace HotChocolate.Fusion.PreMergeValidationRules;

/// <summary>
/// <para>
/// A field in a federated GraphQL schema may be marked <c>@shareable</c>, indicating that the same
/// field can be resolved by multiple schemas without conflict. When a field is <b>not</b> marked as
/// <c>@shareable</c> (sometimes called “non-shareable”), it cannot be provided by more than one
/// schema.
/// </para>
/// <para>
/// Field definitions marked as <c>@external</c> and overridden fields are excluded when validating
/// whether a field is shareable. These annotations indicate specific cases where field ownership
/// lies with another schema or has been replaced.
/// </para>
/// </summary>
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Invalid-Field-Sharing">
/// Specification
/// </seealso>
internal sealed class InvalidFieldSharingRule : IEventHandler<ObjectFieldGroupEvent>
{
public void Handle(ObjectFieldGroupEvent @event, CompositionContext context)
{
var fieldGroup = @event.FieldGroup;

// Exclude external and overridden fields.
var filteredFieldGroup =
fieldGroup
.Where(
i => i.Field.GetRequiredSourceFieldMetadata() is
{
IsExternal: false,
IsOverridden: false
})
.ToImmutableArray();

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

// Remaining fields must be shareable.
foreach (var (field, type, schema) in filteredFieldGroup)
{
if (!field.GetRequiredSourceFieldMetadata().IsShareable)
{
context.Log.Write(InvalidFieldSharing(field, type.Name, schema));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ private void PublishEvents()
MultiValueDictionary<string, InputTypeInfo> inputTypeGroupByName = [];
MultiValueDictionary<string, InputFieldInfo> inputFieldGroupByName = [];
MultiValueDictionary<string, OutputFieldInfo> outputFieldGroupByName = [];
MultiValueDictionary<string, ObjectFieldInfo> objectFieldGroupByName = [];
MultiValueDictionary<string, EnumTypeInfo> enumTypeGroupByName = [];

foreach (var (type, schema) in typeGroup)
Expand Down Expand Up @@ -81,6 +82,13 @@ private void PublishEvents()
outputFieldGroupByName.Add(
field.Name,
new OutputFieldInfo(field, complexType, schema));

if (complexType is MutableObjectTypeDefinition objectType)
{
objectFieldGroupByName.Add(
field.Name,
new ObjectFieldInfo(field, objectType, schema));
}
}

break;
Expand Down Expand Up @@ -131,6 +139,12 @@ private void PublishEvents()
}
}

foreach (var (fieldName, fieldGroup) in objectFieldGroupByName)
{
PublishEvent(
new ObjectFieldGroupEvent(fieldName, [.. fieldGroup], typeName), context);
}

foreach (var (enumName, enumGroup) in enumTypeGroupByName)
{
PublishEvent(new EnumTypeGroupEvent(enumName, [.. enumGroup]), context);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,14 @@
<data name="LogEntryHelper_InterfaceFieldNoImplementation" xml:space="preserve">
<value>The merged object type '{0}' must implement the field '{1}' on interface '{2}'.</value>
</data>
<data name="LogEntryHelper_InvalidFieldSharing" xml:space="preserve">
<value>The field '{0}' in schema '{1}' must be shareable.</value>
</data>
<data name="LogEntryHelper_InvalidGraphQL" xml:space="preserve">
<value>Invalid GraphQL in source schema. Exception message: {0}.</value>
</data>
<data name="LogEntryHelper_InvalidShareableUsage" xml:space="preserve">
<value>The interface field '{0}' in schema '{1}' must not be marked as shareable.</value>
<value>The field '{0}' in schema '{1}' must not be marked as shareable.</value>
</data>
<data name="LogEntryHelper_IsInvalidFields" xml:space="preserve">
<value>The @is directive on argument '{0}' in schema '{1}' specifies an invalid field selection against the composed schema.</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,30 @@ public CompositionResult<MutableSchemaDefinition> Compose()
}

// Preprocess Source Schemas
var preprocessorOptions = _schemaComposerOptions.Preprocessor;
var preprocessResult =
schemas.Select(schema => new SourceSchemaPreprocessor(schema, preprocessorOptions).Process()).Combine();
schemas.Select(schema =>
{
var optionsExist =
_schemaComposerOptions.PreprocessorOptions.TryGetValue(
schema.Name,
out var preprocessorOptions);

if (!optionsExist)
{
preprocessorOptions = new SourceSchemaPreprocessorOptions();
}

return new SourceSchemaPreprocessor(schema, preprocessorOptions).Process();
}).Combine();

if (preprocessResult.IsFailure)
{
return preprocessResult;
}

// Enrich Source Schemas
var enrichmentResult = schemas.Select(schema => new SourceSchemaEnricher(schema).Enrich()).Combine();
var enrichmentResult =
schemas.Select(schema => new SourceSchemaEnricher(schema, schemas).Enrich()).Combine();

if (enrichmentResult.IsFailure)
{
Expand Down Expand Up @@ -153,6 +166,7 @@ public CompositionResult<MutableSchemaDefinition> Compose()
new InputFieldDefaultMismatchRule(),
new InputFieldTypesMergeableRule(),
new InputWithMissingRequiredFieldsRule(),
new InvalidFieldSharingRule(),
new OutputFieldTypesMergeableRule(),
new TypeKindMismatchRule()
];
Expand Down
Loading
Loading