befly 3.9.37 → 3.9.39
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/README.md +38 -39
- package/befly.config.ts +62 -40
- package/checks/checkApi.ts +16 -16
- package/checks/checkApp.ts +19 -25
- package/checks/checkTable.ts +42 -42
- package/docs/README.md +42 -35
- package/docs/{api.md → api/api.md} +225 -235
- package/docs/cipher.md +71 -69
- package/docs/database.md +155 -153
- package/docs/{examples.md → guide/examples.md} +181 -181
- package/docs/guide/quickstart.md +331 -0
- package/docs/hooks/auth.md +38 -0
- package/docs/hooks/cors.md +28 -0
- package/docs/{hook.md → hooks/hook.md} +140 -57
- package/docs/hooks/parser.md +19 -0
- package/docs/hooks/rateLimit.md +47 -0
- package/docs/{redis.md → infra/redis.md} +84 -93
- package/docs/plugins/cipher.md +61 -0
- package/docs/plugins/database.md +128 -0
- package/docs/{plugin.md → plugins/plugin.md} +83 -81
- package/docs/quickstart.md +26 -26
- package/docs/{addon.md → reference/addon.md} +46 -46
- package/docs/{config.md → reference/config.md} +32 -80
- package/docs/{logger.md → reference/logger.md} +52 -52
- package/docs/{sync.md → reference/sync.md} +32 -35
- package/docs/{table.md → reference/table.md} +7 -7
- package/docs/{validator.md → reference/validator.md} +57 -57
- package/hooks/auth.ts +8 -4
- package/hooks/cors.ts +13 -13
- package/hooks/parser.ts +37 -17
- package/hooks/permission.ts +26 -14
- package/hooks/rateLimit.ts +276 -0
- package/hooks/validator.ts +15 -7
- package/lib/asyncContext.ts +43 -0
- package/lib/cacheHelper.ts +212 -81
- package/lib/cacheKeys.ts +38 -0
- package/lib/cipher.ts +30 -30
- package/lib/connect.ts +28 -28
- package/lib/dbHelper.ts +211 -109
- package/lib/jwt.ts +16 -16
- package/lib/logger.ts +610 -19
- package/lib/redisHelper.ts +185 -44
- package/lib/sqlBuilder.ts +90 -91
- package/lib/validator.ts +59 -39
- package/loader/loadApis.ts +53 -47
- package/loader/loadHooks.ts +40 -14
- package/loader/loadPlugins.ts +16 -17
- package/main.ts +57 -47
- package/package.json +47 -45
- package/paths.ts +15 -14
- package/plugins/cache.ts +5 -4
- package/plugins/cipher.ts +3 -3
- package/plugins/config.ts +2 -2
- package/plugins/db.ts +9 -9
- package/plugins/jwt.ts +3 -3
- package/plugins/logger.ts +8 -12
- package/plugins/redis.ts +8 -8
- package/plugins/tool.ts +6 -6
- package/router/api.ts +85 -56
- package/router/static.ts +12 -12
- package/sync/syncAll.ts +12 -12
- package/sync/syncApi.ts +55 -54
- package/sync/syncDb/apply.ts +20 -19
- package/sync/syncDb/constants.ts +25 -23
- package/sync/syncDb/ddl.ts +35 -36
- package/sync/syncDb/helpers.ts +6 -9
- package/sync/syncDb/schema.ts +10 -9
- package/sync/syncDb/sqlite.ts +7 -8
- package/sync/syncDb/table.ts +37 -35
- package/sync/syncDb/tableCreate.ts +21 -20
- package/sync/syncDb/types.ts +23 -20
- package/sync/syncDb/version.ts +10 -10
- package/sync/syncDb.ts +43 -36
- package/sync/syncDev.ts +74 -66
- package/sync/syncMenu.ts +190 -57
- package/tests/api-integration-array-number.test.ts +282 -0
- package/tests/befly-config-env.test.ts +78 -0
- package/tests/cacheHelper.test.ts +135 -104
- package/tests/cacheKeys.test.ts +41 -0
- package/tests/cipher.test.ts +90 -89
- package/tests/dbHelper-advanced.test.ts +140 -134
- package/tests/dbHelper-all-array-types.test.ts +316 -0
- package/tests/dbHelper-array-serialization.test.ts +258 -0
- package/tests/dbHelper-columns.test.ts +56 -55
- package/tests/dbHelper-execute.test.ts +45 -44
- package/tests/dbHelper-joins.test.ts +124 -119
- package/tests/fields-redis-cache.test.ts +29 -27
- package/tests/fields-validate.test.ts +38 -38
- package/tests/getClientIp.test.ts +54 -0
- package/tests/integration.test.ts +69 -67
- package/tests/jwt.test.ts +27 -26
- package/tests/logger.test.ts +267 -34
- package/tests/rateLimit-hook.test.ts +477 -0
- package/tests/redisHelper.test.ts +187 -188
- package/tests/redisKeys.test.ts +6 -73
- package/tests/scanConfig.test.ts +144 -0
- package/tests/sqlBuilder-advanced.test.ts +217 -215
- package/tests/sqlBuilder.test.ts +92 -91
- package/tests/sync-connection.test.ts +29 -29
- package/tests/syncDb-apply.test.ts +97 -96
- package/tests/syncDb-array-number.test.ts +160 -0
- package/tests/syncDb-constants.test.ts +48 -47
- package/tests/syncDb-ddl.test.ts +99 -98
- package/tests/syncDb-helpers.test.ts +29 -28
- package/tests/syncDb-schema.test.ts +61 -60
- package/tests/syncDb-types.test.ts +60 -59
- package/tests/syncMenu-paths.test.ts +68 -0
- package/tests/util.test.ts +42 -41
- package/tests/validator-array-number.test.ts +310 -0
- package/tests/validator-default.test.ts +373 -0
- package/tests/validator.test.ts +271 -266
- package/tsconfig.json +4 -5
- package/types/api.d.ts +7 -12
- package/types/befly.d.ts +60 -13
- package/types/cache.d.ts +8 -4
- package/types/common.d.ts +17 -9
- package/types/context.d.ts +2 -2
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +19 -19
- package/types/hook.d.ts +2 -2
- package/types/jwt.d.ts +118 -0
- package/types/logger.d.ts +30 -0
- package/types/plugin.d.ts +4 -4
- package/types/redis.d.ts +7 -3
- package/types/roleApisCache.ts +23 -0
- package/types/sync.d.ts +10 -10
- package/types/table.d.ts +50 -9
- package/types/validate.d.ts +69 -0
- package/utils/addonHelper.ts +90 -0
- package/utils/arrayKeysToCamel.ts +18 -0
- package/utils/calcPerfTime.ts +13 -0
- package/utils/configTypes.ts +3 -0
- package/utils/cors.ts +19 -0
- package/utils/fieldClear.ts +75 -0
- package/utils/genShortId.ts +12 -0
- package/utils/getClientIp.ts +45 -0
- package/utils/keysToCamel.ts +22 -0
- package/utils/keysToSnake.ts +22 -0
- package/utils/modules.ts +98 -0
- package/utils/pickFields.ts +19 -0
- package/utils/process.ts +56 -0
- package/utils/regex.ts +225 -0
- package/utils/response.ts +115 -0
- package/utils/route.ts +23 -0
- package/utils/scanConfig.ts +142 -0
- package/utils/scanFiles.ts +48 -0
- package/.prettierignore +0 -2
- package/.prettierrc +0 -12
- package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
- package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
- package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
- package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
- package/hooks/requestLogger.ts +0 -84
- package/types/index.ts +0 -24
- package/util.ts +0 -283
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 角色接口权限缓存(Redis)元信息类型
|
|
3
|
+
*
|
|
4
|
+
* 说明:
|
|
5
|
+
* - active/ready 用于全局版本切换与就绪门槛
|
|
6
|
+
* - role meta 用于单角色增量刷新后的可观测性
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type RoleApisCacheMeta = {
|
|
10
|
+
hash: string;
|
|
11
|
+
at: number;
|
|
12
|
+
roles: number;
|
|
13
|
+
members: number;
|
|
14
|
+
/** 可选:用于和 DB 侧的更新时间做一致性对比 */
|
|
15
|
+
updatedAt?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type RoleApisRoleMeta = {
|
|
19
|
+
roleCode: string;
|
|
20
|
+
at: number;
|
|
21
|
+
members: number;
|
|
22
|
+
hash: string;
|
|
23
|
+
};
|
package/types/sync.d.ts
CHANGED
|
@@ -118,7 +118,7 @@ export interface IndexInfo {
|
|
|
118
118
|
* 字段变更接口
|
|
119
119
|
*/
|
|
120
120
|
export interface FieldChange {
|
|
121
|
-
type:
|
|
121
|
+
type: "length" | "datatype" | "comment" | "default" | "nullable";
|
|
122
122
|
current: any;
|
|
123
123
|
expected: any;
|
|
124
124
|
}
|
|
@@ -127,7 +127,7 @@ export interface FieldChange {
|
|
|
127
127
|
* 索引操作接口
|
|
128
128
|
*/
|
|
129
129
|
export interface IndexAction {
|
|
130
|
-
action:
|
|
130
|
+
action: "create" | "drop";
|
|
131
131
|
indexName: string;
|
|
132
132
|
fieldName: string;
|
|
133
133
|
}
|
|
@@ -166,7 +166,7 @@ export interface GlobalStats {
|
|
|
166
166
|
*/
|
|
167
167
|
export interface ParsedFieldRule {
|
|
168
168
|
name: string;
|
|
169
|
-
type:
|
|
169
|
+
type: "string" | "number" | "text" | "array_string" | "array_text";
|
|
170
170
|
min: number | null;
|
|
171
171
|
max: number | null;
|
|
172
172
|
default: any;
|
|
@@ -204,7 +204,7 @@ export interface SyncReportMeta {
|
|
|
204
204
|
timestampStr: string;
|
|
205
205
|
environment: string;
|
|
206
206
|
totalTime: number;
|
|
207
|
-
status:
|
|
207
|
+
status: "success" | "error";
|
|
208
208
|
error?: string;
|
|
209
209
|
}
|
|
210
210
|
|
|
@@ -229,9 +229,9 @@ export interface DatabaseReport {
|
|
|
229
229
|
*/
|
|
230
230
|
export interface TableChangeDetail {
|
|
231
231
|
name: string;
|
|
232
|
-
source:
|
|
232
|
+
source: "app" | "addon";
|
|
233
233
|
addonName?: string;
|
|
234
|
-
action:
|
|
234
|
+
action: "create" | "modify" | "none";
|
|
235
235
|
fields: {
|
|
236
236
|
added: FieldDetail[];
|
|
237
237
|
modified: FieldModification[];
|
|
@@ -263,7 +263,7 @@ export interface FieldModification {
|
|
|
263
263
|
name: string;
|
|
264
264
|
before: FieldDetail;
|
|
265
265
|
after: FieldDetail;
|
|
266
|
-
changeType:
|
|
266
|
+
changeType: "type" | "length" | "default" | "nullable" | "comment";
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
/**
|
|
@@ -358,7 +358,7 @@ export interface MenuTreeNode {
|
|
|
358
358
|
icon?: string;
|
|
359
359
|
sort?: number;
|
|
360
360
|
type?: number;
|
|
361
|
-
action?:
|
|
361
|
+
action?: "created" | "updated" | "none";
|
|
362
362
|
children?: MenuTreeNode[];
|
|
363
363
|
}
|
|
364
364
|
|
|
@@ -391,7 +391,7 @@ export interface MenuDetailWithDiff extends MenuDetail {
|
|
|
391
391
|
*/
|
|
392
392
|
export interface AdminDetail {
|
|
393
393
|
username: string;
|
|
394
|
-
action:
|
|
394
|
+
action: "created" | "updated" | "exists";
|
|
395
395
|
}
|
|
396
396
|
|
|
397
397
|
/**
|
|
@@ -400,5 +400,5 @@ export interface AdminDetail {
|
|
|
400
400
|
export interface RoleDetail {
|
|
401
401
|
name: string;
|
|
402
402
|
permissions: number;
|
|
403
|
-
action:
|
|
403
|
+
action: "created" | "updated" | "exists";
|
|
404
404
|
}
|
package/types/table.d.ts
CHANGED
|
@@ -1,11 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 表类型定义 - 用于增强 DbHelper 泛型推断
|
|
3
|
-
*
|
|
4
|
-
* 基础类型(SystemFields, BaseTable, InsertType, UpdateType, SelectType)
|
|
5
|
-
* 请直接从 befly-shared/types 导入
|
|
6
3
|
*/
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
/**
|
|
6
|
+
* 保留字段(系统自动管理)
|
|
7
|
+
*/
|
|
8
|
+
export type ReservedFields = "id" | "created_at" | "updated_at" | "deleted_at" | "state";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 系统字段(所有表都有的字段)
|
|
12
|
+
*/
|
|
13
|
+
export interface SystemFields {
|
|
14
|
+
/** 主键 ID(雪花 ID) */
|
|
15
|
+
id: number;
|
|
16
|
+
/** 状态:0=已删除, 1=正常, 2=禁用 */
|
|
17
|
+
state: number;
|
|
18
|
+
/** 创建时间(毫秒时间戳) */
|
|
19
|
+
createdAt: number;
|
|
20
|
+
/** 更新时间(毫秒时间戳) */
|
|
21
|
+
updatedAt: number;
|
|
22
|
+
/** 删除时间(毫秒时间戳,软删除时设置) */
|
|
23
|
+
deletedAt: number | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 基础表类型(包含系统字段)
|
|
28
|
+
*/
|
|
29
|
+
export type BaseTable<T extends Record<string, any>> = T & SystemFields;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 插入类型:排除系统自动生成的字段
|
|
33
|
+
*/
|
|
34
|
+
export type InsertType<T> = Omit<T, keyof SystemFields>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 更新类型:所有字段可选,排除不可修改的系统字段
|
|
38
|
+
*/
|
|
39
|
+
export type UpdateType<T> = Partial<Omit<T, "id" | "createdAt" | "updatedAt" | "deletedAt">>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 查询结果类型:完整的表记录
|
|
43
|
+
*/
|
|
44
|
+
export type SelectType<T> = T;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 排除保留字段
|
|
48
|
+
*/
|
|
49
|
+
export type ExcludeReserved<T> = Omit<T, ReservedFields>;
|
|
9
50
|
|
|
10
51
|
// ============================================
|
|
11
52
|
// 数据库表映射接口
|
|
@@ -67,27 +108,27 @@ export type TableUpdateType<K extends TableName> = UpdateType<DatabaseTables[K]>
|
|
|
67
108
|
/**
|
|
68
109
|
* 比较操作符(值类型与字段类型相同)
|
|
69
110
|
*/
|
|
70
|
-
type CompareOperators =
|
|
111
|
+
type CompareOperators = "gt" | "gte" | "lt" | "lte" | "ne" | "not";
|
|
71
112
|
|
|
72
113
|
/**
|
|
73
114
|
* 数组操作符(值类型为字段类型的数组)
|
|
74
115
|
*/
|
|
75
|
-
type ArrayOperators =
|
|
116
|
+
type ArrayOperators = "in" | "nin" | "notIn";
|
|
76
117
|
|
|
77
118
|
/**
|
|
78
119
|
* 字符串操作符(值类型为字符串)
|
|
79
120
|
*/
|
|
80
|
-
type StringOperators =
|
|
121
|
+
type StringOperators = "like" | "notLike";
|
|
81
122
|
|
|
82
123
|
/**
|
|
83
124
|
* 范围操作符(值类型为 [min, max] 元组)
|
|
84
125
|
*/
|
|
85
|
-
type RangeOperators =
|
|
126
|
+
type RangeOperators = "between" | "notBetween";
|
|
86
127
|
|
|
87
128
|
/**
|
|
88
129
|
* 空值操作符(值类型为 boolean)
|
|
89
130
|
*/
|
|
90
|
-
type NullOperators =
|
|
131
|
+
type NullOperators = "null" | "notNull";
|
|
91
132
|
|
|
92
133
|
/**
|
|
93
134
|
* 所有操作符联合类型
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 验证相关类型定义
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 字段类型
|
|
7
|
+
*/
|
|
8
|
+
export type FieldType = "string" | "number" | "text" | "array_string" | "array_text" | "array_number_string" | "array_number_text";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 字段定义类型(对象格式)
|
|
12
|
+
*/
|
|
13
|
+
export interface FieldDefinition {
|
|
14
|
+
/** 字段标签/描述 */
|
|
15
|
+
name: string;
|
|
16
|
+
/** 字段详细说明 */
|
|
17
|
+
detail: string;
|
|
18
|
+
/** 字段类型 */
|
|
19
|
+
type: FieldType;
|
|
20
|
+
/** 最小值/最小长度 */
|
|
21
|
+
min: number | null;
|
|
22
|
+
/** 最大值/最大长度 */
|
|
23
|
+
max: number | null;
|
|
24
|
+
/** 默认值 */
|
|
25
|
+
default: any;
|
|
26
|
+
/** 是否创建索引 */
|
|
27
|
+
index: boolean;
|
|
28
|
+
/** 是否唯一 */
|
|
29
|
+
unique: boolean;
|
|
30
|
+
/** 是否允许为空 */
|
|
31
|
+
nullable: boolean;
|
|
32
|
+
/** 是否无符号(仅 number 类型) */
|
|
33
|
+
unsigned: boolean;
|
|
34
|
+
/** 正则验证 */
|
|
35
|
+
regexp: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 表定义类型(对象格式)
|
|
40
|
+
*/
|
|
41
|
+
export type TableDefinition = Record<string, FieldDefinition>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 验证结果类型
|
|
45
|
+
*/
|
|
46
|
+
export interface ValidateResult {
|
|
47
|
+
/** 状态码:0=通过,1=失败 */
|
|
48
|
+
code: 0 | 1;
|
|
49
|
+
/** 是否失败 */
|
|
50
|
+
failed: boolean;
|
|
51
|
+
/** 第一个错误 */
|
|
52
|
+
firstError: string | null;
|
|
53
|
+
/** 所有错误 */
|
|
54
|
+
errors: string[];
|
|
55
|
+
/** 错误字段列表 */
|
|
56
|
+
errorFields: string[];
|
|
57
|
+
/** 字段错误映射 */
|
|
58
|
+
fieldErrors: Record<string, string>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 单值验证结果
|
|
63
|
+
*/
|
|
64
|
+
export interface SingleResult {
|
|
65
|
+
/** 转换后的值 */
|
|
66
|
+
value: any;
|
|
67
|
+
/** 错误信息(null 表示通过) */
|
|
68
|
+
error: string | null;
|
|
69
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
import { join } from "pathe";
|
|
5
|
+
|
|
6
|
+
import { projectAddonsDir } from "../paths.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 扫描所有可用的 addon
|
|
10
|
+
* 优先从本地 addons/ 目录加载,其次从 node_modules/@befly-addon/ 加载
|
|
11
|
+
* @param cwd - 项目根目录,默认为 process.cwd()
|
|
12
|
+
* @returns addon 名称数组
|
|
13
|
+
*/
|
|
14
|
+
export const scanAddons = (cwd: string = process.cwd()): string[] => {
|
|
15
|
+
const addons = new Set<string>();
|
|
16
|
+
|
|
17
|
+
// 1. 扫描本地 addons 目录(优先级高)
|
|
18
|
+
if (existsSync(projectAddonsDir)) {
|
|
19
|
+
try {
|
|
20
|
+
const localAddons = fs.readdirSync(projectAddonsDir).filter((name) => {
|
|
21
|
+
const fullPath = join(projectAddonsDir, name);
|
|
22
|
+
try {
|
|
23
|
+
const stat = statSync(fullPath);
|
|
24
|
+
return stat.isDirectory() && !name.startsWith("_");
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
localAddons.forEach((name) => addons.add(name));
|
|
30
|
+
} catch {
|
|
31
|
+
// 忽略本地目录读取错误
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. 扫描 node_modules/@befly-addon 目录
|
|
36
|
+
const beflyDir = join(cwd, "node_modules", "@befly-addon");
|
|
37
|
+
if (existsSync(beflyDir)) {
|
|
38
|
+
try {
|
|
39
|
+
const npmAddons = fs.readdirSync(beflyDir).filter((name) => {
|
|
40
|
+
// 如果本地已存在,跳过 npm 包版本
|
|
41
|
+
if (addons.has(name)) return false;
|
|
42
|
+
|
|
43
|
+
const fullPath = join(beflyDir, name);
|
|
44
|
+
try {
|
|
45
|
+
const stat = statSync(fullPath);
|
|
46
|
+
return stat.isDirectory();
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
npmAddons.forEach((name) => addons.add(name));
|
|
52
|
+
} catch {
|
|
53
|
+
// 忽略 npm 目录读取错误
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return Array.from(addons).sort();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 获取 addon 的指定子目录路径
|
|
62
|
+
* 优先返回本地 addons 目录,其次返回 node_modules 目录
|
|
63
|
+
* @param name - addon 名称
|
|
64
|
+
* @param subDir - 子目录名称
|
|
65
|
+
* @param cwd - 项目根目录,默认为 process.cwd()
|
|
66
|
+
* @returns 完整路径
|
|
67
|
+
*/
|
|
68
|
+
export const getAddonDir = (name: string, subDir: string, cwd: string = process.cwd()): string => {
|
|
69
|
+
// 优先使用本地 addons 目录
|
|
70
|
+
// const projectAddonsDir = join(cwd, 'addons');
|
|
71
|
+
// const localPath = join(projectAddonsDir, name, subDir);
|
|
72
|
+
// if (existsSync(localPath)) {
|
|
73
|
+
// return localPath;
|
|
74
|
+
// }
|
|
75
|
+
|
|
76
|
+
// 降级使用 node_modules 目录
|
|
77
|
+
return join(cwd, "node_modules", "@befly-addon", name, subDir);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 检查 addon 子目录是否存在
|
|
82
|
+
* @param name - addon 名称
|
|
83
|
+
* @param subDir - 子目录名称
|
|
84
|
+
* @param cwd - 项目根目录,默认为 process.cwd()
|
|
85
|
+
* @returns 是否存在
|
|
86
|
+
*/
|
|
87
|
+
export const addonDirExists = (name: string, subDir: string, cwd: string = process.cwd()): boolean => {
|
|
88
|
+
const dir = getAddonDir(name, subDir, cwd);
|
|
89
|
+
return existsSync(dir) && statSync(dir).isDirectory();
|
|
90
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { keysToCamel } from "./keysToCamel.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 数组对象字段名批量转小驼峰
|
|
5
|
+
* @param arr - 源数组
|
|
6
|
+
* @returns 字段名转为小驼峰格式的新数组
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* arrayKeysToCamel([
|
|
10
|
+
* { user_id: 1, user_name: 'John' },
|
|
11
|
+
* { user_id: 2, user_name: 'Jane' }
|
|
12
|
+
* ])
|
|
13
|
+
* // [{ userId: 1, userName: 'John' }, { userId: 2, userName: 'Jane' }]
|
|
14
|
+
*/
|
|
15
|
+
export const arrayKeysToCamel = <T = any>(arr: Record<string, any>[]): T[] => {
|
|
16
|
+
if (!arr || !Array.isArray(arr)) return arr as T[];
|
|
17
|
+
return arr.map((item) => keysToCamel<T>(item));
|
|
18
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 计算性能时间差
|
|
3
|
+
*/
|
|
4
|
+
export const calcPerfTime = (startTime: number, endTime: number = Bun.nanoseconds()): string => {
|
|
5
|
+
const elapsedMs = (endTime - startTime) / 1_000_000;
|
|
6
|
+
|
|
7
|
+
if (elapsedMs < 1000) {
|
|
8
|
+
return `${elapsedMs.toFixed(2)} 毫秒`;
|
|
9
|
+
} else {
|
|
10
|
+
const elapsedSeconds = elapsedMs / 1000;
|
|
11
|
+
return `${elapsedSeconds.toFixed(2)} 秒`;
|
|
12
|
+
}
|
|
13
|
+
};
|
package/utils/cors.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CorsConfig } from "../types/befly.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 设置 CORS 响应头
|
|
5
|
+
* @param req - 请求对象
|
|
6
|
+
* @param config - CORS 配置(可选)
|
|
7
|
+
* @returns CORS 响应头对象
|
|
8
|
+
*/
|
|
9
|
+
export function setCorsOptions(req: Request, config: CorsConfig = {}): Record<string, string> {
|
|
10
|
+
const origin = config.origin || "*";
|
|
11
|
+
return {
|
|
12
|
+
"Access-Control-Allow-Origin": origin === "*" ? req.headers.get("origin") || "*" : origin,
|
|
13
|
+
"Access-Control-Allow-Methods": config.methods || "GET, POST, PUT, DELETE, OPTIONS",
|
|
14
|
+
"Access-Control-Allow-Headers": config.allowedHeaders || "Content-Type, Authorization, authorization, token",
|
|
15
|
+
"Access-Control-Expose-Headers": config.exposedHeaders || "Content-Range, X-Content-Range, Authorization, authorization, token",
|
|
16
|
+
"Access-Control-Max-Age": String(config.maxAge || 86400),
|
|
17
|
+
"Access-Control-Allow-Credentials": config.credentials || "true"
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// fieldClear 工具函数实现
|
|
2
|
+
// 支持 pick/omit/keepValues/excludeValues,处理对象和数组
|
|
3
|
+
|
|
4
|
+
export interface FieldClearOptions {
|
|
5
|
+
pickKeys?: string[]; // 只保留这些字段
|
|
6
|
+
omitKeys?: string[]; // 排除这些字段
|
|
7
|
+
keepValues?: any[]; // 只保留这些值
|
|
8
|
+
excludeValues?: any[]; // 排除这些值
|
|
9
|
+
keepMap?: Record<string, any>; // 强制保留的键值对(优先级最高,忽略 excludeValues)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type FieldClearResult<T> = T extends Array<infer U> ? Array<FieldClearResult<U>> : T extends object ? { [K in keyof T]?: T[K] } : T;
|
|
13
|
+
|
|
14
|
+
function isObject(val: unknown): val is Record<string, any> {
|
|
15
|
+
return val !== null && typeof val === "object" && !Array.isArray(val);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isArray(val: unknown): val is any[] {
|
|
19
|
+
return Array.isArray(val);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function fieldClear<T = any>(data: T | T[], options: FieldClearOptions = {}): FieldClearResult<T> {
|
|
23
|
+
const { pickKeys, omitKeys, keepValues, excludeValues, keepMap } = options;
|
|
24
|
+
|
|
25
|
+
const filterObj = (obj: Record<string, any>) => {
|
|
26
|
+
let result: Record<string, any> = {};
|
|
27
|
+
let keys = Object.keys(obj);
|
|
28
|
+
if (pickKeys && pickKeys.length) {
|
|
29
|
+
keys = keys.filter((k) => pickKeys.includes(k));
|
|
30
|
+
}
|
|
31
|
+
if (omitKeys && omitKeys.length) {
|
|
32
|
+
keys = keys.filter((k) => !omitKeys.includes(k));
|
|
33
|
+
}
|
|
34
|
+
for (const key of keys) {
|
|
35
|
+
const value = obj[key];
|
|
36
|
+
|
|
37
|
+
// 1. 优先检查 keepMap
|
|
38
|
+
if (keepMap && key in keepMap) {
|
|
39
|
+
if (Object.is(keepMap[key], value)) {
|
|
40
|
+
result[key] = value;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. 检查 keepValues (只保留指定值)
|
|
46
|
+
if (keepValues && keepValues.length && !keepValues.includes(value)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. 检查 excludeValues (排除指定值)
|
|
51
|
+
if (excludeValues && excludeValues.length && excludeValues.includes(value)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
result[key] = value;
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (isArray(data)) {
|
|
60
|
+
return (data as any[])
|
|
61
|
+
.map((item) => (isObject(item) ? filterObj(item) : item))
|
|
62
|
+
.filter((item) => {
|
|
63
|
+
if (isObject(item)) {
|
|
64
|
+
// 只保留有内容的对象
|
|
65
|
+
return Object.keys(item).length > 0;
|
|
66
|
+
}
|
|
67
|
+
// 原始值直接保留
|
|
68
|
+
return true;
|
|
69
|
+
}) as FieldClearResult<T>;
|
|
70
|
+
}
|
|
71
|
+
if (isObject(data)) {
|
|
72
|
+
return filterObj(data as Record<string, any>) as FieldClearResult<T>;
|
|
73
|
+
}
|
|
74
|
+
return data as FieldClearResult<T>;
|
|
75
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 生成短 ID
|
|
3
|
+
* 由时间戳(base36)+ 随机字符组成,约 13 位
|
|
4
|
+
* - 前 8 位:时间戳(可排序)
|
|
5
|
+
* - 后 5 位:随机字符(防冲突)
|
|
6
|
+
* @returns 短 ID 字符串
|
|
7
|
+
* @example
|
|
8
|
+
* genShortId() // "lxyz1a2b3c4"
|
|
9
|
+
*/
|
|
10
|
+
export function genShortId(): string {
|
|
11
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
|
12
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 获取客户端 IP(优先代理头,其次 Bun server.requestIP 兜底)
|
|
3
|
+
*
|
|
4
|
+
* 注意:目前策略是“尽量取到 IP”,未做 trustProxy 防伪造控制。
|
|
5
|
+
*/
|
|
6
|
+
export function getClientIp(req: Request, server?: any): string {
|
|
7
|
+
// 1) 代理/网关常见头(优先取)
|
|
8
|
+
const xForwardedFor = req.headers.get("x-forwarded-for");
|
|
9
|
+
if (typeof xForwardedFor === "string" && xForwardedFor.trim()) {
|
|
10
|
+
const first = xForwardedFor.split(",")[0];
|
|
11
|
+
if (typeof first === "string" && first.trim()) {
|
|
12
|
+
return first.trim();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const xRealIp = req.headers.get("x-real-ip");
|
|
17
|
+
if (typeof xRealIp === "string" && xRealIp.trim()) {
|
|
18
|
+
return xRealIp.trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const cfConnectingIp = req.headers.get("cf-connecting-ip");
|
|
22
|
+
if (typeof cfConnectingIp === "string" && cfConnectingIp.trim()) {
|
|
23
|
+
return cfConnectingIp.trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const xClientIp = req.headers.get("x-client-ip");
|
|
27
|
+
if (typeof xClientIp === "string" && xClientIp.trim()) {
|
|
28
|
+
return xClientIp.trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const trueClientIp = req.headers.get("true-client-ip");
|
|
32
|
+
if (typeof trueClientIp === "string" && trueClientIp.trim()) {
|
|
33
|
+
return trueClientIp.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2) 连接层兜底:Bun server.requestIP(req)
|
|
37
|
+
if (server && typeof server.requestIP === "function") {
|
|
38
|
+
const ipInfo = server.requestIP(req);
|
|
39
|
+
if (ipInfo && typeof ipInfo.address === "string" && ipInfo.address.trim()) {
|
|
40
|
+
return ipInfo.address.trim();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return "unknown";
|
|
45
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isPlainObject } from "es-toolkit/compat";
|
|
2
|
+
import { camelCase } from "es-toolkit/string";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 对象字段名转小驼峰
|
|
6
|
+
* @param obj - 源对象
|
|
7
|
+
* @returns 字段名转为小驼峰格式的新对象
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* keysToCamel({ user_id: 123, user_name: 'John' }) // { userId: 123, userName: 'John' }
|
|
11
|
+
* keysToCamel({ created_at: 1697452800000 }) // { createdAt: 1697452800000 }
|
|
12
|
+
*/
|
|
13
|
+
export const keysToCamel = <T = any>(obj: Record<string, any>): T => {
|
|
14
|
+
if (!obj || !isPlainObject(obj)) return obj as T;
|
|
15
|
+
|
|
16
|
+
const result: any = {};
|
|
17
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
18
|
+
const camelKey = camelCase(key);
|
|
19
|
+
result[camelKey] = value;
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isPlainObject } from "es-toolkit/compat";
|
|
2
|
+
import { snakeCase } from "es-toolkit/string";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 对象字段名转下划线
|
|
6
|
+
* @param obj - 源对象
|
|
7
|
+
* @returns 字段名转为下划线格式的新对象
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* keysToSnake({ userId: 123, userName: 'John' }) // { user_id: 123, user_name: 'John' }
|
|
11
|
+
* keysToSnake({ createdAt: 1697452800000 }) // { created_at: 1697452800000 }
|
|
12
|
+
*/
|
|
13
|
+
export const keysToSnake = <T = any>(obj: Record<string, any>): T => {
|
|
14
|
+
if (!obj || !isPlainObject(obj)) return obj as T;
|
|
15
|
+
|
|
16
|
+
const result: any = {};
|
|
17
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
18
|
+
const snakeKey = snakeCase(key);
|
|
19
|
+
result[snakeKey] = value;
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
};
|