1
1
import type { DocumentTypeDecoration } from "@graphql-typed-document-node/core" ;
2
+ import { print } from "graphql" ;
3
+ import { isNode } from "graphql/language/ast.js" ;
4
+ import {
5
+ asRequestBody ,
6
+ asRequestURL ,
7
+ createRequest ,
8
+ isPersistedQuery ,
9
+ } from "request" ;
2
10
import invariant from "tiny-invariant" ;
3
- import { getDocumentId , type GqlResponse } from "./helpers" ;
4
11
import {
5
- createSha256 ,
6
12
errorMessage ,
7
- extractOperationName ,
13
+ getDocumentId ,
8
14
getQueryType ,
9
15
hasPersistedQueryError ,
10
16
mergeHeaders ,
17
+ type GqlResponse ,
11
18
} from "./helpers" ;
12
- import { print } from "graphql" ;
13
- import { isNode } from "graphql/language/ast.js" ;
14
19
15
20
type Options = {
16
21
/**
17
- * Enable use of persisted queries, this will always add a extra roundtrip to the server if queries aren't cacheable
22
+ * Enable use of automated persisted queries, this will always add a extra
23
+ * roundtrip to the server if queries aren't cacheable
18
24
* @default false
19
25
*/
26
+ apq ?: boolean ;
27
+
28
+ /** Deprecated: use `apq: <boolean>` */
20
29
persistedQueries ?: boolean ;
21
30
22
31
/**
@@ -49,81 +58,52 @@ type RequestOptions = {
49
58
export type ClientFetcher = < TResponse , TVariables > (
50
59
astNode : DocumentTypeDecoration < TResponse , TVariables > ,
51
60
variables ?: TVariables ,
52
- options ?: RequestOptions | AbortSignal , // Backwards compatibility
61
+ options ?: RequestOptions ,
53
62
) => Promise < GqlResponse < TResponse > > ;
54
63
55
64
export const initClientFetcher =
56
65
(
57
66
endpoint : string ,
58
67
{
68
+ apq = false ,
59
69
persistedQueries = false ,
60
70
defaultTimeout = 30000 ,
61
71
defaultHeaders = { } ,
62
- createDocumentId = < TResult , TVariables > (
63
- query : DocumentTypeDecoration < TResult , TVariables > ,
64
- ) : string | undefined => getDocumentId ( query ) ,
72
+ createDocumentId = getDocumentId ,
65
73
} : Options = { } ,
66
74
) : ClientFetcher =>
67
75
/**
68
76
* Executes a GraphQL query post request on the client.
69
77
*
70
- * This is the only fetcher that uses user information in the call since all user information is only
71
- * used after rendering the page for caching reasons.
78
+ * This is the only fetcher that uses user information in the call since all
79
+ * user information is only used after rendering the page for caching reasons.
72
80
*/
73
81
async < TResponse , TVariables > (
74
82
astNode : DocumentTypeDecoration < TResponse , TVariables > ,
75
83
variables ?: TVariables ,
76
- optionsOrSignal : RequestOptions | AbortSignal = {
84
+ options : RequestOptions = {
77
85
signal : AbortSignal . timeout ( defaultTimeout ) ,
78
- } satisfies RequestOptions ,
86
+ } ,
79
87
) : Promise < GqlResponse < TResponse > > => {
80
- // For backwards compatibility, when options is an AbortSignal we transform
81
- // it into a RequestOptions object
82
- const options : RequestOptions = { } ;
83
- if ( optionsOrSignal instanceof AbortSignal ) {
84
- options . signal = optionsOrSignal ;
85
- } else {
86
- Object . assign ( options , optionsOrSignal ) ;
87
- }
88
-
89
88
// Make sure that we always have a default signal set
90
89
if ( ! options . signal ) {
91
90
options . signal = AbortSignal . timeout ( defaultTimeout ) ;
92
91
}
93
92
94
93
const query = isNode ( astNode ) ? print ( astNode ) : astNode . toString ( ) ;
95
-
96
- const operationName = extractOperationName ( query ) ;
97
94
const documentId = createDocumentId ( astNode ) ;
98
-
99
- let extensions = { } ;
100
- if ( persistedQueries ) {
101
- const hash = await createSha256 ( query ) ;
102
-
103
- extensions = {
104
- persistedQuery : {
105
- version : 1 ,
106
- sha256Hash : hash ,
107
- } ,
108
- } ;
109
- }
110
-
111
- const url = new URL ( endpoint ) ;
112
- url . searchParams . set ( "op" , operationName ?? "" ) ;
95
+ const request = await createRequest ( query , variables , documentId ) ;
113
96
114
97
let response : GqlResponse < TResponse > | undefined = undefined ;
115
-
116
98
const headers = mergeHeaders ( { ...defaultHeaders , ...options . headers } ) ;
117
99
100
+ const queryType = getQueryType ( query ) ;
101
+
102
+ apq = apq || persistedQueries ;
103
+
118
104
// For queries we can use GET requests if persisted queries are enabled
119
- if ( persistedQueries && getQueryType ( query ) === "query" ) {
120
- url . searchParams . set ( "extensions" , JSON . stringify ( extensions ) ) ;
121
- if ( variables ) {
122
- url . searchParams . set ( "variables" , JSON . stringify ( variables ) ) ;
123
- }
124
- if ( documentId ) {
125
- url . searchParams . set ( "documentId" , documentId ) ;
126
- }
105
+ if ( queryType === "query" && ( apq || isPersistedQuery ( request ) ) ) {
106
+ const url = asRequestURL ( endpoint , request ) ;
127
107
response = await parseResponse < GqlResponse < TResponse > > ( ( ) =>
128
108
fetch ( url . toString ( ) , {
129
109
headers : Object . fromEntries ( headers . entries ( ) ) ,
@@ -134,13 +114,17 @@ export const initClientFetcher =
134
114
) ;
135
115
}
136
116
137
- if ( ! response || hasPersistedQueryError ( response ) ) {
138
- // Persisted query not used or found, fall back to POST request and include extension to cache the query on the server
117
+ if (
118
+ ! response ||
119
+ ( isPersistedQuery ( request ) && hasPersistedQueryError ( response ) )
120
+ ) {
121
+ // Persisted query not used or found, fall back to POST request and
122
+ // include extension to cache the query on the server
139
123
response = await parseResponse < GqlResponse < TResponse > > ( ( ) =>
140
- fetch ( url . toString ( ) , {
124
+ fetch ( endpoint , {
141
125
headers : Object . fromEntries ( headers . entries ( ) ) ,
142
126
method : "POST" ,
143
- body : JSON . stringify ( { documentId , query , variables , extensions } ) ,
127
+ body : asRequestBody ( request ) ,
144
128
credentials : "include" ,
145
129
signal : options . signal ,
146
130
} ) ,
0 commit comments