Add trigger builder helpers to improve DX for custom triggers

- Add createTrigger() and createAdvancedTrigger() helpers
- Add preset builders: webhookTrigger, cronTrigger, eventTrigger, etc.
- Implement virtual fields with JSON backing for trigger parameters
- Eliminate 90% of boilerplate when creating custom triggers
- Add /helpers export path for trigger builders
- Fix field name clashing between built-in and custom trigger parameters
- Add comprehensive examples and documentation
- Maintain backward compatibility with existing triggers

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-07 15:30:10 +02:00
parent 0a036752ea
commit c47197223c
18 changed files with 1185 additions and 623 deletions

View File

@@ -0,0 +1,218 @@
# Trigger Builder Examples
The new trigger builder API dramatically reduces boilerplate when creating custom triggers.
## Before vs After
### Before (Manual Approach)
```typescript
const customTrigger = {
slug: 'order-webhook',
inputs: [
{
name: 'webhookSecret',
type: 'text',
required: true,
virtual: true,
admin: {
condition: (_, siblingData) => siblingData?.type === 'order-webhook',
description: 'Secret for webhook validation'
},
hooks: {
afterRead: [({ siblingData }) => siblingData?.parameters?.webhookSecret],
beforeChange: [({ value, siblingData }) => {
if (!siblingData.parameters) siblingData.parameters = {}
siblingData.parameters.webhookSecret = value
return undefined
}]
}
},
{
name: 'orderStatuses',
type: 'select',
hasMany: true,
options: ['pending', 'processing', 'completed'],
defaultValue: ['completed'],
virtual: true,
admin: {
condition: (_, siblingData) => siblingData?.type === 'order-webhook',
description: 'Order statuses that trigger the workflow'
},
hooks: {
afterRead: [({ siblingData }) => siblingData?.parameters?.orderStatuses || ['completed']],
beforeChange: [({ value, siblingData }) => {
if (!siblingData.parameters) siblingData.parameters = {}
siblingData.parameters.orderStatuses = value
return undefined
}]
}
}
// ... imagine more fields with similar boilerplate
]
}
```
### After (Builder Approach)
```typescript
import { createTrigger } from '@xtr-dev/payload-automation/helpers'
const orderWebhook = createTrigger('order-webhook').parameters({
webhookSecret: {
type: 'text',
required: true,
admin: {
description: 'Secret for webhook validation'
}
},
orderStatuses: {
type: 'select',
hasMany: true,
options: ['pending', 'processing', 'completed'],
defaultValue: ['completed'],
admin: {
description: 'Order statuses that trigger the workflow'
}
}
})
```
## Built-in Trigger Presets
### Webhook Trigger
```typescript
import { webhookTrigger } from '@xtr-dev/payload-automation/helpers'
const paymentWebhook = webhookTrigger('payment-webhook')
.parameter('currency', {
type: 'select',
options: ['USD', 'EUR', 'GBP'],
defaultValue: 'USD'
})
.build()
```
### Scheduled/Cron Trigger
```typescript
import { cronTrigger } from '@xtr-dev/payload-automation/helpers'
const dailyReport = cronTrigger('daily-report')
.parameter('reportFormat', {
type: 'select',
options: ['pdf', 'csv', 'json'],
defaultValue: 'pdf'
})
.build()
```
### Manual Trigger (No Parameters)
```typescript
import { manualTrigger } from '@xtr-dev/payload-automation/helpers'
const backupTrigger = manualTrigger('manual-backup')
```
## Advanced Usage
### Extending Common Parameters
```typescript
import { createAdvancedTrigger, webhookParameters } from '@xtr-dev/payload-automation/helpers'
const advancedWebhook = createAdvancedTrigger('advanced-webhook')
.extend(webhookParameters) // Includes path, secret, headers
.parameter('retryAttempts', {
type: 'number',
min: 0,
max: 5,
defaultValue: 3
})
.parameter('timeout', {
type: 'number',
min: 1000,
max: 30000,
defaultValue: 5000,
admin: {
description: 'Timeout in milliseconds'
}
})
.build()
```
### Custom Validation
```typescript
const validatedTrigger = createTrigger('validated-trigger').parameters({
email: {
type: 'email',
required: true,
validate: (value) => {
if (value?.endsWith('@spam.com')) {
return 'Spam domains not allowed'
}
return true
}
},
webhookUrl: {
type: 'text',
required: true,
validate: (value) => {
try {
const url = new URL(value)
if (!['http:', 'https:'].includes(url.protocol)) {
return 'Only HTTP/HTTPS URLs allowed'
}
} catch {
return 'Please enter a valid URL'
}
return true
}
}
})
```
## Usage in Plugin Configuration
```typescript
import { workflowsPlugin } from '@xtr-dev/payload-automation'
import {
createTrigger,
webhookTrigger,
cronTrigger
} from '@xtr-dev/payload-automation/helpers'
export default buildConfig({
plugins: [
workflowsPlugin({
triggers: [
// Mix different trigger types
createTrigger('user-signup').parameters({
source: {
type: 'select',
options: ['web', 'mobile', 'api'],
required: true
}
}),
webhookTrigger('payment-received')
.parameter('minimumAmount', { type: 'number', min: 0 })
.build(),
cronTrigger('weekly-cleanup')
.parameter('deleteOlderThan', {
type: 'number',
defaultValue: 30,
admin: { description: 'Delete records older than N days' }
})
.build()
]
})
]
})
```
## Benefits
- **90% less boilerplate** - No manual hooks, conditions, or virtual field setup
- **Type safety** - Full TypeScript support
- **Reusable patterns** - Common trigger types as presets
- **Composable** - Mix builders with manual fields
- **Backward compatible** - Existing triggers continue to work
- **Validation built-in** - Parameter validation handled automatically

