Repository: urql-graphql/urql
Pull request #3059 (2023-03-16 00:19:47)

fix(core/graphcache): Add missing hasNext/stale flags to cached results
## Summary

Ensures that the `ssrExchange` and Graphcache's `cacheExchange` don't accidentally remove `hasNext` (or `stale`) flags.

## Set of changes

- Add `hasNext` passthrough to Graphcache
- Add `hasNext` passthrough to `ssrExchange`



Patch:
diff --git a/.changeset/polite-penguins-notice.md b/.changeset/polite-penguins-notice.md
new file mode 100644
index 00000000..72bd7480
--- /dev/null
+++ b/.changeset/polite-penguins-notice.md
@@ -0,0 +1,6 @@
+---
+'@urql/exchange-graphcache': patch
+'@urql/core': patch
+---
+
+Add missing `hasNext` and `stale` passthroughs on caching exchanges.
diff --git a/exchanges/graphcache/src/cacheExchange.ts b/exchanges/graphcache/src/cacheExchange.ts
index 5e8ff9c7..8eb26d50 100644
--- a/exchanges/graphcache/src/cacheExchange.ts
+++ b/exchanges/graphcache/src/cacheExchange.ts
@@ -195,8 +195,6 @@ export const cacheExchange = <C extends Partial<CacheExchangeOpts>>(
     result: OperationResult,
     pendingOperations: Operations
   ): OperationResult => {
-    const { error, extensions } = result;
-
     // Retrieve the original operation to remove changes made by formatDocument
     const originalOperation = operations.get(result.operation.key);
     const operation = originalOperation
@@ -261,7 +259,14 @@ export const cacheExchange = <C extends Partial<CacheExchangeOpts>>(
       updateDependencies(result.operation, queryDependencies);
     }
 
-    return { data, error, extensions, operation };
+    return {
+      operation,
+      data,
+      error: result.error,
+      extensions: result.extensions,
+      hasNext: result.hasNext,
+      stale: result.stale,
+    };
   };
 
   return ops$ => {
@@ -339,6 +344,7 @@ export const cacheExchange = <C extends Partial<CacheExchangeOpts>>(
             error: res.error,
             extensions: res.extensions,
             stale: shouldReexecute || isPartial,
+            hasNext: false,
           };
 
           if (!shouldReexecute) {
diff --git a/packages/core/src/exchanges/ssr.ts b/packages/core/src/exchanges/ssr.ts
index 7b8583c7..2f7732b9 100644
--- a/packages/core/src/exchanges/ssr.ts
+++ b/packages/core/src/exchanges/ssr.ts
@@ -105,19 +105,25 @@ export interface SSRExchange extends Exchange {
 
 /** Serialize an OperationResult to plain JSON */
 const serializeResult = (
-  { hasNext, data, extensions, error }: OperationResult,
+  result: OperationResult,
   includeExtensions: boolean
 ): SerializedResult => {
-  const result: SerializedResult = {};
-  if (data !== undefined) result.data = JSON.stringify(data);
-  if (includeExtensions && extensions !== undefined) {
-    result.extensions = JSON.stringify(extensions);
+  const serialized: SerializedResult = {
+    data: JSON.stringify(result.data),
+    hasNext: result.hasNext,
+  };
+
+  if (result.data !== undefined) {
+    serialized.data = JSON.stringify(result.data);
+  }
+
+  if (includeExtensions && result.extensions !== undefined) {
+    serialized.extensions = JSON.stringify(result.extensions);
   }
-  if (hasNext) result.hasNext = true;
 
-  if (error) {
-    result.error = {
-      graphQLErrors: error.graphQLErrors.map(error => {
+  if (result.error) {
+    serialized.error = {
+      graphQLErrors: result.error.graphQLErrors.map(error => {
         if (!error.path && !error.extensions) return error.message;
 
         return {
@@ -128,12 +134,12 @@ const serializeResult = (
       }),
     };
 
-    if (error.networkError) {
-      result.error.networkError = '' + error.networkError;
+    if (result.error.networkError) {
+      serialized.error.networkError = '' + result.error.networkError;
     }
   }
 
-  return result;
+  return serialized;
 };
 
 /** Deserialize plain JSON to an OperationResult
@@ -158,7 +164,8 @@ const deserializeResult = (
         graphQLErrors: result.error.graphQLErrors,
       })
     : undefined,
-  hasNext: result.hasNext,
+  stale: false,
+  hasNext: !!result.hasNext,
 });
 
 const revalidated = new Set<number>();


Test patch:
diff --git a/exchanges/graphcache/src/offlineExchange.test.ts b/exchanges/graphcache/src/offlineExchange.test.ts
index a6ffebc9..384eec39 100644
--- a/exchanges/graphcache/src/offlineExchange.test.ts
+++ b/exchanges/graphcache/src/offlineExchange.test.ts
@@ -226,6 +226,7 @@ describe('offline', () => {
       error: undefined,
       extensions: undefined,
       operation: expect.any(Object),
+      hasNext: false,
       stale: false,
     });
 
diff --git a/packages/core/src/exchanges/ssr.test.ts b/packages/core/src/exchanges/ssr.test.ts
index f2d1dd55..fe3d62b2 100644
--- a/packages/core/src/exchanges/ssr.test.ts
+++ b/packages/core/src/exchanges/ssr.test.ts
@@ -198,6 +198,8 @@ it('resolves cached query results correctly', () => {
   expect(output).not.toHaveBeenCalled();
   expect(onPush).toHaveBeenCalledWith({
     ...queryResponse,
+    stale: false,
+    hasNext: false,
     operation: {
       ...queryResponse.operation,
       context: {
@@ -234,8 +236,9 @@ it('resolves deferred, cached query results correctly', () => {
   expect(output).toHaveBeenCalledTimes(1);
   expect(onPush).toHaveBeenCalledTimes(2);
   expect(onPush.mock.calls[1][0]).toEqual({
-    hasNext: true,
     ...queryResponse,
+    hasNext: true,
+    stale: false,
     operation: {
       ...queryResponse.operation,
       context: {
@@ -267,6 +270,8 @@ it('deletes cached results in non-suspense environments', async () => {
   expect(Object.keys(ssr.extractData()).length).toBe(0);
   expect(onPush).toHaveBeenCalledWith({
     ...queryResponse,
+    stale: false,
+    hasNext: false,
     operation: {
       ...queryResponse.operation,
       context: {