graphql-request
graphql-request copied to clipboard
Automatic Persisted Queries
can we use persisted queries with graphql-request?
I wrote a fetch implementation that implements graphql persisted queries and can be used with graphql-request
(and probably also with other clients but I haven't tested this).
Use it like this:
const client = new GraphQLClient(
graphUrl,
{
fetch: createPersistedQueryFetch(fetch)
}
);
It'd be nice if graphql-request
added support for request transformers, but this approach works well for me in my project.
Hi, Thanks for the fetch implementation @talzion12.
Unfortunately, the sha256 function in your snippet didn't work in my case (iOS), so I had to replace it with the help of native implementation (https://github.com/itinance/react-native-sha256). Then it worked.
I'm pasting here my version, so that it could help others if they get the same issue.
const VERSION = 1;
import { sha256 } from 'react-native-sha256';
type Fetch = typeof fetch;
/**
* Creates a fetch implementation that sends GraphQL persisted query requests.
*/
export const createPersistedQueryFetch =
(fetchImpl: Fetch): Fetch =>
async (info, init) => {
const request = { info, init };
const processor = getRequestProcessor(request);
const requestWithQueryHash = await processor.addHash(request);
const requestWithoutQuery = processor.removeQuery(requestWithQueryHash);
// send a request without the query
const res = await fetchImpl(
requestWithoutQuery.info,
requestWithoutQuery.init,
);
const body = await res.json();
// if the query was not found in the server,
// send another request with the query
if (isPersistedQueryNotFoundError(body)) {
return fetchImpl(requestWithQueryHash.info, requestWithQueryHash.init);
} else {
res.json = () => Promise.resolve(body);
return res;
}
};
/**
* Manipulates a fetch request, implemented per HTTP method type.
*/
interface RequestProcessor {
/**
* Removes the GraphQL query argument from the request
*/
removeQuery(request: Request): Request;
/**
* Adds the GraphQL request query hash to the request
*/
addHash(request: Request): Promise<Request>;
}
function getRequestProcessor(request: Request) {
const method = (request.init?.method ?? 'GET').toUpperCase();
const requestProcessor = requestProcessorByMethod[method];
if (!requestProcessor) {
throw new Error('Unsupported request method: ' + method);
}
return requestProcessor;
}
const requestProcessorByMethod: Record<string, RequestProcessor> = {
GET: {
removeQuery: request => {
const [url, params] = splitUrlAndSearchParams(
getRequestInfoUrl(request.info),
);
params.delete('query');
return {
...request,
info: requestInfoWithUpdatedUrl(
request.info,
`${url}?${params.toString()}`,
),
};
},
addHash: async request => {
const [url, params] = splitUrlAndSearchParams(
getRequestInfoUrl(request.info),
);
const query = params.get('query');
if (!query) {
throw new Error('GET request must contain a query parameter');
}
const hash = await sha256(query);
params.append(
'extensions',
JSON.stringify({
persistedQuery: {
version: VERSION,
sha256Hash: hash,
},
}),
);
return {
...request,
info: requestInfoWithUpdatedUrl(
request.info,
`${url}?${params.toString()}`,
),
};
},
},
POST: {
removeQuery: request => {
if (typeof request.init?.body !== 'string') {
throw new Error('POST request must contain a body');
}
const body = JSON.parse(request.init.body);
const { query, ...bodyWithoutQuery } = body;
return {
...request,
init: {
...request.init,
body: JSON.stringify(bodyWithoutQuery),
},
};
},
addHash: async request => {
if (typeof request.init?.body !== 'string') {
throw new Error('POST request must contain a body');
}
const body = JSON.parse(request.init.body);
if (typeof body.query !== 'string') {
throw new Error('POST request body must contain a query');
}
const hash = await sha256(body.query);
return {
...request,
init: {
...request.init,
body: JSON.stringify({
...body,
extensions: {
persistedQuery: {
version: VERSION,
sha256Hash: hash,
},
},
}),
},
};
},
},
};
interface Request {
info: RequestInfo;
init?: RequestInit;
}
function requestInfoWithUpdatedUrl(
info: RequestInfo,
url: string,
): RequestInfo {
return typeof info === 'string'
? url
: {
...info,
url,
};
}
function getRequestInfoUrl(info: RequestInfo) {
return typeof info === 'string' ? info : info.url;
}
function splitUrlAndSearchParams(
url: string,
): [urlWithoutSearchParams: string, params: URLSearchParams] {
const startOfSearchParams = url.indexOf('?');
return startOfSearchParams === -1
? [url, new URLSearchParams()]
: [
url.slice(0, startOfSearchParams),
new URLSearchParams(url.slice(startOfSearchParams)),
];
}
interface GraphQLResponse {
errors?: {
message?: string;
}[];
}
function isPersistedQueryNotFoundError(resBody: GraphQLResponse) {
return (
resBody.errors &&
resBody.errors.length > 0 &&
resBody.errors.find(err => err.message === 'PersistedQueryNotFound') != null
);
}
@jasonkuhrt May I ask if this feature interests you?
Can we at least put the @talzion12 and @rubanraj54 code in the Readme and in a "plugins" folder so the magic of open source can infect all future users better than us who will make improvements?
@talzion12, @rubanraj54 do you have new code? I think this is not usable with today graphql-request
.
I'm getting the error Failed to execute 'text' on 'Response': body stream already read
in console.
This is the issue:
export const createPersistedQueryFetch =
(fetchImpl: Fetch): Fetch =>
async (info, init) => {
// ...
// send a request without the query
const res = await fetchImpl(
requestWithoutQuery.info,
requestWithoutQuery.init
);
const body = await res.json();
// if the query was not found in the server, send another request with the query
if (isPersistedQueryNotFoundError(body)) {
return fetchImpl(requestWithQueryHash.info, requestWithQueryHash.init);
} else {
res.json = () => Promise.resolve(body);
return res;
}
};
graphql-request is trying to re-read the body already read in createPersistedQueryFetch
function.
Why the re-assignment to res.json
(with res.json = () => Promise.resolve(body)
) is not working?
I found a way to make it work:
export const createPersistedQueryFetch = (fetchImpl: Fetch): Fetch =>
async (info, init) => {
const request = { info, init };
const processor = getRequestProcessor(request);
const requestWithQueryHash = await processor.addHash(request);
const requestWithoutQuery = processor.removeQuery(requestWithQueryHash);
// send a request without the query
const res = await fetchImpl(requestWithoutQuery.info, requestWithoutQuery.init);
const body = await res.json();
// if the query was not found in the server,
// send another request with the query
if (isPersistedQueryNotFoundError(body)) {
return fetchImpl(requestWithQueryHash.info, requestWithQueryHash.init);
} else {
// ---------> Here the changes! <-----------
// I'm returning a new Response here
return new Response(JSON.stringify(body), {
status: res.status,
statusText: res.statusText,
headers: res.headers
});
}
};
I would like to know your thoughts and if there is room for improvement.
I don't think it's the best solution, but it's working great so far.
@frederikhors sorry man, I moved to a new organization. So not sure what they are doing now with the code now.
I just contributed to the bounty on this issue:
https://until.dev/bounty/jasonkuhrt/graphql-request/269
The current bounty for completing it is $20.00 if it is closed within 1 month, and decreases after that.