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:
2025-09-10 18:08:25 +02:00
parent 0f741acf73
commit acdfa411e4
22 changed files with 228 additions and 1217 deletions

View File

@@ -1,7 +1,7 @@
import {WorkflowExecutor} from "../core/workflow-executor.js"
export const createCollectionTriggerHook = (collectionSlug: string, hookType: string) => {
return async (args: HookArgs) => {
return async (args: any) => {
const req = 'req' in args ? args.req :
'args' in args ? args.args.req :
undefined

View File

@@ -1,17 +1,21 @@
import type {CollectionConfig, TaskConfig} from "payload"
import type {CollectionConfig, GlobalConfig, TaskConfig} from "payload"
import type {Trigger} from "../triggers/types.js"
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?: {
[key in TSlug]?: {
[key in keyof CollectionConfig['hooks']]?: true
} | true
}
globalTriggers?: {
[key in TGlobal]?: {
[key in keyof GlobalConfig['hooks']]?: true
} | true
}
enabled?: boolean
steps: TaskConfig<string>[]
triggers?: TriggerConfig[]
webhookPrefix?: string
}

95
src/plugin/global-hook.ts Normal file
View 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
}
}
}

View File

@@ -5,12 +5,9 @@ import type {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 {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 {createCollectionTriggerHook} from "./collection-hook.js"
import {createGlobalTriggerHook} from "./global-hook.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) {
config.jobs = {tasks: []}
}
@@ -124,8 +184,6 @@ export const workflowsPlugin =
}
}
// Initialize webhook endpoint
initWebhookEndpoint(config, pluginOptions.webhookPrefix || 'webhook')
// Set up onInit to initialize features
const incomingOnInit = config.onInit
@@ -139,19 +197,8 @@ export const workflowsPlugin =
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`)
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)
// Log trigger configuration
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('Plugin initialized successfully - all hooks registered')
}

View File

@@ -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')
}
}

View File

@@ -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')
}
})
}

View File

@@ -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]
}
}

View File

@@ -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')
}