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
@@ -1,172 +1,132 @@
1
- import template from "art-template";
1
+ import artTemplate from "art-template";
2
2
  import dayjs from "dayjs";
3
+ import { createRequire } from "module";
3
4
  import relativeTime from "dayjs/plugin/relativeTime.js";
4
5
  import "dayjs/locale/zh-cn.js";
5
- import { createRequire } from 'module';
6
- import { importjs } from "../global/import.js";
6
+ import { marked } from "marked";
7
7
  import { filterXSS } from "../security/xss-filter.js";
8
+ import logger from "../utils/logger.js";
8
9
 
9
10
  const require = createRequire(import.meta.url);
10
- const { marked } = require('marked');
11
11
 
12
- // ============================================================
13
- // art-template 过滤器注册(原 extend/art-template.js,合并至此)
14
- // ============================================================
12
+ // 初始化日期工具
15
13
  dayjs.extend(relativeTime);
16
- dayjs.locale('zh-cn');
17
-
18
- // 禁用原生模板引擎,防止模板直接调用 nodejs 语法
19
- template.defaults.native = false;
14
+ dayjs.locale("zh-cn");
15
+ // 关闭模板原生NodeJS语法,安全隔离
16
+ artTemplate.defaults.native = false;
20
17
 
21
18
  /**
22
- * 日期格式化过滤器
23
- * @param {Date|string|number} date - 日期对象、日期字符串或时间戳
24
- * @param {string} format - 日期格式字符串
25
- * @returns {string} 格式化后的日期字符串
19
+ * 日期格式化
20
+ * @param {Date|string|number} date
21
+ * @param {string} format
22
+ * @returns {string}
26
23
  */
