• v3
  • Features
  • Persisted Operations

Persisted operations is a mechanism for preventing the execution of arbitary GraphQL operation documents. By default, the persisted operations plugin follows the the APQ Specification of Apollo for SENDING hashes to the server. However, you can change this behavior by overriding the getPersistedOperationKey option to support Relay's specification for example.

Installation

yarn add @graphql-yoga/persisted-operations

Quick Start

import { createYoga } from 'graphql-yoga'
import { createServer } from 'node:http'
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'
 
const store = {
  ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38:
    '{__typename}',
}
 
const yoga = createYoga({
  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"}}}'

Then afterwards we can send the same payload againl, but this time omit the query field.

Especially for big GraphQL document strings, the subsequent payload can be much smaller.

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 client-code in a JSON file.

Example Map

{
  "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38": "{__typename}",
  "c7a30a69b731d1af42a4ba02f2fa7a5771b6c44dcafb7c3e5fa4232c012bf5e7": "mutation {__typename}"
}

This map can then be used to persist the GraphQL documents in the server.

import { createYoga } from 'graphql-yoga'
import { createServer } from 'node:http'
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'
import persistedOperations from './persistedOperations.json'
 
const yoga = createYoga({
  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 doucmentation 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.

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 } from 'graphql-yoga'
import { createServer } from 'node:http'
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'
import persistedOperations from './persistedOperations.json'
 
const yoga = createYoga({
  plugins: [
    usePersistedOperations({
      getPersistedOperationKey(params: GraphQLParams & { doc_id: string }) {
        return params.doc_id
      }
      getPersistedOperation(key: string) {
        return persistedOperations[key]
      },
    }),
  ],
})
 
const server = createServer(yoga)
server.listen(4000, () => {
    console.info('Server is running on http://localhost:4000/graphql')
})
Last updated on September 27, 2022