From 0f741acf73093bc27dde70132e5815a46e835dfe Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Wed, 10 Sep 2025 17:36:56 +0200 Subject: [PATCH] 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 --- MIGRATION-v0.0.24.md | 187 ------- NOT-IMPLEMENTING.md | 101 ---- dev/app/(payload)/admin/importMap.js | 2 - eslint.config.js | 1 + src/collections/Workflow.ts | 116 ++-- src/components/WorkflowExecutionStatus.tsx | 15 +- src/core/workflow-executor.ts | 511 +++++++----------- src/exports/client.ts | 1 - .../helpers.ts => fields/parameter.ts} | 22 +- src/plugin/collection-hook.ts | 60 ++ src/plugin/config-types.ts | 23 +- src/plugin/index.ts | 229 ++------ src/plugin/init-collection-hooks.ts | 138 ----- src/triggers/collection-hook-trigger.ts | 36 ++ src/triggers/collection-trigger.ts | 22 - src/triggers/global-trigger.ts | 4 +- src/triggers/index.ts | 2 +- src/triggers/types.ts | 2 +- src/triggers/webhook-trigger.ts | 4 +- 19 files changed, 399 insertions(+), 1077 deletions(-) delete mode 100644 MIGRATION-v0.0.24.md delete mode 100644 NOT-IMPLEMENTING.md rename src/{triggers/helpers.ts => fields/parameter.ts} (56%) create mode 100644 src/plugin/collection-hook.ts delete mode 100644 src/plugin/init-collection-hooks.ts create mode 100644 src/triggers/collection-hook-trigger.ts delete mode 100644 src/triggers/collection-trigger.ts diff --git a/MIGRATION-v0.0.24.md b/MIGRATION-v0.0.24.md deleted file mode 100644 index 0d34374..0000000 --- a/MIGRATION-v0.0.24.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/NOT-IMPLEMENTING.md b/NOT-IMPLEMENTING.md deleted file mode 100644 index f374840..0000000 --- a/NOT-IMPLEMENTING.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/dev/app/(payload)/admin/importMap.js b/dev/app/(payload)/admin/importMap.js index f0e7177..54f346d 100644 --- a/dev/app/(payload)/admin/importMap.js +++ b/dev/app/(payload)/admin/importMap.js @@ -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 { ErrorDisplay as ErrorDisplay_6f365a93b6cb4b34ad564b391e21db6f } from '@xtr-dev/payload-automation/client' export const importMap = { - "@xtr-dev/payload-automation/client#WorkflowExecutionStatus": WorkflowExecutionStatus_6f365a93b6cb4b34ad564b391e21db6f, "@xtr-dev/payload-automation/client#StatusCell": StatusCell_6f365a93b6cb4b34ad564b391e21db6f, "@xtr-dev/payload-automation/client#ErrorDisplay": ErrorDisplay_6f365a93b6cb4b34ad564b391e21db6f } diff --git a/eslint.config.js b/eslint.config.js index 59916c2..5e32000 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -31,6 +31,7 @@ export default [ 'perfectionist/sort-object-types': 'off', 'perfectionist/sort-objects': 'off', 'perfectionist/sort-exports': 'off', + 'perfectionist/sort-imports': 'off' }, }, { diff --git a/src/collections/Workflow.ts b/src/collections/Workflow.ts index cd05848..3026c2d 100644 --- a/src/collections/Workflow.ts +++ b/src/collections/Workflow.ts @@ -1,23 +1,26 @@ -import type {CollectionConfig, Field} from 'payload' +import type {CollectionConfig} from 'payload' import type {WorkflowsPluginConfig} from "../plugin/config-types.js" +import {parameter} from "../fields/parameter.js" +import {collectionHookTrigger} from "../triggers/index.js" + export const createWorkflowCollection: (options: WorkflowsPluginConfig) => CollectionConfig = (options) => { - const {steps} = options - const triggers = (options.triggers || []).map(t => t(options)) + const steps = options.steps || [] + const triggers = (options.triggers || []).map(t => t(options)).concat(collectionHookTrigger(options)) return { slug: 'workflows', - access: { - create: () => true, + access: { + create: () => true, delete: () => true, read: () => true, update: () => true, - }, + }, admin: { defaultColumns: ['name', 'updatedAt'], - description: 'Create and manage automated workflows.', - group: 'Automation', - useAsTitle: 'name', + description: 'Create and manage automated workflows.', + group: 'Automation', + useAsTitle: 'name', }, fields: [ { @@ -35,16 +38,6 @@ export const createWorkflowCollection: (options: WorkflowsPlug 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', type: 'array', @@ -53,7 +46,7 @@ export const createWorkflowCollection: (options: WorkflowsPlug name: 'type', type: 'select', options: [ - ...(triggers || []).map(t => t.slug) + ...triggers.map(t => t.slug) ] }, { @@ -64,6 +57,8 @@ export const createWorkflowCollection: (options: WorkflowsPlug }, defaultValue: {} }, + // Virtual fields for custom triggers + ...triggers.flatMap(t => (t.parameters || []).map(p => parameter(t.slug, p as any))), { name: 'condition', type: 'text', @@ -72,8 +67,6 @@ export const createWorkflowCollection: (options: WorkflowsPlug }, required: false }, - // Virtual fields for custom triggers - ...(triggers || []).flatMap(t => (t.fields || [])) ] }, { @@ -81,58 +74,25 @@ export const createWorkflowCollection: (options: WorkflowsPlug type: 'array', fields: [ { - type: 'row', - fields: [ - { - name: 'step', - type: 'select', - options: steps.map(t => t.slug) - }, - { - name: 'name', - type: 'text', - } - ] + name: 'name', + type: 'text', + defaultValue: 'Unnamed Step' }, - ...(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: { - ...(field.admin || {}), - condition: (...args: any[]) => args[1]?.step === step.slug && ( - (field.admin as any)?.condition ? - (field.admin as any).condition.call(this, ...args) : - true - ), - }, - virtual: true, - }; - - // Add hooks to store/retrieve from the step's input data - 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: 'type', + type: 'select', + options: steps.map(t => t.slug) + }, + { + name: 'parameters', + type: 'json', + admin: { + hidden: true, + }, + defaultValue: {} + }, + // Virtual fields for custom triggers + ...steps.flatMap(step => (step.inputSchema || []).map(s => parameter(step.slug, s as any))), { name: 'dependencies', type: 'text', @@ -153,11 +113,11 @@ export const createWorkflowCollection: (options: WorkflowsPlug ], } ], - versions: { - drafts: { - autosave: false, + versions: { + drafts: { + autosave: false, + }, + maxPerDoc: 10, }, - maxPerDoc: 10, - }, } } diff --git a/src/components/WorkflowExecutionStatus.tsx b/src/components/WorkflowExecutionStatus.tsx index 5a4eed5..910fa3b 100644 --- a/src/components/WorkflowExecutionStatus.tsx +++ b/src/components/WorkflowExecutionStatus.tsx @@ -48,20 +48,7 @@ export const WorkflowExecutionStatus: React.FC = ( } if (runs.length === 0) { - return ( -
- 📋 No execution history yet -
- This workflow hasn't been triggered yet. -
- ) + return null } const getStatusIcon = (status: string) => { diff --git a/src/core/workflow-executor.ts b/src/core/workflow-executor.ts index d1b5893..fdf0dd8 100644 --- a/src/core/workflow-executor.ts +++ b/src/core/workflow-executor.ts @@ -5,26 +5,26 @@ import type { Payload, PayloadRequest } from 'payload' export type PayloadWorkflow = { id: number name: string - description?: string | null + description?: null | string triggers?: Array<{ - type?: string | null - condition?: string | null + type?: null | string + condition?: null | string parameters?: { - collectionSlug?: string | null - operation?: string | null - webhookPath?: string | null - global?: string | null - globalOperation?: string | null + collectionSlug?: null | string + operation?: null | string + webhookPath?: null | string + global?: null | string + globalOperation?: null | string [key: string]: unknown } | null [key: string]: unknown }> | null steps?: Array<{ - step?: string | null - name?: string | null + step?: null | string + name?: null | string input?: unknown - dependencies?: string[] | null - condition?: string | null + dependencies?: null | string[] + condition?: null | string [key: string]: unknown }> | null [key: string]: unknown @@ -33,14 +33,14 @@ export type PayloadWorkflow = { import { JSONPath } from 'jsonpath-plus' // Helper type to extract workflow step data from the generated types -export type WorkflowStep = NonNullable[0] & { +export type WorkflowStep = { name: string // Ensure name is always present for our execution logic -} +} & NonNullable[0] -// Helper type to extract workflow trigger data from the generated types -export type WorkflowTrigger = NonNullable[0] & { +// Helper type to extract workflow trigger data from the generated types +export type WorkflowTrigger = { type: string // Ensure type is always present for our execution logic -} +} & NonNullable[0] export interface ExecutionContext { steps: Record = {} - + // 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)) { if (!coreFields.includes(key)) { - inputFields[key] = value + // 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 + } } } + // 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 const resolvedInput = this.resolveStepInput(inputFields, context) context.steps[stepName].input = resolvedInput @@ -230,8 +262,8 @@ export class WorkflowExecutor { id: job.id, req }) - - this.logger.info({ + + this.logger.info({ jobId: job.id, runResult: runResults, hasResult: !!runResults @@ -276,7 +308,7 @@ export class WorkflowExecutor { if (!errorMessage && taskStatus?.output?.error) { errorMessage = taskStatus.output.error } - + // Check if task handler returned with state='failed' if (!errorMessage && taskStatus?.state === 'failed') { errorMessage = 'Task handler returned a failed state' @@ -337,7 +369,7 @@ export class WorkflowExecutor { const errorDetails = this.extractErrorDetailsFromJob(completedJob, context.steps[stepName], stepName) if (errorDetails) { context.steps[stepName].errorDetails = errorDetails - + this.logger.info({ stepName, errorType: errorDetails.errorType, @@ -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 */ @@ -457,6 +578,22 @@ export class WorkflowExecutor { 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 */ @@ -486,14 +623,14 @@ export class WorkflowExecutor { path: value, wrap: false }) - + this.logger.debug({ key, jsonPath: value, result: JSON.stringify(result).substring(0, 200), resultType: Array.isArray(result) ? 'array' : typeof result }, 'JSONPath resolved successfully') - + resolved[key] = result } catch (error) { this.logger.warn({ @@ -510,7 +647,7 @@ export class WorkflowExecutor { key, nestedKeys: Object.keys(value as Record) }, 'Recursively resolving nested object') - + resolved[key] = this.resolveStepInput(value as Record, context) } else { // Keep literal values as-is @@ -531,22 +668,22 @@ export class WorkflowExecutor { */ private safeSerialize(obj: unknown): unknown { const seen = new WeakSet() - + const serialize = (value: unknown): unknown => { if (value === null || typeof value !== 'object') { return value } - - if (seen.has(value as object)) { + + if (seen.has(value)) { return '[Circular Reference]' } - - seen.add(value as object) - + + seen.add(value) + if (Array.isArray(value)) { return value.map(serialize) } - + const result: Record = {} for (const [key, val] of Object.entries(value as Record)) { try { @@ -560,94 +697,13 @@ export class WorkflowExecutor { result[key] = '[Non-serializable]' } } - + return result } - + 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 */ @@ -695,16 +751,16 @@ export class WorkflowExecutor { try { // Check if this is a comparison expression const comparisonMatch = condition.match(/^(.+?)\s*(==|!=|>|<|>=|<=)\s*(.+)$/) - + if (comparisonMatch) { const [, leftExpr, operator, rightExpr] = comparisonMatch - + // Evaluate left side (should be JSONPath) const leftValue = this.resolveJSONPathValue(leftExpr.trim(), context) - + // Parse right side (could be string, number, boolean, or JSONPath) const rightValue = this.parseConditionValue(rightExpr.trim(), context) - + this.logger.debug({ condition, leftExpr: leftExpr.trim(), @@ -715,32 +771,32 @@ export class WorkflowExecutor { leftType: typeof leftValue, rightType: typeof rightValue }, 'Evaluating comparison condition') - + // Perform comparison let result: boolean switch (operator) { - case '==': - result = leftValue === rightValue - break case '!=': result = leftValue !== rightValue break - case '>': - result = Number(leftValue) > Number(rightValue) - break case '<': result = Number(leftValue) < Number(rightValue) break - case '>=': - result = Number(leftValue) >= Number(rightValue) - break case '<=': result = Number(leftValue) <= Number(rightValue) break + case '==': + result = leftValue === rightValue + break + case '>': + result = Number(leftValue) > Number(rightValue) + break + case '>=': + result = Number(leftValue) >= Number(rightValue) + break default: throw new Error(`Unknown comparison operator: ${operator}`) } - + this.logger.debug({ condition, result, @@ -748,7 +804,7 @@ export class WorkflowExecutor { rightValue, operator }, 'Comparison condition evaluation completed') - + return result } else { // Treat as simple JSONPath boolean evaluation @@ -792,49 +848,6 @@ export class WorkflowExecutor { return false } } - - /** - * 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 @@ -977,148 +990,4 @@ export class WorkflowExecutor { 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 { - - 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') - } - } } diff --git a/src/exports/client.ts b/src/exports/client.ts index ffe3e24..8e14a56 100644 --- a/src/exports/client.ts +++ b/src/exports/client.ts @@ -4,7 +4,6 @@ export { TriggerWorkflowButton } from '../components/TriggerWorkflowButton.js' export { StatusCell } from '../components/StatusCell.js' export { ErrorDisplay } from '../components/ErrorDisplay.js' -export { WorkflowExecutionStatus } from '../components/WorkflowExecutionStatus.js' // Future client components can be added here: // export { default as WorkflowDashboard } from '../components/WorkflowDashboard/index.js' diff --git a/src/triggers/helpers.ts b/src/fields/parameter.ts similarity index 56% rename from src/triggers/helpers.ts rename to src/fields/parameter.ts index 2fe7b4c..1b3d33f 100644 --- a/src/triggers/helpers.ts +++ b/src/fields/parameter.ts @@ -1,30 +1,14 @@ import type {Field} from "payload" -import type {Trigger} from "./types.js" -type Options = { - 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 => ({ +export const parameter = (slug: string, field: {name: string} & Field): Field => ({ ...field, - name: '__trigger_' + field.name, + name: 'parameter' + field.name.replace(/^\w/, c => c.toUpperCase()), admin: { ...(field.admin as unknown || {}), condition: (_, siblingData, __) => { const previous = field.admin?.condition?.call(null, _, siblingData, __) - return previous || (siblingData?.type === slug) + return (previous === undefined || previous) && (siblingData?.type === slug) }, }, hooks: { diff --git a/src/plugin/collection-hook.ts b/src/plugin/collection-hook.ts new file mode 100644 index 0000000..a10cbc6 --- /dev/null +++ b/src/plugin/collection-hook.ts @@ -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 + } + } + } +} diff --git a/src/plugin/config-types.ts b/src/plugin/config-types.ts index 9d465f0..34f445a 100644 --- a/src/plugin/config-types.ts +++ b/src/plugin/config-types.ts @@ -1,24 +1,17 @@ -import type {TaskConfig} from "payload" +import type {CollectionConfig, TaskConfig} from "payload" import type {Trigger} from "../triggers/types.js" -export type CollectionTriggerConfigCrud = { - create?: true - delete?: true - read?: true - update?: true -} +export type TriggerConfig = (config: WorkflowsPluginConfig) => Trigger -export type CollectionTriggerConfig = CollectionTriggerConfigCrud | true - -export type TriggerConfig = (config: WorkflowsPluginConfig) => Trigger - -export type WorkflowsPluginConfig = { - collectionTriggers: { - [key in TSlug]?: CollectionTriggerConfig +export type WorkflowsPluginConfig = { + collectionTriggers?: { + [key in TSlug]?: { + [key in keyof CollectionConfig['hooks']]?: true + } | true } enabled?: boolean - steps: TaskConfig[], + steps: TaskConfig[] triggers?: TriggerConfig[] webhookPrefix?: string } diff --git a/src/plugin/index.ts b/src/plugin/index.ts index 998bb39..267bf20 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -1,9 +1,4 @@ -import type { - CollectionAfterChangeHook, - Config, - PayloadRequest, - TypeWithID -} from 'payload' +import type {CollectionConfig, Config} from 'payload' 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 {initWorkflowHooks} from './init-workflow-hooks.js' import {getConfigLogger, initializeLogger} from './logger.js' +import {createCollectionTriggerHook} from "./collection-hook.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 => { - 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 = (pluginOptions: WorkflowsPluginConfig, config: Config) => { // Add workflow collections if (!config.collections) { @@ -119,70 +26,16 @@ const applyCollectionsConfig = (pluginOptions: WorkflowsPlugin ) } -/** - * Create a collection hook that executes workflows - */ -const createAutomationHook = (): CollectionAfterChangeHook => { - return async function payloadAutomationHook(args) { - const logger = args.req?.payload?.logger || console +type AnyHook = + CollectionConfig['hooks'] extends infer H + ? H extends Record + ? NonNullable extends (infer U)[] + ? U + : never + : never + : never; - try { - 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 - } - } -} +type HookArgs = Parameters[0] export const workflowsPlugin = (pluginOptions: WorkflowsPluginConfig) => @@ -199,13 +52,15 @@ export const workflowsPlugin = const logger = getConfigLogger() if (config.collections && pluginOptions.collectionTriggers) { - for (const [triggerSlug, triggerConfig] of Object.entries(pluginOptions.collectionTriggers)) { - if (!triggerConfig) {continue} + for (const [collectionSlug, triggerConfig] of Object.entries(pluginOptions.collectionTriggers)) { + if (!triggerConfig) { + continue + } // 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) { - logger.warn(`Collection '${triggerSlug}' not found in config.collections`) + logger.warn(`Collection '${collectionSlug}' not found in config.collections`) continue } @@ -215,19 +70,47 @@ export const workflowsPlugin = if (!collection.hooks) { collection.hooks = {} } - if (!collection.hooks.afterChange) { - collection.hooks.afterChange = [] - } - // Add the hook to the collection config - const automationHook = createAutomationHook() - // Mark it for debugging - Object.defineProperty(automationHook, '__isAutomationHook', { - value: true, - enumerable: false + // 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 + } + + const hookKey = hookName as keyof typeof collection.hooks + + // 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 + Object.defineProperty(automationHook, '__isAutomationHook', { + value: true, + enumerable: false + }) + Object.defineProperty(automationHook, '__hookType', { + value: hookKey, + enumerable: false + }) + + // Add the hook to the collection + ;(collection.hooks![hookKey] as Array).push(automationHook) + + logger.debug(`Registered ${hookKey} hook for collection '${collectionSlug}'`) }) - - collection.hooks.afterChange.push(automationHook) } } diff --git a/src/plugin/init-collection-hooks.ts b/src/plugin/init-collection-hooks.ts deleted file mode 100644 index f2b37fe..0000000 --- a/src/plugin/init-collection-hooks.ts +++ /dev/null @@ -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(pluginOptions: WorkflowsPluginConfig, 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') - } - } -} diff --git a/src/triggers/collection-hook-trigger.ts b/src/triggers/collection-hook-trigger.ts new file mode 100644 index 0000000..a3b2975 --- /dev/null +++ b/src/triggers/collection-hook-trigger.ts @@ -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" + ] + } + ] +}) diff --git a/src/triggers/collection-trigger.ts b/src/triggers/collection-trigger.ts deleted file mode 100644 index c83ccd3..0000000 --- a/src/triggers/collection-trigger.ts +++ /dev/null @@ -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', - ], - } - ] -}) diff --git a/src/triggers/global-trigger.ts b/src/triggers/global-trigger.ts index 598888d..3402a78 100644 --- a/src/triggers/global-trigger.ts +++ b/src/triggers/global-trigger.ts @@ -2,7 +2,7 @@ import type {TriggerConfig} from '../plugin/config-types.js' export const globalTrigger: TriggerConfig = () => ({ slug: 'global', - fields: [ + parameters: [ { name: 'global', type: 'select', @@ -22,4 +22,4 @@ export const globalTrigger: TriggerConfig = () => ({ ], } ] -}) \ No newline at end of file +}) diff --git a/src/triggers/index.ts b/src/triggers/index.ts index 5d5567a..9b34e52 100644 --- a/src/triggers/index.ts +++ b/src/triggers/index.ts @@ -1,3 +1,3 @@ -export { collectionTrigger } from './collection-trigger.js' +export { collectionHookTrigger } from './collection-hook-trigger.js' export { globalTrigger } from './global-trigger.js' export { webhookTrigger } from './webhook-trigger.js' diff --git a/src/triggers/types.ts b/src/triggers/types.ts index 965b86f..6caf1e1 100644 --- a/src/triggers/types.ts +++ b/src/triggers/types.ts @@ -2,5 +2,5 @@ import type {Field} from "payload" export type Trigger = { slug: string - fields: Field[] + parameters: Field[] } diff --git a/src/triggers/webhook-trigger.ts b/src/triggers/webhook-trigger.ts index 873df94..0d2ae62 100644 --- a/src/triggers/webhook-trigger.ts +++ b/src/triggers/webhook-trigger.ts @@ -2,7 +2,7 @@ import type {TriggerConfig} from '../plugin/config-types.js' export const webhookTrigger: TriggerConfig = () => ({ slug: 'webhook', - fields: [ + parameters: [ { name: 'webhookPath', type: 'text', @@ -17,4 +17,4 @@ export const webhookTrigger: TriggerConfig = () => ({ }, } ] -}) \ No newline at end of file +})