1- import type { DocumentTypeDecoration } from "@graphql-typed-document-node/core" ;
2- import { SpanStatusCode , trace } from "@opentelemetry/api" ;
1+ import type { DocumentTypeDecoration } from "@graphql-typed-document-node/core" ;
2+ import { SpanStatusCode , trace } from "@opentelemetry/api" ;
33import invariant from "tiny-invariant" ;
44import {
55 getDocumentId ,
@@ -10,15 +10,16 @@ import {
1010 mergeHeaders ,
1111 hasPersistedQueryError ,
1212} from "./helpers" ;
13- import { print } from "graphql" ;
14- import { isNode } from "graphql/language/ast.js" ;
13+ import { print , type GraphQLError } from "graphql" ;
14+ import { isNode } from "graphql/language/ast.js" ;
1515import {
1616 createRequest ,
1717 createRequestBody ,
1818 createRequestURL ,
1919 type GraphQLRequest ,
2020 isPersistedQuery ,
2121} from "./request" ;
22+ import { toe } from "graphql-toe" ;
2223
2324type RequestOptions = {
2425 /**
@@ -73,6 +74,32 @@ const tracer = trace.getTracer(
7374 process . env . PACKAGE_VERSION ,
7475) ;
7576
77+ // Wraps the initServerFetcher function, which returns the result wrapped in the graphql-toe library. This will throw
78+ // an error if a field is used that had an entry in the error response array
79+ export const initStrictServerFetcher = ( url : string , options : Options = { } ) => {
80+ const fetcher = initServerFetcher ( url , options ) ;
81+ return async < TResponse extends Record < string , any > , TVariables > (
82+ astNode : DocumentTypeDecoration < TResponse , TVariables > ,
83+ variables : TVariables ,
84+ cacheOptions : CacheOptions ,
85+ requestOptions : RequestOptions = { } ,
86+ ) : Promise < TResponse > => {
87+ const response = await fetcher (
88+ astNode ,
89+ variables ,
90+ cacheOptions ,
91+ requestOptions ,
92+ ) ;
93+
94+ return toe < TResponse > (
95+ response as unknown as {
96+ data ?: TResponse | null | undefined ;
97+ errors ?: readonly GraphQLError [ ] | undefined ;
98+ } ,
99+ ) ;
100+ } ;
101+ } ;
102+
76103export const initServerFetcher =
77104 (
78105 url : string ,
@@ -84,122 +111,122 @@ export const initServerFetcher =
84111 createDocumentId = getDocumentId ,
85112 } : Options = { } ,
86113 ) =>
87- async < TResponse , TVariables > (
88- astNode : DocumentTypeDecoration < TResponse , TVariables > ,
89- variables : TVariables ,
90- { cache, next = { } } : CacheOptions ,
91- options : RequestOptions = { } ,
92- ) : Promise < GqlResponse < TResponse > > => {
93- const query = isNode ( astNode ) ? print ( astNode ) : astNode . toString ( ) ;
94-
95- const documentId = createDocumentId ( astNode ) ;
96- const request = await createRequest (
97- query ,
98- variables ,
99- documentId ,
100- includeQuery ,
101- ) ;
102- const requestOptions : RequestOptions = {
103- ...options ,
104- signal :
105- defaultTimeout !== undefined && ! options . signal
106- ? AbortSignal . timeout ( defaultTimeout )
107- : options . signal ,
108- headers : mergeHeaders ( { ...defaultHeaders , ...options . headers } ) ,
109- } ;
114+ async < TResponse , TVariables > (
115+ astNode : DocumentTypeDecoration < TResponse , TVariables > ,
116+ variables : TVariables ,
117+ { cache, next = { } } : CacheOptions ,
118+ options : RequestOptions = { } ,
119+ ) : Promise < GqlResponse < TResponse > > => {
120+ const query = isNode ( astNode ) ? print ( astNode ) : astNode . toString ( ) ;
110121
111- // When cache is disabled we always make a POST request and set the
112- // cache to no-store to prevent any caching
113- if ( dangerouslyDisableCache ) {
114- // If we force the cache field we shouldn't set revalidate at all, it will
115- // throw a warning otherwise
116- delete next . revalidate ;
117- delete request . extensions ?. persistedQuery ;
122+ const documentId = createDocumentId ( astNode ) ;
123+ const request = await createRequest (
124+ query ,
125+ variables ,
126+ documentId ,
127+ includeQuery ,
128+ ) ;
129+ const requestOptions : RequestOptions = {
130+ ...options ,
131+ signal :
132+ defaultTimeout !== undefined && ! options . signal
133+ ? AbortSignal . timeout ( defaultTimeout )
134+ : options . signal ,
135+ headers : mergeHeaders ( { ...defaultHeaders , ...options . headers } ) ,
136+ } ;
118137
119- return tracer . startActiveSpan ( request . operationName , async ( span ) => {
120- try {
121- const response = await gqlPost (
122- url ,
123- request ,
124- { ...next , cache : "no-store" } ,
125- requestOptions ,
126- ) ;
138+ // When cache is disabled we always make a POST request and set the
139+ // cache to no-store to prevent any caching
140+ if ( dangerouslyDisableCache ) {
141+ // If we force the cache field we shouldn't set revalidate at all, it will
142+ // throw a warning otherwise
143+ delete next . revalidate ;
144+ delete request . extensions ?. persistedQuery ;
127145
128- span . end ( ) ;
129- return response as GqlResponse < TResponse > ;
130- } catch ( err : any ) {
131- span . setStatus ( {
132- code : SpanStatusCode . ERROR ,
133- message : err ?. message ?? String ( err ) ,
134- } ) ;
135- throw err ;
136- }
137- } ) ;
138- }
146+ return tracer . startActiveSpan ( request . operationName , async ( span ) => {
147+ try {
148+ const response = await gqlPost (
149+ url ,
150+ request ,
151+ { ...next , cache : "no-store" } ,
152+ requestOptions ,
153+ ) ;
139154
140- // Skip automatic persisted queries if operation is a mutation
141- const queryType = getQueryType ( query ) ;
142- if ( queryType === "mutation" ) {
155+ span . end ( ) ;
156+ return response as GqlResponse < TResponse > ;
157+ } catch ( err : any ) {
158+ span . setStatus ( {
159+ code : SpanStatusCode . ERROR ,
160+ message : err ?. message ?? String ( err ) ,
161+ } ) ;
162+ throw err ;
163+ }
164+ } ) ;
165+ }
166+
167+ // Skip automatic persisted queries if operation is a mutation
168+ const queryType = getQueryType ( query ) ;
169+ if ( queryType === "mutation" ) {
170+ return tracer . startActiveSpan ( request . operationName , async ( span ) => {
171+ try {
172+ const response = await gqlPost (
173+ url ,
174+ request ,
175+ { cache, next} ,
176+ requestOptions ,
177+ ) ;
178+
179+ span . end ( ) ;
180+ return response as GqlResponse < TResponse > ;
181+ } catch ( err : unknown ) {
182+ span . setStatus ( {
183+ code : SpanStatusCode . ERROR ,
184+ message : err instanceof Error ? err . message : String ( err ) ,
185+ } ) ;
186+ throw err ;
187+ }
188+ } ) ;
189+ }
190+
191+ // Otherwise, try to get the cached query
143192 return tracer . startActiveSpan ( request . operationName , async ( span ) => {
144193 try {
145- const response = await gqlPost (
194+ let response = await gqlPersistedQuery (
146195 url ,
147196 request ,
148- { cache, next } ,
197+ { cache, next} ,
149198 requestOptions ,
150199 ) ;
151200
201+ // If this is not a persisted query, but we tried to use automatic
202+ // persisted queries (APQ) then we retry with a POST
203+ if ( ! isPersistedQuery ( request ) && hasPersistedQueryError ( response ) ) {
204+ // If the cached query doesn't exist, fall back to POST request and
205+ // let the server cache it.
206+ response = await gqlPost (
207+ url ,
208+ request ,
209+ { cache, next} ,
210+ requestOptions ,
211+ ) ;
212+ }
213+
152214 span . end ( ) ;
153215 return response as GqlResponse < TResponse > ;
154- } catch ( err : unknown ) {
216+ } catch ( err : any ) {
155217 span . setStatus ( {
156218 code : SpanStatusCode . ERROR ,
157- message : err instanceof Error ? err . message : String ( err ) ,
219+ message : err ? .message ?? String ( err ) ,
158220 } ) ;
159221 throw err ;
160222 }
161223 } ) ;
162- }
163-
164- // Otherwise, try to get the cached query
165- return tracer . startActiveSpan ( request . operationName , async ( span ) => {
166- try {
167- let response = await gqlPersistedQuery (
168- url ,
169- request ,
170- { cache, next } ,
171- requestOptions ,
172- ) ;
173-
174- // If this is not a persisted query, but we tried to use automatic
175- // persisted queries (APQ) then we retry with a POST
176- if ( ! isPersistedQuery ( request ) && hasPersistedQueryError ( response ) ) {
177- // If the cached query doesn't exist, fall back to POST request and
178- // let the server cache it.
179- response = await gqlPost (
180- url ,
181- request ,
182- { cache, next } ,
183- requestOptions ,
184- ) ;
185- }
186-
187- span . end ( ) ;
188- return response as GqlResponse < TResponse > ;
189- } catch ( err : any ) {
190- span . setStatus ( {
191- code : SpanStatusCode . ERROR ,
192- message : err ?. message ?? String ( err ) ,
193- } ) ;
194- throw err ;
195- }
196- } ) ;
197- } ;
224+ } ;
198225
199226const gqlPost = async < TVariables > (
200227 url : string ,
201228 request : GraphQLRequest < TVariables > ,
202- { cache, next } : CacheOptions ,
229+ { cache, next} : CacheOptions ,
203230 options : RequestOptions ,
204231) => {
205232 const endpoint = new URL ( url ) ;
@@ -220,7 +247,7 @@ const gqlPost = async <TVariables>(
220247const gqlPersistedQuery = async < TVariables > (
221248 endpoint : string ,
222249 request : GraphQLRequest < TVariables > ,
223- { cache, next } : CacheOptions ,
250+ { cache, next} : CacheOptions ,
224251 options : RequestOptions ,
225252) => {
226253 const url = createRequestURL ( endpoint , request ) ;
0 commit comments