Skip to content

Commit

Permalink
[Fusion] Added pre-merge validation rule "KeyInvalidFieldsRule" (#7874)
Browse files Browse the repository at this point in the history
  • Loading branch information
glen-84 authored Dec 30, 2024
1 parent ecf9247 commit 10c79ae
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public static class LogEntryCodes
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 @@ -204,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,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 @@ -54,6 +54,9 @@
<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 @@ -53,6 +53,7 @@ private CompositionResult<SchemaDefinition> MergeSchemaDefinitions(CompositionCo
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,154 @@
using HotChocolate.Fusion.Logging;
using HotChocolate.Fusion.PreMergeValidation;
using HotChocolate.Fusion.PreMergeValidation.Rules;

namespace HotChocolate.Composition.PreMergeValidation.Rules;

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

[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_INVALID_FIELDS"));
Assert.True(context.Log.All(e => e.Severity == LogSeverity.Error));
}

public static TheoryData<string[]> ValidExamplesData()
{
return new TheoryData<string[]>
{
// In this example, the `fields` argument of the `@key` directive is properly defined
// with valid syntax and references existing fields.
{
[
"""
type Product @key(fields: "sku featuredItem { id }") {
sku: String!
featuredItem: Node!
}
interface Node {
id: ID!
}
"""
]
}
};
}

public static TheoryData<string[], string[]> InvalidExamplesData()
{
return new TheoryData<string[], string[]>
{
// In this example, the `fields` argument of the `@key` directive references a field
// `id`, which does not exist on the `Product` type.
{
[
"""
type Product @key(fields: "id") {
sku: String!
}
"""
],
[
"An @key directive on type 'Product' in schema 'A' references field " +
"'Product.id', which does not exist."
]
},
// Nested field.
{
[
"""
type Product @key(fields: "info { category { id } }") {
info: ProductInfo!
}
type ProductInfo {
subcategory: Category
}
type Category {
name: String!
}
"""
],
[
"An @key directive on type 'Product' in schema 'A' references field " +
"'ProductInfo.category', which does not exist."
]
},
// Multiple nested fields.
{
[
"""
type Product @key(fields: "category { id name } info { id }") {
category: ProductCategory!
}
type ProductCategory {
description: String
}
type ProductInfo {
updatedAt: DateTime!
}
"""
],
[
"An @key directive on type 'Product' in schema 'A' references field " +
"'ProductCategory.id', which does not exist.",

"An @key directive on type 'Product' in schema 'A' references field " +
"'ProductCategory.name', which does not exist.",

"An @key directive on type 'Product' in schema 'A' references field " +
"'Product.info', which does not exist."
]
},
// Multiple keys.
{
[
"""
type Product @key(fields: "id") @key(fields: "name") {
sku: String!
}
"""
],
[
"An @key directive on type 'Product' in schema 'A' references field " +
"'Product.id', which does not exist.",

"An @key directive on type 'Product' in schema 'A' references field " +
"'Product.name', which does not exist."
]
}
};
}
}

0 comments on commit 10c79ae

Please sign in to comment.