Remove initCollectionHooks and associated migration guides

- Delete `initCollectionHooks` implementation and its usage references
- Remove `MIGRATION-v0.0.24.md` and `NOT-IMPLEMENTING.md` as they are now obsolete
- Update related workflow executor logic and TypeScript definitions, ensuring compatibility
- Simplify error handling, input parsing, and logging within workflow execution
- Clean up and refactor redundant code to improve maintainability
This commit is contained in:
2025-09-10 17:36:56 +02:00
parent 435f9b0c69
commit 0f741acf73
19 changed files with 399 additions and 1077 deletions

View File

@@ -0,0 +1,60 @@
import {WorkflowExecutor} from "../core/workflow-executor.js"
export const createCollectionTriggerHook = (collectionSlug: string, hookType: string) => {
return async (args: HookArgs) => {
const req = 'req' in args ? args.req :
'args' in args ? args.args.req :
undefined
if (!req) {
throw new Error('No request object found in hook arguments')
}
const payload = req.payload
const {docs: workflows} = await payload.find({
collection: 'workflows',
depth: 2,
limit: 100,
where: {
'triggers.parameters.collectionSlug': {
equals: collectionSlug
},
'triggers.parameters.hook': {
equals: hookType
},
'triggers.type': {
equals: 'collection-hook'
}
}
})
const executor = new WorkflowExecutor(payload, payload.logger)
// invoke each workflow
for (const workflow of workflows) {
// Create execution context
const context = {
steps: {},
trigger: {
...args,
type: 'collection',
collection: collectionSlug,
}
}
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await executor.execute(workflow as any, context, req)
payload.logger.info({
workflowId: workflow.id,
collection: collectionSlug,
hookType
}, 'Workflow executed successfully')
} catch (error) {
payload.logger.error({
workflowId: workflow.id,
collection: collectionSlug,
hookType,
error: error instanceof Error ? error.message : 'Unknown error'
}, 'Workflow execution failed')
// Don't throw to prevent breaking the original operation
}
}
}
}

View File

