befly 3.16.1 → 3.16.4

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/dist/index.js CHANGED
@@ -195,21 +195,6 @@ export class Befly {
195
195
  noLog = true;
196
196
  }
197
197
  }
198
- const writePlainLinesToConsole = (prefix, errName, message) => {
199
- const text = String(message || "");
200
- const parts = text.split("\n");
201
- const firstLine = parts.length > 0 ? String(parts[0] || "") : "";
202
- const head = `[${prefix}] 启动失败: ${errName}: ${firstLine}`;
203
- try {
204
- process.stderr.write(`${head}\n`);
205
- for (let i = 1; i < parts.length; i = i + 1) {
206
- process.stderr.write(`${parts[i]}\n`);
207
- }
208
- }
209
- catch {
210
- // ignore
211
- }
212
- };
213
198
  // 注意:测试要求 start() 失败时必须 Logger.error 一次,所以这里始终调用 error。
214
199
  // 但如果下层已经记录过详细堆栈,则入口只输出摘要,避免重复。
215
200
  if (!alreadyLogged && multiline) {
@@ -221,7 +206,17 @@ export class Befly {
221
206
  errorKind: kind,
222
207
  errorMessage: errMessage
223
208
  }, { truncate: false, console: false });
224
- writePlainLinesToConsole(appName, errName, errMessage);
209
+ {
210
+ const text = String(errMessage || "");
211
+ const parts = text.split("\n");
212
+ const firstLine = parts.length > 0 ? String(parts[0] || "") : "";
213
+ const lines = [];
214
+ lines.push(`[${appName}] 启动失败: ${errName}: ${firstLine}`);
215
+ for (let i = 1; i < parts.length; i = i + 1) {
216
+ lines.push(String(parts[i] || ""));
217
+ }
218
+ Logger.printPlainLines(lines, { stream: "stderr" });
219
+ }
225
220
  }
