befly 3.8.34 → 3.9.0

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/befly.config.ts CHANGED
@@ -85,6 +85,6 @@ export const beflyConfig = (await scanConfig({
85
85
  dirs: ['configs'],
86
86
  files: ['befly.common', `befly.${envSuffix}`, 'befly.local'],
87
87
  extensions: ['.json'],
88
- mode: 'merge',
88
+ mode: 'all',
89
89
  defaults: defaultOptions
90
90
  })) as BeflyOptions;
package/hooks/parser.ts CHANGED
@@ -36,7 +36,7 @@ const hook: Hook = {
36
36
  try {
37
37
  // JSON 格式
38
38
  if (contentType.includes('application/json')) {
39
- const body = await ctx.req.json();
39
+ const body = (await ctx.req.json()) as Record<string, any>;
40
40
  if (isPlainObject(ctx.api.fields) && !isEmpty(ctx.api.fields)) {
41
41
  ctx.body = pickFields(body, Object.keys(ctx.api.fields));
42
42
  } else {
@@ -45,7 +45,7 @@ const hook: Hook = {
45
45
  } else if (contentType.includes('application/xml') || contentType.includes('text/xml')) {
46
46
  // XML 格式
47
47
  const text = await ctx.req.text();
48
- const body = xmlParser.parse(text);
48
+ const body = xmlParser.parse(text) as Record<string, any>;
49
49
  if (isPlainObject(ctx.api.fields) && !isEmpty(ctx.api.fields)) {
50
50
  ctx.body = pickFields(body, Object.keys(ctx.api.fields));
51
51
  } else {
@@ -40,8 +40,7 @@ const hook: Hook = {
40
40
  // 验证角色权限
41
41
  const apiPath = `${ctx.req.method}${new URL(ctx.req.url).pathname}`;
42
42
  const roleApisKey = RedisKeys.roleApis(ctx.user.roleCode);
43
- const isMember = await befly.redis.sismember(roleApisKey, apiPath);
44
- hasPermission = isMember === 1;
43
+ hasPermission = await befly.redis.sismember(roleApisKey, apiPath);
45
44
  } catch (error) {
46
45
  // Redis 异常时降级为拒绝访问
47
46
  befly.logger.warn({ err: error, route: ctx.route }, 'Redis 权限检查失败');
@@ -225,8 +225,7 @@ export class CacheHelper {
225
225
  */
226
226
  async checkRolePermission(roleCode: string, apiPath: string): Promise<boolean> {
227
227
  try {
228
- const result = await this.befly.redis.sismember(RedisKeys.roleApis(roleCode), apiPath);
229
- return result === 1;
228
+ return await this.befly.redis.sismember(RedisKeys.roleApis(roleCode), apiPath);
230
229
  } catch (error: any) {
231
230
  Logger.error({ err: error, roleCode: roleCode }, '检查角色权限失败');
232
231
  return false;
package/lib/cipher.ts CHANGED
@@ -77,10 +77,10 @@ export class Cipher {
77
77
  * RSA-SHA256 签名
78
78
  * @param data - 要签名的数据
79
79
  * @param privateKey - 私钥
80
- * @param encoding - 输出编码
80
+ * @param encoding - 输出编码('hex' | 'base64' | 'base64url')
81
81
  * @returns RSA-SHA256 签名
82
82
  */
83
- static rsaSha256(data: string, privateKey: string | Buffer, encoding: BufferEncoding = 'hex'): string {
83
+ static rsaSha256(data: string, privateKey: string | Buffer, encoding: 'hex' | 'base64' | 'base64url' = 'hex'): string {
84
84
  const sign = createSign('RSA-SHA256');
85
85
  sign.update(data);
86
86
  const signature = sign.sign(privateKey, encoding);
@@ -253,6 +253,7 @@ export class Cipher {
253
253
  * @returns 64位哈希值
254
254
  */
255
255
  static fastHash(data: string | Uint8Array, seed: number = 0): number {
256
- return Bun.hash(data, seed);
256
+ const result = Bun.hash(data, seed);
257
+ return typeof result === 'bigint' ? Number(result) : result;
257
258
  }
258
259
  }
package/lib/dbHelper.ts CHANGED
@@ -86,7 +86,7 @@ export class DbHelper {
86
86
  private async getTableColumns(table: string): Promise<string[]> {
87
87
  // 1. 先查 Redis 缓存
88
88
  const cacheKey = RedisKeys.tableColumns(table);
89
- let columns = await this.befly.redis.getObject<string[]>(cacheKey);
89
+ const columns = await this.befly.redis.getObject<string[]>(cacheKey);
90
90
 
91
91
  if (columns && columns.length > 0) {
92
92
  return columns;
@@ -100,12 +100,12 @@ export class DbHelper {
100
100
  throw new Error(`表 ${table} 不存在或没有字段`);
101
101
  }
102
102
 
103
- columns = result.map((row: any) => row.Field);
103
+ const columnNames = result.map((row: any) => row.Field) as string[];
104
104
 
105
105
  // 3. 写入 Redis 缓存
106
- await this.befly.redis.setObject(cacheKey, columns, RedisTTL.tableColumns);
106
+ await this.befly.redis.setObject(cacheKey, columnNames, RedisTTL.tableColumns);
107
107
 
108
- return columns;
108
+ return columnNames;
109
109
  }
110
110
 
111
111
  /**
@@ -171,7 +171,7 @@ export class DbHelper {
171
171
  * 统一的查询参数预处理方法
172
172
  */
173
173
  private async prepareQueryOptions(options: QueryOptions) {
174
- const cleanWhere = this.cleanFields(options.where);
174
+ const cleanWhere = this.cleanFields(options.where || {});
175
175
 
176
176
  // 处理 fields(支持排除语法)
177
177
  const processedFields = await this.fieldsToSnake(snakeCase(options.table), options.fields || []);
@@ -401,7 +401,7 @@ export class DbHelper {
401
401
  * getOne({ table: 'userProfile', fields: ['userId', 'userName'] })
402
402
  * getOne({ table: 'user_profile', fields: ['user_id', 'user_name'] })
403
403
  */
404
- async getOne<T = any>(options: QueryOptions): Promise<T | null> {
404
+ async getOne<T extends Record<string, any> = Record<string, any>>(options: QueryOptions): Promise<T | null> {
405
405
  const { table, fields, where } = await this.prepareQueryOptions(options);
406
406
 
407
407
  const builder = new SqlBuilder().select(fields).from(table).where(this.addDefaultStateFilter(where));
@@ -427,7 +427,7 @@ export class DbHelper {
427
427
  * // 使用小驼峰格式(推荐)
428
428
  * getList({ table: 'userProfile', fields: ['userId', 'userName', 'createdAt'] })
429
429
  */
430
- async getList<T = any>(options: QueryOptions): Promise<ListResult<T>> {
430
+ async getList<T extends Record<string, any> = Record<string, any>>(options: QueryOptions): Promise<ListResult<T>> {
431
431
  const prepared = await this.prepareQueryOptions(options);
432
432
 
433
433
  // 参数上限校验
@@ -451,7 +451,7 @@ export class DbHelper {
451
451
  // 如果总数为 0,直接返回,不执行第二次查询
452
452
  if (total === 0) {
453
453
  return {
454
- lists: [],
454
+ list: [],
455
455
  total: 0,
456
456
  page: prepared.page,
457
457
  limit: prepared.limit,
@@ -476,7 +476,7 @@ export class DbHelper {
476
476
 
477
477
  // 转换 BIGINT 字段(id, pid 等)为数字类型
478
478
  return {
479
- lists: this.convertBigIntFields<T>(camelList),
479
+ list: this.convertBigIntFields<T>(camelList),
480
480
  total: total,
481
481
  page: prepared.page,
482
482
  limit: prepared.limit,
@@ -493,7 +493,7 @@ export class DbHelper {
493
493
  * // 使用小驼峰格式(推荐)
494
494
  * getAll({ table: 'userProfile', fields: ['userId', 'userName'] })
495
495
  */
496
- async getAll<T = any>(options: Omit<QueryOptions, 'page' | 'limit'>): Promise<T[]> {
496
+ async getAll<T extends Record<string, any> = Record<string, any>>(options: Omit<QueryOptions, 'page' | 'limit'>): Promise<T[]> {
497
497
  // 添加硬性上限保护,防止内存溢出
498
498
  const MAX_LIMIT = 10000;
499
499
  const WARNING_LIMIT = 1000;
@@ -745,6 +745,7 @@ export class DbHelper {
745
745
 
746
746
  /**
747
747
  * 执行事务
748
+ * 使用 Bun SQL 的 begin 方法开启事务
748
749
  */
749
750
  async trans<T = any>(callback: TransactionCallback<T>): Promise<T> {
750
751
  if (this.isTransaction) {
@@ -752,37 +753,12 @@ export class DbHelper {
752
753
  return await callback(this);
753
754
  }
754
755
 
755
- // 开启新事务
756
- const conn = await this.befly.db.transaction();
757
- let committed = false;
758
-
759
- try {
760
- const trans = new DbHelper(this.befly, conn);
761
- const result = await callback(trans);
762
-
763
- // 提交事务
764
- try {
765
- await conn.query('COMMIT');
766
- committed = true;
767
- } catch (commitError: any) {
768
- Logger.error({ err: commitError }, '事务提交失败,正在回滚');
769
- await conn.query('ROLLBACK');
770
- throw new Error(`事务提交失败: ${commitError.message}`);
771
- }
772
-
773
- return result;
774
- } catch (error: any) {
775
- // 回调执行失败,回滚事务
776
- if (!committed) {
777
- try {
778
- await conn.query('ROLLBACK');
779
- Logger.warn('事务已回滚');
780
- } catch (rollbackError: any) {
781
- Logger.error({ err: rollbackError }, '事务回滚失败');
782
- }
783
- }
784
- throw error;
785
- }
756
+ // 使用 Bun SQL 的 begin 方法开启事务
757
+ // begin 方法会自动处理 commit/rollback
758
+ return await this.sql.begin(async (tx: any) => {
759
+ const trans = new DbHelper(this.befly, tx);
760
+ return await callback(trans);
761
+ });
786
762
  }
787
763
 
788
764
  /**
package/lib/jwt.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { createSigner, createVerifier, createDecoder } from 'fast-jwt';
6
6
 
7
+ import type { Algorithm as FastJwtAlgorithm } from 'fast-jwt';
7
8
  import type { AuthConfig } from '../types/befly.js';
8
9
  import type { JwtPayload, JwtSignOptions, JwtVerifyOptions, JwtDecoded, JwtHeader } from 'befly-shared/types';
9
10
 
@@ -26,15 +27,18 @@ export class Jwt {
26
27
  }
27
28
 
28
29
  sign(payload: JwtPayload, options: JwtSignOptions = {}): string {
30
+ const key = options.secret || this.config.secret || 'befly-secret';
31
+ const algorithm = (options.algorithm || this.config.algorithm || 'HS256') as FastJwtAlgorithm;
32
+
29
33
  const signer = createSigner({
30
- key: options.secret || this.config.secret,
31
- algorithm: options.algorithm || this.config.algorithm,
34
+ key: key,
35
+ algorithm: algorithm,
32
36
  expiresIn: options.expiresIn || this.config.expiresIn,
33
37
  iss: options.issuer,
34
38
  aud: options.audience,
35
39
  sub: options.subject,
36
40
  jti: options.jwtId,
37
- notBefore: options.notBefore
41
+ notBefore: typeof options.notBefore === 'number' ? options.notBefore : undefined
38
42
  });
39
43
  return signer(payload);
40
44
  }
@@ -43,9 +47,13 @@ export class Jwt {
43
47
  if (!token || typeof token !== 'string') {
44
48
  throw new Error('Token必须是非空字符串');
45
49
  }
50
+ const key = options.secret || this.config.secret || 'befly-secret';
51
+ const algorithm = (this.config.algorithm || 'HS256') as FastJwtAlgorithm;
52
+ const algorithms: FastJwtAlgorithm[] = options.algorithms ? (options.algorithms as FastJwtAlgorithm[]) : [algorithm];
53
+
46
54
  const verifier = createVerifier({
47
- key: options.secret || this.config.secret,
48
- algorithms: options.algorithms || [this.config.algorithm || 'HS256'],
55
+ key: key,
56
+ algorithms: algorithms,
49
57
  allowedIss: options.issuer,
50
58
  allowedAud: options.audience,
51
59
  allowedSub: options.subject,
@@ -140,14 +140,15 @@ export class RedisHelper {
140
140
  /**
141
141
  * 检查键是否存在
142
142
  * @param key - 键名
143
+ * @returns 是否存在(true/false)
143
144
  */
144
- async exists(key: string): Promise<number> {
145
+ async exists(key: string): Promise<boolean> {
145
146
  try {
146
147
  const pkey = `${this.prefix}${key}`;
147
148
  return await this.client.exists(pkey);
148
149
  } catch (error: any) {
149
150
  Logger.error('Redis exists 错误', error);
150
- return 0;
151
+ return false;
151
152
  }
152
153
  }
153
154
 
@@ -221,15 +222,15 @@ export class RedisHelper {
221
222
  * 判断成员是否在 Set 中
222
223
  * @param key - 键名
223
224
  * @param member - 成员
224
- * @returns 1 表示存在,0 表示不存在
225
+ * @returns 是否存在(true/false)
225
226
  */
226
- async sismember(key: string, member: string): Promise<number> {
227
+ async sismember(key: string, member: string): Promise<boolean> {
227
228
  try {
228
229
  const pkey = `${this.prefix}${key}`;
229
230
  return await this.client.sismember(pkey, member);
230
231
  } catch (error: any) {
231
232
  Logger.error('Redis sismember 错误', error);
232
- return 0;
233
+ return false;
233
234
  }
234
235
  }
235
236
 
@@ -293,8 +294,7 @@ export class RedisHelper {
293
294
  }
294
295
 
295
296
  try {
296
- const results = await Promise.all(items.map((item) => this.sismember(item.key, item.member)));
297
- return results.map((r) => r > 0);
297
+ return await Promise.all(items.map((item) => this.sismember(item.key, item.member)));
298
298
  } catch (error: any) {
299
299
  Logger.error('Redis sismemberBatch 错误', error);
300
300
  return items.map(() => false);
@@ -389,8 +389,7 @@ export class RedisHelper {
389
389
  }
390
390
 
391
391
  try {
392
- const results = await Promise.all(keys.map((key) => this.exists(key)));
393
- return results.map((r) => r > 0);
392
+ return await Promise.all(keys.map((key) => this.exists(key)));
394
393
  } catch (error: any) {
395
394
  Logger.error('Redis existsBatch 错误', error);
396
395
  return keys.map(() => false);
package/lib/sqlBuilder.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * 提供链式 API 构建 SQL 查询
4
4
  */
5
5
 
6
- import type { WhereConditions, OrderByField } from '../types/common.js';
6
+ import type { WhereConditions, OrderByField, WhereOperator, OrderDirection, SqlQuery, InsertData, UpdateData } from '../types/common.js';
7
7
  import type { SqlValue } from 'befly-shared/types';
8
8
 
9
9
  /**
package/lib/validator.ts CHANGED
@@ -178,7 +178,8 @@ export class Validator {
178
178
  private static resolveRegex(regexp: string | null): string | null {
179
179
  if (!regexp) return null;
180
180
  if (regexp.startsWith('@')) {
181
- return RegexAliases[regexp.substring(1)] || regexp;
181
+ const key = regexp.substring(1) as keyof typeof RegexAliases;
182
+ return RegexAliases[key] || regexp;
182
183
  }
183
184
  return regexp;
184
185
  }
@@ -19,7 +19,7 @@ export async function loadHooks(hooks: Hook[]): Promise<void> {
19
19
 
20
20
  // 2. 过滤禁用的钩子
21
21
  const disableHooks = beflyConfig.disableHooks || [];
22
- const enabledHooks = coreHooks.filter((hook) => !disableHooks.includes(hook.name));
22
+ const enabledHooks = coreHooks.filter((hook) => hook.name && !disableHooks.includes(hook.name));
23
23
 
24
24
  if (disableHooks.length > 0) {
25
25
  Logger.info({ hooks: disableHooks }, '禁用钩子');
@@ -39,7 +39,7 @@ export async function loadPlugins(plugins: Plugin[], context: BeflyContext): Pro
39
39
 
40
40
  // 5. 过滤禁用的插件
41
41
  const disablePlugins = beflyConfig.disablePlugins || [];
42
- const enabledPlugins = allPlugins.filter((plugin) => !disablePlugins.includes(plugin.name));
42
+ const enabledPlugins = allPlugins.filter((plugin) => plugin.name && !disablePlugins.includes(plugin.name));
43
43
 
44
44
  if (disablePlugins.length > 0) {
45
45
  Logger.info({ plugins: disablePlugins }, '禁用插件');
package/main.ts CHANGED
@@ -42,7 +42,7 @@ export class Befly {
42
42
  private hooks: Hook[] = [];
43
43
 
44
44
  /** 应用上下文 */
45
- public context: BeflyContext = {};
45
+ public context: Partial<BeflyContext> = {};
46
46
 
47
47
  /** 配置引用(延迟加载) */
48
48
  private config: BeflyOptions | null = null;
@@ -51,7 +51,7 @@ export class Befly {
51
51
  * 启动完整的生命周期流程
52
52
  * @returns HTTP 服务器实例
53
53
  */
54
- public async start(): Promise<Server> {
54
+ public async start(): Promise<ReturnType<typeof Bun.serve>> {
55
55
  try {
56
56
  const serverStartTime = Bun.nanoseconds();
57
57
 
@@ -65,7 +65,7 @@ export class Befly {
65
65
  await checkApi();
66
66
 
67
67
  // 2. 加载插件
68
- await loadPlugins(this.plugins, this.context);
68
+ await loadPlugins(this.plugins, this.context as BeflyContext);
69
69
 
70
70
  // 3. 加载钩子
71
71
  await loadHooks(this.hooks);
@@ -84,7 +84,7 @@ export class Befly {
84
84
  hostname: this.config!.appHost,
85
85
  routes: {
86
86
  '/': () => Response.json({ code: 0, msg: `${this.config!.appName} 接口服务已启动` }),
87
- '/api/*': apiHandler(this.apis, this.hooks, this.context),
87
+ '/api/*': apiHandler(this.apis, this.hooks, this.context as BeflyContext),
88
88
  '/*': staticHandler()
89
89
  },
90
90
  error: (error: Error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.8.34",
3
+ "version": "3.9.0",
4
4
  "description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
5
5
  "type": "module",
6
6
  "private": false,
@@ -64,7 +64,7 @@
64
64
  "bun": ">=1.3.0"
65
65
  },
66
66
  "dependencies": {
67
- "befly-shared": "^1.1.2",
67
+ "befly-shared": "^1.2.0",
68
68
  "chalk": "^5.6.2",
69
69
  "es-toolkit": "^1.42.0",
70
70
  "fast-jwt": "^6.1.0",
@@ -73,7 +73,7 @@
73
73
  "pino": "^10.1.0",
74
74
  "pino-roll": "^4.0.0"
75
75
  },
76
- "gitHead": "e6528128e0532df2c2f0ad96e32bc73e167aac84",
76
+ "gitHead": "ebb51601e566ac01ef09824309a6ec60a5d32388",
77
77
  "devDependencies": {
78
78
  "typescript": "^5.9.3"
79
79
  }
@@ -11,9 +11,9 @@ import { isMySQL, isPG, isSQLite, IS_PLAN, CHANGE_TYPE_LABELS, getTypeMapping }
11
11
  import { logFieldChange, resolveDefaultValue, isStringOrArrayType } from './helpers.js';
12
12
  import { executeDDLSafely, buildIndexSQL } from './ddl.js';
13
13
  import { rebuildSqliteTable } from './sqlite.js';
14
- import type { FieldChange, IndexAction, TablePlan, ColumnInfo } from '../../types.js';
14
+ import type { FieldChange, IndexAction, TablePlan, ColumnInfo } from '../../types/sync.js';
15
15
  import type { SQL } from 'bun';
16
- import type { FieldDefinition } from 'befly/types/common';
16
+ import type { FieldDefinition } from 'befly-shared/types';
17
17
 
18
18
  /**
19
19
  * 构建 ALTER TABLE SQL 语句
@@ -15,7 +15,8 @@ import { quoteIdentifier, escapeComment } from './helpers.js';
15
15
  import { resolveDefaultValue, generateDefaultSql, getSqlType } from './types.js';
16
16
 
17
17
  import type { SQL } from 'bun';
18
- import type { FieldDefinition, AnyObject } from 'befly/types/common.js';
18
+ import type { FieldDefinition } from 'befly-shared/types';
19
+ import type { AnyObject } from '../../types/common.js';
19
20
 
20
21
  /**
21
22
  * 构建索引操作 SQL(统一使用在线策略)
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { isMySQL, isPG, isSQLite } from './constants.js';
11
- import type { ColumnInfo, IndexInfo } from '../../types.js';
11
+ import type { ColumnInfo, IndexInfo } from '../../types/sync.js';
12
12
  import type { SQL } from 'bun';
13
13
 
14
14
  /**
@@ -83,6 +83,7 @@ export async function getTableColumns(sql: SQL, tableName: string, dbName: strin
83
83
  columns[row.COLUMN_NAME] = {
84
84
  type: row.DATA_TYPE,
85
85
  columnType: row.COLUMN_TYPE,
86
+ length: row.CHARACTER_MAXIMUM_LENGTH,
86
87
  max: row.CHARACTER_MAXIMUM_LENGTH,
87
88
  nullable: row.IS_NULLABLE === 'YES',
88
89
  defaultValue: defaultValue,
@@ -111,6 +112,7 @@ export async function getTableColumns(sql: SQL, tableName: string, dbName: strin
111
112
  columns[row.column_name] = {
112
113
  type: row.data_type,
113
114
  columnType: row.data_type,
115
+ length: row.character_maximum_length,
114
116
  max: row.character_maximum_length,
115
117
  nullable: String(row.is_nullable).toUpperCase() === 'YES',
116
118
  defaultValue: row.column_default,
@@ -130,6 +132,7 @@ export async function getTableColumns(sql: SQL, tableName: string, dbName: strin
130
132
  columns[row.name] = {
131
133
  type: baseType.toLowerCase(),
132
134
  columnType: baseType.toLowerCase(),
135
+ length: max,
133
136
  max: max,
134
137
  nullable: row.notnull === 0,
135
138
  defaultValue: row.dflt_value,
@@ -186,7 +189,7 @@ export async function getTableIndexes(sql: SQL, tableName: string, dbName: strin
186
189
  const list = await sql.unsafe(`PRAGMA index_list(${tableName})`);
187
190
  for (const idx of list) {
188
191
  const info = await sql.unsafe(`PRAGMA index_info(${idx.name})`);
189
- const cols = info.map((r) => r.name);
192
+ const cols = info.map((r: any) => r.name);
190
193
  if (cols.length === 1) indexes[idx.name] = cols;
191
194
  }
192
195
  }
@@ -9,6 +9,7 @@ import { Logger } from '../../lib/logger.js';
9
9
  import { IS_PLAN } from './constants.js';
10
10
  import { createTable } from './tableCreate.js';
11
11
  import type { SQL } from 'bun';
12
+ import type { FieldDefinition } from 'befly-shared/types';
12
13
 
13
14
  /**
14
15
  * SQLite 重建表迁移(简化版)
@@ -25,10 +26,10 @@ import type { SQL } from 'bun';
25
26
  * @param tableName - 表名
26
27
  * @param fields - 字段定义对象
27
28
  */
28
- export async function rebuildSqliteTable(sql: SQL, tableName: string, fields: Record<string, string>): Promise<void> {
29
+ export async function rebuildSqliteTable(sql: SQL, tableName: string, fields: Record<string, FieldDefinition>): Promise<void> {
29
30
  // 1. 读取现有列顺序
30
31
  const info = await sql.unsafe(`PRAGMA table_info(${tableName})`);
31
- const existingCols = info.map((r) => r.name);
32
+ const existingCols = info.map((r: any) => r.name);
32
33
  const targetCols = ['id', 'created_at', 'updated_at', 'deleted_at', 'state', ...Object.keys(fields)];
33
34
  const tmpTable = `${tableName}__tmp__${Date.now()}`;
34
35
 
@@ -14,9 +14,9 @@ import { logFieldChange, resolveDefaultValue, generateDefaultSql, isStringOrArra
14
14
  import { generateDDLClause, isPgCompatibleTypeChange } from './ddl.js';
15
15
  import { getTableColumns, getTableIndexes } from './schema.js';
16
16
  import { compareFieldDefinition, applyTablePlan } from './apply.js';
17
- import type { TablePlan } from '../../types.js';
17
+ import type { TablePlan } from '../../types/sync.js';
18
18
  import type { SQL } from 'bun';
19
- import type { FieldDefinition } from 'befly/types/common';
19
+ import type { FieldDefinition } from 'befly-shared/types';
20
20
 
21
21
  /**
22
22
  * 同步表结构(对比和应用变更)
@@ -34,30 +34,30 @@ import type { FieldDefinition } from 'befly/types/common';
34
34
  * @param dbName - 数据库名称
35
35
  */
36
36
  export async function modifyTable(sql: SQL, tableName: string, fields: Record<string, FieldDefinition>, force: boolean = false, dbName?: string): Promise<TablePlan> {
37
- const existingColumns = await getTableColumns(sql, tableName, dbName);
38
- const existingIndexes = await getTableIndexes(sql, tableName, dbName);
37
+ const existingColumns = await getTableColumns(sql, tableName, dbName || '');
38
+ const existingIndexes = await getTableIndexes(sql, tableName, dbName || '');
39
39
  let changed = false;
40
40
 
41
- const addClauses = [];
42
- const modifyClauses = [];
43
- const defaultClauses = [];
44
- const indexActions = [];
41
+ const addClauses: string[] = [];
42
+ const modifyClauses: string[] = [];
43
+ const defaultClauses: string[] = [];
44
+ const indexActions: Array<{ action: 'create' | 'drop'; indexName: string; fieldName: string }> = [];
45
45
 
46
46
  for (const [fieldKey, fieldDef] of Object.entries(fields)) {
47
47
  // 转换字段名为下划线格式
48
48
  const dbFieldName = snakeCase(fieldKey);
49
49
 
50
50
  if (existingColumns[dbFieldName]) {
51
- const comparison = compareFieldDefinition(existingColumns[dbFieldName], fieldDef, dbFieldName);
51
+ const comparison = compareFieldDefinition(existingColumns[dbFieldName], fieldDef);
52
52
  if (comparison.length > 0) {
53
53
  for (const c of comparison) {
54
54
  // 使用统一的日志格式函数和常量标签
55
- const changeLabel = CHANGE_TYPE_LABELS[c.type] || '未知';
55
+ const changeLabel = CHANGE_TYPE_LABELS[c.type as keyof typeof CHANGE_TYPE_LABELS] || '未知';
56
56
  logFieldChange(tableName, dbFieldName, c.type, c.current, c.expected, changeLabel);
57
57
  }
58
58
 
59
- if (isStringOrArrayType(fieldDef.type) && existingColumns[dbFieldName].max) {
60
- if (existingColumns[dbFieldName].max > fieldDef.max) {
59
+ if (isStringOrArrayType(fieldDef.type) && existingColumns[dbFieldName].max && fieldDef.max !== null) {
60
+ if (existingColumns[dbFieldName].max! > fieldDef.max) {
61
61
  if (force) {
62
62
  Logger.warn(`[强制执行] ${tableName}.${dbFieldName} 长度收缩 ${existingColumns[dbFieldName].max} -> ${fieldDef.max}`);
63
63
  } else {
@@ -106,8 +106,8 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
106
106
  // 若不仅仅是默认值变化,继续生成修改子句
107
107
  if (!onlyDefaultChanged) {
108
108
  let skipModify = false;
109
- if (hasLengthChange && isStringOrArrayType(fieldDef.type) && existingColumns[dbFieldName].max) {
110
- const isShrink = existingColumns[dbFieldName].max > fieldDef.max;
109
+ if (hasLengthChange && isStringOrArrayType(fieldDef.type) && existingColumns[dbFieldName].max && fieldDef.max !== null) {
110
+ const isShrink = existingColumns[dbFieldName].max! > fieldDef.max;
111
111
  if (isShrink && !force) skipModify = true;
112
112
  }
113
113
 
@@ -16,7 +16,7 @@ import { buildSystemColumnDefs, buildBusinessColumnDefs, buildIndexSQL } from '.
16
16
  import { getTableIndexes } from './schema.js';
17
17
 
18
18
  import type { SQL } from 'bun';
19
- import type { FieldDefinition } from 'befly/types/common';
19
+ import type { FieldDefinition } from 'befly-shared/types';
20
20
 
21
21
  /**
22
22
  * 为 PostgreSQL 表添加列注释
@@ -74,7 +74,7 @@ async function createTableIndexes(sql: SQL, tableName: string, fields: Record<st
74
74
  // 获取现有索引(MySQL 不支持 IF NOT EXISTS,需要先检查)
75
75
  let existingIndexes: Record<string, string[]> = {};
76
76
  if (isMySQL()) {
77
- existingIndexes = await getTableIndexes(sql, tableName, dbName);
77
+ existingIndexes = await getTableIndexes(sql, tableName, dbName || '');
78
78
  }
79
79
 
80
80
  // 系统字段索引
@@ -70,7 +70,7 @@ export function getSqlType(fieldType: string, fieldMax: number | null, unsigned:
70
70
  * resolveDefaultValue('admin', 'string') // => 'admin'
71
71
  * resolveDefaultValue(0, 'number') // => 0
72
72
  */
73
- export function resolveDefaultValue(fieldDefault: any, fieldType: 'number' | 'string' | 'text' | 'array'): any {
73
+ export function resolveDefaultValue(fieldDefault: any, fieldType: string): any {
74
74
  // null 或字符串 'null' 都表示使用类型默认值
75
75
  if (fieldDefault !== null && fieldDefault !== 'null') {
76
76
  return fieldDefault;
@@ -83,6 +83,8 @@ export function resolveDefaultValue(fieldDefault: any, fieldType: 'number' | 'st
83
83
  case 'string':
84
84
  return '';
85
85
  case 'array':
86
+ case 'array_string':
87
+ case 'array_text':
86
88
  return '[]';
87
89
  case 'text':
88
90
  // text 类型不设置默认值,保持 'null'
@@ -104,15 +106,16 @@ export function resolveDefaultValue(fieldDefault: any, fieldType: 'number' | 'st
104
106
  * generateDefaultSql('admin', 'string') // => " DEFAULT 'admin'"
105
107
  * generateDefaultSql('', 'string') // => " DEFAULT ''"
106
108
  * generateDefaultSql('null', 'text') // => ''
109
+ * generateDefaultSql('[]', 'array_text') // => '' (TEXT 类型不能有默认值)
107
110
  */
108
- export function generateDefaultSql(actualDefault: any, fieldType: 'number' | 'string' | 'text' | 'array'): string {
109
- // text 类型不设置默认值
110
- if (fieldType === 'text' || actualDefault === 'null') {
111
+ export function generateDefaultSql(actualDefault: any, fieldType: string): string {
112
+ // text 和 array_text 类型不设置默认值(MySQL TEXT 类型不支持默认值)
113
+ if (fieldType === 'text' || fieldType === 'array_text' || actualDefault === 'null') {
111
114
  return '';
112
115
  }
113
116
 
114
- // 仅 number/string/array 类型设置默认值
115
- if (fieldType === 'number' || fieldType === 'string' || fieldType === 'array') {
117
+ // 仅 number/string/array_string 类型设置默认值
118
+ if (fieldType === 'number' || fieldType === 'string' || fieldType === 'array_string') {
116
119
  if (typeof actualDefault === 'number' && !Number.isNaN(actualDefault)) {
117
120
  return ` DEFAULT ${actualDefault}`;
118
121
  } else {
package/sync/syncDb.ts CHANGED
@@ -117,7 +117,7 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
117
117
  applyFieldDefaults(fieldDef);
118
118
  }
119
119
 
120
- const dbName = beflyConfig.db?.database;
120
+ const dbName = beflyConfig.db?.database || '';
121
121
  const existsTable = await tableExists(sql!, tableName, dbName);
122
122
 
123
123
  // 读取 force 参数
package/sync/syncDev.ts CHANGED
@@ -7,8 +7,6 @@
7
7
  * - 表名: addon_admin_admin
8
8
  */
9
9
 
10
- import { scanAddons, getAddonDir, normalizeModuleForSync } from 'befly-shared/addonHelper';
11
-
12
10
  import { Logger } from '../lib/logger.js';
13
11
  import { Cipher } from '../lib/cipher.js';
14
12
  import { Connect } from '../lib/connect.js';
@@ -17,7 +15,7 @@ import { RedisHelper } from '../lib/redisHelper.js';
17
15
  import { CacheHelper } from '../lib/cacheHelper.js';
18
16
  import { beflyConfig } from '../befly.config.js';
19
17
 
20
- import type { SyncDevOptions, SyncDevStats } from '../types/index.js';
18
+ import type { SyncDevOptions } from '../types/index.js';
21
19
 
22
20
  /**
23
21
  * SyncDev 命令主函数
package/sync/syncMenu.ts CHANGED
@@ -243,7 +243,7 @@ async function syncMenus(helper: any, menus: MenuConfig[]): Promise<void> {
243
243
 
244
244
  for (const menu of menus) {
245
245
  try {
246
- await syncMenuRecursive(helper, menu, 0, existingMenuMap, 1);
246
+ await syncMenuRecursive(helper, menu, 0, existingMenuMap);
247
247
  } catch (error: any) {
248
248
  Logger.error({ err: error, menu: menu.name }, '同步菜单失败');
249
249
  throw error;
@@ -261,7 +261,7 @@ describe('CacheHelper', () => {
261
261
 
262
262
  describe('checkRolePermission', () => {
263
263
  it('有权限时返回 true', async () => {
264
- mockRedis.sismember = mock(() => Promise.resolve(1));
264
+ mockRedis.sismember = mock(() => Promise.resolve(true));
265
265
 
266
266
  const result = await cacheHelper.checkRolePermission('admin', 'POST/api/login');
267
267
 
@@ -270,7 +270,7 @@ describe('CacheHelper', () => {
270
270
  });
271
271
 
272
272
  it('无权限时返回 false', async () => {
273
- mockRedis.sismember = mock(() => Promise.resolve(0));
273
+ mockRedis.sismember = mock(() => Promise.resolve(false));
274
274
 
275
275
  const result = await cacheHelper.checkRolePermission('user', 'POST/api/admin/del');
276
276
 
@@ -219,14 +219,14 @@ describe('Cipher - 快速哈希', () => {
219
219
 
220
220
  test('快速哈希 - 字符串输入', () => {
221
221
  const result = Cipher.fastHash(testData);
222
- expect(typeof result).toBe('bigint');
223
- expect(result).toBeGreaterThan(0n);
222
+ expect(typeof result).toBe('number');
223
+ expect(result).toBeGreaterThan(0);
224
224
  });
225
225
 
226
226
  test('快速哈希 - Uint8Array 输入', () => {
227
227
  const bytes = new TextEncoder().encode(testData);
228
228
  const result = Cipher.fastHash(bytes);
229
- expect(typeof result).toBe('bigint');
229
+ expect(typeof result).toBe('number');
230
230
  });
231
231
 
232
232
  test('快速哈希 - 相同输入产生相同哈希', () => {
@@ -243,6 +243,6 @@ describe('Cipher - 快速哈希', () => {
243
243
 
244
244
  test('快速哈希 - 空字符串', () => {
245
245
  const result = Cipher.fastHash('');
246
- expect(typeof result).toBe('bigint');
246
+ expect(typeof result).toBe('number');
247
247
  });
248
248
  });
@@ -93,8 +93,12 @@ describe('generateDefaultSql', () => {
93
93
  expect(generateDefaultSql('null', 'text')).toBe('');
94
94
  });
95
95
 
96
- test('array 类型生成 JSON 数组默认值', () => {
97
- expect(generateDefaultSql('[]', 'array')).toBe(" DEFAULT '[]'");
96
+ test('array_string 类型生成 JSON 数组默认值', () => {
97
+ expect(generateDefaultSql('[]', 'array_string')).toBe(" DEFAULT '[]'");
98
+ });
99
+
100
+ test('array_text 类型不生成默认值(MySQL TEXT 不支持)', () => {
101
+ expect(generateDefaultSql('[]', 'array_text')).toBe('');
98
102
  });
99
103
 
100
104
  test('单引号被正确转义', () => {
package/tsconfig.json CHANGED
@@ -46,11 +46,10 @@
46
46
  "types": ["bun"],
47
47
 
48
48
  // 路径
49
- "baseUrl": ".",
50
49
  "paths": {
51
50
  "@/*": ["./*"]
52
51
  }
53
52
  },
54
53
  "include": ["**/*.ts", "**/*.js"],
55
- "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.js"]
54
+ "exclude": ["node_modules", "dist", "logs", "temp", "main.single.ts", "**/*.test.ts", "**/*.test.js"]
56
55
  }
package/types/befly.d.ts CHANGED
@@ -142,31 +142,31 @@ export interface BeflyOptions {
142
142
  export interface BeflyContext extends KeyValue {
143
143
  // ========== 核心插件 ==========
144
144
  /** 数据库助手 */
145
- db?: DbHelper;
145
+ db: DbHelper;
146
146
 
147
147
  /** Redis 助手 */
148
- redis?: RedisHelper;
148
+ redis: RedisHelper;
149
149
 
150
150
  /** 日志器 */
151
- logger?: typeof Logger;
151
+ logger: typeof Logger;
152
152
 
153
153
  /** 缓存助手 */
154
- cache?: CacheHelper;
154
+ cache: CacheHelper;
155
155
 
156
156
  /** 工具函数 */
157
- tool?: {
157
+ tool: {
158
158
  Yes: (msg: string, data?: any, other?: Record<string, any>) => { code: 0; msg: string; data: any };
159
159
  No: (msg: string, data?: any, other?: Record<string, any>) => { code: 1; msg: string; data: any };
160
160
  };
161
161
 
162
162
  /** 加密解密 */
163
- cipher?: typeof Cipher;
163
+ cipher: typeof Cipher;
164
164
 
165
165
  /** JWT 令牌 */
166
- jwt?: typeof Jwt;
166
+ jwt: Jwt;
167
167
 
168
168
  /** 项目配置 */
169
- config?: BeflyOptions;
169
+ config: BeflyOptions;
170
170
 
171
171
  // ========== 动态插件 ==========
172
172
  /** 组件插件:addon_{addonName}_{pluginName} */
package/types/common.d.ts CHANGED
@@ -3,6 +3,55 @@
3
3
  * Core 专用类型,通用类型请直接从 befly-shared/types 导入
4
4
  */
5
5
 
6
+ import type { SqlValue } from 'befly-shared/types';
7
+
8
+ // ============================================
9
+ // SQL 查询相关类型
10
+ // ============================================
11
+
12
+ /**
13
+ * WHERE 条件操作符
14
+ */
15
+ export type WhereOperator = '$eq' | '$ne' | '$not' | '$gt' | '$gte' | '$lt' | '$lte' | '$like' | '$notLike' | '$in' | '$notIn' | '$nin' | '$isNull' | '$isNotNull' | '$null' | '$notNull' | '$between' | '$notBetween';
16
+
17
+ /**
18
+ * WHERE 条件类型
19
+ */
20
+ export type WhereConditions = Record<string, any>;
21
+
22
+ /**
23
+ * 排序方向
24
+ */
25
+ export type OrderDirection = 'ASC' | 'DESC';
26
+
27
+ /**
28
+ * 排序字段
29
+ */
30
+ export type OrderByField = string | { field: string; direction: OrderDirection };
31
+
32
+ /**
33
+ * SQL 查询结果
34
+ */
35
+ export interface SqlQuery {
36
+ sql: string;
37
+ params: SqlValue[];
38
+ }
39
+
40
+ /**
41
+ * 插入数据类型
42
+ */
43
+ export type InsertData = Record<string, SqlValue> | Record<string, SqlValue>[];
44
+
45
+ /**
46
+ * 更新数据类型
47
+ */
48
+ export type UpdateData = Record<string, SqlValue>;
49
+
50
+ /**
51
+ * 任意对象类型
52
+ */
53
+ export type AnyObject = Record<string, any>;
54
+
6
55
  // ============================================
7
56
  // Core 专用类型(不适合放在 shared 中的类型)
8
57
  // ============================================
@@ -3,15 +3,35 @@
3
3
  */
4
4
 
5
5
  import type { SqlValue } from 'befly-shared/types';
6
- import type { WhereConditions } from './common';
7
6
  import type { DatabaseTables, TableName, TableType, TableInsertType, TableUpdateType, TypedWhereConditions } from './table';
8
7
 
9
- // 重新导出 WhereOperator 和 WhereConditions,供其他模块使用
10
- export type { WhereOperator, WhereConditions } from './index';
11
-
12
8
  // 重新导出表类型工具
13
9
  export type { DatabaseTables, TableName, TableType, TableInsertType, TableUpdateType, SystemFields, BaseTable, InsertType, UpdateType, SelectType, TypedWhereConditions } from './table';
14
10
 
11
+ // ============================================
12
+ // SQL 查询相关类型
13
+ // ============================================
14
+
15
+ /**
16
+ * WHERE 条件操作符
17
+ */
18
+ export type WhereOperator = '$eq' | '$ne' | '$not' | '$gt' | '$gte' | '$lt' | '$lte' | '$like' | '$notLike' | '$in' | '$notIn' | '$nin' | '$isNull' | '$isNotNull' | '$null' | '$notNull' | '$between' | '$notBetween';
19
+
20
+ /**
21
+ * WHERE 条件类型
22
+ */
23
+ export type WhereConditions = Record<string, any>;
24
+
25
+ /**
26
+ * 排序方向
27
+ */
28
+ export type OrderDirection = 'ASC' | 'DESC';
29
+
30
+ /**
31
+ * 排序字段
32
+ */
33
+ export type OrderByField = string | { field: string; direction: OrderDirection };
34
+
15
35
  // ============================================
16
36
  // 泛型查询选项(类型安全版本)
17
37
  // ============================================
package/types/index.ts CHANGED
@@ -8,12 +8,17 @@
8
8
  export * from './api.js';
9
9
  export * from './befly.js';
10
10
  export * from './cache.js';
11
- export * from './common.js';
12
11
  export * from './context.js';
13
- export * from './database.js';
14
12
  export * from './hook.js';
15
13
  export * from './plugin.js';
16
14
  export * from './redis.js';
17
15
  export * from './table.js';
18
- export * from './validator.js';
19
- export * from './sync.js';
16
+
17
+ // common.js 包含基础类型
18
+ export * from './common.js';
19
+
20
+ // database.js 中除了与 common.js 重复的类型外,其他都导出
21
+ export type { TypedQueryOptions, TypedInsertOptions, TypedUpdateOptions, TypedDeleteOptions, QueryOptions, InsertOptions, UpdateOptions, DeleteOptions, ListResult, TransactionCallback, DbType, ColumnInfo, IndexInfo } from './database.js';
22
+
23
+ // sync.js 中不重复的类型
24
+ export type { SyncDbOptions, SyncOptions, SyncMenuOptions, MenuConfig, SyncDevOptions, SyncApiOptions, ApiInfo, SyncMenuStats, SyncApiStats, GlobalStats, PhaseStats, SyncReport, SyncReportMeta, DatabaseReport, ApiReport, MenuReport } from './sync.js';
package/types/plugin.d.ts CHANGED
@@ -15,6 +15,11 @@ export type PluginInitFunction = (befly: BeflyContext) => Promise<any> | any;
15
15
  */
16
16
  export type Next = () => Promise<void>;
17
17
 
18
+ /**
19
+ * 插件请求钩子类型
20
+ */
21
+ export type PluginRequestHook = (ctx: RequestContext, next: Next) => Promise<void> | void;
22
+
18
23
  /**
19
24
  * 插件配置类型
20
25
  */
package/types/sync.d.ts CHANGED
@@ -101,6 +101,7 @@ export interface ColumnInfo {
101
101
  type: string;
102
102
  columnType: string;
103
103
  length: number | null;
104
+ max?: number | null;
104
105
  nullable: boolean;
105
106
  defaultValue: any;
106
107
  comment: string | null;
@@ -117,7 +118,7 @@ export interface IndexInfo {
117
118
  * 字段变更接口
118
119
  */
119
120
  export interface FieldChange {
120
- type: 'length' | 'datatype' | 'comment' | 'default';
121
+ type: 'length' | 'datatype' | 'comment' | 'default' | 'nullable';
121
122
  current: any;
122
123
  expected: any;
123
124
  }