chanjs 2.7.16 → 2.8.0

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
 
@@ -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/core/loader.js CHANGED
@@ -60,17 +60,27 @@ export async function loadController(moduleName) {
60
60
  if (!inst || typeof inst !== "object") continue;
61
61
 
62
62
  // 原型方法批量bind,幂等标记避免重复绑定
63
- const proto = Object.getPrototypeOf(inst);
64
- if (!proto) continue;
65
- Object.getOwnPropertyNames(proto).forEach(key => {
66
- const fn = inst[key];
67
- if (key === "constructor" || typeof fn !== "function" || fn[BOUND_SYMBOL]) return;
68
- const boundFn = fn.bind(inst);
69
- Object.defineProperty(boundFn, BOUND_SYMBOL, {
70
- value: true, enumerable: false, writable: false, configurable: false
63
+ // 遍历整条原型链(含父类继承的方法):子类方法定义在自身原型、父类模板方法定义在父类原型,
64
+ // bind 直接原型会导致继承方法 this 丢失(Express 非点调用时 this=undefined)。
65
+ // ⚠️ 必须用 getOwnPropertyDescriptor 取 value:BaseComponent 的 app/config/db 是 getter-only,
66
+ // 其中 db 返回 knex 实例(typeof 为 function),直接 inst[key] 会触发 getter 且赋值只读属性抛错。
67
+ let proto = Object.getPrototypeOf(inst);
68
+ while (proto && proto !== Object.prototype) {
69
+ Object.getOwnPropertyNames(proto).forEach(key => {
70
+ if (key === "constructor") return;
71
+ const desc = Object.getOwnPropertyDescriptor(proto, key);
72
+ // 只 bind 数据方法(value 为函数);getter/setter 访问器跳过
73
+ if (!desc || typeof desc.value !== "function") return;
74
+ const fn = desc.value;
75
+ if (fn[BOUND_SYMBOL]) return;
76
+ const boundFn = fn.bind(inst);
77
+ Object.defineProperty(boundFn, BOUND_SYMBOL, {
78
+ value: true, enumerable: false, writable: false, configurable: false
79
+ });
80
+ inst[key] = boundFn;
71
81
  });
72
- inst[key] = boundFn;
73
- });
82
+ proto = Object.getPrototypeOf(proto);
83
+ }
74
84
 
75
85
  ctrlMap[ctrlName] = inst;
76
86
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "chanjs",
4
- "version": "2.7.16",
4
+ "version": "2.8.0",
5
5
  "description": "chanjs基于 Node.js + Express 5 的标准 HMVC 框架(NHMVC),纯 JavaScript(ESM)开发。",
6
6
  "main": "index.js",
7
7
  "module": "index.js",