From 393f332e81b2f95c7cecbeb36b849a22566c3e48 Mon Sep 17 00:00:00 2001 From: Glen Date: Wed, 5 Jun 2024 10:56:05 +0200 Subject: [PATCH] Add IBM cost analysis --- .build/Build.Tests.2.cs | 4 + .build/Helpers.cs | 1 + .../Execution/HotChocolate.Execution.csproj | 1 + .../Core/src/Types/HotChocolate.Types.csproj | 1 + .../CostAnalysis/Directory.Build.props | 10 + .../HotChocolate.CostAnalysis.sln | 36 + .../CostAnalysis/Attributes/CostAttribute.cs | 66 ++ .../Attributes/ListSizeAttribute.cs | 55 ++ .../Caching/DefaultCostMetricsCache.cs | 25 + .../CostAnalysis/Caching/ICostMetricsCache.cs | 53 ++ .../CostAnalysis/src/CostAnalysis/Cost.cs | 7 + .../CostAnalysis/CostAnalysisMiddleware.cs | 108 +++ .../src/CostAnalysis/CostAnalysisOptions.cs | 14 + .../src/CostAnalysis/CostAnalysisVisitor.cs | 775 ++++++++++++++++++ .../src/CostAnalysis/CostByLocation.cs | 8 + .../src/CostAnalysis/CostCountType.cs | 8 + .../CostIntrospectionTypeInterceptor.cs | 67 ++ .../src/CostAnalysis/CostMetrics.cs | 33 + .../CostArgumentDescriptorExtensions.cs | 35 + .../CostEnumTypeDescriptorExtensions.cs | 66 ++ .../CostInputFieldDescriptorExtensions.cs | 35 + .../CostObjectFieldDescriptorExtensions.cs | 35 + .../CostObjectTypeDescriptorExtensions.cs | 35 + ...ListSizeObjectFieldDescriptorExtensions.cs | 55 ++ .../CostAnalysis/Directives/CostDirective.cs | 21 + .../Directives/CostDirectiveType.cs | 37 + .../Directives/ListSizeDirective.cs | 56 ++ .../Directives/ListSizeDirectiveType.cs | 56 ++ .../src/CostAnalysis/ErrorHelper.cs | 41 + .../HotChocolate.CostAnalysis.csproj | 28 + .../src/CostAnalysis/NodeContextData.cs | 49 ++ .../CostAnalysisResources.Designer.cs | 89 ++ .../Properties/CostAnalysisResources.resx | 30 + .../Properties/InternalsVisibleTo.cs | 3 + .../RequestExecutorBuilderExtensions.cs | 91 ++ .../CostAnalysis/Types/CostByLocationType.cs | 5 + .../CostAnalysis/Types/CostCountTypeType.cs | 5 + .../src/CostAnalysis/Types/CostMetricsType.cs | 122 +++ .../src/CostAnalysis/Types/CostType.cs | 6 + .../CostAnalysis/WellKnownArgumentNames.cs | 11 + .../src/CostAnalysis/WellKnownContextData.cs | 6 + .../CostAnalysis/src/Directory.Build.props | 4 + .../test/CostAnalysis.Tests/AttributeTests.cs | 150 ++++ .../test/CostAnalysis.Tests/CachingTests.cs | 85 ++ .../DescriptorExtensionTests.cs | 165 ++++ .../Doubles/FakeCostMetricsCache.cs | 48 ++ .../HotChocolate.CostAnalysis.Tests.csproj | 14 + .../CostAnalysis.Tests/IntrospectionTests.cs | 337 ++++++++ .../SpecificationExampleTests.cs | 187 +++++ .../StaticQueryAnalysisTests.cs | 376 +++++++++ ...ecute_CostQuery_ReturnsExpectedResult_0.md | 715 ++++++++++++++++ ...ecute_CostQuery_ReturnsExpectedResult_1.md | 145 ++++ ...ecute_CostQuery_ReturnsExpectedResult_2.md | 137 ++++ ...ecute_CostQuery_ReturnsExpectedResult_3.md | 129 +++ ...ecute_CostQuery_ReturnsExpectedResult_4.md | 141 ++++ ...ecute_CostQuery_ReturnsExpectedResult_5.md | 145 ++++ ...ecute_CostQuery_ReturnsExpectedResult_6.md | 129 +++ ...ecute_CostQuery_ReturnsExpectedResult_7.md | 308 +++++++ ...ecute_CostQuery_ReturnsExpectedResult_8.md | 143 ++++ ...ficationExample_ReturnsExpectedResult_0.md | 65 ++ ...ficationExample_ReturnsExpectedResult_1.md | 53 ++ ...ficationExample_ReturnsExpectedResult_2.md | 61 ++ ...ficationExample_ReturnsExpectedResult_3.md | 58 ++ ...ficationExample_ReturnsExpectedResult_4.md | 66 ++ ...ficationExample_ReturnsExpectedResult_5.md | 69 ++ ...ficationExample_ReturnsExpectedResult_6.md | 60 ++ ...ConnectionQuery_ReturnsExpectedResult_0.md | 245 ++++++ ...ecute_ListQuery_ReturnsExpectedResult_0.md | 94 +++ ...ecute_ListQuery_ReturnsExpectedResult_1.md | 94 +++ ...ecute_ListQuery_ReturnsExpectedResult_2.md | 94 +++ ...ecute_ListQuery_ReturnsExpectedResult_3.md | 94 +++ ...ecute_ListQuery_ReturnsExpectedResult_4.md | 46 ++ ...ecute_ListQuery_ReturnsExpectedResult_5.md | 95 +++ ...ecute_ListQuery_ReturnsExpectedResult_6.md | 47 ++ ...ecute_ListQuery_ReturnsExpectedResult_7.md | 90 ++ ...ecute_ListQuery_ReturnsExpectedResult_8.md | 94 +++ .../CostAnalysis/test/Directory.Build.props | 42 + 77 files changed, 7014 insertions(+) create mode 100644 src/HotChocolate/CostAnalysis/Directory.Build.props create mode 100644 src/HotChocolate/CostAnalysis/HotChocolate.CostAnalysis.sln create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Attributes/CostAttribute.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Attributes/ListSizeAttribute.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Caching/DefaultCostMetricsCache.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Caching/ICostMetricsCache.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Cost.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalysisMiddleware.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalysisOptions.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalysisVisitor.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/CostByLocation.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/CostCountType.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/CostIntrospectionTypeInterceptor.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/CostMetrics.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostArgumentDescriptorExtensions.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostEnumTypeDescriptorExtensions.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostInputFieldDescriptorExtensions.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostObjectFieldDescriptorExtensions.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostObjectTypeDescriptorExtensions.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/ListSizeObjectFieldDescriptorExtensions.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Directives/CostDirective.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Directives/CostDirectiveType.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Directives/ListSizeDirective.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Directives/ListSizeDirectiveType.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/ErrorHelper.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/HotChocolate.CostAnalysis.csproj create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/NodeContextData.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Properties/CostAnalysisResources.Designer.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Properties/CostAnalysisResources.resx create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Properties/InternalsVisibleTo.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/RequestExecutorBuilderExtensions.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/CostByLocationType.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/CostCountTypeType.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/CostMetricsType.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/CostType.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/WellKnownArgumentNames.cs create mode 100644 src/HotChocolate/CostAnalysis/src/CostAnalysis/WellKnownContextData.cs create mode 100644 src/HotChocolate/CostAnalysis/src/Directory.Build.props create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/AttributeTests.cs create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/CachingTests.cs create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/DescriptorExtensionTests.cs create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/Doubles/FakeCostMetricsCache.cs create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/HotChocolate.CostAnalysis.Tests.csproj create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/IntrospectionTests.cs create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/SpecificationExampleTests.cs create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/StaticQueryAnalysisTests.cs create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_0.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_1.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_2.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_3.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_4.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_5.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_6.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_7.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_8.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_0.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_1.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_2.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_3.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_4.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_5.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_6.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ConnectionQuery_ReturnsExpectedResult_0.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_0.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_1.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_2.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_3.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_4.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_5.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_6.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_7.md create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_8.md create mode 100644 src/HotChocolate/CostAnalysis/test/Directory.Build.props diff --git a/.build/Build.Tests.2.cs b/.build/Build.Tests.2.cs index edbc34dabe3..9b1dee01101 100644 --- a/.build/Build.Tests.2.cs +++ b/.build/Build.Tests.2.cs @@ -49,6 +49,10 @@ partial class Build .Produces(TestResultDirectory / "*.trx") .Executes(() => RunTests(SourceDirectory / "HotChocolate" / "Core" / "HotChocolate.Core.sln")); + Target TestHotChocolateCostAnalysis => _ => _ + .Produces(TestResultDirectory / "*.trx") + .Executes(() => RunTests(SourceDirectory / "HotChocolate" / "CostAnalysis" / "HotChocolate.CostAnalysis.sln")); + Target TestHotChocolateData => _ => _ .Produces(TestResultDirectory / "*.trx") .Executes(() => RunTests(SourceDirectory / "HotChocolate" / "Data" / "HotChocolate.Data.sln")); diff --git a/.build/Helpers.cs b/.build/Helpers.cs index 2b1e0611489..106292c307a 100644 --- a/.build/Helpers.cs +++ b/.build/Helpers.cs @@ -16,6 +16,7 @@ static class Helpers Path.Combine("HotChocolate", "AspNetCore"), Path.Combine("HotChocolate", "AzureFunctions"), Path.Combine("HotChocolate", "Core"), + Path.Combine("HotChocolate", "CostAnalysis"), Path.Combine("HotChocolate", "Caching"), Path.Combine("HotChocolate", "Diagnostics"), Path.Combine("HotChocolate", "Language"), diff --git a/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj b/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj index 086a0c66e6e..07263d9b0fd 100644 --- a/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj +++ b/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj @@ -14,6 +14,7 @@ + diff --git a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj index bdf44ee9716..ce403c60f97 100644 --- a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj +++ b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj @@ -16,6 +16,7 @@ + diff --git a/src/HotChocolate/CostAnalysis/Directory.Build.props b/src/HotChocolate/CostAnalysis/Directory.Build.props new file mode 100644 index 00000000000..70f4818b593 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/Directory.Build.props @@ -0,0 +1,10 @@ + + + + + $(Library3TargetFrameworks) + enable + enable + + + diff --git a/src/HotChocolate/CostAnalysis/HotChocolate.CostAnalysis.sln b/src/HotChocolate/CostAnalysis/HotChocolate.CostAnalysis.sln new file mode 100644 index 00000000000..35364bef7dd --- /dev/null +++ b/src/HotChocolate/CostAnalysis/HotChocolate.CostAnalysis.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3F8C3AE9-6085-43F1-A593-CC7C0B9A4989}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.CostAnalysis", "src\CostAnalysis\HotChocolate.CostAnalysis.csproj", "{CBC13F2C-A2CC-43C7-A60B-DA83ADCD0314}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{10D8894D-98C1-4F32-A6DE-8E2268E0D69B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.CostAnalysis.Tests", "test\CostAnalysis.Tests\HotChocolate.CostAnalysis.Tests.csproj", "{6F56773B-1192-4951-8F4A-6C2E5835410D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CBC13F2C-A2CC-43C7-A60B-DA83ADCD0314}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBC13F2C-A2CC-43C7-A60B-DA83ADCD0314}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBC13F2C-A2CC-43C7-A60B-DA83ADCD0314}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBC13F2C-A2CC-43C7-A60B-DA83ADCD0314}.Release|Any CPU.Build.0 = Release|Any CPU + {6F56773B-1192-4951-8F4A-6C2E5835410D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F56773B-1192-4951-8F4A-6C2E5835410D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F56773B-1192-4951-8F4A-6C2E5835410D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F56773B-1192-4951-8F4A-6C2E5835410D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {CBC13F2C-A2CC-43C7-A60B-DA83ADCD0314} = {3F8C3AE9-6085-43F1-A593-CC7C0B9A4989} + {6F56773B-1192-4951-8F4A-6C2E5835410D} = {10D8894D-98C1-4F32-A6DE-8E2268E0D69B} + EndGlobalSection +EndGlobal diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Attributes/CostAttribute.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Attributes/CostAttribute.cs new file mode 100644 index 00000000000..85dda64d8a0 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Attributes/CostAttribute.cs @@ -0,0 +1,66 @@ +using System.Reflection; +using HotChocolate.CostAnalysis.Directives; +using HotChocolate.Types; +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.CostAnalysis.Attributes; + +[AttributeUsage( + AttributeTargets.Class + | AttributeTargets.Enum + | AttributeTargets.Method + | AttributeTargets.Parameter + | AttributeTargets.Property + | AttributeTargets.Struct)] +public sealed class CostAttribute : DescriptorAttribute +{ + private readonly string _weight; + + /// + /// Applies the @cost directive. 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. + /// + /// + /// The weight argument defines what value to add to the overall cost for every + /// appearance, or possible appearance, of a type, field, argument, etc. + /// + public CostAttribute(string weight) + { + if (weight is null) + { + throw new ArgumentNullException(nameof(weight)); + } + + _weight = weight; + } + + protected internal override void TryConfigure( + IDescriptorContext context, + IDescriptor descriptor, + ICustomAttributeProvider element) + { + switch (descriptor) + { + case IArgumentDescriptor argumentDescriptor: + argumentDescriptor.Directive(new CostDirective(_weight)); + break; + + case IEnumTypeDescriptor enumTypeDescriptor: + enumTypeDescriptor.Directive(new CostDirective(_weight)); + break; + + case IInputFieldDescriptor inputFieldDescriptor: + inputFieldDescriptor.Directive(new CostDirective(_weight)); + break; + + case IObjectFieldDescriptor objectFieldDescriptor: + objectFieldDescriptor.Directive(new CostDirective(_weight)); + break; + + case IObjectTypeDescriptor objectTypeDescriptor: + objectTypeDescriptor.Directive(new CostDirective(_weight)); + break; + } + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Attributes/ListSizeAttribute.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Attributes/ListSizeAttribute.cs new file mode 100644 index 00000000000..5f316431945 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Attributes/ListSizeAttribute.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using HotChocolate.CostAnalysis.Directives; +using HotChocolate.Types; +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.CostAnalysis.Attributes; + +/// +/// Applies the @listSize directive. 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. +/// +public sealed class ListSizeAttribute : ObjectFieldDescriptorAttribute +{ + private readonly int? _assumedSize; + + /// + /// The maximum length of the list returned by this field. + /// + public int AssumedSize + { + get => _assumedSize ?? 0; + init => _assumedSize = value; + } + + /// + /// The arguments of this field with numeric type that are slicing arguments. Their value + /// determines the size of the returned list. + /// + public string[]? SlicingArguments { get; set; } + + /// + /// The subfield(s) that the list size applies to. + /// + public string[]? SizedFields { get; set; } + + /// + /// Whether to require a single slicing argument in the query. If that is not the case (i.e., if + /// none or multiple slicing arguments are present), the static analysis will throw an error. + /// + public bool RequireOneSlicingArgument { get; init; } = true; + + protected override void OnConfigure( + IDescriptorContext context, + IObjectFieldDescriptor descriptor, + MemberInfo member) + { + descriptor.Directive( + new ListSizeDirective( + _assumedSize, + SlicingArguments?.ToList(), + SizedFields?.ToList(), + RequireOneSlicingArgument)); + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Caching/DefaultCostMetricsCache.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Caching/DefaultCostMetricsCache.cs new file mode 100644 index 00000000000..484b57c21e0 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Caching/DefaultCostMetricsCache.cs @@ -0,0 +1,25 @@ +using System.Diagnostics.CodeAnalysis; +using HotChocolate.Utilities; + +namespace HotChocolate.CostAnalysis.Caching; + +internal sealed class DefaultCostMetricsCache(int capacity = 100) : ICostMetricsCache +{ + private readonly Cache _cache = new(capacity); + + public int Capacity => _cache.Capacity; + + public int Count => _cache.Usage; + + public bool TryGetCostMetrics( + string operationId, + [NotNullWhen(true)] out CostMetrics? costMetrics) + => _cache.TryGet(operationId, out costMetrics); + + public void TryAddCostMetrics( + string operationId, + CostMetrics costMetrics) + => _cache.GetOrCreate(operationId, () => costMetrics); + + public void Clear() => _cache.Clear(); +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Caching/ICostMetricsCache.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Caching/ICostMetricsCache.cs new file mode 100644 index 00000000000..15d44e5bd7a --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Caching/ICostMetricsCache.cs @@ -0,0 +1,53 @@ +using System.Diagnostics.CodeAnalysis; + +namespace HotChocolate.CostAnalysis.Caching; + +internal interface ICostMetricsCache +{ + /// + /// Gets the maximum number of CostMetrics instances that can be cached. The default + /// value is 100. The minimum allowed value is 10. + /// + int Capacity { get; } + + /// + /// Gets the number of CostMetrics instances residing in the cache. + /// + int Count { get; } + + /// + /// Tries to get a CostMetrics instance by . + /// + /// + /// The internal operation ID. + /// + /// + /// The CostMetrics instance that is associated with the ID or null + /// if no CostMetrics instance was found that matches the specified ID. + /// + /// + /// true if a CostMetrics instance was found that matches the specified + /// , otherwise false. + /// + bool TryGetCostMetrics( + string operationId, + [NotNullWhen(true)] out CostMetrics? costMetrics); + + /// + /// Tries to add a new CostMetrics instance to the cache. + /// + /// + /// The internal operation ID. + /// + /// + /// The CostMetrics instance that shall be cached. + /// + void TryAddCostMetrics( + string operationId, + CostMetrics costMetrics); + + /// + /// Clears all items from the cache. + /// + void Clear(); +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Cost.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Cost.cs new file mode 100644 index 00000000000..3664af701e4 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Cost.cs @@ -0,0 +1,7 @@ +namespace HotChocolate.CostAnalysis; + +/// https://ibm.github.io/graphql-specs/cost-spec.html#sec-__cost +internal sealed class Cost(CostMetrics requestCosts) +{ + public CostMetrics RequestCosts { get; } = requestCosts; +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalysisMiddleware.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalysisMiddleware.cs new file mode 100644 index 00000000000..557cea1c249 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalysisMiddleware.cs @@ -0,0 +1,108 @@ +using HotChocolate.CostAnalysis.Caching; +using HotChocolate.Execution; +using HotChocolate.Execution.Processing; +using HotChocolate.Language; +using HotChocolate.Validation; +using Microsoft.Extensions.DependencyInjection; +using static HotChocolate.CostAnalysis.WellKnownContextData; + +namespace HotChocolate.CostAnalysis; + +internal sealed class CostAnalysisMiddleware( + RequestDelegate next, + CostAnalysisOptions options, + DocumentValidatorContextPool contextPool, + ICostMetricsCache cache, + CostAnalysisVisitor costAnalysisVisitor) +{ + public async ValueTask InvokeAsync(IRequestContext context) + { + if (context.Document is not null && context.OperationId is not null) + { + var document = context.Document; + var operationDefinition = + context.Operation?.Definition ?? + document.GetOperation(context.Request.OperationName); + + var validatorContext = contextPool.Get(); + + try + { + if (!cache.TryGetCostMetrics(context.OperationId, out var costMetrics)) + { + PrepareContext(context, document, validatorContext); + + costAnalysisVisitor.Visit(operationDefinition, validatorContext); + + costMetrics = (CostMetrics)validatorContext.ContextData[RequestCosts]!; + + cache.TryAddCostMetrics(context.OperationId, costMetrics); + } + + if (costMetrics.FieldCost > options.MaxFieldCost) + { + // FIXME: This is not ending the request. + context.Result = ErrorHelper.MaxFieldCostReached( + costMetrics.FieldCost, + options.MaxFieldCost); + + return; + } + + if (costMetrics.TypeCost > options.MaxTypeCost) + { + // FIXME: This is not ending the request. + context.Result = ErrorHelper.MaxTypeCostReached( + costMetrics.TypeCost, + options.MaxTypeCost); + + return; + } + } + finally + { + validatorContext.Clear(); + contextPool.Return(validatorContext); + } + } + + await next(context).ConfigureAwait(false); + } + + private static void PrepareContext( + IRequestContext requestContext, + DocumentNode document, + DocumentValidatorContext validatorContext) + { + validatorContext.Schema = requestContext.Schema; + + foreach (var definitionNode in document.Definitions) + { + if (definitionNode is FragmentDefinitionNode fragmentDefinition) + { + validatorContext.Fragments[fragmentDefinition.Name.Value] = fragmentDefinition; + } + } + + validatorContext.ContextData = requestContext.ContextData; + } + + public static RequestCoreMiddleware Create() + { + return (core, next) => + { + var options = core.Services.GetRequiredService(); + var contextPool = core.Services.GetRequiredService(); + var cache = core.Services.GetRequiredService(); + + var middleware = new CostAnalysisMiddleware( + next, + options, + contextPool, + cache, + new CostAnalysisVisitor()); + + return context => middleware.InvokeAsync(context); + }; + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalysisOptions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalysisOptions.cs new file mode 100644 index 00000000000..ada8a0f25b3 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalysisOptions.cs @@ -0,0 +1,14 @@ +namespace HotChocolate.CostAnalysis; + +public sealed class CostAnalysisOptions +{ + /// + /// Gets or sets the maximum allowed field cost. + /// + public double MaxFieldCost { get; set; } = 1_000; + + /// + /// Gets or sets the maximum allowed type cost. + /// + public double MaxTypeCost { get; set; } = 1_000; +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalysisVisitor.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalysisVisitor.cs new file mode 100644 index 00000000000..4f6689154d5 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalysisVisitor.cs @@ -0,0 +1,775 @@ +using System.Diagnostics; +using HotChocolate.CostAnalysis.Directives; +using HotChocolate.Language; +using HotChocolate.Language.Visitors; +using HotChocolate.Types; +using HotChocolate.Validation; +using static HotChocolate.CostAnalysis.WellKnownArgumentNames; +using CostDirective = HotChocolate.CostAnalysis.Directives.CostDirective; +using IHasDirectives = HotChocolate.Types.IHasDirectives; + +namespace HotChocolate.CostAnalysis; + +internal sealed class CostAnalysisVisitor() : TypeDocumentValidatorVisitor( + new SyntaxVisitorOptions + { + VisitArguments = true, + VisitDirectives = true + }) +{ + private const int DefaultListSize = 1; + private const string Index = "Index"; + private const string FieldListSize = "FieldListSize"; + private const string ListItemCount = "ListItemCount"; + private const string ListIndex = "ListIndex"; + + private readonly CostMetrics _requestCosts = new(); + private readonly List _pathElements = []; + private readonly NodeContextData _nodeContextData = []; + private readonly Dictionary _parentFieldListSizes = []; + + private int _fieldCount = 1; + private int _listSizeProduct = 1; + private string _path = ""; + + protected override ISyntaxVisitorAction Enter( + OperationDefinitionNode node, + IDocumentValidatorContext context) + { + AddPathElement(context, node); + + Debug.WriteLine($"Entering OperationDefinitionNode: {_path}"); + + var action = base.Enter(node, context); + + // Example: Query + _requestCosts.TypeCounts.Increment(context.Types.Peek().NamedType().Name); + + return action; + } + + protected override ISyntaxVisitorAction Enter( + VariableDefinitionNode node, + IDocumentValidatorContext context) + { + AddPathElement(context, node); + + Debug.WriteLine($"Entering VariableDefinitionNode: {_path}"); + + _requestCosts.FieldCostByLocation[_path] = 0; + + return base.Enter(node, context); + } + + protected override ISyntaxVisitorAction Enter( + DirectiveNode node, + IDocumentValidatorContext context) + { + AddPathElement(context, node); + + Debug.WriteLine($"Entering DirectiveNode: {_path}"); + + _requestCosts.FieldCostByLocation[_path] = 0; + + if (context.Schema.TryGetDirectiveType(node.Name.Value, out var directive)) + { + // Example: @directive + var schemaCoordinate = new SchemaCoordinate(directive.Name, ofDirective: true); + + _requestCosts.DirectiveCounts.Increment(schemaCoordinate.ToString(), _fieldCount); + + context.Directives.Push(directive); + + return Continue; + } + + return Skip; + } + + protected override ISyntaxVisitorAction Enter( + ArgumentNode node, + IDocumentValidatorContext context) + { + AddPathElement(context, node); + + Debug.WriteLine($"Entering ArgumentNode: {_path}"); + + if (context.Directives.TryPeek(out var directive) && + directive.Arguments.TryGetField(node.Name.Value, out var directiveArgument)) + { + // Example: @directive(argument:) + var schemaCoordinate = new SchemaCoordinate( + directive.Name, + argumentName: directiveArgument.Name, + ofDirective: true); + + _requestCosts.ArgumentCounts.Increment(schemaCoordinate.ToString(), _fieldCount); + _requestCosts.FieldCostByLocation.Increment( + _path, + GetCostWeight(directiveArgument) * _fieldCount); + + if (directiveArgument.Type.NamedType() is InputObjectType inputType) + { + _requestCosts.InputTypeCounts.Increment(inputType.Name, _fieldCount); + } + + context.InputFields.Push(directiveArgument); + context.Types.Push(directiveArgument.Type); + + return Continue; + } + + if (context.OutputFields.TryPeek(out var field) && + field.Arguments.TryGetField(node.Name.Value, out var fieldArgument)) + { + // Example: Type.field(argument:) + var schemaCoordinate = new SchemaCoordinate( + field.DeclaringType.Name, + field.Name, + fieldArgument.Name); + + _requestCosts.ArgumentCounts.Increment(schemaCoordinate.ToString(), _fieldCount); + _requestCosts.FieldCostByLocation.Increment( + _path, + GetCostWeight(fieldArgument) * _fieldCount); + + if (fieldArgument.Type.NamedType() is InputObjectType inputType) + { + _requestCosts.InputTypeCounts.Increment(inputType.Name, _fieldCount); + } + + context.InputFields.Push(fieldArgument); + context.Types.Push(fieldArgument.Type); + + return Continue; + } + + return Skip; + } + + protected override ISyntaxVisitorAction Enter( + ObjectFieldNode node, + IDocumentValidatorContext context) + { + AddPathElement(context, node); + + Debug.WriteLine($"Entering ObjectFieldNode: {_path}"); + + if (context.Types.TryPeek(out var type) && + type.NamedType() is InputObjectType inputType && + inputType.Fields.TryGetField(node.Name.Value, out var inputField)) + { + _requestCosts.InputFieldCounts.Increment(inputField.Coordinate.ToString(), _fieldCount); + _requestCosts.FieldCostByLocation.Increment( + _path, + GetCostWeight(inputField) * _fieldCount); + + if (inputField.Type.NamedType() is InputObjectType inputFieldType) + { + _requestCosts.InputTypeCounts.Increment(inputFieldType.Name, _fieldCount); + } + + context.InputFields.Push(inputField); + context.Types.Push(inputField.Type); + + return Continue; + } + + return Skip; + } + + protected override ISyntaxVisitorAction Leave( + ObjectFieldNode node, + IDocumentValidatorContext context) + { + Debug.WriteLine($"Leaving ObjectFieldNode: {_path}"); + + var childCost = _requestCosts.FieldCostByLocation[_path]; + RemovePathElement(); + _requestCosts.FieldCostByLocation.Increment(_path, childCost); + + context.InputFields.Pop(); + context.Types.Pop(); + + return Continue; + } + + protected override ISyntaxVisitorAction Leave( + ArgumentNode node, + IDocumentValidatorContext context) + { + Debug.WriteLine($"Leaving ArgumentNode: {_path}"); + + var childCost = _requestCosts.FieldCostByLocation[_path]; + RemovePathElement(); + _requestCosts.FieldCostByLocation.Increment(_path, childCost); + + context.InputFields.Pop(); + context.Types.Pop(); + + return Continue; + } + + protected override ISyntaxVisitorAction Leave( + DirectiveNode node, + IDocumentValidatorContext context) + { + Debug.WriteLine($"Leaving DirectiveNode: {_path}"); + + var childCost = _requestCosts.FieldCostByLocation[_path]; + RemovePathElement(); + _requestCosts.FieldCostByLocation.Increment(_path, childCost); + + context.Directives.Pop(); + + return Continue; + } + + protected override ISyntaxVisitorAction Leave( + VariableDefinitionNode node, + IDocumentValidatorContext context) + { + Debug.WriteLine($"Leaving VariableDefinitionNode: {_path}"); + + var childCost = _requestCosts.FieldCostByLocation[_path]; + RemovePathElement(); + _requestCosts.FieldCostByLocation.Increment(_path, childCost); + + return Continue; + } + + protected override ISyntaxVisitorAction Enter( + SelectionSetNode node, + IDocumentValidatorContext context) + { + Debug.WriteLine($"Entering SelectionSetNode: {_path}"); + + var uniqueNodeCounts = new Dictionary(); + + // Calculate counts per unique node (by name). + foreach (var selection in node.Selections) + { + if (selection is FieldNode or InlineFragmentNode) + { + uniqueNodeCounts.Increment(GetNodeName(selection)); + } + } + + // Remove counts <= 1. + var uniqueNodeCountsAboveOne = new Dictionary(); + + foreach (var (name, count) in uniqueNodeCounts) + { + if (count > 1) + { + uniqueNodeCountsAboveOne.Add(name, count); + } + } + + // Assign indexes to nodes for later use. + for (var i = node.Selections.Count - 1; i >= 0; i--) + { + var selection = node.Selections[i]; + + if (selection is FieldNode or InlineFragmentNode) + { + var nodeName = GetNodeName(selection); + + if (uniqueNodeCountsAboveOne.TryGetValue(nodeName, out var count)) + { + _nodeContextData.Set(selection, Index, count - 1); + + uniqueNodeCountsAboveOne[nodeName] = count - 1; + } + } + } + + return Continue; + } + + protected override ISyntaxVisitorAction Enter( + FieldNode node, + IDocumentValidatorContext context) + { + if (context.Types.TryPeek(out var type) && + type.NamedType() is IComplexOutputType outputType && + outputType.Fields.TryGetField(node.Name.Value, out var outputField)) + { + // Skip introspection fields. + if (outputField.IsIntrospectionField) + { + return Skip; + } + + AddPathElement(context, node); + Debug.WriteLine($"Entering FieldNode: {_path}"); + + // Capture the field count to be used for other counts within the field. + _fieldCount = _listSizeProduct; + + // Example: Type.field + _requestCosts.FieldCounts.Increment(outputField.Coordinate.ToString(), _fieldCount); + _requestCosts.FieldCostByLocation.Increment( + _path, + GetCostWeight(outputField) * _fieldCount); + + var listSizeDirective = outputField.Directives.FirstOrDefault(); + + if (listSizeDirective is null) + { + // Check parent field for listSize directive. + if (context.OutputFields.TryPeek(out var parentField)) + { + listSizeDirective = parentField.Directives.FirstOrDefault(); + + if (listSizeDirective is not null) + { + var sizedFields = + listSizeDirective.GetArgumentValue?>(SizedFields); + + if (sizedFields?.Contains(outputField.Name) == true) + { + // Take list size from parent field. + _nodeContextData.Set( + node, + FieldListSize, + _parentFieldListSizes[parentField]); + + _listSizeProduct *= _parentFieldListSizes[parentField]; + } + } + } + } + else + { + var sizedFields = listSizeDirective.GetArgumentValue?>(SizedFields); + var listSize = GetListSize(context, listSizeDirective, node, outputField); + + if (sizedFields is null) + { + // Apply the list size to the current field. + _nodeContextData.Set(node, FieldListSize, listSize); + + _listSizeProduct *= listSize; + } + else + { + // Store the list size to be accessed via the parent field. + _parentFieldListSizes.Add(outputField, listSize); + } + } + + // Example: Type + _requestCosts.TypeCounts.Increment(outputField.Type.NamedType().Name, _listSizeProduct); + _requestCosts.TypeCostByLocation.Increment( + _path, + GetCostWeight(outputField.Type.NamedType()) * _listSizeProduct); + + context.OutputFields.Push(outputField); + context.Types.Push(outputField.Type); + + return Continue; + } + + return Skip; + } + + protected override ISyntaxVisitorAction Enter( + ListValueNode node, + IDocumentValidatorContext context) + { + _nodeContextData.Set(node, ListItemCount, node.Items.Count); + _nodeContextData.Set(node, ListIndex, 0); + + return Continue; + } + + protected override ISyntaxVisitorAction Enter( + ObjectValueNode node, + IDocumentValidatorContext context) + { + if (context.Path.Peek() is ListValueNode listValueNode) + { + var itemCount = _nodeContextData.Get(listValueNode, ListItemCount); + var currentIndex = _nodeContextData.Get(listValueNode, ListIndex); + + if (itemCount > 1) + { + AddPathElement($"[{currentIndex}]"); + _nodeContextData.Set(listValueNode, ListIndex, ++currentIndex); + } + } + + return Continue; + } + + protected override ISyntaxVisitorAction Leave( + ObjectValueNode node, + IDocumentValidatorContext context) + { + if (context.Path.Peek() is ListValueNode) + { + var childCost = _requestCosts.FieldCostByLocation[_path]; + RemovePathElement(); + _requestCosts.FieldCostByLocation.Increment(_path, childCost); + } + + return Continue; + } + + protected override ISyntaxVisitorAction Leave( + ListValueNode node, + IDocumentValidatorContext context) + { + _nodeContextData.Remove(node, ListItemCount); + + return Continue; + } + + protected override ISyntaxVisitorAction Enter( + InlineFragmentNode node, + IDocumentValidatorContext context) + { + AddPathElement(context, node); + + Debug.WriteLine($"Entering InlineFragmentNode: {_path}"); + + return base.Enter(node, context); + } + + protected override ISyntaxVisitorAction Leave( + InlineFragmentNode node, + IDocumentValidatorContext context) + { + Debug.WriteLine($"Leaving InlineFragmentNode: {_path}"); + + var childCost = _requestCosts.FieldCostByLocation[_path]; + RemovePathElement(); + _requestCosts.FieldCostByLocation.Increment(_path, childCost); + + return base.Leave(node, context); + } + + protected override ISyntaxVisitorAction Enter( + FragmentSpreadNode node, + IDocumentValidatorContext context) + { + AddPathElement(context, node); + + Debug.WriteLine($"Entering FragmentSpreadNode: {_path}"); + + return Continue; + } + + protected override ISyntaxVisitorAction Leave( + FragmentSpreadNode node, + IDocumentValidatorContext context) + { + Debug.WriteLine($"Leaving FragmentSpreadNode: {_path}"); + + var childCost = _requestCosts.FieldCostByLocation[_path]; + RemovePathElement(); + _requestCosts.FieldCostByLocation.Increment(_path, childCost); + + return Continue; + } + + protected override ISyntaxVisitorAction Enter( + FragmentDefinitionNode node, + IDocumentValidatorContext context) + { + // FIXME: Fragment definitions (~~fragment) should start from the root. + AddPathElement(context, node); + + Debug.WriteLine($"Entering FragmentDefinitionNode: {_path}"); + + return base.Enter(node, context); + } + + protected override ISyntaxVisitorAction Leave( + FragmentDefinitionNode node, + IDocumentValidatorContext context) + { + Debug.WriteLine($"Leaving FragmentDefinitionNode: {_path}"); + + var childCost = _requestCosts.FieldCostByLocation[_path]; + RemovePathElement(); + _requestCosts.FieldCostByLocation.Increment(_path, childCost); + + return base.Leave(node, context); + } + + protected override ISyntaxVisitorAction Leave( + FieldNode node, + IDocumentValidatorContext context) + { + Debug.WriteLine($"Leaving FieldNode: {_path}"); + + // https://ibm.github.io/graphql-specs/cost-spec.html#sec-weight + // "The overall calculated weight of a field or directive can never be negative, and if + // found to be negative must be rounded up to zero" + var childFieldCost = Math.Max(_requestCosts.FieldCostByLocation[_path], 0); + var childTypeCost = Math.Max(_requestCosts.TypeCostByLocation[_path], 0); + RemovePathElement(); + _requestCosts.FieldCostByLocation.Increment(_path, childFieldCost); + _requestCosts.TypeCostByLocation.Increment(_path, childTypeCost); + + context.Types.Pop(); + var outputField = context.OutputFields.Pop(); + + if (_nodeContextData.TryGet(node, FieldListSize, out var listSize)) + { + _listSizeProduct /= listSize; + } + + _nodeContextData.Remove(node, FieldListSize); + _parentFieldListSizes.Remove(outputField); + + return Continue; + } + + protected override ISyntaxVisitorAction Leave( + OperationDefinitionNode node, + IDocumentValidatorContext context) + { + Debug.WriteLine($"Leaving OperationDefinitionNode: {_path}"); + + _requestCosts.FieldCost = _requestCosts.FieldCostByLocation[_path]; + _requestCosts.TypeCost = _requestCosts.TypeCostByLocation[_path]; + + context.ContextData.Add(WellKnownContextData.RequestCosts, _requestCosts); + + return base.Leave(node, context); + } + + private void AddPathElement(IDocumentValidatorContext context, ISyntaxNode node) + { + var pathElement = GetPathSegment(context, node); + _pathElements.Push(pathElement); + _path += pathElement; + } + + private void AddPathElement(string pathElement) + { + _pathElements.Push(pathElement); + _path += pathElement; + } + + private void RemovePathElement() + { + var pathElement = _pathElements.Pop(); + _path = _path.Remove(_path.Length - pathElement.Length); + } + + private static double GetCostWeight(IInputField field) + { + // Use weight from @cost directive. + if (field is IHasDirectives fieldWithDirectives) + { + var costDirective = + fieldWithDirectives.Directives + .FirstOrDefault()?.AsValue(); + + if (costDirective is not null) + { + return double.Parse(costDirective.Weight); + } + } + + // https://ibm.github.io/graphql-specs/cost-spec.html#sec-weight + // "Weights for all composite input and output types default to "1.0"" + return field.Type.IsCompositeType() || field.Type.IsListType() ? 1.0 : 0.0; + } + + private static double GetCostWeight(IOutputField field) + { + // Use weight from @cost directive. + if (field is IHasDirectives fieldWithDirectives) + { + var costDirective = + fieldWithDirectives.Directives + .FirstOrDefault()?.AsValue(); + + if (costDirective is not null) + { + return double.Parse(costDirective.Weight); + } + } + + // https://ibm.github.io/graphql-specs/cost-spec.html#sec-weight + // "Weights for all composite input and output types default to "1.0"" + return field.Type.IsCompositeType() || field.Type.IsListType() ? 1.0 : 0.0; + } + + private static double GetCostWeight(IType type) + { + // Use weight from @cost directive. + if (type is IHasDirectives typeWithDirectives) + { + var costDirective = + typeWithDirectives.Directives + .FirstOrDefault()?.AsValue(); + + if (costDirective is not null) + { + return double.Parse(costDirective.Weight); + } + } + + // https://ibm.github.io/graphql-specs/cost-spec.html#sec-weight + // "Weights for all composite input and output types default to "1.0"" + return type.IsCompositeType() || type.IsListType() ? 1.0 : 0.0; + } + + private static int GetListSize( + IDocumentValidatorContext context, + Directive listSizeDirective, + FieldNode fieldNode, + IOutputField outputField) + { + var slicingArgumentNames = + listSizeDirective.GetArgumentValue?>(SlicingArguments); + + if (slicingArgumentNames is not null) + { + var requireOneSlicingArgument = + listSizeDirective.GetArgumentValue(RequireOneSlicingArgument); + + List slicingValues = []; + var argumentCount = 0; + + foreach (var slicingArgumentName in slicingArgumentNames) + { + var slicingArgument = + fieldNode.Arguments.SingleOrDefault(a => a.Name.Value == slicingArgumentName); + + if (slicingArgument is not null) + { + argumentCount++; + + switch (slicingArgument.Value) + { + case IntValueNode intValueNode: + slicingValues.Add(intValueNode.ToInt32()); + + continue; + + case VariableNode variableNode + when context.Variables[variableNode.Name.Value].DefaultValue is + IntValueNode intValueNode: + + slicingValues.Add(intValueNode.ToInt32()); + + continue; + } + } + + var defaultValue = outputField.Arguments[slicingArgumentName].DefaultValue; + + if (defaultValue is IntValueNode defaultValueNode) + { + slicingValues.Add(defaultValueNode.ToInt32()); + } + } + + // 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. + if (requireOneSlicingArgument && argumentCount != 1) + { + // FIXME: Exception type, handling, and localization. + throw new Exception( + $"Expected 1 slicing argument, {argumentCount} provided."); + } + + if (slicingValues.Count != 0) + { + return slicingValues.Max(); + } + } + + var assumedSize = listSizeDirective.GetArgumentValue(AssumedSize); + + return assumedSize ?? DefaultListSize; + } + + private static string GetNodeName(ISelectionNode selection) + { + return selection switch + { + FieldNode f => f.Alias?.Value ?? f.Name.Value, + InlineFragmentNode i => i.TypeCondition?.Name() ?? "~", + _ => throw new ArgumentOutOfRangeException(nameof(selection), selection, null) + }; + } + + private string GetPathSegment(IDocumentValidatorContext context, ISyntaxNode node) + { + return node switch + { + ArgumentNode a => $"({a.Name.Value}:)", + DirectiveNode d => $".@{d.Name.Value}", + FieldNode f => $".{f.Alias?.Value ?? f.Name.Value}{GetIndexSuffix(f)}", + FragmentDefinitionNode f => $"~~{f.Name.Value}", + FragmentSpreadNode f => $".~{f.Name.Value}", + InlineFragmentNode i => $".on~{GetFragmentTypeName(i, context)}{GetIndexSuffix(i)}", + ObjectFieldNode o => $".{o.Name.Value}", + OperationDefinitionNode o => o.Name?.Value ?? o.Operation.ToString().ToLowerInvariant(), + VariableDefinitionNode v => $"(${v.Variable.Name.Value}:)", + _ => throw new NotImplementedException() + }; + } + + private string? GetIndexSuffix(ISyntaxNode node) + { + if (_nodeContextData.TryGetValue(node, out var contextData) && + contextData.TryGetValue(Index, out var index)) + { + return $"[{index}]"; + } + + return null; + } + + private static string GetFragmentTypeName( + InlineFragmentNode fragmentNode, + IDocumentValidatorContext context) + { + return + fragmentNode.TypeCondition?.Name.Value ?? context.OutputFields.Peek().Type.TypeName(); + } +} + +file static class DictionaryExtensions +{ + public static void Increment( + this Dictionary dictionary, + string key, + double amount = 1) + { + if (dictionary.TryGetValue(key, out var count)) + { + dictionary[key] = count + amount; + } + else + { + dictionary[key] = amount; + } + } + + public static void Increment( + this Dictionary dictionary, + string key, + int amount = 1) + { + if (dictionary.TryGetValue(key, out var count)) + { + dictionary[key] = count + amount; + } + else + { + dictionary[key] = amount; + } + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostByLocation.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostByLocation.cs new file mode 100644 index 00000000000..4eaaea68ec8 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostByLocation.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.CostAnalysis; + +internal sealed class CostByLocation(string path, double cost) +{ + public string Path { get; } = path; + + public double Cost { get; } = cost; +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostCountType.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostCountType.cs new file mode 100644 index 00000000000..6b778b8a660 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostCountType.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.CostAnalysis; + +internal sealed class CostCountType(string name, int value) +{ + public string Name { get; } = name; + + public int Value { get; } = value; +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostIntrospectionTypeInterceptor.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostIntrospectionTypeInterceptor.cs new file mode 100644 index 00000000000..e8a2eac481d --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostIntrospectionTypeInterceptor.cs @@ -0,0 +1,67 @@ +using HotChocolate.Configuration; +using HotChocolate.CostAnalysis.Properties; +using HotChocolate.CostAnalysis.Types; +using HotChocolate.Language; +using HotChocolate.Resolvers; +using HotChocolate.Types.Descriptors; +using HotChocolate.Types.Descriptors.Definitions; +using static HotChocolate.CostAnalysis.WellKnownContextData; + +namespace HotChocolate.CostAnalysis; + +internal sealed class CostIntrospectionTypeInterceptor : TypeInterceptor +{ + /// Gets the field name of the __cost introspection field. + public const string Cost = "__cost"; + + private IDescriptorContext _context = default!; + private ObjectTypeDefinition? _queryTypeDefinition; + + internal override void InitializeContext( + IDescriptorContext context, + TypeInitializer typeInitializer, + TypeRegistry typeRegistry, + TypeLookup typeLookup, + TypeReferenceResolver typeReferenceResolver) + { + _context = context; + } + + internal override void OnAfterResolveRootType( + ITypeCompletionContext completionContext, + ObjectTypeDefinition definition, + OperationType operationType) + { + if (operationType is OperationType.Query) + { + _queryTypeDefinition = definition; + } + } + + public override void OnBeforeCompleteTypes() + { + _queryTypeDefinition?.Fields.Insert(0, CreateCostField(_context)); + } + + private static ObjectFieldDefinition CreateCostField(IDescriptorContext context) + { + var descriptor = ObjectFieldDescriptor.New(context, Cost); + + descriptor + .Type() + .Description(CostAnalysisResources.CostType_Description); + + var definition = descriptor.Definition; + definition.IsIntrospectionField = true; + definition.PureResolver = Resolver; + + return definition; + + static Cost Resolver(IPureResolverContext ctx) + { + var requestCosts = (CostMetrics)ctx.ContextData[RequestCosts]!; + + return new Cost(requestCosts); + } + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostMetrics.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostMetrics.cs new file mode 100644 index 00000000000..a3ec7711ef2 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostMetrics.cs @@ -0,0 +1,33 @@ +namespace HotChocolate.CostAnalysis; + +/// https://ibm.github.io/graphql-specs/cost-spec.html#sec-__cost +internal sealed class CostMetrics +{ + /// https://ibm.github.io/graphql-specs/cost-spec.html#sec-Field-Counts + public Dictionary FieldCounts { get; } = []; + + /// https://ibm.github.io/graphql-specs/cost-spec.html#sec-Type-Counts + public Dictionary TypeCounts { get; } = []; + + /// https://ibm.github.io/graphql-specs/cost-spec.html#sec-Input-Type-Counts + public Dictionary InputTypeCounts { get; } = []; + + /// https://ibm.github.io/graphql-specs/cost-spec.html#sec-Input-Field-Counts + public Dictionary InputFieldCounts { get; } = []; + + /// https://ibm.github.io/graphql-specs/cost-spec.html#sec-Argument-Counts + public Dictionary ArgumentCounts { get; } = []; + + /// https://ibm.github.io/graphql-specs/cost-spec.html#sec-Directive-Counts + public Dictionary DirectiveCounts { get; } = []; + + /// https://ibm.github.io/graphql-specs/cost-spec.html#sec-Field-Cost + public double FieldCost { get; set; } = 0; + + /// https://ibm.github.io/graphql-specs/cost-spec.html#sec-Type-Cost + public double TypeCost { get; set; } = 0; + + public Dictionary FieldCostByLocation { get; } = []; + + public Dictionary TypeCostByLocation { get; } = []; +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostArgumentDescriptorExtensions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostArgumentDescriptorExtensions.cs new file mode 100644 index 00000000000..d0fe2f706bb --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostArgumentDescriptorExtensions.cs @@ -0,0 +1,35 @@ +using HotChocolate.CostAnalysis.Directives; +using HotChocolate.Types; + +namespace HotChocolate.CostAnalysis.DescriptorExtensions; + +public static class CostArgumentDescriptorExtensions +{ + /// + /// Applies the @cost directive. 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. + /// + /// + /// The argument descriptor. + /// + /// + /// The weight argument defines what value to add to the overall cost for every + /// appearance, or possible appearance, of this argument. + /// + /// + /// Returns the argument descriptor for configuration chaining. + /// + /// + /// is null. + /// + public static IArgumentDescriptor Cost(this IArgumentDescriptor descriptor, string weight) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + return descriptor.Directive(new CostDirective(weight)); + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostEnumTypeDescriptorExtensions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostEnumTypeDescriptorExtensions.cs new file mode 100644 index 00000000000..24843866f62 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostEnumTypeDescriptorExtensions.cs @@ -0,0 +1,66 @@ +using HotChocolate.CostAnalysis.Directives; +using HotChocolate.Types; + +namespace HotChocolate.CostAnalysis.DescriptorExtensions; + +public static class CostEnumTypeDescriptorExtensions +{ + /// + /// Applies the @cost directive. 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. + /// + /// + /// The enum type descriptor. + /// + /// + /// The weight argument defines what value to add to the overall cost for every + /// appearance, or possible appearance, of this enum type. + /// + /// + /// Returns the enum type descriptor for configuration chaining. + /// + /// + /// is null. + /// + public static IEnumTypeDescriptor Cost(this IEnumTypeDescriptor descriptor, string weight) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + return descriptor.Directive(new CostDirective(weight)); + } + + /// + /// Applies the @cost directive. + /// 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. + /// + /// + /// The enum type descriptor. + /// + /// + /// The weight argument defines what value to add to the overall cost for every + /// appearance, or possible appearance, of this enum type. + /// + /// + /// Returns the enum type descriptor for configuration chaining. + /// + /// + /// is null. + /// + public static IEnumTypeDescriptor Cost( + this IEnumTypeDescriptor descriptor, + string weight) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + return descriptor.Directive(new CostDirective(weight)); + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostInputFieldDescriptorExtensions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostInputFieldDescriptorExtensions.cs new file mode 100644 index 00000000000..41dc72f439a --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostInputFieldDescriptorExtensions.cs @@ -0,0 +1,35 @@ +using HotChocolate.CostAnalysis.Directives; +using HotChocolate.Types; + +namespace HotChocolate.CostAnalysis.DescriptorExtensions; + +public static class CostInputFieldDescriptorExtensions +{ + /// + /// Applies the @cost directive. 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. + /// + /// + /// The input field descriptor. + /// + /// + /// The weight argument defines what value to add to the overall cost for every + /// appearance, or possible appearance, of this input field. + /// + /// + /// Returns the input field descriptor for configuration chaining. + /// + /// + /// is null. + /// + public static IInputFieldDescriptor Cost(this IInputFieldDescriptor descriptor, string weight) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + return descriptor.Directive(new CostDirective(weight)); + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostObjectFieldDescriptorExtensions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostObjectFieldDescriptorExtensions.cs new file mode 100644 index 00000000000..a2faaeddf31 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostObjectFieldDescriptorExtensions.cs @@ -0,0 +1,35 @@ +using HotChocolate.CostAnalysis.Directives; +using HotChocolate.Types; + +namespace HotChocolate.CostAnalysis.DescriptorExtensions; + +public static class CostObjectFieldDescriptorExtensions +{ + /// + /// Applies the @cost directive. 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. + /// + /// + /// The object field descriptor. + /// + /// + /// The weight argument defines what value to add to the overall cost for every + /// appearance, or possible appearance, of this object field. + /// + /// + /// Returns the object field descriptor for configuration chaining. + /// + /// + /// is null. + /// + public static IObjectFieldDescriptor Cost(this IObjectFieldDescriptor descriptor, string weight) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + return descriptor.Directive(new CostDirective(weight)); + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostObjectTypeDescriptorExtensions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostObjectTypeDescriptorExtensions.cs new file mode 100644 index 00000000000..a97b74753ec --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/CostObjectTypeDescriptorExtensions.cs @@ -0,0 +1,35 @@ +using HotChocolate.CostAnalysis.Directives; +using HotChocolate.Types; + +namespace HotChocolate.CostAnalysis.DescriptorExtensions; + +public static class CostObjectTypeDescriptorExtensions +{ + /// + /// Applies the @cost directive. 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. + /// + /// + /// The object type descriptor. + /// + /// + /// The weight argument defines what value to add to the overall cost for every + /// appearance, or possible appearance, of this object type. + /// + /// + /// Returns the object type descriptor for configuration chaining. + /// + /// + /// is null. + /// + public static IObjectTypeDescriptor Cost(this IObjectTypeDescriptor descriptor, string weight) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + return descriptor.Directive(new CostDirective(weight)); + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/ListSizeObjectFieldDescriptorExtensions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/ListSizeObjectFieldDescriptorExtensions.cs new file mode 100644 index 00000000000..528ce881215 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/DescriptorExtensions/ListSizeObjectFieldDescriptorExtensions.cs @@ -0,0 +1,55 @@ +using HotChocolate.CostAnalysis.Directives; +using HotChocolate.Types; + +namespace HotChocolate.CostAnalysis.DescriptorExtensions; + +public static class ListSizeObjectFieldDescriptorExtensions +{ + /// + /// Applies the @listSize directive. 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. + /// + /// + /// The object field descriptor. + /// + /// + /// The maximum length of the list returned by this field. + /// + /// + /// The arguments of this field with numeric type that are slicing arguments. Their value + /// determines the size of the returned list. + /// + /// + /// The subfield(s) that the list size applies to. + /// + /// + /// 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. + /// + /// + /// Returns the object field descriptor for configuration chaining. + /// + /// + /// is null. + /// + public static IObjectFieldDescriptor ListSize( + this IObjectFieldDescriptor descriptor, + int? assumedSize = null, + List? slicingArguments = null, + List? sizedFields = null, + bool requireOneSlicingArgument = true) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + return descriptor.Directive( + new ListSizeDirective( + assumedSize, + slicingArguments, + sizedFields, + requireOneSlicingArgument)); + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Directives/CostDirective.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Directives/CostDirective.cs new file mode 100644 index 00000000000..384ec2c8240 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Directives/CostDirective.cs @@ -0,0 +1,21 @@ +namespace HotChocolate.CostAnalysis.Directives; + +/// +/// 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. +/// +/// +/// Specification URL +/// +public sealed class CostDirective(string weight) +{ + /// + /// The weight argument defines what value to add to the overall cost for every + /// appearance, or possible appearance, of a type, field, argument, etc. + /// + /// + /// Specification URL + /// + public string Weight { get; } = weight; +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Directives/CostDirectiveType.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Directives/CostDirectiveType.cs new file mode 100644 index 00000000000..592f1761f26 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Directives/CostDirectiveType.cs @@ -0,0 +1,37 @@ +using HotChocolate.Types; + +namespace HotChocolate.CostAnalysis.Directives; + +/// +/// 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. +/// +/// +/// Specification URL +/// +public sealed class CostDirectiveType : DirectiveType +{ + protected override void Configure(IDirectiveTypeDescriptor descriptor) + { + descriptor + .Name("cost") + .Description( + "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.") + .Location( + DirectiveLocation.ArgumentDefinition | + DirectiveLocation.Enum | + DirectiveLocation.FieldDefinition | + DirectiveLocation.InputFieldDefinition | + DirectiveLocation.Object | + DirectiveLocation.Scalar); + + descriptor + .Argument(t => t.Weight) + .Description( + "The `weight` argument defines what value to add to the overall cost for every " + + "appearance, or possible appearance, of a type, field, argument, etc."); + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Directives/ListSizeDirective.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Directives/ListSizeDirective.cs new file mode 100644 index 00000000000..9e984f3219c --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Directives/ListSizeDirective.cs @@ -0,0 +1,56 @@ +namespace HotChocolate.CostAnalysis.Directives; + +/// +/// 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. +/// +/// +/// Specification URL +/// +public sealed class ListSizeDirective( + int? assumedSize = null, + List? slicingArguments = null, + List? sizedFields = null, + bool requireOneSlicingArgument = true) +{ + /// + /// The assumedSize argument can be used to statically define the maximum length of a + /// list returned by a field. + /// + /// + /// Specification URL + /// + public int? AssumedSize { get; } = assumedSize; + + /// + /// 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. + /// + /// + /// Specification URL + /// + public List? SlicingArguments { get; } = slicingArguments; + + /// + /// 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. + /// + /// + /// Specification URL + /// + public List? SizedFields { get; } = sizedFields; + + /// + /// 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 will throw an error. + /// + /// + /// Specification URL + /// + public bool RequireOneSlicingArgument { get; } = requireOneSlicingArgument; +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Directives/ListSizeDirectiveType.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Directives/ListSizeDirectiveType.cs new file mode 100644 index 00000000000..2b7c126eaf8 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Directives/ListSizeDirectiveType.cs @@ -0,0 +1,56 @@ +using HotChocolate.Types; + +namespace HotChocolate.CostAnalysis.Directives; + +/// +/// 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. +/// +/// +/// Specification URL +/// +public sealed class ListSizeDirectiveType : DirectiveType +{ + protected override void Configure(IDirectiveTypeDescriptor descriptor) + { + descriptor + .Name("listSize") + .Description( + "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.") + .Location(DirectiveLocation.FieldDefinition); + + descriptor + .Argument(t => t.AssumedSize) + .Description( + "The `assumedSize` argument can be used to statically define the maximum length " + + "of a list returned by a field."); + + descriptor + .Argument(t => t.SlicingArguments) + .Description( + "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."); + + descriptor + .Argument(t => t.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."); + + descriptor + .Argument(t => t.RequireOneSlicingArgument) + .DefaultValue(true) + .Description( + "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."); + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/ErrorHelper.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/ErrorHelper.cs new file mode 100644 index 00000000000..d96f659c9f6 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/ErrorHelper.cs @@ -0,0 +1,41 @@ +using HotChocolate.CostAnalysis.Properties; +using HotChocolate.Execution; + +namespace HotChocolate.CostAnalysis; + +internal static class ErrorHelper +{ + public static IOperationResult MaxFieldCostReached(double fieldCost, double maxFieldCost) + { + return OperationResultBuilder.CreateError( + new Error( + CostAnalysisResources.ErrorHelper_MaxFieldCostReached, + ErrorCodes.Execution.ComplexityExceeded, // FIXME: Add error code. + extensions: new Dictionary + { + { nameof(fieldCost), fieldCost }, + { nameof(maxFieldCost), maxFieldCost } + }), + contextData: new Dictionary + { + { HotChocolate.WellKnownContextData.ValidationErrors, true } // FIXME: Should this remain? + }); + } + + public static IOperationResult MaxTypeCostReached(double typeCost, double maxTypeCost) + { + return OperationResultBuilder.CreateError( + new Error( + CostAnalysisResources.ErrorHelper_MaxTypeCostReached, + ErrorCodes.Execution.ComplexityExceeded, // FIXME: Add error code. + extensions: new Dictionary + { + { nameof(typeCost), typeCost }, + { nameof(maxTypeCost), maxTypeCost } + }), + contextData: new Dictionary + { + { HotChocolate.WellKnownContextData.ValidationErrors, true } // FIXME: Should this remain? + }); + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/HotChocolate.CostAnalysis.csproj b/src/HotChocolate/CostAnalysis/src/CostAnalysis/HotChocolate.CostAnalysis.csproj new file mode 100644 index 00000000000..06aa573960b --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/HotChocolate.CostAnalysis.csproj @@ -0,0 +1,28 @@ + + + + enable + enable + + + + + + + + + + ResXFileCodeGenerator + CostAnalysisResources.Designer.cs + + + + + + True + True + CostAnalysisResources.resx + + + + diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/NodeContextData.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/NodeContextData.cs new file mode 100644 index 00000000000..65fc40db273 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/NodeContextData.cs @@ -0,0 +1,49 @@ +using HotChocolate.Language; + +namespace HotChocolate.CostAnalysis; + +internal sealed class NodeContextData : Dictionary> +{ + public void Set(ISyntaxNode node, string key, int value) + { + if (!TryGetValue(node, out var contextData)) + { + contextData = new Dictionary(); + this[node] = contextData; + } + + this[node][key] = value; + } + + public int Get(ISyntaxNode node, string key) + { + if (TryGetValue(node, out var contextData) && contextData.TryGetValue(key, out var value)) + { + return value; + } + + throw new KeyNotFoundException(); + } + + public bool TryGet(ISyntaxNode node, string key, out int value) + { + if (TryGetValue(node, out var contextData) && contextData.TryGetValue(key, out var v)) + { + value = v; + + return true; + } + + value = default; + + return false; + } + + public void Remove(ISyntaxNode node, string key) + { + if (TryGetValue(node, out var contextData)) + { + contextData.Remove(key); + } + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Properties/CostAnalysisResources.Designer.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Properties/CostAnalysisResources.Designer.cs new file mode 100644 index 00000000000..25ac79fc087 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Properties/CostAnalysisResources.Designer.cs @@ -0,0 +1,89 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace HotChocolate.CostAnalysis.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class CostAnalysisResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal CostAnalysisResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HotChocolate.CostAnalysis.Properties.CostAnalysisResources", typeof(CostAnalysisResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The results of cost analysis.. + /// + internal static string CostType_Description { + get { + return ResourceManager.GetString("CostType_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The maximum allowed field cost was exceeded.. + /// + internal static string ErrorHelper_MaxFieldCostReached { + get { + return ResourceManager.GetString("ErrorHelper_MaxFieldCostReached", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The maximum allowed type cost was exceeded.. + /// + internal static string ErrorHelper_MaxTypeCostReached { + get { + return ResourceManager.GetString("ErrorHelper_MaxTypeCostReached", resourceCulture); + } + } + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Properties/CostAnalysisResources.resx b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Properties/CostAnalysisResources.resx new file mode 100644 index 00000000000..7b2a8c268b9 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Properties/CostAnalysisResources.resx @@ -0,0 +1,30 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The results of cost analysis. + + + The maximum allowed field cost was exceeded. + + + The maximum allowed type cost was exceeded. + + diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Properties/InternalsVisibleTo.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Properties/InternalsVisibleTo.cs new file mode 100644 index 00000000000..b1ff1757925 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Properties/InternalsVisibleTo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("HotChocolate.CostAnalysis.Tests")] diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/RequestExecutorBuilderExtensions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/RequestExecutorBuilderExtensions.cs new file mode 100644 index 00000000000..64a88f45506 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/RequestExecutorBuilderExtensions.cs @@ -0,0 +1,91 @@ +using HotChocolate.CostAnalysis.Caching; +using HotChocolate.CostAnalysis.Directives; +using HotChocolate.CostAnalysis.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.CostAnalysis; + +public static class RequestExecutorBuilderExtensions +{ + public static IRequestExecutorBuilder UseDefaultPipelineWithCostAnalysis( + this IRequestExecutorBuilder builder) + { + return builder + .UseInstrumentation() + .UseExceptions() + .UseTimeout() + .UseDocumentCache() + .UseDocumentParser() + .UseDocumentValidation() + .UseOperationCache() + .UseCostAnalysis() + .UseOperationResolver() + .UseOperationVariableCoercion() + .UseOperationExecution(); + } + + public static IRequestExecutorBuilder UseCostAnalysis( + this IRequestExecutorBuilder builder) + { + return builder + .AddCostAnalysis() + .UseRequest(CostAnalysisMiddleware.Create()); + } + + /// + /// Modify cost analysis options. + /// + /// + /// The GraphQL configuration builder. + /// + /// + /// A delegate to mutate the configuration object. + /// + /// + /// Returns the for chaining in more configurations. + /// + /// + /// The or is null. + /// + public static IRequestExecutorBuilder ModifyCostAnalysisOptions( + this IRequestExecutorBuilder builder, + Action configure) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configure is null) + { + throw new ArgumentNullException(nameof(configure)); + } + + builder.ConfigureSchema( + (serviceProvider, _) => + { + var options = serviceProvider.GetRequiredService(); + + configure(options); + }); + + return builder; + } + + internal static IRequestExecutorBuilder AddCostAnalysis(this IRequestExecutorBuilder builder) + { + builder.Services + .AddSingleton() + .AddSingleton(); + + return builder + .AddDirectiveType() + .AddDirectiveType() + .AddType() + .AddType() + .AddType() + .AddType() + .TryAddTypeInterceptor(); + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/CostByLocationType.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/CostByLocationType.cs new file mode 100644 index 00000000000..11c9d1c0b0f --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/CostByLocationType.cs @@ -0,0 +1,5 @@ +using HotChocolate.Types; + +namespace HotChocolate.CostAnalysis.Types; + +internal sealed class CostByLocationType : ObjectType; diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/CostCountTypeType.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/CostCountTypeType.cs new file mode 100644 index 00000000000..63d75769997 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/CostCountTypeType.cs @@ -0,0 +1,5 @@ +using HotChocolate.Types; + +namespace HotChocolate.CostAnalysis.Types; + +internal sealed class CostCountTypeType : ObjectType; diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/CostMetricsType.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/CostMetricsType.cs new file mode 100644 index 00000000000..f90156ce013 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/CostMetricsType.cs @@ -0,0 +1,122 @@ +using System.Text.RegularExpressions; +using HotChocolate.Types; +using static HotChocolate.CostAnalysis.WellKnownArgumentNames; + +namespace HotChocolate.CostAnalysis.Types; + +internal sealed class CostMetricsType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.BindFieldsExplicitly(); + + descriptor + .Field(c => c.FieldCounts) + .Argument(RegexName, d => d.Type()) + .Resolve(context => ToCostCountTypes( + context.Parent().FieldCounts, + context.ArgumentValue(RegexName))); + + descriptor + .Field(c => c.TypeCounts) + .Argument(RegexName, d => d.Type()) + .Resolve(context => ToCostCountTypes( + context.Parent().TypeCounts, + context.ArgumentValue(RegexName))); + + descriptor + .Field(c => c.InputTypeCounts) + .Argument(RegexName, d => d.Type()) + .Resolve(context => ToCostCountTypes( + context.Parent().InputTypeCounts, + context.ArgumentValue(RegexName))); + + descriptor + .Field(c => c.InputFieldCounts) + .Argument(RegexName, d => d.Type()) + .Resolve(context => ToCostCountTypes( + context.Parent().InputFieldCounts, + context.ArgumentValue(RegexName))); + + descriptor + .Field(c => c.ArgumentCounts) + .Argument(RegexName, d => d.Type()) + .Resolve(context => ToCostCountTypes( + context.Parent().ArgumentCounts, + context.ArgumentValue(RegexName))); + + descriptor + .Field(c => c.DirectiveCounts) + .Argument(RegexName, d => d.Type()) + .Resolve(context => ToCostCountTypes( + context.Parent().DirectiveCounts, + context.ArgumentValue(RegexName))); + + descriptor + .Field(c => c.FieldCost); + + descriptor + .Field(c => c.TypeCost); + + descriptor + .Field(c => c.FieldCostByLocation) + .Argument(RegexPath, d => d.Type()) + .Resolve(context => ToCostsByLocation( + context.Parent().FieldCostByLocation, + context.ArgumentValue(RegexPath))); + + descriptor + .Field(c => c.TypeCostByLocation) + .Argument(RegexPath, d => d.Type()) + .Resolve(context => ToCostsByLocation( + context.Parent().TypeCostByLocation, + context.ArgumentValue(RegexPath))); + } + + private static IEnumerable ToCostCountTypes( + Dictionary dictionary, + string? regexName) + { + var regex = regexName is null ? null : CreateRegex(regexName); + + foreach (var (key, value) in dictionary) + { + if (regex?.IsMatch(key) == false) + { + continue; + } + + yield return new CostCountType(key, value); + } + } + + private static IEnumerable ToCostsByLocation( + Dictionary dictionary, + string? regexPath) + { + var regex = regexPath is null ? null : CreateRegex(regexPath); + + foreach (var (key, value) in dictionary) + { + if (regex?.IsMatch(key) == false) + { + continue; + } + + yield return new CostByLocation(key, value); + } + } + + private static Regex CreateRegex(string regexString) + { + // This regular expression always applies to the full name/path, as if the start string and + // end string qualifiers are always specified. + // https://ibm.github.io/graphql-specs/cost-spec.html#sec-regexName-Arguments + // https://ibm.github.io/graphql-specs/cost-spec.html#sec-regexPath-Arguments + regexString = "^" + regexString.TrimStart('^').TrimEnd('$') + "$"; + + var regex = new Regex(regexString, RegexOptions.Compiled | RegexOptions.NonBacktracking); + + return regex; + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/CostType.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/CostType.cs new file mode 100644 index 00000000000..45ad69a495a --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Types/CostType.cs @@ -0,0 +1,6 @@ +using HotChocolate.Types; + +namespace HotChocolate.CostAnalysis.Types; + +/// https://ibm.github.io/graphql-specs/cost-spec.html#sec-__cost +internal sealed class CostType : ObjectType; diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/WellKnownArgumentNames.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/WellKnownArgumentNames.cs new file mode 100644 index 00000000000..140bde8432c --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/WellKnownArgumentNames.cs @@ -0,0 +1,11 @@ +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"; +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/WellKnownContextData.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/WellKnownContextData.cs new file mode 100644 index 00000000000..964a24eb89d --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/WellKnownContextData.cs @@ -0,0 +1,6 @@ +namespace HotChocolate.CostAnalysis; + +internal static class WellKnownContextData +{ + public const string RequestCosts = "HotChocolate.CostAnalysis.RequestCosts"; +} diff --git a/src/HotChocolate/CostAnalysis/src/Directory.Build.props b/src/HotChocolate/CostAnalysis/src/Directory.Build.props new file mode 100644 index 00000000000..6b1707b5ed2 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/src/Directory.Build.props @@ -0,0 +1,4 @@ + + + + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/AttributeTests.cs b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/AttributeTests.cs new file mode 100644 index 00000000000..3f64ac1d241 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/AttributeTests.cs @@ -0,0 +1,150 @@ +using HotChocolate.CostAnalysis.Attributes; +using HotChocolate.CostAnalysis.Directives; +using HotChocolate.Types; + +namespace HotChocolate.CostAnalysis; + +public sealed class AttributeTests +{ + [Fact] + public void Cost_ArgumentAttribute_AppliesDirective() + { + // arrange & act + var query = CreateSchema().GetType(OperationTypeNames.Query); + + var costDirective = query.Fields["examples"] + .Arguments["_"] + .Directives + .Single(d => d.Type.Name == "cost") + .AsValue(); + + // assert + Assert.Equal("8.0", costDirective.Weight); + } + + [Fact] + public void Cost_EnumTypeAttribute_AppliesDirective() + { + // arrange & act + var exampleEnum = CreateSchema().GetType(nameof(ExampleEnum)); + + var costDirective = exampleEnum + .Directives + .Single(d => d.Type.Name == "cost") + .AsValue(); + + // assert + Assert.Equal("0.0", costDirective.Weight); + } + + [Fact] + public void Cost_InputFieldAttribute_AppliesDirective() + { + // arrange & act + var exampleInput = CreateSchema().GetType(nameof(ExampleInput)); + + var costDirective = exampleInput.Fields["field"] + .Directives + .Single(d => d.Type.Name == "cost") + .AsValue(); + + // assert + Assert.Equal("-3.0", costDirective.Weight); + } + + [Fact] + public void Cost_ObjectFieldAttribute_AppliesDirective() + { + // arrange & act + var query = CreateSchema().GetType(OperationTypeNames.Query); + + var costDirective = query.Fields["examples"] + .Directives + .Single(d => d.Type.Name == "cost") + .AsValue(); + + // assert + Assert.Equal("5.0", costDirective.Weight); + } + + [Fact] + public void Cost_ObjectTypeAttribute_AppliesDirective() + { + // arrange & act + var example = CreateSchema().GetType(nameof(Example)); + + var costDirective = example.Directives + .Single(d => d.Type.Name == "cost") + .AsValue(); + + // assert + Assert.Equal("10.0", costDirective.Weight); + } + + [Fact] + public void ListSize_ObjectFieldAttribute_AppliesDirective() + { + // arrange & act + var query = CreateSchema().GetType(OperationTypeNames.Query); + + var costDirective = query.Fields["examples"] + .Directives + .Single(d => d.Type.Name == "listSize") + .AsValue(); + + // assert + Assert.Equal(10, costDirective.AssumedSize); + Assert.Equal(["first", "last"], costDirective.SlicingArguments); + Assert.Equal(["edges", "nodes"], costDirective.SizedFields); + Assert.False(costDirective.RequireOneSlicingArgument); + } + + private static ISchema CreateSchema() + { + return SchemaBuilder.New() + .AddQueryType(new ObjectType(d => d.Name(OperationTypeNames.Query))) + .AddType(typeof(Queries)) + .AddDirectiveType() + .AddDirectiveType() + .AddEnumType() + .Use(next => next) + .Create(); + } + + [QueryType] + private static class Queries + { + [ListSize( + AssumedSize = 10, + SlicingArguments = ["first", "last"], + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false)] + [Cost("5.0")] + // ReSharper disable once UnusedMember.Local + public static List GetExamples([Cost("8.0")] ExampleInput _) + { + return [new Example(ExampleEnum.Member)]; + } + } + + [ObjectType] + [Cost("10.0")] + private sealed class Example(ExampleEnum field) + { + // ReSharper disable once UnusedMember.Local + public ExampleEnum Field { get; set; } = field; + } + + [InputObjectType] + // ReSharper disable once ClassNeverInstantiated.Local + private sealed class ExampleInput(string field) + { + [Cost("-3.0")] + // ReSharper disable once UnusedMember.Local + public string Field { get; set; } = field; + } + + [EnumType] + [Cost("0.0")] + private enum ExampleEnum { Member } +} diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/CachingTests.cs b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/CachingTests.cs new file mode 100644 index 00000000000..b2f108ca1df --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/CachingTests.cs @@ -0,0 +1,85 @@ +using HotChocolate.CostAnalysis.Caching; +using HotChocolate.CostAnalysis.Doubles; +using HotChocolate.Execution; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace HotChocolate.CostAnalysis; + +public sealed class CachingTests +{ + [Fact] + public async Task Execute_SameQueryTwice_UsesCostMetricsCache() + { + // arrange + const string schema = + """ + type Query { + examples(limit: Int! @cost(weight: "2.0")): [Example!]! + @cost(weight: "3.0") @listSize(slicingArguments: ["limit"]) + } + + type Example @cost(weight: "4.0") { + exampleField1: Boolean! + exampleField2: Int! + } + """; + + const string query = + """ + query { + examples(limit: 10) { + exampleField1 + exampleField2 + } + + __cost { + requestCosts { + fieldCounts { name, value } + typeCounts { name, value } + inputTypeCounts { name, value } + inputFieldCounts { name, value } + argumentCounts { name, value } + directiveCounts { name, value } + + fieldCost + typeCost + + fieldCostByLocation { path, cost } + typeCostByLocation { path, cost } + } + } + } + """; + + var requestExecutor = await CreateRequestExecutorBuilder() + .AddDocumentFromString(schema) + .BuildRequestExecutorAsync(); + + var cache = (FakeCostMetricsCache)requestExecutor.Schema.Services + .GetRequiredService(); + + // act + await requestExecutor.ExecuteAsync(query); + await requestExecutor.ExecuteAsync(query); + + // assert + Assert.Equal(1, cache.Misses); + Assert.Equal(1, cache.Additions); + Assert.Equal(1, cache.Hits); + } + + private static IRequestExecutorBuilder CreateRequestExecutorBuilder() + { + var requestExecutorBuilder = new ServiceCollection() + .AddGraphQLServer() + .UseDefaultPipelineWithCostAnalysis() + .UseField(next => next); + + requestExecutorBuilder.Services.Replace( + new ServiceDescriptor(typeof(ICostMetricsCache), new FakeCostMetricsCache())); + + return requestExecutorBuilder; + } +} diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/DescriptorExtensionTests.cs b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/DescriptorExtensionTests.cs new file mode 100644 index 00000000000..8405f55b0be --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/DescriptorExtensionTests.cs @@ -0,0 +1,165 @@ +using HotChocolate.CostAnalysis.DescriptorExtensions; +using HotChocolate.CostAnalysis.Directives; +using HotChocolate.Types; + +namespace HotChocolate.CostAnalysis; + +public sealed class DescriptorExtensionTests +{ + [Fact] + public void Cost_ArgumentDescriptor_AppliesDirective() + { + // arrange & act + var schema = SchemaBuilder.New() + .AddQueryType(d => d + .Name(OperationTypeNames.Query) + .Field("field") + .Argument("a", a => a.Type().Cost("5.0")) + .Type()) + .AddDirectiveType() + .Use(next => next) + .Create(); + + var query = schema.GetType(OperationTypeNames.Query); + + var costDirective = query.Fields["field"] + .Arguments["a"] + .Directives + .Single(d => d.Type.Name == "cost") + .AsValue(); + + // assert + Assert.Equal("5.0", costDirective.Weight); + } + + [Fact] + public void Cost_EnumTypeDescriptor_AppliesDirective() + { + // arrange & act + var schema = SchemaBuilder.New() + .AddQueryType(d => d + .Name(OperationTypeNames.Query) + .Field("field") + .Type()) + .AddDirectiveType() + .AddEnumType(d => d.Name("Example").Cost("5.0").Value("EnumMember1")) + .AddEnumType(d => d.Cost("10.0")) + .Use(next => next) + .Create(); + + var enumType1 = schema.GetType("Example"); + var directive1 = enumType1.Directives.Single(d => d.Type.Name == "cost"); + var costDirective1 = directive1.AsValue(); + + var enumType2 = schema.GetType(nameof(ExampleEnum)); + var directive2 = enumType2.Directives.Single(d => d.Type.Name == "cost"); + var costDirective2 = directive2.AsValue(); + + // assert + Assert.Equal("5.0", costDirective1.Weight); + Assert.Equal("10.0", costDirective2.Weight); + } + + [Fact] + public void Cost_InputFieldDescriptor_AppliesDirective() + { + // arrange & act + var schema = SchemaBuilder.New() + .AddQueryType(d => d + .Name(OperationTypeNames.Query) + .Field("field") + .Type()) + .AddDirectiveType() + .AddInputObjectType( + d => d + .Name("input") + .Field("field") + .Type() + .Cost("5.0")) + .Use(next => next) + .Create(); + + var input = schema.GetType("input"); + var directive = input.Fields["field"].Directives.Single(d => d.Type.Name == "cost"); + var costDirective = directive.AsValue(); + + // assert + Assert.Equal("5.0", costDirective.Weight); + } + + [Fact] + public void Cost_ObjectFieldDescriptor_AppliesDirective() + { + // arrange & act + var schema = SchemaBuilder.New() + .AddQueryType(d => d + .Name(OperationTypeNames.Query) + .Field("field") + .Type() + .Cost("5.0")) + .AddDirectiveType() + .Use(next => next) + .Create(); + + var query = schema.GetType(OperationTypeNames.Query); + var directive = query.Fields["field"].Directives.Single(d => d.Type.Name == "cost"); + var costDirective = directive.AsValue(); + + // assert + Assert.Equal("5.0", costDirective.Weight); + } + + [Fact] + public void Cost_ObjectTypeDescriptor_AppliesDirective() + { + // arrange & act + var schema = SchemaBuilder.New() + .AddQueryType(d => d + .Name(OperationTypeNames.Query) + .Cost("5.0") + .Field("field") + .Type()) + .AddDirectiveType() + .Use(next => next) + .Create(); + + var query = schema.GetType(OperationTypeNames.Query); + var directive = query.Directives.Single(d => d.Type.Name == "cost"); + var costDirective = directive.AsValue(); + + // assert + Assert.Equal("5.0", costDirective.Weight); + } + + [Fact] + public void ListSize_ObjectFieldDescriptor_AppliesDirective() + { + // arrange & act + var schema = SchemaBuilder.New() + .AddQueryType(d => d + .Name(OperationTypeNames.Query) + .Field("field") + .Type>() + .ListSize( + assumedSize: 10, + slicingArguments: ["first", "last"], + sizedFields: ["edges", "nodes"], + requireOneSlicingArgument: false)) + .AddDirectiveType() + .Use(next => next) + .Create(); + + var query = schema.GetType(OperationTypeNames.Query); + var directive = query.Fields["field"].Directives.Single(d => d.Type.Name == "listSize"); + var listSizeDirective = directive.AsValue(); + + // assert + Assert.Equal(10, listSizeDirective.AssumedSize); + Assert.Equal(["first", "last"], listSizeDirective.SlicingArguments); + Assert.Equal(["edges", "nodes"], listSizeDirective.SizedFields); + Assert.False(listSizeDirective.RequireOneSlicingArgument); + } + + // ReSharper disable once UnusedMember.Local + private enum ExampleEnum { Member } +} diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/Doubles/FakeCostMetricsCache.cs b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/Doubles/FakeCostMetricsCache.cs new file mode 100644 index 00000000000..c40e13e5401 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/Doubles/FakeCostMetricsCache.cs @@ -0,0 +1,48 @@ +using System.Diagnostics.CodeAnalysis; +using HotChocolate.CostAnalysis.Caching; +using HotChocolate.Utilities; + +namespace HotChocolate.CostAnalysis.Doubles; + +internal class FakeCostMetricsCache(int capacity = 100) : ICostMetricsCache +{ + private readonly Cache _cache = new(capacity); + + public int Capacity => _cache.Capacity; + + public int Count => _cache.Usage; + + public int Hits { get; private set; } + + public int Misses { get; private set; } + + public int Additions { get; private set; } + + public bool TryGetCostMetrics( + string operationId, + [NotNullWhen(true)] out CostMetrics? costMetrics) + { + var result = _cache.TryGet(operationId, out costMetrics); + + if (result) + { + Hits++; + } + else + { + Misses++; + } + + return result; + } + + public void TryAddCostMetrics( + string operationId, + CostMetrics costMetrics) + { + _cache.GetOrCreate(operationId, () => costMetrics); + Additions++; + } + + public void Clear() => _cache.Clear(); +} diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/HotChocolate.CostAnalysis.Tests.csproj b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/HotChocolate.CostAnalysis.Tests.csproj new file mode 100644 index 00000000000..f41a49e1666 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/HotChocolate.CostAnalysis.Tests.csproj @@ -0,0 +1,14 @@ + + + + HotChocolate.CostAnalysis.Tests + HotChocolate.CostAnalysis + + + + + + + + + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/IntrospectionTests.cs b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/IntrospectionTests.cs new file mode 100644 index 00000000000..5f221d663ec --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/IntrospectionTests.cs @@ -0,0 +1,337 @@ +using CookieCrumble; +using HotChocolate.Execution; +using HotChocolate.Execution.Configuration; +using HotChocolate.Language; +using HotChocolate.Types; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.CostAnalysis; + +public sealed class IntrospectionTests +{ + [Theory] + [MemberData(nameof(CostQueryData))] + public async Task Execute_CostQuery_ReturnsExpectedResult(int index, string costQuery) + { + // arrange + const string schema = + """ + # Weights used: + # 1.0 = (default for composite and list types) + # 2.0 = ArgumentDefinition + # 3.0 = FieldDefinition + # 4.0 = Object + # 5.0 = InputFieldDefinition + # 6.0 = Scalar + # 7.0 = Enum + + type Query { + examples(limit: Int! @cost(weight: "2.0")): [Example1!]! + @cost(weight: "3.0") @listSize(slicingArguments: ["limit"]) + } + + type Example1 @cost(weight: "4.0") { + example1Field1(arg1: String!, arg2: String!): Boolean! + example1Field2(arg1Input1: Input1!): Example2! + example1Field3: Example3! + } + + type Example2 { + example2Field1(arg1: String!, arg2: String!): Boolean! + example2Field2(arg1Input2: [Input2!]!): Int! + } + + type Example3 { + example3Field1: Scalar1! + example3Field2: Enum1! + } + + input Input1 { input1Field1: String! @cost(weight: "5.0"), input1Field2: Input2! } + input Input2 { input2Field1: String!, input2Field2: String! } + + scalar Scalar1 @cost(weight: "6.0") + + enum Enum1 @cost(weight: "7.0") { ENUM_VALUE1 } + + directive @example(dirArg1: Int!, dirArg2: Input1!) on + | FIELD + | FRAGMENT_DEFINITION + | FRAGMENT_SPREAD + | INLINE_FRAGMENT + | QUERY + | VARIABLE_DEFINITION + """; + + var query = + $$""" + query($limit: Int! = 10 {{ExampleDirective(1)}}) {{ExampleDirective(2)}} { + examples(limit: $limit) {{ExampleDirective(3)}} { + ... {{ExampleDirective(4)}} { + example1Field1(arg1: "", arg2: "") {{ExampleDirective(5)}} + } + + # Repeated to test indexed paths (f.e. "query.examples.on~Example1[1]"). + ... { example1Field1(arg1: "", arg2: "") } + + example1Field2( + arg1Input1: { + input1Field1: "" + input1Field2: { input2Field1: "", input2Field2: "" } + } + ) { + ...fragment1 {{ExampleDirective(6)}} + } + + example1Field3 { example3Field1, aliasField2: example3Field2 } + + # Repeated to test indexed paths (f.e. "query.examples.example1Field3[1]") + example1Field3 { aliasField1: example3Field1, example3Field2 } + } + + {{costQuery}} + } + + fragment fragment1 on Example2 {{ExampleDirective(7)}} { + example2Field1(arg1: "", arg2: "") {{ExampleDirective(8)}} + example2Field2(arg1Input2: [ + { input2Field1: "", input2Field2: "" } + { input2Field1: "", input2Field2: "" } + ]) + } + """; + + var snapshot = new Snapshot(postFix: index.ToString()); + + snapshot + .Add(schema, "Schema") + .Add(query, "Query"); + + var requestExecutor = await CreateRequestExecutorBuilder() + .AddDocumentFromString(schema) + .BuildRequestExecutorAsync(); + + // act + var result = await requestExecutor.ExecuteAsync(query); + + snapshot.AddResult(result.ExpectQueryResult(), "Result"); + + // assert + await snapshot.MatchMarkdownAsync(); + } + + private static string ExampleDirective(int i) + { + return + $$"""@example(dirArg1: {{i}}, dirArg2: { """ + + """input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } })"""; + } + + public static TheoryData CostQueryData() + { + return new TheoryData + { + // All counts. + { + 0, + """ + __cost { + requestCosts { + fieldCounts { name, value } + typeCounts { name, value } + inputTypeCounts { name, value } + inputFieldCounts { name, value } + argumentCounts { name, value } + directiveCounts { name, value } + + fieldCost + typeCost + + fieldCostByLocation { path, cost } + typeCostByLocation { path, cost } + } + } + """ + }, + // Filtered field counts. + { + 1, + """ + __cost { + requestCosts { + example1Field1Counts: fieldCounts(regexName: "Example1\\.example1Field1") + { name, value } + fieldCountsInExample1Type: fieldCounts(regexName: "Example1\\..+") + { name, value } + } + } + """ + }, + // Filtered type counts. + { + 2, + """ + __cost { + requestCosts { + example1Counts: typeCounts(regexName: "Example1") + { name, value } + endsWithTCounts: typeCounts(regexName: ".*t") + { name, value } + } + } + """ + }, + // Filtered input type counts. + { + 3, + """ + __cost { + requestCosts { + input2Counts: inputTypeCounts(regexName: "Input2") + { name, value } + } + } + """ + }, + // Filtered input field counts. + { + 4, + """ + __cost { + requestCosts { + input1Field1Counts: inputFieldCounts(regexName: "Input1\\.input1Field1") + { name, value } + fieldCountsInInput2Type: inputFieldCounts(regexName: "Input2\\..+") + { name, value } + } + } + """ + }, + // Filtered argument counts. + { + 5, + """ + __cost { + requestCosts { + argsNamedArg1Counts: argumentCounts(regexName: ".+\\(arg1:\\)") + { name, value } + argsOnExampleDirectiveCounts: argumentCounts(regexName: "@example\\(.+") + { name, value } + } + } + """ + }, + // Filtered directive counts. + { + 6, + """ + __cost { + requestCosts { + exampleDirectiveCounts: directiveCounts(regexName: "@example") + { name, value } + } + } + """ + }, + // Filtered field costs by location. + { + 7, + """ + __cost { + requestCosts { + exampleDirectiveOnQuery: + fieldCostByLocation(regexPath: "query\\.@example.*") + { path, cost } + fragment1: + fieldCostByLocation(regexPath: ".*~fragment1.*") + { path, cost } + example1Field3: + fieldCostByLocation(regexPath: ".*\\.example1Field3\\[\\d+\\]") + { path, cost } + } + } + """ + }, + // Filtered type costs by location. + { + 8, + """ + __cost { + requestCosts { + examplesField: + typeCostByLocation(regexPath: "query\\.examples") + { path, cost } + example1Field3: + typeCostByLocation(regexPath: ".*\\.example1Field3\\[\\d+\\]") + { path, cost } + } + } + """ + } + }; + } + + private static IRequestExecutorBuilder CreateRequestExecutorBuilder() + { + return new ServiceCollection() + .AddGraphQLServer() + .UseDefaultPipelineWithCostAnalysis() + .AddResolver( + "Query", + "examples", + _ => new List + { + new(true, new Example2(true, 1, ""), new Example3("", Enum1.EnumValue1)) + }) + .AddResolver( + "Example1", + "example1Field1", + context => context.Parent().Field1) + .AddResolver( + "Example1", + "example1Field2", + context => context.Parent().Field2) + .AddResolver( + "Example1", + "example1Field3", + context => context.Parent().Field3) + .AddResolver( + "Example2", + "example2Field1", + context => context.Parent().Field1) + .AddResolver( + "Example2", + "example2Field2", + context => context.Parent().Field2) + .AddResolver( + "Example2", + "example2Field3", + context => context.Parent().Field3) + .AddResolver( + "Example3", + "example3Field1", + context => context.Parent().Field1) + .AddResolver( + "Example3", + "example3Field2", + context => context.Parent().Field2) + .AddType(); + } + + private sealed record Example1(bool Field1, Example2 Field2, Example3 Field3); + private sealed record Example2(bool Field1, int Field2, string Field3); + private sealed record Example3(string Field1, Enum1 Field2); + + private enum Enum1 + { + EnumValue1 + } +} + +public sealed class Scalar1Type() : ScalarType("Scalar1") +{ + public override IValueNode ParseResult(object? resultValue) => ParseValue(resultValue); + + protected override string ParseLiteral(StringValueNode valueSyntax) => valueSyntax.Value; + + protected override StringValueNode ParseValue(string runtimeValue) => new(runtimeValue); +} diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/SpecificationExampleTests.cs b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/SpecificationExampleTests.cs new file mode 100644 index 00000000000..f91380cb644 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/SpecificationExampleTests.cs @@ -0,0 +1,187 @@ +using CookieCrumble; +using HotChocolate.Execution; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using ObjectResult = HotChocolate.Execution.Processing.ObjectResult; + +namespace HotChocolate.CostAnalysis; + +public sealed class SpecificationExampleTests +{ + [Theory] + [MemberData(nameof(SpecificationExampleData))] + public async Task Execute_SpecificationExample_ReturnsExpectedResult( + int index, + string schema, + string query, + double expectedFieldCost) + { + // arrange + query = + $$""" + query Example { + {{query}} + + __cost { + requestCosts { + fieldCostByLocation { path, cost } + fieldCost + } + } + } + """; + + var snapshot = new Snapshot(postFix: index.ToString()); + + snapshot + .Add(schema, "Schema") + .Add(query, "Query"); + + var requestExecutor = await CreateRequestExecutorBuilder() + .AddDocumentFromString(schema) + .BuildRequestExecutorAsync(); + + // act + var result = await requestExecutor.ExecuteAsync(query); + var queryResult = result.ExpectQueryResult(); + + snapshot.AddResult(queryResult, "Result"); + + // assert + var data = Assert.IsType(queryResult.Data); + var cost = Assert.IsType(data.GetValueOrDefault("__cost")); + var requestCosts = Assert.IsType(cost.GetValueOrDefault("requestCosts")); + var fieldCost = Assert.IsType(requestCosts.GetValueOrDefault("fieldCost")); + + Assert.Equal(expectedFieldCost, fieldCost); + await snapshot.MatchMarkdownAsync(); + } + + public static TheoryData SpecificationExampleData() + { + return new TheoryData + { + // https://ibm.github.io/graphql-specs/cost-spec.html#sec-Example + { + 0, + """ + type User { + name: String + age: Int @cost(weight: "2.0") + } + + type Query { + users(max: Int): [User] @listSize(slicingArguments: ["max"]) + } + """, + """ + users(max: 5) { + age + } + """, + 11 + }, + // https://ibm.github.io/graphql-specs/cost-spec.html#sec-Field-Cost.Example-Argument-Weights + // Without argument. + { + 1, + """ + type Query { + topProducts(filter: Filter @cost(weight: "15.0")): [String] + @cost(weight: "5.0") @listSize(assumedSize: 10) + } + + input Filter { field: Boolean! } + """, + "topProducts", + 5 + }, + // https://ibm.github.io/graphql-specs/cost-spec.html#sec-Field-Cost.Example-Argument-Weights + // With argument. + { + 2, + """ + type Query { + topProducts(filter: Filter @cost(weight: "15.0")): [String] + @cost(weight: "5.0") @listSize(assumedSize: 10) + } + + input Filter { field: Boolean! } + """, + "topProducts(filter: { field: true })", + 20 + }, + // https://ibm.github.io/graphql-specs/cost-spec.html#sec-Field-Cost.Example-Negative-Weights + // Without argument. + { + 3, + """ + type Query { + mostPopularProduct(approx: Approximate @cost(weight: "-3.0")): Product + @cost(weight: "5.0") + } + + input Approximate { field: Boolean! } + type Product { field: Boolean! } + """, + "mostPopularProduct { field }", + 5 + }, + // https://ibm.github.io/graphql-specs/cost-spec.html#sec-Field-Cost.Example-Negative-Weights + // With argument. + { + 4, + """ + type Query { + mostPopularProduct(approx: Approximate @cost(weight: "-3.0")): Product + @cost(weight: "5.0") + } + + input Approximate { field: Boolean! } + type Product { field: Boolean! } + """, + "mostPopularProduct(approx: { field: true }) { field }", + 2 + }, + // https://ibm.github.io/graphql-specs/cost-spec.html#sec-Field-Cost.Example-Input-Field-Weights + { + 5, + """ + input Filter { + approx: Approximate @cost(weight: "-12.0") + } + + type Query { + topProducts(filter: Filter @cost(weight: "15.0")): [String] + @cost(weight: "5.0") @listSize(assumedSize: 10) + } + + input Approximate { field: Boolean! } + """, + "topProducts(filter: { approx: { field: true } })", + 8 + }, + // https://ibm.github.io/graphql-specs/cost-spec.html#sec-Field-Cost.Example-Directive-Arguments + { + 6, + """ + directive @approx(tolerance: Float! @cost(weight: "-1.0")) on FIELD + + type Query { + example: [String] @cost(weight: "5.0") + } + """, + "example @approx(tolerance: 0)", + 5 // FIXME: Should be 4. See https://github.com/ChilliCream/graphql-platform/pull/7130. + } + }; + } + + private static IRequestExecutorBuilder CreateRequestExecutorBuilder() + { + return new ServiceCollection() + .AddGraphQLServer() + .UseDefaultPipelineWithCostAnalysis() + .UseField(next => next); + } +} diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/StaticQueryAnalysisTests.cs b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/StaticQueryAnalysisTests.cs new file mode 100644 index 00000000000..48395b5e977 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/StaticQueryAnalysisTests.cs @@ -0,0 +1,376 @@ +using CookieCrumble; +using HotChocolate.Execution; +using HotChocolate.Execution.Configuration; +using HotChocolate.Types.Pagination; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.CostAnalysis; + +public sealed class StaticQueryAnalysisTests +{ + [Theory] + [MemberData(nameof(ListQueryData))] + public async Task Execute_ListQuery_ReturnsExpectedResult( + int index, + string schema, + string query) + { + // arrange + schema = + $$""" + type Query { + {{schema}} + } + + type Example { + field1: Boolean! + field2: Int! + } + """; + + query = + $$""" + query { + {{query}} + + __cost { + requestCosts { + fieldCounts { name, value } + typeCounts { name, value } + inputTypeCounts { name, value } + inputFieldCounts { name, value } + argumentCounts { name, value } + directiveCounts { name, value } + } + } + } + """; + + var snapshot = new Snapshot(postFix: index.ToString()); + + snapshot + .Add(schema, "Schema") + .Add(query, "Query"); + + var requestExecutor = await CreateRequestExecutorBuilder() + .AddDocumentFromString(schema) + .BuildRequestExecutorAsync(); + + // act + var result = await requestExecutor.ExecuteAsync(query); + + snapshot.AddResult(result.ExpectQueryResult(), "Result"); + + // assert + await snapshot.MatchMarkdownAsync(); + } + + [Theory] + [MemberData(nameof(ConnectionQueryData))] + public async Task Execute_ConnectionQuery_ReturnsExpectedResult( + int index, + string schema, + string query) + { + // arrange + schema = + $$""" + type Query { + {{schema}} + } + + type Example1 { + field1: Boolean! + field2(first: Int, after: String, last: Int, before: String): Examples2Connection + @listSize(slicingArguments: ["first", "last"], sizedFields: ["edges"]) + } + + type Example2 { + field1: Boolean! + field2: Int! + } + + type Examples1Connection { + pageInfo: PageInfo! + edges: [Examples1Edge!] + nodes: [Example1!] + } + + type Examples2Connection { + pageInfo: PageInfo! + edges: [Examples2Edge!] + nodes: [Example2!] + } + + type Examples1Edge { + cursor: String! + node: Example1! + } + + type Examples2Edge { + cursor: String! + node: Example2! + } + + type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String + } + """; + + query = + $$""" + query { + {{query}} + + __cost { + requestCosts { + fieldCounts { name, value } + typeCounts { name, value } + inputTypeCounts { name, value } + inputFieldCounts { name, value } + argumentCounts { name, value } + directiveCounts { name, value } + } + } + } + """; + + var snapshot = new Snapshot(postFix: index.ToString()); + + snapshot + .Add(schema, "Schema") + .Add(query, "Query"); + + var requestExecutor = await CreateRequestExecutorBuilder() + .AddDocumentFromString(schema) + .BuildRequestExecutorAsync(); + + // act + var result = await requestExecutor.ExecuteAsync(query); + + snapshot.AddResult(result.ExpectQueryResult(), "Result"); + + // assert + await snapshot.MatchMarkdownAsync(); + } + + public static TheoryData ListQueryData() + { + return new TheoryData + { + // No @listSize directive. + { + 0, + "examples(limit: Int): [Example!]!", + "examples(limit: 10) { field1, field2 }" + }, + // @listSize directive without arguments. + { + 1, + "examples(limit: Int): [Example!]! @listSize", + "examples(limit: 10) { field1, field2 }" + }, + // @listSize directive with slicing arguments (integer limit in query). + { + 2, + """examples(limit: Int): [Example!]! @listSize(slicingArguments: ["limit"])""", + "examples(limit: 10) { field1, field2 }" + }, + // @listSize directive with slicing arguments (null limit in query). + { + 3, + """examples(limit: Int): [Example!]! @listSize(slicingArguments: ["limit"])""", + "examples(limit: null) { field1, field2 }" + }, + // @listSize directive with slicing arguments (no limit in query). + // Error: "Expected 1 slicing argument, 0 provided.". + { + 4, + """examples(limit: Int): [Example!]! @listSize(slicingArguments: ["limit"])""", + "examples { field1, field2 }" + }, + // @listSize directive with slicing arguments (null limit in query, with assumedSize). + { + 5, + """ + examples(limit: Int): [Example!]! + @listSize(slicingArguments: ["limit"], assumedSize: 10) + """, + "examples(limit: null) { field1, field2 }" + }, + // @listSize directive with slicing arguments (no limit in query, with assumedSize). + // Error: "Expected 1 slicing argument, 0 provided.". + { + 6, + """ + examples(limit: Int): [Example!]! + @listSize(slicingArguments: ["limit"], assumedSize: 10) + """, + "examples { field1, field2 }" + }, + // @listSize directive with slicing arguments. + // (no limit in query, with requireOneSlicingArgument: false). + { + 7, + """ + examples(limit: Int): [Example!]! + @listSize(slicingArguments: ["limit"], requireOneSlicingArgument: false) + """, + "examples { field1, field2 }" + }, + // @listSize directive with slicing arguments. + // (no limit in query, with assumedSize and requireOneSlicingArgument: false). + { + 8, + """ + examples(limit: Int): [Example!]! + @listSize( + slicingArguments: ["limit"], + assumedSize: 10, + requireOneSlicingArgument: false + ) + """, + "examples { field1, field2 }" + } + }; + } + + public static TheoryData ConnectionQueryData() + { + return new TheoryData + { + // Nested connections. @listSize directives with slicing arguments and sizedFields. + { + 0, + """ + examples1(first: Int, after: String, last: Int, before: String): Examples1Connection + @listSize(slicingArguments: ["first", "last"], sizedFields: ["edges", "nodes"]) + """, + """ + examples1(first: 10) { # Examples1Connection x1 + pageInfo { # PageInfo x1 + hasNextPage # Boolean x1 + } + edges { # Examples1Edge x10 + node { # Example1 x10 + field1 # Boolean x10 + field2(first: 10) { # Examples2Connection x10 + pageInfo { # PageInfo x10 + hasNextPage # Boolean x10 + } + edges { # Examples2Edge x(10x10) + node { # Example2 x(10x10) + field1 # Boolean x(10x10) + field2 # Int x(10x10) + } + } + } + } + } + nodes { # Example1 x10 + field1 # Boolean x10 + } + } + """ + } + }; + } + + private static IRequestExecutorBuilder CreateRequestExecutorBuilder() + { + return new ServiceCollection() + .AddGraphQLServer() + .UseDefaultPipelineWithCostAnalysis() + .AddResolver( + "Query", + "example", + _ => new Example(true, 1)) + .AddResolver( + "Query", + "examples", + _ => new List { new(true, 1) }) + .AddResolver( + "Query", + "examples1", + _ => new Connection( + [ + new Edge( + new Example1( + true, + new Connection( + [new Edge(new Example2(true, 1), "start")], + new ConnectionPageInfo(true, false, "start", "end"))), + "start") + ], + new ConnectionPageInfo(true, false, "start", "end"))) + .AddResolver("Example", "field1", context => context.Parent().Field1) + .AddResolver("Example1", "field1", context => context.Parent().Field1) + .AddResolver("Example2", "field1", context => context.Parent().Field1) + .AddResolver("Example", "field2", context => context.Parent().Field2) + .AddResolver("Example1", "field2", context => context.Parent().Field2) + .AddResolver("Example2", "field2", context => context.Parent().Field2) + .AddResolver( + "Examples1Connection", + "pageInfo", + context => context.Parent>().Info) + .AddResolver( + "Examples2Connection", + "pageInfo", + context => context.Parent>().Info) + .AddResolver( + "Examples1Connection", + "edges", + context => context.Parent>().Edges) + .AddResolver( + "Examples2Connection", + "edges", + context => context.Parent>().Edges) + .AddResolver( + "Examples1Connection", + "nodes", + context => context.Parent>().Edges.Select(e => e.Node)) + .AddResolver( + "Examples2Connection", + "nodes", + context => context.Parent>().Edges.Select(e => e.Node)) + .AddResolver( + "Examples1Edge", + "cursor", + context => context.Parent>().Cursor) + .AddResolver( + "Examples2Edge", + "cursor", + context => context.Parent>().Cursor) + .AddResolver( + "Examples1Edge", + "node", + context => context.Parent>().Node) + .AddResolver( + "Examples2Edge", + "node", + context => context.Parent>().Node) + .AddResolver( + "PageInfo", + "hasNextPage", + context => context.Parent().HasNextPage) + .AddResolver( + "PageInfo", + "hasPreviousPage", + context => context.Parent().HasPreviousPage) + .AddResolver( + "PageInfo", + "startCursor", + context => context.Parent().StartCursor) + .AddResolver( + "PageInfo", + "endCursor", + context => context.Parent().EndCursor); + } + + private sealed record Example(bool Field1, int Field2); + private sealed record Example1(bool Field1, Connection Field2); + private sealed record Example2(bool Field1, int Field2); +} diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_0.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_0.md new file mode 100644 index 00000000000..a67b478f05c --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_0.md @@ -0,0 +1,715 @@ +# Execute_CostQuery_ReturnsExpectedResult + +## Schema + +```text +# Weights used: +# 1.0 = (default for composite and list types) +# 2.0 = ArgumentDefinition +# 3.0 = FieldDefinition +# 4.0 = Object +# 5.0 = InputFieldDefinition +# 6.0 = Scalar +# 7.0 = Enum + +type Query { + examples(limit: Int! @cost(weight: "2.0")): [Example1!]! + @cost(weight: "3.0") @listSize(slicingArguments: ["limit"]) +} + +type Example1 @cost(weight: "4.0") { + example1Field1(arg1: String!, arg2: String!): Boolean! + example1Field2(arg1Input1: Input1!): Example2! + example1Field3: Example3! +} + +type Example2 { + example2Field1(arg1: String!, arg2: String!): Boolean! + example2Field2(arg1Input2: [Input2!]!): Int! +} + +type Example3 { + example3Field1: Scalar1! + example3Field2: Enum1! +} + +input Input1 { input1Field1: String! @cost(weight: "5.0"), input1Field2: Input2! } +input Input2 { input2Field1: String!, input2Field2: String! } + +scalar Scalar1 @cost(weight: "6.0") + +enum Enum1 @cost(weight: "7.0") { ENUM_VALUE1 } + +directive @example(dirArg1: Int!, dirArg2: Input1!) on + | FIELD + | FRAGMENT_DEFINITION + | FRAGMENT_SPREAD + | INLINE_FRAGMENT + | QUERY + | VARIABLE_DEFINITION +``` + +## Query + +```text +query($limit: Int! = 10 @example(dirArg1: 1, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } })) @example(dirArg1: 2, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + examples(limit: $limit) @example(dirArg1: 3, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + ... @example(dirArg1: 4, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example1Field1(arg1: "", arg2: "") @example(dirArg1: 5, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + # Repeated to test indexed paths (f.e. "query.examples.on~Example1[1]"). + ... { example1Field1(arg1: "", arg2: "") } + + example1Field2( + arg1Input1: { + input1Field1: "" + input1Field2: { input2Field1: "", input2Field2: "" } + } + ) { + ...fragment1 @example(dirArg1: 6, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + example1Field3 { example3Field1, aliasField2: example3Field2 } + + # Repeated to test indexed paths (f.e. "query.examples.example1Field3[1]") + example1Field3 { aliasField1: example3Field1, example3Field2 } + } + + __cost { + requestCosts { + fieldCounts { name, value } + typeCounts { name, value } + inputTypeCounts { name, value } + inputFieldCounts { name, value } + argumentCounts { name, value } + directiveCounts { name, value } + + fieldCost + typeCost + + fieldCostByLocation { path, cost } + typeCostByLocation { path, cost } + } +} +} + +fragment fragment1 on Example2 @example(dirArg1: 7, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example2Field1(arg1: "", arg2: "") @example(dirArg1: 8, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + example2Field2(arg1Input2: [ + { input2Field1: "", input2Field2: "" } + { input2Field1: "", input2Field2: "" } + ]) +} +``` + +## Result + +```text +{ + "data": { + "examples": [ + { + "example1Field1": true, + "example1Field2": { + "example2Field1": true, + "example2Field2": 1 + }, + "example1Field3": { + "example3Field1": "", + "aliasField2": "ENUM_VALUE1", + "aliasField1": "", + "example3Field2": "ENUM_VALUE1" + } + } + ], + "__cost": { + "requestCosts": { + "fieldCounts": [ + { + "name": "Query.examples", + "value": 1 + }, + { + "name": "Example1.example1Field1", + "value": 20 + }, + { + "name": "Example1.example1Field2", + "value": 10 + }, + { + "name": "Example2.example2Field1", + "value": 10 + }, + { + "name": "Example2.example2Field2", + "value": 10 + }, + { + "name": "Example1.example1Field3", + "value": 20 + }, + { + "name": "Example3.example3Field1", + "value": 20 + }, + { + "name": "Example3.example3Field2", + "value": 20 + } + ], + "typeCounts": [ + { + "name": "Query", + "value": 1 + }, + { + "name": "Example1", + "value": 10 + }, + { + "name": "Boolean", + "value": 30 + }, + { + "name": "Example2", + "value": 10 + }, + { + "name": "Int", + "value": 10 + }, + { + "name": "Example3", + "value": 20 + }, + { + "name": "Scalar1", + "value": 20 + }, + { + "name": "Enum1", + "value": 20 + } + ], + "inputTypeCounts": [ + { + "name": "Input1", + "value": 54 + }, + { + "name": "Input2", + "value": 64 + } + ], + "inputFieldCounts": [ + { + "name": "Input1.input1Field1", + "value": 54 + }, + { + "name": "Input1.input1Field2", + "value": 54 + }, + { + "name": "Input2.input2Field1", + "value": 74 + }, + { + "name": "Input2.input2Field2", + "value": 74 + } + ], + "argumentCounts": [ + { + "name": "@example(dirArg1:)", + "value": 44 + }, + { + "name": "@example(dirArg2:)", + "value": 44 + }, + { + "name": "Query.examples(limit:)", + "value": 1 + }, + { + "name": "Example1.example1Field1(arg1:)", + "value": 20 + }, + { + "name": "Example1.example1Field1(arg2:)", + "value": 20 + }, + { + "name": "Example1.example1Field2(arg1Input1:)", + "value": 10 + }, + { + "name": "Example2.example2Field1(arg1:)", + "value": 10 + }, + { + "name": "Example2.example2Field1(arg2:)", + "value": 10 + }, + { + "name": "Example2.example2Field2(arg1Input2:)", + "value": 10 + } + ], + "directiveCounts": [ + { + "name": "@example", + "value": 44 + } + ], + "fieldCost": 315, + "typeCost": 210, + "fieldCostByLocation": [ + { + "path": "query($limit:)", + "cost": 5 + }, + { + "path": "query($limit:).@example", + "cost": 5 + }, + { + "path": "query($limit:).@example(dirArg1:)", + "cost": 0 + }, + { + "path": "query($limit:).@example(dirArg2:)", + "cost": 5 + }, + { + "path": "query($limit:).@example(dirArg2:).input1Field1", + "cost": 5 + }, + { + "path": "query($limit:).@example(dirArg2:).input1Field2", + "cost": 0 + }, + { + "path": "query($limit:).@example(dirArg2:).input1Field2.input2Field1", + "cost": 0 + }, + { + "path": "query($limit:).@example(dirArg2:).input1Field2.input2Field2", + "cost": 0 + }, + { + "path": "query", + "cost": 315 + }, + { + "path": "query.@example", + "cost": 5 + }, + { + "path": "query.@example(dirArg1:)", + "cost": 0 + }, + { + "path": "query.@example(dirArg2:)", + "cost": 5 + }, + { + "path": "query.@example(dirArg2:).input1Field1", + "cost": 5 + }, + { + "path": "query.@example(dirArg2:).input1Field2", + "cost": 0 + }, + { + "path": "query.@example(dirArg2:).input1Field2.input2Field1", + "cost": 0 + }, + { + "path": "query.@example(dirArg2:).input1Field2.input2Field2", + "cost": 0 + }, + { + "path": "query.examples", + "cost": 305 + }, + { + "path": "query.examples(limit:)", + "cost": 2 + }, + { + "path": "query.examples.@example", + "cost": 5 + }, + { + "path": "query.examples.@example(dirArg1:)", + "cost": 0 + }, + { + "path": "query.examples.@example(dirArg2:)", + "cost": 5 + }, + { + "path": "query.examples.@example(dirArg2:).input1Field1", + "cost": 5 + }, + { + "path": "query.examples.@example(dirArg2:).input1Field2", + "cost": 0 + }, + { + "path": "query.examples.@example(dirArg2:).input1Field2.input2Field1", + "cost": 0 + }, + { + "path": "query.examples.@example(dirArg2:).input1Field2.input2Field2", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[0].@example", + "cost": 5 + }, + { + "path": "query.examples.on~Example1[0].@example(dirArg1:)", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[0].@example(dirArg2:)", + "cost": 5 + }, + { + "path": "query.examples.on~Example1[0].@example(dirArg2:).input1Field1", + "cost": 5 + }, + { + "path": "query.examples.on~Example1[0].@example(dirArg2:).input1Field2", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[0].@example(dirArg2:).input1Field2.input2Field1", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[0].@example(dirArg2:).input1Field2.input2Field2", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[0]", + "cost": 55 + }, + { + "path": "query.examples.on~Example1[0].example1Field1", + "cost": 50 + }, + { + "path": "query.examples.on~Example1[0].example1Field1(arg1:)", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[0].example1Field1(arg2:)", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[0].example1Field1.@example", + "cost": 50 + }, + { + "path": "query.examples.on~Example1[0].example1Field1.@example(dirArg1:)", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[0].example1Field1.@example(dirArg2:)", + "cost": 50 + }, + { + "path": "query.examples.on~Example1[0].example1Field1.@example(dirArg2:).input1Field1", + "cost": 50 + }, + { + "path": "query.examples.on~Example1[0].example1Field1.@example(dirArg2:).input1Field2", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[0].example1Field1.@example(dirArg2:).input1Field2.input2Field1", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[0].example1Field1.@example(dirArg2:).input1Field2.input2Field2", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[1].example1Field1", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[1].example1Field1(arg1:)", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[1].example1Field1(arg2:)", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[1]", + "cost": 0 + }, + { + "path": "query.examples.example1Field2", + "cost": 220 + }, + { + "path": "query.examples.example1Field2(arg1Input1:)", + "cost": 50 + }, + { + "path": "query.examples.example1Field2(arg1Input1:).input1Field1", + "cost": 50 + }, + { + "path": "query.examples.example1Field2(arg1Input1:).input1Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field2(arg1Input1:).input1Field2.input2Field1", + "cost": 0 + }, + { + "path": "query.examples.example1Field2(arg1Input1:).input1Field2.input2Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1.@example", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1.@example(dirArg1:)", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1.@example(dirArg2:)", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1.@example(dirArg2:).input1Field1", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1.@example(dirArg2:).input1Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1.@example(dirArg2:).input1Field2.input2Field1", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1.@example(dirArg2:).input1Field2.input2Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1", + "cost": 160 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.@example", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.@example(dirArg1:)", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.@example(dirArg2:)", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.@example(dirArg2:).input1Field1", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.@example(dirArg2:).input1Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.@example(dirArg2:).input1Field2.input2Field1", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.@example(dirArg2:).input1Field2.input2Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1", + "cost": 110 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1(arg1:)", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1(arg2:)", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1.@example", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1.@example(dirArg1:)", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1.@example(dirArg2:)", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1.@example(dirArg2:).input1Field1", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1.@example(dirArg2:).input1Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1.@example(dirArg2:).input1Field2.input2Field1", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1.@example(dirArg2:).input1Field2.input2Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2", + "cost": 10 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2(arg1Input2:)", + "cost": 10 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2(arg1Input2:)[0].input2Field1", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2(arg1Input2:)[0]", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2(arg1Input2:)[0].input2Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2(arg1Input2:)[1].input2Field1", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2(arg1Input2:)[1]", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2(arg1Input2:)[1].input2Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field3[0]", + "cost": 10 + }, + { + "path": "query.examples.example1Field3[0].example3Field1", + "cost": 0 + }, + { + "path": "query.examples.example1Field3[0].aliasField2", + "cost": 0 + }, + { + "path": "query.examples.example1Field3[1]", + "cost": 10 + }, + { + "path": "query.examples.example1Field3[1].aliasField1", + "cost": 0 + }, + { + "path": "query.examples.example1Field3[1].example3Field2", + "cost": 0 + } + ], + "typeCostByLocation": [ + { + "path": "query.examples", + "cost": 210 + }, + { + "path": "query.examples.on~Example1[0].example1Field1", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[0]", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[1].example1Field1", + "cost": 0 + }, + { + "path": "query.examples.on~Example1[1]", + "cost": 0 + }, + { + "path": "query.examples.example1Field2", + "cost": 10 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field3[0]", + "cost": 80 + }, + { + "path": "query.examples.example1Field3[0].example3Field1", + "cost": 0 + }, + { + "path": "query.examples.example1Field3[0].aliasField2", + "cost": 70 + }, + { + "path": "query.examples.example1Field3[1]", + "cost": 80 + }, + { + "path": "query.examples.example1Field3[1].aliasField1", + "cost": 0 + }, + { + "path": "query.examples.example1Field3[1].example3Field2", + "cost": 70 + }, + { + "path": "query", + "cost": 210 + } + ] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_1.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_1.md new file mode 100644 index 00000000000..f768dd62553 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_1.md @@ -0,0 +1,145 @@ +# Execute_CostQuery_ReturnsExpectedResult + +## Schema + +```text +# Weights used: +# 1.0 = (default for composite and list types) +# 2.0 = ArgumentDefinition +# 3.0 = FieldDefinition +# 4.0 = Object +# 5.0 = InputFieldDefinition +# 6.0 = Scalar +# 7.0 = Enum + +type Query { + examples(limit: Int! @cost(weight: "2.0")): [Example1!]! + @cost(weight: "3.0") @listSize(slicingArguments: ["limit"]) +} + +type Example1 @cost(weight: "4.0") { + example1Field1(arg1: String!, arg2: String!): Boolean! + example1Field2(arg1Input1: Input1!): Example2! + example1Field3: Example3! +} + +type Example2 { + example2Field1(arg1: String!, arg2: String!): Boolean! + example2Field2(arg1Input2: [Input2!]!): Int! +} + +type Example3 { + example3Field1: Scalar1! + example3Field2: Enum1! +} + +input Input1 { input1Field1: String! @cost(weight: "5.0"), input1Field2: Input2! } +input Input2 { input2Field1: String!, input2Field2: String! } + +scalar Scalar1 @cost(weight: "6.0") + +enum Enum1 @cost(weight: "7.0") { ENUM_VALUE1 } + +directive @example(dirArg1: Int!, dirArg2: Input1!) on + | FIELD + | FRAGMENT_DEFINITION + | FRAGMENT_SPREAD + | INLINE_FRAGMENT + | QUERY + | VARIABLE_DEFINITION +``` + +## Query + +```text +query($limit: Int! = 10 @example(dirArg1: 1, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } })) @example(dirArg1: 2, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + examples(limit: $limit) @example(dirArg1: 3, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + ... @example(dirArg1: 4, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example1Field1(arg1: "", arg2: "") @example(dirArg1: 5, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + # Repeated to test indexed paths (f.e. "query.examples.on~Example1[1]"). + ... { example1Field1(arg1: "", arg2: "") } + + example1Field2( + arg1Input1: { + input1Field1: "" + input1Field2: { input2Field1: "", input2Field2: "" } + } + ) { + ...fragment1 @example(dirArg1: 6, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + example1Field3 { example3Field1, aliasField2: example3Field2 } + + # Repeated to test indexed paths (f.e. "query.examples.example1Field3[1]") + example1Field3 { aliasField1: example3Field1, example3Field2 } + } + + __cost { + requestCosts { + example1Field1Counts: fieldCounts(regexName: "Example1\\.example1Field1") + { name, value } + fieldCountsInExample1Type: fieldCounts(regexName: "Example1\\..+") + { name, value } + } +} +} + +fragment fragment1 on Example2 @example(dirArg1: 7, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example2Field1(arg1: "", arg2: "") @example(dirArg1: 8, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + example2Field2(arg1Input2: [ + { input2Field1: "", input2Field2: "" } + { input2Field1: "", input2Field2: "" } + ]) +} +``` + +## Result + +```text +{ + "data": { + "examples": [ + { + "example1Field1": true, + "example1Field2": { + "example2Field1": true, + "example2Field2": 1 + }, + "example1Field3": { + "example3Field1": "", + "aliasField2": "ENUM_VALUE1", + "aliasField1": "", + "example3Field2": "ENUM_VALUE1" + } + } + ], + "__cost": { + "requestCosts": { + "example1Field1Counts": [ + { + "name": "Example1.example1Field1", + "value": 20 + } + ], + "fieldCountsInExample1Type": [ + { + "name": "Example1.example1Field1", + "value": 20 + }, + { + "name": "Example1.example1Field2", + "value": 10 + }, + { + "name": "Example1.example1Field3", + "value": 20 + } + ] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_2.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_2.md new file mode 100644 index 00000000000..3f954265505 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_2.md @@ -0,0 +1,137 @@ +# Execute_CostQuery_ReturnsExpectedResult + +## Schema + +```text +# Weights used: +# 1.0 = (default for composite and list types) +# 2.0 = ArgumentDefinition +# 3.0 = FieldDefinition +# 4.0 = Object +# 5.0 = InputFieldDefinition +# 6.0 = Scalar +# 7.0 = Enum + +type Query { + examples(limit: Int! @cost(weight: "2.0")): [Example1!]! + @cost(weight: "3.0") @listSize(slicingArguments: ["limit"]) +} + +type Example1 @cost(weight: "4.0") { + example1Field1(arg1: String!, arg2: String!): Boolean! + example1Field2(arg1Input1: Input1!): Example2! + example1Field3: Example3! +} + +type Example2 { + example2Field1(arg1: String!, arg2: String!): Boolean! + example2Field2(arg1Input2: [Input2!]!): Int! +} + +type Example3 { + example3Field1: Scalar1! + example3Field2: Enum1! +} + +input Input1 { input1Field1: String! @cost(weight: "5.0"), input1Field2: Input2! } +input Input2 { input2Field1: String!, input2Field2: String! } + +scalar Scalar1 @cost(weight: "6.0") + +enum Enum1 @cost(weight: "7.0") { ENUM_VALUE1 } + +directive @example(dirArg1: Int!, dirArg2: Input1!) on + | FIELD + | FRAGMENT_DEFINITION + | FRAGMENT_SPREAD + | INLINE_FRAGMENT + | QUERY + | VARIABLE_DEFINITION +``` + +## Query + +```text +query($limit: Int! = 10 @example(dirArg1: 1, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } })) @example(dirArg1: 2, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + examples(limit: $limit) @example(dirArg1: 3, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + ... @example(dirArg1: 4, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example1Field1(arg1: "", arg2: "") @example(dirArg1: 5, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + # Repeated to test indexed paths (f.e. "query.examples.on~Example1[1]"). + ... { example1Field1(arg1: "", arg2: "") } + + example1Field2( + arg1Input1: { + input1Field1: "" + input1Field2: { input2Field1: "", input2Field2: "" } + } + ) { + ...fragment1 @example(dirArg1: 6, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + example1Field3 { example3Field1, aliasField2: example3Field2 } + + # Repeated to test indexed paths (f.e. "query.examples.example1Field3[1]") + example1Field3 { aliasField1: example3Field1, example3Field2 } + } + + __cost { + requestCosts { + example1Counts: typeCounts(regexName: "Example1") + { name, value } + endsWithTCounts: typeCounts(regexName: ".*t") + { name, value } + } +} +} + +fragment fragment1 on Example2 @example(dirArg1: 7, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example2Field1(arg1: "", arg2: "") @example(dirArg1: 8, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + example2Field2(arg1Input2: [ + { input2Field1: "", input2Field2: "" } + { input2Field1: "", input2Field2: "" } + ]) +} +``` + +## Result + +```text +{ + "data": { + "examples": [ + { + "example1Field1": true, + "example1Field2": { + "example2Field1": true, + "example2Field2": 1 + }, + "example1Field3": { + "example3Field1": "", + "aliasField2": "ENUM_VALUE1", + "aliasField1": "", + "example3Field2": "ENUM_VALUE1" + } + } + ], + "__cost": { + "requestCosts": { + "example1Counts": [ + { + "name": "Example1", + "value": 10 + } + ], + "endsWithTCounts": [ + { + "name": "Int", + "value": 10 + } + ] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_3.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_3.md new file mode 100644 index 00000000000..bb5a9bbcebb --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_3.md @@ -0,0 +1,129 @@ +# Execute_CostQuery_ReturnsExpectedResult + +## Schema + +```text +# Weights used: +# 1.0 = (default for composite and list types) +# 2.0 = ArgumentDefinition +# 3.0 = FieldDefinition +# 4.0 = Object +# 5.0 = InputFieldDefinition +# 6.0 = Scalar +# 7.0 = Enum + +type Query { + examples(limit: Int! @cost(weight: "2.0")): [Example1!]! + @cost(weight: "3.0") @listSize(slicingArguments: ["limit"]) +} + +type Example1 @cost(weight: "4.0") { + example1Field1(arg1: String!, arg2: String!): Boolean! + example1Field2(arg1Input1: Input1!): Example2! + example1Field3: Example3! +} + +type Example2 { + example2Field1(arg1: String!, arg2: String!): Boolean! + example2Field2(arg1Input2: [Input2!]!): Int! +} + +type Example3 { + example3Field1: Scalar1! + example3Field2: Enum1! +} + +input Input1 { input1Field1: String! @cost(weight: "5.0"), input1Field2: Input2! } +input Input2 { input2Field1: String!, input2Field2: String! } + +scalar Scalar1 @cost(weight: "6.0") + +enum Enum1 @cost(weight: "7.0") { ENUM_VALUE1 } + +directive @example(dirArg1: Int!, dirArg2: Input1!) on + | FIELD + | FRAGMENT_DEFINITION + | FRAGMENT_SPREAD + | INLINE_FRAGMENT + | QUERY + | VARIABLE_DEFINITION +``` + +## Query + +```text +query($limit: Int! = 10 @example(dirArg1: 1, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } })) @example(dirArg1: 2, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + examples(limit: $limit) @example(dirArg1: 3, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + ... @example(dirArg1: 4, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example1Field1(arg1: "", arg2: "") @example(dirArg1: 5, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + # Repeated to test indexed paths (f.e. "query.examples.on~Example1[1]"). + ... { example1Field1(arg1: "", arg2: "") } + + example1Field2( + arg1Input1: { + input1Field1: "" + input1Field2: { input2Field1: "", input2Field2: "" } + } + ) { + ...fragment1 @example(dirArg1: 6, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + example1Field3 { example3Field1, aliasField2: example3Field2 } + + # Repeated to test indexed paths (f.e. "query.examples.example1Field3[1]") + example1Field3 { aliasField1: example3Field1, example3Field2 } + } + + __cost { + requestCosts { + input2Counts: inputTypeCounts(regexName: "Input2") + { name, value } + } +} +} + +fragment fragment1 on Example2 @example(dirArg1: 7, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example2Field1(arg1: "", arg2: "") @example(dirArg1: 8, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + example2Field2(arg1Input2: [ + { input2Field1: "", input2Field2: "" } + { input2Field1: "", input2Field2: "" } + ]) +} +``` + +## Result + +```text +{ + "data": { + "examples": [ + { + "example1Field1": true, + "example1Field2": { + "example2Field1": true, + "example2Field2": 1 + }, + "example1Field3": { + "example3Field1": "", + "aliasField2": "ENUM_VALUE1", + "aliasField1": "", + "example3Field2": "ENUM_VALUE1" + } + } + ], + "__cost": { + "requestCosts": { + "input2Counts": [ + { + "name": "Input2", + "value": 64 + } + ] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_4.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_4.md new file mode 100644 index 00000000000..a73a4c649b8 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_4.md @@ -0,0 +1,141 @@ +# Execute_CostQuery_ReturnsExpectedResult + +## Schema + +```text +# Weights used: +# 1.0 = (default for composite and list types) +# 2.0 = ArgumentDefinition +# 3.0 = FieldDefinition +# 4.0 = Object +# 5.0 = InputFieldDefinition +# 6.0 = Scalar +# 7.0 = Enum + +type Query { + examples(limit: Int! @cost(weight: "2.0")): [Example1!]! + @cost(weight: "3.0") @listSize(slicingArguments: ["limit"]) +} + +type Example1 @cost(weight: "4.0") { + example1Field1(arg1: String!, arg2: String!): Boolean! + example1Field2(arg1Input1: Input1!): Example2! + example1Field3: Example3! +} + +type Example2 { + example2Field1(arg1: String!, arg2: String!): Boolean! + example2Field2(arg1Input2: [Input2!]!): Int! +} + +type Example3 { + example3Field1: Scalar1! + example3Field2: Enum1! +} + +input Input1 { input1Field1: String! @cost(weight: "5.0"), input1Field2: Input2! } +input Input2 { input2Field1: String!, input2Field2: String! } + +scalar Scalar1 @cost(weight: "6.0") + +enum Enum1 @cost(weight: "7.0") { ENUM_VALUE1 } + +directive @example(dirArg1: Int!, dirArg2: Input1!) on + | FIELD + | FRAGMENT_DEFINITION + | FRAGMENT_SPREAD + | INLINE_FRAGMENT + | QUERY + | VARIABLE_DEFINITION +``` + +## Query + +```text +query($limit: Int! = 10 @example(dirArg1: 1, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } })) @example(dirArg1: 2, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + examples(limit: $limit) @example(dirArg1: 3, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + ... @example(dirArg1: 4, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example1Field1(arg1: "", arg2: "") @example(dirArg1: 5, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + # Repeated to test indexed paths (f.e. "query.examples.on~Example1[1]"). + ... { example1Field1(arg1: "", arg2: "") } + + example1Field2( + arg1Input1: { + input1Field1: "" + input1Field2: { input2Field1: "", input2Field2: "" } + } + ) { + ...fragment1 @example(dirArg1: 6, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + example1Field3 { example3Field1, aliasField2: example3Field2 } + + # Repeated to test indexed paths (f.e. "query.examples.example1Field3[1]") + example1Field3 { aliasField1: example3Field1, example3Field2 } + } + + __cost { + requestCosts { + input1Field1Counts: inputFieldCounts(regexName: "Input1\\.input1Field1") + { name, value } + fieldCountsInInput2Type: inputFieldCounts(regexName: "Input2\\..+") + { name, value } + } +} +} + +fragment fragment1 on Example2 @example(dirArg1: 7, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example2Field1(arg1: "", arg2: "") @example(dirArg1: 8, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + example2Field2(arg1Input2: [ + { input2Field1: "", input2Field2: "" } + { input2Field1: "", input2Field2: "" } + ]) +} +``` + +## Result + +```text +{ + "data": { + "examples": [ + { + "example1Field1": true, + "example1Field2": { + "example2Field1": true, + "example2Field2": 1 + }, + "example1Field3": { + "example3Field1": "", + "aliasField2": "ENUM_VALUE1", + "aliasField1": "", + "example3Field2": "ENUM_VALUE1" + } + } + ], + "__cost": { + "requestCosts": { + "input1Field1Counts": [ + { + "name": "Input1.input1Field1", + "value": 54 + } + ], + "fieldCountsInInput2Type": [ + { + "name": "Input2.input2Field1", + "value": 74 + }, + { + "name": "Input2.input2Field2", + "value": 74 + } + ] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_5.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_5.md new file mode 100644 index 00000000000..e2b10892f69 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_5.md @@ -0,0 +1,145 @@ +# Execute_CostQuery_ReturnsExpectedResult + +## Schema + +```text +# Weights used: +# 1.0 = (default for composite and list types) +# 2.0 = ArgumentDefinition +# 3.0 = FieldDefinition +# 4.0 = Object +# 5.0 = InputFieldDefinition +# 6.0 = Scalar +# 7.0 = Enum + +type Query { + examples(limit: Int! @cost(weight: "2.0")): [Example1!]! + @cost(weight: "3.0") @listSize(slicingArguments: ["limit"]) +} + +type Example1 @cost(weight: "4.0") { + example1Field1(arg1: String!, arg2: String!): Boolean! + example1Field2(arg1Input1: Input1!): Example2! + example1Field3: Example3! +} + +type Example2 { + example2Field1(arg1: String!, arg2: String!): Boolean! + example2Field2(arg1Input2: [Input2!]!): Int! +} + +type Example3 { + example3Field1: Scalar1! + example3Field2: Enum1! +} + +input Input1 { input1Field1: String! @cost(weight: "5.0"), input1Field2: Input2! } +input Input2 { input2Field1: String!, input2Field2: String! } + +scalar Scalar1 @cost(weight: "6.0") + +enum Enum1 @cost(weight: "7.0") { ENUM_VALUE1 } + +directive @example(dirArg1: Int!, dirArg2: Input1!) on + | FIELD + | FRAGMENT_DEFINITION + | FRAGMENT_SPREAD + | INLINE_FRAGMENT + | QUERY + | VARIABLE_DEFINITION +``` + +## Query + +```text +query($limit: Int! = 10 @example(dirArg1: 1, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } })) @example(dirArg1: 2, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + examples(limit: $limit) @example(dirArg1: 3, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + ... @example(dirArg1: 4, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example1Field1(arg1: "", arg2: "") @example(dirArg1: 5, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + # Repeated to test indexed paths (f.e. "query.examples.on~Example1[1]"). + ... { example1Field1(arg1: "", arg2: "") } + + example1Field2( + arg1Input1: { + input1Field1: "" + input1Field2: { input2Field1: "", input2Field2: "" } + } + ) { + ...fragment1 @example(dirArg1: 6, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + example1Field3 { example3Field1, aliasField2: example3Field2 } + + # Repeated to test indexed paths (f.e. "query.examples.example1Field3[1]") + example1Field3 { aliasField1: example3Field1, example3Field2 } + } + + __cost { + requestCosts { + argsNamedArg1Counts: argumentCounts(regexName: ".+\\(arg1:\\)") + { name, value } + argsOnExampleDirectiveCounts: argumentCounts(regexName: "@example\\(.+") + { name, value } + } +} +} + +fragment fragment1 on Example2 @example(dirArg1: 7, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example2Field1(arg1: "", arg2: "") @example(dirArg1: 8, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + example2Field2(arg1Input2: [ + { input2Field1: "", input2Field2: "" } + { input2Field1: "", input2Field2: "" } + ]) +} +``` + +## Result + +```text +{ + "data": { + "examples": [ + { + "example1Field1": true, + "example1Field2": { + "example2Field1": true, + "example2Field2": 1 + }, + "example1Field3": { + "example3Field1": "", + "aliasField2": "ENUM_VALUE1", + "aliasField1": "", + "example3Field2": "ENUM_VALUE1" + } + } + ], + "__cost": { + "requestCosts": { + "argsNamedArg1Counts": [ + { + "name": "Example1.example1Field1(arg1:)", + "value": 20 + }, + { + "name": "Example2.example2Field1(arg1:)", + "value": 10 + } + ], + "argsOnExampleDirectiveCounts": [ + { + "name": "@example(dirArg1:)", + "value": 44 + }, + { + "name": "@example(dirArg2:)", + "value": 44 + } + ] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_6.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_6.md new file mode 100644 index 00000000000..61e23ccad43 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_6.md @@ -0,0 +1,129 @@ +# Execute_CostQuery_ReturnsExpectedResult + +## Schema + +```text +# Weights used: +# 1.0 = (default for composite and list types) +# 2.0 = ArgumentDefinition +# 3.0 = FieldDefinition +# 4.0 = Object +# 5.0 = InputFieldDefinition +# 6.0 = Scalar +# 7.0 = Enum + +type Query { + examples(limit: Int! @cost(weight: "2.0")): [Example1!]! + @cost(weight: "3.0") @listSize(slicingArguments: ["limit"]) +} + +type Example1 @cost(weight: "4.0") { + example1Field1(arg1: String!, arg2: String!): Boolean! + example1Field2(arg1Input1: Input1!): Example2! + example1Field3: Example3! +} + +type Example2 { + example2Field1(arg1: String!, arg2: String!): Boolean! + example2Field2(arg1Input2: [Input2!]!): Int! +} + +type Example3 { + example3Field1: Scalar1! + example3Field2: Enum1! +} + +input Input1 { input1Field1: String! @cost(weight: "5.0"), input1Field2: Input2! } +input Input2 { input2Field1: String!, input2Field2: String! } + +scalar Scalar1 @cost(weight: "6.0") + +enum Enum1 @cost(weight: "7.0") { ENUM_VALUE1 } + +directive @example(dirArg1: Int!, dirArg2: Input1!) on + | FIELD + | FRAGMENT_DEFINITION + | FRAGMENT_SPREAD + | INLINE_FRAGMENT + | QUERY + | VARIABLE_DEFINITION +``` + +## Query + +```text +query($limit: Int! = 10 @example(dirArg1: 1, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } })) @example(dirArg1: 2, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + examples(limit: $limit) @example(dirArg1: 3, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + ... @example(dirArg1: 4, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example1Field1(arg1: "", arg2: "") @example(dirArg1: 5, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + # Repeated to test indexed paths (f.e. "query.examples.on~Example1[1]"). + ... { example1Field1(arg1: "", arg2: "") } + + example1Field2( + arg1Input1: { + input1Field1: "" + input1Field2: { input2Field1: "", input2Field2: "" } + } + ) { + ...fragment1 @example(dirArg1: 6, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + example1Field3 { example3Field1, aliasField2: example3Field2 } + + # Repeated to test indexed paths (f.e. "query.examples.example1Field3[1]") + example1Field3 { aliasField1: example3Field1, example3Field2 } + } + + __cost { + requestCosts { + exampleDirectiveCounts: directiveCounts(regexName: "@example") + { name, value } + } +} +} + +fragment fragment1 on Example2 @example(dirArg1: 7, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example2Field1(arg1: "", arg2: "") @example(dirArg1: 8, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + example2Field2(arg1Input2: [ + { input2Field1: "", input2Field2: "" } + { input2Field1: "", input2Field2: "" } + ]) +} +``` + +## Result + +```text +{ + "data": { + "examples": [ + { + "example1Field1": true, + "example1Field2": { + "example2Field1": true, + "example2Field2": 1 + }, + "example1Field3": { + "example3Field1": "", + "aliasField2": "ENUM_VALUE1", + "aliasField1": "", + "example3Field2": "ENUM_VALUE1" + } + } + ], + "__cost": { + "requestCosts": { + "exampleDirectiveCounts": [ + { + "name": "@example", + "value": 44 + } + ] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_7.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_7.md new file mode 100644 index 00000000000..b89e4766d63 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_7.md @@ -0,0 +1,308 @@ +# Execute_CostQuery_ReturnsExpectedResult + +## Schema + +```text +# Weights used: +# 1.0 = (default for composite and list types) +# 2.0 = ArgumentDefinition +# 3.0 = FieldDefinition +# 4.0 = Object +# 5.0 = InputFieldDefinition +# 6.0 = Scalar +# 7.0 = Enum + +type Query { + examples(limit: Int! @cost(weight: "2.0")): [Example1!]! + @cost(weight: "3.0") @listSize(slicingArguments: ["limit"]) +} + +type Example1 @cost(weight: "4.0") { + example1Field1(arg1: String!, arg2: String!): Boolean! + example1Field2(arg1Input1: Input1!): Example2! + example1Field3: Example3! +} + +type Example2 { + example2Field1(arg1: String!, arg2: String!): Boolean! + example2Field2(arg1Input2: [Input2!]!): Int! +} + +type Example3 { + example3Field1: Scalar1! + example3Field2: Enum1! +} + +input Input1 { input1Field1: String! @cost(weight: "5.0"), input1Field2: Input2! } +input Input2 { input2Field1: String!, input2Field2: String! } + +scalar Scalar1 @cost(weight: "6.0") + +enum Enum1 @cost(weight: "7.0") { ENUM_VALUE1 } + +directive @example(dirArg1: Int!, dirArg2: Input1!) on + | FIELD + | FRAGMENT_DEFINITION + | FRAGMENT_SPREAD + | INLINE_FRAGMENT + | QUERY + | VARIABLE_DEFINITION +``` + +## Query + +```text +query($limit: Int! = 10 @example(dirArg1: 1, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } })) @example(dirArg1: 2, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + examples(limit: $limit) @example(dirArg1: 3, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + ... @example(dirArg1: 4, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example1Field1(arg1: "", arg2: "") @example(dirArg1: 5, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + # Repeated to test indexed paths (f.e. "query.examples.on~Example1[1]"). + ... { example1Field1(arg1: "", arg2: "") } + + example1Field2( + arg1Input1: { + input1Field1: "" + input1Field2: { input2Field1: "", input2Field2: "" } + } + ) { + ...fragment1 @example(dirArg1: 6, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + example1Field3 { example3Field1, aliasField2: example3Field2 } + + # Repeated to test indexed paths (f.e. "query.examples.example1Field3[1]") + example1Field3 { aliasField1: example3Field1, example3Field2 } + } + + __cost { + requestCosts { + exampleDirectiveOnQuery: + fieldCostByLocation(regexPath: "query\\.@example.*") + { path, cost } + fragment1: + fieldCostByLocation(regexPath: ".*~fragment1.*") + { path, cost } + example1Field3: + fieldCostByLocation(regexPath: ".*\\.example1Field3\\[\\d+\\]") + { path, cost } + } +} +} + +fragment fragment1 on Example2 @example(dirArg1: 7, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example2Field1(arg1: "", arg2: "") @example(dirArg1: 8, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + example2Field2(arg1Input2: [ + { input2Field1: "", input2Field2: "" } + { input2Field1: "", input2Field2: "" } + ]) +} +``` + +## Result + +```text +{ + "data": { + "examples": [ + { + "example1Field1": true, + "example1Field2": { + "example2Field1": true, + "example2Field2": 1 + }, + "example1Field3": { + "example3Field1": "", + "aliasField2": "ENUM_VALUE1", + "aliasField1": "", + "example3Field2": "ENUM_VALUE1" + } + } + ], + "__cost": { + "requestCosts": { + "exampleDirectiveOnQuery": [ + { + "path": "query.@example", + "cost": 5 + }, + { + "path": "query.@example(dirArg1:)", + "cost": 0 + }, + { + "path": "query.@example(dirArg2:)", + "cost": 5 + }, + { + "path": "query.@example(dirArg2:).input1Field1", + "cost": 5 + }, + { + "path": "query.@example(dirArg2:).input1Field2", + "cost": 0 + }, + { + "path": "query.@example(dirArg2:).input1Field2.input2Field1", + "cost": 0 + }, + { + "path": "query.@example(dirArg2:).input1Field2.input2Field2", + "cost": 0 + } + ], + "fragment1": [ + { + "path": "query.examples.example1Field2.~fragment1.@example", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1.@example(dirArg1:)", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1.@example(dirArg2:)", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1.@example(dirArg2:).input1Field1", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1.@example(dirArg2:).input1Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1.@example(dirArg2:).input1Field2.input2Field1", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1.@example(dirArg2:).input1Field2.input2Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1", + "cost": 160 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.@example", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.@example(dirArg1:)", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.@example(dirArg2:)", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.@example(dirArg2:).input1Field1", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.@example(dirArg2:).input1Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.@example(dirArg2:).input1Field2.input2Field1", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.@example(dirArg2:).input1Field2.input2Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1", + "cost": 110 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1(arg1:)", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1(arg2:)", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1.@example", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1.@example(dirArg1:)", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1.@example(dirArg2:)", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1.@example(dirArg2:).input1Field1", + "cost": 50 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1.@example(dirArg2:).input1Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1.@example(dirArg2:).input1Field2.input2Field1", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field1.@example(dirArg2:).input1Field2.input2Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2", + "cost": 10 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2(arg1Input2:)", + "cost": 10 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2(arg1Input2:)[0].input2Field1", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2(arg1Input2:)[0]", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2(arg1Input2:)[0].input2Field2", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2(arg1Input2:)[1].input2Field1", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2(arg1Input2:)[1]", + "cost": 0 + }, + { + "path": "query.examples.example1Field2.~fragment1~~fragment1.example2Field2(arg1Input2:)[1].input2Field2", + "cost": 0 + } + ], + "example1Field3": [ + { + "path": "query.examples.example1Field3[0]", + "cost": 10 + }, + { + "path": "query.examples.example1Field3[1]", + "cost": 10 + } + ] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_8.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_8.md new file mode 100644 index 00000000000..750fdc0b4c8 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/IntrospectionTests.Execute_CostQuery_ReturnsExpectedResult_8.md @@ -0,0 +1,143 @@ +# Execute_CostQuery_ReturnsExpectedResult + +## Schema + +```text +# Weights used: +# 1.0 = (default for composite and list types) +# 2.0 = ArgumentDefinition +# 3.0 = FieldDefinition +# 4.0 = Object +# 5.0 = InputFieldDefinition +# 6.0 = Scalar +# 7.0 = Enum + +type Query { + examples(limit: Int! @cost(weight: "2.0")): [Example1!]! + @cost(weight: "3.0") @listSize(slicingArguments: ["limit"]) +} + +type Example1 @cost(weight: "4.0") { + example1Field1(arg1: String!, arg2: String!): Boolean! + example1Field2(arg1Input1: Input1!): Example2! + example1Field3: Example3! +} + +type Example2 { + example2Field1(arg1: String!, arg2: String!): Boolean! + example2Field2(arg1Input2: [Input2!]!): Int! +} + +type Example3 { + example3Field1: Scalar1! + example3Field2: Enum1! +} + +input Input1 { input1Field1: String! @cost(weight: "5.0"), input1Field2: Input2! } +input Input2 { input2Field1: String!, input2Field2: String! } + +scalar Scalar1 @cost(weight: "6.0") + +enum Enum1 @cost(weight: "7.0") { ENUM_VALUE1 } + +directive @example(dirArg1: Int!, dirArg2: Input1!) on + | FIELD + | FRAGMENT_DEFINITION + | FRAGMENT_SPREAD + | INLINE_FRAGMENT + | QUERY + | VARIABLE_DEFINITION +``` + +## Query + +```text +query($limit: Int! = 10 @example(dirArg1: 1, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } })) @example(dirArg1: 2, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + examples(limit: $limit) @example(dirArg1: 3, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + ... @example(dirArg1: 4, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example1Field1(arg1: "", arg2: "") @example(dirArg1: 5, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + # Repeated to test indexed paths (f.e. "query.examples.on~Example1[1]"). + ... { example1Field1(arg1: "", arg2: "") } + + example1Field2( + arg1Input1: { + input1Field1: "" + input1Field2: { input2Field1: "", input2Field2: "" } + } + ) { + ...fragment1 @example(dirArg1: 6, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + } + + example1Field3 { example3Field1, aliasField2: example3Field2 } + + # Repeated to test indexed paths (f.e. "query.examples.example1Field3[1]") + example1Field3 { aliasField1: example3Field1, example3Field2 } + } + + __cost { + requestCosts { + examplesField: + typeCostByLocation(regexPath: "query\\.examples") + { path, cost } + example1Field3: + typeCostByLocation(regexPath: ".*\\.example1Field3\\[\\d+\\]") + { path, cost } + } +} +} + +fragment fragment1 on Example2 @example(dirArg1: 7, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) { + example2Field1(arg1: "", arg2: "") @example(dirArg1: 8, dirArg2: { input1Field1: "", input1Field2: { input2Field1: "", input2Field2: "" } }) + example2Field2(arg1Input2: [ + { input2Field1: "", input2Field2: "" } + { input2Field1: "", input2Field2: "" } + ]) +} +``` + +## Result + +```text +{ + "data": { + "examples": [ + { + "example1Field1": true, + "example1Field2": { + "example2Field1": true, + "example2Field2": 1 + }, + "example1Field3": { + "example3Field1": "", + "aliasField2": "ENUM_VALUE1", + "aliasField1": "", + "example3Field2": "ENUM_VALUE1" + } + } + ], + "__cost": { + "requestCosts": { + "examplesField": [ + { + "path": "query.examples", + "cost": 210 + } + ], + "example1Field3": [ + { + "path": "query.examples.example1Field3[0]", + "cost": 80 + }, + { + "path": "query.examples.example1Field3[1]", + "cost": 80 + } + ] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_0.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_0.md new file mode 100644 index 00000000000..56afe74997f --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_0.md @@ -0,0 +1,65 @@ +# Execute_SpecificationExample_ReturnsExpectedResult + +## Schema + +```text +type User { + name: String + age: Int @cost(weight: "2.0") +} + +type Query { + users(max: Int): [User] @listSize(slicingArguments: ["max"]) +} +``` + +## Query + +```text +query Example { + users(max: 5) { + age +} + + __cost { + requestCosts { + fieldCostByLocation { path, cost } + fieldCost + } + } +} +``` + +## Result + +```text +{ + "data": { + "users": null, + "__cost": { + "requestCosts": { + "fieldCostByLocation": [ + { + "path": "Example.users", + "cost": 11 + }, + { + "path": "Example.users(max:)", + "cost": 0 + }, + { + "path": "Example.users.age", + "cost": 10 + }, + { + "path": "Example", + "cost": 11 + } + ], + "fieldCost": 11 + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_1.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_1.md new file mode 100644 index 00000000000..edca2fcf0f0 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_1.md @@ -0,0 +1,53 @@ +# Execute_SpecificationExample_ReturnsExpectedResult + +## Schema + +```text +type Query { + topProducts(filter: Filter @cost(weight: "15.0")): [String] + @cost(weight: "5.0") @listSize(assumedSize: 10) +} + +input Filter { field: Boolean! } +``` + +## Query + +```text +query Example { + topProducts + + __cost { + requestCosts { + fieldCostByLocation { path, cost } + fieldCost + } + } +} +``` + +## Result + +```text +{ + "data": { + "topProducts": null, + "__cost": { + "requestCosts": { + "fieldCostByLocation": [ + { + "path": "Example.topProducts", + "cost": 5 + }, + { + "path": "Example", + "cost": 5 + } + ], + "fieldCost": 5 + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_2.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_2.md new file mode 100644 index 00000000000..a6fe396fee0 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_2.md @@ -0,0 +1,61 @@ +# Execute_SpecificationExample_ReturnsExpectedResult + +## Schema + +```text +type Query { + topProducts(filter: Filter @cost(weight: "15.0")): [String] + @cost(weight: "5.0") @listSize(assumedSize: 10) +} + +input Filter { field: Boolean! } +``` + +## Query + +```text +query Example { + topProducts(filter: { field: true }) + + __cost { + requestCosts { + fieldCostByLocation { path, cost } + fieldCost + } + } +} +``` + +## Result + +```text +{ + "data": { + "topProducts": null, + "__cost": { + "requestCosts": { + "fieldCostByLocation": [ + { + "path": "Example.topProducts", + "cost": 20 + }, + { + "path": "Example.topProducts(filter:)", + "cost": 15 + }, + { + "path": "Example.topProducts(filter:).field", + "cost": 0 + }, + { + "path": "Example", + "cost": 20 + } + ], + "fieldCost": 20 + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_3.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_3.md new file mode 100644 index 00000000000..806cca819e9 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_3.md @@ -0,0 +1,58 @@ +# Execute_SpecificationExample_ReturnsExpectedResult + +## Schema + +```text +type Query { + mostPopularProduct(approx: Approximate @cost(weight: "-3.0")): Product + @cost(weight: "5.0") +} + +input Approximate { field: Boolean! } +type Product { field: Boolean! } +``` + +## Query + +```text +query Example { + mostPopularProduct { field } + + __cost { + requestCosts { + fieldCostByLocation { path, cost } + fieldCost + } + } +} +``` + +## Result + +```text +{ + "data": { + "mostPopularProduct": null, + "__cost": { + "requestCosts": { + "fieldCostByLocation": [ + { + "path": "Example.mostPopularProduct", + "cost": 5 + }, + { + "path": "Example.mostPopularProduct.field", + "cost": 0 + }, + { + "path": "Example", + "cost": 5 + } + ], + "fieldCost": 5 + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_4.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_4.md new file mode 100644 index 00000000000..30448cd3310 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_4.md @@ -0,0 +1,66 @@ +# Execute_SpecificationExample_ReturnsExpectedResult + +## Schema + +```text +type Query { + mostPopularProduct(approx: Approximate @cost(weight: "-3.0")): Product + @cost(weight: "5.0") +} + +input Approximate { field: Boolean! } +type Product { field: Boolean! } +``` + +## Query + +```text +query Example { + mostPopularProduct(approx: { field: true }) { field } + + __cost { + requestCosts { + fieldCostByLocation { path, cost } + fieldCost + } + } +} +``` + +## Result + +```text +{ + "data": { + "mostPopularProduct": null, + "__cost": { + "requestCosts": { + "fieldCostByLocation": [ + { + "path": "Example.mostPopularProduct", + "cost": 2 + }, + { + "path": "Example.mostPopularProduct(approx:)", + "cost": -3 + }, + { + "path": "Example.mostPopularProduct(approx:).field", + "cost": 0 + }, + { + "path": "Example.mostPopularProduct.field", + "cost": 0 + }, + { + "path": "Example", + "cost": 2 + } + ], + "fieldCost": 2 + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_5.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_5.md new file mode 100644 index 00000000000..fb88eb98e20 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_5.md @@ -0,0 +1,69 @@ +# Execute_SpecificationExample_ReturnsExpectedResult + +## Schema + +```text +input Filter { + approx: Approximate @cost(weight: "-12.0") +} + +type Query { + topProducts(filter: Filter @cost(weight: "15.0")): [String] + @cost(weight: "5.0") @listSize(assumedSize: 10) +} + +input Approximate { field: Boolean! } +``` + +## Query + +```text +query Example { + topProducts(filter: { approx: { field: true } }) + + __cost { + requestCosts { + fieldCostByLocation { path, cost } + fieldCost + } + } +} +``` + +## Result + +```text +{ + "data": { + "topProducts": null, + "__cost": { + "requestCosts": { + "fieldCostByLocation": [ + { + "path": "Example.topProducts", + "cost": 8 + }, + { + "path": "Example.topProducts(filter:)", + "cost": 3 + }, + { + "path": "Example.topProducts(filter:).approx", + "cost": -12 + }, + { + "path": "Example.topProducts(filter:).approx.field", + "cost": 0 + }, + { + "path": "Example", + "cost": 8 + } + ], + "fieldCost": 8 + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_6.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_6.md new file mode 100644 index 00000000000..b90bd29b4ab --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/SpecificationExampleTests.Execute_SpecificationExample_ReturnsExpectedResult_6.md @@ -0,0 +1,60 @@ +# Execute_SpecificationExample_ReturnsExpectedResult + +## Schema + +```text +directive @approx(tolerance: Float! @cost(weight: "-1.0")) on FIELD + +type Query { + example: [String] @cost(weight: "5.0") +} +``` + +## Query + +```text +query Example { + example @approx(tolerance: 0) + + __cost { + requestCosts { + fieldCostByLocation { path, cost } + fieldCost + } + } +} +``` + +## Result + +```text +{ + "data": { + "example": null, + "__cost": { + "requestCosts": { + "fieldCostByLocation": [ + { + "path": "Example.example", + "cost": 5 + }, + { + "path": "Example.example.@approx", + "cost": 0 + }, + { + "path": "Example.example.@approx(tolerance:)", + "cost": 0 + }, + { + "path": "Example", + "cost": 5 + } + ], + "fieldCost": 5 + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ConnectionQuery_ReturnsExpectedResult_0.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ConnectionQuery_ReturnsExpectedResult_0.md new file mode 100644 index 00000000000..9e9a337df48 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ConnectionQuery_ReturnsExpectedResult_0.md @@ -0,0 +1,245 @@ +# Execute_ConnectionQuery_ReturnsExpectedResult + +## Schema + +```text +type Query { + examples1(first: Int, after: String, last: Int, before: String): Examples1Connection + @listSize(slicingArguments: ["first", "last"], sizedFields: ["edges", "nodes"]) +} + +type Example1 { + field1: Boolean! + field2(first: Int, after: String, last: Int, before: String): Examples2Connection + @listSize(slicingArguments: ["first", "last"], sizedFields: ["edges"]) +} + +type Example2 { + field1: Boolean! + field2: Int! +} + +type Examples1Connection { + pageInfo: PageInfo! + edges: [Examples1Edge!] + nodes: [Example1!] +} + +type Examples2Connection { + pageInfo: PageInfo! + edges: [Examples2Edge!] + nodes: [Example2!] +} + +type Examples1Edge { + cursor: String! + node: Example1! +} + +type Examples2Edge { + cursor: String! + node: Example2! +} + +type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String +} +``` + +## Query + +```text +query { + examples1(first: 10) { # Examples1Connection x1 + pageInfo { # PageInfo x1 + hasNextPage # Boolean x1 + } + edges { # Examples1Edge x10 + node { # Example1 x10 + field1 # Boolean x10 + field2(first: 10) { # Examples2Connection x10 + pageInfo { # PageInfo x10 + hasNextPage # Boolean x10 + } + edges { # Examples2Edge x(10x10) + node { # Example2 x(10x10) + field1 # Boolean x(10x10) + field2 # Int x(10x10) + } + } + } + } + } + nodes { # Example1 x10 + field1 # Boolean x10 + } +} + + __cost { + requestCosts { + fieldCounts { name, value } + typeCounts { name, value } + inputTypeCounts { name, value } + inputFieldCounts { name, value } + argumentCounts { name, value } + directiveCounts { name, value } + } + } +} +``` + +## Result + +```text +{ + "data": { + "examples1": { + "pageInfo": { + "hasNextPage": true + }, + "edges": [ + { + "node": { + "field1": true, + "field2": { + "pageInfo": { + "hasNextPage": true + }, + "edges": [ + { + "node": { + "field1": true, + "field2": 1 + } + } + ] + } + } + } + ], + "nodes": [ + { + "field1": true + } + ] + }, + "__cost": { + "requestCosts": { + "fieldCounts": [ + { + "name": "Query.examples1", + "value": 1 + }, + { + "name": "Examples1Connection.pageInfo", + "value": 1 + }, + { + "name": "PageInfo.hasNextPage", + "value": 11 + }, + { + "name": "Examples1Connection.edges", + "value": 1 + }, + { + "name": "Examples1Edge.node", + "value": 10 + }, + { + "name": "Example1.field1", + "value": 20 + }, + { + "name": "Example1.field2", + "value": 10 + }, + { + "name": "Examples2Connection.pageInfo", + "value": 10 + }, + { + "name": "Examples2Connection.edges", + "value": 10 + }, + { + "name": "Examples2Edge.node", + "value": 100 + }, + { + "name": "Example2.field1", + "value": 100 + }, + { + "name": "Example2.field2", + "value": 100 + }, + { + "name": "Examples1Connection.nodes", + "value": 1 + } + ], + "typeCounts": [ + { + "name": "Query", + "value": 1 + }, + { + "name": "Examples1Connection", + "value": 1 + }, + { + "name": "PageInfo", + "value": 11 + }, + { + "name": "Boolean", + "value": 131 + }, + { + "name": "Examples1Edge", + "value": 10 + }, + { + "name": "Example1", + "value": 20 + }, + { + "name": "Examples2Connection", + "value": 10 + }, + { + "name": "Examples2Edge", + "value": 100 + }, + { + "name": "Example2", + "value": 100 + }, + { + "name": "Int", + "value": 100 + } + ], + "inputTypeCounts": [], + "inputFieldCounts": [], + "argumentCounts": [ + { + "name": "Query.examples1(first:)", + "value": 1 + }, + { + "name": "Example1.field2(first:)", + "value": 10 + } + ], + "directiveCounts": [] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_0.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_0.md new file mode 100644 index 00000000000..f0b0261ae6c --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_0.md @@ -0,0 +1,94 @@ +# Execute_ListQuery_ReturnsExpectedResult + +## Schema + +```text +type Query { + examples(limit: Int): [Example!]! +} + +type Example { + field1: Boolean! + field2: Int! +} +``` + +## Query + +```text +query { + examples(limit: 10) { field1, field2 } + + __cost { + requestCosts { + fieldCounts { name, value } + typeCounts { name, value } + inputTypeCounts { name, value } + inputFieldCounts { name, value } + argumentCounts { name, value } + directiveCounts { name, value } + } + } +} +``` + +## Result + +```text +{ + "data": { + "examples": [ + { + "field1": true, + "field2": 1 + } + ], + "__cost": { + "requestCosts": { + "fieldCounts": [ + { + "name": "Query.examples", + "value": 1 + }, + { + "name": "Example.field1", + "value": 1 + }, + { + "name": "Example.field2", + "value": 1 + } + ], + "typeCounts": [ + { + "name": "Query", + "value": 1 + }, + { + "name": "Example", + "value": 1 + }, + { + "name": "Boolean", + "value": 1 + }, + { + "name": "Int", + "value": 1 + } + ], + "inputTypeCounts": [], + "inputFieldCounts": [], + "argumentCounts": [ + { + "name": "Query.examples(limit:)", + "value": 1 + } + ], + "directiveCounts": [] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_1.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_1.md new file mode 100644 index 00000000000..056e60ae2e0 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_1.md @@ -0,0 +1,94 @@ +# Execute_ListQuery_ReturnsExpectedResult + +## Schema + +```text +type Query { + examples(limit: Int): [Example!]! @listSize +} + +type Example { + field1: Boolean! + field2: Int! +} +``` + +## Query + +```text +query { + examples(limit: 10) { field1, field2 } + + __cost { + requestCosts { + fieldCounts { name, value } + typeCounts { name, value } + inputTypeCounts { name, value } + inputFieldCounts { name, value } + argumentCounts { name, value } + directiveCounts { name, value } + } + } +} +``` + +## Result + +```text +{ + "data": { + "examples": [ + { + "field1": true, + "field2": 1 + } + ], + "__cost": { + "requestCosts": { + "fieldCounts": [ + { + "name": "Query.examples", + "value": 1 + }, + { + "name": "Example.field1", + "value": 1 + }, + { + "name": "Example.field2", + "value": 1 + } + ], + "typeCounts": [ + { + "name": "Query", + "value": 1 + }, + { + "name": "Example", + "value": 1 + }, + { + "name": "Boolean", + "value": 1 + }, + { + "name": "Int", + "value": 1 + } + ], + "inputTypeCounts": [], + "inputFieldCounts": [], + "argumentCounts": [ + { + "name": "Query.examples(limit:)", + "value": 1 + } + ], + "directiveCounts": [] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_2.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_2.md new file mode 100644 index 00000000000..8b7570fb88f --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_2.md @@ -0,0 +1,94 @@ +# Execute_ListQuery_ReturnsExpectedResult + +## Schema + +```text +type Query { + examples(limit: Int): [Example!]! @listSize(slicingArguments: ["limit"]) +} + +type Example { + field1: Boolean! + field2: Int! +} +``` + +## Query + +```text +query { + examples(limit: 10) { field1, field2 } + + __cost { + requestCosts { + fieldCounts { name, value } + typeCounts { name, value } + inputTypeCounts { name, value } + inputFieldCounts { name, value } + argumentCounts { name, value } + directiveCounts { name, value } + } + } +} +``` + +## Result + +```text +{ + "data": { + "examples": [ + { + "field1": true, + "field2": 1 + } + ], + "__cost": { + "requestCosts": { + "fieldCounts": [ + { + "name": "Query.examples", + "value": 1 + }, + { + "name": "Example.field1", + "value": 10 + }, + { + "name": "Example.field2", + "value": 10 + } + ], + "typeCounts": [ + { + "name": "Query", + "value": 1 + }, + { + "name": "Example", + "value": 10 + }, + { + "name": "Boolean", + "value": 10 + }, + { + "name": "Int", + "value": 10 + } + ], + "inputTypeCounts": [], + "inputFieldCounts": [], + "argumentCounts": [ + { + "name": "Query.examples(limit:)", + "value": 1 + } + ], + "directiveCounts": [] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_3.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_3.md new file mode 100644 index 00000000000..99f8af75043 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_3.md @@ -0,0 +1,94 @@ +# Execute_ListQuery_ReturnsExpectedResult + +## Schema + +```text +type Query { + examples(limit: Int): [Example!]! @listSize(slicingArguments: ["limit"]) +} + +type Example { + field1: Boolean! + field2: Int! +} +``` + +## Query + +```text +query { + examples(limit: null) { field1, field2 } + + __cost { + requestCosts { + fieldCounts { name, value } + typeCounts { name, value } + inputTypeCounts { name, value } + inputFieldCounts { name, value } + argumentCounts { name, value } + directiveCounts { name, value } + } + } +} +``` + +## Result + +```text +{ + "data": { + "examples": [ + { + "field1": true, + "field2": 1 + } + ], + "__cost": { + "requestCosts": { + "fieldCounts": [ + { + "name": "Query.examples", + "value": 1 + }, + { + "name": "Example.field1", + "value": 1 + }, + { + "name": "Example.field2", + "value": 1 + } + ], + "typeCounts": [ + { + "name": "Query", + "value": 1 + }, + { + "name": "Example", + "value": 1 + }, + { + "name": "Boolean", + "value": 1 + }, + { + "name": "Int", + "value": 1 + } + ], + "inputTypeCounts": [], + "inputFieldCounts": [], + "argumentCounts": [ + { + "name": "Query.examples(limit:)", + "value": 1 + } + ], + "directiveCounts": [] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_4.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_4.md new file mode 100644 index 00000000000..cc8f98c3b95 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_4.md @@ -0,0 +1,46 @@ +# Execute_ListQuery_ReturnsExpectedResult + +## Schema + +```text +type Query { + examples(limit: Int): [Example!]! @listSize(slicingArguments: ["limit"]) +} + +type Example { + field1: Boolean! + field2: Int! +} +``` + +## Query + +```text +query { + examples { field1, field2 } + + __cost { + requestCosts { + fieldCounts { name, value } + typeCounts { name, value } + inputTypeCounts { name, value } + inputFieldCounts { name, value } + argumentCounts { name, value } + directiveCounts { name, value } + } + } +} +``` + +## Result + +```text +{ + "errors": [ + { + "message": "Unexpected Execution Error" + } + ] +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_5.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_5.md new file mode 100644 index 00000000000..5239fcc2673 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_5.md @@ -0,0 +1,95 @@ +# Execute_ListQuery_ReturnsExpectedResult + +## Schema + +```text +type Query { + examples(limit: Int): [Example!]! + @listSize(slicingArguments: ["limit"], assumedSize: 10) +} + +type Example { + field1: Boolean! + field2: Int! +} +``` + +## Query + +```text +query { + examples(limit: null) { field1, field2 } + + __cost { + requestCosts { + fieldCounts { name, value } + typeCounts { name, value } + inputTypeCounts { name, value } + inputFieldCounts { name, value } + argumentCounts { name, value } + directiveCounts { name, value } + } + } +} +``` + +## Result + +```text +{ + "data": { + "examples": [ + { + "field1": true, + "field2": 1 + } + ], + "__cost": { + "requestCosts": { + "fieldCounts": [ + { + "name": "Query.examples", + "value": 1 + }, + { + "name": "Example.field1", + "value": 10 + }, + { + "name": "Example.field2", + "value": 10 + } + ], + "typeCounts": [ + { + "name": "Query", + "value": 1 + }, + { + "name": "Example", + "value": 10 + }, + { + "name": "Boolean", + "value": 10 + }, + { + "name": "Int", + "value": 10 + } + ], + "inputTypeCounts": [], + "inputFieldCounts": [], + "argumentCounts": [ + { + "name": "Query.examples(limit:)", + "value": 1 + } + ], + "directiveCounts": [] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_6.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_6.md new file mode 100644 index 00000000000..c355d3720c2 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_6.md @@ -0,0 +1,47 @@ +# Execute_ListQuery_ReturnsExpectedResult + +## Schema + +```text +type Query { + examples(limit: Int): [Example!]! + @listSize(slicingArguments: ["limit"], assumedSize: 10) +} + +type Example { + field1: Boolean! + field2: Int! +} +``` + +## Query + +```text +query { + examples { field1, field2 } + + __cost { + requestCosts { + fieldCounts { name, value } + typeCounts { name, value } + inputTypeCounts { name, value } + inputFieldCounts { name, value } + argumentCounts { name, value } + directiveCounts { name, value } + } + } +} +``` + +## Result + +```text +{ + "errors": [ + { + "message": "Unexpected Execution Error" + } + ] +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_7.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_7.md new file mode 100644 index 00000000000..c0c8f006b5e --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_7.md @@ -0,0 +1,90 @@ +# Execute_ListQuery_ReturnsExpectedResult + +## Schema + +```text +type Query { + examples(limit: Int): [Example!]! + @listSize(slicingArguments: ["limit"], requireOneSlicingArgument: false) +} + +type Example { + field1: Boolean! + field2: Int! +} +``` + +## Query + +```text +query { + examples { field1, field2 } + + __cost { + requestCosts { + fieldCounts { name, value } + typeCounts { name, value } + inputTypeCounts { name, value } + inputFieldCounts { name, value } + argumentCounts { name, value } + directiveCounts { name, value } + } + } +} +``` + +## Result + +```text +{ + "data": { + "examples": [ + { + "field1": true, + "field2": 1 + } + ], + "__cost": { + "requestCosts": { + "fieldCounts": [ + { + "name": "Query.examples", + "value": 1 + }, + { + "name": "Example.field1", + "value": 1 + }, + { + "name": "Example.field2", + "value": 1 + } + ], + "typeCounts": [ + { + "name": "Query", + "value": 1 + }, + { + "name": "Example", + "value": 1 + }, + { + "name": "Boolean", + "value": 1 + }, + { + "name": "Int", + "value": 1 + } + ], + "inputTypeCounts": [], + "inputFieldCounts": [], + "argumentCounts": [], + "directiveCounts": [] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_8.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_8.md new file mode 100644 index 00000000000..b5db3cb94c8 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/StaticQueryAnalysisTests.Execute_ListQuery_ReturnsExpectedResult_8.md @@ -0,0 +1,94 @@ +# Execute_ListQuery_ReturnsExpectedResult + +## Schema + +```text +type Query { + examples(limit: Int): [Example!]! + @listSize( + slicingArguments: ["limit"], + assumedSize: 10, + requireOneSlicingArgument: false + ) +} + +type Example { + field1: Boolean! + field2: Int! +} +``` + +## Query + +```text +query { + examples { field1, field2 } + + __cost { + requestCosts { + fieldCounts { name, value } + typeCounts { name, value } + inputTypeCounts { name, value } + inputFieldCounts { name, value } + argumentCounts { name, value } + directiveCounts { name, value } + } + } +} +``` + +## Result + +```text +{ + "data": { + "examples": [ + { + "field1": true, + "field2": 1 + } + ], + "__cost": { + "requestCosts": { + "fieldCounts": [ + { + "name": "Query.examples", + "value": 1 + }, + { + "name": "Example.field1", + "value": 10 + }, + { + "name": "Example.field2", + "value": 10 + } + ], + "typeCounts": [ + { + "name": "Query", + "value": 1 + }, + { + "name": "Example", + "value": 10 + }, + { + "name": "Boolean", + "value": 10 + }, + { + "name": "Int", + "value": 10 + } + ], + "inputTypeCounts": [], + "inputFieldCounts": [], + "argumentCounts": [], + "directiveCounts": [] + } + } + } +} +``` + diff --git a/src/HotChocolate/CostAnalysis/test/Directory.Build.props b/src/HotChocolate/CostAnalysis/test/Directory.Build.props new file mode 100644 index 00000000000..171a90dde24 --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/Directory.Build.props @@ -0,0 +1,42 @@ + + + + + false + false + false + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + +