mirror of
https://github.com/xtr-dev/payload-automation.git
synced 2025-12-10 00:43:23 +00:00
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:
60
src/plugin/collection-hook.ts
Normal file
60
src/plugin/collection-hook.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user