befly 2.3.3 → 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.
- package/apis/health/info.ts +64 -0
- package/apis/tool/tokenCheck.ts +51 -0
- package/bin/befly.ts +202 -0
- package/checks/conflict.ts +408 -0
- package/checks/{table.js → table.ts} +139 -61
- package/config/env.ts +218 -0
- package/config/reserved.ts +96 -0
- package/main.ts +101 -0
- package/package.json +44 -8
- package/plugins/{db.js → db.ts} +24 -11
- package/plugins/logger.ts +28 -0
- package/plugins/redis.ts +51 -0
- package/plugins/tool.ts +34 -0
- package/scripts/syncDb/apply.ts +171 -0
- package/scripts/syncDb/constants.ts +70 -0
- package/scripts/syncDb/ddl.ts +182 -0
- package/scripts/syncDb/helpers.ts +172 -0
- package/scripts/syncDb/index.ts +215 -0
- package/scripts/syncDb/schema.ts +199 -0
- package/scripts/syncDb/sqlite.ts +50 -0
- package/scripts/syncDb/state.ts +104 -0
- package/scripts/syncDb/table.ts +204 -0
- package/scripts/syncDb/tableCreate.ts +142 -0
- package/scripts/syncDb/tests/constants.test.ts +104 -0
- package/scripts/syncDb/tests/ddl.test.ts +134 -0
- package/scripts/syncDb/tests/helpers.test.ts +70 -0
- package/scripts/syncDb/types.ts +92 -0
- package/scripts/syncDb/version.ts +73 -0
- package/scripts/syncDb.ts +9 -0
- package/scripts/{syncDev.js → syncDev.ts} +41 -25
- package/system.ts +149 -0
- package/tables/_common.json +21 -0
- package/tables/admin.json +10 -0
- package/tsconfig.json +58 -0
- package/types/api.d.ts +246 -0
- package/types/befly.d.ts +234 -0
- package/types/common.d.ts +215 -0
- package/types/context.ts +167 -0
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +278 -0
- package/types/index.d.ts +16 -0
- package/types/index.ts +459 -0
- package/types/jwt.d.ts +99 -0
- package/types/logger.d.ts +43 -0
- package/types/plugin.d.ts +109 -0
- package/types/redis.d.ts +44 -0
- package/types/tool.d.ts +67 -0
- package/types/validator.d.ts +45 -0
- package/utils/addonHelper.ts +60 -0
- package/utils/api.ts +23 -0
- package/utils/{colors.js → colors.ts} +79 -21
- package/utils/crypto.ts +308 -0
- package/utils/datetime.ts +51 -0
- package/utils/dbHelper.ts +142 -0
- package/utils/errorHandler.ts +68 -0
- package/utils/index.ts +46 -0
- package/utils/jwt.ts +493 -0
- package/utils/logger.ts +284 -0
- package/utils/objectHelper.ts +68 -0
- package/utils/pluginHelper.ts +62 -0
- package/utils/redisHelper.ts +338 -0
- package/utils/response.ts +38 -0
- package/utils/{sqlBuilder.js → sqlBuilder.ts} +233 -97
- package/utils/sqlHelper.ts +447 -0
- package/utils/tableHelper.ts +167 -0
- package/utils/tool.ts +230 -0
- package/utils/typeHelper.ts +101 -0
- package/utils/validate.ts +451 -0
- package/utils/{xml.js → xml.ts} +100 -74
- package/.npmrc +0 -3
- package/.prettierignore +0 -2
- package/.prettierrc +0 -11
- package/apis/health/info.js +0 -49
- package/apis/tool/tokenCheck.js +0 -29
- package/bin/befly.js +0 -109
- package/config/env.js +0 -64
- package/main.js +0 -579
- package/plugins/logger.js +0 -14
- package/plugins/redis.js +0 -32
- package/plugins/tool.js +0 -8
- package/scripts/syncDb.js +0 -752
- package/system.js +0 -118
- package/tables/common.json +0 -16
- package/tables/tool.json +0 -6
- package/utils/api.js +0 -27
- package/utils/crypto.js +0 -260
- package/utils/index.js +0 -334
- package/utils/jwt.js +0 -387
- package/utils/logger.js +0 -143
- package/utils/redisHelper.js +0 -74
- package/utils/sqlManager.js +0 -471
- package/utils/tool.js +0 -31
- package/utils/validate.js +0 -226
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syncDb 主入口文件
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* - 协调所有模块,执行数据库表结构同步
|
|
6
|
+
* - 处理核心表、项目表、addon 表
|
|
7
|
+
* - 提供统计信息和错误处理
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { Logger } from '../../utils/logger.js';
|
|
12
|
+
import { Env } from '../../config/env.js';
|
|
13
|
+
import { createSqlClient, toSnakeTableName } from '../../utils/dbHelper.js';
|
|
14
|
+
import checkTable from '../../checks/table.js';
|
|
15
|
+
import { __dirtables, getProjectDir } from '../../system.js';
|
|
16
|
+
import { scanAddons, hasAddonDir, getAddonDir } from '../../utils/addonHelper.js';
|
|
17
|
+
|
|
18
|
+
// 导入模块化的功能
|
|
19
|
+
import { ensureDbVersion } from './version.js';
|
|
20
|
+
import { tableExists } from './schema.js';
|
|
21
|
+
import { createTable, modifyTable } from './table.js';
|
|
22
|
+
import { PerformanceTracker, ProgressLogger } from './state.js';
|
|
23
|
+
import type { SQL } from 'bun';
|
|
24
|
+
|
|
25
|
+
// 全局 SQL 客户端实例
|
|
26
|
+
let sql: SQL | null = null;
|
|
27
|
+
|
|
28
|
+
// 全局统计对象
|
|
29
|
+
const globalCount: Record<string, number> = {
|
|
30
|
+
processedTables: 0,
|
|
31
|
+
createdTables: 0,
|
|
32
|
+
modifiedTables: 0,
|
|
33
|
+
addFields: 0,
|
|
34
|
+
nameChanges: 0,
|
|
35
|
+
typeChanges: 0,
|
|
36
|
+
minChanges: 0,
|
|
37
|
+
maxChanges: 0,
|
|
38
|
+
defaultChanges: 0,
|
|
39
|
+
indexCreate: 0,
|
|
40
|
+
indexDrop: 0
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 主同步函数
|
|
45
|
+
*
|
|
46
|
+
* 流程:
|
|
47
|
+
* 1. 验证表定义文件
|
|
48
|
+
* 2. 建立数据库连接并检查版本
|
|
49
|
+
* 3. 扫描表定义文件(核心表、项目表、addon表)
|
|
50
|
+
* 4. 对比并应用表结构变更
|
|
51
|
+
* 5. 输出统计信息
|
|
52
|
+
*/
|
|
53
|
+
export const SyncDb = async (): Promise<void> => {
|
|
54
|
+
const perfTracker = new PerformanceTracker();
|
|
55
|
+
const progressLogger = new ProgressLogger();
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
Logger.info('开始数据库表结构同步...');
|
|
59
|
+
|
|
60
|
+
// 重置全局统计,避免多次调用累加
|
|
61
|
+
for (const k of Object.keys(globalCount)) {
|
|
62
|
+
if (typeof globalCount[k] === 'number') globalCount[k] = 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 阶段1:验证表定义文件
|
|
66
|
+
perfTracker.markPhase('validation');
|
|
67
|
+
if (!(await checkTable())) {
|
|
68
|
+
throw new Error('表定义验证失败');
|
|
69
|
+
}
|
|
70
|
+
Logger.info(`✓ 表定义验证完成,耗时: ${perfTracker.getPhaseTime('validation')}`);
|
|
71
|
+
|
|
72
|
+
// 阶段2:建立数据库连接并检查版本
|
|
73
|
+
perfTracker.markPhase('connection');
|
|
74
|
+
sql = await createSqlClient({ max: 1 });
|
|
75
|
+
await ensureDbVersion(sql);
|
|
76
|
+
Logger.info(`✓ 数据库连接建立,耗时: ${perfTracker.getPhaseTime('connection')}`);
|
|
77
|
+
|
|
78
|
+
// 阶段3:扫描表定义文件
|
|
79
|
+
perfTracker.markPhase('scan');
|
|
80
|
+
const tablesGlob = new Bun.Glob('*.json');
|
|
81
|
+
const directories: Array<{ path: string; isCore: boolean; addonName?: string }> = [
|
|
82
|
+
{ path: __dirtables, isCore: true },
|
|
83
|
+
{ path: getProjectDir('tables'), isCore: false }
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// 添加所有 addon 的 tables 目录
|
|
87
|
+
const addons = scanAddons();
|
|
88
|
+
for (const addon of addons) {
|
|
89
|
+
if (hasAddonDir(addon, 'tables')) {
|
|
90
|
+
directories.push({
|
|
91
|
+
path: getAddonDir(addon, 'tables'),
|
|
92
|
+
isCore: false,
|
|
93
|
+
addonName: addon
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 统计表文件总数
|
|
99
|
+
let totalTables = 0;
|
|
100
|
+
for (const dirConfig of directories) {
|
|
101
|
+
for await (const file of tablesGlob.scan({
|
|
102
|
+
cwd: dirConfig.path,
|
|
103
|
+
absolute: true,
|
|
104
|
+
onlyFiles: true
|
|
105
|
+
})) {
|
|
106
|
+
const fileName = path.basename(file, '.json');
|
|
107
|
+
if (!fileName.startsWith('_')) {
|
|
108
|
+
totalTables++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
perfTracker.finishPhase('scan');
|
|
113
|
+
Logger.info(`✓ 扫描完成,发现 ${totalTables} 个表定义文件,耗时: ${perfTracker.getPhaseTime('scan')}`);
|
|
114
|
+
|
|
115
|
+
// 阶段4:处理表文件
|
|
116
|
+
perfTracker.markPhase('process');
|
|
117
|
+
let processedCount = 0;
|
|
118
|
+
|
|
119
|
+
for (const dirConfig of directories) {
|
|
120
|
+
const { path: dir, isCore, addonName } = dirConfig;
|
|
121
|
+
const dirType = isCore ? '内核' : addonName ? `组件[${addonName}]` : '项目';
|
|
122
|
+
|
|
123
|
+
for await (const file of tablesGlob.scan({
|
|
124
|
+
cwd: dir,
|
|
125
|
+
absolute: true,
|
|
126
|
+
onlyFiles: true
|
|
127
|
+
})) {
|
|
128
|
+
const fileName = path.basename(file, '.json');
|
|
129
|
+
|
|
130
|
+
// 跳过以下划线开头的文件(这些是公共字段规则,不是表定义)
|
|
131
|
+
if (fileName.startsWith('_')) {
|
|
132
|
+
Logger.info(`跳过非表定义文件: ${fileName}.json`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 确定表名前缀:
|
|
137
|
+
// - 核心表:sys_ 前缀
|
|
138
|
+
// - addon 表:{addonName}_ 前缀
|
|
139
|
+
// - 项目表:无前缀
|
|
140
|
+
let tableName = toSnakeTableName(fileName);
|
|
141
|
+
if (isCore) {
|
|
142
|
+
tableName = `sys_${tableName}`;
|
|
143
|
+
} else if (addonName) {
|
|
144
|
+
tableName = `${addonName}_${tableName}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
processedCount++;
|
|
148
|
+
progressLogger.logTableProgress(processedCount, totalTables, tableName);
|
|
149
|
+
Logger.info(` 类型: ${dirType}`);
|
|
150
|
+
|
|
151
|
+
const tableDefinition = await Bun.file(file).json();
|
|
152
|
+
const existsTable = await tableExists(sql!, tableName);
|
|
153
|
+
|
|
154
|
+
if (existsTable) {
|
|
155
|
+
await modifyTable(sql!, tableName, tableDefinition, globalCount);
|
|
156
|
+
} else {
|
|
157
|
+
await createTable(sql!, tableName, tableDefinition);
|
|
158
|
+
globalCount.createdTables++;
|
|
159
|
+
}
|
|
160
|
+
globalCount.processedTables++;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
perfTracker.finishPhase('process');
|
|
165
|
+
Logger.info(`✓ 表处理完成,耗时: ${perfTracker.getPhaseTime('process')}`);
|
|
166
|
+
|
|
167
|
+
// 阶段5:显示统计信息
|
|
168
|
+
Logger.info('\n=== 同步统计信息 ===');
|
|
169
|
+
Logger.info(`总耗时: ${perfTracker.getTotalTime()}`);
|
|
170
|
+
Logger.info(`处理表总数: ${globalCount.processedTables}`);
|
|
171
|
+
Logger.info(`创建表: ${globalCount.createdTables}`);
|
|
172
|
+
Logger.info(`修改表: ${globalCount.modifiedTables}`);
|
|
173
|
+
Logger.info(`字段新增: ${globalCount.addFields}`);
|
|
174
|
+
Logger.info(`字段名称变更: ${globalCount.nameChanges}`);
|
|
175
|
+
Logger.info(`字段类型变更: ${globalCount.typeChanges}`);
|
|
176
|
+
Logger.info(`字段最小值变更: ${globalCount.minChanges}`);
|
|
177
|
+
Logger.info(`字段最大值变更: ${globalCount.maxChanges}`);
|
|
178
|
+
Logger.info(`字段默认值变更: ${globalCount.defaultChanges}`);
|
|
179
|
+
Logger.info(`索引新增: ${globalCount.indexCreate}`);
|
|
180
|
+
Logger.info(`索引删除: ${globalCount.indexDrop}`);
|
|
181
|
+
|
|
182
|
+
if (globalCount.processedTables === 0) {
|
|
183
|
+
Logger.warn('没有找到任何表定义文件');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 输出性能统计
|
|
187
|
+
perfTracker.logStats();
|
|
188
|
+
} catch (error: any) {
|
|
189
|
+
Logger.error(`数据库同步失败: ${error.message}`);
|
|
190
|
+
Logger.error(`错误详情: ${error.stack}`);
|
|
191
|
+
if (error.code) {
|
|
192
|
+
Logger.error(`错误代码: ${error.code}`);
|
|
193
|
+
}
|
|
194
|
+
if (error.errno) {
|
|
195
|
+
Logger.error(`错误编号: ${error.errno}`);
|
|
196
|
+
}
|
|
197
|
+
process.exit(1);
|
|
198
|
+
} finally {
|
|
199
|
+
if (sql) {
|
|
200
|
+
try {
|
|
201
|
+
await sql.close();
|
|
202
|
+
} catch (error: any) {
|
|
203
|
+
Logger.warn('关闭数据库连接时出错:', error.message);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// 如果直接运行此脚本(Bun 支持 import.meta.main)
|
|
210
|
+
if (import.meta.main) {
|
|
211
|
+
SyncDb().catch((error) => {
|
|
212
|
+
console.error('❌ 数据库同步失败:', error);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
});
|
|
215
|
+
}
|