befly 3.20.8 → 3.20.9
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/dict/all.js +2 -2
- package/apis/dict/detail.js +2 -2
- package/apis/dict/list.js +2 -2
- package/hooks/permission.js +1 -2
- package/lib/cacheHelper.js +16 -17
- package/lib/dbHelper/builders.js +91 -207
- package/lib/dbHelper/dataOps.js +119 -123
- package/lib/dbHelper/validate.js +88 -20
- package/lib/sqlBuilder/batch.js +7 -8
- package/lib/sqlBuilder/check.js +19 -87
- package/lib/sqlBuilder/compiler.js +91 -90
- package/lib/sqlBuilder/parser.js +122 -103
- package/lib/sqlBuilder/util.js +66 -53
- package/package.json +2 -2
- package/lib/cacheKeys.js +0 -42
- package/lib/sqlBuilder/errors.js +0 -60
package/lib/sqlBuilder/parser.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { SqlErrors } from "./errors.js";
|
|
2
1
|
import { validateParam } from "./util.js";
|
|
3
2
|
import { isNonEmptyString, isString } from "../../utils/is.js";
|
|
4
3
|
|
|
@@ -19,7 +18,7 @@ export function appendSelectItems(list, fields) {
|
|
|
19
18
|
return;
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
throw new Error(
|
|
21
|
+
throw new Error("SELECT 字段必须是字符串或数组", {
|
|
23
22
|
cause: null,
|
|
24
23
|
code: "validation"
|
|
25
24
|
});
|
|
@@ -27,7 +26,7 @@ export function appendSelectItems(list, fields) {
|
|
|
27
26
|
|
|
28
27
|
export function appendSelectRaw(list, expr) {
|
|
29
28
|
if (!isNonEmptyString(expr)) {
|
|
30
|
-
throw new Error(
|
|
29
|
+
throw new Error(`selectRaw 需要非空字符串 (expr: ${String(expr)})`, {
|
|
31
30
|
cause: null,
|
|
32
31
|
code: "validation"
|
|
33
32
|
});
|
|
@@ -37,7 +36,7 @@ export function appendSelectRaw(list, expr) {
|
|
|
37
36
|
|
|
38
37
|
export function setFromValue(state, table, isRaw) {
|
|
39
38
|
if (!isNonEmptyString(table)) {
|
|
40
|
-
const err = isRaw ?
|
|
39
|
+
const err = isRaw ? `fromRaw 需要非空字符串 (tableExpr: ${String(table)})` : `FROM 表名必须是非空字符串 (table: ${String(table)})`;
|
|
41
40
|
throw new Error(err, {
|
|
42
41
|
cause: null,
|
|
43
42
|
code: "validation"
|
|
@@ -49,7 +48,7 @@ export function setFromValue(state, table, isRaw) {
|
|
|
49
48
|
|
|
50
49
|
export function appendJoinItem(list, joinType, table, on) {
|
|
51
50
|
if (!isString(table) || !isString(on)) {
|
|
52
|
-
throw new Error(
|
|
51
|
+
throw new Error(`JOIN 表名和条件必须是字符串 (table: ${String(table)}, on: ${String(on)})`, {
|
|
53
52
|
cause: null,
|
|
54
53
|
code: "validation"
|
|
55
54
|
});
|
|
@@ -60,7 +59,7 @@ export function appendJoinItem(list, joinType, table, on) {
|
|
|
60
59
|
|
|
61
60
|
export function appendOrderByItems(list, fields) {
|
|
62
61
|
if (!Array.isArray(fields)) {
|
|
63
|
-
throw new Error(
|
|
62
|
+
throw new Error('orderBy 必须是字符串数组,格式为 "字段#方向"', {
|
|
64
63
|
cause: null,
|
|
65
64
|
code: "validation"
|
|
66
65
|
});
|
|
@@ -68,7 +67,7 @@ export function appendOrderByItems(list, fields) {
|
|
|
68
67
|
|
|
69
68
|
for (const item of fields) {
|
|
70
69
|
if (!isString(item) || !item.includes("#")) {
|
|
71
|
-
throw new Error(
|
|
70
|
+
throw new Error(`orderBy 字段必须是 "字段#方向" 格式的字符串(例如:"name#ASC", "id#DESC") (item: ${String(item)})`, {
|
|
72
71
|
cause: null,
|
|
73
72
|
code: "validation"
|
|
74
73
|
});
|
|
@@ -76,7 +75,7 @@ export function appendOrderByItems(list, fields) {
|
|
|
76
75
|
|
|
77
76
|
const parts = item.split("#");
|
|
78
77
|
if (parts.length !== 2) {
|
|
79
|
-
throw new Error(
|
|
78
|
+
throw new Error(`orderBy 字段必须是 "字段#方向" 格式的字符串(例如:"name#ASC", "id#DESC") (item: ${String(item)})`, {
|
|
80
79
|
cause: null,
|
|
81
80
|
code: "validation"
|
|
82
81
|
});
|
|
@@ -88,14 +87,14 @@ export function appendOrderByItems(list, fields) {
|
|
|
88
87
|
const cleanDir = direction.trim().toUpperCase();
|
|
89
88
|
|
|
90
89
|
if (!cleanField) {
|
|
91
|
-
throw new Error(
|
|
90
|
+
throw new Error(`orderBy 中字段名不能为空 (item: ${item})`, {
|
|
92
91
|
cause: null,
|
|
93
92
|
code: "validation"
|
|
94
93
|
});
|
|
95
94
|
}
|
|
96
95
|
|
|
97
96
|
if (!["ASC", "DESC"].includes(cleanDir)) {
|
|
98
|
-
throw new Error(
|
|
97
|
+
throw new Error(`ORDER BY 方向必须是 ASC 或 DESC (direction: ${cleanDir})`, {
|
|
99
98
|
cause: null,
|
|
100
99
|
code: "validation"
|
|
101
100
|
});
|
|
@@ -107,30 +106,24 @@ export function appendOrderByItems(list, fields) {
|
|
|
107
106
|
|
|
108
107
|
export function appendWhereInput(root, conditionOrField, value) {
|
|
109
108
|
if (conditionOrField && typeof conditionOrField === "object" && !Array.isArray(conditionOrField)) {
|
|
110
|
-
|
|
111
|
-
if (node && node.items.length > 0) {
|
|
112
|
-
root.items.push(node);
|
|
113
|
-
}
|
|
109
|
+
appendWhereObject(root, conditionOrField);
|
|
114
110
|
return;
|
|
115
111
|
}
|
|
116
112
|
|
|
117
113
|
if (isString(conditionOrField)) {
|
|
118
114
|
if (value === undefined) {
|
|
119
|
-
throw new Error(
|
|
115
|
+
throw new Error("where(field, value) 不允许省略 value。若需传入原始 WHERE,请使用 whereRaw", {
|
|
120
116
|
cause: null,
|
|
121
117
|
code: "validation"
|
|
122
118
|
});
|
|
123
119
|
}
|
|
124
|
-
|
|
125
|
-
if (node) {
|
|
126
|
-
root.items.push(node);
|
|
127
|
-
}
|
|
120
|
+
appendWhereNode(root, buildOperatorNode(conditionOrField, "=", value));
|
|
128
121
|
}
|
|
129
122
|
}
|
|
130
123
|
|
|
131
124
|
export function appendWhereRaw(root, sql, params) {
|
|
132
125
|
if (!isNonEmptyString(sql)) {
|
|
133
|
-
throw new Error(
|
|
126
|
+
throw new Error(`whereRaw 需要非空字符串 (sql: ${String(sql)})`, {
|
|
134
127
|
cause: null,
|
|
135
128
|
code: "validation"
|
|
136
129
|
});
|
|
@@ -144,6 +137,19 @@ export function appendWhereRaw(root, sql, params) {
|
|
|
144
137
|
root.items.push({ type: "raw", sql: sql, params: paramList });
|
|
145
138
|
}
|
|
146
139
|
|
|
140
|
+
function appendWhereNode(root, node) {
|
|
141
|
+
if (node) {
|
|
142
|
+
root.items.push(node);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function appendWhereObject(root, whereObj) {
|
|
147
|
+
const node = parseWhereObject(whereObj);
|
|
148
|
+
if (node.items.length > 0) {
|
|
149
|
+
root.items.push(node);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
147
153
|
function parseWhereObject(whereObj) {
|
|
148
154
|
if (!whereObj || typeof whereObj !== "object") {
|
|
149
155
|
return { type: "group", join: "AND", items: [] };
|
|
@@ -157,117 +163,130 @@ function parseWhereObject(whereObj) {
|
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
if (key === "$and") {
|
|
160
|
-
|
|
161
|
-
for (const condition of value) {
|
|
162
|
-
if (condition && typeof condition === "object" && !Array.isArray(condition)) {
|
|
163
|
-
const sub = parseWhereObject(condition);
|
|
164
|
-
if (sub.items.length > 0) {
|
|
165
|
-
if (sub.join === "AND") {
|
|
166
|
-
for (const item of sub.items) {
|
|
167
|
-
group.items.push(item);
|
|
168
|
-
}
|
|
169
|
-
} else {
|
|
170
|
-
group.items.push(sub);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
166
|
+
appendAndConditions(group, value);
|
|
176
167
|
continue;
|
|
177
168
|
}
|
|
178
169
|
|
|
179
170
|
if (key === "$or") {
|
|
180
|
-
|
|
181
|
-
const orGroup = { type: "group", join: "OR", items: [] };
|
|
182
|
-
for (const condition of value) {
|
|
183
|
-
if (!condition || typeof condition !== "object" || Array.isArray(condition)) {
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
const sub = parseWhereObject(condition);
|
|
187
|
-
if (sub.items.length > 0) {
|
|
188
|
-
orGroup.items.push(sub);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
if (orGroup.items.length > 0) {
|
|
192
|
-
group.items.push(orGroup);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
171
|
+
appendOrConditions(group, value);
|
|
195
172
|
continue;
|
|
196
173
|
}
|
|
197
174
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
175
|
+
appendFieldCondition(group, key, value);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return group;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function appendAndConditions(group, value) {
|
|
182
|
+
if (!Array.isArray(value)) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const condition of value) {
|
|
187
|
+
if (!condition || typeof condition !== "object" || Array.isArray(condition)) {
|
|
206
188
|
continue;
|
|
207
189
|
}
|
|
208
190
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
191
|
+
const sub = parseWhereObject(condition);
|
|
192
|
+
if (sub.items.length === 0) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (sub.join === "AND") {
|
|
197
|
+
for (const item of sub.items) {
|
|
198
|
+
group.items.push(item);
|
|
215
199
|
}
|
|
216
200
|
continue;
|
|
217
201
|
}
|
|
218
202
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
203
|
+
group.items.push(sub);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function appendOrConditions(group, value) {
|
|
208
|
+
if (!Array.isArray(value)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const orGroup = { type: "group", join: "OR", items: [] };
|
|
213
|
+
for (const condition of value) {
|
|
214
|
+
if (!condition || typeof condition !== "object" || Array.isArray(condition)) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const sub = parseWhereObject(condition);
|
|
219
|
+
if (sub.items.length > 0) {
|
|
220
|
+
orGroup.items.push(sub);
|
|
222
221
|
}
|
|
223
222
|
}
|
|
224
223
|
|
|
225
|
-
|
|
224
|
+
if (orGroup.items.length > 0) {
|
|
225
|
+
group.items.push(orGroup);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function appendFieldCondition(group, key, value) {
|
|
230
|
+
if (key.includes("$")) {
|
|
231
|
+
const lastDollarIndex = key.lastIndexOf("$");
|
|
232
|
+
appendWhereNode(group, buildOperatorNode(key.substring(0, lastDollarIndex), `$${key.substring(lastDollarIndex + 1)}`, value));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
237
|
+
for (const [operator, operatorValue] of Object.entries(value)) {
|
|
238
|
+
appendWhereNode(group, buildOperatorNode(key, operator, operatorValue));
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
appendWhereNode(group, buildOperatorNode(key, "=", value));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function buildArrayOperatorNode(fieldName, operator, value, errorFactory, emptyMessage) {
|
|
247
|
+
if (!Array.isArray(value)) {
|
|
248
|
+
throw new Error(errorFactory(operator), {
|
|
249
|
+
cause: null,
|
|
250
|
+
code: "validation"
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (value.length === 0) {
|
|
254
|
+
throw new Error(emptyMessage, {
|
|
255
|
+
cause: null,
|
|
256
|
+
code: "validation"
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { type: "op", field: fieldName, operator: operator, value: value };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function buildRangeOrNullOperatorNode(fieldName, operator, value) {
|
|
264
|
+
if (operator === "$between" || operator === "$notBetween") {
|
|
265
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
266
|
+
return { type: "op", field: fieldName, operator: operator, value: value };
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (value === true) {
|
|
272
|
+
return { type: "op", field: fieldName, operator: operator, value: value };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return null;
|
|
226
276
|
}
|
|
227
277
|
|
|
228
278
|
function buildOperatorNode(fieldName, operator, value) {
|
|
229
279
|
switch (operator) {
|
|
230
280
|
case "$in":
|
|
231
|
-
|
|
232
|
-
throw new Error(SqlErrors.IN_NEED_ARRAY(operator), {
|
|
233
|
-
cause: null,
|
|
234
|
-
code: "validation"
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
if (value.length === 0) {
|
|
238
|
-
throw new Error(SqlErrors.IN_NEED_NON_EMPTY, {
|
|
239
|
-
cause: null,
|
|
240
|
-
code: "validation"
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
return { type: "op", field: fieldName, operator: operator, value: value };
|
|
281
|
+
return buildArrayOperatorNode(fieldName, operator, value, (currentOperator) => `$in 操作符的值必须是数组 (operator: ${currentOperator})`, "$in 操作符的数组不能为空。提示:空数组会导致查询永远不匹配任何记录,这通常不是预期行为。请检查查询条件或移除该字段。");
|
|
244
282
|
case "$nin":
|
|
245
283
|
case "$notIn":
|
|
246
|
-
|
|
247
|
-
throw new Error(SqlErrors.NIN_NEED_ARRAY(operator), {
|
|
248
|
-
cause: null,
|
|
249
|
-
code: "validation"
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
if (value.length === 0) {
|
|
253
|
-
throw new Error(SqlErrors.NIN_NEED_NON_EMPTY, {
|
|
254
|
-
cause: null,
|
|
255
|
-
code: "validation"
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
return { type: "op", field: fieldName, operator: operator, value: value };
|
|
284
|
+
return buildArrayOperatorNode(fieldName, operator, value, (currentOperator) => `$nin/$notIn 操作符的值必须是数组 (operator: ${currentOperator})`, "$nin/$notIn 操作符的数组不能为空。提示:空数组会导致查询匹配所有记录,这通常不是预期行为。请检查查询条件或移除该字段。");
|
|
259
285
|
case "$between":
|
|
260
286
|
case "$notBetween":
|
|
261
|
-
if (Array.isArray(value) && value.length === 2) {
|
|
262
|
-
return { type: "op", field: fieldName, operator: operator, value: value };
|
|
263
|
-
}
|
|
264
|
-
return null;
|
|
265
287
|
case "$null":
|
|
266
288
|
case "$notNull":
|
|
267
|
-
|
|
268
|
-
return { type: "op", field: fieldName, operator: operator, value: value };
|
|
269
|
-
}
|
|
270
|
-
return null;
|
|
289
|
+
return buildRangeOrNullOperatorNode(fieldName, operator, value);
|
|
271
290
|
case "$like":
|
|
272
291
|
case "like":
|
|
273
292
|
case "$leftLike":
|
package/lib/sqlBuilder/util.js
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
import { isNullable, isString } from "../../utils/is.js";
|
|
2
2
|
import { SqlCheck } from "./check.js";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
function isQuotedIdentPaired(value) {
|
|
5
|
+
const trimmed = value.trim();
|
|
6
|
+
if (trimmed.length < 2) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const first = trimmed[0];
|
|
11
|
+
const last = trimmed[trimmed.length - 1];
|
|
12
|
+
return (first === "`" && last === "`") || (first === '"' && last === '"');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function startsWithIdentifierQuote(value) {
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
return trimmed.startsWith("`") || trimmed.startsWith('"');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function assertPairedQuotedIdent(value, label) {
|
|
21
|
+
if (startsWithIdentifierQuote(value) && !isQuotedIdentPaired(value)) {
|
|
22
|
+
throw new Error(`${label} 引用不完整,请使用成对的 \`...\` 或 "..." (value: ${value})`, {
|
|
23
|
+
cause: null,
|
|
24
|
+
code: "validation"
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function escapeIdentifierPart(part, kind, quoteIdent, unpairedErrorFactory) {
|
|
30
|
+
if (isQuotedIdent(part)) {
|
|
31
|
+
return part;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (startsWithQuote(part)) {
|
|
35
|
+
throw new Error(unpairedErrorFactory(part), {
|
|
36
|
+
cause: null,
|
|
37
|
+
code: "validation"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
SqlCheck.assertSafeIdentifierPart(part, kind);
|
|
42
|
+
return quoteIdent(part);
|
|
43
|
+
}
|
|
4
44
|
|
|
5
45
|
export function resolveQuoteIdent(options) {
|
|
6
46
|
if (options && options.quoteIdent) {
|
|
@@ -9,7 +49,7 @@ export function resolveQuoteIdent(options) {
|
|
|
9
49
|
|
|
10
50
|
return (identifier) => {
|
|
11
51
|
if (!isString(identifier)) {
|
|
12
|
-
throw new Error(
|
|
52
|
+
throw new Error(`quoteIdent 需要字符串类型标识符 (identifier: ${String(identifier)})`, {
|
|
13
53
|
cause: null,
|
|
14
54
|
code: "validation"
|
|
15
55
|
});
|
|
@@ -17,7 +57,7 @@ export function resolveQuoteIdent(options) {
|
|
|
17
57
|
|
|
18
58
|
const trimmed = identifier.trim();
|
|
19
59
|
if (!trimmed) {
|
|
20
|
-
throw new Error(
|
|
60
|
+
throw new Error("SQL 标识符不能为空", {
|
|
21
61
|
cause: null,
|
|
22
62
|
code: "validation"
|
|
23
63
|
});
|
|
@@ -29,11 +69,11 @@ export function resolveQuoteIdent(options) {
|
|
|
29
69
|
}
|
|
30
70
|
|
|
31
71
|
export function isQuotedIdent(value) {
|
|
32
|
-
return
|
|
72
|
+
return isQuotedIdentPaired(value);
|
|
33
73
|
}
|
|
34
74
|
|
|
35
75
|
export function startsWithQuote(value) {
|
|
36
|
-
return
|
|
76
|
+
return startsWithIdentifierQuote(value);
|
|
37
77
|
}
|
|
38
78
|
|
|
39
79
|
export function escapeField(field, quoteIdent) {
|
|
@@ -43,7 +83,7 @@ export function escapeField(field, quoteIdent) {
|
|
|
43
83
|
|
|
44
84
|
const trimmed = field.trim();
|
|
45
85
|
|
|
46
|
-
|
|
86
|
+
assertPairedQuotedIdent(trimmed, "字段标识符");
|
|
47
87
|
|
|
48
88
|
if (trimmed === "*" || isQuotedIdent(trimmed)) {
|
|
49
89
|
return trimmed;
|
|
@@ -52,7 +92,7 @@ export function escapeField(field, quoteIdent) {
|
|
|
52
92
|
try {
|
|
53
93
|
SqlCheck.assertNoExprField(trimmed);
|
|
54
94
|
} catch {
|
|
55
|
-
throw new Error(
|
|
95
|
+
throw new Error(`字段包含函数/表达式,请使用 selectRaw/whereRaw (field: ${trimmed})`, {
|
|
56
96
|
cause: null,
|
|
57
97
|
code: "validation"
|
|
58
98
|
});
|
|
@@ -61,7 +101,7 @@ export function escapeField(field, quoteIdent) {
|
|
|
61
101
|
if (trimmed.toUpperCase().includes(" AS ")) {
|
|
62
102
|
const parts = trimmed.split(/\s+AS\s+/i);
|
|
63
103
|
if (parts.length !== 2) {
|
|
64
|
-
throw new Error(
|
|
104
|
+
throw new Error(`字段格式非法,请使用简单字段名或安全引用,复杂表达式请使用 selectRaw/whereRaw (field: ${trimmed})`, {
|
|
65
105
|
cause: null,
|
|
66
106
|
code: "validation"
|
|
67
107
|
});
|
|
@@ -71,7 +111,14 @@ export function escapeField(field, quoteIdent) {
|
|
|
71
111
|
const aliasPart = parts[1];
|
|
72
112
|
const cleanFieldPart = fieldPart.trim();
|
|
73
113
|
const cleanAliasPart = aliasPart.trim();
|
|
74
|
-
|
|
114
|
+
if (!isQuotedIdent(cleanAliasPart)) {
|
|
115
|
+
if (!SqlCheck.SAFE_IDENTIFIER_RE.test(cleanAliasPart)) {
|
|
116
|
+
throw new Error(`无效的字段别名: ${cleanAliasPart}`, {
|
|
117
|
+
cause: null,
|
|
118
|
+
code: "validation"
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
75
122
|
return `${escapeField(cleanFieldPart, quoteIdent)} AS ${cleanAliasPart}`;
|
|
76
123
|
}
|
|
77
124
|
|
|
@@ -83,7 +130,7 @@ export function escapeField(field, quoteIdent) {
|
|
|
83
130
|
if (cleanPart === "*" || isQuotedIdent(cleanPart)) {
|
|
84
131
|
return cleanPart;
|
|
85
132
|
}
|
|
86
|
-
|
|
133
|
+
assertPairedQuotedIdent(cleanPart, "字段标识符");
|
|
87
134
|
return quoteIdent(cleanPart);
|
|
88
135
|
})
|
|
89
136
|
.join(".");
|
|
@@ -105,14 +152,14 @@ export function escapeTable(table, quoteIdent) {
|
|
|
105
152
|
|
|
106
153
|
const parts = trimmed.split(/\s+/).filter((p) => p.length > 0);
|
|
107
154
|
if (parts.length === 0) {
|
|
108
|
-
throw new Error(
|
|
155
|
+
throw new Error("FROM 表名不能为空", {
|
|
109
156
|
cause: null,
|
|
110
157
|
code: "validation"
|
|
111
158
|
});
|
|
112
159
|
}
|
|
113
160
|
|
|
114
161
|
if (parts.length > 2) {
|
|
115
|
-
throw new Error(
|
|
162
|
+
throw new Error(`不支持的表引用格式(包含过多片段)。请使用 fromRaw 显式传入复杂表达式 (table: ${trimmed})`, {
|
|
116
163
|
cause: null,
|
|
117
164
|
code: "validation"
|
|
118
165
|
});
|
|
@@ -124,7 +171,7 @@ export function escapeTable(table, quoteIdent) {
|
|
|
124
171
|
|
|
125
172
|
const nameSegments = namePart.split(".");
|
|
126
173
|
if (nameSegments.length > 2) {
|
|
127
|
-
throw new Error(
|
|
174
|
+
throw new Error(`不支持的表引用格式(schema 层级过深)。请使用 fromRaw (table: ${trimmed})`, {
|
|
128
175
|
cause: null,
|
|
129
176
|
code: "validation"
|
|
130
177
|
});
|
|
@@ -137,49 +184,15 @@ export function escapeTable(table, quoteIdent) {
|
|
|
137
184
|
const schema = schemaRaw.trim();
|
|
138
185
|
const tableName = tableNameRaw.trim();
|
|
139
186
|
|
|
140
|
-
const escapedSchema =
|
|
141
|
-
|
|
142
|
-
: (() => {
|
|
143
|
-
if (startsWithQuote(schema) && !isQuotedIdent(schema)) {
|
|
144
|
-
throw new Error(SqlErrors.SCHEMA_QUOTE_NOT_PAIRED(schema), {
|
|
145
|
-
cause: null,
|
|
146
|
-
code: "validation"
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
SqlCheck.assertSafeIdentifierPart(schema, "schema");
|
|
150
|
-
return quoteIdent(schema);
|
|
151
|
-
})();
|
|
152
|
-
|
|
153
|
-
const escapedTableName = isQuotedIdent(tableName)
|
|
154
|
-
? tableName
|
|
155
|
-
: (() => {
|
|
156
|
-
if (startsWithQuote(tableName) && !isQuotedIdent(tableName)) {
|
|
157
|
-
throw new Error(SqlErrors.TABLE_QUOTE_NOT_PAIRED(tableName), {
|
|
158
|
-
cause: null,
|
|
159
|
-
code: "validation"
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
SqlCheck.assertSafeIdentifierPart(tableName, "table");
|
|
163
|
-
return quoteIdent(tableName);
|
|
164
|
-
})();
|
|
187
|
+
const escapedSchema = escapeIdentifierPart(schema, "schema", quoteIdent, (part) => `schema 标识符引用不完整,请使用成对的 \`...\` 或 "..." (schema: ${part})`);
|
|
188
|
+
const escapedTableName = escapeIdentifierPart(tableName, "table", quoteIdent, (part) => `table 标识符引用不完整,请使用成对的 \`...\` 或 "..." (table: ${part})`);
|
|
165
189
|
|
|
166
190
|
escapedName = `${escapedSchema}.${escapedTableName}`;
|
|
167
191
|
} else {
|
|
168
192
|
const tableNameRaw = nameSegments[0];
|
|
169
193
|
const tableName = tableNameRaw.trim();
|
|
170
194
|
|
|
171
|
-
|
|
172
|
-
escapedName = tableName;
|
|
173
|
-
} else {
|
|
174
|
-
if (startsWithQuote(tableName) && !isQuotedIdent(tableName)) {
|
|
175
|
-
throw new Error(SqlErrors.TABLE_QUOTE_NOT_PAIRED(tableName), {
|
|
176
|
-
cause: null,
|
|
177
|
-
code: "validation"
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
SqlCheck.assertSafeIdentifierPart(tableName, "table");
|
|
181
|
-
escapedName = quoteIdent(tableName);
|
|
182
|
-
}
|
|
195
|
+
escapedName = escapeIdentifierPart(tableName, "table", quoteIdent, (part) => `table 标识符引用不完整,请使用成对的 \`...\` 或 "..." (table: ${part})`);
|
|
183
196
|
}
|
|
184
197
|
|
|
185
198
|
if (aliasPart) {
|
|
@@ -196,7 +209,7 @@ export function validateParam(value) {
|
|
|
196
209
|
|
|
197
210
|
export function normalizeLimitValue(count, offset) {
|
|
198
211
|
if (typeof count !== "number" || count < 0) {
|
|
199
|
-
throw new Error(
|
|
212
|
+
throw new Error(`LIMIT 数量必须是非负数 (count: ${String(count)})`, {
|
|
200
213
|
cause: null,
|
|
201
214
|
code: "validation"
|
|
202
215
|
});
|
|
@@ -206,7 +219,7 @@ export function normalizeLimitValue(count, offset) {
|
|
|
206
219
|
let offsetValue = null;
|
|
207
220
|
if (!isNullable(offset)) {
|
|
208
221
|
if (typeof offset !== "number" || offset < 0) {
|
|
209
|
-
throw new Error(
|
|
222
|
+
throw new Error(`OFFSET 必须是非负数 (offset: ${String(offset)})`, {
|
|
210
223
|
cause: null,
|
|
211
224
|
code: "validation"
|
|
212
225
|
});
|
|
@@ -219,7 +232,7 @@ export function normalizeLimitValue(count, offset) {
|
|
|
219
232
|
|
|
220
233
|
export function normalizeOffsetValue(count) {
|
|
221
234
|
if (typeof count !== "number" || count < 0) {
|
|
222
|
-
throw new Error(
|
|
235
|
+
throw new Error(`OFFSET 必须是非负数 (count: ${String(count)})`, {
|
|
223
236
|
cause: null,
|
|
224
237
|
code: "validation"
|
|
225
238
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.20.
|
|
4
|
-
"gitHead": "
|
|
3
|
+
"version": "3.20.9",
|
|
4
|
+
"gitHead": "c6107d7e5006ffeba25cedef95ae0c7a5d294673",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",
|
|
7
7
|
"keywords": [
|
package/lib/cacheKeys.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cache Key 统一管理
|
|
3
|
-
* 所有缓存键在此统一定义,避免硬编码分散
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Cache Key 生成函数集合
|
|
8
|
-
*/
|
|
9
|
-
export class CacheKeys {
|
|
10
|
-
/** 所有接口缓存2 */
|
|
11
|
-
static apisAll() {
|
|
12
|
-
return "apis:all";
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** 所有菜单缓存 */
|
|
16
|
-
static menusAll() {
|
|
17
|
-
return "menus:all";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** 角色信息缓存(完整角色对象) */
|
|
21
|
-
static roleInfo(roleCode) {
|
|
22
|
-
return `role:info:${roleCode}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 角色菜单权限缓存(Set 集合)
|
|
27
|
-
* - key: role:menus:${roleCode}
|
|
28
|
-
* - member: menu.path(例如 /permission/role)
|
|
29
|
-
*/
|
|
30
|
-
static roleMenus(roleCode) {
|
|
31
|
-
return `role:menus:${roleCode}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 角色接口权限缓存(Set 集合)
|
|
36
|
-
* - key: role:apis:${roleCode}
|
|
37
|
-
* - member: url.pathname(例如 /api/user/login;与 method 无关)
|
|
38
|
-
*/
|
|
39
|
-
static roleApis(roleCode) {
|
|
40
|
-
return `role:apis:${roleCode}`;
|
|
41
|
-
}
|
|
42
|
-
}
|