@open-mercato/core 0.4.2-canary-5d2c419a9b → 0.4.2-canary-9e0237de8e

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 (158) hide show
  1. package/dist/generated/entities/workflow_event_trigger/index.js +33 -0
  2. package/dist/generated/entities/workflow_event_trigger/index.js.map +7 -0
  3. package/dist/generated/entities.ids.generated.js +1 -0
  4. package/dist/generated/entities.ids.generated.js.map +2 -2
  5. package/dist/generated/entity-fields-registry.js +2 -0
  6. package/dist/generated/entity-fields-registry.js.map +2 -2
  7. package/dist/modules/auth/events.js +30 -0
  8. package/dist/modules/auth/events.js.map +7 -0
  9. package/dist/modules/business_rules/api/execute/[ruleId]/route.js +145 -0
  10. package/dist/modules/business_rules/api/execute/[ruleId]/route.js.map +7 -0
  11. package/dist/modules/business_rules/data/validators.js +34 -0
  12. package/dist/modules/business_rules/data/validators.js.map +2 -2
  13. package/dist/modules/business_rules/index.js +21 -1
  14. package/dist/modules/business_rules/index.js.map +2 -2
  15. package/dist/modules/business_rules/lib/rule-engine.js +182 -1
  16. package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
  17. package/dist/modules/catalog/events.js +34 -0
  18. package/dist/modules/catalog/events.js.map +7 -0
  19. package/dist/modules/customers/events.js +49 -0
  20. package/dist/modules/customers/events.js.map +7 -0
  21. package/dist/modules/directory/events.js +23 -0
  22. package/dist/modules/directory/events.js.map +7 -0
  23. package/dist/modules/sales/acl.js +1 -0
  24. package/dist/modules/sales/acl.js.map +2 -2
  25. package/dist/modules/sales/backend/sales/documents/[id]/page.js +12 -0
  26. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  27. package/dist/modules/sales/commands/documents.js +62 -0
  28. package/dist/modules/sales/commands/documents.js.map +2 -2
  29. package/dist/modules/sales/events.js +63 -0
  30. package/dist/modules/sales/events.js.map +7 -0
  31. package/dist/modules/sales/lib/dictionaries.js +3 -0
  32. package/dist/modules/sales/lib/dictionaries.js.map +2 -2
  33. package/dist/modules/sales/lib/frontend/documentDataEvents.js +25 -0
  34. package/dist/modules/sales/lib/frontend/documentDataEvents.js.map +7 -0
  35. package/dist/modules/workflows/acl.js +2 -0
  36. package/dist/modules/workflows/acl.js.map +2 -2
  37. package/dist/modules/workflows/api/instances/route.js +18 -6
  38. package/dist/modules/workflows/api/instances/route.js.map +2 -2
  39. package/dist/modules/workflows/api/tasks/route.js +6 -1
  40. package/dist/modules/workflows/api/tasks/route.js.map +2 -2
  41. package/dist/modules/workflows/backend/definitions/[id]/page.js +9 -1
  42. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  43. package/dist/modules/workflows/backend/definitions/[id]/page.meta.js +1 -1
  44. package/dist/modules/workflows/backend/definitions/[id]/page.meta.js.map +2 -2
  45. package/dist/modules/workflows/backend/definitions/create/page.js +24 -15
  46. package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
  47. package/dist/modules/workflows/backend/definitions/create/page.meta.js +1 -1
  48. package/dist/modules/workflows/backend/definitions/create/page.meta.js.map +2 -2
  49. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +150 -132
  50. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  51. package/dist/modules/workflows/backend/definitions/visual-editor/page.meta.js +1 -1
  52. package/dist/modules/workflows/backend/definitions/visual-editor/page.meta.js.map +2 -2
  53. package/dist/modules/workflows/backend/events/[id]/page.js +1 -1
  54. package/dist/modules/workflows/backend/events/[id]/page.js.map +2 -2
  55. package/dist/modules/workflows/backend/events/[id]/page.meta.js +2 -2
  56. package/dist/modules/workflows/backend/events/[id]/page.meta.js.map +2 -2
  57. package/dist/modules/workflows/backend/instances/[id]/page.meta.js +2 -2
  58. package/dist/modules/workflows/backend/instances/[id]/page.meta.js.map +2 -2
  59. package/dist/modules/workflows/backend/tasks/[id]/page.js +1 -1
  60. package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
  61. package/dist/modules/workflows/backend/tasks/[id]/page.meta.js +2 -2
  62. package/dist/modules/workflows/backend/tasks/[id]/page.meta.js.map +2 -2
  63. package/dist/modules/workflows/backend/tasks/page.js +5 -6
  64. package/dist/modules/workflows/backend/tasks/page.js.map +2 -2
  65. package/dist/modules/workflows/cli.js +81 -3
  66. package/dist/modules/workflows/cli.js.map +3 -3
  67. package/dist/modules/workflows/components/DefinitionTriggersEditor.js +481 -0
  68. package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +7 -0
  69. package/dist/modules/workflows/components/EventTriggersEditor.js +553 -0
  70. package/dist/modules/workflows/components/EventTriggersEditor.js.map +7 -0
  71. package/dist/modules/workflows/data/entities.js +64 -1
  72. package/dist/modules/workflows/data/entities.js.map +2 -2
  73. package/dist/modules/workflows/data/validators.js +115 -0
  74. package/dist/modules/workflows/data/validators.js.map +2 -2
  75. package/dist/modules/workflows/events.js +38 -0
  76. package/dist/modules/workflows/events.js.map +7 -0
  77. package/dist/modules/workflows/examples/checkout-demo-definition.json +1 -5
  78. package/dist/modules/workflows/examples/order-approval-definition.json +257 -0
  79. package/dist/modules/workflows/examples/order-approval-guard-rules.json +32 -0
  80. package/dist/modules/workflows/lib/activity-executor.js +75 -13
  81. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  82. package/dist/modules/workflows/lib/event-trigger-service.js +308 -0
  83. package/dist/modules/workflows/lib/event-trigger-service.js.map +7 -0
  84. package/dist/modules/workflows/lib/graph-utils.js +71 -2
  85. package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
  86. package/dist/modules/workflows/lib/seeds.js +17 -4
  87. package/dist/modules/workflows/lib/seeds.js.map +2 -2
  88. package/dist/modules/workflows/lib/start-validator.js +33 -23
  89. package/dist/modules/workflows/lib/start-validator.js.map +2 -2
  90. package/dist/modules/workflows/lib/transition-handler.js +157 -45
  91. package/dist/modules/workflows/lib/transition-handler.js.map +3 -3
  92. package/dist/modules/workflows/migrations/Migration20260123143500.js +36 -0
  93. package/dist/modules/workflows/migrations/Migration20260123143500.js.map +7 -0
  94. package/dist/modules/workflows/subscribers/event-trigger.js +78 -0
  95. package/dist/modules/workflows/subscribers/event-trigger.js.map +7 -0
  96. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +323 -0
  97. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +7 -0
  98. package/dist/modules/workflows/widgets/injection/order-approval/widget.js +17 -0
  99. package/dist/modules/workflows/widgets/injection/order-approval/widget.js.map +7 -0
  100. package/dist/modules/workflows/widgets/injection-table.js +19 -0
  101. package/dist/modules/workflows/widgets/injection-table.js.map +7 -0
  102. package/generated/entities/workflow_event_trigger/index.ts +15 -0
  103. package/generated/entities.ids.generated.ts +1 -0
  104. package/generated/entity-fields-registry.ts +2 -0
  105. package/package.json +2 -2
  106. package/src/modules/auth/events.ts +39 -0
  107. package/src/modules/business_rules/api/execute/[ruleId]/route.ts +163 -0
  108. package/src/modules/business_rules/data/validators.ts +40 -0
  109. package/src/modules/business_rules/index.ts +25 -0
  110. package/src/modules/business_rules/lib/rule-engine.ts +281 -1
  111. package/src/modules/catalog/events.ts +45 -0
  112. package/src/modules/customers/events.ts +63 -0
  113. package/src/modules/directory/events.ts +31 -0
  114. package/src/modules/sales/acl.ts +1 -0
  115. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +16 -0
  116. package/src/modules/sales/commands/documents.ts +75 -1
  117. package/src/modules/sales/events.ts +82 -0
  118. package/src/modules/sales/lib/dictionaries.ts +3 -0
  119. package/src/modules/sales/lib/frontend/documentDataEvents.ts +28 -0
  120. package/src/modules/workflows/acl.ts +2 -0
  121. package/src/modules/workflows/api/__tests__/instances.route.test.ts +5 -2
  122. package/src/modules/workflows/api/instances/route.ts +21 -7
  123. package/src/modules/workflows/api/tasks/route.ts +7 -1
  124. package/src/modules/workflows/backend/definitions/[id]/page.meta.ts +1 -1
  125. package/src/modules/workflows/backend/definitions/[id]/page.tsx +9 -0
  126. package/src/modules/workflows/backend/definitions/create/page.meta.ts +1 -1
  127. package/src/modules/workflows/backend/definitions/create/page.tsx +9 -0
  128. package/src/modules/workflows/backend/definitions/visual-editor/page.meta.ts +1 -1
  129. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +21 -3
  130. package/src/modules/workflows/backend/events/[id]/page.meta.ts +2 -2
  131. package/src/modules/workflows/backend/events/[id]/page.tsx +1 -1
  132. package/src/modules/workflows/backend/instances/[id]/page.meta.ts +2 -2
  133. package/src/modules/workflows/backend/tasks/[id]/page.meta.ts +2 -2
  134. package/src/modules/workflows/backend/tasks/[id]/page.tsx +1 -1
  135. package/src/modules/workflows/backend/tasks/page.tsx +5 -6
  136. package/src/modules/workflows/cli.ts +111 -0
  137. package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +581 -0
  138. package/src/modules/workflows/components/EventTriggersEditor.tsx +664 -0
  139. package/src/modules/workflows/data/entities.ts +124 -0
  140. package/src/modules/workflows/data/validators.ts +138 -0
  141. package/src/modules/workflows/events.ts +49 -0
  142. package/src/modules/workflows/examples/checkout-demo-definition.json +1 -5
  143. package/src/modules/workflows/examples/order-approval-definition.json +257 -0
  144. package/src/modules/workflows/examples/order-approval-guard-rules.json +32 -0
  145. package/src/modules/workflows/i18n/en.json +71 -0
  146. package/src/modules/workflows/lib/__tests__/activity-executor.test.ts +43 -36
  147. package/src/modules/workflows/lib/__tests__/transition-handler.test.ts +170 -90
  148. package/src/modules/workflows/lib/activity-executor.ts +129 -16
  149. package/src/modules/workflows/lib/event-trigger-service.ts +557 -0
  150. package/src/modules/workflows/lib/graph-utils.ts +117 -2
  151. package/src/modules/workflows/lib/seeds.ts +29 -8
  152. package/src/modules/workflows/lib/start-validator.ts +38 -28
  153. package/src/modules/workflows/lib/transition-handler.ts +208 -55
  154. package/src/modules/workflows/migrations/Migration20260123143500.ts +38 -0
  155. package/src/modules/workflows/subscribers/event-trigger.ts +109 -0
  156. package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +446 -0
  157. package/src/modules/workflows/widgets/injection/order-approval/widget.ts +16 -0
  158. package/src/modules/workflows/widgets/injection-table.ts +21 -0
