chanjs 2.8.9 → 2.8.10

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.
@@ -7,7 +7,8 @@ import {
7
7
  CODE_PARAM_MISSING,
8
8
  } from "../response/code.js";
9
9
 
10
- const SORT_FIELD_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
10
+ // 合法标识符:列名或「表别名.列名」(join 查询需显式前缀消歧,如 p.price)
11
+ const SORT_FIELD_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$/;
11
12
  const OPERATORS = { $like:'like', $gt:'>', $gte:'>=', $lt:'<', $lte:'<=', $ne:'<>' };
12
13
 
13
14
  /**
@@ -69,6 +70,52 @@ function applyQuery(dbQuery, query) {
69
70
  return dbQuery;
70
71
  }
71
72
 
73
+ /**
74
+ * URL 平铺查询参数 → applyQuery 条件对象(通用搜索基建)
75
+ *
76
+ * @param {object} query - 平铺查询参数(如 req.query),值为字符串/数字
77
+ * @param {Object<string, Function>} fieldMap - 筛选字段白名单映射表:
78
+ * { urlParam: (value) => 条件片段 | null }
79
+ * - 仅声明的参数会进入查询,未声明的一律忽略(防任意列注入)
80
+ * - 转换函数负责类型收敛与 like 转义(utils.escapeLike),返回 null/undefined 表示无效值,忽略
81
+ * @returns {object} 条件对象,可直接交给 applyQuery 拼接 knex where
82
+ *
83
+ * @example
84
+ * const FILTERS = {
85
+ * keyword: v => ({ title: { $like: `%${escapeLike(v)}%` } }),
86
+ * price_min: v => Number.isFinite(Number(v)) ? { price: { $gte: Number(v) } } : null,
87
+ * };
88
+ * mapUrlFilters({ keyword: "液压", price_min: "100" }, FILTERS);
89
+ * // → { title: { $like: "%液压%" }, price: { $gte: 100 } }
90
+ */
91
+ function mapUrlFilters(query, fieldMap) {
92
+ const filter = {};
93
+ for (const [key, map] of Object.entries(fieldMap || {})) {
94
+ const raw = query?.[key];
95
+ if (raw === undefined || raw === null || String(raw).trim() === "") continue;
96
+ let piece;
97
+ try {
98
+ piece = map(String(raw).trim());
99
+ } catch {
100
+ continue; // 转换函数异常视为无效参数,静默忽略(面向用户输入,前台容错)
101
+ }
102
+ if (!piece || typeof piece !== "object") continue;
103
+ for (const [field, cond] of Object.entries(piece)) {
104
+ const prev = filter[field];
105
+ // 同字段多操作符深合并(如 price 的 $gte 与 $lte 共存成区间)
106
+ if (
107
+ prev && typeof prev === "object" && !Array.isArray(prev) &&
108
+ cond && typeof cond === "object" && !Array.isArray(cond)
109
+ ) {
110
+ filter[field] = { ...prev, ...cond };
111
+ } else {
112
+ filter[field] = cond;
113
+ }
114
+ }
115
+ }
116
+ return filter;
117
+ }
118
+
72
119
  /**
73
120
  * 应用字段白名单 + 排序白名单到 query
74
121
  * @param {import('knex').Knex.QueryBuilder} q
@@ -270,9 +317,8 @@ class Repository extends BaseComponent {
270
317
 
271
318
  /**
272
319
  * 更新内部原语(私有方法):子类无法覆写。
273
- * 历史教训:此前 updateById 直接调 this.update({query,data}),
274
- * 业务 Service 普遍以扁平 body 形状覆写 update(body),多态派发回子类后
275
- * {query,data} 被解构成 id=undefined → 更新静默失效(无 SQL、仍返回旧数据)。
320
+ * 业务 Service 普遍以扁平 body 形状覆写 update(body),若框架内部走多态的 this.update,
321
+ * {query,data} 会被子类签名解构致 id=undefined、更新静默失效。
276
322
  * 框架内部路径必须走本私有方法,杜绝该类命名冲突。
277
323
  */
278
324
  async #updateRaw(query, data) {
@@ -490,4 +536,7 @@ class Repository extends BaseComponent {
490
536
  }
491
537
  }
492
538
 
539
+ // 查询条件组装原语(业务 service 可直接组合使用,不必继承 Repository)
540
+ export { applyQuery, mapUrlFilters };
541
+
493
542
  export default Repository;
