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
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const DB_DIALECT_SET = new Set(["mysql", "postgresql", "sqlite"]);
|
|
2
1
|
function isNonEmptyString(value) {
|
|
3
2
|
return typeof value === "string" && value.trim().length > 0;
|
|
4
3
|
}
|
|
@@ -8,39 +7,11 @@ function isValidPort(value) {
|
|
|
8
7
|
function isValidNonNegativeInt(value) {
|
|
9
8
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 && Math.floor(value) === value;
|
|
10
9
|
}
|
|
11
|
-
function normalizeDbDialect(dialect) {
|
|
12
|
-
const d = String(dialect || "")
|
|
13
|
-
.toLowerCase()
|
|
14
|
-
.trim();
|
|
15
|
-
if (d === "postgresql")
|
|
16
|
-
return "postgresql";
|
|
17
|
-
if (d === "sqlite")
|
|
18
|
-
return "sqlite";
|
|
19
|
-
if (d === "mysql")
|
|
20
|
-
return "mysql";
|
|
21
|
-
return d;
|
|
22
|
-
}
|
|
23
10
|
function validateDbConfig(db) {
|
|
24
11
|
if (!db) {
|
|
25
12
|
throw new Error("配置错误:缺少 db 配置(config.db)");
|
|
26
13
|
}
|
|
27
|
-
|
|
28
|
-
if (!DB_DIALECT_SET.has(dialect)) {
|
|
29
|
-
throw new Error(`配置错误:db.dialect 只允许 mysql/postgresql/sqlite,当前值=${String(db.dialect)}`);
|
|
30
|
-
}
|
|
31
|
-
// sqlite:database 表示文件路径或 ":memory:";这里要求非空。
|
|
32
|
-
if (dialect === "sqlite") {
|
|
33
|
-
if (!isNonEmptyString(db.database)) {
|
|
34
|
-
throw new Error(`配置错误:db.dialect=sqlite 时必须设置 db.database(sqlite 文件路径或 :memory:),当前值=${String(db.database)}`);
|
|
35
|
-
}
|
|
36
|
-
if (db.poolMax !== undefined) {
|
|
37
|
-
if (typeof db.poolMax !== "number" || !Number.isFinite(db.poolMax) || db.poolMax <= 0) {
|
|
38
|
-
throw new Error(`配置错误:db.poolMax 必须为正数,当前值=${String(db.poolMax)}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
// mysql/postgresql:必须提供 host/port/username/password/database
|
|
14
|
+
// MySQL:必须提供 host/port/username/password/database
|
|
44
15
|
if (!isNonEmptyString(db.host)) {
|
|
45
16
|
throw new Error(`配置错误:db.host 必须为非空字符串,当前值=${String(db.host)}`);
|
|
46
17
|
}
|
|
@@ -88,10 +88,13 @@ const LOWER_CAMEL_CASE_REGEX = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
|
|
|
88
88
|
* 必须为中文、数字、字母、下划线、短横线、空格
|
|
89
89
|
*/
|
|
90
90
|
const FIELD_NAME_REGEX = /^[\u4e00-\u9fa5a-zA-Z0-9 _-]+$/;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
// MySQL VARCHAR 最大长度(utf8mb4 下受行大小限制;本项目采用保守上限,用于表定义校验)
|
|
92
|
+
const MAX_VARCHAR_LENGTH = 16383;
|
|
93
|
+
// 项目索引策略(更简单、更保守):
|
|
94
|
+
// - index=true: 只允许 max<=500
|
|
95
|
+
// - unique=true: 只允许 max<=180
|
|
96
|
+
const MAX_INDEX_STRING_LENGTH_FOR_INDEX = 500;
|
|
97
|
+
const MAX_INDEX_STRING_LENGTH_FOR_UNIQUE = 180;
|
|
95
98
|
/**
|
|
96
99
|
* 检查表定义文件
|
|
97
100
|
* @throws 当检查失败时抛出异常
|
|
@@ -261,6 +264,16 @@ export async function checkTable(tables) {
|
|
|
261
264
|
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
|
|
262
265
|
hasError = true;
|
|
263
266
|
}
|
|
267
|
+
else {
|
|
268
|
+
if (field.index === true && fieldMax > MAX_INDEX_STRING_LENGTH_FOR_INDEX) {
|
|
269
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 设置了 index=true,` + `但 max=${fieldMax} 超出允许范围(要求 <= ${MAX_INDEX_STRING_LENGTH_FOR_INDEX})`);
|
|
270
|
+
hasError = true;
|
|
271
|
+
}
|
|
272
|
+
if (field.unique === true && fieldMax > MAX_INDEX_STRING_LENGTH_FOR_UNIQUE) {
|
|
273
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 设置了 unique=true,` + `但 max=${fieldMax} 超出允许范围(要求 <= ${MAX_INDEX_STRING_LENGTH_FOR_UNIQUE})`);
|
|
274
|
+
hasError = true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
264
277
|
// default 规则(table 定义专用):
|
|
265
278
|
// - string:default 若存在且非 null,必须为 string
|
|
266
279
|
// - array_*_string:default 若存在且非 null,必须为 string,且建议为 JSON 数组字符串(如 "[]")
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ import { syncApi } from "./sync/syncApi";
|
|
|
25
25
|
import { syncCache } from "./sync/syncCache";
|
|
26
26
|
import { syncDev } from "./sync/syncDev";
|
|
27
27
|
import { syncMenu } from "./sync/syncMenu";
|
|
28
|
-
import {
|
|
28
|
+
import { SyncTable } from "./sync/syncTable";
|
|
29
29
|
// 工具
|
|
30
30
|
import { calcPerfTime } from "./utils/calcPerfTime";
|
|
31
31
|
import { getProcessRole } from "./utils/processInfo";
|
|
@@ -92,7 +92,7 @@ export class Befly {
|
|
|
92
92
|
// 注意:这里不做兼容别名(例如 dbHelper=db),要求上下文必须注入标准字段。
|
|
93
93
|
this.assertStartContextReady();
|
|
94
94
|
// 5. 自动同步 (仅主进程执行,避免集群模式下重复执行)
|
|
95
|
-
await
|
|
95
|
+
await new SyncTable(this.context).run(tables);
|
|
96
96
|
await syncApi(this.context, apis);
|
|
97
97
|
await syncMenu(this.context, checkedMenus);
|
|
98
98
|
const devEmail = this.config.devEmail;
|
package/dist/lib/cacheKeys.d.ts
CHANGED
package/dist/lib/cacheKeys.js
CHANGED
package/dist/lib/connect.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export declare class Connect {
|
|
|
14
14
|
private static sqlConnectedAt;
|
|
15
15
|
private static redisConnectedAt;
|
|
16
16
|
private static sqlPoolMax;
|
|
17
|
+
private static mysqlVersionText;
|
|
18
|
+
private static mysqlVersionMajor;
|
|
17
19
|
/**
|
|
18
20
|
* 连接 SQL 数据库
|
|
19
21
|
* @returns SQL 客户端实例
|
|
@@ -69,6 +71,8 @@ export declare class Connect {
|
|
|
69
71
|
connectedAt: number | null;
|
|
70
72
|
uptime: number | null;
|
|
71
73
|
poolMax: number;
|
|
74
|
+
mysqlVersionText: string | null;
|
|
75
|
+
mysqlVersionMajor: number | null;
|
|
72
76
|
};
|
|
73
77
|
redis: {
|
|
74
78
|
connected: boolean;
|
package/dist/lib/connect.js
CHANGED
|
@@ -15,6 +15,9 @@ export class Connect {
|
|
|
15
15
|
static sqlConnectedAt = null;
|
|
16
16
|
static redisConnectedAt = null;
|
|
17
17
|
static sqlPoolMax = 1;
|
|
18
|
+
// MySQL 版本信息(用于启动期校验与监控)
|
|
19
|
+
static mysqlVersionText = null;
|
|
20
|
+
static mysqlVersionMajor = null;
|
|
18
21
|
// ========================================
|
|
19
22
|
// SQL 连接管理
|
|
20
23
|
// ========================================
|
|
@@ -24,51 +27,50 @@ export class Connect {
|
|
|
24
27
|
*/
|
|
25
28
|
static async connectSql(dbConfig) {
|
|
26
29
|
const config = dbConfig || {};
|
|
27
|
-
//
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (dialect === "sqlite") {
|
|
36
|
-
finalUrl = database;
|
|
30
|
+
// 构建 MySQL 连接字符串(不做隐式默认;缺失直接报错,避免连错库/错密码)
|
|
31
|
+
const host = typeof config.host === "string" ? config.host.trim() : "";
|
|
32
|
+
const port = typeof config.port === "number" ? config.port : NaN;
|
|
33
|
+
const username = typeof config.username === "string" ? config.username.trim() : "";
|
|
34
|
+
const password = config.password === undefined ? "" : typeof config.password === "string" ? config.password : "";
|
|
35
|
+
const database = typeof config.database === "string" ? config.database.trim() : "";
|
|
36
|
+
if (!host) {
|
|
37
|
+
throw new Error("数据库配置不完整:db.host 缺失");
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
throw new Error("数据库配置不完整,请检查配置参数");
|
|
41
|
-
}
|
|
42
|
-
finalUrl = `${dialect}://${user}:${password}@${host}:${port}/${database}`;
|
|
39
|
+
if (!Number.isFinite(port) || port < 1 || port > 65535) {
|
|
40
|
+
throw new Error(`数据库配置不完整:db.port 非法(当前值:${String(config.port)})`);
|
|
43
41
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
sql = new SQL(finalUrl);
|
|
42
|
+
if (!username) {
|
|
43
|
+
throw new Error("数据库配置不完整:db.username 缺失");
|
|
47
44
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
url: finalUrl,
|
|
51
|
-
max: config.poolMax ?? 1,
|
|
52
|
-
bigint: false
|
|
53
|
-
});
|
|
45
|
+
if (!database) {
|
|
46
|
+
throw new Error("数据库配置不完整:db.database 缺失");
|
|
54
47
|
}
|
|
48
|
+
const user = encodeURIComponent(username);
|
|
49
|
+
const pass = encodeURIComponent(password);
|
|
50
|
+
const db = encodeURIComponent(database);
|
|
51
|
+
const finalUrl = `mysql://${user}:${pass}@${host}:${port}/${db}`;
|
|
52
|
+
const sql = new SQL({
|
|
53
|
+
url: finalUrl,
|
|
54
|
+
max: config.poolMax ?? 1,
|
|
55
|
+
bigint: false
|
|
56
|
+
});
|
|
55
57
|
try {
|
|
56
58
|
const timeout = 30000;
|
|
57
59
|
const healthCheckPromise = (async () => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
version = v?.[0]?.version;
|
|
60
|
+
const v = await sql `SELECT VERSION() AS version`;
|
|
61
|
+
const versionText = typeof v?.[0]?.version === "string" ? v?.[0]?.version : String(v?.[0]?.version || "");
|
|
62
|
+
// 常见格式:8.0.36 / 8.0.36-xxx
|
|
63
|
+
const majorText = versionText.split(".")[0];
|
|
64
|
+
const major = Number(majorText);
|
|
65
|
+
if (!Number.isFinite(major)) {
|
|
66
|
+
throw new Error(`无法解析 MySQL 版本信息: ${versionText}`);
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
version = v?.[0]?.version;
|
|
68
|
+
if (major < 8) {
|
|
69
|
+
throw new Error(`仅支持 MySQL 8.0+,当前版本:${versionText}`);
|
|
70
70
|
}
|
|
71
|
-
|
|
71
|
+
this.mysqlVersionText = versionText;
|
|
72
|
+
this.mysqlVersionMajor = major;
|
|
73
|
+
return versionText;
|
|
72
74
|
})();
|
|
73
75
|
const timeoutPromise = new Promise((_, reject) => {
|
|
74
76
|
setTimeout(() => {
|
|
@@ -132,11 +134,13 @@ export class Connect {
|
|
|
132
134
|
const password = config.password || "";
|
|
133
135
|
const db = config.db || 0;
|
|
134
136
|
let auth = "";
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
const encodedUsername = username ? encodeURIComponent(username) : "";
|
|
138
|
+
const encodedPassword = password ? encodeURIComponent(password) : "";
|
|
139
|
+
if (encodedUsername && encodedPassword) {
|
|
140
|
+
auth = `${encodedUsername}:${encodedPassword}@`;
|
|
137
141
|
}
|
|
138
|
-
else if (
|
|
139
|
-
auth = `:${
|
|
142
|
+
else if (encodedPassword) {
|
|
143
|
+
auth = `:${encodedPassword}@`;
|
|
140
144
|
}
|
|
141
145
|
const url = `redis://${auth}${host}:${port}/${db}`;
|
|
142
146
|
const redis = new RedisClient(url, {
|
|
@@ -228,7 +232,9 @@ export class Connect {
|
|
|
228
232
|
connected: this.sqlClient !== null,
|
|
229
233
|
connectedAt: this.sqlConnectedAt,
|
|
230
234
|
uptime: this.sqlConnectedAt ? now - this.sqlConnectedAt : null,
|
|
231
|
-
poolMax: this.sqlPoolMax
|
|
235
|
+
poolMax: this.sqlPoolMax,
|
|
236
|
+
mysqlVersionText: this.mysqlVersionText,
|
|
237
|
+
mysqlVersionMajor: this.mysqlVersionMajor
|
|
232
238
|
},
|
|
233
239
|
redis: {
|
|
234
240
|
connected: this.redisClient !== null,
|
|
@@ -261,5 +267,7 @@ export class Connect {
|
|
|
261
267
|
this.sqlConnectedAt = null;
|
|
262
268
|
this.redisConnectedAt = null;
|
|
263
269
|
this.sqlPoolMax = 1;
|
|
270
|
+
this.mysqlVersionText = null;
|
|
271
|
+
this.mysqlVersionMajor = null;
|
|
264
272
|
}
|
|
265
273
|
}
|
package/dist/lib/dbHelper.d.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { WhereConditions, SqlValue } from "../types/common";
|
|
6
6
|
import type { QueryOptions, InsertOptions, UpdateOptions, DeleteOptions, DbPageResult, DbListResult, TransactionCallback, DbResult, ListSql } from "../types/database";
|
|
7
|
-
import type { DbDialect } from "./dbDialect";
|
|
8
7
|
type RedisCacheLike = {
|
|
9
8
|
getObject<T = unknown>(key: string): Promise<T | null>;
|
|
10
9
|
setObject<T = unknown>(key: string, obj: T, ttl?: number | null): Promise<string | null>;
|
|
@@ -13,14 +12,16 @@ type RedisCacheLike = {
|
|
|
13
12
|
type SqlClientLike = {
|
|
14
13
|
unsafe<TResult = unknown>(sqlStr: string, params?: SqlValue[]): Promise<TResult>;
|
|
15
14
|
};
|
|
15
|
+
type DbIdMode = "timeId" | "autoId";
|
|
16
16
|
/**
|
|
17
17
|
* 数据库助手类
|
|
18
18
|
*/
|
|
19
19
|
export declare class DbHelper {
|
|
20
20
|
private redis;
|
|
21
|
-
private
|
|
21
|
+
private dbName;
|
|
22
22
|
private sql;
|
|
23
23
|
private isTransaction;
|
|
24
|
+
private idMode;
|
|
24
25
|
/**
|
|
25
26
|
* 构造函数
|
|
26
27
|
* @param redis - Redis 实例
|
|
@@ -28,8 +29,9 @@ export declare class DbHelper {
|
|
|
28
29
|
*/
|
|
29
30
|
constructor(options: {
|
|
30
31
|
redis: RedisCacheLike;
|
|
32
|
+
dbName: string;
|
|
31
33
|
sql?: SqlClientLike | null;
|
|
32
|
-
|
|
34
|
+
idMode?: DbIdMode;
|
|
33
35
|
});
|
|
34
36
|
private createSqlBuilder;
|
|
35
37
|
private getTableColumns;
|
|
@@ -149,7 +151,7 @@ export declare class DbHelper {
|
|
|
149
151
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
150
152
|
*/
|
|
151
153
|
insData<TInsert extends Record<string, SqlValue> = Record<string, SqlValue>>(options: Omit<InsertOptions, "data"> & {
|
|
152
|
-
data: TInsert
|
|
154
|
+
data: TInsert;
|
|
153
155
|
}): Promise<DbResult<number>>;
|
|
154
156
|
/**
|
|
155
157
|
* 批量插入数据(真正的批量操作)
|
package/dist/lib/dbHelper.js
CHANGED
|
@@ -4,16 +4,22 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { convertBigIntFields } from "../utils/convertBigIntFields";
|
|
6
6
|
import { fieldClear } from "../utils/fieldClear";
|
|
7
|
-
import { toSqlParams } from "../utils/
|
|
8
|
-
import { toNumberFromSql } from "../utils/sqlResult";
|
|
7
|
+
import { toNumberFromSql, toSqlParams } from "../utils/sqlUtil";
|
|
9
8
|
import { arrayKeysToCamel, isPlainObject, keysToCamel, snakeCase } from "../utils/util";
|
|
10
|
-
import { CacheKeys } from "./cacheKeys";
|
|
11
|
-
import { MySqlDialect } from "./dbDialect";
|
|
12
9
|
import { DbUtils } from "./dbUtils";
|
|
13
10
|
import { Logger } from "./logger";
|
|
14
11
|
import { SqlBuilder } from "./sqlBuilder";
|
|
15
12
|
import { SqlCheck } from "./sqlCheck";
|
|
16
|
-
|
|
13
|
+
function quoteIdentMySql(identifier) {
|
|
14
|
+
if (typeof identifier !== "string") {
|
|
15
|
+
throw new Error(`quoteIdentifier 需要字符串类型标识符 (identifier: ${String(identifier)})`);
|
|
16
|
+
}
|
|
17
|
+
const trimmed = identifier.trim();
|
|
18
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
|
|
19
|
+
throw new Error(`无效的 SQL 标识符: ${trimmed}`);
|
|
20
|
+
}
|
|
21
|
+
return `\`${trimmed}\``;
|
|
22
|
+
}
|
|
17
23
|
function hasBegin(sql) {
|
|
18
24
|
return typeof sql.begin === "function";
|
|
19
25
|
}
|
|
@@ -35,9 +41,10 @@ class DbSqlError extends Error {
|
|
|
35
41
|
*/
|
|
36
42
|
export class DbHelper {
|
|
37
43
|
redis;
|
|
38
|
-
|
|
44
|
+
dbName;
|
|
39
45
|
sql = null;
|
|
40
46
|
isTransaction = false;
|
|
47
|
+
idMode;
|
|
41
48
|
/**
|
|
42
49
|
* 构造函数
|
|
43
50
|
* @param redis - Redis 实例
|
|
@@ -45,38 +52,37 @@ export class DbHelper {
|
|
|
45
52
|
*/
|
|
46
53
|
constructor(options) {
|
|
47
54
|
this.redis = options.redis;
|
|
55
|
+
if (typeof options.dbName !== "string" || options.dbName.trim() === "") {
|
|
56
|
+
throw new Error("DbHelper 初始化失败:dbName 必须为非空字符串");
|
|
57
|
+
}
|
|
58
|
+
this.dbName = options.dbName;
|
|
48
59
|
this.sql = options.sql || null;
|
|
49
60
|
this.isTransaction = Boolean(options.sql);
|
|
50
|
-
//
|
|
51
|
-
this.
|
|
61
|
+
// 默认保持历史行为:timeId
|
|
62
|
+
this.idMode = options.idMode === "autoId" ? "autoId" : "timeId";
|
|
52
63
|
}
|
|
53
64
|
createSqlBuilder() {
|
|
54
|
-
return new SqlBuilder({ quoteIdent:
|
|
65
|
+
return new SqlBuilder({ quoteIdent: quoteIdentMySql });
|
|
55
66
|
}
|
|
56
67
|
/**
|
|
57
|
-
*
|
|
68
|
+
* 获取表的所有字段名
|
|
58
69
|
* @param table - 表名(下划线格式)
|
|
59
70
|
* @returns 字段名数组(下划线格式)
|
|
60
71
|
*/
|
|
61
72
|
async getTableColumns(table) {
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
if (columns && columns.length > 0) {
|
|
66
|
-
return columns;
|
|
67
|
-
}
|
|
68
|
-
// 2. 缓存未命中,查询数据库
|
|
69
|
-
const query = this.dialect.getTableColumnsQuery(table);
|
|
70
|
-
const execRes = await this.executeSelect(query.sql, query.params);
|
|
73
|
+
// 查询数据库
|
|
74
|
+
const quotedTable = quoteIdentMySql(table);
|
|
75
|
+
const execRes = await this.executeSelect(`SHOW COLUMNS FROM ${quotedTable}`, []);
|
|
71
76
|
const result = execRes.data;
|
|
72
77
|
if (!result || result.length === 0) {
|
|
73
78
|
throw new Error(`表 ${table} 不存在或没有字段`);
|
|
74
79
|
}
|
|
75
|
-
const columnNames =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
const columnNames = [];
|
|
81
|
+
for (const row of result) {
|
|
82
|
+
const name = row["Field"];
|
|
83
|
+
if (typeof name === "string" && name.length > 0) {
|
|
84
|
+
columnNames.push(name);
|
|
85
|
+
}
|
|
80
86
|
}
|
|
81
87
|
return columnNames;
|
|
82
88
|
}
|
|
@@ -215,8 +221,7 @@ export class DbHelper {
|
|
|
215
221
|
async tableExists(tableName) {
|
|
216
222
|
// 将表名转换为下划线格式
|
|
217
223
|
const snakeTableName = snakeCase(tableName);
|
|
218
|
-
const
|
|
219
|
-
const execRes = await this.executeSelect(query.sql, query.params);
|
|
224
|
+
const execRes = await this.executeSelect("SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", [snakeTableName]);
|
|
220
225
|
const exists = (execRes.data?.[0]?.count || 0) > 0;
|
|
221
226
|
return {
|
|
222
227
|
data: exists,
|
|
@@ -510,14 +515,20 @@ export class DbHelper {
|
|
|
510
515
|
const { table, data } = options;
|
|
511
516
|
const snakeTable = snakeCase(table);
|
|
512
517
|
const now = Date.now();
|
|
513
|
-
let
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
|
|
518
|
+
let processed;
|
|
519
|
+
if (this.idMode === "autoId") {
|
|
520
|
+
processed = DbUtils.buildInsertRow({ idMode: "autoId", data: data, now: now });
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
let id;
|
|
524
|
+
try {
|
|
525
|
+
id = await this.redis.genTimeID();
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
throw new Error(`生成 ID 失败,Redis 可能不可用 (table: ${table})`, { cause: error });
|
|
529
|
+
}
|
|
530
|
+
processed = DbUtils.buildInsertRow({ idMode: "timeId", data: data, id: id, now: now });
|
|
519
531
|
}
|
|
520
|
-
const processed = DbUtils.buildInsertRow({ data: data, id: id, now: now });
|
|
521
532
|
// 入口校验:保证进入 SqlBuilder 的数据无 undefined
|
|
522
533
|
SqlCheck.assertNoUndefinedInRecord(processed, `insData 插入数据 (table: ${snakeTable})`);
|
|
523
534
|
// 构建 SQL
|
|
@@ -528,7 +539,11 @@ export class DbHelper {
|
|
|
528
539
|
const processedId = processed["id"];
|
|
529
540
|
const processedIdNum = typeof processedId === "number" ? processedId : 0;
|
|
530
541
|
const lastInsertRowidNum = toNumberFromSql(execRes.data?.lastInsertRowid);
|
|
531
|
-
|
|
542
|
+
// timeId:优先返回显式写入的 id;autoId:依赖 lastInsertRowid
|
|
543
|
+
const insertedId = this.idMode === "autoId" ? lastInsertRowidNum || 0 : processedIdNum || lastInsertRowidNum || 0;
|
|
544
|
+
if (this.idMode === "autoId" && insertedId <= 0) {
|
|
545
|
+
throw new Error(`插入失败:autoId 模式下无法获取 lastInsertRowid (table: ${table})`);
|
|
546
|
+
}
|
|
532
547
|
return {
|
|
533
548
|
data: insertedId,
|
|
534
549
|
sql: execRes.sql
|
|
@@ -556,20 +571,30 @@ export class DbHelper {
|
|
|
556
571
|
}
|
|
557
572
|
// 转换表名:小驼峰 → 下划线
|
|
558
573
|
const snakeTable = snakeCase(table);
|
|
559
|
-
// 批量生成 ID(逐个获取)
|
|
560
|
-
const ids = [];
|
|
561
|
-
for (let i = 0; i < dataList.length; i++) {
|
|
562
|
-
ids.push(await this.redis.genTimeID());
|
|
563
|
-
}
|
|
564
574
|
const now = Date.now();
|
|
565
575
|
// 处理所有数据(自动添加系统字段)
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
576
|
+
let ids = [];
|
|
577
|
+
let processedList;
|
|
578
|
+
if (this.idMode === "autoId") {
|
|
579
|
+
processedList = dataList.map((data) => {
|
|
580
|
+
return DbUtils.buildInsertRow({ idMode: "autoId", data: data, now: now });
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
// 批量生成 ID(逐个获取)
|
|
585
|
+
const nextIds = [];
|
|
586
|
+
for (let i = 0; i < dataList.length; i++) {
|
|
587
|
+
nextIds.push(await this.redis.genTimeID());
|
|
570
588
|
}
|
|
571
|
-
|
|
572
|
-
|
|
589
|
+
ids = nextIds;
|
|
590
|
+
processedList = dataList.map((data, index) => {
|
|
591
|
+
const id = nextIds[index];
|
|
592
|
+
if (typeof id !== "number") {
|
|
593
|
+
throw new Error(`批量插入生成 ID 失败:ids[${index}] 不是 number (table: ${snakeTable})`);
|
|
594
|
+
}
|
|
595
|
+
return DbUtils.buildInsertRow({ idMode: "timeId", data: data, id: id, now: now });
|
|
596
|
+
});
|
|
597
|
+
}
|
|
573
598
|
// 入口校验:保证进入 SqlBuilder 的批量数据结构一致且无 undefined
|
|
574
599
|
const insertFields = SqlCheck.assertBatchInsertRowsConsistent(processedList, { table: snakeTable });
|
|
575
600
|
// 构建批量插入 SQL
|
|
@@ -578,6 +603,22 @@ export class DbHelper {
|
|
|
578
603
|
// 在事务中执行批量插入
|
|
579
604
|
try {
|
|
580
605
|
const execRes = await this.executeRun(sql, params);
|
|
606
|
+
if (this.idMode === "autoId") {
|
|
607
|
+
const firstId = toNumberFromSql(execRes.data?.lastInsertRowid);
|
|
608
|
+
if (firstId <= 0) {
|
|
609
|
+
throw new Error(`批量插入失败:autoId 模式下无法获取 lastInsertRowid (table: ${table})`);
|
|
610
|
+
}
|
|
611
|
+
// 说明:这里假设 auto_increment_increment = 1(默认)。
|
|
612
|
+
// 如需支持非 1,请在此处增加查询 @@auto_increment_increment 并调整推导规则。
|
|
613
|
+
const outIds = [];
|
|
614
|
+
for (let i = 0; i < dataList.length; i++) {
|
|
615
|
+
outIds.push(firstId + i);
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
data: outIds,
|
|
619
|
+
sql: execRes.sql
|
|
620
|
+
};
|
|
621
|
+
}
|
|
581
622
|
return {
|
|
582
623
|
data: ids,
|
|
583
624
|
sql: execRes.sql
|
|
@@ -608,7 +649,7 @@ export class DbHelper {
|
|
|
608
649
|
table: snakeTable,
|
|
609
650
|
idField: "id",
|
|
610
651
|
ids: ids,
|
|
611
|
-
quoteIdent:
|
|
652
|
+
quoteIdent: quoteIdentMySql
|
|
612
653
|
});
|
|
613
654
|
const execRes = await this.executeRun(query.sql, query.params);
|
|
614
655
|
const changes = toNumberFromSql(execRes.data?.changes);
|
|
@@ -649,7 +690,7 @@ export class DbHelper {
|
|
|
649
690
|
idField: "id",
|
|
650
691
|
rows: processedList,
|
|
651
692
|
fields: fields,
|
|
652
|
-
quoteIdent:
|
|
693
|
+
quoteIdent: quoteIdentMySql,
|
|
653
694
|
updatedAtField: "updated_at",
|
|
654
695
|
updatedAtValue: now,
|
|
655
696
|
stateField: "state",
|
|
@@ -766,7 +807,7 @@ export class DbHelper {
|
|
|
766
807
|
// 使用 Bun SQL 的 begin 方法开启事务
|
|
767
808
|
// begin 方法会自动处理 commit/rollback
|
|
768
809
|
return await sql.begin(async (tx) => {
|
|
769
|
-
const trans = new DbHelper({ redis: this.redis, sql: tx,
|
|
810
|
+
const trans = new DbHelper({ redis: this.redis, dbName: this.dbName, sql: tx, idMode: this.idMode });
|
|
770
811
|
return await callback(trans);
|
|
771
812
|
});
|
|
772
813
|
}
|
|
@@ -781,29 +822,29 @@ export class DbHelper {
|
|
|
781
822
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
782
823
|
*/
|
|
783
824
|
async exists(options) {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
if (
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
825
|
+
if (Array.isArray(options.joins) && options.joins.length > 0) {
|
|
826
|
+
throw new Error("exists 不支持 joins(请使用显式 query 或拆分查询)");
|
|
827
|
+
}
|
|
828
|
+
const rawTable = typeof options.table === "string" ? options.table.trim() : "";
|
|
829
|
+
if (!rawTable) {
|
|
830
|
+
throw new Error("exists.table 不能为空");
|
|
831
|
+
}
|
|
832
|
+
if (rawTable.includes(" ")) {
|
|
833
|
+
throw new Error(`exists 不支持别名表写法(table: ${rawTable})`);
|
|
834
|
+
}
|
|
835
|
+
if (rawTable.includes(".")) {
|
|
836
|
+
throw new Error(`exists 不支持 schema.table 写法(table: ${rawTable})`);
|
|
837
|
+
}
|
|
838
|
+
const snakeTable = snakeCase(rawTable);
|
|
839
|
+
const cleanWhere = fieldClear(options.where || {}, { excludeValues: [null, undefined] });
|
|
840
|
+
const snakeWhere = DbUtils.whereKeysToSnake(cleanWhere);
|
|
841
|
+
const whereFiltered = DbUtils.addDefaultStateFilter(snakeWhere, snakeTable, false);
|
|
842
|
+
// 使用 COUNT(1) 实现:语义清晰、适配现有返回结构
|
|
843
|
+
const builder = this.createSqlBuilder().selectRaw("COUNT(1) as cnt").from(snakeTable).where(whereFiltered).limit(1);
|
|
800
844
|
const { sql, params } = builder.toSelectSql();
|
|
801
845
|
const execRes = await this.executeSelect(sql, params);
|
|
802
846
|
const exists = (execRes.data?.[0]?.cnt || 0) > 0;
|
|
803
|
-
return {
|
|
804
|
-
data: exists,
|
|
805
|
-
sql: execRes.sql
|
|
806
|
-
};
|
|
847
|
+
return { data: exists, sql: execRes.sql };
|
|
807
848
|
}
|
|
808
849
|
/**
|
|
809
850
|
* 查询单个字段值(带字段名验证)
|
|
@@ -811,6 +852,20 @@ export class DbHelper {
|
|
|
811
852
|
*/
|
|
812
853
|
async getFieldValue(options) {
|
|
813
854
|
const field = options.field;
|
|
855
|
+
if (Array.isArray(options.joins) && options.joins.length > 0) {
|
|
856
|
+
throw new Error("getFieldValue 不支持 joins(请使用 getOne/getList 并自行取字段)");
|
|
857
|
+
}
|
|
858
|
+
const rawTable = typeof options.table === "string" ? options.table.trim() : "";
|
|
859
|
+
if (!rawTable) {
|
|
860
|
+
throw new Error("getFieldValue.table 不能为空");
|
|
861
|
+
}
|
|
862
|
+
if (rawTable.includes(" ")) {
|
|
863
|
+
throw new Error(`getFieldValue 不支持别名表写法(table: ${rawTable})`);
|
|
864
|
+
}
|
|
865
|
+
if (rawTable.includes(".")) {
|
|
866
|
+
throw new Error(`getFieldValue 不支持 schema.table 写法(table: ${rawTable})`);
|
|
867
|
+
}
|
|
868
|
+
// (其余逻辑保持不变)
|
|
814
869
|
// 验证字段名格式(只允许字母、数字、下划线)
|
|
815
870
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) {
|
|
816
871
|
throw new Error(`无效的字段名: ${field},只允许字母、数字和下划线`);
|
|
@@ -895,8 +950,8 @@ export class DbHelper {
|
|
|
895
950
|
const builder = this.createSqlBuilder().where(whereFiltered);
|
|
896
951
|
const { sql: whereClause, params: whereParams } = builder.getWhereConditions();
|
|
897
952
|
// 构建安全的 UPDATE SQL(表名和字段名使用反引号转义,已经是下划线格式)
|
|
898
|
-
const quotedTable =
|
|
899
|
-
const quotedField =
|
|
953
|
+
const quotedTable = quoteIdentMySql(snakeTable);
|
|
954
|
+
const quotedField = quoteIdentMySql(snakeField);
|
|
900
955
|
const sql = whereClause ? `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ? WHERE ${whereClause}` : `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ?`;
|
|
901
956
|
const execRes = await this.executeRun(sql, [value, ...whereParams]);
|
|
902
957
|
const changes = toNumberFromSql(execRes.data?.changes);
|