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..8ddbed3b443 --- /dev/null +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.OptInFeatures.cs @@ -0,0 +1,24 @@ +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) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(feature); + ArgumentNullException.ThrowIfNull(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/src/Types/Configuration/Validation/RequiresOptInValidationRule.cs b/src/HotChocolate/Core/src/Types/Configuration/Validation/RequiresOptInValidationRule.cs new file mode 100644 index 00000000000..d4bbcd1db42 --- /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, + ISchemaDefinition schema, + ICollection errors) + { + if (!context.Options.EnableOptInFeatures) + { + return; + } + + foreach (var type in schema.Types) + { + switch (type) + { + case IInputObjectTypeDefinition inputObjectType: + foreach (var field in inputObjectType.Fields) + { + if (field.Type.IsNonNullType() && field.DefaultValue is null) + { + var requiresOptInDirectives = field.Directives + .Where(d => d.Definition is RequiresOptInDirectiveType); + + foreach (var _ in requiresOptInDirectives) + { + errors.Add(RequiresOptInOnRequiredInputField( + inputObjectType, + field)); + } + } + } + + break; + + case IObjectTypeDefinition 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.Definition 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 7f5414daf9b..608c25c2308 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 365414ac29f..de0613012a9 100644 --- a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs +++ b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs @@ -192,6 +192,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 75d5f80b31f..953350c55a0 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.. /// @@ -1541,6 +1559,60 @@ 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 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.. + /// + 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}. /// @@ -1622,6 +1694,42 @@ 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 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.. + /// + 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 a73b5767ccc..f684c8c800a 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx @@ -997,4 +997,40 @@ Type: `{0}` The specified type kind is not supported. + + 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. + + + 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/SchemaBuilder.cs b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs index 2b6918797f4..90a444045c4 100644 --- a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs +++ b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs @@ -38,6 +38,7 @@ private SchemaBuilder() typeInterceptors.TryAdd(new MiddlewareValidationTypeInterceptor()); typeInterceptors.TryAdd(new SemanticNonNullTypeInterceptor()); typeInterceptors.TryAdd(new StoreGlobalPagingOptionsTypeInterceptor()); + typeInterceptors.TryAdd(new OptInFeaturesTypeInterceptor()); Features.Set(typeInterceptors); } diff --git a/src/HotChocolate/Core/src/Types/SchemaOptions.cs b/src/HotChocolate/Core/src/Types/SchemaOptions.cs index adfcdb53cd8..7fa031cad7a 100644 --- a/src/HotChocolate/Core/src/Types/SchemaOptions.cs +++ b/src/HotChocolate/Core/src/Types/SchemaOptions.cs @@ -126,6 +126,9 @@ public FieldBindingFlags DefaultFieldBindingFlags /// public bool EnableTag { get; set; } = true; + /// + public bool EnableOptInFeatures { get; set; } + /// public DependencyInjectionScope DefaultQueryDependencyInjectionScope { get; set; } = DependencyInjectionScope.Resolver; diff --git a/src/HotChocolate/Core/src/Types/Types/Directives/Directives.cs b/src/HotChocolate/Core/src/Types/Types/Directives/Directives.cs index c16e8979216..8f0454630b0 100644 --- a/src/HotChocolate/Core/src/Types/Types/Directives/Directives.cs +++ b/src/HotChocolate/Core/src/Types/Types/Directives/Directives.cs @@ -49,6 +49,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..d9b2e3a1292 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Directives/OptInFeatureStabilityDirective.cs @@ -0,0 +1,54 @@ +using HotChocolate.Properties; +using HotChocolate.Utilities; + +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 not a valid name. + /// + /// + /// is not a valid name. + /// + public OptInFeatureStabilityDirective(string feature, string 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; + } + + /// + /// 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..b45994714d1 --- /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(DirectiveNames.OptInFeatureStability.Name) + .Description(TypeResources.OptInFeatureStabilityDirectiveType_TypeDescription) + .Location(DirectiveLocation.Schema) + .Repeatable(); + + descriptor + .Argument(t => t.Feature) + .Name(DirectiveNames.OptInFeatureStability.Arguments.Feature) + .Description(TypeResources.OptInFeatureStabilityDirectiveType_FeatureDescription) + .Type>(); + + descriptor + .Argument(t => t.Stability) + .Name(DirectiveNames.OptInFeatureStability.Arguments.Stability) + .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..cce9176fc84 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirective.cs @@ -0,0 +1,70 @@ +#nullable enable + +using HotChocolate.Properties; +using HotChocolate.Utilities; + +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( + DirectiveNames.RequiresOptIn.Name, + 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 not a valid name. + /// + public RequiresOptInDirective(string feature) + { + if (!feature.IsValidGraphQLName()) + { + throw new ArgumentException( + TypeResources.RequiresOptInDirective_FeatureName_NotValid, + nameof(feature)); + } + + Feature = 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..28c7a4dbfaa --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Directives/RequiresOptInDirectiveExtensions.cs @@ -0,0 +1,144 @@ +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) + { + ArgumentNullException.ThrowIfNull(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..b79a4efec1a --- /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(DirectiveNames.RequiresOptIn.Name) + .Description(TypeResources.RequiresOptInDirectiveType_TypeDescription) + .Location(DirectiveLocation.ArgumentDefinition) + .Location(DirectiveLocation.EnumValue) + .Location(DirectiveLocation.FieldDefinition) + .Location(DirectiveLocation.InputFieldDefinition); + + descriptor + .Argument(t => t.Feature) + .Name(DirectiveNames.RequiresOptIn.Arguments.Feature) + .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..d3cec228b31 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Interceptors/OptInFeaturesTypeInterceptor.cs @@ -0,0 +1,63 @@ +using HotChocolate.Configuration; +using HotChocolate.Types.Descriptors; +using HotChocolate.Types.Descriptors.Configurations; + +namespace HotChocolate.Types.Interceptors; + +internal sealed class OptInFeaturesTypeInterceptor : TypeInterceptor +{ + public override bool IsEnabled(IDescriptorContext context) + => context.Options.EnableOptInFeatures; + + private readonly OptInFeatures _optInFeatures = []; + + public override void OnBeforeCompleteName( + ITypeCompletionContext completionContext, + TypeSystemConfiguration configuration) + { + if (configuration is SchemaTypeConfiguration schema) + { + schema.Features.Set(_optInFeatures); + } + } + + public override void OnBeforeCompleteType( + ITypeCompletionContext completionContext, + TypeSystemConfiguration configuration) + { + switch (configuration) + { + case EnumTypeConfiguration enumType: + _optInFeatures.UnionWith(enumType.Values.SelectMany(v => v.GetOptInFeatures())); + + break; + + case InputObjectTypeConfiguration inputType: + _optInFeatures.UnionWith(inputType.Fields.SelectMany(f => f.GetOptInFeatures())); + + break; + + case ObjectTypeConfiguration 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 IDirectiveConfigurationProvider configuration) + { + return configuration.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 36949192026..9e6854ff5d1 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 0ab464d48e0..50b97f83f82 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 ObjectTypeConfiguration CreateConfiguration(ITypeDiscoveryCon { 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 ObjectTypeConfiguration CreateConfiguration(ITypeDiscoveryCon 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.ToSyntaxNode()); + + public static object RequiresOptIn(IResolverContext context) + => context.Parent().Directives + .Where(t => t.Definition is RequiresOptInDirectiveType) + .Select(d => d.ToValue().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 3abe67ce297..a8b9f129c9c 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__Field.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__Field.cs @@ -19,11 +19,14 @@ protected override ObjectTypeConfiguration CreateConfiguration(ITypeDiscoveryCon { 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 argumentListType = Parse($"[{nameof(__InputValue)}!]!"); var directiveListType = Parse($"[{nameof(__AppliedDirective)}!]!"); + var optInFeaturesEnabled = context.DescriptorContext.Options.EnableOptInFeatures; + var def = new ObjectTypeConfiguration( Names.__Field, TypeResources.Field_Description, @@ -33,7 +36,12 @@ protected override ObjectTypeConfiguration CreateConfiguration(ITypeDiscoveryCon { 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 ObjectTypeConfiguration CreateConfiguration(ITypeDiscoveryCon 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.Definition is RequiresOptInDirectiveType) + .Select(d => d.ToValue().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 => Unsafe.As(t.Definition).IsPublic) .Select(d => d.ToSyntaxNode()); + + public static object RequiresOptIn(IResolverContext context) => + context.Parent() + .Directives + .Where(t => t.Definition is RequiresOptInDirectiveType) + .Select(d => d.ToValue().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 ad9e3b0f561..ef174a1af13 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__InputValue.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__InputValue.cs @@ -20,6 +20,7 @@ protected override ObjectTypeConfiguration CreateConfiguration(ITypeDiscoveryCon { 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)}!]!"); @@ -55,6 +56,14 @@ protected override ObjectTypeConfiguration CreateConfiguration(ITypeDiscoveryCon pureResolver: Resolvers.AppliedDirectives)); } + if (context.DescriptorContext.Options.EnableOptInFeatures) + { + def.Fields.Add(new( + Names.RequiresOptIn, + type: nonNullStringListType, + pureResolver: Resolvers.RequiresOptIn)); + } + return def; } @@ -86,6 +95,12 @@ public static object AppliedDirectives(IResolverContext context) .Directives .Where(t => Unsafe.As(t.Definition).IsPublic) .Select(d => d.ToSyntaxNode()); + + public static object RequiresOptIn(IResolverContext context) + => context.Parent() + .Directives + .Where(t => t.Definition is RequiresOptInDirectiveType) + .Select(d => d.ToValue().Feature); } public static class Names @@ -99,6 +114,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..79b9ed0aac9 --- /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.Configurations; +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 ObjectTypeConfiguration CreateConfiguration(ITypeDiscoveryContext context) + { + var nonNullStringType = Parse($"{ScalarNames.String}!"); + + return new ObjectTypeConfiguration( + 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 80c18d027f0..675e8bb344f 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__Schema.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__Schema.cs @@ -1,8 +1,10 @@ #pragma warning disable IDE1006 // Naming Styles using System.Runtime.CompilerServices; using HotChocolate.Configuration; +using HotChocolate.Features; using HotChocolate.Resolvers; using HotChocolate.Types.Descriptors.Configurations; +using HotChocolate.Types.Interceptors; using static HotChocolate.Properties.TypeResources; using static HotChocolate.Types.Descriptors.TypeReference; @@ -22,6 +24,8 @@ protected override ObjectTypeConfiguration CreateConfiguration(ITypeDiscoveryCon 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 ObjectTypeConfiguration(Names.__Schema, Schema_Description, typeof(ISchemaDefinition)) { @@ -56,6 +60,19 @@ protected override ObjectTypeConfiguration CreateConfiguration(ITypeDiscoveryCon 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; } @@ -85,6 +102,14 @@ public static object AppliedDirectives(IResolverContext context) => context.Parent().Directives .Where(t => Unsafe.As(t).IsPublic) .Select(d => d.ToSyntaxNode()); + + public static object OptInFeatures(IResolverContext context) + => context.Parent().Features.GetRequired(); + + public static object OptInFeatureStability(IResolverContext context) + => context.Parent().Directives + .Where(t => t.Definition is OptInFeatureStabilityDirectiveType) + .Select(d => d.ToSyntaxNode()); } public static class Names @@ -98,6 +123,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 9b7516287b2..ca6e114dd45 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__Type.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__Type.cs @@ -27,6 +27,9 @@ protected override ObjectTypeConfiguration CreateConfiguration(ITypeDiscoveryCon 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 ObjectTypeConfiguration( Names.__Type, @@ -38,7 +41,12 @@ protected override ObjectTypeConfiguration CreateConfiguration(ITypeDiscoveryCon 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 = { @@ -51,7 +59,12 @@ protected override ObjectTypeConfiguration CreateConfiguration(ITypeDiscoveryCon }, 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 ObjectTypeConfiguration CreateConfiguration(ITypeDiscoveryCon } } }, - new(Names.InputFields, + new( + Names.InputFields, type: inputValueListType, - pureResolver: Resolvers.InputFields) + pureResolver: optInFeaturesEnabled + ? Resolvers.InputFieldsWithOptIn + : Resolvers.InputFields) { Arguments = { @@ -97,6 +113,21 @@ protected override ObjectTypeConfiguration CreateConfiguration(ITypeDiscoveryCon 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; } @@ -111,7 +142,35 @@ public static object Kind(IResolverContext context) public static object? Description(IResolverContext context) => context.Parent() is ITypeDefinition n ? n.Description : null; - public static object? Fields(IResolverContext context) + public static object? FieldsWithOptIn(IResolverContext context) + { + var type = context.Parent(); + + if (type is IComplexTypeDefinition) + { + 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.Definition is RequiresOptInDirectiveType) + .Select(d => d.ToValue().Feature) + .Any(feature => includeOptIn.Contains(feature))); + } + + return default; + } + + public static IEnumerable? Fields(IResolverContext context) { var type = context.Parent(); var includeDeprecated = context.ArgumentValue(Names.IncludeDeprecated); @@ -138,14 +197,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.Definition is RequiresOptInDirectiveType) + .Select(d => d.ToValue().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 IInputObjectTypeDefinition) + { + 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.Definition is RequiresOptInDirectiveType) + .Select(d => d.ToValue().Feature) + .Any(feature => includeOptIn.Contains(feature))); + } + + return default; + } + + public static IEnumerable? InputFields(IResolverContext context) => context.Parent() is IInputObjectTypeDefinition 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 c3c9d3d871c..c0ff2d53e97 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/ErrorHelper.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/ErrorHelper.cs @@ -556,4 +556,24 @@ public static ISchemaError DuplicateFieldName( .SetTypeSystemObject(type) .Build(); } + + public static ISchemaError RequiresOptInOnRequiredInputField( + IInputObjectTypeDefinition type, + IInputValueDefinition field) + => SchemaErrorBuilder.New() + .SetMessage(ErrorHelper_RequiresOptInOnRequiredInputField) + .SetType(type) + .SetField(field) + .Build(); + + public static ISchemaError RequiresOptInOnRequiredArgument( + IComplexTypeDefinition type, + IOutputFieldDefinition field, + IInputValueDefinition argument) + => SchemaErrorBuilder.New() + .SetMessage(ErrorHelper_RequiresOptInOnRequiredArgument) + .SetType(type) + .SetField(field) + .SetArgument(argument) + .Build(); } 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..430e77f3290 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/DependencyInjection/RequestExecutorBuilderExtensions_OptInFeatures.Tests.cs @@ -0,0 +1,81 @@ +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" + } + ] + } + } + } + """); + } +} 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..1226027846b --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/OptInFeaturesIntrospectionTests.cs @@ -0,0 +1,561 @@ +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 Schema 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 1ae9297cf58..0faa20ffa68 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/TypeValidationTestBase.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/TypeValidationTestBase.cs @@ -9,7 +9,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(); } @@ -20,7 +24,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..f267aef4421 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/OptInFeatureStabilityDirectiveTests.cs @@ -0,0 +1,89 @@ +#nullable enable + +using HotChocolate.Execution; +using Microsoft.Extensions.DependencyInjection; + +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() + { + // 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..41a3b5a9d2b --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/RequiresOptInDirectiveTests.cs @@ -0,0 +1,141 @@ +#nullable enable + +using HotChocolate.Execution; +using Microsoft.Extensions.DependencyInjection; + +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() + { + // 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 diff --git a/src/HotChocolate/Primitives/src/Primitives/Types/WellKnownDirectives.cs b/src/HotChocolate/Primitives/src/Primitives/Types/WellKnownDirectives.cs index 28aa11658c4..01430456cda 100644 --- a/src/HotChocolate/Primitives/src/Primitives/Types/WellKnownDirectives.cs +++ b/src/HotChocolate/Primitives/src/Primitives/Types/WellKnownDirectives.cs @@ -232,4 +232,53 @@ public static class Internal /// public const string Name = "internal"; } + + /// + /// The name constants of the @requiresOptIn directive. + /// + public static class RequiresOptIn + { + /// + /// The name of the @requiresOptIn directive. + /// + public const string Name = "requiresOptIn"; + + /// + /// The argument names of the @requiresOptIn directive. + /// + public static class Arguments + { + /// + /// The name of the @requiresOptIn feature argument. + /// + public const string Feature = "feature"; + } + } + + /// + /// The name constants of the @optInFeatureStability directive. + /// + public static class OptInFeatureStability + { + /// + /// The name of the @optInFeatureStability directive. + /// + public const string Name = "optInFeatureStability"; + + /// + /// The argument names of the @optInFeatureStability directive. + /// + public static class Arguments + { + /// + /// The name of the @optInFeatureStability feature argument. + /// + public const string Feature = "feature"; + + /// + /// The name of the @optInFeatureStability stability argument. + /// + public const string Stability = "stability"; + } + } }