@@ -68,9 +68,8 @@ export function registerErrorHandler(chan) {
68
68
  const userId = req.user?.id ?? "-";
69
69
 
70
70
  // 日志分级:业务错误 4xx → warn,系统错误 5xx → error(唯一打印点,杜绝重复刷屏)
71
- // ⚠️ pino 的日志方法依赖 this,不能解引用后裸调(logFn = logger.warn; logFn(...) 会在
72
- // pino/lib/tools.js 抛 TypeError: Cannot read properties of undefined (reading 'Symbol(pino.msgPrefix)'),
73
- // 把真实的业务错误整个掩盖成 500),必须用箭头函数包一层保住 this
71
+ // pino 的日志方法依赖 this,不能解引用后裸调(会抛 TypeError 把真实业务错误掩盖成 500),
72
+ // 必须用箭头函数包一层保住 this
74
73
  const logFn = httpStatus < 500 ? (msg) => logger.warn(msg) : (msg) => logger.error(msg);
75
74
  logFn(JSON.stringify({
76
75
  type: err.name || "UnknownError",
package/core/loader.js CHANGED
@@ -63,8 +63,8 @@ export async function loadController(moduleName) {
63
63
  // 原型方法批量bind,幂等标记避免重复绑定
64
64
  // 遍历整条原型链(含父类继承的方法):子类方法定义在自身原型、父类模板方法定义在父类原型,
65
65
  // 只 bind 直接原型会导致继承方法 this 丢失(Express 非点调用时 this=undefined)。
66
- // ⚠️ 必须用 getOwnPropertyDescriptor 取 value:BaseComponent 的 app/config/db 是 getter-only,
67
- // 其中 db 返回 knex 实例(typeof 为 function),直接 inst[key] 会触发 getter 且赋值只读属性抛错。
66
+ // 必须用 getOwnPropertyDescriptor 取 value:BaseComponent 的 app/config/db 是 getter-only,
67
+ // 其中 db 返回 knex 实例(typeof 为 function),直接 inst[key] 会触发 getter 且赋值只读属性抛错。
68
68
  let proto = Object.getPrototypeOf(inst);
69
69
  while (proto && proto !== Object.prototype) {
70
70
  Object.getOwnPropertyNames(proto).forEach(key => {
package/index.js CHANGED
@@ -22,6 +22,9 @@ export { BaseComponent } from "./core/BaseComponent.js";
22
22
  export { default as Controller } from "./core/Controller.js";
23
23
  export { default as Repository } from "./core/Repository.js";
24
24
  export { Service } from "./core/Service.js";
25
+ // 查询条件组装原语(通用搜索基建:URL 平铺参数 → knex 安全 where)
26
+ export { applyQuery, mapUrlFilters } from "./core/Repository.js";
27
+ export { escapeLike } from "./utils/escape.js";
25
28
 
26
29
  // ===================== 基础设施(事件总线 / 定时任务 / i18n)=====================
27
30
  export { EventBus, event } from "./core/EventBus.js";
@@ -76,7 +76,7 @@ export function validateAll(schemas) {
76
76
  setValidated(req, source, result.data);
77
77
  aggregated[source] = result.data;
78
78
  }
79
- // 聚合多来源校验结果,避免用单一来源覆盖已校验数据(修复原末行用 req.body 覆盖问题)
79
+ // 聚合多来源校验结果,避免用单一来源覆盖已校验数据
80
80
  req.validated = aggregated;
81
81
  next();
82
82
  };
package/middleware/waf.js CHANGED
@@ -19,9 +19,8 @@ const WAF_KNOWN_KEYS = new Set(["enabled", "rateLimit", "ignorePaths", "bodyXssP
19
19
 
20
20
  const TRUSTED_IPS = new Set(["127.0.0.1", "::1"]);
21
21
 
22
- // URL/query 关键词检测保留的分类——只保留真正危险、几乎不会在正常 URL 中出现的攻击特征。
23
- // 去掉 wholeWord / extensions / directories / sensitiveIdentifiers / commandInjection,
24
- // 这些在正常 URL(?debug=1、/internal/、/.well-known/、wget/scp/dd 等)极易误伤正常请求。
22
+ // URL/query 关键词检测只保留真正危险、几乎不会在正常 URL 中出现的攻击特征,
23
+ // 避免 ?debug=1、/internal/、/.well-known/、wget/scp/dd 等正常请求被误伤。
25
24
  const URL_SCAN_CATEGORIES = ["sqlInjection", "xss", "pathTraversal", "encoding"];
26
25
 
27
26
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "chanjs",
4
- "version": "2.8.9",
4
+ "version": "2.8.10",
5
5
  "description": "chanjs基于 Node.js + Express 5 的标准 HMVC 框架(NHMVC),纯 JavaScript(ESM)开发。",
6
6
  "main": "index.js",
7
7
  "module": "index.js",
@@ -0,0 +1,8 @@
1
+ /**
2
+ * LIKE 关键词转义(% _ \),防止用户输入的通配符干扰模糊查询语义。
3
+ * 仅用于用户输入侧(如 mapUrlFilters 的字段映射函数内部);
4
+ * 内部代码构造的查询条件为可信输入,无需转义。
5
+ */
6
+ export function escapeLike(str = "") {
7
+ return String(str).replace(/[%_\\]/g, "\\$&");
8
+ }
package/utils/index.js CHANGED
@@ -45,3 +45,4 @@ export { filterFields } from "./filter.js";
45
45
 
46
46
  // 分页工具
47
47
  export { pages } from "./pages.js";
48
+ export { escapeLike } from "./escape.js";
package/utils/logger.js CHANGED
@@ -24,8 +24,7 @@ const root = pino(
24
24
  * 附加参数序列化:error/warn 的第二参(Error/对象)拼进消息本身。
25
25
  *
26
26
  * 背景:pino-pretty 的 messageFormat 字符串模板只渲染 msg 字段,
27
- * 「logger.error(msg, err)」的 err 会被整体丢弃(连堆栈都不输出),
28
- * 曾导致 Gather.js 加载失败只剩「导入失败 xxx.js:」空冒号、无法定位根因。
27
+ * 「logger.error(msg, err)」的 err 会被整体丢弃(连堆栈都不输出)。
29
28
  * 在封装层统一序列化,保证文档承诺的「第二参数自动解析堆栈」真实生效。
30
29
  * @param {unknown[]} args error/warn 的除消息外全部参数
31
30
  * @returns {string} 可读文本(Error 优先 stack,对象 JSON,其余 String)