// api/applications.rs — Server functions for applications and their port associations use leptos::prelude::*; use serde::{Deserialize, Serialize}; use crate::models::Application; // Application row enriched with the number of hosts that use at least one of // its registered ports. Host count is computed via the join: // application_ports → host_ports (matched on port_number) → hosts #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ApplicationWithCounts { pub id: i64, pub name: String, /// Distinct hosts that have at least one port matching this application. pub host_count: i64, } // ─── Queries ────────────────────────────────────────────────────────────────── /// Returns all applications enriched with their associated host count. #[server] pub async fn get_applications_with_counts() -> Result, ServerFnError> { use sqlx::{AnyPool, Row}; let pool = use_context::() .ok_or_else(|| ServerFnError::new("Database pool not found in context"))?; let rows = sqlx::query( "SELECT a.id, a.name, COUNT(DISTINCT hp.host_id) AS host_count FROM applications a LEFT JOIN application_ports ap ON ap.application_id = a.id LEFT JOIN host_ports hp ON hp.port_number = ap.port_number GROUP BY a.id, a.name ORDER BY a.name", ) .fetch_all(&pool) .await .map_err(|e| ServerFnError::new(e.to_string()))?; Ok(rows .into_iter() .map(|row| ApplicationWithCounts { id: row.get("id"), name: row.get("name"), host_count: row.get("host_count"), }) .collect()) } /// Returns all applications ordered by name. #[server] pub async fn get_applications() -> Result, ServerFnError> { use sqlx::AnyPool; use crate::server::repository::applications as repo; let pool = use_context::() .ok_or_else(|| ServerFnError::new("Database pool not found in context"))?; repo::list_applications(&pool) .await .map_err(|e| ServerFnError::new(e.to_string())) } /// Returns the port numbers associated with an application. #[server] pub async fn get_ports_for_application( application_id: i64, ) -> Result, ServerFnError> { use sqlx::AnyPool; use crate::server::repository::applications as repo; let pool = use_context::() .ok_or_else(|| ServerFnError::new("Database pool not found in context"))?; repo::list_ports_for_application(&pool, application_id) .await .map_err(|e| ServerFnError::new(e.to_string())) } // ─── Mutations ──────────────────────────────────────────────────────────────── /// Creates a new application and returns the created record. #[server] pub async fn create_application(name: String) -> Result { use sqlx::AnyPool; use crate::server::repository::applications as repo; let pool = use_context::() .ok_or_else(|| ServerFnError::new("Database pool not found in context"))?; repo::create_application(&pool, &name) .await .map_err(|e| ServerFnError::new(e.to_string())) } /// Deletes an application and all its port associations. /// /// Returns `true` if the application existed and was deleted. #[server] pub async fn delete_application(id: i64) -> Result { use sqlx::AnyPool; use crate::server::repository::applications as repo; let pool = use_context::() .ok_or_else(|| ServerFnError::new("Database pool not found in context"))?; repo::delete_application(&pool, id) .await .map_err(|e| ServerFnError::new(e.to_string())) } /// Associates a port number with an application. /// /// If the association already exists, this is a no-op. #[server] pub async fn add_port_to_application( application_id: i64, port_number: u16, ) -> Result<(), ServerFnError> { use sqlx::AnyPool; use crate::server::repository::applications as repo; let pool = use_context::() .ok_or_else(|| ServerFnError::new("Database pool not found in context"))?; repo::add_port_to_application(&pool, application_id, port_number) .await .map_err(|e| ServerFnError::new(e.to_string())) } /// Removes a port association from an application. /// /// If the association does not exist, this is a no-op. #[server] pub async fn remove_port_from_application( application_id: i64, port_number: u16, ) -> Result<(), ServerFnError> { use sqlx::AnyPool; use crate::server::repository::applications as repo; let pool = use_context::() .ok_or_else(|| ServerFnError::new("Database pool not found in context"))?; repo::remove_port_from_application(&pool, application_id, port_number) .await .map_err(|e| ServerFnError::new(e.to_string())) }