nsgm-cli 2.1.21 → 2.1.23
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/README.md +40 -0
- package/client/components/Button.tsx +0 -2
- package/client/layout/index.tsx +2 -4
- package/client/utils/common.ts +12 -10
- package/client/utils/fetch.ts +1 -1
- package/client/utils/menu.tsx +0 -1
- package/client/utils/sso.ts +13 -3
- package/generation/client/utils/menu.tsx +0 -1
- package/jest.config.js +4 -4
- package/lib/generate_create.js +9 -0
- package/lib/generators/dataloader-generator.d.ts +12 -0
- package/lib/generators/dataloader-generator.js +221 -0
- package/lib/generators/resolver-generator.d.ts +2 -1
- package/lib/generators/resolver-generator.js +117 -24
- package/lib/generators/schema-generator.js +1 -0
- package/lib/index.js +11 -8
- package/lib/server/dataloaders/index.d.ts +38 -0
- package/lib/server/dataloaders/index.js +33 -0
- package/lib/server/dataloaders/template-dataloader.d.ts +48 -0
- package/lib/server/dataloaders/template-dataloader.js +131 -0
- package/lib/server/debug/dataloader-debug.d.ts +63 -0
- package/lib/server/debug/dataloader-debug.js +192 -0
- package/lib/server/graphql.js +9 -0
- package/lib/server/utils/dataloader-monitor.d.ts +87 -0
- package/lib/server/utils/dataloader-monitor.js +199 -0
- package/lib/tsconfig.build.tsbuildinfo +1 -1
- package/lib/utils.js +1 -1
- package/next-env.d.ts +1 -0
- package/next-i18next.config.js +7 -5
- package/next.config.js +34 -112
- package/package.json +6 -3
- package/pages/_app.tsx +7 -7
- package/pages/_document.tsx +0 -1
- package/pages/_error.tsx +0 -1
- package/pages/api/sso/ticketCheck.ts +117 -0
- package/pages/index.tsx +10 -3
- package/pages/login.tsx +41 -11
- package/pages/template/manage.tsx +16 -2
- package/server/apis/sso.js +22 -4
- package/server/modules/template/resolver.js +101 -21
- package/server/modules/template/schema.js +1 -0
package/README.md
CHANGED
|
@@ -383,6 +383,8 @@ npm run export
|
|
|
383
383
|
|
|
384
384
|
### Production Deployment
|
|
385
385
|
|
|
386
|
+
#### Local Deployment
|
|
387
|
+
|
|
386
388
|
```bash
|
|
387
389
|
# Start production server
|
|
388
390
|
npm start
|
|
@@ -391,6 +393,44 @@ npm start
|
|
|
391
393
|
pm2 start npm --name "nsgm-app" -- start
|
|
392
394
|
```
|
|
393
395
|
|
|
396
|
+
#### Vercel Deployment (Recommended)
|
|
397
|
+
|
|
398
|
+
NSGM CLI 完全支持 Vercel 部署,包括自动化 CI/CD 流程。
|
|
399
|
+
|
|
400
|
+
**快速开始:**
|
|
401
|
+
|
|
402
|
+
1. 推送项目到 GitHub
|
|
403
|
+
2. 访问 [Vercel Dashboard](https://vercel.com/dashboard)
|
|
404
|
+
3. 导入 GitHub 仓库
|
|
405
|
+
4. 配置环境变量(参考 `.env.vercel.example`)
|
|
406
|
+
5. 点击 "Deploy"
|
|
407
|
+
|
|
408
|
+
**详细指南:** 查看 [VERCEL_DEPLOYMENT.md](VERCEL_DEPLOYMENT.md)
|
|
409
|
+
|
|
410
|
+
**特性:**
|
|
411
|
+
|
|
412
|
+
- ✅ 自动 CI/CD 流程
|
|
413
|
+
- ✅ 预览环境(每个 PR)
|
|
414
|
+
- ✅ 自动 HTTPS
|
|
415
|
+
- ✅ 全球 CDN
|
|
416
|
+
- ✅ 无服务器函数
|
|
417
|
+
- ✅ 一键回滚
|
|
418
|
+
|
|
419
|
+
**环境变量配置:**
|
|
420
|
+
|
|
421
|
+
```
|
|
422
|
+
NODE_ENV=production
|
|
423
|
+
LOGIN_USERNAME=admin
|
|
424
|
+
LOGIN_PASSWORD_HASH=your_hash
|
|
425
|
+
DATABASE_URL=mysql://...
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**获取密码哈希:**
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
npm run generate-password yourPassword
|
|
432
|
+
```
|
|
433
|
+
|
|
394
434
|
## 🤝 Contributing
|
|
395
435
|
|
|
396
436
|
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
package/client/layout/index.tsx
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
import { useRouter } from "next/router";
|
|
14
14
|
import _ from "lodash";
|
|
15
15
|
import menuConfig, { getMenuConfig } from "@/utils/menu";
|
|
16
|
-
import getConfig from "next/config";
|
|
17
16
|
import { LogoutOutlined } from "@ant-design/icons";
|
|
18
17
|
import LanguageSwitcher from "@/components/LanguageSwitcher";
|
|
19
18
|
import { useTranslation } from "next-i18next";
|
|
@@ -33,9 +32,8 @@ interface MenuItem {
|
|
|
33
32
|
subMenus?: SubMenuItem[];
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const { prefix } = publicRuntimeConfig;
|
|
35
|
+
// 从环境变量获取 prefix
|
|
36
|
+
const prefix = process.env.NEXT_PUBLIC_PREFIX || "";
|
|
39
37
|
|
|
40
38
|
const getLocationKey = () => {
|
|
41
39
|
const result = {
|
package/client/utils/common.ts
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import getConfig from "next/config";
|
|
2
1
|
import _ from "lodash";
|
|
3
2
|
|
|
4
3
|
export const getLocalEnv = () => {
|
|
5
|
-
|
|
6
|
-
const { publicRuntimeConfig } = nextConfig;
|
|
7
|
-
let { env = "uat" } = publicRuntimeConfig;
|
|
4
|
+
let env = process.env.NEXT_PUBLIC_ENV || "uat";
|
|
8
5
|
env = env.toLowerCase();
|
|
9
6
|
return env;
|
|
10
7
|
};
|
|
11
8
|
|
|
12
9
|
export const getLocalApiPrefix = () => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
let
|
|
16
|
-
const
|
|
10
|
+
let protocol = process.env.NEXT_PUBLIC_PROTOCOL || "http";
|
|
11
|
+
let host = process.env.NEXT_PUBLIC_HOST || "localhost";
|
|
12
|
+
let port = process.env.NEXT_PUBLIC_PORT || "3000";
|
|
13
|
+
const prefix = process.env.NEXT_PUBLIC_PREFIX || "";
|
|
14
|
+
const isExport = process.env.NEXT_PUBLIC_IS_EXPORT === "true";
|
|
17
15
|
|
|
18
16
|
let localApiPrefix = "";
|
|
19
17
|
|
|
@@ -27,12 +25,16 @@ export const getLocalApiPrefix = () => {
|
|
|
27
25
|
protocol = protocol.split(":")[0];
|
|
28
26
|
}
|
|
29
27
|
host = location.hostname;
|
|
30
|
-
port = location.port
|
|
28
|
+
port = location.port;
|
|
31
29
|
}
|
|
32
30
|
// 服务器端:直接使用配置中的值,无需额外处理
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
// 只在非标准端口时才添加端口号
|
|
34
|
+
const isStandardPort = (protocol === "https" && port === "443") || (protocol === "http" && port === "80") || !port;
|
|
35
|
+
const portStr = isStandardPort ? "" : `:${port}`;
|
|
36
|
+
|
|
37
|
+
localApiPrefix = `${protocol}://${host}${portStr}${prefix}`;
|
|
36
38
|
return localApiPrefix;
|
|
37
39
|
};
|
|
38
40
|
|
package/client/utils/fetch.ts
CHANGED
|
@@ -188,7 +188,7 @@ export const getLocalGraphql = async (query: string, variables: any = {}) => {
|
|
|
188
188
|
};
|
|
189
189
|
|
|
190
190
|
const retryResponse = await axios.post(
|
|
191
|
-
|
|
191
|
+
`/api/graphql`,
|
|
192
192
|
{ query, variables },
|
|
193
193
|
{ headers: retryHeaders, withCredentials: true }
|
|
194
194
|
);
|
package/client/utils/menu.tsx
CHANGED
package/client/utils/sso.ts
CHANGED
|
@@ -210,22 +210,32 @@ export const directLogin = (userName: string, userPassword: string, callback: an
|
|
|
210
210
|
// 使用 encodeURIComponent 处理可能的特殊字符,然后再进行 Base64 编码
|
|
211
211
|
const safeStr = handleXSS(`${userName},${userPassword}`);
|
|
212
212
|
const encodedName = btoa(encodeURIComponent(safeStr));
|
|
213
|
-
const
|
|
213
|
+
const apiPrefix = getLocalApiPrefix();
|
|
214
|
+
const url = `${apiPrefix}/rest/sso/ticketCheck?ticket=XXX&name=${encodedName}`;
|
|
215
|
+
|
|
216
|
+
console.warn("[Login] Login URL:", url);
|
|
217
|
+
console.warn("[Login] Username:", userName);
|
|
214
218
|
|
|
215
219
|
return fetch(url)
|
|
216
|
-
.then((response) =>
|
|
220
|
+
.then((response) => {
|
|
221
|
+
console.warn("[Login] Response status:", response.status);
|
|
222
|
+
return response.json();
|
|
223
|
+
})
|
|
217
224
|
.then((data) => {
|
|
225
|
+
console.warn("[Login] Response data:", data);
|
|
218
226
|
if (data && data.returnCode === 0) {
|
|
219
227
|
// 登录成功,设置cookie
|
|
220
228
|
if (typeof window !== "undefined") {
|
|
229
|
+
console.warn("[Login] Login successful");
|
|
221
230
|
storeLogin(data.cookieValue, data.cookieExpire, data.userAttr, callback);
|
|
222
231
|
return { success: true };
|
|
223
232
|
}
|
|
224
233
|
}
|
|
234
|
+
console.warn("[Login] Login failed, returnCode:", data?.returnCode, "message:", data?.message);
|
|
225
235
|
return { success: false, message: "用户名或密码错误" };
|
|
226
236
|
})
|
|
227
237
|
.catch((error) => {
|
|
228
|
-
console.
|
|
238
|
+
console.warn("[Login] Login request failed:", error);
|
|
229
239
|
return { success: false, message: "登录请求失败,请稍后重试" };
|
|
230
240
|
});
|
|
231
241
|
};
|
package/jest.config.js
CHANGED
|
@@ -37,10 +37,10 @@ module.exports = {
|
|
|
37
37
|
coverageReporters: ['json', 'lcov', 'text', 'clover', 'html'],
|
|
38
38
|
coverageThreshold: {
|
|
39
39
|
global: {
|
|
40
|
-
branches:
|
|
41
|
-
functions:
|
|
42
|
-
lines:
|
|
43
|
-
statements:
|
|
40
|
+
branches: 2,
|
|
41
|
+
functions: 3,
|
|
42
|
+
lines: 5,
|
|
43
|
+
statements: 5
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
}
|
package/lib/generate_create.js
CHANGED
|
@@ -48,6 +48,7 @@ const resolver_generator_1 = require("./generators/resolver-generator");
|
|
|
48
48
|
const service_generator_1 = require("./generators/service-generator");
|
|
49
49
|
const page_generator_1 = require("./generators/page-generator");
|
|
50
50
|
const file_generator_1 = require("./generators/file-generator");
|
|
51
|
+
const dataloader_generator_1 = require("./generators/dataloader-generator");
|
|
51
52
|
// 常量定义
|
|
52
53
|
const TEMPLATE_FILES = {
|
|
53
54
|
reduxActions: "redux/template/manage/actions.ts",
|
|
@@ -207,6 +208,7 @@ const generateDynamicFiles = (controller, action, paths, fields, dictionary) =>
|
|
|
207
208
|
const resolverGenerator = new resolver_generator_1.ResolverGenerator(controller, action, fields);
|
|
208
209
|
const serviceGenerator = new service_generator_1.ServiceGenerator(controller, action, fields);
|
|
209
210
|
const pageGenerator = new page_generator_1.PageGenerator(controller, action, fields);
|
|
211
|
+
const dataLoaderGenerator = new dataloader_generator_1.DataLoaderGenerator(controller, action, fields);
|
|
210
212
|
// 根据 dictionary 确定文件生成器的项目路径
|
|
211
213
|
const projectPath = !dictionary || dictionary === "." ? "." : path_1.default.join(constants_1.destFolder, dictionary);
|
|
212
214
|
const fileGenerator = new file_generator_1.FileGenerator(projectPath);
|
|
@@ -216,6 +218,11 @@ const generateDynamicFiles = (controller, action, paths, fields, dictionary) =>
|
|
|
216
218
|
fs_1.default.writeFileSync(paths.destServerModulesResolver, resolverGenerator.generate());
|
|
217
219
|
fs_1.default.writeFileSync(paths.destClientAction, serviceGenerator.generate());
|
|
218
220
|
fs_1.default.writeFileSync(paths.destPagesAction, pageGenerator.generate());
|
|
221
|
+
// 生成 DataLoader 文件
|
|
222
|
+
const dataLoaderPath = (0, path_1.resolve)(`${projectPath}/server/dataloaders/${controller}-dataloader.ts`);
|
|
223
|
+
(0, utils_1.mkdirSync)(path_1.default.dirname(dataLoaderPath));
|
|
224
|
+
fs_1.default.writeFileSync(dataLoaderPath, dataLoaderGenerator.generate());
|
|
225
|
+
console.log(`🚀 已生成 DataLoader 文件: ${dataLoaderPath}`);
|
|
219
226
|
// 生成多语言文件
|
|
220
227
|
fileGenerator.generateI18nFiles(controller, action, fields);
|
|
221
228
|
};
|
|
@@ -302,6 +309,8 @@ const createFiles = (controller, action, dictionary, fields) => {
|
|
|
302
309
|
paths.destClientServiceController,
|
|
303
310
|
paths.destClientStyledController,
|
|
304
311
|
paths.destServerModulesController,
|
|
312
|
+
// 添加 DataLoader 目录
|
|
313
|
+
(0, path_1.resolve)(`${getDestPath(constants_1.destServerPath)}/dataloaders`),
|
|
305
314
|
];
|
|
306
315
|
createDirectoryStructure(basePaths);
|
|
307
316
|
console.log("Directory structure created");
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaseGenerator } from "./base-generator";
|
|
2
|
+
/**
|
|
3
|
+
* DataLoader生成器
|
|
4
|
+
* 自动生成对应的 DataLoader 文件
|
|
5
|
+
*/
|
|
6
|
+
export declare class DataLoaderGenerator extends BaseGenerator {
|
|
7
|
+
generate(): string;
|
|
8
|
+
/**
|
|
9
|
+
* 生成外键 DataLoader
|
|
10
|
+
*/
|
|
11
|
+
private generateForeignKeyLoaders;
|
|
12
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DataLoaderGenerator = void 0;
|
|
4
|
+
const base_generator_1 = require("./base-generator");
|
|
5
|
+
/**
|
|
6
|
+
* DataLoader生成器
|
|
7
|
+
* 自动生成对应的 DataLoader 文件
|
|
8
|
+
*/
|
|
9
|
+
class DataLoaderGenerator extends base_generator_1.BaseGenerator {
|
|
10
|
+
generate() {
|
|
11
|
+
const capitalizedController = this.getCapitalizedController();
|
|
12
|
+
const selectFields = this.fields.map((f) => f.name).join(", ");
|
|
13
|
+
// const searchableFields = this.getSearchableFields(); // 暂时注释掉未使用的变量
|
|
14
|
+
return `import DataLoader from 'dataloader';
|
|
15
|
+
import { executeQuery } from '../utils/common';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ${capitalizedController} DataLoader
|
|
19
|
+
* 针对 ${this.controller} 表的批量数据加载器,解决 N+1 查询问题
|
|
20
|
+
*/
|
|
21
|
+
export class ${capitalizedController}DataLoader {
|
|
22
|
+
// 按 ID 批量加载 ${this.controller}
|
|
23
|
+
public readonly byId: DataLoader<number, any>;
|
|
24
|
+
|
|
25
|
+
// 按名称批量加载 ${this.controller}
|
|
26
|
+
public readonly byName: DataLoader<string, any>;
|
|
27
|
+
|
|
28
|
+
// 按名称模糊搜索 ${this.controller}
|
|
29
|
+
public readonly searchByName: DataLoader<string, any[]>;
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
// 按 ID 批量加载
|
|
33
|
+
this.byId = new DataLoader(
|
|
34
|
+
async (ids: readonly number[]) => {
|
|
35
|
+
try {
|
|
36
|
+
console.log(\`🔍 DataLoader: 批量加载 \${ids.length} 个 ${this.controller} by ID\`);
|
|
37
|
+
|
|
38
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
39
|
+
const sql = \`SELECT ${selectFields} FROM ${this.controller} WHERE id IN (\${placeholders})\`;
|
|
40
|
+
|
|
41
|
+
const results = await executeQuery(sql, [...ids]);
|
|
42
|
+
|
|
43
|
+
// 确保返回顺序与输入 keys 一致,未找到的返回 null
|
|
44
|
+
return ids.map(id =>
|
|
45
|
+
results.find((row: any) => row.id === id) || null
|
|
46
|
+
);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('DataLoader byId 批量加载失败:', error);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
cache: true,
|
|
54
|
+
maxBatchSize: 100,
|
|
55
|
+
batchScheduleFn: callback => setTimeout(callback, 10), // 10ms 内的请求合并
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// 按名称批量加载
|
|
60
|
+
this.byName = new DataLoader(
|
|
61
|
+
async (names: readonly string[]) => {
|
|
62
|
+
try {
|
|
63
|
+
console.log(\`🔍 DataLoader: 批量加载 \${names.length} 个 ${this.controller} by name\`);
|
|
64
|
+
|
|
65
|
+
const placeholders = names.map(() => '?').join(',');
|
|
66
|
+
const sql = \`SELECT ${selectFields} FROM ${this.controller} WHERE name IN (\${placeholders})\`;
|
|
67
|
+
|
|
68
|
+
const results = await executeQuery(sql, [...names]);
|
|
69
|
+
|
|
70
|
+
// 确保返回顺序与输入 keys 一致
|
|
71
|
+
return names.map(name =>
|
|
72
|
+
results.find((row: any) => row.name === name) || null
|
|
73
|
+
);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('DataLoader byName 批量加载失败:', error);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
cache: true,
|
|
81
|
+
maxBatchSize: 50,
|
|
82
|
+
batchScheduleFn: callback => setTimeout(callback, 10),
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// 按名称模糊搜索(返回数组)
|
|
87
|
+
this.searchByName = new DataLoader(
|
|
88
|
+
async (searchTerms: readonly string[]) => {
|
|
89
|
+
try {
|
|
90
|
+
console.log(\`🔍 DataLoader: 批量搜索 \${searchTerms.length} 个关键词\`);
|
|
91
|
+
|
|
92
|
+
// 对于搜索,我们需要为每个搜索词执行独立的查询
|
|
93
|
+
const results = await Promise.all(
|
|
94
|
+
searchTerms.map(async (term) => {
|
|
95
|
+
const sql = 'SELECT ${selectFields} FROM ${this.controller} WHERE name LIKE ?';
|
|
96
|
+
return executeQuery(sql, [\`%\${term}%\`]);
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return results;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error('DataLoader searchByName 批量搜索失败:', error);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
cache: true,
|
|
108
|
+
maxBatchSize: 20, // 搜索请求较少,降低批量大小
|
|
109
|
+
batchScheduleFn: callback => setTimeout(callback, 20), // 稍长的等待时间
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
${this.generateForeignKeyLoaders()}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 清除所有缓存
|
|
118
|
+
*/
|
|
119
|
+
clearAll(): void {
|
|
120
|
+
this.byId.clearAll();
|
|
121
|
+
this.byName.clearAll();
|
|
122
|
+
this.searchByName.clearAll();
|
|
123
|
+
console.log('🧹 ${capitalizedController} DataLoader 缓存已清空');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 清除特定 ID 的缓存
|
|
128
|
+
*/
|
|
129
|
+
clearById(id: number): void {
|
|
130
|
+
this.byId.clear(id);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 清除特定名称的缓存
|
|
135
|
+
*/
|
|
136
|
+
clearByName(name: string): void {
|
|
137
|
+
this.byName.clear(name);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 预加载数据到缓存
|
|
142
|
+
*/
|
|
143
|
+
prime(id: number, data: any): void {
|
|
144
|
+
this.byId.prime(id, data);
|
|
145
|
+
if (data && data.name) {
|
|
146
|
+
this.byName.prime(data.name, data);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 获取缓存统计信息
|
|
152
|
+
*/
|
|
153
|
+
getStats() {
|
|
154
|
+
return {
|
|
155
|
+
byId: {
|
|
156
|
+
cacheMap: this.byId.cacheMap?.size || 0,
|
|
157
|
+
name: '${capitalizedController}.byId'
|
|
158
|
+
},
|
|
159
|
+
byName: {
|
|
160
|
+
cacheMap: this.byName.cacheMap?.size || 0,
|
|
161
|
+
name: '${capitalizedController}.byName'
|
|
162
|
+
},
|
|
163
|
+
searchByName: {
|
|
164
|
+
cacheMap: this.searchByName.cacheMap?.size || 0,
|
|
165
|
+
name: '${capitalizedController}.searchByName'
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 创建 ${capitalizedController} DataLoader 实例
|
|
173
|
+
*/
|
|
174
|
+
export function create${capitalizedController}DataLoader(): ${capitalizedController}DataLoader {
|
|
175
|
+
return new ${capitalizedController}DataLoader();
|
|
176
|
+
}`;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 生成外键 DataLoader
|
|
180
|
+
*/
|
|
181
|
+
generateForeignKeyLoaders() {
|
|
182
|
+
const foreignKeys = this.fields.filter((f) => f.name.endsWith("_id") && f.name !== "id");
|
|
183
|
+
if (foreignKeys.length === 0) {
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
return foreignKeys
|
|
187
|
+
.map((fk) => {
|
|
188
|
+
const relatedTable = fk.name.replace("_id", "");
|
|
189
|
+
const capitalizedRelated = relatedTable.charAt(0).toUpperCase() + relatedTable.slice(1);
|
|
190
|
+
return `
|
|
191
|
+
// 按 ${fk.name} 批量加载相关的 ${this.controller}
|
|
192
|
+
this.by${capitalizedRelated}Id = new DataLoader(
|
|
193
|
+
async (${fk.name}s: readonly number[]) => {
|
|
194
|
+
try {
|
|
195
|
+
console.log(\`🔍 DataLoader: 批量加载 \${${fk.name}s.length} 个 ${this.controller} by ${fk.name}\`);
|
|
196
|
+
|
|
197
|
+
const placeholders = ${fk.name}s.map(() => '?').join(',');
|
|
198
|
+
const sql = \`SELECT ${this.fields.map((f) => f.name).join(", ")} FROM ${this.controller} WHERE ${fk.name} IN (\${placeholders})\`;
|
|
199
|
+
|
|
200
|
+
const results = await executeQuery(sql, [...${fk.name}s]);
|
|
201
|
+
|
|
202
|
+
// 按外键分组
|
|
203
|
+
return ${fk.name}s.map(${fk.name} =>
|
|
204
|
+
results.filter((row: any) => row.${fk.name} === ${fk.name})
|
|
205
|
+
);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error('DataLoader by${capitalizedRelated}Id 批量加载失败:', error);
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
cache: true,
|
|
213
|
+
maxBatchSize: 50,
|
|
214
|
+
batchScheduleFn: callback => setTimeout(callback, 10),
|
|
215
|
+
}
|
|
216
|
+
);`;
|
|
217
|
+
})
|
|
218
|
+
.join("\n");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
exports.DataLoaderGenerator = DataLoaderGenerator;
|
|
@@ -10,5 +10,6 @@ export declare class ResolverGenerator extends BaseGenerator {
|
|
|
10
10
|
private generateUpdateValidation;
|
|
11
11
|
private generateUpdateValues;
|
|
12
12
|
private generateBatchReturnObject;
|
|
13
|
-
private
|
|
13
|
+
private generateDataLoaderSearchLogic;
|
|
14
|
+
private generateNewRecordObject;
|
|
14
15
|
}
|