Skip to content

Commit

Permalink
[Fusion] Added pre-merge validation rule "KeyFieldsHasArgumentsRule" (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
glen-84 authored Dec 30, 2024
1 parent 5c44baa commit ecf9247
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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 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
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 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));
}
}
}

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,6 +48,9 @@
<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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ private CompositionResult<SchemaDefinition> MergeSchemaDefinitions(CompositionCo
new ExternalMissingOnBaseRule(),
new ExternalUnusedRule(),
new KeyDirectiveInFieldsArgumentRule(),
new KeyFieldsHasArgumentsRule(),
new KeyFieldsSelectInvalidTypeRule(),
new OutputFieldTypesMergeableRule(),
new RootMutationUsedRule(),
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."
]
}
};
}
}

0 comments on commit ecf9247

Please sign in to comment.