@gadmin2n/schematics 0.0.150 → 0.0.151
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/CODEBUDDY.md +125 -36
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +42 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/gitignore +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/.seed-timestamp +0 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/check-dev-env.js +218 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/page-manage.ts +78 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +37 -67
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +0 -17
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.controller.ts +46 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +60 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-callback.controller.ts +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/workflow-callback-url.controller.ts +9 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.controller.ts +55 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.module.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/page-transfer.service.ts +491 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/perm-transfer.dto.ts +213 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/perm-transfer.module.ts +28 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/resource-transfer.service.ts +137 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/role-transfer.service.ts +248 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/transfer-token.service.ts +65 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/user-transfer.service.ts +146 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.ts +55 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.module.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.controller.ts +55 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.module.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user-group.provider.spec.ts +46 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user-group.provider.ts +3 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +55 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.module.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +96 -14
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.ts +41 -41
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-database-function.spec.ts +36 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-database-function.ts +90 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-catalog.spec.ts +4 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-trigger.pg.spec.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution-idempotency.spec.ts +115 -63
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-outbox.service.spec.ts +166 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-outbox.service.ts +217 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.spec.ts +33 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.ts +57 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.spec.ts +196 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.ts +125 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-node-type.controller.ts +9 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +148 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.module.ts +12 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +14 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +75 -45
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +25 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/TransferExportModal.tsx +115 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/TransferImportModal.tsx +257 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +0 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +40 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +15 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +40 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +15 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/plugins/devShellPlugin.ts +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agendaJob/list.tsx +89 -65
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/list.tsx +51 -127
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/permissionReadme/index.tsx +53 -109
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/list.tsx +45 -41
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/list.tsx +51 -72
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +49 -40
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +16 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ReadonlyNodePanel.test.tsx +28 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +60 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +94 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +13 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/schema-form/default-widgets.tsx +3 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +8 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +167 -94
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +95 -95
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerLifecycleConfigEditor.test.tsx +81 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerLifecycleConfigEditor.tsx +126 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +48 -52
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/forEachConnection.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/newNodeConfig.ts +1 -3
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/ac.config.ts +0 -24
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/SqlModal.tsx +0 -495
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
DraftWorkflowNode,
|
|
21
21
|
WorkflowCompilerErrorCode,
|
|
22
22
|
} from './workflow-compiler.types';
|
|
23
|
+
import {
|
|
24
|
+
listWorkflowDatabaseFunctions,
|
|
25
|
+
type WorkflowDatabaseFunctionOption,
|
|
26
|
+
} from './workflow-database-function';
|
|
23
27
|
import { switchCaseValidationError } from './workflow-switch-config';
|
|
24
28
|
|
|
25
29
|
@Injectable()
|
|
@@ -85,6 +89,9 @@ export class WorkflowCompilerService {
|
|
|
85
89
|
);
|
|
86
90
|
|
|
87
91
|
const compiledNodes: CompiledWorkflowNode[] = [];
|
|
92
|
+
let databaseFunctionsPromise:
|
|
93
|
+
| Promise<WorkflowDatabaseFunctionOption[]>
|
|
94
|
+
| undefined;
|
|
88
95
|
for (const node of draftDsl.nodes) {
|
|
89
96
|
const nodeType = typeByName.get(node.type);
|
|
90
97
|
if (!nodeType)
|
|
@@ -192,6 +199,15 @@ export class WorkflowCompilerService {
|
|
|
192
199
|
'HTTP Request Connection',
|
|
193
200
|
);
|
|
194
201
|
}
|
|
202
|
+
if (nodeType.runtimeKey === 'event_trigger') {
|
|
203
|
+
const failureCallback = asObject(asObject(config).failureCallback);
|
|
204
|
+
if (Object.keys(failureCallback).length > 0)
|
|
205
|
+
await this.validateIntegrationConnection(
|
|
206
|
+
node,
|
|
207
|
+
failureCallback.connectionRef,
|
|
208
|
+
'业务失败通知连接',
|
|
209
|
+
);
|
|
210
|
+
}
|
|
195
211
|
if (nodeType.runtimeKey === 'human_approval') {
|
|
196
212
|
const approvalConfig = asObject(config);
|
|
197
213
|
if (approvalConfig.external) {
|
|
@@ -224,15 +240,34 @@ export class WorkflowCompilerService {
|
|
|
224
240
|
}
|
|
225
241
|
if (nodeType.runtimeKey === 'db_execute') {
|
|
226
242
|
const databaseConfig = config as Record<string, unknown>;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
243
|
+
if ('sql' in databaseConfig || 'allowWrite' in databaseConfig)
|
|
244
|
+
this.fail(
|
|
245
|
+
'NODE_TYPE_MISMATCH',
|
|
246
|
+
node,
|
|
247
|
+
'数据库节点不再支持 SQL,请选择 workflow_api 中的 Function',
|
|
248
|
+
);
|
|
249
|
+
const functionRef = asObject(databaseConfig.functionRef);
|
|
250
|
+
const argumentBindings = Array.isArray(databaseConfig.argumentBindings)
|
|
251
|
+
? databaseConfig.argumentBindings
|
|
252
|
+
: [];
|
|
253
|
+
databaseFunctionsPromise ??= listWorkflowDatabaseFunctions(this.prisma);
|
|
254
|
+
const databaseFunction = (await databaseFunctionsPromise).find(
|
|
255
|
+
(item) =>
|
|
256
|
+
item.schema === functionRef.schema &&
|
|
257
|
+
item.name === functionRef.name &&
|
|
258
|
+
item.identityArguments === functionRef.identityArguments,
|
|
259
|
+
);
|
|
260
|
+
if (!databaseFunction)
|
|
261
|
+
this.fail(
|
|
262
|
+
'NODE_TYPE_MISMATCH',
|
|
263
|
+
node,
|
|
264
|
+
'选择的数据库 Function 不存在或已不再开放给 Workflow',
|
|
265
|
+
);
|
|
266
|
+
if (argumentBindings.length !== databaseFunction.arguments.length)
|
|
230
267
|
this.fail(
|
|
231
268
|
'NODE_TYPE_MISMATCH',
|
|
232
269
|
node,
|
|
233
|
-
|
|
234
|
-
? '数据库操作只允许单条查询、新增、更新或删除语句'
|
|
235
|
-
: '当前数据库操作节点只允许单条只读查询',
|
|
270
|
+
`数据库 Function 需要 ${databaseFunction.arguments.length} 个参数`,
|
|
236
271
|
);
|
|
237
272
|
}
|
|
238
273
|
if (nodeType.runtimeKey === 'sub_workflow') {
|
|
@@ -468,41 +503,6 @@ function clone<T>(value: T): T {
|
|
|
468
503
|
return JSON.parse(JSON.stringify(value)) as T;
|
|
469
504
|
}
|
|
470
505
|
|
|
471
|
-
function isSingleReadOnlySql(sql: string): boolean {
|
|
472
|
-
const normalized = normalizeSqlForPolicy(sql);
|
|
473
|
-
if (!/^(select|with)\b/i.test(normalized)) return false;
|
|
474
|
-
return !/\b(insert|update|delete|merge|call|copy|create|alter|drop|truncate|grant|revoke|vacuum|refresh)\b/i.test(
|
|
475
|
-
normalized,
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function isDatabaseSqlAllowed(sql: string, allowWrite: boolean): boolean {
|
|
480
|
-
const normalized = normalizeSqlForPolicy(sql);
|
|
481
|
-
const writableCte =
|
|
482
|
-
/^with\b/i.test(normalized) &&
|
|
483
|
-
/\b(insert|update|delete)\b/i.test(normalized) &&
|
|
484
|
-
!/\b(merge|call|copy|create|alter|drop|truncate|grant|revoke|vacuum|refresh)\b/i.test(
|
|
485
|
-
normalized,
|
|
486
|
-
);
|
|
487
|
-
return (
|
|
488
|
-
isSingleReadOnlySql(sql) ||
|
|
489
|
-
(allowWrite &&
|
|
490
|
-
(/^(insert|update|delete)\b/i.test(normalized) || writableCte))
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function normalizeSqlForPolicy(sql: string): string {
|
|
495
|
-
const withoutContent = sql
|
|
496
|
-
.replace(/\$(\w*)\$[\s\S]*?\$\1\$/g, ' ')
|
|
497
|
-
.replace(/'(?:''|[^'])*'/g, ' ')
|
|
498
|
-
.replace(/"(?:""|[^"])*"/g, ' ')
|
|
499
|
-
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
500
|
-
.replace(/--[^\r\n]*/g, ' ')
|
|
501
|
-
.trim();
|
|
502
|
-
const withoutTrailingSemicolon = withoutContent.replace(/;\s*$/, '').trim();
|
|
503
|
-
return withoutTrailingSemicolon.includes(';') ? '' : withoutTrailingSemicolon;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
506
|
function asObject(value: unknown): Record<string, unknown> {
|
|
507
507
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
508
508
|
? (value as Record<string, unknown>)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { listWorkflowDatabaseFunctions } from './workflow-database-function';
|
|
2
|
+
|
|
3
|
+
describe('workflow database Function catalog', () => {
|
|
4
|
+
it('returns scalar workflow_api Functions with ordered input arguments', async () => {
|
|
5
|
+
const prisma = {
|
|
6
|
+
$queryRaw: jest.fn().mockResolvedValue([
|
|
7
|
+
{
|
|
8
|
+
schema: 'workflow_api',
|
|
9
|
+
name: 'approve_request',
|
|
10
|
+
identity_arguments: 'p_request_id bigint, p_approved boolean',
|
|
11
|
+
return_type: 'jsonb',
|
|
12
|
+
arguments: [
|
|
13
|
+
{ position: 1, name: 'p_request_id', type: 'bigint' },
|
|
14
|
+
{ position: 2, name: 'p_approved', type: 'boolean' },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
]),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
await expect(listWorkflowDatabaseFunctions(prisma as any)).resolves.toEqual(
|
|
21
|
+
[
|
|
22
|
+
{
|
|
23
|
+
schema: 'workflow_api',
|
|
24
|
+
name: 'approve_request',
|
|
25
|
+
identityArguments: 'p_request_id bigint, p_approved boolean',
|
|
26
|
+
returnType: 'jsonb',
|
|
27
|
+
arguments: [
|
|
28
|
+
{ position: 1, name: 'p_request_id', type: 'bigint' },
|
|
29
|
+
{ position: 2, name: 'p_approved', type: 'boolean' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
);
|
|
34
|
+
expect(prisma.$queryRaw).toHaveBeenCalledTimes(1);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { PrismaService } from 'nestjs-prisma';
|
|
2
|
+
|
|
3
|
+
export const WORKFLOW_DATABASE_FUNCTION_SCHEMA = 'workflow_api';
|
|
4
|
+
|
|
5
|
+
export interface WorkflowDatabaseFunctionArgument {
|
|
6
|
+
position: number;
|
|
7
|
+
name: string;
|
|
8
|
+
type: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WorkflowDatabaseFunctionOption {
|
|
12
|
+
schema: string;
|
|
13
|
+
name: string;
|
|
14
|
+
identityArguments: string;
|
|
15
|
+
returnType: string;
|
|
16
|
+
arguments: WorkflowDatabaseFunctionArgument[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface WorkflowDatabaseFunctionRow {
|
|
20
|
+
schema: string;
|
|
21
|
+
name: string;
|
|
22
|
+
identity_arguments: string;
|
|
23
|
+
return_type: string;
|
|
24
|
+
arguments: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function listWorkflowDatabaseFunctions(
|
|
28
|
+
prisma: PrismaService,
|
|
29
|
+
): Promise<WorkflowDatabaseFunctionOption[]> {
|
|
30
|
+
const rows = await prisma.$queryRaw<WorkflowDatabaseFunctionRow[]>`
|
|
31
|
+
SELECT namespace.nspname AS schema,
|
|
32
|
+
proc.proname AS name,
|
|
33
|
+
pg_get_function_identity_arguments(proc.oid) AS identity_arguments,
|
|
34
|
+
pg_get_function_result(proc.oid) AS return_type,
|
|
35
|
+
COALESCE(
|
|
36
|
+
(
|
|
37
|
+
SELECT jsonb_agg(
|
|
38
|
+
jsonb_build_object(
|
|
39
|
+
'position', argument.position,
|
|
40
|
+
'name', COALESCE(
|
|
41
|
+
proc.proargnames[argument.position],
|
|
42
|
+
'arg_' || argument.position::text
|
|
43
|
+
),
|
|
44
|
+
'type', format_type(
|
|
45
|
+
proc.proargtypes[argument.position - 1],
|
|
46
|
+
NULL
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
ORDER BY argument.position
|
|
50
|
+
)
|
|
51
|
+
FROM generate_series(1, proc.pronargs) AS argument(position)
|
|
52
|
+
),
|
|
53
|
+
'[]'::jsonb
|
|
54
|
+
) AS arguments
|
|
55
|
+
FROM pg_proc proc
|
|
56
|
+
JOIN pg_namespace namespace ON namespace.oid = proc.pronamespace
|
|
57
|
+
WHERE namespace.nspname = 'workflow_api'
|
|
58
|
+
AND proc.prokind = 'f'
|
|
59
|
+
AND proc.proretset = FALSE
|
|
60
|
+
ORDER BY proc.proname, identity_arguments
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
return rows.map((row) => ({
|
|
64
|
+
schema: row.schema,
|
|
65
|
+
name: row.name,
|
|
66
|
+
identityArguments: row.identity_arguments,
|
|
67
|
+
returnType: row.return_type,
|
|
68
|
+
arguments: normalizeArguments(row.arguments),
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeArguments(
|
|
73
|
+
value: unknown,
|
|
74
|
+
): WorkflowDatabaseFunctionArgument[] {
|
|
75
|
+
if (!Array.isArray(value)) return [];
|
|
76
|
+
return value.map((item, index) => {
|
|
77
|
+
const argument = asObject(item);
|
|
78
|
+
return {
|
|
79
|
+
position: Number(argument.position ?? index + 1),
|
|
80
|
+
name: String(argument.name ?? `arg_${index + 1}`),
|
|
81
|
+
type: String(argument.type ?? ''),
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function asObject(value: unknown): Record<string, unknown> {
|
|
87
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
88
|
+
? (value as Record<string, unknown>)
|
|
89
|
+
: {};
|
|
90
|
+
}
|
|
@@ -179,6 +179,7 @@ describe('PostgreSQL business event registration', () => {
|
|
|
179
179
|
{} as any,
|
|
180
180
|
{} as any,
|
|
181
181
|
{} as any,
|
|
182
|
+
{} as any,
|
|
182
183
|
);
|
|
183
184
|
|
|
184
185
|
await expect(
|
|
@@ -224,6 +225,7 @@ describe('PostgreSQL business event registration', () => {
|
|
|
224
225
|
{} as any,
|
|
225
226
|
{} as any,
|
|
226
227
|
{} as any,
|
|
228
|
+
{} as any,
|
|
227
229
|
);
|
|
228
230
|
|
|
229
231
|
await expect(
|
|
@@ -267,6 +269,7 @@ describe('PostgreSQL business event registration', () => {
|
|
|
267
269
|
{} as any,
|
|
268
270
|
{} as any,
|
|
269
271
|
{} as any,
|
|
272
|
+
{} as any,
|
|
270
273
|
);
|
|
271
274
|
|
|
272
275
|
await service.registerBusinessEvent(
|
|
@@ -312,6 +315,7 @@ describe('PostgreSQL business event registration', () => {
|
|
|
312
315
|
{} as any,
|
|
313
316
|
{} as any,
|
|
314
317
|
{} as any,
|
|
318
|
+
{} as any,
|
|
315
319
|
);
|
|
316
320
|
|
|
317
321
|
await expect(
|
|
@@ -32,6 +32,9 @@ describe('Workflow execution idempotency', () => {
|
|
|
32
32
|
update: jest.fn(),
|
|
33
33
|
updateMany: jest.fn().mockResolvedValue({ count: 0 }),
|
|
34
34
|
},
|
|
35
|
+
workflowEventDelivery: {
|
|
36
|
+
updateMany: jest.fn().mockResolvedValue({ count: 0 }),
|
|
37
|
+
},
|
|
35
38
|
};
|
|
36
39
|
prisma.$transaction = jest.fn(async (callback: any) =>
|
|
37
40
|
callback({
|
|
@@ -46,8 +49,17 @@ describe('Workflow execution idempotency', () => {
|
|
|
46
49
|
cancelWorkflow: jest.fn().mockResolvedValue(undefined),
|
|
47
50
|
inspectWorkflow: jest.fn(),
|
|
48
51
|
};
|
|
49
|
-
const
|
|
50
|
-
|
|
52
|
+
const lifecycle: any = {
|
|
53
|
+
markFailed: jest.fn().mockResolvedValue(true),
|
|
54
|
+
};
|
|
55
|
+
const service = new WorkflowService(
|
|
56
|
+
prisma,
|
|
57
|
+
temporal,
|
|
58
|
+
{} as any,
|
|
59
|
+
{} as any,
|
|
60
|
+
lifecycle,
|
|
61
|
+
);
|
|
62
|
+
return { prisma, temporal, lifecycle, service };
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
it('persists the key and starts one deterministic Temporal execution', async () => {
|
|
@@ -95,7 +107,7 @@ describe('Workflow execution idempotency', () => {
|
|
|
95
107
|
});
|
|
96
108
|
|
|
97
109
|
it('rechecks enabled state while holding the Workflow row lock', async () => {
|
|
98
|
-
const { prisma, temporal, service } = setup();
|
|
110
|
+
const { prisma, temporal, lifecycle, service } = setup();
|
|
99
111
|
prisma.$transaction.mockImplementationOnce(async (callback: any) =>
|
|
100
112
|
callback({
|
|
101
113
|
$queryRaw: jest.fn().mockResolvedValue([{ isEnabled: false }]),
|
|
@@ -221,7 +233,7 @@ describe('Workflow execution idempotency', () => {
|
|
|
221
233
|
});
|
|
222
234
|
|
|
223
235
|
it('marks a reserved instance failed when Temporal cannot start it', async () => {
|
|
224
|
-
const { prisma, temporal, service } = setup();
|
|
236
|
+
const { prisma, temporal, lifecycle, service } = setup();
|
|
225
237
|
const created = {
|
|
226
238
|
id: BigInt(21),
|
|
227
239
|
workflowId,
|
|
@@ -238,14 +250,12 @@ describe('Workflow execution idempotency', () => {
|
|
|
238
250
|
service.executeWorkflow(workflowId, {}, 'manual', temporal),
|
|
239
251
|
).rejects.toThrow('Temporal unavailable');
|
|
240
252
|
|
|
241
|
-
expect(
|
|
253
|
+
expect(lifecycle.markFailed).toHaveBeenCalledWith(
|
|
242
254
|
expect.objectContaining({
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}),
|
|
248
|
-
data: expect.objectContaining({ status: 'FAILED' }),
|
|
255
|
+
instanceId: BigInt(21),
|
|
256
|
+
expectedStatuses: ['PENDING'],
|
|
257
|
+
temporalRunId: 'wf-7-21',
|
|
258
|
+
result: expect.objectContaining({ code: 'TEMPORAL_START_FAILED' }),
|
|
249
259
|
}),
|
|
250
260
|
);
|
|
251
261
|
});
|
|
@@ -588,7 +598,9 @@ describe('Workflow execution idempotency', () => {
|
|
|
588
598
|
context: { workflow: { orderId: 'O-1' }, nodes: {} },
|
|
589
599
|
temporalRunId: 'wf-7-21',
|
|
590
600
|
};
|
|
591
|
-
prisma.workflowInstance.findMany
|
|
601
|
+
prisma.workflowInstance.findMany
|
|
602
|
+
.mockResolvedValueOnce([])
|
|
603
|
+
.mockResolvedValueOnce([reserved]);
|
|
592
604
|
prisma.workflowVersion.findUnique.mockResolvedValueOnce({
|
|
593
605
|
dsl: version.dsl,
|
|
594
606
|
});
|
|
@@ -622,6 +634,46 @@ describe('Workflow execution idempotency', () => {
|
|
|
622
634
|
data: expect.objectContaining({ status: 'RUNNING' }),
|
|
623
635
|
}),
|
|
624
636
|
);
|
|
637
|
+
expect(prisma.workflowEventDelivery.updateMany).toHaveBeenCalledWith({
|
|
638
|
+
where: { workflowInstanceId: BigInt(21), status: 'FAILED' },
|
|
639
|
+
data: {
|
|
640
|
+
status: 'DISPATCHED',
|
|
641
|
+
dispatchedAt: expect.any(Date),
|
|
642
|
+
lastError: null,
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('fails through lifecycle handling when a missing Temporal execution cannot restart', async () => {
|
|
648
|
+
const { prisma, temporal, lifecycle, service } = setup();
|
|
649
|
+
const reserved = {
|
|
650
|
+
id: BigInt(21),
|
|
651
|
+
workflowId,
|
|
652
|
+
versionId: version.id,
|
|
653
|
+
status: 'PENDING',
|
|
654
|
+
context: { workflow: {}, nodes: {} },
|
|
655
|
+
temporalRunId: 'wf-7-21',
|
|
656
|
+
};
|
|
657
|
+
prisma.workflowInstance.findMany
|
|
658
|
+
.mockResolvedValueOnce([])
|
|
659
|
+
.mockResolvedValueOnce([reserved]);
|
|
660
|
+
prisma.workflowVersion.findUnique.mockResolvedValueOnce({
|
|
661
|
+
dsl: version.dsl,
|
|
662
|
+
});
|
|
663
|
+
temporal.inspectWorkflow.mockResolvedValueOnce({ kind: 'MISSING' });
|
|
664
|
+
temporal.startWorkflow.mockRejectedValueOnce(new Error('Temporal down'));
|
|
665
|
+
|
|
666
|
+
await service.reconcileUnstartedInstances();
|
|
667
|
+
|
|
668
|
+
expect(lifecycle.markFailed).toHaveBeenCalledWith({
|
|
669
|
+
instanceId: BigInt(21),
|
|
670
|
+
expectedStatuses: ['PENDING'],
|
|
671
|
+
temporalRunId: 'wf-7-21',
|
|
672
|
+
result: {
|
|
673
|
+
code: 'TEMPORAL_START_RECONCILIATION_FAILED',
|
|
674
|
+
message: 'Temporal down',
|
|
675
|
+
},
|
|
676
|
+
});
|
|
625
677
|
});
|
|
626
678
|
|
|
627
679
|
it('cancels a reconciled execution when cancellation wins the restart race', async () => {
|
|
@@ -634,7 +686,9 @@ describe('Workflow execution idempotency', () => {
|
|
|
634
686
|
context: { workflow: {}, nodes: {} },
|
|
635
687
|
temporalRunId: 'wf-7-21',
|
|
636
688
|
};
|
|
637
|
-
prisma.workflowInstance.findMany
|
|
689
|
+
prisma.workflowInstance.findMany
|
|
690
|
+
.mockResolvedValueOnce([])
|
|
691
|
+
.mockResolvedValueOnce([reserved]);
|
|
638
692
|
prisma.workflowVersion.findUnique.mockResolvedValueOnce({
|
|
639
693
|
dsl: version.dsl,
|
|
640
694
|
});
|
|
@@ -660,16 +714,18 @@ describe('Workflow execution idempotency', () => {
|
|
|
660
714
|
|
|
661
715
|
it('recovers a completed Temporal result without exposing internal context', async () => {
|
|
662
716
|
const { prisma, temporal, service } = setup();
|
|
663
|
-
prisma.workflowInstance.findMany
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
717
|
+
prisma.workflowInstance.findMany
|
|
718
|
+
.mockResolvedValueOnce([])
|
|
719
|
+
.mockResolvedValueOnce([
|
|
720
|
+
{
|
|
721
|
+
id: BigInt(21),
|
|
722
|
+
workflowId,
|
|
723
|
+
versionId: version.id,
|
|
724
|
+
status: 'PENDING',
|
|
725
|
+
context: { workflow: { orderId: 'O-1' }, nodes: {} },
|
|
726
|
+
temporalRunId: 'wf-7-21',
|
|
727
|
+
},
|
|
728
|
+
]);
|
|
673
729
|
prisma.workflowInstance.updateMany
|
|
674
730
|
.mockResolvedValueOnce({ count: 0 })
|
|
675
731
|
.mockResolvedValueOnce({ count: 1 });
|
|
@@ -704,17 +760,19 @@ describe('Workflow execution idempotency', () => {
|
|
|
704
760
|
});
|
|
705
761
|
|
|
706
762
|
it('reconciles a stale RUNNING instance after Temporal reaches a terminal state', async () => {
|
|
707
|
-
const { prisma, temporal, service } = setup();
|
|
708
|
-
prisma.workflowInstance.findMany
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
763
|
+
const { prisma, temporal, lifecycle, service } = setup();
|
|
764
|
+
prisma.workflowInstance.findMany
|
|
765
|
+
.mockResolvedValueOnce([])
|
|
766
|
+
.mockResolvedValueOnce([
|
|
767
|
+
{
|
|
768
|
+
id: BigInt(21),
|
|
769
|
+
workflowId,
|
|
770
|
+
versionId: version.id,
|
|
771
|
+
status: 'RUNNING',
|
|
772
|
+
context: { workflow: { orderId: 'O-1' }, nodes: {} },
|
|
773
|
+
temporalRunId: 'wf-7-21',
|
|
774
|
+
},
|
|
775
|
+
]);
|
|
718
776
|
temporal.inspectWorkflow.mockResolvedValueOnce({
|
|
719
777
|
kind: 'FOUND',
|
|
720
778
|
status: 'FAILED',
|
|
@@ -724,47 +782,41 @@ describe('Workflow execution idempotency', () => {
|
|
|
724
782
|
|
|
725
783
|
await service.reconcileUnstartedInstances();
|
|
726
784
|
|
|
727
|
-
expect(
|
|
785
|
+
expect(lifecycle.markFailed).toHaveBeenCalledWith(
|
|
728
786
|
expect.objectContaining({
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
temporalRunId: 'wf-7-21',
|
|
733
|
-
},
|
|
734
|
-
data: expect.objectContaining({ status: 'FAILED' }),
|
|
787
|
+
instanceId: BigInt(21),
|
|
788
|
+
expectedStatuses: ['PENDING', 'RUNNING'],
|
|
789
|
+
temporalRunId: 'wf-7-21',
|
|
735
790
|
}),
|
|
736
791
|
);
|
|
737
792
|
expect(temporal.startWorkflow).not.toHaveBeenCalled();
|
|
738
793
|
});
|
|
739
794
|
|
|
740
795
|
it('fails a stale RUNNING instance when its Temporal execution is missing', async () => {
|
|
741
|
-
const { prisma, temporal, service } = setup();
|
|
742
|
-
prisma.workflowInstance.findMany
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
796
|
+
const { prisma, temporal, lifecycle, service } = setup();
|
|
797
|
+
prisma.workflowInstance.findMany
|
|
798
|
+
.mockResolvedValueOnce([])
|
|
799
|
+
.mockResolvedValueOnce([
|
|
800
|
+
{
|
|
801
|
+
id: BigInt(21),
|
|
802
|
+
workflowId,
|
|
803
|
+
versionId: version.id,
|
|
804
|
+
status: 'RUNNING',
|
|
805
|
+
context: { workflow: {}, nodes: {} },
|
|
806
|
+
temporalRunId: 'wf-7-21',
|
|
807
|
+
},
|
|
808
|
+
]);
|
|
752
809
|
temporal.inspectWorkflow.mockResolvedValueOnce({ kind: 'MISSING' });
|
|
753
810
|
|
|
754
811
|
await service.reconcileUnstartedInstances();
|
|
755
812
|
|
|
756
|
-
expect(
|
|
813
|
+
expect(lifecycle.markFailed).toHaveBeenCalledWith(
|
|
757
814
|
expect.objectContaining({
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
data: expect.objectContaining({
|
|
764
|
-
status: 'FAILED',
|
|
765
|
-
result: expect.objectContaining({
|
|
766
|
-
code: 'TEMPORAL_EXECUTION_MISSING',
|
|
767
|
-
}),
|
|
815
|
+
instanceId: BigInt(21),
|
|
816
|
+
expectedStatuses: ['RUNNING'],
|
|
817
|
+
temporalRunId: 'wf-7-21',
|
|
818
|
+
result: expect.objectContaining({
|
|
819
|
+
code: 'TEMPORAL_EXECUTION_MISSING',
|
|
768
820
|
}),
|
|
769
821
|
}),
|
|
770
822
|
);
|