@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.
- package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.local.md +8 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.md +70 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/gitignore +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +67 -15
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +17 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +5 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.spec.ts +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.ts +15 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.spec.ts +2 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.spec.ts +28 -29
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.ts +16 -34
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.dto.ts +3 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.spec.ts +61 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.spec.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/index.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.controller.ts +11 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.spec.ts +126 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.ts +136 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.controller.ts +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.dto.ts +8 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.spec.ts +62 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.ts +94 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.types.ts +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staffDirectory.module.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user.provider.ts +6 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +20 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +21 -19
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +11 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +97 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts +150 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.controller.ts +13 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.dto.ts +13 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.errors.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.spec.ts +125 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.ts +118 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +20 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +6 -9
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-runtime.facade.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +4 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +5 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +44 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +15 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +2 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.spec.ts +33 -22
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.ts +10 -23
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.test.tsx +69 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.tsx +145 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/index.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/staff-directory.api.ts +16 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/index.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +20 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +49 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +49 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/PeopleGroupModal.tsx +181 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +203 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/people-groups.api.ts +145 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/PersonModal.tsx +112 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +214 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/person-directory.api.ts +79 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/form-modal.tsx +7 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +7 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeOutputSummary.test.tsx +20 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +97 -28
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +87 -119
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.test.tsx +22 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.tsx +8 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connections.css +8 -46
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connectionsApi.ts +7 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.test.tsx +32 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +163 -106
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +8 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +26 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +34 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-config.css +13 -16
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +185 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +224 -105
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CodeConfigEditor.test.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +21 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +1 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.test.tsx +160 -8
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.tsx +125 -55
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +47 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.test.tsx +33 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.tsx +28 -46
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/human-approval-config.css +16 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/subject-provider.api.ts +0 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.test.tsx +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +14 -17
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CreateNodeInstanceDrawer.tsx +0 -162
- 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)
|
|
@@ -12,8 +12,9 @@ import {
|
|
|
12
12
|
*
|
|
13
13
|
* 职责(仅本模块自家):
|
|
14
14
|
* 1. permission 父分组 page (顶级 admin/administration → permission)
|
|
15
|
-
* 2.
|
|
16
|
-
*
|
|
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
|
-
{
|
|
58
|
-
|
|
59
|
-
|
|
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:
|
|
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(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
{
|
|
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',
|
package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts
CHANGED
|
@@ -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: {
|
|
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'
|
|
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:
|
|
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 [
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
17
|
-
inputKey: 'review_groups',
|
|
15
|
+
inputKey: 'backup_approvers',
|
|
18
16
|
},
|
|
19
17
|
],
|
|
20
18
|
{
|
|
21
|
-
request_owner:
|
|
22
|
-
|
|
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('
|
|
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: '
|
|
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
|
|
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
|
|
26
|
+
const provider =
|
|
27
|
+
source.source === 'DYNAMIC' ? 'USER' : source.provider ?? 'USER';
|
|
28
|
+
const refs = normalizeRefs(
|
|
28
29
|
source.source === 'DYNAMIC'
|
|
29
|
-
? normalizeDynamicRefs(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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].
|
|
38
|
+
return [...refsByProvider].flatMap(([provider, refs]) =>
|
|
39
|
+
refs.size ? [{ provider, refs: [...refs] }] : [],
|
|
40
|
+
);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
function normalizeDynamicRefs(
|
|
43
|
-
value:
|
|
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
|
-
@
|
|
24
|
-
@
|
|
25
|
-
provider
|
|
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',
|
|
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',
|
|
116
|
+
subjects: [{ source: 'STATIC', refs: ['reviewer'] }],
|
|
117
117
|
approvalRule: { mode: 'ANY' },
|
|
118
118
|
rejectionRule: { mode: 'ANY' },
|
|
119
119
|
timeout: { durationSeconds: 0, action: 'REJECTED' },
|