From c65b337f6afb89f86190b3a92504c66faf08cdd4 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Mon, 28 Jun 2021 19:15:26 -0400 Subject: [PATCH] Prefer existing.pageInfo cursors in relayStylePagination read function. Should fix #8267, taking inspiration from @migueloller's suggestion in https://github.com/apollographql/apollo-client/issues/8267#issue-895915003 --- src/cache/inmemory/__tests__/policies.ts | 2 +- .../__tests__/relayStylePagination.test.ts | 109 +++++++++++++++++- src/utilities/policies/pagination.ts | 19 ++- 3 files changed, 122 insertions(+), 8 deletions(-) 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, }, }; },