@gadmin2n/schematics 0.0.146 → 0.0.148

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/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.local.md +8 -0
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.md +70 -0
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +1 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/gitignore +1 -0
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +67 -15
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +17 -4
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +5 -0
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.spec.ts +24 -0
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.ts +15 -0
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.spec.ts +2 -6
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.spec.ts +28 -29
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.ts +16 -34
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.dto.ts +3 -3
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.spec.ts +61 -6
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.spec.ts +2 -2
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/index.ts +2 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.controller.ts +11 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.spec.ts +126 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.ts +136 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.controller.ts +24 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.dto.ts +8 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.spec.ts +62 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.ts +94 -0
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.types.ts +6 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staffDirectory.module.ts +10 -0
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user.provider.ts +6 -2
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +20 -12
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +21 -19
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +11 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +97 -0
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts +150 -2
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.controller.ts +13 -1
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.dto.ts +13 -1
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.errors.ts +1 -0
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.spec.ts +125 -0
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.ts +118 -5
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +20 -3
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +6 -9
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-runtime.facade.ts +2 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +4 -1
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +5 -1
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +44 -0
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +15 -1
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +2 -3
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.spec.ts +33 -22
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.ts +10 -23
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.test.tsx +69 -0
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.tsx +145 -0
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/index.ts +2 -0
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/staff-directory.api.ts +16 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/index.ts +1 -0
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +10 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +20 -2
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +6 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +49 -11
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +49 -11
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/PeopleGroupModal.tsx +181 -0
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +203 -0
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/people-groups.api.ts +145 -0
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/PersonModal.tsx +112 -0
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +214 -0
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/person-directory.api.ts +79 -0
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/form-modal.tsx +7 -11
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +7 -7
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeOutputSummary.test.tsx +20 -0
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +97 -28
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +87 -119
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.test.tsx +22 -1
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.tsx +8 -2
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connections.css +8 -46
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connectionsApi.ts +7 -0
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.test.tsx +32 -3
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +163 -106
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +8 -1
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +26 -0
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +34 -6
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-config.css +13 -16
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +185 -0
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +224 -105
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CodeConfigEditor.test.tsx +1 -1
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +21 -12
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +1 -2
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.test.tsx +160 -8
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.tsx +125 -55
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +47 -3
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.test.tsx +33 -1
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.tsx +28 -46
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/human-approval-config.css +16 -0
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/subject-provider.api.ts +0 -6
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.test.tsx +24 -0
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +14 -17
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
  93. package/package.json +1 -1
  94. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CreateNodeInstanceDrawer.tsx +0 -162
  95. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/UserGroupCreateModal.tsx +0 -72
