@open-mercato/core 0.4.2-canary-51881f6bf3 → 0.4.2-canary-5f415b8a44

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 (155) 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 +59 -58
  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 +22 -5
  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 +59 -58
  104. package/generated/entity-fields-registry.ts +2 -0
  105. package/package.json +3 -5
  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 +74 -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/instances/route.ts +21 -7
  122. package/src/modules/workflows/api/tasks/route.ts +7 -1
  123. package/src/modules/workflows/backend/definitions/[id]/page.meta.ts +1 -1
  124. package/src/modules/workflows/backend/definitions/[id]/page.tsx +9 -0
  125. package/src/modules/workflows/backend/definitions/create/page.meta.ts +1 -1
  126. package/src/modules/workflows/backend/definitions/create/page.tsx +9 -0
  127. package/src/modules/workflows/backend/definitions/visual-editor/page.meta.ts +1 -1
  128. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +21 -3
  129. package/src/modules/workflows/backend/events/[id]/page.meta.ts +2 -2
  130. package/src/modules/workflows/backend/events/[id]/page.tsx +1 -1
  131. package/src/modules/workflows/backend/instances/[id]/page.meta.ts +2 -2
  132. package/src/modules/workflows/backend/tasks/[id]/page.meta.ts +2 -2
  133. package/src/modules/workflows/backend/tasks/[id]/page.tsx +1 -1
  134. package/src/modules/workflows/backend/tasks/page.tsx +5 -6
  135. package/src/modules/workflows/cli.ts +111 -0
  136. package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +581 -0
  137. package/src/modules/workflows/components/EventTriggersEditor.tsx +664 -0
  138. package/src/modules/workflows/data/entities.ts +124 -0
  139. package/src/modules/workflows/data/validators.ts +138 -0
  140. package/src/modules/workflows/events.ts +49 -0
  141. package/src/modules/workflows/examples/checkout-demo-definition.json +1 -5
  142. package/src/modules/workflows/examples/order-approval-definition.json +257 -0
  143. package/src/modules/workflows/examples/order-approval-guard-rules.json +32 -0
  144. package/src/modules/workflows/i18n/en.json +71 -0
  145. package/src/modules/workflows/lib/activity-executor.ts +129 -16
  146. package/src/modules/workflows/lib/event-trigger-service.ts +557 -0
  147. package/src/modules/workflows/lib/graph-utils.ts +117 -2
  148. package/src/modules/workflows/lib/seeds.ts +34 -8
  149. package/src/modules/workflows/lib/start-validator.ts +38 -28
  150. package/src/modules/workflows/lib/transition-handler.ts +208 -55
  151. package/src/modules/workflows/migrations/Migration20260123143500.ts +38 -0
  152. package/src/modules/workflows/subscribers/event-trigger.ts +109 -0
  153. package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +446 -0
  154. package/src/modules/workflows/widgets/injection/order-approval/widget.ts +16 -0
  155. package/src/modules/workflows/widgets/injection-table.ts +21 -0
@@ -7,6 +7,8 @@ import checkoutDemoDefinition from '../examples/checkout-demo-definition.json'
7
7
  import guardRulesExample from '../examples/guard-rules-example.json'
8
8
  import salesPipelineDefinition from '../examples/sales-pipeline-definition.json'
9
9
  import simpleApprovalDefinition from '../examples/simple-approval-definition.json'
10
+ import orderApprovalDefinition from '../examples/order-approval-definition.json'
11
+ import orderApprovalGuardRules from '../examples/order-approval-guard-rules.json'
10
12
 
11
13
  export type WorkflowSeedScope = { tenantId: string; organizationId: string }
12
14
 
@@ -51,6 +53,8 @@ const embeddedSeeds: Record<string, unknown> = {
51
53
  'guard-rules-example.json': guardRulesExample,
52
54
  'sales-pipeline-definition.json': salesPipelineDefinition,
53
55
  'simple-approval-definition.json': simpleApprovalDefinition,
56
+ 'order-approval-definition.json': orderApprovalDefinition,
57
+ 'order-approval-guard-rules.json': orderApprovalGuardRules,
54
58
  }
55
59
 
