befly 3.9.38 → 3.9.39

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 (155) hide show
  1. package/README.md +37 -38
  2. package/befly.config.ts +62 -40
  3. package/checks/checkApi.ts +16 -16
  4. package/checks/checkApp.ts +19 -25
  5. package/checks/checkTable.ts +42 -42
  6. package/docs/README.md +42 -35
  7. package/docs/{api.md → api/api.md} +223 -231
  8. package/docs/cipher.md +71 -69
  9. package/docs/database.md +143 -141
  10. package/docs/{examples.md → guide/examples.md} +181 -181
  11. package/docs/guide/quickstart.md +331 -0
  12. package/docs/hooks/auth.md +38 -0
  13. package/docs/hooks/cors.md +28 -0
  14. package/docs/{hook.md → hooks/hook.md} +140 -57
  15. package/docs/hooks/parser.md +19 -0
  16. package/docs/hooks/rateLimit.md +47 -0
  17. package/docs/{redis.md → infra/redis.md} +84 -93
  18. package/docs/plugins/cipher.md +61 -0
  19. package/docs/plugins/database.md +128 -0
  20. package/docs/{plugin.md → plugins/plugin.md} +83 -81
  21. package/docs/quickstart.md +26 -26
  22. package/docs/{addon.md → reference/addon.md} +46 -46
  23. package/docs/{config.md → reference/config.md} +32 -80
  24. package/docs/{logger.md → reference/logger.md} +52 -52
  25. package/docs/{sync.md → reference/sync.md} +32 -35
  26. package/docs/{table.md → reference/table.md} +1 -1
  27. package/docs/{validator.md → reference/validator.md} +57 -57
  28. package/hooks/auth.ts +8 -4
  29. package/hooks/cors.ts +13 -13
  30. package/hooks/parser.ts +37 -17
  31. package/hooks/permission.ts +26 -14
  32. package/hooks/rateLimit.ts +276 -0
  33. package/hooks/validator.ts +7 -7
  34. package/lib/asyncContext.ts +43 -0
  35. package/lib/cacheHelper.ts +212 -77
  36. package/lib/cacheKeys.ts +38 -0
  37. package/lib/cipher.ts +30 -30
  38. package/lib/connect.ts +28 -28
  39. package/lib/dbHelper.ts +183 -102
  40. package/lib/jwt.ts +16 -16
  41. package/lib/logger.ts +610 -19
  42. package/lib/redisHelper.ts +185 -44
  43. package/lib/sqlBuilder.ts +90 -91
  44. package/lib/validator.ts +59 -39
  45. package/loader/loadApis.ts +48 -44
  46. package/loader/loadHooks.ts +40 -14
  47. package/loader/loadPlugins.ts +16 -17
  48. package/main.ts +57 -47
  49. package/package.json +47 -45
  50. package/paths.ts +15 -14
  51. package/plugins/cache.ts +5 -4
  52. package/plugins/cipher.ts +3 -3
  53. package/plugins/config.ts +2 -2
  54. package/plugins/db.ts +9 -9
  55. package/plugins/jwt.ts +3 -3
  56. package/plugins/logger.ts +8 -12
  57. package/plugins/redis.ts +8 -8
  58. package/plugins/tool.ts +6 -6
  59. package/router/api.ts +85 -56
  60. package/router/static.ts +12 -12
  61. package/sync/syncAll.ts +12 -12
  62. package/sync/syncApi.ts +55 -52
  63. package/sync/syncDb/apply.ts +20 -19
  64. package/sync/syncDb/constants.ts +25 -23
  65. package/sync/syncDb/ddl.ts +35 -36
  66. package/sync/syncDb/helpers.ts +6 -9
  67. package/sync/syncDb/schema.ts +10 -9
  68. package/sync/syncDb/sqlite.ts +7 -8
  69. package/sync/syncDb/table.ts +37 -35
  70. package/sync/syncDb/tableCreate.ts +21 -20
  71. package/sync/syncDb/types.ts +23 -20
  72. package/sync/syncDb/version.ts +10 -10
  73. package/sync/syncDb.ts +43 -36
  74. package/sync/syncDev.ts +74 -65
  75. package/sync/syncMenu.ts +190 -55
  76. package/tests/api-integration-array-number.test.ts +282 -0
  77. package/tests/befly-config-env.test.ts +78 -0
  78. package/tests/cacheHelper.test.ts +135 -104
  79. package/tests/cacheKeys.test.ts +41 -0
  80. package/tests/cipher.test.ts +90 -89
  81. package/tests/dbHelper-advanced.test.ts +140 -134
  82. package/tests/dbHelper-all-array-types.test.ts +316 -0
  83. package/tests/dbHelper-array-serialization.test.ts +258 -0
  84. package/tests/dbHelper-columns.test.ts +56 -55
  85. package/tests/dbHelper-execute.test.ts +45 -44
  86. package/tests/dbHelper-joins.test.ts +124 -119
  87. package/tests/fields-redis-cache.test.ts +29 -27
  88. package/tests/fields-validate.test.ts +38 -38
  89. package/tests/getClientIp.test.ts +54 -0
  90. package/tests/integration.test.ts +69 -67
  91. package/tests/jwt.test.ts +27 -26
  92. package/tests/logger.test.ts +267 -34
  93. package/tests/rateLimit-hook.test.ts +477 -0
  94. package/tests/redisHelper.test.ts +187 -188
  95. package/tests/redisKeys.test.ts +6 -73
  96. package/tests/scanConfig.test.ts +144 -0
  97. package/tests/sqlBuilder-advanced.test.ts +217 -215
  98. package/tests/sqlBuilder.test.ts +92 -91
  99. package/tests/sync-connection.test.ts +29 -29
  100. package/tests/syncDb-apply.test.ts +97 -96
  101. package/tests/syncDb-array-number.test.ts +160 -0
  102. package/tests/syncDb-constants.test.ts +48 -47
  103. package/tests/syncDb-ddl.test.ts +99 -98
  104. package/tests/syncDb-helpers.test.ts +29 -28
  105. package/tests/syncDb-schema.test.ts +61 -60
  106. package/tests/syncDb-types.test.ts +60 -59
  107. package/tests/syncMenu-paths.test.ts +68 -0
  108. package/tests/util.test.ts +42 -41
  109. package/tests/validator-array-number.test.ts +310 -0
  110. package/tests/validator-default.test.ts +373 -0
  111. package/tests/validator.test.ts +271 -266
  112. package/tsconfig.json +4 -5
  113. package/types/api.d.ts +7 -12
  114. package/types/befly.d.ts +60 -13
  115. package/types/cache.d.ts +8 -4
  116. package/types/common.d.ts +17 -9
  117. package/types/context.d.ts +2 -2
  118. package/types/crypto.d.ts +23 -0
  119. package/types/database.d.ts +19 -19
  120. package/types/hook.d.ts +2 -2
  121. package/types/jwt.d.ts +118 -0
  122. package/types/logger.d.ts +30 -0
  123. package/types/plugin.d.ts +4 -4
  124. package/types/redis.d.ts +7 -3
  125. package/types/roleApisCache.ts +23 -0
  126. package/types/sync.d.ts +10 -10
  127. package/types/table.d.ts +50 -9
  128. package/types/validate.d.ts +69 -0
  129. package/utils/addonHelper.ts +90 -0
  130. package/utils/arrayKeysToCamel.ts +18 -0
  131. package/utils/calcPerfTime.ts +13 -0
  132. package/utils/configTypes.ts +3 -0
  133. package/utils/cors.ts +19 -0
  134. package/utils/fieldClear.ts +75 -0
  135. package/utils/genShortId.ts +12 -0
  136. package/utils/getClientIp.ts +45 -0
  137. package/utils/keysToCamel.ts +22 -0
  138. package/utils/keysToSnake.ts +22 -0
  139. package/utils/modules.ts +98 -0
  140. package/utils/pickFields.ts +19 -0
  141. package/utils/process.ts +56 -0
  142. package/utils/regex.ts +225 -0
  143. package/utils/response.ts +115 -0
  144. package/utils/route.ts +23 -0
  145. package/utils/scanConfig.ts +142 -0
  146. package/utils/scanFiles.ts +48 -0
  147. package/.prettierignore +0 -2
  148. package/.prettierrc +0 -12
  149. package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
  150. package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
  151. package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
  152. package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
  153. package/hooks/requestLogger.ts +0 -84
  154. package/types/index.ts +0 -24
  155. package/util.ts +0 -283
