Skip to content

Commit

Permalink
feat: allow accessing request within persisted operation id extraction (
Browse files Browse the repository at this point in the history
#3183)

* feat: allow accessing request within persisted operation id extraction

* chore: add changeset

* chore: types

* try fixing CI

---------

Co-authored-by: Valentin Cocaud <v.cocaud@gmail.com>
  • Loading branch information
n1ru4l and EmrysMyrddin authored Feb 23, 2024
1 parent 8dfbdcc commit 6725f8e
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 3 deletions.
6 changes: 6 additions & 0 deletions .changeset/long-tips-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-yoga/plugin-persisted-operations': minor
---

Inject request into `extractPersistedOperationId` function for allowing to extract the ID based on
request header, query parameters or request path.
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
if: ${{ github.event.pull_request.title != 'Upcoming Release Changes' }}
with:
npmTag: rc
restoreDeletedChangesets: true
restoreDeletedChangesets: false
buildScript: build
nodeVersion: 20
packageManager: pnpm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,4 +391,82 @@ describe('Persisted Operations', () => {
expect(body.errors).toBeUndefined();
expect(body.data.__typename).toBe('Query');
});

it('extract key from request query parameter', async () => {
const store = new Map<string, string>();
const yoga = createYoga({
plugins: [
usePersistedOperations({
getPersistedOperation(key: string) {
return store.get(key) || null;
},
extractPersistedOperationId(_params, request) {
const url = new URL(request.url);
return url.searchParams.get('id');
},
}),
],
schema,
});
const persistedOperationKey = 'my-persisted-operation';
store.set(persistedOperationKey, '{__typename}');
const response = await yoga.fetch(`http://yoga/graphql?id=${persistedOperationKey}`);

const body = await response.json();
expect(body.errors).toBeUndefined();
expect(body.data.__typename).toBe('Query');
});

it('extract key from request header', async () => {
const store = new Map<string, string>();
const yoga = createYoga({
plugins: [
usePersistedOperations({
getPersistedOperation(key: string) {
return store.get(key) || null;
},
extractPersistedOperationId(_params, request) {
return request.headers.get('x-document-id');
},
}),
],
schema,
});
const persistedOperationKey = 'my-persisted-operation';
store.set(persistedOperationKey, '{__typename}');
const response = await yoga.fetch(`http://yoga/graphql`, {
headers: {
'x-document-id': persistedOperationKey,
},
});

const body = await response.json();
expect(body.errors).toBeUndefined();
expect(body.data.__typename).toBe('Query');
});

it('extract key from path', async () => {
const store = new Map<string, string>();
const yoga = createYoga({
graphqlEndpoint: '/graphql/:document_id?',
plugins: [
usePersistedOperations({
getPersistedOperation(key: string) {
return store.get(key) || null;
},
extractPersistedOperationId(_params, request) {
return request.url.split('/graphql/').pop() ?? null;
},
}),
],
schema,
});
const persistedOperationKey = 'my-persisted-operation';
store.set(persistedOperationKey, '{__typename}');
const response = await yoga.fetch(`http://yoga/graphql/${persistedOperationKey}`);

const body = await response.json();
expect(body.errors).toBeUndefined();
expect(body.data.__typename).toBe('Query');
});
});
7 changes: 5 additions & 2 deletions packages/plugins/persisted-operations/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ export interface GraphQLErrorOptions {
extensions?: Maybe<GraphQLErrorExtensions>;
}

export type ExtractPersistedOperationId = (params: GraphQLParams) => null | string;
export type ExtractPersistedOperationId = (
params: GraphQLParams,
request: Request,
) => null | string;

export const defaultExtractPersistedOperationId: ExtractPersistedOperationId = (
params: GraphQLParams,
Expand Down Expand Up @@ -126,7 +129,7 @@ export function usePersistedOperations<
return;
}

const persistedOperationKey = extractPersistedOperationId(params);
const persistedOperationKey = extractPersistedOperationId(params, request);

if (persistedOperationKey == null) {
throw keyNotFoundErrorFactory(payload);
Expand Down
126 changes: 126 additions & 0 deletions website/src/pages/docs/features/persisted-operations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,132 @@ server.listen(4000, () => {
})
```

## Advanced persisted operation id Extraction from HTTP Request

You can extract the persisted operation id from the request using the `extractPersistedOperationId`

### Query Parameters Recipe

```ts filename="Extract persisted operation id from query parameters" {22-25}
import { createServer } from 'node:http'
import { createSchema, createYoga } from 'graphql-yoga'
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'

const store = {
ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38: '{__typename}'
}

const yoga = createYoga({
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
hello: String!
}
`
}),
plugins: [
usePersistedOperations({
getPersistedOperation(sha256Hash: string) {
return store[sha256Hash]
},
extractPersistedOperationId(_params, request) {
const url = new URL(request.url)
return url.searchParams.get('id')
}
})
]
})

const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql')
})
```

### Header Recipe

You can also use the request headers to extract the persisted operation id.

```ts filename="Extract persisted operation id from headers" {22-24}
import { createServer } from 'node:http'
import { createSchema, createYoga } from 'graphql-yoga'
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'

const store = {
ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38: '{__typename}'
}

const yoga = createYoga({
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
hello: String!
}
`
}),
plugins: [
usePersistedOperations({
getPersistedOperation(sha256Hash: string) {
return store[sha256Hash]
},
extractPersistedOperationId(_params, request) {
return request.headers.get('x-document-id')
}
})
]
})

const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql')
})
```

### Path Recipe

You can also the the request path to extract the persisted operation id. This requires you to also
customize the GraphQL endpoint. The underlying implementation for the URL matching is powered by the
[URL Pattern API](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API).

This combination is powerful as it allows you to use the persisted operation id as it can easily be
combined with any type of HTTP proxy cache.

```ts filename="Extract persisted operation id from path" {10,23-25}
import { createServer } from 'node:http'
import { createSchema, createYoga } from 'graphql-yoga'
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'

const store = {
ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38: '{__typename}'
}

const yoga = createYoga({
graphqlEndpoint: '/graphql/:document_id?',
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
hello: String!
}
`
}),
plugins: [
usePersistedOperations({
getPersistedOperation(sha256Hash: string) {
return store[sha256Hash]
},
extractPersistedOperationId(_params, request) {
return request.url.split('/graphql/').pop() ?? null
}
})
]
})

const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql')
})
```

## Using an external Persisted Operation Store

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

0 comments on commit 6725f8e

Please sign in to comment.