From 661d34ddcb8ccd56f1059f622809ddbac295f5fc Mon Sep 17 00:00:00 2001 From: Vasili Pascal Date: Fri, 6 Nov 2026 10:25:03 +0200 Subject: [PATCH] fix(marketplace): derive template deploy_count from deployment records stack_template.deploy_count was 0 for every template on /api/templates and the storefront: the counter is only incremented by the marketplace deploy-complete hooks, which never fire from the web pipeline (the stack_template_deployment event table is empty and the counter untouched), while real deployments are recorded as project rows with source_template_id. Compute deploy_count as GREATEST of the stored counter, the number of project records with source_template_id, and the number of verified stack_template_deployment events. Existing deployments are counted immediately and future ones keep counting even when a hook is missed. The 'popular' sort now orders by the derived value instead of the stale counter. All queries are runtime query_as, so no .sqlx data changes. --- src/db/marketplace.rs | 52 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/src/db/marketplace.rs b/src/db/marketplace.rs index 38944cc0..f33ed1f9 100644 --- a/src/db/marketplace.rs +++ b/src/db/marketplace.rs @@ -87,7 +87,11 @@ pub async fn list_approved_by_creator( t.status, t.is_configurable, t.view_count, - t.deploy_count, + GREATEST( + t.deploy_count, + (SELECT COUNT(*) FROM project p WHERE p.source_template_id = t.id)::int, + (SELECT COUNT(*) FROM stack_template_deployment d WHERE d.template_id = t.id)::int + ) AS deploy_count, t.required_plan_name, t.price, t.billing_cycle, @@ -110,7 +114,7 @@ pub async fn list_approved_by_creator( match sort.unwrap_or("recent") { "popular" => base.push_str( - " ORDER BY (t.verifications @> '{\"hardened_images\":true}') DESC, t.deploy_count DESC, t.view_count DESC", + " ORDER BY (t.verifications @> '{\"hardened_images\":true}') DESC, deploy_count DESC, t.view_count DESC", ), "rating" => base.push_str(&format!( " ORDER BY (t.verifications @> '{{\"hardened_images\":true}}') DESC, {} DESC NULLS LAST", @@ -160,7 +164,11 @@ pub async fn list_approved( t.status, t.is_configurable, t.view_count, - t.deploy_count, + GREATEST( + t.deploy_count, + (SELECT COUNT(*) FROM project p WHERE p.source_template_id = t.id)::int, + (SELECT COUNT(*) FROM stack_template_deployment d WHERE d.template_id = t.id)::int + ) AS deploy_count, t.required_plan_name, t.price, t.billing_cycle, @@ -201,7 +209,7 @@ pub async fn list_approved( match sort.unwrap_or("recent") { // Hardened images always float to the top of each sort bucket "popular" => base.push_str( - " ORDER BY (t.verifications @> '{\"hardened_images\":true}') DESC, t.deploy_count DESC, t.view_count DESC", + " ORDER BY (t.verifications @> '{\"hardened_images\":true}') DESC, deploy_count DESC, t.view_count DESC", ), "rating" => base.push_str(&format!( " ORDER BY (t.verifications @> '{{\"hardened_images\":true}}') DESC, {} DESC NULLS LAST", @@ -264,7 +272,11 @@ pub async fn get_by_slug_and_user( t.status, t.is_configurable, t.view_count, - t.deploy_count, + GREATEST( + t.deploy_count, + (SELECT COUNT(*) FROM project p WHERE p.source_template_id = t.id)::int, + (SELECT COUNT(*) FROM stack_template_deployment d WHERE d.template_id = t.id)::int + ) AS deploy_count, t.required_plan_name, t.price, t.billing_cycle, @@ -319,7 +331,11 @@ pub async fn get_by_slug_with_latest( t.status, t.is_configurable, t.view_count, - t.deploy_count, + GREATEST( + t.deploy_count, + (SELECT COUNT(*) FROM project p WHERE p.source_template_id = t.id)::int, + (SELECT COUNT(*) FROM stack_template_deployment d WHERE d.template_id = t.id)::int + ) AS deploy_count, t.required_plan_name, t.price, t.billing_cycle, @@ -438,7 +454,11 @@ pub async fn get_by_id( t.status, t.is_configurable, t.view_count, - t.deploy_count, + GREATEST( + t.deploy_count, + (SELECT COUNT(*) FROM project p WHERE p.source_template_id = t.id)::int, + (SELECT COUNT(*) FROM stack_template_deployment d WHERE d.template_id = t.id)::int + ) AS deploy_count, t.created_at, t.updated_at, t.approved_at, @@ -1419,7 +1439,11 @@ pub async fn list_mine(pool: &PgPool, user_id: &str) -> Result Result, S t.status, t.is_configurable, t.view_count, - t.deploy_count, + GREATEST( + t.deploy_count, + (SELECT COUNT(*) FROM project p WHERE p.source_template_id = t.id)::int, + (SELECT COUNT(*) FROM stack_template_deployment d WHERE d.template_id = t.id)::int + ) AS deploy_count, t.required_plan_name, t.price, t.billing_cycle, @@ -2996,7 +3024,11 @@ pub async fn get_approved_by_slug( t.status, t.is_configurable, t.view_count, - t.deploy_count, + GREATEST( + t.deploy_count, + (SELECT COUNT(*) FROM project p WHERE p.source_template_id = t.id)::int, + (SELECT COUNT(*) FROM stack_template_deployment d WHERE d.template_id = t.id)::int + ) AS deploy_count, t.required_plan_name, t.price, t.billing_cycle,