Skip to content
Open
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
4 changes: 2 additions & 2 deletions src/HotChocolate/Fusion/benchmarks/k6/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)..\'))" />

<PropertyGroup>
<TargetFrameworks>net11.0</TargetFrameworks>
<TargetFramework>net11.0</TargetFramework>
<TargetFrameworks>net10.0</TargetFrameworks>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ public FusionLookupMutableDirectiveDefinition(
Description = FusionLookupMutableDirectiveDefinition_Argument_Internal_Description
});

Arguments.Add(new MutableInputFieldDefinition(ArgumentNames.PropagateNull, new NonNullType(booleanType))
{
DefaultValue = new BooleanValueNode(false),
Description = FusionLookupMutableDirectiveDefinition_Argument_PropagateNull_Description
});

IsRepeatable = true;

Locations =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using HotChocolate.Types;
using HotChocolate.Types.Mutable;
using static HotChocolate.Fusion.Properties.CompositionResources;

namespace HotChocolate.Fusion.Definitions;

/// <summary>
/// The <c>@propagateNull</c> directive marks a lookup field whose null result invalidates the
/// entity node resolved by that lookup.
/// </summary>
internal sealed class PropagateNullMutableDirectiveDefinition : MutableDirectiveDefinition
{
public PropagateNullMutableDirectiveDefinition() : base(WellKnownDirectiveNames.PropagateNull)
{
Description = PropagateNullMutableDirectiveDefinition_Description;

Locations = DirectiveLocation.FieldDefinition;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public bool HasOverrideDirective
public bool HasProvidesDirective
=> outputField.Features.GetRequired<SourceOutputFieldMetadata>().HasProvidesDirective;

public bool HasPropagateNullDirective
=> outputField.Features.GetRequired<SourceOutputFieldMetadata>().HasPropagateNullDirective;

public bool HasShareableDirective
=> outputField.Features.GetRequired<SourceOutputFieldMetadata>().HasShareableDirective;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ internal sealed class SourceOutputFieldMetadata

public bool HasProvidesDirective { get; set; }

public bool HasPropagateNullDirective { get; set; }

public bool HasShareableDirective { get; set; }

public bool IsExternal => HasExternalDirective;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal static class FusionBuiltIns
new KeyMutableDirectiveDefinition(s_fieldSelectionSetType),
new LookupMutableDirectiveDefinition(),
new OverrideMutableDirectiveDefinition(s_stringType),
new PropagateNullMutableDirectiveDefinition(),
new ProvidesMutableDirectiveDefinition(s_fieldSelectionSetType),
new RequireMutableDirectiveDefinition(s_fieldSelectionMapType),
new ShareableMutableDirectiveDefinition(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public static class LogEntryCodes
public const string OutputFieldTypesNotMergeable = "OUTPUT_FIELD_TYPES_NOT_MERGEABLE";
public const string OverrideFromSelf = "OVERRIDE_FROM_SELF";
public const string OverrideOnInterface = "OVERRIDE_ON_INTERFACE";
public const string PropagateNullDirectiveNotRepeatable = "PROPAGATE_NULL_DIRECTIVE_NOT_REPEATABLE";
public const string PropagateNullOnNonLookupField = "PROPAGATE_NULL_ON_NON_LOOKUP_FIELD";
public const string ProvidesDirectiveInFieldsArgument = "PROVIDES_DIRECTIVE_IN_FIELDS_ARGUMENT";
public const string ProvidesFieldsHasArguments = "PROVIDES_FIELDS_HAS_ARGUMENTS";
public const string ProvidesFieldsMissingExternal = "PROVIDES_FIELDS_MISSING_EXTERNAL";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,38 @@ public static LogEntry ProvidesOnNonCompositeField(
.Build();
}

public static LogEntry PropagateNullOnNonLookupField(
MutableOutputFieldDefinition field,
MutableSchemaDefinition schema)
{
return LogEntryBuilder.New()
.SetMessage(
LogEntryHelper_PropagateNullOnNonLookupField,
field.Coordinate.ToString(),
schema.Name)
.SetCode(LogEntryCodes.PropagateNullOnNonLookupField)
.SetSeverity(LogSeverity.Error)
.SetTypeSystemMember(field)
.SetSchema(schema)
.Build();
}

public static LogEntry PropagateNullDirectiveNotRepeatable(
MutableOutputFieldDefinition field,
MutableSchemaDefinition schema)
{
return LogEntryBuilder.New()
.SetMessage(
LogEntryHelper_PropagateNullDirectiveNotRepeatable,
field.Coordinate.ToString(),
schema.Name)
.SetCode(LogEntryCodes.PropagateNullDirectiveNotRepeatable)
.SetSeverity(LogSeverity.Error)
.SetTypeSystemMember(field)
.SetSchema(schema)
.Build();
}

public static LogEntry EventStreamMessageInvalidFields(
MutableOutputFieldDefinition field,
MutableSchemaDefinition schema,
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 @@ -168,6 +168,9 @@
<data name="FusionLookupMutableDirectiveDefinition_Argument_Path_Description" xml:space="preserve">
<value>The path to the lookup field relative to the Query type.</value>
</data>
<data name="FusionLookupMutableDirectiveDefinition_Argument_PropagateNull_Description" xml:space="preserve">
<value>Specifies whether a null result from this lookup invalidates the resolved entity.</value>
</data>
<data name="FusionLookupMutableDirectiveDefinition_Argument_Schema_Description" xml:space="preserve">
<value>The name of the source schema where the annotated entity type can be looked up from.</value>
</data>
Expand Down Expand Up @@ -423,6 +426,12 @@
<data name="LogEntryHelper_OverrideOnInterface" xml:space="preserve">
<value>The interface field '{0}' in schema '{1}' must not be annotated with the @override directive.</value>
</data>
<data name="LogEntryHelper_PropagateNullOnNonLookupField" xml:space="preserve">
<value>The field '{0}' in schema '{1}' includes a @propagateNull directive, but is not a lookup field.</value>
</data>
<data name="LogEntryHelper_PropagateNullDirectiveNotRepeatable" xml:space="preserve">
<value>The field '{0}' in schema '{1}' includes multiple @propagateNull directives, but @propagateNull is not repeatable.</value>
</data>
<data name="LogEntryHelper_ProvidesDirectiveInFieldsArgument" xml:space="preserve">
<value>The @provides directive on field '{0}' in schema '{1}' references field '{2}', which must not include directive applications.</value>
</data>
Expand Down Expand Up @@ -489,6 +498,9 @@
<data name="LookupMutableDirectiveDefinition_Description" xml:space="preserve">
<value>The @lookup directive is used within a source schema to specify output fields that can be used by the distributed GraphQL executor to resolve an entity by a stable key.</value>
</data>
<data name="PropagateNullMutableDirectiveDefinition_Description" xml:space="preserve">
<value>The @propagateNull directive marks a lookup field whose null result invalidates the resolved entity.</value>
</data>
<data name="OverrideMutableDirectiveDefinition_Argument_From_Description" xml:space="preserve">
<value>The name of the source schema that originally provided this field.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ public CompositionResult<MutableSchemaDefinition> Compose()
new LookupReturnsNonNullableTypeRule(),
new OverrideFromSelfRule(),
new OverrideOnInterfaceRule(),
new PropagateNullNotRepeatableRule(),
new PropagateNullOnNonLookupFieldRule(),
new ProvidesDirectiveInFieldsArgumentRule(),
new ProvidesFieldsHasArgumentsRule(),
new ProvidesFieldsMissingExternalRule(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ private void EnrichOutputField(MutableOutputFieldDefinition outputField)
sourceMetadata.HasEventCursorDirective = outputField.Directives.ContainsName(EventCursor);
sourceMetadata.HasInternalDirective = outputField.Directives.ContainsName(WellKnownDirectiveNames.Internal);
sourceMetadata.HasOverrideDirective = outputField.Directives.ContainsName(Override);
sourceMetadata.HasPropagateNullDirective = outputField.Directives.ContainsName(PropagateNull);
sourceMetadata.HasProvidesDirective = outputField.Directives.ContainsName(Provides);
sourceMetadata.HasShareableDirective = outputField.Directives.ContainsName(Shareable);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,15 +243,25 @@ private void AddFusionLookupDirectives(MutableSchemaDefinition mergedSchema)
valueSelectionGroup.ConvertAll(
a => new StringValueNode(a.ToString(indented: false))));

var argumentAssignments = new List<ArgumentAssignment>
{
new(ArgumentNames.Schema, schemaArgument),
new(ArgumentNames.Key, keyArgument),
new(ArgumentNames.Field, fieldArgument),
new(ArgumentNames.Map, mapArgument),
new(ArgumentNames.Path, pathArgument),
new(ArgumentNames.Internal, @internal)
};

if (sourceField.HasPropagateNullDirective)
{
argumentAssignments.Add(new ArgumentAssignment(ArgumentNames.PropagateNull, true));
}

mergedType.Directives.Add(
new Directive(
_fusionDirectiveDefinitions[DirectiveNames.FusionLookup],
new ArgumentAssignment(ArgumentNames.Schema, schemaArgument),
new ArgumentAssignment(ArgumentNames.Key, keyArgument),
new ArgumentAssignment(ArgumentNames.Field, fieldArgument),
new ArgumentAssignment(ArgumentNames.Map, mapArgument),
new ArgumentAssignment(ArgumentNames.Path, pathArgument),
new ArgumentAssignment(ArgumentNames.Internal, @internal)));
argumentAssignments));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using HotChocolate.Fusion.Events;
using HotChocolate.Fusion.Events.Contracts;
using HotChocolate.Fusion.Extensions;
using static HotChocolate.Fusion.Logging.LogEntryHelper;

namespace HotChocolate.Fusion.SourceSchemaValidationRules;

/// <summary>
/// The <c>@propagateNull</c> directive is not repeatable, so a field must not apply it more than
/// once. Applying it multiple times is invalid and raises a PROPAGATE_NULL_DIRECTIVE_NOT_REPEATABLE
/// error.
/// </summary>
internal sealed class PropagateNullNotRepeatableRule : IEventHandler<OutputFieldEvent>
{
public void Handle(OutputFieldEvent @event, CompositionContext context)
{
var (field, _, schema) = @event;

if (field.HasPropagateNullDirective
&& field.Directives.AsEnumerable().Count(
d => d.Name == WellKnownDirectiveNames.PropagateNull) > 1)
{
context.Log.Write(PropagateNullDirectiveNotRepeatable(field, schema));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using HotChocolate.Fusion.Events;
using HotChocolate.Fusion.Events.Contracts;
using HotChocolate.Fusion.Extensions;
using static HotChocolate.Fusion.Logging.LogEntryHelper;

namespace HotChocolate.Fusion.SourceSchemaValidationRules;

/// <summary>
/// The <c>@propagateNull</c> directive changes how a null lookup result affects its target entity.
/// It is only meaningful on fields that are also marked as lookup fields. Applying it to a
/// non-lookup field is invalid and raises a PROPAGATE_NULL_ON_NON_LOOKUP_FIELD error.
/// </summary>
internal sealed class PropagateNullOnNonLookupFieldRule : IEventHandler<OutputFieldEvent>
{
public void Handle(OutputFieldEvent @event, CompositionContext context)
{
var (field, _, schema) = @event;

if (field is { HasPropagateNullDirective: true, IsLookup: false })
{
context.Log.Write(PropagateNullOnNonLookupField(field, schema));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ internal static class WellKnownArgumentNames
public const string Path = "path";
public const string Pattern = "pattern";
public const string Policy = "policy";
public const string PropagateNull = "propagateNull";
public const string Provides = "provides";
public const string Requirements = "requirements";
public const string RequireOneSlicingArgument = "requireOneSlicingArgument";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ internal static class WellKnownDirectiveNames
public const string McpToolAnnotations = "mcpToolAnnotations";
public const string OneOf = DirectiveNames.OneOf.Name;
public const string Override = DirectiveNames.Override.Name;
public const string PropagateNull = "propagateNull";
public const string Provides = DirectiveNames.Provides.Name;
public const string Require = DirectiveNames.Require.Name;
public const string SerializeAs = DirectiveNames.SerializeAs.Name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ private static ImmutableArray<Lookup> GetLookupBySchema(
lookup.Field.Name.Value,
lookup.Field.Type.NamedType().Name.Value,
lookup.Internal,
lookup.PropagateNull,
arguments.ToImmutable(),
fields,
lookup.Path));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ directive @fusion__lookup(
field: fusion__FieldDefinition!
map: [fusion__FieldSelectionMap!]!
path: fusion__FieldSelectionPath
propagateNull: Boolean! = false
internal: Boolean! = false
) repeatable on OBJECT | INTERFACE | UNION
*/
Expand All @@ -19,6 +20,7 @@ internal sealed class LookupDirective(
FieldDefinitionNode field,
ImmutableArray<string> map,
ImmutableArray<string> path,
bool propagateNull,
bool @internal = false)
{
public SchemaKey SchemaKey { get; } = schemaKey;
Expand All @@ -31,5 +33,7 @@ internal sealed class LookupDirective(

public ImmutableArray<string> Path { get; } = path;

public bool PropagateNull { get; } = propagateNull;

public bool Internal { get; } = @internal;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public static LookupDirective Parse(DirectiveNode directive)
FieldDefinitionNode? field = null;
ImmutableArray<string>? map = null;
ImmutableArray<string>? path = null;
bool? propagateNull = null;
bool? @internal = null;

foreach (var argument in directive.Arguments)
Expand Down Expand Up @@ -52,6 +53,10 @@ public static LookupDirective Parse(DirectiveNode directive)
}
break;

case "propagateNull":
propagateNull = ((BooleanValueNode)argument.Value).Value;
break;

case "internal":
@internal = ((BooleanValueNode)argument.Value).Value;
break;
Expand Down Expand Up @@ -86,7 +91,14 @@ public static LookupDirective Parse(DirectiveNode directive)
"The `map` argument is required on the @lookup directive.");
}

return new LookupDirective(new SchemaKey(schemaKey), key, field, map.Value, path ?? [], @internal ?? false);
return new LookupDirective(
new SchemaKey(schemaKey),
key,
field,
map.Value,
path ?? [],
propagateNull ?? false,
@internal ?? false);
}

private static ImmutableArray<string> ParseMap(IValueNode value)
Expand Down
Loading