feat(mongodb): bson dumps and database backup/restore
This commit is contained in:
1 parent
ecc1135de3
commit
f5673ca0a1
54 files changed
+6355
-106
No files matched your search
@@ -0,0 +1,438 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ArrowLeft, ArrowRight, DatabaseBackup, FolderOpen, Loader2, Play, Square, X } from "@lucide/vue";
|
||||
import { Dialog, DialogFooter, DialogHeader, DialogScrollContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { connectionIsEffectivelyReadOnly } from "@/lib/database/readOnlyWriteAccess";
|
||||
import { executeWithProductionContextGuard } from "@/lib/database/productionExecutionGuard";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
import * as api from "@/lib/backend/api";
|
||||
|
||||
const props = defineProps<{ connectionId: string; database: string; mode: "dump" | "restore" }>();
|
||||
const open = defineModel<boolean>("open", { default: false });
|
||||
const { t } = useI18n();
|
||||
const store = useConnectionStore();
|
||||
const desktop = isTauriRuntime();
|
||||
const restoring = computed(() => props.mode === "restore");
|
||||
const connection = computed(() => store.getConfig(props.connectionId));
|
||||
const readonly = computed(() => restoring.value && connectionIsEffectivelyReadOnly(connection.value));
|
||||
const format = ref<api.MongoDumpFormat>("archive");
|
||||
const gzip = ref(false);
|
||||
const catalog = ref<api.MongoDumpCatalog | null>(null);
|
||||
const sourceRef = ref<string | null>(null);
|
||||
const source = ref<api.MongoDumpSourceInput | null>(null);
|
||||
const sourceName = ref("");
|
||||
const sourceDatabase = ref("");
|
||||
const targetDatabase = ref(props.database);
|
||||
const outputPath = ref(desktop ? "" : `${props.database}.archive`);
|
||||
const selected = ref<string[]>([]);
|
||||
const dropExisting = ref(false);
|
||||
const restoreOptions = ref(true);
|
||||
const restoreIndexes = ref(true);
|
||||
const stopOnError = ref(true);
|
||||
const objcheck = ref(false);
|
||||
const batchSize = ref(500);
|
||||
const loading = ref(false);
|
||||
const confirming = ref(false);
|
||||
const running = ref(false);
|
||||
const cancelling = ref(false);
|
||||
const error = ref("");
|
||||
const step = ref<"configure" | "confirm" | "result">("configure");
|
||||
const progress = ref<api.MongoDatabaseDumpProgress | null>(null);
|
||||
const taskId = ref("");
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
let generation = 0;
|
||||
let readController: AbortController | null = null;
|
||||
let uploadController: AbortController | null = null;
|
||||
const readUpload = ref<{ loaded: number; total: number } | null>(null);
|
||||
function gib(value: number) {
|
||||
return (value / 1024 ** 3).toFixed(2);
|
||||
}
|
||||
|
||||
const entries = computed(() => catalog.value?.collections.filter((entry) => !restoring.value || entry.database === sourceDatabase.value) ?? []);
|
||||
const allSelected = computed(() => entries.value.length > 0 && selected.value.length === entries.value.length);
|
||||
const canReview = computed(
|
||||
() =>
|
||||
!!catalog.value &&
|
||||
!loading.value &&
|
||||
!confirming.value &&
|
||||
!running.value &&
|
||||
!readonly.value &&
|
||||
(entries.value.length === 0 || selected.value.length > 0) &&
|
||||
(restoring.value ? !!sourceRef.value && !!targetDatabase.value.trim() && (!catalog.value.databases.length || catalog.value.databases.includes(sourceDatabase.value)) : !!outputPath.value.trim()),
|
||||
);
|
||||
|
||||
function releaseSource() {
|
||||
const reference = sourceRef.value;
|
||||
sourceRef.value = null;
|
||||
if (reference) void api.releaseMongodbRestoreSource(reference);
|
||||
}
|
||||
function invalidate() {
|
||||
generation += 1;
|
||||
releaseSource();
|
||||
catalog.value = null;
|
||||
selected.value = [];
|
||||
error.value = "";
|
||||
step.value = "configure";
|
||||
}
|
||||
function cleanup() {
|
||||
generation += 1;
|
||||
readController?.abort();
|
||||
if (!running.value) releaseSource();
|
||||
}
|
||||
onBeforeUnmount(cleanup);
|
||||
watch(
|
||||
open,
|
||||
async (value) => {
|
||||
if (!value) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
if (restoring.value) return;
|
||||
const current = ++generation;
|
||||
loading.value = true;
|
||||
try {
|
||||
await store.ensureConnected(props.connectionId);
|
||||
const result = await api.inspectMongodbDatabaseDump(props.connectionId, props.database);
|
||||
if (current !== generation) return;
|
||||
catalog.value = result;
|
||||
selected.value = result.collections.map((entry) => entry.name);
|
||||
} catch (cause) {
|
||||
if (current === generation) error.value = String(cause instanceof Error ? cause.message : cause);
|
||||
} finally {
|
||||
if (current === generation) loading.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
watch([format, gzip], () => {
|
||||
if (restoring.value) invalidate();
|
||||
else outputPath.value = desktop ? "" : `${props.database}.archive${gzip.value ? ".gz" : ""}`;
|
||||
});
|
||||
watch(format, () => {
|
||||
if (restoring.value) {
|
||||
source.value = null;
|
||||
sourceName.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
function setFormat(value: unknown) {
|
||||
if (value === "directory" || value === "archive") format.value = value;
|
||||
}
|
||||
function setSourceDatabase(value: unknown) {
|
||||
if (typeof value !== "string") return;
|
||||
sourceDatabase.value = value;
|
||||
selected.value = entries.value.map((entry) => entry.name);
|
||||
}
|
||||
function selectAll(event: Event) {
|
||||
selected.value = (event.target as HTMLInputElement).checked ? entries.value.map((entry) => entry.name) : [];
|
||||
}
|
||||
function selectCollection(name: string, event: Event) {
|
||||
selected.value = (event.target as HTMLInputElement).checked ? [...selected.value, name] : selected.value.filter((value) => value !== name);
|
||||
}
|
||||
|
||||
async function chooseSource() {
|
||||
if (!desktop) {
|
||||
fileInput.value?.click();
|
||||
return;
|
||||
}
|
||||
const { open: choose } = await import("@tauri-apps/plugin-dialog");
|
||||
const path = await choose({ directory: format.value === "directory", multiple: false });
|
||||
if (typeof path === "string") {
|
||||
invalidate();
|
||||
source.value = path;
|
||||
sourceName.value = path;
|
||||
}
|
||||
}
|
||||
function chooseFiles(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const files = Array.from(input.files ?? []);
|
||||
input.value = "";
|
||||
if (!files.length) return;
|
||||
invalidate();
|
||||
source.value = format.value === "directory" ? files : files[0]!;
|
||||
sourceName.value = format.value === "directory" ? `${(files[0]!.webkitRelativePath || files[0]!.name).split("/")[0]} (${files.length})` : files[0]!.name;
|
||||
}
|
||||
async function readSource() {
|
||||
if (!source.value) return;
|
||||
releaseSource();
|
||||
const current = ++generation;
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
catalog.value = null;
|
||||
readUpload.value = null;
|
||||
readController = new AbortController();
|
||||
try {
|
||||
const result = await api.prepareMongodbRestoreSource(source.value, format.value, gzip.value, {
|
||||
signal: readController.signal,
|
||||
onUploadProgress: (loaded, total) => {
|
||||
readUpload.value = { loaded, total };
|
||||
},
|
||||
});
|
||||
if (current !== generation) {
|
||||
void api.releaseMongodbRestoreSource(result.sourceRef);
|
||||
return;
|
||||
}
|
||||
sourceRef.value = result.sourceRef;
|
||||
catalog.value = result;
|
||||
setSourceDatabase(result.databases.length === 1 ? result.databases[0] : "");
|
||||
} catch (cause) {
|
||||
if (current === generation) error.value = String(cause instanceof Error ? cause.message : cause);
|
||||
} finally {
|
||||
if (current === generation) loading.value = false;
|
||||
readController = null;
|
||||
}
|
||||
}
|
||||
async function chooseDestination() {
|
||||
const { open: choose, save } = await import("@tauri-apps/plugin-dialog");
|
||||
if (format.value === "directory") {
|
||||
const parent = await choose({ directory: true, multiple: false });
|
||||
if (typeof parent === "string") outputPath.value = `${parent.replace(/[\\/]$/, "")}/${props.database}-dump`;
|
||||
} else {
|
||||
const path = await save({ defaultPath: `${props.database}.archive${gzip.value ? ".gz" : ""}`, filters: [{ name: "MongoDB archive", extensions: [gzip.value ? "gz" : "archive"] }] });
|
||||
if (path) outputPath.value = path;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshDatabase() {
|
||||
await store.loadMongoDatabases(props.connectionId);
|
||||
await store.loadMongoCollections(props.connectionId, targetDatabase.value);
|
||||
for (const collection of selected.value) {
|
||||
store.mongoImportCompleted = { connectionId: props.connectionId, database: targetDatabase.value, collection, at: Date.now() };
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
async function execute() {
|
||||
if (!canReview.value) return;
|
||||
confirming.value = true;
|
||||
const current = generation;
|
||||
try {
|
||||
targetDatabase.value = targetDatabase.value.trim();
|
||||
if (restoring.value) {
|
||||
const allowed = await executeWithProductionContextGuard({
|
||||
connection: connection.value,
|
||||
database: targetDatabase.value.trim(),
|
||||
source: t("mongoDump.restoreTitle"),
|
||||
reviewText: `${dropExisting.value ? "DROP selected collections; " : ""}RESTORE ${JSON.stringify(sourceDatabase.value)} INTO ${JSON.stringify(targetDatabase.value.trim())}: ${selected.value.map((name) => JSON.stringify(name)).join(", ")}`,
|
||||
execute: async () => true,
|
||||
});
|
||||
if (!allowed) return;
|
||||
}
|
||||
if (current !== generation || !open.value) return;
|
||||
running.value = true;
|
||||
confirming.value = false;
|
||||
step.value = "result";
|
||||
error.value = "";
|
||||
progress.value = null;
|
||||
taskId.value = uuid();
|
||||
const onProgress = (value: api.MongoDatabaseDumpProgress) => {
|
||||
progress.value = value;
|
||||
};
|
||||
await store.ensureConnected(props.connectionId);
|
||||
if (restoring.value) {
|
||||
uploadController = new AbortController();
|
||||
const selectedPaths = new Set(entries.value.filter((entry) => selected.value.includes(entry.name)).flatMap((entry) => entry.sourceFiles ?? []));
|
||||
const upload = !desktop && format.value === "directory" && Array.isArray(source.value) ? { files: source.value.filter((file) => selectedPaths.has(file.webkitRelativePath || file.name)), gzip: gzip.value, signal: uploadController.signal } : undefined;
|
||||
progress.value = await api.restoreMongodbDatabase(
|
||||
{
|
||||
taskId: taskId.value,
|
||||
connectionId: props.connectionId,
|
||||
database: targetDatabase.value.trim(),
|
||||
sourceDatabase: sourceDatabase.value,
|
||||
sourceRef: sourceRef.value!,
|
||||
collections: [...selected.value],
|
||||
dropExisting: dropExisting.value,
|
||||
restoreOptions: restoreOptions.value,
|
||||
restoreIndexes: restoreIndexes.value,
|
||||
stopOnError: stopOnError.value,
|
||||
objcheck: objcheck.value,
|
||||
batchSize: Math.max(100, Math.min(5000, Math.trunc(Number(batchSize.value)) || 500)),
|
||||
},
|
||||
onProgress,
|
||||
upload,
|
||||
);
|
||||
void refreshDatabase().catch((cause) => console.error("Refresh after MongoDB restore failed", cause));
|
||||
} else {
|
||||
progress.value = await api.dumpMongodbDatabase({ taskId: taskId.value, connectionId: props.connectionId, database: props.database, filePath: outputPath.value.trim(), format: format.value, gzip: gzip.value, collections: [...selected.value] }, onProgress);
|
||||
}
|
||||
} catch (cause) {
|
||||
error.value = String(cause instanceof Error ? cause.message : cause);
|
||||
if (cause instanceof DOMException && cause.name === "AbortError") {
|
||||
progress.value = { taskId: taskId.value, status: "cancelled", phase: "done", collection: null, collectionsDone: 0, collectionsTotal: selected.value.length, documentsRead: 0, documentsWritten: 0, documentsFailed: 0, indexesCreated: 0, elapsedMs: 0, errorMessage: null, filePath: null };
|
||||
error.value = "";
|
||||
}
|
||||
} finally {
|
||||
confirming.value = false;
|
||||
running.value = false;
|
||||
cancelling.value = false;
|
||||
uploadController = null;
|
||||
}
|
||||
}
|
||||
async function cancel() {
|
||||
cancelling.value = true;
|
||||
uploadController?.abort();
|
||||
try {
|
||||
await api.cancelMongodbDatabaseDump(taskId.value);
|
||||
} catch (cause) {
|
||||
cancelling.value = false;
|
||||
error.value = String(cause instanceof Error ? cause.message : cause);
|
||||
}
|
||||
}
|
||||
function close() {
|
||||
if (!running.value && !confirming.value) open.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:open="open"
|
||||
@update:open="
|
||||
(value) => {
|
||||
if (!value) close();
|
||||
}
|
||||
"
|
||||
>
|
||||
<DialogScrollContent class="flex max-h-[calc(var(--dbx-viewport-height)-4rem)] min-h-0 flex-col overflow-hidden sm:max-w-[860px]">
|
||||
<DialogHeader class="shrink-0 pr-8"
|
||||
><DialogTitle class="flex items-center gap-2 text-base"><DatabaseBackup class="h-4 w-4" />{{ t(restoring ? "mongoDump.restoreTitle" : "mongoDump.dumpTitle") }}</DialogTitle></DialogHeader
|
||||
>
|
||||
<div class="min-h-0 flex-1 space-y-4 overflow-y-auto py-3 text-sm">
|
||||
<div class="border-b pb-2 font-medium break-all">{{ connection?.name }} / {{ props.database }}</div>
|
||||
<div v-if="readonly" class="text-xs text-amber-600">{{ t("mongoDump.readonly") }}</div>
|
||||
<template v-if="step === 'configure'">
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div class="space-y-1">
|
||||
<Label>{{ t("mongoDump.format") }}</Label
|
||||
><Select :model-value="format" :disabled="loading" @update:model-value="setFormat"
|
||||
><SelectTrigger class="h-8"><SelectValue /></SelectTrigger
|
||||
><SelectContent
|
||||
><SelectItem value="archive">{{ t("mongoDump.archive") }}</SelectItem
|
||||
><SelectItem v-if="desktop || restoring" value="directory">{{ t("mongoDump.directory") }}</SelectItem></SelectContent
|
||||
></Select
|
||||
>
|
||||
</div>
|
||||
<label class="flex min-h-8 items-center gap-2 self-end"><input v-model="gzip" type="checkbox" :disabled="loading" />{{ t("mongoDump.gzip") }}</label>
|
||||
</div>
|
||||
<div v-if="restoring" class="space-y-2">
|
||||
<Label>{{ t("mongoDump.source") }}</Label>
|
||||
<input ref="fileInput" type="file" class="hidden" :multiple="format === 'directory'" :webkitdirectory="format === 'directory' ? '' : undefined" @change="chooseFiles" />
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="min-w-0 flex-1 basis-full break-all text-xs sm:basis-0">{{ sourceName || t("tableImport.noFileSelected") }}</div>
|
||||
<Button variant="outline" size="sm" :disabled="loading" @click="chooseSource"><FolderOpen class="mr-1.5 h-4 w-4" />{{ t("tableImport.selectFile") }}</Button
|
||||
><Button size="sm" :disabled="!source || loading" @click="readSource"><Loader2 v-if="loading" class="mr-1.5 h-4 w-4 animate-spin" /><Play v-else class="mr-1.5 h-4 w-4" />{{ t("mongoDump.readSource") }}</Button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div class="space-y-1">
|
||||
<Label>{{ t("mongoDump.sourceDatabase") }}</Label
|
||||
><Select :model-value="sourceDatabase" :disabled="!catalog || loading" @update:model-value="setSourceDatabase"
|
||||
><SelectTrigger class="h-8 w-full min-w-0 [&>span]:min-w-0 [&>span]:truncate"><SelectValue /></SelectTrigger
|
||||
><SelectContent
|
||||
><SelectItem v-for="db in catalog?.databases ?? []" :key="db" :value="db">{{ db }}</SelectItem></SelectContent
|
||||
></Select
|
||||
>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label>{{ t("mongoDump.targetDatabase") }}</Label
|
||||
><Input v-model="targetDatabase" class="h-8" :disabled="loading" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="space-y-1">
|
||||
<Label>{{ t("mongoDump.destination") }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model="outputPath" class="h-8 min-w-0" :readonly="desktop" /><Button v-if="desktop" variant="outline" size="icon" class="h-8 w-8 shrink-0" :title="t('mongoDump.chooseDestination')" :aria-label="t('mongoDump.chooseDestination')" @click="chooseDestination"
|
||||
><FolderOpen class="h-4 w-4"
|
||||
/></Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" class="flex items-center gap-2 text-xs text-muted-foreground"><Loader2 class="h-4 w-4 animate-spin" />{{ t(restoring ? "mongoDump.preparing" : "mongoDump.loading") }}</div>
|
||||
<div v-if="loading && readUpload" class="text-xs tabular-nums">{{ t("mongoDump.uploadBytes", { done: gib(readUpload.loaded), total: gib(readUpload.total) }) }}</div>
|
||||
<div v-if="catalog" class="space-y-2">
|
||||
<div class="text-xs text-muted-foreground">{{ t("mongoDump.selected", { count: selected.length }) }}</div>
|
||||
<div class="max-h-64 overflow-auto border-y">
|
||||
<table class="w-full min-w-[480px] table-fixed text-xs">
|
||||
<thead class="sticky top-0 bg-background">
|
||||
<tr>
|
||||
<th class="w-8 p-2"><input type="checkbox" :checked="allSelected" :aria-label="t('mongoDump.selectAll')" @change="selectAll" /></th>
|
||||
<th class="w-2/5 p-2 text-left">{{ t("mongoDump.collection") }}</th>
|
||||
<th class="p-2 text-left">{{ t("mongoDump.kind") }}</th>
|
||||
<th class="p-2 text-right">{{ t("mongoDump.documents") }}</th>
|
||||
<th class="p-2 text-right">{{ t("mongoDump.indexes") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="entry in entries" :key="entry.name" class="border-t">
|
||||
<td class="p-2"><input type="checkbox" :checked="selected.includes(entry.name)" :aria-label="entry.name" @change="selectCollection(entry.name, $event)" /></td>
|
||||
<td class="p-2 break-all">{{ entry.name }}</td>
|
||||
<td class="p-2 break-all">{{ entry.kind }}</td>
|
||||
<td class="p-2 text-right">{{ entry.documents ?? "-" }}</td>
|
||||
<td class="p-2 text-right">{{ entry.indexes }}</td>
|
||||
</tr>
|
||||
<tr v-if="!entries.length">
|
||||
<td colspan="5" class="p-4 text-center text-muted-foreground">{{ t("mongoDump.empty") }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="restoring" class="space-y-3 border-t pt-3">
|
||||
<label class="flex items-center gap-2"><input v-model="dropExisting" type="checkbox" />{{ t("mongoDump.dropExisting") }}</label>
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<label class="flex items-center gap-2"><input v-model="restoreOptions" type="checkbox" />{{ t("mongoDump.restoreOptions") }}</label
|
||||
><label class="flex items-center gap-2"><input v-model="restoreIndexes" type="checkbox" />{{ t("mongoDump.restoreIndexes") }}</label
|
||||
><label class="flex items-center gap-2"><input v-model="stopOnError" type="checkbox" />{{ t("mongoDump.stopOnError") }}</label>
|
||||
<label class="flex items-center gap-2"><input v-model="objcheck" type="checkbox" />{{ t("mongoDump.objcheck") }}</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Label class="shrink-0">{{ t("mongoDump.batchSize") }}</Label
|
||||
><Input v-model.number="batchSize" type="number" min="100" max="5000" class="h-8 w-24" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="step === 'confirm'"
|
||||
><p>{{ t(restoring ? "mongoDump.confirmRestore" : "mongoDump.confirmDump", { count: selected.length, database: restoring ? targetDatabase : props.database }) }}</p>
|
||||
<p v-if="restoring" :class="dropExisting ? 'text-destructive' : 'text-muted-foreground'">{{ t(dropExisting ? "mongoDump.confirmDrop" : "mongoDump.confirmAppend") }}</p>
|
||||
<div class="max-h-56 overflow-auto border-y py-2 text-xs">
|
||||
<div v-for="name in selected" :key="name" class="py-1 break-all">{{ name }}</div>
|
||||
</div>
|
||||
<div v-if="!restoring" class="break-all text-xs text-muted-foreground">{{ outputPath }}</div></template
|
||||
>
|
||||
<template v-else
|
||||
><div class="flex items-center gap-2 font-medium">
|
||||
<Loader2 v-if="running" class="h-4 w-4 animate-spin" />{{ running ? t(`mongoDump.phase.${progress?.phase ?? "preparing"}`) : t(progress?.status === "done" ? "mongoDump.done" : progress?.status === "cancelled" ? "mongoDump.cancelled" : "mongoDump.failed") }}
|
||||
</div>
|
||||
<div class="break-all text-xs">{{ progress?.collection }}</div>
|
||||
<div v-if="progress?.phase === 'uploading'" class="text-xs tabular-nums">{{ t("mongoDump.uploadBytes", { done: gib(progress.bytesProcessed ?? 0), total: gib(progress.bytesTotal ?? 0) }) }}</div>
|
||||
<div v-if="restoring && progress?.phase === 'data'" class="text-xs tabular-nums">{{ t("mongoDump.readBytes", { size: gib(progress.bytesProcessed ?? 0) }) }}</div>
|
||||
<div v-if="progress?.phase === 'validating'" class="text-xs tabular-nums">{{ t("mongoDump.validationBytes", { size: gib(progress.bytesProcessed ?? 0), count: progress.documentsValidated ?? 0 }) }}</div>
|
||||
<progress class="h-2 w-full" :value="progress?.collectionsDone ?? 0" :max="Math.max(1, progress?.collectionsTotal ?? selected.length)" />
|
||||
<div class="text-xs">{{ t("mongoDump.progress", { done: progress?.collectionsDone ?? 0, total: progress?.collectionsTotal ?? selected.length }) }}</div>
|
||||
<dl class="grid grid-cols-2 gap-3 border-y py-3 text-xs">
|
||||
<dt>{{ t("mongoDump.read") }}</dt>
|
||||
<dd class="text-right tabular-nums">{{ progress?.documentsRead ?? 0 }}</dd>
|
||||
<dt v-if="restoring">{{ t("mongoDump.written") }}</dt>
|
||||
<dd v-if="restoring" class="text-right tabular-nums">{{ progress?.documentsWritten ?? 0 }}</dd>
|
||||
<dt>{{ t("mongoDump.rejected") }}</dt>
|
||||
<dd class="text-right tabular-nums">{{ progress?.documentsFailed ?? 0 }}</dd>
|
||||
<dt v-if="restoring">{{ t("mongoDump.createdIndexes") }}</dt>
|
||||
<dd v-if="restoring" class="text-right tabular-nums">{{ progress?.indexesCreated ?? 0 }}</dd>
|
||||
</dl></template
|
||||
>
|
||||
<div v-if="error" class="break-words border-l-2 border-destructive pl-3 text-xs text-destructive" role="alert">{{ error }}</div>
|
||||
</div>
|
||||
<DialogFooter class="shrink-0"
|
||||
><Button v-if="running" variant="destructive" :disabled="cancelling" @click="cancel"><Square class="mr-1.5 h-4 w-4" />{{ t("mongoDump.cancel") }}</Button
|
||||
><Button v-else variant="outline" :disabled="confirming" @click="close"><X class="mr-1.5 h-4 w-4" />{{ t("mongoDump.close") }}</Button
|
||||
><Button v-if="step !== 'configure' && !running" variant="outline" :disabled="confirming" @click="step = 'configure'"><ArrowLeft class="mr-1.5 h-4 w-4" />{{ t("mongoDump.back") }}</Button
|
||||
><Button v-if="step === 'configure'" :disabled="!canReview" @click="step = 'confirm'"><ArrowRight class="mr-1.5 h-4 w-4" />{{ t("mongoDump.review") }}</Button
|
||||
><Button v-if="step === 'confirm'" :disabled="!canReview" @click="execute"><Loader2 v-if="confirming" class="mr-1.5 h-4 w-4 animate-spin" /><Play v-else class="mr-1.5 h-4 w-4" />{{ t(restoring ? "mongoDump.restore" : "mongoDump.dump") }}</Button></DialogFooter
|
||||
>
|
||||
</DialogScrollContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
@@ -14,7 +14,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { connectionIsEffectivelyReadOnly } from "@/lib/database/readOnlyWriteAccess";
|
||||
import { executeWithProductionContextGuard } from "@/lib/database/productionExecutionGuard";
|
||||
import { TABLE_IMPORT_ENCODING_OPTIONS } from "@/lib/table/tableImport";
|
||||
import { importPreviewInput, importSourceDisplayName, importTextDelimiterForName, uploadedImportSourceFromPreview, type UploadedImportSource } from "@/lib/import/importSource";
|
||||
import { importPreviewInput, importSourceDisplayName, uploadedImportSourceFromPreview, type UploadedImportSource } from "@/lib/import/importSource";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import * as api from "@/lib/backend/api";
|
||||
@@ -93,13 +93,6 @@ const parseOptions = computed((): api.MongoImportParseOptions => {
|
||||
};
|
||||
});
|
||||
|
||||
function formatFromName(name: string): api.MongoImportFormat {
|
||||
const lower = name.toLowerCase();
|
||||
if (lower.endsWith(".ndjson") || lower.endsWith(".jsonl")) return "ndjson";
|
||||
if (lower.endsWith(".json")) return "json";
|
||||
return "csv";
|
||||
}
|
||||
|
||||
function releasePreviewSource() {
|
||||
const sourceRef = uploadedSource?.sourceRef;
|
||||
uploadedSource = null;
|
||||
@@ -111,10 +104,7 @@ function assignSource(source: string | File) {
|
||||
releasePreviewSource();
|
||||
selectedSource.value = source;
|
||||
sourceName.value = importSourceDisplayName(source);
|
||||
format.value = formatFromName(sourceName.value);
|
||||
typeMode.value = format.value === "csv" ? "auto" : "extendedJson";
|
||||
columnTypeOverrides.value = {};
|
||||
delimiter.value = importTextDelimiterForName(sourceName.value);
|
||||
wizardStep.value = "source";
|
||||
queuePreview();
|
||||
}
|
||||
@@ -127,7 +117,7 @@ async function selectFile() {
|
||||
const { open: openDialog } = await import("@tauri-apps/plugin-dialog");
|
||||
const selected = await openDialog({
|
||||
multiple: false,
|
||||
filters: [{ name: t("mongo.import.fileFilter"), extensions: ["csv", "json", "ndjson", "jsonl", "tsv"] }],
|
||||
filters: [{ name: t("mongo.import.fileFilter"), extensions: ["bson", "gz", "csv", "json", "ndjson", "jsonl", "tsv"] }],
|
||||
});
|
||||
if (!selected || Array.isArray(selected)) return;
|
||||
assignSource(selected);
|
||||
@@ -143,15 +133,17 @@ function handleFileInputChange(event: Event) {
|
||||
function queuePreview() {
|
||||
if (!selectedSource.value) return;
|
||||
if (previewReloadTimer) clearTimeout(previewReloadTimer);
|
||||
const requestId = ++previewRequestId;
|
||||
loadingPreview.value = true;
|
||||
previewReloadTimer = setTimeout(() => {
|
||||
void loadPreview();
|
||||
previewReloadTimer = null;
|
||||
void loadPreview(requestId);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
async function loadPreview() {
|
||||
async function loadPreview(requestId: number) {
|
||||
const source = selectedSource.value;
|
||||
if (!source) return;
|
||||
const requestId = ++previewRequestId;
|
||||
const keepExistingPreview = !!preview.value;
|
||||
if (!keepExistingPreview) {
|
||||
loadingPreview.value = true;
|
||||
@@ -165,7 +157,10 @@ async function loadPreview() {
|
||||
previewLimit: previewLimit.value,
|
||||
sourceRef: input.sourceRef,
|
||||
});
|
||||
if (requestId !== previewRequestId) return;
|
||||
if (requestId !== previewRequestId) {
|
||||
if (next.sourceRef && next.sourceRef !== uploadedSource?.sourceRef) void api.releaseMongodbImportSource(next.sourceRef);
|
||||
return;
|
||||
}
|
||||
preview.value = next;
|
||||
previewError.value = "";
|
||||
uploadedSource = uploadedImportSourceFromPreview(next) ?? uploadedSource;
|
||||
@@ -178,13 +173,21 @@ async function loadPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
watch([format, hasHeader, typeMode], () => {
|
||||
if (Object.keys(columnTypeOverrides.value).length) columnTypeOverrides.value = {};
|
||||
});
|
||||
watch(
|
||||
[format, hasHeader, typeMode],
|
||||
() => {
|
||||
if (Object.keys(columnTypeOverrides.value).length) columnTypeOverrides.value = {};
|
||||
},
|
||||
{ flush: "sync" },
|
||||
);
|
||||
|
||||
watch([format, encoding, delimiter, hasHeader, trim, emptyAsNull, typeMode, recognizeObjectIdHex, previewLimit, columnTypeOverrides], () => {
|
||||
if (selectedSource.value) queuePreview();
|
||||
});
|
||||
watch(
|
||||
[format, encoding, delimiter, hasHeader, trim, emptyAsNull, typeMode, recognizeObjectIdHex, previewLimit, columnTypeOverrides],
|
||||
() => {
|
||||
if (selectedSource.value) queuePreview();
|
||||
},
|
||||
{ flush: "sync" },
|
||||
);
|
||||
|
||||
watch(open, (value) => {
|
||||
if (value) return;
|
||||
@@ -197,6 +200,10 @@ watch(open, (value) => {
|
||||
});
|
||||
|
||||
function reset() {
|
||||
previewRequestId += 1;
|
||||
if (previewReloadTimer) clearTimeout(previewReloadTimer);
|
||||
previewReloadTimer = null;
|
||||
loadingPreview.value = false;
|
||||
releasePreviewSource();
|
||||
selectedSource.value = null;
|
||||
sourceName.value = "";
|
||||
@@ -211,8 +218,12 @@ function reset() {
|
||||
importId.value = "";
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (!running.value) reset();
|
||||
});
|
||||
|
||||
function canConfirm() {
|
||||
return !!preview.value && !loadingPreview.value && !previewError.value && !effectivelyReadOnly.value && (!preview.value.errors.length || skipErrorRows.value);
|
||||
return !!preview.value && preview.value.format === format.value && !loadingPreview.value && !previewError.value && !effectivelyReadOnly.value && (!preview.value.errors.length || skipErrorRows.value);
|
||||
}
|
||||
|
||||
async function startImport() {
|
||||
@@ -287,7 +298,7 @@ function formatPreviewCell(value: unknown) {
|
||||
}
|
||||
|
||||
function setFormat(value: unknown) {
|
||||
if (value === "csv" || value === "json" || value === "ndjson") format.value = value;
|
||||
if (value === "csv" || value === "json" || value === "ndjson" || value === "bson") format.value = value;
|
||||
}
|
||||
|
||||
function setEncoding(value: unknown) {
|
||||
@@ -343,7 +354,7 @@ function requestClose() {
|
||||
</DialogHeader>
|
||||
<div class="min-h-0 flex-1 space-y-4 overflow-y-auto py-2 pr-1 text-sm">
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-2">
|
||||
<input ref="fileInput" type="file" class="hidden" accept=".csv,.json,.ndjson,.jsonl,.tsv" @change="handleFileInputChange" />
|
||||
<input ref="fileInput" type="file" class="hidden" accept=".bson,.bson.gz,.csv,.json,.ndjson,.jsonl,.tsv" @change="handleFileInputChange" />
|
||||
<div class="flex h-10 min-w-0 items-center gap-2 rounded-md border bg-muted/20 px-3">
|
||||
<span class="shrink-0 text-xs text-muted-foreground">{{ t("mongo.import.target") }}</span>
|
||||
<span class="min-w-0 truncate text-sm font-medium">{{ targetLabel }}</span>
|
||||
@@ -366,10 +377,11 @@ function requestClose() {
|
||||
<SelectItem value="csv">CSV</SelectItem>
|
||||
<SelectItem value="json">JSON</SelectItem>
|
||||
<SelectItem value="ndjson">NDJSON</SelectItem>
|
||||
<SelectItem value="bson">BSON dump</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<div v-if="format !== 'bson'" class="space-y-1.5">
|
||||
<Label class="text-xs">{{ t("tableImport.encoding") }}</Label>
|
||||
<Select :model-value="encoding" @update:model-value="setEncoding">
|
||||
<SelectTrigger class="h-8 text-xs"><SelectValue /></SelectTrigger>
|
||||
@@ -382,7 +394,7 @@ function requestClose() {
|
||||
<Label class="text-xs">{{ t("tableImport.delimiter") }}</Label>
|
||||
<Input v-model="delimiter" class="h-8 text-xs font-mono" :aria-label="t('tableImport.delimiter')" />
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<div v-if="format !== 'bson'" class="space-y-1.5">
|
||||
<Label class="text-xs">{{ t("mongo.import.typeMode") }}</Label>
|
||||
<Select :model-value="typeMode" :aria-label="t('mongo.import.typeMode')" @update:model-value="setTypeMode">
|
||||
<SelectTrigger class="h-8 text-xs"><SelectValue /></SelectTrigger>
|
||||
@@ -405,13 +417,14 @@ function requestClose() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-2 rounded-md border p-3">
|
||||
<div v-if="format !== 'bson'" class="grid grid-cols-2 gap-x-4 gap-y-2 rounded-md border p-3">
|
||||
<label class="flex items-center gap-2 text-xs"><input v-model="hasHeader" type="checkbox" class="h-3.5 w-3.5 accent-primary" :aria-label="t('tableImport.hasHeader')" /> {{ t("tableImport.hasHeader") }}</label>
|
||||
<label class="flex items-center gap-2 text-xs"><input v-model="trim" type="checkbox" class="h-3.5 w-3.5 accent-primary" /> {{ t("tableImport.trimValues") }}</label>
|
||||
<label class="flex items-center gap-2 text-xs"><input v-model="emptyAsNull" type="checkbox" class="h-3.5 w-3.5 accent-primary" /> {{ t("mongo.import.emptyAsNull") }}</label>
|
||||
<label class="flex items-center gap-2 text-xs"><input v-model="recognizeObjectIdHex" type="checkbox" class="h-3.5 w-3.5 accent-primary" /> {{ t("mongo.import.recognizeObjectIdHex") }}</label>
|
||||
<label class="flex items-center gap-2 text-xs"><input v-model="skipErrorRows" type="checkbox" class="h-3.5 w-3.5 accent-primary" /> {{ t("mongo.import.skipErrorRows") }}</label>
|
||||
</div>
|
||||
<label v-else class="flex items-center gap-2 text-xs"><input v-model="skipErrorRows" type="checkbox" class="h-3.5 w-3.5 accent-primary" /> {{ t("mongo.import.skipErrorRows") }}</label>
|
||||
|
||||
<div v-if="loadingPreview && !preview" class="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Loader2 class="h-3.5 w-3.5 animate-spin" />
|
||||
@@ -438,7 +451,7 @@ function requestClose() {
|
||||
check-position="right"
|
||||
@update:model-value="(value) => setColumnType(column.name, value)"
|
||||
/>
|
||||
<span v-else-if="format !== 'csv'" class="text-muted-foreground">({{ column.inferredType }})</span>
|
||||
<span v-else-if="format !== 'csv' && format !== 'bson'" class="text-muted-foreground">({{ column.inferredType }})</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { createApp, nextTick, type App } from "vue";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
api: { inspectMongodbDatabaseDump: vi.fn(), prepareMongodbRestoreSource: vi.fn(), releaseMongodbRestoreSource: vi.fn(), dumpMongodbDatabase: vi.fn(), restoreMongodbDatabase: vi.fn(), cancelMongodbDatabaseDump: vi.fn() },
|
||||
store: { getConfig: vi.fn(), ensureConnected: vi.fn(), loadMongoDatabases: vi.fn(), loadMongoCollections: vi.fn(), mongoImportCompleted: null },
|
||||
guard: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/backend/api", () => mocks.api);
|
||||
vi.mock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.mock("@/stores/connectionStore", () => ({ useConnectionStore: () => mocks.store }));
|
||||
vi.mock("vue-i18n", async (original) => ({ ...(await original<typeof import("vue-i18n")>()), useI18n: () => ({ t: (key: string) => key }) }));
|
||||
vi.mock("@/lib/database/productionExecutionGuard", () => ({ executeWithProductionContextGuard: mocks.guard }));
|
||||
import MongoDatabaseDumpDialog from "../MongoDatabaseDumpDialog.vue";
|
||||
|
||||
const catalog = {
|
||||
databases: ["source"],
|
||||
collections: [
|
||||
{ database: "source", name: "records", kind: "collection", documents: 2, sizeBytes: 30, indexes: 2 },
|
||||
{ database: "source", name: "empty", kind: "collection", documents: 0, sizeBytes: 0, indexes: 0 },
|
||||
],
|
||||
};
|
||||
const done = { taskId: "task", status: "done", phase: "done", collection: "records", collectionsDone: 2, collectionsTotal: 2, documentsRead: 2, documentsWritten: 2, documentsFailed: 0, indexesCreated: 2, elapsedMs: 10, errorMessage: null, filePath: null };
|
||||
let app: App | null;
|
||||
async function flush() {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await Promise.resolve();
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
function button(key: string): HTMLButtonElement {
|
||||
const button = Array.from(document.querySelectorAll("button")).find((button) => button.textContent?.trim() === key);
|
||||
if (!button) throw new Error(`Missing button ${key}`);
|
||||
return button;
|
||||
}
|
||||
function checkbox(key: string): HTMLInputElement {
|
||||
const label = Array.from(document.querySelectorAll("label")).find((label) => label.textContent?.trim() === key);
|
||||
const checkbox = label?.querySelector<HTMLInputElement>("input[type=checkbox]");
|
||||
if (!checkbox) throw new Error(`Missing checkbox ${key}`);
|
||||
return checkbox;
|
||||
}
|
||||
async function mount(mode: "dump" | "restore") {
|
||||
app = createApp(MongoDatabaseDumpDialog, { open: true, connectionId: "c1", database: "target", mode });
|
||||
app.mount(document.body);
|
||||
await flush();
|
||||
}
|
||||
async function filesSelected(files: File[]) {
|
||||
const input = document.querySelector<HTMLInputElement>("input[type=file]")!;
|
||||
Object.defineProperty(input, "files", { configurable: true, value: files });
|
||||
input.dispatchEvent(new Event("change"));
|
||||
await flush();
|
||||
}
|
||||
async function formatSelected(value: string) {
|
||||
document.querySelector<HTMLElement>("[role=combobox]")!.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }));
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
await flush();
|
||||
const option = Array.from(document.querySelectorAll<HTMLElement>("[role=option]")).find((option) => option.textContent?.trim() === value)!;
|
||||
option.focus();
|
||||
option.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }));
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
await flush();
|
||||
}
|
||||
|
||||
describe("MongoDatabaseDumpDialog", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.clearAllMocks();
|
||||
mocks.store.getConfig.mockReturnValue({ name: "MongoDB", read_only: false });
|
||||
mocks.store.ensureConnected.mockResolvedValue(undefined);
|
||||
mocks.api.inspectMongodbDatabaseDump.mockResolvedValue(catalog);
|
||||
mocks.api.prepareMongodbRestoreSource.mockResolvedValue({ ...catalog, sourceRef: "prepared-one" });
|
||||
mocks.api.releaseMongodbRestoreSource.mockResolvedValue(true);
|
||||
mocks.api.dumpMongodbDatabase.mockResolvedValue(done);
|
||||
mocks.api.restoreMongodbDatabase.mockResolvedValue(done);
|
||||
mocks.guard.mockImplementation(async ({ execute }) => execute());
|
||||
});
|
||||
afterEach(() => {
|
||||
app?.unmount();
|
||||
app = null;
|
||||
document.body.innerHTML = "";
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("exports selected collections using the explicit archive and gzip options", async () => {
|
||||
await mount("dump");
|
||||
checkbox("mongoDump.gzip").click();
|
||||
document.querySelector<HTMLInputElement>('input[aria-label="empty"]')!.click();
|
||||
await flush();
|
||||
button("mongoDump.review").click();
|
||||
await flush();
|
||||
button("mongoDump.dump").click();
|
||||
await flush();
|
||||
expect(mocks.api.dumpMongodbDatabase).toHaveBeenCalledWith(expect.objectContaining({ connectionId: "c1", database: "target", format: "archive", gzip: true, collections: ["records"], filePath: "target.archive.gz" }), expect.any(Function));
|
||||
expect(mocks.guard).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("restores a directory into the chosen database only after drop confirmation", async () => {
|
||||
await mount("restore");
|
||||
await formatSelected("mongoDump.directory");
|
||||
checkbox("mongoDump.gzip").click();
|
||||
await filesSelected([new File([], "records.bson.gz")]);
|
||||
button("mongoDump.readSource").click();
|
||||
await flush();
|
||||
expect(mocks.api.prepareMongodbRestoreSource).toHaveBeenCalledWith(expect.any(Array), "directory", true, expect.objectContaining({ signal: expect.any(AbortSignal) }));
|
||||
checkbox("mongoDump.dropExisting").click();
|
||||
await flush();
|
||||
button("mongoDump.review").click();
|
||||
await flush();
|
||||
expect(document.body.textContent).toContain("mongoDump.confirmDrop");
|
||||
expect(mocks.api.restoreMongodbDatabase).not.toHaveBeenCalled();
|
||||
button("mongoDump.restore").click();
|
||||
await flush();
|
||||
expect(mocks.api.restoreMongodbDatabase).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ database: "target", sourceDatabase: "source", sourceRef: "prepared-one", dropExisting: true, restoreOptions: true, restoreIndexes: true, objcheck: false, batchSize: 500 }),
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ gzip: true }),
|
||||
);
|
||||
expect(mocks.guard).toHaveBeenCalledWith(expect.objectContaining({ database: "target", reviewText: expect.stringContaining("DROP selected collections") }));
|
||||
});
|
||||
|
||||
it("passes optional objcheck without a separate prevalidation request", async () => {
|
||||
await mount("restore");
|
||||
expect(checkbox("mongoDump.objcheck").checked).toBe(false);
|
||||
await filesSelected([new File([], "backup.archive")]);
|
||||
button("mongoDump.readSource").click();
|
||||
await flush();
|
||||
checkbox("mongoDump.objcheck").click();
|
||||
button("mongoDump.review").click();
|
||||
await flush();
|
||||
button("mongoDump.restore").click();
|
||||
await flush();
|
||||
expect(mocks.api.restoreMongodbDatabase).toHaveBeenCalledWith(expect.objectContaining({ objcheck: true }), expect.any(Function), undefined);
|
||||
expect(mocks.api.prepareMongodbRestoreSource).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("starts only one restore when confirmation is pending and the button is clicked twice", async () => {
|
||||
let allow!: (value: boolean) => void;
|
||||
mocks.guard.mockReturnValue(
|
||||
new Promise<boolean>((resolve) => {
|
||||
allow = resolve;
|
||||
}),
|
||||
);
|
||||
await mount("restore");
|
||||
await filesSelected([new File([], "backup.archive")]);
|
||||
button("mongoDump.readSource").click();
|
||||
await flush();
|
||||
button("mongoDump.review").click();
|
||||
await flush();
|
||||
const restore = button("mongoDump.restore");
|
||||
restore.click();
|
||||
restore.click();
|
||||
await flush();
|
||||
expect(mocks.guard).toHaveBeenCalledTimes(1);
|
||||
expect(button("mongoDump.back").disabled).toBe(true);
|
||||
expect(button("mongoDump.close").disabled).toBe(true);
|
||||
expect(mocks.api.restoreMongodbDatabase).not.toHaveBeenCalled();
|
||||
allow(true);
|
||||
await flush();
|
||||
expect(mocks.api.restoreMongodbDatabase).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not restore if the dialog is unmounted while confirmation is pending", async () => {
|
||||
let allow!: (value: boolean) => void;
|
||||
mocks.guard.mockReturnValue(
|
||||
new Promise<boolean>((resolve) => {
|
||||
allow = resolve;
|
||||
}),
|
||||
);
|
||||
await mount("restore");
|
||||
await filesSelected([new File([], "backup.archive")]);
|
||||
button("mongoDump.readSource").click();
|
||||
await flush();
|
||||
button("mongoDump.review").click();
|
||||
await flush();
|
||||
button("mongoDump.restore").click();
|
||||
await flush();
|
||||
app!.unmount();
|
||||
app = null;
|
||||
allow(true);
|
||||
await flush();
|
||||
expect(mocks.api.restoreMongodbDatabase).not.toHaveBeenCalled();
|
||||
expect(mocks.store.ensureConnected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("unlocks the dialog when production confirmation is declined", async () => {
|
||||
mocks.guard.mockResolvedValueOnce(false);
|
||||
await mount("restore");
|
||||
await filesSelected([new File([], "backup.archive")]);
|
||||
button("mongoDump.readSource").click();
|
||||
await flush();
|
||||
button("mongoDump.review").click();
|
||||
await flush();
|
||||
button("mongoDump.restore").click();
|
||||
await flush();
|
||||
expect(mocks.api.restoreMongodbDatabase).not.toHaveBeenCalled();
|
||||
expect(button("mongoDump.restore").disabled).toBe(false);
|
||||
button("mongoDump.restore").click();
|
||||
await flush();
|
||||
expect(mocks.api.restoreMongodbDatabase).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("invalidates a prepared source when gzip changes without inferring the format from its name", async () => {
|
||||
await mount("restore");
|
||||
await filesSelected([new File([], "misleading.bson.gz")]);
|
||||
button("mongoDump.readSource").click();
|
||||
await flush();
|
||||
expect(mocks.api.prepareMongodbRestoreSource).toHaveBeenCalledWith(expect.any(File), "archive", false, expect.any(Object));
|
||||
checkbox("mongoDump.gzip").click();
|
||||
await flush();
|
||||
expect(mocks.api.releaseMongodbRestoreSource).toHaveBeenCalledWith("prepared-one");
|
||||
expect(button("mongoDump.review").disabled).toBe(true);
|
||||
expect(mocks.api.prepareMongodbRestoreSource).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("releases a late preview after the dialog is unmounted", async () => {
|
||||
let resolve!: (value: unknown) => void;
|
||||
mocks.api.prepareMongodbRestoreSource.mockReturnValueOnce(
|
||||
new Promise((done) => {
|
||||
resolve = done;
|
||||
}),
|
||||
);
|
||||
await mount("restore");
|
||||
await filesSelected([new File([], "backup.archive")]);
|
||||
button("mongoDump.readSource").click();
|
||||
await flush();
|
||||
app!.unmount();
|
||||
app = null;
|
||||
resolve({ ...catalog, sourceRef: "late-source" });
|
||||
await flush();
|
||||
expect(mocks.api.releaseMongodbRestoreSource).toHaveBeenCalledWith("late-source");
|
||||
});
|
||||
});
|
||||
@@ -143,6 +143,53 @@ describe("MongoImportDialog", () => {
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
it("does not override the selected format from the file extension", async () => {
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup() {
|
||||
return () => h(MongoImportDialog, { open: true, connectionId: "c1", database: "shop", collection: "orders" });
|
||||
},
|
||||
}),
|
||||
);
|
||||
app.mount(document.body);
|
||||
await nextTick();
|
||||
|
||||
const input = document.querySelector("input[type='file']") as HTMLInputElement;
|
||||
const file = new File([new Uint8Array([5, 0, 0, 0, 0])], "orders.bson.gz", { type: "application/gzip" });
|
||||
Object.defineProperty(input, "files", { value: [file] });
|
||||
input.dispatchEvent(new Event("change"));
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
await Promise.resolve();
|
||||
await nextTick();
|
||||
|
||||
expect(api.previewMongodbImportFile.mock.calls[0]?.[1]).toMatchObject({
|
||||
format: "csv",
|
||||
parseOptions: { delimiter: ",", typeMode: "auto" },
|
||||
});
|
||||
expect(document.body.textContent).toContain("tableImport.encoding");
|
||||
expect(document.body.textContent).toContain("mongo.import.typeMode");
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
it("keeps an explicitly selected BSON format when changing files", async () => {
|
||||
api.previewMongodbImportFile.mockResolvedValue(previewResult({ format: "bson" }));
|
||||
const app = createApp(MongoImportDialog, { open: true, connectionId: "c1", database: "shop", collection: "orders" });
|
||||
app.mount(document.body);
|
||||
await nextTick();
|
||||
await setLabeledSelect("tableImport.sourceFormat", "bson");
|
||||
expect(document.body.textContent).not.toContain("tableImport.encoding");
|
||||
|
||||
for (const name of ["orders.bson", "renamed.json"]) {
|
||||
const input = document.querySelector("input[type='file']") as HTMLInputElement;
|
||||
Object.defineProperty(input, "files", { configurable: true, value: [new File([new Uint8Array([5, 0, 0, 0, 0])], name)] });
|
||||
input.dispatchEvent(new Event("change"));
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
await nextTick();
|
||||
expect(api.previewMongodbImportFile.mock.lastCall?.[1]).toMatchObject({ format: "bson" });
|
||||
}
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
it("reuses the uploaded sourceRef on later previews and releases it on close", async () => {
|
||||
const open = ref(true);
|
||||
const app = createApp(
|
||||
@@ -180,6 +227,9 @@ describe("MongoImportDialog", () => {
|
||||
|
||||
const header = document.querySelector("input[type='checkbox']") as HTMLInputElement;
|
||||
header.click();
|
||||
await nextTick();
|
||||
const next = Array.from(document.querySelectorAll("button")).find((button) => button.textContent?.includes("tableImport.next"));
|
||||
expect(next?.disabled).toBe(true);
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
await Promise.resolve();
|
||||
await nextTick();
|
||||
@@ -194,7 +244,7 @@ describe("MongoImportDialog", () => {
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
async function mountWithFile(fileName: string, preview: MongoImportPreview) {
|
||||
async function mountWithFile(fileName: string, preview: MongoImportPreview, format: MongoImportPreview["format"] = "csv") {
|
||||
api.previewMongodbImportFile.mockResolvedValue(preview);
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
@@ -211,6 +261,7 @@ describe("MongoImportDialog", () => {
|
||||
);
|
||||
app.mount(document.body);
|
||||
await nextTick();
|
||||
if (format !== "csv") await setLabeledSelect("tableImport.sourceFormat", format);
|
||||
const input = document.querySelector("input[type='file']") as HTMLInputElement;
|
||||
const file = new File(["id\n1"], fileName, { type: "text/csv" });
|
||||
Object.defineProperty(input, "files", { configurable: true, value: [file] });
|
||||
@@ -317,6 +368,7 @@ describe("MongoImportDialog", () => {
|
||||
fileName: "orders.json",
|
||||
columns: [{ name: "id", inferredType: "string" }],
|
||||
}),
|
||||
"json",
|
||||
);
|
||||
expect(columnTypeTrigger()).toBeNull();
|
||||
expect(document.body.textContent).toContain("(string)");
|
||||
|
||||
@@ -14,6 +14,7 @@ const SchemaDiagramDialog = defineAsyncComponent(() => import("@/components/diag
|
||||
const DatabaseDocsDialog = defineAsyncComponent(() => import("@/components/docs/DatabaseDocsDialog.vue"));
|
||||
const TableImportDialog = defineAsyncComponent(() => import("@/components/import/TableImportDialog.vue"));
|
||||
const MongoImportDialog = defineAsyncComponent(() => import("@/components/document/MongoImportDialog.vue"));
|
||||
const MongoDatabaseDumpDialog = defineAsyncComponent(() => import("@/components/document/MongoDatabaseDumpDialog.vue"));
|
||||
const FieldLineageDialog = defineAsyncComponent(() => import("@/components/lineage/FieldLineageDialog.vue"));
|
||||
const ConfigPassphraseDialog = defineAsyncComponent(() => import("@/components/config/ConfigPassphraseDialog.vue"));
|
||||
const ConfigConnectionSelectDialog = defineAsyncComponent(() => import("@/components/config/ConfigConnectionSelectDialog.vue"));
|
||||
@@ -301,6 +302,13 @@ watch(
|
||||
:prefill-table="dialogs.tableImportPrefillTable.value"
|
||||
/>
|
||||
<MongoImportDialog v-model:open="dialogs.showMongoImportDialog.value" :connection-id="dialogs.mongoImportPrefillConnectionId.value" :database="dialogs.mongoImportPrefillDatabase.value" :collection="dialogs.mongoImportPrefillCollection.value" />
|
||||
<MongoDatabaseDumpDialog
|
||||
v-if="dialogs.showMongoDatabaseDumpDialog.value"
|
||||
v-model:open="dialogs.showMongoDatabaseDumpDialog.value"
|
||||
:connection-id="dialogs.mongoDatabaseDumpPrefillConnectionId.value"
|
||||
:database="dialogs.mongoDatabaseDumpPrefillDatabase.value"
|
||||
:mode="dialogs.mongoDatabaseDumpMode.value"
|
||||
/>
|
||||
<DataGenerateDialog
|
||||
v-if="dialogs.showTableDataGenerateDialog.value"
|
||||
v-model:open="dialogs.showTableDataGenerateDialog.value"
|
||||
|
||||
@@ -410,15 +410,31 @@ const { copyStructureAs, copyStructureDocText, copyStructurePreview, exportData,
|
||||
acceptedSelectionIds: () => acceptedSelectionIds,
|
||||
});
|
||||
|
||||
const { openAllDatabasesExport, openDataCompare, openDatabaseExport, openDatabaseSearch, openDiagram, openDocs, openFieldLineage, openMongoImport, openScheduledBackups, openSchemaDiff, openSchemaDiffForRoutine, openSqlFileExecution, openStructureEditor, openTableImport, openTransfer } =
|
||||
useSidebarTreeToolRuntime({
|
||||
activeNode,
|
||||
connectionStore,
|
||||
queryStore,
|
||||
settingsStore,
|
||||
tableChildObjectName: tableChildDropObjectName,
|
||||
acceptedSelectionIds: () => acceptedSelectionIds,
|
||||
});
|
||||
const {
|
||||
openAllDatabasesExport,
|
||||
openDataCompare,
|
||||
openDatabaseExport,
|
||||
openDatabaseSearch,
|
||||
openDiagram,
|
||||
openDocs,
|
||||
openFieldLineage,
|
||||
openMongoImport,
|
||||
openMongoDatabaseDump,
|
||||
openScheduledBackups,
|
||||
openSchemaDiff,
|
||||
openSchemaDiffForRoutine,
|
||||
openSqlFileExecution,
|
||||
openStructureEditor,
|
||||
openTableImport,
|
||||
openTransfer,
|
||||
} = useSidebarTreeToolRuntime({
|
||||
activeNode,
|
||||
connectionStore,
|
||||
queryStore,
|
||||
settingsStore,
|
||||
tableChildObjectName: tableChildDropObjectName,
|
||||
acceptedSelectionIds: () => acceptedSelectionIds,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
"rename-started": [];
|
||||
@@ -5870,6 +5886,8 @@ function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
if (node.type === "mongo-db") {
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("transfer.dataTransfer"), action: openTransfer, icon: ArrowRightLeft });
|
||||
items.push({ label: t("mongoDump.menuDump"), action: () => openMongoDatabaseDump("dump"), icon: Upload });
|
||||
items.push({ label: t("mongoDump.menuRestore"), action: () => openMongoDatabaseDump("restore"), icon: Download });
|
||||
}
|
||||
if (node.type === "redis-db") {
|
||||
items.push({ label: "", separator: true });
|
||||
@@ -5950,6 +5968,8 @@ function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
children: [
|
||||
{ label: "CSV", action: () => void exportMongoCollection("csv") },
|
||||
{ label: "NDJSON", action: () => void exportMongoCollection("ndjson") },
|
||||
{ label: "BSON dump", action: () => void exportMongoCollection("bson") },
|
||||
{ label: "BSON dump (gzip)", action: () => void exportMongoCollection("bsonGzip") },
|
||||
],
|
||||
});
|
||||
if (canDropMongoCollection.value) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+24
@@ -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,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import * as messages from "../locales/mongodbDatabaseDump";
|
||||
|
||||
function leaves(value: Record<string, unknown>, prefix = ""): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).flatMap(([key, child]) => {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
if (typeof child === "string") return [[path, child]];
|
||||
return Object.entries(leaves(child as Record<string, unknown>, path));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
describe("MongoDB dump translations", () => {
|
||||
const source = leaves(messages.mongodbDatabaseDumpEn);
|
||||
for (const [locale, value] of Object.entries(messages)) {
|
||||
it(`${locale} includes every key and preserves placeholders`, () => {
|
||||
const translated = leaves(value);
|
||||
expect(Object.keys(translated).sort()).toEqual(Object.keys(source).sort());
|
||||
for (const [key, text] of Object.entries(translated)) {
|
||||
expect(text.trim(), key).not.toBe("");
|
||||
expect((text.match(/\{\w+\}/g) ?? []).sort(), key).toEqual((source[key]!.match(/\{\w+\}/g) ?? []).sort());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import docs from "./docs/en";
|
||||
import { consulUiMessages } from "./consulUi";
|
||||
import { sqlServerTraceMessages as sqlServerTrace } from "./sqlServerTraceMessages";
|
||||
import { meilisearchManagementEn } from "./meilisearchManagement";
|
||||
import { mongodbDatabaseDumpEn as mongoDump } from "./mongodbDatabaseDump";
|
||||
|
||||
const consul = {
|
||||
...consulUiMessages,
|
||||
@@ -5482,6 +5483,7 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
mongoDump,
|
||||
mongo: {
|
||||
documents: "{count} documents",
|
||||
addField: "Add Field",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { withEnglishFallback } from "./fallback";
|
||||
import { mongodbDatabaseDumpEs as mongoDump } from "./mongodbDatabaseDump";
|
||||
import { meilisearchManagementEs } from "./meilisearchManagement";
|
||||
import { redisGroupingEs as redisGrouping } from "./redisGrouping";
|
||||
import docs from "./docs/es";
|
||||
@@ -83,6 +84,7 @@ const consul = {
|
||||
};
|
||||
|
||||
export default withEnglishFallback({
|
||||
mongoDump,
|
||||
cellTransform: {
|
||||
title: "Vista previa de conversión",
|
||||
description: "Convierte el valor actual para verlo o copiarlo. El valor original y el borrador no cambian.",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { withEnglishFallback } from "./fallback";
|
||||
import { mongodbDatabaseDumpIt as mongoDump } from "./mongodbDatabaseDump";
|
||||
import { meilisearchManagementIt } from "./meilisearchManagement";
|
||||
import { redisGroupingIt as redisGrouping } from "./redisGrouping";
|
||||
import docs from "./docs/it";
|
||||
@@ -82,6 +83,7 @@ const consul = {
|
||||
};
|
||||
|
||||
export default withEnglishFallback({
|
||||
mongoDump,
|
||||
cellTransform: {
|
||||
title: "Anteprima conversione",
|
||||
description: "Converte il valore corrente per visualizzarlo o copiarlo. Il valore originale e la bozza restano invariati.",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { withEnglishFallback } from "./fallback";
|
||||
import { mongodbDatabaseDumpJa as mongoDump } from "./mongodbDatabaseDump";
|
||||
import docs from "./docs/ja";
|
||||
import { consulJaMessages } from "./consulJa";
|
||||
import { nacosAccessControlMessages, nacosAccessControlTabs } from "./nacosAccessControl";
|
||||
@@ -83,6 +84,7 @@ const consul = {
|
||||
};
|
||||
|
||||
export default withEnglishFallback({
|
||||
mongoDump,
|
||||
cellTransform: {
|
||||
title: "変換プレビュー",
|
||||
description: "現在の値を変換して表示またはコピーします。元の値と編集中の内容は変更されません。",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { withEnglishFallback } from "./fallback";
|
||||
import { mongodbDatabaseDumpKo as mongoDump } from "./mongodbDatabaseDump";
|
||||
import { meilisearchManagementKo } from "./meilisearchManagement";
|
||||
import { redisGroupingKo as redisGrouping } from "./redisGrouping";
|
||||
import docs from "./docs/ko";
|
||||
@@ -82,6 +83,7 @@ const consul = {
|
||||
};
|
||||
|
||||
export default withEnglishFallback({
|
||||
mongoDump,
|
||||
cellTransform: {
|
||||
title: "변환 미리보기",
|
||||
description: "현재 값을 변환하여 보거나 복사합니다. 원래 값과 편집 초안은 변경되지 않습니다.",
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
export const mongodbDatabaseDumpEn = {
|
||||
selectAll: "Select all collections",
|
||||
menuDump: "Dump database...",
|
||||
menuRestore: "Restore database...",
|
||||
dumpTitle: "MongoDB Database Dump",
|
||||
restoreTitle: "MongoDB Database Restore",
|
||||
format: "Format",
|
||||
archive: "MongoDB archive",
|
||||
directory: "MongoDB dump directory",
|
||||
gzip: "Gzip",
|
||||
source: "Backup source",
|
||||
readSource: "Read backup",
|
||||
sourceDatabase: "Source database",
|
||||
targetDatabase: "Target database",
|
||||
destination: "Destination",
|
||||
chooseDestination: "Choose destination",
|
||||
collection: "Collection",
|
||||
kind: "Type",
|
||||
documents: "Documents",
|
||||
indexes: "Indexes",
|
||||
dropExisting: "Drop selected target collections before restoring",
|
||||
restoreOptions: "Restore collection options",
|
||||
restoreIndexes: "Restore indexes",
|
||||
stopOnError: "Stop on write errors",
|
||||
objcheck: "Check BSON documents before insertion (objcheck)",
|
||||
readBytes: "Read: {size} GiB decoded",
|
||||
batchSize: "Batch size",
|
||||
selected: "{count} collections selected",
|
||||
empty: "No collections",
|
||||
preparing: "Reading backup...",
|
||||
loading: "Loading collections...",
|
||||
dump: "Start dump",
|
||||
restore: "Start restore",
|
||||
review: "Review",
|
||||
back: "Back",
|
||||
close: "Close",
|
||||
cancel: "Cancel task",
|
||||
confirmDump: "Dump {count} collections from {database}.",
|
||||
confirmRestore: "Restore {count} collections into {database}.",
|
||||
confirmDrop: "The selected target collections will be deleted and recreated.",
|
||||
confirmAppend: "Documents will be appended to existing collections.",
|
||||
readonly: "This connection is read-only.",
|
||||
done: "Completed",
|
||||
cancelled: "Cancelled",
|
||||
failed: "Failed",
|
||||
progress: "Collections: {done} / {total}",
|
||||
read: "Documents read",
|
||||
written: "Documents restored",
|
||||
rejected: "Failed documents",
|
||||
createdIndexes: "Indexes restored",
|
||||
uploadBytes: "Uploaded: {done} / {total} GiB",
|
||||
validationBytes: "Validated: {size} GiB decoded, {count} documents",
|
||||
phase: { preparing: "Preparing", uploading: "Uploading selected files", validating: "Validating backup", data: "Collection data", indexes: "Restoring indexes", views: "Restoring views", archive: "Writing backup", done: "Finished" },
|
||||
};
|
||||
|
||||
export const mongodbDatabaseDumpZhCN = {
|
||||
selectAll: "选择全部集合",
|
||||
menuDump: "备份数据库...",
|
||||
menuRestore: "恢复数据库...",
|
||||
dumpTitle: "MongoDB 数据库备份",
|
||||
restoreTitle: "MongoDB 数据库恢复",
|
||||
format: "格式",
|
||||
archive: "MongoDB archive",
|
||||
directory: "MongoDB dump 目录",
|
||||
gzip: "Gzip 压缩",
|
||||
source: "备份源",
|
||||
readSource: "读取备份",
|
||||
sourceDatabase: "源数据库",
|
||||
targetDatabase: "目标数据库",
|
||||
destination: "保存位置",
|
||||
chooseDestination: "选择保存位置",
|
||||
collection: "集合",
|
||||
kind: "类型",
|
||||
documents: "文档数",
|
||||
indexes: "索引数",
|
||||
dropExisting: "恢复前删除选中的目标同名集合",
|
||||
restoreOptions: "恢复集合选项",
|
||||
restoreIndexes: "恢复索引",
|
||||
stopOnError: "遇到写入错误时停止",
|
||||
objcheck: "写入前检查 BSON 文档(objcheck)",
|
||||
readBytes: "已读取:解压后 {size} GiB",
|
||||
batchSize: "批大小",
|
||||
selected: "已选择 {count} 个集合",
|
||||
empty: "没有集合",
|
||||
preparing: "正在读取备份...",
|
||||
loading: "正在读取集合...",
|
||||
dump: "开始备份",
|
||||
restore: "开始恢复",
|
||||
review: "确认操作",
|
||||
back: "返回",
|
||||
close: "关闭",
|
||||
cancel: "取消任务",
|
||||
confirmDump: "备份数据库 {database} 中的 {count} 个集合。",
|
||||
confirmRestore: "将 {count} 个集合恢复到数据库 {database}。",
|
||||
confirmDrop: "选中的目标同名集合将被删除并重建。",
|
||||
confirmAppend: "文档将追加到现有集合。",
|
||||
readonly: "该连接处于只读模式。",
|
||||
done: "已完成",
|
||||
cancelled: "已取消",
|
||||
failed: "失败",
|
||||
progress: "集合:{done} / {total}",
|
||||
read: "已读取文档",
|
||||
written: "已恢复文档",
|
||||
rejected: "失败文档",
|
||||
createdIndexes: "已恢复索引",
|
||||
uploadBytes: "已上传:{done} / {total} GiB",
|
||||
validationBytes: "已校验:解压后 {size} GiB,{count} 个文档",
|
||||
phase: { preparing: "准备中", uploading: "上传选中的文件", validating: "校验备份", data: "集合数据", indexes: "恢复索引", views: "恢复视图", archive: "写入备份", done: "已结束" },
|
||||
};
|
||||
|
||||
export const mongodbDatabaseDumpEs = {
|
||||
selectAll: "Seleccionar todas las colecciones",
|
||||
menuDump: "Crear copia de la base de datos...",
|
||||
menuRestore: "Restaurar base de datos...",
|
||||
dumpTitle: "Copia de seguridad de MongoDB",
|
||||
restoreTitle: "Restauración de MongoDB",
|
||||
format: "Formato",
|
||||
archive: "MongoDB archive",
|
||||
directory: "Directorio de copia de MongoDB",
|
||||
gzip: "Gzip",
|
||||
source: "Origen de la copia",
|
||||
readSource: "Leer copia",
|
||||
sourceDatabase: "Base de datos de origen",
|
||||
targetDatabase: "Base de datos de destino",
|
||||
destination: "Destino",
|
||||
chooseDestination: "Elegir destino",
|
||||
collection: "Colección",
|
||||
kind: "Tipo",
|
||||
documents: "Documentos",
|
||||
indexes: "Índices",
|
||||
dropExisting: "Eliminar las colecciones de destino seleccionadas antes de restaurar",
|
||||
restoreOptions: "Restaurar opciones de colección",
|
||||
restoreIndexes: "Restaurar índices",
|
||||
stopOnError: "Detener al producirse errores de escritura",
|
||||
objcheck: "Comprobar documentos BSON antes de insertarlos (objcheck)",
|
||||
readBytes: "Leídos: {size} GiB descomprimidos",
|
||||
batchSize: "Tamaño del lote",
|
||||
selected: "{count} colecciones seleccionadas",
|
||||
empty: "No hay colecciones",
|
||||
preparing: "Leyendo copia...",
|
||||
loading: "Cargando colecciones...",
|
||||
dump: "Iniciar copia",
|
||||
restore: "Iniciar restauración",
|
||||
review: "Revisar",
|
||||
back: "Volver",
|
||||
close: "Cerrar",
|
||||
cancel: "Cancelar tarea",
|
||||
confirmDump: "Crear una copia de {count} colecciones de {database}.",
|
||||
confirmRestore: "Restaurar {count} colecciones en {database}.",
|
||||
confirmDrop: "Las colecciones de destino seleccionadas se eliminarán y se volverán a crear.",
|
||||
confirmAppend: "Los documentos se añadirán a las colecciones existentes.",
|
||||
readonly: "Esta conexión es de solo lectura.",
|
||||
done: "Completado",
|
||||
cancelled: "Cancelado",
|
||||
failed: "Error",
|
||||
progress: "Colecciones: {done} / {total}",
|
||||
read: "Documentos leídos",
|
||||
written: "Documentos restaurados",
|
||||
rejected: "Documentos fallidos",
|
||||
createdIndexes: "Índices restaurados",
|
||||
uploadBytes: "Subidos: {done} / {total} GiB",
|
||||
validationBytes: "Validados: {size} GiB descomprimidos, {count} documentos",
|
||||
phase: { preparing: "Preparando", uploading: "Subiendo archivos seleccionados", validating: "Validando copia", data: "Datos de las colecciones", indexes: "Restaurando índices", views: "Restaurando vistas", archive: "Escribiendo copia", done: "Finalizado" },
|
||||
};
|
||||
|
||||
export const mongodbDatabaseDumpIt = {
|
||||
selectAll: "Seleziona tutte le raccolte",
|
||||
menuDump: "Backup del database...",
|
||||
menuRestore: "Ripristina database...",
|
||||
dumpTitle: "Backup del database MongoDB",
|
||||
restoreTitle: "Ripristino del database MongoDB",
|
||||
format: "Formato",
|
||||
archive: "MongoDB archive",
|
||||
directory: "Directory di backup MongoDB",
|
||||
gzip: "Gzip",
|
||||
source: "Origine del backup",
|
||||
readSource: "Leggi backup",
|
||||
sourceDatabase: "Database di origine",
|
||||
targetDatabase: "Database di destinazione",
|
||||
destination: "Destinazione",
|
||||
chooseDestination: "Scegli destinazione",
|
||||
collection: "Raccolta",
|
||||
kind: "Tipo",
|
||||
documents: "Documenti",
|
||||
indexes: "Indici",
|
||||
dropExisting: "Elimina le raccolte di destinazione selezionate prima del ripristino",
|
||||
restoreOptions: "Ripristina opzioni delle raccolte",
|
||||
restoreIndexes: "Ripristina indici",
|
||||
stopOnError: "Interrompi in caso di errori di scrittura",
|
||||
objcheck: "Verifica i documenti BSON prima di inserirli (objcheck)",
|
||||
readBytes: "Letti: {size} GiB decompressi",
|
||||
batchSize: "Dimensione del batch",
|
||||
selected: "{count} raccolte selezionate",
|
||||
empty: "Nessuna raccolta",
|
||||
preparing: "Lettura del backup...",
|
||||
loading: "Caricamento delle raccolte...",
|
||||
dump: "Avvia backup",
|
||||
restore: "Avvia ripristino",
|
||||
review: "Verifica",
|
||||
back: "Indietro",
|
||||
close: "Chiudi",
|
||||
cancel: "Annulla attività",
|
||||
confirmDump: "Esegui il backup di {count} raccolte da {database}.",
|
||||
confirmRestore: "Ripristina {count} raccolte in {database}.",
|
||||
confirmDrop: "Le raccolte di destinazione selezionate verranno eliminate e ricreate.",
|
||||
confirmAppend: "I documenti verranno aggiunti alle raccolte esistenti.",
|
||||
readonly: "Questa connessione è di sola lettura.",
|
||||
done: "Completato",
|
||||
cancelled: "Annullato",
|
||||
failed: "Non riuscito",
|
||||
progress: "Raccolte: {done} / {total}",
|
||||
read: "Documenti letti",
|
||||
written: "Documenti ripristinati",
|
||||
rejected: "Documenti non riusciti",
|
||||
createdIndexes: "Indici ripristinati",
|
||||
uploadBytes: "Caricati: {done} / {total} GiB",
|
||||
validationBytes: "Verificati: {size} GiB decompressi, {count} documenti",
|
||||
phase: { preparing: "Preparazione", uploading: "Caricamento dei file selezionati", validating: "Verifica del backup", data: "Dati delle raccolte", indexes: "Ripristino degli indici", views: "Ripristino delle viste", archive: "Scrittura del backup", done: "Terminato" },
|
||||
};
|
||||
|
||||
export const mongodbDatabaseDumpJa = {
|
||||
selectAll: "すべてのコレクションを選択",
|
||||
menuDump: "データベースをバックアップ...",
|
||||
menuRestore: "データベースを復元...",
|
||||
dumpTitle: "MongoDB データベースのバックアップ",
|
||||
restoreTitle: "MongoDB データベースの復元",
|
||||
format: "形式",
|
||||
archive: "MongoDB archive",
|
||||
directory: "MongoDB dump ディレクトリ",
|
||||
gzip: "Gzip 圧縮",
|
||||
source: "バックアップ元",
|
||||
readSource: "バックアップを読み込む",
|
||||
sourceDatabase: "復元元データベース",
|
||||
targetDatabase: "復元先データベース",
|
||||
destination: "保存先",
|
||||
chooseDestination: "保存先を選択",
|
||||
collection: "コレクション",
|
||||
kind: "種類",
|
||||
documents: "ドキュメント数",
|
||||
indexes: "インデックス数",
|
||||
dropExisting: "復元前に選択した復元先の同名コレクションを削除",
|
||||
restoreOptions: "コレクションのオプションを復元",
|
||||
restoreIndexes: "インデックスを復元",
|
||||
stopOnError: "書き込みエラーが発生したら停止",
|
||||
objcheck: "書き込み前に BSON ドキュメントを検証(objcheck)",
|
||||
readBytes: "読み込み済み: 展開後 {size} GiB",
|
||||
batchSize: "バッチサイズ",
|
||||
selected: "{count} 個のコレクションを選択済み",
|
||||
empty: "コレクションがありません",
|
||||
preparing: "バックアップを読み込み中...",
|
||||
loading: "コレクションを読み込み中...",
|
||||
dump: "バックアップ開始",
|
||||
restore: "復元開始",
|
||||
review: "操作を確認",
|
||||
back: "戻る",
|
||||
close: "閉じる",
|
||||
cancel: "タスクをキャンセル",
|
||||
confirmDump: "{database} の {count} 個のコレクションをバックアップします。",
|
||||
confirmRestore: "{count} 個のコレクションを {database} に復元します。",
|
||||
confirmDrop: "選択した復元先の同名コレクションは削除され、再作成されます。",
|
||||
confirmAppend: "ドキュメントは既存のコレクションに追加されます。",
|
||||
readonly: "この接続は読み取り専用です。",
|
||||
done: "完了",
|
||||
cancelled: "キャンセル済み",
|
||||
failed: "失敗",
|
||||
progress: "コレクション: {done} / {total}",
|
||||
read: "読み込んだドキュメント",
|
||||
written: "復元したドキュメント",
|
||||
rejected: "失敗したドキュメント",
|
||||
createdIndexes: "復元したインデックス",
|
||||
uploadBytes: "アップロード済み: {done} / {total} GiB",
|
||||
validationBytes: "検証済み: 展開後 {size} GiB、{count} ドキュメント",
|
||||
phase: { preparing: "準備中", uploading: "選択したファイルをアップロード中", validating: "バックアップを検証中", data: "コレクションデータ", indexes: "インデックスを復元中", views: "ビューを復元中", archive: "バックアップを書き込み中", done: "終了" },
|
||||
};
|
||||
|
||||
export const mongodbDatabaseDumpKo = {
|
||||
selectAll: "모든 컬렉션 선택",
|
||||
menuDump: "데이터베이스 백업...",
|
||||
menuRestore: "데이터베이스 복원...",
|
||||
dumpTitle: "MongoDB 데이터베이스 백업",
|
||||
restoreTitle: "MongoDB 데이터베이스 복원",
|
||||
format: "형식",
|
||||
archive: "MongoDB archive",
|
||||
directory: "MongoDB dump 디렉터리",
|
||||
gzip: "Gzip 압축",
|
||||
source: "백업 원본",
|
||||
readSource: "백업 읽기",
|
||||
sourceDatabase: "원본 데이터베이스",
|
||||
targetDatabase: "대상 데이터베이스",
|
||||
destination: "저장 위치",
|
||||
chooseDestination: "저장 위치 선택",
|
||||
collection: "컬렉션",
|
||||
kind: "유형",
|
||||
documents: "문서 수",
|
||||
indexes: "인덱스 수",
|
||||
dropExisting: "복원 전 선택한 대상의 동명 컬렉션 삭제",
|
||||
restoreOptions: "컬렉션 옵션 복원",
|
||||
restoreIndexes: "인덱스 복원",
|
||||
stopOnError: "쓰기 오류 발생 시 중지",
|
||||
objcheck: "쓰기 전 BSON 문서 검사 (objcheck)",
|
||||
readBytes: "읽음: 압축 해제 후 {size} GiB",
|
||||
batchSize: "배치 크기",
|
||||
selected: "컬렉션 {count}개 선택됨",
|
||||
empty: "컬렉션 없음",
|
||||
preparing: "백업 읽는 중...",
|
||||
loading: "컬렉션 불러오는 중...",
|
||||
dump: "백업 시작",
|
||||
restore: "복원 시작",
|
||||
review: "작업 확인",
|
||||
back: "뒤로",
|
||||
close: "닫기",
|
||||
cancel: "작업 취소",
|
||||
confirmDump: "{database}의 컬렉션 {count}개를 백업합니다.",
|
||||
confirmRestore: "컬렉션 {count}개를 {database}에 복원합니다.",
|
||||
confirmDrop: "선택한 대상의 동명 컬렉션이 삭제된 후 다시 생성됩니다.",
|
||||
confirmAppend: "기존 컬렉션에 문서가 추가됩니다.",
|
||||
readonly: "이 연결은 읽기 전용입니다.",
|
||||
done: "완료",
|
||||
cancelled: "취소됨",
|
||||
failed: "실패",
|
||||
progress: "컬렉션: {done} / {total}",
|
||||
read: "읽은 문서",
|
||||
written: "복원된 문서",
|
||||
rejected: "실패한 문서",
|
||||
createdIndexes: "복원된 인덱스",
|
||||
uploadBytes: "업로드됨: {done} / {total} GiB",
|
||||
validationBytes: "검증됨: 압축 해제 후 {size} GiB, 문서 {count}개",
|
||||
phase: { preparing: "준비 중", uploading: "선택한 파일 업로드 중", validating: "백업 검증 중", data: "컬렉션 데이터", indexes: "인덱스 복원 중", views: "뷰 복원 중", archive: "백업 쓰는 중", done: "종료됨" },
|
||||
};
|
||||
|
||||
export const mongodbDatabaseDumpPtBR = {
|
||||
selectAll: "Selecionar todas as coleções",
|
||||
menuDump: "Fazer backup do banco de dados...",
|
||||
menuRestore: "Restaurar banco de dados...",
|
||||
dumpTitle: "Backup do banco de dados MongoDB",
|
||||
restoreTitle: "Restauração do banco de dados MongoDB",
|
||||
format: "Formato",
|
||||
archive: "MongoDB archive",
|
||||
directory: "Diretório de backup do MongoDB",
|
||||
gzip: "Gzip",
|
||||
source: "Origem do backup",
|
||||
readSource: "Ler backup",
|
||||
sourceDatabase: "Banco de dados de origem",
|
||||
targetDatabase: "Banco de dados de destino",
|
||||
destination: "Destino",
|
||||
chooseDestination: "Escolher destino",
|
||||
collection: "Coleção",
|
||||
kind: "Tipo",
|
||||
documents: "Documentos",
|
||||
indexes: "Índices",
|
||||
dropExisting: "Excluir as coleções de destino selecionadas antes de restaurar",
|
||||
restoreOptions: "Restaurar opções das coleções",
|
||||
restoreIndexes: "Restaurar índices",
|
||||
stopOnError: "Parar ao ocorrerem erros de gravação",
|
||||
objcheck: "Verificar documentos BSON antes da inserção (objcheck)",
|
||||
readBytes: "Lidos: {size} GiB descompactados",
|
||||
batchSize: "Tamanho do lote",
|
||||
selected: "{count} coleções selecionadas",
|
||||
empty: "Nenhuma coleção",
|
||||
preparing: "Lendo backup...",
|
||||
loading: "Carregando coleções...",
|
||||
dump: "Iniciar backup",
|
||||
restore: "Iniciar restauração",
|
||||
review: "Revisar",
|
||||
back: "Voltar",
|
||||
close: "Fechar",
|
||||
cancel: "Cancelar tarefa",
|
||||
confirmDump: "Fazer backup de {count} coleções de {database}.",
|
||||
confirmRestore: "Restaurar {count} coleções em {database}.",
|
||||
confirmDrop: "As coleções de destino selecionadas serão excluídas e recriadas.",
|
||||
confirmAppend: "Os documentos serão adicionados às coleções existentes.",
|
||||
readonly: "Esta conexão é somente leitura.",
|
||||
done: "Concluído",
|
||||
cancelled: "Cancelado",
|
||||
failed: "Falhou",
|
||||
progress: "Coleções: {done} / {total}",
|
||||
read: "Documentos lidos",
|
||||
written: "Documentos restaurados",
|
||||
rejected: "Documentos com falha",
|
||||
createdIndexes: "Índices restaurados",
|
||||
uploadBytes: "Enviados: {done} / {total} GiB",
|
||||
validationBytes: "Validados: {size} GiB descompactados, {count} documentos",
|
||||
phase: { preparing: "Preparando", uploading: "Enviando arquivos selecionados", validating: "Validando backup", data: "Dados das coleções", indexes: "Restaurando índices", views: "Restaurando visualizações", archive: "Gravando backup", done: "Finalizado" },
|
||||
};
|
||||
|
||||
export const mongodbDatabaseDumpZhTW = {
|
||||
selectAll: "選取所有集合",
|
||||
menuDump: "備份資料庫...",
|
||||
menuRestore: "還原資料庫...",
|
||||
dumpTitle: "MongoDB 資料庫備份",
|
||||
restoreTitle: "MongoDB 資料庫還原",
|
||||
format: "格式",
|
||||
archive: "MongoDB archive",
|
||||
directory: "MongoDB dump 目錄",
|
||||
gzip: "Gzip 壓縮",
|
||||
source: "備份來源",
|
||||
readSource: "讀取備份",
|
||||
sourceDatabase: "來源資料庫",
|
||||
targetDatabase: "目標資料庫",
|
||||
destination: "儲存位置",
|
||||
chooseDestination: "選擇儲存位置",
|
||||
collection: "集合",
|
||||
kind: "類型",
|
||||
documents: "文件數",
|
||||
indexes: "索引數",
|
||||
dropExisting: "還原前刪除選取的目標同名集合",
|
||||
restoreOptions: "還原集合選項",
|
||||
restoreIndexes: "還原索引",
|
||||
stopOnError: "遇到寫入錯誤時停止",
|
||||
objcheck: "寫入前檢查 BSON 文件(objcheck)",
|
||||
readBytes: "已讀取:解壓縮後 {size} GiB",
|
||||
batchSize: "批次大小",
|
||||
selected: "已選取 {count} 個集合",
|
||||
empty: "沒有集合",
|
||||
preparing: "正在讀取備份...",
|
||||
loading: "正在載入集合...",
|
||||
dump: "開始備份",
|
||||
restore: "開始還原",
|
||||
review: "確認操作",
|
||||
back: "返回",
|
||||
close: "關閉",
|
||||
cancel: "取消任務",
|
||||
confirmDump: "備份資料庫 {database} 中的 {count} 個集合。",
|
||||
confirmRestore: "將 {count} 個集合還原至資料庫 {database}。",
|
||||
confirmDrop: "選取的目標同名集合將被刪除並重建。",
|
||||
confirmAppend: "文件將附加至現有集合。",
|
||||
readonly: "此連線處於唯讀模式。",
|
||||
done: "已完成",
|
||||
cancelled: "已取消",
|
||||
failed: "失敗",
|
||||
progress: "集合:{done} / {total}",
|
||||
read: "已讀取文件",
|
||||
written: "已還原文件",
|
||||
rejected: "失敗文件",
|
||||
createdIndexes: "已還原索引",
|
||||
uploadBytes: "已上傳:{done} / {total} GiB",
|
||||
validationBytes: "已驗證:解壓縮後 {size} GiB,{count} 個文件",
|
||||
phase: { preparing: "準備中", uploading: "上傳選取的檔案", validating: "驗證備份", data: "集合資料", indexes: "還原索引", views: "還原檢視表", archive: "寫入備份", done: "已結束" },
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { withEnglishFallback } from "./fallback";
|
||||
import { mongodbDatabaseDumpPtBR as mongoDump } from "./mongodbDatabaseDump";
|
||||
import { meilisearchManagementPtBR } from "./meilisearchManagement";
|
||||
import { redisGroupingPtBR as redisGrouping } from "./redisGrouping";
|
||||
import docs from "./docs/pt-BR";
|
||||
@@ -83,6 +84,7 @@ const consul = {
|
||||
};
|
||||
|
||||
export default withEnglishFallback({
|
||||
mongoDump,
|
||||
cellTransform: {
|
||||
title: "Prévia da conversão",
|
||||
description: "Converte o valor atual para visualizar ou copiar. O valor original e o rascunho permanecem inalterados.",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { withEnglishFallback } from "./fallback";
|
||||
import docs from "./docs/zh-CN";
|
||||
import { consul } from "./consulZhCN";
|
||||
import { meilisearchManagementZhCN } from "./meilisearchManagement";
|
||||
import { mongodbDatabaseDumpZhCN as mongoDump } from "./mongodbDatabaseDump";
|
||||
|
||||
export default withEnglishFallback({
|
||||
cellTransform: {
|
||||
@@ -5457,6 +5458,7 @@ export default withEnglishFallback({
|
||||
},
|
||||
},
|
||||
},
|
||||
mongoDump,
|
||||
mongo: {
|
||||
documents: "{count} 个文档",
|
||||
addField: "添加字段",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { redisGroupingZhTW as redisGrouping } from "./redisGrouping";
|
||||
import { withEnglishFallback } from "./fallback";
|
||||
import { mongodbDatabaseDumpZhTW as mongoDump } from "./mongodbDatabaseDump";
|
||||
import { meilisearchManagementZhTW } from "./meilisearchManagement";
|
||||
import docs from "./docs/zh-TW";
|
||||
import { consulZhTWMessages } from "./consulZhTW";
|
||||
@@ -82,6 +83,7 @@ const consul = {
|
||||
};
|
||||
|
||||
export default withEnglishFallback({
|
||||
mongoDump,
|
||||
cellTransform: {
|
||||
title: "轉換預覽",
|
||||
description: "將目前內容轉換後檢視或複製,不改變原值和編輯草稿。",
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { prepareMongodbRestoreSource, restoreMongodbDatabase } from "../http";
|
||||
|
||||
function file(name: string, size = 0) {
|
||||
const value = new File([], name);
|
||||
Object.defineProperty(value, "webkitRelativePath", { value: `dump/source/${name}` });
|
||||
Object.defineProperty(value, "size", { value: size });
|
||||
return value;
|
||||
}
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
describe("MongoDB metadata-first transport", () => {
|
||||
it("previews a multi-gigabyte directory without uploading any BSON bodies", async () => {
|
||||
const fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({ sourceRef: "catalog", databases: ["source"], collections: [] })));
|
||||
vi.stubGlobal("fetch", fetch);
|
||||
const data = file("large.bson.gz", 5 * 1024 ** 3);
|
||||
const metadata = file("large.metadata.json.gz", 200);
|
||||
await prepareMongodbRestoreSource([data, metadata], "directory", true);
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
const form = fetch.mock.calls[0]![1].body as FormData;
|
||||
expect(form.get("format")).toBe("directory");
|
||||
expect(form.get("gzip")).toBe("true");
|
||||
expect(JSON.parse(form.get("manifest") as string)).toEqual([
|
||||
{ path: "dump/source/large.bson.gz", sizeBytes: 5 * 1024 ** 3 },
|
||||
{ path: "dump/source/large.metadata.json.gz", sizeBytes: 200 },
|
||||
]);
|
||||
expect(form.getAll("file")).toHaveLength(1);
|
||||
expect((form.get("file") as File).name).toBe("dump/source/large.metadata.json.gz");
|
||||
});
|
||||
|
||||
it("checks the effective server limit before the confirmed data upload", async () => {
|
||||
const fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify(1024 ** 3)));
|
||||
vi.stubGlobal("fetch", fetch);
|
||||
const request = { taskId: "task", connectionId: "conn", database: "target", sourceDatabase: "source", sourceRef: "catalog", dropExisting: true, restoreOptions: true, restoreIndexes: true, stopOnError: true, batchSize: 500 };
|
||||
await expect(restoreMongodbDatabase(request, vi.fn(), { files: [file("large.bson", 5 * 1024 ** 3)], gzip: false })).rejects.toThrow("exceeds");
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(fetch.mock.calls[0]![0]).toContain("/upload-limit");
|
||||
});
|
||||
|
||||
it("passes cancellation to catalog upload without a separate restore request", async () => {
|
||||
const controller = new AbortController();
|
||||
const fetch = vi.fn().mockRejectedValue(new DOMException("Aborted", "AbortError"));
|
||||
vi.stubGlobal("fetch", fetch);
|
||||
controller.abort();
|
||||
await expect(prepareMongodbRestoreSource([file("a.bson")], "directory", false, { signal: controller.signal })).rejects.toThrow("Aborted");
|
||||
expect(fetch.mock.calls[0]![1].signal).toBe(controller.signal);
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -495,6 +495,13 @@ export const cancelMongodbImport = forward("cancelMongodbImport");
|
||||
export const releaseMongodbImportSource = forward("releaseMongodbImportSource");
|
||||
export const exportMongodbQuery = forward("exportMongodbQuery");
|
||||
export const cancelMongodbExport = forward("cancelMongodbExport");
|
||||
export const inspectMongodbDatabaseDump = forward("inspectMongodbDatabaseDump");
|
||||
export const prepareMongodbRestoreSource = forward("prepareMongodbRestoreSource");
|
||||
export const releaseMongodbRestoreSource = forward("releaseMongodbRestoreSource");
|
||||
export const dumpMongodbDatabase = forward("dumpMongodbDatabase");
|
||||
export const restoreMongodbDatabase = forward("restoreMongodbDatabase");
|
||||
export const cancelMongodbDatabaseDump = forward("cancelMongodbDatabaseDump");
|
||||
export type { MongoDumpFormat, MongoDumpSourceInput, MongoDumpCatalog, MongoDumpCollection, MongoRestoreSourcePreview, MongoDatabaseDumpRequest, MongoDatabaseRestoreRequest, MongoDatabaseDumpProgress } from "./mongodbDumpTypes";
|
||||
|
||||
// Database Export
|
||||
export const beginDatabaseBackupSnapshot = forward("beginDatabaseBackupSnapshot");
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { MongoDumpFormat, MongoDumpSourceInput, MongoDumpCatalog, MongoRestoreSourcePreview, MongoDatabaseDumpRequest, MongoDatabaseRestoreRequest, MongoDatabaseDumpProgress } from "./mongodbDumpTypes";
|
||||
import type { MongoRestoreUpload, MongoSourceReadOptions } from "./mongodbDumpTypes";
|
||||
import type {
|
||||
ConnectionConfig,
|
||||
ConnectionTestResult,
|
||||
@@ -2674,6 +2676,143 @@ export async function releaseTableImportSource(sourceRef: string): Promise<boole
|
||||
return result.released;
|
||||
}
|
||||
|
||||
export function inspectMongodbDatabaseDump(connectionId: string, database: string): Promise<MongoDumpCatalog> {
|
||||
return post("/api/mongo/dump/catalog", { connectionId, database });
|
||||
}
|
||||
async function checkMongoUploadSize(files: File[]) {
|
||||
const limit = await get<number>("/api/mongo/dump/upload-limit");
|
||||
const size = files.reduce((total, file) => total + file.size, 0);
|
||||
if (size > limit) throw new Error(`MongoDB upload: ${(size / 1024 ** 3).toFixed(2)} GiB exceeds ${(limit / 1024 ** 3).toFixed(2)} GiB (DBX_MAX_UPLOAD_MB)`);
|
||||
}
|
||||
|
||||
function uploadMongoForm(url: string, form: FormData, options?: MongoSourceReadOptions): Promise<MongoRestoreSourcePreview> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const abort = () => xhr.abort();
|
||||
const cleanup = () => options?.signal?.removeEventListener("abort", abort);
|
||||
if (options?.signal?.aborted) {
|
||||
reject(new DOMException("Cancelled", "AbortError"));
|
||||
return;
|
||||
}
|
||||
xhr.open("POST", apiUrl(url));
|
||||
xhr.upload.onprogress = (event) => options?.onUploadProgress?.(event.loaded, event.lengthComputable ? event.total : 0);
|
||||
xhr.onload = async () => {
|
||||
cleanup();
|
||||
try {
|
||||
if (xhr.status < 200 || xhr.status >= 300) throw await backendResponseError(new Response(xhr.responseText, { status: xhr.status }));
|
||||
resolve(JSON.parse(xhr.responseText));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => {
|
||||
cleanup();
|
||||
reject(new Error("MongoDB upload connection failed"));
|
||||
};
|
||||
xhr.onabort = () => {
|
||||
cleanup();
|
||||
reject(new DOMException("Cancelled", "AbortError"));
|
||||
};
|
||||
options?.signal?.addEventListener("abort", abort, { once: true });
|
||||
xhr.send(form);
|
||||
});
|
||||
}
|
||||
|
||||
export async function prepareMongodbRestoreSource(source: MongoDumpSourceInput, format: MongoDumpFormat, gzip: boolean, options?: MongoSourceReadOptions): Promise<MongoRestoreSourcePreview> {
|
||||
if (typeof source === "string") throw new Error("Web restores require uploaded files");
|
||||
const files = Array.isArray(source) ? source : [source];
|
||||
const form = new FormData();
|
||||
form.append("format", format);
|
||||
form.append("gzip", String(gzip));
|
||||
if (format === "directory") {
|
||||
form.append("manifest", JSON.stringify(files.map((file) => ({ path: file.webkitRelativePath || file.name, sizeBytes: file.size }))));
|
||||
for (const file of files) if (/\.metadata\.json(?:\.gz)?$/.test(file.name)) form.append("file", file, file.webkitRelativePath || file.name);
|
||||
} else {
|
||||
await checkMongoUploadSize(files);
|
||||
for (const file of files) form.append("file", file, file.name);
|
||||
return uploadMongoForm("/api/mongo/dump/source", form, options);
|
||||
}
|
||||
const response = await fetch(apiUrl("/api/mongo/dump/source"), { method: "POST", body: form, signal: options?.signal });
|
||||
if (!response.ok) throw await backendResponseError(response);
|
||||
return response.json();
|
||||
}
|
||||
export async function releaseMongodbRestoreSource(sourceRef: string): Promise<boolean> {
|
||||
return (await post<{ released: boolean }>("/api/mongo/dump/source/release", { sourceRef })).released;
|
||||
}
|
||||
async function runMongodbDatabaseTask(mode: "export" | "restore", request: MongoDatabaseDumpRequest | MongoDatabaseRestoreRequest, onProgress: (progress: MongoDatabaseDumpProgress) => void): Promise<MongoDatabaseDumpProgress> {
|
||||
await post(`/api/mongo/dump/${mode}`, { request });
|
||||
return new Promise((resolve, reject) => {
|
||||
const events = new EventSource(apiUrl(`/api/mongo/dump/progress/${request.taskId}`));
|
||||
events.onmessage = (event) => {
|
||||
const progress: MongoDatabaseDumpProgress = JSON.parse(event.data);
|
||||
onProgress(progress);
|
||||
if (progress.status === "running") return;
|
||||
events.close();
|
||||
if (progress.status !== "done") {
|
||||
reject(new Error(progress.errorMessage || "MongoDB database task failed"));
|
||||
return;
|
||||
}
|
||||
if (mode === "export") {
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = apiUrl(`/api/mongo/export/download/${request.taskId}`);
|
||||
anchor.click();
|
||||
}
|
||||
resolve(progress);
|
||||
};
|
||||
events.onerror = () => {
|
||||
events.close();
|
||||
reject(new Error("MongoDB database progress connection failed"));
|
||||
};
|
||||
});
|
||||
}
|
||||
export function dumpMongodbDatabase(request: MongoDatabaseDumpRequest, onProgress: (progress: MongoDatabaseDumpProgress) => void) {
|
||||
return runMongodbDatabaseTask("export", request, onProgress);
|
||||
}
|
||||
export async function restoreMongodbDatabase(request: MongoDatabaseRestoreRequest, onProgress: (progress: MongoDatabaseDumpProgress) => void, upload?: MongoRestoreUpload) {
|
||||
let acquired: string | undefined;
|
||||
try {
|
||||
if (upload) {
|
||||
await checkMongoUploadSize(upload.files);
|
||||
const form = new FormData();
|
||||
form.append("format", "directory");
|
||||
form.append("gzip", String(upload.gzip));
|
||||
form.append("request", JSON.stringify(request));
|
||||
for (const file of upload.files) form.append("file", file, file.webkitRelativePath || file.name);
|
||||
const started = Date.now();
|
||||
const preview = await uploadMongoForm("/api/mongo/dump/source/upload", form, {
|
||||
signal: upload.signal,
|
||||
onUploadProgress: (loaded, total) =>
|
||||
onProgress({
|
||||
taskId: request.taskId,
|
||||
status: "running",
|
||||
phase: "uploading",
|
||||
collection: null,
|
||||
collectionsDone: 0,
|
||||
collectionsTotal: request.collections?.length ?? 0,
|
||||
documentsRead: 0,
|
||||
documentsWritten: 0,
|
||||
documentsFailed: 0,
|
||||
indexesCreated: 0,
|
||||
elapsedMs: Date.now() - started,
|
||||
errorMessage: null,
|
||||
filePath: null,
|
||||
bytesProcessed: loaded,
|
||||
bytesTotal: total,
|
||||
}),
|
||||
});
|
||||
acquired = preview.sourceRef;
|
||||
if (upload.signal?.aborted) throw new DOMException("Cancelled", "AbortError");
|
||||
request = { ...request, sourceRef: acquired };
|
||||
}
|
||||
return await runMongodbDatabaseTask("restore", request, onProgress);
|
||||
} finally {
|
||||
if (acquired) void releaseMongodbRestoreSource(acquired).catch(() => {});
|
||||
}
|
||||
}
|
||||
export async function cancelMongodbDatabaseDump(taskId: string): Promise<boolean> {
|
||||
return (await post<{ cancelled: boolean }>("/api/mongo/dump/cancel", { taskId })).cancelled;
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
export type MongoDumpFormat = "directory" | "archive";
|
||||
export type MongoDumpSourceInput = string | File | File[];
|
||||
|
||||
export interface MongoDumpCollection {
|
||||
database: string;
|
||||
name: string;
|
||||
kind: string;
|
||||
documents: number | null;
|
||||
sizeBytes: number;
|
||||
indexes: number;
|
||||
sourceFiles?: string[];
|
||||
}
|
||||
export interface MongoDumpCatalog {
|
||||
databases: string[];
|
||||
collections: MongoDumpCollection[];
|
||||
}
|
||||
export interface MongoRestoreSourcePreview extends MongoDumpCatalog {
|
||||
sourceRef: string;
|
||||
}
|
||||
export interface MongoDatabaseDumpRequest {
|
||||
taskId: string;
|
||||
connectionId: string;
|
||||
database: string;
|
||||
filePath: string;
|
||||
format: MongoDumpFormat;
|
||||
gzip: boolean;
|
||||
collections?: string[];
|
||||
}
|
||||
export interface MongoDatabaseRestoreRequest {
|
||||
taskId: string;
|
||||
connectionId: string;
|
||||
database: string;
|
||||
sourceDatabase: string;
|
||||
sourceRef: string;
|
||||
collections?: string[];
|
||||
dropExisting: boolean;
|
||||
restoreOptions: boolean;
|
||||
restoreIndexes: boolean;
|
||||
stopOnError: boolean;
|
||||
objcheck?: boolean;
|
||||
batchSize: number;
|
||||
executionId?: string;
|
||||
}
|
||||
export interface MongoDatabaseDumpProgress {
|
||||
taskId: string;
|
||||
status: "running" | "done" | "error" | "cancelled";
|
||||
phase: "preparing" | "uploading" | "validating" | "data" | "indexes" | "views" | "archive" | "done";
|
||||
collection: string | null;
|
||||
collectionsDone: number;
|
||||
collectionsTotal: number;
|
||||
documentsRead: number;
|
||||
documentsWritten: number;
|
||||
documentsFailed: number;
|
||||
indexesCreated: number;
|
||||
elapsedMs: number;
|
||||
errorMessage: string | null;
|
||||
filePath: string | null;
|
||||
bytesProcessed?: number;
|
||||
bytesTotal?: number;
|
||||
documentsValidated?: number;
|
||||
}
|
||||
|
||||
export interface MongoRestoreUpload {
|
||||
files: File[];
|
||||
gzip: boolean;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface MongoSourceReadOptions {
|
||||
signal?: AbortSignal;
|
||||
onUploadProgress?: (loaded: number, total: number) => void;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { invoke as tauriInvoke } from "@tauri-apps/api/core";
|
||||
import type { MongoDumpFormat, MongoDumpSourceInput, MongoDumpCatalog, MongoRestoreSourcePreview, MongoDatabaseDumpRequest, MongoDatabaseRestoreRequest, MongoDatabaseDumpProgress } from "./mongodbDumpTypes";
|
||||
import type { MongoRestoreUpload, MongoSourceReadOptions } from "./mongodbDumpTypes";
|
||||
import { assertUpdateAllowsCommand } from "@/lib/app/updatePreparation";
|
||||
import { collectBrowserSupportInfo } from "@/lib/app/supportInfo";
|
||||
|
||||
@@ -5070,12 +5072,42 @@ export async function releaseTableImportSource(_sourceRef: string): Promise<bool
|
||||
return false;
|
||||
}
|
||||
|
||||
export type MongoImportFormat = "csv" | "json" | "ndjson";
|
||||
export function inspectMongodbDatabaseDump(connectionId: string, database: string): Promise<MongoDumpCatalog> {
|
||||
return invoke("inspect_mongodb_database_dump", { connectionId, database });
|
||||
}
|
||||
export function prepareMongodbRestoreSource(source: MongoDumpSourceInput, format: MongoDumpFormat, gzip: boolean, _options?: MongoSourceReadOptions): Promise<MongoRestoreSourcePreview> {
|
||||
if (typeof source !== "string") throw new Error("Desktop restores require a file or directory path");
|
||||
return invoke("prepare_mongodb_restore_source", { request: { path: source, format, gzip } });
|
||||
}
|
||||
export function releaseMongodbRestoreSource(sourceRef: string): Promise<boolean> {
|
||||
return invoke("release_mongodb_restore_source", { sourceRef });
|
||||
}
|
||||
async function runMongodbDatabaseTask(command: string, request: MongoDatabaseDumpRequest | MongoDatabaseRestoreRequest, onProgress: (progress: MongoDatabaseDumpProgress) => void): Promise<MongoDatabaseDumpProgress> {
|
||||
const unlisten = await listen<MongoDatabaseDumpProgress>("mongo-database-dump-progress", (event) => {
|
||||
if (event.payload.taskId === request.taskId) onProgress(event.payload);
|
||||
});
|
||||
try {
|
||||
return await invoke(command, { request });
|
||||
} finally {
|
||||
unlisten();
|
||||
}
|
||||
}
|
||||
export function dumpMongodbDatabase(request: MongoDatabaseDumpRequest, onProgress: (progress: MongoDatabaseDumpProgress) => void) {
|
||||
return runMongodbDatabaseTask("dump_mongodb_database", request, onProgress);
|
||||
}
|
||||
export function restoreMongodbDatabase(request: MongoDatabaseRestoreRequest, onProgress: (progress: MongoDatabaseDumpProgress) => void, _upload?: MongoRestoreUpload) {
|
||||
return runMongodbDatabaseTask("restore_mongodb_database", request, onProgress);
|
||||
}
|
||||
export function cancelMongodbDatabaseDump(taskId: string): Promise<boolean> {
|
||||
return invoke("cancel_mongodb_database_dump", { taskId });
|
||||
}
|
||||
|
||||
export type MongoImportFormat = "csv" | "json" | "ndjson" | "bson";
|
||||
export type MongoImportTypeMode = "string" | "auto" | "extendedJson";
|
||||
export type MongoImportInferredType = "boolean" | "integer" | "decimal" | "date" | "objectId" | "object" | "array" | "mixed" | "string";
|
||||
export type MongoImportStatus = "running" | "done" | "error" | "cancelled";
|
||||
export type MongoImportPhase = "preparing" | "parsing" | "writing" | "done";
|
||||
export type MongoExportFormat = "csv" | "ndjson";
|
||||
export type MongoExportFormat = "csv" | "ndjson" | "bson";
|
||||
export type MongoExportStatus = "running" | "done" | "error" | "cancelled";
|
||||
|
||||
export interface MongoImportIssue {
|
||||
@@ -5176,6 +5208,7 @@ export interface MongoExportRequest {
|
||||
collation?: string | null;
|
||||
format: MongoExportFormat;
|
||||
includeHeader?: boolean;
|
||||
gzip?: boolean;
|
||||
filePath: string;
|
||||
executionId?: string | null;
|
||||
}
|
||||
|
||||
@@ -559,6 +559,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
schema?: string;
|
||||
tableName?: string;
|
||||
} | null>(null);
|
||||
const mongoDatabaseDumpSource = ref<{ connectionId: string; database: string; mode: "dump" | "restore" } | null>(null);
|
||||
const mongoImportSource = ref<{
|
||||
connectionId: string;
|
||||
database: string;
|
||||
@@ -9409,6 +9410,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
diagramSource,
|
||||
docsSource,
|
||||
tableImportSource,
|
||||
mongoDatabaseDumpSource,
|
||||
mongoImportSource,
|
||||
mongoImportCompleted,
|
||||
tableDataGenerateSource,
|
||||
|
||||
Reference in new issue
Block a user