@@ -1,24 +1,17 @@
import type {TaskConfig} from "payload"
import type {CollectionConfig, TaskConfig} from "payload"
import type {Trigger} from "../triggers/types.js"
export type CollectionTriggerConfigCrud = {
create?: true
delete?: true
read?: true
update?: true
}
export type TriggerConfig = (config: WorkflowsPluginConfig) => Trigger
export type CollectionTriggerConfig = CollectionTriggerConfigCrud | true
export type TriggerConfig = <T extends string>(config: WorkflowsPluginConfig<T>) => Trigger
export type WorkflowsPluginConfig<TSlug extends string> = {
collectionTriggers: {
[key in TSlug]?: CollectionTriggerConfig
export type WorkflowsPluginConfig<TSlug extends string = string> = {
collectionTriggers?: {
[key in TSlug]?: {
[key in keyof CollectionConfig['hooks']]?: true
} | true
}
enabled?: boolean
steps: TaskConfig<string>[],
steps: TaskConfig<string>[]
triggers?: TriggerConfig[]
webhookPrefix?: string
}

View File

@@ -1,9 +1,4 @@
import type {
CollectionAfterChangeHook,
Config,
PayloadRequest,
TypeWithID
} from 'payload'
import type {CollectionConfig, Config} from 'payload'
import type {WorkflowsPluginConfig} from "./config-types.js"
@@ -15,98 +10,10 @@ 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"
export {getLogger} from './logger.js'
/**
* Helper function to create failed workflow runs for tracking errors
*/
const createFailedWorkflowRun = async (
collectionSlug: string,
operation: string,
doc: TypeWithID,
previousDoc: TypeWithID,
req: PayloadRequest,
errorMessage: string
): Promise<void> => {
try {
const logger = req?.payload?.logger || console
// Only create failed workflow runs if we have a payload instance
if (!req?.payload || !collectionSlug) {
return
}
// Find workflows that should have been triggered
const workflows = await req.payload.find({
collection: 'workflows',
limit: 10,
req,
where: {
'triggers.parameters.collectionSlug': {
equals: collectionSlug
},
'triggers.parameters.operation': {
equals: operation
},
'triggers.type': {
equals: 'collection'
}
}
})
// Create failed workflow runs for each matching workflow
for (const workflow of workflows.docs) {
await req.payload.create({
collection: 'workflow-runs',
data: {
completedAt: new Date().toISOString(),
context: {
steps: {},
trigger: {
type: 'collection',
collection: collectionSlug,
doc,
operation,
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: req?.user?.email || 'system',
workflow: workflow.id,
workflowVersion: 1
},
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
const logger = req?.payload?.logger || console
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) {
@@ -119,70 +26,16 @@ const applyCollectionsConfig = <T extends string>(pluginOptions: WorkflowsPlugin
)
}
/**
* Create a collection hook that executes workflows
*/
const createAutomationHook = <T extends TypeWithID>(): CollectionAfterChangeHook<T> => {
return async function payloadAutomationHook(args) {
const logger = args.req?.payload?.logger || console
type AnyHook =
CollectionConfig['hooks'] extends infer H
? H extends Record<string, unknown>
? NonNullable<H[keyof H]> extends (infer U)[]
? U
: never
: never
: never;
try {
logger.info({
collection: args.collection?.slug,
docId: args.doc?.id,
hookType: 'automation',
operation: args.operation
}, 'Collection automation hook triggered')
// Create executor on-demand
const executor = new WorkflowExecutor(args.req.payload, logger)
logger.debug('Executing triggered workflows...')
await 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.collection.slug,
args.operation,
args.doc,
args.previousDoc,
args.req,
errorMessage
)
} 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
}
}
}
type HookArgs = Parameters<AnyHook>[0]
export const workflowsPlugin =
<TSlug extends string>(pluginOptions: WorkflowsPluginConfig<TSlug>) =>
@@ -199,13 +52,15 @@ export const workflowsPlugin =
const logger = getConfigLogger()
if (config.collections && pluginOptions.collectionTriggers) {
for (const [triggerSlug, triggerConfig] of Object.entries(pluginOptions.collectionTriggers)) {
if (!triggerConfig) {continue}
for (const [collectionSlug, triggerConfig] of Object.entries(pluginOptions.collectionTriggers)) {
if (!triggerConfig) {
continue
}
// Find the collection config that matches
const collectionIndex = config.collections.findIndex(c => c.slug === triggerSlug)
const collectionIndex = config.collections.findIndex(c => c.slug === collectionSlug)
if (collectionIndex === -1) {
logger.warn(`Collection '${triggerSlug}' not found in config.collections`)
logger.warn(`Collection '${collectionSlug}' not found in config.collections`)
continue
}
@@ -215,19 +70,47 @@ export const workflowsPlugin =
if (!collection.hooks) {
collection.hooks = {}
}
if (!collection.hooks.afterChange) {
collection.hooks.afterChange = []
}
// Add the hook to the collection config
const automationHook = createAutomationHook()
// Mark it for debugging
Object.defineProperty(automationHook, '__isAutomationHook', {
value: true,
enumerable: false
// Determine which hooks to register based on config
const hooksToRegister = triggerConfig === true
? {
afterChange: true,
afterDelete: true,
afterRead: true,
}
: triggerConfig
// Register each configured hook
Object.entries(hooksToRegister).forEach(([hookName, enabled]) => {
if (!enabled) {
return
}
const hookKey = hookName as keyof typeof collection.hooks
// Initialize the hook array if needed
if (!collection.hooks![hookKey]) {
collection.hooks![hookKey] = []
}
// Create the automation hook for this specific collection and hook type
const automationHook = createCollectionTriggerHook(collectionSlug, 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 collection
;(collection.hooks![hookKey] as Array<unknown>).push(automationHook)
logger.debug(`Registered ${hookKey} hook for collection '${collectionSlug}'`)
})
collection.hooks.afterChange.push(automationHook)
}
}

View File

@@ -1,138 +0,0 @@
import type {Payload} from "payload"
import type {Logger} from "pino"
import type { WorkflowExecutor } from "../core/workflow-executor.js"
import type {CollectionTriggerConfigCrud, WorkflowsPluginConfig} from "./config-types.js"
export function initCollectionHooks<T extends string>(pluginOptions: WorkflowsPluginConfig<T>, payload: Payload, logger: Payload['logger'], executor: WorkflowExecutor) {
if (!pluginOptions.collectionTriggers || Object.keys(pluginOptions.collectionTriggers).length === 0) {
logger.warn('No collection triggers configured in plugin options')
return
}
logger.info({
configuredCollections: Object.keys(pluginOptions.collectionTriggers),
availableCollections: Object.keys(payload.collections)
}, 'Starting collection hook registration')
// Add hooks to configured collections
for (const [collectionSlug, triggerConfig] of Object.entries(pluginOptions.collectionTriggers)) {
if (!triggerConfig) {
logger.debug({collectionSlug}, 'Skipping collection with falsy trigger config')
continue
}
const collection = payload.collections[collectionSlug as T]
const crud: CollectionTriggerConfigCrud = triggerConfig === true ? {
create: true,
delete: true,
read: true,
update: true,
} : triggerConfig
if (!collection.config.hooks) {
collection.config.hooks = {} as typeof collection.config.hooks
}
if (crud.update || crud.create) {
collection.config.hooks.afterChange = collection.config.hooks.afterChange || []
collection.config.hooks.afterChange.push(async (change) => {
const operation = change.operation as 'create' | 'update'
logger.info({
slug: change.collection.slug,
operation,
docId: change.doc?.id,
previousDocId: change.previousDoc?.id,
hasExecutor: !!executor,
executorType: typeof executor
}, 'Collection automation hook triggered')
try {
// Execute workflows for this trigger
await executor.executeTriggeredWorkflows(
change.collection.slug,
operation,
change.doc,
change.previousDoc,
change.req
)
logger.info({
slug: change.collection.slug,
operation,
docId: change.doc?.id
}, 'Workflow execution completed successfully')
} catch (error) {
logger.error({
slug: change.collection.slug,
operation,
docId: change.doc?.id,
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined
}, 'AUTOMATION PLUGIN: executeTriggeredWorkflows failed')
// Don't re-throw to avoid breaking other hooks
}
})
}
if (crud.read) {
collection.config.hooks.afterRead = collection.config.hooks.afterRead || []
collection.config.hooks.afterRead.push(async (change) => {
logger.debug({
slug: change.collection.slug,
operation: 'read',
}, 'Collection hook triggered')
// Execute workflows for this trigger
await executor.executeTriggeredWorkflows(
change.collection.slug,
'read',
change.doc,
undefined,
change.req
)
})
}
if (crud.delete) {
collection.config.hooks.afterDelete = collection.config.hooks.afterDelete || []
collection.config.hooks.afterDelete.push(async (change) => {
logger.debug({
slug: change.collection.slug,
operation: 'delete',
}, 'Collection hook triggered')
// Execute workflows for this trigger
await executor.executeTriggeredWorkflows(
change.collection.slug,
'delete',
change.doc,
undefined,
change.req
)
})
}
if (collection) {
logger.info({
collectionSlug,
hooksRegistered: {
afterChange: crud.update || crud.create,
afterRead: crud.read,
afterDelete: crud.delete
}
}, 'Collection hooks registered successfully')
} else {
logger.error({
collectionSlug,
availableCollections: Object.keys(payload.collections)
}, 'Collection not found for trigger configuration - check collection slug spelling')
}
}
}