chanjs 2.1.1 → 2.3.1

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 (61) hide show
  1. package/App.js +384 -0
  2. package/base/Context.js +78 -0
  3. package/base/Controller.js +137 -0
  4. package/base/Database.js +314 -0
  5. package/base/Service.js +539 -0
  6. package/common/api.js +25 -0
  7. package/common/category.js +22 -0
  8. package/common/code.js +42 -0
  9. package/common/email.js +110 -0
  10. package/common/index.js +7 -0
  11. package/common/pages.js +86 -0
  12. package/common/sms.js +104 -0
  13. package/common/utils.js +73 -0
  14. package/config/code.js +110 -52
  15. package/config/index.js +10 -0
  16. package/config/paths.js +60 -0
  17. package/extend/art-template.js +46 -28
  18. package/extend/index.js +6 -0
  19. package/global/env.js +11 -5
  20. package/global/global.js +63 -39
  21. package/global/import.js +43 -39
  22. package/global/index.js +8 -3
  23. package/helper/cache.js +182 -0
  24. package/helper/data-parse.js +121 -37
  25. package/helper/db.js +71 -83
  26. package/helper/file.js +158 -208
  27. package/helper/filter.js +34 -0
  28. package/helper/html.js +30 -47
  29. package/helper/index.js +29 -5
  30. package/helper/ip.js +48 -31
  31. package/helper/jwt.js +78 -11
  32. package/helper/loader.js +93 -50
  33. package/helper/request.js +41 -144
  34. package/helper/sign.js +96 -33
  35. package/helper/time.js +89 -74
  36. package/helper/tree.js +77 -0
  37. package/index.js +15 -181
  38. package/middleware/cookie.js +20 -4
  39. package/middleware/cors.js +20 -0
  40. package/middleware/favicon.js +21 -5
  41. package/middleware/header.js +26 -9
  42. package/middleware/index.js +14 -23
  43. package/middleware/preventRetry.js +30 -0
  44. package/middleware/setBody.js +24 -10
  45. package/middleware/static.js +31 -10
  46. package/middleware/template.js +34 -14
  47. package/middleware/validator.js +43 -23
  48. package/middleware/waf.js +147 -287
  49. package/package.json +1 -1
  50. package/utils/checker.js +68 -0
  51. package/utils/error-handler.js +115 -0
  52. package/utils/error.js +81 -0
  53. package/utils/index.js +6 -0
  54. package/utils/keywords.js +126 -0
  55. package/utils/rate-limit.js +116 -0
  56. package/utils/response.js +103 -64
  57. package/utils/xss-filter.js +42 -0
  58. package/core/controller.js +0 -33
  59. package/core/index.js +0 -3
  60. package/core/service.js +0 -307
  61. package/middleware/log.js +0 -21
