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/lib/sqlBuilder.js CHANGED
@@ -3,20 +3,8 @@
3
3
  * 提供链式 API 构建 SQL 查询
4
4
  */
5
5
 
6
- import { assertBatchInsertRowsConsistent, assertNoUndefinedParam, escapeField, escapeTable, normalizeLimitValue, normalizeOffsetValue, resolveQuoteIdent, validateParam } from "./dbUtil.js";
7
- import { isNonEmptyString, isString } from "../utils/is.js";
8
-
9
- function createModel() {
10
- return {
11
- select: [],
12
- from: null,
13
- where: { type: "group", join: "AND", items: [] },
14
- joins: [],
15
- orderBy: [],
16
- limit: null,
17
- offset: null
18
- };
19
- }
6
+ import { escapeField, escapeTable, resolveQuoteIdent } from "./dbUtil.js";
7
+ import { isNonEmptyString } from "../utils/is.js";
20
8
 
21
9
  /**
22
10
  * SQL 构建器类
@@ -27,188 +15,37 @@ export class SqlBuilder {
27
15
 
28
16
  constructor(options = {}) {
29
17
  this._quoteIdent = resolveQuoteIdent(options);
30
- this._model = createModel();
18
+ this._model = {
19
+ select: [],
20
+ from: null,
21
+ where: { type: "group", join: "AND", items: [] },
22
+ joins: [],
23
+ orderBy: [],
24
+ limit: null,
25
+ offset: null
26
+ };
31
27
  }
32
28
 
33
29
  /**
34
30
  * 重置构建器状态
35
31
  */
36
32
  reset() {
37
- this._model = createModel();
33
+ this._model = {
34
+ select: [],
35
+ from: null,
36
+ where: { type: "group", join: "AND", items: [] },
37
+ joins: [],
38
+ orderBy: [],
39
+ limit: null,
40
+ offset: null
41
+ };
38
42
  return this;
39
43
  }
40
44
 
