Skip to content

Commit 6725f8e

Browse files
n1ru4lEmrysMyrddin
andauthored
feat: allow accessing request within persisted operation id extraction (#3183)
* feat: allow accessing request within persisted operation id extraction * chore: add changeset * chore: types * try fixing CI --------- Co-authored-by: Valentin Cocaud <[email protected]>
1 parent 8dfbdcc commit 6725f8e

File tree

5 files changed

+216
-3
lines changed

5 files changed

+216
-3
lines changed

.changeset/long-tips-try.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphql-yoga/plugin-persisted-operations': minor
3+
---
4+
5+
Inject request into `extractPersistedOperationId` function for allowing to extract the ID based on
6+
request header, query parameters or request path.

.github/workflows/pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
if: ${{ github.event.pull_request.title != 'Upcoming Release Changes' }}
2424
with:
2525
npmTag: rc
26-
restoreDeletedChangesets: true
26+
restoreDeletedChangesets: false
2727
buildScript: build
2828
nodeVersion: 20
2929
packageManager: pnpm

packages/plugins/persisted-operations/__tests__/persisted-operations.spec.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,4 +391,82 @@ describe('Persisted Operations', () => {
391391
expect(body.errors).toBeUndefined();
392392
expect(body.data.__typename).toBe('Query');
393393
});
394+
395+
it('extract key from request query parameter', async () => {
396+
const store = new Map<string, string>();
397+
const yoga = createYoga({
398+
plugins: [
399+
usePersistedOperations({
400+
getPersistedOperation(key: string) {
401+
return store.get(key) || null;
402+
},
403+
extractPersistedOperationId(_params, request) {
404+
const url = new URL(request.url);
405+
return url.searchParams.get('id');
406+
},
407+
}),
408+
],
409+
schema,
410+
});
411+
const persistedOperationKey = 'my-persisted-operation';
412+
store.set(persistedOperationKey, '{__typename}');
413+
const response = await yoga.fetch(`http://yoga/graphql?id=${persistedOperationKey}`);
414+
415+
const body = await response.json();
416+
expect(body.errors).toBeUndefined();
417+
expect(body.data.__typename).toBe('Query');
418+
});
419+
420+
it('extract key from request header', async () => {
421+
const store = new Map<string, string>();
422+
const yoga = createYoga({
423+
plugins: [
424+
usePersistedOperations({
425+
getPersistedOperation(key: string) {
426+
return store.get(key) || null;
427+
},
428+
extractPersistedOperationId(_params, request) {
429+
return request.headers.get('x-document-id');
430+
},
431+
}),
432+
],
433+
schema,
434+
});
435+
const persistedOperationKey = 'my-persisted-operation';
436+
store.set(persistedOperationKey, '{__typename}');
437+
const response = await yoga.fetch(`http://yoga/graphql`, {
438+
headers: {
439+
'x-document-id': persistedOperationKey,
440+
},
441+
});
442+
443+
const body = await response.json();
444+
expect(body.errors).toBeUndefined();
445+
expect(body.data.__typename).toBe('Query');
446+
});
447+
448+
it('extract key from path', async () => {
449+
const store = new Map<string, string>();
450+
const yoga = createYoga({
451+
graphqlEndpoint: '/graphql/:document_id?',
452+
plugins: [
453+
usePersistedOperations({
454+
getPersistedOperation(key: string) {
455+
return store.get(key) || null;
456+
},
457+
extractPersistedOperationId(_params, request) {
458+
return request.url.split('/graphql/').pop() ?? null;
459+
},
460+
}),
461+
],
462+
schema,
463+
});
464+
const persistedOperationKey = 'my-persisted-operation';
465+
store.set(persistedOperationKey, '{__typename}');
466+
const response = await yoga.fetch(`http://yoga/graphql/${persistedOperationKey}`);
467+
468+
const body = await response.json();
469+
expect(body.errors).toBeUndefined();
470+
expect(body.data.__typename).toBe('Query');
471+
});
394472
});

packages/plugins/persisted-operations/src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ export interface GraphQLErrorOptions {
1818
extensions?: Maybe<GraphQLErrorExtensions>;
1919
}
2020

21-
export type ExtractPersistedOperationId = (params: GraphQLParams) => null | string;
21+
export type ExtractPersistedOperationId = (
22+
params: GraphQLParams,
23+
request: Request,
24+
) => null | string;
2225

