befly 3.0.0 → 3.0.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.
Files changed (60) hide show
  1. package/checks/conflict.ts +35 -114
  2. package/checks/table.ts +31 -63
  3. package/config/env.ts +3 -3
  4. package/config/fields.ts +55 -0
  5. package/config/regexAliases.ts +51 -0
  6. package/config/reserved.ts +1 -1
  7. package/main.ts +17 -71
  8. package/package.json +7 -28
  9. package/plugins/db.ts +11 -10
  10. package/plugins/redis.ts +5 -9
  11. package/scripts/syncDb/apply.ts +3 -3
  12. package/scripts/syncDb/constants.ts +2 -1
  13. package/scripts/syncDb/ddl.ts +15 -8
  14. package/scripts/syncDb/helpers.ts +3 -2
  15. package/scripts/syncDb/index.ts +23 -35
  16. package/scripts/syncDb/state.ts +8 -6
  17. package/scripts/syncDb/table.ts +32 -22
  18. package/scripts/syncDb/tableCreate.ts +9 -3
  19. package/scripts/syncDb/tests/constants.test.ts +2 -1
  20. package/scripts/syncDb.ts +10 -9
  21. package/types/addon.d.ts +53 -0
  22. package/types/api.d.ts +17 -14
  23. package/types/befly.d.ts +2 -6
  24. package/types/context.d.ts +7 -0
  25. package/types/database.d.ts +9 -14
  26. package/types/index.d.ts +442 -8
  27. package/types/index.ts +35 -56
  28. package/types/redis.d.ts +2 -0
  29. package/types/validator.d.ts +0 -2
  30. package/types/validator.ts +43 -0
  31. package/utils/colors.ts +117 -37
  32. package/utils/database.ts +348 -0
  33. package/utils/dbHelper.ts +687 -116
  34. package/utils/helper.ts +812 -0
  35. package/utils/index.ts +10 -23
  36. package/utils/logger.ts +78 -171
  37. package/utils/redisHelper.ts +135 -152
  38. package/{types/context.ts → utils/requestContext.ts} +3 -3
  39. package/utils/sqlBuilder.ts +142 -165
  40. package/utils/validate.ts +51 -9
  41. package/apis/health/info.ts +0 -64
  42. package/apis/tool/tokenCheck.ts +0 -51
  43. package/bin/befly.ts +0 -202
  44. package/bunfig.toml +0 -3
  45. package/plugins/tool.ts +0 -34
  46. package/scripts/syncDev.ts +0 -112
  47. package/system.ts +0 -149
  48. package/tables/_common.json +0 -21
  49. package/tables/admin.json +0 -10
  50. package/utils/addonHelper.ts +0 -60
  51. package/utils/api.ts +0 -23
  52. package/utils/datetime.ts +0 -51
  53. package/utils/errorHandler.ts +0 -68
  54. package/utils/objectHelper.ts +0 -68
  55. package/utils/pluginHelper.ts +0 -62
  56. package/utils/response.ts +0 -38
  57. package/utils/sqlHelper.ts +0 -447
  58. package/utils/tableHelper.ts +0 -167
  59. package/utils/tool.ts +0 -230
  60. package/utils/typeHelper.ts +0 -101
@@ -8,7 +8,8 @@
8
8
  */
9
9
 
10
10
  import { Logger } from '../../utils/logger.js';
11
- import { parseRule } from '../../utils/tableHelper.js';
11
+ import { toSnakeCase } from '../../utils/helper.js';
12
+ import { parseRule } from '../../utils/helper.js';
12
13
  import { IS_MYSQL, IS_PG, IS_SQLITE, SYSTEM_INDEX_FIELDS, CHANGE_TYPE_LABELS, typeMapping } from './constants.js';
13
14
  import { quoteIdentifier, logFieldChange, resolveDefaultValue, generateDefaultSql, isStringOrArrayType, getSqlType } from './helpers.js';
14
15
  import { buildIndexSQL, generateDDLClause, isPgCompatibleTypeChange } from './ddl.js';
@@ -56,13 +57,16 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
56
57
  const indexActions = [];
