diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeAttribute.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeAttribute.cs index 3d1daf2c133..fa51729ebbc 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeAttribute.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeAttribute.cs @@ -13,6 +13,7 @@ namespace HotChocolate.CostAnalysis.Types; public sealed class ListSizeAttribute : ObjectFieldDescriptorAttribute { private readonly int? _assumedSize; + private readonly bool? _requireOneSlicingArgument; /// /// The maximum length of the list returned by this field. @@ -44,7 +45,11 @@ public int AssumedSize /// Whether to require a single slicing argument in the query. If that is not the case (i.e., if /// none or multiple slicing arguments are present), the static analysis will throw an error. /// - public bool RequireOneSlicingArgument { get; init; } = true; + public bool RequireOneSlicingArgument + { + get => _requireOneSlicingArgument ?? true; + init => _requireOneSlicingArgument = value; + } protected override void OnConfigure( IDescriptorContext context, @@ -56,6 +61,6 @@ protected override void OnConfigure( _assumedSize, SlicingArguments?.ToImmutableArray(), SizedFields?.ToImmutableArray(), - RequireOneSlicingArgument)); + _requireOneSlicingArgument)); } } diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeDirective.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeDirective.cs index 897818e226f..5be8e89bb59 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeDirective.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeDirective.cs @@ -31,11 +31,7 @@ public ListSizeDirective( SlicingArguments = slicingArguments ?? ImmutableArray.Empty; SizedFields = sizedFields ?? ImmutableArray.Empty; SlicingArgumentDefaultValue = slicingArgumentDefaultValue; - - // https://ibm.github.io/graphql-specs/cost-spec.html#sec-requireOneSlicingArgument - // Per default, requireOneSlicingArgument is enabled, - // and has to be explicitly disabled if not desired for a field. - RequireOneSlicingArgument = SlicingArguments is { Length: > 0 } && (requireOneSlicingArgument ?? true); + RequireOneSlicingArgument = requireOneSlicingArgument; } /// @@ -86,5 +82,5 @@ public ListSizeDirective( /// /// Specification URL /// - public bool RequireOneSlicingArgument { get; } + public bool? RequireOneSlicingArgument { get; } } diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeDirectiveType.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeDirectiveType.cs index bdfded05c21..d6a569da288 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeDirectiveType.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeDirectiveType.cs @@ -170,7 +170,10 @@ private static DirectiveNode FormatValue(object value) arguments.Add(new ArgumentNode(SizedFields, directive.SizedFields.ToListValueNode())); } - arguments.Add(new ArgumentNode(RequireOneSlicingArgument, directive.RequireOneSlicingArgument)); + if (directive.RequireOneSlicingArgument is not null) + { + arguments.Add(new ArgumentNode(RequireOneSlicingArgument, directive.RequireOneSlicingArgument.Value)); + } return new DirectiveNode(_name, arguments.ToImmutableArray()); } diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/AttributeTests.cs b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/AttributeTests.cs index c41d68fe4c1..08dddfc9d7d 100644 --- a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/AttributeTests.cs +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/AttributeTests.cs @@ -1,4 +1,6 @@ +using CookieCrumble; using HotChocolate.CostAnalysis.Types; +using HotChocolate.Language.Utilities; using HotChocolate.Types; namespace HotChocolate.CostAnalysis; @@ -112,6 +114,36 @@ public void ListSize_ObjectFieldAttribute_AppliesDirective() Assert.False(costDirective.RequireOneSlicingArgument); } + [Fact] + public void ListSize_ObjectFieldAttribute_AppliesRequireOneSlicingArgumentCorrectly() + { + // arrange & act + var query = CreateSchema().GetType(OperationTypeNames.Query); + + var listSizeDirective1Sdl = query.Fields["examplesAssumedSizeOnly"] + .Directives + .Single(d => d.Type.Name == "listSize") + .AsSyntaxNode() + .Print(); + + var listSizeDirective2Sdl = query.Fields["examplesRequireOneSlicingArgumentTrue"] + .Directives + .Single(d => d.Type.Name == "listSize") + .AsSyntaxNode() + .Print(); + + var listSizeDirective3Sdl = query.Fields["examplesRequireOneSlicingArgumentFalse"] + .Directives + .Single(d => d.Type.Name == "listSize") + .AsSyntaxNode() + .Print(); + + // assert + listSizeDirective1Sdl.MatchInlineSnapshot("@listSize(assumedSize: 10)"); + listSizeDirective2Sdl.MatchInlineSnapshot("@listSize(requireOneSlicingArgument: true)"); + listSizeDirective3Sdl.MatchInlineSnapshot("@listSize(requireOneSlicingArgument: false)"); + } + private static ISchema CreateSchema() { return SchemaBuilder.New() @@ -128,6 +160,8 @@ private static ISchema CreateSchema() [QueryType] private static class Queries { + private static readonly List List = [new Example(ExampleEnum.Member)]; + [ListSize( AssumedSize = 10, SlicingArguments = ["first", "last"], @@ -137,7 +171,28 @@ private static class Queries // ReSharper disable once UnusedMember.Local public static List GetExamples([Cost(8.0)] ExampleInput _) { - return [new Example(ExampleEnum.Member)]; + return List; + } + + [ListSize(AssumedSize = 10)] + // ReSharper disable once UnusedMember.Local + public static List GetExamplesAssumedSizeOnly() + { + return List; + } + + [ListSize(RequireOneSlicingArgument = true)] + // ReSharper disable once UnusedMember.Local + public static List GetExamplesRequireOneSlicingArgumentTrue() + { + return List; + } + + [ListSize(RequireOneSlicingArgument = false)] + // ReSharper disable once UnusedMember.Local + public static List GetExamplesRequireOneSlicingArgumentFalse() + { + return List; } }