@gadmin2n/schematics 0.0.146 → 0.0.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.local.md +8 -0
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.md +70 -0
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +1 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/gitignore +1 -0
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +67 -15
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +17 -4
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +5 -0
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.spec.ts +24 -0
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.ts +15 -0
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.spec.ts +2 -6
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.spec.ts +28 -29
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.ts +16 -34
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.dto.ts +3 -3
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.spec.ts +61 -6
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.spec.ts +2 -2
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/index.ts +2 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.controller.ts +11 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.spec.ts +126 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.ts +136 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.controller.ts +24 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.dto.ts +8 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.spec.ts +62 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.ts +94 -0
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.types.ts +6 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staffDirectory.module.ts +10 -0
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user.provider.ts +6 -2
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +20 -12
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +21 -19
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +11 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +97 -0
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts +150 -2
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.controller.ts +13 -1
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.dto.ts +13 -1
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.errors.ts +1 -0
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.spec.ts +125 -0
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.ts +118 -5
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +20 -3
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +6 -9
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-runtime.facade.ts +2 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +4 -1
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +5 -1
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +44 -0
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +15 -1
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +2 -3
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.spec.ts +33 -22
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.ts +10 -23
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.test.tsx +69 -0
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.tsx +145 -0
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/index.ts +2 -0
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/staff-directory.api.ts +16 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/index.ts +1 -0
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +10 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +20 -2
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +6 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +49 -11
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +49 -11
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/PeopleGroupModal.tsx +181 -0
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +203 -0
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/people-groups.api.ts +145 -0
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/PersonModal.tsx +112 -0
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +214 -0
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/person-directory.api.ts +79 -0
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/form-modal.tsx +7 -11
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +7 -7
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeOutputSummary.test.tsx +20 -0
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +97 -28
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +87 -119
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.test.tsx +22 -1
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.tsx +8 -2
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connections.css +8 -46
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connectionsApi.ts +7 -0
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.test.tsx +32 -3
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +163 -106
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +8 -1
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +26 -0
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +34 -6
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-config.css +13 -16
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +185 -0
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +224 -105
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CodeConfigEditor.test.tsx +1 -1
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +21 -12
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +1 -2
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.test.tsx +160 -8
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.tsx +125 -55
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +47 -3
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.test.tsx +33 -1
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.tsx +28 -46
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/human-approval-config.css +16 -0
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/subject-provider.api.ts +0 -6
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.test.tsx +24 -0
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +14 -17
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
  93. package/package.json +1 -1
  94. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CreateNodeInstanceDrawer.tsx +0 -162
  95. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/UserGroupCreateModal.tsx +0 -72
