diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java index 8d28e1093e..6f735c3fd4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java @@ -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 cursorStrategy() { + return CursorStrategy.withEncoder(new ScrollPositionCursorStrategy(), CursorEncoder.base64()); + } + + @Bean + @SuppressWarnings("unchecked") + GraphQlSourceBuilderCustomizer cursorStrategyCustomizer(CursorStrategy cursorStrategy) { + if (cursorStrategy.supports(ScrollPosition.class)) { + CursorStrategy scrollCursorStrategy = (CursorStrategy) 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 diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java index 409193ba09..b39901b974 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java @@ -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 { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/resources/graphql/schema.graphqls b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/graphql/schema.graphqls index c2009b57ff..ea9b003d91 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/resources/graphql/schema.graphqls +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/graphql/schema.graphqls @@ -1,4 +1,5 @@ type Query { greeting(name: String! = "Spring"): String! bookById(id: ID): Book + books: BookConnection } \ No newline at end of file