@gadmin2n/schematics 0.0.136 → 0.0.137
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/Dockerfile.server +2 -2
- 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/canvas.prisma +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +14 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/User.ts +4 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.md +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.yml +13 -92
- package/dist/lib/application/files/gadmin2-game-angle-demo/readme.md +105 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/ensure-database.js +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +5 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/prisma.config.ts +9 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/notify-dev-servers.js +9 -8
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/wait-for-nest-ready.js +5 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +43 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +9 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflows.ts +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.dto.ts +27 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.spec.ts +430 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.ts +203 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +36 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.module.ts +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +54 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +245 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.module.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.spec.ts +366 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.ts +110 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +19 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +15 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +79 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +41 -19
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/start-prod.sh +5 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/README.md +37 -573
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/ecosystem.dev.config.cjs +12 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +3 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +39 -26
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/i18n.ts +3 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasEditPage.tsx +23 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +66 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +16 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +21 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts +30 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ExportModal.tsx +84 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ImportModal.tsx +166 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/hooks/useCanvasHistory.ts +52 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.tsx +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +6 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +17 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +18 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +225 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/utilities/lazy-with-reload.ts +31 -0
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.spec.ts +0 -220
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.ts +0 -132
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/TODO.md +0 -152
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/config/development-sql.yaml +0 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/docker-compose.yml +0 -25
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/package.json +0 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.dockerignore +0 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.env +0 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/Dockerfile +0 -79
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/GRACEFUL-DEPLOYMENT.md +0 -270
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/SANDBOX.md +0 -122
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package-lock.json +0 -4285
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package.json +0 -28
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/code-execute.ts +0 -51
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-execute.ts +0 -85
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-query.ts +0 -35
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/http-request.ts +0 -54
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/index.ts +0 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/reporting.ts +0 -85
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/send-notification.ts +0 -47
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/config.ts +0 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/condition.ts +0 -101
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/context.ts +0 -58
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/graph.ts +0 -184
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/helpers.ts +0 -133
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/node-types.ts +0 -96
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/types.ts +0 -77
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/validate.ts +0 -109
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/index.ts +0 -101
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/outbox-poller.ts +0 -382
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/code-execute.test.ts +0 -44
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/http-request.test.ts +0 -87
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/cron-trigger-workflow.test.ts +0 -20
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/helpers.test.ts +0 -225
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/node-type-consistency.test.ts +0 -101
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/validate.test.ts +0 -205
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/workflows/dsl-workflow.ts +0 -499
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/tsconfig.json +0 -19
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/vitest.config.ts +0 -8
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/yarn.lock +0 -1905
|
@@ -60,12 +60,20 @@ export async function seedWorkflowModule(prisma: PrismaClient): Promise<void> {
|
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
code: 'workflow_node_instance',
|
|
63
|
-
zhName: 'Node
|
|
63
|
+
zhName: 'Node 实例',
|
|
64
64
|
enName: 'Node Instance',
|
|
65
65
|
sortOrder: 2,
|
|
66
66
|
resourceCode: 'workflowNodeInstance',
|
|
67
67
|
resourceDescription: 'Workflow node instance management',
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
code: 'workflow_temporal_tenant',
|
|
71
|
+
zhName: '服务注册',
|
|
72
|
+
enName: 'Service Registration',
|
|
73
|
+
sortOrder: 3,
|
|
74
|
+
resourceCode: 'temporalTenantConfig',
|
|
75
|
+
resourceDescription: 'Temporal tenant config management',
|
|
76
|
+
},
|
|
69
77
|
];
|
|
70
78
|
|
|
71
79
|
const allPages = [{ id: workflowPage.id, code: workflowPage.code }];
|
|
@@ -24,24 +24,28 @@ const WORKFLOWS = [
|
|
|
24
24
|
type: 'http_request',
|
|
25
25
|
label: '拉取外部数据',
|
|
26
26
|
position: { x: 350, y: 200 },
|
|
27
|
+
config: {},
|
|
27
28
|
},
|
|
28
29
|
{
|
|
29
30
|
id: 'action_2',
|
|
30
31
|
type: 'code',
|
|
31
32
|
label: '数据转换',
|
|
32
33
|
position: { x: 600, y: 200 },
|
|
34
|
+
config: {},
|
|
33
35
|
},
|
|
34
36
|
{
|
|
35
37
|
id: 'action_3',
|
|
36
38
|
type: 'db_execute',
|
|
37
39
|
label: '写入数据库',
|
|
38
40
|
position: { x: 850, y: 200 },
|
|
41
|
+
config: {},
|
|
39
42
|
},
|
|
40
43
|
{
|
|
41
44
|
id: 'action_4',
|
|
42
45
|
type: 'send_notification',
|
|
43
46
|
label: '发送通知',
|
|
44
47
|
position: { x: 1100, y: 200 },
|
|
48
|
+
config: {},
|
|
45
49
|
},
|
|
46
50
|
],
|
|
47
51
|
edges: [
|
|
@@ -72,36 +76,42 @@ const WORKFLOWS = [
|
|
|
72
76
|
type: 'set_variable',
|
|
73
77
|
label: '提取用户信息',
|
|
74
78
|
position: { x: 350, y: 200 },
|
|
79
|
+
config: {},
|
|
75
80
|
},
|
|
76
81
|
{
|
|
77
82
|
id: 'approval_1',
|
|
78
83
|
type: 'approval',
|
|
79
84
|
label: '管理员审批',
|
|
80
85
|
position: { x: 600, y: 200 },
|
|
86
|
+
config: {},
|
|
81
87
|
},
|
|
82
88
|
{
|
|
83
89
|
id: 'condition_1',
|
|
84
90
|
type: 'if_else',
|
|
85
91
|
label: '审批结果判断',
|
|
86
92
|
position: { x: 850, y: 200 },
|
|
93
|
+
config: {},
|
|
87
94
|
},
|
|
88
95
|
{
|
|
89
96
|
id: 'action_2',
|
|
90
97
|
type: 'db_execute',
|
|
91
98
|
label: '激活用户',
|
|
92
99
|
position: { x: 1100, y: 100 },
|
|
100
|
+
config: {},
|
|
93
101
|
},
|
|
94
102
|
{
|
|
95
103
|
id: 'action_3',
|
|
96
104
|
type: 'send_notification',
|
|
97
105
|
label: '发送欢迎邮件',
|
|
98
106
|
position: { x: 1350, y: 100 },
|
|
107
|
+
config: {},
|
|
99
108
|
},
|
|
100
109
|
{
|
|
101
110
|
id: 'action_4',
|
|
102
111
|
type: 'send_notification',
|
|
103
112
|
label: '发送拒绝通知',
|
|
104
113
|
position: { x: 1100, y: 300 },
|
|
114
|
+
config: {},
|
|
105
115
|
},
|
|
106
116
|
],
|
|
107
117
|
edges: [
|
|
@@ -146,30 +156,35 @@ const WORKFLOWS = [
|
|
|
146
156
|
type: 'event_trigger',
|
|
147
157
|
label: '监听错误事件',
|
|
148
158
|
position: { x: 100, y: 200 },
|
|
159
|
+
config: {},
|
|
149
160
|
},
|
|
150
161
|
{
|
|
151
162
|
id: 'action_1',
|
|
152
163
|
type: 'db_query',
|
|
153
164
|
label: '查询近5分钟错误数',
|
|
154
165
|
position: { x: 350, y: 200 },
|
|
166
|
+
config: {},
|
|
155
167
|
},
|
|
156
168
|
{
|
|
157
169
|
id: 'condition_1',
|
|
158
170
|
type: 'if_else',
|
|
159
171
|
label: '错误率 > 5%?',
|
|
160
172
|
position: { x: 600, y: 200 },
|
|
173
|
+
config: {},
|
|
161
174
|
},
|
|
162
175
|
{
|
|
163
176
|
id: 'action_2',
|
|
164
177
|
type: 'send_notification',
|
|
165
178
|
label: '发送告警',
|
|
166
179
|
position: { x: 850, y: 100 },
|
|
180
|
+
config: {},
|
|
167
181
|
},
|
|
168
182
|
{
|
|
169
183
|
id: 'action_3',
|
|
170
184
|
type: 'code',
|
|
171
185
|
label: '记录日志',
|
|
172
186
|
position: { x: 850, y: 300 },
|
|
187
|
+
config: {},
|
|
173
188
|
},
|
|
174
189
|
],
|
|
175
190
|
edges: [
|
|
@@ -214,24 +229,28 @@ const WORKFLOWS = [
|
|
|
214
229
|
type: 'db_query',
|
|
215
230
|
label: '查询数据',
|
|
216
231
|
position: { x: 350, y: 200 },
|
|
232
|
+
config: {},
|
|
217
233
|
},
|
|
218
234
|
{
|
|
219
235
|
id: 'loop_1',
|
|
220
236
|
type: 'for_each',
|
|
221
237
|
label: '遍历分页',
|
|
222
238
|
position: { x: 600, y: 200 },
|
|
239
|
+
config: {},
|
|
223
240
|
},
|
|
224
241
|
{
|
|
225
242
|
id: 'action_2',
|
|
226
243
|
type: 'code',
|
|
227
244
|
label: '转换为CSV',
|
|
228
245
|
position: { x: 850, y: 200 },
|
|
246
|
+
config: {},
|
|
229
247
|
},
|
|
230
248
|
{
|
|
231
249
|
id: 'action_3',
|
|
232
250
|
type: 'send_notification',
|
|
233
251
|
label: '发送邮件',
|
|
234
252
|
position: { x: 1100, y: 200 },
|
|
253
|
+
config: {},
|
|
235
254
|
},
|
|
236
255
|
],
|
|
237
256
|
edges: [
|
|
@@ -262,30 +281,35 @@ const WORKFLOWS = [
|
|
|
262
281
|
type: 'parallel',
|
|
263
282
|
label: '并行部署',
|
|
264
283
|
position: { x: 350, y: 200 },
|
|
284
|
+
config: {},
|
|
265
285
|
},
|
|
266
286
|
{
|
|
267
287
|
id: 'action_1',
|
|
268
288
|
type: 'http_request',
|
|
269
289
|
label: '部署 Staging',
|
|
270
290
|
position: { x: 600, y: 100 },
|
|
291
|
+
config: {},
|
|
271
292
|
},
|
|
272
293
|
{
|
|
273
294
|
id: 'approval_1',
|
|
274
295
|
type: 'approval',
|
|
275
296
|
label: 'Production 审批',
|
|
276
297
|
position: { x: 600, y: 300 },
|
|
298
|
+
config: {},
|
|
277
299
|
},
|
|
278
300
|
{
|
|
279
301
|
id: 'action_2',
|
|
280
302
|
type: 'http_request',
|
|
281
303
|
label: '部署 Production',
|
|
282
304
|
position: { x: 850, y: 300 },
|
|
305
|
+
config: {},
|
|
283
306
|
},
|
|
284
307
|
{
|
|
285
308
|
id: 'action_3',
|
|
286
309
|
type: 'send_notification',
|
|
287
310
|
label: '部署完成通知',
|
|
288
311
|
position: { x: 1100, y: 200 },
|
|
312
|
+
config: {},
|
|
289
313
|
},
|
|
290
314
|
],
|
|
291
315
|
edges: [
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { IsObject } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
export class ImportPayloadDto {
|
|
4
|
+
@IsObject()
|
|
5
|
+
payload: ExportPayload;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ExportPayload {
|
|
9
|
+
version: number;
|
|
10
|
+
exportedAt: string;
|
|
11
|
+
canvas: {
|
|
12
|
+
code: string;
|
|
13
|
+
name: string;
|
|
14
|
+
items: any[];
|
|
15
|
+
layout: any[];
|
|
16
|
+
};
|
|
17
|
+
savedQueries: Array<{
|
|
18
|
+
sourceId: string;
|
|
19
|
+
sql: string;
|
|
20
|
+
description: string;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ImportPreviewResponse {
|
|
25
|
+
current: Partial<ExportPayload> | null;
|
|
26
|
+
incoming: ExportPayload;
|
|
27
|
+
}
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { NotFoundException } from '@nestjs/common';
|
|
2
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
3
|
+
import { PrismaService } from 'nestjs-prisma';
|
|
4
|
+
import { CanvasExportService } from './canvas-export.service';
|
|
5
|
+
|
|
6
|
+
describe('CanvasExportService', () => {
|
|
7
|
+
let service: CanvasExportService;
|
|
8
|
+
let prisma: any;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
prisma = {
|
|
12
|
+
canvas: {
|
|
13
|
+
findUnique: jest.fn(),
|
|
14
|
+
findFirst: jest.fn(),
|
|
15
|
+
upsert: jest.fn(),
|
|
16
|
+
},
|
|
17
|
+
savedQuery: {
|
|
18
|
+
findMany: jest.fn(),
|
|
19
|
+
findFirst: jest.fn(),
|
|
20
|
+
create: jest.fn(),
|
|
21
|
+
},
|
|
22
|
+
page: {
|
|
23
|
+
findFirst: jest.fn(),
|
|
24
|
+
delete: jest.fn(),
|
|
25
|
+
},
|
|
26
|
+
rolePages: {
|
|
27
|
+
deleteMany: jest.fn(),
|
|
28
|
+
},
|
|
29
|
+
$transaction: jest.fn(async (cb: any) =>
|
|
30
|
+
cb({
|
|
31
|
+
canvas: prisma.canvas,
|
|
32
|
+
savedQuery: prisma.savedQuery,
|
|
33
|
+
page: prisma.page,
|
|
34
|
+
rolePages: prisma.rolePages,
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
40
|
+
providers: [
|
|
41
|
+
CanvasExportService,
|
|
42
|
+
{ provide: PrismaService, useValue: prisma },
|
|
43
|
+
],
|
|
44
|
+
}).compile();
|
|
45
|
+
|
|
46
|
+
service = module.get<CanvasExportService>(CanvasExportService);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('generateExportToken', () => {
|
|
50
|
+
it('throws NotFoundException when canvas does not exist', async () => {
|
|
51
|
+
prisma.canvas.findUnique.mockResolvedValueOnce(null);
|
|
52
|
+
|
|
53
|
+
await expect(
|
|
54
|
+
service.generateExportToken('nonexistent-id', 'https://example.com'),
|
|
55
|
+
).rejects.toThrow(NotFoundException);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns a URL containing the canvas code and a token', async () => {
|
|
59
|
+
prisma.canvas.findUnique.mockResolvedValueOnce({ code: 'cv_abc123' });
|
|
60
|
+
|
|
61
|
+
const result = await service.generateExportToken(
|
|
62
|
+
'canvas-id-1',
|
|
63
|
+
'https://example.com/api',
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(result.url).toContain(
|
|
67
|
+
'https://example.com/api/canvas/export/cv_abc123?token=',
|
|
68
|
+
);
|
|
69
|
+
expect(prisma.canvas.findUnique).toHaveBeenCalledWith({
|
|
70
|
+
where: { id: 'canvas-id-1' },
|
|
71
|
+
select: { code: true },
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('getExportPayload', () => {
|
|
77
|
+
const CODE = 'cv_abc123';
|
|
78
|
+
let validToken: string;
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
82
|
+
const jwt = require('jsonwebtoken');
|
|
83
|
+
validToken = jwt.sign(
|
|
84
|
+
{ canvasCode: CODE },
|
|
85
|
+
process.env.TAIHU_ODC_APP_TOKEN || 'gadmin-default-secret',
|
|
86
|
+
{ expiresIn: 1800 },
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('throws UnauthorizedException for invalid token', async () => {
|
|
91
|
+
await expect(
|
|
92
|
+
service.getExportPayload(CODE, 'garbage-token'),
|
|
93
|
+
).rejects.toThrow('Export token is invalid or expired');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('throws UnauthorizedException when token code mismatches', async () => {
|
|
97
|
+
await expect(
|
|
98
|
+
service.getExportPayload('different-code', validToken),
|
|
99
|
+
).rejects.toThrow('Token does not match this canvas');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('throws NotFoundException when canvas does not exist', async () => {
|
|
103
|
+
prisma.canvas.findUnique.mockResolvedValueOnce(null);
|
|
104
|
+
|
|
105
|
+
await expect(service.getExportPayload(CODE, validToken)).rejects.toThrow(
|
|
106
|
+
'Canvas not found',
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('extracts referenced SavedQuery ids from item code and includes them in payload', async () => {
|
|
111
|
+
prisma.canvas.findUnique.mockResolvedValueOnce({
|
|
112
|
+
code: CODE,
|
|
113
|
+
name: 'My Dashboard',
|
|
114
|
+
items: [
|
|
115
|
+
{
|
|
116
|
+
id: 'item1',
|
|
117
|
+
componentType: 'Table',
|
|
118
|
+
code: "fetch('/api/canvas/saved-query/42/run')",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'item2',
|
|
122
|
+
componentType: 'BarChart',
|
|
123
|
+
code: "fetch('/api/canvas/saved-query/7/run')",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
layout: [{ i: 'item1', x: 0, y: 0, w: 4, h: 4 }],
|
|
127
|
+
});
|
|
128
|
+
prisma.savedQuery.findMany.mockResolvedValueOnce([
|
|
129
|
+
{ id: 42, sql: 'SELECT * FROM t_a', description: 'desc a' },
|
|
130
|
+
{ id: 7, sql: 'SELECT * FROM t_b', description: 'desc b' },
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
const result = await service.getExportPayload(CODE, validToken);
|
|
134
|
+
|
|
135
|
+
expect(prisma.savedQuery.findMany).toHaveBeenCalledWith({
|
|
136
|
+
where: { id: { in: [42, 7] } },
|
|
137
|
+
});
|
|
138
|
+
expect(result.canvas.code).toBe(CODE);
|
|
139
|
+
expect(result.canvas.name).toBe('My Dashboard');
|
|
140
|
+
expect(result.savedQueries).toEqual([
|
|
141
|
+
{ sourceId: '42', sql: 'SELECT * FROM t_a', description: 'desc a' },
|
|
142
|
+
{ sourceId: '7', sql: 'SELECT * FROM t_b', description: 'desc b' },
|
|
143
|
+
]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('returns empty savedQueries when no items reference saved-query', async () => {
|
|
147
|
+
prisma.canvas.findUnique.mockResolvedValueOnce({
|
|
148
|
+
code: CODE,
|
|
149
|
+
name: 'Empty Dashboard',
|
|
150
|
+
items: [
|
|
151
|
+
{ id: 'item1', componentType: 'NumCard', code: '<div>static</div>' },
|
|
152
|
+
],
|
|
153
|
+
layout: [],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const result = await service.getExportPayload(CODE, validToken);
|
|
157
|
+
|
|
158
|
+
expect(prisma.savedQuery.findMany).not.toHaveBeenCalled();
|
|
159
|
+
expect(result.savedQueries).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('importPreview', () => {
|
|
164
|
+
it('throws BadRequestException when payload has no canvas code', async () => {
|
|
165
|
+
await expect(
|
|
166
|
+
service.importPreview({
|
|
167
|
+
version: 1,
|
|
168
|
+
exportedAt: '',
|
|
169
|
+
canvas: {} as any,
|
|
170
|
+
savedQueries: [],
|
|
171
|
+
}),
|
|
172
|
+
).rejects.toThrow('Invalid payload: missing canvas code');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('returns current=null when no existing canvas matches the code', async () => {
|
|
176
|
+
prisma.canvas.findFirst.mockResolvedValueOnce(null);
|
|
177
|
+
|
|
178
|
+
const payload = {
|
|
179
|
+
version: 1,
|
|
180
|
+
exportedAt: '2026-01-01T00:00:00.000Z',
|
|
181
|
+
canvas: { code: 'cv_new', name: 'New', items: [], layout: [] },
|
|
182
|
+
savedQueries: [],
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const result = await service.importPreview(payload);
|
|
186
|
+
|
|
187
|
+
expect(result.current).toBeNull();
|
|
188
|
+
expect(result.incoming).toEqual(payload);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('returns current with existing canvas data when code matches', async () => {
|
|
192
|
+
prisma.canvas.findFirst.mockResolvedValueOnce({
|
|
193
|
+
code: 'cv_existing',
|
|
194
|
+
name: 'Old Name',
|
|
195
|
+
items: [{ id: 'x', componentType: 'Table', code: 'old' }],
|
|
196
|
+
layout: [{ i: 'x', x: 0, y: 0, w: 1, h: 1 }],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const payload = {
|
|
200
|
+
version: 1,
|
|
201
|
+
exportedAt: '2026-01-01T00:00:00.000Z',
|
|
202
|
+
canvas: {
|
|
203
|
+
code: 'cv_existing',
|
|
204
|
+
name: 'New Name',
|
|
205
|
+
items: [],
|
|
206
|
+
layout: [],
|
|
207
|
+
},
|
|
208
|
+
savedQueries: [],
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const result = await service.importPreview(payload);
|
|
212
|
+
|
|
213
|
+
expect(result.current).toEqual({
|
|
214
|
+
canvas: {
|
|
215
|
+
code: 'cv_existing',
|
|
216
|
+
name: 'Old Name',
|
|
217
|
+
items: [{ id: 'x', componentType: 'Table', code: 'old' }],
|
|
218
|
+
layout: [{ i: 'x', x: 0, y: 0, w: 1, h: 1 }],
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
expect(result.incoming).toEqual(payload);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('importConfirm', () => {
|
|
226
|
+
it('throws BadRequestException when payload has no canvas code', async () => {
|
|
227
|
+
await expect(
|
|
228
|
+
service.importConfirm(
|
|
229
|
+
{ version: 1, exportedAt: '', canvas: {} as any, savedQueries: [] },
|
|
230
|
+
'user-1',
|
|
231
|
+
),
|
|
232
|
+
).rejects.toThrow('Invalid payload: missing canvas code');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('creates a new canvas owned by the importing user when code does not exist', async () => {
|
|
236
|
+
prisma.canvas.findFirst.mockResolvedValueOnce(null); // 检查是否已存在(发布清理判断)
|
|
237
|
+
prisma.canvas.upsert.mockResolvedValueOnce({
|
|
238
|
+
id: 'new-canvas-id',
|
|
239
|
+
name: 'Imported Dashboard',
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const payload = {
|
|
243
|
+
version: 1,
|
|
244
|
+
exportedAt: '2026-01-01T00:00:00.000Z',
|
|
245
|
+
canvas: {
|
|
246
|
+
code: 'cv_new',
|
|
247
|
+
name: 'Imported Dashboard',
|
|
248
|
+
items: [
|
|
249
|
+
{ id: 'x', componentType: 'NumCard', code: '<div>static</div>' },
|
|
250
|
+
],
|
|
251
|
+
layout: [],
|
|
252
|
+
},
|
|
253
|
+
savedQueries: [],
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const result = await service.importConfirm(payload, 'user-1');
|
|
257
|
+
|
|
258
|
+
expect(prisma.canvas.upsert).toHaveBeenCalledWith({
|
|
259
|
+
where: { code: 'cv_new' },
|
|
260
|
+
create: {
|
|
261
|
+
code: 'cv_new',
|
|
262
|
+
name: 'Imported Dashboard',
|
|
263
|
+
userId: 'user-1',
|
|
264
|
+
items: payload.canvas.items,
|
|
265
|
+
layout: [],
|
|
266
|
+
},
|
|
267
|
+
update: {
|
|
268
|
+
name: 'Imported Dashboard',
|
|
269
|
+
items: payload.canvas.items,
|
|
270
|
+
layout: [],
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
expect(result).toEqual({
|
|
274
|
+
success: true,
|
|
275
|
+
canvasId: 'new-canvas-id',
|
|
276
|
+
canvasName: 'Imported Dashboard',
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('reuses existing SavedQuery id when sql text matches, and rewrites item code', async () => {
|
|
281
|
+
prisma.canvas.findFirst.mockResolvedValueOnce(null);
|
|
282
|
+
prisma.savedQuery.findFirst.mockResolvedValueOnce({
|
|
283
|
+
id: 99,
|
|
284
|
+
sql: 'SELECT 1',
|
|
285
|
+
description: 'd',
|
|
286
|
+
});
|
|
287
|
+
prisma.canvas.upsert.mockResolvedValueOnce({ id: 'cid', name: 'N' });
|
|
288
|
+
|
|
289
|
+
const payload = {
|
|
290
|
+
version: 1,
|
|
291
|
+
exportedAt: '',
|
|
292
|
+
canvas: {
|
|
293
|
+
code: 'cv_x',
|
|
294
|
+
name: 'N',
|
|
295
|
+
items: [
|
|
296
|
+
{
|
|
297
|
+
id: 'i1',
|
|
298
|
+
componentType: 'Table',
|
|
299
|
+
code: "fetch('/api/canvas/saved-query/42/run')",
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
layout: [],
|
|
303
|
+
},
|
|
304
|
+
savedQueries: [{ sourceId: '42', sql: 'SELECT 1', description: 'd' }],
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
await service.importConfirm(payload, 'user-1');
|
|
308
|
+
|
|
309
|
+
expect(prisma.savedQuery.findFirst).toHaveBeenCalledWith({
|
|
310
|
+
where: { sql: 'SELECT 1' },
|
|
311
|
+
});
|
|
312
|
+
expect(prisma.savedQuery.create).not.toHaveBeenCalled();
|
|
313
|
+
expect(prisma.canvas.upsert).toHaveBeenCalledWith(
|
|
314
|
+
expect.objectContaining({
|
|
315
|
+
create: expect.objectContaining({
|
|
316
|
+
items: [
|
|
317
|
+
{
|
|
318
|
+
id: 'i1',
|
|
319
|
+
componentType: 'Table',
|
|
320
|
+
code: "fetch('/api/canvas/saved-query/99/run')",
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
}),
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('creates a new SavedQuery when no matching sql exists in target env', async () => {
|
|
329
|
+
prisma.canvas.findFirst.mockResolvedValueOnce(null);
|
|
330
|
+
prisma.savedQuery.findFirst.mockResolvedValueOnce(null);
|
|
331
|
+
prisma.savedQuery.create.mockResolvedValueOnce({
|
|
332
|
+
id: 100,
|
|
333
|
+
sql: 'SELECT 2',
|
|
334
|
+
description: 'd2',
|
|
335
|
+
});
|
|
336
|
+
prisma.canvas.upsert.mockResolvedValueOnce({ id: 'cid', name: 'N' });
|
|
337
|
+
|
|
338
|
+
const payload = {
|
|
339
|
+
version: 1,
|
|
340
|
+
exportedAt: '',
|
|
341
|
+
canvas: {
|
|
342
|
+
code: 'cv_y',
|
|
343
|
+
name: 'N',
|
|
344
|
+
items: [
|
|
345
|
+
{
|
|
346
|
+
id: 'i1',
|
|
347
|
+
componentType: 'Table',
|
|
348
|
+
code: "fetch('/api/canvas/saved-query/7/run')",
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
layout: [],
|
|
352
|
+
},
|
|
353
|
+
savedQueries: [{ sourceId: '7', sql: 'SELECT 2', description: 'd2' }],
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
await service.importConfirm(payload, 'user-1');
|
|
357
|
+
|
|
358
|
+
expect(prisma.savedQuery.create).toHaveBeenCalledWith({
|
|
359
|
+
data: { sql: 'SELECT 2', description: 'd2' },
|
|
360
|
+
});
|
|
361
|
+
expect(prisma.canvas.upsert).toHaveBeenCalledWith(
|
|
362
|
+
expect.objectContaining({
|
|
363
|
+
create: expect.objectContaining({
|
|
364
|
+
items: [
|
|
365
|
+
{
|
|
366
|
+
id: 'i1',
|
|
367
|
+
componentType: 'Table',
|
|
368
|
+
code: "fetch('/api/canvas/saved-query/100/run')",
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
}),
|
|
372
|
+
}),
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('cleans up Page and RolePages when overwriting an already-published canvas', async () => {
|
|
377
|
+
prisma.canvas.findFirst.mockResolvedValueOnce({
|
|
378
|
+
id: 'existing-id',
|
|
379
|
+
code: 'cv_pub',
|
|
380
|
+
publishedPageCode: 'my-page-slug',
|
|
381
|
+
});
|
|
382
|
+
prisma.page.findFirst.mockResolvedValueOnce({ id: 5 });
|
|
383
|
+
prisma.canvas.upsert.mockResolvedValueOnce({
|
|
384
|
+
id: 'existing-id',
|
|
385
|
+
name: 'N',
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const payload = {
|
|
389
|
+
version: 1,
|
|
390
|
+
exportedAt: '',
|
|
391
|
+
canvas: { code: 'cv_pub', name: 'N', items: [], layout: [] },
|
|
392
|
+
savedQueries: [],
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
await service.importConfirm(payload, 'user-1');
|
|
396
|
+
|
|
397
|
+
expect(prisma.page.findFirst).toHaveBeenCalledWith({
|
|
398
|
+
where: { code: 'my-page-slug' },
|
|
399
|
+
});
|
|
400
|
+
expect(prisma.rolePages.deleteMany).toHaveBeenCalledWith({
|
|
401
|
+
where: { pageId: 5 },
|
|
402
|
+
});
|
|
403
|
+
expect(prisma.page.delete).toHaveBeenCalledWith({ where: { id: 5 } });
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('does not touch Page/RolePages when overwriting a canvas that is not published', async () => {
|
|
407
|
+
prisma.canvas.findFirst.mockResolvedValueOnce({
|
|
408
|
+
id: 'existing-id',
|
|
409
|
+
code: 'cv_unpub',
|
|
410
|
+
publishedPageCode: null,
|
|
411
|
+
});
|
|
412
|
+
prisma.canvas.upsert.mockResolvedValueOnce({
|
|
413
|
+
id: 'existing-id',
|
|
414
|
+
name: 'N',
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const payload = {
|
|
418
|
+
version: 1,
|
|
419
|
+
exportedAt: '',
|
|
420
|
+
canvas: { code: 'cv_unpub', name: 'N', items: [], layout: [] },
|
|
421
|
+
savedQueries: [],
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
await service.importConfirm(payload, 'user-1');
|
|
425
|
+
|
|
426
|
+
expect(prisma.page.findFirst).not.toHaveBeenCalled();
|
|
427
|
+
expect(prisma.rolePages.deleteMany).not.toHaveBeenCalled();
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
});
|