mock数据分组管理

This commit is contained in:
dongzp 2026-06-04 15:24:11 +08:00
parent b1e4d2d7e7
commit 31095c967e
5 changed files with 521 additions and 25 deletions

View File

@ -123,16 +123,27 @@
</el-card> </el-card>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="Mock 数据配置" name="mocks"> <el-tab-pane label="数据配置" name="mocks">
<el-card class="section-card"> <el-card class="section-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<strong>Mock 数据配置</strong> <strong>数据配置(mock)</strong>
<div style="display:flex;gap:8px;align-items:center;"> <div style="display:flex;gap:8px;align-items:center;">
<el-select size="small" v-model="mockGroupFilter" clearable placeholder="按分组筛选" style="width:160px;">
<el-option label="全部" :value="null"></el-option>
<el-option label="未分组" :value="-1"></el-option>
<el-option v-for="g in mockGroups" :key="g.id" :label="g.name" :value="g.id"></el-option>
</el-select>
<el-input size="small" v-model="mockFileSearch" placeholder="搜索别名或路径" clearable style="width:220px;" prefix-icon="el-icon-search"></el-input> <el-input size="small" v-model="mockFileSearch" placeholder="搜索别名或路径" clearable style="width:220px;" prefix-icon="el-icon-search"></el-input>
<el-button size="mini" type="primary" @click="openMockFileDialogForCreate">新增 Mock 文件</el-button> <el-button size="mini" type="primary" @click="openMockFileDialogForCreate">新增 Mock 文件</el-button>
<el-button size="mini" @click="openMockGroupDialog">分组管理</el-button>
</div> </div>
</div> </div>
<el-table :data="pagedMockFiles" border> <el-table :data="pagedMockFiles" border>
<el-table-column label="分组" width="140">
<template slot-scope="scope">
<span>{{ getGroupName(scope.row.groupId) }}</span>
</template>
</el-table-column>
<el-table-column label="别名" min-width="180"> <el-table-column label="别名" min-width="180">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ scope.row.alias || "-" }}</span> <span>{{ scope.row.alias || "-" }}</span>
@ -313,19 +324,37 @@
></el-input> ></el-input>
</el-form-item> </el-form-item>
<el-form-item label="Mock 文件"> <el-form-item label="Mock 文件">
<el-select <div style="display:flex;gap:8px;">
v-model="routeDialog.form.filePath" <el-select
filterable v-model="routeDialog.form.groupFilter"
placeholder="请选择 mock 文件" clearable
style="width: 100%;" placeholder="选择分组"
> style="width: 180px;"
<el-option @change="onRouteDialogGroupChange"
v-for="item in mockFiles" >
:key="item.filePath" <el-option label="全部" :value="null"></el-option>
:label="item.alias ? item.alias + ' (' + item.filePath + ')' : item.filePath" <el-option label="无分组" :value="-1"></el-option>
:value="item.filePath" <el-option
></el-option> v-for="g in mockGroups"
</el-select> :key="g.id"
:label="g.name"
:value="g.id"
></el-option>
</el-select>
<el-select
v-model="routeDialog.form.filePath"
filterable
placeholder="请选择 mock 文件"
style="flex: 1;"
>
<el-option
v-for="item in routeDialogMockFiles"
:key="item.filePath"
:label="item.alias ? item.alias + ' (' + item.filePath + ')' : item.filePath"
:value="item.filePath"
></el-option>
</el-select>
</div>
</el-form-item> </el-form-item>
<el-form-item label="返回状态码"> <el-form-item label="返回状态码">
<el-select <el-select
@ -369,6 +398,12 @@
placeholder="可选:用于展示,支持任意文本" placeholder="可选:用于展示,支持任意文本"
></el-input> ></el-input>
</el-form-item> </el-form-item>
<el-form-item label="分组">
<el-select v-model="mockFileDialog.form.groupId" clearable placeholder="选择分组" style="width: 100%;">
<el-option label="无分组" :value="null"></el-option>
<el-option v-for="g in mockGroups" :key="g.id" :label="g.name" :value="g.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="Mock 文件路径"> <el-form-item label="Mock 文件路径">
<el-input <el-input
v-model="mockFileDialog.form.fileName" v-model="mockFileDialog.form.fileName"
@ -435,6 +470,67 @@
<el-button type="primary" @click="submitBatchImport">导入</el-button> <el-button type="primary" @click="submitBatchImport">导入</el-button>
</span> </span>
</el-dialog> </el-dialog>
<el-dialog
title="分组管理"
:visible.sync="mockGroupDialog.visible"
width="600px"
>
<div style="margin-bottom:12px;">
<el-button size="mini" type="primary" @click="addMockGroup">新增分组</el-button>
</div>
<el-table :data="mockGroups" border size="small" style="width: 100%;">
<el-table-column prop="id" label="ID" width="60"></el-table-column>
<el-table-column label="分组名称" min-width="200">
<template slot-scope="scope">
<el-input
v-if="mockGroupDialog.editingId === scope.row.id"
v-model="mockGroupDialog.form.name"
size="small"
@blur="saveMockGroupEdit(scope.row)"
@keyup.enter.native="saveMockGroupEdit(scope.row)"
></el-input>
<span v-else>{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column label="文件数" width="80" align="center">
<template slot-scope="scope">
<span>{{ getGroupFileCount(scope.row.id) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="160">
<template slot-scope="scope">
<el-button size="mini" type="primary" plain @click="startEditMockGroup(scope.row)">修改</el-button>
<el-button size="mini" type="danger" @click="confirmDeleteGroup(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
<el-dialog
title="删除分组确认"
:visible.sync="deleteGroupDialog.visible"
width="500px"
>
<div style="margin-bottom:16px;">
<p>分组「{{ deleteGroupDialog.groupName }}」下有 <strong>{{ deleteGroupDialog.fileCount }}</strong> 个 mock 文件。</p>
<p>请选择对这些文件的处理方式:</p>
</div>
<el-radio-group v-model="deleteGroupDialog.action" style="display:flex;flex-direction:column;gap:12px;">
<el-radio label="reassign">转移到其他分组</el-radio>
<el-radio label="unassign">取消分组(设为未分组)</el-radio>
<el-radio label="delete">删除全部文件</el-radio>
</el-radio-group>
<div v-if="deleteGroupDialog.action === 'reassign'" style="margin-top:12px;">
<el-select v-model="deleteGroupDialog.targetGroupId" placeholder="选择目标分组" style="width:100%;">
<el-option v-for="g in mockGroups.filter(function(g){return g.id !== deleteGroupDialog.groupId})" :key="g.id" :label="g.name" :value="g.id"></el-option>
</el-select>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="deleteGroupDialog.visible = false">取消</el-button>
<el-button type="danger" @click="submitDeleteGroup">确认删除</el-button>
</span>
</el-dialog>
</div> </div>
</div> </div>
@ -459,6 +555,22 @@
input: "", input: "",
}, },
mockFiles: [], mockFiles: [],
mockGroups: [],
mockGroupFilter: null,
mockGroupDialog: {
visible: false,
mode: "create",
editingId: null,
form: { name: "" },
},
deleteGroupDialog: {
visible: false,
groupId: null,
groupName: "",
fileCount: 0,
action: "",
targetGroupId: null,
},
contentTypeOptions: [ contentTypeOptions: [
"application/json", "application/json",
"application/x-www-form-urlencoded", "application/x-www-form-urlencoded",
@ -515,6 +627,7 @@
filePath: "", filePath: "",
statusCode: 200, statusCode: 200,
enabled: true, enabled: true,
groupFilter: null,
}, },
}, },
mockFileDialog: { mockFileDialog: {
@ -531,11 +644,13 @@
watch: { watch: {
routeSearch: function () { this.routePagination.currentPage = 1; }, routeSearch: function () { this.routePagination.currentPage = 1; },
mockFileSearch: function () { this.mockFilePagination.currentPage = 1; }, mockFileSearch: function () { this.mockFilePagination.currentPage = 1; },
mockGroupFilter: function () { this.mockFilePagination.currentPage = 1; },
apiSearch: function () { this.apiPagination.currentPage = 1; }, apiSearch: function () { this.apiPagination.currentPage = 1; },
}, },
created: async function () { created: async function () {
await this.loadApiList(); await this.loadApiList();
await this.loadMockFiles(); await this.loadMockFiles();
await this.loadMockGroups();
await this.loadConfig(); await this.loadConfig();
}, },
methods: { methods: {
@ -648,6 +763,7 @@
filePath: "", filePath: "",
statusCode: 200, statusCode: 200,
enabled: true, enabled: true,
groupFilter: null,
}; };
}, },
isApiListLocked: function () { isApiListLocked: function () {
@ -663,6 +779,10 @@
this.routeDialog.form.route = selected.route; this.routeDialog.form.route = selected.route;
this.routeDialog.form.apiName = selected.name; this.routeDialog.form.apiName = selected.name;
}, },
onRouteDialogGroupChange: function () {
// 切换分组时清空已选文件
this.routeDialog.form.filePath = "";
},
openRouteDialogForCreate: function () { openRouteDialogForCreate: function () {
this.routeDialog.mode = "create"; this.routeDialog.mode = "create";
this.routeDialog.editingIndex = -1; this.routeDialog.editingIndex = -1;
@ -678,6 +798,8 @@
this.routeDialog.mode = "edit"; this.routeDialog.mode = "edit";
this.routeDialog.editingIndex = actualIndex; this.routeDialog.editingIndex = actualIndex;
var matched = this.findApiByRoute(item.route || ""); var matched = this.findApiByRoute(item.route || "");
var mockFile = this.mockFiles.find(function (f) { return f.filePath === item.filePath; });
var groupFilter = mockFile && mockFile.groupId ? mockFile.groupId : null;
this.routeDialog.form = { this.routeDialog.form = {
apiName: matched ? matched.name : item.apiName || "", apiName: matched ? matched.name : item.apiName || "",
selectedApiRoute: matched ? matched.route : "", selectedApiRoute: matched ? matched.route : "",
@ -686,6 +808,7 @@
filePath: item.filePath || "mock/", filePath: item.filePath || "mock/",
statusCode: this.normalizeStatusCode(item.statusCode), statusCode: this.normalizeStatusCode(item.statusCode),
enabled: item.enabled !== false, enabled: item.enabled !== false,
groupFilter: groupFilter,
}; };
this.routeDialog.visible = true; this.routeDialog.visible = true;
}, },
@ -694,6 +817,7 @@
fileName: "", fileName: "",
alias: "", alias: "",
content: "", content: "",
groupId: null,
}; };
}, },
openMockFileDialogForCreate: function () { openMockFileDialogForCreate: function () {
@ -709,6 +833,7 @@
fileName: name, fileName: name,
alias: String(item.alias || ""), alias: String(item.alias || ""),
content: String(item.content || ""), content: String(item.content || ""),
groupId: item.groupId || null,
}; };
this.mockFileDialog.visible = true; this.mockFileDialog.visible = true;
}, },
@ -745,6 +870,7 @@
filePath: filePath, filePath: filePath,
alias: this.mockFileDialog.form.alias, alias: this.mockFileDialog.form.alias,
content: this.mockFileDialog.form.content, content: this.mockFileDialog.form.content,
groupId: this.mockFileDialog.form.groupId,
}), }),
}); });
var data = await resp.json(); var data = await resp.json();
@ -1010,6 +1136,148 @@
this.batchImportDialog.visible = false; this.batchImportDialog.visible = false;
await this.loadApiList(); await this.loadApiList();
}, },
getGroupName: function (groupId) {
if (!groupId) return "未分组";
var group = this.mockGroups.find(function (g) { return g.id === groupId; });
return group ? group.name : "未分组";
},
getGroupFileCount: function (groupId) {
return this.mockFiles.filter(function (f) { return f.groupId === groupId; }).length;
},
loadMockGroups: async function () {
try {
var resp = await fetch("/__mock-groups");
var data = await resp.json();
if (!resp.ok || data.success === false) {
throw new Error(data.error || "加载分组失败");
}
this.mockGroups = Array.isArray(data.list) ? data.list : [];
} catch (err) {
this.mockGroups = [];
this.$message.error("加载分组失败: " + err.message);
}
},
openMockGroupDialog: function () {
this.mockGroupDialog.editingId = null;
this.mockGroupDialog.form.name = "";
this.mockGroupDialog.visible = true;
},
addMockGroup: async function () {
var self = this;
this.$prompt("请输入分组名称", "新增分组", {
confirmButtonText: "确定",
cancelButtonText: "取消",
inputPattern: /\S+/,
inputErrorMessage: "分组名称不能为空",
}).then(async function (res) {
var name = (res.value || "").trim();
try {
var resp = await fetch("/__mock-groups", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: name }),
});
var data = await resp.json();
if (!resp.ok || data.success === false) {
throw new Error(data.error || "新增分组失败");
}
self.$message.success("分组已创建");
await self.loadMockGroups();
} catch (err) {
self.$message.error("新增分组失败: " + err.message);
}
}).catch(function () {});
},
startEditMockGroup: function (group) {
this.mockGroupDialog.editingId = group.id;
this.mockGroupDialog.form.name = group.name;
},
saveMockGroupEdit: async function (group) {
var newName = (this.mockGroupDialog.form.name || "").trim();
this.mockGroupDialog.editingId = null;
if (!newName || newName === group.name) return;
try {
var resp = await fetch("/__mock-groups", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: group.id, name: newName }),
});
var data = await resp.json();
if (!resp.ok || data.success === false) {
throw new Error(data.error || "修改分组失败");
}
this.$message.success("分组已修改");
await this.loadMockGroups();
} catch (err) {
this.$message.error("修改分组失败: " + err.message);
}
},
confirmDeleteGroup: async function (group) {
var fileCount = this.getGroupFileCount(group.id);
if (fileCount === 0) {
// 无文件,直接删除
try {
var resp = await fetch("/__mock-groups", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: group.id }),
});
var data = await resp.json();
if (!resp.ok || data.success === false) {
throw new Error(data.error || "删除分组失败");
}
this.$message.success("分组已删除");
await this.loadMockGroups();
} catch (err) {
this.$message.error("删除分组失败: " + err.message);
}
return;
}
// 有文件,弹出确认对话框
this.deleteGroupDialog.groupId = group.id;
this.deleteGroupDialog.groupName = group.name;
this.deleteGroupDialog.fileCount = fileCount;
this.deleteGroupDialog.action = "";
this.deleteGroupDialog.targetGroupId = null;
this.deleteGroupDialog.visible = true;
},
submitDeleteGroup: async function () {
var action = this.deleteGroupDialog.action;
if (!action) {
this.$message.error("请选择处理方式");
return;
}
if (action === "reassign" && !this.deleteGroupDialog.targetGroupId) {
this.$message.error("请选择目标分组");
return;
}
var payload = {
id: this.deleteGroupDialog.groupId,
action: action === "unassign" ? "reassign" : action,
};
if (action === "reassign") {
payload.targetGroupId = this.deleteGroupDialog.targetGroupId;
} else if (action === "unassign") {
payload.targetGroupId = null;
}
try {
var resp = await fetch("/__mock-groups", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
var data = await resp.json();
if (!resp.ok || data.success === false) {
throw new Error(data.error || "删除分组失败");
}
this.$message.success("分组已删除");
this.deleteGroupDialog.visible = false;
await this.loadMockGroups();
await this.loadMockFiles();
} catch (err) {
this.$message.error("删除分组失败: " + err.message);
}
},
}, },
computed: { computed: {
filteredRoutes: function () { filteredRoutes: function () {
@ -1030,13 +1298,24 @@
}, },
filteredMockFiles: function () { filteredMockFiles: function () {
var q = (this.mockFileSearch || "").toLowerCase().trim(); var q = (this.mockFileSearch || "").toLowerCase().trim();
if (!q) return this.mockFiles; var groupFilter = this.mockGroupFilter;
return this.mockFiles.filter(function (item) { var list = this.mockFiles;
return ( if (groupFilter !== null && groupFilter !== undefined) {
(item.filePath || "").toLowerCase().indexOf(q) !== -1 || if (groupFilter === -1) {
(item.alias || "").toLowerCase().indexOf(q) !== -1 list = list.filter(function (item) { return !item.groupId; });
); } else {
}); list = list.filter(function (item) { return item.groupId === groupFilter; });
}
}
if (q) {
list = list.filter(function (item) {
return (
(item.filePath || "").toLowerCase().indexOf(q) !== -1 ||
(item.alias || "").toLowerCase().indexOf(q) !== -1
);
});
}
return list;
}, },
filteredApiList: function () { filteredApiList: function () {
var q = (this.apiSearch || "").toLowerCase().trim(); var q = (this.apiSearch || "").toLowerCase().trim();
@ -1057,6 +1336,16 @@
pagedApiList: function () { pagedApiList: function () {
return this.getPagedData(this.filteredApiList, this.apiPagination); return this.getPagedData(this.filteredApiList, this.apiPagination);
}, },
routeDialogMockFiles: function () {
var groupFilter = this.routeDialog.form.groupFilter;
if (groupFilter === null || groupFilter === undefined) {
return this.mockFiles;
}
if (groupFilter === -1) {
return this.mockFiles.filter(function (f) { return !f.groupId; });
}
return this.mockFiles.filter(function (f) { return f.groupId === groupFilter; });
},
}, },
}); });
</script> </script>

