Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fusion] Added post-merge validation rule "NoQueriesRule" #7988

Merged
merged 1 commit into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public static class LogEntryCodes
public const string KeyInvalidSyntax = "KEY_INVALID_SYNTAX";
public const string LookupReturnsList = "LOOKUP_RETURNS_LIST";
public const string LookupReturnsNonNullableType = "LOOKUP_RETURNS_NON_NULLABLE_TYPE";
public const string NoQueries = "NO_QUERIES";
public const string OutputFieldTypesNotMergeable = "OUTPUT_FIELD_TYPES_NOT_MERGEABLE";
public const string OverrideFromSelf = "OVERRIDE_FROM_SELF";
public const string OverrideOnInterface = "OVERRIDE_ON_INTERFACE";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,17 @@ public static LogEntry LookupReturnsNonNullableType(
schema);
}

public static LogEntry NoQueries(ObjectTypeDefinition queryType, SchemaDefinition schema)
{
return new LogEntry(
string.Format(LogEntryHelper_NoQueries),
LogEntryCodes.NoQueries,
LogSeverity.Error,
new SchemaCoordinate(queryType.Name),
queryType,
schema);
}

public static LogEntry OutputFieldTypesNotMergeable(
OutputFieldDefinition field,
string typeName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using HotChocolate.Fusion.Events;
using HotChocolate.Fusion.Events.Contracts;
using HotChocolate.Fusion.Extensions;
using static HotChocolate.Fusion.Logging.LogEntryHelper;

namespace HotChocolate.Fusion.PostMergeValidationRules;

/// <summary>
/// <para>
/// This rule ensures that the composed schema includes at least one accessible field on the root
/// <c>Query</c> type.
/// </para>
/// <para>
/// In GraphQL, the <c>Query</c> type is essential as it defines the entry points for read
/// operations. If none of the composed schemas expose any query fields, the composed schema would
/// lack a root query, making it an invalid GraphQL schema.
/// </para>
/// </summary>
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-No-Queries">
/// Specification
/// </seealso>
internal sealed class NoQueriesRule : IEventHandler<ObjectTypeEvent>
{
public void Handle(ObjectTypeEvent @event, CompositionContext context)
{
var (objectType, schema) = @event;

if (objectType != schema.QueryType)
{
return;
}

var accessibleFields = objectType.Fields.Where(f => !f.HasInaccessibleDirective());

if (!accessibleFields.Any())
{
context.Log.Write(NoQueries(objectType, 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 @@ -114,6 +114,9 @@
<data name="LogEntryHelper_LookupReturnsNonNullableType" xml:space="preserve">
<value>The lookup field '{0}' in schema '{1}' should return a nullable type.</value>
</data>
<data name="LogEntryHelper_NoQueries" xml:space="preserve">
<value>The merged query type has no accessible fields.</value>
</data>
<data name="LogEntryHelper_OutputFieldTypesNotMergeable" xml:space="preserve">
<value>The output 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 @@ -123,6 +123,7 @@ public CompositionResult<SchemaDefinition> Compose()
new EmptyMergedEnumTypeRule(),
new EmptyMergedInterfaceTypeRule(),
new EmptyMergedObjectTypeRule(),
new EmptyMergedUnionTypeRule()
new EmptyMergedUnionTypeRule(),
new NoQueriesRule()
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
using System.Collections.Immutable;
using HotChocolate.Fusion.Logging;

namespace HotChocolate.Fusion.PostMergeValidationRules;

public sealed class NoQueriesRuleTests : CompositionTestBase
{
private static readonly object s_rule = new NoQueriesRule();
private static readonly ImmutableArray<object> s_rules = [s_rule];
private readonly CompositionLog _log = new();

[Theory]
[MemberData(nameof(ValidExamplesData))]
public void Examples_Valid(string[] sdl)
{
// arrange
var schemas = CreateSchemaDefinitions(sdl);
var merger = new SourceSchemaMerger(schemas);
var mergeResult = merger.Merge();
var validator = new PostMergeValidator(mergeResult.Value, s_rules, schemas, _log);

// act
var result = validator.Validate();

// assert
Assert.True(result.IsSuccess);
Assert.True(_log.IsEmpty);
}

[Theory]
[MemberData(nameof(InvalidExamplesData))]
public void Examples_Invalid(string[] sdl, string[] errorMessages)
{
// arrange
var schemas = CreateSchemaDefinitions(sdl);
var merger = new SourceSchemaMerger(schemas);
var mergeResult = merger.Merge();
var validator = new PostMergeValidator(mergeResult.Value, s_rules, schemas, _log);

// act
var result = validator.Validate();

// assert
Assert.True(result.IsFailure);
Assert.Equal(errorMessages, _log.Select(e => e.Message).ToArray());
Assert.True(_log.All(e => e.Code == "NO_QUERIES"));
Assert.True(_log.All(e => e.Severity == LogSeverity.Error));
}

public static TheoryData<string[]> ValidExamplesData()
{
return new TheoryData<string[]>
{
// In this example, at least one schema provides accessible query fields, satisfying the
// rule.
{
[
"""
# Schema A
type Query {
product(id: ID!): Product
}

type Product {
id: ID!
}
""",
"""
# Schema B
type Query {
review(id: ID!): Review
}

type Review {
id: ID!
content: String
rating: Int
}
"""
]
},
// Even if some query fields are marked as @inaccessible, as long as there is at least
// one accessible query field in the composed schema, the rule is satisfied.
{
[
"""
# Schema A
type Query {
internalData: InternalData @inaccessible
}

type InternalData {
secret: String
}
""",
"""
# Schema B
type Query {
product(id: ID!): Product
}

type Product {
id: ID!
name: String
}
"""
]
}
};
}

public static TheoryData<string[], string[]> InvalidExamplesData()
{
return new TheoryData<string[], string[]>
{
// If all query fields in all schemas are marked as @inaccessible, the composed schema
// will lack accessible query fields, violating the rule.
{
[
"""
# Schema A
type Query {
internalData: InternalData @inaccessible
}

type InternalData {
secret: String
}
""",
"""
# Schema B
type Query {
adminStats: AdminStats @inaccessible
}

type AdminStats {
userCount: Int
}
"""
],
[
"The merged query type has no accessible fields."
]
}
};
}
}
Loading