27
- template.defaults.imports.dateFormat = function (date, format) {
24
+ artTemplate.defaults.imports.dateFormat = (date, format) => {
28
25
  if (!date) return "";
29
- if (date instanceof Date || typeof date === "string" || typeof date === "number") {
30
- date = dayjs(date);
31
- } else {
32
- return "";
33
- }
34
- return date.format(format);
26
+ const d = dayjs(["string", "number", "object"].includes(typeof date) ? date : null);
27
+ return d.isValid() ? d.format(format) : "";
35
28
  };
36
29
 
37
30
  /**
38
- * 相对时间过滤器(如"刚刚"、"5分钟前"、"3小时前")
39
- * @param {Date|string|number} date - 日期对象、日期字符串或时间戳
40
- * @returns {string} 相对时间字符串
31
+ * 相对时间(刚刚/几分钟前)
32
+ * @param {Date|string|number} date
33
+ * @returns {string}
41
34
  */
42
- template.defaults.imports.timeAgo = function (date) {
35
+ artTemplate.defaults.imports.timeAgo = date => {
43
36
  if (!date) return "";
44
37
  const d = dayjs(date);
45
- if (!d.isValid()) return "";
46
- return d.fromNow();
38
+ return d.isValid() ? d.fromNow() : "";
47
39
  };
48
40
 
49
41
  /**
50
- * 字符串截断过滤器
51
- * @param {string} str - 原始字符串
52
- * @param {number} length - 截断长度,默认10
53
- * @returns {string} 截断后的字符串
42
+ * 字符串截断
43
+ * @param {string} str
44
+ * @param {number} [length=10]
45
+ * @returns {string}
54
46
  */
55
- template.defaults.imports.truncate = (str, length = 10) => {
56
- return str.length > length ? str.slice(0, length) + "..." : str;
57
- };
47
+ artTemplate.defaults.imports.truncate = (str, length = 10) =>
48
+ str?.length > length ? `${str.slice(0, length)}...` : str || "";
58
49
 
59
50
  /**
60
- * 安全的 JSON 序列化过滤器(模板调试用)
61
- * @param {Object} obj - 要序列化的对象
62
- * @param {Array} keys - 可选,只返回指定的字段
63
- * @returns {string} JSON字符串
64
- * @description
65
- * 安全改进:用 Object.prototype.hasOwnProperty.call 防止原型污染
51
+ * 安全序列化JSON,防原型污染
52
+ * @param {any} obj
53
+ * @param {string[]} [keys] 指定输出字段
54
+ * @returns {string}
66
55
  */
67
- template.defaults.imports.safeStringify = (obj, keys) => {
68
- if (!obj) return 'null';
69
- if (keys && Array.isArray(keys) && keys.length > 0) {
70
- const filteredObj = {};
71
- keys.forEach(key => {
72
- // 用原型上的方法调用,避免 obj 重写 hasOwnProperty 导致原型污染
73
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
74
- filteredObj[key] = obj[key];
75
- }
56
+ artTemplate.defaults.imports.safeStringify = (obj, keys) => {
57
+ if (!obj) return "null";
58
+ if (Array.isArray(keys) && keys.length) {
59
+ const filtered = {};
60
+ keys.forEach(k => {
61
+ if (Object.prototype.hasOwnProperty.call(obj, k)) filtered[k] = obj[k];
76
62
  });
77
- return JSON.stringify(filteredObj, null, 2);
63
+ return JSON.stringify(filtered, null, 2);
78
64
  }
79
65
  return JSON.stringify(obj, null, 2);
80
66
  };
81
67
 
82
68
  /**
83
- * 获取对象所有 key 的过滤器(调试查看数据结构)
84
- * @param {Object} obj - 要获取 key 的对象
85
- * @param {string} separator - 分隔符,默认换行
86
- * @returns {string} 所有 key 的字符串
69
+ * 获取对象key列表
70
+ * @param {object} obj
71
+ * @param {string} [separator="\n"]
72
+ * @returns {string}
87
73
  */
88
- template.defaults.imports.objKeys = (obj, separator) => {
89
- if (!obj || typeof obj !== 'object') return '';
90
- const sep = separator !== undefined ? separator : '\n';
91
- return Object.keys(obj).join(sep);
92
- };
74
+ artTemplate.defaults.imports.objKeys = (obj, separator = "\n") =>
75
+ typeof obj === "object" && obj ? Object.keys(obj).join(separator) : "";
93
76
 
94
77
  /**
95
- * Markdown 渲染过滤器
96
- * 自动检测内容是否为 Markdown 格式,渲染为 HTML
97
- * @param {string} content - 文章内容
98
- * @param {string} editorType - 编辑器类型,'md' 触发 Markdown 渲染
99
- * @param {number} allowScript - 是否允许 script,1 允许
100
- * @returns {string} 渲染后的 HTML
101
- * @description
102
- * 安全改进:
103
- * - 渲染后通过 filterXSS 过滤危险标签(script、onerror、onload 等)
104
- * - 仅在 allowScript=1 时跳过过滤(受信任内容)
78
+ * Markdown渲染 + XSS过滤
79
+ * @param {string} content
80
+ * @param {"md"|"rich"} [editorType="rich"]
81
+ * @param {0|1} [allowScript=0] 1=信任内容不做XSS过滤
82
+ * @returns {string}
105
83
  */
106
- template.defaults.imports.renderContent = (content, editorType = 'rich', allowScript = 0) => {
107
- if (!content || typeof content !== 'string') return content || '';
108
-
84
+ artTemplate.defaults.imports.renderContent = (content, editorType = "rich", allowScript = 0) => {
85
+ if (typeof content !== "string") return content || "";
109
86
  let html = content;
110
- // Markdown 转换
111
- if (editorType === 'md') {
87
+
88
+ if (editorType === "md") {
112
89
  try {
113
- html = marked.parse(content);
114
- } catch (err) {
115
- console.error('[renderContent] Markdown 渲染失败:', err.message);
116
- html = content;
90
+ // marked v4+ 支持异步扩展,返回 string | Promise<string>
91
+ // 同步场景下直接取结果;若返回 Promise 则降级为空字符串
92
+ const result = marked.parse(content);
93
+ html = typeof result === "string" ? result : "";
94
+ } catch (e) {
95
+ logger.error(`[renderContent] MD解析失败: ${e.message}`);
117
96
  }
118
97
  }
119
- // 非 allowScript=1 时通过 filterXSS 完整过滤,移除 script/onevent 等危险内容
98
+
120
99
  if (Number(allowScript) !== 1) {
121
- try {
122
- html = filterXSS(html);
123
- } catch (err) {
124
- console.error('[renderContent] XSS 过滤失败:', err.message);
100
+ try { html = filterXSS(html); }
101
+ catch (e) {
102
+ logger.error(`[renderContent] XSS过滤失败: ${e.message}`);
125
103
  }
126
104
  }
127
105
  return html;
128
106
  };
129
107
 
130
- // ============================================================
131
- // 模板引擎中间件配置
132
- // ============================================================
133
-
134
108
  /**
135
- * 设置模板引擎中间件
136
- * @param {Object} app - Express 应用实例
137
- * @param {Object} config - 配置选项
138
- * @param {Array<string>} config.views - 模板目录数组
139
- * @param {string} config.NODE_ENV - 运行环境
140
- * @description
141
- * 为 Express 应用配置 art-template 模板引擎
142
- * 支持多个模板目录,自动添加 web 模块视图目录
143
- * @example
144
- * setTemplate(app, {
145
- * views: ['./views'],
146
- * NODE_ENV: 'development'
147
- * });
109
+ * art-template模板引擎中间件
110
+ * @param {express.Application} app
111
+ * @param {{views:string[],NODE_ENV:string}} config
148
112
  */
149
- export let setTemplate = (app, config) => {
113
+ export const template = (app, config) => {
150
114
  const { views, NODE_ENV } = config;
151
- const isProduction = NODE_ENV === "production" || NODE_ENV === "prd";
152
- console.log("模板缓存->", isProduction);
153
- const all = [...views];
115
+ const isProduction = ["production", "prd"].includes(NODE_ENV);
116
+ logger.info("模板缓存开启:", isProduction);
117
+
154
118
  app.set("view options", {
155
119
  debug: !isProduction,
156
120
  cache: isProduction,
157
121
  minimize: true,
158
122
  });
159
123
  app.set("view engine", "html");
160
- app.set("views", all);
161
- // 引擎加载容错:importjs 抛错时降级为内置默认 engineFn,避免启动失败
124
+ app.set("views", [...views]);
125
+
162
126
  try {
163
- app.engine(".html", importjs("express-art-template"));
127
+ app.engine(".html", require("express-art-template"));
164
128
  } catch (err) {
165
- console.error('[setTemplate] 加载 express-art-template 失败,降级默认引擎:', err.message);
166
- const engineFn = (path, options, callback) => {
167
- // 兜底:直接返回模板路径,避免渲染崩溃
168
- callback(null, `<pre>Template engine fallback: ${path}</pre>`);
169
- };
170
- app.engine(".html", engineFn);
129
+ logger.error(`[template] 模板引擎加载失败,启用降级渲染: ${err.message}`);
130
+ app.engine(".html", (path, _, cb) => cb(null, `<pre>模板降级: ${path}</pre>`));
171
131
  }
172
- };
132
+ };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * 声明式参数校验中间件(基于 zod)
3
+ * 在 Controller 上定义 static rules,框架自动校验 req.body/req.query/req.params
4
+ */
5
+ import { ValidationError } from "../core/errors.js";
6
+
7
+ /**
8
+ * 写回校验结果:Express 5 的 req.query 为只读 getter,直接赋值会静默失败,
9
+ * 需用 defineProperty 定义自有属性遮蔽原型 getter。
10
+ * @private
11
+ */
12
+ function setValidated(req, source, value) {
13
+ if (source === 'query') {
14
+ Object.defineProperty(req, 'query', {
15
+ value,
16
+ writable: true,
17
+ enumerable: true,
18
+ configurable: true,
19
+ });
20
+ } else {
21
+ req[source] = value;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * 创建 zod 校验中间件
27
+ * @param {import('zod').ZodSchema} schema - zod schema
28
+ * @param {"body"|"query"|"params"} [source='body'] - 校验来源
29
+ * @returns {Function} Express 中间件
30
+ *
31
+ * @example
32
+ * // 在 Controller 上定义 rules
33
+ * class ArticleController extends Controller {
34
+ * static rules = {
35
+ * create: z.object({ title: z.string().min(1), content: z.string() }),
36
+ * };
37
+ * }
38
+ *
39
+ * // router 中使用
40
+ * router.post('/create', validate(ArticleController.rules.create), ctrl.create.bind(ctrl));
41
+ */
42
+ export function validate(schema, source = 'body') {
43
+ return (req, res, next) => {
44
+ const result = schema.safeParse(req[source]);
45
+ if (!result.success) {
46
+ const fields = result.error.issues?.map(i => i.path.join('.')) || [];
47
+ const msg = result.error.issues?.map(i => i.message).join('; ') || '参数校验失败';
48
+ const err = new ValidationError(msg, fields);
49
+ return next(err);
50
+ }
51
+ // 使用 setValidated 统一处理,解决 Express 5 req.query 只读问题
52
+ setValidated(req, source, result.data);
53
+ // 同时设置 req.validated 作为统一访问点(向后兼容)
54
+ req.validated = result.data;
55
+ next();
56
+ };
57
+ }
58
+
59
+ /**
60
+ * 批量校验多个来源
61
+ * @param {Object} schemas - { body?, query?, params? } 各来源的 zod schema
62
+ * @returns {Function} Express 中间件
63
+ */
64
+ export function validateAll(schemas) {
65
+ return (req, res, next) => {
66
+ for (const [source, schema] of Object.entries(schemas)) {
67
+ if (!schema) continue;
68
+ const result = schema.safeParse(req[source]);
69
+ if (!result.success) {
70
+ const fields = result.error.issues?.map(i => `${source}.${i.path.join('.')}`) || [];
71
+ const msg = result.error.issues?.map(i => i.message).join('; ') || '参数校验失败';
72
+ const err = new ValidationError(msg, fields);
73
+ return next(err);
74
+ }
75
+ setValidated(req, source, result.data);
76
+ }
77
+ next();
78
+ };
79
+ }