diff --git a/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs index 074df4ef520..4f9b0675ba7 100644 --- a/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs @@ -1,21 +1,20 @@ #nullable enable using HotChocolate.Configuration; -using HotChocolate.Internal; using HotChocolate.Language; -using HotChocolate.Language.Visitors; using HotChocolate.Resolvers; using HotChocolate.Types; using HotChocolate.Types.Descriptors; using HotChocolate.Types.Descriptors.Definitions; using HotChocolate.Types.Helpers; +using HotChocolate.Types.Relay; namespace HotChocolate; -// TODO: Ignore node and error interface from semantic non-null public class SemanticNonNullTypeInterceptor : TypeInterceptor { private ITypeInspector _typeInspector = null!; + private ExtendedTypeReference _nodeTypeReference = null!; internal override bool IsEnabled(IDescriptorContext context) => context.Options.EnableSemanticNonNull; @@ -28,6 +27,8 @@ internal override void InitializeContext( TypeReferenceResolver typeReferenceResolver) { _typeInspector = context.TypeInspector; + + _nodeTypeReference = _typeInspector.GetTypeRef(typeof(NodeType)); } public override void OnAfterCompleteName(ITypeCompletionContext completionContext, DefinitionBase definition) @@ -45,6 +46,8 @@ public override void OnAfterCompleteName(ITypeCompletionContext completionContex return; } + var implementsNode = objectDef.Interfaces.Any(i => i.Equals(_nodeTypeReference)); + foreach (var field in objectDef.Fields) { if (field.IsIntrospectionField) @@ -52,6 +55,11 @@ public override void OnAfterCompleteName(ITypeCompletionContext completionContex continue; } + if (implementsNode && field.Name == "id") + { + continue; + } + ApplySemanticNonNullDirective(field, completionContext); field.FormatterDefinitions.Add(CreateSemanticNonNullResultFormatterDefinition()); @@ -59,6 +67,12 @@ public override void OnAfterCompleteName(ITypeCompletionContext completionContex } else if (definition is InterfaceTypeDefinition interfaceDef) { + if (interfaceDef.Name == "Node") + { + // The Node interface is well defined, so we don't want to go and change the type of its fields. + return; + } + foreach (var field in interfaceDef.Fields) { ApplySemanticNonNullDirective(field, completionContext); diff --git a/src/HotChocolate/Core/test/Types.Tests/SemanticNonNullTests.cs b/src/HotChocolate/Core/test/Types.Tests/SemanticNonNullTests.cs index 83e8f7f87b2..951ddf29d48 100644 --- a/src/HotChocolate/Core/test/Types.Tests/SemanticNonNullTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/SemanticNonNullTests.cs @@ -3,6 +3,7 @@ using HotChocolate.Execution; using HotChocolate.Tests; using HotChocolate.Types; +using HotChocolate.Types.Relay; using Microsoft.Extensions.DependencyInjection; namespace HotChocolate; @@ -10,6 +11,22 @@ namespace HotChocolate; // TODO: Test node & paging public class SemanticNonNullTests { + [Fact] + public async Task Object_Implementing_Node() + { + await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => + { + o.EnableSemanticNonNull = true; + o.EnsureAllNodesCanBeResolved = false; + }) + .AddQueryType() + .AddGlobalObjectIdentification() + .BuildSchemaAsync() + .MatchSnapshotAsync(); + } + [Fact] public async Task MutationConventions() { @@ -26,15 +43,6 @@ public async Task MutationConventions() .MatchSnapshotAsync(); } - public class Mutation - { - [UseMutationConvention] - [Error] - public bool DoSomething() => true; - } - - public class MyException : Exception; - [Fact] public async Task Derive_SemanticNonNull_From_ImplementationFirst() { @@ -283,4 +291,22 @@ public class Foo { public string Bar { get; } = default!; } + + [ObjectType("Query")] + public class QueryWithNode + { + public MyNode GetMyNode() => new(1); + } + + [Node] + public record MyNode([property: ID] int Id); + + public class Mutation + { + [UseMutationConvention] + [Error] + public bool DoSomething() => true; + } + + public class MyException : Exception; } diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_Implementing_Node.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_Implementing_Node.snap new file mode 100644 index 00000000000..b2c648696dd --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_Implementing_Node.snap @@ -0,0 +1,23 @@ +schema { + query: Query +} + +"The node interface is implemented by entities that have a global unique identifier." +interface Node { + id: ID! +} + +type MyNode implements Node { + id: ID! +} + +type Query { + "Fetches an object given its ID." + node("ID of the object." id: ID!): Node + "Lookup nodes by a list of IDs." + nodes("The list of node IDs." ids: [ID!]!): [Node]! + myNode: MyNode @semanticNonNull +} + +"TODO" +directive @semanticNonNull("TODO" levels: [Int!] = [ 0 ]) on FIELD_DEFINITION