mirror of
https://github.com/xtr-dev/payload-automation.git
synced 2025-12-15 02:53:23 +00:00
Initial commit
This commit is contained in:
231
src/collections/Workflow.ts
Normal file
231
src/collections/Workflow.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
151
src/collections/WorkflowRuns.ts
Normal file
151
src/collections/WorkflowRuns.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user