@open-mercato/core 0.4.2-canary-d0a025141f → 0.4.2-canary-3efa759f5c
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.ids.generated.js +0 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +0 -2
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/business_rules/data/validators.js +0 -34
- package/dist/modules/business_rules/data/validators.js.map +2 -2
- package/dist/modules/business_rules/index.js +1 -21
- package/dist/modules/business_rules/index.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-engine.js +1 -182
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/sales/acl.js +0 -1
- package/dist/modules/sales/acl.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +0 -12
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +0 -62
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/lib/dictionaries.js +0 -3
- package/dist/modules/sales/lib/dictionaries.js.map +2 -2
- package/dist/modules/workflows/acl.js +0 -2
- package/dist/modules/workflows/acl.js.map +2 -2
- package/dist/modules/workflows/api/instances/route.js +6 -18
- package/dist/modules/workflows/api/instances/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/route.js +1 -6
- package/dist/modules/workflows/api/tasks/route.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/[id]/page.js +1 -9
- 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 +15 -24
- 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 +132 -150
- 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 +6 -5
- package/dist/modules/workflows/backend/tasks/page.js.map +2 -2
- package/dist/modules/workflows/cli.js +3 -81
- package/dist/modules/workflows/cli.js.map +3 -3
- package/dist/modules/workflows/data/entities.js +1 -64
- package/dist/modules/workflows/data/entities.js.map +2 -2
- package/dist/modules/workflows/data/validators.js +0 -115
- package/dist/modules/workflows/data/validators.js.map +2 -2
- package/dist/modules/workflows/examples/checkout-demo-definition.json +5 -1
- package/dist/modules/workflows/lib/activity-executor.js +13 -75
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/graph-utils.js +2 -71
- package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
- package/dist/modules/workflows/lib/seeds.js +5 -22
- package/dist/modules/workflows/lib/seeds.js.map +2 -2
- package/dist/modules/workflows/lib/start-validator.js +23 -33
- package/dist/modules/workflows/lib/start-validator.js.map +2 -2
- package/dist/modules/workflows/lib/transition-handler.js +45 -157
- package/dist/modules/workflows/lib/transition-handler.js.map +3 -3
- package/generated/entities.ids.generated.ts +0 -1
- package/generated/entity-fields-registry.ts +0 -2
- package/package.json +2 -2
- package/src/modules/business_rules/data/validators.ts +0 -40
- package/src/modules/business_rules/index.ts +0 -25
- package/src/modules/business_rules/lib/rule-engine.ts +1 -281
- package/src/modules/sales/acl.ts +0 -1
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +0 -16
- package/src/modules/sales/commands/documents.ts +1 -74
- package/src/modules/sales/lib/dictionaries.ts +0 -3
- package/src/modules/workflows/acl.ts +0 -2
- package/src/modules/workflows/api/__tests__/instances.route.test.ts +2 -5
- package/src/modules/workflows/api/instances/route.ts +7 -21
- package/src/modules/workflows/api/tasks/route.ts +1 -7
- package/src/modules/workflows/backend/definitions/[id]/page.meta.ts +1 -1
- package/src/modules/workflows/backend/definitions/[id]/page.tsx +0 -9
- package/src/modules/workflows/backend/definitions/create/page.meta.ts +1 -1
- package/src/modules/workflows/backend/definitions/create/page.tsx +0 -9
- package/src/modules/workflows/backend/definitions/visual-editor/page.meta.ts +1 -1
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +3 -21
- 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 +6 -5
- package/src/modules/workflows/cli.ts +0 -111
- package/src/modules/workflows/data/entities.ts +0 -124
- package/src/modules/workflows/data/validators.ts +0 -138
- package/src/modules/workflows/examples/checkout-demo-definition.json +5 -1
- package/src/modules/workflows/i18n/en.json +0 -71
- package/src/modules/workflows/lib/__tests__/activity-executor.test.ts +36 -43
- package/src/modules/workflows/lib/__tests__/transition-handler.test.ts +90 -170
- package/src/modules/workflows/lib/activity-executor.ts +16 -129
- package/src/modules/workflows/lib/graph-utils.ts +2 -117
- package/src/modules/workflows/lib/seeds.ts +8 -34
- package/src/modules/workflows/lib/start-validator.ts +28 -38
- package/src/modules/workflows/lib/transition-handler.ts +55 -208
- package/dist/generated/entities/workflow_event_trigger/index.js +0 -33
- package/dist/generated/entities/workflow_event_trigger/index.js.map +0 -7
- package/dist/modules/auth/events.js +0 -30
- package/dist/modules/auth/events.js.map +0 -7
- package/dist/modules/business_rules/api/execute/[ruleId]/route.js +0 -145
- package/dist/modules/business_rules/api/execute/[ruleId]/route.js.map +0 -7
- package/dist/modules/catalog/events.js +0 -34
- package/dist/modules/catalog/events.js.map +0 -7
- package/dist/modules/customers/events.js +0 -49
- package/dist/modules/customers/events.js.map +0 -7
- package/dist/modules/directory/events.js +0 -23
- package/dist/modules/directory/events.js.map +0 -7
- package/dist/modules/sales/events.js +0 -63
- package/dist/modules/sales/events.js.map +0 -7
- package/dist/modules/sales/lib/frontend/documentDataEvents.js +0 -25
- package/dist/modules/sales/lib/frontend/documentDataEvents.js.map +0 -7
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js +0 -481
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +0 -7
- package/dist/modules/workflows/components/EventTriggersEditor.js +0 -553
- package/dist/modules/workflows/components/EventTriggersEditor.js.map +0 -7
- package/dist/modules/workflows/events.js +0 -38
- package/dist/modules/workflows/events.js.map +0 -7
- package/dist/modules/workflows/examples/order-approval-definition.json +0 -257
- package/dist/modules/workflows/examples/order-approval-guard-rules.json +0 -32
- package/dist/modules/workflows/lib/event-trigger-service.js +0 -308
- package/dist/modules/workflows/lib/event-trigger-service.js.map +0 -7
- package/dist/modules/workflows/migrations/Migration20260123143500.js +0 -36
- package/dist/modules/workflows/migrations/Migration20260123143500.js.map +0 -7
- package/dist/modules/workflows/subscribers/event-trigger.js +0 -78
- package/dist/modules/workflows/subscribers/event-trigger.js.map +0 -7
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +0 -323
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +0 -7
- package/dist/modules/workflows/widgets/injection/order-approval/widget.js +0 -17
- package/dist/modules/workflows/widgets/injection/order-approval/widget.js.map +0 -7
- package/dist/modules/workflows/widgets/injection-table.js +0 -19
- package/dist/modules/workflows/widgets/injection-table.js.map +0 -7
- package/generated/entities/workflow_event_trigger/index.ts +0 -15
- package/src/modules/auth/events.ts +0 -39
- package/src/modules/business_rules/api/execute/[ruleId]/route.ts +0 -163
- package/src/modules/catalog/events.ts +0 -45
- package/src/modules/customers/events.ts +0 -63
- package/src/modules/directory/events.ts +0 -31
- package/src/modules/sales/events.ts +0 -82
- package/src/modules/sales/lib/frontend/documentDataEvents.ts +0 -28
- package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +0 -581
- package/src/modules/workflows/components/EventTriggersEditor.tsx +0 -664
- package/src/modules/workflows/events.ts +0 -49
- package/src/modules/workflows/examples/order-approval-definition.json +0 -257
- package/src/modules/workflows/examples/order-approval-guard-rules.json +0 -32
- package/src/modules/workflows/lib/event-trigger-service.ts +0 -557
- package/src/modules/workflows/migrations/Migration20260123143500.ts +0 -38
- package/src/modules/workflows/subscribers/event-trigger.ts +0 -109
- package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +0 -446
- package/src/modules/workflows/widgets/injection/order-approval/widget.ts +0 -16
- package/src/modules/workflows/widgets/injection-table.ts +0 -21
|
@@ -1,557 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workflows Module - Event Trigger Service
|
|
3
|
-
*
|
|
4
|
-
* Core service for evaluating and processing workflow event triggers.
|
|
5
|
-
* Handles pattern matching, filter evaluation, context mapping, and workflow starting.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { EntityManager } from '@mikro-orm/core'
|
|
9
|
-
import type { AwilixContainer } from 'awilix'
|
|
10
|
-
import type { CacheService } from '@open-mercato/cache'
|
|
11
|
-
import {
|
|
12
|
-
WorkflowEventTrigger,
|
|
13
|
-
WorkflowDefinition,
|
|
14
|
-
WorkflowInstance,
|
|
15
|
-
type TriggerFilterCondition,
|
|
16
|
-
type TriggerContextMapping,
|
|
17
|
-
type WorkflowEventTriggerConfig,
|
|
18
|
-
type WorkflowDefinitionTrigger,
|
|
19
|
-
} from '../data/entities'
|
|
20
|
-
import { startWorkflow, executeWorkflow } from './workflow-executor'
|
|
21
|
-
|
|
22
|
-
// ============================================================================
|
|
23
|
-
// Types
|
|
24
|
-
// ============================================================================
|
|
25
|
-
|
|
26
|
-
export interface EventTriggerContext {
|
|
27
|
-
eventName: string
|
|
28
|
-
payload: Record<string, unknown>
|
|
29
|
-
tenantId: string
|
|
30
|
-
organizationId: string
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface ProcessTriggersResult {
|
|
34
|
-
triggered: number
|
|
35
|
-
skipped: number
|
|
36
|
-
errors: Array<{ triggerId: string; error: string }>
|
|
37
|
-
instances: Array<{ triggerId: string; instanceId: string }>
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Unified trigger interface for both legacy (entity) and embedded (definition) triggers
|
|
42
|
-
*/
|
|
43
|
-
export interface UnifiedTrigger {
|
|
44
|
-
id: string // For legacy: entity ID, for embedded: `${definitionId}:${triggerId}`
|
|
45
|
-
triggerId: string
|
|
46
|
-
name: string
|
|
47
|
-
description?: string | null
|
|
48
|
-
eventPattern: string
|
|
49
|
-
config: WorkflowEventTriggerConfig | null
|
|
50
|
-
enabled: boolean
|
|
51
|
-
priority: number
|
|
52
|
-
workflowDefinitionId: string
|
|
53
|
-
workflowId: string
|
|
54
|
-
workflowVersion: number
|
|
55
|
-
source: 'legacy' | 'embedded'
|
|
56
|
-
tenantId: string
|
|
57
|
-
organizationId: string
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
interface CachedTriggers {
|
|
61
|
-
triggers: UnifiedTrigger[]
|
|
62
|
-
cachedAt: number
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Cache TTL: 5 minutes
|
|
66
|
-
const TRIGGER_CACHE_TTL = 5 * 60 * 1000
|
|
67
|
-
|
|
68
|
-
// ============================================================================
|
|
69
|
-
// Pattern Matching
|
|
70
|
-
// ============================================================================
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Match an event name against a pattern.
|
|
74
|
-
*
|
|
75
|
-
* Supports:
|
|
76
|
-
* - Exact match: `customers.people.created`
|
|
77
|
-
* - Wildcard `*` matches single segment: `customers.*` matches `customers.people` but not `customers.people.created`
|
|
78
|
-
* - Global wildcard: `*` alone matches all events
|
|
79
|
-
*/
|
|
80
|
-
export function matchEventPattern(eventName: string, pattern: string): boolean {
|
|
81
|
-
// Global wildcard matches all events
|
|
82
|
-
if (pattern === '*') return true
|
|
83
|
-
|
|
84
|
-
// Exact match
|
|
85
|
-
if (pattern === eventName) return true
|
|
86
|
-
|
|
87
|
-
// No wildcards in pattern means we need exact match, which already failed
|
|
88
|
-
if (!pattern.includes('*')) return false
|
|
89
|
-
|
|
90
|
-
// Convert pattern to regex:
|
|
91
|
-
// - Escape regex special chars (except *)
|
|
92
|
-
// - Replace * with [^.]+ (match one or more non-dot chars)
|
|
93
|
-
const regexPattern = pattern
|
|
94
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
95
|
-
.replace(/\*/g, '[^.]+')
|
|
96
|
-
const regex = new RegExp(`^${regexPattern}$`)
|
|
97
|
-
return regex.test(eventName)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ============================================================================
|
|
101
|
-
// Filter Evaluation
|
|
102
|
-
// ============================================================================
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Get a nested value from an object using dot notation.
|
|
106
|
-
*/
|
|
107
|
-
function getNestedValue(obj: unknown, path: string): unknown {
|
|
108
|
-
if (obj === null || obj === undefined) return undefined
|
|
109
|
-
|
|
110
|
-
const parts = path.split('.')
|
|
111
|
-
let current: unknown = obj
|
|
112
|
-
|
|
113
|
-
for (const part of parts) {
|
|
114
|
-
if (current === null || current === undefined) return undefined
|
|
115
|
-
if (typeof current !== 'object') return undefined
|
|
116
|
-
current = (current as Record<string, unknown>)[part]
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return current
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Evaluate a single filter condition against the event payload.
|
|
124
|
-
*/
|
|
125
|
-
function evaluateCondition(condition: TriggerFilterCondition, payload: Record<string, unknown>): boolean {
|
|
126
|
-
const value = getNestedValue(payload, condition.field)
|
|
127
|
-
const expected = condition.value
|
|
128
|
-
|
|
129
|
-
switch (condition.operator) {
|
|
130
|
-
case 'eq':
|
|
131
|
-
return value === expected
|
|
132
|
-
|
|
133
|
-
case 'neq':
|
|
134
|
-
return value !== expected
|
|
135
|
-
|
|
136
|
-
case 'gt':
|
|
137
|
-
return typeof value === 'number' && typeof expected === 'number' && value > expected
|
|
138
|
-
|
|
139
|
-
case 'gte':
|
|
140
|
-
return typeof value === 'number' && typeof expected === 'number' && value >= expected
|
|
141
|
-
|
|
142
|
-
case 'lt':
|
|
143
|
-
return typeof value === 'number' && typeof expected === 'number' && value < expected
|
|
144
|
-
|
|
145
|
-
case 'lte':
|
|
146
|
-
return typeof value === 'number' && typeof expected === 'number' && value <= expected
|
|
147
|
-
|
|
148
|
-
case 'contains':
|
|
149
|
-
if (typeof value === 'string' && typeof expected === 'string') {
|
|
150
|
-
return value.includes(expected)
|
|
151
|
-
}
|
|
152
|
-
if (Array.isArray(value)) {
|
|
153
|
-
return value.includes(expected)
|
|
154
|
-
}
|
|
155
|
-
return false
|
|
156
|
-
|
|
157
|
-
case 'startsWith':
|
|
158
|
-
return typeof value === 'string' && typeof expected === 'string' && value.startsWith(expected)
|
|
159
|
-
|
|
160
|
-
case 'endsWith':
|
|
161
|
-
return typeof value === 'string' && typeof expected === 'string' && value.endsWith(expected)
|
|
162
|
-
|
|
163
|
-
case 'in':
|
|
164
|
-
return Array.isArray(expected) && expected.includes(value)
|
|
165
|
-
|
|
166
|
-
case 'notIn':
|
|
167
|
-
return Array.isArray(expected) && !expected.includes(value)
|
|
168
|
-
|
|
169
|
-
case 'exists':
|
|
170
|
-
return value !== undefined && value !== null
|
|
171
|
-
|
|
172
|
-
case 'notExists':
|
|
173
|
-
return value === undefined || value === null
|
|
174
|
-
|
|
175
|
-
case 'regex':
|
|
176
|
-
if (typeof value !== 'string' || typeof expected !== 'string') return false
|
|
177
|
-
try {
|
|
178
|
-
const regex = new RegExp(expected)
|
|
179
|
-
return regex.test(value)
|
|
180
|
-
} catch {
|
|
181
|
-
return false
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
default:
|
|
185
|
-
return false
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Evaluate all filter conditions against the event payload.
|
|
191
|
-
* All conditions must pass (AND logic).
|
|
192
|
-
*/
|
|
193
|
-
export function evaluateFilterConditions(
|
|
194
|
-
conditions: TriggerFilterCondition[] | undefined,
|
|
195
|
-
payload: Record<string, unknown>
|
|
196
|
-
): boolean {
|
|
197
|
-
if (!conditions || conditions.length === 0) return true
|
|
198
|
-
|
|
199
|
-
return conditions.every(condition => evaluateCondition(condition, payload))
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// ============================================================================
|
|
203
|
-
// Context Mapping
|
|
204
|
-
// ============================================================================
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Map event payload to workflow initial context.
|
|
208
|
-
*/
|
|
209
|
-
export function mapEventToContext(
|
|
210
|
-
mapping: TriggerContextMapping[] | undefined,
|
|
211
|
-
payload: Record<string, unknown>
|
|
212
|
-
): Record<string, unknown> {
|
|
213
|
-
const context: Record<string, unknown> = {}
|
|
214
|
-
|
|
215
|
-
if (!mapping || mapping.length === 0) return context
|
|
216
|
-
|
|
217
|
-
for (const item of mapping) {
|
|
218
|
-
const value = getNestedValue(payload, item.sourceExpression)
|
|
219
|
-
context[item.targetKey] = value !== undefined ? value : item.defaultValue
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return context
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ============================================================================
|
|
226
|
-
// Trigger Loading with Caching
|
|
227
|
-
// ============================================================================
|
|
228
|
-
|
|
229
|
-
// In-memory cache for triggers (per tenant/org)
|
|
230
|
-
const triggerCache = new Map<string, CachedTriggers>()
|
|
231
|
-
|
|
232
|
-
function getCacheKey(tenantId: string, organizationId: string): string {
|
|
233
|
-
return `${tenantId}:${organizationId}`
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Load legacy triggers from WorkflowEventTrigger entity table.
|
|
238
|
-
* For backward compatibility with existing triggers.
|
|
239
|
-
*/
|
|
240
|
-
async function loadLegacyTriggers(
|
|
241
|
-
em: EntityManager,
|
|
242
|
-
tenantId: string,
|
|
243
|
-
organizationId: string
|
|
244
|
-
): Promise<UnifiedTrigger[]> {
|
|
245
|
-
const legacyTriggers = await em.find(
|
|
246
|
-
WorkflowEventTrigger,
|
|
247
|
-
{
|
|
248
|
-
tenantId,
|
|
249
|
-
organizationId,
|
|
250
|
-
enabled: true,
|
|
251
|
-
deletedAt: null,
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
orderBy: { priority: 'DESC', createdAt: 'ASC' },
|
|
255
|
-
}
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
// Get definitions for these triggers to get workflowId
|
|
259
|
-
const definitionIds = [...new Set(legacyTriggers.map(t => t.workflowDefinitionId))]
|
|
260
|
-
const definitions = definitionIds.length > 0 ? await em.find(WorkflowDefinition, {
|
|
261
|
-
id: { $in: definitionIds },
|
|
262
|
-
enabled: true,
|
|
263
|
-
deletedAt: null,
|
|
264
|
-
}) : []
|
|
265
|
-
const definitionMap = new Map(definitions.map(d => [d.id, d]))
|
|
266
|
-
|
|
267
|
-
return legacyTriggers
|
|
268
|
-
.filter(t => definitionMap.has(t.workflowDefinitionId))
|
|
269
|
-
.map(t => {
|
|
270
|
-
const def = definitionMap.get(t.workflowDefinitionId)!
|
|
271
|
-
return {
|
|
272
|
-
id: t.id,
|
|
273
|
-
triggerId: t.id,
|
|
274
|
-
name: t.name,
|
|
275
|
-
description: t.description,
|
|
276
|
-
eventPattern: t.eventPattern,
|
|
277
|
-
config: t.config ?? null,
|
|
278
|
-
enabled: t.enabled,
|
|
279
|
-
priority: t.priority,
|
|
280
|
-
workflowDefinitionId: t.workflowDefinitionId,
|
|
281
|
-
workflowId: def.workflowId,
|
|
282
|
-
workflowVersion: def.version,
|
|
283
|
-
source: 'legacy' as const,
|
|
284
|
-
tenantId: t.tenantId,
|
|
285
|
-
organizationId: t.organizationId,
|
|
286
|
-
}
|
|
287
|
-
})
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Load embedded triggers from workflow definitions.
|
|
292
|
-
* New triggers are embedded directly in the definition JSONB.
|
|
293
|
-
*/
|
|
294
|
-
async function loadEmbeddedTriggers(
|
|
295
|
-
em: EntityManager,
|
|
296
|
-
tenantId: string,
|
|
297
|
-
organizationId: string
|
|
298
|
-
): Promise<UnifiedTrigger[]> {
|
|
299
|
-
// Load all enabled definitions that may have triggers
|
|
300
|
-
const definitions = await em.find(
|
|
301
|
-
WorkflowDefinition,
|
|
302
|
-
{
|
|
303
|
-
tenantId,
|
|
304
|
-
organizationId,
|
|
305
|
-
enabled: true,
|
|
306
|
-
deletedAt: null,
|
|
307
|
-
}
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
const triggers: UnifiedTrigger[] = []
|
|
311
|
-
|
|
312
|
-
for (const def of definitions) {
|
|
313
|
-
const embeddedTriggers = def.definition?.triggers as WorkflowDefinitionTrigger[] | undefined
|
|
314
|
-
if (!embeddedTriggers || embeddedTriggers.length === 0) continue
|
|
315
|
-
|
|
316
|
-
for (const trigger of embeddedTriggers) {
|
|
317
|
-
if (!trigger.enabled) continue
|
|
318
|
-
|
|
319
|
-
triggers.push({
|
|
320
|
-
id: `${def.id}:${trigger.triggerId}`,
|
|
321
|
-
triggerId: trigger.triggerId,
|
|
322
|
-
name: trigger.name,
|
|
323
|
-
description: trigger.description ?? null,
|
|
324
|
-
eventPattern: trigger.eventPattern,
|
|
325
|
-
config: trigger.config ?? null,
|
|
326
|
-
enabled: trigger.enabled,
|
|
327
|
-
priority: trigger.priority,
|
|
328
|
-
workflowDefinitionId: def.id,
|
|
329
|
-
workflowId: def.workflowId,
|
|
330
|
-
workflowVersion: def.version,
|
|
331
|
-
source: 'embedded' as const,
|
|
332
|
-
tenantId,
|
|
333
|
-
organizationId,
|
|
334
|
-
})
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return triggers
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Load all enabled triggers for a tenant/organization with caching.
|
|
343
|
-
* Merges both legacy (entity) triggers and embedded (definition) triggers.
|
|
344
|
-
*/
|
|
345
|
-
export async function loadTriggersForTenant(
|
|
346
|
-
em: EntityManager,
|
|
347
|
-
tenantId: string,
|
|
348
|
-
organizationId: string,
|
|
349
|
-
cacheService?: CacheService
|
|
350
|
-
): Promise<UnifiedTrigger[]> {
|
|
351
|
-
const cacheKey = getCacheKey(tenantId, organizationId)
|
|
352
|
-
|
|
353
|
-
// Check in-memory cache
|
|
354
|
-
const cached = triggerCache.get(cacheKey)
|
|
355
|
-
if (cached && Date.now() - cached.cachedAt < TRIGGER_CACHE_TTL) {
|
|
356
|
-
return cached.triggers
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Load from both sources
|
|
360
|
-
const [legacyTriggers, embeddedTriggers] = await Promise.all([
|
|
361
|
-
loadLegacyTriggers(em, tenantId, organizationId),
|
|
362
|
-
loadEmbeddedTriggers(em, tenantId, organizationId),
|
|
363
|
-
])
|
|
364
|
-
|
|
365
|
-
// Merge and sort by priority (higher first)
|
|
366
|
-
const allTriggers = [...legacyTriggers, ...embeddedTriggers]
|
|
367
|
-
.sort((a, b) => b.priority - a.priority)
|
|
368
|
-
|
|
369
|
-
// Update cache
|
|
370
|
-
triggerCache.set(cacheKey, {
|
|
371
|
-
triggers: allTriggers,
|
|
372
|
-
cachedAt: Date.now(),
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
return allTriggers
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Invalidate trigger cache for a tenant/organization.
|
|
380
|
-
* Call this when:
|
|
381
|
-
* - Legacy triggers are created/updated/deleted
|
|
382
|
-
* - Workflow definitions with embedded triggers are created/updated/deleted
|
|
383
|
-
*/
|
|
384
|
-
export function invalidateTriggerCache(tenantId: string, organizationId?: string): void {
|
|
385
|
-
if (organizationId) {
|
|
386
|
-
// Invalidate specific org
|
|
387
|
-
const cacheKey = getCacheKey(tenantId, organizationId)
|
|
388
|
-
triggerCache.delete(cacheKey)
|
|
389
|
-
} else {
|
|
390
|
-
// Invalidate all orgs for tenant
|
|
391
|
-
for (const key of triggerCache.keys()) {
|
|
392
|
-
if (key.startsWith(`${tenantId}:`)) {
|
|
393
|
-
triggerCache.delete(key)
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// ============================================================================
|
|
400
|
-
// Trigger Matching
|
|
401
|
-
// ============================================================================
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Find all triggers that match an event.
|
|
405
|
-
*/
|
|
406
|
-
export async function findMatchingTriggers(
|
|
407
|
-
em: EntityManager,
|
|
408
|
-
context: EventTriggerContext
|
|
409
|
-
): Promise<UnifiedTrigger[]> {
|
|
410
|
-
const triggers = await loadTriggersForTenant(
|
|
411
|
-
em,
|
|
412
|
-
context.tenantId,
|
|
413
|
-
context.organizationId
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
return triggers.filter(trigger => {
|
|
417
|
-
// Check event pattern
|
|
418
|
-
if (!matchEventPattern(context.eventName, trigger.eventPattern)) {
|
|
419
|
-
return false
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Check filter conditions
|
|
423
|
-
if (!evaluateFilterConditions(trigger.config?.filterConditions, context.payload)) {
|
|
424
|
-
return false
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return true
|
|
428
|
-
})
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// ============================================================================
|
|
432
|
-
// Trigger Processing
|
|
433
|
-
// ============================================================================
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Check if max concurrent instances limit is reached.
|
|
437
|
-
*/
|
|
438
|
-
async function checkConcurrencyLimit(
|
|
439
|
-
em: EntityManager,
|
|
440
|
-
trigger: UnifiedTrigger
|
|
441
|
-
): Promise<boolean> {
|
|
442
|
-
const maxInstances = trigger.config?.maxConcurrentInstances
|
|
443
|
-
|
|
444
|
-
if (!maxInstances) return true // No limit
|
|
445
|
-
|
|
446
|
-
// Count running instances for this trigger's workflow definition
|
|
447
|
-
const runningCount = await em.count(WorkflowInstance, {
|
|
448
|
-
definitionId: trigger.workflowDefinitionId,
|
|
449
|
-
status: { $in: ['RUNNING', 'WAITING_FOR_ACTIVITIES'] },
|
|
450
|
-
tenantId: trigger.tenantId,
|
|
451
|
-
organizationId: trigger.organizationId,
|
|
452
|
-
deletedAt: null,
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
return runningCount < maxInstances
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Process all matching triggers for an event and start workflows.
|
|
460
|
-
*/
|
|
461
|
-
export async function processEventTriggers(
|
|
462
|
-
em: EntityManager,
|
|
463
|
-
container: AwilixContainer,
|
|
464
|
-
context: EventTriggerContext
|
|
465
|
-
): Promise<ProcessTriggersResult> {
|
|
466
|
-
const result: ProcessTriggersResult = {
|
|
467
|
-
triggered: 0,
|
|
468
|
-
skipped: 0,
|
|
469
|
-
errors: [],
|
|
470
|
-
instances: [],
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Find matching triggers
|
|
474
|
-
const triggers = await findMatchingTriggers(em, context)
|
|
475
|
-
|
|
476
|
-
if (triggers.length === 0) {
|
|
477
|
-
return result
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Process each trigger (definitions already validated during loading)
|
|
481
|
-
for (const trigger of triggers) {
|
|
482
|
-
try {
|
|
483
|
-
// Check concurrency limit
|
|
484
|
-
const canStart = await checkConcurrencyLimit(em, trigger)
|
|
485
|
-
if (!canStart) {
|
|
486
|
-
console.log(`[workflow-trigger] Skipping trigger "${trigger.name}": max concurrent instances reached`)
|
|
487
|
-
result.skipped++
|
|
488
|
-
continue
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Map event payload to workflow context
|
|
492
|
-
const mappedContext = mapEventToContext(trigger.config?.contextMapping, context.payload)
|
|
493
|
-
|
|
494
|
-
// Extract entity info from payload for metadata
|
|
495
|
-
const payloadId = context.payload?.id as string | undefined
|
|
496
|
-
const payloadEntityType = context.payload?.entityType as string | undefined
|
|
497
|
-
|
|
498
|
-
// Include event metadata and payload in context
|
|
499
|
-
const initialContext = {
|
|
500
|
-
// Include raw event payload fields (e.g., id, organizationId, tenantId)
|
|
501
|
-
...context.payload,
|
|
502
|
-
// Override with explicit mappings if provided
|
|
503
|
-
...mappedContext,
|
|
504
|
-
__trigger: {
|
|
505
|
-
triggerId: trigger.id,
|
|
506
|
-
triggerName: trigger.name,
|
|
507
|
-
eventName: context.eventName,
|
|
508
|
-
eventPayload: context.payload,
|
|
509
|
-
triggeredAt: new Date().toISOString(),
|
|
510
|
-
source: trigger.source,
|
|
511
|
-
},
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Start workflow
|
|
515
|
-
const instance = await startWorkflow(em, {
|
|
516
|
-
workflowId: trigger.workflowId,
|
|
517
|
-
version: trigger.workflowVersion,
|
|
518
|
-
initialContext,
|
|
519
|
-
metadata: {
|
|
520
|
-
initiatedBy: `trigger:${trigger.id}`,
|
|
521
|
-
// Include entityId and entityType for widget discovery
|
|
522
|
-
entityId: payloadId,
|
|
523
|
-
entityType: payloadEntityType || trigger.config?.entityType,
|
|
524
|
-
labels: {
|
|
525
|
-
trigger_id: trigger.id,
|
|
526
|
-
trigger_name: trigger.name,
|
|
527
|
-
event_name: context.eventName,
|
|
528
|
-
trigger_source: trigger.source,
|
|
529
|
-
},
|
|
530
|
-
},
|
|
531
|
-
tenantId: context.tenantId,
|
|
532
|
-
organizationId: context.organizationId,
|
|
533
|
-
})
|
|
534
|
-
|
|
535
|
-
result.triggered++
|
|
536
|
-
result.instances.push({
|
|
537
|
-
triggerId: trigger.id,
|
|
538
|
-
instanceId: instance.id,
|
|
539
|
-
})
|
|
540
|
-
|
|
541
|
-
// Execute workflow asynchronously (don't wait)
|
|
542
|
-
executeWorkflow(em.fork(), container, instance.id).catch(err => {
|
|
543
|
-
console.error(`[workflow-trigger] Error executing workflow ${instance.id}:`, err)
|
|
544
|
-
})
|
|
545
|
-
|
|
546
|
-
} catch (error) {
|
|
547
|
-
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
548
|
-
console.error(`[workflow-trigger] Error processing trigger "${trigger.name}":`, error)
|
|
549
|
-
result.errors.push({
|
|
550
|
-
triggerId: trigger.id,
|
|
551
|
-
error: errorMessage,
|
|
552
|
-
})
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
return result
|
|
557
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
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
|
-
}
|