@open-mercato/core 0.6.3-develop.3876.1.d40fe4ec2d → 0.6.3-develop.3881.1.0b590ac4eb
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/modules/attachments/api/file/[id]/route.js +7 -2
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +7 -4
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/audit_logs/services/accessLogService.js +127 -8
- package/dist/modules/audit_logs/services/accessLogService.js.map +2 -2
- package/dist/modules/auth/di.js +17 -3
- package/dist/modules/auth/di.js.map +2 -2
- package/dist/modules/auth/services/rbacDefaultCache.js +110 -0
- package/dist/modules/auth/services/rbacDefaultCache.js.map +7 -0
- package/dist/modules/currencies/api/currencies/route.js +3 -4
- package/dist/modules/currencies/api/currencies/route.js.map +2 -2
- package/dist/modules/currencies/api/exchange-rates/route.js +3 -4
- package/dist/modules/currencies/api/exchange-rates/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +26 -24
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +26 -0
- package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +7 -0
- package/dist/modules/directory/utils/organizationScope.js +85 -0
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/[id]/page.js +2 -1
- package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/create/page.js +4 -2
- package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +20 -3
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/dist/modules/workflows/components/ActivitiesEditor.js +34 -1
- package/dist/modules/workflows/components/ActivitiesEditor.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialog.js +153 -17
- package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
- package/dist/modules/workflows/components/StepsEditor.js +31 -0
- package/dist/modules/workflows/components/StepsEditor.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowGraph.js +3 -2
- package/dist/modules/workflows/components/WorkflowGraph.js.map +2 -2
- package/dist/modules/workflows/components/nodes/WaitForTimerNode.js +54 -0
- package/dist/modules/workflows/components/nodes/WaitForTimerNode.js.map +7 -0
- package/dist/modules/workflows/components/nodes/index.js +3 -1
- package/dist/modules/workflows/components/nodes/index.js.map +2 -2
- package/dist/modules/workflows/data/validators.js +117 -0
- package/dist/modules/workflows/data/validators.js.map +2 -2
- package/dist/modules/workflows/di.js +5 -1
- package/dist/modules/workflows/di.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +42 -1
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/activity-queue-types.js.map +2 -2
- package/dist/modules/workflows/lib/activity-worker-handler.js +24 -0
- package/dist/modules/workflows/lib/activity-worker-handler.js.map +2 -2
- package/dist/modules/workflows/lib/duration.js +32 -0
- package/dist/modules/workflows/lib/duration.js.map +7 -0
- package/dist/modules/workflows/lib/event-logger.js +1 -0
- package/dist/modules/workflows/lib/event-logger.js.map +2 -2
- package/dist/modules/workflows/lib/format-validation-error.js +12 -0
- package/dist/modules/workflows/lib/format-validation-error.js.map +7 -0
- package/dist/modules/workflows/lib/graph-utils.js +6 -3
- package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
- package/dist/modules/workflows/lib/node-type-icons.js +9 -5
- package/dist/modules/workflows/lib/node-type-icons.js.map +2 -2
- package/dist/modules/workflows/lib/signal-handler.js +55 -23
- package/dist/modules/workflows/lib/signal-handler.js.map +2 -2
- package/dist/modules/workflows/lib/step-handler.js +79 -29
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- package/dist/modules/workflows/lib/timer-handler.js +159 -0
- package/dist/modules/workflows/lib/timer-handler.js.map +7 -0
- package/dist/modules/workflows/lib/workflow-executor.js +1 -1
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/dist/modules/workflows/workers/workflow-activities.worker.js +20 -4
- package/dist/modules/workflows/workers/workflow-activities.worker.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/api/file/[id]/route.ts +7 -2
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -4
- package/src/modules/audit_logs/services/accessLogService.ts +179 -15
- package/src/modules/auth/di.ts +26 -3
- package/src/modules/auth/services/rbacDefaultCache.ts +145 -0
- package/src/modules/currencies/api/currencies/route.ts +3 -4
- package/src/modules/currencies/api/exchange-rates/route.ts +3 -4
- package/src/modules/customers/api/people/route.ts +27 -25
- package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +39 -0
- package/src/modules/directory/utils/organizationScope.ts +121 -0
- package/src/modules/workflows/backend/definitions/[id]/page.tsx +3 -2
- package/src/modules/workflows/backend/definitions/create/page.tsx +4 -2
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +18 -1
- package/src/modules/workflows/components/ActivitiesEditor.tsx +40 -0
- package/src/modules/workflows/components/NodeEditDialog.tsx +218 -30
- package/src/modules/workflows/components/StepsEditor.tsx +36 -0
- package/src/modules/workflows/components/WorkflowGraph.tsx +2 -1
- package/src/modules/workflows/components/nodes/WaitForTimerNode.tsx +70 -0
- package/src/modules/workflows/components/nodes/index.ts +3 -0
- package/src/modules/workflows/data/validators.ts +121 -0
- package/src/modules/workflows/di.ts +4 -0
- package/src/modules/workflows/i18n/de.json +10 -1
- package/src/modules/workflows/i18n/en.json +10 -1
- package/src/modules/workflows/i18n/es.json +10 -1
- package/src/modules/workflows/i18n/pl.json +10 -1
- package/src/modules/workflows/lib/activity-executor.ts +86 -2
- package/src/modules/workflows/lib/activity-queue-types.ts +18 -11
- package/src/modules/workflows/lib/activity-worker-handler.ts +29 -0
- package/src/modules/workflows/lib/duration.ts +51 -0
- package/src/modules/workflows/lib/event-logger.ts +1 -0
- package/src/modules/workflows/lib/format-validation-error.ts +30 -0
- package/src/modules/workflows/lib/graph-utils.ts +3 -0
- package/src/modules/workflows/lib/node-type-icons.ts +6 -2
- package/src/modules/workflows/lib/signal-handler.ts +62 -24
- package/src/modules/workflows/lib/step-handler.ts +107 -50
- package/src/modules/workflows/lib/timer-handler.ts +213 -0
- package/src/modules/workflows/lib/workflow-executor.ts +1 -1
- package/src/modules/workflows/workers/workflow-activities.worker.ts +33 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { CircleDot, CircleStop, User, Zap, Workflow, Clock, LucideIcon } from 'lucide-react'
|
|
1
|
+
import { CircleDot, CircleStop, User, Zap, Workflow, Clock, Timer, LucideIcon } from 'lucide-react'
|
|
2
2
|
|
|
3
|
-
export type NodeType = 'start' | 'end' | 'userTask' | 'automated' | 'subWorkflow' | 'waitForSignal'
|
|
3
|
+
export type NodeType = 'start' | 'end' | 'userTask' | 'automated' | 'subWorkflow' | 'waitForSignal' | 'waitForTimer'
|
|
4
4
|
|
|
5
5
|
export const NODE_TYPE_ICONS: Record<NodeType, LucideIcon> = {
|
|
6
6
|
start: CircleDot,
|
|
@@ -9,6 +9,7 @@ export const NODE_TYPE_ICONS: Record<NodeType, LucideIcon> = {
|
|
|
9
9
|
automated: Zap,
|
|
10
10
|
subWorkflow: Workflow,
|
|
11
11
|
waitForSignal: Clock,
|
|
12
|
+
waitForTimer: Timer,
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export const NODE_TYPE_COLORS: Record<NodeType, string> = {
|
|
@@ -18,6 +19,7 @@ export const NODE_TYPE_COLORS: Record<NodeType, string> = {
|
|
|
18
19
|
automated: 'text-amber-500',
|
|
19
20
|
subWorkflow: 'text-purple-500',
|
|
20
21
|
waitForSignal: 'text-purple-500',
|
|
22
|
+
waitForTimer: 'text-cyan-500',
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export const NODE_TYPE_LABELS: Record<NodeType, { title: string; description: string }> = {
|
|
@@ -27,6 +29,7 @@ export const NODE_TYPE_LABELS: Record<NodeType, { title: string; description: st
|
|
|
27
29
|
automated: { title: 'AUTOMATED', description: 'System task' },
|
|
28
30
|
subWorkflow: { title: 'SUB-WORKFLOW', description: 'Invoke workflow' },
|
|
29
31
|
waitForSignal: { title: 'WAIT FOR SIGNAL', description: 'Pause for external event' },
|
|
32
|
+
waitForTimer: { title: 'WAIT FOR TIMER', description: 'Pause for a duration' },
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
const STEP_TYPE_TO_NODE_TYPE: Record<string, NodeType> = {
|
|
@@ -36,6 +39,7 @@ const STEP_TYPE_TO_NODE_TYPE: Record<string, NodeType> = {
|
|
|
36
39
|
AUTOMATED: 'automated',
|
|
37
40
|
SUB_WORKFLOW: 'subWorkflow',
|
|
38
41
|
WAIT_FOR_SIGNAL: 'waitForSignal',
|
|
42
|
+
WAIT_FOR_TIMER: 'waitForTimer',
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export function stepTypeToNodeType(stepType: string): NodeType {
|
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { EntityManager } from '@mikro-orm/core'
|
|
8
|
+
import type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'
|
|
8
9
|
import type { AwilixContainer } from 'awilix'
|
|
10
|
+
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
9
11
|
import { WorkflowInstance, WorkflowDefinition, StepInstance } from '../data/entities'
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import * as
|
|
13
|
-
import * as
|
|
12
|
+
import type * as eventLoggerModule from './event-logger'
|
|
13
|
+
import type * as stepHandlerModule from './step-handler'
|
|
14
|
+
import type * as transitionHandlerModule from './transition-handler'
|
|
15
|
+
import type * as workflowExecutorModule from './workflow-executor'
|
|
14
16
|
|
|
15
17
|
export interface SendSignalOptions {
|
|
16
18
|
/**
|
|
@@ -61,12 +63,23 @@ export async function sendSignal(
|
|
|
61
63
|
): Promise<void> {
|
|
62
64
|
const { instanceId, signalName, payload, userId, tenantId, organizationId } = options
|
|
63
65
|
|
|
66
|
+
const eventLogger = container.resolve<typeof eventLoggerModule>('eventLogger')
|
|
67
|
+
const stepHandler = container.resolve<typeof stepHandlerModule>('stepHandler')
|
|
68
|
+
const transitionHandler = container.resolve<typeof transitionHandlerModule>('transitionHandler')
|
|
69
|
+
const workflowExecutor = container.resolve<typeof workflowExecutorModule>('workflowExecutor')
|
|
70
|
+
|
|
64
71
|
// Fetch workflow instance
|
|
65
|
-
const instance = await
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
const instance = await findOneWithDecryption(
|
|
73
|
+
em as PostgreSqlEntityManager,
|
|
74
|
+
WorkflowInstance,
|
|
75
|
+
{
|
|
76
|
+
id: instanceId,
|
|
77
|
+
tenantId,
|
|
78
|
+
organizationId,
|
|
79
|
+
},
|
|
80
|
+
undefined,
|
|
81
|
+
{ tenantId, organizationId },
|
|
82
|
+
)
|
|
70
83
|
|
|
71
84
|
if (!instance) {
|
|
72
85
|
throw new SignalError(
|
|
@@ -85,8 +98,19 @@ export async function sendSignal(
|
|
|
85
98
|
)
|
|
86
99
|
}
|
|
87
100
|
|
|
88
|
-
// Load workflow definition to check current step
|
|
89
|
-
const definition = await
|
|
101
|
+
// Load workflow definition with tenant/org scope to check current step
|
|
102
|
+
const definition = await findOneWithDecryption(
|
|
103
|
+
em as PostgreSqlEntityManager,
|
|
104
|
+
WorkflowDefinition,
|
|
105
|
+
{
|
|
106
|
+
id: instance.definitionId,
|
|
107
|
+
tenantId: instance.tenantId,
|
|
108
|
+
organizationId: instance.organizationId,
|
|
109
|
+
deletedAt: null,
|
|
110
|
+
},
|
|
111
|
+
undefined,
|
|
112
|
+
{ tenantId: instance.tenantId, organizationId: instance.organizationId },
|
|
113
|
+
)
|
|
90
114
|
if (!definition) {
|
|
91
115
|
throw new SignalError(
|
|
92
116
|
'Workflow definition not found',
|
|
@@ -133,7 +157,7 @@ export async function sendSignal(
|
|
|
133
157
|
instance.updatedAt = now
|
|
134
158
|
|
|
135
159
|
// Log signal received event
|
|
136
|
-
await logWorkflowEvent(em, {
|
|
160
|
+
await eventLogger.logWorkflowEvent(em, {
|
|
137
161
|
workflowInstanceId: instance.id,
|
|
138
162
|
eventType: 'SIGNAL_RECEIVED',
|
|
139
163
|
eventData: {
|
|
@@ -146,11 +170,19 @@ export async function sendSignal(
|
|
|
146
170
|
})
|
|
147
171
|
|
|
148
172
|
// Find active step instance and exit it
|
|
149
|
-
const stepInstance = await
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
173
|
+
const stepInstance = await findOneWithDecryption(
|
|
174
|
+
em as PostgreSqlEntityManager,
|
|
175
|
+
StepInstance,
|
|
176
|
+
{
|
|
177
|
+
workflowInstanceId: instance.id,
|
|
178
|
+
stepId: instance.currentStepId,
|
|
179
|
+
status: 'ACTIVE',
|
|
180
|
+
tenantId: instance.tenantId,
|
|
181
|
+
organizationId: instance.organizationId,
|
|
182
|
+
},
|
|
183
|
+
undefined,
|
|
184
|
+
{ tenantId: instance.tenantId, organizationId: instance.organizationId },
|
|
185
|
+
)
|
|
154
186
|
|
|
155
187
|
if (stepInstance) {
|
|
156
188
|
await stepHandler.exitStep(em, stepInstance, {
|
|
@@ -212,7 +244,7 @@ export async function sendSignal(
|
|
|
212
244
|
}
|
|
213
245
|
|
|
214
246
|
// Resume workflow execution
|
|
215
|
-
await executeWorkflow(em, container, instance.id, { userId })
|
|
247
|
+
await workflowExecutor.executeWorkflow(em, container, instance.id, { userId })
|
|
216
248
|
}
|
|
217
249
|
|
|
218
250
|
/**
|
|
@@ -226,12 +258,18 @@ export async function sendSignalByCorrelationKey(
|
|
|
226
258
|
const { correlationKey, signalName, payload, userId, tenantId, organizationId } = options
|
|
227
259
|
|
|
228
260
|
// Find all paused instances with this correlation key
|
|
229
|
-
const instances = await
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
261
|
+
const instances = await findWithDecryption(
|
|
262
|
+
em as PostgreSqlEntityManager,
|
|
263
|
+
WorkflowInstance,
|
|
264
|
+
{
|
|
265
|
+
correlationKey,
|
|
266
|
+
status: 'PAUSED',
|
|
267
|
+
tenantId,
|
|
268
|
+
organizationId,
|
|
269
|
+
},
|
|
270
|
+
undefined,
|
|
271
|
+
{ tenantId, organizationId },
|
|
272
|
+
)
|
|
235
273
|
|
|
236
274
|
let signalsProcessed = 0
|
|
237
275
|
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
type StepInstanceStatus,
|
|
20
20
|
type WorkflowStepType,
|
|
21
21
|
} from '../data/entities'
|
|
22
|
+
import { parseDuration } from './duration'
|
|
23
|
+
import { logWorkflowEvent } from './event-logger'
|
|
22
24
|
|
|
23
25
|
// ============================================================================
|
|
24
26
|
// Types and Interfaces
|
|
@@ -333,9 +335,11 @@ async function executeStepByType(
|
|
|
333
335
|
case 'WAIT_FOR_SIGNAL':
|
|
334
336
|
return await handleWaitForSignalStep(em, instance, stepInstance, stepDef, context)
|
|
335
337
|
|
|
338
|
+
case 'WAIT_FOR_TIMER':
|
|
339
|
+
return await handleWaitForTimerStep(em, instance, stepInstance, stepDef, context)
|
|
340
|
+
|
|
336
341
|
case 'PARALLEL_FORK':
|
|
337
342
|
case 'PARALLEL_JOIN':
|
|
338
|
-
case 'WAIT_FOR_TIMER':
|
|
339
343
|
// These will be implemented in later phases
|
|
340
344
|
throw new StepExecutionError(
|
|
341
345
|
`Step type not yet implemented: ${stepType}`,
|
|
@@ -756,66 +760,119 @@ async function handleWaitForSignalStep(
|
|
|
756
760
|
}
|
|
757
761
|
}
|
|
758
762
|
|
|
759
|
-
// ============================================================================
|
|
760
|
-
// Helper Functions
|
|
761
|
-
// ============================================================================
|
|
762
|
-
|
|
763
763
|
/**
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
* Supports:
|
|
767
|
-
* - ISO 8601: PT5M (5 minutes), PT1H (1 hour), P1D (1 day), P3D (3 days)
|
|
768
|
-
* - Simple formats: 5m, 1h, 3d, 30s
|
|
764
|
+
* Handle WAIT_FOR_TIMER step - pause workflow until a timer fires.
|
|
769
765
|
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
766
|
+
* Reads `duration` (relative, e.g. "PT5M") or `until` (ISO 8601 datetime) from
|
|
767
|
+
* `stepDef.config` (preferred — matches StepsEditor) or `stepDef.timerConfig`.
|
|
768
|
+
* Enqueues a delayed timer job on the workflow-activities queue; when the job
|
|
769
|
+
* is processed by the activity worker, it calls `timerHandler.fireTimer` to
|
|
770
|
+
* resume the workflow.
|
|
772
771
|
*/
|
|
773
|
-
function
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
772
|
+
async function handleWaitForTimerStep(
|
|
773
|
+
em: EntityManager,
|
|
774
|
+
instance: WorkflowInstance,
|
|
775
|
+
stepInstance: StepInstance,
|
|
776
|
+
stepDef: any,
|
|
777
|
+
context: StepExecutionContext
|
|
778
|
+
): Promise<StepExecutionResult> {
|
|
779
|
+
const timerConfig = stepDef.config || stepDef.timerConfig || {}
|
|
780
|
+
const duration: string | undefined = timerConfig.duration
|
|
781
|
+
const until: string | undefined = timerConfig.until
|
|
782
|
+
|
|
783
|
+
if (!duration && !until) {
|
|
784
|
+
throw new StepExecutionError(
|
|
785
|
+
'WAIT_FOR_TIMER requires either "duration" (e.g., "PT5M") or "until" (ISO 8601 datetime)',
|
|
786
|
+
'TIMER_CONFIG_MISSING',
|
|
787
|
+
{ stepId: stepDef.stepId }
|
|
789
788
|
)
|
|
790
789
|
}
|
|
791
790
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
case 'd':
|
|
802
|
-
return value * 24 * 60 * 60 * 1000
|
|
803
|
-
case 'h':
|
|
804
|
-
return value * 60 * 60 * 1000
|
|
805
|
-
case 'm':
|
|
806
|
-
return value * 60 * 1000
|
|
807
|
-
case 's':
|
|
808
|
-
return value * 1000
|
|
791
|
+
let fireAtMs: number
|
|
792
|
+
if (until) {
|
|
793
|
+
const targetDate = new Date(until)
|
|
794
|
+
if (isNaN(targetDate.getTime())) {
|
|
795
|
+
throw new StepExecutionError(
|
|
796
|
+
`WAIT_FOR_TIMER invalid "until" datetime: ${until}`,
|
|
797
|
+
'TIMER_CONFIG_INVALID',
|
|
798
|
+
{ until }
|
|
799
|
+
)
|
|
809
800
|
}
|
|
801
|
+
fireAtMs = targetDate.getTime()
|
|
802
|
+
} else {
|
|
803
|
+
fireAtMs = Date.now() + parseDuration(duration as string)
|
|
810
804
|
}
|
|
811
805
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
)
|
|
806
|
+
const delayMs = fireAtMs - Date.now()
|
|
807
|
+
const fireAt = new Date(fireAtMs)
|
|
808
|
+
|
|
809
|
+
// Immediate-fire path: skip the queue round-trip if the timer is in the past
|
|
810
|
+
if (delayMs <= 0) {
|
|
811
|
+
return {
|
|
812
|
+
status: 'COMPLETED',
|
|
813
|
+
outputData: {
|
|
814
|
+
stepType: 'WAIT_FOR_TIMER',
|
|
815
|
+
timerFiredImmediately: true,
|
|
816
|
+
fireAt,
|
|
817
|
+
duration,
|
|
818
|
+
until,
|
|
819
|
+
},
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const now = new Date()
|
|
824
|
+
|
|
825
|
+
// Enqueue delayed timer job via the shared activity queue.
|
|
826
|
+
// Imported here to avoid a top-level cycle between step-handler and activity-executor.
|
|
827
|
+
const { enqueueTimerJob } = await import('./activity-executor')
|
|
828
|
+
const jobId = await enqueueTimerJob({
|
|
829
|
+
workflowInstanceId: instance.id,
|
|
830
|
+
stepInstanceId: stepInstance.id,
|
|
831
|
+
tenantId: instance.tenantId,
|
|
832
|
+
organizationId: instance.organizationId,
|
|
833
|
+
userId: context.userId,
|
|
834
|
+
fireAt: fireAt.toISOString(),
|
|
835
|
+
delayMs,
|
|
836
|
+
})
|
|
837
|
+
|
|
838
|
+
await logWorkflowEvent(em, {
|
|
839
|
+
workflowInstanceId: instance.id,
|
|
840
|
+
stepInstanceId: stepInstance.id,
|
|
841
|
+
eventType: 'TIMER_AWAITING',
|
|
842
|
+
eventData: {
|
|
843
|
+
fireAt: fireAt.toISOString(),
|
|
844
|
+
duration: duration || null,
|
|
845
|
+
until: until || null,
|
|
846
|
+
jobId,
|
|
847
|
+
},
|
|
848
|
+
userId: context.userId,
|
|
849
|
+
tenantId: instance.tenantId,
|
|
850
|
+
organizationId: instance.organizationId,
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
instance.status = 'PAUSED'
|
|
854
|
+
instance.pausedAt = now
|
|
855
|
+
instance.updatedAt = now
|
|
856
|
+
await em.flush()
|
|
857
|
+
|
|
858
|
+
return {
|
|
859
|
+
status: 'WAITING',
|
|
860
|
+
waitReason: 'TIMER',
|
|
861
|
+
outputData: {
|
|
862
|
+
fireAt,
|
|
863
|
+
duration,
|
|
864
|
+
until,
|
|
865
|
+
jobId,
|
|
866
|
+
},
|
|
867
|
+
}
|
|
817
868
|
}
|
|
818
869
|
|
|
870
|
+
// ============================================================================
|
|
871
|
+
// Helper Functions
|
|
872
|
+
// ============================================================================
|
|
873
|
+
|
|
874
|
+
// parseDuration is imported from ./duration
|
|
875
|
+
|
|
819
876
|
/**
|
|
820
877
|
* Log step-related event to event sourcing table
|
|
821
878
|
*/
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timer Handler Service
|
|
3
|
+
*
|
|
4
|
+
* Fires timers for WAIT_FOR_TIMER steps: resumes a paused workflow instance
|
|
5
|
+
* when its scheduled timer job is processed by the activity worker.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EntityManager } from '@mikro-orm/core'
|
|
9
|
+
import type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'
|
|
10
|
+
import type { AwilixContainer } from 'awilix'
|
|
11
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
12
|
+
import { WorkflowInstance, WorkflowDefinition, StepInstance } from '../data/entities'
|
|
13
|
+
import type * as eventLoggerModule from './event-logger'
|
|
14
|
+
import type * as stepHandlerModule from './step-handler'
|
|
15
|
+
import type * as transitionHandlerModule from './transition-handler'
|
|
16
|
+
import type * as workflowExecutorModule from './workflow-executor'
|
|
17
|
+
|
|
18
|
+
export interface FireTimerOptions {
|
|
19
|
+
instanceId: string
|
|
20
|
+
stepInstanceId?: string
|
|
21
|
+
userId?: string
|
|
22
|
+
tenantId: string
|
|
23
|
+
organizationId: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class TimerError extends Error {
|
|
27
|
+
constructor(
|
|
28
|
+
message: string,
|
|
29
|
+
public code: string,
|
|
30
|
+
public details?: any
|
|
31
|
+
) {
|
|
32
|
+
super(message)
|
|
33
|
+
this.name = 'TimerError'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Fire a timer and resume workflow execution.
|
|
39
|
+
*
|
|
40
|
+
* Mirrors `sendSignal` from signal-handler.ts — verifies the instance is
|
|
41
|
+
* paused at a WAIT_FOR_TIMER step, logs TIMER_FIRED, exits the step, then
|
|
42
|
+
* executes auto transitions and resumes the workflow.
|
|
43
|
+
*/
|
|
44
|
+
export async function fireTimer(
|
|
45
|
+
em: EntityManager,
|
|
46
|
+
container: AwilixContainer,
|
|
47
|
+
options: FireTimerOptions
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
const { instanceId, stepInstanceId, userId, tenantId, organizationId } = options
|
|
50
|
+
|
|
51
|
+
const eventLogger = container.resolve<typeof eventLoggerModule>('eventLogger')
|
|
52
|
+
const stepHandler = container.resolve<typeof stepHandlerModule>('stepHandler')
|
|
53
|
+
const transitionHandler = container.resolve<typeof transitionHandlerModule>('transitionHandler')
|
|
54
|
+
const workflowExecutor = container.resolve<typeof workflowExecutorModule>('workflowExecutor')
|
|
55
|
+
|
|
56
|
+
const instance = await findOneWithDecryption(
|
|
57
|
+
em as PostgreSqlEntityManager,
|
|
58
|
+
WorkflowInstance,
|
|
59
|
+
{
|
|
60
|
+
id: instanceId,
|
|
61
|
+
tenantId,
|
|
62
|
+
organizationId,
|
|
63
|
+
},
|
|
64
|
+
undefined,
|
|
65
|
+
{ tenantId, organizationId },
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if (!instance) {
|
|
69
|
+
throw new TimerError(
|
|
70
|
+
'Workflow instance not found',
|
|
71
|
+
'INSTANCE_NOT_FOUND',
|
|
72
|
+
{ instanceId }
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (instance.status !== 'PAUSED') {
|
|
77
|
+
throw new TimerError(
|
|
78
|
+
'Workflow is not paused',
|
|
79
|
+
'WORKFLOW_NOT_PAUSED',
|
|
80
|
+
{ instanceId, status: instance.status }
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const definition = await findOneWithDecryption(
|
|
85
|
+
em as PostgreSqlEntityManager,
|
|
86
|
+
WorkflowDefinition,
|
|
87
|
+
{
|
|
88
|
+
id: instance.definitionId,
|
|
89
|
+
tenantId: instance.tenantId,
|
|
90
|
+
organizationId: instance.organizationId,
|
|
91
|
+
deletedAt: null,
|
|
92
|
+
},
|
|
93
|
+
undefined,
|
|
94
|
+
{ tenantId: instance.tenantId, organizationId: instance.organizationId },
|
|
95
|
+
)
|
|
96
|
+
if (!definition) {
|
|
97
|
+
throw new TimerError(
|
|
98
|
+
'Workflow definition not found',
|
|
99
|
+
'DEFINITION_NOT_FOUND',
|
|
100
|
+
{ definitionId: instance.definitionId }
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const currentStep = definition.definition.steps.find(
|
|
105
|
+
(s: any) => s.stepId === instance.currentStepId
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if (!currentStep || currentStep.stepType !== 'WAIT_FOR_TIMER') {
|
|
109
|
+
throw new TimerError(
|
|
110
|
+
'Workflow is not waiting for timer',
|
|
111
|
+
'NOT_WAITING_FOR_TIMER',
|
|
112
|
+
{ instanceId, currentStepId: instance.currentStepId }
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const now = new Date()
|
|
117
|
+
instance.updatedAt = now
|
|
118
|
+
|
|
119
|
+
await eventLogger.logWorkflowEvent(em, {
|
|
120
|
+
workflowInstanceId: instance.id,
|
|
121
|
+
stepInstanceId,
|
|
122
|
+
eventType: 'TIMER_FIRED',
|
|
123
|
+
eventData: {
|
|
124
|
+
stepId: instance.currentStepId,
|
|
125
|
+
firedAt: now.toISOString(),
|
|
126
|
+
},
|
|
127
|
+
userId,
|
|
128
|
+
tenantId: instance.tenantId,
|
|
129
|
+
organizationId: instance.organizationId,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const stepInstance = stepInstanceId
|
|
133
|
+
? await findOneWithDecryption(
|
|
134
|
+
em as PostgreSqlEntityManager,
|
|
135
|
+
StepInstance,
|
|
136
|
+
{
|
|
137
|
+
id: stepInstanceId,
|
|
138
|
+
workflowInstanceId: instance.id,
|
|
139
|
+
tenantId: instance.tenantId,
|
|
140
|
+
organizationId: instance.organizationId,
|
|
141
|
+
},
|
|
142
|
+
undefined,
|
|
143
|
+
{ tenantId: instance.tenantId, organizationId: instance.organizationId },
|
|
144
|
+
)
|
|
145
|
+
: await findOneWithDecryption(
|
|
146
|
+
em as PostgreSqlEntityManager,
|
|
147
|
+
StepInstance,
|
|
148
|
+
{
|
|
149
|
+
workflowInstanceId: instance.id,
|
|
150
|
+
stepId: instance.currentStepId,
|
|
151
|
+
status: 'ACTIVE',
|
|
152
|
+
tenantId: instance.tenantId,
|
|
153
|
+
organizationId: instance.organizationId,
|
|
154
|
+
},
|
|
155
|
+
undefined,
|
|
156
|
+
{ tenantId: instance.tenantId, organizationId: instance.organizationId },
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if (stepInstance) {
|
|
160
|
+
await stepHandler.exitStep(em, stepInstance, {
|
|
161
|
+
firedAt: now.toISOString(),
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const autoTransitions = (definition.definition.transitions || []).filter(
|
|
166
|
+
(t: any) => t.fromStepId === instance.currentStepId && t.trigger === 'auto'
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if (autoTransitions.length === 0) {
|
|
170
|
+
instance.status = 'RUNNING'
|
|
171
|
+
await em.flush()
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const transitionContext = {
|
|
176
|
+
workflowContext: instance.context,
|
|
177
|
+
userId,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const validTransitions = await transitionHandler.findValidTransitions(
|
|
181
|
+
em,
|
|
182
|
+
instance,
|
|
183
|
+
instance.currentStepId,
|
|
184
|
+
transitionContext
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
const firstValidTransition = validTransitions.find((t) => t.isValid)
|
|
188
|
+
|
|
189
|
+
if (!firstValidTransition || !firstValidTransition.transition) {
|
|
190
|
+
instance.status = 'RUNNING'
|
|
191
|
+
await em.flush()
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const transitionResult = await transitionHandler.executeTransition(
|
|
196
|
+
em,
|
|
197
|
+
container,
|
|
198
|
+
instance,
|
|
199
|
+
instance.currentStepId,
|
|
200
|
+
firstValidTransition.transition.toStepId,
|
|
201
|
+
transitionContext
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if (!transitionResult.success) {
|
|
205
|
+
throw new TimerError(
|
|
206
|
+
'Transition failed after timer fired',
|
|
207
|
+
'TRANSITION_FAILED',
|
|
208
|
+
{ error: transitionResult.error }
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await workflowExecutor.executeWorkflow(em, container, instance.id, { userId })
|
|
213
|
+
}
|
|
@@ -325,7 +325,7 @@ export async function executeWorkflow(
|
|
|
325
325
|
if (
|
|
326
326
|
currentStep?.stepType === 'USER_TASK' ||
|
|
327
327
|
currentStep?.stepType === 'WAIT_FOR_SIGNAL' ||
|
|
328
|
-
currentStep?.stepType === '
|
|
328
|
+
currentStep?.stepType === 'WAIT_FOR_TIMER'
|
|
329
329
|
) {
|
|
330
330
|
return {
|
|
331
331
|
status: 'RUNNING',
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { QueuedJob, JobContext, WorkerMeta } from '@open-mercato/queue'
|
|
12
|
-
import {
|
|
12
|
+
import type { WorkflowActivityJob } from '../lib/activity-queue-types'
|
|
13
13
|
import type { EntityManager } from '@mikro-orm/core'
|
|
14
14
|
import type { AwilixContainer } from 'awilix'
|
|
15
15
|
import { WorkflowInstance } from '../data/entities'
|
|
@@ -23,12 +23,18 @@ import {
|
|
|
23
23
|
executeFunction,
|
|
24
24
|
} from '../lib/activity-executor'
|
|
25
25
|
|
|
26
|
-
// Worker metadata for auto-discovery
|
|
26
|
+
// Worker metadata for auto-discovery.
|
|
27
|
+
// NOTE: `queue` MUST be a string literal (or locally-declared const) so the
|
|
28
|
+
// generator's AST-based extractor can resolve it when Node cannot import the
|
|
29
|
+
// .ts source file directly. Importing `WORKFLOW_ACTIVITIES_QUEUE_NAME` from
|
|
30
|
+
// another module breaks auto-discovery and silently drops the worker from
|
|
31
|
+
// `modules.generated.ts`.
|
|
32
|
+
const WORKFLOW_ACTIVITIES_QUEUE = 'workflow-activities'
|
|
27
33
|
const DEFAULT_CONCURRENCY = 1
|
|
28
34
|
const envConcurrency = process.env.WORKERS_WORKFLOW_ACTIVITIES_CONCURRENCY
|
|
29
35
|
|
|
30
36
|
export const metadata: WorkerMeta = {
|
|
31
|
-
queue:
|
|
37
|
+
queue: WORKFLOW_ACTIVITIES_QUEUE,
|
|
32
38
|
id: 'workflows:workflow-activities',
|
|
33
39
|
concurrency: envConcurrency ? parseInt(envConcurrency, 10) : DEFAULT_CONCURRENCY,
|
|
34
40
|
}
|
|
@@ -54,10 +60,6 @@ export default async function handle(
|
|
|
54
60
|
const { payload } = job
|
|
55
61
|
const startTime = Date.now()
|
|
56
62
|
|
|
57
|
-
console.log(
|
|
58
|
-
`[workflows:activity-worker] Processing activity ${payload.activityId} (${payload.activityType}) for workflow instance ${payload.workflowInstanceId} (job ${ctx.jobId}, attempt ${ctx.attemptNumber})`
|
|
59
|
-
)
|
|
60
|
-
|
|
61
63
|
// Resolve services from DI container
|
|
62
64
|
const em = ctx.resolve<EntityManager>('em')
|
|
63
65
|
|
|
@@ -65,6 +67,27 @@ export default async function handle(
|
|
|
65
67
|
// The ctx already has the resolve method we need, we just need to cast it
|
|
66
68
|
const container = ctx as unknown as AwilixContainer
|
|
67
69
|
|
|
70
|
+
// Timer jobs (kind: 'timer') are a distinct flow — they resume a paused
|
|
71
|
+
// workflow at a WAIT_FOR_TIMER step rather than running an activity.
|
|
72
|
+
if (payload.kind === 'timer') {
|
|
73
|
+
console.log(
|
|
74
|
+
`[workflows:activity-worker] Firing timer for instance ${payload.workflowInstanceId} (job ${ctx.jobId})`
|
|
75
|
+
)
|
|
76
|
+
const { fireTimer } = await import('../lib/timer-handler')
|
|
77
|
+
await fireTimer(em, container, {
|
|
78
|
+
instanceId: payload.workflowInstanceId,
|
|
79
|
+
stepInstanceId: payload.stepInstanceId,
|
|
80
|
+
tenantId: payload.tenantId,
|
|
81
|
+
organizationId: payload.organizationId,
|
|
82
|
+
userId: payload.userId,
|
|
83
|
+
})
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(
|
|
88
|
+
`[workflows:activity-worker] Processing activity ${payload.activityId} (${payload.activityType}) for workflow instance ${payload.workflowInstanceId} (job ${ctx.jobId}, attempt ${ctx.attemptNumber})`
|
|
89
|
+
)
|
|
90
|
+
|
|
68
91
|
try {
|
|
69
92
|
// Fetch workflow instance with tenant/org scoping
|
|
70
93
|
const instance = await em.findOne(WorkflowInstance, {
|
|
@@ -114,6 +137,9 @@ export default async function handle(
|
|
|
114
137
|
return await executeCallWebhook(payload.activityConfig, activityContext, { signal })
|
|
115
138
|
case 'EXECUTE_FUNCTION':
|
|
116
139
|
return await executeFunction(payload.activityConfig, activityContext, container)
|
|
140
|
+
case 'WAIT':
|
|
141
|
+
// Delay already handled by queue's delayMs — return success immediately
|
|
142
|
+
return { waited: true }
|
|
117
143
|
default:
|
|
118
144
|
throw new Error(`Unsupported activity type: ${payload.activityType}`)
|
|
119
145
|
}
|