diff --git a/apps/desktop/src/components/mongo/MongoDocBrowser.vue b/apps/desktop/src/components/document/DocumentBrowser.vue similarity index 82% rename from apps/desktop/src/components/mongo/MongoDocBrowser.vue rename to apps/desktop/src/components/document/DocumentBrowser.vue index 7b105cb1cc..162964e865 100644 --- a/apps/desktop/src/components/mongo/MongoDocBrowser.vue +++ b/apps/desktop/src/components/document/DocumentBrowser.vue @@ -12,6 +12,7 @@ import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue"; import DataGrid from "@/components/grid/DataGrid.vue"; import * as api from "@/lib/api"; import { clampSearchSplitWidth } from "@/lib/dataGridSearchSplit"; +import { buildDocumentFilterCondition, combineDocumentFilterConditions, currentDocumentFilterJson, defaultDocumentFilterRule, documentFilterModeNeedsValue, documentFilterModeOptions, documentStoreProviderFor, type DocumentFilterMode, type DocumentFilterRule } from "@/lib/documentStoreProvider"; import { normalizeResultPageSize } from "@/lib/paginationPageSize"; import { useSettingsStore } from "@/stores/settingsStore"; import JsonEditNode from "./JsonEditNode.vue"; @@ -60,22 +61,16 @@ const tableFindPaneWidth = ref(null); const isResizingTableSearchSplit = ref(false); let tableSearchSplitStartX = 0; let tableSearchSplitStartWidth = 0; +const documentStoreProvider = computed(() => documentStoreProviderFor(props.databaseType)); const tableFindPaneStyle = computed(() => { if (tableFindPaneWidth.value == null) return {}; return { flex: `0 0 ${tableFindPaneWidth.value}px` }; }); -const documentStoreLabels = computed(() => - props.databaseType === "elasticsearch" - ? { - documentsLabel: "Documents", - queryPreview: `${props.collection}/_search`, - } - : { - documentsLabel: t("mongo.documents", { count: total.value }), - queryPreview: mongoQueryPreview.value, - }, -); +const documentStoreLabels = computed(() => ({ + documentsLabel: documentStoreProvider.value.documentsLabel({ total: total.value, t }), + queryPreview: documentQueryPreview.value, +})); type PendingDelete = { kind: "document"; index: number } | { kind: "field"; index: number; name: string }; type LocalFilterSummary = { @@ -84,28 +79,9 @@ type LocalFilterSummary = { values: string[]; hiddenValueCount: number; }; -type MongoFilterMode = "equals" | "not-equals" | "like" | "not-like" | "greater-than" | "less-than" | "is-null" | "is-not-null"; -type MongoFilterRule = { - id: string; - fieldName: string; - mode: MongoFilterMode; - rawValue: string; - conjunction: "AND" | "OR"; -}; - -const mongoFilterModeOptions: Array<{ value: MongoFilterMode; labelKey: string }> = [ - { value: "equals", labelKey: "grid.filterBuilderEquals" }, - { value: "not-equals", labelKey: "grid.filterBuilderNotEquals" }, - { value: "like", labelKey: "grid.filterBuilderContains" }, - { value: "not-like", labelKey: "grid.filterBuilderNotContains" }, - { value: "greater-than", labelKey: "grid.filterBuilderGreaterThan" }, - { value: "less-than", labelKey: "grid.filterBuilderLessThan" }, - { value: "is-null", labelKey: "grid.filterBuilderIsNull" }, - { value: "is-not-null", labelKey: "grid.filterBuilderIsNotNull" }, -]; -const mongoFilterBuilderOpen = ref(false); -const mongoFilterRules = ref([]); -const appliedMongoFilter = ref | null>(null); +const documentFilterBuilderOpen = ref(false); +const documentFilterRules = ref([]); +const appliedDocumentFilter = ref | null>(null); const pendingDelete = ref(null); @@ -165,139 +141,77 @@ const gridResult = computed(() => { return { columns, rows, affected_rows: 0, execution_time_ms: 0, truncated: false }; }); -const mongoFilterFieldOptions = computed(() => gridResult.value.columns); -const mongoStructuredFilterCount = computed(() => (appliedMongoFilter.value ? 1 : 0)); +const documentFilterFieldOptions = computed(() => gridResult.value.columns); +const documentStructuredFilterCount = computed(() => (appliedDocumentFilter.value ? 1 : 0)); -function defaultMongoFilterRule(): MongoFilterRule { - return { - id: uuid(), - fieldName: mongoFilterFieldOptions.value[0] ?? "", - mode: "equals", - rawValue: "", - conjunction: "AND", - }; +function createDocumentFilterRule(): DocumentFilterRule { + return defaultDocumentFilterRule(uuid(), documentFilterFieldOptions.value[0] ?? ""); } -function ensureMongoFilterRule() { - if (mongoFilterRules.value.length === 0 && mongoFilterFieldOptions.value.length > 0) { - mongoFilterRules.value = [defaultMongoFilterRule()]; +function ensureDocumentFilterRule() { + if (documentFilterRules.value.length === 0 && documentFilterFieldOptions.value.length > 0) { + documentFilterRules.value = [createDocumentFilterRule()]; } } -function addMongoFilterRule() { - ensureMongoFilterRule(); - mongoFilterRules.value = [...mongoFilterRules.value, defaultMongoFilterRule()]; +function addDocumentFilterRule() { + ensureDocumentFilterRule(); + documentFilterRules.value = [...documentFilterRules.value, createDocumentFilterRule()]; } -function removeMongoFilterRule(ruleId: string) { - mongoFilterRules.value = mongoFilterRules.value.filter((rule) => rule.id !== ruleId); - if (mongoFilterRules.value.length === 0) appliedMongoFilter.value = null; +function removeDocumentFilterRule(ruleId: string) { + documentFilterRules.value = documentFilterRules.value.filter((rule) => rule.id !== ruleId); + if (documentFilterRules.value.length === 0) appliedDocumentFilter.value = null; } -function updateMongoFilterRule(ruleId: string, patch: Partial) { - mongoFilterRules.value = mongoFilterRules.value.map((rule) => { +function updateDocumentFilterRule(ruleId: string, patch: Partial) { + documentFilterRules.value = documentFilterRules.value.map((rule) => { if (rule.id !== ruleId) return rule; const next = { ...rule, ...patch }; - if (!mongoFilterModeNeedsValue(next.mode)) next.rawValue = ""; + if (!documentFilterModeNeedsValue(next.mode)) next.rawValue = ""; return next; }); } -function resetMongoFilterBuilder() { - appliedMongoFilter.value = null; - mongoFilterRules.value = mongoFilterFieldOptions.value.length > 0 ? [defaultMongoFilterRule()] : []; +function resetDocumentFilterBuilder() { + appliedDocumentFilter.value = null; + documentFilterRules.value = documentFilterFieldOptions.value.length > 0 ? [createDocumentFilterRule()] : []; } -function mongoFilterModeNeedsValue(mode: MongoFilterMode): boolean { - return mode !== "is-null" && mode !== "is-not-null"; +function currentDocumentFilter(): string | undefined { + return currentDocumentFilterJson(filterInput.value, appliedDocumentFilter.value); } -function parseMongoFilterValue(raw: string): unknown { - const trimmed = raw.trim(); - if (!trimmed) return ""; - try { - return JSON.parse(trimmed); - } catch { - return trimmed; - } -} - -function mongoConditionForRule(rule: MongoFilterRule): Record | null { - if (!rule.fieldName) return null; - if (mongoFilterModeNeedsValue(rule.mode) && !rule.rawValue.trim()) return null; - const value = mongoFilterModeNeedsValue(rule.mode) ? parseMongoFilterValue(rule.rawValue) : null; - switch (rule.mode) { - case "equals": - return { [rule.fieldName]: value }; - case "not-equals": - return { [rule.fieldName]: { $ne: value } }; - case "like": - return { [rule.fieldName]: { $regex: String(value), $options: "i" } }; - case "not-like": - return { [rule.fieldName]: { $not: { $regex: String(value), $options: "i" } } }; - case "greater-than": - return { [rule.fieldName]: { $gt: value } }; - case "less-than": - return { [rule.fieldName]: { $lt: value } }; - case "is-null": - return { [rule.fieldName]: null }; - case "is-not-null": - return { [rule.fieldName]: { $ne: null } }; - } -} - -function combineMongoConditions(conditions: Record[], rules: MongoFilterRule[]): Record | null { - if (conditions.length === 0) return null; - let result = conditions[0]; - for (let i = 1; i < conditions.length; i++) { - const operator = rules[i]?.conjunction === "OR" ? "$or" : "$and"; - result = { [operator]: [result, conditions[i]] }; - } - return result; -} - -function parseMongoFilterInput(): Record { - const trimmed = filterInput.value.trim(); - if (!trimmed) return {}; - const parsed = JSON.parse(trimmed); - return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record) : {}; -} - -function currentMongoFilter(): string | undefined { - const manual = parseMongoFilterInput(); - const structured = appliedMongoFilter.value; - const filter = structured ? (Object.keys(manual).length ? { $and: [manual, structured] } : structured) : manual; - return Object.keys(filter).length ? JSON.stringify(filter) : undefined; -} - -const mongoQueryPreview = computed(() => { +const documentQueryPreview = computed(() => { let filter = "{}"; try { - filter = currentMongoFilter() ?? "{}"; + filter = currentDocumentFilter() ?? "{}"; } catch { filter = filterInput.value.trim() || "{}"; } - const sort = sortInput.value.trim(); - const parts = [`db.${props.collection}.find(${filter})`]; - if (sort) parts.push(`.sort(${sort})`); - parts.push(`.skip(${page.value * pageSize.value}).limit(${pageSize.value})`); - return parts.join(""); + return documentStoreProvider.value.queryPreview({ + collection: props.collection, + filterJson: filter, + sortJson: sortInput.value.trim(), + skip: page.value * pageSize.value, + limit: pageSize.value, + }); }); -async function applyMongoStructuredFilters() { - const items = mongoFilterRules.value.map((rule) => ({ rule, condition: mongoConditionForRule(rule) })).filter((item): item is { rule: MongoFilterRule; condition: Record } => !!item.condition); - const structured = combineMongoConditions( +async function applyDocumentStructuredFilters() { + const items = documentFilterRules.value.map((rule) => ({ rule, condition: buildDocumentFilterCondition(rule) })).filter((item): item is { rule: DocumentFilterRule; condition: Record } => !!item.condition); + const structured = combineDocumentFilterConditions( items.map((item) => item.condition), items.map((item) => item.rule), ); - appliedMongoFilter.value = structured; - mongoFilterBuilderOpen.value = false; + appliedDocumentFilter.value = structured; + documentFilterBuilderOpen.value = false; applyFilter(); } -function clearMongoFilters(clearLocalFilter?: (columnIndex?: number) => void) { - appliedMongoFilter.value = null; - resetMongoFilterBuilder(); +function clearDocumentFilters(clearLocalFilter?: (columnIndex?: number) => void) { + appliedDocumentFilter.value = null; + resetDocumentFilterBuilder(); clearLocalFilter?.(); applyFilter(); } @@ -346,9 +260,9 @@ async function load() { loading.value = true; error.value = ""; try { - const filter = currentMongoFilter(); + const filter = currentDocumentFilter(); const sort = sortInput.value.trim() || undefined; - const result = await api.mongoFindDocuments(props.connectionId, props.database, props.collection, page.value * pageSize.value, pageSize.value, filter, sort); + const result = await api.documentFindDocuments(props.connectionId, props.database, props.collection, page.value * pageSize.value, pageSize.value, filter, sort); const nextDocuments = result.documents.map(asRecord); documents.value = nextDocuments; if (nextDocuments.length > 0) { @@ -382,11 +296,7 @@ function paginate(offset: number, limit: number) { } function onSort(column: string, _columnIndex: number, direction: "asc" | "desc" | null) { - if (direction) { - sortInput.value = JSON.stringify({ [column]: direction === "asc" ? 1 : -1 }); - } else { - sortInput.value = ""; - } + sortInput.value = documentStoreProvider.value.sortInputForColumn(column, direction); page.value = 0; load(); } @@ -791,24 +701,24 @@ function resetTableSearchSplitWidth() {