@gadmin2n/schematics 0.0.152 → 0.0.153

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 (43) 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/config/prisma/workflow.prisma +1 -0
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.seed-timestamp +1 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/index.ts +2 -1
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +23 -44
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +25 -4
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +74 -64
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.ts +0 -7
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +17 -0
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +58 -3
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.ts +66 -6
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution-idempotency.spec.ts +61 -149
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +1 -1
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.spec.ts +1 -1
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.ts +1 -1
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.spec.ts +2 -2
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +38 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +18 -7
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.dto.ts +1 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +131 -242
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +7 -5
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +6 -36
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +0 -4
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +29 -7
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +29 -7
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +20 -17
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +38 -35
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.tsx +6 -1
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.tsx +24 -22
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/EnhancedFlowRenderer.tsx +0 -11
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/StatusCards.tsx +24 -0
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +20 -18
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +7 -0
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +28 -79
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instances.tsx +5 -0
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/NodeInstanceForm.tsx +0 -1
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +227 -97
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +62 -0
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/settings/index.tsx +73 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +21 -46
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +2 -0
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/newNodeConfig.ts +1 -0
  43. package/package.json +1 -1
@@ -47,6 +47,7 @@ describe('Workflow execution idempotency', () => {
47
47
  temporalRunId: 'wf-7-21',
48
48
  }),
49
49
  cancelWorkflow: jest.fn().mockResolvedValue(undefined),
50
+ signalWorkflow: jest.fn().mockResolvedValue(undefined),
50
51
  inspectWorkflow: jest.fn(),
51
52
  };
52
53
  const lifecycle: any = {
@@ -362,7 +363,7 @@ describe('Workflow execution idempotency', () => {
362
363
  );
363
364
  });
364
365
 
