diff --git a/admin.html b/admin.html
index 0f11d9d..39560ad 100644
--- a/admin.html
+++ b/admin.html
@@ -137,6 +137,11 @@
新增 Mock 文件
+
+
+ {{ scope.row.alias || "-" }}
+
+
{{ scope.row.filePath }}
@@ -266,7 +271,7 @@
@@ -307,10 +312,16 @@
width="760px"
>
+
+
+
@@ -398,6 +409,7 @@
mode: "create",
form: {
filePath: "mock/",
+ alias: "",
content: "",
},
},
@@ -548,6 +560,7 @@
getDefaultMockFileForm: function () {
return {
filePath: "mock/",
+ alias: "",
content: "",
};
},
@@ -560,6 +573,7 @@
this.mockFileDialog.mode = "edit";
this.mockFileDialog.form = {
filePath: item.filePath || "mock/",
+ alias: String(item.alias || ""),
content: String(item.content || ""),
};
this.mockFileDialog.visible = true;
@@ -589,6 +603,7 @@
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
filePath: this.mockFileDialog.form.filePath,
+ alias: this.mockFileDialog.form.alias,
content: this.mockFileDialog.form.content,
}),
});
diff --git a/config.json b/config.json
index ebae148..66d5285 100644
--- a/config.json
+++ b/config.json
@@ -1,11 +1,11 @@
{
"routes": {
- "/api1/account/bind": "mock/basic-error.json",
+ "#/api1/account/bind": "mock/basic-error.json",
"/api2/admin/banner/list": "mock/banner.txt",
"/api2/home/ad/list": "mock/basic-error.json"
},
"routeStatuses": {
- "/api1/account/bind": 500,
+ "#/api1/account/bind": 500,
"/api2/admin/banner/list": 200,
"/api2/home/ad/list": 200
},
diff --git a/index.api.ts b/index.api.ts
index 797b661..3760793 100644
--- a/index.api.ts
+++ b/index.api.ts
@@ -49,6 +49,7 @@ interface ApiListItem {
interface MockFileItem {
filePath: string;
+ alias: string;
content: string;
}
@@ -60,6 +61,7 @@ interface RouteRow {
interface MockFileRow {
file_path: string;
+ alias: string;
}
// 存储当前的路由配置
@@ -186,9 +188,15 @@ async function initDatabase(): Promise {
`);
await dbRun(`
CREATE TABLE IF NOT EXISTS mock_files (
- file_path TEXT PRIMARY KEY
+ file_path TEXT PRIMARY KEY,
+ alias TEXT NOT NULL DEFAULT ''
)
`);
+ await dbRun("ALTER TABLE mock_files ADD COLUMN alias TEXT NOT NULL DEFAULT ''").catch(
+ () => {
+ // ignore when column already exists
+ },
+ );
// 清理不该出现在 mock 列表中的系统文件
await dbRun(
"DELETE FROM mock_files WHERE file_path = ? OR file_path LIKE ?",
@@ -246,19 +254,31 @@ async function loadApiListFromDb(): Promise {
return rows.map((row) => ({ route: row.route, name: row.name }));
}
-async function upsertMockFilePathToDb(filePath: string): Promise {
- await dbRun("INSERT OR IGNORE INTO mock_files(file_path) VALUES(?)", [filePath]);
+async function upsertMockFilePathToDb(filePath: string, alias?: string): Promise {
+ if (typeof alias === "string") {
+ await dbRun(
+ "INSERT INTO mock_files(file_path, alias) VALUES(?, ?) ON CONFLICT(file_path) DO UPDATE SET alias = excluded.alias",
+ [filePath, alias],
+ );
+ return;
+ }
+ await dbRun("INSERT OR IGNORE INTO mock_files(file_path, alias) VALUES(?, '')", [
+ filePath,
+ ]);
}
async function removeMockFilePathFromDb(filePath: string): Promise {
await dbRun("DELETE FROM mock_files WHERE file_path = ?", [filePath]);
}
-async function loadMockFilePathsFromDb(): Promise {
+async function loadMockFilePathsFromDb(): Promise {
const rows = await dbAll(
- "SELECT file_path FROM mock_files ORDER BY file_path ASC",
+ "SELECT file_path, alias FROM mock_files ORDER BY file_path ASC",
);
- return rows.map((row) => row.file_path);
+ return rows.map((row) => ({
+ file_path: row.file_path,
+ alias: String(row.alias || ""),
+ }));
}
// 加载配置文件
@@ -417,6 +437,12 @@ function normalizeMockFilePath(filePath: string): string {
if (path.isAbsolute(relative)) {
throw new Error("filePath must be a relative path");
}
+ const innerPath = relative.replace(/^mock\//, "");
+ if (!/^[A-Za-z0-9/.]+$/.test(innerPath)) {
+ throw new Error(
+ "filePath only allows English letters, numbers, '/', and '.'",
+ );
+ }
return relative;
}
@@ -452,8 +478,9 @@ function walkMockFiles(dir: string, baseDir: string, result: string[]): void {
async function loadMockFiles(): Promise {
let files = await loadMockFilePathsFromDb();
files = files.filter(
- (filePath) =>
- filePath !== "mock/api-list.json" && !filePath.endsWith(".sqlite3"),
+ (item) =>
+ item.file_path !== "mock/api-list.json" &&
+ !item.file_path.endsWith(".sqlite3"),
);
if (files.length === 0 && fs.existsSync(MOCK_DIR)) {
const fsFiles: string[] = [];
@@ -462,15 +489,17 @@ async function loadMockFiles(): Promise {
for (const filePath of fsFiles) {
await upsertMockFilePathToDb(filePath);
}
- files = fsFiles;
+ files = fsFiles.map((filePath) => ({ file_path: filePath, alias: "" }));
}
const result: MockFileItem[] = [];
- for (const filePath of files) {
+ for (const item of files) {
+ const filePath = item.file_path;
const fullPath = path.join(__dirname, filePath);
if (!fs.existsSync(fullPath)) continue;
result.push({
filePath,
+ alias: String(item.alias || ""),
content: fs.readFileSync(fullPath, "utf-8"),
});
}
@@ -878,6 +907,7 @@ const proxyServer = http.createServer(async (clientReq, clientRes) => {
.then(async (bodyText) => {
const body = bodyText ? JSON.parse(bodyText) : {};
const filePath = String(body.filePath || "").trim();
+ const alias = String(body.alias || "");
const fullPath = resolveMockFullPath(filePath);
const normalizedPath = normalizeMockFilePath(filePath);
@@ -885,12 +915,13 @@ const proxyServer = http.createServer(async (clientReq, clientRes) => {
const content = String(body.content || "");
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content, "utf-8");
- await upsertMockFilePathToDb(normalizedPath);
+ await upsertMockFilePathToDb(normalizedPath, alias);
clientRes.writeHead(200, { "Content-Type": "application/json" });
clientRes.end(
JSON.stringify({
success: true,
filePath: normalizedPath,
+ alias,
}),
);
return;