@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
@@ -12,7 +12,7 @@ const registration = {
12
12
  registrationKey: 'a'.repeat(64),
13
13
  schemaName: 'public',
14
14
  tableName: 't_hardware_asset',
15
- operation: 'UPDATE',
15
+ operations: ['UPDATE'],
16
16
  watchedFields: ['status'],
17
17
  primaryKeyFields: ['id'],
18
18
  isEnabled: true,
@@ -67,6 +67,7 @@ describe('PostgreSQL business event registration', () => {
67
67
  type: 'event_trigger',
68
68
  config: {
69
69
  registrationCode: registration.code,
70
+ operationRules: [{ operation: 'UPDATE' }],
70
71
  processingMode: 'SERIAL_BY_KEY',
71
72
  concurrencyKeyPath: 'primaryKey.id',
72
73
  },
@@ -98,6 +99,7 @@ describe('PostgreSQL business event registration', () => {
98
99
  type: 'event_trigger',
99
100
  config: {
100
101
  registrationCode: registration.code,
102
+ operationRules: [{ operation: 'UPDATE' }],
101
103
  processingMode: 'SERIAL_BY_KEY',
102
104
  concurrencyKeyPath: 'missing',
103
105
  },
@@ -116,17 +118,22 @@ describe('PostgreSQL business event registration', () => {
116
118
  config: {
117
119
  registrationCode: registration.code,
118
120
  processingMode: 'PARALLEL',
119
- match: {
120
- combineOperation: 'AND',
121
- conditions: [
122
- {
123
- id: 'condition-1',
124
- left: { source: 'WORKFLOW', path: leftPath },
125
- operator: 'EQ',
126
- right: { source: 'FIXED', valueType, value: 'APPROVED' },
121
+ operationRules: [
122
+ {
123
+ operation: 'UPDATE',
124
+ match: {
125
+ combineOperation: 'AND',
126
+ conditions: [
127
+ {
128
+ id: 'condition-1',
129
+ left: { source: 'WORKFLOW', path: leftPath },
130
+ operator: 'EQ',
131
+ right: { source: 'FIXED', valueType, value: 'APPROVED' },
132
+ },
133
+ ],
127
134
  },
128
- ],
129
- },
135
+ },
136
+ ],
130
137
  },
131
138
  });
132
139
 
@@ -179,7 +186,7 @@ describe('PostgreSQL business event registration', () => {
179
186
  {
180
187
  schemaName: 'public',
181
188
  tableName: 't_hardware_asset',
182
- operation: 'UPDATE',
189
+ operations: ['UPDATE'],
183
190
  watchedFields: ['status'],
184
191
  },
185
192
  'admin',
@@ -224,7 +231,7 @@ describe('PostgreSQL business event registration', () => {
224
231
  {
225
232
  schemaName: 'another_schema',
226
233
  tableName: 't_hardware_asset',
227
- operation: 'UPDATE',
234
+ operations: ['UPDATE'],
228
235
  watchedFields: ['status'],
229
236
  },
230
237
  'admin',
@@ -236,8 +243,8 @@ describe('PostgreSQL business event registration', () => {
236
243
  it('registers INSERT for the whole row without requiring selected fields', async () => {
237
244
  const insertRegistration = {
238
245
  ...registration,
239
- operation: 'INSERT',
240
- watchedFields: ['id', 'status'],
246
+ operations: ['INSERT'],
247
+ watchedFields: [],
241
248
  };
242
249
  const executeRawUnsafe = jest.fn().mockResolvedValue(0);
243
250
  const tx = {
@@ -266,14 +273,14 @@ describe('PostgreSQL business event registration', () => {
266
273
  {
267
274
  schemaName: 'public',
268
275
  tableName: 't_hardware_asset',
269
- operation: 'INSERT',
276
+ operations: ['INSERT'],
270
277
  watchedFields: [],
271
278
  },
272
279
  'admin',
273
280
  );
274
281
 
275
282
  expect(tx.workflowBusinessEventRegistration.create).toHaveBeenCalledWith({
276
- data: expect.objectContaining({ watchedFields: ['id', 'status'] }),
283
+ data: expect.objectContaining({ watchedFields: [] }),
277
284
  });
278
285
  expect(executeRawUnsafe).toHaveBeenCalledWith(
279
286
  expect.stringContaining('AFTER INSERT ON'),
@@ -321,3 +328,145 @@ describe('PostgreSQL business event registration', () => {
321
328
  );
322
329
  });
323
330
  });
331
+
332
+ describe('multi-operation event rules', () => {
333
+ const [event] = buildBusinessEventCatalog(
334
+ [{ ...registration, operations: ['INSERT', 'UPDATE', 'DELETE'] }],
335
+ columns,
336
+ );
337
+ const condition = (path: string, values: string[]) => ({
338
+ id: path,
339
+ left: { source: 'WORKFLOW', path },
340
+ operator: 'IN',
341
+ right: {
342
+ source: 'FIXED',
343
+ valueType: 'STRING',
344
+ value: JSON.stringify(values),
345
+ },
346
+ });
347
+ const validate = (operationRules: unknown) =>
348
+ validateBusinessEventTriggers(
349
+ {
350
+ nodes: [
351
+ {
352
+ type: 'event_trigger',
353
+ config: {
354
+ registrationCode: event.registrationCode,
355
+ operationRules,
356
+ },
357
+ },
358
+ ],
359
+ },
360
+ [event],
361
+ );
362
+
363
+ it('requires stable primary keys for serial processing when before/after can be null', () => {
364
+ expect(
365
+ event.fields.find((field) => field.path === 'primaryKey.id')?.nullable,
366
+ ).toBe(false);
367
+ expect(
368
+ event.fields.find((field) => field.path === 'after.id')?.nullable,
369
+ ).toBe(true);
370
+ expect(
371
+ event.fields.find((field) => field.path === 'before.id')?.nullable,
372
+ ).toBe(true);
373
+ expect(() =>
374
+ validateBusinessEventTriggers(
375
+ {
376
+ nodes: [
377
+ {
378
+ type: 'event_trigger',
379
+ config: {
380
+ registrationCode: event.registrationCode,
381
+ operationRules: [{ operation: 'INSERT' }],
382
+ processingMode: 'SERIAL_BY_KEY',
383
+ concurrencyKeyPath: 'after.id',
384
+ },
385
+ },
386
+ ],
387
+ },
388
+ [event],
389
+ ),
390
+ ).toThrow(BadRequestException);
391
+ });
392
+
393
+ it('accepts independent insert and status transition rules', () => {
394
+ expect(() =>
395
+ validate([
396
+ {
397
+ operation: 'INSERT',
398
+ match: {
399
+ combineOperation: 'AND',
400
+ conditions: [
401
+ condition('after.status', ['PENDING', 'PENDING_SUPERIOR']),
402
+ ],
403
+ },
404
+ },
405
+ {
406
+ operation: 'UPDATE',
407
+ match: {
408
+ combineOperation: 'AND',
409
+ conditions: [
410
+ condition('before.status', ['DRAFT', 'REJECTED']),
411
+ condition('after.status', ['PENDING', 'PENDING_SUPERIOR']),
412
+ ],
413
+ },
414
+ },
415
+ ]),
416
+ ).not.toThrow();
417
+ });
418
+
419
+ it.each([
420
+ [],
421
+ [{ operation: 'UPSERT' }],
422
+ [{ operation: 'INSERT' }, { operation: 'INSERT' }],
423
+ [
424
+ {
425
+ operation: 'INSERT',
426
+ match: {
427
+ combineOperation: 'AND',
428
+ conditions: [condition('before.status', ['DRAFT'])],
429
+ },
430
+ },
431
+ ],
432
+ [
433
+ {
434
+ operation: 'DELETE',
435
+ match: {
436
+ combineOperation: 'AND',
437
+ conditions: [condition('after.status', ['DRAFT'])],
438
+ },
439
+ },
440
+ ],
441
+ [
442
+ {
443
+ operation: 'UPDATE',
444
+ match: {
445
+ combineOperation: 'AND',
446
+ conditions: [condition('after.status', [])],
447
+ },
448
+ },
449
+ ],
450
+ ])('rejects invalid or unavailable rules: %j', (...rules) => {
451
+ expect(() => validate(rules)).toThrow(BadRequestException);
452
+ });
453
+
454
+ it('rejects operations outside the registered set', () => {
455
+ expect(() =>
456
+ validateBusinessEventTriggers(
457
+ {
458
+ nodes: [
459
+ {
460
+ type: 'event_trigger',
461
+ config: {
462
+ registrationCode: event.registrationCode,
463
+ operationRules: [{ operation: 'DELETE' }],
464
+ },
465
+ },
466
+ ],
467
+ },
468
+ buildBusinessEventCatalog([registration], columns),
469
+ ),
470
+ ).toThrow(BadRequestException);
471
+ });
472
+ });
@@ -0,0 +1,82 @@
1
+ import { WorkflowService } from './workflow.service';
2
+
3
+ // Opt-in, isolated database check. All fixtures and DDL are rolled back.
4
+ const pgTest = process.env.WORKFLOW_EVENT_PG_TEST === '1' ? it : it.skip;
5
+
6
+ pgTest(
7
+ 'captures insert/delete rows and only real watched UPDATE changes, atomically',
8
+ async () => {
9
+ const { Client } = require('pg');
10
+ const client = new Client({ connectionString: process.env.DATABASE_URL });
11
+ const schemaName = `wf_event_test_${process.pid}`;
12
+ await client.connect();
13
+ try {
14
+ await client.query('BEGIN');
15
+ await client.query(`CREATE SCHEMA "${schemaName}"`);
16
+ await client.query(`SET LOCAL search_path TO "${schemaName}"`);
17
+ await client.query(
18
+ `CREATE TYPE "WorkflowEventOperation" AS ENUM ('INSERT', 'UPDATE', 'DELETE')`,
19
+ );
20
+ await client.query(
21
+ `CREATE TABLE request (id integer PRIMARY KEY, status text NOT NULL, title text)`,
22
+ );
23
+ await client.query(`CREATE TABLE t_workflow_event_outbox (
24
+ id serial PRIMARY KEY, registration_id bigint, operation "WorkflowEventOperation",
25
+ schema_name text, table_name text, primary_key jsonb, before jsonb, after jsonb,
26
+ changed_fields jsonb, status text, retry_count integer,
27
+ next_attempt_at timestamptz, created_at timestamptz, updated_at timestamptz
28
+ )`);
29
+ const tx = {
30
+ $queryRaw: async () => [{ schemaName }],
31
+ $executeRawUnsafe: (sql: string) => client.query(sql),
32
+ };
33
+ const service = new WorkflowService(
34
+ {} as any,
35
+ {} as any,
36
+ {} as any,
37
+ {} as any,
38
+ );
39
+ await (service as any).installBusinessEventTrigger(tx, {
40
+ id: BigInt(1),
41
+ schemaName,
42
+ tableName: 'request',
43
+ operations: ['INSERT', 'UPDATE', 'DELETE'],
44
+ watchedFields: ['status'],
45
+ primaryKeyFields: ['id'],
46
+ });
47
+ await client.query(`INSERT INTO request VALUES (1, 'DRAFT', 'original')`);
48
+ await client.query(`UPDATE request SET title = 'edited' WHERE id = 1`);
49
+ await client.query(`UPDATE request SET status = 'DRAFT' WHERE id = 1`);
50
+ await client.query(`UPDATE request SET status = 'PENDING' WHERE id = 1`);
51
+ await client.query('SAVEPOINT submission');
52
+ await client.query(`UPDATE request SET status = 'APPROVED' WHERE id = 1`);
53
+ await client.query('ROLLBACK TO SAVEPOINT submission');
54
+ await client.query(`DELETE FROM request WHERE id = 1`);
55
+ const { rows } = await client.query(
56
+ 'SELECT * FROM t_workflow_event_outbox ORDER BY id',
57
+ );
58
+ expect(rows.map((row: any) => row.operation)).toEqual([
59
+ 'INSERT',
60
+ 'UPDATE',
61
+ 'DELETE',
62
+ ]);
63
+ expect(rows[0]).toMatchObject({
64
+ before: null,
65
+ after: { status: 'DRAFT' },
66
+ primary_key: { id: 1 },
67
+ });
68
+ expect(rows[1]).toMatchObject({
69
+ before: { status: 'DRAFT' },
70
+ after: { status: 'PENDING' },
71
+ changed_fields: ['status'],
72
+ });
73
+ expect(rows[2]).toMatchObject({
74
+ before: { status: 'PENDING' },
75
+ after: null,
76
+ });
77
+ } finally {
78
+ await client.query('ROLLBACK');
79
+ await client.end();
80
+ }
81
+ },
82
+ );
@@ -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
  }
@@ -1,5 +1,7 @@
1
1
  import { Prisma } from '@prisma/client';
2
2
  import {
3
+ ArrayMinSize,
4
+ ArrayUnique,
3
5
  IsArray,
4
6
  IsBoolean,
5
7
  IsEnum,
@@ -32,8 +34,11 @@ export class RegisterBusinessEventDto {
32
34
  @Matches(/^[A-Za-z_][A-Za-z0-9_$]*$/)
33
35
  tableName: string;
34
36
 
35
- @IsEnum(WorkflowEventOperationEnum)
36
- operation: WorkflowEventOperationEnum;
37
+ @IsArray()
38
+ @ArrayMinSize(1)
39
+ @ArrayUnique()
40
+ @IsEnum(WorkflowEventOperationEnum, { each: true })
41
+ operations: WorkflowEventOperationEnum[];
37
42
 
38
43
  @IsArray()
39
44
  @IsString({ each: true })
@@ -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;