@xtr-dev/payload-automation 0.0.42 → 0.0.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -49
- package/dist/collections/Steps.d.ts +6 -0
- package/dist/collections/Steps.js +166 -0
- package/dist/collections/Steps.js.map +1 -0
- package/dist/collections/Triggers.d.ts +7 -0
- package/dist/collections/Triggers.js +224 -0
- package/dist/collections/Triggers.js.map +1 -0
- package/dist/collections/Workflow.d.ts +5 -2
- package/dist/collections/Workflow.js +179 -39
- package/dist/collections/Workflow.js.map +1 -1
- package/dist/collections/WorkflowRuns.d.ts +4 -0
- package/dist/collections/WorkflowRuns.js +219 -24
- package/dist/collections/WorkflowRuns.js.map +1 -1
- package/dist/components/WorkflowBuilder/WorkflowBuilder.js.map +1 -1
- package/dist/core/expression-engine.d.ts +58 -0
- package/dist/core/expression-engine.js +191 -0
- package/dist/core/expression-engine.js.map +1 -0
- package/dist/core/workflow-executor.d.ts +70 -56
- package/dist/core/workflow-executor.js +354 -677
- package/dist/core/workflow-executor.js.map +1 -1
- package/dist/exports/client.js +1 -3
- package/dist/exports/client.js.map +1 -1
- package/dist/exports/views.js +2 -4
- package/dist/exports/views.js.map +1 -1
- package/dist/fields/parameter.js +8 -3
- package/dist/fields/parameter.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin/config-types.d.ts +43 -5
- package/dist/plugin/config-types.js +3 -1
- package/dist/plugin/config-types.js.map +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.js +82 -28
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/trigger-hook.d.ts +13 -0
- package/dist/plugin/trigger-hook.js +184 -0
- package/dist/plugin/trigger-hook.js.map +1 -0
- package/dist/steps/create-step.d.ts +66 -0
- package/dist/steps/create-step.js +59 -0
- package/dist/steps/create-step.js.map +1 -0
- package/dist/steps/index.d.ts +2 -0
- package/dist/steps/index.js +3 -0
- package/dist/steps/index.js.map +1 -1
- package/dist/steps/read-document-handler.js +1 -1
- package/dist/steps/read-document-handler.js.map +1 -1
- package/dist/steps/update-document-handler.js +1 -1
- package/dist/steps/update-document-handler.js.map +1 -1
- package/dist/triggers/hook-options.d.ts +34 -0
- package/dist/triggers/hook-options.js +158 -0
- package/dist/triggers/hook-options.js.map +1 -0
- package/dist/triggers/index.d.ts +2 -2
- package/dist/triggers/index.js +1 -2
- package/dist/triggers/index.js.map +1 -1
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.js +4 -5
- package/dist/types/index.js.map +1 -1
- package/dist/utils/validation.d.ts +64 -0
- package/dist/utils/validation.js +107 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +2 -1
- package/dist/plugin/collection-hook.d.ts +0 -1
- package/dist/plugin/collection-hook.js +0 -92
- package/dist/plugin/collection-hook.js.map +0 -1
- package/dist/plugin/global-hook.d.ts +0 -1
- package/dist/plugin/global-hook.js +0 -83
- package/dist/plugin/global-hook.js.map +0 -1
- package/dist/triggers/collection-trigger.d.ts +0 -2
- package/dist/triggers/collection-trigger.js +0 -36
- package/dist/triggers/collection-trigger.js.map +0 -1
- package/dist/triggers/global-trigger.d.ts +0 -2
- package/dist/triggers/global-trigger.js +0 -29
- package/dist/triggers/global-trigger.js.map +0 -1
- package/dist/triggers/types.d.ts +0 -5
- package/dist/triggers/types.js +0 -3
- package/dist/triggers/types.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { evaluateCondition as evalCondition, resolveStepInput as resolveInput } from './expression-engine.js';
|
|
2
2
|
export class WorkflowExecutor {
|
|
3
3
|
payload;
|
|
4
4
|
logger;
|
|
@@ -7,507 +7,294 @@ export class WorkflowExecutor {
|
|
|
7
7
|
this.logger = logger;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
* Resolve workflow steps by loading base step configurations and merging with overrides
|
|
11
|
+
*/ async resolveWorkflowSteps(workflow) {
|
|
12
|
+
const resolvedSteps = [];
|
|
13
|
+
if (!workflow.steps || workflow.steps.length === 0) {
|
|
14
|
+
return resolvedSteps;
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Convert numeric fields
|
|
36
|
-
if (numericFields.some((field)=>key.toLowerCase().includes(field))) {
|
|
37
|
-
const numValue = Number(value);
|
|
38
|
-
if (!isNaN(numValue)) {
|
|
39
|
-
this.logger.debug({
|
|
40
|
-
key,
|
|
41
|
-
originalValue: value,
|
|
42
|
-
convertedValue: numValue
|
|
43
|
-
}, 'Auto-converted field to number');
|
|
44
|
-
return numValue;
|
|
16
|
+
for(let i = 0; i < workflow.steps.length; i++){
|
|
17
|
+
const workflowStep = workflow.steps[i];
|
|
18
|
+
let baseStep;
|
|
19
|
+
if (typeof workflowStep.step === 'object' && workflowStep.step !== null) {
|
|
20
|
+
baseStep = workflowStep.step;
|
|
21
|
+
} else {
|
|
22
|
+
try {
|
|
23
|
+
baseStep = await this.payload.findByID({
|
|
24
|
+
collection: 'automation-steps',
|
|
25
|
+
id: workflowStep.step,
|
|
26
|
+
depth: 0
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
this.logger.error({
|
|
30
|
+
stepId: workflowStep.step,
|
|
31
|
+
stepIndex: i,
|
|
32
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
33
|
+
}, 'Failed to load step configuration');
|
|
34
|
+
throw new Error(`Failed to load step ${workflowStep.step}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
35
|
+
}
|
|
45
36
|
}
|
|
37
|
+
const baseConfig = baseStep.config || {};
|
|
38
|
+
const overrides = workflowStep.inputOverrides || {};
|
|
39
|
+
const mergedConfig = {
|
|
40
|
+
...baseConfig,
|
|
41
|
+
...overrides
|
|
42
|
+
};
|
|
43
|
+
const dependencies = (workflowStep.dependencies || []).map((d)=>d.stepIndex);
|
|
44
|
+
resolvedSteps.push({
|
|
45
|
+
stepIndex: i,
|
|
46
|
+
stepId: baseStep.id,
|
|
47
|
+
stepName: workflowStep.stepName || baseStep.name || `step-${i}`,
|
|
48
|
+
stepType: baseStep.type,
|
|
49
|
+
config: mergedConfig,
|
|
50
|
+
condition: workflowStep.condition,
|
|
51
|
+
dependencies,
|
|
52
|
+
retryOnFailure: baseStep.retryOnFailure,
|
|
53
|
+
maxRetries: baseStep.maxRetries || workflow.maxRetries || 3,
|
|
54
|
+
retryDelay: baseStep.retryDelay || workflow.retryDelay || 1000
|
|
55
|
+
});
|
|
46
56
|
}
|
|
47
|
-
|
|
48
|
-
if (booleanFields.some((field)=>key.toLowerCase().includes(field))) {
|
|
49
|
-
if (value === 'true') return true;
|
|
50
|
-
if (value === 'false') return false;
|
|
51
|
-
}
|
|
52
|
-
// Try to parse as number if it looks numeric
|
|
53
|
-
if (/^\d+$/.test(value)) {
|
|
54
|
-
const numValue = parseInt(value, 10);
|
|
55
|
-
this.logger.debug({
|
|
56
|
-
key,
|
|
57
|
-
originalValue: value,
|
|
58
|
-
convertedValue: numValue
|
|
59
|
-
}, 'Auto-converted numeric string to number');
|
|
60
|
-
return numValue;
|
|
61
|
-
}
|
|
62
|
-
// Try to parse as float if it looks like a decimal
|
|
63
|
-
if (/^\d+\.\d+$/.test(value)) {
|
|
64
|
-
const floatValue = parseFloat(value);
|
|
65
|
-
this.logger.debug({
|
|
66
|
-
key,
|
|
67
|
-
originalValue: value,
|
|
68
|
-
convertedValue: floatValue
|
|
69
|
-
}, 'Auto-converted decimal string to number');
|
|
70
|
-
return floatValue;
|
|
71
|
-
}
|
|
72
|
-
// Return as string if no conversion applies
|
|
73
|
-
return value;
|
|
57
|
+
return resolvedSteps;
|
|
74
58
|
}
|
|
75
59
|
/**
|
|
76
|
-
*
|
|
77
|
-
*/
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
60
|
+
* Resolve step execution order based on dependencies
|
|
61
|
+
*/ resolveExecutionOrder(steps) {
|
|
62
|
+
const indegree = new Map();
|
|
63
|
+
const dependents = new Map();
|
|
64
|
+
for (const step of steps){
|
|
65
|
+
indegree.set(step.stepIndex, step.dependencies.length);
|
|
66
|
+
dependents.set(step.stepIndex, []);
|
|
83
67
|
}
|
|
84
|
-
|
|
85
|
-
|
|
68
|
+
for (const step of steps){
|
|
69
|
+
for (const depIndex of step.dependencies){
|
|
70
|
+
const deps = dependents.get(depIndex) || [];
|
|
71
|
+
deps.push(step.stepIndex);
|
|
72
|
+
dependents.set(depIndex, deps);
|
|
73
|
+
}
|
|
86
74
|
}
|
|
87
|
-
|
|
88
|
-
|
|
75
|
+
const executionBatches = [];
|
|
76
|
+
const processed = new Set();
|
|
77
|
+
while(processed.size < steps.length){
|
|
78
|
+
const currentBatch = [];
|
|
79
|
+
for (const step of steps){
|
|
80
|
+
if (!processed.has(step.stepIndex) && indegree.get(step.stepIndex) === 0) {
|
|
81
|
+
currentBatch.push(step);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (currentBatch.length === 0) {
|
|
85
|
+
throw new Error('Circular dependency detected in workflow steps');
|
|
86
|
+
}
|
|
87
|
+
executionBatches.push(currentBatch);
|
|
88
|
+
for (const step of currentBatch){
|
|
89
|
+
processed.add(step.stepIndex);
|
|
90
|
+
for (const depIndex of dependents.get(step.stepIndex) || []){
|
|
91
|
+
indegree.set(depIndex, (indegree.get(depIndex) || 1) - 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
89
94
|
}
|
|
90
|
-
return
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Evaluate a step condition using JSONPath
|
|
94
|
-
*/ evaluateStepCondition(condition, context) {
|
|
95
|
-
return this.evaluateCondition(condition, context);
|
|
95
|
+
return executionBatches;
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
98
|
* Execute a single workflow step
|
|
99
|
-
*/ async executeStep(step,
|
|
100
|
-
const
|
|
99
|
+
*/ async executeStep(step, context, req, stepResults, jobMeta) {
|
|
100
|
+
const result = {
|
|
101
|
+
step: step.stepId,
|
|
102
|
+
stepName: step.stepName,
|
|
103
|
+
stepIndex: step.stepIndex,
|
|
104
|
+
status: 'running',
|
|
105
|
+
startedAt: new Date().toISOString(),
|
|
106
|
+
retryCount: 0
|
|
107
|
+
};
|
|
101
108
|
this.logger.info({
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
stepName: step.stepName,
|
|
110
|
+
stepType: step.stepType,
|
|
111
|
+
stepIndex: step.stepIndex
|
|
105
112
|
}, 'Executing step');
|
|
106
113
|
// Check step condition if present
|
|
107
114
|
if (step.condition) {
|
|
108
|
-
this.
|
|
109
|
-
condition: step.condition,
|
|
110
|
-
stepName,
|
|
111
|
-
availableSteps: Object.keys(context.steps),
|
|
112
|
-
completedSteps: Object.entries(context.steps).filter(([_, s])=>s.state === 'succeeded').map(([name])=>name),
|
|
113
|
-
triggerType: context.trigger?.type
|
|
114
|
-
}, 'Evaluating step condition');
|
|
115
|
-
const conditionMet = this.evaluateStepCondition(step.condition, context);
|
|
115
|
+
const conditionMet = await this.evaluateCondition(step.condition, context);
|
|
116
116
|
if (!conditionMet) {
|
|
117
117
|
this.logger.info({
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
contextSnapshot: JSON.stringify({
|
|
121
|
-
stepOutputs: Object.entries(context.steps).reduce((acc, [name, step])=>{
|
|
122
|
-
acc[name] = {
|
|
123
|
-
state: step.state,
|
|
124
|
-
hasOutput: !!step.output
|
|
125
|
-
};
|
|
126
|
-
return acc;
|
|
127
|
-
}, {}),
|
|
128
|
-
triggerData: context.trigger?.data ? 'present' : 'absent'
|
|
129
|
-
})
|
|
118
|
+
stepName: step.stepName,
|
|
119
|
+
condition: step.condition
|
|
130
120
|
}, 'Step condition not met, skipping');
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
reason: 'Condition not met',
|
|
137
|
-
skipped: true
|
|
138
|
-
},
|
|
139
|
-
state: 'succeeded'
|
|
121
|
+
result.status = 'skipped';
|
|
122
|
+
result.completedAt = new Date().toISOString();
|
|
123
|
+
result.output = {
|
|
124
|
+
reason: 'Condition not met',
|
|
125
|
+
skipped: true
|
|
140
126
|
};
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
return;
|
|
127
|
+
context.steps[step.stepName] = {
|
|
128
|
+
state: 'skipped',
|
|
129
|
+
output: result.output
|
|
130
|
+
};
|
|
131
|
+
return result;
|
|
146
132
|
}
|
|
147
|
-
this.logger.info({
|
|
148
|
-
condition: step.condition,
|
|
149
|
-
stepName,
|
|
150
|
-
contextSnapshot: JSON.stringify({
|
|
151
|
-
stepOutputs: Object.entries(context.steps).reduce((acc, [name, step])=>{
|
|
152
|
-
acc[name] = {
|
|
153
|
-
state: step.state,
|
|
154
|
-
hasOutput: !!step.output
|
|
155
|
-
};
|
|
156
|
-
return acc;
|
|
157
|
-
}, {}),
|
|
158
|
-
triggerData: context.trigger?.data ? 'present' : 'absent'
|
|
159
|
-
})
|
|
160
|
-
}, 'Step condition met, proceeding with execution');
|
|
161
133
|
}
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
output: undefined,
|
|
134
|
+
// Resolve input using JSONata expressions
|
|
135
|
+
const resolvedInput = await this.resolveStepInput(step.config, context);
|
|
136
|
+
result.input = resolvedInput;
|
|
137
|
+
context.steps[step.stepName] = {
|
|
167
138
|
state: 'running',
|
|
168
|
-
|
|
139
|
+
input: resolvedInput
|
|
169
140
|
};
|
|
170
|
-
// Move taskSlug declaration outside try block so it's accessible in catch
|
|
171
|
-
const taskSlug = step.type;
|
|
172
141
|
try {
|
|
173
|
-
// Get input configuration from the step
|
|
174
|
-
const inputConfig = step.input || {};
|
|
175
|
-
// Resolve input data using Handlebars templates
|
|
176
|
-
const resolvedInput = this.resolveStepInput(inputConfig, context, taskSlug);
|
|
177
|
-
context.steps[stepName].input = resolvedInput;
|
|
178
|
-
if (!taskSlug) {
|
|
179
|
-
throw new Error(`Step ${stepName} is missing a task type`);
|
|
180
|
-
}
|
|
181
|
-
this.logger.info({
|
|
182
|
-
hasInput: !!resolvedInput,
|
|
183
|
-
hasReq: !!req,
|
|
184
|
-
stepName,
|
|
185
|
-
taskSlug
|
|
186
|
-
}, 'Queueing task');
|
|
187
142
|
const job = await this.payload.jobs.queue({
|
|
188
143
|
input: resolvedInput,
|
|
189
144
|
req,
|
|
190
|
-
task:
|
|
145
|
+
task: step.stepType
|
|
191
146
|
});
|
|
192
|
-
//
|
|
193
|
-
this
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const runResults = await this.payload.jobs.runByID({
|
|
147
|
+
// Update the job with automation context fields
|
|
148
|
+
// This allows tracking which workflow run triggered this job
|
|
149
|
+
await this.payload.update({
|
|
150
|
+
collection: 'payload-jobs',
|
|
197
151
|
id: job.id,
|
|
152
|
+
data: {
|
|
153
|
+
automationWorkflow: jobMeta.automationWorkflowId,
|
|
154
|
+
automationWorkflowRun: jobMeta.automationWorkflowRunId,
|
|
155
|
+
automationTrigger: jobMeta.automationTriggerId,
|
|
156
|
+
automationStepName: step.stepName
|
|
157
|
+
},
|
|
198
158
|
req
|
|
199
159
|
});
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
hasResult: !!runResults
|
|
204
|
-
}, 'Job run completed');
|
|
205
|
-
// Give a small delay to ensure job is fully processed
|
|
206
|
-
await new Promise((resolve)=>setTimeout(resolve, 100));
|
|
207
|
-
// Get the job result
|
|
208
|
-
const completedJob = await this.payload.findByID({
|
|
160
|
+
// Run the job and capture the result directly from runByID
|
|
161
|
+
// This is important because PayloadCMS may delete jobs on completion (deleteJobOnComplete: true by default)
|
|
162
|
+
const runResult = await this.payload.jobs.runByID({
|
|
209
163
|
id: job.id,
|
|
210
|
-
collection: 'payload-jobs',
|
|
211
164
|
req
|
|
212
165
|
});
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (!errorMessage && taskStatus?.output?.error) {
|
|
236
|
-
errorMessage = taskStatus.output.error;
|
|
166
|
+
// Check the job status from the run result
|
|
167
|
+
// runByID returns { jobStatus: { [jobId]: { status: 'success' | 'error' | ... } }, ... }
|
|
168
|
+
const jobStatus = runResult?.jobStatus?.[job.id];
|
|
169
|
+
const jobSucceeded = jobStatus?.status === 'success';
|
|
170
|
+
if (jobSucceeded) {
|
|
171
|
+
// Job completed successfully - try to get output from the job if it still exists
|
|
172
|
+
// Note: Job may have been deleted if deleteJobOnComplete is true
|
|
173
|
+
let output = {};
|
|
174
|
+
try {
|
|
175
|
+
const completedJob = await this.payload.findByID({
|
|
176
|
+
id: job.id,
|
|
177
|
+
collection: 'payload-jobs',
|
|
178
|
+
req
|
|
179
|
+
});
|
|
180
|
+
const taskStatus = completedJob.taskStatus?.[completedJob.taskSlug]?.[completedJob.totalTried];
|
|
181
|
+
output = taskStatus?.output || {};
|
|
182
|
+
} catch {
|
|
183
|
+
// Job was deleted after completion - this is expected behavior with deleteJobOnComplete: true
|
|
184
|
+
// The job succeeded, so we proceed without the output
|
|
185
|
+
this.logger.debug({
|
|
186
|
+
stepName: step.stepName
|
|
187
|
+
}, 'Job was deleted after successful completion (deleteJobOnComplete)');
|
|
237
188
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
189
|
+
result.status = 'succeeded';
|
|
190
|
+
result.output = output;
|
|
191
|
+
result.completedAt = new Date().toISOString();
|
|
192
|
+
result.duration = new Date(result.completedAt).getTime() - new Date(result.startedAt).getTime();
|
|
193
|
+
} else {
|
|
194
|
+
// Job failed - try to get error details from the job
|
|
195
|
+
let errorMessage = 'Task failed';
|
|
196
|
+
try {
|
|
197
|
+
const completedJob = await this.payload.findByID({
|
|
198
|
+
id: job.id,
|
|
199
|
+
collection: 'payload-jobs',
|
|
200
|
+
req
|
|
201
|
+
});
|
|
202
|
+
const taskStatus = completedJob.taskStatus?.[completedJob.taskSlug]?.[completedJob.totalTried];
|
|
203
|
+
if (completedJob.log && completedJob.log.length > 0) {
|
|
204
|
+
const latestLog = completedJob.log[completedJob.log.length - 1];
|
|
205
|
+
errorMessage = latestLog.error?.message || latestLog.error || errorMessage;
|
|
244
206
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (!errorMessage && completedJob.result) {
|
|
248
|
-
const result = completedJob.result;
|
|
249
|
-
if (result.error) {
|
|
250
|
-
errorMessage = result.error;
|
|
207
|
+
if (taskStatus?.output?.errorMessage) {
|
|
208
|
+
errorMessage = taskStatus.output.errorMessage;
|
|
251
209
|
}
|
|
210
|
+
} catch {
|
|
211
|
+
// Job may have been deleted - use the job status from run result
|
|
212
|
+
errorMessage = `Task failed with status: ${jobStatus?.status || 'unknown'}`;
|
|
252
213
|
}
|
|
253
|
-
|
|
254
|
-
if (!errorMessage) {
|
|
255
|
-
const jobDetails = {
|
|
256
|
-
taskSlug,
|
|
257
|
-
hasError: completedJob.hasError,
|
|
258
|
-
taskStatus: taskStatus?.complete,
|
|
259
|
-
totalTried: completedJob.totalTried
|
|
260
|
-
};
|
|
261
|
-
errorMessage = `Task ${taskSlug} failed without detailed error information. Job details: ${JSON.stringify(jobDetails)}`;
|
|
262
|
-
}
|
|
214
|
+
throw new Error(errorMessage);
|
|
263
215
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
};
|
|
269
|
-
// Store the output and error
|
|
270
|
-
context.steps[stepName].output = result.output;
|
|
271
|
-
context.steps[stepName].state = result.state;
|
|
272
|
-
if (result.error) {
|
|
273
|
-
context.steps[stepName].error = result.error;
|
|
274
|
-
}
|
|
275
|
-
// Independent execution tracking (not dependent on PayloadCMS task status)
|
|
276
|
-
context.steps[stepName].executionInfo = {
|
|
277
|
-
completed: true,
|
|
278
|
-
success: result.state === 'succeeded',
|
|
279
|
-
executedAt: new Date().toISOString(),
|
|
280
|
-
duration: Date.now() - (context.steps[stepName]._startTime || Date.now())
|
|
216
|
+
context.steps[step.stepName] = {
|
|
217
|
+
state: 'succeeded',
|
|
218
|
+
input: resolvedInput,
|
|
219
|
+
output: result.output
|
|
281
220
|
};
|
|
282
|
-
// For failed steps, try to extract detailed error information from the job logs
|
|
283
|
-
// This approach is more reliable than external storage and persists with the workflow
|
|
284
|
-
if (result.state === 'failed') {
|
|
285
|
-
const errorDetails = this.extractErrorDetailsFromJob(completedJob, context.steps[stepName], stepName);
|
|
286
|
-
if (errorDetails) {
|
|
287
|
-
context.steps[stepName].errorDetails = errorDetails;
|
|
288
|
-
this.logger.info({
|
|
289
|
-
stepName,
|
|
290
|
-
errorType: errorDetails.errorType,
|
|
291
|
-
duration: errorDetails.duration,
|
|
292
|
-
attempts: errorDetails.attempts
|
|
293
|
-
}, 'Extracted detailed error information for failed step');
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
this.logger.debug({
|
|
297
|
-
context
|
|
298
|
-
}, 'Step execution context');
|
|
299
|
-
if (result.state !== 'succeeded') {
|
|
300
|
-
throw new Error(result.error || `Step ${stepName} failed`);
|
|
301
|
-
}
|
|
302
221
|
this.logger.info({
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}, 'Step completed');
|
|
306
|
-
// Update workflow run with current step results if workflowRunId is provided
|
|
307
|
-
if (workflowRunId) {
|
|
308
|
-
await this.updateWorkflowRunContext(workflowRunId, context, req);
|
|
309
|
-
}
|
|
222
|
+
stepName: step.stepName,
|
|
223
|
+
duration: result.duration
|
|
224
|
+
}, 'Step completed successfully');
|
|
310
225
|
} catch (error) {
|
|
311
226
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
failureReason: errorMessage
|
|
227
|
+
result.status = 'failed';
|
|
228
|
+
result.error = errorMessage;
|
|
229
|
+
result.completedAt = new Date().toISOString();
|
|
230
|
+
result.duration = new Date(result.completedAt).getTime() - new Date(result.startedAt).getTime();
|
|
231
|
+
context.steps[step.stepName] = {
|
|
232
|
+
state: 'failed',
|
|
233
|
+
input: resolvedInput,
|
|
234
|
+
error: errorMessage
|
|
321
235
|
};
|
|
322
236
|
this.logger.error({
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
stepName,
|
|
326
|
-
taskSlug
|
|
237
|
+
stepName: step.stepName,
|
|
238
|
+
error: errorMessage
|
|
327
239
|
}, 'Step execution failed');
|
|
328
|
-
// Update workflow run with current step results if workflowRunId is provided
|
|
329
|
-
if (workflowRunId) {
|
|
330
|
-
try {
|
|
331
|
-
await this.updateWorkflowRunContext(workflowRunId, context, req);
|
|
332
|
-
} catch (updateError) {
|
|
333
|
-
this.logger.error({
|
|
334
|
-
error: updateError instanceof Error ? updateError.message : 'Unknown error',
|
|
335
|
-
stepName
|
|
336
|
-
}, 'Failed to update workflow run context after step failure');
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
240
|
throw error;
|
|
340
241
|
}
|
|
242
|
+
return result;
|
|
341
243
|
}
|
|
342
244
|
/**
|
|
343
|
-
*
|
|
344
|
-
*/
|
|
245
|
+
* Resolve step input using JSONata expressions
|
|
246
|
+
*/ async resolveStepInput(config, context) {
|
|
345
247
|
try {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const latestLog = logs[logs.length - 1];
|
|
350
|
-
// Extract error message from job error or log
|
|
351
|
-
const errorMessage = job.error?.message || latestLog?.error?.message || 'Unknown error';
|
|
352
|
-
// For timeout scenarios, check if it's a timeout based on duration and timeout setting
|
|
353
|
-
let errorType = this.classifyErrorType(errorMessage);
|
|
354
|
-
// Special handling for HTTP timeouts - if task failed and duration exceeds timeout, it's likely a timeout
|
|
355
|
-
if (errorType === 'unknown' && input.timeout && stepContext.executionInfo?.duration) {
|
|
356
|
-
const timeoutMs = parseInt(input.timeout) || 30000;
|
|
357
|
-
const actualDuration = stepContext.executionInfo.duration;
|
|
358
|
-
// If execution duration is close to or exceeds timeout, classify as timeout
|
|
359
|
-
if (actualDuration >= timeoutMs * 0.9) {
|
|
360
|
-
errorType = 'timeout';
|
|
361
|
-
this.logger.debug({
|
|
362
|
-
timeoutMs,
|
|
363
|
-
actualDuration,
|
|
364
|
-
stepName
|
|
365
|
-
}, 'Classified error as timeout based on duration analysis');
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
// Calculate duration from execution info if available
|
|
369
|
-
const duration = stepContext.executionInfo?.duration || 0;
|
|
370
|
-
// Extract attempt count from logs
|
|
371
|
-
const attempts = job.totalTried || 1;
|
|
372
|
-
return {
|
|
373
|
-
stepId: `${stepName}-${Date.now()}`,
|
|
374
|
-
errorType,
|
|
375
|
-
duration,
|
|
376
|
-
attempts,
|
|
377
|
-
finalError: errorMessage,
|
|
378
|
-
context: {
|
|
379
|
-
url: input.url,
|
|
380
|
-
method: input.method,
|
|
381
|
-
timeout: input.timeout,
|
|
382
|
-
statusCode: latestLog?.output?.status,
|
|
383
|
-
headers: input.headers
|
|
384
|
-
},
|
|
385
|
-
timestamp: new Date().toISOString()
|
|
386
|
-
};
|
|
248
|
+
return await resolveInput(config, context, {
|
|
249
|
+
timeout: 5000
|
|
250
|
+
});
|
|
387
251
|
} catch (error) {
|
|
388
252
|
this.logger.warn({
|
|
389
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
return null;
|
|
253
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
254
|
+
}, 'Failed to resolve step input, using raw config');
|
|
255
|
+
return config;
|
|
393
256
|
}
|
|
394
257
|
}
|
|
395
258
|
/**
|
|
396
|
-
*
|
|
397
|
-
*/
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
// Build the step map and dependency graph
|
|
402
|
-
for (const step of steps){
|
|
403
|
-
const stepName = step.name || `step-${steps.indexOf(step)}`;
|
|
404
|
-
const dependencies = step.dependencies || [];
|
|
405
|
-
stepMap.set(stepName, {
|
|
406
|
-
...step,
|
|
407
|
-
name: stepName,
|
|
408
|
-
dependencies
|
|
259
|
+
* Evaluate a condition using JSONata
|
|
260
|
+
*/ async evaluateCondition(condition, context) {
|
|
261
|
+
try {
|
|
262
|
+
return await evalCondition(condition, context, {
|
|
263
|
+
timeout: 5000
|
|
409
264
|
});
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
while(processed.size < steps.length){
|
|
417
|
-
const currentBatch = [];
|
|
418
|
-
// Find all steps with no remaining dependencies
|
|
419
|
-
for (const [stepName, inDegree] of indegree.entries()){
|
|
420
|
-
if (inDegree === 0 && !processed.has(stepName)) {
|
|
421
|
-
const step = stepMap.get(stepName);
|
|
422
|
-
if (step) {
|
|
423
|
-
currentBatch.push(step);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
if (currentBatch.length === 0) {
|
|
428
|
-
throw new Error('Circular dependency detected in workflow steps');
|
|
429
|
-
}
|
|
430
|
-
executionBatches.push(currentBatch);
|
|
431
|
-
// Update indegrees for next iteration
|
|
432
|
-
for (const step of currentBatch){
|
|
433
|
-
processed.add(step.name);
|
|
434
|
-
// Reduce indegree for steps that depend on completed steps
|
|
435
|
-
for (const [otherStepName, dependencies] of dependencyGraph.entries()){
|
|
436
|
-
if (dependencies.includes(step.name) && !processed.has(otherStepName)) {
|
|
437
|
-
indegree.set(otherStepName, (indegree.get(otherStepName) || 0) - 1);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return executionBatches;
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Resolve step input using Handlebars templates with automatic type conversion
|
|
446
|
-
*/ resolveStepInput(config, context, stepType) {
|
|
447
|
-
const resolved = {};
|
|
448
|
-
this.logger.debug({
|
|
449
|
-
configKeys: Object.keys(config),
|
|
450
|
-
contextSteps: Object.keys(context.steps),
|
|
451
|
-
triggerType: context.trigger?.type,
|
|
452
|
-
stepType
|
|
453
|
-
}, 'Starting step input resolution with Handlebars');
|
|
454
|
-
for (const [key, value] of Object.entries(config)){
|
|
455
|
-
if (typeof value === 'string') {
|
|
456
|
-
// Check if the string contains Handlebars templates
|
|
457
|
-
if (value.includes('{{') && value.includes('}}')) {
|
|
458
|
-
this.logger.debug({
|
|
459
|
-
key,
|
|
460
|
-
template: value,
|
|
461
|
-
availableSteps: Object.keys(context.steps),
|
|
462
|
-
hasTriggerData: !!context.trigger?.data,
|
|
463
|
-
hasTriggerDoc: !!context.trigger?.doc
|
|
464
|
-
}, 'Processing Handlebars template');
|
|
465
|
-
try {
|
|
466
|
-
const template = Handlebars.compile(value);
|
|
467
|
-
const result = template(context);
|
|
468
|
-
this.logger.debug({
|
|
469
|
-
key,
|
|
470
|
-
template: value,
|
|
471
|
-
result: JSON.stringify(result).substring(0, 200),
|
|
472
|
-
resultType: typeof result
|
|
473
|
-
}, 'Handlebars template resolved successfully');
|
|
474
|
-
resolved[key] = this.convertValueType(result, key);
|
|
475
|
-
} catch (error) {
|
|
476
|
-
this.logger.warn({
|
|
477
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
478
|
-
key,
|
|
479
|
-
template: value,
|
|
480
|
-
contextSnapshot: JSON.stringify(context).substring(0, 500)
|
|
481
|
-
}, 'Failed to resolve Handlebars template');
|
|
482
|
-
resolved[key] = value; // Keep original value if resolution fails
|
|
483
|
-
}
|
|
484
|
-
} else {
|
|
485
|
-
// Regular string, apply type conversion
|
|
486
|
-
resolved[key] = this.convertValueType(value, key);
|
|
487
|
-
}
|
|
488
|
-
} else if (typeof value === 'object' && value !== null) {
|
|
489
|
-
// Recursively resolve nested objects
|
|
490
|
-
this.logger.debug({
|
|
491
|
-
key,
|
|
492
|
-
nestedKeys: Object.keys(value)
|
|
493
|
-
}, 'Recursively resolving nested object');
|
|
494
|
-
resolved[key] = this.resolveStepInput(value, context, stepType);
|
|
495
|
-
} else {
|
|
496
|
-
// Keep literal values as-is
|
|
497
|
-
resolved[key] = value;
|
|
498
|
-
}
|
|
265
|
+
} catch (error) {
|
|
266
|
+
this.logger.warn({
|
|
267
|
+
condition,
|
|
268
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
269
|
+
}, 'Failed to evaluate condition');
|
|
270
|
+
return false;
|
|
499
271
|
}
|
|
500
|
-
this.logger.debug({
|
|
501
|
-
resolvedKeys: Object.keys(resolved),
|
|
502
|
-
originalKeys: Object.keys(config)
|
|
503
|
-
}, 'Step input resolution completed');
|
|
504
|
-
return resolved;
|
|
505
272
|
}
|
|
506
273
|
/**
|
|
507
|
-
* Safely serialize an object
|
|
274
|
+
* Safely serialize an object for storage
|
|
508
275
|
*/ safeSerialize(obj) {
|
|
509
276
|
const seen = new WeakSet();
|
|
510
|
-
|
|
277
|
+
// Keys to completely exclude from serialization
|
|
278
|
+
const excludeKeys = new Set([
|
|
279
|
+
'table',
|
|
280
|
+
'schema',
|
|
281
|
+
'_',
|
|
282
|
+
'__',
|
|
283
|
+
'payload',
|
|
284
|
+
'res',
|
|
285
|
+
'transactionID',
|
|
286
|
+
'i18n',
|
|
287
|
+
'fallbackLocale'
|
|
288
|
+
]);
|
|
289
|
+
// For req object, only keep these useful debugging properties
|
|
290
|
+
const reqAllowedKeys = new Set([
|
|
291
|
+
'payloadAPI',
|
|
292
|
+
'locale',
|
|
293
|
+
'user',
|
|
294
|
+
'method',
|
|
295
|
+
'url'
|
|
296
|
+
]);
|
|
297
|
+
const serialize = (value, parentKey)=>{
|
|
511
298
|
if (value === null || typeof value !== 'object') {
|
|
512
299
|
return value;
|
|
513
300
|
}
|
|
@@ -516,18 +303,20 @@ export class WorkflowExecutor {
|
|
|
516
303
|
}
|
|
517
304
|
seen.add(value);
|
|
518
305
|
if (Array.isArray(value)) {
|
|
519
|
-
return value.map(serialize);
|
|
306
|
+
return value.map((v)=>serialize(v));
|
|
520
307
|
}
|
|
521
308
|
const result = {};
|
|
522
309
|
for (const [key, val] of Object.entries(value)){
|
|
523
310
|
try {
|
|
524
|
-
|
|
525
|
-
if (key === 'table' || key === 'schema' || key === '_' || key === '__') {
|
|
311
|
+
if (excludeKeys.has(key)) {
|
|
526
312
|
continue;
|
|
527
313
|
}
|
|
528
|
-
|
|
314
|
+
// Special handling for req object - only include allowed keys
|
|
315
|
+
if (parentKey === 'req' && !reqAllowedKeys.has(key)) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
result[key] = serialize(val, key);
|
|
529
319
|
} catch {
|
|
530
|
-
// Skip properties that can't be accessed or serialized
|
|
531
320
|
result[key] = '[Non-serializable]';
|
|
532
321
|
}
|
|
533
322
|
}
|
|
@@ -536,277 +325,165 @@ export class WorkflowExecutor {
|
|
|
536
325
|
return serialize(obj);
|
|
537
326
|
}
|
|
538
327
|
/**
|
|
539
|
-
*
|
|
540
|
-
*/ async
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
328
|
+
* Execute a workflow with the given context
|
|
329
|
+
*/ async execute(workflow, context, req, firedTrigger) {
|
|
330
|
+
this.logger.info({
|
|
331
|
+
workflowId: workflow.id,
|
|
332
|
+
workflowName: workflow.name,
|
|
333
|
+
triggerId: firedTrigger?.id,
|
|
334
|
+
triggerName: firedTrigger?.name
|
|
335
|
+
}, 'Starting workflow execution');
|
|
336
|
+
const resolvedSteps = await this.resolveWorkflowSteps(workflow);
|
|
337
|
+
const stepResults = [];
|
|
338
|
+
for (const step of resolvedSteps){
|
|
339
|
+
stepResults.push({
|
|
340
|
+
step: step.stepId,
|
|
341
|
+
stepName: step.stepName,
|
|
342
|
+
stepIndex: step.stepIndex,
|
|
343
|
+
status: 'pending'
|
|
553
344
|
});
|
|
554
|
-
|
|
555
|
-
|
|
345
|
+
}
|
|
346
|
+
const workflowRun = await this.payload.create({
|
|
556
347
|
collection: 'workflow-runs',
|
|
557
348
|
data: {
|
|
558
|
-
|
|
349
|
+
workflow: workflow.id,
|
|
350
|
+
workflowVersion: 1,
|
|
351
|
+
firedTrigger: firedTrigger?.id,
|
|
352
|
+
triggerData: this.safeSerialize(context.trigger),
|
|
353
|
+
status: 'running',
|
|
354
|
+
startedAt: new Date().toISOString(),
|
|
355
|
+
triggeredBy: context.trigger.req?.user?.email || 'system',
|
|
356
|
+
stepResults,
|
|
357
|
+
context: this.safeSerialize(context),
|
|
358
|
+
inputs: this.safeSerialize(context.trigger),
|
|
359
|
+
logs: [
|
|
360
|
+
{
|
|
361
|
+
timestamp: new Date().toISOString(),
|
|
362
|
+
level: 'info',
|
|
363
|
+
message: 'Workflow execution started'
|
|
364
|
+
}
|
|
365
|
+
]
|
|
559
366
|
},
|
|
560
367
|
req
|
|
561
368
|
});
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* Evaluate a condition using Handlebars templates and comparison operators
|
|
565
|
-
*/ evaluateCondition(condition, context) {
|
|
566
|
-
this.logger.debug({
|
|
567
|
-
condition,
|
|
568
|
-
contextKeys: Object.keys(context),
|
|
569
|
-
triggerType: context.trigger?.type,
|
|
570
|
-
triggerData: context.trigger?.data,
|
|
571
|
-
triggerDoc: context.trigger?.doc ? 'present' : 'absent'
|
|
572
|
-
}, 'Starting condition evaluation');
|
|
573
|
-
try {
|
|
574
|
-
// Check if this is a comparison expression
|
|
575
|
-
const comparisonMatch = condition.match(/^(.+?)\s*(==|!=|>|<|>=|<=)\s*(.+)$/);
|
|
576
|
-
if (comparisonMatch) {
|
|
577
|
-
const [, leftExpr, operator, rightExpr] = comparisonMatch;
|
|
578
|
-
// Evaluate left side (could be Handlebars template or JSONPath)
|
|
579
|
-
const leftValue = this.resolveConditionValue(leftExpr.trim(), context);
|
|
580
|
-
// Evaluate right side (could be Handlebars template, JSONPath, or literal)
|
|
581
|
-
const rightValue = this.resolveConditionValue(rightExpr.trim(), context);
|
|
582
|
-
this.logger.debug({
|
|
583
|
-
condition,
|
|
584
|
-
leftExpr: leftExpr.trim(),
|
|
585
|
-
leftValue,
|
|
586
|
-
operator,
|
|
587
|
-
rightExpr: rightExpr.trim(),
|
|
588
|
-
rightValue,
|
|
589
|
-
leftType: typeof leftValue,
|
|
590
|
-
rightType: typeof rightValue
|
|
591
|
-
}, 'Evaluating comparison condition');
|
|
592
|
-
// Perform comparison
|
|
593
|
-
let result;
|
|
594
|
-
switch(operator){
|
|
595
|
-
case '!=':
|
|
596
|
-
result = leftValue !== rightValue;
|
|
597
|
-
break;
|
|
598
|
-
case '<':
|
|
599
|
-
result = Number(leftValue) < Number(rightValue);
|
|
600
|
-
break;
|
|
601
|
-
case '<=':
|
|
602
|
-
result = Number(leftValue) <= Number(rightValue);
|
|
603
|
-
break;
|
|
604
|
-
case '==':
|
|
605
|
-
result = leftValue === rightValue;
|
|
606
|
-
break;
|
|
607
|
-
case '>':
|
|
608
|
-
result = Number(leftValue) > Number(rightValue);
|
|
609
|
-
break;
|
|
610
|
-
case '>=':
|
|
611
|
-
result = Number(leftValue) >= Number(rightValue);
|
|
612
|
-
break;
|
|
613
|
-
default:
|
|
614
|
-
throw new Error(`Unknown comparison operator: ${operator}`);
|
|
615
|
-
}
|
|
616
|
-
this.logger.debug({
|
|
617
|
-
condition,
|
|
618
|
-
result,
|
|
619
|
-
leftValue,
|
|
620
|
-
rightValue,
|
|
621
|
-
operator
|
|
622
|
-
}, 'Comparison condition evaluation completed');
|
|
623
|
-
return result;
|
|
624
|
-
} else {
|
|
625
|
-
// Treat as template or JSONPath boolean evaluation
|
|
626
|
-
const result = this.resolveConditionValue(condition, context);
|
|
627
|
-
this.logger.debug({
|
|
628
|
-
condition,
|
|
629
|
-
result,
|
|
630
|
-
resultType: Array.isArray(result) ? 'array' : typeof result,
|
|
631
|
-
resultLength: Array.isArray(result) ? result.length : undefined
|
|
632
|
-
}, 'Boolean evaluation result');
|
|
633
|
-
// Handle different result types
|
|
634
|
-
let finalResult;
|
|
635
|
-
if (Array.isArray(result)) {
|
|
636
|
-
finalResult = result.length > 0 && Boolean(result[0]);
|
|
637
|
-
} else {
|
|
638
|
-
finalResult = Boolean(result);
|
|
639
|
-
}
|
|
640
|
-
this.logger.debug({
|
|
641
|
-
condition,
|
|
642
|
-
finalResult,
|
|
643
|
-
originalResult: result
|
|
644
|
-
}, 'Boolean condition evaluation completed');
|
|
645
|
-
return finalResult;
|
|
646
|
-
}
|
|
647
|
-
} catch (error) {
|
|
648
|
-
this.logger.warn({
|
|
649
|
-
condition,
|
|
650
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
651
|
-
errorStack: error instanceof Error ? error.stack : undefined
|
|
652
|
-
}, 'Failed to evaluate condition');
|
|
653
|
-
// If condition evaluation fails, assume false
|
|
654
|
-
return false;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
/**
|
|
658
|
-
* Resolve a condition value using Handlebars templates or JSONPath
|
|
659
|
-
*/ resolveConditionValue(expr, context) {
|
|
660
|
-
// Handle string literals
|
|
661
|
-
if (expr.startsWith('"') && expr.endsWith('"') || expr.startsWith("'") && expr.endsWith("'")) {
|
|
662
|
-
return expr.slice(1, -1) // Remove quotes
|
|
663
|
-
;
|
|
664
|
-
}
|
|
665
|
-
// Handle boolean literals
|
|
666
|
-
if (expr === 'true') {
|
|
667
|
-
return true;
|
|
668
|
-
}
|
|
669
|
-
if (expr === 'false') {
|
|
670
|
-
return false;
|
|
671
|
-
}
|
|
672
|
-
// Handle number literals
|
|
673
|
-
if (/^-?\d+(?:\.\d+)?$/.test(expr)) {
|
|
674
|
-
return Number(expr);
|
|
675
|
-
}
|
|
676
|
-
// Handle Handlebars templates
|
|
677
|
-
if (expr.includes('{{') && expr.includes('}}')) {
|
|
678
|
-
try {
|
|
679
|
-
const template = Handlebars.compile(expr);
|
|
680
|
-
return template(context);
|
|
681
|
-
} catch (error) {
|
|
682
|
-
this.logger.warn({
|
|
683
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
684
|
-
expr
|
|
685
|
-
}, 'Failed to resolve Handlebars condition');
|
|
686
|
-
return false;
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
// Return as string if nothing else matches
|
|
690
|
-
return expr;
|
|
691
|
-
}
|
|
692
|
-
/**
|
|
693
|
-
* Execute a workflow with the given context
|
|
694
|
-
*/ async execute(workflow, context, req) {
|
|
695
|
-
this.logger.info({
|
|
696
|
-
workflowId: workflow.id,
|
|
697
|
-
workflowName: workflow.name
|
|
698
|
-
}, 'Starting workflow execution');
|
|
699
|
-
const serializeContext = ()=>({
|
|
700
|
-
steps: this.safeSerialize(context.steps),
|
|
701
|
-
trigger: {
|
|
702
|
-
type: context.trigger.type,
|
|
703
|
-
collection: context.trigger.collection,
|
|
704
|
-
data: this.safeSerialize(context.trigger.data),
|
|
705
|
-
doc: this.safeSerialize(context.trigger.doc),
|
|
706
|
-
operation: context.trigger.operation,
|
|
707
|
-
previousDoc: this.safeSerialize(context.trigger.previousDoc),
|
|
708
|
-
triggeredAt: context.trigger.triggeredAt,
|
|
709
|
-
user: context.trigger.req?.user
|
|
710
|
-
}
|
|
711
|
-
});
|
|
712
369
|
this.logger.info({
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}, 'About to create workflow run record');
|
|
723
|
-
// Create a workflow run record
|
|
724
|
-
let workflowRun;
|
|
725
|
-
try {
|
|
726
|
-
workflowRun = await this.payload.create({
|
|
727
|
-
collection: 'workflow-runs',
|
|
728
|
-
data: {
|
|
729
|
-
context: serializeContext(),
|
|
730
|
-
startedAt: new Date().toISOString(),
|
|
731
|
-
status: 'running',
|
|
732
|
-
triggeredBy: context.trigger.req?.user?.email || 'system',
|
|
733
|
-
workflow: workflow.id,
|
|
734
|
-
workflowVersion: 1 // Default version since generated type doesn't have _version field
|
|
735
|
-
},
|
|
736
|
-
req
|
|
737
|
-
});
|
|
738
|
-
this.logger.info({
|
|
739
|
-
workflowRunId: workflowRun.id,
|
|
740
|
-
workflowId: workflow.id,
|
|
741
|
-
workflowName: workflow.name
|
|
742
|
-
}, 'Workflow run record created successfully');
|
|
743
|
-
} catch (error) {
|
|
744
|
-
this.logger.error({
|
|
745
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
746
|
-
errorStack: error instanceof Error ? error.stack : undefined,
|
|
747
|
-
workflowId: workflow.id,
|
|
748
|
-
workflowName: workflow.name
|
|
749
|
-
}, 'Failed to create workflow run record');
|
|
750
|
-
throw error;
|
|
751
|
-
}
|
|
370
|
+
workflowRunId: workflowRun.id,
|
|
371
|
+
workflowId: workflow.id
|
|
372
|
+
}, 'Workflow run record created');
|
|
373
|
+
// Create job metadata for tracking workflow context in payload-jobs
|
|
374
|
+
const jobMeta = {
|
|
375
|
+
automationWorkflowId: workflow.id,
|
|
376
|
+
automationWorkflowRunId: workflowRun.id,
|
|
377
|
+
automationTriggerId: firedTrigger?.id
|
|
378
|
+
};
|
|
752
379
|
try {
|
|
753
|
-
|
|
754
|
-
const executionBatches = this.resolveExecutionOrder(workflow.steps || []);
|
|
380
|
+
const executionBatches = this.resolveExecutionOrder(resolvedSteps);
|
|
755
381
|
this.logger.info({
|
|
756
|
-
|
|
757
|
-
|
|
382
|
+
batchCount: executionBatches.length,
|
|
383
|
+
batchSizes: executionBatches.map((b)=>b.length)
|
|
758
384
|
}, 'Resolved step execution order');
|
|
759
|
-
// Execute each batch in sequence, but steps within each batch in parallel
|
|
760
385
|
for(let batchIndex = 0; batchIndex < executionBatches.length; batchIndex++){
|
|
761
386
|
const batch = executionBatches[batchIndex];
|
|
762
387
|
this.logger.info({
|
|
763
388
|
batchIndex,
|
|
764
389
|
stepCount: batch.length,
|
|
765
|
-
stepNames: batch.map((s)=>s.
|
|
390
|
+
stepNames: batch.map((s)=>s.stepName)
|
|
766
391
|
}, 'Executing batch');
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
392
|
+
const batchPromises = batch.map(async (step)=>{
|
|
393
|
+
try {
|
|
394
|
+
const result = await this.executeStep(step, context, req, stepResults, jobMeta);
|
|
395
|
+
const idx = stepResults.findIndex((r)=>r.stepIndex === step.stepIndex);
|
|
396
|
+
if (idx !== -1) {
|
|
397
|
+
stepResults[idx] = result;
|
|
398
|
+
}
|
|
399
|
+
return result;
|
|
400
|
+
} catch (error) {
|
|
401
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
402
|
+
const idx = stepResults.findIndex((r)=>r.stepIndex === step.stepIndex);
|
|
403
|
+
if (idx !== -1) {
|
|
404
|
+
stepResults[idx] = {
|
|
405
|
+
...stepResults[idx],
|
|
406
|
+
status: 'failed',
|
|
407
|
+
error: errorMessage,
|
|
408
|
+
completedAt: new Date().toISOString()
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
if (workflow.errorHandling === 'stop') {
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
this.logger.warn({
|
|
415
|
+
stepName: step.stepName,
|
|
416
|
+
error: errorMessage
|
|
417
|
+
}, 'Step failed but continuing due to error handling setting');
|
|
418
|
+
}
|
|
419
|
+
});
|
|
770
420
|
await Promise.all(batchPromises);
|
|
771
|
-
this.
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
421
|
+
await this.payload.update({
|
|
422
|
+
id: workflowRun.id,
|
|
423
|
+
collection: 'workflow-runs',
|
|
424
|
+
data: {
|
|
425
|
+
stepResults,
|
|
426
|
+
context: this.safeSerialize(context)
|
|
427
|
+
},
|
|
428
|
+
req
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
const outputs = {};
|
|
432
|
+
for (const result of stepResults){
|
|
433
|
+
if (result.status === 'succeeded' && result.output) {
|
|
434
|
+
outputs[result.stepName] = result.output;
|
|
435
|
+
}
|
|
775
436
|
}
|
|
776
|
-
// Update workflow run as completed
|
|
777
437
|
await this.payload.update({
|
|
778
438
|
id: workflowRun.id,
|
|
779
439
|
collection: 'workflow-runs',
|
|
780
440
|
data: {
|
|
441
|
+
status: 'completed',
|
|
781
442
|
completedAt: new Date().toISOString(),
|
|
782
|
-
|
|
783
|
-
|
|
443
|
+
stepResults,
|
|
444
|
+
context: this.safeSerialize(context),
|
|
445
|
+
outputs,
|
|
446
|
+
logs: [
|
|
447
|
+
...workflowRun.logs || [],
|
|
448
|
+
{
|
|
449
|
+
timestamp: new Date().toISOString(),
|
|
450
|
+
level: 'info',
|
|
451
|
+
message: 'Workflow execution completed successfully'
|
|
452
|
+
}
|
|
453
|
+
]
|
|
784
454
|
},
|
|
785
455
|
req
|
|
786
456
|
});
|
|
787
457
|
this.logger.info({
|
|
788
|
-
|
|
789
|
-
workflowId: workflow.id
|
|
790
|
-
workflowName: workflow.name
|
|
458
|
+
workflowRunId: workflowRun.id,
|
|
459
|
+
workflowId: workflow.id
|
|
791
460
|
}, 'Workflow execution completed');
|
|
792
461
|
} catch (error) {
|
|
793
|
-
|
|
462
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
794
463
|
await this.payload.update({
|
|
795
464
|
id: workflowRun.id,
|
|
796
465
|
collection: 'workflow-runs',
|
|
797
466
|
data: {
|
|
467
|
+
status: 'failed',
|
|
798
468
|
completedAt: new Date().toISOString(),
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
469
|
+
stepResults,
|
|
470
|
+
context: this.safeSerialize(context),
|
|
471
|
+
error: errorMessage,
|
|
472
|
+
logs: [
|
|
473
|
+
...workflowRun.logs || [],
|
|
474
|
+
{
|
|
475
|
+
timestamp: new Date().toISOString(),
|
|
476
|
+
level: 'error',
|
|
477
|
+
message: `Workflow execution failed: ${errorMessage}`
|
|
478
|
+
}
|
|
479
|
+
]
|
|
802
480
|
},
|
|
803
481
|
req
|
|
804
482
|
});
|
|
805
483
|
this.logger.error({
|
|
806
|
-
|
|
807
|
-
runId: workflowRun.id,
|
|
484
|
+
workflowRunId: workflowRun.id,
|
|
808
485
|
workflowId: workflow.id,
|
|
809
|
-
|
|
486
|
+
error: errorMessage
|
|
810
487
|
}, 'Workflow execution failed');
|
|
811
488
|
throw error;
|
|
812
489
|
}
|