befly 3.15.1 → 3.15.3

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.
@@ -1,4 +1,14 @@
1
- import type { WhereConditions } from "../types/common";
1
+ import type { SqlValue, WhereConditions } from "../types/common";
2
+ type BuildInsertRowOptions = {
3
+ idMode: "timeId";
4
+ data: Record<string, SqlValue>;
5
+ id: number;
6
+ now: number;
7
+ } | {
8
+ idMode: "autoId";
9
+ data: Record<string, SqlValue>;
10
+ now: number;
11
+ };
2
12
  export declare class DbUtils {
3
13
  static parseTableRef(tableRef: string): {
4
14
  schema: string | null;
@@ -51,11 +61,7 @@ export declare class DbUtils {
51
61
  static stripSystemFieldsForWrite(data: Record<string, any>, options: {
52
62
  allowState: boolean;
53
63
  }): Record<string, any>;
54
- static buildInsertRow(options: {
55
- data: Record<string, any>;
56
- id: number;
57
- now: number;
58
- }): Record<string, any>;
64
+ static buildInsertRow(options: BuildInsertRowOptions): Record<string, any>;
59
65
  static buildUpdateRow(options: {
60
66
  data: Record<string, any>;
61
67
  now: number;
@@ -66,3 +72,4 @@ export declare class DbUtils {
66
72
  allowState: boolean;
67
73
  }): Record<string, any>;
68
74
  }
75
+ export {};
@@ -1,5 +1,6 @@
1
1
  import { fieldClear } from "../utils/fieldClear";
2
2
  import { keysToSnake, snakeCase } from "../utils/util";
3
+ import { SqlCheck } from "./sqlCheck";
3
4
  export class DbUtils {
4
5
  static parseTableRef(tableRef) {
5
6
  if (typeof tableRef !== "string") {
@@ -92,11 +93,7 @@ export class DbUtils {
92
93
  // 情况2:指定包含字段
93
94
  if (classified.type === "include") {
94
95
  return classified.fields.map((field) => {
95
- // 保留函数和特殊字段
96
- if (field.includes("(") || field.includes(" ")) {
97
- return field;
98
- }
99
- return snakeCase(field);
96
+ return DbUtils.processJoinField(field);
100
97
  });
101
98
  }
102
99
  // 情况3:排除字段
@@ -124,6 +121,11 @@ export class DbUtils {
124
121
  if (fields.some((f) => !f || typeof f !== "string" || f.trim() === "")) {
125
122
  throw new Error("fields 不能包含空字符串或无效值");
126
123
  }
124
+ // 统一禁止函数/表达式:复杂表达式请使用 selectRaw/whereRaw
125
+ for (const rawField of fields) {
126
+ const checkField = rawField.startsWith("!") ? rawField.substring(1) : rawField;
127
+ SqlCheck.assertNoExprField(checkField);
128
+ }
127
129
  // 统计包含字段和排除字段
128
130
  const includeFields = fields.filter((f) => !f.startsWith("!"));
129
131
  const excludeFields = fields.filter((f) => f.startsWith("!"));
@@ -161,8 +163,10 @@ export class DbUtils {
161
163
  });
162
164
  }
163
165
  static processJoinField(field) {
164
- // 跳过函数、星号、已处理的字段
165
- if (field.includes("(") || field === "*" || field.startsWith("`")) {
166
+ // 统一禁止函数/表达式:复杂表达式请使用 SqlBuilder.selectRaw
167
+ SqlCheck.assertNoExprField(field);
168
+ // 跳过星号、已引用字段
169
+ if (field === "*" || field.startsWith("`")) {
166
170
  return field;
167
171
  }
168
172
  // 处理别名 AS
@@ -200,6 +204,8 @@ export class DbUtils {
200
204
  if (key === "$or" || key === "$and") {
201
205
  return key;
202
206
  }
207
+ // 统一禁止函数/表达式:复杂表达式请使用 whereRaw
208
+ SqlCheck.assertNoExprField(key);
203
209
  // 处理带操作符的字段名(如 user.userId$gt)
204
210
  if (key.includes("$")) {
205
211
  const lastDollarIndex = key.lastIndexOf("$");
@@ -328,6 +334,8 @@ export class DbUtils {
328
334
  result[key] = Array.isArray(value) ? value.map((item) => DbUtils.whereKeysToSnake(item)) : [];
329
335
  continue;
330
336
  }
337
+ // 统一禁止函数/表达式:复杂表达式请使用 whereRaw
338
+ SqlCheck.assertNoExprField(key);
331
339
  // 处理带操作符的字段名(如 userId$gt)
332
340
  if (key.includes("$")) {
333
341
  const lastDollarIndex = key.lastIndexOf("$");
@@ -427,7 +435,13 @@ export class DbUtils {
427
435
  for (const [key, value] of Object.entries(userData)) {
428
436
  result[key] = value;
429
437
  }
430
- result["id"] = options.id;
438
+ if (options.idMode === "timeId") {
439
+ if (!Number.isFinite(options.id) || options.id <= 0) {
440
+ throw new Error(`buildInsertRow(timeId) 失败:id 必须为 > 0 的有限 number (id: ${String(options.id)})`);
441
+ }
442
+ result["id"] = options.id;
443
+ }
444
+ // autoId 模式:不写入 id,交给 MySQL AUTO_INCREMENT 生成
431
445
  result["created_at"] = options.now;
432
446
  result["updated_at"] = options.now;
433
447
  result["state"] = 1;
@@ -3,7 +3,6 @@
3
3
  * 初始化数据库连接和 SQL 管理器
4
4
  */
5
5
  import { Connect } from "../lib/connect";
6
- import { getDialectByName } from "../lib/dbDialect";
7
6
  import { DbHelper } from "../lib/dbHelper";
8
7
  import { Logger } from "../lib/logger";
9
8
  /**
@@ -15,16 +14,19 @@ const dbPlugin = {
15
14
  deps: ["logger", "redis"],
16
15
  async handler(befly) {
17
16
  const env = befly.config?.nodeEnv;
17
+ const dbName = String(befly.config?.db?.database || "");
18
+ if (!dbName) {
19
+ throw new Error("数据库初始化失败:befly.config.db.database 缺失");
20
+ }
18
21
  if (!befly.redis) {
19
22
  throw new Error("Redis 未初始化");
20
23
  }
21
24
  try {
22
25
  const sql = Connect.getSql();
23
- const rawDbDialect = befly.config?.db?.dialect;
24
- const dialectName = rawDbDialect === "postgresql" || rawDbDialect === "sqlite" ? rawDbDialect : "mysql";
25
- const dialect = getDialectByName(dialectName);
26
+ const idMode = befly.config?.db?.idMode;
27
+ const normalizedIdMode = idMode === "autoId" ? "autoId" : "timeId";
26
28
  // 创建数据库管理器实例
27
- const dbManager = new DbHelper({ redis: befly.redis, sql: sql, dialect: dialect });
29
+ const dbManager = new DbHelper({ redis: befly.redis, dbName: dbName, sql: sql, idMode: normalizedIdMode });
28
30
  return dbManager;
29
31
  }
30
32
  catch (error) {
@@ -145,6 +145,8 @@ export async function syncDev(ctx, config = {}) {
145
145
  code: roleConfig.code,
146
146
  name: roleConfig.name,
147
147
  description: roleConfig.description,
148
+ menus: [],
149
+ apis: [],
148
150
  sort: roleConfig.sort
149
151
  }
150
152
  });
@@ -1,13 +1,13 @@
1
1
  /**
2
- * syncTable 命令 - 同步数据库表结构(单文件版)
2
+ * syncTable 命令 - 同步数据库表结构(MySQL 8+ only / 单文件版)
3
3
  *
4
4
  * 说明:
5
5
  * - 历史上该能力拆分在 packages/core/sync/syncTable/* 多个模块中
6
6
  * - 现在按项目要求,将所有实现合并到本文件(目录 packages/core/sync/syncTable/ 已删除)
7
+ * - core 仅支持 MySQL 8.0+
7
8
  */
8
- import type { DbDialectName } from "../lib/dbDialect";
9
9
  import type { DbResult, SqlInfo } from "../types/database";
10
- import type { ColumnInfo, FieldChange, IndexInfo } from "../types/sync";
10
+ import type { ColumnInfo, FieldChange, IndexInfo, TablePlan } from "../types/sync";
11
11
  import type { FieldDefinition } from "../types/validate";
12
12
  import type { ScanFileResult } from "../utils/scanFiles";
13
13
  type SqlExecutor = {
@@ -15,41 +15,32 @@ type SqlExecutor = {
15
15
  };
16
16
  type SyncTableContext = {
17
17
  db: SqlExecutor;
18
- redis: {
19
- delBatch(keys: string[]): Promise<unknown>;
20
- };
21
18
  config: {
22
19
  db?: {
23
- dialect?: DbDialectName;
24
20
  database?: string;
25
21
  };
26
22
  };
27
23
  };
28
- type DbDialect = DbDialectName;
29
- declare function createRuntimeForIO(dbDialect: DbDialect, db: SqlExecutor, dbName?: string): SyncRuntimeForIO;
30
- /**
31
- * 文件导航(推荐阅读顺序)
32
- * 1) 同步表:入口(本段下方)
33
- * 2) 版本/常量/方言判断(DB_VERSION_REQUIREMENTS 等)
34
- * 3) 通用 DDL 工具(quote/type/default/ddl/index SQL)
35
- * 4) Runtime I/O(只读元信息:表/列/索引/版本)
36
- * 5) plan/apply(写变更:建表/改表/SQLite 重建)
37
- */
38
- type SyncTableFn = ((ctx: SyncTableContext, items: ScanFileResult[]) => Promise<void>) & {
39
- TestKit: typeof SYNC_TABLE_TEST_KIT;
24
+ type SystemFieldMeta = {
25
+ name: "id" | "created_at" | "updated_at" | "deleted_at" | "state";
26
+ comment: string;
27
+ needsIndex: boolean;
28
+ mysqlDdl: string;
40
29
  };
41
30
  /**
42
- * 数据库同步命令入口(函数模式)
31
+ * 数据库同步命令入口(class 模式)
43
32
  */
44
- export declare const syncTable: SyncTableFn;
45
- declare const SYNC_TABLE_TEST_KIT: {
46
- DB_VERSION_REQUIREMENTS: {
33
+ export declare class SyncTable {
34
+ /**
35
+ * 数据库版本要求(MySQL only)
36
+ */
37
+ static DB_VERSION_REQUIREMENTS: {
47
38
  readonly MYSQL_MIN_MAJOR: 8;
48
- readonly POSTGRES_MIN_MAJOR: 17;
49
- readonly SQLITE_MIN_VERSION: "3.50.0";
50
- readonly SQLITE_MIN_VERSION_NUM: 35000;
51
39
  };
52
- CHANGE_TYPE_LABELS: {
40
+ /**
41
+ * 字段变更类型的中文标签映射
42
+ */
43
+ static CHANGE_TYPE_LABELS: {
53
44
  readonly length: "长度";
54
45
  readonly datatype: "类型";
55
46
  readonly comment: "注释";
@@ -57,95 +48,68 @@ declare const SYNC_TABLE_TEST_KIT: {
57
48
  readonly nullable: "可空约束";
58
49
  readonly unique: "唯一约束";
59
50
  };
60
- MYSQL_TABLE_CONFIG: {
51
+ /**
52
+ * MySQL 表配置
53
+ */
54
+ static MYSQL_TABLE_CONFIG: {
61
55
  readonly ENGINE: "InnoDB";
62
56
  readonly CHARSET: "utf8mb4";
63
57
  readonly COLLATE: "utf8mb4_0900_ai_ci";
64
58
  };
65
- SYSTEM_INDEX_FIELDS: readonly string[];
66
- getTypeMapping: typeof getTypeMapping;
67
- quoteIdentifier: typeof quoteIdentifier;
68
- escapeComment: typeof escapeComment;
69
- normalizeFieldDefinitionInPlace: typeof normalizeFieldDefinitionInPlace;
70
- isStringOrArrayType: typeof isStringOrArrayType;
71
- getSqlType: typeof getSqlType;
72
- resolveDefaultValue: typeof resolveDefaultValue;
73
- generateDefaultSql: typeof generateDefaultSql;
74
- buildIndexSQL: typeof buildIndexSQL;
75
- buildSystemColumnDefs: typeof buildSystemColumnDefs;
76
- buildBusinessColumnDefs: typeof buildBusinessColumnDefs;
77
- generateDDLClause: typeof generateDDLClause;
78
- isCompatibleTypeChange: typeof isCompatibleTypeChange;
79
- compareFieldDefinition: typeof compareFieldDefinition;
80
- tableExistsRuntime: typeof tableExistsRuntime;
81
- getTableColumnsRuntime: typeof getTableColumnsRuntime;
82
- getTableIndexesRuntime: typeof getTableIndexesRuntime;
83
- createRuntime: typeof createRuntimeForIO;
84
- };
85
- /**
86
- * 获取字段类型映射(根据当前数据库类型)
87
- */
88
- declare function getTypeMapping(dbDialect: DbDialect): Record<string, string>;
89
- /**
90
- * 根据数据库类型引用标识符
91
- */
92
- declare function quoteIdentifier(dbDialect: DbDialect, identifier: string): string;
93
- /**
94
- * 转义 SQL 注释中的双引号
95
- */
96
- declare function escapeComment(str: string): string;
97
- /**
98
- * 为字段定义应用默认值(就地归一化)
99
- */
100
- declare function normalizeFieldDefinitionInPlace(fieldDef: FieldDefinition): void;
101
- /**
102
- * 判断是否为字符串或数组类型(需要长度参数)
103
- */
104
- declare function isStringOrArrayType(fieldType: string): boolean;
105
- /**
106
- * 获取 SQL 数据类型
107
- */
108
- declare function getSqlType(dbDialect: DbDialect, fieldType: string, fieldMax: number | null, unsigned?: boolean): string;
109
- /**
110
- * 处理默认值:将 null 或 'null' 字符串转换为对应类型的默认值
111
- */
112
- declare function resolveDefaultValue(fieldDefault: any, fieldType: string): any;
113
- /**
114
- * 生成 SQL DEFAULT 子句
115
- */
116
- declare function generateDefaultSql(actualDefault: any, fieldType: string): string;
117
- /**
118
- * 构建索引操作 SQL(统一使用在线策略)
119
- */
120
- declare function buildIndexSQL(dbDialect: DbDialect, tableName: string, indexName: string, fieldName: string, action: "create" | "drop"): string;
121
- /**
122
- * 构建系统字段列定义
123
- */
124
- declare function buildSystemColumnDefs(dbDialect: DbDialect): string[];
125
- /**
126
- * 构建业务字段列定义
127
- */
128
- declare function buildBusinessColumnDefs(dbDialect: DbDialect, fields: Record<string, FieldDefinition>): string[];
129
- /**
130
- * 生成字段 DDL 子句(不含 ALTER TABLE 前缀)
131
- */
132
- declare function generateDDLClause(dbDialect: DbDialect, fieldKey: string, fieldDef: FieldDefinition, isAdd?: boolean): string;
133
- /**
134
- * 判断是否为兼容的类型变更(宽化型变更,无数据丢失风险)
135
- */
136
- declare function isCompatibleTypeChange(currentType: string | null | undefined, newType: string | null | undefined): boolean;
137
- type SyncRuntimeForIO = Readonly<{
138
- dbDialect: DbDialect;
139
- db: SqlExecutor;
140
- dbName: string;
141
- }>;
142
- declare function tableExistsRuntime(runtime: SyncRuntimeForIO, tableName: string): Promise<boolean>;
143
- declare function getTableColumnsRuntime(runtime: SyncRuntimeForIO, tableName: string): Promise<{
144
- [key: string]: ColumnInfo;
145
- }>;
146
- declare function getTableIndexesRuntime(runtime: SyncRuntimeForIO, tableName: string): Promise<IndexInfo>;
147
- /**
148
- * 比较字段定义变化
149
- */
150
- declare function compareFieldDefinition(dbDialect: DbDialect, existingColumn: Pick<ColumnInfo, "type" | "columnType" | "max" | "nullable" | "defaultValue" | "comment">, fieldDef: FieldDefinition): FieldChange[];
59
+ static SYSTEM_FIELDS: ReadonlyArray<SystemFieldMeta>;
60
+ static SYSTEM_INDEX_FIELDS: ReadonlyArray<string>;
61
+ private db;
62
+ private dbName;
63
+ constructor(ctx: SyncTableContext);
64
+ run(items: ScanFileResult[]): Promise<void>;
65
+ static buildRuntimeIoError(operation: string, tableName: string, error: unknown): Error & {
66
+ sqlInfo?: SqlInfo;
67
+ };
68
+ static getTypeMapping(): Record<string, string>;
69
+ static normalizeFieldDefinitionInPlace(fieldDef: FieldDefinition): void;
70
+ static isStringOrArrayType(fieldType: string): boolean;
71
+ /**
72
+ * MySQL 标识符安全包裹:仅允许 [a-zA-Z_][a-zA-Z0-9_]*,并用反引号包裹。
73
+ *
74
+ * 说明:
75
+ * - 这是 syncTable 内部用于拼接 DDL 的安全阀
76
+ * - 若未来需要支持更复杂的标识符(如关键字/点号/反引号转义),应在此处统一扩展并补测试
77
+ */
78
+ static quoteIdentifier(identifier: string): string;
79
+ static getSqlType(fieldType: string, fieldMax: number | null, unsigned?: boolean): string;
80
+ static resolveDefaultValue(fieldDefault: any, fieldType: string): any;
81
+ static generateDefaultSql(actualDefault: any, fieldType: string): string;
82
+ static buildIndexSQL(tableName: string, indexName: string, fieldName: string, action: "create" | "drop"): string;
83
+ static getSystemColumnDef(fieldName: string): string | null;
84
+ static buildSystemColumnDefs(): string[];
85
+ static buildBusinessColumnDefs(fields: Record<string, FieldDefinition>): string[];
86
+ static generateDDLClause(fieldKey: string, fieldDef: FieldDefinition, isAdd?: boolean): string;
87
+ static executeDDLSafely(db: SqlExecutor, stmt: string): Promise<boolean>;
88
+ static isCompatibleTypeChange(currentType: string | null | undefined, newType: string | null | undefined): boolean;
89
+ static compareFieldDefinition(existingColumn: Pick<ColumnInfo, "type" | "columnType" | "max" | "nullable" | "defaultValue" | "comment">, fieldDef: FieldDefinition): FieldChange[];
90
+ /**
91
+ * 只读查询 information_schema.TABLES,用于判断表是否存在。
92
+ * - 该方法不会执行 DDL,不会修改数据库结构
93
+ * - 失败时会包装错误信息(含 tableName / operation)以便排查
94
+ */
95
+ static tableExists(db: SqlExecutor, dbName: string, tableName: string): Promise<boolean>;
96
+ /**
97
+ * 只读查询 information_schema.COLUMNS,读取列元信息。
98
+ * - 该方法不会执行 DDL,不会修改数据库结构
99
+ * - 返回结构用于与字段定义做对比(compareFieldDefinition)
100
+ */
101
+ static getTableColumns(db: SqlExecutor, dbName: string, tableName: string): Promise<{
102
+ [key: string]: ColumnInfo;
103
+ }>;
104
+ /**
105
+ * 只读查询 information_schema.STATISTICS,读取(非主键)索引元信息。
106
+ * - 该方法不会执行 DDL,不会修改数据库结构
107
+ * - 仅返回 PRIMARY 之外的索引(PRIMARY 会被同步逻辑视为系统约束)
108
+ */
109
+ static getTableIndexes(db: SqlExecutor, dbName: string, tableName: string): Promise<IndexInfo>;
110
+ static ensureDbVersion(db: SqlExecutor): Promise<void>;
111
+ static applyTablePlan(db: SqlExecutor, tableName: string, plan: TablePlan): Promise<void>;
112
+ static createTable(db: SqlExecutor, tableName: string, fields: Record<string, FieldDefinition>, systemIndexFields?: ReadonlyArray<string>): Promise<void>;
113
+ static modifyTable(db: SqlExecutor, dbName: string, tableName: string, fields: Record<string, FieldDefinition>): Promise<TablePlan>;
114
+ }
151
115
  export {};