@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.
Files changed (107) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/attachments/api/file/[id]/route.js +7 -2
  3. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  4. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +7 -4
  5. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  6. package/dist/modules/audit_logs/services/accessLogService.js +127 -8
  7. package/dist/modules/audit_logs/services/accessLogService.js.map +2 -2
  8. package/dist/modules/auth/di.js +17 -3
  9. package/dist/modules/auth/di.js.map +2 -2
  10. package/dist/modules/auth/services/rbacDefaultCache.js +110 -0
  11. package/dist/modules/auth/services/rbacDefaultCache.js.map +7 -0
  12. package/dist/modules/currencies/api/currencies/route.js +3 -4
  13. package/dist/modules/currencies/api/currencies/route.js.map +2 -2
  14. package/dist/modules/currencies/api/exchange-rates/route.js +3 -4
  15. package/dist/modules/currencies/api/exchange-rates/route.js.map +2 -2
  16. package/dist/modules/customers/api/people/route.js +26 -24
  17. package/dist/modules/customers/api/people/route.js.map +2 -2
  18. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +26 -0
  19. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +7 -0
  20. package/dist/modules/directory/utils/organizationScope.js +85 -0
  21. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  22. package/dist/modules/workflows/backend/definitions/[id]/page.js +2 -1
  23. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  24. package/dist/modules/workflows/backend/definitions/create/page.js +4 -2
  25. package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
  26. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +20 -3
  27. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  28. package/dist/modules/workflows/components/ActivitiesEditor.js +34 -1
  29. package/dist/modules/workflows/components/ActivitiesEditor.js.map +2 -2
  30. package/dist/modules/workflows/components/NodeEditDialog.js +153 -17
  31. package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
  32. package/dist/modules/workflows/components/StepsEditor.js +31 -0
  33. package/dist/modules/workflows/components/StepsEditor.js.map +2 -2
  34. package/dist/modules/workflows/components/WorkflowGraph.js +3 -2
  35. package/dist/modules/workflows/components/WorkflowGraph.js.map +2 -2
  36. package/dist/modules/workflows/components/nodes/WaitForTimerNode.js +54 -0
  37. package/dist/modules/workflows/components/nodes/WaitForTimerNode.js.map +7 -0
  38. package/dist/modules/workflows/components/nodes/index.js +3 -1
  39. package/dist/modules/workflows/components/nodes/index.js.map +2 -2
  40. package/dist/modules/workflows/data/validators.js +117 -0
  41. package/dist/modules/workflows/data/validators.js.map +2 -2
  42. package/dist/modules/workflows/di.js +5 -1
  43. package/dist/modules/workflows/di.js.map +2 -2
  44. package/dist/modules/workflows/lib/activity-executor.js +42 -1
  45. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  46. package/dist/modules/workflows/lib/activity-queue-types.js.map +2 -2
  47. package/dist/modules/workflows/lib/activity-worker-handler.js +24 -0
  48. package/dist/modules/workflows/lib/activity-worker-handler.js.map +2 -2
  49. package/dist/modules/workflows/lib/duration.js +32 -0
  50. package/dist/modules/workflows/lib/duration.js.map +7 -0
  51. package/dist/modules/workflows/lib/event-logger.js +1 -0
  52. package/dist/modules/workflows/lib/event-logger.js.map +2 -2
  53. package/dist/modules/workflows/lib/format-validation-error.js +12 -0
  54. package/dist/modules/workflows/lib/format-validation-error.js.map +7 -0
  55. package/dist/modules/workflows/lib/graph-utils.js +6 -3
  56. package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
  57. package/dist/modules/workflows/lib/node-type-icons.js +9 -5
  58. package/dist/modules/workflows/lib/node-type-icons.js.map +2 -2
  59. package/dist/modules/workflows/lib/signal-handler.js +55 -23
  60. package/dist/modules/workflows/lib/signal-handler.js.map +2 -2
  61. package/dist/modules/workflows/lib/step-handler.js +79 -29
  62. package/dist/modules/workflows/lib/step-handler.js.map +2 -2
  63. package/dist/modules/workflows/lib/timer-handler.js +159 -0
  64. package/dist/modules/workflows/lib/timer-handler.js.map +7 -0
  65. package/dist/modules/workflows/lib/workflow-executor.js +1 -1
  66. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  67. package/dist/modules/workflows/workers/workflow-activities.worker.js +20 -4
  68. package/dist/modules/workflows/workers/workflow-activities.worker.js.map +2 -2
  69. package/package.json +7 -7
  70. package/src/modules/attachments/api/file/[id]/route.ts +7 -2
  71. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -4
  72. package/src/modules/audit_logs/services/accessLogService.ts +179 -15
  73. package/src/modules/auth/di.ts +26 -3
  74. package/src/modules/auth/services/rbacDefaultCache.ts +145 -0
  75. package/src/modules/currencies/api/currencies/route.ts +3 -4
  76. package/src/modules/currencies/api/exchange-rates/route.ts +3 -4
  77. package/src/modules/customers/api/people/route.ts +27 -25
  78. package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +39 -0
  79. package/src/modules/directory/utils/organizationScope.ts +121 -0
  80. package/src/modules/workflows/backend/definitions/[id]/page.tsx +3 -2
  81. package/src/modules/workflows/backend/definitions/create/page.tsx +4 -2
  82. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +18 -1
  83. package/src/modules/workflows/components/ActivitiesEditor.tsx +40 -0
  84. package/src/modules/workflows/components/NodeEditDialog.tsx +218 -30
  85. package/src/modules/workflows/components/StepsEditor.tsx +36 -0
  86. package/src/modules/workflows/components/WorkflowGraph.tsx +2 -1
  87. package/src/modules/workflows/components/nodes/WaitForTimerNode.tsx +70 -0
  88. package/src/modules/workflows/components/nodes/index.ts +3 -0
  89. package/src/modules/workflows/data/validators.ts +121 -0
  90. package/src/modules/workflows/di.ts +4 -0
  91. package/src/modules/workflows/i18n/de.json +10 -1
  92. package/src/modules/workflows/i18n/en.json +10 -1
  93. package/src/modules/workflows/i18n/es.json +10 -1
  94. package/src/modules/workflows/i18n/pl.json +10 -1
  95. package/src/modules/workflows/lib/activity-executor.ts +86 -2
  96. package/src/modules/workflows/lib/activity-queue-types.ts +18 -11
  97. package/src/modules/workflows/lib/activity-worker-handler.ts +29 -0
  98. package/src/modules/workflows/lib/duration.ts +51 -0
  99. package/src/modules/workflows/lib/event-logger.ts +1 -0
  100. package/src/modules/workflows/lib/format-validation-error.ts +30 -0
  101. package/src/modules/workflows/lib/graph-utils.ts +3 -0
  102. package/src/modules/workflows/lib/node-type-icons.ts +6 -2
  103. package/src/modules/workflows/lib/signal-handler.ts +62 -24
  104. package/src/modules/workflows/lib/step-handler.ts +107 -50
  105. package/src/modules/workflows/lib/timer-handler.ts +213 -0
  106. package/src/modules/workflows/lib/workflow-executor.ts +1 -1
  107. 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 { logWorkflowEvent } from './event-logger'
