befly 3.15.0 → 3.15.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/README.md +1 -1
- package/dist/befly.config.js +1 -1
- package/dist/befly.js +316 -223
- package/dist/befly.min.js +13 -13
- package/dist/checks/checkConfig.d.ts +9 -0
- package/dist/checks/checkConfig.js +116 -0
- package/dist/checks/checkTable.js +105 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +16 -9
- package/dist/lib/connect.js +6 -6
- package/dist/plugins/cache.js +2 -2
- package/dist/plugins/db.js +3 -3
- package/dist/sync/syncApi.js +2 -2
- package/dist/sync/syncCache.js +1 -1
- package/dist/sync/syncDev.js +2 -2
- package/dist/sync/syncMenu.js +4 -4
- package/dist/sync/syncTable.d.ts +13 -9
- package/dist/sync/syncTable.js +119 -202
- package/dist/types/befly.d.ts +2 -2
- package/dist/types/database.d.ts +1 -1
- package/dist/types/table.d.ts +1 -1
- package/dist/types/validate.d.ts +1 -1
- package/package.json +2 -2
package/dist/sync/syncTable.js
CHANGED
|
@@ -10,6 +10,20 @@ import { getDialectByName, getSyncTableColumnsInfoQuery, getSyncTableIndexesQuer
|
|
|
10
10
|
import { Logger } from "../lib/logger";
|
|
11
11
|
import { normalizeFieldDefinition } from "../utils/normalizeFieldDefinition";
|
|
12
12
|
import { snakeCase } from "../utils/util";
|
|
13
|
+
function createRuntimeForIO(dbDialect, db, dbName = "") {
|
|
14
|
+
return {
|
|
15
|
+
dbDialect: dbDialect,
|
|
16
|
+
db: db,
|
|
17
|
+
dbName: dbName
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function buildRuntimeIoError(operation, tableName, error) {
|
|
21
|
+
const errMsg = String(error?.message || error);
|
|
22
|
+
const outErr = new Error(`同步表:读取元信息失败,操作=${operation},表=${tableName},错误=${errMsg}`);
|
|
23
|
+
if (error?.sqlInfo)
|
|
24
|
+
outErr.sqlInfo = error.sqlInfo;
|
|
25
|
+
return outErr;
|
|
26
|
+
}
|
|
13
27
|
/**
|
|
14
28
|
* 数据库同步命令入口(函数模式)
|
|
15
29
|
*/
|
|
@@ -18,39 +32,22 @@ export const syncTable = (async (ctx, items) => {
|
|
|
18
32
|
// 记录处理过的表名(用于清理缓存)
|
|
19
33
|
const processedTables = [];
|
|
20
34
|
if (!Array.isArray(items)) {
|
|
21
|
-
throw new Error("
|
|
35
|
+
throw new Error("同步表:请传入多个表定义组成的数组");
|
|
22
36
|
}
|
|
23
|
-
if (!ctx) {
|
|
24
|
-
throw new Error("
|
|
25
|
-
}
|
|
26
|
-
if (!ctx.db) {
|
|
27
|
-
throw new Error("syncTable(ctx, items) 缺少 ctx.db");
|
|
37
|
+
if (!ctx?.db) {
|
|
38
|
+
throw new Error("同步表:ctx.db 未初始化");
|
|
28
39
|
}
|
|
29
40
|
if (!ctx.redis) {
|
|
30
|
-
throw new Error("
|
|
41
|
+
throw new Error("同步表:ctx.redis 未初始化");
|
|
31
42
|
}
|
|
32
43
|
if (!ctx.config) {
|
|
33
|
-
throw new Error("
|
|
34
|
-
}
|
|
35
|
-
// DbDialect 归一化(允许值与映射关系):
|
|
36
|
-
//
|
|
37
|
-
// | ctx.config.db.type 输入 | 归一化 dbDialect |
|
|
38
|
-
// |------------------------|------------------|
|
|
39
|
-
// | mysql / 其他 / 空值 | mysql |
|
|
40
|
-
// | postgres / postgresql | postgresql |
|
|
41
|
-
// | sqlite | sqlite |
|
|
42
|
-
//
|
|
43
|
-
// 约束:后续若新增方言,必须同步更新:
|
|
44
|
-
// - 这里的归一化
|
|
45
|
-
// - ensureDbVersion / runtime I/O / DDL 分支
|
|
46
|
-
const dbType = String(ctx.config.db?.type || "mysql").toLowerCase();
|
|
47
|
-
let dbDialect = "mysql";
|
|
48
|
-
if (dbType === "postgres" || dbType === "postgresql") {
|
|
49
|
-
dbDialect = "postgresql";
|
|
44
|
+
throw new Error("同步表:ctx.config 未初始化");
|
|
50
45
|
}
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
if (!ctx.config.db?.dialect) {
|
|
47
|
+
throw new Error("同步表:ctx.config.db.dialect 缺失");
|
|
53
48
|
}
|
|
49
|
+
// DbDialect(按项目约定:正常启动时会先通过 checkConfig,因此这里直接使用配置值)
|
|
50
|
+
const dbDialect = ctx.config.db.dialect;
|
|
54
51
|
// 检查数据库版本(复用 ctx.db 的现有连接/事务)
|
|
55
52
|
await ensureDbVersion(dbDialect, ctx.db);
|
|
56
53
|
const databaseName = ctx.config.db?.database || "";
|
|
@@ -64,32 +61,17 @@ export const syncTable = (async (ctx, items) => {
|
|
|
64
61
|
if (!item || item.type !== "table") {
|
|
65
62
|
continue;
|
|
66
63
|
}
|
|
67
|
-
if (item.source !== "app" && item.source !== "addon" && item.source !== "core") {
|
|
68
|
-
Logger.warn(`syncTable 跳过未知来源表定义: source=${String(item.source)} fileName=${String(item.fileName)}`);
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
64
|
// 确定表名:
|
|
72
65
|
// - addon 表:addon_{addonName}_{fileName}
|
|
73
66
|
// - app/core 表:{fileName}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
if (item.source === "addon") {
|
|
77
|
-
if (!item.addonName || String(item.addonName).trim() === "") {
|
|
78
|
-
throw new Error(`syncTable addon 表缺少 addonName: fileName=${String(item.fileName)}`);
|
|
79
|
-
}
|
|
80
|
-
tableName = `addon_${snakeCase(item.addonName)}_${baseTableName}`;
|
|
81
|
-
}
|
|
82
|
-
const tableDefinition = item.content;
|
|
83
|
-
if (!tableDefinition || typeof tableDefinition !== "object") {
|
|
84
|
-
throw new Error(`syncTable 表定义无效: table=${tableName}`);
|
|
85
|
-
}
|
|
67
|
+
const tableName = item.source === "addon" ? `addon_${snakeCase(item.addonName)}_${snakeCase(item.fileName)}` : snakeCase(item.fileName);
|
|
68
|
+
const tableFields = item.content;
|
|
86
69
|
// 为字段属性设置默认值:表定义来自 JSON/扫描结果,字段可能缺省。
|
|
87
70
|
// 缺省会让 diff/DDL 生成出现 undefined vs null 等差异,导致错误的变更判断。
|
|
88
|
-
for (const fieldDef of Object.values(
|
|
89
|
-
|
|
71
|
+
for (const fieldDef of Object.values(tableFields)) {
|
|
72
|
+
normalizeFieldDefinitionInPlace(fieldDef);
|
|
90
73
|
}
|
|
91
74
|
const existsTable = await tableExistsRuntime(runtime, tableName);
|
|
92
|
-
const tableFields = tableDefinition;
|
|
93
75
|
if (existsTable) {
|
|
94
76
|
await modifyTableRuntime(runtime, tableName, tableFields);
|
|
95
77
|
}
|
|
@@ -205,7 +187,7 @@ const SYNC_TABLE_TEST_KIT = {
|
|
|
205
187
|
getTypeMapping: getTypeMapping,
|
|
206
188
|
quoteIdentifier: quoteIdentifier,
|
|
207
189
|
escapeComment: escapeComment,
|
|
208
|
-
|
|
190
|
+
normalizeFieldDefinitionInPlace: normalizeFieldDefinitionInPlace,
|
|
209
191
|
isStringOrArrayType: isStringOrArrayType,
|
|
210
192
|
getSqlType: getSqlType,
|
|
211
193
|
resolveDefaultValue: resolveDefaultValue,
|
|
@@ -219,13 +201,7 @@ const SYNC_TABLE_TEST_KIT = {
|
|
|
219
201
|
tableExistsRuntime: tableExistsRuntime,
|
|
220
202
|
getTableColumnsRuntime: getTableColumnsRuntime,
|
|
221
203
|
getTableIndexesRuntime: getTableIndexesRuntime,
|
|
222
|
-
createRuntime:
|
|
223
|
-
return {
|
|
224
|
-
dbDialect: dbDialect,
|
|
225
|
-
db: db,
|
|
226
|
-
dbName: dbName
|
|
227
|
-
};
|
|
228
|
-
}
|
|
204
|
+
createRuntime: createRuntimeForIO
|
|
229
205
|
};
|
|
230
206
|
// 测试能力挂载(避免导出零散函数,同时确保运行时存在)
|
|
231
207
|
syncTable.TestKit = SYNC_TABLE_TEST_KIT;
|
|
@@ -282,88 +258,19 @@ function normalizeColumnDefaultValue(value) {
|
|
|
282
258
|
// 注意:这里刻意不封装“logFieldChange/formatFieldList”之类的一次性工具函数,
|
|
283
259
|
// 以减少抽象层级(按项目要求:能直写就直写)。
|
|
284
260
|
/**
|
|
285
|
-
*
|
|
261
|
+
* 为字段定义应用默认值(就地归一化)
|
|
286
262
|
*/
|
|
287
|
-
function
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
continue;
|
|
299
|
-
if (!isJsonValue(v))
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
return true;
|
|
303
|
-
}
|
|
304
|
-
return false;
|
|
305
|
-
}
|
|
306
|
-
function applyFieldDefaults(fieldDef) {
|
|
307
|
-
if (!fieldDef || typeof fieldDef !== "object")
|
|
308
|
-
return;
|
|
309
|
-
const record = fieldDef;
|
|
310
|
-
const name = record["name"];
|
|
311
|
-
const type = record["type"];
|
|
312
|
-
if (typeof name !== "string" || typeof type !== "string")
|
|
313
|
-
return;
|
|
314
|
-
const minRaw = record["min"];
|
|
315
|
-
const maxRaw = record["max"];
|
|
316
|
-
const defaultRaw = record["default"];
|
|
317
|
-
const detailRaw = record["detail"];
|
|
318
|
-
const indexRaw = record["index"];
|
|
319
|
-
const uniqueRaw = record["unique"];
|
|
320
|
-
const nullableRaw = record["nullable"];
|
|
321
|
-
const unsignedRaw = record["unsigned"];
|
|
322
|
-
const regexpRaw = record["regexp"];
|
|
323
|
-
const input = {
|
|
324
|
-
name: name,
|
|
325
|
-
type: type
|
|
326
|
-
};
|
|
327
|
-
if (typeof detailRaw === "string") {
|
|
328
|
-
input.detail = detailRaw;
|
|
329
|
-
}
|
|
330
|
-
if (typeof minRaw === "number" || minRaw === null) {
|
|
331
|
-
input.min = minRaw;
|
|
332
|
-
}
|
|
333
|
-
if (typeof maxRaw === "number" || maxRaw === null) {
|
|
334
|
-
input.max = maxRaw;
|
|
335
|
-
}
|
|
336
|
-
if (defaultRaw === null) {
|
|
337
|
-
input.default = null;
|
|
338
|
-
}
|
|
339
|
-
else if (isJsonValue(defaultRaw)) {
|
|
340
|
-
input.default = defaultRaw;
|
|
341
|
-
}
|
|
342
|
-
if (typeof indexRaw === "boolean") {
|
|
343
|
-
input.index = indexRaw;
|
|
344
|
-
}
|
|
345
|
-
if (typeof uniqueRaw === "boolean") {
|
|
346
|
-
input.unique = uniqueRaw;
|
|
347
|
-
}
|
|
348
|
-
if (typeof nullableRaw === "boolean") {
|
|
349
|
-
input.nullable = nullableRaw;
|
|
350
|
-
}
|
|
351
|
-
if (typeof unsignedRaw === "boolean") {
|
|
352
|
-
input.unsigned = unsignedRaw;
|
|
353
|
-
}
|
|
354
|
-
if (typeof regexpRaw === "string" || regexpRaw === null) {
|
|
355
|
-
input.regexp = regexpRaw;
|
|
356
|
-
}
|
|
357
|
-
const normalized = normalizeFieldDefinition(input);
|
|
358
|
-
record["detail"] = normalized.detail;
|
|
359
|
-
record["min"] = normalized.min;
|
|
360
|
-
record["max"] = normalized.max;
|
|
361
|
-
record["default"] = normalized.default;
|
|
362
|
-
record["index"] = normalized.index;
|
|
363
|
-
record["unique"] = normalized.unique;
|
|
364
|
-
record["nullable"] = normalized.nullable;
|
|
365
|
-
record["unsigned"] = normalized.unsigned;
|
|
366
|
-
record["regexp"] = normalized.regexp;
|
|
263
|
+
function normalizeFieldDefinitionInPlace(fieldDef) {
|
|
264
|
+
const normalized = normalizeFieldDefinition(fieldDef);
|
|
265
|
+
fieldDef.detail = normalized.detail;
|
|
266
|
+
fieldDef.min = normalized.min;
|
|
267
|
+
fieldDef.max = normalized.max;
|
|
268
|
+
fieldDef.default = normalized.default;
|
|
269
|
+
fieldDef.index = normalized.index;
|
|
270
|
+
fieldDef.unique = normalized.unique;
|
|
271
|
+
fieldDef.nullable = normalized.nullable;
|
|
272
|
+
fieldDef.unsigned = normalized.unsigned;
|
|
273
|
+
fieldDef.regexp = normalized.regexp;
|
|
367
274
|
}
|
|
368
275
|
/**
|
|
369
276
|
* 判断是否为字符串或数组类型(需要长度参数)
|
|
@@ -620,41 +527,45 @@ function isCompatibleTypeChange(currentType, newType) {
|
|
|
620
527
|
// 读:表是否存在
|
|
621
528
|
// ---------------------------------------------------------------------------
|
|
622
529
|
async function tableExistsRuntime(runtime, tableName) {
|
|
623
|
-
const db = runtime.db;
|
|
624
|
-
if (!db)
|
|
625
|
-
throw new Error("SQL 执行器未初始化");
|
|
626
530
|
try {
|
|
627
531
|
// 统一交由方言层构造 SQL;syncTable 仅决定“要查哪个 schema/db”。
|
|
628
532
|
// - MySQL:传 runtime.dbName(information_schema.table_schema)
|
|
629
533
|
// - PostgreSQL:固定 public(项目约定)
|
|
630
534
|
// - SQLite:忽略 schema
|
|
631
|
-
|
|
632
|
-
if (runtime.dbDialect === "mysql") {
|
|
633
|
-
schema = runtime.dbName;
|
|
634
|
-
}
|
|
635
|
-
else if (runtime.dbDialect === "postgresql") {
|
|
636
|
-
schema = "public";
|
|
637
|
-
}
|
|
535
|
+
const schema = runtime.dbDialect === "mysql" ? runtime.dbName : runtime.dbDialect === "postgresql" ? "public" : undefined;
|
|
638
536
|
const q = getDialectByName(runtime.dbDialect).tableExistsQuery(tableName, schema);
|
|
639
|
-
const res = await db.unsafe(q.sql, q.params);
|
|
537
|
+
const res = await runtime.db.unsafe(q.sql, q.params);
|
|
640
538
|
return (res.data?.[0]?.count || 0) > 0;
|
|
641
539
|
}
|
|
642
540
|
catch (error) {
|
|
643
|
-
|
|
644
|
-
const outErr = new Error(`runtime I/O 失败: op=tableExists table=${tableName} err=${errMsg}`);
|
|
645
|
-
if (error?.sqlInfo)
|
|
646
|
-
outErr.sqlInfo = error.sqlInfo;
|
|
647
|
-
throw outErr;
|
|
541
|
+
throw buildRuntimeIoError("检查表是否存在", tableName, error);
|
|
648
542
|
}
|
|
649
543
|
}
|
|
650
544
|
// ---------------------------------------------------------------------------
|
|
651
545
|
// 读:列信息
|
|
652
546
|
// ---------------------------------------------------------------------------
|
|
653
547
|
async function getTableColumnsRuntime(runtime, tableName) {
|
|
548
|
+
// 返回的列数据示例
|
|
549
|
+
// [{
|
|
550
|
+
// is_system: {
|
|
551
|
+
// type: "bigint",
|
|
552
|
+
// columnType: "bigint unsigned",
|
|
553
|
+
// length: null,
|
|
554
|
+
// max: null,
|
|
555
|
+
// nullable: false,
|
|
556
|
+
// defaultValue: "0",
|
|
557
|
+
// comment: '',
|
|
558
|
+
// },
|
|
559
|
+
// description: {
|
|
560
|
+
// type: "varchar",
|
|
561
|
+
// columnType: "varchar(500)",
|
|
562
|
+
// length: 500,
|
|
563
|
+
// max: 500,
|
|
564
|
+
// nullable: false,
|
|
565
|
+
// defaultValue: "",
|
|
566
|
+
// comment: '',
|
|
567
|
+
// }]
|
|
654
568
|
const columns = {};
|
|
655
|
-
const db = runtime.db;
|
|
656
|
-
if (!db)
|
|
657
|
-
throw new Error("SQL 执行器未初始化");
|
|
658
569
|
try {
|
|
659
570
|
// 方言差异说明:
|
|
660
571
|
// - MySQL:information_schema.columns 最完整,包含 COLUMN_TYPE 与 COLUMN_COMMENT。
|
|
@@ -662,31 +573,32 @@ async function getTableColumnsRuntime(runtime, tableName) {
|
|
|
662
573
|
// - SQLite:PRAGMA table_info 仅提供 type/notnull/default 等有限信息,无列注释。
|
|
663
574
|
if (runtime.dbDialect === "mysql") {
|
|
664
575
|
const q = getSyncTableColumnsInfoQuery({ dialect: "mysql", table: tableName, dbName: runtime.dbName });
|
|
665
|
-
const result = await db.unsafe(q.columns.sql, q.columns.params);
|
|
576
|
+
const result = await runtime.db.unsafe(q.columns.sql, q.columns.params);
|
|
666
577
|
for (const row of result.data) {
|
|
667
578
|
const defaultValue = normalizeColumnDefaultValue(row.COLUMN_DEFAULT);
|
|
668
579
|
columns[row.COLUMN_NAME] = {
|
|
669
|
-
|
|
670
|
-
|
|
580
|
+
// 防御性:某些 driver/编码设置可能导致字符串字段不是 string(如 Buffer/number/null)。
|
|
581
|
+
type: String(row.DATA_TYPE ?? ""),
|
|
582
|
+
columnType: String(row.COLUMN_TYPE ?? ""),
|
|
671
583
|
length: row.CHARACTER_MAXIMUM_LENGTH,
|
|
672
584
|
max: row.CHARACTER_MAXIMUM_LENGTH,
|
|
673
585
|
nullable: row.IS_NULLABLE === "YES",
|
|
674
586
|
defaultValue: defaultValue,
|
|
675
|
-
comment: row.COLUMN_COMMENT
|
|
587
|
+
comment: row.COLUMN_COMMENT === null ? null : String(row.COLUMN_COMMENT)
|
|
676
588
|
};
|
|
677
589
|
}
|
|
678
590
|
}
|
|
679
591
|
else if (runtime.dbDialect === "postgresql") {
|
|
680
592
|
const q = getSyncTableColumnsInfoQuery({ dialect: "postgresql", table: tableName, dbName: runtime.dbName });
|
|
681
|
-
const result = await db.unsafe(q.columns.sql, q.columns.params);
|
|
682
|
-
const comments = q.comments ? (await db.unsafe(q.comments.sql, q.comments.params)).data : [];
|
|
593
|
+
const result = await runtime.db.unsafe(q.columns.sql, q.columns.params);
|
|
594
|
+
const comments = q.comments ? (await runtime.db.unsafe(q.comments.sql, q.comments.params)).data : [];
|
|
683
595
|
const commentMap = {};
|
|
684
596
|
for (const r of comments)
|
|
685
597
|
commentMap[r.column_name] = r.column_comment;
|
|
686
598
|
for (const row of result.data) {
|
|
687
599
|
columns[row.column_name] = {
|
|
688
|
-
type: row.data_type,
|
|
689
|
-
columnType: row.data_type,
|
|
600
|
+
type: String(row.data_type ?? ""),
|
|
601
|
+
columnType: String(row.data_type ?? ""),
|
|
690
602
|
length: row.character_maximum_length,
|
|
691
603
|
max: row.character_maximum_length,
|
|
692
604
|
nullable: String(row.is_nullable).toUpperCase() === "YES",
|
|
@@ -697,24 +609,18 @@ async function getTableColumnsRuntime(runtime, tableName) {
|
|
|
697
609
|
}
|
|
698
610
|
else if (runtime.dbDialect === "sqlite") {
|
|
699
611
|
const q = getSyncTableColumnsInfoQuery({ dialect: "sqlite", table: tableName, dbName: runtime.dbName });
|
|
700
|
-
const result = await db.unsafe(q.columns.sql, q.columns.params);
|
|
612
|
+
const result = await runtime.db.unsafe(q.columns.sql, q.columns.params);
|
|
701
613
|
for (const row of result.data) {
|
|
702
614
|
let baseType = String(row.type || "").toUpperCase();
|
|
703
615
|
let max = null;
|
|
704
616
|
const m = /^(\w+)\s*\((\d+)\)/.exec(baseType);
|
|
705
|
-
if (m) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
if (typeof base === "string") {
|
|
709
|
-
baseType = base;
|
|
710
|
-
}
|
|
711
|
-
if (typeof maxText === "string") {
|
|
712
|
-
max = Number(maxText);
|
|
713
|
-
}
|
|
617
|
+
if (m && m[1] && m[2]) {
|
|
618
|
+
baseType = m[1];
|
|
619
|
+
max = Number(m[2]);
|
|
714
620
|
}
|
|
715
621
|
columns[row.name] = {
|
|
716
|
-
type: baseType.toLowerCase(),
|
|
717
|
-
columnType: baseType.toLowerCase(),
|
|
622
|
+
type: String(baseType).toLowerCase(),
|
|
623
|
+
columnType: String(baseType).toLowerCase(),
|
|
718
624
|
length: max,
|
|
719
625
|
max: max,
|
|
720
626
|
nullable: row.notnull === 0,
|
|
@@ -726,21 +632,22 @@ async function getTableColumnsRuntime(runtime, tableName) {
|
|
|
726
632
|
return columns;
|
|
727
633
|
}
|
|
728
634
|
catch (error) {
|
|
729
|
-
|
|
730
|
-
const outErr = new Error(`runtime I/O 失败: op=getTableColumns table=${tableName} err=${errMsg}`);
|
|
731
|
-
if (error?.sqlInfo)
|
|
732
|
-
outErr.sqlInfo = error.sqlInfo;
|
|
733
|
-
throw outErr;
|
|
635
|
+
throw buildRuntimeIoError("读取列信息", tableName, error);
|
|
734
636
|
}
|
|
735
637
|
}
|
|
736
638
|
// ---------------------------------------------------------------------------
|
|
737
639
|
// 读:索引信息(单列索引)
|
|
738
640
|
// ---------------------------------------------------------------------------
|
|
739
641
|
async function getTableIndexesRuntime(runtime, tableName) {
|
|
642
|
+
// 索引返回示例
|
|
643
|
+
// {
|
|
644
|
+
// code: [ "code" ],
|
|
645
|
+
// idx_created_at: [ "created_at" ],
|
|
646
|
+
// idx_group: [ "group" ],
|
|
647
|
+
// idx_state: [ "state" ],
|
|
648
|
+
// idx_updated_at: [ "updated_at" ],
|
|
649
|
+
// }
|
|
740
650
|
const indexes = {};
|
|
741
|
-
const db = runtime.db;
|
|
742
|
-
if (!db)
|
|
743
|
-
throw new Error("SQL 执行器未初始化");
|
|
744
651
|
try {
|
|
745
652
|
// 方言差异说明:
|
|
746
653
|
// - MySQL:information_schema.statistics 直接给出 index -> column 映射。
|
|
@@ -748,7 +655,7 @@ async function getTableIndexesRuntime(runtime, tableName) {
|
|
|
748
655
|
// - SQLite:PRAGMA index_list + index_info;同样仅收集单列索引,避免多列索引误判。
|
|
749
656
|
if (runtime.dbDialect === "mysql") {
|
|
750
657
|
const q = getSyncTableIndexesQuery({ dialect: "mysql", table: tableName, dbName: runtime.dbName });
|
|
751
|
-
const result = await db.unsafe(q.sql, q.params);
|
|
658
|
+
const result = await runtime.db.unsafe(q.sql, q.params);
|
|
752
659
|
for (const row of result.data) {
|
|
753
660
|
const indexName = row.INDEX_NAME;
|
|
754
661
|
const current = indexes[indexName];
|
|
@@ -762,7 +669,7 @@ async function getTableIndexesRuntime(runtime, tableName) {
|
|
|
762
669
|
}
|
|
763
670
|
else if (runtime.dbDialect === "postgresql") {
|
|
764
671
|
const q = getSyncTableIndexesQuery({ dialect: "postgresql", table: tableName, dbName: runtime.dbName });
|
|
765
|
-
const result = await db.unsafe(q.sql, q.params);
|
|
672
|
+
const result = await runtime.db.unsafe(q.sql, q.params);
|
|
766
673
|
for (const row of result.data) {
|
|
767
674
|
const m = /\(([^)]+)\)/.exec(row.indexdef);
|
|
768
675
|
if (m) {
|
|
@@ -774,10 +681,10 @@ async function getTableIndexesRuntime(runtime, tableName) {
|
|
|
774
681
|
}
|
|
775
682
|
else if (runtime.dbDialect === "sqlite") {
|
|
776
683
|
const quotedTable = quoteIdentifier("sqlite", tableName);
|
|
777
|
-
const list = await db.unsafe(`PRAGMA index_list(${quotedTable})`);
|
|
684
|
+
const list = await runtime.db.unsafe(`PRAGMA index_list(${quotedTable})`);
|
|
778
685
|
for (const idx of list.data) {
|
|
779
686
|
const quotedIndex = quoteIdentifier("sqlite", idx.name);
|
|
780
|
-
const info = await db.unsafe(`PRAGMA index_info(${quotedIndex})`);
|
|
687
|
+
const info = await runtime.db.unsafe(`PRAGMA index_info(${quotedIndex})`);
|
|
781
688
|
const cols = info.data.map((r) => r.name);
|
|
782
689
|
if (cols.length === 1)
|
|
783
690
|
indexes[idx.name] = cols;
|
|
@@ -786,11 +693,7 @@ async function getTableIndexesRuntime(runtime, tableName) {
|
|
|
786
693
|
return indexes;
|
|
787
694
|
}
|
|
788
695
|
catch (error) {
|
|
789
|
-
|
|
790
|
-
const outErr = new Error(`runtime I/O 失败: op=getTableIndexes table=${tableName} err=${errMsg}`);
|
|
791
|
-
if (error?.sqlInfo)
|
|
792
|
-
outErr.sqlInfo = error.sqlInfo;
|
|
793
|
-
throw outErr;
|
|
696
|
+
throw buildRuntimeIoError("读取索引信息", tableName, error);
|
|
794
697
|
}
|
|
795
698
|
}
|
|
796
699
|
// ---------------------------------------------------------------------------
|
|
@@ -800,39 +703,37 @@ async function getTableIndexesRuntime(runtime, tableName) {
|
|
|
800
703
|
* 数据库版本检查(按方言)
|
|
801
704
|
*/
|
|
802
705
|
async function ensureDbVersion(dbDialect, db) {
|
|
803
|
-
if (!db)
|
|
804
|
-
throw new Error("SQL 执行器未初始化");
|
|
805
706
|
if (dbDialect === "mysql") {
|
|
806
707
|
const r = await db.unsafe("SELECT VERSION() AS version");
|
|
807
708
|
if (!r.data || r.data.length === 0 || !r.data[0]?.version) {
|
|
808
|
-
throw new Error("
|
|
709
|
+
throw new Error("同步表:无法获取 MySQL 版本信息");
|
|
809
710
|
}
|
|
810
711
|
const version = r.data[0].version;
|
|
811
712
|
const majorPart = String(version).split(".")[0] || "0";
|
|
812
713
|
const majorVersion = parseInt(majorPart, 10);
|
|
813
714
|
if (!Number.isFinite(majorVersion) || majorVersion < DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR) {
|
|
814
|
-
throw new Error(
|
|
715
|
+
throw new Error(`同步表:仅支持 MySQL ${DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR}.0+(当前版本:${version})`);
|
|
815
716
|
}
|
|
816
717
|
return;
|
|
817
718
|
}
|
|
818
719
|
if (dbDialect === "postgresql") {
|
|
819
720
|
const r = await db.unsafe("SELECT version() AS version");
|
|
820
721
|
if (!r.data || r.data.length === 0 || !r.data[0]?.version) {
|
|
821
|
-
throw new Error("
|
|
722
|
+
throw new Error("同步表:无法获取 PostgreSQL 版本信息");
|
|
822
723
|
}
|
|
823
724
|
const versionText = r.data[0].version;
|
|
824
725
|
const m = /PostgreSQL\s+(\d+)/i.exec(versionText);
|
|
825
726
|
const majorText = m ? m[1] : undefined;
|
|
826
727
|
const major = typeof majorText === "string" ? parseInt(majorText, 10) : NaN;
|
|
827
728
|
if (!Number.isFinite(major) || major < DB_VERSION_REQUIREMENTS.POSTGRES_MIN_MAJOR) {
|
|
828
|
-
throw new Error(
|
|
729
|
+
throw new Error(`同步表:要求 PostgreSQL >= ${DB_VERSION_REQUIREMENTS.POSTGRES_MIN_MAJOR}(当前:${versionText})`);
|
|
829
730
|
}
|
|
830
731
|
return;
|
|
831
732
|
}
|
|
832
733
|
if (dbDialect === "sqlite") {
|
|
833
734
|
const r = await db.unsafe("SELECT sqlite_version() AS version");
|
|
834
735
|
if (!r.data || r.data.length === 0 || !r.data[0]?.version) {
|
|
835
|
-
throw new Error("
|
|
736
|
+
throw new Error("同步表:无法获取 SQLite 版本信息");
|
|
836
737
|
}
|
|
837
738
|
const version = r.data[0].version;
|
|
838
739
|
const parts = String(version)
|
|
@@ -843,7 +744,7 @@ async function ensureDbVersion(dbDialect, db) {
|
|
|
843
744
|
const patch = parts[2] ?? 0;
|
|
844
745
|
const vnum = maj * 10000 + min * 100 + patch;
|
|
845
746
|
if (!Number.isFinite(vnum) || vnum < DB_VERSION_REQUIREMENTS.SQLITE_MIN_VERSION_NUM) {
|
|
846
|
-
throw new Error(
|
|
747
|
+
throw new Error(`同步表:要求 SQLite >= ${DB_VERSION_REQUIREMENTS.SQLITE_MIN_VERSION}(当前:${version})`);
|
|
847
748
|
}
|
|
848
749
|
return;
|
|
849
750
|
}
|
|
@@ -869,7 +770,7 @@ function compareFieldDefinition(dbDialect, existingColumn, fieldDef) {
|
|
|
869
770
|
}
|
|
870
771
|
}
|
|
871
772
|
if (dbDialect !== "sqlite") {
|
|
872
|
-
const currentComment = existingColumn.comment || "";
|
|
773
|
+
const currentComment = existingColumn.comment === null || existingColumn.comment === undefined ? "" : String(existingColumn.comment);
|
|
873
774
|
if (currentComment !== normalized.name) {
|
|
874
775
|
changes.push({
|
|
875
776
|
type: "comment",
|
|
@@ -881,10 +782,26 @@ function compareFieldDefinition(dbDialect, existingColumn, fieldDef) {
|
|
|
881
782
|
const typeMapping = getTypeMapping(dbDialect);
|
|
882
783
|
const mapped = typeMapping[normalized.type];
|
|
883
784
|
if (typeof mapped !== "string") {
|
|
884
|
-
throw new Error(
|
|
785
|
+
throw new Error(`同步表:未知字段类型映射(方言=${dbDialect},类型=${String(normalized.type)})`);
|
|
885
786
|
}
|
|
886
787
|
const expectedType = mapped.toLowerCase();
|
|
887
|
-
|
|
788
|
+
// 防御性:理论上 ColumnInfo.type/columnType 都应为 string,但线上偶发出现 number/null/Buffer 等导致崩溃。
|
|
789
|
+
// 同时:columnType 可能包含长度/unsigned(如 varchar(255), bigint unsigned),这里归一化为“基础类型”再比较。
|
|
790
|
+
let rawType = "";
|
|
791
|
+
if (typeof existingColumn.type === "string" && existingColumn.type.trim() !== "") {
|
|
792
|
+
rawType = existingColumn.type;
|
|
793
|
+
}
|
|
794
|
+
else if (typeof existingColumn.columnType === "string" && existingColumn.columnType.trim() !== "") {
|
|
795
|
+
rawType = existingColumn.columnType;
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
rawType = String(existingColumn.type ?? "");
|
|
799
|
+
}
|
|
800
|
+
const currentType = rawType
|
|
801
|
+
.toLowerCase()
|
|
802
|
+
.replace(/\s*unsigned/gi, "")
|
|
803
|
+
.replace(/\([^)]*\)/g, "")
|
|
804
|
+
.trim();
|
|
888
805
|
if (currentType !== expectedType) {
|
|
889
806
|
changes.push({
|
|
890
807
|
type: "datatype",
|
|
@@ -922,7 +839,7 @@ async function rebuildSqliteTable(runtime, tableName, fields) {
|
|
|
922
839
|
// - 只复制 targetCols 与 existingCols 的交集,避免因新增列/删除列导致 INSERT 失败。
|
|
923
840
|
// - 不做额外的数据转换/回填:保持迁移路径尽量“纯结构同步”。
|
|
924
841
|
if (runtime.dbDialect !== "sqlite") {
|
|
925
|
-
throw new Error(
|
|
842
|
+
throw new Error(`同步表:SQLite 重建表仅支持 sqlite 方言(当前:${String(runtime.dbDialect)})`);
|
|
926
843
|
}
|
|
927
844
|
const quotedSourceTable = quoteIdentifier("sqlite", tableName);
|
|
928
845
|
const info = await runtime.db.unsafe(`PRAGMA table_info(${quotedSourceTable})`);
|
package/dist/types/befly.d.ts
CHANGED
|
@@ -21,8 +21,8 @@ export type BeflyRuntimeEnv = Record<string, string | undefined>;
|
|
|
21
21
|
export interface DatabaseConfig {
|
|
22
22
|
/** 是否启用数据库 (0: 关闭, 1: 开启) @default 0 */
|
|
23
23
|
enable?: number;
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/** 数据库方言 ('mysql' | 'postgresql' | 'sqlite') @default 'sqlite' */
|
|
25
|
+
dialect?: "mysql" | "postgresql" | "sqlite";
|
|
26
26
|
/** 数据库主机 @default '127.0.0.1' */
|
|
27
27
|
host?: string;
|
|
28
28
|
/** 数据库端口 @default 3306 */
|
package/dist/types/database.d.ts
CHANGED
package/dist/types/table.d.ts
CHANGED
package/dist/types/validate.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.15.
|
|
4
|
-
"gitHead": "
|
|
3
|
+
"version": "3.15.1",
|
|
4
|
+
"gitHead": "540d9551c589cb33d711c593e561f0fffc0aac0c",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
|
|
7
7
|
"keywords": [
|