@open-mercato/core 0.6.3-develop.3876.1.d40fe4ec2d → 0.6.3-develop.3894.1.352abf4240

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 (140) 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/backend/auth/profile/page.js +1 -1
  9. package/dist/modules/auth/backend/auth/profile/page.js.map +2 -2
  10. package/dist/modules/auth/backend/profile/change-password/page.js +1 -1
  11. package/dist/modules/auth/backend/profile/change-password/page.js.map +2 -2
  12. package/dist/modules/auth/backend/users/[id]/edit/page.js +1 -1
  13. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  14. package/dist/modules/auth/backend/users/create/page.js +6 -1
  15. package/dist/modules/auth/backend/users/create/page.js.map +2 -2
  16. package/dist/modules/auth/di.js +17 -3
  17. package/dist/modules/auth/di.js.map +2 -2
  18. package/dist/modules/auth/services/rbacDefaultCache.js +110 -0
  19. package/dist/modules/auth/services/rbacDefaultCache.js.map +7 -0
  20. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +8 -1
  21. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  22. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +3 -2
  23. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  24. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js +3 -2
  25. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js.map +2 -2
  26. package/dist/modules/configs/cli.js +27 -14
  27. package/dist/modules/configs/cli.js.map +2 -2
  28. package/dist/modules/currencies/api/currencies/route.js +3 -4
  29. package/dist/modules/currencies/api/currencies/route.js.map +2 -2
  30. package/dist/modules/currencies/api/exchange-rates/route.js +3 -4
  31. package/dist/modules/currencies/api/exchange-rates/route.js.map +2 -2
  32. package/dist/modules/customers/api/people/route.js +26 -24
  33. package/dist/modules/customers/api/people/route.js.map +2 -2
  34. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +26 -0
  35. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +7 -0
  36. package/dist/modules/directory/utils/organizationScope.js +85 -0
  37. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  38. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +1 -1
  39. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +2 -2
  40. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js +1 -1
  41. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js.map +2 -2
  42. package/dist/modules/sales/components/channels/ChannelOfferForm.js +1 -1
  43. package/dist/modules/sales/components/channels/ChannelOfferForm.js.map +2 -2
  44. package/dist/modules/workflows/backend/definitions/[id]/page.js +2 -1
  45. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  46. package/dist/modules/workflows/backend/definitions/create/page.js +4 -2
  47. package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
  48. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +20 -3
  49. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  50. package/dist/modules/workflows/components/ActivitiesEditor.js +34 -1
  51. package/dist/modules/workflows/components/ActivitiesEditor.js.map +2 -2
  52. package/dist/modules/workflows/components/NodeEditDialog.js +153 -17
  53. package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
  54. package/dist/modules/workflows/components/StepsEditor.js +31 -0
  55. package/dist/modules/workflows/components/StepsEditor.js.map +2 -2
  56. package/dist/modules/workflows/components/WorkflowGraph.js +3 -2
  57. package/dist/modules/workflows/components/WorkflowGraph.js.map +2 -2
  58. package/dist/modules/workflows/components/nodes/WaitForTimerNode.js +54 -0
  59. package/dist/modules/workflows/components/nodes/WaitForTimerNode.js.map +7 -0
  60. package/dist/modules/workflows/components/nodes/index.js +3 -1
  61. package/dist/modules/workflows/components/nodes/index.js.map +2 -2
  62. package/dist/modules/workflows/data/validators.js +117 -0
  63. package/dist/modules/workflows/data/validators.js.map +2 -2
  64. package/dist/modules/workflows/di.js +5 -1
  65. package/dist/modules/workflows/di.js.map +2 -2
  66. package/dist/modules/workflows/lib/activity-executor.js +42 -1
  67. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  68. package/dist/modules/workflows/lib/activity-queue-types.js.map +2 -2
  69. package/dist/modules/workflows/lib/activity-worker-handler.js +24 -0
  70. package/dist/modules/workflows/lib/activity-worker-handler.js.map +2 -2
  71. package/dist/modules/workflows/lib/duration.js +32 -0
  72. package/dist/modules/workflows/lib/duration.js.map +7 -0
  73. package/dist/modules/workflows/lib/event-logger.js +1 -0
  74. package/dist/modules/workflows/lib/event-logger.js.map +2 -2
  75. package/dist/modules/workflows/lib/format-validation-error.js +12 -0
  76. package/dist/modules/workflows/lib/format-validation-error.js.map +7 -0
  77. package/dist/modules/workflows/lib/graph-utils.js +6 -3
  78. package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
  79. package/dist/modules/workflows/lib/node-type-icons.js +9 -5
  80. package/dist/modules/workflows/lib/node-type-icons.js.map +2 -2
  81. package/dist/modules/workflows/lib/signal-handler.js +55 -23
  82. package/dist/modules/workflows/lib/signal-handler.js.map +2 -2
  83. package/dist/modules/workflows/lib/step-handler.js +79 -29
  84. package/dist/modules/workflows/lib/step-handler.js.map +2 -2
  85. package/dist/modules/workflows/lib/timer-handler.js +159 -0
  86. package/dist/modules/workflows/lib/timer-handler.js.map +7 -0
  87. package/dist/modules/workflows/lib/workflow-executor.js +1 -1
  88. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  89. package/dist/modules/workflows/workers/workflow-activities.worker.js +20 -4
  90. package/dist/modules/workflows/workers/workflow-activities.worker.js.map +2 -2
  91. package/package.json +7 -7
  92. package/src/modules/attachments/api/file/[id]/route.ts +7 -2
  93. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -4
  94. package/src/modules/audit_logs/services/accessLogService.ts +179 -15
  95. package/src/modules/auth/backend/auth/profile/page.tsx +1 -1
  96. package/src/modules/auth/backend/profile/change-password/page.tsx +1 -1
  97. package/src/modules/auth/backend/users/[id]/edit/page.tsx +1 -1
  98. package/src/modules/auth/backend/users/create/page.tsx +6 -1
  99. package/src/modules/auth/di.ts +26 -3
  100. package/src/modules/auth/services/rbacDefaultCache.ts +145 -0
  101. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +8 -1
  102. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +3 -2
  103. package/src/modules/catalog/backend/catalog/products/[productId]/variants/create/page.tsx +3 -2
  104. package/src/modules/configs/cli.ts +34 -13
  105. package/src/modules/currencies/api/currencies/route.ts +3 -4
  106. package/src/modules/currencies/api/exchange-rates/route.ts +3 -4
  107. package/src/modules/customers/api/people/route.ts +27 -25
  108. package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +39 -0
  109. package/src/modules/directory/utils/organizationScope.ts +121 -0
  110. package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +1 -1
  111. package/src/modules/sales/backend/sales/channels/[channelId]/edit/page.tsx +1 -1
  112. package/src/modules/sales/components/channels/ChannelOfferForm.tsx +1 -1
  113. package/src/modules/workflows/backend/definitions/[id]/page.tsx +3 -2
  114. package/src/modules/workflows/backend/definitions/create/page.tsx +4 -2
  115. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +18 -1
  116. package/src/modules/workflows/components/ActivitiesEditor.tsx +40 -0
  117. package/src/modules/workflows/components/NodeEditDialog.tsx +218 -30
  118. package/src/modules/workflows/components/StepsEditor.tsx +36 -0
  119. package/src/modules/workflows/components/WorkflowGraph.tsx +2 -1
  120. package/src/modules/workflows/components/nodes/WaitForTimerNode.tsx +70 -0
  121. package/src/modules/workflows/components/nodes/index.ts +3 -0
  122. package/src/modules/workflows/data/validators.ts +121 -0
  123. package/src/modules/workflows/di.ts +4 -0
  124. package/src/modules/workflows/i18n/de.json +10 -1
  125. package/src/modules/workflows/i18n/en.json +10 -1
  126. package/src/modules/workflows/i18n/es.json +10 -1
  127. package/src/modules/workflows/i18n/pl.json +10 -1
  128. package/src/modules/workflows/lib/activity-executor.ts +86 -2
  129. package/src/modules/workflows/lib/activity-queue-types.ts +18 -11
  130. package/src/modules/workflows/lib/activity-worker-handler.ts +29 -0
  131. package/src/modules/workflows/lib/duration.ts +51 -0
  132. package/src/modules/workflows/lib/event-logger.ts +1 -0
  133. package/src/modules/workflows/lib/format-validation-error.ts +30 -0
  134. package/src/modules/workflows/lib/graph-utils.ts +3 -0
  135. package/src/modules/workflows/lib/node-type-icons.ts +6 -2
  136. package/src/modules/workflows/lib/signal-handler.ts +62 -24
  137. package/src/modules/workflows/lib/step-handler.ts +107 -50
  138. package/src/modules/workflows/lib/timer-handler.ts +213 -0
  139. package/src/modules/workflows/lib/workflow-executor.ts +1 -1
  140. package/src/modules/workflows/workers/workflow-activities.worker.ts +33 -7
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Parse ISO 8601 duration to milliseconds
3
+ *
4
+ * Supports:
5
+ * - ISO 8601: PT5M (5 minutes), PT1H (1 hour), P1D (1 day), P3D (3 days)
6
+ * - Simple formats: 5m, 1h, 3d, 30s
7
+ *
8
+ * @param duration - Duration string
9
+ * @returns Duration in milliseconds
10
+ */
11
+ export function parseDuration(duration: string): number {
12
+ // Try ISO 8601 format first: P[n]DT[n]H[n]M[n]S
13
+ const iso8601Regex = /P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?/
14
+ const iso8601Match = duration.match(iso8601Regex)
15
+
16
+ if (iso8601Match && iso8601Match[0] === duration) {
17
+ const days = parseInt(iso8601Match[1] || '0')
18
+ const hours = parseInt(iso8601Match[2] || '0')
19
+ const minutes = parseInt(iso8601Match[3] || '0')
20
+ const seconds = parseInt(iso8601Match[4] || '0')
21
+
22
+ return (
23
+ days * 24 * 60 * 60 * 1000 +
24
+ hours * 60 * 60 * 1000 +
25
+ minutes * 60 * 1000 +
26
+ seconds * 1000
27
+ )
28
+ }
29
+
30
+ // Try simple format: 1d, 5h, 30m, 45s
31
+ const simpleRegex = /^(\d+)(d|h|m|s)$/
32
+ const simpleMatch = duration.match(simpleRegex)
33
+
34
+ if (simpleMatch) {
35
+ const value = parseInt(simpleMatch[1])
36
+ const unit = simpleMatch[2]
37
+
38
+ switch (unit) {
39
+ case 'd':
40
+ return value * 24 * 60 * 60 * 1000
41
+ case 'h':
42
+ return value * 60 * 60 * 1000
43
+ case 'm':
44
+ return value * 60 * 1000
45
+ case 's':
46
+ return value * 1000
47
+ }
48
+ }
49
+
50
+ throw new Error(`Invalid duration format: ${duration}`)
51
+ }
@@ -73,6 +73,7 @@ export const WorkflowEventTypes = {
73
73
  SIGNAL_TIMEOUT: 'SIGNAL_TIMEOUT',
74
74
 
75
75
  // Timer events (Phase 9)
76
+ TIMER_AWAITING: 'TIMER_AWAITING',
76
77
  TIMER_FIRED: 'TIMER_FIRED',
77
78
  TIMER_CANCELLED: 'TIMER_CANCELLED',
78
79
  } as const
