From 2f8d695877a331c4f3b597ed92ed81b17c6589dc Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 16 Dec 2024 13:53:38 +0100 Subject: [PATCH] Adds request cost overrides. (#7818) --- .../test/AspNetCore.Tests/CostTests.cs | 50 +++++ ...ests.Cost_Exceeded_With_Cost_Override.snap | 12 ++ ...aMiddlewareTests.Download_GraphQL_SDL.snap | 6 +- ...s.Download_GraphQL_SDL_Explicit_Route.snap | 6 +- ...L_SDL_Explicit_Route_Explicit_Pattern.snap | 6 +- ...MiddlewareTests.Download_GraphQL_Schema.md | 10 +- ...oad_GraphQL_Schema_Slicing_Args_Enabled.md | 10 +- .../src/Abstractions/WellKnownContextData.cs | 5 + .../src/CostAnalysis/CostAnalyzer.cs | 8 +- .../CostAnalysis/CostAnalyzerMiddleware.cs | 23 ++- .../src/CostAnalysis/CostAnalyzerMode.cs | 2 +- .../src/CostAnalysis/CostTypeInterceptor.cs | 16 +- ...nalyzerRequestExecutorBuilderExtensions.cs | 10 + ...AnalyzerObjectFieldDescriptorExtensions.cs | 9 +- .../CostAnalyzerRequestContextExtensions.cs | 87 +++++++- .../src/CostAnalysis/Options/CostOptions.cs | 5 + .../Options/RequestCostOptions.cs | 22 +++ .../CostAnalysis/Types/ListSizeAttribute.cs | 6 + .../CostAnalysis/Types/ListSizeDirective.cs | 17 +- .../Types/ListSizeDirectiveType.cs | 28 ++- .../Utilities/CostAnalyzerUtilities.cs | 19 +- .../CostAnalysis/WellKnownArgumentNames.cs | 3 +- .../test/CostAnalysis.Tests/PagingTests.cs | 113 ++++++++++- .../PagingTests.Do_Not_Apply_Defaults.graphql | 142 ++++++++++++++ ...ult_Page_Size_When_Default_Is_Specified.md | 185 ++++++++++++++++++ ...Ensure_Paging_Defaults_Are_Applied.graphql | 12 +- .../PagingTests.Filtering_Not_Used.md | 4 +- ...iltering_Specific_Expensive_Filter_Used.md | 4 +- ...ingTests.Filtering_Specific_Filter_Used.md | 4 +- .../PagingTests.Filtering_Variable.md | 4 +- ...ult_Page_Size_When_Default_Is_Specified.md | 185 ++++++++++++++++++ ...ospectionClientTests.IntrospectServer.snap | 2 +- ...st.Execute_StarWarsIntrospection_Test.snap | 28 +++ 33 files changed, 965 insertions(+), 78 deletions(-) create mode 100644 src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/CostTests.Cost_Exceeded_With_Cost_Override.snap create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Options/RequestCostOptions.cs create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Do_Not_Apply_Defaults.graphql create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Do_Not_Use_Default_Page_Size_When_Default_Is_Specified.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Use_Default_Page_Size_When_Default_Is_Specified.md diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/CostTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/CostTests.cs index 3033e6f626d..8170f6e63bf 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/CostTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/CostTests.cs @@ -1,9 +1,13 @@ #if NET7_0_OR_GREATER +using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json; using CookieCrumble; using HotChocolate.AspNetCore.Tests.Utilities; +using HotChocolate.Execution; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; namespace HotChocolate.AspNetCore; @@ -97,5 +101,51 @@ public async Task Request_Validate_Cost_Header() Assert.NotNull(response); result?.RootElement.MatchSnapshot(); } + + [Fact] + public async Task Cost_Exceeded_With_Cost_Override() + { + // arrange + var server = CreateStarWarsServer( + configureServices: services => services + .AddGraphQLServer() + .AddHttpRequestInterceptor() ); + + var uri = new Uri("http://localhost:5000/graphql"); + + var requestBody = + """ + { + "query" : "query Test($id: String!){human(id: $id){name}}" + "variables" : { "id" : "1000" } + } + """; + + var content = new StringContent(requestBody, Encoding.UTF8, "application/json"); + + // act + using var httpClient = server.CreateClient(); + var response = await httpClient.PostAsync(uri, content); + + // assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var result = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(response); + result?.RootElement.MatchSnapshot(); + } + + public class CostInterceptor : DefaultHttpRequestInterceptor + { + public override ValueTask OnCreateAsync( + HttpContext context, + IRequestExecutor requestExecutor, + OperationRequestBuilder requestBuilder, + CancellationToken cancellationToken) + { + var costOptions = requestExecutor.GetCostOptions(); + requestBuilder.SetCostOptions(costOptions with { MaxTypeCost = 1}); + return base.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken); + } + } } #endif diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/CostTests.Cost_Exceeded_With_Cost_Override.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/CostTests.Cost_Exceeded_With_Cost_Override.snap new file mode 100644 index 00000000000..d534d5d3d80 --- /dev/null +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/CostTests.Cost_Exceeded_With_Cost_Override.snap @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "message": "The maximum allowed type cost was exceeded.", + "extensions": { + "typeCost": 2, + "maxTypeCost": 1, + "code": "HC0047" + } + } + ] +} diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL.snap index 30c65b8cacb..21f3e15f9ad 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL.snap @@ -17,7 +17,7 @@ type Droid implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) height(unit: Unit): Float primaryFunction: String traits: JSON @@ -45,7 +45,7 @@ type Human implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) otherHuman: Human height(unit: Unit): Float homePlanet: String @@ -128,7 +128,7 @@ directive @defer("If this argument label has a value other than null, it will be directive @foo(bar: Int!) on SUBSCRIPTION "The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." -directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis 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." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION +directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." slicingArgumentDefaultValue: Int "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis 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." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION "The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! = 0 "Streamed when true." if: Boolean) on FIELD diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route.snap index b2a1cda45f1..737ddde8670 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route.snap @@ -17,7 +17,7 @@ type Droid implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) height(unit: Unit): Float primaryFunction: String traits: JSON @@ -45,7 +45,7 @@ type Human implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) otherHuman: Human height(unit: Unit): Float homePlanet: String @@ -126,7 +126,7 @@ directive @cost("The `weight` argument defines what value to add to the overall directive @defer("If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String "Deferred when true." if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT "The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." -directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis 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." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION +directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." slicingArgumentDefaultValue: Int "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis 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." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION "The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! = 0 "Streamed when true." if: Boolean) on FIELD diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route_Explicit_Pattern.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route_Explicit_Pattern.snap index b2a1cda45f1..737ddde8670 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route_Explicit_Pattern.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route_Explicit_Pattern.snap @@ -17,7 +17,7 @@ type Droid implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) height(unit: Unit): Float primaryFunction: String traits: JSON @@ -45,7 +45,7 @@ type Human implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) otherHuman: Human height(unit: Unit): Float homePlanet: String @@ -126,7 +126,7 @@ directive @cost("The `weight` argument defines what value to add to the overall directive @defer("If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String "Deferred when true." if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT "The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." -directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis 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." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION +directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." slicingArgumentDefaultValue: Int "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis 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." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION "The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! = 0 "Streamed when true." if: Boolean) on FIELD diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema.md b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema.md index 72badc86f61..2577501c118 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema.md +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema.md @@ -2,12 +2,12 @@ ```text Headers: -ETag: "1-LXklYHgmBtUalJ0Ugb0vQzLmP6FuyFt1keIXDn4SE/Y=" +ETag: "1-zgi5AzGsi9KCkeA00b2KpL3HoZ++qVVoP05qFxiKUig=" Cache-Control: public, must-revalidate, max-age=3600 Content-Type: application/graphql; charset=utf-8 Content-Disposition: attachment; filename="schema.graphql" Last-Modified: Fri, 01 Jan 2021 00:00:00 GMT -Content-Length: 7304 +Content-Length: 7567 --------------------------> Status Code: OK --------------------------> @@ -30,7 +30,7 @@ type Droid implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) height(unit: Unit): Float primaryFunction: String traits: JSON @@ -58,7 +58,7 @@ type Human implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) otherHuman: Human height(unit: Unit): Float homePlanet: String @@ -141,7 +141,7 @@ directive @defer("If this argument label has a value other than null, it will be directive @foo(bar: Int!) on SUBSCRIPTION "The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." -directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis 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." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION +directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." slicingArgumentDefaultValue: Int "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis 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." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION "The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! = 0 "Streamed when true." if: Boolean) on FIELD diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Slicing_Args_Enabled.md b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Slicing_Args_Enabled.md index e1d4b309a5f..a791e7a118e 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Slicing_Args_Enabled.md +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Slicing_Args_Enabled.md @@ -2,12 +2,12 @@ ```text Headers: -ETag: "1-SGSC/P4ipajfTuy/tz1WcPpecD5c1THPYtIkhxzZoQE=" +ETag: "1-tZzSREUjWTbo3aR/f3UyqLOpQlYXAhTMxMLBUopbx10=" Cache-Control: public, must-revalidate, max-age=3600 Content-Type: application/graphql; charset=utf-8 Content-Disposition: attachment; filename="schema.graphql" Last-Modified: Fri, 01 Jan 2021 00:00:00 GMT -Content-Length: 7236 +Content-Length: 7499 --------------------------> Status Code: OK --------------------------> @@ -30,7 +30,7 @@ type Droid implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ]) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ]) height(unit: Unit): Float primaryFunction: String traits: JSON @@ -58,7 +58,7 @@ type Human implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ]) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ]) otherHuman: Human height(unit: Unit): Float homePlanet: String @@ -141,7 +141,7 @@ directive @defer("If this argument label has a value other than null, it will be directive @foo(bar: Int!) on SUBSCRIPTION "The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." -directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis 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." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION +directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." slicingArgumentDefaultValue: Int "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis 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." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION "The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! = 0 "Streamed when true." if: Boolean) on FIELD diff --git a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs index efd00842378..4bb3d96a00e 100644 --- a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs +++ b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs @@ -309,6 +309,11 @@ public static class WellKnownContextData /// public const string ValidateCost = "HotChocolate.CostAnalysis.ValidateCost"; + /// + /// The key to access the cost options on the context data.. + /// + public const string RequestCostOptions = "HotChocolate.CostAnalysis.CostRequestOptions"; + /// /// The key to access the paging observers stored on the local resolver state. /// diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalyzer.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalyzer.cs index abcbc9562b3..591f257d30b 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalyzer.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalyzer.cs @@ -11,7 +11,7 @@ namespace HotChocolate.CostAnalysis; -internal sealed class CostAnalyzer(CostOptions options) : TypeDocumentValidatorVisitor +internal sealed class CostAnalyzer(RequestCostOptions options) : TypeDocumentValidatorVisitor { private readonly Dictionary _selectionSetCost = new(); private readonly HashSet _processed = new(); @@ -280,9 +280,9 @@ private CostSummary GetSelectionSetCost(SelectionSetNode selectionSetNode) if ((argument.Flags & FieldFlags.FilterArgument) == FieldFlags.FilterArgument && argumentNode.Value.Kind == SyntaxKind.Variable - && options.Filtering.VariableMultiplier.HasValue) + && options.FilterVariableMultiplier.HasValue) { - argumentCost *= options.Filtering.VariableMultiplier.Value; + argumentCost *= options.FilterVariableMultiplier.Value; } fieldCost += argumentCost; @@ -317,7 +317,7 @@ private CostSummary GetSelectionSetCost(SelectionSetNode selectionSetNode) // if the field is a list type we are multiplying the cost // by the estimated list size. - var listSize = field.GetListSize(arguments, listSizeDirective, context.Variables); + var listSize = field.GetListSize(arguments, listSizeDirective); typeCost *= listSize; selectionSetCost *= listSize; } diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalyzerMiddleware.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalyzerMiddleware.cs index ebef63d639a..a13cc8c04c2 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalyzerMiddleware.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalyzerMiddleware.cs @@ -14,7 +14,7 @@ namespace HotChocolate.CostAnalysis; internal sealed class CostAnalyzerMiddleware( RequestDelegate next, - [SchemaService] CostOptions options, + [SchemaService] RequestCostOptions options, DocumentValidatorContextPool contextPool, ICostMetricsCache cache, [SchemaService] IExecutionDiagnosticEvents diagnosticEvents) @@ -35,7 +35,8 @@ public async ValueTask InvokeAsync(IRequestContext context) context.OperationId = operationId; } - var mode = context.GetCostAnalyzerMode(options); + var requestOptions = context.TryGetCostOptions() ?? options; + var mode = context.GetCostAnalyzerMode(requestOptions.EnforceCostLimits); if (mode == CostAnalyzerMode.Skip) { @@ -43,7 +44,7 @@ public async ValueTask InvokeAsync(IRequestContext context) return; } - if (!TryAnalyze(context, mode, context.Document, operationId, out var costMetrics)) + if (!TryAnalyze(context, requestOptions, mode, context.Document, operationId, out var costMetrics)) { // a error happened during the analysis and the error is already set. return; @@ -65,6 +66,7 @@ context.Result is null private bool TryAnalyze( IRequestContext context, + RequestCostOptions requestOptions, CostAnalyzerMode mode, DocumentNode document, string operationId, @@ -80,12 +82,13 @@ private bool TryAnalyze( // we check if the operation was already resolved by another middleware, // if not we resolve the operation. var operationDefinition = - context.Operation?.Definition ?? document.GetOperation(context.Request.OperationName); + context.Operation?.Definition + ?? document.GetOperation(context.Request.OperationName); validatorContext = contextPool.Get(); PrepareContext(context, document, validatorContext); - var analyzer = new CostAnalyzer(options); + var analyzer = new CostAnalyzer(requestOptions); costMetrics = analyzer.Analyze(operationDefinition, validatorContext); cache.TryAddCostMetrics(operationId, costMetrics); } @@ -95,20 +98,20 @@ private bool TryAnalyze( if ((mode & CostAnalyzerMode.Enforce) == CostAnalyzerMode.Enforce) { - if (costMetrics.FieldCost > options.MaxFieldCost) + if (costMetrics.FieldCost > requestOptions.MaxFieldCost) { context.Result = ErrorHelper.MaxFieldCostReached( costMetrics, - options.MaxFieldCost, + requestOptions.MaxFieldCost, (mode & CostAnalyzerMode.Report) == CostAnalyzerMode.Report); return false; } - if (costMetrics.TypeCost > options.MaxTypeCost) + if (costMetrics.TypeCost > requestOptions.MaxTypeCost) { context.Result = ErrorHelper.MaxTypeCostReached( costMetrics, - options.MaxTypeCost, + requestOptions.MaxTypeCost, (mode & CostAnalyzerMode.Report) == CostAnalyzerMode.Report); return false; } @@ -155,7 +158,7 @@ public static RequestCoreMiddleware Create() return (core, next) => { // this needs to be a schema service - var options = core.SchemaServices.GetRequiredService(); + var options = core.SchemaServices.GetRequiredService(); var contextPool = core.Services.GetRequiredService(); var cache = core.Services.GetRequiredService(); var diagnosticEvents = core.SchemaServices.GetRequiredService(); diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalyzerMode.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalyzerMode.cs index 181c7f831ed..4bc887ae754 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalyzerMode.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalyzerMode.cs @@ -17,7 +17,7 @@ internal enum CostAnalyzerMode Analyze = 1, /// - /// Enforces the defined cost limits but does not report any metrics in the response. + /// Includes the operation cost metrics in the response. /// Report = 2, diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs index 9b179f602ba..ae769173431 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs @@ -32,15 +32,16 @@ private readonly ImmutableArray _offsetSizedFields internal override uint Position => int.MaxValue; + internal override bool IsEnabled(IDescriptorContext context) + => context.Services.GetRequiredService().ApplyCostDefaults; + internal override void InitializeContext( IDescriptorContext context, TypeInitializer typeInitializer, TypeRegistry typeRegistry, TypeLookup typeLookup, TypeReferenceResolver typeReferenceResolver) - { - _options = context.Services.GetRequiredService(); - } + => _options = context.Services.GetRequiredService(); public override void OnAfterCompleteName(ITypeCompletionContext completionContext, DefinitionBase definition) { @@ -79,12 +80,19 @@ public override void OnAfterCompleteName(ITypeCompletionContext completionContex slicingArgs.Length > 0 && (options.RequirePagingBoundaries ?? false); + int? slicingArgumentDefaultValue = null; + if (_options.ApplySlicingArgumentDefaultValue) + { + slicingArgumentDefaultValue = options.DefaultPageSize ?? DefaultPageSize; + } + fieldDef.AddDirective( new ListSizeDirective( assumedSize, slicingArgs, sizeFields, - requirePagingBoundaries), + requirePagingBoundaries, + slicingArgumentDefaultValue), completionContext.DescriptorContext.TypeInspector); } diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/DependencyInjection/CostAnalyzerRequestExecutorBuilderExtensions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/DependencyInjection/CostAnalyzerRequestExecutorBuilderExtensions.cs index b84499359c4..64560d2c558 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/DependencyInjection/CostAnalyzerRequestExecutorBuilderExtensions.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/DependencyInjection/CostAnalyzerRequestExecutorBuilderExtensions.cs @@ -48,6 +48,16 @@ public static IRequestExecutorBuilder AddCostAnalyzer(this IRequestExecutorBuild return options; }); + + services.TryAddSingleton(sp => + { + var requestOptions = sp.GetRequiredService(); + return new RequestCostOptions( + requestOptions.MaxFieldCost, + requestOptions.MaxTypeCost, + requestOptions.EnforceCostLimits, + requestOptions.Filtering.VariableMultiplier); + }); }) .AddDirectiveType() .AddDirectiveType() diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerObjectFieldDescriptorExtensions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerObjectFieldDescriptorExtensions.cs index 3bad7dfdf56..1ccb17f1b8c 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerObjectFieldDescriptorExtensions.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerObjectFieldDescriptorExtensions.cs @@ -60,6 +60,9 @@ public static IObjectFieldDescriptor Cost(this IObjectFieldDescriptor descriptor /// 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. /// + /// + /// The default value to use for slicing arguments if no slicing argument is provided. + /// /// /// Returns the object field descriptor for configuration chaining. /// @@ -71,7 +74,8 @@ public static IObjectFieldDescriptor ListSize( int? assumedSize = null, ImmutableArray? slicingArguments = null, ImmutableArray? sizedFields = null, - bool requireOneSlicingArgument = true) + bool requireOneSlicingArgument = true, + int? slicingArgumentDefaultValue = null) { if (descriptor is null) { @@ -83,6 +87,7 @@ public static IObjectFieldDescriptor ListSize( assumedSize, slicingArguments, sizedFields, - requireOneSlicingArgument)); + requireOneSlicingArgument, + slicingArgumentDefaultValue)); } } diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerRequestContextExtensions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerRequestContextExtensions.cs index 91adbf8c712..e3c42d18810 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerRequestContextExtensions.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerRequestContextExtensions.cs @@ -1,4 +1,6 @@ using HotChocolate.CostAnalysis; +using HotChocolate.Resolvers; +using Microsoft.Extensions.DependencyInjection; namespace HotChocolate.Execution; @@ -56,18 +58,13 @@ public static CostMetrics GetCostMetrics( internal static CostAnalyzerMode GetCostAnalyzerMode( this IRequestContext context, - CostOptions options) + bool enforceCostLimits) { if (context is null) { throw new ArgumentNullException(nameof(context)); } - if (options is null) - { - throw new ArgumentNullException(nameof(options)); - } - if (context.ContextData.ContainsKey(WellKnownContextData.ValidateCost)) { return CostAnalyzerMode.Analyze | CostAnalyzerMode.Report; @@ -75,7 +72,7 @@ internal static CostAnalyzerMode GetCostAnalyzerMode( var flags = CostAnalyzerMode.Analyze; - if (options.EnforceCostLimits) + if (enforceCostLimits) { flags |= CostAnalyzerMode.Enforce; } @@ -89,4 +86,80 @@ internal static CostAnalyzerMode GetCostAnalyzerMode( return flags; } + + /// + /// Gets the cost options for the current request. + /// + /// + /// The request context. + /// + /// + /// Returns the cost options. + /// + public static RequestCostOptions GetCostOptions(this IRequestContext context) + { + if (context.ContextData.TryGetValue(WellKnownContextData.RequestCostOptions, out var value) + && value is RequestCostOptions options) + { + return options; + } + + return context.Schema.Services.GetRequiredService(); + } + + /// + /// Gets the global cost options from the executor. + /// + /// + /// The GraphQL executor. + /// + /// + /// Returns the global cost options. + /// + public static RequestCostOptions GetCostOptions(this IRequestExecutor executor) + { + return executor.Schema.Services.GetRequiredService(); + } + + internal static RequestCostOptions? TryGetCostOptions(this IRequestContext context) + { + if (context.ContextData.TryGetValue(WellKnownContextData.RequestCostOptions, out var value) + && value is RequestCostOptions options) + { + return options; + } + + return null; + } + + /// + /// Sets the cost options for the current request. + /// + /// + /// The request context. + /// + /// + /// The cost options. + /// + public static void SetCostOptions(this IRequestContext context, RequestCostOptions options) + { + context.ContextData[WellKnownContextData.RequestCostOptions] = options; + } + + /// + /// Sets the cost options for the current request. + /// + /// + /// The operation request builder. + /// + /// + /// The cost options. + /// + /// + /// Returns the operation request builder. + /// + public static OperationRequestBuilder SetCostOptions( + this OperationRequestBuilder builder, + RequestCostOptions options) + => builder.SetGlobalState(WellKnownContextData.RequestCostOptions, options); } diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Options/CostOptions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Options/CostOptions.cs index 6d84907069b..b036e72187b 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Options/CostOptions.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Options/CostOptions.cs @@ -25,6 +25,11 @@ public sealed class CostOptions /// public bool ApplyCostDefaults { get; set; } = true; + /// + /// Defines if the non-spec slicing argument default value shall be applied. + /// + public bool ApplySlicingArgumentDefaultValue { get; set; } = true; + /// /// Gets or sets the default cost for an async resolver pipeline. /// diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Options/RequestCostOptions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Options/RequestCostOptions.cs new file mode 100644 index 00000000000..0b111285f7d --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Options/RequestCostOptions.cs @@ -0,0 +1,22 @@ +namespace HotChocolate.CostAnalysis; + +/// +/// Request options for cost analysis. +/// +/// +/// The maximum allowed field cost. +/// +/// +/// The maximum allowed type cost. +/// +/// +/// Defines if the analyzer shall enforce cost limits. +/// +/// +/// The filter variable multiplier. +/// +public record RequestCostOptions( + double MaxFieldCost, + double MaxTypeCost, + bool EnforceCostLimits, + int? FilterVariableMultiplier); diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeAttribute.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeAttribute.cs index 16bf14efee0..3d1daf2c133 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeAttribute.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeAttribute.cs @@ -29,6 +29,12 @@ public int AssumedSize /// public string[]? SlicingArguments { get; init; } + /// + /// The default value for a slicing argument, which is used if the argument is not present in a + /// query. + /// + public int? SlicingArgumentDefaultValue { get; init; } + /// /// The subfield(s) that the list size applies to. /// diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeDirective.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeDirective.cs index ceace987f75..897818e226f 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeDirective.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeDirective.cs @@ -20,14 +20,17 @@ public sealed class ListSizeDirective /// /// Specification URL /// - public ListSizeDirective(int? assumedSize = null, + public ListSizeDirective( + int? assumedSize = null, ImmutableArray? slicingArguments = null, ImmutableArray? sizedFields = null, - bool? requireOneSlicingArgument = null) + bool? requireOneSlicingArgument = null, + int? slicingArgumentDefaultValue = null) { AssumedSize = assumedSize; 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, @@ -54,6 +57,16 @@ public ListSizeDirective(int? assumedSize = null, /// public ImmutableArray SlicingArguments { get; } + /// + /// Specifies the default value to use for slicing arguments if no slicing argument is provided. + /// + /// + /// This property is a non-spec addition and can provide a default value for slicing arguments if no slicing + /// argument is provided. This is useful for fields that have a default value for a slicing + /// argument that cannot be expressed in the schema. + /// + public int? SlicingArgumentDefaultValue { get; } + /// /// The sizedFields argument can be used to define that the value of the /// assumedSize argument or of a slicing argument does not affect the size of a list diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeDirectiveType.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeDirectiveType.cs index c20b9f8a55d..bdfded05c21 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeDirectiveType.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/ListSizeDirectiveType.cs @@ -48,6 +48,14 @@ protected override void Configure(IDirectiveTypeDescriptor de "determines the size of the list returned by that field. It may specify a list " + "of multiple slicing arguments."); + descriptor + .Argument(t => t.SlicingArgumentDefaultValue) + .Name(SlicingArgumentDefaultValue) + .Type() + .Description( + "The `slicingArgumentDefaultValue` argument can be used to define a default value " + + "for a slicing argument, which is used if the argument is not present in a query."); + descriptor .Argument(t => t.SizedFields) .Name(SizedFields) @@ -81,6 +89,7 @@ private static object ParseLiteral(DirectiveNode directiveNode) var slicingArguments = ImmutableArray.Empty; var sizedFields = ImmutableArray.Empty; var requireOneSlicingArgument = false; + int? slicingArgumentDefaultValue = null; foreach (var argument in directiveNode.Arguments) { @@ -107,12 +116,21 @@ private static object ParseLiteral(DirectiveNode directiveNode) requireOneSlicingArgument = argument.Value.ExpectBoolean(); break; + case SlicingArgumentDefaultValue: + slicingArgumentDefaultValue = argument.Value.ExpectInt(); + break; + default: throw new InvalidOperationException("Invalid argument name."); } } - return new ListSizeDirective(assumedSize, slicingArguments, sizedFields, requireOneSlicingArgument); + return new ListSizeDirective( + assumedSize, + slicingArguments, + sizedFields, + requireOneSlicingArgument, + slicingArgumentDefaultValue); } protected override Func OnCompleteFormat( @@ -137,6 +155,14 @@ private static DirectiveNode FormatValue(object value) if (directive.SlicingArguments.Length > 0) { arguments.Add(new ArgumentNode(SlicingArguments, directive.SlicingArguments.ToListValueNode())); + + if(directive.SlicingArgumentDefaultValue.HasValue) + { + arguments.Add( + new ArgumentNode( + SlicingArgumentDefaultValue, + directive.SlicingArgumentDefaultValue.Value)); + } } if (directive.SizedFields.Length > 0) diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Utilities/CostAnalyzerUtilities.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Utilities/CostAnalyzerUtilities.cs index edde29c1f76..fb6d3199d16 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Utilities/CostAnalyzerUtilities.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Utilities/CostAnalyzerUtilities.cs @@ -88,8 +88,7 @@ public static double GetTypeWeight(this IType type) public static double GetListSize( this IOutputField field, IReadOnlyList arguments, - ListSizeDirective? listSizeDirective, - IDictionary variables) + ListSizeDirective? listSizeDirective) { const int defaultListSize = 1; @@ -114,11 +113,10 @@ public static double GetListSize( slicingValues[index++] = intValueNode.ToInt32(); continue; - case VariableNode variableNode - when variables[variableNode.Name.Value].DefaultValue is - IntValueNode intValueNode: - slicingValues[index++] = intValueNode.ToInt32(); - continue; + // if one of the slicing arguments is variable we will assume the + // maximum allowed page size. + case VariableNode when listSizeDirective.AssumedSize.HasValue: + return listSizeDirective.AssumedSize.Value; } } @@ -129,6 +127,13 @@ when variables[variableNode.Name.Value].DefaultValue is } } + if (index == 0 && listSizeDirective.SlicingArgumentDefaultValue.HasValue) + { + // if no slicing arguments were found we assume the + // paging default size if one is set. + return listSizeDirective.SlicingArgumentDefaultValue.Value; + } + if (index == 1) { return slicingValues[0]; diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/WellKnownArgumentNames.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/WellKnownArgumentNames.cs index f268ff0b07f..c76f59bc7be 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/WellKnownArgumentNames.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/WellKnownArgumentNames.cs @@ -3,10 +3,9 @@ namespace HotChocolate.CostAnalysis; internal static class WellKnownArgumentNames { public const string AssumedSize = "assumedSize"; - public const string RegexName = "regexName"; - public const string RegexPath = "regexPath"; public const string RequireOneSlicingArgument = "requireOneSlicingArgument"; public const string SizedFields = "sizedFields"; public const string SlicingArguments = "slicingArguments"; + public const string SlicingArgumentDefaultValue = "slicingArgumentDefaultValue"; public const string Weight = "weight"; } diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/PagingTests.cs b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/PagingTests.cs index 2bbf4ca8415..69913b82b90 100644 --- a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/PagingTests.cs +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/PagingTests.cs @@ -27,6 +27,22 @@ public async Task Ensure_Paging_Defaults_Are_Applied() schema.MatchSnapshot(); } + [Fact] + public async Task Do_Not_Apply_Defaults() + { + var schema = + await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .AddFiltering() + .AddSorting() + .ModifyPagingOptions(o => o.RequirePagingBoundaries = true) + .ModifyCostOptions(o => o.ApplyCostDefaults = false) + .BuildSchemaAsync(); + + schema.MatchSnapshot(); + } + [Fact] public async Task Filtering_Not_Used() { @@ -69,7 +85,7 @@ public async Task Filtering_Not_Used() """ { "fieldCost": 6, - "typeCost": 52 + "typeCost": 12 } """); @@ -382,7 +398,7 @@ public async Task Filtering_Specific_Filter_Used() """ { "fieldCost": 9, - "typeCost": 52 + "typeCost": 12 } """); @@ -435,7 +451,7 @@ public async Task Filtering_Specific_Expensive_Filter_Used() """ { "fieldCost": 10, - "typeCost": 52 + "typeCost": 12 } """); @@ -488,7 +504,7 @@ public async Task Filtering_Variable() """ { "fieldCost": 10, - "typeCost": 52 + "typeCost": 12 } """); @@ -499,6 +515,95 @@ await snapshot .MatchMarkdownAsync(); } + [Fact] + public async Task Use_Default_Page_Size_When_Default_Is_Specified() + { + // arrange + var snapshot = new Snapshot(); + + var operation = + Utf8GraphQLParser.Parse( + """ + { + books { + nodes { + title + } + } + } + """); + + var request = + OperationRequestBuilder.New() + .SetDocument(operation) + .ReportCost() + .Build(); + + var executor = + await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .AddFiltering() + .AddSorting() + .ModifyPagingOptions(o => o.DefaultPageSize = 2) + .BuildRequestExecutorAsync(); + + // act + var response = await executor.ExecuteAsync(request); + + // assert + await snapshot + .Add(operation, "Operation") + .Add(response, "Response") + .Add(executor.Schema, "Schema") + .MatchMarkdownAsync(); + } + + [Fact] + public async Task Do_Not_Use_Default_Page_Size_When_Default_Is_Specified() + { + // arrange + var snapshot = new Snapshot(); + + var operation = + Utf8GraphQLParser.Parse( + """ + { + books { + nodes { + title + } + } + } + """); + + var request = + OperationRequestBuilder.New() + .SetDocument(operation) + .ReportCost() + .Build(); + + var executor = + await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .AddFiltering() + .AddSorting() + .ModifyPagingOptions(o => o.DefaultPageSize = 2) + .ModifyCostOptions(o => o.ApplySlicingArgumentDefaultValue = false) + .BuildRequestExecutorAsync(); + + // act + var response = await executor.ExecuteAsync(request); + + // assert + await snapshot + .Add(operation, "Operation") + .Add(response, "Response") + .Add(executor.Schema, "Schema") + .MatchMarkdownAsync(); + } + public class Query { [UsePaging] diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Do_Not_Apply_Defaults.graphql b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Do_Not_Apply_Defaults.graphql new file mode 100644 index 00000000000..529d01bb859 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Do_Not_Apply_Defaults.graphql @@ -0,0 +1,142 @@ +schema { + query: Query +} + +type Author { + name: String! +} + +"A connection to a list of items." +type AuthorsConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [AuthorsEdge!] + "A flattened list of the nodes." + nodes: [Author!] +} + +"An edge in a connection." +type AuthorsEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Author! +} + +type Book { + title: String! + authors("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): AuthorsConnection +} + +"A connection to a list of items." +type BooksConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [BooksEdge!] + "A flattened list of the nodes." + nodes: [Book!] +} + +"An edge in a connection." +type BooksEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Book! +} + +"A segment of a collection." +type BooksOffsetCollectionSegment { + "Information to aid in pagination." + pageInfo: CollectionSegmentInfo! + "A flattened list of the items." + items: [Book!] +} + +"A segment of a collection." +type BooksTotalCollectionSegment { + "Information to aid in pagination." + pageInfo: CollectionSegmentInfo! + "A flattened list of the items." + items: [Book!] + totalCount: Int! +} + +"A connection to a list of items." +type BooksTotalConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [BooksTotalEdge!] + "A flattened list of the nodes." + nodes: [Book!] + "Identifies the total count of items in the connection." + totalCount: Int! +} + +"An edge in a connection." +type BooksTotalEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Book! +} + +"Information about the offset pagination." +type CollectionSegmentInfo { + "Indicates whether more items exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more items exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! +} + +"Information about pagination in a connection." +type PageInfo { + "Indicates whether more edges exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more edges exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! + "When paginating backwards, the cursor to continue." + startCursor: String + "When paginating forwards, the cursor to continue." + endCursor: String +} + +type Query { + books("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: BookFilterInput order: [BookSortInput!]): BooksConnection + booksWithTotalCount("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: BookFilterInput order: [BookSortInput!]): BooksTotalConnection + booksOffset(skip: Int take: Int where: BookFilterInput order: [BookSortInput!]): BooksOffsetCollectionSegment + booksOffsetWithTotalCount(skip: Int take: Int where: BookFilterInput order: [BookSortInput!]): BooksTotalCollectionSegment +} + +input BookFilterInput { + and: [BookFilterInput!] + or: [BookFilterInput!] + title: StringOperationFilterInput +} + +input BookSortInput { + title: SortEnumType +} + +input StringOperationFilterInput { + and: [StringOperationFilterInput!] + or: [StringOperationFilterInput!] + eq: String + neq: String + contains: String + ncontains: String + in: [String] + nin: [String] + startsWith: String + nstartsWith: String + endsWith: String + nendsWith: String +} + +enum SortEnumType { + ASC + DESC +} diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Do_Not_Use_Default_Page_Size_When_Default_Is_Specified.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Do_Not_Use_Default_Page_Size_When_Default_Is_Specified.md new file mode 100644 index 00000000000..eb8da3790ca --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Do_Not_Use_Default_Page_Size_When_Default_Is_Specified.md @@ -0,0 +1,185 @@ +# Do_Not_Use_Default_Page_Size_When_Default_Is_Specified + +## Operation + +```graphql +{ + books { + nodes { + title + } + } +} +``` + +## Response + +```json +{ + "data": { + "books": { + "nodes": [] + } + }, + "extensions": { + "operationCost": { + "fieldCost": 11, + "typeCost": 52 + } + } +} +``` + +## Schema + +```graphql +schema { + query: Query +} + +type Author { + name: String! +} + +"A connection to a list of items." +type AuthorsConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [AuthorsEdge!] + "A flattened list of the nodes." + nodes: [Author!] +} + +"An edge in a connection." +type AuthorsEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Author! +} + +type Book { + title: String! + authors("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): AuthorsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) +} + +"A connection to a list of items." +type BooksConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [BooksEdge!] + "A flattened list of the nodes." + nodes: [Book!] +} + +"An edge in a connection." +type BooksEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Book! +} + +"A segment of a collection." +type BooksOffsetCollectionSegment { + "Information to aid in pagination." + pageInfo: CollectionSegmentInfo! + "A flattened list of the items." + items: [Book!] +} + +"A segment of a collection." +type BooksTotalCollectionSegment { + "Information to aid in pagination." + pageInfo: CollectionSegmentInfo! + "A flattened list of the items." + items: [Book!] + totalCount: Int! @cost(weight: "10") +} + +"A connection to a list of items." +type BooksTotalConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [BooksTotalEdge!] + "A flattened list of the nodes." + nodes: [Book!] + "Identifies the total count of items in the connection." + totalCount: Int! @cost(weight: "10") +} + +"An edge in a connection." +type BooksTotalEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Book! +} + +"Information about the offset pagination." +type CollectionSegmentInfo { + "Indicates whether more items exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more items exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! +} + +"Information about pagination in a connection." +type PageInfo { + "Indicates whether more edges exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more edges exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! + "When paginating backwards, the cursor to continue." + startCursor: String + "When paginating forwards, the cursor to continue." + endCursor: String +} + +type Query { + books("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: BookFilterInput @cost(weight: "10") order: [BookSortInput!] @cost(weight: "10")): BooksConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + booksWithTotalCount("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: BookFilterInput @cost(weight: "10") order: [BookSortInput!] @cost(weight: "10")): BooksTotalConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + booksOffset(skip: Int take: Int where: BookFilterInput @cost(weight: "10") order: [BookSortInput!] @cost(weight: "10")): BooksOffsetCollectionSegment @listSize(assumedSize: 50, slicingArguments: [ "take" ], sizedFields: [ "items" ], requireOneSlicingArgument: false) @cost(weight: "10") + booksOffsetWithTotalCount(skip: Int take: Int where: BookFilterInput @cost(weight: "10") order: [BookSortInput!] @cost(weight: "10")): BooksTotalCollectionSegment @listSize(assumedSize: 50, slicingArguments: [ "take" ], sizedFields: [ "items" ], requireOneSlicingArgument: false) @cost(weight: "10") +} + +input BookFilterInput { + and: [BookFilterInput!] + or: [BookFilterInput!] + title: StringOperationFilterInput +} + +input BookSortInput { + title: SortEnumType @cost(weight: "10") +} + +input StringOperationFilterInput { + and: [StringOperationFilterInput!] + or: [StringOperationFilterInput!] + eq: String @cost(weight: "10") + neq: String @cost(weight: "10") + contains: String @cost(weight: "20") + ncontains: String @cost(weight: "20") + in: [String] @cost(weight: "10") + nin: [String] @cost(weight: "10") + startsWith: String @cost(weight: "20") + nstartsWith: String @cost(weight: "20") + endsWith: String @cost(weight: "20") + nendsWith: String @cost(weight: "20") +} + +enum SortEnumType { + ASC + DESC +} + +"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response." +directive @cost("The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." weight: String!) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION + +"The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." +directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." slicingArgumentDefaultValue: Int "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis 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." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Ensure_Paging_Defaults_Are_Applied.graphql b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Ensure_Paging_Defaults_Are_Applied.graphql index 2d8d451093e..a442411be1f 100644 --- a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Ensure_Paging_Defaults_Are_Applied.graphql +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Ensure_Paging_Defaults_Are_Applied.graphql @@ -26,7 +26,7 @@ type AuthorsEdge { type Book { title: String! - authors("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): AuthorsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ]) + authors("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): AuthorsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ]) } "A connection to a list of items." @@ -105,10 +105,10 @@ type PageInfo { } type Query { - books("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: BookFilterInput @cost(weight: "10") order: [BookSortInput!] @cost(weight: "10")): BooksConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ]) @cost(weight: "10") - booksWithTotalCount("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: BookFilterInput @cost(weight: "10") order: [BookSortInput!] @cost(weight: "10")): BooksTotalConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ]) @cost(weight: "10") - booksOffset(skip: Int take: Int where: BookFilterInput @cost(weight: "10") order: [BookSortInput!] @cost(weight: "10")): BooksOffsetCollectionSegment @listSize(assumedSize: 50, slicingArguments: [ "take" ], sizedFields: [ "items" ]) @cost(weight: "10") - booksOffsetWithTotalCount(skip: Int take: Int where: BookFilterInput @cost(weight: "10") order: [BookSortInput!] @cost(weight: "10")): BooksTotalCollectionSegment @listSize(assumedSize: 50, slicingArguments: [ "take" ], sizedFields: [ "items" ]) @cost(weight: "10") + books("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: BookFilterInput @cost(weight: "10") order: [BookSortInput!] @cost(weight: "10")): BooksConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ]) @cost(weight: "10") + booksWithTotalCount("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: BookFilterInput @cost(weight: "10") order: [BookSortInput!] @cost(weight: "10")): BooksTotalConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ]) @cost(weight: "10") + booksOffset(skip: Int take: Int where: BookFilterInput @cost(weight: "10") order: [BookSortInput!] @cost(weight: "10")): BooksOffsetCollectionSegment @listSize(assumedSize: 50, slicingArguments: [ "take" ], slicingArgumentDefaultValue: 10, sizedFields: [ "items" ]) @cost(weight: "10") + booksOffsetWithTotalCount(skip: Int take: Int where: BookFilterInput @cost(weight: "10") order: [BookSortInput!] @cost(weight: "10")): BooksTotalCollectionSegment @listSize(assumedSize: 50, slicingArguments: [ "take" ], slicingArgumentDefaultValue: 10, sizedFields: [ "items" ]) @cost(weight: "10") } input BookFilterInput { @@ -145,4 +145,4 @@ enum SortEnumType { directive @cost("The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." weight: String!) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION "The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." -directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis 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." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION +directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." slicingArgumentDefaultValue: Int "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis 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." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Filtering_Not_Used.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Filtering_Not_Used.md index 4b621d04385..a41d781ccde 100644 --- a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Filtering_Not_Used.md +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Filtering_Not_Used.md @@ -17,7 +17,7 @@ ```json { "fieldCost": 6, - "typeCost": 52 + "typeCost": 12 } ``` @@ -33,7 +33,7 @@ "extensions": { "operationCost": { "fieldCost": 11, - "typeCost": 52 + "typeCost": 12 } } } diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Filtering_Specific_Expensive_Filter_Used.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Filtering_Specific_Expensive_Filter_Used.md index 6596be628ee..bed8786b283 100644 --- a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Filtering_Specific_Expensive_Filter_Used.md +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Filtering_Specific_Expensive_Filter_Used.md @@ -17,7 +17,7 @@ ```json { "fieldCost": 10, - "typeCost": 52 + "typeCost": 12 } ``` @@ -33,7 +33,7 @@ "extensions": { "operationCost": { "fieldCost": 42, - "typeCost": 52 + "typeCost": 12 } } } diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Filtering_Specific_Filter_Used.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Filtering_Specific_Filter_Used.md index 13442194c7b..9abe12cd5a4 100644 --- a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Filtering_Specific_Filter_Used.md +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Filtering_Specific_Filter_Used.md @@ -17,7 +17,7 @@ ```json { "fieldCost": 9, - "typeCost": 52 + "typeCost": 12 } ``` @@ -33,7 +33,7 @@ "extensions": { "operationCost": { "fieldCost": 32, - "typeCost": 52 + "typeCost": 12 } } } diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Filtering_Variable.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Filtering_Variable.md index ae154500bc1..9147cb4c836 100644 --- a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Filtering_Variable.md +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Filtering_Variable.md @@ -17,7 +17,7 @@ query($where: BookFilterInput) { ```json { "fieldCost": 10, - "typeCost": 52 + "typeCost": 12 } ``` @@ -33,7 +33,7 @@ query($where: BookFilterInput) { "extensions": { "operationCost": { "fieldCost": 901, - "typeCost": 52 + "typeCost": 12 } } } diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Use_Default_Page_Size_When_Default_Is_Specified.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Use_Default_Page_Size_When_Default_Is_Specified.md new file mode 100644 index 00000000000..39c02b57c43 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Use_Default_Page_Size_When_Default_Is_Specified.md @@ -0,0 +1,185 @@ +# Use_Default_Page_Size_When_Default_Is_Specified + +## Operation + +```graphql +{ + books { + nodes { + title + } + } +} +``` + +## Response + +```json +{ + "data": { + "books": { + "nodes": [] + } + }, + "extensions": { + "operationCost": { + "fieldCost": 11, + "typeCost": 4 + } + } +} +``` + +## Schema + +```graphql +schema { + query: Query +} + +type Author { + name: String! +} + +"A connection to a list of items." +type AuthorsConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [AuthorsEdge!] + "A flattened list of the nodes." + nodes: [Author!] +} + +"An edge in a connection." +type AuthorsEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Author! +} + +type Book { + title: String! + authors("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): AuthorsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 2, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) +} + +"A connection to a list of items." +type BooksConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [BooksEdge!] + "A flattened list of the nodes." + nodes: [Book!] +} + +"An edge in a connection." +type BooksEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Book! +} + +"A segment of a collection." +type BooksOffsetCollectionSegment { + "Information to aid in pagination." + pageInfo: CollectionSegmentInfo! + "A flattened list of the items." + items: [Book!] +} + +"A segment of a collection." +type BooksTotalCollectionSegment { + "Information to aid in pagination." + pageInfo: CollectionSegmentInfo! + "A flattened list of the items." + items: [Book!] + totalCount: Int! @cost(weight: "10") +} + +"A connection to a list of items." +type BooksTotalConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [BooksTotalEdge!] + "A flattened list of the nodes." + nodes: [Book!] + "Identifies the total count of items in the connection." + totalCount: Int! @cost(weight: "10") +} + +"An edge in a connection." +type BooksTotalEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Book! +} + +"Information about the offset pagination." +type CollectionSegmentInfo { + "Indicates whether more items exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more items exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! +} + +"Information about pagination in a connection." +type PageInfo { + "Indicates whether more edges exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more edges exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! + "When paginating backwards, the cursor to continue." + startCursor: String + "When paginating forwards, the cursor to continue." + endCursor: String +} + +type Query { + books("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: BookFilterInput @cost(weight: "10") order: [BookSortInput!] @cost(weight: "10")): BooksConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 2, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + booksWithTotalCount("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: BookFilterInput @cost(weight: "10") order: [BookSortInput!] @cost(weight: "10")): BooksTotalConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 2, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + booksOffset(skip: Int take: Int where: BookFilterInput @cost(weight: "10") order: [BookSortInput!] @cost(weight: "10")): BooksOffsetCollectionSegment @listSize(assumedSize: 50, slicingArguments: [ "take" ], slicingArgumentDefaultValue: 2, sizedFields: [ "items" ], requireOneSlicingArgument: false) @cost(weight: "10") + booksOffsetWithTotalCount(skip: Int take: Int where: BookFilterInput @cost(weight: "10") order: [BookSortInput!] @cost(weight: "10")): BooksTotalCollectionSegment @listSize(assumedSize: 50, slicingArguments: [ "take" ], slicingArgumentDefaultValue: 2, sizedFields: [ "items" ], requireOneSlicingArgument: false) @cost(weight: "10") +} + +input BookFilterInput { + and: [BookFilterInput!] + or: [BookFilterInput!] + title: StringOperationFilterInput +} + +input BookSortInput { + title: SortEnumType @cost(weight: "10") +} + +input StringOperationFilterInput { + and: [StringOperationFilterInput!] + or: [StringOperationFilterInput!] + eq: String @cost(weight: "10") + neq: String @cost(weight: "10") + contains: String @cost(weight: "20") + ncontains: String @cost(weight: "20") + in: [String] @cost(weight: "10") + nin: [String] @cost(weight: "10") + startsWith: String @cost(weight: "20") + nstartsWith: String @cost(weight: "20") + endsWith: String @cost(weight: "20") + nendsWith: String @cost(weight: "20") +} + +enum SortEnumType { + ASC + DESC +} + +"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response." +directive @cost("The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." weight: String!) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION + +"The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." +directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." slicingArgumentDefaultValue: Int "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis 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." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION +``` + diff --git a/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/__snapshots__/IntrospectionClientTests.IntrospectServer.snap b/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/__snapshots__/IntrospectionClientTests.IntrospectServer.snap index 88333bf5516..fe4ae70bfe9 100644 --- a/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/__snapshots__/IntrospectionClientTests.IntrospectServer.snap +++ b/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/__snapshots__/IntrospectionClientTests.IntrospectServer.snap @@ -128,6 +128,6 @@ scalar Long directive @cost("The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." weight: String!) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION "The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." -directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis 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." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION +directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." slicingArgumentDefaultValue: Int "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis 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." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION directive @foo(bar: Int!) on SUBSCRIPTION diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/__snapshots__/StarWarsIntrospectionTest.Execute_StarWarsIntrospection_Test.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/__snapshots__/StarWarsIntrospectionTest.Execute_StarWarsIntrospection_Test.snap index 112324bc208..cb0b9828ac7 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/__snapshots__/StarWarsIntrospectionTest.Execute_StarWarsIntrospection_Test.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/__snapshots__/StarWarsIntrospectionTest.Execute_StarWarsIntrospection_Test.snap @@ -2462,6 +2462,16 @@ }, "DefaultValue": null }, + { + "Name": "slicingArgumentDefaultValue", + "Description": "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query.", + "Type": { + "Kind": "Scalar", + "Name": "Int", + "OfType": null + }, + "DefaultValue": null + }, { "Name": "sizedFields", "Description": "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields.", @@ -6824,6 +6834,24 @@ }, "DefaultValue": null }, + { + "__typename": "__InputValue", + "Name": "slicingArgumentDefaultValue", + "Description": "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query.", + "Type": { + "__typename": "__Type", + "Name": "Int", + "Kind": "Scalar", + "Description": null, + "Fields": null, + "InputFields": null, + "Interfaces": null, + "EnumValues": null, + "PossibleTypes": null, + "OfType": null + }, + "DefaultValue": null + }, { "__typename": "__InputValue", "Name": "sizedFields",