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
|
@@ -44,34 +44,58 @@ module.exports = {
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
|
|
47
|
-
// 根据ID获取${this.controller}
|
|
48
|
-
${this.controller}Get: async ({ id }) => {
|
|
47
|
+
// 根据ID获取${this.controller} - 使用 DataLoader 优化
|
|
48
|
+
${this.controller}Get: async ({ id }, context) => {
|
|
49
49
|
try {
|
|
50
50
|
const validId = validateId(id);
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
const values = [validId];
|
|
54
|
-
|
|
55
|
-
console.log('根据ID查询${this.controller}:', { sql, values });
|
|
52
|
+
console.log('🚀 使用 DataLoader 根据ID查询${this.controller}:', { id: validId });
|
|
56
53
|
|
|
57
|
-
|
|
54
|
+
// 使用 DataLoader 批量加载,自动去重和缓存
|
|
55
|
+
const result = await context.dataloaders.${this.controller}.byId.load(validId);
|
|
58
56
|
|
|
59
|
-
if (
|
|
57
|
+
if (!result) {
|
|
60
58
|
throw new Error(\`ID为 \${validId} 的${this.controller}不存在\`);
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
return
|
|
61
|
+
return result;
|
|
64
62
|
} catch (error) {
|
|
65
63
|
console.error('获取${this.controller}失败:', error.message);
|
|
66
64
|
throw error;
|
|
67
65
|
}
|
|
68
66
|
},
|
|
69
67
|
|
|
70
|
-
//
|
|
71
|
-
${this.controller}
|
|
68
|
+
// 批量获取${this.controller} - 新增方法,展示 DataLoader 批量能力
|
|
69
|
+
${this.controller}BatchGet: async ({ ids }, context) => {
|
|
70
|
+
try {
|
|
71
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
72
|
+
throw new Error('ID列表不能为空');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 验证所有ID
|
|
76
|
+
const validIds = ids.map(id => validateId(id));
|
|
77
|
+
|
|
78
|
+
console.log('🚀 使用 DataLoader 批量查询${this.controller}:', { ids: validIds });
|
|
79
|
+
|
|
80
|
+
// DataLoader 自动批量处理,一次查询获取所有数据
|
|
81
|
+
const results = await context.dataloaders.${this.controller}.byId.loadMany(validIds);
|
|
82
|
+
|
|
83
|
+
// 过滤掉 null 结果(未找到的记录)
|
|
84
|
+
return results.filter(result => result !== null && !(result instanceof Error));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('批量获取${this.controller}失败:', error.message);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
// 搜索${this.controller}(分页)- 使用 DataLoader 优化搜索
|
|
92
|
+
${this.controller}Search: async ({ page = 0, pageSize = 10, data = {} }, context) => {
|
|
72
93
|
try {
|
|
73
94
|
const { page: validPage, pageSize: validPageSize } = validatePagination(page, pageSize);
|
|
74
95
|
|
|
96
|
+
${this.generateDataLoaderSearchLogic(searchableFields)}
|
|
97
|
+
|
|
98
|
+
// 原始查询方式(作为备用)
|
|
75
99
|
const values = [];
|
|
76
100
|
const countValues = [];
|
|
77
101
|
|
|
@@ -83,7 +107,7 @@ ${searchConditions}
|
|
|
83
107
|
|
|
84
108
|
values.push(validPageSize, validPage * validPageSize);
|
|
85
109
|
|
|
86
|
-
console.log('搜索${this.controller}
|
|
110
|
+
console.log('搜索${this.controller}(备用查询):', { sql, values, countSql, countValues });
|
|
87
111
|
|
|
88
112
|
return await executePaginatedQuery(sql, countSql, values, countValues);
|
|
89
113
|
} catch (error) {
|
|
@@ -92,8 +116,8 @@ ${searchConditions}
|
|
|
92
116
|
}
|
|
93
117
|
},
|
|
94
118
|
|
|
95
|
-
// 添加${this.controller}
|
|
96
|
-
${this.controller}Add: async ({ data }) => {
|
|
119
|
+
// 添加${this.controller} - 添加 DataLoader 缓存预加载
|
|
120
|
+
${this.controller}Add: async ({ data }, context) => {
|
|
97
121
|
try {
|
|
98
122
|
${this.generateNewValidationCalls(insertFields)}
|
|
99
123
|
|
|
@@ -103,7 +127,16 @@ ${this.generateNewValidationCalls(insertFields)}
|
|
|
103
127
|
console.log('添加${this.controller}:', { sql, values });
|
|
104
128
|
|
|
105
129
|
const results = await executeQuery(sql, values);
|
|
106
|
-
|
|
130
|
+
const insertId = results.insertId;
|
|
131
|
+
|
|
132
|
+
// 预加载新数据到 DataLoader 缓存
|
|
133
|
+
if (insertId && context?.dataloaders?.${this.controller}) {
|
|
134
|
+
const newRecord = { id: insertId, ${this.generateNewRecordObject(insertFields)} };
|
|
135
|
+
context.dataloaders.${this.controller}.prime(insertId, newRecord);
|
|
136
|
+
console.log('🚀 新${this.controller}已预加载到 DataLoader 缓存:', newRecord);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return insertId;
|
|
107
140
|
} catch (error) {
|
|
108
141
|
console.error('添加${this.controller}失败:', error.message);
|
|
109
142
|
throw error;
|
|
@@ -129,7 +162,7 @@ ${this.generateBatchValidation(insertFields)}
|
|
|
129
162
|
|
|
130
163
|
const placeholders = validatedDatas.map(() => '(${insertPlaceholders})').join(',');
|
|
131
164
|
const sql = \`INSERT INTO ${this.controller} (${insertFieldNames}) VALUES \${placeholders}\`;
|
|
132
|
-
const values = validatedDatas.flatMap(data => [${
|
|
165
|
+
const values = validatedDatas.flatMap(data => [${insertFields.map((f) => `data.${f.name}`).join(", ")}]);
|
|
133
166
|
|
|
134
167
|
console.log('批量添加${this.controller}:', { sql, values });
|
|
135
168
|
|
|
@@ -141,8 +174,8 @@ ${this.generateBatchValidation(insertFields)}
|
|
|
141
174
|
}
|
|
142
175
|
},
|
|
143
176
|
|
|
144
|
-
// 更新${this.controller}
|
|
145
|
-
${this.controller}Update: async ({ id, data }) => {
|
|
177
|
+
// 更新${this.controller} - 添加 DataLoader 缓存清理
|
|
178
|
+
${this.controller}Update: async ({ id, data }, context) => {
|
|
146
179
|
try {
|
|
147
180
|
const validId = validateId(id);
|
|
148
181
|
|
|
@@ -163,6 +196,12 @@ ${this.generateUpdateValidation(insertFields)}
|
|
|
163
196
|
throw new Error(\`ID为 \${validId} 的${this.controller}不存在\`);
|
|
164
197
|
}
|
|
165
198
|
|
|
199
|
+
// 清除 DataLoader 缓存,确保下次查询获取最新数据
|
|
200
|
+
if (context?.dataloaders?.${this.controller}) {
|
|
201
|
+
context.dataloaders.${this.controller}.clearById(validId);
|
|
202
|
+
console.log('🧹 已清除 DataLoader 缓存:', { id: validId });
|
|
203
|
+
}
|
|
204
|
+
|
|
166
205
|
return true;
|
|
167
206
|
} catch (error) {
|
|
168
207
|
console.error('更新${this.controller}失败:', error.message);
|
|
@@ -170,8 +209,8 @@ ${this.generateUpdateValidation(insertFields)}
|
|
|
170
209
|
}
|
|
171
210
|
},
|
|
172
211
|
|
|
173
|
-
// 删除${this.controller}
|
|
174
|
-
${this.controller}Delete: async ({ id }) => {
|
|
212
|
+
// 删除${this.controller} - 添加 DataLoader 缓存清理
|
|
213
|
+
${this.controller}Delete: async ({ id }, context) => {
|
|
175
214
|
try {
|
|
176
215
|
const validId = validateId(id);
|
|
177
216
|
|
|
@@ -186,6 +225,12 @@ ${this.generateUpdateValidation(insertFields)}
|
|
|
186
225
|
throw new Error(\`ID为 \${validId} 的${this.controller}不存在\`);
|
|
187
226
|
}
|
|
188
227
|
|
|
228
|
+
// 清除 DataLoader 缓存
|
|
229
|
+
if (context?.dataloaders?.${this.controller}) {
|
|
230
|
+
context.dataloaders.${this.controller}.clearById(validId);
|
|
231
|
+
console.log('🧹 已清除 DataLoader 缓存:', { id: validId });
|
|
232
|
+
}
|
|
233
|
+
|
|
189
234
|
return true;
|
|
190
235
|
} catch (error) {
|
|
191
236
|
console.error('删除${this.controller}失败:', error.message);
|
|
@@ -193,8 +238,8 @@ ${this.generateUpdateValidation(insertFields)}
|
|
|
193
238
|
}
|
|
194
239
|
},
|
|
195
240
|
|
|
196
|
-
// 批量删除${this.controller}
|
|
197
|
-
${this.controller}BatchDelete: async ({ ids }) => {
|
|
241
|
+
// 批量删除${this.controller} - 添加 DataLoader 缓存清理
|
|
242
|
+
${this.controller}BatchDelete: async ({ ids }, context) => {
|
|
198
243
|
try {
|
|
199
244
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
200
245
|
throw new Error('批量删除的ID列表不能为空');
|
|
@@ -220,6 +265,14 @@ ${this.generateUpdateValidation(insertFields)}
|
|
|
220
265
|
throw new Error('没有找到要删除的${this.controller}');
|
|
221
266
|
}
|
|
222
267
|
|
|
268
|
+
// 批量清除 DataLoader 缓存
|
|
269
|
+
if (context?.dataloaders?.${this.controller}) {
|
|
270
|
+
validIds.forEach(id => {
|
|
271
|
+
context.dataloaders.${this.controller}.clearById(id);
|
|
272
|
+
});
|
|
273
|
+
console.log('🧹 已批量清除 DataLoader 缓存:', { ids: validIds });
|
|
274
|
+
}
|
|
275
|
+
|
|
223
276
|
return true;
|
|
224
277
|
} catch (error) {
|
|
225
278
|
console.error('批量删除${this.controller}失败:', error.message);
|
|
@@ -335,8 +388,48 @@ ${this.generateUpdateValidation(insertFields)}
|
|
|
335
388
|
})
|
|
336
389
|
.join(", ");
|
|
337
390
|
}
|
|
338
|
-
generateBatchInsertValues(insertFields) {
|
|
339
|
-
|
|
391
|
+
// private generateBatchInsertValues(insertFields: any[]): string {
|
|
392
|
+
// return insertFields.map((f) => `data.${f.name}`).join(", ");
|
|
393
|
+
// }
|
|
394
|
+
generateDataLoaderSearchLogic(searchableFields) {
|
|
395
|
+
if (searchableFields.length === 0)
|
|
396
|
+
return "";
|
|
397
|
+
const nameField = searchableFields.find((f) => f.name === "name");
|
|
398
|
+
if (!nameField)
|
|
399
|
+
return "";
|
|
400
|
+
return `// 如果有名称搜索,尝试使用 DataLoader 搜索缓存
|
|
401
|
+
if (data.name && data.name.trim() !== '') {
|
|
402
|
+
console.log('🚀 使用 DataLoader 搜索${this.controller}:', { searchTerm: data.name.trim() });
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
// 使用 DataLoader 进行搜索(这里会缓存搜索结果)
|
|
406
|
+
const searchResults = await context.dataloaders.${this.controller}.searchByName.load(data.name.trim());
|
|
407
|
+
|
|
408
|
+
// 手动分页处理
|
|
409
|
+
const totalCounts = searchResults.length;
|
|
410
|
+
const startIndex = validPage * validPageSize;
|
|
411
|
+
const endIndex = startIndex + validPageSize;
|
|
412
|
+
const items = searchResults.slice(startIndex, endIndex);
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
totalCounts,
|
|
416
|
+
items
|
|
417
|
+
};
|
|
418
|
+
} catch (dataLoaderError) {
|
|
419
|
+
console.warn('DataLoader 搜索失败,回退到直接查询:', dataLoaderError.message);
|
|
420
|
+
// 如果 DataLoader 失败,回退到原始查询方式
|
|
421
|
+
}
|
|
422
|
+
}`;
|
|
423
|
+
}
|
|
424
|
+
generateNewRecordObject(insertFields) {
|
|
425
|
+
return insertFields
|
|
426
|
+
.map((f) => {
|
|
427
|
+
if (f.type === "integer") {
|
|
428
|
+
return `${f.name}: valid${f.name.charAt(0).toUpperCase() + f.name.slice(1)}`;
|
|
429
|
+
}
|
|
430
|
+
return `${f.name}: data.${f.name}`;
|
|
431
|
+
})
|
|
432
|
+
.join(", ");
|
|
340
433
|
}
|
|
341
434
|
}
|
|
342
435
|
exports.ResolverGenerator = ResolverGenerator;
|
|
@@ -23,6 +23,7 @@ class SchemaGenerator extends base_generator_1.BaseGenerator {
|
|
|
23
23
|
query: \`
|
|
24
24
|
${this.controller}(page: Int, pageSize: Int): ${pluralTypeName}
|
|
25
25
|
${this.controller}Get(id: Int): ${capitalizedTypeName}
|
|
26
|
+
${this.controller}BatchGet(ids: [Int]): [${capitalizedTypeName}]
|
|
26
27
|
${this.controller}Search(page: Int, pageSize: Int, data: ${capitalizedTypeName}SearchInput): ${pluralTypeName}
|
|
27
28
|
\`,
|
|
28
29
|
mutation: \`
|
package/lib/index.js
CHANGED
|
@@ -19,7 +19,6 @@ const path_1 = __importDefault(require("path"));
|
|
|
19
19
|
const body_parser_1 = __importDefault(require("body-parser"));
|
|
20
20
|
const express_fileupload_1 = __importDefault(require("express-fileupload"));
|
|
21
21
|
const graphql_1 = __importDefault(require("./server/graphql"));
|
|
22
|
-
const config_1 = __importDefault(require("next/config"));
|
|
23
22
|
const cors_1 = __importDefault(require("cors"));
|
|
24
23
|
const express_session_1 = __importDefault(require("express-session"));
|
|
25
24
|
const csrf_1 = require("./server/csrf");
|
|
@@ -121,9 +120,10 @@ const startExpress = (options, callback, command = "dev") => {
|
|
|
121
120
|
server.use((0, express_fileupload_1.default)());
|
|
122
121
|
server.use("/static", express_1.default.static(path_1.default.join(__dirname, "public")));
|
|
123
122
|
server.use("/graphql", (0, graphql_1.default)(command));
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
const
|
|
123
|
+
// 从环境变量读取配置
|
|
124
|
+
const host = process.env.NEXT_PUBLIC_HOST || "localhost";
|
|
125
|
+
const port = process.env.NEXT_PUBLIC_PORT || "3000";
|
|
126
|
+
const prefix = process.env.NEXT_PUBLIC_PREFIX || "";
|
|
127
127
|
// 提供 CSRF token 的端点
|
|
128
128
|
server.get("/csrf-token", csrf_1.getCSRFToken);
|
|
129
129
|
if (prefix !== "") {
|
|
@@ -148,7 +148,10 @@ const startExpress = (options, callback, command = "dev") => {
|
|
|
148
148
|
};
|
|
149
149
|
exports.startExpress = startExpress;
|
|
150
150
|
// 使用新的 CLI 架构
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
// 仅在非测试环境中执行 CLI
|
|
152
|
+
if (process.env.NODE_ENV !== "test") {
|
|
153
|
+
(0, cli_1.runCli)().catch((error) => {
|
|
154
|
+
console.error("CLI 执行失败:", error);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { TemplateDataLoader } from "./template-dataloader";
|
|
2
|
+
/**
|
|
3
|
+
* DataLoader 上下文接口
|
|
4
|
+
*/
|
|
5
|
+
export interface DataLoaderContext extends Record<string, unknown> {
|
|
6
|
+
dataloaders: {
|
|
7
|
+
template: TemplateDataLoader;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 创建 DataLoader 上下文
|
|
12
|
+
* 每个 GraphQL 请求都会创建新的 DataLoader 实例,确保请求隔离
|
|
13
|
+
*/
|
|
14
|
+
export declare function createDataLoaderContext(): DataLoaderContext;
|
|
15
|
+
/**
|
|
16
|
+
* DataLoader 统计信息
|
|
17
|
+
*/
|
|
18
|
+
export declare function getDataLoaderStats(context: DataLoaderContext): {
|
|
19
|
+
template: {
|
|
20
|
+
byId: {
|
|
21
|
+
cacheMap: any;
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
24
|
+
byName: {
|
|
25
|
+
cacheMap: any;
|
|
26
|
+
name: string;
|
|
27
|
+
};
|
|
28
|
+
searchByName: {
|
|
29
|
+
cacheMap: any;
|
|
30
|
+
name: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
timestamp: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* 清除所有 DataLoader 缓存
|
|
37
|
+
*/
|
|
38
|
+
export declare function clearAllDataLoaderCache(context: DataLoaderContext): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createDataLoaderContext = createDataLoaderContext;
|
|
4
|
+
exports.getDataLoaderStats = getDataLoaderStats;
|
|
5
|
+
exports.clearAllDataLoaderCache = clearAllDataLoaderCache;
|
|
6
|
+
const template_dataloader_1 = require("./template-dataloader");
|
|
7
|
+
/**
|
|
8
|
+
* 创建 DataLoader 上下文
|
|
9
|
+
* 每个 GraphQL 请求都会创建新的 DataLoader 实例,确保请求隔离
|
|
10
|
+
*/
|
|
11
|
+
function createDataLoaderContext() {
|
|
12
|
+
return {
|
|
13
|
+
dataloaders: {
|
|
14
|
+
template: (0, template_dataloader_1.createTemplateDataLoader)(),
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* DataLoader 统计信息
|
|
20
|
+
*/
|
|
21
|
+
function getDataLoaderStats(context) {
|
|
22
|
+
return {
|
|
23
|
+
template: context.dataloaders.template.getStats(),
|
|
24
|
+
timestamp: new Date().toISOString(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 清除所有 DataLoader 缓存
|
|
29
|
+
*/
|
|
30
|
+
function clearAllDataLoaderCache(context) {
|
|
31
|
+
context.dataloaders.template.clearAll();
|
|
32
|
+
console.log("🧹 所有 DataLoader 缓存已清空");
|
|
33
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import DataLoader from "dataloader";
|
|
2
|
+
/**
|
|
3
|
+
* Template DataLoader
|
|
4
|
+
* 针对 template 表的批量数据加载器,解决 N+1 查询问题
|
|
5
|
+
*/
|
|
6
|
+
export declare class TemplateDataLoader {
|
|
7
|
+
readonly byId: DataLoader<number, any>;
|
|
8
|
+
readonly byName: DataLoader<string, any>;
|
|
9
|
+
readonly searchByName: DataLoader<string, any[]>;
|
|
10
|
+
constructor();
|
|
11
|
+
/**
|
|
12
|
+
* 清除所有缓存
|
|
13
|
+
*/
|
|
14
|
+
clearAll(): void;
|
|
15
|
+
/**
|
|
16
|
+
* 清除特定 ID 的缓存
|
|
17
|
+
*/
|
|
18
|
+
clearById(id: number): void;
|
|
19
|
+
/**
|
|
20
|
+
* 清除特定名称的缓存
|
|
21
|
+
*/
|
|
22
|
+
clearByName(name: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* 预加载数据到缓存
|
|
25
|
+
*/
|
|
26
|
+
prime(id: number, data: any): void;
|
|
27
|
+
/**
|
|
28
|
+
* 获取缓存统计信息
|
|
29
|
+
*/
|
|
30
|
+
getStats(): {
|
|
31
|
+
byId: {
|
|
32
|
+
cacheMap: any;
|
|
33
|
+
name: string;
|
|
34
|
+
};
|
|
35
|
+
byName: {
|
|
36
|
+
cacheMap: any;
|
|
37
|
+
name: string;
|
|
38
|
+
};
|
|
39
|
+
searchByName: {
|
|
40
|
+
cacheMap: any;
|
|
41
|
+
name: string;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 创建 Template DataLoader 实例
|
|
47
|
+
*/
|
|
48
|
+
export declare function createTemplateDataLoader(): TemplateDataLoader;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TemplateDataLoader = void 0;
|
|
7
|
+
exports.createTemplateDataLoader = createTemplateDataLoader;
|
|
8
|
+
const dataloader_1 = __importDefault(require("dataloader"));
|
|
9
|
+
const db_1 = __importDefault(require("../db"));
|
|
10
|
+
/**
|
|
11
|
+
* Template DataLoader
|
|
12
|
+
* 针对 template 表的批量数据加载器,解决 N+1 查询问题
|
|
13
|
+
*/
|
|
14
|
+
class TemplateDataLoader {
|
|
15
|
+
constructor() {
|
|
16
|
+
// 按 ID 批量加载
|
|
17
|
+
this.byId = new dataloader_1.default(async (ids) => {
|
|
18
|
+
try {
|
|
19
|
+
console.log(`🔍 DataLoader: 批量加载 ${ids.length} 个 template by ID`);
|
|
20
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
21
|
+
const sql = `SELECT id, name FROM template WHERE id IN (${placeholders})`;
|
|
22
|
+
const results = await db_1.default.executeQuery(sql, [...ids]);
|
|
23
|
+
// 确保返回顺序与输入 keys 一致,未找到的返回 null
|
|
24
|
+
return ids.map((id) => results.find((row) => row.id === id) || null);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.error("DataLoader byId 批量加载失败:", error);
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}, {
|
|
31
|
+
cache: true,
|
|
32
|
+
maxBatchSize: 100,
|
|
33
|
+
batchScheduleFn: (callback) => setTimeout(callback, 10), // 10ms 内的请求合并
|
|
34
|
+
});
|
|
35
|
+
// 按名称批量加载
|
|
36
|
+
this.byName = new dataloader_1.default(async (names) => {
|
|
37
|
+
try {
|
|
38
|
+
console.log(`🔍 DataLoader: 批量加载 ${names.length} 个 template by name`);
|
|
39
|
+
const placeholders = names.map(() => "?").join(",");
|
|
40
|
+
const sql = `SELECT id, name FROM template WHERE name IN (${placeholders})`;
|
|
41
|
+
const results = await db_1.default.executeQuery(sql, [...names]);
|
|
42
|
+
// 确保返回顺序与输入 keys 一致
|
|
43
|
+
return names.map((name) => results.find((row) => row.name === name) || null);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error("DataLoader byName 批量加载失败:", error);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}, {
|
|
50
|
+
cache: true,
|
|
51
|
+
maxBatchSize: 50,
|
|
52
|
+
batchScheduleFn: (callback) => setTimeout(callback, 10),
|
|
53
|
+
});
|
|
54
|
+
// 按名称模糊搜索(返回数组)
|
|
55
|
+
this.searchByName = new dataloader_1.default(async (searchTerms) => {
|
|
56
|
+
try {
|
|
57
|
+
console.log(`🔍 DataLoader: 批量搜索 ${searchTerms.length} 个关键词`);
|
|
58
|
+
// 对于搜索,我们需要为每个搜索词执行独立的查询
|
|
59
|
+
const results = await Promise.all(searchTerms.map(async (term) => {
|
|
60
|
+
const sql = "SELECT id, name FROM template WHERE name LIKE ?";
|
|
61
|
+
return db_1.default.executeQuery(sql, [`%${term}%`]);
|
|
62
|
+
}));
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error("DataLoader searchByName 批量搜索失败:", error);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}, {
|
|
70
|
+
cache: true,
|
|
71
|
+
maxBatchSize: 20, // 搜索请求较少,降低批量大小
|
|
72
|
+
batchScheduleFn: (callback) => setTimeout(callback, 20), // 稍长的等待时间
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 清除所有缓存
|
|
77
|
+
*/
|
|
78
|
+
clearAll() {
|
|
79
|
+
this.byId.clearAll();
|
|
80
|
+
this.byName.clearAll();
|
|
81
|
+
this.searchByName.clearAll();
|
|
82
|
+
console.log("🧹 Template DataLoader 缓存已清空");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 清除特定 ID 的缓存
|
|
86
|
+
*/
|
|
87
|
+
clearById(id) {
|
|
88
|
+
this.byId.clear(id);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 清除特定名称的缓存
|
|
92
|
+
*/
|
|
93
|
+
clearByName(name) {
|
|
94
|
+
this.byName.clear(name);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 预加载数据到缓存
|
|
98
|
+
*/
|
|
99
|
+
prime(id, data) {
|
|
100
|
+
this.byId.prime(id, data);
|
|
101
|
+
if (data?.name) {
|
|
102
|
+
this.byName.prime(data.name, data);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 获取缓存统计信息
|
|
107
|
+
*/
|
|
108
|
+
getStats() {
|
|
109
|
+
return {
|
|
110
|
+
byId: {
|
|
111
|
+
cacheMap: this.byId.cacheMap?.size || 0,
|
|
112
|
+
name: "Template.byId",
|
|
113
|
+
},
|
|
114
|
+
byName: {
|
|
115
|
+
cacheMap: this.byName.cacheMap?.size || 0,
|
|
116
|
+
name: "Template.byName",
|
|
117
|
+
},
|
|
118
|
+
searchByName: {
|
|
119
|
+
cacheMap: this.searchByName.cacheMap?.size || 0,
|
|
120
|
+
name: "Template.searchByName",
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
exports.TemplateDataLoader = TemplateDataLoader;
|
|
126
|
+
/**
|
|
127
|
+
* 创建 Template DataLoader 实例
|
|
128
|
+
*/
|
|
129
|
+
function createTemplateDataLoader() {
|
|
130
|
+
return new TemplateDataLoader();
|
|
131
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { DataLoaderContext } from "../dataloaders";
|
|
2
|
+
/**
|
|
3
|
+
* DataLoader 调试和监控 API
|
|
4
|
+
* 在开发环境中提供 DataLoader 性能监控接口
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* 获取 DataLoader 统计信息的 GraphQL resolver
|
|
8
|
+
*/
|
|
9
|
+
export declare const dataLoaderStatsResolver: {
|
|
10
|
+
dataLoaderStats: (_: any, context: DataLoaderContext) => Promise<{
|
|
11
|
+
status: string;
|
|
12
|
+
score: number;
|
|
13
|
+
summary: {
|
|
14
|
+
totalLoaders: number;
|
|
15
|
+
totalRequests: number;
|
|
16
|
+
totalBatchRequests: number;
|
|
17
|
+
totalCacheHits: number;
|
|
18
|
+
totalCacheMisses: number;
|
|
19
|
+
};
|
|
20
|
+
loaders: any[];
|
|
21
|
+
recommendations: string[];
|
|
22
|
+
contextStats: {
|
|
23
|
+
template: {
|
|
24
|
+
byId: {
|
|
25
|
+
cacheMap: any;
|
|
26
|
+
name: string;
|
|
27
|
+
};
|
|
28
|
+
byName: {
|
|
29
|
+
cacheMap: any;
|
|
30
|
+
name: string;
|
|
31
|
+
};
|
|
32
|
+
searchByName: {
|
|
33
|
+
cacheMap: any;
|
|
34
|
+
name: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
timestamp: string;
|
|
38
|
+
} | null;
|
|
39
|
+
timestamp: string;
|
|
40
|
+
}>;
|
|
41
|
+
resetDataLoaderStats: () => Promise<{
|
|
42
|
+
success: boolean;
|
|
43
|
+
message: string;
|
|
44
|
+
timestamp: string;
|
|
45
|
+
}>;
|
|
46
|
+
clearDataLoaderCache: (_: any, context: DataLoaderContext) => Promise<{
|
|
47
|
+
success: boolean;
|
|
48
|
+
message: string;
|
|
49
|
+
timestamp: string;
|
|
50
|
+
}>;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* DataLoader 调试 Schema
|
|
54
|
+
*/
|
|
55
|
+
export declare const dataLoaderDebugSchema: {
|
|
56
|
+
query: string;
|
|
57
|
+
mutation: string;
|
|
58
|
+
type: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Express 路由:DataLoader 调试接口
|
|
62
|
+
*/
|
|
63
|
+
export declare function createDataLoaderDebugRoutes(app: any): void;
|