mirror of
https://github.com/xtr-dev/payload-automation.git
synced 2025-12-10 00:43:23 +00:00
Remove initCollectionHooks and associated migration guides
- Delete `initCollectionHooks` implementation and its usage references - Remove `MIGRATION-v0.0.24.md` and `NOT-IMPLEMENTING.md` as they are now obsolete - Update related workflow executor logic and TypeScript definitions, ensuring compatibility - Simplify error handling, input parsing, and logging within workflow execution - Clean up and refactor redundant code to improve maintainability
This commit is contained in:
@@ -1,187 +0,0 @@
|
|||||||
# Migration Guide: v0.0.23 → v0.0.24
|
|
||||||
|
|
||||||
## What's New
|
|
||||||
|
|
||||||
Version 0.0.24 introduces **trigger builder helpers** that dramatically reduce boilerplate when creating custom triggers, plus fixes field name clashing between built-in and external trigger parameters.
|
|
||||||
|
|
||||||
## Breaking Changes
|
|
||||||
|
|
||||||
**None** - This is a fully backward-compatible release. All existing triggers continue to work exactly as before.
|
|
||||||
|
|
||||||
## New Features
|
|
||||||
|
|
||||||
### 1. Trigger Builder Helpers
|
|
||||||
|
|
||||||
New helper functions eliminate 90% of boilerplate when creating custom triggers:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm update @xtr-dev/payload-automation
|
|
||||||
```
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Import the new helpers
|
|
||||||
import {
|
|
||||||
createTrigger,
|
|
||||||
webhookTrigger,
|
|
||||||
cronTrigger
|
|
||||||
} from '@xtr-dev/payload-automation/helpers'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Fixed Field Name Clashing
|
|
||||||
|
|
||||||
Built-in trigger parameters now use a JSON backing store to prevent conflicts with custom trigger fields.
|
|
||||||
|
|
||||||
## Migration Steps
|
|
||||||
|
|
||||||
### Step 1: Update Package
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install @xtr-dev/payload-automation@latest
|
|
||||||
# or
|
|
||||||
pnpm update @xtr-dev/payload-automation
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: (Optional) Modernize Custom Triggers
|
|
||||||
|
|
||||||
**Your existing triggers will continue to work**, but you can optionally migrate to the cleaner syntax:
|
|
||||||
|
|
||||||
#### Before (Still Works)
|
|
||||||
```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
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ... more boilerplate
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### After (Recommended)
|
|
||||||
```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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Add more parameters easily
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: (Optional) Use Preset Builders
|
|
||||||
|
|
||||||
For common trigger patterns:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { webhookTrigger, cronTrigger } from '@xtr-dev/payload-automation/helpers'
|
|
||||||
|
|
||||||
// Webhook trigger with built-in path, secret, headers parameters
|
|
||||||
const paymentWebhook = webhookTrigger('payment-webhook')
|
|
||||||
.parameter('currency', {
|
|
||||||
type: 'select',
|
|
||||||
options: ['USD', 'EUR', 'GBP']
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
|
|
||||||
// Cron trigger with built-in expression, timezone parameters
|
|
||||||
const dailyReport = cronTrigger('daily-report')
|
|
||||||
.parameter('format', {
|
|
||||||
type: 'select',
|
|
||||||
options: ['pdf', 'csv']
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quick Migration Examples
|
|
||||||
|
|
||||||
### Simple Trigger Migration
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// OLD WAY (still works)
|
|
||||||
{
|
|
||||||
slug: 'user-signup',
|
|
||||||
inputs: [/* 20+ lines of boilerplate per field */]
|
|
||||||
}
|
|
||||||
|
|
||||||
// NEW WAY (recommended)
|
|
||||||
import { createTrigger } from '@xtr-dev/payload-automation/helpers'
|
|
||||||
|
|
||||||
const userSignup = createTrigger('user-signup').parameters({
|
|
||||||
source: {
|
|
||||||
type: 'select',
|
|
||||||
options: ['web', 'mobile', 'api'],
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
userType: {
|
|
||||||
type: 'select',
|
|
||||||
options: ['regular', 'premium'],
|
|
||||||
defaultValue: 'regular'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Webhook Trigger Migration
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// OLD WAY
|
|
||||||
{
|
|
||||||
slug: 'payment-webhook',
|
|
||||||
inputs: [/* Manual webhookPath field + lots of boilerplate */]
|
|
||||||
}
|
|
||||||
|
|
||||||
// NEW WAY
|
|
||||||
import { webhookTrigger } from '@xtr-dev/payload-automation/helpers'
|
|
||||||
|
|
||||||
const paymentWebhook = webhookTrigger('payment-webhook')
|
|
||||||
.parameter('minimumAmount', {
|
|
||||||
type: 'number',
|
|
||||||
min: 0
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Benefits of Migration
|
|
||||||
|
|
||||||
- **90% less code** - Eliminate virtual field boilerplate
|
|
||||||
- **No field name conflicts** - Built-in parameters isolated
|
|
||||||
- **Better TypeScript support** - Full type inference
|
|
||||||
- **Preset patterns** - Common trigger types ready-to-use
|
|
||||||
- **Composable API** - Easy to extend and customize
|
|
||||||
|
|
||||||
## Compatibility
|
|
||||||
|
|
||||||
- ✅ **Existing triggers** continue to work unchanged
|
|
||||||
- ✅ **Mix old and new** trigger styles in same config
|
|
||||||
- ✅ **No database changes** required
|
|
||||||
- ✅ **PayloadCMS field compatibility** maintained
|
|
||||||
|
|
||||||
## Need Help?
|
|
||||||
|
|
||||||
- [View examples](./examples/trigger-builders.ts)
|
|
||||||
- [Read documentation](./examples/README-trigger-builders.md)
|
|
||||||
- [Report issues](https://github.com/xtr-dev/payload-automation/issues)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**TL;DR**: Update the package, optionally migrate custom triggers to use the new helpers for cleaner code. All existing triggers continue to work without changes.
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
# Steps and Triggers Not Implementing
|
|
||||||
|
|
||||||
This document lists workflow steps and triggers that are intentionally **not** being implemented in the core plugin. These are either better suited as custom user implementations or fall outside the plugin's scope.
|
|
||||||
|
|
||||||
## Steps Not Implementing
|
|
||||||
|
|
||||||
### Workflow Orchestration
|
|
||||||
- **Stop Workflow** - Can be achieved through conditional logic
|
|
||||||
- **Run Workflow** - Adds complexity to execution tracking and circular dependency management
|
|
||||||
- **Parallel Fork/Join** - Current dependency system already enables parallel execution
|
|
||||||
|
|
||||||
### External Service Integrations
|
|
||||||
- **GraphQL Query** - Better as custom HTTP request step
|
|
||||||
- **S3/Cloud Storage** - Too provider-specific
|
|
||||||
- **Message Queue** (Kafka, RabbitMQ, SQS) - Infrastructure-specific
|
|
||||||
- **SMS** (Twilio, etc.) - Requires external accounts
|
|
||||||
- **Push Notifications** - Platform-specific implementation
|
|
||||||
- **Slack/Discord/Teams** - Better as custom HTTP webhooks
|
|
||||||
- **Calendar Integration** - Too many providers to support
|
|
||||||
|
|
||||||
### AI/ML Operations
|
|
||||||
- **AI Prompt** (OpenAI, Claude, etc.) - Requires API keys, better as custom implementation
|
|
||||||
- **Text Analysis** - Too many variations and providers
|
|
||||||
- **Image Processing** - Better handled by dedicated services
|
|
||||||
|
|
||||||
### Specialized Data Operations
|
|
||||||
- **Database Query** (Direct SQL/NoSQL) - Security concerns, bypasses Payload
|
|
||||||
- **File Operations** - Complex permission and security implications
|
|
||||||
- **Hash/Encrypt** - Security-sensitive, needs careful implementation
|
|
||||||
- **RSS/Feed Processing** - Too specific for core plugin
|
|
||||||
|
|
||||||
## Triggers Not Implementing
|
|
||||||
|
|
||||||
### Workflow Events
|
|
||||||
- **Workflow Complete/Failed** - Adds circular dependency complexity
|
|
||||||
- **Step Failed** - Complicates error handling flow
|
|
||||||
|
|
||||||
### System Events
|
|
||||||
- **File Upload** - Can use collection hooks on media collections
|
|
||||||
- **User Authentication** (Login/Logout) - Security implications
|
|
||||||
- **Server Start/Stop** - Lifecycle management complexity
|
|
||||||
- **Cache Clear** - Too implementation-specific
|
|
||||||
- **Migration/Backup Events** - Infrastructure-specific
|
|
||||||
|
|
||||||
### External Monitoring
|
|
||||||
- **Email Received** (IMAP/POP3) - Requires mail server setup
|
|
||||||
- **Git Webhooks** - Better as standard webhook triggers
|
|
||||||
- **Performance Alerts** - Requires monitoring infrastructure
|
|
||||||
- **Error Events** - Better handled by dedicated error tracking
|
|
||||||
|
|
||||||
### Time-Based
|
|
||||||
- **Cron Triggers** - Complex timezone handling, process management, and reliability concerns. Use webhook triggers with external cron services instead (GitHub Actions, Vercel Cron, AWS EventBridge, etc.)
|
|
||||||
- **Recurring Patterns** (e.g., "every 2nd Tuesday") - Complex parsing and timezone handling
|
|
||||||
- **Date Range Triggers** - Can be achieved with conditional logic in workflows
|
|
||||||
|
|
||||||
## Why These Aren't Core Features
|
|
||||||
|
|
||||||
1. **Maintainability**: Each external integration requires ongoing maintenance as APIs change
|
|
||||||
2. **Security**: Many features have security implications that are better handled by users who understand their specific requirements
|
|
||||||
3. **Flexibility**: Users can implement these as custom steps/triggers tailored to their needs
|
|
||||||
4. **Scope**: The plugin focuses on being a solid workflow engine, not an everything-integration platform
|
|
||||||
5. **Dependencies**: Avoiding external service dependencies keeps the plugin lightweight
|
|
||||||
|
|
||||||
## What Users Can Do Instead
|
|
||||||
|
|
||||||
- Implement custom steps using the plugin's TaskConfig interface
|
|
||||||
- Use HTTP Request step for most external integrations
|
|
||||||
- Create custom triggers through Payload hooks
|
|
||||||
- Build specialized workflow packages on top of this plugin
|
|
||||||
|
|
||||||
### Cron Alternative: Webhook + External Service
|
|
||||||
|
|
||||||
Instead of built-in cron triggers, use webhook triggers with external cron services:
|
|
||||||
|
|
||||||
**GitHub Actions** (Free):
|
|
||||||
```yaml
|
|
||||||
# .github/workflows/daily-report.yml
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 9 * * *' # Daily at 9 AM UTC
|
|
||||||
jobs:
|
|
||||||
trigger-workflow:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: curl -X POST https://your-app.com/api/workflows-webhook/daily-report
|
|
||||||
```
|
|
||||||
|
|
||||||
**Vercel Cron** (Serverless):
|
|
||||||
```js
|
|
||||||
// api/cron/daily.js
|
|
||||||
export default async function handler(req, res) {
|
|
||||||
await fetch('https://your-app.com/api/workflows-webhook/daily-report', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ source: 'vercel-cron' })
|
|
||||||
});
|
|
||||||
res.status(200).json({ success: true });
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits**: Better reliability, proper process isolation, easier debugging, and leverages existing infrastructure.
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import { WorkflowExecutionStatus as WorkflowExecutionStatus_6f365a93b6cb4b34ad564b391e21db6f } from '@xtr-dev/payload-automation/client'
|
|
||||||
import { StatusCell as StatusCell_6f365a93b6cb4b34ad564b391e21db6f } from '@xtr-dev/payload-automation/client'
|
import { StatusCell as StatusCell_6f365a93b6cb4b34ad564b391e21db6f } from '@xtr-dev/payload-automation/client'
|
||||||
import { ErrorDisplay as ErrorDisplay_6f365a93b6cb4b34ad564b391e21db6f } from '@xtr-dev/payload-automation/client'
|
import { ErrorDisplay as ErrorDisplay_6f365a93b6cb4b34ad564b391e21db6f } from '@xtr-dev/payload-automation/client'
|
||||||
|
|
||||||
export const importMap = {
|
export const importMap = {
|
||||||
"@xtr-dev/payload-automation/client#WorkflowExecutionStatus": WorkflowExecutionStatus_6f365a93b6cb4b34ad564b391e21db6f,
|
|
||||||
"@xtr-dev/payload-automation/client#StatusCell": StatusCell_6f365a93b6cb4b34ad564b391e21db6f,
|
"@xtr-dev/payload-automation/client#StatusCell": StatusCell_6f365a93b6cb4b34ad564b391e21db6f,
|
||||||
"@xtr-dev/payload-automation/client#ErrorDisplay": ErrorDisplay_6f365a93b6cb4b34ad564b391e21db6f
|
"@xtr-dev/payload-automation/client#ErrorDisplay": ErrorDisplay_6f365a93b6cb4b34ad564b391e21db6f
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export default [
|
|||||||
'perfectionist/sort-object-types': 'off',
|
'perfectionist/sort-object-types': 'off',
|
||||||
'perfectionist/sort-objects': 'off',
|
'perfectionist/sort-objects': 'off',
|
||||||
'perfectionist/sort-exports': 'off',
|
'perfectionist/sort-exports': 'off',
|
||||||
|
'perfectionist/sort-imports': 'off'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import type {CollectionConfig, Field} from 'payload'
|
import type {CollectionConfig} from 'payload'
|
||||||
|
|
||||||
import type {WorkflowsPluginConfig} from "../plugin/config-types.js"
|
import type {WorkflowsPluginConfig} from "../plugin/config-types.js"
|
||||||
|
|
||||||
|
import {parameter} from "../fields/parameter.js"
|
||||||
|
import {collectionHookTrigger} from "../triggers/index.js"
|
||||||
|
|
||||||
export const createWorkflowCollection: <T extends string>(options: WorkflowsPluginConfig<T>) => CollectionConfig = (options) => {
|
export const createWorkflowCollection: <T extends string>(options: WorkflowsPluginConfig<T>) => CollectionConfig = (options) => {
|
||||||
const {steps} = options
|
const steps = options.steps || []
|
||||||
const triggers = (options.triggers || []).map(t => t(options))
|
const triggers = (options.triggers || []).map(t => t(options)).concat(collectionHookTrigger(options))
|
||||||
return {
|
return {
|
||||||
slug: 'workflows',
|
slug: 'workflows',
|
||||||
access: {
|
access: {
|
||||||
@@ -35,16 +38,6 @@ export const createWorkflowCollection: <T extends string>(options: WorkflowsPlug
|
|||||||
description: 'Optional description of what this workflow does',
|
description: 'Optional description of what this workflow does',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'executionStatus',
|
|
||||||
type: 'ui',
|
|
||||||
admin: {
|
|
||||||
components: {
|
|
||||||
Field: '@xtr-dev/payload-automation/client#WorkflowExecutionStatus'
|
|
||||||
},
|
|
||||||
condition: (data) => !!data?.id // Only show for existing workflows
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'triggers',
|
name: 'triggers',
|
||||||
type: 'array',
|
type: 'array',
|
||||||
@@ -53,7 +46,7 @@ export const createWorkflowCollection: <T extends string>(options: WorkflowsPlug
|
|||||||
name: 'type',
|
name: 'type',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
...(triggers || []).map(t => t.slug)
|
...triggers.map(t => t.slug)
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -64,6 +57,8 @@ export const createWorkflowCollection: <T extends string>(options: WorkflowsPlug
|
|||||||
},
|
},
|
||||||
defaultValue: {}
|
defaultValue: {}
|
||||||
},
|
},
|
||||||
|
// Virtual fields for custom triggers
|
||||||
|
...triggers.flatMap(t => (t.parameters || []).map(p => parameter(t.slug, p as any))),
|
||||||
{
|
{
|
||||||
name: 'condition',
|
name: 'condition',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -72,8 +67,6 @@ export const createWorkflowCollection: <T extends string>(options: WorkflowsPlug
|
|||||||
},
|
},
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
// Virtual fields for custom triggers
|
|
||||||
...(triggers || []).flatMap(t => (t.fields || []))
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -81,58 +74,25 @@ export const createWorkflowCollection: <T extends string>(options: WorkflowsPlug
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: 'row',
|
name: 'name',
|
||||||
fields: [
|
type: 'text',
|
||||||
|
defaultValue: 'Unnamed Step'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'step',
|
name: 'type',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: steps.map(t => t.slug)
|
options: steps.map(t => t.slug)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'parameters',
|
||||||
type: 'text',
|
type: 'json',
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
...(steps || []).flatMap(step => (step.inputSchema || []).map(field => {
|
|
||||||
const originalName = (field as any).name;
|
|
||||||
const resultField: any = {
|
|
||||||
...field,
|
|
||||||
// Prefix field name with step slug to avoid conflicts
|
|
||||||
name: `__step_${step.slug}_${originalName}`,
|
|
||||||
admin: {
|
admin: {
|
||||||
...(field.admin || {}),
|
hidden: true,
|
||||||
condition: (...args: any[]) => args[1]?.step === step.slug && (
|
|
||||||
(field.admin as any)?.condition ?
|
|
||||||
(field.admin as any).condition.call(this, ...args) :
|
|
||||||
true
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
virtual: true,
|
defaultValue: {}
|
||||||
};
|
},
|
||||||
|
// Virtual fields for custom triggers
|
||||||
// Add hooks to store/retrieve from the step's input data
|
...steps.flatMap(step => (step.inputSchema || []).map(s => parameter(step.slug, s as any))),
|
||||||
resultField.hooks = {
|
|
||||||
...((field as any).hooks || {}),
|
|
||||||
afterRead: [
|
|
||||||
...(((field as any).hooks)?.afterRead || []),
|
|
||||||
({ siblingData }: any) => {
|
|
||||||
// Read from step input data using original field name
|
|
||||||
return siblingData?.[originalName] || (field as any).defaultValue;
|
|
||||||
}
|
|
||||||
],
|
|
||||||
beforeChange: [
|
|
||||||
...(((field as any).hooks)?.beforeChange || []),
|
|
||||||
({ siblingData, value }: any) => {
|
|
||||||
// Store in step data using original field name
|
|
||||||
siblingData[originalName] = value;
|
|
||||||
return undefined; // Don't store the prefixed field
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
return resultField as Field;
|
|
||||||
})),
|
|
||||||
{
|
{
|
||||||
name: 'dependencies',
|
name: 'dependencies',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|||||||
@@ -48,20 +48,7 @@ export const WorkflowExecutionStatus: React.FC<WorkflowExecutionStatusProps> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (runs.length === 0) {
|
if (runs.length === 0) {
|
||||||
return (
|
return null
|
||||||
<div style={{
|
|
||||||
padding: '16px',
|
|
||||||
backgroundColor: '#F9FAFB',
|
|
||||||
border: '1px solid #E5E7EB',
|
|
||||||
borderRadius: '8px',
|
|
||||||
color: '#6B7280',
|
|
||||||
textAlign: 'center'
|
|
||||||
}}>
|
|
||||||
📋 No execution history yet
|
|
||||||
<br />
|
|
||||||
<small>This workflow hasn't been triggered yet.</small>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
const getStatusIcon = (status: string) => {
|
||||||
|
|||||||
@@ -5,26 +5,26 @@ import type { Payload, PayloadRequest } from 'payload'
|
|||||||
export type PayloadWorkflow = {
|
export type PayloadWorkflow = {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: null | string
|
||||||
triggers?: Array<{
|
triggers?: Array<{
|
||||||
type?: string | null
|
type?: null | string
|
||||||
condition?: string | null
|
condition?: null | string
|
||||||
parameters?: {
|
parameters?: {
|
||||||
collectionSlug?: string | null
|
collectionSlug?: null | string
|
||||||
operation?: string | null
|
operation?: null | string
|
||||||
webhookPath?: string | null
|
webhookPath?: null | string
|
||||||
global?: string | null
|
global?: null | string
|
||||||
globalOperation?: string | null
|
globalOperation?: null | string
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
} | null
|
} | null
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}> | null
|
}> | null
|
||||||
steps?: Array<{
|
steps?: Array<{
|
||||||
step?: string | null
|
step?: null | string
|
||||||
name?: string | null
|
name?: null | string
|
||||||
input?: unknown
|
input?: unknown
|
||||||
dependencies?: string[] | null
|
dependencies?: null | string[]
|
||||||
condition?: string | null
|
condition?: null | string
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}> | null
|
}> | null
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
@@ -33,14 +33,14 @@ export type PayloadWorkflow = {
|
|||||||
import { JSONPath } from 'jsonpath-plus'
|
import { JSONPath } from 'jsonpath-plus'
|
||||||
|
|
||||||
// Helper type to extract workflow step data from the generated types
|
// Helper type to extract workflow step data from the generated types
|
||||||
export type WorkflowStep = NonNullable<PayloadWorkflow['steps']>[0] & {
|
export type WorkflowStep = {
|
||||||
name: string // Ensure name is always present for our execution logic
|
name: string // Ensure name is always present for our execution logic
|
||||||
}
|
} & NonNullable<PayloadWorkflow['steps']>[0]
|
||||||
|
|
||||||
// Helper type to extract workflow trigger data from the generated types
|
// Helper type to extract workflow trigger data from the generated types
|
||||||
export type WorkflowTrigger = NonNullable<PayloadWorkflow['triggers']>[0] & {
|
export type WorkflowTrigger = {
|
||||||
type: string // Ensure type is always present for our execution logic
|
type: string // Ensure type is always present for our execution logic
|
||||||
}
|
} & NonNullable<PayloadWorkflow['triggers']>[0]
|
||||||
|
|
||||||
export interface ExecutionContext {
|
export interface ExecutionContext {
|
||||||
steps: Record<string, {
|
steps: Record<string, {
|
||||||
@@ -89,6 +89,7 @@ export interface ExecutionContext {
|
|||||||
email?: string
|
email?: string
|
||||||
id?: string
|
id?: string
|
||||||
}
|
}
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +99,25 @@ export class WorkflowExecutor {
|
|||||||
private logger: Payload['logger']
|
private logger: Payload['logger']
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classifies error types based on error messages
|
||||||
|
*/
|
||||||
|
private classifyErrorType(errorMessage: string): string {
|
||||||
|
if (errorMessage.includes('timeout') || errorMessage.includes('ETIMEDOUT')) {
|
||||||
|
return 'timeout'
|
||||||
|
}
|
||||||
|
if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('getaddrinfo')) {
|
||||||
|
return 'dns'
|
||||||
|
}
|
||||||
|
if (errorMessage.includes('ECONNREFUSED') || errorMessage.includes('ECONNRESET')) {
|
||||||
|
return 'connection'
|
||||||
|
}
|
||||||
|
if (errorMessage.includes('network') || errorMessage.includes('fetch')) {
|
||||||
|
return 'network'
|
||||||
|
}
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluate a step condition using JSONPath
|
* Evaluate a step condition using JSONPath
|
||||||
*/
|
*/
|
||||||
@@ -189,19 +209,31 @@ export class WorkflowExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Move taskSlug declaration outside try block so it's accessible in catch
|
// Move taskSlug declaration outside try block so it's accessible in catch
|
||||||
const taskSlug = step.step // Use the 'step' field for task type
|
const taskSlug = step.type as string
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract input data from step - PayloadCMS flattens inputSchema fields to step level
|
// Extract input data from step - PayloadCMS flattens inputSchema fields to step level
|
||||||
const inputFields: Record<string, unknown> = {}
|
const inputFields: Record<string, unknown> = {}
|
||||||
|
|
||||||
// Get all fields except the core step fields
|
// Get all fields except the core step fields
|
||||||
const coreFields = ['step', 'name', 'dependencies', 'condition']
|
const coreFields = ['step', 'name', 'dependencies', 'condition', 'type', 'id', 'parameters']
|
||||||
for (const [key, value] of Object.entries(step)) {
|
for (const [key, value] of Object.entries(step)) {
|
||||||
if (!coreFields.includes(key)) {
|
if (!coreFields.includes(key)) {
|
||||||
|
// Handle flattened parameters (remove 'parameter' prefix)
|
||||||
|
if (key.startsWith('parameter')) {
|
||||||
|
const cleanKey = key.replace('parameter', '')
|
||||||
|
const properKey = cleanKey.charAt(0).toLowerCase() + cleanKey.slice(1)
|
||||||
|
inputFields[properKey] = value
|
||||||
|
} else {
|
||||||
inputFields[key] = value
|
inputFields[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also extract from nested parameters object if it exists
|
||||||
|
if (step.parameters && typeof step.parameters === 'object') {
|
||||||
|
Object.assign(inputFields, step.parameters)
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve input data using JSONPath
|
// Resolve input data using JSONPath
|
||||||
const resolvedInput = this.resolveStepInput(inputFields, context)
|
const resolvedInput = this.resolveStepInput(inputFields, context)
|
||||||
@@ -400,6 +432,95 @@ export class WorkflowExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts detailed error information from job logs and input
|
||||||
|
*/
|
||||||
|
private extractErrorDetailsFromJob(job: any, stepContext: any, stepName: string) {
|
||||||
|
try {
|
||||||
|
// Get error information from multiple sources
|
||||||
|
const input = stepContext.input || {}
|
||||||
|
const logs = job.log || []
|
||||||
|
const latestLog = logs[logs.length - 1]
|
||||||
|
|
||||||
|
// Extract error message from job error or log
|
||||||
|
const errorMessage = job.error?.message || latestLog?.error?.message || 'Unknown error'
|
||||||
|
|
||||||
|
// For timeout scenarios, check if it's a timeout based on duration and timeout setting
|
||||||
|
let errorType = this.classifyErrorType(errorMessage)
|
||||||
|
|
||||||
|
// Special handling for HTTP timeouts - if task failed and duration exceeds timeout, it's likely a timeout
|
||||||
|
if (errorType === 'unknown' && input.timeout && stepContext.executionInfo?.duration) {
|
||||||
|
const timeoutMs = parseInt(input.timeout) || 30000
|
||||||
|
const actualDuration = stepContext.executionInfo.duration
|
||||||
|
|
||||||
|
// If execution duration is close to or exceeds timeout, classify as timeout
|
||||||
|
if (actualDuration >= (timeoutMs * 0.9)) { // 90% of timeout threshold
|
||||||
|
errorType = 'timeout'
|
||||||
|
this.logger.debug({
|
||||||
|
timeoutMs,
|
||||||
|
actualDuration,
|
||||||
|
stepName
|
||||||
|
}, 'Classified error as timeout based on duration analysis')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate duration from execution info if available
|
||||||
|
const duration = stepContext.executionInfo?.duration || 0
|
||||||
|
|
||||||
|
// Extract attempt count from logs
|
||||||
|
const attempts = job.totalTried || 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
stepId: `${stepName}-${Date.now()}`,
|
||||||
|
errorType,
|
||||||
|
duration,
|
||||||
|
attempts,
|
||||||
|
finalError: errorMessage,
|
||||||
|
context: {
|
||||||
|
url: input.url,
|
||||||
|
method: input.method,
|
||||||
|
timeout: input.timeout,
|
||||||
|
statusCode: latestLog?.output?.status,
|
||||||
|
headers: input.headers
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn({
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
stepName
|
||||||
|
}, 'Failed to extract error details from job')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a condition value (string literal, number, boolean, or JSONPath)
|
||||||
|
*/
|
||||||
|
private parseConditionValue(expr: string, context: ExecutionContext): any {
|
||||||
|
// Handle string literals
|
||||||
|
if ((expr.startsWith('"') && expr.endsWith('"')) || (expr.startsWith("'") && expr.endsWith("'"))) {
|
||||||
|
return expr.slice(1, -1) // Remove quotes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle boolean literals
|
||||||
|
if (expr === 'true') {return true}
|
||||||
|
if (expr === 'false') {return false}
|
||||||
|
|
||||||
|
// Handle number literals
|
||||||
|
if (/^-?\d+(?:\.\d+)?$/.test(expr)) {
|
||||||
|
return Number(expr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JSONPath expressions
|
||||||
|
if (expr.startsWith('$')) {
|
||||||
|
return this.resolveJSONPathValue(expr, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as string if nothing else matches
|
||||||
|
return expr
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve step execution order based on dependencies
|
* Resolve step execution order based on dependencies
|
||||||
*/
|
*/
|
||||||
@@ -457,6 +578,22 @@ export class WorkflowExecutor {
|
|||||||
return executionBatches
|
return executionBatches
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a JSONPath value from the context
|
||||||
|
*/
|
||||||
|
private resolveJSONPathValue(expr: string, context: ExecutionContext): any {
|
||||||
|
if (expr.startsWith('$')) {
|
||||||
|
const result = JSONPath({
|
||||||
|
json: context,
|
||||||
|
path: expr,
|
||||||
|
wrap: false
|
||||||
|
})
|
||||||
|
// Return first result if array, otherwise the result itself
|
||||||
|
return Array.isArray(result) && result.length > 0 ? result[0] : result
|
||||||
|
}
|
||||||
|
return expr
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve step input using JSONPath expressions
|
* Resolve step input using JSONPath expressions
|
||||||
*/
|
*/
|
||||||
@@ -537,11 +674,11 @@ export class WorkflowExecutor {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seen.has(value as object)) {
|
if (seen.has(value)) {
|
||||||
return '[Circular Reference]'
|
return '[Circular Reference]'
|
||||||
}
|
}
|
||||||
|
|
||||||
seen.add(value as object)
|
seen.add(value)
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value.map(serialize)
|
return value.map(serialize)
|
||||||
@@ -567,87 +704,6 @@ export class WorkflowExecutor {
|
|||||||
return serialize(obj)
|
return serialize(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts detailed error information from job logs and input
|
|
||||||
*/
|
|
||||||
private extractErrorDetailsFromJob(job: any, stepContext: any, stepName: string) {
|
|
||||||
try {
|
|
||||||
// Get error information from multiple sources
|
|
||||||
const input = stepContext.input || {}
|
|
||||||
const logs = job.log || []
|
|
||||||
const latestLog = logs[logs.length - 1]
|
|
||||||
|
|
||||||
// Extract error message from job error or log
|
|
||||||
const errorMessage = job.error?.message || latestLog?.error?.message || 'Unknown error'
|
|
||||||
|
|
||||||
// For timeout scenarios, check if it's a timeout based on duration and timeout setting
|
|
||||||
let errorType = this.classifyErrorType(errorMessage)
|
|
||||||
|
|
||||||
// Special handling for HTTP timeouts - if task failed and duration exceeds timeout, it's likely a timeout
|
|
||||||
if (errorType === 'unknown' && input.timeout && stepContext.executionInfo?.duration) {
|
|
||||||
const timeoutMs = parseInt(input.timeout) || 30000
|
|
||||||
const actualDuration = stepContext.executionInfo.duration
|
|
||||||
|
|
||||||
// If execution duration is close to or exceeds timeout, classify as timeout
|
|
||||||
if (actualDuration >= (timeoutMs * 0.9)) { // 90% of timeout threshold
|
|
||||||
errorType = 'timeout'
|
|
||||||
this.logger.debug({
|
|
||||||
timeoutMs,
|
|
||||||
actualDuration,
|
|
||||||
stepName
|
|
||||||
}, 'Classified error as timeout based on duration analysis')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate duration from execution info if available
|
|
||||||
const duration = stepContext.executionInfo?.duration || 0
|
|
||||||
|
|
||||||
// Extract attempt count from logs
|
|
||||||
const attempts = job.totalTried || 1
|
|
||||||
|
|
||||||
return {
|
|
||||||
stepId: `${stepName}-${Date.now()}`,
|
|
||||||
errorType,
|
|
||||||
duration,
|
|
||||||
attempts,
|
|
||||||
finalError: errorMessage,
|
|
||||||
context: {
|
|
||||||
url: input.url,
|
|
||||||
method: input.method,
|
|
||||||
timeout: input.timeout,
|
|
||||||
statusCode: latestLog?.output?.status,
|
|
||||||
headers: input.headers
|
|
||||||
},
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn({
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
stepName
|
|
||||||
}, 'Failed to extract error details from job')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Classifies error types based on error messages
|
|
||||||
*/
|
|
||||||
private classifyErrorType(errorMessage: string): string {
|
|
||||||
if (errorMessage.includes('timeout') || errorMessage.includes('ETIMEDOUT')) {
|
|
||||||
return 'timeout'
|
|
||||||
}
|
|
||||||
if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('getaddrinfo')) {
|
|
||||||
return 'dns'
|
|
||||||
}
|
|
||||||
if (errorMessage.includes('ECONNREFUSED') || errorMessage.includes('ECONNRESET')) {
|
|
||||||
return 'connection'
|
|
||||||
}
|
|
||||||
if (errorMessage.includes('network') || errorMessage.includes('fetch')) {
|
|
||||||
return 'network'
|
|
||||||
}
|
|
||||||
return 'unknown'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update workflow run with current context
|
* Update workflow run with current context
|
||||||
*/
|
*/
|
||||||
@@ -719,24 +775,24 @@ export class WorkflowExecutor {
|
|||||||
// Perform comparison
|
// Perform comparison
|
||||||
let result: boolean
|
let result: boolean
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case '==':
|
|
||||||
result = leftValue === rightValue
|
|
||||||
break
|
|
||||||
case '!=':
|
case '!=':
|
||||||
result = leftValue !== rightValue
|
result = leftValue !== rightValue
|
||||||
break
|
break
|
||||||
case '>':
|
|
||||||
result = Number(leftValue) > Number(rightValue)
|
|
||||||
break
|
|
||||||
case '<':
|
case '<':
|
||||||
result = Number(leftValue) < Number(rightValue)
|
result = Number(leftValue) < Number(rightValue)
|
||||||
break
|
break
|
||||||
case '>=':
|
|
||||||
result = Number(leftValue) >= Number(rightValue)
|
|
||||||
break
|
|
||||||
case '<=':
|
case '<=':
|
||||||
result = Number(leftValue) <= Number(rightValue)
|
result = Number(leftValue) <= Number(rightValue)
|
||||||
break
|
break
|
||||||
|
case '==':
|
||||||
|
result = leftValue === rightValue
|
||||||
|
break
|
||||||
|
case '>':
|
||||||
|
result = Number(leftValue) > Number(rightValue)
|
||||||
|
break
|
||||||
|
case '>=':
|
||||||
|
result = Number(leftValue) >= Number(rightValue)
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown comparison operator: ${operator}`)
|
throw new Error(`Unknown comparison operator: ${operator}`)
|
||||||
}
|
}
|
||||||
@@ -793,49 +849,6 @@ export class WorkflowExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a JSONPath value from the context
|
|
||||||
*/
|
|
||||||
private resolveJSONPathValue(expr: string, context: ExecutionContext): any {
|
|
||||||
if (expr.startsWith('$')) {
|
|
||||||
const result = JSONPath({
|
|
||||||
json: context,
|
|
||||||
path: expr,
|
|
||||||
wrap: false
|
|
||||||
})
|
|
||||||
// Return first result if array, otherwise the result itself
|
|
||||||
return Array.isArray(result) && result.length > 0 ? result[0] : result
|
|
||||||
}
|
|
||||||
return expr
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a condition value (string literal, number, boolean, or JSONPath)
|
|
||||||
*/
|
|
||||||
private parseConditionValue(expr: string, context: ExecutionContext): any {
|
|
||||||
// Handle string literals
|
|
||||||
if ((expr.startsWith('"') && expr.endsWith('"')) || (expr.startsWith("'") && expr.endsWith("'"))) {
|
|
||||||
return expr.slice(1, -1) // Remove quotes
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle boolean literals
|
|
||||||
if (expr === 'true') return true
|
|
||||||
if (expr === 'false') return false
|
|
||||||
|
|
||||||
// Handle number literals
|
|
||||||
if (/^-?\d+(\.\d+)?$/.test(expr)) {
|
|
||||||
return Number(expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle JSONPath expressions
|
|
||||||
if (expr.startsWith('$')) {
|
|
||||||
return this.resolveJSONPathValue(expr, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return as string if nothing else matches
|
|
||||||
return expr
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a workflow with the given context
|
* Execute a workflow with the given context
|
||||||
*/
|
*/
|
||||||
@@ -977,148 +990,4 @@ export class WorkflowExecutor {
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find and execute workflows triggered by a collection operation
|
|
||||||
*/
|
|
||||||
async executeTriggeredWorkflows(
|
|
||||||
collection: string,
|
|
||||||
operation: 'create' | 'delete' | 'read' | 'update',
|
|
||||||
doc: unknown,
|
|
||||||
previousDoc: unknown,
|
|
||||||
req: PayloadRequest
|
|
||||||
): Promise<void> {
|
|
||||||
|
|
||||||
this.logger.info({
|
|
||||||
collection,
|
|
||||||
operation,
|
|
||||||
docId: (doc as any)?.id
|
|
||||||
}, 'executeTriggeredWorkflows called')
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Find workflows with matching triggers
|
|
||||||
const workflows = await this.payload.find({
|
|
||||||
collection: 'workflows',
|
|
||||||
depth: 2, // Include steps and triggers
|
|
||||||
limit: 100,
|
|
||||||
req
|
|
||||||
})
|
|
||||||
|
|
||||||
this.logger.info({
|
|
||||||
workflowCount: workflows.docs.length
|
|
||||||
}, 'Found workflows to check')
|
|
||||||
|
|
||||||
for (const workflow of workflows.docs) {
|
|
||||||
// Check if this workflow has a matching trigger
|
|
||||||
const triggers = workflow.triggers as Array<{
|
|
||||||
condition?: string
|
|
||||||
type: string
|
|
||||||
parameters?: {
|
|
||||||
collection?: string
|
|
||||||
collectionSlug?: string
|
|
||||||
operation?: string
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
|
|
||||||
this.logger.debug({
|
|
||||||
workflowId: workflow.id,
|
|
||||||
workflowName: workflow.name,
|
|
||||||
triggerCount: triggers?.length || 0
|
|
||||||
}, 'Checking workflow triggers')
|
|
||||||
|
|
||||||
const matchingTriggers = triggers?.filter(trigger =>
|
|
||||||
trigger.type === 'collection-trigger' &&
|
|
||||||
(trigger.parameters?.collection === collection || trigger.parameters?.collectionSlug === collection) &&
|
|
||||||
trigger.parameters?.operation === operation
|
|
||||||
) || []
|
|
||||||
|
|
||||||
this.logger.info({
|
|
||||||
workflowId: workflow.id,
|
|
||||||
workflowName: workflow.name,
|
|
||||||
matchingTriggerCount: matchingTriggers.length,
|
|
||||||
targetCollection: collection,
|
|
||||||
targetOperation: operation
|
|
||||||
}, 'Matching triggers found')
|
|
||||||
|
|
||||||
for (const trigger of matchingTriggers) {
|
|
||||||
this.logger.info({
|
|
||||||
workflowId: workflow.id,
|
|
||||||
workflowName: workflow.name,
|
|
||||||
triggerDetails: {
|
|
||||||
type: trigger.type,
|
|
||||||
collection: trigger.parameters?.collection,
|
|
||||||
collectionSlug: trigger.parameters?.collectionSlug,
|
|
||||||
operation: trigger.parameters?.operation,
|
|
||||||
hasCondition: !!trigger.condition
|
|
||||||
}
|
|
||||||
}, 'Processing matching trigger - about to execute workflow')
|
|
||||||
|
|
||||||
// Create execution context for condition evaluation
|
|
||||||
const context: ExecutionContext = {
|
|
||||||
steps: {},
|
|
||||||
trigger: {
|
|
||||||
type: 'collection',
|
|
||||||
collection,
|
|
||||||
doc,
|
|
||||||
operation,
|
|
||||||
previousDoc,
|
|
||||||
req
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check trigger condition if present
|
|
||||||
if (trigger.condition) {
|
|
||||||
this.logger.debug({
|
|
||||||
collection,
|
|
||||||
operation,
|
|
||||||
condition: trigger.condition,
|
|
||||||
workflowId: workflow.id,
|
|
||||||
workflowName: workflow.name
|
|
||||||
}, 'Evaluating trigger condition')
|
|
||||||
|
|
||||||
const conditionMet = this.evaluateCondition(trigger.condition, context)
|
|
||||||
|
|
||||||
if (!conditionMet) {
|
|
||||||
this.logger.info({
|
|
||||||
collection,
|
|
||||||
condition: trigger.condition,
|
|
||||||
operation,
|
|
||||||
workflowId: workflow.id,
|
|
||||||
workflowName: workflow.name,
|
|
||||||
docSnapshot: JSON.stringify(doc).substring(0, 200)
|
|
||||||
}, 'Trigger condition not met, skipping workflow')
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.info({
|
|
||||||
collection,
|
|
||||||
condition: trigger.condition,
|
|
||||||
operation,
|
|
||||||
workflowId: workflow.id,
|
|
||||||
workflowName: workflow.name,
|
|
||||||
docSnapshot: JSON.stringify(doc).substring(0, 200)
|
|
||||||
}, 'Trigger condition met')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.info({
|
|
||||||
collection,
|
|
||||||
operation,
|
|
||||||
workflowId: workflow.id,
|
|
||||||
workflowName: workflow.name
|
|
||||||
}, 'Triggering workflow')
|
|
||||||
|
|
||||||
// Execute the workflow
|
|
||||||
await this.execute(workflow as PayloadWorkflow, context, req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error({ error: error instanceof Error ? error.message : 'Unknown error' }, 'Workflow execution failed')
|
|
||||||
this.logger.error({
|
|
||||||
collection,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
operation
|
|
||||||
}, 'Failed to execute triggered workflows')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
export { TriggerWorkflowButton } from '../components/TriggerWorkflowButton.js'
|
export { TriggerWorkflowButton } from '../components/TriggerWorkflowButton.js'
|
||||||
export { StatusCell } from '../components/StatusCell.js'
|
export { StatusCell } from '../components/StatusCell.js'
|
||||||
export { ErrorDisplay } from '../components/ErrorDisplay.js'
|
export { ErrorDisplay } from '../components/ErrorDisplay.js'
|
||||||
export { WorkflowExecutionStatus } from '../components/WorkflowExecutionStatus.js'
|
|
||||||
|
|
||||||
// Future client components can be added here:
|
// Future client components can be added here:
|
||||||
// export { default as WorkflowDashboard } from '../components/WorkflowDashboard/index.js'
|
// export { default as WorkflowDashboard } from '../components/WorkflowDashboard/index.js'
|
||||||
|
|||||||
@@ -1,30 +1,14 @@
|
|||||||
import type {Field} from "payload"
|
import type {Field} from "payload"
|
||||||
|
|
||||||
import type {Trigger} from "./types.js"
|
|
||||||
|
|
||||||
type Options = {
|
export const parameter = (slug: string, field: {name: string} & Field): Field => ({
|
||||||
slug: string,
|
|
||||||
fields?: ({name: string} & Field)[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const trigger = ({
|
|
||||||
slug,
|
|
||||||
fields
|
|
||||||
}: Options): Trigger => {
|
|
||||||
return {
|
|
||||||
slug,
|
|
||||||
fields: (fields || []).map(f => triggerField(slug, f))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const triggerField = (slug: string, field: {name: string} & Field): Field => ({
|
|
||||||
...field,
|
...field,
|
||||||
name: '__trigger_' + field.name,
|
name: 'parameter' + field.name.replace(/^\w/, c => c.toUpperCase()),
|
||||||
admin: {
|
admin: {
|
||||||
...(field.admin as unknown || {}),
|
...(field.admin as unknown || {}),
|
||||||
condition: (_, siblingData, __) => {
|
condition: (_, siblingData, __) => {
|
||||||
const previous = field.admin?.condition?.call(null, _, siblingData, __)
|
const previous = field.admin?.condition?.call(null, _, siblingData, __)
|
||||||
return previous || (siblingData?.type === slug)
|
return (previous === undefined || previous) && (siblingData?.type === slug)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
60
src/plugin/collection-hook.ts
Normal file
60
src/plugin/collection-hook.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import {WorkflowExecutor} from "../core/workflow-executor.js"
|
||||||
|
|
||||||
|
export const createCollectionTriggerHook = (collectionSlug: string, hookType: string) => {
|
||||||
|
return async (args: HookArgs) => {
|
||||||
|
const req = 'req' in args ? args.req :
|
||||||
|
'args' in args ? args.args.req :
|
||||||
|
undefined
|
||||||
|
if (!req) {
|
||||||
|
throw new Error('No request object found in hook arguments')
|
||||||
|
}
|
||||||
|
const payload = req.payload
|
||||||
|
const {docs: workflows} = await payload.find({
|
||||||
|
collection: 'workflows',
|
||||||
|
depth: 2,
|
||||||
|
limit: 100,
|
||||||
|
where: {
|
||||||
|
'triggers.parameters.collectionSlug': {
|
||||||
|
equals: collectionSlug
|
||||||
|
},
|
||||||
|
'triggers.parameters.hook': {
|
||||||
|
equals: hookType
|
||||||
|
},
|
||||||
|
'triggers.type': {
|
||||||
|
equals: 'collection-hook'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const executor = new WorkflowExecutor(payload, payload.logger)
|
||||||
|
// invoke each workflow
|
||||||
|
for (const workflow of workflows) {
|
||||||
|
// Create execution context
|
||||||
|
const context = {
|
||||||
|
steps: {},
|
||||||
|
trigger: {
|
||||||
|
...args,
|
||||||
|
type: 'collection',
|
||||||
|
collection: collectionSlug,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
await executor.execute(workflow as any, context, req)
|
||||||
|
payload.logger.info({
|
||||||
|
workflowId: workflow.id,
|
||||||
|
collection: collectionSlug,
|
||||||
|
hookType
|
||||||
|
}, 'Workflow executed successfully')
|
||||||
|
} catch (error) {
|
||||||
|
payload.logger.error({
|
||||||
|
workflowId: workflow.id,
|
||||||
|
collection: collectionSlug,
|
||||||
|
hookType,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
}, 'Workflow execution failed')
|
||||||
|
// Don't throw to prevent breaking the original operation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,17 @@
|
|||||||
import type {TaskConfig} from "payload"
|
import type {CollectionConfig, TaskConfig} from "payload"
|
||||||
|
|
||||||
import type {Trigger} from "../triggers/types.js"
|
import type {Trigger} from "../triggers/types.js"
|
||||||
|
|
||||||
export type CollectionTriggerConfigCrud = {
|
export type TriggerConfig = (config: WorkflowsPluginConfig) => Trigger
|
||||||
create?: true
|
|
||||||
delete?: true
|
|
||||||
read?: true
|
|
||||||
update?: true
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CollectionTriggerConfig = CollectionTriggerConfigCrud | true
|
export type WorkflowsPluginConfig<TSlug extends string = string> = {
|
||||||
|
collectionTriggers?: {
|
||||||
export type TriggerConfig = <T extends string>(config: WorkflowsPluginConfig<T>) => Trigger
|
[key in TSlug]?: {
|
||||||
|
[key in keyof CollectionConfig['hooks']]?: true
|
||||||
export type WorkflowsPluginConfig<TSlug extends string> = {
|
} | true
|
||||||
collectionTriggers: {
|
|
||||||
[key in TSlug]?: CollectionTriggerConfig
|
|
||||||
}
|
}
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
steps: TaskConfig<string>[],
|
steps: TaskConfig<string>[]
|
||||||
triggers?: TriggerConfig[]
|
triggers?: TriggerConfig[]
|
||||||
webhookPrefix?: string
|
webhookPrefix?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import type {
|
import type {CollectionConfig, Config} from 'payload'
|
||||||
CollectionAfterChangeHook,
|
|
||||||
Config,
|
|
||||||
PayloadRequest,
|
|
||||||
TypeWithID
|
|
||||||
} from 'payload'
|
|
||||||
|
|
||||||
import type {WorkflowsPluginConfig} from "./config-types.js"
|
import type {WorkflowsPluginConfig} from "./config-types.js"
|
||||||
|
|
||||||
@@ -15,98 +10,10 @@ import {initStepTasks} from "./init-step-tasks.js"
|
|||||||
import {initWebhookEndpoint} from "./init-webhook.js"
|
import {initWebhookEndpoint} from "./init-webhook.js"
|
||||||
import {initWorkflowHooks} from './init-workflow-hooks.js'
|
import {initWorkflowHooks} from './init-workflow-hooks.js'
|
||||||
import {getConfigLogger, initializeLogger} from './logger.js'
|
import {getConfigLogger, initializeLogger} from './logger.js'
|
||||||
|
import {createCollectionTriggerHook} from "./collection-hook.js"
|
||||||
|
|
||||||
export {getLogger} from './logger.js'
|
export {getLogger} from './logger.js'
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to create failed workflow runs for tracking errors
|
|
||||||
*/
|
|
||||||
const createFailedWorkflowRun = async (
|
|
||||||
collectionSlug: string,
|
|
||||||
operation: string,
|
|
||||||
doc: TypeWithID,
|
|
||||||
previousDoc: TypeWithID,
|
|
||||||
req: PayloadRequest,
|
|
||||||
errorMessage: string
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const logger = req?.payload?.logger || console
|
|
||||||
|
|
||||||
// Only create failed workflow runs if we have a payload instance
|
|
||||||
if (!req?.payload || !collectionSlug) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find workflows that should have been triggered
|
|
||||||
const workflows = await req.payload.find({
|
|
||||||
collection: 'workflows',
|
|
||||||
limit: 10,
|
|
||||||
req,
|
|
||||||
where: {
|
|
||||||
'triggers.parameters.collectionSlug': {
|
|
||||||
equals: collectionSlug
|
|
||||||
},
|
|
||||||
'triggers.parameters.operation': {
|
|
||||||
equals: operation
|
|
||||||
},
|
|
||||||
'triggers.type': {
|
|
||||||
equals: 'collection'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Create failed workflow runs for each matching workflow
|
|
||||||
for (const workflow of workflows.docs) {
|
|
||||||
await req.payload.create({
|
|
||||||
collection: 'workflow-runs',
|
|
||||||
data: {
|
|
||||||
completedAt: new Date().toISOString(),
|
|
||||||
context: {
|
|
||||||
steps: {},
|
|
||||||
trigger: {
|
|
||||||
type: 'collection',
|
|
||||||
collection: collectionSlug,
|
|
||||||
doc,
|
|
||||||
operation,
|
|
||||||
previousDoc,
|
|
||||||
triggeredAt: new Date().toISOString()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: `Hook execution failed: ${errorMessage}`,
|
|
||||||
inputs: {},
|
|
||||||
logs: [{
|
|
||||||
level: 'error',
|
|
||||||
message: `Hook execution failed: ${errorMessage}`,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
}],
|
|
||||||
outputs: {},
|
|
||||||
startedAt: new Date().toISOString(),
|
|
||||||
status: 'failed',
|
|
||||||
steps: [],
|
|
||||||
triggeredBy: req?.user?.email || 'system',
|
|
||||||
workflow: workflow.id,
|
|
||||||
workflowVersion: 1
|
|
||||||
},
|
|
||||||
req
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (workflows.docs.length > 0) {
|
|
||||||
logger.info({
|
|
||||||
errorMessage,
|
|
||||||
workflowCount: workflows.docs.length
|
|
||||||
}, 'Created failed workflow runs for hook execution error')
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// Don't let workflow run creation failures break the original operation
|
|
||||||
const logger = req?.payload?.logger || console
|
|
||||||
logger.warn({
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error'
|
|
||||||
}, 'Failed to create failed workflow run record')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const applyCollectionsConfig = <T extends string>(pluginOptions: WorkflowsPluginConfig<T>, config: Config) => {
|
const applyCollectionsConfig = <T extends string>(pluginOptions: WorkflowsPluginConfig<T>, config: Config) => {
|
||||||
// Add workflow collections
|
// Add workflow collections
|
||||||
if (!config.collections) {
|
if (!config.collections) {
|
||||||
@@ -119,70 +26,16 @@ const applyCollectionsConfig = <T extends string>(pluginOptions: WorkflowsPlugin
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
type AnyHook =
|
||||||
* Create a collection hook that executes workflows
|
CollectionConfig['hooks'] extends infer H
|
||||||
*/
|
? H extends Record<string, unknown>
|
||||||
const createAutomationHook = <T extends TypeWithID>(): CollectionAfterChangeHook<T> => {
|
? NonNullable<H[keyof H]> extends (infer U)[]
|
||||||
return async function payloadAutomationHook(args) {
|
? U
|
||||||
const logger = args.req?.payload?.logger || console
|
: never
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
|
||||||
try {
|
type HookArgs = Parameters<AnyHook>[0]
|
||||||
logger.info({
|
|
||||||
collection: args.collection?.slug,
|
|
||||||
docId: args.doc?.id,
|
|
||||||
hookType: 'automation',
|
|
||||||
operation: args.operation
|
|
||||||
}, 'Collection automation hook triggered')
|
|
||||||
|
|
||||||
// Create executor on-demand
|
|
||||||
const executor = new WorkflowExecutor(args.req.payload, logger)
|
|
||||||
|
|
||||||
logger.debug('Executing triggered workflows...')
|
|
||||||
await executor.executeTriggeredWorkflows(
|
|
||||||
args.collection.slug,
|
|
||||||
args.operation,
|
|
||||||
args.doc,
|
|
||||||
args.previousDoc,
|
|
||||||
args.req
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info({
|
|
||||||
collection: args.collection?.slug,
|
|
||||||
docId: args.doc?.id,
|
|
||||||
operation: args.operation
|
|
||||||
}, 'Workflow execution completed successfully')
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
|
||||||
|
|
||||||
logger.error({
|
|
||||||
collection: args.collection?.slug,
|
|
||||||
docId: args.doc?.id,
|
|
||||||
error: errorMessage,
|
|
||||||
errorStack: error instanceof Error ? error.stack : undefined,
|
|
||||||
operation: args.operation
|
|
||||||
}, 'Hook execution failed')
|
|
||||||
|
|
||||||
// Create a failed workflow run to track this error
|
|
||||||
try {
|
|
||||||
await createFailedWorkflowRun(
|
|
||||||
args.collection.slug,
|
|
||||||
args.operation,
|
|
||||||
args.doc,
|
|
||||||
args.previousDoc,
|
|
||||||
args.req,
|
|
||||||
errorMessage
|
|
||||||
)
|
|
||||||
} catch (createError) {
|
|
||||||
logger.error({
|
|
||||||
error: createError instanceof Error ? createError.message : 'Unknown error'
|
|
||||||
}, 'Failed to create workflow run for hook error')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't throw to prevent breaking the original operation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const workflowsPlugin =
|
export const workflowsPlugin =
|
||||||
<TSlug extends string>(pluginOptions: WorkflowsPluginConfig<TSlug>) =>
|
<TSlug extends string>(pluginOptions: WorkflowsPluginConfig<TSlug>) =>
|
||||||
@@ -199,13 +52,15 @@ export const workflowsPlugin =
|
|||||||
const logger = getConfigLogger()
|
const logger = getConfigLogger()
|
||||||
|
|
||||||
if (config.collections && pluginOptions.collectionTriggers) {
|
if (config.collections && pluginOptions.collectionTriggers) {
|
||||||
for (const [triggerSlug, triggerConfig] of Object.entries(pluginOptions.collectionTriggers)) {
|
for (const [collectionSlug, triggerConfig] of Object.entries(pluginOptions.collectionTriggers)) {
|
||||||
if (!triggerConfig) {continue}
|
if (!triggerConfig) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Find the collection config that matches
|
// Find the collection config that matches
|
||||||
const collectionIndex = config.collections.findIndex(c => c.slug === triggerSlug)
|
const collectionIndex = config.collections.findIndex(c => c.slug === collectionSlug)
|
||||||
if (collectionIndex === -1) {
|
if (collectionIndex === -1) {
|
||||||
logger.warn(`Collection '${triggerSlug}' not found in config.collections`)
|
logger.warn(`Collection '${collectionSlug}' not found in config.collections`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,19 +70,47 @@ export const workflowsPlugin =
|
|||||||
if (!collection.hooks) {
|
if (!collection.hooks) {
|
||||||
collection.hooks = {}
|
collection.hooks = {}
|
||||||
}
|
}
|
||||||
if (!collection.hooks.afterChange) {
|
|
||||||
collection.hooks.afterChange = []
|
// Determine which hooks to register based on config
|
||||||
|
const hooksToRegister = triggerConfig === true
|
||||||
|
? {
|
||||||
|
afterChange: true,
|
||||||
|
afterDelete: true,
|
||||||
|
afterRead: true,
|
||||||
|
}
|
||||||
|
: triggerConfig
|
||||||
|
|
||||||
|
// Register each configured hook
|
||||||
|
Object.entries(hooksToRegister).forEach(([hookName, enabled]) => {
|
||||||
|
if (!enabled) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the hook to the collection config
|
const hookKey = hookName as keyof typeof collection.hooks
|
||||||
const automationHook = createAutomationHook()
|
|
||||||
|
// Initialize the hook array if needed
|
||||||
|
if (!collection.hooks![hookKey]) {
|
||||||
|
collection.hooks![hookKey] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the automation hook for this specific collection and hook type
|
||||||
|
const automationHook = createCollectionTriggerHook(collectionSlug, hookKey)
|
||||||
|
|
||||||
// Mark it for debugging
|
// Mark it for debugging
|
||||||
Object.defineProperty(automationHook, '__isAutomationHook', {
|
Object.defineProperty(automationHook, '__isAutomationHook', {
|
||||||
value: true,
|
value: true,
|
||||||
enumerable: false
|
enumerable: false
|
||||||
})
|
})
|
||||||
|
Object.defineProperty(automationHook, '__hookType', {
|
||||||
|
value: hookKey,
|
||||||
|
enumerable: false
|
||||||
|
})
|
||||||
|
|
||||||
collection.hooks.afterChange.push(automationHook)
|
// Add the hook to the collection
|
||||||
|
;(collection.hooks![hookKey] as Array<unknown>).push(automationHook)
|
||||||
|
|
||||||
|
logger.debug(`Registered ${hookKey} hook for collection '${collectionSlug}'`)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
import type {Payload} from "payload"
|
|
||||||
import type {Logger} from "pino"
|
|
||||||
|
|
||||||
import type { WorkflowExecutor } from "../core/workflow-executor.js"
|
|
||||||
import type {CollectionTriggerConfigCrud, WorkflowsPluginConfig} from "./config-types.js"
|
|
||||||
|
|
||||||
export function initCollectionHooks<T extends string>(pluginOptions: WorkflowsPluginConfig<T>, payload: Payload, logger: Payload['logger'], executor: WorkflowExecutor) {
|
|
||||||
|
|
||||||
if (!pluginOptions.collectionTriggers || Object.keys(pluginOptions.collectionTriggers).length === 0) {
|
|
||||||
logger.warn('No collection triggers configured in plugin options')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info({
|
|
||||||
configuredCollections: Object.keys(pluginOptions.collectionTriggers),
|
|
||||||
availableCollections: Object.keys(payload.collections)
|
|
||||||
}, 'Starting collection hook registration')
|
|
||||||
|
|
||||||
// Add hooks to configured collections
|
|
||||||
for (const [collectionSlug, triggerConfig] of Object.entries(pluginOptions.collectionTriggers)) {
|
|
||||||
if (!triggerConfig) {
|
|
||||||
logger.debug({collectionSlug}, 'Skipping collection with falsy trigger config')
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const collection = payload.collections[collectionSlug as T]
|
|
||||||
const crud: CollectionTriggerConfigCrud = triggerConfig === true ? {
|
|
||||||
create: true,
|
|
||||||
delete: true,
|
|
||||||
read: true,
|
|
||||||
update: true,
|
|
||||||
} : triggerConfig
|
|
||||||
|
|
||||||
if (!collection.config.hooks) {
|
|
||||||
collection.config.hooks = {} as typeof collection.config.hooks
|
|
||||||
}
|
|
||||||
|
|
||||||
if (crud.update || crud.create) {
|
|
||||||
collection.config.hooks.afterChange = collection.config.hooks.afterChange || []
|
|
||||||
collection.config.hooks.afterChange.push(async (change) => {
|
|
||||||
const operation = change.operation as 'create' | 'update'
|
|
||||||
|
|
||||||
|
|
||||||
logger.info({
|
|
||||||
slug: change.collection.slug,
|
|
||||||
operation,
|
|
||||||
docId: change.doc?.id,
|
|
||||||
previousDocId: change.previousDoc?.id,
|
|
||||||
hasExecutor: !!executor,
|
|
||||||
executorType: typeof executor
|
|
||||||
}, 'Collection automation hook triggered')
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
// Execute workflows for this trigger
|
|
||||||
await executor.executeTriggeredWorkflows(
|
|
||||||
change.collection.slug,
|
|
||||||
operation,
|
|
||||||
change.doc,
|
|
||||||
change.previousDoc,
|
|
||||||
change.req
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
logger.info({
|
|
||||||
slug: change.collection.slug,
|
|
||||||
operation,
|
|
||||||
docId: change.doc?.id
|
|
||||||
}, 'Workflow execution completed successfully')
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
logger.error({
|
|
||||||
slug: change.collection.slug,
|
|
||||||
operation,
|
|
||||||
docId: change.doc?.id,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
stack: error instanceof Error ? error.stack : undefined
|
|
||||||
}, 'AUTOMATION PLUGIN: executeTriggeredWorkflows failed')
|
|
||||||
// Don't re-throw to avoid breaking other hooks
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (crud.read) {
|
|
||||||
collection.config.hooks.afterRead = collection.config.hooks.afterRead || []
|
|
||||||
collection.config.hooks.afterRead.push(async (change) => {
|
|
||||||
logger.debug({
|
|
||||||
slug: change.collection.slug,
|
|
||||||
operation: 'read',
|
|
||||||
}, 'Collection hook triggered')
|
|
||||||
|
|
||||||
// Execute workflows for this trigger
|
|
||||||
await executor.executeTriggeredWorkflows(
|
|
||||||
change.collection.slug,
|
|
||||||
'read',
|
|
||||||
change.doc,
|
|
||||||
undefined,
|
|
||||||
change.req
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (crud.delete) {
|
|
||||||
collection.config.hooks.afterDelete = collection.config.hooks.afterDelete || []
|
|
||||||
collection.config.hooks.afterDelete.push(async (change) => {
|
|
||||||
logger.debug({
|
|
||||||
slug: change.collection.slug,
|
|
||||||
operation: 'delete',
|
|
||||||
}, 'Collection hook triggered')
|
|
||||||
|
|
||||||
// Execute workflows for this trigger
|
|
||||||
await executor.executeTriggeredWorkflows(
|
|
||||||
change.collection.slug,
|
|
||||||
'delete',
|
|
||||||
change.doc,
|
|
||||||
undefined,
|
|
||||||
change.req
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collection) {
|
|
||||||
logger.info({
|
|
||||||
collectionSlug,
|
|
||||||
hooksRegistered: {
|
|
||||||
afterChange: crud.update || crud.create,
|
|
||||||
afterRead: crud.read,
|
|
||||||
afterDelete: crud.delete
|
|
||||||
}
|
|
||||||
}, 'Collection hooks registered successfully')
|
|
||||||
} else {
|
|
||||||
logger.error({
|
|
||||||
collectionSlug,
|
|
||||||
availableCollections: Object.keys(payload.collections)
|
|
||||||
}, 'Collection not found for trigger configuration - check collection slug spelling')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
36
src/triggers/collection-hook-trigger.ts
Normal file
36
src/triggers/collection-hook-trigger.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import type {TriggerConfig} from '../plugin/config-types.js'
|
||||||
|
|
||||||
|
export const collectionHookTrigger: TriggerConfig = ({collectionTriggers}) => ({
|
||||||
|
slug: 'collection-hook',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'collectionSlug',
|
||||||
|
type: 'select',
|
||||||
|
options: Object.keys(collectionTriggers || {}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hook',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
"afterChange",
|
||||||
|
"afterDelete",
|
||||||
|
"afterError",
|
||||||
|
"afterForgotPassword",
|
||||||
|
"afterLogin",
|
||||||
|
"afterLogout",
|
||||||
|
"afterMe",
|
||||||
|
"afterOperation",
|
||||||
|
"afterRead",
|
||||||
|
"afterRefresh",
|
||||||
|
"beforeChange",
|
||||||
|
"beforeDelete",
|
||||||
|
"beforeLogin",
|
||||||
|
"beforeOperation",
|
||||||
|
"beforeRead",
|
||||||
|
"beforeValidate",
|
||||||
|
"me",
|
||||||
|
"refresh"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import type {TriggerConfig} from '../plugin/config-types.js'
|
|
||||||
|
|
||||||
export const collectionTrigger: TriggerConfig = ({collectionTriggers}) => ({
|
|
||||||
slug: 'collection',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'collectionSlug',
|
|
||||||
type: 'select',
|
|
||||||
options: Object.keys(collectionTriggers || {}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'operation',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
'create',
|
|
||||||
'delete',
|
|
||||||
'read',
|
|
||||||
'update',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
@@ -2,7 +2,7 @@ import type {TriggerConfig} from '../plugin/config-types.js'
|
|||||||
|
|
||||||
export const globalTrigger: TriggerConfig = () => ({
|
export const globalTrigger: TriggerConfig = () => ({
|
||||||
slug: 'global',
|
slug: 'global',
|
||||||
fields: [
|
parameters: [
|
||||||
{
|
{
|
||||||
name: 'global',
|
name: 'global',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export { collectionTrigger } from './collection-trigger.js'
|
export { collectionHookTrigger } from './collection-hook-trigger.js'
|
||||||
export { globalTrigger } from './global-trigger.js'
|
export { globalTrigger } from './global-trigger.js'
|
||||||
export { webhookTrigger } from './webhook-trigger.js'
|
export { webhookTrigger } from './webhook-trigger.js'
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ import type {Field} from "payload"
|
|||||||
|
|
||||||
export type Trigger = {
|
export type Trigger = {
|
||||||
slug: string
|
slug: string
|
||||||
fields: Field[]
|
parameters: Field[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type {TriggerConfig} from '../plugin/config-types.js'
|
|||||||
|
|
||||||
export const webhookTrigger: TriggerConfig = () => ({
|
export const webhookTrigger: TriggerConfig = () => ({
|
||||||
slug: 'webhook',
|
slug: 'webhook',
|
||||||
fields: [
|
parameters: [
|
||||||
{
|
{
|
||||||
name: 'webhookPath',
|
name: 'webhookPath',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|||||||
Reference in New Issue
Block a user