Initial commit

This commit is contained in:
2025-08-22 21:09:48 +02:00
commit 2d84f535f4
68 changed files with 34807 additions and 0 deletions

231
src/collections/Workflow.ts Normal file
View File

@@ -0,0 +1,231 @@
import type {CollectionConfig, Field} from 'payload'
import type {WorkflowsPluginConfig} from "../plugin/config-types.js"
export const createWorkflowCollection: <T extends string>(options: WorkflowsPluginConfig<T>) => CollectionConfig = ({
collectionTriggers,
steps,
triggers
}) => ({
slug: 'workflows',
access: {
create: () => true,
delete: () => true,
read: () => true,
update: () => true,
},
admin: {
defaultColumns: ['name', 'updatedAt'],
description: 'Create and manage automated workflows.',
group: 'Automation',
useAsTitle: 'name',
},
fields: [
{
name: 'name',
type: 'text',
admin: {
description: 'Human-readable name for the workflow',
},
required: true,
},
{
name: 'description',
type: 'textarea',
admin: {
description: 'Optional description of what this workflow does',
},
},
{
name: 'triggers',
type: 'array',
fields: [
{
name: 'type',
type: 'select',
options: [
'collection-trigger',
'webhook-trigger',
'global-trigger',
'cron-trigger',
...(triggers || []).map(t => t.slug)
]
},
{
name: 'collection',
type: 'select',
admin: {
condition: (_, siblingData) => siblingData?.type === 'collection-trigger',
description: 'Collection that triggers the workflow',
},
options: Object.keys(collectionTriggers || {})
},
{
name: 'operation',
type: 'select',
admin: {
condition: (_, siblingData) => siblingData?.type === 'collection-trigger',
description: 'Collection operation that triggers the workflow',
},
options: [
'create',
'delete',
'read',
'update',
]
},
{
name: 'webhookPath',
type: 'text',
admin: {
condition: (_, siblingData) => siblingData?.type === 'webhook-trigger',
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-trigger' && !value) {
return 'Webhook path is required for webhook triggers'
}
return true
}
},
{
name: 'global',
type: 'select',
admin: {
condition: (_, siblingData) => siblingData?.type === 'global-trigger',
description: 'Global that triggers the workflow',
},
options: [] // Will be populated dynamically based on available globals
},
{
name: 'globalOperation',
type: 'select',
admin: {
condition: (_, siblingData) => siblingData?.type === 'global-trigger',
description: 'Global operation that triggers the workflow',
},
options: [
'update'
]
},
{
name: 'cronExpression',
type: 'text',
admin: {
condition: (_, siblingData) => siblingData?.type === 'cron-trigger',
description: 'Cron expression for scheduled execution (e.g., "0 0 * * *" for daily at midnight)',
placeholder: '0 0 * * *'
},
validate: (value: any, {siblingData}: any) => {
if (siblingData?.type === 'cron-trigger' && !value) {
return 'Cron expression is required for cron triggers'
}
// Validate cron expression format if provided
if (siblingData?.type === 'cron-trigger' && value) {
// Basic format validation - should be 5 parts separated by spaces
const cronParts = value.trim().split(/\s+/)
if (cronParts.length !== 5) {
return 'Invalid cron expression format. Expected 5 parts: "minute hour day month weekday" (e.g., "0 9 * * 1")'
}
// Additional validation could use node-cron but we avoid dynamic imports here
// The main validation happens at runtime in the cron scheduler
}
return true
}
},
{
name: 'timezone',
type: 'text',
admin: {
condition: (_, siblingData) => siblingData?.type === 'cron-trigger',
description: 'Timezone for cron execution (e.g., "America/New_York", "Europe/London"). Defaults to UTC.',
placeholder: 'UTC'
},
defaultValue: 'UTC',
validate: (value: any, {siblingData}: any) => {
if (siblingData?.type === 'cron-trigger' && value) {
try {
// Test if timezone is valid by trying to create a date with it
new Intl.DateTimeFormat('en', {timeZone: value})
return true
} catch {
return `Invalid timezone: ${value}. Please use a valid IANA timezone identifier (e.g., "America/New_York", "Europe/London")`
}
}
return true
}
},
{
name: 'condition',
type: 'text',
admin: {
description: 'JSONPath expression that must evaluate to true for this trigger to execute the workflow (e.g., "$.doc.status == \'published\'")'
},
required: false
},
...(triggers || []).flatMap(t => (t.inputs || []).map(f => ({
...f,
admin: {
...(f.admin || {}),
condition: (...args) => args[1]?.type === t.slug && (
f.admin?.condition ?
f.admin.condition.call(this, ...args) :
true
),
},
} as Field)))
]
},
{
name: 'steps',
type: 'array',
fields: [
{
type: 'row',
fields: [
{
name: 'step',
type: 'select',
options: steps.map(t => t.slug)
},
{
name: 'name',
type: 'text',
}
]
},
{
name: 'input',
type: 'json',
required: false
},
{
name: 'dependencies',
type: 'text',
admin: {
description: 'Step names that must complete before this step can run'
},
hasMany: true,
required: false
},
{
name: 'condition',
type: 'text',
admin: {
description: 'JSONPath expression that must evaluate to true for this step to execute (e.g., "$.trigger.doc.status == \'published\'")'
},
required: false
},
],
}
],
versions: {
drafts: {
autosave: false,
},
maxPerDoc: 10,
},
})

