Repository: FormidableLabs/urql
Pull request #804 (2020-05-10 15:14:36)

(persisted-fetch) - support persisted get
## Summary

Implements #799 

## Set of changes

- Makes the `persistedFetchExchange` accept options


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

Latest commit: c98cb111e0e14a88ebde25475f188cb36b4a7e90

**We got this.**

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

  | Name                           | Type  |
| ------------------------------ | ----- |
| @urql/exchange-persisted-fetch | Major |

</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/support-persisted-get?filename=.changeset/angry-adults-serve.md&value=---%0A%22%40urql%2Fexchange-persisted-fetch%22%3A%20patch%0A---%0A%0A(persisted-fetch)%20-%20support%20persisted%20get%0A)



Patch:
diff --git a/.changeset/big-worms-punch.md b/.changeset/big-worms-punch.md
new file mode 100644
index 00000000..8caca168
--- /dev/null
+++ b/.changeset/big-worms-punch.md
@@ -0,0 +1,25 @@
+---
+'@urql/exchange-persisted-fetch': major
+---
+
+Make the persistedFetchExchange accept an optional argument, this argument consists of one option `preferGetForPersistedQueries`.
+
+To migrate you will have to change from
+
+```js
+import { persistedFetchExchange } from '@urql/exchange-persisted-fetch';
+
+createClient({
+  exchanges: [persistedFetchExchange],
+});
+```
+
+to
+
+```js
+import { persistedFetchExchange } from '@urql/exchange-persisted-fetch';
+
+createClient({
+  exchanges: [persistedFetchExchange()],
+});
+```
diff --git a/.changeset/tiny-elephants-push.md b/.changeset/tiny-elephants-push.md
new file mode 100644
index 00000000..ff62e6df
--- /dev/null
+++ b/.changeset/tiny-elephants-push.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-persisted-fetch': patch
+---
+
+Replace `js-sha256` polyfill for Node.js support with Node's Native Crypto API
diff --git a/exchanges/persisted-fetch/package.json b/exchanges/persisted-fetch/package.json
index e8de6a17..d57378d3 100644
--- a/exchanges/persisted-fetch/package.json
+++ b/exchanges/persisted-fetch/package.json
@@ -50,7 +50,6 @@
   },
   "dependencies": {
     "@urql/core": ">=1.11.7",
-    "js-sha256": "^0.9.0",
     "wonka": "^4.0.10"
   },
   "peerDependencies": {
diff --git a/exchanges/persisted-fetch/src/persistedFetchExchange.ts b/exchanges/persisted-fetch/src/persistedFetchExchange.ts
index 17625218..c58a6a78 100644
--- a/exchanges/persisted-fetch/src/persistedFetchExchange.ts
+++ b/exchanges/persisted-fetch/src/persistedFetchExchange.ts
@@ -30,10 +30,15 @@ import {
 
 import { hash } from './sha256';
 
-export const persistedFetchExchange: Exchange = ({
-  forward,
-  dispatchDebug,
-}) => {
+interface PersistedFetchExchangeOptions {
+  preferGetForPersistedQueries?: boolean;
+}
+
+export const persistedFetchExchange = (
+  options?: PersistedFetchExchangeOptions
+): Exchange => ({ forward, dispatchDebug }) => {
+  if (!options) options = {};
+
   let supportsPersistedQueries = true;
 
   return ops$ => {
@@ -52,7 +57,7 @@ export const persistedFetchExchange: Exchange = ({
         if (!supportsPersistedQueries) {
           // Runs the usual non-persisted fetchExchange query logic
           return pipe(
-            makePersistedFetchSource(operation, body, dispatchDebug),
+            makePersistedFetchSource(operation, body, dispatchDebug, false),
             takeUntil(teardown$)
           );
         }
@@ -72,7 +77,13 @@ export const persistedFetchExchange: Exchange = ({
               },
             };
 
-            return makePersistedFetchSource(operation, body, dispatchDebug);
+            return makePersistedFetchSource(
+              operation,
+              body,
+              dispatchDebug,
+              !!(options as PersistedFetchExchangeOptions)
+                .preferGetForPersistedQueries
+            );
           }),
           mergeMap(result => {
             if (result.error && isPersistedUnsupported(result.error)) {
@@ -81,11 +92,21 @@ export const persistedFetchExchange: Exchange = ({
               body.extensions = undefined;
               // Disable future persisted queries
               supportsPersistedQueries = false;
-              return makePersistedFetchSource(operation, body, dispatchDebug);
+              return makePersistedFetchSource(
+                operation,
+                body,
+                dispatchDebug,
+                false
+              );
             } else if (result.error && isPersistedMiss(result.error)) {
               // Add query to the body but leave SHA256 hash intact
               body.query = query;
-              return makePersistedFetchSource(operation, body, dispatchDebug);
+              return makePersistedFetchSource(
+                operation,
+                body,
+                dispatchDebug,
+                false
+              );
             }
 
             return fromValue(result);
@@ -108,20 +129,28 @@ export const persistedFetchExchange: Exchange = ({
 const makePersistedFetchSource = (
   operation: Operation,
   body: FetchBody,
-  dispatchDebug: ExchangeInput['dispatchDebug']
+  dispatchDebug: ExchangeInput['dispatchDebug'],
+  useGet: boolean
 ): Source<OperationResult> => {
+  const newOperation = {
+    ...operation,
+    context: {
+      ...operation.context,
+      preferGetMethod: useGet || operation.context.preferGetMethod,
+    },
+  };
   const url = makeFetchURL(
-    operation,
+    newOperation,
     body.query ? body : { ...body, query: '' }
   );
-  const fetchOptions = makeFetchOptions(operation, body);
+  const fetchOptions = makeFetchOptions(newOperation, body);
 
   dispatchDebug({
     type: 'fetchRequest',
     message: !body.query
       ? 'A fetch request for a persisted query is being executed.'
       : 'A fetch request is being executed.',
-    operation,
+    operation: newOperation,
     data: {
       url,
       fetchOptions,
@@ -129,7 +158,7 @@ const makePersistedFetchSource = (
   });
 
   return pipe(
-    makeFetchSource(operation, url, fetchOptions),
+    makeFetchSource(newOperation, url, fetchOptions),
     onPush(result => {
       const persistFail =
         result.error &&
diff --git a/exchanges/persisted-fetch/src/sha256.ts b/exchanges/persisted-fetch/src/sha256.ts
index c37aa262..eaf87c6a 100644
--- a/exchanges/persisted-fetch/src/sha256.ts
+++ b/exchanges/persisted-fetch/src/sha256.ts
@@ -1,5 +1,3 @@
-import sha256Standard from 'js-sha256';
-
 const jsCrypto =
   typeof window !== 'undefined'
     ? window.crypto || (window as any).msCrypto
@@ -8,26 +6,15 @@ const cryptoSubtle =
   jsCrypto && (jsCrypto.subtle || (jsCrypto as any).webkitSubtle);
 const isIE = !!(jsCrypto && (window as any).msCrypto);
 
-const sha256 = (bytes: Uint8Array): Promise<Uint8Array> => {
-  if (!cryptoSubtle) {
-    if (process.env.NODE_ENV !== 'production') {
-      console.warn(
-        '[@urql/exchange-persisted-fetch]: The window.crypto.subtle API is not available.\n' +
-          'This is an unexpected error. Please report it by filing a GitHub Issue.'
-      );
-    }
-
-    return Promise.resolve(new Uint8Array(0));
-  }
-
-  const hash = cryptoSubtle.digest({ name: 'SHA-256' }, bytes);
+const sha256Browser = (bytes: Uint8Array): Promise<Uint8Array> => {
+  const hash = cryptoSubtle!.digest({ name: 'SHA-256' }, bytes);
   return new Promise((resolve, reject) => {
     if (isIE) {
       // IE11
-      (hash as any).oncomplete = function onComplete(event) {
+      (hash as any).oncomplete = function onComplete(event: any) {
         resolve(new Uint8Array(event.target.result));
       };
-      (hash as any).onerror = function onError(error) {
+      (hash as any).onerror = function onError(error: Error) {
         reject(error);
       };
     } else {
@@ -43,10 +30,34 @@ const sha256 = (bytes: Uint8Array): Promise<Uint8Array> => {
   });
 };
 
+const nodeCrypto =
+  typeof window === 'undefined' ? eval("require('crypto')") : null;
+
 export const hash = async (query: string): Promise<string> => {
+  if (
+    typeof window === 'undefined'
+      ? !nodeCrypto || !nodeCrypto.createHash
+      : !cryptoSubtle
+  ) {
+    if (process.env.NODE_ENV !== 'production') {
+      console.warn(
+        '[@urql/exchange-persisted-fetch]: The ' +
+          (typeof window === 'undefined'
+            ? 'Node Crypto'
+            : 'window.crypto.subtle') +
+          ' API is not available.\n' +
+          'This is an unexpected error. Please report it by filing a GitHub Issue.'
+      );
+    }
+
+    return Promise.resolve('');
+  }
+
   // Node.js support
   if (typeof window === 'undefined') {
-    return Promise.resolve(sha256Standard.sha256(query));
+    return Promise.resolve(
+      '' + nodeCrypto.createHash('sha256').update(query).digest('hex')
+    );
   }
 
   let buf: Uint8Array;
@@ -61,7 +72,7 @@ export const hash = async (query: string): Promise<string> => {
     }
   }
 
-  const out = await sha256(buf);
+  const out = await sha256Browser(buf);
 
   let hash = '';
   for (let i = 0, l = out.length; i < l; i++) {
diff --git a/yarn.lock b/yarn.lock
index e1eabad7..791150af 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8308,11 +8308,6 @@ js-levenshtein@^1.1.3:
   resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
   integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
 
-js-sha256@^0.9.0:
-  version "0.9.0"
-  resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"
-  integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==
-
 "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"


Test patch:
diff --git a/exchanges/persisted-fetch/src/persistedFetchExchange.test.ts b/exchanges/persisted-fetch/src/persistedFetchExchange.test.ts
index 5f41ac9a..79be2811 100644
--- a/exchanges/persisted-fetch/src/persistedFetchExchange.test.ts
+++ b/exchanges/persisted-fetch/src/persistedFetchExchange.test.ts
@@ -48,7 +48,7 @@ it('accepts successful persisted query responses', async () => {
 
   const actual = await pipe(
     fromValue(queryOperation),
-    persistedFetchExchange(exchangeArgs),
+    persistedFetchExchange()(exchangeArgs),
     toPromise
   );
 
@@ -74,7 +74,7 @@ it('supports cache-miss persisted query errors', async () => {
 
   const actual = await pipe(
     fromValue(queryOperation),
-    persistedFetchExchange(exchangeArgs),
+    persistedFetchExchange()(exchangeArgs),
     toPromise
   );
 
@@ -84,6 +84,35 @@ it('supports cache-miss persisted query errors', async () => {
   expect(actual.data).not.toBeUndefined();
 });
 
+it('supports GET exclusively for persisted queries', async () => {
+  const expectedMiss = {
+    errors: [{ message: 'PersistedQueryNotFound' }],
+  };
+
+  const expectedRetry = {
+    data: {
+      test: true,
+    },
+  };
+
+  fetch
+    .mockResolvedValueOnce({ json: () => expectedMiss })
+    .mockResolvedValueOnce({ json: () => expectedRetry });
+
+  const actual = await pipe(
+    fromValue(queryOperation),
+    persistedFetchExchange({ preferGetForPersistedQueries: true })(
+      exchangeArgs
+    ),
+    toPromise
+  );
+
+  expect(fetch).toHaveBeenCalledTimes(2);
+  expect(fetch.mock.calls[0][1].method).toEqual('GET');
+  expect(fetch.mock.calls[1][1].method).toEqual('POST');
+  expect(actual.data).not.toBeUndefined();
+});
+
 it('supports unsupported persisted query errors', async () => {
   const expectedMiss = {
     errors: [{ message: 'PersistedQueryNotSupported' }],
@@ -102,7 +131,7 @@ it('supports unsupported persisted query errors', async () => {
 
   const actual = await pipe(
     fromArray([queryOperation, queryOperation]),
-    persistedFetchExchange(exchangeArgs),
+    persistedFetchExchange()(exchangeArgs),
     toPromise
   );
 
@@ -118,7 +147,7 @@ it('ignores mutations', async () => {
 
   pipe(
     fromValue(mutationOperation),
-    persistedFetchExchange(exchangeArgs),
+    persistedFetchExchange()(exchangeArgs),
     subscribe(result)
   );
 
@@ -143,7 +172,7 @@ it('correctly generates an SHA256 hash', async () => {
 
   await pipe(
     fromValue(queryOperation),
-    persistedFetchExchange(exchangeArgs),
+    persistedFetchExchange()(exchangeArgs),
     toPromise
   );
 
diff --git a/exchanges/persisted-fetch/src/sha256.test.ts b/exchanges/persisted-fetch/src/sha256.test.ts
deleted file mode 100644
index c3e059ba..00000000
--- a/exchanges/persisted-fetch/src/sha256.test.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * @jest-environment node
- */
-
-import { sha256 as sha25Standard } from 'js-sha256';
-import { hash } from './sha256';
-
-describe('hash', () => {
-  it('is spec-compliant', async () => {
-    expect(await hash('Hello')).toMatchInlineSnapshot(
-      `"185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"`
-    );
-    expect(await hash('Hello')).toBe(sha25Standard('Hello'));
-  });
-});