befly 3.76.0 → 3.76.2
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/Befly.js +159 -0
- package/README.md +50 -11
- package/apis/admin/delete.js +1 -3
- package/apis/admin/insert.js +2 -1
- package/apis/admin/select.js +2 -7
- package/apis/admin/update.js +3 -8
- package/apis/api/select.js +2 -7
- package/apis/auth/login.js +16 -20
- package/apis/dict/delete.js +1 -1
- package/apis/dict/detail.js +1 -3
- package/apis/dict/select.js +1 -2
- package/apis/dict/update.js +1 -1
- package/apis/dictType/delete.js +1 -1
- package/apis/dictType/detail.js +1 -1
- package/apis/dictType/insert.js +1 -1
- package/apis/dictType/select.js +2 -7
- package/apis/dictType/update.js +2 -2
- package/apis/email/config.js +4 -7
- package/apis/email/logList.js +2 -7
- package/apis/email/send.js +1 -2
- package/apis/email/verify.js +1 -3
- package/apis/loginLog/select.js +2 -7
- package/apis/menu/select.js +2 -7
- package/apis/operateLog/select.js +2 -7
- package/apis/role/apiSave.js +2 -2
- package/apis/role/menuSave.js +2 -2
- package/apis/role/select.js +2 -7
- package/apis/source/imageList.js +2 -5
- package/apis/tongJi/dailyReport.js +12 -13
- package/apis/tongJi/dailyReportSelect.js +1 -2
- package/apis/tongJi/dailyStats.js +1 -1
- package/apis/tongJi/dailyStatsDistribution.js +2 -2
- package/apis/tongJi/errorList.js +1 -2
- package/apis/tongJi/errorReport.js +1 -2
- package/apis/tongJi/projectDelete.js +1 -3
- package/apis/tongJi/projectSelect.js +2 -4
- package/apis/tongJi/projectUpdate.js +2 -2
- package/apis/upload/file.js +7 -5
- package/checks/api.js +43 -66
- package/checks/config.js +17 -12
- package/checks/corns.js +28 -46
- package/checks/field.js +18 -0
- package/checks/items.js +16 -60
- package/checks/menu.js +28 -39
- package/checks/table.js +106 -26
- package/checks/validation.js +13 -0
- package/configs/beflyConfig.json +6 -6
- package/configs/constConfig.js +2 -63
- package/exports.js +2 -0
- package/hooks/auth.js +11 -7
- package/hooks/parser.js +55 -59
- package/hooks/permission.js +5 -2
- package/hooks/rateLimit.js +25 -62
- package/hooks/validator.js +43 -31
- package/index.js +43 -344
- package/{lib → libs}/cacheHelper.js +3 -8
- package/libs/cron.js +60 -0
- package/libs/logger/index.js +1 -0
- package/libs/logger/logger.js +245 -0
- package/libs/logger/sanitize.js +115 -0
- package/{lib → libs/mysql}/dbHelper.js +100 -73
- package/{lib → libs/mysql}/dbParse.js +16 -9
- package/{lib → libs/mysql}/dbUtil.js +15 -32
- package/libs/mysql/error.js +29 -0
- package/libs/mysql/index.js +6 -0
- package/libs/mysql/is.js +29 -0
- package/libs/mysql/mysql.js +43 -0
- package/libs/mysql/sql/identifier.js +85 -0
- package/libs/mysql/sql/index.js +2 -0
- package/{lib → libs/mysql/sql}/sqlBuilder.js +2 -3
- package/libs/mysql/util.js +44 -0
- package/libs/redis/index.js +2 -0
- package/libs/redis/redis.js +204 -0
- package/libs/redis/redisError.js +8 -0
- package/libs/tableModel.js +72 -0
- package/libs/tool.js +33 -0
- package/libs/validator/compiler.js +248 -0
- package/libs/validator/index.js +10 -0
- package/libs/validator/parser.js +173 -0
- package/libs/validator/util.js +5 -0
- package/package.json +10 -8
- package/paths.js +2 -3
- package/plugins/cache.js +2 -1
- package/plugins/email.js +4 -4
- package/plugins/mysql.js +5 -15
- package/plugins/redis.js +4 -12
- package/router/api.js +14 -10
- package/router/root.js +14 -0
- package/router/static.js +3 -3
- package/schemas/api.json +99 -0
- package/schemas/config.json +255 -0
- package/schemas/corn.json +39 -0
- package/schemas/item.json +43 -0
- package/schemas/menu.json +44 -0
- package/schemas/table.json +80 -0
- package/scripts/syncDb/apply.js +87 -0
- package/scripts/syncDb/ddl.js +43 -0
- package/scripts/syncDb/index.js +175 -30
- package/scripts/syncDb/model.js +60 -0
- package/scripts/syncDb/plan.js +160 -0
- package/scripts/syncDb/preflight.js +98 -0
- package/scripts/syncDb/report.js +177 -162
- package/scripts/syncDb/snapshot.js +118 -0
- package/sync/syncUtil.js +7 -13
- package/tables/admin.json +51 -53
- package/tables/api.json +29 -40
- package/tables/dailyReport.json +85 -73
- package/tables/dict.json +27 -39
- package/tables/dictType.json +20 -34
- package/tables/emailLog.json +49 -47
- package/tables/errorReport.json +86 -60
- package/tables/file.json +47 -50
- package/tables/loginLog.json +68 -55
- package/tables/menu.json +20 -34
- package/tables/operateLog.json +53 -47
- package/tables/project.json +18 -30
- package/tables/role.json +33 -37
- package/utils/cors.js +37 -41
- package/utils/deepMerge.js +3 -1
- package/utils/error.js +1 -56
- package/utils/id.js +44 -0
- package/utils/is.js +6 -243
- package/utils/response.js +12 -1
- package/utils/scanFiles.js +13 -4
- package/utils/scanSources.js +58 -20
- package/utils/util.js +11 -163
- package/apis/_apis.js +0 -20
- package/apis/auth/sendSmsCode.js +0 -24
- package/apis/role/delete.js +0 -41
- package/apis/role/insert.js +0 -45
- package/apis/role/save.js +0 -36
- package/apis/role/update.js +0 -66
- package/configs/regexpAlias.json +0 -55
- package/lib/connect.js +0 -119
- package/lib/logger.js +0 -430
- package/lib/redisHelper.js +0 -399
- package/lib/validator.js +0 -274
- package/lib/xmlParse.js +0 -149
- package/plugins/config.js +0 -13
- package/plugins/cron.js +0 -115
- package/plugins/logger.js +0 -17
- package/plugins/tool.js +0 -131
- package/plugins/xmlParse.js +0 -10
- package/schemas/config.js +0 -67
- package/scripts/syncDb/context.js +0 -120
- package/scripts/syncDb/diff.js +0 -279
- package/scripts/syncDb/transform.js +0 -113
- package/sql/befly.sql +0 -271
- package/utils/email.js +0 -3
- package/utils/fieldClear.js +0 -22
- package/utils/formatValidationIssues.js +0 -28
- package/utils/getClientIp.js +0 -47
- package/utils/loggerUtils.js +0 -196
- package/utils/regexpUtil.js +0 -52
- /package/{lib → libs}/smtpText.js +0 -0
package/utils/is.js
CHANGED
|
@@ -1,267 +1,30 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 判断值是否为 plain object(原型为 Object.prototype 或 null)。
|
|
3
|
-
*/
|
|
4
1
|
export function isPlainObject(value) {
|
|
5
|
-
if (typeof value !== "object" || value === null)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const proto = Object.getPrototypeOf(value);
|
|
10
|
-
return proto === Object.prototype || proto === null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* 判断值是否为 JSON 基础类型(string/number/boolean/null)。
|
|
15
|
-
*/
|
|
16
|
-
export function isJsonPrimitive(value) {
|
|
17
|
-
if (value === null) {
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
|
-
return typeof value === "string" || isNumber(value) || isBoolean(value);
|
|
2
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3
|
+
const prototype = Object.getPrototypeOf(value);
|
|
4
|
+
return prototype === Object.prototype || prototype === null;
|
|
21
5
|
}
|
|
22
6
|
|
|
23
|
-
/**
|
|
24
|
-
* 判断值是否为有限数值。
|
|
25
|
-
*/
|
|
26
|
-
export function isFiniteNumber(value) {
|
|
27
|
-
return isNumber(value) && Number.isFinite(value);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 判断值是否为字符串。
|
|
32
|
-
*/
|
|
33
7
|
export function isString(value) {
|
|
34
8
|
return typeof value === "string";
|
|
35
9
|
}
|
|
36
10
|
|
|
37
|
-
/**
|
|
38
|
-
* 判断值是否为 number。
|
|
39
|
-
*/
|
|
40
11
|
export function isNumber(value) {
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 判断值是否为 boolean。
|
|
46
|
-
*/
|
|
47
|
-
export function isBoolean(value) {
|
|
48
|
-
return Object.prototype.toString.call(value) === "[object Boolean]";
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* 判断值是否为 function。
|
|
53
|
-
*/
|
|
54
|
-
export function isFunction(value) {
|
|
55
|
-
return typeof value === "function";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* 判断值是否为 nullable(默认仅 null/undefined)。
|
|
60
|
-
* 可通过 extraValues 指定更多“空值”。
|
|
61
|
-
*/
|
|
62
|
-
export function isNullable(value, extraValues = []) {
|
|
63
|
-
if (value === null || value === undefined) {
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (!Array.isArray(extraValues) || extraValues.length === 0) {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
for (const item of extraValues) {
|
|
72
|
-
if (Object.is(value, item)) {
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return false;
|
|
12
|
+
return typeof value === "number";
|
|
78
13
|
}
|
|
79
14
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
*/
|
|
83
|
-
export function isMinLengthString(value, minLength) {
|
|
84
|
-
if (typeof value !== "string") {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
if (!Number.isFinite(minLength)) {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
if (minLength <= 0) {
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
return value.trim().length >= minLength;
|
|
15
|
+
export function isNullable(value) {
|
|
16
|
+
return value === null || value === undefined;
|
|
94
17
|
}
|
|
95
18
|
|
|
96
|
-
/**
|
|
97
|
-
* 判断值是否为非空字符串。
|
|
98
|
-
*/
|
|
99
19
|
export function isNonEmptyString(value) {
|
|
100
20
|
return typeof value === "string" && value.trim().length > 0;
|
|
101
21
|
}
|
|
102
22
|
|
|
103
|
-
/**
|
|
104
|
-
* 判断字符串是否允许空字符串且不允许首尾空白。
|
|
105
|
-
*/
|
|
106
|
-
export function isNoTrimStringAllowEmpty(value) {
|
|
107
|
-
if (typeof value !== "string") {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (value.length === 0) {
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const firstCharCode = value.codePointAt(0);
|
|
116
|
-
if (isWhitespaceCharCode(firstCharCode)) {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const lastCharCode = value.codePointAt(value.length - 1);
|
|
121
|
-
if (isWhitespaceCharCode(lastCharCode)) {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function isWhitespaceCharCode(code) {
|
|
129
|
-
if (code === 9 || code === 10 || code === 11 || code === 12 || code === 13) {
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
if (code === 32 || code === 133 || code === 160) {
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
if (code === 5760 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279) {
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
if (code >= 8192 && code <= 8202) {
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* 判断是否为小驼峰命名(可选以下划线开头)。
|
|
146
|
-
*/
|
|
147
|
-
export function isLowerCamelCaseName(value) {
|
|
148
|
-
if (!isNonEmptyString(value)) {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
return /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/.test(value);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* 判断值是否为合法端口号(1..65535)。
|
|
156
|
-
*/
|
|
157
|
-
export function isValidPort(value) {
|
|
158
|
-
return isFiniteNumber(value) && value >= 1 && value <= 65535;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* 判断值是否为合法非负整数。
|
|
163
|
-
*/
|
|
164
|
-
export function isValidNonNegativeInt(value) {
|
|
165
|
-
return Number.isInteger(value) && value >= 0;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* 判断值是否为合法正整数。
|
|
170
|
-
*/
|
|
171
23
|
export function isValidPositiveInt(value) {
|
|
172
24
|
return Number.isInteger(value) && value >= 1;
|
|
173
25
|
}
|
|
174
26
|
|
|
175
|
-
/**
|
|
176
|
-
* 判断值是否为有效 IANA 时区字符串。
|
|
177
|
-
*/
|
|
178
|
-
export function isValidTimeZone(value) {
|
|
179
|
-
if (!isNonEmptyString(value)) return false;
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
// RangeError: Invalid time zone specified
|
|
183
|
-
new Intl.DateTimeFormat("en-US", { timeZone: value }).format(0);
|
|
184
|
-
return true;
|
|
185
|
-
} catch {
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* 判断值是否为可注册的 cron 表达式(与 Bun.cron 注册行为一致:
|
|
192
|
-
* parse 抛异常或 8 年内无匹配都视为非法,避免启动后才报错)。
|
|
193
|
-
*/
|
|
194
|
-
export function isValidCronSchedule(value) {
|
|
195
|
-
if (!isString(value)) return false;
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
return Bun.cron.parse(value) !== null;
|
|
199
|
-
} catch {
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* 判断值是否为有限整数。
|
|
206
|
-
*/
|
|
207
|
-
export function isIntegerNumber(value) {
|
|
208
|
-
return isNumber(value) && Number.isInteger(value) && Number.isFinite(value);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* 判断字符串是否为正则字面量(/source/flags)。
|
|
213
|
-
*/
|
|
214
|
-
export function isRegexLiteral(input) {
|
|
215
|
-
const trimmed = String(input || "").trim();
|
|
216
|
-
if (!isMinLengthString(trimmed, 2)) {
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
if (!trimmed.startsWith("/")) {
|
|
220
|
-
return false;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const lastSlash = trimmed.lastIndexOf("/");
|
|
224
|
-
if (lastSlash <= 0) {
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const flags = trimmed.slice(lastSlash + 1);
|
|
229
|
-
if (flags !== "" && !/^[gimsuy]+$/.test(flags)) {
|
|
230
|
-
return false;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* 判断输入是否为正则规则(字面量或 @ 别名)。
|
|
238
|
-
*/
|
|
239
|
-
export function isRegexInput(input) {
|
|
240
|
-
const trimmed = String(input || "").trim();
|
|
241
|
-
// 约束:正则 input 仅允许 /source/flags 形式;同时保留 @alias 作为正则别名。
|
|
242
|
-
// 说明:不再支持直接写 source(如 ^\d+$),避免与普通字符串/枚举产生歧义。
|
|
243
|
-
return isRegexLiteral(trimmed) || trimmed.startsWith("@");
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* 判断当前进程是否为主进程。
|
|
248
|
-
* bm2 的实例 0 是主进程;未由 bm2 启动时视为主进程。
|
|
249
|
-
*/
|
|
250
27
|
export function isPrimaryProcess() {
|
|
251
28
|
const instance = Bun.env.BM2_APP_INSTANCE;
|
|
252
29
|
return instance === "0" || instance === 0 || instance === undefined;
|
|
253
30
|
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* 日志敏感键判定
|
|
257
|
-
*/
|
|
258
|
-
export function isSensitiveKey(key, matcher) {
|
|
259
|
-
const lower = String(key).toLowerCase();
|
|
260
|
-
if (matcher.exactSet.has(lower)) return true;
|
|
261
|
-
|
|
262
|
-
for (const part of matcher.contains) {
|
|
263
|
-
if (lower.includes(part)) return true;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return false;
|
|
267
|
-
}
|
package/utils/response.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import { Logger } from "
|
|
1
|
+
import { Logger } from "#befly/libs/logger/index.js";
|
|
2
|
+
|
|
2
3
|
import { isString } from "./is.js";
|
|
3
4
|
|
|
5
|
+
const errorStatus = {
|
|
6
|
+
parser: 400,
|
|
7
|
+
validator: 400,
|
|
8
|
+
auth: 401,
|
|
9
|
+
permission: 403,
|
|
10
|
+
method: 405,
|
|
11
|
+
rateLimit: 429
|
|
12
|
+
};
|
|
13
|
+
|
|
4
14
|
/**
|
|
5
15
|
* 创建错误响应(专用于 Hook 中间件)
|
|
6
16
|
* 在钩子中提前拦截请求时使用
|
|
@@ -39,6 +49,7 @@ export function ErrorResponse(ctx, msg, code = 1, data = null, detail = null, re
|
|
|
39
49
|
reason: reason
|
|
40
50
|
},
|
|
41
51
|
{
|
|
52
|
+
status: errorStatus[reason] || 400,
|
|
42
53
|
headers: ctx.corsHeaders
|
|
43
54
|
}
|
|
44
55
|
);
|
package/utils/scanFiles.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
|
|
3
|
-
import { relative, normalize, parse, join } from "pathe";
|
|
2
|
+
import { join, normalize, parse, relative } from "node:path";
|
|
4
3
|
|
|
5
4
|
import { createError } from "./error.js";
|
|
6
5
|
import { importDefault } from "./importDefault.js";
|
|
7
6
|
import { isPlainObject } from "./is.js";
|
|
8
7
|
|
|
8
|
+
const selectFields = {
|
|
9
|
+
page: { name: "页码", paramType: "integer", minValue: 1, maxValue: 10000, paramDefault: 1 },
|
|
10
|
+
limit: { name: "每页数量", paramType: "integer", minValue: 1, maxValue: 100, paramDefault: 20 },
|
|
11
|
+
keyword: { name: "关键字", paramType: "string", minValue: 0, maxValue: 100, paramDefault: "" },
|
|
12
|
+
state: { name: "状态", paramType: "integer" }
|
|
13
|
+
};
|
|
14
|
+
|
|
9
15
|
/**
|
|
10
16
|
* 扫描指定目录下的文件
|
|
11
17
|
* @param dir 目录路径
|
|
@@ -28,7 +34,6 @@ export async function scanFiles(dir, source, type, pattern) {
|
|
|
28
34
|
});
|
|
29
35
|
|
|
30
36
|
for await (const file of files) {
|
|
31
|
-
// 使用 pathe.normalize 统一路径分隔符为 /
|
|
32
37
|
const filePath = normalize(file);
|
|
33
38
|
const parsedFile = parse(filePath);
|
|
34
39
|
|
|
@@ -37,7 +42,7 @@ export async function scanFiles(dir, source, type, pattern) {
|
|
|
37
42
|
|
|
38
43
|
// 计算相对路径(去除扩展名)
|
|
39
44
|
const parsedRelativePath = parse(relative(dir, filePath));
|
|
40
|
-
const relativePath = parsedRelativePath.dir ? join(parsedRelativePath.dir, parsedRelativePath.name) : parsedRelativePath.name;
|
|
45
|
+
const relativePath = (parsedRelativePath.dir ? join(parsedRelativePath.dir, parsedRelativePath.name) : parsedRelativePath.name).replaceAll("\\", "/");
|
|
41
46
|
|
|
42
47
|
// 固定默认过滤(不可关闭):忽略下划线开头的文件/目录
|
|
43
48
|
if (relativePath.split("/").some((part) => part.startsWith("_"))) continue;
|
|
@@ -64,6 +69,10 @@ export async function scanFiles(dir, source, type, pattern) {
|
|
|
64
69
|
base[key] = value;
|
|
65
70
|
}
|
|
66
71
|
}
|
|
72
|
+
if (type === "api") {
|
|
73
|
+
base.category ||= "other";
|
|
74
|
+
if (base.category === "select") base.fields = { ...selectFields, ...base.fields };
|
|
75
|
+
}
|
|
67
76
|
results.push(base);
|
|
68
77
|
}
|
|
69
78
|
return results;
|
package/utils/scanSources.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildTableModel } from "../libs/tableModel.js";
|
|
1
2
|
import {
|
|
2
3
|
//
|
|
3
4
|
coreTableDir,
|
|
@@ -14,23 +15,63 @@ import {
|
|
|
14
15
|
import { createError } from "./error.js";
|
|
15
16
|
import { scanFiles } from "./scanFiles.js";
|
|
16
17
|
|
|
17
|
-
export
|
|
18
|
+
export function assertUnique(items, key, label) {
|
|
19
|
+
const values = new Map();
|
|
20
|
+
for (const item of items) {
|
|
21
|
+
const value = item[key];
|
|
22
|
+
if (!values.has(value)) {
|
|
23
|
+
values.set(value, item);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const first = values.get(value);
|
|
27
|
+
throw createError(`${label}冲突:${value},${first.filePath} 与 ${item.filePath} 不允许同时存在`, {
|
|
28
|
+
code: "policy",
|
|
29
|
+
subsystem: "scan",
|
|
30
|
+
operation: "unique",
|
|
31
|
+
key: key,
|
|
32
|
+
value: value
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function assertAppApiNamespace(item) {
|
|
38
|
+
if (item.relativePath.split("/")[0] !== "core") return;
|
|
39
|
+
throw createError(`app 接口目录下禁止存在 core 一级目录:${item.filePath}`, {
|
|
40
|
+
code: "policy",
|
|
41
|
+
subsystem: "scan",
|
|
42
|
+
operation: "scanSources"
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function addTable(tables, table) {
|
|
47
|
+
if (tables[table.codeName]) {
|
|
48
|
+
throw createError(`Table 冲突:${table.codeName},${tables[table.codeName].fileName} 与 ${table.fileName} 不允许同时存在`, {
|
|
49
|
+
code: "policy",
|
|
50
|
+
subsystem: "scan",
|
|
51
|
+
operation: "unique",
|
|
52
|
+
key: "codeName",
|
|
53
|
+
value: table.codeName
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
tables[table.codeName] = table;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function scanTables({ beflyMode = "auto" } = {}) {
|
|
60
|
+
const tables = {};
|
|
61
|
+
const allCoreTables = await scanFiles(coreTableDir, "core", "table", "*.json");
|
|
62
|
+
for (const item of allCoreTables) addTable(tables, buildTableModel(item.fieldsDef, { source: "core", fileName: item.fileName, beflyMode: beflyMode }));
|
|
63
|
+
const allAppTables = await scanFiles(appTableDir, "app", "table", "*.json");
|
|
64
|
+
for (const item of allAppTables) addTable(tables, buildTableModel(item.fieldsDef, { source: "app", fileName: item.fileName, beflyMode: beflyMode }));
|
|
65
|
+
return tables;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const scanSources = async ({ beflyMode = "auto" } = {}) => {
|
|
18
69
|
const apis = [];
|
|
19
70
|
const plugins = [];
|
|
20
71
|
const hooks = [];
|
|
21
|
-
const tables = {};
|
|
72
|
+
const tables = await scanTables({ beflyMode: beflyMode });
|
|
22
73
|
const corns = [];
|
|
23
74
|
|
|
24
|
-
// 处理表格
|
|
25
|
-
const allCoreTables = await scanFiles(coreTableDir, "core", "table", "*.json");
|
|
26
|
-
for (const item of allCoreTables) {
|
|
27
|
-
tables[`befly${item.fileName.charAt(0).toUpperCase()}${item.fileName.slice(1)}`] = item.fieldsDef;
|
|
28
|
-
}
|
|
29
|
-
const allAppTables = await scanFiles(appTableDir, "app", "table", "*.json");
|
|
30
|
-
for (const item of allAppTables) {
|
|
31
|
-
tables[item.fileName] = item.fieldsDef;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
75
|
// 处理插件
|
|
35
76
|
const allCorePlugins = await scanFiles(corePluginDir, "core", "plugin", "*.js");
|
|
36
77
|
for (const item of allCorePlugins) {
|
|
@@ -68,17 +109,14 @@ export const scanSources = async () => {
|
|
|
68
109
|
}
|
|
69
110
|
const allAppApis = await scanFiles(appApiDir, "app", "api", "**/*.js");
|
|
70
111
|
for (const item of allAppApis) {
|
|
71
|
-
|
|
72
|
-
if (item.relativePath.split("/")[0] === "core") {
|
|
73
|
-
throw createError(`app 接口目录下禁止存在 core 一级目录:${item.filePath}`, {
|
|
74
|
-
code: "policy",
|
|
75
|
-
subsystem: "scan",
|
|
76
|
-
operation: "scanSources"
|
|
77
|
-
});
|
|
78
|
-
}
|
|
112
|
+
assertAppApiNamespace(item);
|
|
79
113
|
apis.push(item);
|
|
80
114
|
}
|
|
81
115
|
|
|
116
|
+
assertUnique(plugins, "fileName", "Plugin 文件名");
|
|
117
|
+
assertUnique(hooks, "fileName", "Hook 文件名");
|
|
118
|
+
assertUnique(corns, "fileName", "Cron 文件名");
|
|
119
|
+
|
|
82
120
|
return {
|
|
83
121
|
hooks: hooks,
|
|
84
122
|
plugins: plugins,
|
package/utils/util.js
CHANGED
|
@@ -1,197 +1,45 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 注意:本文件用于集中维护 core 内部通用 utils。
|
|
3
|
-
* - 按项目规范:避免零散小文件噪音;实现集中在本文件,而不是做 re-export 聚合。
|
|
4
|
-
* - 仅在 packages/core 内部使用;core 对外不承诺这些路径导出。
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
import { createError } from "./error.js";
|
|
8
|
-
import { isPlainObject } from "./is.js";
|
|
9
2
|
|
|
10
|
-
/**
|
|
11
|
-
* 通用等待工具:轮询直到条件满足或超时(while + sleep 实现,无定时器句柄,无泄漏风险)
|
|
12
|
-
* @param {string} label - 等待说明(用于超时错误信息)
|
|
13
|
-
* @param {Function} check - 异步条件函数,返回 true 表示就绪
|
|
14
|
-
* @param {number} [intervalMs=200] - 轮询间隔(毫秒)
|
|
15
|
-
* @param {number} [timeoutMs=60000] - 超时时间(毫秒)
|
|
16
|
-
*/
|
|
17
3
|
export async function waitFor({ label, check, intervalMs = 200, timeoutMs = 60000 }) {
|
|
18
4
|
const deadline = Date.now() + timeoutMs;
|
|
19
5
|
while (Date.now() < deadline) {
|
|
20
|
-
if (await check())
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
6
|
+
if (await check()) return;
|
|
23
7
|
await Bun.sleep(intervalMs);
|
|
24
8
|
}
|
|
25
9
|
throw createError(`等待${label}超时`, { code: "runtime", subsystem: "start", operation: "waitFor" });
|
|
26
10
|
}
|
|
27
11
|
|
|
28
|
-
/**
|
|
29
|
-
* 判断 bigint 是否可安全转换为 number。
|
|
30
|
-
* - 超出安全整数范围则不可转换
|
|
31
|
-
* - 转换后若是科学计数法表示则不可转换
|
|
32
|
-
*/
|
|
33
|
-
export function canConvertToNumber(value) {
|
|
34
|
-
const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
|
|
35
|
-
const minSafe = BigInt(Number.MIN_SAFE_INTEGER);
|
|
36
|
-
|
|
37
|
-
if (value > maxSafe || value < minSafe) {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const asNumber = Number(value);
|
|
42
|
-
if (!Number.isSafeInteger(asNumber)) {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const text = String(asNumber);
|
|
47
|
-
if (text.includes("e") || text.includes("E")) {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return asNumber;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* 获取值的类型标识(数组/对象等做细分)。
|
|
56
|
-
*/
|
|
57
12
|
export function getTypeTag(value) {
|
|
58
13
|
if (value === null) return "null";
|
|
59
14
|
if (value === undefined) return "undefined";
|
|
60
15
|
if (Array.isArray(value)) return "array";
|
|
61
|
-
|
|
62
|
-
const t = typeof value;
|
|
63
|
-
if (t === "string") return "string";
|
|
64
|
-
if (t === "number") return "number";
|
|
65
|
-
if (t === "boolean") return "boolean";
|
|
66
|
-
if (t === "bigint") return "bigint";
|
|
67
|
-
if (t === "symbol") return "symbol";
|
|
68
|
-
if (t === "function") return "function";
|
|
69
|
-
|
|
70
|
-
return "object";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 归一化正整数:非法值回退,超出范围裁剪。
|
|
75
|
-
*/
|
|
76
|
-
export function normalizePositiveInt(value, fallback, min, max) {
|
|
77
|
-
if (!Number.isFinite(value)) return fallback;
|
|
78
|
-
const v = Math.floor(value);
|
|
79
|
-
if (v < min) return fallback;
|
|
80
|
-
if (v > max) return max;
|
|
81
|
-
return v;
|
|
16
|
+
return typeof value;
|
|
82
17
|
}
|
|
83
18
|
|
|
84
|
-
// 将字符串拆分为词段(用于 camelCase/snakeCase)
|
|
85
19
|
function toWordParts(input) {
|
|
86
|
-
|
|
20
|
+
return String(input)
|
|
87
21
|
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
88
22
|
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
89
23
|
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
90
|
-
.trim()
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return normalized.split(/\s+/).filter((p) => p.length > 0);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 首字母大写
|
|
100
|
-
function upperFirst(s) {
|
|
101
|
-
if (s.length === 0) {
|
|
102
|
-
return s;
|
|
103
|
-
}
|
|
104
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
24
|
+
.trim()
|
|
25
|
+
.split(/\s+/)
|
|
26
|
+
.filter(Boolean);
|
|
105
27
|
}
|
|
106
28
|
|
|
107
|
-
/**
|
|
108
|
-
* 把字符串转为小驼峰。
|
|
109
|
-
* - 主要用于文件名/目录名(例如 my_plugin / my-plugin / my plugin)。
|
|
110
|
-
*/
|
|
111
|
-
export function camelCase(input) {
|
|
112
|
-
const parts = toWordParts(input);
|
|
113
|
-
if (parts.length === 0) {
|
|
114
|
-
return "";
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const firstPart = parts[0];
|
|
118
|
-
if (!firstPart) {
|
|
119
|
-
return "";
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const first = firstPart.toLowerCase();
|
|
123
|
-
const rest = parts.slice(1).map((p) => upperFirst(p.toLowerCase()));
|
|
124
|
-
return [first].concat(rest).join("");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* 把字符串转为 snake_case。
|
|
129
|
-
* - 主要用于表名/字段名(例如 userId -> user_id)。
|
|
130
|
-
*/
|
|
131
29
|
export function snakeCase(input) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return parts.map((p) => p.toLowerCase()).join("_");
|
|
30
|
+
return toWordParts(input)
|
|
31
|
+
.map((part) => part.toLowerCase())
|
|
32
|
+
.join("_");
|
|
138
33
|
}
|
|
139
34
|
|
|
140
|
-
/**
|
|
141
|
-
* 对象字段名转下划线
|
|
142
|
-
* @param obj - 源对象
|
|
143
|
-
* @returns 字段名转为下划线格式的新对象
|
|
144
|
-
*/
|
|
145
|
-
export const keysToSnake = (obj) => {
|
|
146
|
-
if (!obj || !isPlainObject(obj)) return obj;
|
|
147
|
-
|
|
148
|
-
const result = {};
|
|
149
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
150
|
-
const snakeKey = snakeCase(key);
|
|
151
|
-
result[snakeKey] = value;
|
|
152
|
-
}
|
|
153
|
-
return result;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* 获取运行模式(开发/正式的统一判断,仅以 NODE_ENV 为准)
|
|
158
|
-
* - 线上部署(bm2 等)注入 NODE_ENV=production
|
|
159
|
-
* - 本地开发未设置 NODE_ENV,默认 "development"
|
|
160
|
-
*/
|
|
161
35
|
export function getRunMode() {
|
|
162
36
|
return Bun.env.NODE_ENV || "development";
|
|
163
37
|
}
|
|
164
38
|
|
|
165
|
-
/**
|
|
166
|
-
* 将 session 有效天数转换为秒。
|
|
167
|
-
*/
|
|
168
39
|
export function toSessionTtlSeconds(ttlDays) {
|
|
169
|
-
|
|
170
|
-
return Math.floor(ttlDays * 24 * 60 * 60);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (typeof ttlDays === "string") {
|
|
174
|
-
const parsed = Number(ttlDays.trim());
|
|
175
|
-
if (Number.isFinite(parsed) && parsed > 0) {
|
|
176
|
-
return Math.floor(parsed * 24 * 60 * 60);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return 7 * 24 * 60 * 60;
|
|
40
|
+
return ttlDays * 24 * 60 * 60;
|
|
181
41
|
}
|
|
182
42
|
|
|
183
|
-
/**
|
|
184
|
-
* 生成短 ID
|
|
185
|
-
* 由时间戳(base36)+ 随机字符组成,约 13 位
|
|
186
|
-
* - 前 8 位:时间戳(可排序)
|
|
187
|
-
* - 后 5 位:随机字符(防冲突)
|
|
188
|
-
* @returns 短 ID 字符串
|
|
189
|
-
*/
|
|
190
43
|
export function genShortId() {
|
|
191
|
-
|
|
192
|
-
let rand = "";
|
|
193
|
-
while (rand.length < 10) {
|
|
194
|
-
rand = rand + Math.random().toString(36).slice(2);
|
|
195
|
-
}
|
|
196
|
-
return timePart + rand.slice(0, 10);
|
|
44
|
+
return `${Date.now().toString(36)}${crypto.randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
197
45
|
}
|
package/apis/_apis.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export const queryFields = {
|
|
2
|
-
page: {
|
|
3
|
-
name: "页码",
|
|
4
|
-
input: "integer",
|
|
5
|
-
min: 1,
|
|
6
|
-
max: 10000
|
|
7
|
-
},
|
|
8
|
-
limit: {
|
|
9
|
-
name: "每页数量",
|
|
10
|
-
input: "integer",
|
|
11
|
-
min: 1,
|
|
12
|
-
max: 100
|
|
13
|
-
},
|
|
14
|
-
keyword: {
|
|
15
|
-
name: "关键字",
|
|
16
|
-
input: "string",
|
|
17
|
-
min: 0,
|
|
18
|
-
max: 100
|
|
19
|
-
}
|
|
20
|
-
};
|
package/apis/auth/sendSmsCode.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import adminTable from "#befly/tables/admin.json";
|
|
2
|
-
|
|
3
|
-
export default {
|
|
4
|
-
name: "发送短信验证码",
|
|
5
|
-
method: "POST",
|
|
6
|
-
body: "none",
|
|
7
|
-
auth: false,
|
|
8
|
-
fields: {
|
|
9
|
-
phone: adminTable.phone
|
|
10
|
-
},
|
|
11
|
-
required: ["phone"],
|
|
12
|
-
handler: async (befly, ctx) => {
|
|
13
|
-
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
|
14
|
-
|
|
15
|
-
if (befly.redis) {
|
|
16
|
-
const key = `sms_code:${ctx.body.phone}`;
|
|
17
|
-
await befly.redis.setString(key, code, 300);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const isDev = befly.config ? befly.config.runMode === "development" : false;
|
|
21
|
-
|
|
22
|
-
return befly.tool.Yes("验证码已发送", isDev ? { code: code } : null);
|
|
23
|
-
}
|
|
24
|
-
};
|