@@ -0,0 +1,30 @@
1
+ type ZodIssueLite = {
2
+ path?: Array<string | number>
3
+ message?: string
4
+ }
5
+
6
+ type ApiErrorBody = {
7
+ error?: string
8
+ details?: ZodIssueLite[]
9
+ }
10
+
11
+ /**
12
+ * Format an API validation error body into a user-readable message.
13
+ *
14
+ * The workflow definitions API returns `{ error: 'Validation failed', details: ZodIssue[] }`
15
+ * for schema failures. The generic `error` string is useless to the user — the actionable
16
+ * information lives in `details[0].path` and `details[0].message`. This helper mirrors the
17
+ * visual editor's `Schema error: <path> - <message>` format so both editors surface the
18
+ * same diagnostic.
19
+ */
20
+ export function formatWorkflowValidationError(
21
+ body: ApiErrorBody | null | undefined,
22
+ fallback: string,
23
+ ): string {
24
+ const issue = body?.details?.[0]
25
+ if (issue?.message) {
26
+ const path = (issue.path ?? []).join('.')
27
+ return path ? `${path} - ${issue.message}` : issue.message
28
+ }
29
+ return body?.error || fallback
30
+ }
@@ -452,6 +452,7 @@ function mapNodeTypeToStepType(nodeType: string): string {
452
452
  automated: 'AUTOMATED',
453
453
  decision: 'DECISION',
454
454
  waitForSignal: 'WAIT_FOR_SIGNAL',
455
+ waitForTimer: 'WAIT_FOR_TIMER',
455
456
  }
456
457
  return mapping[nodeType] || 'AUTOMATED'
457
458
  }
@@ -467,6 +468,7 @@ function mapStepTypeToNodeType(stepType: string): string {
467
468
  AUTOMATED: 'automated',
468
469
  DECISION: 'decision',
469
470
  WAIT_FOR_SIGNAL: 'waitForSignal',
471
+ WAIT_FOR_TIMER: 'waitForTimer',
470
472
  }
471
473
  return mapping[stepType] || 'automated'
472
474
  }
@@ -482,6 +484,7 @@ function getBadgeForNodeType(nodeType: string): string {
482
484
  automated: 'Automated',
483
485
  decision: 'Decision',
484
486
  waitForSignal: 'Wait for Signal',
487
+ waitForTimer: 'Wait for Timer',
485
488
  }
486
489
  return badges[nodeType] || 'Task'
487
490
  }
@@ -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
  */