Skip to content

Commit

Permalink
[Fusion] Added pre-merge validation rule "KeyDirectiveInFieldsArgumen…
Browse files Browse the repository at this point in the history
…tRule" (#7872)
  • Loading branch information
glen-84 authored Dec 30, 2024
1 parent a265c91 commit 5c44baa
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public static class LogEntryCodes
public const string ExternalArgumentDefaultMismatch = "EXTERNAL_ARGUMENT_DEFAULT_MISMATCH";
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 KeyFieldsSelectInvalidType = "KEY_FIELDS_SELECT_INVALID_TYPE";
public const string OutputFieldTypesNotMergeable = "OUTPUT_FIELD_TYPES_NOT_MERGEABLE";
public const string RootMutationUsed = "ROOT_MUTATION_USED";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Immutable;
using HotChocolate.Skimmed;
using static HotChocolate.Fusion.Properties.CompositionResources;

Expand Down Expand Up @@ -144,6 +145,25 @@ public static LogEntry ExternalUnused(
schema);
}

public static LogEntry KeyDirectiveInFieldsArgument(
string entityTypeName,
Directive keyDirective,
ImmutableArray<string> fieldNamePath,
SchemaDefinition schema)
{
return new LogEntry(
string.Format(
LogEntryHelper_KeyDirectiveInFieldsArgument,
entityTypeName,
schema.Name,
string.Join(".", fieldNamePath)),
LogEntryCodes.KeyDirectiveInFieldsArg,
LogSeverity.Error,
new SchemaCoordinate(entityTypeName),
keyDirective,
schema);
}

public static LogEntry KeyFieldsSelectInvalidType(
string entityTypeName,
Directive keyDirective,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Immutable;
using HotChocolate.Fusion.Events;
using HotChocolate.Language;
using HotChocolate.Skimmed;

namespace HotChocolate.Fusion.PreMergeValidation;
Expand Down Expand Up @@ -32,6 +33,13 @@ internal record KeyFieldEvent(
ComplexTypeDefinition Type,
SchemaDefinition Schema) : IEvent;

internal record KeyFieldNodeEvent(
ComplexTypeDefinition EntityType,
Directive KeyDirective,
FieldNode FieldNode,
ImmutableArray<string> FieldNamePath,
SchemaDefinition Schema) : IEvent;

internal record OutputFieldEvent(
OutputFieldDefinition Field,
INamedTypeDefinition Type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ private void PublishEntityEvents(
selectionSet,
entityType,
keyDirective,
[],
entityType,
schema,
context);
Expand All @@ -157,6 +158,7 @@ private void PublishKeyFieldEvents(
SelectionSetNode selectionSet,
ComplexTypeDefinition entityType,
Directive keyDirective,
List<string> fieldNamePath,
ComplexTypeDefinition parentType,
SchemaDefinition schema,
CompositionContext context)
Expand All @@ -165,6 +167,17 @@ private void PublishKeyFieldEvents(
{
if (selection is FieldNode fieldNode)
{
fieldNamePath.Add(fieldNode.Name.Value);

PublishEvent(
new KeyFieldNodeEvent(
entityType,
keyDirective,
fieldNode,
[.. fieldNamePath],
schema),
context);

if (parentType.Fields.TryGetField(fieldNode.Name.Value, out var field))
{
PublishEvent(
Expand All @@ -188,10 +201,13 @@ private void PublishKeyFieldEvents(
fieldNode.SelectionSet,
entityType,
keyDirective,
fieldNamePath,
parentType,
schema,
context);
}

fieldNamePath = [];
}
}
}
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>
/// The <c>@key</c> directive specifies the set of fields used to uniquely identify an entity. The
/// <c>fields</c> argument must consist of a valid GraphQL selection set that does not include any
/// directive applications. Directives in the <c>fields</c> argument are not supported.
/// </summary>
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Key-Directive-in-Fields-Argument">
/// Specification
/// </seealso>
internal sealed class KeyDirectiveInFieldsArgumentRule : IEventHandler<KeyFieldNodeEvent>
{
public void Handle(KeyFieldNodeEvent @event, CompositionContext context)
{
var (entityType, keyDirective, fieldNode, fieldNamePath, schema) = @event;

if (fieldNode.Directives.Count != 0)
{
context.Log.Write(
KeyDirectiveInFieldsArgument(
entityType.Name,
keyDirective,
fieldNamePath,
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 @@ -45,6 +45,9 @@
<data name="LogEntryHelper_ExternalUnused" xml:space="preserve">
<value>External field '{0}' in schema '{1}' is not referenced by an @provides directive in the schema.</value>
</data>
<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_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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ private CompositionResult<SchemaDefinition> MergeSchemaDefinitions(CompositionCo
new ExternalArgumentDefaultMismatchRule(),
new ExternalMissingOnBaseRule(),
new ExternalUnusedRule(),
new KeyDirectiveInFieldsArgumentRule(),
new KeyFieldsSelectInvalidTypeRule(),
new OutputFieldTypesMergeableRule(),
new RootMutationUsedRule(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using HotChocolate.Fusion.Logging;
using HotChocolate.Fusion.PreMergeValidation;
using HotChocolate.Fusion.PreMergeValidation.Rules;

namespace HotChocolate.Composition.PreMergeValidation.Rules;

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

[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_DIRECTIVE_IN_FIELDS_ARG"));
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 does not include any
// directive applications, satisfying the rule.
{
[
"""
type User @key(fields: "id name") {
id: ID!
name: String
}
"""
]
}
};
}

public static TheoryData<string[], string[]> InvalidExamplesData()
{
return new TheoryData<string[], string[]>
{
// In this example, the `fields` argument of the `@key` directive includes a directive
// application `@lowercase`, which is not allowed.
{
[
"""
directive @lowercase on FIELD_DEFINITION
type User @key(fields: "id name @lowercase") {
id: ID!
name: String
}
"""
],
[
"An @key directive on type 'User' in schema 'A' references field 'name', " +
"which must not include directive applications."
]
},
// In this example, the `fields` argument includes a directive application `@lowercase`
// nested inside the selection set, which is also invalid.
{
[
"""
directive @lowercase on FIELD_DEFINITION
type User @key(fields: "id name { firstName @lowercase }") {
id: ID!
name: FullName
}
type FullName {
firstName: String
lastName: String
}
"""
],
[
"An @key directive on type 'User' in schema 'A' references field " +
"'name.firstName', which must not include directive applications."
]
},
// Multiple keys.
{
[
"""
directive @example on FIELD_DEFINITION
type User @key(fields: "id @example") @key(fields: "name @example") {
id: ID!
name: String
}
"""
],
[
"An @key directive on type 'User' in schema 'A' references field 'id', " +
"which must not include directive applications.",

"An @key directive on type 'User' in schema 'A' references field 'name', " +
"which must not include directive applications."
]
}
};
}
}

0 comments on commit 5c44baa

Please sign in to comment.