Skip to content

Commit f1a8275

Browse files
committed
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. Closes spring-projectsgh-34630
1 parent fd01b3d commit f1a8275

File tree

3 files changed

+73
-4
lines changed

3 files changed

+73
-4
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,22 @@
4040
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4141
import org.springframework.boot.convert.ApplicationConversionService;
4242
import org.springframework.context.annotation.Bean;
43+
import org.springframework.context.annotation.Configuration;
4344
import org.springframework.context.annotation.ImportRuntimeHints;
4445
import org.springframework.core.io.Resource;
4546
import org.springframework.core.io.support.ResourcePatternResolver;
4647
import org.springframework.core.log.LogMessage;
48+
import org.springframework.data.domain.ScrollPosition;
4749
import org.springframework.graphql.ExecutionGraphQlService;
4850
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
51+
import org.springframework.graphql.data.pagination.ConnectionFieldTypeVisitor;
52+
import org.springframework.graphql.data.pagination.CursorEncoder;
53+
import org.springframework.graphql.data.pagination.CursorStrategy;
54+
import org.springframework.graphql.data.query.ScrollPositionCursorStrategy;
55+
import org.springframework.graphql.data.query.SliceConnectionAdapter;
56+
import org.springframework.graphql.data.query.WindowConnectionAdapter;
4957
import org.springframework.graphql.execution.BatchLoaderRegistry;
58+
import org.springframework.graphql.execution.ConnectionTypeDefinitionConfigurer;
5059
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
5160
import org.springframework.graphql.execution.DefaultBatchLoaderRegistry;
5261
import org.springframework.graphql.execution.DefaultExecutionGraphQlService;
@@ -94,6 +103,7 @@ public GraphQlSource graphQlSource(ResourcePatternResolver resourcePatternResolv
94103
if (!properties.getSchema().getIntrospection().isEnabled()) {
95104
builder.configureRuntimeWiring(this::enableIntrospection);
96105
}
106+
builder.configureTypeDefinitions(new ConnectionTypeDefinitionConfigurer());
97107
wiringConfigurers.orderedStream().forEach(builder::configureRuntimeWiring);
98108
sourceCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
99109
return builder.build();
@@ -154,6 +164,32 @@ DataFetcherExceptionResolver annotatedControllerConfigurerDataFetcherExceptionRe
154164
return annotatedControllerConfigurer.getExceptionResolver();
155165
}
156166

