diff --git a/crates-tauri/yaak-app/src/http_request.rs b/crates-tauri/yaak-app/src/http_request.rs index e7996a0c..9af4d55b 100644 --- a/crates-tauri/yaak-app/src/http_request.rs +++ b/crates-tauri/yaak-app/src/http_request.rs @@ -178,11 +178,14 @@ async fn send_http_request_inner( window.db().resolve_environments(&workspace.id, folder_id, environment_id.as_deref())?; let request = render_http_request(&resolved, env_chain, &cb, &RenderOptions::throw()).await?; + // Resolve inherited settings for this request + let resolved_settings = window.db().resolve_settings_for_http_request(&resolved)?; + // Build the sendable request using the new SendableHttpRequest type let options = SendableHttpRequestOptions { - follow_redirects: workspace.setting_follow_redirects, - timeout: if workspace.setting_request_timeout > 0 { - Some(Duration::from_millis(workspace.setting_request_timeout.unsigned_abs() as u64)) + follow_redirects: resolved_settings.follow_redirects, + timeout: if resolved_settings.request_timeout > 0 { + Some(Duration::from_millis(resolved_settings.request_timeout.unsigned_abs() as u64)) } else { None }, @@ -231,7 +234,7 @@ async fn send_http_request_inner( let client = connection_manager .get_client(&HttpConnectionOptions { id: plugin_context.id.clone(), - validate_certificates: workspace.setting_validate_certificates, + validate_certificates: resolved_settings.validate_certificates, proxy: proxy_setting, client_certificate, }) diff --git a/crates-tauri/yaak-app/src/lib.rs b/crates-tauri/yaak-app/src/lib.rs index ee3e281b..59b32461 100644 --- a/crates-tauri/yaak-app/src/lib.rs +++ b/crates-tauri/yaak-app/src/lib.rs @@ -233,7 +233,7 @@ async fn cmd_grpc_reflect( &uri, &proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect(), &metadata, - workspace.setting_validate_certificates, + workspace.setting_validate_certificates.unwrap_or(true), client_certificate, skip_cache.unwrap_or(false), ) @@ -327,7 +327,7 @@ async fn cmd_grpc_go( uri.as_str(), &proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect(), &metadata, - workspace.setting_validate_certificates, + workspace.setting_validate_certificates.unwrap_or(true), client_cert.clone(), ) .await; diff --git a/crates-tauri/yaak-app/src/ws_ext.rs b/crates-tauri/yaak-app/src/ws_ext.rs index ab1f7f95..ac235ed2 100644 --- a/crates-tauri/yaak-app/src/ws_ext.rs +++ b/crates-tauri/yaak-app/src/ws_ext.rs @@ -355,7 +355,7 @@ pub async fn cmd_ws_connect( url.as_str(), headers, receive_tx, - workspace.setting_validate_certificates, + workspace.setting_validate_certificates.unwrap_or(true), client_cert, ) .await diff --git a/crates/yaak-models/migrations/20260109201041_layered_settings.sql b/crates/yaak-models/migrations/20260109201041_layered_settings.sql new file mode 100644 index 00000000..216b475f --- /dev/null +++ b/crates/yaak-models/migrations/20260109201041_layered_settings.sql @@ -0,0 +1,9 @@ +-- Add nullable settings columns to folders (NULL = inherit from parent) +ALTER TABLE folders ADD COLUMN setting_request_timeout INTEGER DEFAULT NULL; +ALTER TABLE folders ADD COLUMN setting_validate_certificates BOOLEAN DEFAULT NULL; +ALTER TABLE folders ADD COLUMN setting_follow_redirects BOOLEAN DEFAULT NULL; + +-- Add nullable settings columns to http_requests (NULL = inherit from parent) +ALTER TABLE http_requests ADD COLUMN setting_request_timeout INTEGER DEFAULT NULL; +ALTER TABLE http_requests ADD COLUMN setting_validate_certificates BOOLEAN DEFAULT NULL; +ALTER TABLE http_requests ADD COLUMN setting_follow_redirects BOOLEAN DEFAULT NULL; diff --git a/crates/yaak-models/src/models.rs b/crates/yaak-models/src/models.rs index 5a8eda22..28ab20b7 100644 --- a/crates/yaak-models/src/models.rs +++ b/crates/yaak-models/src/models.rs @@ -1,8 +1,4 @@ use crate::error::Result; -use crate::models::HttpRequestIden::{ - Authentication, AuthenticationType, Body, BodyType, CreatedAt, Description, FolderId, Headers, - Method, Name, SortPriority, UpdatedAt, Url, UrlParameters, WorkspaceId, -}; use crate::util::{UpdateSource, generate_prefixed_id}; use chrono::{NaiveDateTime, Utc}; use rusqlite::Row; @@ -115,6 +111,36 @@ impl Default for EditorKeymap { } } +/// Settings that can be inherited at workspace → folder → request level. +/// All fields optional - None means "inherit from parent" (or use default if at root). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export, export_to = "gen_models.ts")] +pub struct HttpRequestSettingsOverride { + pub setting_validate_certificates: Option, + pub setting_follow_redirects: Option, + pub setting_request_timeout: Option, +} + +/// Resolved settings with concrete values (after inheritance + defaults applied) +#[derive(Debug, Clone, PartialEq)] +pub struct ResolvedHttpRequestSettings { + pub validate_certificates: bool, + pub follow_redirects: bool, + pub request_timeout: i32, +} + +impl ResolvedHttpRequestSettings { + /// Default values when nothing is set in the inheritance chain + pub fn defaults() -> Self { + Self { + validate_certificates: true, + follow_redirects: true, + request_timeout: 0, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[serde(default, rename_all = "camelCase")] #[ts(export, export_to = "gen_models.ts")] @@ -297,12 +323,10 @@ pub struct Workspace { pub name: String, pub encryption_key_challenge: Option, - // Settings - #[serde(default = "default_true")] - pub setting_validate_certificates: bool, - #[serde(default = "default_true")] - pub setting_follow_redirects: bool, - pub setting_request_timeout: i32, + // Inheritable settings (Option = can be null, defaults applied at resolution time) + pub setting_validate_certificates: Option, + pub setting_follow_redirects: Option, + pub setting_request_timeout: Option, } impl UpsertModelInfo for Workspace { @@ -726,6 +750,11 @@ pub struct Folder { pub headers: Vec, pub name: String, pub sort_priority: f64, + + // Inheritable settings (Option = null means inherit from parent) + pub setting_validate_certificates: Option, + pub setting_follow_redirects: Option, + pub setting_request_timeout: Option, } impl UpsertModelInfo for Folder { @@ -765,6 +794,9 @@ impl UpsertModelInfo for Folder { (Description, self.description.into()), (Name, self.name.trim().into()), (SortPriority, self.sort_priority.into()), + (SettingValidateCertificates, self.setting_validate_certificates.into()), + (SettingFollowRedirects, self.setting_follow_redirects.into()), + (SettingRequestTimeout, self.setting_request_timeout.into()), ]) } @@ -778,6 +810,9 @@ impl UpsertModelInfo for Folder { FolderIden::Description, FolderIden::FolderId, FolderIden::SortPriority, + FolderIden::SettingValidateCertificates, + FolderIden::SettingFollowRedirects, + FolderIden::SettingRequestTimeout, ] } @@ -800,6 +835,9 @@ impl UpsertModelInfo for Folder { headers: serde_json::from_str(&headers).unwrap_or_default(), authentication_type: row.get("authentication_type")?, authentication: serde_json::from_str(&authentication).unwrap_or_default(), + setting_validate_certificates: row.get("setting_validate_certificates")?, + setting_follow_redirects: row.get("setting_follow_redirects")?, + setting_request_timeout: row.get("setting_request_timeout")?, }) } } @@ -857,6 +895,11 @@ pub struct HttpRequest { pub sort_priority: f64, pub url: String, pub url_parameters: Vec, + + // Inheritable settings (Option = null means inherit from parent) + pub setting_validate_certificates: Option, + pub setting_follow_redirects: Option, + pub setting_request_timeout: Option, } impl UpsertModelInfo for HttpRequest { @@ -884,6 +927,7 @@ impl UpsertModelInfo for HttpRequest { self, source: &UpdateSource, ) -> Result)>> { + use HttpRequestIden::*; Ok(vec![ (CreatedAt, upsert_date(source, self.created_at)), (UpdatedAt, upsert_date(source, self.updated_at)), @@ -900,10 +944,14 @@ impl UpsertModelInfo for HttpRequest { (AuthenticationType, self.authentication_type.into()), (Headers, serde_json::to_string(&self.headers)?.into()), (SortPriority, self.sort_priority.into()), + (SettingValidateCertificates, self.setting_validate_certificates.into()), + (SettingFollowRedirects, self.setting_follow_redirects.into()), + (SettingRequestTimeout, self.setting_request_timeout.into()), ]) } fn update_columns() -> Vec { + use HttpRequestIden::*; vec![ UpdatedAt, WorkspaceId, @@ -919,6 +967,9 @@ impl UpsertModelInfo for HttpRequest { Url, UrlParameters, SortPriority, + SettingValidateCertificates, + SettingFollowRedirects, + SettingRequestTimeout, ] } @@ -945,6 +996,9 @@ impl UpsertModelInfo for HttpRequest { sort_priority: row.get("sort_priority")?, url: row.get("url")?, url_parameters: serde_json::from_str(url_parameters.as_str()).unwrap_or_default(), + setting_validate_certificates: row.get("setting_validate_certificates")?, + setting_follow_redirects: row.get("setting_follow_redirects")?, + setting_request_timeout: row.get("setting_request_timeout")?, }) } } diff --git a/crates/yaak-models/src/queries/http_requests.rs b/crates/yaak-models/src/queries/http_requests.rs index a4d6fe21..1f13f551 100644 --- a/crates/yaak-models/src/queries/http_requests.rs +++ b/crates/yaak-models/src/queries/http_requests.rs @@ -1,6 +1,6 @@ use crate::db_context::DbContext; use crate::error::Result; -use crate::models::{Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden}; +use crate::models::{Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden, ResolvedHttpRequestSettings}; use crate::util::UpdateSource; use serde_json::Value; use std::collections::BTreeMap; @@ -103,4 +103,79 @@ impl<'a> DbContext<'a> { } Ok(children) } + + /// Resolve settings for an HTTP request by walking the inheritance chain: + /// Workspace → Folder(s) → Request + /// Last non-None value wins, then defaults are applied. + pub fn resolve_settings_for_http_request( + &self, + http_request: &HttpRequest, + ) -> Result { + let workspace = self.get_workspace(&http_request.workspace_id)?; + + // Start with None for all settings + let mut validate_certs: Option = None; + let mut follow_redirects: Option = None; + let mut timeout: Option = None; + + // Apply workspace settings + if workspace.setting_validate_certificates.is_some() { + validate_certs = workspace.setting_validate_certificates; + } + if workspace.setting_follow_redirects.is_some() { + follow_redirects = workspace.setting_follow_redirects; + } + if workspace.setting_request_timeout.is_some() { + timeout = workspace.setting_request_timeout; + } + + // Apply folder chain settings (root first, immediate parent last) + if let Some(folder_id) = &http_request.folder_id { + let folders = self.get_folder_ancestors(folder_id)?; + for folder in folders { + if folder.setting_validate_certificates.is_some() { + validate_certs = folder.setting_validate_certificates; + } + if folder.setting_follow_redirects.is_some() { + follow_redirects = folder.setting_follow_redirects; + } + if folder.setting_request_timeout.is_some() { + timeout = folder.setting_request_timeout; + } + } + } + + // Apply request-level settings (highest priority) + if http_request.setting_validate_certificates.is_some() { + validate_certs = http_request.setting_validate_certificates; + } + if http_request.setting_follow_redirects.is_some() { + follow_redirects = http_request.setting_follow_redirects; + } + if http_request.setting_request_timeout.is_some() { + timeout = http_request.setting_request_timeout; + } + + // Apply defaults for anything still None + Ok(ResolvedHttpRequestSettings { + validate_certificates: validate_certs.unwrap_or(true), + follow_redirects: follow_redirects.unwrap_or(true), + request_timeout: timeout.unwrap_or(0), + }) + } + + /// Get folder ancestors in order from root to immediate parent + fn get_folder_ancestors(&self, folder_id: &str) -> Result> { + let mut ancestors = Vec::new(); + let mut current_id = Some(folder_id.to_string()); + + while let Some(id) = current_id { + let folder = self.get_folder(&id)?; + current_id = folder.folder_id.clone(); + ancestors.push(folder); + } + + ancestors.reverse(); // Root first, immediate parent last + Ok(ancestors) + } } diff --git a/crates/yaak-models/src/queries/workspaces.rs b/crates/yaak-models/src/queries/workspaces.rs index b374c8de..ae8486ea 100644 --- a/crates/yaak-models/src/queries/workspaces.rs +++ b/crates/yaak-models/src/queries/workspaces.rs @@ -20,8 +20,8 @@ impl<'a> DbContext<'a> { workspaces.push(self.upsert_workspace( &Workspace { name: "Yaak".to_string(), - setting_follow_redirects: true, - setting_validate_certificates: true, + setting_follow_redirects: Some(true), + setting_validate_certificates: Some(true), ..Default::default() }, &UpdateSource::Background,