feat(mongodb): add Compass-style collection import/export

This commit is contained in:
onenewcode authored and GitHub committed 2026-09-12 21:59:29 +08:00
1 parent bf4874906c
commit 8ff56b4837
47 files changed
+5626 -52

No files matched your search

+19
View File
@@ -458,6 +458,12 @@ export const previewTableImportFile = forward("previewTableImportFile");
export const importTableFile = forward("importTableFile");
export const cancelTableImport = forward("cancelTableImport");
export const releaseTableImportSource = forward("releaseTableImportSource");
export const previewMongodbImportFile = forward("previewMongodbImportFile");
export const importMongodbFile = forward("importMongodbFile");
export const cancelMongodbImport = forward("cancelMongodbImport");
export const releaseMongodbImportSource = forward("releaseMongodbImportSource");
export const exportMongodbQuery = forward("exportMongodbQuery");
export const cancelMongodbExport = forward("cancelMongodbExport");
// Database Export
export const beginDatabaseBackupSnapshot = forward("beginDatabaseBackupSnapshot");
@@ -995,6 +1001,19 @@ export type {
TableImportRequest,
TableImportSummary,
TableImportProgress,
MongoImportFormat,
MongoImportTypeMode,
MongoImportIssue,
MongoImportParseOptions,
MongoImportPreviewRequest,
MongoImportPreview,
MongoImportRequest,
MongoImportProgress,
MongoImportSummary,
MongoExportFormat,
MongoExportRequest,
MongoExportProgress,
MongoExportSummary,
DatabaseExportRequest,
ExportProgress,
TableExportProgress,
+124
View File
@@ -132,6 +132,14 @@ import type {
TableImportRequest,
TableImportSummary,
TableImportProgress,
MongoImportPreviewRequest,
MongoImportPreview,
MongoImportRequest,
MongoImportProgress,
MongoImportSummary,
MongoExportRequest,
MongoExportProgress,
MongoExportSummary,
DatabaseBackupSnapshot,
DatabaseExportRequest,
ExportProgress,
@@ -2469,6 +2477,122 @@ export async function releaseTableImportSource(sourceRef: string): Promise<boole
return result.released;
}
export async function previewMongodbImportFile(fileOrPath: string | File | MongoImportPreviewRequest, options: Partial<MongoImportPreviewRequest> = {}): Promise<MongoImportPreview> {
if (typeof fileOrPath === "object" && !(fileOrPath instanceof File)) {
throw new Error("previewMongodbImportFile in web mode requires a File object for upload previews");
}
if (typeof fileOrPath === "string") {
if (!options.sourceRef) {
throw new Error("previewMongodbImportFile in web mode requires a File object for new uploads");
}
const res = await fetch(apiUrl("/api/mongo/import/preview-source"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sourceRef: options.sourceRef,
format: options.format,
parseOptions: options.parseOptions,
previewLimit: options.previewLimit,
}),
});
if (!res.ok) throw await backendResponseError(res);
return res.json();
}
const formData = new FormData();
formData.append("file", fileOrPath);
if (options.format) formData.append("format", options.format);
if (options.parseOptions) formData.append("parseOptions", JSON.stringify(options.parseOptions));
if (options.previewLimit != null) formData.append("previewLimit", String(options.previewLimit));
const res = await fetch(apiUrl("/api/mongo/import/preview"), {
method: "POST",
body: formData,
});
if (!res.ok) throw await backendResponseError(res);
return res.json();
}
export async function importMongodbFile(request: MongoImportRequest, onProgress: (progress: MongoImportProgress) => void): Promise<MongoImportSummary> {
const res = await fetch(apiUrl("/api/mongo/import/execute"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ request }),
});
if (!res.ok) throw await backendResponseError(res);
return new Promise((resolve, reject) => {
const es = new EventSource(apiUrl(`/api/mongo/import/progress/${request.importId}`));
es.onmessage = (e) => {
const progress: MongoImportProgress = JSON.parse(e.data);
onProgress(progress);
if (progress.status === "done") {
es.close();
resolve({
importId: progress.importId,
rowsInserted: progress.rowsInserted,
rowsFailed: progress.rowsFailed,
batchesCommitted: progress.batchesCommitted,
elapsedMs: progress.elapsedMs,
});
} else if (progress.status === "error" || progress.status === "cancelled") {
es.close();
reject(new Error(progress.errorMessage || "MongoDB import failed"));
}
};
es.onerror = () => {
es.close();
reject(new Error("MongoDB import SSE connection failed"));
};
});
}
export async function cancelMongodbImport(importId: string): Promise<boolean> {
return post("/api/mongo/import/cancel", { importId });
}
export async function releaseMongodbImportSource(sourceRef: string): Promise<boolean> {
const result = await post<{ released: boolean }>("/api/mongo/import/source/release", { sourceRef });
return result.released;
}
export async function exportMongodbQuery(request: MongoExportRequest, onProgress: (progress: MongoExportProgress) => void): Promise<MongoExportSummary> {
const res = await fetch(apiUrl("/api/mongo/export"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ request }),
});
if (!res.ok) throw await backendResponseError(res);
return new Promise((resolve, reject) => {
const es = new EventSource(apiUrl(`/api/mongo/export/progress/${request.exportId}`));
es.onmessage = (e) => {
const progress: MongoExportProgress = JSON.parse(e.data);
onProgress(progress);
if (progress.status === "done" || progress.status === "error" || progress.status === "cancelled") {
es.close();
if (progress.status === "done") {
const a = document.createElement("a");
a.href = apiUrl(`/api/mongo/export/download/${request.exportId}`);
a.click();
resolve({
exportId: progress.exportId,
documentsExported: progress.documentsRead,
filePath: request.filePath,
elapsedMs: progress.elapsedMs,
});
} else {
reject(new Error(progress.errorMessage || "MongoDB export failed"));
}
}
};
es.onerror = () => {
es.close();
reject(new Error("MongoDB export SSE connection failed"));
};
});
}
export async function cancelMongodbExport(exportId: string): Promise<boolean> {
return post("/api/mongo/export/cancel", { exportId });
}
// ---------------------------------------------------------------------------
// Database Export
// ---------------------------------------------------------------------------
+184
View File
@@ -4900,6 +4900,190 @@ export async function releaseTableImportSource(_sourceRef: string): Promise<bool
return false;
}
export type MongoImportFormat = "csv" | "json" | "ndjson";
export type MongoImportTypeMode = "string" | "auto" | "extendedJson";
export type MongoImportInferredType = "boolean" | "integer" | "decimal" | "date" | "object" | "array" | "string";
export type MongoImportStatus = "running" | "done" | "error" | "cancelled";
export type MongoImportPhase = "preparing" | "parsing" | "writing" | "done";
export type MongoExportFormat = "csv" | "ndjson";
export type MongoExportStatus = "running" | "done" | "error" | "cancelled";
export interface MongoImportIssue {
code: string;
message: string;
row?: number | null;
column?: string | null;
value?: string | null;
batch?: number | null;
retryable?: boolean;
}
export interface MongoImportParseOptions {
encoding?: TableImportTextEncoding | null;
delimiter?: string | null;
hasHeader?: boolean | null;
trim?: boolean | null;
emptyAsNull?: boolean | null;
typeMode?: MongoImportTypeMode | null;
recognizeObjectIdHex?: boolean | null;
skipErrorRows?: boolean | null;
}
export interface MongoImportPreviewRequest {
filePath: string;
sourceRef?: string | null;
format: MongoImportFormat;
parseOptions?: MongoImportParseOptions;
previewLimit?: number | null;
}
export interface MongoImportColumn {
name: string;
inferredType: MongoImportInferredType;
sampleValues?: unknown[];
}
export interface MongoImportPreview {
sourceRef?: string | null;
format: MongoImportFormat;
detectedEncoding?: TableImportTextEncoding | null;
fileName: string;
filePath: string;
sizeBytes: number;
columns: MongoImportColumn[];
rows: Record<string, unknown>[];
rowNumbers?: number[];
warnings: MongoImportIssue[];
errors: MongoImportIssue[];
estimatedRows?: number | null;
estimatedRowsExact: boolean;
}
export interface MongoImportRequest {
importId: string;
connectionId: string;
database: string;
collection: string;
filePath: string;
sourceRef?: string | null;
format: MongoImportFormat;
parseOptions?: MongoImportParseOptions;
batchSize: number;
executionId?: string | null;
}
export interface MongoImportProgress {
importId: string;
phase: MongoImportPhase;
status: MongoImportStatus;
rowsRead: number;
rowsInserted: number;
rowsFailed: number;
batchesCommitted: number;
totalRows?: number | null;
errorRows?: MongoImportIssue[];
errorMessage?: string | null;
elapsedMs: number;
}
export interface MongoImportSummary {
importId: string;
rowsInserted: number;
rowsFailed: number;
batchesCommitted: number;
elapsedMs: number;
}
export interface MongoExportRequest {
exportId: string;
connectionId: string;
database: string;
collection: string;
filter?: string | null;
sort?: string | null;
projection?: string | null;
collation?: string | null;
format: MongoExportFormat;
includeHeader?: boolean;
filePath: string;
executionId?: string | null;
}
export interface MongoExportProgress {
exportId: string;
status: MongoExportStatus;
documentsRead: number;
bytesWritten: number;
totalDocuments?: number | null;
errorMessage?: string | null;
elapsedMs: number;
}
export interface MongoExportSummary {
exportId: string;
documentsExported: number;
filePath: string;
elapsedMs: number;
}
export async function previewMongodbImportFile(filePathOrRequest: string | File | MongoImportPreviewRequest, options: Partial<MongoImportPreviewRequest> = {}): Promise<MongoImportPreview> {
if (typeof filePathOrRequest !== "string" && !("filePath" in filePathOrRequest)) {
throw new Error("previewMongodbImportFile in desktop mode requires a file path, not a File object");
}
const request: MongoImportPreviewRequest = typeof filePathOrRequest === "string" ? { format: options.format ?? "csv", ...options, filePath: filePathOrRequest } : filePathOrRequest;
return invoke("preview_mongodb_import_file", { request });
}
export async function importMongodbFile(request: MongoImportRequest, onProgress: (progress: MongoImportProgress) => void): Promise<MongoImportSummary> {
const unlisten: UnlistenFn = await listen<MongoImportProgress>("mongo-import-progress", (event) => {
if (event.payload.importId === request.importId) {
onProgress(event.payload);
if (event.payload.status === "done" || event.payload.status === "error" || event.payload.status === "cancelled") {
unlisten();
}
}
});
try {
const summary = await invoke<MongoImportSummary>("import_mongodb_file", { request });
unlisten();
return summary;
} catch (e) {
unlisten();
throw e instanceof BackendErrorException ? e : new BackendErrorException(e);
}
}
export async function cancelMongodbImport(importId: string): Promise<boolean> {
return invoke("cancel_mongodb_import", { importId });
}
export async function releaseMongodbImportSource(_sourceRef: string): Promise<boolean> {
return false;
}
export async function exportMongodbQuery(request: MongoExportRequest, onProgress: (progress: MongoExportProgress) => void): Promise<MongoExportSummary> {
const unlisten: UnlistenFn = await listen<MongoExportProgress>("mongo-export-progress", (event) => {
if (event.payload.exportId === request.exportId) {
onProgress(event.payload);
if (event.payload.status === "done" || event.payload.status === "error" || event.payload.status === "cancelled") {
unlisten();
}
}
});
try {
const summary = await invoke<MongoExportSummary>("export_mongodb_query", { request });
unlisten();
return summary;
} catch (e) {
unlisten();
throw e instanceof BackendErrorException ? e : new BackendErrorException(e);
}
}
export async function cancelMongodbExport(exportId: string): Promise<boolean> {
return invoke("cancel_mongodb_export", { exportId });
}
// --- Database Export ---
export interface DatabaseExportRequest {
exportId: string;