// api/networks.rs — Server functions for networks use leptos::prelude::*; use serde::{Deserialize, Serialize}; use crate::models::Network; // Network row augmented with pre-computed counts. // Defined here (not in models.rs) because it is a presentation model // specific to the Networks page, not a pure domain entity. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct NetworkWithCounts { pub id: i64, pub name: String, pub cidr: String, /// Number of hosts whose IP falls within this network's CIDR range. pub host_count: i64, /// Number of distinct applications linked via ports open on hosts in this network. pub application_count: i64, } // ─── Queries ────────────────────────────────────────────────────────────────── /// Returns all networks from the database. /// /// Called by the Networks page to populate the list. #[server] pub async fn get_networks() -> Result, ServerFnError> { use sqlx::AnyPool; use crate::server::repository::networks as repo; let pool = use_context::() .ok_or_else(|| ServerFnError::new("Database pool not found in context"))?; repo::list_networks(&pool) .await .map_err(|e| ServerFnError::new(e.to_string())) } /// Returns all networks enriched with host and application counts. #[server] pub async fn get_networks_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 n.id, n.name, n.cidr, (SELECT COUNT(*) FROM hosts WHERE network_id = n.id) AS host_count, (SELECT COUNT(DISTINCT ap.application_id) FROM hosts h JOIN host_ports hp ON hp.host_id = h.id JOIN application_ports ap ON ap.port_number = hp.port_number WHERE h.network_id = n.id) AS application_count FROM networks n ORDER BY n.id", ) .fetch_all(&pool) .await .map_err(|e| ServerFnError::new(e.to_string()))?; let networks = rows .into_iter() .map(|row| NetworkWithCounts { id: row.get("id"), name: row.get("name"), cidr: row.get("cidr"), host_count: row.get("host_count"), application_count: row.get("application_count"), }) .collect(); Ok(networks) } /// Returns a single network by id, or an error if it does not exist. #[server] pub async fn get_network(id: i64) -> Result { use sqlx::AnyPool; use crate::server::repository::networks as repo; let pool = use_context::() .ok_or_else(|| ServerFnError::new("Database pool not found in context"))?; repo::find_network(&pool, id) .await .map_err(|e| ServerFnError::new(e.to_string()))? .ok_or_else(|| ServerFnError::new(format!("Network {id} not found"))) } // ─── Mutations ──────────────────────────────────────────────────────────────── /// Creates a new network with the given name and CIDR block. /// /// Returns an error if the CIDR is malformed or already exists. #[server] pub async fn create_network(name: String, cidr: String) -> Result { use sqlx::AnyPool; use crate::server::{repository::networks as repo, validation::validate_cidr}; let pool = use_context::() .ok_or_else(|| ServerFnError::new("Database pool not found in context"))?; if name.trim().is_empty() { return Err(ServerFnError::new("Network name cannot be empty")); } validate_cidr(&cidr).map_err(|e| ServerFnError::new(e.to_string()))?; repo::create_network(&pool, name.trim(), &cidr) .await .map_err(|e| ServerFnError::new(e.to_string())) } /// Deletes a network by id. /// /// Also deletes all hosts in that network (via `ON DELETE CASCADE`). /// Returns `true` if the network existed and was deleted. #[server] pub async fn delete_network(id: i64) -> Result { use sqlx::AnyPool; use crate::server::repository::networks as repo; let pool = use_context::() .ok_or_else(|| ServerFnError::new("Database pool not found in context"))?; repo::delete_network(&pool, id) .await .map_err(|e| ServerFnError::new(e.to_string())) }