befly 3.6.0 → 3.7.1

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/check.ts ADDED
@@ -0,0 +1,239 @@
1
+ /**
2
+ * 表规则检查器 - TypeScript 版本
3
+ * 验证表定义文件的格式和规则
4
+ */
5
+
6
+ import { basename } from 'pathe';
7
+ import { Logger } from './lib/logger.js';
8
+ import { parseRule } from './util.js';
9
+ import { projectTableDir } from './paths.js';
10
+ import { Addon } from './lib/addon.js';
11
+
12
+ /**
13
+ * 表文件信息接口
14
+ */
15
+ interface TableFileInfo {
16
+ /** 表文件路径 */
17
+ file: string;
18
+ /** 文件类型:project(项目)或 addon(组件) */
19
+ type: 'project' | 'addon';
20
+ /** 如果是 addon 类型,记录 addon 名称 */
21
+ addonName?: string;
22
+ }
23
+
24
+ /**
25
+ * 保留字段列表
26
+ */
27
+ const RESERVED_FIELDS = ['id', 'created_at', 'updated_at', 'deleted_at', 'state'] as const;
28
+
29
+ /**
30
+ * 允许的字段类型
31
+ */
32
+ const FIELD_TYPES = ['string', 'number', 'text', 'array_string', 'array_text'] as const;
33
+
34
+ /**
35
+ * 小驼峰命名正则
36
+ * 可选:以下划线开头(用于特殊文件,如通用字段定义)
37
+ * 必须以小写字母开头,后续可包含小写/数字,或多个 [大写+小写/数字] 片段
38
+ * 示例:userTable、testCustomers、common
39
+ */
40
+ const LOWER_CAMEL_CASE_REGEX = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
41
+
42
+ /**
43
+ * 字段名称正则
44
+ * 必须为中文、数字、字母、下划线、短横线、空格
45
+ */
46
+ const FIELD_NAME_REGEX = /^[\u4e00-\u9fa5a-zA-Z0-9 _-]+$/;
47
+
48
+ /**
49
+ * VARCHAR 最大长度限制
50
+ */
51
+ const MAX_VARCHAR_LENGTH = 65535;
52
+
53
+ /**
54
+ * 检查表定义文件
55
+ * @throws 当检查失败时抛出异常
56
+ */
57
+ export const checkDefault = async function (): Promise<void> {
58
+ try {
59
+ const tablesGlob = new Bun.Glob('*.json');
60
+
61
+ // 统计信息
62
+ let totalFiles = 0;
63
+ let totalRules = 0;
64
+ let validFiles = 0;
65
+ let invalidFiles = 0;
66
+
67
+ // 收集所有表文件
68
+ const allTableFiles: TableFileInfo[] = [];
69
+
70
+ // 收集项目表字段定义文件
71
+ for await (const file of tablesGlob.scan({
72
+ cwd: projectTableDir,
73
+ absolute: true,
74
+ onlyFiles: true
75
+ })) {
76
+ allTableFiles.push({ file: file, type: 'project' });
77
+ }
78
+
79
+ // 收集 addon 表字段定义文件
80
+ const addons = Addon.scan();
81
+ for (const addonName of addons) {
82
+ const addonTablesDir = Addon.getDir(addonName, 'tables');
83
+
84
+ for await (const file of tablesGlob.scan({
85
+ cwd: addonTablesDir,
86
+ absolute: true,
87
+ onlyFiles: true
88
+ })) {
89
+ allTableFiles.push({ file: file, type: 'addon', addonName: addonName });
90
+ }
91
+ }
92
+
93
+ // 合并进行验证逻辑
94
+ for (const { file, type, addonName } of allTableFiles) {
95
+ totalFiles++;
96
+ const fileName = basename(file);
97
+ const fileBaseName = basename(file, '.json');
98
+ const fileType = type === 'project' ? '项目' : `组件${addonName}`;
99
+
100
+ try {
101
+ // 1) 文件名小驼峰校验
102
+ if (!LOWER_CAMEL_CASE_REGEX.test(fileBaseName)) {
103
+ Logger.warn(`${fileType}表 ${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
104
+ // 命名不合规,记录错误并计为无效文件,继续下一个文件
105
+ invalidFiles++;
106
+ continue;
107
+ }
108
+
109
+ // 读取并解析 JSON 文件
110
+ const table = await Bun.file(file).json();
111
+ let fileValid = true;
112
+ let fileRules = 0;
113
+
114
+ // 检查 table 中的每个验证规则
115
+ for (const [colKey, rule] of Object.entries(table)) {
116
+ if (typeof rule !== 'string') {
117
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 规则必须为字符串`);
118
+ fileValid = false;
119
+ continue;
120
+ }
121
+
122
+ // 验证规则格式
123
+ fileRules++;
124
+ totalRules++;
125
+
126
+ // 检查是否使用了保留字段
127
+ if (RESERVED_FIELDS.includes(colKey as any)) {
128
+ Logger.warn(`${fileType}表 ${fileName} 文件包含保留字段 ${colKey},` + `不能在表定义中使用以下字段: ${RESERVED_FIELDS.join(', ')}`);
129
+ fileValid = false;
130
+ }
131
+
132
+ // 使用 parseRule 解析字段规则
133
+ let parsed;
134
+ try {
135
+ parsed = parseRule(rule);
136
+ } catch (error: any) {
137
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段规则解析失败:${error.message}`);
138
+ fileValid = false;
139
+ continue;
140
+ }
141
+
142
+ const { name: fieldName, type: fieldType, min: fieldMin, max: fieldMax, default: fieldDefault, index: fieldIndex, regex: fieldRegx } = parsed;
143
+
144
+ // 第1个值:名称必须为中文、数字、字母、下划线、短横线、空格
145
+ if (!FIELD_NAME_REGEX.test(fieldName)) {
146
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
147
+ fileValid = false;
148
+ }
149
+
150
+ // 第2个值:字段类型必须为string,number,text,array_string,array_text之一
151
+ if (!FIELD_TYPES.includes(fieldType as any)) {
152
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join('、')}之一`);
153
+ fileValid = false;
154
+ }
155
+
156
+ // 第3/4个值:需要是 null 或 数字
157
+ if (!(fieldMin === null || typeof fieldMin === 'number')) {
158
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 格式错误,必须为null或数字`);
159
+ fileValid = false;
160
+ }
161
+ if (!(fieldMax === null || typeof fieldMax === 'number')) {
162
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最大值 "${fieldMax}" 格式错误,必须为null或数字`);
163
+ fileValid = false;
164
+ }
165
+
166
+ // 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
167
+ if (fieldMin !== null && fieldMax !== null) {
168
+ if (fieldMin > fieldMax) {
169
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
170
+ fileValid = false;
171
+ }
172
+ }
173
+
174
+ // 第6个值:是否创建索引必须为0或1
175
+ if (fieldIndex !== 0 && fieldIndex !== 1) {
176
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 索引标识 "${fieldIndex}" 格式错误,必须为0或1`);
177
+ fileValid = false;
178
+ }
179
+
180
+ // 第7个值:必须为null或正则表达式(parseRule已经验证过了)
181
+ // parseRule 已经将正则字符串转换为 RegExp 或 null,这里不需要再验证
182
+
183
+ // 第4个值与类型联动校验 + 默认值规则
184
+ if (fieldType === 'text') {
185
+ // text:min/max 必须为 null,默认值必须为 'null'
186
+ if (fieldMin !== null) {
187
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最小值必须为 null,当前为 "${fieldMin}"`);
188
+ fileValid = false;
189
+ }
190
+ if (fieldMax !== null) {
191
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最大长度必须为 null,当前为 "${fieldMax}"`);
192
+ fileValid = false;
193
+ }
194
+ if (fieldDefault !== 'null') {
195
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 text 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
196
+ fileValid = false;
197
+ }
198
+ } else if (fieldType === 'string' || fieldType === 'array') {
199
+ if (fieldMax === null || typeof fieldMax !== 'number') {
200
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
201
+ fileValid = false;
202
+ } else if (fieldMax > MAX_VARCHAR_LENGTH) {
203
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
204
+ fileValid = false;
205
+ }
206
+ } else if (fieldType === 'number') {
207
+ if (fieldDefault !== 'null' && typeof fieldDefault !== 'number') {
208
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或null,当前为 "${fieldDefault}"`);
209
+ fileValid = false;
210
+ }
211
+ }
212
+ }
213
+
214
+ if (fileValid) {
215
+ validFiles++;
216
+ // Logger.info(`${fileType}表 ${fileName} 验证通过(${fileRules} 个字段)`);
217
+ } else {
218
+ invalidFiles++;
219
+ }
220
+ } catch (error: any) {
221
+ Logger.error(`${fileType}表 ${fileName} 解析失败`, error);
222
+ invalidFiles++;
223
+ }
224
+ }
225
+
226
+ // 输出统计信息
227
+ // Logger.info(` 总文件数: ${totalFiles}`);
228
+ // Logger.info(` 总规则数: ${totalRules}`);
229
+ // Logger.info(` 通过文件: ${validFiles}`);
230
+ // Logger.info(` 失败文件: ${invalidFiles}`);
231
+
232
+ if (invalidFiles > 0) {
233
+ throw new Error('表定义检查失败,请修复上述错误后重试');
234
+ }
235
+ } catch (error: any) {
236
+ Logger.error('数据表定义检查过程中出错', error);
237
+ throw error;
238
+ }
239
+ };
package/lib/database.ts CHANGED
@@ -94,7 +94,7 @@ export class Database {
94
94
  Logger.error('数据库连接测试失败', error);
95
95
 
96
96
  try {
97
- await sql.close();
97
+ await sql?.close();
98
98
  } catch (cleanupError) {}
99
99
 
100
100
  throw error;
@@ -109,7 +109,7 @@ export class Database {
109
109
  try {
110
110
  await this.sqlClient.close();
111
111
  } catch (error: any) {
112
- Logger.warn('关闭 SQL 连接时出错:', error.message);
112
+ Logger.error('关闭 SQL 连接时出错', error);
113
113
  }
114
114
  this.sqlClient = null;
115
115
  }
@@ -200,7 +200,7 @@ export class Database {
200
200
  try {
201
201
  this.redisClient.close();
202
202
  } catch (error: any) {
203
- Logger.warn('关闭 Redis 连接时出错:', error);
203
+ Logger.error('关闭 Redis 连接时出错', error);
204
204
  }
205
205
  this.redisClient = null;
206
206
  }
package/lib/dbHelper.ts CHANGED
@@ -410,7 +410,7 @@ export class DbHelper {
410
410
  try {
411
411
  processed.id = await this.befly.redis.genTimeID();
412
412
  } catch (error: any) {
413
- throw new Error(`生成 ID 失败,Redis 可能不可用 (table: ${table}): ${error.message}`);
413
+ throw new Error(`生成 ID 失败,Redis 可能不可用 (table: ${table})`, error);
414
414
  }
415
415
 
416
416
  // 强制生成时间戳(不可被用户覆盖)
@@ -634,7 +634,7 @@ export class DbHelper {
634
634
  await conn.query('ROLLBACK');
635
635
  Logger.warn('事务已回滚');
636
636
  } catch (rollbackError: any) {
637
- Logger.error('事务回滚失败:', rollbackError);
637
+ Logger.error('事务回滚失败', rollbackError);
638
638
  }
639
639
  }
640
640
  throw error;
package/lib/middleware.ts CHANGED
@@ -132,8 +132,8 @@ export async function parsePostParams(api: ApiRoute, ctx: RequestContext): Promi
132
132
  }
