//! Persistence for runtime-editable application settings (`app_settings`), //! a small key-value JSONB table (one row per subsystem group). Mirrors the //! `crawler_state` access style: plain async fns over `&PgPool`, the `value` //! a `serde_json::Value` the caller (de)serializes into a settings DTO. use sqlx::{PgExecutor, PgPool}; /// Fetch the raw JSONB for a settings group, or `None` if unset. pub async fn get(pool: &PgPool, key: &str) -> sqlx::Result> { sqlx::query_scalar("SELECT value FROM app_settings WHERE key = $1") .bind(key) .fetch_optional(pool) .await } /// Insert-or-replace the JSONB for a settings group, stamping `updated_at`. /// Generic over the executor so it can run inside the same transaction as /// the matching admin-audit insert. pub async fn upsert<'e, E: PgExecutor<'e>>( executor: E, key: &str, value: &serde_json::Value, ) -> sqlx::Result<()> { sqlx::query( "INSERT INTO app_settings (key, value, updated_at) \ VALUES ($1, $2, now()) \ ON CONFLICT (key) DO UPDATE \ SET value = EXCLUDED.value, updated_at = now()", ) .bind(key) .bind(value) .execute(executor) .await?; Ok(()) } /// Write the value only if the row is absent (the env → DB boot seed). Returns /// `true` when a row was inserted, `false` when one already existed. Uses /// `ON CONFLICT DO NOTHING` so a concurrent boot can't race two seeds. pub async fn seed_if_absent( pool: &PgPool, key: &str, value: &serde_json::Value, ) -> sqlx::Result { let result = sqlx::query( "INSERT INTO app_settings (key, value) VALUES ($1, $2) \ ON CONFLICT (key) DO NOTHING", ) .bind(key) .bind(value) .execute(pool) .await?; Ok(result.rows_affected() > 0) }