befly 3.8.29 → 3.8.30
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 +8 -6
- package/checks/checkApi.ts +2 -1
- package/checks/checkTable.ts +3 -2
- package/hooks/parser.ts +5 -3
- package/hooks/permission.ts +12 -5
- package/lib/cacheHelper.ts +76 -62
- package/lib/connect.ts +8 -35
- 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 +9 -8
- package/loader/loadApis.ts +4 -7
- package/loader/loadHooks.ts +2 -2
- package/loader/loadPlugins.ts +4 -4
- package/main.ts +4 -17
- package/package.json +9 -8
- package/paths.ts +0 -6
- package/plugins/db.ts +2 -2
- package/plugins/jwt.ts +5 -5
- package/plugins/redis.ts +1 -1
- package/router/api.ts +2 -2
- package/router/static.ts +1 -2
- package/sync/syncAll.ts +2 -2
- package/sync/syncApi.ts +10 -7
- package/sync/syncDb/apply.ts +1 -2
- package/sync/syncDb.ts +6 -10
- package/sync/syncDev.ts +10 -48
- package/sync/syncMenu.ts +11 -8
- 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 +15 -26
- package/tests/jwt.test.ts +36 -94
- package/tests/logger.test.ts +32 -34
- package/tests/redisHelper.test.ts +270 -0
- 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/types/befly.d.ts +2 -15
- package/types/common.d.ts +11 -93
- package/types/database.d.ts +216 -5
- package/types/index.ts +1 -0
- package/types/logger.d.ts +11 -41
- package/types/table.d.ts +213 -0
- package/hooks/_rateLimit.ts +0 -64
- package/lib/regexAliases.ts +0 -59
- package/lib/xml.ts +0 -383
- package/tests/xml.test.ts +0 -101
package/types/table.d.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表类型定义 - 用于增强 DbHelper 泛型推断
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ============================================
|
|
6
|
+
// 基础表类型
|
|
7
|
+
// ============================================
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 系统字段(所有表都有的字段)
|
|
11
|
+
*/
|
|
12
|
+
export interface SystemFields {
|
|
13
|
+
/** 主键 ID(雪花 ID) */
|
|
14
|
+
id: number;
|
|
15
|
+
/** 状态:0=已删除, 1=正常, 2=禁用 */
|
|
16
|
+
state: number;
|
|
17
|
+
/** 创建时间(毫秒时间戳) */
|
|
18
|
+
createdAt: number;
|
|
19
|
+
/** 更新时间(毫秒时间戳) */
|
|
20
|
+
updatedAt: number;
|
|
21
|
+
/** 删除时间(毫秒时间戳,软删除时设置) */
|
|
22
|
+
deletedAt: number | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 基础表类型(包含系统字段)
|
|
27
|
+
*/
|
|
28
|
+
export type BaseTable<T extends Record<string, any>> = T & SystemFields;
|
|
29
|
+
|
|
30
|
+
// ============================================
|
|
31
|
+
// 表操作类型工具
|
|
32
|
+
// ============================================
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 插入类型:排除系统自动生成的字段
|
|
36
|
+
*/
|
|
37
|
+
export type InsertType<T> = Omit<T, keyof SystemFields>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 更新类型:所有字段可选,排除不可修改的系统字段
|
|
41
|
+
*/
|
|
42
|
+
export type UpdateType<T> = Partial<Omit<T, 'id' | 'createdAt' | 'updatedAt' | 'deletedAt'>>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 查询结果类型:完整的表记录
|
|
46
|
+
*/
|
|
47
|
+
export type SelectType<T> = T;
|
|
48
|
+
|
|
49
|
+
// ============================================
|
|
50
|
+
// 数据库表映射接口
|
|
51
|
+
// ============================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 数据库表映射
|
|
55
|
+
* 用户可以在项目中扩展此接口来添加表类型
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* // 在项目的 types/index.d.ts 中扩展
|
|
60
|
+
* declare module 'befly/types/table' {
|
|
61
|
+
* interface DatabaseTables {
|
|
62
|
+
* user: BaseTable<{
|
|
63
|
+
* email: string;
|
|
64
|
+
* username: string;
|
|
65
|
+
* password: string;
|
|
66
|
+
* nickname: string | null;
|
|
67
|
+
* }>;
|
|
68
|
+
* article: BaseTable<{
|
|
69
|
+
* title: string;
|
|
70
|
+
* content: string;
|
|
71
|
+
* authorId: number;
|
|
72
|
+
* }>;
|
|
73
|
+
* }
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export interface DatabaseTables {
|
|
78
|
+
// 用户通过模块扩展添加表类型
|
|
79
|
+
[tableName: string]: BaseTable<Record<string, any>>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 表名类型(从 DatabaseTables 提取所有键)
|
|
84
|
+
*/
|
|
85
|
+
export type TableName = keyof DatabaseTables;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 根据表名获取表类型
|
|
89
|
+
*/
|
|
90
|
+
export type TableType<K extends TableName> = DatabaseTables[K];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 根据表名获取插入类型
|
|
94
|
+
*/
|
|
95
|
+
export type TableInsertType<K extends TableName> = InsertType<DatabaseTables[K]>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 根据表名获取更新类型
|
|
99
|
+
*/
|
|
100
|
+
export type TableUpdateType<K extends TableName> = UpdateType<DatabaseTables[K]>;
|
|
101
|
+
|
|
102
|
+
// ============================================
|
|
103
|
+
// WHERE 条件类型增强
|
|
104
|
+
// ============================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 比较操作符(值类型与字段类型相同)
|
|
108
|
+
*/
|
|
109
|
+
type CompareOperators = 'gt' | 'gte' | 'lt' | 'lte' | 'ne' | 'not';
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 数组操作符(值类型为字段类型的数组)
|
|
113
|
+
*/
|
|
114
|
+
type ArrayOperators = 'in' | 'nin' | 'notIn';
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 字符串操作符(值类型为字符串)
|
|
118
|
+
*/
|
|
119
|
+
type StringOperators = 'like' | 'notLike';
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 范围操作符(值类型为 [min, max] 元组)
|
|
123
|
+
*/
|
|
124
|
+
type RangeOperators = 'between' | 'notBetween';
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 空值操作符(值类型为 boolean)
|
|
128
|
+
*/
|
|
129
|
+
type NullOperators = 'null' | 'notNull';
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 所有操作符联合类型
|
|
133
|
+
*/
|
|
134
|
+
export type FieldOperator = `$${CompareOperators}` | `$${ArrayOperators}` | `$${StringOperators}` | `$${RangeOperators}` | `$${NullOperators}`;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 比较操作符条件(值类型与字段类型相同)
|
|
138
|
+
* @example { userId$gt: 10, age$lte: 65 }
|
|
139
|
+
*/
|
|
140
|
+
type CompareConditions<T> = {
|
|
141
|
+
[K in keyof T as `${K & string}$${CompareOperators}`]?: T[K];
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 数组操作符条件(值类型为字段类型的数组)
|
|
146
|
+
* @example { status$in: [1, 2, 3], roleId$nin: [100, 200] }
|
|
147
|
+
*/
|
|
148
|
+
type ArrayConditions<T> = {
|
|
149
|
+
[K in keyof T as `${K & string}$${ArrayOperators}`]?: T[K][];
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 字符串操作符条件(值类型为字符串)
|
|
154
|
+
* @example { name$like: '%test%', email$notLike: '%spam%' }
|
|
155
|
+
*/
|
|
156
|
+
type StringConditions<T> = {
|
|
157
|
+
[K in keyof T as `${K & string}$${StringOperators}`]?: string;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 范围操作符条件(值类型为 [min, max] 元组)
|
|
162
|
+
* @example { age$between: [18, 65], createdAt$notBetween: [start, end] }
|
|
163
|
+
*/
|
|
164
|
+
type RangeConditions<T> = {
|
|
165
|
+
[K in keyof T as `${K & string}$${RangeOperators}`]?: [T[K], T[K]];
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 空值操作符条件(值类型为 true)
|
|
170
|
+
* @example { deletedAt$null: true, avatar$notNull: true }
|
|
171
|
+
*/
|
|
172
|
+
type NullConditions<T> = {
|
|
173
|
+
[K in keyof T as `${K & string}$${NullOperators}`]?: true;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 类型安全的 WHERE 条件
|
|
178
|
+
*
|
|
179
|
+
* 支持的操作符:
|
|
180
|
+
* - 比较:$gt, $gte, $lt, $lte, $ne, $not(值类型与字段相同)
|
|
181
|
+
* - 数组:$in, $nin, $notIn(值类型为数组)
|
|
182
|
+
* - 字符串:$like, $notLike(值类型为字符串)
|
|
183
|
+
* - 范围:$between, $notBetween(值类型为 [min, max] 元组)
|
|
184
|
+
* - 空值:$null, $notNull(值类型为 true)
|
|
185
|
+
* - 逻辑:$or, $and(递归条件数组)
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* const where: TypedWhereConditions<UserTable> = {
|
|
190
|
+
* state: 1, // 精确匹配
|
|
191
|
+
* age$gte: 18, // 大于等于
|
|
192
|
+
* roleId$in: [1, 2, 3], // IN 数组
|
|
193
|
+
* name$like: '%test%', // LIKE 匹配
|
|
194
|
+
* createdAt$between: [start, end], // BETWEEN 范围
|
|
195
|
+
* deletedAt$null: true, // IS NULL
|
|
196
|
+
* $or: [ // OR 条件
|
|
197
|
+
* { email$like: '%@gmail.com' },
|
|
198
|
+
* { email$like: '%@qq.com' }
|
|
199
|
+
* ]
|
|
200
|
+
* };
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export type TypedWhereConditions<T> = Partial<T> & // 精确匹配
|
|
204
|
+
CompareConditions<T> & // 比较操作符
|
|
205
|
+
ArrayConditions<T> & // 数组操作符
|
|
206
|
+
StringConditions<T> & // 字符串操作符
|
|
207
|
+
RangeConditions<T> & // 范围操作符
|
|
208
|
+
NullConditions<T> & { // 空值操作符
|
|
209
|
+
/** OR 条件组 */
|
|
210
|
+
$or?: TypedWhereConditions<T>[];
|
|
211
|
+
/** AND 条件组 */
|
|
212
|
+
$and?: TypedWhereConditions<T>[];
|
|
213
|
+
};
|
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;
|