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/table.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* syncDb 表操作模块
|
|
3
3
|
*
|
|
4
4
|
* 包含:
|
|
@@ -7,16 +7,19 @@
|
|
|
7
7
|
* - 应用变更计划
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
10
|
+
import type { TablePlan } from "../../types/sync.js";
|
|
11
|
+
import type { FieldDefinition } from "../../types/validate.js";
|
|
12
|
+
import type { SQL } from "bun";
|
|
13
|
+
|
|
14
|
+
import { snakeCase } from "es-toolkit/string";
|
|
15
|
+
|
|
16
|
+
import { Logger } from "../../lib/logger.js";
|
|
17
|
+
import { compareFieldDefinition, applyTablePlan } from "./apply.js";
|
|
18
|
+
import { isMySQL, isPG, CHANGE_TYPE_LABELS, getTypeMapping, SYSTEM_INDEX_FIELDS } from "./constants.js";
|
|
19
|
+
import { generateDDLClause, getSystemColumnDef, isCompatibleTypeChange } from "./ddl.js";
|
|
20
|
+
import { logFieldChange } from "./helpers.js";
|
|
21
|
+
import { getTableColumns, getTableIndexes } from "./schema.js";
|
|
22
|
+
import { generateDefaultSql, isStringOrArrayType, resolveDefaultValue } from "./types.js";
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* 同步表结构(对比和应用变更)
|
|
@@ -34,14 +37,14 @@ import type { FieldDefinition } from 'befly-shared/types';
|
|
|
34
37
|
* @param dbName - 数据库名称
|
|
35
38
|
*/
|
|
36
39
|
export async function modifyTable(sql: SQL, tableName: string, fields: Record<string, FieldDefinition>, force: boolean = false, dbName?: string): Promise<TablePlan> {
|
|
37
|
-
const existingColumns = await getTableColumns(sql, tableName, dbName ||
|
|
38
|
-
const existingIndexes = await getTableIndexes(sql, tableName, dbName ||
|
|
40
|
+
const existingColumns = await getTableColumns(sql, tableName, dbName || "");
|
|
41
|
+
const existingIndexes = await getTableIndexes(sql, tableName, dbName || "");
|
|
39
42
|
let changed = false;
|
|
40
43
|
|
|
41
44
|
const addClauses: string[] = [];
|
|
42
45
|
const modifyClauses: string[] = [];
|
|
43
46
|
const defaultClauses: string[] = [];
|
|
44
|
-
const indexActions: Array<{ action:
|
|
47
|
+
const indexActions: Array<{ action: "create" | "drop"; indexName: string; fieldName: string }> = [];
|
|
45
48
|
|
|
46
49
|
for (const [fieldKey, fieldDef] of Object.entries(fields)) {
|
|
47
50
|
// 转换字段名为下划线格式
|
|
@@ -52,7 +55,7 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
52
55
|
if (comparison.length > 0) {
|
|
53
56
|
for (const c of comparison) {
|
|
54
57
|
// 使用统一的日志格式函数和常量标签
|
|
55
|
-
const changeLabel = CHANGE_TYPE_LABELS[c.type as keyof typeof CHANGE_TYPE_LABELS] ||
|
|
58
|
+
const changeLabel = CHANGE_TYPE_LABELS[c.type as keyof typeof CHANGE_TYPE_LABELS] || "未知";
|
|
56
59
|
logFieldChange(tableName, dbFieldName, c.type, c.current, c.expected, changeLabel);
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -66,20 +69,20 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
const hasTypeChange = comparison.some((c) => c.type ===
|
|
70
|
-
const hasLengthChange = comparison.some((c) => c.type ===
|
|
71
|
-
const onlyDefaultChanged = comparison.every((c) => c.type ===
|
|
72
|
-
const defaultChanged = comparison.some((c) => c.type ===
|
|
72
|
+
const hasTypeChange = comparison.some((c) => c.type === "datatype");
|
|
73
|
+
const hasLengthChange = comparison.some((c) => c.type === "length");
|
|
74
|
+
const onlyDefaultChanged = comparison.every((c) => c.type === "default");
|
|
75
|
+
const defaultChanged = comparison.some((c) => c.type === "default");
|
|
73
76
|
|
|
74
77
|
// 类型变更检查:只允许兼容的宽化型变更(如 INT -> BIGINT)
|
|
75
78
|
if (hasTypeChange) {
|
|
76
|
-
const typeChange = comparison.find((c) => c.type ===
|
|
77
|
-
const currentType = String(typeChange?.current ||
|
|
79
|
+
const typeChange = comparison.find((c) => c.type === "datatype");
|
|
80
|
+
const currentType = String(typeChange?.current || "").toLowerCase();
|
|
78
81
|
const typeMapping = getTypeMapping();
|
|
79
|
-
const expectedType = typeMapping[fieldDef.type]?.toLowerCase() ||
|
|
82
|
+
const expectedType = typeMapping[fieldDef.type]?.toLowerCase() || "";
|
|
80
83
|
|
|
81
84
|
if (!isCompatibleTypeChange(currentType, expectedType)) {
|
|
82
|
-
const errorMsg = [`禁止字段类型变更: ${tableName}.${dbFieldName}`, `当前类型: ${typeChange?.current}`, `目标类型: ${typeChange?.expected}`, `说明: 仅允许宽化型变更(如 INT->BIGINT, VARCHAR->TEXT),其他类型变更需要手动处理`].join(
|
|
85
|
+
const errorMsg = [`禁止字段类型变更: ${tableName}.${dbFieldName}`, `当前类型: ${typeChange?.current}`, `目标类型: ${typeChange?.expected}`, `说明: 仅允许宽化型变更(如 INT->BIGINT, VARCHAR->TEXT),其他类型变更需要手动处理`].join("\n");
|
|
83
86
|
throw new Error(errorMsg);
|
|
84
87
|
}
|
|
85
88
|
Logger.debug(`[兼容类型变更] ${tableName}.${dbFieldName} ${currentType} -> ${expectedType}`);
|
|
@@ -92,18 +95,18 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
92
95
|
|
|
93
96
|
// 生成 SQL DEFAULT 值(不包含前导空格,因为要用于 ALTER COLUMN)
|
|
94
97
|
let v: string | null = null;
|
|
95
|
-
if (actualDefault !==
|
|
98
|
+
if (actualDefault !== "null") {
|
|
96
99
|
const defaultSql = generateDefaultSql(actualDefault, fieldDef.type);
|
|
97
100
|
// 移除前导空格 ' DEFAULT ' -> 'DEFAULT '
|
|
98
|
-
v = defaultSql.trim().replace(/^DEFAULT\s+/,
|
|
101
|
+
v = defaultSql.trim().replace(/^DEFAULT\s+/, "");
|
|
99
102
|
}
|
|
100
103
|
|
|
101
|
-
if (v !== null && v !==
|
|
104
|
+
if (v !== null && v !== "") {
|
|
102
105
|
if (isPG()) {
|
|
103
106
|
defaultClauses.push(`ALTER COLUMN "${dbFieldName}" SET DEFAULT ${v}`);
|
|
104
107
|
} else if (isMySQL() && onlyDefaultChanged) {
|
|
105
108
|
// MySQL 的 TEXT/BLOB 不允许 DEFAULT,跳过 text 类型
|
|
106
|
-
if (fieldDef.type !==
|
|
109
|
+
if (fieldDef.type !== "text") {
|
|
107
110
|
defaultClauses.push(`ALTER COLUMN \`${dbFieldName}\` SET DEFAULT ${v}`);
|
|
108
111
|
}
|
|
109
112
|
}
|
|
@@ -123,7 +126,6 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
123
126
|
changed = true;
|
|
124
127
|
}
|
|
125
128
|
} else {
|
|
126
|
-
const lenPart = isStringOrArrayType(fieldDef.type) ? ` 长度:${parseInt(String(fieldDef.max))}` : '';
|
|
127
129
|
// Logger.debug(` + 新增字段 ${dbFieldName} (${fieldDef.type}${lenPart})`);
|
|
128
130
|
addClauses.push(generateDDLClause(fieldKey, fieldDef, true));
|
|
129
131
|
changed = true;
|
|
@@ -132,7 +134,7 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
132
134
|
|
|
133
135
|
// 检查并添加缺失的系统字段(created_at, updated_at, deleted_at, state)
|
|
134
136
|
// 注意:id 是主键,不会缺失;这里只处理可能缺失的其他系统字段
|
|
135
|
-
const systemFieldNames = [
|
|
137
|
+
const systemFieldNames = ["created_at", "updated_at", "deleted_at", "state"];
|
|
136
138
|
for (const sysFieldName of systemFieldNames) {
|
|
137
139
|
if (!existingColumns[sysFieldName]) {
|
|
138
140
|
const colDef = getSystemColumnDef(sysFieldName);
|
|
@@ -150,7 +152,7 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
150
152
|
// 字段已存在或刚添加到 addClauses 中
|
|
151
153
|
const fieldWillExist = existingColumns[sysField] || systemFieldNames.includes(sysField);
|
|
152
154
|
if (fieldWillExist && !existingIndexes[idxName]) {
|
|
153
|
-
indexActions.push({ action:
|
|
155
|
+
indexActions.push({ action: "create", indexName: idxName, fieldName: sysField });
|
|
154
156
|
changed = true;
|
|
155
157
|
}
|
|
156
158
|
}
|
|
@@ -163,10 +165,10 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
163
165
|
const indexName = `idx_${dbFieldName}`;
|
|
164
166
|
// 如果字段有 unique 约束,跳过创建普通索引(unique 会自动创建唯一索引)
|
|
165
167
|
if (fieldDef.index && !fieldDef.unique && !existingIndexes[indexName]) {
|
|
166
|
-
indexActions.push({ action:
|
|
168
|
+
indexActions.push({ action: "create", indexName: indexName, fieldName: dbFieldName });
|
|
167
169
|
changed = true;
|
|
168
170
|
} else if (!fieldDef.index && existingIndexes[indexName] && existingIndexes[indexName].length === 1) {
|
|
169
|
-
indexActions.push({ action:
|
|
171
|
+
indexActions.push({ action: "drop", indexName: indexName, fieldName: dbFieldName });
|
|
170
172
|
changed = true;
|
|
171
173
|
}
|
|
172
174
|
}
|
|
@@ -179,12 +181,12 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
179
181
|
const dbFieldName = snakeCase(fieldKey);
|
|
180
182
|
|
|
181
183
|
if (existingColumns[dbFieldName]) {
|
|
182
|
-
const curr = existingColumns[dbFieldName].comment ||
|
|
183
|
-
const want = fieldDef.name && fieldDef.name !==
|
|
184
|
+
const curr = existingColumns[dbFieldName].comment || "";
|
|
185
|
+
const want = fieldDef.name && fieldDef.name !== "null" ? String(fieldDef.name) : "";
|
|
184
186
|
if (want !== curr) {
|
|
185
187
|
// 防止 SQL 注入:转义单引号
|
|
186
188
|
const escapedWant = want.replace(/'/g, "''");
|
|
187
|
-
commentActions.push(`COMMENT ON COLUMN "${tableName}"."${dbFieldName}" IS ${want ? `'${escapedWant}'` :
|
|
189
|
+
commentActions.push(`COMMENT ON COLUMN "${tableName}"."${dbFieldName}" IS ${want ? `'${escapedWant}'` : "NULL"}`);
|
|
188
190
|
changed = true;
|
|
189
191
|
}
|
|
190
192
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* syncDb 表创建模块
|
|
3
3
|
*
|
|
4
4
|
* 包含:
|
|
@@ -8,15 +8,16 @@
|
|
|
8
8
|
*
|
|
9
9
|
* 注意:此模块从 table.ts 中提取,用于解除循环依赖
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
import { buildSystemColumnDefs, buildBusinessColumnDefs, buildIndexSQL } from './ddl.js';
|
|
16
|
-
import { getTableIndexes } from './schema.js';
|
|
11
|
+
import type { FieldDefinition } from "../../types/validate.js";
|
|
12
|
+
import type { SQL } from "bun";
|
|
13
|
+
|
|
14
|
+
import { snakeCase } from "es-toolkit/string";
|
|
17
15
|
|
|
18
|
-
import
|
|
19
|
-
import
|
|
16
|
+
import { Logger } from "../../lib/logger.js";
|
|
17
|
+
import { isMySQL, isPG, IS_PLAN, MYSQL_TABLE_CONFIG } from "./constants.js";
|
|
18
|
+
import { buildSystemColumnDefs, buildBusinessColumnDefs, buildIndexSQL } from "./ddl.js";
|
|
19
|
+
import { quoteIdentifier } from "./helpers.js";
|
|
20
|
+
import { getTableIndexes } from "./schema.js";
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* 为 PostgreSQL 表添加列注释(使用字段的 name 作为注释)
|
|
@@ -28,11 +29,11 @@ import type { FieldDefinition } from 'befly-shared/types';
|
|
|
28
29
|
async function addPostgresComments(sql: SQL, tableName: string, fields: Record<string, FieldDefinition>): Promise<void> {
|
|
29
30
|
// 系统字段注释
|
|
30
31
|
const systemComments = [
|
|
31
|
-
[
|
|
32
|
-
[
|
|
33
|
-
[
|
|
34
|
-
[
|
|
35
|
-
[
|
|
32
|
+
["id", "主键ID"],
|
|
33
|
+
["created_at", "创建时间"],
|
|
34
|
+
["updated_at", "更新时间"],
|
|
35
|
+
["deleted_at", "删除时间"],
|
|
36
|
+
["state", "状态字段"]
|
|
36
37
|
];
|
|
37
38
|
|
|
38
39
|
for (const [name, comment] of systemComments) {
|
|
@@ -74,7 +75,7 @@ async function createTableIndexes(sql: SQL, tableName: string, fields: Record<st
|
|
|
74
75
|
// 获取现有索引(MySQL 不支持 IF NOT EXISTS,需要先检查)
|
|
75
76
|
let existingIndexes: Record<string, string[]> = {};
|
|
76
77
|
if (isMySQL()) {
|
|
77
|
-
existingIndexes = await getTableIndexes(sql, tableName, dbName ||
|
|
78
|
+
existingIndexes = await getTableIndexes(sql, tableName, dbName || "");
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
// 系统字段索引
|
|
@@ -84,7 +85,7 @@ async function createTableIndexes(sql: SQL, tableName: string, fields: Record<st
|
|
|
84
85
|
if (isMySQL() && existingIndexes[indexName]) {
|
|
85
86
|
continue;
|
|
86
87
|
}
|
|
87
|
-
const stmt = buildIndexSQL(tableName, indexName, sysField,
|
|
88
|
+
const stmt = buildIndexSQL(tableName, indexName, sysField, "create");
|
|
88
89
|
if (IS_PLAN) {
|
|
89
90
|
Logger.debug(`[计划] ${stmt}`);
|
|
90
91
|
} else {
|
|
@@ -103,7 +104,7 @@ async function createTableIndexes(sql: SQL, tableName: string, fields: Record<st
|
|
|
103
104
|
if (isMySQL() && existingIndexes[indexName]) {
|
|
104
105
|
continue;
|
|
105
106
|
}
|
|
106
|
-
const stmt = buildIndexSQL(tableName, indexName, dbFieldName,
|
|
107
|
+
const stmt = buildIndexSQL(tableName, indexName, dbFieldName, "create");
|
|
107
108
|
if (IS_PLAN) {
|
|
108
109
|
Logger.debug(`[计划] ${stmt}`);
|
|
109
110
|
} else {
|
|
@@ -127,12 +128,12 @@ async function createTableIndexes(sql: SQL, tableName: string, fields: Record<st
|
|
|
127
128
|
* @param systemIndexFields - 系统字段索引列表(可选,默认使用 ['created_at', 'updated_at', 'state'])
|
|
128
129
|
* @param dbName - 数据库名称(用于检查索引是否存在)
|
|
129
130
|
*/
|
|
130
|
-
export async function createTable(sql: SQL, tableName: string, fields: Record<string, FieldDefinition>, systemIndexFields: string[] = [
|
|
131
|
+
export async function createTable(sql: SQL, tableName: string, fields: Record<string, FieldDefinition>, systemIndexFields: string[] = ["created_at", "updated_at", "state"], dbName?: string): Promise<void> {
|
|
131
132
|
// 构建列定义
|
|
132
133
|
const colDefs = [...buildSystemColumnDefs(), ...buildBusinessColumnDefs(fields)];
|
|
133
134
|
|
|
134
135
|
// 生成 CREATE TABLE 语句
|
|
135
|
-
const cols = colDefs.join(
|
|
136
|
+
const cols = colDefs.join(",\n ");
|
|
136
137
|
const tableQuoted = quoteIdentifier(tableName);
|
|
137
138
|
const { ENGINE, CHARSET, COLLATE } = MYSQL_TABLE_CONFIG;
|
|
138
139
|
const createSQL = isMySQL()
|
|
@@ -144,7 +145,7 @@ export async function createTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
144
145
|
)`;
|
|
145
146
|
|
|
146
147
|
if (IS_PLAN) {
|
|
147
|
-
Logger.debug(`[计划] ${createSQL.replace(/\n+/g,
|
|
148
|
+
Logger.debug(`[计划] ${createSQL.replace(/\n+/g, " ")}`);
|
|
148
149
|
} else {
|
|
149
150
|
await sql.unsafe(createSQL);
|
|
150
151
|
}
|
package/sync/syncDb/types.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - 类型判断工具
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { isMySQL, getTypeMapping } from
|
|
10
|
+
import { isMySQL, getTypeMapping } from "./constants.js";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* 判断是否为字符串或数组类型(需要长度参数)
|
|
@@ -18,12 +18,13 @@ import { isMySQL, getTypeMapping } from './constants.js';
|
|
|
18
18
|
* @example
|
|
19
19
|
* isStringOrArrayType('string') // => true
|
|
20
20
|
* isStringOrArrayType('array_string') // => true
|
|
21
|
+
* isStringOrArrayType('array_number_string') // => true
|
|
21
22
|
* isStringOrArrayType('array_text') // => false
|
|
22
23
|
* isStringOrArrayType('number') // => false
|
|
23
24
|
* isStringOrArrayType('text') // => false
|
|
24
25
|
*/
|
|
25
26
|
export function isStringOrArrayType(fieldType: string): boolean {
|
|
26
|
-
return fieldType ===
|
|
27
|
+
return fieldType === "string" || fieldType === "array_string" || fieldType === "array_number_string";
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -47,8 +48,8 @@ export function getSqlType(fieldType: string, fieldMax: number | null, unsigned:
|
|
|
47
48
|
return `${typeMapping[fieldType]}(${fieldMax})`;
|
|
48
49
|
}
|
|
49
50
|
// 处理 UNSIGNED 修饰符(仅 MySQL number 类型)
|
|
50
|
-
const baseType = typeMapping[fieldType] ||
|
|
51
|
-
if (isMySQL() && fieldType ===
|
|
51
|
+
const baseType = typeMapping[fieldType] || "TEXT";
|
|
52
|
+
if (isMySQL() && fieldType === "number" && unsigned) {
|
|
52
53
|
return `${baseType} UNSIGNED`;
|
|
53
54
|
}
|
|
54
55
|
return baseType;
|
|
@@ -73,22 +74,24 @@ export function getSqlType(fieldType: string, fieldMax: number | null, unsigned:
|
|
|
73
74
|
*/
|
|
74
75
|
export function resolveDefaultValue(fieldDefault: any, fieldType: string): any {
|
|
75
76
|
// null 或字符串 'null' 都表示使用类型默认值
|
|
76
|
-
if (fieldDefault !== null && fieldDefault !==
|
|
77
|
+
if (fieldDefault !== null && fieldDefault !== "null") {
|
|
77
78
|
return fieldDefault;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
// null 表示使用类型默认值
|
|
81
82
|
switch (fieldType) {
|
|
82
|
-
case
|
|
83
|
+
case "number":
|
|
83
84
|
return 0;
|
|
84
|
-
case
|
|
85
|
-
return
|
|
86
|
-
case
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
case
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
case "string":
|
|
86
|
+
return "";
|
|
87
|
+
case "array_string":
|
|
88
|
+
case "array_number_string":
|
|
89
|
+
return "[]";
|
|
90
|
+
case "text":
|
|
91
|
+
case "array_text":
|
|
92
|
+
case "array_number_text":
|
|
93
|
+
// text/array_text/array_number_text 类型不设置默认值(MySQL TEXT 不支持),保持 'null'
|
|
94
|
+
return "null";
|
|
92
95
|
default:
|
|
93
96
|
return fieldDefault;
|
|
94
97
|
}
|
|
@@ -110,13 +113,13 @@ export function resolveDefaultValue(fieldDefault: any, fieldType: string): any {
|
|
|
110
113
|
*/
|
|
111
114
|
export function generateDefaultSql(actualDefault: any, fieldType: string): string {
|
|
112
115
|
// text 和 array_text 类型不设置默认值(MySQL TEXT 类型不支持默认值)
|
|
113
|
-
if (fieldType ===
|
|
114
|
-
return
|
|
116
|
+
if (fieldType === "text" || fieldType === "array_text" || actualDefault === "null") {
|
|
117
|
+
return "";
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
// 仅 number/string/array_string 类型设置默认值
|
|
118
|
-
if (fieldType ===
|
|
119
|
-
if (typeof actualDefault ===
|
|
120
|
+
// 仅 number/string/array_string/array_number_string 类型设置默认值
|
|
121
|
+
if (fieldType === "number" || fieldType === "string" || fieldType === "array_string" || fieldType === "array_number_string") {
|
|
122
|
+
if (typeof actualDefault === "number" && !Number.isNaN(actualDefault)) {
|
|
120
123
|
return ` DEFAULT ${actualDefault}`;
|
|
121
124
|
} else {
|
|
122
125
|
// 字符串需要转义单引号:' -> ''
|
|
@@ -125,5 +128,5 @@ export function generateDefaultSql(actualDefault: any, fieldType: string): strin
|
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
130
|
|
|
128
|
-
return
|
|
131
|
+
return "";
|
|
129
132
|
}
|
package/sync/syncDb/version.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* syncDb 数据库版本检查模块
|
|
3
3
|
*
|
|
4
4
|
* 包含:
|
|
5
5
|
* - 数据库版本验证(MySQL/PostgreSQL/SQLite)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import
|
|
8
|
+
import type { SQL } from "bun";
|
|
9
|
+
|
|
10
|
+
import { DB_VERSION_REQUIREMENTS, isMySQL, isPG, isSQLite } from "./constants.js";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* 数据库版本检查(按方言)
|
|
@@ -21,15 +21,15 @@ import type { SQL } from 'bun';
|
|
|
21
21
|
* @throws {Error} 如果数据库版本不符合要求或无法获取版本信息
|
|
22
22
|
*/
|
|
23
23
|
export async function ensureDbVersion(sql: SQL): Promise<void> {
|
|
24
|
-
if (!sql) throw new Error(
|
|
24
|
+
if (!sql) throw new Error("SQL 客户端未初始化");
|
|
25
25
|
|
|
26
26
|
if (isMySQL()) {
|
|
27
27
|
const r = await sql`SELECT VERSION() AS version`;
|
|
28
28
|
if (!r || r.length === 0 || !r[0]?.version) {
|
|
29
|
-
throw new Error(
|
|
29
|
+
throw new Error("无法获取 MySQL 版本信息");
|
|
30
30
|
}
|
|
31
31
|
const version = r[0].version;
|
|
32
|
-
const majorVersion = parseInt(String(version).split(
|
|
32
|
+
const majorVersion = parseInt(String(version).split(".")[0], 10);
|
|
33
33
|
if (!Number.isFinite(majorVersion) || majorVersion < DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR) {
|
|
34
34
|
throw new Error(`此脚本仅支持 MySQL ${DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR}.0+,当前版本: ${version}`);
|
|
35
35
|
}
|
|
@@ -39,7 +39,7 @@ export async function ensureDbVersion(sql: SQL): Promise<void> {
|
|
|
39
39
|
if (isPG()) {
|
|
40
40
|
const r = await sql`SELECT version() AS version`;
|
|
41
41
|
if (!r || r.length === 0 || !r[0]?.version) {
|
|
42
|
-
throw new Error(
|
|
42
|
+
throw new Error("无法获取 PostgreSQL 版本信息");
|
|
43
43
|
}
|
|
44
44
|
const versionText = r[0].version;
|
|
45
45
|
const m = /PostgreSQL\s+(\d+)/i.exec(versionText);
|
|
@@ -53,12 +53,12 @@ export async function ensureDbVersion(sql: SQL): Promise<void> {
|
|
|
53
53
|
if (isSQLite()) {
|
|
54
54
|
const r = await sql`SELECT sqlite_version() AS version`;
|
|
55
55
|
if (!r || r.length === 0 || !r[0]?.version) {
|
|
56
|
-
throw new Error(
|
|
56
|
+
throw new Error("无法获取 SQLite 版本信息");
|
|
57
57
|
}
|
|
58
58
|
const version = r[0].version;
|
|
59
59
|
// 强制最低版本:SQLite ≥ 3.50.0
|
|
60
60
|
const [maj, min, patch] = String(version)
|
|
61
|
-
.split(
|
|
61
|
+
.split(".")
|
|
62
62
|
.map((v) => parseInt(v, 10) || 0);
|
|
63
63
|
const vnum = maj * 10000 + min * 100 + patch; // 3.50.0 -> 35000
|
|
64
64
|
if (!Number.isFinite(vnum) || vnum < DB_VERSION_REQUIREMENTS.SQLITE_MIN_VERSION_NUM) {
|
package/sync/syncDb.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* SyncDb 命令 - 同步数据库表结构
|
|
3
3
|
*
|
|
4
4
|
* 功能:
|
|
@@ -7,28 +7,30 @@
|
|
|
7
7
|
* - 提供统计信息和错误处理
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
10
|
+
import type { SyncDbOptions } from "../types/sync.js";
|
|
11
|
+
import type { SQL } from "bun";
|
|
12
|
+
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
|
|
15
|
+
import { snakeCase } from "es-toolkit/string";
|
|
16
|
+
import { resolve } from "pathe";
|
|
17
|
+
|
|
18
|
+
import { beflyConfig } from "../befly.config.js";
|
|
19
|
+
import { checkTable } from "../checks/checkTable.js";
|
|
20
|
+
import { CacheKeys } from "../lib/cacheKeys.js";
|
|
21
|
+
import { Connect } from "../lib/connect.js";
|
|
22
|
+
import { Logger } from "../lib/logger.js";
|
|
23
|
+
import { RedisHelper } from "../lib/redisHelper.js";
|
|
24
|
+
import { projectDir } from "../paths.js";
|
|
25
|
+
import { scanAddons, addonDirExists, getAddonDir } from "../utils/addonHelper.js";
|
|
26
|
+
import { scanFiles } from "../utils/scanFiles.js";
|
|
27
|
+
import { setDbType } from "./syncDb/constants.js";
|
|
28
|
+
import { applyFieldDefaults } from "./syncDb/helpers.js";
|
|
29
|
+
import { tableExists } from "./syncDb/schema.js";
|
|
30
|
+
import { modifyTable } from "./syncDb/table.js";
|
|
31
|
+
import { createTable } from "./syncDb/tableCreate.js";
|
|
22
32
|
// 导入模块化的功能
|
|
23
|
-
import { ensureDbVersion } from
|
|
24
|
-
import { tableExists } from './syncDb/schema.js';
|
|
25
|
-
import { modifyTable } from './syncDb/table.js';
|
|
26
|
-
import { createTable } from './syncDb/tableCreate.js';
|
|
27
|
-
import { applyFieldDefaults } from './syncDb/helpers.js';
|
|
28
|
-
import { setDbType } from './syncDb/constants.js';
|
|
29
|
-
import { beflyConfig } from '../befly.config.js';
|
|
30
|
-
import type { SQL } from 'bun';
|
|
31
|
-
import type { SyncDbOptions } from '../types/index.js';
|
|
33
|
+
import { ensureDbVersion } from "./syncDb/version.js";
|
|
32
34
|
|
|
33
35
|
// 全局 SQL 客户端实例
|
|
34
36
|
let sql: SQL | null = null;
|
|
@@ -51,7 +53,7 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
|
|
|
51
53
|
processedTables.length = 0;
|
|
52
54
|
|
|
53
55
|
// 设置数据库类型(从配置获取)
|
|
54
|
-
const dbType = beflyConfig.db?.type ||
|
|
56
|
+
const dbType = beflyConfig.db?.type || "mysql";
|
|
55
57
|
setDbType(dbType);
|
|
56
58
|
|
|
57
59
|
// 验证表定义文件
|
|
@@ -65,21 +67,26 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
|
|
|
65
67
|
await Connect.connectRedis();
|
|
66
68
|
|
|
67
69
|
// 扫描表定义文件
|
|
68
|
-
const directories: Array<{
|
|
70
|
+
const directories: Array<{
|
|
71
|
+
path: string;
|
|
72
|
+
type: "app" | "addon";
|
|
73
|
+
addonName?: string;
|
|
74
|
+
addonNameSnake?: string;
|
|
75
|
+
}> = [];
|
|
69
76
|
|
|
70
77
|
// 1. 项目表(无前缀)- 如果 tables 目录存在
|
|
71
|
-
const projectTablesDir = resolve(projectDir,
|
|
78
|
+
const projectTablesDir = resolve(projectDir, "tables");
|
|
72
79
|
if (existsSync(projectTablesDir)) {
|
|
73
|
-
directories.push({ path: projectTablesDir, type:
|
|
80
|
+
directories.push({ path: projectTablesDir, type: "app" });
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
// 添加所有 addon 的 tables 目录(addon_{name}_ 前缀)
|
|
77
84
|
const addons = scanAddons();
|
|
78
85
|
for (const addon of addons) {
|
|
79
|
-
if (addonDirExists(addon,
|
|
86
|
+
if (addonDirExists(addon, "tables")) {
|
|
80
87
|
directories.push({
|
|
81
|
-
path: getAddonDir(addon,
|
|
82
|
-
type:
|
|
88
|
+
path: getAddonDir(addon, "tables"),
|
|
89
|
+
type: "addon",
|
|
83
90
|
addonName: addon,
|
|
84
91
|
addonNameSnake: snakeCase(addon) // 提前转换,避免每个文件都转换
|
|
85
92
|
});
|
|
@@ -90,7 +97,7 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
|
|
|
90
97
|
for (const dirConfig of directories) {
|
|
91
98
|
const { path: dir, type } = dirConfig;
|
|
92
99
|
|
|
93
|
-
const files = await scanFiles(dir,
|
|
100
|
+
const files = await scanFiles(dir, "*.json");
|
|
94
101
|
|
|
95
102
|
for (const { filePath: file, fileName } of files) {
|
|
96
103
|
// 确定表名:
|
|
@@ -99,7 +106,7 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
|
|
|
99
106
|
// - 项目表:{表名}
|
|
100
107
|
// 例如:user.json → user
|
|
101
108
|
let tableName = snakeCase(fileName);
|
|
102
|
-
if (type ===
|
|
109
|
+
if (type === "addon" && dirConfig.addonNameSnake) {
|
|
103
110
|
// addon 表,使用提前转换好的名称
|
|
104
111
|
tableName = `addon_${dirConfig.addonNameSnake}_${tableName}`;
|
|
105
112
|
}
|
|
@@ -109,7 +116,7 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
|
|
|
109
116
|
continue;
|
|
110
117
|
}
|
|
111
118
|
|
|
112
|
-
const tableDefinitionModule = await import(file, { with: { type:
|
|
119
|
+
const tableDefinitionModule = await import(file, { with: { type: "json" } });
|
|
113
120
|
const tableDefinition = tableDefinitionModule.default;
|
|
114
121
|
|
|
115
122
|
// 为字段属性设置默认值
|
|
@@ -117,7 +124,7 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
|
|
|
117
124
|
applyFieldDefaults(fieldDef);
|
|
118
125
|
}
|
|
119
126
|
|
|
120
|
-
const dbName = beflyConfig.db?.database ||
|
|
127
|
+
const dbName = beflyConfig.db?.database || "";
|
|
121
128
|
const existsTable = await tableExists(sql!, tableName, dbName);
|
|
122
129
|
|
|
123
130
|
// 读取 force 参数
|
|
@@ -126,7 +133,7 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
|
|
|
126
133
|
if (existsTable) {
|
|
127
134
|
await modifyTable(sql!, tableName, tableDefinition, force, dbName);
|
|
128
135
|
} else {
|
|
129
|
-
await createTable(sql!, tableName, tableDefinition, [
|
|
136
|
+
await createTable(sql!, tableName, tableDefinition, ["created_at", "updated_at", "state"], dbName);
|
|
130
137
|
}
|
|
131
138
|
|
|
132
139
|
// 记录处理过的表名(用于清理缓存)
|
|
@@ -137,11 +144,11 @@ export async function syncDbCommand(options: SyncDbOptions = {}): Promise<void>
|
|
|
137
144
|
// 清理 Redis 缓存(如果有表被处理)
|
|
138
145
|
if (processedTables.length > 0) {
|
|
139
146
|
const redisHelper = new RedisHelper();
|
|
140
|
-
const cacheKeys = processedTables.map((tableName) =>
|
|
147
|
+
const cacheKeys = processedTables.map((tableName) => CacheKeys.tableColumns(tableName));
|
|
141
148
|
await redisHelper.delBatch(cacheKeys);
|
|
142
149
|
}
|
|
143
150
|
} catch (error: any) {
|
|
144
|
-
Logger.error({ err: error },
|
|
151
|
+
Logger.error({ err: error }, "数据库同步失败");
|
|
145
152
|
throw error;
|
|
146
153
|
} finally {
|
|
147
154
|
if (sql) {
|