From 4b7497a908dfc5aff31ee62c6ba15e535e2d6237 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 9 Jan 2026 20:22:53 -0800 Subject: [PATCH] feat: implement layered settings system for HTTP requests and folders Add support for settings overrides at folder and HTTP request levels. Introduces nullable settings columns to database tables and implements resolution logic to merge workspace, folder, and request-level settings with proper precedence. --- crates-tauri/yaak-app/src/http_request.rs | 11 ++- crates-tauri/yaak-app/src/lib.rs | 4 +- crates-tauri/yaak-app/src/ws_ext.rs | 2 +- .../20260109201041_layered_settings.sql | 9 +++ crates/yaak-models/src/models.rs | 74 +++++++++++++++--- .../yaak-models/src/queries/http_requests.rs | 77 ++++++++++++++++++- crates/yaak-models/src/queries/workspaces.rs | 4 +- 7 files changed, 161 insertions(+), 20 deletions(-) create mode 100644 crates/yaak-models/migrations/20260109201041_layered_settings.sql diff --git a/crates-tauri/yaak-app/src/http_request.rs b/crates-tauri/yaak-app/src/http_request.rs index e7996a0c6..9af4d55bc 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 ee3e281bd..59b324611 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 ab1f7f95d..ac235ed2a 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 000000000..216b475f8 --- /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 5a8eda222..28ab20b7a 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 a4d6fe21e..1f13f551d 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 b374c8dea..ae8486ea5 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,