Skip to content

Commit bd6ca27

Browse files
committed
Merge branch '1.2.x'
2 parents eba7e95 + 248cfe2 commit bd6ca27

13 files changed

+340
-167
lines changed

spring-graphql/src/main/java/org/springframework/graphql/data/query/QueryByExampleDataFetcher.java

Lines changed: 111 additions & 43 deletions
Large diffs are not rendered by default.

spring-graphql/src/main/java/org/springframework/graphql/data/query/QuerydslDataFetcher.java

Lines changed: 119 additions & 49 deletions
Large diffs are not rendered by default.

spring-graphql/src/main/java/org/springframework/graphql/data/query/RepositoryUtils.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616
package org.springframework.graphql.data.query;
1717

1818
import java.lang.reflect.Type;
19+
import java.util.function.Function;
1920

2021
import graphql.schema.DataFetchingEnvironment;
2122

@@ -84,22 +85,25 @@ public static CursorStrategy<ScrollPosition> defaultCursorStrategy() {
8485
return CursorStrategy.withEncoder(new ScrollPositionCursorStrategy(), CursorEncoder.base64());
8586
}
8687

88+
public static int defaultScrollCount() {
89+
return 20;
90+
}
91+
92+
public static Function<Boolean, ScrollPosition> defaultScrollPosition() {
93+
return forward -> ScrollPosition.offset();
94+
}
95+
8796
public static ScrollSubrange defaultScrollSubrange() {
8897
return ScrollSubrange.create(ScrollPosition.offset(), 20, true);
8998
}
9099

91100
public static ScrollSubrange getScrollSubrange(
92-
DataFetchingEnvironment env, CursorStrategy<ScrollPosition> strategy,
93-
ScrollSubrange defaultSubrange) {
101+
DataFetchingEnvironment env, CursorStrategy<ScrollPosition> strategy) {
94102

95103
boolean forward = !env.getArguments().containsKey("last");
96-
97104
Integer count = env.getArgument(forward ? "first" : "last");
98-
count = (count != null ? count : defaultSubrange.count().getAsInt());
99-
100105
String cursor = env.getArgument(forward ? "after" : "before");
101-
ScrollPosition position = (cursor != null ? strategy.fromCursor(cursor) : defaultSubrange.position().get());
102-
106+
ScrollPosition position = (cursor != null ? strategy.fromCursor(cursor) : null);
103107
return ScrollSubrange.create(position, count, forward);
104108
}
105109

