From 8d738274e6830f93bc55fbd817abd6ca96d791fb Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 19 Sep 2022 21:53:26 +0200 Subject: [PATCH] Fixed node discovery when using the NodeResolverAttribute on the query type (#5412) --- .../Core/src/Fetching/BatchScheduler.cs | 5 +- .../Types/Configuration/TypeInitializer.cs | 37 ++- .../Relay/NodeResolverTypeInterceptor.cs | 254 ++++++++++-------- 3 files changed, 160 insertions(+), 136 deletions(-) diff --git a/src/HotChocolate/Core/src/Fetching/BatchScheduler.cs b/src/HotChocolate/Core/src/Fetching/BatchScheduler.cs index e53ce9da3cf..9cb2ff16fe8 100644 --- a/src/HotChocolate/Core/src/Fetching/BatchScheduler.cs +++ b/src/HotChocolate/Core/src/Fetching/BatchScheduler.cs @@ -88,7 +88,10 @@ public void Schedule(Func dispatch) { _listeners[i].Invoke(this, EventArgs.Empty); } - catch { } + catch + { + // ignored + } } } } diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeInitializer.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeInitializer.cs index a677883e5b1..6d7d687f5ef 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/TypeInitializer.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/TypeInitializer.cs @@ -33,6 +33,7 @@ internal sealed class TypeInitializer private readonly List _temp = new(); private readonly List _typeRefs = new(); private readonly HashSet _typeRefSet = new(); + private readonly List _rootTypes = new(); public TypeInitializer( IDescriptorContext descriptorContext, @@ -84,8 +85,10 @@ public void Initialize() // with all types (implicit and explicit) known we complete the type names. CompleteNames(); - // with the type names all known we can now build pairs to bring together types and - // their type extensions. + // with the type names all known we will announce the root type objects. + ResolveRootTyped(); + + // we can now build pairs to bring together types and their type extensions. MergeTypeExtensions(); // last we complete the types. Completing types means that we will assign all @@ -187,11 +190,9 @@ private void RegisterImplicitInterfaceDependencies() private void CompleteNames() { - var rootTypes = new List(); - _interceptor.OnBeforeCompleteTypeNames(); - if (ProcessTypes(TypeDependencyKind.Named, type => CompleteTypeName(type, rootTypes)) && + if (ProcessTypes(TypeDependencyKind.Named, type => CompleteTypeName(type)) && _interceptor.TriggerAggregations) { _interceptor.OnTypesCompletedName(_typeRegistry.Types); @@ -200,14 +201,6 @@ private void CompleteNames() EnsureNoErrors(); _interceptor.OnAfterCompleteTypeNames(); - - foreach (var type in rootTypes) - { - _interceptor.OnAfterResolveRootType( - type.Context, - ((ObjectType)type.Type.Type).Definition!, - type.Kind); - } } internal RegisteredType InitializeType( @@ -231,11 +224,6 @@ private void CompleteNames() } internal bool CompleteTypeName(RegisteredType registeredType) - => CompleteTypeName(registeredType, new List()); - - private bool CompleteTypeName( - RegisteredType registeredType, - List rootTypes) { registeredType.PrepareForCompletion( _typeReferenceResolver, @@ -256,7 +244,7 @@ internal bool CompleteTypeName(RegisteredType registeredType) if (kind is not RootTypeKind.None) { - rootTypes.Add( + _rootTypes.Add( new RegisteredRootType( registeredType, registeredType, @@ -266,6 +254,17 @@ internal bool CompleteTypeName(RegisteredType registeredType) return true; } + private void ResolveRootTyped() + { + foreach (var type in _rootTypes) + { + _interceptor.OnAfterResolveRootType( + type.Context, + ((ObjectType)type.Type.Type).Definition!, + type.Kind); + } + } + private void MergeTypeExtensions() { _interceptor.OnBeforeMergeTypeExtensions(); diff --git a/src/HotChocolate/Core/src/Types/Types/Relay/NodeResolverTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Types/Relay/NodeResolverTypeInterceptor.cs index b9445e214c0..e96863f00f3 100644 --- a/src/HotChocolate/Core/src/Types/Types/Relay/NodeResolverTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Relay/NodeResolverTypeInterceptor.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using HotChocolate.Configuration; @@ -20,7 +21,18 @@ namespace HotChocolate.Types.Relay; internal sealed class NodeResolverTypeInterceptor : TypeInterceptor { private readonly List> _nodes = new(); - private ObjectType? _queryType; + + private ITypeCompletionContext? CompletionContext { get; set; } + + private ObjectType? QueryType { get; set; } + + private ObjectTypeDefinition? TypeDef { get; set; } + + [MemberNotNullWhen(true, nameof(QueryType), nameof(TypeDef), nameof(CompletionContext))] + private bool IsInitialized + => QueryType is not null && + TypeDef is not null && + CompletionContext is not null; internal override void OnAfterResolveRootType( ITypeCompletionContext completionContext, @@ -32,131 +44,141 @@ internal sealed class NodeResolverTypeInterceptor : TypeInterceptor definition is ObjectTypeDefinition typeDef && completionContext.Type is ObjectType queryType) { - var typeInspector = completionContext.TypeInspector; + CompletionContext = completionContext; + TypeDef = typeDef; + QueryType = queryType; + } + } + + public override void OnAfterMergeTypeExtensions() + { + if (!IsInitialized) + { + return; + } - // we store the query types as state on the type interceptor, - // so that we can use it to get the final field resolver pipeline - // form the query fields that double as node resolver once they are - // fully compiled. - _queryType = queryType; + // we store the query types as state on the type interceptor, + // so that we can use it to get the final field resolver pipeline + // form the query fields that double as node resolver once they are + // fully compiled. + var typeInspector = CompletionContext.TypeInspector; - foreach (var fieldDef in typeDef.Fields) + foreach (var fieldDef in TypeDef.Fields) + { + var resolverMember = fieldDef.ResolverMember ?? fieldDef.Member; + + // candidate fields that we might be able to use as node resolvers must specify + // a resolver member. Delegates or expressions are not supported as node resolvers. + // Further, we only will look at annotated fields. This feature is always opt-in. + if (fieldDef.Type is not null && + resolverMember is not null && + fieldDef.Expression is null && + resolverMember.IsDefined(typeof(NodeResolverAttribute))) { - var resolverMember = fieldDef.ResolverMember ?? fieldDef.Member; - - // candidate fields that we might be able to use as node resolvers must specify - // a resolver member. Delegates or expressions are not supported as node resolvers. - // Further, we only will look at annotated fields. This feature is always opt-in. - if (fieldDef.Type is not null && - resolverMember is not null && - fieldDef.Expression is null && - resolverMember.IsDefined(typeof(NodeResolverAttribute))) + // Query fields that users want to reuse as node resolvers must exactly specify + // one argument and that argument must be the node id. + if (fieldDef.Arguments.Count != 1) + { + CompletionContext.ReportError( + NodeResolver_MustHaveExactlyOneIdArg( + fieldDef.Name, + QueryType)); + continue; + } + + // We will capture the argument and ensure that it has a type. + // If ut does not have a type something is wrong with the initialization + // process and we will fail the initialization. + var argument = fieldDef.Arguments[0]; + + if (argument.Type is null) { - // Query fields that users want to reuse as node resolvers must exactly specify - // one argument and that argument must be the node id. - if (fieldDef.Arguments.Count != 1) - { - completionContext.ReportError( - NodeResolver_MustHaveExactlyOneIdArg( - fieldDef.Name, - queryType)); - continue; - } - - // We will capture the argument and ensure that it has a type. - // If ut does not have a type something is wrong with the initialization - // process and we will fail the initialization. - var argument = fieldDef.Arguments[0]; - - if (argument.Type is null) - { - throw NodeResolver_ArgumentTypeMissing(); - } - - // Next we will capture the field result type and ensure that it is an - // object type. - // Node resolvers can only be object types. - // Interfaces, unions are not allowed as we resolve a concrete node type. - // Also we cannot use resolvers that return a list or really anything else - // then an object type. - var fieldType = completionContext.GetType(fieldDef.Type); - - if (!fieldType.IsObjectType()) - { - completionContext.ReportError( - NodeResolver_MustReturnObject( - fieldDef.Name, - queryType)); - continue; - } - - // Once we have the type instance we need to grab it type definition to - // inject a placeholder for the node resolver pipeline into the types - // context data. - var fieldTypeDef = ((ObjectType)fieldType.NamedType()).Definition; - - if (fieldTypeDef is null) - { - throw NodeResolver_ObjNoDefinition(); - } - - // Before we go any further we will ensure that the type either implements the - // node interface already or it contains an id field. - if (!ImplementsNode(completionContext, typeDef)) - { - // we will ensure that the object type is implementing the node type interface. - fieldTypeDef.Interfaces.Add(typeInspector.GetTypeRef(typeof(NodeType))); - } - - var idDef = fieldTypeDef.Fields.FirstOrDefault(t => t.Name.EqualsOrdinal(Id)); - - if (idDef is null) - { - completionContext.ReportError( - NodeResolver_NodeTypeHasNoId( - (ObjectType)fieldType.NamedType())); - continue; - } - - // Now that we know we can infer a node resolver form the annotated query field - // we will start mutating the type and field. - // First we are adding a marker to the node type`s context data. - // We will replace this later with a NodeResolverInfo instance that - // allows the node field to resolve a node instance by its ID. - fieldTypeDef.ContextData[NodeResolver] = fieldDef.Name; - - // We also want to ensure that the node id argument is always a non-null - // ID type. So, if the user has not specified that we are making sure of this - // by overwriting the arguments type reference. - argument.Type = typeInspector.GetTypeRef(typeof(NonNullType)); - - // We also need to add an input formatter to the argument the decodes passed - // in ID values. - RelayIdFieldHelpers.AddSerializerToInputField( - completionContext, - argument, - fieldTypeDef.Name); - - // As with the id argument we also want to make sure that the ID field of - // the fields result type is a non-null ID type. - idDef.Type = argument.Type; - - // For the id field we need to make sure that a result formatter is registered - // that encodes the IDs returned from the id field. - RelayIdFieldHelpers.ApplyIdToField(idDef); - - // Last we register the context data of our node with the type - // interceptors state. - // We do that to replace our marker with the actual NodeResolverInfo instance. - _nodes.Add(fieldTypeDef.ContextData); + throw NodeResolver_ArgumentTypeMissing(); } + + // Next we will capture the field result type and ensure that it is an + // object type. + // Node resolvers can only be object types. + // Interfaces, unions are not allowed as we resolve a concrete node type. + // Also we cannot use resolvers that return a list or really anything else + // then an object type. + var fieldType = CompletionContext.GetType(fieldDef.Type); + + if (!fieldType.IsObjectType()) + { + CompletionContext.ReportError( + NodeResolver_MustReturnObject( + fieldDef.Name, + QueryType)); + continue; + } + + // Once we have the type instance we need to grab it type definition to + // inject a placeholder for the node resolver pipeline into the types + // context data. + var fieldTypeDef = ((ObjectType)fieldType.NamedType()).Definition; + + if (fieldTypeDef is null) + { + throw NodeResolver_ObjNoDefinition(); + } + + // Before we go any further we will ensure that the type either implements the + // node interface already or it contains an id field. + if (!ImplementsNode(CompletionContext, TypeDef)) + { + // we will ensure that the object type is implementing the node type interface. + fieldTypeDef.Interfaces.Add(typeInspector.GetTypeRef(typeof(NodeType))); + } + + var idDef = fieldTypeDef.Fields.FirstOrDefault(t => t.Name.EqualsOrdinal(Id)); + + if (idDef is null) + { + CompletionContext.ReportError( + NodeResolver_NodeTypeHasNoId( + (ObjectType)fieldType.NamedType())); + continue; + } + + // Now that we know we can infer a node resolver form the annotated query field + // we will start mutating the type and field. + // First we are adding a marker to the node type`s context data. + // We will replace this later with a NodeResolverInfo instance that + // allows the node field to resolve a node instance by its ID. + fieldTypeDef.ContextData[NodeResolver] = fieldDef.Name; + + // We also want to ensure that the node id argument is always a non-null + // ID type. So, if the user has not specified that we are making sure of this + // by overwriting the arguments type reference. + argument.Type = typeInspector.GetTypeRef(typeof(NonNullType)); + + // We also need to add an input formatter to the argument the decodes passed + // in ID values. + RelayIdFieldHelpers.AddSerializerToInputField( + CompletionContext, + argument, + fieldTypeDef.Name); + + // As with the id argument we also want to make sure that the ID field of + // the fields result type is a non-null ID type. + idDef.Type = argument.Type; + + // For the id field we need to make sure that a result formatter is registered + // that encodes the IDs returned from the id field. + RelayIdFieldHelpers.ApplyIdToField(idDef); + + // Last we register the context data of our node with the type + // interceptors state. + // We do that to replace our marker with the actual NodeResolverInfo instance. + _nodes.Add(fieldTypeDef.ContextData); } } } public override void OnAfterCompleteTypes() { - if (_queryType is not null && _nodes.Count > 0) + if (QueryType is not null && _nodes.Count > 0) { // After all types are completed it is guaranteed that all // query field resolver pipelines are fully compiled. @@ -164,7 +186,7 @@ public override void OnAfterCompleteTypes() foreach (var node in _nodes) { var fieldName = (string)node[NodeResolver]!; - var field = _queryType.Fields[fieldName]; + var field = QueryType.Fields[fieldName]; node[NodeResolver] = new NodeResolverInfo(field.Arguments[0], field.Middleware); } }