proper productionize project

This commit is contained in:
2026-07-15 23:54:53 -07:00
parent b8850c87a4
commit edb6d2b5b4
46 changed files with 11346 additions and 1560 deletions
+175
View File
@@ -0,0 +1,175 @@
use std::{fmt, str::FromStr};
use deadpool_postgres::{Manager, ManagerConfig, Object, Pool, RecyclingMethod, Runtime};
use serde_json::Value;
use tokio_postgres::{Config as PostgresConfig, NoTls, Transaction};
use uuid::Uuid;
/// Cloneable database handle intended to live directly in Axum's `AppState`.
///
/// HTTP handlers should acquire a pooled connection through [`Db::get`]. Any
/// query against an RLS-protected tenant table must run in a transaction after
/// calling [`Db::set_tenant`]. `SET LOCAL` is important: a session-level setting
/// could otherwise leak an identity when the connection returns to the pool.
#[derive(Clone)]
pub struct Db {
pool: Pool,
}
impl Db {
pub fn from_pool(pool: Pool) -> Self {
Self { pool }
}
pub fn pool(&self) -> &Pool {
&self.pool
}
pub fn connect(database_url: &str, max_size: usize) -> Result<Self, DbError> {
let config = PostgresConfig::from_str(database_url)
.map_err(|error| DbError::Configuration(error.to_string()))?;
let manager = Manager::from_config(
config,
NoTls,
ManagerConfig {
recycling_method: RecyclingMethod::Fast,
},
);
let pool = Pool::builder(manager)
.runtime(Runtime::Tokio1)
.max_size(max_size.max(1))
.build()
.map_err(|error| DbError::Configuration(error.to_string()))?;
Ok(Self { pool })
}
pub async fn get(&self) -> Result<Object, DbError> {
self.pool
.get()
.await
.map_err(|error| DbError::Pool(error.to_string()))
}
/// Applies the additive multi-tenant migration. The legacy migrations must
/// already have run because this migration deliberately links their tables.
pub async fn migrate_multitenancy(&self) -> Result<(), DbError> {
let client = self.get().await?;
client
.batch_execute(include_str!("../migrations/003_multitenancy.sql"))
.await
.map_err(DbError::Postgres)
}
pub async fn set_tenant(transaction: &Transaction<'_>, user_id: Uuid) -> Result<(), DbError> {
transaction
.query_one(
"SELECT set_config('app.user_id', $1, true)",
&[&user_id.to_string()],
)
.await
.map_err(DbError::Postgres)?;
Ok(())
}
/// Minimal, non-secret account inventory for the source supervisor at
/// startup. Component rows are deliberately loaded in a second,
/// tenant-scoped query so RLS remains effective.
pub async fn list_active_tenants(&self) -> Result<Vec<ActiveTenant>, DbError> {
let client = self.get().await?;
let rows = client
.query(
"SELECT owner_user_id,room_id,source_id FROM list_active_live_sources()",
&[],
)
.await?;
Ok(rows
.into_iter()
.map(|row| ActiveTenant {
user_id: row.get(0),
room_id: row.get(1),
source_id: row.get(2),
})
.collect())
}
pub async fn list_tenant_components(
&self,
user_id: Uuid,
) -> Result<Vec<ComponentRecord>, DbError> {
let mut client = self.get().await?;
let transaction = client.transaction().await?;
Self::set_tenant(&transaction, user_id).await?;
let rows = transaction
.query(
"SELECT id,source_id,kind,name,settings,settings_version,enabled \
FROM component_instances WHERE owner_user_id=$1 ORDER BY created_at",
&[&user_id],
)
.await?;
transaction.commit().await?;
Ok(rows
.into_iter()
.map(|row| ComponentRecord {
id: row.get(0),
owner_user_id: user_id,
source_id: row.get(1),
kind: row.get(2),
name: row.get(3),
settings: row.get(4),
settings_version: row.get(5),
enabled: row.get(6),
})
.collect())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ActiveTenant {
pub user_id: Uuid,
pub room_id: String,
pub source_id: Uuid,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ComponentRecord {
pub id: Uuid,
pub owner_user_id: Uuid,
pub source_id: Uuid,
pub kind: String,
pub name: String,
pub settings: Value,
pub settings_version: i32,
pub enabled: bool,
}
#[derive(Debug)]
pub enum DbError {
Configuration(String),
Pool(String),
Postgres(tokio_postgres::Error),
}
impl fmt::Display for DbError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Configuration(message) => write!(formatter, "database configuration: {message}"),
Self::Pool(message) => write!(formatter, "database pool: {message}"),
Self::Postgres(error) => write!(formatter, "database query: {error}"),
}
}
}
impl std::error::Error for DbError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Postgres(error) => Some(error),
_ => None,
}
}
}
impl From<tokio_postgres::Error> for DbError {
fn from(error: tokio_postgres::Error) -> Self {
Self::Postgres(error)
}
}