Skip to content

Commit

Permalink
Fixed node discovery when using the NodeResolverAttribute on the quer…
Browse files Browse the repository at this point in the history
…y type (#5412)
  • Loading branch information
michaelstaib committed Sep 19, 2022
1 parent 9d34f13 commit 8d73827
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 136 deletions.
5 changes: 4 additions & 1 deletion src/HotChocolate/Core/src/Fetching/BatchScheduler.cs
Expand Up @@ -88,7 +88,10 @@ public void Schedule(Func<ValueTask> dispatch)
{
_listeners[i].Invoke(this, EventArgs.Empty);
}
catch { }
catch
{
// ignored
}
}
}
}
Expand Down
37 changes: 18 additions & 19 deletions src/HotChocolate/Core/src/Types/Configuration/TypeInitializer.cs
Expand Up @@ -33,6 +33,7 @@ internal sealed class TypeInitializer
private readonly List<RegisteredType> _temp = new();
private readonly List<ITypeReference> _typeRefs = new();
private readonly HashSet<ITypeReference> _typeRefSet = new();
private readonly List<RegisteredRootType> _rootTypes = new();

public TypeInitializer(
IDescriptorContext descriptorContext,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -187,11 +190,9 @@ private void RegisterImplicitInterfaceDependencies()

private void CompleteNames()
{
var rootTypes = new List<RegisteredRootType>();

_interceptor.OnBeforeCompleteTypeNames();

if (ProcessTypes(TypeDependencyKind.Named, type => CompleteTypeName(type, rootTypes)) &&
if (ProcessTypes(TypeDependencyKind.Named, type => CompleteTypeName(type)) &&
_interceptor.TriggerAggregations)
{
_interceptor.OnTypesCompletedName(_typeRegistry.Types);
Expand All @@ -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(
Expand All @@ -231,11 +224,6 @@ private void CompleteNames()
}

internal bool CompleteTypeName(RegisteredType registeredType)
=> CompleteTypeName(registeredType, new List<RegisteredRootType>());

private bool CompleteTypeName(
RegisteredType registeredType,
List<RegisteredRootType> rootTypes)
{
registeredType.PrepareForCompletion(
_typeReferenceResolver,
Expand All @@ -256,7 +244,7 @@ internal bool CompleteTypeName(RegisteredType registeredType)

if (kind is not RootTypeKind.None)
{
rootTypes.Add(
_rootTypes.Add(
new RegisteredRootType(
registeredType,
registeredType,
Expand All @@ -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();
Expand Down
@@ -1,6 +1,7 @@
#nullable enable

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using HotChocolate.Configuration;
Expand All @@ -20,7 +21,18 @@ namespace HotChocolate.Types.Relay;
internal sealed class NodeResolverTypeInterceptor : TypeInterceptor
{
private readonly List<IDictionary<string, object?>> _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,
Expand All @@ -32,139 +44,149 @@ 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<IType>(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<IdType>));

// 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<IType>(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<IdType>));

// 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.
// So, we can start replacing our marker with the actual NodeResolverInfo.
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);
}
}
Expand Down

0 comments on commit 8d73827

Please sign in to comment.