Pagination test with controller method
See gh-620
This commit is contained in:
@@ -53,8 +53,8 @@ public class PaginationRequestMethodArgumentResolver<P> implements HandlerMethod
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) throws Exception {
|
||||
boolean forward = !environment.getArguments().containsKey("last");
|
||||
String cursor = environment.getArgument(forward ? "before" : "after");
|
||||
Integer count = environment.getArgument(forward ? "first" : "last");
|
||||
String cursor = environment.getArgument(forward ? "after" : "before");
|
||||
P position = (cursor != null ? this.cursorStrategy.fromCursor(cursor) : null);
|
||||
return createRequest(position, count, forward);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -39,8 +39,7 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
class ArgumentResolverTestSupport {
|
||||
|
||||
private static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE =
|
||||
new TypeReference<Map<String, Object>>() {};
|
||||
private static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeReference<>() {};
|
||||
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.graphql.data.method.annotation.support;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import graphql.schema.DataFetchingEnvironmentImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.data.domain.Window;
|
||||
import org.springframework.graphql.Book;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.graphql.data.pagination.CursorStrategy;
|
||||
import org.springframework.graphql.data.pagination.PaginationRequest;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link PaginationRequestMethodArgumentResolver}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class PaginationRequestMethodArgumentResolverTests extends ArgumentResolverTestSupport {
|
||||
|
||||
private final PaginationRequestMethodArgumentResolver<MyPosition> resolver =
|
||||
new PaginationRequestMethodArgumentResolver<>(new MyPositionCursorStrategy());
|
||||
|
||||
private final MethodParameter param =
|
||||
methodParam(BookController.class, "getBooks", PaginationRequest.class);
|
||||
|
||||
|
||||
@Test
|
||||
void supports() {
|
||||
assertThat(this.resolver.supportsParameter(this.param)).isTrue();
|
||||
|
||||
MethodParameter param = methodParam(BookController.class, "getBooksWithUnknownPosition", PaginationRequest.class);
|
||||
assertThat(this.resolver.supportsParameter(param)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardPagination() throws Exception {
|
||||
int count = 10;
|
||||
int index = 25;
|
||||
Map<String, Object> arguments = Map.of("first", count, "after", String.valueOf(index));
|
||||
Object result = this.resolver.resolveArgument(this.param, environment(arguments));
|
||||
|
||||
testRequest(count, index, result, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void backwardPagination() throws Exception {
|
||||
int count = 20;
|
||||
int index = 100;
|
||||
Map<String, Object> arguments = Map.of("last", count, "before", String.valueOf(index));
|
||||
Object result = this.resolver.resolveArgument(this.param, environment(arguments));
|
||||
|
||||
testRequest(count, index, result, false);
|
||||
}
|
||||
|
||||
private static void testRequest(int count, int index, Object result, boolean forward) {
|
||||
PaginationRequest<MyPosition> request = (PaginationRequest<MyPosition>) result;
|
||||
assertThat(request.position().get().index()).isEqualTo(index);
|
||||
assertThat(request.count().get()).isEqualTo(count);
|
||||
assertThat(request.forward()).isEqualTo(forward);
|
||||
}
|
||||
|
||||
private static DataFetchingEnvironment environment(Map<String, Object> arguments) {
|
||||
return DataFetchingEnvironmentImpl.newDataFetchingEnvironment().arguments(arguments).build();
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Controller
|
||||
private static class BookController {
|
||||
|
||||
@QueryMapping
|
||||
public Window<Book> getBooks(PaginationRequest<MyPosition> request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public Window<Book> getBooksWithUnknownPosition(PaginationRequest<UnknownPosition> request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class MyPositionCursorStrategy implements CursorStrategy<MyPosition> {
|
||||
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> targetType) {
|
||||
return targetType.equals(MyPosition.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toCursor(MyPosition position) {
|
||||
return String.valueOf(position.index());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MyPosition fromCursor(String cursor) {
|
||||
return new MyPosition(Integer.parseInt(cursor));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private record MyPosition(int index) {
|
||||
}
|
||||
|
||||
|
||||
private static class UnknownPosition {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.graphql.data.method.annotation.support;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.data.domain.OffsetScrollPosition;
|
||||
import org.springframework.data.domain.Window;
|
||||
import org.springframework.graphql.Book;
|
||||
import org.springframework.graphql.BookSource;
|
||||
import org.springframework.graphql.ExecutionGraphQlResponse;
|
||||
import org.springframework.graphql.ExecutionGraphQlService;
|
||||
import org.springframework.graphql.GraphQlSetup;
|
||||
import org.springframework.graphql.TestExecutionRequest;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.graphql.data.pagination.ConnectionFieldTypeVisitor;
|
||||
import org.springframework.graphql.data.query.ScrollPositionCursorStrategy;
|
||||
import org.springframework.graphql.data.query.ScrollRequest;
|
||||
import org.springframework.graphql.data.query.WindowConnectionAdapter;
|
||||
import org.springframework.graphql.execution.ConnectionTypeGenerator;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* GraphQL paginated requests handled through {@code @SchemaMapping} methods.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class SchemaMappingPaginationTests {
|
||||
|
||||
private static final String SCHEMA = """
|
||||
type Query {
|
||||
books(first:Int, after:String): BookConnection
|
||||
}
|
||||
type Book {
|
||||
id: ID
|
||||
name: String
|
||||
}
|
||||
""";
|
||||
|
||||
|
||||
@Test
|
||||
void forwardPagination() throws Exception {
|
||||
|
||||
String document = """
|
||||
{
|
||||
books(first:2, after:"O_3") {
|
||||
edges {
|
||||
cursor,
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
startCursor,
|
||||
endCursor,
|
||||
hasPreviousPage,
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
ExecutionGraphQlService graphQlService = graphQlService((configurer, setup) -> {
|
||||
|
||||
ConnectionTypeGenerator typeGenerator = new ConnectionTypeGenerator();
|
||||
setup.typeDefinitionRegistryConfigurer(typeGenerator::generateConnectionTypes);
|
||||
|
||||
ScrollPositionCursorStrategy cursorStrategy = new ScrollPositionCursorStrategy();
|
||||
WindowConnectionAdapter connectionAdapter = new WindowConnectionAdapter(cursorStrategy);
|
||||
setup.typeVisitor(ConnectionFieldTypeVisitor.create(List.of(connectionAdapter)));
|
||||
|
||||
configurer.setCursorStrategy(cursorStrategy);
|
||||
});
|
||||
|
||||
ExecutionGraphQlResponse response =
|
||||
graphQlService.execute(TestExecutionRequest.forDocument(document)).block();
|
||||
|
||||
assertThat(new ObjectMapper().writeValueAsString(response.getData()))
|
||||
.as("Errors: " + response.getErrors()).isEqualTo(
|
||||
"{\"books\":{" +
|
||||
"\"edges\":[" +
|
||||
"{\"cursor\":\"O_0\",\"node\":{\"id\":\"4\",\"name\":\"To The Lighthouse\"}}," +
|
||||
"{\"cursor\":\"O_1\",\"node\":{\"id\":\"5\",\"name\":\"Animal Farm\"}}" +
|
||||
"]," +
|
||||
"\"pageInfo\":{" +
|
||||
"\"startCursor\":\"O_0\"," +
|
||||
"\"endCursor\":\"O_1\"," +
|
||||
"\"hasPreviousPage\":false," +
|
||||
"\"hasNextPage\":false" +
|
||||
"}}}");
|
||||
}
|
||||
|
||||
private ExecutionGraphQlService graphQlService(BiConsumer<AnnotatedControllerConfigurer, GraphQlSetup> consumer) {
|
||||
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
context.register(BookController.class);
|
||||
context.refresh();
|
||||
|
||||
AnnotatedControllerConfigurer configurer = new AnnotatedControllerConfigurer();
|
||||
configurer.setApplicationContext(context);
|
||||
|
||||
GraphQlSetup setup = GraphQlSetup.schemaContent(this.SCHEMA).runtimeWiring(configurer);
|
||||
consumer.accept(configurer, setup);
|
||||
|
||||
configurer.afterPropertiesSet();
|
||||
|
||||
return setup.toGraphQlService();
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Controller
|
||||
private static class BookController {
|
||||
|
||||
@QueryMapping
|
||||
public Window<Book> books(ScrollRequest request) {
|
||||
int offset = (int) ((OffsetScrollPosition) request.position().get()).getOffset();
|
||||
int count = request.count().get();
|
||||
List<Book> books = BookSource.books().subList(offset, offset + count);
|
||||
return Window.from(books, OffsetScrollPosition::of);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user