Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib authored Dec 30, 2024
2 parents efe1996 + 10c79ae commit 6a40bd1
Show file tree
Hide file tree
Showing 11 changed files with 446 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ public static class LogEntryCodes
public const string ExternalMissingOnBase = "EXTERNAL_MISSING_ON_BASE";
public const string ExternalUnused = "EXTERNAL_UNUSED";
public const string KeyDirectiveInFieldsArg = "KEY_DIRECTIVE_IN_FIELDS_ARG";
public const string KeyFieldsHasArgs = "KEY_FIELDS_HAS_ARGS";
public const string KeyFieldsSelectInvalidType = "KEY_FIELDS_SELECT_INVALID_TYPE";
public const string KeyInvalidFields = "KEY_INVALID_FIELDS";
public const string OutputFieldTypesNotMergeable = "OUTPUT_FIELD_TYPES_NOT_MERGEABLE";
public const string RootMutationUsed = "ROOT_MUTATION_USED";
public const string RootQueryUsed = "ROOT_QUERY_USED";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,26 @@ public static LogEntry KeyDirectiveInFieldsArgument(
schema);
}

public static LogEntry KeyFieldsHasArguments(
string entityTypeName,
Directive keyDirective,
string fieldName,
string typeName,
SchemaDefinition schema)
{
return new LogEntry(
string.Format(
LogEntryHelper_KeyFieldsHasArguments,
entityTypeName,
schema.Name,
new SchemaCoordinate(typeName, fieldName)),
LogEntryCodes.KeyFieldsHasArgs,
LogSeverity.Error,
new SchemaCoordinate(entityTypeName),
keyDirective,
schema);
}

public static LogEntry KeyFieldsSelectInvalidType(
string entityTypeName,
Directive keyDirective,
Expand All @@ -184,6 +204,26 @@ public static LogEntry KeyFieldsSelectInvalidType(
schema);
}

public static LogEntry KeyInvalidFields(
string entityTypeName,
Directive keyDirective,
string fieldName,
string typeName,
SchemaDefinition schema)
{
return new LogEntry(
string.Format(
LogEntryHelper_KeyInvalidFields,
entityTypeName,
schema.Name,
new SchemaCoordinate(typeName, fieldName)),
LogEntryCodes.KeyInvalidFields,
LogSeverity.Error,
new SchemaCoordinate(entityTypeName),
keyDirective,
schema);
}

