befly 3.8.19 → 3.8.20
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 +7 -6
- package/bunfig.toml +1 -1
- package/lib/database.ts +28 -25
- package/lib/dbHelper.ts +3 -3
- package/lib/jwt.ts +90 -99
- package/lib/logger.ts +44 -23
- package/lib/redisHelper.ts +19 -22
- package/loader/loadApis.ts +69 -114
- package/loader/loadHooks.ts +65 -0
- package/loader/loadPlugins.ts +50 -219
- package/main.ts +106 -133
- package/package.json +15 -7
- package/paths.ts +20 -0
- package/plugins/cache.ts +1 -3
- package/plugins/db.ts +8 -11
- package/plugins/logger.ts +5 -3
- package/plugins/redis.ts +10 -14
- package/router/api.ts +60 -106
- package/router/root.ts +15 -12
- package/router/static.ts +54 -58
- package/sync/syncAll.ts +58 -0
- package/sync/syncApi.ts +264 -0
- package/sync/syncDb/apply.ts +194 -0
- package/sync/syncDb/constants.ts +76 -0
- package/sync/syncDb/ddl.ts +194 -0
- package/sync/syncDb/helpers.ts +200 -0
- package/sync/syncDb/index.ts +164 -0
- package/sync/syncDb/schema.ts +201 -0
- package/sync/syncDb/sqlite.ts +50 -0
- package/sync/syncDb/table.ts +321 -0
- package/sync/syncDb/tableCreate.ts +146 -0
- package/sync/syncDb/version.ts +72 -0
- package/sync/syncDb.ts +19 -0
- package/sync/syncDev.ts +206 -0
- package/sync/syncMenu.ts +331 -0
- package/tsconfig.json +2 -4
- package/types/api.d.ts +6 -0
- package/types/befly.d.ts +152 -28
- package/types/context.d.ts +29 -3
- package/types/hook.d.ts +35 -0
- package/types/index.ts +14 -1
- package/types/plugin.d.ts +6 -7
- package/types/sync.d.ts +403 -0
- package/check.ts +0 -378
- package/env.ts +0 -106
- package/lib/middleware.ts +0 -275
- package/types/env.ts +0 -65
- package/types/util.d.ts +0 -45
- package/util.ts +0 -257
package/lib/middleware.ts
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 中间件集合
|
|
3
|
-
* 整合所有请求处理中间件
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { isEmpty, isPlainObject } from 'es-toolkit/compat';
|
|
7
|
-
import { Env } from '../env.js';
|
|
8
|
-
import { Logger } from './logger.js';
|
|
9
|
-
import { Jwt } from './jwt.js';
|
|
10
|
-
import { Xml } from './xml.js';
|
|
11
|
-
import { Validator } from './validator.js';
|
|
12
|
-
import { pickFields } from '../util.js';
|
|
13
|
-
import type { ApiRoute } from '../types/api.js';
|
|
14
|
-
import type { RequestContext } from '../types/context.js';
|
|
15
|
-
import type { Plugin } from '../types/plugin.js';
|
|
16
|
-
import type { BeflyContext } from '../types/befly.js';
|
|
17
|
-
|
|
18
|
-
// ========================================
|
|
19
|
-
// JWT 认证中间件
|
|
20
|
-
// ========================================
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* 从请求头中提取并验证JWT token
|
|
24
|
-
*/
|
|
25
|
-
export async function authenticate(ctx: RequestContext): Promise<void> {
|
|
26
|
-
const authHeader = ctx.request.headers.get('authorization');
|
|
27
|
-
|
|
28
|
-
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
29
|
-
const token = authHeader.substring(7);
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
const payload = await Jwt.verify(token);
|
|
33
|
-
ctx.user = payload;
|
|
34
|
-
} catch (error: any) {
|
|
35
|
-
ctx.user = {};
|
|
36
|
-
}
|
|
37
|
-
} else {
|
|
38
|
-
ctx.user = {};
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ========================================
|
|
43
|
-
// CORS 中间件
|
|
44
|
-
// ========================================
|
|
45
|
-
|
|
46
|
-
export interface CorsResult {
|
|
47
|
-
headers: Record<string, string>;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* 设置 CORS 选项
|
|
52
|
-
* 根据环境变量或请求头动态设置跨域配置
|
|
53
|
-
*
|
|
54
|
-
* 注意:Access-Control-Allow-Origin 只能返回单个源,不能用逗号分隔多个源
|
|
55
|
-
* 如果配置了多个允许的源,需要根据请求的 Origin 动态返回匹配的源
|
|
56
|
-
*
|
|
57
|
-
* @param req - 请求对象
|
|
58
|
-
* @returns CORS 配置对象
|
|
59
|
-
*/
|
|
60
|
-
export const setCorsOptions = (req: Request): CorsResult => {
|
|
61
|
-
return {
|
|
62
|
-
headers: {
|
|
63
|
-
'Access-Control-Allow-Origin': Env.CORS_ALLOWED_ORIGIN === '*' ? req.headers.get('origin') : Env.CORS_ALLOWED_ORIGIN,
|
|
64
|
-
'Access-Control-Allow-Methods': Env.CORS_ALLOWED_METHODS,
|
|
65
|
-
'Access-Control-Allow-Headers': Env.CORS_ALLOWED_HEADERS,
|
|
66
|
-
'Access-Control-Expose-Headers': Env.CORS_EXPOSE_HEADERS,
|
|
67
|
-
'Access-Control-Max-Age': Env.CORS_MAX_AGE || 86400,
|
|
68
|
-
'Access-Control-Allow-Credentials': Env.CORS_ALLOW_CREDENTIALS || 'true'
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 处理OPTIONS预检请求
|
|
75
|
-
* @param corsOptions - CORS 配置对象
|
|
76
|
-
* @returns 204 响应
|
|
77
|
-
*/
|
|
78
|
-
export function handleOptionsRequest(corsOptions: CorsResult): Response {
|
|
79
|
-
return new Response(null, {
|
|
80
|
-
status: 204,
|
|
81
|
-
headers: corsOptions.headers
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ========================================
|
|
86
|
-
// 参数解析中间件
|
|
87
|
-
// ========================================
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* 解析GET请求参数
|
|
91
|
-
*/
|
|
92
|
-
export function parseGetParams(api: ApiRoute, ctx: RequestContext): void {
|
|
93
|
-
const url = new URL(ctx.request.url);
|
|
94
|
-
|
|
95
|
-
if (isPlainObject(api.fields) && !isEmpty(api.fields)) {
|
|
96
|
-
ctx.body = pickFields(Object.fromEntries(url.searchParams), Object.keys(api.fields));
|
|
97
|
-
} else {
|
|
98
|
-
ctx.body = Object.fromEntries(url.searchParams);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* 解析POST请求参数
|
|
104
|
-
*/
|
|
105
|
-
export async function parsePostParams(api: ApiRoute, ctx: RequestContext): Promise<boolean> {
|
|
106
|
-
try {
|
|
107
|
-
const contentType = ctx.request.headers.get('content-type') || '';
|
|
108
|
-
|
|
109
|
-
if (contentType.indexOf('json') !== -1) {
|
|
110
|
-
try {
|
|
111
|
-
ctx.body = await ctx.request.json();
|
|
112
|
-
} catch (err) {
|
|
113
|
-
// 如果没有 body 或 JSON 解析失败,默认为空对象
|
|
114
|
-
ctx.body = {};
|
|
115
|
-
}
|
|
116
|
-
} else if (contentType.indexOf('xml') !== -1) {
|
|
117
|
-
const textData = await ctx.request.text();
|
|
118
|
-
const xmlData = Xml.parse(textData);
|
|
119
|
-
ctx.body = xmlData?.xml ? xmlData.xml : xmlData;
|
|
120
|
-
} else if (contentType.indexOf('form-data') !== -1) {
|
|
121
|
-
ctx.body = await ctx.request.formData();
|
|
122
|
-
} else if (contentType.indexOf('x-www-form-urlencoded') !== -1) {
|
|
123
|
-
const text = await ctx.request.text();
|
|
124
|
-
const formData = new URLSearchParams(text);
|
|
125
|
-
ctx.body = Object.fromEntries(formData);
|
|
126
|
-
} else {
|
|
127
|
-
ctx.body = {};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (isPlainObject(api.fields) && !isEmpty(api.fields)) {
|
|
131
|
-
ctx.body = pickFields(ctx.body, Object.keys(api.fields));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return true;
|
|
135
|
-
} catch (error: any) {
|
|
136
|
-
Logger.error('处理请求参数时发生错误', error);
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ========================================
|
|
142
|
-
// 权限验证中间件
|
|
143
|
-
// ========================================
|
|
144
|
-
|
|
145
|
-
export interface PermissionResult {
|
|
146
|
-
code: 0 | 1;
|
|
147
|
-
msg: string;
|
|
148
|
-
data: any;
|
|
149
|
-
[key: string]: any;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* 检查权限
|
|
154
|
-
*
|
|
155
|
-
* 验证流程:
|
|
156
|
-
* 1. auth=false → 公开接口,直接通过
|
|
157
|
-
* 2. auth=true → 需要登录认证
|
|
158
|
-
* - 未登录 → 拒绝
|
|
159
|
-
* - roleCode='dev' → 超级管理员,拥有所有权限
|
|
160
|
-
* - 其他角色 → 检查接口是否在角色的可访问接口列表中(通过 Redis SISMEMBER 预判断)
|
|
161
|
-
*
|
|
162
|
-
* @param api - API 路由配置
|
|
163
|
-
* @param ctx - 请求上下文
|
|
164
|
-
* @param hasPermission - 是否有权限(通过 Redis SISMEMBER 预先判断)
|
|
165
|
-
* @returns 统一的响应格式 { code: 0/1, msg, data, ...extra }
|
|
166
|
-
*/
|
|
167
|
-
export function checkPermission(api: ApiRoute, ctx: RequestContext, hasPermission: boolean = false): PermissionResult {
|
|
168
|
-
// 1. 公开接口(auth=false),无需验证
|
|
169
|
-
if (api.auth === false) {
|
|
170
|
-
return { code: 0, msg: '', data: {} };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 2. 需要登录的接口(auth=true)
|
|
174
|
-
// 2.1 检查是否登录
|
|
175
|
-
if (!ctx.user?.id) {
|
|
176
|
-
return {
|
|
177
|
-
code: 1,
|
|
178
|
-
msg: '未登录',
|
|
179
|
-
data: {},
|
|
180
|
-
login: 'no'
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// 2.2 dev 角色拥有所有权限(超级管理员)
|
|
185
|
-
if (ctx.user.roleCode === 'dev') {
|
|
186
|
-
return { code: 0, msg: '', data: {} };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// 2.3 检查接口权限(基于 Redis SISMEMBER 预判断结果)
|
|
190
|
-
if (!hasPermission) {
|
|
191
|
-
return {
|
|
192
|
-
code: 1,
|
|
193
|
-
msg: '没有权限访问此接口',
|
|
194
|
-
data: {}
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// 验证通过
|
|
199
|
-
return { code: 0, msg: '', data: {} };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// ========================================
|
|
203
|
-
// 插件钩子中间件
|
|
204
|
-
// ========================================
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* 执行所有插件的onGet钩子
|
|
208
|
-
*/
|
|
209
|
-
export async function executePluginHooks(pluginLists: Plugin[], appContext: BeflyContext, ctx: RequestContext): Promise<void> {
|
|
210
|
-
for await (const plugin of pluginLists) {
|
|
211
|
-
try {
|
|
212
|
-
if (typeof plugin?.onGet === 'function') {
|
|
213
|
-
await plugin?.onGet(appContext, ctx, ctx.request);
|
|
214
|
-
}
|
|
215
|
-
} catch (error: any) {
|
|
216
|
-
Logger.error('插件处理请求时发生错误', error);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ========================================
|
|
222
|
-
// 请求日志中间件
|
|
223
|
-
// ========================================
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* 过滤日志字段(内部函数)
|
|
227
|
-
* 用于从请求体中排除敏感字段(如密码、令牌等)
|
|
228
|
-
* @param body - 请求体对象
|
|
229
|
-
* @param excludeFields - 要排除的字段名(逗号分隔)
|
|
230
|
-
* @returns 过滤后的对象
|
|
231
|
-
*/
|
|
232
|
-
function filterLogFields(body: any, excludeFields: string = ''): any {
|
|
233
|
-
if (!body || (!isPlainObject(body) && !Array.isArray(body))) return body;
|
|
234
|
-
|
|
235
|
-
const fieldsArray = excludeFields
|
|
236
|
-
.split(',')
|
|
237
|
-
.map((field) => field.trim())
|
|
238
|
-
.filter((field) => field.length > 0);
|
|
239
|
-
|
|
240
|
-
const filtered: any = {};
|
|
241
|
-
for (const [key, value] of Object.entries(body)) {
|
|
242
|
-
if (!fieldsArray.includes(key)) {
|
|
243
|
-
filtered[key] = value;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return filtered;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* 记录请求日志
|
|
251
|
-
*/
|
|
252
|
-
export function logRequest(apiPath: string, ctx: RequestContext): void {
|
|
253
|
-
Logger.info({
|
|
254
|
-
msg: '通用接口日志',
|
|
255
|
-
请求路径: apiPath,
|
|
256
|
-
请求方法: ctx.request.method,
|
|
257
|
-
用户信息: ctx.user,
|
|
258
|
-
请求参数: filterLogFields(ctx.body, Env.LOG_EXCLUDE_FIELDS),
|
|
259
|
-
耗时: Date.now() - ctx.startTime + 'ms'
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// ========================================
|
|
264
|
-
// 参数验证中间件
|
|
265
|
-
// ========================================
|
|
266
|
-
|
|
267
|
-
// 创建全局validator实例
|
|
268
|
-
const validator = new Validator();
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* 验证请求参数
|
|
272
|
-
*/
|
|
273
|
-
export function validateParams(api: ApiRoute, ctx: RequestContext) {
|
|
274
|
-
return validator.validate(ctx.body, api.fields, api.required);
|
|
275
|
-
}
|
package/types/env.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 环境变量配置接口
|
|
3
|
-
*/
|
|
4
|
-
export interface EnvConfig {
|
|
5
|
-
// ========== 项目配置 ==========
|
|
6
|
-
NODE_ENV: string;
|
|
7
|
-
APP_NAME: string;
|
|
8
|
-
APP_PORT: number;
|
|
9
|
-
APP_HOST: string;
|
|
10
|
-
DEV_EMAIL: string;
|
|
11
|
-
DEV_PASSWORD: string;
|
|
12
|
-
BODY_LIMIT: number;
|
|
13
|
-
PARAMS_CHECK: string;
|
|
14
|
-
|
|
15
|
-
// ========== 日志配置 ==========
|
|
16
|
-
LOG_DEBUG: number;
|
|
17
|
-
LOG_EXCLUDE_FIELDS: string;
|
|
18
|
-
LOG_DIR: string;
|
|
19
|
-
LOG_TO_CONSOLE: number;
|
|
20
|
-
LOG_MAX_SIZE: number;
|
|
21
|
-
|
|
22
|
-
// ========== 时区配置 ==========
|
|
23
|
-
TZ: string;
|
|
24
|
-
|
|
25
|
-
// ========== 数据库配置 ==========
|
|
26
|
-
DATABASE_ENABLE: number;
|
|
27
|
-
DB_TYPE: string;
|
|
28
|
-
DB_HOST: string;
|
|
29
|
-
DB_PORT: number;
|
|
30
|
-
DB_USER: string;
|
|
31
|
-
DB_PASS: string;
|
|
32
|
-
DB_NAME: string;
|
|
33
|
-
DB_POOL_MAX: number;
|
|
34
|
-
|
|
35
|
-
// ========== Redis 配置 ==========
|
|
36
|
-
REDIS_HOST: string;
|
|
37
|
-
REDIS_PORT: number;
|
|
38
|
-
REDIS_USERNAME: string;
|
|
39
|
-
REDIS_PASSWORD: string;
|
|
40
|
-
REDIS_DB: number;
|
|
41
|
-
REDIS_KEY_PREFIX: string;
|
|
42
|
-
|
|
43
|
-
// ========== JWT 配置 ==========
|
|
44
|
-
JWT_SECRET: string;
|
|
45
|
-
JWT_EXPIRES_IN: string;
|
|
46
|
-
JWT_ALGORITHM: string;
|
|
47
|
-
|
|
48
|
-
// ========== CORS 配置 ==========
|
|
49
|
-
CORS_ALLOWED_ORIGIN: string;
|
|
50
|
-
CORS_ALLOWED_METHODS: string;
|
|
51
|
-
CORS_ALLOWED_HEADERS: string;
|
|
52
|
-
CORS_EXPOSE_HEADERS: string;
|
|
53
|
-
CORS_MAX_AGE: number;
|
|
54
|
-
CORS_ALLOW_CREDENTIALS: string;
|
|
55
|
-
|
|
56
|
-
// ========== 邮件配置 ==========
|
|
57
|
-
MAIL_HOST: string;
|
|
58
|
-
MAIL_PORT: number;
|
|
59
|
-
MAIL_POOL: string;
|
|
60
|
-
MAIL_SECURE: string;
|
|
61
|
-
MAIL_USER: string;
|
|
62
|
-
MAIL_PASS: string;
|
|
63
|
-
MAIL_SENDER: string;
|
|
64
|
-
MAIL_ADDRESS: string;
|
|
65
|
-
}
|
package/types/util.d.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* util.ts 相关类型定义
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { RedisClient } from 'bun';
|
|
6
|
-
import type { DbHelper } from '../lib/dbHelper.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 数据清洗选项
|
|
10
|
-
*/
|
|
11
|
-
export interface DataCleanOptions {
|
|
12
|
-
/** 排除的字段名 */
|
|
13
|
-
excludeKeys?: string[];
|
|
14
|
-
/** 包含的字段名(优先级高于 excludeKeys) */
|
|
15
|
-
includeKeys?: string[];
|
|
16
|
-
/** 要移除的值 */
|
|
17
|
-
removeValues?: any[];
|
|
18
|
-
/** 字符串最大长度(超出截断) */
|
|
19
|
-
maxLen?: number;
|
|
20
|
-
/** 是否深度处理嵌套对象 */
|
|
21
|
-
deep?: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 数据库连接集合(内部使用)
|
|
26
|
-
*/
|
|
27
|
-
export interface DatabaseConnections {
|
|
28
|
-
redis: RedisClient | null;
|
|
29
|
-
sql: any;
|
|
30
|
-
helper: DbHelper | null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 请求上下文接口
|
|
35
|
-
*/
|
|
36
|
-
export interface RequestContext {
|
|
37
|
-
/** 请求体参数 */
|
|
38
|
-
body: Record<string, any>;
|
|
39
|
-
/** 用户信息 */
|
|
40
|
-
user: Record<string, any>;
|
|
41
|
-
/** 原始请求对象 */
|
|
42
|
-
request: Request;
|
|
43
|
-
/** 请求开始时间(毫秒) */
|
|
44
|
-
startTime: number;
|
|
45
|
-
}
|
package/util.ts
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import { join } from 'pathe';
|
|
3
|
-
import { readdirSync, statSync, readFileSync, existsSync } from 'node:fs';
|
|
4
|
-
import { isEmpty, isPlainObject } from 'es-toolkit/compat';
|
|
5
|
-
import { snakeCase, camelCase, kebabCase } from 'es-toolkit/string';
|
|
6
|
-
import { Env } from './env.js';
|
|
7
|
-
import { Logger } from './lib/logger.js';
|
|
8
|
-
import { projectDir, projectAddonsDir } from './paths.js';
|
|
9
|
-
import type { KeyValue } from './types/common.js';
|
|
10
|
-
import type { JwtPayload, JwtSignOptions, JwtVerifyOptions } from './types/jwt';
|
|
11
|
-
import type { Plugin } from './types/plugin.js';
|
|
12
|
-
|
|
13
|
-
// ========================================
|
|
14
|
-
// API 响应工具
|
|
15
|
-
// ========================================
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 成功响应
|
|
19
|
-
*/
|
|
20
|
-
export const Yes = <T = any>(msg: string = '', data: T | {} = {}, other: KeyValue = {}): { code: 0; msg: string; data: T | {} } & KeyValue => {
|
|
21
|
-
return {
|
|
22
|
-
...other,
|
|
23
|
-
code: 0,
|
|
24
|
-
msg: msg,
|
|
25
|
-
data: data
|
|
26
|
-
};
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 失败响应
|
|
31
|
-
*/
|
|
32
|
-
export const No = <T = any>(msg: string = '', data: T | {} = {}, other: KeyValue = {}): { code: 1; msg: string; data: T | {} } & KeyValue => {
|
|
33
|
-
return {
|
|
34
|
-
...other,
|
|
35
|
-
code: 1,
|
|
36
|
-
msg: msg,
|
|
37
|
-
data: data
|
|
38
|
-
};
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
// ========================================
|
|
42
|
-
// 动态导入工具
|
|
43
|
-
// ========================================
|
|
44
|
-
|
|
45
|
-
// ========================================
|
|
46
|
-
// 字段转换工具(重新导出 lib/convert.ts)
|
|
47
|
-
// ========================================
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* 对象字段名转下划线
|
|
51
|
-
* @param obj - 源对象
|
|
52
|
-
* @returns 字段名转为下划线格式的新对象
|
|
53
|
-
*
|
|
54
|
-
* @example
|
|
55
|
-
* keysToSnake({ userId: 123, userName: 'John' }) // { user_id: 123, user_name: 'John' }
|
|
56
|
-
* keysToSnake({ createdAt: 1697452800000 }) // { created_at: 1697452800000 }
|
|
57
|
-
*/
|
|
58
|
-
export const keysToSnake = <T = any>(obj: Record<string, any>): T => {
|
|
59
|
-
if (!obj || !isPlainObject(obj)) return obj as T;
|
|
60
|
-
|
|
61
|
-
const result: any = {};
|
|
62
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
63
|
-
const snakeKey = snakeCase(key);
|
|
64
|
-
result[snakeKey] = value;
|
|
65
|
-
}
|
|
66
|
-
return result;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* 对象字段名转小驼峰
|
|
71
|
-
* @param obj - 源对象
|
|
72
|
-
* @returns 字段名转为小驼峰格式的新对象
|
|
73
|
-
*
|
|
74
|
-
* @example
|
|
75
|
-
* keysToCamel({ user_id: 123, user_name: 'John' }) // { userId: 123, userName: 'John' }
|
|
76
|
-
* keysToCamel({ created_at: 1697452800000 }) // { createdAt: 1697452800000 }
|
|
77
|
-
*/
|
|
78
|
-
export const keysToCamel = <T = any>(obj: Record<string, any>): T => {
|
|
79
|
-
if (!obj || !isPlainObject(obj)) return obj as T;
|
|
80
|
-
|
|
81
|
-
const result: any = {};
|
|
82
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
83
|
-
const camelKey = camelCase(key);
|
|
84
|
-
result[camelKey] = value;
|
|
85
|
-
}
|
|
86
|
-
return result;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* 数组对象字段名批量转小驼峰
|
|
91
|
-
* @param arr - 源数组
|
|
92
|
-
* @returns 字段名转为小驼峰格式的新数组
|
|
93
|
-
*
|
|
94
|
-
* @example
|
|
95
|
-
* arrayKeysToCamel([
|
|
96
|
-
* { user_id: 1, user_name: 'John' },
|
|
97
|
-
* { user_id: 2, user_name: 'Jane' }
|
|
98
|
-
* ])
|
|
99
|
-
* // [{ userId: 1, userName: 'John' }, { userId: 2, userName: 'Jane' }]
|
|
100
|
-
*/
|
|
101
|
-
export const arrayKeysToCamel = <T = any>(arr: Record<string, any>[]): T[] => {
|
|
102
|
-
if (!arr || !Array.isArray(arr)) return arr as T[];
|
|
103
|
-
return arr.map((item) => keysToCamel<T>(item));
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// ========================================
|
|
107
|
-
// 对象操作工具
|
|
108
|
-
// ========================================
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* 挑选指定字段
|
|
112
|
-
*/
|
|
113
|
-
export const pickFields = <T extends Record<string, any>>(obj: T, keys: string[]): Partial<T> => {
|
|
114
|
-
if (!obj || (!isPlainObject(obj) && !Array.isArray(obj))) {
|
|
115
|
-
return {};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const result: any = {};
|
|
119
|
-
for (const key of keys) {
|
|
120
|
-
if (key in obj) {
|
|
121
|
-
result[key] = obj[key];
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return result;
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* 字段清理
|
|
130
|
-
*/
|
|
131
|
-
export const fieldClear = <T extends Record<string, any> = any>(data: T, excludeValues: any[] = [null, undefined], keepValues: Record<string, any> = {}): Partial<T> => {
|
|
132
|
-
if (!data || !isPlainObject(data)) {
|
|
133
|
-
return {};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const result: any = {};
|
|
137
|
-
|
|
138
|
-
for (const [key, value] of Object.entries(data)) {
|
|
139
|
-
if (key in keepValues) {
|
|
140
|
-
if (Object.is(keepValues[key], value)) {
|
|
141
|
-
result[key] = value;
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const shouldExclude = excludeValues.some((excludeVal) => Object.is(excludeVal, value));
|
|
147
|
-
if (shouldExclude) {
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
result[key] = value;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return result;
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
// ========================================
|
|
158
|
-
// 日期时间工具
|
|
159
|
-
// ========================================
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* 计算性能时间差
|
|
163
|
-
*/
|
|
164
|
-
export const calcPerfTime = (startTime: number, endTime: number = Bun.nanoseconds()): string => {
|
|
165
|
-
const elapsedMs = (endTime - startTime) / 1_000_000;
|
|
166
|
-
|
|
167
|
-
if (elapsedMs < 1000) {
|
|
168
|
-
return `${elapsedMs.toFixed(2)} 毫秒`;
|
|
169
|
-
} else {
|
|
170
|
-
const elapsedSeconds = elapsedMs / 1000;
|
|
171
|
-
return `${elapsedSeconds.toFixed(2)} 秒`;
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
// ========================================
|
|
176
|
-
// Addon 工具函数
|
|
177
|
-
// ========================================
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* 扫描所有可用的 addon
|
|
181
|
-
* 优先从本地 addons/ 目录加载,其次从 node_modules/@befly-addon/ 加载
|
|
182
|
-
* @returns addon 名称数组
|
|
183
|
-
*/
|
|
184
|
-
export const scanAddons = (): string[] => {
|
|
185
|
-
const addons = new Set<string>();
|
|
186
|
-
|
|
187
|
-
// 1. 扫描本地 addons 目录(优先级高)
|
|
188
|
-
// if (existsSync(projectAddonsDir)) {
|
|
189
|
-
// try {
|
|
190
|
-
// const localAddons = fs.readdirSync(projectAddonsDir).filter((name) => {
|
|
191
|
-
// const fullPath = join(projectAddonsDir, name);
|
|
192
|
-
// try {
|
|
193
|
-
// const stat = statSync(fullPath);
|
|
194
|
-
// return stat.isDirectory() && !name.startsWith('_');
|
|
195
|
-
// } catch {
|
|
196
|
-
// return false;
|
|
197
|
-
// }
|
|
198
|
-
// });
|
|
199
|
-
// localAddons.forEach((name) => addons.add(name));
|
|
200
|
-
// } catch (err) {
|
|
201
|
-
// // 忽略本地目录读取错误
|
|
202
|
-
// }
|
|
203
|
-
// }
|
|
204
|
-
|
|
205
|
-
// 2. 扫描 node_modules/@befly-addon 目录
|
|
206
|
-
const beflyDir = join(projectDir, 'node_modules', '@befly-addon');
|
|
207
|
-
if (existsSync(beflyDir)) {
|
|
208
|
-
try {
|
|
209
|
-
const npmAddons = fs.readdirSync(beflyDir).filter((name) => {
|
|
210
|
-
// 如果本地已存在,跳过 npm 包版本
|
|
211
|
-
if (addons.has(name)) return false;
|
|
212
|
-
|
|
213
|
-
const fullPath = join(beflyDir, name);
|
|
214
|
-
try {
|
|
215
|
-
const stat = statSync(fullPath);
|
|
216
|
-
return stat.isDirectory();
|
|
217
|
-
} catch {
|
|
218
|
-
return false;
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
npmAddons.forEach((name) => addons.add(name));
|
|
222
|
-
} catch {
|
|
223
|
-
// 忽略 npm 目录读取错误
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return Array.from(addons).sort();
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* 获取 addon 的指定子目录路径
|
|
232
|
-
* 优先返回本地 addons 目录,其次返回 node_modules 目录
|
|
233
|
-
* @param name - addon 名称
|
|
234
|
-
* @param subDir - 子目录名称
|
|
235
|
-
* @returns 完整路径
|
|
236
|
-
*/
|
|
237
|
-
export const getAddonDir = (name: string, subDir: string): string => {
|
|
238
|
-
// 优先使用本地 addons 目录
|
|
239
|
-
// const localPath = join(projectAddonsDir, name, subDir);
|
|
240
|
-
// if (existsSync(localPath)) {
|
|
241
|
-
// return localPath;
|
|
242
|
-
// }
|
|
243
|
-
|
|
244
|
-
// 降级使用 node_modules 目录
|
|
245
|
-
return join(projectDir, 'node_modules', '@befly-addon', name, subDir);
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* 检查 addon 子目录是否存在
|
|
250
|
-
* @param name - addon 名称
|
|
251
|
-
* @param subDir - 子目录名称
|
|
252
|
-
* @returns 是否存在
|
|
253
|
-
*/
|
|
254
|
-
export const addonDirExists = (name: string, subDir: string): boolean => {
|
|
255
|
-
const dir = getAddonDir(name, subDir);
|
|
256
|
-
return existsSync(dir) && statSync(dir).isDirectory();
|
|
257
|
-
};
|