From 9e6053adf5beb21da36d7188f349e2068be4671c Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Tue, 2 Jun 2026 12:34:25 +0200 Subject: [PATCH] fix: inline get_draft query field instead of flattening --- backend/windmill-api-flows/src/flows.rs | 23 ++++++++------- backend/windmill-api-scripts/src/scripts.rs | 24 +++++++++------- backend/windmill-api/src/apps.rs | 32 ++++++++------------- backend/windmill-store/src/variables.rs | 12 +++++--- 4 files changed, 46 insertions(+), 45 deletions(-) diff --git a/backend/windmill-api-flows/src/flows.rs b/backend/windmill-api-flows/src/flows.rs index 13d13555e6..3d32e9d6d2 100644 --- a/backend/windmill-api-flows/src/flows.rs +++ b/backend/windmill-api-flows/src/flows.rs @@ -20,10 +20,8 @@ use windmill_api_auth::{ }; use windmill_common::workspaces::{check_deploy_rules, RuleCheckResult}; use windmill_common::{ - user_drafts::{ - fetch_draft_only, maybe_overlay_draft, UserDraftItemKind, WithDraftOverlay, WithDraftQuery, - }, - utils::{WithStarredInfoQuery, HTTP_CLIENT}, + user_drafts::{fetch_draft_only, maybe_overlay_draft, UserDraftItemKind, WithDraftOverlay}, + utils::HTTP_CLIENT, webhook::{WebhookMessage, WebhookShared}, DB, }; @@ -1368,12 +1366,15 @@ async fn get_deployment_status( Ok(Json(deployment_status)) } +// Fields inlined rather than flattened from WithStarredInfoQuery / +// WithDraftQuery — see the same comment on `GetScriptByPathQuery` in +// scripts.rs: axum's `serde_urlencoded` query extractor doesn't preserve +// the "true"/"false" → bool conversion through `#[serde(flatten)]`. #[derive(Deserialize)] struct GetFlowByPathQuery { - #[serde(flatten)] - starred: WithStarredInfoQuery, - #[serde(flatten)] - draft: WithDraftQuery, + with_starred_info: Option, + #[serde(default)] + get_draft: bool, } async fn get_flow_by_path( @@ -1386,7 +1387,7 @@ async fn get_flow_by_path( let path = path.to_path(); check_scopes(&authed, || format!("flows:read:{}", path))?; let mut tx = user_db.begin(&authed).await?; - let flow_o = if query.starred.with_starred_info.unwrap_or(false) { + let flow_o = if query.with_starred_info.unwrap_or(false) { sqlx::query_as::<_, FlowWithStarred>( r#" SELECT @@ -1478,12 +1479,12 @@ async fn get_flow_by_path( &authed.email, UserDraftItemKind::Flow, path, - query.draft.get_draft, + query.get_draft, flow, ) .await? } - None if query.draft.get_draft => { + None if query.get_draft => { fetch_draft_only(&db, &w_id, &authed.email, UserDraftItemKind::Flow, path) .await? .ok_or_else(|| { diff --git a/backend/windmill-api-scripts/src/scripts.rs b/backend/windmill-api-scripts/src/scripts.rs index c1f55cb2a3..9c8f7dd7c3 100644 --- a/backend/windmill-api-scripts/src/scripts.rs +++ b/backend/windmill-api-scripts/src/scripts.rs @@ -12,9 +12,7 @@ use windmill_api_auth::{ check_scopes, maybe_refresh_folders, require_owner_of_path, ApiAuthed, }; use windmill_common::{ - user_drafts::{ - fetch_draft_only, maybe_overlay_draft, UserDraftItemKind, WithDraftOverlay, WithDraftQuery, - }, + user_drafts::{fetch_draft_only, maybe_overlay_draft, UserDraftItemKind, WithDraftOverlay}, utils::{BulkDeleteRequest, WithStarredInfoQuery, HTTP_CLIENT}, webhook::{WebhookMessage, WebhookShared}, workspaces::{check_deploy_rules, RuleCheckResult}, @@ -1640,12 +1638,18 @@ pub async fn pick_hub_script_by_path( Ok::<_, Error>((status_code, headers, response)) } +// NOTE: inlined fields rather than `#[serde(flatten)]` on +// `WithStarredInfoQuery` / `WithDraftQuery`. axum's default query +// extractor uses `serde_urlencoded`, and flatten there routes through an +// internal map that drops type info — so `?get_draft=true` arrives as a +// `String` and fails the inner `bool` deserializer. Inlining keeps the +// fields on the top-level struct so `serde_urlencoded`'s own bool +// adapter sees the value directly. #[derive(Deserialize)] struct GetScriptByPathQuery { - #[serde(flatten)] - starred: WithStarredInfoQuery, - #[serde(flatten)] - draft: WithDraftQuery, + with_starred_info: Option, + #[serde(default)] + get_draft: bool, } #[axum::debug_handler] @@ -1660,7 +1664,7 @@ async fn get_script_by_path( check_scopes(&authed, || format!("scripts:read:{}", path))?; let mut tx = user_db.begin(&authed).await?; - let script_o = if query.starred.with_starred_info.unwrap_or(false) { + let script_o = if query.with_starred_info.unwrap_or(false) { sqlx::query_as::<_, ScriptWithStarred>( "SELECT s.*, favorite.path IS NOT NULL as starred FROM script s @@ -1707,12 +1711,12 @@ async fn get_script_by_path( &authed.email, UserDraftItemKind::Script, path, - query.draft.get_draft, + query.get_draft, script, ) .await? } - None if query.draft.get_draft => { + None if query.get_draft => { fetch_draft_only(&db, &w_id, &authed.email, UserDraftItemKind::Script, path) .await? .ok_or_else(|| { diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index bc217279c7..d56bef1040 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -12,7 +12,7 @@ use crate::{ db::{ApiAuthed, DB}, jobs::RunJobQuery, users::{require_owner_of_path, require_path_read_access_for_preview, OptAuthed}, - utils::{check_scopes, WithStarredInfoQuery}, + utils::check_scopes, webhook_util::{WebhookMessage, WebhookShared}, HTTP_CLIENT, }; @@ -58,9 +58,7 @@ use windmill_common::{ get_payload_tag_from_prefixed_path, resolve_delete_after_secs, schedule_job_deletion, JobPayload, RawCode, }, - user_drafts::{ - fetch_draft_only, maybe_overlay_draft, UserDraftItemKind, WithDraftOverlay, WithDraftQuery, - }, + user_drafts::{fetch_draft_only, maybe_overlay_draft, UserDraftItemKind, WithDraftOverlay}, users::username_to_permissioned_as, utils::{ http_get_from_hub, not_found_if_none, paginate, query_elems_from_hub, require_admin, @@ -548,12 +546,15 @@ async fn get_raw_app_data( // Ok(Json(version)) // } +// Fields inlined rather than flattened from WithStarredInfoQuery / +// WithDraftQuery — see the same comment on `GetScriptByPathQuery` in +// scripts.rs: axum's `serde_urlencoded` query extractor doesn't preserve +// the "true"/"false" → bool conversion through `#[serde(flatten)]`. #[derive(Deserialize)] struct GetAppQuery { - #[serde(flatten)] - starred: WithStarredInfoQuery, - #[serde(flatten)] - draft: WithDraftQuery, + with_starred_info: Option, + #[serde(default)] + get_draft: bool, /// When no deployed app exists at this path and `get_draft` is set, /// `raw_app` picks which draft kind to look up (`raw_app` or `app`). /// Ignored when a deployed row exists — the row's own `raw_app` @@ -574,7 +575,7 @@ async fn get_app( check_scopes(&authed, || format!("apps:read:{}", path))?; let mut tx = user_db.begin(&authed).await?; - let app_o = if query.starred.with_starred_info.unwrap_or(false) { + let app_o = if query.with_starred_info.unwrap_or(false) { sqlx::query_as::<_, AppWithLastVersionAndStarred>( "SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path, app.extra_perms, app_version.value, @@ -622,18 +623,9 @@ async fn get_app( } else { UserDraftItemKind::App }; - maybe_overlay_draft( - &db, - &w_id, - &authed.email, - kind, - path, - query.draft.get_draft, - app, - ) - .await? + maybe_overlay_draft(&db, &w_id, &authed.email, kind, path, query.get_draft, app).await? } - None if query.draft.get_draft => { + None if query.get_draft => { let kind = if query.raw_app.unwrap_or(false) { UserDraftItemKind::RawApp } else { diff --git a/backend/windmill-store/src/variables.rs b/backend/windmill-store/src/variables.rs index efdf941d94..c619aa6de5 100644 --- a/backend/windmill-store/src/variables.rs +++ b/backend/windmill-store/src/variables.rs @@ -35,7 +35,7 @@ use windmill_common::{ db::{DbWithOptAuthed, UserDB}, error::{Error, JsonResult, Result}, scripts::ScriptHash, - user_drafts::{maybe_overlay_draft, UserDraftItemKind, WithDraftOverlay, WithDraftQuery}, + user_drafts::{maybe_overlay_draft, UserDraftItemKind, WithDraftOverlay}, utils::{not_found_if_none, paginate, Pagination, StripPath, WarnAfterExt}, variables::{ build_crypt, get_reserved_variables, ContextualVariable, CreateVariable, ListableVariable, @@ -204,12 +204,16 @@ async fn list_variables( Ok(Json(rows)) } +// `get_draft` inlined rather than flattened from WithDraftQuery — see +// the same comment on `GetScriptByPathQuery` in scripts.rs: axum's +// `serde_urlencoded` query extractor doesn't preserve the "true"/"false" +// → bool conversion through `#[serde(flatten)]`. #[derive(Deserialize)] struct GetVariableQuery { decrypt_secret: Option, include_encrypted: Option, - #[serde(flatten)] - draft: WithDraftQuery, + #[serde(default)] + get_draft: bool, } async fn get_variable( @@ -311,7 +315,7 @@ async fn get_variable( &authed.email, UserDraftItemKind::Variable, path, - q.draft.get_draft, + q.get_draft, r, ) .await?;