Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

feat(persisted): Replace persistedFetchExchange with generic persistedExchange (#3057)

+5
.changeset/pretty-cows-dance.md
···
+
---
+
'@urql/core': patch
+
---
+
+
Add logic for `request.extensions.persistedQuery` to `@urql/core` to omit sending `query` as needed.
+5
.changeset/strange-apples-trade.md
···
+
---
+
'@urql/exchange-persisted-fetch': major
+
---
+
+
Remove `persistedFetchExchange` and instead implement `persistedExchange`. This exchange must be placed in front of a terminating exchange (such as the default `fetchExchange` or a `subscriptionExchange` that supports persisted queries), and only modifies incoming operations to contain `extensions.persistedQuery`, which is sent on via the API. If the API expects Automatic Persisted Queries, requests are retried by this exchange internally.
+5
.changeset/three-poets-think.md
···
+
---
+
'@urql/exchange-persisted-fetch': major
+
---
+
+
Rename `@urql/exchange-persisted-fetch` to `@urql/exchange-persisted`
+37 -69
docs/advanced/persistence-and-uploads.md
···
# Persisted Queries and Uploads
`urql` supports both [Automatic Persisted
-
Queries](https://www.apollographql.com/docs/apollo-server/performance/apq/) and [File
-
Uploads](https://www.apollographql.com/docs/apollo-server/data/file-uploads/).
-
Both of these features are implemented by enhancing or swapping out the default
-
[`fetchExchange`](../api/core.md#fetchexchange).
+
Queries](https://www.apollographql.com/docs/apollo-server/performance/apq/), Persisted Queries, and
+
[File Uploads](https://www.apollographql.com/docs/apollo-server/data/file-uploads/).
+
+
While File Uploads should work without any modifications, an additional exchange must be installed
+
and added for Persisted Queries to work.
## Automatic Persisted Queries
···
requests. If we only send the persisted queries with hashes as GET requests then they become a lot
easier for a CDN to cache, as by default most caches would not cache POST requests automatically.
-
In `urql`, we may use the `@urql/exchange-persisted-fetch` package's `persistedFetchExchange` to
-
implement Automatic Persisted Queries. This exchange works alongside other fetch exchanges and only
-
handles `query` operations.
+
In `urql`, we may use the `@urql/exchange-persisted` package's `persistedExchange` to
+
implement Automatic Persisted Queries. This exchange works alongside the default `fetchExchange`
+
and other exchanges by adding the `extensions.persistedQuery` parameters to a GraphQL request.
### Installation & Setup
-
First install `@urql/exchange-persisted-fetch` alongside `urql`:
+
First install `@urql/exchange-persisted` alongside `urql`:
```sh
-
yarn add @urql/exchange-persisted-fetch
+
yarn add @urql/exchange-persisted
# or
-
npm install --save @urql/exchange-persisted-fetch
+
npm install --save @urql/exchange-persisted
```
-
You'll then need to add the `persistedFetchExchange` method, that this package exposes,
+
You'll then need to add the `persistedExchange` function, that this package exposes,
to your `exchanges`.
```js
import { createClient, dedupExchange, fetchExchange, cacheExchange } from 'urql';
-
import { persistedFetchExchange } from '@urql/exchange-persisted-fetch';
+
import { persistedExchange } from '@urql/exchange-persisted-fetch';
const client = createClient({
url: 'http://localhost:1234/graphql',
exchanges: [
dedupExchange,
cacheExchange,
-
persistedFetchExchange({
+
persistedExchange({
preferGetForPersistedQueries: true,
}),
fetchExchange,
···
As we can see, typically it's recommended to set `preferGetForPersistedQueries` to `true` to force
all persisted queries to use GET requests instead of POST so that CDNs can do their job.
-
We also added the `persistedFetchExchange` in front of the usual `fetchExchange`, since it only
-
handles queries but not mutations.
+
We also added the `persistedExchange` in front of the usual `fetchExchange`, since it has to
+
update operations before they reach an exchange that talks to an API.
The `preferGetForPersistedQueries` is similar to the [`Client`'s
`preferGetMethod`](../api/core.md#client) but only switches persisted queries to use GET requests
instead. This is preferable since sometimes the GraphQL query can grow too large for a simple GET
-
query to handle, while the `persistedFetchExchange`'s SHA256 hashes will remain predictably small.
+
query to handle, while the `persistedExchange`'s SHA256 hashes will remain predictably small.
### Customizing Hashing
-
The `persistedFetchExchange` also accepts a `generateHash` option. This may be used to swap out the
+
The `persistedExchange` also accepts a `generateHash` option. This may be used to swap out the
exchange's default method of generating SHA256 hashes. By default, the exchange will use the
-
built-in [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) on the
-
browser, which has been implemented to support IE11 as well. In Node.js it'll use the [Node
-
Crypto Module](https://nodejs.org/api/crypto.html) instead.
+
built-in [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) when it's
+
available, and in Node.js it'll use the [Node Crypto Module](https://nodejs.org/api/crypto.html)
+
instead.
If you're using [the `graphql-persisted-document-loader` for
-
Webpack](https://github.com/leoasis/graphql-persisted-document-loader) for instance, then you will
+
Webpack](https://github.com/leoasis/graphql-persisted-document-loader), for instance, then you will
already have a loader generating SHA256 hashes for you at compile time. In that case we could swap
out the `generateHash` function with a much simpler one that uses the `generateHash` function's
second argument, a GraphQL `DocumentNode` object.
···
```
If you're using **React Native** then you may not have access to the Web Crypto API, which means
-
that you have to provide your own SHA256 function to the `persistedFetchExchange`. Luckily we can do
+
that you have to provide your own SHA256 function to the `persistedExchange`. Luckily, we can do
so easily by using the first argument `generateHash` receives, a GraphQL query as a string.
```js
···
```
Additionally, if the API only expects persisted queries and not arbitrary ones and all queries are
-
pre-registered against the API then the `persistedFetchExchange` may be put into a **non-automatic**
+
pre-registered against the API then the `persistedExchange` may be put into a **non-automatic**
persisted queries mode by giving it the `enforcePersistedQueries: true` option. This disables any
retry logic and assumes that persisted queries will be handled like regular GraphQL requests.
-
[Read more about `@urql/persisted-fetch-exchange` in our API
-
docs.](../api/persisted-fetch-exchange.md)
-
## File Uploads
-
GraphQL server frameworks like [Apollo Server support an unofficial spec for file
-
uploads.](https://www.apollographql.com/docs/apollo-server/data/file-uploads/) This allows us to
-
define mutations on our API that accept an `Upload` input, which on the client would be a variable
-
that we can set to a [File](https://developer.mozilla.org/en-US/docs/Web/API/File), which we'd
-
typically retrieve via a [file input for
-
instance](https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications).
+
Many GraphQL server frameworks and APIs support the ["GraphQL Multipart Request
+
Spec](https://github.com/jaydenseric/graphql-multipart-request-spec) to allow files to be uploaded.
+
Often, this is defined in schemas using a `File` or `Upload` input.
+
This allows us to pass a `File` or `Blob` directly to our GraphQL requests as variables, and the
+
spec requires us to perform this request as a multipart upload.
-
In `urql`, we may use the `@urql/exchange-multipart-fetch` package's `multipartFetchExchange` to
-
support file uploads, which is a drop-in replacement for the default
-
[`fetchExchange`](../api/core.md#fetchexchange). It may also be used [alongside the
-
`persistedFetchExchange`](#automatic-persisted-queries).
+
Files are often handled in the browser via the [File API](https://developer.mozilla.org/en-US/docs/Web/API/File),
+
which we may typically get to via a [file input](https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications)
+
for example.
-
It works by using the [`extract-files` package](https://www.npmjs.com/package/extract-files). When
-
the `multipartFetchExchange` sees at least one `File` in the variables it receives for a mutation,
-
then it will send a `multipart/form-data` POST request instead of a standard `application/json`
-
one. This is basically the same kind of request that we'd expect to send for regular HTML forms.
-
-
### Installation & Setup
+
In `urql`, these are supported natively, so as long as your JS environment supports either `File` or
+
`Blob`s, you can pass these directly to any `urql` API via your `variables`, and the default
+
`fetchExchange` will swich to using a multipart request instead.
-
First install `@urql/exchange-multipart-fetch` alongside `urql`:
-
-
```sh
-
yarn add @urql/exchange-multipart-fetch
-
# or
-
npm install --save @urql/exchange-multipart-fetch
-
```
-
-
The `multipartFetchExchange` is a drop-in replacement for the `fetchExchange`, which should be
-
replaced in the list of `exchanges`:
-
-
```js
-
import { createClient, dedupExchange, cacheExchange } from 'urql';
-
import { multipartFetchExchange } from '@urql/exchange-multipart-fetch';
-
-
const client = createClient({
-
url: 'http://localhost:3000/graphql',
-
exchanges: [dedupExchange, cacheExchange, multipartFetchExchange],
-
});
-
```
-
-
If you're using the `persistedFetchExchange` then put the `persistedFetchExchange` in front of the
-
`multipartFetchExchange`, since only the latter is a full replacement for the `fetchExchange`, and
-
the former only handled query operations.
-
-
[Read more about `@urql/multipart-fetch-exchange` in our API
-
docs.](../api/multipart-fetch-exchange.md)
+
Previously, this worked by installing the [`@urql/multipart-fetch-exchange` package](../api/multipart-fetch-exchange.md),
+
however, this package has been deprecated and file uploads are now built into `@urql/core`.
-1
docs/api/README.md
···
- [`@urql/exchange-retry` API docs](./retry-exchange.md)
- [`@urql/exchange-execute` API docs](./execute-exchange.md)
- [`@urql/exchange-multipart-fetch` API docs](./multipart-fetch-exchange.md)
-
- [`@urql/exchange-persisted-fetch` API docs](./persisted-fetch-exchange.md)
- [`@urql/exchange-request-policy` API docs](./request-policy-exchange.md)
- [`@urql/exchange-auth` API docs](./auth-exchange.md)
- [`@urql/exchange-refocus` API docs](./refocus-exchange.md)
+1 -2
docs/api/multipart-fetch-exchange.md
···
Spec](https://github.com/jaydenseric/graphql-multipart-request-spec) which is supported by the
[Apollo Sever package](https://www.apollographql.com/docs/apollo-server/data/file-uploads/).
-
This exchange uses the same fetch logic as the [`fetchExchange`](./core.md#fetchexchange) and the
-
[`persistedFetchExchange`](./persisted-fetch-exchange.md) by reusing logic from `@urql/core/internal`.
+
This exchange uses the same fetch logic as the [`fetchExchange`](./core.md#fetchexchange) and by reusing logic from `@urql/core/internal`.
The `multipartFetchExchange` is a drop-in replacement for the default
[`fetchExchange`](./core.md#fetchexchange) and will act exactly like the `fetchExchange` unless the
`variables` that it receives for mutations contain any `File`s as detected by the `extract-files` package.
-68
docs/api/persisted-fetch-exchange.md
···
-
---
-
title: '@urql/exchange-persisted-fetch'
-
order: 8
-
---
-
-
# Persisted Fetch Exchange
-
-
The `@urql/exchange-persisted-fetch` package contains an addon `persistedFetchExchange` for `urql`
-
that enables the use of _Automatic Persisted Queries_ with `urql`.
-
-
It follows the unofficial [GraphQL Persisted Queries
-
Spec](https://github.com/apollographql/apollo-link-persisted-queries#apollo-engine) which is
-
supported by the
-
[Apollo Sever package](https://www.apollographql.com/docs/apollo-server/performance/apq/).
-
-
This exchange uses the same fetch logic as the [`fetchExchange`](./core.md#fetchexchange) and the
-
[`multipartFetchExchange`](./multipart-fetch-exchange.md) by reusing logic from `@urql/core/internal`.
-
The `persistedFetchExchange` will attempt to send queries with an additional SHA256 hash to the
-
GraphQL API and will otherwise, when Automatic Persisted Queries are unsupported or when a mutation
-
or subscription is sent, forward the operation to the next exchange. Hence it should always be added
-
in front of another [`fetchExchange`](./core.md#fetchexchange).
-
-
The `persistedFetchExchange` will use the built-in [Web Crypto
-
API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) for SHA256 hashing in the
-
browser, which has been implemented to support IE11 as well. In Node.js it'll use the [Node
-
Crypto Module](https://nodejs.org/api/crypto.html) instead. It's also possible to use the
-
`generateHash` option to alter how the SHA256 hash is generated or retrieved.
-
-
## Installation and Setup
-
-
First install `@urql/exchange-persisted-fetch` alongside `urql`:
-
-
```sh
-
yarn add @urql/exchange-persisted-fetch
-
# or
-
npm install --save @urql/exchange-persisted-fetch
-
```
-
-
You'll then need to add the `persistedFetchExchange`, that this package exposes, to your
-
`exchanges`, in front of the `fetchExchange`. If you're using the
-
[`multipartFetchExchange`](./multipart-fetch-exchange.md) then it must be added in front of that
-
instead:
-
-
```js
-
import { createClient, dedupExchange, fetchExchange, cacheExchange } from 'urql';
-
import { persistedFetchExchange } from '@urql/exchange-persisted-fetch';
-
-
const client = createClient({
-
url: 'http://localhost:1234/graphql',
-
exchanges: [
-
dedupExchange,
-
cacheExchange,
-
persistedFetchExchange({
-
/* optional config */
-
}),
-
fetchExchange,
-
],
-
});
-
```
-
-
## Options
-
-
| Option | Description |
-
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-
| `preferGetForPersistedQueries` | This is similar to [the `Client`'s `preferGetMethod` option](./core.md#client) and will cause all persisted queries to be sent using a GET request. |
-
| `enforcePersistedQueries` | This option enforced persisted queries. Instead of allowing automatic persisted queries or triggering any retry logic when the API responds, it instead assumes that persisted queries will succeed and run like normal GraphQL API requests. |
-
| `generateHash` | This option accepts a function that receives the `query` as a string and the raw `DocumentNode` as a second argument and must return a `Promise<string>` resolving to a SHA256 hash. This can be used to swap out the SHA256 API, e.g. for React Native, or to use pre-generated SHA256 strings from the `DocumentNode`. |
-
| `enableForMutation` | This option allows mutations to be persisted in addition to queries. It's false by default. When a persisted mutation is requested, `preferGetForPersistedQueries` will be ignored and a POST method will always be used. |
+22 -22
docs/comparison.md
···
### Core Features
-
| | urql | Apollo | Relay |
-
| ------------------------------------------ | ----------------------------------- | -------------------------------------------------------------------------- | ------------------------------ |
-
| Extensible on a network level | ✅ Exchanges | ✅ Links | ✅ Network Layers |
-
| Extensible on a cache / control flow level | ✅ Exchanges | 🛑 | 🛑 |
-
| Base Bundle Size | **5.9kB** (7.1kB with bindings) | 32.9kB | 27.7kB (34.1kB with bindings) |
-
| Devtools | ✅ | ✅ | ✅ |
-
| Subscriptions | ✅ | ✅ | ✅ |
-
| Client-side Rehydration | ✅ | ✅ | ✅ |
-
| Polled Queries | 🔶 | ✅ | ✅ |
-
| Lazy Queries | ✅ | ✅ | ✅ |
-
| Stale while Revalidate / Cache and Network | ✅ | ✅ | ✅ |
-
| Focus Refetching | ✅ `@urql/exchange-refocus` | 🛑 | 🛑 |
-
| Stale Time Configuration | ✅ `@urql/exchange-request-policy` | ✅ | 🛑 |
-
| Persisted Queries | ✅ `@urql/exchange-persisted-fetch` | ✅ `apollo-link-persisted-queries` | ✅ |
-
| Batched Queries | 🛑 | ✅ `apollo-link-batch-http` | 🟡 `react-relay-network-layer` |
-
| Live Queries | 🛑 | 🛑 | ✅ |
-
| Defer & Stream Directives | ✅ | ✅ / 🛑 (`@defer` is supported in >=3.7.0, `@stream` is not yet supported) | 🟡 (unreleased) |
-
| Switching to `GET` method | ✅ | ✅ | 🟡 `react-relay-network-layer` |
-
| File Uploads | ✅ | 🟡 `apollo-upload-client` | 🛑 |
-
| Retrying Failed Queries | ✅ `@urql/exchange-retry` | ✅ `apollo-link-retry` | ✅ `DefaultNetworkLayer` |
-
| Easy Authentication Flows | ✅ `@urql/exchange-auth` | 🛑 (no docs for refresh-based authentication) | 🟡 `react-relay-network-layer` |
-
| Automatic Refetch after Mutation | ✅ (with document cache) | 🛑 | ✅ |
+
| | urql | Apollo | Relay |
+
| ------------------------------------------ | ---------------------------------- | -------------------------------------------------------------------------- | ------------------------------ |
+
| Extensible on a network level | ✅ Exchanges | ✅ Links | ✅ Network Layers |
+
| Extensible on a cache / control flow level | ✅ Exchanges | 🛑 | 🛑 |
+
| Base Bundle Size | **5.9kB** (7.1kB with bindings) | 32.9kB | 27.7kB (34.1kB with bindings) |
+
| Devtools | ✅ | ✅ | ✅ |
+
| Subscriptions | ✅ | ✅ | ✅ |
+
| Client-side Rehydration | ✅ | ✅ | ✅ |
+
| Polled Queries | 🔶 | ✅ | ✅ |
+
| Lazy Queries | ✅ | ✅ | ✅ |
+
| Stale while Revalidate / Cache and Network | ✅ | ✅ | ✅ |
+
| Focus Refetching | ✅ `@urql/exchange-refocus` | 🛑 | 🛑 |
+
| Stale Time Configuration | ✅ `@urql/exchange-request-policy` | ✅ | 🛑 |
+
| Persisted Queries | ✅ `@urql/exchange-persisted` | ✅ `apollo-link-persisted-queries` | ✅ |
+
| Batched Queries | 🛑 | ✅ `apollo-link-batch-http` | 🟡 `react-relay-network-layer` |
+
| Live Queries | 🛑 | 🛑 | ✅ |
+
| Defer & Stream Directives | ✅ | ✅ / 🛑 (`@defer` is supported in >=3.7.0, `@stream` is not yet supported) | 🟡 (unreleased) |
+
| Switching to `GET` method | ✅ | ✅ | 🟡 `react-relay-network-layer` |
+
| File Uploads | ✅ | 🟡 `apollo-upload-client` | 🛑 |
+
| Retrying Failed Queries | ✅ `@urql/exchange-retry` | ✅ `apollo-link-retry` | ✅ `DefaultNetworkLayer` |
+
| Easy Authentication Flows | ✅ `@urql/exchange-auth` | 🛑 (no docs for refresh-based authentication) | 🟡 `react-relay-network-layer` |
+
| Automatic Refetch after Mutation | ✅ (with document cache) | 🛑 | ✅ |
Typically these are all additional addon features that you may expect from a GraphQL client, no
matter which framework you use it with. It's worth mentioning that all three clients support some
exchanges/persisted-fetch/CHANGELOG.md exchanges/persisted/CHANGELOG.md
-67
exchanges/persisted-fetch/README.md
···
-
# @urql/exchange-persisted-fetch
-
-
The `persistedFetchExchange` is an exchange that builds on the regular `fetchExchange`
-
but adds support for Persisted Queries.
-
-
## Quick Start Guide
-
-
First install `@urql/exchange-persisted-fetch` alongside `urql`:
-
-
```sh
-
yarn add @urql/exchange-persisted-fetch
-
# or
-
npm install --save @urql/exchange-persisted-fetch
-
```
-
-
You'll then need to add the `persistedFetchExchange` method, that this package exposes,
-
to your `exchanges`.
-
-
```js
-
import { createClient, dedupExchange, fetchExchange, cacheExchange } from 'urql';
-
import { persistedFetchExchange } from '@urql/exchange-persisted-fetch';
-
-
const client = createClient({
-
url: 'http://localhost:1234/graphql',
-
exchanges: [
-
dedupExchange,
-
cacheExchange,
-
persistedFetchExchange({
-
/* optional config */
-
}),
-
fetchExchange,
-
],
-
});
-
```
-
-
The `persistedQueryExchange` supports three configuration options:
-
-
- `preferGetForPersistedQueries`: Use `GET` for fetches with persisted queries
-
- `enforcePersistedQueries`: This disables _automatic persisted queries_ and disables any retry
-
logic for how the API responds to persisted queries. Instead it's assumed that they'll always
-
succeed.
-
- `generateHash`: A function that takes a GraphQL query and returns the hashed result. This defaults to the `window.crypto` API in the browser and the `crypto` module in node.
-
-
The `persistedFetchExchange` only handles queries, so for mutations we keep the
-
`fetchExchange` around alongside of it.
-
-
## Avoid hashing during runtime
-
-
If you want to generate hashes at build-time you can use a [webpack-loader](https://github.com/leoasis/graphql-persisted-document-loader) to achieve this,
-
when using this all you need to do in this exchange is the following:
-
-
```js
-
import { createClient, dedupExchange, fetchExchange, cacheExchange } from 'urql';
-
import { persistedFetchExchange } from '@urql/exchange-persisted-fetch';
-
-
const client = createClient({
-
url: 'http://localhost:1234/graphql',
-
exchanges: [
-
dedupExchange,
-
cacheExchange,
-
persistedFetchExchange({
-
generateHash: (_, document) => document.documentId,
-
}),
-
fetchExchange,
-
],
-
});
-
```
+8 -8
exchanges/persisted-fetch/package.json exchanges/persisted/package.json
···
{
-
"name": "@urql/exchange-persisted-fetch",
+
"name": "@urql/exchange-persisted",
"version": "2.1.0",
"description": "An exchange that allows for persisted queries support when fetching queries",
"sideEffects": false,
···
"repository": {
"type": "git",
"url": "https://github.com/urql-graphql/urql.git",
-
"directory": "exchanges/persisted-fetch"
+
"directory": "exchanges/persisted"
},
"keywords": [
"urql",
···
"persisted queries",
"exchanges"
],
-
"main": "dist/urql-exchange-persisted-fetch",
-
"module": "dist/urql-exchange-persisted-fetch.mjs",
-
"types": "dist/urql-exchange-persisted-fetch.d.ts",
+
"main": "dist/urql-exchange-persisted",
+
"module": "dist/urql-exchange-persisted.mjs",
+
"types": "dist/urql-exchange-persisted.d.ts",
"source": "src/index.ts",
"exports": {
".": {
-
"import": "./dist/urql-exchange-persisted-fetch.mjs",
-
"require": "./dist/urql-exchange-persisted-fetch.js",
-
"types": "./dist/urql-exchange-persisted-fetch.d.ts",
+
"import": "./dist/urql-exchange-persisted.mjs",
+
"require": "./dist/urql-exchange-persisted.js",
+
"types": "./dist/urql-exchange-persisted.d.ts",
"source": "./src/index.ts"
},
"./package.json": "./package.json"
-9
exchanges/persisted-fetch/src/__snapshots__/persistedFetchExchange.test.ts.snap
···
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-
exports[`accepts successful persisted query responses 1`] = `"{\\"extensions\\":{\\"persistedQuery\\":{\\"sha256Hash\\":\\"b4228e10e04c59def248546d305b710309c1b297423b38eb64f989a89a398cd8\\",\\"version\\":1}},\\"operationName\\":\\"getUser\\",\\"variables\\":{\\"name\\":\\"Clara\\"}}"`;
-
-
exports[`supports cache-miss persisted query errors 1`] = `"{\\"extensions\\":{\\"persistedQuery\\":{\\"sha256Hash\\":\\"b4228e10e04c59def248546d305b710309c1b297423b38eb64f989a89a398cd8\\",\\"version\\":1}},\\"operationName\\":\\"getUser\\",\\"variables\\":{\\"name\\":\\"Clara\\"}}"`;
-
-
exports[`supports cache-miss persisted query errors 2`] = `"{\\"extensions\\":{\\"persistedQuery\\":{\\"sha256Hash\\":\\"b4228e10e04c59def248546d305b710309c1b297423b38eb64f989a89a398cd8\\",\\"version\\":1}},\\"operationName\\":\\"getUser\\",\\"query\\":\\"query getUser($name: String) {\\\\n user(name: $name) {\\\\n id\\\\n firstName\\\\n lastName\\\\n }\\\\n}\\",\\"variables\\":{\\"name\\":\\"Clara\\"}}"`;
-
-
exports[`supports unsupported persisted query errors 1`] = `"{\\"extensions\\":{\\"persistedQuery\\":{\\"sha256Hash\\":\\"b4228e10e04c59def248546d305b710309c1b297423b38eb64f989a89a398cd8\\",\\"version\\":1}},\\"operationName\\":\\"getUser\\",\\"variables\\":{\\"name\\":\\"Clara\\"}}"`;
-1
exchanges/persisted-fetch/src/index.ts
···
-
export * from './persistedFetchExchange';
-282
exchanges/persisted-fetch/src/persistedFetchExchange.test.ts
···
-
/**
-
* @vitest-environment node
-
*/
-
-
import { empty, fromValue, fromArray, pipe, Source, toPromise } from 'wonka';
-
import { vi, expect, it, afterEach, Mock } from 'vitest';
-
-
import { DocumentNode, print } from 'graphql';
-
import { Client, OperationResult } from '@urql/core';
-
-
import { queryOperation } from './test-utils';
-
import { hash } from './sha256';
-
import { persistedFetchExchange } from './persistedFetchExchange';
-
-
const fetch = (global as any).fetch as Mock;
-
-
const exchangeArgs = {
-
dispatchDebug: vi.fn(),
-
forward: () => empty as Source<OperationResult>,
-
client: ({
-
debugTarget: {
-
dispatchEvent: vi.fn(),
-
},
-
} as any) as Client,
-
};
-
-
afterEach(() => {
-
fetch.mockClear();
-
});
-
-
it('accepts successful persisted query responses', async () => {
-
const expected = JSON.stringify({
-
data: {
-
test: true,
-
},
-
});
-
-
fetch.mockResolvedValueOnce({
-
status: 200,
-
headers: { get: () => 'application/json' },
-
text: () => Promise.resolve(expected),
-
});
-
-
const actual = await pipe(
-
fromValue(queryOperation),
-
persistedFetchExchange()(exchangeArgs),
-
toPromise
-
);
-
-
expect(fetch).toHaveBeenCalledTimes(1);
-
expect(fetch.mock.calls[0][1].body).toMatchSnapshot();
-
expect(actual.data).not.toBeUndefined();
-
});
-
-
it('supports cache-miss persisted query errors', async () => {
-
const expectedMiss = JSON.stringify({
-
errors: [{ message: 'PersistedQueryNotFound' }],
-
});
-
-
const expectedRetry = JSON.stringify({
-
data: {
-
test: true,
-
},
-
});
-
-
fetch
-
.mockResolvedValueOnce({
-
status: 200,
-
headers: { get: () => 'application/json' },
-
text: () => Promise.resolve(expectedMiss),
-
})
-
.mockResolvedValueOnce({
-
status: 200,
-
headers: { get: () => 'application/json' },
-
text: () => Promise.resolve(expectedRetry),
-
});
-
-
const actual = await pipe(
-
fromValue(queryOperation),
-
persistedFetchExchange()(exchangeArgs),
-
toPromise
-
);
-
-
expect(fetch).toHaveBeenCalledTimes(2);
-
expect(fetch.mock.calls[0][1].body).toMatchSnapshot();
-
expect(fetch.mock.calls[1][1].body).toMatchSnapshot();
-
expect(actual.data).not.toBeUndefined();
-
});
-
-
it('supports GET exclusively for persisted queries', async () => {
-
const expectedMiss = JSON.stringify({
-
errors: [{ message: 'PersistedQueryNotFound' }],
-
});
-
-
const expectedRetry = JSON.stringify({
-
data: {
-
test: true,
-
},
-
});
-
-
fetch
-
.mockResolvedValueOnce({
-
status: 200,
-
headers: { get: () => 'application/json' },
-
text: () => Promise.resolve(expectedMiss),
-
})
-
.mockResolvedValueOnce({
-
status: 200,
-
headers: { get: () => 'application/json' },
-
text: () => Promise.resolve(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 = JSON.stringify({
-
errors: [{ message: 'PersistedQueryNotSupported' }],
-
});
-
-
const expectedRetry = JSON.stringify({
-
data: {
-
test: true,
-
},
-
});
-
-
fetch
-
.mockResolvedValueOnce({
-
status: 200,
-
headers: { get: () => 'application/json' },
-
text: () => Promise.resolve(expectedMiss),
-
})
-
.mockResolvedValueOnce({
-
status: 200,
-
headers: { get: () => 'application/json' },
-
text: () => Promise.resolve(expectedRetry),
-
})
-
.mockResolvedValueOnce({
-
status: 200,
-
headers: { get: () => 'application/json' },
-
text: () => Promise.resolve(expectedRetry),
-
});
-
-
const actual = await pipe(
-
fromArray([queryOperation, queryOperation]),
-
persistedFetchExchange()(exchangeArgs),
-
toPromise
-
);
-
-
expect(fetch).toHaveBeenCalledTimes(3);
-
expect(fetch.mock.calls[0][1].body).toMatchSnapshot();
-
expect(fetch.mock.calls[1][1].body).toEqual(fetch.mock.calls[1][1].body);
-
expect(actual.data).not.toBeUndefined();
-
});
-
-
it('correctly generates an SHA256 hash', async () => {
-
const expected = JSON.stringify({
-
data: {
-
test: true,
-
},
-
});
-
-
fetch.mockResolvedValue({
-
text: () => Promise.resolve(expected),
-
});
-
-
const queryHash = await hash(print(queryOperation.query));
-
-
await pipe(
-
fromValue(queryOperation),
-
persistedFetchExchange()(exchangeArgs),
-
toPromise
-
);
-
-
expect(fetch).toHaveBeenCalledTimes(1);
-
-
const body = JSON.parse(fetch.mock.calls[0][1].body);
-
-
expect(queryHash).toBe(
-
'b4228e10e04c59def248546d305b710309c1b297423b38eb64f989a89a398cd8'
-
);
-
-
expect(body).toMatchObject({
-
extensions: {
-
persistedQuery: {
-
version: 1,
-
sha256Hash: queryHash,
-
},
-
},
-
});
-
});
-
-
it('supports a custom hash function', async () => {
-
const expected = JSON.stringify({
-
data: {
-
test: true,
-
},
-
});
-
-
fetch.mockResolvedValueOnce({
-
text: () => Promise.resolve(expected),
-
});
-
-
const hashFn = vi.fn((_input: string, _doc: DocumentNode) => {
-
return Promise.resolve('hello');
-
});
-
-
await pipe(
-
fromValue(queryOperation),
-
persistedFetchExchange({ generateHash: hashFn })(exchangeArgs),
-
toPromise
-
);
-
-
expect(fetch).toHaveBeenCalledTimes(1);
-
-
const body = JSON.parse(fetch.mock.calls[0][1].body);
-
-
expect(body).toMatchObject({
-
extensions: {
-
persistedQuery: {
-
version: 1,
-
sha256Hash: 'hello',
-
},
-
},
-
});
-
const queryString = `query getUser($name: String) {
-
user(name: $name) {
-
id
-
firstName
-
lastName
-
}
-
}`;
-
expect(hashFn).toBeCalledWith(queryString, queryOperation.query);
-
});
-
-
it('falls back to a non-persisted query if the hash is falsy', async () => {
-
const expected = JSON.stringify({
-
data: {
-
test: true,
-
},
-
});
-
-
fetch.mockResolvedValueOnce({
-
text: () => Promise.resolve(expected),
-
});
-
-
const hashFn = vi.fn(() => Promise.resolve(''));
-
-
await pipe(
-
fromValue(queryOperation),
-
persistedFetchExchange({ generateHash: hashFn })(exchangeArgs),
-
toPromise
-
);
-
-
expect(fetch).toHaveBeenCalledTimes(1);
-
-
const body = JSON.parse(fetch.mock.calls[0][1].body);
-
-
expect(body).toMatchObject({
-
query:
-
'query getUser($name: String) {\n' +
-
' user(name: $name) {\n' +
-
' id\n' +
-
' firstName\n' +
-
' lastName\n' +
-
' }\n' +
-
'}',
-
operationName: 'getUser',
-
variables: { name: 'Clara' },
-
});
-
});
-215
exchanges/persisted-fetch/src/persistedFetchExchange.ts
···
-
/* eslint-disable @typescript-eslint/no-use-before-define */
-
import {
-
Source,
-
fromValue,
-
fromPromise,
-
filter,
-
merge,
-
mergeMap,
-
pipe,
-
share,
-
onPush,
-
takeUntil,
-
} from 'wonka';
-
-
import {
-
makeOperation,
-
CombinedError,
-
ExchangeInput,
-
Exchange,
-
Operation,
-
OperationResult,
-
} from '@urql/core';
-
-
import {
-
FetchBody,
-
makeFetchBody,
-
makeFetchURL,
-
makeFetchOptions,
-
makeFetchSource,
-
} from '@urql/core/internal';
-
import { DocumentNode } from 'graphql';
-
-
import { hash } from './sha256';
-
-
interface PersistedFetchExchangeOptions {
-
preferGetForPersistedQueries?: boolean;
-
enforcePersistedQueries?: boolean;
-
generateHash?: (query: string, document: DocumentNode) => Promise<string>;
-
enableForMutation?: boolean;
-
}
-
-
export const persistedFetchExchange = (
-
options?: PersistedFetchExchangeOptions
-
): Exchange => ({ forward, dispatchDebug }) => {
-
if (!options) options = {};
-
-
const preferGetForPersistedQueries = !!options.preferGetForPersistedQueries;
-
const enforcePersistedQueries = !!options.enforcePersistedQueries;
-
const hashFn = options.generateHash || hash;
-
const enableForMutation = !!options.enableForMutation;
-
let supportsPersistedQueries = true;
-
-
const operationFilter = (operation: Operation) =>
-
(enableForMutation && operation.kind === 'mutation') ||
-
operation.kind === 'query';
-
-
return ops$ => {
-
const sharedOps$ = share(ops$);
-
const fetchResults$ = pipe(
-
sharedOps$,
-
filter(operationFilter),
-
mergeMap(operation => {
-
const { key } = operation;
-
const teardown$ = pipe(
-
sharedOps$,
-
filter(op => op.kind === 'teardown' && op.key === key)
-
);
-
-
const body = makeFetchBody(operation);
-
if (!supportsPersistedQueries) {
-
// Runs the usual non-persisted fetchExchange query logic
-
return pipe(
-
makePersistedFetchSource(operation, body, dispatchDebug, false),
-
takeUntil(teardown$)
-
);
-
}
-
-
const query: string = body.query!;
-
-
return pipe(
-
// Hash the given GraphQL query
-
fromPromise(hashFn(query, operation.query)),
-
mergeMap(sha256Hash => {
-
// if the hashing operation was successful, add the persisted query extension
-
if (sha256Hash) {
-
// Attach SHA256 hash and remove query from body
-
body.query = undefined;
-
body.extensions = {
-
...body.extensions,
-
persistedQuery: {
-
version: 1,
-
sha256Hash,
-
},
-
};
-
}
-
const useGet =
-
operation.kind === 'query' &&
-
preferGetForPersistedQueries &&
-
!!sha256Hash;
-
return makePersistedFetchSource(
-
operation,
-
body,
-
dispatchDebug,
-
useGet
-
);
-
}),
-
mergeMap(result => {
-
if (!enforcePersistedQueries) {
-
if (result.error && isPersistedUnsupported(result.error)) {
-
// Reset the body back to its non-persisted state
-
body.query = query;
-
if (body.extensions && body.extensions.persistedQuery)
-
body.extensions.persistedQuery = undefined;
-
// Disable future persisted queries if they're not enforced
-
supportsPersistedQueries = false;
-
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,
-
false
-
);
-
}
-
}
-
-
return fromValue(result);
-
}),
-
takeUntil(teardown$)
-
);
-
})
-
);
-
-
const forward$ = pipe(
-
sharedOps$,
-
filter(operation => !operationFilter(operation)),
-
forward
-
);
-
-
return merge([fetchResults$, forward$]);
-
};
-
};
-
-
const makePersistedFetchSource = (
-
operation: Operation,
-
body: FetchBody,
-
dispatchDebug: ExchangeInput['dispatchDebug'],
-
useGet: boolean
-
): Source<OperationResult> => {
-
const newOperation = makeOperation(operation.kind, operation, {
-
...operation.context,
-
preferGetMethod: useGet ? 'force' : operation.context.preferGetMethod,
-
});
-
-
const url = makeFetchURL(newOperation, 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: newOperation,
-
data: {
-
url,
-
fetchOptions,
-
},
-
});
-
-
let fetch$ = makeFetchSource(newOperation, url, fetchOptions);
-
-
if (process.env.NODE_ENV !== 'production') {
-
fetch$ = pipe(
-
fetch$,
-
onPush(result => {
-
const persistFail =
-
result.error &&
-
(isPersistedMiss(result.error) ||
-
isPersistedUnsupported(result.error));
-
const error = !result.data ? result.error : undefined;
-
-
dispatchDebug({
-
// TODO: Assign a new name to this once @urql/devtools supports it
-
type: persistFail || error ? 'fetchError' : 'fetchSuccess',
-
message: persistFail
-
? 'A Persisted Query request has failed. A non-persisted GraphQL request will follow.'
-
: `A ${
-
error ? 'failed' : 'successful'
-
} fetch response has been returned.`,
-
operation,
-
data: {
-
url,
-
fetchOptions,
-
value: persistFail ? result.error! : error || result,
-
},
-
});
-
})
-
);
-
}
-
-
return fetch$;
-
};
-
-
const isPersistedMiss = (error: CombinedError): boolean =>
-
error.graphQLErrors.some(x => x.message === 'PersistedQueryNotFound');
-
-
const isPersistedUnsupported = (error: CombinedError): boolean =>
-
error.graphQLErrors.some(x => x.message === 'PersistedQueryNotSupported');
+1 -1
exchanges/persisted-fetch/src/sha256.ts exchanges/persisted/src/sha256.ts
···
};
export const hash = async (query: string): Promise<string> => {
-
if (webCrypto) {
+
if (webCrypto && webCrypto.subtle) {
const digest = await webCrypto.subtle.digest(
{ name: 'SHA-256' },
new TextEncoder().encode(query)
exchanges/persisted-fetch/src/test-utils.ts exchanges/persisted/src/test-utils.ts
exchanges/persisted-fetch/tsconfig.json exchanges/persisted/tsconfig.json
+63
exchanges/persisted/README.md
···
+
# @urql/exchange-persisted
+
+
The `persistedExchange` is an exchange that allows other terminating exchanges to support Persisted Queries, and is as such placed in front of either the default `fetchExchange` or
+
other terminating exchanges.
+
+
## Quick Start Guide
+
+
First install `@urql/exchange-persisted` alongside `urql`:
+
+
```sh
+
yarn add @urql/exchange-persisted
+
# or
+
npm install --save @urql/exchange-persisted
+
```
+
+
You'll then need to add the `persistedExchange` function, that this package exposes,
+
to your `exchanges`.
+
+
```js
+
import { createClient, dedupExchange, fetchExchange, cacheExchange } from 'urql';
+
import { persistedExchange } from '@urql/exchange-persisted';
+
+
const client = createClient({
+
url: 'http://localhost:1234/graphql',
+
exchanges: [
+
dedupExchange,
+
cacheExchange,
+
persistedExchange({
+
/* optional config */
+
}),
+
fetchExchange,
+
],
+
});
+
```
+
+
The `persistedExchange` supports three configuration options:
+
+
- `preferGetForPersistedQueries`: Enforce `GET` method to be used by the default `fetchExchange` for persisted queries
+
- `enforcePersistedQueries`: This disables _automatic persisted queries_ and disables any retry logic for how the API responds to persisted queries. Instead it's assumed that they'll always succeed.
+
- `generateHash`: A function that takes a GraphQL query and returns the hashed result. This defaults to the `window.crypto` API in the browser and the `crypto` module in Node.
+
- `enableForMutation`: By default, the exchange only handles `query` operations, but enabling this allows it to handle mutations as well.
+
+
## Avoid hashing during runtime
+
+
If you want to generate hashes at build-time you can use a [webpack-loader](https://github.com/leoasis/graphql-persisted-document-loader) to achieve this,
+
when using this all you need to do in this exchange is the following:
+
+
```js
+
import { createClient, dedupExchange, fetchExchange, cacheExchange } from 'urql';
+
import { persistedExchange } from '@urql/exchange-persisted';
+
+
const client = createClient({
+
url: 'http://localhost:1234/graphql',
+
exchanges: [
+
dedupExchange,
+
cacheExchange,
+
persistedExchange({
+
generateHash: (_, document) => document.documentId,
+
}),
+
fetchExchange,
+
],
+
});
+
```
+1
exchanges/persisted/src/index.ts
···
+
export * from './persistedExchange';
+120
exchanges/persisted/src/persistedExchange.test.ts
···
+
import {
+
Source,
+
pipe,
+
fromValue,
+
fromArray,
+
toPromise,
+
delay,
+
take,
+
tap,
+
map,
+
} from 'wonka';
+
+
import { Client, Operation, OperationResult, CombinedError } from '@urql/core';
+
+
import { vi, expect, it } from 'vitest';
+
import {
+
queryResponse,
+
queryOperation,
+
} from '../../../packages/core/src/test-utils';
+
import { persistedExchange } from './persistedExchange';
+
+
const makeExchangeArgs = () => {
+
const operations: Operation[] = [];
+
+
const result = vi.fn(
+
(operation: Operation): OperationResult => ({ ...queryResponse, operation })
+
);
+
+
return {
+
operations,
+
result,
+
exchangeArgs: {
+
forward: (op$: Source<Operation>) =>
+
pipe(
+
op$,
+
tap(op => operations.push(op)),
+
map(result)
+
),
+
client: new Client({ url: '/api', exchanges: [] }),
+
} as any,
+
};
+
};
+
+
it('adds the APQ extensions correctly', async () => {
+
const { exchangeArgs } = makeExchangeArgs();
+
+
const res = await pipe(
+
fromValue(queryOperation),
+
persistedExchange()(exchangeArgs),
+
take(1),
+
toPromise
+
);
+
+
expect(res.operation.context.persistAttempt).toBe(true);
+
expect(res.operation.extensions).toEqual({
+
persistedQuery: {
+
version: 1,
+
sha256Hash: expect.any(String),
+
miss: undefined,
+
},
+
});
+
});
+
+
it('retries query when persisted query resulted in miss', async () => {
+
const { result, operations, exchangeArgs } = makeExchangeArgs();
+
+
result.mockImplementationOnce(operation => ({
+
...queryResponse,
+
operation,
+
error: new CombinedError({
+
graphQLErrors: [{ message: 'PersistedQueryNotFound' }],
+
}),
+
}));
+
+
const res = await pipe(
+
fromValue(queryOperation),
+
persistedExchange()(exchangeArgs),
+
take(1),
+
toPromise
+
);
+
+
expect(res.operation.context.persistAttempt).toBe(true);
+
expect(operations.length).toBe(2);
+
+
expect(operations[1].extensions).toEqual({
+
persistedQuery: {
+
version: 1,
+
sha256Hash: expect.any(String),
+
miss: true,
+
},
+
});
+
});
+
+
it('retries query persisted query resulted in unsupported', async () => {
+
const { result, operations, exchangeArgs } = makeExchangeArgs();
+
+
result.mockImplementationOnce(operation => ({
+
...queryResponse,
+
operation,
+
error: new CombinedError({
+
graphQLErrors: [{ message: 'PersistedQueryNotSupported' }],
+
}),
+
}));
+
+
await pipe(
+
fromArray([queryOperation, queryOperation]),
+
delay(0),
+
persistedExchange()(exchangeArgs),
+
take(2),
+
toPromise
+
);
+
+
expect(operations.length).toBe(3);
+
+
expect(operations[1].extensions).toEqual({
+
persistedQuery: undefined,
+
});
+
+
expect(operations[2].extensions).toEqual(undefined);
+
});
+143
exchanges/persisted/src/persistedExchange.ts
···
+
import {
+
map,
+
makeSubject,
+
fromPromise,
+
filter,
+
merge,
+
mergeMap,
+
pipe,
+
share,
+
} from 'wonka';
+
+
import {
+
makeOperation,
+
stringifyDocument,
+
PersistedRequestExtensions,
+
OperationResult,
+
CombinedError,
+
Exchange,
+
Operation,
+
} from '@urql/core';
+
+
import type { DocumentNode } from 'graphql';
+
+
import { hash } from './sha256';
+
+
const isPersistedMiss = (error: CombinedError): boolean =>
+
error.graphQLErrors.some(x => x.message === 'PersistedQueryNotFound');
+
+
const isPersistedUnsupported = (error: CombinedError): boolean =>
+
error.graphQLErrors.some(x => x.message === 'PersistedQueryNotSupported');
+
+
export interface PersistedExchangeOptions {
+
preferGetForPersistedQueries?: boolean;
+
enforcePersistedQueries?: boolean;
+
generateHash?: (query: string, document: DocumentNode) => Promise<string>;
+
enableForMutation?: boolean;
+
}
+
+
export const persistedExchange = (
+
options?: PersistedExchangeOptions
+
): Exchange => ({ forward }) => {
+
if (!options) options = {};
+
+
const preferGetForPersistedQueries = !!options.preferGetForPersistedQueries;
+
const enforcePersistedQueries = !!options.enforcePersistedQueries;
+
const hashFn = options.generateHash || hash;
+
const enableForMutation = !!options.enableForMutation;
+
let supportsPersistedQueries = true;
+
+
const operationFilter = (operation: Operation) =>
+
supportsPersistedQueries &&
+
!operation.context.persistAttempt &&
+
((enableForMutation && operation.kind === 'mutation') ||
+
operation.kind === 'query');
+
+
return operations$ => {
+
const retries = makeSubject<Operation>();
+
const sharedOps$ = share(operations$);
+
+
const forwardedOps$ = pipe(
+
sharedOps$,
+
filter(operation => !operationFilter(operation))
+
);
+
+
const persistedOps$ = pipe(
+
sharedOps$,
+
filter(operationFilter),
+
map(async operation => {
+
const persistedOperation = makeOperation(operation.kind, operation, {
+
...operation.context,
+
persistAttempt: true,
+
});
+
+
const sha256Hash = await hashFn(
+
stringifyDocument(operation.query),
+
operation.query
+
);
+
if (sha256Hash) {
+
persistedOperation.extensions = {
+
...persistedOperation.extensions,
+
persistedQuery: {
+
version: 1,
+
sha256Hash,
+
},
+
};
+
if (
+
persistedOperation.kind === 'query' &&
+
preferGetForPersistedQueries
+
) {
+
persistedOperation.context.preferGetMethod = 'force';
+
}
+
}
+
+
return persistedOperation;
+
}),
+
mergeMap(fromPromise)
+
);
+
+
return pipe(
+
merge([persistedOps$, forwardedOps$, retries.source]),
+
forward,
+
map(result => {
+
if (
+
!enforcePersistedQueries &&
+
result.operation.extensions &&
+
result.operation.extensions.persistedQuery
+
) {
+
if (result.error && isPersistedUnsupported(result.error)) {
+
// Disable future persisted queries if they're not enforced
+
supportsPersistedQueries = false;
+
// Update operation with unsupported attempt
+
const followupOperation = makeOperation(
+
result.operation.kind,
+
result.operation
+
);
+
if (followupOperation.extensions)
+
delete followupOperation.extensions.persistedQuery;
+
retries.next(followupOperation);
+
return null;
+
} else if (result.error && isPersistedMiss(result.error)) {
+
// Update operation with unsupported attempt
+
const followupOperation = makeOperation(
+
result.operation.kind,
+
result.operation
+
);
+
// Mark as missed persisted query
+
followupOperation.extensions = {
+
...followupOperation.extensions,
+
persistedQuery: {
+
...(followupOperation.extensions || {}).persistedQuery,
+
miss: true,
+
} as PersistedRequestExtensions,
+
};
+
retries.next(followupOperation);
+
return null;
+
}
+
}
+
return result;
+
}),
+
filter((result): result is OperationResult => !!result)
+
);
+
};
+
};
+1
packages/core/src/index.ts
···
export {
CombinedError,
stringifyVariables,
+
stringifyDocument,
createRequest,
makeResult,
makeErrorResult,
+39
packages/core/src/internal/fetchOptions.test.ts
···
import { queryOperation, mutationOperation } from '../test-utils';
import { makeFetchBody, makeFetchURL, makeFetchOptions } from './fetchOptions';
+
describe('makeFetchBody', () => {
+
it('creates a fetch body', () => {
+
const body = makeFetchBody(queryOperation);
+
expect(body).toMatchInlineSnapshot(`
+
{
+
"extensions": undefined,
+
"operationName": "getUser",
+
"query": "query getUser($name: String) {
+
user(name: $name) {
+
id
+
firstName
+
lastName
+
}
+
}",
+
"variables": {
+
"name": "Clara",
+
},
+
}
+
`);
+
});
+
+
it('omits the query property when APQ is set', () => {
+
const apqOperation = makeOperation(queryOperation.kind, queryOperation);
+
+
apqOperation.extensions = {
+
...apqOperation.extensions,
+
persistedQuery: {
+
version: 1,
+
sha256Hash: '[test]',
+
},
+
};
+
+
expect(makeFetchBody(apqOperation).query).toBe(undefined);
+
+
apqOperation.extensions.persistedQuery!.miss = true;
+
expect(makeFetchBody(apqOperation).query).not.toBe(undefined);
+
});
+
});
+
describe('makeFetchURL', () => {
it('returns the URL by default', () => {
const body = makeFetchBody(queryOperation);
+5 -1
packages/core/src/internal/fetchOptions.ts
···
Data = any,
Variables extends AnyVariables = AnyVariables
>(request: Omit<GraphQLRequest<Data, Variables>, 'key'>): FetchBody {
+
const isAPQ =
+
request.extensions &&
+
request.extensions.persistedQuery &&
+
!request.extensions.persistedQuery.miss;
return {
-
query: stringifyDocument(request.query),
+
query: isAPQ ? undefined : stringifyDocument(request.query),
operationName: getOperationName(request.query),
variables: request.variables || undefined,
extensions: request.extensions,
+22 -1
packages/core/src/types.ts
···
*/
type Extensions = Record<string, any>;
+
/** Extensions sub-property on `persistedQuery` for Automatic Persisted Queries.
+
*
+
* @remarks
+
* This is part of the Automatic Persisted Query defacto standard and allows an API
+
* request to omit the `query`, instead sending this `sha256Hash`.
+
*/
+
export interface PersistedRequestExtensions {
+
version?: 1;
+
sha256Hash: string;
+
/** Set when a `sha256Hash` previously experienced a miss which will force `query` to be sent. */
+
miss?: boolean;
+
}
+
+
/** Extensions which may be palced on {@link GraphQLRequest | GraphQLRequests}.
+
* @see {@link https://github.com/graphql/graphql-over-http/blob/1928447/spec/GraphQLOverHTTP.md#request-parameters} for the GraphQL over HTTP spec
+
*/
+
export interface RequestExtensions {
+
persistedQuery?: PersistedRequestExtensions;
+
[extension: string]: any;
+
}
+
/** Incremental Payloads sent as part of "Incremental Delivery" patching prior result data.
*
* @remarks
···
/** Additional metadata that a GraphQL API may accept for spec extensions.
* @see {@link https://github.com/graphql/graphql-over-http/blob/1928447/spec/GraphQLOverHTTP.md#request-parameters} for the GraphQL over HTTP spec
*/
-
extensions?: Record<string, any> | undefined;
+
extensions?: RequestExtensions | undefined;
}
/** Parameters from which {@link GraphQLRequest | GraphQLRequests} are created from.
+8 -2
packages/core/src/utils/request.ts
···
import { HashValue, phash } from './hash';
import { stringifyVariables } from './variables';
-
import { TypedDocumentNode, AnyVariables, GraphQLRequest } from '../types';
+
+
import type {
+
TypedDocumentNode,
+
AnyVariables,
+
GraphQLRequest,
+
RequestExtensions,
+
} from '../types';
interface WritableLocation {
loc: Location | undefined;
···
>(
_query: string | DocumentNode | TypedDocumentNode<Data, Variables>,
_variables: Variables,
-
extensions?: Record<string, any> | undefined
+
extensions?: RequestExtensions | undefined
): GraphQLRequest<Data, Variables> => {
const variables = _variables || ({} as Variables);
const query = keyDocument(_query);
+1 -1
pnpm-lock.yaml
···
devDependencies:
graphql: 16.0.1
-
exchanges/persisted-fetch:
+
exchanges/persisted:
specifiers:
'@urql/core': '>=3.2.2'
graphql: ^16.0.0