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/befly.js +179 -142
- package/dist/befly.min.js +20 -18
- package/dist/index.js +11 -16
- package/dist/lib/dbHelper.js +8 -9
- package/dist/lib/dbUtils.d.ts +16 -0
- package/dist/lib/dbUtils.js +80 -0
- package/dist/lib/logger.d.ts +12 -6
- package/dist/lib/logger.js +102 -115
- package/dist/sync/syncTable.js +16 -1
- package/dist/utils/loadMenuConfigs.js +3 -1
- package/package.json +2 -2
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
|
-
|
|
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({
|
package/dist/lib/dbHelper.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
957
|
+
// 清理 where 条件(排除 null 和 undefined,递归)
|
|
958
|
+
const cleanWhere = DbUtils.clearDeep(where);
|
|
960
959
|
// 转换 where 条件字段名:小驼峰 → 下划线
|
|
961
960
|
const snakeWhere = DbUtils.whereKeysToSnake(cleanWhere);
|
|
962
961
|
// 使用 SqlBuilder 构建安全的 WHERE 条件
|
package/dist/lib/dbUtils.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/dbUtils.js
CHANGED
|
@@ -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)})`);
|
package/dist/lib/logger.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/lib/logger.js
CHANGED
|
@@ -21,11 +21,8 @@ let sanitizeOptions = {
|
|
|
21
21
|
sanitizeObjectKeys: 500,
|
|
22
22
|
sensitiveKeyMatcher: buildSensitiveKeyMatcher({ builtinPatterns: BUILTIN_SENSITIVE_KEYS, userPatterns: [] })
|
|
23
23
|
};
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
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
|
|
574
|
-
|
|
575
|
-
const
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
590
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
691
|
-
|
|
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
|
-
|
|
694
|
-
|
|
676
|
+
}
|
|
677
|
+
info(input, options) {
|
|
678
|
+
this.write("info", input, options);
|
|
695
679
|
}
|
|
696
680
|
warn(input, options) {
|
|
697
|
-
|
|
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
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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();
|
package/dist/sync/syncTable.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
4
|
-
"gitHead": "
|
|
3
|
+
"version": "3.16.4",
|
|
4
|
+
"gitHead": "9823cc29996ba9950dd08bfd72856e3b65815039",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
|
|
7
7
|
"keywords": [
|