migrated remaining components to rust.
This commit is contained in:
371
src-tauri/Cargo.lock
generated
371
src-tauri/Cargo.lock
generated
@@ -493,6 +493,16 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
@@ -516,9 +526,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -529,7 +539,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -829,6 +839,15 @@ version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.0"
|
||||
@@ -951,6 +970,15 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -958,7 +986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared",
|
||||
"foreign-types-shared 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -972,6 +1000,12 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
@@ -1382,6 +1416,25 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"indexmap 2.12.1",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -1480,6 +1533,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
@@ -1491,6 +1545,38 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.18"
|
||||
@@ -1510,9 +1596,11 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1971,6 +2059,16 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -2013,6 +2111,23 @@ dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
@@ -2387,6 +2502,50 @@ dependencies = [
|
||||
"pathdiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -2978,22 +3137,31 @@ checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
@@ -3012,15 +3180,32 @@ dependencies = [
|
||||
"chrono",
|
||||
"directories",
|
||||
"redb",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-opener",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[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.16",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -3043,6 +3228,39 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
@@ -3064,6 +3282,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
@@ -3121,6 +3348,29 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.24.0"
|
||||
@@ -3394,7 +3644,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2 0.5.2",
|
||||
@@ -3476,6 +3726,12 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.7"
|
||||
@@ -3529,6 +3785,27 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"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"
|
||||
@@ -3550,7 +3827,7 @@ checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.6.2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics",
|
||||
"crossbeam-channel",
|
||||
"dispatch",
|
||||
@@ -3969,9 +4246,41 @@ dependencies = [
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.17"
|
||||
@@ -4249,6 +4558,12 @@ dependencies = [
|
||||
"unic-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
@@ -4261,6 +4576,12 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[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.7"
|
||||
@@ -4273,6 +4594,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "urlpattern"
|
||||
version = "0.3.0"
|
||||
@@ -4309,6 +4636,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.1"
|
||||
@@ -4691,6 +5024,17 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
@@ -4745,6 +5089,15 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
@@ -5264,6 +5617,12 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.3"
|
||||
|
||||
@@ -32,3 +32,8 @@ thiserror = "2"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
directories = "5"
|
||||
|
||||
# HTTP client
|
||||
reqwest = { version = "0.12", features = ["json", "multipart"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
urlencoding = "2"
|
||||
|
||||
|
||||
51
src-tauri/src/collections/commands.rs
Normal file
51
src-tauri/src/collections/commands.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use tauri::State;
|
||||
|
||||
use crate::db::Database;
|
||||
|
||||
use super::service::CollectionService;
|
||||
use super::types::{Collection, CreateCollectionInput, UpdateCollectionInput};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_collections(db: State<Database>) -> Result<Vec<Collection>, String> {
|
||||
let service = CollectionService::new(db.inner().clone());
|
||||
service.get_all().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_collection(db: State<Database>, id: String) -> Result<Collection, String> {
|
||||
let service = CollectionService::new(db.inner().clone());
|
||||
service.get(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_collections_by_workspace(
|
||||
db: State<Database>,
|
||||
workspace_id: String,
|
||||
) -> Result<Vec<Collection>, String> {
|
||||
let service = CollectionService::new(db.inner().clone());
|
||||
service.get_by_workspace(&workspace_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_collection(
|
||||
db: State<Database>,
|
||||
input: CreateCollectionInput,
|
||||
) -> Result<Collection, String> {
|
||||
let service = CollectionService::new(db.inner().clone());
|
||||
service.create(input).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_collection(
|
||||
db: State<Database>,
|
||||
input: UpdateCollectionInput,
|
||||
) -> Result<Collection, String> {
|
||||
let service = CollectionService::new(db.inner().clone());
|
||||
service.update(input).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_collection(db: State<Database>, id: String) -> Result<(), String> {
|
||||
let service = CollectionService::new(db.inner().clone());
|
||||
service.delete(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
8
src-tauri/src/collections/mod.rs
Normal file
8
src-tauri/src/collections/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
mod commands;
|
||||
mod service;
|
||||
mod types;
|
||||
|
||||
pub use commands::*;
|
||||
#[allow(unused_imports)]
|
||||
pub use types::{Collection, CreateCollectionInput, UpdateCollectionInput};
|
||||
pub(crate) use service::CollectionService;
|
||||
164
src-tauri/src/collections/service.rs
Normal file
164
src-tauri/src/collections/service.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
use chrono::Utc;
|
||||
use redb::ReadableTable;
|
||||
|
||||
use crate::db::{
|
||||
Database, DbError, DbResult, COLLECTIONS, COLLECTIONS_BY_WORKSPACE,
|
||||
};
|
||||
|
||||
use super::types::{Collection, CreateCollectionInput, UpdateCollectionInput};
|
||||
|
||||
pub struct CollectionService {
|
||||
db: Database,
|
||||
}
|
||||
|
||||
impl CollectionService {
|
||||
pub fn new(db: Database) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub fn get_all(&self) -> DbResult<Vec<Collection>> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let table = read_txn.open_table(COLLECTIONS)?;
|
||||
|
||||
let mut collections = Vec::new();
|
||||
for entry in table.iter()? {
|
||||
let (_, value) = entry?;
|
||||
let collection: Collection = serde_json::from_str(value.value())
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
collections.push(collection);
|
||||
}
|
||||
|
||||
collections.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(collections)
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &str) -> DbResult<Collection> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let table = read_txn.open_table(COLLECTIONS)?;
|
||||
|
||||
let value = table
|
||||
.get(id)?
|
||||
.ok_or_else(|| DbError::NotFound(format!("Collection not found: {}", id)))?;
|
||||
|
||||
let collection: Collection = serde_json::from_str(value.value())
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
Ok(collection)
|
||||
}
|
||||
|
||||
pub fn get_by_workspace(&self, workspace_id: &str) -> DbResult<Vec<Collection>> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let idx_table = read_txn.open_table(COLLECTIONS_BY_WORKSPACE)?;
|
||||
|
||||
let collection_ids: Vec<String> = match idx_table.get(workspace_id)? {
|
||||
Some(value) => serde_json::from_str(value.value())
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
drop(idx_table);
|
||||
drop(read_txn);
|
||||
|
||||
let mut collections = Vec::new();
|
||||
for id in collection_ids {
|
||||
if let Ok(collection) = self.get(&id) {
|
||||
collections.push(collection);
|
||||
}
|
||||
}
|
||||
|
||||
collections.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(collections)
|
||||
}
|
||||
|
||||
pub fn create(&self, input: CreateCollectionInput) -> DbResult<Collection> {
|
||||
let collection = Collection::new(input.name, input.description, input.workspace_id.clone());
|
||||
|
||||
let json = serde_json::to_string(&collection)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
let write_txn = self.db.begin_write()?;
|
||||
|
||||
{
|
||||
let mut table = write_txn.open_table(COLLECTIONS)?;
|
||||
table.insert(collection.id.as_str(), json.as_str())?;
|
||||
}
|
||||
|
||||
// Update index
|
||||
{
|
||||
let mut idx_table = write_txn.open_table(COLLECTIONS_BY_WORKSPACE)?;
|
||||
let ids_json = match idx_table.get(input.workspace_id.as_str())? {
|
||||
Some(value) => value.value().to_string(),
|
||||
None => "[]".to_string(),
|
||||
};
|
||||
|
||||
let mut ids: Vec<String> = serde_json::from_str(&ids_json)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
ids.push(collection.id.clone());
|
||||
|
||||
let new_json = serde_json::to_string(&ids)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
idx_table.insert(input.workspace_id.as_str(), new_json.as_str())?;
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(collection)
|
||||
}
|
||||
|
||||
pub fn update(&self, input: UpdateCollectionInput) -> DbResult<Collection> {
|
||||
let mut collection = self.get(&input.id)?;
|
||||
|
||||
if let Some(name) = input.name {
|
||||
collection.name = name;
|
||||
}
|
||||
if let Some(description) = input.description {
|
||||
collection.description = description;
|
||||
}
|
||||
collection.updated_at = Utc::now();
|
||||
|
||||
let json = serde_json::to_string(&collection)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
let write_txn = self.db.begin_write()?;
|
||||
{
|
||||
let mut table = write_txn.open_table(COLLECTIONS)?;
|
||||
table.insert(collection.id.as_str(), json.as_str())?;
|
||||
}
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(collection)
|
||||
}
|
||||
|
||||
pub fn delete(&self, id: &str) -> DbResult<()> {
|
||||
let collection = self.get(id)?;
|
||||
|
||||
let write_txn = self.db.begin_write()?;
|
||||
|
||||
// Remove from collections table
|
||||
{
|
||||
let mut table = write_txn.open_table(COLLECTIONS)?;
|
||||
table.remove(id)?;
|
||||
}
|
||||
|
||||
// Update index
|
||||
{
|
||||
let mut idx_table = write_txn.open_table(COLLECTIONS_BY_WORKSPACE)?;
|
||||
let ids_json = idx_table
|
||||
.get(collection.workspace_id.as_str())?
|
||||
.map(|v| v.value().to_string())
|
||||
.unwrap_or_else(|| "[]".to_string());
|
||||
let mut ids: Vec<String> = serde_json::from_str(&ids_json)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
ids.retain(|i| i != id);
|
||||
let new_json = serde_json::to_string(&ids)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
idx_table.insert(collection.workspace_id.as_str(), new_json.as_str())?;
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
43
src-tauri/src/collections/types.rs
Normal file
43
src-tauri/src/collections/types.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Collection {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub workspace_id: String,
|
||||
#[serde(default = "Utc::now")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
#[serde(default = "Utc::now")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub fn new(name: String, description: String, workspace_id: String) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
name,
|
||||
description,
|
||||
workspace_id,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateCollectionInput {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub workspace_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateCollectionInput {
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
174
src-tauri/src/http/client.rs
Normal file
174
src-tauri/src/http/client.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use reqwest::{header::HeaderMap, Client, Method};
|
||||
|
||||
use super::types::{HttpRequest, HttpResponse, HttpResponseHeader};
|
||||
|
||||
pub async fn execute_request(request: HttpRequest) -> Result<HttpResponse, String> {
|
||||
let client = Client::new();
|
||||
let start = Instant::now();
|
||||
|
||||
// Parse method
|
||||
let method = match request.method.to_uppercase().as_str() {
|
||||
"GET" => Method::GET,
|
||||
"POST" => Method::POST,
|
||||
"PUT" => Method::PUT,
|
||||
"PATCH" => Method::PATCH,
|
||||
"DELETE" => Method::DELETE,
|
||||
"HEAD" => Method::HEAD,
|
||||
"OPTIONS" => Method::OPTIONS,
|
||||
_ => return Err(format!("Unsupported HTTP method: {}", request.method)),
|
||||
};
|
||||
|
||||
// Build URL with query params
|
||||
let mut url = request.url.clone();
|
||||
let enabled_params: Vec<_> = request
|
||||
.params
|
||||
.iter()
|
||||
.filter(|p| p.enabled && !p.key.is_empty())
|
||||
.collect();
|
||||
|
||||
if !enabled_params.is_empty() {
|
||||
let query_string: String = enabled_params
|
||||
.iter()
|
||||
.map(|p| format!("{}={}", urlencoding::encode(&p.key), urlencoding::encode(&p.value)))
|
||||
.collect::<Vec<_>>()
|
||||
.join("&");
|
||||
|
||||
if url.contains('?') {
|
||||
url = format!("{}&{}", url, query_string);
|
||||
} else {
|
||||
url = format!("{}?{}", url, query_string);
|
||||
}
|
||||
}
|
||||
|
||||
// Build headers
|
||||
let mut headers = HeaderMap::new();
|
||||
for header in &request.headers {
|
||||
if header.enabled && !header.key.is_empty() {
|
||||
if let (Ok(name), Ok(value)) = (
|
||||
header.key.parse::<reqwest::header::HeaderName>(),
|
||||
header.value.parse::<reqwest::header::HeaderValue>(),
|
||||
) {
|
||||
headers.insert(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build request
|
||||
let mut req_builder = client.request(method, &url).headers(headers);
|
||||
|
||||
// Add body based on body type
|
||||
match request.body_type.as_str() {
|
||||
"json" => {
|
||||
req_builder = req_builder
|
||||
.header("Content-Type", "application/json")
|
||||
.body(request.body.clone());
|
||||
}
|
||||
"xml" => {
|
||||
req_builder = req_builder
|
||||
.header("Content-Type", "application/xml")
|
||||
.body(request.body.clone());
|
||||
}
|
||||
"text" => {
|
||||
req_builder = req_builder
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(request.body.clone());
|
||||
}
|
||||
"html" => {
|
||||
req_builder = req_builder
|
||||
.header("Content-Type", "text/html")
|
||||
.body(request.body.clone());
|
||||
}
|
||||
"x-www-form-urlencoded" => {
|
||||
let enabled_form: Vec<_> = request
|
||||
.form_data
|
||||
.iter()
|
||||
.filter(|f| f.enabled && !f.key.is_empty())
|
||||
.collect();
|
||||
|
||||
let form_string: String = enabled_form
|
||||
.iter()
|
||||
.map(|f| format!("{}={}", urlencoding::encode(&f.key), urlencoding::encode(&f.value)))
|
||||
.collect::<Vec<_>>()
|
||||
.join("&");
|
||||
|
||||
req_builder = req_builder
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.body(form_string);
|
||||
}
|
||||
"form-data" => {
|
||||
let mut form = reqwest::multipart::Form::new();
|
||||
for item in &request.form_data {
|
||||
if item.enabled && !item.key.is_empty() {
|
||||
if item.item_type == "file" {
|
||||
// For file uploads, read the file
|
||||
match std::fs::read(&item.value) {
|
||||
Ok(contents) => {
|
||||
let file_name = std::path::Path::new(&item.value)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("file")
|
||||
.to_string();
|
||||
let part = reqwest::multipart::Part::bytes(contents)
|
||||
.file_name(file_name);
|
||||
form = form.part(item.key.clone(), part);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!("Failed to read file {}: {}", item.value, e));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
form = form.text(item.key.clone(), item.value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
req_builder = req_builder.multipart(form);
|
||||
}
|
||||
_ => {
|
||||
// "none" or unknown - no body
|
||||
}
|
||||
}
|
||||
|
||||
// Execute request
|
||||
let response = req_builder
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Request failed: {}", e))?;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
let status = response.status().as_u16();
|
||||
let status_text = response
|
||||
.status()
|
||||
.canonical_reason()
|
||||
.unwrap_or("Unknown")
|
||||
.to_string();
|
||||
|
||||
// Collect headers
|
||||
let response_headers: Vec<HttpResponseHeader> = response
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(k, v)| HttpResponseHeader {
|
||||
key: k.to_string(),
|
||||
value: v.to_str().unwrap_or("").to_string(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Get body
|
||||
let body_bytes = response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read response body: {}", e))?;
|
||||
|
||||
let size_bytes = body_bytes.len();
|
||||
let body = String::from_utf8_lossy(&body_bytes).to_string();
|
||||
|
||||
Ok(HttpResponse {
|
||||
status,
|
||||
status_text,
|
||||
headers: response_headers,
|
||||
body,
|
||||
time_ms: elapsed.as_millis() as u64,
|
||||
size_bytes,
|
||||
})
|
||||
}
|
||||
5
src-tauri/src/http/mod.rs
Normal file
5
src-tauri/src/http/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod client;
|
||||
mod types;
|
||||
|
||||
pub use client::execute_request;
|
||||
pub use types::{HttpRequest, HttpResponse, HttpResponseHeader};
|
||||
55
src-tauri/src/http/types.rs
Normal file
55
src-tauri/src/http/types.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HttpRequest {
|
||||
pub method: String,
|
||||
pub url: String,
|
||||
#[serde(default)]
|
||||
pub headers: Vec<HttpRequestHeader>,
|
||||
#[serde(default)]
|
||||
pub params: Vec<HttpRequestParam>,
|
||||
pub body_type: String,
|
||||
#[serde(default)]
|
||||
pub body: String,
|
||||
#[serde(default)]
|
||||
pub form_data: Vec<HttpFormDataItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HttpRequestHeader {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HttpRequestParam {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HttpFormDataItem {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
#[serde(rename = "type")]
|
||||
pub item_type: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HttpResponse {
|
||||
pub status: u16,
|
||||
pub status_text: String,
|
||||
pub headers: Vec<HttpResponseHeader>,
|
||||
pub body: String,
|
||||
pub time_ms: u64,
|
||||
pub size_bytes: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HttpResponseHeader {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
@@ -1,11 +1,28 @@
|
||||
// Resona - API Client Application
|
||||
|
||||
mod collections;
|
||||
mod db;
|
||||
mod http;
|
||||
mod requests;
|
||||
mod variables;
|
||||
mod workspaces;
|
||||
|
||||
use db::Database;
|
||||
use http::{HttpRequest, HttpResponse};
|
||||
|
||||
// Re-export workspace commands for generate_handler macro
|
||||
use collections::{
|
||||
create_collection, delete_collection, get_collection, get_collections,
|
||||
get_collections_by_workspace, update_collection,
|
||||
};
|
||||
use requests::{
|
||||
create_request, delete_request, get_all_requests_by_workspace, get_request,
|
||||
get_requests_by_collection, get_standalone_requests_by_workspace, update_request,
|
||||
};
|
||||
use variables::{
|
||||
create_variable, delete_variable, get_collection_variables, get_global_variables,
|
||||
get_request_variables, get_resolved_variables, get_variable, get_workspace_variables,
|
||||
update_variable,
|
||||
};
|
||||
use workspaces::{
|
||||
add_workspace_to_sync_group, create_sync_group, create_workspace, delete_sync_group,
|
||||
delete_workspace, get_sync_group, get_sync_group_for_workspace, get_sync_groups,
|
||||
@@ -13,9 +30,13 @@ use workspaces::{
|
||||
update_sync_group, update_workspace,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
async fn send_http_request(request: HttpRequest) -> Result<HttpResponse, String> {
|
||||
http::execute_request(request).await
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// Initializing the database
|
||||
let db = Database::open().expect("Failed to initialize database");
|
||||
|
||||
tauri::Builder::default()
|
||||
@@ -38,6 +59,33 @@ pub fn run() {
|
||||
get_workspaces_by_sync_group,
|
||||
add_workspace_to_sync_group,
|
||||
remove_workspace_from_sync_group,
|
||||
// Collection commands
|
||||
get_collections,
|
||||
get_collection,
|
||||
get_collections_by_workspace,
|
||||
create_collection,
|
||||
update_collection,
|
||||
delete_collection,
|
||||
// Request commands
|
||||
get_request,
|
||||
get_requests_by_collection,
|
||||
get_standalone_requests_by_workspace,
|
||||
get_all_requests_by_workspace,
|
||||
create_request,
|
||||
update_request,
|
||||
delete_request,
|
||||
// Variable commands
|
||||
get_variable,
|
||||
get_global_variables,
|
||||
get_workspace_variables,
|
||||
get_collection_variables,
|
||||
get_request_variables,
|
||||
get_resolved_variables,
|
||||
create_variable,
|
||||
update_variable,
|
||||
delete_variable,
|
||||
// HTTP client
|
||||
send_http_request,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
63
src-tauri/src/requests/commands.rs
Normal file
63
src-tauri/src/requests/commands.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use tauri::State;
|
||||
|
||||
use crate::db::Database;
|
||||
|
||||
use super::service::RequestService;
|
||||
use super::types::{CreateRequestInput, Request, UpdateRequestInput};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_request(db: State<Database>, id: String) -> Result<Request, String> {
|
||||
let service = RequestService::new(db.inner().clone());
|
||||
service.get(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_requests_by_collection(
|
||||
db: State<Database>,
|
||||
collection_id: String,
|
||||
) -> Result<Vec<Request>, String> {
|
||||
let service = RequestService::new(db.inner().clone());
|
||||
service.get_by_collection(&collection_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_standalone_requests_by_workspace(
|
||||
db: State<Database>,
|
||||
workspace_id: String,
|
||||
) -> Result<Vec<Request>, String> {
|
||||
let service = RequestService::new(db.inner().clone());
|
||||
service.get_standalone_by_workspace(&workspace_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_all_requests_by_workspace(
|
||||
db: State<Database>,
|
||||
workspace_id: String,
|
||||
) -> Result<Vec<Request>, String> {
|
||||
let service = RequestService::new(db.inner().clone());
|
||||
service.get_all_by_workspace(&workspace_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_request(
|
||||
db: State<Database>,
|
||||
input: CreateRequestInput,
|
||||
) -> Result<Request, String> {
|
||||
let service = RequestService::new(db.inner().clone());
|
||||
service.create(input).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_request(
|
||||
db: State<Database>,
|
||||
input: UpdateRequestInput,
|
||||
) -> Result<Request, String> {
|
||||
let service = RequestService::new(db.inner().clone());
|
||||
service.update(input).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_request(db: State<Database>, id: String) -> Result<(), String> {
|
||||
let service = RequestService::new(db.inner().clone());
|
||||
service.delete(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
11
src-tauri/src/requests/mod.rs
Normal file
11
src-tauri/src/requests/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
mod commands;
|
||||
mod service;
|
||||
mod types;
|
||||
|
||||
pub use commands::*;
|
||||
#[allow(unused_imports)]
|
||||
pub use types::{
|
||||
BodyType, CreateRequestInput, FormDataItem, HttpMethod, Request, RequestHeader,
|
||||
RequestParam, UpdateRequestInput,
|
||||
};
|
||||
pub(crate) use service::RequestService;
|
||||
298
src-tauri/src/requests/service.rs
Normal file
298
src-tauri/src/requests/service.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
use chrono::Utc;
|
||||
use redb::ReadableTable;
|
||||
|
||||
use crate::db::{
|
||||
Database, DbError, DbResult, REQUESTS, REQUESTS_BY_COLLECTION, REQUESTS_BY_WORKSPACE,
|
||||
};
|
||||
|
||||
use super::types::{CreateRequestInput, Request, UpdateRequestInput};
|
||||
|
||||
pub struct RequestService {
|
||||
db: Database,
|
||||
}
|
||||
|
||||
impl RequestService {
|
||||
pub fn new(db: Database) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &str) -> DbResult<Request> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let table = read_txn.open_table(REQUESTS)?;
|
||||
|
||||
let value = table
|
||||
.get(id)?
|
||||
.ok_or_else(|| DbError::NotFound(format!("Request not found: {}", id)))?;
|
||||
|
||||
let request: Request = serde_json::from_str(value.value())
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
pub fn get_by_collection(&self, collection_id: &str) -> DbResult<Vec<Request>> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let idx_table = read_txn.open_table(REQUESTS_BY_COLLECTION)?;
|
||||
|
||||
let request_ids: Vec<String> = match idx_table.get(collection_id)? {
|
||||
Some(value) => serde_json::from_str(value.value())
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
drop(idx_table);
|
||||
drop(read_txn);
|
||||
|
||||
let mut requests = Vec::new();
|
||||
for id in request_ids {
|
||||
if let Ok(request) = self.get(&id) {
|
||||
requests.push(request);
|
||||
}
|
||||
}
|
||||
|
||||
requests.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(requests)
|
||||
}
|
||||
|
||||
pub fn get_standalone_by_workspace(&self, workspace_id: &str) -> DbResult<Vec<Request>> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let idx_table = read_txn.open_table(REQUESTS_BY_WORKSPACE)?;
|
||||
|
||||
let request_ids: Vec<String> = match idx_table.get(workspace_id)? {
|
||||
Some(value) => serde_json::from_str(value.value())
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
drop(idx_table);
|
||||
drop(read_txn);
|
||||
|
||||
let mut requests = Vec::new();
|
||||
for id in request_ids {
|
||||
if let Ok(request) = self.get(&id) {
|
||||
if request.collection_id.is_none() {
|
||||
requests.push(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requests.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(requests)
|
||||
}
|
||||
|
||||
pub fn get_all_by_workspace(&self, workspace_id: &str) -> DbResult<Vec<Request>> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let idx_table = read_txn.open_table(REQUESTS_BY_WORKSPACE)?;
|
||||
|
||||
let request_ids: Vec<String> = match idx_table.get(workspace_id)? {
|
||||
Some(value) => serde_json::from_str(value.value())
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
drop(idx_table);
|
||||
drop(read_txn);
|
||||
|
||||
let mut requests = Vec::new();
|
||||
for id in request_ids {
|
||||
if let Ok(request) = self.get(&id) {
|
||||
requests.push(request);
|
||||
}
|
||||
}
|
||||
|
||||
requests.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(requests)
|
||||
}
|
||||
|
||||
pub fn create(&self, input: CreateRequestInput) -> DbResult<Request> {
|
||||
let mut request = Request::new(input.name, input.method, input.workspace_id.clone());
|
||||
request.url = input.url;
|
||||
request.headers = input.headers;
|
||||
request.params = input.params;
|
||||
request.body_type = input.body_type;
|
||||
request.body = input.body;
|
||||
request.form_data = input.form_data;
|
||||
request.collection_id = input.collection_id.clone();
|
||||
|
||||
let json = serde_json::to_string(&request)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
let write_txn = self.db.begin_write()?;
|
||||
|
||||
{
|
||||
let mut table = write_txn.open_table(REQUESTS)?;
|
||||
table.insert(request.id.as_str(), json.as_str())?;
|
||||
}
|
||||
|
||||
// Update workspace index
|
||||
{
|
||||
let mut idx_table = write_txn.open_table(REQUESTS_BY_WORKSPACE)?;
|
||||
let ids_json = match idx_table.get(input.workspace_id.as_str())? {
|
||||
Some(value) => value.value().to_string(),
|
||||
None => "[]".to_string(),
|
||||
};
|
||||
|
||||
let mut ids: Vec<String> = serde_json::from_str(&ids_json)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
ids.push(request.id.clone());
|
||||
|
||||
let new_json = serde_json::to_string(&ids)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
idx_table.insert(input.workspace_id.as_str(), new_json.as_str())?;
|
||||
}
|
||||
|
||||
// Update collection index if applicable
|
||||
if let Some(ref collection_id) = input.collection_id {
|
||||
let mut idx_table = write_txn.open_table(REQUESTS_BY_COLLECTION)?;
|
||||
let ids_json = match idx_table.get(collection_id.as_str())? {
|
||||
Some(value) => value.value().to_string(),
|
||||
None => "[]".to_string(),
|
||||
};
|
||||
|
||||
let mut ids: Vec<String> = serde_json::from_str(&ids_json)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
ids.push(request.id.clone());
|
||||
|
||||
let new_json = serde_json::to_string(&ids)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
idx_table.insert(collection_id.as_str(), new_json.as_str())?;
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
pub fn update(&self, input: UpdateRequestInput) -> DbResult<Request> {
|
||||
let mut request = self.get(&input.id)?;
|
||||
let old_collection_id = request.collection_id.clone();
|
||||
|
||||
if let Some(name) = input.name {
|
||||
request.name = name;
|
||||
}
|
||||
if let Some(method) = input.method {
|
||||
request.method = method;
|
||||
}
|
||||
if let Some(url) = input.url {
|
||||
request.url = url;
|
||||
}
|
||||
if let Some(headers) = input.headers {
|
||||
request.headers = headers;
|
||||
}
|
||||
if let Some(params) = input.params {
|
||||
request.params = params;
|
||||
}
|
||||
if let Some(body_type) = input.body_type {
|
||||
request.body_type = body_type;
|
||||
}
|
||||
if let Some(body) = input.body {
|
||||
request.body = body;
|
||||
}
|
||||
if let Some(form_data) = input.form_data {
|
||||
request.form_data = form_data;
|
||||
}
|
||||
if let Some(collection_id) = input.collection_id {
|
||||
request.collection_id = collection_id;
|
||||
}
|
||||
request.updated_at = Utc::now();
|
||||
|
||||
let json = serde_json::to_string(&request)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
let write_txn = self.db.begin_write()?;
|
||||
|
||||
{
|
||||
let mut table = write_txn.open_table(REQUESTS)?;
|
||||
table.insert(request.id.as_str(), json.as_str())?;
|
||||
}
|
||||
|
||||
// Update collection index if collection changed
|
||||
if old_collection_id != request.collection_id {
|
||||
// Remove from old collection index
|
||||
if let Some(ref old_id) = old_collection_id {
|
||||
let mut idx_table = write_txn.open_table(REQUESTS_BY_COLLECTION)?;
|
||||
let ids_json = idx_table
|
||||
.get(old_id.as_str())?
|
||||
.map(|v| v.value().to_string())
|
||||
.unwrap_or_else(|| "[]".to_string());
|
||||
let mut ids: Vec<String> = serde_json::from_str(&ids_json)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
ids.retain(|i| i != &request.id);
|
||||
let new_json = serde_json::to_string(&ids)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
idx_table.insert(old_id.as_str(), new_json.as_str())?;
|
||||
}
|
||||
|
||||
// Add to new collection index
|
||||
if let Some(ref new_id) = request.collection_id {
|
||||
let mut idx_table = write_txn.open_table(REQUESTS_BY_COLLECTION)?;
|
||||
let ids_json = idx_table
|
||||
.get(new_id.as_str())?
|
||||
.map(|v| v.value().to_string())
|
||||
.unwrap_or_else(|| "[]".to_string());
|
||||
let mut ids: Vec<String> = serde_json::from_str(&ids_json)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
if !ids.contains(&request.id) {
|
||||
ids.push(request.id.clone());
|
||||
}
|
||||
let new_json = serde_json::to_string(&ids)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
idx_table.insert(new_id.as_str(), new_json.as_str())?;
|
||||
}
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
pub fn delete(&self, id: &str) -> DbResult<()> {
|
||||
let request = self.get(id)?;
|
||||
|
||||
let write_txn = self.db.begin_write()?;
|
||||
|
||||
// Remove from requests table
|
||||
{
|
||||
let mut table = write_txn.open_table(REQUESTS)?;
|
||||
table.remove(id)?;
|
||||
}
|
||||
|
||||
// Update workspace index
|
||||
{
|
||||
let mut idx_table = write_txn.open_table(REQUESTS_BY_WORKSPACE)?;
|
||||
let ids_json = idx_table
|
||||
.get(request.workspace_id.as_str())?
|
||||
.map(|v| v.value().to_string())
|
||||
.unwrap_or_else(|| "[]".to_string());
|
||||
let mut ids: Vec<String> = serde_json::from_str(&ids_json)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
ids.retain(|i| i != id);
|
||||
let new_json = serde_json::to_string(&ids)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
idx_table.insert(request.workspace_id.as_str(), new_json.as_str())?;
|
||||
}
|
||||
|
||||
// Update collection index if applicable
|
||||
if let Some(ref collection_id) = request.collection_id {
|
||||
let mut idx_table = write_txn.open_table(REQUESTS_BY_COLLECTION)?;
|
||||
let ids_json = idx_table
|
||||
.get(collection_id.as_str())?
|
||||
.map(|v| v.value().to_string())
|
||||
.unwrap_or_else(|| "[]".to_string());
|
||||
let mut ids: Vec<String> = serde_json::from_str(&ids_json)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
ids.retain(|i| i != id);
|
||||
let new_json = serde_json::to_string(&ids)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
idx_table.insert(collection_id.as_str(), new_json.as_str())?;
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
142
src-tauri/src/requests/types.rs
Normal file
142
src-tauri/src/requests/types.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum HttpMethod {
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Patch,
|
||||
Delete,
|
||||
Head,
|
||||
Options,
|
||||
}
|
||||
|
||||
impl Default for HttpMethod {
|
||||
fn default() -> Self {
|
||||
Self::Get
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum BodyType {
|
||||
None,
|
||||
Json,
|
||||
Xml,
|
||||
Text,
|
||||
Html,
|
||||
FormData,
|
||||
#[serde(rename = "x-www-form-urlencoded")]
|
||||
XWwwFormUrlencoded,
|
||||
}
|
||||
|
||||
impl Default for BodyType {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RequestHeader {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RequestParam {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FormDataItem {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
#[serde(rename = "type")]
|
||||
pub item_type: String, // "text" or "file"
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Request {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub method: HttpMethod,
|
||||
pub url: String,
|
||||
#[serde(default)]
|
||||
pub headers: Vec<RequestHeader>,
|
||||
#[serde(default)]
|
||||
pub params: Vec<RequestParam>,
|
||||
#[serde(default)]
|
||||
pub body_type: BodyType,
|
||||
#[serde(default)]
|
||||
pub body: String,
|
||||
#[serde(default)]
|
||||
pub form_data: Vec<FormDataItem>,
|
||||
pub collection_id: Option<String>,
|
||||
pub workspace_id: String,
|
||||
#[serde(default = "Utc::now")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
#[serde(default = "Utc::now")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
pub fn new(name: String, method: HttpMethod, workspace_id: String) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
name,
|
||||
method,
|
||||
url: String::new(),
|
||||
headers: Vec::new(),
|
||||
params: Vec::new(),
|
||||
body_type: BodyType::None,
|
||||
body: String::new(),
|
||||
form_data: Vec::new(),
|
||||
collection_id: None,
|
||||
workspace_id,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateRequestInput {
|
||||
pub name: String,
|
||||
pub method: HttpMethod,
|
||||
#[serde(default)]
|
||||
pub url: String,
|
||||
#[serde(default)]
|
||||
pub headers: Vec<RequestHeader>,
|
||||
#[serde(default)]
|
||||
pub params: Vec<RequestParam>,
|
||||
#[serde(default)]
|
||||
pub body_type: BodyType,
|
||||
#[serde(default)]
|
||||
pub body: String,
|
||||
#[serde(default)]
|
||||
pub form_data: Vec<FormDataItem>,
|
||||
pub collection_id: Option<String>,
|
||||
pub workspace_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateRequestInput {
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
pub method: Option<HttpMethod>,
|
||||
pub url: Option<String>,
|
||||
pub headers: Option<Vec<RequestHeader>>,
|
||||
pub params: Option<Vec<RequestParam>>,
|
||||
pub body_type: Option<BodyType>,
|
||||
pub body: Option<String>,
|
||||
pub form_data: Option<Vec<FormDataItem>>,
|
||||
pub collection_id: Option<Option<String>>,
|
||||
}
|
||||
86
src-tauri/src/variables/commands.rs
Normal file
86
src-tauri/src/variables/commands.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use tauri::State;
|
||||
|
||||
use crate::db::Database;
|
||||
|
||||
use super::service::VariableService;
|
||||
use super::types::{CreateVariableInput, ResolvedVariable, UpdateVariableInput, Variable};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_variable(db: State<Database>, id: String) -> Result<Variable, String> {
|
||||
let service = VariableService::new(db.inner().clone());
|
||||
service.get(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_global_variables(db: State<Database>) -> Result<Vec<Variable>, String> {
|
||||
let service = VariableService::new(db.inner().clone());
|
||||
service.get_global().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_workspace_variables(
|
||||
db: State<Database>,
|
||||
workspace_id: String,
|
||||
) -> Result<Vec<Variable>, String> {
|
||||
let service = VariableService::new(db.inner().clone());
|
||||
service.get_by_workspace(&workspace_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_collection_variables(
|
||||
db: State<Database>,
|
||||
collection_id: String,
|
||||
) -> Result<Vec<Variable>, String> {
|
||||
let service = VariableService::new(db.inner().clone());
|
||||
service.get_by_collection(&collection_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_request_variables(
|
||||
db: State<Database>,
|
||||
request_id: String,
|
||||
) -> Result<Vec<Variable>, String> {
|
||||
let service = VariableService::new(db.inner().clone());
|
||||
service.get_by_request(&request_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_resolved_variables(
|
||||
db: State<Database>,
|
||||
workspace_id: Option<String>,
|
||||
collection_id: Option<String>,
|
||||
request_id: Option<String>,
|
||||
) -> Result<Vec<ResolvedVariable>, String> {
|
||||
let service = VariableService::new(db.inner().clone());
|
||||
service
|
||||
.get_resolved(
|
||||
workspace_id.as_deref(),
|
||||
collection_id.as_deref(),
|
||||
request_id.as_deref(),
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_variable(
|
||||
db: State<Database>,
|
||||
input: CreateVariableInput,
|
||||
) -> Result<Variable, String> {
|
||||
let service = VariableService::new(db.inner().clone());
|
||||
service.create(input).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_variable(
|
||||
db: State<Database>,
|
||||
input: UpdateVariableInput,
|
||||
) -> Result<Variable, String> {
|
||||
let service = VariableService::new(db.inner().clone());
|
||||
service.update(input).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_variable(db: State<Database>, id: String) -> Result<(), String> {
|
||||
let service = VariableService::new(db.inner().clone());
|
||||
service.delete(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
8
src-tauri/src/variables/mod.rs
Normal file
8
src-tauri/src/variables/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
mod commands;
|
||||
mod service;
|
||||
mod types;
|
||||
|
||||
pub use commands::*;
|
||||
#[allow(unused_imports)]
|
||||
pub use types::{CreateVariableInput, UpdateVariableInput, Variable, VariableScope};
|
||||
pub(crate) use service::VariableService;
|
||||
236
src-tauri/src/variables/service.rs
Normal file
236
src-tauri/src/variables/service.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
use chrono::Utc;
|
||||
use redb::ReadableTable;
|
||||
|
||||
use crate::db::{Database, DbError, DbResult, VARIABLES, VARIABLES_BY_SCOPE};
|
||||
|
||||
use super::types::{CreateVariableInput, ResolvedVariable, UpdateVariableInput, Variable, VariableScope};
|
||||
|
||||
pub struct VariableService {
|
||||
db: Database,
|
||||
}
|
||||
|
||||
impl VariableService {
|
||||
pub fn new(db: Database) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &str) -> DbResult<Variable> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let table = read_txn.open_table(VARIABLES)?;
|
||||
|
||||
let value = table
|
||||
.get(id)?
|
||||
.ok_or_else(|| DbError::NotFound(format!("Variable not found: {}", id)))?;
|
||||
|
||||
let variable: Variable = serde_json::from_str(value.value())
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
Ok(variable)
|
||||
}
|
||||
|
||||
fn get_by_scope_key(&self, scope_key: &str) -> DbResult<Vec<Variable>> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let idx_table = read_txn.open_table(VARIABLES_BY_SCOPE)?;
|
||||
|
||||
let variable_ids: Vec<String> = match idx_table.get(scope_key)? {
|
||||
Some(value) => serde_json::from_str(value.value())
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
drop(idx_table);
|
||||
drop(read_txn);
|
||||
|
||||
let mut variables = Vec::new();
|
||||
for id in variable_ids {
|
||||
if let Ok(variable) = self.get(&id) {
|
||||
variables.push(variable);
|
||||
}
|
||||
}
|
||||
|
||||
variables.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(variables)
|
||||
}
|
||||
|
||||
pub fn get_global(&self) -> DbResult<Vec<Variable>> {
|
||||
self.get_by_scope_key("global")
|
||||
}
|
||||
|
||||
pub fn get_by_workspace(&self, workspace_id: &str) -> DbResult<Vec<Variable>> {
|
||||
self.get_by_scope_key(&format!("workspace:{}", workspace_id))
|
||||
}
|
||||
|
||||
pub fn get_by_collection(&self, collection_id: &str) -> DbResult<Vec<Variable>> {
|
||||
self.get_by_scope_key(&format!("collection:{}", collection_id))
|
||||
}
|
||||
|
||||
pub fn get_by_request(&self, request_id: &str) -> DbResult<Vec<Variable>> {
|
||||
self.get_by_scope_key(&format!("request:{}", request_id))
|
||||
}
|
||||
|
||||
pub fn get_resolved(
|
||||
&self,
|
||||
workspace_id: Option<&str>,
|
||||
collection_id: Option<&str>,
|
||||
request_id: Option<&str>,
|
||||
) -> DbResult<Vec<ResolvedVariable>> {
|
||||
let mut resolved_map = std::collections::HashMap::new();
|
||||
|
||||
// Global variables (lowest priority)
|
||||
for var in self.get_global()? {
|
||||
resolved_map.insert(var.name.clone(), ResolvedVariable {
|
||||
name: var.name,
|
||||
value: var.value,
|
||||
scope: var.scope,
|
||||
is_secret: var.is_secret,
|
||||
});
|
||||
}
|
||||
|
||||
// Workspace variables
|
||||
if let Some(ws_id) = workspace_id {
|
||||
for var in self.get_by_workspace(ws_id)? {
|
||||
resolved_map.insert(var.name.clone(), ResolvedVariable {
|
||||
name: var.name,
|
||||
value: var.value,
|
||||
scope: var.scope,
|
||||
is_secret: var.is_secret,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Collection variables
|
||||
if let Some(coll_id) = collection_id {
|
||||
for var in self.get_by_collection(coll_id)? {
|
||||
resolved_map.insert(var.name.clone(), ResolvedVariable {
|
||||
name: var.name,
|
||||
value: var.value,
|
||||
scope: var.scope,
|
||||
is_secret: var.is_secret,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Request variables (highest priority)
|
||||
if let Some(req_id) = request_id {
|
||||
for var in self.get_by_request(req_id)? {
|
||||
resolved_map.insert(var.name.clone(), ResolvedVariable {
|
||||
name: var.name,
|
||||
value: var.value,
|
||||
scope: var.scope,
|
||||
is_secret: var.is_secret,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut resolved: Vec<_> = resolved_map.into_values().collect();
|
||||
resolved.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
pub fn create(&self, input: CreateVariableInput) -> DbResult<Variable> {
|
||||
let mut variable = Variable::new(
|
||||
input.name,
|
||||
input.value,
|
||||
input.scope,
|
||||
input.scope_id,
|
||||
);
|
||||
variable.is_secret = input.is_secret;
|
||||
variable.description = input.description;
|
||||
|
||||
let scope_key = variable.scope_key();
|
||||
let json = serde_json::to_string(&variable)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
let write_txn = self.db.begin_write()?;
|
||||
|
||||
{
|
||||
let mut table = write_txn.open_table(VARIABLES)?;
|
||||
table.insert(variable.id.as_str(), json.as_str())?;
|
||||
}
|
||||
|
||||
// Update scope index
|
||||
{
|
||||
let mut idx_table = write_txn.open_table(VARIABLES_BY_SCOPE)?;
|
||||
let ids_json = match idx_table.get(scope_key.as_str())? {
|
||||
Some(value) => value.value().to_string(),
|
||||
None => "[]".to_string(),
|
||||
};
|
||||
|
||||
let mut ids: Vec<String> = serde_json::from_str(&ids_json)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
ids.push(variable.id.clone());
|
||||
|
||||
let new_json = serde_json::to_string(&ids)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
idx_table.insert(scope_key.as_str(), new_json.as_str())?;
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(variable)
|
||||
}
|
||||
|
||||
pub fn update(&self, input: UpdateVariableInput) -> DbResult<Variable> {
|
||||
let mut variable = self.get(&input.id)?;
|
||||
|
||||
if let Some(name) = input.name {
|
||||
variable.name = name;
|
||||
}
|
||||
if let Some(value) = input.value {
|
||||
variable.value = value;
|
||||
}
|
||||
if let Some(is_secret) = input.is_secret {
|
||||
variable.is_secret = is_secret;
|
||||
}
|
||||
if let Some(description) = input.description {
|
||||
variable.description = Some(description);
|
||||
}
|
||||
variable.updated_at = Utc::now();
|
||||
|
||||
let json = serde_json::to_string(&variable)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
let write_txn = self.db.begin_write()?;
|
||||
{
|
||||
let mut table = write_txn.open_table(VARIABLES)?;
|
||||
table.insert(variable.id.as_str(), json.as_str())?;
|
||||
}
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(variable)
|
||||
}
|
||||
|
||||
pub fn delete(&self, id: &str) -> DbResult<()> {
|
||||
let variable = self.get(id)?;
|
||||
let scope_key = variable.scope_key();
|
||||
|
||||
let write_txn = self.db.begin_write()?;
|
||||
|
||||
// Remove from variables table
|
||||
{
|
||||
let mut table = write_txn.open_table(VARIABLES)?;
|
||||
table.remove(id)?;
|
||||
}
|
||||
|
||||
// Update scope index
|
||||
{
|
||||
let mut idx_table = write_txn.open_table(VARIABLES_BY_SCOPE)?;
|
||||
let ids_json = idx_table
|
||||
.get(scope_key.as_str())?
|
||||
.map(|v| v.value().to_string())
|
||||
.unwrap_or_else(|| "[]".to_string());
|
||||
let mut ids: Vec<String> = serde_json::from_str(&ids_json)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
ids.retain(|i| i != id);
|
||||
let new_json = serde_json::to_string(&ids)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
idx_table.insert(scope_key.as_str(), new_json.as_str())?;
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
87
src-tauri/src/variables/types.rs
Normal file
87
src-tauri/src/variables/types.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VariableScope {
|
||||
Global,
|
||||
Workspace,
|
||||
Collection,
|
||||
Request,
|
||||
}
|
||||
|
||||
impl Default for VariableScope {
|
||||
fn default() -> Self {
|
||||
Self::Global
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Variable {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
pub scope: VariableScope,
|
||||
pub scope_id: Option<String>,
|
||||
pub is_secret: bool,
|
||||
pub description: Option<String>,
|
||||
#[serde(default = "Utc::now")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
#[serde(default = "Utc::now")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Variable {
|
||||
pub fn new(name: String, value: String, scope: VariableScope, scope_id: Option<String>) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
name,
|
||||
value,
|
||||
scope,
|
||||
scope_id,
|
||||
is_secret: false,
|
||||
description: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scope_key(&self) -> String {
|
||||
match self.scope {
|
||||
VariableScope::Global => "global".to_string(),
|
||||
VariableScope::Workspace => format!("workspace:{}", self.scope_id.as_deref().unwrap_or("")),
|
||||
VariableScope::Collection => format!("collection:{}", self.scope_id.as_deref().unwrap_or("")),
|
||||
VariableScope::Request => format!("request:{}", self.scope_id.as_deref().unwrap_or("")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateVariableInput {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
pub scope: VariableScope,
|
||||
pub scope_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub is_secret: bool,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateVariableInput {
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
pub value: Option<String>,
|
||||
pub is_secret: Option<bool>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ResolvedVariable {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
pub scope: VariableScope,
|
||||
pub is_secret: bool,
|
||||
}
|
||||
@@ -1,188 +1,279 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { Collection } from "$lib/types/collection";
|
||||
import type { Request, HttpMethod } from "$lib/types/request";
|
||||
import type { Request } from "$lib/types/request";
|
||||
|
||||
const collections: Map<string, Collection> = new Map<string, Collection>([
|
||||
[
|
||||
"col-1",
|
||||
{
|
||||
id: "col-1",
|
||||
name: "User API",
|
||||
description: "User management endpoints",
|
||||
workspaceId: "1",
|
||||
requests: [
|
||||
{
|
||||
id: "req-1",
|
||||
name: "Get Users",
|
||||
method: "GET",
|
||||
url: "https://api.example.com/users",
|
||||
headers: [],
|
||||
params: [],
|
||||
bodyType: "none",
|
||||
body: "",
|
||||
formData: [],
|
||||
collectionId: "col-1",
|
||||
workspaceId: "1",
|
||||
},
|
||||
{
|
||||
id: "req-2",
|
||||
name: "Create User",
|
||||
method: "POST",
|
||||
url: "https://api.example.com/users",
|
||||
headers: [
|
||||
{ key: "Content-Type", value: "application/json", enabled: true },
|
||||
],
|
||||
params: [],
|
||||
bodyType: "json",
|
||||
body: '{"name": "John", "email": "john@example.com"}',
|
||||
formData: [],
|
||||
collectionId: "col-1",
|
||||
workspaceId: "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
"col-2",
|
||||
{
|
||||
id: "col-2",
|
||||
name: "Products API",
|
||||
description: "Product catalog endpoints",
|
||||
workspaceId: "1",
|
||||
requests: [
|
||||
{
|
||||
id: "req-3",
|
||||
name: "List Products",
|
||||
method: "GET",
|
||||
url: "https://api.example.com/products",
|
||||
headers: [],
|
||||
params: [{ key: "limit", value: "10", enabled: true }],
|
||||
bodyType: "none",
|
||||
body: "",
|
||||
formData: [],
|
||||
collectionId: "col-2",
|
||||
workspaceId: "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
// Collection types for Rust backend
|
||||
type RustCollection = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
workspace_id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
const standaloneRequests: Map<string, Request> = new Map<string, Request>([
|
||||
[
|
||||
"req-standalone-1",
|
||||
{
|
||||
id: "req-standalone-1",
|
||||
name: "Health Check",
|
||||
method: "GET",
|
||||
url: "https://api.example.com/health",
|
||||
headers: [],
|
||||
params: [],
|
||||
bodyType: "none",
|
||||
body: "",
|
||||
formData: [],
|
||||
collectionId: null,
|
||||
workspaceId: "1",
|
||||
},
|
||||
],
|
||||
]);
|
||||
type CreateCollectionInput = {
|
||||
name: string;
|
||||
description: string;
|
||||
workspace_id: string;
|
||||
};
|
||||
|
||||
type UpdateCollectionInput = {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
// Request types for Rust backend
|
||||
type RustRequest = {
|
||||
id: string;
|
||||
name: string;
|
||||
method: string;
|
||||
url: string;
|
||||
headers: { key: string; value: string; enabled: boolean }[];
|
||||
params: { key: string; value: string; enabled: boolean }[];
|
||||
body_type: string;
|
||||
body: string;
|
||||
form_data: {
|
||||
key: string;
|
||||
value: string;
|
||||
item_type: string;
|
||||
enabled: boolean;
|
||||
}[];
|
||||
collection_id: string | null;
|
||||
workspace_id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
type CreateRequestInput = {
|
||||
name: string;
|
||||
method: string;
|
||||
url: string;
|
||||
headers: { key: string; value: string; enabled: boolean }[];
|
||||
params: { key: string; value: string; enabled: boolean }[];
|
||||
body_type: string;
|
||||
body: string;
|
||||
form_data: {
|
||||
key: string;
|
||||
value: string;
|
||||
item_type: string;
|
||||
enabled: boolean;
|
||||
}[];
|
||||
collection_id: string | null;
|
||||
workspace_id: string;
|
||||
};
|
||||
|
||||
type UpdateRequestInput = {
|
||||
id: string;
|
||||
name?: string;
|
||||
method?: string;
|
||||
url?: string;
|
||||
headers?: { key: string; value: string; enabled: boolean }[];
|
||||
params?: { key: string; value: string; enabled: boolean }[];
|
||||
body_type?: string;
|
||||
body?: string;
|
||||
form_data?: {
|
||||
key: string;
|
||||
value: string;
|
||||
item_type: string;
|
||||
enabled: boolean;
|
||||
}[];
|
||||
collection_id?: string | null;
|
||||
};
|
||||
|
||||
// Convert Rust collection to frontend Collection
|
||||
function toCollection(
|
||||
rust: RustCollection,
|
||||
requests: Request[] = []
|
||||
): Collection {
|
||||
return {
|
||||
id: rust.id,
|
||||
name: rust.name,
|
||||
description: rust.description,
|
||||
workspaceId: rust.workspace_id,
|
||||
requests,
|
||||
};
|
||||
}
|
||||
|
||||
// Convert Rust request to frontend Request
|
||||
function toRequest(rust: RustRequest): Request {
|
||||
return {
|
||||
id: rust.id,
|
||||
name: rust.name,
|
||||
method: rust.method as Request["method"],
|
||||
url: rust.url,
|
||||
headers: rust.headers,
|
||||
params: rust.params,
|
||||
bodyType: rust.body_type as Request["bodyType"],
|
||||
body: rust.body,
|
||||
formData: rust.form_data.map((f) => ({
|
||||
key: f.key,
|
||||
value: f.value,
|
||||
type: f.item_type as "text" | "file",
|
||||
enabled: f.enabled,
|
||||
})),
|
||||
collectionId: rust.collection_id,
|
||||
workspaceId: rust.workspace_id,
|
||||
};
|
||||
}
|
||||
|
||||
export async function get_collections_by_workspace(
|
||||
workspaceId: string
|
||||
): Promise<Collection[]> {
|
||||
return [...collections.values()].filter((c) => c.workspaceId === workspaceId);
|
||||
const rustCollections = await invoke<RustCollection[]>(
|
||||
"get_collections_by_workspace",
|
||||
{ workspaceId }
|
||||
);
|
||||
|
||||
const collections: Collection[] = [];
|
||||
for (const rc of rustCollections) {
|
||||
const rustRequests = await invoke<RustRequest[]>(
|
||||
"get_requests_by_collection",
|
||||
{
|
||||
collectionId: rc.id,
|
||||
}
|
||||
);
|
||||
collections.push(toCollection(rc, rustRequests.map(toRequest)));
|
||||
}
|
||||
|
||||
return collections;
|
||||
}
|
||||
|
||||
export async function get_collection(
|
||||
id: string
|
||||
): Promise<Collection | undefined> {
|
||||
return collections.get(id);
|
||||
try {
|
||||
const rc = await invoke<RustCollection>("get_collection", { id });
|
||||
const rustRequests = await invoke<RustRequest[]>(
|
||||
"get_requests_by_collection",
|
||||
{
|
||||
collectionId: id,
|
||||
}
|
||||
);
|
||||
return toCollection(rc, rustRequests.map(toRequest));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function create_collection(
|
||||
collection: Omit<Collection, "id" | "requests">
|
||||
): Promise<Collection> {
|
||||
const newCollection: Collection = {
|
||||
...collection,
|
||||
id: crypto.randomUUID(),
|
||||
requests: [],
|
||||
export async function create_collection(collection: {
|
||||
name: string;
|
||||
description: string;
|
||||
workspaceId: string;
|
||||
}): Promise<Collection> {
|
||||
const input: CreateCollectionInput = {
|
||||
name: collection.name,
|
||||
description: collection.description,
|
||||
workspace_id: collection.workspaceId,
|
||||
};
|
||||
collections.set(newCollection.id, newCollection);
|
||||
return newCollection;
|
||||
const rc = await invoke<RustCollection>("create_collection", { input });
|
||||
return toCollection(rc, []);
|
||||
}
|
||||
|
||||
export async function update_collection(
|
||||
id: string,
|
||||
updates: Partial<Pick<Collection, "name" | "description">>
|
||||
): Promise<boolean> {
|
||||
const collection = collections.get(id);
|
||||
if (!collection) return false;
|
||||
|
||||
if (updates.name !== undefined) collection.name = updates.name;
|
||||
if (updates.description !== undefined)
|
||||
collection.description = updates.description;
|
||||
return true;
|
||||
try {
|
||||
const input: UpdateCollectionInput = {
|
||||
id,
|
||||
name: updates.name,
|
||||
description: updates.description,
|
||||
};
|
||||
await invoke<RustCollection>("update_collection", { input });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function delete_collection(id: string): Promise<boolean> {
|
||||
return collections.delete(id);
|
||||
try {
|
||||
await invoke<void>("delete_collection", { id });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function get_standalone_requests_by_workspace(
|
||||
workspaceId: string
|
||||
): Promise<Request[]> {
|
||||
return [...standaloneRequests.values()].filter(
|
||||
(r) => r.workspaceId === workspaceId
|
||||
const rustRequests = await invoke<RustRequest[]>(
|
||||
"get_standalone_requests_by_workspace",
|
||||
{ workspaceId }
|
||||
);
|
||||
return rustRequests.map(toRequest);
|
||||
}
|
||||
|
||||
export async function get_request(id: string): Promise<Request | undefined> {
|
||||
for (const collection of collections.values()) {
|
||||
const request = collection.requests.find((r) => r.id === id);
|
||||
if (request) return request;
|
||||
try {
|
||||
const rr = await invoke<RustRequest>("get_request", { id });
|
||||
return toRequest(rr);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return standaloneRequests.get(id);
|
||||
}
|
||||
|
||||
export async function create_request(
|
||||
request: Omit<Request, "id">
|
||||
): Promise<Request> {
|
||||
const newRequest: Request = {
|
||||
...request,
|
||||
id: crypto.randomUUID(),
|
||||
const input: CreateRequestInput = {
|
||||
name: request.name,
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
params: request.params,
|
||||
body_type: request.bodyType,
|
||||
body: request.body,
|
||||
form_data: request.formData.map((f) => ({
|
||||
key: f.key,
|
||||
value: f.value,
|
||||
item_type: f.type,
|
||||
enabled: f.enabled,
|
||||
})),
|
||||
collection_id: request.collectionId,
|
||||
workspace_id: request.workspaceId,
|
||||
};
|
||||
|
||||
if (request.collectionId) {
|
||||
const collection = collections.get(request.collectionId);
|
||||
if (collection) {
|
||||
collection.requests.push(newRequest);
|
||||
}
|
||||
} else {
|
||||
standaloneRequests.set(newRequest.id, newRequest);
|
||||
}
|
||||
|
||||
return newRequest;
|
||||
const rr = await invoke<RustRequest>("create_request", { input });
|
||||
return toRequest(rr);
|
||||
}
|
||||
|
||||
export async function update_request(
|
||||
id: string,
|
||||
updates: Partial<Omit<Request, "id">>
|
||||
): Promise<boolean> {
|
||||
const request = await get_request(id);
|
||||
if (!request) return false;
|
||||
|
||||
Object.assign(request, updates);
|
||||
return true;
|
||||
try {
|
||||
const input: UpdateRequestInput = {
|
||||
id,
|
||||
name: updates.name,
|
||||
method: updates.method,
|
||||
url: updates.url,
|
||||
headers: updates.headers,
|
||||
params: updates.params,
|
||||
body_type: updates.bodyType,
|
||||
body: updates.body,
|
||||
form_data: updates.formData?.map((f) => ({
|
||||
key: f.key,
|
||||
value: f.value,
|
||||
item_type: f.type,
|
||||
enabled: f.enabled,
|
||||
})),
|
||||
collection_id: updates.collectionId,
|
||||
};
|
||||
await invoke<RustRequest>("update_request", { input });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function delete_request(id: string): Promise<boolean> {
|
||||
if (standaloneRequests.delete(id)) return true;
|
||||
|
||||
for (const collection of collections.values()) {
|
||||
const index = collection.requests.findIndex((r) => r.id === id);
|
||||
if (index !== -1) {
|
||||
collection.requests.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
await invoke<void>("delete_request", { id });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
104
src/lib/services/http.ts
Normal file
104
src/lib/services/http.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { Request } from "$lib/types/request";
|
||||
import type { ResolvedVariable } from "$lib/types/variable";
|
||||
import { interpolate_variables } from "./variables";
|
||||
|
||||
export type HttpResponse = {
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: { key: string; value: string }[];
|
||||
body: string;
|
||||
timeMs: number;
|
||||
sizeBytes: number;
|
||||
};
|
||||
|
||||
type RustHttpRequest = {
|
||||
method: string;
|
||||
url: string;
|
||||
headers: { key: string; value: string; enabled: boolean }[];
|
||||
params: { key: string; value: string; enabled: boolean }[];
|
||||
body_type: string;
|
||||
body: string;
|
||||
form_data: {
|
||||
key: string;
|
||||
value: string;
|
||||
item_type: string;
|
||||
enabled: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
type RustHttpResponse = {
|
||||
status: number;
|
||||
status_text: string;
|
||||
headers: { key: string; value: string }[];
|
||||
body: string;
|
||||
time_ms: number;
|
||||
size_bytes: number;
|
||||
};
|
||||
|
||||
export async function send_request(
|
||||
request: Request,
|
||||
variables: ResolvedVariable[] = []
|
||||
): Promise<HttpResponse> {
|
||||
// Interpolate variables in URL
|
||||
const url = interpolate_variables(request.url, variables);
|
||||
|
||||
// Interpolate variables in headers
|
||||
const headers = request.headers.map((h) => ({
|
||||
key: interpolate_variables(h.key, variables),
|
||||
value: interpolate_variables(h.value, variables),
|
||||
enabled: h.enabled,
|
||||
}));
|
||||
|
||||
// Interpolate variables in params
|
||||
const params = request.params.map((p) => ({
|
||||
key: interpolate_variables(p.key, variables),
|
||||
value: interpolate_variables(p.value, variables),
|
||||
enabled: p.enabled,
|
||||
}));
|
||||
|
||||
// Interpolate variables in body
|
||||
const body = interpolate_variables(request.body, variables);
|
||||
|
||||
// Interpolate variables in form data
|
||||
const formData = request.formData.map((f) => ({
|
||||
key: interpolate_variables(f.key, variables),
|
||||
value: interpolate_variables(f.value, variables),
|
||||
item_type: f.type,
|
||||
enabled: f.enabled,
|
||||
}));
|
||||
|
||||
const rustRequest: RustHttpRequest = {
|
||||
method: request.method,
|
||||
url,
|
||||
headers,
|
||||
params,
|
||||
body_type: request.bodyType,
|
||||
body,
|
||||
form_data: formData,
|
||||
};
|
||||
|
||||
const response = await invoke<RustHttpResponse>("send_http_request", {
|
||||
request: rustRequest,
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
statusText: response.status_text,
|
||||
headers: response.headers,
|
||||
body: response.body,
|
||||
timeMs: response.time_ms,
|
||||
sizeBytes: response.size_bytes,
|
||||
};
|
||||
}
|
||||
|
||||
export function format_size(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
export function format_time(ms: number): string {
|
||||
if (ms < 1000) return `${ms} ms`;
|
||||
return `${(ms / 1000).toFixed(2)} s`;
|
||||
}
|
||||
@@ -1,93 +1,116 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type {
|
||||
Variable,
|
||||
VariableScope,
|
||||
ResolvedVariable,
|
||||
} from "$lib/types/variable";
|
||||
|
||||
const variables: Map<string, Variable> = new Map<string, Variable>([
|
||||
[
|
||||
"var-1",
|
||||
{
|
||||
id: "var-1",
|
||||
name: "BASE_URL",
|
||||
value: "https://api.example.com",
|
||||
scope: "global",
|
||||
scopeId: null,
|
||||
isSecret: false,
|
||||
description: "Base URL for all API requests",
|
||||
},
|
||||
],
|
||||
[
|
||||
"var-2",
|
||||
{
|
||||
id: "var-2",
|
||||
name: "API_KEY",
|
||||
value: "sk-1234567890",
|
||||
scope: "global",
|
||||
scopeId: null,
|
||||
isSecret: true,
|
||||
description: "API authentication key",
|
||||
},
|
||||
],
|
||||
[
|
||||
"var-3",
|
||||
{
|
||||
id: "var-3",
|
||||
name: "USER_ID",
|
||||
value: "user-123",
|
||||
scope: "workspace",
|
||||
scopeId: "1",
|
||||
isSecret: false,
|
||||
description: "Default user ID for testing",
|
||||
},
|
||||
],
|
||||
[
|
||||
"var-4",
|
||||
{
|
||||
id: "var-4",
|
||||
name: "AUTH_TOKEN",
|
||||
value: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
|
||||
scope: "workspace",
|
||||
scopeId: "1",
|
||||
isSecret: true,
|
||||
description: "Authentication token",
|
||||
},
|
||||
],
|
||||
]);
|
||||
// Rust variable types
|
||||
type RustVariable = {
|
||||
id: string;
|
||||
name: string;
|
||||
value: string;
|
||||
scope: string;
|
||||
scope_id: string | null;
|
||||
is_secret: boolean;
|
||||
description: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export async function get_all_variables(): Promise<Variable[]> {
|
||||
return [...variables.values()];
|
||||
type RustResolvedVariable = {
|
||||
name: string;
|
||||
value: string;
|
||||
scope: string;
|
||||
is_secret: boolean;
|
||||
};
|
||||
|
||||
type CreateVariableInput = {
|
||||
name: string;
|
||||
value: string;
|
||||
scope: string;
|
||||
scope_id: string | null;
|
||||
is_secret: boolean;
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
type UpdateVariableInput = {
|
||||
id: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
is_secret?: boolean;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
function toVariable(rust: RustVariable): Variable {
|
||||
return {
|
||||
id: rust.id,
|
||||
name: rust.name,
|
||||
value: rust.value,
|
||||
scope: rust.scope as VariableScope,
|
||||
scopeId: rust.scope_id,
|
||||
isSecret: rust.is_secret,
|
||||
description: rust.description ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function toResolvedVariable(rust: RustResolvedVariable): ResolvedVariable {
|
||||
return {
|
||||
name: rust.name,
|
||||
value: rust.value,
|
||||
scope: rust.scope as VariableScope,
|
||||
isSecret: rust.is_secret,
|
||||
};
|
||||
}
|
||||
|
||||
export async function get_global_variables(): Promise<Variable[]> {
|
||||
const vars = await invoke<RustVariable[]>("get_global_variables");
|
||||
return vars.map(toVariable);
|
||||
}
|
||||
|
||||
export async function get_workspace_variables(
|
||||
workspaceId: string
|
||||
): Promise<Variable[]> {
|
||||
const vars = await invoke<RustVariable[]>("get_workspace_variables", {
|
||||
workspaceId,
|
||||
});
|
||||
return vars.map(toVariable);
|
||||
}
|
||||
|
||||
export async function get_collection_variables(
|
||||
collectionId: string
|
||||
): Promise<Variable[]> {
|
||||
const vars = await invoke<RustVariable[]>("get_collection_variables", {
|
||||
collectionId,
|
||||
});
|
||||
return vars.map(toVariable);
|
||||
}
|
||||
|
||||
export async function get_request_variables(
|
||||
requestId: string
|
||||
): Promise<Variable[]> {
|
||||
const vars = await invoke<RustVariable[]>("get_request_variables", {
|
||||
requestId,
|
||||
});
|
||||
return vars.map(toVariable);
|
||||
}
|
||||
|
||||
export async function get_variables_by_scope(
|
||||
scope: VariableScope,
|
||||
scopeId: string | null
|
||||
): Promise<Variable[]> {
|
||||
return [...variables.values()].filter(
|
||||
(v) => v.scope === scope && v.scopeId === scopeId
|
||||
);
|
||||
}
|
||||
|
||||
export async function get_global_variables(): Promise<Variable[]> {
|
||||
return get_variables_by_scope("global", null);
|
||||
}
|
||||
|
||||
export async function get_workspace_variables(
|
||||
workspaceId: string
|
||||
): Promise<Variable[]> {
|
||||
return get_variables_by_scope("workspace", workspaceId);
|
||||
}
|
||||
|
||||
export async function get_collection_variables(
|
||||
collectionId: string
|
||||
): Promise<Variable[]> {
|
||||
return get_variables_by_scope("collection", collectionId);
|
||||
}
|
||||
|
||||
export async function get_request_variables(
|
||||
requestId: string
|
||||
): Promise<Variable[]> {
|
||||
return get_variables_by_scope("request", requestId);
|
||||
switch (scope) {
|
||||
case "global":
|
||||
return get_global_variables();
|
||||
case "workspace":
|
||||
return scopeId ? get_workspace_variables(scopeId) : [];
|
||||
case "collection":
|
||||
return scopeId ? get_collection_variables(scopeId) : [];
|
||||
case "request":
|
||||
return scopeId ? get_request_variables(scopeId) : [];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function get_resolved_variables(
|
||||
@@ -95,83 +118,64 @@ export async function get_resolved_variables(
|
||||
collectionId: string | null,
|
||||
requestId: string | null
|
||||
): Promise<ResolvedVariable[]> {
|
||||
const resolved: Map<string, ResolvedVariable> = new Map();
|
||||
|
||||
const globalVars = await get_global_variables();
|
||||
for (const v of globalVars) {
|
||||
resolved.set(v.name, {
|
||||
name: v.name,
|
||||
value: v.value,
|
||||
scope: v.scope,
|
||||
isSecret: v.isSecret,
|
||||
});
|
||||
}
|
||||
|
||||
const workspaceVars = await get_workspace_variables(workspaceId);
|
||||
for (const v of workspaceVars) {
|
||||
resolved.set(v.name, {
|
||||
name: v.name,
|
||||
value: v.value,
|
||||
scope: v.scope,
|
||||
isSecret: v.isSecret,
|
||||
});
|
||||
}
|
||||
|
||||
if (collectionId) {
|
||||
const collectionVars = await get_collection_variables(collectionId);
|
||||
for (const v of collectionVars) {
|
||||
resolved.set(v.name, {
|
||||
name: v.name,
|
||||
value: v.value,
|
||||
scope: v.scope,
|
||||
isSecret: v.isSecret,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (requestId) {
|
||||
const requestVars = await get_request_variables(requestId);
|
||||
for (const v of requestVars) {
|
||||
resolved.set(v.name, {
|
||||
name: v.name,
|
||||
value: v.value,
|
||||
scope: v.scope,
|
||||
isSecret: v.isSecret,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [...resolved.values()];
|
||||
const vars = await invoke<RustResolvedVariable[]>("get_resolved_variables", {
|
||||
workspaceId,
|
||||
collectionId,
|
||||
requestId,
|
||||
});
|
||||
return vars.map(toResolvedVariable);
|
||||
}
|
||||
|
||||
export async function get_variable(id: string): Promise<Variable | undefined> {
|
||||
return variables.get(id);
|
||||
try {
|
||||
const v = await invoke<RustVariable>("get_variable", { id });
|
||||
return toVariable(v);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function create_variable(
|
||||
variable: Omit<Variable, "id">
|
||||
): Promise<Variable> {
|
||||
const newVariable: Variable = {
|
||||
...variable,
|
||||
id: crypto.randomUUID(),
|
||||
const input: CreateVariableInput = {
|
||||
name: variable.name,
|
||||
value: variable.value,
|
||||
scope: variable.scope,
|
||||
scope_id: variable.scopeId,
|
||||
is_secret: variable.isSecret,
|
||||
description: variable.description ?? null,
|
||||
};
|
||||
variables.set(newVariable.id, newVariable);
|
||||
return newVariable;
|
||||
const v = await invoke<RustVariable>("create_variable", { input });
|
||||
return toVariable(v);
|
||||
}
|
||||
|
||||
export async function update_variable(
|
||||
id: string,
|
||||
updates: Partial<Omit<Variable, "id">>
|
||||
): Promise<boolean> {
|
||||
const variable = variables.get(id);
|
||||
if (!variable) return false;
|
||||
|
||||
Object.assign(variable, updates);
|
||||
return true;
|
||||
try {
|
||||
const input: UpdateVariableInput = {
|
||||
id,
|
||||
name: updates.name,
|
||||
value: updates.value,
|
||||
is_secret: updates.isSecret,
|
||||
description: updates.description,
|
||||
};
|
||||
await invoke<RustVariable>("update_variable", { input });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function delete_variable(id: string): Promise<boolean> {
|
||||
return variables.delete(id);
|
||||
try {
|
||||
await invoke<void>("delete_variable", { id });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function interpolate_variables(
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
let selectedWorkspace = $state<Workspace | null>(null);
|
||||
let workspaceName = $state("");
|
||||
let workspaceDescription = $state("");
|
||||
let workspaceTags = $state<string[]>([]);
|
||||
let tagInput = $state("");
|
||||
|
||||
let settingsOpen = $state(false);
|
||||
let variablesOpen = $state(false);
|
||||
@@ -64,6 +66,8 @@
|
||||
selectedWorkspace = null;
|
||||
workspaceName = "";
|
||||
workspaceDescription = "";
|
||||
workspaceTags = [];
|
||||
tagInput = "";
|
||||
dialogOpen = true;
|
||||
}
|
||||
|
||||
@@ -72,9 +76,23 @@
|
||||
selectedWorkspace = workspace;
|
||||
workspaceName = workspace.Name ?? "";
|
||||
workspaceDescription = workspace.Description ?? "";
|
||||
workspaceTags = [...(workspace.Tags ?? [])];
|
||||
tagInput = "";
|
||||
dialogOpen = true;
|
||||
}
|
||||
|
||||
function addTag() {
|
||||
const trimmed = tagInput.trim();
|
||||
if (trimmed && !workspaceTags.includes(trimmed)) {
|
||||
workspaceTags = [...workspaceTags, trimmed];
|
||||
tagInput = "";
|
||||
}
|
||||
}
|
||||
|
||||
function removeTag(tag: string) {
|
||||
workspaceTags = workspaceTags.filter((t) => t !== tag);
|
||||
}
|
||||
|
||||
function openDuplicateDialog(workspace: Workspace) {
|
||||
workspaceToDuplicate = workspace;
|
||||
duplicateDialogOpen = true;
|
||||
@@ -94,13 +112,14 @@
|
||||
await create_workspace({
|
||||
name: workspaceName,
|
||||
description: workspaceDescription,
|
||||
tags: [],
|
||||
tags: workspaceTags,
|
||||
});
|
||||
} else if (selectedWorkspace != null) {
|
||||
await update_workspace({
|
||||
id: selectedWorkspace.Id,
|
||||
name: workspaceName,
|
||||
description: workspaceDescription,
|
||||
tags: workspaceTags,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -283,6 +302,40 @@
|
||||
bind:value={workspaceDescription}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<Label for="workspace-tags" class="text-end pt-2">Tags</Label>
|
||||
<div class="col-span-3 space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
id="workspace-tags"
|
||||
placeholder="Add a tag..."
|
||||
bind:value={tagInput}
|
||||
onkeydown={(e) =>
|
||||
e.key === "Enter" && (e.preventDefault(), addTag())}
|
||||
/>
|
||||
<Button type="button" variant="outline" size="sm" onclick={addTag}>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
{#if workspaceTags.length > 0}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each workspaceTags as tag}
|
||||
<Badge variant="secondary" class="gap-1">
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 hover:text-destructive"
|
||||
onclick={() => removeTag(tag)}
|
||||
aria-label="Remove tag"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Dialog.Footer>
|
||||
<Button type="submit">
|
||||
{dialogMode === "create" ? "Create workspace" : "Save changes"}
|
||||
|
||||
Reference in New Issue
Block a user