Skip to content

Commit d017506

Browse files
committed
Pagination test with controller method
See gh-620
1 parent 6630ca0 commit d017506

File tree

4 files changed

+282
-4
lines changed

4 files changed

+282
-4
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ public boolean supportsParameter(MethodParameter parameter) {
5353
@Override
5454
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) throws Exception {
5555
boolean forward = !environment.getArguments().containsKey("last");
56-
String cursor = environment.getArgument(forward ? "before" : "after");
5756
Integer count = environment.getArgument(forward ? "first" : "last");
57+
String cursor = environment.getArgument(forward ? "after" : "before");
5858
P position = (cursor != null ? this.cursorStrategy.fromCursor(cursor) : null);
5959
return createRequest(position, count, forward);
6060
}

spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentResolverTestSupport.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -39,8 +39,7 @@
3939
*/
4040
class ArgumentResolverTestSupport {
4141

42-
private static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE =
43-
new TypeReference<Map<String, Object>>() {};
42+
private static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeReference<>() {};
4443

4544

4645
private final ObjectMapper mapper = new ObjectMapper();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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+
import java.util.Map;
20+
21+
import graphql.schema.DataFetchingEnvironment;
22+
import graphql.schema.DataFetchingEnvironmentImpl;
23+
import org.junit.jupiter.api.Test;
24+
25+
import org.springframework.core.MethodParameter;
26+
import org.springframework.data.domain.Window;
27+
import org.springframework.graphql.Book;
28+
import org.springframework.graphql.data.method.annotation.QueryMapping;
29+
import org.springframework.graphql.data.pagination.CursorStrategy;
30+
import org.springframework.graphql.data.pagination.PaginationRequest;
31+
import org.springframework.stereotype.Controller;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
35+
/**
36+
* Unit tests for {@link PaginationRequestMethodArgumentResolver}.
37+
* @author Rossen Stoyanchev
38+
*/
39+
public class PaginationRequestMethodArgumentResolverTests extends ArgumentResolverTestSupport {
40+
41+
private final PaginationRequestMethodArgumentResolver<MyPosition> resolver =
42+
new PaginationRequestMethodArgumentResolver<>(new MyPositionCursorStrategy());
43+
44+
private final MethodParameter param =
45+
methodParam(BookController.class, "getBooks", PaginationRequest.class);
46+
47+
48+
@Test
49+
void supports() {
50+
assertThat(this.resolver.supportsParameter(this.param)).isTrue();
51+
52+
MethodParameter param = methodParam(BookController.class, "getBooksWithUnknownPosition", PaginationRequest.class);
53+
assertThat(this.resolver.supportsParameter(param)).isFalse();
54+
}
55+
56+
@Test
57+
void forwardPagination() throws Exception {
58+
int count = 10;
59+
int index = 25;
60+
Map<String, Object> arguments = Map.of("first", count, "after", String.valueOf(index));
61+
Object result = this.resolver.resolveArgument(this.param, environment(arguments));
62+
63+
testRequest(count, index, result, true);
64+
}
65+
66+
@Test
67+
void backwardPagination() throws Exception {
68+
int count = 20;
69+
int index = 100;
70+
Map<String, Object> arguments = Map.of("last", count, "before", String.valueOf(index));
71+
Object result = this.resolver.resolveArgument(this.param, environment(arguments));
72+
73+
testRequest(count, index, result, false);
74+
}
75+
76+
private static void testRequest(int count, int index, Object result, boolean forward) {
77+
PaginationRequest<MyPosition> request = (PaginationRequest<MyPosition>) result;
78+
assertThat(request.position().get().index()).isEqualTo(index);
79+
assertThat(request.count().get()).isEqualTo(count);
80+
assertThat(request.forward()).isEqualTo(forward);
81+
}
82+
83+
private static DataFetchingEnvironment environment(Map<String, Object> arguments) {
84+
return DataFetchingEnvironmentImpl.newDataFetchingEnvironment().arguments(arguments).build();
85+
}
86+
87+
88+
@SuppressWarnings("unused")
89+
@Controller
90+
private static class BookController {
91+
92+
@QueryMapping
93+
public Window<Book> getBooks(PaginationRequest<MyPosition> request) {
94+
return null;
95+
}
96+
97+
@QueryMapping
98+
public Window<Book> getBooksWithUnknownPosition(PaginationRequest<UnknownPosition> request) {
99+
return null;
100+
}
101+
102+
}
103+
104+
105+
private static class MyPositionCursorStrategy implements CursorStrategy<MyPosition> {
106+
107+
108+
@Override
109+
public boolean supports(Class<?> targetType) {
110+
return targetType.equals(MyPosition.class);
111+
}
112+
113+
@Override
114+
public String toCursor(MyPosition position) {
115+
return String.valueOf(position.index());
116+
}
117+
118+
@Override
119+
public MyPosition fromCursor(String cursor) {
120+
return new MyPosition(Integer.parseInt(cursor));
121+
}
122+
123+
}
124+
125+
126+
private record MyPosition(int index) {
127+
}
128+
129+
130+
private static class UnknownPosition {
131+
}
132+
133+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright 2002-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+
package org.springframework.graphql.data.method.annotation.support;
17+
18+
import java.util.List;
19+
import java.util.function.BiConsumer;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;
23+
24+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
25+
import org.springframework.data.domain.OffsetScrollPosition;
26+
import org.springframework.data.domain.Window;
27+
import org.springframework.graphql.Book;
28+
import org.springframework.graphql.BookSource;
29+
import org.springframework.graphql.ExecutionGraphQlResponse;
30+
import org.springframework.graphql.ExecutionGraphQlService;
31+
import org.springframework.graphql.GraphQlSetup;
32+
import org.springframework.graphql.TestExecutionRequest;
33+
import org.springframework.graphql.data.method.annotation.QueryMapping;
34+
import org.springframework.graphql.data.pagination.ConnectionFieldTypeVisitor;
35+
import org.springframework.graphql.data.query.ScrollPositionCursorStrategy;
36+
import org.springframework.graphql.data.query.ScrollRequest;
37+
import org.springframework.graphql.data.query.WindowConnectionAdapter;
38+
import org.springframework.graphql.execution.ConnectionTypeGenerator;
39+
import org.springframework.stereotype.Controller;
40+
41+
import static org.assertj.core.api.Assertions.assertThat;
42+
43+
/**
44+
* GraphQL paginated requests handled through {@code @SchemaMapping} methods.
45+
*
46+
* @author Rossen Stoyanchev
47+
*/
48+
public class SchemaMappingPaginationTests {
49+
50+
private static final String SCHEMA = """
51+
type Query {
52+
books(first:Int, after:String): BookConnection
53+
}
54+
type Book {
55+
id: ID
56+
name: String
57+
}
58+
""";
59+
60+
61+
@Test
62+
void forwardPagination() throws Exception {
63+
64+
String document = """
65+
{
66+
books(first:2, after:"O_3") {
67+
edges {
68+
cursor,
69+
node {
70+
id
71+
name
72+
}
73+
}
74+
pageInfo {
75+
startCursor,
76+
endCursor,
77+
hasPreviousPage,
78+
hasNextPage
79+
}
80+
}
81+
}
82+
""";
83+
84+
ExecutionGraphQlService graphQlService = graphQlService((configurer, setup) -> {
85+
86+
ConnectionTypeGenerator typeGenerator = new ConnectionTypeGenerator();
87+
setup.typeDefinitionRegistryConfigurer(typeGenerator::generateConnectionTypes);
88+
89+
ScrollPositionCursorStrategy cursorStrategy = new ScrollPositionCursorStrategy();
90+
WindowConnectionAdapter connectionAdapter = new WindowConnectionAdapter(cursorStrategy);
91+
setup.typeVisitor(ConnectionFieldTypeVisitor.create(List.of(connectionAdapter)));
92+
93+
configurer.setCursorStrategy(cursorStrategy);
94+
});
95+
96+
ExecutionGraphQlResponse response =
97+
graphQlService.execute(TestExecutionRequest.forDocument(document)).block();
98+
99+
assertThat(new ObjectMapper().writeValueAsString(response.getData()))
100+
.as("Errors: " + response.getErrors()).isEqualTo(
101+
"{\"books\":{" +
102+
"\"edges\":[" +
103+
"{\"cursor\":\"O_0\",\"node\":{\"id\":\"4\",\"name\":\"To The Lighthouse\"}}," +
104+
"{\"cursor\":\"O_1\",\"node\":{\"id\":\"5\",\"name\":\"Animal Farm\"}}" +
105+
"]," +
106+
"\"pageInfo\":{" +
107+
"\"startCursor\":\"O_0\"," +
108+
"\"endCursor\":\"O_1\"," +
109+
"\"hasPreviousPage\":false," +
110+
"\"hasNextPage\":false" +
111+
"}}}");
112+
}
113+
114+
private ExecutionGraphQlService graphQlService(BiConsumer<AnnotatedControllerConfigurer, GraphQlSetup> consumer) {
115+
116+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
117+
context.register(BookController.class);
118+
context.refresh();
119+
120+
AnnotatedControllerConfigurer configurer = new AnnotatedControllerConfigurer();
121+
configurer.setApplicationContext(context);
122+
123+
GraphQlSetup setup = GraphQlSetup.schemaContent(this.SCHEMA).runtimeWiring(configurer);
124+
consumer.accept(configurer, setup);
125+
126+
configurer.afterPropertiesSet();
127+
128+
return setup.toGraphQlService();
129+
}
130+
131+
132+
@SuppressWarnings("unused")
133+
@Controller
134+
private static class BookController {
135+
136+
@QueryMapping
137+
public Window<Book> books(ScrollRequest request) {
138+
int offset = (int) ((OffsetScrollPosition) request.position().get()).getOffset();
139+
int count = request.count().get();
140+
List<Book> books = BookSource.books().subList(offset, offset + count);
141+
return Window.from(books, OffsetScrollPosition::of);
142+
}
143+
144+
}
145+
146+
}

0 commit comments

Comments
 (0)