migrated remaining components to rust.

This commit is contained in:
xyroscar
2025-11-26 21:43:39 -08:00
parent 0d23ffcaec
commit 0f6d7c052b
22 changed files with 2377 additions and 282 deletions

View 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())
}

View 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;

View 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(())
}
}

View 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>,
}

View 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,
})
}

View File

@@ -0,0 +1,5 @@
mod client;
mod types;
pub use client::execute_request;
pub use types::{HttpRequest, HttpResponse, HttpResponseHeader};

View 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,
}

View File

@@ -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");

View 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())
}

View 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;

View 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(())
}
}

View 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>>,
}

View 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())
}

View 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;

View 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(())
}
}

View 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,
}