Repository: apollographql/apollo-client

Problem statement:
A __typename of Query leads to incorrect caching
<!--
  Thanks for filing an issue on Apollo Client!

  Please make sure that you include the following information to ensure that your issue is actionable.

  If you don't follow the template, your issue may end up being closed without anyone looking at it carefully, because it is not actionable for us without the information in this template.

  **PLEASE NOTE:** Feature requests and non-bug related discussions are no longer managed in this repo. Feature requests should be opened in https://github.com/apollographql/apollo-feature-requests.
-->

**Intended outcome:**
<!--
What you were trying to accomplish when the bug occurred, and as much code as possible related to the source of the problem.
-->

Getting the correct results from a query

**Actual outcome:**

If using a type with `__typename` of `Query`, I get incorrect results instead. For nested references to the type, each reference is incorrectly resolved as the first instance in the result. Specifically (from the repro case):

Query:

```
  query Items {
    items {
      id
      query { id }
    }
  }
```

the data returned for the graphql operation is

```
[
 { "id": 0, "query": { "id": 0 } },
 { "id": 1, "query": { "id": 1 } },
 { "id": 2, "query": { "id": 2 } }
]
```

but the data from useQuery is:

```
{
  "items": [
    {
      "__typename": "Item",
      "id": "0",
      "query": {
        "id": "0"
      }
    },
    {
      "__typename": "Item",
      "id": "1",
      "query": {
        "id": "0"
      }
    },
    {
      "__typename": "Item",
      "id": "2",
      "query": {
        "id": "0"
      }
    }
  ]
}
```

Note how each nested `query` incorrectly has an `id` of `0` (instead of an id matching its `item`).

**How to reproduce the issue:**

See https://github.com/uhoh-itsmaciek/react-apollo-error-template -- thanks for the awesome template!

**Versions**

```
  System:
    OS: Linux 5.3 Ubuntu 19.10 (Eoan Ermine)
  Binaries:
    Node: 13.8.0 - ~/n/bin/node
    Yarn: 1.22.4 - ~/n/bin/yarn
    npm: 6.14.4 - ~/n/bin/npm
  Browsers:
    Chrome: 84.0.4147.89
    Firefox: 78.0.2
  npmPackages:
    @apollo/client: ^3.0.0 => 3.0.2 
  npmGlobalPackages:
    apollo: 2.18.3
```


Hints:

Patch:
diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts
--- a/src/cache/inmemory/policies.ts
+++ b/src/cache/inmemory/policies.ts
@@ -198,12 +198,13 @@ export const defaultDataIdFromObject = (
         _id !== void 0 ? { _id } :
         void 0;
     }
-    const idValue = id || _id;
-    if (idValue !== void 0) {
+    // If there is no object.id, fall back to object._id.
+    if (id === void 0) id = _id;
+    if (id !== void 0) {
       return `${__typename}:${(
-        typeof idValue === "number" ||
-        typeof idValue === "string"
-      ) ? idValue : JSON.stringify(idValue)}`;
+        typeof id === "number" ||
+        typeof id === "string"
+      ) ? id : JSON.stringify(id)}`;
     }
   }
 };
@@ -372,7 +373,12 @@ export class Policies {
     const old = this.rootTypenamesById[rootId];
     if (typename !== old) {
       invariant(!old || old === which, `Cannot change root ${which} __typename more than once`);
+      // First, delete any old __typename associated with this rootId from
+      // rootIdsByTypename.
+      if (old) delete this.rootIdsByTypename[old];
+      // Now make this the only __typename that maps to this rootId.
       this.rootIdsByTypename[typename] = rootId;
+      // Finally, update the __typename associated with this rootId.
       this.rootTypenamesById[rootId] = typename;
     }
   }


Test patch:
diff --git a/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap b/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap
--- a/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap
+++ b/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap
@@ -1,5 +1,55 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`type policies can alter the root query __typename 1`] = `
+Object {
+  "Query:0": Object {
+    "__typename": "Query",
+    "id": 0,
+  },
+  "Query:1": Object {
+    "__typename": "Query",
+    "id": 1,
+  },
+  "Query:2": Object {
+    "__typename": "Query",
+    "id": 2,
+  },
+  "Query:3": Object {
+    "__typename": "Query",
+    "id": 3,
+  },
+  "ROOT_QUERY": Object {
+    "__typename": "RootQuery",
+    "items": Array [
+      Object {
+        "id": 0,
+        "query": Object {
+          "__ref": "Query:0",
+        },
+      },
+      Object {
+        "id": 1,
+        "query": Object {
+          "__ref": "Query:1",
+        },
+      },
+      Object {
+        "id": 2,
+        "query": Object {
+          "__ref": "Query:2",
+        },
+      },
+      Object {
+        "id": 3,
+        "query": Object {
+          "__ref": "Query:3",
+        },
+      },
+    ],
+  },
+}
+`;
+
 exports[`type policies field policies can handle Relay-style pagination 1`] = `
 Object {
   "Artist:{\\"href\\":\\"/artist/jean-michel-basquiat\\"}": Object {
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
@@ -3968,4 +3968,63 @@ describe("type policies", function () {
     // Unchanged because the merge function prefers the existing object.
     expect(cache.extract()).toEqual(snapshot);
   });
+
+  it("can alter the root query __typename", function () {
+    const cache = new InMemoryCache({
+      typePolicies: {
+        RootQuery: {
+          queryType: true,
+        },
+      }
+    });
+
+    const ALL_ITEMS = gql`
+      query Items {
+        __typename
+        items {
+          id
+          query {
+            id
+          }
+        }
+      }
+    `;
+
+    function makeItem(id: number) {
+      return {
+        id,
+        query: {
+          __typename: "Query",
+          id,
+        },
+      };
+    }
+
+    cache.writeQuery({
+      query: ALL_ITEMS,
+      data: {
+        __typename: "RootQuery",
+        items: [
+          makeItem(0),
+          makeItem(1),
+          makeItem(2),
+          makeItem(3),
+        ],
+      },
+    });
+
+    expect(cache.extract()).toMatchSnapshot();
+
+    expect(cache.readQuery({
+      query: ALL_ITEMS,
+    })).toEqual({
+      __typename: "RootQuery",
+      items: [
+        makeItem(0),
+        makeItem(1),
+        makeItem(2),
+        makeItem(3),
+      ],
+    });
+  });
 });