nsgm-cli 2.1.22 → 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/client/utils/common.ts +6 -2
- package/client/utils/fetch.ts +1 -1
- package/client/utils/sso.ts +13 -3
- 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/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-i18next.config.js +7 -5
- package/next.config.js +19 -3
- package/package.json +2 -1
- package/pages/_app.tsx +6 -3
- package/pages/api/sso/ticketCheck.ts +117 -0
- package/pages/index.tsx +10 -2
- package/pages/login.tsx +40 -10
- package/pages/template/manage.tsx +15 -1
- package/server/apis/sso.js +22 -4
- package/server/modules/template/resolver.js +101 -21
- package/server/modules/template/schema.js +1 -0
package/client/utils/common.ts
CHANGED
|
@@ -25,12 +25,16 @@ export const getLocalApiPrefix = () => {
|
|
|
25
25
|
protocol = protocol.split(":")[0];
|
|
26
26
|
}
|
|
27
27
|
host = location.hostname;
|
|
28
|
-
port = location.port
|
|
28
|
+
port = location.port;
|
|
29
29
|
}
|
|
30
30
|
// 服务器端:直接使用配置中的值,无需额外处理
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
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}`;
|
|
34
38
|
return localApiPrefix;
|
|
35
39
|
};
|
|
36
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/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/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
|
}
|
|
@@ -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: \`
|
|
@@ -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
|
+
}
|