From 4bd3b4a9581cd94b2fe79d60fe6f6c29e2d6eb0d Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 15 Nov 2026 18:47:58 +0300 Subject: [PATCH] feat(rust): MongoDB adapter with connect, list, and find MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the official mongodb crate so Tauri can open a session, ping, list databases/collections, and find documents with a limit — same IPC shape as Redis for the upcoming browser stub. Closes #1893 --- apps/tauri/src-tauri/Cargo.lock | 822 +++++++++++++++++- apps/tauri/src-tauri/Cargo.toml | 1 + apps/tauri/src-tauri/src/commands.rs | 203 ++++- apps/tauri/src-tauri/src/connection/id.rs | 4 +- .../tauri/src-tauri/src/connection/manager.rs | 64 +- apps/tauri/src-tauri/src/connection/mod.rs | 2 + apps/tauri/src-tauri/src/connection/mongo.rs | 388 +++++++++ .../src-tauri/src/connection/sql_guard.rs | 5 + apps/tauri/src-tauri/src/lib.rs | 22 +- apps/tauri/src-tauri/src/profiles/store.rs | 25 + apps/tauri/src-tauri/src/staging/ipc.rs | 3 + 11 files changed, 1493 insertions(+), 46 deletions(-) create mode 100644 apps/tauri/src-tauri/src/connection/mongo.rs diff --git a/apps/tauri/src-tauri/Cargo.lock b/apps/tauri/src-tauri/Cargo.lock index 06e4b9c8..d3b212dd 100644 --- a/apps/tauri/src-tauri/Cargo.lock +++ b/apps/tauri/src-tauri/Cargo.lock @@ -15,6 +15,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -339,6 +340,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -348,6 +361,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -424,6 +446,29 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "bson" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969a9ba84b0ff843813e7249eed1678d9b6607ce5a3b8f0a47af3fcf7978e6e" +dependencies = [ + "ahash", + "base64 0.22.1", + "bitvec", + "getrandom 0.2.17", + "getrandom 0.3.4", + "hex", + "indexmap 2.14.2", + "js-sys", + "once_cell", + "rand 0.9.5", + "serde", + "serde_bytes", + "serde_json", + "time", + "uuid", +] + [[package]] name = "bumpalo" version = "3.20.4" @@ -568,6 +613,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.45" @@ -580,6 +636,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "combine" version = "4.6.9" @@ -609,6 +671,32 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be95d4a884358a21a199689323c29e9256b4a1b315e22f183d155422041d3a9a" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + [[package]] name = "cookie" version = "0.18.2" @@ -678,6 +766,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -702,6 +799,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.17" @@ -711,6 +814,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.14" @@ -726,6 +838,12 @@ version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -736,6 +854,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cssparser" version = "0.37.0" @@ -781,6 +908,15 @@ dependencies = [ "link-section", ] +[[package]] +name = "ctutils" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261af4dc7a06ea8a527a8717d8cbba3417530d6ea14e1edb3de6b67806be474f" +dependencies = [ + "cmov", +] + [[package]] name = "darling" version = "0.24.1" @@ -815,6 +951,12 @@ dependencies = [ "syn 3.0.10", ] +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + [[package]] name = "dbus" version = "0.9.12" @@ -842,7 +984,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] @@ -856,6 +998,28 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive-syn-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive-where" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e2b94854e8576378ccda7c8de8a66ed8b4e8acbd2c50ec3418ea6c8aaf4b567" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -883,12 +1047,24 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.3", + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "dirs" version = "5.0.1" @@ -1282,6 +1458,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures-channel" version = "0.3.34" @@ -1494,8 +1676,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1505,9 +1689,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1519,6 +1705,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", ] [[package]] @@ -1743,13 +1930,83 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hickory-net" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c480823ed7c2c5d0f09c41020cb6b7c28029ce60ec42dc942158dcf22f8e0a4d" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "futures-channel", + "futures-io", + "futures-util", + "hickory-proto", + "idna", + "ipnet", + "jni 0.22.4", + "rand 0.10.4", + "thiserror 2.0.21", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-proto" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b92608f679a6fa515dd1d15c1ff89443026e391200a2c840c7afcba482893d" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni 0.22.4", + "once_cell", + "prefix-trie", + "rand 0.10.4", + "ring", + "thiserror 2.0.21", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3da5255c95d5a716857d54b5b8f4e8d67c3484d3beaaaae2ce25063b3ba981" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-net", + "hickory-proto", + "ipconfig", + "ipnet", + "jni 0.22.4", + "moka", + "ndk-context", + "once_cell", + "parking_lot", + "rand 0.10.4", + "resolv-conf", + "smallvec", + "system-configuration", + "thiserror 2.0.21", + "tokio", + "tracing", +] + [[package]] name = "hkdf" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -1758,7 +2015,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", ] [[package]] @@ -1830,6 +2096,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.12.0" @@ -2049,11 +2324,27 @@ dependencies = [ "cfb", ] +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2 0.6.5", + "widestring", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", +] + [[package]] name = "ipnet" version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" +dependencies = [ + "serde", +] [[package]] name = "ipnetwork" @@ -2189,6 +2480,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -2416,6 +2737,54 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "macro_magic" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc33f9f0351468d26fbc53d9ce00a096c8522ecb42f19b50f34f2c422f76d21d" +dependencies = [ + "macro_magic_core", + "macro_magic_macros", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "macro_magic_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1687dc887e42f352865a393acae7cf79d98fab6351cde1f58e9e057da89bf150" +dependencies = [ + "const-random", + "derive-syn-parse", + "macro_magic_core_macros", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "macro_magic_core_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "macro_magic_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" +dependencies = [ + "macro_magic_core", + "quote", + "syn 2.0.119", +] + [[package]] name = "markup5ever" version = "0.39.0" @@ -2440,12 +2809,22 @@ dependencies = [ [[package]] name = "md-5" -version = "0.10.6" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "md-5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ "cfg-if", - "digest", + "digest 0.11.3", ] [[package]] @@ -2500,6 +2879,98 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moka" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "mongocrypt" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8426a875ded61430d4a811dbfda7633b6b8af0225c547fc6c28b8b0aa7d79a13" +dependencies = [ + "bson", + "mongocrypt-sys", + "once_cell", + "serde", +] + +[[package]] +name = "mongocrypt-sys" +version = "0.1.6+1.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "851fac73f7fe22f6a3ab87f720ce509cae7c9fd08e7dd27866cc232dee07ccf4" + +[[package]] +name = "mongodb" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea2d64bd31b98421057b171d28cf4b342d39a925fa49bc5d52dea74a1810965" +dependencies = [ + "base64 0.22.1", + "bitflags 2.13.2", + "bson", + "derive-where", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hickory-net", + "hickory-proto", + "hickory-resolver", + "hmac 0.13.0", + "macro_magic", + "md-5 0.11.0", + "mongocrypt", + "mongodb-internal-macros", + "pbkdf2", + "percent-encoding", + "rand 0.9.5", + "rustc_version_runtime", + "rustls", + "serde", + "serde_bytes", + "serde_with", + "sha1 0.11.0", + "sha2 0.11.1", + "socket2 0.6.5", + "stringprep", + "strsim", + "take_mut", + "thiserror 2.0.21", + "tokio", + "tokio-rustls", + "tokio-util", + "typed-builder", + "uuid", + "webpki-roots", +] + +[[package]] +name = "mongodb-internal-macros" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd97b470151aea4f92331b5d3d3803e734777cee86a171ebbd083d6956c4c3dd" +dependencies = [ + "macro_magic", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "muda" version = "0.20.0" @@ -2864,6 +3335,10 @@ name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "open" @@ -2946,6 +3421,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3202,6 +3686,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -3285,6 +3780,7 @@ dependencies = [ "ipnetwork", "keyring", "mac_address", + "mongodb", "redis", "rusqlite", "rust_decimal", @@ -3330,6 +3826,12 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.8" @@ -3351,6 +3853,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6fa277c67e6ea590466f04c3fe82cb509f0864fe470643e5570de9514a9b11" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -3389,6 +3902,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -3561,6 +4080,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + [[package]] name = "rfd" version = "0.17.2" @@ -3587,14 +4112,28 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rsa" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", @@ -3653,6 +4192,16 @@ dependencies = [ "semver", ] +[[package]] +name = "rustc_version_runtime" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" +dependencies = [ + "rustc_version", + "semver", +] + [[package]] name = "rustix" version = "1.1.6" @@ -3666,6 +4215,41 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "102f35f7de7b06d8c61f1cff6592f5253e0dea431c74867aad5dfd70f44663fb" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a0760a887c14823fca5a1c970b01d47c328c696c0c64829d1df50e8f853fd5" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.23" @@ -3848,6 +4432,16 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.229" @@ -3885,6 +4479,8 @@ version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ + "foldhash 0.2.0", + "indexmap 2.14.2", "itoa", "memchr", "serde", @@ -4013,8 +4609,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", ] [[package]] @@ -4030,8 +4637,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", ] [[package]] @@ -4056,7 +4674,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -4066,6 +4684,22 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.4" @@ -4227,7 +4861,7 @@ dependencies = [ "rust_decimal", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "thiserror 2.0.21", "tokio", @@ -4265,7 +4899,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -4288,7 +4922,7 @@ dependencies = [ "bytes", "chrono", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -4298,10 +4932,10 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "once_cell", "percent-encoding", @@ -4309,8 +4943,8 @@ dependencies = [ "rsa", "rust_decimal", "serde", - "sha1", - "sha2", + "sha1 0.10.7", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -4340,20 +4974,20 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "home", "ipnetwork", "itoa", "log", "mac_address", - "md-5", + "md-5 0.10.6", "memchr", "once_cell", "rand 0.8.8", "rust_decimal", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -4548,6 +5182,27 @@ dependencies = [ "syn 3.0.10", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -4561,6 +5216,18 @@ dependencies = [ "version-compare", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + [[package]] name = "tao" version = "0.37.2" @@ -4579,7 +5246,7 @@ dependencies = [ "gdkwayland-sys", "gdkx11-sys", "gtk", - "jni", + "jni 0.21.1", "libc", "log", "ndk", @@ -4612,6 +5279,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target-lexicon" version = "0.12.16" @@ -4635,7 +5308,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", - "jni", + "jni 0.21.1", "libc", "log", "mime", @@ -4709,7 +5382,7 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "syn 2.0.119", "tauri-utils", "thiserror 2.0.21", @@ -4823,7 +5496,7 @@ dependencies = [ "dpi", "gtk", "http", - "jni", + "jni 0.21.1", "objc2", "objc2-ui-kit", "objc2-web-kit", @@ -4846,7 +5519,7 @@ checksum = "e1aa9d8b7a120f879f44fdc1053f7b7dbf416bf0bbc6a72b714dec46b782b52f" dependencies = [ "gtk", "http", - "jni", + "jni 0.21.1", "log", "objc2", "objc2-app-kit", @@ -5004,6 +5677,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.5" @@ -5030,6 +5712,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2 0.6.5", "tokio-macros", "windows-sys 0.61.2", @@ -5046,6 +5729,16 @@ dependencies = [ "syn 3.0.10", ] +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.19" @@ -5065,7 +5758,9 @@ checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", + "futures-util", "libc", "pin-project-lite", "tokio", @@ -5272,6 +5967,26 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-builder" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "typeid" version = "1.0.3" @@ -5328,6 +6043,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -5615,6 +6336,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-roots" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7265c9301c16a8711c992c2d8da90ee77ef54b1ed51482d6dfeb00dc756b1297" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.39.1" @@ -5659,6 +6389,12 @@ dependencies = [ "wasite", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -5789,6 +6525,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -6171,7 +6918,7 @@ dependencies = [ "gtk", "http", "javascriptcore-rs", - "jni", + "jni 0.21.1", "libc", "ndk", "objc2", @@ -6182,7 +6929,7 @@ dependencies = [ "objc2-web-kit", "percent-encoding", "raw-window-handle", - "sha2", + "sha2 0.10.9", "soup3", "tao-macros", "thiserror 2.0.21", @@ -6196,6 +6943,15 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.21.0" diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml index 2291d69c..4e709356 100644 --- a/apps/tauri/src-tauri/Cargo.toml +++ b/apps/tauri/src-tauri/Cargo.toml @@ -44,6 +44,7 @@ ipnetwork = "0.20" mac_address = "1.1" bit-vec = "0.6.3" redis = { version = "0.27", features = ["aio", "tokio-comp"] } +mongodb = "3.10.1" [profile.release] codegen-units = 1 diff --git a/apps/tauri/src-tauri/src/commands.rs b/apps/tauri/src-tauri/src/commands.rs index 71c1221f..db01fcf8 100644 --- a/apps/tauri/src-tauri/src/commands.rs +++ b/apps/tauri/src-tauri/src/commands.rs @@ -1,7 +1,7 @@ use crate::connection::{ ConnectionConfig, ConnectionError, ConnectionId, ConnectionManager, ConnectionRegistry, - ErdSchema, PostgresObjectDetail, RedisKeyMeta, RedisKeyView, RedisScanPage, SchemaObject, - SchemaObjectKind, SqlObjectKind, SqlObjectNode, SqliteOverview, TableSchema, + ErdSchema, MongoFindResult, PostgresObjectDetail, RedisKeyMeta, RedisKeyView, RedisScanPage, + SchemaObject, SchemaObjectKind, SqlObjectKind, SqlObjectNode, SqliteOverview, TableSchema, }; use crate::profiles::{ConnectionProfile, ProfileStore, ProfileStoreError}; use crate::secrets::SharedSecretStore; @@ -41,6 +41,21 @@ pub struct ConnectRedisRequest { pub id: Option, } +/// Mongo save+connect args. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectMongoRequest { + pub name: String, + pub host: String, + pub port: u16, + pub database: String, + pub username: Option, + pub password: Option, + pub auth_source: Option, + pub folder_id: Option, + pub id: Option, +} + #[tauri::command] pub fn db_connect_sqlite( path: String, @@ -110,6 +125,72 @@ pub fn db_connect_redis( }) } +#[tauri::command] +pub fn db_connect_mongo( + host: String, + port: u16, + database: String, + username: Option, + password: Option, + auth_source: Option, + id: Option, + registry: State<'_, ConnectionRegistry>, +) -> Result { + registry.connect(ConnectionConfig::Mongo { + host, + port, + database, + username, + password, + auth_source, + id, + }) +} + +#[tauri::command] +pub fn mongo_ping( + id: ConnectionId, + registry: State<'_, ConnectionRegistry>, +) -> Result { + registry.with_mongo(&id, |s| s.ping()) +} + +#[tauri::command] +pub fn mongo_list_databases( + id: ConnectionId, + registry: State<'_, ConnectionRegistry>, +) -> Result, ConnectionError> { + registry.with_mongo(&id, |s| s.list_databases()) +} + +#[tauri::command] +pub fn mongo_list_collections( + id: ConnectionId, + database: String, + registry: State<'_, ConnectionRegistry>, +) -> Result, ConnectionError> { + registry.with_mongo(&id, |s| s.list_collections(&database)) +} + +#[tauri::command] +pub fn mongo_find( + id: ConnectionId, + database: String, + collection: String, + filter_json: Option, + limit: Option, + registry: State<'_, ConnectionRegistry>, +) -> Result { + registry.with_mongo(&id, |s| { + s.find( + &database, + &collection, + filter_json.as_deref(), + limit, + ) + }) +} + #[tauri::command] pub fn redis_ping( id: ConnectionId, @@ -562,6 +643,124 @@ pub fn profiles_connect_redis( Ok(profile) } +/// Save Mongo profile metadata (no password) and open a live session. +#[tauri::command] +pub fn profiles_connect_mongo( + request: ConnectMongoRequest, + store: State<'_, ProfileStore>, + secrets: State<'_, SharedSecretStore>, + registry: State<'_, ConnectionRegistry>, +) -> Result { + let ConnectMongoRequest { + name, + host, + port, + database, + username, + password, + auth_source, + folder_id, + id, + } = request; + + let database = { + let t = database.trim(); + if t.is_empty() { + "querya".to_owned() + } else { + t.to_owned() + } + }; + let user = username + .as_deref() + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(str::to_owned); + let auth = auth_source + .as_deref() + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(str::to_owned); + + let display_name = { + let t = name.trim(); + if t.is_empty() { + format!("{host}:{port}/{database}") + } else { + t.to_owned() + } + }; + + let mut profile = match id.filter(|s| !s.trim().is_empty()) { + Some(existing_id) => store + .get(&existing_id) + .map_err(|e| ConnectionError::io(e.to_string()))? + .map(|mut p| { + p.name = display_name.clone(); + p.host = Some(host.clone()); + p.port = Some(i64::from(port)); + p.database_name = Some(database.clone()); + p.username = user.clone(); + p.folder_id = folder_id.clone(); + p.driver = "mongo".into(); + p.path = None; + p + }) + .unwrap_or_else(|| { + let mut p = ConnectionProfile::new_mongo( + display_name.clone(), + host.clone(), + port, + database.clone(), + user.clone(), + ); + p.id = existing_id; + p.folder_id = folder_id.clone(); + p + }), + None => { + let mut p = ConnectionProfile::new_mongo( + display_name, + host.clone(), + port, + database.clone(), + user.clone(), + ); + p.folder_id = folder_id; + p + } + }; + profile.touch(); + store + .upsert(&profile) + .map_err(|e| ConnectionError::io(e.to_string()))?; + + let password = match password { + Some(p) => { + secrets + .set_password(&profile.id, &p) + .map_err(|e| ConnectionError::io(format!("keyring write: {e}")))?; + Some(p) + } + None => secrets + .get_password(&profile.id) + .map_err(|e| ConnectionError::io(format!("keyring read: {e}")))?, + }; + + if !registry.is_connected(&ConnectionId::from(profile.id.as_str())) { + registry.connect(ConnectionConfig::Mongo { + host, + port, + database, + username: user, + password, + auth_source: auth, + id: Some(profile.id.clone()), + })?; + } + Ok(profile) +} + #[tauri::command] pub fn db_disconnect( id: ConnectionId, diff --git a/apps/tauri/src-tauri/src/connection/id.rs b/apps/tauri/src-tauri/src/connection/id.rs index 94804e04..1017d903 100644 --- a/apps/tauri/src-tauri/src/connection/id.rs +++ b/apps/tauri/src-tauri/src/connection/id.rs @@ -41,7 +41,7 @@ impl From<&str> for ConnectionId { } } -/// Driver family wired (or stubbed) in this crate. Mongo lands with its epic. +/// Driver family wired (or stubbed) in this crate. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum DriverKind { @@ -49,6 +49,7 @@ pub enum DriverKind { Postgres, Mysql, Redis, + Mongo, } impl DriverKind { @@ -58,6 +59,7 @@ impl DriverKind { Self::Postgres => "postgres", Self::Mysql => "mysql", Self::Redis => "redis", + Self::Mongo => "mongo", } } } diff --git a/apps/tauri/src-tauri/src/connection/manager.rs b/apps/tauri/src-tauri/src/connection/manager.rs index 5a8d64c8..217e33be 100644 --- a/apps/tauri/src-tauri/src/connection/manager.rs +++ b/apps/tauri/src-tauri/src/connection/manager.rs @@ -1,5 +1,6 @@ use super::error::{ConnectionError, ConnectionResult}; use super::id::{ConnectionId, DriverKind}; +use super::mongo::MongoSession; use super::mysql::MysqlSession; use super::postgres::PostgresSession; use super::query::QueryEvent; @@ -55,6 +56,19 @@ pub enum ConnectionConfig { #[serde(default)] id: Option, }, + Mongo { + host: String, + port: u16, + database: String, + #[serde(default)] + username: Option, + #[serde(default)] + password: Option, + #[serde(default)] + auth_source: Option, + #[serde(default)] + id: Option, + }, } impl ConnectionConfig { @@ -64,6 +78,7 @@ impl ConnectionConfig { Self::Postgres { .. } => DriverKind::Postgres, Self::Mysql { .. } => DriverKind::Mysql, Self::Redis { .. } => DriverKind::Redis, + Self::Mongo { .. } => DriverKind::Mongo, } } } @@ -105,7 +120,7 @@ impl ConnectionRegistry { Self::with_builtin_drivers() } - /// Registry with SQLite + Postgres + MySQL + Redis openers. + /// Registry with SQLite + Postgres + MySQL + Redis + Mongo openers. pub fn with_builtin_drivers() -> Self { Self::with_opener(|config| match config { ConnectionConfig::Sqlite { path, id } => { @@ -171,6 +186,26 @@ impl ConnectionRegistry { })?; Ok(Arc::new(session) as SessionArc) } + ConnectionConfig::Mongo { + host, + port, + database, + username, + password, + auth_source, + id, + } => { + let session = super::mongo::open_mongo(super::mongo::MongoConnectParams { + host, + port, + database, + username, + password, + auth_source, + id: id.filter(|s| !s.trim().is_empty()).map(ConnectionId::from), + })?; + Ok(Arc::new(session) as SessionArc) + } }) } @@ -272,6 +307,21 @@ impl ConnectionRegistry { f(redis) } + pub fn with_mongo( + &self, + id: &ConnectionId, + f: impl FnOnce(&MongoSession) -> ConnectionResult, + ) -> ConnectionResult { + let session = self.session_arc(id)?; + let mongo = session + .as_any() + .downcast_ref::() + .ok_or(ConnectionError::UnsupportedDriver { + driver: session.driver(), + })?; + f(mongo) + } + /// Driver-aware query stream (SQLite + Postgres + MySQL). /// /// `confirmed` must be true for DROP / TRUNCATE / unconditional DELETE|UPDATE @@ -288,7 +338,7 @@ impl ConnectionRegistry { let driver = self .driver_of(id) .ok_or_else(|| ConnectionError::NotFound { id: id.clone() })?; - if matches!(driver, DriverKind::Redis) { + if matches!(driver, DriverKind::Redis | DriverKind::Mongo) { return Err(ConnectionError::UnsupportedDriver { driver }); } super::sql_guard::assert_query_allowed(sql, driver, confirmed)?; @@ -298,7 +348,9 @@ impl ConnectionRegistry { self.with_postgres(id, |s| s.execute_streaming(sql, limit, emit)) } DriverKind::Mysql => self.with_mysql(id, |s| s.execute_streaming(sql, limit, emit)), - DriverKind::Redis => Err(ConnectionError::UnsupportedDriver { driver }), + DriverKind::Redis | DriverKind::Mongo => { + Err(ConnectionError::UnsupportedDriver { driver }) + } } } @@ -356,6 +408,9 @@ impl ConnectionRegistry { DriverKind::Redis => Err(ConnectionError::UnsupportedDriver { driver: DriverKind::Redis, }), + DriverKind::Mongo => Err(ConnectionError::UnsupportedDriver { + driver: DriverKind::Mongo, + }), } } @@ -403,6 +458,9 @@ impl ConnectionRegistry { DriverKind::Redis => Err(ConnectionError::UnsupportedDriver { driver: DriverKind::Redis, }), + DriverKind::Mongo => Err(ConnectionError::UnsupportedDriver { + driver: DriverKind::Mongo, + }), } } } diff --git a/apps/tauri/src-tauri/src/connection/mod.rs b/apps/tauri/src-tauri/src/connection/mod.rs index d0f8302c..2eeb9fa6 100644 --- a/apps/tauri/src-tauri/src/connection/mod.rs +++ b/apps/tauri/src-tauri/src/connection/mod.rs @@ -15,6 +15,7 @@ mod postgres_object_detail; mod postgres_object_tree; pub(crate) mod postgres_query; mod query; +pub(crate) mod mongo; pub(crate) mod redis; pub(crate) mod runtime; mod smoke_slice; @@ -33,5 +34,6 @@ pub use manager::{ConnectionConfig, ConnectionManager, ConnectionRegistry}; pub use object_tree::{ObjectTreeProvider, SqlObjectKind, SqlObjectNode}; pub use postgres_object_detail::PostgresObjectDetail; pub use query::QueryEvent; +pub use mongo::MongoFindResult; pub use redis::{RedisKeyMeta, RedisKeyView, RedisScanPage}; pub use sqlite_introspect::SqliteOverview; diff --git a/apps/tauri/src-tauri/src/connection/mongo.rs b/apps/tauri/src-tauri/src/connection/mongo.rs new file mode 100644 index 00000000..64f305d0 --- /dev/null +++ b/apps/tauri/src-tauri/src/connection/mongo.rs @@ -0,0 +1,388 @@ +//! MongoDB driver via the official `mongodb` crate (connect + list + find). + +use super::error::{ConnectionError, ConnectionResult}; +use super::id::{ConnectionId, DriverKind}; +use super::manager::ConnectionSession; +use super::runtime; +use futures_util::TryStreamExt; +use mongodb::bson::{doc, Document}; +use mongodb::options::{ClientOptions, FindOptions}; +use mongodb::Client; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +/// Flutter `kMongoDefaultFindLimit` parity. +pub const DEFAULT_FIND_LIMIT: i64 = 1000; + +pub struct MongoSession { + id: ConnectionId, + host: String, + port: u16, + database: String, + client: Arc>>, +} + +impl MongoSession { + pub fn host(&self) -> &str { + &self.host + } + + pub fn port(&self) -> u16 { + self.port + } + + pub fn database(&self) -> &str { + &self.database + } + + pub fn clone_handle(&self) -> Self { + Self { + id: self.id.clone(), + host: self.host.clone(), + port: self.port, + database: self.database.clone(), + client: Arc::clone(&self.client), + } + } + + fn with_client( + &self, + f: impl FnOnce(&Client) -> ConnectionResult, + ) -> ConnectionResult { + let guard = self + .client + .lock() + .map_err(|_| ConnectionError::io("mongo client lock poisoned"))?; + let client = guard + .as_ref() + .ok_or_else(|| ConnectionError::Disconnected { + id: self.id.clone(), + })?; + f(client) + } + + pub fn ping(&self) -> ConnectionResult { + self.with_client(|client| { + runtime::block_on(async { + client + .database("admin") + .run_command(doc! { "ping": 1 }) + .await + .map_err(|e| ConnectionError::driver(DriverKind::Mongo, e.to_string()))?; + Ok("ok".into()) + }) + }) + } + + pub fn list_databases(&self) -> ConnectionResult> { + self.with_client(|client| { + runtime::block_on(async { + client + .list_database_names() + .await + .map_err(|e| ConnectionError::driver(DriverKind::Mongo, e.to_string())) + }) + }) + } + + pub fn list_collections(&self, database: &str) -> ConnectionResult> { + let db = database.trim(); + if db.is_empty() { + return Err(ConnectionError::invalid_config("mongo database is empty")); + } + self.with_client(|client| { + runtime::block_on(async { + client + .database(db) + .list_collection_names() + .await + .map_err(|e| ConnectionError::driver(DriverKind::Mongo, e.to_string())) + }) + }) + } + + pub fn find( + &self, + database: &str, + collection: &str, + filter_json: Option<&str>, + limit: Option, + ) -> ConnectionResult { + let db = database.trim(); + let coll = collection.trim(); + if db.is_empty() || coll.is_empty() { + return Err(ConnectionError::invalid_config( + "mongo database and collection are required", + )); + } + let limit = limit + .unwrap_or(DEFAULT_FIND_LIMIT) + .clamp(1, DEFAULT_FIND_LIMIT); + + let filter = match filter_json.map(str::trim).filter(|s| !s.is_empty()) { + Some(raw) => { + let v: JsonValue = serde_json::from_str(raw).map_err(|e| { + ConnectionError::invalid_config(format!("invalid mongo filter JSON: {e}")) + })?; + mongodb::bson::to_document(&v).map_err(|e| { + ConnectionError::invalid_config(format!("mongo filter must be an object: {e}")) + })? + } + None => Document::new(), + }; + + self.with_client(|client| { + runtime::block_on(async { + let options = FindOptions::builder().limit(limit).build(); + let mut cursor = client + .database(db) + .collection::(coll) + .find(filter) + .with_options(options) + .await + .map_err(|e| ConnectionError::driver(DriverKind::Mongo, e.to_string()))?; + + let mut documents = Vec::new(); + while let Some(doc) = cursor + .try_next() + .await + .map_err(|e| ConnectionError::driver(DriverKind::Mongo, e.to_string()))? + { + let json = bson_document_to_json(doc)?; + documents.push(json); + } + let truncated = documents.len() as i64 >= limit; + Ok(MongoFindResult { + documents, + truncated, + }) + }) + }) + } +} + +impl ConnectionSession for MongoSession { + fn id(&self) -> &ConnectionId { + &self.id + } + + fn driver(&self) -> DriverKind { + DriverKind::Mongo + } + + fn disconnect(&self) -> ConnectionResult<()> { + let mut guard = self + .client + .lock() + .map_err(|_| ConnectionError::io("mongo client lock poisoned"))?; + *guard = None; + Ok(()) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[derive(Debug, Clone)] +pub struct MongoConnectParams { + pub host: String, + pub port: u16, + pub database: String, + pub username: Option, + pub password: Option, + pub auth_source: Option, + pub id: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MongoFindResult { + pub documents: Vec, + pub truncated: bool, +} + +/// Open a Mongo client and smoke with ping. +pub fn open_mongo(params: MongoConnectParams) -> ConnectionResult { + let host = params.host.trim(); + let database = params.database.trim(); + if host.is_empty() { + return Err(ConnectionError::invalid_config("mongo host is empty")); + } + if params.port == 0 { + return Err(ConnectionError::invalid_config("mongo port is 0")); + } + let database = if database.is_empty() { + "querya".to_owned() + } else { + database.to_owned() + }; + + let uri = if host.contains("://") { + host.to_owned() + } else { + build_uri( + host, + params.port, + &database, + params.username.as_deref(), + params.password.as_deref(), + params.auth_source.as_deref(), + ) + }; + + let client = runtime::block_on(async { + let mut opts = ClientOptions::parse(&uri) + .await + .map_err(|e| ConnectionError::invalid_config(format!("invalid mongo uri: {e}")))?; + opts.server_selection_timeout = Some(Duration::from_secs(8)); + opts.connect_timeout = Some(Duration::from_secs(8)); + Client::with_options(opts) + .map_err(|e| ConnectionError::driver(DriverKind::Mongo, e.to_string())) + })?; + + runtime::block_on(async { + client + .database("admin") + .run_command(doc! { "ping": 1 }) + .await + .map_err(|e| ConnectionError::driver(DriverKind::Mongo, e.to_string())) + })?; + + let id = params + .id + .filter(|i| !i.as_str().trim().is_empty()) + .unwrap_or_default(); + + Ok(MongoSession { + id, + host: if host.contains("://") { + host.to_owned() + } else { + host.to_owned() + }, + port: params.port, + database, + client: Arc::new(Mutex::new(Some(client))), + }) +} + +fn build_uri( + host: &str, + port: u16, + database: &str, + username: Option<&str>, + password: Option<&str>, + auth_source: Option<&str>, +) -> String { + let auth_db = auth_source.unwrap_or("admin"); + match (username.filter(|s| !s.is_empty()), password) { + (Some(user), Some(pw)) if !pw.is_empty() => { + format!( + "mongodb://{}:{}@{}:{}/{}?authSource={}", + urlencoding_user(user), + urlencoding_user(pw), + host, + port, + database, + auth_db + ) + } + (Some(user), _) => format!( + "mongodb://{}@{}:{}/{}?authSource={}", + urlencoding_user(user), + host, + port, + database, + auth_db + ), + _ => format!("mongodb://{host}:{port}/{database}"), + } +} + +fn urlencoding_user(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + for b in s.bytes() { + match b { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { + out.push(b as char); + } + _ => out.push_str(&format!("%{b:02X}")), + } + } + out +} + +fn bson_document_to_json(doc: Document) -> ConnectionResult { + serde_json::to_value(doc) + .map_err(|e| ConnectionError::driver(DriverKind::Mongo, e.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn skip_live() -> bool { + if std::env::var_os("QUERYA_SKIP_MONGO").is_some() { + eprintln!("skip mongo live test (QUERYA_SKIP_MONGO)"); + return true; + } + false + } + + fn live_params() -> MongoConnectParams { + let uri = std::env::var("QUERYA_MONGO_URI").unwrap_or_else(|_| { + "mongodb://querya:querya@127.0.0.1:27017/querya?authSource=admin".into() + }); + MongoConnectParams { + host: uri, + port: 27017, + database: "querya".into(), + username: None, + password: None, + auth_source: None, + id: None, + } + } + + #[test] + fn open_rejects_empty_host() { + let result = open_mongo(MongoConnectParams { + host: " ".into(), + port: 27017, + database: "querya".into(), + username: None, + password: None, + auth_source: None, + id: None, + }); + assert!(matches!(result, Err(ConnectionError::InvalidConfig { .. }))); + } + + #[test] + fn live_ping_list_find() { + if skip_live() { + return; + } + let session = match open_mongo(live_params()) { + Ok(s) => s, + Err(e) => { + eprintln!("skip mongo live test (connect failed: {e})"); + return; + } + }; + assert_eq!(session.ping().unwrap(), "ok"); + let dbs = session.list_databases().unwrap(); + assert!(dbs.iter().any(|d| d == "querya"), "{dbs:?}"); + let cols = session.list_collections("querya").unwrap(); + assert!( + cols.iter().any(|c| c == "users"), + "expected users in {cols:?}" + ); + let found = session.find("querya", "users", None, Some(5)).unwrap(); + assert!(!found.documents.is_empty(), "users should have docs"); + session.disconnect().unwrap(); + } +} diff --git a/apps/tauri/src-tauri/src/connection/sql_guard.rs b/apps/tauri/src-tauri/src/connection/sql_guard.rs index 6a45b02e..12e8cba2 100644 --- a/apps/tauri/src-tauri/src/connection/sql_guard.rs +++ b/apps/tauri/src-tauri/src/connection/sql_guard.rs @@ -122,6 +122,11 @@ fn parse_statements(sql: &str, driver: DriverKind) -> ConnectionResult { + return Err(ConnectionError::UnsupportedDriver { + driver: DriverKind::Mongo, + }); + } }; match primary { diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs index b67a28ee..f519b7a6 100644 --- a/apps/tauri/src-tauri/src/lib.rs +++ b/apps/tauri/src-tauri/src/lib.rs @@ -6,7 +6,7 @@ mod staging; pub use connection::{ ColumnMeta, ConnectionConfig, ConnectionError, ConnectionId, ConnectionManager, - ConnectionRegistry, DriverKind, ErdColumn, ErdRelation, ErdSchema, ErdTable, + ConnectionRegistry, DriverKind, ErdColumn, ErdRelation, ErdSchema, ErdTable, MongoFindResult, ObjectTreeProvider, PostgresObjectDetail, QueryEvent, RedisKeyMeta, RedisKeyView, RedisScanPage, SchemaObject, SchemaObjectKind, SqlObjectKind, SqlObjectNode, SqliteOverview, TableSchema, }; @@ -20,12 +20,14 @@ pub use staging::{ }; use commands::{ - db_apply_staging, db_connect_mysql, db_connect_postgres, db_connect_redis, db_connect_sqlite, - db_describe_object, db_disconnect, db_erd_schema, db_execute_query, db_is_connected, - db_list_children, db_list_columns, db_list_schemas, db_list_tables, db_postgres_object_detail, - db_sqlite_overview, profiles_connect_mysql, profiles_connect_postgres, profiles_connect_redis, - profiles_connect_sqlite, profiles_delete, profiles_list, profiles_upsert, redis_get, - redis_key_metas, redis_ping, redis_read_key, redis_scan, redis_set, + db_apply_staging, db_connect_mongo, db_connect_mysql, db_connect_postgres, db_connect_redis, + db_connect_sqlite, db_describe_object, db_disconnect, db_erd_schema, db_execute_query, + db_is_connected, db_list_children, db_list_columns, db_list_schemas, db_list_tables, + db_postgres_object_detail, db_sqlite_overview, mongo_find, mongo_list_collections, + mongo_list_databases, mongo_ping, profiles_connect_mongo, profiles_connect_mysql, + profiles_connect_postgres, profiles_connect_redis, profiles_connect_sqlite, profiles_delete, + profiles_list, profiles_upsert, redis_get, redis_key_metas, redis_ping, redis_read_key, + redis_scan, redis_set, }; #[tauri::command] @@ -97,6 +99,7 @@ pub fn run() { db_connect_postgres, db_connect_mysql, db_connect_redis, + db_connect_mongo, db_disconnect, db_is_connected, db_list_schemas, @@ -115,6 +118,10 @@ pub fn run() { redis_set, redis_key_metas, redis_read_key, + mongo_ping, + mongo_list_databases, + mongo_list_collections, + mongo_find, profiles_list, profiles_upsert, profiles_delete, @@ -122,6 +129,7 @@ pub fn run() { profiles_connect_postgres, profiles_connect_mysql, profiles_connect_redis, + profiles_connect_mongo, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/apps/tauri/src-tauri/src/profiles/store.rs b/apps/tauri/src-tauri/src/profiles/store.rs index 86f2bcee..22c1c5dc 100644 --- a/apps/tauri/src-tauri/src/profiles/store.rs +++ b/apps/tauri/src-tauri/src/profiles/store.rs @@ -127,6 +127,31 @@ impl ConnectionProfile { } } + /// MongoDB profile — `database_name` is the default database. + pub fn new_mongo( + name: impl Into, + host: impl Into, + port: u16, + database: impl Into, + username: Option, + ) -> Self { + let now = now_rfc3339(); + Self { + id: Uuid::new_v4().to_string(), + driver: "mongo".into(), + name: name.into(), + path: None, + host: Some(host.into()), + port: Some(i64::from(port)), + username, + database_name: Some(database.into()), + folder_id: None, + sort_order: 0, + created_at: now.clone(), + updated_at: now, + } + } + pub fn touch(&mut self) { self.updated_at = now_rfc3339(); } diff --git a/apps/tauri/src-tauri/src/staging/ipc.rs b/apps/tauri/src-tauri/src/staging/ipc.rs index 8f947635..91715508 100644 --- a/apps/tauri/src-tauri/src/staging/ipc.rs +++ b/apps/tauri/src-tauri/src/staging/ipc.rs @@ -91,6 +91,9 @@ pub fn apply_staging_request( DriverKind::Redis => Err(ConnectionError::UnsupportedDriver { driver: DriverKind::Redis, }), + DriverKind::Mongo => Err(ConnectionError::UnsupportedDriver { + driver: DriverKind::Mongo, + }), }?; Ok(result)