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.
@@ -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(true);
231
+ prepareDirectory(config.clearDevelopmentLog);
231
232
  },
232
233
  setMock: function (mock) {
233
234
  mockInstance = mock;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.76.3",
3
+ "version": "3.76.5",
4
4
  "gitHead": "49c39d36695036e85fc64083cc43c1652fff96cb",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",
@@ -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, singleTable) {
24
- const snapshot = await readDatabaseSnapshot(mysql);
25
- const plan = buildSchemaPlan(targetTables, snapshot.tables, { singleTable: singleTable });
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 tableName = options.tableName || "";
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: 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 tables = await scanTables({ beflyMode: mysqlConfig.beflyMode });
63
- const tableErrors = await checkTable(tables);
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(tables, tableName);
68
+ targetTables = buildTargetDatabaseModel(selectedTables);
67
69
  client = createSyncDbClient(mysqlConfig);
68
- const built = await buildPlan(client, targetTables, Boolean(tableName));
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, Boolean(tableName));
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, Boolean(tableName))).plan
143
+ verify: async (connection) => (await buildPlan(connection, targetTables, tableNames)).plan
142
144
  });
143
- const finalBuilt = await buildPlan(client, targetTables, Boolean(tableName));
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, Boolean(tableName))).plan;
174
+ context.remainingPlan = (await buildPlan(client, targetTables, tableNames)).plan;
173
175
  } catch {
174
176
  // 保留原始执行错误,存档沿用执行前计划
175
177
  }
@@ -22,15 +22,27 @@ function buildColumn(fieldName, field) {
22
22
  };
23
23
  }
24
24
 
25
- export function buildTargetDatabaseModel(tables, tableName) {
26
- let selectedTables = Object.values(tables);
27
- if (tableName) {
28
- selectedTables = selectedTables.filter((table) => table.codeName === tableName || table.fileName === tableName);
29
- if (selectedTables.length === 0) throw new TypeError(`找不到 Table 定义:${tableName}`);
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),
@@ -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 (!singleTable) {
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
  }
@@ -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
- return targetFieldTypeText(operation.after);
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 change = blocked.before !== undefined || blocked.after !== undefined ? `${formatValue(blocked.before)} -> ${formatValue(blocked.after)}` : blocked.reason;
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
- 字段类型: targetFieldTypeText(blocked.target || blocked.after),
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 `新增字段:${operation.after.dbName},类型=${operation.after.fieldType},NULL=${operation.after.nullable ? "是" : "否"},默认值=${formatValue(operation.after.default)}`;
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 operation.changes.map((change) => `${change.property}: ${formatValue(change.before)} -> ${formatValue(change.after)}`).join(";");
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
- export async function readDatabaseSnapshot(mysql) {
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 [tableResult, columnResult, indexResult] = await Promise.all([mysql.execute(TABLES_SQL), mysql.execute(COLUMNS_SQL), mysql.execute(INDEXES_SQL)]);
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 || [])