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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/dist/generated/entities/workflow_event_trigger/index.js +33 -0
  2. package/dist/generated/entities/workflow_event_trigger/index.js.map +7 -0
  3. package/dist/generated/entities.ids.generated.js +1 -0
  4. package/dist/generated/entities.ids.generated.js.map +2 -2
  5. package/dist/generated/entity-fields-registry.js +2 -0
  6. package/dist/generated/entity-fields-registry.js.map +2 -2
  7. package/dist/modules/auth/events.js +30 -0
  8. package/dist/modules/auth/events.js.map +7 -0
  9. package/dist/modules/business_rules/api/execute/[ruleId]/route.js +145 -0
  10. package/dist/modules/business_rules/api/execute/[ruleId]/route.js.map +7 -0
  11. package/dist/modules/business_rules/data/validators.js +34 -0
  12. package/dist/modules/business_rules/data/validators.js.map +2 -2
  13. package/dist/modules/business_rules/index.js +21 -1
  14. package/dist/modules/business_rules/index.js.map +2 -2
  15. package/dist/modules/business_rules/lib/rule-engine.js +182 -1
  16. package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
  17. package/dist/modules/catalog/events.js +34 -0
  18. package/dist/modules/catalog/events.js.map +7 -0
  19. package/dist/modules/customers/events.js +49 -0
  20. package/dist/modules/customers/events.js.map +7 -0
  21. package/dist/modules/directory/events.js +23 -0
  22. package/dist/modules/directory/events.js.map +7 -0
  23. package/dist/modules/sales/acl.js +1 -0
  24. package/dist/modules/sales/acl.js.map +2 -2
  25. package/dist/modules/sales/backend/sales/documents/[id]/page.js +12 -0
  26. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  27. package/dist/modules/sales/commands/documents.js +62 -0
  28. package/dist/modules/sales/commands/documents.js.map +2 -2
  29. package/dist/modules/sales/events.js +63 -0
  30. package/dist/modules/sales/events.js.map +7 -0
  31. package/dist/modules/sales/lib/dictionaries.js +3 -0
  32. package/dist/modules/sales/lib/dictionaries.js.map +2 -2
  33. package/dist/modules/sales/lib/frontend/documentDataEvents.js +25 -0
  34. package/dist/modules/sales/lib/frontend/documentDataEvents.js.map +7 -0
  35. package/dist/modules/workflows/acl.js +2 -0
  36. package/dist/modules/workflows/acl.js.map +2 -2
  37. package/dist/modules/workflows/api/instances/route.js +18 -6
  38. package/dist/modules/workflows/api/instances/route.js.map +2 -2
  39. package/dist/modules/workflows/api/tasks/route.js +6 -1
  40. package/dist/modules/workflows/api/tasks/route.js.map +2 -2
  41. package/dist/modules/workflows/backend/definitions/[id]/page.js +9 -1
  42. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  43. package/dist/modules/workflows/backend/definitions/[id]/page.meta.js +1 -1
  44. package/dist/modules/workflows/backend/definitions/[id]/page.meta.js.map +2 -2
  45. package/dist/modules/workflows/backend/definitions/create/page.js +24 -15
  46. package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
  47. package/dist/modules/workflows/backend/definitions/create/page.meta.js +1 -1
  48. package/dist/modules/workflows/backend/definitions/create/page.meta.js.map +2 -2
  49. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +150 -132
  50. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  51. package/dist/modules/workflows/backend/definitions/visual-editor/page.meta.js +1 -1
  52. package/dist/modules/workflows/backend/definitions/visual-editor/page.meta.js.map +2 -2
  53. package/dist/modules/workflows/backend/events/[id]/page.js +1 -1
  54. package/dist/modules/workflows/backend/events/[id]/page.js.map +2 -2
  55. package/dist/modules/workflows/backend/events/[id]/page.meta.js +2 -2
  56. package/dist/modules/workflows/backend/events/[id]/page.meta.js.map +2 -2
  57. package/dist/modules/workflows/backend/instances/[id]/page.meta.js +2 -2
  58. package/dist/modules/workflows/backend/instances/[id]/page.meta.js.map +2 -2
  59. package/dist/modules/workflows/backend/tasks/[id]/page.js +1 -1
  60. package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
  61. package/dist/modules/workflows/backend/tasks/[id]/page.meta.js +2 -2
  62. package/dist/modules/workflows/backend/tasks/[id]/page.meta.js.map +2 -2
  63. package/dist/modules/workflows/backend/tasks/page.js +5 -6
  64. package/dist/modules/workflows/backend/tasks/page.js.map +2 -2
  65. package/dist/modules/workflows/cli.js +81 -3
  66. package/dist/modules/workflows/cli.js.map +3 -3
  67. package/dist/modules/workflows/components/DefinitionTriggersEditor.js +481 -0
  68. package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +7 -0
  69. package/dist/modules/workflows/components/EventTriggersEditor.js +553 -0
  70. package/dist/modules/workflows/components/EventTriggersEditor.js.map +7 -0
  71. package/dist/modules/workflows/data/entities.js +64 -1
  72. package/dist/modules/workflows/data/entities.js.map +2 -2
  73. package/dist/modules/workflows/data/validators.js +115 -0
  74. package/dist/modules/workflows/data/validators.js.map +2 -2
  75. package/dist/modules/workflows/events.js +38 -0
  76. package/dist/modules/workflows/events.js.map +7 -0
  77. package/dist/modules/workflows/examples/checkout-demo-definition.json +1 -5
  78. package/dist/modules/workflows/examples/order-approval-definition.json +257 -0
  79. package/dist/modules/workflows/examples/order-approval-guard-rules.json +32 -0
  80. package/dist/modules/workflows/lib/activity-executor.js +75 -13
  81. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  82. package/dist/modules/workflows/lib/event-trigger-service.js +308 -0
  83. package/dist/modules/workflows/lib/event-trigger-service.js.map +7 -0
  84. package/dist/modules/workflows/lib/graph-utils.js +71 -2
  85. package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
  86. package/dist/modules/workflows/lib/seeds.js +17 -4
  87. package/dist/modules/workflows/lib/seeds.js.map +2 -2
  88. package/dist/modules/workflows/lib/start-validator.js +33 -23
  89. package/dist/modules/workflows/lib/start-validator.js.map +2 -2
  90. package/dist/modules/workflows/lib/transition-handler.js +157 -45
  91. package/dist/modules/workflows/lib/transition-handler.js.map +3 -3
  92. package/dist/modules/workflows/migrations/Migration20260123143500.js +36 -0
  93. package/dist/modules/workflows/migrations/Migration20260123143500.js.map +7 -0
  94. package/dist/modules/workflows/subscribers/event-trigger.js +78 -0
  95. package/dist/modules/workflows/subscribers/event-trigger.js.map +7 -0
  96. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +323 -0
  97. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +7 -0
  98. package/dist/modules/workflows/widgets/injection/order-approval/widget.js +17 -0
  99. package/dist/modules/workflows/widgets/injection/order-approval/widget.js.map +7 -0
  100. package/dist/modules/workflows/widgets/injection-table.js +19 -0
  101. package/dist/modules/workflows/widgets/injection-table.js.map +7 -0
  102. package/generated/entities/workflow_event_trigger/index.ts +15 -0
  103. package/generated/entities.ids.generated.ts +1 -0
  104. package/generated/entity-fields-registry.ts +2 -0
  105. package/package.json +2 -2
  106. package/src/modules/auth/events.ts +39 -0
  107. package/src/modules/business_rules/api/execute/[ruleId]/route.ts +163 -0
  108. package/src/modules/business_rules/data/validators.ts +40 -0
  109. package/src/modules/business_rules/index.ts +25 -0
  110. package/src/modules/business_rules/lib/rule-engine.ts +281 -1
  111. package/src/modules/catalog/events.ts +45 -0
  112. package/src/modules/customers/events.ts +63 -0
  113. package/src/modules/directory/events.ts +31 -0
  114. package/src/modules/sales/acl.ts +1 -0
  115. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +16 -0
  116. package/src/modules/sales/commands/documents.ts +75 -1
  117. package/src/modules/sales/events.ts +82 -0
  118. package/src/modules/sales/lib/dictionaries.ts +3 -0
  119. package/src/modules/sales/lib/frontend/documentDataEvents.ts +28 -0
  120. package/src/modules/workflows/acl.ts +2 -0
  121. package/src/modules/workflows/api/__tests__/instances.route.test.ts +5 -2
  122. package/src/modules/workflows/api/instances/route.ts +21 -7
  123. package/src/modules/workflows/api/tasks/route.ts +7 -1
  124. package/src/modules/workflows/backend/definitions/[id]/page.meta.ts +1 -1
  125. package/src/modules/workflows/backend/definitions/[id]/page.tsx +9 -0
  126. package/src/modules/workflows/backend/definitions/create/page.meta.ts +1 -1
  127. package/src/modules/workflows/backend/definitions/create/page.tsx +9 -0
  128. package/src/modules/workflows/backend/definitions/visual-editor/page.meta.ts +1 -1
  129. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +21 -3
  130. package/src/modules/workflows/backend/events/[id]/page.meta.ts +2 -2
  131. package/src/modules/workflows/backend/events/[id]/page.tsx +1 -1
  132. package/src/modules/workflows/backend/instances/[id]/page.meta.ts +2 -2
  133. package/src/modules/workflows/backend/tasks/[id]/page.meta.ts +2 -2
  134. package/src/modules/workflows/backend/tasks/[id]/page.tsx +1 -1
  135. package/src/modules/workflows/backend/tasks/page.tsx +5 -6
  136. package/src/modules/workflows/cli.ts +111 -0
  137. package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +581 -0
  138. package/src/modules/workflows/components/EventTriggersEditor.tsx +664 -0
  139. package/src/modules/workflows/data/entities.ts +124 -0
  140. package/src/modules/workflows/data/validators.ts +138 -0
  141. package/src/modules/workflows/events.ts +49 -0
  142. package/src/modules/workflows/examples/checkout-demo-definition.json +1 -5
  143. package/src/modules/workflows/examples/order-approval-definition.json +257 -0
  144. package/src/modules/workflows/examples/order-approval-guard-rules.json +32 -0
  145. package/src/modules/workflows/i18n/en.json +71 -0
  146. package/src/modules/workflows/lib/__tests__/activity-executor.test.ts +43 -36
  147. package/src/modules/workflows/lib/__tests__/transition-handler.test.ts +170 -90
  148. package/src/modules/workflows/lib/activity-executor.ts +129 -16
  149. package/src/modules/workflows/lib/event-trigger-service.ts +557 -0
  150. package/src/modules/workflows/lib/graph-utils.ts +117 -2
  151. package/src/modules/workflows/lib/seeds.ts +29 -8
  152. package/src/modules/workflows/lib/start-validator.ts +38 -28
  153. package/src/modules/workflows/lib/transition-handler.ts +208 -55
  154. package/src/modules/workflows/migrations/Migration20260123143500.ts +38 -0
  155. package/src/modules/workflows/subscribers/event-trigger.ts +109 -0
  156. package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +446 -0
  157. package/src/modules/workflows/widgets/injection/order-approval/widget.ts +16 -0
  158. package/src/modules/workflows/widgets/injection-table.ts +21 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.4.2-canary-5d2c419a9b",
