chanjs 2.7.15 → 2.7.17

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 CHANGED
@@ -20,7 +20,7 @@
20
20
 
21
21
  ### 核心架构
22
22
 
23
- - 原生基于 Express 5+
23
+ - 原生基于 Express 5+ mysql/pgsql/sqlite 三款数据库支持
24
24
  - 最低运行环境 Node.js 22.18+
25
25
  - 全量 ES Modules(import / export)
26
26
  - **标准 HMVC 架构:模块自治 + 双通道跨模块协作**
package/core/App.js CHANGED
@@ -151,7 +151,7 @@ export default class Chan {
151
151
  const dbKey = dbCfg.key ?? String(idx);
152
152
  let conn;
153
153
  try {
154
- conn = this.dbManager.add(dbKey, dbCfg, { isDefault: idx === 0 });
154
+ conn = await this.dbManager.add(dbKey, dbCfg, { isDefault: idx === 0 });
155
155
  // 首个成功连接即作为默认 this.db,避免 idx===0 失败时 this.db 恒为 null
156
156
  if (!this.db) this.db = conn;
157
157
  } catch (err) {
package/core/Database.js CHANGED
@@ -4,6 +4,29 @@ import logger from "../utils/logger.js";
4
4
  const DEFAULT_NAME = "default";
5
5
  const DEFAULT_SLOW_THRESHOLD = 200; // 慢查询阈值(ms),可运行时调整
6
6
 
7
+ /**
8
+ * 自定义 dialect 别名 → 客户端类(异步工厂)。
9
+ * config 中 client 只写字符串(如 "sqlite"),由框架在此按类型自动挂载驱动,
10
+ * 业务侧无需 import 客户端实现。
11
+ * 必须动态 import:sqlite 适配器依赖内建模块 node:sqlite(Node 22+),
12
+ * 静态引入会在 ESM 加载期解析导致低版本 Node 连 mysql/pg 也无法启动。
13
+ */
14
+ const CUSTOM_CLIENTS = Object.freeze({
15
+ sqlite: () => import("./adaptor/sqliteAdapter.js").then((m) => m.default),
16
+ });
17
+
18
+ /** 把配置中的方言字符串解析为 knex 客户端类(按需加载;无匹配则原样返回) */
19
+ async function resolveClient(config) {
20
+ if (config && typeof config.client === "string") {
21
+ const loader = CUSTOM_CLIENTS[config.client];
22
+ if (loader) {
23
+ const Client = await loader();
24
+ return { ...config, client: Client };
25
+ }
26
+ }
27
+ return config;
28
+ }
29
+
7
30
  /**
8
31
  * 多 Knex 连接管理器:统一管理连接、心跳、批量销毁、慢查询监控。
9
32
  *
@@ -36,10 +59,10 @@ class DatabaseManager {
36
59
  * @param {string} name - 连接标识
37
60
  * @param {object} config - Knex 配置
38
61
  * @param {{isDefault?: boolean}} [opts]
39
- * @returns {import("knex").Knex}
62
+ * @returns {Promise<import("knex").Knex>}
40
63
  */
41
- add(name, config, { isDefault = false } = {}) {
42
- const conn = knex(config);
64
+ async add(name, config, { isDefault = false } = {}) {
65
+ const conn = knex(await resolveClient(config));
43
66
  this._connections.set(name, conn);
44
67
  if (isDefault || this._connections.size === 1) this._defaultName = name;
45
68
 
@@ -256,8 +256,14 @@ class Repository extends BaseComponent {
256
256
  return { success:true, code:CODE_OK, msg:"删除成功", data:{ affectedRows:rows } };
257
257
  }
258
258
 
259
- /** 按条件更新记录 */
260
- async update({ query, data }={}) {
259
+ /**
260
+ * 更新内部原语(私有方法):子类无法覆写。
261
+ * 历史教训:此前 updateById 直接调 this.update({query,data}),
262
+ * 业务 Service 普遍以扁平 body 形状覆写 update(body),多态派发回子类后
263
+ * {query,data} 被解构成 id=undefined → 更新静默失效(无 SQL、仍返回旧数据)。
264
+ * 框架内部路径必须走本私有方法,杜绝该类命名冲突。
265
+ */
266
+ async #updateRaw(query, data) {
261
267
  this._checkDB();
262
268
  if (!query || !data || !Object.keys(query).length || !Object.keys(data).length) {
263
269
  return { success:false, code:CODE_PARAM_INVALID, msg:"参数无效", data:{} };
@@ -270,10 +276,16 @@ class Repository extends BaseComponent {
270
276
  return { success:true, code:CODE_OK, msg:"更新成功", data:{ affectedRows:rows } };
271
277
  }
272
278
 
273
- /** 根据ID更新,返回更新后完整数据(复用 update 走统一 applyQuery 校验) */
279
+ /** 按条件更新记录 */
280
+ async update({ query, data }={}) {
281
+ return this.#updateRaw(query, data);
282
+ }
283
+
284
+ /** 根据ID更新,返回更新后完整数据;失败不再被忽略,直接透传错误体 */
274
285
  async updateById(id, data={}) {
275
286
  if (!id || !Object.keys(data).length) return { success:false, code:CODE_PARAM_INVALID, msg:"参数无效", data:{} };
276
- await this.update({ query: { id }, data });
287
+ const res = await this.#updateRaw({ id }, data);
288
+ if (!res.success) return res;
277
289
  return this.findById(id);
278
290
  }
279
291
 
@@ -0,0 +1,104 @@
1
+ // sqliteAdapter.js
2
+ // 基于 Node 22 内置 node:sqlite(DatabaseSync)实现的 knex SQLite 适配器,零第三方依赖。
3
+ // 仅替换底层连接与执行层,SQL 编译/方言完全复用 knex 自带的 SQLite 驱动,
4
+ // 因此与官方 client:"sqlite3" 产出的 SQL 一致,只是不依赖 sqlite3 npm 包。
5
+ //
6
+ // 挂载方式:config 中 sqlite 分支填写纯字符串 client:"sqlite" 即可,
7
+ // 由 Database.js 在初始化连接时按类型自动映射到本适配器,业务侧无需 import。
8
+ import { createRequire } from "module";
9
+ import { DatabaseSync } from "node:sqlite";
10
+
11
+ const require = createRequire(import.meta.url);
12
+ // 复用 knex 内置 SQLite 方言(SQL 编译器 / schema 编译器 / 事务 / DDL)
13
+ const Client_SQLite = require("knex/lib/dialects/sqlite3/index.js");
14
+
15
+ /**
16
+ * node:sqlite 的绑定值约束较小,这里统一规范化再绑定:
17
+ * - undefined -> null(跳过绑定会报错)
18
+ * - boolean -> 0/1(SQLite 无 bool 类型)
19
+ * - Date -> ISO 字符串(极少数未格式化兜底)
20
+ */
21
+ function toSqliteValue(value) {
22
+ if (value === undefined) return null;
23
+ if (typeof value === "boolean") return value ? 1 : 0;
24
+ if (value instanceof Date) return value.toISOString();
25
+ return value;
26
+ }
27
+
28
+ /** 归一化位置参数与具名参数两种绑定形态 */
29
+ function normalizeBindings(bindings) {
30
+ if (bindings == null) return [];
31
+ if (Array.isArray(bindings)) return bindings.map(toSqliteValue);
32
+ if (typeof bindings === "object") return Object.values(bindings).map(toSqliteValue);
33
+ return bindings;
34
+ }
35
+
36
+ class SqliteAdapter extends Client_SQLite {
37
+ // 官方客户端 _driver() 会 require("sqlite3"),这里改为静态标志,
38
+ // 避免任何路径触发未安装的 sqlite3 包(本项目不缓存驱动实例)。
39
+ _driver() {
40
+ return { OPEN_READWRITE: 0x00000002, OPEN_CREATE: 0x00000004 };
41
+ }
42
+
43
+ // 用内置 node:sqlite 同步打开连接(knex 接受同步返回值)
44
+ acquireRawConnection() {
45
+ const filename = this.connectionSettings?.filename || ":memory:";
46
+ const conn = new DatabaseSync(filename);
47
+ // 与官方 sqlite3 客户端默认口径一致,关闭外键强制以免种子/迁移级联约束影响
48
+ try {
49
+ conn.exec("PRAGMA foreign_keys = OFF");
50
+ } catch {
51
+ /* 忽略 */
52
+ }
53
+ return conn;
54
+ }
55
+
56
+ async destroyRawConnection(connection) {
57
+ try {
58
+ connection.close();
59
+ } catch {
60
+ /* 忽略 */
61
+ }
62
+ }
63
+
64
+ // 核心:用 node:sqlite 同步 API 执行 knex 编译好的 SQL。
65
+ // knex 的 SQLite 编译器产出 `?` 占位 + 数组绑定,与 node:sqlite 完全兼容。
66
+ // 说明:StatementSync 由 Node GC 自动回收,无需(在 v22 亦无)finalize 方法。
67
+ _query(connection, obj) {
68
+ if (!obj.sql) throw new Error("The query is empty");
69
+
70
+ const bindings = normalizeBindings(obj.bindings);
71
+ const useRun = ["insert", "update"].includes(obj.method)
72
+ ? !obj.returning
73
+ : ["del", "counter"].includes(obj.method);
74
+
75
+ const stmt = connection.prepare(obj.sql);
76
+ try {
77
+ if (useRun) {
78
+ const info = stmt.run(...bindings);
79
+ obj.context = {
80
+ lastID: Number(info.lastInsertRowid ?? 0),
81
+ changes: info.changes ?? 0,
82
+ };
83
+ } else {
84
+ obj.response = stmt.all(...bindings);
85
+ }
86
+ } catch (err) {
87
+ err.message = `[sqlite-node] 执行失败: ${err.message} | SQL: ${obj.sql}`;
88
+ throw err;
89
+ }
90
+
91
+ return Promise.resolve(obj);
92
+ }
93
+
94
+ validateConnection(connection) {
95
+ return !!connection;
96
+ }
97
+ }
98
+
99
+ Object.assign(SqliteAdapter.prototype, {
100
+ dialect: "sqlite3",
101
+ driverName: "sqlite3-node",
102
+ });
103
+
104
+ export default SqliteAdapter;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "chanjs",
4
- "version": "2.7.15",
4
+ "version": "2.7.17",
5
5
  "description": "chanjs基于 Node.js + Express 5 的标准 HMVC 框架(NHMVC),纯 JavaScript(ESM)开发。",
6
6
  "main": "index.js",
7
7
  "module": "index.js",