befly 3.20.10 → 3.21.0

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.20.10",
4
- "gitHead": "6cffd9804b01481592d2a4e0eea31db3dbe30854",
3
+ "version": "3.21.0",
4
+ "gitHead": "bdc30cd18f3fe6a692d170ae2146ca9ba4e62edc",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",
7
7
  "keywords": [
@@ -52,8 +52,8 @@
52
52
  "test": "bun test"
53
53
  },
54
54
  "dependencies": {
55
- "fast-xml-parser": "^5.5.8",
56
- "nodemailer": "^8.0.2",
55
+ "fast-xml-parser": "^5.5.9",
56
+ "nodemailer": "^8.0.4",
57
57
  "pathe": "^2.0.3",
58
58
  "ua-parser-js": "^2.0.9",
59
59
  "zod": "^4.0.0"
package/plugins/mysql.js CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { Connect } from "../lib/connect.js";
7
- import { DbHelper } from "../lib/dbHelper/index.js";
7
+ import { DbHelper } from "../lib/dbHelper.js";
8
8
  import { Logger } from "../lib/logger.js";
9
9
 
10
10
  /**
@@ -2,7 +2,7 @@ import { mkdir } from "node:fs/promises";
2
2
  import { basename, resolve, join } from "node:path";
3
3
 
4
4
  import { Connect } from "../../lib/connect.js";
5
- import { DbHelper } from "../../lib/dbHelper/index.js";
5
+ import { DbHelper } from "../../lib/dbHelper.js";
6
6
  import { Logger } from "../../lib/logger.js";
7
7
  import { importDefault } from "../../utils/importDefault.js";
8
8
  import { isNonEmptyString, isPlainObject } from "../../utils/is.js";
@@ -1,658 +0,0 @@
1
- import { fieldClear } from "../../utils/fieldClear.js";
2
- import { isFiniteNumber, isNonEmptyString, isNullable, isPlainObject, isString } from "../../utils/is.js";
3
- import { arrayKeysToCamel, keysToCamel, keysToSnake, snakeCase } from "../../utils/util.js";
4
- import { SqlBuilder } from "../sqlBuilder/index.js";
5
- import { getJoinMainQualifier, normalizeBigIntValues, normalizeTableRef, quoteIdentMySql } from "./util.js";
6
- import { assertNoExprField, validateAndClassifyFields, validateExcludeFieldsResult, validateQueryOptions, validateTimeIdValue } from "./validate.js";
7
-
8
- function shouldExcludeFieldValue(key, value, excludeValueMap) {
9
- if (!isPlainObject(excludeValueMap)) {
10
- return false;
11
- }
12
-
13
- const excludeValues = excludeValueMap[key];
14
- if (!Array.isArray(excludeValues) || excludeValues.length === 0) {
15
- return false;
16
- }
17
-
18
- for (const excludeValue of excludeValues) {
19
- if (Object.is(value, excludeValue)) {
20
- return true;
21
- }
22
- }
23
-
24
- return false;
25
- }
26
-
27
- function mergeExcludeValueMap(parentMap, currentMap) {
28
- if (!isPlainObject(parentMap) && !isPlainObject(currentMap)) {
29
- return null;
30
- }
31
-
32
- const result = {};
33
- if (isPlainObject(parentMap)) {
34
- for (const [key, value] of Object.entries(parentMap)) {
35
- result[key] = value;
36
- }
37
- }
38
- if (isPlainObject(currentMap)) {
39
- for (const [key, value] of Object.entries(currentMap)) {
40
- result[key] = value;
41
- }
42
- }
43
-
44
- return result;
45
- }
46
-
47
- export function clearDeep(value, options) {
48
- const arrayObjectKeys = Array.isArray(options?.arrayObjectKeys) ? options.arrayObjectKeys : ["$or", "$and"];
49
- const arrayObjectKeySet = new Set();
50
- for (const key of arrayObjectKeys) {
51
- arrayObjectKeySet.add(key);
52
- }
53
-
54
- const depthRaw = isFiniteNumber(options?.depth) ? Math.floor(options.depth) : 0;
55
- const depth = depthRaw < 0 ? 0 : depthRaw;
56
-
57
- const clearInternal = (input, remainingDepth, inheritedExcludeValueMap) => {
58
- if (!input || typeof input !== "object") {
59
- return input;
60
- }
61
-
62
- if (Array.isArray(input)) {
63
- return input;
64
- }
65
-
66
- const canRecurse = remainingDepth === 0 || remainingDepth > 1;
67
- const childDepth = remainingDepth === 0 ? 0 : remainingDepth - 1;
68
- const result = {};
69
- const currentExcludeValueMap = mergeExcludeValueMap(inheritedExcludeValueMap, input.$exclude);
70
-
71
- for (const [key, item] of Object.entries(input)) {
72
- if (key === "$exclude") {
73
- continue;
74
- }
75
-
76
- if (isNullable(item)) {
77
- continue;
78
- }
79
-
80
- if (shouldExcludeFieldValue(key, item, currentExcludeValueMap)) {
81
- continue;
82
- }
83
-
84
- if (arrayObjectKeySet.has(key)) {
85
- if (!Array.isArray(item)) {
86
- continue;
87
- }
88
-
89
- const outList = [];
90
- for (const child of item) {
91
- if (!child || typeof child !== "object" || Array.isArray(child)) {
92
- continue;
93
- }
94
- const cleaned = clearInternal(child, remainingDepth, currentExcludeValueMap);
95
- if (!cleaned || typeof cleaned !== "object" || Array.isArray(cleaned)) {
96
- continue;
97
- }
98
- if (Object.keys(cleaned).length === 0) {
99
- continue;
100
- }
101
- outList.push(cleaned);
102
- }
103
-
104
- if (outList.length > 0) {
105
- result[key] = outList;
106
- }
107
- continue;
108
- }
109
-
110
- if (typeof item === "object" && !Array.isArray(item)) {
111
- if (!canRecurse) {
112
- result[key] = item;
113
- continue;
114
- }
115
-
116
- const cleanedObj = clearInternal(item, childDepth, currentExcludeValueMap);
117
- if (!cleanedObj || typeof cleanedObj !== "object" || Array.isArray(cleanedObj)) {
118
- continue;
119
- }
120
- if (Object.keys(cleanedObj).length === 0) {
121
- continue;
122
- }
123
- result[key] = cleanedObj;
124
- continue;
125
- }
126
-
127
- result[key] = item;
128
- }
129
-
130
- return result;
131
- };
132
-
133
- return clearInternal(value, depth, options?.excludeValueMap);
134
- }
135
-
136
- export async function fieldsToSnake(table, fields, getTableColumns) {
137
- if (!fields || !Array.isArray(fields)) {
138
- return ["*"];
139
- }
140
-
141
- const classified = validateAndClassifyFields(fields);
142
- if (classified.type === "all") {
143
- return ["*"];
144
- }
145
- if (classified.type === "include") {
146
- return classified.fields.map((field) => processJoinField(field));
147
- }
148
- if (classified.type === "exclude") {
149
- const allColumns = await getTableColumns(table);
150
- const excludeSnakeFields = classified.fields.map((f) => snakeCase(f));
151
- const resultFields = allColumns.filter((col) => !excludeSnakeFields.includes(col));
152
- validateExcludeFieldsResult(resultFields, table, excludeSnakeFields);
153
- return resultFields;
154
- }
155
- return ["*"];
156
- }
157
-
158
- export function orderByToSnake(orderBy) {
159
- return normalizeOrderBy(orderBy, (field) => snakeCase(field));
160
- }
161
-
162
- function normalizeQualifierField(field) {
163
- const parts = field.split(".");
164
- if (parts.length === 1) {
165
- return snakeCase(field);
166
- }
167
-
168
- const fieldName = parts[parts.length - 1].trim();
169
- const qualifier = parts
170
- .slice(0, parts.length - 1)
171
- .map((item) => snakeCase(item.trim()))
172
- .join(".");
173
-
174
- if (fieldName === "*") {
175
- return `${qualifier}.*`;
176
- }
177
-
178
- return `${qualifier}.${snakeCase(fieldName)}`;
179
- }
180
-
181
- function normalizeOrderBy(orderBy, mapField) {
182
- if (!orderBy || !Array.isArray(orderBy)) {
183
- return orderBy;
184
- }
185
-
186
- return orderBy.map((item) => {
187
- if (!isString(item) || !item.includes("#")) {
188
- return item;
189
- }
190
-
191
- const parts = item.split("#");
192
- if (parts.length !== 2) {
193
- return item;
194
- }
195
-
196
- return `${mapField(parts[0].trim())}#${parts[1].trim()}`;
197
- });
198
- }
199
-
200
- function mapWhereKeys(where, mapKey) {
201
- if (!where || typeof where !== "object") {
202
- return where;
203
- }
204
- if (Array.isArray(where)) {
205
- return where.map((item) => mapWhereKeys(item, mapKey));
206
- }
207
-
208
- const result = {};
209
- for (const [key, value] of Object.entries(where)) {
210
- if (key === "$or" || key === "$and") {
211
- result[key] = Array.isArray(value) ? value.map((item) => mapWhereKeys(item, mapKey)) : [];
212
- continue;
213
- }
214
-
215
- const normalizedKey = mapKey(key);
216
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
217
- result[normalizedKey] = mapWhereKeys(value, mapKey);
218
- continue;
219
- }
220
-
221
- result[normalizedKey] = value;
222
- }
223
-
224
- return result;
225
- }
226
-
227
- export function processJoinField(field) {
228
- assertNoExprField(field);
229
- if (field === "*" || field.startsWith("`")) {
230
- return field;
231
- }
232
-
233
- if (field.toUpperCase().includes(" AS ")) {
234
- const parts = field.split(/\s+AS\s+/i);
235
- const fieldPart = parts[0];
236
- const aliasPart = parts[1];
237
- if (!isString(fieldPart) || !isString(aliasPart)) {
238
- return field;
239
- }
240
- return `${processJoinField(fieldPart.trim())} AS ${aliasPart.trim()}`;
241
- }
242
-
243
- return normalizeQualifierField(field);
244
- }
245
-
246
- function processJoinWhereKey(key) {
247
- if (key === "$or" || key === "$and") {
248
- return key;
249
- }
250
-
251
- assertNoExprField(key);
252
- if (key.includes("$")) {
253
- const lastDollarIndex = key.lastIndexOf("$");
254
- const fieldPart = key.substring(0, lastDollarIndex);
255
- const operator = key.substring(lastDollarIndex);
256
-
257
- return `${normalizeQualifierField(fieldPart)}${operator}`;
258
- }
259
-
260
- return normalizeQualifierField(key);
261
- }
262
-
263
- export function processJoinWhere(where) {
264
- return mapWhereKeys(where, processJoinWhereKey);
265
- }
266
-
267
- export function processJoinOrderBy(orderBy) {
268
- return normalizeOrderBy(orderBy, processJoinField);
269
- }
270
-
271
- export function parseLeftJoinItem(joinTable, joinItem) {
272
- const parts = joinItem.split(" ");
273
- return {
274
- type: "left",
275
- table: normalizeTableRef(joinTable),
276
- on: processJoinOn(`${parts[0]} = ${parts[1]}`)
277
- };
278
- }
279
-
280
- export function processJoinOn(on) {
281
- if (!isString(on)) {
282
- return String(on);
283
- }
284
-
285
- const raw = String(on);
286
- if (!isNonEmptyString(raw)) {
287
- return raw;
288
- }
289
-
290
- const replaceSegment = (segment) => {
291
- return segment.replace(/([a-zA-Z_][a-zA-Z0-9_]*)(\s*\.\s*)([a-zA-Z_][a-zA-Z0-9_]*)/g, (_match, left, dot, right) => {
292
- return `${snakeCase(left)}${dot}${snakeCase(right)}`;
293
- });
294
- };
295
-
296
- let result = "";
297
- let buffer = "";
298
- let quote = null;
299
- let prev = "";
300
-
301
- for (const ch of raw) {
302
- if (quote) {
303
- result += ch;
304
- if (ch === quote && prev !== "\\") {
305
- quote = null;
306
- }
307
- prev = ch;
308
- continue;
309
- }
310
-
311
- if (ch === "'" || ch === '"' || ch === "`") {
312
- if (buffer.length > 0) {
313
- result += replaceSegment(buffer);
314
- buffer = "";
315
- }
316
- quote = ch;
317
- result += ch;
318
- prev = ch;
319
- continue;
320
- }
321
-
322
- buffer += ch;
323
- prev = ch;
324
- }
325
-
326
- if (buffer.length > 0) {
327
- result += replaceSegment(buffer);
328
- }
329
-
330
- return result;
331
- }
332
-
333
- export function addDefaultStateFilter(where = {}, table, hasLeftJoin = false, beflyMode = "auto") {
334
- if (beflyMode === "manual") {
335
- return where;
336
- }
337
-
338
- const hasStateCondition = Object.keys(where).some((key) => key.startsWith("state") || key.includes(".state"));
339
- if (hasStateCondition) {
340
- return where;
341
- }
342
-
343
- if (hasLeftJoin && table) {
344
- const result = {};
345
- for (const [key, value] of Object.entries(where)) {
346
- result[key] = value;
347
- }
348
- result[`${table}.state$gt`] = 0;
349
- return result;
350
- }
351
-
352
- const result = {};
353
- for (const [key, value] of Object.entries(where)) {
354
- result[key] = value;
355
- }
356
- result.state$gt = 0;
357
- return result;
358
- }
359
-
360
- export function whereKeysToSnake(where) {
361
- return mapWhereKeys(where, (key) => {
362
- assertNoExprField(key);
363
- if (key.includes("$")) {
364
- const lastDollarIndex = key.lastIndexOf("$");
365
- const fieldName = key.substring(0, lastDollarIndex);
366
- const operator = key.substring(lastDollarIndex);
367
- return `${snakeCase(fieldName)}${operator}`;
368
- }
369
-
370
- return snakeCase(key);
371
- });
372
- }
373
-
374
- function serializeArrayFields(data) {
375
- const serialized = {};
376
- for (const [key, value] of Object.entries(data)) {
377
- if (isNullable(value)) {
378
- serialized[key] = value;
379
- continue;
380
- }
381
- if (Array.isArray(value)) {
382
- serialized[key] = JSON.stringify(value);
383
- continue;
384
- }
385
- serialized[key] = value;
386
- }
387
- return serialized;
388
- }
389
-
390
- export function deserializeArrayFields(data) {
391
- if (!data) {
392
- return null;
393
- }
394
- const deserialized = {};
395
- for (const [key, value] of Object.entries(data)) {
396
- deserialized[key] = value;
397
- }
398
-
399
- for (const [key, value] of Object.entries(deserialized)) {
400
- if (!isString(value)) {
401
- continue;
402
- }
403
- if (value.startsWith("[") && value.endsWith("]")) {
404
- try {
405
- const parsed = JSON.parse(value);
406
- if (Array.isArray(parsed)) {
407
- deserialized[key] = parsed;
408
- }
409
- } catch {
410
- continue;
411
- }
412
- }
413
- }
414
-
415
- return deserialized;
416
- }
417
-
418
- function cleanAndSnakeAndSerializeWriteData(data, excludeValues = [null, undefined]) {
419
- const cleanData = fieldClear(data, { excludeValues: excludeValues });
420
- const snakeData = keysToSnake(cleanData);
421
- return serializeArrayFields(snakeData);
422
- }
423
-
424
- function cloneRecord(record) {
425
- const result = {};
426
- for (const [key, value] of Object.entries(record)) {
427
- result[key] = value;
428
- }
429
- return result;
430
- }
431
-
432
- function prepareWriteUserData(data, allowState) {
433
- const serializedData = cleanAndSnakeAndSerializeWriteData(data);
434
- return stripSystemFieldsForWrite(serializedData, { allowState: allowState });
435
- }
436
-
437
- function stripSystemFieldsForWrite(data, options) {
438
- const result = {};
439
- for (const [key, value] of Object.entries(data)) {
440
- if (key === "id") continue;
441
- if (key === "created_at") continue;
442
- if (key === "updated_at") continue;
443
- if (key === "deleted_at") continue;
444
- if (!options.allowState && key === "state") continue;
445
- result[key] = value;
446
- }
447
- return result;
448
- }
449
-
450
- export function buildInsertRow(options) {
451
- const result = cloneRecord(prepareWriteUserData(options.data, false));
452
-
453
- if (options.beflyMode === "auto") {
454
- validateTimeIdValue(options.id);
455
- result["id"] = options.id;
456
- }
457
-
458
- if (options.beflyMode !== "manual") {
459
- result["created_at"] = options.now;
460
- result["updated_at"] = options.now;
461
- result["state"] = 1;
462
- }
463
- return result;
464
- }
465
-
466
- export function buildUpdateRow(options) {
467
- const result = cloneRecord(prepareWriteUserData(options.data, options.allowState));
468
- if (options.beflyMode !== "manual") {
469
- result["updated_at"] = options.now;
470
- }
471
- return result;
472
- }
473
-
474
- export function buildPartialUpdateData(options) {
475
- return prepareWriteUserData(options.data, options.allowState);
476
- }
477
-
478
- export const builderMethods = {
479
- createSqlBuilder() {
480
- return new SqlBuilder({ quoteIdent: quoteIdentMySql });
481
- },
482
-
483
- createListBuilder(prepared, whereFiltered, limit, offset) {
484
- const builder = this.createSqlBuilder().select(prepared.fields).from(prepared.table).where(whereFiltered).limit(limit);
485
- this.applyLeftJoins(builder, prepared.leftJoins);
486
- if (!isNullable(offset)) {
487
- builder.offset(offset);
488
- }
489
- if (prepared.orderBy && prepared.orderBy.length > 0) {
490
- builder.orderBy(prepared.orderBy);
491
- }
492
- return builder;
493
- },
494
-
495
- async fetchCount(prepared, whereFiltered, alias) {
496
- const builder = this.createSqlBuilder().selectRaw(alias).from(prepared.table).where(whereFiltered);
497
- this.applyLeftJoins(builder, prepared.leftJoins);
498
- const result = builder.toSelectSql();
499
- const executeRes = await this.execute(result.sql, result.params);
500
- const countRow = executeRes.data?.[0] || null;
501
- const normalizedCountRow = countRow ? normalizeBigIntValues(countRow) : null;
502
- const total = normalizedCountRow?.total ?? normalizedCountRow?.count ?? 0;
503
- return {
504
- total: total,
505
- sql: executeRes.sql
506
- };
507
- },
508
-
509
- normalizeRowData(row) {
510
- if (!row) {
511
- return {};
512
- }
513
-
514
- const camelRow = keysToCamel(row);
515
- const deserialized = deserializeArrayFields(camelRow);
516
- if (!deserialized) {
517
- return {};
518
- }
519
-
520
- const convertedList = normalizeBigIntValues([deserialized]);
521
- if (convertedList && convertedList.length > 0) {
522
- return convertedList[0];
523
- }
524
-
525
- return deserialized;
526
- },
527
-
528
- normalizeListData(list) {
529
- const camelList = arrayKeysToCamel(list);
530
- const deserializedList = camelList.map((item) => deserializeArrayFields(item)).filter((item) => item !== null);
531
- return normalizeBigIntValues(deserializedList);
532
- },
533
-
534
- normalizeLeftJoinOptions(options, cleanWhere) {
535
- const processedFields = (options.fields || []).map((field) => processJoinField(field));
536
- const mainTableRef = options.table[0];
537
- const joinTableRefs = options.table.slice(1);
538
- const normalizedTableRef = normalizeTableRef(mainTableRef);
539
- const mainQualifier = getJoinMainQualifier(mainTableRef);
540
- const leftJoins = options.leftJoin.map((joinItem, index) => parseLeftJoinItem(joinTableRefs[index], joinItem));
541
-
542
- return {
543
- table: normalizedTableRef,
544
- tableQualifier: mainQualifier,
545
- fields: processedFields.length > 0 ? processedFields : ["*"],
546
- where: processJoinWhere(cleanWhere),
547
- leftJoins: leftJoins,
548
- orderBy: processJoinOrderBy(options.orderBy || []),
549
- page: options.page || 1,
550
- limit: options.limit || 10
551
- };
552
- },
553
-
554
- async normalizeSingleTableOptions(options, cleanWhere) {
555
- const tableRef = Array.isArray(options.table) ? options.table[0] : options.table;
556
- const snakeTable = snakeCase(tableRef);
557
- const processedFields = await fieldsToSnake(snakeTable, options.fields || [], this.getTableColumns.bind(this));
558
-
559
- return {
560
- table: snakeTable,
561
- tableQualifier: snakeTable,
562
- fields: processedFields,
563
- where: whereKeysToSnake(cleanWhere),
564
- leftJoins: [],
565
- orderBy: orderByToSnake(options.orderBy || []),
566
- page: options.page || 1,
567
- limit: options.limit || 10
568
- };
569
- },
570
-
571
- buildQueryOptions(options, defaults) {
572
- const output = {
573
- table: options.table,
574
- page: defaults.page,
575
- limit: defaults.limit
576
- };
577
-
578
- if (options.fields !== undefined) output.fields = options.fields;
579
- if (options.where !== undefined) output.where = options.where;
580
- if (options.leftJoin !== undefined) output.leftJoin = options.leftJoin;
581
- if (options.orderBy !== undefined) output.orderBy = options.orderBy;
582
- return output;
583
- },
584
-
585
- resolveFieldValue(record, field) {
586
- if (!isPlainObject(record)) {
587
- return null;
588
- }
589
-
590
- if (Object.hasOwn(record, field)) {
591
- return record[field];
592
- }
593
-
594
- const camelField = field.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
595
- if (camelField !== field && Object.hasOwn(record, camelField)) {
596
- return record[camelField];
597
- }
598
-
599
- const snakeField = field.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
600
- if (snakeField !== field && Object.hasOwn(record, snakeField)) {
601
- return record[snakeField];
602
- }
603
-
604
- return null;
605
- },
606
-
607
- async getTableColumns(table) {
608
- const quotedTable = quoteIdentMySql(table);
609
- const executeRes = await this.execute(`SHOW COLUMNS FROM ${quotedTable}`, []);
610
- const result = executeRes.data;
611
-
612
- if (!result || result.length === 0) {
613
- throw new Error(`表 ${table} 不存在或没有字段`, {
614
- cause: null,
615
- code: "runtime"
616
- });
617
- }
618
-
619
- const columnNames = [];
620
- for (const row of result) {
621
- const record = row;
622
- const name = record["Field"];
623
- if (isNonEmptyString(name)) {
624
- columnNames.push(name);
625
- }
626
- }
627
-
628
- return columnNames;
629
- },
630
-
631
- async prepareQueryOptions(options, label = "queryOptions") {
632
- validateQueryOptions(options, label);
633
- const cleanWhere = clearDeep(options.where || {});
634
- const hasLeftJoin = Array.isArray(options.leftJoin) && options.leftJoin.length > 0;
635
-
636
- if (hasLeftJoin) {
637
- return this.normalizeLeftJoinOptions(options, cleanWhere);
638
- }
639
-
640
- return await this.normalizeSingleTableOptions(options, cleanWhere);
641
- },
642
-
643
- async prepareReadContext(options, label = "queryOptions") {
644
- const prepared = await this.prepareQueryOptions(options, label);
645
- return {
646
- prepared: prepared,
647
- whereFiltered: addDefaultStateFilter(prepared.where, prepared.tableQualifier, prepared.leftJoins.length > 0, this.beflyMode)
648
- };
649
- },
650
-
651
- applyLeftJoins(builder, leftJoins) {
652
- if (!leftJoins || leftJoins.length === 0) return;
653
-
654
- for (const join of leftJoins) {
655
- builder.leftJoin(join.table, join.on);
656
- }
657
- }
658
- };