Skip to content

Commit

Permalink
[Fusion] Added pre-merge validation rule "KeyFieldsSelectInvalidTypeR…
Browse files Browse the repository at this point in the history
…ule" (#7871)
  • Loading branch information
glen-84 authored Dec 27, 2024
1 parent 538621a commit a265c91
Show file tree
Hide file tree
Showing 11 changed files with 395 additions and 0 deletions.
1 change: 1 addition & 0 deletions dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ Storeless
strawberryshake
streamable
Structs
subgraphs
sublicensable
supergraph
Swashbuckle
Expand Down
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 KeyFieldsSelectInvalidType = "KEY_FIELDS_SELECT_INVALID_TYPE";
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 @@ -144,6 +144,26 @@ public static LogEntry ExternalUnused(
schema);
}

public static LogEntry KeyFieldsSelectInvalidType(
string entityTypeName,
Directive keyDirective,
string fieldName,
string typeName,
SchemaDefinition schema)
{
return new LogEntry(
string.Format(
LogEntryHelper_KeyFieldsSelectInvalidType,
entityTypeName,
schema.Name,
new SchemaCoordinate(typeName, fieldName)),
LogEntryCodes.KeyFieldsSelectInvalidType,
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 @@ -25,6 +25,13 @@ internal record FieldArgumentGroupEvent(
string FieldName,
string TypeName) : IEvent;

internal record KeyFieldEvent(
ComplexTypeDefinition EntityType,
Directive KeyDirective,
OutputFieldDefinition Field,
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 @@ -3,7 +3,9 @@
using HotChocolate.Fusion.Errors;
using HotChocolate.Fusion.Events;
using HotChocolate.Fusion.Results;
using HotChocolate.Language;
using HotChocolate.Skimmed;
using static HotChocolate.Language.Utf8GraphQLParser;

namespace HotChocolate.Fusion.PreMergeValidation;

Expand Down Expand Up @@ -36,6 +38,11 @@ private void PublishEvents(CompositionContext context)

if (type is ComplexTypeDefinition complexType)
{
if (complexType.Directives.ContainsName(WellKnownDirectiveNames.Key))
{
PublishEntityEvents(complexType, schema, context);
}

foreach (var field in complexType.Fields)
{
PublishEvent(new OutputFieldEvent(field, type, schema), context);
Expand Down Expand Up @@ -108,6 +115,87 @@ private void PublishEvents(CompositionContext context)
}
}

private void PublishEntityEvents(
ComplexTypeDefinition entityType,
SchemaDefinition schema,
CompositionContext context)
{
var keyDirectives =
entityType.Directives
.Where(d => d.Name == WellKnownDirectiveNames.Key)
.ToArray();

foreach (var keyDirective in keyDirectives)
{
if (
!keyDirective.Arguments.TryGetValue(WellKnownArgumentNames.Fields, out var f)
|| f is not StringValueNode fields)
{
continue;
}

try
{
var selectionSet = Syntax.ParseSelectionSet($"{{{fields.Value}}}");

PublishKeyFieldEvents(
selectionSet,
entityType,
keyDirective,
entityType,
schema,
context);
}
catch (SyntaxException)
{
// Ignore.
}
}
}

private void PublishKeyFieldEvents(
SelectionSetNode selectionSet,
ComplexTypeDefinition entityType,
Directive keyDirective,
ComplexTypeDefinition parentType,
SchemaDefinition schema,
CompositionContext context)
{
foreach (var selection in selectionSet.Selections)
{
if (selection is FieldNode fieldNode)
{
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)
{
parentType = fieldType;
}
}

if (fieldNode.SelectionSet is not null)
{
PublishKeyFieldEvents(
fieldNode.SelectionSet,
entityType,
keyDirective,
parentType,
schema,
context);
}
}
}
}

private void PublishEvent<TEvent>(TEvent @event, CompositionContext context)
where TEvent : IEvent
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using HotChocolate.Fusion.Events;
using HotChocolate.Skimmed;
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 reference scalars or object types to ensure a valid and consistent
/// representation of the entity across subgraphs. Fields of types <c>List</c>, <c>Interface</c>, or
/// <c>Union</c> cannot be part of a <c>@key</c> because they do not have a well-defined unique
/// value.
/// </summary>
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Key-Fields-Select-Invalid-Type">
/// Specification
/// </seealso>
internal sealed class KeyFieldsSelectInvalidTypeRule : IEventHandler<KeyFieldEvent>
{
public void Handle(KeyFieldEvent @event, CompositionContext context)
{
var (entityType, keyDirective, field, type, schema) = @event;

var fieldType = field.Type.NullableType();

if (fieldType is InterfaceTypeDefinition or ListTypeDefinition or UnionTypeDefinition)
{
context.Log.Write(
KeyFieldsSelectInvalidType(
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 @@ -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_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_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 @@ -50,6 +50,7 @@ private CompositionResult<SchemaDefinition> MergeSchemaDefinitions(CompositionCo
new ExternalArgumentDefaultMismatchRule(),
new ExternalMissingOnBaseRule(),
new ExternalUnusedRule(),
new KeyFieldsSelectInvalidTypeRule(),
new OutputFieldTypesMergeableRule(),
new RootMutationUsedRule(),
new RootQueryUsedRule(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ internal static class WellKnownDirectiveNames
{
public const string External = "external";
public const string Inaccessible = "inaccessible";
public const string Key = "key";
public const string Provides = "provides";
}
Loading

0 comments on commit a265c91

Please sign in to comment.