use super::merge_headers; use crate::blob_manager::BlobManager; use crate::client_db::ClientDb; use crate::error::Result; use crate::models::{ AnyModel, CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, GraphQlIntrospection, GraphQlIntrospectionIden, GrpcConnection, GrpcConnectionIden, GrpcEvent, GrpcEventIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestHeader, HttpRequestIden, HttpResponse, HttpResponseEvent, HttpResponseEventIden, HttpResponseIden, ResolvedHttpRequestSettings, ResolvedSetting, SyncState, SyncStateIden, WebsocketConnection, WebsocketConnectionIden, WebsocketEvent, WebsocketEventIden, WebsocketRequest, WebsocketRequestIden, Workspace, WorkspaceIden, WorkspaceMeta, WorkspaceMetaIden, }; use crate::util::UpdateSource; use log::warn; use serde_json::Value; use std::collections::BTreeMap; impl<'a> ClientDb<'a> { pub fn get_workspace(&self, id: &str) -> Result { self.find_one(WorkspaceIden::Id, id) } pub fn list_workspaces(&self) -> Result> { let mut workspaces = self.find_all()?; if workspaces.is_empty() { workspaces.push(self.upsert_workspace( &Workspace { name: "Yaak".to_string(), ..Default::default() }, &UpdateSource::Background, )?) } Ok(workspaces) } /// Delete a workspace and everything in it. /// /// Children are bulk-deleted with one statement per table and are NOT /// individually recorded in model_changes or emitted as events — the single /// workspace delete event implies the subtree (see [`ModelChangeEvent::Delete`]). /// This keeps huge workspaces (thousands of requests) fast and avoids /// flooding event consumers. pub fn delete_workspace( &self, workspace: &Workspace, source: &UpdateSource, blobs: &BlobManager, ) -> Result { let wid = workspace.id.as_str(); // Collect response cleanup targets before their rows disappear. The actual // cleanup runs at the end: response bodies live on disk and in the blob DB, // which don't participate in this transaction, so removing them must wait // until every statement that could fail (and roll back the rows) is done. let responses = self.find_many::(HttpResponseIden::WorkspaceId, wid, None)?; // Sync and the CLI call this on a plain connection where each statement // would otherwise commit on its own, leaving a partially-deleted workspace // if one fails. A savepoint makes the cascade atomic there, and nests // harmlessly inside the interactive path's transaction. let conn = self.conn().resolve(); conn.execute_batch("SAVEPOINT delete_workspace")?; let result: Result = (|| { self.delete_many_untracked::( HttpResponseEventIden::WorkspaceId, wid, )?; self.delete_many_untracked::(HttpResponseIden::WorkspaceId, wid)?; self.delete_many_untracked::(HttpRequestIden::WorkspaceId, wid)?; self.delete_many_untracked::(GrpcEventIden::WorkspaceId, wid)?; self.delete_many_untracked::(GrpcConnectionIden::WorkspaceId, wid)?; self.delete_many_untracked::(GrpcRequestIden::WorkspaceId, wid)?; self.delete_many_untracked::(WebsocketEventIden::WorkspaceId, wid)?; self.delete_many_untracked::( WebsocketConnectionIden::WorkspaceId, wid, )?; self.delete_many_untracked::(WebsocketRequestIden::WorkspaceId, wid)?; self.delete_many_untracked::( GraphQlIntrospectionIden::WorkspaceId, wid, )?; self.delete_many_untracked::(FolderIden::WorkspaceId, wid)?; self.delete_many_untracked::(EnvironmentIden::WorkspaceId, wid)?; self.delete_many_untracked::(CookieJarIden::WorkspaceId, wid)?; self.delete_many_untracked::(SyncStateIden::WorkspaceId, wid)?; self.delete_many_untracked::(WorkspaceMetaIden::WorkspaceId, wid)?; self.delete(workspace, source) })(); let deleted = match result { Ok(deleted) => { conn.execute_batch("RELEASE delete_workspace")?; deleted } Err(e) => { let _ = conn .execute_batch("ROLLBACK TO delete_workspace; RELEASE delete_workspace"); return Err(e); } }; // Best-effort cleanup of response bodies (disk files and blob chunks). // Failures only orphan unreferenced data, and are logged. let blob_ctx = blobs.connect(); for m in responses { if let Some(p) = m.body_path { if let Err(e) = std::fs::remove_file(&p) { warn!("Failed to delete response body file {p:?}: {e}"); } } if let Err(e) = blob_ctx.delete_chunks_like(&format!("{}.%", m.id)) { warn!("Failed to delete blobs for response {}: {e}", m.id); } } Ok(deleted) } pub fn delete_workspace_by_id( &self, id: &str, source: &UpdateSource, blobs: &BlobManager, ) -> Result { let workspace = self.get_workspace(id)?; self.delete_workspace(&workspace, source, blobs) } pub fn upsert_workspace(&self, w: &Workspace, source: &UpdateSource) -> Result { self.upsert(w, source) } pub fn resolve_auth_for_workspace( &self, workspace: &Workspace, ) -> (Option, BTreeMap, String) { ( workspace.authentication_type.clone(), workspace.authentication.clone(), workspace.id.clone(), ) } pub fn resolve_headers_for_workspace(&self, workspace: &Workspace) -> Vec { merge_headers(default_headers(), workspace.headers.clone()) } pub fn resolve_settings_for_workspace( &self, workspace: &Workspace, ) -> ResolvedHttpRequestSettings { ResolvedHttpRequestSettings { validate_certificates: ResolvedSetting::from_model( workspace.setting_validate_certificates, AnyModel::Workspace(workspace.clone()), ), follow_redirects: ResolvedSetting::from_model( workspace.setting_follow_redirects, AnyModel::Workspace(workspace.clone()), ), request_timeout: ResolvedSetting::from_model( workspace.setting_request_timeout, AnyModel::Workspace(workspace.clone()), ), request_message_size: ResolvedSetting::from_model( workspace.setting_request_message_size, AnyModel::Workspace(workspace.clone()), ), send_cookies: ResolvedSetting::from_model( workspace.setting_send_cookies, AnyModel::Workspace(workspace.clone()), ), store_cookies: ResolvedSetting::from_model( workspace.setting_store_cookies, AnyModel::Workspace(workspace.clone()), ), } } } /// Global default headers that are always sent with requests unless overridden. /// These are prepended to the inheritance chain so workspace/folder/request headers /// can override or disable them. pub fn default_headers() -> Vec { vec![ HttpRequestHeader { name: "User-Agent".to_string(), value: "yaak".to_string(), ..Default::default() }, HttpRequestHeader { name: "Accept".to_string(), value: "*/*".to_string(), ..Default::default() }, ] } #[cfg(test)] mod tests { use crate::init_in_memory; #[test] fn bootstraps_first_workspace_with_real_defaults() { let (query_manager, _blob_manager, _rx) = init_in_memory().expect("Failed to init DB"); let db = query_manager.connect(); let workspaces = db.list_workspaces().expect("Failed to list workspaces"); let workspace = workspaces.first().expect("No workspace was bootstrapped"); // This workspace is built in Rust and never deserialized, so it only gets // these values if `Workspace::default()` carries them. Asserted through the // DB round trip, since the column values are what a fresh install lives with. assert!(workspace.setting_send_cookies, "setting_send_cookies"); assert!(workspace.setting_store_cookies, "setting_store_cookies"); assert!(workspace.setting_follow_redirects, "setting_follow_redirects"); assert!(workspace.setting_validate_certificates, "setting_validate_certificates"); assert_eq!( workspace.setting_request_message_size, crate::models::DEFAULT_REQUEST_MESSAGE_SIZE ); } }