Repository: FormidableLabs/urql
Pull request #651 (2020-03-20 19:22:54)

(graphcache) - Remove dictionary data from results
As discussed, this may cause more confusion than what it's worth.

changeset-bot[bot]: ###  💥  No Changeset

Latest commit: d4b764ba42663ca807f566cf1a39d623378aef27

Merging this PR will not cause any packages to be released. If these changes should not cause updates to packages in this repo, this is fine 🙂

**If these changes should be published to npm, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/FormidableLabs/urql/new/refactor/no-dictionary-data?filename=.changeset/heavy-beds-enjoy.md&value=---%0A%22%40urql%2Fexchange-graphcache%22%3A%20patch%0A---%0A%0A(graphcache)%20-%20Remove%20dictionary%20data%20from%20results%0A)



Patch:
diff --git a/.changeset/cold-socks-juggle.md b/.changeset/cold-socks-juggle.md
new file mode 100644
index 00000000..a3304bc3
--- /dev/null
+++ b/.changeset/cold-socks-juggle.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-graphcache': patch
+---
+
+Refactor to replace dictionary-based (`Object.create(null)`) results with regular objects
diff --git a/.changeset/wicked-cows-heal.md b/.changeset/wicked-cows-heal.md
new file mode 100644
index 00000000..9f4e6190
--- /dev/null
+++ b/.changeset/wicked-cows-heal.md
@@ -0,0 +1,5 @@
+---
+'@urql/core': patch
+---
+
+Add support for variables that contain non-plain objects without any enumerable keys, e.g. `File` or `Blob`. In this case `stringifyVariables` will now use a stable (but random) key, which means that mutations containing `File`s — or other objects like this — will now be distinct, as they should be.
diff --git a/exchanges/graphcache/src/ast/variables.ts b/exchanges/graphcache/src/ast/variables.ts
index 291d1650..a708e361 100644
--- a/exchanges/graphcache/src/ast/variables.ts
+++ b/exchanges/graphcache/src/ast/variables.ts
@@ -6,7 +6,6 @@ import {
 
 import { getName } from './node';
 
-import { makeDict } from '../helpers/dict';
 import { Variables } from '../types';
 
 /** Evaluates a fields arguments taking vars into account */
@@ -14,7 +13,7 @@ export const getFieldArguments = (
   node: FieldNode,
   vars: Variables
 ): null | Variables => {
-  const args = makeDict();
+  const args = {};
   let argsSize = 0;
   if (node.arguments && node.arguments.length) {
     for (let i = 0, l = node.arguments.length; i < l; i++) {
@@ -53,7 +52,7 @@ export const normalizeVariables = (
   node: OperationDefinitionNode,
   input: void | object
 ): Variables => {
-  const vars = makeDict();
+  const vars = {};
   if (!input) return vars;
 
   if (node.variableDefinitions) {
diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts
index f570e687..7807b1a9 100644
--- a/exchanges/graphcache/src/operations/query.ts
+++ b/exchanges/graphcache/src/operations/query.ts
@@ -35,7 +35,6 @@ import {
 
 import * as InMemoryData from '../store/data';
 import { warn, pushDebugNode } from '../helpers/help';
-import { makeDict } from '../helpers/dict';
 
 import {
   Context,
@@ -89,7 +88,7 @@ export const read = (
     pushDebugNode(rootKey, operation);
   }
 
-  let data = input || makeDict();
+  let data: Data | undefined = input || ({} as Data);
   data =
     rootKey !== ctx.store.rootFields['query']
       ? readRoot(ctx, rootKey, rootSelect, data)
@@ -113,7 +112,7 @@ const readRoot = (
   }
 
   const iter = new SelectionIterator(entityKey, entityKey, select, ctx);
-  const data = makeDict();
+  const data = {} as Data;
   data.__typename = originalData.__typename;
 
   let node: FieldNode | void;
@@ -150,7 +149,7 @@ const readRootField = (
   if (entityKey !== null) {
     // We assume that since this is used for result data this can never be undefined,
     // since the result data has already been written to the cache
-    const fieldValue = readSelection(ctx, entityKey, select, makeDict());
+    const fieldValue = readSelection(ctx, entityKey, select, {} as Data);
     return fieldValue === undefined ? null : fieldValue;
   } else {
     return readRoot(ctx, originalData.__typename, select, originalData);
@@ -211,7 +210,7 @@ export const readFragment = (
   );
 
   return (
-    readSelection(ctx, entityKey, getSelectionSet(fragment), makeDict()) || null
+    readSelection(ctx, entityKey, getSelectionSet(fragment), {} as Data) || null
   );
 };
 
@@ -290,7 +289,7 @@ const readSelection = (
 
       dataFieldValue = resolvers[fieldName](
         data,
-        fieldArgs || makeDict(),
+        fieldArgs || ({} as Data),
         store,
         ctx
       );
@@ -304,7 +303,7 @@ const readSelection = (
           fieldName,
           key,
           getSelectionSet(node),
-          (data[fieldAlias] as Data) || makeDict(),
+          (data[fieldAlias] || {}) as Data,
           dataFieldValue
         );
       }
@@ -417,7 +416,7 @@ const resolveResolverResult = (
   } else if (result === null || result === undefined) {
     return result;
   } else if (isDataOrKey(result)) {
-    const data = prevData === undefined ? makeDict() : prevData;
+    const data = (prevData === undefined ? {} : prevData) as Data;
     return typeof result === 'string'
       ? readSelection(ctx, result, select, data)
       : readSelection(ctx, key, select, data, result);
@@ -471,7 +470,7 @@ const resolveLink = (
       ctx,
       link,
       select,
-      prevData === undefined ? makeDict() : prevData
+      prevData === undefined ? ({} as any) : prevData
     );
   }
 };
diff --git a/exchanges/graphcache/src/operations/write.ts b/exchanges/graphcache/src/operations/write.ts
index cc712e8d..912647fc 100644
--- a/exchanges/graphcache/src/operations/write.ts
+++ b/exchanges/graphcache/src/operations/write.ts
@@ -27,7 +27,6 @@ import {
 } from '../store';
 
 import * as InMemoryData from '../store/data';
-import { makeDict } from '../helpers/dict';
 import {
   Context,
   SelectionIterator,
@@ -88,7 +87,7 @@ export const writeOptimistic = (
 
   const operation = getMainOperation(request.query);
   const result: WriteResult = {
-    data: makeDict(),
+    data: {} as Data,
     dependencies: getCurrentDependencies(),
   };
   const operationName = store.rootFields[operation.operation];
@@ -226,7 +225,7 @@ const writeSelection = (
       // We have to update the context to reflect up-to-date ResolveInfo
       updateContext(ctx, typename, typename, fieldKey, fieldName);
       fieldValue = data[fieldAlias] = ensureData(
-        resolver(fieldArgs || makeDict(), ctx.store, ctx)
+        resolver(fieldArgs || {}, ctx.store, ctx)
       );
     }
 
@@ -264,7 +263,7 @@ const writeSelection = (
       const updater = ctx.store.updates[typename][fieldName];
       if (updater) {
         data[fieldName] = fieldValue;
-        updater(data, fieldArgs || makeDict(), ctx.store, ctx);
+        updater(data, fieldArgs || {}, ctx.store, ctx);
       }
     }
   }
diff --git a/packages/core/src/utils/stringifyVariables.ts b/packages/core/src/utils/stringifyVariables.ts
index e8a2f3f1..2ddab6c4 100644
--- a/packages/core/src/utils/stringifyVariables.ts
+++ b/packages/core/src/utils/stringifyVariables.ts
@@ -1,4 +1,5 @@
 const seen = new Set();
+const cache = new WeakMap();
 
 const stringify = (x: any): string => {
   if (x === undefined) {
@@ -29,6 +30,15 @@ const stringify = (x: any): string => {
   }
 
   const keys = Object.keys(x).sort();
+  if (!keys.length && x.constructor && x.constructor !== Object) {
+    const key =
+      cache.get(x) ||
+      Math.random()
+        .toString(36)
+        .slice(2);
+    cache.set(x, key);
+    return `{"__key":"${key}"}`;
+  }
 
   seen.add(x);
   out = '{';


Test patch:
diff --git a/packages/core/src/utils/stringifyVariables.test.ts b/packages/core/src/utils/stringifyVariables.test.ts
index 784ea552..3489fb93 100644
--- a/packages/core/src/utils/stringifyVariables.test.ts
+++ b/packages/core/src/utils/stringifyVariables.test.ts
@@ -29,7 +29,22 @@ it('throws for circular structures', () => {
   }).toThrow();
 });
 
-it('stringifies date correctly', () => {
+it('stringifies dates correctly', () => {
   const date = new Date('2019-12-11T04:20:00');
   expect(stringifyVariables(date)).toBe(date.toJSON());
 });
+
+it('stringifies dictionaries (Object.create(null)) correctly', () => {
+  expect(stringifyVariables(Object.create(null))).toBe('{}');
+});
+
+it('stringifies files correctly', () => {
+  const file = new File([0] as any, 'test.js');
+  Object.defineProperty(file, 'lastModified', { value: 123 });
+  const str = stringifyVariables(file);
+  expect(str).toBe(stringifyVariables(file));
+
+  const otherFile = new File([0] as any, 'otherFile.js');
+  Object.defineProperty(otherFile, 'lastModified', { value: 234 });
+  expect(str).not.toBe(stringifyVariables(otherFile));
+});