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(); - } - }