chanjs 2.7.4 → 2.7.6

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.
Files changed (94) hide show
  1. package/USAGE.md +533 -0
  2. package/config/index.js +37 -6
  3. package/core/App.js +166 -0
  4. package/core/BaseComponent.js +27 -0
  5. package/core/Container.js +68 -0
  6. package/core/Controller.js +29 -0
  7. package/core/Database.js +93 -0
  8. package/core/Repository.js +323 -0
  9. package/core/Service.js +11 -0
  10. package/core/bootstrap/error-handler.js +101 -0
  11. package/core/bootstrap/hook-runner.js +64 -0
  12. package/core/bootstrap/middleware.js +35 -0
  13. package/core/bootstrap/router-loader.js +53 -0
  14. package/core/errors.js +251 -0
  15. package/core/loader.js +89 -0
  16. package/core/registry.js +17 -0
  17. package/doc/Cache.md +279 -106
  18. package/doc/Common.md +590 -134
  19. package/doc/Controller.md +166 -95
  20. package/doc/Help.md +299 -698
  21. package/doc/QuickStart.md +116 -0
  22. package/doc/Repository.md +560 -0
  23. package/doc/Service.md +201 -527
  24. package/index.js +61 -37
  25. package/middleware/body.js +17 -0
  26. package/middleware/cookie.js +7 -15
  27. package/middleware/cors.js +9 -27
  28. package/middleware/favicon.js +15 -17
  29. package/middleware/header.js +15 -16
  30. package/middleware/index.js +11 -11
  31. package/middleware/log.js +26 -56
  32. package/middleware/static.js +15 -28
  33. package/middleware/template.js +75 -115
  34. package/middleware/validate.js +79 -0
  35. package/middleware/waf.js +176 -197
  36. package/package.json +9 -2
  37. package/response/code.js +73 -0
  38. package/response/index.js +9 -6
  39. package/response/response.js +82 -236
  40. package/security/checker.js +26 -74
  41. package/security/index.js +4 -9
  42. package/security/jwt.js +84 -139
  43. package/security/keywords.js +33 -137
  44. package/security/rate-limit.js +38 -80
  45. package/security/sign.js +83 -176
  46. package/security/xss-filter.js +21 -53
  47. package/storage/cache.js +58 -198
  48. package/storage/index.js +3 -6
  49. package/storage/redis.js +124 -181
  50. package/storage/store.js +163 -188
  51. package/utils/data-parse.js +42 -186
  52. package/utils/file.js +73 -244
  53. package/utils/filter.js +22 -25
  54. package/utils/html.js +49 -33
  55. package/utils/index.js +20 -7
  56. package/utils/ip.js +31 -71
  57. package/utils/logger.js +117 -0
  58. package/utils/pages.js +55 -0
  59. package/utils/paths.js +18 -0
  60. package/utils/request.js +95 -136
  61. package/utils/signal.js +87 -0
  62. package/utils/time.js +33 -75
  63. package/utils/tree.js +112 -104
  64. package/App.js +0 -533
  65. package/base/Aop.js +0 -195
  66. package/base/Container.js +0 -161
  67. package/base/Controller.js +0 -65
  68. package/base/Database.js +0 -133
  69. package/base/Event.js +0 -61
  70. package/base/Repository.js +0 -644
  71. package/common/api.js +0 -35
  72. package/common/code.js +0 -52
  73. package/common/email.js +0 -191
  74. package/common/index.js +0 -5
  75. package/common/pages.js +0 -120
  76. package/common/utils.js +0 -73
  77. package/config/code.js +0 -166
  78. package/config/paths.js +0 -60
  79. package/doc/Aop.md +0 -269
  80. package/doc/Email.md +0 -114
  81. package/doc/Event.md +0 -232
  82. package/global/env.js +0 -11
  83. package/global/import.js +0 -39
  84. package/global/index.js +0 -8
  85. package/helper/index.js +0 -79
  86. package/loader/index.js +0 -6
  87. package/loader/loader.js +0 -138
  88. package/middleware/compress.js +0 -185
  89. package/middleware/setBody.js +0 -32
  90. package/realtime/index.js +0 -7
  91. package/realtime/sse.js +0 -424
  92. package/realtime/websocket.js +0 -540
  93. package/schedule/index.js +0 -6
  94. package/schedule/schedule.js +0 -491
