befly 3.21.1 → 3.22.0
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/admin/list.js +1 -1
- package/apis/admin/upd.js +2 -2
- package/apis/dict/all.js +1 -1
- package/apis/dict/detail.js +1 -1
- package/apis/dict/list.js +1 -1
- package/apis/dict/upd.js +1 -1
- package/apis/dictType/upd.js +1 -1
- package/apis/role/all.js +1 -1
- package/apis/role/list.js +1 -1
- package/apis/role/upd.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 +201 -736
- package/lib/dbParse.js +1045 -0
- package/lib/dbUtil.js +83 -438
- package/lib/logger.js +21 -45
- package/lib/sqlBuilder.js +78 -294
- 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/lib/dbUtil.js
CHANGED
|
@@ -1,72 +1,6 @@
|
|
|
1
1
|
import { isFiniteNumber, isNonEmptyString, isNullable, isString } from "../utils/is.js";
|
|
2
2
|
import { snakeCase } from "../utils/util.js";
|
|
3
3
|
|
|
4
|
-
export const SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
5
|
-
|
|
6
|
-
export function assertNoUndefinedParam(value, label) {
|
|
7
|
-
if (value === undefined) {
|
|
8
|
-
throw new Error(`${label} 不能为 undefined`, {
|
|
9
|
-
cause: null,
|
|
10
|
-
code: "validation"
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function assertSafeIdentifierPart(part, kind) {
|
|
16
|
-
if (!SAFE_IDENTIFIER_RE.test(part)) {
|
|
17
|
-
throw new Error(`无效的 ${kind} 标识符: ${part}`, {
|
|
18
|
-
cause: null,
|
|
19
|
-
code: "validation"
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function validateQuoteIdentifierInput(identifier) {
|
|
25
|
-
if (!isString(identifier)) {
|
|
26
|
-
throw new Error(`quoteIdent 需要字符串类型标识符 (identifier: ${String(identifier)})`, {
|
|
27
|
-
cause: null,
|
|
28
|
-
code: "validation"
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const trimmed = identifier.trim();
|
|
33
|
-
if (!trimmed) {
|
|
34
|
-
throw new Error("SQL 标识符不能为空", {
|
|
35
|
-
cause: null,
|
|
36
|
-
code: "validation"
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return trimmed;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function validateLimitValue(count) {
|
|
44
|
-
if (typeof count !== "number" || count < 0) {
|
|
45
|
-
throw new Error(`LIMIT 数量必须是非负数 (count: ${String(count)})`, {
|
|
46
|
-
cause: null,
|
|
47
|
-
code: "validation"
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function validateOffsetValue(offset, label = "OFFSET") {
|
|
53
|
-
if (typeof offset !== "number" || offset < 0) {
|
|
54
|
-
throw new Error(`${label} 必须是非负数 (offset: ${String(offset)})`, {
|
|
55
|
-
cause: null,
|
|
56
|
-
code: "validation"
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function validateExcludeFieldsResult(resultFields, table, excludeSnakeFields) {
|
|
62
|
-
if (resultFields.length === 0) {
|
|
63
|
-
throw new Error(`排除字段后没有剩余字段可查询。表: ${table}, 排除: ${excludeSnakeFields.join(", ")}`, {
|
|
64
|
-
cause: null,
|
|
65
|
-
code: "validation"
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
4
|
export function assertNoExprField(field) {
|
|
71
5
|
if (!isString(field)) {
|
|
72
6
|
return;
|
|
@@ -84,93 +18,6 @@ export function assertNoExprField(field) {
|
|
|
84
18
|
}
|
|
85
19
|
}
|
|
86
20
|
|
|
87
|
-
export function assertNoUndefinedInRecord(row, label) {
|
|
88
|
-
for (const [key, value] of Object.entries(row)) {
|
|
89
|
-
if (value === undefined) {
|
|
90
|
-
throw new Error(`${label} 存在 undefined 字段值 (field: ${key})`, {
|
|
91
|
-
cause: null,
|
|
92
|
-
code: "validation"
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function assertBatchInsertRowsConsistent(rows, options) {
|
|
99
|
-
if (!Array.isArray(rows)) {
|
|
100
|
-
throw new Error("批量插入 rows 必须是数组", {
|
|
101
|
-
cause: null,
|
|
102
|
-
code: "validation"
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
if (rows.length === 0) {
|
|
106
|
-
throw new Error(`插入数据不能为空 (table: ${options.table})`, {
|
|
107
|
-
cause: null,
|
|
108
|
-
code: "validation"
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const first = rows[0];
|
|
113
|
-
if (!first || typeof first !== "object" || Array.isArray(first)) {
|
|
114
|
-
throw new Error(`批量插入的每一行必须是对象 (table: ${options.table}, rowIndex: 0)`, {
|
|
115
|
-
cause: null,
|
|
116
|
-
code: "validation"
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const fields = Object.keys(first);
|
|
121
|
-
if (fields.length === 0) {
|
|
122
|
-
throw new Error(`插入数据必须至少有一个字段 (table: ${options.table})`, {
|
|
123
|
-
cause: null,
|
|
124
|
-
code: "validation"
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const fieldSet = new Set(fields);
|
|
129
|
-
for (let i = 0; i < rows.length; i++) {
|
|
130
|
-
const row = rows[i];
|
|
131
|
-
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
132
|
-
throw new Error(`批量插入的每一行必须是对象 (table: ${options.table}, rowIndex: ${i})`, {
|
|
133
|
-
cause: null,
|
|
134
|
-
code: "validation"
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const rowKeys = Object.keys(row);
|
|
139
|
-
if (rowKeys.length !== fields.length) {
|
|
140
|
-
throw new Error(`批量插入每行字段必须一致 (table: ${options.table}, rowIndex: ${i})`, {
|
|
141
|
-
cause: null,
|
|
142
|
-
code: "validation"
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
for (const key of rowKeys) {
|
|
147
|
-
if (!fieldSet.has(key)) {
|
|
148
|
-
throw new Error(`批量插入每行字段必须一致 (table: ${options.table}, rowIndex: ${i}, extraField: ${key})`, {
|
|
149
|
-
cause: null,
|
|
150
|
-
code: "validation"
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
for (const field of fields) {
|
|
156
|
-
if (!(field in row)) {
|
|
157
|
-
throw new Error(`批量插入缺少字段 (table: ${options.table}, rowIndex: ${i}, field: ${field})`, {
|
|
158
|
-
cause: null,
|
|
159
|
-
code: "validation"
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
if (row[field] === undefined) {
|
|
163
|
-
throw new Error(`批量插入字段值不能为 undefined (table: ${options.table}, rowIndex: ${i}, field: ${field})`, {
|
|
164
|
-
cause: null,
|
|
165
|
-
code: "validation"
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return fields;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
21
|
export function parseTableRef(tableRef) {
|
|
175
22
|
if (!isString(tableRef)) {
|
|
176
23
|
throw new Error(`tableRef 必须是字符串 (tableRef: ${String(tableRef)})`, {
|
|
@@ -180,7 +27,7 @@ export function parseTableRef(tableRef) {
|
|
|
180
27
|
}
|
|
181
28
|
|
|
182
29
|
const trimmed = tableRef.trim();
|
|
183
|
-
if (!
|
|
30
|
+
if (!trimmed) {
|
|
184
31
|
throw new Error("tableRef 不能为空", {
|
|
185
32
|
cause: null,
|
|
186
33
|
code: "validation"
|
|
@@ -188,12 +35,6 @@ export function parseTableRef(tableRef) {
|
|
|
188
35
|
}
|
|
189
36
|
|
|
190
37
|
const parts = trimmed.split(/\s+/).filter((part) => part.length > 0);
|
|
191
|
-
if (parts.length === 0) {
|
|
192
|
-
throw new Error("tableRef 不能为空", {
|
|
193
|
-
cause: null,
|
|
194
|
-
code: "validation"
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
38
|
if (parts.length > 2) {
|
|
198
39
|
throw new Error(`不支持的表引用格式(包含过多片段)。请使用最简形式:table 或 table alias 或 schema.table 或 schema.table alias (tableRef: ${trimmed})`, {
|
|
199
40
|
cause: null,
|
|
@@ -258,76 +99,6 @@ export function parseTableRef(tableRef) {
|
|
|
258
99
|
return { schema: null, table: table, alias: aliasPart };
|
|
259
100
|
}
|
|
260
101
|
|
|
261
|
-
export function validateAndClassifyFields(fields) {
|
|
262
|
-
if (!fields || fields.length === 0) {
|
|
263
|
-
return { type: "all", fields: [] };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (fields.some((field) => field === "*")) {
|
|
267
|
-
throw new Error("fields 不支持 * 星号,请使用空数组 [] 或不传参数表示查询所有字段", {
|
|
268
|
-
cause: null,
|
|
269
|
-
code: "validation"
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (fields.some((field) => !isNonEmptyString(field))) {
|
|
274
|
-
throw new Error("fields 不能包含空字符串或无效值", {
|
|
275
|
-
cause: null,
|
|
276
|
-
code: "validation"
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
for (const rawField of fields) {
|
|
281
|
-
const checkField = rawField.startsWith("!") ? rawField.substring(1) : rawField;
|
|
282
|
-
assertNoExprField(checkField);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const includeFields = fields.filter((field) => !field.startsWith("!"));
|
|
286
|
-
const excludeFields = fields.filter((field) => field.startsWith("!"));
|
|
287
|
-
|
|
288
|
-
if (includeFields.length > 0 && excludeFields.length === 0) {
|
|
289
|
-
return { type: "include", fields: includeFields };
|
|
290
|
-
}
|
|
291
|
-
if (excludeFields.length > 0 && includeFields.length === 0) {
|
|
292
|
-
return { type: "exclude", fields: excludeFields.map((field) => field.substring(1)) };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
throw new Error('fields 不能同时包含普通字段和排除字段(! 开头)。只能使用以下3种方式之一:\n1. 空数组 [] 或不传(查询所有)\n2. 全部指定字段 ["id", "name"]\n3. 全部排除字段 ["!password", "!token"]', {
|
|
296
|
-
cause: null,
|
|
297
|
-
code: "validation"
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
export function validateExplicitReadFields(fields) {
|
|
302
|
-
const classified = validateAndClassifyFields(fields);
|
|
303
|
-
if (classified.type === "all") {
|
|
304
|
-
throw new Error("查询必须显式传 fields,不支持空 fields 或查询全部字段", {
|
|
305
|
-
cause: null,
|
|
306
|
-
code: "validation"
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return classified;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
export function validateExplicitLeftJoinFields(fields) {
|
|
314
|
-
const classified = validateAndClassifyFields(fields);
|
|
315
|
-
if (classified.type === "all") {
|
|
316
|
-
throw new Error("leftJoin 查询必须显式传 fields,不支持空 fields 或查询全部字段", {
|
|
317
|
-
cause: null,
|
|
318
|
-
code: "validation"
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
if (classified.type === "exclude") {
|
|
322
|
-
throw new Error("leftJoin 查询不支持排除字段,必须显式指定主表/从表字段", {
|
|
323
|
-
cause: null,
|
|
324
|
-
code: "validation"
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
return classified;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
102
|
function isQuotedIdentPaired(value) {
|
|
332
103
|
const trimmed = value.trim();
|
|
333
104
|
if (trimmed.length < 2) {
|
|
@@ -344,52 +115,53 @@ function startsWithIdentifierQuote(value) {
|
|
|
344
115
|
return trimmed.startsWith("`") || trimmed.startsWith('"');
|
|
345
116
|
}
|
|
346
117
|
|
|
347
|
-
function
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
cause: null,
|
|
351
|
-
code: "validation"
|
|
352
|
-
});
|
|
118
|
+
export function resolveQuoteIdent(options) {
|
|
119
|
+
if (options && options.quoteIdent) {
|
|
120
|
+
return options.quoteIdent;
|
|
353
121
|
}
|
|
122
|
+
|
|
123
|
+
return (identifier) => {
|
|
124
|
+
const trimmed = isString(identifier) ? identifier.trim() : String(identifier).trim();
|
|
125
|
+
const escaped = trimmed.replace(/`/g, "``");
|
|
126
|
+
return `\`${escaped}\``;
|
|
127
|
+
};
|
|
354
128
|
}
|
|
355
129
|
|
|
356
|
-
function
|
|
357
|
-
if (
|
|
358
|
-
return
|
|
130
|
+
export function parseFieldAliasParts(field) {
|
|
131
|
+
if (!isString(field)) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const trimmed = field.trim();
|
|
136
|
+
if (!trimmed) {
|
|
137
|
+
return null;
|
|
359
138
|
}
|
|
360
139
|
|
|
361
|
-
if (
|
|
362
|
-
throw new Error(
|
|
140
|
+
if (/\s+AS\s+/i.test(trimmed)) {
|
|
141
|
+
throw new Error(`字段别名不支持 AS,请使用单个空格分隔字段与别名 (field: ${trimmed})`, {
|
|
363
142
|
cause: null,
|
|
364
143
|
code: "validation"
|
|
365
144
|
});
|
|
366
145
|
}
|
|
367
146
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
export function resolveQuoteIdent(options) {
|
|
373
|
-
if (options && options.quoteIdent) {
|
|
374
|
-
return options.quoteIdent;
|
|
147
|
+
if (!/\s/.test(trimmed)) {
|
|
148
|
+
return null;
|
|
375
149
|
}
|
|
376
150
|
|
|
377
|
-
|
|
378
|
-
|
|
151
|
+
if (!/^[^\s]+ [^\s]+$/.test(trimmed)) {
|
|
152
|
+
throw new Error(`字段别名必须使用单个空格分隔字段与别名 (field: ${trimmed})`, {
|
|
153
|
+
cause: null,
|
|
154
|
+
code: "validation"
|
|
155
|
+
});
|
|
156
|
+
}
|
|
379
157
|
|
|
380
|
-
|
|
381
|
-
|
|
158
|
+
const parts = trimmed.split(" ");
|
|
159
|
+
return {
|
|
160
|
+
fieldPart: parts[0],
|
|
161
|
+
aliasPart: parts[1]
|
|
382
162
|
};
|
|
383
163
|
}
|
|
384
164
|
|
|
385
|
-
export function isQuotedIdent(value) {
|
|
386
|
-
return isQuotedIdentPaired(value);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export function startsWithQuote(value) {
|
|
390
|
-
return startsWithIdentifierQuote(value);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
165
|
export function escapeField(field, quoteIdent) {
|
|
394
166
|
if (!isString(field)) {
|
|
395
167
|
return field;
|
|
@@ -397,43 +169,25 @@ export function escapeField(field, quoteIdent) {
|
|
|
397
169
|
|
|
398
170
|
const trimmed = field.trim();
|
|
399
171
|
|
|
400
|
-
|
|
172
|
+
if (!trimmed) {
|
|
173
|
+
return trimmed;
|
|
174
|
+
}
|
|
401
175
|
|
|
402
|
-
if (trimmed === "*" ||
|
|
176
|
+
if (trimmed === "*" || isQuotedIdentPaired(trimmed)) {
|
|
403
177
|
return trimmed;
|
|
404
178
|
}
|
|
405
179
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
} catch {
|
|
409
|
-
throw new Error(`字段包含函数/表达式,请使用 selectRaw/whereRaw (field: ${trimmed})`, {
|
|
410
|
-
cause: null,
|
|
411
|
-
code: "validation"
|
|
412
|
-
});
|
|
180
|
+
if (startsWithIdentifierQuote(trimmed) && !isQuotedIdentPaired(trimmed) && !trimmed.includes(".")) {
|
|
181
|
+
return trimmed;
|
|
413
182
|
}
|
|
414
183
|
|
|
415
|
-
if (trimmed.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
throw new Error(`字段格式非法,请使用简单字段名或安全引用,复杂表达式请使用 selectRaw/whereRaw (field: ${trimmed})`, {
|
|
419
|
-
cause: null,
|
|
420
|
-
code: "validation"
|
|
421
|
-
});
|
|
422
|
-
}
|
|
184
|
+
if (trimmed.includes("(") || trimmed.includes(")") || /\s+AS\s+/i.test(trimmed)) {
|
|
185
|
+
return trimmed;
|
|
186
|
+
}
|
|
423
187
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const cleanAliasPart = aliasPart.trim();
|
|
428
|
-
if (!isQuotedIdent(cleanAliasPart)) {
|
|
429
|
-
if (!SAFE_IDENTIFIER_RE.test(cleanAliasPart)) {
|
|
430
|
-
throw new Error(`无效的字段别名: ${cleanAliasPart}`, {
|
|
431
|
-
cause: null,
|
|
432
|
-
code: "validation"
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
return `${escapeField(cleanFieldPart, quoteIdent)} AS ${cleanAliasPart}`;
|
|
188
|
+
const aliasMatch = trimmed.match(/^([^\s]+)\s+([^\s]+)$/);
|
|
189
|
+
if (aliasMatch) {
|
|
190
|
+
return `${escapeField(aliasMatch[1].trim(), quoteIdent)} ${aliasMatch[2].trim()}`;
|
|
437
191
|
}
|
|
438
192
|
|
|
439
193
|
if (trimmed.includes(".")) {
|
|
@@ -441,10 +195,12 @@ export function escapeField(field, quoteIdent) {
|
|
|
441
195
|
return parts
|
|
442
196
|
.map((part) => {
|
|
443
197
|
const cleanPart = part.trim();
|
|
444
|
-
if (cleanPart
|
|
198
|
+
if (!cleanPart) {
|
|
199
|
+
return cleanPart;
|
|
200
|
+
}
|
|
201
|
+
if (cleanPart === "*" || isQuotedIdentPaired(cleanPart) || startsWithIdentifierQuote(cleanPart)) {
|
|
445
202
|
return cleanPart;
|
|
446
203
|
}
|
|
447
|
-
assertPairedQuotedIdent(cleanPart, "字段标识符");
|
|
448
204
|
return quoteIdent(cleanPart);
|
|
449
205
|
})
|
|
450
206
|
.join(".");
|
|
@@ -460,129 +216,51 @@ export function escapeTable(table, quoteIdent) {
|
|
|
460
216
|
|
|
461
217
|
const trimmed = table.trim();
|
|
462
218
|
|
|
463
|
-
if (
|
|
219
|
+
if (!trimmed) {
|
|
464
220
|
return trimmed;
|
|
465
221
|
}
|
|
466
222
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
throw new Error("FROM 表名不能为空", {
|
|
470
|
-
cause: null,
|
|
471
|
-
code: "validation"
|
|
472
|
-
});
|
|
223
|
+
if (isQuotedIdentPaired(trimmed)) {
|
|
224
|
+
return trimmed;
|
|
473
225
|
}
|
|
474
226
|
|
|
227
|
+
if (startsWithIdentifierQuote(trimmed) && !isQuotedIdentPaired(trimmed) && !trimmed.includes(".")) {
|
|
228
|
+
return trimmed;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const parts = trimmed.split(/\s+/).filter((p) => p.length > 0);
|
|
475
232
|
if (parts.length > 2) {
|
|
476
|
-
|
|
477
|
-
cause: null,
|
|
478
|
-
code: "validation"
|
|
479
|
-
});
|
|
233
|
+
return trimmed;
|
|
480
234
|
}
|
|
481
235
|
|
|
482
236
|
const namePart = parts[0].trim();
|
|
483
237
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const nameSegments = namePart.split(".");
|
|
487
|
-
if (nameSegments.length > 2) {
|
|
488
|
-
throw new Error(`不支持的表引用格式(schema 层级过深)。请使用 fromRaw (table: ${trimmed})`, {
|
|
489
|
-
cause: null,
|
|
490
|
-
code: "validation"
|
|
491
|
-
});
|
|
238
|
+
if (namePart.split(".").some((part) => startsWithIdentifierQuote(part.trim()) && !isQuotedIdentPaired(part.trim()))) {
|
|
239
|
+
return trimmed;
|
|
492
240
|
}
|
|
493
241
|
|
|
494
|
-
|
|
495
|
-
if (nameSegments.length === 2) {
|
|
496
|
-
const schemaRaw = nameSegments[0];
|
|
497
|
-
const tableNameRaw = nameSegments[1];
|
|
498
|
-
const schema = schemaRaw.trim();
|
|
499
|
-
const tableName = tableNameRaw.trim();
|
|
500
|
-
|
|
501
|
-
const escapedSchema = escapeIdentifierPart(schema, "schema", quoteIdent, (part) => `schema 标识符引用不完整,请使用成对的 \`...\` 或 "..." (schema: ${part})`);
|
|
502
|
-
const escapedTableName = escapeIdentifierPart(tableName, "table", quoteIdent, (part) => `table 标识符引用不完整,请使用成对的 \`...\` 或 "..." (table: ${part})`);
|
|
503
|
-
|
|
504
|
-
escapedName = `${escapedSchema}.${escapedTableName}`;
|
|
505
|
-
} else {
|
|
506
|
-
const tableNameRaw = nameSegments[0];
|
|
507
|
-
const tableName = tableNameRaw.trim();
|
|
242
|
+
const aliasPart = parts.length === 2 ? parts[1].trim() : null;
|
|
508
243
|
|
|
509
|
-
|
|
510
|
-
|
|
244
|
+
const nameSegments = namePart.split(".");
|
|
245
|
+
const escapedName = nameSegments
|
|
246
|
+
.map((part) => {
|
|
247
|
+
const cleanPart = part.trim();
|
|
248
|
+
if (!cleanPart) {
|
|
249
|
+
return cleanPart;
|
|
250
|
+
}
|
|
251
|
+
if (isQuotedIdentPaired(cleanPart) || startsWithIdentifierQuote(cleanPart)) {
|
|
252
|
+
return cleanPart;
|
|
253
|
+
}
|
|
254
|
+
return quoteIdent(cleanPart);
|
|
255
|
+
})
|
|
256
|
+
.join(".");
|
|
511
257
|
|
|
512
258
|
if (aliasPart) {
|
|
513
|
-
assertSafeIdentifierPart(aliasPart, "alias");
|
|
514
259
|
return `${escapedName} ${aliasPart}`;
|
|
515
260
|
}
|
|
516
261
|
|
|
517
262
|
return escapedName;
|
|
518
263
|
}
|
|
519
|
-
|
|
520
|
-
export function validateParam(value) {
|
|
521
|
-
assertNoUndefinedParam(value, "SQL 参数值");
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
export function normalizeLimitValue(count, offset) {
|
|
525
|
-
validateLimitValue(count);
|
|
526
|
-
const limitValue = Math.floor(count);
|
|
527
|
-
|
|
528
|
-
let offsetValue = null;
|
|
529
|
-
if (!isNullable(offset)) {
|
|
530
|
-
validateOffsetValue(offset);
|
|
531
|
-
offsetValue = Math.floor(offset);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
return { limitValue: limitValue, offsetValue: offsetValue };
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
export function normalizeOffsetValue(count) {
|
|
538
|
-
validateOffsetValue(count, "OFFSET");
|
|
539
|
-
return Math.floor(count);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
export function toSqlParams(params) {
|
|
543
|
-
if (isNullable(params)) {
|
|
544
|
-
return [];
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
if (!Array.isArray(params)) {
|
|
548
|
-
throw new Error(`SQL 参数必须是数组,当前类型: ${typeof params}`, {
|
|
549
|
-
cause: null,
|
|
550
|
-
code: "validation"
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const out = [];
|
|
555
|
-
for (const value of params) {
|
|
556
|
-
if (value === undefined) {
|
|
557
|
-
throw new Error("SQL 参数不能为 undefined", {
|
|
558
|
-
cause: null,
|
|
559
|
-
code: "validation"
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (typeof value === "bigint") {
|
|
564
|
-
out.push(value.toString());
|
|
565
|
-
continue;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
out.push(value);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
return out;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
function normalizeTableRef(tableRef) {
|
|
575
|
-
const parsed = parseTableRef(tableRef);
|
|
576
|
-
const schemaPart = parsed.schema ? snakeCase(parsed.schema) : null;
|
|
577
|
-
const tablePart = snakeCase(parsed.table);
|
|
578
|
-
const aliasPart = parsed.alias ? snakeCase(parsed.alias) : null;
|
|
579
|
-
let result = schemaPart ? `${schemaPart}.${tablePart}` : tablePart;
|
|
580
|
-
if (aliasPart) {
|
|
581
|
-
result = `${result} ${aliasPart}`;
|
|
582
|
-
}
|
|
583
|
-
return result;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
264
|
function shouldExcludeFieldValue(field, value, excludeValueMap) {
|
|
587
265
|
if (!excludeValueMap || excludeValueMap.size === 0) {
|
|
588
266
|
return false;
|
|
@@ -724,13 +402,18 @@ export async function fieldsToSnake(table, classified, getTableColumns) {
|
|
|
724
402
|
const allColumns = await getTableColumns(table);
|
|
725
403
|
const excludeSnakeFields = classified.fields.map((field) => snakeCase(field));
|
|
726
404
|
const resultFields = allColumns.filter((column) => !excludeSnakeFields.includes(column));
|
|
727
|
-
|
|
405
|
+
if (resultFields.length === 0) {
|
|
406
|
+
throw new Error(`排除字段后没有剩余字段可查询。表: ${table}, 排除: ${excludeSnakeFields.join(", ")}`, {
|
|
407
|
+
cause: null,
|
|
408
|
+
code: "validation"
|
|
409
|
+
});
|
|
410
|
+
}
|
|
728
411
|
return resultFields;
|
|
729
412
|
}
|
|
730
413
|
return ["*"];
|
|
731
414
|
}
|
|
732
415
|
|
|
733
|
-
function normalizeQualifierField(field) {
|
|
416
|
+
export function normalizeQualifierField(field) {
|
|
734
417
|
const parts = field.split(".");
|
|
735
418
|
if (parts.length === 1) {
|
|
736
419
|
return snakeCase(field);
|
|
@@ -805,14 +488,9 @@ export function processJoinField(field) {
|
|
|
805
488
|
return field;
|
|
806
489
|
}
|
|
807
490
|
|
|
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()}`;
|
|
491
|
+
const aliasParts = parseFieldAliasParts(field);
|
|
492
|
+
if (aliasParts) {
|
|
493
|
+
return `${processJoinField(aliasParts.fieldPart.trim())} ${aliasParts.aliasPart.trim()}`;
|
|
816
494
|
}
|
|
817
495
|
|
|
818
496
|
return normalizeQualifierField(field);
|
|
@@ -896,39 +574,6 @@ export function processJoinOn(on) {
|
|
|
896
574
|
return result;
|
|
897
575
|
}
|
|
898
576
|
|
|
899
|
-
export function parseLeftJoinItem(joinTable, joinItem) {
|
|
900
|
-
const parts = joinItem.split(" ");
|
|
901
|
-
return {
|
|
902
|
-
type: "left",
|
|
903
|
-
table: normalizeTableRef(joinTable),
|
|
904
|
-
on: processJoinOn(`${parts[0]} = ${parts[1]}`)
|
|
905
|
-
};
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
export function addDefaultStateFilter(where = {}, table, hasLeftJoin = false, beflyMode = "auto") {
|
|
909
|
-
if (beflyMode === "manual") {
|
|
910
|
-
return where;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
const hasStateCondition = Object.keys(where).some((key) => key.startsWith("state") || key.includes(".state"));
|
|
914
|
-
if (hasStateCondition) {
|
|
915
|
-
return where;
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
const result = {};
|
|
919
|
-
for (const [key, value] of Object.entries(where)) {
|
|
920
|
-
result[key] = value;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
if (hasLeftJoin && table) {
|
|
924
|
-
result[`${table}.state$gt`] = 0;
|
|
925
|
-
return result;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
result.state$gt = 0;
|
|
929
|
-
return result;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
577
|
export function whereKeysToSnake(where) {
|
|
933
578
|
return mapWhereKeys(where, (key) => {
|
|
934
579
|
assertNoExprField(key);
|