@maiyunnet/kebab 9.13.8 → 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/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
  });
@@ -96,7 +98,7 @@ export default class Mod {
96
98
  'value': kebab.DbValue;
97
99
  };
98
100
  /** --- 创建 JSON 字符串对象,用于 PGSQL 的 jsonb 字段 --- */
99
- static json(obj: kebab.Json): any;
101
+ static json<T>(obj: T): T;
100
102
  /**
101
103
  * --- 添加一个序列(允许超过 65536 的占位符会被拆分多次执行) ---
102
104
  * @param db 数据库对象
@@ -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();
@@ -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
  }