use crate::error::{DbError, DbResult}; use config::database::{DatabaseConfig, DatabaseRole as ConfigRole}; use sea_orm::prelude::async_trait::async_trait; use sea_orm::{ Database as SeaDatabase, DatabaseConnection, DbBackend, DbErr, ExecResult, QueryResult, Statement, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DatabaseRole { Primary, Replica, } impl From for DatabaseRole { fn from(role: ConfigRole) -> Self { match role { ConfigRole::Primary => DatabaseRole::Primary, ConfigRole::Replica => DatabaseRole::Replica, } } } impl From for ConfigRole { fn from(role: DatabaseRole) -> Self { match role { DatabaseRole::Primary => ConfigRole::Primary, DatabaseRole::Replica => ConfigRole::Replica, } } } pub struct AppDatabase { config: DatabaseConfig, connection: crate::DbConn, } impl AppDatabase { pub async fn connect(config: DatabaseConfig) -> DbResult { config.validate().map_err(DbError::config)?; let primary_opts = config.primary_connect_options().map_err(DbError::config)?; let primary = SeaDatabase::connect(primary_opts).await?; let mut replicas = Vec::with_capacity(config.replica_count()); for i in 0..config.replica_count() { let opts = config.replica_connect_options(i).map_err(DbError::config)?; let conn = SeaDatabase::connect(opts).await?; replicas.push(conn); } Ok(Self { config, connection: crate::DbConn::new(primary, replicas), }) } pub fn config(&self) -> &DatabaseConfig { &self.config } pub fn primary(&self) -> &DatabaseConnection { self.connection.write() } pub fn replicas(&self) -> &[DatabaseConnection] { self.connection.replicas() } pub fn replica(&self, index: usize) -> Option<&DatabaseConnection> { self.connection.replicas().get(index) } pub fn replica_count(&self) -> usize { self.connection.replicas().len() } pub fn has_replicas(&self) -> bool { !self.connection.replicas().is_empty() } pub fn write(&self) -> &DatabaseConnection { self.connection.write() } pub fn read(&self) -> &crate::DbConn { self.connection.read() } pub fn connection(&self) -> &crate::DbConn { &self.connection } } #[async_trait] impl sea_orm::ConnectionTrait for AppDatabase { fn get_database_backend(&self) -> DbBackend { self.connection.get_database_backend() } async fn execute_raw(&self, stmt: Statement) -> Result { self.connection.read().execute_raw(stmt).await } async fn execute_unprepared(&self, sql: &str) -> Result { self.connection.read().execute_unprepared(sql).await } async fn query_one_raw(&self, stmt: Statement) -> Result, DbErr> { self.connection.read().query_one_raw(stmt).await } async fn query_all_raw(&self, stmt: Statement) -> Result, DbErr> { self.connection.read().query_all_raw(stmt).await } } /// Create the local settings/anchor row on demand. It contains no identity /// projection; it exists so local preferences and foreign keys have a stable /// row even when the user never went through KenApi's login callback. pub async fn ensure_local_user( database: &impl sea_orm::ConnectionTrait, uid: i64, ) -> Result<(), DbErr> { use models::users::user::{self, ProfileVisibility}; use sea_orm::{ActiveModelTrait, EntityTrait, Set}; if user::Entity::find_by_id(uid).one(database).await?.is_some() { return Ok(()); } user::ActiveModel { uid: Set(uid), is_restricted: Set(false), visibility: Set(ProfileVisibility::Public), last_login_at: Set(None), last_login_ip_hash: Set(None), local_avatar_url: Set(String::new()), timezone: Set(None), locale: Set(None), language: Set(None), version: Set(1), updated_at: Set(time::OffsetDateTime::now_utc()), } .insert(database) .await?; Ok(()) }