Improve "Connection" schema type checks

ConnectionFieldTypeVisitor now checks the complete structure
of Connection, Edge, and PageInfo schema types before
decorating a DataFetcher.

See gh-709
This commit is contained in:
rstoyanchev
2023-05-22 12:50:57 +01:00
parent 095d720941
commit d113831648
2 changed files with 142 additions and 19 deletions

View File

@@ -34,8 +34,10 @@ import graphql.schema.DataFetchingEnvironment;
import graphql.schema.GraphQLCodeRegistry;
import graphql.schema.GraphQLFieldDefinition;
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;
@@ -101,14 +103,54 @@ public final class ConnectionFieldTypeVisitor extends GraphQLTypeVisitorStub {
return TraversalControl.CONTINUE;
}
private static boolean isConnectionField(GraphQLFieldDefinition fieldDefinition) {
GraphQLType returnType = fieldDefinition.getType();
if (returnType instanceof GraphQLNonNull nonNullType) {
returnType = nonNullType.getWrappedType();
private static boolean isConnectionField(GraphQLFieldDefinition field) {
GraphQLObjectType type = getAsObjectType(field);
if (type == null || !type.getName().endsWith("Connection")) {
return false;
}
return (returnType instanceof GraphQLObjectType objectType &&
objectType.getName().endsWith("Connection") &&
objectType.getField("pageInfo") != null);
GraphQLObjectType edgeType = getEdgeType(type.getField("edges"));
if (edgeType == null || !edgeType.getName().endsWith("Edge")) {
return false;
}
if (edgeType.getField("node") == null || edgeType.getField("cursor") == null) {
return false;
}
GraphQLObjectType pageInfoType = getAsObjectType(type.getField("pageInfo"));
if (pageInfoType == null || !pageInfoType.getName().equals("PageInfo")) {
return false;
}
if (pageInfoType.getField("hasPreviousPage") == null || pageInfoType.getField("hasNextPage") == null ||
pageInfoType.getField("startCursor") == null || pageInfoType.getField("endCursor") == null) {
return false;
}
return true;
}
@Nullable
private static GraphQLObjectType getAsObjectType(@Nullable GraphQLFieldDefinition field) {
return (getType(field) instanceof GraphQLObjectType type ? type : null);
}
@Nullable
private static GraphQLObjectType getEdgeType(@Nullable GraphQLFieldDefinition field) {
if (getType(field) instanceof GraphQLList listType) {
if (listType.getWrappedType() instanceof GraphQLObjectType type) {
return type;
}
}
return null;
}
@Nullable
private static GraphQLType getType(@Nullable GraphQLFieldDefinition field) {
if (field == null) {
return null;
}
GraphQLOutputType type = field.getType();
return (type instanceof GraphQLNonNull nonNullType ? nonNullType.getWrappedType() : type);
}

View File

@@ -19,14 +19,28 @@ package org.springframework.graphql.data.pagination;
import java.util.Collection;
import java.util.List;
import graphql.schema.DataFetcher;
import graphql.schema.FieldCoordinates;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLSchema;
import graphql.schema.PropertyDataFetcher;
import graphql.schema.SchemaTransformer;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.core.io.Resource;
import org.springframework.graphql.BookSource;
import org.springframework.graphql.ExecutionGraphQlResponse;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.graphql.ResponseHelper;
import org.springframework.graphql.execution.ConnectionTypeDefinitionConfigurer;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link ConnectionFieldTypeVisitor}.
@@ -69,18 +83,6 @@ public class ConnectionFieldTypeVisitorTests {
);
}
@Test // gh-707
void trivialDataFetcherIsSkipped() {
Mono<ExecutionGraphQlResponse> response = GraphQlSetup.schemaResource(BookSource.paginationSchema)
.dataFetcher("Query", "books", new PropertyDataFetcher<>("books"))
.connectionSupport(new ListConnectionAdapter())
.toGraphQlService()
.execute(BookSource.booksConnectionQuery(null));
ResponseHelper.forResponse(response).assertData("{\"books\":null}");
}
@Test // gh-707
void nullValueTreatedAsEmptyConnection() {
@@ -103,6 +105,85 @@ public class ConnectionFieldTypeVisitorTests {
}
@Nested
class DecorationTests {
@Test // gh-707
void trivialDataFetcherIsNotDecorated() throws Exception {
FieldCoordinates coordinates = FieldCoordinates.coordinates("Query", "books");
DataFetcher<?> dataFetcher = new PropertyDataFetcher<>("books");
DataFetcher<?> actual =
applyConnectionFieldTypeVisitor(BookSource.paginationSchema, coordinates, dataFetcher);
assertThat(actual).isSameAs(dataFetcher);
}
@Test // gh-709
void connectionTypeWithoutEdgesIsNotDecorated() throws Exception {
String schemaContent = """
type Query {
puzzles: PuzzleConnection
}
type PuzzleConnection {
pageInfo: PuzzlePageInfo!
puzzles: [PuzzleEdge]
}
type PuzzlePageInfo {
total: Int!
}
type PuzzleEdge {
puzzle: Puzzle
cursor: String!
}
type Puzzle {
title: String!
}
""";
FieldCoordinates coordinates = FieldCoordinates.coordinates("Query", "puzzles");
DataFetcher<?> dataFetcher = env -> null;
DataFetcher<?> actual = applyConnectionFieldTypeVisitor(schemaContent, coordinates, dataFetcher);
assertThat(actual).isSameAs(dataFetcher);
}
private static DataFetcher<?> applyConnectionFieldTypeVisitor(
Object schemaSource, FieldCoordinates coordinates, DataFetcher<?> fetcher) throws Exception {
TypeDefinitionRegistry registry;
if (schemaSource instanceof Resource resource) {
registry = new SchemaParser().parse(resource.getInputStream());
}
else if (schemaSource instanceof String schemaContent) {
registry = new SchemaParser().parse(schemaContent);
}
else {
throw new IllegalArgumentException();
}
new ConnectionTypeDefinitionConfigurer().configure(registry);
GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(
registry, RuntimeWiring.newRuntimeWiring()
.type(coordinates.getTypeName(), b -> b.dataFetcher(coordinates.getFieldName(), fetcher))
.build());
ConnectionFieldTypeVisitor visitor =
ConnectionFieldTypeVisitor.create(List.of(new ListConnectionAdapter()));
schema = new SchemaTransformer().transform(schema, visitor);
GraphQLFieldDefinition field = schema.getFieldDefinition(coordinates);
return schema.getCodeRegistry().getDataFetcher(coordinates, field);
}
}
private static class ListConnectionAdapter implements ConnectionAdapter {
private int initialOffset = 0;