@@ -78,16 +78,35 @@ async function seedWorkflowDefinition(
78
78
  })
79
79
 
80
80
  if (existing) {
81
- // Check if the definition needs to be updated (e.g., missing preConditions on START step)
81
+ // Check if the definition needs to be updated by comparing steps and transitions
82
+ const seedStepCount = seed.definition.steps.length
83
+ const existingStepCount = existing.definition.steps.length
84
+ const seedTransitionCount = seed.definition.transitions.length
85
+ const existingTransitionCount = existing.definition.transitions.length
86
+
87
+ // Check for preConditions on transitions
88
+ const seedHasTransitionPreConditions = seed.definition.transitions.some(
89
+ (t: any) => t.preConditions && t.preConditions.length > 0
90
+ )
91
+ const existingHasTransitionPreConditions = existing.definition.transitions.some(
92
+ (t: any) => t.preConditions && t.preConditions.length > 0
93
+ )
94
+
95
+ // Check for preConditions on START step
82
96
  const seedStartStep = seed.definition.steps.find((s: any) => s.stepType === 'START')
83
97
  const existingStartStep = existing.definition.steps.find((s: any) => s.stepType === 'START')
84
-
85
- const seedHasPreConditions = seedStartStep?.preConditions && seedStartStep.preConditions.length > 0
86
- const existingHasPreConditions = existingStartStep?.preConditions && existingStartStep.preConditions.length > 0
87
-
88
- // Update if seed has preConditions but existing doesn't
89
- if (seedHasPreConditions && !existingHasPreConditions) {
90
- console.log(`[seed] Updating workflow ${workflowId} with preConditions`)
98
+ const seedHasStartPreConditions = seedStartStep?.preConditions && seedStartStep.preConditions.length > 0
99
+ const existingHasStartPreConditions = existingStartStep?.preConditions && existingStartStep.preConditions.length > 0
100
+
101
+ // Update if structure has changed
102
+ const needsUpdate =
103
+ seedStepCount !== existingStepCount ||
104
+ seedTransitionCount !== existingTransitionCount ||
105
+ (seedHasStartPreConditions && !existingHasStartPreConditions) ||
106
+ (seedHasTransitionPreConditions && !existingHasTransitionPreConditions)
107
+
108
+ if (needsUpdate) {
109
+ console.log(`[seed] Updating workflow ${workflowId} (steps: ${existingStepCount}→${seedStepCount}, transitions: ${existingTransitionCount}→${seedTransitionCount})`)
91
110
  existing.definition = seed.definition
92
111
  await em.flush()
93
112
  return true
@@ -160,4 +179,6 @@ export async function seedExampleWorkflows(em: EntityManager, scope: WorkflowSee
160
179
  await seedGuardRules(em, scope, 'guard-rules-example.json')
161
180
  await seedWorkflowDefinition(em, scope, 'sales-pipeline-definition.json')
162
181
  await seedWorkflowDefinition(em, scope, 'simple-approval-definition.json')
182
+ await seedGuardRules(em, scope, 'order-approval-guard-rules.json')
183
+ await seedWorkflowDefinition(em, scope, 'order-approval-definition.json')
163
184
  }
@@ -128,59 +128,69 @@ export async function validateWorkflowStart(
128
128
  const validatedRules: ValidatedRule[] = []
129
129
 
130
130
  for (const condition of preConditions) {
131
- const ruleContext: ruleEngine.RuleEngineContext = {
132
- entityType: `workflow:${workflowId}:start`,
133
- entityId: 'pre_start_validation',
134
- eventType: 'validate_start',
131
+ // Execute rule directly by string rule_id
132
+ const result = await ruleEngine.executeRuleByRuleId(em, {
133
+ ruleId: condition.ruleId, // String identifier like "workflow_checkout_inventory_available"
135
134
  data: {
136
135
  workflowId,
137
136
  workflowContext: context,
138
137
  },
139
138
  tenantId,
140
139
  organizationId,
140
+ entityType: `workflow:${workflowId}:start`,
141
+ entityId: 'pre_start_validation',
142
+ eventType: 'validate_start',
141
143
  dryRun: true, // Don't log execution during validation
142
- }
143
-
144
- // Find applicable rules for this context
145
- const rules = await ruleEngine.findApplicableRules(em, {
146
- entityType: ruleContext.entityType,
147
- eventType: ruleContext.eventType,
148
- tenantId,
149
- organizationId,
150
- ruleType: 'GUARD',
151
144
  })
152
145
 
153
- const rule = rules.find(r => r.ruleId === condition.ruleId)
146
+ validatedRules.push({
147
+ ruleId: condition.ruleId,
148
+ passed: result.conditionResult,
149
+ executionTime: result.executionTime,
150
+ })
154
151
 
155
- if (!rule) {
156
- // Rule not found - if required, this is an error
152
+ // Handle rule not found
153
+ if (result.error === 'Rule not found') {
157
154
  if (condition.required) {
158
155
  errors.push({
159
156
  ruleId: condition.ruleId,
160
- message: getLocalizedMessage(condition, null, locale, `Business rule '${condition.ruleId}' not found`),
157
+ message: getLocalizedMessage(condition, null, locale, `Business rule not found: ${condition.ruleId}`),
161
158
  code: 'RULE_NOT_FOUND',
162
159
  })
163
- validatedRules.push({ ruleId: condition.ruleId, passed: false })
164
160
  }
165
161
  continue
166
162
  }
167
163
 
168
- // Execute the single rule
169
- const result = await ruleEngine.executeSingleRule(em, rule, ruleContext)
164
+ // Handle disabled rule
165
+ if (result.error === 'Rule is disabled') {
166
+ if (condition.required) {
167
+ errors.push({
168
+ ruleId: condition.ruleId,
169
+ message: getLocalizedMessage(condition, null, locale, `Business rule is disabled: ${result.ruleName}`),
170
+ code: 'RULE_DISABLED',
171
+ })
172
+ }
173
+ continue
174
+ }
170
175
 
171
- validatedRules.push({
172
- ruleId: condition.ruleId,
173
- passed: result.conditionResult,
174
- executionTime: result.executionTime,
175
- })
176
+ // Handle other errors (not yet effective, expired, etc.)
177
+ if (result.error && condition.required) {
178
+ errors.push({
179
+ ruleId: condition.ruleId,
180
+ message: getLocalizedMessage(condition, null, locale, `Rule error: ${result.error}`),
181
+ code: 'RULE_ERROR',
182
+ })
183
+ continue
184
+ }
176
185
 
186
+ // Handle condition failure
177
187
  if (!result.conditionResult && condition.required) {
178
- // Get localized message from condition, rule failure actions, or default
188
+ // Get localized message from condition or use default with rule name
179
189
  const message = getLocalizedMessage(
180
190
  condition,
181
- rule,
191
+ null,
182
192
  locale,
183
- `Pre-condition '${rule.ruleName || condition.ruleId}' failed`
193
+ `Pre-condition '${result.ruleName || condition.ruleId}' failed`
184
194
  )
185
195
  errors.push({
186
196
  ruleId: condition.ruleId,
@@ -20,7 +20,6 @@ import {
20
20
  } from '../data/entities'
21
21
  import * as ruleEvaluator from '../../business_rules/lib/rule-evaluator'
22
22
  import * as ruleEngine from '../../business_rules/lib/rule-engine'
23
- import type { RuleEngineContext } from '../../business_rules/lib/rule-engine'
24
23
  import * as activityExecutor from './activity-executor'
25
24
  import type { ActivityDefinition } from './activity-executor'
26
25
  import * as stepHandler from './step-handler'
@@ -195,11 +194,15 @@ export async function evaluateTransition(
195
194
  /**
196
195
  * Find all valid transitions from current step
197
196
  *
197
+ * This function evaluates both inline conditions AND preConditions (business rules)
198
+ * to determine which transitions are truly valid. This is important for decision
199
+ * branching where multiple transitions exist with different preConditions.
200
+ *
198
201
  * @param em - Entity manager
199
202
  * @param instance - Workflow instance
200
203
  * @param fromStepId - Current step ID
201
204
  * @param context - Evaluation context
202
- * @returns Array of evaluation results for all transitions
205
+ * @returns Array of evaluation results for all transitions, sorted by priority (desc)
203
206
  */
204
207
  export async function findValidTransitions(
205
208
  em: EntityManager,
@@ -217,16 +220,17 @@ export async function findValidTransitions(
217
220
  return []
218
221
  }
219
222
 
220
- // Find all transitions from current step
221
- const transitions = (definition.definition.transitions || []).filter(
222
- (t: any) => t.fromStepId === fromStepId
223
- )
223
+ // Find all transitions from current step, sorted by priority (highest first)
224
+ const transitions = (definition.definition.transitions || [])
225
+ .filter((t: any) => t.fromStepId === fromStepId)
226
+ .sort((a: any, b: any) => (b.priority || 0) - (a.priority || 0))
224
227
 
225
- // Evaluate each transition
228
+ // Evaluate each transition including preConditions
226
229
  const results: TransitionEvaluationResult[] = []
227
230
 
228
231
  for (const transition of transitions) {
229
- const result = await evaluateTransition(
232
+ // First check inline condition
233
+ const conditionResult = await evaluateTransition(
230
234
  em,
231
235
  instance,
232
236
  fromStepId,
@@ -234,7 +238,42 @@ export async function findValidTransitions(
234
238
  context
235
239
  )
236
240
 
237
- results.push(result)
241
+ if (!conditionResult.isValid) {
242
+ results.push(conditionResult)
243
+ continue
244
+ }
245
+
246
+ // Also evaluate preConditions if they exist
247
+ const preConditions = transition.preConditions || []
248
+ if (preConditions.length > 0) {
249
+ const preConditionsResult = await evaluatePreConditions(
250
+ em,
251
+ instance,
252
+ transition,
253
+ context as TransitionExecutionContext
254
+ )
255
+
256
+ if (!preConditionsResult.allowed) {
257
+ // Transition is invalid due to preConditions
258
+ const failedRules = preConditionsResult.executedRules
259
+ .filter((r) => !r.conditionResult)
260
+ .map((r) => r.rule.ruleId || r.rule.ruleName)
261
+
262
+ results.push({
263
+ isValid: false,
264
+ transition,
265
+ reason: `Pre-conditions failed: ${failedRules.join(', ')}`,
266
+ failedConditions: failedRules,
267
+ })
268
+ continue
269
+ }
270
+ }
271
+
272
+ // Transition is valid (both condition and preConditions passed)
273
+ results.push({
274
+ ...conditionResult,
275
+ transition,
276
+ })
238
277
  }
239
278
 
240
279
  return results
@@ -659,6 +698,10 @@ async function evaluateTransitionConditions(
659
698
  * Pre-conditions are GUARD rules that must pass before transition can execute.
660
699
  * If any GUARD rule fails, the transition is blocked.
661
700
  *
701
+ * If the transition defines specific preConditions with ruleIds, those are
702
+ * executed directly via executeRuleByRuleId. Otherwise, falls back to
703
+ * discovery-based execution via executeRules.
704
+ *
662
705
  * @param em - Entity manager
663
706
  * @param instance - Workflow instance
664
707
  * @param transition - Transition definition
@@ -686,32 +729,88 @@ async function evaluatePreConditions(
686
729
  }
687
730
  }
688
731
 
689
- // Build rule engine context
690
- const ruleContext: RuleEngineContext = {
691
- entityType: `workflow:${definition.workflowId}:transition`,
692
- entityId: transition.transitionId || `${transition.fromStepId}->${transition.toStepId}`,
693
- eventType: 'pre_transition',
694
- data: {
695
- workflowInstanceId: instance.id,
696
- workflowId: definition.workflowId,
697
- fromStepId: transition.fromStepId,
698
- toStepId: transition.toStepId,
699
- workflowContext: {
700
- ...instance.context,
701
- ...context.workflowContext,
702
- },
703
- triggerData: context.triggerData,
704
- },
705
- user: context.userId ? { id: context.userId } : undefined,
706
- tenantId: instance.tenantId,
707
- organizationId: instance.organizationId,
708
- executedBy: context.userId,
732
+ // Check if transition has specific preConditions defined
733
+ const preConditions = transition.preConditions || []
734
+
735
+ // If no pre-conditions defined, allow transition
736
+ if (preConditions.length === 0) {
737
+ return {
738
+ allowed: true,
739
+ executedRules: [],
740
+ totalExecutionTime: 0,
741
+ }
709
742
  }
710
743
 
711
- // Execute rules - only GUARD rules will affect the 'allowed' status
712
- const result = await ruleEngine.executeRules(em, ruleContext, { eventBus })
744
+ // Execute each pre-condition rule directly by ruleId
745
+ const startTime = Date.now()
746
+ const executedRules: ruleEngine.RuleExecutionResult[] = []
747
+ const errors: string[] = []
748
+ let allowed = true
713
749
 
714
- return result
750
+ for (const condition of preConditions) {
751
+ const result = await ruleEngine.executeRuleByRuleId(em, {
752
+ ruleId: condition.ruleId, // String identifier
753
+ data: {
754
+ workflowInstanceId: instance.id,
755
+ workflowId: definition.workflowId,
756
+ fromStepId: transition.fromStepId,
757
+ toStepId: transition.toStepId,
758
+ workflowContext: {
759
+ ...instance.context,
760
+ ...context.workflowContext,
761
+ },
762
+ triggerData: context.triggerData,
763
+ },
764
+ user: context.userId ? { id: context.userId } : undefined,
765
+ tenantId: instance.tenantId,
766
+ organizationId: instance.organizationId,
767
+ executedBy: context.userId,
768
+ entityType: `workflow:${definition.workflowId}:transition`,
769
+ entityId: transition.transitionId || `${transition.fromStepId}->${transition.toStepId}`,
770
+ eventType: 'pre_transition',
771
+ })
772
+
773
+ // Create a compatible RuleExecutionResult for tracking
774
+ // We don't have the full BusinessRule entity, but we can create a partial result
775
+ const ruleResult: ruleEngine.RuleExecutionResult = {
776
+ rule: {
777
+ ruleId: result.ruleId,
778
+ ruleName: result.ruleName,
779
+ ruleType: 'GUARD',
780
+ } as any,
781
+ conditionResult: result.conditionResult,
782
+ actionsExecuted: result.actionsExecuted,
783
+ executionTime: result.executionTime,
784
+ error: result.error,
785
+ logId: result.logId,
786
+ }
787
+ executedRules.push(ruleResult)
788
+
789
+ // Handle rule errors
790
+ if (result.error) {
791
+ // Rule not found, disabled, or other errors
792
+ const isRequired = condition.required !== false // Default to required
793
+ if (isRequired) {
794
+ allowed = false
795
+ errors.push(`Rule '${result.ruleId}': ${result.error}`)
796
+ }
797
+ continue
798
+ }
799
+
800
+ // If required and condition failed, block transition
801
+ const isRequired = condition.required !== false // Default to required
802
+ if (isRequired && !result.conditionResult) {
803
+ allowed = false
804
+ errors.push(`Pre-condition '${result.ruleName || result.ruleId}' failed`)
805
+ }
806
+ }
807
+
808
+ return {
809
+ allowed,
810
+ executedRules,
811
+ totalExecutionTime: Date.now() - startTime,
812
+ errors: errors.length > 0 ? errors : undefined,
813
+ }
715
814
  } catch (error) {
716
815
  console.error('Error evaluating pre-conditions:', error)
717
816
  return {
@@ -729,6 +828,9 @@ async function evaluatePreConditions(
729
828
  * Post-conditions are GUARD rules that should pass after transition executes.
730
829
  * Unlike pre-conditions, post-condition failures are logged but don't block the transition.
731
830
  *
831
+ * If the transition defines specific postConditions with ruleIds, those are
832
+ * executed directly via executeRuleByRuleId. Otherwise, returns allowed: true.
833
+ *
732
834
  * @param em - Entity manager
733
835
  * @param instance - Workflow instance
734
836
  * @param transition - Transition definition
@@ -756,32 +858,83 @@ async function evaluatePostConditions(
756
858
  }
757
859
  }
758
860
 
759
- // Build rule engine context
760
- const ruleContext: RuleEngineContext = {
761
- entityType: `workflow:${definition.workflowId}:transition`,
762
- entityId: transition.transitionId || `${transition.fromStepId}->${transition.toStepId}`,
763
- eventType: 'post_transition',
764
- data: {
765
- workflowInstanceId: instance.id,
766
- workflowId: definition.workflowId,
767
- fromStepId: transition.fromStepId,
768
- toStepId: transition.toStepId,
769
- workflowContext: {
770
- ...instance.context,
771
- ...context.workflowContext,
772
- },
773
- triggerData: context.triggerData,
774
- },
775
- user: context.userId ? { id: context.userId } : undefined,
776
- tenantId: instance.tenantId,
777
- organizationId: instance.organizationId,
778
- executedBy: context.userId,
861
+ // Check if transition has specific postConditions defined
862
+ const postConditions = transition.postConditions || []
863
+
864
+ // If no post-conditions defined, allow
865
+ if (postConditions.length === 0) {
866
+ return {
867
+ allowed: true,
868
+ executedRules: [],
869
+ totalExecutionTime: 0,
870
+ }
779
871
  }
780
872
 
781
- // Execute rules
782
- const result = await ruleEngine.executeRules(em, ruleContext, { eventBus })
873
+ // Execute each post-condition rule directly by ruleId
874
+ const startTime = Date.now()
875
+ const executedRules: ruleEngine.RuleExecutionResult[] = []
876
+ const errors: string[] = []
877
+ let allowed = true
783
878
 
784
- return result
879
+ for (const condition of postConditions) {
880
+ const result = await ruleEngine.executeRuleByRuleId(em, {
881
+ ruleId: condition.ruleId, // String identifier
882
+ data: {
883
+ workflowInstanceId: instance.id,
884
+ workflowId: definition.workflowId,
885
+ fromStepId: transition.fromStepId,
886
+ toStepId: transition.toStepId,
887
+ workflowContext: {
888
+ ...instance.context,
889
+ ...context.workflowContext,
890
+ },
891
+ triggerData: context.triggerData,
892
+ },
893
+ user: context.userId ? { id: context.userId } : undefined,
894
+ tenantId: instance.tenantId,
895
+ organizationId: instance.organizationId,
896
+ executedBy: context.userId,
897
+ entityType: `workflow:${definition.workflowId}:transition`,
898
+ entityId: transition.transitionId || `${transition.fromStepId}->${transition.toStepId}`,
899
+ eventType: 'post_transition',
900
+ })
901
+
902
+ // Create a compatible RuleExecutionResult for tracking
903
+ const ruleResult: ruleEngine.RuleExecutionResult = {
904
+ rule: {
905
+ ruleId: result.ruleId,
906
+ ruleName: result.ruleName,
907
+ ruleType: 'GUARD',
908
+ } as any,
909
+ conditionResult: result.conditionResult,
910
+ actionsExecuted: result.actionsExecuted,
911
+ executionTime: result.executionTime,
912
+ error: result.error,
913
+ logId: result.logId,
914
+ }
915
+ executedRules.push(ruleResult)
916
+
917
+ // Handle rule errors
918
+ if (result.error) {
919
+ errors.push(`Rule '${result.ruleId}': ${result.error}`)
920
+ // Post-conditions don't block, but track the failure
921
+ allowed = false
922
+ continue
923
+ }
924
+
925
+ // Track condition failures (post-conditions are warnings, not blockers)
926
+ if (!result.conditionResult) {
927
+ allowed = false
928
+ errors.push(`Post-condition '${result.ruleName || result.ruleId}' failed`)
929
+ }
930
+ }
931
+
932
+ return {
933
+ allowed,
934
+ executedRules,
935
+ totalExecutionTime: Date.now() - startTime,
936
+ errors: errors.length > 0 ? errors : undefined,
937
+ }
785
938
  } catch (error) {
786
939
  console.error('Error evaluating post-conditions:', error)
787
940
  return {
@@ -0,0 +1,38 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260123143500 extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ // Create workflow_event_triggers table
7
+ this.addSql(`
8
+ create table "workflow_event_triggers" (
9
+ "id" uuid not null default gen_random_uuid(),
10
+ "name" varchar(255) not null,
11
+ "description" text null,
12
+ "workflow_definition_id" uuid not null,
13
+ "event_pattern" varchar(255) not null,
14
+ "config" jsonb null,
15
+ "enabled" bool not null default true,
16
+ "priority" int4 not null default 0,
17
+ "tenant_id" uuid not null,
18
+ "organization_id" uuid not null,
19
+ "created_by" varchar(255) null,
20
+ "updated_by" varchar(255) null,
21
+ "created_at" timestamptz(6) not null,
22
+ "updated_at" timestamptz(6) not null,
23
+ "deleted_at" timestamptz(6) null,
24
+ constraint "workflow_event_triggers_pkey" primary key ("id")
25
+ );
26
+ `);
27
+
28
+ // Create indexes
29
+ this.addSql(`create index "workflow_event_triggers_event_pattern_idx" on "workflow_event_triggers" ("event_pattern", "enabled");`);
30
+ this.addSql(`create index "workflow_event_triggers_definition_idx" on "workflow_event_triggers" ("workflow_definition_id");`);
31
+ this.addSql(`create index "workflow_event_triggers_tenant_org_idx" on "workflow_event_triggers" ("tenant_id", "organization_id");`);
32
+ this.addSql(`create index "workflow_event_triggers_enabled_priority_idx" on "workflow_event_triggers" ("enabled", "priority");`);
33
+ }
34
+
35
+ override async down(): Promise<void> {
36
+ this.addSql(`drop table if exists "workflow_event_triggers" cascade;`);
37
+ }
38
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Workflows Module - Event Trigger Subscriber
3
+ *
4
+ * Wildcard subscriber that listens to all events and evaluates
5
+ * workflow event triggers. When a matching trigger is found,
6
+ * the corresponding workflow is started with mapped context.
7
+ */
8
+
9
+ import type { EntityManager } from '@mikro-orm/core'
10
+ import type { AwilixContainer } from 'awilix'
11
+
12
+ export const metadata = {
13
+ event: '*', // Subscribe to ALL events
14
+ persistent: true, // Ensure reliability
15
+ id: 'workflows:event-trigger',
16
+ }
17
+
18
+ // Events that should never trigger workflows (internal/system events)
19
+ const EXCLUDED_EVENT_PREFIXES = [
20
+ 'query_index.', // Internal indexing events
21
+ 'search.', // Internal search events
22
+ 'workflows.', // Workflow internal events (avoid recursion)
23
+ 'cache.', // Cache events
24
+ 'queue.', // Queue events
25
+ ]
26
+
27
+ /**
28
+ * Check if an event should be excluded from trigger processing.
29
+ */
30
+ function isExcludedEvent(eventName: string): boolean {
31
+ return EXCLUDED_EVENT_PREFIXES.some(prefix => eventName.startsWith(prefix))
32
+ }
33
+
34
+ export default async function handle(
35
+ payload: unknown,
36
+ ctx: { resolve: <T = unknown>(name: string) => T; eventName?: string }
37
+ ): Promise<void> {
38
+ const eventName = ctx.eventName
39
+ if (!eventName) {
40
+ // Skip if no event name (shouldn't happen, but be safe)
41
+ return
42
+ }
43
+
44
+ // Skip excluded events
45
+ if (isExcludedEvent(eventName)) {
46
+ return
47
+ }
48
+
49
+ // Ensure payload is an object
50
+ const eventPayload = (payload && typeof payload === 'object' ? payload : {}) as Record<string, unknown>
51
+
52
+ // Extract tenant/org from payload
53
+ const tenantId = eventPayload?.tenantId as string | undefined
54
+ const organizationId = eventPayload?.organizationId as string | undefined
55
+
56
+ // Skip events without tenant context
57
+ if (!tenantId || !organizationId) {
58
+ return
59
+ }
60
+
61
+ // Get dependencies from container
62
+ let em: EntityManager
63
+ let container: AwilixContainer
64
+
65
+ try {
66
+ em = ctx.resolve<EntityManager>('em')
67
+ // Create a minimal container wrapper using the resolve function
68
+ // This avoids the need to register 'container' as a self-reference in DI
69
+ container = {
70
+ resolve: ctx.resolve,
71
+ // Provide minimal AwilixContainer interface for type compatibility
72
+ cradle: new Proxy({}, {
73
+ get: (_target, prop: string) => ctx.resolve(prop),
74
+ }),
75
+ } as unknown as AwilixContainer
76
+ } catch (error) {
77
+ // DI not available - skip
78
+ console.warn(`[workflow-trigger] Cannot resolve dependencies for event "${eventName}":`, error)
79
+ return
80
+ }
81
+
82
+ // Import service dynamically to avoid circular dependencies
83
+ const { processEventTriggers } = await import('../lib/event-trigger-service')
84
+
85
+ try {
86
+ const result = await processEventTriggers(em, container, {
87
+ eventName,
88
+ payload: eventPayload,
89
+ tenantId,
90
+ organizationId,
91
+ })
92
+
93
+ if (result.triggered > 0) {
94
+ console.log(
95
+ `[workflow-trigger] Triggered ${result.triggered} workflow(s) for "${eventName}"` +
96
+ (result.skipped > 0 ? ` (${result.skipped} skipped)` : '') +
97
+ (result.errors.length > 0 ? ` (${result.errors.length} errors)` : '')
98
+ )
99
+ }
100
+
101
+ if (result.errors.length > 0) {
102
+ for (const err of result.errors) {
103
+ console.error(`[workflow-trigger] Trigger ${err.triggerId} failed:`, err.error)
104
+ }
105
+ }
106
+ } catch (error) {
107
+ console.error(`[workflow-trigger] Error processing triggers for "${eventName}":`, error)
108
+ }
109
+ }