@@ -1,4 +1,4 @@
1
- /**
1
+ /**
2
2
  * syncDb 表操作模块
3
3
  *
4
4
  * 包含:
@@ -7,16 +7,19 @@
7
7
  * - 应用变更计划
8
8
  */
9
9
 
10
- import { snakeCase } from 'es-toolkit/string';
11
- import { Logger } from '../../lib/logger.js';
12
- import { isMySQL, isPG, CHANGE_TYPE_LABELS, getTypeMapping, SYSTEM_INDEX_FIELDS } from './constants.js';
13
- import { logFieldChange, resolveDefaultValue, generateDefaultSql, isStringOrArrayType } from './helpers.js';
14
- import { generateDDLClause, getSystemColumnDef, isCompatibleTypeChange } from './ddl.js';
15
- import { getTableColumns, getTableIndexes } from './schema.js';
16
- import { compareFieldDefinition, applyTablePlan } from './apply.js';
17
- import type { TablePlan } from '../../types/sync.js';
18
- import type { SQL } from 'bun';
19
- import type { FieldDefinition } from 'befly-shared/types';
10
+ import type { TablePlan } from "../../types/sync.js";
11
+ import type { FieldDefinition } from "../../types/validate.js";
12
+ import type { SQL } from "bun";
13
+
14
+ import { snakeCase } from "es-toolkit/string";
15
+
16
+ import { Logger } from "../../lib/logger.js";
17
+ import { compareFieldDefinition, applyTablePlan } from "./apply.js";
18
+ import { isMySQL, isPG, CHANGE_TYPE_LABELS, getTypeMapping, SYSTEM_INDEX_FIELDS } from "./constants.js";
19
+ import { generateDDLClause, getSystemColumnDef, isCompatibleTypeChange } from "./ddl.js";
20
+ import { logFieldChange } from "./helpers.js";
21
+ import { getTableColumns, getTableIndexes } from "./schema.js";
22
+ import { generateDefaultSql, isStringOrArrayType, resolveDefaultValue } from "./types.js";
20
23
 
