proper productionize project
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user