{"id":20253632,"url":"https://github.com/graphile/persisted-operations","last_synced_at":"2025-04-10T23:43:24.657Z","repository":{"id":45984597,"uuid":"298524935","full_name":"graphile/persisted-operations","owner":"graphile","description":"Persisted operations (aka \"persisted queries\") support for PostGraphile","archived":false,"fork":false,"pushed_at":"2024-09-20T11:40:14.000Z","size":139,"stargazers_count":42,"open_issues_count":3,"forks_count":4,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-10T23:43:18.849Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/graphile.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-09-25T09:15:22.000Z","updated_at":"2025-02-28T08:05:05.000Z","dependencies_parsed_at":"2024-01-18T09:48:41.080Z","dependency_job_id":"1ead6478-356e-4093-a96f-0751a3e6b1a5","html_url":"https://github.com/graphile/persisted-operations","commit_stats":{"total_commits":21,"total_committers":5,"mean_commits":4.2,"dds":"0.19047619047619047","last_synced_commit":"855d3fbce533d1d284ad25e87c5ac43d551a376c"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphile%2Fpersisted-operations","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphile%2Fpersisted-operations/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphile%2Fpersisted-operations/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphile%2Fpersisted-operations/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/graphile","download_url":"https://codeload.github.com/graphile/persisted-operations/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248317726,"owners_count":21083527,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-14T10:26:27.532Z","updated_at":"2025-04-10T23:43:24.618Z","avatar_url":"https://github.com/graphile.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @graphile/persisted-operations\n\nPersisted operations (aka \"persisted queries\", \"query allowlist\") support for\nPostGraphile's GraphQL server. Applies to both standard POST requests and to\nwebsocket connections. Works for all operation types (queries, mutations and\nsubscriptions).\n\nSee\n[Production Considerations](https://www.graphile.org/postgraphile/production/)\nfor why you might want this.\n\nWe recommend that all GraphQL servers (PostGraphile or otherwise) that only\nintend first party clients to use the GraphQL schema should use persisted\noperations to mitigate attacks against the GraphQL API and to help track the\nfields that have been used. This package is our solution for PostGraphile's\nserver, for other servers you will need different software.\n\n\u003c!-- SPONSORS_BEGIN --\u003e\n\n## Crowd-funded open-source software\n\nTo help us develop this software sustainably, we ask all individuals and\nbusinesses that use it to help support its ongoing maintenance and development\nvia sponsorship.\n\n### [Click here to find out more about sponsors and sponsorship.](https://www.graphile.org/sponsor/)\n\nAnd please give some love to our featured sponsors 🤩:\n\n\u003ctable\u003e\u003ctr\u003e\n\u003ctd align=\"center\"\u003e\u003ca href=\"https://www.the-guild.dev/\"\u003e\u003cimg src=\"https://graphile.org/images/sponsors/theguild.png\" width=\"90\" height=\"90\" alt=\"The Guild\" /\u003e\u003cbr /\u003eThe Guild\u003c/a\u003e *\u003c/td\u003e\n\u003ctd align=\"center\"\u003e\u003ca href=\"https://dovetailapp.com/\"\u003e\u003cimg src=\"https://graphile.org/images/sponsors/dovetail.png\" width=\"90\" height=\"90\" alt=\"Dovetail\" /\u003e\u003cbr /\u003eDovetail\u003c/a\u003e *\u003c/td\u003e\n\u003ctd align=\"center\"\u003e\u003ca href=\"https://stellate.co/\"\u003e\u003cimg src=\"https://graphile.org/images/sponsors/Stellate.png\" width=\"90\" height=\"90\" alt=\"Stellate\" /\u003e\u003cbr /\u003eStellate\u003c/a\u003e *\u003c/td\u003e\n\u003ctd align=\"center\"\u003e\u003ca href=\"https://gosteelhead.com/\"\u003e\u003cimg src=\"https://graphile.org/images/sponsors/steelhead.svg\" width=\"90\" height=\"90\" alt=\"Steelhead\" /\u003e\u003cbr /\u003eSteelhead\u003c/a\u003e *\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\u003ctd align=\"center\"\u003e\u003ca href=\"\"\u003e\u003cimg src=\"https://graphile.org/images/sponsors/latchbio.jpg\" width=\"90\" height=\"90\" alt=\"LatchBio\" /\u003e\u003cbr /\u003eLatchBio\u003c/a\u003e *\u003c/td\u003e\n\u003c/tr\u003e\u003c/table\u003e\n\n\u003cem\u003e\\* Sponsors the entire Graphile suite\u003c/em\u003e\n\n\u003c!-- SPONSORS_END --\u003e\n\n## Usage\n\n_See: [Server Plugins](https://www.graphile.org/postgraphile/plugins/)._\n\nPostGraphile CLI:\n\n```\npostgraphile \\\n  --plugins @graphile/persisted-operations \\\n  --persisted-operations-directory path/to/folder/ \\\n  ...\n```\n\nPostGraphile library:\n\n```\nimport { postgraphile, makePluginHook } from \"postgraphile\";\nimport PersistedOperationsPlugin from \"@graphile/persisted-operations\";\n\nconst pluginHook = makePluginHook([PersistedOperationsPlugin]);\n\nconst postGraphileMiddleware = postgraphile(databaseUrl, \"app_public\", {\n  pluginHook,\n  persistedOperationsDirectory: `${__dirname}/.persisted_operations/`,\n});\n```\n\n### Library Options\n\nThis plugin adds the following options to the PostGraphile library options:\n\n````ts\n/**\n * This function will be passed a GraphQL request object (normally `{query:\n * string, variables?: any, operationName?: string, extensions?: any}`, but\n * in the case of persisted operations it likely won't have a `query`\n * property), and must extract the hash to use to identify the persisted\n * operation. For Apollo Client, this might be something like:\n * `request?.extensions?.persistedQuery?.sha256Hash`; for Relay something\n * like: `request?.documentId`.\n */\nhashFromPayload?(request: any): string;\n\n/**\n * We can read persisted operations from a folder (they must be named\n * `\u003chash\u003e.graphql`); this is mostly used for PostGraphile CLI. When used\n * in this way, the first request for a hash will read the file\n * synchronously, and then the result will be cached such that the\n * **synchronous filesystem read** will only impact the first use of that\n * hash. We periodically scan the folder for new files, requests for hashes\n * that were not present in our last scan of the folder will be rejected to\n * mitigate denial of service attacks asking for non-existent hashes.\n */\npersistedOperationsDirectory?: string;\n\n/**\n * An optional string-string key-value object defining the persisted\n * operations, where the keys are the hashes, and the values are the\n * operation document strings to use.\n */\npersistedOperations?: { [hash: string]: string };\n\n/**\n * If your known persisted operations may change over time, or you'd rather\n * load them on demand, you may supply this function. Note this function is\n * both **synchronous** and **performance critical** so you should use\n * caching to improve performance of any follow-up requests for the same\n * hash. This function is not suitable for fetching operations from remote\n * stores (e.g. S3).\n */\npersistedOperationsGetter?: PersistedOperationGetter;\n\n/**\n * There are situations where you may want to allow arbitrary operations\n * (for example using GraphiQL in development, or allowing an admin to\n * make arbitrary requests in production) whilst enforcing Persisted\n * Operations for the application and non-admin users. This function\n * allows you to determine under which circumstances persisted operations\n * may be bypassed.\n *\n * IMPORTANT: this function must not throw!\n *\n * @example\n *\n * ```\n * app.use(postgraphile(DATABASE_URL, SCHEMAS, {\n *   allowUnpersistedOperation(req) {\n *     return process.env.NODE_ENV === \"development\" \u0026\u0026 req.headers.referer?.endsWith(\"/graphiql\");\n *   }\n * });\n * ```\n */\n allowUnpersistedOperation?: boolean | ((request: IncomingMessage, payload: RequestPayload): boolean);\n````\n\nAll these options are optional; but you should specify exactly one of\n`persistedOperationsDirectory`, `persistedOperations` or\n`persistedOperationsGetter` for this plugin to be useful.\n\n## Generating Persisted Operations\n\n### Relay\n\nRelay has built-in support for persisted operations:\n\nhttps://relay.dev/docs/guides/persisted-queries/\n\nPass `--persist-output ./path/to/server.json` to `relay-compiler` along with\nyour normal options to have it generate the persisted operations file. You can\nthen use the `addToPersistedOperations.js` script below to split this operations\nJSON file into one file per query that can be passed to\n`--persisted-operations-directory`.\n\nThen in your network config (`fetchQuery` function) you need to change the\n`query: operation.text` to `documentId: operation.id`\n[as shown in the relay docs](https://relay.dev/docs/guides/persisted-queries/#network-layer-changes).\n\n### GraphQL-Code-Generator\n\nFor everything except Relay, we recommend that you generate persisted operations\nwhen you build your clients using the awesome\n[graphql-code-generator](https://github.com/dotansimha/graphql-code-generator)\nproject and the\n[graphql-codegen-persisted-query-ids](https://www.npmjs.com/package/graphql-codegen-persisted-query-ids)\n(tested with v0.1.2) plugin. A config might look like:\n\n```yaml\nschema: \"schema.graphql\"\ndocuments: \"src/**/*.graphql\"\nhooks:\n  afterAllFileWrite:\n    - node addToPersistedOperations.js\ngenerates:\n  client.json:\n    plugins:\n      - graphql-codegen-persisted-query-ids:\n          output: client\n          algorithm: sha256\n  server.json:\n    plugins:\n      - graphql-codegen-persisted-query-ids:\n          output: server\n          algorithm: sha256\n```\n\nWe also recommend using the file `addToPersistedOperations.js` to write the\n`server.json` file contents out to separate GraphQL files every time the code is\nbuilt for easier version control:\n\n```js\n// addToPersistedOperations.js\nconst map = require(\"./server.json\");\nconst { promises: fsp } = require(\"fs\");\n\nasync function main() {\n  await Promise.all(\n    Object.entries(map).map(([hash, query]) =\u003e\n      fsp.writeFile(`${__dirname}/.persisted_operations/${hash}.graphql`, query)\n    )\n  );\n}\n\nmain().catch((e) =\u003e {\n  console.error(e);\n  process.exit(1);\n});\n```\n\nThen you pass the `.persisted_operations` folder to PostGraphile:\n\n```\npostgraphile \\\n  --plugins @graphile/persisted-operations \\\n  --persisted-operations-directory $(pwd)/.persisted_operations/ \\\n  -c mydb\n```\n\n#### Apollo Client\n\nYou can configure Apollo Client to send the persisted operations generated by\n`graphql-codegen` with `@apollo/client/link/persisted-queries`:\n\n```ts\nimport { createPersistedQueryLink } from \"@apollo/client/link/persisted-queries\";\nimport { usePregeneratedHashes as withPregeneratedHashes } from \"graphql-codegen-persisted-query-ids/lib/apollo\";\nimport hashes from \"./path/to/client.json\";\n\nconst persistedLink = createPersistedQueryLink({\n  useGETForHashedQueries: false,\n  generateHash: withPregeneratedHashes(hashes),\n  disable: () =\u003e false,\n});\n\n// ...\n\nconst client = new ApolloClient({\n  link: ApolloLink.from([persistedLink, httpLink]),\n  // ...\n});\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgraphile%2Fpersisted-operations","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgraphile%2Fpersisted-operations","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgraphile%2Fpersisted-operations/lists"}