befly 3.9.38 → 3.9.40
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/README.md +37 -38
- package/befly.config.ts +62 -40
- package/checks/checkApi.ts +16 -16
- package/checks/checkApp.ts +19 -25
- package/checks/checkTable.ts +42 -42
- package/docs/README.md +42 -35
- package/docs/{api.md → api/api.md} +223 -231
- package/docs/cipher.md +71 -69
- package/docs/database.md +143 -141
- package/docs/{examples.md → guide/examples.md} +181 -181
- package/docs/guide/quickstart.md +331 -0
- package/docs/hooks/auth.md +38 -0
- package/docs/hooks/cors.md +28 -0
- package/docs/{hook.md → hooks/hook.md} +140 -57
- package/docs/hooks/parser.md +19 -0
- package/docs/hooks/rateLimit.md +47 -0
- package/docs/{redis.md → infra/redis.md} +84 -93
- package/docs/plugins/cipher.md +61 -0
- package/docs/plugins/database.md +128 -0
- package/docs/{plugin.md → plugins/plugin.md} +83 -81
- package/docs/quickstart.md +26 -26
- package/docs/{addon.md → reference/addon.md} +46 -46
- package/docs/{config.md → reference/config.md} +32 -80
- package/docs/{logger.md → reference/logger.md} +52 -52
- package/docs/{sync.md → reference/sync.md} +32 -35
- package/docs/{table.md → reference/table.md} +1 -1
- package/docs/{validator.md → reference/validator.md} +57 -57
- package/hooks/auth.ts +8 -4
- package/hooks/cors.ts +13 -13
- package/hooks/parser.ts +37 -17
- package/hooks/permission.ts +26 -14
- package/hooks/rateLimit.ts +276 -0
- package/hooks/validator.ts +8 -8
- package/lib/asyncContext.ts +43 -0
- package/lib/cacheHelper.ts +212 -77
- package/lib/cacheKeys.ts +38 -0
- package/lib/cipher.ts +30 -30
- package/lib/connect.ts +28 -28
- package/lib/dbHelper.ts +183 -102
- package/lib/jwt.ts +16 -16
- package/lib/logger.ts +610 -19
- package/lib/redisHelper.ts +185 -44
- package/lib/sqlBuilder.ts +90 -91
- package/lib/validator.ts +59 -39
- package/loader/loadApis.ts +48 -44
- package/loader/loadHooks.ts +40 -14
- package/loader/loadPlugins.ts +16 -17
- package/main.ts +57 -47
- package/package.json +47 -45
- package/paths.ts +15 -14
- package/plugins/cache.ts +5 -4
- package/plugins/cipher.ts +3 -3
- package/plugins/config.ts +2 -2
- package/plugins/db.ts +9 -9
- package/plugins/jwt.ts +3 -3
- package/plugins/logger.ts +8 -12
- package/plugins/redis.ts +8 -8
- package/plugins/tool.ts +6 -6
- package/router/api.ts +85 -56
- package/router/static.ts +12 -12
- package/sync/syncAll.ts +12 -12
- package/sync/syncApi.ts +55 -52
- package/sync/syncDb/apply.ts +20 -19
- package/sync/syncDb/constants.ts +25 -23
- package/sync/syncDb/ddl.ts +35 -36
- package/sync/syncDb/helpers.ts +6 -9
- package/sync/syncDb/schema.ts +10 -9
- package/sync/syncDb/sqlite.ts +7 -8
- package/sync/syncDb/table.ts +37 -35
- package/sync/syncDb/tableCreate.ts +21 -20
- package/sync/syncDb/types.ts +23 -20
- package/sync/syncDb/version.ts +10 -10
- package/sync/syncDb.ts +43 -36
- package/sync/syncDev.ts +74 -65
- package/sync/syncMenu.ts +190 -55
- package/tests/api-integration-array-number.test.ts +282 -0
- package/tests/befly-config-env.test.ts +78 -0
- package/tests/cacheHelper.test.ts +135 -104
- package/tests/cacheKeys.test.ts +41 -0
- package/tests/cipher.test.ts +90 -89
- package/tests/dbHelper-advanced.test.ts +140 -134
- package/tests/dbHelper-all-array-types.test.ts +316 -0
- package/tests/dbHelper-array-serialization.test.ts +258 -0
- package/tests/dbHelper-columns.test.ts +56 -55
- package/tests/dbHelper-execute.test.ts +45 -44
- package/tests/dbHelper-joins.test.ts +124 -119
- package/tests/fields-redis-cache.test.ts +29 -27
- package/tests/fields-validate.test.ts +38 -38
- package/tests/getClientIp.test.ts +54 -0
- package/tests/integration.test.ts +69 -67
- package/tests/jwt.test.ts +27 -26
- package/tests/logger.test.ts +267 -34
- package/tests/rateLimit-hook.test.ts +477 -0
- package/tests/redisHelper.test.ts +187 -188
- package/tests/redisKeys.test.ts +6 -73
- package/tests/scanConfig.test.ts +144 -0
- package/tests/sqlBuilder-advanced.test.ts +217 -215
- package/tests/sqlBuilder.test.ts +92 -91
- package/tests/sync-connection.test.ts +29 -29
- package/tests/syncDb-apply.test.ts +97 -96
- package/tests/syncDb-array-number.test.ts +160 -0
- package/tests/syncDb-constants.test.ts +48 -47
- package/tests/syncDb-ddl.test.ts +99 -98
- package/tests/syncDb-helpers.test.ts +29 -28
- package/tests/syncDb-schema.test.ts +61 -60
- package/tests/syncDb-types.test.ts +60 -59
- package/tests/syncMenu-paths.test.ts +68 -0
- package/tests/util.test.ts +42 -41
- package/tests/validator-array-number.test.ts +310 -0
- package/tests/validator-default.test.ts +373 -0
- package/tests/validator.test.ts +271 -266
- package/tsconfig.json +4 -5
- package/types/api.d.ts +7 -12
- package/types/befly.d.ts +60 -13
- package/types/cache.d.ts +8 -4
- package/types/common.d.ts +17 -9
- package/types/context.d.ts +2 -2
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +19 -19
- package/types/hook.d.ts +2 -2
- package/types/jwt.d.ts +118 -0
- package/types/logger.d.ts +30 -0
- package/types/plugin.d.ts +4 -4
- package/types/redis.d.ts +7 -3
- package/types/roleApisCache.ts +23 -0
- package/types/sync.d.ts +10 -10
- package/types/table.d.ts +50 -9
- package/types/validate.d.ts +69 -0
- package/utils/addonHelper.ts +90 -0
- package/utils/arrayKeysToCamel.ts +18 -0
- package/utils/calcPerfTime.ts +13 -0
- package/utils/configTypes.ts +3 -0
- package/utils/cors.ts +19 -0
- package/utils/fieldClear.ts +75 -0
- package/utils/genShortId.ts +12 -0
- package/utils/getClientIp.ts +45 -0
- package/utils/keysToCamel.ts +22 -0
- package/utils/keysToSnake.ts +22 -0
- package/utils/modules.ts +98 -0
- package/utils/pickFields.ts +19 -0
- package/utils/process.ts +56 -0
- package/utils/regex.ts +225 -0
- package/utils/response.ts +115 -0
- package/utils/route.ts +23 -0
- package/utils/scanConfig.ts +142 -0
- package/utils/scanFiles.ts +48 -0
- package/.prettierignore +0 -2
- package/.prettierrc +0 -12
- package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
- package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
- package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
- package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
- package/hooks/requestLogger.ts +0 -84
- package/types/index.ts +0 -24
- package/util.ts +0 -283
package/sync/syncDb/apply.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* syncDb 变更应用模块
|
|
3
3
|
*
|
|
4
4
|
* 包含:
|
|
@@ -6,14 +6,15 @@
|
|
|
6
6
|
* - 应用表结构变更计划
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import
|
|
9
|
+
import type { FieldChange, TablePlan, ColumnInfo } from "../../types/sync.js";
|
|
10
|
+
import type { FieldDefinition } from "../../types/validate.js";
|
|
11
|
+
import type { SQL } from "bun";
|
|
12
|
+
|
|
13
|
+
import { Logger } from "../../lib/logger.js";
|
|
14
|
+
import { isMySQL, isPG, isSQLite, IS_PLAN, getTypeMapping } from "./constants.js";
|
|
15
|
+
import { executeDDLSafely, buildIndexSQL } from "./ddl.js";
|
|
16
|
+
import { rebuildSqliteTable } from "./sqlite.js";
|
|
17
|
+
import { isStringOrArrayType, resolveDefaultValue } from "./types.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* 构建 ALTER TABLE SQL 语句
|
|
@@ -28,9 +29,9 @@ import type { FieldDefinition } from 'befly-shared/types';
|
|
|
28
29
|
*/
|
|
29
30
|
function buildAlterTableSQL(tableName: string, clauses: string[]): string {
|
|
30
31
|
if (isMySQL()) {
|
|
31
|
-
return `ALTER TABLE \`${tableName}\` ${clauses.join(
|
|
32
|
+
return `ALTER TABLE \`${tableName}\` ${clauses.join(", ")}, ALGORITHM=INSTANT, LOCK=NONE`;
|
|
32
33
|
}
|
|
33
|
-
return `ALTER TABLE "${tableName}" ${clauses.join(
|
|
34
|
+
return `ALTER TABLE "${tableName}" ${clauses.join(", ")}`;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -54,7 +55,7 @@ export function compareFieldDefinition(existingColumn: ColumnInfo, fieldDef: Fie
|
|
|
54
55
|
if (!isSQLite() && isStringOrArrayType(fieldDef.type)) {
|
|
55
56
|
if (existingColumn.max !== fieldDef.max) {
|
|
56
57
|
changes.push({
|
|
57
|
-
type:
|
|
58
|
+
type: "length",
|
|
58
59
|
current: existingColumn.max,
|
|
59
60
|
expected: fieldDef.max
|
|
60
61
|
});
|
|
@@ -63,10 +64,10 @@ export function compareFieldDefinition(existingColumn: ColumnInfo, fieldDef: Fie
|
|
|
63
64
|
|
|
64
65
|
// 检查注释变化(MySQL/PG 支持列注释,对比数据库 comment 与字段 name)
|
|
65
66
|
if (!isSQLite()) {
|
|
66
|
-
const currentComment = existingColumn.comment ||
|
|
67
|
+
const currentComment = existingColumn.comment || "";
|
|
67
68
|
if (currentComment !== fieldDef.name) {
|
|
68
69
|
changes.push({
|
|
69
|
-
type:
|
|
70
|
+
type: "comment",
|
|
70
71
|
current: currentComment,
|
|
71
72
|
expected: fieldDef.name
|
|
72
73
|
});
|
|
@@ -80,7 +81,7 @@ export function compareFieldDefinition(existingColumn: ColumnInfo, fieldDef: Fie
|
|
|
80
81
|
|
|
81
82
|
if (currentType !== expectedType) {
|
|
82
83
|
changes.push({
|
|
83
|
-
type:
|
|
84
|
+
type: "datatype",
|
|
84
85
|
current: currentType,
|
|
85
86
|
expected: expectedType
|
|
86
87
|
});
|
|
@@ -90,7 +91,7 @@ export function compareFieldDefinition(existingColumn: ColumnInfo, fieldDef: Fie
|
|
|
90
91
|
const expectedNullable = fieldDef.nullable;
|
|
91
92
|
if (existingColumn.nullable !== expectedNullable) {
|
|
92
93
|
changes.push({
|
|
93
|
-
type:
|
|
94
|
+
type: "nullable",
|
|
94
95
|
current: existingColumn.nullable,
|
|
95
96
|
expected: expectedNullable
|
|
96
97
|
});
|
|
@@ -102,7 +103,7 @@ export function compareFieldDefinition(existingColumn: ColumnInfo, fieldDef: Fie
|
|
|
102
103
|
// 检查默认值变化
|
|
103
104
|
if (String(existingColumn.defaultValue) !== String(expectedDefault)) {
|
|
104
105
|
changes.push({
|
|
105
|
-
type:
|
|
106
|
+
type: "default",
|
|
106
107
|
current: existingColumn.defaultValue,
|
|
107
108
|
expected: expectedDefault
|
|
108
109
|
});
|
|
@@ -169,13 +170,13 @@ export async function applyTablePlan(sql: SQL, tableName: string, fields: Record
|
|
|
169
170
|
} else {
|
|
170
171
|
try {
|
|
171
172
|
await sql.unsafe(stmt);
|
|
172
|
-
if (act.action ===
|
|
173
|
+
if (act.action === "create") {
|
|
173
174
|
Logger.debug(`[索引变化] 新建索引 ${tableName}.${act.indexName} (${act.fieldName})`);
|
|
174
175
|
} else {
|
|
175
176
|
Logger.debug(`[索引变化] 删除索引 ${tableName}.${act.indexName} (${act.fieldName})`);
|
|
176
177
|
}
|
|
177
178
|
} catch (error: any) {
|
|
178
|
-
Logger.error({ err: error, table: tableName, index: act.indexName, field: act.fieldName }, `${act.action ===
|
|
179
|
+
Logger.error({ err: error, table: tableName, index: act.indexName, field: act.fieldName }, `${act.action === "create" ? "创建" : "删除"}索引失败`);
|
|
179
180
|
throw error;
|
|
180
181
|
}
|
|
181
182
|
}
|
package/sync/syncDb/constants.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* syncDb 常量定义模块
|
|
3
3
|
*
|
|
4
4
|
* 包含:
|
|
@@ -14,25 +14,25 @@
|
|
|
14
14
|
export const DB_VERSION_REQUIREMENTS = {
|
|
15
15
|
MYSQL_MIN_MAJOR: 8,
|
|
16
16
|
POSTGRES_MIN_MAJOR: 17,
|
|
17
|
-
SQLITE_MIN_VERSION:
|
|
17
|
+
SQLITE_MIN_VERSION: "3.50.0",
|
|
18
18
|
SQLITE_MIN_VERSION_NUM: 35000 // 3 * 10000 + 50 * 100
|
|
19
19
|
} as const;
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* 需要创建索引的系统字段
|
|
23
23
|
*/
|
|
24
|
-
export const SYSTEM_INDEX_FIELDS = [
|
|
24
|
+
export const SYSTEM_INDEX_FIELDS = ["created_at", "updated_at", "state"] as const;
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* 字段变更类型的中文标签映射
|
|
28
28
|
*/
|
|
29
29
|
export const CHANGE_TYPE_LABELS = {
|
|
30
|
-
length:
|
|
31
|
-
datatype:
|
|
32
|
-
comment:
|
|
33
|
-
default:
|
|
34
|
-
nullable:
|
|
35
|
-
unique:
|
|
30
|
+
length: "长度",
|
|
31
|
+
datatype: "类型",
|
|
32
|
+
comment: "注释",
|
|
33
|
+
default: "默认值",
|
|
34
|
+
nullable: "可空约束",
|
|
35
|
+
unique: "唯一约束"
|
|
36
36
|
} as const;
|
|
37
37
|
|
|
38
38
|
/**
|
|
@@ -44,23 +44,23 @@ export const CHANGE_TYPE_LABELS = {
|
|
|
44
44
|
* - COLLATE: utf8mb4_0900_ai_ci(MySQL 8.0 推荐,不区分重音和大小写)
|
|
45
45
|
*/
|
|
46
46
|
export const MYSQL_TABLE_CONFIG = {
|
|
47
|
-
ENGINE:
|
|
48
|
-
CHARSET:
|
|
49
|
-
COLLATE:
|
|
47
|
+
ENGINE: "InnoDB",
|
|
48
|
+
CHARSET: "utf8mb4",
|
|
49
|
+
COLLATE: "utf8mb4_0900_ai_ci"
|
|
50
50
|
} as const;
|
|
51
51
|
|
|
52
52
|
// 是否为计划模式(仅输出 SQL 不执行)
|
|
53
|
-
export const IS_PLAN = process.argv.includes(
|
|
53
|
+
export const IS_PLAN = process.argv.includes("--plan");
|
|
54
54
|
|
|
55
55
|
// 数据库类型(运行时设置,默认 mysql)
|
|
56
|
-
let _dbType: string =
|
|
56
|
+
let _dbType: string = "mysql";
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* 设置数据库类型(由 syncDbCommand 调用)
|
|
60
60
|
* @param dbType - 数据库类型(mysql/postgresql/postgres/sqlite)
|
|
61
61
|
*/
|
|
62
62
|
export function setDbType(dbType: string): void {
|
|
63
|
-
_dbType = (dbType ||
|
|
63
|
+
_dbType = (dbType || "mysql").toLowerCase();
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
@@ -72,15 +72,15 @@ export function getDbType(): string {
|
|
|
72
72
|
|
|
73
73
|
// 数据库类型判断(getter 函数,运行时动态计算)
|
|
74
74
|
export function isMySQL(): boolean {
|
|
75
|
-
return _dbType ===
|
|
75
|
+
return _dbType === "mysql";
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export function isPG(): boolean {
|
|
79
|
-
return _dbType ===
|
|
79
|
+
return _dbType === "postgresql" || _dbType === "postgres";
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export function isSQLite(): boolean {
|
|
83
|
-
return _dbType ===
|
|
83
|
+
return _dbType === "sqlite";
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
// 兼容旧代码的静态别名(通过 getter 实现动态获取)
|
|
@@ -108,10 +108,12 @@ export function getTypeMapping(): Record<string, string> {
|
|
|
108
108
|
const isMysql = isMySQL();
|
|
109
109
|
|
|
110
110
|
return {
|
|
111
|
-
number: isSqlite ?
|
|
112
|
-
string: isSqlite ?
|
|
113
|
-
text: isMysql ?
|
|
114
|
-
array_string: isSqlite ?
|
|
115
|
-
array_text: isMysql ?
|
|
111
|
+
number: isSqlite ? "INTEGER" : isPg ? "BIGINT" : "BIGINT",
|
|
112
|
+
string: isSqlite ? "TEXT" : isPg ? "character varying" : "VARCHAR",
|
|
113
|
+
text: isMysql ? "MEDIUMTEXT" : "TEXT",
|
|
114
|
+
array_string: isSqlite ? "TEXT" : isPg ? "character varying" : "VARCHAR",
|
|
115
|
+
array_text: isMysql ? "MEDIUMTEXT" : "TEXT",
|
|
116
|
+
array_number_string: isSqlite ? "TEXT" : isPg ? "character varying" : "VARCHAR",
|
|
117
|
+
array_number_text: isMysql ? "MEDIUMTEXT" : "TEXT"
|
|
116
118
|
};
|
|
117
119
|
}
|
package/sync/syncDb/ddl.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* syncDb DDL 构建模块
|
|
3
3
|
*
|
|
4
4
|
* 包含:
|
|
@@ -8,15 +8,14 @@
|
|
|
8
8
|
* - 构建系统列和业务列定义
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
import { resolveDefaultValue, generateDefaultSql, getSqlType } from './types.js';
|
|
11
|
+
import type { FieldDefinition } from "../../types/validate.js";
|
|
12
|
+
import type { SQL } from "bun";
|
|
13
|
+
|
|
14
|
+
import { snakeCase } from "es-toolkit/string";
|
|
16
15
|
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
16
|
+
import { isMySQL, isPG } from "./constants.js";
|
|
17
|
+
import { quoteIdentifier, escapeComment } from "./helpers.js";
|
|
18
|
+
import { resolveDefaultValue, generateDefaultSql, getSqlType } from "./types.js";
|
|
20
19
|
|
|
21
20
|
/**
|
|
22
21
|
* 构建索引操作 SQL(统一使用在线策略)
|
|
@@ -27,26 +26,26 @@ import type { AnyObject } from '../../types/common.js';
|
|
|
27
26
|
* @param action - 操作类型(create/drop)
|
|
28
27
|
* @returns SQL 语句
|
|
29
28
|
*/
|
|
30
|
-
export function buildIndexSQL(tableName: string, indexName: string, fieldName: string, action:
|
|
29
|
+
export function buildIndexSQL(tableName: string, indexName: string, fieldName: string, action: "create" | "drop"): string {
|
|
31
30
|
const tableQuoted = quoteIdentifier(tableName);
|
|
32
31
|
const indexQuoted = quoteIdentifier(indexName);
|
|
33
32
|
const fieldQuoted = quoteIdentifier(fieldName);
|
|
34
33
|
|
|
35
34
|
if (isMySQL()) {
|
|
36
35
|
const parts = [];
|
|
37
|
-
if (action ===
|
|
36
|
+
if (action === "create") {
|
|
38
37
|
parts.push(`ADD INDEX ${indexQuoted} (${fieldQuoted})`);
|
|
39
38
|
} else {
|
|
40
39
|
parts.push(`DROP INDEX ${indexQuoted}`);
|
|
41
40
|
}
|
|
42
41
|
// 始终使用在线算法
|
|
43
|
-
parts.push(
|
|
44
|
-
parts.push(
|
|
45
|
-
return `ALTER TABLE ${tableQuoted} ${parts.join(
|
|
42
|
+
parts.push("ALGORITHM=INPLACE");
|
|
43
|
+
parts.push("LOCK=NONE");
|
|
44
|
+
return `ALTER TABLE ${tableQuoted} ${parts.join(", ")}`;
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
if (isPG()) {
|
|
49
|
-
if (action ===
|
|
48
|
+
if (action === "create") {
|
|
50
49
|
// 始终使用 CONCURRENTLY
|
|
51
50
|
return `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${indexQuoted} ON ${tableQuoted}(${fieldQuoted})`;
|
|
52
51
|
}
|
|
@@ -54,7 +53,7 @@ export function buildIndexSQL(tableName: string, indexName: string, fieldName: s
|
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
// SQLite
|
|
57
|
-
if (action ===
|
|
56
|
+
if (action === "create") {
|
|
58
57
|
return `CREATE INDEX IF NOT EXISTS ${indexQuoted} ON ${tableQuoted}(${fieldQuoted})`;
|
|
59
58
|
}
|
|
60
59
|
return `DROP INDEX IF EXISTS ${indexQuoted}`;
|
|
@@ -92,7 +91,7 @@ export function getSystemColumnDef(fieldName: string): string | null {
|
|
|
92
91
|
* @returns 系统字段的列定义数组
|
|
93
92
|
*/
|
|
94
93
|
export function buildSystemColumnDefs(): string[] {
|
|
95
|
-
return [getSystemColumnDef(
|
|
94
|
+
return [getSystemColumnDef("id")!, getSystemColumnDef("created_at")!, getSystemColumnDef("updated_at")!, getSystemColumnDef("deleted_at")!, getSystemColumnDef("state")!];
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
/**
|
|
@@ -115,8 +114,8 @@ export function buildBusinessColumnDefs(fields: Record<string, FieldDefinition>)
|
|
|
115
114
|
const defaultSql = generateDefaultSql(actualDefault, fieldDef.type);
|
|
116
115
|
|
|
117
116
|
// 构建约束
|
|
118
|
-
const uniqueSql = fieldDef.unique ?
|
|
119
|
-
const nullableSql = fieldDef.nullable ?
|
|
117
|
+
const uniqueSql = fieldDef.unique ? " UNIQUE" : "";
|
|
118
|
+
const nullableSql = fieldDef.nullable ? " NULL" : " NOT NULL";
|
|
120
119
|
|
|
121
120
|
if (isMySQL()) {
|
|
122
121
|
colDefs.push(`\`${dbFieldName}\` ${sqlType}${uniqueSql}${nullableSql}${defaultSql} COMMENT "${escapeComment(fieldDef.name)}"`);
|
|
@@ -147,11 +146,11 @@ export function generateDDLClause(fieldKey: string, fieldDef: FieldDefinition, i
|
|
|
147
146
|
const defaultSql = generateDefaultSql(actualDefault, fieldDef.type);
|
|
148
147
|
|
|
149
148
|
// 构建约束
|
|
150
|
-
const uniqueSql = fieldDef.unique ?
|
|
151
|
-
const nullableSql = fieldDef.nullable ?
|
|
149
|
+
const uniqueSql = fieldDef.unique ? " UNIQUE" : "";
|
|
150
|
+
const nullableSql = fieldDef.nullable ? " NULL" : " NOT NULL";
|
|
152
151
|
|
|
153
152
|
if (isMySQL()) {
|
|
154
|
-
return `${isAdd ?
|
|
153
|
+
return `${isAdd ? "ADD COLUMN" : "MODIFY COLUMN"} \`${dbFieldName}\` ${sqlType}${uniqueSql}${nullableSql}${defaultSql} COMMENT "${escapeComment(fieldDef.name)}"`;
|
|
155
154
|
}
|
|
156
155
|
if (isPG()) {
|
|
157
156
|
if (isAdd) return `ADD COLUMN IF NOT EXISTS "${dbFieldName}" ${sqlType}${uniqueSql}${nullableSql}${defaultSql}`;
|
|
@@ -160,7 +159,7 @@ export function generateDDLClause(fieldKey: string, fieldDef: FieldDefinition, i
|
|
|
160
159
|
}
|
|
161
160
|
// SQLite 仅支持 ADD COLUMN(>=3.50.0:支持 IF NOT EXISTS)
|
|
162
161
|
if (isAdd) return `ADD COLUMN IF NOT EXISTS "${dbFieldName}" ${sqlType}${uniqueSql}${nullableSql}${defaultSql}`;
|
|
163
|
-
return
|
|
162
|
+
return "";
|
|
164
163
|
}
|
|
165
164
|
|
|
166
165
|
/**
|
|
@@ -182,17 +181,17 @@ export async function executeDDLSafely(sql: SQL, stmt: string): Promise<boolean>
|
|
|
182
181
|
return true;
|
|
183
182
|
} catch (error: any) {
|
|
184
183
|
// MySQL 专用降级路径
|
|
185
|
-
if (stmt.includes(
|
|
186
|
-
const inplaceSql = stmt.replace(/ALGORITHM=INSTANT/g,
|
|
184
|
+
if (stmt.includes("ALGORITHM=INSTANT")) {
|
|
185
|
+
const inplaceSql = stmt.replace(/ALGORITHM=INSTANT/g, "ALGORITHM=INPLACE");
|
|
187
186
|
try {
|
|
188
187
|
await sql.unsafe(inplaceSql);
|
|
189
188
|
return true;
|
|
190
|
-
} catch
|
|
189
|
+
} catch {
|
|
191
190
|
// 最后尝试传统DDL:移除 ALGORITHM/LOCK 附加子句后执行
|
|
192
191
|
const traditionSql = stmt
|
|
193
|
-
.replace(/,\s*ALGORITHM=INPLACE/g,
|
|
194
|
-
.replace(/,\s*ALGORITHM=INSTANT/g,
|
|
195
|
-
.replace(/,\s*LOCK=(NONE|SHARED|EXCLUSIVE)/g,
|
|
192
|
+
.replace(/,\s*ALGORITHM=INPLACE/g, "")
|
|
193
|
+
.replace(/,\s*ALGORITHM=INSTANT/g, "")
|
|
194
|
+
.replace(/,\s*LOCK=(NONE|SHARED|EXCLUSIVE)/g, "");
|
|
196
195
|
await sql.unsafe(traditionSql);
|
|
197
196
|
return true;
|
|
198
197
|
}
|
|
@@ -216,8 +215,8 @@ export async function executeDDLSafely(sql: SQL, stmt: string): Promise<boolean>
|
|
|
216
215
|
* @returns 是否为兼容变更
|
|
217
216
|
*/
|
|
218
217
|
export function isCompatibleTypeChange(currentType: string, newType: string): boolean {
|
|
219
|
-
const c = String(currentType ||
|
|
220
|
-
const n = String(newType ||
|
|
218
|
+
const c = String(currentType || "").toLowerCase();
|
|
219
|
+
const n = String(newType || "").toLowerCase();
|
|
221
220
|
|
|
222
221
|
// 相同类型不算变更
|
|
223
222
|
if (c === n) return false;
|
|
@@ -226,8 +225,8 @@ export function isCompatibleTypeChange(currentType: string, newType: string): bo
|
|
|
226
225
|
const extractBaseType = (t: string): string => {
|
|
227
226
|
// 移除 unsigned 和括号内容
|
|
228
227
|
return t
|
|
229
|
-
.replace(/\s*unsigned/gi,
|
|
230
|
-
.replace(/\([^)]*\)/g,
|
|
228
|
+
.replace(/\s*unsigned/gi, "")
|
|
229
|
+
.replace(/\([^)]*\)/g, "")
|
|
231
230
|
.trim();
|
|
232
231
|
};
|
|
233
232
|
|
|
@@ -235,7 +234,7 @@ export function isCompatibleTypeChange(currentType: string, newType: string): bo
|
|
|
235
234
|
const nBase = extractBaseType(n);
|
|
236
235
|
|
|
237
236
|
// MySQL/通用 整数类型宽化(小 -> 大)
|
|
238
|
-
const intTypes = [
|
|
237
|
+
const intTypes = ["tinyint", "smallint", "mediumint", "int", "integer", "bigint"];
|
|
239
238
|
const cIntIdx = intTypes.indexOf(cBase);
|
|
240
239
|
const nIntIdx = intTypes.indexOf(nBase);
|
|
241
240
|
if (cIntIdx !== -1 && nIntIdx !== -1 && nIntIdx > cIntIdx) {
|
|
@@ -244,9 +243,9 @@ export function isCompatibleTypeChange(currentType: string, newType: string): bo
|
|
|
244
243
|
|
|
245
244
|
// 字符串类型宽化
|
|
246
245
|
// MySQL: varchar -> text/mediumtext/longtext
|
|
247
|
-
if (cBase ===
|
|
246
|
+
if (cBase === "varchar" && (nBase === "text" || nBase === "mediumtext" || nBase === "longtext")) return true;
|
|
248
247
|
// PG: character varying -> text
|
|
249
|
-
if (cBase ===
|
|
248
|
+
if (cBase === "character varying" && nBase === "text") return true;
|
|
250
249
|
|
|
251
250
|
return false;
|
|
252
251
|
}
|
package/sync/syncDb/helpers.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* syncDb 辅助工具模块
|
|
3
3
|
*
|
|
4
4
|
* 包含:
|
|
@@ -7,11 +7,8 @@
|
|
|
7
7
|
* - 字段默认值应用
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
// 从 types.ts 重新导出,保持向后兼容
|
|
14
|
-
export { isStringOrArrayType, getSqlType, resolveDefaultValue, generateDefaultSql } from './types.js';
|
|
10
|
+
import { Logger } from "../../lib/logger.js";
|
|
11
|
+
import { isMySQL, isPG } from "./constants.js";
|
|
15
12
|
|
|
16
13
|
/**
|
|
17
14
|
* 根据数据库类型引用标识符
|
|
@@ -66,7 +63,7 @@ export function logFieldChange(tableName: string, fieldName: string, changeType:
|
|
|
66
63
|
* @returns 格式化的字符串(逗号分隔)
|
|
67
64
|
*/
|
|
68
65
|
export function formatFieldList(fields: string[]): string {
|
|
69
|
-
return fields.map((f) => quoteIdentifier(f)).join(
|
|
66
|
+
return fields.map((f) => quoteIdentifier(f)).join(", ");
|
|
70
67
|
}
|
|
71
68
|
|
|
72
69
|
/**
|
|
@@ -75,9 +72,9 @@ export function formatFieldList(fields: string[]): string {
|
|
|
75
72
|
* @param fieldDef - 字段定义对象
|
|
76
73
|
*/
|
|
77
74
|
export function applyFieldDefaults(fieldDef: any): void {
|
|
78
|
-
fieldDef.detail = fieldDef.detail ??
|
|
75
|
+
fieldDef.detail = fieldDef.detail ?? "";
|
|
79
76
|
fieldDef.min = fieldDef.min ?? 0;
|
|
80
|
-
fieldDef.max = fieldDef.max ?? (fieldDef.type ===
|
|
77
|
+
fieldDef.max = fieldDef.max ?? (fieldDef.type === "number" ? Number.MAX_SAFE_INTEGER : 100);
|
|
81
78
|
fieldDef.default = fieldDef.default ?? null;
|
|
82
79
|
fieldDef.index = fieldDef.index ?? false;
|
|
83
80
|
fieldDef.unique = fieldDef.unique ?? false;
|
package/sync/syncDb/schema.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* syncDb 表结构查询模块
|
|
3
3
|
*
|
|
4
4
|
* 包含:
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* - 获取表的索引信息
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import type {
|
|
12
|
-
|
|
10
|
+
import type { ColumnInfo, IndexInfo } from "../../types/sync.js";
|
|
11
|
+
import type { SQL } from "bun";
|
|
12
|
+
|
|
13
|
+
import { isMySQL, isPG, isSQLite } from "./constants.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* 判断表是否存在(返回布尔值)
|
|
@@ -20,7 +21,7 @@ import type { SQL } from 'bun';
|
|
|
20
21
|
* @returns 表是否存在
|
|
21
22
|
*/
|
|
22
23
|
export async function tableExists(sql: SQL, tableName: string, dbName: string): Promise<boolean> {
|
|
23
|
-
if (!sql) throw new Error(
|
|
24
|
+
if (!sql) throw new Error("SQL 客户端未初始化");
|
|
24
25
|
|
|
25
26
|
try {
|
|
26
27
|
if (isMySQL()) {
|
|
@@ -85,7 +86,7 @@ export async function getTableColumns(sql: SQL, tableName: string, dbName: strin
|
|
|
85
86
|
columnType: row.COLUMN_TYPE,
|
|
86
87
|
length: row.CHARACTER_MAXIMUM_LENGTH,
|
|
87
88
|
max: row.CHARACTER_MAXIMUM_LENGTH,
|
|
88
|
-
nullable: row.IS_NULLABLE ===
|
|
89
|
+
nullable: row.IS_NULLABLE === "YES",
|
|
89
90
|
defaultValue: defaultValue,
|
|
90
91
|
comment: row.COLUMN_COMMENT
|
|
91
92
|
};
|
|
@@ -114,7 +115,7 @@ export async function getTableColumns(sql: SQL, tableName: string, dbName: strin
|
|
|
114
115
|
columnType: row.data_type,
|
|
115
116
|
length: row.character_maximum_length,
|
|
116
117
|
max: row.character_maximum_length,
|
|
117
|
-
nullable: String(row.is_nullable).toUpperCase() ===
|
|
118
|
+
nullable: String(row.is_nullable).toUpperCase() === "YES",
|
|
118
119
|
defaultValue: row.column_default,
|
|
119
120
|
comment: commentMap[row.column_name] ?? null
|
|
120
121
|
};
|
|
@@ -122,7 +123,7 @@ export async function getTableColumns(sql: SQL, tableName: string, dbName: strin
|
|
|
122
123
|
} else if (isSQLite()) {
|
|
123
124
|
const result = await sql.unsafe(`PRAGMA table_info(${tableName})`);
|
|
124
125
|
for (const row of result) {
|
|
125
|
-
let baseType = String(row.type ||
|
|
126
|
+
let baseType = String(row.type || "").toUpperCase();
|
|
126
127
|
let max = null;
|
|
127
128
|
const m = /^(\w+)\s*\((\d+)\)/.exec(baseType);
|
|
128
129
|
if (m) {
|
|
@@ -181,7 +182,7 @@ export async function getTableIndexes(sql: SQL, tableName: string, dbName: strin
|
|
|
181
182
|
for (const row of result) {
|
|
182
183
|
const m = /\(([^)]+)\)/.exec(row.indexdef);
|
|
183
184
|
if (m) {
|
|
184
|
-
const col = m[1].replace(
|
|
185
|
+
const col = m[1].replace(/"/g, "").trim();
|
|
185
186
|
indexes[row.indexname] = [col];
|
|
186
187
|
}
|
|
187
188
|
}
|
package/sync/syncDb/sqlite.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* syncDb SQLite 特殊处理模块
|
|
3
3
|
*
|
|
4
4
|
* 包含:
|
|
5
5
|
* - SQLite 重建表迁移(处理列修改等不支持的操作)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
import
|
|
12
|
-
import type { FieldDefinition } from 'befly-shared/types';
|
|
8
|
+
import type { FieldDefinition } from "../../types/validate.js";
|
|
9
|
+
import type { SQL } from "bun";
|
|
10
|
+
|
|
11
|
+
import { createTable } from "./tableCreate.js";
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* SQLite 重建表迁移(简化版)
|
|
@@ -30,7 +29,7 @@ export async function rebuildSqliteTable(sql: SQL, tableName: string, fields: Re
|
|
|
30
29
|
// 1. 读取现有列顺序
|
|
31
30
|
const info = await sql.unsafe(`PRAGMA table_info(${tableName})`);
|
|
32
31
|
const existingCols = info.map((r: any) => r.name);
|
|
33
|
-
const targetCols = [
|
|
32
|
+
const targetCols = ["id", "created_at", "updated_at", "deleted_at", "state", ...Object.keys(fields)];
|
|
34
33
|
const tmpTable = `${tableName}__tmp__${Date.now()}`;
|
|
35
34
|
|
|
36
35
|
// 2. 创建新表(使用当前定义)
|
|
@@ -39,7 +38,7 @@ export async function rebuildSqliteTable(sql: SQL, tableName: string, fields: Re
|
|
|
39
38
|
// 3. 拷贝数据(按交集列)
|
|
40
39
|
const commonCols = targetCols.filter((c) => existingCols.includes(c));
|
|
41
40
|
if (commonCols.length > 0) {
|
|
42
|
-
const colsSql = commonCols.map((c) => `"${c}"`).join(
|
|
41
|
+
const colsSql = commonCols.map((c) => `"${c}"`).join(", ");
|
|
43
42
|
await sql.unsafe(`INSERT INTO "${tmpTable}" (${colsSql}) SELECT ${colsSql} FROM "${tableName}"`);
|
|
44
43
|
}
|
|
45
44
|
|