befly 3.15.21 → 3.15.23

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/dist/index.js CHANGED
@@ -26,7 +26,7 @@ import { syncCache } from "./sync/syncCache";
26
26
  import { syncDev } from "./sync/syncDev";
27
27
  import { syncMenu } from "./sync/syncMenu";
28
28
  import { SyncTable } from "./sync/syncTable";
29
- import { isCoreError } from "./types/coreError";
29
+ import { CoreError, isCoreError } from "./types/coreError";
30
30
  // 工具
31
31
  import { calcPerfTime } from "./utils/calcPerfTime";
32
32
  import { getProcessRole } from "./utils/processInfo";
@@ -226,10 +226,22 @@ export class Befly {
226
226
  catch {
227
227
  // ignore
228
228
  }
229
- if (error instanceof Error) {
229
+ // 重要:避免宿主入口再打印一遍相同错误。
230
+ // 约定:start() 失败后向上抛出的异常应可被识别为“已在 core 侧输出过日志”。
231
+ // - 若本身已是 CoreError:标记 logged=true 后原样抛出
232
+ // - 否则:包装成 CoreError(logged=true, cause=原始错误)
233
+ if (isCoreError(error)) {
234
+ error.logged = true;
230
235
  throw error;
231
236
  }
232
- throw new Error(String(error));
237
+ throw new CoreError({
238
+ kind: kind,
239
+ message: errMessage,
240
+ logged: true,
241
+ noLog: noLog,
242
+ meta: { subsystem: "start", operation: "start" },
243
+ cause: error
244
+ });
233
245
  }
234
246
  }
235
247
  }