package/core/App.js ADDED
@@ -0,0 +1,166 @@
1
+ import express from "express";
2
+ import DatabaseManager from "./Database.js";
3
+ import { setApp } from "./registry.js";
4
+ import { Paths } from "../utils/paths.js";
5
+ import { loadConfig } from "./loader.js";
6
+ import { store } from "../storage/index.js";
7
+ import logger from "../utils/logger.js";
8
+ import { AppError, describeError, errorExtraProps } from "./errors.js";
9
+ import { register as registerShutdown, shutdown as runShutdown } from "../utils/signal.js";
10
+ import { registerCoreMiddleware } from "./bootstrap/middleware.js";
11
+ import { loadModuleRouter, loadCommonRouter } from "./bootstrap/router-loader.js";
12
+ import { registerErrorHandler } from "./bootstrap/error-handler.js";
13
+ import { runHooks } from "./bootstrap/hook-runner.js";
14
+ import { loadDotEnv } from "../config/index.js";
15
+
16
+ // 初始化环境变量
17
+ loadDotEnv();
18
+
19
+ /**
20
+ * Chan 应用核心类
21
+ */
22
+ export default class Chan {
23
+ constructor() {
24
+ this.app = express();
25
+ this.router = express.Router();
26
+ this.dbManager = new DatabaseManager();
27
+
28
+ this.config = null;
29
+ this.paths = Paths;
30
+ this.db = null;
31
+ this.hooks = [];
32
+ this.server = null;
33
+ }
34
+
35
+ /** 完整启动流程 */
36
+ async start() {
37
+ await this.#loadConfig();
38
+ logger.info("[Chan] 配置加载完成");
39
+
40
+ await this.#initStore();
41
+ logger.info("[Chan] 缓存初始化完成");
42
+
43
+ await this.#loadDb();
44
+ logger.info("[Chan] 数据库加载完成");
45
+
46
+ setApp(this);
47
+ logger.info("[Chan] 全局应用实例注册完成");
48
+
49
+ await this.#regCoreMiddleware();
50
+ logger.info("[Chan] 核心中间件注册完成");
51
+
52
+ this.#setupApp();
53
+ await this.#loadRoutes();
54
+ this.#mountRouter();
55
+ logger.info("[Chan] 路由加载挂载完成");
56
+
57
+ this.#regErrHandler();
58
+ logger.info("[Chan] 全局错误中间件注册完成");
59
+
60
+ await this.#runHooks();
61
+ logger.info("[Chan] 启动钩子全部执行完毕");
62
+ }
63
+
64
+ /** 注册启动前置钩子 */
65
+ beforeStart(fn) {
66
+ if (typeof fn !== "function") {
67
+ throw new AppError("PARAM_INVALID", "beforeStart 参数必须为函数", 400);
68
+ }
69
+ this.hooks.push(fn);
70
+ }
71
+
72
+ /** 执行优雅停机流程 */
73
+ async shutdown() {
74
+ await runShutdown(this);
75
+ }
76
+
77
+ /**
78
+ * 启动 HTTP 监听服务
79
+ * @param {(port: number) => void} cb 启动成功回调
80
+ */
81
+ run(cb) {
82
+ const port = Number(this.config.PORT) || 3000;
83
+ this.server = this.app.listen(port, () => {
84
+ logger.info(`[Chan] HTTP服务启动,监听端口:${port}`);
85
+ cb?.(port);
86
+ });
87
+ this.server.on("error", (err) => {
88
+ logger.error(`[Chan] HTTP服务启动失败: ${err.message}`, err);
89
+ });
90
+ registerShutdown(this);
91
+ }
92
+
93
+ async #loadConfig() {
94
+ this.config = await loadConfig();
95
+ }
96
+
97
+ async #initStore() {
98
+ const cfg = this.config;
99
+ try {
100
+ await store.init({
101
+ REDIS_ENABLED: !!cfg?.REDIS_ENABLED,
102
+ REDIS: cfg?.REDIS ?? {}
103
+ });
104
+ if (!cfg?.REDIS_ENABLED) logger.info("[Store] Redis未开启,使用内存缓存");
105
+ } catch (err) {
106
+ logger.warn("[Store] Redis连接失败,自动降级内存缓存");
107
+ logger.error(`[Store] 异常信息:${describeError(err)}${errorExtraProps(err)}`, err);
108
+ }
109
+ }
110
+
111
+ async #loadDb() {
112
+ const dbList = this.config?.db ?? [];
113
+ if (!Array.isArray(dbList) || dbList.length === 0) return;
114
+
115
+ for (const [idx, dbCfg] of dbList.entries()) {
116
+ const dbKey = dbCfg.key ?? String(idx);
117
+ let conn;
118
+ try {
119
+ conn = this.dbManager.add(dbKey, dbCfg, { isDefault: idx === 0 });
120
+ // 首个成功连接即作为默认 this.db,避免 idx===0 失败时 this.db 恒为 null
121
+ if (!this.db) this.db = conn;
122
+ } catch (err) {
123
+ logger.error(`[DB] 数据库[${dbKey}]初始化失败:${describeError(err)}${errorExtraProps(err)}`, err);
124
+ continue;
125
+ }
126
+
127
+ const ok = await this.dbManager.ping(dbKey);
128
+ ok
129
+ ? logger.info(`[DB] 数据库[${dbKey}]连接正常`)
130
+ : logger.error(`[DB] ⚠️ 数据库[${dbKey}]连通性校验失败,请检查库服务、账号配置`);
131
+ }
132
+ logger.info(`[DB] 数据库初始化结束,共配置${dbList.length}个连接`);
133
+ }
134
+
135
+ async #regCoreMiddleware() {
136
+ await registerCoreMiddleware(this);
137
+ }
138
+
139
+ /**
140
+ * 配置 Express trust proxy
141
+ * 默认只信任本机回环;生产环境通过 TRUSTED_PROXIES 注入真实上游代理
142
+ * 切勿使用 true(信任任意来源)
143
+ */
144
+ #setupApp() {
145
+ const env = process.env.TRUSTED_PROXIES;
146
+ const trustedProxies = env ? env.split(",").map(s => s.trim()).filter(Boolean) : ["loopback"];
147
+ this.app.set("trust proxy", trustedProxies);
148
+ }
149
+
150
+ async #loadRoutes() {
151
+ await loadModuleRouter(this);
152
+ await loadCommonRouter(this);
153
+ }
154
+
155
+ #mountRouter() {
156
+ this.app.use(this.router);
157
+ }
158
+
159
+ #regErrHandler() {
160
+ registerErrorHandler(this);
161
+ }
162
+
163
+ async #runHooks() {
164
+ await runHooks(this);
165
+ }
166
+ }
@@ -0,0 +1,27 @@
1
+ import { getApp } from "./registry.js";
2
+
3
+ /**
4
+ * 轻量基础组件类:提供 app / config / db 快捷访问
5
+ * Controller / Service / Repository 统一继承此类,
6
+ * 不再强制绑定组件加载(Container)职责。
7
+ */
8
+ export class BaseComponent {
9
+ /** 获取全局应用实例 */
10
+ get app() { return getApp(); }
11
+
12
+ /**
13
+ * 全局配置快捷访问
14
+ * 业务 Controller/Service/Repository 大量使用 `this.config.xxx` 读取配置项
15
+ * 统一从 app.config 取,未初始化时返回空对象避免 undefined.xxx 报错
16
+ */
17
+ get config() {
18
+ return this.app?.config ?? {};
19
+ }
20
+
21
+ /** 数据库连接快捷访问(默认连接) */
22
+ get db() {
23
+ return this.app?.db ?? null;
24
+ }
25
+ }
26
+
27
+ export default BaseComponent;
@@ -0,0 +1,68 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { Paths } from '../utils/paths.js';
4
+ import { BaseComponent } from './BaseComponent.js';
5
+ import logger from "../utils/logger.js";
6
+
7
+ const NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
8
+
9
+ /**
10
+ * 组件容器:按需动态加载 controller/service 组件(组合模式)
11
+ * 继承 BaseComponent 获得 app/config/db 访问,额外提供组件加载能力
12
+ */
13
+ export class Container extends BaseComponent {
14
+ constructor(type = 'service') {
15
+ super();
16
+ this._componentCache = new Map();
17
+ this._type = type;
18
+ this._baseDir = Paths.modulesPath;
19
+ }
20
+
21
+ /**
22
+ * 按需获取组件实例,自动缓存
23
+ * @param {string} moduleName 模块名
24
+ * @param {string} fileName 组件文件名(不含 .js)
25
+ */
26
+ async get(moduleName, fileName) {
27
+ const cacheKey = `${moduleName}.${fileName}`;
28
+ if (this._componentCache.has(cacheKey)) return this._componentCache.get(cacheKey);
29
+
30
+ try {
31
+ const instance = await this._loadFile(moduleName, fileName);
32
+ if (!instance) logger.info(`[Container] ${moduleName}/${this._type}/${fileName}.js 导出实例为空`);
33
+ return instance;
34
+ } catch (err) {
35
+ logger.error(`[Container] 加载组件失败 ${moduleName}/${fileName}:${err.message}`);
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /** 物理加载文件、导入模块、安全校验 */
41
+ async _loadFile(moduleName, fileName) {
42
+ const cacheKey = `${moduleName}.${fileName}`;
43
+
44
+ if (!NAME_REGEX.test(moduleName) || !NAME_REGEX.test(fileName)) {
45
+ throw new Error(`非法名称 ${moduleName}/${fileName}`);
46
+ }
47
+
48
+ const filePath = path.resolve(this._baseDir, moduleName, this._type, `${fileName}.js`);
49
+ const relativePath = path.relative(this._baseDir, filePath);
50
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
51
+ throw new Error(`路径越界拦截:${filePath}`);
52
+ }
53
+
54
+ try {
55
+ await fs.promises.access(filePath, fs.constants.R_OK);
56
+ } catch {
57
+ logger.info(`[Container] 文件不存在:${moduleName}/${this._type}/${fileName}.js`);
58
+ return null;
59
+ }
60
+
61
+ const mod = await import(`file://${filePath}`);
62
+ const instance = mod.default;
63
+ this._componentCache.set(cacheKey, instance);
64
+ return instance;
65
+ }
66
+ }
67
+
68
+ export default Container;
@@ -0,0 +1,29 @@
1
+ import { success, fail } from "../response/response.js";
2
+ import Container from "./Container.js";
3
+
4
+ /**
5
+ * 控制器基类,继承组件容器,内置统一成功/失败响应封装
6
+ * 响应结构 { success, code, msg, data }
7
+ * 错误码规范:0成功 / 1xxx业务 / 5xxx系统 / 6xxx数据库
8
+ */
9
+ export default class Controller extends Container {
10
+ constructor() {
11
+ super("controller");
12
+ }
13
+
14
+ /**
15
+ * 成功返回,默认code=0
16
+ * @param {{data?: any, msg?: string}} opts
17
+ */
18
+ success({ data, msg = "操作成功" } = {}) {
19
+ return success({ data: data ?? {}, msg });
20
+ }
21
+
22
+ /**
23
+ * 失败返回,传字符串直接作为提示文案,默认code=1008
24
+ * @param {string | {msg?: string, code?: number}} opts
25
+ */
26
+ fail(opts = {}) {
27
+ return typeof opts === "string" ? fail({ msg: opts }) : fail(opts);
28
+ }
29
+ }
@@ -0,0 +1,93 @@
1
+ import knex from "knex";
2
+ import logger from "../utils/logger.js";
3
+
4
+ const DEFAULT_NAME = "default";
5
+
6
+ /**
7
+ * 多Knex连接管理器,统一管理连接、心跳、批量销毁
8
+ */
9
+ class DatabaseManager {
10
+ constructor() {
11
+ this._connections = new Map();
12
+ this._defaultName = DEFAULT_NAME;
13
+ // 健康缓存:运行时由 error-handler 实时更新,用于鉴权层廉价区分「用户未登录」与「库挂了」
14
+ this._health = new Map();
15
+ }
16
+
17
+ /**
18
+ * 添加数据库连接,首个连接 / isDefault=true 自动设为默认
19
+ * @param {string} name 连接标识
20
+ * @param {object} config Knex配置
21
+ * @param {{isDefault?: boolean}} opts
22
+ * @returns {knex.Knex}
23
+ */
24
+ add(name, config, { isDefault = false } = {}) {
25
+ const conn = knex(config);
26
+ this._connections.set(name, conn);
27
+ if (isDefault || this._connections.size === 1) this._defaultName = name;
28
+ return conn;
29
+ }
30
+
31
+ /**
32
+ * 获取指定连接,不存在抛异常
33
+ * @param {string} [name=this._defaultName]
34
+ * @returns {knex.Knex}
35
+ */
36
+ get(name = this._defaultName) {
37
+ const conn = this._connections.get(name);
38
+ if (!conn) throw new Error(`Database connection "${name}" not found`);
39
+ return conn;
40
+ }
41
+
42
+ /** 批量关闭全部连接,单个失败不阻断其他 */
43
+ async closeAll() {
44
+ const results = [];
45
+ for (const [name, conn] of this._connections) {
46
+ try {
47
+ await conn.destroy();
48
+ results.push({ name, success: true });
49
+ } catch (err) {
50
+ results.push({ name, success: false, error: err.message });
51
+ logger.error(`[Database] 关闭连接 ${name} 失败`, err);
52
+ }
53
+ }
54
+ this._connections.clear();
55
+ logger.info(`[Database] 全部连接关闭完成,总计 ${results.length} 个`);
56
+ return results;
57
+ }
58
+
59
+ /**
60
+ * 心跳探测,SELECT 1 校验连通性(启动时 + 运行时按需调用)
61
+ * @param {string} [name=this._defaultName]
62
+ * @returns {boolean}
63
+ */
64
+ async ping(name = this._defaultName) {
65
+ const conn = this._connections.get(name);
66
+ if (!conn) {
67
+ this._health.set(name, false);
68
+ return false;
69
+ }
70
+ try {
71
+ await conn.raw("SELECT 1");
72
+ this._health.set(name, true);
73
+ return true;
74
+ } catch (err) {
75
+ this._health.set(name, false);
76
+ logger.error(`[Database] ping ${name} 连通校验失败`, err);
77
+ return false;
78
+ }
79
+ }
80
+
81
+ /** 运行时主动标记连接不可用(error-handler 捕获 DB 异常时调用) */
82
+ markDown(name = this._defaultName) {
83
+ this._health.set(name, false);
84
+ }
85
+
86
+ /** 运行时标记连接恢复可用(error-handler 捕获业务成功响应时调用) */
87
+ markUp(name = this._defaultName) {
88
+ this._health.set(name, true);
89
+ }
90
+ }
91
+
92
+ export default DatabaseManager;
93
+