// 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 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; // `use_context` retrieves a value previously registered with `provide_context`. // The pool was injected in main.rs before every request. // `ok_or_else` converts `None` into an error (defensive: should never happen). let pool = use_context::() .ok_or_else(|| ServerFnError::new("Database pool not found in context"))?; // Propagate any DB error as a ServerFnError so the client sees a clean message. repo::list_networks(&pool) .await .map_err(|e| ServerFnError::new(e.to_string())) } /// Returns all networks enriched with host and application counts. /// /// A single SQL query fetches everything at once using correlated subqueries, /// avoiding N+1 round-trips regardless of the number of networks. /// /// `application_count` = distinct applications whose registered ports appear /// among the ports open on hosts in each network (via host_ports → application_ports). #[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.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"), cidr: row.get("cidr"), host_count: row.get("host_count"), application_count: row.get("application_count"), }) .collect(); Ok(networks) } // ─── Mutations ──────────────────────────────────────────────────────────────── /// Creates a new network with the given CIDR block. /// /// Returns the created record (with its auto-generated id). /// Returns an error if the CIDR is malformed or already exists. #[server] pub async fn create_network(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"))?; // Validate the CIDR before touching the database. // Example of a valid CIDR: "192.168.1.0/24" validate_cidr(&cidr).map_err(|e| ServerFnError::new(e.to_string()))?; repo::create_network(&pool, &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())) }