Skip to content

Commit

Permalink
[Fusion] Added pre-merge validation rule "ProvidesInvalidFieldsTypeRu…
Browse files Browse the repository at this point in the history
…le" (#7914)
  • Loading branch information
glen-84 authored Jan 8, 2025
1 parent 97fade0 commit dbe8b36
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,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 ProvidesInvalidFieldsType = "PROVIDES_INVALID_FIELDS_TYPE";
public const string ProvidesInvalidSyntax = "PROVIDES_INVALID_SYNTAX";
public const string ProvidesOnNonCompositeField = "PROVIDES_ON_NON_COMPOSITE_FIELD";
public const string QueryRootTypeInaccessible = "QUERY_ROOT_TYPE_INACCESSIBLE";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,23 @@ public static LogEntry ProvidesFieldsMissingExternal(
schema);
}

public static LogEntry ProvidesInvalidFieldsType(
Directive providesDirective,
string fieldName,
string typeName,
SchemaDefinition schema)
{
var coordinate = new SchemaCoordinate(typeName, fieldName);

return new LogEntry(
string.Format(LogEntryHelper_ProvidesInvalidFieldsType, coordinate, schema.Name),
LogEntryCodes.ProvidesInvalidFieldsType,
LogSeverity.Error,
coordinate,
providesDirective,
schema);
}

public static LogEntry ProvidesInvalidSyntax(
Directive providesDirective,
string fieldName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ internal record ProvidesFieldsInvalidSyntaxEvent(
ComplexTypeDefinition Type,
SchemaDefinition Schema) : IEvent;

internal record ProvidesFieldsInvalidTypeEvent(
Directive ProvidesDirective,
OutputFieldDefinition Field,
ComplexTypeDefinition Type,
SchemaDefinition Schema) : IEvent;

internal record RequireFieldNodeEvent(
FieldNode FieldNode,
ImmutableArray<string> FieldNamePath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,10 @@ private void PublishProvidesEvents(
!providesDirective.Arguments.TryGetValue(WellKnownArgumentNames.Fields, out var f)
|| f is not StringValueNode fields)
{
PublishEvent(
new ProvidesFieldsInvalidTypeEvent(providesDirective, field, type, schema),
context);

return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using HotChocolate.Fusion.Events;
using static HotChocolate.Fusion.Logging.LogEntryHelper;

namespace HotChocolate.Fusion.PreMergeValidation.Rules;

/// <summary>
/// The <c>@provides</c> directive indicates that a field is <b>providing</b> one or more additional
/// fields on the returned (child) type. The <c>fields</c> argument accepts a <b>string</b>
/// representing a GraphQL selection set (for example, <c>"title author"</c>). If the <c>fields</c>
/// argument is given as a non-string type (e.g., <c>Boolean</c>, <c>Int</c>, <c>Array</c>), the
/// schema fails to compose because it cannot interpret a valid selection set.
/// </summary>
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Provides-Invalid-Fields-Type">
/// Specification
/// </seealso>
internal sealed class ProvidesInvalidFieldsTypeRule : IEventHandler<ProvidesFieldsInvalidTypeEvent>
{
public void Handle(ProvidesFieldsInvalidTypeEvent @event, CompositionContext context)
{
var (providesDirective, field, type, schema) = @event;

context.Log.Write(
ProvidesInvalidFieldsType(providesDirective, 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 @@ -99,6 +99,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_ProvidesInvalidFieldsType" xml:space="preserve">
<value>The @provides directive on field '{0}' in schema '{1}' must specify a string value for the 'fields' argument.</value>
</data>
<data name="LogEntryHelper_ProvidesInvalidSyntax" xml:space="preserve">
<value>The @provides directive on field '{0}' in schema '{1}' contains invalid syntax in the 'fields' argument.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ private CompositionResult<SchemaDefinition> MergeSchemaDefinitions(CompositionCo
new ProvidesDirectiveInFieldsArgumentRule(),
new ProvidesFieldsHasArgumentsRule(),
new ProvidesFieldsMissingExternalRule(),
new ProvidesInvalidFieldsTypeRule(),
new ProvidesInvalidSyntaxRule(),
new ProvidesOnNonCompositeFieldRule(),
new QueryRootTypeInaccessibleRule(),
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 ProvidesInvalidFieldsTypeRuleTests : CompositionTestBase
{
private readonly PreMergeValidator _preMergeValidator =
new([new ProvidesInvalidFieldsTypeRule()]);

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

public static TheoryData<string[]> ValidExamplesData()
{
return new TheoryData<string[]>
{
// In this example, the @provides directive on "details" uses the string
// "features specifications" to specify that both fields are provided in the child type
// "ProductDetails".
{
[
"""
type Product {
id: ID!
details: ProductDetails @provides(fields: "features specifications")
}
type ProductDetails {
features: [String]
specifications: String
}
type Query {
products: [Product]
}
"""
]
}
};
}

public static TheoryData<string[], string[]> InvalidExamplesData()
{
return new TheoryData<string[], string[]>
{
// Here, the @provides directive includes a numeric value (123) instead of a string in
// its "fields" argument. This invalid usage raises a PROVIDES_INVALID_FIELDS_TYPE
// error.
{
[
"""
type Product {
id: ID!
details: ProductDetails @provides(fields: 123)
}
type ProductDetails {
features: [String]
specifications: String
}
"""
],
[
"The @provides directive on field 'Product.details' in schema 'A' must " +
"specify a string value for the 'fields' argument."
]
}
};
}
}

0 comments on commit dbe8b36

Please sign in to comment.