mirror of
https://github.com/xtr-dev/payload-automation.git
synced 2025-12-10 00:43:23 +00:00
Remove unused plugin modules and their associated tests
- Delete `init-global-hooks.ts`, `init-step-tasks.ts`, `init-webhook.ts`, and `init-workflow-hooks.ts` - Remove obsolete components: `TriggerWorkflowButton` and `WorkflowExecutionStatus` - Clean up unused trigger files: `webhook-trigger.ts` - Delete webhook-related integration tests: `webhook-triggers.spec.ts` - Streamline related documentation and improve maintainability by eliminating deprecated code
This commit is contained in:
@@ -24,13 +24,24 @@ if (!process.env.ROOT_DIR) {
|
|||||||
const buildConfigWithMemoryDB = async () => {
|
const buildConfigWithMemoryDB = async () => {
|
||||||
// Use MongoDB adapter for testing instead of SQLite
|
// Use MongoDB adapter for testing instead of SQLite
|
||||||
const { mongooseAdapter } = await import('@payloadcms/db-mongodb')
|
const { mongooseAdapter } = await import('@payloadcms/db-mongodb')
|
||||||
|
|
||||||
return buildConfig({
|
return buildConfig({
|
||||||
admin: {
|
admin: {
|
||||||
importMap: {
|
importMap: {
|
||||||
baseDir: path.resolve(dirname, '..'),
|
baseDir: path.resolve(dirname, '..'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
globals: [
|
||||||
|
{
|
||||||
|
slug: 'settings',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'siteName',
|
||||||
|
type: 'text'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
collections: [
|
collections: [
|
||||||
{
|
{
|
||||||
slug: 'posts',
|
slug: 'posts',
|
||||||
@@ -96,14 +107,13 @@ const buildConfigWithMemoryDB = async () => {
|
|||||||
posts: true,
|
posts: true,
|
||||||
media: true
|
media: true
|
||||||
},
|
},
|
||||||
|
globalTriggers: {
|
||||||
|
settings: true
|
||||||
|
},
|
||||||
steps: [
|
steps: [
|
||||||
HttpRequestStepTask,
|
HttpRequestStepTask,
|
||||||
CreateDocumentStepTask
|
CreateDocumentStepTask
|
||||||
],
|
],
|
||||||
triggers: [
|
|
||||||
|
|
||||||
],
|
|
||||||
webhookPrefix: '/workflows-webhook'
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
secret: process.env.PAYLOAD_SECRET || 'test-secret_key',
|
secret: process.env.PAYLOAD_SECRET || 'test-secret_key',
|
||||||
|
|||||||
@@ -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)
|
|
||||||
})
|
|
||||||
@@ -31,7 +31,9 @@ export default [
|
|||||||
'perfectionist/sort-object-types': 'off',
|
'perfectionist/sort-object-types': 'off',
|
||||||
'perfectionist/sort-objects': 'off',
|
'perfectionist/sort-objects': 'off',
|
||||||
'perfectionist/sort-exports': 'off',
|
'perfectionist/sort-exports': 'off',
|
||||||
'perfectionist/sort-imports': 'off'
|
'perfectionist/sort-imports': 'off',
|
||||||
|
'perfectionist/sort-switch-case': 'off',
|
||||||
|
'perfectionist/sort-interfaces': 'off'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import type {CollectionConfig} from 'payload'
|
|||||||
import type {WorkflowsPluginConfig} from "../plugin/config-types.js"
|
import type {WorkflowsPluginConfig} from "../plugin/config-types.js"
|
||||||
|
|
||||||
import {parameter} from "../fields/parameter.js"
|
import {parameter} from "../fields/parameter.js"
|
||||||
import {collectionHookTrigger} from "../triggers/index.js"
|
import {collectionTrigger, globalTrigger} from "../triggers/index.js"
|
||||||
|
|
||||||
export const createWorkflowCollection: <T extends string>(options: WorkflowsPluginConfig<T>) => CollectionConfig = (options) => {
|
export const createWorkflowCollection: <T extends string>(options: WorkflowsPluginConfig<T>) => CollectionConfig = (options) => {
|
||||||
const steps = options.steps || []
|
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 {
|
return {
|
||||||
slug: 'workflows',
|
slug: 'workflows',
|
||||||
access: {
|
access: {
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ interface ErrorDisplayProps {
|
|||||||
path?: string
|
path?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
readOnly = false
|
readOnly = false
|
||||||
}) => {
|
}) => {
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|||||||
technical: error
|
technical: error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.includes('Network error') || error.includes('fetch')) {
|
if (error.includes('Network error') || error.includes('fetch')) {
|
||||||
return {
|
return {
|
||||||
type: 'network',
|
type: 'network',
|
||||||
@@ -41,7 +41,7 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|||||||
technical: error
|
technical: error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.includes('Hook execution failed')) {
|
if (error.includes('Hook execution failed')) {
|
||||||
return {
|
return {
|
||||||
type: 'hook',
|
type: 'hook',
|
||||||
@@ -50,7 +50,7 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|||||||
technical: error
|
technical: error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.includes('Executor not available')) {
|
if (error.includes('Executor not available')) {
|
||||||
return {
|
return {
|
||||||
type: 'executor',
|
type: 'executor',
|
||||||
@@ -59,7 +59,7 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|||||||
technical: error
|
technical: error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.includes('Collection slug is required') || error.includes('Document data is required')) {
|
if (error.includes('Collection slug is required') || error.includes('Document data is required')) {
|
||||||
return {
|
return {
|
||||||
type: 'validation',
|
type: 'validation',
|
||||||
@@ -68,7 +68,7 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|||||||
technical: error
|
technical: error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.includes('status') && error.includes('4')) {
|
if (error.includes('status') && error.includes('4')) {
|
||||||
return {
|
return {
|
||||||
type: 'client',
|
type: 'client',
|
||||||
@@ -77,7 +77,7 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|||||||
technical: error
|
technical: error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.includes('status') && error.includes('5')) {
|
if (error.includes('status') && error.includes('5')) {
|
||||||
return {
|
return {
|
||||||
type: 'server',
|
type: 'server',
|
||||||
@@ -127,7 +127,7 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|||||||
const errorColor = getErrorColor(errorInfo.type)
|
const errorColor = getErrorColor(errorInfo.type)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
border: `2px solid ${errorColor}30`,
|
border: `2px solid ${errorColor}30`,
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
backgroundColor: `${errorColor}08`,
|
backgroundColor: `${errorColor}08`,
|
||||||
@@ -135,9 +135,9 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|||||||
marginTop: '8px'
|
marginTop: '8px'
|
||||||
}}>
|
}}>
|
||||||
{/* Error Header */}
|
{/* Error Header */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '12px',
|
gap: '12px',
|
||||||
marginBottom: '12px'
|
marginBottom: '12px'
|
||||||
}}>
|
}}>
|
||||||
@@ -145,15 +145,15 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|||||||
{getErrorIcon(errorInfo.type)}
|
{getErrorIcon(errorInfo.type)}
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<h4 style={{
|
<h4 style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
color: errorColor,
|
color: errorColor,
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
fontWeight: '600'
|
fontWeight: '600'
|
||||||
}}>
|
}}>
|
||||||
{errorInfo.title}
|
{errorInfo.title}
|
||||||
</h4>
|
</h4>
|
||||||
<p style={{
|
<p style={{
|
||||||
margin: '4px 0 0 0',
|
margin: '4px 0 0 0',
|
||||||
color: '#6B7280',
|
color: '#6B7280',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
@@ -168,14 +168,14 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: expanded ? '12px' : '0' }}>
|
<div style={{ marginBottom: expanded ? '12px' : '0' }}>
|
||||||
<Button
|
<Button
|
||||||
|
buttonStyle="secondary"
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
size="small"
|
size="small"
|
||||||
buttonStyle="secondary"
|
|
||||||
>
|
>
|
||||||
{expanded ? 'Hide' : 'Show'} Technical Details
|
{expanded ? 'Hide' : 'Show'} Technical Details
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div style={{
|
<div style={{
|
||||||
backgroundColor: '#F8F9FA',
|
backgroundColor: '#F8F9FA',
|
||||||
@@ -194,7 +194,7 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Actions */}
|
{/* Quick Actions */}
|
||||||
<div style={{
|
<div style={{
|
||||||
marginTop: '12px',
|
marginTop: '12px',
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
backgroundColor: `${errorColor}10`,
|
backgroundColor: `${errorColor}10`,
|
||||||
@@ -253,11 +253,11 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|||||||
{/* Hidden textarea for editing if needed */}
|
{/* Hidden textarea for editing if needed */}
|
||||||
{!readOnly && onChange && (
|
{!readOnly && onChange && (
|
||||||
<textarea
|
<textarea
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { Button, toast } from '@payloadcms/ui'
|
|
||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
interface TriggerWorkflowButtonProps {
|
|
||||||
workflowId: string
|
|
||||||
workflowName: string
|
|
||||||
triggerSlug?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TriggerWorkflowButton: React.FC<TriggerWorkflowButtonProps> = ({
|
|
||||||
workflowId,
|
|
||||||
workflowName,
|
|
||||||
triggerSlug = 'manual-trigger'
|
|
||||||
}) => {
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
|
|
||||||
const handleTrigger = async () => {
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/workflows/trigger-custom', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
workflowId,
|
|
||||||
triggerSlug,
|
|
||||||
data: {
|
|
||||||
triggeredAt: new Date().toISOString(),
|
|
||||||
source: 'admin-button'
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.message || 'Failed to trigger workflow')
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
|
|
||||||
toast.success(`Workflow "${workflowName}" triggered successfully! Run ID: ${result.runId}`)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error triggering workflow:', error)
|
|
||||||
toast.error(`Failed to trigger workflow: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
onClick={handleTrigger}
|
|
||||||
disabled={loading}
|
|
||||||
size="small"
|
|
||||||
buttonStyle="secondary"
|
|
||||||
>
|
|
||||||
{loading ? 'Triggering...' : 'Trigger Workflow'}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react'
|
|
||||||
import { Button } from '@payloadcms/ui'
|
|
||||||
|
|
||||||
interface WorkflowRun {
|
|
||||||
id: string
|
|
||||||
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
|
|
||||||
startedAt: string
|
|
||||||
completedAt?: string
|
|
||||||
error?: string
|
|
||||||
triggeredBy: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WorkflowExecutionStatusProps {
|
|
||||||
workflowId: string | number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WorkflowExecutionStatus: React.FC<WorkflowExecutionStatusProps> = ({ workflowId }) => {
|
|
||||||
const [runs, setRuns] = useState<WorkflowRun[]>([])
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [expanded, setExpanded] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchRecentRuns = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/workflow-runs?where[workflow][equals]=${workflowId}&limit=5&sort=-startedAt`)
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json()
|
|
||||||
setRuns(data.docs || [])
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to fetch workflow runs:', error)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchRecentRuns()
|
|
||||||
}, [workflowId])
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: '16px', color: '#6B7280' }}>
|
|
||||||
Loading execution history...
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (runs.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'pending': return '⏳'
|
|
||||||
case 'running': return '🔄'
|
|
||||||
case 'completed': return '✅'
|
|
||||||
case 'failed': return '❌'
|
|
||||||
case 'cancelled': return '⏹️'
|
|
||||||
default: return '❓'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'pending': return '#6B7280'
|
|
||||||
case 'running': return '#3B82F6'
|
|
||||||
case 'completed': return '#10B981'
|
|
||||||
case 'failed': return '#EF4444'
|
|
||||||
case 'cancelled': return '#F59E0B'
|
|
||||||
default: return '#6B7280'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
|
||||||
const date = new Date(dateString)
|
|
||||||
const now = new Date()
|
|
||||||
const diffMs = now.getTime() - date.getTime()
|
|
||||||
|
|
||||||
if (diffMs < 60000) { // Less than 1 minute
|
|
||||||
return 'Just now'
|
|
||||||
} else if (diffMs < 3600000) { // Less than 1 hour
|
|
||||||
return `${Math.floor(diffMs / 60000)} min ago`
|
|
||||||
} else if (diffMs < 86400000) { // Less than 1 day
|
|
||||||
return `${Math.floor(diffMs / 3600000)} hrs ago`
|
|
||||||
} else {
|
|
||||||
return date.toLocaleDateString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDuration = (startedAt: string, completedAt?: string) => {
|
|
||||||
const start = new Date(startedAt)
|
|
||||||
const end = completedAt ? new Date(completedAt) : new Date()
|
|
||||||
const diffMs = end.getTime() - start.getTime()
|
|
||||||
|
|
||||||
if (diffMs < 1000) return '<1s'
|
|
||||||
if (diffMs < 60000) return `${Math.floor(diffMs / 1000)}s`
|
|
||||||
if (diffMs < 3600000) return `${Math.floor(diffMs / 60000)}m ${Math.floor((diffMs % 60000) / 1000)}s`
|
|
||||||
return `${Math.floor(diffMs / 3600000)}h ${Math.floor((diffMs % 3600000) / 60000)}m`
|
|
||||||
}
|
|
||||||
|
|
||||||
const recentRun = runs[0]
|
|
||||||
const recentStatus = getStatusIcon(recentRun.status)
|
|
||||||
const recentColor = getStatusColor(recentRun.status)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{
|
|
||||||
border: '1px solid #E5E7EB',
|
|
||||||
borderRadius: '8px',
|
|
||||||
backgroundColor: '#FAFAFA'
|
|
||||||
}}>
|
|
||||||
{/* Summary Header */}
|
|
||||||
<div style={{
|
|
||||||
padding: '16px',
|
|
||||||
borderBottom: expanded ? '1px solid #E5E7EB' : 'none',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center'
|
|
||||||
}}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
||||||
<span style={{ fontSize: '20px' }}>{recentStatus}</span>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontWeight: '600', color: recentColor }}>
|
|
||||||
Last run: {recentRun.status}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '13px', color: '#6B7280' }}>
|
|
||||||
{formatDate(recentRun.startedAt)} • Duration: {getDuration(recentRun.startedAt, recentRun.completedAt)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => setExpanded(!expanded)}
|
|
||||||
size="small"
|
|
||||||
buttonStyle="secondary"
|
|
||||||
>
|
|
||||||
{expanded ? 'Hide' : 'Show'} History ({runs.length})
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Detailed History */}
|
|
||||||
{expanded && (
|
|
||||||
<div style={{ padding: '16px' }}>
|
|
||||||
<h4 style={{ margin: '0 0 12px 0', fontSize: '14px', fontWeight: '600' }}>
|
|
||||||
Recent Executions
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
{runs.map((run, index) => (
|
|
||||||
<div
|
|
||||||
key={run.id}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '8px 12px',
|
|
||||||
marginBottom: index < runs.length - 1 ? '8px' : '0',
|
|
||||||
backgroundColor: 'white',
|
|
||||||
border: '1px solid #E5E7EB',
|
|
||||||
borderRadius: '6px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
||||||
<span style={{ fontSize: '16px' }}>
|
|
||||||
{getStatusIcon(run.status)}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div style={{
|
|
||||||
fontSize: '13px',
|
|
||||||
fontWeight: '500',
|
|
||||||
color: getStatusColor(run.status)
|
|
||||||
}}>
|
|
||||||
{run.status.charAt(0).toUpperCase() + run.status.slice(1)}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '12px', color: '#6B7280' }}>
|
|
||||||
{formatDate(run.startedAt)} • {run.triggeredBy}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
fontSize: '12px',
|
|
||||||
color: '#6B7280',
|
|
||||||
textAlign: 'right'
|
|
||||||
}}>
|
|
||||||
<div>
|
|
||||||
{getDuration(run.startedAt, run.completedAt)}
|
|
||||||
</div>
|
|
||||||
{run.error && (
|
|
||||||
<div style={{ color: '#EF4444', marginTop: '2px' }}>
|
|
||||||
Error
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
marginTop: '12px',
|
|
||||||
textAlign: 'center'
|
|
||||||
}}>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
// Navigate to workflow runs filtered by this workflow
|
|
||||||
window.location.href = `/admin/collections/workflow-runs?where[workflow][equals]=${workflowId}`
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
buttonStyle="secondary"
|
|
||||||
>
|
|
||||||
View All Runs →
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ export type PayloadWorkflow = {
|
|||||||
parameters?: {
|
parameters?: {
|
||||||
collectionSlug?: null | string
|
collectionSlug?: null | string
|
||||||
operation?: null | string
|
operation?: null | string
|
||||||
webhookPath?: null | string
|
|
||||||
global?: null | string
|
global?: null | string
|
||||||
globalOperation?: null | string
|
globalOperation?: null | string
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
@@ -43,54 +42,8 @@ export type WorkflowTrigger = {
|
|||||||
} & NonNullable<PayloadWorkflow['triggers']>[0]
|
} & NonNullable<PayloadWorkflow['triggers']>[0]
|
||||||
|
|
||||||
export interface ExecutionContext {
|
export interface ExecutionContext {
|
||||||
steps: Record<string, {
|
steps: Record<string, any>
|
||||||
error?: string
|
trigger: Record<string, any>
|
||||||
input: unknown
|
|
||||||
output: unknown
|
|
||||||
state: 'failed' | 'pending' | 'running' | 'succeeded'
|
|
||||||
_startTime?: number
|
|
||||||
executionInfo?: {
|
|
||||||
completed: boolean
|
|
||||||
success: boolean
|
|
||||||
executedAt: string
|
|
||||||
duration: number
|
|
||||||
failureReason?: string
|
|
||||||
}
|
|
||||||
errorDetails?: {
|
|
||||||
stepId: string
|
|
||||||
errorType: string
|
|
||||||
duration: number
|
|
||||||
attempts: number
|
|
||||||
finalError: string
|
|
||||||
context: {
|
|
||||||
url?: string
|
|
||||||
method?: string
|
|
||||||
timeout?: number
|
|
||||||
statusCode?: number
|
|
||||||
headers?: Record<string, string>
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
timestamp: string
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
trigger: {
|
|
||||||
collection?: string
|
|
||||||
data?: unknown
|
|
||||||
doc?: unknown
|
|
||||||
headers?: Record<string, string>
|
|
||||||
operation?: string
|
|
||||||
path?: string
|
|
||||||
previousDoc?: unknown
|
|
||||||
req?: PayloadRequest
|
|
||||||
triggeredAt?: string
|
|
||||||
type: string
|
|
||||||
user?: {
|
|
||||||
collection?: string
|
|
||||||
email?: string
|
|
||||||
id?: string
|
|
||||||
}
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WorkflowExecutor {
|
export class WorkflowExecutor {
|
||||||
@@ -229,7 +182,7 @@ export class WorkflowExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also extract from nested parameters object if it exists
|
// Also extract from nested parameters object if it exists
|
||||||
if (step.parameters && typeof step.parameters === 'object') {
|
if (step.parameters && typeof step.parameters === 'object') {
|
||||||
Object.assign(inputFields, step.parameters)
|
Object.assign(inputFields, step.parameters)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Client-side components that may have CSS imports or PayloadCMS UI dependencies
|
// Client-side components that may have CSS imports or PayloadCMS UI dependencies
|
||||||
// These are separated to avoid CSS import errors during Node.js type generation
|
// These are separated to avoid CSS import errors during Node.js type generation
|
||||||
|
|
||||||
export { TriggerWorkflowButton } from '../components/TriggerWorkflowButton.js'
|
|
||||||
export { StatusCell } from '../components/StatusCell.js'
|
export { StatusCell } from '../components/StatusCell.js'
|
||||||
export { ErrorDisplay } from '../components/ErrorDisplay.js'
|
export { ErrorDisplay } from '../components/ErrorDisplay.js'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {WorkflowExecutor} from "../core/workflow-executor.js"
|
import {WorkflowExecutor} from "../core/workflow-executor.js"
|
||||||
|
|
||||||
export const createCollectionTriggerHook = (collectionSlug: string, hookType: string) => {
|
export const createCollectionTriggerHook = (collectionSlug: string, hookType: string) => {
|
||||||
return async (args: HookArgs) => {
|
return async (args: any) => {
|
||||||
const req = 'req' in args ? args.req :
|
const req = 'req' in args ? args.req :
|
||||||
'args' in args ? args.args.req :
|
'args' in args ? args.args.req :
|
||||||
undefined
|
undefined
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import type {CollectionConfig, TaskConfig} from "payload"
|
import type {CollectionConfig, GlobalConfig, TaskConfig} from "payload"
|
||||||
|
|
||||||
import type {Trigger} from "../triggers/types.js"
|
import type {Trigger} from "../triggers/types.js"
|
||||||
|
|
||||||
export type TriggerConfig = (config: WorkflowsPluginConfig) => Trigger
|
export type TriggerConfig = (config: WorkflowsPluginConfig) => Trigger
|
||||||
|
|
||||||
export type WorkflowsPluginConfig<TSlug extends string = string> = {
|
export type WorkflowsPluginConfig<TSlug extends string = string, TGlobal extends string = string> = {
|
||||||
collectionTriggers?: {
|
collectionTriggers?: {
|
||||||
[key in TSlug]?: {
|
[key in TSlug]?: {
|
||||||
[key in keyof CollectionConfig['hooks']]?: true
|
[key in keyof CollectionConfig['hooks']]?: true
|
||||||
} | true
|
} | true
|
||||||
}
|
}
|
||||||
|
globalTriggers?: {
|
||||||
|
[key in TGlobal]?: {
|
||||||
|
[key in keyof GlobalConfig['hooks']]?: true
|
||||||
|
} | true
|
||||||
|
}
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
steps: TaskConfig<string>[]
|
steps: TaskConfig<string>[]
|
||||||
triggers?: TriggerConfig[]
|
triggers?: TriggerConfig[]
|
||||||
webhookPrefix?: string
|
|
||||||
}
|
}
|
||||||
|
|||||||
95
src/plugin/global-hook.ts
Normal file
95
src/plugin/global-hook.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import {WorkflowExecutor} from '../core/workflow-executor.js'
|
||||||
|
|
||||||
|
export const createGlobalTriggerHook = (globalSlug: string, hookType: string) => {
|
||||||
|
return async function payloadGlobalAutomationHook(args: any) {
|
||||||
|
const req = 'req' in args ? args.req :
|
||||||
|
'args' in args ? args.args.req :
|
||||||
|
undefined
|
||||||
|
if (!req) {
|
||||||
|
throw new Error('No request object found in global hook arguments')
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = req.payload
|
||||||
|
const logger = payload.logger
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info({
|
||||||
|
global: globalSlug,
|
||||||
|
hookType,
|
||||||
|
operation: hookType
|
||||||
|
}, 'Global automation hook triggered')
|
||||||
|
|
||||||
|
// Create executor on-demand
|
||||||
|
const executor = new WorkflowExecutor(payload, logger)
|
||||||
|
|
||||||
|
logger.debug('Executing triggered global workflows...')
|
||||||
|
|
||||||
|
// Find workflows with matching global triggers
|
||||||
|
const {docs: workflows} = await payload.find({
|
||||||
|
collection: 'workflows',
|
||||||
|
depth: 2,
|
||||||
|
limit: 100,
|
||||||
|
where: {
|
||||||
|
'triggers.parameters.global': {
|
||||||
|
equals: globalSlug
|
||||||
|
},
|
||||||
|
'triggers.parameters.operation': {
|
||||||
|
equals: hookType
|
||||||
|
},
|
||||||
|
'triggers.type': {
|
||||||
|
equals: 'global-hook'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Execute each matching workflow
|
||||||
|
for (const workflow of workflows) {
|
||||||
|
// Create execution context
|
||||||
|
const context = {
|
||||||
|
steps: {},
|
||||||
|
trigger: {
|
||||||
|
...args,
|
||||||
|
type: 'global',
|
||||||
|
global: globalSlug,
|
||||||
|
operation: hookType,
|
||||||
|
req
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await executor.execute(workflow, context, req)
|
||||||
|
logger.info({
|
||||||
|
workflowId: workflow.id,
|
||||||
|
global: globalSlug,
|
||||||
|
hookType
|
||||||
|
}, 'Global workflow executed successfully')
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({
|
||||||
|
workflowId: workflow.id,
|
||||||
|
global: globalSlug,
|
||||||
|
hookType,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
}, 'Global workflow execution failed')
|
||||||
|
// Don't throw to prevent breaking the original operation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
global: globalSlug,
|
||||||
|
hookType
|
||||||
|
}, 'Global workflow execution completed successfully')
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
|
||||||
|
logger.error({
|
||||||
|
global: globalSlug,
|
||||||
|
hookType,
|
||||||
|
error: errorMessage,
|
||||||
|
errorStack: error instanceof Error ? error.stack : undefined
|
||||||
|
}, 'Global hook execution failed')
|
||||||
|
|
||||||
|
// Don't throw to prevent breaking the original operation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,9 @@ import type {WorkflowsPluginConfig} from "./config-types.js"
|
|||||||
import {createWorkflowCollection} from '../collections/Workflow.js'
|
import {createWorkflowCollection} from '../collections/Workflow.js'
|
||||||
import {WorkflowRunsCollection} from '../collections/WorkflowRuns.js'
|
import {WorkflowRunsCollection} from '../collections/WorkflowRuns.js'
|
||||||
import {WorkflowExecutor} from '../core/workflow-executor.js'
|
import {WorkflowExecutor} from '../core/workflow-executor.js'
|
||||||
import {initGlobalHooks} from "./init-global-hooks.js"
|
|
||||||
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 {getConfigLogger, initializeLogger} from './logger.js'
|
||||||
import {createCollectionTriggerHook} from "./collection-hook.js"
|
import {createCollectionTriggerHook} from "./collection-hook.js"
|
||||||
|
import {createGlobalTriggerHook} from "./global-hook.js"
|
||||||
|
|
||||||
export {getLogger} from './logger.js'
|
export {getLogger} from './logger.js'
|
||||||
|
|
||||||
@@ -114,6 +111,69 @@ export const workflowsPlugin =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle global triggers similarly to collection triggers
|
||||||
|
if (config.globals && pluginOptions.globalTriggers) {
|
||||||
|
for (const [globalSlug, triggerConfig] of Object.entries(pluginOptions.globalTriggers)) {
|
||||||
|
if (!triggerConfig) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the global config that matches
|
||||||
|
const globalIndex = config.globals.findIndex(g => g.slug === globalSlug)
|
||||||
|
if (globalIndex === -1) {
|
||||||
|
logger.warn(`Global '${globalSlug}' not found in config.globals`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const global = config.globals[globalIndex]
|
||||||
|
|
||||||
|
// Initialize hooks if needed
|
||||||
|
if (!global.hooks) {
|
||||||
|
global.hooks = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which hooks to register based on config
|
||||||
|
const hooksToRegister = triggerConfig === true
|
||||||
|
? {
|
||||||
|
afterChange: true,
|
||||||
|
afterRead: true,
|
||||||
|
}
|
||||||
|
: triggerConfig
|
||||||
|
|
||||||
|
// Register each configured hook
|
||||||
|
Object.entries(hooksToRegister).forEach(([hookName, enabled]) => {
|
||||||
|
if (!enabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookKey = hookName as keyof typeof global.hooks
|
||||||
|
|
||||||
|
// Initialize the hook array if needed
|
||||||
|
if (!global.hooks![hookKey]) {
|
||||||
|
global.hooks![hookKey] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the automation hook for this specific global and hook type
|
||||||
|
const automationHook = createGlobalTriggerHook(globalSlug, 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 global
|
||||||
|
;(global.hooks![hookKey] as Array<unknown>).push(automationHook)
|
||||||
|
|
||||||
|
logger.debug(`Registered ${hookKey} hook for global '${globalSlug}'`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!config.jobs) {
|
if (!config.jobs) {
|
||||||
config.jobs = {tasks: []}
|
config.jobs = {tasks: []}
|
||||||
}
|
}
|
||||||
@@ -124,8 +184,6 @@ export const workflowsPlugin =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize webhook endpoint
|
|
||||||
initWebhookEndpoint(config, pluginOptions.webhookPrefix || 'webhook')
|
|
||||||
|
|
||||||
// Set up onInit to initialize features
|
// Set up onInit to initialize features
|
||||||
const incomingOnInit = config.onInit
|
const incomingOnInit = config.onInit
|
||||||
@@ -139,19 +197,8 @@ export const workflowsPlugin =
|
|||||||
const logger = initializeLogger(payload)
|
const logger = initializeLogger(payload)
|
||||||
logger.info('Logger initialized with payload instance')
|
logger.info('Logger initialized with payload instance')
|
||||||
|
|
||||||
// Log collection trigger configuration
|
// Log trigger configuration
|
||||||
logger.info(`Plugin configuration: ${Object.keys(pluginOptions.collectionTriggers || {}).length} collection triggers, ${pluginOptions.steps?.length || 0} steps`)
|
logger.info(`Plugin configuration: ${Object.keys(pluginOptions.collectionTriggers || {}).length} collection triggers, ${Object.keys(pluginOptions.globalTriggers || {}).length} global triggers, ${pluginOptions.steps?.length || 0} steps`)
|
||||||
|
|
||||||
logger.info('Initializing global hooks...')
|
|
||||||
// Create executor for global hooks
|
|
||||||
const executor = new WorkflowExecutor(payload, logger)
|
|
||||||
initGlobalHooks(payload, logger, executor)
|
|
||||||
|
|
||||||
logger.info('Initializing workflow hooks...')
|
|
||||||
initWorkflowHooks(payload, logger)
|
|
||||||
|
|
||||||
logger.info('Initializing step tasks...')
|
|
||||||
initStepTasks(pluginOptions, payload, logger)
|
|
||||||
|
|
||||||
logger.info('Plugin initialized successfully - all hooks registered')
|
logger.info('Plugin initialized successfully - all hooks registered')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
import type { Payload, PayloadRequest } from "payload"
|
|
||||||
import type { Logger } from "pino"
|
|
||||||
|
|
||||||
import type { WorkflowExecutor, PayloadWorkflow } from "../core/workflow-executor.js"
|
|
||||||
|
|
||||||
export function initGlobalHooks(payload: Payload, logger: Payload['logger'], executor: WorkflowExecutor) {
|
|
||||||
// Get all globals from the config
|
|
||||||
const globals = payload.config.globals || []
|
|
||||||
|
|
||||||
for (const globalConfig of globals) {
|
|
||||||
const globalSlug = globalConfig.slug
|
|
||||||
|
|
||||||
// Add afterChange hook to global
|
|
||||||
if (!globalConfig.hooks) {
|
|
||||||
globalConfig.hooks = {
|
|
||||||
afterChange: [],
|
|
||||||
afterRead: [],
|
|
||||||
beforeChange: [],
|
|
||||||
beforeRead: [],
|
|
||||||
beforeValidate: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!globalConfig.hooks.afterChange) {
|
|
||||||
globalConfig.hooks.afterChange = []
|
|
||||||
}
|
|
||||||
|
|
||||||
globalConfig.hooks.afterChange.push(async (change) => {
|
|
||||||
logger.debug({
|
|
||||||
global: globalSlug,
|
|
||||||
operation: 'update'
|
|
||||||
}, 'Global hook triggered')
|
|
||||||
|
|
||||||
// Execute workflows for this global trigger
|
|
||||||
await executeTriggeredGlobalWorkflows(
|
|
||||||
globalSlug,
|
|
||||||
'update',
|
|
||||||
change.doc,
|
|
||||||
change.previousDoc,
|
|
||||||
change.req,
|
|
||||||
payload,
|
|
||||||
logger,
|
|
||||||
executor
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info({ globalSlug }, 'Global hooks registered')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeTriggeredGlobalWorkflows(
|
|
||||||
globalSlug: string,
|
|
||||||
operation: 'update',
|
|
||||||
doc: Record<string, any>,
|
|
||||||
previousDoc: Record<string, any>,
|
|
||||||
req: PayloadRequest,
|
|
||||||
payload: Payload,
|
|
||||||
logger: Payload['logger'],
|
|
||||||
executor: WorkflowExecutor
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Find workflows with matching global triggers
|
|
||||||
const workflows = await payload.find({
|
|
||||||
collection: 'workflows',
|
|
||||||
depth: 2,
|
|
||||||
limit: 100,
|
|
||||||
req,
|
|
||||||
where: {
|
|
||||||
'triggers.global': {
|
|
||||||
equals: globalSlug
|
|
||||||
},
|
|
||||||
'triggers.globalOperation': {
|
|
||||||
equals: operation
|
|
||||||
},
|
|
||||||
'triggers.type': {
|
|
||||||
equals: 'global-trigger'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const workflow of workflows.docs) {
|
|
||||||
logger.info({
|
|
||||||
globalSlug,
|
|
||||||
operation,
|
|
||||||
workflowId: workflow.id,
|
|
||||||
workflowName: workflow.name
|
|
||||||
}, 'Triggering global workflow')
|
|
||||||
|
|
||||||
// Create execution context
|
|
||||||
const context = {
|
|
||||||
steps: {},
|
|
||||||
trigger: {
|
|
||||||
type: 'global',
|
|
||||||
doc,
|
|
||||||
global: globalSlug,
|
|
||||||
operation,
|
|
||||||
previousDoc,
|
|
||||||
req
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the workflow
|
|
||||||
await executor.execute(workflow as PayloadWorkflow, context, req)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
globalSlug,
|
|
||||||
operation
|
|
||||||
}, 'Failed to execute triggered global workflows')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import type {Payload} from "payload"
|
|
||||||
import type {Logger} from "pino"
|
|
||||||
|
|
||||||
import type {WorkflowsPluginConfig} from "./config-types.js"
|
|
||||||
|
|
||||||
export function initStepTasks<T extends string>(pluginOptions: WorkflowsPluginConfig<T>, payload: Payload, logger: Payload['logger']) {
|
|
||||||
logger.info({ stepCount: pluginOptions.steps.length, steps: pluginOptions.steps.map(s => s.slug) }, 'Step tasks were registered during config phase')
|
|
||||||
|
|
||||||
// Verify that the tasks are available in the job system
|
|
||||||
const availableTasks = payload.config.jobs?.tasks?.map(t => t.slug) || []
|
|
||||||
const pluginTasks = pluginOptions.steps.map(s => s.slug)
|
|
||||||
|
|
||||||
pluginTasks.forEach(taskSlug => {
|
|
||||||
if (availableTasks.includes(taskSlug)) {
|
|
||||||
logger.info({ taskSlug }, 'Step task confirmed available in job system')
|
|
||||||
} else {
|
|
||||||
logger.error({ taskSlug }, 'Step task not found in job system - this will cause execution failures')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
import type {Config, PayloadRequest} from 'payload'
|
|
||||||
|
|
||||||
import {type PayloadWorkflow, WorkflowExecutor} from '../core/workflow-executor.js'
|
|
||||||
import {getConfigLogger, initializeLogger} from './logger.js'
|
|
||||||
|
|
||||||
export function initWebhookEndpoint(config: Config, webhookPrefix = 'webhook'): void {
|
|
||||||
const logger = getConfigLogger()
|
|
||||||
// Ensure the prefix starts with a slash
|
|
||||||
const normalizedPrefix = webhookPrefix.startsWith('/') ? webhookPrefix : `/${webhookPrefix}`
|
|
||||||
|
|
||||||
// Define webhook endpoint
|
|
||||||
const webhookEndpoint = {
|
|
||||||
handler: async (req: PayloadRequest) => {
|
|
||||||
const {path} = req.routeParams as { path: string }
|
|
||||||
const webhookData = req.body || {}
|
|
||||||
|
|
||||||
logger.debug('Webhook endpoint handler called, path: ' + path)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Find workflows with matching webhook triggers
|
|
||||||
const workflows = await req.payload.find({
|
|
||||||
collection: 'workflows',
|
|
||||||
depth: 2,
|
|
||||||
limit: 100,
|
|
||||||
req,
|
|
||||||
where: {
|
|
||||||
'triggers.type': {
|
|
||||||
equals: 'webhook-trigger'
|
|
||||||
},
|
|
||||||
'triggers.webhookPath': {
|
|
||||||
equals: path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (workflows.docs.length === 0) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({error: 'No workflows found for this webhook path'}),
|
|
||||||
{
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
status: 404
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a workflow executor for this request
|
|
||||||
const logger = initializeLogger(req.payload)
|
|
||||||
const executor = new WorkflowExecutor(req.payload, logger)
|
|
||||||
|
|
||||||
const executionPromises = workflows.docs.map(async (workflow) => {
|
|
||||||
try {
|
|
||||||
// Create execution context for the webhook trigger
|
|
||||||
const context = {
|
|
||||||
steps: {},
|
|
||||||
trigger: {
|
|
||||||
type: 'webhook',
|
|
||||||
data: webhookData,
|
|
||||||
headers: Object.fromEntries(req.headers?.entries() || []),
|
|
||||||
path,
|
|
||||||
req
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the matching trigger and check its condition if present
|
|
||||||
const triggers = workflow.triggers as Array<{
|
|
||||||
condition?: string
|
|
||||||
type: string
|
|
||||||
parameters?: {
|
|
||||||
webhookPath?: string
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
|
|
||||||
const matchingTrigger = triggers?.find(trigger =>
|
|
||||||
trigger.type === 'webhook-trigger' &&
|
|
||||||
trigger.parameters?.webhookPath === path
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check trigger condition if present
|
|
||||||
if (matchingTrigger?.condition) {
|
|
||||||
logger.debug({
|
|
||||||
condition: matchingTrigger.condition,
|
|
||||||
path,
|
|
||||||
webhookData: JSON.stringify(webhookData).substring(0, 200),
|
|
||||||
headers: Object.keys(context.trigger.headers || {}),
|
|
||||||
workflowId: workflow.id,
|
|
||||||
workflowName: workflow.name
|
|
||||||
}, 'Evaluating webhook trigger condition')
|
|
||||||
|
|
||||||
const conditionMet = executor.evaluateCondition(matchingTrigger.condition, context)
|
|
||||||
|
|
||||||
if (!conditionMet) {
|
|
||||||
logger.info({
|
|
||||||
condition: matchingTrigger.condition,
|
|
||||||
path,
|
|
||||||
webhookDataSnapshot: JSON.stringify(webhookData).substring(0, 200),
|
|
||||||
workflowId: workflow.id,
|
|
||||||
workflowName: workflow.name
|
|
||||||
}, 'Webhook trigger condition not met, skipping workflow')
|
|
||||||
|
|
||||||
return { reason: 'Condition not met', status: 'skipped', workflowId: workflow.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info({
|
|
||||||
condition: matchingTrigger.condition,
|
|
||||||
path,
|
|
||||||
webhookDataSnapshot: JSON.stringify(webhookData).substring(0, 200),
|
|
||||||
workflowId: workflow.id,
|
|
||||||
workflowName: workflow.name
|
|
||||||
}, 'Webhook trigger condition met')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the workflow
|
|
||||||
await executor.execute(workflow as PayloadWorkflow, context, req)
|
|
||||||
|
|
||||||
return { status: 'triggered', workflowId: workflow.id }
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
status: 'failed',
|
|
||||||
workflowId: workflow.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const results = await Promise.allSettled(executionPromises)
|
|
||||||
const resultsData = results.map((result, index) => {
|
|
||||||
const baseResult = { workflowId: workflows.docs[index].id }
|
|
||||||
if (result.status === 'fulfilled') {
|
|
||||||
return { ...baseResult, ...result.value }
|
|
||||||
} else {
|
|
||||||
return { ...baseResult, error: result.reason, status: 'failed' }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
message: `Triggered ${workflows.docs.length} workflow(s)`,
|
|
||||||
results: resultsData
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
status: 200
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
details: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
error: 'Failed to process webhook'
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
status: 500
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
method: 'post' as const,
|
|
||||||
path: `${normalizedPrefix}/:path`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the webhook endpoint already exists to avoid duplicates
|
|
||||||
const existingEndpoint = config.endpoints?.find(endpoint =>
|
|
||||||
endpoint.path === webhookEndpoint.path && endpoint.method === webhookEndpoint.method
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!existingEndpoint) {
|
|
||||||
// Combine existing endpoints with the webhook endpoint
|
|
||||||
config.endpoints = [...(config.endpoints || []), webhookEndpoint]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import type { Payload } from 'payload'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize hooks for the workflows collection
|
|
||||||
* Currently minimal - can be extended for future workflow management features
|
|
||||||
*/
|
|
||||||
export function initWorkflowHooks(payload: Payload, logger: Payload['logger']): void {
|
|
||||||
// Future workflow hooks can be added here
|
|
||||||
// For example: workflow validation, cleanup, statistics, etc.
|
|
||||||
|
|
||||||
logger.debug('Workflow hooks initialized')
|
|
||||||
}
|
|
||||||
@@ -72,7 +72,7 @@ describe('WorkflowExecutor', () => {
|
|||||||
describe('resolveStepInput', () => {
|
describe('resolveStepInput', () => {
|
||||||
it('should resolve all JSONPath expressions in step config', () => {
|
it('should resolve all JSONPath expressions in step config', () => {
|
||||||
const config = {
|
const config = {
|
||||||
url: '$.trigger.webhook.url',
|
url: '$.trigger.data.url',
|
||||||
message: 'Static message',
|
message: 'Static message',
|
||||||
data: {
|
data: {
|
||||||
id: '$.trigger.doc.id',
|
id: '$.trigger.doc.id',
|
||||||
@@ -83,7 +83,7 @@ describe('WorkflowExecutor', () => {
|
|||||||
const context = {
|
const context = {
|
||||||
trigger: {
|
trigger: {
|
||||||
doc: { id: 'doc-123', title: 'Doc Title' },
|
doc: { id: 'doc-123', title: 'Doc Title' },
|
||||||
webhook: { url: 'https://example.com/webhook' }
|
data: { url: 'https://example.com/webhook' }
|
||||||
},
|
},
|
||||||
steps: {}
|
steps: {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type {TriggerConfig} from '../plugin/config-types.js'
|
import type {TriggerConfig} from '../plugin/config-types.js'
|
||||||
|
|
||||||
export const collectionHookTrigger: TriggerConfig = ({collectionTriggers}) => ({
|
export const collectionTrigger: TriggerConfig = ({collectionTriggers}) => ({
|
||||||
slug: 'collection-hook',
|
slug: 'collection-hook',
|
||||||
parameters: [
|
parameters: [
|
||||||
{
|
{
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type {TriggerConfig} from '../plugin/config-types.js'
|
import type {TriggerConfig} from '../plugin/config-types.js'
|
||||||
|
|
||||||
export const globalTrigger: TriggerConfig = () => ({
|
export const globalTrigger: TriggerConfig = ({globalTriggers}) => ({
|
||||||
slug: 'global',
|
slug: 'global-hook',
|
||||||
parameters: [
|
parameters: [
|
||||||
{
|
{
|
||||||
name: 'global',
|
name: 'global',
|
||||||
@@ -9,16 +9,20 @@ export const globalTrigger: TriggerConfig = () => ({
|
|||||||
admin: {
|
admin: {
|
||||||
description: 'Global that triggers the workflow',
|
description: 'Global that triggers the workflow',
|
||||||
},
|
},
|
||||||
options: [], // Will be populated dynamically based on available globals
|
options: Object.keys(globalTriggers || {}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'globalOperation',
|
name: 'operation',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
admin: {
|
admin: {
|
||||||
description: 'Global operation that triggers the workflow',
|
description: 'Global hook that triggers the workflow',
|
||||||
},
|
},
|
||||||
options: [
|
options: [
|
||||||
'update'
|
"afterChange",
|
||||||
|
"afterRead",
|
||||||
|
"beforeChange",
|
||||||
|
"beforeRead",
|
||||||
|
"beforeValidate"
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
export { collectionHookTrigger } from './collection-hook-trigger.js'
|
export { collectionTrigger } from './collection-trigger.js'
|
||||||
export { globalTrigger } from './global-trigger.js'
|
export { globalTrigger } from './global-trigger.js'
|
||||||
export { webhookTrigger } from './webhook-trigger.js'
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import type {TriggerConfig} from '../plugin/config-types.js'
|
|
||||||
|
|
||||||
export const webhookTrigger: TriggerConfig = () => ({
|
|
||||||
slug: 'webhook',
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
name: 'webhookPath',
|
|
||||||
type: 'text',
|
|
||||||
admin: {
|
|
||||||
description: 'URL path for the webhook (e.g., "my-webhook"). Full URL will be /api/workflows-webhook/my-webhook',
|
|
||||||
},
|
|
||||||
validate: (value: any, {siblingData}: any) => {
|
|
||||||
if (siblingData?.type === 'webhook' && !value && !siblingData?.parameters?.webhookPath) {
|
|
||||||
return 'Webhook path is required for webhook triggers'
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user