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:
committed by
Andy Wilkinson
parent
f13d6e6862
commit
7109542e3f
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
type Query {
|
||||
greeting(name: String! = "Spring"): String!
|
||||
bookById(id: ID): Book
|
||||
books: BookConnection
|
||||
}
|
||||
Reference in New Issue
Block a user