3
+ "version": "0.4.2-canary-9e0237de8e",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -207,7 +207,7 @@
207
207
  }
208
208
  },
209
209
  "dependencies": {
210
- "@open-mercato/shared": "0.4.2-canary-5d2c419a9b",
210
+ "@open-mercato/shared": "0.4.2-canary-9e0237de8e",
211
211
  "@xyflow/react": "^12.6.0",
212
212
  "date-fns": "^4.1.0",
213
213
  "date-fns-tz": "^3.2.0"
@@ -0,0 +1,39 @@
1
+ import { createModuleEvents } from '@open-mercato/shared/modules/events'
2
+
3
+ /**
4
+ * Auth Module Events
5
+ *
6
+ * Declares all events that can be emitted by the auth module.
7
+ */
8
+ const events = [
9
+ // Users
10
+ { id: 'auth.users.created', label: 'User Created', entity: 'users', category: 'crud' },
11
+ { id: 'auth.users.updated', label: 'User Updated', entity: 'users', category: 'crud' },
12
+ { id: 'auth.users.deleted', label: 'User Deleted', entity: 'users', category: 'crud' },
13
+
14
+ // Roles
15
+ { id: 'auth.roles.created', label: 'Role Created', entity: 'roles', category: 'crud' },
16
+ { id: 'auth.roles.updated', label: 'Role Updated', entity: 'roles', category: 'crud' },
17
+ { id: 'auth.roles.deleted', label: 'Role Deleted', entity: 'roles', category: 'crud' },
18
+
19
+ // Authentication events
20
+ { id: 'auth.login.success', label: 'Login Successful', category: 'lifecycle' },
21
+ { id: 'auth.login.failed', label: 'Login Failed', category: 'lifecycle' },
22
+ { id: 'auth.logout', label: 'User Logged Out', category: 'lifecycle' },
23
+ { id: 'auth.password.changed', label: 'Password Changed', category: 'lifecycle' },
24
+ { id: 'auth.password.reset.requested', label: 'Password Reset Requested', category: 'lifecycle' },
25
+ { id: 'auth.password.reset.completed', label: 'Password Reset Completed', category: 'lifecycle' },
26
+ ] as const
27
+
28
+ export const eventsConfig = createModuleEvents({
29
+ moduleId: 'auth',
30
+ events,
31
+ })
32
+
33
+ /** Type-safe event emitter for auth module */
34
+ export const emitAuthEvent = eventsConfig.emit
35
+
36
+ /** Event IDs that can be emitted by the auth module */
37
+ export type AuthEventId = typeof events[number]['id']
38
+
39
+ export default eventsConfig
@@ -0,0 +1,163 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
4
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
+ import type { EntityManager } from '@mikro-orm/postgresql'
7
+ import * as ruleEngine from '../../../lib/rule-engine'
8
+
9
+ const executeByIdRequestSchema = z.object({
10
+ data: z.any(),
11
+ dryRun: z.boolean().optional().default(false),
12
+ entityType: z.string().optional(),
13
+ entityId: z.string().optional(),
14
+ eventType: z.string().optional(),
15
+ })
16
+
17
+ const executeByIdResponseSchema = z.object({
18
+ success: z.boolean(),
19
+ ruleId: z.string(),
20
+ ruleName: z.string(),
21
+ conditionResult: z.boolean(),
22
+ actionsExecuted: z.object({
23
+ success: z.boolean(),
24
+ results: z.array(z.object({
25
+ type: z.string(),
26
+ success: z.boolean(),
27
+ error: z.string().optional(),
28
+ })),
29
+ }).nullable(),
30
+ executionTime: z.number(),
31
+ error: z.string().optional(),
32
+ logId: z.string().optional(),
33
+ })
34
+
35
+ const errorResponseSchema = z.object({
36
+ error: z.string(),
37
+ })
38
+
39
+ const routeMetadata = {
40
+ POST: { requireAuth: true, requireFeatures: ['business_rules.execute'] },
41
+ }
42
+
43
+ export const metadata = routeMetadata
44
+
45
+ interface RouteContext {
46
+ params: Promise<{ ruleId: string }>
47
+ }
48
+
49
+ export async function POST(req: Request, context: RouteContext) {
50
+ const auth = await getAuthFromRequest(req)
51
+ if (!auth) {
52
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
53
+ }
54
+
55
+ const params = await context.params
56
+ const ruleId = params.ruleId
57
+
58
+ if (!ruleId || !z.uuid().safeParse(ruleId).success) {
59
+ return NextResponse.json({ error: 'Invalid rule ID' }, { status: 400 })
60
+ }
61
+
62
+ const container = await createRequestContainer()
63
+ const em = container.resolve('em') as EntityManager
64
+
65
+ let body: any
66
+ try {
67
+ body = await req.json()
68
+ } catch {
69
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
70
+ }
71
+
72
+ const parsed = executeByIdRequestSchema.safeParse(body)
73
+ if (!parsed.success) {
74
+ const errors = parsed.error.issues.map(e => `${e.path.join('.')}: ${e.message}`)
75
+ return NextResponse.json({ error: `Validation failed: ${errors.join(', ')}` }, { status: 400 })
76
+ }
77
+
78
+ const { data, dryRun, entityType, entityId, eventType } = parsed.data
79
+
80
+ const execContext: ruleEngine.DirectRuleExecutionContext = {
81
+ ruleId,
82
+ data,
83
+ user: {
84
+ id: auth.sub,
85
+ email: auth.email,
86
+ role: (auth.role as string) ?? undefined,
87
+ },
88
+ tenantId: auth.tenantId ?? '',
89
+ organizationId: auth.orgId ?? '',
90
+ executedBy: auth.sub ?? auth.email ?? undefined,
91
+ dryRun,
92
+ entityType,
93
+ entityId,
94
+ eventType,
95
+ }
96
+
97
+ try {
98
+ const result = await ruleEngine.executeRuleById(em, execContext)
99
+
100
+ const response = {
101
+ success: result.success,
102
+ ruleId: result.ruleId,
103
+ ruleName: result.ruleName,
104
+ conditionResult: result.conditionResult,
105
+ actionsExecuted: result.actionsExecuted ? {
106
+ success: result.actionsExecuted.success,
107
+ results: result.actionsExecuted.results.map(ar => ({
108
+ type: ar.action.type,
109
+ success: ar.success,
110
+ error: ar.error,
111
+ })),
112
+ } : null,
113
+ executionTime: result.executionTime,
114
+ error: result.error,
115
+ logId: result.logId,
116
+ }
117
+
118
+ // Return appropriate status based on result
119
+ const status = result.success ? 200 : (result.error === 'Rule not found' ? 404 : 200)
120
+ return NextResponse.json(response, { status })
121
+ } catch (error) {
122
+ const errorMessage = error instanceof Error ? error.message : String(error)
123
+ return NextResponse.json(
124
+ { error: `Rule execution failed: ${errorMessage}` },
125
+ { status: 500 }
126
+ )
127
+ }
128
+ }
129
+
130
+ export const openApi: OpenApiRouteDoc = {
131
+ tag: 'Business Rules',
132
+ summary: 'Execute a specific business rule by ID',
133
+ methods: {
134
+ POST: {
135
+ summary: 'Execute a specific rule by its database UUID',
136
+ description: 'Directly executes a specific business rule identified by its UUID, bypassing the normal entityType/eventType discovery mechanism. Useful for workflows and targeted rule execution.',
137
+ pathParams: z.object({
138
+ ruleId: z.string().uuid().describe('The database UUID of the business rule to execute'),
139
+ }),
140
+ requestBody: {
141
+ contentType: 'application/json',
142
+ schema: executeByIdRequestSchema,
143
+ },
144
+ responses: [
145
+ {
146
+ status: 200,
147
+ description: 'Rule executed successfully',
148
+ schema: executeByIdResponseSchema,
149
+ },
150
+ {
151
+ status: 404,
152
+ description: 'Rule not found',
153
+ schema: errorResponseSchema,
154
+ },
155
+ ],
156
+ errors: [
157
+ { status: 400, description: 'Invalid request payload or rule ID', schema: errorResponseSchema },
158
+ { status: 401, description: 'Unauthorized', schema: errorResponseSchema },
159
+ { status: 500, description: 'Execution error', schema: errorResponseSchema },
160
+ ],
161
+ },
162
+ },
163
+ }
@@ -287,3 +287,43 @@ export const ruleDiscoveryOptionsSchema = z.object({
287
287
  })
288
288
 
289
289
  export type RuleDiscoveryOptionsInput = z.infer<typeof ruleDiscoveryOptionsSchema>
290
+
291
+ // Direct Rule Execution Context Schema (for executing a specific rule by ID)
292
+ export const directRuleExecutionContextSchema = z.object({
293
+ ruleId: z.uuid('ruleId must be a valid UUID'),
294
+ data: z.any(),
295
+ user: z.looseObject({
296
+ id: z.string().optional(),
297
+ email: z.string().optional(),
298
+ role: z.string().optional(),
299
+ }).optional(),
300
+ tenantId: z.uuid('tenantId must be a valid UUID'),
301
+ organizationId: z.uuid('organizationId must be a valid UUID'),
302
+ executedBy: z.string().optional(),
303
+ dryRun: z.boolean().optional(),
304
+ entityType: z.string().optional(),
305
+ entityId: z.string().optional(),
306
+ eventType: z.string().optional(),
307
+ })
308
+
309
+ export type DirectRuleExecutionContextInput = z.infer<typeof directRuleExecutionContextSchema>
310
+
311
+ // Rule ID Execution Context Schema (for executing a specific rule by its string rule_id identifier)
312
+ export const ruleIdExecutionContextSchema = z.object({
313
+ ruleId: z.string().min(1, 'ruleId must be a non-empty string').max(50),
314
+ data: z.any(),
315
+ user: z.looseObject({
316
+ id: z.string().optional(),
317
+ email: z.string().optional(),
318
+ role: z.string().optional(),
319
+ }).optional(),
320
+ tenantId: z.uuid('tenantId must be a valid UUID'),
321
+ organizationId: z.uuid('organizationId must be a valid UUID'),
322
+ executedBy: z.string().optional(),
323
+ dryRun: z.boolean().optional(),
324
+ entityType: z.string().optional(),
325
+ entityId: z.string().optional(),
326
+ eventType: z.string().optional(),
327
+ })
328
+
329
+ export type RuleIdExecutionContextInput = z.infer<typeof ruleIdExecutionContextSchema>
@@ -8,3 +8,28 @@ export const metadata: ModuleInfo = {
8
8
  author: 'Patryk Lewczuk',
9
9
  license: 'Proprietary',
10
10
  }
11
+
12
+ // Export rule engine types and functions for programmatic usage
13
+ export {
14
+ executeRules,
15
+ executeRuleById,
16
+ executeRuleByRuleId,
17
+ executeSingleRule,
18
+ findApplicableRules,
19
+ logRuleExecution,
20
+ type RuleEngineContext,
21
+ type RuleEngineResult,
22
+ type RuleExecutionResult,
23
+ type RuleDiscoveryOptions,
24
+ type DirectRuleExecutionContext,
25
+ type DirectRuleExecutionResult,
26
+ type RuleIdExecutionContext,
27
+ } from './lib/rule-engine'
28
+
29
+ // Export validator schemas
30
+ export {
31
+ directRuleExecutionContextSchema,
32
+ ruleIdExecutionContextSchema,
33
+ type DirectRuleExecutionContextInput,
34
+ type RuleIdExecutionContextInput,
35
+ } from './data/validators'
@@ -5,7 +5,7 @@ import * as ruleEvaluator from './rule-evaluator'
5
5
  import * as actionExecutor from './action-executor'
6
6
  import type { RuleEvaluationContext } from './rule-evaluator'
7
7
  import type { ActionContext, ActionExecutionOutcome } from './action-executor'
8
- import { ruleEngineContextSchema, ruleDiscoveryOptionsSchema } from '../data/validators'
8
+ import { ruleEngineContextSchema, ruleDiscoveryOptionsSchema, directRuleExecutionContextSchema, ruleIdExecutionContextSchema } from '../data/validators'
9
9
 
10
10
  /**
11
11
  * Constants
@@ -99,6 +99,65 @@ type RuleExecutionFailedPayload = {
99
99
  organizationId?: string | null
100
100
  }
101
101
 
102
+ /**
103
+ * Direct rule execution context (for executing a specific rule by ID)
104
+ */
105
+ export interface DirectRuleExecutionContext {
106
+ ruleId: string // Database UUID of the rule
107
+ data: any
108
+ user?: {
109
+ id?: string
110
+ email?: string
111
+ role?: string
112
+ [key: string]: any
113
+ }
114
+ tenantId: string
115
+ organizationId: string
116
+ executedBy?: string
117
+ dryRun?: boolean
118
+ // Optional for logging (falls back to rule's entityType)
119
+ entityType?: string
120
+ entityId?: string
121
+ eventType?: string
122
+ }
123
+
124
+ /**
125
+ * Direct rule execution result
126
+ */
127
+ export interface DirectRuleExecutionResult {
128
+ success: boolean
129
+ ruleId: string
130
+ ruleName: string
131
+ conditionResult: boolean
132
+ actionsExecuted: ActionExecutionOutcome | null
133
+ executionTime: number
134
+ error?: string
135
+ logId?: string
136
+ }
137
+
138
+ /**
139
+ * Context for executing a rule by its string rule_id identifier
140
+ * Unlike DirectRuleExecutionContext which uses database UUID,
141
+ * this uses the string identifier (e.g., "workflow_checkout_inventory_available")
142
+ */
143
+ export interface RuleIdExecutionContext {
144
+ ruleId: string // String identifier (e.g., "workflow_checkout_inventory_available")
145
+ data: any
146
+ user?: {
147
+ id?: string
148
+ email?: string
149
+ role?: string
150
+ [key: string]: any
151
+ }
152
+ tenantId: string
153
+ organizationId: string
154
+ executedBy?: string
155
+ dryRun?: boolean
156
+ entityType?: string
157
+ entityId?: string
158
+ eventType?: string
159
+ }
160
+
102
161
  /**
103
162
  * Execute a function with a timeout
104
163
  */
@@ -449,6 +508,227 @@ export async function findApplicableRules(
449
508
  })
