Repository: FormidableLabs/urql
Pull request #1985 (2021-09-23 10:17:17)

(core) - Add `includeExtensions` option to `ssrExchange`
## Summary

I opened a discussion about this some months ago [here](https://github.com/FormidableLabs/urql/discussions/1764), but it did not get much attention so I'm trying to implement this myself :)

Adding a new `includeExtensions?: boolean` option to `ssrExchange`. When this option is enabled the `extensions` field of the operation results will be included in the serialised cache, and will be restored when restoring the cache.

The motivation behind this is to make cached and non-cached operations behave more consistently.
For example, when `extensions` include some metadata for the consumer to use (e.g. tracing/tracking info), then these data are not available when restoring the cache currently, as the `extensions` field don't get serialised/restored.

I added this as an option instead of making it the default behaviour to make this change backward compatible.

## Set of changes

- `@urql/core`: Adding `includeExtensions` as option to `ssrExchange`, and implemented the feature. Added a scenario to unit test to cover this.
- `urql-exchange-graphcache-default-storage`: Updating `@urql/core` dependency version to latest. I did not do this manually, this got updated when running `changeset`. I'm happy to revert this if you think it's a problem.


changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: 96e7658c2f0c01a9cb279e313591823a0beb38d7

**The changes in this PR will be included in the next version bump.**

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

  | Name       | Type  |
| ---------- | ----- |
| @urql/core | Patch |

</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/dios-david/urql/new/add-includeExtensions-to-ssrCache?filename=.changeset/fast-eagles-grin.md&value=---%0A%22%40urql%2Fcore%22%3A%20patch%0A%22%40urql%2Fexchange-graphcache%22%3A%20patch%0A---%0A%0AAdd%20%60includeExtensions%60%20option%20to%20%60ssrExchange%60%0A)



Patch:
diff --git a/.changeset/tidy-tables-promise.md b/.changeset/tidy-tables-promise.md
new file mode 100644
index 00000000..f4af3df8
--- /dev/null
+++ b/.changeset/tidy-tables-promise.md
@@ -0,0 +1,5 @@
+---
+'@urql/core': patch
+---
+
+Adding option to `ssrExchange` to include the `extensions` field of operation results in the cache.
diff --git a/exchanges/graphcache/default-storage/package.json b/exchanges/graphcache/default-storage/package.json
index 9067571b..1a6cc69b 100644
--- a/exchanges/graphcache/default-storage/package.json
+++ b/exchanges/graphcache/default-storage/package.json
@@ -16,7 +16,7 @@
     "./package.json": "./package.json"
   },
   "dependencies": {
-    "@urql/core": ">=2.1.5",
+    "@urql/core": ">=2.3.2",
     "wonka": "^4.0.14"
   }
 }
diff --git a/packages/core/src/exchanges/ssr.ts b/packages/core/src/exchanges/ssr.ts
index 3590662b..855d63ff 100644
--- a/packages/core/src/exchanges/ssr.ts
+++ b/packages/core/src/exchanges/ssr.ts
@@ -7,6 +7,7 @@ import { reexecuteOperation } from './cache';
 export interface SerializedResult {
   hasNext?: boolean;
   data?: string | undefined; // JSON string of data
+  extensions?: string | undefined; // JSON string of data
   error?: {
     graphQLErrors: Array<Partial<GraphQLError> | string>;
     networkError?: string;
@@ -21,6 +22,7 @@ export interface SSRExchangeParams {
   isClient?: boolean;
   initialState?: SSRData;
   staleWhileRevalidate?: boolean;
+  includeExtensions?: boolean;
 }
 
 export interface SSRExchange extends Exchange {
@@ -31,13 +33,15 @@ export interface SSRExchange extends Exchange {
 }
 
 /** Serialize an OperationResult to plain JSON */
-const serializeResult = ({
-  hasNext,
-  data,
-  error,
-}: OperationResult): SerializedResult => {
+const serializeResult = (
+  { hasNext, data, extensions, error }: OperationResult,
+  includeExtensions: boolean
+): SerializedResult => {
   const result: SerializedResult = {};
   if (data !== undefined) result.data = JSON.stringify(data);
+  if (includeExtensions && extensions !== undefined) {
+    result.extensions = JSON.stringify(extensions);
+  }
   if (hasNext) result.hasNext = true;
 
   if (error) {
@@ -64,11 +68,15 @@ const serializeResult = ({
 /** Deserialize plain JSON to an OperationResult */
 const deserializeResult = (
   operation: Operation,
-  result: SerializedResult
+  result: SerializedResult,
+  includeExtensions: boolean
 ): OperationResult => ({
   operation,
   data: result.data ? JSON.parse(result.data) : undefined,
-  extensions: undefined,
+  extensions:
+    includeExtensions && result.extensions
+      ? JSON.parse(result.extensions)
+      : undefined,
   error: result.error
     ? new CombinedError({
         networkError: result.error.networkError
@@ -85,6 +93,7 @@ const revalidated = new Set<number>();
 /** The ssrExchange can be created to capture data during SSR and also to rehydrate it on the client */
 export const ssrExchange = (params?: SSRExchangeParams): SSRExchange => {
   const staleWhileRevalidate = !!(params && params.staleWhileRevalidate);
+  const includeExtensions = !!(params && params.includeExtensions);
   const data: Record<string, SerializedResult | null> = {};
 
   // On the client-side, we delete results from the cache as they're resolved
@@ -129,7 +138,7 @@ export const ssrExchange = (params?: SSRExchangeParams): SSRExchange => {
       filter(operation => !!data[operation.key]),
       map(op => {
         const serialized = data[op.key]!;
-        const result = deserializeResult(op, serialized);
+        const result = deserializeResult(op, serialized, includeExtensions);
         if (staleWhileRevalidate && !revalidated.has(op.key)) {
           result.stale = true;
           revalidated.add(op.key);
@@ -147,7 +156,7 @@ export const ssrExchange = (params?: SSRExchangeParams): SSRExchange => {
         tap((result: OperationResult) => {
           const { operation } = result;
           if (operation.kind !== 'mutation') {
-            const serialized = serializeResult(result);
+            const serialized = serializeResult(result, includeExtensions);
             data[operation.key] = serialized;
           }
         })


Test patch:
diff --git a/packages/core/src/exchanges/ssr.test.ts b/packages/core/src/exchanges/ssr.test.ts
index 76b0548a..a30e47b7 100644
--- a/packages/core/src/exchanges/ssr.test.ts
+++ b/packages/core/src/exchanges/ssr.test.ts
@@ -116,6 +116,34 @@ it('caches errored query results correctly', () => {
   });
 });
 
+it('caches extensions when includeExtensions=true', () => {
+  output.mockReturnValueOnce({
+    ...queryResponse,
+    extensions: {
+      foo: 'bar',
+    },
+  });
+
+  const ssr = ssrExchange({
+    includeExtensions: true,
+  });
+  const { source: ops$, next } = input;
+  const exchange = ssr(exchangeInput)(ops$);
+
+  publish(exchange);
+  next(queryOperation);
+
+  const data = ssr.extractData();
+  expect(Object.keys(data)).toEqual(['' + queryOperation.key]);
+
+  expect(data).toEqual({
+    [queryOperation.key]: {
+      data: '{"user":{"name":"Clive"}}',
+      extensions: '{"foo":"bar"}',
+    },
+  });
+});
+
 it('caches complex GraphQLErrors in query results correctly', () => {
   output.mockReturnValueOnce({
     ...queryResponse,