21
24
  /**
22
25
  * 同步表结构(对比和应用变更)
@@ -34,14 +37,14 @@ import type { FieldDefinition } from 'befly-shared/types';
34
37
  * @param dbName - 数据库名称
35
38
  */
36
39
  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 || '');
40
+ const existingColumns = await getTableColumns(sql, tableName, dbName || "");
41
+ const existingIndexes = await getTableIndexes(sql, tableName, dbName || "");
39
42
  let changed = false;
40
43
 
41
44
  const addClauses: string[] = [];
42
45
  const modifyClauses: string[] = [];
43
46
  const defaultClauses: string[] = [];
44
- const indexActions: Array<{ action: 'create' | 'drop'; indexName: string; fieldName: string }> = [];
47
+ const indexActions: Array<{ action: "create" | "drop"; indexName: string; fieldName: string }> = [];
45
48
 
46
49
  for (const [fieldKey, fieldDef] of Object.entries(fields)) {
47
50
  // 转换字段名为下划线格式
@@ -52,7 +55,7 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
52
55
  if (comparison.length > 0) {
53
56
  for (const c of comparison) {
54
57
  // 使用统一的日志格式函数和常量标签
55
- const changeLabel = CHANGE_TYPE_LABELS[c.type as keyof typeof CHANGE_TYPE_LABELS] || '未知';
58
+ const changeLabel = CHANGE_TYPE_LABELS[c.type as keyof typeof CHANGE_TYPE_LABELS] || "未知";
56
59
  logFieldChange(tableName, dbFieldName, c.type, c.current, c.expected, changeLabel);
57
60
  }
58
61
 
@@ -66,20 +69,20 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
66
69
  }
67
70
  }
68
71
 
69
- const hasTypeChange = comparison.some((c) => c.type === 'datatype');
70
- const hasLengthChange = comparison.some((c) => c.type === 'length');
71
- const onlyDefaultChanged = comparison.every((c) => c.type === 'default');
72
- const defaultChanged = comparison.some((c) => c.type === 'default');
72
+ const hasTypeChange = comparison.some((c) => c.type === "datatype");
73
+ const hasLengthChange = comparison.some((c) => c.type === "length");
74
+ const onlyDefaultChanged = comparison.every((c) => c.type === "default");
75
+ const defaultChanged = comparison.some((c) => c.type === "default");
73
76
 
74
77
  // 类型变更检查:只允许兼容的宽化型变更(如 INT -> BIGINT)
