@gadmin2n/schematics 0.0.147 → 0.0.149

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 (106) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +2 -1
  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 +83 -53
  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/agenda/agenda.controller.ts +4 -0
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.service.ts +31 -4
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.spec.ts +2 -6
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.spec.ts +28 -29
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.ts +16 -34
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.dto.ts +3 -3
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.spec.ts +61 -6
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.spec.ts +2 -2
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/index.ts +2 -0
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.controller.ts +11 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.spec.ts +126 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.ts +136 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.controller.ts +24 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.dto.ts +8 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.spec.ts +62 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.ts +94 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.types.ts +6 -0
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staffDirectory.module.ts +10 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user.provider.ts +6 -2
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +20 -12
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +21 -19
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +11 -0
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +97 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts +150 -2
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.controller.ts +13 -1
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.dto.ts +13 -1
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.errors.ts +1 -0
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.spec.ts +125 -0
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.ts +118 -5
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +20 -3
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +6 -9
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-catalog.spec.ts +166 -17
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-trigger.pg.spec.ts +82 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-runtime.facade.ts +2 -0
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +4 -1
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +5 -1
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.dto.ts +7 -2
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +44 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +106 -37
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +59 -3
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.spec.ts +33 -22
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.ts +10 -23
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.test.tsx +69 -0
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.tsx +145 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/index.ts +2 -0
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/staff-directory.api.ts +16 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/index.ts +1 -0
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +10 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +20 -2
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +6 -0
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +60 -14
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +60 -14
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/index.tsx +41 -1
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/PeopleGroupModal.tsx +181 -0
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +203 -0
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/people-groups.api.ts +145 -0
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/PersonModal.tsx +112 -0
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +214 -0
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/person-directory.api.ts +79 -0
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/form-modal.tsx +7 -11
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +7 -7
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/RegisterBusinessEventModal.test.tsx +46 -5
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/RegisterBusinessEventModal.tsx +14 -10
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.test.tsx +1 -1
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.tsx +2 -1
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.test.tsx +1 -1
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.tsx +2 -2
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeOutputSummary.test.tsx +20 -0
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +97 -28
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +87 -119
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.test.tsx +22 -1
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.tsx +8 -2
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connections.css +8 -46
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connectionsApi.ts +7 -0
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.test.tsx +32 -3
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +163 -106
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +8 -1
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +26 -0
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +34 -6
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-config.css +13 -16
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +185 -0
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +224 -105
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CodeConfigEditor.test.tsx +1 -1
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +21 -12
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.test.tsx +140 -0
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +124 -62
  93. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.test.tsx +160 -8
  94. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.tsx +125 -55
  95. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +78 -17
  96. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.test.tsx +33 -1
  97. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.tsx +28 -46
  98. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/event-catalog.api.ts +4 -2
  99. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/human-approval-config.css +16 -0
  100. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/subject-provider.api.ts +0 -6
  101. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.test.tsx +24 -0
  102. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +14 -17
  103. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
  104. package/package.json +1 -1
  105. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CreateNodeInstanceDrawer.tsx +0 -162
  106. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/UserGroupCreateModal.tsx +0 -72
@@ -4,6 +4,7 @@ export type UserGroupErrorCode =
4
4
  | 'USER_GROUP_CYCLE'
5
5
  | 'USER_GROUP_DEPTH_EXCEEDED'
6
6
  | 'USER_GROUP_MEMBER_INVALID'
7
+ | 'USER_GROUP_IN_USE'
7
8
  | 'USER_GROUP_REVISION_CONFLICT'
8
9
  | 'USER_GROUP_NOT_FOUND';
9
10
 
@@ -1,3 +1,4 @@
1
+ import { Prisma } from '@prisma/client';
1
2
  import { UserGroupService } from './user-group.service';
2
3
 
