Repository: apollographql/apollo-client

Problem statement:
Merge warnings reported for JSONObject custom scalar
**Intended outcome:**
My understanding is that all scalar values (custom or otherwise) should be treated opaquely and replaced when updating. Such updates should not trigger a data loss merge warning.

**Actual outcome:**
When merging objects containing fields of custom scalar type JSONObject, merge warning are generated in the console for these fields.

**How to reproduce the issue:**
Include [JSONObject custom scalar](https://github.com/Urigo/graphql-scalars) into your schema and resolver map and use it in a type definition

```
scalar JSONObject

type Deployment {
   id: ID!
   state: JSONObject
}
```
execute a query to fetch an object that uses JSONObject, then update the data in the cache so the merge algorithm runs.

```
query DeploymentQuery($id: ID) {
    deployment(id: $id)  {
        id
        state
    }
}
```

Notice that when a deployment object that has a different value for state is being merged, you see warnings in the console about potential data loss for `state` field.

**Versions**
```
  System:
    OS: macOS 10.15.6
  Binaries:
    Node: 12.18.2 - ~/.nvm/versions/node/v12.18.2/bin/node
    npm: 6.14.5 - ~/.nvm/versions/node/v12.18.2/bin/npm
  Browsers:
    Chrome: 85.0.4183.121
    Firefox: 79.0
    Safari: 14.0
  npmPackages:
    @apollo/client: ^3.2.0 => 3.2.0
    apollo-angular: ^2.0.3 => 2.0.4
```


Hints:

Patch:
diff --git a/src/cache/inmemory/writeToStore.ts b/src/cache/inmemory/writeToStore.ts
--- a/src/cache/inmemory/writeToStore.ts
+++ b/src/cache/inmemory/writeToStore.ts
@@ -284,6 +284,15 @@ export class StoreWriter {
       }
 
       if (process.env.NODE_ENV !== "production") {
+        const hasSelectionSet = (storeFieldName: string) =>
+          fieldsWithSelectionSets.has(fieldNameFromStoreName(storeFieldName));
+        const fieldsWithSelectionSets = new Set<string>();
+        workSet.forEach(selection => {
+          if (isField(selection) && selection.selectionSet) {
+            fieldsWithSelectionSets.add(selection.name.value);
+          }
+        });
+
         const hasMergeFunction = (storeFieldName: string) => {
           const childTree = mergeTree.map.get(storeFieldName);
           return Boolean(childTree && childTree.info && childTree.info.merge);
@@ -291,8 +300,11 @@ export class StoreWriter {
 
         Object.keys(incomingFields).forEach(storeFieldName => {
           // If a merge function was defined for this field, trust that it
-          // did the right thing about (not) clobbering data.
-          if (!hasMergeFunction(storeFieldName)) {
+          // did the right thing about (not) clobbering data. If the field
+          // has no selection set, it's a scalar field, so it doesn't need
+          // a merge function (even if it's an object, like JSON data).
+          if (hasSelectionSet(storeFieldName) &&
+              !hasMergeFunction(storeFieldName)) {
             warnAboutDataLoss(
               entityRef,
               incomingFields,


Test patch:
diff --git a/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap b/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap
--- a/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap
+++ b/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap
@@ -1,5 +1,39 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`writing to the store "Cache data maybe lost..." warnings should not warn when scalar fields are updated 1`] = `
+Object {
+  "ROOT_QUERY": Object {
+    "__typename": "Query",
+    "currentTime({\\"tz\\":\\"UTC-5\\"})": Object {
+      "localeString": "9/25/2020, 1:08:33 PM",
+    },
+    "someJSON": Object {
+      "foos": Array [
+        "bar",
+        "baz",
+      ],
+      "oyez": 3,
+    },
+  },
+}
+`;
+
+exports[`writing to the store "Cache data maybe lost..." warnings should not warn when scalar fields are updated 2`] = `
+Object {
+  "ROOT_QUERY": Object {
+    "__typename": "Query",
+    "currentTime({\\"tz\\":\\"UTC-5\\"})": Object {
+      "msSinceEpoch": 1601053713081,
+    },
+    "someJSON": Object {
+      "asdf": "middle",
+      "qwer": "upper",
+      "zxcv": "lower",
+    },
+  },
+}
+`;
+
 exports[`writing to the store user objects should be able to have { __typename: "Mutation" } 1`] = `
 Object {
   "Gene:{\\"id\\":\\"SLC45A2\\"}": Object {
diff --git a/src/cache/inmemory/__tests__/writeToStore.ts b/src/cache/inmemory/__tests__/writeToStore.ts
--- a/src/cache/inmemory/__tests__/writeToStore.ts
+++ b/src/cache/inmemory/__tests__/writeToStore.ts
@@ -1582,6 +1582,72 @@ describe('writing to the store', () => {
     });
   });
 
+  describe('"Cache data maybe lost..." warnings', () => {
+    const { warn } = console;
+    let warnings: any[][] = [];
+
+    beforeEach(() => {
+      warnings.length = 0;
+      console.warn = (...args: any[]) => {
+        warnings.push(args);
+      };
+    });
+
+    afterEach(() => {
+      console.warn = warn;
+    });
+
+    it("should not warn when scalar fields are updated", () => {
+      const cache = new InMemoryCache;
+
+      const query = gql`
+        query {
+          someJSON
+          currentTime(tz: "UTC-5")
+        }
+      `;
+
+      expect(warnings).toEqual([]);
+
+      const date = new Date(1601053713081);
+
+      cache.writeQuery({
+        query,
+        data: {
+          someJSON: {
+            oyez: 3,
+            foos: ["bar", "baz"],
+          },
+          currentTime: {
+            localeString: date.toLocaleString("en-US", {
+              timeZone: "America/New_York",
+            }),
+          },
+        },
+      });
+
+      expect(cache.extract()).toMatchSnapshot();
+      expect(warnings).toEqual([]);
+
+      cache.writeQuery({
+        query,
+        data: {
+          someJSON: {
+            qwer: "upper",
+            asdf: "middle",
+            zxcv: "lower",
+          },
+          currentTime: {
+            msSinceEpoch: date.getTime(),
+          },
+        },
+      });
+
+      expect(cache.extract()).toMatchSnapshot();
+      expect(warnings).toEqual([]);
+    });
+  });
+
   describe('writeResultToStore shape checking', () => {
     const query = gql`
       query {