@open-mercato/core 0.4.2-canary-5d2c419a9b → 0.4.2-canary-9e0237de8e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/entities/workflow_event_trigger/index.js +33 -0
- package/dist/generated/entities/workflow_event_trigger/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +1 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/auth/events.js +30 -0
- package/dist/modules/auth/events.js.map +7 -0
- package/dist/modules/business_rules/api/execute/[ruleId]/route.js +145 -0
- package/dist/modules/business_rules/api/execute/[ruleId]/route.js.map +7 -0
- package/dist/modules/business_rules/data/validators.js +34 -0
- package/dist/modules/business_rules/data/validators.js.map +2 -2
- package/dist/modules/business_rules/index.js +21 -1
- package/dist/modules/business_rules/index.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-engine.js +182 -1
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/catalog/events.js +34 -0
- package/dist/modules/catalog/events.js.map +7 -0
- package/dist/modules/customers/events.js +49 -0
- package/dist/modules/customers/events.js.map +7 -0
- package/dist/modules/directory/events.js +23 -0
- package/dist/modules/directory/events.js.map +7 -0
- package/dist/modules/sales/acl.js +1 -0
- package/dist/modules/sales/acl.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +12 -0
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +62 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/events.js +63 -0
- package/dist/modules/sales/events.js.map +7 -0
- package/dist/modules/sales/lib/dictionaries.js +3 -0
- package/dist/modules/sales/lib/dictionaries.js.map +2 -2
- package/dist/modules/sales/lib/frontend/documentDataEvents.js +25 -0
- package/dist/modules/sales/lib/frontend/documentDataEvents.js.map +7 -0
- package/dist/modules/workflows/acl.js +2 -0
- package/dist/modules/workflows/acl.js.map +2 -2
- package/dist/modules/workflows/api/instances/route.js +18 -6
- package/dist/modules/workflows/api/instances/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/route.js +6 -1
- package/dist/modules/workflows/api/tasks/route.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/[id]/page.js +9 -1
- package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/[id]/page.meta.js +1 -1
- package/dist/modules/workflows/backend/definitions/[id]/page.meta.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/create/page.js +24 -15
- package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/create/page.meta.js +1 -1
- package/dist/modules/workflows/backend/definitions/create/page.meta.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +150 -132
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.meta.js +1 -1
- package/dist/modules/workflows/backend/definitions/visual-editor/page.meta.js.map +2 -2
- package/dist/modules/workflows/backend/events/[id]/page.js +1 -1
- package/dist/modules/workflows/backend/events/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/events/[id]/page.meta.js +2 -2
- package/dist/modules/workflows/backend/events/[id]/page.meta.js.map +2 -2
- package/dist/modules/workflows/backend/instances/[id]/page.meta.js +2 -2
- package/dist/modules/workflows/backend/instances/[id]/page.meta.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.js +1 -1
- package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.meta.js +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.meta.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/page.js +5 -6
- package/dist/modules/workflows/backend/tasks/page.js.map +2 -2
- package/dist/modules/workflows/cli.js +81 -3
- package/dist/modules/workflows/cli.js.map +3 -3
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js +481 -0
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +7 -0
- package/dist/modules/workflows/components/EventTriggersEditor.js +553 -0
- package/dist/modules/workflows/components/EventTriggersEditor.js.map +7 -0
- package/dist/modules/workflows/data/entities.js +64 -1
- package/dist/modules/workflows/data/entities.js.map +2 -2
- package/dist/modules/workflows/data/validators.js +115 -0
- package/dist/modules/workflows/data/validators.js.map +2 -2
- package/dist/modules/workflows/events.js +38 -0
- package/dist/modules/workflows/events.js.map +7 -0
- package/dist/modules/workflows/examples/checkout-demo-definition.json +1 -5
- package/dist/modules/workflows/examples/order-approval-definition.json +257 -0
- package/dist/modules/workflows/examples/order-approval-guard-rules.json +32 -0
- package/dist/modules/workflows/lib/activity-executor.js +75 -13
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/event-trigger-service.js +308 -0
- package/dist/modules/workflows/lib/event-trigger-service.js.map +7 -0
- package/dist/modules/workflows/lib/graph-utils.js +71 -2
- package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
- package/dist/modules/workflows/lib/seeds.js +17 -4
- package/dist/modules/workflows/lib/seeds.js.map +2 -2
- package/dist/modules/workflows/lib/start-validator.js +33 -23
- package/dist/modules/workflows/lib/start-validator.js.map +2 -2
- package/dist/modules/workflows/lib/transition-handler.js +157 -45
- package/dist/modules/workflows/lib/transition-handler.js.map +3 -3
- package/dist/modules/workflows/migrations/Migration20260123143500.js +36 -0
- package/dist/modules/workflows/migrations/Migration20260123143500.js.map +7 -0
- package/dist/modules/workflows/subscribers/event-trigger.js +78 -0
- package/dist/modules/workflows/subscribers/event-trigger.js.map +7 -0
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +323 -0
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +7 -0
- package/dist/modules/workflows/widgets/injection/order-approval/widget.js +17 -0
- package/dist/modules/workflows/widgets/injection/order-approval/widget.js.map +7 -0
- package/dist/modules/workflows/widgets/injection-table.js +19 -0
- package/dist/modules/workflows/widgets/injection-table.js.map +7 -0
- package/generated/entities/workflow_event_trigger/index.ts +15 -0
- package/generated/entities.ids.generated.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +2 -2
- package/src/modules/auth/events.ts +39 -0
- package/src/modules/business_rules/api/execute/[ruleId]/route.ts +163 -0
- package/src/modules/business_rules/data/validators.ts +40 -0
- package/src/modules/business_rules/index.ts +25 -0
- package/src/modules/business_rules/lib/rule-engine.ts +281 -1
- package/src/modules/catalog/events.ts +45 -0
- package/src/modules/customers/events.ts +63 -0
- package/src/modules/directory/events.ts +31 -0
- package/src/modules/sales/acl.ts +1 -0
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +16 -0
- package/src/modules/sales/commands/documents.ts +75 -1
- package/src/modules/sales/events.ts +82 -0
- package/src/modules/sales/lib/dictionaries.ts +3 -0
- package/src/modules/sales/lib/frontend/documentDataEvents.ts +28 -0
- package/src/modules/workflows/acl.ts +2 -0
- package/src/modules/workflows/api/__tests__/instances.route.test.ts +5 -2
- package/src/modules/workflows/api/instances/route.ts +21 -7
- package/src/modules/workflows/api/tasks/route.ts +7 -1
- package/src/modules/workflows/backend/definitions/[id]/page.meta.ts +1 -1
- package/src/modules/workflows/backend/definitions/[id]/page.tsx +9 -0
- package/src/modules/workflows/backend/definitions/create/page.meta.ts +1 -1
- package/src/modules/workflows/backend/definitions/create/page.tsx +9 -0
- package/src/modules/workflows/backend/definitions/visual-editor/page.meta.ts +1 -1
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +21 -3
- package/src/modules/workflows/backend/events/[id]/page.meta.ts +2 -2
- package/src/modules/workflows/backend/events/[id]/page.tsx +1 -1
- package/src/modules/workflows/backend/instances/[id]/page.meta.ts +2 -2
- package/src/modules/workflows/backend/tasks/[id]/page.meta.ts +2 -2
- package/src/modules/workflows/backend/tasks/[id]/page.tsx +1 -1
- package/src/modules/workflows/backend/tasks/page.tsx +5 -6
- package/src/modules/workflows/cli.ts +111 -0
- package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +581 -0
- package/src/modules/workflows/components/EventTriggersEditor.tsx +664 -0
- package/src/modules/workflows/data/entities.ts +124 -0
- package/src/modules/workflows/data/validators.ts +138 -0
- package/src/modules/workflows/events.ts +49 -0
- package/src/modules/workflows/examples/checkout-demo-definition.json +1 -5
- package/src/modules/workflows/examples/order-approval-definition.json +257 -0
- package/src/modules/workflows/examples/order-approval-guard-rules.json +32 -0
- package/src/modules/workflows/i18n/en.json +71 -0
- package/src/modules/workflows/lib/__tests__/activity-executor.test.ts +43 -36
- package/src/modules/workflows/lib/__tests__/transition-handler.test.ts +170 -90
- package/src/modules/workflows/lib/activity-executor.ts +129 -16
- package/src/modules/workflows/lib/event-trigger-service.ts +557 -0
- package/src/modules/workflows/lib/graph-utils.ts +117 -2
- package/src/modules/workflows/lib/seeds.ts +29 -8
- package/src/modules/workflows/lib/start-validator.ts +38 -28
- package/src/modules/workflows/lib/transition-handler.ts +208 -55
- package/src/modules/workflows/migrations/Migration20260123143500.ts +38 -0
- package/src/modules/workflows/subscribers/event-trigger.ts +109 -0
- package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +446 -0
- package/src/modules/workflows/widgets/injection/order-approval/widget.ts +16 -0
- package/src/modules/workflows/widgets/injection-table.ts +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.4.2-canary-
|
|
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-
|
|
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
|