Skip to content

Commit 614c732

Browse files
authored
feat: Add support for persisted documents via the documentId property (#3515)
1 parent e8e16bb commit 614c732

4 files changed

Lines changed: 51 additions & 7 deletions

File tree

.changeset/two-bananas-grow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@urql/core': minor
3+
---
4+
5+
Add support for sending persisted documents. Any `DocumentNode` with no/empty definitions and a `documentId` property is considered a persisted document. When this is detected a `documentId` parameter rather than a `query` string is sent to the GraphQL API, similar to Automatic Persisted Queries (APQs). However, APQs are only supported via `@urql/exchange-persisted`, while support for `documentId` is now built-in.

packages/core/src/internal/fetchOptions.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// @vitest-environment jsdom
22

33
import { expect, describe, it } from 'vitest';
4+
import { Kind } from '@0no-co/graphql.web';
45
import { makeOperation } from '../utils/operation';
56
import { queryOperation, mutationOperation } from '../test-utils';
67
import { makeFetchBody, makeFetchURL, makeFetchOptions } from './fetchOptions';
@@ -10,6 +11,7 @@ describe('makeFetchBody', () => {
1011
const body = makeFetchBody(queryOperation);
1112
expect(body).toMatchInlineSnapshot(`
1213
{
14+
"documentId": undefined,
1315
"extensions": undefined,
1416
"operationName": "getUser",
1517
"query": "query getUser($name: String) {
@@ -42,6 +44,22 @@ describe('makeFetchBody', () => {
4244
apqOperation.extensions.persistedQuery!.miss = true;
4345
expect(makeFetchBody(apqOperation).query).not.toBe(undefined);
4446
});
47+
48+
it('omits the query property when query is a persisted document', () => {
49+
// A persisted documents is one that carries a `documentId` property and
50+
// has no definitions
51+
const persistedOperation = makeOperation(queryOperation.kind, {
52+
...queryOperation,
53+
query: {
54+
kind: Kind.DOCUMENT,
55+
definitions: [],
56+
documentId: 'TestDocumentId',
57+
},
58+
});
59+
60+
expect(makeFetchBody(persistedOperation).query).toBe(undefined);
61+
expect(makeFetchBody(persistedOperation).documentId).toBe('TestDocumentId');
62+
});
4563
});
4664

4765
describe('makeFetchURL', () => {

packages/core/src/internal/fetchOptions.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { AnyVariables, GraphQLRequest, Operation } from '../types';
1010
/** Abstract definition of the JSON data sent during GraphQL HTTP POST requests. */
1111
export interface FetchBody {
1212
query?: string;
13+
documentId?: string;
1314
operationName: string | undefined;
1415
variables: undefined | Record<string, any>;
1516
extensions: undefined | Record<string, any>;
@@ -24,16 +25,31 @@ export function makeFetchBody<
2425
Data = any,
2526
Variables extends AnyVariables = AnyVariables,
2627
>(request: Omit<GraphQLRequest<Data, Variables>, 'key'>): FetchBody {
27-
const isAPQ =
28-
request.extensions &&
29-
request.extensions.persistedQuery &&
30-
!request.extensions.persistedQuery.miss;
31-
return {
32-
query: isAPQ ? undefined : stringifyDocument(request.query),
28+
const body: FetchBody = {
29+
query: undefined,
30+
documentId: undefined,
3331
operationName: getOperationName(request.query),
3432
variables: request.variables || undefined,
3533
extensions: request.extensions,
3634
};
35+
36+
if (
37+
'documentId' in request.query &&
38+
request.query.documentId &&
39+
// NOTE: We have to check that the document will definitely be sent
40+
// as a persisted document to avoid breaking changes
41+
(!request.query.definitions || !request.query.definitions.length)
42+
) {
43+
body.documentId = request.query.documentId;
44+
} else if (
45+
!request.extensions ||
46+
!request.extensions.persistedQuery ||
47+
!!request.extensions.persistedQuery.miss
48+
) {
49+
body.query = stringifyDocument(request.query);
50+
}
51+
52+
return body;
3753
}
3854

3955
/** Creates a URL that will be called for a GraphQL HTTP request.

packages/core/src/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import type { Subscription, Source } from 'wonka';
99
import type { Client } from './client';
1010
import type { CombinedError } from './utils/error';
1111

12+
/** A GraphQL persisted document will contain `documentId` that replaces its definitions */
13+
export interface PersistedDocument extends DocumentNode {
14+
documentId?: string;
15+
}
16+
1217
/** A GraphQL `DocumentNode` with attached generics for its result data and variables.
1318
*
1419
* @remarks
@@ -341,7 +346,7 @@ export interface GraphQLRequest<
341346
* In `urql`, we expect a document to only contain a single operation that is executed rather than
342347
* multiple ones by convention.
343348
*/
344-
query: DocumentNode | TypedDocumentNode<Data, Variables>;
349+
query: DocumentNode | PersistedDocument | TypedDocumentNode<Data, Variables>;
345350
/** Variables used to execute the `query` document.
346351
*
347352
* @remarks

0 commit comments

Comments
 (0)