@mindbase/express-common 1.0.7 → 1.0.9

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.
@@ -0,0 +1,111 @@
1
+ import { Request, Response } from "express";
2
+
3
+ /**
4
+ * 标准 API 响应结构
5
+ */
6
+ export interface ApiResponse<T = any> {
7
+ code: number;
8
+ data: T;
9
+ msg: string;
10
+ }
11
+
12
+ /**
13
+ * 字段约束规则
14
+ */
15
+ export interface FieldConstraints {
16
+ min?: number;
17
+ max?: number;
18
+ minLength?: number;
19
+ maxLength?: number;
20
+ pattern?: string;
21
+ email?: boolean;
22
+ url?: boolean;
23
+ uuid?: boolean;
24
+ int?: boolean;
25
+ positive?: boolean;
26
+ nonnegative?: boolean;
27
+ custom?: string[]; // 自定义错误消息
28
+ }
29
+
30
+ /**
31
+ * 字段验证规则
32
+ */
33
+ export interface FieldValidation {
34
+ type: "string" | "number" | "boolean" | "array" | "object" | "date" | "file" | "any";
35
+ required: boolean;
36
+ optional?: boolean;
37
+ nullable?: boolean;
38
+ defaultValue?: any;
39
+ constraints?: FieldConstraints;
40
+ items?: FieldValidation; // 数组元素类型
41
+ properties?: Record<string, FieldValidation>; // 对象属性
42
+ }
43
+
44
+ /**
45
+ * 请求 Schema 结构
46
+ */
47
+ export interface RequestSchema {
48
+ target: "body" | "query" | "params";
49
+ fields: Record<string, FieldValidation>;
50
+ }
51
+
52
+ /**
53
+ * 响应 Schema 结构
54
+ */
55
+ export interface ResponseSchema {
56
+ description?: string;
57
+ fields: Record<string, FieldValidation>;
58
+ }
59
+
60
+ /**
61
+ * 路由文档配置
62
+ */
63
+ export interface RouteDocConfig<TRequest = any, TResponse = any> {
64
+ summary?: string;
65
+ description?: string;
66
+ request?: TRequest;
67
+ response?: TResponse;
68
+ }
69
+
70
+ /**
71
+ * 路由信息接口
72
+ */
73
+ export interface RouteInfo {
74
+ id?: number;
75
+ module: string;
76
+ method: string;
77
+ path: string;
78
+ fullPath: string;
79
+ summary: string;
80
+ description: string;
81
+ requestSchema?: RequestSchema;
82
+ responseSchema?: ResponseSchema;
83
+ middlewares?: string[];
84
+ filePath: string;
85
+ createdAt?: number;
86
+ updatedAt?: number;
87
+ }
88
+
89
+ /**
90
+ * 标记路由文档配置
91
+ */
92
+ export function RouteDoc<TRequest = any, TResponse = any>(config: RouteDocConfig<TRequest, TResponse>) {
93
+ return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
94
+ // 运行时不做任何处理,仅用于类型定义和静态分析
95
+ };
96
+ }
97
+
98
+ /**
99
+ * 模块列表项接口
100
+ */
101
+ export interface ModuleItem {
102
+ name: string;
103
+ count: number;
104
+ }
105
+
106
+ /**
107
+ * 文档数据库配置接口
108
+ */
109
+ export interface DocDatabaseConfig {
110
+ path: string;
111
+ }
@@ -0,0 +1,12 @@
1
+ import { AppState, MindBaseAppOptions } from ".";
2
+
3
+ declare global {
4
+ namespace Express {
5
+ interface Application {
6
+ getDB: () => AppState["db"];
7
+ configs: MindBaseAppOptions;
8
+ }
9
+ }
10
+ }
11
+
12
+ export {};
package/types/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ export { AppState, MindBaseAppOptions } from "../core/state";
2
+ export { MindBaseApp } from "../core/app";
3
+ export { LogLevel } from "../utils/Logger";
4
+ export { IpInfo } from "../middleware/IpParser";
5
+ export { UaInfo } from "../middleware/UaParser";
6
+ export { ScanResult } from "../feature/scanner/FileScanner";
7
+ export { CronConfig } from "../feature/cron/CronManager";
8
+ export {
9
+ RouteDoc,
10
+ ApiResponse,
11
+ RouteInfo,
12
+ ModuleItem,
13
+ DocDatabaseConfig,
14
+ FieldValidation,
15
+ FieldConstraints,
16
+ RequestSchema,
17
+ ResponseSchema,
18
+ RouteDocConfig
19
+ } from "./DocTypes";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 创建业务错误
3
+ * @param code HTTP 状态码
4
+ * @param msg 错误消息
5
+ */
6
+ export function createAppError(code: number, msg: string): Error {
7
+ const err = new Error(msg);
8
+ (err as any).statusCode = code;
9
+ (err as any).isAppError = true;
10
+ return err;
11
+ }
12
+
13
+ /**
14
+ * 便捷工厂函数
15
+ */
16
+ export const Errors = {
17
+ badRequest: (msg: string) => createAppError(400, msg),
18
+ unauthorized: (msg: string) => createAppError(401, msg),
19
+ forbidden: (msg: string) => createAppError(403, msg),
20
+ notFound: (msg: string) => createAppError(404, msg),
21
+ };
@@ -0,0 +1,34 @@
1
+ import { AppState } from "../core/state";
2
+ import { ScanResult } from "../types/Index";
3
+ import { registerMiddleware } from "./MiddlewareRegistry";
4
+ import { registerRoute } from "./RouteRegistry";
5
+ import logger from "./Logger";
6
+
7
+ /**
8
+ * 封装细节:遍历扫描结果并注册所有组件(中间件和路由)
9
+ */
10
+ export async function registerComponents(state: AppState, scannedResults: ScanResult[]) {
11
+ // 1. 先注册所有中间件
12
+ for (const item of scannedResults) {
13
+ if (item.type === "middleware") {
14
+ registerMiddleware(state.express, item);
15
+ }
16
+ }
17
+
18
+ // 2. 再注册所有路由
19
+ for (const item of scannedResults) {
20
+ if (item.type === "route") {
21
+ registerRoute(state.express, item, state.options.apiPrefix);
22
+ }
23
+ }
24
+
25
+ // 3. 所有组件注册完成后,触发路由资源同步(如果有)
26
+ const syncAppRoutes = state.express.get("syncAppRoutes");
27
+ if (syncAppRoutes && typeof syncAppRoutes === "function") {
28
+ try {
29
+ await syncAppRoutes();
30
+ } catch (error) {
31
+ logger.error("[ComponentRegistry] 路由资源同步失败:", error);
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,121 @@
1
+ import { AppState } from "../core/state";
2
+ import logger from "./Logger";
3
+
4
+ /**
5
+ * 通过代码调用的方式执行数据库迁移
6
+ * @param db 数据库实例
7
+ * @param schemas 数据库 schema 定义
8
+ * @returns 迁移是否成功
9
+ */
10
+ export async function executeDatabaseMigration(db: any, schemas: Record<string, any>): Promise<boolean> {
11
+ try {
12
+ if (!db) {
13
+ logger.error("数据库实例不能为空");
14
+ return false;
15
+ }
16
+
17
+ if (!schemas) {
18
+ logger.error("数据库 schema 定义不能为空");
19
+ return false;
20
+ }
21
+
22
+ logger.startup("数据库", "开始执行数据库表结构同步...");
23
+
24
+ // 方案:调用 drizzle-kit push 命令
25
+ try {
26
+ const { spawn } = await import("child_process");
27
+
28
+ logger.startup("数据库", "正在调用 drizzle-kit push 同步表结构...");
29
+
30
+ // 使用 drizzle-kit push 命令,捕获输出以判断错误类型
31
+ const result = await new Promise<boolean>((resolve) => {
32
+ let output = "";
33
+ let errorOutput = "";
34
+
35
+ const push = spawn("npx", ["drizzle-kit", "push"], {
36
+ shell: true,
37
+ });
38
+
39
+ push.stdout?.on("data", (data) => {
40
+ const text = data.toString();
41
+ output += text;
42
+ });
43
+
44
+ push.stderr?.on("data", (data) => {
45
+ const text = data.toString();
46
+ errorOutput += text;
47
+ });
48
+
49
+ push.on("close", (code) => {
50
+ if (code === 0) {
51
+ logger.startup("数据库", "✅ drizzle-kit push 执行成功");
52
+ resolve(true);
53
+ } else {
54
+ // 检查是否是已存在对象的良性错误
55
+ const errorMsg = errorOutput || output;
56
+ const benignErrors = [
57
+ "already exists",
58
+ "索引.*已存在",
59
+ "index.*already exists",
60
+ "table.*already exists",
61
+ ];
62
+
63
+ const isBenign = benignErrors.some((pattern) =>
64
+ new RegExp(pattern, "i").test(errorMsg)
65
+ );
66
+
67
+ if (isBenign) {
68
+ logger.startup("数据库", "✅ 数据库结构已同步(索引/表已存在)");
69
+ resolve(true);
70
+ } else {
71
+ logger.error(`❌ drizzle-kit push 执行失败,退出码: ${code}`);
72
+ if (errorMsg) {
73
+ logger.error(errorMsg);
74
+ }
75
+ resolve(false);
76
+ }
77
+ }
78
+ });
79
+
80
+ push.on("error", (error) => {
81
+ logger.error("执行 drizzle-kit push 时出错:", error);
82
+ resolve(false);
83
+ });
84
+ });
85
+
86
+ return result;
87
+ } catch (pushError) {
88
+ logger.error("调用 drizzle-kit push 失败:", pushError);
89
+ logger.info("💡 提示:请手动运行 'npx drizzle-kit push' 来同步表结构");
90
+ return false;
91
+ }
92
+ } catch (error) {
93
+ logger.error("数据库迁移执行失败:", error);
94
+ return false;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * 检查并执行数据库迁移(如果需要)
100
+ * @param state 应用状态
101
+ * @returns 迁移是否成功
102
+ */
103
+ export async function checkAndMigrateDatabase(state: AppState): Promise<boolean> {
104
+ try {
105
+ if (!state || !state.db || !state.schemas) {
106
+ logger.error("应用状态、数据库实例或 schema 定义不能为空");
107
+ return false;
108
+ }
109
+
110
+ // 检查 schema 是否有定义
111
+ if (Object.keys(state.schemas).length === 0) {
112
+ return true;
113
+ }
114
+
115
+ // 执行数据库迁移
116
+ return await executeDatabaseMigration(state.db, state.schemas);
117
+ } catch (error) {
118
+ logger.error("检查并执行数据库迁移失败:", error);
119
+ return false;
120
+ }
121
+ }
package/utils/Dayjs.ts ADDED
@@ -0,0 +1,16 @@
1
+ import dayjs from "dayjs";
2
+ import "dayjs/locale/zh-cn";
3
+ import duration from "dayjs/plugin/duration";
4
+ import relativeTime from "dayjs/plugin/relativeTime";
5
+ import utc from "dayjs/plugin/utc";
6
+
7
+ // 设置中文语言包
8
+ dayjs.locale("zh-cn");
9
+
10
+ // 注册常用插件
11
+ dayjs.extend(utc);
12
+ dayjs.extend(duration);
13
+ dayjs.extend(relativeTime);
14
+
15
+ export default dayjs;
16
+ export { dayjs };
@@ -0,0 +1,279 @@
1
+ import Database from "better-sqlite3";
2
+ import { Database as DatabaseType } from "better-sqlite3";
3
+ import * as path from "path";
4
+ import * as fs from "fs";
5
+ import { RouteInfo, ModuleItem, DocDatabaseConfig } from "../types/DocTypes";
6
+ import logger from "./Logger";
7
+
8
+ /**
9
+ * 文档管理器
10
+ * 管理文档数据,与独立的 SQLite 数据库交互
11
+ */
12
+ export class DocManager {
13
+ private db: DatabaseType | null = null;
14
+ private config: DocDatabaseConfig;
15
+
16
+ constructor(config: DocDatabaseConfig) {
17
+ this.config = config;
18
+ this.init();
19
+ }
20
+
21
+ /**
22
+ * 初始化数据库
23
+ */
24
+ public init(): void {
25
+ try {
26
+ // 确保数据库目录存在
27
+ const dbDir = path.dirname(this.config.path);
28
+ if (!fs.existsSync(dbDir)) {
29
+ fs.mkdirSync(dbDir, { recursive: true });
30
+ }
31
+
32
+ // 连接数据库
33
+ this.db = new Database(this.config.path);
34
+
35
+ // 创建表结构
36
+ this.createTables();
37
+ } catch (error) {
38
+ logger.error("文档数据库初始化失败:", error);
39
+ this.db = null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * 创建表结构
45
+ */
46
+ private createTables(): void {
47
+ if (!this.db) return;
48
+
49
+ // 创建 doc_routes 表
50
+ this.db.exec(`
51
+ CREATE TABLE IF NOT EXISTS doc_routes (
52
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
53
+ module TEXT NOT NULL,
54
+ method TEXT NOT NULL,
55
+ path TEXT NOT NULL,
56
+ full_path TEXT NOT NULL,
57
+ summary TEXT,
58
+ description TEXT,
59
+ request_schema TEXT,
60
+ response_schema TEXT,
61
+ middlewares TEXT,
62
+ file_path TEXT NOT NULL,
63
+ created_at INTEGER NOT NULL,
64
+ updated_at INTEGER NOT NULL,
65
+ UNIQUE(module, method, path)
66
+ );
67
+ `);
68
+
69
+ // 创建索引
70
+ this.db.exec(`
71
+ CREATE INDEX IF NOT EXISTS idx_doc_routes_module ON doc_routes(module);
72
+ CREATE INDEX IF NOT EXISTS idx_doc_routes_full_path ON doc_routes(full_path);
73
+ `);
74
+ }
75
+
76
+ /**
77
+ * 保存路由信息
78
+ * @param routeInfo 路由信息
79
+ * @param apiPrefix API 前缀
80
+ * @param moduleName 模块名称
81
+ */
82
+ public saveRoute(routeInfo: RouteInfo, apiPrefix: string = "/api", moduleName: string): void {
83
+ if (!this.db) return;
84
+
85
+ try {
86
+ // 计算完整路径
87
+ const fullPath = `${apiPrefix}/${moduleName}${routeInfo.path}`.replace(/\/+/g, "/");
88
+
89
+ // 准备数据
90
+ const now = Date.now();
91
+ const data = {
92
+ module: moduleName,
93
+ method: routeInfo.method,
94
+ path: routeInfo.path,
95
+ full_path: fullPath,
96
+ summary: routeInfo.summary,
97
+ description: routeInfo.description,
98
+ request_schema: routeInfo.requestSchema ? JSON.stringify(routeInfo.requestSchema) : null,
99
+ response_schema: routeInfo.responseSchema ? JSON.stringify(routeInfo.responseSchema) : null,
100
+ middlewares: routeInfo.middlewares ? JSON.stringify(routeInfo.middlewares) : null,
101
+ file_path: routeInfo.filePath,
102
+ created_at: now,
103
+ updated_at: now,
104
+ };
105
+
106
+ // 插入或更新数据
107
+ const stmt = this.db.prepare(`
108
+ INSERT INTO doc_routes (
109
+ module, method, path, full_path, summary, description,
110
+ request_schema, response_schema, middlewares, file_path, created_at, updated_at
111
+ ) VALUES (
112
+ @module, @method, @path, @full_path, @summary, @description,
113
+ @request_schema, @response_schema, @middlewares, @file_path, @created_at, @updated_at
114
+ ) ON CONFLICT(module, method, path) DO UPDATE SET
115
+ full_path = @full_path,
116
+ summary = @summary,
117
+ description = @description,
118
+ request_schema = @request_schema,
119
+ response_schema = @response_schema,
120
+ middlewares = @middlewares,
121
+ file_path = @file_path,
122
+ updated_at = @updated_at
123
+ `);
124
+
125
+ stmt.run(data);
126
+ } catch (error) {
127
+ logger.error("保存路由信息失败:", error);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * 获取模块列表
133
+ * @returns 模块列表
134
+ */
135
+ public getModules(): ModuleItem[] {
136
+ if (!this.db) return [];
137
+
138
+ try {
139
+ const stmt = this.db.prepare(`
140
+ SELECT module, COUNT(*) as count
141
+ FROM doc_routes
142
+ GROUP BY module
143
+ ORDER BY module
144
+ `);
145
+
146
+ return stmt.all() as ModuleItem[];
147
+ } catch (error) {
148
+ logger.error("获取模块列表失败:", error);
149
+ return [];
150
+ }
151
+ }
152
+
153
+ /**
154
+ * 获取路由列表
155
+ * @param module 模块名称(可选)
156
+ * @returns 路由列表
157
+ */
158
+ public getRoutes(module?: string): RouteInfo[] {
159
+ if (!this.db) return [];
160
+
161
+ try {
162
+ let query = `
163
+ SELECT id, module, method, path, full_path, summary, description,
164
+ request_schema, response_schema, middlewares, file_path, created_at, updated_at
165
+ FROM doc_routes
166
+ `;
167
+
168
+ const params: any = {};
169
+
170
+ if (module) {
171
+ query += " WHERE module = @module";
172
+ params.module = module;
173
+ }
174
+
175
+ query += " ORDER BY module, method, path";
176
+
177
+ const stmt = this.db.prepare(query);
178
+ const rows = stmt.all(params) as any[];
179
+
180
+ // 转换数据格式
181
+ return rows.map((row) => ({
182
+ id: row.id,
183
+ module: row.module,
184
+ method: row.method,
185
+ path: row.path,
186
+ fullPath: row.full_path,
187
+ summary: row.summary,
188
+ description: row.description,
189
+ requestSchema: row.request_schema ? JSON.parse(row.request_schema) : undefined,
190
+ responseSchema: row.response_schema ? JSON.parse(row.response_schema) : undefined,
191
+ middlewares: row.middlewares ? JSON.parse(row.middlewares) : undefined,
192
+ filePath: row.file_path,
193
+ createdAt: row.created_at,
194
+ updatedAt: row.updated_at,
195
+ }));
196
+ } catch (error) {
197
+ logger.error("获取路由列表失败:", error);
198
+ return [];
199
+ }
200
+ }
201
+
202
+ /**
203
+ * 获取路由详情
204
+ * @param id 路由 ID
205
+ * @returns 路由详情
206
+ */
207
+ public getRouteById(id: number): RouteInfo | null {
208
+ if (!this.db) return null;
209
+
210
+ try {
211
+ const stmt = this.db.prepare(`
212
+ SELECT id, module, method, path, full_path, summary, description,
213
+ request_schema, response_schema, middlewares, file_path, created_at, updated_at
214
+ FROM doc_routes
215
+ WHERE id = @id
216
+ `);
217
+
218
+ const row = stmt.get({ id }) as any;
219
+
220
+ if (!row) return null;
221
+
222
+ // 转换数据格式
223
+ return {
224
+ id: row.id,
225
+ module: row.module,
226
+ method: row.method,
227
+ path: row.path,
228
+ fullPath: row.full_path,
229
+ summary: row.summary,
230
+ description: row.description,
231
+ requestSchema: row.request_schema ? JSON.parse(row.request_schema) : undefined,
232
+ responseSchema: row.response_schema ? JSON.parse(row.response_schema) : undefined,
233
+ middlewares: row.middlewares ? JSON.parse(row.middlewares) : undefined,
234
+ filePath: row.file_path,
235
+ createdAt: row.created_at,
236
+ updatedAt: row.updated_at,
237
+ };
238
+ } catch (error) {
239
+ logger.error("获取路由详情失败:", error);
240
+ return null;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * 清空所有路由数据
246
+ */
247
+ public clearAllRoutes(): void {
248
+ if (!this.db) return;
249
+
250
+ try {
251
+ this.db.exec("DELETE FROM doc_routes");
252
+ } catch (error) {
253
+ logger.error("清空路由数据失败:", error);
254
+ }
255
+ }
256
+
257
+ /**
258
+ * 关闭数据库连接
259
+ */
260
+ public close(): void {
261
+ if (this.db) {
262
+ this.db.close();
263
+ this.db = null;
264
+ logger.info("文档数据库连接已关闭");
265
+ }
266
+ }
267
+ }
268
+
269
+ // 导出单例实例
270
+ export let docManager: DocManager;
271
+
272
+ /**
273
+ * 初始化文档管理器
274
+ * @param config 配置
275
+ */
276
+ export function initDocManager(config: DocDatabaseConfig): DocManager {
277
+ docManager = new DocManager(config);
278
+ return docManager;
279
+ }
@@ -0,0 +1,41 @@
1
+ import { Express } from "express";
2
+ import logger from "./Logger";
3
+
4
+ /**
5
+ * 启动 HTTP 服务
6
+ * @param app Express 实例
7
+ * @param port 监听端口
8
+ * @returns Promise<void>
9
+ * @throws 如果服务器启动失败
10
+ */
11
+ export async function startServer(app: Express, port: number): Promise<void> {
12
+ if (!app) {
13
+ throw new Error("Express 实例不能为空");
14
+ }
15
+
16
+ if (!port || typeof port !== "number" || port <= 0 || port > 65535) {
17
+ throw new Error(`无效的端口号: ${port}`);
18
+ }
19
+
20
+ return new Promise<void>((resolve, reject) => {
21
+ try {
22
+ const server = app.listen(port, () => {
23
+ resolve();
24
+ });
25
+
26
+ // 处理服务器错误
27
+ server.on("error", (error) => {
28
+ logger.error("服务器启动失败:", error);
29
+ reject(new Error(`服务器启动失败:${error instanceof Error ? error.message : String(error)}`));
30
+ });
31
+
32
+ // 处理服务器关闭
33
+ server.on("close", () => {
34
+ logger.info("服务器已关闭");
35
+ });
36
+ } catch (error) {
37
+ logger.error("服务器启动异常:", error);
38
+ reject(new Error(`服务器启动异常:${error instanceof Error ? error.message : String(error)}`));
39
+ }
40
+ });
41
+ }