Repository: FormidableLabs/urql
Pull request #657 (2020-03-22 18:55:15)

(graphcache) - optimistic support for mutations without selection
## Summary

This PR adds support for  adding optimistic values when there's no selectionSet for your mutation.

## Set of changes

- Add optimistic support for mutations without selectionset


changeset-bot[bot]: ###  🦋  Changeset is good to go

Latest commit: 63bf80c26e1ed88f8471a62c0dbe547e194b6bf5

**We got this.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                      | Type  |
| ------------------------- | ----- |
| @urql/exchange-graphcache | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/FormidableLabs/urql/new/feat/optimistic-scalars?filename=.changeset/moody-lizards-type.md&value=---%0A%22%40urql%2Fexchange-graphcache%22%3A%20patch%0A---%0A%0A(graphcache)%20-%20optimistic%20support%20for%20mutations%20without%20selection%0A)



Patch:
diff --git a/.changeset/twelve-trainers-sing.md b/.changeset/twelve-trainers-sing.md
new file mode 100644
index 00000000..71b07614
--- /dev/null
+++ b/.changeset/twelve-trainers-sing.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-graphcache': minor
+---
+
+Support optimistic values for mutations without a selectionset
diff --git a/exchanges/graphcache/src/operations/write.ts b/exchanges/graphcache/src/operations/write.ts
index 047cacc8..cc712e8d 100644
--- a/exchanges/graphcache/src/operations/write.ts
+++ b/exchanges/graphcache/src/operations/write.ts
@@ -190,7 +190,8 @@ const writeSelection = (
     const fieldName = getName(node);
     const fieldArgs = getFieldArguments(node, ctx.variables);
     const fieldKey = keyOfField(fieldName, fieldArgs);
-    const fieldValue = data[getFieldAlias(node)];
+    const fieldAlias = getFieldAlias(node);
+    let fieldValue = data[fieldAlias];
 
     if (process.env.NODE_ENV !== 'production') {
       if (!isRoot && fieldValue === undefined) {
@@ -219,35 +220,34 @@ const writeSelection = (
       }
     }
 
-    if (node.selectionSet) {
-      let fieldData: Data | NullArray<Data> | null;
-      // Process optimistic updates, if this is a `writeOptimistic` operation
-      // otherwise read the field value from data and write it
-      if (ctx.optimistic && isRoot) {
-        const resolver = ctx.store.optimisticMutations[fieldName];
-        if (!resolver) continue;
-        // We have to update the context to reflect up-to-date ResolveInfo
-        updateContext(ctx, typename, typename, fieldKey, fieldName);
-        fieldData = ensureData(
-          resolver(fieldArgs || makeDict(), ctx.store, ctx)
-        );
-        data[fieldName] = fieldData;
-      } else {
-        fieldData = ensureData(fieldValue);
-      }
+    if (ctx.optimistic && isRoot) {
+      const resolver = ctx.store.optimisticMutations[fieldName];
+      if (!resolver) continue;
+      // 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)
+      );
+    }
 
+    if (node.selectionSet) {
       // Process the field and write links for the child entities that have been written
       if (entityKey && !isRoot) {
         const key = joinKeys(entityKey, fieldKey);
-        const link = writeField(ctx, getSelectionSet(node), fieldData, key);
+        const link = writeField(
+          ctx,
+          getSelectionSet(node),
+          ensureData(fieldValue),
+          key
+        );
         InMemoryData.writeLink(entityKey || typename, fieldKey, link);
       } else {
-        writeField(ctx, getSelectionSet(node), fieldData);
+        writeField(ctx, getSelectionSet(node), ensureData(fieldValue));
       }
     } else if (entityKey && !isRoot) {
       // This is a leaf node, so we're setting the field's value directly
       InMemoryData.writeRecord(entityKey || typename, fieldKey, fieldValue);
-    } else if (ctx.optimistic && isRoot) continue;
+    }
 
     if (isRoot) {
       // We have to update the context to reflect up-to-date ResolveInfo
@@ -263,6 +263,7 @@ const writeSelection = (
       // so that the data is already available in-store if necessary
       const updater = ctx.store.updates[typename][fieldName];
       if (updater) {
+        data[fieldName] = fieldValue;
         updater(data, fieldArgs || makeDict(), ctx.store, ctx);
       }
     }
diff --git a/exchanges/suspense/package.json b/exchanges/suspense/package.json
index c61d4b9c..06f32af2 100644
--- a/exchanges/suspense/package.json
+++ b/exchanges/suspense/package.json
@@ -23,14 +23,6 @@
   "module": "dist/urql-exchange-suspense.es.js",
   "types": "dist/types/index.d.ts",
   "source": "src/index.ts",
-  "exports": {
-    ".": {
-      "import": "./dist/urql-exchange-suspense.mjs",
-      "require": "./dist/urql-exchange-suspense.js",
-      "types": "./dist/types/index.d.ts",
-      "source": "./src/index.ts"
-    }
-  },
   "files": [
     "LICENSE",
     "CHANGELOG.md",


Test patch:
diff --git a/exchanges/graphcache/src/cacheExchange.test.ts b/exchanges/graphcache/src/cacheExchange.test.ts
index 38cfbc18..b3f10849 100644
--- a/exchanges/graphcache/src/cacheExchange.test.ts
+++ b/exchanges/graphcache/src/cacheExchange.test.ts
@@ -397,7 +397,7 @@ describe('data dependencies', () => {
     expect(result).toHaveBeenCalledTimes(2);
   });
 
-  it('writes optimistic mutations to the cache', () => {
+  it('does not reach updater when mutation has no selectionset in optimistic phase', () => {
     jest.useFakeTimers();
 
     const mutation = gql`
@@ -452,6 +452,133 @@ describe('data dependencies', () => {
     jest.runAllTimers();
     expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(1);
   });
+
+  it('does reach updater when mutation has no selectionset in optimistic phase with optimistic update', () => {
+    jest.useFakeTimers();
+
+    const mutation = gql`
+      mutation {
+        concealAuthor
+      }
+    `;
+
+    const mutationData = {
+      __typename: 'Mutation',
+      concealAuthor: true,
+    };
+
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const { source: ops$, next } = makeSubject<Operation>();
+
+    jest.spyOn(client, 'reexecuteOperation').mockImplementation(next);
+
+    const opMutation = client.createRequestOperation('mutation', {
+      key: 1,
+      query: mutation,
+    });
+
+    const response = jest.fn(
+      (forwardOp: Operation): OperationResult => {
+        if (forwardOp.key === 1) {
+          return { operation: opMutation, data: mutationData };
+        }
+
+        return undefined as any;
+      }
+    );
+
+    const result = jest.fn();
+    const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response));
+
+    const updates = {
+      Mutation: {
+        concealAuthor: jest.fn(),
+      },
+    };
+
+    const optimistic = {
+      concealAuthor: jest.fn(() => true) as any,
+    };
+
+    pipe(
+      cacheExchange({ updates, optimistic })({ forward, client })(ops$),
+      tap(result),
+      publish
+    );
+
+    next(opMutation);
+    expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1);
+    expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(1);
+
+    jest.runAllTimers();
+    expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(2);
+  });
+
+  it('respects aliases in the optimistic update data that is written', () => {
+    jest.useFakeTimers();
+
+    const mutation = gql`
+      mutation {
+        concealed: concealAuthor
+      }
+    `;
+
+    const mutationData = {
+      __typename: 'Mutation',
+      concealed: true,
+    };
+
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const { source: ops$, next } = makeSubject<Operation>();
+
+    jest.spyOn(client, 'reexecuteOperation').mockImplementation(next);
+
+    const opMutation = client.createRequestOperation('mutation', {
+      key: 1,
+      query: mutation,
+    });
+
+    const response = jest.fn(
+      (forwardOp: Operation): OperationResult => {
+        if (forwardOp.key === 1) {
+          return { operation: opMutation, data: mutationData };
+        }
+
+        return undefined as any;
+      }
+    );
+
+    const result = jest.fn();
+    const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response));
+
+    const updates = {
+      Mutation: {
+        concealAuthor: jest.fn(),
+      },
+    };
+
+    const optimistic = {
+      concealAuthor: jest.fn(() => true) as any,
+    };
+
+    pipe(
+      cacheExchange({ updates, optimistic })({ forward, client })(ops$),
+      tap(result),
+      publish
+    );
+
+    next(opMutation);
+    expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1);
+    expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(1);
+
+    const data = updates.Mutation.concealAuthor.mock.calls[0][0];
+    // Expect both fields to exist
+    expect(data.concealed).toBe(true);
+    expect(data.concealAuthor).toBe(true);
+
+    jest.runAllTimers();
+    expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(2);
+  });
 });
 
 describe('optimistic updates', () => {