View File

@@ -0,0 +1,151 @@
import type { CollectionConfig } from 'payload'
export const WorkflowRunsCollection: CollectionConfig = {
slug: 'workflow-runs',
access: {
create: () => true,
delete: () => true,
read: () => true,
update: () => true,
},
admin: {
defaultColumns: ['workflow', 'status', 'triggeredBy', 'startedAt', 'duration'],
group: 'Automation',
pagination: {
defaultLimit: 50,
},
useAsTitle: 'id',
},
fields: [
{
name: 'workflow',
type: 'relationship',
admin: {
description: 'Reference to the workflow that was executed',
},
relationTo: 'workflows',
required: true,
},
{
name: 'workflowVersion',
type: 'number',
admin: {
description: 'Version of the workflow that was executed',
},
required: true,
},
{
name: 'status',
type: 'select',
admin: {
description: 'Current execution status',
},
defaultValue: 'pending',
options: [
{
label: 'Pending',
value: 'pending',
},
{
label: 'Running',
value: 'running',
},
{
label: 'Completed',
value: 'completed',
},
{
label: 'Failed',
value: 'failed',
},
{
label: 'Cancelled',
value: 'cancelled',
},
],
required: true,
},
{
name: 'startedAt',
type: 'date',
admin: {
date: {
displayFormat: 'yyyy-MM-dd HH:mm:ss',
},
description: 'When execution began',
},
required: true,
},
{
name: 'completedAt',
type: 'date',
admin: {
date: {
displayFormat: 'yyyy-MM-dd HH:mm:ss',
},
description: 'When execution finished',
},
},
{
name: 'duration',
type: 'number',
admin: {
description: 'Total execution time in milliseconds',
readOnly: true,
},
},
{
name: 'context',
type: 'json'
},
{
name: 'inputs',
type: 'json',
admin: {
description: 'Input data provided when the workflow was triggered',
},
defaultValue: {},
required: true,
},
{
name: 'outputs',
type: 'json',
admin: {
description: 'Final output data from completed steps',
},
},
{
name: 'triggeredBy',
type: 'text',
admin: {
description: 'User, system, or trigger type that initiated execution',
},
required: true,
},
{
name: 'steps',
type: 'json',
admin: {
description: 'Array of step execution results',
},
defaultValue: [],
required: true,
},
{
name: 'error',
type: 'textarea',
admin: {
description: 'Error message if workflow execution failed',
},
},
{
name: 'logs',
type: 'json',
admin: {
description: 'Detailed execution logs',
},
defaultValue: [],
required: true,
},
],
}