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.
@@ -1,63 +1,22 @@
1
- import { isMinLengthString, isNonEmptyString } from "../../utils/is.js";
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
- throw new Error(`${label} 不能为 undefined`, {
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
- throw new Error(`无效的 ${kind} 标识符: ${part}`, {
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
- throw new Error(`字段包含函数/表达式,请使用 selectRaw/whereRaw (field: ${trimmed})`, {
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
- throw new Error("批量插入 rows 必须是数组", {
80
- cause: null,
81
- code: "validation"
82
- });
35
+ throwValidation("批量插入 rows 必须是数组");
83
36
  }
84
37
  if (rows.length === 0) {
85
- throw new Error(`插入数据不能为空 (table: ${options.table})`, {
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
- throw new Error(`批量插入的每一行必须是对象 (table: ${options.table}, rowIndex: 0)`, {
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
- throw new Error(`插入数据必须至少有一个字段 (table: ${options.table})`, {
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
- throw new Error(`批量插入的每一行必须是对象 (table: ${options.table}, rowIndex: ${i})`, {
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
- throw new Error(`批量插入每行字段必须一致 (table: ${options.table}, rowIndex: ${i})`, {
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
- throw new Error(`批量插入每行字段必须一致 (table: ${options.table}, rowIndex: ${i}, extraField: ${key})`, {
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
- throw new Error(`批量插入缺少字段 (table: ${options.table}, rowIndex: ${i}, field: ${field})`, {
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
- export function compileSelect(model, quoteIdent) {
7
- let sql = "SELECT ";
8
- const params = [];
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(SqlErrors.FROM_REQUIRED, {
13
+ throw new Error(errorMessage, {
24
14
  cause: null,
25
15
  code: "validation"
26
16
  });
27
17
  }
28
18
 
29
- const fromSql = model.from.type === "raw" ? model.from.value : escapeTable(model.from.value, quoteIdent);
30
- sql += ` FROM ${fromSql}`;
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
- const whereResult = compileWhere(model.where, quoteIdent);
44
- if (whereResult.sql) {
45
- sql += ` WHERE ${whereResult.sql}`;
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
- if (model.orderBy.length > 0) {
52
- const orderSql = model.orderBy
53
- .map((item) => {
54
- const escapedField = escapeField(item.field, quoteIdent);
55
- return `${escapedField} ${item.dir}`;
56
- })
57
- .join(", ");
58
- sql += ` ORDER BY ${orderSql}`;
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
- if (model.limit !== null) {
62
- sql += ` LIMIT ${model.limit}`;
63
- if (model.offset !== null) {
64
- sql += ` OFFSET ${model.offset}`;
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
- export function compileCount(model, quoteIdent) {
72
- let sql = "SELECT COUNT(*) as total";
73
- const params = [];
59
+ function compileOrderBySql(orderBy, quoteIdent) {
60
+ if (orderBy.length === 0) {
61
+ return "";
62
+ }
74
63
 
75
- if (!model.from) {
76
- throw new Error(SqlErrors.COUNT_NEED_FROM, {
77
- cause: null,
78
- code: "validation"
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
- const fromSql = model.from.type === "raw" ? model.from.value : escapeTable(model.from.value, quoteIdent);
83
- sql += ` FROM ${fromSql}`;
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
- const whereResult = compileWhere(model.where, quoteIdent);
97
- if (whereResult.sql) {
98
- sql += ` WHERE ${whereResult.sql}`;
99
- for (const param of whereResult.params) {
100
- params.push(param);
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
- return { sql: sql, params: params };
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(SqlErrors.INSERT_NEED_TABLE(table), {
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(SqlErrors.INSERT_NEED_DATA(table, data), {
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(SqlErrors.INSERT_NEED_AT_LEAST_ONE_FIELD(table), {
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(SqlErrors.UPDATE_NEED_TABLE, {
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(SqlErrors.UPDATE_NEED_OBJECT, {
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(SqlErrors.UPDATE_NEED_AT_LEAST_ONE_FIELD, {
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
- for (const param of whereResult.params) {
204
- params.push(param);
205
- }
208
+ pushParams(params, whereResult.params);
206
209
  } else {
207
- throw new Error(SqlErrors.UPDATE_NEED_WHERE, {
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(SqlErrors.DELETE_NEED_TABLE, {
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(SqlErrors.DELETE_NEED_WHERE, {
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
- for (const param of built.params) {
273
- params.push(param);
274
- }
275
+ pushParams(params, built.params);
275
276
  }
276
277
  return { sql: parts.join(` ${node.join} `), params: params };
277
278
  }