befly 3.8.27 → 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.
Files changed (65) hide show
  1. package/README.md +8 -6
  2. package/checks/checkApi.ts +2 -1
  3. package/checks/checkTable.ts +3 -2
  4. package/hooks/parser.ts +5 -3
  5. package/hooks/permission.ts +12 -5
  6. package/lib/cacheHelper.ts +76 -62
  7. package/lib/connect.ts +8 -35
  8. package/lib/dbHelper.ts +14 -11
  9. package/lib/jwt.ts +58 -437
  10. package/lib/logger.ts +76 -197
  11. package/lib/redisHelper.ts +163 -1
  12. package/lib/sqlBuilder.ts +2 -1
  13. package/lib/validator.ts +9 -8
  14. package/loader/loadApis.ts +4 -7
  15. package/loader/loadHooks.ts +2 -2
  16. package/loader/loadPlugins.ts +4 -4
  17. package/main.ts +4 -17
  18. package/package.json +10 -9
  19. package/paths.ts +0 -6
  20. package/plugins/db.ts +2 -2
  21. package/plugins/jwt.ts +5 -5
  22. package/plugins/redis.ts +1 -1
  23. package/router/api.ts +2 -2
  24. package/router/static.ts +1 -2
  25. package/sync/syncAll.ts +2 -2
  26. package/sync/syncApi.ts +10 -7
  27. package/sync/syncDb/apply.ts +11 -11
  28. package/sync/syncDb/constants.ts +61 -12
  29. package/sync/syncDb/ddl.ts +7 -7
  30. package/sync/syncDb/helpers.ts +3 -3
  31. package/sync/syncDb/schema.ts +16 -19
  32. package/sync/syncDb/table.ts +6 -5
  33. package/sync/syncDb/tableCreate.ts +7 -7
  34. package/sync/syncDb/types.ts +3 -2
  35. package/sync/syncDb/version.ts +4 -4
  36. package/sync/syncDb.ts +11 -10
  37. package/sync/syncDev.ts +10 -48
  38. package/sync/syncMenu.ts +11 -8
  39. package/tests/cacheHelper.test.ts +327 -0
  40. package/tests/dbHelper-columns.test.ts +5 -20
  41. package/tests/dbHelper-execute.test.ts +14 -68
  42. package/tests/fields-redis-cache.test.ts +5 -3
  43. package/tests/integration.test.ts +15 -26
  44. package/tests/jwt.test.ts +36 -94
  45. package/tests/logger.test.ts +32 -34
  46. package/tests/redisHelper.test.ts +270 -0
  47. package/tests/redisKeys.test.ts +76 -0
  48. package/tests/sync-connection.test.ts +0 -6
  49. package/tests/syncDb-apply.test.ts +3 -2
  50. package/tests/syncDb-constants.test.ts +15 -14
  51. package/tests/syncDb-ddl.test.ts +3 -2
  52. package/tests/syncDb-helpers.test.ts +3 -2
  53. package/tests/syncDb-schema.test.ts +3 -3
  54. package/tests/syncDb-types.test.ts +3 -2
  55. package/tests/util.test.ts +5 -1
  56. package/types/befly.d.ts +2 -15
  57. package/types/common.d.ts +11 -93
  58. package/types/database.d.ts +216 -5
  59. package/types/index.ts +1 -0
  60. package/types/logger.d.ts +11 -41
  61. package/types/table.d.ts +213 -0
  62. package/hooks/_rateLimit.ts +0 -64
  63. package/lib/regexAliases.ts +0 -59
  64. package/lib/xml.ts +0 -383
  65. package/tests/xml.test.ts +0 -101
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { snakeCase } from 'es-toolkit/string';
11
11
  import { Logger } from '../../lib/logger.js';
