@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.
- package/dist/generated/entities/workflow_event_trigger/index.js +33 -0
- package/dist/generated/entities/workflow_event_trigger/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +1 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/auth/events.js +30 -0
- package/dist/modules/auth/events.js.map +7 -0
- package/dist/modules/business_rules/api/execute/[ruleId]/route.js +145 -0
- package/dist/modules/business_rules/api/execute/[ruleId]/route.js.map +7 -0
- package/dist/modules/business_rules/data/validators.js +34 -0
- package/dist/modules/business_rules/data/validators.js.map +2 -2
- package/dist/modules/business_rules/index.js +21 -1
- package/dist/modules/business_rules/index.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-engine.js +182 -1
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/catalog/events.js +34 -0
- package/dist/modules/catalog/events.js.map +7 -0
- package/dist/modules/customers/events.js +49 -0
- package/dist/modules/customers/events.js.map +7 -0
- package/dist/modules/directory/events.js +23 -0
- package/dist/modules/directory/events.js.map +7 -0
- package/dist/modules/sales/acl.js +1 -0
- package/dist/modules/sales/acl.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +12 -0
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +62 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/events.js +63 -0
- package/dist/modules/sales/events.js.map +7 -0
- package/dist/modules/sales/lib/dictionaries.js +3 -0
- package/dist/modules/sales/lib/dictionaries.js.map +2 -2
- package/dist/modules/sales/lib/frontend/documentDataEvents.js +25 -0
- package/dist/modules/sales/lib/frontend/documentDataEvents.js.map +7 -0
- package/dist/modules/workflows/acl.js +2 -0
- package/dist/modules/workflows/acl.js.map +2 -2
- package/dist/modules/workflows/api/instances/route.js +18 -6
- package/dist/modules/workflows/api/instances/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/route.js +6 -1
- package/dist/modules/workflows/api/tasks/route.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/[id]/page.js +9 -1
- package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/[id]/page.meta.js +1 -1
- package/dist/modules/workflows/backend/definitions/[id]/page.meta.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/create/page.js +24 -15
- package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/create/page.meta.js +1 -1
- package/dist/modules/workflows/backend/definitions/create/page.meta.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +150 -132
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.meta.js +1 -1
- package/dist/modules/workflows/backend/definitions/visual-editor/page.meta.js.map +2 -2
- package/dist/modules/workflows/backend/events/[id]/page.js +1 -1
- package/dist/modules/workflows/backend/events/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/events/[id]/page.meta.js +2 -2
- package/dist/modules/workflows/backend/events/[id]/page.meta.js.map +2 -2
- package/dist/modules/workflows/backend/instances/[id]/page.meta.js +2 -2
- package/dist/modules/workflows/backend/instances/[id]/page.meta.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.js +1 -1
- package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.meta.js +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.meta.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/page.js +5 -6
- package/dist/modules/workflows/backend/tasks/page.js.map +2 -2
- package/dist/modules/workflows/cli.js +81 -3
- package/dist/modules/workflows/cli.js.map +3 -3
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js +481 -0
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +7 -0
- package/dist/modules/workflows/components/EventTriggersEditor.js +553 -0
- package/dist/modules/workflows/components/EventTriggersEditor.js.map +7 -0
- package/dist/modules/workflows/data/entities.js +64 -1
- package/dist/modules/workflows/data/entities.js.map +2 -2
- package/dist/modules/workflows/data/validators.js +115 -0
- package/dist/modules/workflows/data/validators.js.map +2 -2
- package/dist/modules/workflows/events.js +38 -0
- package/dist/modules/workflows/events.js.map +7 -0
- package/dist/modules/workflows/examples/checkout-demo-definition.json +1 -5
- package/dist/modules/workflows/examples/order-approval-definition.json +257 -0
- package/dist/modules/workflows/examples/order-approval-guard-rules.json +32 -0
- package/dist/modules/workflows/lib/activity-executor.js +75 -13
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/event-trigger-service.js +308 -0
- package/dist/modules/workflows/lib/event-trigger-service.js.map +7 -0
- package/dist/modules/workflows/lib/graph-utils.js +71 -2
- package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
- package/dist/modules/workflows/lib/seeds.js +17 -4
- package/dist/modules/workflows/lib/seeds.js.map +2 -2
- package/dist/modules/workflows/lib/start-validator.js +33 -23
- package/dist/modules/workflows/lib/start-validator.js.map +2 -2
- package/dist/modules/workflows/lib/transition-handler.js +157 -45
- package/dist/modules/workflows/lib/transition-handler.js.map +3 -3
- package/dist/modules/workflows/migrations/Migration20260123143500.js +36 -0
- package/dist/modules/workflows/migrations/Migration20260123143500.js.map +7 -0
- package/dist/modules/workflows/subscribers/event-trigger.js +78 -0
- package/dist/modules/workflows/subscribers/event-trigger.js.map +7 -0
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +323 -0
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +7 -0
- package/dist/modules/workflows/widgets/injection/order-approval/widget.js +17 -0
- package/dist/modules/workflows/widgets/injection/order-approval/widget.js.map +7 -0
- package/dist/modules/workflows/widgets/injection-table.js +19 -0
- package/dist/modules/workflows/widgets/injection-table.js.map +7 -0
- package/generated/entities/workflow_event_trigger/index.ts +15 -0
- package/generated/entities.ids.generated.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +2 -2
- package/src/modules/auth/events.ts +39 -0
- package/src/modules/business_rules/api/execute/[ruleId]/route.ts +163 -0
- package/src/modules/business_rules/data/validators.ts +40 -0
- package/src/modules/business_rules/index.ts +25 -0
- package/src/modules/business_rules/lib/rule-engine.ts +281 -1
- package/src/modules/catalog/events.ts +45 -0
- package/src/modules/customers/events.ts +63 -0
- package/src/modules/directory/events.ts +31 -0
- package/src/modules/sales/acl.ts +1 -0
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +16 -0
- package/src/modules/sales/commands/documents.ts +75 -1
- package/src/modules/sales/events.ts +82 -0
- package/src/modules/sales/lib/dictionaries.ts +3 -0
- package/src/modules/sales/lib/frontend/documentDataEvents.ts +28 -0
- package/src/modules/workflows/acl.ts +2 -0
- package/src/modules/workflows/api/__tests__/instances.route.test.ts +5 -2
- package/src/modules/workflows/api/instances/route.ts +21 -7
- package/src/modules/workflows/api/tasks/route.ts +7 -1
- package/src/modules/workflows/backend/definitions/[id]/page.meta.ts +1 -1
- package/src/modules/workflows/backend/definitions/[id]/page.tsx +9 -0
- package/src/modules/workflows/backend/definitions/create/page.meta.ts +1 -1
- package/src/modules/workflows/backend/definitions/create/page.tsx +9 -0
- package/src/modules/workflows/backend/definitions/visual-editor/page.meta.ts +1 -1
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +21 -3
- package/src/modules/workflows/backend/events/[id]/page.meta.ts +2 -2
- package/src/modules/workflows/backend/events/[id]/page.tsx +1 -1
- package/src/modules/workflows/backend/instances/[id]/page.meta.ts +2 -2
- package/src/modules/workflows/backend/tasks/[id]/page.meta.ts +2 -2
- package/src/modules/workflows/backend/tasks/[id]/page.tsx +1 -1
- package/src/modules/workflows/backend/tasks/page.tsx +5 -6
- package/src/modules/workflows/cli.ts +111 -0
- package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +581 -0
- package/src/modules/workflows/components/EventTriggersEditor.tsx +664 -0
- package/src/modules/workflows/data/entities.ts +124 -0
- package/src/modules/workflows/data/validators.ts +138 -0
- package/src/modules/workflows/events.ts +49 -0
- package/src/modules/workflows/examples/checkout-demo-definition.json +1 -5
- package/src/modules/workflows/examples/order-approval-definition.json +257 -0
- package/src/modules/workflows/examples/order-approval-guard-rules.json +32 -0
- package/src/modules/workflows/i18n/en.json +71 -0
- package/src/modules/workflows/lib/__tests__/activity-executor.test.ts +43 -36
- package/src/modules/workflows/lib/__tests__/transition-handler.test.ts +170 -90
- package/src/modules/workflows/lib/activity-executor.ts +129 -16
- package/src/modules/workflows/lib/event-trigger-service.ts +557 -0
- package/src/modules/workflows/lib/graph-utils.ts +117 -2
- package/src/modules/workflows/lib/seeds.ts +29 -8
- package/src/modules/workflows/lib/start-validator.ts +38 -28
- package/src/modules/workflows/lib/transition-handler.ts +208 -55
- package/src/modules/workflows/migrations/Migration20260123143500.ts +38 -0
- package/src/modules/workflows/subscribers/event-trigger.ts +109 -0
- package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +446 -0
- package/src/modules/workflows/widgets/injection/order-approval/widget.ts +16 -0
- 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
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
146
|
+
validatedRules.push({
|
|
147
|
+
ruleId: condition.ruleId,
|
|
148
|
+
passed: result.conditionResult,
|
|
149
|
+
executionTime: result.executionTime,
|
|
150
|
+
})
|
|
154
151
|
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
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
|
-
//
|
|
169
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
188
|
+
// Get localized message from condition or use default with rule name
|
|
179
189
|
const message = getLocalizedMessage(
|
|
180
190
|
condition,
|
|
181
|
-
|
|
191
|
+
null,
|
|
182
192
|
locale,
|
|
183
|
-
`Pre-condition '${
|
|
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 || [])
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
|
712
|
-
const
|
|
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
|
-
|
|
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
|
-
//
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
|
782
|
-
const
|
|
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
|
-
|
|
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
|
+
}
|