@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
@@ -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
- UseGuards,
13
+ UnauthorizedException,
13
14
  } from '@nestjs/common';
14
15
  import { ApiTags } from '@nestjs/swagger';
15
16
  import { PrismaService } from 'nestjs-prisma';
16
- import AuthGuard, { AllowUnauthorizedRequest } from '../../lib/auth.guard';
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 {}