spring-graphql/src/main/java/org/springframework/graphql/data/query/ScrollSubrange.java

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,20 @@ else if (pos instanceof KeysetScrollPosition keysetPosition) {
7171

7272

7373
/**
74-
* Create a {@link ScrollSubrange} instance.
75-
* @param position the position relative to which to scroll, or {@code null}
76-
* for scrolling from the beginning
77-
* @param count the number of elements requested
78-
* @param forward whether to return elements after (true) or before (false)
79-
* the element at the given position
80-
* @return the created subrange
74+
* Create a {@link ScrollSubrange} from the given inputs.
75+
* <p>Pagination with offset-based scrolling is always forward and inclusive
76+
* of the referenced item. Therefore, an {@link OffsetScrollPosition} is
77+
* adjusted as follows. For forward pagination, advanced by 1. For backward
78+
* pagination, advanced back by the count, and switched to forward.
79+
* @param position the reference position, or {@code null} if not specified
80+
* @param count how many to return, or {@code null} if not specified
81+
* @param forward whether scroll forward (true) or backward (false)
82+
* @return the created instance
8183
* @since 1.2.4
8284
*/
83-
public static ScrollSubrange create(@Nullable ScrollPosition position, @Nullable Integer count, boolean forward) {
85+
public static ScrollSubrange create(
86+
@Nullable ScrollPosition position, @Nullable Integer count, boolean forward) {
87+
8488
if (count != null && count < 0) {
8589
count = null;
8690
}
@@ -98,16 +102,24 @@ else if (position instanceof KeysetScrollPosition keysetScrollPosition) {
98102
private static ScrollSubrange initFromOffsetPosition(
99103
OffsetScrollPosition position, @Nullable Integer count, boolean forward) {
100104

101-
if (!forward) {
105+
// Offset is inclusive, adapt to exclusive:
106+
// - for forward, add 1 to return items after position
107+
// - for backward, subtract count to get items before position
108+
109+
if (forward) {
110+
position = position.advanceBy(1);
111+
}
112+
else {
102113
int countOrZero = (count != null ? count : 0);
103-
if (countOrZero < position.getOffset()) {
104-
position = position.advanceBy(-countOrZero-1);
114+
if (position.getOffset() >= countOrZero) {
115+
position = position.advanceBy(-countOrZero);
105116
}
106117
else {
107-
count = (position.getOffset() > 0 ? (int) (position.getOffset() - 1) : 0);
108-
position = null;
118+
count = (int) position.getOffset();
119+
position = ScrollPosition.offset();
109120
}
110121
}
122+
111123
return new ScrollSubrange(position, count, true, null);
112124
}
113125

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -49,7 +49,7 @@ public class SchemaMappingPaginationTests {
4949
@Test
5050
void forwardPagination() {
5151

52-
String document = BookSource.booksConnectionQuery("first:2, after:\"O_3\"");
52+
String document = BookSource.booksConnectionQuery("first:2, after:\"O_2\"");
5353
Mono<ExecutionGraphQlResponse> response = graphQlService().execute(document);
5454

5555
ResponseHelper.forResponse(response).assertData(

spring-graphql/src/test/java/org/springframework/graphql/data/query/QuerydslDataFetcherTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,12 @@ void shouldFetchWindow() {
139139
ResponseHelper.forResponse(response).assertData(
140140
"{\"books\":{" +
141141
"\"edges\":[" +
142-
"{\"cursor\":\"O_4\",\"node\":{\"id\":\"42\",\"name\":\"Hitchhiker's Guide to the Galaxy\"}}," +
143-
"{\"cursor\":\"O_5\",\"node\":{\"id\":\"53\",\"name\":\"Breaking Bad\"}}" +
142+
"{\"cursor\":\"O_0\",\"node\":{\"id\":\"42\",\"name\":\"Hitchhiker's Guide to the Galaxy\"}}," +
143+
"{\"cursor\":\"O_1\",\"node\":{\"id\":\"53\",\"name\":\"Breaking Bad\"}}" +
144144
"]," +
145145
"\"pageInfo\":{" +
146-
"\"startCursor\":\"O_4\"," +
147-
"\"endCursor\":\"O_5\"," +
146+
"\"startCursor\":\"O_0\"," +
147+
"\"endCursor\":\"O_1\"," +
148148
"\"hasPreviousPage\":true," +
149149
"\"hasNextPage\":false" +
150150
"}}}"

spring-graphql/src/test/java/org/springframework/graphql/data/query/RepositoryUtilsTests.java

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ public class RepositoryUtilsTests {
3737

3838
private final CursorStrategy<ScrollPosition> cursorStrategy = RepositoryUtils.defaultCursorStrategy();
3939

40-
private final ScrollSubrange defaultSubrange = ScrollSubrange.create(ScrollPosition.offset(50), 20, true);
41-
4240

4341
@Test
4442
void buildScrollSubrangeForward() {
@@ -48,9 +46,9 @@ void buildScrollSubrangeForward() {
4846
DataFetchingEnvironment env = environment(
4947
Map.of("first", count, "after", cursorStrategy.toCursor(offset)));
5048

51-
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange);
49+
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy);
5250

53-
assertThat(range.position().get()).isEqualTo(offset);
51+
assertThat(range.position().get()).isEqualTo(ScrollPosition.offset(11));
5452
assertThat(range.count().getAsInt()).isEqualTo(count);
5553
assertThat(range.forward()).isTrue();
5654
}
@@ -63,42 +61,49 @@ void buildScrollSubrangeBackward() {
6361
DataFetchingEnvironment env = environment(
6462
Map.of("last", count, "before", cursorStrategy.toCursor(offset)));
6563

66-
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange);
64+
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy);
6765

68-
assertThat(range.position().get()).isEqualTo(offset.advanceBy(-count-1));
66+
assertThat(range.position().get()).isEqualTo(ScrollPosition.offset(5));
6967
assertThat(range.count().getAsInt()).isEqualTo(count);
7068
assertThat(range.forward()).isTrue();
7169
}
7270

7371
@Test
74-
void buildScrollSubrangeForwardWithDefaultScrollSubrange() {
72+
void noInput() {
7573
DataFetchingEnvironment env = environment(Collections.emptyMap());
76-
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange);
74+
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy);
7775

78-
assertThat(range.position().get()).isEqualTo(getDefaultPosition());
79-
assertThat(range.count().getAsInt()).isEqualTo(this.defaultSubrange.count().getAsInt());
76+
assertThat(range.position()).isNotPresent();
77+
assertThat(range.count()).isNotPresent();
8078
assertThat(range.forward()).isTrue();
8179
}
8280

83-
@Test // gh-900
84-
void buildScrollSubrangeBackwardFromDefaultPosition() {
81+
@Test
82+
void buildScrollSubrangeForwardWithoutPosition() {
8583
int count = 5;
86-
DataFetchingEnvironment env = environment(Map.of("last", count));
87-
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange);
84+
DataFetchingEnvironment env = environment(Map.of("first", count));
85+
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy);
8886

89-
assertThat(range.position().get()).isEqualTo(getDefaultPosition().advanceBy(-count-1));
87+
assertThat(range.position()).isNotPresent();
9088
assertThat(range.count().getAsInt()).isEqualTo(count);
9189
assertThat(range.forward()).isTrue();
9290
}
9391

92+
@Test
93+
void buildScrollSubrangeBackwardWithoutPosition() {
94+
int count = 5;
95+
DataFetchingEnvironment env = environment(Map.of("last", count));
96+
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy);
97+
98+
assertThat(range.position()).isNotPresent();
99+
assertThat(range.count().getAsInt()).isEqualTo(count);
100+
assertThat(range.forward()).isFalse();
101+
}
102+
94103
private static DataFetchingEnvironment environment(Map<String, Object> arguments) {
95104
return DataFetchingEnvironmentImpl.newDataFetchingEnvironment()
96105
.arguments(arguments)
97106
.build();
98107
}
99108

100-
private OffsetScrollPosition getDefaultPosition() {
101-
return (OffsetScrollPosition) defaultSubrange.position().get();
102-
}
103-
104109
}

spring-graphql/src/test/java/org/springframework/graphql/data/query/ScrollSubrangeTests.java

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -37,40 +37,53 @@
3737
public class ScrollSubrangeTests {
3838

3939
@Test
40-
void offset() {
41-
ScrollPosition position = ScrollPosition.offset(30);
40+
void offsetForward() {
4241
int count = 10;
42+
ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.offset(30), count, true);
4343

44-
ScrollSubrange subrange = ScrollSubrange.create(position, count, true);
45-
assertThat(((OffsetScrollPosition) subrange.position().get())).isEqualTo(position);
44+
assertThat(getOffset(subrange)).isEqualTo(31);
4645
assertThat(subrange.count().orElse(0)).isEqualTo(count);
4746
assertThat(subrange.forward()).isTrue();
47+
}
4848

49-
subrange = ScrollSubrange.create(position, count, false);
50-
assertThat(((OffsetScrollPosition) subrange.position().get()).getOffset()).isEqualTo(19);
49+
@Test
50+
void offsetBackward() {
51+
int count = 10;
52+
ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.offset(30), count, false);
53+
54+
assertThat(getOffset(subrange)).isEqualTo(20);
5155
assertThat(subrange.count().orElse(0)).isEqualTo(count);
5256
assertThat(subrange.forward()).isTrue();
5357
}
5458

5559
@Test
56-
void keyset() {
60+
void keysetForward() {
5761
Map<String, Object> keys = new LinkedHashMap<>();
5862
keys.put("firstName", "Joseph");
5963
keys.put("lastName", "Heller");
6064
keys.put("id", 103);
6165

62-
ScrollPosition position = ScrollPosition.forward(keys);
6366
int count = 10;
67+
ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.forward(keys), count, true);
6468

65-
ScrollSubrange subrange = ScrollSubrange.create(position, count, true);
6669
KeysetScrollPosition actualPosition = (KeysetScrollPosition) subrange.position().get();
6770
assertThat(actualPosition.getKeys()).isEqualTo(keys);
6871
assertThat(actualPosition.getDirection()).isEqualTo(Direction.FORWARD);
6972
assertThat(subrange.count().orElse(0)).isEqualTo(count);
7073
assertThat(subrange.forward()).isTrue();
74+
}
75+
76+
@Test
77+
void keysetBackward() {
78+
Map<String, Object> keys = new LinkedHashMap<>();
79+
keys.put("firstName", "Joseph");
80+
keys.put("lastName", "Heller");
81+
keys.put("id", 103);
7182

72-
subrange = ScrollSubrange.create(position, count, false);
73-
actualPosition = (KeysetScrollPosition) subrange.position().get();
83+
int count = 10;
84+
ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.forward(keys), count, false);
85+
86+
KeysetScrollPosition actualPosition = (KeysetScrollPosition) subrange.position().get();
7487
assertThat(actualPosition.getKeys()).isEqualTo(keys);
7588
assertThat(actualPosition.getDirection()).isEqualTo(Direction.BACKWARD);
7689
assertThat(subrange.count().orElse(0)).isEqualTo(count);
@@ -87,33 +100,34 @@ void nullInput() {
87100
}
88101

89102
@Test
90-
void offsetBackwardPaginationWithInsufficientCount() {
91-
ScrollPosition position = ScrollPosition.offset(5);
92-
ScrollSubrange subrange = ScrollSubrange.create(position, 10, false);
103+
void offsetBackwardWithInsufficientCount() {
104+
ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.offset(5), 10, false);
93105

94-
assertThat(subrange.position()).isNotPresent();
95-
assertThat(subrange.count().getAsInt()).isEqualTo(4);
106+
assertThat(getOffset(subrange)).isEqualTo(0);
107+
assertThat(subrange.count().getAsInt()).isEqualTo(5);
96108
assertThat(subrange.forward()).isTrue();
97109
}
98110

99111
@Test
100-
void offsetBackwardPaginationWithOffsetZero() {
101-
ScrollPosition position = ScrollPosition.offset(0);
102-
ScrollSubrange subrange = ScrollSubrange.create(position, 10, false);
112+
void offsetBackwardFromInitialOffset() {
113+
ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.offset(0), 10, false);
103114

104-
assertThat(subrange.position()).isNotPresent();
115+
assertThat(getOffset(subrange)).isEqualTo(0);
105116
assertThat(subrange.count().getAsInt()).isEqualTo(0);
106117
assertThat(subrange.forward()).isTrue();
107118
}
108119

109120
@Test
110-
void offsetBackwardPaginationWithNullCount() {
111-
ScrollPosition position = ScrollPosition.offset(30);
112-
ScrollSubrange subrange = ScrollSubrange.create(position, null, false);
121+
void offsetBackwardWithNullCount() {
122+
ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.offset(30), null, false);
113123

114-
assertThat(subrange.position()).hasValue(ScrollPosition.offset(29));
124+
assertThat(getOffset(subrange)).isEqualTo(30);
115125
assertThat(subrange.count()).isNotPresent();
116126
assertThat(subrange.forward()).isTrue();
117127
}
118128

129+
private static long getOffset(ScrollSubrange subrange) {
130+
return ((OffsetScrollPosition) subrange.position().get()).getOffset();
131+
}
132+
119133
}

spring-graphql/src/test/java/org/springframework/graphql/data/query/jpa/QueryByExampleDataFetcherJpaTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ void shouldFetchWindow() {
137137

138138
Mono<WebGraphQlResponse> response = graphQlSetup
139139
.toWebGraphQlHandler()
140-
.handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_3\"")));
140+
.handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_2\"")));
141141

142142
List<Map<String, Object>> edges = ResponseHelper.forResponse(response).toEntity("books.edges", List.class);
143143
assertThat(edges.size()).isEqualTo(2);

spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherMongoDbTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ void shouldFetchWindow() {
134134

135135
Mono<WebGraphQlResponse> response = graphQlSetup
136136
.toWebGraphQlHandler()
137-
.handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_3\"")));
137+
.handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_2\"")));
138138

139139
List<Map<String, Object>> edges = ResponseHelper.forResponse(response).toEntity("books.edges", List.class);
140140
assertThat(edges.size()).isEqualTo(2);

0 commit comments

Comments
 (0)