befly 3.0.0 → 3.1.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.
- package/checks/conflict.ts +35 -114
- package/checks/table.ts +31 -63
- package/config/env.ts +3 -3
- package/config/fields.ts +55 -0
- package/config/regexAliases.ts +51 -0
- package/config/reserved.ts +1 -1
- package/main.ts +17 -71
- package/package.json +7 -28
- package/plugins/db.ts +11 -10
- package/plugins/redis.ts +5 -9
- package/scripts/syncDb/apply.ts +3 -3
- package/scripts/syncDb/constants.ts +2 -1
- package/scripts/syncDb/ddl.ts +15 -8
- package/scripts/syncDb/helpers.ts +3 -2
- package/scripts/syncDb/index.ts +23 -35
- package/scripts/syncDb/state.ts +8 -6
- package/scripts/syncDb/table.ts +32 -22
- package/scripts/syncDb/tableCreate.ts +9 -3
- package/scripts/syncDb/tests/constants.test.ts +2 -1
- package/scripts/syncDb.ts +10 -9
- package/types/addon.d.ts +53 -0
- package/types/api.d.ts +17 -14
- package/types/befly.d.ts +2 -6
- package/types/context.d.ts +7 -0
- package/types/database.d.ts +9 -14
- package/types/index.d.ts +442 -8
- package/types/index.ts +35 -56
- package/types/redis.d.ts +2 -0
- package/types/validator.d.ts +0 -2
- package/types/validator.ts +43 -0
- package/utils/colors.ts +117 -37
- package/utils/database.ts +348 -0
- package/utils/dbHelper.ts +687 -116
- package/utils/helper.ts +812 -0
- package/utils/index.ts +10 -23
- package/utils/logger.ts +78 -171
- package/utils/redisHelper.ts +135 -152
- package/{types/context.ts → utils/requestContext.ts} +3 -3
- package/utils/sqlBuilder.ts +142 -165
- package/utils/validate.ts +51 -9
- package/apis/health/info.ts +0 -64
- package/apis/tool/tokenCheck.ts +0 -51
- package/bin/befly.ts +0 -202
- package/bunfig.toml +0 -3
- package/plugins/tool.ts +0 -34
- package/scripts/syncDev.ts +0 -112
- package/system.ts +0 -149
- package/tables/_common.json +0 -21
- package/tables/admin.json +0 -10
- package/utils/addonHelper.ts +0 -60
- package/utils/api.ts +0 -23
- package/utils/datetime.ts +0 -51
- package/utils/errorHandler.ts +0 -68
- package/utils/objectHelper.ts +0 -68
- package/utils/pluginHelper.ts +0 -62
- package/utils/response.ts +0 -38
- package/utils/sqlHelper.ts +0 -447
- package/utils/tableHelper.ts +0 -167
- package/utils/tool.ts +0 -230
- package/utils/typeHelper.ts +0 -101
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 内置正则表达式别名
|
|
3
|
+
*
|
|
4
|
+
* 使用方式:在字段定义的 regex 位置使用 @别名 格式
|
|
5
|
+
* 例如:'字段名|array_text|null|null|null|0|@number'
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const RegexAliases = {
|
|
9
|
+
// 数字类型
|
|
10
|
+
number: '^\\d+$', // 纯数字
|
|
11
|
+
integer: '^-?\\d+$', // 整数(含负数)
|
|
12
|
+
float: '^-?\\d+(\\.\\d+)?$', // 浮点数
|
|
13
|
+
positive: '^[1-9]\\d*$', // 正整数(不含0)
|
|
14
|
+
|
|
15
|
+
// 字符串类型
|
|
16
|
+
word: '^[a-zA-Z]+$', // 纯字母
|
|
17
|
+
alphanumeric: '^[a-zA-Z0-9]+$', // 字母+数字
|
|
18
|
+
alphanumeric_: '^[a-zA-Z0-9_]+$', // 字母+数字+下划线
|
|
19
|
+
lowercase: '^[a-z]+$', // 小写字母
|
|
20
|
+
uppercase: '^[A-Z]+$', // 大写字母
|
|
21
|
+
|
|
22
|
+
// 中文
|
|
23
|
+
chinese: '^[\\u4e00-\\u9fa5]+$', // 纯中文
|
|
24
|
+
chinese_word: '^[\\u4e00-\\u9fa5a-zA-Z]+$', // 中文+字母
|
|
25
|
+
|
|
26
|
+
// 常用格式
|
|
27
|
+
email: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$', // 邮箱
|
|
28
|
+
phone: '^1[3-9]\\d{9}$', // 中国手机号
|
|
29
|
+
url: '^https?://', // URL
|
|
30
|
+
ip: '^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$', // IPv4
|
|
31
|
+
|
|
32
|
+
// 特殊格式
|
|
33
|
+
uuid: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', // UUID
|
|
34
|
+
hex: '^[0-9a-fA-F]+$', // 十六进制
|
|
35
|
+
base64: '^[A-Za-z0-9+/=]+$', // Base64
|
|
36
|
+
|
|
37
|
+
// 日期时间
|
|
38
|
+
date: '^\\d{4}-\\d{2}-\\d{2}$', // YYYY-MM-DD
|
|
39
|
+
time: '^\\d{2}:\\d{2}:\\d{2}$', // HH:MM:SS
|
|
40
|
+
datetime: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}', // ISO 8601
|
|
41
|
+
|
|
42
|
+
// 代码相关
|
|
43
|
+
variable: '^[a-zA-Z_][a-zA-Z0-9_]*$', // 变量名
|
|
44
|
+
constant: '^[A-Z][A-Z0-9_]*$', // 常量名(大写)
|
|
45
|
+
|
|
46
|
+
// 空值
|
|
47
|
+
empty: '^$', // 空字符串
|
|
48
|
+
notempty: '.+' // 非空
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
export type RegexAliasName = keyof typeof RegexAliases;
|
package/config/reserved.ts
CHANGED
package/main.ts
CHANGED
|
@@ -1,101 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Befly 框架主入口文件
|
|
3
|
-
*
|
|
2
|
+
* Befly 框架主入口文件
|
|
3
|
+
* 提供简洁的框架接口,核心逻辑已提取到 lifecycle 层
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Env } from './config/env.js';
|
|
7
|
-
import {
|
|
8
|
-
import { Yes, No } from './utils/index.js';
|
|
7
|
+
import { Fields } from './config/fields.js';
|
|
8
|
+
import { Yes, No, cleanData } from './utils/index.js';
|
|
9
9
|
import { Logger } from './utils/logger.js';
|
|
10
10
|
import { Jwt } from './utils/jwt.js';
|
|
11
11
|
import { Validator } from './utils/validate.js';
|
|
12
12
|
import { Crypto2 } from './utils/crypto.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { Bootstrap } from './lifecycle/bootstrap.js';
|
|
13
|
+
import { DbHelper } from './utils/dbHelper.js';
|
|
14
|
+
import { createSqlClient, getRedis, getSql, getDbHelper, initDatabase, closeDatabase } from './utils/database.js';
|
|
15
|
+
import { RedisHelper } from './utils/redisHelper.js';
|
|
16
|
+
import { Lifecycle } from './lifecycle/lifecycle.js';
|
|
18
17
|
|
|
19
18
|
import type { Server } from 'bun';
|
|
20
|
-
import type {
|
|
21
|
-
import type { ApiRoute, ApiHandler } from './types/api.js';
|
|
22
|
-
import type { BeflyContext, BeflyOptions, RequestContext } from './types/befly.js';
|
|
19
|
+
import type { BeflyContext, BeflyOptions } from './types/befly.js';
|
|
23
20
|
|
|
24
21
|
/**
|
|
25
22
|
* Befly 框架核心类
|
|
23
|
+
* 职责:管理应用上下文和生命周期
|
|
26
24
|
*/
|
|
27
25
|
export class Befly {
|
|
28
|
-
/**
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
/** 插件列表 */
|
|
32
|
-
private pluginLists: Plugin[];
|
|
26
|
+
/** 生命周期管理器 */
|
|
27
|
+
private lifecycle: Lifecycle;
|
|
33
28
|
|
|
34
29
|
/** 应用上下文 */
|
|
35
30
|
public appContext: BeflyContext;
|
|
36
31
|
|
|
37
|
-
/** 应用配置选项 */
|
|
38
|
-
private appOptions: BeflyOptions;
|
|
39
|
-
|
|
40
|
-
// 原构造函数被替换
|
|
41
32
|
constructor(options: BeflyOptions = {}) {
|
|
42
|
-
this.
|
|
43
|
-
this.pluginLists = [];
|
|
33
|
+
this.lifecycle = new Lifecycle(options);
|
|
44
34
|
this.appContext = {};
|
|
45
|
-
this.appOptions = options;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* 系统检查(已提取到 lifecycle/checker.ts)
|
|
50
|
-
*/
|
|
51
|
-
async initCheck(): Promise<void> {
|
|
52
|
-
await Checker.run();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 加载插件(已提取到 lifecycle/loader.ts)
|
|
57
|
-
*/
|
|
58
|
-
async loadPlugins(): Promise<void> {
|
|
59
|
-
await Loader.loadPlugins(this);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 加载API路由(已提取到 lifecycle/loader.ts)
|
|
64
|
-
*/
|
|
65
|
-
async loadApis(dirName: string, options?: { isAddon?: boolean; addonName?: string }): Promise<void> {
|
|
66
|
-
await Loader.loadApis(dirName, this.apiRoutes, options);
|
|
67
35
|
}
|
|
68
36
|
|
|
69
37
|
/**
|
|
70
38
|
* 启动服务器
|
|
39
|
+
* @param callback - 启动完成后的回调函数
|
|
71
40
|
*/
|
|
72
41
|
async listen(callback?: (server: Server) => void): Promise<void> {
|
|
73
|
-
|
|
74
|
-
Logger.info('开始启动 Befly 服务器...');
|
|
75
|
-
|
|
76
|
-
// 执行启动前的检查和加载
|
|
77
|
-
await this.initCheck();
|
|
78
|
-
await this.loadPlugins();
|
|
79
|
-
await this.loadApis('core');
|
|
80
|
-
|
|
81
|
-
// 加载 addon APIs
|
|
82
|
-
const addons = scanAddons();
|
|
83
|
-
for (const addon of addons) {
|
|
84
|
-
if (hasAddonDir(addon, 'apis')) {
|
|
85
|
-
await this.loadApis(addon, { isAddon: true, addonName: addon });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
await this.loadApis('app');
|
|
90
|
-
|
|
91
|
-
const totalStartupTime = calcPerfTime(serverStartTime);
|
|
92
|
-
Logger.info(`服务器启动准备完成,总耗时: ${totalStartupTime}`);
|
|
93
|
-
|
|
94
|
-
// 启动HTTP服务器(路由处理逻辑已提取到 router/*.ts)
|
|
95
|
-
await Bootstrap.start(this, callback);
|
|
42
|
+
await this.lifecycle.start(this.appContext, callback);
|
|
96
43
|
}
|
|
97
44
|
}
|
|
98
45
|
|
|
99
|
-
//
|
|
100
|
-
export { Env,
|
|
101
|
-
export type { ApiOptions, FieldRules } from './types/api.js';
|
|
46
|
+
// 核心类和工具导出(只导出运行时代码,不导出类型)
|
|
47
|
+
export { Env, Fields, Jwt, Validator, Crypto2, Logger, Yes, No, cleanData, DbHelper, createSqlClient, RedisHelper, getRedis, getSql, getDbHelper, initDatabase, closeDatabase };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -9,37 +9,19 @@
|
|
|
9
9
|
"registry": "https://registry.npmjs.org"
|
|
10
10
|
},
|
|
11
11
|
"main": "main.ts",
|
|
12
|
-
"types": "
|
|
12
|
+
"types": "./types/index.d.ts",
|
|
13
13
|
"exports": {
|
|
14
14
|
".": {
|
|
15
|
-
"types": "./main.ts",
|
|
16
15
|
"default": "./main.ts"
|
|
17
16
|
},
|
|
18
17
|
"./types": {
|
|
19
|
-
"types": "./types/index.ts",
|
|
20
|
-
"default": "./types/index.ts"
|
|
21
|
-
},
|
|
22
|
-
"./types/*": {
|
|
23
|
-
"types": "./types/*.ts",
|
|
24
|
-
"default": "./types/*.ts"
|
|
25
|
-
},
|
|
26
|
-
"./utils": {
|
|
27
|
-
"types": "./utils/index.ts",
|
|
28
|
-
"default": "./utils/index.ts"
|
|
29
|
-
},
|
|
30
|
-
"./utils/*": {
|
|
31
|
-
"types": "./utils/*.ts",
|
|
32
|
-
"default": "./utils/*.ts"
|
|
33
|
-
},
|
|
34
|
-
"./config": {
|
|
35
|
-
"types": "./config/env.ts",
|
|
36
|
-
"default": "./config/env.ts"
|
|
18
|
+
"types": "./types/index.d.ts",
|
|
19
|
+
"default": "./types/index.d.ts"
|
|
37
20
|
}
|
|
38
21
|
},
|
|
39
|
-
"
|
|
40
|
-
"
|
|
22
|
+
"scripts": {
|
|
23
|
+
"bundler": "bun build ./main.ts --outfile ./main.single.ts --minify --target bun"
|
|
41
24
|
},
|
|
42
|
-
"scripts": {},
|
|
43
25
|
"keywords": [
|
|
44
26
|
"bun",
|
|
45
27
|
"api",
|
|
@@ -56,7 +38,6 @@
|
|
|
56
38
|
"homepage": "https://chensuiyi.me",
|
|
57
39
|
"license": "Apache-2.0",
|
|
58
40
|
"files": [
|
|
59
|
-
"bin/",
|
|
60
41
|
"apis/",
|
|
61
42
|
"checks/",
|
|
62
43
|
"config/",
|
|
@@ -85,7 +66,5 @@
|
|
|
85
66
|
"engines": {
|
|
86
67
|
"bun": ">=1.3.0"
|
|
87
68
|
},
|
|
88
|
-
"
|
|
89
|
-
"@types/bun": "latest"
|
|
90
|
-
}
|
|
69
|
+
"gitHead": "ac46f8ed275518b2494b47323df5ed37feb6fb7d"
|
|
91
70
|
}
|
package/plugins/db.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import { Env } from '../config/env.js';
|
|
7
7
|
import { Logger } from '../utils/logger.js';
|
|
8
|
-
import { createSqlClient } from '../utils/
|
|
9
|
-
import {
|
|
8
|
+
import { createSqlClient } from '../utils/database.js';
|
|
9
|
+
import { DbHelper } from '../utils/dbHelper.js';
|
|
10
10
|
import type { Plugin } from '../types/plugin.js';
|
|
11
11
|
import type { BeflyContext } from '../types/befly.js';
|
|
12
12
|
|
|
@@ -17,16 +17,21 @@ const dbPlugin: Plugin = {
|
|
|
17
17
|
name: '_db',
|
|
18
18
|
after: ['_redis'],
|
|
19
19
|
|
|
20
|
-
async onInit(befly: BeflyContext): Promise<
|
|
20
|
+
async onInit(befly: BeflyContext): Promise<DbHelper | Record<string, never>> {
|
|
21
21
|
let sql: any = null;
|
|
22
22
|
|
|
23
23
|
try {
|
|
24
24
|
if (Env.DB_ENABLE === 1) {
|
|
25
25
|
// 创建 Bun SQL 客户端(内置连接池),并确保连接验证成功后再继续
|
|
26
|
-
|
|
26
|
+
// 从环境变量读取连接超时配置
|
|
27
|
+
const connectionTimeout = process.env.DB_CONNECTION_TIMEOUT ? parseInt(process.env.DB_CONNECTION_TIMEOUT) : 5000;
|
|
28
|
+
|
|
29
|
+
sql = await createSqlClient({
|
|
30
|
+
connectionTimeout
|
|
31
|
+
});
|
|
27
32
|
|
|
28
33
|
// 创建数据库管理器实例,直接传入 sql 对象
|
|
29
|
-
const dbManager = new
|
|
34
|
+
const dbManager = new DbHelper(befly, sql);
|
|
30
35
|
|
|
31
36
|
Logger.info('数据库插件初始化成功');
|
|
32
37
|
return dbManager;
|
|
@@ -35,11 +40,7 @@ const dbPlugin: Plugin = {
|
|
|
35
40
|
return {};
|
|
36
41
|
}
|
|
37
42
|
} catch (error: any) {
|
|
38
|
-
Logger.error(
|
|
39
|
-
msg: '数据库初始化失败',
|
|
40
|
-
message: error.message,
|
|
41
|
-
stack: error.stack
|
|
42
|
-
});
|
|
43
|
+
Logger.error('数据库初始化失败', error);
|
|
43
44
|
|
|
44
45
|
// 清理资源
|
|
45
46
|
if (sql) {
|
package/plugins/redis.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import { Env } from '../config/env.js';
|
|
7
7
|
import { Logger } from '../utils/logger.js';
|
|
8
|
-
import { RedisHelper
|
|
8
|
+
import { RedisHelper } from '../utils/redisHelper.js';
|
|
9
|
+
import { initRedisOnly } from '../utils/database.js';
|
|
9
10
|
import type { Plugin } from '../types/plugin.js';
|
|
10
11
|
import type { BeflyContext } from '../types/befly.js';
|
|
11
12
|
|
|
@@ -19,8 +20,8 @@ const redisPlugin: Plugin = {
|
|
|
19
20
|
async onInit(befly: BeflyContext): Promise<typeof RedisHelper | Record<string, never>> {
|
|
20
21
|
try {
|
|
21
22
|
if (Env.REDIS_ENABLE === 1) {
|
|
22
|
-
// 初始化 Redis
|
|
23
|
-
await
|
|
23
|
+
// 初始化 Redis 客户端(统一使用 database.ts 的连接管理)
|
|
24
|
+
await initRedisOnly();
|
|
24
25
|
|
|
25
26
|
Logger.info('Redis 插件初始化成功', {
|
|
26
27
|
host: Env.REDIS_HOST,
|
|
@@ -35,12 +36,7 @@ const redisPlugin: Plugin = {
|
|
|
35
36
|
return {};
|
|
36
37
|
}
|
|
37
38
|
} catch (error: any) {
|
|
38
|
-
Logger.error(
|
|
39
|
-
msg: 'Redis 初始化失败',
|
|
40
|
-
message: error.message,
|
|
41
|
-
code: error.code,
|
|
42
|
-
stack: error.stack
|
|
43
|
-
});
|
|
39
|
+
Logger.error('Redis 初始化失败', error);
|
|
44
40
|
|
|
45
41
|
// 插件内禁止直接退出进程,抛出异常交由主流程统一处理
|
|
46
42
|
throw error;
|
package/scripts/syncDb/apply.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { Logger } from '../../utils/logger.js';
|
|
10
|
-
import { parseRule } from '../../utils/
|
|
10
|
+
import { parseRule } from '../../utils/helper.js';
|
|
11
11
|
import { IS_MYSQL, IS_PG, IS_SQLITE, CHANGE_TYPE_LABELS, typeMapping } from './constants.js';
|
|
12
12
|
import { logFieldChange, resolveDefaultValue, isStringOrArrayType } from './helpers.js';
|
|
13
13
|
import { executeDDLSafely, buildIndexSQL } from './ddl.js';
|
|
@@ -151,8 +151,8 @@ export async function applyTablePlan(sql: SQL, tableName: string, fields: Record
|
|
|
151
151
|
Logger.info(`[索引变化] 删除索引 ${tableName}.${act.indexName} (${act.fieldName})`);
|
|
152
152
|
}
|
|
153
153
|
} catch (error: any) {
|
|
154
|
-
Logger.error(`${act.action === 'create' ? '创建' : '删除'}
|
|
155
|
-
Logger.
|
|
154
|
+
Logger.error(`${act.action === 'create' ? '创建' : '删除'}索引失败:`, error);
|
|
155
|
+
Logger.warn(`表名: ${tableName}, 索引名: ${act.indexName}, 字段: ${act.fieldName}`);
|
|
156
156
|
throw error;
|
|
157
157
|
}
|
|
158
158
|
}
|
|
@@ -66,5 +66,6 @@ export const typeMapping = {
|
|
|
66
66
|
number: IS_SQLITE ? 'INTEGER' : 'BIGINT',
|
|
67
67
|
string: IS_SQLITE ? 'TEXT' : IS_PG ? 'character varying' : 'VARCHAR',
|
|
68
68
|
text: IS_MYSQL ? 'MEDIUMTEXT' : 'TEXT',
|
|
69
|
-
|
|
69
|
+
array_string: IS_SQLITE ? 'TEXT' : IS_PG ? 'character varying' : 'VARCHAR',
|
|
70
|
+
array_text: IS_MYSQL ? 'MEDIUMTEXT' : 'TEXT'
|
|
70
71
|
};
|
package/scripts/syncDb/ddl.ts
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { Logger } from '../../utils/logger.js';
|
|
12
|
-
import { parseRule } from '../../utils/
|
|
13
|
-
import {
|
|
12
|
+
import { parseRule, toSnakeCase } from '../../utils/helper.js';
|
|
13
|
+
import type { ParsedFieldRule, AnyObject } from '../../types/common.js';
|
|
14
|
+
import { IS_MYSQL, IS_PG, typeMapping } from './constants.js';
|
|
14
15
|
import { quoteIdentifier, resolveDefaultValue, generateDefaultSql, getSqlType, escapeComment } from './helpers.js';
|
|
15
16
|
import type { SQL } from 'bun';
|
|
16
17
|
|
|
@@ -78,6 +79,9 @@ export function buildBusinessColumnDefs(fields: Record<string, string>): string[
|
|
|
78
79
|
const colDefs: string[] = [];
|
|
79
80
|
|
|
80
81
|
for (const [fieldKey, fieldRule] of Object.entries(fields)) {
|
|
82
|
+
// 转换字段名为下划线格式
|
|
83
|
+
const dbFieldName = toSnakeCase(fieldKey);
|
|
84
|
+
|
|
81
85
|
const parsed = parseRule(fieldRule);
|
|
82
86
|
const { name: fieldName, type: fieldType, max: fieldMax, default: fieldDefault } = parsed;
|
|
83
87
|
const sqlType = getSqlType(fieldType, fieldMax);
|
|
@@ -87,9 +91,9 @@ export function buildBusinessColumnDefs(fields: Record<string, string>): string[
|
|
|
87
91
|
const defaultSql = generateDefaultSql(actualDefault, fieldType);
|
|
88
92
|
|
|
89
93
|
if (IS_MYSQL) {
|
|
90
|
-
colDefs.push(`\`${
|
|
94
|
+
colDefs.push(`\`${dbFieldName}\` ${sqlType} NOT NULL${defaultSql} COMMENT "${escapeComment(fieldName)}"`);
|
|
91
95
|
} else {
|
|
92
|
-
colDefs.push(`"${
|
|
96
|
+
colDefs.push(`"${dbFieldName}" ${sqlType} NOT NULL${defaultSql}`);
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -105,6 +109,9 @@ export function buildBusinessColumnDefs(fields: Record<string, string>): string[
|
|
|
105
109
|
* @returns DDL 子句
|
|
106
110
|
*/
|
|
107
111
|
export function generateDDLClause(fieldKey: string, fieldRule: string, isAdd: boolean = false): string {
|
|
112
|
+
// 转换字段名为下划线格式
|
|
113
|
+
const dbFieldName = toSnakeCase(fieldKey);
|
|
114
|
+
|
|
108
115
|
const parsed = parseRule(fieldRule);
|
|
109
116
|
const { name: fieldName, type: fieldType, max: fieldMax, default: fieldDefault } = parsed;
|
|
110
117
|
const sqlType = getSqlType(fieldType, fieldMax);
|
|
@@ -114,15 +121,15 @@ export function generateDDLClause(fieldKey: string, fieldRule: string, isAdd: bo
|
|
|
114
121
|
const defaultSql = generateDefaultSql(actualDefault, fieldType);
|
|
115
122
|
|
|
116
123
|
if (IS_MYSQL) {
|
|
117
|
-
return `${isAdd ? 'ADD COLUMN' : 'MODIFY COLUMN'} \`${
|
|
124
|
+
return `${isAdd ? 'ADD COLUMN' : 'MODIFY COLUMN'} \`${dbFieldName}\` ${sqlType} NOT NULL${defaultSql} COMMENT "${escapeComment(fieldName)}"`;
|
|
118
125
|
}
|
|
119
126
|
if (IS_PG) {
|
|
120
|
-
if (isAdd) return `ADD COLUMN IF NOT EXISTS "${
|
|
127
|
+
if (isAdd) return `ADD COLUMN IF NOT EXISTS "${dbFieldName}" ${sqlType} NOT NULL${defaultSql}`;
|
|
121
128
|
// PG 修改:类型与非空可分条执行,生成 TYPE 改变;非空另由上层统一控制
|
|
122
|
-
return `ALTER COLUMN "${
|
|
129
|
+
return `ALTER COLUMN "${dbFieldName}" TYPE ${sqlType}`;
|
|
123
130
|
}
|
|
124
131
|
// SQLite 仅支持 ADD COLUMN(>=3.50.0:支持 IF NOT EXISTS)
|
|
125
|
-
if (isAdd) return `ADD COLUMN IF NOT EXISTS "${
|
|
132
|
+
if (isAdd) return `ADD COLUMN IF NOT EXISTS "${dbFieldName}" ${sqlType} NOT NULL${defaultSql}`;
|
|
126
133
|
return '';
|
|
127
134
|
}
|
|
128
135
|
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { IS_MYSQL, IS_PG, typeMapping } from './constants.js';
|
|
12
|
-
import { isType } from '../../utils/
|
|
12
|
+
import { isType } from '../../utils/helper.js';
|
|
13
|
+
import { Logger } from '../../utils/logger.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* 根据数据库类型引用标识符
|
|
@@ -158,7 +159,7 @@ export function escapeComment(str: string): string {
|
|
|
158
159
|
* @param changeLabel - 变更类型的中文标签
|
|
159
160
|
*/
|
|
160
161
|
export function logFieldChange(tableName: string, fieldName: string, changeType: string, oldValue: any, newValue: any, changeLabel: string): void {
|
|
161
|
-
|
|
162
|
+
Logger.info(` 修改表 ${tableName} 的字段 ${fieldName} 的${changeLabel}: ${oldValue} -> ${newValue}`);
|
|
162
163
|
}
|
|
163
164
|
|
|
164
165
|
/**
|
package/scripts/syncDb/index.ts
CHANGED
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { Logger } from '../../utils/logger.js';
|
|
12
12
|
import { Env } from '../../config/env.js';
|
|
13
|
-
import { createSqlClient
|
|
13
|
+
import { createSqlClient } from '../../utils/database.js';
|
|
14
|
+
import { toSnakeCase } from '../../utils/helper.js';
|
|
14
15
|
import checkTable from '../../checks/table.js';
|
|
15
|
-
import {
|
|
16
|
-
import { scanAddons,
|
|
16
|
+
import { paths } from '../../paths.js';
|
|
17
|
+
import { scanAddons, getAddonDir, addonDirExists } from '../../utils/helper.js';
|
|
17
18
|
|
|
18
19
|
// 导入模块化的功能
|
|
19
20
|
import { ensureDbVersion } from './version.js';
|
|
@@ -63,30 +64,27 @@ export const SyncDb = async (): Promise<void> => {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// 阶段1:验证表定义文件
|
|
66
|
-
perfTracker.markPhase('
|
|
67
|
+
perfTracker.markPhase('表定义验证');
|
|
67
68
|
if (!(await checkTable())) {
|
|
68
69
|
throw new Error('表定义验证失败');
|
|
69
70
|
}
|
|
70
|
-
Logger.info(`✓ 表定义验证完成,耗时: ${perfTracker.getPhaseTime('
|
|
71
|
+
Logger.info(`✓ 表定义验证完成,耗时: ${perfTracker.getPhaseTime('表定义验证')}`);
|
|
71
72
|
|
|
72
73
|
// 阶段2:建立数据库连接并检查版本
|
|
73
|
-
perfTracker.markPhase('
|
|
74
|
+
perfTracker.markPhase('数据库连接');
|
|
74
75
|
sql = await createSqlClient({ max: 1 });
|
|
75
76
|
await ensureDbVersion(sql);
|
|
76
|
-
Logger.info(`✓ 数据库连接建立,耗时: ${perfTracker.getPhaseTime('
|
|
77
|
+
Logger.info(`✓ 数据库连接建立,耗时: ${perfTracker.getPhaseTime('数据库连接')}`);
|
|
77
78
|
|
|
78
79
|
// 阶段3:扫描表定义文件
|
|
79
|
-
perfTracker.markPhase('
|
|
80
|
+
perfTracker.markPhase('扫描表文件');
|
|
80
81
|
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
|
-
];
|
|
82
|
+
const directories: Array<{ path: string; isCore: boolean; addonName?: string }> = [{ path: paths.projectTableDir, isCore: false }];
|
|
85
83
|
|
|
86
84
|
// 添加所有 addon 的 tables 目录
|
|
87
85
|
const addons = scanAddons();
|
|
88
86
|
for (const addon of addons) {
|
|
89
|
-
if (
|
|
87
|
+
if (addonDirExists(addon, 'tables')) {
|
|
90
88
|
directories.push({
|
|
91
89
|
path: getAddonDir(addon, 'tables'),
|
|
92
90
|
isCore: false,
|
|
@@ -109,16 +107,16 @@ export const SyncDb = async (): Promise<void> => {
|
|
|
109
107
|
}
|
|
110
108
|
}
|
|
111
109
|
}
|
|
112
|
-
perfTracker.finishPhase('
|
|
113
|
-
Logger.info(`✓ 扫描完成,发现 ${totalTables} 个表定义文件,耗时: ${perfTracker.getPhaseTime('
|
|
110
|
+
perfTracker.finishPhase('扫描表文件');
|
|
111
|
+
Logger.info(`✓ 扫描完成,发现 ${totalTables} 个表定义文件,耗时: ${perfTracker.getPhaseTime('扫描表文件')}`);
|
|
114
112
|
|
|
115
113
|
// 阶段4:处理表文件
|
|
116
|
-
perfTracker.markPhase('
|
|
114
|
+
perfTracker.markPhase('同步处理');
|
|
117
115
|
let processedCount = 0;
|
|
118
116
|
|
|
119
117
|
for (const dirConfig of directories) {
|
|
120
118
|
const { path: dir, isCore, addonName } = dirConfig;
|
|
121
|
-
const dirType =
|
|
119
|
+
const dirType = addonName ? `组件${addonName}` : '项目';
|
|
122
120
|
|
|
123
121
|
for await (const file of tablesGlob.scan({
|
|
124
122
|
cwd: dir,
|
|
@@ -134,14 +132,11 @@ export const SyncDb = async (): Promise<void> => {
|
|
|
134
132
|
}
|
|
135
133
|
|
|
136
134
|
// 确定表名前缀:
|
|
137
|
-
// -
|
|
138
|
-
// - addon 表:{addonName}_ 前缀
|
|
135
|
+
// - addon 表:addon_{addonName}_ 前缀
|
|
139
136
|
// - 项目表:无前缀
|
|
140
|
-
let tableName =
|
|
141
|
-
if (
|
|
142
|
-
tableName = `
|
|
143
|
-
} else if (addonName) {
|
|
144
|
-
tableName = `${addonName}_${tableName}`;
|
|
137
|
+
let tableName = toSnakeCase(fileName);
|
|
138
|
+
if (addonName) {
|
|
139
|
+
tableName = `addon_${addonName}_${tableName}`;
|
|
145
140
|
}
|
|
146
141
|
|
|
147
142
|
processedCount++;
|
|
@@ -161,8 +156,8 @@ export const SyncDb = async (): Promise<void> => {
|
|
|
161
156
|
}
|
|
162
157
|
}
|
|
163
158
|
|
|
164
|
-
perfTracker.finishPhase('
|
|
165
|
-
Logger.info(`✓ 表处理完成,耗时: ${perfTracker.getPhaseTime('
|
|
159
|
+
perfTracker.finishPhase('同步处理');
|
|
160
|
+
Logger.info(`✓ 表处理完成,耗时: ${perfTracker.getPhaseTime('同步处理')}`);
|
|
166
161
|
|
|
167
162
|
// 阶段5:显示统计信息
|
|
168
163
|
Logger.info('\n=== 同步统计信息 ===');
|
|
@@ -186,14 +181,7 @@ export const SyncDb = async (): Promise<void> => {
|
|
|
186
181
|
// 输出性能统计
|
|
187
182
|
perfTracker.logStats();
|
|
188
183
|
} catch (error: any) {
|
|
189
|
-
Logger.error(
|
|
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
|
-
}
|
|
184
|
+
Logger.error(`数据库同步失败`, error);
|
|
197
185
|
process.exit(1);
|
|
198
186
|
} finally {
|
|
199
187
|
if (sql) {
|
|
@@ -209,7 +197,7 @@ export const SyncDb = async (): Promise<void> => {
|
|
|
209
197
|
// 如果直接运行此脚本(Bun 支持 import.meta.main)
|
|
210
198
|
if (import.meta.main) {
|
|
211
199
|
SyncDb().catch((error) => {
|
|
212
|
-
|
|
200
|
+
Logger.error('数据库同步失败', error);
|
|
213
201
|
process.exit(1);
|
|
214
202
|
});
|
|
215
203
|
}
|
package/scripts/syncDb/state.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* - 进度信息记录
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { Logger } from '../../utils/logger.js';
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* 阶段统计信息
|
|
11
13
|
*/
|
|
@@ -67,13 +69,13 @@ export class PerformanceTracker {
|
|
|
67
69
|
* 输出所有阶段统计
|
|
68
70
|
*/
|
|
69
71
|
logStats(): void {
|
|
70
|
-
|
|
72
|
+
Logger.info('\n⏱️ 性能统计:');
|
|
71
73
|
for (const [phase, stats] of this.phases) {
|
|
72
74
|
const duration = stats.duration || Date.now() - stats.startTime;
|
|
73
75
|
const timeStr = duration > 1000 ? `${(duration / 1000).toFixed(2)}s` : `${duration}ms`;
|
|
74
|
-
|
|
76
|
+
Logger.info(` ${phase}: ${timeStr}`);
|
|
75
77
|
}
|
|
76
|
-
|
|
78
|
+
Logger.info(` 总耗时: ${this.getTotalTime()}`);
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -85,20 +87,20 @@ export class ProgressLogger {
|
|
|
85
87
|
* 记录表处理进度
|
|
86
88
|
*/
|
|
87
89
|
logTableProgress(current: number, total: number, tableName: string): void {
|
|
88
|
-
|
|
90
|
+
Logger.info(`\n[${current}/${total}] 处理表: ${tableName}`);
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
/**
|
|
92
94
|
* 记录字段变更进度
|
|
93
95
|
*/
|
|
94
96
|
logFieldChangeProgress(current: number, total: number, fieldName: string, changeType: string): void {
|
|
95
|
-
|
|
97
|
+
Logger.info(` [${current}/${total}] 修改字段 ${fieldName} (${changeType})`);
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
/**
|
|
99
101
|
* 记录索引创建进度
|
|
100
102
|
*/
|
|
101
103
|
logIndexProgress(current: number, total: number, indexName: string): void {
|
|
102
|
-
|
|
104
|
+
Logger.info(` [${current}/${total}] 创建索引: ${indexName}`);
|
|
103
105
|
}
|
|
104
106
|
}
|