@xtr-dev/payload-automation 0.0.21 â 0.0.23
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 +73 -2
- package/dist/collections/Workflow.js +19 -7
- package/dist/collections/Workflow.js.map +1 -1
- package/dist/collections/WorkflowRuns.js +14 -7
- package/dist/collections/WorkflowRuns.js.map +1 -1
- package/dist/components/StatusCell.d.ts +6 -0
- package/dist/components/StatusCell.js +75 -0
- package/dist/components/StatusCell.js.map +1 -0
- package/dist/components/WorkflowExecutionStatus.d.ts +6 -0
- package/dist/components/WorkflowExecutionStatus.js +287 -0
- package/dist/components/WorkflowExecutionStatus.js.map +1 -0
- package/dist/core/workflow-executor.d.ts +32 -0
- package/dist/core/workflow-executor.js +162 -7
- package/dist/core/workflow-executor.js.map +1 -1
- package/dist/exports/client.d.ts +2 -0
- package/dist/exports/client.js +4 -1
- package/dist/exports/client.js.map +1 -1
- package/dist/plugin/index.js +131 -42
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/init-step-tasks.js +15 -1
- package/dist/plugin/init-step-tasks.js.map +1 -1
- package/dist/steps/create-document.js +1 -1
- package/dist/steps/create-document.js.map +1 -1
- package/dist/steps/delete-document.js +2 -2
- package/dist/steps/delete-document.js.map +1 -1
- package/dist/steps/http-request-handler.js +229 -10
- package/dist/steps/http-request-handler.js.map +1 -1
- package/dist/steps/http-request.d.ts +147 -4
- package/dist/steps/http-request.js +189 -3
- package/dist/steps/http-request.js.map +1 -1
- package/dist/steps/read-document.js +2 -2
- package/dist/steps/read-document.js.map +1 -1
- package/dist/steps/send-email.js +5 -5
- package/dist/steps/send-email.js.map +1 -1
- package/dist/steps/update-document.js +2 -2
- package/dist/steps/update-document.js.map +1 -1
- package/dist/test/create-document-step.test.js +378 -0
- package/dist/test/create-document-step.test.js.map +1 -0
- package/dist/test/http-request-step.test.js +361 -0
- package/dist/test/http-request-step.test.js.map +1 -0
- package/dist/test/workflow-executor.test.js +530 -0
- package/dist/test/workflow-executor.test.js.map +1 -0
- package/package.json +4 -1
- package/dist/test/basic.test.d.ts +0 -1
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { Button } from '@payloadcms/ui';
|
|
5
|
+
export const WorkflowExecutionStatus = ({ workflowId })=>{
|
|
6
|
+
const [runs, setRuns] = useState([]);
|
|
7
|
+
const [loading, setLoading] = useState(true);
|
|
8
|
+
const [expanded, setExpanded] = useState(false);
|
|
9
|
+
useEffect(()=>{
|
|
10
|
+
const fetchRecentRuns = async ()=>{
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch(`/api/workflow-runs?where[workflow][equals]=${workflowId}&limit=5&sort=-startedAt`);
|
|
13
|
+
if (response.ok) {
|
|
14
|
+
const data = await response.json();
|
|
15
|
+
setRuns(data.docs || []);
|
|
16
|
+
}
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.warn('Failed to fetch workflow runs:', error);
|
|
19
|
+
} finally{
|
|
20
|
+
setLoading(false);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
fetchRecentRuns();
|
|
24
|
+
}, [
|
|
25
|
+
workflowId
|
|
26
|
+
]);
|
|
27
|
+
if (loading) {
|
|
28
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
29
|
+
style: {
|
|
30
|
+
padding: '16px',
|
|
31
|
+
color: '#6B7280'
|
|
32
|
+
},
|
|
33
|
+
children: "Loading execution history..."
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (runs.length === 0) {
|
|
37
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
38
|
+
style: {
|
|
39
|
+
padding: '16px',
|
|
40
|
+
backgroundColor: '#F9FAFB',
|
|
41
|
+
border: '1px solid #E5E7EB',
|
|
42
|
+
borderRadius: '8px',
|
|
43
|
+
color: '#6B7280',
|
|
44
|
+
textAlign: 'center'
|
|
45
|
+
},
|
|
46
|
+
children: [
|
|
47
|
+
"đ No execution history yet",
|
|
48
|
+
/*#__PURE__*/ _jsx("br", {}),
|
|
49
|
+
/*#__PURE__*/ _jsx("small", {
|
|
50
|
+
children: "This workflow hasn't been triggered yet."
|
|
51
|
+
})
|
|
52
|
+
]
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const getStatusIcon = (status)=>{
|
|
56
|
+
switch(status){
|
|
57
|
+
case 'pending':
|
|
58
|
+
return 'âŗ';
|
|
59
|
+
case 'running':
|
|
60
|
+
return 'đ';
|
|
61
|
+
case 'completed':
|
|
62
|
+
return 'â
';
|
|
63
|
+
case 'failed':
|
|
64
|
+
return 'â';
|
|
65
|
+
case 'cancelled':
|
|
66
|
+
return 'âšī¸';
|
|
67
|
+
default:
|
|
68
|
+
return 'â';
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const getStatusColor = (status)=>{
|
|
72
|
+
switch(status){
|
|
73
|
+
case 'pending':
|
|
74
|
+
return '#6B7280';
|
|
75
|
+
case 'running':
|
|
76
|
+
return '#3B82F6';
|
|
77
|
+
case 'completed':
|
|
78
|
+
return '#10B981';
|
|
79
|
+
case 'failed':
|
|
80
|
+
return '#EF4444';
|
|
81
|
+
case 'cancelled':
|
|
82
|
+
return '#F59E0B';
|
|
83
|
+
default:
|
|
84
|
+
return '#6B7280';
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const formatDate = (dateString)=>{
|
|
88
|
+
const date = new Date(dateString);
|
|
89
|
+
const now = new Date();
|
|
90
|
+
const diffMs = now.getTime() - date.getTime();
|
|
91
|
+
if (diffMs < 60000) {
|
|
92
|
+
return 'Just now';
|
|
93
|
+
} else if (diffMs < 3600000) {
|
|
94
|
+
return `${Math.floor(diffMs / 60000)} min ago`;
|
|
95
|
+
} else if (diffMs < 86400000) {
|
|
96
|
+
return `${Math.floor(diffMs / 3600000)} hrs ago`;
|
|
97
|
+
} else {
|
|
98
|
+
return date.toLocaleDateString();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const getDuration = (startedAt, completedAt)=>{
|
|
102
|
+
const start = new Date(startedAt);
|
|
103
|
+
const end = completedAt ? new Date(completedAt) : new Date();
|
|
104
|
+
const diffMs = end.getTime() - start.getTime();
|
|
105
|
+
if (diffMs < 1000) return '<1s';
|
|
106
|
+
if (diffMs < 60000) return `${Math.floor(diffMs / 1000)}s`;
|
|
107
|
+
if (diffMs < 3600000) return `${Math.floor(diffMs / 60000)}m ${Math.floor(diffMs % 60000 / 1000)}s`;
|
|
108
|
+
return `${Math.floor(diffMs / 3600000)}h ${Math.floor(diffMs % 3600000 / 60000)}m`;
|
|
109
|
+
};
|
|
110
|
+
const recentRun = runs[0];
|
|
111
|
+
const recentStatus = getStatusIcon(recentRun.status);
|
|
112
|
+
const recentColor = getStatusColor(recentRun.status);
|
|
113
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
114
|
+
style: {
|
|
115
|
+
border: '1px solid #E5E7EB',
|
|
116
|
+
borderRadius: '8px',
|
|
117
|
+
backgroundColor: '#FAFAFA'
|
|
118
|
+
},
|
|
119
|
+
children: [
|
|
120
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
121
|
+
style: {
|
|
122
|
+
padding: '16px',
|
|
123
|
+
borderBottom: expanded ? '1px solid #E5E7EB' : 'none',
|
|
124
|
+
display: 'flex',
|
|
125
|
+
justifyContent: 'space-between',
|
|
126
|
+
alignItems: 'center'
|
|
127
|
+
},
|
|
128
|
+
children: [
|
|
129
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
130
|
+
style: {
|
|
131
|
+
display: 'flex',
|
|
132
|
+
alignItems: 'center',
|
|
133
|
+
gap: '12px'
|
|
134
|
+
},
|
|
135
|
+
children: [
|
|
136
|
+
/*#__PURE__*/ _jsx("span", {
|
|
137
|
+
style: {
|
|
138
|
+
fontSize: '20px'
|
|
139
|
+
},
|
|
140
|
+
children: recentStatus
|
|
141
|
+
}),
|
|
142
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
143
|
+
children: [
|
|
144
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
145
|
+
style: {
|
|
146
|
+
fontWeight: '600',
|
|
147
|
+
color: recentColor
|
|
148
|
+
},
|
|
149
|
+
children: [
|
|
150
|
+
"Last run: ",
|
|
151
|
+
recentRun.status
|
|
152
|
+
]
|
|
153
|
+
}),
|
|
154
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
155
|
+
style: {
|
|
156
|
+
fontSize: '13px',
|
|
157
|
+
color: '#6B7280'
|
|
158
|
+
},
|
|
159
|
+
children: [
|
|
160
|
+
formatDate(recentRun.startedAt),
|
|
161
|
+
" âĸ Duration: ",
|
|
162
|
+
getDuration(recentRun.startedAt, recentRun.completedAt)
|
|
163
|
+
]
|
|
164
|
+
})
|
|
165
|
+
]
|
|
166
|
+
})
|
|
167
|
+
]
|
|
168
|
+
}),
|
|
169
|
+
/*#__PURE__*/ _jsxs(Button, {
|
|
170
|
+
onClick: ()=>setExpanded(!expanded),
|
|
171
|
+
size: "small",
|
|
172
|
+
buttonStyle: "secondary",
|
|
173
|
+
children: [
|
|
174
|
+
expanded ? 'Hide' : 'Show',
|
|
175
|
+
" History (",
|
|
176
|
+
runs.length,
|
|
177
|
+
")"
|
|
178
|
+
]
|
|
179
|
+
})
|
|
180
|
+
]
|
|
181
|
+
}),
|
|
182
|
+
expanded && /*#__PURE__*/ _jsxs("div", {
|
|
183
|
+
style: {
|
|
184
|
+
padding: '16px'
|
|
185
|
+
},
|
|
186
|
+
children: [
|
|
187
|
+
/*#__PURE__*/ _jsx("h4", {
|
|
188
|
+
style: {
|
|
189
|
+
margin: '0 0 12px 0',
|
|
190
|
+
fontSize: '14px',
|
|
191
|
+
fontWeight: '600'
|
|
192
|
+
},
|
|
193
|
+
children: "Recent Executions"
|
|
194
|
+
}),
|
|
195
|
+
runs.map((run, index)=>/*#__PURE__*/ _jsxs("div", {
|
|
196
|
+
style: {
|
|
197
|
+
display: 'flex',
|
|
198
|
+
justifyContent: 'space-between',
|
|
199
|
+
alignItems: 'center',
|
|
200
|
+
padding: '8px 12px',
|
|
201
|
+
marginBottom: index < runs.length - 1 ? '8px' : '0',
|
|
202
|
+
backgroundColor: 'white',
|
|
203
|
+
border: '1px solid #E5E7EB',
|
|
204
|
+
borderRadius: '6px'
|
|
205
|
+
},
|
|
206
|
+
children: [
|
|
207
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
208
|
+
style: {
|
|
209
|
+
display: 'flex',
|
|
210
|
+
alignItems: 'center',
|
|
211
|
+
gap: '10px'
|
|
212
|
+
},
|
|
213
|
+
children: [
|
|
214
|
+
/*#__PURE__*/ _jsx("span", {
|
|
215
|
+
style: {
|
|
216
|
+
fontSize: '16px'
|
|
217
|
+
},
|
|
218
|
+
children: getStatusIcon(run.status)
|
|
219
|
+
}),
|
|
220
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
221
|
+
children: [
|
|
222
|
+
/*#__PURE__*/ _jsx("div", {
|
|
223
|
+
style: {
|
|
224
|
+
fontSize: '13px',
|
|
225
|
+
fontWeight: '500',
|
|
226
|
+
color: getStatusColor(run.status)
|
|
227
|
+
},
|
|
228
|
+
children: run.status.charAt(0).toUpperCase() + run.status.slice(1)
|
|
229
|
+
}),
|
|
230
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
231
|
+
style: {
|
|
232
|
+
fontSize: '12px',
|
|
233
|
+
color: '#6B7280'
|
|
234
|
+
},
|
|
235
|
+
children: [
|
|
236
|
+
formatDate(run.startedAt),
|
|
237
|
+
" âĸ ",
|
|
238
|
+
run.triggeredBy
|
|
239
|
+
]
|
|
240
|
+
})
|
|
241
|
+
]
|
|
242
|
+
})
|
|
243
|
+
]
|
|
244
|
+
}),
|
|
245
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
246
|
+
style: {
|
|
247
|
+
fontSize: '12px',
|
|
248
|
+
color: '#6B7280',
|
|
249
|
+
textAlign: 'right'
|
|
250
|
+
},
|
|
251
|
+
children: [
|
|
252
|
+
/*#__PURE__*/ _jsx("div", {
|
|
253
|
+
children: getDuration(run.startedAt, run.completedAt)
|
|
254
|
+
}),
|
|
255
|
+
run.error && /*#__PURE__*/ _jsx("div", {
|
|
256
|
+
style: {
|
|
257
|
+
color: '#EF4444',
|
|
258
|
+
marginTop: '2px'
|
|
259
|
+
},
|
|
260
|
+
children: "Error"
|
|
261
|
+
})
|
|
262
|
+
]
|
|
263
|
+
})
|
|
264
|
+
]
|
|
265
|
+
}, run.id)),
|
|
266
|
+
/*#__PURE__*/ _jsx("div", {
|
|
267
|
+
style: {
|
|
268
|
+
marginTop: '12px',
|
|
269
|
+
textAlign: 'center'
|
|
270
|
+
},
|
|
271
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
272
|
+
onClick: ()=>{
|
|
273
|
+
// Navigate to workflow runs filtered by this workflow
|
|
274
|
+
window.location.href = `/admin/collections/workflow-runs?where[workflow][equals]=${workflowId}`;
|
|
275
|
+
},
|
|
276
|
+
size: "small",
|
|
277
|
+
buttonStyle: "secondary",
|
|
278
|
+
children: "View All Runs â"
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
]
|
|
282
|
+
})
|
|
283
|
+
]
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
//# sourceMappingURL=WorkflowExecutionStatus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/WorkflowExecutionStatus.tsx"],"sourcesContent":["'use client'\n\nimport React, { useState, useEffect } from 'react'\nimport { Button } from '@payloadcms/ui'\n\ninterface WorkflowRun {\n id: string\n status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'\n startedAt: string\n completedAt?: string\n error?: string\n triggeredBy: string\n}\n\ninterface WorkflowExecutionStatusProps {\n workflowId: string | number\n}\n\nexport const WorkflowExecutionStatus: React.FC<WorkflowExecutionStatusProps> = ({ workflowId }) => {\n const [runs, setRuns] = useState<WorkflowRun[]>([])\n const [loading, setLoading] = useState(true)\n const [expanded, setExpanded] = useState(false)\n\n useEffect(() => {\n const fetchRecentRuns = async () => {\n try {\n const response = await fetch(`/api/workflow-runs?where[workflow][equals]=${workflowId}&limit=5&sort=-startedAt`)\n if (response.ok) {\n const data = await response.json()\n setRuns(data.docs || [])\n }\n } catch (error) {\n console.warn('Failed to fetch workflow runs:', error)\n } finally {\n setLoading(false)\n }\n }\n\n fetchRecentRuns()\n }, [workflowId])\n\n if (loading) {\n return (\n <div style={{ padding: '16px', color: '#6B7280' }}>\n Loading execution history...\n </div>\n )\n }\n\n if (runs.length === 0) {\n return (\n <div style={{ \n padding: '16px',\n backgroundColor: '#F9FAFB',\n border: '1px solid #E5E7EB',\n borderRadius: '8px',\n color: '#6B7280',\n textAlign: 'center'\n }}>\n đ No execution history yet\n <br />\n <small>This workflow hasn't been triggered yet.</small>\n </div>\n )\n }\n\n const getStatusIcon = (status: string) => {\n switch (status) {\n case 'pending': return 'âŗ'\n case 'running': return 'đ'\n case 'completed': return 'â
'\n case 'failed': return 'â'\n case 'cancelled': return 'âšī¸'\n default: return 'â'\n }\n }\n\n const getStatusColor = (status: string) => {\n switch (status) {\n case 'pending': return '#6B7280'\n case 'running': return '#3B82F6'\n case 'completed': return '#10B981'\n case 'failed': return '#EF4444'\n case 'cancelled': return '#F59E0B'\n default: return '#6B7280'\n }\n }\n\n const formatDate = (dateString: string) => {\n const date = new Date(dateString)\n const now = new Date()\n const diffMs = now.getTime() - date.getTime()\n \n if (diffMs < 60000) { // Less than 1 minute\n return 'Just now'\n } else if (diffMs < 3600000) { // Less than 1 hour\n return `${Math.floor(diffMs / 60000)} min ago`\n } else if (diffMs < 86400000) { // Less than 1 day\n return `${Math.floor(diffMs / 3600000)} hrs ago`\n } else {\n return date.toLocaleDateString()\n }\n }\n\n const getDuration = (startedAt: string, completedAt?: string) => {\n const start = new Date(startedAt)\n const end = completedAt ? new Date(completedAt) : new Date()\n const diffMs = end.getTime() - start.getTime()\n \n if (diffMs < 1000) return '<1s'\n if (diffMs < 60000) return `${Math.floor(diffMs / 1000)}s`\n if (diffMs < 3600000) return `${Math.floor(diffMs / 60000)}m ${Math.floor((diffMs % 60000) / 1000)}s`\n return `${Math.floor(diffMs / 3600000)}h ${Math.floor((diffMs % 3600000) / 60000)}m`\n }\n\n const recentRun = runs[0]\n const recentStatus = getStatusIcon(recentRun.status)\n const recentColor = getStatusColor(recentRun.status)\n\n return (\n <div style={{\n border: '1px solid #E5E7EB',\n borderRadius: '8px',\n backgroundColor: '#FAFAFA'\n }}>\n {/* Summary Header */}\n <div style={{\n padding: '16px',\n borderBottom: expanded ? '1px solid #E5E7EB' : 'none',\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center'\n }}>\n <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n <span style={{ fontSize: '20px' }}>{recentStatus}</span>\n <div>\n <div style={{ fontWeight: '600', color: recentColor }}>\n Last run: {recentRun.status}\n </div>\n <div style={{ fontSize: '13px', color: '#6B7280' }}>\n {formatDate(recentRun.startedAt)} âĸ Duration: {getDuration(recentRun.startedAt, recentRun.completedAt)}\n </div>\n </div>\n </div>\n \n <Button\n onClick={() => setExpanded(!expanded)}\n size=\"small\"\n buttonStyle=\"secondary\"\n >\n {expanded ? 'Hide' : 'Show'} History ({runs.length})\n </Button>\n </div>\n\n {/* Detailed History */}\n {expanded && (\n <div style={{ padding: '16px' }}>\n <h4 style={{ margin: '0 0 12px 0', fontSize: '14px', fontWeight: '600' }}>\n Recent Executions\n </h4>\n \n {runs.map((run, index) => (\n <div\n key={run.id}\n style={{\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n padding: '8px 12px',\n marginBottom: index < runs.length - 1 ? '8px' : '0',\n backgroundColor: 'white',\n border: '1px solid #E5E7EB',\n borderRadius: '6px'\n }}\n >\n <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>\n <span style={{ fontSize: '16px' }}>\n {getStatusIcon(run.status)}\n </span>\n \n <div>\n <div style={{ \n fontSize: '13px', \n fontWeight: '500',\n color: getStatusColor(run.status)\n }}>\n {run.status.charAt(0).toUpperCase() + run.status.slice(1)}\n </div>\n <div style={{ fontSize: '12px', color: '#6B7280' }}>\n {formatDate(run.startedAt)} âĸ {run.triggeredBy}\n </div>\n </div>\n </div>\n \n <div style={{ \n fontSize: '12px', \n color: '#6B7280',\n textAlign: 'right'\n }}>\n <div>\n {getDuration(run.startedAt, run.completedAt)}\n </div>\n {run.error && (\n <div style={{ color: '#EF4444', marginTop: '2px' }}>\n Error\n </div>\n )}\n </div>\n </div>\n ))}\n \n <div style={{ \n marginTop: '12px', \n textAlign: 'center' \n }}>\n <Button\n onClick={() => {\n // Navigate to workflow runs filtered by this workflow\n window.location.href = `/admin/collections/workflow-runs?where[workflow][equals]=${workflowId}`\n }}\n size=\"small\"\n buttonStyle=\"secondary\"\n >\n View All Runs â\n </Button>\n </div>\n </div>\n )}\n </div>\n )\n}"],"names":["React","useState","useEffect","Button","WorkflowExecutionStatus","workflowId","runs","setRuns","loading","setLoading","expanded","setExpanded","fetchRecentRuns","response","fetch","ok","data","json","docs","error","console","warn","div","style","padding","color","length","backgroundColor","border","borderRadius","textAlign","br","small","getStatusIcon","status","getStatusColor","formatDate","dateString","date","Date","now","diffMs","getTime","Math","floor","toLocaleDateString","getDuration","startedAt","completedAt","start","end","recentRun","recentStatus","recentColor","borderBottom","display","justifyContent","alignItems","gap","span","fontSize","fontWeight","onClick","size","buttonStyle","h4","margin","map","run","index","marginBottom","charAt","toUpperCase","slice","triggeredBy","marginTop","id","window","location","href"],"mappings":"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"}
|
|
@@ -32,6 +32,30 @@ export interface ExecutionContext {
|
|
|
32
32
|
input: unknown;
|
|
33
33
|
output: unknown;
|
|
34
34
|
state: 'failed' | 'pending' | 'running' | 'succeeded';
|
|
35
|
+
_startTime?: number;
|
|
36
|
+
executionInfo?: {
|
|
37
|
+
completed: boolean;
|
|
38
|
+
success: boolean;
|
|
39
|
+
executedAt: string;
|
|
40
|
+
duration: number;
|
|
41
|
+
failureReason?: string;
|
|
42
|
+
};
|
|
43
|
+
errorDetails?: {
|
|
44
|
+
stepId: string;
|
|
45
|
+
errorType: string;
|
|
46
|
+
duration: number;
|
|
47
|
+
attempts: number;
|
|
48
|
+
finalError: string;
|
|
49
|
+
context: {
|
|
50
|
+
url?: string;
|
|
51
|
+
method?: string;
|
|
52
|
+
timeout?: number;
|
|
53
|
+
statusCode?: number;
|
|
54
|
+
headers?: Record<string, string>;
|
|
55
|
+
[key: string]: any;
|
|
56
|
+
};
|
|
57
|
+
timestamp: string;
|
|
58
|
+
};
|
|
35
59
|
}>;
|
|
36
60
|
trigger: {
|
|
37
61
|
collection?: string;
|
|
@@ -75,6 +99,14 @@ export declare class WorkflowExecutor {
|
|
|
75
99
|
* Safely serialize an object, handling circular references and non-serializable values
|
|
76
100
|
*/
|
|
77
101
|
private safeSerialize;
|
|
102
|
+
/**
|
|
103
|
+
* Extracts detailed error information from job logs and input
|
|
104
|
+
*/
|
|
105
|
+
private extractErrorDetailsFromJob;
|
|
106
|
+
/**
|
|
107
|
+
* Classifies error types based on error messages
|
|
108
|
+
*/
|
|
109
|
+
private classifyErrorType;
|
|
78
110
|
/**
|
|
79
111
|
* Update workflow run with current context
|
|
80
112
|
*/
|
|
@@ -81,14 +81,29 @@ export class WorkflowExecutor {
|
|
|
81
81
|
error: undefined,
|
|
82
82
|
input: undefined,
|
|
83
83
|
output: undefined,
|
|
84
|
-
state: 'running'
|
|
84
|
+
state: 'running',
|
|
85
|
+
_startTime: Date.now() // Track execution start time for independent duration tracking
|
|
85
86
|
};
|
|
86
87
|
// Move taskSlug declaration outside try block so it's accessible in catch
|
|
87
88
|
const taskSlug = step.step // Use the 'step' field for task type
|
|
88
89
|
;
|
|
89
90
|
try {
|
|
91
|
+
// Extract input data from step - PayloadCMS flattens inputSchema fields to step level
|
|
92
|
+
const inputFields = {};
|
|
93
|
+
// Get all fields except the core step fields
|
|
94
|
+
const coreFields = [
|
|
95
|
+
'step',
|
|
96
|
+
'name',
|
|
97
|
+
'dependencies',
|
|
98
|
+
'condition'
|
|
99
|
+
];
|
|
100
|
+
for (const [key, value] of Object.entries(step)){
|
|
101
|
+
if (!coreFields.includes(key)) {
|
|
102
|
+
inputFields[key] = value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
90
105
|
// Resolve input data using JSONPath
|
|
91
|
-
const resolvedInput = this.resolveStepInput(
|
|
106
|
+
const resolvedInput = this.resolveStepInput(inputFields, context);
|
|
92
107
|
context.steps[stepName].input = resolvedInput;
|
|
93
108
|
if (!taskSlug) {
|
|
94
109
|
throw new Error(`Step ${stepName} is missing a task type`);
|
|
@@ -104,17 +119,33 @@ export class WorkflowExecutor {
|
|
|
104
119
|
req,
|
|
105
120
|
task: taskSlug
|
|
106
121
|
});
|
|
107
|
-
// Run the job immediately
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
// Run the specific job immediately and wait for completion
|
|
123
|
+
this.logger.info({
|
|
124
|
+
jobId: job.id
|
|
125
|
+
}, 'Running job immediately using runByID');
|
|
126
|
+
const runResults = await this.payload.jobs.runByID({
|
|
127
|
+
id: job.id,
|
|
110
128
|
req
|
|
111
129
|
});
|
|
130
|
+
this.logger.info({
|
|
131
|
+
jobId: job.id,
|
|
132
|
+
runResult: runResults,
|
|
133
|
+
hasResult: !!runResults
|
|
134
|
+
}, 'Job run completed');
|
|
135
|
+
// Give a small delay to ensure job is fully processed
|
|
136
|
+
await new Promise((resolve)=>setTimeout(resolve, 100));
|
|
112
137
|
// Get the job result
|
|
113
138
|
const completedJob = await this.payload.findByID({
|
|
114
139
|
id: job.id,
|
|
115
140
|
collection: 'payload-jobs',
|
|
116
141
|
req
|
|
117
142
|
});
|
|
143
|
+
this.logger.info({
|
|
144
|
+
jobId: job.id,
|
|
145
|
+
totalTried: completedJob.totalTried,
|
|
146
|
+
hasError: completedJob.hasError,
|
|
147
|
+
taskStatus: completedJob.taskStatus ? Object.keys(completedJob.taskStatus) : 'null'
|
|
148
|
+
}, 'Retrieved job results');
|
|
118
149
|
const taskStatus = completedJob.taskStatus?.[completedJob.taskSlug]?.[completedJob.totalTried];
|
|
119
150
|
const isComplete = taskStatus?.complete === true;
|
|
120
151
|
const hasError = completedJob.hasError || !isComplete;
|
|
@@ -130,9 +161,34 @@ export class WorkflowExecutor {
|
|
|
130
161
|
if (!errorMessage && completedJob.error) {
|
|
131
162
|
errorMessage = completedJob.error.message || completedJob.error;
|
|
132
163
|
}
|
|
133
|
-
//
|
|
164
|
+
// Try to get error from task output if available
|
|
165
|
+
if (!errorMessage && taskStatus?.output?.error) {
|
|
166
|
+
errorMessage = taskStatus.output.error;
|
|
167
|
+
}
|
|
168
|
+
// Check if task handler returned with state='failed'
|
|
169
|
+
if (!errorMessage && taskStatus?.state === 'failed') {
|
|
170
|
+
errorMessage = 'Task handler returned a failed state';
|
|
171
|
+
// Try to get more specific error from output
|
|
172
|
+
if (taskStatus.output?.error) {
|
|
173
|
+
errorMessage = taskStatus.output.error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Check for network errors in the job data
|
|
177
|
+
if (!errorMessage && completedJob.result) {
|
|
178
|
+
const result = completedJob.result;
|
|
179
|
+
if (result.error) {
|
|
180
|
+
errorMessage = result.error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Final fallback to generic message with more detail
|
|
134
184
|
if (!errorMessage) {
|
|
135
|
-
|
|
185
|
+
const jobDetails = {
|
|
186
|
+
taskSlug,
|
|
187
|
+
hasError: completedJob.hasError,
|
|
188
|
+
taskStatus: taskStatus?.complete,
|
|
189
|
+
totalTried: completedJob.totalTried
|
|
190
|
+
};
|
|
191
|
+
errorMessage = `Task ${taskSlug} failed without detailed error information. Job details: ${JSON.stringify(jobDetails)}`;
|
|
136
192
|
}
|
|
137
193
|
}
|
|
138
194
|
const result = {
|
|
@@ -146,6 +202,27 @@ export class WorkflowExecutor {
|
|
|
146
202
|
if (result.error) {
|
|
147
203
|
context.steps[stepName].error = result.error;
|
|
148
204
|
}
|
|
205
|
+
// Independent execution tracking (not dependent on PayloadCMS task status)
|
|
206
|
+
context.steps[stepName].executionInfo = {
|
|
207
|
+
completed: true,
|
|
208
|
+
success: result.state === 'succeeded',
|
|
209
|
+
executedAt: new Date().toISOString(),
|
|
210
|
+
duration: Date.now() - (context.steps[stepName]._startTime || Date.now())
|
|
211
|
+
};
|
|
212
|
+
// For failed steps, try to extract detailed error information from the job logs
|
|
213
|
+
// This approach is more reliable than external storage and persists with the workflow
|
|
214
|
+
if (result.state === 'failed') {
|
|
215
|
+
const errorDetails = this.extractErrorDetailsFromJob(completedJob, context.steps[stepName], stepName);
|
|
216
|
+
if (errorDetails) {
|
|
217
|
+
context.steps[stepName].errorDetails = errorDetails;
|
|
218
|
+
this.logger.info({
|
|
219
|
+
stepName,
|
|
220
|
+
errorType: errorDetails.errorType,
|
|
221
|
+
duration: errorDetails.duration,
|
|
222
|
+
attempts: errorDetails.attempts
|
|
223
|
+
}, 'Extracted detailed error information for failed step');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
149
226
|
this.logger.debug({
|
|
150
227
|
context
|
|
151
228
|
}, 'Step execution context');
|
|
@@ -164,6 +241,14 @@ export class WorkflowExecutor {
|
|
|
164
241
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
165
242
|
context.steps[stepName].state = 'failed';
|
|
166
243
|
context.steps[stepName].error = errorMessage;
|
|
244
|
+
// Independent execution tracking for failed steps
|
|
245
|
+
context.steps[stepName].executionInfo = {
|
|
246
|
+
completed: true,
|
|
247
|
+
success: false,
|
|
248
|
+
executedAt: new Date().toISOString(),
|
|
249
|
+
duration: Date.now() - (context.steps[stepName]._startTime || Date.now()),
|
|
250
|
+
failureReason: errorMessage
|
|
251
|
+
};
|
|
167
252
|
this.logger.error({
|
|
168
253
|
error: errorMessage,
|
|
169
254
|
input: context.steps[stepName].input,
|
|
@@ -325,6 +410,76 @@ export class WorkflowExecutor {
|
|
|
325
410
|
return serialize(obj);
|
|
326
411
|
}
|
|
327
412
|
/**
|
|
413
|
+
* Extracts detailed error information from job logs and input
|
|
414
|
+
*/ extractErrorDetailsFromJob(job, stepContext, stepName) {
|
|
415
|
+
try {
|
|
416
|
+
// Get error information from multiple sources
|
|
417
|
+
const input = stepContext.input || {};
|
|
418
|
+
const logs = job.log || [];
|
|
419
|
+
const latestLog = logs[logs.length - 1];
|
|
420
|
+
// Extract error message from job error or log
|
|
421
|
+
const errorMessage = job.error?.message || latestLog?.error?.message || 'Unknown error';
|
|
422
|
+
// For timeout scenarios, check if it's a timeout based on duration and timeout setting
|
|
423
|
+
let errorType = this.classifyErrorType(errorMessage);
|
|
424
|
+
// Special handling for HTTP timeouts - if task failed and duration exceeds timeout, it's likely a timeout
|
|
425
|
+
if (errorType === 'unknown' && input.timeout && stepContext.executionInfo?.duration) {
|
|
426
|
+
const timeoutMs = parseInt(input.timeout) || 30000;
|
|
427
|
+
const actualDuration = stepContext.executionInfo.duration;
|
|
428
|
+
// If execution duration is close to or exceeds timeout, classify as timeout
|
|
429
|
+
if (actualDuration >= timeoutMs * 0.9) {
|
|
430
|
+
errorType = 'timeout';
|
|
431
|
+
this.logger.debug({
|
|
432
|
+
timeoutMs,
|
|
433
|
+
actualDuration,
|
|
434
|
+
stepName
|
|
435
|
+
}, 'Classified error as timeout based on duration analysis');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// Calculate duration from execution info if available
|
|
439
|
+
const duration = stepContext.executionInfo?.duration || 0;
|
|
440
|
+
// Extract attempt count from logs
|
|
441
|
+
const attempts = job.totalTried || 1;
|
|
442
|
+
return {
|
|
443
|
+
stepId: `${stepName}-${Date.now()}`,
|
|
444
|
+
errorType,
|
|
445
|
+
duration,
|
|
446
|
+
attempts,
|
|
447
|
+
finalError: errorMessage,
|
|
448
|
+
context: {
|
|
449
|
+
url: input.url,
|
|
450
|
+
method: input.method,
|
|
451
|
+
timeout: input.timeout,
|
|
452
|
+
statusCode: latestLog?.output?.status,
|
|
453
|
+
headers: input.headers
|
|
454
|
+
},
|
|
455
|
+
timestamp: new Date().toISOString()
|
|
456
|
+
};
|
|
457
|
+
} catch (error) {
|
|
458
|
+
this.logger.warn({
|
|
459
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
460
|
+
stepName
|
|
461
|
+
}, 'Failed to extract error details from job');
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Classifies error types based on error messages
|
|
467
|
+
*/ classifyErrorType(errorMessage) {
|
|
468
|
+
if (errorMessage.includes('timeout') || errorMessage.includes('ETIMEDOUT')) {
|
|
469
|
+
return 'timeout';
|
|
470
|
+
}
|
|
471
|
+
if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('getaddrinfo')) {
|
|
472
|
+
return 'dns';
|
|
473
|
+
}
|
|
474
|
+
if (errorMessage.includes('ECONNREFUSED') || errorMessage.includes('ECONNRESET')) {
|
|
475
|
+
return 'connection';
|
|
476
|
+
}
|
|
477
|
+
if (errorMessage.includes('network') || errorMessage.includes('fetch')) {
|
|
478
|
+
return 'network';
|
|
479
|
+
}
|
|
480
|
+
return 'unknown';
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
328
483
|
* Update workflow run with current context
|
|
329
484
|
*/ async updateWorkflowRunContext(workflowRunId, context, req) {
|
|
330
485
|
const serializeContext = ()=>({
|