Merge pull request #34677 from bclozel

* gh-34677:
  Polish "Configure support for GraphQL pagination and sorting"
  Configure support for GraphQL pagination and sorting

Closes gh-34677
This commit is contained in:
Andy Wilkinson 2023-03-23 13:41:21 +00:00
commit 07cb38bb15
3 changed files with 72 additions and 6 deletions

View File

@ -40,13 +40,23 @@ 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.pagination.EncodingCursorStrategy;
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 +104,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 +165,32 @@ public class GraphQlAutoConfiguration {
return annotatedControllerConfigurer.getExceptionResolver();
}
@ConditionalOnClass(ScrollPosition.class)
@Configuration(proxyBeanMethods = false)
static class GraphQlDataAutoConfiguration {
@Bean
@ConditionalOnMissingBean
EncodingCursorStrategy<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,7 @@ 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.EncodingCursorStrategy;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.DataLoaderRegistrar;
@ -58,12 +64,11 @@ 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);
});
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GraphQlSource.class)
.hasSingleBean(BatchLoaderRegistry.class)
.hasSingleBean(ExecutionGraphQlService.class)
.hasSingleBean(AnnotatedControllerConfigurer.class)
.hasSingleBean(EncodingCursorStrategy.class));
}
@Test
@ -195,6 +200,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
}