From d8381e6cf3169fb1cb441f5f62eef66b85d3c049 Mon Sep 17 00:00:00 2001 From: James Bodkin Date: Thu, 3 Apr 2025 10:48:39 +0100 Subject: [PATCH] Allow non-nullable types in GraphQL Relay Closes gh-1173 Signed-off-by: James Bodkin [brian.clozel@broadcom.com: remove unrelated test] Signed-off-by: Brian Clozel --- .../ConnectionFieldTypeVisitor.java | 18 ++++++--- .../ConnectionFieldTypeVisitorTests.java | 40 ++++++++++++++++++- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/ConnectionFieldTypeVisitor.java b/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/ConnectionFieldTypeVisitor.java index 464f8e7d..c610b9f1 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/ConnectionFieldTypeVisitor.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/ConnectionFieldTypeVisitor.java @@ -39,7 +39,6 @@ import graphql.schema.GraphQLFieldsContainer; import graphql.schema.GraphQLList; import graphql.schema.GraphQLNonNull; import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLOutputType; import graphql.schema.GraphQLSchemaElement; import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeVisitorStub; @@ -153,7 +152,7 @@ public final class ConnectionFieldTypeVisitor extends GraphQLTypeVisitorStub { @Nullable private static GraphQLObjectType getEdgeType(@Nullable GraphQLFieldDefinition field) { if (getType(field) instanceof GraphQLList listType) { - if (listType.getWrappedType() instanceof GraphQLObjectType type) { + if (unwrapNonNullType(listType.getWrappedType()) instanceof GraphQLObjectType type) { return type; } } @@ -165,7 +164,14 @@ public final class ConnectionFieldTypeVisitor extends GraphQLTypeVisitorStub { if (field == null) { return null; } - GraphQLOutputType type = field.getType(); + return unwrapNonNullType(field.getType()); + } + + @Nullable + private static GraphQLType unwrapNonNullType(@Nullable GraphQLType type) { + if (type == null) { + return null; + } return (type instanceof GraphQLNonNull nonNullType) ? nonNullType.getWrappedType() : type; } @@ -184,14 +190,16 @@ public final class ConnectionFieldTypeVisitor extends GraphQLTypeVisitorStub { /** * {@code DataFetcher} decorator that adapts return values with an adapter. + * @param delegate the datafetcher delegate + * @param adapter the connection adapter to use */ - private record ConnectionDataFetcher(DataFetcher delegate, ConnectionAdapter adapter) implements DataFetcher { + record ConnectionDataFetcher(DataFetcher delegate, ConnectionAdapter adapter) implements DataFetcher { private static final Connection EMPTY_CONNECTION = new DefaultConnection<>(Collections.emptyList(), new DefaultPageInfo(null, null, false, false)); - private ConnectionDataFetcher { + ConnectionDataFetcher { Assert.notNull(delegate, "DataFetcher delegate is required"); Assert.notNull(adapter, "ConnectionAdapter is required"); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/pagination/ConnectionFieldTypeVisitorTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/pagination/ConnectionFieldTypeVisitorTests.java index 84912e67..717d6a40 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/pagination/ConnectionFieldTypeVisitorTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/pagination/ConnectionFieldTypeVisitorTests.java @@ -205,6 +205,44 @@ public class ConnectionFieldTypeVisitorTests { assertThat(actual).isSameAs(dataFetcher); } + @Test + void connectionTypeWithNonNullEdgesIsDecorated() throws Exception { + String schemaContent = """ + type Query { + libraries(first: Int, after: String, last: Int, before: String): LibraryConnection! + } + + type LibraryConnection { + edges: [LibraryEdge!]! + pageInfo: PageInfo! + } + + type LibraryEdge { + node: Library! + cursor: String! + } + + type Library { + name: String + } + + type PageInfo { + hasPreviousPage: Boolean! + hasNextPage: Boolean! + startCursor: String + endCursor: String + } + """; + + FieldCoordinates coordinates = FieldCoordinates.coordinates("Query", "libraries"); + DataFetcher dataFetcher = env -> null; + + DataFetcher actual = + applyConnectionFieldTypeVisitor(schemaContent, coordinates, dataFetcher); + + assertThat(actual).isInstanceOf(ConnectionFieldTypeVisitor.ConnectionDataFetcher.class); + } + private static DataFetcher applyConnectionFieldTypeVisitor( Object schemaSource, FieldCoordinates coordinates, DataFetcher fetcher) throws Exception { @@ -291,8 +329,6 @@ public class ConnectionFieldTypeVisitorTests { } } - - private static class ListConnectionAdapter implements ConnectionAdapter { private int initialOffset = 0;