View File

@ -9,6 +9,13 @@ import {
insertApiItemToDb, insertApiItemToDb,
updateApiItemInDb, updateApiItemInDb,
deleteApiItemFromDb, deleteApiItemFromDb,
loadMockGroups,
insertMockGroup,
updateMockGroup,
deleteMockGroup,
countMockFilesByGroup,
updateMockFilesGroupId,
deleteMockFilesByGroup,
} from "./db"; } from "./db";
import { import {
loadMockFiles, loadMockFiles,
@ -393,9 +400,10 @@ async function handleMockFiles(
if (clientReq.method === "POST") { if (clientReq.method === "POST") {
const content = String(body.content || ""); const content = String(body.content || "");
const groupId = body.groupId != null ? Number(body.groupId) : null;
fs.mkdirSync(path.dirname(fullPath), { recursive: true }); fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content, "utf-8"); fs.writeFileSync(fullPath, content, "utf-8");
await upsertMockFilePathToDb(normalizedPath, alias); await upsertMockFilePathToDb(normalizedPath, alias, groupId);
clientRes.writeHead(200, { "Content-Type": "application/json" }); clientRes.writeHead(200, { "Content-Type": "application/json" });
clientRes.end( clientRes.end(
JSON.stringify({ JSON.stringify({
@ -432,6 +440,113 @@ async function handleMockFiles(
}); });
} }
async function handleMockGroups(
clientReq: http.IncomingMessage,
clientRes: http.ServerResponse,
): Promise<void> {
if (
clientReq.method !== "GET" &&
clientReq.method !== "POST" &&
clientReq.method !== "PUT" &&
clientReq.method !== "DELETE"
) {
clientRes.writeHead(405, {
"Content-Type": "application/json",
Allow: "GET, POST, PUT, DELETE",
});
clientRes.end(
JSON.stringify({
success: false,
error: "Method Not Allowed",
allow: ["GET, POST, PUT, DELETE"],
}),
);
return;
}
if (clientReq.method === "GET") {
const list = await loadMockGroups();
clientRes.writeHead(200, { "Content-Type": "application/json" });
clientRes.end(JSON.stringify({ success: true, list }));
return;
}
readBody(clientReq)
.then(async (bodyText) => {
const body = bodyText ? JSON.parse(bodyText) : {};
if (clientReq.method === "POST") {
const name = String(body.name || "").trim();
if (!name) {
throw new Error("分组名称不能为空");
}
const id = await insertMockGroup(name);
clientRes.writeHead(200, { "Content-Type": "application/json" });
clientRes.end(JSON.stringify({ success: true, id, name }));
return;
}
if (clientReq.method === "PUT") {
const id = Number(body.id);
const name = String(body.name || "").trim();
if (!id || !name) {
throw new Error("id 和 name 不能为空");
}
await updateMockGroup(id, name);
clientRes.writeHead(200, { "Content-Type": "application/json" });
clientRes.end(JSON.stringify({ success: true, id, name }));
return;
}
// DELETE
const id = Number(body.id);
if (!id) {
throw new Error("id 不能为空");
}
const fileCount = await countMockFilesByGroup(id);
const action = String(body.action || "").trim();
if (fileCount > 0 && action === "reassign") {
const targetGroupId = body.targetGroupId != null ? Number(body.targetGroupId) : null;
await updateMockFilesGroupId(id, targetGroupId);
} else if (fileCount > 0 && action === "delete") {
const deletedFiles = await deleteMockFilesByGroup(id);
// 删除磁盘上的文件
for (const file of deletedFiles) {
const fullPath = path.join(__dirname, "..", file.file_path);
if (fs.existsSync(fullPath)) {
fs.unlinkSync(fullPath);
}
}
} else if (fileCount > 0) {
// 有文件但未指定 action,返回需要确认的信息
clientRes.writeHead(200, { "Content-Type": "application/json" });
clientRes.end(
JSON.stringify({
success: false,
needConfirm: true,
fileCount,
message: `该分组下有 ${fileCount} 个 mock 文件,请指定操作`,
}),
);
return;
}
await deleteMockGroup(id);
clientRes.writeHead(200, { "Content-Type": "application/json" });
clientRes.end(JSON.stringify({ success: true, id }));
})
.catch((error) => {
clientRes.writeHead(400, { "Content-Type": "application/json" });
clientRes.end(
JSON.stringify({
success: false,
error: (error as Error).message,
}),
);
});
}
async function handleAdminPage( async function handleAdminPage(
clientReq: http.IncomingMessage, clientReq: http.IncomingMessage,
clientRes: http.ServerResponse, clientRes: http.ServerResponse,
@ -476,6 +591,9 @@ export async function dispatchAdmin(
case "/__mock-files": case "/__mock-files":
await handleMockFiles(clientReq, clientRes); await handleMockFiles(clientReq, clientRes);
return true; return true;
case "/__mock-groups":
await handleMockGroups(clientReq, clientRes);
return true;
case "/__admin": case "/__admin":
await handleAdminPage(clientReq, clientRes); await handleAdminPage(clientReq, clientRes);
return true; return true;

View File

@ -7,6 +7,7 @@ import type {
ApiListItem, ApiListItem,
AppConfig, AppConfig,
MockFileRow, MockFileRow,
MockGroupRow,
RouteApiNameMap, RouteApiNameMap,
RouteConfig, RouteConfig,
RouteEnabledMap, RouteEnabledMap,
@ -100,6 +101,12 @@ export async function initDatabase(): Promise<void> {
name TEXT NOT NULL name TEXT NOT NULL
) )
`); `);
await dbRun(`
CREATE TABLE IF NOT EXISTS mock_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
)
`);
await dbRun(` await dbRun(`
CREATE TABLE IF NOT EXISTS mock_files ( CREATE TABLE IF NOT EXISTS mock_files (
file_path TEXT PRIMARY KEY, file_path TEXT PRIMARY KEY,
@ -111,6 +118,11 @@ export async function initDatabase(): Promise<void> {
).catch(() => { ).catch(() => {
// ignore when column already exists // ignore when column already exists
}); });
await dbRun(
"ALTER TABLE mock_files ADD COLUMN group_id INTEGER",
).catch(() => {
// ignore when column already exists
});
await dbRun(` await dbRun(`
CREATE TABLE IF NOT EXISTS server_config ( CREATE TABLE IF NOT EXISTS server_config (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
@ -211,8 +223,16 @@ export async function deleteApiItemFromDb(route: string): Promise<void> {
export async function upsertMockFilePathToDb( export async function upsertMockFilePathToDb(
filePath: string, filePath: string,
alias?: string, alias?: string,
groupId?: number | null,
): Promise<void> { ): Promise<void> {
if (typeof alias === "string") { if (typeof alias === "string") {
if (typeof groupId !== "undefined") {
await dbRun(
"INSERT INTO mock_files(file_path, alias, group_id) VALUES(?, ?, ?) ON CONFLICT(file_path) DO UPDATE SET alias = excluded.alias, group_id = excluded.group_id",
[filePath, alias, groupId ?? null],
);
return;
}
await dbRun( await dbRun(
"INSERT INTO mock_files(file_path, alias) VALUES(?, ?) ON CONFLICT(file_path) DO UPDATE SET alias = excluded.alias", "INSERT INTO mock_files(file_path, alias) VALUES(?, ?) ON CONFLICT(file_path) DO UPDATE SET alias = excluded.alias",
[filePath, alias], [filePath, alias],
@ -230,14 +250,75 @@ export async function removeMockFilePathFromDb(filePath: string): Promise<void>
export async function loadMockFilePathsFromDb(): Promise<MockFileRow[]> { export async function loadMockFilePathsFromDb(): Promise<MockFileRow[]> {
const rows = await dbAll<MockFileRow>( const rows = await dbAll<MockFileRow>(
"SELECT file_path, alias FROM mock_files ORDER BY file_path ASC", "SELECT file_path, alias, group_id FROM mock_files ORDER BY file_path ASC",
); );
return rows.map((row) => ({ return rows.map((row) => ({
file_path: row.file_path, file_path: row.file_path,
alias: String(row.alias || ""), alias: String(row.alias || ""),
group_id: row.group_id ?? null,
})); }));
} }
// ── mock_groups CRUD ──
export async function loadMockGroups(): Promise<MockGroupRow[]> {
const rows = await dbAll<MockGroupRow>(
"SELECT id, name FROM mock_groups ORDER BY id ASC",
);
return rows.map((row) => ({ id: row.id, name: row.name }));
}
export async function insertMockGroup(name: string): Promise<number> {
await dbRun("INSERT INTO mock_groups(name) VALUES(?)", [name]);
const row = await dbAll<{ id: number }>(
"SELECT id FROM mock_groups WHERE name = ?",
[name],
);
return row[0]?.id ?? 0;
}
export async function updateMockGroup(
id: number,
name: string,
): Promise<void> {
await dbRun("UPDATE mock_groups SET name = ? WHERE id = ?", [name, id]);
}
export async function deleteMockGroup(id: number): Promise<void> {
await dbRun("DELETE FROM mock_groups WHERE id = ?", [id]);
}
export async function countMockFilesByGroup(
groupId: number,
): Promise<number> {
const rows = await dbAll<{ cnt: number }>(
"SELECT COUNT(*) AS cnt FROM mock_files WHERE group_id = ?",
[groupId],
);
return rows[0]?.cnt ?? 0;
}
export async function updateMockFilesGroupId(
oldGroupId: number,
newGroupId: number | null,
): Promise<void> {
await dbRun("UPDATE mock_files SET group_id = ? WHERE group_id = ?", [
newGroupId,
oldGroupId,
]);
}
export async function deleteMockFilesByGroup(
groupId: number,
): Promise<MockFileRow[]> {
const rows = await dbAll<MockFileRow>(
"SELECT file_path FROM mock_files WHERE group_id = ?",
[groupId],
);
await dbRun("DELETE FROM mock_files WHERE group_id = ?", [groupId]);
return rows.map((row) => ({ file_path: row.file_path, alias: "", group_id: null }));
}
const DEFAULT_SERVER_CONFIG: AppConfig = { const DEFAULT_SERVER_CONFIG: AppConfig = {
cacheConfig: true, cacheConfig: true,
reloadOnChange: true, reloadOnChange: true,

View File

@ -63,7 +63,7 @@ export async function loadMockFiles(): Promise<MockFileItem[]> {
for (const filePath of fsFiles) { for (const filePath of fsFiles) {
await upsertMockFilePathToDb(filePath); await upsertMockFilePathToDb(filePath);
} }
files = fsFiles.map((filePath) => ({ file_path: filePath, alias: "" })); files = fsFiles.map((filePath) => ({ file_path: filePath, alias: "", group_id: null }));
} }
const result: MockFileItem[] = []; const result: MockFileItem[] = [];
@ -75,6 +75,7 @@ export async function loadMockFiles(): Promise<MockFileItem[]> {
filePath, filePath,
alias: String(item.alias || ""), alias: String(item.alias || ""),
content: fs.readFileSync(fullPath, "utf-8"), content: fs.readFileSync(fullPath, "utf-8"),
groupId: item.group_id ?? null,
}); });
} }
return result; return result;

View File

@ -35,6 +35,7 @@ export interface MockFileItem {
filePath: string; filePath: string;
alias: string; alias: string;
content: string; content: string;
groupId: number | null;
} }
export interface RouteRow { export interface RouteRow {
@ -53,7 +54,13 @@ export interface RouteApiNameMap {
[route: string]: string; [route: string]: string;
} }
export interface MockGroupRow {
id: number;
name: string;
}
export interface MockFileRow { export interface MockFileRow {
file_path: string; file_path: string;
alias: string; alias: string;
group_id: number | null;
} }