Persisted Operations
Persisted operations is a mechanism for preventing the execution of arbitrary GraphQL operation documents. By default, the persisted operations plugin follows the the APQ Specification for SENDING hashes to the server.
However, you can customize the protocol to comply to other implementations e.g. used by Relay persisted queries.
change this behavior by overriding the getPersistedOperationKey
option to support Relay's specification for example.
Quick Start
Persisted operations requires installing a separate package.
yarn add @graphql-yoga/plugin-persisted-operations
import { createYoga, createSchema } from 'graphql-yoga'
import { createServer } from 'node:http'
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]
}
})
]
})
const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql')
})
Start your yoga server and send the following request.
curl -X POST -H 'Content-Type: application/json' http://localhost:4000/graphql \
-d '{"extensions":{"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}}'
{"data":{"__typename":"Query"}}
As you can see, the persisted operations plugin is able to execute the operation without the need to send the full operation document.
If you now sent a normal GraphQL operation that is not within the store, it will be rejected.
curl -X POST -H 'Content-Type: application/json' http://localhost:4000/graphql \
-d '{"query": "{__typename}"}'
{"errors":[{"message":"PersistedQueryOnly"}]}
Extracting client operations
You can use GraphQL Code Generator with the graphql-codegen-persisted-query-ids
plugin for extracting a map of persisted query ids and their corresponding GraphQL documents from your application/client-code in a JSON file.
{
"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38": "{__typename}",
"c7a30a69b731d1af42a4ba02f2fa7a5771b6c44dcafb7c3e5fa4232c012bf5e7": "mutation {__typename}"
}
This map can then be used to persist the GraphQL documents in the server.
import { createYoga, createSchema } from 'graphql-yoga'
import { createServer } from 'node:http'
import { readFileSync } from 'node:fs'
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'
const persistedOperations = JSON.parse(
readFileSync('./persistedOperations.json', 'utf-8')
)
const yoga = createYoga({
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
hello: String!
}
`
}),
plugins: [
usePersistedOperations({
getPersistedOperation(key: string) {
return persistedOperations[key]
}
})
]
})
const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql')
})
Sending the hash from the client
The persisted operations plugin follows the the APQ Specification of Apollo for SENDING hashes to the server.
GraphQL clients such Apollo Client
and Urql
support that out of the box.
Check the corresponding documentation for more information.
Allowing arbitrary GraphQL operations
Sometimes it is handy to allow non-persisted operations aside from the persisted ones. E.g. you want to allow developers to execute arbitrary GraphQL operations on your production server.
This can be achieved using the allowArbitraryOperations
option.
const plugin = usePersistedOperations({
allowArbitraryOperations: (request) =>
request.headers.request.headers.get('x-allow-arbitrary-operations') ===
'true'
})
Use this option with caution!
Using Relay's Persisted Queries Specification
If you are using Relay's Persisted Queries specification, you can configure the plugin like below;
import { createYoga, createSchema } from 'graphql-yoga'
import { createServer } from 'node:http'
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({
extractPersistedOperationId(params: Record<string, unknown>) {
if (typeof payload.doc_id === 'string') {
return payload.doc_id
}
return null
}
getPersistedOperation(key: string) {
return store[key]
},
}),
],
})
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 point it might become impractible to store all persisted operations in memory.
In such a scenario you can use an external persisted operation store.
You can return a Promise
from the getPersistedOperation
function and call any database or external service to retrieve the persisted operation.
For the best performance a mixture of an LRU in-memory store and external persisted operation store is recommended.
import { createYoga } from 'graphql-yoga'
import { createServer } from 'node:http'
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'
const yoga = createYoga({
plugins: [
usePersistedOperations({
async getPersistedOperation(key: string) {
return await fetch(`https://localhost:9999/document/${key}`).then(
(res) => res.json()
)
}
})
]
})
const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql')
})