@@ -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' } },
@@ -22,6 +22,7 @@ export interface WorkflowCancellationSummary {
22
22
  export interface CancelWorkflowExecutionInput {
23
23
  tx: Prisma.TransactionClient;
24
24
  instanceId: bigint;
25
+ requestedBy: string;
25
26
  reason?: string;
26
27
  }
27
28
 
@@ -60,6 +61,7 @@ export class WorkflowRuntimeFacade {
60
61
  await this.workflows.cancelExecutionInTransaction(
61
62
  input.tx,
62
63
  input.instanceId,
64
+ input.requestedBy,
63
65
  input.reason,
64
66
  );
65
67
  return { instanceId: input.instanceId.toString(), status: 'CANCELLED' };
@@ -81,7 +81,9 @@ describe('WorkflowController runtime contracts', () => {
81
81
  prisma as never,
82
82
  );
83
83
 
84
- await expect(controller.cancelInstance(42)).resolves.toEqual({
84
+ await expect(
85
+ controller.cancelInstance(42, { user: { userid: 'admin-user' } }),
86
+ ).resolves.toEqual({
85
87
  instanceId: '42',
86
88
  status: 'CANCELLED',
87
89
  });
@@ -89,6 +91,7 @@ describe('WorkflowController runtime contracts', () => {
89
91
  expect(runtime.cancelExecution).toHaveBeenCalledWith({
90
92
  tx,
91
93
  instanceId: BigInt(42),
94
+ requestedBy: 'admin-user',
92
95
  });
93
96
  });
94
97
  });
@@ -272,11 +272,15 @@ export class WorkflowController {
272
272
  }
273
273
 
274
274
  @Post('instance/:instanceId/cancel')
275
- async cancelInstance(@Param('instanceId', ParseIntPipe) instanceId: number) {
275
+ async cancelInstance(
276
+ @Param('instanceId', ParseIntPipe) instanceId: number,
277
+ @Req() req,
278
+ ) {
276
279
  return this.prisma.$transaction((tx) =>
277
280
  this.workflowRuntime.cancelExecution({
278
281
  tx,
279
282
  instanceId: BigInt(instanceId),
283
+ requestedBy: req.user.userid,
280
284
  }),
281
285
  );
282
286
  }
@@ -7,6 +7,50 @@ import { signWebhookPayload } from './webhook-signature.util';
7
7
  import { WorkflowCompilerService } from './workflow-compiler.service';
8
8
  import { WorkflowService } from './workflow.service';
9
9
 
10
+ describe('WorkflowService — workflow list run counts', () => {
11
+ it('includes cancelled instances in the run summary', async () => {
12
+ const prisma = {
13
+ workflow: {
14
+ findMany: jest.fn().mockResolvedValue([
15
+ {
16
+ id: BigInt(42),
17
+ dsl: null,
18
+ versions: [],
19
+ _count: { instances: 2 },
20
+ },
21
+ ]),
22
+ count: jest.fn().mockResolvedValue(1),
23
+ },
24
+ workflowInstance: {
25
+ groupBy: jest.fn().mockResolvedValue([
26
+ {
27
+ workflowId: BigInt(42),
28
+ status: 'CANCELLED',
29
+ _count: 2,
30
+ },
31
+ ]),
32
+ findMany: jest.fn().mockResolvedValue([]),
33
+ },
34
+ };
35
+ const service = new WorkflowService(
36
+ prisma as any,
37
+ {} as any,
38
+ {} as any,
39
+ {} as any,
40
+ );
41
+
42
+ const result = await service.findMany({});
43
+
44
+ expect(result.items[0].runs).toEqual({
45
+ pending: 0,
46
+ running: 0,
47
+ completed: 0,
48
+ failed: 0,
49
+ cancelled: 2,
50
+ });
51
+ });
52
+ });
53
+
10
54
  describe('WorkflowService — cron lifecycle hooks', () => {
11
55
  let service: WorkflowService;
12
56
  let prisma: any;
@@ -592,7 +592,13 @@ export class WorkflowService {
592
592
 
593
593
  const runsMap = new Map<
594
594
  bigint,
595
- { pending: number; running: number; completed: number; failed: number }
595
+ {
596
+ pending: number;
597
+ running: number;
598
+ completed: number;
599
+ failed: number;
600
+ cancelled: number;
601
+ }
596
602
  >();
597
603
  for (const row of instanceCounts) {
598
604
  if (!runsMap.has(row.workflowId)) {
@@ -601,6 +607,7 @@ export class WorkflowService {
601
607
  running: 0,
602
608
  completed: 0,
603
609
  failed: 0,
610
+ cancelled: 0,
604
611
  });
605
612
  }
606
613
  const entry = runsMap.get(row.workflowId)!;
@@ -627,6 +634,7 @@ export class WorkflowService {
627
634
  running: 0,
628
635
  completed: 0,
629
636
  failed: 0,
637
+ cancelled: 0,
630
638
  },
631
639
  lastRun: lastRunMap.get(item.id)?.createdAt || null,
632
640
  }));
@@ -2260,8 +2268,13 @@ export class WorkflowService {
2260
2268
  async cancelExecutionInTransaction(
2261
2269
  tx: Prisma.TransactionClient,
2262
2270
  instanceId: bigint,
2271
+ requestedBy: string,
2263
2272
  reason?: string,
2264
2273
  ) {
2274
+ const normalizedRequestedBy = requestedBy.trim().slice(0, 128);
2275
+ if (!normalizedRequestedBy)
2276
+ throw new BadRequestException('Cancellation requester is required');
2277
+
2265
2278
  const rows = await tx.$queryRaw<
2266
2279
  Array<{
2267
2280
  id: bigint;
@@ -2301,6 +2314,7 @@ export class WorkflowService {
2301
2314
  status: 'CANCELLED',
2302
2315
  finishedAt: now,
2303
2316
  cancellationRequestedAt: now,
2317
+ cancellationRequestedBy: normalizedRequestedBy,
2304
2318
  cancellationReason: reason?.trim().slice(0, 512) || null,
2305
2319
  cancellationStatus: instance.temporalRunId ? 'PENDING' : 'COMPLETED',
2306
2320
  cancellationLastError: null,
@@ -112,7 +112,7 @@ describe('NodeConfigValidatorService', () => {
112
112
  },
113
113
  details: [],
114
114
  },
115
- subjects: [{ source: 'STATIC', provider: 'USER', refs: ['reviewer'] }],
115
+ subjects: [{ source: 'STATIC', refs: ['reviewer'] }],
116
116
  rejectionRule: { mode: 'ANY' },
117
117
  timeout: { durationSeconds: 0, action: 'REJECTED' },
118
118
  };
@@ -148,7 +148,7 @@ describe('NodeConfigValidatorService', () => {
148
148
  },
149
149
  details: [],
150
150
  },
151
- subjects: [{ source: 'STATIC', provider: 'USER', refs: ['reviewer'] }],
151
+ subjects: [{ source: 'STATIC', refs: ['reviewer'] }],
152
152
  approvalRule: { mode: 'ANY' },
153
153
  rejectionRule: { mode: 'ANY' },
154
154
  timeout: { durationSeconds: 0, action: 'REJECTED' },
@@ -210,7 +210,6 @@ describe('NodeConfigValidatorService', () => {
210
210
  subjects: [
211
211
  {
212
212
  source: 'DYNAMIC',
213
- provider: 'USER',
214
213
  inputKey: 'direct_manager',
215
214
  label: '查询直属领导',
216
215
  },
@@ -1,5 +1,4 @@
1
1
  import { BadRequestException } from '@nestjs/common';
2
- import { SubjectProviderError } from '../subjectProvider/subject-provider.errors';
3
2
  import { SubjectProviderRegistry } from '../subjectProvider/subject-provider.registry';
4
3
  import {
5
4
  ActorContext,
@@ -28,9 +27,9 @@ const schema = {
28
27
  },
29
28
  };
30
29
 
31
- function provider(): jest.Mocked<SubjectProvider> {
30
+ function provider(key = 'USER'): jest.Mocked<SubjectProvider> {
32
31
  return {
33
- key: 'USER_GROUP',
32
+ key,
34
33
  descriptor: jest.fn(),
35
34
  search: jest.fn(),
36
35
  create: jest.fn(),
@@ -58,8 +57,8 @@ describe('SubjectConfigValidatorService', () => {
58
57
  {
59
58
  approval: {
60
59
  subjects: [
61
- { provider: 'USER_GROUP', refs: ['group:ops'] },
62
- { provider: 'USER_GROUP', refs: ['group:security'] },
60
+ { source: 'STATIC', refs: ['alice'] },
61
+ { source: 'STATIC', refs: ['bob'] },
63
62
  ],
64
63
  },
65
64
  },
@@ -67,15 +66,39 @@ describe('SubjectConfigValidatorService', () => {
67
66
  );
68
67
 
69
68
  expect(subjectProvider.validate).toHaveBeenCalledWith(
70
- ['group:ops', 'group:security'],
69
+ ['alice', 'bob'],
71
70
  actor,
72
71
  );
73
72
  });
74
73
 
74
+ it('validates fixed user groups with the user-group provider', async () => {
75
+ const userGroupProvider = provider('USER_GROUP');
76
+ registry.register(userGroupProvider);
77
+
78
+ await service.validateSubjects(
79
+ schema,
80
+ {
81
+ approval: {
82
+ subjects: [
83
+ {
84
+ source: 'STATIC',
85
+ provider: 'USER_GROUP',
86
+ refs: ['finance'],
87
+ },
88
+ ],
89
+ },
90
+ },
91
+ actor,
92
+ );
93
+
94
+ expect(userGroupProvider.validate).toHaveBeenCalledWith(['finance'], actor);
95
+ expect(subjectProvider.validate).not.toHaveBeenCalled();
96
+ });
97
+
75
98
  it('rejects malformed, duplicate, and provider-invalid references', async () => {
76
99
  subjectProvider.validate.mockResolvedValue({
77
100
  valid: false,
78
- issues: [{ ref: 'group:missing', code: 'NOT_FOUND', message: 'Missing' }],
101
+ issues: [{ ref: 'missing', code: 'NOT_FOUND', message: 'Missing' }],
79
102
  });
80
103
 
81
104
  await expect(
@@ -83,7 +106,7 @@ describe('SubjectConfigValidatorService', () => {
83
106
  schema,
84
107
  {
85
108
  approval: {
86
- subjects: [{ provider: 'USER_GROUP', refs: ['group:missing'] }],
109
+ subjects: [{ source: 'STATIC', refs: ['missing'] }],
87
110
  },
88
111
  },
89
112
  actor,
@@ -96,19 +119,7 @@ describe('SubjectConfigValidatorService', () => {
96
119
  });
97
120
  });
98
121
 
99
- it('preserves registry errors and sanitizes unexpected provider failures', async () => {
100
- await expect(
101
- service.validateSubjects(
102
- schema,
103
- {
104
- approval: {
105
- subjects: [{ provider: 'MISSING', refs: ['value'] }],
106
- },
107
- },
108
- actor,
109
- ),
110
- ).rejects.toBeInstanceOf(SubjectProviderError);
111
-
122
+ it('sanitizes unexpected provider failures', async () => {
112
123
  subjectProvider.validate.mockRejectedValueOnce(
113
124
  new BadRequestException({ code: 'PROVIDER_LEAK', secret: 'secret DSN' }),
114
125
  );
@@ -117,7 +128,7 @@ describe('SubjectConfigValidatorService', () => {
117
128
  schema,
118
129
  {
119
130
  approval: {
120
- subjects: [{ provider: 'USER_GROUP', refs: ['group:ops'] }],
131
+ subjects: [{ source: 'STATIC', refs: ['alice'] }],
121
132
  },
122
133
  },
123
134
  actor,
@@ -137,16 +137,10 @@ export class SubjectConfigValidatorService {
137
137
  return;
138
138
  }
139
139
 
140
- const provider = validNonBlankString(entry.provider);
141
140
  const source = entry.source ?? 'STATIC';
141
+ const provider =
142
+ source === 'DYNAMIC' ? 'USER' : validProvider(entry.provider) ?? 'USER';
142
143
  const rawRefs = entry.refs;
143
- if (!provider) {
144
- issues.push({
145
- path: `${entryPath}/provider`,
146
- code: 'INVALID_PROVIDER',
147
- message: 'provider must be a nonblank string',
148
- });
149
- }
150
144
  if (source === 'DYNAMIC') {
151
145
  const inputKey = validNonBlankString(entry.inputKey);
152
146
  if (!inputKey) {
@@ -164,11 +158,9 @@ export class SubjectConfigValidatorService {
164
158
  } else {
165
159
  seenDynamicInputs.add(inputKey);
166
160
  }
167
- if (provider) {
168
- grouped.set(provider, grouped.get(provider) ?? []);
169
- if (!providerPaths.has(provider))
170
- providerPaths.set(provider, entryPath);
171
- }
161
+ grouped.set(provider, grouped.get(provider) ?? []);
162
+ if (!providerPaths.has(provider))
163
+ providerPaths.set(provider, entryPath);
172
164
  return;
173
165
  }
174
166
  if (!Array.isArray(rawRefs) || rawRefs.length === 0) {
@@ -190,17 +182,8 @@ export class SubjectConfigValidatorService {
190
182
  });
191
183
  return;
192
184
  }
193
- if (!provider) return;
194
-
195
- const duplicateKey = `${provider}\0${ref}`;
185
+ const duplicateKey = `${provider}\u0000${ref}`;
196
186
  if (seen.has(duplicateKey)) {
197
- issues.push({
198
- path: `${entryPath}/refs/${refIndex}`,
199
- provider,
200
- ref,
201
- code: 'DUPLICATE_REF',
202
- message: 'duplicate ref for provider',
203
- });
204
187
  return;
205
188
  }
206
189
 
@@ -232,6 +215,10 @@ function validNonBlankString(value: unknown): string | undefined {
232
215
  : undefined;
233
216
  }
234
217
 
218
+ function validProvider(value: unknown): 'USER' | 'USER_GROUP' | undefined {
219
+ return value === 'USER' || value === 'USER_GROUP' ? value : undefined;
220
+ }
221
+
235
222
  function toJsonPointer(path: readonly string[]): string {
236
223
  if (path.length === 0) return '/';
237
224
  return `/${path