@@ -85,8 +85,9 @@ export declare class SyncTable {
85
85
  private static buildFieldPlan;
86
86
  private static buildSystemFieldPlan;
87
87
  private static buildIndexPlan;
88
- private static assertCompatibleTypeChange;
88
+ private static getIncompatibleTypeChange;
89
89
  private static collectIncompatibleTypeChanges;
90
+ private static throwIfIncompatibleTypeChanges;
90
91
  private static truncateForLog;
91
92
  static stripAlgorithmAndLock(stmt: string): string;
92
93
  static buildDdlFallbackCandidates(stmt: string): Array<{
@@ -7,6 +7,7 @@
7
7
  * - core 仅支持 MySQL 8.0+
8
8
  */
9
9
  import { Logger } from "../lib/logger";
10
+ import { CoreError, isCoreError } from "../types/coreError";
10
11
  import { normalizeFieldDefinition } from "../utils/normalizeFieldDefinition";
11
12
  import { escapeComment, normalizeColumnDefaultValue } from "../utils/sqlUtil";
12
13
  import { snakeCase } from "../utils/util";
@@ -139,23 +140,8 @@ export class SyncTable {
139
140
  }
140
141
  tableTasks.push(task);
141
142
  }
142
- if (incompatibleTypeChanges.length > 0) {
143
- const lines = [];
144
- for (const change of incompatibleTypeChanges) {
145
- lines.push(`- ${change.tableName}.${change.dbFieldName}: ${change.currentType} -> ${change.expectedType}`);
146
- }
147
- const msgLines = [];
148
- msgLines.push("禁止字段类型变更(检测到不兼容/收缩变更):");
149
- for (const line of lines) {
150
- msgLines.push(line);
151
- }
152
- msgLines.push("说明: 仅允许同类型的宽化变更(如 TINYINT->SMALLINT->INT->BIGINT),以及部分兼容变更(如 VARCHAR->TEXT、CHAR/VARCHAR 互转、float->double)。");
153
- msgLines.push("提示: 若确需收缩,请先手工迁移/清洗数据后再执行同步。");
154
- // 预检/校验类错误:不在 syncTable 内部重复打印 error,让上层启动流程统一输出一次即可。
155
- const err = new Error(msgLines.join("\n"));
156
- err.__syncTableNoLog = true;
157
- throw err;
158
- }
143
+ // 预检阶段若已检测到不兼容/收缩变更:直接汇总抛错(不在 syncTable 内部重复打印)。
144
+ SyncTable.throwIfIncompatibleTypeChanges(incompatibleTypeChanges);
159
145
  // 预检通过后:基于预检阶段缓存的元信息为每张“已存在的表”构建 plan。
160
146
  // 说明:这里构建 plan 不会执行 DDL;仅用于后续 applyTablePlan。
161
147
  for (const task of tableTasks) {
@@ -176,7 +162,13 @@ export class SyncTable {
176
162
  task.plan = built.plan;
177
163
  task.planSummary = built.summary;
178
164
  task.planDetails = built.details;
165
+ // 双保险:若 plan 构建阶段发现不兼容/收缩变更,也统一汇总到同一列表。
166
+ for (const change of built.incompatibleTypeChanges) {
167
+ incompatibleTypeChanges.push(change);
168
+ }
179
169
  }
170
+ // plan 阶段补充的兼容性检查:保证“遇到第一条 throw”不会中止汇总。
171
+ SyncTable.throwIfIncompatibleTypeChanges(incompatibleTypeChanges);
180
172
  // 预检通过后,再执行实际同步(DDL)。
181
173
  for (const task of tableTasks) {
182
174
  const item = task.item;
@@ -254,6 +246,10 @@ export class SyncTable {
254
246
  throw error;
255
247
  }
256
248
  // 预检/校验类错误:不在 syncTable 打印 error,直接抛给上层(例如 start())统一处理。
249
+ if (isCoreError(error) && error.noLog === true) {
250
+ throw error;
251
+ }
252
+ // 预检/校验类错误:不在 syncTable 打印 error,直接抛给上层(例如 start())统一处理。
257
253
  if (error?.__syncTableNoLog === true) {
258
254
  throw error;
259
255
  }
@@ -521,6 +517,7 @@ export class SyncTable {
521
517
  const addedBusiness = fieldPlan.addedBusiness;
522
518
  const modified = fieldPlan.modified;
523
519
  const addedSystem = systemPlan.addedSystem;
520
+ const incompatibleTypeChanges = fieldPlan.incompatibleTypeChanges;
524
521
  const plan = {
525
522
  changed: alterClauses.length > 0 || indexActions.length > 0,
526
523
  alterClauses: alterClauses,
@@ -537,7 +534,8 @@ export class SyncTable {
537
534
  details: {
538
535
  fieldChanges: fieldPlan.changeDetails,
539
536
  indexChanges: indexPlan.indexActions
540
- }
537
+ },
538
+ incompatibleTypeChanges: incompatibleTypeChanges
541
539
  };
542
540
  }
543
541
  static buildFieldPlan(options) {
@@ -545,6 +543,7 @@ export class SyncTable {
545
543
  let addedBusiness = 0;
546
544
  let modified = 0;
547
545
  const changeDetails = [];
546
+ const incompatibleTypeChanges = [];
548
547
  for (const [fieldKey, fieldDef] of Object.entries(options.fields)) {
549
548
  const dbFieldName = snakeCase(fieldKey);
550
549
  if (options.existingColumns[dbFieldName]) {
@@ -552,7 +551,16 @@ export class SyncTable {
552
551
  if (comparison.length > 0) {
553
552
  modified = modified + 1;
554
553
  changeDetails.push({ fieldKey: fieldKey, dbFieldName: dbFieldName, changes: comparison });
555
- SyncTable.assertCompatibleTypeChange(options.tableName, dbFieldName, comparison, fieldDef);
554
+ // 兼容性检查:不在这里 throw,避免“遇到第一条就中止导致无法汇总”。
555
+ // 统一由 run() 收集所有不兼容项后再一次性抛错。
556
+ const typeChange = comparison.find((c) => c.type === "datatype");
557
+ if (typeChange) {
558
+ const expectedType = SyncTable.getSqlType(fieldDef.type, fieldDef.max ?? null, fieldDef.unsigned ?? false, fieldDef.precision ?? null, fieldDef.scale ?? null);
559
+ const incompatible = SyncTable.getIncompatibleTypeChange(options.tableName, dbFieldName, String(typeChange.current ?? ""), expectedType);
560
+ if (incompatible) {
561
+ incompatibleTypeChanges.push(incompatible);
562
+ }
563
+ }
556
564
  // 简化实现:无论变更类型(含仅默认值变更),统一走 MODIFY COLUMN。
557
565
  alterClauses.push(SyncTable.generateDDLClause(fieldKey, fieldDef, false));
558
566
  }
@@ -562,7 +570,7 @@ export class SyncTable {
562
570
  alterClauses.push(SyncTable.generateDDLClause(fieldKey, fieldDef, true));
563
571
  }
564
572
  }
565
- return { alterClauses: alterClauses, addedBusiness: addedBusiness, modified: modified, changeDetails: changeDetails };
573
+ return { alterClauses: alterClauses, addedBusiness: addedBusiness, modified: modified, changeDetails: changeDetails, incompatibleTypeChanges: incompatibleTypeChanges };
566
574
  }
567
575
  static buildSystemFieldPlan(options) {
568
576
  const alterClauses = [];
@@ -602,12 +610,9 @@ export class SyncTable {
602
610
  ];
603
611
  return { indexActions: indexActions };
604
612
  }
605
- static assertCompatibleTypeChange(tableName, dbFieldName, comparison, fieldDef) {
606
- const typeChange = comparison.find((c) => c.type === "datatype");
607
- if (!typeChange)
608
- return;
609
- const currentType = String(typeChange.current || "").toLowerCase();
610
- const expectedType = SyncTable.getSqlType(fieldDef.type, fieldDef.max ?? null, fieldDef.unsigned ?? false, fieldDef.precision ?? null, fieldDef.scale ?? null).toLowerCase();
613
+ static getIncompatibleTypeChange(tableName, dbFieldName, currentTypeRaw, expectedTypeRaw) {
614
+ const currentType = String(currentTypeRaw || "").toLowerCase();
615
+ const expectedType = String(expectedTypeRaw || "").toLowerCase();
611
616
  const currentBase = currentType
612
617
  .replace(/\s*unsigned/gi, "")
613
618
  .replace(/\([^)]*\)/g, "")
@@ -616,8 +621,15 @@ export class SyncTable {
616
621
  .replace(/\s*unsigned/gi, "")
617
622
  .replace(/\([^)]*\)/g, "")
618
623
  .trim();
619
- if (currentBase !== expectedBase && !SyncTable.isCompatibleTypeChange(currentType, expectedType))
620
- throw new Error([`禁止字段类型变更: ${tableName}.${dbFieldName}`, `当前类型: ${typeChange.current}`, `目标类型: ${typeChange.expected}`, "说明: 仅允许宽化型变更(如 INT->BIGINT, VARCHAR->TEXT),以及 CHAR/VARCHAR 互转;DATETIME 与 BIGINT 不允许互转(需要手动迁移数据)"].join("\n"));
624
+ if (currentBase !== expectedBase && !SyncTable.isCompatibleTypeChange(currentType, expectedType)) {
625
+ return {
626
+ tableName: tableName,
627
+ dbFieldName: dbFieldName,
628
+ currentType: String(currentTypeRaw ?? ""),
629
+ expectedType: String(expectedTypeRaw ?? "")
630
+ };
631
+ }
632
+ return null;
621
633
  }
622
634
  static collectIncompatibleTypeChanges(tableName, existingColumns, fields) {
623
635
  const out = [];
@@ -630,27 +642,36 @@ export class SyncTable {
630
642
  const typeChange = comparison.find((c) => c.type === "datatype");
631
643
  if (!typeChange)
632
644
  continue;
633
- const currentType = String(typeChange.current || "").toLowerCase();
634
- const expectedType = String(typeChange.expected || "").toLowerCase();
635
- const currentBase = currentType
636
- .replace(/\s*unsigned/gi, "")
637
- .replace(/\([^)]*\)/g, "")
638
- .trim();
639
- const expectedBase = expectedType
640
- .replace(/\s*unsigned/gi, "")
641
- .replace(/\([^)]*\)/g, "")
642
- .trim();
643
- if (currentBase !== expectedBase && !SyncTable.isCompatibleTypeChange(currentType, expectedType)) {
644
- out.push({
645
- tableName: tableName,
646
- dbFieldName: dbFieldName,
647
- currentType: String(typeChange.current ?? ""),
648
- expectedType: String(typeChange.expected ?? "")
649
- });
645
+ const expectedType = SyncTable.getSqlType(fieldDef.type, fieldDef.max ?? null, fieldDef.unsigned ?? false, fieldDef.precision ?? null, fieldDef.scale ?? null);
646
+ const incompatible = SyncTable.getIncompatibleTypeChange(tableName, dbFieldName, String(typeChange.current ?? ""), expectedType);
647
+ if (incompatible) {
648
+ out.push(incompatible);
650
649
  }
651
650
  }
652
651
  return out;
653
652
  }
653
+ static throwIfIncompatibleTypeChanges(incompatibleTypeChanges) {
654
+ if (incompatibleTypeChanges.length === 0) {
655
+ return;
656
+ }
657
+ const lines = [];
658
+ for (const change of incompatibleTypeChanges) {
659
+ lines.push(`- ${change.tableName}.${change.dbFieldName}: ${change.currentType} -> ${change.expectedType}`);
660
+ }
661
+ const msgLines = [];
662
+ msgLines.push("禁止字段类型变更(检测到不兼容/收缩变更):");
663
+ for (const line of lines) {
664
+ msgLines.push(line);
665
+ }
666
+ msgLines.push("说明: 仅允许同类型的宽化变更(如 TINYINT->SMALLINT->INT->BIGINT),以及部分兼容变更(如 VARCHAR->TEXT、CHAR/VARCHAR 互转、float->double)。");
667
+ msgLines.push("提示: 若确需收缩,请先手工迁移/清洗数据后再执行同步。");
668
+ throw new CoreError({
669
+ kind: "policy",
670
+ message: msgLines.join("\n"),
671
+ noLog: true,
672
+ meta: { subsystem: "syncTable", operation: "precheck" }
673
+ });
674
+ }
654
675
  /* ---------------------------------------------------------------------- */
655
676
  /* DDL 执行(失败时降级 algorithm/lock) */
656
677
  /* ---------------------------------------------------------------------- */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.15.21",
4
- "gitHead": "f804b648c68421288137547d70a978e8230ece2f",
3
+ "version": "3.15.23",
4
+ "gitHead": "d1f9f15baf452a71fcebc220a9b2a20376616628",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
7
7
  "keywords": [