@open-mercato/core 0.6.7-develop.6677.1.beabb7ca12 → 0.6.7-develop.6685.1.77c0a5591b
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/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/gateway_payment_operation/index.js +2 -0
- package/dist/generated/entities/gateway_payment_operation/index.js.map +2 -2
- package/dist/generated/entities/gateway_transaction/index.js +2 -0
- package/dist/generated/entities/gateway_transaction/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/payment_gateways/api/capture/route.js +1 -1
- package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
- package/dist/modules/payment_gateways/data/entities.js +7 -0
- package/dist/modules/payment_gateways/data/entities.js.map +2 -2
- package/dist/modules/payment_gateways/lib/capture-ledger.js +183 -0
- package/dist/modules/payment_gateways/lib/capture-ledger.js.map +7 -0
- package/dist/modules/payment_gateways/lib/gateway-service.js +70 -8
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/payment-operation-idempotency.js +1 -0
- package/dist/modules/payment_gateways/lib/payment-operation-idempotency.js.map +2 -2
- package/dist/modules/payment_gateways/migrations/Migration20260725101500_payment_gateways.js +16 -0
- package/dist/modules/payment_gateways/migrations/Migration20260725101500_payment_gateways.js.map +7 -0
- package/dist/modules/query_index/lib/indexer.js +12 -2
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +5 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/dist/modules/workflows/components/ActivitiesEditor.js +67 -11
- package/dist/modules/workflows/components/ActivitiesEditor.js.map +2 -2
- package/dist/modules/workflows/components/TransitionsEditor.js +205 -163
- package/dist/modules/workflows/components/TransitionsEditor.js.map +2 -2
- package/dist/modules/workflows/data/validators.js +34 -1
- package/dist/modules/workflows/data/validators.js.map +3 -3
- package/dist/modules/workflows/lib/activity-executor.js +18 -2
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/format-validation-error.js +46 -1
- package/dist/modules/workflows/lib/format-validation-error.js.map +2 -2
- package/generated/entities/gateway_payment_operation/index.ts +1 -0
- package/generated/entities/gateway_transaction/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/modules/payment_gateways/api/capture/route.ts +1 -1
- package/src/modules/payment_gateways/data/entities.ts +8 -2
- package/src/modules/payment_gateways/lib/capture-ledger.ts +260 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +83 -10
- package/src/modules/payment_gateways/lib/payment-operation-idempotency.ts +1 -0
- package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +34 -0
- package/src/modules/payment_gateways/migrations/Migration20260725101500_payment_gateways.ts +21 -0
- package/src/modules/query_index/lib/indexer.ts +25 -2
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +12 -3
- package/src/modules/workflows/components/ActivitiesEditor.tsx +96 -12
- package/src/modules/workflows/components/TransitionsEditor.tsx +75 -17
- package/src/modules/workflows/data/validators.ts +52 -1
- package/src/modules/workflows/i18n/de.json +2 -0
- package/src/modules/workflows/i18n/en.json +2 -0
- package/src/modules/workflows/i18n/es.json +2 -0
- package/src/modules/workflows/i18n/pl.json +2 -0
- package/src/modules/workflows/lib/activity-executor.ts +38 -3
- package/src/modules/workflows/lib/format-validation-error.ts +60 -0
|
@@ -26,7 +26,11 @@ interface Activity {
|
|
|
26
26
|
retryDelay?: number
|
|
27
27
|
backoffMultiplier?: number
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
// Milliseconds, matching the executor and the definition schema. This field
|
|
30
|
+
// used to be written as `timeout` (a number) while the schema typed `timeout`
|
|
31
|
+
// as an ISO 8601 string, so saving a timeout from this editor failed
|
|
32
|
+
// validation outright (#4424).
|
|
33
|
+
timeoutMs?: number
|
|
30
34
|
compensation?: Record<string, any>
|
|
31
35
|
}
|
|
32
36
|
|
|
@@ -46,8 +50,59 @@ const ACTIVITY_TYPES = [
|
|
|
46
50
|
{ value: 'WAIT', label: 'Wait' },
|
|
47
51
|
]
|
|
48
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Per-activity draft of the raw JSON config text (#4234).
|
|
55
|
+
*
|
|
56
|
+
* The config textarea used to be controlled directly by
|
|
57
|
+
* `JSON.stringify(activity.config)`, and its onChange dropped anything that
|
|
58
|
+
* did not parse. Every intermediate keystroke of a hand edit is invalid JSON,
|
|
59
|
+
* so the state never advanced and React re-rendered the previous serialized
|
|
60
|
+
* value — the field read as frozen/non-editable right after a config was
|
|
61
|
+
* pasted. Keeping the raw text locally lets the user type freely; the parsed
|
|
62
|
+
* object is propagated whenever the text is valid, and an inline error is shown
|
|
63
|
+
* while it is not.
|
|
64
|
+
*/
|
|
65
|
+
type ConfigDraft = { text: string; error: string | null }
|
|
66
|
+
|
|
67
|
+
function serializeConfig(config: Record<string, unknown> | undefined): string {
|
|
68
|
+
return JSON.stringify(config ?? {}, null, 2)
|
|
69
|
+
}
|
|
70
|
+
|
|
49
71
|
export function ActivitiesEditor({ value = [], onChange, error }: ActivitiesEditorProps) {
|
|
50
72
|
const t = useT()
|
|
73
|
+
const [configDrafts, setConfigDrafts] = React.useState<Record<number, ConfigDraft>>({})
|
|
74
|
+
|
|
75
|
+
const configTextFor = (index: number, activity: Activity): string =>
|
|
76
|
+
configDrafts[index]?.text ?? serializeConfig(activity.config)
|
|
77
|
+
|
|
78
|
+
const handleConfigTextChange = (index: number, text: string) => {
|
|
79
|
+
let parsed: Record<string, unknown> | null = null
|
|
80
|
+
let parseError: string | null = null
|
|
81
|
+
try {
|
|
82
|
+
const candidate = JSON.parse(text) as unknown
|
|
83
|
+
if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {
|
|
84
|
+
parsed = candidate as Record<string, unknown>
|
|
85
|
+
} else {
|
|
86
|
+
parseError = t('workflows.activities.configMustBeObject', 'Config must be a JSON object')
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
parseError = err instanceof Error ? err.message : t('workflows.activities.configInvalidJson', 'Invalid JSON')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setConfigDrafts((drafts) => ({ ...drafts, [index]: { text, error: parseError } }))
|
|
93
|
+
if (parsed) updateActivity(index, 'config', parsed)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Drop the raw draft once the field loses focus and its content is valid, so
|
|
97
|
+
// the textarea goes back to mirroring the canonical (re-formatted) config.
|
|
98
|
+
const handleConfigBlur = (index: number) => {
|
|
99
|
+
setConfigDrafts((drafts) => {
|
|
100
|
+
if (!drafts[index] || drafts[index].error) return drafts
|
|
101
|
+
const next = { ...drafts }
|
|
102
|
+
delete next[index]
|
|
103
|
+
return next
|
|
104
|
+
})
|
|
105
|
+
}
|
|
51
106
|
|
|
52
107
|
const addActivity = () => {
|
|
53
108
|
const newActivity: Activity = {
|
|
@@ -85,6 +140,18 @@ export function ActivitiesEditor({ value = [], onChange, error }: ActivitiesEdit
|
|
|
85
140
|
|
|
86
141
|
const removeActivity = (index: number) => {
|
|
87
142
|
onChange(value.filter((_, i) => i !== index))
|
|
143
|
+
// Keep the index-keyed drafts aligned with the re-indexed activities:
|
|
144
|
+
// drop the removed row's draft and shift every later draft down by one,
|
|
145
|
+
// otherwise a pending (invalid) draft would render over a different row.
|
|
146
|
+
setConfigDrafts((drafts) => {
|
|
147
|
+
const next: Record<number, ConfigDraft> = {}
|
|
148
|
+
for (const key of Object.keys(drafts)) {
|
|
149
|
+
const i = Number(key)
|
|
150
|
+
if (i === index) continue
|
|
151
|
+
next[i > index ? i - 1 : i] = drafts[i]
|
|
152
|
+
}
|
|
153
|
+
return next
|
|
154
|
+
})
|
|
88
155
|
}
|
|
89
156
|
|
|
90
157
|
const moveActivity = (index: number, direction: 'up' | 'down') => {
|
|
@@ -96,6 +163,18 @@ export function ActivitiesEditor({ value = [], onChange, error }: ActivitiesEdit
|
|
|
96
163
|
updated[index] = updated[newIndex]
|
|
97
164
|
updated[newIndex] = temp
|
|
98
165
|
onChange(updated)
|
|
166
|
+
// Swap the two rows' drafts too, so an in-progress edit follows its activity.
|
|
167
|
+
setConfigDrafts((drafts) => {
|
|
168
|
+
if (!(index in drafts) && !(newIndex in drafts)) return drafts
|
|
169
|
+
const next = { ...drafts }
|
|
170
|
+
const atIndex = drafts[index]
|
|
171
|
+
const atNew = drafts[newIndex]
|
|
172
|
+
if (atNew !== undefined) next[index] = atNew
|
|
173
|
+
else delete next[index]
|
|
174
|
+
if (atIndex !== undefined) next[newIndex] = atIndex
|
|
175
|
+
else delete next[newIndex]
|
|
176
|
+
return next
|
|
177
|
+
})
|
|
99
178
|
}
|
|
100
179
|
|
|
101
180
|
return (
|
|
@@ -211,8 +290,8 @@ export function ActivitiesEditor({ value = [], onChange, error }: ActivitiesEdit
|
|
|
211
290
|
<Input
|
|
212
291
|
id={`activity-${index}-timeout`}
|
|
213
292
|
type="number"
|
|
214
|
-
value={activity.
|
|
215
|
-
onChange={(e) => updateActivity(index, '
|
|
293
|
+
value={activity.timeoutMs || ''}
|
|
294
|
+
onChange={(e) => updateActivity(index, 'timeoutMs', e.target.value ? parseInt(e.target.value) : undefined)}
|
|
216
295
|
placeholder="30000"
|
|
217
296
|
className="mt-1"
|
|
218
297
|
/>
|
|
@@ -318,19 +397,24 @@ export function ActivitiesEditor({ value = [], onChange, error }: ActivitiesEdit
|
|
|
318
397
|
</Label>
|
|
319
398
|
<Textarea
|
|
320
399
|
id={`activity-${index}-config`}
|
|
321
|
-
value={
|
|
322
|
-
onChange={(e) =>
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
} catch {
|
|
327
|
-
// Invalid JSON, don't update
|
|
328
|
-
}
|
|
329
|
-
}}
|
|
400
|
+
value={configTextFor(index, activity)}
|
|
401
|
+
onChange={(e) => handleConfigTextChange(index, e.target.value)}
|
|
402
|
+
onBlur={() => handleConfigBlur(index)}
|
|
403
|
+
aria-invalid={configDrafts[index]?.error ? true : undefined}
|
|
404
|
+
aria-describedby={configDrafts[index]?.error ? `activity-${index}-config-error` : undefined}
|
|
330
405
|
placeholder='{"key": "value"}'
|
|
331
406
|
rows={3}
|
|
332
407
|
className="mt-1 font-mono text-xs"
|
|
333
408
|
/>
|
|
409
|
+
{configDrafts[index]?.error ? (
|
|
410
|
+
<p
|
|
411
|
+
id={`activity-${index}-config-error`}
|
|
412
|
+
className="mt-1 text-xs text-status-error-text"
|
|
413
|
+
role="alert"
|
|
414
|
+
>
|
|
415
|
+
{configDrafts[index]?.error}
|
|
416
|
+
</p>
|
|
417
|
+
) : null}
|
|
334
418
|
</div>
|
|
335
419
|
)}
|
|
336
420
|
</div>
|
|
@@ -26,7 +26,11 @@ interface Activity {
|
|
|
26
26
|
retryDelay?: number
|
|
27
27
|
backoffMultiplier?: number
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
// Milliseconds, matching the executor and the definition schema. This field
|
|
30
|
+
// used to be written as `timeout` (a number) while the schema typed `timeout`
|
|
31
|
+
// as an ISO 8601 string, so saving a timeout from this editor failed
|
|
32
|
+
// validation outright (#4424).
|
|
33
|
+
timeoutMs?: number
|
|
30
34
|
compensation?: Record<string, any>
|
|
31
35
|
}
|
|
32
36
|
|
|
@@ -66,8 +70,53 @@ const ACTIVITY_TYPES = [
|
|
|
66
70
|
{ value: 'WAIT', label: 'Wait' },
|
|
67
71
|
]
|
|
68
72
|
|
|
73
|
+
type ConfigDraft = { text: string; error: string | null }
|
|
74
|
+
|
|
69
75
|
export function TransitionsEditor({ value = [], onChange, steps = [], error }: TransitionsEditorProps) {
|
|
70
76
|
const t = useT()
|
|
77
|
+
const [configDrafts, setConfigDrafts] = React.useState<Record<string, ConfigDraft>>({})
|
|
78
|
+
|
|
79
|
+
const configDraftKey = (transition: Transition, activity: Activity) =>
|
|
80
|
+
`${transition.transitionId}:${activity.activityId}`
|
|
81
|
+
|
|
82
|
+
const handleConfigTextChange = (
|
|
83
|
+
transitionIndex: number,
|
|
84
|
+
activityIndex: number,
|
|
85
|
+
text: string,
|
|
86
|
+
) => {
|
|
87
|
+
const transition = value[transitionIndex]
|
|
88
|
+
const activity = transition.activities?.[activityIndex]
|
|
89
|
+
if (!activity) return
|
|
90
|
+
|
|
91
|
+
let parsed: Record<string, unknown> | null = null
|
|
92
|
+
let parseError: string | null = null
|
|
93
|
+
try {
|
|
94
|
+
const candidate = JSON.parse(text) as unknown
|
|
95
|
+
if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {
|
|
96
|
+
parsed = candidate as Record<string, unknown>
|
|
97
|
+
} else {
|
|
98
|
+
parseError = t('workflows.activities.configMustBeObject', 'Config must be a JSON object')
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
parseError = err instanceof Error
|
|
102
|
+
? err.message
|
|
103
|
+
: t('workflows.activities.configInvalidJson', 'Invalid JSON')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const key = configDraftKey(transition, activity)
|
|
107
|
+
setConfigDrafts((drafts) => ({ ...drafts, [key]: { text, error: parseError } }))
|
|
108
|
+
if (parsed) updateActivity(transitionIndex, activityIndex, 'config', parsed)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const handleConfigBlur = (transition: Transition, activity: Activity) => {
|
|
112
|
+
const key = configDraftKey(transition, activity)
|
|
113
|
+
setConfigDrafts((drafts) => {
|
|
114
|
+
if (!drafts[key] || drafts[key].error) return drafts
|
|
115
|
+
const next = { ...drafts }
|
|
116
|
+
delete next[key]
|
|
117
|
+
return next
|
|
118
|
+
})
|
|
119
|
+
}
|
|
71
120
|
|
|
72
121
|
const addTransition = () => {
|
|
73
122
|
const newTransition: Transition = {
|
|
@@ -370,9 +419,12 @@ export function TransitionsEditor({ value = [], onChange, steps = [], error }: T
|
|
|
370
419
|
)}
|
|
371
420
|
|
|
372
421
|
<div className="space-y-2">
|
|
373
|
-
{(transition.activities || []).map((activity, activityIndex) =>
|
|
374
|
-
|
|
375
|
-
|
|
422
|
+
{(transition.activities || []).map((activity, activityIndex) => {
|
|
423
|
+
const draftKey = configDraftKey(transition, activity)
|
|
424
|
+
const configDraft = configDrafts[draftKey]
|
|
425
|
+
return (
|
|
426
|
+
<div key={draftKey} className="p-3 border rounded-md bg-muted shadow-sm border-l-4 border-l-green-500">
|
|
427
|
+
<div className="space-y-2">
|
|
376
428
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
|
377
429
|
<div className="flex-1 grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
378
430
|
<div>
|
|
@@ -461,8 +513,8 @@ export function TransitionsEditor({ value = [], onChange, steps = [], error }: T
|
|
|
461
513
|
<Input
|
|
462
514
|
id={`activity-${index}-${activityIndex}-timeout`}
|
|
463
515
|
type="number"
|
|
464
|
-
value={activity.
|
|
465
|
-
onChange={(e) => updateActivity(index, activityIndex, '
|
|
516
|
+
value={activity.timeoutMs || ''}
|
|
517
|
+
onChange={(e) => updateActivity(index, activityIndex, 'timeoutMs', e.target.value ? parseInt(e.target.value) : undefined)}
|
|
466
518
|
placeholder="30000"
|
|
467
519
|
className="mt-1 text-xs h-8"
|
|
468
520
|
/>
|
|
@@ -529,23 +581,29 @@ export function TransitionsEditor({ value = [], onChange, steps = [], error }: T
|
|
|
529
581
|
</Label>
|
|
530
582
|
<Textarea
|
|
531
583
|
id={`activity-${index}-${activityIndex}-config`}
|
|
532
|
-
value={JSON.stringify(activity.config || {}, null, 2)}
|
|
533
|
-
onChange={(e) =>
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
} catch {
|
|
538
|
-
// Invalid JSON, don't update
|
|
539
|
-
}
|
|
540
|
-
}}
|
|
584
|
+
value={configDraft?.text ?? JSON.stringify(activity.config || {}, null, 2)}
|
|
585
|
+
onChange={(e) => handleConfigTextChange(index, activityIndex, e.target.value)}
|
|
586
|
+
onBlur={() => handleConfigBlur(transition, activity)}
|
|
587
|
+
aria-invalid={configDraft?.error ? true : undefined}
|
|
588
|
+
aria-describedby={configDraft?.error ? `activity-${index}-${activityIndex}-config-error` : undefined}
|
|
541
589
|
placeholder='{"key": "value"}'
|
|
542
590
|
rows={2}
|
|
543
591
|
className="mt-1 font-mono text-xs"
|
|
544
592
|
/>
|
|
593
|
+
{configDraft?.error ? (
|
|
594
|
+
<p
|
|
595
|
+
id={`activity-${index}-${activityIndex}-config-error`}
|
|
596
|
+
className="mt-1 text-xs text-status-error-text"
|
|
597
|
+
role="alert"
|
|
598
|
+
>
|
|
599
|
+
{configDraft.error}
|
|
600
|
+
</p>
|
|
601
|
+
) : null}
|
|
602
|
+
</div>
|
|
545
603
|
</div>
|
|
546
604
|
</div>
|
|
547
|
-
|
|
548
|
-
)
|
|
605
|
+
)
|
|
606
|
+
})}
|
|
549
607
|
</div>
|
|
550
608
|
</div>
|
|
551
609
|
</div>
|
|
@@ -202,6 +202,20 @@ export const activityRetryPolicySchema = z.object({
|
|
|
202
202
|
maxIntervalMs: z.number().int().min(0),
|
|
203
203
|
})
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Config fields each activity executor requires to run (see
|
|
207
|
+
* `lib/activity-executor.ts`). WAIT is handled separately below because it
|
|
208
|
+
* takes "duration" OR "until" rather than a fixed key set.
|
|
209
|
+
*/
|
|
210
|
+
const REQUIRED_ACTIVITY_CONFIG_KEYS: Partial<Record<ActivityType, readonly string[]>> = {
|
|
211
|
+
SEND_EMAIL: ['to', 'subject'],
|
|
212
|
+
CALL_API: ['endpoint'],
|
|
213
|
+
CALL_WEBHOOK: ['url'],
|
|
214
|
+
UPDATE_ENTITY: ['commandId', 'input'],
|
|
215
|
+
EMIT_EVENT: ['eventName'],
|
|
216
|
+
EXECUTE_FUNCTION: ['functionName'],
|
|
217
|
+
}
|
|
218
|
+
|
|
205
219
|
// Activity definition (embedded in transitions)
|
|
206
220
|
export const activityDefinitionSchema = z.object({
|
|
207
221
|
activityId: z.string().min(1).max(100).regex(/^[a-z0-9_-]+$/, 'Activity ID must contain only lowercase letters, numbers, hyphens, and underscores'),
|
|
@@ -210,12 +224,49 @@ export const activityDefinitionSchema = z.object({
|
|
|
210
224
|
config: z.record(z.string(), z.any()),
|
|
211
225
|
async: z.boolean().default(false).optional(), // For Phase 8.3
|
|
212
226
|
retryPolicy: activityRetryPolicySchema.optional(),
|
|
213
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Per-activity timeout in milliseconds. This is what the editor writes and
|
|
229
|
+
* what `executeActivity` reads; it was missing from the schema, so
|
|
230
|
+
* `z.object()` stripped it on save and UI-configured timeouts silently did
|
|
231
|
+
* nothing (#4424).
|
|
232
|
+
*/
|
|
233
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
234
|
+
/**
|
|
235
|
+
* @deprecated Use `timeoutMs`. Accepted for definitions already stored with
|
|
236
|
+
* an ISO 8601 duration string; the executor normalizes it to milliseconds.
|
|
237
|
+
*/
|
|
238
|
+
timeout: z.string().optional(),
|
|
214
239
|
compensation: z.object({
|
|
215
240
|
activityId: z.string().min(1), // ID of compensation activity
|
|
216
241
|
automatic: z.boolean().default(true).optional() // Auto-trigger on failure
|
|
217
242
|
}).optional(), // Compensation configuration (Phase 8.2)
|
|
218
243
|
}).superRefine((activity, ctx) => {
|
|
244
|
+
// Config keys each activity executor requires at runtime. Without this, an
|
|
245
|
+
// activity missing e.g. CALL_API's `endpoint` saved cleanly from the visual
|
|
246
|
+
// editor and only failed once an instance ran it — the "edit silently
|
|
247
|
+
// disappeared / nothing told me why" report in #4232 (and the class of bug
|
|
248
|
+
// #4322 was an instance of). Validating here surfaces the exact missing field
|
|
249
|
+
// at edit time, since both the editor's save path and the API route parse
|
|
250
|
+
// through this schema.
|
|
251
|
+
const requiredConfigKeys = REQUIRED_ACTIVITY_CONFIG_KEYS[activity.activityType]
|
|
252
|
+
if (requiredConfigKeys) {
|
|
253
|
+
const config = activity.config || {}
|
|
254
|
+
for (const key of requiredConfigKeys) {
|
|
255
|
+
const value = config[key]
|
|
256
|
+
const isEmpty =
|
|
257
|
+
value == null ||
|
|
258
|
+
(typeof value === 'string' && value.trim() === '') ||
|
|
259
|
+
(key === 'input' && typeof value !== 'object')
|
|
260
|
+
if (isEmpty) {
|
|
261
|
+
ctx.addIssue({
|
|
262
|
+
code: 'custom',
|
|
263
|
+
path: ['config', key],
|
|
264
|
+
message: `${activity.activityType} activity requires "${key}"`,
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
219
270
|
if (activity.activityType !== 'WAIT') return
|
|
220
271
|
const config = activity.config || {}
|
|
221
272
|
const hasDuration = config.duration != null && config.duration !== ''
|
|
@@ -80,6 +80,8 @@
|
|
|
80
80
|
"workflows.activities.async": "Asynchron",
|
|
81
81
|
"workflows.activities.compensation": "Kompensation",
|
|
82
82
|
"workflows.activities.config": "Konfiguration",
|
|
83
|
+
"workflows.activities.configInvalidJson": "Ungültiges JSON",
|
|
84
|
+
"workflows.activities.configMustBeObject": "Konfiguration muss ein JSON-Objekt sein",
|
|
83
85
|
"workflows.activities.deleteActivity": "Aktivität löschen",
|
|
84
86
|
"workflows.activities.editActivity": "Aktivität bearbeiten",
|
|
85
87
|
"workflows.activities.plural": "Aktivitäten",
|
|
@@ -80,6 +80,8 @@
|
|
|
80
80
|
"workflows.activities.async": "Asynchronous",
|
|
81
81
|
"workflows.activities.compensation": "Compensation",
|
|
82
82
|
"workflows.activities.config": "Configuration",
|
|
83
|
+
"workflows.activities.configInvalidJson": "Invalid JSON",
|
|
84
|
+
"workflows.activities.configMustBeObject": "Config must be a JSON object",
|
|
83
85
|
"workflows.activities.deleteActivity": "Delete Activity",
|
|
84
86
|
"workflows.activities.editActivity": "Edit Activity",
|
|
85
87
|
"workflows.activities.plural": "Activities",
|
|
@@ -80,6 +80,8 @@
|
|
|
80
80
|
"workflows.activities.async": "Asincrona",
|
|
81
81
|
"workflows.activities.compensation": "Compensacion",
|
|
82
82
|
"workflows.activities.config": "Configuracion",
|
|
83
|
+
"workflows.activities.configInvalidJson": "JSON no válido",
|
|
84
|
+
"workflows.activities.configMustBeObject": "La configuración debe ser un objeto JSON",
|
|
83
85
|
"workflows.activities.deleteActivity": "Eliminar actividad",
|
|
84
86
|
"workflows.activities.editActivity": "Editar actividad",
|
|
85
87
|
"workflows.activities.plural": "Actividades",
|
|
@@ -80,6 +80,8 @@
|
|
|
80
80
|
"workflows.activities.async": "Asynchroniczne",
|
|
81
81
|
"workflows.activities.compensation": "Kompensacja",
|
|
82
82
|
"workflows.activities.config": "Konfiguracja",
|
|
83
|
+
"workflows.activities.configInvalidJson": "Nieprawidłowy JSON",
|
|
84
|
+
"workflows.activities.configMustBeObject": "Konfiguracja musi być obiektem JSON",
|
|
83
85
|
"workflows.activities.deleteActivity": "Usuń Działanie",
|
|
84
86
|
"workflows.activities.editActivity": "Edytuj Działanie",
|
|
85
87
|
"workflows.activities.plural": "Działania",
|
|
@@ -107,9 +107,42 @@ export interface ActivityDefinition {
|
|
|
107
107
|
async?: boolean // Flag to execute activity asynchronously via queue
|
|
108
108
|
retryPolicy?: RetryPolicy
|
|
109
109
|
timeoutMs?: number
|
|
110
|
+
/**
|
|
111
|
+
* @deprecated Use `timeoutMs`. Legacy ISO 8601 duration string accepted by
|
|
112
|
+
* the definition schema before #4424; normalized by `resolveActivityTimeoutMs`.
|
|
113
|
+
*/
|
|
114
|
+
timeout?: string
|
|
110
115
|
compensate?: boolean // Flag to execute compensation on failure
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Effective timeout for an activity, in milliseconds.
|
|
120
|
+
*
|
|
121
|
+
* The editor and this executor both speak `timeoutMs`, but the definition
|
|
122
|
+
* schema historically accepted only an ISO 8601 `timeout` string — so stored
|
|
123
|
+
* definitions can carry either. Prefer `timeoutMs`; fall back to parsing
|
|
124
|
+
* `timeout`, ignoring a malformed value rather than throwing mid-execution
|
|
125
|
+
* (an unparseable timeout must not fail an activity that would otherwise
|
|
126
|
+
* succeed). Returns undefined when no usable timeout is configured (#4424).
|
|
127
|
+
*/
|
|
128
|
+
export function resolveActivityTimeoutMs(activity: {
|
|
129
|
+
timeoutMs?: number
|
|
130
|
+
timeout?: string
|
|
131
|
+
}): number | undefined {
|
|
132
|
+
if (typeof activity.timeoutMs === 'number' && activity.timeoutMs > 0) {
|
|
133
|
+
return activity.timeoutMs
|
|
134
|
+
}
|
|
135
|
+
if (typeof activity.timeout === 'string' && activity.timeout.trim().length > 0) {
|
|
136
|
+
try {
|
|
137
|
+
const parsed = parseDuration(activity.timeout.trim())
|
|
138
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed
|
|
139
|
+
} catch {
|
|
140
|
+
return undefined
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return undefined
|
|
144
|
+
}
|
|
145
|
+
|
|
113
146
|
export interface RetryPolicy {
|
|
114
147
|
maxAttempts: number
|
|
115
148
|
initialIntervalMs: number
|
|
@@ -332,11 +365,13 @@ export async function executeActivity(
|
|
|
332
365
|
try {
|
|
333
366
|
const startTime = Date.now()
|
|
334
367
|
|
|
335
|
-
// Execute with timeout if specified
|
|
336
|
-
|
|
368
|
+
// Execute with timeout if specified (timeoutMs, or a legacy ISO 8601
|
|
369
|
+
// `timeout` string normalized to ms — see resolveActivityTimeoutMs).
|
|
370
|
+
const timeoutMs = resolveActivityTimeoutMs(activity)
|
|
371
|
+
const result = timeoutMs
|
|
337
372
|
? await executeWithTimeout(
|
|
338
373
|
() => executeActivityByType(em, container, activity, context),
|
|
339
|
-
|
|
374
|
+
timeoutMs
|
|
340
375
|
)
|
|
341
376
|
: await executeActivityByType(em, container, activity, context)
|
|
342
377
|
|
|
@@ -8,6 +8,66 @@ type ApiErrorBody = {
|
|
|
8
8
|
details?: ZodIssueLite[]
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// Collection segments in a definition path, mapped to the label an operator
|
|
12
|
+
// recognizes from the editor. Indexes are rendered 1-based.
|
|
13
|
+
const COLLECTION_LABELS: Record<string, string> = {
|
|
14
|
+
steps: 'step',
|
|
15
|
+
transitions: 'transition',
|
|
16
|
+
activities: 'activity',
|
|
17
|
+
triggers: 'trigger',
|
|
18
|
+
preConditions: 'pre-condition',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Turn a raw Zod path into something an operator can act on:
|
|
23
|
+
* `steps.2.activities.0.config.endpoint` → `step 3 › activity 1 › config.endpoint`.
|
|
24
|
+
*
|
|
25
|
+
* The raw dotted path reads as internal JSON and gives no hint which node to
|
|
26
|
+
* open in the visual editor (#4232).
|
|
27
|
+
*/
|
|
28
|
+
export function humanizeDefinitionIssuePath(path: ReadonlyArray<PropertyKey>): string {
|
|
29
|
+
if (!path.length) return 'definition'
|
|
30
|
+
const parts: string[] = []
|
|
31
|
+
let pending: string | null = null
|
|
32
|
+
|
|
33
|
+
for (const rawSegment of path) {
|
|
34
|
+
// Zod types issue paths as PropertyKey; symbols can't appear in JSON data
|
|
35
|
+
// but narrow defensively rather than casting.
|
|
36
|
+
const segment = typeof rawSegment === 'symbol' ? rawSegment.toString() : rawSegment
|
|
37
|
+
if (typeof segment === 'number') {
|
|
38
|
+
parts.push(pending ? `${pending} ${segment + 1}` : `#${segment + 1}`)
|
|
39
|
+
pending = null
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
const label = COLLECTION_LABELS[segment]
|
|
43
|
+
if (label) {
|
|
44
|
+
if (pending) parts.push(pending)
|
|
45
|
+
pending = label
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
if (pending) {
|
|
49
|
+
parts.push(pending)
|
|
50
|
+
pending = null
|
|
51
|
+
}
|
|
52
|
+
parts.push(segment)
|
|
53
|
+
}
|
|
54
|
+
if (pending) parts.push(pending)
|
|
55
|
+
|
|
56
|
+
const [head, ...rest] = parts
|
|
57
|
+
if (!rest.length) return head
|
|
58
|
+
// Keep trailing field names dotted (config.endpoint), collections chevroned.
|
|
59
|
+
const grouped: string[] = [head]
|
|
60
|
+
for (const part of rest) {
|
|
61
|
+
const isIndexed = /\s\d+$/.test(part) || part.startsWith('#')
|
|
62
|
+
if (!isIndexed && grouped.length > 0 && !/\s\d+$/.test(grouped[grouped.length - 1]) && !grouped[grouped.length - 1].startsWith('#')) {
|
|
63
|
+
grouped[grouped.length - 1] = `${grouped[grouped.length - 1]}.${part}`
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
grouped.push(part)
|
|
67
|
+
}
|
|
68
|
+
return grouped.join(' › ')
|
|
69
|
+
}
|
|
70
|
+
|
|
11
71
|
/**
|
|
12
72
|
* Format an API validation error body into a user-readable message.
|
|
13
73
|
*
|