feat(mongodb): bson dumps and database backup/restore

This commit is contained in:
Hand Sonic authored and GitHub committed 2026-09-16 22:46:48 +08:00
1 parent ecc1135de3
commit f5673ca0a1
54 files changed
+6355 -106

No files matched your search

@@ -158,6 +158,39 @@ describe("useSidebarTreeExportRuntime", () => {
expect(toastMock).toHaveBeenCalledWith("grid.exported");
});
it.each(["bson", "bsonGzip"] as const)("exports an official-compatible %s collection dump", async (mode) => {
apiMock.exportMongodbQuery.mockImplementation(async (_request, onProgress) => {
onProgress({ exportId: "export-1", status: "done", documentsRead: 2, bytesWritten: 64, elapsedMs: 4 });
return { exportId: "export-1", documentsExported: 2, filePath: "orders.bson.gz", elapsedMs: 4 };
});
const activeNode = shallowRef({ id: "col-1", type: "mongo-collection", label: "orders", connectionId: "conn-1", database: "shop", children: [] } as TreeNode);
const connectionStore = {
ensureConnected: vi.fn(),
getConfig: vi.fn(() => ({ db_type: "mongodb" })),
treeNodes: [],
selectedTreeNodeIds: [],
};
const runtime = useSidebarTreeExportRuntime({
activeNode,
connectionStore: connectionStore as never,
settingsStore: exportSettings() as never,
acceptedSelectionIds: () => null,
});
await runtime.exportMongoCollection(mode);
expect(apiMock.exportMongodbQuery).toHaveBeenCalledWith(
expect.objectContaining({
database: "shop",
collection: "orders",
format: "bson",
gzip: mode === "bsonGzip",
filePath: mode === "bsonGzip" ? "orders.bson.gz" : "orders.bson",
}),
expect.any(Function),
);
});
it("loads and joins every selected DDL in tree order", async () => {
apiMock.getTableDdl.mockResolvedValueOnce("CREATE TABLE one (id INT)").mockResolvedValueOnce("CREATE VIEW two AS SELECT 1;");
const first = { id: "table-1", type: "table", label: "one", connectionId: "conn-1", database: "db", schema: "main" } as TreeNode;
@@ -11,6 +11,7 @@ function setup(node: Partial<TreeNode>, options: { treeNodes?: TreeNode[]; selec
diagramSource: null as unknown,
databaseExportSource: null as unknown,
mongoImportSource: undefined as unknown,
mongoDatabaseDumpSource: null as unknown,
schemaDiffSource: null as unknown,
treeNodes: options.treeNodes ?? [],
selectedTreeNodeIds: options.selectedTreeNodeIds ?? [],
@@ -129,6 +130,29 @@ describe("useSidebarTreeToolRuntime mongo import", () => {
});
});
describe("useSidebarTreeToolRuntime MongoDB dump and restore", () => {
it.each(["dump", "restore"] as const)("opens %s for the selected MongoDB database", (mode) => {
const { connectionStore, runtime } = setup({ type: "mongo-db", connectionId: "conn-1", database: "shop" });
runtime.openMongoDatabaseDump(mode);
expect(connectionStore.mongoDatabaseDumpSource).toEqual({ connectionId: "conn-1", database: "shop", mode });
});
it.each<Partial<TreeNode>>([
{ type: "table", connectionId: "conn-1", database: "shop" },
{ type: "mongo-collection", connectionId: "conn-1", database: "shop" },
{ type: "mongo-db", database: "shop" },
{ type: "mongo-db", connectionId: "conn-1" },
])("does not open a database restore from an invalid context: %j", (node) => {
const { connectionStore, runtime } = setup(node);
runtime.openMongoDatabaseDump("restore");
expect(connectionStore.mongoDatabaseDumpSource).toBeNull();
});
});
describe("useSidebarTreeToolRuntime openSchemaDiffForRoutine", () => {
it("prefills schema diff with a signature-aware routine key", () => {
const { connectionStore, runtime } = setup({
@@ -15,6 +15,10 @@ const showDiagramDialog = ref(false);
const showDocsDialog = ref(false);
const showTableImportDialog = ref(false);
const showMongoImportDialog = ref(false);
const showMongoDatabaseDumpDialog = ref(false);
const mongoDatabaseDumpPrefillConnectionId = ref("");
const mongoDatabaseDumpPrefillDatabase = ref("");
const mongoDatabaseDumpMode = ref<"dump" | "restore">("dump");
const showTableDataGenerateDialog = ref(false);
const showFieldLineageDialog = ref(false);
const showDatabaseSearchDialog = ref(false);
@@ -263,6 +267,18 @@ export function useDialogSources() {
},
);
watch(
() => connectionStore.mongoDatabaseDumpSource,
(source) => {
if (!source) return;
mongoDatabaseDumpPrefillConnectionId.value = source.connectionId;
mongoDatabaseDumpPrefillDatabase.value = source.database;
mongoDatabaseDumpMode.value = source.mode;
showMongoDatabaseDumpDialog.value = true;
connectionStore.mongoDatabaseDumpSource = null;
},
);
watch(
() => connectionStore.tableDataGenerateSource,
(v) => {
@@ -519,6 +535,10 @@ export function useDialogSources() {
showDocsDialog,
showTableImportDialog,
showMongoImportDialog,
showMongoDatabaseDumpDialog,
mongoDatabaseDumpPrefillConnectionId,
mongoDatabaseDumpPrefillDatabase,
mongoDatabaseDumpMode,
showTableDataGenerateDialog,
showFieldLineageDialog,
showDatabaseSearchDialog,
@@ -274,6 +274,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
if (format === "csv") return "CSV";
if (format === "json") return "JSON";
if (format === "ndjson") return "NDJSON";
if (format === "bson" || format === "bson.gz") return "MongoDB BSON dump";
if (format === "xlsx") return "Excel";
return "SQL";
}
@@ -287,7 +288,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
const { save } = await import("@tauri-apps/plugin-dialog");
const path = await save({
defaultPath: fileName,
filters: [{ name: exportFilterName(format), extensions: [format] }],
filters: [{ name: exportFilterName(format), extensions: [format === "bson.gz" ? "gz" : format] }],
});
return path ? String(path) : null;
}
@@ -472,10 +473,12 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
return pickTableExportDirectory();
}
async function exportMongoCollection(format: "csv" | "ndjson") {
async function exportMongoCollection(outputFormat: "csv" | "ndjson" | "bson" | "bsonGzip") {
const node = activeNode.value;
if (node.type !== "mongo-collection" || !node.connectionId || !node.database) return;
const outputPath = await resolveExportOutputPath(node.label, format);
const format: api.MongoExportFormat = outputFormat === "bsonGzip" ? "bson" : outputFormat;
const fileExtension = outputFormat === "bsonGzip" ? "bson.gz" : outputFormat;
const outputPath = await resolveExportOutputPath(node.label, fileExtension);
if (!outputPath) return;
let task: ExportTask | null = null;
try {
@@ -490,6 +493,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
collection: node.label,
format,
includeHeader: true,
gzip: outputFormat === "bsonGzip",
filePath: outputPath,
},
(progress) => {
@@ -149,6 +149,12 @@ export function useSidebarTreeToolRuntime(options: SidebarTreeToolRuntimeOptions
};
}
function openMongoDatabaseDump(mode: "dump" | "restore") {
const node = activeNode.value;
if (node.type !== "mongo-db" || !node.connectionId || !node.database) return;
connectionStore.mongoDatabaseDumpSource = { connectionId: node.connectionId, database: node.database, mode };
}
function openStructureEditor() {
const node = activeNode.value;
if (!node.connectionId || !node.database) return;
@@ -191,6 +197,7 @@ export function useSidebarTreeToolRuntime(options: SidebarTreeToolRuntimeOptions
openDocs,
openFieldLineage,
openMongoImport,
openMongoDatabaseDump,
openScheduledBackups,
openSchemaDiff,
openSchemaDiffForRoutine,