56
60
  function readExampleJson<T>(fileName: string): T {
@@ -90,16 +94,35 @@ async function seedWorkflowDefinition(
90
94
  })
91
95
 
92
96
  if (existing) {
93
- // Check if the definition needs to be updated (e.g., missing preConditions on START step)
97
+ // Check if the definition needs to be updated by comparing steps and transitions
98
+ const seedStepCount = seed.definition.steps.length
99
+ const existingStepCount = existing.definition.steps.length
100
+ const seedTransitionCount = seed.definition.transitions.length
101
+ const existingTransitionCount = existing.definition.transitions.length
102
+
103
+ // Check for preConditions on transitions
104
+ const seedHasTransitionPreConditions = seed.definition.transitions.some(
105
+ (t: any) => t.preConditions && t.preConditions.length > 0
106
+ )
107
+ const existingHasTransitionPreConditions = existing.definition.transitions.some(
108
+ (t: any) => t.preConditions && t.preConditions.length > 0
109
+ )
110
+
111
+ // Check for preConditions on START step
94
112
  const seedStartStep = seed.definition.steps.find((s: any) => s.stepType === 'START')
95
113
  const existingStartStep = existing.definition.steps.find((s: any) => s.stepType === 'START')
96
-
97
- const seedHasPreConditions = seedStartStep?.preConditions && seedStartStep.preConditions.length > 0
98
- const existingHasPreConditions = existingStartStep?.preConditions && existingStartStep.preConditions.length > 0
99
-
100
- // Update if seed has preConditions but existing doesn't
101
- if (seedHasPreConditions && !existingHasPreConditions) {
102
- console.log(`[seed] Updating workflow ${workflowId} with preConditions`)
114
+ const seedHasStartPreConditions = seedStartStep?.preConditions && seedStartStep.preConditions.length > 0
115
+ const existingHasStartPreConditions = existingStartStep?.preConditions && existingStartStep.preConditions.length > 0
116
+
117
+ // Update if structure has changed
118
+ const needsUpdate =
119
+ seedStepCount !== existingStepCount ||
120
+ seedTransitionCount !== existingTransitionCount ||
121
+ (seedHasStartPreConditions && !existingHasStartPreConditions) ||
122
+ (seedHasTransitionPreConditions && !existingHasTransitionPreConditions)
123
+
124
+ if (needsUpdate) {
125
+ console.log(`[seed] Updating workflow ${workflowId} (steps: ${existingStepCount}→${seedStepCount}, transitions: ${existingTransitionCount}→${seedTransitionCount})`)
103
126
  existing.definition = seed.definition
104
127
  await em.flush()
105
128
  return true
@@ -172,4 +195,7 @@ export async function seedExampleWorkflows(em: EntityManager, scope: WorkflowSee
172
195
  await seedGuardRules(em, scope, 'guard-rules-example.json')
173
196
  await seedWorkflowDefinition(em, scope, 'sales-pipeline-definition.json')
174
197
  await seedWorkflowDefinition(em, scope, 'simple-approval-definition.json')
198
+ // Seed order approval guard rules before the workflow definition
199
+ await seedGuardRules(em, scope, 'order-approval-guard-rules.json')
200
+ await seedWorkflowDefinition(em, scope, 'order-approval-definition.json')
175
201
  }
@@ -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,
@@ -19,7 +19,6 @@ import {
19
19
  } from '../data/entities'
20
20
  import * as ruleEvaluator from '../../business_rules/lib/rule-evaluator'
21
21
  import * as ruleEngine from '../../business_rules/lib/rule-engine'
22
- import type { RuleEngineContext } from '../../business_rules/lib/rule-engine'
23
22
  import * as activityExecutor from './activity-executor'
24
23
  import type { ActivityDefinition } from './activity-executor'
25
24
  import * as stepHandler from './step-handler'
@@ -194,11 +193,15 @@ export async function evaluateTransition(
194
193
  /**
195
194
  * Find all valid transitions from current step
196
195
  *
196
+ * This function evaluates both inline conditions AND preConditions (business rules)
197
+ * to determine which transitions are truly valid. This is important for decision
198
+ * branching where multiple transitions exist with different preConditions.
199
+ *
197
200
  * @param em - Entity manager
198
201
  * @param instance - Workflow instance
199
202
  * @param fromStepId - Current step ID
200
203
  * @param context - Evaluation context
201
- * @returns Array of evaluation results for all transitions
204
+ * @returns Array of evaluation results for all transitions, sorted by priority (desc)
202
205
  */
203
206
  export async function findValidTransitions(
204
207
  em: EntityManager,
@@ -216,16 +219,17 @@ export async function findValidTransitions(
216
219
  return []
217
220
  }
218
221
 
219
- // Find all transitions from current step
220
- const transitions = (definition.definition.transitions || []).filter(
221
- (t: any) => t.fromStepId === fromStepId
222
- )
222
+ // Find all transitions from current step, sorted by priority (highest first)
223
+ const transitions = (definition.definition.transitions || [])
224
+ .filter((t: any) => t.fromStepId === fromStepId)
225
+ .sort((a: any, b: any) => (b.priority || 0) - (a.priority || 0))
223
226
 
224
- // Evaluate each transition
227
+ // Evaluate each transition including preConditions
225
228
  const results: TransitionEvaluationResult[] = []
226
229
 
227
230
  for (const transition of transitions) {
228
- const result = await evaluateTransition(
231
+ // First check inline condition
232
+ const conditionResult = await evaluateTransition(
229
233
  em,
230
234
  instance,
231
235
  fromStepId,
@@ -233,7 +237,42 @@ export async function findValidTransitions(
233
237
  context
234
238
  )
235
239
 
236
- results.push(result)
240
+ if (!conditionResult.isValid) {
241
+ results.push(conditionResult)
242
+ continue
243
+ }
244
+
245
+ // Also evaluate preConditions if they exist
246
+ const preConditions = transition.preConditions || []
247
+ if (preConditions.length > 0) {
248
+ const preConditionsResult = await evaluatePreConditions(
249
+ em,
250
+ instance,
251
+ transition,
252
+ context as TransitionExecutionContext
253
+ )
254
+
255
+ if (!preConditionsResult.allowed) {
256
+ // Transition is invalid due to preConditions
257
+ const failedRules = preConditionsResult.executedRules
258
+ .filter((r) => !r.conditionResult)
259
+ .map((r) => r.rule.ruleId || r.rule.ruleName)
260
+
261
+ results.push({
262
+ isValid: false,
263
+ transition,
264
+ reason: `Pre-conditions failed: ${failedRules.join(', ')}`,
265
+ failedConditions: failedRules,
266
+ })
267
+ continue
268
+ }
269
+ }
270
+
271
+ // Transition is valid (both condition and preConditions passed)
272
+ results.push({
273
+ ...conditionResult,
274
+ transition,
275
+ })
237
276
  }
238
277
 
239
278
  return results
@@ -649,6 +688,10 @@ async function evaluateTransitionConditions(
649
688
  * Pre-conditions are GUARD rules that must pass before transition can execute.
650
689
  * If any GUARD rule fails, the transition is blocked.
651
690
  *
691
+ * If the transition defines specific preConditions with ruleIds, those are
692
+ * executed directly via executeRuleByRuleId. Otherwise, falls back to
693
+ * discovery-based execution via executeRules.
694
+ *
652
695
  * @param em - Entity manager
653
696
  * @param instance - Workflow instance
654
697
  * @param transition - Transition definition
@@ -675,32 +718,88 @@ async function evaluatePreConditions(
675
718
  }
676
719
  }
677
720
 
678
- // Build rule engine context
679
- const ruleContext: RuleEngineContext = {
680
- entityType: `workflow:${definition.workflowId}:transition`,
681
- entityId: transition.transitionId || `${transition.fromStepId}->${transition.toStepId}`,
682
- eventType: 'pre_transition',
683
- data: {
684
- workflowInstanceId: instance.id,
685
- workflowId: definition.workflowId,
686
- fromStepId: transition.fromStepId,
687
- toStepId: transition.toStepId,
688
- workflowContext: {
689
- ...instance.context,
690
- ...context.workflowContext,
691
- },
692
- triggerData: context.triggerData,
693
- },
694
- user: context.userId ? { id: context.userId } : undefined,
695
- tenantId: instance.tenantId,
696
- organizationId: instance.organizationId,
697
- executedBy: context.userId,
721
+ // Check if transition has specific preConditions defined
722
+ const preConditions = transition.preConditions || []
723
+
724
+ // If no pre-conditions defined, allow transition
725
+ if (preConditions.length === 0) {
726
+ return {
727
+ allowed: true,
728
+ executedRules: [],
729
+ totalExecutionTime: 0,
730
+ }
698
731
  }
699
732
 
700
- // Execute rules - only GUARD rules will affect the 'allowed' status
701
- const result = await ruleEngine.executeRules(em, ruleContext)
733
+ // Execute each pre-condition rule directly by ruleId
734
+ const startTime = Date.now()
735
+ const executedRules: ruleEngine.RuleExecutionResult[] = []
736
+ const errors: string[] = []
737
+ let allowed = true
702
738
 
703
- return result
739
+ for (const condition of preConditions) {
740
+ const result = await ruleEngine.executeRuleByRuleId(em, {
741
+ ruleId: condition.ruleId, // String identifier
742
+ data: {
743
+ workflowInstanceId: instance.id,
744
+ workflowId: definition.workflowId,
745
+ fromStepId: transition.fromStepId,
746
+ toStepId: transition.toStepId,
747
+ workflowContext: {
748
+ ...instance.context,
749
+ ...context.workflowContext,
750
+ },
751
+ triggerData: context.triggerData,
752
+ },
753
+ user: context.userId ? { id: context.userId } : undefined,
754
+ tenantId: instance.tenantId,
755
+ organizationId: instance.organizationId,
756
+ executedBy: context.userId,
757
+ entityType: `workflow:${definition.workflowId}:transition`,
758
+ entityId: transition.transitionId || `${transition.fromStepId}->${transition.toStepId}`,
759
+ eventType: 'pre_transition',
760
+ })
761
+
762
+ // Create a compatible RuleExecutionResult for tracking
763
+ // We don't have the full BusinessRule entity, but we can create a partial result
764
+ const ruleResult: ruleEngine.RuleExecutionResult = {
765
+ rule: {
766
+ ruleId: result.ruleId,
767
+ ruleName: result.ruleName,
768
+ ruleType: 'GUARD',
769
+ } as any,
770
+ conditionResult: result.conditionResult,
771
+ actionsExecuted: result.actionsExecuted,
772
+ executionTime: result.executionTime,
773
+ error: result.error,
774
+ logId: result.logId,
775
+ }
776
+ executedRules.push(ruleResult)
777
+
778
+ // Handle rule errors
779
+ if (result.error) {
780
+ // Rule not found, disabled, or other errors
781
+ const isRequired = condition.required !== false // Default to required
782
+ if (isRequired) {
783
+ allowed = false
784
+ errors.push(`Rule '${result.ruleId}': ${result.error}`)
785
+ }
786
+ continue
787
+ }
788
+
789
+ // If required and condition failed, block transition
790
+ const isRequired = condition.required !== false // Default to required
791
+ if (isRequired && !result.conditionResult) {
792
+ allowed = false
793
+ errors.push(`Pre-condition '${result.ruleName || result.ruleId}' failed`)
794
+ }
795
+ }
796
+
797
+ return {
798
+ allowed,
799
+ executedRules,
800
+ totalExecutionTime: Date.now() - startTime,
801
+ errors: errors.length > 0 ? errors : undefined,
802
+ }
704
803
  } catch (error) {
705
804
  console.error('Error evaluating pre-conditions:', error)
706
805
  return {
@@ -718,6 +817,9 @@ async function evaluatePreConditions(
718
817
  * Post-conditions are GUARD rules that should pass after transition executes.
719
818
  * Unlike pre-conditions, post-condition failures are logged but don't block the transition.
720
819
  *
820
+ * If the transition defines specific postConditions with ruleIds, those are
821
+ * executed directly via executeRuleByRuleId. Otherwise, returns allowed: true.
822
+ *
721
823
  * @param em - Entity manager
722
824
  * @param instance - Workflow instance
723
825
  * @param transition - Transition definition
@@ -744,32 +846,83 @@ async function evaluatePostConditions(
744
846
  }
745
847
  }
746
848
 
747
- // Build rule engine context
748
- const ruleContext: RuleEngineContext = {
749
- entityType: `workflow:${definition.workflowId}:transition`,
750
- entityId: transition.transitionId || `${transition.fromStepId}->${transition.toStepId}`,
751
- eventType: 'post_transition',
752
- data: {
753
- workflowInstanceId: instance.id,
754
- workflowId: definition.workflowId,
755
- fromStepId: transition.fromStepId,
756
- toStepId: transition.toStepId,
757
- workflowContext: {
758
- ...instance.context,
759
- ...context.workflowContext,
760
- },
761
- triggerData: context.triggerData,
762
- },
763
- user: context.userId ? { id: context.userId } : undefined,
764
- tenantId: instance.tenantId,
765
- organizationId: instance.organizationId,
766
- executedBy: context.userId,
849
+ // Check if transition has specific postConditions defined
850
+ const postConditions = transition.postConditions || []
851
+
852
+ // If no post-conditions defined, allow
853
+ if (postConditions.length === 0) {
854
+ return {
855
+ allowed: true,
856
+ executedRules: [],
857
+ totalExecutionTime: 0,
858
+ }
767
859
  }
768
860
 
769
- // Execute rules
770
- const result = await ruleEngine.executeRules(em, ruleContext)
861
+ // Execute each post-condition rule directly by ruleId
862
+ const startTime = Date.now()
863
+ const executedRules: ruleEngine.RuleExecutionResult[] = []
864
+ const errors: string[] = []
865
+ let allowed = true
771
866
 
772
- return result
867
+ for (const condition of postConditions) {
868
+ const result = await ruleEngine.executeRuleByRuleId(em, {
869
+ ruleId: condition.ruleId, // String identifier
870
+ data: {
871
+ workflowInstanceId: instance.id,
872
+ workflowId: definition.workflowId,
873
+ fromStepId: transition.fromStepId,
874
+ toStepId: transition.toStepId,
875
+ workflowContext: {
876
+ ...instance.context,
877
+ ...context.workflowContext,
878
+ },
879
+ triggerData: context.triggerData,
880
+ },
881
+ user: context.userId ? { id: context.userId } : undefined,
882
+ tenantId: instance.tenantId,
883
+ organizationId: instance.organizationId,
884
+ executedBy: context.userId,
885
+ entityType: `workflow:${definition.workflowId}:transition`,
886
+ entityId: transition.transitionId || `${transition.fromStepId}->${transition.toStepId}`,
887
+ eventType: 'post_transition',
888
+ })
889
+
890
+ // Create a compatible RuleExecutionResult for tracking
891
+ const ruleResult: ruleEngine.RuleExecutionResult = {
892
+ rule: {
893
+ ruleId: result.ruleId,
894
+ ruleName: result.ruleName,
895
+ ruleType: 'GUARD',
896
+ } as any,
897
+ conditionResult: result.conditionResult,
898
+ actionsExecuted: result.actionsExecuted,
899
+ executionTime: result.executionTime,
900
+ error: result.error,
901
+ logId: result.logId,
902
+ }
903
+ executedRules.push(ruleResult)
904
+
905
+ // Handle rule errors
906
+ if (result.error) {
907
+ errors.push(`Rule '${result.ruleId}': ${result.error}`)
908
+ // Post-conditions don't block, but track the failure
909
+ allowed = false
910
+ continue
911
+ }
912
+
913
+ // Track condition failures (post-conditions are warnings, not blockers)
914
+ if (!result.conditionResult) {
915
+ allowed = false
916
+ errors.push(`Post-condition '${result.ruleName || result.ruleId}' failed`)
917
+ }
918
+ }
919
+
920
+ return {
921
+ allowed,
922
+ executedRules,
923
+ totalExecutionTime: Date.now() - startTime,
924
+ errors: errors.length > 0 ? errors : undefined,
925
+ }
773
926
  } catch (error) {
774
927
  console.error('Error evaluating post-conditions:', error)
775
928
  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
+ }