diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs index 07244979d22..971bc8e86c9 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs @@ -19,6 +19,7 @@ public static class LogEntryCodes public const string ProvidesOnNonCompositeField = "PROVIDES_ON_NON_COMPOSITE_FIELD"; public const string QueryRootTypeInaccessible = "QUERY_ROOT_TYPE_INACCESSIBLE"; public const string RequireDirectiveInFieldsArg = "REQUIRE_DIRECTIVE_IN_FIELDS_ARG"; + public const string RequireInvalidFieldsType = "REQUIRE_INVALID_FIELDS_TYPE"; public const string RootMutationUsed = "ROOT_MUTATION_USED"; public const string RootQueryUsed = "ROOT_QUERY_USED"; public const string RootSubscriptionUsed = "ROOT_SUBSCRIPTION_USED"; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs index 6799f125eec..b7932cdccae 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs @@ -401,6 +401,24 @@ public static LogEntry RequireDirectiveInFieldsArgument( schema); } + public static LogEntry RequireInvalidFieldsType( + Directive requireDirective, + string argumentName, + string fieldName, + string typeName, + SchemaDefinition schema) + { + var coordinate = new SchemaCoordinate(typeName, fieldName, argumentName); + + return new LogEntry( + string.Format(LogEntryHelper_RequireInvalidFieldsType, coordinate, schema.Name), + LogEntryCodes.RequireInvalidFieldsType, + LogSeverity.Error, + coordinate, + requireDirective, + schema); + } + public static LogEntry RootMutationUsed(SchemaDefinition schema) { return new LogEntry( diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/RequireInvalidFieldsTypeRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/RequireInvalidFieldsTypeRule.cs new file mode 100644 index 00000000000..a4ddeec347e --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/RequireInvalidFieldsTypeRule.cs @@ -0,0 +1,40 @@ +using HotChocolate.Fusion.Events; +using HotChocolate.Language; +using static HotChocolate.Fusion.Logging.LogEntryHelper; +using static HotChocolate.Fusion.WellKnownArgumentNames; +using static HotChocolate.Fusion.WellKnownDirectiveNames; + +namespace HotChocolate.Fusion.PreMergeValidation.Rules; + +/// +/// When using the @require directive, the fields argument must always be a string +/// that defines a (potentially nested) selection set of fields from the same type. If the +/// fields argument is provided as a type other than a string (such as an integer, boolean, +/// or enum), the directive usage is invalid and will cause schema composition to fail. +/// +/// +/// Specification +/// +internal sealed class RequireInvalidFieldsTypeRule : IEventHandler +{ + public void Handle(FieldArgumentEvent @event, CompositionContext context) + { + var (argument, field, type, schema) = @event; + + var requireDirective = argument.Directives.FirstOrDefault(Require); + + if ( + requireDirective is not null + && requireDirective.Arguments.TryGetValue(Fields, out var fields) + && fields is not StringValueNode) + { + context.Log.Write( + RequireInvalidFieldsType( + requireDirective, + argument.Name, + field.Name, + type.Name, + schema)); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs index 835085fbcc1..58845c208a3 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs @@ -257,6 +257,15 @@ internal static string LogEntryHelper_RequireDirectiveInFieldsArgument { } } + /// + /// Looks up a localized string similar to The @require directive on argument '{0}' in schema '{1}' must specify a string value for the 'fields' argument.. + /// + internal static string LogEntryHelper_RequireInvalidFieldsType { + get { + return ResourceManager.GetString("LogEntryHelper_RequireInvalidFieldsType", resourceCulture); + } + } + /// /// Looks up a localized string similar to The root mutation type in schema '{0}' must be named 'Mutation'.. /// diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx index 42920b9a109..99a34c4181f 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx @@ -84,6 +84,9 @@ The @require directive on argument '{0}' in schema '{1}' references field '{2}', which must not include directive applications. + + The @require directive on argument '{0}' in schema '{1}' must specify a string value for the 'fields' argument. + The root mutation type in schema '{0}' must be named 'Mutation'. diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs index 0dbd89781d2..5011415d397 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs @@ -63,6 +63,7 @@ private CompositionResult MergeSchemaDefinitions(CompositionCo new ProvidesOnNonCompositeFieldRule(), new QueryRootTypeInaccessibleRule(), new RequireDirectiveInFieldsArgumentRule(), + new RequireInvalidFieldsTypeRule(), new RootMutationUsedRule(), new RootQueryUsedRule(), new RootSubscriptionUsedRule() diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/RequireInvalidFieldsTypeRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/RequireInvalidFieldsTypeRuleTests.cs new file mode 100644 index 00000000000..ef1bcf71970 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/RequireInvalidFieldsTypeRuleTests.cs @@ -0,0 +1,95 @@ +using HotChocolate.Fusion.Logging; +using HotChocolate.Fusion.PreMergeValidation; +using HotChocolate.Fusion.PreMergeValidation.Rules; + +namespace HotChocolate.Composition.PreMergeValidation.Rules; + +public sealed class RequireInvalidFieldsTypeRuleTests : CompositionTestBase +{ + private readonly PreMergeValidator _preMergeValidator = + new([new RequireInvalidFieldsTypeRule()]); + + [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 == "REQUIRE_INVALID_FIELDS_TYPE")); + Assert.True(context.Log.All(e => e.Severity == LogSeverity.Error)); + } + + public static TheoryData ValidExamplesData() + { + return new TheoryData + { + // In the following example, the @require directive’s "fields" argument is a valid + // string and satisfies the rule. + { + [ + """ + type User @key(fields: "id") { + id: ID! + profile(name: String! @require(fields: "name")): Profile + } + + type Profile { + id: ID! + name: String + } + """ + ] + } + }; + } + + public static TheoryData InvalidExamplesData() + { + return new TheoryData + { + // Since "fields" is set to 123 (an integer) instead of a string, this violates the rule + // and triggers a REQUIRE_INVALID_FIELDS_TYPE error. + { + [ + """ + type User @key(fields: "id") { + id: ID! + profile(name: String! @require(fields: 123)): Profile + } + + type Profile { + id: ID! + name: String + } + """ + ], + [ + "The @require directive on argument 'User.profile(name:)' in schema 'A' must " + + "specify a string value for the 'fields' argument." + ] + } + }; + } +}