11
- import { executeWorkflow } from './workflow-executor'
12
- import * as stepHandler from './step-handler'
13
- import * as transitionHandler from './transition-handler'
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 em.findOne(WorkflowInstance, {
66
- id: instanceId,
67
- tenantId,
68
- organizationId,
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 em.findOne(WorkflowDefinition, instance.definitionId)
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 em.findOne(StepInstance, {
150
- workflowInstanceId: instance.id,
151
- stepId: instance.currentStepId,
152
- status: 'ACTIVE',
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 em.find(WorkflowInstance, {
230
- correlationKey,
231
- status: 'PAUSED',
232
- tenantId,
233
- organizationId,
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
- * Parse ISO 8601 duration to milliseconds
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
- * @param duration - Duration string
771
- * @returns Duration in milliseconds
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 parseDuration(duration: string): number {
774
- // Try ISO 8601 format first: P[n]DT[n]H[n]M[n]S
775
- const iso8601Regex = /P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?/
776
- const iso8601Match = duration.match(iso8601Regex)
777
-
778
- if (iso8601Match && iso8601Match[0] === duration) {
779
- const days = parseInt(iso8601Match[1] || '0')
780
- const hours = parseInt(iso8601Match[2] || '0')
781
- const minutes = parseInt(iso8601Match[3] || '0')
782
- const seconds = parseInt(iso8601Match[4] || '0')
783
-
784
- return (
785
- days * 24 * 60 * 60 * 1000 +
786
- hours * 60 * 60 * 1000 +
787
- minutes * 60 * 1000 +
788
- seconds * 1000
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
- // Try simple format: 1d, 5h, 30m, 45s
793
- const simpleRegex = /^(\d+)(d|h|m|s)$/
794
- const simpleMatch = duration.match(simpleRegex)
795
-
796
- if (simpleMatch) {
797
- const value = parseInt(simpleMatch[1])
798
- const unit = simpleMatch[2]
799
-
800
- switch (unit) {
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
- throw new StepExecutionError(
813
- `Invalid duration format: ${duration}`,
814
- 'INVALID_DURATION',
815
- { duration }
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 === 'TIMER'
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 { WORKFLOW_ACTIVITIES_QUEUE_NAME, type WorkflowActivityJob } from '../lib/activity-queue-types'
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: WORKFLOW_ACTIVITIES_QUEUE_NAME,
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
  }