@open-mercato/core 0.6.2-develop.3400.2.d948268f50 → 0.6.2-develop.3406.1.2b403f40da

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 (95) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/workflow_definition/index.js +2 -0
  3. package/dist/generated/entities/workflow_definition/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +1 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/modules/sales/workflows.js +180 -0
  7. package/dist/modules/sales/workflows.js.map +7 -0
  8. package/dist/modules/workflows/api/definitions/[id]/customize/route.js +172 -0
  9. package/dist/modules/workflows/api/definitions/[id]/customize/route.js.map +7 -0
  10. package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js +205 -0
  11. package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js.map +7 -0
  12. package/dist/modules/workflows/api/definitions/[id]/route.js +91 -2
  13. package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
  14. package/dist/modules/workflows/api/definitions/route.js +32 -9
  15. package/dist/modules/workflows/api/definitions/route.js.map +2 -2
  16. package/dist/modules/workflows/api/definitions/serialize.js +36 -1
  17. package/dist/modules/workflows/api/definitions/serialize.js.map +2 -2
  18. package/dist/modules/workflows/backend/definitions/[id]/page.js +161 -52
  19. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  20. package/dist/modules/workflows/backend/definitions/page.js +37 -34
  21. package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
  22. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +134 -41
  23. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  24. package/dist/modules/workflows/cli.js +19 -121
  25. package/dist/modules/workflows/cli.js.map +2 -2
  26. package/dist/modules/workflows/components/formConfig.js +1 -1
  27. package/dist/modules/workflows/components/formConfig.js.map +2 -2
  28. package/dist/modules/workflows/data/entities.js +3 -0
  29. package/dist/modules/workflows/data/entities.js.map +2 -2
  30. package/dist/modules/workflows/data/validators.js +2 -2
  31. package/dist/modules/workflows/data/validators.js.map +2 -2
  32. package/dist/modules/workflows/events.js +2 -0
  33. package/dist/modules/workflows/events.js.map +2 -2
  34. package/dist/modules/workflows/examples/guard-rules-example.json +6 -6
  35. package/dist/modules/workflows/examples/order-approval-guard-rules.json +2 -2
  36. package/dist/modules/workflows/frontend/checkout-demo/page.js +4 -4
  37. package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
  38. package/dist/modules/workflows/lib/code-registry.js +40 -0
  39. package/dist/modules/workflows/lib/code-registry.js.map +7 -0
  40. package/dist/modules/workflows/lib/find-definition.js +58 -0
  41. package/dist/modules/workflows/lib/find-definition.js.map +7 -0
  42. package/dist/modules/workflows/lib/seeds.js +0 -3
  43. package/dist/modules/workflows/lib/seeds.js.map +2 -2
  44. package/dist/modules/workflows/lib/start-validator.js +1 -20
  45. package/dist/modules/workflows/lib/start-validator.js.map +2 -2
  46. package/dist/modules/workflows/lib/workflow-executor.js +1 -19
  47. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  48. package/dist/modules/workflows/migrations/Migration20260414120000.js +13 -0
  49. package/dist/modules/workflows/migrations/Migration20260414120000.js.map +7 -0
  50. package/dist/modules/workflows/migrations/Migration20260428102318.js +32 -0
  51. package/dist/modules/workflows/migrations/Migration20260428102318.js.map +7 -0
  52. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +1 -1
  53. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +1 -1
  54. package/dist/modules/workflows/workflows.js +260 -0
  55. package/dist/modules/workflows/workflows.js.map +7 -0
  56. package/generated/entities/workflow_definition/index.ts +1 -0
  57. package/generated/entity-fields-registry.ts +1 -0
  58. package/package.json +7 -7
  59. package/src/modules/sales/workflows.ts +184 -0
  60. package/src/modules/workflows/api/definitions/[id]/customize/route.ts +197 -0
  61. package/src/modules/workflows/api/definitions/[id]/reset-to-code/route.ts +252 -0
  62. package/src/modules/workflows/api/definitions/[id]/route.ts +108 -4
  63. package/src/modules/workflows/api/definitions/route.ts +54 -10
  64. package/src/modules/workflows/api/definitions/serialize.ts +42 -0
  65. package/src/modules/workflows/backend/definitions/[id]/page.tsx +135 -11
  66. package/src/modules/workflows/backend/definitions/page.tsx +45 -33
  67. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +222 -97
  68. package/src/modules/workflows/cli.ts +32 -155
  69. package/src/modules/workflows/components/formConfig.tsx +2 -2
  70. package/src/modules/workflows/data/entities.ts +4 -1
  71. package/src/modules/workflows/data/validators.ts +4 -3
  72. package/src/modules/workflows/events.ts +2 -0
  73. package/src/modules/workflows/examples/guard-rules-example.json +6 -6
  74. package/src/modules/workflows/examples/order-approval-guard-rules.json +2 -2
  75. package/src/modules/workflows/frontend/checkout-demo/page.tsx +4 -4
  76. package/src/modules/workflows/i18n/de.json +16 -0
  77. package/src/modules/workflows/i18n/en.json +16 -0
  78. package/src/modules/workflows/i18n/es.json +16 -0
  79. package/src/modules/workflows/i18n/pl.json +16 -0
  80. package/src/modules/workflows/lib/code-registry.ts +65 -0
  81. package/src/modules/workflows/lib/find-definition.ts +100 -0
  82. package/src/modules/workflows/lib/seeds.ts +4 -3
  83. package/src/modules/workflows/lib/start-validator.ts +2 -35
  84. package/src/modules/workflows/lib/workflow-executor.ts +2 -39
  85. package/src/modules/workflows/migrations/.snapshot-open-mercato.json +10 -0
  86. package/src/modules/workflows/migrations/Migration20260414120000.ts +13 -0
  87. package/src/modules/workflows/migrations/Migration20260428102318.ts +57 -0
  88. package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +1 -1
  89. package/src/modules/workflows/workflows.ts +278 -0
  90. package/dist/modules/workflows/examples/checkout-demo-definition.json +0 -486
  91. package/dist/modules/workflows/examples/order-approval-definition.json +0 -257
  92. package/dist/modules/workflows/examples/simple-approval-definition.json +0 -134
  93. package/src/modules/workflows/examples/checkout-demo-definition.json +0 -486
  94. package/src/modules/workflows/examples/order-approval-definition.json +0 -257
  95. package/src/modules/workflows/examples/simple-approval-definition.json +0 -134
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Sales Module — Code-Based Workflow Definitions
3
+ *
4
+ * Order approval workflow shipped with the sales module.
5
+ */
6
+
7
+ import { defineWorkflow, createWorkflowsModuleConfig } from '@open-mercato/shared/modules/workflows'
8
+
9
+ const orderApproval = defineWorkflow({
10
+ workflowId: 'sales.order-approval',
11
+ workflowName: 'Sales Order Approval Workflow',
12
+ description: 'Approval workflow for sales orders requiring authorization before processing',
13
+ metadata: { category: 'Sales', tags: ['sales', 'approval', 'order'], icon: 'check-circle' },
14
+ steps: [
15
+ { stepId: 'start', stepName: 'Start', stepType: 'START', description: 'Initialize order approval workflow' },
16
+ {
17
+ stepId: 'pending_approval',
18
+ stepName: 'Pending Approval',
19
+ stepType: 'USER_TASK',
20
+ description: 'Order awaiting approval decision',
21
+ userTaskConfig: {
22
+ formSchema: {
23
+ type: 'object',
24
+ required: ['decision'],
25
+ properties: {
26
+ comments: { type: 'string', title: 'Comments', description: 'Optional comments for the decision' },
27
+ decision: { enum: ['approve', 'reject'], type: 'string', title: 'Decision', description: 'Approve or reject the order' },
28
+ },
29
+ },
30
+ slaDuration: 'PT24H',
31
+ },
32
+ },
33
+ { stepId: 'approved', stepName: 'Approved', stepType: 'AUTOMATED', description: 'Order has been approved' },
34
+ { stepId: 'rejected', stepName: 'Rejected', stepType: 'AUTOMATED', description: 'Order has been rejected' },
35
+ { stepId: 'end', stepName: 'Complete', stepType: 'END', description: 'Workflow complete' },
36
+ ] as const,
37
+ transitions: [
38
+ {
39
+ transitionId: 'start_to_pending',
40
+ transitionName: 'Submit for Approval',
41
+ fromStepId: 'start',
42
+ toStepId: 'pending_approval',
43
+ trigger: 'auto',
44
+ priority: 100,
45
+ continueOnActivityFailure: true,
46
+ activities: [
47
+ {
48
+ activityId: 'set_pending_status',
49
+ activityName: 'Set Order to Pending Approval',
50
+ activityType: 'UPDATE_ENTITY',
51
+ async: false,
52
+ config: {
53
+ commandId: 'sales.orders.update',
54
+ statusDictionary: 'sales.order_status',
55
+ input: { id: '{{context.id}}', statusValue: 'pending_approval' },
56
+ },
57
+ retryPolicy: { maxAttempts: 3, initialIntervalMs: 1000, backoffCoefficient: 2, maxIntervalMs: 10000 },
58
+ },
59
+ {
60
+ activityId: 'emit_approval_requested',
61
+ activityName: 'Emit Approval Requested Event',
62
+ activityType: 'EMIT_EVENT',
63
+ async: true,
64
+ config: {
65
+ eventName: 'sales.order.approval.requested',
66
+ payload: { orderId: '{{context.id}}', workflowInstanceId: '{{workflow.instanceId}}' },
67
+ },
68
+ },
69
+ ],
70
+ },
71
+ {
72
+ transitionId: 'pending_approval_to_approved',
73
+ transitionName: 'Approve Order',
74
+ fromStepId: 'pending_approval',
75
+ toStepId: 'approved',
76
+ trigger: 'auto',
77
+ priority: 100,
78
+ preConditions: [{ ruleId: 'workflow_order_approval_check_approved', required: true }],
79
+ continueOnActivityFailure: true,
80
+ activities: [
81
+ {
82
+ activityId: 'set_approved_status',
83
+ activityName: 'Set Order to Approved',
84
+ activityType: 'UPDATE_ENTITY',
85
+ async: false,
86
+ config: {
87
+ commandId: 'sales.orders.update',
88
+ statusDictionary: 'sales.order_status',
89
+ input: { id: '{{context.id}}', statusValue: 'approved' },
90
+ },
91
+ retryPolicy: { maxAttempts: 3, initialIntervalMs: 1000, backoffCoefficient: 2, maxIntervalMs: 10000 },
92
+ },
93
+ {
94
+ activityId: 'emit_order_approved',
95
+ activityName: 'Emit Order Approved Event',
96
+ activityType: 'EMIT_EVENT',
97
+ async: true,
98
+ config: {
99
+ eventName: 'sales.order.approval.approved',
100
+ payload: {
101
+ orderId: '{{context.id}}',
102
+ workflowInstanceId: '{{workflow.instanceId}}',
103
+ approvedBy: '{{context.completedBy}}',
104
+ comments: '{{context.comments}}',
105
+ },
106
+ },
107
+ },
108
+ ],
109
+ },
110
+ {
111
+ transitionId: 'pending_approval_to_rejected',
112
+ transitionName: 'Reject Order',
113
+ fromStepId: 'pending_approval',
114
+ toStepId: 'rejected',
115
+ trigger: 'auto',
116
+ priority: 90,
117
+ preConditions: [{ ruleId: 'workflow_order_approval_check_rejected', required: true }],
118
+ continueOnActivityFailure: true,
119
+ activities: [
120
+ {
121
+ activityId: 'set_rejected_status',
122
+ activityName: 'Set Order to Rejected',
123
+ activityType: 'UPDATE_ENTITY',
124
+ async: false,
125
+ config: {
126
+ commandId: 'sales.orders.update',
127
+ statusDictionary: 'sales.order_status',
128
+ input: { id: '{{context.id}}', statusValue: 'rejected' },
129
+ },
130
+ retryPolicy: { maxAttempts: 3, initialIntervalMs: 1000, backoffCoefficient: 2, maxIntervalMs: 10000 },
131
+ },
132
+ {
133
+ activityId: 'emit_order_rejected',
134
+ activityName: 'Emit Order Rejected Event',
135
+ activityType: 'EMIT_EVENT',
136
+ async: true,
137
+ config: {
138
+ eventName: 'sales.order.approval.rejected',
139
+ payload: {
140
+ orderId: '{{context.id}}',
141
+ workflowInstanceId: '{{workflow.instanceId}}',
142
+ rejectedBy: '{{context.completedBy}}',
143
+ comments: '{{context.comments}}',
144
+ },
145
+ },
146
+ },
147
+ ],
148
+ },
149
+ {
150
+ transitionId: 'approved_to_end',
151
+ transitionName: 'Complete After Approval',
152
+ fromStepId: 'approved',
153
+ toStepId: 'end',
154
+ trigger: 'auto',
155
+ priority: 100,
156
+ continueOnActivityFailure: true,
157
+ },
158
+ {
159
+ transitionId: 'rejected_to_end',
160
+ transitionName: 'Complete After Rejection',
161
+ fromStepId: 'rejected',
162
+ toStepId: 'end',
163
+ trigger: 'auto',
164
+ priority: 100,
165
+ continueOnActivityFailure: true,
166
+ },
167
+ ],
168
+ triggers: [{
169
+ triggerId: 'order_approval_trigger',
170
+ name: 'Order Approval Trigger',
171
+ description: 'Triggers when a new sales order is created',
172
+ eventPattern: 'sales.orders.created',
173
+ config: { entityType: 'SalesOrder' },
174
+ enabled: true,
175
+ priority: 0,
176
+ }],
177
+ })
178
+
179
+ export const workflowsConfig = createWorkflowsModuleConfig({
180
+ moduleId: 'sales',
181
+ workflows: [orderApproval],
182
+ })
183
+
184
+ export default workflowsConfig
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Customize Code-Based Workflow Definition
3
+ *
4
+ * POST /api/workflows/definitions/[id]/customize
5
+ *
6
+ * Creates a DB override row for a code-based workflow definition, seeded from
7
+ * the current code registry values. The id param must be of the form
8
+ * "code:<workflowId>".
9
+ */
10
+
11
+ import { NextRequest, NextResponse } from 'next/server'
12
+ import { z } from 'zod'
13
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
14
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
15
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
16
+ import { validateCrudMutationGuard, runCrudMutationGuardAfterSuccess } from '@open-mercato/shared/lib/crud/mutation-guard'
17
+ import { WorkflowDefinition } from '../../../../data/entities'
18
+ import { serializeWorkflowDefinition } from '../../serialize'
19
+ import { getCodeWorkflow } from '../../../../lib/code-registry'
20
+
21
+ export const metadata = {
22
+ requireAuth: true,
23
+ requireFeatures: ['workflows.definitions.edit'],
24
+ }
25
+
26
+ interface RouteContext {
27
+ params: Promise<{
28
+ id: string
29
+ }>
30
+ }
31
+
32
+ export async function POST(request: NextRequest, context: RouteContext) {
33
+ try {
34
+ const params = await context.params
35
+ const container = await createRequestContainer()
36
+ const em = container.resolve('em')
37
+ const auth = await getAuthFromRequest(request)
38
+
39
+ if (!auth) {
40
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
41
+ }
42
+
43
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request })
44
+ const tenantId = auth.tenantId
45
+ const organizationId = scope?.selectedId ?? auth.orgId
46
+
47
+ if (!params.id.startsWith('code:')) {
48
+ return NextResponse.json(
49
+ { error: 'Customize is only supported for code-based workflow definitions' },
50
+ { status: 400 },
51
+ )
52
+ }
53
+
54
+ const workflowId = params.id.slice(5)
55
+ const codeDef = getCodeWorkflow(workflowId)
56
+ if (!codeDef) {
57
+ return NextResponse.json({ error: 'Workflow definition not found' }, { status: 404 })
58
+ }
59
+
60
+ const guardResult = await validateCrudMutationGuard(container, {
61
+ tenantId: tenantId ?? '',
62
+ organizationId: organizationId ?? null,
63
+ userId: auth.sub ?? '',
64
+ resourceKind: 'workflows.definition',
65
+ resourceId: params.id,
66
+ operation: 'custom',
67
+ requestMethod: 'POST',
68
+ requestHeaders: request.headers,
69
+ })
70
+ if (guardResult && !guardResult.ok) {
71
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
72
+ }
73
+
74
+ const existingOverride = await em.findOne(WorkflowDefinition, {
75
+ workflowId: codeDef.workflowId,
76
+ tenantId,
77
+ })
78
+
79
+ let saved: WorkflowDefinition
80
+ if (existingOverride) {
81
+ existingOverride.deletedAt = null
82
+ existingOverride.workflowName = codeDef.workflowName
83
+ existingOverride.description = codeDef.description
84
+ existingOverride.version = codeDef.version
85
+ existingOverride.definition = codeDef.definition
86
+ existingOverride.metadata = codeDef.metadata ?? null
87
+ existingOverride.enabled = codeDef.enabled
88
+ existingOverride.codeWorkflowId = codeDef.workflowId
89
+ existingOverride.updatedBy = auth.sub
90
+ existingOverride.updatedAt = new Date()
91
+ await em.flush()
92
+ saved = existingOverride
93
+ } else {
94
+ const override = em.create(WorkflowDefinition, {
95
+ workflowId: codeDef.workflowId,
96
+ workflowName: codeDef.workflowName,
97
+ description: codeDef.description,
98
+ version: codeDef.version,
99
+ definition: codeDef.definition,
100
+ metadata: codeDef.metadata,
101
+ enabled: codeDef.enabled,
102
+ codeWorkflowId: codeDef.workflowId,
103
+ tenantId,
104
+ organizationId,
105
+ createdBy: auth.sub,
106
+ updatedBy: auth.sub,
107
+ createdAt: new Date(),
108
+ updatedAt: new Date(),
109
+ })
110
+ em.persist(override)
111
+ await em.flush()
112
+ saved = override
113
+ }
114
+
115
+ if (guardResult?.shouldRunAfterSuccess) {
116
+ await runCrudMutationGuardAfterSuccess(container, {
117
+ tenantId: tenantId ?? '',
118
+ organizationId: organizationId ?? null,
119
+ userId: auth.sub ?? '',
120
+ resourceKind: 'workflows.definition',
121
+ resourceId: String(saved.id),
122
+ operation: 'custom',
123
+ requestMethod: 'POST',
124
+ requestHeaders: request.headers,
125
+ metadata: guardResult.metadata,
126
+ })
127
+ }
128
+
129
+ try {
130
+ const eventBus = container.resolve('eventBus') as
131
+ | { emitEvent(event: string, payload: unknown, options?: unknown): Promise<void> }
132
+ | undefined
133
+ if (eventBus && typeof eventBus.emitEvent === 'function') {
134
+ await eventBus.emitEvent(
135
+ 'workflows.definition.customized',
136
+ {
137
+ id: saved.id,
138
+ workflowId: saved.workflowId,
139
+ codeWorkflowId: saved.codeWorkflowId ?? null,
140
+ tenantId: saved.tenantId,
141
+ organizationId: saved.organizationId,
142
+ userId: auth.sub ?? null,
143
+ },
144
+ { tenantId: saved.tenantId, organizationId: saved.organizationId, persistent: true },
145
+ )
146
+ }
147
+ } catch (eventError) {
148
+ console.error('Failed to emit workflows.definition.customized event:', eventError)
149
+ }
150
+
151
+ return NextResponse.json({
152
+ data: serializeWorkflowDefinition(saved),
153
+ message: 'Workflow definition customized successfully',
154
+ })
155
+ } catch (error) {
156
+ console.error('Error customizing workflow definition:', error)
157
+ return NextResponse.json({ error: 'Failed to customize workflow definition' }, { status: 500 })
158
+ }
159
+ }
160
+
161
+ export const openApi = {
162
+ methods: {
163
+ POST: {
164
+ summary: 'Customize code-based workflow definition',
165
+ description: 'Creates a DB override for a code-based workflow definition, seeded from the current code registry values. The id param must be of the form "code:<workflowId>".',
166
+ tags: ['Workflows'],
167
+ pathParams: z.object({
168
+ id: z.string().describe('Must be of the form "code:<workflowId>"'),
169
+ }),
170
+ responses: [
171
+ {
172
+ status: 200,
173
+ description: 'Workflow definition customized successfully',
174
+ example: {
175
+ data: {
176
+ id: '123e4567-e89b-12d3-a456-426614174000',
177
+ workflowId: 'workflows.simple-approval',
178
+ workflowName: 'Simple Approval Workflow',
179
+ source: 'code_override',
180
+ },
181
+ message: 'Workflow definition customized successfully',
182
+ },
183
+ },
184
+ {
185
+ status: 400,
186
+ description: 'Not a code-based id',
187
+ example: { error: 'Customize is only supported for code-based workflow definitions' },
188
+ },
189
+ {
190
+ status: 404,
191
+ description: 'Code workflow not found',
192
+ example: { error: 'Workflow definition not found' },
193
+ },
194
+ ],
195
+ },
196
+ },
197
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Reset Customized Workflow Definition to Code
3
+ *
4
+ * POST /api/workflows/definitions/[id]/reset-to-code
5
+ *
6
+ * Deletes the DB override row for a code-based workflow definition,
7
+ * reverting it back to the original code registry version.
8
+ */
9
+
10
+ import { NextRequest, NextResponse } from 'next/server'
11
+ import { z } from 'zod'
12
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
13
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
14
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
15
+ import { validateCrudMutationGuard, runCrudMutationGuardAfterSuccess } from '@open-mercato/shared/lib/crud/mutation-guard'
16
+ import { WorkflowDefinition, WorkflowInstance } from '../../../../data/entities'
17
+ import { serializeCodeWorkflowDefinition } from '../../serialize'
18
+ import { getCodeWorkflow } from '../../../../lib/code-registry'
19
+
20
+ export const metadata = {
21
+ requireAuth: true,
22
+ requireFeatures: ['workflows.definitions.edit'],
23
+ }
24
+
25
+ interface RouteContext {
26
+ params: Promise<{
27
+ id: string
28
+ }>
29
+ }
30
+
31
+ /**
32
+ * POST /api/workflows/definitions/[id]/reset-to-code
33
+ *
34
+ * Reset a customized code workflow definition back to its original code version.
35
+ */
36
+ export async function POST(
37
+ request: NextRequest,
38
+ context: RouteContext
39
+ ) {
40
+ try {
41
+ const params = await context.params
42
+ const container = await createRequestContainer()
43
+ const em = container.resolve('em')
44
+ const auth = await getAuthFromRequest(request)
45
+
46
+ if (!auth) {
47
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
48
+ }
49
+
50
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request })
51
+ const tenantId = auth.tenantId
52
+ const organizationId = scope?.selectedId ?? auth.orgId
53
+
54
+ // Check edit permission
55
+ const rbacService = container.resolve('rbacService')
56
+ const hasPermission = await rbacService.userHasAllFeatures(
57
+ auth.sub,
58
+ ['workflows.definitions.edit'],
59
+ {
60
+ tenantId,
61
+ organizationId,
62
+ }
63
+ )
64
+
65
+ if (!hasPermission) {
66
+ return NextResponse.json(
67
+ { error: 'Insufficient permissions' },
68
+ { status: 403 }
69
+ )
70
+ }
71
+
72
+ // Find existing definition
73
+ const definition = await em.findOne(WorkflowDefinition, {
74
+ id: params.id,
75
+ tenantId,
76
+ organizationId,
77
+ deletedAt: null,
78
+ })
79
+
80
+ if (!definition) {
81
+ return NextResponse.json(
82
+ { error: 'Workflow definition not found' },
83
+ { status: 404 }
84
+ )
85
+ }
86
+
87
+ // Verify this is a code-based override
88
+ if (!definition.codeWorkflowId) {
89
+ return NextResponse.json(
90
+ { error: 'This workflow definition is not a code-based override and cannot be reset' },
91
+ { status: 400 }
92
+ )
93
+ }
94
+
95
+ // Check if there are active workflow instances using this definition
96
+ const activeInstances = await em.count(WorkflowInstance, {
97
+ definitionId: definition.id,
98
+ status: { $in: ['RUNNING', 'WAITING'] },
99
+ })
100
+
101
+ if (activeInstances > 0) {
102
+ return NextResponse.json(
103
+ {
104
+ error: `Cannot reset workflow definition with ${activeInstances} active instance(s)`,
105
+ },
106
+ { status: 409 }
107
+ )
108
+ }
109
+
110
+ const guardResult = await validateCrudMutationGuard(container, {
111
+ tenantId: tenantId ?? '',
112
+ organizationId: organizationId ?? null,
113
+ userId: auth.sub ?? '',
114
+ resourceKind: 'workflows.definition',
115
+ resourceId: String(definition.id),
116
+ operation: 'custom',
117
+ requestMethod: 'POST',
118
+ requestHeaders: request.headers,
119
+ })
120
+ if (guardResult && !guardResult.ok) {
121
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
122
+ }
123
+
124
+ // Look up the original code definition before deleting
125
+ const codeDef = getCodeWorkflow(definition.codeWorkflowId)
126
+
127
+ // Snapshot identifying fields before remove() detaches the entity
128
+ const removedSnapshot = {
129
+ id: String(definition.id),
130
+ workflowId: definition.workflowId,
131
+ codeWorkflowId: definition.codeWorkflowId,
132
+ tenantId: definition.tenantId,
133
+ organizationId: definition.organizationId,
134
+ }
135
+
136
+ // Hard-delete the DB override row
137
+ em.remove(definition)
138
+ await em.flush()
139
+
140
+ if (guardResult?.shouldRunAfterSuccess) {
141
+ await runCrudMutationGuardAfterSuccess(container, {
142
+ tenantId: tenantId ?? '',
143
+ organizationId: organizationId ?? null,
144
+ userId: auth.sub ?? '',
145
+ resourceKind: 'workflows.definition',
146
+ resourceId: removedSnapshot.id,
147
+ operation: 'custom',
148
+ requestMethod: 'POST',
149
+ requestHeaders: request.headers,
150
+ metadata: guardResult.metadata,
151
+ })
152
+ }
153
+
154
+ try {
155
+ const eventBus = container.resolve('eventBus') as
156
+ | { emitEvent(event: string, payload: unknown, options?: unknown): Promise<void> }
157
+ | undefined
158
+ if (eventBus && typeof eventBus.emitEvent === 'function') {
159
+ await eventBus.emitEvent(
160
+ 'workflows.definition.reset_to_code',
161
+ {
162
+ id: removedSnapshot.id,
163
+ workflowId: removedSnapshot.workflowId,
164
+ codeWorkflowId: removedSnapshot.codeWorkflowId,
165
+ tenantId: removedSnapshot.tenantId,
166
+ organizationId: removedSnapshot.organizationId,
167
+ userId: auth.sub ?? null,
168
+ },
169
+ {
170
+ tenantId: removedSnapshot.tenantId,
171
+ organizationId: removedSnapshot.organizationId,
172
+ persistent: true,
173
+ },
174
+ )
175
+ }
176
+ } catch (eventError) {
177
+ console.error('Failed to emit workflows.definition.reset_to_code event:', eventError)
178
+ }
179
+
180
+ if (!codeDef) {
181
+ return NextResponse.json({
182
+ message: 'Workflow definition reset to code version (code definition no longer registered)',
183
+ data: null,
184
+ })
185
+ }
186
+
187
+ const syntheticId = `code:${codeDef.workflowId}`
188
+
189
+ return NextResponse.json({
190
+ data: serializeCodeWorkflowDefinition(codeDef, syntheticId),
191
+ message: 'Workflow definition reset to code version',
192
+ })
193
+ } catch (error) {
194
+ console.error('Error resetting workflow definition to code:', error)
195
+ return NextResponse.json(
196
+ { error: 'Failed to reset workflow definition to code' },
197
+ { status: 500 }
198
+ )
199
+ }
200
+ }
201
+
202
+ export const openApi = {
203
+ methods: {
204
+ POST: {
205
+ summary: 'Reset workflow definition to code version',
206
+ description: 'Deletes the DB override for a code-based workflow definition, reverting it to the original code registry version. Cannot be reset if there are active instances.',
207
+ tags: ['Workflows'],
208
+ pathParams: z.object({
209
+ id: z.string().uuid(),
210
+ }),
211
+ responses: [
212
+ {
213
+ status: 200,
214
+ description: 'Workflow definition reset to code version',
215
+ example: {
216
+ data: {
217
+ id: 'code:checkout-flow',
218
+ workflowId: 'checkout-flow',
219
+ workflowName: 'Checkout Flow',
220
+ description: 'Code-defined checkout workflow',
221
+ version: 1,
222
+ source: 'code',
223
+ isCodeBased: true,
224
+ },
225
+ message: 'Workflow definition reset to code version',
226
+ },
227
+ },
228
+ {
229
+ status: 400,
230
+ description: 'Definition is not a code-based override',
231
+ example: {
232
+ error: 'This workflow definition is not a code-based override and cannot be reset',
233
+ },
234
+ },
235
+ {
236
+ status: 404,
237
+ description: 'Workflow definition not found',
238
+ example: {
239
+ error: 'Workflow definition not found',
240
+ },
241
+ },
242
+ {
243
+ status: 409,
244
+ description: 'Cannot reset - active workflow instances exist',
245
+ example: {
246
+ error: 'Cannot reset workflow definition with 3 active instance(s)',
247
+ },
248
+ },
249
+ ],
250
+ },
251
+ },
252
+ }