From 8f0ee4bcef7437ffac9e1f0a92126732f4b4bcbc Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Wed, 10 Sep 2025 13:23:27 +0200 Subject: [PATCH] Disable perfectionist ESLint rules - Disable 'perfectionist/sort-object-types' and 'perfectionist/sort-objects' - Allow natural object property ordering without enforced sorting --- eslint.config.js | 2 + examples/README-trigger-builders.md | 218 -------------------- examples/README.md | 68 ------- examples/custom-trigger-example.ts | 274 ------------------------- examples/manual-trigger-example.ts | 122 ----------- examples/trigger-builders.ts | 300 ---------------------------- src/exports/helpers.ts | 38 ---- src/triggers/helpers.ts | 0 src/triggers/types.ts | 0 src/utils/trigger-helpers.ts | 158 --------------- src/utils/trigger-presets.ts | 157 --------------- 11 files changed, 2 insertions(+), 1335 deletions(-) delete mode 100644 examples/README-trigger-builders.md delete mode 100644 examples/README.md delete mode 100644 examples/custom-trigger-example.ts delete mode 100644 examples/manual-trigger-example.ts delete mode 100644 examples/trigger-builders.ts delete mode 100644 src/exports/helpers.ts create mode 100644 src/triggers/helpers.ts create mode 100644 src/triggers/types.ts delete mode 100644 src/utils/trigger-helpers.ts delete mode 100644 src/utils/trigger-presets.ts diff --git a/eslint.config.js b/eslint.config.js index ac9b921..ab109e6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -28,6 +28,8 @@ export default [ rules: { 'no-restricted-exports': 'off', 'no-console': 'off', + 'perfectionist/sort-object-types': 'off', + 'perfectionist/sort-objects': 'off', }, }, { diff --git a/examples/README-trigger-builders.md b/examples/README-trigger-builders.md deleted file mode 100644 index 8d5b584..0000000 --- a/examples/README-trigger-builders.md +++ /dev/null @@ -1,218 +0,0 @@ -# Trigger Builder Examples - -The new trigger builder API dramatically reduces boilerplate when creating custom triggers. - -## Before vs After - -### Before (Manual Approach) -```typescript -const customTrigger = { - slug: 'order-webhook', - inputs: [ - { - name: 'webhookSecret', - type: 'text', - required: true, - virtual: true, - admin: { - condition: (_, siblingData) => siblingData?.type === 'order-webhook', - description: 'Secret for webhook validation' - }, - hooks: { - afterRead: [({ siblingData }) => siblingData?.parameters?.webhookSecret], - beforeChange: [({ value, siblingData }) => { - if (!siblingData.parameters) siblingData.parameters = {} - siblingData.parameters.webhookSecret = value - return undefined - }] - } - }, - { - name: 'orderStatuses', - type: 'select', - hasMany: true, - options: ['pending', 'processing', 'completed'], - defaultValue: ['completed'], - virtual: true, - admin: { - condition: (_, siblingData) => siblingData?.type === 'order-webhook', - description: 'Order statuses that trigger the workflow' - }, - hooks: { - afterRead: [({ siblingData }) => siblingData?.parameters?.orderStatuses || ['completed']], - beforeChange: [({ value, siblingData }) => { - if (!siblingData.parameters) siblingData.parameters = {} - siblingData.parameters.orderStatuses = value - return undefined - }] - } - } - // ... imagine more fields with similar boilerplate - ] -} -``` - -### After (Builder Approach) -```typescript -import { createTrigger } from '@xtr-dev/payload-automation/helpers' - -const orderWebhook = createTrigger('order-webhook').parameters({ - webhookSecret: { - type: 'text', - required: true, - admin: { - description: 'Secret for webhook validation' - } - }, - orderStatuses: { - type: 'select', - hasMany: true, - options: ['pending', 'processing', 'completed'], - defaultValue: ['completed'], - admin: { - description: 'Order statuses that trigger the workflow' - } - } -}) -``` - -## Built-in Trigger Presets - -### Webhook Trigger -```typescript -import { webhookTrigger } from '@xtr-dev/payload-automation/helpers' - -const paymentWebhook = webhookTrigger('payment-webhook') - .parameter('currency', { - type: 'select', - options: ['USD', 'EUR', 'GBP'], - defaultValue: 'USD' - }) - .build() -``` - -### Scheduled/Cron Trigger -```typescript -import { cronTrigger } from '@xtr-dev/payload-automation/helpers' - -const dailyReport = cronTrigger('daily-report') - .parameter('reportFormat', { - type: 'select', - options: ['pdf', 'csv', 'json'], - defaultValue: 'pdf' - }) - .build() -``` - -### Manual Trigger (No Parameters) -```typescript -import { manualTrigger } from '@xtr-dev/payload-automation/helpers' - -const backupTrigger = manualTrigger('manual-backup') -``` - -## Advanced Usage - -### Extending Common Parameters -```typescript -import { createAdvancedTrigger, webhookParameters } from '@xtr-dev/payload-automation/helpers' - -const advancedWebhook = createAdvancedTrigger('advanced-webhook') - .extend(webhookParameters) // Includes path, secret, headers - .parameter('retryAttempts', { - type: 'number', - min: 0, - max: 5, - defaultValue: 3 - }) - .parameter('timeout', { - type: 'number', - min: 1000, - max: 30000, - defaultValue: 5000, - admin: { - description: 'Timeout in milliseconds' - } - }) - .build() -``` - -### Custom Validation -```typescript -const validatedTrigger = createTrigger('validated-trigger').parameters({ - email: { - type: 'email', - required: true, - validate: (value) => { - if (value?.endsWith('@spam.com')) { - return 'Spam domains not allowed' - } - return true - } - }, - webhookUrl: { - type: 'text', - required: true, - validate: (value) => { - try { - const url = new URL(value) - if (!['http:', 'https:'].includes(url.protocol)) { - return 'Only HTTP/HTTPS URLs allowed' - } - } catch { - return 'Please enter a valid URL' - } - return true - } - } -}) -``` - -## Usage in Plugin Configuration - -```typescript -import { workflowsPlugin } from '@xtr-dev/payload-automation' -import { - createTrigger, - webhookTrigger, - cronTrigger -} from '@xtr-dev/payload-automation/helpers' - -export default buildConfig({ - plugins: [ - workflowsPlugin({ - triggers: [ - // Mix different trigger types - createTrigger('user-signup').parameters({ - source: { - type: 'select', - options: ['web', 'mobile', 'api'], - required: true - } - }), - - webhookTrigger('payment-received') - .parameter('minimumAmount', { type: 'number', min: 0 }) - .build(), - - cronTrigger('weekly-cleanup') - .parameter('deleteOlderThan', { - type: 'number', - defaultValue: 30, - admin: { description: 'Delete records older than N days' } - }) - .build() - ] - }) - ] -}) -``` - -## Benefits - -- **90% less boilerplate** - No manual hooks, conditions, or virtual field setup -- **Type safety** - Full TypeScript support -- **Reusable patterns** - Common trigger types as presets -- **Composable** - Mix builders with manual fields -- **Backward compatible** - Existing triggers continue to work -- **Validation built-in** - Parameter validation handled automatically \ No newline at end of file diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index b3f86a5..0000000 --- a/examples/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# PayloadCMS Workflows Plugin Examples - -This directory contains example code demonstrating how to use the PayloadCMS Workflows plugin. - -## Manual Trigger Example - -The `manual-trigger-example.ts` file shows how to: -- Create a workflow with a manual trigger button in the admin UI -- Trigger workflows programmatically using custom triggers -- Access trigger data in workflow steps using JSONPath - -### Setting up a Manual Trigger Workflow - -1. Configure the plugin with a custom trigger: -```typescript -workflowsPlugin({ - triggers: [ - { - slug: 'manual-trigger', - inputs: [] // No inputs needed for simple manual triggers - } - ], - // ... other config -}) -``` - -2. Create a workflow with the manual trigger: -```typescript -await payload.create({ - collection: 'workflows', - data: { - name: 'My Manual Workflow', - triggers: [ - { - type: 'manual-trigger' - } - ], - steps: [ - // Your workflow steps here - ] - } -}) -``` - -3. The workflow will now have a "Trigger Workflow" button in the admin UI - -### Triggering Workflows Programmatically - -```typescript -import { triggerCustomWorkflow } from '@xtr-dev/payload-automation' - -// Trigger all workflows with 'manual-trigger' -const results = await triggerCustomWorkflow(payload, { - slug: 'manual-trigger', - data: { - // Custom data to pass to the workflow - source: 'api', - timestamp: new Date().toISOString() - } -}) -``` - -### Accessing Trigger Data in Steps - -Use JSONPath expressions to access trigger data in your workflow steps: -- `$.trigger.data.source` - Access custom data fields -- `$.trigger.type` - The trigger type -- `$.trigger.triggeredAt` - When the trigger was activated \ No newline at end of file diff --git a/examples/custom-trigger-example.ts b/examples/custom-trigger-example.ts deleted file mode 100644 index 0618543..0000000 --- a/examples/custom-trigger-example.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { buildConfig } from 'payload' -import { workflowsPlugin, triggerCustomWorkflow } from '@xtr-dev/payload-automation' -import type { Field } from 'payload' - -// Example: Data import trigger with custom fields -const dataImportFields: Field[] = [ - { - name: 'sourceUrl', - type: 'text', - required: true, - admin: { - description: 'URL of the data source to import from' - } - }, - { - name: 'format', - type: 'select', - options: ['json', 'csv', 'xml'], - required: true, - admin: { - description: 'Format of the data to import' - } - }, - { - name: 'mapping', - type: 'json', - admin: { - description: 'Field mapping configuration' - } - } -] - -// Example: Manual review trigger with approval fields -const manualReviewFields: Field[] = [ - { - name: 'reviewerId', - type: 'text', - required: true, - admin: { - description: 'ID of the reviewer' - } - }, - { - name: 'reviewNotes', - type: 'textarea', - admin: { - description: 'Notes from the review' - } - }, - { - name: 'approved', - type: 'checkbox', - defaultValue: false, - admin: { - description: 'Whether the item was approved' - } - } -] - -export default buildConfig({ - // ... other config - - plugins: [ - workflowsPlugin({ - collectionTriggers: { - posts: true, // Enable all CRUD triggers for posts - products: { // Selective triggers for products - create: true, - update: true - } - }, - - // Define custom triggers that will appear in the workflow UI - triggers: [ - { - slug: 'data-import', - inputs: dataImportFields - }, - { - slug: 'manual-review', - inputs: manualReviewFields - }, - { - slug: 'scheduled-report', - inputs: [ - { - name: 'reportType', - type: 'select', - options: ['daily', 'weekly', 'monthly'], - required: true - } - ] - } - ], - - steps: [ - // ... your workflow steps - ] - }) - ], - - onInit: async (payload) => { - // Example 1: Trigger workflow from external data source - // This could be called from a webhook, scheduled job, or any other event - const handleDataImport = async (sourceUrl: string, format: string) => { - const results = await triggerCustomWorkflow(payload, { - slug: 'data-import', - data: { - sourceUrl, - format, - mapping: { - title: 'name', - description: 'summary' - }, - importedAt: new Date().toISOString() - } - }) - - console.log('Data import workflows triggered:', results) - } - - // Example 2: Trigger workflow after custom business logic - const handleDocumentReview = async (documentId: string, reviewerId: string, approved: boolean) => { - // Perform your custom review logic here - const reviewData = { - documentId, - reviewerId, - reviewNotes: approved ? 'Document meets all requirements' : 'Needs revision', - approved, - reviewedAt: new Date().toISOString() - } - - // Trigger workflows that listen for manual review - const results = await triggerCustomWorkflow(payload, { - slug: 'manual-review', - data: reviewData, - user: { - id: reviewerId, - email: 'reviewer@example.com' - } - }) - - return results - } - - // Example 3: Integrate with external services - // You could set up listeners for external events - if (process.env.ENABLE_EXTERNAL_SYNC) { - // Listen to external service events (example with a hypothetical event emitter) - // externalService.on('data-ready', async (event) => { - // await triggerCustomWorkflow(payload, { - // slug: 'data-import', - // data: event.data - // }) - // }) - } - - // Example 4: Create scheduled reports using node-cron or similar - // This shows how you might trigger a custom workflow on a schedule - // without using the built-in cron trigger - const scheduleReports = async () => { - // This could be called by a cron job or scheduled task - await triggerCustomWorkflow(payload, { - slug: 'scheduled-report', - data: { - reportType: 'daily', - generatedAt: new Date().toISOString(), - metrics: { - totalUsers: 1000, - activeUsers: 750, - newSignups: 25 - } - } - }) - } - - // Example 5: Hook into collection operations for complex logic - const postsCollection = payload.collections.posts - if (postsCollection) { - postsCollection.config.hooks = postsCollection.config.hooks || {} - postsCollection.config.hooks.afterChange = postsCollection.config.hooks.afterChange || [] - - postsCollection.config.hooks.afterChange.push(async ({ doc, operation, req }) => { - // Custom logic to determine if we should trigger a workflow - if (operation === 'create' && doc.status === 'published') { - // Trigger a custom workflow for newly published posts - await triggerCustomWorkflow(payload, { - slug: 'manual-review', - data: { - documentId: doc.id, - documentType: 'post', - reviewerId: 'auto-review', - reviewNotes: 'Automatically queued for review', - approved: false - }, - req // Pass the request context - }) - } - }) - } - - // Make functions available globally for testing/debugging - ;(global as any).handleDataImport = handleDataImport - ;(global as any).handleDocumentReview = handleDocumentReview - ;(global as any).scheduleReports = scheduleReports - } -}) - -// Example workflow configuration that would use these custom triggers: -/* -{ - name: "Process Data Import", - triggers: [{ - type: "data-import", - sourceUrl: "https://api.example.com/data", - format: "json", - mapping: { ... } - }], - steps: [ - { - step: "http-request", - name: "fetch-data", - input: { - url: "$.trigger.data.sourceUrl", - method: "GET" - } - }, - { - step: "create-document", - name: "import-records", - input: { - collection: "imported-data", - data: "$.steps.fetch-data.output.body" - }, - dependencies: ["fetch-data"] - } - ] -} - -{ - name: "Review Approval Workflow", - triggers: [{ - type: "manual-review", - reviewerId: "", - reviewNotes: "", - approved: false - }], - steps: [ - { - step: "update-document", - name: "update-status", - input: { - collection: "documents", - id: "$.trigger.data.documentId", - data: { - status: "$.trigger.data.approved ? 'approved' : 'rejected'", - reviewedBy: "$.trigger.data.reviewerId", - reviewedAt: "$.trigger.data.reviewedAt" - } - } - }, - { - step: "send-email", - name: "notify-author", - input: { - to: "author@example.com", - subject: "Document Review Complete", - text: "Your document has been $.trigger.data.approved ? 'approved' : 'rejected'" - }, - dependencies: ["update-status"] - } - ] -} -*/ \ No newline at end of file diff --git a/examples/manual-trigger-example.ts b/examples/manual-trigger-example.ts deleted file mode 100644 index 0ef7387..0000000 --- a/examples/manual-trigger-example.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Example: Manual Trigger Workflow - * - * This example shows how to create a workflow that can be triggered - * manually from the PayloadCMS admin interface using a custom button. - */ - -import type { Payload } from 'payload' - -/** - * Create a workflow with manual trigger - */ -export async function createManualTriggerWorkflow(payload: Payload) { - const workflow = await payload.create({ - collection: 'workflows', - data: { - name: 'Manual Data Processing', - description: 'A workflow that can be triggered manually from the admin UI', - triggers: [ - { - type: 'manual-trigger' // This enables the trigger button in the admin - } - ], - steps: [ - { - name: 'fetch-data', - type: 'http-request-step', - input: { - url: 'https://api.example.com/data', - method: 'GET' - } - }, - { - name: 'process-data', - type: 'create-document', - input: { - collection: 'auditLog', - data: { - message: 'Manual workflow executed', - triggeredAt: '$.trigger.data.timestamp' - } - }, - dependencies: ['fetch-data'] // This step depends on fetch-data - } - ] - } - }) - - console.log('Created workflow:', workflow.id) - return workflow -} - -/** - * Trigger a workflow programmatically using the custom trigger - */ -export async function triggerWorkflowProgrammatically(payload: Payload) { - // Import the trigger functions from the plugin - const { triggerCustomWorkflow, triggerWorkflowById } = await import('@xtr-dev/payload-automation') - - // Option 1: Trigger all workflows with a specific trigger slug - const results = await triggerCustomWorkflow(payload, { - slug: 'manual-trigger', - data: { - source: 'api', - timestamp: new Date().toISOString(), - user: 'system' - } - }) - - console.log('Triggered workflows:', results) - - // Option 2: Trigger a specific workflow by ID - const workflowId = 'your-workflow-id' - const result = await triggerWorkflowById( - payload, - workflowId, - 'manual-trigger', - { - source: 'api', - timestamp: new Date().toISOString() - } - ) - - console.log('Triggered workflow:', result) -} - -/** - * Example usage in your application - */ -export async function setupManualTriggerExample(payload: Payload) { - // Create the workflow - const workflow = await createManualTriggerWorkflow(payload) - - // The workflow is now available in the admin UI with a trigger button - console.log('Workflow created! You can now:') - console.log('1. Go to the admin UI and navigate to the Workflows collection') - console.log('2. Open the workflow:', workflow.name) - console.log('3. Click the "Trigger Workflow" button to execute it manually') - - // You can also trigger it programmatically - await triggerWorkflowProgrammatically(payload) -} - -/** - * Notes: - * - * 1. The manual trigger button appears automatically in the workflow admin UI - * when a workflow has a trigger with type 'manual-trigger' - * - * 2. You can have multiple triggers on the same workflow, including manual triggers - * - * 3. The trigger passes data to the workflow execution context, accessible via: - * - $.trigger.data - The custom data passed when triggering - * - $.trigger.type - The trigger type ('manual-trigger') - * - $.trigger.triggeredAt - Timestamp of when the trigger was activated - * - * 4. Manual triggers are useful for: - * - Administrative tasks - * - Data migration workflows - * - Testing and debugging - * - On-demand processing - */ \ No newline at end of file diff --git a/examples/trigger-builders.ts b/examples/trigger-builders.ts deleted file mode 100644 index a35b669..0000000 --- a/examples/trigger-builders.ts +++ /dev/null @@ -1,300 +0,0 @@ -/** - * Examples demonstrating the new trigger builder API - * This shows the before/after comparison and various usage patterns - */ - -import { - createTrigger, - createAdvancedTrigger, - webhookTrigger, - cronTrigger, - eventTrigger, - manualTrigger, - apiTrigger, - webhookParameters, - cronParameters -} from '../src/exports/helpers.js' - -/** - * BEFORE: Manual trigger definition with lots of boilerplate - */ -const oldWayTrigger = { - slug: 'order-webhook', - inputs: [ - { - name: 'webhookSecret', - type: 'text', - required: true, - virtual: true, - admin: { - condition: (_, siblingData) => siblingData?.type === 'order-webhook', - description: 'Secret for webhook validation' - }, - hooks: { - afterRead: [({ siblingData }) => siblingData?.parameters?.webhookSecret], - beforeChange: [({ value, siblingData }) => { - if (!siblingData.parameters) siblingData.parameters = {} - siblingData.parameters.webhookSecret = value - return undefined - }] - } - }, - { - name: 'orderStatuses', - type: 'select', - hasMany: true, - options: ['pending', 'processing', 'completed'], - defaultValue: ['completed'], - virtual: true, - admin: { - condition: (_, siblingData) => siblingData?.type === 'order-webhook', - description: 'Order statuses that trigger the workflow' - }, - hooks: { - afterRead: [({ siblingData }) => siblingData?.parameters?.orderStatuses || ['completed']], - beforeChange: [({ value, siblingData }) => { - if (!siblingData.parameters) siblingData.parameters = {} - siblingData.parameters.orderStatuses = value - return undefined - }] - } - } - // ... imagine more fields with similar boilerplate - ] -} as const - -/** - * AFTER: Clean trigger definition using builders - */ - -// 1. Simple trigger with parameters -const orderWebhook = createTrigger('order-webhook').parameters({ - webhookSecret: { - type: 'text', - required: true, - admin: { - description: 'Secret for webhook validation' - } - }, - orderStatuses: { - type: 'select', - hasMany: true, - options: ['pending', 'processing', 'completed'], - defaultValue: ['completed'], - admin: { - description: 'Order statuses that trigger the workflow' - } - }, - minimumAmount: { - type: 'number', - min: 0, - admin: { - description: 'Minimum order amount to trigger workflow' - } - } -}) - -// 2. Using preset webhook builder -const paymentWebhook = webhookTrigger('payment-webhook') - .parameter('currency', { - type: 'select', - options: ['USD', 'EUR', 'GBP'], - defaultValue: 'USD' - }) - .parameter('paymentMethods', { - type: 'select', - hasMany: true, - options: ['credit_card', 'paypal', 'bank_transfer'] - }) - .build() - -// 3. Scheduled trigger using cron builder -const dailyReport = cronTrigger('daily-report') - .parameter('reportFormat', { - type: 'select', - options: [ - { label: 'PDF Report', value: 'pdf' }, - { label: 'CSV Export', value: 'csv' }, - { label: 'JSON Data', value: 'json' } - ], - defaultValue: 'pdf' - }) - .parameter('includeCharts', { - type: 'checkbox', - defaultValue: true, - admin: { - description: 'Include visual charts in the report' - } - }) - .build() - -// 4. Event-driven trigger -const userActivity = eventTrigger('user-activity') - .parameter('actionTypes', { - type: 'select', - hasMany: true, - options: ['login', 'logout', 'profile_update', 'password_change'], - admin: { - description: 'User actions that should trigger this workflow' - } - }) - .parameter('userRoles', { - type: 'select', - hasMany: true, - options: ['admin', 'editor', 'user'], - admin: { - description: 'Only trigger for users with these roles' - } - }) - .build() - -// 5. Simple manual trigger (no parameters) -const manualBackup = manualTrigger('manual-backup') - -// 6. API trigger with authentication -const externalApi = apiTrigger('external-api') - .parameter('allowedOrigins', { - type: 'textarea', - admin: { - description: 'Comma-separated list of allowed origins' - }, - validate: (value) => { - if (value && typeof value === 'string') { - const origins = value.split(',').map(s => s.trim()) - const validOrigins = origins.every(origin => { - try { - new URL(origin) - return true - } catch { - return false - } - }) - if (!validOrigins) { - return 'All origins must be valid URLs' - } - } - return true - } - }) - .build() - -// 7. Complex trigger extending common parameters -const advancedWebhook = createAdvancedTrigger('advanced-webhook') - .extend(webhookParameters) // Start with webhook basics - .parameter('retryConfig', { - type: 'group', - fields: [ - { - name: 'maxRetries', - type: 'number', - min: 0, - max: 10, - defaultValue: 3 - }, - { - name: 'retryDelay', - type: 'number', - min: 1000, - max: 60000, - defaultValue: 5000, - admin: { - description: 'Delay between retries in milliseconds' - } - } - ] - }) - .parameter('filters', { - type: 'array', - fields: [ - { - name: 'field', - type: 'text', - required: true - }, - { - name: 'operator', - type: 'select', - options: ['equals', 'not_equals', 'contains', 'greater_than'], - required: true - }, - { - name: 'value', - type: 'text', - required: true - } - ] - }) - .build() - -// 8. Custom parameter validation -const validatedTrigger = createTrigger('validated-trigger').parameters({ - email: { - type: 'email', - required: true, - validate: (value) => { - if (value && typeof value === 'string') { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - if (!emailRegex.test(value)) { - return 'Please enter a valid email address' - } - // Custom business logic validation - if (value.endsWith('@example.com')) { - return 'Example.com emails are not allowed' - } - } - return true - } - }, - webhookUrl: { - type: 'text', - required: true, - validate: (value) => { - if (value && typeof value === 'string') { - try { - const url = new URL(value) - if (!['http:', 'https:'].includes(url.protocol)) { - return 'URL must use HTTP or HTTPS protocol' - } - if (url.hostname === 'localhost') { - return 'Localhost URLs are not allowed in production' - } - } catch { - return 'Please enter a valid URL' - } - } - return true - } - } -}) - -/** - * Export all triggers for use in plugin configuration - */ -export const exampleTriggers = [ - orderWebhook, - paymentWebhook, - dailyReport, - userActivity, - manualBackup, - externalApi, - advancedWebhook, - validatedTrigger -] - -/** - * Usage in payload.config.ts: - * - * ```typescript - * import { workflowsPlugin } from '@xtr-dev/payload-automation' - * import { exampleTriggers } from './examples/trigger-builders' - * - * export default buildConfig({ - * plugins: [ - * workflowsPlugin({ - * triggers: exampleTriggers, - * // ... other config - * }) - * ] - * }) - * ``` - */ \ No newline at end of file diff --git a/src/exports/helpers.ts b/src/exports/helpers.ts deleted file mode 100644 index ca3720c..0000000 --- a/src/exports/helpers.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Trigger builder helpers for creating custom triggers with less boilerplate - * - * @example - * ```typescript - * import { createTrigger, createTriggerField, webhookTrigger } from '@xtr-dev/payload-automation/helpers' - * - * // Simple trigger with array of fields - * const myTrigger = createTrigger('my-trigger', [ - * { name: 'apiKey', type: 'text', required: true }, - * { name: 'timeout', type: 'number', defaultValue: 30 } - * ]) - * - * // Single field with virtual storage - * const field = createTriggerField( - * { name: 'webhookUrl', type: 'text', required: true }, - * 'my-trigger' - * ) - * - * // Webhook trigger preset - * const orderWebhook = webhookTrigger('order-webhook') - * ``` - */ - -// Core helpers -export { - createTriggerField, - createTrigger -} from '../utils/trigger-helpers.js' - -// Preset builders -export { - webhookTrigger, - cronTrigger, - eventTrigger, - manualTrigger, - apiTrigger -} from '../utils/trigger-presets.js' \ No newline at end of file diff --git a/src/triggers/helpers.ts b/src/triggers/helpers.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/triggers/types.ts b/src/triggers/types.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/trigger-helpers.ts b/src/utils/trigger-helpers.ts deleted file mode 100644 index 73178bd..0000000 --- a/src/utils/trigger-helpers.ts +++ /dev/null @@ -1,158 +0,0 @@ -import type { Field } from 'payload' - -import type { CustomTriggerConfig } from '../plugin/config-types.js' - -// Types for better type safety -interface FieldWithName { - name: string - [key: string]: unknown -} - -interface HookContext { - siblingData: Record - value?: unknown -} - -interface ValidationContext { - siblingData: Record -} - -/** - * Creates a virtual field for a trigger parameter that stores its value in the parameters JSON field - * - * @param field - Standard PayloadCMS field configuration (must be a data field with a name) - * @param triggerSlug - The slug of the trigger this field belongs to - * @returns Modified field with virtual storage hooks and proper naming - * - * @example - * ```typescript - * const myTrigger: CustomTriggerConfig = { - * slug: 'my-trigger', - * inputs: [ - * createTriggerField({ - * name: 'webhookUrl', - * type: 'text', - * required: true, - * admin: { - * description: 'URL to call when triggered' - * } - * }, 'my-trigger') - * ] - * } - * ``` - */ -export function createTriggerField(field: FieldWithName, triggerSlug: string): Field { - const originalName = field.name - if (!originalName) { - throw new Error('Field must have a name property') - } - - // Create a unique field name by prefixing with trigger slug - const uniqueFieldName = `__trigger_${triggerSlug}_${originalName}` - - const resultField: Record = { - ...field, - admin: { - ...(field.admin as Record || {}), - condition: (data: unknown, siblingData: Record) => { - // Only show this field when the trigger type matches - const triggerMatches = siblingData?.type === triggerSlug - - // If the original field had a condition, combine it with our trigger condition - const originalCondition = (field.admin as Record)?.condition - if (originalCondition && typeof originalCondition === 'function') { - return triggerMatches && (originalCondition as (data: unknown, siblingData: Record) => boolean)(data, siblingData) - } - - return triggerMatches - } - }, - hooks: { - ...(field.hooks as Record || {}), - afterRead: [ - ...((field.hooks as Record)?.afterRead || []), - ({ siblingData }: HookContext) => { - // Read the value from the parameters JSON field - const parameters = siblingData?.parameters as Record - return parameters?.[originalName] ?? (field as Record).defaultValue - } - ], - beforeChange: [ - ...((field.hooks as Record)?.beforeChange || []), - ({ siblingData, value }: HookContext) => { - // Store the value in the parameters JSON field - if (!siblingData.parameters) { - siblingData.parameters = {} - } - const parameters = siblingData.parameters as Record - parameters[originalName] = value - return undefined // Virtual field, don't store directly - } - ] - }, - name: uniqueFieldName, - virtual: true, - } - - // Only add validate if the field supports it (data fields) - const hasValidation = (field as Record).validate || (field as Record).required - if (hasValidation) { - resultField.validate = (value: unknown, args: ValidationContext) => { - const parameters = args.siblingData?.parameters as Record - const paramValue = value ?? parameters?.[originalName] - - // Check required validation - const isRequired = (field as Record).required - if (isRequired && args.siblingData?.type === triggerSlug && !paramValue) { - const fieldLabel = (field as Record).label as string - const adminDesc = ((field as Record).admin as Record)?.description as string - const label = fieldLabel || adminDesc || originalName - return `${label} is required for ${triggerSlug}` - } - - // Run original validation if present - const originalValidate = (field as Record).validate - if (originalValidate && typeof originalValidate === 'function') { - return (originalValidate as (value: unknown, args: ValidationContext) => boolean | string)(paramValue, args) - } - - return true - } - } - - return resultField as Field -} - -/** - * Creates a custom trigger configuration with the provided fields - * - * @param slug - Unique identifier for the trigger - * @param fields - Array of PayloadCMS fields that will be shown as trigger parameters - * @returns Complete trigger configuration - * - * @example - * ```typescript - * const webhookTrigger = createTrigger('webhook', [ - * { - * name: 'url', - * type: 'text', - * required: true, - * admin: { - * description: 'Webhook URL' - * } - * }, - * { - * name: 'method', - * type: 'select', - * options: ['GET', 'POST', 'PUT', 'DELETE'], - * defaultValue: 'POST' - * } - * ]) - * ``` - */ -export function createTrigger(slug: string, fields: FieldWithName[]): CustomTriggerConfig { - return { - slug, - inputs: fields.map(field => createTriggerField(field, slug)) - } -} \ No newline at end of file diff --git a/src/utils/trigger-presets.ts b/src/utils/trigger-presets.ts deleted file mode 100644 index 479c291..0000000 --- a/src/utils/trigger-presets.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { createTrigger } from './trigger-helpers.js' -import type { CustomTriggerConfig } from '../plugin/config-types.js' - -/** - * Preset trigger builders for common patterns - */ - -/** - * Create a webhook trigger with common webhook parameters pre-configured - */ -export function webhookTrigger(slug: string): CustomTriggerConfig { - return createTrigger(slug, [ - { - name: 'path', - type: 'text', - required: true, - admin: { - description: 'URL path for the webhook endpoint (e.g., "my-webhook")' - }, - validate: (value: any) => { - if (typeof value === 'string' && value.includes(' ')) { - return 'Webhook path cannot contain spaces' - } - return true - } - }, - { - name: 'secret', - type: 'text', - admin: { - description: 'Secret key for webhook signature validation (optional but recommended)' - } - }, - { - name: 'headers', - type: 'json', - admin: { - description: 'Expected HTTP headers for validation (JSON object)' - } - } - ]) -} - -/** - * Create a scheduled/cron trigger with timing parameters pre-configured - */ -export function cronTrigger(slug: string): CustomTriggerConfig { - return createTrigger(slug, [ - { - name: 'expression', - type: 'text', - required: true, - admin: { - description: 'Cron expression for scheduling (e.g., "0 9 * * 1" for every Monday at 9 AM)', - placeholder: '0 9 * * 1' - } - }, - { - name: 'timezone', - type: 'text', - defaultValue: 'UTC', - admin: { - description: 'Timezone for cron execution (e.g., "America/New_York", "Europe/London")', - placeholder: 'UTC' - }, - validate: (value: any) => { - if (value) { - try { - new Intl.DateTimeFormat('en', { timeZone: value as string }) - return true - } catch { - return `Invalid timezone: ${value}. Please use a valid IANA timezone identifier` - } - } - return true - } - } - ]) -} - -/** - * Create an event-driven trigger with event filtering parameters - */ -export function eventTrigger(slug: string): CustomTriggerConfig { - return createTrigger(slug, [ - { - name: 'eventTypes', - type: 'select', - hasMany: true, - options: [ - { label: 'User Created', value: 'user.created' }, - { label: 'User Updated', value: 'user.updated' }, - { label: 'Document Published', value: 'document.published' }, - { label: 'Payment Completed', value: 'payment.completed' } - ], - admin: { - description: 'Event types that should trigger this workflow' - } - }, - { - name: 'filters', - type: 'json', - admin: { - description: 'JSON filters to apply to event data (e.g., {"status": "active"})' - } - } - ]) -} - -/** - * Create a simple manual trigger (no parameters needed) - */ -export function manualTrigger(slug: string): CustomTriggerConfig { - return { - slug, - inputs: [] - } -} - -/** - * Create an API trigger for external systems to call - */ -export function apiTrigger(slug: string): CustomTriggerConfig { - return createTrigger(slug, [ - { - name: 'endpoint', - type: 'text', - required: true, - admin: { - description: 'API endpoint path (e.g., "/api/triggers/my-trigger")' - } - }, - { - name: 'method', - type: 'select', - options: ['GET', 'POST', 'PUT', 'PATCH'], - defaultValue: 'POST', - admin: { - description: 'HTTP method for the API endpoint' - } - }, - { - name: 'authentication', - type: 'select', - options: [ - { label: 'None', value: 'none' }, - { label: 'API Key', value: 'api-key' }, - { label: 'Bearer Token', value: 'bearer' }, - { label: 'Basic Auth', value: 'basic' } - ], - defaultValue: 'api-key', - admin: { - description: 'Authentication method for the API endpoint' - } - } - ]) -} \ No newline at end of file