From 98172306387326881ed58bf410d8e076d76b3fec Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 20 Feb 2024 21:13:37 +0000 Subject: [PATCH 1/2] Adapt offset scroll from inclusive to exclusive Both forward and backward scrolling needed adjustment. For forward, we were not advancing by 1. For backward, we were advancing by the count and 1 more than necessary. Closes gh-916 --- .../graphql/data/query/ScrollSubrange.java | 38 +++++++---- .../support/SchemaMappingPaginationTests.java | 4 +- .../data/query/QuerydslDataFetcherTests.java | 8 +-- .../data/query/RepositoryUtilsTests.java | 8 +-- .../data/query/ScrollSubrangeTests.java | 68 +++++++++++-------- .../QueryByExampleDataFetcherJpaTests.java | 2 +- ...QueryByExampleDataFetcherMongoDbTests.java | 2 +- ...xampleDataFetcherReactiveMongoDbTests.java | 2 +- .../QueryByExampleDataFetcherNeo4jTests.java | 2 +- ...xampleDataFetcherReactiveNeo4jDbTests.java | 2 +- 10 files changed, 81 insertions(+), 55 deletions(-) diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/query/ScrollSubrange.java b/spring-graphql/src/main/java/org/springframework/graphql/data/query/ScrollSubrange.java index 3cd347d3..4aefdcf6 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/query/ScrollSubrange.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/query/ScrollSubrange.java @@ -71,16 +71,20 @@ public final class ScrollSubrange extends Subrange { /** - * Create a {@link ScrollSubrange} instance. - * @param position the position relative to which to scroll, or {@code null} - * for scrolling from the beginning - * @param count the number of elements requested - * @param forward whether to return elements after (true) or before (false) - * the element at the given position - * @return the created subrange + * Create a {@link ScrollSubrange} from the given inputs. + *

Pagination with offset-based scrolling is always forward and inclusive + * of the referenced item. Therefore, an {@link OffsetScrollPosition} is + * adjusted as follows. For forward pagination, advanced by 1. For backward + * pagination, advanced back by the count, and switched to forward. + * @param position the reference position, or {@code null} if not specified + * @param count how many to return, or {@code null} if not specified + * @param forward whether scroll forward (true) or backward (false) + * @return the created instance * @since 1.2.4 */ - public static ScrollSubrange create(@Nullable ScrollPosition position, @Nullable Integer count, boolean forward) { + public static ScrollSubrange create( + @Nullable ScrollPosition position, @Nullable Integer count, boolean forward) { + if (count != null && count < 0) { count = null; } @@ -98,16 +102,24 @@ public final class ScrollSubrange extends Subrange { private static ScrollSubrange initFromOffsetPosition( OffsetScrollPosition position, @Nullable Integer count, boolean forward) { - if (!forward) { + // Offset is inclusive, adapt to exclusive: + // - for forward, add 1 to return items after position + // - for backward, subtract count to get items before position + + if (forward) { + position = position.advanceBy(1); + } + else { int countOrZero = (count != null ? count : 0); - if (countOrZero < position.getOffset()) { - position = position.advanceBy(-countOrZero-1); + if (position.getOffset() >= countOrZero) { + position = position.advanceBy(-countOrZero); } else { - count = (position.getOffset() > 0 ? (int) (position.getOffset() - 1) : 0); - position = null; + count = (int) position.getOffset(); + position = ScrollPosition.offset(); } } + return new ScrollSubrange(position, count, true, null); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingPaginationTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingPaginationTests.java index 8008091a..04e2f097 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingPaginationTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingPaginationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -49,7 +49,7 @@ public class SchemaMappingPaginationTests { @Test void forwardPagination() { - String document = BookSource.booksConnectionQuery("first:2, after:\"O_3\""); + String document = BookSource.booksConnectionQuery("first:2, after:\"O_2\""); Mono response = graphQlService().execute(document); ResponseHelper.forResponse(response).assertData( diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/query/QuerydslDataFetcherTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/query/QuerydslDataFetcherTests.java index b8aa82af..0eb54c67 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/query/QuerydslDataFetcherTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/query/QuerydslDataFetcherTests.java @@ -139,12 +139,12 @@ class QuerydslDataFetcherTests { ResponseHelper.forResponse(response).assertData( "{\"books\":{" + "\"edges\":[" + - "{\"cursor\":\"O_4\",\"node\":{\"id\":\"42\",\"name\":\"Hitchhiker's Guide to the Galaxy\"}}," + - "{\"cursor\":\"O_5\",\"node\":{\"id\":\"53\",\"name\":\"Breaking Bad\"}}" + + "{\"cursor\":\"O_0\",\"node\":{\"id\":\"42\",\"name\":\"Hitchhiker's Guide to the Galaxy\"}}," + + "{\"cursor\":\"O_1\",\"node\":{\"id\":\"53\",\"name\":\"Breaking Bad\"}}" + "]," + "\"pageInfo\":{" + - "\"startCursor\":\"O_4\"," + - "\"endCursor\":\"O_5\"," + + "\"startCursor\":\"O_0\"," + + "\"endCursor\":\"O_1\"," + "\"hasPreviousPage\":true," + "\"hasNextPage\":false" + "}}}" diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/query/RepositoryUtilsTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/query/RepositoryUtilsTests.java index 0a523e3b..11a8acc4 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/query/RepositoryUtilsTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/query/RepositoryUtilsTests.java @@ -50,7 +50,7 @@ public class RepositoryUtilsTests { ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange); - assertThat(range.position().get()).isEqualTo(offset); + assertThat(range.position().get()).isEqualTo(ScrollPosition.offset(11)); assertThat(range.count().getAsInt()).isEqualTo(count); assertThat(range.forward()).isTrue(); } @@ -65,7 +65,7 @@ public class RepositoryUtilsTests { ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange); - assertThat(range.position().get()).isEqualTo(offset.advanceBy(-count-1)); + assertThat(range.position().get()).isEqualTo(ScrollPosition.offset(5)); assertThat(range.count().getAsInt()).isEqualTo(count); assertThat(range.forward()).isTrue(); } @@ -75,7 +75,7 @@ public class RepositoryUtilsTests { DataFetchingEnvironment env = environment(Collections.emptyMap()); ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange); - assertThat(range.position().get()).isEqualTo(getDefaultPosition()); + assertThat(range.position().get()).isEqualTo(getDefaultPosition().advanceBy(1)); assertThat(range.count().getAsInt()).isEqualTo(this.defaultSubrange.count().getAsInt()); assertThat(range.forward()).isTrue(); } @@ -86,7 +86,7 @@ public class RepositoryUtilsTests { DataFetchingEnvironment env = environment(Map.of("last", count)); ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange); - assertThat(range.position().get()).isEqualTo(getDefaultPosition().advanceBy(-count-1)); + assertThat(range.position().get()).isEqualTo(getDefaultPosition().advanceBy(-count)); assertThat(range.count().getAsInt()).isEqualTo(count); assertThat(range.forward()).isTrue(); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/query/ScrollSubrangeTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/query/ScrollSubrangeTests.java index 0776e8ac..c8babeea 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/query/ScrollSubrangeTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/query/ScrollSubrangeTests.java @@ -37,40 +37,53 @@ import static org.assertj.core.api.Assertions.assertThat; public class ScrollSubrangeTests { @Test - void offset() { - ScrollPosition position = ScrollPosition.offset(30); + void offsetForward() { int count = 10; + ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.offset(30), count, true); - ScrollSubrange subrange = ScrollSubrange.create(position, count, true); - assertThat(((OffsetScrollPosition) subrange.position().get())).isEqualTo(position); - assertThat(subrange.count().orElse(0)).isEqualTo(count); - assertThat(subrange.forward()).isTrue(); - - subrange = ScrollSubrange.create(position, count, false); - assertThat(((OffsetScrollPosition) subrange.position().get()).getOffset()).isEqualTo(19); + assertThat(getOffset(subrange)).isEqualTo(31); assertThat(subrange.count().orElse(0)).isEqualTo(count); assertThat(subrange.forward()).isTrue(); } @Test - void keyset() { + void offsetBackward() { + int count = 10; + ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.offset(30), count, false); + + assertThat(getOffset(subrange)).isEqualTo(20); + assertThat(subrange.count().orElse(0)).isEqualTo(count); + assertThat(subrange.forward()).isTrue(); + } + + @Test + void keysetForward() { Map keys = new LinkedHashMap<>(); keys.put("firstName", "Joseph"); keys.put("lastName", "Heller"); keys.put("id", 103); - ScrollPosition position = ScrollPosition.forward(keys); int count = 10; + ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.forward(keys), count, true); - ScrollSubrange subrange = ScrollSubrange.create(position, count, true); KeysetScrollPosition actualPosition = (KeysetScrollPosition) subrange.position().get(); assertThat(actualPosition.getKeys()).isEqualTo(keys); assertThat(actualPosition.getDirection()).isEqualTo(Direction.FORWARD); assertThat(subrange.count().orElse(0)).isEqualTo(count); assertThat(subrange.forward()).isTrue(); + } - subrange = ScrollSubrange.create(position, count, false); - actualPosition = (KeysetScrollPosition) subrange.position().get(); + @Test + void keysetBackward() { + Map keys = new LinkedHashMap<>(); + keys.put("firstName", "Joseph"); + keys.put("lastName", "Heller"); + keys.put("id", 103); + + int count = 10; + ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.forward(keys), count, false); + + KeysetScrollPosition actualPosition = (KeysetScrollPosition) subrange.position().get(); assertThat(actualPosition.getKeys()).isEqualTo(keys); assertThat(actualPosition.getDirection()).isEqualTo(Direction.BACKWARD); assertThat(subrange.count().orElse(0)).isEqualTo(count); @@ -87,33 +100,34 @@ public class ScrollSubrangeTests { } @Test - void offsetBackwardPaginationWithInsufficientCount() { - ScrollPosition position = ScrollPosition.offset(5); - ScrollSubrange subrange = ScrollSubrange.create(position, 10, false); + void offsetBackwardWithInsufficientCount() { + ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.offset(5), 10, false); - assertThat(subrange.position()).isNotPresent(); - assertThat(subrange.count().getAsInt()).isEqualTo(4); + assertThat(getOffset(subrange)).isEqualTo(0); + assertThat(subrange.count().getAsInt()).isEqualTo(5); assertThat(subrange.forward()).isTrue(); } @Test - void offsetBackwardPaginationWithOffsetZero() { - ScrollPosition position = ScrollPosition.offset(0); - ScrollSubrange subrange = ScrollSubrange.create(position, 10, false); + void offsetBackwardFromInitialOffset() { + ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.offset(0), 10, false); - assertThat(subrange.position()).isNotPresent(); + assertThat(getOffset(subrange)).isEqualTo(0); assertThat(subrange.count().getAsInt()).isEqualTo(0); assertThat(subrange.forward()).isTrue(); } @Test - void offsetBackwardPaginationWithNullCount() { - ScrollPosition position = ScrollPosition.offset(30); - ScrollSubrange subrange = ScrollSubrange.create(position, null, false); + void offsetBackwardWithNullCount() { + ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.offset(30), null, false); - assertThat(subrange.position()).hasValue(ScrollPosition.offset(29)); + assertThat(getOffset(subrange)).isEqualTo(30); assertThat(subrange.count()).isNotPresent(); assertThat(subrange.forward()).isTrue(); } + private static long getOffset(ScrollSubrange subrange) { + return ((OffsetScrollPosition) subrange.position().get()).getOffset(); + } + } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/query/jpa/QueryByExampleDataFetcherJpaTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/query/jpa/QueryByExampleDataFetcherJpaTests.java index 0a41f632..18d30715 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/query/jpa/QueryByExampleDataFetcherJpaTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/query/jpa/QueryByExampleDataFetcherJpaTests.java @@ -137,7 +137,7 @@ class QueryByExampleDataFetcherJpaTests { Mono response = graphQlSetup .toWebGraphQlHandler() - .handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_3\""))); + .handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_2\""))); List> edges = ResponseHelper.forResponse(response).toEntity("books.edges", List.class); assertThat(edges.size()).isEqualTo(2); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherMongoDbTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherMongoDbTests.java index 58dbf134..9cd3d855 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherMongoDbTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherMongoDbTests.java @@ -134,7 +134,7 @@ class QueryByExampleDataFetcherMongoDbTests { Mono response = graphQlSetup .toWebGraphQlHandler() - .handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_3\""))); + .handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_2\""))); List> edges = ResponseHelper.forResponse(response).toEntity("books.edges", List.class); assertThat(edges.size()).isEqualTo(2); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherReactiveMongoDbTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherReactiveMongoDbTests.java index 3019acd6..a5e32c0b 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherReactiveMongoDbTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherReactiveMongoDbTests.java @@ -159,7 +159,7 @@ class QueryByExampleDataFetcherReactiveMongoDbTests { Mono response = graphQlSetup .toWebGraphQlHandler() - .handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_3\""))); + .handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_2\""))); List> edges = ResponseHelper.forResponse(response).toEntity("books.edges", List.class); assertThat(edges.size()).isEqualTo(2); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/query/neo4j/QueryByExampleDataFetcherNeo4jTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/query/neo4j/QueryByExampleDataFetcherNeo4jTests.java index 9943dd6d..406f096a 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/query/neo4j/QueryByExampleDataFetcherNeo4jTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/query/neo4j/QueryByExampleDataFetcherNeo4jTests.java @@ -136,7 +136,7 @@ class QueryByExampleDataFetcherNeo4jTests { Mono response = graphQlSetup .toWebGraphQlHandler() - .handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_3\""))); + .handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_2\""))); List> edges = ResponseHelper.forResponse(response).toEntity("books.edges", List.class); assertThat(edges.size()).isEqualTo(2); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/query/neo4j/QueryByExampleDataFetcherReactiveNeo4jDbTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/query/neo4j/QueryByExampleDataFetcherReactiveNeo4jDbTests.java index 994162d2..0361f5d1 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/query/neo4j/QueryByExampleDataFetcherReactiveNeo4jDbTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/query/neo4j/QueryByExampleDataFetcherReactiveNeo4jDbTests.java @@ -172,7 +172,7 @@ class QueryByExampleDataFetcherReactiveNeo4jDbTests { Mono response = graphQlSetup .toWebGraphQlHandler() - .handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_3\""))); + .handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_2\""))); List> edges = ResponseHelper.forResponse(response).toEntity("books.edges", List.class); assertThat(edges.size()).isEqualTo(2); From 248cfe22bacb51371abe4813a179f8c882a90da1 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 20 Feb 2024 22:52:08 +0000 Subject: [PATCH 2/2] Revise default ScrollSubrange for QueryDsl and QBE The QueryDsl and QBE data fetcher builders now accept a default count and a function for a default position with awareness of the scroll direction. Closes gh-917 --- .../data/query/QueryByExampleDataFetcher.java | 154 +++++++++++----- .../data/query/QuerydslDataFetcher.java | 168 +++++++++++++----- .../graphql/data/query/RepositoryUtils.java | 20 ++- .../data/query/RepositoryUtilsTests.java | 39 ++-- 4 files changed, 264 insertions(+), 117 deletions(-) diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/query/QueryByExampleDataFetcher.java b/spring-graphql/src/main/java/org/springframework/graphql/data/query/QueryByExampleDataFetcher.java index f9d90569..352a8a99 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/query/QueryByExampleDataFetcher.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/query/QueryByExampleDataFetcher.java @@ -35,6 +35,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.data.domain.Example; +import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.OffsetScrollPosition; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; @@ -370,24 +371,29 @@ public abstract class QueryByExampleDataFetcher { private final CursorStrategy cursorStrategy; @Nullable - private final ScrollSubrange defaultSubrange; + private final Integer defaultScrollCount; + + @Nullable + private final Function defaultScrollPosition; private final Sort sort; @SuppressWarnings("unchecked") Builder(QueryByExampleExecutor executor, Class domainType) { - this(executor, TypeInformation.of((Class) domainType), domainType, null, null, Sort.unsorted()); + this(executor, TypeInformation.of((Class) domainType), domainType, null, null, null, Sort.unsorted()); } Builder(QueryByExampleExecutor executor, TypeInformation domainType, Class resultType, - @Nullable CursorStrategy cursorStrategy, @Nullable ScrollSubrange defaultSubrange, + @Nullable CursorStrategy cursorStrategy, + @Nullable Integer defaultScrollCount, @Nullable Function defaultScrollPosition, Sort sort) { this.executor = executor; this.domainType = domainType; this.resultType = resultType; this.cursorStrategy = cursorStrategy; - this.defaultSubrange = defaultSubrange; + this.defaultScrollCount = defaultScrollCount; + this.defaultScrollPosition = defaultScrollPosition; this.sort = sort; } @@ -402,8 +408,8 @@ public abstract class QueryByExampleDataFetcher { */ public

Builder projectAs(Class

projectionType) { Assert.notNull(projectionType, "Projection type must not be null"); - return new Builder<>(this.executor, this.domainType, - projectionType, this.cursorStrategy, this.defaultSubrange, this.sort); + return new Builder<>(this.executor, this.domainType, projectionType, + this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, this.sort); } /** @@ -416,8 +422,26 @@ public abstract class QueryByExampleDataFetcher { * @since 1.2.0 */ public Builder cursorStrategy(@Nullable CursorStrategy cursorStrategy) { + return new Builder<>(this.executor, this.domainType, this.resultType, + cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, this.sort); + } + + /** + * Configure a default scroll count to use, and function to return a default + * {@link ScrollPosition} for forward vs backward pagination. + *

For offset scrolling, use {@link ScrollPosition#offset()} to scroll + * from the beginning. Currently, it is not possible to go back from the end. + *

For keyset scrolling, use {@link ScrollPosition#keyset()} to scroll + * from the beginning, or {@link KeysetScrollPosition#reverse()} the same + * to go back from the end. + *

By default a count of 20 and {@link ScrollPosition#offset()} are used. + * @since 1.2.5 + */ + public Builder defaultScrollSubrange( + int defaultCount, Function defaultPosition) { + return new Builder<>(this.executor, this.domainType, - this.resultType, cursorStrategy, this.defaultSubrange, this.sort); + this.resultType, this.cursorStrategy, defaultCount, defaultPosition, this.sort); } /** @@ -427,11 +451,16 @@ public abstract class QueryByExampleDataFetcher { * count of 20. * @return a new {@link Builder} instance with all previously configured * options and {@code Sort} applied - * @since 1.2.0 + * @deprecated in favor of {@link #defaultScrollSubrange(int, Function)} */ + @SuppressWarnings("OptionalGetWithoutIsPresent") + @Deprecated(since = "1.2.5", forRemoval = true) public Builder defaultScrollSubrange(@Nullable ScrollSubrange defaultSubrange) { return new Builder<>(this.executor, this.domainType, - this.resultType, this.cursorStrategy, defaultSubrange, this.sort); + this.resultType, this.cursorStrategy, + (defaultSubrange != null ? defaultSubrange.count().getAsInt() : null), + (defaultSubrange != null ? forward -> defaultSubrange.position().get() : null), + this.sort); } /** @@ -442,8 +471,8 @@ public abstract class QueryByExampleDataFetcher { */ public Builder sortBy(Sort sort) { Assert.notNull(sort, "Sort must not be null"); - return new Builder<>(this.executor, this.domainType, - this.resultType, this.cursorStrategy, this.defaultSubrange, sort); + return new Builder<>(this.executor, this.domainType, this.resultType, + this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, sort); } /** @@ -469,7 +498,8 @@ public abstract class QueryByExampleDataFetcher { return new ScrollableEntityFetcher<>( this.executor, this.domainType, this.resultType, (this.cursorStrategy != null ? this.cursorStrategy : RepositoryUtils.defaultCursorStrategy()), - (this.defaultSubrange != null ? this.defaultSubrange : RepositoryUtils.defaultScrollSubrange()), + (this.defaultScrollCount != null ? this.defaultScrollCount : RepositoryUtils.defaultScrollCount()), + (this.defaultScrollPosition != null ? this.defaultScrollPosition : RepositoryUtils.defaultScrollPosition()), this.sort); } @@ -516,25 +546,30 @@ public abstract class QueryByExampleDataFetcher { private final CursorStrategy cursorStrategy; @Nullable - private final ScrollSubrange defaultSubrange; + private final Integer defaultScrollCount; + + @Nullable + private final Function defaultScrollPosition; private final Sort sort; @SuppressWarnings("unchecked") ReactiveBuilder(ReactiveQueryByExampleExecutor executor, Class domainType) { - this(executor, TypeInformation.of((Class) domainType), domainType, null, null, Sort.unsorted()); + this(executor, TypeInformation.of((Class) domainType), domainType, null, null, null, Sort.unsorted()); } ReactiveBuilder( ReactiveQueryByExampleExecutor executor, TypeInformation domainType, Class resultType, - @Nullable CursorStrategy cursorStrategy, @Nullable ScrollSubrange defaultSubrange, + @Nullable CursorStrategy cursorStrategy, + @Nullable Integer defaultScrollCount, @Nullable Function defaultScrollPosition, Sort sort) { this.executor = executor; this.domainType = domainType; this.resultType = resultType; this.cursorStrategy = cursorStrategy; - this.defaultSubrange = defaultSubrange; + this.defaultScrollCount = defaultScrollCount; + this.defaultScrollPosition = defaultScrollPosition; this.sort = sort; } @@ -550,7 +585,7 @@ public abstract class QueryByExampleDataFetcher { public

ReactiveBuilder projectAs(Class

projectionType) { Assert.notNull(projectionType, "Projection type must not be null"); return new ReactiveBuilder<>(this.executor, this.domainType, - projectionType, this.cursorStrategy, this.defaultSubrange, this.sort); + projectionType, this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, this.sort); } /** @@ -563,8 +598,26 @@ public abstract class QueryByExampleDataFetcher { * @since 1.2.0 */ public ReactiveBuilder cursorStrategy(@Nullable CursorStrategy cursorStrategy) { + return new ReactiveBuilder<>(this.executor, this.domainType, this.resultType, + cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, this.sort); + } + + /** + * Configure a default scroll count to use, and function to return a default + * {@link ScrollPosition} for forward vs backward pagination. + *

For offset scrolling, use {@link ScrollPosition#offset()} to scroll + * from the beginning. Currently, it is not possible to go back from the end. + *

For keyset scrolling, use {@link ScrollPosition#keyset()} to scroll + * from the beginning, or {@link KeysetScrollPosition#reverse()} the same + * to go back from the end. + *

By default a count of 20 and {@link ScrollPosition#offset()} are used. + * @since 1.2.5 + */ + public ReactiveBuilder defaultScrollSubrange( + int defaultCount, Function defaultPosition) { + return new ReactiveBuilder<>(this.executor, this.domainType, - this.resultType, cursorStrategy, this.defaultSubrange, this.sort); + this.resultType, this.cursorStrategy, defaultCount, defaultPosition, this.sort); } /** @@ -574,11 +627,16 @@ public abstract class QueryByExampleDataFetcher { * count of 20. * @return a new {@link Builder} instance with all previously configured * options and {@code Sort} applied - * @since 1.2.0 + * @deprecated in favor of {@link #defaultScrollSubrange(int, Function)} */ + @SuppressWarnings("OptionalGetWithoutIsPresent") + @Deprecated(since = "1.2.5", forRemoval = true) public ReactiveBuilder defaultScrollSubrange(@Nullable ScrollSubrange defaultSubrange) { return new ReactiveBuilder<>(this.executor, this.domainType, - this.resultType, this.cursorStrategy, defaultSubrange, this.sort); + this.resultType, this.cursorStrategy, + (defaultSubrange != null ? defaultSubrange.count().getAsInt() : null), + (defaultSubrange != null ? forward -> defaultSubrange.position().get() : null), + this.sort); } /** @@ -589,8 +647,8 @@ public abstract class QueryByExampleDataFetcher { */ public ReactiveBuilder sortBy(Sort sort) { Assert.notNull(sort, "Sort must not be null"); - return new ReactiveBuilder<>(this.executor, this.domainType, - this.resultType, this.cursorStrategy, this.defaultSubrange, sort); + return new ReactiveBuilder<>(this.executor, this.domainType, this.resultType, + this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, sort); } /** @@ -616,7 +674,8 @@ public abstract class QueryByExampleDataFetcher { return new ReactiveScrollableEntityFetcher<>( this.executor, this.domainType, this.resultType, (this.cursorStrategy != null ? this.cursorStrategy : RepositoryUtils.defaultCursorStrategy()), - (this.defaultSubrange != null ? this.defaultSubrange : RepositoryUtils.defaultScrollSubrange()), + (this.defaultScrollCount != null ? this.defaultScrollCount : RepositoryUtils.defaultScrollCount()), + (this.defaultScrollPosition != null ? this.defaultScrollPosition : RepositoryUtils.defaultScrollPosition()), this.sort); } @@ -747,23 +806,27 @@ public abstract class QueryByExampleDataFetcher { private final CursorStrategy cursorStrategy; - private final ScrollSubrange defaultSubrange; + private final int defaultCount; + + private final Function defaultPosition; private final ResolvableType scrollableResultType; ScrollableEntityFetcher( QueryByExampleExecutor executor, TypeInformation domainType, Class resultType, - CursorStrategy cursorStrategy, ScrollSubrange defaultSubrange, Sort sort) { + CursorStrategy cursorStrategy, + int defaultCount, + Function defaultPosition, + Sort sort) { super(executor, domainType, resultType, sort); Assert.notNull(cursorStrategy, "CursorStrategy is required"); - Assert.notNull(defaultSubrange, "Default ScrollSubrange is required"); - Assert.isTrue(defaultSubrange.position().isPresent(), "Default ScrollPosition is required"); - Assert.isTrue(defaultSubrange.count().isPresent(), "Default scroll limit is required"); + Assert.notNull(defaultPosition, "'defaultPosition' is required"); this.cursorStrategy = cursorStrategy; - this.defaultSubrange = defaultSubrange; + this.defaultCount = defaultCount; + this.defaultPosition = defaultPosition; this.scrollableResultType = ResolvableType.forClassWithGenerics(Window.class, resultType); } @@ -772,12 +835,12 @@ public abstract class QueryByExampleDataFetcher { return ResolvableType.forClassWithGenerics(Iterable.class, this.scrollableResultType); } - @SuppressWarnings("OptionalGetWithoutIsPresent") @Override protected Iterable getResult(FluentQuery.FetchableFluentQuery queryToUse, DataFetchingEnvironment env) { - ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy, this.defaultSubrange); - int count = range.count().getAsInt(); - ScrollPosition position = range.position().get(); + ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy); + int count = range.count().orElse(this.defaultCount); + ScrollPosition position = (range.position().isPresent() ? + range.position().get() : this.defaultPosition.apply(range.forward())); return queryToUse.limit(count).scroll(position); } @@ -891,26 +954,30 @@ public abstract class QueryByExampleDataFetcher { private final CursorStrategy cursorStrategy; - private final ScrollSubrange defaultSubrange; + private final int defaultCount; + + private final Function defaultPosition; private final Sort sort; ReactiveScrollableEntityFetcher( ReactiveQueryByExampleExecutor executor, TypeInformation domainType, Class resultType, - CursorStrategy cursorStrategy, ScrollSubrange defaultSubrange, Sort sort) { + CursorStrategy cursorStrategy, + int defaultCount, + Function defaultPosition, + Sort sort) { super(domainType); Assert.notNull(cursorStrategy, "CursorStrategy is required"); - Assert.notNull(defaultSubrange, "Default ScrollSubrange is required"); - Assert.isTrue(defaultSubrange.position().isPresent(), "Default ScrollPosition is required"); - Assert.isTrue(defaultSubrange.count().isPresent(), "Default scroll limit is required"); + Assert.notNull(defaultPosition, "'defaultPosition' is required"); this.executor = executor; this.resultType = resultType; this.scrollableResultType = ResolvableType.forClassWithGenerics(Iterable.class, resultType); this.cursorStrategy = cursorStrategy; - this.defaultSubrange = defaultSubrange; + this.defaultCount = defaultCount; + this.defaultPosition = defaultPosition; this.sort = sort; } @@ -920,7 +987,7 @@ public abstract class QueryByExampleDataFetcher { } @Override - @SuppressWarnings({"unchecked", "OptionalGetWithoutIsPresent"}) + @SuppressWarnings("unchecked") public Mono> get(DataFetchingEnvironment env) throws BindException { return this.executor.findBy(buildExample(env), query -> { FluentQuery.ReactiveFluentQuery queryToUse = (FluentQuery.ReactiveFluentQuery) query; @@ -936,9 +1003,10 @@ public abstract class QueryByExampleDataFetcher { queryToUse = queryToUse.project(buildPropertyPaths(env.getSelectionSet(), this.resultType)); } - ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy, this.defaultSubrange); - int count = range.count().getAsInt(); - ScrollPosition position = range.position().get(); + ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy); + int count = range.count().orElse(this.defaultCount); + ScrollPosition position = (range.position().isPresent() ? + range.position().get() : this.defaultPosition.apply(range.forward())); return queryToUse.limit(count).scroll(position).map(Function.identity()); }); } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/query/QuerydslDataFetcher.java b/spring-graphql/src/main/java/org/springframework/graphql/data/query/QuerydslDataFetcher.java index 93813f77..532aacae 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/query/QuerydslDataFetcher.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/query/QuerydslDataFetcher.java @@ -36,6 +36,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.OffsetScrollPosition; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; @@ -412,7 +413,10 @@ public abstract class QuerydslDataFetcher { private final CursorStrategy cursorStrategy; @Nullable - private final ScrollSubrange defaultSubrange; + private final Integer defaultScrollCount; + + @Nullable + private final Function defaultScrollPosition; private final Sort sort; @@ -421,18 +425,20 @@ public abstract class QuerydslDataFetcher { @SuppressWarnings("unchecked") Builder(QuerydslPredicateExecutor executor, Class domainType) { this(executor, TypeInformation.of((Class) domainType), - domainType, null, null, Sort.unsorted(), NO_OP_BINDER_CUSTOMIZER); + domainType, null, null, null, Sort.unsorted(), NO_OP_BINDER_CUSTOMIZER); } Builder(QuerydslPredicateExecutor executor, TypeInformation domainType, Class resultType, - @Nullable CursorStrategy cursorStrategy, @Nullable ScrollSubrange defaultSubrange, + @Nullable CursorStrategy cursorStrategy, + @Nullable Integer defaultScrollCount, @Nullable Function defaultScrollPosition, Sort sort, QuerydslBinderCustomizer> customizer) { this.executor = executor; this.domainType = domainType; this.resultType = resultType; this.cursorStrategy = cursorStrategy; - this.defaultSubrange = defaultSubrange; + this.defaultScrollCount = defaultScrollCount; + this.defaultScrollPosition = defaultScrollPosition; this.sort = sort; this.customizer = customizer; } @@ -449,7 +455,8 @@ public abstract class QuerydslDataFetcher { public

Builder projectAs(Class

projectionType) { Assert.notNull(projectionType, "Projection type must not be null"); return new Builder<>(this.executor, this.domainType, projectionType, - this.cursorStrategy, this.defaultSubrange, this.sort, this.customizer); + this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, + this.sort, this.customizer); } /** @@ -463,21 +470,43 @@ public abstract class QuerydslDataFetcher { */ public Builder cursorStrategy(@Nullable CursorStrategy cursorStrategy) { return new Builder<>(this.executor, this.domainType, this.resultType, - cursorStrategy, this.defaultSubrange, this.sort, this.customizer); + cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, + this.sort, this.customizer); + } + + /** + * Configure a default scroll count to use, and function to return a default + * {@link ScrollPosition} for forward vs backward pagination. + *

For offset scrolling, use {@link ScrollPosition#offset()} to scroll + * from the beginning. Currently, it is not possible to go back from the end. + *

For keyset scrolling, use {@link ScrollPosition#keyset()} to scroll + * from the beginning, or {@link KeysetScrollPosition#reverse()} the same + * to go back from the end. + *

By default a count of 20 and {@link ScrollPosition#offset()} are used. + * @since 1.2.5 + */ + public Builder defaultScrollSubrange( + int defaultCount, Function defaultPosition) { + + return new Builder<>(this.executor, this.domainType, this.resultType, + this.cursorStrategy, defaultCount, defaultPosition, this.sort, this.customizer); } /** * Configure a {@link ScrollSubrange} to use when a paginated request does * not specify a cursor and/or a count of items. - *

By default, this is {@link OffsetScrollPosition#offset()} with a - * count of 20. - * @return a new {@link Builder} instance with all previously configured - * options and {@code Sort} applied + *

By default, this is {@link OffsetScrollPosition#offset()} with a count of 20. + * @return a new {@link Builder} instance * @since 1.2.0 + * @deprecated in favor of {@link #defaultScrollSubrange(int, Function)} */ + @SuppressWarnings("OptionalGetWithoutIsPresent") + @Deprecated(since = "1.2.5", forRemoval = true) public Builder defaultScrollSubrange(@Nullable ScrollSubrange defaultSubrange) { - return new Builder<>(this.executor, this.domainType, this.resultType, - this.cursorStrategy, defaultSubrange, this.sort, this.customizer); + return new Builder<>(this.executor, this.domainType, this.resultType, this.cursorStrategy, + (defaultSubrange != null ? defaultSubrange.count().getAsInt() : null), + (defaultSubrange != null ? forward -> defaultSubrange.position().get() : null), + this.sort, this.customizer); } /** @@ -489,7 +518,8 @@ public abstract class QuerydslDataFetcher { public Builder sortBy(Sort sort) { Assert.notNull(sort, "Sort must not be null"); return new Builder<>(this.executor, this.domainType, this.resultType, - this.cursorStrategy, this.defaultSubrange, sort, customizer); + this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, + sort, customizer); } /** @@ -508,7 +538,8 @@ public abstract class QuerydslDataFetcher { public Builder customizer(QuerydslBinderCustomizer> customizer) { Assert.notNull(customizer, "QuerydslBinderCustomizer must not be null"); return new Builder<>(this.executor, this.domainType, this.resultType, - this.cursorStrategy, this.defaultSubrange, this.sort, customizer); + this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, + this.sort, customizer); } /** @@ -536,7 +567,8 @@ public abstract class QuerydslDataFetcher { return new ScrollableEntityFetcher<>( this.executor, this.domainType, this.resultType, (this.cursorStrategy != null ? this.cursorStrategy : RepositoryUtils.defaultCursorStrategy()), - (this.defaultSubrange != null ? this.defaultSubrange : RepositoryUtils.defaultScrollSubrange()), + (this.defaultScrollCount != null ? this.defaultScrollCount : RepositoryUtils.defaultScrollCount()), + (this.defaultScrollPosition != null ? this.defaultScrollPosition : RepositoryUtils.defaultScrollPosition()), this.sort, this.customizer); } @@ -583,7 +615,10 @@ public abstract class QuerydslDataFetcher { private final CursorStrategy cursorStrategy; @Nullable - private final ScrollSubrange defaultSubrange; + private final Integer defaultScrollCount; + + @Nullable + private final Function defaultScrollPosition; private final Sort sort; @@ -592,19 +627,21 @@ public abstract class QuerydslDataFetcher { @SuppressWarnings("unchecked") ReactiveBuilder(ReactiveQuerydslPredicateExecutor executor, Class domainType) { this(executor, TypeInformation.of((Class) domainType), - domainType, null, null, Sort.unsorted(), NO_OP_BINDER_CUSTOMIZER); + domainType, null, null, null, Sort.unsorted(), NO_OP_BINDER_CUSTOMIZER); } ReactiveBuilder( ReactiveQuerydslPredicateExecutor executor, TypeInformation domainType, Class resultType, - @Nullable CursorStrategy cursorStrategy, @Nullable ScrollSubrange defaultSubrange, + @Nullable CursorStrategy cursorStrategy, + @Nullable Integer defaultScrollCount, @Nullable Function defaultScrollPosition, Sort sort, QuerydslBinderCustomizer> customizer) { this.executor = executor; this.domainType = domainType; this.resultType = resultType; this.cursorStrategy = cursorStrategy; - this.defaultSubrange = defaultSubrange; + this.defaultScrollCount = defaultScrollCount; + this.defaultScrollPosition = defaultScrollPosition; this.sort = sort; this.customizer = customizer; } @@ -621,7 +658,8 @@ public abstract class QuerydslDataFetcher { public

ReactiveBuilder projectAs(Class

projectionType) { Assert.notNull(projectionType, "Projection type must not be null"); return new ReactiveBuilder<>(this.executor, this.domainType, projectionType, - this.cursorStrategy, this.defaultSubrange, this.sort, this.customizer); + this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, + this.sort, this.customizer); } /** @@ -635,21 +673,44 @@ public abstract class QuerydslDataFetcher { */ public ReactiveBuilder cursorStrategy(@Nullable CursorStrategy cursorStrategy) { return new ReactiveBuilder<>(this.executor, this.domainType, this.resultType, - cursorStrategy, this.defaultSubrange, this.sort, this.customizer); + cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, + this.sort, this.customizer); + } + + /** + * Configure a default scroll count to use, and function to return a default + * {@link ScrollPosition} for forward vs backward pagination. + *

For offset scrolling, use {@link ScrollPosition#offset()} to scroll + * from the beginning. Currently, it is not possible to go back from the end. + *

For keyset scrolling, use {@link ScrollPosition#keyset()} to scroll + * from the beginning, or {@link KeysetScrollPosition#reverse()} the same + * to go back from the end. + *

By default a count of 20 and {@link ScrollPosition#offset()} are used. + * @since 1.2.5 + */ + public ReactiveBuilder defaultScrollSubrange( + int defaultCount, Function defaultPosition) { + + return new ReactiveBuilder<>(this.executor, this.domainType, this.resultType, + this.cursorStrategy, defaultCount, defaultPosition, this.sort, this.customizer); } /** * Configure a {@link ScrollSubrange} to use when a paginated request does * not specify a cursor and/or a count of items. - *

By default, this is {@link OffsetScrollPosition#offset()} with a - * count of 20. - * @return a new {@link Builder} instance with all previously configured - * options and {@code Sort} applied + *

By default, this is {@link OffsetScrollPosition#offset()} with a count of 20. + * @return a new {@link Builder} instance * @since 1.2.0 + * @deprecated in favor of {@link #defaultScrollSubrange(int, Function)} */ + @SuppressWarnings("OptionalGetWithoutIsPresent") + @Deprecated(since = "1.2.5", forRemoval = true) public ReactiveBuilder defaultScrollSubrange(@Nullable ScrollSubrange defaultSubrange) { return new ReactiveBuilder<>(this.executor, this.domainType, this.resultType, - this.cursorStrategy, defaultSubrange, this.sort, this.customizer); + this.cursorStrategy, + (defaultSubrange != null ? defaultSubrange.count().getAsInt() : null), + (defaultSubrange != null ? forward -> defaultSubrange.position().get() : null), + this.sort, this.customizer); } /** @@ -661,7 +722,8 @@ public abstract class QuerydslDataFetcher { public ReactiveBuilder sortBy(Sort sort) { Assert.notNull(sort, "Sort must not be null"); return new ReactiveBuilder<>(this.executor, this.domainType, this.resultType, - this.cursorStrategy, this.defaultSubrange, sort, this.customizer); + this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, + sort, this.customizer); } /** @@ -680,7 +742,8 @@ public abstract class QuerydslDataFetcher { public ReactiveBuilder customizer(QuerydslBinderCustomizer> customizer) { Assert.notNull(customizer, "QuerydslBinderCustomizer must not be null"); return new ReactiveBuilder<>(this.executor, this.domainType, this.resultType, - this.cursorStrategy, this.defaultSubrange, this.sort, customizer); + this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, + this.sort, customizer); } /** @@ -708,7 +771,8 @@ public abstract class QuerydslDataFetcher { return new ReactiveScrollableEntityFetcher<>( this.executor, this.domainType, this.resultType, (this.cursorStrategy != null ? this.cursorStrategy : RepositoryUtils.defaultCursorStrategy()), - (this.defaultSubrange != null ? this.defaultSubrange : RepositoryUtils.defaultScrollSubrange()), + (this.defaultScrollCount != null ? this.defaultScrollCount : RepositoryUtils.defaultScrollCount()), + (this.defaultScrollPosition != null ? this.defaultScrollPosition : RepositoryUtils.defaultScrollPosition()), this.sort, this.customizer); } @@ -843,33 +907,35 @@ public abstract class QuerydslDataFetcher { private final CursorStrategy cursorStrategy; - private final ScrollSubrange defaultSubrange; + private final int defaultCount; + + private final Function defaultPosition; ScrollableEntityFetcher(QuerydslPredicateExecutor executor, TypeInformation domainType, Class resultType, CursorStrategy cursorStrategy, - ScrollSubrange defaultSubrange, + int defaultCount, + Function defaultPosition, Sort sort, QuerydslBinderCustomizer> customizer) { super(executor, domainType, resultType, sort, customizer); Assert.notNull(cursorStrategy, "CursorStrategy is required"); - Assert.notNull(defaultSubrange, "Default ScrollSubrange is required"); - Assert.isTrue(defaultSubrange.position().isPresent(), "Default ScrollPosition is required"); - Assert.isTrue(defaultSubrange.count().isPresent(), "Default scroll limit is required"); + Assert.notNull(defaultPosition, "'defaultPosition' is required"); this.cursorStrategy = cursorStrategy; - this.defaultSubrange = defaultSubrange; + this.defaultCount = defaultCount; + this.defaultPosition = defaultPosition; } - @SuppressWarnings("OptionalGetWithoutIsPresent") @Override protected Iterable getResult(FetchableFluentQuery queryToUse, DataFetchingEnvironment env) { - ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy, this.defaultSubrange); - int count = range.count().getAsInt(); - ScrollPosition position = range.position().get(); + ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy); + int count = range.count().orElse(this.defaultCount); + ScrollPosition position = (range.position().isPresent() ? + range.position().get() : this.defaultPosition.apply(range.forward())); return queryToUse.limit(count).scroll(position); } @@ -987,7 +1053,9 @@ public abstract class QuerydslDataFetcher { private final CursorStrategy cursorStrategy; - private final ScrollSubrange defaultSubrange; + private final int defaultCount; + + private final Function defaultPosition; private final Sort sort; @@ -995,22 +1063,23 @@ public abstract class QuerydslDataFetcher { ReactiveScrollableEntityFetcher(ReactiveQuerydslPredicateExecutor executor, TypeInformation domainType, Class resultType, - CursorStrategy cursorStrategy, ScrollSubrange defaultSubrange, + CursorStrategy cursorStrategy, + int defaultCount, + Function defaultPosition, Sort sort, QuerydslBinderCustomizer> customizer) { super(domainType, (QuerydslBinderCustomizer) customizer); Assert.notNull(cursorStrategy, "CursorStrategy is required"); - Assert.notNull(defaultSubrange, "Default ScrollSubrange is required"); - Assert.isTrue(defaultSubrange.position().isPresent(), "Default ScrollPosition is required"); - Assert.isTrue(defaultSubrange.count().isPresent(), "Default scroll limit is required"); + Assert.notNull(defaultPosition, "'defaultPosition' is required"); this.executor = executor; this.resultType = resultType; this.scrollableResultType = ResolvableType.forClassWithGenerics(Iterable.class, resultType); this.cursorStrategy = cursorStrategy; - this.defaultSubrange = defaultSubrange; + this.defaultCount = defaultCount; + this.defaultPosition = defaultPosition; this.sort = sort; } @@ -1020,7 +1089,7 @@ public abstract class QuerydslDataFetcher { } @Override - @SuppressWarnings({"unchecked", "OptionalGetWithoutIsPresent"}) + @SuppressWarnings("unchecked") public Mono> get(DataFetchingEnvironment env) { return this.executor.findBy(buildPredicate(env), query -> { FluentQuery.ReactiveFluentQuery queryToUse = (FluentQuery.ReactiveFluentQuery) query; @@ -1036,9 +1105,10 @@ public abstract class QuerydslDataFetcher { queryToUse = queryToUse.project(buildPropertyPaths(env.getSelectionSet(), this.resultType)); } - ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy, this.defaultSubrange); - int count = range.count().getAsInt(); - ScrollPosition position = range.position().get(); + ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy); + int count = range.count().orElse(this.defaultCount); + ScrollPosition position = (range.position().isPresent() ? + range.position().get() : this.defaultPosition.apply(range.forward())); return queryToUse.limit(count).scroll(position).map(Function.identity()); }); } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/query/RepositoryUtils.java b/spring-graphql/src/main/java/org/springframework/graphql/data/query/RepositoryUtils.java index 7b0a1875..3734f1c1 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/query/RepositoryUtils.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/query/RepositoryUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -16,6 +16,7 @@ package org.springframework.graphql.data.query; import java.lang.reflect.Type; +import java.util.function.Function; import graphql.schema.DataFetchingEnvironment; @@ -84,22 +85,25 @@ class RepositoryUtils { return CursorStrategy.withEncoder(new ScrollPositionCursorStrategy(), CursorEncoder.base64()); } + public static int defaultScrollCount() { + return 20; + } + + public static Function defaultScrollPosition() { + return forward -> ScrollPosition.offset(); + } + public static ScrollSubrange defaultScrollSubrange() { return ScrollSubrange.create(ScrollPosition.offset(), 20, true); } public static ScrollSubrange getScrollSubrange( - DataFetchingEnvironment env, CursorStrategy strategy, - ScrollSubrange defaultSubrange) { + DataFetchingEnvironment env, CursorStrategy strategy) { boolean forward = !env.getArguments().containsKey("last"); - Integer count = env.getArgument(forward ? "first" : "last"); - count = (count != null ? count : defaultSubrange.count().getAsInt()); - String cursor = env.getArgument(forward ? "after" : "before"); - ScrollPosition position = (cursor != null ? strategy.fromCursor(cursor) : defaultSubrange.position().get()); - + ScrollPosition position = (cursor != null ? strategy.fromCursor(cursor) : null); return ScrollSubrange.create(position, count, forward); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/query/RepositoryUtilsTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/query/RepositoryUtilsTests.java index 11a8acc4..239fab79 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/query/RepositoryUtilsTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/query/RepositoryUtilsTests.java @@ -37,8 +37,6 @@ public class RepositoryUtilsTests { private final CursorStrategy cursorStrategy = RepositoryUtils.defaultCursorStrategy(); - private final ScrollSubrange defaultSubrange = ScrollSubrange.create(ScrollPosition.offset(50), 20, true); - @Test void buildScrollSubrangeForward() { @@ -48,7 +46,7 @@ public class RepositoryUtilsTests { DataFetchingEnvironment env = environment( Map.of("first", count, "after", cursorStrategy.toCursor(offset))); - ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange); + ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy); assertThat(range.position().get()).isEqualTo(ScrollPosition.offset(11)); assertThat(range.count().getAsInt()).isEqualTo(count); @@ -63,7 +61,7 @@ public class RepositoryUtilsTests { DataFetchingEnvironment env = environment( Map.of("last", count, "before", cursorStrategy.toCursor(offset))); - ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange); + ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy); assertThat(range.position().get()).isEqualTo(ScrollPosition.offset(5)); assertThat(range.count().getAsInt()).isEqualTo(count); @@ -71,34 +69,41 @@ public class RepositoryUtilsTests { } @Test - void buildScrollSubrangeForwardWithDefaultScrollSubrange() { + void noInput() { DataFetchingEnvironment env = environment(Collections.emptyMap()); - ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange); + ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy); - assertThat(range.position().get()).isEqualTo(getDefaultPosition().advanceBy(1)); - assertThat(range.count().getAsInt()).isEqualTo(this.defaultSubrange.count().getAsInt()); + assertThat(range.position()).isNotPresent(); + assertThat(range.count()).isNotPresent(); assertThat(range.forward()).isTrue(); } - @Test // gh-900 - void buildScrollSubrangeBackwardFromDefaultPosition() { + @Test + void buildScrollSubrangeForwardWithoutPosition() { int count = 5; - DataFetchingEnvironment env = environment(Map.of("last", count)); - ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange); + DataFetchingEnvironment env = environment(Map.of("first", count)); + ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy); - assertThat(range.position().get()).isEqualTo(getDefaultPosition().advanceBy(-count)); + assertThat(range.position()).isNotPresent(); assertThat(range.count().getAsInt()).isEqualTo(count); assertThat(range.forward()).isTrue(); } + @Test + void buildScrollSubrangeBackwardWithoutPosition() { + int count = 5; + DataFetchingEnvironment env = environment(Map.of("last", count)); + ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy); + + assertThat(range.position()).isNotPresent(); + assertThat(range.count().getAsInt()).isEqualTo(count); + assertThat(range.forward()).isFalse(); + } + private static DataFetchingEnvironment environment(Map arguments) { return DataFetchingEnvironmentImpl.newDataFetchingEnvironment() .arguments(arguments) .build(); } - private OffsetScrollPosition getDefaultPosition() { - return (OffsetScrollPosition) defaultSubrange.position().get(); - } - }