-
Notifications
You must be signed in to change notification settings - Fork 308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Automatic Persisted Queries #269
Comments
I wrote a fetch implementation that implements graphql persisted queries and can be used with Use it like this: const client = new GraphQLClient(
graphUrl,
{
fetch: createPersistedQueryFetch(fetch)
}
); It'd be nice if |
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 |
I'm getting the error |
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 Why the re-assignment to |
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. |
@frederikhors It looks like |
@ericfreese THANKS! Do you mean this? // ... other
if (!res.ok) return res;
const body = await res.clone().json();
if (isPersistedQueryNotFoundError(body)) {
return fetchImpl(requestWithQueryHash.info, requestWithQueryHash.init);
} else {
return res;
} |
Hi @jasonkuhrt, we implemented a wrapper of graphql-request client where we added APQ using an implementation similar to the ones proposed above, but the result is suboptimal and we think there may be improvements in putting all of that into graphql-request itself. I saw that you're working on an extension system. Do you think that APQ should be part of extensions or can we try implement it in the core? |
What do you mean? |
Our implementation follows the following flow:
Because of this, we fall in a scenario similar to the one shown above:
and as you can see we're "stringifying" and "parsing" the body twice (one in |
There should be an extension to achieve the goals of this issue. I'll try to make the extension system support what's needed to make that happen. I am not opposed to being a first party extension. |
Can someone please summarize in high level point form what the requirements here are? E.g. A GraphQL document just needs to be SHA'd and have that value sent to the server in place of the actual document? What about variables? Etc. Thanks |
Reference for APQ: https://www.apollographql.com/docs/apollo-server/performance/apq/ (it's from Apollo but it's become the de facto standard)
(A note on point 1: to avoid calculating sha256 on each request, we run a codegen with our queries to pre-calculate the sha and bundle them with our code) |
Can I ask you how? |
@frederikhors we're using graphql-codegen Client Preset with
This generates:
Then, it's just a matter of connecting this to graphql-request (in our wrapper we send the dictionary to the client upon creation, and send the appropriate hash when we need to run the query) |
Thank you @marcoreni I'll take a look at that as a next step for this issue. Will remove the needs more info label once I have had time to do that. ty! |
I found this helpful as a kind of primer on the topic https://the-guild.dev/graphql/yoga-server/docs/features/persisted-operations. Because Graffle will have a query builder interface, solutions that is based on statically analyzing for GraphQL syntax documents is not going to work. I am curious about using the Graffle CLI to expose a solution native to Graffle. The entire thing is rather ambitious for me though and I don't expect this to get done soon. If anyone has proposals/hints on solutions let me know. Just keep in mind the query builder interface requirement of Graffle. |
@marcoreni I'm currently looking at migrating our Apollo codegen solution over to a React Query + graphql-request codegen solution. We also use APQ at the moment and would love to see your wrapper solution for reference if you're happy to share. |
can we use persisted queries with graphql-request?
The text was updated successfully, but these errors were encountered: