@gadmin2n/schematics 0.0.147 → 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 (92) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +1 -0
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +67 -15
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +17 -4
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +5 -0
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.spec.ts +24 -0
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.ts +15 -0
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.spec.ts +2 -6
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.spec.ts +28 -29
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.ts +16 -34
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.dto.ts +3 -3
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.spec.ts +61 -6
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.spec.ts +2 -2
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/index.ts +2 -0
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.controller.ts +11 -0
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.spec.ts +126 -0
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.ts +136 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.controller.ts +24 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.dto.ts +8 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.spec.ts +62 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.ts +94 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.types.ts +6 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staffDirectory.module.ts +10 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user.provider.ts +6 -2
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +20 -12
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +21 -19
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +11 -0
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +97 -0
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts +150 -2
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.controller.ts +13 -1
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.dto.ts +13 -1
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.errors.ts +1 -0
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.spec.ts +125 -0
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.ts +118 -5
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +20 -3
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +6 -9
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-runtime.facade.ts +2 -0
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +4 -1
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +5 -1
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +44 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +15 -1
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +2 -3
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.spec.ts +33 -22
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.ts +10 -23
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.test.tsx +69 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.tsx +145 -0
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/index.ts +2 -0
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/staff-directory.api.ts +16 -0
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/index.ts +1 -0
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +10 -0
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +20 -2
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +6 -0
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +49 -11
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +49 -11
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/PeopleGroupModal.tsx +181 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +203 -0
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/people-groups.api.ts +145 -0
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/PersonModal.tsx +112 -0
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +214 -0
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/person-directory.api.ts +79 -0
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/form-modal.tsx +7 -11
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +7 -7
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeOutputSummary.test.tsx +20 -0
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +97 -28
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +87 -119
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.test.tsx +22 -1
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.tsx +8 -2
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connections.css +8 -46
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connectionsApi.ts +7 -0
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.test.tsx +32 -3
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +163 -106
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +8 -1
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +26 -0
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +34 -6
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-config.css +13 -16
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +185 -0
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +224 -105
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CodeConfigEditor.test.tsx +1 -1
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +21 -12
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +1 -2
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.test.tsx +160 -8
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.tsx +125 -55
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +47 -3
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.test.tsx +33 -1
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.tsx +28 -46
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/human-approval-config.css +16 -0
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/subject-provider.api.ts +0 -6
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.test.tsx +24 -0
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +14 -17
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
  90. package/package.json +1 -1
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CreateNodeInstanceDrawer.tsx +0 -162
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/UserGroupCreateModal.tsx +0 -72
@@ -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. 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' },
@@ -5,6 +5,7 @@ import { ExternalInteractionModule } from './externalInteraction/externalInterac
5
5
  import { HttpRequestModule } from './httpRequest/httpRequest.module';
6
6
  import { HumanApprovalModule } from './humanApproval/humanApproval.module';
7
7
  import { IntegrationConnectionModule } from './integrationConnection/integrationConnection.module';
8
+ import { StaffDirectoryModule } from './staffDirectory/staffDirectory.module';
8
9
  import { SubjectProviderModule } from './subjectProvider/subjectProvider.module';
9
10
  import { UserGroupModule } from './userGroup/userGroup.module';
10
11
  import { WorkflowModule } from './workflow/workflow.module';
@@ -17,6 +18,7 @@ export default [
17
18
  HumanApprovalModule,
18
19
  HttpRequestModule,
19
20
  IntegrationConnectionModule,
21
+ StaffDirectoryModule,
20
22
  WorkflowModule,
21
23
  SubjectProviderModule,
22
24
  UserGroupModule,
@@ -2,6 +2,7 @@ import { ACGuard } from '@gadmin2n/nest-common';
2
2
  import {
3
3
  Body,
4
4
  Controller,
5
+ Delete,
5
6
  Get,
6
7
  Header,
7
8
  Param,
@@ -87,4 +88,14 @@ export class IntegrationConnectionController {
87
88
  ) {
88
89
  return this.service.update(code, input, actorContextFromRequest(request));
89
90
  }
91
+
92
+ @Delete(':code')
93
+ @UseRoles({
94
+ resource: 'integrationConnection',
95
+ action: 'delete',
96
+ possession: 'any',
97
+ })
98
+ remove(@Param('code') code: string) {
99
+ return this.service.remove(code);
100
+ }
90
101
  }