use std::collections::HashMap; use std::fs; use log::error; use rand::distributions::{Alphanumeric, DistString}; use serde::{Deserialize, Serialize}; use sqlx::types::chrono::NaiveDateTime; use sqlx::types::{Json, JsonValue}; use sqlx::{Pool, Sqlite}; use tauri::{AppHandle, Manager, WebviewWindow, Wry}; use tokio::sync::Mutex; pub enum ModelType { TypeCookieJar, TypeEnvironment, TypeFolder, TypeGrpcConnection, TypeGrpcEvent, TypeGrpcRequest, TypeHttpRequest, TypeHttpResponse, TypeWorkspace, } impl ModelType { pub fn id_prefix(&self) -> String { match self { ModelType::TypeCookieJar => "cj", ModelType::TypeEnvironment => "ev", ModelType::TypeFolder => "fl", ModelType::TypeGrpcConnection => "gc", ModelType::TypeGrpcEvent => "ge", ModelType::TypeGrpcRequest => "gr", ModelType::TypeHttpRequest => "rq", ModelType::TypeHttpResponse => "rs", ModelType::TypeWorkspace => "wk", } .to_string() } } fn default_true() -> bool { true } #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct Settings { pub id: String, pub model: String, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub theme: String, pub appearance: String, pub theme_dark: String, pub theme_light: String, pub update_channel: String, pub interface_font_size: i64, pub interface_scale: i64, pub editor_font_size: i64, pub editor_soft_wrap: bool, pub open_workspace_new_window: Option, } #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct Workspace { pub id: String, pub model: String, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub name: String, pub description: String, pub variables: Json>, // Settings #[serde(default = "default_true")] pub setting_validate_certificates: bool, #[serde(default = "default_true")] pub setting_follow_redirects: bool, pub setting_request_timeout: i64, } // Implement default for Workspace impl Workspace { pub(crate) fn new(name: String) -> Self { Self { name, model: "workspace".to_string(), setting_validate_certificates: true, setting_follow_redirects: true, ..Default::default() } } } #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] pub struct CookieX {} #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct CookieJar { pub id: String, pub model: String, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub workspace_id: String, pub name: String, pub cookies: Json>, } #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct Environment { pub id: String, pub workspace_id: String, pub model: String, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub name: String, pub variables: Json>, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct EnvironmentVariable { #[serde(default = "default_true")] pub enabled: bool, pub name: String, pub value: String, } #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct Folder { pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub id: String, pub workspace_id: String, pub folder_id: Option, pub model: String, pub name: String, pub sort_priority: f64, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct HttpRequestHeader { #[serde(default = "default_true")] pub enabled: bool, pub name: String, pub value: String, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct HttpUrlParameter { #[serde(default = "default_true")] pub enabled: bool, pub name: String, pub value: String, } fn default_http_request_method() -> String { "GET".to_string() } #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct HttpRequest { pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub id: String, pub workspace_id: String, pub folder_id: Option, pub model: String, pub sort_priority: f64, pub name: String, pub url: String, pub url_parameters: Json>, #[serde(default = "default_http_request_method")] pub method: String, pub body: Json>, pub body_type: Option, pub authentication: Json>, pub authentication_type: Option, pub headers: Json>, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct HttpResponseHeader { pub name: String, pub value: String, } #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct HttpResponse { pub id: String, pub model: String, pub workspace_id: String, pub request_id: String, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub error: Option, pub url: String, pub content_length: Option, pub version: Option, pub elapsed: i64, pub elapsed_headers: i64, pub remote_addr: Option, pub status: i64, pub status_reason: Option, pub body_path: Option, pub headers: Json>, } impl HttpResponse { pub(crate) fn new() -> Self { Self { model: "http_response".to_string(), ..Default::default() } } } #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct GrpcMetadataEntry { #[serde(default = "default_true")] pub enabled: bool, pub name: String, pub value: String, } #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct GrpcRequest { pub id: String, pub model: String, pub workspace_id: String, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub folder_id: Option, pub name: String, pub sort_priority: f64, pub url: String, pub service: Option, pub method: Option, pub message: String, pub authentication_type: Option, pub authentication: Json>, pub metadata: Json>, } #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct GrpcConnection { pub id: String, pub model: String, pub workspace_id: String, pub request_id: String, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub service: String, pub method: String, pub elapsed: i64, pub status: i64, pub url: String, pub error: Option, pub trailers: Json>, } #[derive(sqlx::Type, Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "snake_case")] #[sqlx(rename_all = "snake_case")] pub enum GrpcEventType { Info, Error, ClientMessage, ServerMessage, ConnectionStart, ConnectionEnd, } impl Default for GrpcEventType { fn default() -> Self { GrpcEventType::Info } } #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct GrpcEvent { pub id: String, pub model: String, pub workspace_id: String, pub request_id: String, pub connection_id: String, pub created_at: NaiveDateTime, pub content: String, pub event_type: GrpcEventType, pub metadata: Json>, pub status: Option, pub error: Option, } #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct KeyValue { pub model: String, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub namespace: String, pub key: String, pub value: String, } pub async fn set_key_value_string( mgr: &impl Manager, namespace: &str, key: &str, value: &str, ) -> (KeyValue, bool) { let encoded = serde_json::to_string(value); set_key_value_raw(mgr, namespace, key, &encoded.unwrap()).await } pub async fn set_key_value_int( mgr: &impl Manager, namespace: &str, key: &str, value: i32, ) -> (KeyValue, bool) { let encoded = serde_json::to_string(&value); set_key_value_raw(mgr, namespace, key, &encoded.unwrap()).await } pub async fn get_key_value_string( mgr: &impl Manager, namespace: &str, key: &str, default: &str, ) -> String { match get_key_value_raw(mgr, namespace, key).await { None => default.to_string(), Some(v) => { let result = serde_json::from_str(&v.value); match result { Ok(v) => v, Err(e) => { error!("Failed to parse string key value: {}", e); default.to_string() } } } } } pub async fn get_key_value_int( mgr: &impl Manager, namespace: &str, key: &str, default: i32, ) -> i32 { match get_key_value_raw(mgr, namespace, key).await { None => default.clone(), Some(v) => { let result = serde_json::from_str(&v.value); match result { Ok(v) => v, Err(e) => { error!("Failed to parse int key value: {}", e); default.clone() } } } } } pub async fn set_key_value_raw( mgr: &impl Manager, namespace: &str, key: &str, value: &str, ) -> (KeyValue, bool) { let db = get_db(mgr).await; let existing = get_key_value_raw(mgr, namespace, key).await; sqlx::query!( r#" INSERT INTO key_values (namespace, key, value) VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET updated_at = CURRENT_TIMESTAMP, value = excluded.value "#, namespace, key, value, ) .execute(&db) .await .expect("Failed to insert key value"); let kv = get_key_value_raw(mgr, namespace, key) .await .expect("Failed to get key value"); (kv, existing.is_none()) } pub async fn get_key_value_raw( mgr: &impl Manager, namespace: &str, key: &str, ) -> Option { let db = get_db(mgr).await; sqlx::query_as!( KeyValue, r#" SELECT model, created_at, updated_at, namespace, key, value FROM key_values WHERE namespace = ? AND key = ? "#, namespace, key, ) .fetch_one(&db) .await .ok() } pub async fn list_workspaces(mgr: &impl Manager) -> Result, sqlx::Error> { let db = get_db(mgr).await; sqlx::query_as!( Workspace, r#" SELECT id, model, created_at, updated_at, name, description, setting_request_timeout, setting_follow_redirects, setting_validate_certificates, variables AS "variables!: sqlx::types::Json>" FROM workspaces "#, ) .fetch_all(&db) .await } pub async fn get_workspace(mgr: &impl Manager, id: &str) -> Result { let db = get_db(mgr).await; sqlx::query_as!( Workspace, r#" SELECT id, model, created_at, updated_at, name, description, setting_request_timeout, setting_follow_redirects, setting_validate_certificates, variables AS "variables!: sqlx::types::Json>" FROM workspaces WHERE id = ? "#, id, ) .fetch_one(&db) .await } pub async fn delete_workspace(window: &WebviewWindow, id: &str) -> Result { let db = get_db(window).await; let workspace = get_workspace(window, id).await?; let _ = sqlx::query!( r#" DELETE FROM workspaces WHERE id = ? "#, id, ) .execute(&db) .await; for r in list_responses_by_workspace_id(window, id).await? { delete_http_response(window, &r.id).await?; } emit_deleted_model(window, workspace) } pub async fn get_cookie_jar(mgr: &impl Manager, id: &str) -> Result { let db = get_db(mgr).await; sqlx::query_as!( CookieJar, r#" SELECT id, model, created_at, updated_at, workspace_id, name, cookies AS "cookies!: sqlx::types::Json>" FROM cookie_jars WHERE id = ? "#, id, ) .fetch_one(&db) .await } pub async fn list_cookie_jars( mgr: &impl Manager, workspace_id: &str, ) -> Result, sqlx::Error> { let db = get_db(mgr).await; sqlx::query_as!( CookieJar, r#" SELECT id, model, created_at, updated_at, workspace_id, name, cookies AS "cookies!: sqlx::types::Json>" FROM cookie_jars WHERE workspace_id = ? "#, workspace_id, ) .fetch_all(&db) .await } pub async fn delete_cookie_jar(window: &WebviewWindow, id: &str) -> Result { let cookie_jar = get_cookie_jar(window, id).await?; let db = get_db(window).await; let _ = sqlx::query!( r#" DELETE FROM cookie_jars WHERE id = ? "#, id, ) .execute(&db) .await; emit_deleted_model(window, cookie_jar) } pub async fn duplicate_grpc_request( window: &WebviewWindow, id: &str, ) -> Result { let mut request = get_grpc_request(window, id).await?.clone(); request.id = "".to_string(); upsert_grpc_request(window, &request).await } pub async fn upsert_grpc_request( window: &WebviewWindow, request: &GrpcRequest, ) -> Result { let db = get_db(window).await; let id = match request.id.as_str() { "" => generate_model_id(ModelType::TypeGrpcRequest), _ => request.id.to_string(), }; let trimmed_name = request.name.trim(); sqlx::query!( r#" INSERT INTO grpc_requests ( id, name, workspace_id, folder_id, sort_priority, url, service, method, message, authentication_type, authentication, metadata ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET updated_at = CURRENT_TIMESTAMP, name = excluded.name, folder_id = excluded.folder_id, sort_priority = excluded.sort_priority, url = excluded.url, service = excluded.service, method = excluded.method, message = excluded.message, authentication_type = excluded.authentication_type, authentication = excluded.authentication, metadata = excluded.metadata "#, id, trimmed_name, request.workspace_id, request.folder_id, request.sort_priority, request.url, request.service, request.method, request.message, request.authentication_type, request.authentication, request.metadata, ) .execute(&db) .await?; match get_grpc_request(window, &id).await { Ok(m) => Ok(emit_upserted_model(window, m)), Err(e) => Err(e), } } pub async fn get_grpc_request( mgr: &impl Manager, id: &str, ) -> Result { let db = get_db(mgr).await; sqlx::query_as!( GrpcRequest, r#" SELECT id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority, url, service, method, message, authentication_type, authentication AS "authentication!: Json>", metadata AS "metadata!: sqlx::types::Json>" FROM grpc_requests WHERE id = ? "#, id, ) .fetch_one(&db) .await } pub async fn list_grpc_requests( mgr: &impl Manager, workspace_id: &str, ) -> Result, sqlx::Error> { let db = get_db(mgr).await; sqlx::query_as!( GrpcRequest, r#" SELECT id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority, url, service, method, message, authentication_type, authentication AS "authentication!: Json>", metadata AS "metadata!: sqlx::types::Json>" FROM grpc_requests WHERE workspace_id = ? "#, workspace_id, ) .fetch_all(&db) .await } pub async fn upsert_grpc_connection( window: &WebviewWindow, connection: &GrpcConnection, ) -> Result { let db = get_db(window).await; let id = match connection.id.as_str() { "" => generate_model_id(ModelType::TypeGrpcConnection), _ => connection.id.to_string(), }; sqlx::query!( r#" INSERT INTO grpc_connections ( id, workspace_id, request_id, service, method, elapsed, status, error, trailers, url ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET updated_at = CURRENT_TIMESTAMP, service = excluded.service, method = excluded.method, elapsed = excluded.elapsed, status = excluded.status, error = excluded.error, trailers = excluded.trailers, url = excluded.url "#, id, connection.workspace_id, connection.request_id, connection.service, connection.method, connection.elapsed, connection.status, connection.error, connection.trailers, connection.url, ) .execute(&db) .await?; match get_grpc_connection(window, &id).await { Ok(m) => Ok(emit_upserted_model(window, m)), Err(e) => Err(e), } } pub async fn get_grpc_connection( mgr: &impl Manager, id: &str, ) -> Result { let db = get_db(mgr).await; sqlx::query_as!( GrpcConnection, r#" SELECT id, model, workspace_id, request_id, created_at, updated_at, service, method, elapsed, status, error, url, trailers AS "trailers!: sqlx::types::Json>" FROM grpc_connections WHERE id = ? "#, id, ) .fetch_one(&db) .await } pub async fn list_grpc_connections( mgr: &impl Manager, request_id: &str, ) -> Result, sqlx::Error> { let db = get_db(mgr).await; sqlx::query_as!( GrpcConnection, r#" SELECT id, model, workspace_id, request_id, created_at, updated_at, service, method, elapsed, status, error, url, trailers AS "trailers!: sqlx::types::Json>" FROM grpc_connections WHERE request_id = ? ORDER BY created_at DESC "#, request_id, ) .fetch_all(&db) .await } pub async fn upsert_grpc_event( window: &WebviewWindow, event: &GrpcEvent, ) -> Result { let db = get_db(window).await; let id = match event.id.as_str() { "" => generate_model_id(ModelType::TypeGrpcEvent), _ => event.id.to_string(), }; sqlx::query!( r#" INSERT INTO grpc_events ( id, workspace_id, request_id, connection_id, content, event_type, metadata, status, error ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET updated_at = CURRENT_TIMESTAMP, content = excluded.content, event_type = excluded.event_type, metadata = excluded.metadata, status = excluded.status, error = excluded.error "#, id, event.workspace_id, event.request_id, event.connection_id, event.content, event.event_type, event.metadata, event.status, event.error, ) .execute(&db) .await?; match get_grpc_event(window, &id).await { Ok(m) => Ok(emit_upserted_model(window, m)), Err(e) => Err(e), } } pub async fn get_grpc_event(mgr: &impl Manager, id: &str) -> Result { let db = get_db(mgr).await; sqlx::query_as!( GrpcEvent, r#" SELECT id, model, workspace_id, request_id, connection_id, created_at, content, status, error, event_type AS "event_type!: GrpcEventType", metadata AS "metadata!: sqlx::types::Json>" FROM grpc_events WHERE id = ? "#, id, ) .fetch_one(&db) .await } pub async fn list_grpc_events( mgr: &impl Manager, connection_id: &str, ) -> Result, sqlx::Error> { let db = get_db(mgr).await; sqlx::query_as!( GrpcEvent, r#" SELECT id, model, workspace_id, request_id, connection_id, created_at, content, status, error, event_type AS "event_type!: GrpcEventType", metadata AS "metadata!: sqlx::types::Json>" FROM grpc_events WHERE connection_id = ? "#, connection_id, ) .fetch_all(&db) .await } pub async fn upsert_cookie_jar( window: &WebviewWindow, cookie_jar: &CookieJar, ) -> Result { let id = match cookie_jar.id.as_str() { "" => generate_model_id(ModelType::TypeCookieJar), _ => cookie_jar.id.to_string(), }; let trimmed_name = cookie_jar.name.trim(); let db = get_db(window).await; sqlx::query!( r#" INSERT INTO cookie_jars ( id, workspace_id, name, cookies ) VALUES (?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET updated_at = CURRENT_TIMESTAMP, name = excluded.name, cookies = excluded.cookies "#, id, cookie_jar.workspace_id, trimmed_name, cookie_jar.cookies, ) .execute(&db) .await?; match get_cookie_jar(window, &id).await { Ok(m) => Ok(emit_upserted_model(window, m)), Err(e) => Err(e), } } pub async fn list_environments( mgr: &impl Manager, workspace_id: &str, ) -> Result, sqlx::Error> { let db = get_db(mgr).await; sqlx::query_as!( Environment, r#" SELECT id, workspace_id, model, created_at, updated_at, name, variables AS "variables!: sqlx::types::Json>" FROM environments WHERE workspace_id = ? "#, workspace_id, ) .fetch_all(&db) .await } pub async fn delete_environment( window: &WebviewWindow, id: &str, ) -> Result { let db = get_db(window).await; let env = get_environment(window, id).await?; let _ = sqlx::query!( r#" DELETE FROM environments WHERE id = ? "#, id, ) .execute(&db) .await; emit_deleted_model(window, env) } async fn get_settings(mgr: &impl Manager) -> Result { let db = get_db(mgr).await; sqlx::query_as!( Settings, r#" SELECT id, model, created_at, updated_at, theme, appearance, theme_dark, theme_light, update_channel, interface_font_size, interface_scale, editor_font_size, editor_soft_wrap, open_workspace_new_window FROM settings WHERE id = 'default' "#, ) .fetch_one(&db) .await } pub async fn get_or_create_settings(mgr: &impl Manager) -> Settings { if let Ok(settings) = get_settings(mgr).await { return settings; } let db = get_db(mgr).await; sqlx::query!( r#" INSERT INTO settings (id) VALUES ('default') "#, ) .execute(&db) .await .expect("Failed to insert settings"); get_settings(mgr).await.expect("Failed to get settings") } pub async fn update_settings( window: &WebviewWindow, settings: Settings, ) -> Result { let db = get_db(window).await; sqlx::query!( r#" UPDATE settings SET ( theme, appearance, theme_dark, theme_light, update_channel, interface_font_size, interface_scale, editor_font_size, editor_soft_wrap, open_workspace_new_window ) = (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) WHERE id = 'default'; "#, settings.theme, settings.appearance, settings.theme_dark, settings.theme_light, settings.update_channel, settings.interface_font_size, settings.interface_scale, settings.editor_font_size, settings.editor_soft_wrap, settings.open_workspace_new_window, ) .execute(&db) .await?; match get_settings(window).await { Ok(m) => Ok(emit_upserted_model(window, m)), Err(e) => Err(e), } } pub async fn upsert_environment( window: &WebviewWindow, environment: Environment, ) -> Result { let id = match environment.id.as_str() { "" => generate_model_id(ModelType::TypeEnvironment), _ => environment.id.to_string(), }; let trimmed_name = environment.name.trim(); let db = get_db(window).await; sqlx::query!( r#" INSERT INTO environments ( id, workspace_id, name, variables ) VALUES (?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET updated_at = CURRENT_TIMESTAMP, name = excluded.name, variables = excluded.variables "#, id, environment.workspace_id, trimmed_name, environment.variables, ) .execute(&db) .await?; match get_environment(window, &id).await { Ok(m) => Ok(emit_upserted_model(window, m)), Err(e) => Err(e), } } pub async fn get_environment( mgr: &impl Manager, id: &str, ) -> Result { let db = get_db(mgr).await; sqlx::query_as!( Environment, r#" SELECT id, model, workspace_id, created_at, updated_at, name, variables AS "variables!: sqlx::types::Json>" FROM environments WHERE id = ? "#, id, ) .fetch_one(&db) .await } pub async fn get_folder(mgr: &impl Manager, id: &str) -> Result { let db = get_db(mgr).await; sqlx::query_as!( Folder, r#" SELECT id, model, workspace_id, created_at, updated_at, folder_id, name, sort_priority FROM folders WHERE id = ? "#, id, ) .fetch_one(&db) .await } pub async fn list_folders( mgr: &impl Manager, workspace_id: &str, ) -> Result, sqlx::Error> { let db = get_db(mgr).await; sqlx::query_as!( Folder, r#" SELECT id, model, workspace_id, created_at, updated_at, folder_id, name, sort_priority FROM folders WHERE workspace_id = ? "#, workspace_id, ) .fetch_all(&db) .await } pub async fn delete_folder(window: &WebviewWindow, id: &str) -> Result { let folder = get_folder(window, id).await?; let db = get_db(window).await; let _ = sqlx::query!( r#" DELETE FROM folders WHERE id = ? "#, id, ) .execute(&db) .await; emit_deleted_model(window, folder) } pub async fn upsert_folder(window: &WebviewWindow, r: Folder) -> Result { let id = match r.id.as_str() { "" => generate_model_id(ModelType::TypeFolder), _ => r.id.to_string(), }; let trimmed_name = r.name.trim(); let db = get_db(window).await; sqlx::query!( r#" INSERT INTO folders ( id, workspace_id, folder_id, name, sort_priority ) VALUES (?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET updated_at = CURRENT_TIMESTAMP, name = excluded.name, folder_id = excluded.folder_id, sort_priority = excluded.sort_priority "#, id, r.workspace_id, r.folder_id, trimmed_name, r.sort_priority, ) .execute(&db) .await?; match get_folder(window, &id).await { Ok(m) => Ok(emit_upserted_model(window, m)), Err(e) => Err(e), } } pub async fn duplicate_http_request( window: &WebviewWindow, id: &str, ) -> Result { let mut request = get_http_request(window, id).await?.clone(); request.id = "".to_string(); upsert_http_request(window, request).await } pub async fn upsert_http_request( window: &WebviewWindow, r: HttpRequest, ) -> Result { let id = match r.id.as_str() { "" => generate_model_id(ModelType::TypeHttpRequest), _ => r.id.to_string(), }; let trimmed_name = r.name.trim(); let db = get_db(window).await; sqlx::query!( r#" INSERT INTO http_requests ( id, workspace_id, folder_id, name, url, url_parameters, method, body, body_type, authentication, authentication_type, headers, sort_priority ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET updated_at = CURRENT_TIMESTAMP, name = excluded.name, folder_id = excluded.folder_id, method = excluded.method, headers = excluded.headers, body = excluded.body, body_type = excluded.body_type, authentication = excluded.authentication, authentication_type = excluded.authentication_type, url = excluded.url, url_parameters = excluded.url_parameters, sort_priority = excluded.sort_priority "#, id, r.workspace_id, r.folder_id, trimmed_name, r.url, r.url_parameters, r.method, r.body, r.body_type, r.authentication, r.authentication_type, r.headers, r.sort_priority, ) .execute(&db) .await?; match get_http_request(window, &id).await { Ok(m) => Ok(emit_upserted_model(window, m)), Err(e) => Err(e), } } pub async fn list_http_requests( mgr: &impl Manager, workspace_id: &str, ) -> Result, sqlx::Error> { let db = get_db(mgr).await; sqlx::query_as!( HttpRequest, r#" SELECT id, model, workspace_id, folder_id, created_at, updated_at, name, url, url_parameters AS "url_parameters!: sqlx::types::Json>", method, body_type, authentication_type, sort_priority, body AS "body!: Json>", authentication AS "authentication!: Json>", headers AS "headers!: sqlx::types::Json>" FROM http_requests WHERE workspace_id = ? "#, workspace_id, ) .fetch_all(&db) .await } pub async fn get_http_request( mgr: &impl Manager, id: &str, ) -> Result { let db = get_db(mgr).await; sqlx::query_as!( HttpRequest, r#" SELECT id, model, workspace_id, folder_id, created_at, updated_at, name, url, method, body_type, authentication_type, sort_priority, url_parameters AS "url_parameters!: sqlx::types::Json>", body AS "body!: Json>", authentication AS "authentication!: Json>", headers AS "headers!: sqlx::types::Json>" FROM http_requests WHERE id = ? "#, id, ) .fetch_one(&db) .await } pub async fn delete_http_request( window: &WebviewWindow, id: &str, ) -> Result { let req = get_http_request(window, id).await?; // DB deletes will cascade but this will delete the files delete_all_http_responses(window, id).await?; let db = get_db(window).await; let _ = sqlx::query!( r#" DELETE FROM http_requests WHERE id = ? "#, id, ) .execute(&db) .await; emit_deleted_model(window, req) } #[allow(clippy::too_many_arguments)] pub async fn create_http_response( window: &WebviewWindow, request_id: &str, elapsed: i64, elapsed_headers: i64, url: &str, status: i64, status_reason: Option<&str>, content_length: Option, body_path: Option<&str>, headers: Vec, version: Option<&str>, remote_addr: Option<&str>, ) -> Result { let req = get_http_request(window, request_id).await?; let id = generate_model_id(ModelType::TypeHttpResponse); let headers_json = Json(headers); let db = get_db(window).await; sqlx::query!( r#" INSERT INTO http_responses ( id, request_id, workspace_id, elapsed, elapsed_headers, url, status, status_reason, content_length, body_path, headers, version, remote_addr ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); "#, id, request_id, req.workspace_id, elapsed, elapsed_headers, url, status, status_reason, content_length, body_path, headers_json, version, remote_addr, ) .execute(&db) .await?; match get_http_response(window, &id).await { Ok(m) => Ok(emit_upserted_model(window, m)), Err(e) => Err(e), } } pub async fn cancel_pending_grpc_connections(app: &AppHandle) -> Result<(), sqlx::Error> { let db = get_db(app).await; sqlx::query!( r#" UPDATE grpc_connections SET (elapsed) = (-1) WHERE elapsed = 0; "#, ) .execute(&db) .await?; Ok(()) } pub async fn cancel_pending_responses(app: &AppHandle) -> Result<(), sqlx::Error> { let db = get_db(app).await; sqlx::query!( r#" UPDATE http_responses SET (elapsed, status_reason) = (-1, 'Cancelled') WHERE elapsed = 0; "#, ) .execute(&db) .await?; Ok(()) } pub async fn update_response_if_id( window: &WebviewWindow, response: &HttpResponse, ) -> Result { if response.id.is_empty() { Ok(response.clone()) } else { update_response(window, response).await } } pub async fn upsert_workspace( window: &WebviewWindow, workspace: Workspace, ) -> Result { let id = match workspace.id.as_str() { "" => generate_model_id(ModelType::TypeWorkspace), _ => workspace.id.to_string(), }; let trimmed_name = workspace.name.trim(); let db = get_db(window).await; sqlx::query!( r#" INSERT INTO workspaces ( id, name, description, variables, setting_request_timeout, setting_follow_redirects, setting_validate_certificates ) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET updated_at = CURRENT_TIMESTAMP, name = excluded.name, description = excluded.description, variables = excluded.variables, setting_request_timeout = excluded.setting_request_timeout, setting_follow_redirects = excluded.setting_follow_redirects, setting_validate_certificates = excluded.setting_validate_certificates "#, id, trimmed_name, workspace.description, workspace.variables, workspace.setting_request_timeout, workspace.setting_follow_redirects, workspace.setting_validate_certificates, ) .execute(&db) .await?; match get_workspace(window, &id).await { Ok(m) => Ok(emit_upserted_model(window, m)), Err(e) => Err(e), } } pub async fn update_response( window: &WebviewWindow, response: &HttpResponse, ) -> Result { let db = get_db(window).await; sqlx::query!( r#" UPDATE http_responses SET ( elapsed, elapsed_headers, url, status, status_reason, content_length, body_path, error, headers, version, remote_addr, updated_at ) = (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?; "#, response.elapsed, response.elapsed_headers, response.url, response.status, response.status_reason, response.content_length, response.body_path, response.error, response.headers, response.version, response.remote_addr, response.id, ) .execute(&db) .await?; match get_http_response(window, &response.id).await { Ok(m) => Ok(emit_upserted_model(window, m)), Err(e) => Err(e), } } pub async fn get_http_response( mgr: &impl Manager, id: &str, ) -> Result { let db = get_db(mgr).await; sqlx::query_as!( HttpResponse, r#" SELECT id, model, workspace_id, request_id, updated_at, created_at, url, status, status_reason, content_length, body_path, elapsed, elapsed_headers, error, version, remote_addr, headers AS "headers!: sqlx::types::Json>" FROM http_responses WHERE id = ? "#, id, ) .fetch_one(&db) .await } pub async fn list_responses( mgr: &impl Manager, request_id: &str, limit: Option, ) -> Result, sqlx::Error> { let limit_unwrapped = limit.unwrap_or_else(|| i64::MAX); let db = get_db(mgr).await; sqlx::query_as!( HttpResponse, r#" SELECT id, model, workspace_id, request_id, updated_at, created_at, url, status, status_reason, content_length, body_path, elapsed, elapsed_headers, error, version, remote_addr, headers AS "headers!: sqlx::types::Json>" FROM http_responses WHERE request_id = ? ORDER BY created_at DESC LIMIT ? "#, request_id, limit_unwrapped, ) .fetch_all(&db) .await } pub async fn list_responses_by_workspace_id( mgr: &impl Manager, workspace_id: &str, ) -> Result, sqlx::Error> { let db = get_db(mgr).await; sqlx::query_as!( HttpResponse, r#" SELECT id, model, workspace_id, request_id, updated_at, created_at, url, status, status_reason, content_length, body_path, elapsed, elapsed_headers, error, version, remote_addr, headers AS "headers!: sqlx::types::Json>" FROM http_responses WHERE workspace_id = ? ORDER BY created_at DESC "#, workspace_id, ) .fetch_all(&db) .await } pub async fn delete_grpc_request( window: &WebviewWindow, id: &str, ) -> Result { let req = get_grpc_request(window, id).await?; let db = get_db(window).await; let _ = sqlx::query!( r#" DELETE FROM grpc_requests WHERE id = ? "#, id, ) .execute(&db) .await; emit_deleted_model(window, req) } pub async fn delete_grpc_connection( window: &WebviewWindow, id: &str, ) -> Result { let resp = get_grpc_connection(window, id).await?; let db = get_db(window).await; let _ = sqlx::query!( r#" DELETE FROM grpc_connections WHERE id = ? "#, id, ) .execute(&db) .await; emit_deleted_model(window, resp) } pub async fn delete_http_response( window: &WebviewWindow, id: &str, ) -> Result { let resp = get_http_response(window, id).await?; // Delete the body file if it exists if let Some(p) = resp.body_path.clone() { if let Err(e) = fs::remove_file(p) { error!("Failed to delete body file: {}", e); }; } let db = get_db(window).await; let _ = sqlx::query!( r#" DELETE FROM http_responses WHERE id = ? "#, id, ) .execute(&db) .await; emit_deleted_model(window, resp) } pub async fn delete_all_grpc_connections( window: &WebviewWindow, request_id: &str, ) -> Result<(), sqlx::Error> { for r in list_grpc_connections(window, request_id).await? { delete_grpc_connection(window, &r.id).await?; } Ok(()) } pub async fn delete_all_http_responses( window: &WebviewWindow, request_id: &str, ) -> Result<(), sqlx::Error> { for r in list_responses(window, request_id, None).await? { delete_http_response(window, &r.id).await?; } Ok(()) } pub fn generate_model_id(model: ModelType) -> String { let id = generate_id(); format!("{}_{}", model.id_prefix(), id) } pub fn generate_id() -> String { Alphanumeric.sample_string(&mut rand::thread_rng(), 10) } #[derive(Default, Debug, Deserialize, Serialize)] #[serde(default, rename_all = "camelCase")] pub struct WorkspaceExport { pub yaak_version: String, pub yaak_schema: i64, pub timestamp: NaiveDateTime, pub resources: WorkspaceExportResources, } #[derive(Default, Debug, Deserialize, Serialize)] #[serde(default, rename_all = "camelCase")] pub struct WorkspaceExportResources { pub workspaces: Vec, pub environments: Vec, pub folders: Vec, pub http_requests: Vec, pub grpc_requests: Vec, } pub async fn get_workspace_export_resources( window: &WebviewWindow, workspace_ids: Vec<&str>, ) -> WorkspaceExport { let app_handle = window.app_handle(); let mut data = WorkspaceExport { yaak_version: app_handle.package_info().version.clone().to_string(), yaak_schema: 2, timestamp: chrono::Utc::now().naive_utc(), resources: WorkspaceExportResources { workspaces: Vec::new(), environments: Vec::new(), folders: Vec::new(), http_requests: Vec::new(), grpc_requests: Vec::new(), }, }; for workspace_id in workspace_ids { data.resources.workspaces.push( get_workspace(window, workspace_id) .await .expect("Failed to get workspace"), ); data.resources.environments.append( &mut list_environments(window, workspace_id) .await .expect("Failed to get environments"), ); data.resources.folders.append( &mut list_folders(window, workspace_id) .await .expect("Failed to get folders"), ); data.resources.http_requests.append( &mut list_http_requests(window, workspace_id) .await .expect("Failed to get http requests"), ); data.resources.grpc_requests.append( &mut list_grpc_requests(window, workspace_id) .await .expect("Failed to get grpc requests"), ); } return data; } #[derive(Clone, Serialize)] #[serde(default, rename_all = "camelCase")] struct ModelPayload { pub model: M, pub window_label: String, } fn emit_upserted_model(window: &WebviewWindow, model: M) -> M { let payload = ModelPayload { model: model.clone(), window_label: window.label().to_string(), }; window.emit("upserted_model", payload).unwrap(); model } fn emit_deleted_model(window: &WebviewWindow, model: M) -> Result { let payload = ModelPayload { model: model.clone(), window_label: window.label().to_string(), }; window.emit("deleted_model", payload).unwrap(); Ok(model) } async fn get_db(w: &impl Manager) -> Pool { let db_state = w.state::>>(); let db = &*db_state.lock().await; db.clone() }