befly 3.9.40 → 3.10.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 (144) hide show
  1. package/README.md +47 -19
  2. package/befly.config.ts +19 -2
  3. package/checks/checkApi.ts +79 -77
  4. package/checks/checkHook.ts +48 -0
  5. package/checks/checkMenu.ts +168 -0
  6. package/checks/checkPlugin.ts +48 -0
  7. package/checks/checkTable.ts +137 -183
  8. package/docs/README.md +17 -11
  9. package/docs/api/api.md +16 -2
  10. package/docs/guide/quickstart.md +31 -10
  11. package/docs/hooks/hook.md +2 -2
  12. package/docs/hooks/rateLimit.md +1 -1
  13. package/docs/infra/redis.md +26 -14
  14. package/docs/plugins/plugin.md +23 -21
  15. package/docs/quickstart.md +5 -328
  16. package/docs/reference/addon.md +0 -4
  17. package/docs/reference/config.md +14 -31
  18. package/docs/reference/logger.md +3 -3
  19. package/docs/reference/sync.md +132 -237
  20. package/docs/reference/table.md +28 -30
  21. package/hooks/auth.ts +3 -4
  22. package/hooks/cors.ts +4 -6
  23. package/hooks/parser.ts +3 -4
  24. package/hooks/permission.ts +3 -4
  25. package/hooks/validator.ts +3 -4
  26. package/lib/cacheHelper.ts +89 -153
  27. package/lib/cacheKeys.ts +1 -1
  28. package/lib/connect.ts +9 -13
  29. package/lib/dbDialect.ts +285 -0
  30. package/lib/dbHelper.ts +179 -507
  31. package/lib/dbUtils.ts +450 -0
  32. package/lib/logger.ts +41 -5
  33. package/lib/redisHelper.ts +1 -0
  34. package/lib/sqlBuilder.ts +358 -58
  35. package/lib/sqlCheck.ts +136 -0
  36. package/lib/validator.ts +1 -1
  37. package/loader/loadApis.ts +23 -126
  38. package/loader/loadHooks.ts +31 -46
  39. package/loader/loadPlugins.ts +37 -52
  40. package/main.ts +58 -19
  41. package/package.json +24 -25
  42. package/paths.ts +14 -14
  43. package/plugins/cache.ts +12 -6
  44. package/plugins/cipher.ts +2 -2
  45. package/plugins/config.ts +6 -8
  46. package/plugins/db.ts +14 -19
  47. package/plugins/jwt.ts +6 -7
  48. package/plugins/logger.ts +7 -9
  49. package/plugins/redis.ts +8 -10
  50. package/plugins/tool.ts +3 -4
  51. package/router/api.ts +3 -2
  52. package/router/static.ts +7 -5
  53. package/sync/syncApi.ts +80 -235
  54. package/sync/syncCache.ts +16 -0
  55. package/sync/syncDev.ts +167 -202
  56. package/sync/syncMenu.ts +230 -444
  57. package/sync/syncTable.ts +1247 -0
  58. package/tests/_mocks/mockSqliteDb.ts +204 -0
  59. package/tests/addonHelper-cache.test.ts +32 -0
  60. package/tests/apiHandler-routePath-only.test.ts +32 -0
  61. package/tests/cacheHelper.test.ts +16 -51
  62. package/tests/checkApi-routePath-strict.test.ts +166 -0
  63. package/tests/checkMenu.test.ts +346 -0
  64. package/tests/checkTable-smoke.test.ts +157 -0
  65. package/tests/dbDialect-cache.test.ts +23 -0
  66. package/tests/dbDialect.test.ts +46 -0
  67. package/tests/dbHelper-advanced.test.ts +1 -1
  68. package/tests/dbHelper-all-array-types.test.ts +15 -15
  69. package/tests/dbHelper-batch-write.test.ts +90 -0
  70. package/tests/dbHelper-columns.test.ts +36 -54
  71. package/tests/dbHelper-execute.test.ts +26 -26
  72. package/tests/dbHelper-joins.test.ts +85 -176
  73. package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
  74. package/tests/fixtures/scanFilesApis/a.ts +3 -0
  75. package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
  76. package/tests/loadPlugins-order-smoke.test.ts +75 -0
  77. package/tests/logger.test.ts +6 -6
  78. package/tests/redisHelper.test.ts +6 -1
  79. package/tests/scanFiles-routePath.test.ts +46 -0
  80. package/tests/smoke-sql.test.ts +24 -0
  81. package/tests/sqlBuilder-advanced.test.ts +18 -5
  82. package/tests/sqlBuilder.test.ts +24 -0
  83. package/tests/sync-init-guard.test.ts +105 -0
  84. package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
  85. package/tests/syncApi-obsolete-records.test.ts +69 -0
  86. package/tests/syncApi-type-compat.test.ts +72 -0
  87. package/tests/syncDev-permissions.test.ts +81 -0
  88. package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
  89. package/tests/syncMenu-duplicate-path.test.ts +122 -0
  90. package/tests/syncMenu-obsolete-records.test.ts +161 -0
  91. package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
  92. package/tests/syncMenu-paths.test.ts +0 -9
  93. package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
  94. package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
  95. package/tests/syncTable-constants.test.ts +101 -0
  96. package/tests/syncTable-db-integration.test.ts +237 -0
  97. package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
  98. package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
  99. package/tests/syncTable-schema.test.ts +99 -0
  100. package/tests/syncTable-testkit.test.ts +25 -0
  101. package/tests/syncTable-types.test.ts +122 -0
  102. package/tests/tableRef-and-deserialize.test.ts +67 -0
  103. package/tsconfig.json +1 -1
  104. package/types/api.d.ts +1 -1
  105. package/types/befly.d.ts +13 -12
  106. package/types/cache.d.ts +2 -2
  107. package/types/context.d.ts +1 -1
  108. package/types/database.d.ts +0 -5
  109. package/types/hook.d.ts +1 -10
  110. package/types/plugin.d.ts +2 -96
  111. package/types/sync.d.ts +19 -25
  112. package/utils/convertBigIntFields.ts +38 -0
  113. package/utils/disableMenusGlob.ts +85 -0
  114. package/utils/importDefault.ts +21 -0
  115. package/utils/isDirentDirectory.ts +23 -0
  116. package/utils/loadMenuConfigs.ts +145 -0
  117. package/utils/processFields.ts +25 -0
  118. package/utils/scanAddons.ts +72 -0
  119. package/utils/scanFiles.ts +129 -21
  120. package/utils/scanSources.ts +64 -0
  121. package/utils/sortModules.ts +137 -0
  122. package/checks/checkApp.ts +0 -55
  123. package/docs/cipher.md +0 -582
  124. package/docs/database.md +0 -1176
  125. package/hooks/rateLimit.ts +0 -276
  126. package/sync/syncAll.ts +0 -35
  127. package/sync/syncDb/apply.ts +0 -192
  128. package/sync/syncDb/constants.ts +0 -119
  129. package/sync/syncDb/ddl.ts +0 -251
  130. package/sync/syncDb/helpers.ts +0 -84
  131. package/sync/syncDb/schema.ts +0 -202
  132. package/sync/syncDb/sqlite.ts +0 -48
  133. package/sync/syncDb/table.ts +0 -207
  134. package/sync/syncDb/tableCreate.ts +0 -163
  135. package/sync/syncDb/types.ts +0 -132
  136. package/sync/syncDb/version.ts +0 -69
  137. package/sync/syncDb.ts +0 -168
  138. package/tests/rateLimit-hook.test.ts +0 -477
  139. package/tests/syncDb-constants.test.ts +0 -130
  140. package/tests/syncDb-schema.test.ts +0 -179
  141. package/tests/syncDb-types.test.ts +0 -139
  142. package/utils/addonHelper.ts +0 -90
  143. package/utils/modules.ts +0 -98
  144. package/utils/route.ts +0 -23
