befly 3.21.1 → 3.21.2
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/README.md +7 -0
- package/apis/dict/all.js +1 -1
- package/apis/dict/detail.js +1 -1
- package/apis/dict/list.js +1 -1
- package/checks/config.js +1 -3
- package/checks/table.js +2 -15
- package/configs/beflyConfig.json +1 -3
- package/index.js +5 -10
- package/lib/dbHelper.js +73 -27
- package/lib/dbUtil.js +136 -35
- package/lib/logger.js +21 -45
- package/package.json +2 -2
- package/plugins/mysql.js +2 -1
- package/scripts/syncDb/context.js +62 -47
- package/scripts/syncDb/diff.js +78 -15
- package/scripts/syncDb/index.js +16 -46
- package/scripts/syncDb/report.js +97 -98
- package/tables/admin.json +24 -0
- package/tables/api.json +24 -0
- package/tables/dict.json +24 -0
- package/tables/dictType.json +24 -0
- package/tables/emailLog.json +24 -0
- package/tables/errorReport.json +140 -0
- package/tables/infoReport.json +123 -0
- package/tables/loginLog.json +24 -0
- package/tables/menu.json +24 -0
- package/tables/operateLog.json +24 -0
- package/tables/role.json +24 -0
- package/tables/sysConfig.json +24 -0
- package/utils/loggerUtils.js +9 -14
- package/utils/scanSources.js +3 -3
- package/scripts/syncDb/query.js +0 -26
package/README.md
CHANGED
|
@@ -9,3 +9,10 @@
|
|
|
9
9
|
"bundle:min": "bun build ./index.js --outfile ./befly.min.js --target bun --format esm --packages bundle --minify",
|
|
10
10
|
"build": "rimraf befly.js befly.min.js && bun run bundle && bun run bundle:min"
|
|
11
11
|
```
|
|
12
|
+
|
|
13
|
+
我们把要求和实现对齐一下,要符合如下规范:
|
|
14
|
+
|
|
15
|
+
1. 单表查询,不传fields或fields为空数组,表示查询所有字段。
|
|
16
|
+
2. 单表查询,fields指定查询字段,表示明确只查询这些字段。
|
|
17
|
+
3. 单表查询,fields用!感叹号开头,表示排除不查询这些字段。
|
|
18
|
+
4. 单表查询,不存在明确查询字段又排除某些字段的混合情况。
|
package/apis/dict/all.js
CHANGED
|
@@ -12,7 +12,7 @@ export default {
|
|
|
12
12
|
const result = await befly.mysql.getAll({
|
|
13
13
|
table: ["beflyDict", "beflyDictType"],
|
|
14
14
|
leftJoin: ["beflyDict.typeCode beflyDictType.code"],
|
|
15
|
-
fields: ["beflyDict.id", "beflyDict.typeCode", "beflyDict.key", "beflyDict.label", "beflyDict.sort", "beflyDict.remark", "beflyDict.createdAt", "beflyDict.updatedAt", "beflyDictType.name
|
|
15
|
+
fields: ["beflyDict.id", "beflyDict.typeCode", "beflyDict.key", "beflyDict.label", "beflyDict.sort", "beflyDict.remark", "beflyDict.createdAt", "beflyDict.updatedAt", "beflyDictType.name typeName"],
|
|
16
16
|
orderBy: ["beflyDict.sort#ASC", "beflyDict.id#ASC"]
|
|
17
17
|
});
|
|
18
18
|
|
package/apis/dict/detail.js
CHANGED
|
@@ -11,7 +11,7 @@ export default {
|
|
|
11
11
|
const dict = await befly.mysql.getOne({
|
|
12
12
|
table: ["beflyDict", "beflyDictType"],
|
|
13
13
|
leftJoin: ["beflyDict.typeCode beflyDictType.code"],
|
|
14
|
-
fields: ["beflyDict.id", "beflyDict.typeCode", "beflyDict.key", "beflyDict.label", "beflyDict.sort", "beflyDict.remark", "beflyDict.createdAt", "beflyDict.updatedAt", "beflyDictType.name
|
|
14
|
+
fields: ["beflyDict.id", "beflyDict.typeCode", "beflyDict.key", "beflyDict.label", "beflyDict.sort", "beflyDict.remark", "beflyDict.createdAt", "beflyDict.updatedAt", "beflyDictType.name typeName"],
|
|
15
15
|
where: { "beflyDict.id": ctx.body.id }
|
|
16
16
|
});
|
|
17
17
|
|
package/apis/dict/list.js
CHANGED
|
@@ -16,7 +16,7 @@ export default {
|
|
|
16
16
|
const result = await befly.mysql.getList({
|
|
17
17
|
table: ["beflyDict", "beflyDictType"],
|
|
18
18
|
leftJoin: ["beflyDict.typeCode beflyDictType.code"],
|
|
19
|
-
fields: ["beflyDict.id", "beflyDict.typeCode", "beflyDict.key", "beflyDict.label", "beflyDict.sort", "beflyDict.remark", "beflyDict.createdAt", "beflyDict.updatedAt", "beflyDictType.name
|
|
19
|
+
fields: ["beflyDict.id", "beflyDict.typeCode", "beflyDict.key", "beflyDict.label", "beflyDict.sort", "beflyDict.remark", "beflyDict.createdAt", "beflyDict.updatedAt", "beflyDictType.name typeName"],
|
|
20
20
|
where: {
|
|
21
21
|
"beflyDict.typeCode": ctx.body.typeCode
|
|
22
22
|
},
|
package/checks/config.js
CHANGED
|
@@ -29,9 +29,7 @@ const configSchema = z
|
|
|
29
29
|
excludeFields: z.array(noTrimString),
|
|
30
30
|
dir: noTrimString,
|
|
31
31
|
console: boolIntSchema,
|
|
32
|
-
maxSize: z.int().min(1)
|
|
33
|
-
maxStringLen: z.int().min(1),
|
|
34
|
-
maxArrayItems: z.int().min(1)
|
|
32
|
+
maxSize: z.int().min(1)
|
|
35
33
|
})
|
|
36
34
|
.strict(),
|
|
37
35
|
|
package/checks/table.js
CHANGED
|
@@ -76,20 +76,7 @@ const fieldDefSchema = z
|
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
const tableContentSchema = z.record(z.string().regex(lowerCamelRegex), fieldDefSchema);
|
|
79
|
-
|
|
80
|
-
const tableItemSchema = z
|
|
81
|
-
.object({
|
|
82
|
-
source: noTrimString.min(1),
|
|
83
|
-
type: noTrimString.min(1),
|
|
84
|
-
filePath: noTrimString.min(1),
|
|
85
|
-
relativePath: noTrimString.min(1),
|
|
86
|
-
apiPath: noTrimString.min(1),
|
|
87
|
-
fileName: z.string().min(1).regex(lowerCamelRegex),
|
|
88
|
-
fieldsDef: tableContentSchema
|
|
89
|
-
})
|
|
90
|
-
.strict();
|
|
91
|
-
|
|
92
|
-
const tableListSchema = z.array(tableItemSchema);
|
|
79
|
+
const tableRegistrySchema = z.record(z.string().regex(lowerCamelRegex), tableContentSchema);
|
|
93
80
|
|
|
94
81
|
/**
|
|
95
82
|
* 检查表定义文件
|
|
@@ -99,7 +86,7 @@ export async function checkTable(tables) {
|
|
|
99
86
|
// 收集所有表文件
|
|
100
87
|
let hasError = false;
|
|
101
88
|
|
|
102
|
-
const schemaResult =
|
|
89
|
+
const schemaResult = tableRegistrySchema.safeParse(tables);
|
|
103
90
|
if (!schemaResult.success) {
|
|
104
91
|
const errors = formatZodIssues(schemaResult.error.issues, { items: tables, itemLabel: "table" });
|
|
105
92
|
Logger.warn("表结构校验失败", { errors: errors }, false);
|
package/configs/beflyConfig.json
CHANGED
package/index.js
CHANGED
|
@@ -26,13 +26,14 @@ import { syncApi } from "./sync/api.js";
|
|
|
26
26
|
import { syncCache } from "./sync/cache.js";
|
|
27
27
|
import { syncDev } from "./sync/dev.js";
|
|
28
28
|
import { syncMenu } from "./sync/menu.js";
|
|
29
|
-
import { syncDbApply, syncDbCheck } from "./scripts/syncDb/index.js";
|
|
30
29
|
// 工具
|
|
31
30
|
import { calcPerfTime } from "./utils/calcPerfTime.js";
|
|
32
31
|
import { scanSources } from "./utils/scanSources.js";
|
|
33
32
|
import { isPrimaryProcess } from "./utils/is.js";
|
|
34
33
|
import { deepMerge } from "./utils/deepMerge.js";
|
|
35
34
|
|
|
35
|
+
export { syncDbApply as syncDb } from "./scripts/syncDb/index.js";
|
|
36
|
+
|
|
36
37
|
function prefixMenuPaths(menus, prefix) {
|
|
37
38
|
const output = [];
|
|
38
39
|
for (const menu of menus) {
|
|
@@ -94,14 +95,6 @@ async function ensureSyncPrerequisites(ctx) {
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
export async function dbCheck(mysqlConfig) {
|
|
98
|
-
return syncDbCheck(mysqlConfig);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export async function dbApply(mysqlConfig) {
|
|
102
|
-
return syncDbApply(mysqlConfig);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
98
|
export async function createBefly(env = {}, config = {}, menus = []) {
|
|
106
99
|
const mergedConfig = deepMerge(beflyConfig, config);
|
|
107
100
|
const mergedMenus = deepMerge(prefixMenuPaths(beflyMenus, "core"), menus);
|
|
@@ -127,6 +120,7 @@ export async function createBefly(env = {}, config = {}, menus = []) {
|
|
|
127
120
|
return new Befly({
|
|
128
121
|
env: env,
|
|
129
122
|
config: mergedConfig,
|
|
123
|
+
tables: tables,
|
|
130
124
|
menus: mergedMenus,
|
|
131
125
|
apis: apis,
|
|
132
126
|
hooks: hooks,
|
|
@@ -143,7 +137,8 @@ export class Befly {
|
|
|
143
137
|
constructor(init = {}) {
|
|
144
138
|
this.context = {
|
|
145
139
|
env: init.env || {},
|
|
146
|
-
config: init.config || {}
|
|
140
|
+
config: init.config || {},
|
|
141
|
+
tables: init.tables || {}
|
|
147
142
|
};
|
|
148
143
|
this.menus = Array.isArray(init.menus) ? init.menus : [];
|
|
149
144
|
this.hooks = Array.isArray(init.hooks) ? init.hooks : [];
|
package/lib/dbHelper.js
CHANGED
|
@@ -614,7 +614,7 @@ function validateDataReadOptions(options, label) {
|
|
|
614
614
|
validateQueryOptions(options, label);
|
|
615
615
|
|
|
616
616
|
const hasLeftJoin = Array.isArray(options.leftJoin) && options.leftJoin.length > 0;
|
|
617
|
-
const classifiedFields = hasLeftJoin ? validateExplicitLeftJoinFields(options.fields) : validateExplicitReadFields(options.fields);
|
|
617
|
+
const classifiedFields = hasLeftJoin ? validateExplicitLeftJoinFields(options.fields, options.table[0]) : validateExplicitReadFields(options.fields);
|
|
618
618
|
|
|
619
619
|
return {
|
|
620
620
|
hasLeftJoin: hasLeftJoin,
|
|
@@ -661,6 +661,30 @@ class DbHelper {
|
|
|
661
661
|
this.sql = options.sql || null;
|
|
662
662
|
this.isTransaction = options.isTransaction === true;
|
|
663
663
|
this.beflyMode = options.beflyMode === "manual" ? "manual" : "auto";
|
|
664
|
+
this.tables = isPlainObject(options.tables) ? options.tables : {};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
getTableDef(table) {
|
|
668
|
+
const tableName = parseTableRef(table).table;
|
|
669
|
+
|
|
670
|
+
if (!/^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/.test(tableName)) {
|
|
671
|
+
throw new Error(`表名必须使用小驼峰写法 (table: ${tableName})`, {
|
|
672
|
+
cause: null,
|
|
673
|
+
code: "validation"
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const tableInfo = this.tables[tableName];
|
|
678
|
+
|
|
679
|
+
if (!isPlainObject(tableInfo)) {
|
|
680
|
+
const sourceLabel = tableName.startsWith("befly") ? "packages/core/tables" : "app tables";
|
|
681
|
+
throw new Error(`表 ${tableName} 缺少本地 tables 定义,请检查 ${sourceLabel}`, {
|
|
682
|
+
cause: null,
|
|
683
|
+
code: "runtime"
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return tableInfo;
|
|
664
688
|
}
|
|
665
689
|
|
|
666
690
|
async execute(sql, params) {
|
|
@@ -815,12 +839,45 @@ class DbHelper {
|
|
|
815
839
|
return normalizeBigIntValues(deserializedList);
|
|
816
840
|
}
|
|
817
841
|
|
|
818
|
-
_joinFieldsToSnake(classifiedFields) {
|
|
819
|
-
if (classifiedFields.type
|
|
820
|
-
return
|
|
842
|
+
async _joinFieldsToSnake(mainTableRef, classifiedFields) {
|
|
843
|
+
if (classifiedFields.type !== "join") {
|
|
844
|
+
return [];
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const processedFields = [];
|
|
848
|
+
const seenFields = new Set();
|
|
849
|
+
const pushField = (field) => {
|
|
850
|
+
if (seenFields.has(field)) {
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
seenFields.add(field);
|
|
854
|
+
processedFields.push(field);
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
if (classifiedFields.mainAllIncluded) {
|
|
858
|
+
const mainColumns = await this.getTableColumns(mainTableRef);
|
|
859
|
+
const excludeFieldSet = new Set(classifiedFields.mainExcludeFields.map((field) => snakeCase(field)));
|
|
860
|
+
|
|
861
|
+
for (const column of mainColumns) {
|
|
862
|
+
if (excludeFieldSet.has(column)) {
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
pushField(`${classifiedFields.mainQualifier}.${column}`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
for (const field of classifiedFields.includeFields) {
|
|
870
|
+
pushField(processJoinField(field));
|
|
821
871
|
}
|
|
822
872
|
|
|
823
|
-
|
|
873
|
+
if (processedFields.length === 0) {
|
|
874
|
+
throw new Error(`排除字段后没有剩余字段可查询。表: ${mainTableRef}, 排除: ${classifiedFields.mainExcludeFields.join(", ")}`, {
|
|
875
|
+
cause: null,
|
|
876
|
+
code: "validation"
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return processedFields;
|
|
824
881
|
}
|
|
825
882
|
|
|
826
883
|
_cleanAndSnakeAndSerializeWriteData(data, excludeValues = [null, undefined]) {
|
|
@@ -874,9 +931,9 @@ class DbHelper {
|
|
|
874
931
|
return this._prepareWriteUserData(options.data, options.allowState);
|
|
875
932
|
}
|
|
876
933
|
|
|
877
|
-
normalizeLeftJoinOptions(options, cleanWhere, classifiedFields) {
|
|
878
|
-
const processedFields = this._joinFieldsToSnake(classifiedFields);
|
|
934
|
+
async normalizeLeftJoinOptions(options, cleanWhere, classifiedFields) {
|
|
879
935
|
const mainTableRef = options.table[0];
|
|
936
|
+
const processedFields = await this._joinFieldsToSnake(mainTableRef, classifiedFields);
|
|
880
937
|
const joinTableRefs = options.table.slice(1);
|
|
881
938
|
const normalizedTableRef = normalizeTableRef(mainTableRef);
|
|
882
939
|
const mainQualifier = getJoinMainQualifier(mainTableRef);
|
|
@@ -916,7 +973,7 @@ class DbHelper {
|
|
|
916
973
|
async normalizeSingleTableOptions(options, cleanWhere, classifiedFields) {
|
|
917
974
|
const tableRef = Array.isArray(options.table) ? options.table[0] : options.table;
|
|
918
975
|
const snakeTable = snakeCase(tableRef);
|
|
919
|
-
const processedFields = await fieldsToSnake(
|
|
976
|
+
const processedFields = await fieldsToSnake(tableRef, classifiedFields, this.getTableColumns.bind(this));
|
|
920
977
|
|
|
921
978
|
return {
|
|
922
979
|
table: snakeTable,
|
|
@@ -967,29 +1024,17 @@ class DbHelper {
|
|
|
967
1024
|
}
|
|
968
1025
|
|
|
969
1026
|
async getTableColumns(table) {
|
|
970
|
-
const
|
|
971
|
-
const
|
|
972
|
-
const quotedTable = quoteIdentMySql(snakeCase(parsed.table));
|
|
973
|
-
const executeRes = await this.execute(`SHOW COLUMNS FROM ${schemaPart}${quotedTable}`, []);
|
|
974
|
-
const result = executeRes.data;
|
|
1027
|
+
const tableInfo = this.getTableDef(table);
|
|
1028
|
+
const fieldNames = Object.keys(tableInfo);
|
|
975
1029
|
|
|
976
|
-
if (
|
|
977
|
-
throw new Error(`表 ${table}
|
|
1030
|
+
if (fieldNames.length === 0) {
|
|
1031
|
+
throw new Error(`表 ${table} 的本地 tables 定义为空或没有字段`, {
|
|
978
1032
|
cause: null,
|
|
979
1033
|
code: "runtime"
|
|
980
1034
|
});
|
|
981
1035
|
}
|
|
982
1036
|
|
|
983
|
-
|
|
984
|
-
for (const row of result) {
|
|
985
|
-
const record = row;
|
|
986
|
-
const name = record.Field;
|
|
987
|
-
if (isNonEmptyString(name)) {
|
|
988
|
-
columnNames.push(name);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
return columnNames;
|
|
1037
|
+
return fieldNames.map((fieldName) => snakeCase(fieldName));
|
|
993
1038
|
}
|
|
994
1039
|
|
|
995
1040
|
applyLeftJoins(builder, leftJoins) {
|
|
@@ -1014,7 +1059,7 @@ class DbHelper {
|
|
|
1014
1059
|
|
|
1015
1060
|
async buildPreparedReadOptions(options, cleanWhere, validation) {
|
|
1016
1061
|
if (validation.hasLeftJoin) {
|
|
1017
|
-
return this.normalizeLeftJoinOptions(options, cleanWhere, validation.classifiedFields);
|
|
1062
|
+
return await this.normalizeLeftJoinOptions(options, cleanWhere, validation.classifiedFields);
|
|
1018
1063
|
}
|
|
1019
1064
|
|
|
1020
1065
|
return await this.normalizeSingleTableOptions(options, cleanWhere, validation.classifiedFields);
|
|
@@ -1600,7 +1645,8 @@ class DbHelper {
|
|
|
1600
1645
|
dbName: this.dbName,
|
|
1601
1646
|
sql: tx,
|
|
1602
1647
|
isTransaction: true,
|
|
1603
|
-
beflyMode: this.beflyMode
|
|
1648
|
+
beflyMode: this.beflyMode,
|
|
1649
|
+
tables: this.tables
|
|
1604
1650
|
});
|
|
1605
1651
|
const result = await callback(trans);
|
|
1606
1652
|
if (result?.code !== undefined && result.code !== 0) {
|
package/lib/dbUtil.js
CHANGED
|
@@ -299,33 +299,113 @@ export function validateAndClassifyFields(fields) {
|
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
export function validateExplicitReadFields(fields) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
302
|
+
return validateAndClassifyFields(fields);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function getTableQualifier(tableRef) {
|
|
306
|
+
const parsed = parseTableRef(tableRef);
|
|
307
|
+
if (parsed.alias) {
|
|
308
|
+
return snakeCase(parsed.alias);
|
|
308
309
|
}
|
|
309
310
|
|
|
310
|
-
return
|
|
311
|
+
return snakeCase(parsed.table);
|
|
311
312
|
}
|
|
312
313
|
|
|
313
|
-
export function validateExplicitLeftJoinFields(fields) {
|
|
314
|
-
|
|
315
|
-
if (classified.type === "all") {
|
|
314
|
+
export function validateExplicitLeftJoinFields(fields, mainTableRef) {
|
|
315
|
+
if (!fields || fields.length === 0) {
|
|
316
316
|
throw new Error("leftJoin 查询必须显式传 fields,不支持空 fields 或查询全部字段", {
|
|
317
317
|
cause: null,
|
|
318
318
|
code: "validation"
|
|
319
319
|
});
|
|
320
320
|
}
|
|
321
|
-
|
|
322
|
-
|
|
321
|
+
|
|
322
|
+
const mainQualifier = getTableQualifier(mainTableRef);
|
|
323
|
+
const mainWildcard = `${mainQualifier}.*`;
|
|
324
|
+
const includeFields = [];
|
|
325
|
+
const mainExcludeFields = [];
|
|
326
|
+
let mainAllIncluded = false;
|
|
327
|
+
|
|
328
|
+
for (const rawField of fields) {
|
|
329
|
+
if (!isNonEmptyString(rawField)) {
|
|
330
|
+
throw new Error("fields 不能包含空字符串或无效值", {
|
|
331
|
+
cause: null,
|
|
332
|
+
code: "validation"
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const trimmed = rawField.trim();
|
|
337
|
+
if (trimmed === "*") {
|
|
338
|
+
throw new Error("fields 不支持 * 星号,请使用空数组 [] 或不传参数表示查询所有字段", {
|
|
339
|
+
cause: null,
|
|
340
|
+
code: "validation"
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (trimmed.startsWith("!")) {
|
|
345
|
+
const fieldPart = trimmed.substring(1).trim();
|
|
346
|
+
if (!isNonEmptyString(fieldPart)) {
|
|
347
|
+
throw new Error("fields 不能包含空字符串或无效值", {
|
|
348
|
+
cause: null,
|
|
349
|
+
code: "validation"
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
if (/\s/.test(fieldPart)) {
|
|
353
|
+
throw new Error("leftJoin 排除字段不支持别名", {
|
|
354
|
+
cause: null,
|
|
355
|
+
code: "validation"
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
assertNoExprField(fieldPart);
|
|
360
|
+
const normalizedField = normalizeQualifierField(fieldPart);
|
|
361
|
+
if (normalizedField === mainWildcard) {
|
|
362
|
+
throw new Error("leftJoin 主表排除字段不能使用 *", {
|
|
363
|
+
cause: null,
|
|
364
|
+
code: "validation"
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (!normalizedField.startsWith(`${mainQualifier}.`)) {
|
|
368
|
+
throw new Error("leftJoin 排除字段仅支持主表字段,请使用主表限定符", {
|
|
369
|
+
cause: null,
|
|
370
|
+
code: "validation"
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
mainExcludeFields.push(normalizedField.substring(mainQualifier.length + 1));
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
assertNoExprField(trimmed);
|
|
379
|
+
const aliasParts = parseFieldAliasParts(trimmed);
|
|
380
|
+
const normalizedField = normalizeQualifierField(aliasParts ? aliasParts.fieldPart.trim() : trimmed);
|
|
381
|
+
if (normalizedField === mainWildcard) {
|
|
382
|
+
if (aliasParts) {
|
|
383
|
+
throw new Error("leftJoin 主表全字段不支持别名", {
|
|
384
|
+
cause: null,
|
|
385
|
+
code: "validation"
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
mainAllIncluded = true;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
includeFields.push(trimmed);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (mainExcludeFields.length > 0 && !mainAllIncluded) {
|
|
396
|
+
throw new Error("leftJoin 使用主表排除字段时,必须同时传主表限定符.*", {
|
|
323
397
|
cause: null,
|
|
324
398
|
code: "validation"
|
|
325
399
|
});
|
|
326
400
|
}
|
|
327
401
|
|
|
328
|
-
return
|
|
402
|
+
return {
|
|
403
|
+
type: "join",
|
|
404
|
+
includeFields: includeFields,
|
|
405
|
+
mainAllIncluded: mainAllIncluded,
|
|
406
|
+
mainExcludeFields: mainExcludeFields,
|
|
407
|
+
mainQualifier: mainQualifier
|
|
408
|
+
};
|
|
329
409
|
}
|
|
330
410
|
|
|
331
411
|
function isQuotedIdentPaired(value) {
|
|
@@ -390,6 +470,41 @@ export function startsWithQuote(value) {
|
|
|
390
470
|
return startsWithIdentifierQuote(value);
|
|
391
471
|
}
|
|
392
472
|
|
|
473
|
+
function parseFieldAliasParts(field) {
|
|
474
|
+
if (!isString(field)) {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const trimmed = field.trim();
|
|
479
|
+
if (!trimmed) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (/\s+AS\s+/i.test(trimmed)) {
|
|
484
|
+
throw new Error(`字段别名不支持 AS,请使用单个空格分隔字段与别名 (field: ${trimmed})`, {
|
|
485
|
+
cause: null,
|
|
486
|
+
code: "validation"
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (!/\s/.test(trimmed)) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (!/^[^\s]+ [^\s]+$/.test(trimmed)) {
|
|
495
|
+
throw new Error(`字段别名必须使用单个空格分隔字段与别名 (field: ${trimmed})`, {
|
|
496
|
+
cause: null,
|
|
497
|
+
code: "validation"
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const parts = trimmed.split(" ");
|
|
502
|
+
return {
|
|
503
|
+
fieldPart: parts[0],
|
|
504
|
+
aliasPart: parts[1]
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
393
508
|
export function escapeField(field, quoteIdent) {
|
|
394
509
|
if (!isString(field)) {
|
|
395
510
|
return field;
|
|
@@ -412,19 +527,10 @@ export function escapeField(field, quoteIdent) {
|
|
|
412
527
|
});
|
|
413
528
|
}
|
|
414
529
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
cause: null,
|
|
420
|
-
code: "validation"
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const fieldPart = parts[0];
|
|
425
|
-
const aliasPart = parts[1];
|
|
426
|
-
const cleanFieldPart = fieldPart.trim();
|
|
427
|
-
const cleanAliasPart = aliasPart.trim();
|
|
530
|
+
const aliasParts = parseFieldAliasParts(trimmed);
|
|
531
|
+
if (aliasParts) {
|
|
532
|
+
const cleanFieldPart = aliasParts.fieldPart.trim();
|
|
533
|
+
const cleanAliasPart = aliasParts.aliasPart.trim();
|
|
428
534
|
if (!isQuotedIdent(cleanAliasPart)) {
|
|
429
535
|
if (!SAFE_IDENTIFIER_RE.test(cleanAliasPart)) {
|
|
430
536
|
throw new Error(`无效的字段别名: ${cleanAliasPart}`, {
|
|
@@ -433,7 +539,7 @@ export function escapeField(field, quoteIdent) {
|
|
|
433
539
|
});
|
|
434
540
|
}
|
|
435
541
|
}
|
|
436
|
-
return `${escapeField(cleanFieldPart, quoteIdent)}
|
|
542
|
+
return `${escapeField(cleanFieldPart, quoteIdent)} ${cleanAliasPart}`;
|
|
437
543
|
}
|
|
438
544
|
|
|
439
545
|
if (trimmed.includes(".")) {
|
|
@@ -805,14 +911,9 @@ export function processJoinField(field) {
|
|
|
805
911
|
return field;
|
|
806
912
|
}
|
|
807
913
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
const aliasPart = parts[1];
|
|
812
|
-
if (!isString(fieldPart) || !isString(aliasPart)) {
|
|
813
|
-
return field;
|
|
814
|
-
}
|
|
815
|
-
return `${processJoinField(fieldPart.trim())} AS ${aliasPart.trim()}`;
|
|
914
|
+
const aliasParts = parseFieldAliasParts(field);
|
|
915
|
+
if (aliasParts) {
|
|
916
|
+
return `${processJoinField(aliasParts.fieldPart.trim())} ${aliasParts.aliasPart.trim()}`;
|
|
816
917
|
}
|
|
817
918
|
|
|
818
919
|
return normalizeQualifierField(field);
|
package/lib/logger.js
CHANGED
|
@@ -19,28 +19,12 @@ const RUNTIME_LOG_DIR = nodePathResolve(INITIAL_CWD, "logs");
|
|
|
19
19
|
const BUILTIN_SENSITIVE_KEYS = ["*password*", "pass", "pwd", "*token*", "access_token", "refresh_token", "accessToken", "refreshToken", "authorization", "cookie", "set-cookie", "*secret*", "apiKey", "api_key", "privateKey", "private_key"];
|
|
20
20
|
|
|
21
21
|
let sanitizeOptions = {
|
|
22
|
-
maxStringLen: 200,
|
|
23
|
-
maxArrayItems: 500,
|
|
24
22
|
sanitizeDepth: 5,
|
|
25
23
|
sanitizeNodes: 5000,
|
|
26
24
|
sanitizeObjectKeys: 500,
|
|
27
25
|
sensitiveKeyMatcher: buildSensitiveKeyMatcher({ builtinPatterns: BUILTIN_SENSITIVE_KEYS, userPatterns: [] })
|
|
28
26
|
};
|
|
29
27
|
|
|
30
|
-
function buildSanitizeOptionsForWriteOptions(writeOptions) {
|
|
31
|
-
if (!writeOptions || writeOptions.truncate !== false) return sanitizeOptions;
|
|
32
|
-
|
|
33
|
-
// 仅关闭“截断”,仍保留敏感字段掩码与结构化清洗(避免泄露敏感信息)。
|
|
34
|
-
return {
|
|
35
|
-
maxStringLen: 200000,
|
|
36
|
-
maxArrayItems: 5000,
|
|
37
|
-
sanitizeDepth: sanitizeOptions.sanitizeDepth,
|
|
38
|
-
sanitizeNodes: sanitizeOptions.sanitizeNodes,
|
|
39
|
-
sanitizeObjectKeys: sanitizeOptions.sanitizeObjectKeys,
|
|
40
|
-
sensitiveKeyMatcher: sanitizeOptions.sensitiveKeyMatcher
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
28
|
let mockInstance = null;
|
|
45
29
|
|
|
46
30
|
let appFileSink = null;
|
|
@@ -381,10 +365,8 @@ export function configure(cfg) {
|
|
|
381
365
|
appFileSink = null;
|
|
382
366
|
errorFileSink = null;
|
|
383
367
|
|
|
384
|
-
//
|
|
368
|
+
// 运行时清洗上限:只保留深度/节点/对象键数量保护。
|
|
385
369
|
sanitizeOptions = {
|
|
386
|
-
maxStringLen: normalizePositiveInt(config.maxStringLen, 200, 20, 200000),
|
|
387
|
-
maxArrayItems: normalizePositiveInt(config.maxArrayItems, 500, 10, 5000),
|
|
388
370
|
sanitizeDepth: normalizePositiveInt(config.sanitizeDepth, 5, 1, 10),
|
|
389
371
|
sanitizeNodes: normalizePositiveInt(config.sanitizeNodes, 5000, 50, 20000),
|
|
390
372
|
sanitizeObjectKeys: normalizePositiveInt(config.sanitizeObjectKeys, 500, 10, 5000),
|
|
@@ -476,16 +458,15 @@ function ensureSinksReady(kind) {
|
|
|
476
458
|
return { fileSink: errorFileSink };
|
|
477
459
|
}
|
|
478
460
|
|
|
479
|
-
function writeJsonl(kind, level, record
|
|
461
|
+
function writeJsonl(kind, level, record) {
|
|
480
462
|
const sinks = ensureSinksReady(kind);
|
|
481
463
|
const time = Date.now();
|
|
482
|
-
const
|
|
483
|
-
const sanitizedRecord = sanitizeLogObject(record, effectiveSanitizeOptions);
|
|
464
|
+
const sanitizedRecord = sanitizeLogObject(record, sanitizeOptions);
|
|
484
465
|
const fileLine = buildJsonLine(level, time, sanitizedRecord);
|
|
485
466
|
sinks.fileSink.enqueue(fileLine);
|
|
486
467
|
}
|
|
487
468
|
|
|
488
|
-
//
|
|
469
|
+
// 对象清洗/脱敏逻辑已下沉到 utils/loggerUtils.js(减少 logger.js 复杂度)。
|
|
489
470
|
|
|
490
471
|
// 日志仅接受 1 个入参(任意类型)。
|
|
491
472
|
// - plain object({})直接作为 record
|
|
@@ -519,16 +500,15 @@ function toRecord(input) {
|
|
|
519
500
|
}
|
|
520
501
|
}
|
|
521
502
|
|
|
522
|
-
function logWrite(level, input
|
|
503
|
+
function logWrite(level, input) {
|
|
523
504
|
// debug!=1/true 则不记录 debug 日志
|
|
524
505
|
if (level === "debug" && config.debug !== 1 && config.debug !== true) return;
|
|
525
506
|
|
|
526
507
|
const record0 = toRecord(input);
|
|
527
508
|
|
|
528
|
-
// 测试场景:mock logger
|
|
509
|
+
// 测试场景:mock logger 走同步写入,并在入口进行清洗/脱敏控制
|
|
529
510
|
if (mockInstance) {
|
|
530
|
-
const
|
|
531
|
-
const sanitized = sanitizeLogObject(record0, effective);
|
|
511
|
+
const sanitized = sanitizeLogObject(record0, sanitizeOptions);
|
|
532
512
|
if (level === "info") {
|
|
533
513
|
mockInstance.info(sanitized);
|
|
534
514
|
} else if (level === "warn") {
|
|
@@ -543,9 +523,9 @@ function logWrite(level, input, options) {
|
|
|
543
523
|
|
|
544
524
|
if (level === "error") {
|
|
545
525
|
// error 仅写入 error 文件
|
|
546
|
-
writeJsonl("error", "error", record0
|
|
526
|
+
writeJsonl("error", "error", record0);
|
|
547
527
|
} else {
|
|
548
|
-
writeJsonl("app", level, record0
|
|
528
|
+
writeJsonl("app", level, record0);
|
|
549
529
|
}
|
|
550
530
|
}
|
|
551
531
|
|
|
@@ -553,41 +533,37 @@ function logWrite(level, input, options) {
|
|
|
553
533
|
* 日志实例(延迟初始化)
|
|
554
534
|
*/
|
|
555
535
|
export const Logger = {
|
|
556
|
-
info(msg, data
|
|
557
|
-
const options = truncate === false ? { truncate: false } : undefined;
|
|
536
|
+
info(msg, data) {
|
|
558
537
|
if (isPlainObject(msg)) {
|
|
559
|
-
logWrite("info", msg
|
|
538
|
+
logWrite("info", msg);
|
|
560
539
|
return;
|
|
561
540
|
}
|
|
562
541
|
|
|
563
|
-
logWrite("info", { msg: msg, data: data }
|
|
542
|
+
logWrite("info", { msg: msg, data: data });
|
|
564
543
|
},
|
|
565
|
-
warn(msg, data
|
|
566
|
-
const options = truncate === false ? { truncate: false } : undefined;
|
|
544
|
+
warn(msg, data) {
|
|
567
545
|
if (isPlainObject(msg)) {
|
|
568
|
-
logWrite("warn", msg
|
|
546
|
+
logWrite("warn", msg);
|
|
569
547
|
return;
|
|
570
548
|
}
|
|
571
549
|
|
|
572
|
-
logWrite("warn", { msg: msg, data: data }
|
|
550
|
+
logWrite("warn", { msg: msg, data: data });
|
|
573
551
|
},
|
|
574
|
-
error(msg, err, data
|
|
575
|
-
const options = truncate === false ? { truncate: false } : undefined;
|
|
552
|
+
error(msg, err, data) {
|
|
576
553
|
if (isPlainObject(msg)) {
|
|
577
|
-
logWrite("error", msg
|
|
554
|
+
logWrite("error", msg);
|
|
578
555
|
return;
|
|
579
556
|
}
|
|
580
557
|
|
|
581
|
-
logWrite("error", { msg: msg, err: err, data: data }
|
|
558
|
+
logWrite("error", { msg: msg, err: err, data: data });
|
|
582
559
|
},
|
|
583
|
-
debug(msg, data
|
|
584
|
-
const options = truncate === false ? { truncate: false } : undefined;
|
|
560
|
+
debug(msg, data) {
|
|
585
561
|
if (isPlainObject(msg)) {
|
|
586
|
-
logWrite("debug", msg
|
|
562
|
+
logWrite("debug", msg);
|
|
587
563
|
return;
|
|
588
564
|
}
|
|
589
565
|
|
|
590
|
-
logWrite("debug", { msg: msg, data: data }
|
|
566
|
+
logWrite("debug", { msg: msg, data: data });
|
|
591
567
|
},
|
|
592
568
|
async flush() {
|
|
593
569
|
await flush();
|