Repository: apollographql/apollo-client

Problem statement:
`relayStylePagination` `read` method clobbers `pageInfo.startCursor` and `pageInfo.endCursor`
**Intended outcome:**
With a query as follows:

```graphql
query {
  someConnectionField {
    edges {
      node {
        id
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
```

I would expect `endCursor` to be the value returned from the API, not `""`.

**Actual outcome:**
Even though the API returns a valid cursor in `endCursor`, the `relayStylePagination` type policy attempts to read it from `edges[0].cursor` (or the last edge's cursor) and completely replaces the one in `pageInfo.

These should default to `pageInfo.{startCursor,endCursor}` and not `""`.
https://github.com/apollographql/apollo-client/blob/cefb3c32621de6973191c7abb6edd365d53cbf41/src/utilities/policies/pagination.ts#L101-L102

**How to reproduce the issue:**
Perform a query as described above while using `relayStylePagination`.



Hints:
Having the same issue, can't use ```relayStylePagination()``` at all. Cursors are always empty.
So i just copied all implementation and replaced
https://github.com/apollographql/apollo-client/blob/54fe88a4dd7d3f0850f37a5dcf93c8404bf6e566/src/utilities/policies/pagination.ts#L101-L102
By
```javascript
let { startCursor } = existing.pageInfo;
let { endCursor } = existing.pageInfo;
```
This is a serious issue. Relay-style pagination is one of the core features offered by all GraphQL implementations, and is included in the official GraphQL docs. Can this please receive further attention?

Patch:
diff --git a/src/utilities/policies/pagination.ts b/src/utilities/policies/pagination.ts
--- a/src/utilities/policies/pagination.ts
+++ b/src/utilities/policies/pagination.ts
@@ -98,20 +98,25 @@ export function relayStylePagination<TNode = Reference>(
       if (!existing) return;
 
       const edges: TRelayEdge<TNode>[] = [];
-      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<TNode = Reference>(
         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,
         },
       };
     },


Test patch:
diff --git a/src/cache/inmemory/__tests__/policies.ts b/src/cache/inmemory/__tests__/policies.ts
--- 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
--- 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