@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.
- 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 +59 -58
- 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 +22 -5
- 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 +59 -58
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +3 -5
- 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 +74 -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/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/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 +34 -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
|
@@ -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
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -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 || [])
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
|
701
|
-
const
|
|
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
|
-
|
|
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
|
-
//
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
|
770
|
-
const
|
|
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
|
-
|
|
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
|
+
}
|