From c7c71850d4ef95c4ad9e349d1c7c45fa76da0b52 Mon Sep 17 00:00:00 2001 From: Doracoin Date: Thu, 11 Jun 2026 17:29:40 +0800 Subject: [PATCH 01/27] fix(windows-build): ssh-agent err when building on windows --- Cargo.lock | 1 + crates/dbx-core/Cargo.toml | 1 + crates/dbx-core/src/db/ssh_tunnel.rs | 9 +++++++++ crates/dbx-core/src/plugins.rs | 1 + 4 files changed, 12 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 7bf0316379..c2320b812f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1906,6 +1906,7 @@ dependencies = [ "log", "mongodb", "mysql_async", + "pageant", "percent-encoding", "portpicker", "rayon", diff --git a/crates/dbx-core/Cargo.toml b/crates/dbx-core/Cargo.toml index 717298b8ca..1e7b2e75d9 100644 --- a/crates/dbx-core/Cargo.toml +++ b/crates/dbx-core/Cargo.toml @@ -39,6 +39,7 @@ futures = "0.3" iana-time-zone = "0.1" mongodb = "3.2.5" russh = "0.60" +pageant = "0.2" portpicker = "0.1.1" csv = "1" calamine = "0.30.1" diff --git a/crates/dbx-core/src/db/ssh_tunnel.rs b/crates/dbx-core/src/db/ssh_tunnel.rs index fd87791d19..2b2f865422 100644 --- a/crates/dbx-core/src/db/ssh_tunnel.rs +++ b/crates/dbx-core/src/db/ssh_tunnel.rs @@ -112,6 +112,7 @@ async fn try_authenticate_with_agent( ssh_user: &str, connect_timeout: &Duration, ) -> Result<(), String> { + #[cfg(unix)] let mut agent = match AgentClient::connect_env().await { Ok(a) => a, Err(e) => { @@ -119,6 +120,14 @@ async fn try_authenticate_with_agent( } }; + #[cfg(windows)] + let mut agent = { + let stream = pageant::PageantStream::new() + .await + .map_err(|e| format!("No SSH password or key provided, and ssh-agent (Pageant) is unavailable: {e}"))?; + AgentClient::connect(stream) + }; + let identities = match agent.request_identities().await { Ok(ids) if ids.is_empty() => { return Err("No SSH password or key provided, and ssh-agent has no identities".to_string()); diff --git a/crates/dbx-core/src/plugins.rs b/crates/dbx-core/src/plugins.rs index ecc80fb1f3..9d7d40761e 100644 --- a/crates/dbx-core/src/plugins.rs +++ b/crates/dbx-core/src/plugins.rs @@ -433,6 +433,7 @@ fn spawn_plugin_child(plugin: &InstalledPlugin, env: &PluginRuntimeEnv) -> Resul #[cfg(windows)] { + #[allow(unused_imports)] use std::os::windows::process::CommandExt; command.creation_flags(CREATE_NO_WINDOW); } From edeee4f7d99a73ae51883e59e359886367264028 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 11 Jun 2026 18:09:41 +0800 Subject: [PATCH 02/27] fix(mongo): preserve bson date fields --- crates/dbx-core/src/db/mongo_driver.rs | 138 ++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 5 deletions(-) diff --git a/crates/dbx-core/src/db/mongo_driver.rs b/crates/dbx-core/src/db/mongo_driver.rs index cf030b60d4..1028bb9550 100644 --- a/crates/dbx-core/src/db/mongo_driver.rs +++ b/crates/dbx-core/src/db/mongo_driver.rs @@ -1,5 +1,5 @@ use mongodb::{ - bson::{doc, oid::ObjectId, Bson, Document}, + bson::{doc, oid::ObjectId, Bson, DateTime, Document}, options::ClientOptions, Client, }; @@ -181,7 +181,8 @@ pub async fn insert_document( collection: &str, doc_json: &str, ) -> Result { - let doc: Document = serde_json::from_str(doc_json).map_err(|e| format!("Invalid JSON: {e}"))?; + let value: serde_json::Value = serde_json::from_str(doc_json).map_err(|e| format!("Invalid JSON: {e}"))?; + let doc = json_object_to_document(&value).map_err(|e| format!("Invalid document: {e}"))?; let col = client.database(database).collection::(collection); let result = col.insert_one(doc).await.map_err(|e| e.to_string())?; Ok(format!("{}", result.inserted_id)) @@ -216,10 +217,13 @@ pub async fn update_document( id: &str, doc_json: &str, ) -> Result { - let mut new_doc: Document = serde_json::from_str(doc_json).map_err(|e| format!("Invalid JSON: {e}"))?; - new_doc.remove("_id"); + let value: serde_json::Value = serde_json::from_str(doc_json).map_err(|e| format!("Invalid JSON: {e}"))?; let col = client.database(database).collection::(collection); for filter in document_id_filters(id) { + let current = col.find_one(filter.clone()).await.map_err(|e| e.to_string())?; + let mut new_doc = json_object_to_document_preserving_existing(&value, current.as_ref()) + .map_err(|e| format!("Invalid document: {e}"))?; + new_doc.remove("_id"); let result = col.replace_one(filter, new_doc.clone()).await.map_err(|e| e.to_string())?; if result.matched_count > 0 { return Ok(result.modified_count); @@ -298,7 +302,10 @@ fn bson_to_json(bson: &Bson) -> serde_json::Value { Bson::Int32(v) => serde_json::json!(v), Bson::Int64(v) => serde_json::json!(v), Bson::ObjectId(oid) => serde_json::Value::String(oid.to_hex()), - Bson::DateTime(dt) => serde_json::Value::String(dt.to_string()), + Bson::DateTime(dt) => serde_json::Value::String(format!( + "ISODate(\"{}\")", + dt.try_to_rfc3339_string().unwrap_or_else(|_| dt.to_string()) + )), Bson::Array(arr) => serde_json::Value::Array(arr.iter().map(bson_to_json).collect()), Bson::Document(doc) => { let mut map = serde_json::Map::new(); @@ -320,6 +327,25 @@ pub fn json_object_to_document(value: &serde_json::Value) -> Result, +) -> Result { + match (value, existing) { + (serde_json::Value::Object(obj), Some(existing)) => Ok(obj + .iter() + .map(|(key, value)| { + let bson = existing + .get(key) + .map(|existing_bson| json_value_to_bson_preserving_existing(value, existing_bson)) + .unwrap_or_else(|| json_value_to_bson(value)); + (key.clone(), bson) + }) + .collect()), + _ => json_object_to_document(value), + } +} + pub fn json_filter_to_document(value: &serde_json::Value) -> Result { match json_filter_value_to_bson(value, None) { Bson::Document(doc) => Ok(doc), @@ -327,6 +353,39 @@ pub fn json_filter_to_document(value: &serde_json::Value) -> Result Bson { + if &bson_to_json(existing) == value { + return existing.clone(); + } + + match (value, existing) { + (serde_json::Value::Array(values), Bson::Array(existing_values)) => Bson::Array( + values + .iter() + .enumerate() + .map(|(index, item)| { + existing_values + .get(index) + .map(|existing_item| json_value_to_bson_preserving_existing(item, existing_item)) + .unwrap_or_else(|| json_value_to_bson(item)) + }) + .collect(), + ), + (serde_json::Value::Object(obj), Bson::Document(existing_doc)) => Bson::Document( + obj.iter() + .map(|(key, item)| { + let bson = existing_doc + .get(key) + .map(|existing_item| json_value_to_bson_preserving_existing(item, existing_item)) + .unwrap_or_else(|| json_value_to_bson(item)); + (key.clone(), bson) + }) + .collect(), + ), + _ => json_value_to_bson(value), + } +} + fn json_value_to_bson(value: &serde_json::Value) -> Bson { match value { serde_json::Value::Null => Bson::Null, @@ -350,6 +409,9 @@ fn json_value_to_bson(value: &serde_json::Value) -> Bson { return Bson::ObjectId(oid); } } + if let Some(date) = parse_extended_json_date(obj) { + return Bson::DateTime(date); + } } let doc: Document = obj.iter().map(|(k, v)| (k.clone(), json_value_to_bson(v))).collect(); Bson::Document(doc) @@ -357,6 +419,19 @@ fn json_value_to_bson(value: &serde_json::Value) -> Bson { } } +fn parse_extended_json_date(obj: &serde_json::Map) -> Option { + match obj.get("$date")? { + serde_json::Value::String(value) => DateTime::parse_rfc3339_str(value).ok(), + serde_json::Value::Number(value) => value.as_i64().map(DateTime::from_millis), + serde_json::Value::Object(inner) if inner.len() == 1 => match inner.get("$numberLong") { + Some(serde_json::Value::String(value)) => value.parse::().ok().map(DateTime::from_millis), + Some(serde_json::Value::Number(value)) => value.as_i64().map(DateTime::from_millis), + _ => None, + }, + _ => None, + } +} + fn json_filter_value_to_bson(value: &serde_json::Value, field_name: Option<&str>) -> Bson { if field_name == Some("_id") { if let Some(id) = value.as_str() { @@ -510,4 +585,57 @@ mod tests { assert!(matches!(doc.get("owner_id"), Some(Bson::String(value)) if value == id)); } + + #[test] + fn bson_to_json_displays_date_as_mongo_isodate() { + let date = DateTime::parse_rfc3339_str("2026-06-10T13:59:31.287Z").unwrap(); + let value = bson_to_json(&Bson::DateTime(date)); + + assert_eq!(value, serde_json::json!("ISODate(\"2026-06-10T13:59:31.287Z\")")); + } + + #[test] + fn json_object_to_document_parses_extended_json_date() { + let value = serde_json::json!({ + "created_at": { "$date": "2026-06-10T13:59:31.287Z" }, + "updated_at": { "$date": { "$numberLong": "1781099971287" } } + }); + let doc = json_object_to_document(&value).unwrap(); + + assert!(matches!(doc.get("created_at"), Some(Bson::DateTime(_)))); + assert!(matches!( + doc.get("updated_at"), + Some(Bson::DateTime(value)) if value.timestamp_millis() == 1_781_099_971_287 + )); + } + + #[test] + fn json_object_to_document_preserves_unchanged_bson_date_fields() { + let date = DateTime::parse_rfc3339_str("2026-06-10T13:59:31.287Z").unwrap(); + let existing = doc! { + "_id": "doc-1", + "created_at": Bson::DateTime(date), + "name": "before", + "profile": { + "last_seen": Bson::DateTime(date), + "status": "old", + }, + }; + let displayed = bson_to_json(&Bson::Document(existing.clone())); + let mut edited = displayed.as_object().cloned().unwrap(); + edited.insert("name".to_string(), serde_json::json!("after")); + if let Some(serde_json::Value::Object(profile)) = edited.get_mut("profile") { + profile.insert("status".to_string(), serde_json::json!("new")); + } + + let doc = + json_object_to_document_preserving_existing(&serde_json::Value::Object(edited), Some(&existing)).unwrap(); + + assert!(matches!(doc.get("created_at"), Some(Bson::DateTime(value)) if *value == date)); + let Some(Bson::Document(profile)) = doc.get("profile") else { + panic!("expected profile document"); + }; + assert!(matches!(profile.get("last_seen"), Some(Bson::DateTime(value)) if *value == date)); + assert!(matches!(profile.get("status"), Some(Bson::String(value)) if value == "new")); + } } From 3f25a69d233f3024846fd9f1600101cc75fa1a05 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 11 Jun 2026 18:34:51 +0800 Subject: [PATCH 03/27] feat(mysql): add unsigned integer types to column type options --- apps/desktop/src/lib/tableStructureEditorState.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/desktop/src/lib/tableStructureEditorState.ts b/apps/desktop/src/lib/tableStructureEditorState.ts index 1702da4f2d..d050b57e50 100644 --- a/apps/desktop/src/lib/tableStructureEditorState.ts +++ b/apps/desktop/src/lib/tableStructureEditorState.ts @@ -4,11 +4,17 @@ import type { ColumnExtra, EditableStructureColumn, EditableStructureIndex } fro export const DATA_TYPE_OPTIONS: Record = { mysql: [ "tinyint", + "tinyint unsigned", "smallint", + "smallint unsigned", "mediumint", + "mediumint unsigned", "int", + "int unsigned", "integer", + "integer unsigned", "bigint", + "bigint unsigned", "float", "double", "double precision", @@ -256,12 +262,18 @@ export function getDataTypeOptions(dbType: DatabaseType | undefined): string[] { export const DEFAULT_TYPE_LENGTHS: Record = { tinyint: "4", + "tinyint unsigned": "4", smallint: "6", + "smallint unsigned": "6", mediumint: "9", + "mediumint unsigned": "9", int: "11", + "int unsigned": "11", integer: "11", + "integer unsigned": "11", int4: "11", bigint: "20", + "bigint unsigned": "20", int8: "20", float: "10,2", real: "10,2", From 7923f4915e4a117ed575d1dd86494fec51833e9d Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 11 Jun 2026 18:56:38 +0800 Subject: [PATCH 04/27] fix(elasticsearch): apply document filters in browser --- .../DocumentBrowser.vue} | 236 ++++--------- .../{mongo => document}/JsonEditNode.vue | 0 .../src/components/layout/ContentArea.vue | 4 +- apps/desktop/src/lib/api.ts | 1 + apps/desktop/src/lib/documentStoreProvider.ts | 258 +++++++++++++++ apps/desktop/src/lib/http.ts | 4 + apps/desktop/src/lib/tauri.ts | 4 + .../dbx-core/src/db/elasticsearch_driver.rs | 313 +++++++++++++++++- crates/dbx-core/src/mongo_ops.rs | 17 +- crates/dbx-web/src/main.rs | 1 + crates/dbx-web/src/routes/mongo.rs | 19 ++ .../app-tests/documentStoreProvider.test.ts | 70 ++++ src-tauri/src/commands/mongo_cmd.rs | 25 ++ src-tauri/src/lib.rs | 1 + 14 files changed, 780 insertions(+), 173 deletions(-) rename apps/desktop/src/components/{mongo/MongoDocBrowser.vue => document/DocumentBrowser.vue} (82%) rename apps/desktop/src/components/{mongo => document}/JsonEditNode.vue (100%) create mode 100644 apps/desktop/src/lib/documentStoreProvider.ts create mode 100644 packages/app-tests/documentStoreProvider.test.ts 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() {