befly 3.8.29 → 3.8.31
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 +91 -6
- package/checks/checkApi.ts +2 -1
- package/checks/checkApp.ts +31 -1
- package/checks/checkTable.ts +3 -2
- package/hooks/cors.ts +3 -3
- package/hooks/parser.ts +8 -6
- package/hooks/permission.ts +12 -5
- package/hooks/validator.ts +1 -1
- package/lib/cacheHelper.ts +73 -65
- package/lib/cipher.ts +2 -1
- package/lib/connect.ts +23 -52
- package/lib/dbHelper.ts +14 -11
- package/lib/jwt.ts +58 -437
- package/lib/logger.ts +76 -197
- package/lib/redisHelper.ts +163 -1
- package/lib/sqlBuilder.ts +2 -1
- package/lib/validator.ts +150 -384
- package/loader/loadApis.ts +4 -7
- package/loader/loadHooks.ts +6 -5
- package/loader/loadPlugins.ts +11 -13
- package/main.ts +26 -53
- package/package.json +10 -8
- package/paths.ts +0 -6
- package/plugins/cipher.ts +1 -1
- package/plugins/config.ts +3 -4
- package/plugins/db.ts +6 -7
- package/plugins/jwt.ts +7 -6
- package/plugins/logger.ts +6 -6
- package/plugins/redis.ts +9 -13
- package/router/api.ts +2 -2
- package/router/static.ts +4 -8
- package/sync/syncAll.ts +8 -13
- package/sync/syncApi.ts +14 -10
- package/sync/syncDb/apply.ts +1 -2
- package/sync/syncDb.ts +12 -15
- package/sync/syncDev.ts +19 -56
- package/sync/syncMenu.ts +182 -137
- package/tests/cacheHelper.test.ts +327 -0
- package/tests/dbHelper-columns.test.ts +5 -20
- package/tests/dbHelper-execute.test.ts +14 -68
- package/tests/fields-redis-cache.test.ts +5 -3
- package/tests/integration.test.ts +17 -32
- package/tests/jwt.test.ts +36 -94
- package/tests/logger.test.ts +32 -34
- package/tests/redisHelper.test.ts +271 -2
- package/tests/redisKeys.test.ts +76 -0
- package/tests/sync-connection.test.ts +0 -6
- package/tests/syncDb-constants.test.ts +12 -12
- package/tests/util.test.ts +5 -1
- package/tests/validator.test.ts +611 -85
- package/types/befly.d.ts +9 -15
- package/types/cache.d.ts +73 -0
- package/types/common.d.ts +10 -128
- package/types/database.d.ts +221 -5
- package/types/index.ts +6 -5
- package/types/plugin.d.ts +1 -4
- package/types/redis.d.ts +37 -2
- package/types/table.d.ts +175 -0
- package/config.ts +0 -70
- package/hooks/_rateLimit.ts +0 -64
- package/lib/regexAliases.ts +0 -59
- package/lib/xml.ts +0 -383
- package/tests/validator-advanced.test.ts +0 -653
- package/tests/xml.test.ts +0 -101
- package/types/addon.d.ts +0 -50
- package/types/crypto.d.ts +0 -23
- package/types/jwt.d.ts +0 -99
- package/types/logger.d.ts +0 -43
- package/types/tool.d.ts +0 -67
- package/types/validator.d.ts +0 -43
package/types/table.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表类型定义 - 用于增强 DbHelper 泛型推断
|
|
3
|
+
*
|
|
4
|
+
* 基础类型(SystemFields, BaseTable, InsertType, UpdateType, SelectType)
|
|
5
|
+
* 请直接从 befly-shared/types 导入
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { BaseTable, InsertType, UpdateType } from 'befly-shared/types';
|
|
9
|
+
|
|
10
|
+
// ============================================
|
|
11
|
+
// 数据库表映射接口
|
|
12
|
+
// ============================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 数据库表映射
|
|
16
|
+
* 用户可以在项目中扩展此接口来添加表类型
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* // 在项目的 types/index.d.ts 中扩展
|
|
21
|
+
* declare module 'befly/types/table' {
|
|
22
|
+
* interface DatabaseTables {
|
|
23
|
+
* user: BaseTable<{
|
|
24
|
+
* email: string;
|
|
25
|
+
* username: string;
|
|
26
|
+
* password: string;
|
|
27
|
+
* nickname: string | null;
|
|
28
|
+
* }>;
|
|
29
|
+
* article: BaseTable<{
|
|
30
|
+
* title: string;
|
|
31
|
+
* content: string;
|
|
32
|
+
* authorId: number;
|
|
33
|
+
* }>;
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export interface DatabaseTables {
|
|
39
|
+
// 用户通过模块扩展添加表类型
|
|
40
|
+
[tableName: string]: BaseTable<Record<string, any>>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 表名类型(从 DatabaseTables 提取所有键)
|
|
45
|
+
*/
|
|
46
|
+
export type TableName = keyof DatabaseTables;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 根据表名获取表类型
|
|
50
|
+
*/
|
|
51
|
+
export type TableType<K extends TableName> = DatabaseTables[K];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 根据表名获取插入类型
|
|
55
|
+
*/
|
|
56
|
+
export type TableInsertType<K extends TableName> = InsertType<DatabaseTables[K]>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 根据表名获取更新类型
|
|
60
|
+
*/
|
|
61
|
+
export type TableUpdateType<K extends TableName> = UpdateType<DatabaseTables[K]>;
|
|
62
|
+
|
|
63
|
+
// ============================================
|
|
64
|
+
// WHERE 条件类型增强
|
|
65
|
+
// ============================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 比较操作符(值类型与字段类型相同)
|
|
69
|
+
*/
|
|
70
|
+
type CompareOperators = 'gt' | 'gte' | 'lt' | 'lte' | 'ne' | 'not';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 数组操作符(值类型为字段类型的数组)
|
|
74
|
+
*/
|
|
75
|
+
type ArrayOperators = 'in' | 'nin' | 'notIn';
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 字符串操作符(值类型为字符串)
|
|
79
|
+
*/
|
|
80
|
+
type StringOperators = 'like' | 'notLike';
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 范围操作符(值类型为 [min, max] 元组)
|
|
84
|
+
*/
|
|
85
|
+
type RangeOperators = 'between' | 'notBetween';
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 空值操作符(值类型为 boolean)
|
|
89
|
+
*/
|
|
90
|
+
type NullOperators = 'null' | 'notNull';
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 所有操作符联合类型
|
|
94
|
+
*/
|
|
95
|
+
export type FieldOperator = `$${CompareOperators}` | `$${ArrayOperators}` | `$${StringOperators}` | `$${RangeOperators}` | `$${NullOperators}`;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 比较操作符条件(值类型与字段类型相同)
|
|
99
|
+
* @example { userId$gt: 10, age$lte: 65 }
|
|
100
|
+
*/
|
|
101
|
+
type CompareConditions<T> = {
|
|
102
|
+
[K in keyof T as `${K & string}$${CompareOperators}`]?: T[K];
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 数组操作符条件(值类型为字段类型的数组)
|
|
107
|
+
* @example { status$in: [1, 2, 3], roleId$nin: [100, 200] }
|
|
108
|
+
*/
|
|
109
|
+
type ArrayConditions<T> = {
|
|
110
|
+
[K in keyof T as `${K & string}$${ArrayOperators}`]?: T[K][];
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 字符串操作符条件(值类型为字符串)
|
|
115
|
+
* @example { name$like: '%test%', email$notLike: '%spam%' }
|
|
116
|
+
*/
|
|
117
|
+
type StringConditions<T> = {
|
|
118
|
+
[K in keyof T as `${K & string}$${StringOperators}`]?: string;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 范围操作符条件(值类型为 [min, max] 元组)
|
|
123
|
+
* @example { age$between: [18, 65], createdAt$notBetween: [start, end] }
|
|
124
|
+
*/
|
|
125
|
+
type RangeConditions<T> = {
|
|
126
|
+
[K in keyof T as `${K & string}$${RangeOperators}`]?: [T[K], T[K]];
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 空值操作符条件(值类型为 true)
|
|
131
|
+
* @example { deletedAt$null: true, avatar$notNull: true }
|
|
132
|
+
*/
|
|
133
|
+
type NullConditions<T> = {
|
|
134
|
+
[K in keyof T as `${K & string}$${NullOperators}`]?: true;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 类型安全的 WHERE 条件
|
|
139
|
+
*
|
|
140
|
+
* 支持的操作符:
|
|
141
|
+
* - 比较:$gt, $gte, $lt, $lte, $ne, $not(值类型与字段相同)
|
|
142
|
+
* - 数组:$in, $nin, $notIn(值类型为数组)
|
|
143
|
+
* - 字符串:$like, $notLike(值类型为字符串)
|
|
144
|
+
* - 范围:$between, $notBetween(值类型为 [min, max] 元组)
|
|
145
|
+
* - 空值:$null, $notNull(值类型为 true)
|
|
146
|
+
* - 逻辑:$or, $and(递归条件数组)
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```typescript
|
|
150
|
+
* const where: TypedWhereConditions<UserTable> = {
|
|
151
|
+
* state: 1, // 精确匹配
|
|
152
|
+
* age$gte: 18, // 大于等于
|
|
153
|
+
* roleId$in: [1, 2, 3], // IN 数组
|
|
154
|
+
* name$like: '%test%', // LIKE 匹配
|
|
155
|
+
* createdAt$between: [start, end], // BETWEEN 范围
|
|
156
|
+
* deletedAt$null: true, // IS NULL
|
|
157
|
+
* $or: [ // OR 条件
|
|
158
|
+
* { email$like: '%@gmail.com' },
|
|
159
|
+
* { email$like: '%@qq.com' }
|
|
160
|
+
* ]
|
|
161
|
+
* };
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export type TypedWhereConditions<T> = Partial<T> & // 精确匹配
|
|
165
|
+
CompareConditions<T> & // 比较操作符
|
|
166
|
+
ArrayConditions<T> & // 数组操作符
|
|
167
|
+
StringConditions<T> & // 字符串操作符
|
|
168
|
+
RangeConditions<T> & // 范围操作符
|
|
169
|
+
NullConditions<T> & {
|
|
170
|
+
// 空值操作符
|
|
171
|
+
/** OR 条件组 */
|
|
172
|
+
$or?: TypedWhereConditions<T>[];
|
|
173
|
+
/** AND 条件组 */
|
|
174
|
+
$and?: TypedWhereConditions<T>[];
|
|
175
|
+
};
|
package/config.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Befly 默认配置
|
|
3
|
-
* 包含所有配置项的默认值
|
|
4
|
-
*/
|
|
5
|
-
import type { BeflyOptions } from './types/befly.js';
|
|
6
|
-
|
|
7
|
-
export const defaultOptions: Required<Omit<BeflyOptions, 'devPassword'>> = {
|
|
8
|
-
// ========== 核心参数 ==========
|
|
9
|
-
nodeEnv: (process.env.NODE_ENV as any) || 'development',
|
|
10
|
-
appName: '野蜂飞舞',
|
|
11
|
-
appPort: 3000,
|
|
12
|
-
appHost: '127.0.0.1',
|
|
13
|
-
devEmail: 'dev@qq.com',
|
|
14
|
-
devPassword: 'beflydev123456',
|
|
15
|
-
bodyLimit: 1048576, // 1MB
|
|
16
|
-
tz: 'Asia/Shanghai',
|
|
17
|
-
|
|
18
|
-
// ========== 日志配置 ==========
|
|
19
|
-
logger: {
|
|
20
|
-
debug: 1,
|
|
21
|
-
excludeFields: 'password,token,secret',
|
|
22
|
-
dir: './logs',
|
|
23
|
-
console: 1,
|
|
24
|
-
maxSize: 10485760 // 10MB
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
// ========== 数据库配置 ==========
|
|
28
|
-
db: {
|
|
29
|
-
type: 'mysql',
|
|
30
|
-
host: '127.0.0.1',
|
|
31
|
-
port: 3306,
|
|
32
|
-
username: 'root',
|
|
33
|
-
password: 'root',
|
|
34
|
-
database: 'befly_demo',
|
|
35
|
-
poolMax: 10
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
// ========== Redis 配置 ==========
|
|
39
|
-
redis: {
|
|
40
|
-
host: '127.0.0.1',
|
|
41
|
-
port: 6379,
|
|
42
|
-
username: '',
|
|
43
|
-
password: '',
|
|
44
|
-
db: 0,
|
|
45
|
-
prefix: 'befly_demo:'
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
// ========== 认证配置 ==========
|
|
49
|
-
auth: {
|
|
50
|
-
secret: 'befly-secret',
|
|
51
|
-
expiresIn: '7d',
|
|
52
|
-
algorithm: 'HS256'
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
// ========== CORS 配置 ==========
|
|
56
|
-
cors: {
|
|
57
|
-
origin: '*',
|
|
58
|
-
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
|
59
|
-
allowedHeaders: 'Content-Type,Authorization',
|
|
60
|
-
exposedHeaders: '',
|
|
61
|
-
maxAge: 86400,
|
|
62
|
-
credentials: 'true'
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
// ========== 禁用配置 ==========
|
|
66
|
-
/** 禁用的钩子列表 */
|
|
67
|
-
disableHooks: [],
|
|
68
|
-
/** 禁用的插件列表 */
|
|
69
|
-
disablePlugins: []
|
|
70
|
-
};
|
package/hooks/_rateLimit.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
// 相对导入
|
|
2
|
-
import { Logger } from '../lib/logger.js';
|
|
3
|
-
import { ErrorResponse } from '../util.js';
|
|
4
|
-
|
|
5
|
-
// 类型导入
|
|
6
|
-
import type { Hook } from '../types/hook.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 接口限流插件
|
|
10
|
-
*
|
|
11
|
-
* 功能:
|
|
12
|
-
* 1. 基于 Redis 实现滑动窗口或固定窗口限流
|
|
13
|
-
* 2. 支持配置格式 "count/seconds" (e.g. "10/60")
|
|
14
|
-
* 3. 针对每个用户(userId)或IP进行限制
|
|
15
|
-
*/
|
|
16
|
-
const hook: Hook = {
|
|
17
|
-
order: 7,
|
|
18
|
-
handler: async (befly, ctx) => {
|
|
19
|
-
const { api } = ctx;
|
|
20
|
-
|
|
21
|
-
// 1. 检查配置
|
|
22
|
-
if (!api || !api.rateLimit || !befly.redis) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// 2. 解析配置 "10/60" -> count=10, seconds=60
|
|
27
|
-
const [countStr, secondsStr] = api.rateLimit.split('/');
|
|
28
|
-
const limitCount = parseInt(countStr, 10);
|
|
29
|
-
const limitSeconds = parseInt(secondsStr, 10);
|
|
30
|
-
|
|
31
|
-
if (isNaN(limitCount) || isNaN(limitSeconds)) {
|
|
32
|
-
Logger.warn(`[RateLimit] Invalid config: ${api.rateLimit}`);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// 3. 生成 Key
|
|
37
|
-
// 优先使用 userId,否则使用 IP
|
|
38
|
-
const identifier = ctx.user?.id || ctx.ip || 'unknown';
|
|
39
|
-
const apiPath = ctx.route || `${ctx.req.method}${new URL(ctx.req.url).pathname}`;
|
|
40
|
-
const key = `rate_limit:${apiPath}:${identifier}`;
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
// 4. 执行限流逻辑 (使用 Redis INCR)
|
|
44
|
-
// 这是一个简单的固定窗口算法,对于严格场景可能需要 Lua 脚本实现滑动窗口
|
|
45
|
-
const current = await befly.redis.incr(key);
|
|
46
|
-
|
|
47
|
-
// 5. 设置过期时间 (如果是新 Key)
|
|
48
|
-
if (current === 1) {
|
|
49
|
-
await befly.redis.expire(key, limitSeconds);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// 6. 判断是否超限
|
|
53
|
-
if (current > limitCount) {
|
|
54
|
-
ctx.response = ErrorResponse(ctx, '请求过于频繁,请稍后再试');
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
} catch (err) {
|
|
58
|
-
Logger.error('[RateLimit] Redis error:', err);
|
|
59
|
-
// Redis 故障时,默认放行,避免阻塞业务
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export default hook;
|
package/lib/regexAliases.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 内置正则表达式别名(Befly 项目专用)
|
|
3
|
-
*/
|
|
4
|
-
export const RegexAliases = {
|
|
5
|
-
// 数字类型
|
|
6
|
-
number: '^\\d+$',
|
|
7
|
-
integer: '^-?\\d+$',
|
|
8
|
-
float: '^-?\\d+(\\.\\d+)?$',
|
|
9
|
-
positive: '^[1-9]\\d*$',
|
|
10
|
-
negative: '^-\\d+$',
|
|
11
|
-
zero: '^0$',
|
|
12
|
-
|
|
13
|
-
// 字符串类型
|
|
14
|
-
word: '^[a-zA-Z]+$',
|
|
15
|
-
alphanumeric: '^[a-zA-Z0-9]+$',
|
|
16
|
-
alphanumeric_: '^[a-zA-Z0-9_]+$',
|
|
17
|
-
lowercase: '^[a-z]+$',
|
|
18
|
-
uppercase: '^[A-Z]+$',
|
|
19
|
-
|
|
20
|
-
// 中文
|
|
21
|
-
chinese: '^[\\u4e00-\\u9fa5]+$',
|
|
22
|
-
chinese_word: '^[\\u4e00-\\u9fa5a-zA-Z]+$',
|
|
23
|
-
|
|
24
|
-
// 常用格式
|
|
25
|
-
email: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
|
|
26
|
-
phone: '^1[3-9]\\d{9}$',
|
|
27
|
-
url: '^https?://',
|
|
28
|
-
ip: '^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$',
|
|
29
|
-
domain: '^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$',
|
|
30
|
-
|
|
31
|
-
// 特殊格式
|
|
32
|
-
uuid: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
|
33
|
-
hex: '^[0-9a-fA-F]+$',
|
|
34
|
-
base64: '^[A-Za-z0-9+/=]+$',
|
|
35
|
-
md5: '^[a-f0-9]{32}$',
|
|
36
|
-
sha1: '^[a-f0-9]{40}$',
|
|
37
|
-
sha256: '^[a-f0-9]{64}$',
|
|
38
|
-
|
|
39
|
-
// 日期时间
|
|
40
|
-
date: '^\\d{4}-\\d{2}-\\d{2}$',
|
|
41
|
-
time: '^\\d{2}:\\d{2}:\\d{2}$',
|
|
42
|
-
datetime: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}',
|
|
43
|
-
year: '^\\d{4}$',
|
|
44
|
-
month: '^(0[1-9]|1[0-2])$',
|
|
45
|
-
day: '^(0[1-9]|[12]\\d|3[01])$',
|
|
46
|
-
|
|
47
|
-
// 代码相关
|
|
48
|
-
variable: '^[a-zA-Z_][a-zA-Z0-9_]*$',
|
|
49
|
-
constant: '^[A-Z][A-Z0-9_]*$',
|
|
50
|
-
package: '^[a-z][a-z0-9-]*$',
|
|
51
|
-
|
|
52
|
-
// 证件相关
|
|
53
|
-
id_card: '^\\d{17}[\\dXx]$',
|
|
54
|
-
passport: '^[a-zA-Z0-9]{5,17}$',
|
|
55
|
-
|
|
56
|
-
// 空值
|
|
57
|
-
empty: '^$',
|
|
58
|
-
notempty: '.+'
|
|
59
|
-
} as const;
|