package/App.js ADDED
@@ -0,0 +1,384 @@
1
+ import express from "express";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ import AppContext from "./base/Context.js";
6
+ import Controller from "./base/Controller.js";
7
+ import DatabaseManager from "./base/Database.js";
8
+
9
+ import Service from "./base/Service.js";
10
+
11
+ import { Paths } from "./config/index.js";
12
+ import { getChildrenId, filterBody, filterImgFromStr, CODE as commonCode, sendMail, genRegEmailHtml, genResetPasswordEmail, pages, getHtmlFilesSync } from "./common/index.js";
13
+ import { db, loadConfig, loadController, loaderSort, prefixDbConfig, formatDateFields, request, delImg, getIp, setToken, getToken, verifyToken, generateToken, getFileTree, readFileContent, saveFileContent, isPathSafe, arrToObj, htmlDecode } from "./helper/index.js";
14
+ import { Cors, setBody, setCookie, setFavicon, setHeader, setStatic, setTemplate, waf } from "./middleware/index.js";
15
+ import { errorResponse, notFoundResponse, parseDatabaseError } from "./utils/error-handler.js";
16
+ import { error as responseError, fail, success } from "./utils/response.js";
17
+
18
+ import "./global/index.js";
19
+
20
+ /**
21
+ * Chan 应用核心类
22
+ * @class Chan
23
+ * @description 管理应用程序的路由、中间件、数据库连接和服务
24
+ * @example
25
+ * const app = new Chan({ port: 3000, env: "production" });
26
+ * await app.start();
27
+ */
28
+ class Chan {
29
+ static helper = {
30
+ loadController,
31
+ db,
32
+ prefixDbConfig,
33
+ loaderSort,
34
+ loadConfig,
35
+ formatDateFields,
36
+ request,
37
+ delImg,
38
+ getIp,
39
+ setToken,
40
+ getToken,
41
+ verifyToken,
42
+ generateToken,
43
+ getFileTree,
44
+ readFileContent,
45
+ saveFileContent,
46
+ isPathSafe,
47
+ arrToObj,
48
+ htmlDecode,
49
+ };
50
+ static common = {
51
+ success,
52
+ fail,
53
+ error: responseError,
54
+ getChildrenId,
55
+ CODE: commonCode,
56
+ sendMail,
57
+ genRegEmailHtml,
58
+ genResetPasswordEmail,
59
+ pages,
60
+ getHtmlFilesSync,
61
+ filterBody,
62
+ filterImgFromStr,
63
+ };
64
+ static config = {};
65
+ static Service = Service;
66
+ static Controller = Controller;
67
+ static paths = Paths;
68
+
69
+ /**
70
+ * 构造函数
71
+ * @constructor
72
+ * @param {Object} options - 应用配置选项
73
+ * @param {number} [options.port=3000] - 服务器端口
74
+ * @param {string} [options.env] - 运行环境
75
+ * @description 初始化 Express 应用和核心组件
76
+ */
77
+ constructor(options = {}) {
78
+ this.app = express();
79
+ this.context = new AppContext();
80
+ this.dbManager = new DatabaseManager();
81
+
82
+ this.options = {
83
+ port: options.port || 3000,
84
+ env: options.env || process.env.NODE_ENV || "development",
85
+ ...options,
86
+ };
87
+
88
+
89
+ this.router = express.Router();
90
+ }
91
+
92
+ /**
93
+ * 启动应用程序
94
+ * @async
95
+ * @description 执行完整的启动流程
96
+ * 包括:初始化配置、数据库、扩展、中间件、路由、错误处理
97
+ * @returns {Promise<void>}
98
+ */
99
+ async start() {
100
+
101
+ global.appContext = this.context;
102
+ global.Chan = Chan;
103
+
104
+ //加载配置
105
+ await this.config();
106
+ //加载数据库
107
+ await this.loadDB();
108
+ //加载扩展方法
109
+ await this.loadExtend();
110
+ //加载中间件
111
+ await this.loadAppMiddleware();
112
+ //设置app
113
+ this.setApp();
114
+ //加载路由
115
+ await this.loadRouter();
116
+ await this.loadCommonRouter();
117
+ this.useRouter();
118
+ //404 500 处理
119
+ this.setErrorHandler();
120
+
121
+ }
122
+
123
+ /**
124
+ * 加载配置
125
+ * @async
126
+ * @returns {Promise<void>}
127
+ * @description 加载应用配置并设置到上下文
128
+ */
129
+ async config() {
130
+ let config = await loadConfig();
131
+ Chan.config = config;
132
+ this.context.set("config", config);
133
+ }
134
+
135
+ /**
136
+ * 加载数据库
137
+ * @async
138
+ * @returns {Promise<void>}
139
+ * @description 根据配置初始化所有数据库连接
140
+ */
141
+ async loadDB() {
142
+ const dbList = Chan.config?.db || [];
143
+ const connections = this.dbManager.getConnections();
144
+
145
+ for (const [index, item] of dbList.entries()) {
146
+ const key = item.key || String(index);
147
+ try {
148
+ const dbConfig = prefixDbConfig(key) || item;
149
+ if (!dbConfig) throw new Error("未找到配置");
150
+
151
+ const connection = this.dbManager.add(key, dbConfig, { isDefault: index === 0 });
152
+ this.context.set(`db:${key}`, connection);
153
+
154
+ if (index === 0) {
155
+ Chan.db = connection;
156
+ }
157
+ } catch (error) {
158
+ console.error(`[DB] 数据库 ${key} 初始化失败: ${error.message}`);
159
+ }
160
+ }
161
+
162
+ console.log(
163
+ `[DB] 初始化完成,已加载 ${dbList.length} 个数据库,成功 ${Object.keys(connections).length} 个`,
164
+ );
165
+ }
166
+
167
+ /**
168
+ * 加载扩展模块
169
+ * @async
170
+ * @returns {Promise<void>}
171
+ * @description 加载 common、helper、extend 目录下的扩展模块
172
+ */
173
+ async loadExtend() {
174
+ const extensions = [
175
+ { _path: Paths.commonPath, key: "common" },
176
+ { _path: Paths.helperPath, key: "helper" },
177
+ { _path: Paths.extendPath, key: "extend" },
178
+ ];
179
+
180
+ for (const { _path, key } of extensions) {
181
+ await this.loadFn(_path, key);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * 加载模块文件
187
+ * @async
188
+ * @param {string} _path - 模块目录路径
189
+ * @param {string} key - 模块键名
190
+ * @returns {Promise<void>}
191
+ * @description 从指定目录加载所有 JS 模块并合并到 Chan 静态属性
192
+ */
193
+ async loadFn(_path, key) {
194
+ if (fs.existsSync(_path)) {
195
+ const files = fs.readdirSync(_path).filter((file) => file.endsWith(".js"));
196
+ for (const file of files) {
197
+ const filePath = path.join(_path, file);
198
+ let helperModule = await importFile(filePath);
199
+ Object.assign(Chan[key], helperModule);
200
+ }
201
+ }
202
+ }
203
+
204
+ /**
205
+ * 初始化中间件
206
+ * @async
207
+ * @returns {Promise<void>}
208
+ * @description 配置并注册所有应用中间件
209
+ */
210
+ async loadAppMiddleware() {
211
+ const config = Chan.config;
212
+ const {
213
+ views = [],
214
+ env = "development",
215
+ APP_NAME = "ChanCMS",
216
+ APP_VERSION = "1.0.0",
217
+ cookieKey,
218
+ BODY_LIMIT = "10mb",
219
+ statics = [],
220
+ cors = {},
221
+ PROXY = "false",
222
+ waf: wafConfig = { enabled: false }
223
+ } = config;
224
+
225
+ await waf(this.app, wafConfig);
226
+ setFavicon(this.app);
227
+ setStatic(this.app, statics);
228
+ setCookie(this.app, cookieKey);
229
+ setBody(this.app, BODY_LIMIT);
230
+ Cors(this.app, cors);
231
+ setTemplate(this.app, { views, NODE_ENV: env });
232
+ setHeader(this.app, { APP_NAME, APP_VERSION });
233
+ }
234
+
235
+ /**
236
+ * 应用配置
237
+ * @private
238
+ * @description 设置 Express 应用配置
239
+ */
240
+ setApp() {
241
+ this.app.set("trust proxy", Chan.config.PROXY === "true");
242
+ this.app.set("env", this.options.env);
243
+ this.app.disable("x-powered-by");
244
+ }
245
+
246
+ /**
247
+ * 应用路由
248
+ * @private
249
+ * @description 将所有路由应用到 Express 应用
250
+ */
251
+ useRouter() {
252
+ this.app.use(this.router);
253
+ }
254
+
255
+ /**
256
+ * 应用错误处理404 500
257
+ * @private
258
+ * @description 配置全局错误处理中间件
259
+ */
260
+ setErrorHandler() {
261
+ //404
262
+ this.app.use((req, res) => {
263
+ res.status(404).json(notFoundResponse(req));
264
+ });
265
+
266
+ // 500
267
+ this.app.use((err, req, res, next) => {
268
+ if (res.headersSent) return next(err);
269
+
270
+ const errorInfo = err?.stack ? this._parseErrorStack(err.stack) : { message: err?.message || err };
271
+ const requestInfo = `${req.method} ${req.originalUrl}`;
272
+ console.error(`[Global Error] ${requestInfo} - ${errorInfo.file}:${errorInfo.line || "?"} - ${errorInfo.message}`);
273
+
274
+ if (err?.isError && err.message) {
275
+ const statusCode = err.statusCode >= 500 ? 500 : 400;
276
+ console.error(`[Global Error Details] Code: ${err.code || 500}, Status: ${statusCode}`);
277
+ return res.status(statusCode).json({
278
+ success: false,
279
+ msg: err.message,
280
+ code: err.code || 500,
281
+ data: process.env.NODE_ENV === "development" ? {
282
+ stack: err.stack,
283
+ file: errorInfo.file,
284
+ line: errorInfo.line,
285
+ column: errorInfo.column,
286
+ url: req.originalUrl,
287
+ method: req.method
288
+ } : {},
289
+ });
290
+ }
291
+
292
+ const parsedError = parseDatabaseError(err);
293
+ console.error(`[Global Error Details] Database Error - ${parsedError.msg}`);
294
+ res.status(parsedError.statusCode).json(errorResponse(err, req));
295
+ });
296
+ }
297
+
298
+ /**
299
+ * 解析错误堆栈
300
+ * @private
301
+ * @param {string} stack - 错误堆栈字符串
302
+ * @returns {Object} 解析后的错误信息
303
+ * @description 从错误堆栈中提取文件名、行号、列号等信息
304
+ */
305
+ _parseErrorStack(stack) {
306
+ if (!stack) return { message: '未知错误' };
307
+
308
+ const stackLines = stack.split('\n');
309
+ if (stackLines.length < 2) {
310
+ return { message: stackLines[0] || '未知错误' };
311
+ }
312
+
313
+ const errorLine = stackLines[1].trim();
314
+ const match = errorLine.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
315
+
316
+ if (match) {
317
+ return {
318
+ message: stackLines[0] || '未知错误',
319
+ function: match[1],
320
+ file: match[2],
321
+ line: match[3],
322
+ column: match[4]
323
+ };
324
+ }
325
+
326
+ const fileMatch = errorLine.match(/at\s+(.+?):(\d+):(\d+)/);
327
+ if (fileMatch) {
328
+ return {
329
+ message: stackLines[0] || '未知错误',
330
+ file: fileMatch[1],
331
+ line: fileMatch[2],
332
+ column: fileMatch[3]
333
+ };
334
+ }
335
+
336
+ return { message: stackLines[0] || '未知错误', file: errorLine };
337
+ }
338
+
339
+
340
+
341
+ /**
342
+ * 加载模块路由
343
+ * @async
344
+ * @returns {Promise<void>}
345
+ * @description 从 app/modules 目录加载所有模块的路由
346
+ */
347
+ async loadRouter() {
348
+ const configPath = path.join(Paths.appPath, "modules");
349
+ if (fs.existsSync(configPath)) {
350
+ const dirs = loaderSort(Chan.config.modules);
351
+ for (const item of dirs) {
352
+ let router = await importFile(`app/modules/${item}/router.js`);
353
+ router(this.app, this.router, Chan.config);
354
+ }
355
+ }
356
+ }
357
+
358
+ /**
359
+ * 加载公共路由
360
+ * @async
361
+ * @returns {Promise<void>}
362
+ * @description 加载 app/router.js 中的公共路由
363
+ */
364
+ async loadCommonRouter() {
365
+ let router = await importFile("app/router.js");
366
+ if (router) {
367
+ router(this.app, this.router, Chan.config);
368
+ }
369
+ }
370
+
371
+ /**
372
+ * 准备启动
373
+ * @param {Function} [cb] - 回调函数
374
+ * @description 在启动前执行回调,传递端口号
375
+ */
376
+ run(cb) {
377
+ this.app.listen(this.options.port, () => {
378
+ console.log(`Server running on port ${this.options.port}`);
379
+ cb?.(this.options.port);
380
+ });
381
+ }
382
+ }
383
+
384
+ export default Chan;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 应用上下文类
3
+ * 用于存储和管理应用的配置和模型实例
4
+ */
5
+ class AppContext {
6
+ /**
7
+ * 构造函数
8
+ * 初始化配置和模型的Map存储
9
+ */
10
+ constructor() {
11
+ this._config = new Map();
12
+ this._models = new Map();
13
+ }
14
+
15
+ /**
16
+ * 设置配置项
17
+ * @param {string} key - 配置键名
18
+ * @param {*} value - 配置值
19
+ */
20
+ set(key, value) {
21
+ this._config.set(key, value);
22
+ }
23
+
24
+ /**
25
+ * 获取配置项
26
+ * @param {string} key - 配置键名
27
+ * @param {*} defaultValue - 默认值
28
+ * @returns {*} 配置值或默认值
29
+ */
30
+ get(key, defaultValue) {
31
+ return this._config.get(key) ?? defaultValue;
32
+ }
33
+
34
+ /**
35
+ * 检查配置项是否存在
36
+ * @param {string} key - 配置键名
37
+ * @returns {boolean} 是否存在
38
+ */
39
+ has(key) {
40
+ return this._config.has(key);
41
+ }
42
+
43
+ /**
44
+ * 设置模型实例
45
+ * @param {string} name - 模型名称
46
+ * @param {Object} instance - 模型实例
47
+ */
48
+ setModel(name, instance) {
49
+ this._models.set(name, instance);
50
+ }
51
+
52
+ /**
53
+ * 获取模型实例
54
+ * @param {string} name - 模型名称
55
+ * @returns {Object|undefined} 模型实例
56
+ */
57
+ getModel(name) {
58
+ return this._models.get(name);
59
+ }
60
+
61
+ /**
62
+ * 获取所有模型实例
63
+ * @returns {Object} 包含所有模型的对象
64
+ */
65
+ getAllModels() {
66
+ return Object.fromEntries(this._models.entries());
67
+ }
68
+
69
+ /**
70
+ * 清空所有配置和模型
71
+ */
72
+ clear() {
73
+ this._config.clear();
74
+ this._models.clear();
75
+ }
76
+ }
77
+
78
+ export default AppContext;
@@ -0,0 +1,137 @@
1
+ import { success, fail, error as responseError } from "../utils/response.js";
2
+
3
+ /**
4
+ * 控制器基类
5
+ * 提供统一的响应格式和常用方法
6
+ */
7
+ export default class Controller {
8
+ /**
9
+ * 构造函数
10
+ * @param {Object} context - 应用上下文实例
11
+ */
12
+ constructor(context = null) {
13
+ if (context) {
14
+ this.context = context;
15
+ } else if (global.appContext) {
16
+ this.context = global.appContext;
17
+ } else {
18
+ this.context = { get: () => null, set: () => null };
19
+ }
20
+ }
21
+
22
+ /**
23
+ * 标准化返回结果
24
+ * @private
25
+ * @param {*} result - 原始结果
26
+ * @param {string} defaultMsg - 默认消息
27
+ * @returns {Object} 标准化的结果对象
28
+ */
29
+ _normalizeResult(result, defaultMsg = "操作成功") {
30
+ if (result === null || result === undefined) {
31
+ return { success: false, code: 5001, msg: "Service 返回空值", data: {} };
32
+ }
33
+
34
+ if (typeof result === 'object' && 'success' in result) {
35
+ return result;
36
+ }
37
+
38
+ if (typeof result === 'object' || Array.isArray(result)) {
39
+ return { success: true, code: 200, msg: defaultMsg, data: result };
40
+ }
41
+
42
+ return { success: true, code: 200, msg: defaultMsg, data: result };
43
+ }
44
+
45
+ /**
46
+ * 返回成功响应
47
+ * @param {*} options - 响应选项或数据
48
+ * @param {*} options.data - 响应数据
49
+ * @param {string} options.msg - 响应消息,默认"操作成功"
50
+ * @returns {Object} 标准成功响应
51
+ */
52
+ success(options, msg = "操作成功") {
53
+ let data, message = msg, extra = {};
54
+
55
+ if (options === null || options === undefined) {
56
+ data = undefined;
57
+ } else if (typeof options === 'object' && options !== null && !Array.isArray(options)) {
58
+ if ('data' in options) {
59
+ data = options.data;
60
+ message = options.msg || msg;
61
+ extra = { ...options };
62
+ delete extra.data;
63
+ delete extra.msg;
64
+ } else if ('success' in options && 'code' in options) {
65
+ return options;
66
+ } else {
67
+ data = options;
68
+ }
69
+ } else {
70
+ data = options;
71
+ }
72
+
73
+ if (data instanceof Error) {
74
+ return this.error({ err: data });
75
+ }
76
+
77
+ const normalized = this._normalizeResult(data, message);
78
+
79
+ if (!normalized.success) {
80
+ return this.fail({ msg: normalized.msg, data: normalized.data, code: normalized.code });
81
+ }
82
+
83
+ return {
84
+ success: true,
85
+ code: 200,
86
+ msg: normalized.msg,
87
+ data: normalized.data,
88
+ ...extra
89
+ };
90
+ }
91
+
92
+ /**
93
+ * 返回失败响应
94
+ * @param {Object} options - 响应选项
95
+ * @param {string} options.msg - 失败消息,默认"操作失败"
96
+ * @param {*} options.data - 响应数据
97
+ * @param {number} options.code - 错误码,默认201
98
+ * @returns {Object} 标准失败响应
99
+ */
100
+ fail({ msg = "操作失败", data = {}, code = 201 } = {}) {
101
+ return fail({ msg, data, code });
102
+ }
103
+
104
+ /**
105
+ * 返回错误响应
106
+ * @param {Object} options - 响应选项
107
+ * @param {Error} options.err - 错误对象
108
+ * @param {*} options.data - 响应数据
109
+ * @param {number} options.code - 错误码,默认500
110
+ * @returns {Object} 标准错误响应
111
+ */
112
+ error({ err, data = {}, code = 500 } = {}) {
113
+ return responseError({ err, data, code });
114
+ }
115
+
116
+ /**
117
+ * 返回分页响应
118
+ * @param {Array} list - 数据列表
119
+ * @param {number} total - 总记录数
120
+ * @param {number} current - 当前页码
121
+ * @param {number} pageSize - 每页大小
122
+ * @returns {Object} 标准分页响应
123
+ */
124
+ paginate(list, total, current, pageSize) {
125
+ return success({
126
+ data: {
127
+ list,
128
+ pagination: {
129
+ total,
130
+ current,
131
+ pageSize,
132
+ totalPages: Math.ceil(total / pageSize),
133
+ },
134
+ },
135
+ });
136
+ }
137
+ }