167+
@ConditionalOnClass(ScrollPosition.class)
168+
@Configuration(proxyBeanMethods = false)
169+
static class GraphQlDataAutoConfiguration {
170+
171+
@Bean
172+
@ConditionalOnMissingBean
173+
CursorStrategy<ScrollPosition> cursorStrategy() {
174+
return CursorStrategy.withEncoder(new ScrollPositionCursorStrategy(), CursorEncoder.base64());
175+
}
176+
177+
@Bean
178+
@SuppressWarnings("unchecked")
179+
GraphQlSourceBuilderCustomizer cursorStrategyCustomizer(CursorStrategy<?> cursorStrategy) {
180+
if (cursorStrategy.supports(ScrollPosition.class)) {
181+
CursorStrategy<ScrollPosition> scrollCursorStrategy = (CursorStrategy<ScrollPosition>) cursorStrategy;
182+
ConnectionFieldTypeVisitor connectionFieldTypeVisitor = ConnectionFieldTypeVisitor
183+
.create(List.of(new WindowConnectionAdapter(scrollCursorStrategy),
184+
new SliceConnectionAdapter(scrollCursorStrategy)));
185+
return (builder) -> builder.typeVisitors(List.of(connectionFieldTypeVisitor));
186+
}
187+
return (builder) -> {
188+
};
189+
}
190+
191+
}
192+
157193
static class GraphQlResourcesRuntimeHints implements RuntimeHintsRegistrar {
158194

159195
@Override

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@
2121
import graphql.GraphQL;
2222
import graphql.execution.instrumentation.ChainedInstrumentation;
2323
import graphql.execution.instrumentation.Instrumentation;
24+
import graphql.schema.FieldCoordinates;
25+
import graphql.schema.GraphQLFieldDefinition;
26+
import graphql.schema.GraphQLObjectType;
27+
import graphql.schema.GraphQLOutputType;
2428
import graphql.schema.GraphQLSchema;
29+
import graphql.schema.PropertyDataFetcher;
2530
import graphql.schema.idl.RuntimeWiring;
2631
import graphql.schema.visibility.DefaultGraphqlFieldVisibility;
2732
import graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility;
@@ -39,6 +44,8 @@
3944
import org.springframework.core.io.ClassPathResource;
4045
import org.springframework.graphql.ExecutionGraphQlService;
4146
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
47+
import org.springframework.graphql.data.pagination.CursorStrategy;
48+
import org.springframework.graphql.data.pagination.EncodingCursorStrategy;
4249
import org.springframework.graphql.execution.BatchLoaderRegistry;
4350
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
4451
import org.springframework.graphql.execution.DataLoaderRegistrar;
@@ -59,10 +66,12 @@ class GraphQlAutoConfigurationTests {
5966
@Test
6067
void shouldContributeDefaultBeans() {
6168
this.contextRunner.run((context) -> {
62-
assertThat(context).hasSingleBean(GraphQlSource.class);
63-
assertThat(context).hasSingleBean(BatchLoaderRegistry.class);
64-
assertThat(context).hasSingleBean(ExecutionGraphQlService.class);
65-
assertThat(context).hasSingleBean(AnnotatedControllerConfigurer.class);
69+
assertThat(context).hasSingleBean(GraphQlSource.class)
70+
.hasSingleBean(BatchLoaderRegistry.class)
71+
.hasSingleBean(ExecutionGraphQlService.class)
72+
.hasSingleBean(AnnotatedControllerConfigurer.class)
73+
.hasSingleBean(CursorStrategy.class);
74+
assertThat(context.getBean(CursorStrategy.class)).isInstanceOf(EncodingCursorStrategy.class);
6675
});
6776
}
6877

@@ -195,6 +204,29 @@ void shouldRegisterHints() {
195204
assertThat(RuntimeHintsPredicates.resource().forResource("graphql/other.graphqls")).accepts(hints);
196205
}
197206

207+
@Test
208+
void shouldContributeConnectionTypeDefinitionConfigurer() {
209+
this.contextRunner.withUserConfiguration(CustomGraphQlBuilderConfiguration.class).run((context) -> {
210+
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
211+
GraphQLSchema schema = graphQlSource.schema();
212+
GraphQLOutputType bookConnection = schema.getQueryType().getField("books").getType();
213+
assertThat(bookConnection).isNotNull().isInstanceOf(GraphQLObjectType.class);
214+
assertThat((GraphQLObjectType) bookConnection)
215+
.satisfies((connection) -> assertThat(connection.getFieldDefinition("edges")).isNotNull());
216+
});
217+
}
218+
219+
@Test
220+
void shouldContributeConnectionDataFetcher() {
221+
this.contextRunner.withUserConfiguration(CustomGraphQlBuilderConfiguration.class).run((context) -> {
222+
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
223+
GraphQLFieldDefinition books = graphQlSource.schema().getQueryType().getField("books");
224+
FieldCoordinates booksCoordinates = FieldCoordinates.coordinates("Query", "books");
225+
assertThat(graphQlSource.schema().getCodeRegistry().getDataFetcher(booksCoordinates, books))
226+
.isNotInstanceOf(PropertyDataFetcher.class);
227+
});
228+
}
229+
198230
@Configuration(proxyBeanMethods = false)
199231
static class CustomGraphQlBuilderConfiguration {
200232

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
type Query {
22
greeting(name: String! = "Spring"): String!
33
bookById(id: ID): Book
4+
books: BookConnection
45
}

0 commit comments

Comments
 (0)