@gadmin2n/schematics 0.0.152 → 0.0.154

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 (110) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/.git-blame-ignore-revs +5 -0
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/.nvmrc +1 -0
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.local.md +8 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.md +4 -4
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.codegen +16 -9
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.server +21 -13
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.web +11 -11
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/Jenkinsfile +1 -1
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/PRODUCT.md +1 -1
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/compose-ctl.sh +1 -1
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +1 -0
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.yml +4 -4
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/readme.md +22 -22
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.oxfmtrc.json +13 -0
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.oxlintrc.json +17 -0
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.seed-timestamp +1 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +14 -13
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/pnpm-workspace.yaml +23 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/index.ts +2 -1
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +23 -44
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +25 -4
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +74 -64
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/auth.guard.ts +4 -1
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.ts +2 -2
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/tracing.ts +4 -6
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.service.ts +2 -2
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.controller.ts +1 -1
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.ts +6 -6
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.controller.ts +1 -1
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/httpRequest/http-request-runtime.controller.ts +6 -6
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/httpRequest/http-request-runtime.util.ts +4 -4
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.ts +1 -1
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.ts +4 -4
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.ts +1 -1
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.ts +2 -2
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.controller.ts +1 -1
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/page-transfer.service.ts +1 -1
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.ts +1 -1
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.controller.ts +1 -1
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.controller.ts +1 -1
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.spec.ts +1 -1
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/subject-provider.errors.ts +4 -1
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/subject-provider.registry.ts +4 -4
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/subject-provider.types.ts +4 -4
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +1 -1
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts +2 -2
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/parameter-binding.ts +4 -4
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.ts +0 -7
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +17 -0
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +58 -3
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.ts +66 -6
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-data-edge.util.ts +5 -5
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution-idempotency.spec.ts +61 -149
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +1 -1
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-handle-validator.ts +2 -2
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.spec.ts +1 -1
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.ts +1 -1
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.spec.ts +2 -2
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +38 -0
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +18 -7
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.dto.ts +1 -0
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +142 -253
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.ts +3 -1
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.controller.ts +1 -1
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.controller.ts +1 -1
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/server/start-prod.sh +4 -4
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/web/.oxfmtrc.json +12 -0
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/web/.oxlintrc.json +24 -0
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/web/README.MD +2 -2
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/web/nginx.conf +0 -4
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +17 -17
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/web/pnpm-workspace.yaml +13 -0
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/VanillaJSONEditor/index.jsx +4 -4
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/propPatcher.ts +36 -8
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +7 -5
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +6 -36
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/style.css +24 -7
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +0 -4
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +29 -7
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +29 -7
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/plugins/devShellPlugin.ts +2 -2
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/components/action-cell.css +1 -1
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +20 -17
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +38 -35
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.tsx +6 -1
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.tsx +24 -22
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/EnhancedFlowRenderer.tsx +0 -11
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/StatusCards.tsx +24 -0
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +20 -18
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +7 -0
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +28 -79
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instances.tsx +5 -0
  93. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/NodeInstanceForm.tsx +0 -1
  94. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +227 -97
  95. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +62 -0
  96. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/settings/index.tsx +73 -0
  97. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +21 -46
  98. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +2 -0
  99. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/newNodeConfig.ts +1 -0
  100. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/index.css +1 -1
  101. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/test/setup.ts +1 -1
  102. package/dist/lib/application/files/gadmin2-game-angle-demo/web/vite.config.ts +53 -41
  103. package/dist/lib/application/files/gadmin2-game-angle-demo/web/vitest.config.ts +8 -2
  104. package/package.json +1 -1
  105. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.eslintrc.js +0 -28
  106. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.prettierignore +0 -7
  107. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.prettierrc +0 -4
  108. package/dist/lib/application/files/gadmin2-game-angle-demo/web/.eslintrc.json +0 -38
  109. package/dist/lib/application/files/gadmin2-game-angle-demo/web/.prettierignore +0 -2
  110. package/dist/lib/application/files/gadmin2-game-angle-demo/web/.prettierrc +0 -4