3
4
  const group = {
@@ -20,6 +21,7 @@ function createHarness() {
20
21
  findMany: jest.fn(),
21
22
  create: jest.fn(),
22
23
  updateMany: jest.fn().mockResolvedValue({ count: 1 }),
24
+ delete: jest.fn().mockResolvedValue(group),
23
25
  },
24
26
  user: { findMany: jest.fn() },
25
27
  userGroupUser: {
@@ -32,6 +34,8 @@ function createHarness() {
32
34
  deleteMany: jest.fn().mockResolvedValue({ count: 0 }),
33
35
  createMany: jest.fn().mockResolvedValue({ count: 1 }),
34
36
  },
37
+ workflow: { findMany: jest.fn().mockResolvedValue([]) },
38
+ approvalRequestSubject: { count: jest.fn().mockResolvedValue(0) },
35
39
  };
36
40
  const prisma = {
37
41
  ...tx,
@@ -44,6 +48,19 @@ function createHarness() {
44
48
  }
45
49
 
46
50
  describe('UserGroupService', () => {
51
+ it('can include inactive groups for the management page', async () => {
52
+ const { service, tx } = createHarness();
53
+ tx.userGroup.findMany.mockResolvedValue([]);
54
+
55
+ await service.search('finance', true);
56
+
57
+ expect(tx.userGroup.findMany).toHaveBeenCalledWith(
58
+ expect.objectContaining({
59
+ where: expect.not.objectContaining({ isActive: true }),
60
+ }),
61
+ );
62
+ });
63
+
47
64
  it('returns direct users and child groups for editing', async () => {
48
65
  const { service, tx } = createHarness();
49
66
  tx.userGroup.findUnique
@@ -148,6 +165,31 @@ describe('UserGroupService', () => {
148
165
  });
149
166
  });
150
167
 
168
+ it('only accepts active non-API users as direct members', async () => {
169
+ const { service, tx } = createHarness();
170
+ tx.user.findMany.mockResolvedValue([{ userid: 'alice' }]);
171
+ tx.userGroup.findMany.mockResolvedValueOnce([]).mockResolvedValueOnce([]);
172
+
173
+ await service.replaceMembers(
174
+ 'finance',
175
+ {
176
+ expectedRevision: 4,
177
+ userIds: ['alice'],
178
+ childGroupCodes: [],
179
+ },
180
+ actor,
181
+ );
182
+
183
+ expect(tx.user.findMany).toHaveBeenCalledWith({
184
+ where: {
185
+ userid: { in: ['alice'] },
186
+ isActive: true,
187
+ isApiKey: false,
188
+ },
189
+ select: { userid: true },
190
+ });
191
+ });
192
+
151
193
  it('returns 409 when expectedRevision is stale', async () => {
152
194
  const { service } = createHarness();
153
195
  await expect(
@@ -158,4 +200,87 @@ describe('UserGroupService', () => {
158
200
  ),
159
201
  ).rejects.toMatchObject({ status: 409 });
160
202
  });
203
+
204
+ it('deletes an unreferenced group and its owned memberships', async () => {
205
+ const { service, tx } = createHarness();
206
+ tx.userGroupChild.findMany.mockResolvedValue([]);
207
+
208
+ await expect(service.deleteGroup('finance', 4)).resolves.toEqual({
209
+ count: 1,
210
+ });
211
+ expect(tx.userGroupUser.deleteMany).toHaveBeenCalledWith({
212
+ where: { groupId: group.id },
213
+ });
214
+ expect(tx.userGroupChild.deleteMany).toHaveBeenCalledWith({
215
+ where: { parentGroupId: group.id },
216
+ });
217
+ expect(tx.userGroup.delete).toHaveBeenCalledWith({
218
+ where: { id: group.id },
219
+ });
220
+ });
221
+
222
+ it('rejects deleting a group referenced by another group', async () => {
223
+ const { service, tx } = createHarness();
224
+ tx.userGroupChild.findMany.mockResolvedValue([
225
+ { parentGroup: { code: 'all-finance', name: 'All Finance' } },
226
+ ]);
227
+
228
+ await expect(service.deleteGroup('finance', 4)).rejects.toMatchObject({
229
+ status: 409,
230
+ response: {
231
+ code: 'USER_GROUP_IN_USE',
232
+ message: expect.stringContaining('All Finance'),
233
+ },
234
+ });
235
+ expect(tx.userGroup.delete).not.toHaveBeenCalled();
236
+ });
237
+
238
+ it('rejects deleting a group referenced by a Workflow', async () => {
239
+ const { service, tx } = createHarness();
240
+ tx.userGroupChild.findMany.mockResolvedValue([]);
241
+ tx.workflow.findMany.mockResolvedValue([
242
+ {
243
+ id: BigInt(9),
244
+ name: 'Finance Approval',
245
+ dsl: {
246
+ nodes: [
247
+ {
248
+ config: {
249
+ subjects: [{ provider: 'USER_GROUP', refs: ['finance'] }],
250
+ },
251
+ },
252
+ ],
253
+ },
254
+ versions: [],
255
+ },
256
+ ]);
257
+
258
+ await expect(service.deleteGroup('finance', 4)).rejects.toMatchObject({
259
+ status: 409,
260
+ response: {
261
+ code: 'USER_GROUP_IN_USE',
262
+ message: expect.stringContaining('Finance Approval'),
263
+ },
264
+ });
265
+ expect(tx.userGroup.delete).not.toHaveBeenCalled();
266
+ });
267
+
268
+ it('returns 409 when a concurrent reference makes the database reject deletion', async () => {
269
+ const { service, tx } = createHarness();
270
+ tx.userGroupChild.findMany.mockResolvedValue([]);
271
+ tx.userGroup.delete.mockRejectedValue(
272
+ new Prisma.PrismaClientKnownRequestError('foreign key conflict', {
273
+ code: 'P2003',
274
+ clientVersion: '6.19.3',
275
+ }),
276
+ );
277
+
278
+ await expect(service.deleteGroup('finance', 4)).rejects.toMatchObject({
279
+ status: 409,
280
+ response: {
281
+ code: 'USER_GROUP_IN_USE',
282
+ message: expect.stringContaining('刷新后重试'),
283
+ },
284
+ });
285
+ });
161
286
  });
@@ -138,11 +138,102 @@ export class UserGroupService {
138
138
  });
139
139
  }
140
140
 
141
- search(query: string) {
141
+ async deleteGroup(code: string, expectedRevision: number) {
142
+ try {
143
+ return await this.prisma.$transaction(async (tx) => {
144
+ const group = await this.findGroup(tx, code);
145
+ this.assertRevision(group.revision, expectedRevision);
146
+
147
+ const [parents, workflows, activeApprovalCount] = await Promise.all([
148
+ tx.userGroupChild.findMany({
149
+ where: { childGroupId: group.id },
150
+ select: {
151
+ parentGroup: { select: { code: true, name: true } },
152
+ },
153
+ }),
154
+ tx.workflow.findMany({
155
+ select: {
156
+ id: true,
157
+ name: true,
158
+ dsl: true,
159
+ versions: { select: { dsl: true } },
160
+ },
161
+ }),
162
+ tx.approvalRequestSubject.count({
163
+ where: {
164
+ provider: 'USER_GROUP',
165
+ ref: group.code,
166
+ request: { status: { in: ['PENDING', 'BLOCKED'] } },
167
+ },
168
+ }),
169
+ ]);
170
+ const parentGroups = parents.map(({ parentGroup }) => parentGroup);
171
+ const referencedWorkflows = workflows
172
+ .filter(
173
+ (workflow) =>
174
+ workflowDslReferencesGroup(workflow.dsl, group.code) ||
175
+ workflow.versions.some((version) =>
176
+ workflowDslReferencesGroup(version.dsl, group.code),
177
+ ),
178
+ )
179
+ .map((workflow) => ({
180
+ id: String(workflow.id),
181
+ name: workflow.name,
182
+ }));
183
+ const reasons = [
184
+ ...(parentGroups.length
185
+ ? [
186
+ `被人员组引用:${parentGroups
187
+ .map((parent) => parent.name)
188
+ .join('、')}`,
189
+ ]
190
+ : []),
191
+ ...(referencedWorkflows.length
192
+ ? [
193
+ `被 Workflow 引用:${referencedWorkflows
194
+ .map((workflow) => workflow.name)
195
+ .join('、')}`,
196
+ ]
197
+ : []),
198
+ ...(activeApprovalCount
199
+ ? [`仍有 ${activeApprovalCount} 个进行中的审批引用`]
200
+ : []),
201
+ ];
202
+ if (reasons.length) {
203
+ throw new UserGroupDomainError(
204
+ 'USER_GROUP_IN_USE',
205
+ `无法删除该人员组:${reasons.join(';')}`,
206
+ 409,
207
+ );
208
+ }
209
+
210
+ await Promise.all([
211
+ tx.userGroupUser.deleteMany({ where: { groupId: group.id } }),
212
+ tx.userGroupChild.deleteMany({ where: { parentGroupId: group.id } }),
213
+ ]);
214
+ await tx.userGroup.delete({ where: { id: group.id } });
215
+ return { count: 1 };
216
+ });
217
+ } catch (error) {
218
+ if (
219
+ error instanceof Prisma.PrismaClientKnownRequestError &&
220
+ error.code === 'P2003'
221
+ ) {
222
+ throw new UserGroupDomainError(
223
+ 'USER_GROUP_IN_USE',
224
+ '该人员组刚被其他数据引用,请刷新后重试',
225
+ 409,
226
+ );
227
+ }
228
+ throw error;
229
+ }
230
+ }
231
+
232
+ search(query: string, includeInactive = false) {
142
233
  const q = query.trim();
143
234
  return this.prisma.userGroup.findMany({
144
235
  where: {
145
- isActive: true,
236
+ ...(!includeInactive && { isActive: true }),
146
237
  ...(q && {
147
238
  OR: [
148
239
  { code: { contains: q, mode: 'insensitive' } },
@@ -156,6 +247,8 @@ export class UserGroupService {
156
247
  name: true,
157
248
  description: true,
158
249
  revision: true,
250
+ isActive: true,
251
+ updatedAt: true,
159
252
  },
160
253
  take: 50,
161
254
  });
@@ -220,7 +313,7 @@ export class UserGroupService {
220
313
  where: { id: { in: ids }, isActive: true },
221
314
  select: {
222
315
  users: {
223
- where: { user: { isActive: true } },
316
+ where: { user: { isActive: true, isApiKey: false } },
224
317
  select: {
225
318
  user: { select: { userid: true, username: true } },
226
319
  },
@@ -267,13 +360,13 @@ export class UserGroupService {
267
360
  private async requireActiveUsers(tx: Transaction, userIds: string[]) {
268
361
  if (userIds.length === 0) return [];
269
362
  const users = await tx.user.findMany({
270
- where: { userid: { in: userIds }, isActive: true },
363
+ where: { userid: { in: userIds }, isActive: true, isApiKey: false },
271
364
  select: { userid: true },
272
365
  });
273
366
  if (users.length !== userIds.length) {
274
367
  throw new UserGroupDomainError(
275
368
  'USER_GROUP_MEMBER_INVALID',
276
- 'Every direct user must exist and be active',
369
+ 'Every direct user must exist, be active, and not be an API key user',
277
370
  );
278
371
  }
279
372
  return users;
@@ -388,3 +481,23 @@ export class UserGroupService {
388
481
  );
389
482
  }
390
483
  }
484
+
485
+ function workflowDslReferencesGroup(
486
+ value: Prisma.JsonValue | null,
487
+ groupCode: string,
488
+ ): boolean {
489
+ if (Array.isArray(value))
490
+ return value.some((item) => workflowDslReferencesGroup(item, groupCode));
491
+ if (!value || typeof value !== 'object') return false;
492
+
493
+ const record = value as Record<string, Prisma.JsonValue>;
494
+ const refs = Array.isArray(record.refs) ? record.refs.map(String) : [];
495
+ if (
496
+ refs.includes(`group:${groupCode}`) ||
497
+ (record.provider === 'USER_GROUP' && refs.includes(groupCode))
498
+ )
499
+ return true;
500
+ return Object.values(record).some((item) =>
501
+ workflowDslReferencesGroup(item, groupCode),
502
+ );
503
+ }
@@ -69,6 +69,7 @@ describe('Workflow cancellation', () => {
69
69
  runtime.cancelExecution({
70
70
  tx: tx as any,
71
71
  instanceId: BigInt(11),
72
+ requestedBy: 'business-user',
72
73
  reason: 'business cancellation',
73
74
  }),
74
75
  ).resolves.toEqual({ instanceId: '11', status: 'CANCELLED' });
@@ -79,6 +80,8 @@ describe('Workflow cancellation', () => {
79
80
  data: expect.objectContaining({
80
81
  status: 'CANCELLED',
81
82
  cancellationStatus: 'PENDING',
83
+ cancellationRequestedAt: expect.any(Date),
84
+ cancellationRequestedBy: 'business-user',
82
85
  cancellationReason: 'business cancellation',
83
86
  }),
84
87
  });
@@ -112,6 +115,7 @@ describe('Workflow cancellation', () => {
112
115
  await runtime.cancelExecution({
113
116
  tx: tx as any,
114
117
  instanceId: execution!.instanceId,
118
+ requestedBy: 'applicant-user',
115
119
  reason: 'applicant cancellation',
116
120
  });
117
121
 
@@ -150,7 +154,11 @@ describe('Workflow cancellation', () => {
150
154
  const { runtime, temporal, tx } = setup({ status: 'COMPLETED' });
151
155
 
152
156
  await expect(
153
- runtime.cancelExecution({ tx: tx as any, instanceId: BigInt(11) }),
157
+ runtime.cancelExecution({
158
+ tx: tx as any,
159
+ instanceId: BigInt(11),
160
+ requestedBy: 'admin-user',
161
+ }),
154
162
  ).rejects.toBeInstanceOf(ConflictException);
155
163
  expect(tx.workflowInstance.update).not.toHaveBeenCalled();
156
164
  expect(tx.approvalRequest.updateMany).not.toHaveBeenCalled();
@@ -164,7 +172,11 @@ describe('Workflow cancellation', () => {
164
172
  });
165
173
 
166
174
  await expect(
167
- runtime.cancelExecution({ tx: tx as any, instanceId: BigInt(11) }),
175
+ runtime.cancelExecution({
176
+ tx: tx as any,
177
+ instanceId: BigInt(11),
178
+ requestedBy: 'admin-user',
179
+ }),
168
180
  ).rejects.toMatchObject({
169
181
  response: {
170
182
  code: 'WORKFLOW_CANCELLATION_BLOCKED',
@@ -180,7 +192,11 @@ describe('Workflow cancellation', () => {
180
192
  });
181
193
 
182
194
  await expect(
183
- runtime.cancelExecution({ tx: tx as any, instanceId: BigInt(11) }),
195
+ runtime.cancelExecution({
196
+ tx: tx as any,
197
+ instanceId: BigInt(11),
198
+ requestedBy: 'second-user',
199
+ }),
184
200
  ).resolves.toEqual({ instanceId: '11', status: 'CANCELLED' });
185
201
  expect(tx.workflowInstance.update).not.toHaveBeenCalled();
186
202
  expect(temporal.cancelWorkflow).not.toHaveBeenCalled();
@@ -193,6 +209,7 @@ describe('Workflow cancellation', () => {
193
209
  runtime.cancelExecution({
194
210
  tx: transactionClient,
195
211
  instanceId: BigInt(11),
212
+ requestedBy: `${reason} user`,
196
213
  reason,
197
214
  }),
198
215
  );
@@ -279,7 +279,7 @@ describe('WorkflowCompilerService', () => {
279
279
  type: 'approval',
280
280
  label: '部门负责人审批',
281
281
  config: {
282
- subjects: [{ source: 'STATIC', provider: 'USER', refs: [' '] }],
282
+ subjects: [{ source: 'STATIC', refs: [' '] }],
283
283
  },
284
284
  },
285
285
  ],
@@ -426,7 +426,7 @@ describe('WorkflowCompilerService', () => {
426
426
  code: 'FINANCE_APPROVAL',
427
427
  nodeTypeId: BigInt(7),
428
428
  isEnabled: true,
429
- config: { subjects: [{ provider: 'USER', refs: ['alice'] }] },
429
+ config: { subjects: [{ source: 'STATIC', refs: ['alice'] }] },
430
430
  };
431
431
  const prisma = {
432
432
  workflowNodeType: {
@@ -483,7 +483,7 @@ describe('WorkflowCompilerService', () => {
483
483
  });
484
484
  instance.config.subjects[0].refs[0] = 'bob';
485
485
  expect(compiled.nodes[0].config).toEqual({
486
- subjects: [{ provider: 'USER', refs: ['alice'] }],
486
+ subjects: [{ source: 'STATIC', refs: ['alice'] }],
487
487
  });
488
488
  });
489
489
 
@@ -529,9 +529,7 @@ describe('WorkflowCompilerService', () => {
529
529
  config: instance.config,
530
530
  configOverride: {
531
531
  timeoutSeconds: 3600,
532
- subjects: [
533
- { source: 'STATIC', provider: 'USER', refs: ['alice'] },
534
- ],
532
+ subjects: [{ source: 'STATIC', refs: ['alice'] }],
535
533
  },
536
534
  instanceRef: {
537
535
  instanceId: '12',
@@ -546,7 +544,7 @@ describe('WorkflowCompilerService', () => {
546
544
 
547
545
  const expectedOverride = {
548
546
  timeoutSeconds: 3600,
549
- subjects: [{ source: 'STATIC', provider: 'USER', refs: ['alice'] }],
547
+ subjects: [{ source: 'STATIC', refs: ['alice'] }],
550
548
  };
551
549
  expect(compiled.nodes[0].config).toEqual(expectedOverride);
552
550
  expect(compiled.nodes[0].source?.configHash).toBe(
@@ -571,7 +569,7 @@ describe('WorkflowCompilerService', () => {
571
569
  },
572
570
  details: [],
573
571
  },
574
- subjects: [{ source: 'STATIC', provider: 'USER', refs: ['alice'] }],
572
+ subjects: [{ source: 'STATIC', refs: ['alice'] }],
575
573
  approvalRule: { mode: 'ANY' },
576
574
  rejectionRule: { mode: 'ANY' },
577
575
  timeout: { durationSeconds: 0, action: 'REJECTED' },
@@ -655,7 +653,6 @@ describe('WorkflowCompilerService', () => {
655
653
  subjects: [
656
654
  {
657
655
  source: 'DYNAMIC',
658
- provider: 'USER',
659
656
  inputKey: 'direct_manager',
660
657
  label: '查询直属领导',
661
658
  lookup: { request: { connectionRef: 'erp' } },