226
221
  else if (alreadyLogged) {
227
222
  Logger.error({
@@ -4,7 +4,6 @@
4
4
  */
5
5
  import { CoreError } from "../types/coreError";
6
6
  import { convertBigIntFields } from "../utils/convertBigIntFields";
7
- import { fieldClear } from "../utils/fieldClear";
8
7
  import { toNumberFromSql, toSqlParams } from "../utils/sqlUtil";
9
8
  import { arrayKeysToCamel, isPlainObject, keysToCamel, snakeCase } from "../utils/util";
10
9
  import { DbUtils } from "./dbUtils";
@@ -91,7 +90,7 @@ export class DbHelper {
91
90
  * 统一的查询参数预处理方法
92
91
  */
93
92
  async prepareQueryOptions(options) {
94
- const cleanWhere = fieldClear(options.where || {}, { excludeValues: [null, undefined] });
93
+ const cleanWhere = DbUtils.clearDeep(options.where || {});
95
94
  const hasJoins = options.joins && options.joins.length > 0;
96
95
  // 联查时使用特殊处理逻辑
97
96
  if (hasJoins) {
@@ -723,8 +722,8 @@ export class DbHelper {
723
722
  */
724
723
  async updData(options) {
725
724
  const { table, data, where } = options;
726
- // 清理条件(排除 null 和 undefined
727
- const cleanWhere = fieldClear(where, { excludeValues: [null, undefined] });
725
+ // 清理条件(排除 null 和 undefined,递归)
726
+ const cleanWhere = DbUtils.clearDeep(where);
728
727
  // 转换表名:小驼峰 → 下划线
729
728
  const snakeTable = snakeCase(table);
730
729
  const snakeWhere = DbUtils.whereKeysToSnake(cleanWhere);
@@ -761,8 +760,8 @@ export class DbHelper {
761
760
  const { table, where } = options;
762
761
  // 转换表名:小驼峰 → 下划线
763
762
  const snakeTable = snakeCase(table);
764
- // 清理条件字段
765
- const cleanWhere = fieldClear(where, { excludeValues: [null, undefined] });
763
+ // 清理条件字段(排除 null 和 undefined,递归)
764
+ const cleanWhere = DbUtils.clearDeep(where);
766
765
  const snakeWhere = DbUtils.whereKeysToSnake(cleanWhere);
767
766
  // 物理删除
768
767
  const builder = this.createSqlBuilder().where(snakeWhere);
@@ -850,7 +849,7 @@ export class DbHelper {
850
849
  throw new Error(`exists 不支持 schema.table 写法(table: ${rawTable})`);
851
850
  }
852
851
  const snakeTable = snakeCase(rawTable);
853
- const cleanWhere = fieldClear(options.where || {}, { excludeValues: [null, undefined] });
852
+ const cleanWhere = DbUtils.clearDeep(options.where || {});
854
853
  const snakeWhere = DbUtils.whereKeysToSnake(cleanWhere);
855
854
  const whereFiltered = DbUtils.addDefaultStateFilter(snakeWhere, snakeTable, false);
856
855
  // 使用 COUNT(1) 实现:语义清晰、适配现有返回结构
@@ -955,8 +954,8 @@ export class DbHelper {
955
954
  if (typeof value !== "number" || isNaN(value)) {
956
955
  throw new Error(`自增值必须是有效的数字 (table: ${table}, field: ${field}, value: ${value})`);
957
956
  }
958
- // 清理 where 条件(排除 null 和 undefined
959
- const cleanWhere = fieldClear(where, { excludeValues: [null, undefined] });
957
+ // 清理 where 条件(排除 null 和 undefined,递归)
958
+ const cleanWhere = DbUtils.clearDeep(where);
960
959
  // 转换 where 条件字段名:小驼峰 → 下划线
961
960
  const snakeWhere = DbUtils.whereKeysToSnake(cleanWhere);
962
961
  // 使用 SqlBuilder 构建安全的 WHERE 条件
@@ -10,6 +10,22 @@ type BuildInsertRowOptions = {
10
10
  now: number;
11
11
  };
12
12
  export declare class DbUtils {
13
+ /**
14
+ * 深度剔除 null/undefined(通用工具)。
15
+ *
16
+ * 说明:
17
+ * - 仅处理 object 结构;数组默认原样返回(避免误处理 $in/$between 这类“值数组”)。
18
+ * - 你可以通过 options.arrayObjectKeys 指定“哪些 key 的数组是对象列表,需要递归清理”。
19
+ * 典型场景:where 的 $or/$and。
20
+ * - 通过 options.depth 控制清理深度:
21
+ * - 0(默认):递归处理(无限深度)
22
+ * - N(正整数):最多处理 N 层(1 表示只处理当前这一层)
23
+ * - 注意:数组本身不计入 depth(例如 where 的 $or/$and 是数组;遍历数组元素不会额外消耗层级)
24
+ */
25
+ static clearDeep(value: any, options?: {
26
+ arrayObjectKeys?: string[];
27
+ depth?: number;
28
+ }): any;
13
29
  static parseTableRef(tableRef: string): {
14
30
  schema: string | null;
15
31
  table: string;
@@ -2,6 +2,86 @@ import { fieldClear } from "../utils/fieldClear";
2
2
  import { keysToSnake, snakeCase } from "../utils/util";
3
3
  import { SqlCheck } from "./sqlCheck";
4
4
  export class DbUtils {
5
+ /**
6
+ * 深度剔除 null/undefined(通用工具)。
7
+ *
8
+ * 说明:
9
+ * - 仅处理 object 结构;数组默认原样返回(避免误处理 $in/$between 这类“值数组”)。
10
+ * - 你可以通过 options.arrayObjectKeys 指定“哪些 key 的数组是对象列表,需要递归清理”。
11
+ * 典型场景:where 的 $or/$and。
12
+ * - 通过 options.depth 控制清理深度:
13
+ * - 0(默认):递归处理(无限深度)
14
+ * - N(正整数):最多处理 N 层(1 表示只处理当前这一层)
15
+ * - 注意:数组本身不计入 depth(例如 where 的 $or/$and 是数组;遍历数组元素不会额外消耗层级)
16
+ */
17
+ static clearDeep(value, options) {
18
+ const arrayObjectKeys = Array.isArray(options?.arrayObjectKeys) ? options.arrayObjectKeys : ["$or", "$and"];
19
+ const arrayObjectKeySet = new Set();
20
+ for (const key of arrayObjectKeys) {
21
+ arrayObjectKeySet.add(key);
22
+ }
23
+ const depthRaw = typeof options?.depth === "number" && Number.isFinite(options.depth) ? Math.floor(options.depth) : 0;
24
+ const depth = depthRaw < 0 ? 0 : depthRaw;
25
+ const clearInternal = (input, remainingDepth) => {
26
+ if (!input || typeof input !== "object") {
27
+ return input;
28
+ }
29
+ if (Array.isArray(input)) {
30
+ return input;
31
+ }
32
+ const canRecurse = remainingDepth === 0 || remainingDepth > 1;
33
+ const childDepth = remainingDepth === 0 ? 0 : remainingDepth - 1;
34
+ const result = {};
35
+ for (const [key, item] of Object.entries(input)) {
36
+ if (item === undefined || item === null) {
37
+ continue;
38
+ }
39
+ if (arrayObjectKeySet.has(key)) {
40
+ if (!Array.isArray(item)) {
41
+ continue;
42
+ }
43
+ // 数组不计入 depth:递归进入数组元素时不消耗 remainingDepth
44
+ const arrayChildDepth = remainingDepth;
45
+ const outList = [];
46
+ for (const child of item) {
47
+ if (!child || typeof child !== "object" || Array.isArray(child)) {
48
+ continue;
49
+ }
50
+ const cleaned = clearInternal(child, arrayChildDepth);
51
+ if (!cleaned || typeof cleaned !== "object" || Array.isArray(cleaned)) {
52
+ continue;
53
+ }
54
+ if (Object.keys(cleaned).length === 0) {
55
+ continue;
56
+ }
57
+ outList.push(cleaned);
58
+ }
59
+ if (outList.length > 0) {
60
+ result[key] = outList;
61
+ }
62
+ continue;
63
+ }
64
+ if (typeof item === "object" && !Array.isArray(item)) {
65
+ if (!canRecurse) {
66
+ result[key] = item;
67
+ continue;
68
+ }
69
+ const cleanedObj = clearInternal(item, childDepth);
70
+ if (!cleanedObj || typeof cleanedObj !== "object" || Array.isArray(cleanedObj)) {
71
+ continue;
72
+ }
73
+ if (Object.keys(cleanedObj).length === 0) {
74
+ continue;
75
+ }
76
+ result[key] = cleanedObj;
77
+ continue;
78
+ }
79
+ result[key] = item;
80
+ }
81
+ return result;
82
+ };
83
+ return clearInternal(value, depth);
84
+ }
5
85
  static parseTableRef(tableRef) {
6
86
  if (typeof tableRef !== "string") {
7
87
  throw new Error(`tableRef 必须是字符串 (tableRef: ${String(tableRef)})`);
@@ -1,12 +1,8 @@
1
1
  /**
2
2
  * 日志系统 - Bun 环境自定义实现(替换 pino / pino-roll)
3
3
  */
4
- import type { LoggerConfig, LoggerSink } from "../types/logger";
4
+ import type { LoggerConfig, LoggerSink, LoggerWriteOptions } from "../types/logger";
5
5
  type SinkLogger = LoggerSink;
6
- type LoggerWriteOptions = {
7
- truncate?: boolean;
8
- console?: boolean;
9
- };
10
6
  export declare function flush(): Promise<void>;
11
7
  export declare function shutdown(): Promise<void>;
12
8
  /**
@@ -23,11 +19,21 @@ export declare function setMockLogger(mock: SinkLogger | null): void;
23
19
  */
24
20
  export declare function getLogger(): SinkLogger;
25
21
  declare class LoggerFacade {
26
- private maybeSanitizeForMock;
22
+ private write;
27
23
  info(input: unknown, options?: LoggerWriteOptions): void;
28
24
  warn(input: unknown, options?: LoggerWriteOptions): void;
29
25
  error(input: unknown, options?: LoggerWriteOptions): void;
30
26
  debug(input: unknown, options?: LoggerWriteOptions): void;
27
+ /**
28
+ * 打印多行纯文本到控制台(stdout/stderr)。
29
+ *
30
+ * 说明:
31
+ * - 该方法不写入 JSONL 文件、不做清洗/脱敏/截断;仅用于“人类可读”的多行输出。
32
+ * - 若需要落盘/采集:请先调用 Logger.info/warn/error/debug 写入 JSONL(可选 console:false),再调用本方法输出多行。
33
+ */
34
+ printPlainLines(input: string | string[], options?: {
35
+ stream?: "stdout" | "stderr";
36
+ }): void;
31
37
  flush(): Promise<void>;
32
38
  configure(cfg: LoggerConfig): void;
33
39
  setMock(mock: SinkLogger | null): void;
@@ -21,11 +21,8 @@ let sanitizeOptions = {
21
21
  sanitizeObjectKeys: 500,
22
22
  sensitiveKeyMatcher: buildSensitiveKeyMatcher({ builtinPatterns: BUILTIN_SENSITIVE_KEYS, userPatterns: [] })
23
23
  };
24
- const recordWriteOptions = new WeakMap();
25
- function buildSanitizeOptionsForWrite(writeOptions) {
26
- if (!writeOptions)
27
- return sanitizeOptions;
28
- if (writeOptions.truncate !== false)
24
+ function buildSanitizeOptionsForWriteOptions(writeOptions) {
25
+ if (!writeOptions || writeOptions.truncate !== false)
29
26
  return sanitizeOptions;
30
27
  // 仅关闭“截断”,仍保留敏感字段掩码与结构化清洗(避免泄露敏感信息)。
31
28
  return {
@@ -45,8 +42,6 @@ const HOSTNAME = (() => {
45
42
  return "unknown";
46
43
  }
47
44
  })();
48
- let instance = null;
49
- let errorInstance = null;
50
45
  let mockInstance = null;
51
46
  let appFileSink = null;
52
47
  let errorFileSink = null;
@@ -369,8 +364,6 @@ export async function shutdown() {
369
364
  return;
370
365
  // 重要:shutdown 可能与后续 Logger.getLogger() 并发。
371
366
  // 因此这里捕获“当前的旧 sink/instance 快照”,只关闭这些快照,避免把新创建的 sink 一并清掉。
372
- const currentInstance = instance;
373
- const currentErrorInstance = errorInstance;
374
367
  const currentAppFileSink = appFileSink;
375
368
  const currentErrorFileSink = errorFileSink;
376
369
  const currentAppConsoleSink = appConsoleSink;
@@ -398,12 +391,6 @@ export async function shutdown() {
398
391
  if (appConsoleSink === currentAppConsoleSink) {
399
392
  appConsoleSink = null;
400
393
  }
401
- if (instance === currentInstance) {
402
- instance = null;
403
- }
404
- if (errorInstance === currentErrorInstance) {
405
- errorInstance = null;
406
- }
407
394
  // shutdown 后允许下一次重新初始化时再次校验/创建目录(测试会清理目录,避免 ENOENT)
408
395
  // 无需缓存状态:确保目录存在是幂等的。
409
396
  }
@@ -450,8 +437,6 @@ export function configure(cfg) {
450
437
  mb = 100;
451
438
  config.maxSize = mb;
452
439
  }
453
- instance = null;
454
- errorInstance = null;
455
440
  appFileSink = null;
456
441
  errorFileSink = null;
457
442
  appConsoleSink = null;
@@ -476,32 +461,20 @@ function getSink(kind) {
476
461
  // 优先返回 mock 实例(用于测试)
477
462
  if (mockInstance)
478
463
  return mockInstance;
479
- if (kind === "app") {
480
- if (instance)
481
- return instance;
482
- }
483
- else {
484
- if (errorInstance)
485
- return errorInstance;
486
- }
487
- ensureLogDirExists();
488
- const maxSizeMb = typeof config.maxSize === "number" ? config.maxSize : 20;
489
- const maxFileBytes = Math.floor(maxSizeMb * 1024 * 1024);
490
- if (kind === "app") {
491
- if (!appFileSink) {
492
- appFileSink = new LogFileSink({ prefix: "app", maxFileBytes: maxFileBytes });
493
- }
494
- if (config.console === 1 && !appConsoleSink) {
495
- appConsoleSink = createStreamSink("stdout");
464
+ return {
465
+ info(record) {
466
+ writeJsonl(kind, "info", record, undefined);
467
+ },
468
+ warn(record) {
469
+ writeJsonl(kind, "warn", record, undefined);
470
+ },
471
+ error(record) {
472
+ writeJsonl(kind, "error", record, undefined);
473
+ },
474
+ debug(record) {
475
+ writeJsonl(kind, "debug", record, undefined);
496
476
  }
497
- instance = createSinkLogger({ fileSink: appFileSink, consoleSink: config.console === 1 ? appConsoleSink : null });
498
- return instance;
499
- }
500
- if (!errorFileSink) {
501
- errorFileSink = new LogFileSink({ prefix: "error", maxFileBytes: maxFileBytes });
502
- }
503
- errorInstance = createSinkLogger({ fileSink: errorFileSink, consoleSink: null });
504
- return errorInstance;
477
+ };
505
478
  }
506
479
  /**
507
480
  * 获取 Logger 实例(延迟初始化)
@@ -570,46 +543,39 @@ function safeJsonStringify(obj) {
570
543
  }
571
544
  }
572
545
  }
573
- function createSinkLogger(options) {
574
- const fileSink = options.fileSink;
575
- const consoleSink = options.consoleSink;
576
- const write = (level, record) => {
577
- if (level === "debug" && config.debug !== 1)
578
- return;
579
- const time = Date.now();
580
- const base = buildBaseFields(level, time);
581
- const input = isPlainObject(record) ? record : { value: record };
582
- let writeOptions = null;
583
- if (input && typeof input === "object") {
584
- writeOptions = recordWriteOptions.get(input) ?? null;
585
- if (writeOptions) {
586
- recordWriteOptions.delete(input);
587
- }
546
+ function ensureSinksReady(kind) {
547
+ ensureLogDirExists();
548
+ const maxSizeMb = typeof config.maxSize === "number" ? config.maxSize : 20;
549
+ const maxFileBytes = Math.floor(maxSizeMb * 1024 * 1024);
550
+ if (kind === "app") {
551
+ if (!appFileSink) {
552
+ appFileSink = new LogFileSink({ prefix: "app", maxFileBytes: maxFileBytes });
588
553
  }
589
- const effectiveSanitizeOptions = buildSanitizeOptionsForWrite(writeOptions);
590
- const sanitizedRecord = sanitizeLogObject(input, effectiveSanitizeOptions);
591
- const fileLine = buildLogLineWithBase(base, sanitizedRecord);
592
- fileSink.enqueue(fileLine);
593
- if (consoleSink) {
594
- if (!writeOptions || writeOptions.console !== false) {
595
- consoleSink.enqueue(fileLine);
596
- }
554
+ if (config.console === 1 && !appConsoleSink) {
555
+ appConsoleSink = createStreamSink("stdout");
597
556
  }
598
- };
599
- return {
600
- info(record) {
601
- write("info", record);
602
- },
603
- warn(record) {
604
- write("warn", record);
605
- },
606
- error(record) {
607
- write("error", record);
608
- },
609
- debug(record) {
610
- write("debug", record);
557
+ return { fileSink: appFileSink, consoleSink: config.console === 1 ? appConsoleSink : null };
558
+ }
559
+ if (!errorFileSink) {
560
+ errorFileSink = new LogFileSink({ prefix: "error", maxFileBytes: maxFileBytes });
561
+ }
562
+ return { fileSink: errorFileSink, consoleSink: null };
563
+ }
564
+ function writeJsonl(kind, level, record, options) {
565
+ if (level === "debug" && config.debug !== 1)
566
+ return;
567
+ const sinks = ensureSinksReady(kind);
568
+ const time = Date.now();
569
+ const base = buildBaseFields(level, time);
570
+ const effectiveSanitizeOptions = buildSanitizeOptionsForWriteOptions(options);
571
+ const sanitizedRecord = sanitizeLogObject(record, effectiveSanitizeOptions);
572
+ const fileLine = buildLogLineWithBase(base, sanitizedRecord);
573
+ sinks.fileSink.enqueue(fileLine);
574
+ if (sinks.consoleSink) {
575
+ if (!options || options.console !== false) {
576
+ sinks.consoleSink.enqueue(fileLine);
611
577
  }
612
- };
578
+ }
613
579
  }
614
580
  // 对象清洗/脱敏/截断逻辑已下沉到 utils/loggerUtils.ts(减少 logger.ts 复杂度)。
615
581
  function metaToObject() {
@@ -679,51 +645,72 @@ function withRequestMetaRecord(record) {
679
645
  return mergeMetaIntoObject(record, meta);
680
646
  }
681
647
  class LoggerFacade {
682
- maybeSanitizeForMock(record, options) {
683
- if (!mockInstance)
684
- return record;
685
- const effective = buildSanitizeOptionsForWrite(options ? options : null);
686
- return sanitizeLogObject(record, effective);
687
- }
688
- info(input, options) {
648
+ write(level, input, options) {
649
+ // debug!=1 则完全不记录 debug 日志(包括文件与控制台)
650
+ if (level === "debug" && config.debug !== 1)
651
+ return;
689
652
  const record0 = withRequestMetaRecord(toRecord(input));
690
- if (!mockInstance && options && isPlainObject(record0)) {
691
- recordWriteOptions.set(record0, options);
653
+ // 测试场景:mock logger 走同步写入,并在 facade 层进行清洗/脱敏/截断控制
654
+ if (mockInstance) {
655
+ const effective = buildSanitizeOptionsForWriteOptions(options);
656
+ const sanitized = sanitizeLogObject(record0, effective);
657
+ if (level === "info") {
658
+ mockInstance.info(sanitized);
659
+ }
660
+ else if (level === "warn") {
661
+ mockInstance.warn(sanitized);
662
+ }
663
+ else if (level === "error") {
664
+ mockInstance.error(sanitized);
665
+ }
666
+ else {
667
+ mockInstance.debug(sanitized);
668
+ }
669
+ return;
670
+ }
671
+ writeJsonl("app", level, record0, options);
672
+ if (level === "error") {
673
+ // error 专属文件:始终镜像一份
674
+ writeJsonl("error", "error", record0, options);
692
675
  }
693
- const record = this.maybeSanitizeForMock(record0, options);
694
- getSink("app").info(record);
676
+ }
677
+ info(input, options) {
678
+ this.write("info", input, options);
695
679
  }
696
680
  warn(input, options) {
697
- const record0 = withRequestMetaRecord(toRecord(input));
698
- if (!mockInstance && options && isPlainObject(record0)) {
699
- recordWriteOptions.set(record0, options);
700
- }
701
- const record = this.maybeSanitizeForMock(record0, options);
702
- getSink("app").warn(record);
681
+ this.write("warn", input, options);
703
682
  }
704
683
  error(input, options) {
705
- const record0 = withRequestMetaRecord(toRecord(input));
706
- if (!mockInstance && options && isPlainObject(record0)) {
707
- recordWriteOptions.set(record0, options);
708
- }
709
- const record = this.maybeSanitizeForMock(record0, options);
710
- getSink("app").error(record);
711
- // 测试场景:启用 mock 时不做镜像,避免调用次数翻倍
712
- if (mockInstance)
713
- return;
714
- // error 专属文件:始终镜像一份
715
- getSink("error").error(record);
684
+ this.write("error", input, options);
716
685
  }
717
686
  debug(input, options) {
718
- // debug!=1 则完全不记录 debug 日志(包括文件与控制台)
719
- if (config.debug !== 1)
720
- return;
721
- const record0 = withRequestMetaRecord(toRecord(input));
722
- if (!mockInstance && options && isPlainObject(record0)) {
723
- recordWriteOptions.set(record0, options);
687
+ this.write("debug", input, options);
688
+ }
689
+ /**
690
+ * 打印多行纯文本到控制台(stdout/stderr)。
691
+ *
692
+ * 说明:
693
+ * - 该方法不写入 JSONL 文件、不做清洗/脱敏/截断;仅用于“人类可读”的多行输出。
694
+ * - 若需要落盘/采集:请先调用 Logger.info/warn/error/debug 写入 JSONL(可选 console:false),再调用本方法输出多行。
695
+ */
696
+ printPlainLines(input, options) {
697
+ const kind = options && options.stream === "stderr" ? "stderr" : "stdout";
698
+ const stream = kind === "stderr" ? process.stderr : process.stdout;
699
+ try {
700
+ if (Array.isArray(input)) {
701
+ for (const line of input) {
702
+ stream.write(`${String(line)}\n`);
703
+ }
704
+ return;
705
+ }
706
+ const parts = String(input).split("\n");
707
+ for (let i = 0; i < parts.length; i = i + 1) {
708
+ stream.write(`${parts[i]}\n`);
709
+ }
710
+ }
711
+ catch {
712
+ // ignore
724
713
  }
725
- const record = this.maybeSanitizeForMock(record0, options);
726
- getSink("app").debug(record);
727
714
  }
728
715
  async flush() {
729
716
  await flush();
@@ -188,6 +188,7 @@ export class SyncTable {
188
188
  // plan 阶段补充的兼容性检查:保证“遇到第一条 throw”不会中止汇总。
189
189
  SyncTable.throwIfIncompatibleTypeChanges(incompatibleTypeChanges);
190
190
  // 预检通过后,再执行实际同步(DDL)。
191
+ const createdTables = [];
191
192
  for (const task of tableTasks) {
192
193
  const item = task.item;
193
194
  const tableName = task.tableName;
@@ -229,6 +230,7 @@ export class SyncTable {
229
230
  }
230
231
  else {
231
232
  await SyncTable.createTable(this.db, tableName, tableFields);
233
+ createdTables.push(tableName);
232
234
  }
233
235
  }
234
236
  catch (error) {
@@ -257,6 +259,20 @@ export class SyncTable {
257
259
  throw error;
258
260
  }
259
261
  }
262
+ // 创建表汇总日志:单条输出(避免“一个表一行”刷屏)。
263
+ if (createdTables.length > 0) {
264
+ const lines = [];
265
+ lines.push(`创建表列表(共${createdTables.length}张):`);
266
+ for (const tableName of createdTables) {
267
+ lines.push(`- ${tableName}`);
268
+ }
269
+ const text = lines.join("\n");
270
+ // 说明:
271
+ // - 参考 start() 对 CoreError.multiline 的处理:文件仍写 JSONL,控制台改为原样多行文本。
272
+ // - 这样既不破坏 JSONL(文件/采集端仍可解析),也避免“每个表一条 JSON”刷屏。
273
+ Logger.debug(text, { truncate: false, console: false });
274
+ Logger.printPlainLines(text, { stream: "stdout" });
275
+ }
260
276
  }
261
277
  catch (error) {
262
278
  // 若已在表级 catch 打印过更详细上下文,则这里避免重复打印。
@@ -1063,7 +1079,6 @@ export class SyncTable {
1063
1079
  const { ENGINE, CHARSET, COLLATE } = SyncTable.MYSQL_TABLE_CONFIG;
1064
1080
  const createSQL = `CREATE TABLE ${tableQuoted} (\n ${cols}\n ) ENGINE=${ENGINE} DEFAULT CHARSET=${CHARSET} COLLATE=${COLLATE}`;
1065
1081
  await db.unsafe(createSQL);
1066
- Logger.debug(`[表 ${tableName}] + 创建表(系统字段 + 业务字段)`);
1067
1082
  const indexClauses = [];
1068
1083
  for (const sysField of systemIndexFields) {
1069
1084
  const indexName = `idx_${sysField}`;
@@ -14,7 +14,9 @@ export function normalizeViewDirMeta(input) {
14
14
  return null;
15
15
  }
16
16
  const orderRaw = record["order"];
17
- const order = typeof orderRaw === "number" && Number.isFinite(orderRaw) && Number.isInteger(orderRaw) && orderRaw >= 0 ? orderRaw : undefined;
17
+ // 注意:菜单校验(checkMenu)要求 sort 最小值为 1。
18
+ // 因此 meta.json 的 order 若提供,也必须是整数且 >= 1。
19
+ const order = typeof orderRaw === "number" && Number.isFinite(orderRaw) && Number.isInteger(orderRaw) && orderRaw >= 1 ? orderRaw : undefined;
18
20
  if (order === undefined) {
19
21
  return {
20
22
  title: title
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.16.1",
4
- "gitHead": "effac3f749af5f1395048a1ed528eac7e55eaed5",
3
+ "version": "3.16.4",
4
+ "gitHead": "9823cc29996ba9950dd08bfd72856e3b65815039",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
7
7
  "keywords": [