diff --git a/CHANGELOG.md b/CHANGELOG.md
index 77d69374fe6..5630f04437e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@
- Fix race condition in `@apollo/client/link/context` that could leak subscriptions if the subscription is cancelled before `operation.setContext` is called.
[@sofianhn](https://github.com/sofianhn) in [#8399](https://github.com/apollographql/apollo-client/pull/8399)
+- Prefer `existing.pageInfo.startCursor` and `endCursor` (if defined) in `read` function of `relayStylePagination` policies.
+ [@benjamn](https://github.com/benjamn) in [#8438](https://github.com/apollographql/apollo-client/pull/8438)
+
## Apollo Client 3.3.20
### Bug fixes
diff --git a/src/cache/inmemory/__tests__/policies.ts b/src/cache/inmemory/__tests__/policies.ts
index 7787beee225..c57253e02dc 100644
--- a/src/cache/inmemory/__tests__/policies.ts
+++ b/src/cache/inmemory/__tests__/policies.ts
@@ -3461,7 +3461,7 @@ describe("type policies", function () {
edges,
pageInfo: {
__typename: "PageInfo",
- startCursor: thirdPageInfo.startCursor,
+ startCursor: fourthPageInfo.startCursor,
endCursor: fifthPageInfo.endCursor,
hasPreviousPage: false,
hasNextPage: true,
diff --git a/src/utilities/policies/__tests__/relayStylePagination.test.ts b/src/utilities/policies/__tests__/relayStylePagination.test.ts
index a2d81ab5736..89583da4ef9 100644
--- a/src/utilities/policies/__tests__/relayStylePagination.test.ts
+++ b/src/utilities/policies/__tests__/relayStylePagination.test.ts
@@ -1,9 +1,116 @@
-import { FieldFunctionOptions, InMemoryCache, isReference, makeReference } from '../../../core';
+import { FieldFunctionOptions, InMemoryCache, isReference, makeReference, StoreObject } from '../../../cache';
import { relayStylePagination, TRelayPageInfo } from '../pagination';
describe('relayStylePagination', () => {
const policy = relayStylePagination();
+ describe('read', () => {
+ const fakeEdges = [
+ { node: { __ref: "A" }, cursor: "cursorA" },
+ { node: { __ref: "B" }, cursor: "cursorB" },
+ { node: { __ref: "C" }, cursor: "cursorC" },
+ ];
+
+ const fakeReadOptions = {
+ canRead() { return true },
+ readField(key: string, obj: StoreObject) {
+ return obj && obj[key];
+ },
+ } as any as FieldFunctionOptions;
+
+ it("should prefer existing.pageInfo.startCursor", () => {
+ const resultWithStartCursor = policy.read!({
+ edges: fakeEdges,
+ pageInfo: {
+ startCursor: "preferredStartCursor",
+ hasPreviousPage: false,
+ hasNextPage: true,
+ } as TRelayPageInfo,
+ }, fakeReadOptions);
+
+ expect(
+ resultWithStartCursor &&
+ resultWithStartCursor.pageInfo
+ ).toEqual({
+ startCursor: "preferredStartCursor",
+ endCursor: "cursorC",
+ hasPreviousPage: false,
+ hasNextPage: true,
+ });
+ });
+
+ it("should prefer existing.pageInfo.endCursor", () => {
+ const resultWithEndCursor = policy.read!({
+ edges: fakeEdges,
+ pageInfo: {
+ endCursor: "preferredEndCursor",
+ hasPreviousPage: false,
+ hasNextPage: true,
+ } as TRelayPageInfo,
+ }, fakeReadOptions);
+
+ expect(
+ resultWithEndCursor &&
+ resultWithEndCursor.pageInfo
+ ).toEqual({
+ startCursor: "cursorA",
+ endCursor: "preferredEndCursor",
+ hasPreviousPage: false,
+ hasNextPage: true,
+ });
+ });
+
+ it("should prefer existing.pageInfo.{start,end}Cursor", () => {
+ const resultWithEndCursor = policy.read!({
+ edges: fakeEdges,
+ pageInfo: {
+ startCursor: "preferredStartCursor",
+ endCursor: "preferredEndCursor",
+ hasPreviousPage: false,
+ hasNextPage: true,
+ },
+ }, fakeReadOptions);
+
+ expect(
+ resultWithEndCursor &&
+ resultWithEndCursor.pageInfo
+ ).toEqual({
+ startCursor: "preferredStartCursor",
+ endCursor: "preferredEndCursor",
+ hasPreviousPage: false,
+ hasNextPage: true,
+ });
+ });
+
+ it("should override pageInfo.{start,end}Cursor if empty strings", () => {
+ const resultWithEndCursor = policy.read!({
+ edges: [
+ { node: { __ref: "A" }, cursor: "" },
+ { node: { __ref: "B" }, cursor: "cursorB" },
+ { node: { __ref: "C" }, cursor: "" },
+ { node: { __ref: "D" }, cursor: "cursorD" },
+ { node: { __ref: "E" } },
+ ],
+ pageInfo: {
+ startCursor: "",
+ endCursor: "",
+ hasPreviousPage: false,
+ hasNextPage: true,
+ },
+ }, fakeReadOptions);
+
+ expect(
+ resultWithEndCursor &&
+ resultWithEndCursor.pageInfo
+ ).toEqual({
+ startCursor: "cursorB",
+ endCursor: "cursorD",
+ hasPreviousPage: false,
+ hasNextPage: true,
+ });
+ });
+ });
+
describe('merge', () => {
const merge = policy.merge;
// The merge function should exist, make TS aware
diff --git a/src/utilities/policies/pagination.ts b/src/utilities/policies/pagination.ts
index fc885888905..e6471b2d99d 100644
--- a/src/utilities/policies/pagination.ts
+++ b/src/utilities/policies/pagination.ts
@@ -98,20 +98,25 @@ export function relayStylePagination(
if (!existing) return;
const edges: TRelayEdge[] = [];
- let startCursor = "";
- let endCursor = "";
+ let firstEdgeCursor = "";
+ let lastEdgeCursor = "";
existing.edges.forEach(edge => {
// Edges themselves could be Reference objects, so it's important
// to use readField to access the edge.edge.node property.
if (canRead(readField("node", edge))) {
edges.push(edge);
if (edge.cursor) {
- startCursor = startCursor || edge.cursor;
- endCursor = edge.cursor;
+ firstEdgeCursor = firstEdgeCursor || edge.cursor || "";
+ lastEdgeCursor = edge.cursor || lastEdgeCursor;
}
}
});
+ const {
+ startCursor,
+ endCursor,
+ } = existing.pageInfo || {};
+
return {
// Some implementations return additional Connection fields, such
// as existing.totalCount. These fields are saved by the merge
@@ -120,8 +125,10 @@ export function relayStylePagination(
edges,
pageInfo: {
...existing.pageInfo,
- startCursor,
- endCursor,
+ // If existing.pageInfo.{start,end}Cursor are undefined or "", default
+ // to firstEdgeCursor and/or lastEdgeCursor.
+ startCursor: startCursor || firstEdgeCursor,
+ endCursor: endCursor || lastEdgeCursor,
},
};
},