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 ffffb115..f9d90569 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 @@ -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. @@ -107,15 +107,9 @@ public abstract class QueryByExampleDataFetcher { private final GraphQlArgumentBinder argumentBinder; - @Nullable - private final CursorStrategy cursorStrategy; - - - QueryByExampleDataFetcher( - TypeInformation domainType, @Nullable CursorStrategy cursorStrategy) { + QueryByExampleDataFetcher(TypeInformation domainType) { this.domainType = domainType; - this.cursorStrategy = cursorStrategy; this.argumentBinder = new GraphQlArgumentBinder(); } @@ -174,11 +168,6 @@ public abstract class QueryByExampleDataFetcher { return Collections.emptyList(); } - protected ScrollSubrange buildScrollSubrange(DataFetchingEnvironment environment) { - Assert.state(this.cursorStrategy != null, "Expected CursorStrategy"); - return RepositoryUtils.buildScrollSubrange(environment, this.cursorStrategy); - } - @Override public String toString() { return getDescription(); @@ -434,7 +423,7 @@ public abstract class QueryByExampleDataFetcher { /** * 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#initial()} with a + *

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 @@ -468,7 +457,7 @@ public abstract class QueryByExampleDataFetcher { * Build a {@link DataFetcher} to fetch many object instances. */ public DataFetcher> many() { - return new ManyEntityFetcher<>(this.executor, this.domainType, this.resultType, null, this.sort); + return new ManyEntityFetcher<>(this.executor, this.domainType, this.resultType, this.sort); } /** @@ -581,7 +570,7 @@ public abstract class QueryByExampleDataFetcher { /** * 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#initial()} with a + *

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 @@ -666,7 +655,7 @@ public abstract class QueryByExampleDataFetcher { SingleEntityFetcher( QueryByExampleExecutor executor, TypeInformation domainType, Class resultType, Sort sort) { - super(domainType, null); + super(domainType); this.executor = executor; this.resultType = resultType; this.sort = sort; @@ -712,10 +701,10 @@ public abstract class QueryByExampleDataFetcher { private final Sort sort; ManyEntityFetcher( - QueryByExampleExecutor executor, TypeInformation domainType, Class resultType, - @Nullable CursorStrategy cursorStrategy, Sort sort) { + QueryByExampleExecutor executor, TypeInformation domainType, + Class resultType, Sort sort) { - super(domainType, cursorStrategy); + super(domainType); this.executor = executor; this.resultType = resultType; this.sort = sort; @@ -756,23 +745,24 @@ public abstract class QueryByExampleDataFetcher { private static class ScrollableEntityFetcher extends ManyEntityFetcher { + private final CursorStrategy cursorStrategy; + private final ScrollSubrange defaultSubrange; private final ResolvableType scrollableResultType; ScrollableEntityFetcher( QueryByExampleExecutor executor, TypeInformation domainType, Class resultType, - CursorStrategy cursorStrategy, - ScrollSubrange defaultSubrange, - Sort sort) { + CursorStrategy cursorStrategy, ScrollSubrange defaultSubrange, Sort sort) { - super(executor, domainType, resultType, cursorStrategy, 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"); + this.cursorStrategy = cursorStrategy; this.defaultSubrange = defaultSubrange; this.scrollableResultType = ResolvableType.forClassWithGenerics(Window.class, resultType); } @@ -785,10 +775,10 @@ public abstract class QueryByExampleDataFetcher { @SuppressWarnings("OptionalGetWithoutIsPresent") @Override protected Iterable getResult(FluentQuery.FetchableFluentQuery queryToUse, DataFetchingEnvironment env) { - ScrollSubrange subrange = buildScrollSubrange(env); - int limit = subrange.count().orElse(this.defaultSubrange.count().getAsInt()); - ScrollPosition position = subrange.position().orElse(this.defaultSubrange.position().get()); - return queryToUse.limit(limit).scroll(position); + ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy, this.defaultSubrange); + int count = range.count().getAsInt(); + ScrollPosition position = range.position().get(); + return queryToUse.limit(count).scroll(position); } } @@ -807,7 +797,7 @@ public abstract class QueryByExampleDataFetcher { ReactiveQueryByExampleExecutor executor, TypeInformation domainType, Class resultType, Sort sort) { - super(domainType, null); + super(domainType); this.executor = executor; this.resultType = resultType; this.sort = sort; @@ -855,7 +845,7 @@ public abstract class QueryByExampleDataFetcher { ReactiveQueryByExampleExecutor executor, TypeInformation domainType, Class resultType, Sort sort) { - super(domainType, null); + super(domainType); this.executor = executor; this.resultType = resultType; this.sort = sort; @@ -899,6 +889,8 @@ public abstract class QueryByExampleDataFetcher { private final ResolvableType scrollableResultType; + private final CursorStrategy cursorStrategy; + private final ScrollSubrange defaultSubrange; private final Sort sort; @@ -907,7 +899,7 @@ public abstract class QueryByExampleDataFetcher { ReactiveQueryByExampleExecutor executor, TypeInformation domainType, Class resultType, CursorStrategy cursorStrategy, ScrollSubrange defaultSubrange, Sort sort) { - super(domainType, cursorStrategy); + super(domainType); Assert.notNull(cursorStrategy, "CursorStrategy is required"); Assert.notNull(defaultSubrange, "Default ScrollSubrange is required"); @@ -917,6 +909,7 @@ public abstract class QueryByExampleDataFetcher { this.executor = executor; this.resultType = resultType; this.scrollableResultType = ResolvableType.forClassWithGenerics(Iterable.class, resultType); + this.cursorStrategy = cursorStrategy; this.defaultSubrange = defaultSubrange; this.sort = sort; } @@ -943,11 +936,10 @@ public abstract class QueryByExampleDataFetcher { queryToUse = queryToUse.project(buildPropertyPaths(env.getSelectionSet(), this.resultType)); } - ScrollSubrange subrange = buildScrollSubrange(env); - int limit = subrange.count().orElse(this.defaultSubrange.count().getAsInt()); - ScrollPosition position = subrange.position().orElse(this.defaultSubrange.position().get()); - - return queryToUse.limit(limit).scroll(position).map(Function.identity()); + ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy, this.defaultSubrange); + int count = range.count().getAsInt(); + ScrollPosition position = range.position().get(); + 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 dc8bc804..93813f77 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 @@ -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. @@ -120,17 +120,10 @@ public abstract class QuerydslDataFetcher { private final QuerydslBinderCustomizer> customizer; - @Nullable - private final CursorStrategy cursorStrategy; - - - QuerydslDataFetcher( - TypeInformation domainType, QuerydslBinderCustomizer> customizer, - @Nullable CursorStrategy cursorStrategy) { + QuerydslDataFetcher(TypeInformation domainType, QuerydslBinderCustomizer> customizer) { this.domainType = domainType; this.customizer = customizer; - this.cursorStrategy = cursorStrategy; } @@ -198,11 +191,6 @@ public abstract class QuerydslDataFetcher { return Collections.emptyList(); } - protected ScrollSubrange buildScrollSubrange(DataFetchingEnvironment environment) { - Assert.state(this.cursorStrategy != null, "Expected CursorStrategy"); - return RepositoryUtils.buildScrollSubrange(environment, this.cursorStrategy); - } - @Override public String toString() { return getDescription(); @@ -481,7 +469,7 @@ public abstract class QuerydslDataFetcher { /** * 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#initial()} with a + *

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 @@ -536,7 +524,7 @@ public abstract class QuerydslDataFetcher { */ public DataFetcher> many() { return new ManyEntityFetcher<>( - this.executor, this.domainType, this.resultType, null, this.sort, this.customizer); + this.executor, this.domainType, this.resultType, this.sort, this.customizer); } /** @@ -653,7 +641,7 @@ public abstract class QuerydslDataFetcher { /** * 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#initial()} with a + *

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 @@ -758,13 +746,11 @@ public abstract class QuerydslDataFetcher { private final Sort sort; @SuppressWarnings({"unchecked", "rawtypes"}) - SingleEntityFetcher(QuerydslPredicateExecutor executor, - TypeInformation domainType, - Class resultType, - Sort sort, - QuerydslBinderCustomizer> customizer) { + SingleEntityFetcher( + QuerydslPredicateExecutor executor, TypeInformation domainType, Class resultType, + Sort sort, QuerydslBinderCustomizer> customizer) { - super(domainType, (QuerydslBinderCustomizer) customizer, null); + super(domainType, (QuerydslBinderCustomizer) customizer); this.executor = executor; this.resultType = resultType; this.sort = sort; @@ -810,13 +796,11 @@ public abstract class QuerydslDataFetcher { private final Sort sort; @SuppressWarnings({"unchecked", "rawtypes"}) - ManyEntityFetcher(QuerydslPredicateExecutor executor, - TypeInformation domainType, - Class resultType, - @Nullable CursorStrategy cursorStrategy, - Sort sort, - QuerydslBinderCustomizer> customizer) { - super(domainType, (QuerydslBinderCustomizer) customizer, cursorStrategy); + ManyEntityFetcher( + QuerydslPredicateExecutor executor, TypeInformation domainType, Class resultType, + Sort sort, QuerydslBinderCustomizer> customizer) { + + super(domainType, (QuerydslBinderCustomizer) customizer); this.executor = executor; this.resultType = resultType; this.sort = sort; @@ -857,6 +841,8 @@ public abstract class QuerydslDataFetcher { private static class ScrollableEntityFetcher extends ManyEntityFetcher { + private final CursorStrategy cursorStrategy; + private final ScrollSubrange defaultSubrange; ScrollableEntityFetcher(QuerydslPredicateExecutor executor, @@ -867,23 +853,24 @@ public abstract class QuerydslDataFetcher { Sort sort, QuerydslBinderCustomizer> customizer) { - super(executor, domainType, resultType, cursorStrategy, sort, 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"); + this.cursorStrategy = cursorStrategy; this.defaultSubrange = defaultSubrange; } @SuppressWarnings("OptionalGetWithoutIsPresent") @Override protected Iterable getResult(FetchableFluentQuery queryToUse, DataFetchingEnvironment env) { - ScrollSubrange subrange = buildScrollSubrange(env); - int limit = subrange.count().orElse(this.defaultSubrange.count().getAsInt()); - ScrollPosition position = subrange.position().orElse(this.defaultSubrange.position().get()); - return queryToUse.limit(limit).scroll(position); + ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy, this.defaultSubrange); + int count = range.count().getAsInt(); + ScrollPosition position = range.position().get(); + return queryToUse.limit(count).scroll(position); } } @@ -899,13 +886,12 @@ public abstract class QuerydslDataFetcher { private final Sort sort; @SuppressWarnings({"unchecked", "rawtypes"}) - ReactiveSingleEntityFetcher(ReactiveQuerydslPredicateExecutor executor, - TypeInformation domainType, - Class resultType, - Sort sort, - QuerydslBinderCustomizer> customizer) { + ReactiveSingleEntityFetcher( + ReactiveQuerydslPredicateExecutor executor, TypeInformation domainType, Class resultType, + Sort sort, QuerydslBinderCustomizer> customizer) { + + super(domainType, (QuerydslBinderCustomizer) customizer); - super(domainType, (QuerydslBinderCustomizer) customizer, null); this.executor = executor; this.resultType = resultType; this.sort = sort; @@ -950,13 +936,12 @@ public abstract class QuerydslDataFetcher { private final Sort sort; @SuppressWarnings({"unchecked", "rawtypes"}) - ReactiveManyEntityFetcher(ReactiveQuerydslPredicateExecutor executor, - TypeInformation domainType, - Class resultType, - Sort sort, - QuerydslBinderCustomizer> customizer) { + ReactiveManyEntityFetcher( + ReactiveQuerydslPredicateExecutor executor, TypeInformation domainType, Class resultType, + Sort sort, QuerydslBinderCustomizer> customizer) { + + super(domainType, (QuerydslBinderCustomizer) customizer); - super(domainType, (QuerydslBinderCustomizer) customizer, null); this.executor = executor; this.resultType = resultType; this.sort = sort; @@ -1000,6 +985,8 @@ public abstract class QuerydslDataFetcher { private final ResolvableType scrollableResultType; + private final CursorStrategy cursorStrategy; + private final ScrollSubrange defaultSubrange; private final Sort sort; @@ -1012,7 +999,7 @@ public abstract class QuerydslDataFetcher { Sort sort, QuerydslBinderCustomizer> customizer) { - super(domainType, (QuerydslBinderCustomizer) customizer, cursorStrategy); + super(domainType, (QuerydslBinderCustomizer) customizer); Assert.notNull(cursorStrategy, "CursorStrategy is required"); Assert.notNull(defaultSubrange, "Default ScrollSubrange is required"); @@ -1022,6 +1009,7 @@ public abstract class QuerydslDataFetcher { this.executor = executor; this.resultType = resultType; this.scrollableResultType = ResolvableType.forClassWithGenerics(Iterable.class, resultType); + this.cursorStrategy = cursorStrategy; this.defaultSubrange = defaultSubrange; this.sort = sort; } @@ -1048,11 +1036,10 @@ public abstract class QuerydslDataFetcher { queryToUse = queryToUse.project(buildPropertyPaths(env.getSelectionSet(), this.resultType)); } - ScrollSubrange subrange = buildScrollSubrange(env); - int limit = subrange.count().orElse(this.defaultSubrange.count().getAsInt()); - ScrollPosition position = subrange.position().orElse(this.defaultSubrange.position().get()); - - return queryToUse.limit(limit).scroll(position).map(Function.identity()); + ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy, this.defaultSubrange); + int count = range.count().getAsInt(); + ScrollPosition position = range.position().get(); + 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 612b7109..7b0a1875 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 @@ -88,14 +88,18 @@ class RepositoryUtils { return ScrollSubrange.create(ScrollPosition.offset(), 20, true); } - public static ScrollSubrange buildScrollSubrange( - DataFetchingEnvironment environment, CursorStrategy cursorStrategy) { + public static ScrollSubrange getScrollSubrange( + DataFetchingEnvironment env, CursorStrategy strategy, + ScrollSubrange defaultSubrange) { + + 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()); - Assert.notNull(cursorStrategy, "CursorStrategy is required to build a ScrollSubrange"); - boolean forward = !environment.getArguments().containsKey("last"); - Integer count = environment.getArgument(forward ? "first" : "last"); - String cursor = environment.getArgument(forward ? "after" : "before"); - ScrollPosition position = (cursor != null ? cursorStrategy.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 new file mode 100644 index 00000000..0a523e3b --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/query/RepositoryUtilsTests.java @@ -0,0 +1,104 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.graphql.data.query; + +import java.util.Collections; +import java.util.Map; + +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.DataFetchingEnvironmentImpl; +import org.junit.jupiter.api.Test; + +import org.springframework.data.domain.OffsetScrollPosition; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.graphql.data.pagination.CursorStrategy; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link RepositoryUtils}. + */ +@SuppressWarnings("OptionalGetWithoutIsPresent") +public class RepositoryUtilsTests { + + private final CursorStrategy cursorStrategy = RepositoryUtils.defaultCursorStrategy(); + + private final ScrollSubrange defaultSubrange = ScrollSubrange.create(ScrollPosition.offset(50), 20, true); + + + @Test + void buildScrollSubrangeForward() { + OffsetScrollPosition offset = ScrollPosition.offset(10); + int count = 5; + + DataFetchingEnvironment env = environment( + Map.of("first", count, "after", cursorStrategy.toCursor(offset))); + + ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange); + + assertThat(range.position().get()).isEqualTo(offset); + assertThat(range.count().getAsInt()).isEqualTo(count); + assertThat(range.forward()).isTrue(); + } + + @Test + void buildScrollSubrangeBackward() { + OffsetScrollPosition offset = ScrollPosition.offset(10); + int count = 5; + + DataFetchingEnvironment env = environment( + Map.of("last", count, "before", cursorStrategy.toCursor(offset))); + + ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange); + + assertThat(range.position().get()).isEqualTo(offset.advanceBy(-count-1)); + assertThat(range.count().getAsInt()).isEqualTo(count); + assertThat(range.forward()).isTrue(); + } + + @Test + void buildScrollSubrangeForwardWithDefaultScrollSubrange() { + DataFetchingEnvironment env = environment(Collections.emptyMap()); + ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange); + + assertThat(range.position().get()).isEqualTo(getDefaultPosition()); + assertThat(range.count().getAsInt()).isEqualTo(this.defaultSubrange.count().getAsInt()); + assertThat(range.forward()).isTrue(); + } + + @Test // gh-900 + void buildScrollSubrangeBackwardFromDefaultPosition() { + int count = 5; + 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.count().getAsInt()).isEqualTo(count); + assertThat(range.forward()).isTrue(); + } + + private static DataFetchingEnvironment environment(Map arguments) { + return DataFetchingEnvironmentImpl.newDataFetchingEnvironment() + .arguments(arguments) + .build(); + } + + private OffsetScrollPosition getDefaultPosition() { + return (OffsetScrollPosition) defaultSubrange.position().get(); + } + +}