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.
- package/README.md +1 -2
- package/dist/befly.config.js +6 -2
- package/dist/befly.js +918 -1277
- package/dist/befly.min.js +13 -15
- package/dist/checks/checkConfig.js +1 -30
- package/dist/checks/checkTable.js +17 -4
- package/dist/index.js +2 -2
- package/dist/lib/cacheKeys.d.ts +0 -2
- package/dist/lib/cacheKeys.js +0 -4
- package/dist/lib/connect.d.ts +4 -0
- package/dist/lib/connect.js +49 -41
- package/dist/lib/dbHelper.d.ts +6 -4
- package/dist/lib/dbHelper.js +125 -70
- package/dist/lib/dbUtils.d.ts +13 -6
- package/dist/lib/dbUtils.js +22 -8
- package/dist/plugins/db.js +7 -5
- package/dist/sync/syncDev.js +2 -0
- package/dist/sync/syncTable.d.ts +78 -114
- package/dist/sync/syncTable.js +550 -996
- package/dist/types/befly.d.ts +8 -2
- package/dist/types/database.d.ts +3 -3
- package/dist/types/table.d.ts +1 -1
- package/dist/utils/convertBigIntFields.js +36 -8
- package/dist/utils/sqlUtil.d.ts +33 -0
- package/dist/utils/sqlUtil.js +146 -0
- package/package.json +2 -2
- package/dist/lib/dbDialect.d.ts +0 -87
- package/dist/lib/dbDialect.js +0 -196
- package/dist/utils/sqlParams.d.ts +0 -10
- package/dist/utils/sqlParams.js +0 -78
- package/dist/utils/sqlResult.d.ts +0 -5
- package/dist/utils/sqlResult.js +0 -7
package/dist/lib/dbUtils.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/lib/dbUtils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/plugins/db.js
CHANGED
|
@@ -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
|
|
24
|
-
const
|
|
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,
|
|
29
|
+
const dbManager = new DbHelper({ redis: befly.redis, dbName: dbName, sql: sql, idMode: normalizedIdMode });
|
|
28
30
|
return dbManager;
|
|
29
31
|
}
|
|
30
32
|
catch (error) {
|
package/dist/sync/syncDev.js
CHANGED
package/dist/sync/syncTable.d.ts
CHANGED
|
@@ -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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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 {};
|