41
- _appendWhereNode(root, node) {
42
- if (node) {
43
- root.items.push(node);
44
- }
45
- }
46
-
47
- _buildArrayOperatorNode(fieldName, operator, value, errorFactory, emptyMessage) {
48
- if (!Array.isArray(value)) {
49
- throw new Error(errorFactory(operator), {
50
- cause: null,
51
- code: "validation"
52
- });
53
- }
54
- if (value.length === 0) {
55
- throw new Error(emptyMessage, {
56
- cause: null,
57
- code: "validation"
58
- });
59
- }
60
-
61
- return { type: "op", field: fieldName, operator: operator, value: value };
62
- }
63
-
64
- _buildRangeOrNullOperatorNode(fieldName, operator, value) {
65
- if (operator === "$between" || operator === "$notBetween") {
66
- if (Array.isArray(value) && value.length === 2) {
67
- return { type: "op", field: fieldName, operator: operator, value: value };
68
- }
69
- return null;
70
- }
71
-
72
- if (value === true) {
73
- return { type: "op", field: fieldName, operator: operator, value: value };
74
- }
75
-
76
- return null;
77
- }
78
-
79
- _buildOperatorNode(fieldName, operator, value) {
80
- switch (operator) {
81
- case "$in":
82
- return this._buildArrayOperatorNode(fieldName, operator, value, (currentOperator) => `$in 操作符的值必须是数组 (operator: ${currentOperator})`, "$in 操作符的数组不能为空。提示:空数组会导致查询永远不匹配任何记录,这通常不是预期行为。请检查查询条件或移除该字段。");
83
- case "$nin":
84
- case "$notIn":
85
- return this._buildArrayOperatorNode(fieldName, operator, value, (currentOperator) => `$nin/$notIn 操作符的值必须是数组 (operator: ${currentOperator})`, "$nin/$notIn 操作符的数组不能为空。提示:空数组会导致查询匹配所有记录,这通常不是预期行为。请检查查询条件或移除该字段。");
86
- case "$between":
87
- case "$notBetween":
88
- case "$null":
89
- case "$notNull":
90
- return this._buildRangeOrNullOperatorNode(fieldName, operator, value);
91
- case "$like":
92
- case "like":
93
- case "$leftLike":
94
- case "leftLike":
95
- case "$rightLike":
96
- case "rightLike":
97
- if (isString(value) && value.trim() === "") {
98
- return null;
99
- }
100
- validateParam(value);
101
- return { type: "op", field: fieldName, operator: operator, value: value };
102
- default:
103
- validateParam(value);
104
- return { type: "op", field: fieldName, operator: operator, value: value };
105
- }
106
- }
107
-
108
- _appendFieldCondition(group, key, value) {
109
- if (key.includes("$")) {
110
- const lastDollarIndex = key.lastIndexOf("$");
111
- this._appendWhereNode(group, this._buildOperatorNode(key.substring(0, lastDollarIndex), `$${key.substring(lastDollarIndex + 1)}`, value));
112
- return;
113
- }
114
-
115
- if (value && typeof value === "object" && !Array.isArray(value)) {
116
- for (const [operator, operatorValue] of Object.entries(value)) {
117
- this._appendWhereNode(group, this._buildOperatorNode(key, operator, operatorValue));
118
- }
119
- return;
120
- }
121
-
122
- this._appendWhereNode(group, this._buildOperatorNode(key, "=", value));
123
- }
124
-
125
- _appendAndConditions(group, value) {
126
- if (!Array.isArray(value)) {
127
- return;
128
- }
129
-
130
- for (const condition of value) {
131
- if (!condition || typeof condition !== "object" || Array.isArray(condition)) {
132
- continue;
133
- }
134
-
135
- const sub = this._parseWhereObject(condition);
136
- if (sub.items.length === 0) {
137
- continue;
138
- }
139
-
140
- if (sub.join === "AND") {
141
- for (const item of sub.items) {
142
- group.items.push(item);
143
- }
144
- continue;
145
- }
146
-
147
- group.items.push(sub);
148
- }
149
- }
150
-
151
- _appendOrConditions(group, value) {
152
- if (!Array.isArray(value)) {
153
- return;
154
- }
155
-
156
- const orGroup = { type: "group", join: "OR", items: [] };
157
- for (const condition of value) {
158
- if (!condition || typeof condition !== "object" || Array.isArray(condition)) {
159
- continue;
160
- }
161
-
162
- const sub = this._parseWhereObject(condition);
163
- if (sub.items.length > 0) {
164
- orGroup.items.push(sub);
165
- }
166
- }
167
-
168
- if (orGroup.items.length > 0) {
169
- group.items.push(orGroup);
170
- }
171
- }
172
-
173
- _parseWhereObject(whereObj) {
174
- if (!whereObj || typeof whereObj !== "object") {
175
- return { type: "group", join: "AND", items: [] };
176
- }
177
-
178
- const group = { type: "group", join: "AND", items: [] };
179
-
180
- for (const [key, value] of Object.entries(whereObj)) {
181
- if (value === undefined) {
182
- continue;
183
- }
184
-
185
- if (key === "$and") {
186
- this._appendAndConditions(group, value);
187
- continue;
188
- }
189
-
190
- if (key === "$or") {
191
- this._appendOrConditions(group, value);
192
- continue;
193
- }
194
-
195
- this._appendFieldCondition(group, key, value);
196
- }
197
-
198
- return group;
199
- }
200
-
201
- _pushWhereParams(target, source) {
202
- for (const param of source) {
203
- target.push(param);
204
- }
205
- }
206
-
207
45
  _compileOperatorNode(node) {
208
46
  const escapedField = escapeField(node.field, this._quoteIdent);
209
47
 
210
- if (node.operator === "$ne" || node.operator === "$not") {
211
- validateParam(node.value);
48
+ if (node.operator === "$not") {
212
49
  return { sql: `${escapedField} != ?`, params: [node.value] };
213
50
  }
214
51
 
@@ -219,50 +56,42 @@ export class SqlBuilder {
219
56
  };
220
57
  }
221
58
 
222
- if (node.operator === "$nin" || node.operator === "$notIn") {
59
+ if (node.operator === "$notIn") {
223
60
  return {
224
61
  sql: `${escapedField} NOT IN (${node.value.map(() => "?").join(",")})`,
225
62
  params: node.value.slice()
226
63
  };
227
64
  }
228
65
 
229
- if (node.operator === "$like" || node.operator === "like") {
230
- validateParam(node.value);
66
+ if (node.operator === "$like") {
231
67
  return { sql: `${escapedField} LIKE ?`, params: [`%${String(node.value)}%`] };
232
68
  }
233
69
 
234
- if (node.operator === "$leftLike" || node.operator === "leftLike") {
235
- validateParam(node.value);
70
+ if (node.operator === "$leftLike") {
236
71
  return { sql: `${escapedField} LIKE ?`, params: [`%${String(node.value)}`] };
237
72
  }
238
73
 
239
- if (node.operator === "$rightLike" || node.operator === "rightLike") {
240
- validateParam(node.value);
74
+ if (node.operator === "$rightLike") {
241
75
  return { sql: `${escapedField} LIKE ?`, params: [`${String(node.value)}%`] };
242
76
  }
243
77
 
244
78
  if (node.operator === "$notLike") {
245
- validateParam(node.value);
246
79
  return { sql: `${escapedField} NOT LIKE ?`, params: [node.value] };
247
80
  }
248
81
 
249
82
  if (node.operator === "$gt") {
250
- validateParam(node.value);
251
83
  return { sql: `${escapedField} > ?`, params: [node.value] };
252
84
  }
253
85
 
254
86
  if (node.operator === "$gte") {
255
- validateParam(node.value);
256
87
  return { sql: `${escapedField} >= ?`, params: [node.value] };
257
88
  }
258
89
 
259
90
  if (node.operator === "$lt") {
260
- validateParam(node.value);
261
91
  return { sql: `${escapedField} < ?`, params: [node.value] };
262
92
  }
263
93
 
264
94
  if (node.operator === "$lte") {
265
- validateParam(node.value);
266
95
  return { sql: `${escapedField} <= ?`, params: [node.value] };
267
96
  }
268
97
 
@@ -282,7 +111,6 @@ export class SqlBuilder {
282
111
  return { sql: `${escapedField} IS NOT NULL`, params: [] };
283
112
  }
284
113
 
285
- validateParam(node.value);
286
114
  return { sql: `${escapedField} = ?`, params: [node.value] };
287
115
  }
288
116
 
@@ -292,7 +120,10 @@ export class SqlBuilder {
292
120
  }
293
121
 
294
122
  if (node.type === "raw") {
295
- return { sql: node.sql, params: Array.isArray(node.params) ? node.params.slice() : [] };
123
+ if (Array.isArray(node.params)) {
124
+ return { sql: node.sql, params: node.params.slice() };
125
+ }
126
+ return { sql: node.sql, params: [] };
296
127
  }
297
128
 
298
129
  if (node.type === "op") {
@@ -320,7 +151,9 @@ export class SqlBuilder {
320
151
  }
321
152
 
322
153
  parts.push(clause);
323
- this._pushWhereParams(params, built.params);
154
+ for (const param of built.params) {
155
+ params.push(param);
156
+ }
324
157
  }
325
158
 
326
159
  return { sql: parts.join(` ${node.join} `), params: params };
@@ -345,9 +178,7 @@ export class SqlBuilder {
345
178
  return this;
346
179
  }
347
180
 
348
- if (isString(fields)) {
349
- this._model.select.push({ type: "field", value: fields });
350
- }
181
+ this._model.select.push({ type: "field", value: fields });
351
182
  return this;
352
183
  }
353
184
 
@@ -378,18 +209,20 @@ export class SqlBuilder {
378
209
  /**
379
210
  * WHERE 条件
380
211
  */
381
- where(conditionOrField, value) {
382
- if (conditionOrField && typeof conditionOrField === "object" && !Array.isArray(conditionOrField)) {
383
- const node = this._parseWhereObject(conditionOrField);
384
- if (node.items.length > 0) {
385
- this._model.where.items.push(node);
386
- }
212
+ where(conditionOrField) {
213
+ if (this._model.where.items.length === 0) {
214
+ this._model.where = conditionOrField;
387
215
  return this;
388
216
  }
389
217
 
390
- if (isString(conditionOrField)) {
391
- this._appendWhereNode(this._model.where, this._buildOperatorNode(conditionOrField, "=", value));
218
+ if (conditionOrField.type === "group" && conditionOrField.join === "AND") {
219
+ for (const item of conditionOrField.items) {
220
+ this._model.where.items.push(item);
221
+ }
222
+ return this;
392
223
  }
224
+
225
+ this._model.where.items.push(conditionOrField);
393
226
  return this;
394
227
  }
395
228
 
@@ -397,9 +230,9 @@ export class SqlBuilder {
397
230
  * WHERE 原始片段(不做转义),可附带参数。
398
231
  */
399
232
  whereRaw(sql, params) {
400
- const paramList = Array.isArray(params) ? params : [];
401
- for (const param of paramList) {
402
- validateParam(param);
233
+ let paramList = [];
234
+ if (Array.isArray(params)) {
235
+ paramList = params;
403
236
  }
404
237
 
405
238
  this._model.where.items.push({ type: "raw", sql: sql, params: paramList });
@@ -416,43 +249,11 @@ export class SqlBuilder {
416
249
 
417
250
  /**
418
251
  * ORDER BY
419
- * @param fields - 格式为 ["field#ASC", "field2#DESC"]
252
+ * @param fields - 格式为 [{ field: "field", dir: "ASC" }]
420
253
  */
421
254
  orderBy(fields) {
422
255
  for (const item of fields) {
423
- if (!isString(item) || !item.includes("#")) {
424
- throw new Error(`orderBy 字段必须是 "字段#方向" 格式的字符串(例如:"name#ASC", "id#DESC") (item: ${String(item)})`, {
425
- cause: null,
426
- code: "validation"
427
- });
428
- }
429
-
430
- const parts = item.split("#");
431
- if (parts.length !== 2) {
432
- throw new Error(`orderBy 字段必须是 "字段#方向" 格式的字符串(例如:"name#ASC", "id#DESC") (item: ${String(item)})`, {
433
- cause: null,
434
- code: "validation"
435
- });
436
- }
437
-
438
- const cleanField = parts[0].trim();
439
- const cleanDir = parts[1].trim().toUpperCase();
440
-
441
- if (!cleanField) {
442
- throw new Error(`orderBy 中字段名不能为空 (item: ${item})`, {
443
- cause: null,
444
- code: "validation"
445
- });
446
- }
447
-
448
- if (!["ASC", "DESC"].includes(cleanDir)) {
449
- throw new Error(`ORDER BY 方向必须是 ASC 或 DESC (direction: ${cleanDir})`, {
450
- cause: null,
451
- code: "validation"
452
- });
453
- }
454
-
455
- this._model.orderBy.push({ field: cleanField, dir: cleanDir });
256
+ this._model.orderBy.push({ field: item.field, dir: item.dir });
456
257
  }
457
258
  return this;
458
259
  }
@@ -461,9 +262,8 @@ export class SqlBuilder {
461
262
  * LIMIT
462
263
  */
463
264
  limit(count, offset) {
464
- const result = normalizeLimitValue(count, offset);
465
- this._model.limit = result.limitValue;
466
- this._model.offset = result.offsetValue;
265
+ this._model.limit = count;
266
+ this._model.offset = offset;
467
267
  return this;
468
268
  }
469
269
 
@@ -471,8 +271,7 @@ export class SqlBuilder {
471
271
  * OFFSET
472
272
  */
473
273
  offset(count) {
474
- const offsetValue = normalizeOffsetValue(count);
475
- this._model.offset = offsetValue;
274
+ this._model.offset = count;
476
275
  return this;
477
276
  }
478
277
 
@@ -480,20 +279,23 @@ export class SqlBuilder {
480
279
  * 构建 SELECT 查询
481
280
  */
482
281
  toSelectSql() {
483
- const selectSql =
484
- this._model.select.length > 0
485
- ? this._model.select
486
- .map((item) => {
487
- if (item.type === "raw") {
488
- return item.value;
489
- }
490
- return escapeField(item.value, this._quoteIdent);
491
- })
492
- .join(", ")
493
- : "*";
282
+ let selectSql = "*";
283
+ if (this._model.select.length > 0) {
284
+ selectSql = this._model.select
285
+ .map((item) => {
286
+ if (item.type === "raw") {
287
+ return item.value;
288
+ }
289
+ return escapeField(item.value, this._quoteIdent);
290
+ })
291
+ .join(", ");
292
+ }
494
293
 
495
294
  const params = [];
496
- const fromSql = this._model.from.type === "raw" ? this._model.from.value : escapeTable(this._model.from.value, this._quoteIdent);
295
+ let fromSql = this._model.from.value;
296
+ if (this._model.from.type !== "raw") {
297
+ fromSql = escapeTable(this._model.from.value, this._quoteIdent);
298
+ }
497
299
  let sql = `SELECT ${selectSql} FROM ${fromSql}`;
498
300
 
499
301
  if (this._model.joins.length > 0) {
@@ -514,7 +316,7 @@ export class SqlBuilder {
514
316
 
515
317
  if (this._model.limit !== null) {
516
318
  sql += ` LIMIT ${this._model.limit}`;
517
- if (this._model.offset !== null) {
319
+ if (this._model.offset !== null && this._model.offset !== undefined) {
518
320
  sql += ` OFFSET ${this._model.offset}`;
519
321
  }
520
322
  }
@@ -529,7 +331,12 @@ export class SqlBuilder {
529
331
  const escapedTable = escapeTable(table, this._quoteIdent);
530
332
 
531
333
  if (Array.isArray(data)) {
532
- const fields = assertBatchInsertRowsConsistent(data, { table: table });
334
+ if (data.length === 0) {
335
+ return { sql: "", params: [] };
336
+ }
337
+
338
+ const firstRow = data[0] || {};
339
+ const fields = Object.keys(firstRow);
533
340
  const escapedFields = fields.map((field) => escapeField(field, this._quoteIdent));
534
341
  const placeholders = fields.map(() => "?").join(", ");
535
342
  const values = data.map(() => `(${placeholders})`).join(", ");
@@ -537,9 +344,7 @@ export class SqlBuilder {
537
344
 
538
345
  for (const row of data) {
539
346
  for (const field of fields) {
540
- const value = row[field];
541
- validateParam(value);
542
- params.push(value);
347
+ params.push(row[field]);
543
348
  }
544
349
  }
545
350
 
@@ -550,16 +355,6 @@ export class SqlBuilder {
550
355
  }
551
356
 
552
357
  const fields = Object.keys(data);
553
- if (fields.length === 0) {
554
- throw new Error(`插入数据必须至少有一个字段 (table: ${table})`, {
555
- cause: null,
556
- code: "validation"
557
- });
558
- }
559
-
560
- for (const field of fields) {
561
- validateParam(data[field]);
562
- }
563
358
 
564
359
  const escapedFields = fields.map((field) => escapeField(field, this._quoteIdent));
565
360
  const placeholders = fields.map(() => "?").join(", ");
@@ -579,12 +374,6 @@ export class SqlBuilder {
579
374
  */
580
375
  toUpdateSql(table, data) {
581
376
  const fields = Object.keys(data);
582
- if (fields.length === 0) {
583
- throw new Error("更新数据必须至少有一个字段", {
584
- cause: null,
585
- code: "validation"
586
- });
587
- }
588
377
 
589
378
  const params = [];
590
379
  for (const value of Object.values(data)) {
@@ -618,7 +407,10 @@ export class SqlBuilder {
618
407
  */
619
408
  toCountSql() {
620
409
  const params = [];
621
- const fromSql = this._model.from.type === "raw" ? this._model.from.value : escapeTable(this._model.from.value, this._quoteIdent);
410
+ let fromSql = this._model.from.value;
411
+ if (this._model.from.type !== "raw") {
412
+ fromSql = escapeTable(this._model.from.value, this._quoteIdent);
413
+ }
622
414
  let sql = `SELECT COUNT(*) as total FROM ${fromSql}`;
623
415
 
624
416
  if (this._model.joins.length > 0) {
@@ -676,7 +468,6 @@ export class SqlBuilder {
676
468
  whenList.push("WHEN ? THEN ?");
677
469
  args.push(row.id);
678
470
  const value = row.data[field];
679
- assertNoUndefinedParam(value, "SQL 参数值");
680
471
  args.push(value);
681
472
  }
682
473
 
@@ -705,10 +496,3 @@ export class SqlBuilder {
705
496
  return { sql: sql, params: args };
706
497
  }
707
498
  }
708
-
709
- /**
710
- * 创建新的 SQL 构建器实例
711
- */
712
- export function createQueryBuilder() {
713
- return new SqlBuilder();
714
- }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.21.2",
4
- "gitHead": "d134ac529ad22517624353dfcfcc1b598f009a49",
3
+ "version": "3.22.1",
4
+ "gitHead": "fdeb630232962a7f6d023d368f90fd9a91ea20a0",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",
7
7
  "keywords": [
package/paths.js CHANGED
@@ -112,9 +112,9 @@ export const appApiDir = join(appDir, "apis");
112
112
  export const appTableDir = join(appDir, "tables");
113
113
 
114
114
  /**
115
- * 项目公共静态目录
115
+ * 项目上传/公共文件目录
116
116
  * @description 默认 {appDir}/public,可通过 config.publicDir 覆盖
117
- * @usage 用于静态文件访问与本地上传保存目录解析
117
+ * @usage 用于本地上传文件保存目录解析,不承担 HTTP 静态托管职责
118
118
  */
119
119
  export function getAppPublicDir(publicDir = "./public") {
120
120
  if (isAbsolute(publicDir)) {
package/router/static.js CHANGED
@@ -20,7 +20,8 @@ export function staticHandler(corsConfig = undefined, publicDir = "./public") {
20
20
  const corsHeaders = setCorsOptions(req, corsConfig);
21
21
 
22
22
  const url = new URL(req.url);
23
- const filePath = join(getAppPublicDir(publicDir), url.pathname);
23
+ const publicPath = url.pathname.replace(/^\/public/, "") || "/";
24
+ const filePath = join(getAppPublicDir(publicDir), publicPath);
24
25
 
25
26
  try {
26
27
  // OPTIONS预检请求
package/sql/befly.sql CHANGED
@@ -79,6 +79,26 @@ CREATE TABLE IF NOT EXISTS `befly_email_log` (
79
79
  PRIMARY KEY (`id`)
80
80
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
81
81
 
82
+ CREATE TABLE IF NOT EXISTS `befly_file` (
83
+ `id` BIGINT NOT NULL,
84
+ `user_id` BIGINT NOT NULL DEFAULT 0,
85
+ `file_path` VARCHAR(500) NOT NULL DEFAULT '',
86
+ `url` VARCHAR(1000) NOT NULL DEFAULT '',
87
+ `is_image` TINYINT NOT NULL DEFAULT 0,
88
+ `file_type` VARCHAR(20) NOT NULL DEFAULT '',
89
+ `file_size` BIGINT NOT NULL DEFAULT 0,
90
+ `file_key` VARCHAR(100) NOT NULL DEFAULT '',
91
+ `file_ext` VARCHAR(20) NOT NULL DEFAULT '',
92
+ `file_name` VARCHAR(200) NOT NULL DEFAULT '',
93
+ `state` TINYINT NOT NULL DEFAULT 1,
94
+ `created_at` BIGINT NOT NULL DEFAULT 0,
95
+ `updated_at` BIGINT NOT NULL DEFAULT 0,
96
+ `deleted_at` BIGINT NULL DEFAULT NULL,
97
+ PRIMARY KEY (`id`),
98
+ KEY `idx_befly_file_user_id` (`user_id`),
99
+ KEY `idx_befly_file_is_image` (`is_image`)
100
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
101
+
82
102
  CREATE TABLE IF NOT EXISTS `befly_login_log` (
83
103
  `id` BIGINT NOT NULL,
84
104
  `admin_id` BIGINT NOT NULL DEFAULT 0,