befly 3.24.19 → 3.25.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/apis/_apis.js +20 -0
- package/apis/admin/delete.js +3 -1
- package/apis/admin/detail.js +1 -3
- package/apis/admin/select.js +9 -7
- package/apis/admin/update.js +2 -2
- package/apis/api/all.js +6 -11
- package/apis/api/select.js +18 -19
- package/apis/dict/_dict.js +24 -0
- package/apis/dict/all.js +6 -4
- package/apis/dict/detail.js +9 -5
- package/apis/dict/insert.js +4 -11
- package/apis/dict/items.js +5 -6
- package/apis/dict/select.js +13 -9
- package/apis/dict/update.js +9 -13
- package/apis/dictType/select.js +4 -4
- package/apis/email/config.js +9 -11
- package/apis/email/logList.js +14 -4
- package/apis/email/send.js +10 -3
- package/apis/email/verify.js +9 -7
- package/apis/loginLog/select.js +11 -4
- package/apis/menu/all.js +13 -19
- package/apis/menu/select.js +19 -14
- package/apis/operateLog/select.js +13 -4
- package/apis/role/_role.js +21 -0
- package/apis/role/all.js +1 -3
- package/apis/role/apiSave.js +8 -15
- package/apis/role/apis.js +4 -10
- package/apis/role/delete.js +28 -36
- package/apis/role/detail.js +4 -10
- package/apis/role/insert.js +12 -10
- package/apis/role/menuSave.js +9 -15
- package/apis/role/menus.js +4 -10
- package/apis/role/save.js +19 -23
- package/apis/role/select.js +12 -9
- package/apis/role/update.js +14 -15
- package/apis/source/imageList.js +3 -3
- package/apis/sysConfig/get.js +11 -23
- package/apis/sysConfig/insert.js +22 -27
- package/apis/sysConfig/select.js +11 -5
- package/apis/sysConfig/update.js +33 -38
- package/apis/tongJi/_tongJi.js +41 -0
- package/apis/tongJi/cacheHealth.js +8 -30
- package/apis/tongJi/errorList.js +26 -27
- package/apis/tongJi/errorReport.js +26 -43
- package/apis/tongJi/errorStats.js +17 -48
- package/apis/tongJi/fallbackReset.js +7 -15
- package/apis/tongJi/infoReport.js +20 -32
- package/apis/tongJi/infoStats.js +5 -17
- package/apis/tongJi/onlineReport.js +50 -56
- package/apis/tongJi/onlineStats.js +97 -111
- package/checks/config.js +44 -1
- package/configs/beflyConfig.json +10 -1
- package/index.js +25 -0
- package/lib/dbHelper.js +1 -1
- package/lib/dbParse.js +61 -99
- package/lib/dbUtil.js +101 -21
- package/lib/redisHelper.js +25 -0
- package/lib/sqlBuilder.js +6 -0
- package/package.json +1 -1
- package/plugins/email.js +3 -6
- package/router/api.js +0 -7
- package/utils/email.js +3 -0
- package/apis/admin/cacheRefresh.js +0 -122
- package/apis/dashboard/configStatus.js +0 -39
- package/apis/dashboard/performanceMetrics.js +0 -23
- package/apis/dashboard/permissionStats.js +0 -27
- package/apis/dashboard/systemInfo.js +0 -19
- package/lib/requestMetrics.js +0 -203
package/checks/config.js
CHANGED
|
@@ -10,6 +10,47 @@ z.config(z.locales.zhCN());
|
|
|
10
10
|
const boolIntSchema = z.union([z.literal(0), z.literal(1), z.literal(true), z.literal(false)]);
|
|
11
11
|
const noTrimString = z.string().refine(isNoTrimStringAllowEmpty, "不允许首尾空格");
|
|
12
12
|
const beflyModeSchema = z.union([z.literal("manual"), z.literal("auto")]);
|
|
13
|
+
const projectListCodeSchema = z.string().regex(/^[a-z][a-zA-Z0-9]*$/, "必须是小驼峰命名");
|
|
14
|
+
const projectListItemSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
code: projectListCodeSchema,
|
|
17
|
+
name: noTrimString.min(1),
|
|
18
|
+
productName: noTrimString.min(1),
|
|
19
|
+
productCode: noTrimString.min(1),
|
|
20
|
+
productVersion: noTrimString.optional()
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
const projectListsSchema = z.array(projectListItemSchema).superRefine((projectLists, ctx) => {
|
|
24
|
+
const codeSet = new Set();
|
|
25
|
+
const productCodeSet = new Set();
|
|
26
|
+
|
|
27
|
+
for (let index = 0; index < projectLists.length; index += 1) {
|
|
28
|
+
const code = projectLists[index].code;
|
|
29
|
+
const productCode = projectLists[index].productCode;
|
|
30
|
+
|
|
31
|
+
if (codeSet.has(code)) {
|
|
32
|
+
ctx.addIssue({
|
|
33
|
+
code: z.ZodIssueCode.custom,
|
|
34
|
+
message: `projectLists[${index}].code 重复`,
|
|
35
|
+
path: [index, "code"]
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
codeSet.add(code);
|
|
41
|
+
|
|
42
|
+
if (productCodeSet.has(productCode)) {
|
|
43
|
+
ctx.addIssue({
|
|
44
|
+
code: z.ZodIssueCode.custom,
|
|
45
|
+
message: `projectLists[${index}].productCode 重复`,
|
|
46
|
+
path: [index, "productCode"]
|
|
47
|
+
});
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
productCodeSet.add(productCode);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
13
54
|
|
|
14
55
|
const configSchema = z
|
|
15
56
|
.object({
|
|
@@ -98,7 +139,9 @@ const configSchema = z
|
|
|
98
139
|
skipRoutes: z.array(noTrimString),
|
|
99
140
|
rules: z.array(z.object({}).passthrough())
|
|
100
141
|
})
|
|
101
|
-
.strict()
|
|
142
|
+
.strict(),
|
|
143
|
+
|
|
144
|
+
projectLists: projectListsSchema
|
|
102
145
|
})
|
|
103
146
|
.strict();
|
|
104
147
|
|
package/configs/beflyConfig.json
CHANGED
package/index.js
CHANGED
|
@@ -55,6 +55,30 @@ function prefixMenuPaths(menus, prefix) {
|
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function mergeProjectListsByCode(projectLists) {
|
|
59
|
+
if (!Array.isArray(projectLists)) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const itemMap = new Map();
|
|
64
|
+
|
|
65
|
+
for (const item of projectLists) {
|
|
66
|
+
const code = String(item?.code || "");
|
|
67
|
+
|
|
68
|
+
if (!code) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (itemMap.has(code)) {
|
|
73
|
+
itemMap.delete(code);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
itemMap.set(code, item);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return Array.from(itemMap.values());
|
|
80
|
+
}
|
|
81
|
+
|
|
58
82
|
async function ensureSyncPrerequisites(ctx) {
|
|
59
83
|
const missingCtxKeys = ["ctx.redis", "ctx.mysql", "ctx.cache"].filter((key) => !ctx[key.slice(4)]);
|
|
60
84
|
if (missingCtxKeys.length > 0) {
|
|
@@ -90,6 +114,7 @@ async function ensureSyncPrerequisites(ctx) {
|
|
|
90
114
|
export async function createBefly(env = {}, config = {}, menus = []) {
|
|
91
115
|
const mergedConfig = deepMerge(beflyConfig, config);
|
|
92
116
|
const mergedMenus = deepMerge(prefixMenuPaths(beflyMenus, "core"), menus);
|
|
117
|
+
mergedConfig.projectLists = mergeProjectListsByCode(mergedConfig.projectLists);
|
|
93
118
|
|
|
94
119
|
const configHasError = await checkConfig(mergedConfig);
|
|
95
120
|
const { apis, tables, plugins, hooks } = await scanSources();
|
package/lib/dbHelper.js
CHANGED
|
@@ -436,7 +436,7 @@ class DbHelper {
|
|
|
436
436
|
}
|
|
437
437
|
|
|
438
438
|
prepareWriteInputData(data) {
|
|
439
|
-
return serializeArrayFields(keysToSnake(fieldClear(data, { excludeValues: [
|
|
439
|
+
return serializeArrayFields(keysToSnake(fieldClear(data, { excludeValues: [undefined] })));
|
|
440
440
|
}
|
|
441
441
|
|
|
442
442
|
prepareWriteUserData(data, allowState) {
|
package/lib/dbParse.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isNonEmptyString, isNullable, isPlainObject, isString } from "../utils/is.js";
|
|
2
2
|
import { snakeCase } from "../utils/util.js";
|
|
3
|
-
import { assertNoExprField, clearDeep, fieldsToSnake, normalizeQualifierField, orderByToSnake, parseFieldAliasParts, parseTableRef, processJoinField, processJoinOn, processJoinOrderBy, processJoinWhere, whereKeysToSnake } from "./dbUtil.js";
|
|
3
|
+
import { assertNoExprField, clearDeep, fieldsToSnake, normalizeQualifierField, orderByToSnake, parseFieldAliasParts, parseFlatWhereKey, parseTableRef, processJoinField, processJoinOn, processJoinOrderBy, processJoinWhere, whereKeysToSnake } from "./dbUtil.js";
|
|
4
4
|
|
|
5
5
|
function assertNonEmptyString(value, label) {
|
|
6
6
|
if (!isNonEmptyString(value)) {
|
|
@@ -38,48 +38,47 @@ function validateWhereObject(where, label, required = false) {
|
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
41
|
+
for (const [key, value] of Object.entries(where)) {
|
|
42
|
+
if (key === "$exclude") {
|
|
43
|
+
if (isNullable(value)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
if (!isPlainObject(value)) {
|
|
48
|
+
throw new Error(`${label}.$exclude 必须是对象`, {
|
|
49
|
+
cause: null,
|
|
50
|
+
code: "validation"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const [excludeKey, excludeValue] of Object.entries(value)) {
|
|
55
|
+
assertNonEmptyString(excludeKey, `${label}.$exclude 的字段名`);
|
|
56
|
+
if (!Array.isArray(excludeValue)) {
|
|
57
|
+
throw new Error(`${label}.$exclude.${excludeKey} 必须是数组`, {
|
|
50
58
|
cause: null,
|
|
51
59
|
code: "validation"
|
|
52
60
|
});
|
|
53
61
|
}
|
|
54
|
-
|
|
55
|
-
for (const [excludeKey, excludeValue] of Object.entries(value)) {
|
|
56
|
-
assertNonEmptyString(excludeKey, `${currentLabel}.$exclude 的字段名`);
|
|
57
|
-
if (!Array.isArray(excludeValue)) {
|
|
58
|
-
throw new Error(`${currentLabel}.$exclude.${excludeKey} 必须是数组`, {
|
|
59
|
-
cause: null,
|
|
60
|
-
code: "validation"
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if ((key === "$or" || key === "$and") && Array.isArray(value)) {
|
|
68
|
-
for (let i = 0; i < value.length; i++) {
|
|
69
|
-
if (isPlainObject(value[i])) {
|
|
70
|
-
visitWhereMeta(value[i], `${currentLabel}.${key}[${i}]`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
continue;
|
|
74
62
|
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
75
65
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
if (key === "$or" || key === "$and") {
|
|
67
|
+
throw new Error(`${label} 不再支持 ${key} 嵌套,请改用 field$op$or 扁平写法`, {
|
|
68
|
+
cause: null,
|
|
69
|
+
code: "validation"
|
|
70
|
+
});
|
|
79
71
|
}
|
|
80
|
-
};
|
|
81
72
|
|
|
82
|
-
|
|
73
|
+
parseFlatWhereKey(key);
|
|
74
|
+
|
|
75
|
+
if (isPlainObject(value)) {
|
|
76
|
+
throw new Error(`${label}.${key} 不再支持嵌套对象操作符,请改用 field$op 扁平写法`, {
|
|
77
|
+
cause: null,
|
|
78
|
+
code: "validation"
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
function validateOrderByItems(orderBy, label) {
|
|
@@ -563,6 +562,14 @@ function appendWhereNode(root, node) {
|
|
|
563
562
|
}
|
|
564
563
|
}
|
|
565
564
|
|
|
565
|
+
function normalizeOrGroupKey(orGroup) {
|
|
566
|
+
if (orGroup === "or") {
|
|
567
|
+
return "or1";
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return orGroup;
|
|
571
|
+
}
|
|
572
|
+
|
|
566
573
|
function buildArrayOperatorNode(fieldName, operator, value, errorFactory, emptyMessage) {
|
|
567
574
|
if (!Array.isArray(value)) {
|
|
568
575
|
throw new Error(errorFactory(operator), {
|
|
@@ -620,69 +627,33 @@ function buildOperatorNode(fieldName, operator, value) {
|
|
|
620
627
|
}
|
|
621
628
|
}
|
|
622
629
|
|
|
623
|
-
function appendFieldCondition(group, key, value) {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
appendWhereNode(group, buildOperatorNode(key.substring(0, lastDollarIndex), `$${key.substring(lastDollarIndex + 1)}`, value));
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
630
|
+
function appendFieldCondition(group, orGroups, key, value) {
|
|
631
|
+
const parsed = parseFlatWhereKey(key);
|
|
632
|
+
const node = buildOperatorNode(parsed.field, parsed.operator, value);
|
|
629
633
|
|
|
630
|
-
if (
|
|
631
|
-
for (const [operator, operatorValue] of Object.entries(value)) {
|
|
632
|
-
appendWhereNode(group, buildOperatorNode(key, operator, operatorValue));
|
|
633
|
-
}
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
appendWhereNode(group, buildOperatorNode(key, "=", value));
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
function appendAndConditions(group, value) {
|
|
641
|
-
if (!Array.isArray(value)) {
|
|
634
|
+
if (!node) {
|
|
642
635
|
return;
|
|
643
636
|
}
|
|
644
637
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
continue;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
const sub = parseWhereObject(condition);
|
|
651
|
-
if (sub.items.length === 0) {
|
|
652
|
-
continue;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
if (sub.join === "AND") {
|
|
656
|
-
for (const item of sub.items) {
|
|
657
|
-
group.items.push(item);
|
|
658
|
-
}
|
|
659
|
-
continue;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
group.items.push(sub);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function appendOrConditions(group, value) {
|
|
667
|
-
if (!Array.isArray(value)) {
|
|
638
|
+
if (!parsed.orGroup) {
|
|
639
|
+
appendWhereNode(group, node);
|
|
668
640
|
return;
|
|
669
641
|
}
|
|
670
642
|
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
if (!condition || typeof condition !== "object" || Array.isArray(condition)) {
|
|
674
|
-
continue;
|
|
675
|
-
}
|
|
643
|
+
const orGroupKey = normalizeOrGroupKey(parsed.orGroup);
|
|
644
|
+
let orGroup = orGroups.get(orGroupKey);
|
|
676
645
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
646
|
+
if (!orGroup) {
|
|
647
|
+
orGroup = createWhereRoot("OR");
|
|
648
|
+
orGroups.set(orGroupKey, orGroup);
|
|
649
|
+
appendWhereNode(group, orGroup);
|
|
681
650
|
}
|
|
682
651
|
|
|
683
|
-
|
|
684
|
-
group
|
|
685
|
-
|
|
652
|
+
appendWhereNode(orGroup, {
|
|
653
|
+
type: "group",
|
|
654
|
+
join: "AND",
|
|
655
|
+
items: [node]
|
|
656
|
+
});
|
|
686
657
|
}
|
|
687
658
|
|
|
688
659
|
function parseWhereObject(whereObj) {
|
|
@@ -691,23 +662,14 @@ function parseWhereObject(whereObj) {
|
|
|
691
662
|
}
|
|
692
663
|
|
|
693
664
|
const group = createWhereRoot();
|
|
665
|
+
const orGroups = new Map();
|
|
694
666
|
|
|
695
667
|
for (const [key, value] of Object.entries(whereObj)) {
|
|
696
|
-
if (value === undefined) {
|
|
697
|
-
continue;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
if (key === "$and") {
|
|
701
|
-
appendAndConditions(group, value);
|
|
702
|
-
continue;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
if (key === "$or") {
|
|
706
|
-
appendOrConditions(group, value);
|
|
668
|
+
if (value === undefined || key === "$exclude") {
|
|
707
669
|
continue;
|
|
708
670
|
}
|
|
709
671
|
|
|
710
|
-
appendFieldCondition(group, key, value);
|
|
672
|
+
appendFieldCondition(group, orGroups, key, value);
|
|
711
673
|
}
|
|
712
674
|
|
|
713
675
|
return group;
|
package/lib/dbUtil.js
CHANGED
|
@@ -252,7 +252,17 @@ function shouldExcludeFieldValue(field, value, excludeValueMap) {
|
|
|
252
252
|
return false;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
|
|
255
|
+
let normalizedField = field;
|
|
256
|
+
|
|
257
|
+
if (isString(field) && field !== "$or" && field !== "$and" && field !== "$exclude") {
|
|
258
|
+
try {
|
|
259
|
+
normalizedField = parseFlatWhereKey(field).field;
|
|
260
|
+
} catch {
|
|
261
|
+
normalizedField = field;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const candidateValues = excludeValueMap.get(normalizedField) || excludeValueMap.get(field);
|
|
256
266
|
if (!candidateValues || candidateValues.length === 0) {
|
|
257
267
|
return false;
|
|
258
268
|
}
|
|
@@ -320,7 +330,7 @@ export function clearDeep(value, options) {
|
|
|
320
330
|
continue;
|
|
321
331
|
}
|
|
322
332
|
|
|
323
|
-
if (
|
|
333
|
+
if (item === undefined) {
|
|
324
334
|
continue;
|
|
325
335
|
}
|
|
326
336
|
|
|
@@ -354,7 +364,7 @@ export function clearDeep(value, options) {
|
|
|
354
364
|
continue;
|
|
355
365
|
}
|
|
356
366
|
|
|
357
|
-
if (typeof item === "object" && !Array.isArray(item)) {
|
|
367
|
+
if (item !== null && typeof item === "object" && !Array.isArray(item)) {
|
|
358
368
|
if (!canRecurse) {
|
|
359
369
|
result[key] = item;
|
|
360
370
|
continue;
|
|
@@ -437,6 +447,89 @@ function normalizeOrderBy(orderBy, mapField) {
|
|
|
437
447
|
});
|
|
438
448
|
}
|
|
439
449
|
|
|
450
|
+
const flatWhereOperatorTokens = new Set(["not", "in", "notIn", "like", "leftLike", "rightLike", "notLike", "gt", "gte", "lt", "lte", "between", "notBetween", "null", "notNull"]);
|
|
451
|
+
|
|
452
|
+
function isFlatWhereOrGroupToken(token) {
|
|
453
|
+
return token === "or" || /^or\d+$/.test(token);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function buildFlatWhereKey(field, operator, orGroup) {
|
|
457
|
+
let key = field;
|
|
458
|
+
|
|
459
|
+
if (operator && operator !== "=") {
|
|
460
|
+
key += operator;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (orGroup) {
|
|
464
|
+
key += `$${orGroup}`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return key;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function parseFlatWhereKey(key) {
|
|
471
|
+
if (!isNonEmptyString(key)) {
|
|
472
|
+
throw new Error("where 字段名不能为空", {
|
|
473
|
+
cause: null,
|
|
474
|
+
code: "validation"
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
assertNoExprField(key);
|
|
479
|
+
|
|
480
|
+
const rawParts = key.split("$").map((item) => item.trim());
|
|
481
|
+
const field = rawParts[0];
|
|
482
|
+
|
|
483
|
+
if (!isNonEmptyString(field)) {
|
|
484
|
+
throw new Error(`where 字段名不能为空 (field: ${key})`, {
|
|
485
|
+
cause: null,
|
|
486
|
+
code: "validation"
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const suffixParts = rawParts.slice(1);
|
|
491
|
+
let orGroup = null;
|
|
492
|
+
|
|
493
|
+
if (suffixParts.length > 0 && isFlatWhereOrGroupToken(suffixParts[suffixParts.length - 1])) {
|
|
494
|
+
orGroup = suffixParts.pop();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (suffixParts.length > 1) {
|
|
498
|
+
throw new Error(`where 字段后缀格式无效,请使用 field、field$op、field$or、field$op$orN (field: ${key})`, {
|
|
499
|
+
cause: null,
|
|
500
|
+
code: "validation"
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (suffixParts.length === 0) {
|
|
505
|
+
return {
|
|
506
|
+
field: field,
|
|
507
|
+
operator: "=",
|
|
508
|
+
orGroup: orGroup
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const operatorToken = suffixParts[0];
|
|
513
|
+
|
|
514
|
+
if (!flatWhereOperatorTokens.has(operatorToken)) {
|
|
515
|
+
throw new Error(`where 操作符无效,请使用受支持的扁平操作符 (field: ${key}, operator: ${operatorToken})`, {
|
|
516
|
+
cause: null,
|
|
517
|
+
code: "validation"
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
field: field,
|
|
523
|
+
operator: `$${operatorToken}`,
|
|
524
|
+
orGroup: orGroup
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function normalizeFlatWhereKey(key, mapField) {
|
|
529
|
+
const parsed = parseFlatWhereKey(key);
|
|
530
|
+
return buildFlatWhereKey(mapField(parsed.field), parsed.operator, parsed.orGroup);
|
|
531
|
+
}
|
|
532
|
+
|
|
440
533
|
function mapWhereKeys(where, mapKey) {
|
|
441
534
|
if (!where || typeof where !== "object") {
|
|
442
535
|
return where;
|
|
@@ -483,20 +576,11 @@ export function processJoinField(field) {
|
|
|
483
576
|
}
|
|
484
577
|
|
|
485
578
|
function processJoinWhereKey(key) {
|
|
486
|
-
if (key === "$or" || key === "$and") {
|
|
579
|
+
if (key === "$or" || key === "$and" || key === "$exclude") {
|
|
487
580
|
return key;
|
|
488
581
|
}
|
|
489
582
|
|
|
490
|
-
|
|
491
|
-
if (key.includes("$")) {
|
|
492
|
-
const lastDollarIndex = key.lastIndexOf("$");
|
|
493
|
-
const fieldPart = key.substring(0, lastDollarIndex);
|
|
494
|
-
const operator = key.substring(lastDollarIndex);
|
|
495
|
-
|
|
496
|
-
return `${normalizeQualifierField(fieldPart)}${operator}`;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
return normalizeQualifierField(key);
|
|
583
|
+
return normalizeFlatWhereKey(key, normalizeQualifierField);
|
|
500
584
|
}
|
|
501
585
|
|
|
502
586
|
export function processJoinWhere(where) {
|
|
@@ -565,15 +649,11 @@ export function processJoinOn(on) {
|
|
|
565
649
|
|
|
566
650
|
export function whereKeysToSnake(where) {
|
|
567
651
|
return mapWhereKeys(where, (key) => {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const lastDollarIndex = key.lastIndexOf("$");
|
|
571
|
-
const fieldName = key.substring(0, lastDollarIndex);
|
|
572
|
-
const operator = key.substring(lastDollarIndex);
|
|
573
|
-
return `${snakeCase(fieldName)}${operator}`;
|
|
652
|
+
if (key === "$or" || key === "$and" || key === "$exclude") {
|
|
653
|
+
return key;
|
|
574
654
|
}
|
|
575
655
|
|
|
576
|
-
return
|
|
656
|
+
return normalizeFlatWhereKey(key, (field) => snakeCase(field));
|
|
577
657
|
});
|
|
578
658
|
}
|
|
579
659
|
|
package/lib/redisHelper.js
CHANGED
|
@@ -448,6 +448,31 @@ export class RedisHelper {
|
|
|
448
448
|
}
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
+
/**
|
|
452
|
+
* 从有序集合删除一个或多个成员
|
|
453
|
+
* @param key - 键名
|
|
454
|
+
* @param members - 成员数组
|
|
455
|
+
*/
|
|
456
|
+
async zrem(key, members) {
|
|
457
|
+
try {
|
|
458
|
+
if (members.length === 0) {
|
|
459
|
+
return 0;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const pkey = `${this.prefix}${key}`;
|
|
463
|
+
const args = [pkey];
|
|
464
|
+
|
|
465
|
+
for (const member of members) {
|
|
466
|
+
args.push(String(member));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return await this.client.zrem.apply(this.client, args);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
Logger.error("Redis zrem 错误", error);
|
|
472
|
+
return 0;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
451
476
|
/**
|
|
452
477
|
* 删除键
|
|
453
478
|
* @param key - 键名
|
package/lib/sqlBuilder.js
CHANGED
|
@@ -47,6 +47,9 @@ export class SqlBuilder {
|
|
|
47
47
|
|
|
48
48
|
switch (node.operator) {
|
|
49
49
|
case "$not":
|
|
50
|
+
if (node.value === null) {
|
|
51
|
+
return { sql: `${escapedField} IS NOT NULL`, params: [] };
|
|
52
|
+
}
|
|
50
53
|
return { sql: `${escapedField} != ?`, params: [node.value] };
|
|
51
54
|
case "$in":
|
|
52
55
|
return {
|
|
@@ -83,6 +86,9 @@ export class SqlBuilder {
|
|
|
83
86
|
case "$notNull":
|
|
84
87
|
return { sql: `${escapedField} IS NOT NULL`, params: [] };
|
|
85
88
|
default:
|
|
89
|
+
if (node.value === null) {
|
|
90
|
+
return { sql: `${escapedField} IS NULL`, params: [] };
|
|
91
|
+
}
|
|
86
92
|
return { sql: `${escapedField} = ?`, params: [node.value] };
|
|
87
93
|
}
|
|
88
94
|
}
|
package/package.json
CHANGED
package/plugins/email.js
CHANGED
|
@@ -5,11 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import nodemailer from "nodemailer";
|
|
7
7
|
|
|
8
|
-
import { Logger } from "
|
|
9
|
-
|
|
10
|
-
function canCreateTransport(config) {
|
|
11
|
-
return Boolean(String(config?.host || "").trim() && String(config?.user || "").trim() && String(config?.pass || "").trim());
|
|
12
|
-
}
|
|
8
|
+
import { Logger } from "#root/lib/logger.js";
|
|
9
|
+
import { hasEmailConfig } from "#root/utils/email.js";
|
|
13
10
|
|
|
14
11
|
export default {
|
|
15
12
|
order: 7,
|
|
@@ -17,7 +14,7 @@ export default {
|
|
|
17
14
|
const config = befly?.config?.email || {};
|
|
18
15
|
let transporter = null;
|
|
19
16
|
|
|
20
|
-
if (
|
|
17
|
+
if (hasEmailConfig(config)) {
|
|
21
18
|
try {
|
|
22
19
|
transporter = nodemailer.createTransport({
|
|
23
20
|
host: config.host,
|
package/router/api.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import picomatch from "picomatch";
|
|
7
7
|
|
|
8
8
|
import { Logger } from "../lib/logger.js";
|
|
9
|
-
import { requestMetrics } from "../lib/requestMetrics.js";
|
|
10
9
|
// 相对导入
|
|
11
10
|
import { setCorsOptions } from "../utils/cors.js";
|
|
12
11
|
import { getClientIp } from "../utils/getClientIp.js";
|
|
@@ -95,9 +94,6 @@ export function apiHandler(apis, hooks, context) {
|
|
|
95
94
|
apiRequired: apiData.required,
|
|
96
95
|
apiFile: apiData.filePath
|
|
97
96
|
};
|
|
98
|
-
const requestStartTime = Date.now();
|
|
99
|
-
let requestHasError = false;
|
|
100
|
-
requestMetrics.onRequestStart();
|
|
101
97
|
|
|
102
98
|
try {
|
|
103
99
|
// 4. 串联执行所有钩子
|
|
@@ -147,7 +143,6 @@ export function apiHandler(apis, hooks, context) {
|
|
|
147
143
|
// 7. 返回响应(自动处理 response/result/日志)
|
|
148
144
|
return FinalResponse(ctx);
|
|
149
145
|
} catch (err) {
|
|
150
|
-
requestHasError = true;
|
|
151
146
|
// 全局错误处理
|
|
152
147
|
Logger.error("请求错误", err, {
|
|
153
148
|
path: ctx.apiPath,
|
|
@@ -167,8 +162,6 @@ export function apiHandler(apis, hooks, context) {
|
|
|
167
162
|
msg: "内部服务错误"
|
|
168
163
|
};
|
|
169
164
|
return FinalResponse(ctx);
|
|
170
|
-
} finally {
|
|
171
|
-
requestMetrics.onRequestEnd(ctx.apiPath, Date.now() - requestStartTime, requestHasError);
|
|
172
165
|
}
|
|
173
166
|
};
|
|
174
167
|
}
|
package/utils/email.js
ADDED