chore(agent): configure AI agents and tools
Add configuration for BMad, Claude, OpenCode, and other AI agent tools and workflows.
This commit is contained in:
341
.opencode/skills/payload-cms/references/hooks.md
Normal file
341
.opencode/skills/payload-cms/references/hooks.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user