@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
@@ -61,7 +61,7 @@ export interface BusinessEventCatalogItem {
61
61
  label: string;
62
62
  schemaName: string;
63
63
  tableName: string;
64
- operation: 'INSERT' | 'UPDATE' | 'DELETE';
64
+ operations: Array<'INSERT' | 'UPDATE' | 'DELETE'>;
65
65
  watchedFields: string[];
66
66
  primaryKeyFields: string[];
67
67
  isEnabled: boolean;
@@ -128,7 +128,7 @@ export function buildBusinessEventCatalog(
128
128
  code: string;
129
129
  schemaName: string;
130
130
  tableName: string;
131
- operation: string;
131
+ operations: string[];
132
132
  watchedFields: Prisma.JsonValue;
133
133
  primaryKeyFields: Prisma.JsonValue;
134
134
  isEnabled: boolean;
@@ -143,12 +143,14 @@ export function buildBusinessEventCatalog(
143
143
  column.schemaName === row.schemaName &&
144
144
  column.tableName === row.tableName,
145
145
  );
146
- const prefixes =
147
- row.operation === 'INSERT'
148
- ? ['after']
149
- : row.operation === 'DELETE'
146
+ const prefixes = [
147
+ ...(row.operations.some((operation) => operation !== 'INSERT')
150
148
  ? ['before']
151
- : ['before', 'after'];
149
+ : []),
150
+ ...(row.operations.some((operation) => operation !== 'DELETE')
151
+ ? ['after']
152
+ : []),
153
+ ];
152
154
  const fields = [
153
155
  ...primaryKeyFields.flatMap((columnName) => {
154
156
  const column = sourceColumns.find(
@@ -164,17 +166,22 @@ export function buildBusinessEventCatalog(
164
166
  `${prefix}.${column.columnName}`,
165
167
  column,
166
168
  false,
167
- column.nullable,
169
+ column.nullable ||
170
+ row.operations.includes(
171
+ prefix === 'before' ? 'INSERT' : 'DELETE',
172
+ ),
168
173
  ),
169
174
  ),
170
175
  ),
171
176
  ];
172
177
  return {
173
178
  registrationCode: row.code,
174
- label: `${row.schemaName}.${row.tableName} · ${row.operation}`,
179
+ label: `${row.schemaName}.${row.tableName} · ${row.operations.join(
180
+ ' / ',
181
+ )}`,
175
182
  schemaName: row.schemaName,
176
183
  tableName: row.tableName,
177
- operation: row.operation as BusinessEventCatalogItem['operation'],
184
+ operations: row.operations as BusinessEventCatalogItem['operations'],
178
185
  watchedFields,
179
186
  primaryKeyFields,
180
187
  isEnabled: row.isEnabled,
@@ -226,7 +233,28 @@ export function validateBusinessEventTriggers(
226
233
  const event = eventByCode.get(registrationCode);
227
234
  if (!event) continue;
228
235
 
229
- validateBusinessEventMatch(config.match, event);
236
+ const rules = config.operationRules;
237
+ if (!Array.isArray(rules) || rules.length === 0)
238
+ throw invalidBusinessEventMatch(event, '请至少启用一种操作的启动规则');
239
+ const seen = new Set<string>();
240
+ for (const value of rules) {
241
+ const rule = asRecord(value);
242
+ const operation =
243
+ rule.operation as BusinessEventCatalogItem['operations'][number];
244
+ if (!event.operations.includes(operation) || seen.has(operation))
245
+ throw invalidBusinessEventMatch(event, '操作未登记或重复配置');
246
+ seen.add(operation);
247
+ // before is unavailable for INSERT; after is unavailable for DELETE.
248
+ const availableEvent = {
249
+ ...event,
250
+ fields: event.fields.filter(
251
+ (field) =>
252
+ !(operation === 'INSERT' && field.path.startsWith('before.')) &&
253
+ !(operation === 'DELETE' && field.path.startsWith('after.')),
254
+ ),
255
+ };
256
+ validateBusinessEventMatch(rule.match, availableEvent);
257
+ }
230
258
 
231
259
  if (config.processingMode !== 'SERIAL_BY_KEY') continue;
232
260
 
@@ -246,7 +274,13 @@ export function validateBusinessEventTriggers(
246
274
  }
247
275
  }
248
276
 
249
- const EVENT_MATCH_OPERATORS = new Set(['EQ', 'NEQ', 'EMPTY', 'NOT_EMPTY']);
277
+ const EVENT_MATCH_OPERATORS = new Set([
278
+ 'EQ',
279
+ 'NEQ',
280
+ 'IN',
281
+ 'EMPTY',
282
+ 'NOT_EMPTY',
283
+ ]);
250
284
  const EVENT_MATCH_UNARY_OPERATORS = new Set(['EMPTY', 'NOT_EMPTY']);
251
285
 
252
286
  function validateBusinessEventMatch(
@@ -292,14 +326,27 @@ function validateBusinessEventMatch(
292
326
  : field.type === 'boolean'
293
327
  ? 'BOOLEAN'
294
328
  : 'STRING';
329
+ let values: unknown[] = [right.value];
330
+ if (operator === 'IN') {
331
+ try {
332
+ values = JSON.parse(String(right.value));
333
+ } catch {
334
+ throw invalidBusinessEventMatch(event, '比较值列表无效');
335
+ }
336
+ }
295
337
  if (
296
338
  right.source !== 'FIXED' ||
297
339
  right.valueType !== expectedValueType ||
298
- typeof right.value !== 'string' ||
299
- (expectedValueType === 'NUMBER' &&
300
- (!right.value.trim() || !Number.isFinite(Number(right.value)))) ||
301
- (expectedValueType === 'BOOLEAN' &&
302
- !['true', 'false'].includes(right.value))
340
+ !Array.isArray(values) ||
341
+ values.length === 0 ||
342
+ values.some(
343
+ (value) =>
344
+ typeof value !== 'string' ||
345
+ (expectedValueType === 'NUMBER' &&
346
+ (!value.trim() || !Number.isFinite(Number(value)))) ||
347
+ (expectedValueType === 'BOOLEAN' &&
348
+ !['true', 'false'].includes(value)),
349
+ )
303
350
  ) {
304
351
  throw invalidBusinessEventMatch(event, `字段“${path}”的比较值类型不正确`);
305
352
  }
@@ -592,7 +639,13 @@ export class WorkflowService {
592
639
 
593
640
  const runsMap = new Map<
594
641
  bigint,
595
- { pending: number; running: number; completed: number; failed: number }
642
+ {
643
+ pending: number;
644
+ running: number;
645
+ completed: number;
646
+ failed: number;
647
+ cancelled: number;
648
+ }
596
649
  >();
597
650
  for (const row of instanceCounts) {
598
651
  if (!runsMap.has(row.workflowId)) {
@@ -601,6 +654,7 @@ export class WorkflowService {
601
654
  running: 0,
602
655
  completed: 0,
603
656
  failed: 0,
657
+ cancelled: 0,
604
658
  });
605
659
  }
606
660
  const entry = runsMap.get(row.workflowId)!;
@@ -627,6 +681,7 @@ export class WorkflowService {
627
681
  running: 0,
628
682
  completed: 0,
629
683
  failed: 0,
684
+ cancelled: 0,
630
685
  },
631
686
  lastRun: lastRunMap.get(item.id)?.createdAt || null,
632
687
  }));
@@ -713,11 +768,7 @@ export class WorkflowService {
713
768
  const [events, columns, schemaName] = await Promise.all([
714
769
  this.prisma.workflowBusinessEventRegistration.findMany({
715
770
  where: { isEnabled: true },
716
- orderBy: [
717
- { schemaName: 'asc' },
718
- { tableName: 'asc' },
719
- { operation: 'asc' },
720
- ],
771
+ orderBy: [{ schemaName: 'asc' }, { tableName: 'asc' }, { code: 'asc' }],
721
772
  }),
722
773
  this.findBusinessEventColumns(),
723
774
  this.findBusinessEventSchema(),
@@ -802,7 +853,7 @@ export class WorkflowService {
802
853
  data: {
803
854
  schemaName: string;
804
855
  tableName: string;
805
- operation: 'INSERT' | 'UPDATE' | 'DELETE';
856
+ operations: Array<'INSERT' | 'UPDATE' | 'DELETE'>;
806
857
  watchedFields: string[];
807
858
  },
808
859
  creator: string,
@@ -823,17 +874,24 @@ export class WorkflowService {
823
874
  if (sourceColumns.length === 0)
824
875
  throw new BadRequestException('请选择可监听的数据库表');
825
876
 
877
+ if (
878
+ !Array.isArray(data.operations) ||
879
+ data.operations.length === 0 ||
880
+ data.operations.some(
881
+ (operation) => !['INSERT', 'UPDATE', 'DELETE'].includes(operation),
882
+ )
883
+ ) {
884
+ throw new BadRequestException('请至少选择一种有效的数据操作');
885
+ }
886
+ const operations = [...new Set(data.operations)].sort();
826
887
  const requestedFields = [
827
888
  ...new Set(
828
889
  data.watchedFields.map((field) => field.trim()).filter(Boolean),
829
890
  ),
830
891
  ].sort();
831
- if (data.operation === 'UPDATE' && requestedFields.length === 0)
892
+ if (operations.includes('UPDATE') && requestedFields.length === 0)
832
893
  throw new BadRequestException('请至少选择一个监听字段');
833
- const watchedFields =
834
- data.operation === 'UPDATE'
835
- ? requestedFields
836
- : sourceColumns.map((column) => column.columnName).sort();
894
+ const watchedFields = operations.includes('UPDATE') ? requestedFields : [];
837
895
  const availableFields = new Set(
838
896
  sourceColumns.map((column) => column.columnName),
839
897
  );
@@ -853,7 +911,7 @@ export class WorkflowService {
853
911
  JSON.stringify({
854
912
  schemaName: data.schemaName,
855
913
  tableName: data.tableName,
856
- operation: data.operation,
914
+ operations,
857
915
  watchedFields,
858
916
  }),
859
917
  )
@@ -877,7 +935,7 @@ export class WorkflowService {
877
935
  registrationKey,
878
936
  schemaName: data.schemaName,
879
937
  tableName: data.tableName,
880
- operation: data.operation,
938
+ operations,
881
939
  watchedFields,
882
940
  primaryKeyFields,
883
941
  creator,
@@ -887,7 +945,9 @@ export class WorkflowService {
887
945
  return registration;
888
946
  });
889
947
  this.logger.log(
890
- `Registered ${event.operation} listener ${event.schemaName}.${event.tableName} (${event.code})`,
948
+ `Registered ${event.operations.join('/')} listener ${event.schemaName}.${
949
+ event.tableName
950
+ } (${event.code})`,
891
951
  );
892
952
  return buildBusinessEventCatalog([event], columns)[0];
893
953
  }
@@ -898,7 +958,7 @@ export class WorkflowService {
898
958
  id: bigint;
899
959
  schemaName: string;
900
960
  tableName: string;
901
- operation: string;
961
+ operations: string[];
902
962
  watchedFields: Prisma.JsonValue;
903
963
  primaryKeyFields: Prisma.JsonValue;
904
964
  },
@@ -926,10 +986,13 @@ export class WorkflowService {
926
986
  const workflowSchema = quoteIdentifier(
927
987
  workflowSchemaRows[0]?.schemaName || 'public',
928
988
  );
929
- const triggerEvent =
930
- event.operation === 'UPDATE'
931
- ? `UPDATE OF ${watchedFields.map(quoteIdentifier).join(', ')}`
932
- : event.operation;
989
+ const triggerEvent = event.operations
990
+ .map((operation) =>
991
+ operation === 'UPDATE'
992
+ ? `UPDATE OF ${watchedFields.map(quoteIdentifier).join(', ')}`
993
+ : operation,
994
+ )
995
+ .join(' OR ');
933
996
 
934
997
  await tx.$executeRawUnsafe(`
935
998
  CREATE OR REPLACE FUNCTION ${schema}.${functionName}()
@@ -2260,8 +2323,13 @@ export class WorkflowService {
2260
2323
  async cancelExecutionInTransaction(
2261
2324
  tx: Prisma.TransactionClient,
2262
2325
  instanceId: bigint,
2326
+ requestedBy: string,
2263
2327
  reason?: string,
2264
2328
  ) {
2329
+ const normalizedRequestedBy = requestedBy.trim().slice(0, 128);
2330
+ if (!normalizedRequestedBy)
2331
+ throw new BadRequestException('Cancellation requester is required');
2332
+
2265
2333
  const rows = await tx.$queryRaw<
2266
2334
  Array<{
2267
2335
  id: bigint;
@@ -2301,6 +2369,7 @@ export class WorkflowService {
2301
2369
  status: 'CANCELLED',
2302
2370
  finishedAt: now,
2303
2371
  cancellationRequestedAt: now,
2372
+ cancellationRequestedBy: normalizedRequestedBy,
2304
2373
  cancellationReason: reason?.trim().slice(0, 512) || null,
2305
2374
  cancellationStatus: instance.temporalRunId ? 'PENDING' : 'COMPLETED',
2306
2375
  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
  },
@@ -354,3 +353,60 @@ describe('NodeConfigValidatorService', () => {
354
353
  ).not.toThrow();
355
354
  });
356
355
  });
356
+
357
+ describe('event trigger operation rules schema', () => {
358
+ const schema = NODE_TYPES.find((node) => node.type === 'event_trigger')!
359
+ .configSchema as JsonSchema;
360
+ const service = new NodeConfigValidatorService();
361
+ const validate = (operationRules: unknown) =>
362
+ service.validateOrThrow('event:5', schema, {
363
+ registrationCode: 'request-changes',
364
+ processingMode: 'PARALLEL',
365
+ operationRules,
366
+ });
367
+ it('accepts multiple operations with list conditions', () => {
368
+ expect(() =>
369
+ validate([
370
+ { operation: 'INSERT' },
371
+ {
372
+ operation: 'UPDATE',
373
+ match: {
374
+ combineOperation: 'AND',
375
+ conditions: [
376
+ {
377
+ id: 'status',
378
+ left: { source: 'WORKFLOW', path: 'after.status' },
379
+ operator: 'IN',
380
+ right: {
381
+ source: 'FIXED',
382
+ valueType: 'STRING',
383
+ value: '["PENDING","PENDING_SUPERIOR"]',
384
+ },
385
+ },
386
+ ],
387
+ },
388
+ },
389
+ ]),
390
+ ).not.toThrow();
391
+ });
392
+ it('rejects empty rules and incomplete conditions', () => {
393
+ expect(() => validate([])).toThrow(BadRequestException);
394
+ expect(() =>
395
+ validate([
396
+ {
397
+ operation: 'UPDATE',
398
+ match: {
399
+ combineOperation: 'AND',
400
+ conditions: [
401
+ {
402
+ id: 'status',
403
+ left: { source: 'WORKFLOW', path: 'after.status' },
404
+ operator: 'IN',
405
+ },
406
+ ],
407
+ },
408
+ },
409
+ ]),
410
+ ).toThrow(BadRequestException);
411
+ });
412
+ });
@@ -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
@@ -0,0 +1,69 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import { act } from 'react';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { searchStaffDirectory } from './staff-directory.api';
5
+ import { StaffSelect } from './StaffSelect';
6
+
7
+ vi.mock('./staff-directory.api', () => ({
8
+ searchStaffDirectory: vi.fn(),
9
+ }));
10
+
11
+ vi.mock('antd', () => ({
12
+ Select: ({ onSearch, onChange, options = [] }: any) => (
13
+ <div>
14
+ <input
15
+ aria-label="staff search"
16
+ onChange={(event) => onSearch(event.target.value)}
17
+ />
18
+ {options.map((option: any) => (
19
+ <button key={option.value} onClick={() => onChange(option.value)}>
20
+ {option.label}
21
+ </button>
22
+ ))}
23
+ </div>
24
+ ),
25
+ Empty: Object.assign(() => <div>empty</div>, {
26
+ PRESENTED_IMAGE_SIMPLE: 'simple',
27
+ }),
28
+ Space: ({ children }: any) => <div>{children}</div>,
29
+ Spin: () => <div>loading</div>,
30
+ Typography: {
31
+ Text: ({ children }: any) => <span>{children}</span>,
32
+ },
33
+ }));
34
+
35
+ describe('StaffSelect', () => {
36
+ beforeEach(() => {
37
+ vi.useFakeTimers();
38
+ vi.clearAllMocks();
39
+ });
40
+
41
+ afterEach(() => vi.useRealTimers());
42
+
43
+ it('searches through the shared backend and returns the selected person', async () => {
44
+ vi.mocked(searchStaffDirectory).mockResolvedValue([
45
+ {
46
+ userId: 'junrujrwang',
47
+ displayName: 'Junru Wang',
48
+ businessGroup: 'OIT',
49
+ },
50
+ ]);
51
+ const onChange = vi.fn();
52
+ const onPersonChange = vi.fn();
53
+ render(<StaffSelect onChange={onChange} onPersonChange={onPersonChange} />);
54
+
55
+ fireEvent.change(screen.getByRole('textbox', { name: 'staff search' }), {
56
+ target: { value: 'junru' },
57
+ });
58
+ await act(() => vi.advanceTimersByTimeAsync(300));
59
+
60
+ expect(searchStaffDirectory).toHaveBeenCalledWith('junru');
61
+ fireEvent.click(
62
+ screen.getByRole('button', { name: 'Junru Wang (junrujrwang)' }),
63
+ );
64
+ expect(onChange).toHaveBeenCalledWith('junrujrwang');
65
+ expect(onPersonChange).toHaveBeenCalledWith(
66
+ expect.objectContaining({ userId: 'junrujrwang' }),
67
+ );
68
+ });
69
+ });