Skip to content

Allow non-nullable "edges" list type in GraphQL relay #1173

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ private static GraphQLObjectType getAsObjectType(@Nullable GraphQLFieldDefinitio
@Nullable
private static GraphQLObjectType getEdgeType(@Nullable GraphQLFieldDefinition field) {
if (getType(field) instanceof GraphQLList listType) {
if (listType.getWrappedType() instanceof GraphQLObjectType type) {
if (getType(listType.getWrappedType()) instanceof GraphQLObjectType type) {
return type;
}
}
Expand All @@ -169,6 +169,14 @@ private static GraphQLType getType(@Nullable GraphQLFieldDefinition field) {
return (type instanceof GraphQLNonNull nonNullType) ? nonNullType.getWrappedType() : type;
}

@Nullable
private static GraphQLType getType(@Nullable GraphQLType type) {
if (type == null) {
return null;
}
return (type instanceof GraphQLNonNull nonNullType) ? nonNullType.getWrappedType() : type;
}


/**
* Create a {@code ConnectionTypeVisitor} instance that delegates to the
Expand All @@ -185,13 +193,13 @@ public static ConnectionFieldTypeVisitor create(List<ConnectionAdapter> adapters
/**
* {@code DataFetcher} decorator that adapts return values with an adapter.
*/
private record ConnectionDataFetcher(DataFetcher<?> delegate, ConnectionAdapter adapter) implements DataFetcher<Object> {
record ConnectionDataFetcher(DataFetcher<?> delegate, ConnectionAdapter adapter) implements DataFetcher<Object> {

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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,44 @@ void connectionTypeWithoutEdgesIsNotDecorated() throws Exception {
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 {

Expand Down Expand Up @@ -291,8 +329,6 @@ public TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition node,
}
}



private static class ListConnectionAdapter implements ConnectionAdapter {

private int initialOffset = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,43 @@ void batchLoader() {
assertThat(author.getLastName()).isEqualTo("Orwell");
}

@Test
void batchLoaderWithNullValue() {
String document = "{ " +
" booksByCriteria(criteria: {author:\"Orwell\"}) { " +
" author {" +
" firstName, " +
" lastName " +
" }" +
" }" +
"}";

this.registry.forTypePair(Long.class, Author.class)
.registerBatchLoader((ids, env) -> Flux.fromIterable(ids.stream().<Author>map(id -> null).toList()));

TestExecutionGraphQlService service = GraphQlSetup.schemaResource(BookSource.schema)
.queryFetcher("booksByCriteria", env -> {
Map<String, Object> criteria = env.getArgument("criteria");
String authorName = (String) criteria.get("author");
return BookSource.findBooksByAuthor(authorName).stream()
.map(book -> new Book(book.getId(), book.getName(), book.getAuthorId()))
.collect(Collectors.toList());
})
.dataFetcher("Book", "author", env -> {
Book book = env.getSource();
DataLoader<Long, Author> dataLoader = env.getDataLoader(Author.class.getName());
return dataLoader.load(book.getAuthorId());
})
.dataLoaders(this.registry)
.toGraphQlService();

Mono<ExecutionGraphQlResponse> responseMono = service.execute(document);

List<Book> books = ResponseHelper.forResponse(responseMono).toList("booksByCriteria", Book.class);
assertThat(books).hasSize(2);

Author author = books.get(0).getAuthor();
assertThat(author).isNull();
}

}