feat(repository): add CRUD layer for all domain entities

- Add server/repository/ module with networks, hosts, ports, applications
- Use sqlx::query() + manual row mapping (no compile-time DB required)
- Handle unique-constraint conflicts with is_unique_violation() for
  cross-database compatibility (SQLite + PostgreSQL via AnyPool)
- add_port_to_host auto-registers the port in the catalog (prevents FK errors)
- application_ports has no FK to ports (intentional: loose association)
- Add DbError::NotFound variant for missing-record cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 21:52:32 +02:00
parent f13097591c
commit a352a8edfd
7 changed files with 480 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
// repository/networks.rs — CRUD for the `networks` table
use sqlx::{AnyPool, Row};
use crate::models::Network;
use crate::server::db::DbError;
// ─── Read ─────────────────────────────────────────────────────────────────────
/// Returns every network ordered by id.
pub async fn list_networks(pool: &AnyPool) -> Result<Vec<Network>, DbError> {
// `fetch_all` runs the query and collects every row into a Vec.
// It returns an error if the query fails; an empty table returns Ok(vec![]).
let rows = sqlx::query("SELECT id, cidr FROM networks ORDER BY id")
.fetch_all(pool)
.await?;
// `.iter().map(...).collect()` transforms each raw DB row into a Network struct.
Ok(rows.iter().map(row_to_network).collect())
}
/// Returns a single network by id, or `None` if it does not exist.
pub async fn find_network(pool: &AnyPool, id: i64) -> Result<Option<Network>, DbError> {
// `fetch_optional` returns `Ok(None)` when no row matches — unlike
// `fetch_one`, which returns an error when nothing is found.
let row = sqlx::query("SELECT id, cidr FROM networks WHERE id = $1")
.bind(id) // `$1` is replaced with the value of `id` at runtime
.fetch_optional(pool)
.await?;
// `Option::map` applies the conversion only if the row is Some.
Ok(row.as_ref().map(row_to_network))
}
// ─── Write ────────────────────────────────────────────────────────────────────
/// Inserts a new network and returns the created record (with its auto-generated id).
///
/// Fails with `DbError::Connection` if the CIDR is already registered
/// (the `cidr` column has a UNIQUE constraint).
///
/// `RETURNING id, cidr` reads back the inserted row in a single round-trip,
/// avoiding a separate SELECT after the INSERT.
/// Requires SQLite ≥ 3.35 (2021) and any PostgreSQL version.
pub async fn create_network(pool: &AnyPool, cidr: &str) -> Result<Network, DbError> {
let row = sqlx::query("INSERT INTO networks (cidr) VALUES ($1) RETURNING id, cidr")
.bind(cidr)
.fetch_one(pool) // exactly one row is returned by RETURNING
.await?;
Ok(row_to_network(&row))
}
/// Deletes a network by id and all its hosts (via `ON DELETE CASCADE`).
///
/// Returns `true` if a row was deleted, `false` if the id did not exist.
pub async fn delete_network(pool: &AnyPool, id: i64) -> Result<bool, DbError> {
// `execute` runs the query without fetching rows.
// `rows_affected()` tells us how many rows were actually deleted.
let result = sqlx::query("DELETE FROM networks WHERE id = $1")
.bind(id)
.execute(pool)
.await?;
Ok(result.rows_affected() > 0)
}
// ─── Row mapping ──────────────────────────────────────────────────────────────
/// Converts a raw database row into a `Network` struct.
///
/// `row.get("col")` extracts a typed value by column name.
/// The type must implement `sqlx::Decode` for the `Any` backend.
fn row_to_network(row: &sqlx::any::AnyRow) -> Network {
Network {
id: row.get("id"),
cidr: row.get("cidr"),
}
}