diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Utilities/CostAnalyzerUtilities.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Utilities/CostAnalyzerUtilities.cs index a193d9713a4..6fa07716213 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Utilities/CostAnalyzerUtilities.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Utilities/CostAnalyzerUtilities.cs @@ -163,18 +163,31 @@ public static void ValidateRequireOneSlicingArgument( // that it should expect that exactly one of the defined slicing arguments is present in // a query. If that is not the case (i.e., if none or multiple slicing arguments are // present), the static analysis may throw an error. - if (listSizeDirective is { RequireOneSlicingArgument: true }) + if (listSizeDirective?.RequireOneSlicingArgument ?? false) { var argumentCount = 0; + var variableCount = 0; foreach (var argumentNode in node.Arguments) { if (listSizeDirective.SlicingArguments.Contains(argumentNode.Name.Value)) { argumentCount++; + + if(argumentNode.Value.Kind == SyntaxKind.Variable) + { + variableCount++; + } } } + if(argumentCount > 0 && + argumentCount == variableCount && + argumentCount <= listSizeDirective.SlicingArguments.Length) + { + return; + } + if (argumentCount != 1) { throw new GraphQLException(ErrorHelper.ExactlyOneSlicingArgMustBeDefined(node, path)); diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/PagingTests.cs b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/PagingTests.cs index ead1f2a3bc7..21176e5f3c1 100644 --- a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/PagingTests.cs +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/PagingTests.cs @@ -122,6 +122,174 @@ await snapshot .MatchMarkdownAsync(); } + [Fact] + public async Task Require_Paging_Boundaries_Single_Boundary_With_Literal() + { + // arrange + var snapshot = new Snapshot(); + + var operation = + Utf8GraphQLParser.Parse( + """ + { + books(first: 1) { + nodes { + title + } + } + } + """); + + var request = + OperationRequestBuilder.New() + .SetDocument(operation) + .ReportCost() + .Build(); + + var executor = + await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .AddFiltering() + .AddSorting() + .BuildRequestExecutorAsync(); + + // act + var response = await executor.ExecuteAsync(request); + + // assert + await snapshot + .Add(operation, "Operation") + .Add(response, "Response") + .MatchMarkdownAsync(); + } + + [Fact] + public async Task Require_Paging_Boundaries_Single_Boundary_With_Variable() + { + // arrange + var snapshot = new Snapshot(); + + var operation = + Utf8GraphQLParser.Parse( + """ + query($first: Int) { + books(first: $first) { + nodes { + title + } + } + } + """); + + var request = + OperationRequestBuilder.New() + .SetDocument(operation) + .ReportCost() + .Build(); + + var executor = + await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .AddFiltering() + .AddSorting() + .BuildRequestExecutorAsync(); + + // act + var response = await executor.ExecuteAsync(request); + + // assert + await snapshot + .Add(operation, "Operation") + .Add(response, "Response") + .MatchMarkdownAsync(); + } + + [Fact] + public async Task Require_Paging_Boundaries_Two_Boundaries_With_Variable() + { + // arrange + var snapshot = new Snapshot(); + + var operation = + Utf8GraphQLParser.Parse( + """ + query($first: Int, $last: Int) { + books(first: $first, last: $last) { + nodes { + title + } + } + } + """); + + var request = + OperationRequestBuilder.New() + .SetDocument(operation) + .ReportCost() + .Build(); + + var executor = + await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .AddFiltering() + .AddSorting() + .BuildRequestExecutorAsync(); + + // act + var response = await executor.ExecuteAsync(request); + + // assert + await snapshot + .Add(operation, "Operation") + .Add(response, "Response") + .MatchMarkdownAsync(); + } + + [Fact] + public async Task Require_Paging_Boundaries_Two_Boundaries_Mixed() + { + // arrange + var snapshot = new Snapshot(); + + var operation = + Utf8GraphQLParser.Parse( + """ + query($first: Int) { + books(first: $first, last: 1) { + nodes { + title + } + } + } + """); + + var request = + OperationRequestBuilder.New() + .SetDocument(operation) + .ReportCost() + .Build(); + + var executor = + await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .AddFiltering() + .AddSorting() + .BuildRequestExecutorAsync(); + + // act + var response = await executor.ExecuteAsync(request); + + // assert + await snapshot + .Add(operation, "Operation") + .Add(response, "Response") + .MatchMarkdownAsync(); + } + [Fact] public async Task Require_Paging_Nested_Boundaries() { diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Require_Paging_Boundaries_Single_Boundary_With_Literal.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Require_Paging_Boundaries_Single_Boundary_With_Literal.md new file mode 100644 index 00000000000..6eb6d5fb9e8 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Require_Paging_Boundaries_Single_Boundary_With_Literal.md @@ -0,0 +1,32 @@ +# Require_Paging_Boundaries_Single_Boundary_With_Literal + +## Operation + +```graphql +{ + books(first: 1) { + nodes { + title + } + } +} +``` + +## Response + +```json +{ + "data": { + "books": { + "nodes": [] + } + }, + "extensions": { + "operationCost": { + "fieldCost": 11, + "typeCost": 3 + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Require_Paging_Boundaries_Single_Boundary_With_Variable.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Require_Paging_Boundaries_Single_Boundary_With_Variable.md new file mode 100644 index 00000000000..deb023538e1 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Require_Paging_Boundaries_Single_Boundary_With_Variable.md @@ -0,0 +1,32 @@ +# Require_Paging_Boundaries_Single_Boundary_With_Variable + +## Operation + +```graphql +query($first: Int) { + books(first: $first) { + nodes { + title + } + } +} +``` + +## Response + +```json +{ + "data": { + "books": { + "nodes": [] + } + }, + "extensions": { + "operationCost": { + "fieldCost": 11, + "typeCost": 52 + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Require_Paging_Boundaries_Two_Boundaries_Mixed.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Require_Paging_Boundaries_Two_Boundaries_Mixed.md new file mode 100644 index 00000000000..e5f9144f913 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Require_Paging_Boundaries_Two_Boundaries_Mixed.md @@ -0,0 +1,38 @@ +# Require_Paging_Boundaries_Two_Boundaries_Mixed + +## Operation + +```graphql +query($first: Int) { + books(first: $first, last: 1) { + nodes { + title + } + } +} +``` + +## Response + +```json +{ + "errors": [ + { + "message": "Exactly one slicing argument must be defined.", + "locations": [ + { + "line": 2, + "column": 5 + } + ], + "path": [ + "books" + ], + "extensions": { + "code": "HC0082" + } + } + ] +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Require_Paging_Boundaries_Two_Boundaries_With_Variable.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Require_Paging_Boundaries_Two_Boundaries_With_Variable.md new file mode 100644 index 00000000000..a753db64e2f --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Require_Paging_Boundaries_Two_Boundaries_With_Variable.md @@ -0,0 +1,32 @@ +# Require_Paging_Boundaries_Two_Boundaries_With_Variable + +## Operation + +```graphql +query($first: Int, $last: Int) { + books(first: $first, last: $last) { + nodes { + title + } + } +} +``` + +## Response + +```json +{ + "data": { + "books": { + "nodes": [] + } + }, + "extensions": { + "operationCost": { + "fieldCost": 11, + "typeCost": 52 + } + } +} +``` + diff --git a/src/HotChocolate/PersistedQueries/test/PersistedQueries.FileSystem.Tests/RequestExecutorBuilderTests.cs b/src/HotChocolate/PersistedQueries/test/PersistedQueries.FileSystem.Tests/RequestExecutorBuilderTests.cs index 64fef30837f..389c35960ce 100644 --- a/src/HotChocolate/PersistedQueries/test/PersistedQueries.FileSystem.Tests/RequestExecutorBuilderTests.cs +++ b/src/HotChocolate/PersistedQueries/test/PersistedQueries.FileSystem.Tests/RequestExecutorBuilderTests.cs @@ -15,17 +15,4 @@ void Action() Assert.Throws(Action); } - - [Fact] - public void AddReadOnlyFileSystemQueryStorage_Services_Is_Null() - { - // arrange - // act - void Action() - => HotChocolateFileSystemPersistedQueriesRequestExecutorBuilderExtensions - .AddReadOnlyFileSystemQueryStorage(null!); - - // assert - Assert.Throws(Action); - } }