Skip to content

Commit 104eccb

Browse files
committed
Support for pagination
ConnectionTypeVisitor to decorate DataFetchers for Connection fields in order to adapt Window, Slice, and others to Connection. A ScrollRequest controller method argument to inject the ScrollPosition and the number of elements requested. SortStrategy for an application to customize how to extract sort details from GraphQL arguments. See gh-620
1 parent 65a0cee commit 104eccb

29 files changed

+1975
-1
lines changed

platform/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ dependencies {
1212
api(platform("io.projectreactor:reactor-bom:2022.0.3"))
1313
api(platform("io.micrometer:micrometer-bom:1.10.4"))
1414
api(platform("io.micrometer:micrometer-tracing-bom:1.0.2"))
15-
api(platform("org.springframework.data:spring-data-bom:2022.0.2"))
15+
api(platform("org.springframework.data:spring-data-bom:2023.0.0-SNAPSHOT"))
1616
api(platform("org.springframework.security:spring-security-bom:6.0.1"))
1717
api(platform("com.querydsl:querydsl-bom:5.0.0"))
1818
api(platform("io.rsocket:rsocket-bom:1.1.3"))

spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767
import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite;
6868
import org.springframework.graphql.data.method.annotation.BatchMapping;
6969
import org.springframework.graphql.data.method.annotation.SchemaMapping;
70+
import org.springframework.graphql.data.pagination.CursorStrategy;
71+
import org.springframework.graphql.data.pagination.SortStrategy;
7072
import org.springframework.graphql.execution.BatchLoaderRegistry;
7173
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
7274
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
@@ -124,6 +126,12 @@ public class AnnotatedControllerConfigurer
124126

125127
private final FormattingConversionService conversionService = new DefaultFormattingConversionService();
126128

129+
@Nullable
130+
private CursorStrategy<?> cursorStrategy;
131+
132+
@Nullable
133+
private SortStrategy<?> sortStrategy;
134+
127135
private final List<HandlerMethodArgumentResolver> customArgumentResolvers = new ArrayList<>(8);
128136

129137
@Nullable
@@ -152,6 +160,32 @@ public void addFormatterRegistrar(FormatterRegistrar registrar) {
152160
registrar.registerFormatters(this.conversionService);
153161
}
154162

163+
/**
164+
* Configure a {@link CursorStrategy} to handle pagination requests, which
165+
* results in one of the following:
166+
* <ul>
167+
* <li>If Spring Data is present, and the strategy supports {@code ScrollPosition},
168+
* then {@link ScrollRequestMethodArgumentResolver} is
169+
* configured as a method argument resolver.
170+
* <li>Otherwise {@link PaginationRequestMethodArgumentResolver} is added
171+
* instead.
172+
* </ul>
173+
* @since 1.2
174+
*/
175+
public void setCursorStrategy(@Nullable CursorStrategy<?> cursorStrategy) {
176+
this.cursorStrategy = cursorStrategy;
177+
}
178+
179+
/**
180+
* Configure a {@link SortStrategy} to extract sort details for pagination
181+
* requests. This results in {@link SortMethodArgumentResolver} being added
182+
* as a method argument resolver.
183+
* @since 1.2
184+
*/
185+
public void setSortStrategy(SortStrategy<?> sortStrategy) {
186+
this.sortStrategy = sortStrategy;
187+
}
188+
155189
/**
156190
* Add a {@link HandlerMethodArgumentResolver} for custom controller method
157191
* arguments. Such custom resolvers are ordered after built-in resolvers
@@ -251,6 +285,12 @@ private HandlerMethodArgumentResolverComposite initArgumentResolvers() {
251285
// Type based
252286
resolvers.addResolver(new DataFetchingEnvironmentMethodArgumentResolver());
253287
resolvers.addResolver(new DataLoaderMethodArgumentResolver());
288+
if (this.cursorStrategy != null) {
289+
resolvers.addResolver(initPaginationResolver(this.cursorStrategy));
290+
}
291+
if (this.sortStrategy != null) {
292+
resolvers.addResolver(new SortMethodArgumentResolver(this.sortStrategy));
293+
}
254294
if (springSecurityPresent) {
255295
resolvers.addResolver(new PrincipalMethodArgumentResolver());
256296
BeanResolver beanResolver = new BeanFactoryResolver(obtainApplicationContext());
@@ -268,6 +308,17 @@ private HandlerMethodArgumentResolverComposite initArgumentResolvers() {
268308
return resolvers;
269309
}
270310

311+
@SuppressWarnings("unchecked")
312+
private HandlerMethodArgumentResolver initPaginationResolver(CursorStrategy<?> cursorStrategy) {
313+
if (springDataPresent) {
314+
if (cursorStrategy.supports(org.springframework.data.domain.ScrollPosition.class)) {
315+
return new ScrollRequestMethodArgumentResolver(
316+
(CursorStrategy<org.springframework.data.domain.ScrollPosition>) cursorStrategy);
317+
}
318+
}
319+
return new PaginationRequestMethodArgumentResolver<>(cursorStrategy);
320+
}
321+
271322
protected final ApplicationContext obtainApplicationContext() {
272323
Assert.state(this.applicationContext != null, "No ApplicationContext");
273324
return this.applicationContext;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2020-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.data.method.annotation.support;
18+
19+
20+
import graphql.schema.DataFetchingEnvironment;
21+
22+
import org.springframework.core.MethodParameter;
23+
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
24+
import org.springframework.graphql.data.pagination.CursorStrategy;
25+
import org.springframework.graphql.data.pagination.PaginationRequest;
26+
import org.springframework.lang.Nullable;
27+
import org.springframework.util.Assert;
28+
29+
/**
30+
* Resolver for a method argument of type {@link PaginationRequest} initialized
31+
* from "first", "last", "before", and "after" GraphQL arguments.
32+
*
33+
* @author Rossen Stoyanchev
34+
* @since 1.2
35+
*/
36+
public class PaginationRequestMethodArgumentResolver<P> implements HandlerMethodArgumentResolver {
37+
38+
private final CursorStrategy<P> cursorStrategy;
39+
40+
41+
public PaginationRequestMethodArgumentResolver(CursorStrategy<P> cursorStrategy) {
42+
Assert.notNull(cursorStrategy, "CursorStrategy is required");
43+
this.cursorStrategy = cursorStrategy;
44+
}
45+
46+
47+
@Override
48+
public boolean supportsParameter(MethodParameter parameter) {
49+
return (parameter.getParameterType().equals(PaginationRequest.class) &&
50+
this.cursorStrategy.supports(parameter.nested().getNestedParameterType()));
51+
}
52+
53+
@Override
54+
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) throws Exception {
55+
boolean forward = !environment.getArguments().containsKey("last");
56+
String cursor = environment.getArgument(forward ? "before" : "after");
57+
Integer count = environment.getArgument(forward ? "first" : "last");
58+
P position = (cursor != null ? this.cursorStrategy.fromCursor(cursor) : null);
59+
return createRequest(position, count, forward);
60+
}
61+
62+
/**
63+
* Create the {@code PaginationRequest} instance.
64+
*/
65+
protected PaginationRequest<P> createRequest(@Nullable P position, @Nullable Integer size, boolean forward) {
66+
return new PaginationRequest<>(position, size, forward);
67+
}
68+
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2020-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.data.method.annotation.support;
18+
19+
20+
import org.springframework.core.MethodParameter;
21+
import org.springframework.data.domain.ScrollPosition;
22+
import org.springframework.graphql.data.pagination.CursorStrategy;
23+
import org.springframework.graphql.data.query.ScrollRequest;
24+
import org.springframework.lang.Nullable;
25+
26+
27+
/**
28+
* Subclass of {@link PaginationRequestMethodArgumentResolver} that supports
29+
* {@link ScrollRequest} with cursors converted to {@link ScrollPosition} for
30+
* forward or backward pagination.
31+
*
32+
* @author Rossen Stoyanchev
33+
* @since 1.2
34+
*/
35+
public class ScrollRequestMethodArgumentResolver extends PaginationRequestMethodArgumentResolver<ScrollPosition> {
36+
37+
38+
public ScrollRequestMethodArgumentResolver(CursorStrategy<ScrollPosition> cursorStrategy) {
39+
super(cursorStrategy);
40+
}
41+
42+
43+
@Override
44+
public boolean supportsParameter(MethodParameter parameter) {
45+
return parameter.getParameterType().equals(ScrollRequest.class);
46+
}
47+
48+
protected ScrollRequest createRequest(@Nullable ScrollPosition position, @Nullable Integer size, boolean forward) {
49+
return new ScrollRequest(position, size, forward);
50+
}
51+
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2020-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.data.method.annotation.support;
18+
19+
20+
import graphql.schema.DataFetchingEnvironment;
21+
22+
import org.springframework.core.MethodParameter;
23+
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
24+
import org.springframework.graphql.data.pagination.SortStrategy;
25+
import org.springframework.util.Assert;
26+
27+
28+
/**
29+
* Resolver for a Sort object decoded with {@link SortStrategy}.
30+
*
31+
* @author Rossen Stoyanchev
32+
* @since 1.2
33+
*/
34+
public class SortMethodArgumentResolver implements HandlerMethodArgumentResolver {
35+
36+
private final SortStrategy<?> sortStrategy;
37+
38+
39+
public SortMethodArgumentResolver(SortStrategy<?> sortStrategy) {
40+
Assert.notNull(sortStrategy, "SortStrategy is required");
41+
this.sortStrategy = sortStrategy;
42+
}
43+
44+
45+
@Override
46+
public boolean supportsParameter(MethodParameter parameter) {
47+
return this.sortStrategy.supports(parameter.getParameterType());
48+
}
49+
50+
@Override
51+
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) {
52+
return this.sortStrategy.extract(environment);
53+
}
54+
55+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2020-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.data.pagination;
18+
19+
20+
import java.nio.charset.Charset;
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.Base64;
23+
24+
25+
/**
26+
* {@link CursorEncoder} that applies Base 64 encoding and decoding.
27+
*
28+
* @author Rossen Stoyanchev
29+
* @since 1.2
30+
*/
31+
final class Base64CursorEncoder implements CursorEncoder {
32+
33+
private final Charset charset = StandardCharsets.UTF_8;
34+
35+
36+
@Override
37+
public String encode(String cursor) {
38+
byte[] bytes = Base64.getEncoder().encode(cursor.getBytes(this.charset));
39+
return new String(bytes, this.charset);
40+
}
41+
42+
@Override
43+
public String decode(String cursor) {
44+
byte[] bytes = Base64.getDecoder().decode(cursor.getBytes(this.charset));
45+
return new String(bytes, this.charset);
46+
}
47+
48+
}

0 commit comments

Comments
 (0)