57
58
 
58
59
  for (const [fieldKey, fieldRule] of Object.entries(fields)) {
59
- if (existingColumns[fieldKey]) {
60
- const comparison = compareFieldDefinition(existingColumns[fieldKey], fieldRule, fieldKey);
60
+ // 转换字段名为下划线格式(用于与数据库字段对比)
61
+ const dbFieldName = toSnakeCase(fieldKey);
62
+
63
+ if (existingColumns[dbFieldName]) {
64
+ const comparison = compareFieldDefinition(existingColumns[dbFieldName], fieldRule, dbFieldName);
61
65
  if (comparison.length > 0) {
62
66
  for (const c of comparison) {
63
67
  // 使用统一的日志格式函数和常量标签
64
68
  const changeLabel = CHANGE_TYPE_LABELS[c.type] || '未知';
65
- logFieldChange(tableName, fieldKey, c.type, c.current, c.expected, changeLabel);
69
+ logFieldChange(tableName, dbFieldName, c.type, c.current, c.expected, changeLabel);
66
70
 
67
71
  // 全量计数:全局累加
68
72
  if (c.type === 'datatype') globalCount.typeChanges++;
@@ -74,9 +78,9 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
74
78
  const parsed = parseRule(fieldRule);
75
79
  const { name: fieldName, type: fieldType, max: fieldMax, default: fieldDefault } = parsed;
76
80
 
77
- if (isStringOrArrayType(fieldType) && existingColumns[fieldKey].length) {
78
- if (existingColumns[fieldKey].length! > fieldMax) {
79
- Logger.warn(`[跳过危险变更] ${tableName}.${fieldKey} 长度收缩 ${existingColumns[fieldKey].length} -> ${fieldMax} 已被跳过(设置 SYNC_DISALLOW_SHRINK=0 可放开)`);
81
+ if (isStringOrArrayType(fieldType) && existingColumns[dbFieldName].length) {
82
+ if (existingColumns[dbFieldName].length! > fieldMax) {
83
+ Logger.warn(`[跳过危险变更] ${tableName}.${dbFieldName} 长度收缩 ${existingColumns[dbFieldName].length} -> ${fieldMax} 已被跳过(设置 SYNC_DISALLOW_SHRINK=0 可放开)`);
80
84
  }
81
85
  }
82
86
 
@@ -87,9 +91,9 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
87
91
 
88
92
  // 严格限制:除 string/array 互转外,禁止任何字段类型变更;一旦发现,立即终止同步
89
93
  if (hasTypeChange) {
90
- const currentSqlType = String(existingColumns[fieldKey].type || '').toLowerCase();
94
+ const currentSqlType = String(existingColumns[dbFieldName].type || '').toLowerCase();
91
95
  const newSqlType = String(typeMapping[fieldType] || '').toLowerCase();
92
- const errorMsg = [`禁止字段类型变更: ${tableName}.${fieldKey}`, `当前类型: ${currentSqlType}`, `目标类型: ${newSqlType}`, `说明: 仅允许 string<->array 互相切换,其他类型变更需要手动处理`].join('\n');
96
+ const errorMsg = [`禁止字段类型变更: ${tableName}.${dbFieldName}`, `当前类型: ${currentSqlType}`, `目标类型: ${newSqlType}`, `说明: 仅允许 string<->array 互相切换,其他类型变更需要手动处理`].join('\n');
93
97
  throw new Error(errorMsg);
94
98
  }
95
99
 
@@ -108,11 +112,11 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
108
112
 
109
113
  if (v !== null && v !== '') {
110
114
  if (IS_PG) {
111
- defaultClauses.push(`ALTER COLUMN "${fieldKey}" SET DEFAULT ${v}`);
115
+ defaultClauses.push(`ALTER COLUMN "${dbFieldName}" SET DEFAULT ${v}`);
112
116
  } else if (IS_MYSQL && onlyDefaultChanged) {
113
117
  // MySQL 的 TEXT/BLOB 不允许 DEFAULT,跳过 text 类型
114
118
  if (fieldType !== 'text') {
115
- defaultClauses.push(`ALTER COLUMN \`${fieldKey}\` SET DEFAULT ${v}`);
119
+ defaultClauses.push(`ALTER COLUMN \`${dbFieldName}\` SET DEFAULT ${v}`);
116
120
  }
117
121
  }
118
122
  }
@@ -121,15 +125,15 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
121
125
  // 若不仅仅是默认值变化,继续生成修改子句
122
126
  if (!onlyDefaultChanged) {
123
127
  let skipModify = false;
124
- if (hasLengthChange && isStringOrArrayType(fieldType) && existingColumns[fieldKey].length) {
125
- const oldLen = existingColumns[fieldKey].length!;
128
+ if (hasLengthChange && isStringOrArrayType(fieldType) && existingColumns[dbFieldName].length) {
129
+ const oldLen = existingColumns[dbFieldName].length!;
126
130
  const isShrink = oldLen > fieldMax;
127
131
  if (isShrink) skipModify = true;
128
132
  }
129
133
 
130
134
  if (hasTypeChange) {
131
- if (IS_PG && isPgCompatibleTypeChange(existingColumns[fieldKey].type, typeMapping[fieldType].toLowerCase())) {
132
- Logger.info(`[PG兼容类型变更] ${tableName}.${fieldKey} ${existingColumns[fieldKey].type} -> ${typeMapping[fieldType].toLowerCase()} 允许执行`);
135
+ if (IS_PG && isPgCompatibleTypeChange(existingColumns[dbFieldName].type, typeMapping[fieldType].toLowerCase())) {
136
+ Logger.info(`[PG兼容类型变更] ${tableName}.${dbFieldName} ${existingColumns[dbFieldName].type} -> ${typeMapping[fieldType].toLowerCase()} 允许执行`);
133
137
  }
134
138
  }
135
139
 
@@ -141,7 +145,7 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
141
145
  const parsed = parseRule(fieldRule);
142
146
  const { name: fieldName, type: fieldType, max: fieldMax, default: fieldDefault } = parsed;
143
147
  const lenPart = isStringOrArrayType(fieldType) ? ` 长度:${parseInt(String(fieldMax))}` : '';
144
- Logger.info(`[新增字段] ${tableName}.${fieldKey} 类型:${fieldType}${lenPart} 默认:${fieldDefault ?? 'NULL'}`);
148
+ Logger.info(`[新增字段] ${tableName}.${dbFieldName} 类型:${fieldType}${lenPart} 默认:${fieldDefault ?? 'NULL'}`);
145
149
  addClauses.push(generateDDLClause(fieldKey, fieldRule, true));
146
150
  changed = true;
147
151
  globalCount.addFields++;
@@ -160,14 +164,17 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
160
164
 
161
165
  // 检查业务字段索引
162
166
  for (const [fieldKey, fieldRule] of Object.entries(fields)) {
167
+ // 转换字段名为下划线格式
168
+ const dbFieldName = toSnakeCase(fieldKey);
169
+
163
170
  const parsed = parseRule(fieldRule);
164
- const indexName = `idx_${fieldKey}`;
171
+ const indexName = `idx_${dbFieldName}`;
165
172
  if (parsed.index === 1 && !existingIndexes[indexName]) {
166
- indexActions.push({ action: 'create', indexName, fieldName: fieldKey });
173
+ indexActions.push({ action: 'create', indexName, fieldName: dbFieldName });
167
174
  changed = true;
168
175
  globalCount.indexCreate++;
169
176
  } else if (!(parsed.index === 1) && existingIndexes[indexName] && existingIndexes[indexName].length === 1) {
170
- indexActions.push({ action: 'drop', indexName, fieldName: fieldKey });
177
+ indexActions.push({ action: 'drop', indexName, fieldName: dbFieldName });
171
178
  changed = true;
172
179
  globalCount.indexDrop++;
173
180
  }
@@ -177,13 +184,16 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
177
184
  const commentActions = [];
178
185
  if (IS_PG) {
179
186
  for (const [fieldKey, fieldRule] of Object.entries(fields)) {
180
- if (existingColumns[fieldKey]) {
187
+ // 转换字段名为下划线格式
188
+ const dbFieldName = toSnakeCase(fieldKey);
189
+
190
+ if (existingColumns[dbFieldName]) {
181
191
  const parsed = parseRule(fieldRule);
182
192
  const { name: fieldName } = parsed;
183
- const curr = existingColumns[fieldKey].comment || '';
193
+ const curr = existingColumns[dbFieldName].comment || '';
184
194
  const want = fieldName && fieldName !== 'null' ? String(fieldName) : '';
185
195
  if (want !== curr) {
186
- commentActions.push(`COMMENT ON COLUMN "${tableName}"."${fieldKey}" IS ${want ? `'${want}'` : 'NULL'}`);
196
+ commentActions.push(`COMMENT ON COLUMN "${tableName}"."${dbFieldName}" IS ${want ? `'${want}'` : 'NULL'}`);
187
197
  changed = true;
188
198
  }
189
199
  }
@@ -13,7 +13,7 @@ import { Logger } from '../../utils/logger.js';
13
13
  import { IS_MYSQL, IS_PG, MYSQL_TABLE_CONFIG } from './constants.js';
14
14
  import { quoteIdentifier } from './helpers.js';
15
15
  import { buildSystemColumnDefs, buildBusinessColumnDefs, buildIndexSQL } from './ddl.js';
16
- import { parseRule } from '../../utils/tableHelper.js';
16
+ import { parseRule, toSnakeCase } from '../../utils/helper.js';
17
17
  import type { SQL } from 'bun';
18
18
 
19
19
  // 是否为计划模式(从环境变量读取)
@@ -47,9 +47,12 @@ async function addPostgresComments(sql: SQL, tableName: string, fields: Record<s
47
47
 
48
48
  // 业务字段注释
49
49
  for (const [fieldKey, fieldRule] of Object.entries(fields)) {
50
+ // 转换字段名为下划线格式
51
+ const dbFieldName = toSnakeCase(fieldKey);
52
+
50
53
  const parsed = parseRule(fieldRule);
51
54
  const { name: fieldName } = parsed;
52
- const stmt = `COMMENT ON COLUMN "${tableName}"."${fieldKey}" IS '${fieldName}'`;
55
+ const stmt = `COMMENT ON COLUMN "${tableName}"."${dbFieldName}" IS '${fieldName}'`;
53
56
  if (IS_PLAN) {
54
57
  Logger.info(`[计划] ${stmt}`);
55
58
  } else {
@@ -81,9 +84,12 @@ async function createTableIndexes(sql: SQL, tableName: string, fields: Record<st
81
84
 
82
85
  // 业务字段索引
83
86
  for (const [fieldKey, fieldRule] of Object.entries(fields)) {
87
+ // 转换字段名为下划线格式
88
+ const dbFieldName = toSnakeCase(fieldKey);
89
+
84
90
  const parsed = parseRule(fieldRule);
85
91
  if (parsed.index === 1) {
86
- const stmt = buildIndexSQL(tableName, `idx_${fieldKey}`, fieldKey, 'create');
92
+ const stmt = buildIndexSQL(tableName, `idx_${dbFieldName}`, dbFieldName, 'create');
87
93
  if (IS_PLAN) {
88
94
  Logger.info(`[计划] ${stmt}`);
89
95
  } else {
@@ -82,7 +82,8 @@ describe('syncDb/constants', () => {
82
82
  expect(typeMapping.number).toBeDefined();
83
83
  expect(typeMapping.string).toBeDefined();
84
84
  expect(typeMapping.text).toBeDefined();
85
- expect(typeMapping.array).toBeDefined();
85
+ expect(typeMapping.array_string).toBeDefined();
86
+ expect(typeMapping.array_text).toBeDefined();
86
87
  });
87
88
 
88
89
  test('不同数据库的类型映射应不同', () => {
package/scripts/syncDb.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { SyncDb } from './syncDb/index.js';
2
-
3
- // 如果直接运行此脚本
4
- if (import.meta.main) {
5
- SyncDb().catch((error) => {
6
- console.error('❌ 数据库同步失败:', error);
7
- process.exit(1);
8
- });
9
- }
1
+ import { SyncDb } from './syncDb/index.js';
2
+ import { Logger } from '../utils/logger.js';
3
+
4
+ // 如果直接运行此脚本
5
+ if (import.meta.main) {
6
+ SyncDb().catch((error) => {
7
+ Logger.error('数据库同步失败', error);
8
+ process.exit(1);
9
+ });
10
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Addon 配置类型定义
3
+ */
4
+
5
+ /**
6
+ * Addon 作者信息
7
+ */
8
+ export interface AddonAuthor {
9
+ /** 作者名称 */
10
+ name: string;
11
+ /** 作者邮箱 */
12
+ email?: string;
13
+ /** 作者网站 */
14
+ url?: string;
15
+ }
16
+
17
+ /**
18
+ * Addon 配置
19
+ */
20
+ export interface AddonConfig {
21
+ /** Addon 唯一标识(小写、短横线或下划线),例如 "admin"、"demo" */
22
+ name: string;
23
+
24
+ /** Addon 人类可读名称,例如 "管理后台" */
25
+ title: string;
26
+
27
+ /** 版本号(语义化版本),例如 "1.0.0" */
28
+ version?: string;
29
+
30
+ /** 简短描述 */
31
+ description?: string;
32
+
33
+ /** 作者信息 */
34
+ author?: AddonAuthor | string;
35
+
36
+ /** 源码仓库链接 */
37
+ repo?: string;
38
+
39
+ /** 关键词(用于搜索和分类) */
40
+ keywords?: string[];
41
+
42
+ /** 主入口文件路径(相对于 addon 目录),例如 "index.ts" */
43
+ entry?: string;
44
+
45
+ /** 是否默认启用 */
46
+ enabled?: boolean;
47
+
48
+ /** 依赖的其他 addon 或核心包 */
49
+ dependencies?: Record<string, string>;
50
+
51
+ /** 许可证 */
52
+ license?: string;
53
+ }
package/types/api.d.ts CHANGED
@@ -99,26 +99,29 @@ export interface ApiOptions {
99
99
  * API 路由配置
100
100
  */
101
101
  export interface ApiRoute<T = any, R = any> {
102
- /** HTTP 方法 */
103
- method: HttpMethod;
104
-
105
- /** 接口名称 */
102
+ /** 接口名称(必填) */
106
103
  name: string;
107
104
 
108
- /** 路由路径(运行时生成) */
109
- route?: string;
105
+ /** 处理器函数(必填) */
106
+ handler: ApiHandler<T, R>;
110
107
 
111
- /** 认证类型 */
112
- auth: boolean | string | string[];
108
+ /** HTTP 方法(可选,默认 POST) */
109
+ method?: HttpMethod;
113
110
 
114
- /** 字段定义(验证规则) */
115
- fields: TableDefinition;
111
+ /** 认证类型(可选,默认 true)
112
+ * - true: 需要登录
113
+ * - false: 公开访问(无需登录)
114
+ */
115
+ auth?: boolean;
116
116
 
117
- /** 必填字段 */
118
- required: string[];
117
+ /** 字段定义(验证规则)(可选,默认 {}) */
118
+ fields?: TableDefinition;
119
119
 
120
- /** 处理器函数 */
121
- handler: ApiHandler<T, R>;
120
+ /** 必填字段(可选,默认 []) */
121
+ required?: string[];
122
+
123
+ /** 路由路径(运行时生成) */
124
+ route?: string;
122
125
  }
123
126
 
124
127
  /**
package/types/befly.d.ts CHANGED
@@ -8,9 +8,8 @@ import type { KeyValue } from './common.js';
8
8
  import type { Logger } from '../utils/logger.js';
9
9
  import type { Jwt } from '../utils/jwt.js';
10
10
  import type { Validator } from '../utils/validate.js';
11
- import type { SqlHelper } from '../utils/sqlHelper.js';
11
+ import type { DbHelper } from '../utils/dbHelper.js';
12
12
  import type { Crypto2 } from '../utils/crypto.js';
13
- import type { Tool } from '../utils/tool.js';
14
13
 
15
14
  /**
16
15
  * Befly 应用选项
@@ -79,14 +78,11 @@ export interface Befly {
79
78
  validator: Validator;
80
79
 
81
80
  /** SQL 管理器 */
82
- sql: SqlHelper;
81
+ sql: DbHelper;
83
82
 
84
83
  /** 加密工具 */
85
84
  crypto: Crypto2;
86
85
 
87
- /** 通用工具 */
88
- tool: Tool;
89
-
90
86
  /** 数据库连接 */
91
87
  db: any;
92
88
 
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 请求上下文类型定义
3
+ * 实现代码位于 utils/requestContext.ts
4
+ */
5
+
6
+ // 重新导出 RequestContext 类
7
+ export { RequestContext } from '../utils/requestContext.js';
@@ -17,16 +17,12 @@ export interface QueryOptions {
17
17
  fields?: string[];
18
18
  /** WHERE 条件 */
19
19
  where?: WhereConditions;
20
- /** 排序 */
21
- orderBy?: string;
20
+ /** 排序(格式:["字段#ASC", "字段#DESC"]) */
21
+ orderBy?: string[];
22
22
  /** 页码(从 1 开始) */
23
23
  page?: number;
24
24
  /** 每页数量 */
25
25
  limit?: number;
26
- /** 是否包含已删除数据 */
27
- includeDeleted?: boolean;
28
- /** 自定义 state 条件 */
29
- customState?: WhereConditions;
30
26
  }
31
27
 
32
28
  /**
@@ -49,8 +45,6 @@ export interface UpdateOptions {
49
45
  data: Record<string, any>;
50
46
  /** WHERE 条件 */
51
47
  where: WhereConditions;
52
- /** 是否包含已删除数据 */
53
- includeDeleted?: boolean;
54
48
  }
55
49
 
56
50
  /**
@@ -61,8 +55,6 @@ export interface DeleteOptions {
61
55
  table: string;
62
56
  /** WHERE 条件 */
63
57
  where: WhereConditions;
64
- /** 是否物理删除(false=软删除,true=物理删除) */
65
- hard?: boolean;
66
58
  }
67
59
 
68
60
  /**
@@ -84,7 +76,7 @@ export interface ListResult<T = any> {
84
76
  /**
85
77
  * 事务回调函数
86
78
  */
87
- export type TransactionCallback<T = any> = (trans: SqlHelper) => Promise<T>;
79
+ export type TransactionCallback<T = any> = (trans: DbHelper) => Promise<T>;
88
80
 
89
81
  /**
90
82
  * SQL 查询结果
@@ -154,10 +146,11 @@ export interface SyncStats {
154
146
  }
155
147
 
156
148
  /**
157
- * SqlHelper 接口(前向声明)
149
+ * DbHelper 接口(前向声明)
158
150
  */
159
- export interface SqlHelper {
160
- getDetail<T = any>(options: QueryOptions): Promise<T | null>;
151
+ export interface DbHelper {
152
+ getCount(options: Omit<QueryOptions, 'fields' | 'page' | 'limit' | 'orderBy'>): Promise<number>;
153
+ getOne<T = any>(options: QueryOptions): Promise<T | null>;
161
154
  getList<T = any>(options: QueryOptions): Promise<ListResult<T>>;
162
155
  getAll<T = any>(options: Omit<QueryOptions, 'page' | 'limit'>): Promise<T[]>;
163
156
  insData(options: InsertOptions): Promise<number>;
@@ -175,6 +168,8 @@ export interface SqlClientOptions {
175
168
  max?: number;
176
169
  /** 是否使用 BigInt */
177
170
  bigint?: boolean;
171
+ /** 连接超时时间(毫秒),默认 5000ms */
172
+ connectionTimeout?: number;
178
173
  /** 其他自定义选项 */
179
174
  [key: string]: any;
180
175
  }