{"id":20253566,"url":"https://github.com/graphile/operation-hooks","last_synced_at":"2026-02-21T20:32:35.230Z","repository":{"id":38359512,"uuid":"159158712","full_name":"graphile/operation-hooks","owner":"graphile","description":"Register asynchronous callbacks before/after operations; uses include: validation, authorization, notification","archived":false,"fork":false,"pushed_at":"2024-09-17T10:33:27.000Z","size":849,"stargazers_count":43,"open_issues_count":2,"forks_count":2,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-10-22T13:37:04.943Z","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":null,"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":null,"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":"2018-11-26T11:28:45.000Z","updated_at":"2024-09-17T10:33:30.000Z","dependencies_parsed_at":"2023-02-08T18:16:03.362Z","dependency_job_id":"817c7330-eb28-48c2-8c4b-fe065da04d57","html_url":"https://github.com/graphile/operation-hooks","commit_stats":{"total_commits":98,"total_committers":2,"mean_commits":49.0,"dds":"0.13265306122448983","last_synced_commit":"1018b3229ec87aca038ee8fd5d85867be6c2e4f6"},"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/graphile/operation-hooks","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphile%2Foperation-hooks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphile%2Foperation-hooks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphile%2Foperation-hooks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphile%2Foperation-hooks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/graphile","download_url":"https://codeload.github.com/graphile/operation-hooks/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphile%2Foperation-hooks/sbom","scorecard":{"id":443793,"data":{"date":"2025-08-11","repo":{"name":"github.com/graphile/operation-hooks","commit":"f938848e3ceda28450e6a66ae7c3ce250072a330"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.4,"checks":[{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Code-Review","score":0,"reason":"Found 0/11 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/ci.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:37: update your workflow using https://app.stepsecurity.io/secureworkflow/graphile/operation-hooks/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:39: update your workflow using https://app.stepsecurity.io/secureworkflow/graphile/operation-hooks/ci.yml/main?enable=pin","Info:   0 out of   2 GitHub-owned GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"License","score":0,"reason":"license file not detected","details":["Warn: project does not have a license file"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Security-Policy","score":4,"reason":"security policy file detected","details":["Info: security policy file detected: github.com/graphile/.github/SECURITY.md:1","Warn: no linked content found","Info: Found disclosure, vulnerability, and/or timelines in security policy: github.com/graphile/.github/SECURITY.md:1","Info: Found text in security policy: github.com/graphile/.github/SECURITY.md:1"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 24 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":0,"reason":"18 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-93q8-gq69-wqmw","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-gxpj-cx7g-858c","Warn: Project is vulnerable to: GHSA-w573-4hg7-7wgq","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-896r-f27r-55mw","Warn: Project is vulnerable to: GHSA-8cf7-32gw-wr33","Warn: Project is vulnerable to: GHSA-hjrf-2m68-5959","Warn: Project is vulnerable to: GHSA-qwph-4952-7xr6","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3","Warn: Project is vulnerable to: GHSA-5fw9-fq32-wv5p","Warn: Project is vulnerable to: GHSA-p8p7-x288-28g6","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw","Warn: Project is vulnerable to: GHSA-72xf-g2v4-qvf3"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-19T06:12:29.853Z","repository_id":38359512,"created_at":"2025-08-19T06:12:29.853Z","updated_at":"2025-08-19T06:12:29.853Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29692521,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-21T18:18:25.093Z","status":"ssl_error","status_checked_at":"2026-02-21T18:18:22.435Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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:25:45.234Z","updated_at":"2026-02-21T20:32:35.214Z","avatar_url":"https://github.com/graphile.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Operation Hooks\n\nThis is a PostGraphile server plugin which encompasses a collection of Graphile\nEngine plugins enabling you to register asynchronous callbacks\nbefore/during/after operations; uses include:\n\n- validation - check that the incoming arguments are valid\n- authorization - check that the user is permitted to take that action\n- error - aborting the action for some reason (e.g. insufficient funds)\n- notification - inform the user of hints, validation errors, warnings, success,\n  and relevant meta-information (e.g. remaining balance)\n- mutation pre-flight - do the preliminary checks of mutation (and throw any\n  errors they may raise) without actually doing the mutation\n\nThe callbacks only affect root fields (e.g. fields on the Query, Mutation and\nSubscription types) and can:\n\n- exit early (with or without an error) - preventing the operation being\n  executed\n- augment the result of the operation (typically in order to add additional\n  information)\n- accumulate metadata from before/after the operation\n- accumulate metadata during the operation (mutations only; e.g. via trigger\n  functions)\n- augment error objects with said metadata\n\n## Usage:\n\nPostGraphile CLI:\n\n```bash\npostgraphile \\\n  --plugins @graphile/operation-hooks \\\n  --operation-messages \\\n  --operation-messages-preflight\n```\n\n(`--operation-messages` exposes generated messages on mutation payloads and\nGraphQL error objects; `--operation-messages-preflight` adds a preflight option\nto mutations which allows the pre-mutation checks to run (and messages to be\ngenerates) but does not actually perform the mutation.)\n\nPostGraphile library:\n\n```js\nconst { postgraphile, makePluginHook } = require(\"postgraphile\");\n\n// This is how we load server plugins into PostGraphile\n// See: https://www.graphile.org/postgraphile/plugins/\nconst pluginHook = makePluginHook([\n  require(\"@graphile/operation-hooks\").default,\n  // Any more PostGraphile server plugins here\n]);\n\nconst postGraphileMiddleware = postgraphile(DATABASE_URL, SCHEMA_NAME, {\n  pluginHook,\n  operationMessages: true,\n  operationMessagesPreflight: true,\n  appendPlugins: [\n    // Add your JS hooks here, e.g.\n    // require('./path/to/my_hook.js')\n  ],\n});\n\n// This example uses `http` but you can use Express, Koa, etc.\nrequire(\"http\").createServer(postGraphileMiddleware).listen(5000);\n\n/*\nconst app = express();\nexpress.use(postGraphileMiddleware);\nexpress.listen(5000);\n*/\n```\n\nIf you want to just use the Graphile Engine plugins without the PostGraphile\nCLI/library integration that's possible too:\n\n```js\nconst { createPostGraphileSchema } = require(\"postgraphile\");\nconst { OperationHooksPlugin } = require(\"@graphile/operation-hooks\");\n\nconst schema = createPostGraphileSchema(DATABASE_URL, SCHEMA_NAME, {\n  appendPlugins: [OperationHooksPlugin],\n});\n```\n\n## Messages (notifications)\n\nThe messages plugin gives you the ability to associate messages with an\noperation. Each message has at least a `level` and `message` field (both are\nstrings).\n\nImagine you have the following GraphQL mutation:\n\n```graphql\ninput SendEmailInput {\n  email: String!\n  subject: String\n  body: String\n}\nextend type Mutation {\n  sendEmail(input: SendEmailInput!): SendEmailPayload\n}\n```\n\nThere's a number of messages you might be interested in sending:\n\n- Validation errors (abort) or warnings (hint, but don't abort):\n  - [B] level: 'error', message: 'Invalid email address - must contain at least\n    one @ symbol', path: ['input', 'email']\n  - [B] level: 'warning', message: 'Missing subject', path: ['input', 'subject']\n  - [E] level: 'error', message: 'The domain for this email is unreachable',\n    path: ['input', 'email']\n- Authorization issues:\n  - [B] level: 'error', message: 'You must be on a paid plan to send emails'\n  - [B] level: 'error', message: 'You are not permitted to email this address',\n    path: ['input', 'email']\n- Business requirements:\n  - [B] level: 'error', message: 'Insufficient credits to send email',\n    remaining_credits: 2, required_credits: 7\n  - [A] level: 'warn', message: 'Your credit is very low', remaining_credits: 9,\n    required_credits: 7\n- Notices:\n  - [E] level: 'error', message: 'Email sending is not available at this time,\n    please try again later'\n  - [B] level: 'notice', message: 'Emails are currently subject to a 3 minute\n    delay due to abuse circumvention; normal service should resume shortly'\n  - [A] level: 'notice', message: 'Email sent, remaining credits: 177',\n    remaining_credits: 177\n  - [A] level: 'notice', message: 'You have 2 unsent emails in your outbox,\n    please review them'\n\nYou'll notice that every message has a `level` string and `message` string, many\nalso have a `path` string array. All messages can optionally define additional\narbitrary keys. I've also tagged each one `[B]` for \"before\" (i.e. this message\nwould be generated before the mutation takes place), `[A]` for \"after\" (i.e.\nthis message would be generated during or after the mutation), and `[E]` for\n\"error\" (i.e. this message may be generated if an error occurred during the\nmutation itself). Note that the `[A]` (after) messages might also be triggered\n_during_ the mutation, rather than afterwards; more on this below.\n\nThe `level` key is treated specially; if any message generated before the\nmutation takes place produces a message with `level='error'` then the mutation\nwill be aborted with an error. The value in doing this with these messages is\nthat more than one error (along with associated warnings, notices, etc.) can be\nraised at the same time, allowing the user to fix multiple issues at once,\nresulting in greater user satisfaction.\n\nMessages are accumulated from all the operation hooks that have been added to\nthe current mutation. One hook producing a message with level=error will not\nprevent further hooks from being called (however you can prevent other hooks\nfrom being called by literally throwing an error).\n\n### Exposing messages\n\nShould you wish to surface notifications via GraphQL (rather than just using the\nbefore/after hooks to cause side effects, or possibly raise 'error' messages),\nyou may use the CLI flag `--operation-messages` or library config\n`operationMessages: true`. Doing so will extend the mutation payloads in your\nGraphQL schema with a `messages` entry, a list of the messages raised, and will\nalso expose relevant messages on any thrown GraphQL errors.\n\nWe will define an `OperationMessageInterface` interface that all messages must\nconform to:\n\n```graphql\ninterface OperationMessageInterface {\n  level: String!\n  message: String!\n  path: [String!]\n}\n```\n\nAnd extend all mutation payloads to expose them:\n\n```graphql\nextend type *MutationPayload {\n  messages: [OperationMessageInterface!]\n}\n```\n\nYou can then define whatever concrete message subtypes you need to be returned.\nA message type must specify at least the 3 fields defined in the interface:\n\n- `level` (required, string)\n  - e.g. `error`, `warning`, `notification`, `info`, `debug`, ...\n  - helps client figure out how to use the message\n  - `error` is special - it will abort the transaction on the server (all others\n    are just passed to client)\n- `message` (required, string)\n  - e.g. `Invalid email`\n  - a human-readable error message; fallback for if the client does not\n    understand the rest of the payload\n- `path` (optional, string array)\n  - e.g. `['input', 'userPatch', 'firstName']`\n  - application developer may find other uses for this, so no further validation\n    will be done\n  - typically denotes the path to the field that caused the error\n\n⚠️ Please note that messages added to errors do NOT conform to the GraphQL\ndefinitions, so be careful to not expose more information than you intend!\n\n## SQL NOTICEs\n\nThis is the easiest way to add messages during a mutation; you just need to\n`RAISE NOTICE` in one of the functions related with your mutation. This could be\nyour [custom mutation](https://www.graphile.org/postgraphile/custom-mutations/)\nfunction itself, or it could be a trigger function called by one of the rows\nyou're manipulating.\n\nImportantly, the NOTICE must use the error code `OPMSG`. It may optionally\ndefine `detail` which is treated as a JSON value and is merged into the message\nobject; if no `level` key is included as part of `detail` then the level will\ndefault to `info`.\n\nMinimal example:\n\n```sql\nRAISE NOTICE 'Your credits are running low.' USING ERRCODE = 'OPMSG';\n```\n\nFuller example:\n\n```sql\nRAISE NOTICE\n  '2 + 2 is %, minus 1 that''s %; quick maths.',\n  (2 + 2),\n  (2 + 2 - 1)\nUSING\n  ERRCODE = 'OPMSG',\n  DETAIL = json_build_object(\n    'level', 'info',\n    'path', array_to_json(ARRAY['noticePath']),\n    'anything_else', 'can_go_here'\n  )::text;\n```\n\nSee the PostgreSQL\n[RAISE](https://www.postgresql.org/docs/current/plpgsql-errors-and-messages.html#PLPGSQL-STATEMENTS-RAISE)\ndocumentation for more information.\n\n## SQL hooks\n\nAdding this schema plugin to a PostGraphile server will give you the ability to\ndefine mutation operation hooks via PostgreSQL functions. These hooks only apply\nto the built in CRUD mutations, for custom mutations or schema extensions you\nshould implement the logic within the mutation (or use the JS hooks interface).\n\n### SQL function requirements\n\nTo be detected as a mutation operation hook, these PostgreSQL functions must\nconform to the following requirements:\n\n- Must be defined in an exposed schema (may be lifted in future)\n- Must be named according to the SQL Operation Callback Naming Convention (see\n  below)\n- Must accept the first 0, 1, 2 or 3 of the following arguments:\n  - `data` (JSON/JSONB) - the data the user submitted to be stored to the record\n    (INSERT: the create object, UPDATE: the patch object, DELETE: `null`)\n  - `tuple` (table type) - the current row in the database (because we expose a\n    lot of methods to mutate the same row)\n    - before insert: `null`\n    - after insert: the new row, using the primary key\n    - before update: the old row, using the unique constraint\n    - after update: the new row, using the primary key\n    - before delete: the old row, using the unique constraint\n    - after delete: `null`\n  - `op` (string) - the operation (`insert`, `update`, or `delete`) - useful if\n    you want to share the same function between multiple operations\n- Must return:\n  - `VOID`, or\n  - `SETOF mutation_message`, or\n  - `mutation_message[]`, or\n  - `TABLE(level text, message text, path text[], ...)`\n  - or can be defined using `OUT` parameters matching the `TABLE` entries above.\n- Must be either `VOLATILE` (default) or `STABLE` (note: should only be `STABLE`\n  if it does not return `VOID`)\n\nRecommendation: add an `@omit` smart comment to the function to have it excluded\nfrom the GraphQL schema.\n\n#### Example\n\nSQL schema:\n\n```sql\ncreate table users (\n  id serial primary key,\n  username text not null unique\n);\ncreate type mutation_message as (\n  level text,\n  message text,\n  path text[],\n  code text\n);\n\ncreate or replace function users_insert_before(input jsonb)\nreturns mutation_message[]\nas $$\nbegin\n  if lower(input -\u003e\u003e 'username') \u003c\u003e (input -\u003e\u003e 'username') then\n    return array[(\n      'error',\n      'Your username must be in lowercase',\n      null,\n      'E83245'\n    )::mutation_message];\n  else\n    return array[(\n      'info',\n      'Nice to meet you, ' || (input -\u003e\u003e 'username'),\n      null,\n      null\n    )::mutation_message];\n  end if;\nend;\n$$ language plpgsql stable;\n\ncomment on function users_insert_before(jsonb) is E'@omit';\n```\n\nGraphQL mutation:\n\n```graphql\nmutation {\n  createUser(input: { user: { username: \"alice\" } }) {\n    user {\n      username\n    }\n    messages {\n      level\n      message\n    }\n  }\n}\n```\n\nResult:\n\n```json\n{\n  \"data\": {\n    \"createUser\": {\n      \"user\": {\n        \"username\": \"alice\"\n      },\n      \"messages\": [\n        {\n          \"level\": \"info\",\n          \"message\": \"Nice to meet you, alice\"\n        }\n      ]\n    }\n  }\n}\n```\n\n### SQL operation callback naming convention\n\nBy default we use the following naming convention:\n\n- start with the GraphQL table name and an underscore (e.g. `users_`)\n- followed by the SQL operation name, lowercase (`insert`, `update` or `delete`)\n- followed by `_before` or `_after` to indicate when it runs\n\ne.g. `users_insert_before`\n\nYou can override this using the inflector `pgOperationHookFunctionName`:\n\n```js\nconst { makeAddInflectorsPlugin } = require(\"graphile-utils\");\n\nmodule.exports = makeAddInflectorsPlugin(\n  {\n    pgOperationHookFunctionName: (table, sqlOp, when, _fieldContext) =\u003e {\n      return `${table.name}_${sqlOp}_${when.toLowerCase()}`;\n    },\n  },\n  true\n);\n```\n\n## Implementing operation hooks in JavaScript\n\nYou can also implement hooks in JavaScript (the SQL hooks are actually\nimplemented using the JavaScript interface). To do so, you use the\n`addOperationHook` API introduced by this plugin. This allows you to write a\nsingle function that handles all root-level queries, mutations and\nsubscriptions; it's then your responsibility to filter this down to what you\nneed. (We'll probably make a helper for this in future!)\n\nYou can load your plugin with the standard `--append-plugins` (library:\n`appendPlugins`) option.\n\nWhat follows is an example plugin, you can see it in use in\n[this example repository](https://github.com/graphile/operation-hooks-example).\n\n```js\n// This plugin logs all attempts at `create` mutations before they're attempted.\n\nconst logCreateMutationsHookFromBuild = (build) =\u003e (fieldContext) =\u003e {\n  // This function is called for every top-level field registered with\n  // Graphile Engine. `fieldContext` is a Context object describing\n  // the field that's being hooked; it could be for a query (`isRootQuery`),\n  // a mutation (`isRootMutation`), or a subscription\n  // (`isRootSubscription`). Return `null` if we don't want to apply this\n  // hook to this `fieldContext`.\n  const {\n    scope: { isRootMutation, isPgCreateMutationField, pgFieldIntrospection },\n  } = fieldContext;\n\n  // If your hook should only apply to mutations you can do this:\n  if (!isRootMutation) return null;\n\n  // You can further limit the functions this hook applies to using\n  // `fieldContext`, e.g. `fieldContext.scope.fieldName` would allow you to\n  // cherry-pick an individual field, or\n  // `fieldContext.scope.isPgCreateMutationField` would tell you that this\n  // is a built in CRUD create mutation field:\n  // https://github.com/graphile/graphile-engine/blob/7d49f8eeb579d12683f1c0c6579d7b230a2a3008/packages/graphile-build-pg/src/plugins/PgMutationCreatePlugin.js#L253-L254\n  if (\n    !isPgCreateMutationField ||\n    !pgFieldIntrospection ||\n    pgFieldIntrospection.kind !== \"class\"\n  ) {\n    return null;\n  }\n\n  // By this point, we're applying the hook to all create mutations\n\n  // Defining the callback up front makes the code easier to read.\n  const tableName = pgFieldIntrospection.name;\n  const logAttempt = (input, args, context, resolveInfo) =\u003e {\n    console.log(\n      `A create was attempted on table ${tableName} by ${\n        context.jwtClaims \u0026\u0026 context.jwtClaims.user_id\n          ? `user with id ${context.jwtClaims.user_id}`\n          : \"an anonymous user\"\n      }`\n    );\n\n    // Our function must return either the input, a derivative of it, or\n    // `null`. If `null` is returned then the null will be returned (without\n    // an error) to the user.\n\n    // Since we wish to continue, we'll just return the input.\n    return input;\n  };\n\n  // Now we tell the hooks system to use it:\n  return {\n    // An optional list of callbacks to call before the operation\n    before: [\n      // You may register more than one callback if you wish, they will be mixed\n      // in with the callbacks registered from other plugins and called in the\n      // order specified by their priority value.\n      {\n        // Priority is a number between 0 and 1000; if you're not sure where to\n        // put it, then 500 is a great starting point.\n        priority: 500,\n        // This function (which can be asynchronous) will be called before the\n        // operation; it will be passed a value that it must return verbatim;\n        // the only other valid return is `null` in which case an error will be thrown.\n        callback: logAttempt,\n      },\n    ],\n\n    // As `before`, except the callback is called after the operation and will\n    // be passed the result of the operation; you may return a derivative of the\n    // result.\n    after: [],\n\n    // As `before`; except the callback is called if an error occurs; it will be\n    // passed the error and must return either the error or a derivative of it.\n    error: [],\n  };\n};\n\n// This exports a standard Graphile Engine plugin that adds the operation\n// hook.\nmodule.exports = function MyOperationHookPlugin(builder) {\n  builder.hook(\"init\", (_, build) =\u003e {\n    // Register our operation hook (passing it the build object):\n    build.addOperationHook(logCreateMutationsHookFromBuild(build));\n\n    // Graphile Engine hooks must always return their input or a derivative of\n    // it.\n    return _;\n  });\n};\n```\n\n## Caveats\n\nDon't try and use this for things like field masking since there's a lot of\ndifferent ways a user can access a field in GraphQL. Field masking should be\nsolved via `makeWrapResolversPlugin` or similar approach instead.\n\nThis is a young plugin, it will evolve over time.\n\nWe don't currently have a neat way for adding other types to the\nOperationMessageInterface, so if you really need to expose additional fields,\nyou can do it using a schema extension:\n\n```js\nconst { gql, makeExtendSchemaPlugin } = require(\"graphile-utils\");\n\nmodule.exports = makeExtendSchemaPlugin(() =\u003e ({\n  typedefs: gql`\n    extend type OperationMessage {\n      anotherField: String\n      yetAnotherField: Float\n    }\n  `,\n  resolvers: {},\n}));\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgraphile%2Foperation-hooks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgraphile%2Foperation-hooks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgraphile%2Foperation-hooks/lists"}