450
509
  }
451
510
 
511
+ /**
512
+ * Execute a specific rule by its database UUID
513
+ * This bypasses the entityType/eventType discovery mechanism and directly executes the rule
514
+ */
515
+ export async function executeRuleById(
516
+ em: EntityManager,
517
+ context: DirectRuleExecutionContext
518
+ ): Promise<DirectRuleExecutionResult> {
519
+ const startTime = Date.now()
520
+
521
+ // Validate input
522
+ const validation = directRuleExecutionContextSchema.safeParse(context)
523
+ if (!validation.success) {
524
+ const validationErrors = validation.error.issues.map(e => `${e.path.join('.')}: ${e.message}`)
525
+ return {
526
+ success: false,
527
+ ruleId: context.ruleId,
528
+ ruleName: 'Unknown',
529
+ conditionResult: false,
530
+ actionsExecuted: null,
531
+ executionTime: Date.now() - startTime,
532
+ error: `Validation failed: ${validationErrors.join(', ')}`,
533
+ }
534
+ }
535
+
536
+ // Fetch rule by ID with tenant/org validation
537
+ const rule = await em.findOne(BusinessRule, {
538
+ id: context.ruleId,
539
+ tenantId: context.tenantId,
540
+ organizationId: context.organizationId,
541
+ deletedAt: null,
542
+ })
543
+
544
+ if (!rule) {
545
+ return {
546
+ success: false,
547
+ ruleId: context.ruleId,
548
+ ruleName: 'Unknown',
549
+ conditionResult: false,
550
+ actionsExecuted: null,
551
+ executionTime: Date.now() - startTime,
552
+ error: 'Rule not found',
553
+ }
554
+ }
555
+
556
+ if (!rule.enabled) {
557
+ return {
558
+ success: false,
559
+ ruleId: rule.ruleId,
560
+ ruleName: rule.ruleName,
561
+ conditionResult: false,
562
+ actionsExecuted: null,
563
+ executionTime: Date.now() - startTime,
564
+ error: 'Rule is disabled',
565
+ }
566
+ }
567
+
568
+ // Check effective date range
569
+ const now = new Date()
570
+ if (rule.effectiveFrom && rule.effectiveFrom > now) {
571
+ return {
572
+ success: false,
573
+ ruleId: rule.ruleId,
574
+ ruleName: rule.ruleName,
575
+ conditionResult: false,
576
+ actionsExecuted: null,
577
+ executionTime: Date.now() - startTime,
578
+ error: `Rule is not yet effective (starts ${rule.effectiveFrom.toISOString()})`,
579
+ }
580
+ }
581
+ if (rule.effectiveTo && rule.effectiveTo < now) {
582
+ return {
583
+ success: false,
584
+ ruleId: rule.ruleId,
585
+ ruleName: rule.ruleName,
586
+ conditionResult: false,
587
+ actionsExecuted: null,
588
+ executionTime: Date.now() - startTime,
589
+ error: `Rule has expired (ended ${rule.effectiveTo.toISOString()})`,
590
+ }
591
+ }
592
+
593
+ // Build RuleEngineContext (use provided entityType or fall back to rule's)
594
+ const engineContext: RuleEngineContext = {
595
+ entityType: context.entityType || rule.entityType,
596
+ entityId: context.entityId,
597
+ eventType: context.eventType || rule.eventType || undefined,
598
+ data: context.data,
599
+ user: context.user,
600
+ tenantId: context.tenantId,
601
+ organizationId: context.organizationId,
602
+ executedBy: context.executedBy,
603
+ dryRun: context.dryRun,
604
+ }
605
+
606
+ // Execute via existing executeSingleRule
607
+ const result = await executeSingleRule(em, rule, engineContext)
608
+
609
+ return {
610
+ success: !result.error,
611
+ ruleId: rule.ruleId,
612
+ ruleName: rule.ruleName,
613
+ conditionResult: result.conditionResult,
614
+ actionsExecuted: result.actionsExecuted,
615
+ executionTime: result.executionTime,
616
+ error: result.error,
617
+ logId: result.logId,
618
+ }
619
+ }
620
+
621
+ /**
622
+ * Execute a rule by its string rule_id identifier
623
+ * Looks up rule by rule_id (string column) + tenant_id (unique constraint)
624
+ * This is useful for workflow conditions that reference rules by their string identifiers
625
+ */
626
+ export async function executeRuleByRuleId(
627
+ em: EntityManager,
628
+ context: RuleIdExecutionContext
629
+ ): Promise<DirectRuleExecutionResult> {
630
+ const startTime = Date.now()
631
+
632
+ // Validate input
633
+ const validation = ruleIdExecutionContextSchema.safeParse(context)
634
+ if (!validation.success) {
635
+ const validationErrors = validation.error.issues.map(e => `${e.path.join('.')}: ${e.message}`)
636
+ return {
637
+ success: false,
638
+ ruleId: context.ruleId || 'unknown',
639
+ ruleName: 'Unknown',
640
+ conditionResult: false,
641
+ actionsExecuted: null,
642
+ executionTime: Date.now() - startTime,
643
+ error: `Validation failed: ${validationErrors.join(', ')}`,
644
+ }
645
+ }
646
+
647
+ // Fetch rule by rule_id (string identifier) + tenant/org
648
+ const rule = await em.findOne(BusinessRule, {
649
+ ruleId: context.ruleId, // String identifier column
650
+ tenantId: context.tenantId,
651
+ organizationId: context.organizationId,
652
+ deletedAt: null,
653
+ })
654
+
655
+ if (!rule) {
656
+ return {
657
+ success: false,
658
+ ruleId: context.ruleId,
659
+ ruleName: 'Unknown',
660
+ conditionResult: false,
661
+ actionsExecuted: null,
662
+ executionTime: Date.now() - startTime,
663
+ error: 'Rule not found',
664
+ }
665
+ }
666
+
667
+ if (!rule.enabled) {
668
+ return {
669
+ success: false,
670
+ ruleId: rule.ruleId,
671
+ ruleName: rule.ruleName,
672
+ conditionResult: false,
673
+ actionsExecuted: null,
674
+ executionTime: Date.now() - startTime,
675
+ error: 'Rule is disabled',
676
+ }
677
+ }
678
+
679
+ // Check effective date range
680
+ const now = new Date()
681
+ if (rule.effectiveFrom && rule.effectiveFrom > now) {
682
+ return {
683
+ success: false,
684
+ ruleId: rule.ruleId,
685
+ ruleName: rule.ruleName,
686
+ conditionResult: false,
687
+ actionsExecuted: null,
688
+ executionTime: Date.now() - startTime,
689
+ error: `Rule is not yet effective (starts ${rule.effectiveFrom.toISOString()})`,
690
+ }
691
+ }
692
+ if (rule.effectiveTo && rule.effectiveTo < now) {
693
+ return {
694
+ success: false,
695
+ ruleId: rule.ruleId,
696
+ ruleName: rule.ruleName,
697
+ conditionResult: false,
698
+ actionsExecuted: null,
699
+ executionTime: Date.now() - startTime,
700
+ error: `Rule has expired (ended ${rule.effectiveTo.toISOString()})`,
701
+ }
702
+ }
703
+
704
+ // Build RuleEngineContext (use provided entityType or fall back to rule's)
705
+ const engineContext: RuleEngineContext = {
706
+ entityType: context.entityType || rule.entityType,
707
+ entityId: context.entityId,
708
+ eventType: context.eventType || rule.eventType || undefined,
709
+ data: context.data,
710
+ user: context.user,
711
+ tenantId: context.tenantId,
712
+ organizationId: context.organizationId,
713
+ executedBy: context.executedBy,
714
+ dryRun: context.dryRun,
715
+ }
716
+
717
+ // Execute via existing executeSingleRule
718
+ const result = await executeSingleRule(em, rule, engineContext)
719
+
720
+ return {
721
+ success: !result.error,
722
+ ruleId: rule.ruleId,
723
+ ruleName: rule.ruleName,
724
+ conditionResult: result.conditionResult,
725
+ actionsExecuted: result.actionsExecuted,
726
+ executionTime: result.executionTime,
727
+ error: result.error,
728
+ logId: result.logId,
729
+ }
730
+ }
731
+
452
732
  /**
453
733
  * Sensitive field patterns to exclude from logs
454
734
  */
