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/errors.js ADDED
@@ -0,0 +1,251 @@
1
+ import {
2
+ CODE_AUTH_FAILED, CODE_TOKEN_EXPIRED, CODE_FORBIDDEN, CODE_NOT_FOUND,
3
+ CODE_CONFLICT, CODE_PARAM_INVALID, CODE_PARAM_MISSING, CODE_BUSINESS_FAIL,
4
+ CODE_RATE_LIMIT, CODE_BLOCKED, CODE_SYSTEM_ERROR, CODE_SERVICE_BUSY,
5
+ CODE_DB_CONNECTION_ERROR, CODE_DB_ACCESS_DENIED, CODE_DB_OPERATION_TIMEOUT,
6
+ } from "../response/code.js";
7
+
8
+ /** 全局错误配置中心 */
9
+ const ERROR_CONFIG = {
10
+ AuthError: { code: CODE_AUTH_FAILED, httpStatus: 401, defaultMsg: '认证失败' },
11
+ TokenExpiredError: { code: CODE_TOKEN_EXPIRED, httpStatus: 401, defaultMsg: '令牌已过期' },
12
+ ForbiddenError: { code: CODE_FORBIDDEN, httpStatus: 403, defaultMsg: '权限不足' },
13
+ NotFoundError: { code: CODE_NOT_FOUND, httpStatus: 404, defaultMsg: '资源不存在' },
14
+ ConflictError: { code: CODE_CONFLICT, httpStatus: 409, defaultMsg: '资源已存在' },
15
+ ValidationError: { code: CODE_PARAM_INVALID, httpStatus: 422, defaultMsg: '参数无效', extraProps: ['fields'] },
16
+ ParamMissingError: { code: CODE_PARAM_MISSING, httpStatus: 400, defaultMsg: '参数缺失' },
17
+ BusinessError: { code: CODE_BUSINESS_FAIL, httpStatus: 400, defaultMsg: '业务处理失败' },
18
+ RateLimitError: { code: CODE_RATE_LIMIT, httpStatus: 429, defaultMsg: '请求过于频繁', extraProps: ['retryAfter'] },
19
+ BlockedError: { code: CODE_BLOCKED, httpStatus: 403, defaultMsg: '访问已被限制', extraProps: ['retryAfter'] },
20
+ SystemError: { code: CODE_SYSTEM_ERROR, httpStatus: 500, defaultMsg: '系统内部错误' },
21
+ ServiceBusyError: { code: CODE_SERVICE_BUSY, httpStatus: 503, defaultMsg: '服务繁忙' },
22
+ DbConnectionError: { code: CODE_DB_CONNECTION_ERROR, httpStatus: 503, defaultMsg: '数据库连接失败' },
23
+ DbAccessDeniedError:{ code: CODE_DB_ACCESS_DENIED, httpStatus: 503, defaultMsg: '数据库访问被拒绝' },
24
+ DbTimeoutError: { code: CODE_DB_OPERATION_TIMEOUT, httpStatus: 503, defaultMsg: '数据库操作超时' },
25
+ };
26
+
27
+ const MAX_RECURSIVE_DEPTH = 10;
28
+
29
+ /**
30
+ * 业务错误基类
31
+ */
32
+ export class AppError extends Error {
33
+ /**
34
+ * @param {number} code 业务码
35
+ * @param {string} msg 提示文案
36
+ * @param {number} httpStatus HTTP状态码
37
+ * @param {Error|unknown} [cause] 底层原始错误
38
+ */
39
+ constructor(code, msg, httpStatus = 400, cause = null) {
40
+ super(msg, { cause });
41
+ this.name = 'AppError';
42
+ this.code = code;
43
+ this.httpStatus = httpStatus;
44
+ this.meta = {};
45
+ if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
46
+ }
47
+
48
+ /** 统一判断是否为业务错误实例(推荐使用独立导出函数 isAppError) */
49
+ static isAppError(err) {
50
+ return err instanceof AppError;
51
+ }
52
+
53
+ /** 自定义序列化 */
54
+ toJSON() {
55
+ const json = { name: this.name, message: this.message, code: this.code, httpStatus: this.httpStatus };
56
+ for (const key of Object.getOwnPropertyNames(this)) {
57
+ const val = this[key];
58
+ if (val === undefined || val === null) continue;
59
+ if (typeof val === 'function' || typeof val === 'symbol') continue;
60
+ json[key] = val;
61
+ }
62
+ return json;
63
+ }
64
+ }
65
+
66
+ /** 动态生成业务错误 Class */
67
+ function createErrorClass(className) {
68
+ const cfg = ERROR_CONFIG[className];
69
+ if (!cfg) throw new Error(`错误类型[${className}]未在ERROR_CONFIG中配置`);
70
+
71
+ return class extends AppError {
72
+ constructor(msgOrOpts, ...rest) {
73
+ // 支持两种调用方式:
74
+ // new XxxError("消息")
75
+ // new XxxError({ msg: "消息", cause: err, ...extraProps })
76
+ let opts;
77
+ if (msgOrOpts && typeof msgOrOpts === 'object') {
78
+ opts = msgOrOpts;
79
+ } else {
80
+ opts = { msg: msgOrOpts };
81
+ // 将 extraProps 按顺序映射到 rest 参数
82
+ if (cfg.extraProps) {
83
+ cfg.extraProps.forEach((key, i) => {
84
+ if (rest[i] !== undefined) opts[key] = rest[i];
85
+ });
86
+ // extraProps 之后的参数视为 cause
87
+ const causeIdx = cfg.extraProps.length;
88
+ if (rest[causeIdx] !== undefined) opts.cause = rest[causeIdx];
89
+ } else if (rest[0] !== undefined) {
90
+ opts.cause = rest[0];
91
+ }
92
+ }
93
+
94
+ super(cfg.code, opts.msg ?? cfg.defaultMsg, cfg.httpStatus, opts.cause ?? null);
95
+ this.name = className;
96
+ // 挂载额外属性(如 fields / retryAfter)
97
+ for (const [k, v] of Object.entries(opts)) {
98
+ if (k !== 'msg' && k !== 'cause' && v !== undefined) this[k] = v;
99
+ }
100
+ }
101
+ };
102
+ }
103
+
104
+ // 业务错误类导出
105
+ export const AuthError = createErrorClass('AuthError');
106
+ export const TokenExpiredError = createErrorClass('TokenExpiredError');
107
+ export const ForbiddenError = createErrorClass('ForbiddenError');
108
+ export const NotFoundError = createErrorClass('NotFoundError');
109
+ export const ConflictError = createErrorClass('ConflictError');
110
+ export const ValidationError = createErrorClass('ValidationError');
111
+ export const ParamMissingError = createErrorClass('ParamMissingError');
112
+ export const BusinessError = createErrorClass('BusinessError');
113
+ export const RateLimitError = createErrorClass('RateLimitError');
114
+ export const BlockedError = createErrorClass('BlockedError');
115
+ export const SystemError = createErrorClass('SystemError');
116
+ export const ServiceBusyError = createErrorClass('ServiceBusyError');
117
+ export const DbConnectionError = createErrorClass('DbConnectionError');
118
+ export const DbAccessDeniedError= createErrorClass('DbAccessDeniedError');
119
+ export const DbTimeoutError = createErrorClass('DbTimeoutError');
120
+
121
+ /**
122
+ * 提取错误自定义附加属性,用于日志打印
123
+ */
124
+ export function errorExtraProps(err) {
125
+ if (!err || !(err instanceof Error)) return "";
126
+ const parts = [];
127
+ for (const k of Object.getOwnPropertyNames(err)) {
128
+ if (k === "message" || k === "stack" || k === "name" || k === "cause" || k === "constructor") continue;
129
+ const val = err[k];
130
+ if (typeof val === 'function' || typeof val === 'symbol') continue;
131
+ try {
132
+ parts.push(`${k}=${typeof val === "string" ? val : JSON.stringify(val)}`);
133
+ } catch {
134
+ parts.push(`${k}=[unserializable]`);
135
+ }
136
+ }
137
+ return parts.length ? ` | ${parts.join("; ")}` : "";
138
+ }
139
+
140
+ /**
141
+ * 递归解析完整可读错误信息,内置递归深度限制防栈溢出
142
+ */
143
+ export function describeError(err, depth = 0) {
144
+ if (!err) return "未知错误";
145
+ if (depth >= MAX_RECURSIVE_DEPTH) return `[递归深度超限(${MAX_RECURSIVE_DEPTH}),停止解析]`;
146
+
147
+ // 聚合错误批量展开子错误
148
+ if (err instanceof AggregateError && Array.isArray(err.errors) && err.errors.length) {
149
+ const reasons = err.errors
150
+ .map((e, i) => ` [${i + 1}] ${describeError(e, depth + 1)}`)
151
+ .join("\n");
152
+ return `聚合错误(AggregateError)共${err.errors.length}个原因:\n${reasons}`;
153
+ }
154
+
155
+ const baseMsg = err.message || String(err);
156
+ let hint = baseMsg;
157
+
158
+ // 数据库/网络错误智能提示
159
+ if (/ECONNREFUSED/.test(baseMsg)) {
160
+ hint = `数据库连接被拒绝(ECONNREFUSED) — 检查数据库服务、地址端口配置: ${baseMsg}`;
161
+ } else if (/ENOTFOUND|EAI_AGAIN/.test(baseMsg)) {
162
+ hint = `数据库域名解析失败 — 检查DB_HOST环境变量: ${baseMsg}`;
163
+ } else if (/ECONNRESET|PROTOCOL_CONNECTION_LOST|ETIMEDOUT/.test(baseMsg)) {
164
+ hint = `数据库连接中断/超时 — 检查服务状态、连接池上限: ${baseMsg}`;
165
+ }
166
+
167
+ const causeText = err.cause
168
+ ? err.cause instanceof Error
169
+ ? ` | cause: ${describeError(err.cause, depth + 1)}`
170
+ : ` | cause: ${String(err.cause)}`
171
+ : "";
172
+
173
+ return `${hint}${causeText}${errorExtraProps(err)}`;
174
+ }
175
+
176
+ /** 全局判断业务错误 */
177
+ export function isAppError(err) {
178
+ return err instanceof AppError;
179
+ }
180
+
181
+ /**
182
+ * 判断是否为数据库连接类错误(code + message 双重检测)
183
+ */
184
+ const CONNECTION_CODES = new Set([
185
+ "ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN", "ETIMEDOUT",
186
+ "ECONNRESET", "PROTOCOL_CONNECTION_LOST", "PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR",
187
+ ]);
188
+ const CONNECTION_KEYWORDS = [
189
+ "econnrefused", "enotfound", "eai_again", "etimedout", "econnreset",
190
+ "connection lost", "connection closed", "pool is full", "too many connections",
191
+ "closed unexpectedly", "connect timeout", "handshake timeout",
192
+ ];
193
+
194
+ function isDbConnectionError(err) {
195
+ if (CONNECTION_CODES.has(err?.code)) return true;
196
+ const msg = String(err?.message || "").toLowerCase();
197
+ return CONNECTION_KEYWORDS.some(kw => msg.includes(kw));
198
+ }
199
+
200
+ /**
201
+ * DB / 底层异常统一拦截器:转换为框架标准 AppError 子类
202
+ * @param {Error} err
203
+ * @returns {AppError}
204
+ */
205
+ export function wrapDbError(err) {
206
+ if (isAppError(err)) return err;
207
+
208
+ // 聚合错误(mysql2 批量操作)提取首个真实原因
209
+ let e = err;
210
+ if (err instanceof AggregateError && Array.isArray(err.errors) && err.errors.length) {
211
+ e = err.errors.find(x => x instanceof Error) ?? err.errors[0];
212
+ }
213
+
214
+ if (isDbConnectionError(e)) return new DbConnectionError("数据库连接失败,请稍后重试", err);
215
+
216
+ // 模板/视图渲染错误:Express 的 res.render 找不到模板时抛出
217
+ const errMsg = String(e?.message || "").toLowerCase();
218
+ if (errMsg.includes("failed to lookup view") || errMsg.includes("no default engine")) {
219
+ return new SystemError("模板渲染异常:" + (e?.message || "视图文件不存在"), err);
220
+ }
221
+
222
+ const driverCode = e?.code;
223
+ switch (driverCode) {
224
+ case "ER_DUP_ENTRY":
225
+ case "23505":
226
+ return new ConflictError("数据已存在", err);
227
+ case "ER_ACCESS_DENIED_ERROR":
228
+ return new DbAccessDeniedError("数据库访问被拒绝", err);
229
+ case "ER_NO_SUCH_TABLE":
230
+ return new SystemError("数据表不存在", err);
231
+ case "ER_BAD_FIELD_ERROR":
232
+ return new ParamMissingError("数据库字段错误", err);
233
+ case "ER_ROW_IS_REFERENCED_2":
234
+ return new ConflictError("存在关联数据,操作失败", err);
235
+ default:
236
+ return new SystemError("数据操作异常", err);
237
+ }
238
+ }
239
+
240
+ /** 解析 V8 堆栈,提取报错文件、行号 */
241
+ export function parseStack(stack) {
242
+ if (!stack || typeof stack !== "string") {
243
+ return { message: "无堆栈信息", file: "-", line: "-" };
244
+ }
245
+ const lines = stack.split("\n").filter(Boolean);
246
+ const message = lines[0];
247
+ const traceLine = lines[1] ?? "";
248
+ const match = traceLine.match(/\(([^:]+):(\d+):\d+\)/) || traceLine.match(/at\s+([^:]+):(\d+):\d+/);
249
+ if (match) return { message, file: match[1], line: match[2] };
250
+ return { message, file: traceLine.trim(), line: "-" };
251
+ }
package/core/loader.js ADDED
@@ -0,0 +1,89 @@
1
+ import fs from "fs";
2
+ import fsp from "fs/promises";
3
+ import path from "path";
4
+ import { pathToFileURL } from "url";
5
+ import { Paths } from "../utils/paths.js";
6
+ import logger from "../utils/logger.js";
7
+
8
+ const BOUND_SYMBOL = Symbol("chanjs:bound");
9
+ const JS_SUFFIX = /\.js$/i;
10
+
11
+ /**
12
+ * 动态导入文件,捕获常见IO异常
13
+ * @param {string} filepath
14
+ * @returns {Promise<any|null>}
15
+ */
16
+ export async function importFile(filepath) {
17
+ if (!filepath || typeof filepath !== "string") {
18
+ logger.error("[Loader] 文件路径非法");
19
+ return null;
20
+ }
21
+ try {
22
+ await fsp.access(filepath);
23
+ const mod = await import(pathToFileURL(filepath).href);
24
+ return mod.default ?? mod;
25
+ } catch (err) {
26
+ if (err.code === "ENOENT") logger.error(`[Loader] 文件不存在: ${filepath}`);
27
+ else if (err.code === "EACCES") logger.error(`[Loader] 无访问权限: ${filepath}`);
28
+ else logger.error(`[Loader] 导入失败 ${filepath}:`, err.message);
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * 模块排序:web模块强制后置
35
+ * @param {string[]} modules
36
+ * @returns {string[]}
37
+ */
38
+ export function loaderSort(modules = []) {
39
+ const webIdx = modules.indexOf("web");
40
+ if (webIdx > -1) modules.push(modules.splice(webIdx, 1)[0]);
41
+ return modules;
42
+ }
43
+
44
+ /**
45
+ * 加载根配置 index.js
46
+ */
47
+ export async function loadConfig() {
48
+ const cfg = await importFile(path.join(Paths.configPath, "index.js"));
49
+ return cfg ?? {};
50
+ }
51
+
52
+ /**
53
+ * 加载指定模块全部Controller,自动绑定实例方法防this丢失
54
+ * @param {string} moduleName
55
+ * @returns {Record<string, any>}
56
+ */
57
+ export async function loadController(moduleName) {
58
+ const ctrlDir = path.join(Paths.modulesPath, moduleName, "controller");
59
+ const ctrlMap = {};
60
+
61
+ if (!fs.existsSync(ctrlDir)) {
62
+ logger.error(`[Loader] 控制器目录不存在: ${ctrlDir}`);
63
+ return ctrlMap;
64
+ }
65
+
66
+ const jsFiles = fs.readdirSync(ctrlDir).filter(f => JS_SUFFIX.test(f));
67
+ for (const file of jsFiles) {
68
+ const filePath = path.join(ctrlDir, file);
69
+ const ctrlName = file.replace(JS_SUFFIX, "");
70
+ const inst = await importFile(filePath);
71
+ if (!inst || typeof inst !== "object") continue;
72
+
73
+ // 原型方法批量bind,幂等标记避免重复绑定
74
+ const proto = Object.getPrototypeOf(inst);
75
+ if (!proto) continue;
76
+ Object.getOwnPropertyNames(proto).forEach(key => {
77
+ const fn = inst[key];
78
+ if (key === "constructor" || typeof fn !== "function" || fn[BOUND_SYMBOL]) return;
79
+ const boundFn = fn.bind(inst);
80
+ Object.defineProperty(boundFn, BOUND_SYMBOL, {
81
+ value: true, enumerable: false, writable: false, configurable: false
82
+ });
83
+ inst[key] = boundFn;
84
+ });
85
+
86
+ ctrlMap[ctrlName] = inst;
87
+ }
88
+ return ctrlMap;
89
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 全局单应用实例注册表
3
+ * 全局仅维护单个Chan实例,多实例采用多进程方案
4
+ */
5
+ let _current = null;
6
+
7
+ /** 设置全局应用实例 */
8
+ export function setApp(app) {
9
+ _current = app;
10
+ }
11
+
12
+ /** 获取全局应用实例,未初始化返回null */
13
+ export function getApp() {
14
+ return _current;
15
+ }
16
+
17
+ export default { setApp, getApp };