vault backup: 2026-05-15 15:51:51

This commit is contained in:
Vadym Samoilenko 2026-05-15 15:51:51 +01:00
parent 85ba36324d
commit 10be227149
4 changed files with 166 additions and 1 deletions

View file

@ -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 |

View file

@ -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 |

View file

@ -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\<Doc\> | 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: <DocumentType, ValueType, SiblingDataType>
type PostTitleHook = FieldHook<Post, string, Post>
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<Doc, Val, Sibling>` 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