@@ -1,207 +0,0 @@
1
- /**
2
- * syncDb 表操作模块
3
- *
4
- * 包含:
5
- * - 修改表结构
6
- * - 对比字段变化
7
- * - 应用变更计划
8
- */
9
-
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";
23
-
24
- /**
25
- * 同步表结构(对比和应用变更)
26
- *
27
- * 主要逻辑:
28
- * 1. 获取表的现有列和索引信息
29
- * 2. 对比每个字段的定义变化
30
- * 3. 生成变更计划(添加/修改/删除列,添加/删除索引)
31
- * 4. 执行变更 SQL
32
- *
33
- * @param sql - SQL 客户端实例
34
- * @param tableName - 表名
35
- * @param fields - 字段定义
36
- * @param force - 是否强制同步(删除多余字段)
37
- * @param dbName - 数据库名称
38
- */
39
- export async function modifyTable(sql: SQL, tableName: string, fields: Record<string, FieldDefinition>, force: boolean = false, dbName?: string): Promise<TablePlan> {
40
- const existingColumns = await getTableColumns(sql, tableName, dbName || "");
41
- const existingIndexes = await getTableIndexes(sql, tableName, dbName || "");
42
- let changed = false;
43
-
44
- const addClauses: string[] = [];
45
- const modifyClauses: string[] = [];
46
- const defaultClauses: string[] = [];
47
- const indexActions: Array<{ action: "create" | "drop"; indexName: string; fieldName: string }> = [];
48
-
49
- for (const [fieldKey, fieldDef] of Object.entries(fields)) {
50
- // 转换字段名为下划线格式
51
- const dbFieldName = snakeCase(fieldKey);
52
-
53
- if (existingColumns[dbFieldName]) {
54
- const comparison = compareFieldDefinition(existingColumns[dbFieldName], fieldDef);
55
- if (comparison.length > 0) {
56
- for (const c of comparison) {
57
- // 使用统一的日志格式函数和常量标签
58
- const changeLabel = CHANGE_TYPE_LABELS[c.type as keyof typeof CHANGE_TYPE_LABELS] || "未知";
59
- logFieldChange(tableName, dbFieldName, c.type, c.current, c.expected, changeLabel);
60
- }
61
-
62
- if (isStringOrArrayType(fieldDef.type) && existingColumns[dbFieldName].max && fieldDef.max !== null) {
63
- if (existingColumns[dbFieldName].max! > fieldDef.max) {
64
- if (force) {
65
- Logger.warn(`[强制执行] ${tableName}.${dbFieldName} 长度收缩 ${existingColumns[dbFieldName].max} -> ${fieldDef.max}`);
66
- } else {
67
- Logger.warn(`[跳过危险变更] ${tableName}.${dbFieldName} 长度收缩 ${existingColumns[dbFieldName].max} -> ${fieldDef.max} 已被跳过(使用 --force 强制执行)`);
68
- }
69
- }
70
- }
71
-
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");
76
-
77
- // 类型变更检查:只允许兼容的宽化型变更(如 INT -> BIGINT)
78
- if (hasTypeChange) {
79
- const typeChange = comparison.find((c) => c.type === "datatype");
80
- const currentType = String(typeChange?.current || "").toLowerCase();
81
- const typeMapping = getTypeMapping();
82
- const expectedType = typeMapping[fieldDef.type]?.toLowerCase() || "";
83
-
84
- if (!isCompatibleTypeChange(currentType, expectedType)) {
85
- const errorMsg = [`禁止字段类型变更: ${tableName}.${dbFieldName}`, `当前类型: ${typeChange?.current}`, `目标类型: ${typeChange?.expected}`, `说明: 仅允许宽化型变更(如 INT->BIGINT, VARCHAR->TEXT),其他类型变更需要手动处理`].join("\n");
86
- throw new Error(errorMsg);
87
- }
88
- Logger.debug(`[兼容类型变更] ${tableName}.${dbFieldName} ${currentType} -> ${expectedType}`);
89
- }
90
-
91
- // 默认值变化处理
92
- if (defaultChanged) {
93
- // 使用公共函数处理默认值
94
- const actualDefault = resolveDefaultValue(fieldDef.default, fieldDef.type);
95
-
96
- // 生成 SQL DEFAULT 值(不包含前导空格,因为要用于 ALTER COLUMN)
97
- let v: string | null = null;
98
- if (actualDefault !== "null") {
99
- const defaultSql = generateDefaultSql(actualDefault, fieldDef.type);
100
- // 移除前导空格 ' DEFAULT ' -> 'DEFAULT '
101
- v = defaultSql.trim().replace(/^DEFAULT\s+/, "");
102
- }
103
-
104
- if (v !== null && v !== "") {
105
- if (isPG()) {
106
- defaultClauses.push(`ALTER COLUMN "${dbFieldName}" SET DEFAULT ${v}`);
107
- } else if (isMySQL() && onlyDefaultChanged) {
108
- // MySQL 的 TEXT/BLOB 不允许 DEFAULT,跳过 text 类型
109
- if (fieldDef.type !== "text") {
110
- defaultClauses.push(`ALTER COLUMN \`${dbFieldName}\` SET DEFAULT ${v}`);
111
- }
112
- }
113
- }
114
- }
115
-
116
- // 若不仅仅是默认值变化,继续生成修改子句
117
- if (!onlyDefaultChanged) {
118
- let skipModify = false;
119
- if (hasLengthChange && isStringOrArrayType(fieldDef.type) && existingColumns[dbFieldName].max && fieldDef.max !== null) {
120
- const isShrink = existingColumns[dbFieldName].max! > fieldDef.max;
121
- if (isShrink && !force) skipModify = true;
122
- }
123
-
124
- if (!skipModify) modifyClauses.push(generateDDLClause(fieldKey, fieldDef, false));
125
- }
126
- changed = true;
127
- }
128
- } else {
129
- // Logger.debug(` + 新增字段 ${dbFieldName} (${fieldDef.type}${lenPart})`);
130
- addClauses.push(generateDDLClause(fieldKey, fieldDef, true));
131
- changed = true;
132
- }
133
- }
134
-
135
- // 检查并添加缺失的系统字段(created_at, updated_at, deleted_at, state)
136
- // 注意:id 是主键,不会缺失;这里只处理可能缺失的其他系统字段
137
- const systemFieldNames = ["created_at", "updated_at", "deleted_at", "state"];
138
- for (const sysFieldName of systemFieldNames) {
139
- if (!existingColumns[sysFieldName]) {
140
- const colDef = getSystemColumnDef(sysFieldName);
141
- if (colDef) {
142
- Logger.debug(` + 新增系统字段 ${sysFieldName}`);
143
- addClauses.push(`ADD COLUMN ${colDef}`);
144
- changed = true;
145
- }
146
- }
147
- }
148
-
149
- // 检查系统字段索引(字段存在或即将被添加时才创建索引)
150
- for (const sysField of SYSTEM_INDEX_FIELDS) {
151
- const idxName = `idx_${sysField}`;
152
- // 字段已存在或刚添加到 addClauses 中
153
- const fieldWillExist = existingColumns[sysField] || systemFieldNames.includes(sysField);
154
- if (fieldWillExist && !existingIndexes[idxName]) {
155
- indexActions.push({ action: "create", indexName: idxName, fieldName: sysField });
156
- changed = true;
157
- }
158
- }
159
-
160
- // 检查业务字段索引
161
- for (const [fieldKey, fieldDef] of Object.entries(fields)) {
162
- // 转换字段名为下划线格式
163
- const dbFieldName = snakeCase(fieldKey);
164
-
165
- const indexName = `idx_${dbFieldName}`;
166
- // 如果字段有 unique 约束,跳过创建普通索引(unique 会自动创建唯一索引)
167
- if (fieldDef.index && !fieldDef.unique && !existingIndexes[indexName]) {
168
- indexActions.push({ action: "create", indexName: indexName, fieldName: dbFieldName });
169
- changed = true;
170
- } else if (!fieldDef.index && existingIndexes[indexName] && existingIndexes[indexName].length === 1) {
171
- indexActions.push({ action: "drop", indexName: indexName, fieldName: dbFieldName });
172
- changed = true;
173
- }
174
- }
175
-
176
- // PG 列注释处理(对比数据库 comment 与字段 name)
177
- const commentActions: string[] = [];
178
- if (isPG()) {
179
- for (const [fieldKey, fieldDef] of Object.entries(fields)) {
180
- // 转换字段名为下划线格式
181
- const dbFieldName = snakeCase(fieldKey);
182
-
183
- if (existingColumns[dbFieldName]) {
184
- const curr = existingColumns[dbFieldName].comment || "";
185
- const want = fieldDef.name && fieldDef.name !== "null" ? String(fieldDef.name) : "";
186
- if (want !== curr) {
187
- // 防止 SQL 注入:转义单引号
188
- const escapedWant = want.replace(/'/g, "''");
189
- commentActions.push(`COMMENT ON COLUMN "${tableName}"."${dbFieldName}" IS ${want ? `'${escapedWant}'` : "NULL"}`);
190
- changed = true;
191
- }
192
- }
193
- }
194
- }
195
-
196
- // 仅当存在实际动作时才认为有变更(避免仅日志的收缩跳过被计为修改)
197
- changed = addClauses.length > 0 || modifyClauses.length > 0 || defaultClauses.length > 0 || indexActions.length > 0 || commentActions.length > 0;
198
-
199
- const plan = { changed, addClauses, modifyClauses, defaultClauses, indexActions, commentActions };
200
-
201
- // 将计划应用(包含 --plan 情况下仅输出)
202
- if (plan.changed) {
203
- await applyTablePlan(sql, tableName, fields, plan);
204
- }
205
-
206
- return plan;
207
- }
@@ -1,163 +0,0 @@
1
- /**
2
- * syncDb 表创建模块
3
- *
4
- * 包含:
5
- * - 创建表(包含系统字段和业务字段)
6
- * - 添加 PostgreSQL 注释
7
- * - 创建表索引
8
- *
9
- * 注意:此模块从 table.ts 中提取,用于解除循环依赖
10
- */
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 { 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";
21
-
22
- /**
23
- * 为 PostgreSQL 表添加列注释(使用字段的 name 作为注释)
24
- *
25
- * @param sql - SQL 客户端实例
26
- * @param tableName - 表名
27
- * @param fields - 字段定义对象
28
- */
29
- async function addPostgresComments(sql: SQL, tableName: string, fields: Record<string, FieldDefinition>): Promise<void> {
30
- // 系统字段注释
31
- const systemComments = [
32
- ["id", "主键ID"],
33
- ["created_at", "创建时间"],
34
- ["updated_at", "更新时间"],
35
- ["deleted_at", "删除时间"],
36
- ["state", "状态字段"]
37
- ];
38
-
39
- for (const [name, comment] of systemComments) {
40
- const stmt = `COMMENT ON COLUMN "${tableName}"."${name}" IS '${comment}'`;
41
- if (IS_PLAN) {
42
- Logger.debug(`[计划] ${stmt}`);
43
- } else {
44
- await sql.unsafe(stmt);
45
- }
46
- }
47
-
48
- // 业务字段注释(使用 fieldDef.name 作为数据库列注释)
49
- for (const [fieldKey, fieldDef] of Object.entries(fields)) {
50
- // 转换字段名为下划线格式
51
- const dbFieldName = snakeCase(fieldKey);
52
-
53
- const { name: fieldName } = fieldDef;
54
- const stmt = `COMMENT ON COLUMN "${tableName}"."${dbFieldName}" IS '${fieldName}'`;
55
- if (IS_PLAN) {
56
- Logger.debug(`[计划] ${stmt}`);
57
- } else {
58
- await sql.unsafe(stmt);
59
- }
60
- }
61
- }
62
-
63
- /**
64
- * 创建表的索引(并行执行以提升性能)
65
- *
66
- * @param sql - SQL 客户端实例
67
- * @param tableName - 表名
68
- * @param fields - 字段定义对象
69
- * @param systemIndexFields - 系统字段索引列表
70
- * @param dbName - 数据库名称(用于检查索引是否存在)
71
- */
72
- async function createTableIndexes(sql: SQL, tableName: string, fields: Record<string, FieldDefinition>, systemIndexFields: string[], dbName?: string): Promise<void> {
73
- const indexTasks: Promise<void>[] = [];
74
-
75
- // 获取现有索引(MySQL 不支持 IF NOT EXISTS,需要先检查)
76
- let existingIndexes: Record<string, string[]> = {};
77
- if (isMySQL()) {
78
- existingIndexes = await getTableIndexes(sql, tableName, dbName || "");
79
- }
80
-
81
- // 系统字段索引
82
- for (const sysField of systemIndexFields) {
83
- const indexName = `idx_${sysField}`;
84
- // MySQL 跳过已存在的索引
85
- if (isMySQL() && existingIndexes[indexName]) {
86
- continue;
87
- }
88
- const stmt = buildIndexSQL(tableName, indexName, sysField, "create");
89
- if (IS_PLAN) {
90
- Logger.debug(`[计划] ${stmt}`);
91
- } else {
92
- indexTasks.push(sql.unsafe(stmt));
93
- }
94
- }
95
-
96
- // 业务字段索引
97
- for (const [fieldKey, fieldDef] of Object.entries(fields)) {
98
- // 转换字段名为下划线格式
99
- const dbFieldName = snakeCase(fieldKey);
100
-
101
- if (fieldDef.index === true) {
102
- const indexName = `idx_${dbFieldName}`;
103
- // MySQL 跳过已存在的索引
104
- if (isMySQL() && existingIndexes[indexName]) {
105
- continue;
106
- }
107
- const stmt = buildIndexSQL(tableName, indexName, dbFieldName, "create");
108
- if (IS_PLAN) {
109
- Logger.debug(`[计划] ${stmt}`);
110
- } else {
111
- indexTasks.push(sql.unsafe(stmt));
112
- }
113
- }
114
- }
115
-
116
- // 并行执行所有索引创建
117
- if (indexTasks.length > 0) {
118
- await Promise.all(indexTasks);
119
- }
120
- }
121
-
122
- /**
123
- * 创建表(包含系统字段和业务字段)
124
- *
125
- * @param sql - SQL 客户端实例
126
- * @param tableName - 表名
127
- * @param fields - 字段定义对象
128
- * @param systemIndexFields - 系统字段索引列表(可选,默认使用 ['created_at', 'updated_at', 'state'])
129
- * @param dbName - 数据库名称(用于检查索引是否存在)
130
- */
131
- export async function createTable(sql: SQL, tableName: string, fields: Record<string, FieldDefinition>, systemIndexFields: string[] = ["created_at", "updated_at", "state"], dbName?: string): Promise<void> {
132
- // 构建列定义
133
- const colDefs = [...buildSystemColumnDefs(), ...buildBusinessColumnDefs(fields)];
134
-
135
- // 生成 CREATE TABLE 语句
136
- const cols = colDefs.join(",\n ");
137
- const tableQuoted = quoteIdentifier(tableName);
138
- const { ENGINE, CHARSET, COLLATE } = MYSQL_TABLE_CONFIG;
139
- const createSQL = isMySQL()
140
- ? `CREATE TABLE ${tableQuoted} (
141
- ${cols}
142
- ) ENGINE=${ENGINE} DEFAULT CHARSET=${CHARSET} COLLATE=${COLLATE}`
143
- : `CREATE TABLE ${tableQuoted} (
144
- ${cols}
145
- )`;
146
-
147
- if (IS_PLAN) {
148
- Logger.debug(`[计划] ${createSQL.replace(/\n+/g, " ")}`);
149
- } else {
150
- await sql.unsafe(createSQL);
151
- }
152
-
153
- // PostgreSQL: 添加列注释(使用字段 name 作为数据库列注释)
154
- if (isPG() && !IS_PLAN) {
155
- await addPostgresComments(sql, tableName, fields);
156
- } else if (isPG() && IS_PLAN) {
157
- // 计划模式也要输出注释语句
158
- await addPostgresComments(sql, tableName, fields);
159
- }
160
-
161
- // 创建索引
162
- await createTableIndexes(sql, tableName, fields, systemIndexFields, dbName);
163
- }
@@ -1,132 +0,0 @@
1
- /**
2
- * syncDb 类型处理模块
3
- *
4
- * 包含:
5
- * - SQL 类型映射和转换
6
- * - 默认值处理
7
- * - 类型判断工具
8
- */
9
-
10
- import { isMySQL, getTypeMapping } from "./constants.js";
11
-
12
- /**
13
- * 判断是否为字符串或数组类型(需要长度参数)
14
- *
15
- * @param fieldType - 字段类型
16
- * @returns 是否为字符串或数组类型
17
- *
18
- * @example
19
- * isStringOrArrayType('string') // => true
20
- * isStringOrArrayType('array_string') // => true
21
- * isStringOrArrayType('array_number_string') // => true
22
- * isStringOrArrayType('array_text') // => false
23
- * isStringOrArrayType('number') // => false
24
- * isStringOrArrayType('text') // => false
25
- */
26
- export function isStringOrArrayType(fieldType: string): boolean {
27
- return fieldType === "string" || fieldType === "array_string" || fieldType === "array_number_string";
28
- }
29
-
30
- /**
31
- * 获取 SQL 数据类型
32
- *
33
- * @param fieldType - 字段类型(number/string/text/array_string/array_text)
34
- * @param fieldMax - 最大长度(string/array_string 类型需要)
35
- * @param unsigned - 是否无符号(仅 MySQL number 类型有效)
36
- * @returns SQL 类型字符串
37
- *
38
- * @example
39
- * getSqlType('string', 100) // => 'VARCHAR(100)'
40
- * getSqlType('number', null, true) // => 'BIGINT UNSIGNED'
41
- * getSqlType('text', null) // => 'MEDIUMTEXT'
42
- * getSqlType('array_string', 500) // => 'VARCHAR(500)'
43
- * getSqlType('array_text', null) // => 'MEDIUMTEXT'
44
- */
45
- export function getSqlType(fieldType: string, fieldMax: number | null, unsigned: boolean = false): string {
46
- const typeMapping = getTypeMapping();
47
- if (isStringOrArrayType(fieldType)) {
48
- return `${typeMapping[fieldType]}(${fieldMax})`;
49
- }
50
- // 处理 UNSIGNED 修饰符(仅 MySQL number 类型)
51
- const baseType = typeMapping[fieldType] || "TEXT";
52
- if (isMySQL() && fieldType === "number" && unsigned) {
53
- return `${baseType} UNSIGNED`;
54
- }
55
- return baseType;
56
- }
57
-
58
- /**
59
- * 处理默认值:将 null 或 'null' 字符串转换为对应类型的默认值
60
- *
61
- * @param fieldDefault - 字段默认值(可能是 null 或 'null' 字符串)
62
- * @param fieldType - 字段类型(number/string/text/array)
63
- * @returns 实际默认值
64
- *
65
- * @example
66
- * resolveDefaultValue(null, 'string') // => ''
67
- * resolveDefaultValue(null, 'number') // => 0
68
- * resolveDefaultValue('null', 'number') // => 0
69
- * resolveDefaultValue(null, 'array_string') // => '[]'
70
- * resolveDefaultValue(null, 'text') // => 'null'
71
- * resolveDefaultValue(null, 'array_text') // => 'null' (TEXT 不支持默认值)
72
- * resolveDefaultValue('admin', 'string') // => 'admin'
73
- * resolveDefaultValue(0, 'number') // => 0
74
- */
75
- export function resolveDefaultValue(fieldDefault: any, fieldType: string): any {
76
- // null 或字符串 'null' 都表示使用类型默认值
77
- if (fieldDefault !== null && fieldDefault !== "null") {
78
- return fieldDefault;
79
- }
80
-
81
- // null 表示使用类型默认值
82
- switch (fieldType) {
83
- case "number":
84
- return 0;
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";
95
- default:
96
- return fieldDefault;
97
- }
98
- }
99
-
100
- /**
101
- * 生成 SQL DEFAULT 子句
102
- *
103
- * @param actualDefault - 实际默认值(已经过 resolveDefaultValue 处理)
104
- * @param fieldType - 字段类型
105
- * @returns SQL DEFAULT 子句字符串(包含前导空格),如果不需要则返回空字符串
106
- *
107
- * @example
108
- * generateDefaultSql(0, 'number') // => ' DEFAULT 0'
109
- * generateDefaultSql('admin', 'string') // => " DEFAULT 'admin'"
110
- * generateDefaultSql('', 'string') // => " DEFAULT ''"
111
- * generateDefaultSql('null', 'text') // => ''
112
- * generateDefaultSql('[]', 'array_text') // => '' (TEXT 类型不能有默认值)
113
- */
114
- export function generateDefaultSql(actualDefault: any, fieldType: string): string {
115
- // text 和 array_text 类型不设置默认值(MySQL TEXT 类型不支持默认值)
116
- if (fieldType === "text" || fieldType === "array_text" || actualDefault === "null") {
117
- return "";
118
- }
119
-
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)) {
123
- return ` DEFAULT ${actualDefault}`;
124
- } else {
125
- // 字符串需要转义单引号:' -> ''
126
- const escaped = String(actualDefault).replace(/'/g, "''");
127
- return ` DEFAULT '${escaped}'`;
128
- }
129
- }
130
-
131
- return "";
132
- }
@@ -1,69 +0,0 @@
1
- /**
2
- * syncDb 数据库版本检查模块
3
- *
4
- * 包含:
5
- * - 数据库版本验证(MySQL/PostgreSQL/SQLite)
6
- */
7
-
8
- import type { SQL } from "bun";
9
-
10
- import { DB_VERSION_REQUIREMENTS, isMySQL, isPG, isSQLite } from "./constants.js";
11
-
12
- /**
13
- * 数据库版本检查(按方言)
14
- *
15
- * 根据当前数据库类型检查版本是否符合最低要求:
16
- * - MySQL: >= 8.0
17
- * - PostgreSQL: >= 17
18
- * - SQLite: >= 3.50.0
19
- *
20
- * @param sql - SQL 客户端实例
21
- * @throws {Error} 如果数据库版本不符合要求或无法获取版本信息
22
- */
23
- export async function ensureDbVersion(sql: SQL): Promise<void> {
24
- if (!sql) throw new Error("SQL 客户端未初始化");
25
-
26
- if (isMySQL()) {
27
- const r = await sql`SELECT VERSION() AS version`;
28
- if (!r || r.length === 0 || !r[0]?.version) {
29
- throw new Error("无法获取 MySQL 版本信息");
30
- }
31
- const version = r[0].version;
32
- const majorVersion = parseInt(String(version).split(".")[0], 10);
33
- if (!Number.isFinite(majorVersion) || majorVersion < DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR) {
34
- throw new Error(`此脚本仅支持 MySQL ${DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR}.0+,当前版本: ${version}`);
35
- }
36
- return;
37
- }
38
-
39
- if (isPG()) {
40
- const r = await sql`SELECT version() AS version`;
41
- if (!r || r.length === 0 || !r[0]?.version) {
42
- throw new Error("无法获取 PostgreSQL 版本信息");
43
- }
44
- const versionText = r[0].version;
45
- const m = /PostgreSQL\s+(\d+)/i.exec(versionText);
46
- const major = m ? parseInt(m[1], 10) : NaN;
47
- if (!Number.isFinite(major) || major < DB_VERSION_REQUIREMENTS.POSTGRES_MIN_MAJOR) {
48
- throw new Error(`此脚本要求 PostgreSQL >= ${DB_VERSION_REQUIREMENTS.POSTGRES_MIN_MAJOR},当前: ${versionText}`);
49
- }
50
- return;
51
- }
52
-
53
- if (isSQLite()) {
54
- const r = await sql`SELECT sqlite_version() AS version`;
55
- if (!r || r.length === 0 || !r[0]?.version) {
56
- throw new Error("无法获取 SQLite 版本信息");
57
- }
58
- const version = r[0].version;
59
- // 强制最低版本:SQLite ≥ 3.50.0
60
- const [maj, min, patch] = String(version)
61
- .split(".")
62
- .map((v) => parseInt(v, 10) || 0);
63
- const vnum = maj * 10000 + min * 100 + patch; // 3.50.0 -> 35000
64
- if (!Number.isFinite(vnum) || vnum < DB_VERSION_REQUIREMENTS.SQLITE_MIN_VERSION_NUM) {
65
- throw new Error(`此脚本要求 SQLite >= ${DB_VERSION_REQUIREMENTS.SQLITE_MIN_VERSION},当前: ${version}`);
66
- }
67
- return;
68
- }
69
- }