@@ -0,0 +1,8 @@
1
+ ## 与 Gadmin2 模板的关系
2
+
3
+ 本项目(gadmin-test)是 gadmin2 模板的**实例项目**,用于日常迭代新功能。迭代完成后需将新特性合并回 gadmin2 模板。
4
+
5
+ | | 路径 |
6
+ |------|------|
7
+ | 实例(本项目) | `/Users/kavenma/Documents/work/code/oit/test/gadmin-test` |
8
+ | 模板(上游) | `/Users/kavenma/Documents/work/code/common/gadmin2/schematics/src/lib/application/files/gadmin2-game-angle-demo` |
@@ -0,0 +1,70 @@
1
+ ## 项目简介
2
+ - 本项目由 lowcode 生成工具 Gadmin2 生成:Admin page(CRUD) + Dashboard,包括前端和后端。
3
+ - 前端的 Admin page(CRUD) 代码由配置(`config/ui/{ModelName}.ts`)驱动生成,后端的数据模型和 CRUD 代码由 prisma schema(`config/prisma/{module}.prisma`)驱动生成。
4
+
5
+ ## 常用命令
6
+
7
+ ```bash
8
+ # 改了 config/prisma/*.prisma —— 迁移数据库 + 生成前后端代码
9
+ gadmin2 g prisma:dev
10
+
11
+ # 只改了 config/ui/*.ts —— 只生成代码,不碰数据库
12
+ gadmin2 g prisma
13
+
14
+ cd server && yarn start:dev # 后端,端口 8000(优先读 server/.env.local)
15
+ cd web && yarn dev # 前端,端口 3000(注意web/ 用 yarn,不用 npm)
16
+ cd server && yarn seed # 执行 seed(页面/资源/权限声明)
17
+ ```
18
+
19
+ > gadmin2 命令需要在项目根目录执行
20
+
21
+ ## Architecture Overview
22
+
23
+ ### Frontend Tech Stack
24
+ React 18, TypeScript, Vite, Ant Design v5, Refinedev, ECharts, i18next
25
+
26
+ ```
27
+ web/src/
28
+ ├── routes/ #页面模块:user/、role/ 等
29
+ ├── components/ # 共享 UI 组件(PageHeader、RowActions、SearchBar 等)
30
+ ├── hooks/ # 自定义 hooks(useECharts、useDynamicResources、useListPageState 等)
31
+ ├── providers/ # auth / data / access control provider
32
+ ├── config/ # routeRegistry.tsx(路由注册)、http.ts
33
+ ├── constants/# layout.ts(页面布局共享常量)
34
+ ├── enums/ # 共享枚举
35
+ └── helpers/ # 工具函数(http.ts的 customRequest 等)
36
+ ```
37
+
38
+ ### Backend Tech Stack
39
+ NestJS v10, Prisma v6, TypeScript, nest-access-control, Winston logger
40
+
41
+ ```
42
+ server/src/
43
+ ├── modules/ # 业务模块,每个含 Module → Controller → Service
44
+ ├── generated/ # Prisma 自动生成(禁止直接修改)
45
+ ├── lib/ # 核心:auth.guard.ts、roles.guard.ts、taihu.ts、woaAuth.ts、logger.ts
46
+ ├── enums/ # 共享枚举
47
+ └── utils/ # 工具函数
48
+ ```
49
+
50
+ ### Permission Model (RBAC)
51
+ 参考 `.agent/references/permission-model.md`
52
+
53
+ ### 数据库迁移、dev/prod 模式差异、认证绕过等本地开发
54
+ 细节见 `.agent/references/local-dev.md`。
55
+
56
+ ### playwright 无头浏览器访问项目页面
57
+ 用 `server/scripts/gen-jwt.ts` 生成临时 token。
58
+
59
+ ## 开发原则(必须遵守)
60
+ - 使用 TypeScript 严格模式
61
+ - 严格遵循前、后端既有技术栈,如无必要不要引入新库
62
+ - 不确定时询问,不要猜;存在歧义时不要默默选择;有更简单的方案要说出来
63
+ - 修改 config/ui/ 或 config/prisma/ 配置后,必须在**项目根目录**执行生成命令 `gadmin2 g prisma` 或者 `gadmin2 g prisma:dev`。
64
+ - 修改 config/prisma/ 后必须同步检查 server/seed/:raw SQL 引用的列/表、seed 数据的必填字段与字段名要与新 schema 一致,改完需要提醒开发者 yarn seed 验证。
65
+ - **优先使用 skill 解决问题** skill 的决策逻辑(配置优先、DB 先于页面等)见 `.agent/references/decision-map.md`
66
+ - **默认业务代码范围:** 前端修改限定在 `web/src/routes/`,后端修改限定在 `server/src/modules/`、`server/seed/`。
67
+ - **配置驱动源码:** `config/prisma/`、`config/ui/` 可按生成流程修改;标准 CRUD 优先改配置,不要绕过配置直接改生成物。
68
+ - **跨切面窄白名单:** `web/src/config/routeRegistry.tsx` 仅可用于注册自定义路由。
69
+ - skill 中列出的目标文件不会自动扩大修改权限。除上述范围外,修改 `web/src/components/`、`web/src/hooks/`、`web/src/constants/`、`web/src/providers/`、全局认证文件,以及 `server/src/lib/`、`server/src/app.module.ts`、`server/src/main.ts` 等跨切面文件,必须由用户明确要求或确认具体实施计划,并限定为最小必要改动。
70
+ - **生成目录禁止直接修改:** `web/src/generated/`、`server/src/generated/` 只能由生成命令更新。
@@ -149,6 +149,7 @@ model WorkflowInstance {
149
149
  retryFromNodeId String? @db.VarChar(64) @map("retry_from_node_id") // 本实例从哪个失败节点继续执行
150
150
  cancellationStatus WorkflowCancellationStatus @default(NONE) @map("cancellation_status")
151
151
  cancellationRequestedAt DateTime? @map("cancellation_requested_at") @db.Timestamptz(3)
152
+ cancellationRequestedBy String? @map("cancellation_requested_by") @db.VarChar(128)
152
153
  cancellationReason String? @map("cancellation_reason") @db.VarChar(512)
153
154
  cancellationLastError String? @map("cancellation_last_error") @db.Text
154
155
  cancellationBlockedAt DateTime? @map("cancellation_blocked_at") @db.Timestamptz(3)
@@ -59,3 +59,4 @@ docs/
59
59
  agent-eval/runs/
60
60
  agent-eval/baseline/
61
61
  tmp/
62
+ CODEBUDDY.local.md
@@ -12,8 +12,9 @@ import {
12
12
  *
13
13
  * 职责(仅本模块自家):
14
14
  * 1. permission 父分组 page (顶级 admin/administration → permission)
15
- * 2. 5 个 leaf page: permission_readme / user / role / page / resource
16
- * 3. 7 个 resource: user / role / rolePages / roleResource / page / pageResource / resource
15
+ * 2. 7 个 leaf page: permission_readme / person_directory / user /
16
+ * people_group / role / page / resource
17
+ * 3. permission 模块所需 resource 及页面绑定
17
18
  * 4. PageResource 绑定(page ↔ resource,多对多)
18
19
  * 5. SYSTEM_ADMIN 对上述全部 page + resource 的全量授权
19
20
  *
@@ -54,14 +55,31 @@ export async function seedPermissionModule(
54
55
  enName: 'Menu Guide',
55
56
  sortOrder: 1005,
56
57
  },
57
- { code: 'user', zhName: '用户管理', enName: 'User Mgnt', sortOrder: 1010 },
58
- { code: 'role', zhName: '角色管理', enName: 'Role Mgnt', sortOrder: 1020 },
59
- { code: 'page', zhName: '页面管理', enName: 'Page Mgnt', sortOrder: 1030 },
58
+ {
59
+ code: 'person_directory',
60
+ zhName: '人员目录',
61
+ enName: 'People Directory',
62
+ sortOrder: 1010,
63
+ },
64
+ {
65
+ code: 'user',
66
+ zhName: '访问授权',
67
+ enName: 'Access Grants',
68
+ sortOrder: 1020,
69
+ },
70
+ {
71
+ code: 'people_group',
72
+ zhName: '人员组',
73
+ enName: 'People Groups',
74
+ sortOrder: 1030,
75
+ },
76
+ { code: 'role', zhName: '角色管理', enName: 'Role Mgnt', sortOrder: 1040 },
77
+ { code: 'page', zhName: '页面管理', enName: 'Page Mgnt', sortOrder: 1050 },
60
78
  {
61
79
  code: 'resource',
62
80
  zhName: '资源管理',
63
81
  enName: 'Resource Management',
64
- sortOrder: 1040,
82
+ sortOrder: 1060,
65
83
  },
66
84
  ];
67
85
 
@@ -69,14 +87,23 @@ export async function seedPermissionModule(
69
87
  pagesByCode.set(permissionPage.code, permissionPage.id);
70
88
 
71
89
  for (const def of leafDefs) {
72
- const p = await upsertPage(prisma, {
73
- code: def.code,
74
- name: def.code,
75
- zhName: def.zhName,
76
- enName: def.enName,
77
- sortOrder: def.sortOrder,
78
- parentId: permissionPage.id,
79
- });
90
+ const p = await upsertPage(
91
+ prisma,
92
+ {
93
+ code: def.code,
94
+ name: def.code,
95
+ zhName: def.zhName,
96
+ enName: def.enName,
97
+ sortOrder: def.sortOrder,
98
+ parentId: permissionPage.id,
99
+ },
100
+ {
101
+ zhName: def.zhName,
102
+ enName: def.enName,
103
+ sortOrder: def.sortOrder,
104
+ parentId: permissionPage.id,
105
+ },
106
+ );
80
107
  pagesByCode.set(p.code, p.id);
81
108
  console.log(`[permission] Page: ${p.code} (id=${p.id})`);
82
109
  }
@@ -87,7 +114,32 @@ export async function seedPermissionModule(
87
114
  description: string;
88
115
  pageCode: string;
89
116
  }[] = [
90
- { code: 'user', description: 'User management', pageCode: 'user' },
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
+ },
91
143
  { code: 'role', description: 'Role management', pageCode: 'role' },
92
144
  {
93
145
  code: 'rolePages',
@@ -980,7 +980,11 @@ const HUMAN_APPROVAL_CONFIG_SCHEMA: NodeTypeSeed['configSchema'] = {
980
980
  type: 'object',
981
981
  properties: {
982
982
  source: { type: 'string', enum: ['STATIC', 'DYNAMIC'] },
983
- provider: { type: 'string', enum: ['USER', 'USER_GROUP'] },
983
+ provider: {
984
+ type: 'string',
985
+ enum: ['USER', 'USER_GROUP'],
986
+ default: 'USER',
987
+ },
984
988
  refs: {
985
989
  type: 'array',
986
990
  minItems: 1,
@@ -998,7 +1002,7 @@ const HUMAN_APPROVAL_CONFIG_SCHEMA: NodeTypeSeed['configSchema'] = {
998
1002
  additionalProperties: false,
999
1003
  },
1000
1004
  },
1001
- required: ['source', 'provider'],
1005
+ required: ['source'],
1002
1006
  allOf: [
1003
1007
  {
1004
1008
  if: { properties: { source: { const: 'STATIC' } } },
@@ -1093,7 +1097,7 @@ const humanApprovalNode: NodeTypeSeed = {
1093
1097
  configSchema: HUMAN_APPROVAL_CONFIG_SCHEMA,
1094
1098
  outputs: ['true', 'false'],
1095
1099
  runtimeKey: 'human_approval',
1096
- schemaVersion: 20,
1100
+ schemaVersion: 22,
1097
1101
  instancePolicy: 'OPTIONAL',
1098
1102
  uiSchema: HUMAN_APPROVAL_UI_SCHEMA,
1099
1103
  };
@@ -1361,7 +1365,16 @@ export const NODE_TYPES: NodeTypeSeed[] = [
1361
1365
 
1362
1366
  export async function seedNodeTypes(prisma: PrismaClient) {
1363
1367
  console.log('Seeding workflow node types...');
1364
- for (const removedType of ['return_result', 'db_query']) {
1368
+ for (const removedType of [
1369
+ 'return_result',
1370
+ 'db_query',
1371
+ 'approval_request_trigger',
1372
+ 'send_notification',
1373
+ 'set_variable',
1374
+ 'submit_approval_result',
1375
+ 'submit_external_result',
1376
+ 'error_handler',
1377
+ ]) {
1365
1378
  const removedNodeType = await prisma.workflowNodeType.findUnique({
1366
1379
  where: { type: removedType },
1367
1380
  select: { id: true },
@@ -60,6 +60,11 @@ const config: Record<string, any> = {
60
60
  integration: {
61
61
  secretKey: process.env.INTEGRATION_SECRET_KEY ?? '',
62
62
  },
63
+ staffDirectory: {
64
+ baseUrl: process.env.STAFF_DIRECTORY_BASE_URL ?? '',
65
+ apiKey: process.env.STAFF_DIRECTORY_API_KEY ?? '',
66
+ searchPath: process.env.STAFF_DIRECTORY_SEARCH_PATH ?? '/api/staff/search',
67
+ },
63
68
  };
64
69
 
65
70
  export default () => config;
@@ -1,4 +1,5 @@
1
1
  import { ArgumentsHost, BadRequestException } from '@nestjs/common';
2
+ import { Prisma } from '@prisma/client';
2
3
  import { StructuredAllExceptionsFilter } from './structured-all-exceptions.filter';
3
4
 
4
5
  function httpHost(url = '/api/workflowNodeInstance') {
@@ -51,4 +52,27 @@ describe('StructuredAllExceptionsFilter', () => {
51
52
  path: '/api/workflowNodeInstance',
52
53
  });
53
54
  });
55
+
56
+ it('returns a safe 409 response for an unhandled Prisma unique conflict', () => {
57
+ const filter = new StructuredAllExceptionsFilter();
58
+ const structured = httpHost('/api/user/createOne');
59
+
60
+ filter.catch(
61
+ new Prisma.PrismaClientKnownRequestError('sensitive database details', {
62
+ code: 'P2002',
63
+ clientVersion: '6.19.3',
64
+ meta: { target: ['userid'] },
65
+ }),
66
+ structured.host,
67
+ );
68
+
69
+ expect(structured.response.status).toHaveBeenCalledWith(409);
70
+ expect(structured.response.json).toHaveBeenCalledWith({
71
+ statusCode: 409,
72
+ code: 'RESOURCE_ALREADY_EXISTS',
73
+ message: '记录已存在,请勿重复提交',
74
+ error: 'Conflict',
75
+ path: '/api/user/createOne',
76
+ });
77
+ });
54
78
  });
@@ -4,6 +4,7 @@ import {
4
4
  ExceptionFilter,
5
5
  HttpStatus,
6
6
  } from '@nestjs/common';
7
+ import { Prisma } from '@prisma/client';
7
8
 
8
9
  interface ControlledException {
9
10
  getStatus?: () => number;
@@ -24,6 +25,20 @@ export class StructuredAllExceptionsFilter implements ExceptionFilter {
24
25
  const error = controlled?.name ?? 'Error';
25
26
  const path = request.url ?? '';
26
27
 
28
+ if (
29
+ exception instanceof Prisma.PrismaClientKnownRequestError &&
30
+ exception.code === 'P2002'
31
+ ) {
32
+ response.status(HttpStatus.CONFLICT).json({
33
+ statusCode: HttpStatus.CONFLICT,
34
+ code: 'RESOURCE_ALREADY_EXISTS',
35
+ message: '记录已存在,请勿重复提交',
36
+ error: 'Conflict',
37
+ path,
38
+ });
39
+ return;
40
+ }
41
+
27
42
  if (isRecord(payload)) {
28
43
  response.status(statusCode).json({
29
44
  ...payload,
@@ -28,12 +28,11 @@ describe('CodeRuntimeController', () => {
28
28
  subjects: [
29
29
  {
30
30
  source: 'DYNAMIC',
31
- provider: 'USER',
32
31
  inputKey: 'direct_manager',
33
32
  lookup: {
34
33
  result: {
35
34
  script:
36
- "return { subjects: [{ provider: 'USER', refs: [response.data.leader] }], requester: input.user_id };",
35
+ 'return [response.data.leader, input.user_id];',
37
36
  },
38
37
  },
39
38
  },
@@ -89,10 +88,7 @@ describe('CodeRuntimeController', () => {
89
88
  workflow: {},
90
89
  }),
91
90
  ).resolves.toEqual({
92
- result: {
93
- subjects: [{ provider: 'USER', refs: ['leader-a'] }],
94
- requester: 'applicant-a',
95
- },
91
+ result: ['leader-a', 'applicant-a'],
96
92
  });
97
93
  });
98
94
  });
@@ -5,36 +5,22 @@ describe('materializeApprovalSubjects', () => {
5
5
  expect(
6
6
  materializeApprovalSubjects(
7
7
  [
8
- { source: 'STATIC', provider: 'USER', refs: ['alice'] },
8
+ { source: 'STATIC', refs: ['alice', 'bob'] },
9
9
  {
10
10
  source: 'DYNAMIC',
11
- provider: 'USER',
12
11
  inputKey: 'request_owner',
13
12
  },
14
13
  {
15
14
  source: 'DYNAMIC',
16
- provider: 'USER_GROUP',
17
- inputKey: 'review_groups',
15
+ inputKey: 'backup_approvers',
18
16
  },
19
17
  ],
20
18
  {
21
- request_owner: {
22
- subjects: [{ provider: 'USER', refs: ['bob'] }],
23
- },
24
- review_groups: {
25
- subjects: [
26
- {
27
- provider: 'USER_GROUP',
28
- refs: ['finance', 'risk', 'finance'],
29
- },
30
- ],
31
- },
19
+ request_owner: ['bob', 'carol'],
20
+ backup_approvers: ['alice', 'dave', 'carol'],
32
21
  },
33
22
  ),
34
- ).toEqual([
35
- { provider: 'USER', refs: ['alice', 'bob'] },
36
- { provider: 'USER_GROUP', refs: ['finance', 'risk'] },
37
- ]);
23
+ ).toEqual([{ provider: 'USER', refs: ['alice', 'bob', 'carol', 'dave'] }]);
38
24
  });
39
25
 
40
26
  it('rejects a missing dynamic approver input', () => {
@@ -43,7 +29,6 @@ describe('materializeApprovalSubjects', () => {
43
29
  [
44
30
  {
45
31
  source: 'DYNAMIC',
46
- provider: 'USER',
47
32
  inputKey: 'request_owner',
48
33
  },
49
34
  ],
@@ -58,32 +43,46 @@ describe('materializeApprovalSubjects', () => {
58
43
  [
59
44
  {
60
45
  source: 'DYNAMIC',
61
- provider: 'USER',
62
46
  inputKey: 'direct_manager',
63
47
  },
64
48
  ],
65
- {
66
- direct_manager: {
67
- subjects: [{ provider: 'USER', refs: ['leader-a'] }],
68
- },
69
- },
49
+ { direct_manager: ['leader-a'] },
70
50
  ),
71
51
  ).toEqual([{ provider: 'USER', refs: ['leader-a'] }]);
72
52
  });
73
53
 
74
- it('rejects a lookup result that does not use the configured provider', () => {
54
+ it('keeps fixed user groups for runtime expansion while deduplicating users', () => {
55
+ expect(
56
+ materializeApprovalSubjects(
57
+ [
58
+ { source: 'STATIC', provider: 'USER', refs: ['alice'] },
59
+ {
60
+ source: 'STATIC',
61
+ provider: 'USER_GROUP',
62
+ refs: ['finance'],
63
+ },
64
+ { source: 'DYNAMIC', inputKey: 'backup_approvers' },
65
+ ],
66
+ { backup_approvers: ['alice', 'bob', 'bob'] },
67
+ ),
68
+ ).toEqual([
69
+ { provider: 'USER', refs: ['alice', 'bob'] },
70
+ { provider: 'USER_GROUP', refs: ['finance'] },
71
+ ]);
72
+ });
73
+
74
+ it('rejects a lookup result that is not a user ID array', () => {
75
75
  expect(() =>
76
76
  materializeApprovalSubjects(
77
77
  [
78
78
  {
79
79
  source: 'DYNAMIC',
80
- provider: 'USER',
81
80
  inputKey: 'direct_manager',
82
81
  },
83
82
  ],
84
83
  {
85
84
  direct_manager: {
86
- subjects: [{ provider: 'USER_GROUP', refs: ['leaders'] }],
85
+ subjects: [{ provider: 'USER', refs: ['leader-a'] }],
87
86
  },
88
87
  },
89
88
  ),
@@ -3,12 +3,11 @@ export type ApprovalSubjectProvider = 'USER' | 'USER_GROUP';
3
3
  export type ApprovalSubjectSource =
4
4
  | {
5
5
  source?: 'STATIC';
6
- provider: ApprovalSubjectProvider;
6
+ provider?: ApprovalSubjectProvider;
7
7
  refs: string[];
8
8
  }
9
9
  | {
10
10
  source: 'DYNAMIC';
11
- provider: ApprovalSubjectProvider;
12
11
  inputKey: string;
13
12
  };
14
13
 
@@ -21,50 +20,33 @@ export function materializeApprovalSubjects(
21
20
  sources: ApprovalSubjectSource[],
22
21
  dynamicInputs: Record<string, unknown> = {},
23
22
  ): MaterializedApprovalSubject[] {
24
- const refsByProvider = new Map<ApprovalSubjectProvider, string[]>();
23
+ const refsByProvider = new Map<ApprovalSubjectProvider, Set<string>>();
25
24
 
26
25
  for (const source of sources) {
27
- const refs =
26
+ const provider =
27
+ source.source === 'DYNAMIC' ? 'USER' : source.provider ?? 'USER';
28
+ const refs = normalizeRefs(
28
29
  source.source === 'DYNAMIC'
29
- ? normalizeDynamicRefs(
30
- dynamicInputs[source.inputKey],
31
- source.inputKey,
32
- source.provider,
33
- )
34
- : normalizeRefs(source.refs);
35
- const current = refsByProvider.get(source.provider) ?? [];
36
- refsByProvider.set(source.provider, [...new Set([...current, ...refs])]);
30
+ ? normalizeDynamicRefs(dynamicInputs[source.inputKey], source.inputKey)
31
+ : source.refs,
32
+ );
33
+ const combined = refsByProvider.get(provider) ?? new Set<string>();
34
+ refs.forEach((ref) => combined.add(ref));
35
+ refsByProvider.set(provider, combined);
37
36
  }
38
37
 
39
- return [...refsByProvider].map(([provider, refs]) => ({ provider, refs }));
38
+ return [...refsByProvider].flatMap(([provider, refs]) =>
39
+ refs.size ? [{ provider, refs: [...refs] }] : [],
40
+ );
40
41
  }
41
42
 
42
- function normalizeDynamicRefs(
43
- value: unknown,
44
- inputKey: string,
45
- provider: ApprovalSubjectProvider,
46
- ): string[] {
47
- const result = asObject(value);
48
- const subjects = Array.isArray(result.subjects) ? result.subjects : [];
49
- const refs = normalizeRefs(
50
- subjects.flatMap((item) => {
51
- const subject = asObject(item);
52
- return subject.provider === provider && Array.isArray(subject.refs)
53
- ? subject.refs
54
- : [];
55
- }),
56
- );
43
+ function normalizeDynamicRefs(value: unknown, inputKey: string): string[] {
44
+ const refs = Array.isArray(value) ? normalizeRefs(value) : [];
57
45
  if (refs.length === 0)
58
46
  throw new Error(`Dynamic approver input "${inputKey}" is missing or empty`);
59
47
  return refs;
60
48
  }
61
49
 
62
- function asObject(value: unknown): Record<string, unknown> {
63
- return value && typeof value === 'object' && !Array.isArray(value)
64
- ? (value as Record<string, unknown>)
65
- : {};
66
- }
67
-
68
50
  function normalizeRefs(values: unknown[]): string[] {
69
51
  return [
70
52
  ...new Set(
@@ -20,9 +20,9 @@ export class ApprovalSubjectConfigDto {
20
20
  @IsIn(['STATIC', 'DYNAMIC'])
21
21
  source: 'STATIC' | 'DYNAMIC';
22
22
 
23
- @IsString()
24
- @IsNotEmpty()
25
- provider: string;
23
+ @IsOptional()
24
+ @IsIn(['USER', 'USER_GROUP'])
25
+ provider?: 'USER' | 'USER_GROUP';
26
26
 
27
27
  @ValidateIf((input) => input.source === 'STATIC')
28
28
  @IsArray()
@@ -2,6 +2,65 @@ import { hashNodeInstanceConfig } from '../workflowNodeInstance/node-instance-co
2
2
  import { HumanApprovalService } from './human-approval.service';
3
3
 
4
4
  describe('HumanApprovalService', () => {
5
+ it('deduplicates people resolved from fixed users and fixed user groups', async () => {
6
+ const userProvider = {
7
+ preview: jest.fn().mockResolvedValue({
8
+ items: [{ ref: 'alice', label: 'Alice' }],
9
+ }),
10
+ resolve: jest.fn().mockResolvedValue([
11
+ {
12
+ principalType: 'USER',
13
+ principalRef: 'alice',
14
+ displayName: 'Alice',
15
+ sourceRefs: ['alice'],
16
+ },
17
+ ]),
18
+ };
19
+ const groupProvider = {
20
+ preview: jest.fn().mockResolvedValue({
21
+ items: [{ ref: 'finance', label: 'Finance' }],
22
+ }),
23
+ resolve: jest.fn().mockResolvedValue([
24
+ {
25
+ principalType: 'USER',
26
+ principalRef: 'alice',
27
+ displayName: 'Alice',
28
+ sourceRefs: ['finance'],
29
+ },
30
+ {
31
+ principalType: 'USER',
32
+ principalRef: 'bob',
33
+ displayName: 'Bob',
34
+ sourceRefs: ['finance'],
35
+ },
36
+ ]),
37
+ };
38
+ const service = new HumanApprovalService(
39
+ {} as any,
40
+ {
41
+ get: jest.fn((key) =>
42
+ key === 'USER_GROUP' ? groupProvider : userProvider,
43
+ ),
44
+ } as any,
45
+ );
46
+
47
+ const result = await (service as any).resolveSubjects(
48
+ [
49
+ { provider: 'USER', refs: ['alice'] },
50
+ { provider: 'USER_GROUP', refs: ['finance'] },
51
+ ],
52
+ { userId: 'runtime' },
53
+ );
54
+
55
+ expect(result.candidates).toEqual([
56
+ expect.objectContaining({
57
+ principalRef: 'alice',
58
+ sourceRefs: ['alice', 'finance'],
59
+ }),
60
+ expect.objectContaining({ principalRef: 'bob' }),
61
+ ]);
62
+ });
63
+
5
64
  it('stores the resolved task in the built-in approval request', async () => {
6
65
  const create = jest.fn().mockResolvedValue({
7
66
  id: BigInt(40),
@@ -56,9 +115,7 @@ describe('HumanApprovalService', () => {
56
115
  },
57
116
  details: [],
58
117
  },
59
- subjects: [
60
- { source: 'STATIC' as const, provider: 'USER', refs: ['alice'] },
61
- ],
118
+ subjects: [{ source: 'STATIC' as const, refs: ['alice'] }],
62
119
  approvalRule: { mode: 'ANY' as const },
63
120
  rejectionRule: { mode: 'ANY' as const },
64
121
  timeout: { durationSeconds: 0, action: 'REJECTED' as const },
@@ -227,9 +284,7 @@ describe('HumanApprovalService', () => {
227
284
  callbackResult: { script: 'return callback.body;' },
228
285
  },
229
286
  task: {},
230
- subjects: [
231
- { source: 'STATIC' as const, provider: 'USER', refs: ['alice'] },
232
- ],
287
+ subjects: [{ source: 'STATIC' as const, refs: ['alice'] }],
233
288
  approvalRule: { mode: 'ANY' as const },
234
289
  rejectionRule: { mode: 'ANY' as const },
235
290
  timeout: { durationSeconds: 60, action: 'REJECTED' as const },
@@ -67,7 +67,7 @@ describe('HumanApprovalRuntimeController', () => {
67
67
  },
68
68
  details: [],
69
69
  },
70
- subjects: [{ source: 'STATIC', provider: 'USER', refs: ['reviewer'] }],
70
+ subjects: [{ source: 'STATIC', refs: ['reviewer'] }],
71
71
  approvalRule: { mode: 'ANY' },
72
72
  rejectionRule: { mode: 'ANY' },
73
73
  timeout: { durationSeconds: 0, action: 'REJECTED' },
@@ -113,7 +113,7 @@ describe('HumanApprovalRuntimeController', () => {
113
113
  it('reuses a completed approval from the original retry operation', async () => {
114
114
  const config = {
115
115
  task: { title: { source: 'FIXED', value: '采购审批' }, details: [] },
116
- subjects: [{ source: 'STATIC', provider: 'USER', refs: ['reviewer'] }],
116
+ subjects: [{ source: 'STATIC', refs: ['reviewer'] }],
117
117
  approvalRule: { mode: 'ANY' },
118
118
  rejectionRule: { mode: 'ANY' },
119
119
  timeout: { durationSeconds: 0, action: 'REJECTED' },