365
- it('returns the existing recovery instead of creating another retry from the same failed run', async () => {
366
+ it('rejects node retry after the Temporal run has already failed', async () => {
366
367
  const { prisma, temporal, service } = setup();
367
368
  prisma.workflowInstance.findUnique.mockResolvedValueOnce({
368
369
  id: BigInt(22),
@@ -372,100 +373,82 @@ describe('Workflow execution idempotency', () => {
372
373
  context: { workflow: { orderId: 'O-1' }, nodes: {} },
373
374
  nodeExecutions: [],
374
375
  });
375
- prisma.workflowInstance.findFirst.mockResolvedValueOnce({
376
- id: BigInt(23),
377
- workflowId,
378
- versionId: version.id,
379
- status: 'COMPLETED',
380
- temporalRunId: 'wf-7-23',
381
- });
382
- const execute = jest.spyOn(service, 'executeWorkflow');
383
376
 
384
377
  await expect(
385
- service.retryInstanceFromNode(BigInt(22), 'request', 'alice', temporal),
378
+ service.retryFailedNode(BigInt(22), 'request', temporal),
386
379
  ).resolves.toEqual({
387
- instanceId: '23',
388
- temporalRunId: 'wf-7-23',
389
- workflowVersionId: '11',
390
- status: 'COMPLETED',
380
+ kind: 'conflict',
381
+ message: '只有等待重试的 Workflow 才能重试失败节点',
391
382
  });
392
- expect(execute).not.toHaveBeenCalled();
383
+ expect(temporal.signalWorkflow).not.toHaveBeenCalled();
393
384
  });
394
385
 
395
- it('creates a checkpointed retry that preserves the original side-effect scope', async () => {
386
+ it('signals the same Temporal run when a waiting node is retried', async () => {
396
387
  const { prisma, temporal, service } = setup();
397
- const approvalOutput = {
398
- previous: { eventId: 'event-1' },
399
- result: { requestId: '6', result: 'APPROVED', approved: true },
400
- };
388
+ const failedAt = new Date('2026-09-16T08:00:00.000Z');
401
389
  prisma.workflowInstance.findUnique.mockResolvedValueOnce({
402
390
  id: BigInt(22),
403
391
  workflowId,
404
392
  versionId: version.id,
405
- status: 'FAILED',
406
- context: {
407
- workflow: { eventId: 'event-1' },
408
- nodes: {},
409
- system: { operationScopeId: '21' },
410
- },
393
+ status: 'WAITING_RETRY',
394
+ temporalRunId: 'wf-7-22',
395
+ context: { workflow: { orderId: 'O-1' }, nodes: {} },
411
396
  nodeExecutions: [
412
397
  {
413
- nodeId: 'approval',
414
- status: 'COMPLETED',
415
- output: approvalOutput,
398
+ nodeId: 'parallel',
399
+ status: 'FAILED',
400
+ output: null,
401
+ updatedAt: failedAt,
416
402
  },
417
- { nodeId: 'request', status: 'FAILED', output: null },
418
403
  ],
419
404
  });
420
405
  prisma.workflowVersion.findUnique.mockResolvedValueOnce({
421
406
  dsl: {
422
407
  nodes: [
423
- { id: 'start', type: 'event_trigger', label: '业务事件触发' },
424
- { id: 'approval', type: 'approval', label: '人工审批' },
425
- { id: 'request', type: 'http_request', label: '确认报废' },
408
+ { id: 'start', type: 'manual_trigger', label: '手动触发' },
409
+ { id: 'parallel', type: 'parallel', label: '并行执行' },
410
+ { id: 'branch-request', type: 'http_request', label: '分支请求' },
426
411
  ],
427
412
  edges: [
428
- { source: 'start', target: 'approval', kind: 'FLOW' },
413
+ { source: 'start', target: 'parallel', kind: 'FLOW' },
414
+ {
415
+ source: 'parallel',
416
+ target: 'branch-request',
417
+ kind: 'CALL',
418
+ pairId: 'branch-a',
419
+ },
429
420
  {
430
- source: 'approval',
431
- target: 'request',
432
- sourceHandle: 'true',
433
- kind: 'FLOW',
421
+ source: 'branch-request',
422
+ target: 'parallel',
423
+ kind: 'RETURN',
424
+ pairId: 'branch-a',
434
425
  },
435
426
  ],
436
427
  },
437
428
  });
438
- const execute = jest
439
- .spyOn(service, 'executeWorkflow')
440
- .mockResolvedValue({ instanceId: '23' } as any);
441
-
442
- await service.retryInstanceFromNode(
443
- BigInt(22),
444
- 'request',
445
- 'alice',
446
- temporal,
447
- );
429
+ const execute = jest.spyOn(service, 'executeWorkflow');
448
430
 
449
- expect(execute).toHaveBeenCalledWith(
450
- workflowId,
451
- {
452
- workflow: { eventId: 'event-1' },
453
- workflowVersionId: version.id,
454
- idempotencyKey: 'retry:22',
431
+ await expect(
432
+ service.retryFailedNode(BigInt(22), 'parallel', temporal),
433
+ ).resolves.toEqual({
434
+ kind: 'ok',
435
+ value: {
436
+ instanceId: '22',
437
+ temporalRunId: 'wf-7-22',
438
+ workflowVersionId: '11',
439
+ status: 'WAITING_RETRY',
455
440
  },
456
- 'alice',
457
- temporal,
441
+ });
442
+
443
+ expect(temporal.signalWorkflow).toHaveBeenCalledWith(
444
+ 'wf-7-22',
445
+ 'retryNode',
458
446
  {
459
- operationScopeId: '21',
460
- retryCheckpoint: {
461
- startNodeId: 'request',
462
- completedNodeIds: ['approval'],
463
- outputs: { approval: approvalOutput },
464
- },
465
- retryOfInstanceId: BigInt(22),
466
- retryFromNodeId: 'request',
447
+ nodeId: 'parallel',
448
+ requestId: 'retry-node:22:parallel:2026-09-16T08:00:00.000Z',
467
449
  },
468
450
  );
451
+ expect(execute).not.toHaveBeenCalled();
469
452
  });
470
453
 
471
454
  it('does not retry from inside a private branch pipeline', async () => {
@@ -474,10 +457,16 @@ describe('Workflow execution idempotency', () => {
474
457
  id: BigInt(22),
475
458
  workflowId,
476
459
  versionId: version.id,
477
- status: 'FAILED',
460
+ status: 'WAITING_RETRY',
461
+ temporalRunId: 'wf-7-22',
478
462
  context: { workflow: {}, nodes: {} },
479
463
  nodeExecutions: [
480
- { nodeId: 'branch-request', status: 'FAILED', output: null },
464
+ {
465
+ nodeId: 'branch-request',
466
+ status: 'FAILED',
467
+ output: null,
468
+ updatedAt: new Date('2026-09-16T08:00:00.000Z'),
469
+ },
481
470
  ],
482
471
  });
483
472
  prisma.workflowVersion.findUnique.mockResolvedValueOnce({
@@ -504,88 +493,11 @@ describe('Workflow execution idempotency', () => {
504
493
  });
505
494
 
506
495
  await expect(
507
- service.retryInstanceFromNode(
508
- BigInt(22),
509
- 'branch-request',
510
- 'alice',
511
- temporal,
512
- ),
513
- ).rejects.toThrow('暂不支持从循环或并行分支内部重试');
514
- });
515
-
516
- it('keeps inherited checkpoint outputs when a resumed run fails farther downstream', async () => {
517
- const { prisma, temporal, service } = setup();
518
- const approvalOutput = {
519
- result: { requestId: '6', result: 'APPROVED', approved: true },
520
- };
521
- const requestOutput = { status: 200, data: { resolved: true } };
522
- prisma.workflowInstance.findUnique.mockResolvedValueOnce({
523
- id: BigInt(23),
524
- workflowId,
525
- versionId: version.id,
526
- status: 'FAILED',
527
- context: {
528
- workflow: { eventId: 'event-1' },
529
- nodes: {},
530
- system: {
531
- operationScopeId: '21',
532
- retryCheckpoint: {
533
- startNodeId: 'request',
534
- completedNodeIds: ['approval'],
535
- outputs: { approval: approvalOutput },
536
- },
537
- },
538
- },
539
- nodeExecutions: [
540
- { nodeId: 'request', status: 'COMPLETED', output: requestOutput },
541
- { nodeId: 'notify', status: 'FAILED', output: null },
542
- ],
543
- });
544
- prisma.workflowVersion.findUnique.mockResolvedValueOnce({
545
- dsl: {
546
- nodes: [
547
- { id: 'start', type: 'event_trigger', label: '业务事件触发' },
548
- { id: 'approval', type: 'approval', label: '人工审批' },
549
- { id: 'request', type: 'http_request', label: '确认报废' },
550
- { id: 'notify', type: 'http_request', label: '发送通知' },
551
- ],
552
- edges: [
553
- { source: 'start', target: 'approval', kind: 'FLOW' },
554
- { source: 'approval', target: 'request', kind: 'FLOW' },
555
- { source: 'request', target: 'notify', kind: 'FLOW' },
556
- ],
557
- },
496
+ service.retryFailedNode(BigInt(22), 'branch-request', temporal),
497
+ ).resolves.toEqual({
498
+ kind: 'invalid_input',
499
+ message: '请从外层循环或并行节点重试',
558
500
  });
559
- const execute = jest
560
- .spyOn(service, 'executeWorkflow')
561
- .mockResolvedValue({ instanceId: '24' } as any);
562
-
563
- await service.retryInstanceFromNode(
564
- BigInt(23),
565
- 'notify',
566
- 'alice',
567
- temporal,
568
- );
569
-
570
- expect(execute).toHaveBeenCalledWith(
571
- workflowId,
572
- expect.objectContaining({
573
- idempotencyKey: 'retry:23',
574
- }),
575
- 'alice',
576
- temporal,
577
- expect.objectContaining({
578
- operationScopeId: '21',
579
- retryCheckpoint: {
580
- startNodeId: 'notify',
581
- completedNodeIds: ['approval', 'request'],
582
- outputs: {
583
- approval: approvalOutput,
584
- request: requestOutput,
585
- },
586
- },
587
- }),
588
- );
589
501
  });
590
502
 
591
503
  it('restarts a stale reserved instance when Temporal has no execution', async () => {
@@ -785,7 +697,7 @@ describe('Workflow execution idempotency', () => {
785
697
  expect(lifecycle.markFailed).toHaveBeenCalledWith(
786
698
  expect.objectContaining({
787
699
  instanceId: BigInt(21),
788
- expectedStatuses: ['PENDING', 'RUNNING'],
700
+ expectedStatuses: ['PENDING', 'RUNNING', 'WAITING_RETRY'],
789
701
  temporalRunId: 'wf-7-21',
790
702
  }),
791
703
  );
@@ -813,7 +725,7 @@ describe('Workflow execution idempotency', () => {
813
725
  expect(lifecycle.markFailed).toHaveBeenCalledWith(
814
726
  expect.objectContaining({
815
727
  instanceId: BigInt(21),
816
- expectedStatuses: ['RUNNING'],
728
+ expectedStatuses: ['RUNNING', 'WAITING_RETRY'],
817
729
  temporalRunId: 'wf-7-21',
818
730
  result: expect.objectContaining({
819
731
  code: 'TEMPORAL_EXECUTION_MISSING',
@@ -24,7 +24,7 @@ export class ApproveInstanceDto {
24
24
  comment?: string;
25
25
  }
26
26
 
27
- export class RetryFromNodeDto {
27
+ export class RetryFailedNodeDto {
28
28
  @IsString()
29
29
  @IsNotEmpty()
30
30
  nodeId: string;
@@ -26,7 +26,7 @@ describe('WorkflowLifecycleRuntimeController', () => {
26
26
  expect(runtimeConfig.assertTenant).toHaveBeenCalledWith('gadmin-test');
27
27
  expect(lifecycle.markFailed).toHaveBeenCalledWith({
28
28
  instanceId: BigInt(45),
29
- expectedStatuses: ['PENDING', 'RUNNING'],
29
+ expectedStatuses: ['PENDING', 'RUNNING', 'WAITING_RETRY'],
30
30
  result: { error: 'dynamic_users is required' },
31
31
  });
32
32
  });
@@ -42,7 +42,7 @@ export class WorkflowLifecycleRuntimeController {
42
42
  const instanceId = BigInt(input.workflowInstanceId);
43
43
  await this.lifecycle.markFailed({
44
44
  instanceId,
45
- expectedStatuses: ['PENDING', 'RUNNING'],
45
+ expectedStatuses: ['PENDING', 'RUNNING', 'WAITING_RETRY'],
46
46
  result: (input.result ?? {
47
47
  code: 'WORKFLOW_EXECUTION_FAILED',
48
48
  }) as Prisma.InputJsonObject,
@@ -61,7 +61,7 @@ describe('WorkflowLifecycleService', () => {
61
61
  await expect(
62
62
  service.markFailed({
63
63
  instanceId: BigInt(45),
64
- expectedStatuses: ['PENDING', 'RUNNING'],
64
+ expectedStatuses: ['PENDING', 'RUNNING', 'WAITING_RETRY'],
65
65
  temporalRunId: 'wf-45',
66
66
  result: { code: 'TEMPORAL_TERMINAL_STATUS_RECONCILED' },
67
67
  finishedAt: failedAt,
@@ -72,7 +72,7 @@ describe('WorkflowLifecycleService', () => {
72
72
  expect(tx.workflowInstance.updateMany).toHaveBeenCalledWith({
73
73
  where: {
74
74
  id: BigInt(45),
75
- status: { in: ['PENDING', 'RUNNING'] },
75
+ status: { in: ['PENDING', 'RUNNING', 'WAITING_RETRY'] },
76
76
  temporalRunId: 'wf-45',
77
77
  },
78
78
  data: {
@@ -1,3 +1,8 @@
1
+ import {
2
+ BadRequestException,
3
+ ConflictException,
4
+ NotFoundException,
5
+ } from '@nestjs/common';
1
6
  import { WorkflowController } from './workflow.controller';
2
7
 
3
8
  describe('WorkflowController runtime contracts', () => {
@@ -94,4 +99,37 @@ describe('WorkflowController runtime contracts', () => {
94
99
  requestedBy: 'admin-user',
95
100
  });
96
101
  });
102
+
103
+ it.each([
104
+ ['not_found', NotFoundException],
105
+ ['conflict', ConflictException],
106
+ ['invalid_input', BadRequestException],
107
+ ] as const)(
108
+ 'maps retry-node %s outcomes at the HTTP boundary',
109
+ async (kind, ExceptionType) => {
110
+ const temporalService = {};
111
+ const workflowService = {
112
+ retryFailedNode: jest.fn().mockResolvedValue({
113
+ kind,
114
+ message: 'retry rejected',
115
+ }),
116
+ };
117
+ const controller = new WorkflowController(
118
+ workflowService as never,
119
+ temporalService as never,
120
+ {} as never,
121
+ {} as never,
122
+ {} as never,
123
+ );
124
+
125
+ await expect(
126
+ controller.retryFailedNode(42, { nodeId: 'request' }),
127
+ ).rejects.toBeInstanceOf(ExceptionType);
128
+ expect(workflowService.retryFailedNode).toHaveBeenCalledWith(
129
+ BigInt(42),
130
+ 'request',
131
+ temporalService,
132
+ );
133
+ },
134
+ );
97
135
  });
@@ -1,11 +1,14 @@
1
1
  import { ACGuard } from '@gadmin2n/nest-common';
2
2
  import {
3
+ BadRequestException,
3
4
  Body,
5
+ ConflictException,
4
6
  Controller,
5
7
  Delete,
6
8
  Get,
7
9
  Headers,
8
10
  HttpCode,
11
+ NotFoundException,
9
12
  Param,
10
13
  ParseIntPipe,
11
14
  Patch,
@@ -26,7 +29,7 @@ import { listWorkflowDatabaseFunctions } from './workflow-database-function';
26
29
  import {
27
30
  ApproveInstanceDto,
28
31
  ExecuteWorkflowDto,
29
- RetryFromNodeDto,
32
+ RetryFailedNodeDto,
30
33
  } from './workflow-execution.dto';
31
34
  import { ImportPayloadDto } from './workflow-export.dto';
32
35
  import { WorkflowExportService } from './workflow-export.service';
@@ -431,23 +434,31 @@ export class WorkflowController {
431
434
  );
432
435
  }
433
436
 
434
- @Post('instance/:instanceId/retry-from-node')
437
+ @Post('instance/:instanceId/retry-failed-node')
435
438
  @UseRoles({
436
439
  resource: 'workflowExecution',
437
440
  action: 'update',
438
441
  possession: 'any',
439
442
  })
440
- async retryInstanceFromNode(
443
+ async retryFailedNode(
441
444
  @Param('instanceId', ParseIntPipe) instanceId: number,
442
- @Body() dto: RetryFromNodeDto,
443
- @Req() req,
445
+ @Body() dto: RetryFailedNodeDto,
444
446
  ) {
445
- return this.workflowService.retryInstanceFromNode(
447
+ const outcome = await this.workflowService.retryFailedNode(
446
448
  BigInt(instanceId),
447
449
  dto.nodeId,
448
- req.user.userid,
449
450
  this.temporalService,
450
451
  );
452
+ switch (outcome.kind) {
453
+ case 'ok':
454
+ return outcome.value;
455
+ case 'not_found':
456
+ throw new NotFoundException(outcome.message);
457
+ case 'conflict':
458
+ throw new ConflictException(outcome.message);
459
+ case 'invalid_input':
460
+ throw new BadRequestException(outcome.message);
461
+ }
451
462
  }
452
463
 
453
464
  @Post('instance/:instanceId/approve')
@@ -61,6 +61,7 @@ export enum WorkflowStatusEnum {
61
61
  export enum InstanceStatusEnum {
62
62
  PENDING = 'PENDING',
63
63
  RUNNING = 'RUNNING',
64
+ WAITING_RETRY = 'WAITING_RETRY',
64
65
  COMPLETED = 'COMPLETED',
65
66
  FAILED = 'FAILED',
66
67
  CANCELLED = 'CANCELLED',