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.
@@ -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
- if (fieldMax !== undefined && (fieldMax === null || typeof fieldMax !== "number")) {
172
- Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
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 !== undefined && fieldMax > MAX_VARCHAR_LENGTH) {
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
  }
@@ -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 && fieldDef?.default !== undefined && fieldDef?.default !== null) {
34
- value = fieldDef.default;
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.NODE_ENV || "development");
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.addons = addons;
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
- await syncDev(this.context, { devEmail: this.config.devEmail, devPassword: this.config.devPassword });
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: this.config.appPort,
103
- hostname: this.config.appHost,
111
+ port: port,
112
+ hostname: hostname,
104
113
  // 开发模式下启用详细错误信息
105
114
  development: this.config.nodeEnv === "development",
106
115
  // 空闲连接超时时间(秒),防止恶意连接占用资源
@@ -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
  }
@@ -6,9 +6,9 @@ type CacheHelperDb = {
6
6
  tableExists(table: string): Promise<{
7
7
  data: boolean;
8
8
  }>;
9
- getAll(options: any): Promise<{
9
+ getAll<TItem = unknown>(options: unknown): Promise<{
10
10
  data: {
11
- lists: any[];
11
+ lists: TItem[];
12
12
  };
13
13
  }>;
14
14
  };
@@ -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, ListResult, AllResult, TransactionCallback, DbResult, ListSql } from "../types/database";
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?: any[]): Promise<DbResult<any>>;
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<T extends Record<string, any> = Record<string, any>>(options: QueryOptions): Promise<DbResult<T | null>>;
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<T extends Record<string, any> = Record<string, any>>(options: QueryOptions): Promise<DbResult<ListResult<T>, ListSql>>;
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<T extends Record<string, any> = Record<string, any>>(options: Omit<QueryOptions, "page" | "limit">): Promise<DbResult<AllResult<T>, ListSql>>;
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): Promise<DbResult<number>>;
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: Record<string, any>[]): Promise<DbResult<number[]>>;
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, any>;
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<T = any>(callback: TransactionCallback<T>): Promise<T>;
190
+ trans<TResult = unknown>(callback: TransactionCallback<TResult, DbHelper>): Promise<TResult>;
175
191
  /**
176
192
  * 执行原始 SQL
177
193
  */
178
- query(sql: string, params?: any[]): Promise<DbResult<any>>;
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<T = any>(options: Omit<QueryOptions, "fields"> & {
204
+ getFieldValue<TValue = unknown>(options: Omit<QueryOptions, "fields"> & {
189
205
  field: string;
190
- }): Promise<DbResult<T | null>>;
206
+ }): Promise<DbResult<TValue | null>>;
191
207
  /**
192
208
  * 自增字段(安全实现,防止 SQL 注入)
193
209
  * @param table - 表名(支持小驼峰或下划线格式,会自动转换)
@@ -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 = !!options.sql;
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, !!joins));
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, !!joins));
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: null,
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: null,
282
+ data: {},
275
283
  sql: execRes.sql
276
284
  };
277
285
  }
278
286
  // 转换 BIGINT 字段(id, pid 等)为数字类型
279
- const data = convertBigIntFields([deserialized])[0];
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, !!prepared.joins);
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 prepared = await this.prepareQueryOptions({ ...options, page: 1, limit: 10 });
393
- const whereFiltered = DbUtils.addDefaultStateFilter(prepared.where, prepared.tableQualifier, !!prepared.joins);
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.id || execRes.data?.lastInsertRowid || 0;
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
- return DbUtils.buildInsertRow({ data: data, id: ids[index], now: now });
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 { field, ...queryOptions } = options;
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 oneRes = await this.getOne({
739
- ...queryOptions,
740
- fields: [field]
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 in result) {
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 in result) {
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 in result) {
807
+ if (snakeField !== field && Object.hasOwn(result, snakeField)) {
767
808
  return {
768
809
  data: result[snakeField],
769
810
  sql: oneRes.sql
@@ -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
- const aliasPart = parts.length === 2 ? parts[1] : null;
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
- const schema = nameSegments.length === 2 ? nameSegments[0] : null;
23
- const table = nameSegments.length === 2 ? nameSegments[1] : nameSegments[0];
24
- return { schema: schema, table: table, alias: aliasPart };
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 [field, direction] = item.split("#");
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].trim();
138
- const aliasPart = parts[1].trim();
139
- return `${DbUtils.processJoinField(fieldPart)} AS ${aliasPart}`;
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
- const tableName = parts[0];
145
- const fieldName = parts[1];
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
- const tableName = parts[0];
164
- const fieldName = parts[1];
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
- const tableName = parts[0];
173
- const fieldName = parts[1];
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 [field, direction] = item.split("#");
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.id = options.id;
356
- result.created_at = options.now;
357
- result.updated_at = options.now;
358
- result.state = 1;
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.updated_at = options.now;
443
+ result["updated_at"] = options.now;
369
444
  return result;
370
445
  }
371
446
  static buildPartialUpdateData(options) {