befly 3.21.2 → 3.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apis/admin/list.js +1 -1
- package/apis/admin/upd.js +2 -2
- 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/apis/source/imageList.js +27 -0
- package/apis/upload/file.js +105 -0
- package/checks/config.js +2 -0
- package/configs/beflyConfig.json +2 -1
- package/configs/beflyMenus.json +12 -0
- package/index.js +5 -5
- package/lib/dbHelper.js +170 -751
- package/lib/dbParse.js +1045 -0
- package/lib/dbUtil.js +52 -508
- package/lib/sqlBuilder.js +78 -294
- package/package.json +2 -2
- package/paths.js +2 -2
- package/router/static.js +2 -1
- package/sql/befly.sql +20 -0
- package/tables/file.json +81 -0
- package/utils/datetime.js +29 -32
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,156 +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
|
-
return validateAndClassifyFields(fields);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function getTableQualifier(tableRef) {
|
|
306
|
-
const parsed = parseTableRef(tableRef);
|
|
307
|
-
if (parsed.alias) {
|
|
308
|
-
return snakeCase(parsed.alias);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return snakeCase(parsed.table);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
export function validateExplicitLeftJoinFields(fields, mainTableRef) {
|
|
315
|
-
if (!fields || fields.length === 0) {
|
|
316
|
-
throw new Error("leftJoin 查询必须显式传 fields,不支持空 fields 或查询全部字段", {
|
|
317
|
-
cause: null,
|
|
318
|
-
code: "validation"
|
|
319
|
-
});
|
|
320
|
-
}
|
|
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 使用主表排除字段时,必须同时传主表限定符.*", {
|
|
397
|
-
cause: null,
|
|
398
|
-
code: "validation"
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
return {
|
|
403
|
-
type: "join",
|
|
404
|
-
includeFields: includeFields,
|
|
405
|
-
mainAllIncluded: mainAllIncluded,
|
|
406
|
-
mainExcludeFields: mainExcludeFields,
|
|
407
|
-
mainQualifier: mainQualifier
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
|
|
411
102
|
function isQuotedIdentPaired(value) {
|
|
412
103
|
const trimmed = value.trim();
|
|
413
104
|
if (trimmed.length < 2) {
|
|
@@ -424,53 +115,19 @@ function startsWithIdentifierQuote(value) {
|
|
|
424
115
|
return trimmed.startsWith("`") || trimmed.startsWith('"');
|
|
425
116
|
}
|
|
426
117
|
|
|
427
|
-
function assertPairedQuotedIdent(value, label) {
|
|
428
|
-
if (startsWithIdentifierQuote(value) && !isQuotedIdentPaired(value)) {
|
|
429
|
-
throw new Error(`${label} 引用不完整,请使用成对的 \`...\` 或 "..." (value: ${value})`, {
|
|
430
|
-
cause: null,
|
|
431
|
-
code: "validation"
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function escapeIdentifierPart(part, kind, quoteIdent, unpairedErrorFactory) {
|
|
437
|
-
if (isQuotedIdent(part)) {
|
|
438
|
-
return part;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (startsWithQuote(part)) {
|
|
442
|
-
throw new Error(unpairedErrorFactory(part), {
|
|
443
|
-
cause: null,
|
|
444
|
-
code: "validation"
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
assertSafeIdentifierPart(part, kind);
|
|
449
|
-
return quoteIdent(part);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
118
|
export function resolveQuoteIdent(options) {
|
|
453
119
|
if (options && options.quoteIdent) {
|
|
454
120
|
return options.quoteIdent;
|
|
455
121
|
}
|
|
456
122
|
|
|
457
123
|
return (identifier) => {
|
|
458
|
-
const trimmed =
|
|
459
|
-
|
|
124
|
+
const trimmed = isString(identifier) ? identifier.trim() : String(identifier).trim();
|
|
460
125
|
const escaped = trimmed.replace(/`/g, "``");
|
|
461
126
|
return `\`${escaped}\``;
|
|
462
127
|
};
|
|
463
128
|
}
|
|
464
129
|
|
|
465
|
-
export function
|
|
466
|
-
return isQuotedIdentPaired(value);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
export function startsWithQuote(value) {
|
|
470
|
-
return startsWithIdentifierQuote(value);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function parseFieldAliasParts(field) {
|
|
130
|
+
export function parseFieldAliasParts(field) {
|
|
474
131
|
if (!isString(field)) {
|
|
475
132
|
return null;
|
|
476
133
|
}
|
|
@@ -512,34 +169,25 @@ export function escapeField(field, quoteIdent) {
|
|
|
512
169
|
|
|
513
170
|
const trimmed = field.trim();
|
|
514
171
|
|
|
515
|
-
|
|
172
|
+
if (!trimmed) {
|
|
173
|
+
return trimmed;
|
|
174
|
+
}
|
|
516
175
|
|
|
517
|
-
if (trimmed === "*" ||
|
|
176
|
+
if (trimmed === "*" || isQuotedIdentPaired(trimmed)) {
|
|
518
177
|
return trimmed;
|
|
519
178
|
}
|
|
520
179
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
} catch {
|
|
524
|
-
throw new Error(`字段包含函数/表达式,请使用 selectRaw/whereRaw (field: ${trimmed})`, {
|
|
525
|
-
cause: null,
|
|
526
|
-
code: "validation"
|
|
527
|
-
});
|
|
180
|
+
if (startsWithIdentifierQuote(trimmed) && !isQuotedIdentPaired(trimmed) && !trimmed.includes(".")) {
|
|
181
|
+
return trimmed;
|
|
528
182
|
}
|
|
529
183
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
cause: null,
|
|
538
|
-
code: "validation"
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
return `${escapeField(cleanFieldPart, quoteIdent)} ${cleanAliasPart}`;
|
|
184
|
+
if (trimmed.includes("(") || trimmed.includes(")") || /\s+AS\s+/i.test(trimmed)) {
|
|
185
|
+
return trimmed;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const aliasMatch = trimmed.match(/^([^\s]+)\s+([^\s]+)$/);
|
|
189
|
+
if (aliasMatch) {
|
|
190
|
+
return `${escapeField(aliasMatch[1].trim(), quoteIdent)} ${aliasMatch[2].trim()}`;
|
|
543
191
|
}
|
|
544
192
|
|
|
545
193
|
if (trimmed.includes(".")) {
|
|
@@ -547,10 +195,12 @@ export function escapeField(field, quoteIdent) {
|
|
|
547
195
|
return parts
|
|
548
196
|
.map((part) => {
|
|
549
197
|
const cleanPart = part.trim();
|
|
550
|
-
if (cleanPart
|
|
198
|
+
if (!cleanPart) {
|
|
199
|
+
return cleanPart;
|
|
200
|
+
}
|
|
201
|
+
if (cleanPart === "*" || isQuotedIdentPaired(cleanPart) || startsWithIdentifierQuote(cleanPart)) {
|
|
551
202
|
return cleanPart;
|
|
552
203
|
}
|
|
553
|
-
assertPairedQuotedIdent(cleanPart, "字段标识符");
|
|
554
204
|
return quoteIdent(cleanPart);
|
|
555
205
|
})
|
|
556
206
|
.join(".");
|
|
@@ -566,129 +216,51 @@ export function escapeTable(table, quoteIdent) {
|
|
|
566
216
|
|
|
567
217
|
const trimmed = table.trim();
|
|
568
218
|
|
|
569
|
-
if (
|
|
219
|
+
if (!trimmed) {
|
|
570
220
|
return trimmed;
|
|
571
221
|
}
|
|
572
222
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
223
|
+
if (isQuotedIdentPaired(trimmed)) {
|
|
224
|
+
return trimmed;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (startsWithIdentifierQuote(trimmed) && !isQuotedIdentPaired(trimmed) && !trimmed.includes(".")) {
|
|
228
|
+
return trimmed;
|
|
579
229
|
}
|
|
580
230
|
|
|
231
|
+
const parts = trimmed.split(/\s+/).filter((p) => p.length > 0);
|
|
581
232
|
if (parts.length > 2) {
|
|
582
|
-
|
|
583
|
-
cause: null,
|
|
584
|
-
code: "validation"
|
|
585
|
-
});
|
|
233
|
+
return trimmed;
|
|
586
234
|
}
|
|
587
235
|
|
|
588
236
|
const namePart = parts[0].trim();
|
|
589
237
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const nameSegments = namePart.split(".");
|
|
593
|
-
if (nameSegments.length > 2) {
|
|
594
|
-
throw new Error(`不支持的表引用格式(schema 层级过深)。请使用 fromRaw (table: ${trimmed})`, {
|
|
595
|
-
cause: null,
|
|
596
|
-
code: "validation"
|
|
597
|
-
});
|
|
238
|
+
if (namePart.split(".").some((part) => startsWithIdentifierQuote(part.trim()) && !isQuotedIdentPaired(part.trim()))) {
|
|
239
|
+
return trimmed;
|
|
598
240
|
}
|
|
599
241
|
|
|
600
|
-
|
|
601
|
-
if (nameSegments.length === 2) {
|
|
602
|
-
const schemaRaw = nameSegments[0];
|
|
603
|
-
const tableNameRaw = nameSegments[1];
|
|
604
|
-
const schema = schemaRaw.trim();
|
|
605
|
-
const tableName = tableNameRaw.trim();
|
|
606
|
-
|
|
607
|
-
const escapedSchema = escapeIdentifierPart(schema, "schema", quoteIdent, (part) => `schema 标识符引用不完整,请使用成对的 \`...\` 或 "..." (schema: ${part})`);
|
|
608
|
-
const escapedTableName = escapeIdentifierPart(tableName, "table", quoteIdent, (part) => `table 标识符引用不完整,请使用成对的 \`...\` 或 "..." (table: ${part})`);
|
|
609
|
-
|
|
610
|
-
escapedName = `${escapedSchema}.${escapedTableName}`;
|
|
611
|
-
} else {
|
|
612
|
-
const tableNameRaw = nameSegments[0];
|
|
613
|
-
const tableName = tableNameRaw.trim();
|
|
242
|
+
const aliasPart = parts.length === 2 ? parts[1].trim() : null;
|
|
614
243
|
|
|
615
|
-
|
|
616
|
-
|
|
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(".");
|
|
617
257
|
|
|
618
258
|
if (aliasPart) {
|
|
619
|
-
assertSafeIdentifierPart(aliasPart, "alias");
|
|
620
259
|
return `${escapedName} ${aliasPart}`;
|
|
621
260
|
}
|
|
622
261
|
|
|
623
262
|
return escapedName;
|
|
624
263
|
}
|
|
625
|
-
|
|
626
|
-
export function validateParam(value) {
|
|
627
|
-
assertNoUndefinedParam(value, "SQL 参数值");
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
export function normalizeLimitValue(count, offset) {
|
|
631
|
-
validateLimitValue(count);
|
|
632
|
-
const limitValue = Math.floor(count);
|
|
633
|
-
|
|
634
|
-
let offsetValue = null;
|
|
635
|
-
if (!isNullable(offset)) {
|
|
636
|
-
validateOffsetValue(offset);
|
|
637
|
-
offsetValue = Math.floor(offset);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
return { limitValue: limitValue, offsetValue: offsetValue };
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
export function normalizeOffsetValue(count) {
|
|
644
|
-
validateOffsetValue(count, "OFFSET");
|
|
645
|
-
return Math.floor(count);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
export function toSqlParams(params) {
|
|
649
|
-
if (isNullable(params)) {
|
|
650
|
-
return [];
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
if (!Array.isArray(params)) {
|
|
654
|
-
throw new Error(`SQL 参数必须是数组,当前类型: ${typeof params}`, {
|
|
655
|
-
cause: null,
|
|
656
|
-
code: "validation"
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
const out = [];
|
|
661
|
-
for (const value of params) {
|
|
662
|
-
if (value === undefined) {
|
|
663
|
-
throw new Error("SQL 参数不能为 undefined", {
|
|
664
|
-
cause: null,
|
|
665
|
-
code: "validation"
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
if (typeof value === "bigint") {
|
|
670
|
-
out.push(value.toString());
|
|
671
|
-
continue;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
out.push(value);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
return out;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function normalizeTableRef(tableRef) {
|
|
681
|
-
const parsed = parseTableRef(tableRef);
|
|
682
|
-
const schemaPart = parsed.schema ? snakeCase(parsed.schema) : null;
|
|
683
|
-
const tablePart = snakeCase(parsed.table);
|
|
684
|
-
const aliasPart = parsed.alias ? snakeCase(parsed.alias) : null;
|
|
685
|
-
let result = schemaPart ? `${schemaPart}.${tablePart}` : tablePart;
|
|
686
|
-
if (aliasPart) {
|
|
687
|
-
result = `${result} ${aliasPart}`;
|
|
688
|
-
}
|
|
689
|
-
return result;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
264
|
function shouldExcludeFieldValue(field, value, excludeValueMap) {
|
|
693
265
|
if (!excludeValueMap || excludeValueMap.size === 0) {
|
|
694
266
|
return false;
|
|
@@ -830,13 +402,18 @@ export async function fieldsToSnake(table, classified, getTableColumns) {
|
|
|
830
402
|
const allColumns = await getTableColumns(table);
|
|
831
403
|
const excludeSnakeFields = classified.fields.map((field) => snakeCase(field));
|
|
832
404
|
const resultFields = allColumns.filter((column) => !excludeSnakeFields.includes(column));
|
|
833
|
-
|
|
405
|
+
if (resultFields.length === 0) {
|
|
406
|
+
throw new Error(`排除字段后没有剩余字段可查询。表: ${table}, 排除: ${excludeSnakeFields.join(", ")}`, {
|
|
407
|
+
cause: null,
|
|
408
|
+
code: "validation"
|
|
409
|
+
});
|
|
410
|
+
}
|
|
834
411
|
return resultFields;
|
|
835
412
|
}
|
|
836
413
|
return ["*"];
|
|
837
414
|
}
|
|
838
415
|
|
|
839
|
-
function normalizeQualifierField(field) {
|
|
416
|
+
export function normalizeQualifierField(field) {
|
|
840
417
|
const parts = field.split(".");
|
|
841
418
|
if (parts.length === 1) {
|
|
842
419
|
return snakeCase(field);
|
|
@@ -997,39 +574,6 @@ export function processJoinOn(on) {
|
|
|
997
574
|
return result;
|
|
998
575
|
}
|
|
999
576
|
|
|
1000
|
-
export function parseLeftJoinItem(joinTable, joinItem) {
|
|
1001
|
-
const parts = joinItem.split(" ");
|
|
1002
|
-
return {
|
|
1003
|
-
type: "left",
|
|
1004
|
-
table: normalizeTableRef(joinTable),
|
|
1005
|
-
on: processJoinOn(`${parts[0]} = ${parts[1]}`)
|
|
1006
|
-
};
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
export function addDefaultStateFilter(where = {}, table, hasLeftJoin = false, beflyMode = "auto") {
|
|
1010
|
-
if (beflyMode === "manual") {
|
|
1011
|
-
return where;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
const hasStateCondition = Object.keys(where).some((key) => key.startsWith("state") || key.includes(".state"));
|
|
1015
|
-
if (hasStateCondition) {
|
|
1016
|
-
return where;
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
const result = {};
|
|
1020
|
-
for (const [key, value] of Object.entries(where)) {
|
|
1021
|
-
result[key] = value;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
if (hasLeftJoin && table) {
|
|
1025
|
-
result[`${table}.state$gt`] = 0;
|
|
1026
|
-
return result;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
result.state$gt = 0;
|
|
1030
|
-
return result;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
577
|
export function whereKeysToSnake(where) {
|
|
1034
578
|
return mapWhereKeys(where, (key) => {
|
|
1035
579
|
assertNoExprField(key);
|