Skip to content

Commit

Permalink
[Fusion] Added pre-merge validation rule "RootMutationUsedRule" (#7865)
Browse files Browse the repository at this point in the history
  • Loading branch information
glen-84 authored Dec 24, 2024
1 parent 02e8e31 commit f035d42
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ public static class LogEntryCodes
public const string ExternalMissingOnBase = "EXTERNAL_MISSING_ON_BASE";
public const string ExternalUnused = "EXTERNAL_UNUSED";
public const string OutputFieldTypesNotMergeable = "OUTPUT_FIELD_TYPES_NOT_MERGEABLE";
public const string RootMutationUsed = "ROOT_MUTATION_USED";
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,14 @@ public static LogEntry OutputFieldTypesNotMergeable(
field,
schemaA);
}

public static LogEntry RootMutationUsed(SchemaDefinition schema)
{
return new LogEntry(
string.Format(LogEntryHelper_RootMutationUsed, schema.Name),
LogEntryCodes.RootMutationUsed,
severity: LogSeverity.Error,
member: schema,
schema: schema);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ internal record OutputFieldGroupEvent(
ImmutableArray<OutputFieldInfo> FieldGroup,
string TypeName) : IEvent;

internal record SchemaEvent(SchemaDefinition Schema) : IEvent;

internal record TypeEvent(
INamedTypeDefinition Type,
SchemaDefinition Schema) : IEvent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ private void PublishEvents(CompositionContext context)

foreach (var schema in context.SchemaDefinitions)
{
PublishEvent(new SchemaEvent(schema), context);

foreach (var type in schema.Types)
{
PublishEvent(new TypeEvent(type, schema), context);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using HotChocolate.Fusion.Events;
using static HotChocolate.Fusion.Logging.LogEntryHelper;

namespace HotChocolate.Fusion.PreMergeValidation.Rules;

/// <summary>
/// This rule enforces that, for any source schema, if a root mutation type is defined, it must be
/// named <c>Mutation</c>. Defining a root mutation type with a name other than <c>Mutation</c> or
/// using a differently named type alongside a type explicitly named <c>Mutation</c> creates
/// inconsistencies in schema design and violates the composite schema specification.
/// </summary>
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Root-Mutation-Used">
/// Specification
/// </seealso>
internal sealed class RootMutationUsedRule : IEventHandler<SchemaEvent>
{
public void Handle(SchemaEvent @event, CompositionContext context)
{
var schema = @event.Schema;
var rootMutation = schema.MutationType;

if (rootMutation is not null && rootMutation.Name != WellKnownTypeNames.Mutation)
{
context.Log.Write(RootMutationUsed(schema));
}

// An object type named 'Mutation' will be set as the root mutation type if it has not yet
// been defined, so it's not necessary to check for this type in the absence of a root
// mutation type.
}
}

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,4 +48,7 @@
<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>
<data name="LogEntryHelper_RootMutationUsed" xml:space="preserve">
<value>The root mutation type in schema '{0}' must be named 'Mutation'.</value>
</data>
</root>
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 OutputFieldTypesMergeableRule()
new OutputFieldTypesMergeableRule(),
new RootMutationUsedRule()
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace HotChocolate.Fusion;

internal static class WellKnownTypeNames
{
public const string Mutation = "Mutation";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using HotChocolate.Fusion.Logging;
using HotChocolate.Fusion.PreMergeValidation;
using HotChocolate.Fusion.PreMergeValidation.Rules;

namespace HotChocolate.Composition.PreMergeValidation.Rules;

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

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

public static TheoryData<string[]> ValidExamplesData()
{
return new TheoryData<string[]>
{
// Valid example.
{
[
"""
schema {
mutation: Mutation
}
type Mutation {
createProduct(name: String): Product
}
type Product {
id: ID!
name: String
}
"""
]
}
};
}

public static TheoryData<string[], string[]> InvalidExamplesData()
{
return new TheoryData<string[], string[]>
{
// The following example violates the rule because `RootMutation` is used as the root
// mutation type, but a type named `Mutation` is also defined.
{
[
"""
schema {
mutation: RootMutation
}
type RootMutation {
createProduct(name: String): Product
}
type Mutation {
deprecatedField: String
}
"""
],
[
"The root mutation type in schema 'A' must be named 'Mutation'."
]
}
};
}
}

0 comments on commit f035d42

Please sign in to comment.