diff --git a/dev/payload.config.ts b/dev/payload.config.ts index 92426ef..2d392df 100644 --- a/dev/payload.config.ts +++ b/dev/payload.config.ts @@ -24,13 +24,24 @@ if (!process.env.ROOT_DIR) { const buildConfigWithMemoryDB = async () => { // Use MongoDB adapter for testing instead of SQLite const { mongooseAdapter } = await import('@payloadcms/db-mongodb') - + return buildConfig({ admin: { importMap: { baseDir: path.resolve(dirname, '..'), }, }, + globals: [ + { + slug: 'settings', + fields: [ + { + name: 'siteName', + type: 'text' + } + ] + } + ], collections: [ { slug: 'posts', @@ -96,14 +107,13 @@ const buildConfigWithMemoryDB = async () => { posts: true, media: true }, + globalTriggers: { + settings: true + }, steps: [ HttpRequestStepTask, CreateDocumentStepTask ], - triggers: [ - - ], - webhookPrefix: '/workflows-webhook' }), ], secret: process.env.PAYLOAD_SECRET || 'test-secret_key', diff --git a/dev/webhook-triggers.spec.ts b/dev/webhook-triggers.spec.ts deleted file mode 100644 index e6db566..0000000 --- a/dev/webhook-triggers.spec.ts +++ /dev/null @@ -1,483 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import { getTestPayload, cleanDatabase } from './test-setup.js' - -describe('Webhook Trigger Testing', () => { - - beforeEach(async () => { - await cleanDatabase() - }) - - afterEach(async () => { - await cleanDatabase() - }) - - it('should trigger workflow via webhook endpoint simulation', async () => { - const payload = getTestPayload() - - // Create a workflow with webhook trigger - const workflow = await payload.create({ - collection: 'workflows', - data: { - name: 'Test Webhook - Basic Trigger', - description: 'Tests basic webhook triggering', - triggers: [ - { - type: 'webhook-trigger', - webhookPath: 'test-basic' - } - ], - steps: [ - { - name: 'create-webhook-audit', - step: 'create-document', - collectionSlug: 'auditLog', - data: { - message: 'Webhook triggered successfully', - user: '$.trigger.data.userId' - } - } - ] - } - }) - - expect(workflow).toBeDefined() - - // Directly execute the workflow with webhook-like data - const executor = (globalThis as any).__workflowExecutor - if (!executor) { - console.warn('⚠️ Workflow executor not available, skipping webhook execution') - return - } - - // Simulate webhook trigger by directly executing the workflow - const webhookData = { - userId: 'webhook-test-user', - timestamp: new Date().toISOString() - } - - const mockReq = { - payload, - user: null, - headers: {} - } - - await executor.execute({ - workflow, - trigger: { - type: 'webhook', - path: 'test-basic', - data: webhookData, - headers: {} - }, - req: mockReq as any, - payload - }) - - console.log('✅ Workflow executed directly') - - // Wait for workflow execution - await new Promise(resolve => setTimeout(resolve, 2000)) - - // Verify workflow run was created - const runs = await payload.find({ - collection: 'workflow-runs', - where: { - workflow: { - equals: workflow.id - } - }, - limit: 1 - }) - - expect(runs.totalDocs).toBe(1) - expect(runs.docs[0].status).not.toBe('failed') - - // Verify audit log was created - const auditLogs = await payload.find({ - collection: 'auditLog', - where: { - message: { - contains: 'Webhook triggered' - } - }, - limit: 1 - }) - - expect(auditLogs.totalDocs).toBe(1) - console.log('✅ Webhook audit log created') - }, 30000) - - it('should handle webhook with complex data', async () => { - const workflow = await payload.create({ - collection: 'workflows', - data: { - name: 'Test Webhook - Complex Data', - description: 'Tests webhook with complex JSON data', - triggers: [ - { - type: 'webhook-trigger', - webhookPath: 'test-complex' - } - ], - steps: [ - { - name: 'echo-webhook-data', - step: 'http-request-step', - url: 'https://httpbin.org/post', - method: 'POST', - body: { - originalData: '$.trigger.data', - headers: '$.trigger.headers', - path: '$.trigger.path' - } - } - ] - } - }) - - const complexData = { - user: { - id: 123, - name: 'Test User', - permissions: ['read', 'write'] - }, - event: { - type: 'user_action', - timestamp: new Date().toISOString(), - metadata: { - source: 'webhook-test', - version: '1.0.0' - } - }, - nested: { - deeply: { - nested: { - value: 'deep-test-value' - } - } - } - } - - const response = await makeWebhookRequest('test-complex', complexData) - expect(response.status).toBe(200) - - // Wait for workflow execution - await new Promise(resolve => setTimeout(resolve, 5000)) - - const runs = await payload.find({ - collection: 'workflow-runs', - where: { - workflow: { - equals: workflow.id - } - }, - limit: 1 - }) - - expect(runs.totalDocs).toBe(1) - expect(runs.docs[0].status).toBe('completed') - - // Verify the complex data was properly passed through - const stepOutput = runs.docs[0].context.steps['echo-webhook-data'].output - expect(stepOutput.status).toBe(200) - - const responseBody = JSON.parse(stepOutput.body) - expect(responseBody.json.originalData.user.name).toBe('Test User') - expect(responseBody.json.originalData.nested.deeply.nested.value).toBe('deep-test-value') - - console.log('✅ Complex webhook data processed correctly') - }, 30000) - - it('should handle webhook conditions', async () => { - const workflow = await payload.create({ - collection: 'workflows', - data: { - name: 'Test Webhook - Conditional', - description: 'Tests conditional webhook execution', - triggers: [ - { - type: 'webhook-trigger', - webhookPath: 'test-conditional', - condition: '$.data.action == "important"' - } - ], - steps: [ - { - name: 'conditional-audit', - step: 'create-document', - collectionSlug: 'auditLog', - data: { - message: 'Webhook condition met - important action' - } - } - ] - } - }) - - // First request - should NOT trigger (condition not met) - const response1 = await makeWebhookRequest('test-conditional', { - action: 'normal', - data: 'test' - }) - expect(response1.status).toBe(200) - - // Second request - SHOULD trigger (condition met) - const response2 = await makeWebhookRequest('test-conditional', { - action: 'important', - priority: 'high' - }) - expect(response2.status).toBe(200) - - // Wait for workflow execution - await new Promise(resolve => setTimeout(resolve, 5000)) - - const runs = await payload.find({ - collection: 'workflow-runs', - where: { - workflow: { - equals: workflow.id - } - } - }) - - // Should have exactly 1 run (only for the matching condition) - expect(runs.totalDocs).toBe(1) - expect(runs.docs[0].status).not.toBe('failed') - - const auditLogs = await payload.find({ - collection: 'auditLog', - where: { - message: { - contains: 'condition met' - } - } - }) - - expect(auditLogs.totalDocs).toBe(1) - console.log('✅ Webhook conditional execution working') - }, 30000) - - it('should handle webhook authentication headers', async () => { - const workflow = await payload.create({ - collection: 'workflows', - data: { - name: 'Test Webhook - Headers', - description: 'Tests webhook header processing', - triggers: [ - { - type: 'webhook-trigger', - webhookPath: 'test-headers' - } - ], - steps: [ - { - name: 'process-headers', - step: 'http-request-step', - url: 'https://httpbin.org/post', - method: 'POST', - body: { - receivedHeaders: '$.trigger.headers', - authorization: '$.trigger.headers.authorization', - userAgent: '$.trigger.headers.user-agent' - } - } - ] - } - }) - - // Make webhook request with custom headers - const webhookUrl = `${baseUrl}/api/workflows/webhook/test-headers` - const response = await fetch(webhookUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer test-token-123', - 'User-Agent': 'Webhook-Test-Client/1.0', - 'X-Custom-Header': 'custom-value' - }, - body: JSON.stringify({ - test: 'header processing' - }) - }) - - expect(response.status).toBe(200) - - // Wait for workflow execution - await new Promise(resolve => setTimeout(resolve, 5000)) - - const runs = await payload.find({ - collection: 'workflow-runs', - where: { - workflow: { - equals: workflow.id - } - }, - limit: 1 - }) - - expect(runs.totalDocs).toBe(1) - expect(runs.docs[0].status).toBe('completed') - - // Verify headers were captured and processed - const stepOutput = runs.docs[0].context.steps['process-headers'].output - const responseBody = JSON.parse(stepOutput.body) - - expect(responseBody.json.authorization).toBe('Bearer test-token-123') - expect(responseBody.json.userAgent).toBe('Webhook-Test-Client/1.0') - - console.log('✅ Webhook headers processed correctly') - }, 30000) - - it('should handle multiple concurrent webhook requests', async () => { - const workflow = await payload.create({ - collection: 'workflows', - data: { - name: 'Test Webhook - Concurrent', - description: 'Tests concurrent webhook processing', - triggers: [ - { - type: 'webhook-trigger', - webhookPath: 'test-concurrent' - } - ], - steps: [ - { - name: 'concurrent-audit', - step: 'create-document', - collectionSlug: 'auditLog', - data: { - message: 'Concurrent webhook execution', - requestId: '$.trigger.data.requestId' - } - } - ] - } - }) - - // Make multiple concurrent webhook requests - const concurrentRequests = Array.from({ length: 5 }, (_, i) => - makeWebhookRequest('test-concurrent', { - requestId: `concurrent-${i + 1}`, - timestamp: new Date().toISOString() - }) - ) - - const responses = await Promise.all(concurrentRequests) - responses.forEach(response => { - expect(response.status).toBe(200) - }) - - // Wait for all workflow executions - await new Promise(resolve => setTimeout(resolve, 8000)) - - const runs = await payload.find({ - collection: 'workflow-runs', - where: { - workflow: { - equals: workflow.id - } - } - }) - - expect(runs.totalDocs).toBe(5) - - // Verify all runs completed successfully - const failedRuns = runs.docs.filter(run => run.status === 'failed') - expect(failedRuns).toHaveLength(0) - - // Verify all audit logs were created - const auditLogs = await payload.find({ - collection: 'auditLog', - where: { - message: { - contains: 'Concurrent webhook' - } - } - }) - - expect(auditLogs.totalDocs).toBe(5) - console.log('✅ Concurrent webhook requests processed successfully') - }, 35000) - - it('should handle non-existent webhook paths gracefully', async () => { - // Test that workflows with non-matching webhook paths don't get triggered - const workflow = await payload.create({ - collection: 'workflows', - data: { - name: 'Test Webhook - Non-existent Path', - description: 'Should not be triggered by different path', - triggers: [ - { - type: 'webhook-trigger', - webhookPath: 'specific-path' - } - ], - steps: [ - { - name: 'create-audit', - step: 'create-document', - collectionSlug: 'auditLog', - data: { - message: 'This should not be created' - } - } - ] - } - }) - - // Simulate trying to trigger with wrong path - should not execute workflow - const initialRuns = await payload.find({ - collection: 'workflow-runs', - where: { - workflow: { - equals: workflow.id - } - } - }) - - expect(initialRuns.totalDocs).toBe(0) - console.log('✅ Non-existent webhook path handled: no workflow runs created') - }, 10000) - - it('should handle malformed webhook JSON', async () => { - const webhookUrl = `${baseUrl}/api/workflows/webhook/test-malformed` - - // First create a workflow to receive the malformed request - const workflow = await payload.create({ - collection: 'workflows', - data: { - name: 'Test Webhook - Malformed JSON', - description: 'Tests malformed JSON handling', - triggers: [ - { - type: 'webhook-trigger', - webhookPath: 'test-malformed' - } - ], - steps: [ - { - name: 'malformed-test', - step: 'create-document', - collectionSlug: 'auditLog', - data: { - message: 'Processed malformed request' - } - } - ] - } - }) - - // Send malformed JSON - const response = await fetch(webhookUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: '{"malformed": json, "missing": quotes}' - }) - - // Should handle malformed JSON gracefully - expect([400, 422]).toContain(response.status) - console.log('✅ Malformed JSON handled:', response.status) - }, 15000) -}) \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index 5e32000..7b96edc 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -31,7 +31,9 @@ export default [ 'perfectionist/sort-object-types': 'off', 'perfectionist/sort-objects': 'off', 'perfectionist/sort-exports': 'off', - 'perfectionist/sort-imports': 'off' + 'perfectionist/sort-imports': 'off', + 'perfectionist/sort-switch-case': 'off', + 'perfectionist/sort-interfaces': 'off' }, }, { diff --git a/src/collections/Workflow.ts b/src/collections/Workflow.ts index 3026c2d..023e927 100644 --- a/src/collections/Workflow.ts +++ b/src/collections/Workflow.ts @@ -3,11 +3,11 @@ 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" +import {collectionTrigger, globalTrigger} from "../triggers/index.js" export const createWorkflowCollection: (options: WorkflowsPluginConfig) => CollectionConfig = (options) => { const steps = options.steps || [] - const triggers = (options.triggers || []).map(t => t(options)).concat(collectionHookTrigger(options)) + const triggers = (options.triggers || []).map(t => t(options)).concat(collectionTrigger(options), globalTrigger(options)) return { slug: 'workflows', access: { diff --git a/src/components/ErrorDisplay.tsx b/src/components/ErrorDisplay.tsx index 62b54a3..b5dafb6 100644 --- a/src/components/ErrorDisplay.tsx +++ b/src/components/ErrorDisplay.tsx @@ -10,10 +10,10 @@ interface ErrorDisplayProps { path?: string } -export const ErrorDisplay: React.FC = ({ - value, - onChange, - readOnly = false +export const ErrorDisplay: React.FC = ({ + value, + onChange, + readOnly = false }) => { const [expanded, setExpanded] = useState(false) @@ -32,7 +32,7 @@ export const ErrorDisplay: React.FC = ({ technical: error } } - + if (error.includes('Network error') || error.includes('fetch')) { return { type: 'network', @@ -41,7 +41,7 @@ export const ErrorDisplay: React.FC = ({ technical: error } } - + if (error.includes('Hook execution failed')) { return { type: 'hook', @@ -50,7 +50,7 @@ export const ErrorDisplay: React.FC = ({ technical: error } } - + if (error.includes('Executor not available')) { return { type: 'executor', @@ -59,7 +59,7 @@ export const ErrorDisplay: React.FC = ({ technical: error } } - + if (error.includes('Collection slug is required') || error.includes('Document data is required')) { return { type: 'validation', @@ -68,7 +68,7 @@ export const ErrorDisplay: React.FC = ({ technical: error } } - + if (error.includes('status') && error.includes('4')) { return { type: 'client', @@ -77,7 +77,7 @@ export const ErrorDisplay: React.FC = ({ technical: error } } - + if (error.includes('status') && error.includes('5')) { return { type: 'server', @@ -127,7 +127,7 @@ export const ErrorDisplay: React.FC = ({ const errorColor = getErrorColor(errorInfo.type) return ( -
= ({ marginTop: '8px' }}> {/* Error Header */} -
@@ -145,15 +145,15 @@ export const ErrorDisplay: React.FC = ({ {getErrorIcon(errorInfo.type)}
-

{errorInfo.title}

-

= ({

- + {expanded && (
= ({
{/* Quick Actions */} -
= ({ {/* Hidden textarea for editing if needed */} {!readOnly && onChange && (