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.
- package/USAGE.md +533 -0
- package/config/index.js +37 -6
- package/core/App.js +166 -0
- package/core/BaseComponent.js +27 -0
- package/core/Container.js +68 -0
- package/core/Controller.js +29 -0
- package/core/Database.js +93 -0
- package/core/Repository.js +323 -0
- package/core/Service.js +11 -0
- package/core/bootstrap/error-handler.js +101 -0
- package/core/bootstrap/hook-runner.js +64 -0
- package/core/bootstrap/middleware.js +35 -0
- package/core/bootstrap/router-loader.js +53 -0
- package/core/errors.js +251 -0
- package/core/loader.js +89 -0
- package/core/registry.js +17 -0
- package/doc/Cache.md +279 -106
- package/doc/Common.md +590 -134
- package/doc/Controller.md +166 -95
- package/doc/Help.md +299 -698
- package/doc/QuickStart.md +116 -0
- package/doc/Repository.md +560 -0
- package/doc/Service.md +201 -527
- package/index.js +61 -37
- package/middleware/body.js +17 -0
- package/middleware/cookie.js +7 -15
- package/middleware/cors.js +9 -27
- package/middleware/favicon.js +15 -17
- package/middleware/header.js +15 -16
- package/middleware/index.js +11 -11
- package/middleware/log.js +26 -56
- package/middleware/static.js +15 -28
- package/middleware/template.js +75 -115
- package/middleware/validate.js +79 -0
- package/middleware/waf.js +176 -197
- package/package.json +9 -2
- package/response/code.js +73 -0
- package/response/index.js +9 -6
- package/response/response.js +82 -236
- package/security/checker.js +26 -74
- package/security/index.js +4 -9
- package/security/jwt.js +84 -139
- package/security/keywords.js +33 -137
- package/security/rate-limit.js +38 -80
- package/security/sign.js +83 -176
- package/security/xss-filter.js +21 -53
- package/storage/cache.js +58 -198
- package/storage/index.js +3 -6
- package/storage/redis.js +124 -181
- package/storage/store.js +163 -188
- package/utils/data-parse.js +42 -186
- package/utils/file.js +73 -244
- package/utils/filter.js +22 -25
- package/utils/html.js +49 -33
- package/utils/index.js +20 -7
- package/utils/ip.js +31 -71
- package/utils/logger.js +117 -0
- package/utils/pages.js +55 -0
- package/utils/paths.js +18 -0
- package/utils/request.js +95 -136
- package/utils/signal.js +87 -0
- package/utils/time.js +33 -75
- package/utils/tree.js +112 -104
- package/App.js +0 -533
- package/base/Aop.js +0 -195
- package/base/Container.js +0 -161
- package/base/Controller.js +0 -65
- package/base/Database.js +0 -133
- package/base/Event.js +0 -61
- package/base/Repository.js +0 -644
- package/common/api.js +0 -35
- package/common/code.js +0 -52
- package/common/email.js +0 -191
- package/common/index.js +0 -5
- package/common/pages.js +0 -120
- package/common/utils.js +0 -73
- package/config/code.js +0 -166
- package/config/paths.js +0 -60
- package/doc/Aop.md +0 -269
- package/doc/Email.md +0 -114
- package/doc/Event.md +0 -232
- package/global/env.js +0 -11
- package/global/import.js +0 -39
- package/global/index.js +0 -8
- package/helper/index.js +0 -79
- package/loader/index.js +0 -6
- package/loader/loader.js +0 -138
- package/middleware/compress.js +0 -185
- package/middleware/setBody.js +0 -32
- package/realtime/index.js +0 -7
- package/realtime/sse.js +0 -424
- package/realtime/websocket.js +0 -540
- package/schedule/index.js +0 -6
- 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
|
+
}
|
package/core/Database.js
ADDED
|
@@ -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
|
+
|