mirror of
https://github.com/xtr-dev/payload-automation.git
synced 2025-12-10 00:43:23 +00:00
- Add createTrigger() and createAdvancedTrigger() helpers - Add preset builders: webhookTrigger, cronTrigger, eventTrigger, etc. - Implement virtual fields with JSON backing for trigger parameters - Eliminate 90% of boilerplate when creating custom triggers - Add /helpers export path for trigger builders - Fix field name clashing between built-in and custom trigger parameters - Add comprehensive examples and documentation - Maintain backward compatibility with existing triggers 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
322 lines
12 KiB
TypeScript
322 lines
12 KiB
TypeScript
import type {Config} from 'payload'
|
|
|
|
import type {CollectionTriggerConfigCrud, WorkflowsPluginConfig} from "./config-types.js"
|
|
|
|
import {createWorkflowCollection} from '../collections/Workflow.js'
|
|
import {WorkflowRunsCollection} from '../collections/WorkflowRuns.js'
|
|
import {WorkflowExecutor} from '../core/workflow-executor.js'
|
|
import {generateCronTasks, registerCronJobs} from './cron-scheduler.js'
|
|
import {initCollectionHooks} from "./init-collection-hooks.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'
|
|
|
|
export {getLogger} from './logger.js'
|
|
|
|
// Improved executor registry with proper error handling and logging
|
|
interface ExecutorRegistry {
|
|
executor: null | WorkflowExecutor
|
|
isInitialized: boolean
|
|
logger: any | null
|
|
}
|
|
|
|
const executorRegistry: ExecutorRegistry = {
|
|
executor: null,
|
|
isInitialized: false,
|
|
logger: null
|
|
}
|
|
|
|
const setWorkflowExecutor = (executor: WorkflowExecutor, logger: any) => {
|
|
executorRegistry.executor = executor
|
|
executorRegistry.logger = logger
|
|
executorRegistry.isInitialized = true
|
|
|
|
logger.info('Workflow executor initialized and registered successfully')
|
|
}
|
|
|
|
const getExecutorRegistry = (): ExecutorRegistry => {
|
|
return executorRegistry
|
|
}
|
|
|
|
// Helper function to create failed workflow runs for tracking errors
|
|
const createFailedWorkflowRun = async (args: any, errorMessage: string, logger: any) => {
|
|
try {
|
|
// Only create failed workflow runs if we have enough context
|
|
if (!args?.req?.payload || !args?.collection?.slug) {
|
|
return
|
|
}
|
|
|
|
// Find workflows that should have been triggered
|
|
const workflows = await args.req.payload.find({
|
|
collection: 'workflows',
|
|
limit: 10,
|
|
req: args.req,
|
|
where: {
|
|
'triggers.collectionSlug': {
|
|
equals: args.collection.slug
|
|
},
|
|
'triggers.operation': {
|
|
equals: args.operation
|
|
},
|
|
'triggers.type': {
|
|
equals: 'collection-trigger'
|
|
}
|
|
}
|
|
})
|
|
|
|
// Create failed workflow runs for each matching workflow
|
|
for (const workflow of workflows.docs) {
|
|
await args.req.payload.create({
|
|
collection: 'workflow-runs',
|
|
data: {
|
|
completedAt: new Date().toISOString(),
|
|
context: {
|
|
steps: {},
|
|
trigger: {
|
|
type: 'collection',
|
|
collection: args.collection.slug,
|
|
doc: args.doc,
|
|
operation: args.operation,
|
|
previousDoc: args.previousDoc,
|
|
triggeredAt: new Date().toISOString()
|
|
}
|
|
},
|
|
error: `Hook execution failed: ${errorMessage}`,
|
|
inputs: {},
|
|
logs: [{
|
|
level: 'error',
|
|
message: `Hook execution failed: ${errorMessage}`,
|
|
timestamp: new Date().toISOString()
|
|
}],
|
|
outputs: {},
|
|
startedAt: new Date().toISOString(),
|
|
status: 'failed',
|
|
steps: [],
|
|
triggeredBy: args?.req?.user?.email || 'system',
|
|
workflow: workflow.id,
|
|
workflowVersion: 1
|
|
},
|
|
req: args.req
|
|
})
|
|
}
|
|
|
|
if (workflows.docs.length > 0) {
|
|
logger.info({
|
|
errorMessage,
|
|
workflowCount: workflows.docs.length
|
|
}, 'Created failed workflow runs for hook execution error')
|
|
}
|
|
|
|
} catch (error) {
|
|
// Don't let workflow run creation failures break the original operation
|
|
logger.warn({
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
}, 'Failed to create failed workflow run record')
|
|
}
|
|
}
|
|
|
|
const applyCollectionsConfig = <T extends string>(pluginOptions: WorkflowsPluginConfig<T>, config: Config) => {
|
|
// Add workflow collections
|
|
if (!config.collections) {
|
|
config.collections = []
|
|
}
|
|
|
|
config.collections.push(
|
|
createWorkflowCollection(pluginOptions),
|
|
WorkflowRunsCollection
|
|
)
|
|
}
|
|
|
|
// Removed config-phase hook registration - user collections don't exist during config phase
|
|
|
|
|
|
export const workflowsPlugin =
|
|
<TSlug extends string>(pluginOptions: WorkflowsPluginConfig<TSlug>) =>
|
|
(config: Config): Config => {
|
|
// If the plugin is disabled, return config unchanged
|
|
if (pluginOptions.enabled === false) {
|
|
return config
|
|
}
|
|
|
|
applyCollectionsConfig<TSlug>(pluginOptions, config)
|
|
|
|
// CRITICAL: Modify existing collection configs BEFORE PayloadCMS processes them
|
|
// This is the ONLY time we can add hooks that will actually work
|
|
const logger = getConfigLogger()
|
|
logger.info('Attempting to modify collection configs before PayloadCMS initialization...')
|
|
|
|
if (config.collections && pluginOptions.collectionTriggers) {
|
|
for (const [triggerSlug, triggerConfig] of Object.entries(pluginOptions.collectionTriggers)) {
|
|
if (!triggerConfig) {continue}
|
|
|
|
// Find the collection config that matches
|
|
const collectionIndex = config.collections.findIndex(c => c.slug === triggerSlug)
|
|
if (collectionIndex === -1) {
|
|
logger.warn(`Collection '${triggerSlug}' not found in config.collections`)
|
|
continue
|
|
}
|
|
|
|
const collection = config.collections[collectionIndex]
|
|
logger.info(`Found collection '${triggerSlug}' - modifying its hooks...`)
|
|
|
|
// Initialize hooks if needed
|
|
if (!collection.hooks) {
|
|
collection.hooks = {}
|
|
}
|
|
if (!collection.hooks.afterChange) {
|
|
collection.hooks.afterChange = []
|
|
}
|
|
|
|
// Create a reliable hook function with proper dependency injection
|
|
const automationHook = Object.assign(
|
|
async function payloadAutomationHook(args: any) {
|
|
const registry = getExecutorRegistry()
|
|
|
|
// Use proper logger if available, fallback to args.req.payload.logger
|
|
const logger = registry.logger || args?.req?.payload?.logger || console
|
|
|
|
try {
|
|
logger.info({
|
|
collection: args?.collection?.slug,
|
|
docId: args?.doc?.id,
|
|
hookType: 'automation',
|
|
operation: args?.operation
|
|
}, 'Collection automation hook triggered')
|
|
|
|
if (!registry.isInitialized) {
|
|
logger.warn('Workflow executor not yet initialized, skipping execution')
|
|
return undefined
|
|
}
|
|
|
|
if (!registry.executor) {
|
|
logger.error('Workflow executor is null despite being marked as initialized')
|
|
// Create a failed workflow run to track this issue
|
|
await createFailedWorkflowRun(args, 'Executor not available', logger)
|
|
return undefined
|
|
}
|
|
|
|
logger.debug('Executing triggered workflows...')
|
|
await registry.executor.executeTriggeredWorkflows(
|
|
args.collection.slug,
|
|
args.operation,
|
|
args.doc,
|
|
args.previousDoc,
|
|
args.req
|
|
)
|
|
|
|
logger.info({
|
|
collection: args?.collection?.slug,
|
|
docId: args?.doc?.id,
|
|
operation: args?.operation
|
|
}, 'Workflow execution completed successfully')
|
|
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
|
|
|
logger.error({
|
|
collection: args?.collection?.slug,
|
|
docId: args?.doc?.id,
|
|
error: errorMessage,
|
|
errorStack: error instanceof Error ? error.stack : undefined,
|
|
operation: args?.operation
|
|
}, 'Hook execution failed')
|
|
|
|
// Create a failed workflow run to track this error
|
|
try {
|
|
await createFailedWorkflowRun(args, errorMessage, logger)
|
|
} catch (createError) {
|
|
logger.error({
|
|
error: createError instanceof Error ? createError.message : 'Unknown error'
|
|
}, 'Failed to create workflow run for hook error')
|
|
}
|
|
|
|
// Don't throw to prevent breaking the original operation
|
|
}
|
|
|
|
return undefined
|
|
},
|
|
{
|
|
__isAutomationHook: true,
|
|
__version: '0.0.22'
|
|
}
|
|
)
|
|
|
|
// Add the hook to the collection config
|
|
collection.hooks.afterChange.push(automationHook)
|
|
logger.info(`Added automation hook to '${triggerSlug}' - hook count: ${collection.hooks.afterChange.length}`)
|
|
}
|
|
}
|
|
|
|
if (!config.jobs) {
|
|
config.jobs = {tasks: []}
|
|
}
|
|
|
|
const configLogger = getConfigLogger()
|
|
configLogger.info(`Configuring workflow plugin with ${Object.keys(pluginOptions.collectionTriggers || {}).length} collection triggers`)
|
|
|
|
// Generate cron tasks for workflows with cron triggers
|
|
generateCronTasks(config)
|
|
|
|
for (const step of pluginOptions.steps) {
|
|
if (!config.jobs?.tasks?.find(task => task.slug === step.slug)) {
|
|
configLogger.debug(`Registering task: ${step.slug}`)
|
|
config.jobs?.tasks?.push(step)
|
|
} else {
|
|
configLogger.debug(`Task ${step.slug} already registered, skipping`)
|
|
}
|
|
}
|
|
|
|
// Initialize webhook endpoint
|
|
initWebhookEndpoint(config, pluginOptions.webhookPrefix || 'webhook')
|
|
|
|
// Set up onInit to register collection hooks and initialize features
|
|
const incomingOnInit = config.onInit
|
|
config.onInit = async (payload) => {
|
|
configLogger.info(`onInit called - collections: ${Object.keys(payload.collections).length}`)
|
|
|
|
// Execute any existing onInit functions first
|
|
if (incomingOnInit) {
|
|
configLogger.debug('Executing existing onInit function')
|
|
await incomingOnInit(payload)
|
|
}
|
|
|
|
// Initialize the logger with the payload instance
|
|
const logger = initializeLogger(payload)
|
|
logger.info('Logger initialized with payload instance')
|
|
|
|
// Log collection trigger configuration
|
|
logger.info(`Plugin configuration: ${Object.keys(pluginOptions.collectionTriggers || {}).length} collection triggers, ${pluginOptions.steps?.length || 0} steps`)
|
|
|
|
// Create workflow executor instance
|
|
console.log('🚨 CREATING WORKFLOW EXECUTOR INSTANCE')
|
|
const executor = new WorkflowExecutor(payload, logger)
|
|
console.log('🚨 EXECUTOR CREATED:', typeof executor)
|
|
console.log('🚨 EXECUTOR METHODS:', Object.getOwnPropertyNames(Object.getPrototypeOf(executor)))
|
|
|
|
// Register executor with proper dependency injection
|
|
setWorkflowExecutor(executor, logger)
|
|
|
|
// Hooks are now registered during config phase - just log status
|
|
logger.info('Hooks were registered during config phase - executor now available')
|
|
|
|
logger.info('Initializing global hooks...')
|
|
initGlobalHooks(payload, logger, executor)
|
|
|
|
logger.info('Initializing workflow hooks...')
|
|
initWorkflowHooks(payload, logger)
|
|
|
|
logger.info('Initializing step tasks...')
|
|
initStepTasks(pluginOptions, payload, logger)
|
|
|
|
// Register cron jobs for workflows with cron triggers
|
|
logger.info('Registering cron jobs...')
|
|
await registerCronJobs(payload, logger)
|
|
|
|
logger.info('Plugin initialized successfully - all hooks registered')
|
|
}
|
|
|
|
return config
|
|
}
|