Files
website-enchun-mgr/.agent/skills/payload-cms/references/hooks.md
pkupuk ad8e2e313e chore(agent): configure AI agents and tools
Add configuration for BMad, Claude, OpenCode, and other AI agent tools and workflows.
2026-02-11 11:51:23 +08:00

342 lines
6.7 KiB
Markdown

# Hooks Reference
## Hook Lifecycle
```
Operation: CREATE
beforeOperation → beforeValidate → beforeChange → [DB Write] → afterChange → afterOperation
Operation: UPDATE
beforeOperation → beforeValidate → beforeChange → [DB Write] → afterChange → afterOperation
Operation: READ
beforeOperation → beforeRead → [DB Read] → afterRead → afterOperation
Operation: DELETE
beforeOperation → beforeDelete → [DB Delete] → afterDelete → afterOperation
```
## Collection Hooks
### beforeValidate
Transform data before validation runs:
```ts
hooks: {
beforeValidate: [
async ({ data, operation, req }) => {
if (operation === 'create') {
data.createdBy = req.user?.id
}
return data // Always return data
},
],
}
```
### beforeChange
Transform data before database write (after validation):
```ts
hooks: {
beforeChange: [
async ({ data, operation, originalDoc, req }) => {
// Auto-generate slug on create
if (operation === 'create' && data.title) {
data.slug = data.title.toLowerCase().replace(/\s+/g, '-')
}
// Track last modified by
data.lastModifiedBy = req.user?.id
return data
},
],
}
```
### afterChange
Side effects after database write:
```ts
hooks: {
afterChange: [
async ({ doc, operation, req, context }) => {
// Prevent infinite loops
if (context.skipAuditLog) return doc
// Create audit log entry
await req.payload.create({
collection: 'audit-logs',
data: {
action: operation,
collection: 'posts',
documentId: doc.id,
userId: req.user?.id,
timestamp: new Date(),
},
req, // CRITICAL: maintains transaction
context: { skipAuditLog: true },
})
return doc
},
],
}
```
### beforeRead
Modify query before database read:
```ts
hooks: {
beforeRead: [
async ({ doc, req }) => {
// doc is the raw database document
// Can modify before afterRead transforms
return doc
},
],
}
```
### afterRead
Transform data before sending to client:
```ts
hooks: {
afterRead: [
async ({ doc, req }) => {
// Add computed field
doc.fullName = `${doc.firstName} ${doc.lastName}`
// Hide sensitive data for non-admins
if (!req.user?.roles?.includes('admin')) {
delete doc.internalNotes
}
return doc
},
],
}
```
### beforeDelete
Pre-delete validation or cleanup:
```ts
hooks: {
beforeDelete: [
async ({ id, req }) => {
// Cascading delete: remove related comments
await req.payload.delete({
collection: 'comments',
where: { post: { equals: id } },
req,
})
},
],
}
```
### afterDelete
Post-delete cleanup:
```ts
hooks: {
afterDelete: [
async ({ doc, req }) => {
// Clean up uploaded files
if (doc.image) {
await deleteFile(doc.image.filename)
}
},
],
}
```
## Field Hooks
Hooks on individual fields:
```ts
{
name: 'slug',
type: 'text',
hooks: {
beforeValidate: [
({ value, data }) => {
if (!value && data?.title) {
return data.title.toLowerCase().replace(/\s+/g, '-')
}
return value
},
],
afterRead: [
({ value }) => value?.toLowerCase(),
],
},
}
```
## Context Pattern
**Prevent infinite loops and share state between hooks:**
```ts
hooks: {
afterChange: [
async ({ doc, req, context }) => {
// Check context flag to prevent loops
if (context.skipNotification) return doc
// Trigger related update with context flag
await req.payload.update({
collection: 'related',
id: doc.relatedId,
data: { updated: true },
req,
context: {
...context,
skipNotification: true, // Prevent loop
},
})
return doc
},
],
}
```
## Transactions
**CRITICAL: Always pass `req` for transaction integrity:**
```ts
hooks: {
afterChange: [
async ({ doc, req }) => {
// ✅ Same transaction - atomic
await req.payload.create({
collection: 'audit-logs',
data: { documentId: doc.id },
req, // REQUIRED
})
// ❌ Separate transaction - can leave inconsistent state
await req.payload.create({
collection: 'audit-logs',
data: { documentId: doc.id },
// Missing req!
})
return doc
},
],
}
```
## Next.js Revalidation with Context Control
```ts
import { revalidatePath, revalidateTag } from 'next/cache'
hooks: {
afterChange: [
async ({ doc, context }) => {
// Skip revalidation for internal updates
if (context.skipRevalidation) return doc
revalidatePath(`/posts/${doc.slug}`)
revalidateTag('posts')
return doc
},
],
}
```
## Auth Hooks (Auth Collections Only)
```ts
export const Users: CollectionConfig = {
slug: 'users',
auth: true,
hooks: {
afterLogin: [
async ({ doc, req }) => {
// Log login
await req.payload.create({
collection: 'login-logs',
data: { userId: doc.id, timestamp: new Date() },
req,
})
return doc
},
],
afterLogout: [
async ({ req }) => {
// Clear session data
},
],
afterMe: [
async ({ doc, req }) => {
// Add extra user info
return doc
},
],
afterRefresh: [
async ({ doc, req }) => {
// Custom token refresh logic
return doc
},
],
afterForgotPassword: [
async ({ args }) => {
// Custom forgot password notification
},
],
},
fields: [...],
}
```
## Hook Arguments Reference
All hooks receive these base arguments:
| Argument | Description |
|----------|-------------|
| `req` | Request object with `payload`, `user`, `locale` |
| `context` | Shared context object between hooks |
| `collection` | Collection config |
Operation-specific arguments:
| Hook | Additional Arguments |
|------|---------------------|
| `beforeValidate` | `data`, `operation`, `originalDoc` |
| `beforeChange` | `data`, `operation`, `originalDoc` |
| `afterChange` | `doc`, `operation`, `previousDoc` |
| `beforeRead` | `doc` |
| `afterRead` | `doc` |
| `beforeDelete` | `id` |
| `afterDelete` | `doc`, `id` |
## Best Practices
1. **Always return the data/doc** - Even if unchanged
2. **Use context for loop prevention** - Check before triggering recursive operations
3. **Pass req for transactions** - Maintains atomicity
4. **Keep hooks focused** - One responsibility per hook
5. **Use field hooks for field-specific logic** - Better encapsulation
6. **Avoid heavy operations in beforeRead** - Runs on every query
7. **Use afterChange for side effects** - Email, webhooks, etc.