133
133
 
134
134
  return true;
135
- } catch (err: any) {
136
- Logger.error('处理请求参数时发生错误', err);
135
+ } catch (error: any) {
136
+ Logger.error('处理请求参数时发生错误', error);
137
137
  return false;
138
138
  }
139
139
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { join, basename } from 'pathe';
7
+ import { existsSync, statSync } from 'node:fs';
7
8
  import { Logger } from '../lib/logger.js';
8
9
  import { calcPerfTime } from '../util.js';
9
10
  import { projectCheckDir } from '../paths.js';
@@ -36,8 +37,10 @@ export class Checker {
36
37
  // 检查目录列表:先项目,后 addons
37
38
  const checkDirs: Array<{ path: string; type: 'app' | 'addon'; addonName?: string }> = [];
38
39
 
39
- // 添加项目 checks 目录
40
- checkDirs.push({ path: projectCheckDir, type: 'app' });
40
+ // 添加项目 checks 目录(如果存在)
41
+ if (existsSync(projectCheckDir) && statSync(projectCheckDir).isDirectory()) {
42
+ checkDirs.push({ path: projectCheckDir, type: 'app' });
43
+ }
41
44
 
42
45
  // 添加所有 addon 的 checks 目录
43
46
  const addons = Addon.scan();
@@ -53,7 +53,6 @@ export class Lifecycle {
53
53
 
54
54
  // 4. 启动 HTTP 服务器
55
55
  const totalStartupTime = calcPerfTime(serverStartTime);
56
- Logger.info(`✓ 服务器启动准备完成,总耗时: ${totalStartupTime}`);
57
56
 
58
57
  return await Bootstrap.start(
59
58
  {
@@ -145,7 +145,6 @@ export class Loader {
145
145
  }
146
146
  }
147
147
  const corePluginsInitTime = calcPerfTime(corePluginsInitStart);
148
- Logger.info(`✓ 核心插件加载完成: ${corePlugins.length} 个,耗时: ${corePluginsScanTime}`);
149
148
 
150
149
  // 扫描 addon 插件目录
151
150
  const addons = Addon.scan();
@@ -213,7 +212,6 @@ export class Loader {
213
212
  }
214
213
  }
215
214
  const addonPluginsInitTime = calcPerfTime(addonPluginsInitStart);
216
- Logger.info(`✓ 组件插件加载完成: ${addonPlugins.length} 个,耗时: ${addonPluginsScanTime}`);
217
215
  }
218
216
  }