@@ -0,0 +1,45 @@
1
+ import { createModuleEvents } from '@open-mercato/shared/modules/events'
2
+
3
+ /**
4
+ * Catalog Module Events
5
+ *
6
+ * Declares all events that can be emitted by the catalog module.
7
+ */
8
+ const events = [
9
+ // Products
10
+ { id: 'catalog.product.created', label: 'Product Created', entity: 'product', category: 'crud' },
11
+ { id: 'catalog.product.updated', label: 'Product Updated', entity: 'product', category: 'crud' },
12
+ { id: 'catalog.product.deleted', label: 'Product Deleted', entity: 'product', category: 'crud' },
13
+
14
+ // Categories
15
+ { id: 'catalog.category.created', label: 'Category Created', entity: 'category', category: 'crud' },
16
+ { id: 'catalog.category.updated', label: 'Category Updated', entity: 'category', category: 'crud' },
17
+ { id: 'catalog.category.deleted', label: 'Category Deleted', entity: 'category', category: 'crud' },
18
+
19
+ // Variants
20
+ { id: 'catalog.variant.created', label: 'Variant Created', entity: 'variant', category: 'crud' },
21
+ { id: 'catalog.variant.updated', label: 'Variant Updated', entity: 'variant', category: 'crud' },
22
+ { id: 'catalog.variant.deleted', label: 'Variant Deleted', entity: 'variant', category: 'crud' },
23
+
24
+ // Prices
25
+ { id: 'catalog.price.created', label: 'Price Created', entity: 'price', category: 'crud' },
26
+ { id: 'catalog.price.updated', label: 'Price Updated', entity: 'price', category: 'crud' },
27
+ { id: 'catalog.price.deleted', label: 'Price Deleted', entity: 'price', category: 'crud' },
28
+
29
+ // Lifecycle events - Pricing resolution
30
+ { id: 'catalog.pricing.resolve.before', label: 'Before Pricing Resolve', category: 'lifecycle', excludeFromTriggers: true },
31
+ { id: 'catalog.pricing.resolve.after', label: 'After Pricing Resolve', category: 'lifecycle', excludeFromTriggers: true },
32
+ ] as const
33
+
34
+ export const eventsConfig = createModuleEvents({
35
+ moduleId: 'catalog',
36
+ events,
37
+ })
38
+
39
+ /** Type-safe event emitter for catalog module */
40
+ export const emitCatalogEvent = eventsConfig.emit
41
+
42
+ /** Event IDs that can be emitted by the catalog module */
43
+ export type CatalogEventId = typeof events[number]['id']
44
+
45
+ export default eventsConfig