befly 3.15.1 → 3.15.2
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 +1 -2
- package/dist/befly.js +856 -1260
- 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 +2 -3
- package/dist/lib/dbHelper.js +67 -51
- package/dist/lib/dbUtils.js +15 -7
- package/dist/plugins/db.js +5 -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 +0 -2
- package/dist/types/database.d.ts +1 -1
- 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>;
|
|
@@ -18,7 +17,7 @@ type SqlClientLike = {
|
|
|
18
17
|
*/
|
|
19
18
|
export declare class DbHelper {
|
|
20
19
|
private redis;
|
|
21
|
-
private
|
|
20
|
+
private dbName;
|
|
22
21
|
private sql;
|
|
23
22
|
private isTransaction;
|
|
24
23
|
/**
|
|
@@ -28,8 +27,8 @@ export declare class DbHelper {
|
|
|
28
27
|
*/
|
|
29
28
|
constructor(options: {
|
|
30
29
|
redis: RedisCacheLike;
|
|
30
|
+
dbName: string;
|
|
31
31
|
sql?: SqlClientLike | null;
|
|
32
|
-
dialect?: DbDialect;
|
|
33
32
|
});
|
|
34
33
|
private createSqlBuilder;
|
|
35
34
|
private getTableColumns;
|
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,7 +41,7 @@ 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;
|
|
41
47
|
/**
|
|
@@ -45,38 +51,35 @@ export class DbHelper {
|
|
|
45
51
|
*/
|
|
46
52
|
constructor(options) {
|
|
47
53
|
this.redis = options.redis;
|
|
54
|
+
if (typeof options.dbName !== "string" || options.dbName.trim() === "") {
|
|
55
|
+
throw new Error("DbHelper 初始化失败:dbName 必须为非空字符串");
|
|
56
|
+
}
|
|
57
|
+
this.dbName = options.dbName;
|
|
48
58
|
this.sql = options.sql || null;
|
|
49
59
|
this.isTransaction = Boolean(options.sql);
|
|
50
|
-
// 默认使用 MySQL 方言(当前 core 的表结构/语法也主要基于 MySQL)
|
|
51
|
-
this.dialect = options.dialect ? options.dialect : new MySqlDialect();
|
|
52
60
|
}
|
|
53
61
|
createSqlBuilder() {
|
|
54
|
-
return new SqlBuilder({ quoteIdent:
|
|
62
|
+
return new SqlBuilder({ quoteIdent: quoteIdentMySql });
|
|
55
63
|
}
|
|
56
64
|
/**
|
|
57
|
-
*
|
|
65
|
+
* 获取表的所有字段名
|
|
58
66
|
* @param table - 表名(下划线格式)
|
|
59
67
|
* @returns 字段名数组(下划线格式)
|
|
60
68
|
*/
|
|
61
69
|
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);
|
|
70
|
+
// 查询数据库
|
|
71
|
+
const quotedTable = quoteIdentMySql(table);
|
|
72
|
+
const execRes = await this.executeSelect(`SHOW COLUMNS FROM ${quotedTable}`, []);
|
|
71
73
|
const result = execRes.data;
|
|
72
74
|
if (!result || result.length === 0) {
|
|
73
75
|
throw new Error(`表 ${table} 不存在或没有字段`);
|
|
74
76
|
}
|
|
75
|
-
const columnNames =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
const columnNames = [];
|
|
78
|
+
for (const row of result) {
|
|
79
|
+
const name = row["Field"];
|
|
80
|
+
if (typeof name === "string" && name.length > 0) {
|
|
81
|
+
columnNames.push(name);
|
|
82
|
+
}
|
|
80
83
|
}
|
|
81
84
|
return columnNames;
|
|
82
85
|
}
|
|
@@ -215,8 +218,7 @@ export class DbHelper {
|
|
|
215
218
|
async tableExists(tableName) {
|
|
216
219
|
// 将表名转换为下划线格式
|
|
217
220
|
const snakeTableName = snakeCase(tableName);
|
|
218
|
-
const
|
|
219
|
-
const execRes = await this.executeSelect(query.sql, query.params);
|
|
221
|
+
const execRes = await this.executeSelect("SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", [snakeTableName]);
|
|
220
222
|
const exists = (execRes.data?.[0]?.count || 0) > 0;
|
|
221
223
|
return {
|
|
222
224
|
data: exists,
|
|
@@ -608,7 +610,7 @@ export class DbHelper {
|
|
|
608
610
|
table: snakeTable,
|
|
609
611
|
idField: "id",
|
|
610
612
|
ids: ids,
|
|
611
|
-
quoteIdent:
|
|
613
|
+
quoteIdent: quoteIdentMySql
|
|
612
614
|
});
|
|
613
615
|
const execRes = await this.executeRun(query.sql, query.params);
|
|
614
616
|
const changes = toNumberFromSql(execRes.data?.changes);
|
|
@@ -649,7 +651,7 @@ export class DbHelper {
|
|
|
649
651
|
idField: "id",
|
|
650
652
|
rows: processedList,
|
|
651
653
|
fields: fields,
|
|
652
|
-
quoteIdent:
|
|
654
|
+
quoteIdent: quoteIdentMySql,
|
|
653
655
|
updatedAtField: "updated_at",
|
|
654
656
|
updatedAtValue: now,
|
|
655
657
|
stateField: "state",
|
|
@@ -766,7 +768,7 @@ export class DbHelper {
|
|
|
766
768
|
// 使用 Bun SQL 的 begin 方法开启事务
|
|
767
769
|
// begin 方法会自动处理 commit/rollback
|
|
768
770
|
return await sql.begin(async (tx) => {
|
|
769
|
-
const trans = new DbHelper({ redis: this.redis,
|
|
771
|
+
const trans = new DbHelper({ redis: this.redis, dbName: this.dbName, sql: tx });
|
|
770
772
|
return await callback(trans);
|
|
771
773
|
});
|
|
772
774
|
}
|
|
@@ -781,29 +783,29 @@ export class DbHelper {
|
|
|
781
783
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
782
784
|
*/
|
|
783
785
|
async exists(options) {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
if (
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
786
|
+
if (Array.isArray(options.joins) && options.joins.length > 0) {
|
|
787
|
+
throw new Error("exists 不支持 joins(请使用显式 query 或拆分查询)");
|
|
788
|
+
}
|
|
789
|
+
const rawTable = typeof options.table === "string" ? options.table.trim() : "";
|
|
790
|
+
if (!rawTable) {
|
|
791
|
+
throw new Error("exists.table 不能为空");
|
|
792
|
+
}
|
|
793
|
+
if (rawTable.includes(" ")) {
|
|
794
|
+
throw new Error(`exists 不支持别名表写法(table: ${rawTable})`);
|
|
795
|
+
}
|
|
796
|
+
if (rawTable.includes(".")) {
|
|
797
|
+
throw new Error(`exists 不支持 schema.table 写法(table: ${rawTable})`);
|
|
798
|
+
}
|
|
799
|
+
const snakeTable = snakeCase(rawTable);
|
|
800
|
+
const cleanWhere = fieldClear(options.where || {}, { excludeValues: [null, undefined] });
|
|
801
|
+
const snakeWhere = DbUtils.whereKeysToSnake(cleanWhere);
|
|
802
|
+
const whereFiltered = DbUtils.addDefaultStateFilter(snakeWhere, snakeTable, false);
|
|
803
|
+
// 使用 COUNT(1) 实现:语义清晰、适配现有返回结构
|
|
804
|
+
const builder = this.createSqlBuilder().selectRaw("COUNT(1) as cnt").from(snakeTable).where(whereFiltered).limit(1);
|
|
800
805
|
const { sql, params } = builder.toSelectSql();
|
|
801
806
|
const execRes = await this.executeSelect(sql, params);
|
|
802
807
|
const exists = (execRes.data?.[0]?.cnt || 0) > 0;
|
|
803
|
-
return {
|
|
804
|
-
data: exists,
|
|
805
|
-
sql: execRes.sql
|
|
806
|
-
};
|
|
808
|
+
return { data: exists, sql: execRes.sql };
|
|
807
809
|
}
|
|
808
810
|
/**
|
|
809
811
|
* 查询单个字段值(带字段名验证)
|
|
@@ -811,6 +813,20 @@ export class DbHelper {
|
|
|
811
813
|
*/
|
|
812
814
|
async getFieldValue(options) {
|
|
813
815
|
const field = options.field;
|
|
816
|
+
if (Array.isArray(options.joins) && options.joins.length > 0) {
|
|
817
|
+
throw new Error("getFieldValue 不支持 joins(请使用 getOne/getList 并自行取字段)");
|
|
818
|
+
}
|
|
819
|
+
const rawTable = typeof options.table === "string" ? options.table.trim() : "";
|
|
820
|
+
if (!rawTable) {
|
|
821
|
+
throw new Error("getFieldValue.table 不能为空");
|
|
822
|
+
}
|
|
823
|
+
if (rawTable.includes(" ")) {
|
|
824
|
+
throw new Error(`getFieldValue 不支持别名表写法(table: ${rawTable})`);
|
|
825
|
+
}
|
|
826
|
+
if (rawTable.includes(".")) {
|
|
827
|
+
throw new Error(`getFieldValue 不支持 schema.table 写法(table: ${rawTable})`);
|
|
828
|
+
}
|
|
829
|
+
// (其余逻辑保持不变)
|
|
814
830
|
// 验证字段名格式(只允许字母、数字、下划线)
|
|
815
831
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) {
|
|
816
832
|
throw new Error(`无效的字段名: ${field},只允许字母、数字和下划线`);
|
|
@@ -895,8 +911,8 @@ export class DbHelper {
|
|
|
895
911
|
const builder = this.createSqlBuilder().where(whereFiltered);
|
|
896
912
|
const { sql: whereClause, params: whereParams } = builder.getWhereConditions();
|
|
897
913
|
// 构建安全的 UPDATE SQL(表名和字段名使用反引号转义,已经是下划线格式)
|
|
898
|
-
const quotedTable =
|
|
899
|
-
const quotedField =
|
|
914
|
+
const quotedTable = quoteIdentMySql(snakeTable);
|
|
915
|
+
const quotedField = quoteIdentMySql(snakeField);
|
|
900
916
|
const sql = whereClause ? `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ? WHERE ${whereClause}` : `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ?`;
|
|
901
917
|
const execRes = await this.executeRun(sql, [value, ...whereParams]);
|
|
902
918
|
const changes = toNumberFromSql(execRes.data?.changes);
|
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("$");
|
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,17 @@ 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
26
|
// 创建数据库管理器实例
|
|
27
|
-
const dbManager = new DbHelper({ redis: befly.redis,
|
|
27
|
+
const dbManager = new DbHelper({ redis: befly.redis, dbName: dbName, sql: sql });
|
|
28
28
|
return dbManager;
|
|
29
29
|
}
|
|
30
30
|
catch (error) {
|