View File

@@ -0,0 +1,300 @@
/**
* Examples demonstrating the new trigger builder API
* This shows the before/after comparison and various usage patterns
*/
import {
createTrigger,
createAdvancedTrigger,
webhookTrigger,
cronTrigger,
eventTrigger,
manualTrigger,
apiTrigger,
webhookParameters,
cronParameters
} from '../src/exports/helpers.js'
/**
* BEFORE: Manual trigger definition with lots of boilerplate
*/
const oldWayTrigger = {
slug: 'order-webhook',
inputs: [
{
name: 'webhookSecret',
type: 'text',
required: true,
virtual: true,
admin: {
condition: (_, siblingData) => siblingData?.type === 'order-webhook',
description: 'Secret for webhook validation'
},
hooks: {
afterRead: [({ siblingData }) => siblingData?.parameters?.webhookSecret],
beforeChange: [({ value, siblingData }) => {
if (!siblingData.parameters) siblingData.parameters = {}
siblingData.parameters.webhookSecret = value
return undefined
}]
}
},
{
name: 'orderStatuses',
type: 'select',
hasMany: true,
options: ['pending', 'processing', 'completed'],
defaultValue: ['completed'],
virtual: true,
admin: {
condition: (_, siblingData) => siblingData?.type === 'order-webhook',
description: 'Order statuses that trigger the workflow'
},
hooks: {
afterRead: [({ siblingData }) => siblingData?.parameters?.orderStatuses || ['completed']],
beforeChange: [({ value, siblingData }) => {
if (!siblingData.parameters) siblingData.parameters = {}
siblingData.parameters.orderStatuses = value
return undefined
}]
}
}
// ... imagine more fields with similar boilerplate
]
} as const
/**
* AFTER: Clean trigger definition using builders
*/
// 1. Simple trigger with parameters
const orderWebhook = createTrigger('order-webhook').parameters({
webhookSecret: {
type: 'text',
required: true,
admin: {
description: 'Secret for webhook validation'
}
},
orderStatuses: {
type: 'select',
hasMany: true,
options: ['pending', 'processing', 'completed'],
defaultValue: ['completed'],
admin: {
description: 'Order statuses that trigger the workflow'
}
},
minimumAmount: {
type: 'number',
min: 0,
admin: {
description: 'Minimum order amount to trigger workflow'
}
}
})
// 2. Using preset webhook builder
const paymentWebhook = webhookTrigger('payment-webhook')
.parameter('currency', {
type: 'select',
options: ['USD', 'EUR', 'GBP'],
defaultValue: 'USD'
})
.parameter('paymentMethods', {
type: 'select',
hasMany: true,
options: ['credit_card', 'paypal', 'bank_transfer']
})
.build()
// 3. Scheduled trigger using cron builder
const dailyReport = cronTrigger('daily-report')
.parameter('reportFormat', {
type: 'select',
options: [
{ label: 'PDF Report', value: 'pdf' },
{ label: 'CSV Export', value: 'csv' },
{ label: 'JSON Data', value: 'json' }
],
defaultValue: 'pdf'
})
.parameter('includeCharts', {
type: 'checkbox',
defaultValue: true,
admin: {
description: 'Include visual charts in the report'
}
})
.build()
// 4. Event-driven trigger
const userActivity = eventTrigger('user-activity')
.parameter('actionTypes', {
type: 'select',
hasMany: true,
options: ['login', 'logout', 'profile_update', 'password_change'],
admin: {
description: 'User actions that should trigger this workflow'
}
})
.parameter('userRoles', {
type: 'select',
hasMany: true,
options: ['admin', 'editor', 'user'],
admin: {
description: 'Only trigger for users with these roles'
}
})
.build()
// 5. Simple manual trigger (no parameters)
const manualBackup = manualTrigger('manual-backup')
// 6. API trigger with authentication
const externalApi = apiTrigger('external-api')
.parameter('allowedOrigins', {
type: 'textarea',
admin: {
description: 'Comma-separated list of allowed origins'
},
validate: (value) => {
if (value && typeof value === 'string') {
const origins = value.split(',').map(s => s.trim())
const validOrigins = origins.every(origin => {
try {
new URL(origin)
return true
} catch {
return false
}
})
if (!validOrigins) {
return 'All origins must be valid URLs'
}
}
return true
}
})
.build()
// 7. Complex trigger extending common parameters
const advancedWebhook = createAdvancedTrigger('advanced-webhook')
.extend(webhookParameters) // Start with webhook basics
.parameter('retryConfig', {
type: 'group',
fields: [
{
name: 'maxRetries',
type: 'number',
min: 0,
max: 10,
defaultValue: 3
},
{
name: 'retryDelay',
type: 'number',
min: 1000,
max: 60000,
defaultValue: 5000,
admin: {
description: 'Delay between retries in milliseconds'
}
}
]
})
.parameter('filters', {
type: 'array',
fields: [
{
name: 'field',
type: 'text',
required: true
},
{
name: 'operator',
type: 'select',
options: ['equals', 'not_equals', 'contains', 'greater_than'],
required: true
},
{
name: 'value',
type: 'text',
required: true
}
]
})
.build()
// 8. Custom parameter validation
const validatedTrigger = createTrigger('validated-trigger').parameters({
email: {
type: 'email',
required: true,
validate: (value) => {
if (value && typeof value === 'string') {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(value)) {
return 'Please enter a valid email address'
}
// Custom business logic validation
if (value.endsWith('@example.com')) {
return 'Example.com emails are not allowed'
}
}
return true
}
},
webhookUrl: {
type: 'text',
required: true,
validate: (value) => {
if (value && typeof value === 'string') {
try {
const url = new URL(value)
if (!['http:', 'https:'].includes(url.protocol)) {
return 'URL must use HTTP or HTTPS protocol'
}
if (url.hostname === 'localhost') {
return 'Localhost URLs are not allowed in production'
}
} catch {
return 'Please enter a valid URL'
}
}
return true
}
}
})
/**
* Export all triggers for use in plugin configuration
*/
export const exampleTriggers = [
orderWebhook,
paymentWebhook,
dailyReport,
userActivity,
manualBackup,
externalApi,
advancedWebhook,
validatedTrigger
]
/**
* Usage in payload.config.ts:
*
* ```typescript
* import { workflowsPlugin } from '@xtr-dev/payload-automation'
* import { exampleTriggers } from './examples/trigger-builders'
*
* export default buildConfig({
* plugins: [
* workflowsPlugin({
* triggers: exampleTriggers,
* // ... other config
* })
* ]
* })
* ```
*/