@maiyunnet/kebab 9.14.0 → 9.14.1
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/doc/kebab-rag.md +291 -147
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/core.js +24 -7
- package/lib/sql.d.ts +26 -0
- package/lib/sql.js +34 -1
- package/package.json +1 -1
- package/sys/ctr.d.ts +4 -0
- package/sys/mod.d.ts +32 -0
- package/sys/mod.js +37 -1
- package/sys/route.d.ts +4 -0
- package/sys/route.js +63 -0
- package/www/example/ctr/test.js +16 -0
- package/www/example/stc/chunk-FG5CKGYR.js +81 -0
- package/www/example/stc/view/hello.page.bundle.js +1 -1
- package/www/example/stc/view/hello.page.css +2 -2
- package/www/example/stc/view/react-router.page.bundle.js +1 -1
- package/www/example/stc/view/react-router.page.css +2 -2
- package/www/example/stc/view/react-router.page.js +1 -1
- package/www/example/stc/view/react-router.page.tsx +1 -1
- package/www/example/stc/view/react.page.bundle.js +3 -3
- package/www/example/stc/view/react.page.css +2 -2
- package/www/example/stc/chunk-YJ3GYATF.js +0 -81
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* --- 本文件用来定义每个目录实体地址的常量 ---
|
|
7
7
|
*/
|
|
8
8
|
/** --- 当前系统版本号 --- */
|
|
9
|
-
export const VER = '9.14.
|
|
9
|
+
export const VER = '9.14.1';
|
|
10
10
|
// --- 服务端用的路径 ---
|
|
11
11
|
const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
|
|
12
12
|
/** --- /xxx/xxx --- */
|
package/lib/core.js
CHANGED
|
@@ -307,10 +307,11 @@ export function muid(ctr, opt = {}) {
|
|
|
307
307
|
export function ip(ctr, req) {
|
|
308
308
|
const headers = ctr instanceof sCtr.Ctr ? ctr.getPrototype('_headers') : ctr;
|
|
309
309
|
if (typeof headers['cf-connecting-ip'] === 'string') {
|
|
310
|
-
return headers['cf-connecting-ip'];
|
|
310
|
+
return normalizeIP(headers['cf-connecting-ip']);
|
|
311
311
|
}
|
|
312
312
|
else if (typeof headers['x-forwarded-for'] === 'string') {
|
|
313
|
-
|
|
313
|
+
// --- X-Forwarded-For 可能为逗号分隔的多个 IP,取第一个即客户端 IP ---
|
|
314
|
+
return normalizeIP(headers['x-forwarded-for'].split(',')[0].trim());
|
|
314
315
|
}
|
|
315
316
|
else {
|
|
316
317
|
if (!req) {
|
|
@@ -321,21 +322,37 @@ export function ip(ctr, req) {
|
|
|
321
322
|
return '';
|
|
322
323
|
}
|
|
323
324
|
}
|
|
324
|
-
return req.socket.remoteAddress ?? '';
|
|
325
|
+
return normalizeIP(req.socket.remoteAddress ?? '');
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
328
|
/** --- 获取 CF 和 X 的 IP --- */
|
|
328
329
|
export function ips(ctr) {
|
|
329
330
|
const headers = ctr instanceof sCtr.Ctr ? ctr.getPrototype('_headers') : ctr;
|
|
330
331
|
return {
|
|
331
|
-
'cf': typeof headers['cf-connecting-ip'] === 'string' ? headers['cf-connecting-ip'] : '',
|
|
332
|
-
'x': typeof headers['x-forwarded-for'] === 'string' ? headers['x-forwarded-for'] : ''
|
|
332
|
+
'cf': typeof headers['cf-connecting-ip'] === 'string' ? normalizeIP(headers['cf-connecting-ip']) : '',
|
|
333
|
+
'x': typeof headers['x-forwarded-for'] === 'string' ? normalizeIP(headers['x-forwarded-for'].split(',')[0].trim()) : ''
|
|
333
334
|
};
|
|
334
335
|
}
|
|
335
336
|
/** --- 使用 X-Forwarded-For 的 CDN 厂商 --- */
|
|
336
337
|
export const REAL_IP_X = 'x-forwarded-for';
|
|
337
338
|
/** --- 使用的是 Cloudflare --- */
|
|
338
339
|
export const REAL_IP_CF = 'cf-connecting-ip';
|
|
340
|
+
/**
|
|
341
|
+
* --- 规范化 IP 地址 ---
|
|
342
|
+
* --- 将 IPv4-mapped IPv6(如 ::ffff:127.0.0.1)转换为纯 IPv4,原生 IPv6 保持不变 ---
|
|
343
|
+
* @param ip 原始 IP 地址
|
|
344
|
+
*/
|
|
345
|
+
function normalizeIP(ip) {
|
|
346
|
+
if (!ip) {
|
|
347
|
+
return '';
|
|
348
|
+
}
|
|
349
|
+
const value = ip.trim();
|
|
350
|
+
const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/i.exec(value);
|
|
351
|
+
if (mapped) {
|
|
352
|
+
return mapped[1];
|
|
353
|
+
}
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
339
356
|
/**
|
|
340
357
|
* --- 获取直连 IP(安全 IP) ---
|
|
341
358
|
* @param ctr
|
|
@@ -346,11 +363,11 @@ export function realIP(ctr, name = '') {
|
|
|
346
363
|
if (name !== '') {
|
|
347
364
|
const value = headers[name];
|
|
348
365
|
if (typeof value === 'string') {
|
|
349
|
-
return value;
|
|
366
|
+
return normalizeIP(value);
|
|
350
367
|
}
|
|
351
368
|
}
|
|
352
369
|
const req = ctr.getPrototype('_req');
|
|
353
|
-
return req.socket.remoteAddress ?? '';
|
|
370
|
+
return normalizeIP(req.socket.remoteAddress ?? '');
|
|
354
371
|
}
|
|
355
372
|
/**
|
|
356
373
|
* --- 间隔一段时间 ---
|
package/lib/sql.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export declare class Sql {
|
|
|
42
42
|
private _placeholderCounter;
|
|
43
43
|
/** --- 是否忽略错误 --- */
|
|
44
44
|
private _ignore;
|
|
45
|
+
/** --- MySQL 优化器 Hint,如 INDEX(table idx) --- */
|
|
46
|
+
private _hint;
|
|
45
47
|
constructor(opt: {
|
|
46
48
|
'service': ESERVICE;
|
|
47
49
|
'ctr'?: ctr.Ctr;
|
|
@@ -51,6 +53,30 @@ export declare class Sql {
|
|
|
51
53
|
'sql'?: string[];
|
|
52
54
|
'alias'?: string[];
|
|
53
55
|
});
|
|
56
|
+
/**
|
|
57
|
+
* --- 设置 MySQL 优化器 Hint ---
|
|
58
|
+
* --- 会以 Optimizer Hint 注释语法注入到 SELECT 关键字后 ---
|
|
59
|
+
* --- 传入 hint 内容即可,无需包裹注释符号 ---
|
|
60
|
+
* @param h Hint 内容
|
|
61
|
+
* @example
|
|
62
|
+
* // --- 强制走指定索引(最常用) ---
|
|
63
|
+
* INDEX(`supply_date_0_0` `idx_supply_date_query`)
|
|
64
|
+
*
|
|
65
|
+
* // --- 指定某表不用某索引 ---
|
|
66
|
+
* NO_INDEX(`supply_date_0_0` `idx_old`)
|
|
67
|
+
*
|
|
68
|
+
* // --- 指定 JOIN 顺序和索引 ---
|
|
69
|
+
* JOIN_ORDER(`a` `b`) INDEX(`a` `idx_a`) INDEX(`b` `idx_b`)
|
|
70
|
+
*
|
|
71
|
+
* // --- 指定 JOIN 中某表使用的索引 ---
|
|
72
|
+
* JOIN_INDEX(`supply_date_0_0` `idx_supply_date_query`)
|
|
73
|
+
*
|
|
74
|
+
* // --- 多表多索引组合 ---
|
|
75
|
+
* INDEX(`t1` `idx_a`) JOIN_INDEX(`t2` `idx_b`)
|
|
76
|
+
*
|
|
77
|
+
* // --- 官方文档: https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html ---
|
|
78
|
+
*/
|
|
79
|
+
hint(h: string): this;
|
|
54
80
|
/**
|
|
55
81
|
* --- 插入数据前导 ---
|
|
56
82
|
* @param table 表名
|
package/lib/sql.js
CHANGED
|
@@ -45,6 +45,8 @@ export class Sql {
|
|
|
45
45
|
_placeholderCounter = 1;
|
|
46
46
|
/** --- 是否忽略错误 --- */
|
|
47
47
|
_ignore = false;
|
|
48
|
+
/** --- MySQL 优化器 Hint,如 INDEX(table idx) --- */
|
|
49
|
+
_hint = '';
|
|
48
50
|
// --- 实例化 ---
|
|
49
51
|
constructor(opt) {
|
|
50
52
|
this._ctr = opt.ctr;
|
|
@@ -60,6 +62,33 @@ export class Sql {
|
|
|
60
62
|
this._alias = opt.alias;
|
|
61
63
|
}
|
|
62
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* --- 设置 MySQL 优化器 Hint ---
|
|
67
|
+
* --- 会以 Optimizer Hint 注释语法注入到 SELECT 关键字后 ---
|
|
68
|
+
* --- 传入 hint 内容即可,无需包裹注释符号 ---
|
|
69
|
+
* @param h Hint 内容
|
|
70
|
+
* @example
|
|
71
|
+
* // --- 强制走指定索引(最常用) ---
|
|
72
|
+
* INDEX(`supply_date_0_0` `idx_supply_date_query`)
|
|
73
|
+
*
|
|
74
|
+
* // --- 指定某表不用某索引 ---
|
|
75
|
+
* NO_INDEX(`supply_date_0_0` `idx_old`)
|
|
76
|
+
*
|
|
77
|
+
* // --- 指定 JOIN 顺序和索引 ---
|
|
78
|
+
* JOIN_ORDER(`a` `b`) INDEX(`a` `idx_a`) INDEX(`b` `idx_b`)
|
|
79
|
+
*
|
|
80
|
+
* // --- 指定 JOIN 中某表使用的索引 ---
|
|
81
|
+
* JOIN_INDEX(`supply_date_0_0` `idx_supply_date_query`)
|
|
82
|
+
*
|
|
83
|
+
* // --- 多表多索引组合 ---
|
|
84
|
+
* INDEX(`t1` `idx_a`) JOIN_INDEX(`t2` `idx_b`)
|
|
85
|
+
*
|
|
86
|
+
* // --- 官方文档: https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html ---
|
|
87
|
+
*/
|
|
88
|
+
hint(h) {
|
|
89
|
+
this._hint = h;
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
63
92
|
// --- 前导 ---
|
|
64
93
|
/**
|
|
65
94
|
* --- 插入数据前导 ---
|
|
@@ -880,7 +909,7 @@ export class Sql {
|
|
|
880
909
|
'data': data,
|
|
881
910
|
'sql': sql,
|
|
882
911
|
'alias': lCore.clone(this._alias),
|
|
883
|
-
});
|
|
912
|
+
}).hint(this._hint);
|
|
884
913
|
}
|
|
885
914
|
// --- 操作 ---
|
|
886
915
|
/**
|
|
@@ -888,6 +917,10 @@ export class Sql {
|
|
|
888
917
|
*/
|
|
889
918
|
getSql() {
|
|
890
919
|
let sql = this._sql.join('');
|
|
920
|
+
// --- 注入优化器 Hint 到 SELECT 关键字后 ---
|
|
921
|
+
if (this._hint && !sql.includes('/*+')) {
|
|
922
|
+
sql = sql.replace(/^SELECT /i, `SELECT /*+ ${this._hint} */ `);
|
|
923
|
+
}
|
|
891
924
|
if (this._pre) {
|
|
892
925
|
return this._alias.reduce((result, item) => {
|
|
893
926
|
if (this._service === ESERVICE.MYSQL) {
|
package/package.json
CHANGED
package/sys/ctr.d.ts
CHANGED
|
@@ -342,5 +342,9 @@ export declare class Ctr {
|
|
|
342
342
|
'maxFileSize'?: number;
|
|
343
343
|
/** --- 允许的文件扩展名(含点号),如 ['.jpg', '.png', '.pdf'] --- */
|
|
344
344
|
'allowedExts'?: string[];
|
|
345
|
+
/** --- 单个字段(非文件)最大字节数,默认 1 MB --- */
|
|
346
|
+
'maxFieldSize'?: number;
|
|
347
|
+
/** --- 整体请求超时时间(毫秒),默认 5 分钟,设为 0 禁用超时 --- */
|
|
348
|
+
'timeout'?: number;
|
|
345
349
|
}): Promise<boolean>;
|
|
346
350
|
}
|
package/sys/mod.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export default class Mod {
|
|
|
80
80
|
'key': string;
|
|
81
81
|
'list': string[];
|
|
82
82
|
};
|
|
83
|
+
/** --- MySQL 优化器 Hint,如 `INDEX(\`t\` \`idx_xx\`)`,注入到 SELECT 后 --- */
|
|
84
|
+
'hint'?: string;
|
|
83
85
|
/** --- MySQL 表前缀或 PostgreSQL Schema 名,优先级:选项 > 类属性 > 配置 --- */
|
|
84
86
|
'pre'?: string;
|
|
85
87
|
});
|
|
@@ -208,6 +210,7 @@ export default class Mod {
|
|
|
208
210
|
'key': string;
|
|
209
211
|
'list': string[];
|
|
210
212
|
};
|
|
213
|
+
'hint'?: string;
|
|
211
214
|
}): T & Record<string, any>;
|
|
212
215
|
/**
|
|
213
216
|
* --- 通过 where 条件获取模型 ---
|
|
@@ -224,6 +227,7 @@ export default class Mod {
|
|
|
224
227
|
'key': string;
|
|
225
228
|
'list': string[];
|
|
226
229
|
};
|
|
230
|
+
'hint'?: string;
|
|
227
231
|
}): T & Record<string, any>;
|
|
228
232
|
/**
|
|
229
233
|
* --- 获取创建对象,通常用于新建数据库条目 ---
|
|
@@ -248,6 +252,7 @@ export default class Mod {
|
|
|
248
252
|
'index'?: string | string[];
|
|
249
253
|
/** --- 通过 key 字段获取,默认为 false,即从主键获取 --- */
|
|
250
254
|
'key'?: boolean;
|
|
255
|
+
'hint'?: string;
|
|
251
256
|
}): Promise<false | null | (T & Record<string, any>)>;
|
|
252
257
|
static one(db: lDb.Pool | lDb.Transaction, s: string | kebab.Json, opt: {
|
|
253
258
|
'ctr'?: sCtr.Ctr;
|
|
@@ -256,6 +261,7 @@ export default class Mod {
|
|
|
256
261
|
'index'?: string | string[];
|
|
257
262
|
'select'?: string | string[];
|
|
258
263
|
'by'?: [string | string[], 'DESC' | 'ASC'];
|
|
264
|
+
'hint'?: string;
|
|
259
265
|
'array': true;
|
|
260
266
|
}): Promise<false | null | Record<string, any>>;
|
|
261
267
|
static one<T extends Mod>(db: lDb.Pool | lDb.Transaction, s: string | kebab.Json, opt?: {
|
|
@@ -265,6 +271,7 @@ export default class Mod {
|
|
|
265
271
|
'index'?: string | string[];
|
|
266
272
|
'select'?: string | string[];
|
|
267
273
|
'by'?: [string | string[], 'DESC' | 'ASC'];
|
|
274
|
+
'hint'?: string;
|
|
268
275
|
'array'?: false;
|
|
269
276
|
}): Promise<false | null | (T & Record<string, any>)>;
|
|
270
277
|
/**
|
|
@@ -279,6 +286,7 @@ export default class Mod {
|
|
|
279
286
|
'pre'?: string;
|
|
280
287
|
'index'?: string | string[];
|
|
281
288
|
'select'?: string | string[];
|
|
289
|
+
'hint'?: string;
|
|
282
290
|
}): Promise<false | null | Record<string, any>>;
|
|
283
291
|
/**
|
|
284
292
|
* --- 根据 where 条件获取主键值列表 ---
|
|
@@ -414,6 +422,30 @@ export default class Mod {
|
|
|
414
422
|
* @param pre 前缀,仅与主表的 pre 不同时传入
|
|
415
423
|
*/
|
|
416
424
|
crossJoin(f: string, s: kebab.Json, index?: string, pre?: string): this;
|
|
425
|
+
/**
|
|
426
|
+
* --- 设置 MySQL 优化器 Hint ---
|
|
427
|
+
* --- 内容会以 Optimizer Hint 注释语法注入到 SELECT 关键字后 ---
|
|
428
|
+
* --- 必须在查询入口调用之后、执行方法之前链式调用 ---
|
|
429
|
+
* @param h Hint 内容
|
|
430
|
+
* @example
|
|
431
|
+
* // 强制走指定索引(最常用)
|
|
432
|
+
* .hint("INDEX(`supply_date_0_0` `idx_supply_date_query`)")
|
|
433
|
+
*
|
|
434
|
+
* // 指定某表不用某索引
|
|
435
|
+
* .hint("NO_INDEX(`supply_date_0_0` `idx_old`)")
|
|
436
|
+
*
|
|
437
|
+
* // 指定 JOIN 顺序和索引
|
|
438
|
+
* .hint("JOIN_ORDER(`a` `b`) INDEX(`a` `idx_a`) INDEX(`b` `idx_b`)")
|
|
439
|
+
*
|
|
440
|
+
* // 指定 JOIN 中某表使用的索引
|
|
441
|
+
* .hint("JOIN_INDEX(`supply_date_0_0` `idx_supply_date_query`)")
|
|
442
|
+
*
|
|
443
|
+
* // 多表多索引组合
|
|
444
|
+
* .hint("INDEX(`t1` `idx_a`) JOIN_INDEX(`t2` `idx_b`)")
|
|
445
|
+
*
|
|
446
|
+
* // 官方文档: https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html
|
|
447
|
+
*/
|
|
448
|
+
hint(h: string): this;
|
|
417
449
|
/**
|
|
418
450
|
* --- 筛选器 ---
|
|
419
451
|
* @param s 筛选条件数组或字符串
|
package/sys/mod.js
CHANGED
|
@@ -89,6 +89,10 @@ export default class Mod {
|
|
|
89
89
|
'ctr': opt.ctr,
|
|
90
90
|
'pre': opt.pre ?? this.constructor._$pre,
|
|
91
91
|
});
|
|
92
|
+
/** --- 设置优化器 Hint --- */
|
|
93
|
+
if (opt.hint) {
|
|
94
|
+
this._sql.hint(opt.hint);
|
|
95
|
+
}
|
|
92
96
|
if (opt.index) {
|
|
93
97
|
this._index = typeof opt.index === 'string' ? [opt.index] : [...new Set(opt.index)];
|
|
94
98
|
}
|
|
@@ -375,7 +379,8 @@ export default class Mod {
|
|
|
375
379
|
'select': c,
|
|
376
380
|
'index': opt.index,
|
|
377
381
|
'alias': opt.alias,
|
|
378
|
-
'contain': opt.contain
|
|
382
|
+
'contain': opt.contain,
|
|
383
|
+
'hint': opt.hint,
|
|
379
384
|
});
|
|
380
385
|
}
|
|
381
386
|
/**
|
|
@@ -393,6 +398,7 @@ export default class Mod {
|
|
|
393
398
|
'index': opt.index,
|
|
394
399
|
'contain': opt.contain,
|
|
395
400
|
'alias': opt.alias,
|
|
401
|
+
'hint': opt.hint,
|
|
396
402
|
});
|
|
397
403
|
}
|
|
398
404
|
/**
|
|
@@ -427,6 +433,7 @@ export default class Mod {
|
|
|
427
433
|
[opt.key ? this._$key : this._$primary]: val,
|
|
428
434
|
}],
|
|
429
435
|
'index': opt.index,
|
|
436
|
+
'hint': opt.hint,
|
|
430
437
|
}).first(opt.lock);
|
|
431
438
|
}
|
|
432
439
|
/**
|
|
@@ -444,6 +451,7 @@ export default class Mod {
|
|
|
444
451
|
'ctr': opt.ctr,
|
|
445
452
|
'pre': opt.pre,
|
|
446
453
|
'where': s,
|
|
454
|
+
'hint': opt.hint,
|
|
447
455
|
});
|
|
448
456
|
if (opt.by) {
|
|
449
457
|
o.by(opt.by[0], opt.by[1]);
|
|
@@ -459,6 +467,7 @@ export default class Mod {
|
|
|
459
467
|
'pre': opt.pre,
|
|
460
468
|
'where': s,
|
|
461
469
|
'index': item,
|
|
470
|
+
'hint': opt.hint,
|
|
462
471
|
});
|
|
463
472
|
if (opt.by) {
|
|
464
473
|
row.by(opt.by[0], opt.by[1]);
|
|
@@ -1307,6 +1316,33 @@ export default class Mod {
|
|
|
1307
1316
|
this._sql.crossJoin(f, s, index ? '_' + index : '', pre);
|
|
1308
1317
|
return this;
|
|
1309
1318
|
}
|
|
1319
|
+
/**
|
|
1320
|
+
* --- 设置 MySQL 优化器 Hint ---
|
|
1321
|
+
* --- 内容会以 Optimizer Hint 注释语法注入到 SELECT 关键字后 ---
|
|
1322
|
+
* --- 必须在查询入口调用之后、执行方法之前链式调用 ---
|
|
1323
|
+
* @param h Hint 内容
|
|
1324
|
+
* @example
|
|
1325
|
+
* // 强制走指定索引(最常用)
|
|
1326
|
+
* .hint("INDEX(`supply_date_0_0` `idx_supply_date_query`)")
|
|
1327
|
+
*
|
|
1328
|
+
* // 指定某表不用某索引
|
|
1329
|
+
* .hint("NO_INDEX(`supply_date_0_0` `idx_old`)")
|
|
1330
|
+
*
|
|
1331
|
+
* // 指定 JOIN 顺序和索引
|
|
1332
|
+
* .hint("JOIN_ORDER(`a` `b`) INDEX(`a` `idx_a`) INDEX(`b` `idx_b`)")
|
|
1333
|
+
*
|
|
1334
|
+
* // 指定 JOIN 中某表使用的索引
|
|
1335
|
+
* .hint("JOIN_INDEX(`supply_date_0_0` `idx_supply_date_query`)")
|
|
1336
|
+
*
|
|
1337
|
+
* // 多表多索引组合
|
|
1338
|
+
* .hint("INDEX(`t1` `idx_a`) JOIN_INDEX(`t2` `idx_b`)")
|
|
1339
|
+
*
|
|
1340
|
+
* // 官方文档: https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html
|
|
1341
|
+
*/
|
|
1342
|
+
hint(h) {
|
|
1343
|
+
this._sql.hint(h);
|
|
1344
|
+
return this;
|
|
1345
|
+
}
|
|
1310
1346
|
/**
|
|
1311
1347
|
* --- 筛选器 ---
|
|
1312
1348
|
* @param s 筛选条件数组或字符串
|
package/sys/route.d.ts
CHANGED
|
@@ -77,6 +77,10 @@ export declare function getFormData(req: http2.Http2ServerRequest | http.Incomin
|
|
|
77
77
|
'maxFileSize'?: number;
|
|
78
78
|
/** --- 允许的文件扩展名(含点号),如 ['.jpg', '.png', '.pdf'] --- */
|
|
79
79
|
'allowedExts'?: string[];
|
|
80
|
+
/** --- 单个字段(非文件)最大字节数,默认 1 MB --- */
|
|
81
|
+
'maxFieldSize'?: number;
|
|
82
|
+
/** --- 整体请求超时时间(毫秒),默认 5 分钟,设为 0 禁用超时 --- */
|
|
83
|
+
'timeout'?: number;
|
|
80
84
|
}): Promise<{
|
|
81
85
|
'post': Record<string, kebab.Json>;
|
|
82
86
|
'files': Record<string, kebab.IPostFile | kebab.IPostFile[]>;
|
package/sys/route.js
CHANGED
|
@@ -935,6 +935,18 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
935
935
|
};
|
|
936
936
|
/** --- 获取的 boundary 文本 --- */
|
|
937
937
|
const boundary = ct.slice(clio + 9);
|
|
938
|
+
// --- 超时护盾:防止网络静默断开时 Promise 永不 resolve ---
|
|
939
|
+
/** --- 超时定时器 --- */
|
|
940
|
+
let timer;
|
|
941
|
+
/** --- 是否已经结束(防止重复 resolve) --- */
|
|
942
|
+
let finished = false;
|
|
943
|
+
/** --- 清理定时器 --- */
|
|
944
|
+
function clearTimer() {
|
|
945
|
+
if (timer) {
|
|
946
|
+
clearTimeout(timer);
|
|
947
|
+
timer = undefined;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
938
950
|
// 一下变量是相对于 data 事件的全局变量 ---
|
|
939
951
|
/** --- 当前临时读入的还未处理的 buffer --- */
|
|
940
952
|
let buffer = Buffer.from('');
|
|
@@ -978,6 +990,23 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
978
990
|
}
|
|
979
991
|
}
|
|
980
992
|
}
|
|
993
|
+
// --- 启动超时定时器(在所有变量声明之后,确保回调闭包可引用) ---
|
|
994
|
+
const timeoutMs = limits.timeout ?? 300_000;
|
|
995
|
+
if (timeoutMs > 0) {
|
|
996
|
+
timer = setTimeout(() => {
|
|
997
|
+
if (finished) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
finished = true;
|
|
1001
|
+
if ((state === EState.FILE) && ftmpName && ftmpStream) {
|
|
1002
|
+
ftmpStream.destroy();
|
|
1003
|
+
}
|
|
1004
|
+
lCore.debug('[ROUTE][GETFORMDATA] formdata request timeout');
|
|
1005
|
+
lCore.log({}, '[ROUTE][GETFORMDATA] formdata request timeout after ' + timeoutMs + 'ms', '-error');
|
|
1006
|
+
cleanupFiles();
|
|
1007
|
+
resolve(false);
|
|
1008
|
+
}, timeoutMs);
|
|
1009
|
+
}
|
|
981
1010
|
/** --- 拒绝当前文件:销毁流、删临时文件、标记 rejected --- */
|
|
982
1011
|
function rejectFile() {
|
|
983
1012
|
rejected = true;
|
|
@@ -988,6 +1017,11 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
988
1017
|
}
|
|
989
1018
|
/** --- 最终输出 --- */
|
|
990
1019
|
function finalize() {
|
|
1020
|
+
if (finished) {
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
finished = true;
|
|
1024
|
+
clearTimer();
|
|
991
1025
|
if (rejected) {
|
|
992
1026
|
cleanupFiles();
|
|
993
1027
|
resolve(false);
|
|
@@ -1064,6 +1098,12 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
1064
1098
|
// --- POST 模式 ---
|
|
1065
1099
|
const io = buffer.indexOf('\r\n--' + boundary);
|
|
1066
1100
|
if (io === -1) {
|
|
1101
|
+
// --- 检查字段大小限制,防止畸形数据导致 buffer 无限增长 ---
|
|
1102
|
+
const maxField = limits.maxFieldSize ?? 1_048_576;
|
|
1103
|
+
if (buffer.length > maxField + boundary.length + 4) {
|
|
1104
|
+
rejected = true;
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1067
1107
|
return;
|
|
1068
1108
|
}
|
|
1069
1109
|
// --- 找到结束标语,写入 POST ---
|
|
@@ -1178,6 +1218,11 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
1178
1218
|
}
|
|
1179
1219
|
});
|
|
1180
1220
|
req.on('error', function (e) {
|
|
1221
|
+
if (finished) {
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
finished = true;
|
|
1225
|
+
clearTimer();
|
|
1181
1226
|
if ((state === EState.FILE) && ftmpName) {
|
|
1182
1227
|
ftmpStream.destroy();
|
|
1183
1228
|
}
|
|
@@ -1186,8 +1231,26 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
1186
1231
|
cleanupFiles();
|
|
1187
1232
|
resolve(false);
|
|
1188
1233
|
});
|
|
1234
|
+
// --- 监听连接关闭(HTTP/2 RST_STREAM、代理断连、或用户主动中断上传等场景) ---
|
|
1235
|
+
req.on('close', function () {
|
|
1236
|
+
if (finished) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
// --- 若数据未读完且连接已关闭,视为传输中断(多数是用户主动取消,无需记录错误) ---
|
|
1240
|
+
if (!readEnd && writeFileLength > 0) {
|
|
1241
|
+
finished = true;
|
|
1242
|
+
clearTimer();
|
|
1243
|
+
if ((state === EState.FILE) && ftmpName && ftmpStream) {
|
|
1244
|
+
ftmpStream.destroy();
|
|
1245
|
+
}
|
|
1246
|
+
lCore.debug('[ROUTE][GETFORMDATA] connection closed before formdata complete');
|
|
1247
|
+
cleanupFiles();
|
|
1248
|
+
resolve(false);
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1189
1251
|
req.on('end', function () {
|
|
1190
1252
|
readEnd = true;
|
|
1253
|
+
clearTimer();
|
|
1191
1254
|
// --- 被拒绝则清理文件并 resolve(false) ---
|
|
1192
1255
|
if (rejected) {
|
|
1193
1256
|
finalize();
|
package/www/example/ctr/test.js
CHANGED
|
@@ -213,6 +213,7 @@ export default class extends sCtr.Ctr {
|
|
|
213
213
|
`<br><a href="${this._config.const.urlBase}test/sql?type=having">View "test/sql?type=having"</a> <a href="${this._config.const.urlBase}test/sql?type=having&s=pgsql">pgsql</a>`,
|
|
214
214
|
`<br><a href="${this._config.const.urlBase}test/sql?type=by">View "test/sql?type=by"</a> <a href="${this._config.const.urlBase}test/sql?type=by&s=pgsql">pgsql</a>`,
|
|
215
215
|
`<br><a href="${this._config.const.urlBase}test/sql?type=field">View "test/sql?type=field"</a> <a href="${this._config.const.urlBase}test/sql?type=field&s=pgsql">pgsql</a>`,
|
|
216
|
+
`<br><a href="${this._config.const.urlBase}test/sql?type=hint">View "test/sql?type=hint"</a>`,
|
|
216
217
|
'<br><br><b>Consistent:</b>',
|
|
217
218
|
`<br><br><a href="${this._config.const.urlBase}test/consistent-hash">View "test/consistent-hash"</a>`,
|
|
218
219
|
`<br><a href="${this._config.const.urlBase}test/consistent-distributed">View "test/consistent-distributed"</a>`,
|
|
@@ -3064,6 +3065,21 @@ Result:<pre id="result">Nothing.</pre>`);
|
|
|
3064
3065
|
echo.push(`<pre>sql.field('*');</pre>` + sql.field('*'));
|
|
3065
3066
|
break;
|
|
3066
3067
|
}
|
|
3068
|
+
case 'hint': {
|
|
3069
|
+
let s = sql.hint('INDEX(`supply_date_0_0` `idx_supply_date_query`)').select('*', 'supply_date_0_0').getSql();
|
|
3070
|
+
let sd = sql.getData();
|
|
3071
|
+
echo.push(`<pre>sql.hint('INDEX(\`supply_date_0_0\` \`idx_supply_date_query\`)').select('*', 'supply_date_0_0');</pre>
|
|
3072
|
+
<b>getSql() :</b> ${s}<br>
|
|
3073
|
+
<b>getData():</b> <pre>${JSON.stringify(sd, undefined, 4)}</pre>
|
|
3074
|
+
<b>format() :</b> ${sql.format(s, sd)}<hr>`);
|
|
3075
|
+
s = sql.hint('INDEX(`supply_date_0_0` `idx_supply_date_query`) NO_INDEX(`supply_date_0_0` `idx_old`)').select('*', 'supply_date_0_0').getSql();
|
|
3076
|
+
sd = sql.getData();
|
|
3077
|
+
echo.push(`<pre>sql.hint('INDEX(\`supply_date_0_0\` \`idx_supply_date_query\`) NO_INDEX(\`supply_date_0_0\` \`idx_old\`)').select('*', 'supply_date_0_0');</pre>
|
|
3078
|
+
<b>getSql() :</b> ${s}<br>
|
|
3079
|
+
<b>getData():</b> <pre>${JSON.stringify(sd, undefined, 4)}</pre>
|
|
3080
|
+
<b>format() :</b> ${sql.format(s, sd)}`);
|
|
3081
|
+
break;
|
|
3082
|
+
}
|
|
3067
3083
|
}
|
|
3068
3084
|
return echo.join('') + '<br><br>' + this._getEnd();
|
|
3069
3085
|
}
|