diff --git a/src/HotChocolate/ApolloFederation/src/ApolloFederation/FederationTypeInterceptor.cs b/src/HotChocolate/ApolloFederation/src/ApolloFederation/FederationTypeInterceptor.cs index 24df3b02ae5..915b4495342 100644 --- a/src/HotChocolate/ApolloFederation/src/ApolloFederation/FederationTypeInterceptor.cs +++ b/src/HotChocolate/ApolloFederation/src/ApolloFederation/FederationTypeInterceptor.cs @@ -42,6 +42,7 @@ internal sealed class FederationTypeInterceptor : TypeInterceptor private readonly List _entityTypes = new(); private IDescriptorContext _context = default!; private ITypeInspector _typeInspector = default!; + private ObjectType _queryType = default!; internal override void InitializeContext( IDescriptorContext context, @@ -84,6 +85,17 @@ public override void OnTypesInitialized() } } + internal override void OnAfterResolveRootType( + ITypeCompletionContext completionContext, + ObjectTypeDefinition definition, + OperationType operationType) + { + if (operationType is OperationType.Query) + { + _queryType = (ObjectType) completionContext.Type; + } + } + public override void OnBeforeCompleteType( ITypeCompletionContext completionContext, DefinitionBase definition) @@ -159,7 +171,7 @@ private void AddServiceTypeToQueryType( ITypeCompletionContext completionContext, DefinitionBase? definition) { - if (completionContext.IsQueryType == true && + if (ReferenceEquals(completionContext.Type, _queryType) && definition is ObjectTypeDefinition objectTypeDefinition) { var serviceFieldDescriptor = ObjectFieldDescriptor.New( diff --git a/src/HotChocolate/Caching/src/Caching/CacheControlValidationTypeInterceptor.cs b/src/HotChocolate/Caching/src/Caching/CacheControlValidationTypeInterceptor.cs index 90703b84edc..bf3aa5bc357 100644 --- a/src/HotChocolate/Caching/src/Caching/CacheControlValidationTypeInterceptor.cs +++ b/src/HotChocolate/Caching/src/Caching/CacheControlValidationTypeInterceptor.cs @@ -1,12 +1,28 @@ using HotChocolate.Configuration; +using HotChocolate.Language; using HotChocolate.Types; using HotChocolate.Types.Descriptors.Definitions; +using IHasDirectives = HotChocolate.Types.IHasDirectives; namespace HotChocolate.Caching; internal sealed class CacheControlValidationTypeInterceptor : TypeInterceptor { - public override void OnValidateType(ITypeSystemObjectContext validationContext, + private ITypeCompletionContext _queryContext = default!; + + internal override void OnAfterResolveRootType( + ITypeCompletionContext completionContext, + ObjectTypeDefinition definition, + OperationType operationType) + { + if (operationType is OperationType.Query) + { + _queryContext = completionContext; + } + } + + public override void OnValidateType( + ITypeSystemObjectContext validationContext, DefinitionBase definition) { if (validationContext.IsIntrospectionType) @@ -18,7 +34,7 @@ public override void OnValidateType(ITypeSystemObjectContext validationContext, { case ObjectType objectType: { - var isQueryType = validationContext is ITypeCompletionContext { IsQueryType: true }; + var isQueryType = ReferenceEquals(validationContext, _queryContext); ValidateCacheControlOnType(validationContext, objectType); diff --git a/src/HotChocolate/Core/src/Authorization/AuthorizationTypeInterceptor.cs b/src/HotChocolate/Core/src/Authorization/AuthorizationTypeInterceptor.cs index 52c05369f78..95eb53896ec 100644 --- a/src/HotChocolate/Core/src/Authorization/AuthorizationTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Authorization/AuthorizationTypeInterceptor.cs @@ -31,6 +31,7 @@ internal sealed partial class AuthorizationTypeInterceptor : TypeInterceptor private TypeRegistry _typeRegistry = default!; private TypeLookup _typeLookup = default!; private ExtensionData _schemaContextData = default!; + private ITypeCompletionContext _queryContext = default!; internal override uint Position => uint.MaxValue; @@ -102,13 +103,24 @@ public override void OnBeforeCompleteTypes() FindFieldsAndApplyAuthMiddleware(state); } + internal override void OnAfterResolveRootType( + ITypeCompletionContext completionContext, + ObjectTypeDefinition definition, + OperationType operationType) + { + if (operationType is OperationType.Query) + { + _queryContext = completionContext; + } + } + public override void OnBeforeCompleteType( ITypeCompletionContext completionContext, DefinitionBase definition) { // last in the initialization we need to intercept the query type and ensure that // authorization configuration is applied to the special introspection and node fields. - if ((completionContext.IsQueryType ?? false) && + if (ReferenceEquals(_queryContext, completionContext) && definition is ObjectTypeDefinition typeDef) { var state = _state ?? throw ThrowHelper.StateNotInitialized(); diff --git a/src/HotChocolate/Core/src/Types.Mutations/MutationConventionTypeInterceptor.cs b/src/HotChocolate/Core/src/Types.Mutations/MutationConventionTypeInterceptor.cs index 7298c74438c..e53981671c6 100644 --- a/src/HotChocolate/Core/src/Types.Mutations/MutationConventionTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types.Mutations/MutationConventionTypeInterceptor.cs @@ -55,25 +55,17 @@ public override void OnBeforeRegisterDependencies( public override void OnAfterCompleteTypeNames() => _mutations = _context.ContextData.GetMutationFields(); - internal override void OnAfterResolveRootType( - ITypeCompletionContext completionContext, - DefinitionBase? definition, - OperationType operationType) + internal override void OnBeforeCompleteMutation( + ITypeCompletionContext completionContext, + ObjectTypeDefinition definition) { - // if the type initialization resolved the root type we will capture its definition - // so we can use it after the type extensions are merged into this type. - if (operationType is OperationType.Mutation) - { - _mutationTypeDef = (ObjectTypeDefinition)definition!; - } - // we need to capture a completion context to resolve types. // any context will do. _completionContext ??= completionContext; - } - - public override void OnAfterMergeTypeExtensions() - { + _mutationTypeDef = definition; + + base.OnBeforeCompleteMutation(completionContext, definition); + // if we have found a mutation type we will start applying the mutation conventions // on the mutations. if (_mutationTypeDef is not null) diff --git a/src/HotChocolate/Core/src/Types/Configuration/AggregateTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Configuration/AggregateTypeInterceptor.cs index 3e0011eb866..801067fad03 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/AggregateTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/AggregateTypeInterceptor.cs @@ -259,7 +259,7 @@ public override void OnAfterCompleteName( internal override void OnAfterResolveRootType( ITypeCompletionContext completionContext, - DefinitionBase definition, + ObjectTypeDefinition definition, OperationType operationType) { ref var first = ref GetReference(); @@ -307,6 +307,19 @@ public override void OnAfterMergeTypeExtensions() } } + internal override void OnBeforeCompleteMutation( + ITypeCompletionContext completionContext, + ObjectTypeDefinition definition) + { + ref var first = ref GetReference(); + var length = _typeInterceptors.Length; + + for (var i = 0; i < length; i++) + { + Unsafe.Add(ref first, i).OnBeforeCompleteMutation(completionContext, definition); + } + } + public override void OnBeforeCompleteTypes() { ref var first = ref GetReference(); diff --git a/src/HotChocolate/Core/src/Types/Configuration/Contracts/ITypeCompletionContext.cs b/src/HotChocolate/Core/src/Types/Configuration/Contracts/ITypeCompletionContext.cs index 81d70293d74..e4c1f4709f1 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/Contracts/ITypeCompletionContext.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/Contracts/ITypeCompletionContext.cs @@ -14,21 +14,6 @@ namespace HotChocolate.Configuration; /// public interface ITypeCompletionContext : ITypeSystemObjectContext { - /// - /// Defines if the type that is being completed is the query type. - /// - bool? IsQueryType { get; } - - /// - /// Defines if the type that is being completed is the mutation type. - /// - bool? IsMutationType { get; } - - /// - /// Defines if the type that is being completed is the subscription type. - /// - bool? IsSubscriptionType { get; } - /// /// Global middleware components. /// diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeInitializer.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeInitializer.cs index 2aba7ba2489..f01decee271 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/TypeInitializer.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/TypeInitializer.cs @@ -373,6 +373,15 @@ t.Type is INamedType n && } _interceptor.OnAfterMergeTypeExtensions(); + + var mutationType = _rootTypes.FirstOrDefault(t => t.Kind == OperationType.Mutation); + + if (mutationType.IsInitialized) + { + _interceptor.OnBeforeCompleteMutation( + mutationType.Type, + ((ObjectType)mutationType.Type.Type).Definition!); + } } private void MergeTypeExtension( @@ -658,6 +667,7 @@ public RegisteredRootType( Context = context; Type = type; Kind = kind; + IsInitialized = true; } public ITypeCompletionContext Context { get; } @@ -665,5 +675,7 @@ public RegisteredRootType( public RegisteredType Type { get; } public OperationType Kind { get; } + + public bool IsInitialized { get; } } } diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs index a737996b541..288343a5857 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs @@ -10,6 +10,7 @@ namespace HotChocolate.Configuration; +// note: this type is considered internal and should not be used by users. /// /// A type initialization interceptors can hook into the various initialization events /// of type system members and change / rewrite them. This is useful in order to transform @@ -17,14 +18,14 @@ namespace HotChocolate.Configuration; /// public abstract class TypeInterceptor { - private const uint _position = uint.MaxValue / 2; + private const uint _defaultPosition = uint.MaxValue / 2; /// /// A weight to order interceptors. /// - internal virtual uint Position => _position; + internal virtual uint Position => _defaultPosition; - public virtual bool IsEnabled(IDescriptorContext context) => true; + internal virtual bool IsEnabled(IDescriptorContext context) => true; /// /// This hook is invoked before anything else any allows for additional modification @@ -169,9 +170,10 @@ public virtual void OnAfterCompleteName( internal virtual void OnAfterResolveRootType( ITypeCompletionContext completionContext, - DefinitionBase definition, - OperationType operationType) { } - + ObjectTypeDefinition definition, + OperationType operationType) + { } + public virtual void OnTypesCompletedName() { } /// @@ -183,6 +185,23 @@ public virtual void OnBeforeMergeTypeExtensions() { } /// This method is called after the type extensions are merged. /// public virtual void OnAfterMergeTypeExtensions() { } + + internal virtual void OnBeforeCompleteMutation( + ITypeCompletionContext completionContext, + ObjectTypeDefinition definition) + { + foreach (var field in definition.Fields) + { + OnBeforeCompleteMutationField(completionContext, field); + } + } + + public virtual void OnBeforeCompleteMutationField( + ITypeCompletionContext completionContext, + ObjectFieldDefinition mutationField) + { + } + /// /// This method is called before the types are completed. diff --git a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj index 0463a05d437..8d90ca26fa1 100644 --- a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj +++ b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj @@ -31,6 +31,7 @@ + diff --git a/src/HotChocolate/Core/src/Types/Types/Interceptors/MiddlewareValidationTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Types/Interceptors/MiddlewareValidationTypeInterceptor.cs index 0cc4868f7b0..14d83d6880d 100644 --- a/src/HotChocolate/Core/src/Types/Types/Interceptors/MiddlewareValidationTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Interceptors/MiddlewareValidationTypeInterceptor.cs @@ -177,7 +177,7 @@ void PrintOther() internal sealed class EnableTrueNullabilityTypeInterceptor : TypeInterceptor { - public override bool IsEnabled(IDescriptorContext context) + internal override bool IsEnabled(IDescriptorContext context) => context.Options.EnableTrueNullability; public override void OnBeforeCreateSchema(IDescriptorContext context, ISchemaBuilder schemaBuilder) diff --git a/src/HotChocolate/Core/src/Types/Types/Introspection/IntrospectionTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Types/Introspection/IntrospectionTypeInterceptor.cs index a2bba9a9800..d4a97173311 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/IntrospectionTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/IntrospectionTypeInterceptor.cs @@ -39,12 +39,12 @@ public override void OnAfterCompleteName( internal override void OnAfterResolveRootType( ITypeCompletionContext completionContext, - DefinitionBase definition, + ObjectTypeDefinition definition, OperationType operationType) { - if (operationType is OperationType.Query && definition is ObjectTypeDefinition typeDef) + if (operationType is OperationType.Query) { - _queryTypeDefinition = typeDef; + _queryTypeDefinition = definition; } } diff --git a/src/HotChocolate/Core/src/Types/Types/ObjectType.Initialization.cs b/src/HotChocolate/Core/src/Types/Types/ObjectType.Initialization.cs index 13f819dfad7..893d57b9466 100644 --- a/src/HotChocolate/Core/src/Types/Types/ObjectType.Initialization.cs +++ b/src/HotChocolate/Core/src/Types/Types/ObjectType.Initialization.cs @@ -71,7 +71,7 @@ protected virtual FieldCollection OnCompleteFields( ITypeCompletionContext context, ObjectTypeDefinition definition) { - if (context.IsMutationType ?? false) + if (((RegisteredType)context).IsMutationType ?? false) { // if this type represents the mutation type we flag all fields as serially executable // so that the operation compiler and execution engine will uphold the spec diff --git a/src/HotChocolate/Core/src/Types/Types/Relay/NodeFieldTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Types/Relay/NodeFieldTypeInterceptor.cs index d9f80a17d1b..8c620000801 100644 --- a/src/HotChocolate/Core/src/Types/Types/Relay/NodeFieldTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Relay/NodeFieldTypeInterceptor.cs @@ -27,13 +27,13 @@ internal sealed class NodeFieldTypeInterceptor : TypeInterceptor internal override void OnAfterResolveRootType( ITypeCompletionContext completionContext, - DefinitionBase definition, + ObjectTypeDefinition definition, OperationType operationType) { - if (operationType is OperationType.Query && definition is ObjectTypeDefinition typeDef) + if (operationType is OperationType.Query) { _queryContext = completionContext; - _queryTypeDefinition = typeDef; + _queryTypeDefinition = definition; } } diff --git a/src/HotChocolate/Core/src/Types/Types/Relay/NodeResolverTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Types/Relay/NodeResolverTypeInterceptor.cs index f4fb7414857..edc792cb34a 100644 --- a/src/HotChocolate/Core/src/Types/Types/Relay/NodeResolverTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Relay/NodeResolverTypeInterceptor.cs @@ -42,16 +42,15 @@ TypeDef is not null && internal override void OnAfterResolveRootType( ITypeCompletionContext completionContext, - DefinitionBase definition, + ObjectTypeDefinition definition, OperationType operationType) { // we are only interested in the query type to infer node resolvers. if (operationType is OperationType.Query && - definition is ObjectTypeDefinition typeDef && completionContext.Type is ObjectType queryType) { CompletionContext = completionContext; - TypeDef = typeDef; + TypeDef = definition; QueryType = queryType; } } diff --git a/src/HotChocolate/Core/src/Types/Types/Relay/QueryFieldTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Types/Relay/QueryFieldTypeInterceptor.cs index 9fd2adce9ce..95825bdd34c 100644 --- a/src/HotChocolate/Core/src/Types/Types/Relay/QueryFieldTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Relay/QueryFieldTypeInterceptor.cs @@ -23,7 +23,7 @@ internal sealed class QueryFieldTypeInterceptor : TypeInterceptor internal override void OnAfterResolveRootType( ITypeCompletionContext completionContext, - DefinitionBase definition, + ObjectTypeDefinition definition, OperationType operationType) { _context ??= completionContext; diff --git a/src/HotChocolate/Core/test/Types.Mutations.Tests/AnnotationBasedMutations.cs b/src/HotChocolate/Core/test/Types.Mutations.Tests/AnnotationBasedMutations.cs index 293918916e4..fbb5e290456 100644 --- a/src/HotChocolate/Core/test/Types.Mutations.Tests/AnnotationBasedMutations.cs +++ b/src/HotChocolate/Core/test/Types.Mutations.Tests/AnnotationBasedMutations.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using System.Reflection; using System.Threading.Tasks; using CookieCrumble; using HotChocolate.Configuration; using HotChocolate.Execution; -using HotChocolate.Types.Descriptors; using HotChocolate.Types.Descriptors.Definitions; using HotChocolate.Types.Relay; using Microsoft.Extensions.DependencyInjection; @@ -28,7 +26,23 @@ public async Task SimpleMutation_Inferred() schema.MatchSnapshot(); } + + [Fact] + public async Task SimpleMutation_Inferred_Global_Errors() + { + var schema = + await new ServiceCollection() + .AddGraphQL() + .AddMutationType() + .AddMutationConventions( + new MutationConventionOptions { ApplyToAllMutations = true }) + .TryAddTypeInterceptor() + .ModifyOptions(o => o.StrictValidation = false) + .BuildSchemaAsync(); + schema.MatchSnapshot(); + } + [Fact] public async Task SimpleMutation_Inferred_Query_Field_Stays_NonNull() { @@ -1397,4 +1411,18 @@ public DoSomething2Payload DoSomething2(int? userId) } public record DoSomething2Payload(int? UserId); + + public record SomeNewError(string Message); + + public class ErrorInterceptor : TypeInterceptor + { + public override void OnBeforeCompleteMutationField( + ITypeCompletionContext completionContext, + ObjectFieldDefinition mutationField) + { + mutationField.AddErrorType( + completionContext.DescriptorContext, + typeof(SomeNewError)); + } + } } diff --git a/src/HotChocolate/Data/src/Data/Projections/ProjectionTypeInterceptor.cs b/src/HotChocolate/Data/src/Data/Projections/ProjectionTypeInterceptor.cs index 09aba48bdb6..1cdc467ef37 100644 --- a/src/HotChocolate/Data/src/Data/Projections/ProjectionTypeInterceptor.cs +++ b/src/HotChocolate/Data/src/Data/Projections/ProjectionTypeInterceptor.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using HotChocolate.Configuration; +using HotChocolate.Language; using HotChocolate.Types; using HotChocolate.Types.Descriptors.Definitions; using static HotChocolate.Data.Projections.ProjectionConvention; @@ -9,11 +10,24 @@ namespace HotChocolate.Data.Projections; internal sealed class ProjectionTypeInterceptor : TypeInterceptor { + private ITypeCompletionContext? _queryContext; + + internal override void OnAfterResolveRootType( + ITypeCompletionContext completionContext, + ObjectTypeDefinition definition, + OperationType operationType) + { + if (operationType is OperationType.Query) + { + _queryContext = completionContext; + } + } + public override void OnAfterCompleteType( ITypeCompletionContext completionContext, DefinitionBase definition) { - if ((completionContext.IsQueryType ?? false) && + if (ReferenceEquals(completionContext, _queryContext) && completionContext.Type is ObjectType { Fields: var fields }) { var foundNode = false; diff --git a/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/SchemaDefinitionTypeInterceptor.cs b/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/SchemaDefinitionTypeInterceptor.cs index ce68cc83b30..0f92daaa95c 100644 --- a/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/SchemaDefinitionTypeInterceptor.cs +++ b/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/SchemaDefinitionTypeInterceptor.cs @@ -1,4 +1,5 @@ using HotChocolate.Configuration; +using HotChocolate.Language; using HotChocolate.Types; using HotChocolate.Types.Descriptors; using HotChocolate.Types.Descriptors.Definitions; @@ -10,19 +11,31 @@ namespace HotChocolate.Stitching.SchemaDefinitions; internal sealed class SchemaDefinitionTypeInterceptor : TypeInterceptor { private readonly bool _publishOnSchema; + private ITypeCompletionContext _queryContext = default!; public SchemaDefinitionTypeInterceptor(bool publishOnSchema) { _publishOnSchema = publishOnSchema; } + internal override void OnAfterResolveRootType( + ITypeCompletionContext completionContext, + ObjectTypeDefinition definition, + OperationType operationType) + { + if (operationType is OperationType.Query) + { + _queryContext = completionContext; + } + } + public override void OnBeforeCompleteType( ITypeCompletionContext completionContext, DefinitionBase definition) { // when we are visiting the query type we will add the schema definition field. if (_publishOnSchema && - (completionContext.IsQueryType ?? false) && + ReferenceEquals(completionContext, _queryContext) && definition is ObjectTypeDefinition objectTypeDefinition && !objectTypeDefinition.Fields.Any(t => t.Name.Equals(SchemaDefinitionField))) {