@@ -88,10 +88,10 @@ export interface SubjectProvider {
88
88
  export type DeepReadonly<T> = T extends (...args: any[]) => unknown
89
89
  ? T
90
90
  : T extends readonly (infer U)[]
91
- ? readonly DeepReadonly<U>[]
92
- : T extends object
93
- ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
94
- : T;
91
+ ? readonly DeepReadonly<U>[]
92
+ : T extends object
93
+ ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
94
+ : T;
95
95
 
96
96
  export type FrozenSubjectProviderDescriptor =
97
97
  DeepReadonly<SubjectProviderDescriptor>;
@@ -145,7 +145,7 @@ export class UserController {
145
145
  @Body() body: UpdateUserDto,
146
146
  @ACAttrFilter() acAttrFilter,
147
147
  ) {
148
- /*
148
+ /*
149
149
  const origin = (await this.userService.findUnique(id, {
150
150
  id: true,
151
151
  // 这里需要根据实际情况填写
@@ -224,8 +224,8 @@ function assignedRoles(value: string): string[] {
224
224
  return Array.isArray(parsed)
225
225
  ? parsed.map(String).filter((role) => role.trim())
226
226
  : value.trim()
227
- ? [value]
228
- : [];
227
+ ? [value]
228
+ : [];
229
229
  } catch {
230
230
  return value.trim() ? [value] : [];
231
231
  }
@@ -62,14 +62,14 @@ export function resolveParameterBinding(
62
62
  binding.source === 'INPUT'
63
63
  ? 'input'
64
64
  : binding.source === 'WORKFLOW'
65
- ? 'workflow'
66
- : 'approval';
65
+ ? 'workflow'
66
+ : 'approval';
67
67
  const root =
68
68
  binding.source === 'INPUT'
69
69
  ? input
70
70
  : binding.source === 'WORKFLOW'
71
- ? workflow
72
- : approval;
71
+ ? workflow
72
+ : approval;
73
73
  const value = readPath(root, binding.path);
74
74
  if (value === undefined)
75
75
  throw new Error(
@@ -36,12 +36,6 @@ export type CronScheduleStatus =
36
36
  | 'MISSING'
37
37
  | 'UNAVAILABLE';
38
38
 
39
- export interface WorkflowRetryCheckpoint {
40
- startNodeId: string;
41
- completedNodeIds: string[];
42
- outputs: Record<string, unknown>;
43
- }
44
-
45
39
  @Injectable()
46
40
  export class TemporalService implements OnModuleInit, OnModuleDestroy {
47
41
  private connection: Connection;
@@ -82,7 +76,6 @@ export class TemporalService implements OnModuleInit, OnModuleDestroy {
82
76
  versionId: number;
83
77
  dsl: any;
84
78
  context: Record<string, any>;
85
- retryCheckpoint?: WorkflowRetryCheckpoint;
86
79
  }): Promise<{ temporalRunId: string }> {
87
80
  if (!this.client) {
88
81
  throw new Error('Temporal client not connected');
@@ -167,6 +167,23 @@ describe('Workflow cancellation', () => {
167
167
  expect(temporal.cancelWorkflow).not.toHaveBeenCalled();
168
168
  });
169
169
 
170
+ it('allows cancellation while the Workflow is waiting for a node retry', async () => {
171
+ const { runtime, tx } = setup({ status: 'WAITING_RETRY' });
172
+
173
+ await expect(
174
+ runtime.cancelExecution({
175
+ tx: tx as any,
176
+ instanceId: BigInt(11),
177
+ requestedBy: 'admin-user',
178
+ }),
179
+ ).resolves.toEqual({ instanceId: '11', status: 'CANCELLED' });
180
+ expect(tx.workflowInstance.update).toHaveBeenCalledWith(
181
+ expect.objectContaining({
182
+ data: expect.objectContaining({ status: 'CANCELLED' }),
183
+ }),
184
+ );
185
+ });
186
+
170
187
  it('rejects cancellation after a blocking approval has resolved', async () => {
171
188
  const { runtime, temporal, tx } = setup({
172
189
  cancellationBlockedAt: new Date('2026-08-20T01:00:00.000Z'),
@@ -949,7 +949,7 @@ describe('WorkflowCompilerService', () => {
949
949
  ).resolves.toBeUndefined();
950
950
  });
951
951
 
952
- it('rejects legacy raw SQL database configuration', async () => {
952
+ it('accepts a safe Raw SQL database configuration', async () => {
953
953
  const prisma = {
954
954
  workflowNodeType: {
955
955
  findMany: jest.fn().mockResolvedValue([
@@ -982,7 +982,62 @@ describe('WorkflowCompilerService', () => {
982
982
  type: 'db_execute',
983
983
  config: {
984
984
  allowWrite: true,
985
- sql: 'UPDATE users SET active = false',
985
+ sql: 'UPDATE users SET active = false WHERE id = $1',
986
+ parameters: [{ source: 'INPUT', path: 'id' }],
987
+ },
988
+ },
989
+ ],
990
+ edges: [],
991
+ },
992
+ { userId: 'author' },
993
+ ),
994
+ ).resolves.toMatchObject({
995
+ nodes: [
996
+ expect.objectContaining({
997
+ id: 'database-1',
998
+ config: expect.objectContaining({
999
+ sql: 'UPDATE users SET active = false WHERE id = $1',
1000
+ }),
1001
+ }),
1002
+ ],
1003
+ });
1004
+ });
1005
+
1006
+ it('rejects unsafe Raw SQL database configuration', async () => {
1007
+ const prisma = {
1008
+ workflowNodeType: {
1009
+ findMany: jest.fn().mockResolvedValue([
1010
+ {
1011
+ id: BigInt(10),
1012
+ type: 'db_execute',
1013
+ runtimeKey: 'db_execute',
1014
+ instancePolicy: 'INLINE_ONLY',
1015
+ schemaVersion: 6,
1016
+ configSchema: { type: 'object' },
1017
+ uiSchema: {},
1018
+ },
1019
+ ]),
1020
+ },
1021
+ workflowNodeInstance: { findMany: jest.fn().mockResolvedValue([]) },
1022
+ };
1023
+ const service = new WorkflowCompilerService(
1024
+ prisma as any,
1025
+ { validateOrThrow: jest.fn() } as any,
1026
+ { assertUiSchemaSupported: jest.fn() } as any,
1027
+ { validateSubjects: jest.fn() } as any,
1028
+ );
1029
+
1030
+ await expect(
1031
+ service.compile(
1032
+ {
1033
+ nodes: [
1034
+ {
1035
+ id: 'database-1',
1036
+ type: 'db_execute',
1037
+ config: {
1038
+ mode: 'SQL',
1039
+ allowWrite: true,
1040
+ sql: 'DROP TABLE users',
986
1041
  },
987
1042
  },
988
1043
  ],
@@ -992,7 +1047,7 @@ describe('WorkflowCompilerService', () => {
992
1047
  ),
993
1048
  ).rejects.toMatchObject({
994
1049
  response: {
995
- message: '数据库节点不再支持 SQL,请选择 workflow_api 中的 Function',
1050
+ message: '数据库操作只允许单条查询、新增、更新或删除语句',
996
1051
  nodeId: 'database-1',
997
1052
  },
998
1053
  });
@@ -240,12 +240,27 @@ export class WorkflowCompilerService {
240
240
  }
241
241
  if (nodeType.runtimeKey === 'db_execute') {
242
242
  const databaseConfig = config as Record<string, unknown>;
243
- if ('sql' in databaseConfig || 'allowWrite' in databaseConfig)
244
- this.fail(
245
- 'NODE_TYPE_MISMATCH',
246
- node,
247
- '数据库节点不再支持 SQL,请选择 workflow_api 中的 Function',
248
- );
243
+ const databaseMode = databaseExecutionMode(databaseConfig);
244
+ if (!databaseMode)
245
+ this.fail('NODE_TYPE_MISMATCH', node, '数据库节点的执行方式无效');
246
+ if (databaseMode === 'SQL') {
247
+ const allowWrite = databaseConfig.allowWrite === true;
248
+ const sql = String(databaseConfig.sql ?? '');
249
+ if (!isDatabaseSqlAllowed(sql, allowWrite))
250
+ this.fail(
251
+ 'NODE_TYPE_MISMATCH',
252
+ node,
253
+ allowWrite
254
+ ? '数据库操作只允许单条查询、新增、更新或删除语句'
255
+ : '当前数据库操作节点只允许单条只读查询',
256
+ );
257
+ compiledNodes.push({
258
+ ...clone(node),
259
+ config,
260
+ runtimeKey: nodeType.runtimeKey || nodeType.type,
261
+ });
262
+ continue;
263
+ }
249
264
  const functionRef = asObject(databaseConfig.functionRef);
250
265
  const argumentBindings = Array.isArray(databaseConfig.argumentBindings)
251
266
  ? databaseConfig.argumentBindings
@@ -503,6 +518,51 @@ function clone<T>(value: T): T {
503
518
  return JSON.parse(JSON.stringify(value)) as T;
504
519
  }
505
520
 
521
+ function databaseExecutionMode(
522
+ config: Record<string, unknown>,
523
+ ): 'FUNCTION' | 'SQL' | null {
524
+ if (config.mode === 'FUNCTION' || config.mode === 'SQL') return config.mode;
525
+ if (config.mode !== undefined) return null;
526
+ return 'sql' in config || 'allowWrite' in config || 'parameters' in config
527
+ ? 'SQL'
528
+ : 'FUNCTION';
529
+ }
530
+
531
+ function isSingleReadOnlySql(sql: string): boolean {
532
+ const normalized = normalizeSqlForPolicy(sql);
533
+ if (!/^(select|with)\b/i.test(normalized)) return false;
534
+ return !/\b(insert|update|delete|merge|call|copy|create|alter|drop|truncate|grant|revoke|vacuum|refresh)\b/i.test(
535
+ normalized,
536
+ );
537
+ }
538
+
539
+ function isDatabaseSqlAllowed(sql: string, allowWrite: boolean): boolean {
540
+ const normalized = normalizeSqlForPolicy(sql);
541
+ const writableCte =
542
+ /^with\b/i.test(normalized) &&
543
+ /\b(insert|update|delete)\b/i.test(normalized) &&
544
+ !/\b(merge|call|copy|create|alter|drop|truncate|grant|revoke|vacuum|refresh)\b/i.test(
545
+ normalized,
546
+ );
547
+ return (
548
+ isSingleReadOnlySql(sql) ||
549
+ (allowWrite &&
550
+ (/^(insert|update|delete)\b/i.test(normalized) || writableCte))
551
+ );
552
+ }
553
+
554
+ function normalizeSqlForPolicy(sql: string): string {
555
+ const withoutContent = sql
556
+ .replace(/\$(\w*)\$[\s\S]*?\$\1\$/g, ' ')
557
+ .replace(/'(?:''|[^'])*'/g, ' ')
558
+ .replace(/"(?:""|[^"])*"/g, ' ')
559
+ .replace(/\/\*[\s\S]*?\*\//g, ' ')
560
+ .replace(/--[^\r\n]*/g, ' ')
561
+ .trim();
562
+ const withoutTrailingSemicolon = withoutContent.replace(/;\s*$/, '').trim();
563
+ return withoutTrailingSemicolon.includes(';') ? '' : withoutTrailingSemicolon;
564
+ }
565
+
506
566
  function asObject(value: unknown): Record<string, unknown> {
507
567
  return value && typeof value === 'object' && !Array.isArray(value)
508
568
  ? (value as Record<string, unknown>)
@@ -396,11 +396,11 @@ function parallelBranches(
396
396
  (item): item is { id: string; label: string } =>
397
397
  Boolean(
398
398
  item &&
399
- typeof item === 'object' &&
400
- typeof item.id === 'string' &&
401
- item.id.length > 0 &&
402
- typeof item.label === 'string' &&
403
- item.label.length > 0,
399
+ typeof item === 'object' &&
400
+ typeof item.id === 'string' &&
401
+ item.id.length > 0 &&
402
+ typeof item.label === 'string' &&
403
+ item.label.length > 0,
404
404
  ),
405
405
  );
406
406
  }
@@ -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;
@@ -94,8 +94,8 @@ export function validateWorkflowHandles(
94
94
  const handles = runtimeOutput
95
95
  ? [runtimeOutput]
96
96
  : declaredOutputs === null
97
- ? deriveDynamicOutputs(sourceNode)
98
- : declaredOutputs;
97
+ ? deriveDynamicOutputs(sourceNode)
98
+ : declaredOutputs;
99
99
 
100
100
  if (!runtimeOutput && declaredOutputs !== null && handles.length === 0) {
101
101
  if (edge.sourceHandle != null && edge.sourceHandle !== '') {
@@ -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: {