12
- import { IS_MYSQL, IS_PG, CHANGE_TYPE_LABELS, typeMapping } from './constants.js';
12
+ import { isMySQL, isPG, CHANGE_TYPE_LABELS, getTypeMapping } from './constants.js';
13
13
  import { logFieldChange, resolveDefaultValue, generateDefaultSql, isStringOrArrayType } from './helpers.js';
14
14
  import { generateDDLClause, isPgCompatibleTypeChange } from './ddl.js';
15
15
  import { getTableColumns, getTableIndexes } from './schema.js';
@@ -92,9 +92,9 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
92
92
  }
93
93
 
94
94
  if (v !== null && v !== '') {
95
- if (IS_PG) {
95
+ if (isPG()) {
96
96
  defaultClauses.push(`ALTER COLUMN "${dbFieldName}" SET DEFAULT ${v}`);
97
- } else if (IS_MYSQL && onlyDefaultChanged) {
97
+ } else if (isMySQL() && onlyDefaultChanged) {
98
98
  // MySQL 的 TEXT/BLOB 不允许 DEFAULT,跳过 text 类型
99
99
  if (fieldDef.type !== 'text') {
100
100
  defaultClauses.push(`ALTER COLUMN \`${dbFieldName}\` SET DEFAULT ${v}`);
@@ -112,7 +112,8 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
112
112
  }
113
113
 
114
114
  if (hasTypeChange) {
115
- if (IS_PG && isPgCompatibleTypeChange(existingColumns[dbFieldName].type, typeMapping[fieldDef.type].toLowerCase())) {
115
+ const typeMapping = getTypeMapping();
116
+ if (isPG() && isPgCompatibleTypeChange(existingColumns[dbFieldName].type, typeMapping[fieldDef.type].toLowerCase())) {
116
117
  Logger.debug(`[PG兼容类型变更] ${tableName}.${dbFieldName} ${existingColumns[dbFieldName].type} -> ${typeMapping[fieldDef.type].toLowerCase()} 允许执行`);
117
118
  }
118
119
  }
@@ -155,7 +156,7 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
155
156
 
156
157
  // PG 列注释处理
157
158
  const commentActions = [];
158
- if (IS_PG) {
159
+ if (isPG()) {
159
160
  for (const [fieldKey, fieldDef] of Object.entries(fields)) {
160
161
  // 转换字段名为下划线格式
161
162
  const dbFieldName = snakeCase(fieldKey);
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { snakeCase } from 'es-toolkit/string';
12
12
  import { Logger } from '../../lib/logger.js';
13
- import { IS_MYSQL, IS_PG, IS_PLAN, MYSQL_TABLE_CONFIG } from './constants.js';
13
+ import { isMySQL, isPG, IS_PLAN, MYSQL_TABLE_CONFIG } from './constants.js';
14
14
  import { quoteIdentifier } from './helpers.js';
15
15
  import { buildSystemColumnDefs, buildBusinessColumnDefs, buildIndexSQL } from './ddl.js';
16
16
  import { getTableIndexes } from './schema.js';
@@ -73,7 +73,7 @@ async function createTableIndexes(sql: SQL, tableName: string, fields: Record<st
73
73
 
74
74
  // 获取现有索引(MySQL 不支持 IF NOT EXISTS,需要先检查)
75
75
  let existingIndexes: Record<string, string[]> = {};
76
- if (IS_MYSQL) {
76
+ if (isMySQL()) {
77
77
  existingIndexes = await getTableIndexes(sql, tableName, dbName);
78
78
  }
79
79
 
@@ -81,7 +81,7 @@ async function createTableIndexes(sql: SQL, tableName: string, fields: Record<st
81
81
  for (const sysField of systemIndexFields) {
82
82
  const indexName = `idx_${sysField}`;
83
83
  // MySQL 跳过已存在的索引
84
- if (IS_MYSQL && existingIndexes[indexName]) {
84
+ if (isMySQL() && existingIndexes[indexName]) {
85
85
  continue;
86
86
  }
87
87
  const stmt = buildIndexSQL(tableName, indexName, sysField, 'create');
@@ -100,7 +100,7 @@ async function createTableIndexes(sql: SQL, tableName: string, fields: Record<st
100
100
  if (fieldDef.index === true) {
101
101
  const indexName = `idx_${dbFieldName}`;
102
102
  // MySQL 跳过已存在的索引
103
- if (IS_MYSQL && existingIndexes[indexName]) {
103
+ if (isMySQL() && existingIndexes[indexName]) {
104
104
  continue;
105
105
  }
106
106
  const stmt = buildIndexSQL(tableName, indexName, dbFieldName, 'create');
@@ -135,7 +135,7 @@ export async function createTable(sql: SQL, tableName: string, fields: Record<st
135
135
  const cols = colDefs.join(',\n ');
136
136
  const tableQuoted = quoteIdentifier(tableName);
137
137
  const { ENGINE, CHARSET, COLLATE } = MYSQL_TABLE_CONFIG;
138
- const createSQL = IS_MYSQL
138
+ const createSQL = isMySQL()
139
139
  ? `CREATE TABLE ${tableQuoted} (
140
140
  ${cols}
141
141
  ) ENGINE=${ENGINE} DEFAULT CHARSET=${CHARSET} COLLATE=${COLLATE}`
@@ -150,9 +150,9 @@ export async function createTable(sql: SQL, tableName: string, fields: Record<st
150
150
  }
151
151
 
152
152
  // PostgreSQL: 添加列注释
153
- if (IS_PG && !IS_PLAN) {
153
+ if (isPG() && !IS_PLAN) {
154
154
  await addPostgresComments(sql, tableName, fields);
155
- } else if (IS_PG && IS_PLAN) {
155
+ } else if (isPG() && IS_PLAN) {
156
156
  // 计划模式也要输出注释语句
157
157
  await addPostgresComments(sql, tableName, fields);
158
158
  }
@@ -7,7 +7,7 @@
7
7
  * - 类型判断工具
8
8
  */
9
9
 
10
- import { IS_MYSQL, typeMapping } from './constants.js';
10
+ import { isMySQL, getTypeMapping } from './constants.js';
11
11
 
12
12
  /**
13
13
  * 判断是否为字符串或数组类型(需要长度参数)
@@ -42,12 +42,13 @@ export function isStringOrArrayType(fieldType: string): boolean {
42
42
  * getSqlType('array_text', null) // => 'MEDIUMTEXT'
43
43
  */
44
44
  export function getSqlType(fieldType: string, fieldMax: number | null, unsigned: boolean = false): string {
45
+ const typeMapping = getTypeMapping();
45
46
  if (isStringOrArrayType(fieldType)) {
46
47
  return `${typeMapping[fieldType]}(${fieldMax})`;
47
48
  }
48
49
  // 处理 UNSIGNED 修饰符(仅 MySQL number 类型)
49
50
  const baseType = typeMapping[fieldType] || 'TEXT';
50
- if (IS_MYSQL && fieldType === 'number' && unsigned) {
51
+ if (isMySQL() && fieldType === 'number' && unsigned) {
51
52
  return `${baseType} UNSIGNED`;
52
53
  }
53
54
  return baseType;
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { Logger } from '../../lib/logger.js';
9
- import { DB_VERSION_REQUIREMENTS, IS_MYSQL, IS_PG, IS_SQLITE } from './constants.js';
9
+ import { DB_VERSION_REQUIREMENTS, isMySQL, isPG, isSQLite } from './constants.js';
10
10
  import type { SQL } from 'bun';
11
11
 
12
12
  /**
@@ -23,7 +23,7 @@ import type { SQL } from 'bun';
23
23
  export async function ensureDbVersion(sql: SQL): Promise<void> {
24
24
  if (!sql) throw new Error('SQL 客户端未初始化');
25
25
 
26
- if (IS_MYSQL) {
26
+ if (isMySQL()) {
27
27
  const r = await sql`SELECT VERSION() AS version`;
28
28
  if (!r || r.length === 0 || !r[0]?.version) {
29
29
  throw new Error('无法获取 MySQL 版本信息');
@@ -36,7 +36,7 @@ export async function ensureDbVersion(sql: SQL): Promise<void> {
36
36
  return;
37
37
  }
38
38
 
39
- if (IS_PG) {
39
+ if (isPG()) {
40
40
  const r = await sql`SELECT version() AS version`;
41
41
  if (!r || r.length === 0 || !r[0]?.version) {
42
42
  throw new Error('无法获取 PostgreSQL 版本信息');
@@ -50,7 +50,7 @@ export async function ensureDbVersion(sql: SQL): Promise<void> {
50
50
  return;
51
51
  }
52
52
 
53
- if (IS_SQLITE) {
53
+ if (isSQLite()) {
54
54
  const r = await sql`SELECT sqlite_version() AS version`;
55
55
  if (!r || r.length === 0 || !r[0]?.version) {
56
56
  throw new Error('无法获取 SQLite 版本信息');
package/sync/syncDb.ts CHANGED
@@ -13,7 +13,9 @@ import { snakeCase } from 'es-toolkit/string';
13
13
  import { Connect } from '../lib/connect.js';
14
14
  import { RedisHelper } from '../lib/redisHelper.js';
15
15
  import { checkTable } from '../checks/checkTable.js';
16
- import { scanFiles, scanAddons, addonDirExists, getAddonDir } from 'befly-util';
16
+ import { scanFiles } from 'befly-shared/scanFiles';
17
+ import { scanAddons, addonDirExists, getAddonDir } from 'befly-shared/addonHelper';
18
+ import { RedisKeys } from 'befly-shared/redisKeys';
17
19
  import { Logger } from '../lib/logger.js';
18
20
  import { projectDir } from '../paths.js';
19
21
 
@@ -23,6 +25,7 @@ import { tableExists } from './syncDb/schema.js';
23
25
  import { modifyTable } from './syncDb/table.js';
24
26
  import { createTable } from './syncDb/tableCreate.js';
25
27
  import { applyFieldDefaults } from './syncDb/helpers.js';
28
+ import { setDbType } from './syncDb/constants.js';
26
29
  import type { SQL } from 'bun';
27
30
  import type { BeflyOptions, SyncDbOptions } from '../types/index.js';
28
31
 
@@ -46,6 +49,10 @@ export async function syncDbCommand(config: BeflyOptions, options: SyncDbOptions
46
49
  // 清空处理记录
47
50
  processedTables.length = 0;
48
51
 
52
+ // 设置数据库类型(从配置获取)
53
+ const dbType = config.db?.type || 'mysql';
54
+ setDbType(dbType);
55
+
49
56
  // 验证表定义文件
50
57
  await checkTable();
51
58
 
@@ -129,17 +136,11 @@ export async function syncDbCommand(config: BeflyOptions, options: SyncDbOptions
129
136
  // 清理 Redis 缓存(如果有表被处理)
130
137
  if (processedTables.length > 0) {
131
138
  const redisHelper = new RedisHelper();
132
- for (const tableName of processedTables) {
133
- const cacheKey = `table:columns:${tableName}`;
134
- try {
135
- await redisHelper.del(cacheKey);
136
- } catch (error: any) {
137
- Logger.warn(`清理表 ${tableName} 的缓存失败: ${error.message}`);
138
- }
139
- }
139
+ const cacheKeys = processedTables.map((tableName) => RedisKeys.tableColumns(tableName));
140
+ await redisHelper.delBatch(cacheKeys);
140
141
  }
141
142
  } catch (error: any) {
142
- Logger.error('数据库同步失败', error);
143
+ Logger.error({ err: error }, '数据库同步失败');
143
144
  throw error;
144
145
  } finally {
145
146
  if (sql) {
package/sync/syncDev.ts CHANGED
@@ -7,12 +7,15 @@
7
7
  * - 表名: addon_admin_admin
8
8
  */
9
9
 
10
- import { scanAddons, getAddonDir, normalizeModuleForSync } from 'befly-util';
10
+ import { scanAddons, getAddonDir, normalizeModuleForSync } from 'befly-shared/addonHelper';
11
11
 
12
12
  import { Logger } from '../lib/logger.js';
13
13
  import { Cipher } from '../lib/cipher.js';
14
14
  import { Connect } from '../lib/connect.js';
15
15
  import { DbHelper } from '../lib/dbHelper.js';
16
+ import { RedisHelper } from '../lib/redisHelper.js';
17
+ import { CacheHelper } from '../lib/cacheHelper.js';
18
+
16
19
  import type { SyncDevOptions, SyncDevStats, BeflyOptions } from '../types/index.js';
17
20
 
18
21
  /**
@@ -33,7 +36,7 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
33
36
  // 连接数据库(SQL + Redis)
34
37
  await Connect.connect(config);
35
38
 
36
- const helper = Connect.getDbHelper();
39
+ const helper = new DbHelper({ redis: new RedisHelper() } as any, Connect.getSql());
37
40
 
38
41
  // 检查 addon_admin_admin 表是否存在
39
42
  const existAdmin = await helper.tableExists('addon_admin_admin');
@@ -152,57 +155,16 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
152
155
  });
153
156
  }
154
157
 
155
- // 缓存角色权限数据到 Redis
158
+ // 缓存角色权限数据到 Redis(复用 CacheHelper 逻辑)
156
159
  try {
157
- // 检查必要的表是否存在
158
- const apiTableExists = await helper.tableExists('addon_admin_api');
159
- const roleTableExists = await helper.tableExists('addon_admin_role');
160
-
161
- if (apiTableExists && roleTableExists) {
162
- // 查询所有角色
163
- const roles = await helper.getAll({
164
- table: 'addon_admin_role',
165
- fields: ['id', 'code', 'apis']
166
- });
167
-
168
- // 查询所有接口
169
- const allApis = await helper.getAll({
170
- table: 'addon_admin_api',
171
- fields: ['id', 'name', 'path', 'method', 'description', 'addonName']
172
- });
173
-
174
- const redis = Connect.getRedis();
175
-
176
- // 为每个角色缓存接口权限
177
- for (const role of roles) {
178
- if (!role.apis) continue;
179
-
180
- // 解析角色的接口 ID 列表
181
- const apiIds = role.apis
182
- .split(',')
183
- .map((id: string) => parseInt(id.trim()))
184
- .filter((id: number) => !isNaN(id));
185
-
186
- // 根据 ID 过滤出接口路径
187
- const roleApiPaths = allApis.filter((api: any) => apiIds.includes(api.id)).map((api: any) => `${api.method}${api.path}`);
188
-
189
- if (roleApiPaths.length === 0) continue;
190
-
191
- // 使用 Redis Set 缓存角色权限
192
- const redisKey = `role:apis:${role.code}`;
193
-
194
- // 先删除旧数据
195
- await redis.del(redisKey);
196
-
197
- // 批量添加到 Set(使用扩展运算符展开数组)
198
- await redis.sadd(redisKey, ...roleApiPaths);
199
- }
200
- }
160
+ const tempBefly = { db: helper, redis: new RedisHelper() } as any;
161
+ const cacheHelper = new CacheHelper(tempBefly);
162
+ await cacheHelper.cacheRolePermissions();
201
163
  } catch (error: any) {
202
164
  // 忽略缓存错误
203
165
  }
204
166
  } catch (error: any) {
205
- Logger.error('同步开发者管理员失败', error);
167
+ Logger.error({ err: error }, '同步开发者管理员失败');
206
168
  throw error;
207
169
  } finally {
208
170
  await Connect.disconnect();
package/sync/syncMenu.ts CHANGED
@@ -17,8 +17,11 @@
17
17
  import { join } from 'pathe';
18
18
  import { cloneDeep } from 'es-toolkit';
19
19
  import { Connect } from '../lib/connect.js';
20
+ import { DbHelper } from '../lib/dbHelper.js';
20
21
  import { RedisHelper } from '../lib/redisHelper.js';
21
- import { scanAddons, getAddonDir, scanConfig } from 'befly-util';
22
+ import { RedisKeys } from 'befly-shared/redisKeys';
23
+ import { scanAddons, getAddonDir } from 'befly-shared/addonHelper';
24
+ import { scanConfig } from 'befly-shared/scanConfig';
22
25
  import { Logger } from '../lib/logger.js';
23
26
  import { projectDir } from '../paths.js';
24
27
 
@@ -204,7 +207,7 @@ async function syncMenus(helper: any, menus: MenuConfig[]): Promise<void> {
204
207
  try {
205
208
  await syncMenuRecursive(helper, menu, 0, existingMenuMap, 1);
206
209
  } catch (error: any) {
207
- Logger.error(`同步菜单 "${menu.name}" 失败`, error.message || String(error));
210
+ Logger.error({ err: error, menu: menu.name }, '同步菜单失败');
208
211
  throw error;
209
212
  }
210
213
  }
@@ -257,7 +260,7 @@ async function loadMenuConfigs(): Promise<Array<{ menus: MenuConfig[]; addonName
257
260
  allMenus.push({ menus: menusWithPrefix, addonName: addonName });
258
261
  }
259
262
  } catch (error: any) {
260
- Logger.warn(`读取 addon 配置失败 ${addonName}: ${error.message}`);
263
+ Logger.warn({ err: error, addon: addonName }, '读取 addon 配置失败');
261
264
  }
262
265
  }
263
266
 
@@ -276,7 +279,7 @@ async function loadMenuConfigs(): Promise<Array<{ menus: MenuConfig[]; addonName
276
279
  allMenus.push({ menus: appMenus, addonName: 'app' });
277
280
  }
278
281
  } catch (error: any) {
279
- Logger.warn(`读取项目配置失败: ${error.message}`);
282
+ Logger.warn({ err: error }, '读取项目配置失败');
280
283
  }
281
284
 
282
285
  return allMenus;
@@ -301,7 +304,7 @@ export async function syncMenuCommand(config: BeflyOptions, options: SyncMenuOpt
301
304
  // 连接数据库(SQL + Redis)
302
305
  await Connect.connect(config);
303
306
 
304
- const helper = Connect.getDbHelper();
307
+ const helper = new DbHelper({ redis: new RedisHelper() } as any, Connect.getSql());
305
308
 
306
309
  // 3. 检查表是否存在(addon_admin_menu 来自 addon-admin 组件)
307
310
  const exists = await helper.tableExists('addon_admin_menu');
@@ -330,12 +333,12 @@ export async function syncMenuCommand(config: BeflyOptions, options: SyncMenuOpt
330
333
  // 8. 缓存菜单数据到 Redis
331
334
  try {
332
335
  const redisHelper = new RedisHelper();
333
- await redisHelper.setObject('menus:all', allMenusData);
336
+ await redisHelper.setObject(RedisKeys.menusAll(), allMenusData);
334
337
  } catch (error: any) {
335
- Logger.warn(`Redis 缓存菜单数据失败: ${error.message}`);
338
+ Logger.warn({ err: error }, 'Redis 缓存菜单数据失败');
336
339
  }
337
340
  } catch (error: any) {
338
- Logger.error('菜单同步失败', error);
341
+ Logger.error({ err: error }, '菜单同步失败');
339
342
  throw error;
340
343
  } finally {
341
344
  await Connect.disconnect();