befly 3.20.7 → 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/utils/util.js +4 -15
- package/lib/cacheKeys.js +0 -42
- package/lib/sqlBuilder/errors.js +0 -60
package/lib/sqlBuilder/check.js
CHANGED
|
@@ -1,63 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isNonEmptyString } from "../../utils/is.js";
|
|
2
|
+
|
|
3
|
+
function throwValidation(message) {
|
|
4
|
+
throw new Error(message, {
|
|
5
|
+
cause: null,
|
|
6
|
+
code: "validation"
|
|
7
|
+
});
|
|
8
|
+
}
|
|
2
9
|
|
|
3
10
|
export class SqlCheck {
|
|
4
11
|
static assertNoUndefinedParam(value, label) {
|
|
5
12
|
if (value === undefined) {
|
|
6
|
-
|
|
7
|
-
cause: null,
|
|
8
|
-
code: "validation"
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
static startsWithQuote(value) {
|
|
14
|
-
const trimmed = value.trim();
|
|
15
|
-
return trimmed.startsWith("`") || trimmed.startsWith('"');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
static isQuotedIdentPaired(value) {
|
|
19
|
-
const trimmed = value.trim();
|
|
20
|
-
if (!isMinLengthString(trimmed, 2)) {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
const first = trimmed[0];
|
|
24
|
-
const last = trimmed[trimmed.length - 1];
|
|
25
|
-
if (first === "`" && last === "`") {
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
if (first === '"' && last === '"') {
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
static assertPairedQuotedIdentIfStartsWithQuote(value, label) {
|
|
35
|
-
if (SqlCheck.startsWithQuote(value) && !SqlCheck.isQuotedIdentPaired(value)) {
|
|
36
|
-
throw new Error(`${label} 引用不完整,请使用成对的 \`...\` 或 "..." (value: ${value})`, {
|
|
37
|
-
cause: null,
|
|
38
|
-
code: "validation"
|
|
39
|
-
});
|
|
13
|
+
throwValidation(`${label} 不能为 undefined`);
|
|
40
14
|
}
|
|
41
15
|
}
|
|
42
16
|
|
|
43
17
|
static assertSafeIdentifierPart(part, kind) {
|
|
44
18
|
if (!SqlCheck.SAFE_IDENTIFIER_RE.test(part)) {
|
|
45
|
-
|
|
46
|
-
cause: null,
|
|
47
|
-
code: "validation"
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
static assertSafeAlias(aliasPart) {
|
|
53
|
-
if (SqlCheck.isQuotedIdentPaired(aliasPart)) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (!SqlCheck.SAFE_IDENTIFIER_RE.test(aliasPart)) {
|
|
57
|
-
throw new Error(`无效的字段别名: ${aliasPart}`, {
|
|
58
|
-
cause: null,
|
|
59
|
-
code: "validation"
|
|
60
|
-
});
|
|
19
|
+
throwValidation(`无效的 ${kind} 标识符: ${part}`);
|
|
61
20
|
}
|
|
62
21
|
}
|
|
63
22
|
|
|
@@ -67,41 +26,26 @@ export class SqlCheck {
|
|
|
67
26
|
}
|
|
68
27
|
const trimmed = field.trim();
|
|
69
28
|
if (trimmed.includes("(") || trimmed.includes(")")) {
|
|
70
|
-
|
|
71
|
-
cause: null,
|
|
72
|
-
code: "validation"
|
|
73
|
-
});
|
|
29
|
+
throwValidation(`字段包含函数/表达式,请使用 selectRaw/whereRaw (field: ${trimmed})`);
|
|
74
30
|
}
|
|
75
31
|
}
|
|
76
32
|
|
|
77
33
|
static assertBatchInsertRowsConsistent(rows, options) {
|
|
78
34
|
if (!Array.isArray(rows)) {
|
|
79
|
-
|
|
80
|
-
cause: null,
|
|
81
|
-
code: "validation"
|
|
82
|
-
});
|
|
35
|
+
throwValidation("批量插入 rows 必须是数组");
|
|
83
36
|
}
|
|
84
37
|
if (rows.length === 0) {
|
|
85
|
-
|
|
86
|
-
cause: null,
|
|
87
|
-
code: "validation"
|
|
88
|
-
});
|
|
38
|
+
throwValidation(`插入数据不能为空 (table: ${options.table})`);
|
|
89
39
|
}
|
|
90
40
|
|
|
91
41
|
const first = rows[0];
|
|
92
42
|
if (!first || typeof first !== "object" || Array.isArray(first)) {
|
|
93
|
-
|
|
94
|
-
cause: null,
|
|
95
|
-
code: "validation"
|
|
96
|
-
});
|
|
43
|
+
throwValidation(`批量插入的每一行必须是对象 (table: ${options.table}, rowIndex: 0)`);
|
|
97
44
|
}
|
|
98
45
|
|
|
99
46
|
const fields = Object.keys(first);
|
|
100
47
|
if (fields.length === 0) {
|
|
101
|
-
|
|
102
|
-
cause: null,
|
|
103
|
-
code: "validation"
|
|
104
|
-
});
|
|
48
|
+
throwValidation(`插入数据必须至少有一个字段 (table: ${options.table})`);
|
|
105
49
|
}
|
|
106
50
|
|
|
107
51
|
const fieldSet = new Set(fields);
|
|
@@ -109,35 +53,23 @@ export class SqlCheck {
|
|
|
109
53
|
for (let i = 0; i < rows.length; i++) {
|
|
110
54
|
const row = rows[i];
|
|
111
55
|
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
112
|
-
|
|
113
|
-
cause: null,
|
|
114
|
-
code: "validation"
|
|
115
|
-
});
|
|
56
|
+
throwValidation(`批量插入的每一行必须是对象 (table: ${options.table}, rowIndex: ${i})`);
|
|
116
57
|
}
|
|
117
58
|
|
|
118
59
|
const rowKeys = Object.keys(row);
|
|
119
60
|
if (rowKeys.length !== fields.length) {
|
|
120
|
-
|
|
121
|
-
cause: null,
|
|
122
|
-
code: "validation"
|
|
123
|
-
});
|
|
61
|
+
throwValidation(`批量插入每行字段必须一致 (table: ${options.table}, rowIndex: ${i})`);
|
|
124
62
|
}
|
|
125
63
|
|
|
126
64
|
for (const key of rowKeys) {
|
|
127
65
|
if (!fieldSet.has(key)) {
|
|
128
|
-
|
|
129
|
-
cause: null,
|
|
130
|
-
code: "validation"
|
|
131
|
-
});
|
|
66
|
+
throwValidation(`批量插入每行字段必须一致 (table: ${options.table}, rowIndex: ${i}, extraField: ${key})`);
|
|
132
67
|
}
|
|
133
68
|
}
|
|
134
69
|
|
|
135
70
|
for (const field of fields) {
|
|
136
71
|
if (!(field in row)) {
|
|
137
|
-
|
|
138
|
-
cause: null,
|
|
139
|
-
code: "validation"
|
|
140
|
-
});
|
|
72
|
+
throwValidation(`批量插入缺少字段 (table: ${options.table}, rowIndex: ${i}, field: ${field})`);
|
|
141
73
|
}
|
|
142
74
|
SqlCheck.assertNoUndefinedParam(row[field], `批量插入字段值 (table: ${options.table}, rowIndex: ${i}, field: ${field})`);
|
|
143
75
|
}
|
|
@@ -1,119 +1,124 @@
|
|
|
1
1
|
import { SqlCheck } from "./check.js";
|
|
2
|
-
import { SqlErrors } from "./errors.js";
|
|
3
2
|
import { escapeField, escapeTable, validateParam } from "./util.js";
|
|
4
3
|
import { isString } from "../../utils/is.js";
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (model.select.length > 0) {
|
|
11
|
-
const fields = model.select.map((item) => {
|
|
12
|
-
if (item.type === "raw") {
|
|
13
|
-
return item.value;
|
|
14
|
-
}
|
|
15
|
-
return escapeField(item.value, quoteIdent);
|
|
16
|
-
});
|
|
17
|
-
sql += fields.join(", ");
|
|
18
|
-
} else {
|
|
19
|
-
sql += "*";
|
|
5
|
+
function pushParams(target, source) {
|
|
6
|
+
for (const param of source) {
|
|
7
|
+
target.push(param);
|
|
20
8
|
}
|
|
9
|
+
}
|
|
21
10
|
|
|
11
|
+
function compileFromSql(model, quoteIdent, errorMessage) {
|
|
22
12
|
if (!model.from) {
|
|
23
|
-
throw new Error(
|
|
13
|
+
throw new Error(errorMessage, {
|
|
24
14
|
cause: null,
|
|
25
15
|
code: "validation"
|
|
26
16
|
});
|
|
27
17
|
}
|
|
28
18
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (model.joins.length > 0) {
|
|
33
|
-
const joinSql = model.joins
|
|
34
|
-
.map((join) => {
|
|
35
|
-
const joinTable = escapeTable(join.table, quoteIdent);
|
|
36
|
-
const joinType = join.type.toUpperCase();
|
|
37
|
-
return `${joinType} JOIN ${joinTable} ON ${join.on}`;
|
|
38
|
-
})
|
|
39
|
-
.join(" ");
|
|
40
|
-
sql += ` ${joinSql}`;
|
|
41
|
-
}
|
|
19
|
+
return model.from.type === "raw" ? model.from.value : escapeTable(model.from.value, quoteIdent);
|
|
20
|
+
}
|
|
42
21
|
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
for (const param of whereResult.params) {
|
|
47
|
-
params.push(param);
|
|
48
|
-
}
|
|
22
|
+
function compileJoinSql(model, quoteIdent) {
|
|
23
|
+
if (model.joins.length === 0) {
|
|
24
|
+
return "";
|
|
49
25
|
}
|
|
50
26
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
27
|
+
return model.joins
|
|
28
|
+
.map((join) => {
|
|
29
|
+
const joinTable = escapeTable(join.table, quoteIdent);
|
|
30
|
+
const joinType = join.type.toUpperCase();
|
|
31
|
+
return `${joinType} JOIN ${joinTable} ON ${join.on}`;
|
|
32
|
+
})
|
|
33
|
+
.join(" ");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function appendCompiledWhere(sql, params, whereNode, quoteIdent) {
|
|
37
|
+
const whereResult = compileWhere(whereNode, quoteIdent);
|
|
38
|
+
if (!whereResult.sql) {
|
|
39
|
+
return sql;
|
|
59
40
|
}
|
|
60
41
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
42
|
+
pushParams(params, whereResult.params);
|
|
43
|
+
return `${sql} WHERE ${whereResult.sql}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function compileReadBaseSql(model, quoteIdent, headSql, fromErrorMessage) {
|
|
47
|
+
const params = [];
|
|
48
|
+
let sql = `${headSql} FROM ${compileFromSql(model, quoteIdent, fromErrorMessage)}`;
|
|
49
|
+
|
|
50
|
+
const joinSql = compileJoinSql(model, quoteIdent);
|
|
51
|
+
if (joinSql) {
|
|
52
|
+
sql += ` ${joinSql}`;
|
|
66
53
|
}
|
|
67
54
|
|
|
55
|
+
sql = appendCompiledWhere(sql, params, model.where, quoteIdent);
|
|
68
56
|
return { sql: sql, params: params };
|
|
69
57
|
}
|
|
70
58
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
59
|
+
function compileOrderBySql(orderBy, quoteIdent) {
|
|
60
|
+
if (orderBy.length === 0) {
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
74
63
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
})
|
|
64
|
+
return orderBy
|
|
65
|
+
.map((item) => {
|
|
66
|
+
const escapedField = escapeField(item.field, quoteIdent);
|
|
67
|
+
return `${escapedField} ${item.dir}`;
|
|
68
|
+
})
|
|
69
|
+
.join(", ");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function appendLimitSql(sql, model) {
|
|
73
|
+
if (model.limit === null) {
|
|
74
|
+
return sql;
|
|
80
75
|
}
|
|
81
76
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (model.joins.length > 0) {
|
|
86
|
-
const joinSql = model.joins
|
|
87
|
-
.map((join) => {
|
|
88
|
-
const joinTable = escapeTable(join.table, quoteIdent);
|
|
89
|
-
const joinType = join.type.toUpperCase();
|
|
90
|
-
return `${joinType} JOIN ${joinTable} ON ${join.on}`;
|
|
91
|
-
})
|
|
92
|
-
.join(" ");
|
|
93
|
-
sql += ` ${joinSql}`;
|
|
77
|
+
let result = `${sql} LIMIT ${model.limit}`;
|
|
78
|
+
if (model.offset !== null) {
|
|
79
|
+
result += ` OFFSET ${model.offset}`;
|
|
94
80
|
}
|
|
95
81
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function compileSelect(model, quoteIdent) {
|
|
86
|
+
const selectSql =
|
|
87
|
+
model.select.length > 0
|
|
88
|
+
? model.select
|
|
89
|
+
.map((item) => {
|
|
90
|
+
if (item.type === "raw") {
|
|
91
|
+
return item.value;
|
|
92
|
+
}
|
|
93
|
+
return escapeField(item.value, quoteIdent);
|
|
94
|
+
})
|
|
95
|
+
.join(", ")
|
|
96
|
+
: "*";
|
|
97
|
+
|
|
98
|
+
const result = compileReadBaseSql(model, quoteIdent, `SELECT ${selectSql}`, "FROM 表名是必需的");
|
|
99
|
+
const orderSql = compileOrderBySql(model.orderBy, quoteIdent);
|
|
100
|
+
if (orderSql) {
|
|
101
|
+
result.sql += ` ORDER BY ${orderSql}`;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
result.sql = appendLimitSql(result.sql, model);
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function compileCount(model, quoteIdent) {
|
|
109
|
+
return compileReadBaseSql(model, quoteIdent, "SELECT COUNT(*) as total", "COUNT 需要 FROM 表名");
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
export function compileInsert(table, data, quoteIdent) {
|
|
108
113
|
if (!table || !isString(table)) {
|
|
109
|
-
throw new Error(
|
|
114
|
+
throw new Error(`INSERT 需要表名 (table: ${String(table)})`, {
|
|
110
115
|
cause: null,
|
|
111
116
|
code: "validation"
|
|
112
117
|
});
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
if (!data || typeof data !== "object") {
|
|
116
|
-
throw new Error(
|
|
121
|
+
throw new Error(`INSERT 需要数据 (table: ${String(table)}, data: ${JSON.stringify(data)})`, {
|
|
117
122
|
cause: null,
|
|
118
123
|
code: "validation"
|
|
119
124
|
});
|
|
@@ -144,7 +149,7 @@ export function compileInsert(table, data, quoteIdent) {
|
|
|
144
149
|
|
|
145
150
|
const fields = Object.keys(data);
|
|
146
151
|
if (fields.length === 0) {
|
|
147
|
-
throw new Error(
|
|
152
|
+
throw new Error(`插入数据必须至少有一个字段 (table: ${table})`, {
|
|
148
153
|
cause: null,
|
|
149
154
|
code: "validation"
|
|
150
155
|
});
|
|
@@ -167,14 +172,14 @@ export function compileInsert(table, data, quoteIdent) {
|
|
|
167
172
|
|
|
168
173
|
export function compileUpdate(model, table, data, quoteIdent) {
|
|
169
174
|
if (!table || !isString(table)) {
|
|
170
|
-
throw new Error(
|
|
175
|
+
throw new Error("UPDATE 需要表名", {
|
|
171
176
|
cause: null,
|
|
172
177
|
code: "validation"
|
|
173
178
|
});
|
|
174
179
|
}
|
|
175
180
|
|
|
176
181
|
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
177
|
-
throw new Error(
|
|
182
|
+
throw new Error("UPDATE 需要数据对象", {
|
|
178
183
|
cause: null,
|
|
179
184
|
code: "validation"
|
|
180
185
|
});
|
|
@@ -182,7 +187,7 @@ export function compileUpdate(model, table, data, quoteIdent) {
|
|
|
182
187
|
|
|
183
188
|
const fields = Object.keys(data);
|
|
184
189
|
if (fields.length === 0) {
|
|
185
|
-
throw new Error(
|
|
190
|
+
throw new Error("更新数据必须至少有一个字段", {
|
|
186
191
|
cause: null,
|
|
187
192
|
code: "validation"
|
|
188
193
|
});
|
|
@@ -200,11 +205,9 @@ export function compileUpdate(model, table, data, quoteIdent) {
|
|
|
200
205
|
const whereResult = compileWhere(model.where, quoteIdent);
|
|
201
206
|
if (whereResult.sql) {
|
|
202
207
|
sql += ` WHERE ${whereResult.sql}`;
|
|
203
|
-
|
|
204
|
-
params.push(param);
|
|
205
|
-
}
|
|
208
|
+
pushParams(params, whereResult.params);
|
|
206
209
|
} else {
|
|
207
|
-
throw new Error(
|
|
210
|
+
throw new Error("为安全起见,UPDATE 需要 WHERE 条件", {
|
|
208
211
|
cause: null,
|
|
209
212
|
code: "validation"
|
|
210
213
|
});
|
|
@@ -215,7 +218,7 @@ export function compileUpdate(model, table, data, quoteIdent) {
|
|
|
215
218
|
|
|
216
219
|
export function compileDelete(model, table, quoteIdent) {
|
|
217
220
|
if (!table || !isString(table)) {
|
|
218
|
-
throw new Error(
|
|
221
|
+
throw new Error("DELETE 需要表名", {
|
|
219
222
|
cause: null,
|
|
220
223
|
code: "validation"
|
|
221
224
|
});
|
|
@@ -228,7 +231,7 @@ export function compileDelete(model, table, quoteIdent) {
|
|
|
228
231
|
if (whereResult.sql) {
|
|
229
232
|
sql += ` WHERE ${whereResult.sql}`;
|
|
230
233
|
} else {
|
|
231
|
-
throw new Error(
|
|
234
|
+
throw new Error("为安全起见,DELETE 需要 WHERE 条件", {
|
|
232
235
|
cause: null,
|
|
233
236
|
code: "validation"
|
|
234
237
|
});
|
|
@@ -269,9 +272,7 @@ export function compileWhere(node, quoteIdent) {
|
|
|
269
272
|
clause = `(${clause})`;
|
|
270
273
|
}
|
|
271
274
|
parts.push(clause);
|
|
272
|
-
|
|
273
|
-
params.push(param);
|
|
274
|
-
}
|
|
275
|
+
pushParams(params, built.params);
|
|
275
276
|
}
|
|
276
277
|
return { sql: parts.join(` ${node.join} `), params: params };
|
|
277
278
|
}
|