befly 3.76.3 → 3.76.5
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/libs/logger/logger.js +2 -1
- package/package.json +1 -1
- package/scripts/syncDb/index.js +16 -14
- package/scripts/syncDb/model.js +19 -7
- package/scripts/syncDb/plan.js +4 -2
- package/scripts/syncDb/report.js +36 -5
- package/scripts/syncDb/snapshot.js +12 -2
package/libs/logger/logger.js
CHANGED
|
@@ -132,6 +132,7 @@ function resetConfig(options = {}) {
|
|
|
132
132
|
debug: options.debug ?? 0,
|
|
133
133
|
dir: dir,
|
|
134
134
|
runtimeEnv: runtimeEnv,
|
|
135
|
+
clearDevelopmentLog: options.clearDevelopmentLog !== false,
|
|
135
136
|
instanceId: Bun.env.BM2_INSTANCE_ID || Bun.env.BM2_APP_INSTANCE || "",
|
|
136
137
|
maxBytes: maxSize * 1024 * 1024
|
|
137
138
|
};
|
|
@@ -227,7 +228,7 @@ export const Logger = {
|
|
|
227
228
|
errorSink = null;
|
|
228
229
|
for (const sink of sinks) sink.shutdown().catch((error) => writeStderr(`关闭失败: ${error.message || error}`));
|
|
229
230
|
resetConfig(options);
|
|
230
|
-
prepareDirectory(
|
|
231
|
+
prepareDirectory(config.clearDevelopmentLog);
|
|
231
232
|
},
|
|
232
233
|
setMock: function (mock) {
|
|
233
234
|
mockInstance = mock;
|
package/package.json
CHANGED
package/scripts/syncDb/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { isPlainObject } from "#befly/utils/is.js";
|
|
|
5
5
|
import { scanTables } from "#befly/utils/scanSources.js";
|
|
6
6
|
|
|
7
7
|
import { applySchemaPlan, createSyncDbClient } from "./apply.js";
|
|
8
|
-
import { buildTargetDatabaseModel } from "./model.js";
|
|
8
|
+
import { buildTargetDatabaseModel, selectTables } from "./model.js";
|
|
9
9
|
import { buildSchemaPlan } from "./plan.js";
|
|
10
10
|
import { runPreflight } from "./preflight.js";
|
|
11
11
|
import { printPreflightFailures, printSyncPlan, writeSyncArchive } from "./report.js";
|
|
@@ -20,9 +20,9 @@ function nowText() {
|
|
|
20
20
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
async function buildPlan(mysql, targetTables,
|
|
24
|
-
const snapshot = await readDatabaseSnapshot(mysql);
|
|
25
|
-
const plan = buildSchemaPlan(targetTables, snapshot.tables, {
|
|
23
|
+
async function buildPlan(mysql, targetTables, tableNames) {
|
|
24
|
+
const snapshot = await readDatabaseSnapshot(mysql, { tableNames: tableNames.length > 0 ? Object.keys(targetTables) : [] });
|
|
25
|
+
const plan = buildSchemaPlan(targetTables, snapshot.tables, { tableNames: tableNames });
|
|
26
26
|
await runPreflight(mysql, plan);
|
|
27
27
|
return { mysqlVersion: snapshot.mysqlVersion, plan: plan };
|
|
28
28
|
}
|
|
@@ -30,14 +30,15 @@ async function buildPlan(mysql, targetTables, singleTable) {
|
|
|
30
30
|
export async function syncDb(mysqlConfig, options = {}) {
|
|
31
31
|
const apply = options.apply === true;
|
|
32
32
|
const list = options.list === true;
|
|
33
|
-
const
|
|
33
|
+
const tableNames = Array.isArray(options.tableNames) ? options.tableNames.filter(Boolean) : options.tableName ? [options.tableName] : [];
|
|
34
|
+
const tableScope = tableNames.length > 0 ? tableNames.join(", ") : "";
|
|
34
35
|
const startedAt = nowText();
|
|
35
36
|
let client;
|
|
36
37
|
let targetTables;
|
|
37
38
|
let archivePath = "";
|
|
38
39
|
let context = {
|
|
39
40
|
apply: apply,
|
|
40
|
-
tableName:
|
|
41
|
+
tableName: tableScope,
|
|
41
42
|
hostname: mysqlConfig?.hostname,
|
|
42
43
|
port: mysqlConfig?.port,
|
|
43
44
|
username: mysqlConfig?.username,
|
|
@@ -59,13 +60,14 @@ export async function syncDb(mysqlConfig, options = {}) {
|
|
|
59
60
|
process.stdout.write(`[syncDb] 用户名:${mysqlConfig.username}\n`);
|
|
60
61
|
process.stdout.write(`[syncDb] 数据库名:${mysqlConfig.database}\n\n`);
|
|
61
62
|
if (apply) archivePath = await writeSyncArchive(context);
|
|
62
|
-
const
|
|
63
|
-
const
|
|
63
|
+
const allTables = await scanTables({ beflyMode: mysqlConfig.beflyMode });
|
|
64
|
+
const selectedTables = selectTables(allTables, tableNames);
|
|
65
|
+
const tableErrors = await checkTable(selectedTables);
|
|
64
66
|
if (tableErrors.length > 0) throw createError("Tables 定义校验失败", { code: "policy", subsystem: "scripts", operation: "syncDb", errors: tableErrors });
|
|
65
67
|
|
|
66
|
-
targetTables = buildTargetDatabaseModel(
|
|
68
|
+
targetTables = buildTargetDatabaseModel(selectedTables);
|
|
67
69
|
client = createSyncDbClient(mysqlConfig);
|
|
68
|
-
const built = await buildPlan(client, targetTables,
|
|
70
|
+
const built = await buildPlan(client, targetTables, tableNames);
|
|
69
71
|
context = {
|
|
70
72
|
...context,
|
|
71
73
|
hostname: mysqlConfig.hostname,
|
|
@@ -113,7 +115,7 @@ export async function syncDb(mysqlConfig, options = {}) {
|
|
|
113
115
|
process.stdout.write(`[syncDb] 同步存档:${archivePath}\n`);
|
|
114
116
|
const executableOperations = context.plan.operations.filter((operation) => operation.preflight?.blocked !== true);
|
|
115
117
|
if (executableOperations.length === 0) {
|
|
116
|
-
const finalBuilt = await buildPlan(client, targetTables,
|
|
118
|
+
const finalBuilt = await buildPlan(client, targetTables, tableNames);
|
|
117
119
|
context.remainingPlan = finalBuilt.plan;
|
|
118
120
|
context.mysqlVersion = finalBuilt.mysqlVersion;
|
|
119
121
|
context.status = finalBuilt.plan.blocked.length > 0 ? "blocked" : "no-change";
|
|
@@ -138,9 +140,9 @@ export async function syncDb(mysqlConfig, options = {}) {
|
|
|
138
140
|
context.results = await applySchemaPlan(client, {
|
|
139
141
|
database: mysqlConfig.database,
|
|
140
142
|
plan: context.plan,
|
|
141
|
-
verify: async (connection) => (await buildPlan(connection, targetTables,
|
|
143
|
+
verify: async (connection) => (await buildPlan(connection, targetTables, tableNames)).plan
|
|
142
144
|
});
|
|
143
|
-
const finalBuilt = await buildPlan(client, targetTables,
|
|
145
|
+
const finalBuilt = await buildPlan(client, targetTables, tableNames);
|
|
144
146
|
context.remainingPlan = finalBuilt.plan;
|
|
145
147
|
context.mysqlVersion = finalBuilt.mysqlVersion;
|
|
146
148
|
context.status = finalBuilt.plan.operations.length === 0 && finalBuilt.plan.blocked.length === 0 ? "applied" : "partial";
|
|
@@ -169,7 +171,7 @@ export async function syncDb(mysqlConfig, options = {}) {
|
|
|
169
171
|
context.error = String(error?.message || error);
|
|
170
172
|
if (client && targetTables && context.results?.length > 0) {
|
|
171
173
|
try {
|
|
172
|
-
context.remainingPlan = (await buildPlan(client, targetTables,
|
|
174
|
+
context.remainingPlan = (await buildPlan(client, targetTables, tableNames)).plan;
|
|
173
175
|
} catch {
|
|
174
176
|
// 保留原始执行错误,存档沿用执行前计划
|
|
175
177
|
}
|
package/scripts/syncDb/model.js
CHANGED
|
@@ -22,15 +22,27 @@ function buildColumn(fieldName, field) {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
function normalizeTableNames(tableNames) {
|
|
26
|
+
const names = typeof tableNames === "string" ? [tableNames] : Array.isArray(tableNames) ? tableNames : [];
|
|
27
|
+
return [...new Set(names.filter(Boolean))];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function selectTables(tables, tableNames = []) {
|
|
31
|
+
const selectedNames = normalizeTableNames(tableNames);
|
|
32
|
+
if (selectedNames.length === 0) return tables;
|
|
33
|
+
|
|
34
|
+
const selectedNameSet = new Set(selectedNames);
|
|
35
|
+
const selectedTables = Object.fromEntries(Object.entries(tables).filter(([key, table]) => selectedNameSet.has(key) || selectedNameSet.has(table.codeName) || selectedNameSet.has(table.fileName)));
|
|
36
|
+
const matchedNames = new Set(Object.values(selectedTables).flatMap((table) => [table.codeName, table.fileName]));
|
|
37
|
+
const missingName = selectedNames.find((name) => !matchedNames.has(name));
|
|
38
|
+
if (missingName) throw new TypeError(`找不到 Table 定义:${missingName}`);
|
|
39
|
+
return selectedTables;
|
|
40
|
+
}
|
|
31
41
|
|
|
42
|
+
export function buildTargetDatabaseModel(tables, tableNames = []) {
|
|
43
|
+
const selectedTables = selectTables(tables, tableNames);
|
|
32
44
|
return Object.fromEntries(
|
|
33
|
-
selectedTables
|
|
45
|
+
Object.values(selectedTables)
|
|
34
46
|
.toSorted((left, right) => snakeCase(left.codeName).localeCompare(snakeCase(right.codeName)))
|
|
35
47
|
.map((table) => [
|
|
36
48
|
snakeCase(table.codeName),
|
package/scripts/syncDb/plan.js
CHANGED
|
@@ -67,7 +67,9 @@ export function updatePlanHash(plan) {
|
|
|
67
67
|
return plan;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
export function buildSchemaPlan(targetTables, actualTables, { singleTable = false } = {}) {
|
|
70
|
+
export function buildSchemaPlan(targetTables, actualTables, { tableNames = [], singleTable = false } = {}) {
|
|
71
|
+
const selectedTableNames = Array.isArray(tableNames) ? tableNames : tableNames ? [tableNames] : [];
|
|
72
|
+
const restrictUnmanagedTables = selectedTableNames.length > 0 || singleTable;
|
|
71
73
|
const plan = { operations: [], notices: [], blocked: [], counts: {} };
|
|
72
74
|
|
|
73
75
|
for (const target of Object.values(targetTables)) {
|
|
@@ -138,7 +140,7 @@ export function buildSchemaPlan(targetTables, actualTables, { singleTable = fals
|
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
142
|
|
|
141
|
-
if (!
|
|
143
|
+
if (!restrictUnmanagedTables) {
|
|
142
144
|
for (const actual of Object.values(actualTables)) {
|
|
143
145
|
if (!targetTables[actual.dbName]) addNotice(plan, { kind: "unmanagedTable", table: actual.dbName, message: "表仅存在于 MySQL,保留且不受同步管理" });
|
|
144
146
|
}
|
package/scripts/syncDb/report.js
CHANGED
|
@@ -39,14 +39,39 @@ function colorText(text, color, enabled) {
|
|
|
39
39
|
return ansi ? `${ansi}${text}\x1b[0m` : text;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
const fieldTypeProperties = new Set(["fieldType", "maxValue", "precision", "scale"]);
|
|
43
|
+
|
|
42
44
|
function targetFieldTypeText(field) {
|
|
43
45
|
if (!field || typeof field !== "object") return "-";
|
|
44
46
|
return field.fieldType === "integer" ? "BIGINT" : field.fieldType === "number" ? `DECIMAL(${field.precision},${field.scale})` : field.fieldType === "varchar" ? `VARCHAR(${field.maxValue})` : field.fieldType === "text" ? "MEDIUMTEXT" : "-";
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
function actualFieldTypeText(field) {
|
|
50
|
+
if (!field || typeof field !== "object") return "-";
|
|
51
|
+
const dbType = String(field.dbType || "").toLowerCase();
|
|
52
|
+
if (["tinyint", "smallint", "mediumint", "int", "bigint"].includes(dbType)) return dbType.toUpperCase();
|
|
53
|
+
if (["decimal", "numeric"].includes(dbType)) return `${dbType.toUpperCase()}(${field.precision},${field.scale})`;
|
|
54
|
+
if (["varchar", "char"].includes(dbType)) return `${dbType.toUpperCase()}(${field.maxValue})`;
|
|
55
|
+
if (dbType) return dbType.toUpperCase();
|
|
56
|
+
return targetFieldTypeText(field);
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
function fieldTypeText(operation) {
|
|
48
60
|
if (!["addColumn", "alterColumn"].includes(operation.kind)) return "-";
|
|
49
|
-
|
|
61
|
+
const after = targetFieldTypeText(operation.after);
|
|
62
|
+
if (operation.kind === "addColumn" || !operation.before) return after;
|
|
63
|
+
const before = actualFieldTypeText(operation.before);
|
|
64
|
+
return before === "-" || before === after ? after : `${before} -> ${after}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function blockedFieldTypeText(blocked) {
|
|
68
|
+
const target = blocked.target;
|
|
69
|
+
if (target && typeof target === "object" && target.fieldType && typeof blocked.before === "string") {
|
|
70
|
+
const before = blocked.before.toUpperCase();
|
|
71
|
+
const after = targetFieldTypeText(target);
|
|
72
|
+
return before === after ? after : `${before} -> ${after}`;
|
|
73
|
+
}
|
|
74
|
+
return targetFieldTypeText(target || blocked.after);
|
|
50
75
|
}
|
|
51
76
|
|
|
52
77
|
function preflightResult(operation, colors) {
|
|
@@ -70,13 +95,14 @@ function buildSyncRows(plan, colors) {
|
|
|
70
95
|
}));
|
|
71
96
|
for (const notice of plan.notices.filter((item) => !createdTables.has(item.table))) rows.push({ 类型: colorText("保留", "deepskyblue", colors), 风险: "-", 表: notice.table, 对象: notice.object || "-", 字段类型: "-", 变更: notice.message, 预检结果: "-" });
|
|
72
97
|
for (const blocked of plan.blocked.filter((item) => !createdTables.has(item.table))) {
|
|
73
|
-
const
|
|
98
|
+
const hasBooleanCompare = typeof blocked.before === "boolean" || typeof blocked.after === "boolean";
|
|
99
|
+
const change = blocked.kind === "preflight" || hasBooleanCompare ? blocked.reason : blocked.before !== undefined || blocked.after !== undefined ? `${formatValue(blocked.before)} -> ${formatValue(blocked.after)}` : blocked.reason;
|
|
74
100
|
rows.push({
|
|
75
101
|
类型: colorText("禁止同步", "orangered", colors),
|
|
76
102
|
风险: colorText("阻塞", "orangered", colors),
|
|
77
103
|
表: blocked.table,
|
|
78
104
|
对象: blocked.object || "-",
|
|
79
|
-
字段类型:
|
|
105
|
+
字段类型: blockedFieldTypeText(blocked),
|
|
80
106
|
变更: change,
|
|
81
107
|
预检结果: blocked.kind === "preflight" ? colorText("否", "red", colors) : "-"
|
|
82
108
|
});
|
|
@@ -139,11 +165,16 @@ function escapeTableValue(value) {
|
|
|
139
165
|
|
|
140
166
|
function archiveChangeText(operation) {
|
|
141
167
|
if (operation.kind === "createTable") return "创建全新表";
|
|
142
|
-
if (operation.kind === "addColumn") return
|
|
168
|
+
if (operation.kind === "addColumn") return `NULL=${operation.after.nullable ? "是" : "否"},默认值=${formatValue(operation.after.default)}`;
|
|
143
169
|
if (operation.kind === "addIndex") return `新增${operation.after.type === "unique" ? "唯一" : "普通"}索引:${operation.after.fields.join(", ")}`;
|
|
144
170
|
if (operation.kind === "replaceIndex") return `${formatValue(operation.before)} -> ${formatValue(operation.after)}`;
|
|
145
171
|
if (operation.kind === "dropIndex") return `删除未在 Tables 中声明的索引:${operation.object}`;
|
|
146
|
-
return
|
|
172
|
+
return (
|
|
173
|
+
operation.changes
|
|
174
|
+
.filter((change) => !fieldTypeProperties.has(change.property))
|
|
175
|
+
.map((change) => `${change.property}: ${formatValue(change.before)} -> ${formatValue(change.after)}`)
|
|
176
|
+
.join(";") || "-"
|
|
177
|
+
);
|
|
147
178
|
}
|
|
148
179
|
|
|
149
180
|
function renderArchive(context) {
|
|
@@ -106,11 +106,21 @@ export function buildDatabaseSnapshot(tableRows, columnRows, indexRows) {
|
|
|
106
106
|
return tables;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
function buildTableFilter(tableNames) {
|
|
110
|
+
const names = Array.isArray(tableNames) ? tableNames.filter(Boolean) : [];
|
|
111
|
+
if (names.length === 0) return { clause: "", params: [] };
|
|
112
|
+
return { clause: ` AND TABLE_NAME IN (${names.map(() => "?").join(", ")})`, params: names };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function readDatabaseSnapshot(mysql, options = {}) {
|
|
116
|
+
const tableFilter = buildTableFilter(options.tableNames);
|
|
110
117
|
const versionRows = (await mysql.execute(VERSION_SQL)).data;
|
|
111
118
|
const versionRow = versionRows[0] || {};
|
|
112
119
|
const mysqlVersion = assertMysql80(versionRow.version, versionRow.versionComment);
|
|
113
|
-
const
|
|
120
|
+
const tableSql = TABLES_SQL.replace("ORDER BY TABLE_NAME", `${tableFilter.clause} ORDER BY TABLE_NAME`);
|
|
121
|
+
const columnSql = COLUMNS_SQL.replace("ORDER BY TABLE_NAME, ORDINAL_POSITION", `${tableFilter.clause} ORDER BY TABLE_NAME, ORDINAL_POSITION`);
|
|
122
|
+
const indexSql = INDEXES_SQL.replace("ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX", `${tableFilter.clause} ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX`);
|
|
123
|
+
const [tableResult, columnResult, indexResult] = await Promise.all([mysql.execute(tableSql, tableFilter.params), mysql.execute(columnSql, tableFilter.params), mysql.execute(indexSql, tableFilter.params)]);
|
|
114
124
|
return {
|
|
115
125
|
mysqlVersion: mysqlVersion,
|
|
116
126
|
tables: buildDatabaseSnapshot(tableResult.data || [], columnResult.data || [], indexResult.data || [])
|