2326
export const defaultExtractPersistedOperationId: ExtractPersistedOperationId = (
2427
params: GraphQLParams,
@@ -126,7 +129,7 @@ export function usePersistedOperations<
126129
return;
127130
}
128131

129-
const persistedOperationKey = extractPersistedOperationId(params);
132+
const persistedOperationKey = extractPersistedOperationId(params, request);
130133

131134
if (persistedOperationKey == null) {
132135
throw keyNotFoundErrorFactory(payload);

website/src/pages/docs/features/persisted-operations.mdx

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,132 @@ server.listen(4000, () => {
307307
})
308308
```
309309

310+
## Advanced persisted operation id Extraction from HTTP Request
311+
312+
You can extract the persisted operation id from the request using the `extractPersistedOperationId`
313+
314+
### Query Parameters Recipe
315+
316+
```ts filename="Extract persisted operation id from query parameters" {22-25}
317+
import { createServer } from 'node:http'
318+
import { createSchema, createYoga } from 'graphql-yoga'
319+
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'
320+
321+
const store = {
322+
ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38: '{__typename}'
323+
}
324+
325+
const yoga = createYoga({
326+
schema: createSchema({
327+
typeDefs: /* GraphQL */ `
328+
type Query {
329+
hello: String!
330+
}
331+
`
332+
}),
333+
plugins: [
334+
usePersistedOperations({
335+
getPersistedOperation(sha256Hash: string) {
336+
return store[sha256Hash]
337+
},
338+
extractPersistedOperationId(_params, request) {
339+
const url = new URL(request.url)
340+
return url.searchParams.get('id')
341+
}
342+
})
343+
]
344+
})
345+
346+
const server = createServer(yoga)
347+
server.listen(4000, () => {
348+
console.info('Server is running on http://localhost:4000/graphql')
349+
})
350+
```
351+
352+
### Header Recipe
353+
354+
You can also use the request headers to extract the persisted operation id.
355+
356+
```ts filename="Extract persisted operation id from headers" {22-24}
357+
import { createServer } from 'node:http'
358+
import { createSchema, createYoga } from 'graphql-yoga'
359+
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'
360+
361+
const store = {
362+
ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38: '{__typename}'
363+
}
364+
365+
const yoga = createYoga({
366+
schema: createSchema({
367+
typeDefs: /* GraphQL */ `
368+
type Query {
369+
hello: String!
370+
}
371+
`
372+
}),
373+
plugins: [
374+
usePersistedOperations({
375+
getPersistedOperation(sha256Hash: string) {
376+
return store[sha256Hash]
377+
},
378+
extractPersistedOperationId(_params, request) {
379+
return request.headers.get('x-document-id')
380+
}
381+
})
382+
]
383+
})
384+
385+
const server = createServer(yoga)
386+
server.listen(4000, () => {
387+
console.info('Server is running on http://localhost:4000/graphql')
388+
})
389+
```
390+
391+
### Path Recipe
392+
393+
You can also the the request path to extract the persisted operation id. This requires you to also
394+
customize the GraphQL endpoint. The underlying implementation for the URL matching is powered by the
395+
[URL Pattern API](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API).
396+
397+
This combination is powerful as it allows you to use the persisted operation id as it can easily be
398+
combined with any type of HTTP proxy cache.
399+
400+
```ts filename="Extract persisted operation id from path" {10,23-25}
401+
import { createServer } from 'node:http'
402+
import { createSchema, createYoga } from 'graphql-yoga'
403+
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'
404+
405+
const store = {
406+
ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38: '{__typename}'
407+
}
408+
409+
const yoga = createYoga({
410+
graphqlEndpoint: '/graphql/:document_id?',
411+
schema: createSchema({
412+
typeDefs: /* GraphQL */ `
413+
type Query {
414+
hello: String!
415+
}
416+
`
417+
}),
418+
plugins: [
419+
usePersistedOperations({
420+
getPersistedOperation(sha256Hash: string) {
421+
return store[sha256Hash]
422+
},
423+
extractPersistedOperationId(_params, request) {
424+
return request.url.split('/graphql/').pop() ?? null
425+
}
426+
})
427+
]
428+
})
429+
430+
const server = createServer(yoga)
431+
server.listen(4000, () => {
432+
console.info('Server is running on http://localhost:4000/graphql')
433+
})
434+
```
435+
310436
## Using an external Persisted Operation Store
311437

312438
As a project grows the amount of GraphQL Clients and GraphQL Operations can grow a lot. At some

0 commit comments

Comments
 (0)