@mindbase/express-common 1.0.0
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/bin/mindbase.ts +52 -0
- package/commands/precache.ts +54 -0
- package/core/app.ts +200 -0
- package/core/module/CreateModule.ts +38 -0
- package/core/module/FindPackageRoot.ts +58 -0
- package/core/module/GetModulePath.ts +58 -0
- package/core/state.ts +72 -0
- package/feature/cron/CronManager.ts +63 -0
- package/feature/scanner/FileScanner.ts +288 -0
- package/index.ts +10 -0
- package/ipipfree.ipdb +0 -0
- package/middleware/Cors.ts +17 -0
- package/middleware/IpParser.ts +81 -0
- package/middleware/UaParser.ts +50 -0
- package/package.json +41 -0
- package/routes/Doc.route.ts +118 -0
- package/tests/Cors.test.ts +34 -0
- package/tests/Dayjs.test.ts +24 -0
- package/tests/FileScanner.test.ts +85 -0
- package/tests/GetModulePath.test.ts +32 -0
- package/tests/IpParser.test.ts +72 -0
- package/tests/Logger.test.ts +68 -0
- package/tests/UaParser.test.ts +41 -0
- package/tsconfig.json +9 -0
- package/types/DocTypes.ts +111 -0
- package/types/index.ts +19 -0
- package/utils/ComponentRegistry.ts +34 -0
- package/utils/DatabaseMigration.ts +121 -0
- package/utils/Dayjs.ts +16 -0
- package/utils/DocManager.ts +274 -0
- package/utils/HttpServer.ts +41 -0
- package/utils/InitDatabase.ts +149 -0
- package/utils/InitErrorHandler.ts +71 -0
- package/utils/InitExpress.ts +35 -0
- package/utils/Logger.ts +206 -0
- package/utils/MiddlewareRegistry.ts +14 -0
- package/utils/ProjectInitializer.ts +283 -0
- package/utils/RouteParser.ts +408 -0
- package/utils/RouteRegistry.ts +66 -0
- package/utils/SchemaMigrate.ts +73 -0
- package/utils/SchemaSync.ts +47 -0
- package/utils/TSTypeParser.ts +455 -0
- package/utils/Validate.ts +25 -0
- package/utils/ZodSchemaParser.ts +420 -0
- package/vitest.config.ts +18 -0
- package/zod/Doc.schema.ts +9 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { glob } from "glob";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import logger from "../../utils/Logger";
|
|
5
|
+
|
|
6
|
+
const CACHE_VERSION = "2.0";
|
|
7
|
+
const CACHE_DIR = path.join(process.cwd(), "node_modules/.cache/mindbase");
|
|
8
|
+
const CACHE_FILE = path.join(CACHE_DIR, "startup-cache.json");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 缓存条目接口
|
|
12
|
+
*/
|
|
13
|
+
interface CacheEntry {
|
|
14
|
+
filePath: string;
|
|
15
|
+
mtime: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 缓存数据接口
|
|
20
|
+
*/
|
|
21
|
+
interface CacheData {
|
|
22
|
+
version: string;
|
|
23
|
+
entries: Map<string, CacheEntry>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 全局缓存数据
|
|
27
|
+
let cacheData: CacheData | null = null;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 扫描结果接口
|
|
31
|
+
*/
|
|
32
|
+
export interface ScanResult {
|
|
33
|
+
fileName: string;
|
|
34
|
+
defaultExport: any;
|
|
35
|
+
allExports?: any;
|
|
36
|
+
type: "route" | "middleware" | "schema";
|
|
37
|
+
filePath: string;
|
|
38
|
+
/** 缓存命中标志(文档已在数据库) */
|
|
39
|
+
cacheHit?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 扫描器状态接口
|
|
44
|
+
*/
|
|
45
|
+
interface ScannerState {
|
|
46
|
+
scanPaths: string[]; // 要扫描的目录数组
|
|
47
|
+
baseDir: string; // 基础目录(最后扫描)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 初始化扫描器状态
|
|
51
|
+
const state: ScannerState = {
|
|
52
|
+
scanPaths: [],
|
|
53
|
+
baseDir: "",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 设置基础目录
|
|
58
|
+
* @param baseDir 基础目录路径
|
|
59
|
+
*/
|
|
60
|
+
export function setBaseDir(baseDir: string): void {
|
|
61
|
+
state.baseDir = baseDir;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 添加扫描目录
|
|
66
|
+
* @param dirPath 要添加的目录绝对路径
|
|
67
|
+
* @throws 如果路径不是目录或不存在
|
|
68
|
+
*/
|
|
69
|
+
export function addScanPath(dirPath: string): void {
|
|
70
|
+
// 验证路径是否为绝对路径
|
|
71
|
+
if (!path.isAbsolute(dirPath)) {
|
|
72
|
+
throw new Error(`路径必须是绝对路径: ${dirPath}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 验证路径是否存在且为目录
|
|
76
|
+
if (!fs.existsSync(dirPath)) {
|
|
77
|
+
throw new Error(`目录不存在: ${dirPath}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!fs.statSync(dirPath).isDirectory()) {
|
|
81
|
+
throw new Error(`路径不是目录: ${dirPath}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 去重:检查路径是否已经存在
|
|
85
|
+
if (state.scanPaths.includes(dirPath)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 检查是否是基础目录的子目录,如果是则跳过
|
|
90
|
+
if (state.baseDir && dirPath.startsWith(state.baseDir)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 检查是否是其他已添加路径的子目录,如果是则跳过
|
|
95
|
+
for (const existingPath of state.scanPaths) {
|
|
96
|
+
if (dirPath.startsWith(existingPath)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 添加到扫描路径数组
|
|
102
|
+
state.scanPaths.push(dirPath);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 验证路径是否为有效目录
|
|
107
|
+
* @param dirPath 目录路径
|
|
108
|
+
* @returns 是否为有效目录
|
|
109
|
+
*/
|
|
110
|
+
function isValidDirectory(dirPath: string): boolean {
|
|
111
|
+
try {
|
|
112
|
+
return fs.statSync(dirPath).isDirectory();
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 从文件路径提取文件名(不含扩展名)
|
|
120
|
+
* @param filePath 文件路径
|
|
121
|
+
* @returns 文件名(不含扩展名)
|
|
122
|
+
*/
|
|
123
|
+
function extractFileName(filePath: string): string {
|
|
124
|
+
const fileName = path.basename(filePath);
|
|
125
|
+
// 移除 .route.ts, .middleware.ts 或 .schema.ts 扩展名
|
|
126
|
+
return fileName.replace(/\.(route|middleware|schema)\.ts$/, "");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 加载缓存数据
|
|
131
|
+
* @returns 缓存数据对象,加载失败返回 null
|
|
132
|
+
*/
|
|
133
|
+
async function loadCache(): Promise<void> {
|
|
134
|
+
try {
|
|
135
|
+
if (!fs.existsSync(CACHE_FILE)) {
|
|
136
|
+
cacheData = { version: CACHE_VERSION, entries: new Map() };
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const json = fs.readFileSync(CACHE_FILE, "utf-8");
|
|
141
|
+
const data = JSON.parse(json);
|
|
142
|
+
|
|
143
|
+
// 检查缓存版本
|
|
144
|
+
if (data.version !== CACHE_VERSION) {
|
|
145
|
+
logger.debug(`缓存版本不匹配 (${data.version} vs ${CACHE_VERSION}),将重新构建`);
|
|
146
|
+
cacheData = { version: CACHE_VERSION, entries: new Map() };
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 重建 Map
|
|
151
|
+
cacheData = {
|
|
152
|
+
version: data.version,
|
|
153
|
+
entries: new Map(Object.entries(data.entries).map(([k, v]: [string, any]) => [k, v])),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
logger.debug(`已加载缓存,包含 ${cacheData.entries.size} 个文件条目`);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
logger.warn(`加载缓存失败,将重新构建: ${error}`);
|
|
159
|
+
cacheData = { version: CACHE_VERSION, entries: new Map() };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 保存缓存数据
|
|
165
|
+
*/
|
|
166
|
+
async function saveCache(): Promise<void> {
|
|
167
|
+
try {
|
|
168
|
+
if (!cacheData) return;
|
|
169
|
+
|
|
170
|
+
// 确保目录存在
|
|
171
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
172
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 序列化 Map 为普通对象
|
|
176
|
+
const data = {
|
|
177
|
+
version: cacheData.version,
|
|
178
|
+
entries: Object.fromEntries(cacheData.entries),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
182
|
+
logger.debug(`缓存已保存,包含 ${cacheData.entries.size} 个文件条目`);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
logger.warn(`保存缓存失败: ${error}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 启动扫描目录
|
|
190
|
+
* @param onlyPaths 是否仅返回路径,不导入模块
|
|
191
|
+
* @returns 扫描结果数组
|
|
192
|
+
*/
|
|
193
|
+
export async function scan(onlyPaths: boolean = false): Promise<ScanResult[]> {
|
|
194
|
+
// 加载缓存
|
|
195
|
+
await loadCache();
|
|
196
|
+
|
|
197
|
+
const results: ScanResult[] = [];
|
|
198
|
+
const allScanPaths = [...state.scanPaths];
|
|
199
|
+
const processedFiles = new Set<string>();
|
|
200
|
+
|
|
201
|
+
// 如果设置了基础目录,将其添加到最后扫描
|
|
202
|
+
if (state.baseDir) {
|
|
203
|
+
allScanPaths.push(state.baseDir);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 循环扫描所有目录
|
|
207
|
+
for (const dirPath of allScanPaths) {
|
|
208
|
+
if (!isValidDirectory(dirPath)) {
|
|
209
|
+
logger.warn(`跳过无效目录: ${dirPath}`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 构建 glob 模式
|
|
214
|
+
const patterns = [
|
|
215
|
+
{ type: "middleware" as const, pattern: "**/*.middleware.ts" },
|
|
216
|
+
{ type: "route" as const, pattern: "**/*.route.ts" },
|
|
217
|
+
{ type: "schema" as const, pattern: "**/*.schema.ts" },
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
for (const { type, pattern } of patterns) {
|
|
221
|
+
const globPattern = path.join(dirPath, pattern).replace(/\\/g, "/");
|
|
222
|
+
try {
|
|
223
|
+
const files = await glob(globPattern, { absolute: true });
|
|
224
|
+
for (const file of files) {
|
|
225
|
+
// 去重:跳过已处理的文件
|
|
226
|
+
if (processedFiles.has(file)) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
processedFiles.add(file);
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const fileName = extractFileName(file);
|
|
233
|
+
|
|
234
|
+
if (onlyPaths) {
|
|
235
|
+
results.push({
|
|
236
|
+
fileName,
|
|
237
|
+
type,
|
|
238
|
+
filePath: file,
|
|
239
|
+
defaultExport: null,
|
|
240
|
+
allExports: null,
|
|
241
|
+
});
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 检查文件修改时间
|
|
246
|
+
const stats = await fs.promises.stat(file);
|
|
247
|
+
const mtime = stats.mtimeMs;
|
|
248
|
+
|
|
249
|
+
// 检查缓存命中
|
|
250
|
+
const cached = cacheData?.entries.get(file);
|
|
251
|
+
const cacheHit = cached && cached.mtime === mtime;
|
|
252
|
+
|
|
253
|
+
if (cacheHit) {
|
|
254
|
+
// 缓存命中,文档已在数据库
|
|
255
|
+
logger.debug(`缓存命中: ${file}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 导入模块(必须执行,获取 handler 用于 app.use)
|
|
259
|
+
const module = await import(file);
|
|
260
|
+
const scanResult: ScanResult = {
|
|
261
|
+
fileName,
|
|
262
|
+
defaultExport: module.default,
|
|
263
|
+
allExports: module,
|
|
264
|
+
type,
|
|
265
|
+
filePath: file,
|
|
266
|
+
cacheHit,
|
|
267
|
+
};
|
|
268
|
+
results.push(scanResult);
|
|
269
|
+
|
|
270
|
+
// 更新缓存(无论是否命中,都更新 mtime)
|
|
271
|
+
if (cacheData) {
|
|
272
|
+
cacheData.entries.set(file, { filePath: file, mtime });
|
|
273
|
+
}
|
|
274
|
+
} catch (error) {
|
|
275
|
+
logger.warn(`处理文件失败: ${file}`, error);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
logger.warn(`扫描目录失败: ${dirPath} (Pattern: ${pattern})`, error);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 保存缓存
|
|
285
|
+
await saveCache();
|
|
286
|
+
|
|
287
|
+
return results;
|
|
288
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createApp } from "./core/app";
|
|
2
|
+
export { addCron, stopCron, stopAllCron } from "./feature/cron/CronManager";
|
|
3
|
+
export { createModule } from "./core/module/CreateModule";
|
|
4
|
+
export { logger } from "./utils/Logger";
|
|
5
|
+
export { dayjs } from "./utils/Dayjs";
|
|
6
|
+
export { validate } from "./utils/Validate";
|
|
7
|
+
export { initDatabase } from "./utils/InitDatabase";
|
|
8
|
+
export { handleSchemaMigrate } from "./utils/SchemaMigrate";
|
|
9
|
+
|
|
10
|
+
export type * from "./types/Index";
|
package/ipipfree.ipdb
ADDED
|
Binary file
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from "express";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 跨域处理
|
|
5
|
+
* @description 为API请求添加处理跨域请求头,并处理Options 预请求
|
|
6
|
+
*/
|
|
7
|
+
export default function (req: Request, res: Response, next: NextFunction) {
|
|
8
|
+
res.header("Access-Control-Allow-Origin", "*");
|
|
9
|
+
res.header("Access-Control-Allow-Credentials", "true");
|
|
10
|
+
res.header("Access-Control-Allow-Methods", "POST,GET,OPTIONS");
|
|
11
|
+
res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");
|
|
12
|
+
if (req.method == "OPTIONS") {
|
|
13
|
+
res.sendStatus(200);
|
|
14
|
+
} else {
|
|
15
|
+
next();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from "express";
|
|
2
|
+
import ipdb from "ipip-ipdb";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import logger from "../utils/Logger";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* IP 解析结果数据结构
|
|
8
|
+
*/
|
|
9
|
+
export interface IpInfo {
|
|
10
|
+
address: string; // 原始 IP 地址
|
|
11
|
+
location: string; // 格式化后的地理位置 (国家 省份 城市)
|
|
12
|
+
country: string; // 国家
|
|
13
|
+
province: string; // 省份/直辖市
|
|
14
|
+
city: string; // 城市
|
|
15
|
+
isp: string; // 运营商
|
|
16
|
+
latitude: string; // 纬度
|
|
17
|
+
longitude: string; // 经度
|
|
18
|
+
isLocal: boolean; // 是否为本地/回环地址
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 预初始化数据库实例
|
|
22
|
+
const dbPath = join(process.cwd(), "ipipfree.ipdb");
|
|
23
|
+
let cityInstance: any;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
cityInstance = new ipdb.City(dbPath);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
logger.error(`[IpParser] Failed to load IP database at: ${dbPath}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* IP 解析中间件
|
|
33
|
+
* 纯净实现:仅解析 IP 信息并打平数据结构,不包含冗余的向后兼容字段。
|
|
34
|
+
*/
|
|
35
|
+
export default function (req: Request, res: Response, next: NextFunction) {
|
|
36
|
+
const ip = (req.headers["real-ip"] || req.headers["x-real-ip"] || req.ip) as string;
|
|
37
|
+
|
|
38
|
+
const info: IpInfo = {
|
|
39
|
+
address: ip,
|
|
40
|
+
location: "未知",
|
|
41
|
+
country: "",
|
|
42
|
+
province: "",
|
|
43
|
+
city: "",
|
|
44
|
+
isp: "",
|
|
45
|
+
latitude: "",
|
|
46
|
+
longitude: "",
|
|
47
|
+
isLocal: false,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// 处理本地回环地址
|
|
51
|
+
const localIdentifiers = ["127.0.0.1", "::1", "::ffff:127.0.0.1", "localhost"];
|
|
52
|
+
if (localIdentifiers.includes(ip)) {
|
|
53
|
+
info.location = "本机";
|
|
54
|
+
info.isLocal = true;
|
|
55
|
+
res.locals.ip = info;
|
|
56
|
+
return next();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 数据库查询
|
|
60
|
+
if (cityInstance) {
|
|
61
|
+
try {
|
|
62
|
+
const data = cityInstance.findMap(ip, "CN");
|
|
63
|
+
if (data) {
|
|
64
|
+
info.country = data.country_name || "";
|
|
65
|
+
info.province = data.region_name || "";
|
|
66
|
+
info.city = data.city_name || "";
|
|
67
|
+
info.isp = data.isp_domain || "";
|
|
68
|
+
info.latitude = data.latitude || "";
|
|
69
|
+
info.longitude = data.longitude || "";
|
|
70
|
+
|
|
71
|
+
const parts = [info.country, info.province, info.city].filter(Boolean);
|
|
72
|
+
info.location = parts.join(" ") || "未知位置";
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
// 查询失败保持默认 info
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
res.locals.ip = info;
|
|
80
|
+
next();
|
|
81
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from "express";
|
|
2
|
+
import { UAParser } from "ua-parser-js";
|
|
3
|
+
|
|
4
|
+
export interface UaInfo {
|
|
5
|
+
ua: string;
|
|
6
|
+
browser: {
|
|
7
|
+
name?: string;
|
|
8
|
+
version?: string;
|
|
9
|
+
major?: string;
|
|
10
|
+
};
|
|
11
|
+
engine: {
|
|
12
|
+
name?: string;
|
|
13
|
+
version?: string;
|
|
14
|
+
};
|
|
15
|
+
os: {
|
|
16
|
+
name?: string;
|
|
17
|
+
version?: string;
|
|
18
|
+
};
|
|
19
|
+
device: {
|
|
20
|
+
model?: string;
|
|
21
|
+
type?: string;
|
|
22
|
+
vendor?: string;
|
|
23
|
+
};
|
|
24
|
+
cpu: {
|
|
25
|
+
architecture?: string;
|
|
26
|
+
};
|
|
27
|
+
isMobile: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* UA 解析中间件
|
|
32
|
+
* 纯净实现:仅解析 User-Agent 信息并存入 res.locals.UA
|
|
33
|
+
*/
|
|
34
|
+
export default function (req: Request, res: Response, next: NextFunction) {
|
|
35
|
+
const parser = new UAParser(req.headers["user-agent"] as string);
|
|
36
|
+
const result = parser.getResult();
|
|
37
|
+
|
|
38
|
+
const ua: UaInfo = {
|
|
39
|
+
ua: result.ua,
|
|
40
|
+
browser: result.browser,
|
|
41
|
+
engine: result.engine,
|
|
42
|
+
os: result.os,
|
|
43
|
+
device: result.device,
|
|
44
|
+
cpu: result.cpu,
|
|
45
|
+
isMobile: result.device.type === "mobile",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
res.locals.UA = ua;
|
|
49
|
+
next();
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mindbase/express-common",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "./index.ts",
|
|
5
|
+
"bin": {
|
|
6
|
+
"mindbase": "./bin/mindbase.ts"
|
|
7
|
+
},
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20.0.0"
|
|
10
|
+
},
|
|
11
|
+
"volta": {
|
|
12
|
+
"node": "20.20.0"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"better-sqlite3": "^11.10.0",
|
|
16
|
+
"cli-table3": "^0.6.3",
|
|
17
|
+
"drizzle-orm": "^0.44.7",
|
|
18
|
+
"drizzle-zod": "^0.5.0",
|
|
19
|
+
"glob": "^10.0.0",
|
|
20
|
+
"prompts": "^2.4.2",
|
|
21
|
+
"radash": "12.1.1",
|
|
22
|
+
"ts-morph": "^27.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
26
|
+
"@types/prompts": "^2.4.9",
|
|
27
|
+
"@vitest/coverage-v8": "^2.1.0",
|
|
28
|
+
"@vitest/ui": "^2.1.0",
|
|
29
|
+
"typescript": "^5.1.3",
|
|
30
|
+
"vitest": "^2.1.0"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"test:watch": "vitest",
|
|
35
|
+
"test:ui": "vitest --ui",
|
|
36
|
+
"test:cov": "vitest run --coverage"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Router, Request, Response } from "express";
|
|
2
|
+
import { docManager } from "../utils/DocManager";
|
|
3
|
+
import { validate } from "../utils/Validate";
|
|
4
|
+
import { routeIdParamsSchema } from "../zod/Doc.schema";
|
|
5
|
+
import { ApiResponse, ModuleItem, RouteInfo } from "../types/DocTypes";
|
|
6
|
+
|
|
7
|
+
const router = Router();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 获取模块列表
|
|
11
|
+
* GET /api/doc/modules
|
|
12
|
+
* @returns {ApiResponse<ModuleItem[]>} 返回模块列表
|
|
13
|
+
*/
|
|
14
|
+
router.get("/modules", (req: Request, res: Response) => {
|
|
15
|
+
try {
|
|
16
|
+
const modules = docManager.getModules();
|
|
17
|
+
const response: ApiResponse = {
|
|
18
|
+
code: 200,
|
|
19
|
+
data: modules,
|
|
20
|
+
msg: "success",
|
|
21
|
+
};
|
|
22
|
+
res.json(response);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
const response: ApiResponse = {
|
|
25
|
+
code: 500,
|
|
26
|
+
data: null,
|
|
27
|
+
msg: "获取模块列表失败",
|
|
28
|
+
};
|
|
29
|
+
res.json(response);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 获取路由列表
|
|
35
|
+
* GET /api/doc/routes
|
|
36
|
+
* 查询参数: module (可选)
|
|
37
|
+
* @returns {ApiResponse<RouteInfo[]>} 返回路由列表
|
|
38
|
+
*/
|
|
39
|
+
router.get("/routes", (req: Request, res: Response) => {
|
|
40
|
+
try {
|
|
41
|
+
const module = req.query.module as string;
|
|
42
|
+
const routes = docManager.getRoutes(module);
|
|
43
|
+
const response: ApiResponse = {
|
|
44
|
+
code: 200,
|
|
45
|
+
data: routes,
|
|
46
|
+
msg: "success",
|
|
47
|
+
};
|
|
48
|
+
res.json(response);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
const response: ApiResponse = {
|
|
51
|
+
code: 500,
|
|
52
|
+
data: null,
|
|
53
|
+
msg: "获取路由列表失败",
|
|
54
|
+
};
|
|
55
|
+
res.json(response);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 获取路由详情
|
|
61
|
+
* GET /api/doc/routes/:id
|
|
62
|
+
* @returns {ApiResponse<RouteInfo>} 返回路由详情
|
|
63
|
+
*/
|
|
64
|
+
router.get("/routes/:id", validate(routeIdParamsSchema as any, "params"), (req: Request, res: Response) => {
|
|
65
|
+
try {
|
|
66
|
+
const { id } = req.params as any;
|
|
67
|
+
const route = docManager.getRouteById(id);
|
|
68
|
+
if (!route) {
|
|
69
|
+
const response: ApiResponse = {
|
|
70
|
+
code: 404,
|
|
71
|
+
data: null,
|
|
72
|
+
msg: "路由不存在",
|
|
73
|
+
};
|
|
74
|
+
return res.json(response);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const response: ApiResponse = {
|
|
78
|
+
code: 200,
|
|
79
|
+
data: route,
|
|
80
|
+
msg: "success",
|
|
81
|
+
};
|
|
82
|
+
res.json(response);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const response: ApiResponse = {
|
|
85
|
+
code: 500,
|
|
86
|
+
data: null,
|
|
87
|
+
msg: "获取路由详情失败",
|
|
88
|
+
};
|
|
89
|
+
res.json(response);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 手动同步文档数据
|
|
95
|
+
* POST /api/doc/sync
|
|
96
|
+
* @returns {ApiResponse<boolean>} 同步成功返回true
|
|
97
|
+
*/
|
|
98
|
+
router.post("/sync", (req: Request, res: Response) => {
|
|
99
|
+
try {
|
|
100
|
+
// 这里可以实现手动同步逻辑
|
|
101
|
+
// 例如:重新扫描所有路由文件并更新文档数据
|
|
102
|
+
const response: ApiResponse = {
|
|
103
|
+
code: 200,
|
|
104
|
+
data: true,
|
|
105
|
+
msg: "同步成功",
|
|
106
|
+
};
|
|
107
|
+
res.json(response);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const response: ApiResponse = {
|
|
110
|
+
code: 500,
|
|
111
|
+
data: null,
|
|
112
|
+
msg: "同步失败",
|
|
113
|
+
};
|
|
114
|
+
res.json(response);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export default router;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import cors from "../middleware/Cors";
|
|
3
|
+
import { Request, Response } from "express";
|
|
4
|
+
|
|
5
|
+
describe("Cors 中间件", () => {
|
|
6
|
+
it("应该设置 CORS 响应头", () => {
|
|
7
|
+
const req = { method: "GET" } as Request;
|
|
8
|
+
const res = {
|
|
9
|
+
header: vi.fn(),
|
|
10
|
+
sendStatus: vi.fn(),
|
|
11
|
+
} as unknown as Response;
|
|
12
|
+
const next = vi.fn();
|
|
13
|
+
|
|
14
|
+
cors(req, res, next);
|
|
15
|
+
|
|
16
|
+
expect(res.header).toHaveBeenCalledWith("Access-Control-Allow-Origin", "*");
|
|
17
|
+
expect(res.header).toHaveBeenCalledWith("Access-Control-Allow-Methods", "POST,GET,OPTIONS");
|
|
18
|
+
expect(next).toHaveBeenCalled();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("应该处理 OPTIONS 请求", () => {
|
|
22
|
+
const req = { method: "OPTIONS" } as Request;
|
|
23
|
+
const res = {
|
|
24
|
+
header: vi.fn(),
|
|
25
|
+
sendStatus: vi.fn(),
|
|
26
|
+
} as unknown as Response;
|
|
27
|
+
const next = vi.fn();
|
|
28
|
+
|
|
29
|
+
cors(req, res, next);
|
|
30
|
+
|
|
31
|
+
expect(res.sendStatus).toHaveBeenCalledWith(200);
|
|
32
|
+
expect(next).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import dayjs from "../utils/Dayjs";
|
|
3
|
+
|
|
4
|
+
describe("Dayjs Utility", () => {
|
|
5
|
+
it("should have zh-cn locale", () => {
|
|
6
|
+
expect(dayjs.locale()).toBe("zh-cn");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("应该具有 duration 插件", () => {
|
|
10
|
+
expect(dayjs.duration).toBeDefined();
|
|
11
|
+
const dur = dayjs.duration(1, "hour");
|
|
12
|
+
expect(dur.asMinutes()).toBe(60);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("应该具有 relativeTime 插件", () => {
|
|
16
|
+
expect(dayjs().fromNow).toBeDefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should have utc plugin", () => {
|
|
20
|
+
expect(dayjs.utc).toBeDefined();
|
|
21
|
+
const now = dayjs.utc();
|
|
22
|
+
expect(now.isUTC()).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
});
|