Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"mode": "pre",
"tag": "beta",
"initialVersions": {
"@labdigital/graphql-fetcher": "2.0.0"
},
"changesets": [
"rich-ants-shop",
"sharp-radios-relate"
]
}
5 changes: 5 additions & 0 deletions .changeset/rich-ants-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@labdigital/graphql-fetcher": major
---

Remove deprecated is "isPersistedQuery" and make apq explicitly opt in
5 changes: 5 additions & 0 deletions .changeset/sharp-radios-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@labdigital/graphql-fetcher": minor
---

Make apq opt in for the server side client instead of opt in
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -70,4 +70,4 @@
"react-dom": ">= 18.2.0"
},
"packageManager": "pnpm@9.15.3"
}
}
2 changes: 1 addition & 1 deletion src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
8 changes: 1 addition & 7 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ type Options = {
*/
apq?: boolean;

/** Deprecated: use `apq: <boolean>` */
persistedQueries?: boolean;

/**
* Sets the default timeout duration in ms after which a request will throw a timeout error
*
Expand Down Expand Up @@ -71,7 +68,6 @@ export const initClientFetcher =
endpoint: string,
{
apq = false,
persistedQueries = false,
defaultTimeout = 30000,
defaultHeaders = {},
includeQuery = false,
Expand Down Expand Up @@ -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<GqlResponse<TResponse>>(() =>
fetch(url.toString(), {
Expand Down
56 changes: 52 additions & 4 deletions src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -257,6 +265,7 @@ describe("gqlServerFetch", () => {

const gqlServerFetch = initServerFetcher("https://localhost/graphql", {
defaultTimeout: 1,
apq: true,
});

fetchMock.mockResponse(successResponse);
Expand Down Expand Up @@ -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 },
},
);
});
150 changes: 96 additions & 54 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -81,6 +87,7 @@ export const initServerFetcher =
defaultTimeout = undefined,
defaultHeaders = {},
includeQuery = false,
apq = false,
createDocumentId = getDocumentId,
}: Options = {},
) =>
Expand Down Expand Up @@ -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<TResponse>;
} catch (err: unknown) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: err instanceof Error ? err.message : String(err),
});
throw err;
}
});
if (!apq) {
return post<TResponse, TVariables>(
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<TResponse, TVariables>(
request,
url,
cache,
next,
requestOptions,
);
}

span.end();
return response as GqlResponse<TResponse>;
} catch (err: any) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: err?.message ?? String(err),
});
throw err;
}
});
return get<TResponse, TVariables>(
request,
url,
cache,
next,
requestOptions,
);
};

const gqlPost = async <TVariables>(
Expand All @@ -204,7 +184,6 @@ const gqlPost = async <TVariables>(
) => {
const endpoint = new URL(url);
endpoint.searchParams.append("op", request.operationName);

const response = await fetch(endpoint.toString(), {
headers: options.headers,
method: "POST",
Expand Down Expand Up @@ -253,3 +232,66 @@ const parseResponse = async (

return await response.json();
};
function get<TResponse, TVariables>(
request: GraphQLRequest<TVariables>,
url: string,
cache: RequestCache | undefined,
next: NextFetchRequestConfig,
requestOptions: RequestOptions,
): GqlResponse<TResponse> | PromiseLike<GqlResponse<TResponse>> {
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<TResponse>;
} catch (err: any) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: err?.message ?? String(err),
});
throw err;
}
});
}

function post<TResponse, TVariables>(
request: GraphQLRequest<TVariables>,
url: string,
cache: RequestCache | undefined,
next: NextFetchRequestConfig,
requestOptions: RequestOptions,
): GqlResponse<TResponse> | PromiseLike<GqlResponse<TResponse>> {
return tracer.startActiveSpan(request.operationName, async (span) => {
try {
const response = await gqlPost(
url,
request,
{ cache, next },
requestOptions,
);

span.end();
return response as GqlResponse<TResponse>;
} catch (err: unknown) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: err instanceof Error ? err.message : String(err),
});
throw err;
}
});
}