Skip to content

Commit 6d280dd

Browse files
committed
feat: explicitly require apq on server fetcher
1 parent 0b99c22 commit 6d280dd

File tree

2 files changed

+148
-58
lines changed

2 files changed

+148
-58
lines changed

src/server.test.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ const errorResponse = JSON.stringify({
2525

2626
describe("gqlServerFetch", () => {
2727
it("should fetch a persisted query", async () => {
28-
const gqlServerFetch = initServerFetcher("https://localhost/graphql");
28+
const gqlServerFetch = initServerFetcher("https://localhost/graphql", {
29+
apq: true,
30+
});
2931
const mockedFetch = fetchMock.mockResponse(successResponse);
3032
const gqlResponse = await gqlServerFetch(
3133
query,
@@ -57,7 +59,9 @@ describe("gqlServerFetch", () => {
5759
});
5860

5961
it("should persist the query if it wasn't persisted yet", async () => {
60-
const gqlServerFetch = initServerFetcher("https://localhost/graphql");
62+
const gqlServerFetch = initServerFetcher("https://localhost/graphql", {
63+
apq: true,
64+
});
6165
// Mock server saying: 'PersistedQueryNotFound'
6266
const mockedFetch = fetchMock
6367
.mockResponseOnce(errorResponse)
@@ -134,7 +138,9 @@ describe("gqlServerFetch", () => {
134138
});
135139

136140
it("should fetch a persisted query without revalidate", async () => {
137-
const gqlServerFetch = initServerFetcher("https://localhost/graphql");
141+
const gqlServerFetch = initServerFetcher("https://localhost/graphql", {
142+
apq: true,
143+
});
138144
const mockedFetch = fetchMock.mockResponse(successResponse);
139145
const gqlResponse = await gqlServerFetch(
140146
query,
@@ -166,7 +172,9 @@ describe("gqlServerFetch", () => {
166172
});
167173

168174
it("should fetch a with custom headers", async () => {
169-
const gqlServerFetch = initServerFetcher("https://localhost/graphql");
175+
const gqlServerFetch = initServerFetcher("https://localhost/graphql", {
176+
apq: true,
177+
});
170178
const mockedFetch = fetchMock.mockResponse(successResponse);
171179
const gqlResponse = await gqlServerFetch(
172180
query,
@@ -257,6 +265,7 @@ describe("gqlServerFetch", () => {
257265

258266
const gqlServerFetch = initServerFetcher("https://localhost/graphql", {
259267
defaultTimeout: 1,
268+
apq: true,
260269
});
261270

262271
fetchMock.mockResponse(successResponse);
@@ -338,3 +347,42 @@ describe("gqlServerFetch", () => {
338347
expect(fetchMock).toHaveBeenCalledTimes(1);
339348
});
340349
});
350+
351+
it("should skip persisted queries if operation apq is disabled", async () => {
352+
const gqlServerFetch = initServerFetcher("https://localhost/graphql", {
353+
apq: false,
354+
});
355+
const mockedFetch = fetchMock.mockResponseOnce(successResponse);
356+
357+
const gqlResponse = await gqlServerFetch(
358+
query,
359+
{ myVar: "baz" },
360+
{
361+
next: { revalidate: 900 },
362+
},
363+
);
364+
365+
expect(gqlResponse).toEqual(response);
366+
expect(mockedFetch).toHaveBeenCalledTimes(1);
367+
expect(mockedFetch).toHaveBeenNthCalledWith(
368+
1,
369+
"https://localhost/graphql?op=myQuery",
370+
{
371+
method: "POST",
372+
body: JSON.stringify({
373+
query: query.toString(),
374+
variables: { myVar: "baz" },
375+
extensions: {
376+
persistedQuery: {
377+
version: 1,
378+
sha256Hash: await createSha256(query.toString()),
379+
},
380+
},
381+
}),
382+
headers: new Headers({
383+
"Content-Type": "application/json",
384+
}),
385+
next: { revalidate: 900 },
386+
},
387+
);
388+
});

src/server.ts

Lines changed: 96 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ type RequestOptions = {
3131
};
3232

3333
type Options = {
34+
/**
35+
* Enable use of automated persisted queries, this will always add a extra
36+
* roundtrip to the server if queries aren't cacheable
37+
* @default false
38+
*/
39+
apq?: boolean;
3440
/**
3541
* Disables all forms of caching for the fetcher, use only in development
3642
*
@@ -81,6 +87,7 @@ export const initServerFetcher =
8187
defaultTimeout = undefined,
8288
defaultHeaders = {},
8389
includeQuery = false,
90+
apq = false,
8491
createDocumentId = getDocumentId,
8592
}: Options = {},
8693
) =>
@@ -137,63 +144,36 @@ export const initServerFetcher =
137144
});
138145
}
139146

140-
// Skip automatic persisted queries if operation is a mutation
141147
const queryType = getQueryType(query);
142-
if (queryType === "mutation") {
143-
return tracer.startActiveSpan(request.operationName, async (span) => {
144-
try {
145-
const response = await gqlPost(
146-
url,
147-
request,
148-
{ cache, next },
149-
requestOptions,
150-
);
151-
152-
span.end();
153-
return response as GqlResponse<TResponse>;
154-
} catch (err: unknown) {
155-
span.setStatus({
156-
code: SpanStatusCode.ERROR,
157-
message: err instanceof Error ? err.message : String(err),
158-
});
159-
throw err;
160-
}
161-
});
148+
if (!apq) {
149+
return post<TResponse, TVariables>(
150+
request,
151+
url,
152+
cache,
153+
next,
154+
requestOptions,
155+
);
162156
}
163157

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-
}
158+
// if apq is enabled, only queries are converted into get calls
159+
// https://www.apollographql.com/docs/apollo-server/performance/apq#using-get-requests-with-apq-on-a-cdn
160+
if (queryType === "mutation") {
161+
return post<TResponse, TVariables>(
162+
request,
163+
url,
164+
cache,
165+
next,
166+
requestOptions,
167+
);
168+
}
186169

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-
});
170+
return get<TResponse, TVariables>(
171+
request,
172+
url,
173+
cache,
174+
next,
175+
requestOptions,
176+
);
197177
};
198178

199179
const gqlPost = async <TVariables>(
@@ -204,7 +184,6 @@ const gqlPost = async <TVariables>(
204184
) => {
205185
const endpoint = new URL(url);
206186
endpoint.searchParams.append("op", request.operationName);
207-
208187
const response = await fetch(endpoint.toString(), {
209188
headers: options.headers,
210189
method: "POST",
@@ -253,3 +232,66 @@ const parseResponse = async (
253232

254233
return await response.json();
255234
};
235+
function get<TResponse, TVariables>(
236+
request: GraphQLRequest<TVariables>,
237+
url: string,
238+
cache: RequestCache | undefined,
239+
next: NextFetchRequestConfig,
240+
requestOptions: RequestOptions,
241+
): GqlResponse<TResponse> | PromiseLike<GqlResponse<TResponse>> {
242+
return tracer.startActiveSpan(request.operationName, async (span) => {
243+
try {
244+
let response = await gqlPersistedQuery(
245+
url,
246+
request,
247+
{ cache, next },
248+
requestOptions,
249+
);
250+
251+
// If this is not a persisted query, but we tried to use automatic
252+
// persisted queries (APQ) then we retry with a POST
253+
if (!isPersistedQuery(request) && hasPersistedQueryError(response)) {
254+
// If the cached query doesn't exist, fall back to POST request and
255+
// let the server cache it.
256+
response = await gqlPost(url, request, { cache, next }, requestOptions);
257+
}
258+
259+
span.end();
260+
return response as GqlResponse<TResponse>;
261+
} catch (err: any) {
262+
span.setStatus({
263+
code: SpanStatusCode.ERROR,
264+
message: err?.message ?? String(err),
265+
});
266+
throw err;
267+
}
268+
});
269+
}
270+
271+
function post<TResponse, TVariables>(
272+
request: GraphQLRequest<TVariables>,
273+
url: string,
274+
cache: RequestCache | undefined,
275+
next: NextFetchRequestConfig,
276+
requestOptions: RequestOptions,
277+
): GqlResponse<TResponse> | PromiseLike<GqlResponse<TResponse>> {
278+
return tracer.startActiveSpan(request.operationName, async (span) => {
279+
try {
280+
const response = await gqlPost(
281+
url,
282+
request,
283+
{ cache, next },
284+
requestOptions,
285+
);
286+
287+
span.end();
288+
return response as GqlResponse<TResponse>;
289+
} catch (err: unknown) {
290+
span.setStatus({
291+
code: SpanStatusCode.ERROR,
292+
message: err instanceof Error ? err.message : String(err),
293+
});
294+
throw err;
295+
}
296+
});
297+
}

0 commit comments

Comments
 (0)