Skip to content

Commit

Permalink
[Fusion] Added pre-merge validation rule "QueryRootTypeInaccessibleRu…
Browse files Browse the repository at this point in the history
…le" (#7886)
  • Loading branch information
glen-84 authored Jan 1, 2025
1 parent 027dc0b commit f7096e8
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public static class LogEntryCodes
public const string ProvidesDirectiveInFieldsArg = "PROVIDES_DIRECTIVE_IN_FIELDS_ARG";
public const string ProvidesFieldsHasArgs = "PROVIDES_FIELDS_HAS_ARGS";
public const string ProvidesFieldsMissingExternal = "PROVIDES_FIELDS_MISSING_EXTERNAL";
public const string QueryRootTypeInaccessible = "QUERY_ROOT_TYPE_INACCESSIBLE";
public const string RootMutationUsed = "ROOT_MUTATION_USED";
public const string RootQueryUsed = "ROOT_QUERY_USED";
public const string RootSubscriptionUsed = "ROOT_SUBSCRIPTION_USED";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,19 @@ public static LogEntry ProvidesFieldsMissingExternal(
schema);
}

public static LogEntry QueryRootTypeInaccessible(
INamedTypeDefinition type,
SchemaDefinition schema)
{
return new LogEntry(
string.Format(LogEntryHelper_QueryRootTypeInaccessible, schema.Name),
LogEntryCodes.QueryRootTypeInaccessible,
LogSeverity.Error,
new SchemaCoordinate(type.Name),
type,
schema);
}

public static LogEntry RootMutationUsed(SchemaDefinition schema)
{
return new LogEntry(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using HotChocolate.Fusion.Events;
using static HotChocolate.Fusion.Logging.LogEntryHelper;

namespace HotChocolate.Fusion.PreMergeValidation.Rules;

/// <summary>
/// Every source schema that contributes to the final composite schema must expose a public
/// (accessible) root query type. Marking the root query type as <c>@inaccessible</c> makes it
/// invisible to the gateway, defeating its purpose as the primary entry point for queries and
/// lookups.
/// </summary>
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Query-Root-Type-Inaccessible">
/// Specification
/// </seealso>
internal sealed class QueryRootTypeInaccessibleRule : IEventHandler<SchemaEvent>
{
public void Handle(SchemaEvent @event, CompositionContext context)
{
var schema = @event.Schema;
var rootQuery = schema.QueryType;

if (rootQuery is not null && !ValidationHelper.IsAccessible(rootQuery))
{
context.Log.Write(QueryRootTypeInaccessible(rootQuery, 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 @@ -75,6 +75,9 @@
<data name="LogEntryHelper_ProvidesFieldsMissingExternal" xml:space="preserve">
<value>The @provides directive on field '{0}' in schema '{1}' references field '{2}', which must be marked as external.</value>
</data>
<data name="LogEntryHelper_QueryRootTypeInaccessible" xml:space="preserve">
<value>The root query type in schema '{0}' must be accessible.</value>
</data>
<data name="LogEntryHelper_RootMutationUsed" xml:space="preserve">
<value>The root mutation type in schema '{0}' must be named 'Mutation'.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ private CompositionResult<SchemaDefinition> MergeSchemaDefinitions(CompositionCo
new ProvidesDirectiveInFieldsArgumentRule(),
new ProvidesFieldsHasArgumentsRule(),
new ProvidesFieldsMissingExternalRule(),
new QueryRootTypeInaccessibleRule(),
new RootMutationUsedRule(),
new RootQueryUsedRule(),
new RootSubscriptionUsedRule()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using HotChocolate.Fusion.Logging;
using HotChocolate.Fusion.PreMergeValidation;
using HotChocolate.Fusion.PreMergeValidation.Rules;

namespace HotChocolate.Composition.PreMergeValidation.Rules;

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

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

public static TheoryData<string[]> ValidExamplesData()
{
return new TheoryData<string[]>
{
// In this example, no @inaccessible annotation is applied to the query root, so the
// rule is satisfied.
{
[
"""
extend schema {
query: Query
}
type Query {
allBooks: [Book]
}
type Book {
id: ID!
title: String
}
"""
]
}
};
}

public static TheoryData<string[], string[]> InvalidExamplesData()
{
return new TheoryData<string[], string[]>
{
// Since the schema marks the query root type as @inaccessible, the rule is violated.
// QUERY_ROOT_TYPE_INACCESSIBLE is raised because a schema’s root query type cannot be
// hidden from consumers.
{
[
"""
extend schema {
query: Query
}
type Query @inaccessible {
allBooks: [Book]
}
type Book {
id: ID!
title: String
}
"""
],
[
"The root query type in schema 'A' must be accessible."
]
}
};
}
}

0 comments on commit f7096e8

Please sign in to comment.