From 8908d5974497b54b63fc3c3a378070b70052c793 Mon Sep 17 00:00:00 2001 From: Glen Date: Mon, 28 Oct 2024 12:44:16 +0200 Subject: [PATCH 1/7] Added opt-in features --- .../src/Abstractions/WellKnownDirectives.cs | 25 + .../Validation/RequiresOptInValidationRule.cs | 67 +++ .../Validation/SchemaValidator.cs | 3 +- .../Core/src/Types/IReadOnlySchemaOptions.cs | 5 + .../Properties/TypeResources.Designer.cs | 81 +++ .../src/Types/Properties/TypeResources.resx | 27 + .../Core/src/Types/SchemaBuilder.cs | 1 + .../Core/src/Types/SchemaOptions.cs | 5 + .../src/Types/Types/Directives/Directives.cs | 9 + .../OptInFeatureStabilityDirective.cs | 37 ++ ...ptInFeatureStabilityDirectiveExtensions.cs | 12 + .../OptInFeatureStabilityDirectiveType.cs | 32 + .../Directives/RequiresOptInAttribute.cs | 66 ++ .../Directives/RequiresOptInDirective.cs | 60 ++ .../RequiresOptInDirectiveExtensions.cs | 147 +++++ .../Directives/RequiresOptInDirectiveType.cs | 38 ++ .../OptInFeaturesTypeInterceptor.cs | 62 ++ .../Types/Introspection/IntrospectionTypes.cs | 5 + .../Types/Types/Introspection/__EnumValue.cs | 15 + .../src/Types/Types/Introspection/__Field.cs | 45 +- .../Types/Types/Introspection/__InputValue.cs | 16 + .../Introspection/__OptInFeatureStability.cs | 58 ++ .../src/Types/Types/Introspection/__Schema.cs | 27 + .../src/Types/Types/Introspection/__Type.cs | 131 +++- .../Core/src/Types/Utilities/ErrorHelper.cs | 20 + .../OptInFeaturesIntrospectionTests.cs | 562 ++++++++++++++++++ .../Validation/RequiresOptInValidation.cs | 83 +++ .../Validation/TypeValidationTestBase.cs | 12 +- ....Must_Not_Appear_On_Required_Argument.snap | 18 + ...Appear_On_Required_Input_Object_Field.snap | 16 + .../OptInFeatureStabilityDirectiveTests.cs | 54 ++ .../Directives/RequiresOptInDirectiveTests.cs | 124 ++++ ...emaAsync_CodeFirst_MatchesSnapshot.graphql | 10 + ...aAsync_SchemaFirst_MatchesSnapshot.graphql | 10 + ...emaAsync_CodeFirst_MatchesSnapshot.graphql | 18 + ...mplementationFirst_MatchesSnapshot.graphql | 18 + ...aAsync_SchemaFirst_MatchesSnapshot.graphql | 18 + 37 files changed, 1925 insertions(+), 12 deletions(-) create mode 100644 src/HotChocolate/Core/src/Types/Configuration/Validation/RequiresOptInValidationRule.cs create mode 100644 src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirective.cs create mode 100644 src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirectiveExtensions.cs create mode 100644 src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirectiveType.cs create mode 100644 src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInAttribute.cs create mode 100644 src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirective.cs create mode 100644 src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirectiveExtensions.cs create mode 100644 src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirectiveType.cs create mode 100644 src/HotChocolate/Core/src/Types/Types/Interceptors/OptInFeaturesTypeInterceptor.cs create mode 100644 src/HotChocolate/Core/src/Types/Types/Introspection/__OptInFeatureStability.cs create mode 100644 src/HotChocolate/Core/test/Execution.Tests/OptInFeaturesIntrospectionTests.cs create mode 100644 src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/RequiresOptInValidation.cs create mode 100644 src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/RequiresOptInValidation.Must_Not_Appear_On_Required_Argument.snap create mode 100644 src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/RequiresOptInValidation.Must_Not_Appear_On_Required_Input_Object_Field.snap create mode 100644 src/HotChocolate/Core/test/Types.Tests/Types/Directives/OptInFeatureStabilityDirectiveTests.cs create mode 100644 src/HotChocolate/Core/test/Types.Tests/Types/Directives/RequiresOptInDirectiveTests.cs create mode 100644 src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/OptInFeatureStabilityDirectiveTests.BuildSchemaAsync_CodeFirst_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/OptInFeatureStabilityDirectiveTests.BuildSchemaAsync_SchemaFirst_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/RequiresOptInDirectiveTests.BuildSchemaAsync_CodeFirst_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/RequiresOptInDirectiveTests.BuildSchemaAsync_ImplementationFirst_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/RequiresOptInDirectiveTests.BuildSchemaAsync_SchemaFirst_MatchesSnapshot.graphql diff --git a/src/HotChocolate/Core/src/Abstractions/WellKnownDirectives.cs b/src/HotChocolate/Core/src/Abstractions/WellKnownDirectives.cs index 9126408eb4b..c05e887b776 100644 --- a/src/HotChocolate/Core/src/Abstractions/WellKnownDirectives.cs +++ b/src/HotChocolate/Core/src/Abstractions/WellKnownDirectives.cs @@ -69,4 +69,29 @@ public static class WellKnownDirectives /// The name of the @tag argument name. /// public const string Name = "name"; + + /// + /// The name of the @requiresOptIn directive. + /// + public const string RequiresOptIn = "requiresOptIn"; + + /// + /// The name of the @requiresOptIn feature argument. + /// + public const string RequiresOptInFeatureArgument = "feature"; + + /// + /// The name of the @optInFeatureStability directive. + /// + public const string OptInFeatureStability = "optInFeatureStability"; + + /// + /// The name of the @optInFeatureStability feature argument. + /// + public const string OptInFeatureStabilityFeatureArgument = "feature"; + + /// + /// The name of the @optInFeatureStability stability argument. + /// + public const string OptInFeatureStabilityStabilityArgument = "stability"; } diff --git a/src/HotChocolate/Core/src/Types/Configuration/Validation/RequiresOptInValidationRule.cs b/src/HotChocolate/Core/src/Types/Configuration/Validation/RequiresOptInValidationRule.cs new file mode 100644 index 00000000000..1c31246defb --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Configuration/Validation/RequiresOptInValidationRule.cs @@ -0,0 +1,67 @@ +using HotChocolate.Types; +using HotChocolate.Types.Descriptors; +using static HotChocolate.Utilities.ErrorHelper; + +namespace HotChocolate.Configuration.Validation; + +internal sealed class RequiresOptInValidationRule : ISchemaValidationRule +{ + public void Validate( + IDescriptorContext context, + ISchema schema, + ICollection errors) + { + if (!context.Options.EnableOptInFeatures) + { + return; + } + + foreach (var type in schema.Types) + { + switch (type) + { + case IInputObjectType inputObjectType: + foreach (var field in inputObjectType.Fields) + { + if (field.Type.IsNonNullType() && field.DefaultValue is null) + { + var requiresOptInDirectives = field.Directives + .Where(d => d.Type is RequiresOptInDirectiveType); + + foreach (var _ in requiresOptInDirectives) + { + errors.Add(RequiresOptInOnRequiredInputField( + inputObjectType, + field)); + } + } + } + + break; + + case IObjectType objectType: + foreach (var field in objectType.Fields) + { + foreach (var argument in field.Arguments) + { + if (argument.Type.IsNonNullType() && argument.DefaultValue is null) + { + var requiresOptInDirectives = argument.Directives + .Where(d => d.Type is RequiresOptInDirectiveType); + + foreach (var _ in requiresOptInDirectives) + { + errors.Add(RequiresOptInOnRequiredArgument( + objectType, + field, + argument)); + } + } + } + } + + break; + } + } + } +} diff --git a/src/HotChocolate/Core/src/Types/Configuration/Validation/SchemaValidator.cs b/src/HotChocolate/Core/src/Types/Configuration/Validation/SchemaValidator.cs index 877f7b5d731..ef7dec99fe8 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/Validation/SchemaValidator.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/Validation/SchemaValidator.cs @@ -12,7 +12,8 @@ internal static class SchemaValidator new DirectiveValidationRule(), new InterfaceHasAtLeastOneImplementationRule(), new IsSelectedPatternValidation(), - new EnsureFieldResultsDeclareErrorsRule() + new EnsureFieldResultsDeclareErrorsRule(), + new RequiresOptInValidationRule() ]; public static IReadOnlyList Validate( diff --git a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs index 78b8062ffe9..85ca1f764b1 100644 --- a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs +++ b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs @@ -185,6 +185,11 @@ public interface IReadOnlySchemaOptions /// bool EnableTag { get; } + /// + /// Specifies that the opt-in features functionality will be enabled. + /// + bool EnableOptInFeatures { get; } + /// /// Specifies the default dependency injection scope for query fields. /// diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs index 6d9cddc987c..71a76a0abf0 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs @@ -1024,6 +1024,24 @@ internal static string ErrorHelper_RequiredFieldCannotBeDeprecated { } } + /// + /// Looks up a localized string similar to The @requiresOptIn directive must not appear on required (non-null without a default) arguments.. + /// + internal static string ErrorHelper_RequiresOptInOnRequiredArgument { + get { + return ResourceManager.GetString("ErrorHelper_RequiresOptInOnRequiredArgument", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The @requiresOptIn directive must not appear on required (non-null without a default) input object field definitions.. + /// + internal static string ErrorHelper_RequiresOptInOnRequiredInputField { + get { + return ResourceManager.GetString("ErrorHelper_RequiresOptInOnRequiredInputField", resourceCulture); + } + } + /// /// Looks up a localized string similar to Field names starting with `__` are reserved for the GraphQL specification.. /// @@ -1514,6 +1532,42 @@ internal static string OneOfDirectiveType_Description { } } + /// + /// Looks up a localized string similar to An OptInFeatureStability object describes the stability level of an opt-in feature.. + /// + internal static string OptInFeatureStability_Description { + get { + return ResourceManager.GetString("OptInFeatureStability_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The name of the feature for which to set the stability.. + /// + internal static string OptInFeatureStabilityDirectiveType_FeatureDescription { + get { + return ResourceManager.GetString("OptInFeatureStabilityDirectiveType_FeatureDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The stability level of the feature.. + /// + internal static string OptInFeatureStabilityDirectiveType_StabilityDescription { + get { + return ResourceManager.GetString("OptInFeatureStabilityDirectiveType_StabilityDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sets the stability level of an opt-in feature.. + /// + internal static string OptInFeatureStabilityDirectiveType_TypeDescription { + get { + return ResourceManager.GetString("OptInFeatureStabilityDirectiveType_TypeDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to The member expression must specify a property or method that is public and that belongs to the type {0}. /// @@ -1595,6 +1649,33 @@ internal static string Relay_NodesField_Ids_Description { } } + /// + /// Looks up a localized string similar to RequiresOptIn is not supported on the specified descriptor.. + /// + internal static string RequiresOptInDirective_Descriptor_NotSupported { + get { + return ResourceManager.GetString("RequiresOptInDirective_Descriptor_NotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The name of the feature that requires opt in.. + /// + internal static string RequiresOptInDirectiveType_FeatureDescription { + get { + return ResourceManager.GetString("RequiresOptInDirectiveType_FeatureDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Indicates that the given field, argument, input field, or enum value requires giving explicit consent before being used.. + /// + internal static string RequiresOptInDirectiveType_TypeDescription { + get { + return ResourceManager.GetString("RequiresOptInDirectiveType_TypeDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to A directive type mustn't be one of the base classes `DirectiveType` or `DirectiveType<T>` but must be a type inheriting from `DirectiveType` or `DirectiveType<T>`.. /// diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx index 00b1dc464ba..5e0865c4390 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx @@ -1003,4 +1003,31 @@ Type: `{0}` Cycle in object graph detected. + + Indicates that the given field, argument, input field, or enum value requires giving explicit consent before being used. + + + The name of the feature that requires opt in. + + + RequiresOptIn is not supported on the specified descriptor. + + + Sets the stability level of an opt-in feature. + + + The name of the feature for which to set the stability. + + + The stability level of the feature. + + + An OptInFeatureStability object describes the stability level of an opt-in feature. + + + The @requiresOptIn directive must not appear on required (non-null without a default) input object field definitions. + + + The @requiresOptIn directive must not appear on required (non-null without a default) arguments. + diff --git a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs index e538d629c02..7e92ee5b991 100644 --- a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs +++ b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs @@ -37,6 +37,7 @@ public partial class SchemaBuilder : ISchemaBuilder typeof(InterfaceCompletionTypeInterceptor), typeof(MiddlewareValidationTypeInterceptor), typeof(EnableTrueNullabilityTypeInterceptor), + typeof(OptInFeaturesTypeInterceptor) ]; private SchemaOptions _options = new(); diff --git a/src/HotChocolate/Core/src/Types/SchemaOptions.cs b/src/HotChocolate/Core/src/Types/SchemaOptions.cs index 3bf03f731da..3d032360063 100644 --- a/src/HotChocolate/Core/src/Types/SchemaOptions.cs +++ b/src/HotChocolate/Core/src/Types/SchemaOptions.cs @@ -215,6 +215,11 @@ public FieldBindingFlags DefaultFieldBindingFlags /// public bool EnableTag { get; set; } = true; + /// + /// Specifies that the opt-in features functionality will be enabled. + /// + public bool EnableOptInFeatures { get; set; } + /// /// Defines the default dependency injection scope for query fields. /// diff --git a/src/HotChocolate/Core/src/Types/Types/Directives/Directives.cs b/src/HotChocolate/Core/src/Types/Types/Directives/Directives.cs index 716b0259114..6ad7ec851c8 100644 --- a/src/HotChocolate/Core/src/Types/Types/Directives/Directives.cs +++ b/src/HotChocolate/Core/src/Types/Types/Directives/Directives.cs @@ -43,6 +43,15 @@ internal static IReadOnlyList CreateReferences( directiveTypes.Add(typeInspector.GetTypeRef(typeof(Tag))); } + if (descriptorContext.Options.EnableOptInFeatures) + { + directiveTypes.Add( + typeInspector.GetTypeRef(typeof(OptInFeatureStabilityDirectiveType))); + + directiveTypes.Add( + typeInspector.GetTypeRef(typeof(RequiresOptInDirectiveType))); + } + directiveTypes.Add(typeInspector.GetTypeRef(typeof(SkipDirectiveType))); directiveTypes.Add(typeInspector.GetTypeRef(typeof(IncludeDirectiveType))); directiveTypes.Add(typeInspector.GetTypeRef(typeof(DeprecatedDirectiveType))); diff --git a/src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirective.cs b/src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirective.cs new file mode 100644 index 00000000000..424832df393 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirective.cs @@ -0,0 +1,37 @@ +namespace HotChocolate.Types; + +public sealed class OptInFeatureStabilityDirective +{ + /// + /// Creates a new instance of . + /// + /// + /// The name of the feature for which to set the stability. + /// + /// + /// The stability level of the feature. + /// + /// + /// is null. + /// + /// + /// is null. + /// + public OptInFeatureStabilityDirective(string feature, string stability) + { + Feature = feature ?? throw new ArgumentNullException(nameof(feature)); + Stability = stability ?? throw new ArgumentNullException(nameof(stability)); + } + + /// + /// The name of the feature for which to set the stability. + /// + [GraphQLDescription("The name of the feature for which to set the stability.")] + public string Feature { get; } + + /// + /// The stability level of the feature. + /// + [GraphQLDescription("The stability level of the feature.")] + public string Stability { get; } +} diff --git a/src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirectiveExtensions.cs b/src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirectiveExtensions.cs new file mode 100644 index 00000000000..8755d6c4e55 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirectiveExtensions.cs @@ -0,0 +1,12 @@ +namespace HotChocolate.Types; + +public static class OptInFeatureStabilityDirectiveExtensions +{ + public static ISchemaTypeDescriptor OptInFeatureStability( + this ISchemaTypeDescriptor descriptor, + string feature, + string stability) + { + return descriptor.Directive(new OptInFeatureStabilityDirective(feature, stability)); + } +} diff --git a/src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirectiveType.cs b/src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirectiveType.cs new file mode 100644 index 00000000000..f29157e7755 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirectiveType.cs @@ -0,0 +1,32 @@ +using HotChocolate.Properties; + +namespace HotChocolate.Types; + +/// +/// Sets the stability level of an opt-in feature. +/// +public sealed class OptInFeatureStabilityDirectiveType + : DirectiveType +{ + protected override void Configure( + IDirectiveTypeDescriptor descriptor) + { + descriptor + .Name(WellKnownDirectives.OptInFeatureStability) + .Description(TypeResources.OptInFeatureStabilityDirectiveType_TypeDescription) + .Location(DirectiveLocation.Schema) + .Repeatable(); + + descriptor + .Argument(t => t.Feature) + .Name(WellKnownDirectives.OptInFeatureStabilityFeatureArgument) + .Description(TypeResources.OptInFeatureStabilityDirectiveType_FeatureDescription) + .Type>(); + + descriptor + .Argument(t => t.Stability) + .Name(WellKnownDirectives.OptInFeatureStabilityStabilityArgument) + .Description(TypeResources.OptInFeatureStabilityDirectiveType_StabilityDescription) + .Type>(); + } +} diff --git a/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInAttribute.cs b/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInAttribute.cs new file mode 100644 index 00000000000..bfc3d37ef77 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInAttribute.cs @@ -0,0 +1,66 @@ +using System.Reflection; +using HotChocolate.Properties; +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Types; + +/// +/// Indicates that the given field, argument, input field, or enum value requires giving explicit +/// consent before being used. +/// +[AttributeUsage( + AttributeTargets.Field // Required for enum values + | AttributeTargets.Method + | AttributeTargets.Parameter + | AttributeTargets.Property, + AllowMultiple = true)] +public sealed class RequiresOptInAttribute : DescriptorAttribute +{ + /// + /// Initializes a new instance of the + /// with a specific feature name and stability. + /// + /// The name of the feature that requires opt in. + public RequiresOptInAttribute(string feature) + { + Feature = feature ?? throw new ArgumentNullException(nameof(feature)); + } + + /// + /// The name of the feature that requires opt in. + /// + public string Feature { get; } + + protected internal override void TryConfigure( + IDescriptorContext context, + IDescriptor descriptor, + ICustomAttributeProvider element) + { + switch (descriptor) + { + case IObjectFieldDescriptor desc: + desc.RequiresOptIn(Feature); + break; + + case IInputFieldDescriptor desc: + desc.RequiresOptIn(Feature); + break; + + case IArgumentDescriptor desc: + desc.RequiresOptIn(Feature); + break; + + case IEnumValueDescriptor desc: + desc.RequiresOptIn(Feature); + break; + + default: + throw new SchemaException( + SchemaErrorBuilder.New() + .SetMessage(TypeResources.RequiresOptInDirective_Descriptor_NotSupported) + .SetExtension("member", element) + .SetExtension("descriptor", descriptor) + .Build()); + } + } +} diff --git a/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirective.cs b/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirective.cs new file mode 100644 index 00000000000..cd032b75355 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirective.cs @@ -0,0 +1,60 @@ +#nullable enable + +namespace HotChocolate.Types; + +/// +/// Indicates that the given field, argument, input field, or enum value requires giving explicit +/// consent before being used. +/// +/// +/// type Session { +/// id: ID! +/// title: String! +/// # [...] +/// startInstant: Instant @requiresOptIn(feature: "experimentalInstantApi") +/// endInstant: Instant @requiresOptIn(feature: "experimentalInstantApi") +/// } +/// +/// +[DirectiveType( + WellKnownDirectives.RequiresOptIn, + DirectiveLocation.ArgumentDefinition | + DirectiveLocation.EnumValue | + DirectiveLocation.FieldDefinition | + DirectiveLocation.InputFieldDefinition, + IsRepeatable = true)] +[GraphQLDescription( + """ + Indicates that the given field, argument, input field, or enum value requires giving explicit + consent before being used. + + type Session { + id: ID! + title: String! + # [...] + startInstant: Instant @requiresOptIn(feature: "experimentalInstantApi") + endInstant: Instant @requiresOptIn(feature: "experimentalInstantApi") + } + """)] +public sealed class RequiresOptInDirective +{ + /// + /// Creates a new instance of . + /// + /// + /// The name of the feature that requires opt in. + /// + /// + /// is null. + /// + public RequiresOptInDirective(string feature) + { + Feature = feature ?? throw new ArgumentNullException(nameof(feature)); + } + + /// + /// The name of the feature that requires opt in. + /// + [GraphQLDescription("The name of the feature that requires opt in.")] + public string Feature { get; } +} diff --git a/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirectiveExtensions.cs b/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirectiveExtensions.cs new file mode 100644 index 00000000000..a1540bf7ef6 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirectiveExtensions.cs @@ -0,0 +1,147 @@ +using HotChocolate.Properties; + +namespace HotChocolate.Types; + +public static class RequiresOptInDirectiveExtensions +{ + /// + /// Adds an @requiresOptIn directive to an . + /// + /// type Book { + /// id: ID! + /// title: String! + /// author: String @requiresOptIn(feature: "your-feature") + /// } + /// + /// + /// + /// The on which this directive shall be annotated. + /// + /// + /// The name of the feature that requires opt in. + /// + /// + /// Returns the on which this directive + /// was applied for configuration chaining. + /// + public static IObjectFieldDescriptor RequiresOptIn( + this IObjectFieldDescriptor descriptor, + string feature) + { + ApplyRequiresOptIn(descriptor, feature); + return descriptor; + } + + /// + /// Adds an @requiresOptIn directive to an . + /// + /// input BookInput { + /// title: String! + /// author: String! + /// publishedDate: String @requiresOptIn(feature: "your-feature") + /// } + /// + /// + /// + /// The on which this directive shall be annotated. + /// + /// + /// The name of the feature that requires opt in. + /// + /// + /// Returns the on which this directive + /// was applied for configuration chaining. + /// + public static IInputFieldDescriptor RequiresOptIn( + this IInputFieldDescriptor descriptor, + string feature) + { + ApplyRequiresOptIn(descriptor, feature); + return descriptor; + } + + /// + /// Adds an @requiresOptIn directive to an . + /// + /// type Query { + /// books(search: String @requiresOptIn(feature: "your-feature")): [Book] + /// } + /// + /// + /// + /// The on which this directive shall be annotated. + /// + /// + /// The name of the feature that requires opt in. + /// + /// + /// Returns the on which this directive + /// was applied for configuration chaining. + /// + public static IArgumentDescriptor RequiresOptIn( + this IArgumentDescriptor descriptor, + string feature) + { + ApplyRequiresOptIn(descriptor, feature); + return descriptor; + } + + /// + /// Adds an @requiresOptIn directive to an . + /// + /// enum Episode { + /// NEWHOPE @requiresOptIn(feature: "your-feature") + /// EMPIRE + /// JEDI + /// } + /// + /// + /// + /// The on which this directive shall be annotated. + /// + /// + /// The name of the feature that requires opt in. + /// + /// + /// Returns the on which this directive + /// was applied for configuration chaining. + /// + public static IEnumValueDescriptor RequiresOptIn( + this IEnumValueDescriptor descriptor, + string feature) + { + ApplyRequiresOptIn(descriptor, feature); + return descriptor; + } + + private static void ApplyRequiresOptIn(this IDescriptor descriptor, string feature) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + switch (descriptor) + { + case IObjectFieldDescriptor desc: + desc.Directive(new RequiresOptInDirective(feature)); + break; + + case IInputFieldDescriptor desc: + desc.Directive(new RequiresOptInDirective(feature)); + break; + + case IArgumentDescriptor desc: + desc.Directive(new RequiresOptInDirective(feature)); + break; + + case IEnumValueDescriptor desc: + desc.Directive(new RequiresOptInDirective(feature)); + break; + + default: + throw new NotSupportedException( + TypeResources.RequiresOptInDirective_Descriptor_NotSupported); + } + } +} diff --git a/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirectiveType.cs b/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirectiveType.cs new file mode 100644 index 00000000000..1a06b993706 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirectiveType.cs @@ -0,0 +1,38 @@ +using HotChocolate.Properties; + +namespace HotChocolate.Types; + +/// +/// Indicates that the given field, argument, input field, or enum value requires giving explicit +/// consent before being used. +/// +/// +/// type Session { +/// id: ID! +/// title: String! +/// # [...] +/// startInstant: Instant @requiresOptIn(feature: "experimentalInstantApi") +/// endInstant: Instant @requiresOptIn(feature: "experimentalInstantApi") +/// } +/// +/// +public sealed class RequiresOptInDirectiveType : DirectiveType +{ + protected override void Configure( + IDirectiveTypeDescriptor descriptor) + { + descriptor + .Name(WellKnownDirectives.RequiresOptIn) + .Description(TypeResources.RequiresOptInDirectiveType_TypeDescription) + .Location(DirectiveLocation.ArgumentDefinition) + .Location(DirectiveLocation.EnumValue) + .Location(DirectiveLocation.FieldDefinition) + .Location(DirectiveLocation.InputFieldDefinition); + + descriptor + .Argument(t => t.Feature) + .Name(WellKnownDirectives.RequiresOptInFeatureArgument) + .Description(TypeResources.RequiresOptInDirectiveType_FeatureDescription) + .Type>(); + } +} diff --git a/src/HotChocolate/Core/src/Types/Types/Interceptors/OptInFeaturesTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Types/Interceptors/OptInFeaturesTypeInterceptor.cs new file mode 100644 index 00000000000..8fe9693d11d --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Interceptors/OptInFeaturesTypeInterceptor.cs @@ -0,0 +1,62 @@ +using HotChocolate.Configuration; +using HotChocolate.Types.Descriptors; +using HotChocolate.Types.Descriptors.Definitions; + +namespace HotChocolate.Types.Interceptors; + +internal sealed class OptInFeaturesTypeInterceptor : TypeInterceptor +{ + internal override bool IsEnabled(IDescriptorContext context) + => context.Options.EnableOptInFeatures; + + private readonly OptInFeatures _optInFeatures = []; + + public override void OnBeforeCompleteName( + ITypeCompletionContext completionContext, + DefinitionBase definition) + { + if (definition is SchemaTypeDefinition schema) + { + schema.Features.Set(_optInFeatures); + } + } + + public override void OnBeforeCompleteType( + ITypeCompletionContext completionContext, + DefinitionBase definition) + { + switch (definition) + { + case EnumTypeDefinition enumType: + _optInFeatures.UnionWith(enumType.Values.SelectMany(v => v.GetOptInFeatures())); + + break; + + case InputObjectTypeDefinition inputType: + _optInFeatures.UnionWith(inputType.Fields.SelectMany(f => f.GetOptInFeatures())); + + break; + + case ObjectTypeDefinition objectType: + _optInFeatures.UnionWith(objectType.Fields.SelectMany(f => f.GetOptInFeatures())); + + _optInFeatures.UnionWith(objectType.Fields.SelectMany( + f => f.Arguments.SelectMany(a => a.GetOptInFeatures()))); + + break; + } + } +} + +file static class Extensions +{ + public static IEnumerable GetOptInFeatures(this IHasDirectiveDefinition definition) + { + return definition.Directives + .Select(d => d.Value) + .OfType() + .Select(r => r.Feature); + } +} + +internal sealed class OptInFeatures : SortedSet; diff --git a/src/HotChocolate/Core/src/Types/Types/Introspection/IntrospectionTypes.cs b/src/HotChocolate/Core/src/Types/Types/Introspection/IntrospectionTypes.cs index 7d6752fd4d9..7afc36b295a 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/IntrospectionTypes.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/IntrospectionTypes.cs @@ -42,6 +42,11 @@ internal static IReadOnlyList CreateReferences( types.Add(context.TypeInspector.GetTypeRef(typeof(__DirectiveArgument))); } + if (context.Options.EnableOptInFeatures) + { + types.Add(context.TypeInspector.GetTypeRef(typeof(__OptInFeatureStability))); + } + return types; } diff --git a/src/HotChocolate/Core/src/Types/Types/Introspection/__EnumValue.cs b/src/HotChocolate/Core/src/Types/Types/Introspection/__EnumValue.cs index 4f68af44f7b..efa72fb006e 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__EnumValue.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__EnumValue.cs @@ -17,6 +17,7 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c { var stringType = Create(ScalarNames.String); var nonNullStringType = Parse($"{ScalarNames.String}!"); + var nonNullStringListType = Parse($"[{ScalarNames.String}!]"); var nonNullBooleanType = Parse($"{ScalarNames.Boolean}!"); var appDirectiveListType = Parse($"[{nameof(__AppliedDirective)}!]!"); @@ -44,6 +45,14 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c pureResolver: Resolvers.AppliedDirectives)); } + if (context.DescriptorContext.Options.EnableOptInFeatures) + { + def.Fields.Add(new( + Names.RequiresOptIn, + type: nonNullStringListType, + pureResolver: Resolvers.RequiresOptIn)); + } + return def; } @@ -65,6 +74,11 @@ public static object AppliedDirectives(IResolverContext context) => context.Parent().Directives .Where(t => t.Type.IsPublic) .Select(d => d.AsSyntaxNode()); + + public static object RequiresOptIn(IResolverContext context) + => context.Parent().Directives + .Where(t => t.Type is RequiresOptInDirectiveType) + .Select(d => d.AsValue().Feature); } public static class Names @@ -76,6 +90,7 @@ public static class Names public const string IsDeprecated = "isDeprecated"; public const string DeprecationReason = "deprecationReason"; public const string AppliedDirectives = "appliedDirectives"; + public const string RequiresOptIn = "requiresOptIn"; } } #pragma warning restore IDE1006 // Naming Styles diff --git a/src/HotChocolate/Core/src/Types/Types/Introspection/__Field.cs b/src/HotChocolate/Core/src/Types/Types/Introspection/__Field.cs index 62a860fd98b..5a013b2d433 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__Field.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__Field.cs @@ -18,12 +18,15 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c { var stringType = Create(ScalarNames.String); var nonNullStringType = Parse($"{ScalarNames.String}!"); + var nonNullStringListType = Parse($"[{ScalarNames.String}!]"); var nonNullTypeType = Parse($"{nameof(__Type)}!"); var nonNullBooleanType = Parse($"{ScalarNames.Boolean}!"); var booleanType = Parse($"{ScalarNames.Boolean}"); var argumentListType = Parse($"[{nameof(__InputValue)}!]!"); var directiveListType = Parse($"[{nameof(__AppliedDirective)}!]!"); + var optInFeaturesEnabled = context.DescriptorContext.Options.EnableOptInFeatures; + var def = new ObjectTypeDefinition( Names.__Field, TypeResources.Field_Description, @@ -33,7 +36,12 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c { new(Names.Name, type: nonNullStringType, pureResolver: Resolvers.Name), new(Names.Description, type: stringType, pureResolver: Resolvers.Description), - new(Names.Args, type: argumentListType, pureResolver: Resolvers.Arguments) + new( + Names.Args, + type: argumentListType, + pureResolver: optInFeaturesEnabled + ? Resolvers.ArgumentsWithOptIn + : Resolvers.Arguments) { Arguments = { @@ -61,6 +69,17 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c pureResolver: Resolvers.AppliedDirectives)); } + if (optInFeaturesEnabled) + { + def.Fields.Single(f => f.Name == Names.Args) + .Arguments + .Add(new(Names.IncludeOptIn, type: nonNullStringListType)); + + def.Fields.Add(new(Names.RequiresOptIn, + type: nonNullStringListType, + pureResolver: Resolvers.RequiresOptIn)); + } + return def; } @@ -72,7 +91,21 @@ public static string Name(IResolverContext context) public static string? Description(IResolverContext context) => context.Parent().Description; - public static object Arguments(IResolverContext context) + public static object ArgumentsWithOptIn(IResolverContext context) + { + var includeOptIn = context.ArgumentValue(Names.IncludeOptIn) ?? []; + + // If an argument requires opting into features "f1" and "f2", then `includeOptIn` + // must list at least one of the features in order for the argument to be included. + return Arguments(context).Where( + a => a + .Directives + .Where(d => d.Type is RequiresOptInDirectiveType) + .Select(d => d.AsValue().Feature) + .Any(feature => includeOptIn.Contains(feature))); + } + + public static IEnumerable Arguments(IResolverContext context) { var field = context.Parent(); return context.ArgumentValue(Names.IncludeDeprecated) @@ -94,6 +127,12 @@ public static object AppliedDirectives(IResolverContext context) => .Directives .Where(t => t.Type.IsPublic) .Select(d => d.AsSyntaxNode()); + + public static object RequiresOptIn(IResolverContext context) => + context.Parent() + .Directives + .Where(t => t.Type is RequiresOptInDirectiveType) + .Select(d => d.AsValue().Feature); } public static class Names @@ -108,6 +147,8 @@ public static class Names public const string IncludeDeprecated = "includeDeprecated"; public const string DeprecationReason = "deprecationReason"; public const string AppliedDirectives = "appliedDirectives"; + public const string RequiresOptIn = "requiresOptIn"; + public const string IncludeOptIn = "includeOptIn"; } } #pragma warning restore IDE1006 // Naming Styles diff --git a/src/HotChocolate/Core/src/Types/Types/Introspection/__InputValue.cs b/src/HotChocolate/Core/src/Types/Types/Introspection/__InputValue.cs index eeffec1488d..b21b5f592b2 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__InputValue.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__InputValue.cs @@ -19,6 +19,7 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c { var stringType = Create(ScalarNames.String); var nonNullStringType = Parse($"{ScalarNames.String}!"); + var nonNullStringListType = Parse($"[{ScalarNames.String}!]"); var nonNullTypeType = Parse($"{nameof(__Type)}!"); var nonNullBooleanType = Parse($"{ScalarNames.Boolean}!"); var appDirectiveListType = Parse($"[{nameof(__AppliedDirective)}!]!"); @@ -54,6 +55,14 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c pureResolver: Resolvers.AppliedDirectives)); } + if (context.DescriptorContext.Options.EnableOptInFeatures) + { + def.Fields.Add(new( + Names.RequiresOptIn, + type: nonNullStringListType, + pureResolver: Resolvers.RequiresOptIn)); + } + return def; } @@ -85,6 +94,12 @@ public static object AppliedDirectives(IResolverContext context) .Directives .Where(t => t.Type.IsPublic) .Select(d => d.AsSyntaxNode()); + + public static object RequiresOptIn(IResolverContext context) + => context.Parent() + .Directives + .Where(t => t.Type is RequiresOptInDirectiveType) + .Select(d => d.AsValue().Feature); } public static class Names @@ -98,6 +113,7 @@ public static class Names public const string AppliedDirectives = "appliedDirectives"; public const string IsDeprecated = "isDeprecated"; public const string DeprecationReason = "deprecationReason"; + public const string RequiresOptIn = "requiresOptIn"; } } #pragma warning restore IDE1006 // Naming Styles diff --git a/src/HotChocolate/Core/src/Types/Types/Introspection/__OptInFeatureStability.cs b/src/HotChocolate/Core/src/Types/Types/Introspection/__OptInFeatureStability.cs new file mode 100644 index 00000000000..25ede0fe29f --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__OptInFeatureStability.cs @@ -0,0 +1,58 @@ +#pragma warning disable IDE1006 // Naming Styles +using HotChocolate.Configuration; +using HotChocolate.Language; +using HotChocolate.Properties; +using HotChocolate.Resolvers; +using HotChocolate.Types.Descriptors.Definitions; +using static HotChocolate.Types.Descriptors.TypeReference; + +#nullable enable + +namespace HotChocolate.Types.Introspection; + +/// +/// An OptInFeatureStability object describes the stability level of an opt-in feature. +/// +[Introspection] +// ReSharper disable once InconsistentNaming +internal sealed class __OptInFeatureStability : ObjectType +{ + protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext context) + { + var nonNullStringType = Parse($"{ScalarNames.String}!"); + + return new ObjectTypeDefinition( + Names.__OptInFeatureStability, + TypeResources.OptInFeatureStability_Description, + typeof(DirectiveNode)) + { + Fields = + { + new(Names.Feature, type: nonNullStringType, pureResolver: Resolvers.Feature), + new(Names.Stability, type: nonNullStringType, pureResolver: Resolvers.Stability) + } + }; + } + + private static class Resolvers + { + public static string Feature(IResolverContext context) => + ((StringValueNode)context.Parent() + .Arguments + .Single(a => a.Name.Value == Names.Feature).Value).Value; + + public static string Stability(IResolverContext context) => + ((StringValueNode)context.Parent() + .Arguments + .Single(a => a.Name.Value == Names.Stability).Value).Value; + } + + public static class Names + { + // ReSharper disable once InconsistentNaming + public const string __OptInFeatureStability = "__OptInFeatureStability"; + public const string Feature = "feature"; + public const string Stability = "stability"; + } +} +#pragma warning restore IDE1006 // Naming Styles diff --git a/src/HotChocolate/Core/src/Types/Types/Introspection/__Schema.cs b/src/HotChocolate/Core/src/Types/Types/Introspection/__Schema.cs index d9da3a034e5..6bab69a5070 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__Schema.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__Schema.cs @@ -1,7 +1,9 @@ #pragma warning disable IDE1006 // Naming Styles using HotChocolate.Configuration; +using HotChocolate.Features; using HotChocolate.Resolvers; using HotChocolate.Types.Descriptors.Definitions; +using HotChocolate.Types.Interceptors; using static HotChocolate.Properties.TypeResources; using static HotChocolate.Types.Descriptors.TypeReference; @@ -21,6 +23,8 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c var nonNullTypeType = Parse($"{nameof(__Type)}!"); var directiveListType = Parse($"[{nameof(__Directive)}!]!"); var appDirectiveListType = Parse($"[{nameof(__AppliedDirective)}!]!"); + var nonNullStringListType = Parse($"[{ScalarNames.String}!]"); + var optInFeatureStabilityListType = Parse($"[{nameof(__OptInFeatureStability)}!]!"); var def = new ObjectTypeDefinition(Names.__Schema, Schema_Description, typeof(ISchema)) { @@ -55,6 +59,19 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c pureResolver: Resolvers.AppliedDirectives)); } + if (context.DescriptorContext.Options.EnableOptInFeatures) + { + def.Fields.Add(new( + Names.OptInFeatures, + type: nonNullStringListType, + pureResolver: Resolvers.OptInFeatures)); + + def.Fields.Add(new( + Names.OptInFeatureStability, + type: optInFeatureStabilityListType, + pureResolver: Resolvers.OptInFeatureStability)); + } + return def; } @@ -82,6 +99,14 @@ public static object AppliedDirectives(IResolverContext context) => context.Parent().Directives .Where(t => t.Type.IsPublic) .Select(d => d.AsSyntaxNode()); + + public static object OptInFeatures(IResolverContext context) + => context.Parent().Features.GetRequired(); + + public static object OptInFeatureStability(IResolverContext context) + => context.Parent().Directives + .Where(t => t.Type is OptInFeatureStabilityDirectiveType) + .Select(d => d.AsSyntaxNode()); } public static class Names @@ -95,6 +120,8 @@ public static class Names public const string SubscriptionType = "subscriptionType"; public const string Directives = "directives"; public const string AppliedDirectives = "appliedDirectives"; + public const string OptInFeatures = "optInFeatures"; + public const string OptInFeatureStability = "optInFeatureStability"; } } #pragma warning restore IDE1006 // Naming Styles diff --git a/src/HotChocolate/Core/src/Types/Types/Introspection/__Type.cs b/src/HotChocolate/Core/src/Types/Types/Introspection/__Type.cs index 6e221afd789..7879d57715a 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__Type.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__Type.cs @@ -25,6 +25,9 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c var enumValueListType = Parse($"[{nameof(__EnumValue)}!]"); var inputValueListType = Parse($"[{nameof(__InputValue)}!]"); var directiveListType = Parse($"[{nameof(__AppliedDirective)}!]!"); + var nonNullStringListType = Parse($"[{ScalarNames.String}!]"); + + var optInFeaturesEnabled = context.DescriptorContext.Options.EnableOptInFeatures; var def = new ObjectTypeDefinition( Names.__Type, @@ -36,7 +39,12 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c new(Names.Kind, type: kindType, pureResolver: Resolvers.Kind), new(Names.Name, type: stringType, pureResolver: Resolvers.Name), new(Names.Description, type: stringType, pureResolver: Resolvers.Description), - new(Names.Fields, type: fieldListType, pureResolver: Resolvers.Fields) + new( + Names.Fields, + type: fieldListType, + pureResolver: optInFeaturesEnabled + ? Resolvers.FieldsWithOptIn + : Resolvers.Fields) { Arguments = { @@ -49,7 +57,12 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c }, new(Names.Interfaces, type: typeListType, pureResolver: Resolvers.Interfaces), new(Names.PossibleTypes, type: typeListType, pureResolver: Resolvers.PossibleTypes), - new(Names.EnumValues, type: enumValueListType, pureResolver: Resolvers.EnumValues) + new( + Names.EnumValues, + type: enumValueListType, + pureResolver: optInFeaturesEnabled + ? Resolvers.EnumValuesWithOptIn + : Resolvers.EnumValues) { Arguments = { @@ -62,9 +75,12 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c }, }, }, - new(Names.InputFields, + new( + Names.InputFields, type: inputValueListType, - pureResolver: Resolvers.InputFields) + pureResolver: optInFeaturesEnabled + ? Resolvers.InputFieldsWithOptIn + : Resolvers.InputFields) { Arguments = { @@ -99,6 +115,21 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c pureResolver: Resolvers.AppliedDirectives)); } + if (optInFeaturesEnabled) + { + def.Fields.Single(f => f.Name == Names.EnumValues) + .Arguments + .Add(new(Names.IncludeOptIn, type: nonNullStringListType)); + + def.Fields.Single(f => f.Name == Names.Fields) + .Arguments + .Add(new(Names.IncludeOptIn, type: nonNullStringListType)); + + def.Fields.Single(f => f.Name == Names.InputFields) + .Arguments + .Add(new(Names.IncludeOptIn, type: nonNullStringListType)); + } + return def; } @@ -113,7 +144,35 @@ public static object Kind(IResolverContext context) public static object? Description(IResolverContext context) => context.Parent() is INamedType n ? n.Description : null; - public static object? Fields(IResolverContext context) + public static object? FieldsWithOptIn(IResolverContext context) + { + var type = context.Parent(); + + if (type is IComplexOutputType) + { + var fields = Fields(context); + + if (fields is null) + { + return default; + } + + var includeOptIn = context.ArgumentValue(Names.IncludeOptIn) ?? []; + + // If a field requires opting into features "f1" and "f2", then `includeOptIn` + // must list at least one of the features in order for the field to be included. + return fields.Where( + f => f + .Directives + .Where(d => d.Type is RequiresOptInDirectiveType) + .Select(d => d.AsValue().Feature) + .Any(feature => includeOptIn.Contains(feature))); + } + + return default; + } + + public static IEnumerable? Fields(IResolverContext context) { var type = context.Parent(); var includeDeprecated = context.ArgumentValue(Names.IncludeDeprecated); @@ -140,14 +199,71 @@ public static object Kind(IResolverContext context) : null : null; - public static object? EnumValues(IResolverContext context) + public static object? EnumValuesWithOptIn(IResolverContext context) + { + var type = context.Parent(); + + if (type is EnumType) + { + var enumValues = EnumValues(context); + + if (enumValues is null) + { + return default; + } + + var includeOptIn = context.ArgumentValue(Names.IncludeOptIn) ?? []; + + // If an enum value requires opting into features "f1" and "f2", then `includeOptIn` + // must list at least one of the features in order for the value to be included. + return enumValues.Where( + v => v + .Directives + .Where(d => d.Type is RequiresOptInDirectiveType) + .Select(d => d.AsValue().Feature) + .Any(feature => includeOptIn.Contains(feature))); + } + + return default; + } + + public static IEnumerable? EnumValues(IResolverContext context) => context.Parent() is EnumType et ? context.ArgumentValue(Names.IncludeDeprecated) ? et.Values : et.Values.Where(t => !t.IsDeprecated) : null; - public static object? InputFields(IResolverContext context) + public static object? InputFieldsWithOptIn(IResolverContext context) + { + var type = context.Parent(); + + if (type is IInputObjectType) + { + var inputFields = InputFields(context); + + if (inputFields is null) + { + return default; + } + + var includeOptIn = context.ArgumentValue(Names.IncludeOptIn) ?? []; + + // If an input field requires opting into features "f1" and "f2", then + // `includeOptIn` must list at least one of the features in order for the field to + // be included. + return inputFields.Where( + f => f + .Directives + .Where(d => d.Type is RequiresOptInDirectiveType) + .Select(d => d.AsValue().Feature) + .Any(feature => includeOptIn.Contains(feature))); + } + + return default; + } + + public static IEnumerable? InputFields(IResolverContext context) => context.Parent() is IInputObjectType iot ? context.ArgumentValue(Names.IncludeDeprecated) ? iot.Fields @@ -195,6 +311,7 @@ public static class Names public const string SpecifiedByUrl = "specifiedByURL"; public const string IncludeDeprecated = "includeDeprecated"; public const string AppliedDirectives = "appliedDirectives"; + public const string IncludeOptIn = "includeOptIn"; } } #pragma warning restore IDE1006 // Naming Styles diff --git a/src/HotChocolate/Core/src/Types/Utilities/ErrorHelper.cs b/src/HotChocolate/Core/src/Types/Utilities/ErrorHelper.cs index 04479e33ece..990573d3dcc 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/ErrorHelper.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/ErrorHelper.cs @@ -538,4 +538,24 @@ public static ISchemaError DuplicateFieldName( .SetTypeSystemObject(type) .Build(); } + + public static ISchemaError RequiresOptInOnRequiredInputField( + IInputObjectType type, + IInputField field) + => SchemaErrorBuilder.New() + .SetMessage(ErrorHelper_RequiresOptInOnRequiredInputField) + .SetType(type) + .SetField(field) + .Build(); + + public static ISchemaError RequiresOptInOnRequiredArgument( + IComplexOutputType type, + IOutputField field, + IInputField argument) + => SchemaErrorBuilder.New() + .SetMessage(ErrorHelper_RequiresOptInOnRequiredArgument) + .SetType(type) + .SetField(field) + .SetArgument(argument) + .Build(); } diff --git a/src/HotChocolate/Core/test/Execution.Tests/OptInFeaturesIntrospectionTests.cs b/src/HotChocolate/Core/test/Execution.Tests/OptInFeaturesIntrospectionTests.cs new file mode 100644 index 00000000000..d843d4ad61c --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/OptInFeaturesIntrospectionTests.cs @@ -0,0 +1,562 @@ +using CookieCrumble; +using HotChocolate.Types; + +namespace HotChocolate.Execution; + +public sealed class OptInFeaturesIntrospectionTests +{ + [Fact] + public async Task Execute_IntrospectionOnSchema_MatchesSnapshot() + { + // arrange + const string query = + """ + { + __schema { + optInFeatures + optInFeatureStability { + feature + stability + } + } + } + """; + + var executor = CreateSchema().MakeExecutable(); + + // act + var result = await executor.ExecuteAsync(query); + + // assert + result.MatchInlineSnapshot( + """ + { + "data": { + "__schema": { + "optInFeatures": [ + "enumValueFeature1", + "enumValueFeature2", + "inputFieldFeature1", + "inputFieldFeature2", + "objectFieldArgFeature1", + "objectFieldArgFeature2", + "objectFieldFeature1", + "objectFieldFeature2" + ], + "optInFeatureStability": [ + { + "feature": "enumValueFeature1", + "stability": "draft" + }, + { + "feature": "enumValueFeature2", + "stability": "experimental" + } + ] + } + } + } + """); + } + + [Fact] + public async Task Execute_IntrospectionOnObjectFields_MatchesSnapshot() + { + // arrange + const string query = + """ + { + __type(name: "Query") { + fields(includeOptIn: ["objectFieldFeature1"]) { + requiresOptIn + } + } + } + """; + + var executor = CreateSchema().MakeExecutable(); + + // act + var result = await executor.ExecuteAsync(query); + + // assert + result.MatchInlineSnapshot( + """ + { + "data": { + "__type": { + "fields": [ + { + "requiresOptIn": [ + "objectFieldFeature1", + "objectFieldFeature2" + ] + } + ] + } + } + } + """); + } + + [Fact] + public async Task Execute_IntrospectionOnObjectFieldsFeatureDoesNotExist_MatchesSnapshot() + { + // arrange + const string query = + """ + { + __type(name: "Query") { + fields(includeOptIn: ["objectFieldFeatureDoesNotExist"]) { + requiresOptIn + } + } + } + """; + + var executor = CreateSchema().MakeExecutable(); + + // act + var result = await executor.ExecuteAsync(query); + + // assert + result.MatchInlineSnapshot( + """ + { + "data": { + "__type": { + "fields": [] + } + } + } + """); + } + + [Fact] + public async Task Execute_IntrospectionOnObjectFieldsNoIncludedFeatures_MatchesSnapshot() + { + // arrange + const string query = + """ + { + __type(name: "Query") { + fields { + requiresOptIn + } + } + } + """; + + var executor = CreateSchema().MakeExecutable(); + + // act + var result = await executor.ExecuteAsync(query); + + // assert + result.MatchInlineSnapshot( + """ + { + "data": { + "__type": { + "fields": [] + } + } + } + """); + } + + [Fact] + public async Task Execute_IntrospectionOnArgs_MatchesSnapshot() + { + // arrange + const string query = + """ + { + __type(name: "Query") { + fields(includeOptIn: ["objectFieldFeature1"]) { + args(includeOptIn: ["objectFieldArgFeature1"]) { + requiresOptIn + } + } + } + } + """; + + var executor = CreateSchema().MakeExecutable(); + + // act + var result = await executor.ExecuteAsync(query); + + // assert + result.MatchInlineSnapshot( + """ + { + "data": { + "__type": { + "fields": [ + { + "args": [ + { + "requiresOptIn": [ + "objectFieldArgFeature1", + "objectFieldArgFeature2" + ] + } + ] + } + ] + } + } + } + """); + } + + [Fact] + public async Task Execute_IntrospectionOnArgsFeatureDoesNotExist_MatchesSnapshot() + { + // arrange + const string query = + """ + { + __type(name: "Query") { + fields(includeOptIn: ["objectFieldFeature1"]) { + args(includeOptIn: ["objectFieldArgFeatureDoesNotExist"]) { + requiresOptIn + } + } + } + } + """; + + var executor = CreateSchema().MakeExecutable(); + + // act + var result = await executor.ExecuteAsync(query); + + // assert + result.MatchInlineSnapshot( + """ + { + "data": { + "__type": { + "fields": [ + { + "args": [] + } + ] + } + } + } + """); + } + + [Fact] + public async Task Execute_IntrospectionOnArgsNoIncludedFeatures_MatchesSnapshot() + { + // arrange + const string query = + """ + { + __type(name: "Query") { + fields(includeOptIn: ["objectFieldFeature1"]) { + args { + requiresOptIn + } + } + } + } + """; + + var executor = CreateSchema().MakeExecutable(); + + // act + var result = await executor.ExecuteAsync(query); + + // assert + result.MatchInlineSnapshot( + """ + { + "data": { + "__type": { + "fields": [ + { + "args": [] + } + ] + } + } + } + """); + } + + [Fact] + public async Task Execute_IntrospectionOnInputFields_MatchesSnapshot() + { + // arrange + const string query = + """ + { + __type(name: "ExampleInput") { + inputFields(includeOptIn: ["inputFieldFeature1"]) { + requiresOptIn + } + } + } + """; + + var executor = CreateSchema().MakeExecutable(); + + // act + var result = await executor.ExecuteAsync(query); + + // assert + result.MatchInlineSnapshot( + """ + { + "data": { + "__type": { + "inputFields": [ + { + "requiresOptIn": [ + "inputFieldFeature1", + "inputFieldFeature2" + ] + } + ] + } + } + } + """); + } + + [Fact] + public async Task Execute_IntrospectionOnInputFieldsFeatureDoesNotExist_MatchesSnapshot() + { + // arrange + const string query = + """ + { + __type(name: "ExampleInput") { + inputFields(includeOptIn: ["inputFieldFeatureDoesNotExist"]) { + requiresOptIn + } + } + } + """; + + var executor = CreateSchema().MakeExecutable(); + + // act + var result = await executor.ExecuteAsync(query); + + // assert + result.MatchInlineSnapshot( + """ + { + "data": { + "__type": { + "inputFields": [] + } + } + } + """); + } + + [Fact] + public async Task Execute_IntrospectionOnInputFieldsNoIncludedFeatures_MatchesSnapshot() + { + // arrange + const string query = + """ + { + __type(name: "ExampleInput") { + inputFields { + requiresOptIn + } + } + } + """; + + var executor = CreateSchema().MakeExecutable(); + + // act + var result = await executor.ExecuteAsync(query); + + // assert + result.MatchInlineSnapshot( + """ + { + "data": { + "__type": { + "inputFields": [] + } + } + } + """); + } + + [Fact] + public async Task Execute_IntrospectionOnEnumValues_MatchesSnapshot() + { + // arrange + const string query = + """ + { + __type(name: "ExampleEnum") { + enumValues(includeOptIn: ["enumValueFeature1"]) { + requiresOptIn + } + } + } + """; + + var executor = CreateSchema().MakeExecutable(); + + // act + var result = await executor.ExecuteAsync(query); + + // assert + result.MatchInlineSnapshot( + """ + { + "data": { + "__type": { + "enumValues": [ + { + "requiresOptIn": [ + "enumValueFeature1", + "enumValueFeature2" + ] + } + ] + } + } + } + """); + } + + [Fact] + public async Task Execute_IntrospectionOnEnumValuesFeatureDoesNotExist_MatchesSnapshot() + { + // arrange + const string query = + """ + { + __type(name: "ExampleEnum") { + enumValues(includeOptIn: ["enumValueFeatureDoesNotExist"]) { + requiresOptIn + } + } + } + """; + + var executor = CreateSchema().MakeExecutable(); + + // act + var result = await executor.ExecuteAsync(query); + + // assert + result.MatchInlineSnapshot( + """ + { + "data": { + "__type": { + "enumValues": [] + } + } + } + """); + } + + [Fact] + public async Task Execute_IntrospectionOnEnumValuesNoIncludedFeatures_MatchesSnapshot() + { + // arrange + const string query = + """ + { + __type(name: "ExampleEnum") { + enumValues { + requiresOptIn + } + } + } + """; + + var executor = CreateSchema().MakeExecutable(); + + // act + var result = await executor.ExecuteAsync(query); + + // assert + result.MatchInlineSnapshot( + """ + { + "data": { + "__type": { + "enumValues": [] + } + } + } + """); + } + + private static ISchema CreateSchema() + { + return SchemaBuilder.New() + .SetSchema( + s => s + .OptInFeatureStability("enumValueFeature1", "draft") + .OptInFeatureStability("enumValueFeature2", "experimental")) + .AddQueryType() + .AddType() + .AddType() + .ModifyOptions(o => o.EnableOptInFeatures = true) + .Create(); + } + + private sealed class QueryType : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name(OperationTypeNames.Query); + + descriptor + .Field("field") + .Type() + .Argument( + "argument", + a => a + .Type() + .RequiresOptIn("objectFieldArgFeature1") + .RequiresOptIn("objectFieldArgFeature2")) + .Resolve(() => 1) + .RequiresOptIn("objectFieldFeature1") + .RequiresOptIn("objectFieldFeature2"); + } + } + + private sealed class ExampleInputType : InputObjectType + { + protected override void Configure(IInputObjectTypeDescriptor descriptor) + { + descriptor + .Field("field") + .Type() + .RequiresOptIn("inputFieldFeature1") + .RequiresOptIn("inputFieldFeature2"); + } + } + + private sealed class ExampleEnumType : EnumType + { + protected override void Configure(IEnumTypeDescriptor descriptor) + { + descriptor + .Name("ExampleEnum") + .Value("VALUE") + .RequiresOptIn("enumValueFeature1") + .RequiresOptIn("enumValueFeature2"); + } + } +} diff --git a/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/RequiresOptInValidation.cs b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/RequiresOptInValidation.cs new file mode 100644 index 00000000000..529d5a7d6ab --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/RequiresOptInValidation.cs @@ -0,0 +1,83 @@ +namespace HotChocolate.Configuration.Validation; + +public class RequiresOptInValidation : TypeValidationTestBase +{ + [Fact] + public void Must_Not_Appear_On_Required_Input_Object_Field() + { + ExpectError( + """ + input Input { + field: Int! + @requiresOptIn(feature: "feature1") + @requiresOptIn(feature: "feature2") + } + """); + } + + [Fact] + public void May_Appear_On_Required_With_Default_Input_Object_Field() + { + ExpectValid( + """ + type Query { field: Int } + + input Input { + field: Int! = 1 @requiresOptIn(feature: "feature") + } + """); + } + + [Fact] + public void May_Appear_On_Nullable_Input_Object_Field() + { + ExpectValid( + """ + type Query { field: Int } + + input Input { + field: Int @requiresOptIn(feature: "feature") + } + """); + } + + [Fact] + public void Must_Not_Appear_On_Required_Argument() + { + ExpectError( + """ + type Object { + field( + argument: Int! + @requiresOptIn(feature: "feature1") + @requiresOptIn(feature: "feature2")): Int + } + """); + } + + [Fact] + public void May_Appear_On_Required_With_Default_Argument() + { + ExpectValid( + """ + type Query { field: Int } + + type Object { + field(argument: Int! = 1 @requiresOptIn(feature: "feature")): Int + } + """); + } + + [Fact] + public void May_Appear_On_Nullable_Argument() + { + ExpectValid( + """ + type Query { field: Int } + + type Object { + field(argument: Int @requiresOptIn(feature: "feature")): Int + } + """); + } +} diff --git a/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/TypeValidationTestBase.cs b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/TypeValidationTestBase.cs index 61572230a38..e8351f157a9 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/TypeValidationTestBase.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/TypeValidationTestBase.cs @@ -10,7 +10,11 @@ public static void ExpectValid(string schema) SchemaBuilder.New() .AddDocumentFromString(schema) .Use(_ => _ => default) - .ModifyOptions(o => o.EnableOneOf = true) + .ModifyOptions(o => + { + o.EnableOneOf = true; + o.EnableOptInFeatures = true; + }) .Create(); } @@ -21,7 +25,11 @@ public static void ExpectError(string schema, params Action[] erro SchemaBuilder.New() .AddDocumentFromString(schema) .Use(_ => _ => default) - .ModifyOptions(o => o.EnableOneOf = true) + .ModifyOptions(o => + { + o.EnableOneOf = true; + o.EnableOptInFeatures = true; + }) .Create(); Assert.Fail("Expected error!"); } diff --git a/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/RequiresOptInValidation.Must_Not_Appear_On_Required_Argument.snap b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/RequiresOptInValidation.Must_Not_Appear_On_Required_Argument.snap new file mode 100644 index 00000000000..b81afc16f53 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/RequiresOptInValidation.Must_Not_Appear_On_Required_Argument.snap @@ -0,0 +1,18 @@ +{ + "message": "The @requiresOptIn directive must not appear on required (non-null without a default) arguments.", + "type": "Object", + "extensions": { + "argument": "argument", + "field": "field" + } +} + +{ + "message": "The @requiresOptIn directive must not appear on required (non-null without a default) arguments.", + "type": "Object", + "extensions": { + "argument": "argument", + "field": "field" + } +} + diff --git a/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/RequiresOptInValidation.Must_Not_Appear_On_Required_Input_Object_Field.snap b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/RequiresOptInValidation.Must_Not_Appear_On_Required_Input_Object_Field.snap new file mode 100644 index 00000000000..95853af7257 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/RequiresOptInValidation.Must_Not_Appear_On_Required_Input_Object_Field.snap @@ -0,0 +1,16 @@ +{ + "message": "The @requiresOptIn directive must not appear on required (non-null without a default) input object field definitions.", + "type": "Input", + "extensions": { + "field": "field" + } +} + +{ + "message": "The @requiresOptIn directive must not appear on required (non-null without a default) input object field definitions.", + "type": "Input", + "extensions": { + "field": "field" + } +} + diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Directives/OptInFeatureStabilityDirectiveTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/OptInFeatureStabilityDirectiveTests.cs new file mode 100644 index 00000000000..5a1c5e8f196 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/OptInFeatureStabilityDirectiveTests.cs @@ -0,0 +1,54 @@ +using CookieCrumble; +using HotChocolate.Execution; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Types.Directives; + +public sealed class OptInFeatureStabilityDirectiveTests +{ + [Fact] + public async Task BuildSchemaAsync_CodeFirst_MatchesSnapshot() + { + // arrange & act + var schema = + await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => o.EnableOptInFeatures = true) + .SetSchema(s => s + .OptInFeatureStability("feature1", "stability1") + .OptInFeatureStability("feature2", "stability2")) + .AddQueryType(d => d.Field("field").Type()) + .UseField(_ => _ => default) + .BuildSchemaAsync(); + + // assert + schema.MatchSnapshot(); + } + + [Fact] + public async Task BuildSchemaAsync_SchemaFirst_MatchesSnapshot() + { + // arrange & act + var schema = + await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => o.EnableOptInFeatures = true) + .AddDocumentFromString( + """ + schema + @optInFeatureStability(feature: "feature1", stability: "stability1") + @optInFeatureStability(feature: "feature2", stability: "stability2") { + query: Query + } + + type Query { + field: Int + } + """) + .UseField(_ => _ => default) + .BuildSchemaAsync(); + + // assert + schema.MatchSnapshot(); + } +} diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Directives/RequiresOptInDirectiveTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/RequiresOptInDirectiveTests.cs new file mode 100644 index 00000000000..f9079076254 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/RequiresOptInDirectiveTests.cs @@ -0,0 +1,124 @@ +using CookieCrumble; +using HotChocolate.Execution; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Types.Directives; + +public sealed class RequiresOptInDirectiveTests +{ + [Fact] + public async Task BuildSchemaAsync_CodeFirst_MatchesSnapshot() + { + // arrange & act + var schema = + await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => o.EnableOptInFeatures = true) + .AddQueryType(d => d + .Field("field") + .Type() + .Argument( + "argument", + a => a + .Type() + .RequiresOptIn("objectFieldArgFeature1") + .RequiresOptIn("objectFieldArgFeature2")) + .Resolve(() => 1) + .RequiresOptIn("objectFieldFeature1") + .RequiresOptIn("objectFieldFeature2")) + .AddInputObjectType(d => d + .Name("Input") + .Field("field") + .Type() + .RequiresOptIn("inputFieldFeature1") + .RequiresOptIn("inputFieldFeature2")) + .AddEnumType(d => d + .Name("Enum") + .Value("VALUE") + .RequiresOptIn("enumValueFeature1") + .RequiresOptIn("enumValueFeature2")) + .BuildSchemaAsync(); + + // assert + schema.MatchSnapshot(); + } + + [Fact] + public async Task BuildSchemaAsync_ImplementationFirst_MatchesSnapshot() + { + // arrange & act + var schema = + await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => o.EnableOptInFeatures = true) + .AddQueryType() + .AddInputObjectType() + .AddType() + .BuildSchemaAsync(); + + // assert + schema.MatchSnapshot(); + } + + [Fact] + public async Task BuildSchemaAsync_SchemaFirst_MatchesSnapshot() + { + // arrange & act + var schema = + await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => o.EnableOptInFeatures = true) + .AddDocumentFromString( + """ + type Query { + field( + argument: Int + @requiresOptIn(feature: "objectFieldArgFeature1") + @requiresOptIn(feature: "objectFieldArgFeature2")): Int + @requiresOptIn(feature: "objectFieldFeature1") + @requiresOptIn(feature: "objectFieldFeature2") + } + + input Input { + field: Int + @requiresOptIn(feature: "inputFieldFeature1") + @requiresOptIn(feature: "inputFieldFeature2") + } + + enum Enum { + VALUE + @requiresOptIn(feature: "enumValueFeature1") + @requiresOptIn(feature: "enumValueFeature2") + } + """) + .UseField(_ => _ => default) + .BuildSchemaAsync(); + + // assert + schema.MatchSnapshot(); + } + + public sealed class Query + { + [RequiresOptIn("objectFieldFeature1")] + [RequiresOptIn("objectFieldFeature2")] + public int? GetField( + [RequiresOptIn("objectFieldArgFeature1")] + [RequiresOptIn("objectFieldArgFeature2")] int? argument) + => argument; + } + + public sealed class Input + { + [RequiresOptIn("inputFieldFeature1")] + [RequiresOptIn("inputFieldFeature2")] + public int? Field { get; set; } + } + + public enum Enum + { + [RequiresOptIn("enumValueFeature1")] + [RequiresOptIn("enumValueFeature2")] + Value + } +} diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/OptInFeatureStabilityDirectiveTests.BuildSchemaAsync_CodeFirst_MatchesSnapshot.graphql b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/OptInFeatureStabilityDirectiveTests.BuildSchemaAsync_CodeFirst_MatchesSnapshot.graphql new file mode 100644 index 00000000000..b4543769c00 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/OptInFeatureStabilityDirectiveTests.BuildSchemaAsync_CodeFirst_MatchesSnapshot.graphql @@ -0,0 +1,10 @@ +schema @optInFeatureStability(feature: "feature1", stability: "stability1") @optInFeatureStability(feature: "feature2", stability: "stability2") { + query: Query +} + +type Query { + field: Int +} + +"Sets the stability level of an opt-in feature." +directive @optInFeatureStability("The name of the feature for which to set the stability." feature: String! "The stability level of the feature." stability: String!) repeatable on SCHEMA diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/OptInFeatureStabilityDirectiveTests.BuildSchemaAsync_SchemaFirst_MatchesSnapshot.graphql b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/OptInFeatureStabilityDirectiveTests.BuildSchemaAsync_SchemaFirst_MatchesSnapshot.graphql new file mode 100644 index 00000000000..b4543769c00 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/OptInFeatureStabilityDirectiveTests.BuildSchemaAsync_SchemaFirst_MatchesSnapshot.graphql @@ -0,0 +1,10 @@ +schema @optInFeatureStability(feature: "feature1", stability: "stability1") @optInFeatureStability(feature: "feature2", stability: "stability2") { + query: Query +} + +type Query { + field: Int +} + +"Sets the stability level of an opt-in feature." +directive @optInFeatureStability("The name of the feature for which to set the stability." feature: String! "The stability level of the feature." stability: String!) repeatable on SCHEMA diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/RequiresOptInDirectiveTests.BuildSchemaAsync_CodeFirst_MatchesSnapshot.graphql b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/RequiresOptInDirectiveTests.BuildSchemaAsync_CodeFirst_MatchesSnapshot.graphql new file mode 100644 index 00000000000..83d995a4cbe --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/RequiresOptInDirectiveTests.BuildSchemaAsync_CodeFirst_MatchesSnapshot.graphql @@ -0,0 +1,18 @@ +schema { + query: Query +} + +type Query { + field(argument: Int @requiresOptIn(feature: "objectFieldArgFeature1") @requiresOptIn(feature: "objectFieldArgFeature2")): Int @requiresOptIn(feature: "objectFieldFeature1") @requiresOptIn(feature: "objectFieldFeature2") +} + +input Input { + field: Int @requiresOptIn(feature: "inputFieldFeature1") @requiresOptIn(feature: "inputFieldFeature2") +} + +enum Enum { + VALUE @requiresOptIn(feature: "enumValueFeature1") @requiresOptIn(feature: "enumValueFeature2") +} + +"Indicates that the given field, argument, input field, or enum value requires giving explicit consent before being used." +directive @requiresOptIn("The name of the feature that requires opt in." feature: String!) repeatable on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/RequiresOptInDirectiveTests.BuildSchemaAsync_ImplementationFirst_MatchesSnapshot.graphql b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/RequiresOptInDirectiveTests.BuildSchemaAsync_ImplementationFirst_MatchesSnapshot.graphql new file mode 100644 index 00000000000..83d995a4cbe --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/RequiresOptInDirectiveTests.BuildSchemaAsync_ImplementationFirst_MatchesSnapshot.graphql @@ -0,0 +1,18 @@ +schema { + query: Query +} + +type Query { + field(argument: Int @requiresOptIn(feature: "objectFieldArgFeature1") @requiresOptIn(feature: "objectFieldArgFeature2")): Int @requiresOptIn(feature: "objectFieldFeature1") @requiresOptIn(feature: "objectFieldFeature2") +} + +input Input { + field: Int @requiresOptIn(feature: "inputFieldFeature1") @requiresOptIn(feature: "inputFieldFeature2") +} + +enum Enum { + VALUE @requiresOptIn(feature: "enumValueFeature1") @requiresOptIn(feature: "enumValueFeature2") +} + +"Indicates that the given field, argument, input field, or enum value requires giving explicit consent before being used." +directive @requiresOptIn("The name of the feature that requires opt in." feature: String!) repeatable on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/RequiresOptInDirectiveTests.BuildSchemaAsync_SchemaFirst_MatchesSnapshot.graphql b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/RequiresOptInDirectiveTests.BuildSchemaAsync_SchemaFirst_MatchesSnapshot.graphql new file mode 100644 index 00000000000..83d995a4cbe --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/__snapshots__/RequiresOptInDirectiveTests.BuildSchemaAsync_SchemaFirst_MatchesSnapshot.graphql @@ -0,0 +1,18 @@ +schema { + query: Query +} + +type Query { + field(argument: Int @requiresOptIn(feature: "objectFieldArgFeature1") @requiresOptIn(feature: "objectFieldArgFeature2")): Int @requiresOptIn(feature: "objectFieldFeature1") @requiresOptIn(feature: "objectFieldFeature2") +} + +input Input { + field: Int @requiresOptIn(feature: "inputFieldFeature1") @requiresOptIn(feature: "inputFieldFeature2") +} + +enum Enum { + VALUE @requiresOptIn(feature: "enumValueFeature1") @requiresOptIn(feature: "enumValueFeature2") +} + +"Indicates that the given field, argument, input field, or enum value requires giving explicit consent before being used." +directive @requiresOptIn("The name of the feature that requires opt in." feature: String!) repeatable on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION From f270649844bb24dfc36e222b322f50a5fa0629b1 Mon Sep 17 00:00:00 2001 From: Glen Date: Mon, 28 Oct 2024 16:07:42 +0200 Subject: [PATCH 2/7] Added OptInFeatureStability extension method to IRequestExecutorBuilder --- ...ExecutorBuilderExtensions.OptInFeatures.cs | 35 ++++++++ ...orBuilderExtensions_OptInFeatures.Tests.cs | 82 +++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.OptInFeatures.cs create mode 100644 src/HotChocolate/Core/test/Execution.Tests/DependencyInjection/RequestExecutorBuilderExtensions_OptInFeatures.Tests.cs diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.OptInFeatures.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.OptInFeatures.cs new file mode 100644 index 00000000000..bebae815d10 --- /dev/null +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.OptInFeatures.cs @@ -0,0 +1,35 @@ +using HotChocolate; +using HotChocolate.Execution.Configuration; +using HotChocolate.Types; + +namespace Microsoft.Extensions.DependencyInjection; + +public static partial class RequestExecutorBuilderExtensions +{ + public static IRequestExecutorBuilder OptInFeatureStability( + this IRequestExecutorBuilder builder, + string feature, + string stability) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (feature is null) + { + throw new ArgumentNullException(nameof(feature)); + } + + if (stability is null) + { + throw new ArgumentNullException(nameof(stability)); + } + + return Configure( + builder, + options => options.OnConfigureSchemaServicesHooks.Add( + (ctx, _) => ctx.SchemaBuilder.AddSchemaConfiguration( + d => d.Directive(new OptInFeatureStabilityDirective(feature, stability))))); + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/DependencyInjection/RequestExecutorBuilderExtensions_OptInFeatures.Tests.cs b/src/HotChocolate/Core/test/Execution.Tests/DependencyInjection/RequestExecutorBuilderExtensions_OptInFeatures.Tests.cs new file mode 100644 index 00000000000..22cf6cea965 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/DependencyInjection/RequestExecutorBuilderExtensions_OptInFeatures.Tests.cs @@ -0,0 +1,82 @@ +using CookieCrumble; +using HotChocolate.Types; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Execution.DependencyInjection; + +public class RequestExecutorBuilderExtensionsOptInFeaturesTests +{ + [Fact] + public void OptInFeatureStability_NullBuilder_ThrowsArgumentNullException() + { + void Fail() => RequestExecutorBuilderExtensions + .OptInFeatureStability(null!, "feature", "stability"); + + Assert.Throws(Fail); + } + + [Fact] + public void OptInFeatureStability_NullFeature_ThrowsArgumentNullException() + { + void Fail() => new ServiceCollection() + .AddGraphQL() + .OptInFeatureStability(null!, "stability"); + + Assert.Throws(Fail); + } + + [Fact] + public void OptInFeatureStability_NullStability_ThrowsArgumentNullException() + { + void Fail() => new ServiceCollection() + .AddGraphQL() + .OptInFeatureStability("feature", null!); + + Assert.Throws(Fail); + } + + [Fact] + public async Task ExecuteRequestAsync_OptInFeatureStability_MatchesSnapshot() + { + (await new ServiceCollection() + .AddGraphQLServer() + .ModifyOptions(o => o.EnableOptInFeatures = true) + .AddQueryType(d => d.Name("Query").Field("foo").Resolve("bar")) + .OptInFeatureStability("feature1", "stability1") + .OptInFeatureStability("feature2", "stability2") + .ExecuteRequestAsync( + OperationRequestBuilder + .New() + .SetDocument( + """ + { + __schema { + optInFeatureStability { + feature + stability + } + } + } + """) + .Build())) + .MatchInlineSnapshot( + """ + { + "data": { + "__schema": { + "optInFeatureStability": [ + { + "feature": "feature1", + "stability": "stability1" + }, + { + "feature": "feature2", + "stability": "stability2" + } + ] + } + } + } + """); + } +} From f01f4151c77c06dbedcfa268743cf54ff00da231 Mon Sep 17 00:00:00 2001 From: Glen Date: Wed, 30 Oct 2024 10:20:10 +0200 Subject: [PATCH 3/7] Enforced valid GraphQL names for features and stability --- .../Properties/TypeResources.Designer.cs | 27 ++++++++++++++ .../src/Types/Properties/TypeResources.resx | 9 +++++ .../OptInFeatureStabilityDirective.cs | 29 +++++++++++---- .../Directives/RequiresOptInDirective.cs | 16 +++++++-- .../OptInFeatureStabilityDirectiveTests.cs | 36 +++++++++++++++++++ .../Directives/RequiresOptInDirectiveTests.cs | 18 ++++++++++ 6 files changed, 126 insertions(+), 9 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs index 71a76a0abf0..13fdcb407bf 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs @@ -1541,6 +1541,24 @@ internal static string OptInFeatureStability_Description { } } + /// + /// Looks up a localized string similar to The feature name must follow the GraphQL type name rules.. + /// + internal static string OptInFeatureStabilityDirective_FeatureName_NotValid { + get { + return ResourceManager.GetString("OptInFeatureStabilityDirective_FeatureName_NotValid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The stability must follow the GraphQL type name rules.. + /// + internal static string OptInFeatureStabilityDirective_Stability_NotValid { + get { + return ResourceManager.GetString("OptInFeatureStabilityDirective_Stability_NotValid", resourceCulture); + } + } + /// /// Looks up a localized string similar to The name of the feature for which to set the stability.. /// @@ -1658,6 +1676,15 @@ internal static string RequiresOptInDirective_Descriptor_NotSupported { } } + /// + /// Looks up a localized string similar to The feature name must follow the GraphQL type name rules.. + /// + internal static string RequiresOptInDirective_FeatureName_NotValid { + get { + return ResourceManager.GetString("RequiresOptInDirective_FeatureName_NotValid", resourceCulture); + } + } + /// /// Looks up a localized string similar to The name of the feature that requires opt in.. /// diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx index 5e0865c4390..3f32b6c3f1d 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx @@ -1030,4 +1030,13 @@ Type: `{0}` The @requiresOptIn directive must not appear on required (non-null without a default) arguments. + + The feature name must follow the GraphQL type name rules. + + + The feature name must follow the GraphQL type name rules. + + + The stability must follow the GraphQL type name rules. + diff --git a/src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirective.cs b/src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirective.cs index 424832df393..d9b2e3a1292 100644 --- a/src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirective.cs +++ b/src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirective.cs @@ -1,3 +1,6 @@ +using HotChocolate.Properties; +using HotChocolate.Utilities; + namespace HotChocolate.Types; public sealed class OptInFeatureStabilityDirective @@ -11,16 +14,30 @@ public sealed class OptInFeatureStabilityDirective /// /// The stability level of the feature. /// - /// - /// is null. + /// + /// is not a valid name. /// - /// - /// is null. + /// + /// is not a valid name. /// public OptInFeatureStabilityDirective(string feature, string stability) { - Feature = feature ?? throw new ArgumentNullException(nameof(feature)); - Stability = stability ?? throw new ArgumentNullException(nameof(stability)); + if (!feature.IsValidGraphQLName()) + { + throw new ArgumentException( + TypeResources.OptInFeatureStabilityDirective_FeatureName_NotValid, + nameof(feature)); + } + + if (!stability.IsValidGraphQLName()) + { + throw new ArgumentException( + TypeResources.OptInFeatureStabilityDirective_Stability_NotValid, + nameof(stability)); + } + + Feature = feature; + Stability = stability; } /// diff --git a/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirective.cs b/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirective.cs index cd032b75355..ae0e0a5bf88 100644 --- a/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirective.cs +++ b/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirective.cs @@ -1,5 +1,8 @@ #nullable enable +using HotChocolate.Properties; +using HotChocolate.Utilities; + namespace HotChocolate.Types; /// @@ -44,12 +47,19 @@ public sealed class RequiresOptInDirective /// /// The name of the feature that requires opt in. /// - /// - /// is null. + /// + /// is not a valid name. /// public RequiresOptInDirective(string feature) { - Feature = feature ?? throw new ArgumentNullException(nameof(feature)); + if (!feature.IsValidGraphQLName()) + { + throw new ArgumentException( + TypeResources.RequiresOptInDirective_FeatureName_NotValid, + nameof(feature)); + } + + Feature = feature; } /// diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Directives/OptInFeatureStabilityDirectiveTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/OptInFeatureStabilityDirectiveTests.cs index 5a1c5e8f196..88a964e0d5d 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Directives/OptInFeatureStabilityDirectiveTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/OptInFeatureStabilityDirectiveTests.cs @@ -1,3 +1,5 @@ +#nullable enable + using CookieCrumble; using HotChocolate.Execution; using Microsoft.Extensions.DependencyInjection; @@ -6,6 +8,40 @@ namespace HotChocolate.Types.Directives; public sealed class OptInFeatureStabilityDirectiveTests { + [Theory] + [InlineData(null)] + [InlineData("123")] + [InlineData("123abc")] + [InlineData("!abc")] + [InlineData("abc!")] + [InlineData("a b c")] + public void OptInFeatureStabilityDirective_InvalidFeatureName_ThrowsArgumentException( + string? feature) + { + // arrange & act + void Action() => _ = new OptInFeatureStabilityDirective(feature!, "stability"); + + // assert + Assert.Throws(Action); + } + + [Theory] + [InlineData(null)] + [InlineData("123")] + [InlineData("123abc")] + [InlineData("!abc")] + [InlineData("abc!")] + [InlineData("a b c")] + public void OptInFeatureStabilityDirective_InvalidStability_ThrowsArgumentException( + string? stability) + { + // arrange & act + void Action() => _ = new OptInFeatureStabilityDirective("feature", stability!); + + // assert + Assert.Throws(Action); + } + [Fact] public async Task BuildSchemaAsync_CodeFirst_MatchesSnapshot() { diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Directives/RequiresOptInDirectiveTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/RequiresOptInDirectiveTests.cs index f9079076254..9fbec195c0d 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Directives/RequiresOptInDirectiveTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/RequiresOptInDirectiveTests.cs @@ -1,3 +1,5 @@ +#nullable enable + using CookieCrumble; using HotChocolate.Execution; using Microsoft.Extensions.DependencyInjection; @@ -6,6 +8,22 @@ namespace HotChocolate.Types.Directives; public sealed class RequiresOptInDirectiveTests { + [Theory] + [InlineData(null)] + [InlineData("123")] + [InlineData("123abc")] + [InlineData("!abc")] + [InlineData("abc!")] + [InlineData("a b c")] + public void RequiresOptInDirective_InvalidFeatureName_ThrowsArgumentException(string? feature) + { + // arrange & act + void Action() => _ = new RequiresOptInDirective(feature!); + + // assert + Assert.Throws(Action); + } + [Fact] public async Task BuildSchemaAsync_CodeFirst_MatchesSnapshot() { From dc2390d43f3bf26e7208e7ac26537067927519b4 Mon Sep 17 00:00:00 2001 From: Glen Date: Sat, 29 Mar 2025 17:00:13 +0200 Subject: [PATCH 4/7] Fixed error --- .../Types/Types/Interceptors/OptInFeaturesTypeInterceptor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HotChocolate/Core/src/Types/Types/Interceptors/OptInFeaturesTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Types/Interceptors/OptInFeaturesTypeInterceptor.cs index 8fe9693d11d..2c7ffb29c78 100644 --- a/src/HotChocolate/Core/src/Types/Types/Interceptors/OptInFeaturesTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Interceptors/OptInFeaturesTypeInterceptor.cs @@ -6,7 +6,7 @@ namespace HotChocolate.Types.Interceptors; internal sealed class OptInFeaturesTypeInterceptor : TypeInterceptor { - internal override bool IsEnabled(IDescriptorContext context) + public override bool IsEnabled(IDescriptorContext context) => context.Options.EnableOptInFeatures; private readonly OptInFeatures _optInFeatures = []; From dbb62215198bcf745773c4b506a856d3573c152a Mon Sep 17 00:00:00 2001 From: Glen Date: Sat, 30 Aug 2025 17:12:03 +0200 Subject: [PATCH 5/7] Fixed analyzer errors --- .../Types/Types/Directives/RequiresOptInDirective.cs | 10 ++++------ .../Types/Introspection/__OptInFeatureStability.cs | 2 -- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirective.cs b/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirective.cs index cce9176fc84..f2d7343ae8a 100644 --- a/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirective.cs +++ b/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirective.cs @@ -1,5 +1,3 @@ -#nullable enable - using HotChocolate.Properties; using HotChocolate.Utilities; @@ -21,10 +19,10 @@ namespace HotChocolate.Types; /// [DirectiveType( DirectiveNames.RequiresOptIn.Name, - DirectiveLocation.ArgumentDefinition | - DirectiveLocation.EnumValue | - DirectiveLocation.FieldDefinition | - DirectiveLocation.InputFieldDefinition, + DirectiveLocation.ArgumentDefinition + | DirectiveLocation.EnumValue + | DirectiveLocation.FieldDefinition + | DirectiveLocation.InputFieldDefinition, IsRepeatable = true)] [GraphQLDescription( """ diff --git a/src/HotChocolate/Core/src/Types/Types/Introspection/__OptInFeatureStability.cs b/src/HotChocolate/Core/src/Types/Types/Introspection/__OptInFeatureStability.cs index 79b9ed0aac9..b5ec4821124 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__OptInFeatureStability.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__OptInFeatureStability.cs @@ -6,8 +6,6 @@ using HotChocolate.Types.Descriptors.Configurations; using static HotChocolate.Types.Descriptors.TypeReference; -#nullable enable - namespace HotChocolate.Types.Introspection; /// From 5ec99d91a746ddf313650850b9bc15716dea49bb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Oct 2025 21:31:35 +0000 Subject: [PATCH 6/7] Update performance data [skip ci] --- .../benchmarks/k6/performance-data.json | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/HotChocolate/AspNetCore/benchmarks/k6/performance-data.json b/src/HotChocolate/AspNetCore/benchmarks/k6/performance-data.json index ebfb1d0e55e..3d40a76fe26 100644 --- a/src/HotChocolate/AspNetCore/benchmarks/k6/performance-data.json +++ b/src/HotChocolate/AspNetCore/benchmarks/k6/performance-data.json @@ -1,19 +1,19 @@ { - "timestamp": "2025-10-22T21:20:42Z", + "timestamp": "2025-10-22T21:31:35Z", "tests": { "single-fetch": { "name": "Single Fetch (50 products, names only)", "response_time": { - "min": 1.263685, - "p50": 1.559399, - "max": 38.71246, - "avg": 1.7221142851338052, - "p90": 2.0438312000000005, - "p95": 2.4335827999999986, - "p99": 4.718469840000003 + "min": 1.328073, + "p50": 1.72286, + "max": 38.405381, + "avg": 1.941161185091782, + "p90": 2.567787, + "p95": 3.0687131999999995, + "p99": 5.216863400000005 }, "throughput": { - "requests_per_second": 78.77851834360554, + "requests_per_second": 78.7801153650015, "total_iterations": 7168 }, "reliability": { @@ -23,17 +23,17 @@ "dataloader": { "name": "DataLoader (50 products with brands)", "response_time": { - "min": 2.509879, - "p50": 3.0185525, - "max": 15.615809, - "avg": 3.3692411639528275, - "p90": 4.274803899999999, - "p95": 5.492716949999999, - "p99": 8.26760959999999 + "min": 2.531472, + "p50": 3.100122, + "max": 17.59209, + "avg": 3.46240276927398, + "p90": 4.558525, + "p95": 5.796345, + "p99": 8.208663200000004 }, "throughput": { - "requests_per_second": 78.6289930864742, - "total_iterations": 7155 + "requests_per_second": 78.61695952952077, + "total_iterations": 7152 }, "reliability": { "error_rate": 0 From 6ea9e8bdd3da2a0a346bc21a1e2f8f5f0f14a30d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Oct 2025 22:47:50 +0000 Subject: [PATCH 7/7] Update performance data [skip ci] --- .../benchmarks/k6/performance-data.json | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/HotChocolate/AspNetCore/benchmarks/k6/performance-data.json b/src/HotChocolate/AspNetCore/benchmarks/k6/performance-data.json index 3d40a76fe26..ff7648ba812 100644 --- a/src/HotChocolate/AspNetCore/benchmarks/k6/performance-data.json +++ b/src/HotChocolate/AspNetCore/benchmarks/k6/performance-data.json @@ -1,20 +1,20 @@ { - "timestamp": "2025-10-22T21:31:35Z", + "timestamp": "2025-10-22T22:47:50Z", "tests": { "single-fetch": { "name": "Single Fetch (50 products, names only)", "response_time": { - "min": 1.328073, - "p50": 1.72286, - "max": 38.405381, - "avg": 1.941161185091782, - "p90": 2.567787, - "p95": 3.0687131999999995, - "p99": 5.216863400000005 + "min": 1.337373, + "p50": 1.721071, + "max": 41.002125, + "avg": 1.8992366079887915, + "p90": 2.361079, + "p95": 2.7686024999999996, + "p99": 5.0604653399999995 }, "throughput": { - "requests_per_second": 78.7801153650015, - "total_iterations": 7168 + "requests_per_second": 78.76259403790162, + "total_iterations": 7166 }, "reliability": { "error_rate": 0 @@ -23,17 +23,17 @@ "dataloader": { "name": "DataLoader (50 products with brands)", "response_time": { - "min": 2.531472, - "p50": 3.100122, - "max": 17.59209, - "avg": 3.46240276927398, - "p90": 4.558525, - "p95": 5.796345, - "p99": 8.208663200000004 + "min": 2.616372, + "p50": 3.2717275, + "max": 15.397216, + "avg": 3.5678075504213473, + "p90": 4.4970379000000005, + "p95": 5.56147314999998, + "p99": 8.216885829999985 }, "throughput": { - "requests_per_second": 78.61695952952077, - "total_iterations": 7152 + "requests_per_second": 78.58751775514484, + "total_iterations": 7151 }, "reliability": { "error_rate": 0