diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000..83827a3 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,11 @@ +{ + "mode": "pre", + "tag": "beta", + "initialVersions": { + "@labdigital/graphql-fetcher": "2.0.0" + }, + "changesets": [ + "rich-ants-shop", + "sharp-radios-relate" + ] +} diff --git a/.changeset/rich-ants-shop.md b/.changeset/rich-ants-shop.md new file mode 100644 index 0000000..205905d --- /dev/null +++ b/.changeset/rich-ants-shop.md @@ -0,0 +1,5 @@ +--- +"@labdigital/graphql-fetcher": major +--- + +Remove deprecated is "isPersistedQuery" and make apq explicitly opt in diff --git a/.changeset/sharp-radios-relate.md b/.changeset/sharp-radios-relate.md new file mode 100644 index 0000000..2a1553a --- /dev/null +++ b/.changeset/sharp-radios-relate.md @@ -0,0 +1,5 @@ +--- +"@labdigital/graphql-fetcher": minor +--- + +Make apq opt in for the server side client instead of opt in diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bda83a..7074d90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # @labdigital/react-query-opal +## 3.0.0-beta.1 + +### Minor Changes + +- Make apq opt in for the server side client instead of opt in + +## 3.0.0-beta.0 + +### Major Changes + +- ff7f7e4: Remove deprecated is "isPersistedQuery" and make apq explicitly opt in + ## 2.0.0 ### Minor Changes diff --git a/package.json b/package.json index 9a1d227..10d8b87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@labdigital/graphql-fetcher", - "version": "2.0.0", + "version": "3.0.0-beta.1", "description": "Custom fetcher for react-query to use with @labdigital/node-federated-token", "type": "module", "main": "./dist/index.cjs", @@ -70,4 +70,4 @@ "react-dom": ">= 18.2.0" }, "packageManager": "pnpm@9.15.3" -} +} \ No newline at end of file diff --git a/src/client.test.ts b/src/client.test.ts index a1983d4..f21d23c 100644 --- a/src/client.test.ts +++ b/src/client.test.ts @@ -37,7 +37,7 @@ describe("gqlClientFetch", () => { const fetcher = initClientFetcher("https://localhost/graphql"); const persistedFetcher = initClientFetcher("https://localhost/graphql", { - persistedQueries: true, + apq: true, }); it("should perform a query", async () => { diff --git a/src/client.ts b/src/client.ts index a8b0a11..e8ecb37 100644 --- a/src/client.ts +++ b/src/client.ts @@ -25,9 +25,6 @@ type Options = { */ apq?: boolean; - /** Deprecated: use `apq: ` */ - persistedQueries?: boolean; - /** * Sets the default timeout duration in ms after which a request will throw a timeout error * @@ -71,7 +68,6 @@ export const initClientFetcher = endpoint: string, { apq = false, - persistedQueries = false, defaultTimeout = 30000, defaultHeaders = {}, includeQuery = false, @@ -110,10 +106,8 @@ export const initClientFetcher = const queryType = getQueryType(query); - apq = apq || persistedQueries; - // For queries we can use GET requests if persisted queries are enabled - if (queryType === "query" && (apq || isPersistedQuery(request))) { + if (queryType === "query" && apq) { const url = createRequestURL(endpoint, request); response = await parseResponse>(() => fetch(url.toString(), { diff --git a/src/server.test.ts b/src/server.test.ts index a739085..94ad16a 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -25,7 +25,9 @@ const errorResponse = JSON.stringify({ describe("gqlServerFetch", () => { it("should fetch a persisted query", async () => { - const gqlServerFetch = initServerFetcher("https://localhost/graphql"); + const gqlServerFetch = initServerFetcher("https://localhost/graphql", { + apq: true, + }); const mockedFetch = fetchMock.mockResponse(successResponse); const gqlResponse = await gqlServerFetch( query, @@ -57,7 +59,9 @@ describe("gqlServerFetch", () => { }); it("should persist the query if it wasn't persisted yet", async () => { - const gqlServerFetch = initServerFetcher("https://localhost/graphql"); + const gqlServerFetch = initServerFetcher("https://localhost/graphql", { + apq: true, + }); // Mock server saying: 'PersistedQueryNotFound' const mockedFetch = fetchMock .mockResponseOnce(errorResponse) @@ -134,7 +138,9 @@ describe("gqlServerFetch", () => { }); it("should fetch a persisted query without revalidate", async () => { - const gqlServerFetch = initServerFetcher("https://localhost/graphql"); + const gqlServerFetch = initServerFetcher("https://localhost/graphql", { + apq: true, + }); const mockedFetch = fetchMock.mockResponse(successResponse); const gqlResponse = await gqlServerFetch( query, @@ -166,7 +172,9 @@ describe("gqlServerFetch", () => { }); it("should fetch a with custom headers", async () => { - const gqlServerFetch = initServerFetcher("https://localhost/graphql"); + const gqlServerFetch = initServerFetcher("https://localhost/graphql", { + apq: true, + }); const mockedFetch = fetchMock.mockResponse(successResponse); const gqlResponse = await gqlServerFetch( query, @@ -257,6 +265,7 @@ describe("gqlServerFetch", () => { const gqlServerFetch = initServerFetcher("https://localhost/graphql", { defaultTimeout: 1, + apq: true, }); fetchMock.mockResponse(successResponse); @@ -338,3 +347,42 @@ describe("gqlServerFetch", () => { expect(fetchMock).toHaveBeenCalledTimes(1); }); }); + +it("should skip persisted queries if operation apq is disabled", async () => { + const gqlServerFetch = initServerFetcher("https://localhost/graphql", { + apq: false, + }); + const mockedFetch = fetchMock.mockResponseOnce(successResponse); + + const gqlResponse = await gqlServerFetch( + query, + { myVar: "baz" }, + { + next: { revalidate: 900 }, + }, + ); + + expect(gqlResponse).toEqual(response); + expect(mockedFetch).toHaveBeenCalledTimes(1); + expect(mockedFetch).toHaveBeenNthCalledWith( + 1, + "https://localhost/graphql?op=myQuery", + { + method: "POST", + body: JSON.stringify({ + query: query.toString(), + variables: { myVar: "baz" }, + extensions: { + persistedQuery: { + version: 1, + sha256Hash: await createSha256(query.toString()), + }, + }, + }), + headers: new Headers({ + "Content-Type": "application/json", + }), + next: { revalidate: 900 }, + }, + ); +}); diff --git a/src/server.ts b/src/server.ts index b5ce68e..6aa2493 100644 --- a/src/server.ts +++ b/src/server.ts @@ -31,6 +31,12 @@ type RequestOptions = { }; type Options = { + /** + * Enable use of automated persisted queries, this will always add a extra + * roundtrip to the server if queries aren't cacheable + * @default false + */ + apq?: boolean; /** * Disables all forms of caching for the fetcher, use only in development * @@ -81,6 +87,7 @@ export const initServerFetcher = defaultTimeout = undefined, defaultHeaders = {}, includeQuery = false, + apq = false, createDocumentId = getDocumentId, }: Options = {}, ) => @@ -137,63 +144,36 @@ export const initServerFetcher = }); } - // Skip automatic persisted queries if operation is a mutation const queryType = getQueryType(query); - if (queryType === "mutation") { - return tracer.startActiveSpan(request.operationName, async (span) => { - try { - const response = await gqlPost( - url, - request, - { cache, next }, - requestOptions, - ); - - span.end(); - return response as GqlResponse; - } catch (err: unknown) { - span.setStatus({ - code: SpanStatusCode.ERROR, - message: err instanceof Error ? err.message : String(err), - }); - throw err; - } - }); + if (!apq) { + return post( + request, + url, + cache, + next, + requestOptions, + ); } - // Otherwise, try to get the cached query - return tracer.startActiveSpan(request.operationName, async (span) => { - try { - let response = await gqlPersistedQuery( - url, - request, - { cache, next }, - requestOptions, - ); - - // If this is not a persisted query, but we tried to use automatic - // persisted queries (APQ) then we retry with a POST - if (!isPersistedQuery(request) && hasPersistedQueryError(response)) { - // If the cached query doesn't exist, fall back to POST request and - // let the server cache it. - response = await gqlPost( - url, - request, - { cache, next }, - requestOptions, - ); - } + // if apq is enabled, only queries are converted into get calls + // https://www.apollographql.com/docs/apollo-server/performance/apq#using-get-requests-with-apq-on-a-cdn + if (queryType === "mutation") { + return post( + request, + url, + cache, + next, + requestOptions, + ); + } - span.end(); - return response as GqlResponse; - } catch (err: any) { - span.setStatus({ - code: SpanStatusCode.ERROR, - message: err?.message ?? String(err), - }); - throw err; - } - }); + return get( + request, + url, + cache, + next, + requestOptions, + ); }; const gqlPost = async ( @@ -204,7 +184,6 @@ const gqlPost = async ( ) => { const endpoint = new URL(url); endpoint.searchParams.append("op", request.operationName); - const response = await fetch(endpoint.toString(), { headers: options.headers, method: "POST", @@ -253,3 +232,66 @@ const parseResponse = async ( return await response.json(); }; +function get( + request: GraphQLRequest, + url: string, + cache: RequestCache | undefined, + next: NextFetchRequestConfig, + requestOptions: RequestOptions, +): GqlResponse | PromiseLike> { + return tracer.startActiveSpan(request.operationName, async (span) => { + try { + let response = await gqlPersistedQuery( + url, + request, + { cache, next }, + requestOptions, + ); + + // If this is not a persisted query, but we tried to use automatic + // persisted queries (APQ) then we retry with a POST + if (!isPersistedQuery(request) && hasPersistedQueryError(response)) { + // If the cached query doesn't exist, fall back to POST request and + // let the server cache it. + response = await gqlPost(url, request, { cache, next }, requestOptions); + } + + span.end(); + return response as GqlResponse; + } catch (err: any) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: err?.message ?? String(err), + }); + throw err; + } + }); +} + +function post( + request: GraphQLRequest, + url: string, + cache: RequestCache | undefined, + next: NextFetchRequestConfig, + requestOptions: RequestOptions, +): GqlResponse | PromiseLike> { + return tracer.startActiveSpan(request.operationName, async (span) => { + try { + const response = await gqlPost( + url, + request, + { cache, next }, + requestOptions, + ); + + span.end(); + return response as GqlResponse; + } catch (err: unknown) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: err instanceof Error ? err.message : String(err), + }); + throw err; + } + }); +}