public static LogEntry OutputFieldTypesNotMergeable(
OutputFieldDefinition field,
string typeName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ internal record KeyFieldNodeEvent(
ImmutableArray<string> FieldNamePath,
SchemaDefinition Schema) : IEvent;

internal record KeyFieldsInvalidReferenceEvent(
ComplexTypeDefinition EntityType,
Directive KeyDirective,
FieldNode FieldNode,
ComplexTypeDefinition Type,
SchemaDefinition Schema) : IEvent;

internal record OutputFieldEvent(
OutputFieldDefinition Field,
INamedTypeDefinition Type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,12 @@ private void PublishKeyFieldEvents(
ComplexTypeDefinition entityType,
Directive keyDirective,
List<string> fieldNamePath,
ComplexTypeDefinition parentType,
ComplexTypeDefinition? parentType,
SchemaDefinition schema,
CompositionContext context)
{
ComplexTypeDefinition? nextParentType = null;

foreach (var selection in selectionSet.Selections)
{
if (selection is FieldNode fieldNode)
Expand All @@ -178,20 +180,36 @@ private void PublishKeyFieldEvents(
schema),
context);

if (parentType.Fields.TryGetField(fieldNode.Name.Value, out var field))
if (parentType is not null)
{
PublishEvent(
new KeyFieldEvent(
entityType,
keyDirective,
field,
parentType,
schema),
context);

if (field.Type.NullableType() is ComplexTypeDefinition fieldType)
if (parentType.Fields.TryGetField(fieldNode.Name.Value, out var field))
{
PublishEvent(
new KeyFieldEvent(
entityType,
keyDirective,
field,
parentType,
schema),
context);

if (field.Type.NullableType() is ComplexTypeDefinition fieldType)
{
nextParentType = fieldType;
}
}
else
{
parentType = fieldType;
PublishEvent(
new KeyFieldsInvalidReferenceEvent(
entityType,
keyDirective,
fieldNode,
parentType,
schema),
context);

nextParentType = null;
}
}

Expand All @@ -202,7 +220,7 @@ private void PublishKeyFieldEvents(
entityType,
keyDirective,
fieldNamePath,
parentType,
nextParentType,
schema,
context);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using HotChocolate.Fusion.Events;
using static HotChocolate.Fusion.Logging.LogEntryHelper;

namespace HotChocolate.Fusion.PreMergeValidation.Rules;

/// <summary>
/// The <c>@key</c> directive is used to define the set of fields that uniquely identify an entity.
/// These fields must not include any field that is defined with arguments, as arguments introduce
/// variability that prevents consistent and valid entity resolution across subgraphs. Fields
/// included in the <c>fields</c> argument of the <c>@key</c> directive must be static and
/// consistently resolvable.
/// </summary>
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Key-Fields-Has-Arguments">
/// Specification
/// </seealso>
internal sealed class KeyFieldsHasArgumentsRule : IEventHandler<KeyFieldEvent>
{
public void Handle(KeyFieldEvent @event, CompositionContext context)
{
var (entityType, keyDirective, field, type, schema) = @event;

if (field.Arguments.Count != 0)
{
context.Log.Write(
KeyFieldsHasArguments(
entityType.Name,
keyDirective,
field.Name,
type.Name,
schema));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using HotChocolate.Fusion.Events;
using static HotChocolate.Fusion.Logging.LogEntryHelper;

namespace HotChocolate.Fusion.PreMergeValidation.Rules;

/// <summary>
/// Even if the selection set for <c>@key(fields: "…")</c> is syntactically valid, field references
/// within that selection set must also refer to <b>actual</b> fields on the annotated type. This
/// includes nested selections, which must appear on the corresponding return type. If any
/// referenced field is missing or incorrectly named, composition fails with a
/// <c>KEY_INVALID_FIELDS</c> error because the entity key cannot be resolved correctly.
/// </summary>
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Key-Invalid-Fields">
/// Specification
/// </seealso>
internal sealed class KeyInvalidFieldsRule : IEventHandler<KeyFieldsInvalidReferenceEvent>
{
public void Handle(KeyFieldsInvalidReferenceEvent @event, CompositionContext context)
{
var (entityType, keyDirective, fieldNode, type, schema) = @event;

context.Log.Write(
KeyInvalidFields(
entityType.Name,
keyDirective,
fieldNode.Name.Value,
type.Name,
schema));
}
}

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 @@ -48,9 +48,15 @@
<data name="LogEntryHelper_KeyDirectiveInFieldsArgument" xml:space="preserve">
<value>An @key directive on type '{0}' in schema '{1}' references field '{2}', which must not include directive applications.</value>
</data>
<data name="LogEntryHelper_KeyFieldsHasArguments" xml:space="preserve">
<value>An @key directive on type '{0}' in schema '{1}' references field '{2}', which must not have arguments.</value>
</data>
<data name="LogEntryHelper_KeyFieldsSelectInvalidType" xml:space="preserve">
<value>An @key directive on type '{0}' in schema '{1}' references field '{2}', which must not be a list, interface, or union type.</value>
</data>
<data name="LogEntryHelper_KeyInvalidFields" xml:space="preserve">
<value>An @key directive on type '{0}' in schema '{1}' references field '{2}', which does not exist.</value>
</data>
<data name="LogEntryHelper_OutputFieldTypesNotMergeable" xml:space="preserve">
<value>Field '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ private CompositionResult<SchemaDefinition> MergeSchemaDefinitions(CompositionCo
new ExternalMissingOnBaseRule(),
new ExternalUnusedRule(),
new KeyDirectiveInFieldsArgumentRule(),
new KeyFieldsHasArgumentsRule(),
new KeyFieldsSelectInvalidTypeRule(),
new KeyInvalidFieldsRule(),
new OutputFieldTypesMergeableRule(),
new RootMutationUsedRule(),
new RootQueryUsedRule(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using HotChocolate.Fusion.Logging;
using HotChocolate.Fusion.PreMergeValidation;
using HotChocolate.Fusion.PreMergeValidation.Rules;

namespace HotChocolate.Composition.PreMergeValidation.Rules;

public sealed class KeyFieldsHasArgumentsRuleTests : CompositionTestBase
{
private readonly PreMergeValidator _preMergeValidator = new([new KeyFieldsHasArgumentsRule()]);

[Theory]
[MemberData(nameof(ValidExamplesData))]
public void Examples_Valid(string[] sdl)
{
// arrange
var context = CreateCompositionContext(sdl);

// act
var result = _preMergeValidator.Validate(context);

// assert
Assert.True(result.IsSuccess);
Assert.True(context.Log.IsEmpty);
}

[Theory]
[MemberData(nameof(InvalidExamplesData))]
public void Examples_Invalid(string[] sdl, string[] errorMessages)
{
// arrange
var context = CreateCompositionContext(sdl);

// act
var result = _preMergeValidator.Validate(context);

// assert
Assert.True(result.IsFailure);
Assert.Equal(errorMessages, context.Log.Select(e => e.Message).ToArray());
Assert.True(context.Log.All(e => e.Code == "KEY_FIELDS_HAS_ARGS"));
Assert.True(context.Log.All(e => e.Severity == LogSeverity.Error));
}

public static TheoryData<string[]> ValidExamplesData()
{
return new TheoryData<string[]>
{
// In this example, the `User` type has a valid `@key` directive that references the
// argument-free fields `id` and `name`.
{
[
"""
type User @key(fields: "id name") {
id: ID!
name: String
tags: [String]
}
"""
]
}
};
}

public static TheoryData<string[], string[]> InvalidExamplesData()
{
return new TheoryData<string[], string[]>
{
// In this example, the `@key` directive references a field (`tags`) that is defined
// with arguments (`limit`), which is not allowed.
{
[
"""
type User @key(fields: "id tags") {
id: ID!
tags(limit: Int = 10): [String]
}
"""
],
[
"An @key directive on type 'User' in schema 'A' references field " +
"'User.tags', which must not have arguments."
]
},
// Nested field.
{
[
"""
type User @key(fields: "id info { tags }") {
id: ID!
info: UserInfo
}
type UserInfo {
tags(limit: Int = 10): [String]
}
"""
],
[
"An @key directive on type 'User' in schema 'A' references field " +
"'UserInfo.tags', which must not have arguments."
]
},
// Multiple keys.
{
[
"""
type User @key(fields: "id") @key(fields: "tags") {
id(global: Boolean = true): ID!
tags(limit: Int = 10): [String]
}
"""
],
[
"An @key directive on type 'User' in schema 'A' references field " +
"'User.id', which must not have arguments.",

"An @key directive on type 'User' in schema 'A' references field " +
"'User.tags', which must not have arguments."
]
}
};
}
}
Loading

0 comments on commit 6a40bd1

Please sign in to comment.