@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
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
Injectable,
|
|
4
|
+
NotFoundException,
|
|
5
|
+
UnauthorizedException,
|
|
6
|
+
} from '@nestjs/common';
|
|
7
|
+
import * as jwt from 'jsonwebtoken';
|
|
8
|
+
import { PrismaService } from 'nestjs-prisma';
|
|
9
|
+
import { ExportPayload, ImportPreviewResponse } from './canvas-export.dto';
|
|
10
|
+
|
|
11
|
+
const EXPORT_TOKEN_SECRET =
|
|
12
|
+
process.env.TAIHU_ODC_APP_TOKEN || 'gadmin-default-secret';
|
|
13
|
+
const EXPORT_TOKEN_EXPIRY = 30 * 60; // 30 minutes in seconds
|
|
14
|
+
|
|
15
|
+
// 匹配 Canvas Item 代码里硬编码的 saved-query 调用,如 fetch('/api/canvas/saved-query/42/run')
|
|
16
|
+
const SAVED_QUERY_ID_PATTERN = /\/canvas\/saved-query\/(\d+)\/run/g;
|
|
17
|
+
|
|
18
|
+
interface ExportTokenPayload {
|
|
19
|
+
canvasCode: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Injectable()
|
|
23
|
+
export class CanvasExportService {
|
|
24
|
+
constructor(private prisma: PrismaService) {}
|
|
25
|
+
|
|
26
|
+
/** Generate a JWT export token for a canvas */
|
|
27
|
+
async generateExportToken(
|
|
28
|
+
canvasId: string,
|
|
29
|
+
baseUrl: string,
|
|
30
|
+
): Promise<{ url: string }> {
|
|
31
|
+
const canvas = await this.prisma.canvas.findUnique({
|
|
32
|
+
where: { id: canvasId },
|
|
33
|
+
select: { code: true },
|
|
34
|
+
});
|
|
35
|
+
if (!canvas) throw new NotFoundException('Canvas not found');
|
|
36
|
+
|
|
37
|
+
const token = jwt.sign(
|
|
38
|
+
{ canvasCode: canvas.code } as ExportTokenPayload,
|
|
39
|
+
EXPORT_TOKEN_SECRET,
|
|
40
|
+
{ expiresIn: EXPORT_TOKEN_EXPIRY },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const url = `${baseUrl}/canvas/export/${canvas.code}?token=${token}`;
|
|
44
|
+
return { url };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 从 canvas items 的 code 字符串里提取所有引用到的 SavedQuery id */
|
|
48
|
+
private extractSavedQueryIds(items: any[]): number[] {
|
|
49
|
+
const ids = new Set<number>();
|
|
50
|
+
for (const item of items ?? []) {
|
|
51
|
+
const code = item?.code ?? '';
|
|
52
|
+
let match: RegExpExecArray | null;
|
|
53
|
+
const pattern = new RegExp(SAVED_QUERY_ID_PATTERN);
|
|
54
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
55
|
+
ids.add(Number(match[1]));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return Array.from(ids);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Verify export token and return the full export payload */
|
|
62
|
+
async getExportPayload(code: string, token: string): Promise<ExportPayload> {
|
|
63
|
+
let decoded: ExportTokenPayload;
|
|
64
|
+
try {
|
|
65
|
+
decoded = jwt.verify(token, EXPORT_TOKEN_SECRET) as ExportTokenPayload;
|
|
66
|
+
} catch {
|
|
67
|
+
throw new UnauthorizedException('Export token is invalid or expired');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (decoded.canvasCode !== code) {
|
|
71
|
+
throw new UnauthorizedException('Token does not match this canvas');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const canvas = await this.prisma.canvas.findUnique({
|
|
75
|
+
where: { code },
|
|
76
|
+
});
|
|
77
|
+
if (!canvas) throw new NotFoundException('Canvas not found');
|
|
78
|
+
|
|
79
|
+
const items = (canvas.items as any[]) ?? [];
|
|
80
|
+
const savedQueryIds = this.extractSavedQueryIds(items);
|
|
81
|
+
|
|
82
|
+
const savedQueries =
|
|
83
|
+
savedQueryIds.length > 0
|
|
84
|
+
? await this.prisma.savedQuery.findMany({
|
|
85
|
+
where: { id: { in: savedQueryIds } },
|
|
86
|
+
})
|
|
87
|
+
: [];
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
version: 1,
|
|
91
|
+
exportedAt: new Date().toISOString(),
|
|
92
|
+
canvas: {
|
|
93
|
+
code: canvas.code,
|
|
94
|
+
name: canvas.name,
|
|
95
|
+
items,
|
|
96
|
+
layout: (canvas.layout as any[]) ?? [],
|
|
97
|
+
},
|
|
98
|
+
savedQueries: savedQueries.map((sq) => ({
|
|
99
|
+
sourceId: String(sq.id),
|
|
100
|
+
sql: sq.sql,
|
|
101
|
+
description: sq.description,
|
|
102
|
+
})),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Preview import — return current state vs incoming for diff */
|
|
107
|
+
async importPreview(payload: ExportPayload): Promise<ImportPreviewResponse> {
|
|
108
|
+
if (!payload?.canvas?.code) {
|
|
109
|
+
throw new BadRequestException('Invalid payload: missing canvas code');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const existing = await this.prisma.canvas.findFirst({
|
|
113
|
+
where: { code: payload.canvas.code },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const current: Partial<ExportPayload> | null = existing
|
|
117
|
+
? {
|
|
118
|
+
canvas: {
|
|
119
|
+
code: existing.code,
|
|
120
|
+
name: existing.name,
|
|
121
|
+
items: (existing.items as any[]) ?? [],
|
|
122
|
+
layout: (existing.layout as any[]) ?? [],
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
: null;
|
|
126
|
+
|
|
127
|
+
return { current, incoming: payload };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Confirm import — execute upsert with SavedQuery id remapping in a transaction */
|
|
131
|
+
async importConfirm(
|
|
132
|
+
payload: ExportPayload,
|
|
133
|
+
userId: string,
|
|
134
|
+
): Promise<{ success: boolean; canvasId: string; canvasName: string }> {
|
|
135
|
+
if (!payload?.canvas?.code) {
|
|
136
|
+
throw new BadRequestException('Invalid payload: missing canvas code');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return this.prisma.$transaction(async (tx) => {
|
|
140
|
+
// 1. 若目标环境已存在同 code 的已发布 Canvas,先清理旧的 Page/RolePages
|
|
141
|
+
const existing = await tx.canvas.findFirst({
|
|
142
|
+
where: { code: payload.canvas.code },
|
|
143
|
+
});
|
|
144
|
+
if (existing?.publishedPageCode) {
|
|
145
|
+
const page = await tx.page.findFirst({
|
|
146
|
+
where: { code: existing.publishedPageCode },
|
|
147
|
+
});
|
|
148
|
+
if (page) {
|
|
149
|
+
await tx.rolePages.deleteMany({ where: { pageId: page.id } });
|
|
150
|
+
await tx.page.delete({ where: { id: page.id } });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 2. 按 sql 文本 upsert SavedQuery,建立 sourceId → newId 映射
|
|
155
|
+
const idMap = new Map<string, string>();
|
|
156
|
+
for (const sq of payload.savedQueries) {
|
|
157
|
+
let record = await tx.savedQuery.findFirst({ where: { sql: sq.sql } });
|
|
158
|
+
if (!record) {
|
|
159
|
+
record = await tx.savedQuery.create({
|
|
160
|
+
data: { sql: sq.sql, description: sq.description },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
idMap.set(sq.sourceId, String(record.id));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 3. 深拷贝 items,重写引用到的 saved-query id
|
|
167
|
+
const items = JSON.parse(JSON.stringify(payload.canvas.items ?? []));
|
|
168
|
+
for (const item of items) {
|
|
169
|
+
if (typeof item.code !== 'string') continue;
|
|
170
|
+
item.code = item.code.replace(
|
|
171
|
+
/\/canvas\/saved-query\/(\d+)\/run/g,
|
|
172
|
+
(match: string, oldId: string) => {
|
|
173
|
+
const newId = idMap.get(oldId);
|
|
174
|
+
return newId ? `/canvas/saved-query/${newId}/run` : match;
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 4. 按 code upsert Canvas(发布状态不参与导入,始终不带 publishedPageCode/publishedAt)
|
|
180
|
+
const canvas = await tx.canvas.upsert({
|
|
181
|
+
where: { code: payload.canvas.code },
|
|
182
|
+
create: {
|
|
183
|
+
code: payload.canvas.code,
|
|
184
|
+
name: payload.canvas.name,
|
|
185
|
+
userId,
|
|
186
|
+
items,
|
|
187
|
+
layout: payload.canvas.layout ?? [],
|
|
188
|
+
},
|
|
189
|
+
update: {
|
|
190
|
+
name: payload.canvas.name,
|
|
191
|
+
items,
|
|
192
|
+
layout: payload.canvas.layout ?? [],
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
success: true,
|
|
198
|
+
canvasId: canvas.id,
|
|
199
|
+
canvasName: canvas.name,
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -8,12 +8,15 @@ import {
|
|
|
8
8
|
ParseIntPipe,
|
|
9
9
|
Post,
|
|
10
10
|
Put,
|
|
11
|
+
Query,
|
|
11
12
|
Req,
|
|
12
|
-
|
|
13
|
+
UnauthorizedException,
|
|
13
14
|
} from '@nestjs/common';
|
|
14
15
|
import { ApiTags } from '@nestjs/swagger';
|
|
15
16
|
import { PrismaService } from 'nestjs-prisma';
|
|
16
|
-
import
|
|
17
|
+
import { AllowUnauthorizedRequest } from '../../lib/auth.guard';
|
|
18
|
+
import { ImportPayloadDto } from './canvas-export.dto';
|
|
19
|
+
import { CanvasExportService } from './canvas-export.service';
|
|
17
20
|
import {
|
|
18
21
|
CreateCanvasDto,
|
|
19
22
|
CreateSavedQueryDto,
|
|
@@ -23,12 +26,12 @@ import {
|
|
|
23
26
|
import { CanvasService } from './canvas.service';
|
|
24
27
|
|
|
25
28
|
@ApiTags('canvas')
|
|
26
|
-
@UseGuards(AuthGuard)
|
|
27
29
|
@Controller('canvas')
|
|
28
30
|
export class CanvasController {
|
|
29
31
|
constructor(
|
|
30
32
|
private readonly canvasService: CanvasService,
|
|
31
33
|
private readonly prisma: PrismaService,
|
|
34
|
+
private readonly canvasExportService: CanvasExportService,
|
|
32
35
|
) {}
|
|
33
36
|
|
|
34
37
|
@Get()
|
|
@@ -98,6 +101,36 @@ export class CanvasController {
|
|
|
98
101
|
return this.canvasService.remove(id, req.user.userid);
|
|
99
102
|
}
|
|
100
103
|
|
|
104
|
+
// ─── Export/Import Endpoints ──────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
@Post(':id/export-token')
|
|
107
|
+
async generateExportToken(@Param('id') id: string, @Req() req) {
|
|
108
|
+
const baseUrl = `${req.protocol}://${req.get('host')}/api`;
|
|
109
|
+
return this.canvasExportService.generateExportToken(id, baseUrl);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Get('export/:code')
|
|
113
|
+
@AllowUnauthorizedRequest()
|
|
114
|
+
async getExportPayload(
|
|
115
|
+
@Param('code') code: string,
|
|
116
|
+
@Query('token') token: string,
|
|
117
|
+
) {
|
|
118
|
+
if (!token) {
|
|
119
|
+
throw new UnauthorizedException('Export token is required');
|
|
120
|
+
}
|
|
121
|
+
return this.canvasExportService.getExportPayload(code, token);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@Post('import-preview')
|
|
125
|
+
async importPreview(@Body() dto: ImportPayloadDto) {
|
|
126
|
+
return this.canvasExportService.importPreview(dto.payload);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@Post('import-confirm')
|
|
130
|
+
async importConfirm(@Body() dto: ImportPayloadDto, @Req() req) {
|
|
131
|
+
return this.canvasExportService.importConfirm(dto.payload, req.user.userid);
|
|
132
|
+
}
|
|
133
|
+
|
|
101
134
|
@Post(':id/publish')
|
|
102
135
|
publish(@Req() req, @Param('id') id: string, @Body() dto: PublishCanvasDto) {
|
|
103
136
|
return this.canvasService.publish(id, req.user.userid, dto);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Module } from '@nestjs/common';
|
|
2
2
|
import { UserModule } from '../user/user.module';
|
|
3
|
+
import { CanvasExportService } from './canvas-export.service';
|
|
3
4
|
import { CanvasController } from './canvas.controller';
|
|
4
5
|
import { CanvasService } from './canvas.service';
|
|
5
6
|
|
|
6
7
|
@Module({
|
|
7
8
|
imports: [UserModule],
|
|
8
9
|
controllers: [CanvasController],
|
|
9
|
-
providers: [CanvasService],
|
|
10
|
+
providers: [CanvasService, CanvasExportService],
|
|
10
11
|
})
|
|
11
12
|
export class CanvasModule {}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/// <reference types="jest" />
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
3
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
4
|
+
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
|
5
|
+
import { TemporalTenantConfigController } from './temporalTenantConfig.controller';
|
|
6
|
+
import { TemporalTenantConfigService } from './temporalTenantConfig.service';
|
|
7
|
+
|
|
8
|
+
// Mock TemporalTenantConfigService:用空对象屏蔽真实 Service 的依赖(PrismaService 等),
|
|
9
|
+
// 避免 NestJS 在测试模块编译时去解析 Service 的构造参数。
|
|
10
|
+
const mockTemporalTenantConfigService = {};
|
|
11
|
+
|
|
12
|
+
// Mock ConfigService
|
|
13
|
+
const mockConfigService = {
|
|
14
|
+
get: jest.fn(),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Mock Logger
|
|
18
|
+
const mockLogger = {
|
|
19
|
+
child: jest.fn().mockReturnThis(),
|
|
20
|
+
info: jest.fn(),
|
|
21
|
+
error: jest.fn(),
|
|
22
|
+
warn: jest.fn(),
|
|
23
|
+
debug: jest.fn(),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe('TemporalTenantConfigController', () => {
|
|
27
|
+
let controller: TemporalTenantConfigController;
|
|
28
|
+
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
31
|
+
controllers: [TemporalTenantConfigController],
|
|
32
|
+
providers: [
|
|
33
|
+
{
|
|
34
|
+
provide: TemporalTenantConfigService,
|
|
35
|
+
useValue: mockTemporalTenantConfigService,
|
|
36
|
+
},
|
|
37
|
+
{ provide: ConfigService, useValue: mockConfigService },
|
|
38
|
+
{ provide: WINSTON_MODULE_PROVIDER, useValue: mockLogger },
|
|
39
|
+
],
|
|
40
|
+
})
|
|
41
|
+
// 兜底:自动 mock 任何未显式提供的依赖
|
|
42
|
+
// (例如 ACGuard 内部的 __roles_builder__ token)
|
|
43
|
+
.useMocker(() => ({}))
|
|
44
|
+
.compile();
|
|
45
|
+
|
|
46
|
+
controller = module.get<TemporalTenantConfigController>(
|
|
47
|
+
TemporalTenantConfigController,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should be defined', () => {
|
|
52
|
+
expect(controller).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACAttrFilter,
|
|
3
|
+
ACGuard,
|
|
4
|
+
ApiPaginatedResponse,
|
|
5
|
+
diskStorage,
|
|
6
|
+
PageDto,
|
|
7
|
+
PageMetaDto,
|
|
8
|
+
PageOptionsDto,
|
|
9
|
+
pick,
|
|
10
|
+
UserType,
|
|
11
|
+
} from '@gadmin2n/nest-common';
|
|
12
|
+
import {
|
|
13
|
+
BadRequestException,
|
|
14
|
+
Body,
|
|
15
|
+
Controller,
|
|
16
|
+
Delete,
|
|
17
|
+
Inject,
|
|
18
|
+
NotFoundException,
|
|
19
|
+
Param,
|
|
20
|
+
ParseIntPipe,
|
|
21
|
+
Patch,
|
|
22
|
+
Post,
|
|
23
|
+
Req,
|
|
24
|
+
UploadedFile,
|
|
25
|
+
UseGuards,
|
|
26
|
+
UseInterceptors,
|
|
27
|
+
} from '@nestjs/common';
|
|
28
|
+
import {
|
|
29
|
+
ApiCreatedResponse,
|
|
30
|
+
ApiNotFoundResponse,
|
|
31
|
+
ApiOkResponse,
|
|
32
|
+
ApiTags,
|
|
33
|
+
} from '@nestjs/swagger';
|
|
34
|
+
import { Prisma } from '@prisma/client';
|
|
35
|
+
import { UseRoles } from 'nest-access-control';
|
|
36
|
+
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
|
37
|
+
import { Logger } from 'winston';
|
|
38
|
+
import { CreateTemporalTenantConfigDto } from '../../generated/temporalTenantConfig/dto/create-temporalTenantConfig.dto';
|
|
39
|
+
import {
|
|
40
|
+
EffectedCount,
|
|
41
|
+
TemporalTenantConfigFindManyArgs,
|
|
42
|
+
TemporalTenantConfigWhereSelect,
|
|
43
|
+
UploadResponse,
|
|
44
|
+
} from '../../generated/temporalTenantConfig/dto/temporalTenantConfig-types.dto';
|
|
45
|
+
import { UpdateTemporalTenantConfigDto } from '../../generated/temporalTenantConfig/dto/update-temporalTenantConfig.dto';
|
|
46
|
+
import { TemporalTenantConfigEntity } from '../../generated/temporalTenantConfig/entities/temporalTenantConfig.entity';
|
|
47
|
+
import { TemporalTenantConfigService } from './temporalTenantConfig.service';
|
|
48
|
+
|
|
49
|
+
import { ConfigService } from '@nestjs/config';
|
|
50
|
+
import { FileInterceptor } from '@nestjs/platform-express';
|
|
51
|
+
|
|
52
|
+
@UseGuards(ACGuard)
|
|
53
|
+
@Controller('temporalTenantConfig')
|
|
54
|
+
@ApiTags('temporalTenantConfig')
|
|
55
|
+
export class TemporalTenantConfigController {
|
|
56
|
+
constructor(
|
|
57
|
+
private readonly temporalTenantConfigService: TemporalTenantConfigService,
|
|
58
|
+
private readonly config: ConfigService,
|
|
59
|
+
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
|
60
|
+
) {}
|
|
61
|
+
|
|
62
|
+
@Post('createOne')
|
|
63
|
+
@UseRoles({
|
|
64
|
+
resource: 'temporalTenantConfig',
|
|
65
|
+
action: 'create',
|
|
66
|
+
possession: 'any',
|
|
67
|
+
})
|
|
68
|
+
@ApiCreatedResponse({ type: TemporalTenantConfigEntity })
|
|
69
|
+
async createOne(
|
|
70
|
+
@Body() body: CreateTemporalTenantConfigDto,
|
|
71
|
+
@ACAttrFilter() acAttrFilter,
|
|
72
|
+
@Req() request: Request & { user: UserType },
|
|
73
|
+
) {
|
|
74
|
+
body.creator = request.user.username;
|
|
75
|
+
|
|
76
|
+
// 先调 shared temporal admin API(原子:失败则不写 DB)
|
|
77
|
+
const res = await fetch(`${body.adminUrl}/admin/tenants`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
'X-Admin-Token': body.adminToken,
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
namespace: body.namespace,
|
|
85
|
+
displayName: body.displayName,
|
|
86
|
+
dbUrl: body.dbUrl,
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
const detail = await res.text().catch(() => '');
|
|
91
|
+
throw new BadRequestException(
|
|
92
|
+
`shared temporal API 注册失败 (${res.status}): ${detail}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return new TemporalTenantConfigEntity(
|
|
97
|
+
await this.temporalTenantConfigService.createOne({
|
|
98
|
+
data: acAttrFilter(body),
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Post('findMany')
|
|
104
|
+
@UseRoles({
|
|
105
|
+
resource: 'temporalTenantConfig',
|
|
106
|
+
action: 'read',
|
|
107
|
+
possession: 'any',
|
|
108
|
+
})
|
|
109
|
+
@ApiPaginatedResponse(TemporalTenantConfigEntity)
|
|
110
|
+
async findMany(
|
|
111
|
+
@Body() body: TemporalTenantConfigFindManyArgs,
|
|
112
|
+
@ACAttrFilter() acAttrFilter,
|
|
113
|
+
): Promise<PageDto<TemporalTenantConfigEntity>> {
|
|
114
|
+
const { itemCount, entities } =
|
|
115
|
+
await this.temporalTenantConfigService.findMany(
|
|
116
|
+
body as Prisma.TemporalTenantConfigFindManyArgs,
|
|
117
|
+
);
|
|
118
|
+
const { skip = 0, take = 10 } =
|
|
119
|
+
body as Prisma.TemporalTenantConfigFindManyArgs;
|
|
120
|
+
const pageOptionsDto: PageOptionsDto = { skip, take };
|
|
121
|
+
const pageMetaDto = new PageMetaDto({ itemCount, pageOptionsDto });
|
|
122
|
+
|
|
123
|
+
return new PageDto(
|
|
124
|
+
entities.map(
|
|
125
|
+
(item) => new TemporalTenantConfigEntity(acAttrFilter(item)),
|
|
126
|
+
),
|
|
127
|
+
pageMetaDto,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@Post('/findUnique/:id')
|
|
132
|
+
@UseRoles({
|
|
133
|
+
resource: 'temporalTenantConfig',
|
|
134
|
+
action: 'read',
|
|
135
|
+
possession: 'any',
|
|
136
|
+
})
|
|
137
|
+
@ApiOkResponse({ type: TemporalTenantConfigEntity })
|
|
138
|
+
@ApiNotFoundResponse({ description: 'Cannot found this record.' })
|
|
139
|
+
async findUnique(
|
|
140
|
+
@Param('id', ParseIntPipe) id: number,
|
|
141
|
+
@Body() body: TemporalTenantConfigWhereSelect,
|
|
142
|
+
@ACAttrFilter() acAttrFilter,
|
|
143
|
+
) {
|
|
144
|
+
const item = await this.temporalTenantConfigService.findUnique(id, body);
|
|
145
|
+
if (!item) {
|
|
146
|
+
throw new NotFoundException();
|
|
147
|
+
}
|
|
148
|
+
return new TemporalTenantConfigEntity(acAttrFilter(item));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@Patch('updateUnique/:id')
|
|
152
|
+
@UseRoles({
|
|
153
|
+
resource: 'temporalTenantConfig',
|
|
154
|
+
action: 'update',
|
|
155
|
+
possession: 'any',
|
|
156
|
+
})
|
|
157
|
+
@ApiOkResponse({ type: TemporalTenantConfigEntity })
|
|
158
|
+
@ApiNotFoundResponse({ description: 'Cannot found this record.' })
|
|
159
|
+
async updateUnique(
|
|
160
|
+
@Param('id', ParseIntPipe) id: number,
|
|
161
|
+
@Body() body: UpdateTemporalTenantConfigDto,
|
|
162
|
+
@ACAttrFilter() acAttrFilter,
|
|
163
|
+
) {
|
|
164
|
+
const existing = await this.temporalTenantConfigService.findUnique(id, {
|
|
165
|
+
adminUrl: true,
|
|
166
|
+
adminToken: true,
|
|
167
|
+
namespace: true,
|
|
168
|
+
});
|
|
169
|
+
if (!existing) throw new NotFoundException();
|
|
170
|
+
|
|
171
|
+
const adminUrl = (body.adminUrl ?? existing.adminUrl) as string;
|
|
172
|
+
const adminToken = (body.adminToken ?? existing.adminToken) as string;
|
|
173
|
+
const namespace = (body.namespace ?? existing.namespace) as string;
|
|
174
|
+
|
|
175
|
+
const patchBody: Record<string, string> = {};
|
|
176
|
+
if (body.dbUrl) patchBody.dbUrl = body.dbUrl;
|
|
177
|
+
if (body.displayName) patchBody.displayName = body.displayName;
|
|
178
|
+
|
|
179
|
+
if (Object.keys(patchBody).length > 0) {
|
|
180
|
+
const res = await fetch(
|
|
181
|
+
`${adminUrl}/admin/tenants/${encodeURIComponent(namespace)}`,
|
|
182
|
+
{
|
|
183
|
+
method: 'PATCH',
|
|
184
|
+
headers: {
|
|
185
|
+
'Content-Type': 'application/json',
|
|
186
|
+
'X-Admin-Token': adminToken,
|
|
187
|
+
},
|
|
188
|
+
body: JSON.stringify(patchBody),
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
if (!res.ok) {
|
|
192
|
+
const detail = await res.text().catch(() => '');
|
|
193
|
+
throw new BadRequestException(
|
|
194
|
+
`shared temporal API 更新失败 (${res.status}): ${detail}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const item = await this.temporalTenantConfigService.updateUnique(
|
|
200
|
+
id,
|
|
201
|
+
acAttrFilter(body),
|
|
202
|
+
);
|
|
203
|
+
return new TemporalTenantConfigEntity(acAttrFilter(item));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@Delete('deleteUnique/:id')
|
|
207
|
+
@UseRoles({
|
|
208
|
+
resource: 'temporalTenantConfig',
|
|
209
|
+
action: 'delete',
|
|
210
|
+
possession: 'any',
|
|
211
|
+
})
|
|
212
|
+
@ApiOkResponse({ type: EffectedCount })
|
|
213
|
+
@ApiNotFoundResponse({ description: 'Cannot found this record.' })
|
|
214
|
+
async deleteUnique(@Param('id', ParseIntPipe) id: number) {
|
|
215
|
+
const res = await this.temporalTenantConfigService.deleteUnique(id);
|
|
216
|
+
if (!res.count) {
|
|
217
|
+
throw new NotFoundException();
|
|
218
|
+
}
|
|
219
|
+
return res;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@Post('upload')
|
|
223
|
+
@UseRoles({
|
|
224
|
+
resource: 'temporalTenantConfig',
|
|
225
|
+
action: 'create',
|
|
226
|
+
possession: 'any',
|
|
227
|
+
})
|
|
228
|
+
@ApiCreatedResponse({ type: UploadResponse })
|
|
229
|
+
@UseInterceptors(FileInterceptor('file'))
|
|
230
|
+
async uploadFile(@UploadedFile() file: Express.Multer.File) {
|
|
231
|
+
const fileName = await diskStorage(
|
|
232
|
+
file,
|
|
233
|
+
this.config.get('media')['rootDir'] + '/temporalTenantConfig/',
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
...pick(file, ['mimetype', 'originalname', 'size']),
|
|
238
|
+
url:
|
|
239
|
+
this.config.get('media')['address'] +
|
|
240
|
+
'/temporalTenantConfig/' +
|
|
241
|
+
fileName,
|
|
242
|
+
name: fileName,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { TemporalTenantConfigController } from './temporalTenantConfig.controller';
|
|
3
|
+
import { TemporalTenantConfigService } from './temporalTenantConfig.service';
|
|
4
|
+
|
|
5
|
+
@Module({
|
|
6
|
+
controllers: [TemporalTenantConfigController],
|
|
7
|
+
providers: [TemporalTenantConfigService],
|
|
8
|
+
exports: [TemporalTenantConfigService],
|
|
9
|
+
})
|
|
10
|
+
export class TemporalTenantConfigModule {}
|