@gadmin2n/schematics 0.0.151 → 0.0.153
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/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/.seed-timestamp +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/index.ts +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +23 -44
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +25 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +74 -64
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.ts +0 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +17 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +58 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.ts +66 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution-idempotency.spec.ts +61 -149
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.spec.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.spec.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +38 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +18 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.dto.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +131 -242
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +7 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +6 -36
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +0 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +29 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +29 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +20 -17
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +38 -35
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.tsx +6 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.tsx +24 -22
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/EnhancedFlowRenderer.tsx +0 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/StatusCards.tsx +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +20 -18
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +7 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +28 -79
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instances.tsx +5 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/NodeInstanceForm.tsx +0 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +227 -97
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +62 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/settings/index.tsx +73 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +21 -46
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/newNodeConfig.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2026-09-18T04:59:46.662Z
|
|
@@ -31,12 +31,13 @@ export async function main() {
|
|
|
31
31
|
|
|
32
32
|
await seedBootstrap(prisma);
|
|
33
33
|
|
|
34
|
+
// Workflow 先建父菜单与「功能设置」页(含旧 4 页面的合并迁移),permission 随后。
|
|
35
|
+
await seedWorkflowModule(prisma);
|
|
34
36
|
await seedPermissionModule(prisma);
|
|
35
37
|
await seedAgendaModule(prisma);
|
|
36
38
|
await seedAuditModule(prisma);
|
|
37
39
|
await seedCanvasModule(prisma);
|
|
38
40
|
await seedGameModule(prisma);
|
|
39
|
-
await seedWorkflowModule(prisma);
|
|
40
41
|
|
|
41
42
|
await seedDataMngtPages(prisma);
|
|
42
43
|
}
|
|
@@ -12,8 +12,9 @@ import {
|
|
|
12
12
|
*
|
|
13
13
|
* 职责(仅本模块自家):
|
|
14
14
|
* 1. permission 父分组 page (顶级 admin/administration → permission)
|
|
15
|
-
* 2.
|
|
16
|
-
*
|
|
15
|
+
* 2. 5 个 leaf page,均挂在 permission 下
|
|
16
|
+
* (原 person_directory / people_group 已并入 Workflow 的「功能设置」页,
|
|
17
|
+
* 由 workflow.seed.ts 声明并迁移)
|
|
17
18
|
* 3. permission 模块所需 resource 及页面绑定
|
|
18
19
|
* 4. PageResource 绑定(page ↔ resource,多对多)
|
|
19
20
|
* 5. SYSTEM_ADMIN 对上述全部 page + resource 的全量授权
|
|
@@ -54,32 +55,35 @@ export async function seedPermissionModule(
|
|
|
54
55
|
zhName: '菜单指引',
|
|
55
56
|
enName: 'Menu Guide',
|
|
56
57
|
sortOrder: 1005,
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
code: 'person_directory',
|
|
60
|
-
zhName: '人员目录',
|
|
61
|
-
enName: 'People Directory',
|
|
62
|
-
sortOrder: 1010,
|
|
58
|
+
parentId: permissionPage.id,
|
|
63
59
|
},
|
|
64
60
|
{
|
|
65
61
|
code: 'user',
|
|
66
|
-
zhName: '
|
|
67
|
-
enName: '
|
|
62
|
+
zhName: '用户管理',
|
|
63
|
+
enName: 'User Mgnt',
|
|
68
64
|
sortOrder: 1020,
|
|
65
|
+
parentId: permissionPage.id,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
code: 'role',
|
|
69
|
+
zhName: '角色管理',
|
|
70
|
+
enName: 'Role Mgnt',
|
|
71
|
+
sortOrder: 1040,
|
|
72
|
+
parentId: permissionPage.id,
|
|
69
73
|
},
|
|
70
74
|
{
|
|
71
|
-
code: '
|
|
72
|
-
zhName: '
|
|
73
|
-
enName: '
|
|
74
|
-
sortOrder:
|
|
75
|
+
code: 'page',
|
|
76
|
+
zhName: '页面管理',
|
|
77
|
+
enName: 'Page Mgnt',
|
|
78
|
+
sortOrder: 1050,
|
|
79
|
+
parentId: permissionPage.id,
|
|
75
80
|
},
|
|
76
|
-
{ code: 'role', zhName: '角色管理', enName: 'Role Mgnt', sortOrder: 1040 },
|
|
77
|
-
{ code: 'page', zhName: '页面管理', enName: 'Page Mgnt', sortOrder: 1050 },
|
|
78
81
|
{
|
|
79
82
|
code: 'resource',
|
|
80
83
|
zhName: '资源管理',
|
|
81
84
|
enName: 'Resource Management',
|
|
82
85
|
sortOrder: 1060,
|
|
86
|
+
parentId: permissionPage.id,
|
|
83
87
|
},
|
|
84
88
|
];
|
|
85
89
|
|
|
@@ -95,13 +99,13 @@ export async function seedPermissionModule(
|
|
|
95
99
|
zhName: def.zhName,
|
|
96
100
|
enName: def.enName,
|
|
97
101
|
sortOrder: def.sortOrder,
|
|
98
|
-
parentId:
|
|
102
|
+
parentId: def.parentId,
|
|
99
103
|
},
|
|
100
104
|
{
|
|
101
105
|
zhName: def.zhName,
|
|
102
106
|
enName: def.enName,
|
|
103
107
|
sortOrder: def.sortOrder,
|
|
104
|
-
parentId:
|
|
108
|
+
parentId: def.parentId,
|
|
105
109
|
},
|
|
106
110
|
);
|
|
107
111
|
pagesByCode.set(p.code, p.id);
|
|
@@ -114,32 +118,7 @@ export async function seedPermissionModule(
|
|
|
114
118
|
description: string;
|
|
115
119
|
pageCode: string;
|
|
116
120
|
}[] = [
|
|
117
|
-
{
|
|
118
|
-
code: 'user',
|
|
119
|
-
description: 'People directory',
|
|
120
|
-
pageCode: 'person_directory',
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
code: 'staffDirectory',
|
|
124
|
-
description: 'Search the shared staff directory',
|
|
125
|
-
pageCode: 'person_directory',
|
|
126
|
-
},
|
|
127
|
-
{ code: 'user', description: 'Access grants', pageCode: 'user' },
|
|
128
|
-
{
|
|
129
|
-
code: 'userGroup:read',
|
|
130
|
-
description: 'Read reusable people groups',
|
|
131
|
-
pageCode: 'people_group',
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
code: 'userGroup:create',
|
|
135
|
-
description: 'Maintain reusable people groups',
|
|
136
|
-
pageCode: 'people_group',
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
code: 'user',
|
|
140
|
-
description: 'Read people for group membership',
|
|
141
|
-
pageCode: 'people_group',
|
|
142
|
-
},
|
|
121
|
+
{ code: 'user', description: 'User management', pageCode: 'user' },
|
|
143
122
|
{ code: 'role', description: 'Role management', pageCode: 'role' },
|
|
144
123
|
{
|
|
145
124
|
code: 'rolePages',
|
package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts
CHANGED
|
@@ -452,12 +452,17 @@ const NODE_DEFINITIONS: BaseNodeTypeSeed[] = [
|
|
|
452
452
|
{
|
|
453
453
|
type: 'db_execute',
|
|
454
454
|
category: 'ACTION',
|
|
455
|
-
label: '
|
|
455
|
+
label: '数据库操作',
|
|
456
456
|
icon: 'ConsoleSqlOutlined',
|
|
457
|
-
description: '
|
|
457
|
+
description: '执行 Raw SQL 或调用当前 GAdmin 数据库中的 Function',
|
|
458
458
|
configSchema: {
|
|
459
459
|
type: 'object',
|
|
460
460
|
properties: {
|
|
461
|
+
mode: {
|
|
462
|
+
type: 'string',
|
|
463
|
+
enum: ['FUNCTION', 'SQL'],
|
|
464
|
+
description: '执行方式',
|
|
465
|
+
},
|
|
461
466
|
functionRef: {
|
|
462
467
|
type: 'object',
|
|
463
468
|
properties: {
|
|
@@ -473,6 +478,19 @@ const NODE_DEFINITIONS: BaseNodeTypeSeed[] = [
|
|
|
473
478
|
items: PARAMETER_BINDING_SCHEMA,
|
|
474
479
|
description: '按 Function 参数顺序提供的值',
|
|
475
480
|
},
|
|
481
|
+
allowWrite: {
|
|
482
|
+
type: 'boolean',
|
|
483
|
+
description: 'Raw SQL 是否允许新增、更新或删除数据',
|
|
484
|
+
},
|
|
485
|
+
sql: {
|
|
486
|
+
type: 'string',
|
|
487
|
+
description: '要执行的单条 SQL',
|
|
488
|
+
},
|
|
489
|
+
parameters: {
|
|
490
|
+
type: 'array',
|
|
491
|
+
items: PARAMETER_BINDING_SCHEMA,
|
|
492
|
+
description: 'Raw SQL 参数占位符对应的值',
|
|
493
|
+
},
|
|
476
494
|
timeoutSeconds: {
|
|
477
495
|
type: 'integer',
|
|
478
496
|
minimum: 1,
|
|
@@ -480,7 +498,6 @@ const NODE_DEFINITIONS: BaseNodeTypeSeed[] = [
|
|
|
480
498
|
description: '执行超时时间(秒)',
|
|
481
499
|
},
|
|
482
500
|
},
|
|
483
|
-
required: ['functionRef', 'argumentBindings'],
|
|
484
501
|
additionalProperties: false,
|
|
485
502
|
},
|
|
486
503
|
inputSchema: { description: '上一个节点的完整结果' },
|
|
@@ -488,6 +505,10 @@ const NODE_DEFINITIONS: BaseNodeTypeSeed[] = [
|
|
|
488
505
|
type: 'object',
|
|
489
506
|
properties: {
|
|
490
507
|
result: { description: 'Function 返回值' },
|
|
508
|
+
rows: { type: 'array' },
|
|
509
|
+
rowCount: { type: 'number' },
|
|
510
|
+
affectedRows: { type: 'number' },
|
|
511
|
+
insertedId: { type: 'string' },
|
|
491
512
|
},
|
|
492
513
|
},
|
|
493
514
|
outputs: [],
|
|
@@ -1316,7 +1337,7 @@ const dedicatedEditorNodes: NodeTypeSeed[] = [
|
|
|
1316
1337
|
type === 'event_trigger'
|
|
1317
1338
|
? 6
|
|
1318
1339
|
: type === 'db_execute'
|
|
1319
|
-
?
|
|
1340
|
+
? 6
|
|
1320
1341
|
: type === 'parallel'
|
|
1321
1342
|
? 6
|
|
1322
1343
|
: ['if_else', 'switch'].includes(type)
|
|
@@ -67,38 +67,22 @@ export async function seedWorkflowModule(prisma: PrismaClient): Promise<void> {
|
|
|
67
67
|
resourceCode: 'workflowNodeInstance',
|
|
68
68
|
resourceDescription: 'Workflow node instance management',
|
|
69
69
|
},
|
|
70
|
-
{
|
|
71
|
-
code: 'workflow_connections',
|
|
72
|
-
zhName: '连接管理',
|
|
73
|
-
enName: 'Connections',
|
|
74
|
-
sortOrder: 3,
|
|
75
|
-
resourceCode: 'integrationConnection',
|
|
76
|
-
resourceDescription: 'Reusable external system connections',
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
code: 'workflow_temporal_tenant',
|
|
80
|
-
zhName: '服务注册',
|
|
81
|
-
enName: 'Service Registration',
|
|
82
|
-
sortOrder: 4,
|
|
83
|
-
resourceCode: 'temporalTenantConfig',
|
|
84
|
-
resourceDescription: 'Temporal tenant config management',
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
code: 'workflow_event_catalog',
|
|
88
|
-
zhName: '业务事件',
|
|
89
|
-
enName: 'Business Events',
|
|
90
|
-
sortOrder: 5,
|
|
91
|
-
resourceCode: 'workflow',
|
|
92
|
-
resourceDescription: 'Read registered Workflow business events',
|
|
93
|
-
},
|
|
94
70
|
{
|
|
95
71
|
code: 'approval_tasks',
|
|
96
72
|
zhName: '审批任务',
|
|
97
73
|
enName: 'Approval Tasks',
|
|
98
|
-
sortOrder:
|
|
74
|
+
sortOrder: 3,
|
|
99
75
|
resourceCode: 'humanApproval',
|
|
100
76
|
resourceDescription: 'Read and decide assigned Human Approval tasks',
|
|
101
77
|
},
|
|
78
|
+
{
|
|
79
|
+
code: 'workflow_settings',
|
|
80
|
+
zhName: '功能设置',
|
|
81
|
+
enName: 'Feature Settings',
|
|
82
|
+
sortOrder: 6,
|
|
83
|
+
resourceCode: 'integrationConnection',
|
|
84
|
+
resourceDescription: 'Reusable external system connections',
|
|
85
|
+
},
|
|
102
86
|
];
|
|
103
87
|
|
|
104
88
|
const allPages = [{ id: workflowPage.id, code: workflowPage.code }];
|
|
@@ -114,7 +98,7 @@ export async function seedWorkflowModule(prisma: PrismaClient): Promise<void> {
|
|
|
114
98
|
sortOrder: def.sortOrder,
|
|
115
99
|
parentId: workflowPage.id,
|
|
116
100
|
},
|
|
117
|
-
def.code === 'approval_tasks'
|
|
101
|
+
def.code === 'approval_tasks' || def.code === 'workflow_settings'
|
|
118
102
|
? { parentId: workflowPage.id, sortOrder: def.sortOrder }
|
|
119
103
|
: {},
|
|
120
104
|
);
|
|
@@ -129,18 +113,6 @@ export async function seedWorkflowModule(prisma: PrismaClient): Promise<void> {
|
|
|
129
113
|
);
|
|
130
114
|
console.log(`[workflow] PageResource: ${def.code} ↔ ${def.resourceCode}`);
|
|
131
115
|
|
|
132
|
-
if (def.code === 'workflow_event_catalog') {
|
|
133
|
-
await bindResourceToPage(
|
|
134
|
-
prisma,
|
|
135
|
-
p.id,
|
|
136
|
-
'workflowEventRegistration',
|
|
137
|
-
'Register database-backed Workflow business events',
|
|
138
|
-
);
|
|
139
|
-
console.log(
|
|
140
|
-
'[workflow] PageResource: workflow_event_catalog ↔ workflowEventRegistration',
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
116
|
if (def.code === 'workflow_list') {
|
|
145
117
|
await bindResourceToPage(
|
|
146
118
|
prisma,
|
|
@@ -151,6 +123,26 @@ export async function seedWorkflowModule(prisma: PrismaClient): Promise<void> {
|
|
|
151
123
|
console.log('[workflow] PageResource: workflow_list ↔ workflowExecution');
|
|
152
124
|
}
|
|
153
125
|
|
|
126
|
+
// 功能设置合并了原服务注册/连接管理/业务事件/人员目录/人员组 5 个页面的全部资源
|
|
127
|
+
if (def.code === 'workflow_settings') {
|
|
128
|
+
const mergedResources = [
|
|
129
|
+
['temporalTenantConfig', 'Temporal tenant config management'],
|
|
130
|
+
['workflow', 'Read registered Workflow business events'],
|
|
131
|
+
[
|
|
132
|
+
'workflowEventRegistration',
|
|
133
|
+
'Register database-backed Workflow business events',
|
|
134
|
+
],
|
|
135
|
+
['user', 'People directory'],
|
|
136
|
+
['staffDirectory', 'Search the shared staff directory'],
|
|
137
|
+
['userGroup:read', 'Read reusable people groups'],
|
|
138
|
+
['userGroup:create', 'Maintain reusable people groups'],
|
|
139
|
+
] as const;
|
|
140
|
+
for (const [code, description] of mergedResources) {
|
|
141
|
+
await bindResourceToPage(prisma, p.id, code, description);
|
|
142
|
+
console.log(`[workflow] PageResource: ${def.code} ↔ ${code}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
154
146
|
// Provider actions live inside the existing Node Instance page. Resolve is
|
|
155
147
|
// seeded now for the later publish/runtime call path; it has no HTTP route yet.
|
|
156
148
|
if (def.code === 'workflow_node_instance') {
|
|
@@ -197,35 +189,53 @@ export async function seedWorkflowModule(prisma: PrismaClient): Promise<void> {
|
|
|
197
189
|
}
|
|
198
190
|
}
|
|
199
191
|
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const connectionsPage = allPages.find(
|
|
208
|
-
(page) => page.code === 'workflow_connections',
|
|
192
|
+
// ── 旧页面合并迁移 ─────────────────────────────────────────────────────────
|
|
193
|
+
// 「功能设置」(workflow_settings) 取代了服务注册/连接管理/业务事件/人员目录/
|
|
194
|
+
// 人员组 5 个旧页面:已授权旧页面的角色改授新页面以保留访问,随后删除旧 page
|
|
195
|
+
// 及其 RolePages/PageResource 关联(RoleResource 保留——资源仍绑定在新页面上)。
|
|
196
|
+
// 旧页面已删除的环境重跑 seed 时本块为 no-op。
|
|
197
|
+
const settingsPage = allPages.find(
|
|
198
|
+
(page) => page.code === 'workflow_settings',
|
|
209
199
|
);
|
|
210
|
-
if (
|
|
211
|
-
const
|
|
212
|
-
where: { pageId: nodeTemplatePage.id },
|
|
213
|
-
select: { roleId: true },
|
|
214
|
-
});
|
|
215
|
-
const connectionResourceIds = await getPageResourceIds(
|
|
200
|
+
if (settingsPage) {
|
|
201
|
+
const settingsResourceIds = await getPageResourceIds(
|
|
216
202
|
prisma,
|
|
217
|
-
|
|
203
|
+
settingsPage.id,
|
|
218
204
|
);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
205
|
+
const legacyPages = await prisma.page.findMany({
|
|
206
|
+
where: {
|
|
207
|
+
code: {
|
|
208
|
+
in: [
|
|
209
|
+
'workflow_temporal_tenant',
|
|
210
|
+
'workflow_connections',
|
|
211
|
+
'workflow_event_catalog',
|
|
212
|
+
'person_directory',
|
|
213
|
+
'people_group',
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
select: { id: true, code: true },
|
|
218
|
+
});
|
|
219
|
+
for (const legacy of legacyPages) {
|
|
220
|
+
const grants = await prisma.rolePages.findMany({
|
|
221
|
+
where: { pageId: legacy.id },
|
|
222
|
+
select: { roleId: true },
|
|
223
|
+
});
|
|
224
|
+
for (const { roleId } of grants) {
|
|
225
|
+
await grantRoleAccess(
|
|
226
|
+
prisma,
|
|
227
|
+
roleId,
|
|
228
|
+
settingsPage.id,
|
|
229
|
+
settingsResourceIds,
|
|
230
|
+
);
|
|
231
|
+
console.log(
|
|
232
|
+
`[workflow] Migrate: role#${roleId} ${legacy.code} → workflow_settings`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
await prisma.rolePages.deleteMany({ where: { pageId: legacy.id } });
|
|
236
|
+
await prisma.pageResource.deleteMany({ where: { pageId: legacy.id } });
|
|
237
|
+
await prisma.page.delete({ where: { id: legacy.id } });
|
|
238
|
+
console.log(`[workflow] Migrate: removed legacy page ${legacy.code}`);
|
|
229
239
|
}
|
|
230
240
|
}
|
|
231
241
|
|
|
@@ -36,12 +36,6 @@ export type CronScheduleStatus =
|
|
|
36
36
|
| 'MISSING'
|
|
37
37
|
| 'UNAVAILABLE';
|
|
38
38
|
|
|
39
|
-
export interface WorkflowRetryCheckpoint {
|
|
40
|
-
startNodeId: string;
|
|
41
|
-
completedNodeIds: string[];
|
|
42
|
-
outputs: Record<string, unknown>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
39
|
@Injectable()
|
|
46
40
|
export class TemporalService implements OnModuleInit, OnModuleDestroy {
|
|
47
41
|
private connection: Connection;
|
|
@@ -82,7 +76,6 @@ export class TemporalService implements OnModuleInit, OnModuleDestroy {
|
|
|
82
76
|
versionId: number;
|
|
83
77
|
dsl: any;
|
|
84
78
|
context: Record<string, any>;
|
|
85
|
-
retryCheckpoint?: WorkflowRetryCheckpoint;
|
|
86
79
|
}): Promise<{ temporalRunId: string }> {
|
|
87
80
|
if (!this.client) {
|
|
88
81
|
throw new Error('Temporal client not connected');
|
|
@@ -167,6 +167,23 @@ describe('Workflow cancellation', () => {
|
|
|
167
167
|
expect(temporal.cancelWorkflow).not.toHaveBeenCalled();
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
+
it('allows cancellation while the Workflow is waiting for a node retry', async () => {
|
|
171
|
+
const { runtime, tx } = setup({ status: 'WAITING_RETRY' });
|
|
172
|
+
|
|
173
|
+
await expect(
|
|
174
|
+
runtime.cancelExecution({
|
|
175
|
+
tx: tx as any,
|
|
176
|
+
instanceId: BigInt(11),
|
|
177
|
+
requestedBy: 'admin-user',
|
|
178
|
+
}),
|
|
179
|
+
).resolves.toEqual({ instanceId: '11', status: 'CANCELLED' });
|
|
180
|
+
expect(tx.workflowInstance.update).toHaveBeenCalledWith(
|
|
181
|
+
expect.objectContaining({
|
|
182
|
+
data: expect.objectContaining({ status: 'CANCELLED' }),
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
170
187
|
it('rejects cancellation after a blocking approval has resolved', async () => {
|
|
171
188
|
const { runtime, temporal, tx } = setup({
|
|
172
189
|
cancellationBlockedAt: new Date('2026-08-20T01:00:00.000Z'),
|
|
@@ -949,7 +949,7 @@ describe('WorkflowCompilerService', () => {
|
|
|
949
949
|
).resolves.toBeUndefined();
|
|
950
950
|
});
|
|
951
951
|
|
|
952
|
-
it('
|
|
952
|
+
it('accepts a safe Raw SQL database configuration', async () => {
|
|
953
953
|
const prisma = {
|
|
954
954
|
workflowNodeType: {
|
|
955
955
|
findMany: jest.fn().mockResolvedValue([
|
|
@@ -982,7 +982,62 @@ describe('WorkflowCompilerService', () => {
|
|
|
982
982
|
type: 'db_execute',
|
|
983
983
|
config: {
|
|
984
984
|
allowWrite: true,
|
|
985
|
-
sql: 'UPDATE users SET active = false',
|
|
985
|
+
sql: 'UPDATE users SET active = false WHERE id = $1',
|
|
986
|
+
parameters: [{ source: 'INPUT', path: 'id' }],
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
],
|
|
990
|
+
edges: [],
|
|
991
|
+
},
|
|
992
|
+
{ userId: 'author' },
|
|
993
|
+
),
|
|
994
|
+
).resolves.toMatchObject({
|
|
995
|
+
nodes: [
|
|
996
|
+
expect.objectContaining({
|
|
997
|
+
id: 'database-1',
|
|
998
|
+
config: expect.objectContaining({
|
|
999
|
+
sql: 'UPDATE users SET active = false WHERE id = $1',
|
|
1000
|
+
}),
|
|
1001
|
+
}),
|
|
1002
|
+
],
|
|
1003
|
+
});
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
it('rejects unsafe Raw SQL database configuration', async () => {
|
|
1007
|
+
const prisma = {
|
|
1008
|
+
workflowNodeType: {
|
|
1009
|
+
findMany: jest.fn().mockResolvedValue([
|
|
1010
|
+
{
|
|
1011
|
+
id: BigInt(10),
|
|
1012
|
+
type: 'db_execute',
|
|
1013
|
+
runtimeKey: 'db_execute',
|
|
1014
|
+
instancePolicy: 'INLINE_ONLY',
|
|
1015
|
+
schemaVersion: 6,
|
|
1016
|
+
configSchema: { type: 'object' },
|
|
1017
|
+
uiSchema: {},
|
|
1018
|
+
},
|
|
1019
|
+
]),
|
|
1020
|
+
},
|
|
1021
|
+
workflowNodeInstance: { findMany: jest.fn().mockResolvedValue([]) },
|
|
1022
|
+
};
|
|
1023
|
+
const service = new WorkflowCompilerService(
|
|
1024
|
+
prisma as any,
|
|
1025
|
+
{ validateOrThrow: jest.fn() } as any,
|
|
1026
|
+
{ assertUiSchemaSupported: jest.fn() } as any,
|
|
1027
|
+
{ validateSubjects: jest.fn() } as any,
|
|
1028
|
+
);
|
|
1029
|
+
|
|
1030
|
+
await expect(
|
|
1031
|
+
service.compile(
|
|
1032
|
+
{
|
|
1033
|
+
nodes: [
|
|
1034
|
+
{
|
|
1035
|
+
id: 'database-1',
|
|
1036
|
+
type: 'db_execute',
|
|
1037
|
+
config: {
|
|
1038
|
+
mode: 'SQL',
|
|
1039
|
+
allowWrite: true,
|
|
1040
|
+
sql: 'DROP TABLE users',
|
|
986
1041
|
},
|
|
987
1042
|
},
|
|
988
1043
|
],
|
|
@@ -992,7 +1047,7 @@ describe('WorkflowCompilerService', () => {
|
|
|
992
1047
|
),
|
|
993
1048
|
).rejects.toMatchObject({
|
|
994
1049
|
response: {
|
|
995
|
-
message: '
|
|
1050
|
+
message: '数据库操作只允许单条查询、新增、更新或删除语句',
|
|
996
1051
|
nodeId: 'database-1',
|
|
997
1052
|
},
|
|
998
1053
|
});
|
|
@@ -240,12 +240,27 @@ export class WorkflowCompilerService {
|
|
|
240
240
|
}
|
|
241
241
|
if (nodeType.runtimeKey === 'db_execute') {
|
|
242
242
|
const databaseConfig = config as Record<string, unknown>;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
);
|
|
243
|
+
const databaseMode = databaseExecutionMode(databaseConfig);
|
|
244
|
+
if (!databaseMode)
|
|
245
|
+
this.fail('NODE_TYPE_MISMATCH', node, '数据库节点的执行方式无效');
|
|
246
|
+
if (databaseMode === 'SQL') {
|
|
247
|
+
const allowWrite = databaseConfig.allowWrite === true;
|
|
248
|
+
const sql = String(databaseConfig.sql ?? '');
|
|
249
|
+
if (!isDatabaseSqlAllowed(sql, allowWrite))
|
|
250
|
+
this.fail(
|
|
251
|
+
'NODE_TYPE_MISMATCH',
|
|
252
|
+
node,
|
|
253
|
+
allowWrite
|
|
254
|
+
? '数据库操作只允许单条查询、新增、更新或删除语句'
|
|
255
|
+
: '当前数据库操作节点只允许单条只读查询',
|
|
256
|
+
);
|
|
257
|
+
compiledNodes.push({
|
|
258
|
+
...clone(node),
|
|
259
|
+
config,
|
|
260
|
+
runtimeKey: nodeType.runtimeKey || nodeType.type,
|
|
261
|
+
});
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
249
264
|
const functionRef = asObject(databaseConfig.functionRef);
|
|
250
265
|
const argumentBindings = Array.isArray(databaseConfig.argumentBindings)
|
|
251
266
|
? databaseConfig.argumentBindings
|
|
@@ -503,6 +518,51 @@ function clone<T>(value: T): T {
|
|
|
503
518
|
return JSON.parse(JSON.stringify(value)) as T;
|
|
504
519
|
}
|
|
505
520
|
|
|
521
|
+
function databaseExecutionMode(
|
|
522
|
+
config: Record<string, unknown>,
|
|
523
|
+
): 'FUNCTION' | 'SQL' | null {
|
|
524
|
+
if (config.mode === 'FUNCTION' || config.mode === 'SQL') return config.mode;
|
|
525
|
+
if (config.mode !== undefined) return null;
|
|
526
|
+
return 'sql' in config || 'allowWrite' in config || 'parameters' in config
|
|
527
|
+
? 'SQL'
|
|
528
|
+
: 'FUNCTION';
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function isSingleReadOnlySql(sql: string): boolean {
|
|
532
|
+
const normalized = normalizeSqlForPolicy(sql);
|
|
533
|
+
if (!/^(select|with)\b/i.test(normalized)) return false;
|
|
534
|
+
return !/\b(insert|update|delete|merge|call|copy|create|alter|drop|truncate|grant|revoke|vacuum|refresh)\b/i.test(
|
|
535
|
+
normalized,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function isDatabaseSqlAllowed(sql: string, allowWrite: boolean): boolean {
|
|
540
|
+
const normalized = normalizeSqlForPolicy(sql);
|
|
541
|
+
const writableCte =
|
|
542
|
+
/^with\b/i.test(normalized) &&
|
|
543
|
+
/\b(insert|update|delete)\b/i.test(normalized) &&
|
|
544
|
+
!/\b(merge|call|copy|create|alter|drop|truncate|grant|revoke|vacuum|refresh)\b/i.test(
|
|
545
|
+
normalized,
|
|
546
|
+
);
|
|
547
|
+
return (
|
|
548
|
+
isSingleReadOnlySql(sql) ||
|
|
549
|
+
(allowWrite &&
|
|
550
|
+
(/^(insert|update|delete)\b/i.test(normalized) || writableCte))
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function normalizeSqlForPolicy(sql: string): string {
|
|
555
|
+
const withoutContent = sql
|
|
556
|
+
.replace(/\$(\w*)\$[\s\S]*?\$\1\$/g, ' ')
|
|
557
|
+
.replace(/'(?:''|[^'])*'/g, ' ')
|
|
558
|
+
.replace(/"(?:""|[^"])*"/g, ' ')
|
|
559
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
560
|
+
.replace(/--[^\r\n]*/g, ' ')
|
|
561
|
+
.trim();
|
|
562
|
+
const withoutTrailingSemicolon = withoutContent.replace(/;\s*$/, '').trim();
|
|
563
|
+
return withoutTrailingSemicolon.includes(';') ? '' : withoutTrailingSemicolon;
|
|
564
|
+
}
|
|
565
|
+
|
|
506
566
|
function asObject(value: unknown): Record<string, unknown> {
|
|
507
567
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
508
568
|
? (value as Record<string, unknown>)
|