Configure support for GraphQL pagination and sorting

This commit auto-configures the new pagination and sorting support for
Spring for GraphQL, if Spring Data is available.
The `GraphQlAutoConfiguration` now contributes a `CursorStrategy` bean
that is used to set up the pagination and sorting data fetching
infrastructure.

This commit also configures by default a
`ConnectionTypeDefinitionConfigurer` that automatically detects
`*Connection` types and contributes the relevant schema definitions
according to the Relay spec.

See gh-34677
This commit is contained in:
Brian Clozel
2023-03-21 09:31:16 +01:00
committed by Andy Wilkinson
parent f13d6e6862
commit 7109542e3f
3 changed files with 73 additions and 4 deletions

View File

@@ -40,13 +40,22 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.log.LogMessage;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.data.pagination.ConnectionFieldTypeVisitor;
import org.springframework.graphql.data.pagination.CursorEncoder;
import org.springframework.graphql.data.pagination.CursorStrategy;
import org.springframework.graphql.data.query.ScrollPositionCursorStrategy;
import org.springframework.graphql.data.query.SliceConnectionAdapter;
import org.springframework.graphql.data.query.WindowConnectionAdapter;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.graphql.execution.ConnectionTypeDefinitionConfigurer;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.DefaultBatchLoaderRegistry;
import org.springframework.graphql.execution.DefaultExecutionGraphQlService;
@@ -94,6 +103,7 @@ public class GraphQlAutoConfiguration {
if (!properties.getSchema().getIntrospection().isEnabled()) {
builder.configureRuntimeWiring(this::enableIntrospection);
}
builder.configureTypeDefinitions(new ConnectionTypeDefinitionConfigurer());
wiringConfigurers.orderedStream().forEach(builder::configureRuntimeWiring);
sourceCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
@@ -154,6 +164,32 @@ public class GraphQlAutoConfiguration {
return annotatedControllerConfigurer.getExceptionResolver();
}
@ConditionalOnClass(ScrollPosition.class)
@Configuration(proxyBeanMethods = false)
static class GraphQlDataAutoConfiguration {
@Bean
@ConditionalOnMissingBean
CursorStrategy<ScrollPosition> cursorStrategy() {
return CursorStrategy.withEncoder(new ScrollPositionCursorStrategy(), CursorEncoder.base64());
}
@Bean
@SuppressWarnings("unchecked")
GraphQlSourceBuilderCustomizer cursorStrategyCustomizer(CursorStrategy<?> cursorStrategy) {
if (cursorStrategy.supports(ScrollPosition.class)) {
CursorStrategy<ScrollPosition> scrollCursorStrategy = (CursorStrategy<ScrollPosition>) cursorStrategy;
ConnectionFieldTypeVisitor connectionFieldTypeVisitor = ConnectionFieldTypeVisitor
.create(List.of(new WindowConnectionAdapter(scrollCursorStrategy),
new SliceConnectionAdapter(scrollCursorStrategy)));
return (builder) -> builder.typeVisitors(List.of(connectionFieldTypeVisitor));
}
return (builder) -> {
};
}
}
static class GraphQlResourcesRuntimeHints implements RuntimeHintsRegistrar {
@Override

View File

@@ -21,7 +21,12 @@ import java.nio.charset.StandardCharsets;
import graphql.GraphQL;
import graphql.execution.instrumentation.ChainedInstrumentation;
import graphql.execution.instrumentation.Instrumentation;
import graphql.schema.FieldCoordinates;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLOutputType;
import graphql.schema.GraphQLSchema;
import graphql.schema.PropertyDataFetcher;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.visibility.DefaultGraphqlFieldVisibility;
import graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility;
@@ -39,6 +44,8 @@ import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.data.pagination.CursorStrategy;
import org.springframework.graphql.data.pagination.EncodingCursorStrategy;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.DataLoaderRegistrar;
@@ -59,10 +66,12 @@ class GraphQlAutoConfigurationTests {
@Test
void shouldContributeDefaultBeans() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(GraphQlSource.class);
assertThat(context).hasSingleBean(BatchLoaderRegistry.class);
assertThat(context).hasSingleBean(ExecutionGraphQlService.class);
assertThat(context).hasSingleBean(AnnotatedControllerConfigurer.class);
assertThat(context).hasSingleBean(GraphQlSource.class)
.hasSingleBean(BatchLoaderRegistry.class)
.hasSingleBean(ExecutionGraphQlService.class)
.hasSingleBean(AnnotatedControllerConfigurer.class)
.hasSingleBean(CursorStrategy.class);
assertThat(context.getBean(CursorStrategy.class)).isInstanceOf(EncodingCursorStrategy.class);
});
}
@@ -195,6 +204,29 @@ class GraphQlAutoConfigurationTests {
assertThat(RuntimeHintsPredicates.resource().forResource("graphql/other.graphqls")).accepts(hints);
}
@Test
void shouldContributeConnectionTypeDefinitionConfigurer() {
this.contextRunner.withUserConfiguration(CustomGraphQlBuilderConfiguration.class).run((context) -> {
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
GraphQLSchema schema = graphQlSource.schema();
GraphQLOutputType bookConnection = schema.getQueryType().getField("books").getType();
assertThat(bookConnection).isNotNull().isInstanceOf(GraphQLObjectType.class);
assertThat((GraphQLObjectType) bookConnection)
.satisfies((connection) -> assertThat(connection.getFieldDefinition("edges")).isNotNull());
});
}
@Test
void shouldContributeConnectionDataFetcher() {
this.contextRunner.withUserConfiguration(CustomGraphQlBuilderConfiguration.class).run((context) -> {
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
GraphQLFieldDefinition books = graphQlSource.schema().getQueryType().getField("books");
FieldCoordinates booksCoordinates = FieldCoordinates.coordinates("Query", "books");
assertThat(graphQlSource.schema().getCodeRegistry().getDataFetcher(booksCoordinates, books))
.isNotInstanceOf(PropertyDataFetcher.class);
});
}
@Configuration(proxyBeanMethods = false)
static class CustomGraphQlBuilderConfiguration {

View File

@@ -1,4 +1,5 @@
type Query {
greeting(name: String! = "Spring"): String!
bookById(id: ID): Book
books: BookConnection
}