diff --git a/raw/hooks__context.md b/raw/_processed/hooks__context.md similarity index 100% rename from raw/hooks__context.md rename to raw/_processed/hooks__context.md diff --git a/wiki/_master-index.md b/wiki/_master-index.md index ed6c6fe..173951b 100644 --- a/wiki/_master-index.md +++ b/wiki/_master-index.md @@ -35,7 +35,7 @@ This 3-hop pattern works for hundreds of articles without vector search. | [[wiki/reports/_index\|reports/]] | Weekly and monthly summaries — generate: `uv run python scripts/report-generator.py --weekly` | 1 | | [[wiki/infrastructure/_index\|infrastructure/]] | Server inventory: all 10 SSH hosts — optical, optical-dev, optical-prod, baic, librechat, modocmms, box-cli, aimpress, pve | 12 | | [[wiki/testing/_index\|testing/]] | Web app testing: functional, performance, security, UI types; TDD/BDD/Agile methodologies; Selenium/Cypress/Playwright/JMeter/OWASP ZAP tools | 1 | -| [[wiki/payloadcms/_index\|payloadcms/]] | Full Payload CMS reference — getting started, config, database (Postgres/MongoDB/SQLite), all 22 field types, access control, hooks, authentication (cookies, JWT, API keys, custom strategies, token data), admin UI, custom components, Lexical rich text, live preview, versions/drafts, Local/REST/GraphQL APIs, queries, plugins, jobs queue, upload, ecommerce, production deploy, TypeScript, migration guides, i18n, localization, hierarchy | 93 | +| [[wiki/payloadcms/_index\|payloadcms/]] | Full Payload CMS reference — getting started, config, database (Postgres/MongoDB/SQLite), all 22 field types, access control, hooks, authentication (cookies, JWT, API keys, custom strategies, token data), admin UI, custom components, Lexical rich text, live preview, versions/drafts, Local/REST/GraphQL APIs, queries, plugins, jobs queue, upload, ecommerce, production deploy, TypeScript, migration guides, i18n, localization, hierarchy | 94 | | [[wiki/shared-patterns/_index\|shared-patterns/]] | Oliver Agency standard library patterns: httpx, structlog, pydantic-settings, alembic — reuse before writing from scratch | 4 | | [[wiki/mistakes/_index\|mistakes/]] | Anti-patterns extracted from sessions — per-stack running lists (fastapi, react, docker, postgres, general) — injected at session start | 5 | diff --git a/wiki/payloadcms/_index.md b/wiki/payloadcms/_index.md index 81af7ca..6d24a2c 100644 --- a/wiki/payloadcms/_index.md +++ b/wiki/payloadcms/_index.md @@ -91,6 +91,7 @@ | [[wiki/payloadcms/hooks-collections\|Collection Hooks]] | All 18 collection hook types (write/read/delete/auth lifecycle), `data` vs `originalDoc` gotcha, auth hooks, TypeScript generics | raw/hooks__collections.md | 2026-05-15 | | [[wiki/payloadcms/concepts-overview\|Core Concepts]] | Config, Collections, Globals, Fields, Hooks, Auth, Access Control, Admin Panel; Local/REST/GraphQL APIs; package structure | raw/getting-started__concepts.md | 2026-05-15 | | [[wiki/payloadcms/hooks-context\|Hooks — Context (req.context)]] | Share data across hooks without duplicate fetches; prevent infinite loop in afterChange with flag pattern; TypeScript module augmentation for RequestContext | raw/hooks__context.md | 2026-05-15 | +| [[wiki/payloadcms/hooks-fields\|Field Hooks]] | Per-field lifecycle hooks: beforeValidate, beforeChange, afterChange, afterRead, beforeDuplicate — args reference, patterns, TypeScript generics | raw/hooks__fields.md | 2026-05-15 | | [[wiki/payloadcms/installation\|Installation]] | Requirements (Node 24+, Next.js 16.2.6+), create-payload-app quickstart, manual install steps, withPayload ESM config, tsconfig path alias | raw/getting-started__installation.md | 2026-05-15 | | [[wiki/payloadcms/graphql-extending\|GraphQL — Custom Queries and Mutations]] | Add custom GraphQL ops via `graphQL.queries`/`graphQL.mutations` in buildConfig; resolver signature, `depth: 0` gotcha, `buildPaginatedListType`, collection graphQL types | raw/graphql__extending.md | 2026-05-15 | | [[wiki/payloadcms/graphql-schema\|GraphQL — Schema Generation]] | `payload-graphql generate:schema` CLI, PAYLOAD_CONFIG_PATH for non-root configs, `interfaceName` for reusable top-level GraphQL types | raw/graphql__graphql-schema.md | 2026-05-15 | diff --git a/wiki/payloadcms/hooks-fields.md b/wiki/payloadcms/hooks-fields.md new file mode 100644 index 0000000..c1ac5cf --- /dev/null +++ b/wiki/payloadcms/hooks-fields.md @@ -0,0 +1,164 @@ +--- +title: "Field Hooks" +aliases: [field-hooks, payload-field-hooks] +tags: [payloadcms, hooks, fields, lifecycle] +sources: [raw/hooks__fields.md] +created: 2026-05-15 +updated: 2026-05-15 +--- + +Field Hooks run on individual fields during Document lifecycle events — allowing isolated, per-field logic separate from [[wiki/payloadcms/hooks-collections|Collection Hooks]]. + +## Config + +Add to any field's `hooks` property: + +```ts +const field: Field = { + name: 'username', + type: 'text', + hooks: { + beforeValidate: [(args) => { ... }], + beforeChange: [(args) => { ... }], + beforeDuplicate:[(args) => { ... }], + afterChange: [(args) => { ... }], + afterRead: [(args) => { ... }], + }, +} +``` + +Each hook type accepts an **array** of sync or async functions. The return value replaces the field value for that operation. + +> **GraphQL warning:** Changing the *type* of data returned from a field breaks GraphQL. Use [[wiki/payloadcms/hooks-collections|Collection Hooks]] for type changes. + +## Hook Types + +| Hook | Runs | Use for | +|------|------|---------| +| `beforeValidate` | create + update, after client validation | Normalise/format input before server validation | +| `beforeChange` | create + update, before validation | Transform or gate data; data is unvalidated here | +| `afterChange` | create + update, after save | Side effects, logging, downstream triggers | +| `afterRead` | read operations | Format for output (dates, masks, derived values) | +| `beforeDuplicate` | document duplication | Avoid unique constraint violations; runs before `beforeValidate` | + +### Hook Execution Order (create/update) + +1. `beforeValidate` (client → server) +2. `validate` (server) +3. `beforeChange` +4. DB write +5. `afterChange` + +On read: `afterRead`. +On duplicate: `beforeDuplicate` → `beforeValidate` → `beforeChange`. + +## Arguments Reference + +All field hooks receive the same args object: + +| Arg | Type | Notes | +|-----|------|-------| +| `value` | any | Current field value | +| `data` | Partial\ | Incoming data (create/update) or full doc (afterRead) | +| `originalDoc` | Doc | Doc before changes (update); resulting doc (afterChange) | +| `previousDoc` | Doc | Doc before changes in `afterChange` | +| `previousValue` | any | Previous field value — `beforeChange` and `afterChange` only | +| `siblingData` | object | Adjacent field values | +| `siblingFields` | Field[] | Adjacent field definitions | +| `previousSiblingDoc` | object | Sibling data before changes — `beforeChange`/`afterChange` only | +| `siblingDocWithLocales` | object | Sibling data with all locales | +| `operation` | string | `'create'` \| `'update'` — useful for branching logic | +| `field` | Field | Field definition | +| `path` | string[] | Runtime path | +| `schemaPath` | string[] | Schema-level path | +| `collection` | Collection \| null | null if field belongs to a Global | +| `global` | Global \| null | null if field belongs to a Collection | +| `findMany` | boolean | `afterRead` only — differentiates find-one vs find-many | +| `overrideAccess` | boolean | Whether access control is bypassed | +| `context` | object | Shared context across hooks — see [[wiki/payloadcms/hooks-context|Hooks Context]] | +| `req` | Request | Web request (mocked for Local API) | + +## Common Patterns + +### Normalise input (beforeValidate) +```ts +beforeValidate: [ + ({ value }) => value?.trim().toLowerCase(), +], +``` + +### Branch on operation (beforeChange) +```ts +beforeChange: [ + ({ value, operation }) => { + if (operation === 'create') { /* extra logic */ } + return value + }, +], +``` + +### Log on change (afterChange) +```ts +afterChange: [ + ({ value, previousValue, req }) => { + if (value !== previousValue) { + console.log(`Changed: ${previousValue} → ${value}`) + } + }, +], +``` + +### Format for display (afterRead) +```ts +afterRead: [ + ({ value }) => new Date(value).toLocaleDateString(), +], +``` + +### Deduplicate on copy (beforeDuplicate) +```ts +beforeDuplicate: [ + ({ value }) => (value ?? 0) + 1, +], +``` +Default behaviour for text fields: Payload appends `" - Copy"` unless you define your own `beforeDuplicate` hook. + +## TypeScript + +```ts +import type { FieldHook } from 'payload' +import type { Post } from '@/payload-types' + +// Three generics: +type PostTitleHook = FieldHook + +const slugifyTitle: PostTitleHook = ({ value, siblingData }) => { + if (!siblingData.slug && value) { + return value.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-') + } + return value +} +``` + +Use all three generics for full autocomplete on `value`, `data`, `siblingData`, and `originalDoc`. + +## Key Takeaways + +- Field hooks are the preferred place to **isolate per-field logic** — keeps Collection hooks clean. +- `operation` arg lets you branch `beforeChange`/`afterChange` between create and update. +- `beforeChange` receives **unvalidated** data — validate manually if using for side effects. +- Changing the return type breaks GraphQL — stick to same type or use Collection hooks. +- `beforeDuplicate` runs **before** `beforeValidate`; default appends `" - Copy"` to unique text fields. +- Use three-generic `FieldHook` for full TypeScript safety. +- `context` (from `req.context`) lets hooks share data without extra DB fetches — see [[wiki/payloadcms/hooks-context|Hooks Context]]. + +## Related + +- [[wiki/payloadcms/hooks-collections|Collection Hooks]] — document-level lifecycle hooks +- [[wiki/payloadcms/hooks-context|Hooks Context (req.context)]] — share state across hooks +- [[wiki/payloadcms/hooks|Hooks Overview]] — hook categories and execution model +- [[wiki/payloadcms/fields-overview|Fields Overview]] — field config, validation, custom components + +## Sources + +- `raw/hooks__fields.md` — https://payloadcms.com/docs/hooks/fields