Compare commits

...

2 Commits

Author SHA1 Message Date
Gregory Schier
0a52032988 Merge branch 'main' into omnara/premium-deviator 2026-01-09 20:23:20 -08:00
Gregory Schier
4b7497a908 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.
2026-01-09 20:22:53 -08:00
7 changed files with 161 additions and 20 deletions

View File

@@ -178,11 +178,14 @@ async fn send_http_request_inner<R: Runtime>(
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<R: Runtime>(
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,
})

View File

@@ -233,7 +233,7 @@ async fn cmd_grpc_reflect<R: Runtime>(
&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<R: Runtime>(
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;

View File

@@ -355,7 +355,7 @@ pub async fn cmd_ws_connect<R: Runtime>(
url.as_str(),
headers,
receive_tx,
workspace.setting_validate_certificates,
workspace.setting_validate_certificates.unwrap_or(true),
client_cert,
)
.await

View File

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

View File

@@ -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<bool>,
pub setting_follow_redirects: Option<bool>,
pub setting_request_timeout: Option<i32>,
}
/// 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<String>,
// 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<bool>,
pub setting_follow_redirects: Option<bool>,
pub setting_request_timeout: Option<i32>,
}
impl UpsertModelInfo for Workspace {
@@ -726,6 +750,11 @@ pub struct Folder {
pub headers: Vec<HttpRequestHeader>,
pub name: String,
pub sort_priority: f64,
// Inheritable settings (Option = null means inherit from parent)
pub setting_validate_certificates: Option<bool>,
pub setting_follow_redirects: Option<bool>,
pub setting_request_timeout: Option<i32>,
}
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<HttpUrlParameter>,
// Inheritable settings (Option = null means inherit from parent)
pub setting_validate_certificates: Option<bool>,
pub setting_follow_redirects: Option<bool>,
pub setting_request_timeout: Option<i32>,
}
impl UpsertModelInfo for HttpRequest {
@@ -884,6 +927,7 @@ impl UpsertModelInfo for HttpRequest {
self,
source: &UpdateSource,
) -> Result<Vec<(impl IntoIden + Eq, impl Into<SimpleExpr>)>> {
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<impl IntoIden> {
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")?,
})
}
}

View File

@@ -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<ResolvedHttpRequestSettings> {
let workspace = self.get_workspace(&http_request.workspace_id)?;
// Start with None for all settings
let mut validate_certs: Option<bool> = None;
let mut follow_redirects: Option<bool> = None;
let mut timeout: Option<i32> = 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<Vec<Folder>> {
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)
}
}

View File

@@ -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,