Repository: FormidableLabs/urql (ts)

Issue:
[next-urql] Graphqlcache resolve twice
Hi,

 ```js
const cache = cacheExchange({
  schema,
  resolvers: {
    City: {
      name(parent, args, cache) {
        const name = cache.resolve(parent, "name");
        return "prefix-" + name;

        // return "prefix-" + parent.name;
      }
    }
  }
});

// index.js
const [{ data }] = useQuery({
    query: CITY_LIST_QUERY
  });

<div>
{data.cities.map(({ id, name }) => (
  <div key={id}>{name}</div>
))}
</div>
```
The `name` field resolve twice.

Result:
```js
prefix-prefix-An Giang
prefix-prefix-Bình Dương
```

Expect:
```js
prefix-An Giang
prefix-Bình Dương
```

Reproduction  https://codesandbox.io/s/distracted-wozniak-tlfvf?file=/pages/index.js

Pull request description:
(core) - Fix mutable data for SSR exchange
Resolves #961

## Summary

This changes the `ssrExchange` to serialize its `OperationResult` data early into JSON so that mutations to it don't get picked up. Such mutations are made by Graphcache for instance, which is a feature to make past data match resolvers and other conditions. Here however, we care about the earliest version of the data from the `fetchExchange`.

Test with Reproduction: https://codesandbox.io/s/distracted-wozniak-tlfvf?file=/pages/index.js

## Set of changes

- Update `SerializedResult[key: number]` to be of type `string | undefined`, i.e. JSON data
- Update tests and add new test to verify behavior


Patch:
diff --git a/.changeset/pink-crabs-clean.md b/.changeset/pink-crabs-clean.md
new file mode 100644
index 00000000..7879ed64
--- /dev/null
+++ b/.changeset/pink-crabs-clean.md
@@ -0,0 +1,5 @@
+---
+'@urql/core': patch
+---
+
+Fix condition where mutated result data would be picked up by the `ssrExchange`, for instance as a result of mutations by Graphcache. Instead the `ssrExchange` now serializes data early.
diff --git a/packages/core/src/exchanges/ssr.ts b/packages/core/src/exchanges/ssr.ts
index 53a56025..edb21646 100644
--- a/packages/core/src/exchanges/ssr.ts
+++ b/packages/core/src/exchanges/ssr.ts
@@ -4,7 +4,7 @@ import { Exchange, OperationResult, Operation } from '../types';
 import { CombinedError } from '../utils';
 
 export interface SerializedResult {
-  data?: any;
+  data?: string | undefined; // JSON string of data
   error?: {
     graphQLErrors: Array<Partial<GraphQLError> | string>;
     networkError?: string;
@@ -35,7 +35,11 @@ const serializeResult = ({
   data,
   error,
 }: OperationResult): SerializedResult => {
-  const result: SerializedResult = { data, error: undefined };
+  const result: SerializedResult = {
+    data: JSON.stringify(data),
+    error: undefined,
+  };
+
   if (error) {
     result.error = {
       graphQLErrors: error.graphQLErrors.map(error => {
@@ -59,10 +63,11 @@ const deserializeResult = (
   operation: Operation,
   result: SerializedResult
 ): OperationResult => {
-  const { error, data } = result;
+  const { error, data: dataJson } = result;
+
   const deserialized: OperationResult = {
     operation,
-    data,
+    data: dataJson ? JSON.parse(dataJson) : undefined,
     extensions: undefined,
     error: error
       ? new CombinedError({


Test patch:
diff --git a/packages/core/src/exchanges/ssr.test.ts b/packages/core/src/exchanges/ssr.test.ts
index f8e14abb..d2b3e161 100644
--- a/packages/core/src/exchanges/ssr.test.ts
+++ b/packages/core/src/exchanges/ssr.test.ts
@@ -2,7 +2,7 @@ import { makeSubject, pipe, map, publish, forEach, Subject } from 'wonka';
 
 import { Client } from '../client';
 import { queryOperation, queryResponse } from '../test-utils';
-import { ExchangeIO, Operation } from '../types';
+import { ExchangeIO, Operation, OperationResult } from '../types';
 import { CombinedError } from '../utils';
 import { ssrExchange } from './ssr';
 
@@ -12,6 +12,11 @@ let client: Client;
 let input: Subject<Operation>;
 let output;
 
+const serializedQueryResponse = {
+  ...queryResponse,
+  data: JSON.stringify(queryResponse.data),
+};
+
 beforeEach(() => {
   input = makeSubject<Operation>();
   output = jest.fn();
@@ -35,7 +40,43 @@ it('caches query results correctly', () => {
 
   expect(data).toEqual({
     [queryOperation.key]: {
-      data: queryResponse.data,
+      data: serializedQueryResponse.data,
+      error: undefined,
+    },
+  });
+});
+
+it('serializes query results quickly', () => {
+  const queryResponse: OperationResult = {
+    operation: queryOperation,
+    data: {
+      user: {
+        name: 'Clive',
+      },
+    },
+  };
+
+  const serializedQueryResponse = {
+    ...queryResponse,
+    data: JSON.stringify(queryResponse.data),
+  };
+
+  output.mockReturnValueOnce(queryResponse);
+
+  const ssr = ssrExchange();
+  const { source: ops$, next } = input;
+  const exchange = ssr(exchangeInput)(ops$);
+
+  publish(exchange);
+  next(queryOperation);
+  queryResponse.data.user.name = 'Not Clive';
+
+  const data = ssr.extractData();
+  expect(Object.keys(data)).toEqual(['' + queryOperation.key]);
+
+  expect(data).toEqual({
+    [queryOperation.key]: {
+      data: serializedQueryResponse.data,
       error: undefined,
     },
   });
@@ -62,7 +103,7 @@ it('caches errored query results correctly', () => {
 
   expect(data).toEqual({
     [queryOperation.key]: {
-      data: null,
+      data: 'null',
       error: {
         graphQLErrors: ['Oh no!'],
         networkError: undefined,
@@ -104,7 +145,7 @@ it('resolves cached query results correctly', () => {
   const onPush = jest.fn();
 
   const ssr = ssrExchange({
-    initialState: { [queryOperation.key]: queryResponse as any },
+    initialState: { [queryOperation.key]: serializedQueryResponse as any },
   });
 
   const { source: ops$, next } = input;
@@ -124,7 +165,7 @@ it('deletes cached results in non-suspense environments', async () => {
   const onPush = jest.fn();
   const ssr = ssrExchange();
 
-  ssr.restoreData({ [queryOperation.key]: queryResponse as any });
+  ssr.restoreData({ [queryOperation.key]: serializedQueryResponse as any });
   expect(Object.keys(ssr.extractData()).length).toBe(1);
 
   const { source: ops$, next } = input;
diff --git a/packages/react-urql/src/test-utils/ssr.test.tsx b/packages/react-urql/src/test-utils/ssr.test.tsx
index 4083cead..3342429d 100644
--- a/packages/react-urql/src/test-utils/ssr.test.tsx
+++ b/packages/react-urql/src/test-utils/ssr.test.tsx
@@ -149,7 +149,12 @@ describe('client-side rehydration', () => {
   });
 
   it('can rehydrate results on the client', async () => {
-    ssr.restoreData({ [queryOperation.key]: queryResponse });
+    ssr.restoreData({
+      [queryOperation.key]: {
+        ...queryResponse,
+        data: JSON.stringify(queryResponse.data),
+      },
+    });
 
     expect(() => {
       pipe(client.executeRequestOperation(queryOperation), publish);