befly 3.14.0 → 3.14.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/dist/befly.js +395 -205
- package/dist/befly.min.js +13 -13
- package/dist/checks/checkTable.js +5 -3
- package/dist/hooks/validator.js +4 -2
- package/dist/index.js +14 -5
- package/dist/lib/asyncContext.js +4 -4
- package/dist/lib/cacheHelper.d.ts +2 -2
- package/dist/lib/dbHelper.d.ts +29 -13
- package/dist/lib/dbHelper.js +66 -25
- package/dist/lib/dbUtils.js +95 -20
- package/dist/lib/jwt.js +31 -19
- package/dist/lib/logger.js +7 -7
- package/dist/lib/sqlBuilder.js +53 -13
- package/dist/lib/validator.js +10 -3
- package/dist/loader/loadApis.js +2 -6
- package/dist/loader/loadHooks.js +1 -1
- package/dist/loader/loadPlugins.js +1 -1
- package/dist/router/api.js +7 -6
- package/dist/sync/syncDev.js +2 -2
- package/dist/sync/syncTable.d.ts +1 -1
- package/dist/sync/syncTable.js +70 -41
- package/dist/types/common.d.ts +9 -9
- package/dist/types/database.d.ts +25 -22
- package/dist/types/validate.d.ts +40 -13
- package/dist/utils/loadMenuConfigs.js +1 -1
- package/dist/utils/loggerUtils.js +7 -3
- package/dist/utils/normalizeFieldDefinition.d.ts +15 -0
- package/dist/utils/normalizeFieldDefinition.js +15 -0
- package/dist/utils/processInfo.js +2 -2
- package/dist/utils/scanFiles.js +12 -12
- package/dist/utils/util.js +10 -3
- package/package.json +2 -2
- package/dist/configs/presetFields.d.ts +0 -4
- package/dist/configs/presetFields.js +0 -10
- package/dist/utils/processAtSymbol.d.ts +0 -4
- package/dist/utils/processAtSymbol.js +0 -21
|
@@ -168,11 +168,13 @@ export async function checkTable(tables) {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
else if (fieldType === "string" || fieldType === "array_string" || fieldType === "array_number_string") {
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
// 约束:string/array_*_string 必须声明 max。
|
|
172
|
+
// 说明:array_*_string 的 max 表示“单个元素字符串长度”,不是数组元素数量。
|
|
173
|
+
if (fieldMax === undefined || fieldMax === null || typeof fieldMax !== "number") {
|
|
174
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `必须设置 max 且类型为数字;其中 array_*_string 的 max 表示单个元素长度,当前为 "${fieldMax}"`);
|
|
173
175
|
hasError = true;
|
|
174
176
|
}
|
|
175
|
-
else if (fieldMax
|
|
177
|
+
else if (fieldMax > MAX_VARCHAR_LENGTH) {
|
|
176
178
|
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
|
|
177
179
|
hasError = true;
|
|
178
180
|
}
|
package/dist/hooks/validator.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// 相对导入
|
|
2
2
|
import { Validator } from "../lib/validator";
|
|
3
|
+
import { normalizeFieldDefinition } from "../utils/normalizeFieldDefinition";
|
|
3
4
|
import { ErrorResponse } from "../utils/response";
|
|
4
5
|
import { isPlainObject, snakeCase } from "../utils/util";
|
|
5
6
|
/**
|
|
@@ -22,6 +23,7 @@ const validatorHook = {
|
|
|
22
23
|
const rawBody = isPlainObject(ctx.body) ? ctx.body : {};
|
|
23
24
|
const nextBody = {};
|
|
24
25
|
for (const [field, fieldDef] of Object.entries(ctx.api.fields)) {
|
|
26
|
+
const normalized = normalizeFieldDefinition(fieldDef);
|
|
25
27
|
let value = rawBody[field];
|
|
26
28
|
if (value === undefined) {
|
|
27
29
|
const snakeField = snakeCase(field);
|
|
@@ -30,8 +32,8 @@ const validatorHook = {
|
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
// 字段未传值且定义了默认值时,应用默认值
|
|
33
|
-
if (value === undefined &&
|
|
34
|
-
value =
|
|
35
|
+
if (value === undefined && normalized.default !== null) {
|
|
36
|
+
value = normalized.default;
|
|
35
37
|
}
|
|
36
38
|
if (value !== undefined) {
|
|
37
39
|
nextBody[field] = value;
|
package/dist/index.js
CHANGED
|
@@ -52,14 +52,14 @@ export class Befly {
|
|
|
52
52
|
try {
|
|
53
53
|
const serverStartTime = Bun.nanoseconds();
|
|
54
54
|
// 0. 加载配置
|
|
55
|
-
this.config = await loadBeflyConfig(env
|
|
55
|
+
this.config = await loadBeflyConfig(env["NODE_ENV"] || "development");
|
|
56
56
|
// 将配置注入到 ctx,供插件/Hook/sync 等按需读取
|
|
57
57
|
this.context.config = this.config;
|
|
58
58
|
// 给插件/Hook/sync 一个统一读取 env 的入口(只从 start 入参注入)
|
|
59
59
|
this.context.env = env;
|
|
60
60
|
const { apis, tables, plugins, hooks, addons } = await scanSources();
|
|
61
61
|
// 让后续 syncMenu 能拿到 addon 的 views 路径等信息
|
|
62
|
-
this.context
|
|
62
|
+
this.context["addons"] = addons;
|
|
63
63
|
await checkApi(apis);
|
|
64
64
|
await checkTable(tables);
|
|
65
65
|
await checkPlugin(plugins);
|
|
@@ -88,7 +88,14 @@ export class Befly {
|
|
|
88
88
|
await syncTable(this.context, tables);
|
|
89
89
|
await syncApi(this.context, apis);
|
|
90
90
|
await syncMenu(this.context, checkedMenus);
|
|
91
|
-
|
|
91
|
+
const devEmail = this.config.devEmail;
|
|
92
|
+
const devPassword = this.config.devPassword;
|
|
93
|
+
if (typeof devEmail === "string" && devEmail.length > 0 && typeof devPassword === "string" && devPassword.length > 0) {
|
|
94
|
+
await syncDev(this.context, { devEmail: devEmail, devPassword: devPassword });
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
Logger.debug("跳过 syncDev:未配置 devEmail/devPassword");
|
|
98
|
+
}
|
|
92
99
|
// 缓存同步:统一在所有同步完成后执行(cacheApis + cacheMenus + rebuildRoleApiPermissions)
|
|
93
100
|
await syncCache(this.context);
|
|
94
101
|
// 3. 加载钩子
|
|
@@ -98,9 +105,11 @@ export class Befly {
|
|
|
98
105
|
// 6. 启动 HTTP服务器
|
|
99
106
|
const apiFetch = apiHandler(this.apis, this.hooks, this.context);
|
|
100
107
|
const staticFetch = staticHandler(this.config.cors);
|
|
108
|
+
const port = typeof this.config.appPort === "number" ? this.config.appPort : 3000;
|
|
109
|
+
const hostname = typeof this.config.appHost === "string" && this.config.appHost.length > 0 ? this.config.appHost : "0.0.0.0";
|
|
101
110
|
const server = Bun.serve({
|
|
102
|
-
port:
|
|
103
|
-
hostname:
|
|
111
|
+
port: port,
|
|
112
|
+
hostname: hostname,
|
|
104
113
|
// 开发模式下启用详细错误信息
|
|
105
114
|
development: this.config.nodeEnv === "development",
|
|
106
115
|
// 空闲连接超时时间(秒),防止恶意连接占用资源
|
package/dist/lib/asyncContext.js
CHANGED
|
@@ -20,8 +20,8 @@ export function setCtxUser(userId, roleCode, nickname, roleType) {
|
|
|
20
20
|
const store = storage.getStore();
|
|
21
21
|
if (!store)
|
|
22
22
|
return;
|
|
23
|
-
store.userId = userId;
|
|
24
|
-
store.roleCode = roleCode;
|
|
25
|
-
store.nickname = nickname;
|
|
26
|
-
store.roleType = roleType;
|
|
23
|
+
store.userId = userId === undefined ? null : userId;
|
|
24
|
+
store.roleCode = roleCode === undefined ? null : roleCode;
|
|
25
|
+
store.nickname = nickname === undefined ? null : nickname;
|
|
26
|
+
store.roleType = roleType === undefined ? null : roleType;
|
|
27
27
|
}
|
package/dist/lib/dbHelper.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* 数据库助手 - TypeScript 版本
|
|
3
3
|
* 提供数据库 CRUD 操作的封装
|
|
4
4
|
*/
|
|
5
|
-
import type { WhereConditions } from "../types/common";
|
|
6
|
-
import type { QueryOptions, InsertOptions, UpdateOptions, DeleteOptions,
|
|
5
|
+
import type { WhereConditions, SqlValue } from "../types/common";
|
|
6
|
+
import type { QueryOptions, InsertOptions, UpdateOptions, DeleteOptions, DbPageResult, DbListResult, TransactionCallback, DbResult, ListSql } from "../types/database";
|
|
7
7
|
import type { DbDialect } from "./dbDialect";
|
|
8
8
|
type RedisCacheLike = {
|
|
9
9
|
getObject<T = any>(key: string): Promise<T | null>;
|
|
@@ -42,7 +42,7 @@ export declare class DbHelper {
|
|
|
42
42
|
* - 复用当前 DbHelper 持有的连接/事务
|
|
43
43
|
* - 统一走 executeWithConn,保持参数校验与错误行为一致
|
|
44
44
|
*/
|
|
45
|
-
unsafe(sqlStr: string, params?:
|
|
45
|
+
unsafe<TResult = unknown>(sqlStr: string, params?: unknown[]): Promise<DbResult<TResult>>;
|
|
46
46
|
/**
|
|
47
47
|
* 检查表是否存在
|
|
48
48
|
* @param tableName - 表名(支持小驼峰,会自动转换为下划线)
|
|
@@ -72,6 +72,12 @@ export declare class DbHelper {
|
|
|
72
72
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换;联查时可带别名如 'order o')
|
|
73
73
|
* @param options.fields - 字段列表(联查时需带表别名,如 'o.id', 'u.username')
|
|
74
74
|
* @param options.joins - 多表联查选项
|
|
75
|
+
* @returns DbResult<TItem>
|
|
76
|
+
*
|
|
77
|
+
* 语义说明(重要):
|
|
78
|
+
* - 本方法不再用 `null` 表示“未命中”。
|
|
79
|
+
* - 当查询未命中(或数据反序列化失败)时,`data` 将返回空对象 `{}`。
|
|
80
|
+
* - 因此业务侧应通过关键字段判断是否存在(例如 `if (!res.data?.id) { ... }`)。
|
|
75
81
|
* @example
|
|
76
82
|
* // 单表查询
|
|
77
83
|
* getOne({ table: 'userProfile', fields: ['userId', 'userName'] })
|
|
@@ -83,7 +89,15 @@ export declare class DbHelper {
|
|
|
83
89
|
* where: { 'o.id': 1 }
|
|
84
90
|
* })
|
|
85
91
|
*/
|
|
86
|
-
getOne<
|
|
92
|
+
getOne<TItem extends Record<string, unknown> = Record<string, unknown>>(options: QueryOptions): Promise<DbResult<TItem>>;
|
|
93
|
+
/**
|
|
94
|
+
* 语义化别名:getDetail(与 getOne 一致)
|
|
95
|
+
*
|
|
96
|
+
* 说明:Befly 早期业务侧习惯用 getDetail 表达“查详情”;这里不引入新的查询逻辑,直接复用 getOne。
|
|
97
|
+
*
|
|
98
|
+
* 语义说明:与 getOne 完全一致,未命中时 `data` 返回 `{}`。
|
|
99
|
+
*/
|
|
100
|
+
getDetail<TItem extends Record<string, unknown> = Record<string, unknown>>(options: QueryOptions): Promise<DbResult<TItem>>;
|
|
87
101
|
/**
|
|
88
102
|
* 查询列表(带分页)
|
|
89
103
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换;联查时可带别名)
|
|
@@ -106,7 +120,7 @@ export declare class DbHelper {
|
|
|
106
120
|
* limit: 10
|
|
107
121
|
* })
|
|
108
122
|
*/
|
|
109
|
-
getList<
|
|
123
|
+
getList<TItem extends Record<string, unknown> = Record<string, unknown>>(options: QueryOptions): Promise<DbResult<DbPageResult<TItem>, ListSql>>;
|
|
110
124
|
/**
|
|
111
125
|
* 查询所有数据(不分页,有上限保护)
|
|
112
126
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换;联查时可带别名)
|
|
@@ -124,23 +138,25 @@ export declare class DbHelper {
|
|
|
124
138
|
* where: { 'o.state': 1 }
|
|
125
139
|
* })
|
|
126
140
|
*/
|
|
127
|
-
getAll<
|
|
141
|
+
getAll<TItem extends Record<string, unknown> = Record<string, unknown>>(options: Omit<QueryOptions, "page" | "limit">): Promise<DbResult<DbListResult<TItem>, ListSql>>;
|
|
128
142
|
/**
|
|
129
143
|
* 插入数据(自动生成 ID、时间戳、state)
|
|
130
144
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
131
145
|
*/
|
|
132
|
-
insData(options: InsertOptions
|
|
146
|
+
insData<TInsert extends Record<string, SqlValue> = Record<string, SqlValue>>(options: Omit<InsertOptions, "data"> & {
|
|
147
|
+
data: TInsert | TInsert[];
|
|
148
|
+
}): Promise<DbResult<number>>;
|
|
133
149
|
/**
|
|
134
150
|
* 批量插入数据(真正的批量操作)
|
|
135
151
|
* 使用 INSERT INTO ... VALUES (...), (...), (...) 语法
|
|
136
152
|
* 自动生成系统字段并包装在事务中
|
|
137
153
|
* @param table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
138
154
|
*/
|
|
139
|
-
insBatch(table: string, dataList:
|
|
155
|
+
insBatch<TInsert extends Record<string, SqlValue> = Record<string, SqlValue>>(table: string, dataList: TInsert[]): Promise<DbResult<number[]>>;
|
|
140
156
|
delForceBatch(table: string, ids: number[]): Promise<DbResult<number>>;
|
|
141
157
|
updBatch(table: string, dataList: Array<{
|
|
142
158
|
id: number;
|
|
143
|
-
data: Record<string,
|
|
159
|
+
data: Record<string, unknown>;
|
|
144
160
|
}>): Promise<DbResult<number>>;
|
|
145
161
|
/**
|
|
146
162
|
* 更新数据(强制更新时间戳,系统字段不可修改)
|
|
@@ -171,11 +187,11 @@ export declare class DbHelper {
|
|
|
171
187
|
* 执行事务
|
|
172
188
|
* 使用 Bun SQL 的 begin 方法开启事务
|
|
173
189
|
*/
|
|
174
|
-
trans<
|
|
190
|
+
trans<TResult = unknown>(callback: TransactionCallback<TResult, DbHelper>): Promise<TResult>;
|
|
175
191
|
/**
|
|
176
192
|
* 执行原始 SQL
|
|
177
193
|
*/
|
|
178
|
-
query(sql: string, params?:
|
|
194
|
+
query<TResult = unknown>(sql: string, params?: unknown[]): Promise<DbResult<TResult>>;
|
|
179
195
|
/**
|
|
180
196
|
* 检查数据是否存在(优化性能)
|
|
181
197
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
@@ -185,9 +201,9 @@ export declare class DbHelper {
|
|
|
185
201
|
* 查询单个字段值(带字段名验证)
|
|
186
202
|
* @param field - 字段名(支持小驼峰或下划线格式)
|
|
187
203
|
*/
|
|
188
|
-
getFieldValue<
|
|
204
|
+
getFieldValue<TValue = unknown>(options: Omit<QueryOptions, "fields"> & {
|
|
189
205
|
field: string;
|
|
190
|
-
}): Promise<DbResult<
|
|
206
|
+
}): Promise<DbResult<TValue | null>>;
|
|
191
207
|
/**
|
|
192
208
|
* 自增字段(安全实现,防止 SQL 注入)
|
|
193
209
|
* @param table - 表名(支持小驼峰或下划线格式,会自动转换)
|
package/dist/lib/dbHelper.js
CHANGED
|
@@ -28,7 +28,7 @@ export class DbHelper {
|
|
|
28
28
|
constructor(options) {
|
|
29
29
|
this.redis = options.redis;
|
|
30
30
|
this.sql = options.sql || null;
|
|
31
|
-
this.isTransaction =
|
|
31
|
+
this.isTransaction = Boolean(options.sql);
|
|
32
32
|
// 默认使用 MySQL 方言(当前 core 的表结构/语法也主要基于 MySQL)
|
|
33
33
|
this.dialect = options.dialect ? options.dialect : new MySqlDialect();
|
|
34
34
|
}
|
|
@@ -217,10 +217,11 @@ export class DbHelper {
|
|
|
217
217
|
*/
|
|
218
218
|
async getCount(options) {
|
|
219
219
|
const { table, where, joins, tableQualifier } = await this.prepareQueryOptions(options);
|
|
220
|
+
const hasJoins = Array.isArray(joins) && joins.length > 0;
|
|
220
221
|
const builder = this.createSqlBuilder()
|
|
221
222
|
.selectRaw("COUNT(*) as count")
|
|
222
223
|
.from(table)
|
|
223
|
-
.where(DbUtils.addDefaultStateFilter(where, tableQualifier,
|
|
224
|
+
.where(DbUtils.addDefaultStateFilter(where, tableQualifier, hasJoins));
|
|
224
225
|
// 添加 JOIN
|
|
225
226
|
this.applyJoins(builder, joins);
|
|
226
227
|
const { sql, params } = builder.toSelectSql();
|
|
@@ -236,6 +237,12 @@ export class DbHelper {
|
|
|
236
237
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换;联查时可带别名如 'order o')
|
|
237
238
|
* @param options.fields - 字段列表(联查时需带表别名,如 'o.id', 'u.username')
|
|
238
239
|
* @param options.joins - 多表联查选项
|
|
240
|
+
* @returns DbResult<TItem>
|
|
241
|
+
*
|
|
242
|
+
* 语义说明(重要):
|
|
243
|
+
* - 本方法不再用 `null` 表示“未命中”。
|
|
244
|
+
* - 当查询未命中(或数据反序列化失败)时,`data` 将返回空对象 `{}`。
|
|
245
|
+
* - 因此业务侧应通过关键字段判断是否存在(例如 `if (!res.data?.id) { ... }`)。
|
|
239
246
|
* @example
|
|
240
247
|
* // 单表查询
|
|
241
248
|
* getOne({ table: 'userProfile', fields: ['userId', 'userName'] })
|
|
@@ -249,10 +256,11 @@ export class DbHelper {
|
|
|
249
256
|
*/
|
|
250
257
|
async getOne(options) {
|
|
251
258
|
const { table, fields, where, joins, tableQualifier } = await this.prepareQueryOptions(options);
|
|
259
|
+
const hasJoins = Array.isArray(joins) && joins.length > 0;
|
|
252
260
|
const builder = this.createSqlBuilder()
|
|
253
261
|
.select(fields)
|
|
254
262
|
.from(table)
|
|
255
|
-
.where(DbUtils.addDefaultStateFilter(where, tableQualifier,
|
|
263
|
+
.where(DbUtils.addDefaultStateFilter(where, tableQualifier, hasJoins));
|
|
256
264
|
// 添加 JOIN
|
|
257
265
|
this.applyJoins(builder, joins);
|
|
258
266
|
const { sql, params } = builder.toSelectSql();
|
|
@@ -262,7 +270,7 @@ export class DbHelper {
|
|
|
262
270
|
const row = result?.[0] || null;
|
|
263
271
|
if (!row) {
|
|
264
272
|
return {
|
|
265
|
-
data:
|
|
273
|
+
data: {},
|
|
266
274
|
sql: execRes.sql
|
|
267
275
|
};
|
|
268
276
|
}
|
|
@@ -271,17 +279,28 @@ export class DbHelper {
|
|
|
271
279
|
const deserialized = DbUtils.deserializeArrayFields(camelRow);
|
|
272
280
|
if (!deserialized) {
|
|
273
281
|
return {
|
|
274
|
-
data:
|
|
282
|
+
data: {},
|
|
275
283
|
sql: execRes.sql
|
|
276
284
|
};
|
|
277
285
|
}
|
|
278
286
|
// 转换 BIGINT 字段(id, pid 等)为数字类型
|
|
279
|
-
const
|
|
287
|
+
const convertedList = convertBigIntFields([deserialized]);
|
|
288
|
+
const data = convertedList[0] ?? deserialized;
|
|
280
289
|
return {
|
|
281
290
|
data: data,
|
|
282
291
|
sql: execRes.sql
|
|
283
292
|
};
|
|
284
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* 语义化别名:getDetail(与 getOne 一致)
|
|
296
|
+
*
|
|
297
|
+
* 说明:Befly 早期业务侧习惯用 getDetail 表达“查详情”;这里不引入新的查询逻辑,直接复用 getOne。
|
|
298
|
+
*
|
|
299
|
+
* 语义说明:与 getOne 完全一致,未命中时 `data` 返回 `{}`。
|
|
300
|
+
*/
|
|
301
|
+
async getDetail(options) {
|
|
302
|
+
return await this.getOne(options);
|
|
303
|
+
}
|
|
285
304
|
/**
|
|
286
305
|
* 查询列表(带分页)
|
|
287
306
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换;联查时可带别名)
|
|
@@ -314,7 +333,7 @@ export class DbHelper {
|
|
|
314
333
|
throw new Error(`每页数量必须在 1 到 1000 之间 (table: ${options.table}, page: ${prepared.page}, limit: ${prepared.limit})`);
|
|
315
334
|
}
|
|
316
335
|
// 构建查询
|
|
317
|
-
const whereFiltered = DbUtils.addDefaultStateFilter(prepared.where, prepared.tableQualifier,
|
|
336
|
+
const whereFiltered = DbUtils.addDefaultStateFilter(prepared.where, prepared.tableQualifier, Array.isArray(prepared.joins) && prepared.joins.length > 0);
|
|
318
337
|
// 查询总数
|
|
319
338
|
const countBuilder = this.createSqlBuilder().selectRaw("COUNT(*) as total").from(prepared.table).where(whereFiltered);
|
|
320
339
|
// 添加 JOIN(计数也需要)
|
|
@@ -389,8 +408,21 @@ export class DbHelper {
|
|
|
389
408
|
// 添加硬性上限保护,防止内存溢出
|
|
390
409
|
const MAX_LIMIT = 10000;
|
|
391
410
|
const WARNING_LIMIT = 1000;
|
|
392
|
-
const
|
|
393
|
-
|
|
411
|
+
const prepareOptions = {
|
|
412
|
+
table: options.table,
|
|
413
|
+
page: 1,
|
|
414
|
+
limit: 10
|
|
415
|
+
};
|
|
416
|
+
if (options.fields !== undefined)
|
|
417
|
+
prepareOptions.fields = options.fields;
|
|
418
|
+
if (options.where !== undefined)
|
|
419
|
+
prepareOptions.where = options.where;
|
|
420
|
+
if (options.joins !== undefined)
|
|
421
|
+
prepareOptions.joins = options.joins;
|
|
422
|
+
if (options.orderBy !== undefined)
|
|
423
|
+
prepareOptions.orderBy = options.orderBy;
|
|
424
|
+
const prepared = await this.prepareQueryOptions(prepareOptions);
|
|
425
|
+
const whereFiltered = DbUtils.addDefaultStateFilter(prepared.where, prepared.tableQualifier, Array.isArray(prepared.joins) && prepared.joins.length > 0);
|
|
394
426
|
// 查询真实总数
|
|
395
427
|
const countBuilder = this.createSqlBuilder().selectRaw("COUNT(*) as total").from(prepared.table).where(whereFiltered);
|
|
396
428
|
// 添加 JOIN(计数也需要)
|
|
@@ -468,7 +500,7 @@ export class DbHelper {
|
|
|
468
500
|
const { sql, params } = builder.toInsertSql(snakeTable, processed);
|
|
469
501
|
// 执行
|
|
470
502
|
const execRes = await this.executeWithConn(sql, params);
|
|
471
|
-
const insertedId = processed
|
|
503
|
+
const insertedId = processed["id"] || execRes.data?.lastInsertRowid || 0;
|
|
472
504
|
return {
|
|
473
505
|
data: insertedId,
|
|
474
506
|
sql: execRes.sql
|
|
@@ -504,7 +536,11 @@ export class DbHelper {
|
|
|
504
536
|
const now = Date.now();
|
|
505
537
|
// 处理所有数据(自动添加系统字段)
|
|
506
538
|
const processedList = dataList.map((data, index) => {
|
|
507
|
-
|
|
539
|
+
const id = ids[index];
|
|
540
|
+
if (typeof id !== "number") {
|
|
541
|
+
throw new Error(`批量插入生成 ID 失败:ids[${index}] 不是 number (table: ${snakeTable})`);
|
|
542
|
+
}
|
|
543
|
+
return DbUtils.buildInsertRow({ data: data, id: id, now: now });
|
|
508
544
|
});
|
|
509
545
|
// 入口校验:保证进入 SqlBuilder 的批量数据结构一致且无 undefined
|
|
510
546
|
const insertFields = SqlCheck.assertBatchInsertRowsConsistent(processedList, { table: snakeTable });
|
|
@@ -730,24 +766,29 @@ export class DbHelper {
|
|
|
730
766
|
* @param field - 字段名(支持小驼峰或下划线格式)
|
|
731
767
|
*/
|
|
732
768
|
async getFieldValue(options) {
|
|
733
|
-
const
|
|
769
|
+
const field = options.field;
|
|
734
770
|
// 验证字段名格式(只允许字母、数字、下划线)
|
|
735
771
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) {
|
|
736
772
|
throw new Error(`无效的字段名: ${field},只允许字母、数字和下划线`);
|
|
737
773
|
}
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
774
|
+
const oneOptions = {
|
|
775
|
+
table: options.table
|
|
776
|
+
};
|
|
777
|
+
if (options.where !== undefined)
|
|
778
|
+
oneOptions.where = options.where;
|
|
779
|
+
if (options.joins !== undefined)
|
|
780
|
+
oneOptions.joins = options.joins;
|
|
781
|
+
if (options.orderBy !== undefined)
|
|
782
|
+
oneOptions.orderBy = options.orderBy;
|
|
783
|
+
if (options.page !== undefined)
|
|
784
|
+
oneOptions.page = options.page;
|
|
785
|
+
if (options.limit !== undefined)
|
|
786
|
+
oneOptions.limit = options.limit;
|
|
787
|
+
oneOptions.fields = [field];
|
|
788
|
+
const oneRes = await this.getOne(oneOptions);
|
|
742
789
|
const result = oneRes.data;
|
|
743
|
-
if (!result) {
|
|
744
|
-
return {
|
|
745
|
-
data: null,
|
|
746
|
-
sql: oneRes.sql
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
790
|
// 尝试直接访问字段(小驼峰)
|
|
750
|
-
if (field
|
|
791
|
+
if (Object.hasOwn(result, field)) {
|
|
751
792
|
return {
|
|
752
793
|
data: result[field],
|
|
753
794
|
sql: oneRes.sql
|
|
@@ -755,7 +796,7 @@ export class DbHelper {
|
|
|
755
796
|
}
|
|
756
797
|
// 转换为小驼峰格式再尝试访问(支持用户传入下划线格式)
|
|
757
798
|
const camelField = field.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
758
|
-
if (camelField !== field && camelField
|
|
799
|
+
if (camelField !== field && Object.hasOwn(result, camelField)) {
|
|
759
800
|
return {
|
|
760
801
|
data: result[camelField],
|
|
761
802
|
sql: oneRes.sql
|
|
@@ -763,7 +804,7 @@ export class DbHelper {
|
|
|
763
804
|
}
|
|
764
805
|
// 转换为下划线格式再尝试访问(支持用户传入小驼峰格式)
|
|
765
806
|
const snakeField = field.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
766
|
-
if (snakeField !== field && snakeField
|
|
807
|
+
if (snakeField !== field && Object.hasOwn(result, snakeField)) {
|
|
767
808
|
return {
|
|
768
809
|
data: result[snakeField],
|
|
769
810
|
sql: oneRes.sql
|
package/dist/lib/dbUtils.js
CHANGED
|
@@ -10,18 +10,44 @@ export class DbUtils {
|
|
|
10
10
|
throw new Error("tableRef 不能为空");
|
|
11
11
|
}
|
|
12
12
|
const parts = trimmed.split(/\s+/).filter((p) => p.length > 0);
|
|
13
|
+
if (parts.length === 0) {
|
|
14
|
+
throw new Error("tableRef 不能为空");
|
|
15
|
+
}
|
|
13
16
|
if (parts.length > 2) {
|
|
14
17
|
throw new Error(`不支持的表引用格式(包含过多片段)。请使用最简形式:table 或 table alias 或 schema.table 或 schema.table alias (tableRef: ${trimmed})`);
|
|
15
18
|
}
|
|
16
19
|
const namePart = parts[0];
|
|
17
|
-
|
|
20
|
+
if (typeof namePart !== "string" || namePart.trim() === "") {
|
|
21
|
+
throw new Error(`tableRef 解析失败:缺少表名 (tableRef: ${trimmed})`);
|
|
22
|
+
}
|
|
23
|
+
let aliasPart = null;
|
|
24
|
+
if (parts.length === 2) {
|
|
25
|
+
const alias = parts[1];
|
|
26
|
+
if (typeof alias !== "string" || alias.trim() === "") {
|
|
27
|
+
throw new Error(`tableRef 解析失败:缺少 alias (tableRef: ${trimmed})`);
|
|
28
|
+
}
|
|
29
|
+
aliasPart = alias;
|
|
30
|
+
}
|
|
18
31
|
const nameSegments = namePart.split(".");
|
|
19
32
|
if (nameSegments.length > 2) {
|
|
20
33
|
throw new Error(`不支持的表引用格式(schema 层级过深) (tableRef: ${trimmed})`);
|
|
21
34
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
if (nameSegments.length === 2) {
|
|
36
|
+
const schema = nameSegments[0];
|
|
37
|
+
const table = nameSegments[1];
|
|
38
|
+
if (typeof schema !== "string" || schema.trim() === "") {
|
|
39
|
+
throw new Error(`tableRef 解析失败:schema 为空 (tableRef: ${trimmed})`);
|
|
40
|
+
}
|
|
41
|
+
if (typeof table !== "string" || table.trim() === "") {
|
|
42
|
+
throw new Error(`tableRef 解析失败:table 为空 (tableRef: ${trimmed})`);
|
|
43
|
+
}
|
|
44
|
+
return { schema: schema, table: table, alias: aliasPart };
|
|
45
|
+
}
|
|
46
|
+
const table = nameSegments[0];
|
|
47
|
+
if (typeof table !== "string" || table.trim() === "") {
|
|
48
|
+
throw new Error(`tableRef 解析失败:table 为空 (tableRef: ${trimmed})`);
|
|
49
|
+
}
|
|
50
|
+
return { schema: null, table: table, alias: aliasPart };
|
|
25
51
|
}
|
|
26
52
|
/**
|
|
27
53
|
* 规范化表引用:只 snakeCase schema/table,本身 alias 保持原样。
|
|
@@ -122,7 +148,15 @@ export class DbUtils {
|
|
|
122
148
|
if (typeof item !== "string" || !item.includes("#")) {
|
|
123
149
|
return item;
|
|
124
150
|
}
|
|
125
|
-
const
|
|
151
|
+
const parts = item.split("#");
|
|
152
|
+
if (parts.length !== 2) {
|
|
153
|
+
return item;
|
|
154
|
+
}
|
|
155
|
+
const field = parts[0];
|
|
156
|
+
const direction = parts[1];
|
|
157
|
+
if (typeof field !== "string" || typeof direction !== "string") {
|
|
158
|
+
return item;
|
|
159
|
+
}
|
|
126
160
|
return `${snakeCase(field.trim())}#${direction.trim()}`;
|
|
127
161
|
});
|
|
128
162
|
}
|
|
@@ -134,15 +168,28 @@ export class DbUtils {
|
|
|
134
168
|
// 处理别名 AS
|
|
135
169
|
if (field.toUpperCase().includes(" AS ")) {
|
|
136
170
|
const parts = field.split(/\s+AS\s+/i);
|
|
137
|
-
const fieldPart = parts[0]
|
|
138
|
-
const aliasPart = parts[1]
|
|
139
|
-
|
|
171
|
+
const fieldPart = parts[0];
|
|
172
|
+
const aliasPart = parts[1];
|
|
173
|
+
if (typeof fieldPart !== "string" || typeof aliasPart !== "string") {
|
|
174
|
+
return field;
|
|
175
|
+
}
|
|
176
|
+
return `${DbUtils.processJoinField(fieldPart.trim())} AS ${aliasPart.trim()}`;
|
|
140
177
|
}
|
|
141
178
|
// 处理表别名.字段名(JOIN 模式下,点号前面通常是别名,不应被 snakeCase 改写)
|
|
142
179
|
if (field.includes(".")) {
|
|
143
180
|
const parts = field.split(".");
|
|
144
|
-
|
|
145
|
-
|
|
181
|
+
if (parts.length < 2) {
|
|
182
|
+
return snakeCase(field);
|
|
183
|
+
}
|
|
184
|
+
// 防御:避免 a..b / a. / .a 等输入
|
|
185
|
+
if (parts.some((p) => p.trim() === "")) {
|
|
186
|
+
return field;
|
|
187
|
+
}
|
|
188
|
+
const fieldName = parts[parts.length - 1];
|
|
189
|
+
const tableName = parts.slice(0, parts.length - 1).join(".");
|
|
190
|
+
if (typeof fieldName !== "string" || typeof tableName !== "string") {
|
|
191
|
+
return snakeCase(field);
|
|
192
|
+
}
|
|
146
193
|
return `${tableName.trim()}.${snakeCase(fieldName)}`;
|
|
147
194
|
}
|
|
148
195
|
// 普通字段
|
|
@@ -160,8 +207,18 @@ export class DbUtils {
|
|
|
160
207
|
const operator = key.substring(lastDollarIndex);
|
|
161
208
|
if (fieldPart.includes(".")) {
|
|
162
209
|
const parts = fieldPart.split(".");
|
|
163
|
-
|
|
164
|
-
|
|
210
|
+
if (parts.length < 2) {
|
|
211
|
+
return `${snakeCase(fieldPart)}${operator}`;
|
|
212
|
+
}
|
|
213
|
+
// 防御:避免 a..b / a. / .a 等输入
|
|
214
|
+
if (parts.some((p) => p.trim() === "")) {
|
|
215
|
+
return `${snakeCase(fieldPart)}${operator}`;
|
|
216
|
+
}
|
|
217
|
+
const fieldName = parts[parts.length - 1];
|
|
218
|
+
const tableName = parts.slice(0, parts.length - 1).join(".");
|
|
219
|
+
if (typeof fieldName !== "string" || typeof tableName !== "string") {
|
|
220
|
+
return `${snakeCase(fieldPart)}${operator}`;
|
|
221
|
+
}
|
|
165
222
|
return `${tableName.trim()}.${snakeCase(fieldName)}${operator}`;
|
|
166
223
|
}
|
|
167
224
|
return `${snakeCase(fieldPart)}${operator}`;
|
|
@@ -169,8 +226,18 @@ export class DbUtils {
|
|
|
169
226
|
// 处理表名.字段名
|
|
170
227
|
if (key.includes(".")) {
|
|
171
228
|
const parts = key.split(".");
|
|
172
|
-
|
|
173
|
-
|
|
229
|
+
if (parts.length < 2) {
|
|
230
|
+
return snakeCase(key);
|
|
231
|
+
}
|
|
232
|
+
// 防御:避免 a..b / a. / .a 等输入
|
|
233
|
+
if (parts.some((p) => p.trim() === "")) {
|
|
234
|
+
return snakeCase(key);
|
|
235
|
+
}
|
|
236
|
+
const fieldName = parts[parts.length - 1];
|
|
237
|
+
const tableName = parts.slice(0, parts.length - 1).join(".");
|
|
238
|
+
if (typeof fieldName !== "string" || typeof tableName !== "string") {
|
|
239
|
+
return snakeCase(key);
|
|
240
|
+
}
|
|
174
241
|
return `${tableName.trim()}.${snakeCase(fieldName)}`;
|
|
175
242
|
}
|
|
176
243
|
// 普通字段
|
|
@@ -206,7 +273,15 @@ export class DbUtils {
|
|
|
206
273
|
if (typeof item !== "string" || !item.includes("#")) {
|
|
207
274
|
return item;
|
|
208
275
|
}
|
|
209
|
-
const
|
|
276
|
+
const parts = item.split("#");
|
|
277
|
+
if (parts.length !== 2) {
|
|
278
|
+
return item;
|
|
279
|
+
}
|
|
280
|
+
const field = parts[0];
|
|
281
|
+
const direction = parts[1];
|
|
282
|
+
if (typeof field !== "string" || typeof direction !== "string") {
|
|
283
|
+
return item;
|
|
284
|
+
}
|
|
210
285
|
return `${DbUtils.processJoinField(field.trim())}#${direction.trim()}`;
|
|
211
286
|
});
|
|
212
287
|
}
|
|
@@ -352,10 +427,10 @@ export class DbUtils {
|
|
|
352
427
|
for (const [key, value] of Object.entries(userData)) {
|
|
353
428
|
result[key] = value;
|
|
354
429
|
}
|
|
355
|
-
result
|
|
356
|
-
result
|
|
357
|
-
result
|
|
358
|
-
result
|
|
430
|
+
result["id"] = options.id;
|
|
431
|
+
result["created_at"] = options.now;
|
|
432
|
+
result["updated_at"] = options.now;
|
|
433
|
+
result["state"] = 1;
|
|
359
434
|
return result;
|
|
360
435
|
}
|
|
361
436
|
static buildUpdateRow(options) {
|
|
@@ -365,7 +440,7 @@ export class DbUtils {
|
|
|
365
440
|
for (const [key, value] of Object.entries(userData)) {
|
|
366
441
|
result[key] = value;
|
|
367
442
|
}
|
|
368
|
-
result
|
|
443
|
+
result["updated_at"] = options.now;
|
|
369
444
|
return result;
|
|
370
445
|
}
|
|
371
446
|
static buildPartialUpdateData(options) {
|