@gadmin2n/schematics 0.0.136 → 0.0.138

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.server +2 -2
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/compose-ctl.sh +1 -1
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/canvas.prisma +1 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +14 -3
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/User.ts +4 -4
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.md +2 -2
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.yml +13 -92
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/readme.md +105 -7
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/ensure-database.js +1 -1
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +5 -5
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/prisma.config.ts +9 -1
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/notify-dev-servers.js +9 -8
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/wait-for-nest-ready.js +5 -5
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +43 -2
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +9 -1
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflows.ts +24 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.dto.ts +27 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.spec.ts +430 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.ts +203 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +36 -3
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.module.ts +2 -1
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +54 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +245 -0
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.module.ts +10 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.spec.ts +366 -0
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.ts +110 -0
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +19 -1
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +15 -0
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +79 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +41 -19
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/start-prod.sh +5 -5
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/README.md +37 -573
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/web/ecosystem.dev.config.cjs +12 -1
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +2 -1
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +3 -2
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +39 -26
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/i18n.ts +3 -1
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +3 -0
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +3 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasEditPage.tsx +23 -5
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +66 -10
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +16 -7
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +21 -0
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts +30 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ExportModal.tsx +84 -0
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ImportModal.tsx +166 -0
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/hooks/useCanvasHistory.ts +52 -13
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.tsx +6 -0
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +6 -1
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +17 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +18 -6
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +225 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/utilities/lazy-with-reload.ts +31 -0
  54. package/package.json +1 -1
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.spec.ts +0 -220
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.ts +0 -132
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/TODO.md +0 -152
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/config/development-sql.yaml +0 -5
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/docker-compose.yml +0 -25
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/package.json +0 -13
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.dockerignore +0 -12
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.env +0 -7
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/Dockerfile +0 -79
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/GRACEFUL-DEPLOYMENT.md +0 -270
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/SANDBOX.md +0 -122
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package-lock.json +0 -4285
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package.json +0 -28
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/code-execute.ts +0 -51
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-execute.ts +0 -85
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-query.ts +0 -35
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/http-request.ts +0 -54
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/index.ts +0 -6
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/reporting.ts +0 -85
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/send-notification.ts +0 -47
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/config.ts +0 -13
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/condition.ts +0 -101
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/context.ts +0 -58
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/graph.ts +0 -184
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/helpers.ts +0 -133
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/node-types.ts +0 -96
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/types.ts +0 -77
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/validate.ts +0 -109
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/index.ts +0 -101
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/outbox-poller.ts +0 -382
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/code-execute.test.ts +0 -44
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/http-request.test.ts +0 -87
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/cron-trigger-workflow.test.ts +0 -20
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/helpers.test.ts +0 -225
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/node-type-consistency.test.ts +0 -101
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/validate.test.ts +0 -205
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/workflows/dsl-workflow.ts +0 -499
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/tsconfig.json +0 -19
  93. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/vitest.config.ts +0 -8
  94. 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
+ });