75
78
  if (hasTypeChange) {
76
- const typeChange = comparison.find((c) => c.type === 'datatype');
77
- const currentType = String(typeChange?.current || '').toLowerCase();
79
+ const typeChange = comparison.find((c) => c.type === "datatype");
80
+ const currentType = String(typeChange?.current || "").toLowerCase();
78
81
  const typeMapping = getTypeMapping();
79
- const expectedType = typeMapping[fieldDef.type]?.toLowerCase() || '';
82
+ const expectedType = typeMapping[fieldDef.type]?.toLowerCase() || "";
80
83
 
81
84
  if (!isCompatibleTypeChange(currentType, expectedType)) {
82
- const errorMsg = [`禁止字段类型变更: ${tableName}.${dbFieldName}`, `当前类型: ${typeChange?.current}`, `目标类型: ${typeChange?.expected}`, `说明: 仅允许宽化型变更(如 INT->BIGINT, VARCHAR->TEXT),其他类型变更需要手动处理`].join('\n');
85
+ const errorMsg = [`禁止字段类型变更: ${tableName}.${dbFieldName}`, `当前类型: ${typeChange?.current}`, `目标类型: ${typeChange?.expected}`, `说明: 仅允许宽化型变更(如 INT->BIGINT, VARCHAR->TEXT),其他类型变更需要手动处理`].join("\n");
83
86
  throw new Error(errorMsg);
84
87
  }
85
88
  Logger.debug(`[兼容类型变更] ${tableName}.${dbFieldName} ${currentType} -> ${expectedType}`);
@@ -92,18 +95,18 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
92
95
 
93
96
  // 生成 SQL DEFAULT 值(不包含前导空格,因为要用于 ALTER COLUMN)
94
97
  let v: string | null = null;
95
- if (actualDefault !== 'null') {
98
+ if (actualDefault !== "null") {
96
99
  const defaultSql = generateDefaultSql(actualDefault, fieldDef.type);
97
100
  // 移除前导空格 ' DEFAULT ' -> 'DEFAULT '
98
- v = defaultSql.trim().replace(/^DEFAULT\s+/, '');
101
+ v = defaultSql.trim().replace(/^DEFAULT\s+/, "");
99
102
  }
100
103
 
101
- if (v !== null && v !== '') {
104
+ if (v !== null && v !== "") {
102
105
  if (isPG()) {
103
106
  defaultClauses.push(`ALTER COLUMN "${dbFieldName}" SET DEFAULT ${v}`);
104
107
  } else if (isMySQL() && onlyDefaultChanged) {
105
108
  // MySQL 的 TEXT/BLOB 不允许 DEFAULT,跳过 text 类型
106
- if (fieldDef.type !== 'text') {
109
+ if (fieldDef.type !== "text") {
107
110
  defaultClauses.push(`ALTER COLUMN \`${dbFieldName}\` SET DEFAULT ${v}`);
108
111
  }
109
112
  }
@@ -123,7 +126,6 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
123
126
  changed = true;
124
127
  }
125
128
  } else {
126
- const lenPart = isStringOrArrayType(fieldDef.type) ? ` 长度:${parseInt(String(fieldDef.max))}` : '';
127
129
  // Logger.debug(` + 新增字段 ${dbFieldName} (${fieldDef.type}${lenPart})`);
128
130
  addClauses.push(generateDDLClause(fieldKey, fieldDef, true));
129
131
  changed = true;
@@ -132,7 +134,7 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
132
134
 
133
135
  // 检查并添加缺失的系统字段(created_at, updated_at, deleted_at, state)
134
136
  // 注意:id 是主键,不会缺失;这里只处理可能缺失的其他系统字段
135
- const systemFieldNames = ['created_at', 'updated_at', 'deleted_at', 'state'];
137
+ const systemFieldNames = ["created_at", "updated_at", "deleted_at", "state"];
136
138
  for (const sysFieldName of systemFieldNames) {
137
139
  if (!existingColumns[sysFieldName]) {
138
140
  const colDef = getSystemColumnDef(sysFieldName);
@@ -150,7 +152,7 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
150
152
  // 字段已存在或刚添加到 addClauses 中
151
153
  const fieldWillExist = existingColumns[sysField] || systemFieldNames.includes(sysField);
152
154
  if (fieldWillExist && !existingIndexes[idxName]) {
153
- indexActions.push({ action: 'create', indexName: idxName, fieldName: sysField });
155
+ indexActions.push({ action: "create", indexName: idxName, fieldName: sysField });
154
156
  changed = true;
155
157
  }
156
158
  }
@@ -163,10 +165,10 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
163
165
  const indexName = `idx_${dbFieldName}`;
164
166
  // 如果字段有 unique 约束,跳过创建普通索引(unique 会自动创建唯一索引)
165
167
  if (fieldDef.index && !fieldDef.unique && !existingIndexes[indexName]) {
166
- indexActions.push({ action: 'create', indexName: indexName, fieldName: dbFieldName });
168
+ indexActions.push({ action: "create", indexName: indexName, fieldName: dbFieldName });
167
169
  changed = true;
168
170
  } else if (!fieldDef.index && existingIndexes[indexName] && existingIndexes[indexName].length === 1) {
169
- indexActions.push({ action: 'drop', indexName: indexName, fieldName: dbFieldName });
171
+ indexActions.push({ action: "drop", indexName: indexName, fieldName: dbFieldName });
170
172
  changed = true;
171
173
  }
172
174
  }
@@ -179,12 +181,12 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
179
181
  const dbFieldName = snakeCase(fieldKey);
180
182
 
181
183
  if (existingColumns[dbFieldName]) {
182
- const curr = existingColumns[dbFieldName].comment || '';
183
- const want = fieldDef.name && fieldDef.name !== 'null' ? String(fieldDef.name) : '';
184
+ const curr = existingColumns[dbFieldName].comment || "";
185
+ const want = fieldDef.name && fieldDef.name !== "null" ? String(fieldDef.name) : "";
184
186
  if (want !== curr) {
185
187
  // 防止 SQL 注入:转义单引号
186
188
  const escapedWant = want.replace(/'/g, "''");
187
- commentActions.push(`COMMENT ON COLUMN "${tableName}"."${dbFieldName}" IS ${want ? `'${escapedWant}'` : 'NULL'}`);
189
+ commentActions.push(`COMMENT ON COLUMN "${tableName}"."${dbFieldName}" IS ${want ? `'${escapedWant}'` : "NULL"}`);
188
190
  changed = true;
189
191
  }
190
192
  }
@@ -1,4 +1,4 @@
1
- /**
1
+ /**
2
2
  * syncDb 表创建模块
3
3
  *
4
4
  * 包含:
@@ -8,15 +8,16 @@
8
8
  *
9
9
  * 注意:此模块从 table.ts 中提取,用于解除循环依赖
10
10
  */
11
- import { snakeCase } from 'es-toolkit/string';
12
- import { Logger } from '../../lib/logger.js';
13
- import { isMySQL, isPG, IS_PLAN, MYSQL_TABLE_CONFIG } from './constants.js';
14
- import { quoteIdentifier } from './helpers.js';
15
- import { buildSystemColumnDefs, buildBusinessColumnDefs, buildIndexSQL } from './ddl.js';
16
- import { getTableIndexes } from './schema.js';
11
+ import type { FieldDefinition } from "../../types/validate.js";
12
+ import type { SQL } from "bun";
13
+
14
+ import { snakeCase } from "es-toolkit/string";
17
15
 
18
- import type { SQL } from 'bun';
19
- import type { FieldDefinition } from 'befly-shared/types';
16
+ import { Logger } from "../../lib/logger.js";
17
+ import { isMySQL, isPG, IS_PLAN, MYSQL_TABLE_CONFIG } from "./constants.js";
18
+ import { buildSystemColumnDefs, buildBusinessColumnDefs, buildIndexSQL } from "./ddl.js";
19
+ import { quoteIdentifier } from "./helpers.js";
20
+ import { getTableIndexes } from "./schema.js";
20
21
 
21
22
  /**
22
23
  * 为 PostgreSQL 表添加列注释(使用字段的 name 作为注释)
@@ -28,11 +29,11 @@ import type { FieldDefinition } from 'befly-shared/types';
28
29
  async function addPostgresComments(sql: SQL, tableName: string, fields: Record<string, FieldDefinition>): Promise<void> {
29
30
  // 系统字段注释
30
31
  const systemComments = [
31
- ['id', '主键ID'],
32
- ['created_at', '创建时间'],
33
- ['updated_at', '更新时间'],
34
- ['deleted_at', '删除时间'],
35
- ['state', '状态字段']
32
+ ["id", "主键ID"],
33
+ ["created_at", "创建时间"],
34
+ ["updated_at", "更新时间"],
35
+ ["deleted_at", "删除时间"],
36
+ ["state", "状态字段"]
36
37
  ];
37
38
 
38
39
  for (const [name, comment] of systemComments) {
@@ -74,7 +75,7 @@ async function createTableIndexes(sql: SQL, tableName: string, fields: Record<st
74
75
  // 获取现有索引(MySQL 不支持 IF NOT EXISTS,需要先检查)
75
76
  let existingIndexes: Record<string, string[]> = {};
76
77
  if (isMySQL()) {
77
- existingIndexes = await getTableIndexes(sql, tableName, dbName || '');
78
+ existingIndexes = await getTableIndexes(sql, tableName, dbName || "");
78
79
  }
79
80
 
80
81
  // 系统字段索引
@@ -84,7 +85,7 @@ async function createTableIndexes(sql: SQL, tableName: string, fields: Record<st
84
85
  if (isMySQL() && existingIndexes[indexName]) {
85
86
  continue;
86
87
  }
87
- const stmt = buildIndexSQL(tableName, indexName, sysField, 'create');
88
+ const stmt = buildIndexSQL(tableName, indexName, sysField, "create");
88
89
  if (IS_PLAN) {
89
90
  Logger.debug(`[计划] ${stmt}`);
90
91
  } else {
@@ -103,7 +104,7 @@ async function createTableIndexes(sql: SQL, tableName: string, fields: Record<st
103
104
  if (isMySQL() && existingIndexes[indexName]) {
104
105
  continue;
105
106
  }
106
- const stmt = buildIndexSQL(tableName, indexName, dbFieldName, 'create');
107
+ const stmt = buildIndexSQL(tableName, indexName, dbFieldName, "create");
107
108
  if (IS_PLAN) {
108
109
  Logger.debug(`[计划] ${stmt}`);
109
110
  } else {
@@ -127,12 +128,12 @@ async function createTableIndexes(sql: SQL, tableName: string, fields: Record<st
127
128
  * @param systemIndexFields - 系统字段索引列表(可选,默认使用 ['created_at', 'updated_at', 'state'])
128
129
  * @param dbName - 数据库名称(用于检查索引是否存在)
129
130
  */
130
- export async function createTable(sql: SQL, tableName: string, fields: Record<string, FieldDefinition>, systemIndexFields: string[] = ['created_at', 'updated_at', 'state'], dbName?: string): Promise<void> {
131
+ export async function createTable(sql: SQL, tableName: string, fields: Record<string, FieldDefinition>, systemIndexFields: string[] = ["created_at", "updated_at", "state"], dbName?: string): Promise<void> {
131
132
  // 构建列定义
132
133
  const colDefs = [...buildSystemColumnDefs(), ...buildBusinessColumnDefs(fields)];
133
134
 
134
135
  // 生成 CREATE TABLE 语句
135
- const cols = colDefs.join(',\n ');
136
+ const cols = colDefs.join(",\n ");
136
137
  const tableQuoted = quoteIdentifier(tableName);
137
138
  const { ENGINE, CHARSET, COLLATE } = MYSQL_TABLE_CONFIG;
138
139
  const createSQL = isMySQL()
@@ -144,7 +145,7 @@ export async function createTable(sql: SQL, tableName: string, fields: Record<st
144
145
  )`;
145
146
 
146
147
  if (IS_PLAN) {
147
- Logger.debug(`[计划] ${createSQL.replace(/\n+/g, ' ')}`);
148
+ Logger.debug(`[计划] ${createSQL.replace(/\n+/g, " ")}`);
148
149
  } else {
149
150
  await sql.unsafe(createSQL);
150
151
  }
@@ -7,7 +7,7 @@
7
7
  * - 类型判断工具
8
8
  */
9
9
 
10
- import { isMySQL, getTypeMapping } from './constants.js';
10
+ import { isMySQL, getTypeMapping } from "./constants.js";
11
11
 
12
12
  /**
13
13
  * 判断是否为字符串或数组类型(需要长度参数)
@@ -18,12 +18,13 @@ import { isMySQL, getTypeMapping } from './constants.js';
18
18
  * @example
19
19
  * isStringOrArrayType('string') // => true
20
20
  * isStringOrArrayType('array_string') // => true
21
+ * isStringOrArrayType('array_number_string') // => true
21
22
  * isStringOrArrayType('array_text') // => false
22
23
  * isStringOrArrayType('number') // => false
23
24
  * isStringOrArrayType('text') // => false
24
25
  */
25
26
  export function isStringOrArrayType(fieldType: string): boolean {
26
- return fieldType === 'string' || fieldType === 'array_string';
27
+ return fieldType === "string" || fieldType === "array_string" || fieldType === "array_number_string";
27
28
  }
28
29
 
29
30
  /**
@@ -47,8 +48,8 @@ export function getSqlType(fieldType: string, fieldMax: number | null, unsigned:
47
48
  return `${typeMapping[fieldType]}(${fieldMax})`;
48
49
  }
49
50
  // 处理 UNSIGNED 修饰符(仅 MySQL number 类型)
50
- const baseType = typeMapping[fieldType] || 'TEXT';
51
- if (isMySQL() && fieldType === 'number' && unsigned) {
51
+ const baseType = typeMapping[fieldType] || "TEXT";
52
+ if (isMySQL() && fieldType === "number" && unsigned) {
52
53
  return `${baseType} UNSIGNED`;
53
54
  }
54
55
  return baseType;
@@ -73,22 +74,24 @@ export function getSqlType(fieldType: string, fieldMax: number | null, unsigned:
73
74
  */
74
75
  export function resolveDefaultValue(fieldDefault: any, fieldType: string): any {
75
76
  // null 或字符串 'null' 都表示使用类型默认值
76
- if (fieldDefault !== null && fieldDefault !== 'null') {
77
+ if (fieldDefault !== null && fieldDefault !== "null") {
77
78
  return fieldDefault;
78
79
  }
79
80
 
80
81
  // null 表示使用类型默认值
81
82
  switch (fieldType) {
82
- case 'number':
83
+ case "number":
83
84
  return 0;
84
- case 'string':
85
- return '';
86
- case 'array_string':
87
- return '[]';
88
- case 'text':
89
- case 'array_text':
90
- // text/array_text 类型不设置默认值(MySQL TEXT 不支持),保持 'null'
91
- return 'null';
85
+ case "string":
86
+ return "";
87
+ case "array_string":
88
+ case "array_number_string":
89
+ return "[]";
90
+ case "text":
91
+ case "array_text":
92
+ case "array_number_text":
93
+ // text/array_text/array_number_text 类型不设置默认值(MySQL TEXT 不支持),保持 'null'
94
+ return "null";
92
95
  default:
93
96
  return fieldDefault;
94
97
  }
@@ -110,13 +113,13 @@ export function resolveDefaultValue(fieldDefault: any, fieldType: string): any {
110
113
  */
111
114
  export function generateDefaultSql(actualDefault: any, fieldType: string): string {
112
115
  // text 和 array_text 类型不设置默认值(MySQL TEXT 类型不支持默认值)
113
- if (fieldType === 'text' || fieldType === 'array_text' || actualDefault === 'null') {
114
- return '';
116
+ if (fieldType === "text" || fieldType === "array_text" || actualDefault === "null") {
117
+ return "";
115
118
  }
116
119
 
117
- // 仅 number/string/array_string 类型设置默认值
118
- if (fieldType === 'number' || fieldType === 'string' || fieldType === 'array_string') {
119
- if (typeof actualDefault === 'number' && !Number.isNaN(actualDefault)) {
120
+ // 仅 number/string/array_string/array_number_string 类型设置默认值
121
+ if (fieldType === "number" || fieldType === "string" || fieldType === "array_string" || fieldType === "array_number_string") {
122
+ if (typeof actualDefault === "number" && !Number.isNaN(actualDefault)) {
120
123
  return ` DEFAULT ${actualDefault}`;
121
124
  } else {
122
125
  // 字符串需要转义单引号:' -> ''
@@ -125,5 +128,5 @@ export function generateDefaultSql(actualDefault: any, fieldType: string): strin
125
128
  }
126
129
  }
127
130
 
128
- return '';
131
+ return "";
129
132
  }
@@ -1,13 +1,13 @@
1
- /**
1
+ /**
2
2
  * syncDb 数据库版本检查模块
3
3
  *
4
4
  * 包含:
5
5
  * - 数据库版本验证(MySQL/PostgreSQL/SQLite)
6
6
  */
7
7
 
8
- import { Logger } from '../../lib/logger.js';
9
- import { DB_VERSION_REQUIREMENTS, isMySQL, isPG, isSQLite } from './constants.js';
10
- import type { SQL } from 'bun';
8
+ import type { SQL } from "bun";
9
+
10
+ import { DB_VERSION_REQUIREMENTS, isMySQL, isPG, isSQLite } from "./constants.js";
11
11
 
12
12
  /**
13
13
  * 数据库版本检查(按方言)
@@ -21,15 +21,15 @@ import type { SQL } from 'bun';
21
21
  * @throws {Error} 如果数据库版本不符合要求或无法获取版本信息
22
22
  */
23
23
  export async function ensureDbVersion(sql: SQL): Promise<void> {
24
- if (!sql) throw new Error('SQL 客户端未初始化');
24
+ if (!sql) throw new Error("SQL 客户端未初始化");
25
25
 
26
26
  if (isMySQL()) {
27
27
  const r = await sql`SELECT VERSION() AS version`;
28
28
  if (!r || r.length === 0 || !r[0]?.version) {
29
- throw new Error('无法获取 MySQL 版本信息');
29
+ throw new Error("无法获取 MySQL 版本信息");
30
30
  }
31
31
  const version = r[0].version;
32
- const majorVersion = parseInt(String(version).split('.')[0], 10);
32
+ const majorVersion = parseInt(String(version).split(".")[0], 10);
33
33
  if (!Number.isFinite(majorVersion) || majorVersion < DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR) {
34
34
  throw new Error(`此脚本仅支持 MySQL ${DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR}.0+,当前版本: ${version}`);
35
35
  }
@@ -39,7 +39,7 @@ export async function ensureDbVersion(sql: SQL): Promise<void> {
39
39
  if (isPG()) {
40
40
  const r = await sql`SELECT version() AS version`;
41
41
  if (!r || r.length === 0 || !r[0]?.version) {
42
- throw new Error('无法获取 PostgreSQL 版本信息');
42
+ throw new Error("无法获取 PostgreSQL 版本信息");
43
43
  }
44
44
  const versionText = r[0].version;
45
45
  const m = /PostgreSQL\s+(\d+)/i.exec(versionText);
@@ -53,12 +53,12 @@ export async function ensureDbVersion(sql: SQL): Promise<void> {
53
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
- throw new Error('无法获取 SQLite 版本信息');
56
+ throw new Error("无法获取 SQLite 版本信息");
57
57
  }
58
58
  const version = r[0].version;
59
59
  // 强制最低版本:SQLite ≥ 3.50.0
60
60
  const [maj, min, patch] = String(version)
61
- .split('.')
61
+ .split(".")
62
62
  .map((v) => parseInt(v, 10) || 0);
63
63
  const vnum = maj * 10000 + min * 100 + patch; // 3.50.0 -> 35000
64
64
  if (!Number.isFinite(vnum) || vnum < DB_VERSION_REQUIREMENTS.SQLITE_MIN_VERSION_NUM) {
package/sync/syncDb.ts CHANGED
@@ -1,4 +1,4 @@
1
- /**
1
+ /**
2
2
  * SyncDb 命令 - 同步数据库表结构
3
3
  *
4
4
  * 功能:
@@ -7,28 +7,30 @@
7
7
  * - 提供统计信息和错误处理
8
8
  */
9
9
 
10
- import { resolve } from 'pathe';
11
- import { existsSync } from 'node:fs';
12
- import { snakeCase } from 'es-toolkit/string';
13
- import { Connect } from '../lib/connect.js';
14
- import { RedisHelper } from '../lib/redisHelper.js';
15
- import { checkTable } from '../checks/checkTable.js';
16
- import { scanFiles } from 'befly-shared/scanFiles';
17
- import { scanAddons, addonDirExists, getAddonDir } from 'befly-shared/addonHelper';
18
- import { RedisKeys } from 'befly-shared/redisKeys';
19
- import { Logger } from '../lib/logger.js';
20
- import { projectDir } from '../paths.js';
21
-
10
+ import type { SyncDbOptions } from "../types/sync.js";
11
+ import type { SQL } from "bun";
12
+
13
+ import { existsSync } from "node:fs";
14
+
15
+ import { snakeCase } from "es-toolkit/string";
16
+ import { resolve } from "pathe";
17
+
18
+ import { beflyConfig } from "../befly.config.js";
19
+ import { checkTable } from "../checks/checkTable.js";
20
+ import { CacheKeys } from "../lib/cacheKeys.js";
21
+ import { Connect } from "../lib/connect.js";
22
+ import { Logger } from "../lib/logger.js";
23
+ import { RedisHelper } from "../lib/redisHelper.js";
24
+ import { projectDir } from "../paths.js";
25
+ import { scanAddons, addonDirExists, getAddonDir } from "../utils/addonHelper.js";
26
+ import { scanFiles } from "../utils/scanFiles.js";
27
+ import { setDbType } from "./syncDb/constants.js";
28
+ import { applyFieldDefaults } from "./syncDb/helpers.js";
29
+ import { tableExists } from "./syncDb/schema.js";
30
+ import { modifyTable } from "./syncDb/table.js";
31
+ import { createTable } from "./syncDb/tableCreate.js";
22
32
  // 导入模块化的功能
23
- import { ensureDbVersion } from './syncDb/version.js';
24
- import { tableExists } from './syncDb/schema.js';
25
- import { modifyTable } from './syncDb/table.js';
26
- import { createTable } from './syncDb/tableCreate.js';
27
- import { applyFieldDefaults } from './syncDb/helpers.js';
28
- import { setDbType } from './syncDb/constants.js';
29
- import { beflyConfig } from '../befly.config.js';
30
- import type { SQL } from 'bun';
31
- import type { SyncDbOptions } from '../types/index.js';
33
+ import { ensureDbVersion } from "./syncDb/version.js";
32
34
 
33
35
  // 全局 SQL 客户端实例
34
36
  let sql: SQL | null = null;
@@ -51,7 +53,7 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
51
53
  processedTables.length = 0;
52
54
 
53
55
  // 设置数据库类型(从配置获取)
54
- const dbType = beflyConfig.db?.type || 'mysql';
56
+ const dbType = beflyConfig.db?.type || "mysql";
55
57
  setDbType(dbType);
56
58
 
57
59
  // 验证表定义文件
@@ -65,21 +67,26 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
65
67
  await Connect.connectRedis();
66
68
 
67
69
  // 扫描表定义文件
68
- const directories: Array<{ path: string; type: 'app' | 'addon'; addonName?: string; addonNameSnake?: string }> = [];
70
+ const directories: Array<{
71
+ path: string;
72
+ type: "app" | "addon";
73
+ addonName?: string;
74
+ addonNameSnake?: string;
75
+ }> = [];
69
76
 
70
77
  // 1. 项目表(无前缀)- 如果 tables 目录存在
71
- const projectTablesDir = resolve(projectDir, 'tables');
78
+ const projectTablesDir = resolve(projectDir, "tables");
72
79
  if (existsSync(projectTablesDir)) {
73
- directories.push({ path: projectTablesDir, type: 'app' });
80
+ directories.push({ path: projectTablesDir, type: "app" });
74
81
  }
75
82
 
76
83
  // 添加所有 addon 的 tables 目录(addon_{name}_ 前缀)
77
84
  const addons = scanAddons();
78
85
  for (const addon of addons) {
79
- if (addonDirExists(addon, 'tables')) {
86
+ if (addonDirExists(addon, "tables")) {
80
87
  directories.push({
81
- path: getAddonDir(addon, 'tables'),
82
- type: 'addon',
88
+ path: getAddonDir(addon, "tables"),
89
+ type: "addon",
83
90
  addonName: addon,
84
91
  addonNameSnake: snakeCase(addon) // 提前转换,避免每个文件都转换
85
92
  });
@@ -90,7 +97,7 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
90
97
  for (const dirConfig of directories) {
91
98
  const { path: dir, type } = dirConfig;
92
99
 
93
- const files = await scanFiles(dir, '*.json');
100
+ const files = await scanFiles(dir, "*.json");
94
101
 
95
102
  for (const { filePath: file, fileName } of files) {
96
103
  // 确定表名:
@@ -99,7 +106,7 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
99
106
  // - 项目表:{表名}
100
107
  // 例如:user.json → user
101
108
  let tableName = snakeCase(fileName);
102
- if (type === 'addon' && dirConfig.addonNameSnake) {
109
+ if (type === "addon" && dirConfig.addonNameSnake) {
103
110
  // addon 表,使用提前转换好的名称
104
111
  tableName = `addon_${dirConfig.addonNameSnake}_${tableName}`;
105
112
  }
@@ -109,7 +116,7 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
109
116
  continue;
110
117
  }
111
118
 
112
- const tableDefinitionModule = await import(file, { with: { type: 'json' } });
119
+ const tableDefinitionModule = await import(file, { with: { type: "json" } });
113
120
  const tableDefinition = tableDefinitionModule.default;
114
121
 
115
122
  // 为字段属性设置默认值
@@ -117,7 +124,7 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
117
124
  applyFieldDefaults(fieldDef);
118
125
  }
119
126
 
120
- const dbName = beflyConfig.db?.database || '';
127
+ const dbName = beflyConfig.db?.database || "";
121
128
  const existsTable = await tableExists(sql!, tableName, dbName);
122
129
 
123
130
  // 读取 force 参数
@@ -126,7 +133,7 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
126
133
  if (existsTable) {
127
134
  await modifyTable(sql!, tableName, tableDefinition, force, dbName);
128
135
  } else {
129
- await createTable(sql!, tableName, tableDefinition, ['created_at', 'updated_at', 'state'], dbName);
136
+ await createTable(sql!, tableName, tableDefinition, ["created_at", "updated_at", "state"], dbName);
130
137
  }
131
138
 
132
139
  // 记录处理过的表名(用于清理缓存)
@@ -137,11 +144,11 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
137
144
  // 清理 Redis 缓存(如果有表被处理)
138
145
  if (processedTables.length > 0) {
139
146
  const redisHelper = new RedisHelper();
140
- const cacheKeys = processedTables.map((tableName) => RedisKeys.tableColumns(tableName));
147
+ const cacheKeys = processedTables.map((tableName) => CacheKeys.tableColumns(tableName));
141
148
  await redisHelper.delBatch(cacheKeys);
142
149
  }
143
150
  } catch (error: any) {
144
- Logger.error({ err: error }, '数据库同步失败');
151
+ Logger.error({ err: error }, "数据库同步失败");
145
152
  throw error;
146
153
  } finally {
147
154
  if (sql) {