befly 2.3.2 → 3.0.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.
Files changed (93) hide show
  1. package/apis/health/info.ts +64 -0
  2. package/apis/tool/tokenCheck.ts +51 -0
  3. package/bin/befly.ts +202 -0
  4. package/checks/conflict.ts +408 -0
  5. package/checks/table.ts +284 -0
  6. package/config/env.ts +218 -0
  7. package/config/reserved.ts +96 -0
  8. package/main.ts +101 -0
  9. package/package.json +45 -16
  10. package/plugins/{db.js → db.ts} +25 -12
  11. package/plugins/logger.ts +28 -0
  12. package/plugins/redis.ts +51 -0
  13. package/plugins/tool.ts +34 -0
  14. package/scripts/syncDb/apply.ts +171 -0
  15. package/scripts/syncDb/constants.ts +70 -0
  16. package/scripts/syncDb/ddl.ts +182 -0
  17. package/scripts/syncDb/helpers.ts +172 -0
  18. package/scripts/syncDb/index.ts +215 -0
  19. package/scripts/syncDb/schema.ts +199 -0
  20. package/scripts/syncDb/sqlite.ts +50 -0
  21. package/scripts/syncDb/state.ts +104 -0
  22. package/scripts/syncDb/table.ts +204 -0
  23. package/scripts/syncDb/tableCreate.ts +142 -0
  24. package/scripts/syncDb/tests/constants.test.ts +104 -0
  25. package/scripts/syncDb/tests/ddl.test.ts +134 -0
  26. package/scripts/syncDb/tests/helpers.test.ts +70 -0
  27. package/scripts/syncDb/types.ts +92 -0
  28. package/scripts/syncDb/version.ts +73 -0
  29. package/scripts/syncDb.ts +9 -0
  30. package/scripts/syncDev.ts +112 -0
  31. package/system.ts +149 -0
  32. package/tables/_common.json +21 -0
  33. package/tables/admin.json +10 -0
  34. package/tsconfig.json +58 -0
  35. package/types/api.d.ts +246 -0
  36. package/types/befly.d.ts +234 -0
  37. package/types/common.d.ts +215 -0
  38. package/types/context.ts +167 -0
  39. package/types/crypto.d.ts +23 -0
  40. package/types/database.d.ts +278 -0
  41. package/types/index.d.ts +16 -0
  42. package/types/index.ts +459 -0
  43. package/types/jwt.d.ts +99 -0
  44. package/types/logger.d.ts +43 -0
  45. package/types/plugin.d.ts +109 -0
  46. package/types/redis.d.ts +44 -0
  47. package/types/tool.d.ts +67 -0
  48. package/types/validator.d.ts +45 -0
  49. package/utils/addonHelper.ts +60 -0
  50. package/utils/api.ts +23 -0
  51. package/utils/{colors.js → colors.ts} +79 -21
  52. package/utils/crypto.ts +308 -0
  53. package/utils/datetime.ts +51 -0
  54. package/utils/dbHelper.ts +142 -0
  55. package/utils/errorHandler.ts +68 -0
  56. package/utils/index.ts +46 -0
  57. package/utils/jwt.ts +493 -0
  58. package/utils/logger.ts +284 -0
  59. package/utils/objectHelper.ts +68 -0
  60. package/utils/pluginHelper.ts +62 -0
  61. package/utils/redisHelper.ts +338 -0
  62. package/utils/response.ts +38 -0
  63. package/utils/{sqlBuilder.js → sqlBuilder.ts} +233 -97
  64. package/utils/sqlHelper.ts +447 -0
  65. package/utils/tableHelper.ts +167 -0
  66. package/utils/tool.ts +230 -0
  67. package/utils/typeHelper.ts +101 -0
  68. package/utils/validate.ts +451 -0
  69. package/utils/{xml.js → xml.ts} +100 -74
  70. package/.npmrc +0 -3
  71. package/.prettierignore +0 -2
  72. package/.prettierrc +0 -11
  73. package/apis/health/info.js +0 -49
  74. package/apis/tool/tokenCheck.js +0 -29
  75. package/checks/table.js +0 -221
  76. package/config/env.js +0 -62
  77. package/main.js +0 -579
  78. package/plugins/logger.js +0 -14
  79. package/plugins/redis.js +0 -32
  80. package/plugins/tool.js +0 -8
  81. package/scripts/syncDb.js +0 -603
  82. package/system.js +0 -118
  83. package/tables/common.json +0 -16
  84. package/tables/tool.json +0 -6
  85. package/utils/api.js +0 -27
  86. package/utils/crypto.js +0 -260
  87. package/utils/index.js +0 -387
  88. package/utils/jwt.js +0 -387
  89. package/utils/logger.js +0 -143
  90. package/utils/redisHelper.js +0 -74
  91. package/utils/sqlManager.js +0 -471
  92. package/utils/tool.js +0 -31
  93. package/utils/validate.js +0 -228
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Tool 插件 - TypeScript 版本
3
+ * 提供数据处理工具
4
+ */
5
+
6
+ import { Tool } from '../utils/tool.js';
7
+ import { Logger } from '../utils/logger.js';
8
+ import type { Plugin } from '../types/plugin.js';
9
+ import type { BeflyContext } from '../types/befly.js';
10
+
11
+ /**
12
+ * Tool 插件
13
+ */
14
+ const toolPlugin: Plugin = {
15
+ name: '_tool',
16
+ after: ['_redis', '_db'],
17
+
18
+ async onInit(befly: BeflyContext): Promise<Tool> {
19
+ try {
20
+ const tool = new Tool(befly);
21
+ Logger.info('Tool 插件初始化成功');
22
+ return tool;
23
+ } catch (error: any) {
24
+ Logger.error({
25
+ msg: 'Tool 初始化失败',
26
+ message: error.message,
27
+ stack: error.stack
28
+ });
29
+ throw error;
30
+ }
31
+ }
32
+ };
33
+
34
+ export default toolPlugin;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * syncDb 变更应用模块
3
+ *
4
+ * 包含:
5
+ * - 比较字段定义变化
6
+ * - 应用表结构变更计划
7
+ */
8
+
9
+ import { Logger } from '../../utils/logger.js';
10
+ import { parseRule } from '../../utils/tableHelper.js';
11
+ import { IS_MYSQL, IS_PG, IS_SQLITE, CHANGE_TYPE_LABELS, typeMapping } from './constants.js';
12
+ import { logFieldChange, resolveDefaultValue, isStringOrArrayType } from './helpers.js';
13
+ import { executeDDLSafely, buildIndexSQL } from './ddl.js';
14
+ import { rebuildSqliteTable } from './sqlite.js';
15
+ import type { FieldChange, IndexAction, TablePlan, ColumnInfo } from './types.js';
16
+ import type { SQL } from 'bun';
17
+
18
+ // 是否为计划模式(从环境变量读取)
19
+ const IS_PLAN = process.argv.includes('--plan');
20
+
21
+ /**
22
+ * 比较字段定义变化
23
+ *
24
+ * 对比现有列信息和新的字段规则,识别变化类型:
25
+ * - 长度变化(string/array 类型)
26
+ * - 注释变化(MySQL/PG)
27
+ * - 数据类型变化
28
+ * - 默认值变化
29
+ *
30
+ * @param existingColumn - 现有列信息
31
+ * @param newRule - 新的字段规则字符串
32
+ * @param colName - 列名(未使用,保留参数兼容性)
33
+ * @returns 变化数组
34
+ */
35
+ export function compareFieldDefinition(existingColumn: ColumnInfo, newRule: string, colName: string): FieldChange[] {
36
+ const parsed = parseRule(newRule);
37
+ const { name: fieldName, type: fieldType, max: fieldMax, default: fieldDefault } = parsed;
38
+ const changes: FieldChange[] = [];
39
+
40
+ // 检查长度变化(string和array类型) - SQLite 不比较长度
41
+ if (!IS_SQLITE && isStringOrArrayType(fieldType)) {
42
+ if (existingColumn.length !== fieldMax) {
43
+ changes.push({
44
+ type: 'length',
45
+ current: existingColumn.length,
46
+ expected: fieldMax
47
+ });
48
+ }
49
+ }
50
+
51
+ // 检查注释变化(MySQL/PG 支持列注释)
52
+ if (!IS_SQLITE) {
53
+ const currentComment = existingColumn.comment || '';
54
+ if (currentComment !== fieldName) {
55
+ changes.push({
56
+ type: 'comment',
57
+ current: currentComment,
58
+ expected: fieldName
59
+ });
60
+ }
61
+ }
62
+
63
+ // 检查数据类型变化(按方言)
64
+ if (existingColumn.type.toLowerCase() !== typeMapping[fieldType].toLowerCase()) {
65
+ changes.push({
66
+ type: 'datatype',
67
+ current: existingColumn.type,
68
+ expected: typeMapping[fieldType].toLowerCase()
69
+ });
70
+ }
71
+
72
+ // 使用公共函数处理默认值
73
+ const expectedDefault = resolveDefaultValue(fieldDefault, fieldType);
74
+
75
+ // 检查默认值变化
76
+ if (String(existingColumn.defaultValue) !== String(expectedDefault)) {
77
+ changes.push({
78
+ type: 'default',
79
+ current: existingColumn.defaultValue,
80
+ expected: expectedDefault
81
+ });
82
+ }
83
+
84
+ return changes;
85
+ }
86
+
87
+ /**
88
+ * 将表结构计划应用到数据库(执行 DDL/索引/注释等)
89
+ *
90
+ * 根据数据库方言和计划内容,执行相应的 DDL 操作:
91
+ * - SQLite: 新增字段直接 ALTER,其他操作需要重建表
92
+ * - MySQL: 尝试在线 DDL(INSTANT/INPLACE)
93
+ * - PostgreSQL: 直接 ALTER
94
+ *
95
+ * @param sql - SQL 客户端实例
96
+ * @param tableName - 表名
97
+ * @param fields - 字段定义对象
98
+ * @param plan - 表结构变更计划
99
+ * @param globalCount - 全局统计对象(用于计数)
100
+ */
101
+ export async function applyTablePlan(sql: SQL, tableName: string, fields: Record<string, string>, plan: TablePlan, globalCount: Record<string, number>): Promise<void> {
102
+ if (!plan || !plan.changed) return;
103
+
104
+ // SQLite: 仅支持部分 ALTER;需要时走重建
105
+ if (IS_SQLITE) {
106
+ if (plan.modifyClauses.length > 0 || plan.defaultClauses.length > 0) {
107
+ if (IS_PLAN) Logger.info(`[计划] 重建表 ${tableName} 以应用列修改/默认值变化`);
108
+ else await rebuildSqliteTable(sql, tableName, fields);
109
+ } else {
110
+ for (const c of plan.addClauses) {
111
+ const stmt = `ALTER TABLE "${tableName}" ${c}`;
112
+ if (IS_PLAN) Logger.info(`[计划] ${stmt}`);
113
+ else await sql.unsafe(stmt);
114
+ }
115
+ }
116
+ } else {
117
+ const clauses = [...plan.addClauses, ...plan.modifyClauses];
118
+ if (clauses.length > 0) {
119
+ const suffix = IS_MYSQL ? ', ALGORITHM=INSTANT, LOCK=NONE' : '';
120
+ const stmt = IS_MYSQL ? `ALTER TABLE \`${tableName}\` ${clauses.join(', ')}${suffix}` : `ALTER TABLE "${tableName}" ${clauses.join(', ')}`;
121
+ if (IS_PLAN) Logger.info(`[计划] ${stmt}`);
122
+ else if (IS_MYSQL) await executeDDLSafely(sql, stmt);
123
+ else await sql.unsafe(stmt);
124
+ }
125
+ }
126
+
127
+ // 默认值专用 ALTER(SQLite 不支持)
128
+ if (plan.defaultClauses.length > 0) {
129
+ if (IS_SQLITE) {
130
+ Logger.warn(`SQLite 不支持修改默认值,表 ${tableName} 的默认值变更已跳过`);
131
+ } else {
132
+ const suffix = IS_MYSQL ? ', ALGORITHM=INSTANT, LOCK=NONE' : '';
133
+ const stmt = IS_MYSQL ? `ALTER TABLE \`${tableName}\` ${plan.defaultClauses.join(', ')}${suffix}` : `ALTER TABLE "${tableName}" ${plan.defaultClauses.join(', ')}`;
134
+ if (IS_PLAN) Logger.info(`[计划] ${stmt}`);
135
+ else if (IS_MYSQL) await executeDDLSafely(sql, stmt);
136
+ else await sql.unsafe(stmt);
137
+ }
138
+ }
139
+
140
+ // 索引操作
141
+ for (const act of plan.indexActions) {
142
+ const stmt = buildIndexSQL(tableName, act.indexName, act.fieldName, act.action);
143
+ if (IS_PLAN) {
144
+ Logger.info(`[计划] ${stmt}`);
145
+ } else {
146
+ try {
147
+ await sql.unsafe(stmt);
148
+ if (act.action === 'create') {
149
+ Logger.info(`[索引变化] 新建索引 ${tableName}.${act.indexName} (${act.fieldName})`);
150
+ } else {
151
+ Logger.info(`[索引变化] 删除索引 ${tableName}.${act.indexName} (${act.fieldName})`);
152
+ }
153
+ } catch (error: any) {
154
+ Logger.error(`${act.action === 'create' ? '创建' : '删除'}索引失败: ${error.message}`);
155
+ Logger.error(`表名: ${tableName}, 索引名: ${act.indexName}, 字段: ${act.fieldName}`);
156
+ throw error;
157
+ }
158
+ }
159
+ }
160
+
161
+ // PG 列注释
162
+ if (IS_PG && plan.commentActions && plan.commentActions.length > 0) {
163
+ for (const stmt of plan.commentActions) {
164
+ if (IS_PLAN) Logger.info(`[计划] ${stmt}`);
165
+ else await sql.unsafe(stmt);
166
+ }
167
+ }
168
+
169
+ // 计数
170
+ globalCount.modifiedTables++;
171
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * syncDb 常量定义模块
3
+ *
4
+ * 包含:
5
+ * - 数据库版本要求
6
+ * - 系统字段定义
7
+ * - 索引字段配置
8
+ * - 字段变更类型标签
9
+ */
10
+
11
+ import { Env } from '../../config/env.js';
12
+
13
+ /**
14
+ * 数据库版本要求
15
+ */
16
+ export const DB_VERSION_REQUIREMENTS = {
17
+ MYSQL_MIN_MAJOR: 8,
18
+ POSTGRES_MIN_MAJOR: 17,
19
+ SQLITE_MIN_VERSION: '3.50.0',
20
+ SQLITE_MIN_VERSION_NUM: 35000 // 3 * 10000 + 50 * 100
21
+ } as const;
22
+
23
+ /**
24
+ * 系统字段定义(所有表都包含的固定字段)
25
+ */
26
+ export const SYSTEM_FIELDS = {
27
+ ID: { name: 'id', comment: '主键ID' },
28
+ CREATED_AT: { name: 'created_at', comment: '创建时间' },
29
+ UPDATED_AT: { name: 'updated_at', comment: '更新时间' },
30
+ DELETED_AT: { name: 'deleted_at', comment: '删除时间' },
31
+ STATE: { name: 'state', comment: '状态字段' }
32
+ } as const;
33
+
34
+ /**
35
+ * 需要创建索引的系统字段
36
+ */
37
+ export const SYSTEM_INDEX_FIELDS = ['created_at', 'updated_at', 'state'] as const;
38
+
39
+ /**
40
+ * 字段变更类型的中文标签映射
41
+ */
42
+ export const CHANGE_TYPE_LABELS = {
43
+ length: '长度',
44
+ datatype: '类型',
45
+ comment: '注释',
46
+ default: '默认值'
47
+ } as const;
48
+
49
+ /**
50
+ * MySQL 表配置(支持环境变量自定义)
51
+ */
52
+ export const MYSQL_TABLE_CONFIG = {
53
+ ENGINE: Env.MYSQL_ENGINE || 'InnoDB',
54
+ CHARSET: Env.MYSQL_CHARSET || 'utf8mb4',
55
+ COLLATE: Env.MYSQL_COLLATE || 'utf8mb4_0900_as_cs'
56
+ } as const;
57
+
58
+ // 数据库类型判断
59
+ export const DB = (Env.DB_TYPE || 'mysql').toLowerCase();
60
+ export const IS_MYSQL = DB === 'mysql';
61
+ export const IS_PG = DB === 'postgresql' || DB === 'postgres';
62
+ export const IS_SQLITE = DB === 'sqlite';
63
+
64
+ // 字段类型映射(按方言)
65
+ export const typeMapping = {
66
+ number: IS_SQLITE ? 'INTEGER' : 'BIGINT',
67
+ string: IS_SQLITE ? 'TEXT' : IS_PG ? 'character varying' : 'VARCHAR',
68
+ text: IS_MYSQL ? 'MEDIUMTEXT' : 'TEXT',
69
+ array: IS_SQLITE ? 'TEXT' : IS_PG ? 'character varying' : 'VARCHAR'
70
+ };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * syncDb DDL 构建模块
3
+ *
4
+ * 包含:
5
+ * - 构建索引 SQL
6
+ * - 生成 DDL 子句(添加/修改列)
7
+ * - 安全执行 DDL(MySQL 降级策略)
8
+ * - 构建系统列和业务列定义
9
+ */
10
+
11
+ import { Logger } from '../../utils/logger.js';
12
+ import { parseRule } from '../../utils/tableHelper.js';
13
+ import { IS_MYSQL, IS_PG, IS_SQLITE, SYSTEM_INDEX_FIELDS, typeMapping } from './constants.js';
14
+ import { quoteIdentifier, resolveDefaultValue, generateDefaultSql, getSqlType, escapeComment } from './helpers.js';
15
+ import type { SQL } from 'bun';
16
+
17
+ /**
18
+ * 构建索引操作 SQL(统一使用在线策略)
19
+ *
20
+ * @param tableName - 表名
21
+ * @param indexName - 索引名
22
+ * @param fieldName - 字段名
23
+ * @param action - 操作类型(create/drop)
24
+ * @returns SQL 语句
25
+ */
26
+ export function buildIndexSQL(tableName: string, indexName: string, fieldName: string, action: 'create' | 'drop'): string {
27
+ const tableQuoted = quoteIdentifier(tableName);
28
+ const indexQuoted = quoteIdentifier(indexName);
29
+ const fieldQuoted = quoteIdentifier(fieldName);
30
+
31
+ if (IS_MYSQL) {
32
+ const parts = [];
33
+ if (action === 'create') {
34
+ parts.push(`ADD INDEX ${indexQuoted} (${fieldQuoted})`);
35
+ } else {
36
+ parts.push(`DROP INDEX ${indexQuoted}`);
37
+ }
38
+ // 始终使用在线算法
39
+ parts.push('ALGORITHM=INPLACE');
40
+ parts.push('LOCK=NONE');
41
+ return `ALTER TABLE ${tableQuoted} ${parts.join(', ')}`;
42
+ }
43
+
44
+ if (IS_PG) {
45
+ if (action === 'create') {
46
+ // 始终使用 CONCURRENTLY
47
+ return `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${indexQuoted} ON ${tableQuoted}(${fieldQuoted})`;
48
+ }
49
+ return `DROP INDEX CONCURRENTLY IF EXISTS ${indexQuoted}`;
50
+ }
51
+
52
+ // SQLite
53
+ if (action === 'create') {
54
+ return `CREATE INDEX IF NOT EXISTS ${indexQuoted} ON ${tableQuoted}(${fieldQuoted})`;
55
+ }
56
+ return `DROP INDEX IF EXISTS ${indexQuoted}`;
57
+ }
58
+
59
+ /**
60
+ * 构建系统字段列定义
61
+ *
62
+ * @returns 系统字段的列定义数组
63
+ */
64
+ export function buildSystemColumnDefs(): string[] {
65
+ if (IS_MYSQL) {
66
+ return ['`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT "主键ID"', '`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT "创建时间"', '`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT "更新时间"', '`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT "删除时间"', '`state` BIGINT NOT NULL DEFAULT 0 COMMENT "状态字段"'];
67
+ }
68
+ return ['"id" INTEGER PRIMARY KEY', '"created_at" INTEGER NOT NULL DEFAULT 0', '"updated_at" INTEGER NOT NULL DEFAULT 0', '"deleted_at" INTEGER NOT NULL DEFAULT 0', '"state" INTEGER NOT NULL DEFAULT 0'];
69
+ }
70
+
71
+ /**
72
+ * 构建业务字段列定义
73
+ *
74
+ * @param fields - 字段定义对象
75
+ * @returns 业务字段的列定义数组
76
+ */
77
+ export function buildBusinessColumnDefs(fields: Record<string, string>): string[] {
78
+ const colDefs: string[] = [];
79
+
80
+ for (const [fieldKey, fieldRule] of Object.entries(fields)) {
81
+ const parsed = parseRule(fieldRule);
82
+ const { name: fieldName, type: fieldType, max: fieldMax, default: fieldDefault } = parsed;
83
+ const sqlType = getSqlType(fieldType, fieldMax);
84
+
85
+ // 使用公共函数处理默认值
86
+ const actualDefault = resolveDefaultValue(fieldDefault, fieldType);
87
+ const defaultSql = generateDefaultSql(actualDefault, fieldType);
88
+
89
+ if (IS_MYSQL) {
90
+ colDefs.push(`\`${fieldKey}\` ${sqlType} NOT NULL${defaultSql} COMMENT "${escapeComment(fieldName)}"`);
91
+ } else {
92
+ colDefs.push(`"${fieldKey}" ${sqlType} NOT NULL${defaultSql}`);
93
+ }
94
+ }
95
+
96
+ return colDefs;
97
+ }
98
+
99
+ /**
100
+ * 生成字段 DDL 子句(不含 ALTER TABLE 前缀)
101
+ *
102
+ * @param fieldKey - 字段键名
103
+ * @param fieldRule - 字段规则字符串
104
+ * @param isAdd - 是否为添加字段(true)还是修改字段(false)
105
+ * @returns DDL 子句
106
+ */
107
+ export function generateDDLClause(fieldKey: string, fieldRule: string, isAdd: boolean = false): string {
108
+ const parsed = parseRule(fieldRule);
109
+ const { name: fieldName, type: fieldType, max: fieldMax, default: fieldDefault } = parsed;
110
+ const sqlType = getSqlType(fieldType, fieldMax);
111
+
112
+ // 使用公共函数处理默认值
113
+ const actualDefault = resolveDefaultValue(fieldDefault, fieldType);
114
+ const defaultSql = generateDefaultSql(actualDefault, fieldType);
115
+
116
+ if (IS_MYSQL) {
117
+ return `${isAdd ? 'ADD COLUMN' : 'MODIFY COLUMN'} \`${fieldKey}\` ${sqlType} NOT NULL${defaultSql} COMMENT "${escapeComment(fieldName)}"`;
118
+ }
119
+ if (IS_PG) {
120
+ if (isAdd) return `ADD COLUMN IF NOT EXISTS "${fieldKey}" ${sqlType} NOT NULL${defaultSql}`;
121
+ // PG 修改:类型与非空可分条执行,生成 TYPE 改变;非空另由上层统一控制
122
+ return `ALTER COLUMN "${fieldKey}" TYPE ${sqlType}`;
123
+ }
124
+ // SQLite 仅支持 ADD COLUMN(>=3.50.0:支持 IF NOT EXISTS)
125
+ if (isAdd) return `ADD COLUMN IF NOT EXISTS "${fieldKey}" ${sqlType} NOT NULL${defaultSql}`;
126
+ return '';
127
+ }
128
+
129
+ /**
130
+ * 安全执行 DDL 语句(MySQL 降级策略)
131
+ *
132
+ * 执行 DDL 时按以下顺序尝试:
133
+ * 1. ALGORITHM=INSTANT (最快,无表锁)
134
+ * 2. ALGORITHM=INPLACE (在线 DDL)
135
+ * 3. 传统 DDL (可能需要表锁)
136
+ *
137
+ * @param sql - SQL 客户端实例
138
+ * @param stmt - DDL 语句
139
+ * @returns 是否执行成功
140
+ * @throws {Error} 如果所有尝试都失败
141
+ */
142
+ export async function executeDDLSafely(sql: SQL, stmt: string): Promise<boolean> {
143
+ try {
144
+ await sql.unsafe(stmt);
145
+ return true;
146
+ } catch (error: any) {
147
+ // MySQL 专用降级路径
148
+ if (stmt.includes('ALGORITHM=INSTANT')) {
149
+ const inplaceSql = stmt.replace(/ALGORITHM=INSTANT/g, 'ALGORITHM=INPLACE');
150
+ try {
151
+ await sql.unsafe(inplaceSql);
152
+ return true;
153
+ } catch (inplaceError) {
154
+ // 最后尝试传统DDL:移除 ALGORITHM/LOCK 附加子句后执行
155
+ const traditionSql = stmt
156
+ .replace(/,\s*ALGORITHM=INPLACE/g, '')
157
+ .replace(/,\s*ALGORITHM=INSTANT/g, '')
158
+ .replace(/,\s*LOCK=(NONE|SHARED|EXCLUSIVE)/g, '');
159
+ await sql.unsafe(traditionSql);
160
+ return true;
161
+ }
162
+ } else {
163
+ throw error;
164
+ }
165
+ }
166
+ }
167
+
168
+ /**
169
+ * PG 兼容类型变更识别:无需数据重写的宽化型变更
170
+ *
171
+ * @param currentType - 当前类型
172
+ * @param newType - 新类型
173
+ * @returns 是否为兼容变更
174
+ */
175
+ export function isPgCompatibleTypeChange(currentType: string, newType: string): boolean {
176
+ const c = String(currentType || '').toLowerCase();
177
+ const n = String(newType || '').toLowerCase();
178
+ // varchar -> text 视为宽化
179
+ if (c === 'character varying' && n === 'text') return true;
180
+ // text -> character varying 非宽化(可能截断),不兼容
181
+ return false;
182
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * syncDb 辅助函数模块
3
+ *
4
+ * 包含:
5
+ * - SQL 标识符引用
6
+ * - 日志格式化
7
+ * - 类型判断工具
8
+ * - 默认值处理
9
+ */
10
+
11
+ import { IS_MYSQL, IS_PG, typeMapping } from './constants.js';
12
+ import { isType } from '../../utils/typeHelper.js';
13
+
14
+ /**
15
+ * 根据数据库类型引用标识符
16
+ *
17
+ * @param identifier - 标识符(表名、列名等)
18
+ * @returns 引用后的标识符
19
+ *
20
+ * @example
21
+ * quoteIdentifier('user_table')
22
+ * // MySQL: `user_table`
23
+ * // PostgreSQL: "user_table"
24
+ * // SQLite: user_table
25
+ */
26
+ export function quoteIdentifier(identifier: string): string {
27
+ if (IS_MYSQL) return `\`${identifier}\``;
28
+ if (IS_PG) return `"${identifier}"`;
29
+ return identifier; // SQLite 无需引用
30
+ }
31
+
32
+ /**
33
+ * 处理默认值:将 'null' 字符串转换为对应类型的默认值
34
+ *
35
+ * @param fieldDefault - 字段默认值(可能是 'null' 字符串)
36
+ * @param fieldType - 字段类型(number/string/text/array)
37
+ * @returns 实际默认值
38
+ *
39
+ * @example
40
+ * resolveDefaultValue('null', 'string') // => ''
41
+ * resolveDefaultValue('null', 'number') // => 0
42
+ * resolveDefaultValue('null', 'array') // => '[]'
43
+ * resolveDefaultValue('null', 'text') // => 'null'
44
+ * resolveDefaultValue('admin', 'string') // => 'admin'
45
+ */
46
+ export function resolveDefaultValue(fieldDefault: any, fieldType: 'number' | 'string' | 'text' | 'array'): any {
47
+ if (fieldDefault !== 'null') {
48
+ return fieldDefault;
49
+ }
50
+
51
+ // null 表示使用类型默认值
52
+ switch (fieldType) {
53
+ case 'number':
54
+ return 0;
55
+ case 'string':
56
+ return '';
57
+ case 'array':
58
+ return '[]';
59
+ case 'text':
60
+ // text 类型不设置默认值,保持 'null'
61
+ return 'null';
62
+ default:
63
+ return fieldDefault;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * 生成 SQL DEFAULT 子句
69
+ *
70
+ * @param actualDefault - 实际默认值(已经过 resolveDefaultValue 处理)
71
+ * @param fieldType - 字段类型
72
+ * @returns SQL DEFAULT 子句字符串(包含前导空格),如果不需要则返回空字符串
73
+ *
74
+ * @example
75
+ * generateDefaultSql(0, 'number') // => ' DEFAULT 0'
76
+ * generateDefaultSql('admin', 'string') // => " DEFAULT 'admin'"
77
+ * generateDefaultSql('', 'string') // => " DEFAULT ''"
78
+ * generateDefaultSql('null', 'text') // => ''
79
+ */
80
+ export function generateDefaultSql(actualDefault: any, fieldType: 'number' | 'string' | 'text' | 'array'): string {
81
+ // text 类型不设置默认值
82
+ if (fieldType === 'text' || actualDefault === 'null') {
83
+ return '';
84
+ }
85
+
86
+ // 仅 number/string/array 类型设置默认值
87
+ if (fieldType === 'number' || fieldType === 'string' || fieldType === 'array') {
88
+ if (isType(actualDefault, 'number')) {
89
+ return ` DEFAULT ${actualDefault}`;
90
+ } else {
91
+ // 字符串需要转义单引号:' -> ''
92
+ const escaped = String(actualDefault).replace(/'/g, "''");
93
+ return ` DEFAULT '${escaped}'`;
94
+ }
95
+ }
96
+
97
+ return '';
98
+ }
99
+
100
+ /**
101
+ * 判断是否为字符串或数组类型(需要长度限制的类型)
102
+ *
103
+ * @param fieldType - 字段类型
104
+ * @returns 是否为 string 或 array
105
+ *
106
+ * @example
107
+ * isStringOrArrayType('string') // => true
108
+ * isStringOrArrayType('array') // => true
109
+ * isStringOrArrayType('number') // => false
110
+ * isStringOrArrayType('text') // => false
111
+ */
112
+ export function isStringOrArrayType(fieldType: string): boolean {
113
+ return fieldType === 'string' || fieldType === 'array';
114
+ }
115
+
116
+ /**
117
+ * 获取 SQL 数据类型
118
+ *
119
+ * @param fieldType - 字段类型(number/string/text/array)
120
+ * @param fieldMax - 最大长度(string/array 类型需要)
121
+ * @returns SQL 类型字符串
122
+ *
123
+ * @example
124
+ * getSqlType('string', 100) // => 'VARCHAR(100)'
125
+ * getSqlType('number', null) // => 'BIGINT'
126
+ * getSqlType('text', null) // => 'TEXT'
127
+ * getSqlType('array', 500) // => 'VARCHAR(500)'
128
+ */
129
+ export function getSqlType(fieldType: string, fieldMax: number | null): string {
130
+ if (isStringOrArrayType(fieldType)) {
131
+ return `${typeMapping[fieldType]}(${fieldMax})`;
132
+ }
133
+ return typeMapping[fieldType];
134
+ }
135
+
136
+ /**
137
+ * 转义 SQL 注释中的双引号
138
+ *
139
+ * @param str - 注释字符串
140
+ * @returns 转义后的字符串
141
+ *
142
+ * @example
143
+ * escapeComment('用户名称') // => '用户名称'
144
+ * escapeComment('用户"昵称"') // => '用户\\"昵称\\"'
145
+ */
146
+ export function escapeComment(str: string): string {
147
+ return String(str).replace(/"/g, '\\"');
148
+ }
149
+
150
+ /**
151
+ * 记录字段变更信息(带缩进和格式化)
152
+ *
153
+ * @param tableName - 表名
154
+ * @param fieldName - 字段名
155
+ * @param changeType - 变更类型(length/datatype/comment/default)
156
+ * @param oldValue - 旧值
157
+ * @param newValue - 新值
158
+ * @param changeLabel - 变更类型的中文标签
159
+ */
160
+ export function logFieldChange(tableName: string, fieldName: string, changeType: string, oldValue: any, newValue: any, changeLabel: string): void {
161
+ console.log(` 修改表 ${tableName} 的字段 ${fieldName} 的${changeLabel}: ${oldValue} -> ${newValue}`);
162
+ }
163
+
164
+ /**
165
+ * 格式化字段列表为可读字符串
166
+ *
167
+ * @param fields - 字段名数组
168
+ * @returns 格式化的字符串(逗号分隔)
169
+ */
170
+ export function formatFieldList(fields: string[]): string {
171
+ return fields.map((f) => quoteIdentifier(f)).join(', ');
172
+ }