219
217
 
@@ -276,12 +274,10 @@ export class Loader {
276
274
  }
277
275
  }
278
276
  const userPluginsInitTime = calcPerfTime(userPluginsInitStart);
279
- Logger.info(`✓ 用户插件加载完成: ${sortedUserPlugins.length} 个,耗时: ${userPluginsInitTime}`);
280
277
  }
281
278
 
282
279
  const totalLoadTime = calcPerfTime(loadStartTime);
283
280
  const totalPluginCount = sortedCorePlugins.length + addonPlugins.length + sortedUserPlugins.length;
284
- Logger.info(`✓ 所有插件加载完成: ${totalPluginCount} 个,总耗时: ${totalLoadTime}`);
285
281
 
286
282
  // 核心插件失败 → 关键错误,必须退出
287
283
  if (hadCorePluginError) {
@@ -413,7 +409,6 @@ export class Loader {
413
409
  }
414
410
 
415
411
  const totalLoadTime = calcPerfTime(loadStartTime);
416
- Logger.info(`✓ ${dirDisplayName}接口加载完成: ${loadedApis}/${totalApis},耗时: ${totalLoadTime}`);
417
412
 
418
413
  // 检查是否有加载失败的 API(理论上不会到达这里,因为上面已经 critical 退出)
419
414
  if (failedApis > 0) {
package/main.ts CHANGED
@@ -14,6 +14,7 @@ import { coreDir } from './paths.js';
14
14
  import { DbHelper } from './lib/dbHelper.js';
15
15
  import { RedisHelper } from './lib/redisHelper.js';
16
16
  import { Addon } from './lib/addon.js';
17
+ import { checkDefault } from './check.js';
17
18
 
18
19
  import type { Server } from 'bun';
19
20
  import type { BeflyContext, BeflyOptions } from './types/befly.js';
@@ -40,23 +41,20 @@ export class Befly {
40
41
  async listen(callback?: (server: Server) => void): Promise<Server> {
41
42
  const server = await this.lifecycle.start(this.appContext, callback);
42
43
 
43
- // 注册优雅关闭信号处理器
44
44
  const gracefulShutdown = async (signal: string) => {
45
- Logger.info(`\n收到 ${signal} 信号,开始优雅关闭...`);
46
-
47
45
  // 1. 停止接收新请求
48
46
  server.stop(true);
49
- Logger.info('HTTP 服务器已停止');
47
+ Logger.info('HTTP 服务器已停止');
50
48
 
51
49
  // 2. 关闭数据库连接
52
50
  try {
53
51
  await Database.disconnect();
54
- Logger.info('数据库连接已关闭');
52
+ Logger.info('数据库连接已关闭');
55
53
  } catch (error: any) {
56
- Logger.warn('⚠️ 关闭数据库连接时出错:', error.message);
54
+ Logger.err('关闭数据库连接时出错:', error);
57
55
  }
58
56
 
59
- Logger.info('服务器已优雅关闭');
57
+ Logger.info('服务器已优雅关闭');
60
58
  process.exit(0);
61
59
  };
62
60
 
@@ -76,9 +74,10 @@ export {
76
74
  Jwt,
77
75
  Yes,
78
76
  No,
79
- coreDir,
80
77
  Database,
81
78
  DbHelper,
82
79
  RedisHelper,
83
- Addon
80
+ Addon,
81
+ coreDir,
82
+ checkDefault
84
83
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.6.0",
3
+ "version": "3.7.1",
4
4
  "description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
5
5
  "type": "module",
6
6
  "private": false,
@@ -38,14 +38,10 @@
38
38
  "homepage": "https://chensuiyi.me",
39
39
  "license": "Apache-2.0",
40
40
  "files": [
41
- "apis/",
42
- "checks/",
43
- "config/",
44
41
  "lib/",
45
42
  "lifecycle/",
46
43
  "plugins/",
47
44
  "router/",
48
- "tables/",
49
45
  "types/",
50
46
  ".gitignore",
51
47
  ".npmignore",
@@ -58,8 +54,8 @@
58
54
  "tsconfig.json",
59
55
  "LICENSE",
60
56
  "main.ts",
57
+ "check.ts",
61
58
  "env.ts",
62
- "entry.ts",
63
59
  "menu.json",
64
60
  "paths.ts",
65
61
  "util.ts",
@@ -73,5 +69,5 @@
73
69
  "es-toolkit": "^1.41.0",
74
70
  "pathe": "^2.0.3"
75
71
  },
76
- "gitHead": "e6d6cd5dd4098fc9b4cf537b0dcdc7e781c0e80e"
72
+ "gitHead": "131c2482f3668346adbbd153e14d5b74cd19e725"
77
73
  }
package/plugins/cache.ts CHANGED
@@ -176,7 +176,6 @@ const cachePlugin: Plugin = {
176
176
  async onInit(befly: BeflyContext): Promise<CacheManager> {
177
177
  try {
178
178
  const cacheManager = new CacheManager(befly);
179
- Logger.info('缓存插件初始化成功');
180
179
  return cacheManager;
181
180
  } catch (error: any) {
182
181
  throw error;
package/plugins/db.ts CHANGED
@@ -33,7 +33,6 @@ const dbPlugin: Plugin = {
33
33
  // 创建数据库管理器实例,直接传入 sql 对象
34
34
  const dbManager = new DbHelper(befly, sql);
35
35
 
36
- Logger.info('数据库插件初始化成功');
37
36
  return dbManager;
38
37
  } else {
39
38
  Logger.warn('数据库未启用(DB_ENABLE≠1),跳过初始化');
package/plugins/logger.ts CHANGED
@@ -16,7 +16,6 @@ const loggerPlugin: Plugin = {
16
16
 
17
17
  async onInit(befly: BeflyContext): Promise<typeof Logger> {
18
18
  try {
19
- Logger.info('日志插件初始化成功');
20
19
  return Logger;
21
20
  } catch (error: any) {
22
21
  // 插件内禁止直接退出进程,抛出异常交由主流程统一处理
package/plugins/redis.ts CHANGED
@@ -23,12 +23,6 @@ const redisPlugin: Plugin = {
23
23
  // 初始化 Redis 客户端(统一使用 database.ts 的连接管理)
24
24
  await Database.connectRedis();
25
25
 
26
- Logger.info('Redis 插件初始化成功', {
27
- host: Env.REDIS_HOST,
28
- port: Env.REDIS_PORT,
29
- db: Env.REDIS_DB
30
- });
31
-
32
26
  // 返回工具对象,向下游以相同 API 暴露
33
27
  return RedisHelper;
34
28
  } else {
package/router/api.ts CHANGED
@@ -115,7 +115,7 @@ export function apiHandler(apiRoutes: Map<string, ApiRoute>, pluginLists: Plugin
115
115
  }
116
116
  } catch (error: any) {
117
117
  // 记录详细的错误日志
118
- Logger.warn(api ? `接口 [${api.name}] 执行失败` : '处理接口请求时发生错误', error);
118
+ Logger.error(api ? `接口 [${api.name}] 执行失败` : '处理接口请求时发生错误', error);
119
119
 
120
120
  return Response.json(No('内部服务器错误'), {
121
121
  headers: corsOptions.headers
package/entry.ts DELETED
@@ -1,9 +0,0 @@
1
- /**
2
- * Befly 默认入口文件
3
- * 当用户项目没有自定义 main.ts 时使用此文件
4
- */
5
-
6
- import { Befly } from 'befly';
7
-
8
- const app = new Befly();
9
- await app.listen();