@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.
- package/dist/lib/application/files/gadmin2-game-angle-demo/.git-blame-ignore-revs +5 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/.nvmrc +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.local.md +8 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.md +4 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.codegen +16 -9
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.server +21 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.web +11 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/Jenkinsfile +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/PRODUCT.md +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/compose-ctl.sh +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.yml +4 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/readme.md +22 -22
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/.oxfmtrc.json +13 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/.oxlintrc.json +17 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/.seed-timestamp +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +14 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/pnpm-workspace.yaml +23 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/index.ts +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +23 -44
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +25 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +74 -64
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/auth.guard.ts +4 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/tracing.ts +4 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.service.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.controller.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.ts +6 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.controller.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/httpRequest/http-request-runtime.controller.ts +6 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/httpRequest/http-request-runtime.util.ts +4 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.ts +4 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.controller.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/page-transfer.service.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.controller.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.controller.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.spec.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/subject-provider.errors.ts +4 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/subject-provider.registry.ts +4 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/subject-provider.types.ts +4 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/parameter-binding.ts +4 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.ts +0 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +17 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +58 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.ts +66 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-data-edge.util.ts +5 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution-idempotency.spec.ts +61 -149
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-handle-validator.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.spec.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.spec.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +38 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +18 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.dto.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +142 -253
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.ts +3 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.controller.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.controller.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/start-prod.sh +4 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/.oxfmtrc.json +12 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/.oxlintrc.json +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/README.MD +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/nginx.conf +0 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +17 -17
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/pnpm-workspace.yaml +13 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/VanillaJSONEditor/index.jsx +4 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/propPatcher.ts +36 -8
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +7 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +6 -36
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/style.css +24 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +0 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +29 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +29 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/plugins/devShellPlugin.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/components/action-cell.css +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +20 -17
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +38 -35
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.tsx +6 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.tsx +24 -22
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/EnhancedFlowRenderer.tsx +0 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/StatusCards.tsx +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +20 -18
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +7 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +28 -79
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instances.tsx +5 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/NodeInstanceForm.tsx +0 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +227 -97
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +62 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/settings/index.tsx +73 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +21 -46
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/newNodeConfig.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/index.css +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/test/setup.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/vite.config.ts +53 -41
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/vitest.config.ts +8 -2
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/.eslintrc.js +0 -28
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/.prettierignore +0 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/.prettierrc +0 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/.eslintrc.json +0 -38
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/.prettierignore +0 -2
- 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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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>;
|
package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
65
|
+
? 'workflow'
|
|
66
|
+
: 'approval';
|
|
67
67
|
const root =
|
|
68
68
|
binding.source === 'INPUT'
|
|
69
69
|
? input
|
|
70
70
|
: binding.source === 'WORKFLOW'
|
|
71
|
-
|
|
72
|
-
|
|
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('
|
|
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: '
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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('
|
|
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.
|
|
378
|
+
service.retryFailedNode(BigInt(22), 'request', temporal),
|
|
386
379
|
).resolves.toEqual({
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
workflowVersionId: '11',
|
|
390
|
-
status: 'COMPLETED',
|
|
380
|
+
kind: 'conflict',
|
|
381
|
+
message: '只有等待重试的 Workflow 才能重试失败节点',
|
|
391
382
|
});
|
|
392
|
-
expect(
|
|
383
|
+
expect(temporal.signalWorkflow).not.toHaveBeenCalled();
|
|
393
384
|
});
|
|
394
385
|
|
|
395
|
-
it('
|
|
386
|
+
it('signals the same Temporal run when a waiting node is retried', async () => {
|
|
396
387
|
const { prisma, temporal, service } = setup();
|
|
397
|
-
const
|
|
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: '
|
|
406
|
-
|
|
407
|
-
|
|
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: '
|
|
414
|
-
status: '
|
|
415
|
-
output:
|
|
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: '
|
|
424
|
-
{ id: '
|
|
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: '
|
|
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: '
|
|
431
|
-
target: '
|
|
432
|
-
|
|
433
|
-
|
|
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(
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
457
|
-
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
expect(temporal.signalWorkflow).toHaveBeenCalledWith(
|
|
444
|
+
'wf-7-22',
|
|
445
|
+
'retryNode',
|
|
458
446
|
{
|
|
459
|
-
|
|
460
|
-
|
|
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: '
|
|
460
|
+
status: 'WAITING_RETRY',
|
|
461
|
+
temporalRunId: 'wf-7-22',
|
|
478
462
|
context: { workflow: {}, nodes: {} },
|
|
479
463
|
nodeExecutions: [
|
|
480
|
-
{
|
|
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.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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',
|
|
@@ -94,8 +94,8 @@ export function validateWorkflowHandles(
|
|
|
94
94
|
const handles = runtimeOutput
|
|
95
95
|
? [runtimeOutput]
|
|
96
96
|
: declaredOutputs === null
|
|
97
|
-
|
|
98
|
-
|
|
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: {
|