Infra refactor
This commit is contained in:
@@ -3,17 +3,19 @@ name = "notes-infra"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = ["sqlite"]
|
||||
sqlite = ["sqlx/sqlite", "tower-sessions-sqlx-store/sqlite"]
|
||||
postgres = ["sqlx/postgres", "tower-sessions-sqlx-store/postgres"]
|
||||
|
||||
[dependencies]
|
||||
notes-domain = { path = "../notes-domain" }
|
||||
async-trait = "0.1.89"
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
sqlx = { version = "0.8.6", features = [
|
||||
"sqlite",
|
||||
"runtime-tokio",
|
||||
"chrono",
|
||||
"migrate",
|
||||
] }
|
||||
sqlx = { version = "0.8.6", features = ["runtime-tokio", "chrono", "migrate"] }
|
||||
thiserror = "2.0.17"
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
tracing = "0.1"
|
||||
uuid = { version = "1.19.0", features = ["v4", "serde"] }
|
||||
tower-sessions = "0.14.0"
|
||||
tower-sessions-sqlx-store = { version = "0.15.0", default-features = false }
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
//! Database connection pool management
|
||||
|
||||
use sqlx::Pool;
|
||||
#[cfg(feature = "postgres")]
|
||||
use sqlx::Postgres;
|
||||
#[cfg(feature = "sqlite")]
|
||||
use sqlx::Sqlite;
|
||||
#[cfg(feature = "sqlite")]
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
|
||||
#[cfg(feature = "sqlite")]
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -32,7 +39,6 @@ impl DatabaseConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an in-memory database config (useful for testing)
|
||||
pub fn in_memory() -> Self {
|
||||
Self {
|
||||
url: "sqlite::memory:".to_string(),
|
||||
@@ -43,7 +49,16 @@ impl DatabaseConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DatabasePool {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Sqlite(Pool<Sqlite>),
|
||||
#[cfg(feature = "postgres")]
|
||||
Postgres(Pool<Postgres>),
|
||||
}
|
||||
|
||||
/// Create a database connection pool
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub async fn create_pool(config: &DatabaseConfig) -> Result<SqlitePool, sqlx::Error> {
|
||||
let options = SqliteConnectOptions::from_str(&config.url)?
|
||||
.create_if_missing(true)
|
||||
@@ -62,8 +77,28 @@ pub async fn create_pool(config: &DatabaseConfig) -> Result<SqlitePool, sqlx::Er
|
||||
}
|
||||
|
||||
/// Run database migrations
|
||||
pub async fn run_migrations(pool: &SqlitePool) -> Result<(), sqlx::Error> {
|
||||
sqlx::migrate!("../migrations").run(pool).await?;
|
||||
pub async fn run_migrations(pool: &DatabasePool) -> Result<(), sqlx::Error> {
|
||||
match pool {
|
||||
#[cfg(feature = "sqlite")]
|
||||
DatabasePool::Sqlite(pool) => {
|
||||
sqlx::migrate!("../migrations").run(pool).await?;
|
||||
}
|
||||
#[cfg(feature = "postgres")]
|
||||
DatabasePool::Postgres(_pool) => {
|
||||
// Placeholder for Postgres migrations
|
||||
// sqlx::migrate!("../migrations/postgres").run(_pool).await?;
|
||||
tracing::warn!("Postgres migrations not yet implemented");
|
||||
return Err(sqlx::Error::Configuration(
|
||||
"Postgres migrations not yet implemented".into(),
|
||||
));
|
||||
}
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => {
|
||||
return Err(sqlx::Error::Configuration(
|
||||
"No database feature enabled".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Database migrations completed successfully");
|
||||
Ok(())
|
||||
@@ -84,7 +119,8 @@ mod tests {
|
||||
async fn test_run_migrations() {
|
||||
let config = DatabaseConfig::in_memory();
|
||||
let pool = create_pool(&config).await.unwrap();
|
||||
let result = run_migrations(&pool).await;
|
||||
let db_pool = DatabasePool::Sqlite(pool);
|
||||
let result = run_migrations(&db_pool).await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
117
notes-infra/src/factory.rs
Normal file
117
notes-infra/src/factory.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{DatabaseConfig, db::DatabasePool};
|
||||
#[cfg(feature = "sqlite")]
|
||||
use crate::{SqliteNoteRepository, SqliteTagRepository, SqliteUserRepository};
|
||||
use notes_domain::{NoteRepository, TagRepository, UserRepository};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FactoryError {
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] sqlx::Error),
|
||||
#[error("Not implemented: {0}")]
|
||||
NotImplemented(String),
|
||||
}
|
||||
|
||||
pub type FactoryResult<T> = Result<T, FactoryError>;
|
||||
|
||||
pub async fn build_database_pool(db_config: &DatabaseConfig) -> FactoryResult<DatabasePool> {
|
||||
if db_config.url.starts_with("sqlite:") {
|
||||
#[cfg(feature = "sqlite")]
|
||||
{
|
||||
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&db_config.url)
|
||||
.await?;
|
||||
Ok(DatabasePool::Sqlite(pool))
|
||||
}
|
||||
#[cfg(not(feature = "sqlite"))]
|
||||
Err(FactoryError::NotImplemented(
|
||||
"SQLite feature not enabled".to_string(),
|
||||
))
|
||||
} else if db_config.url.starts_with("postgres:") {
|
||||
#[cfg(feature = "postgres")]
|
||||
{
|
||||
let pool = sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&db_config.url)
|
||||
.await?;
|
||||
Ok(DatabasePool::Postgres(pool))
|
||||
}
|
||||
#[cfg(not(feature = "postgres"))]
|
||||
Err(FactoryError::NotImplemented(
|
||||
"Postgres feature not enabled".to_string(),
|
||||
))
|
||||
} else {
|
||||
Err(FactoryError::NotImplemented(format!(
|
||||
"Unsupported database URL scheme in: {}",
|
||||
db_config.url
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_note_repository(pool: &DatabasePool) -> FactoryResult<Arc<dyn NoteRepository>> {
|
||||
match pool {
|
||||
#[cfg(feature = "sqlite")]
|
||||
DatabasePool::Sqlite(pool) => Ok(Arc::new(SqliteNoteRepository::new(pool.clone()))),
|
||||
#[cfg(feature = "postgres")]
|
||||
DatabasePool::Postgres(_) => Err(FactoryError::NotImplemented(
|
||||
"Postgres NoteRepository".to_string(),
|
||||
)),
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(FactoryError::NotImplemented(
|
||||
"No database feature enabled".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_tag_repository(pool: &DatabasePool) -> FactoryResult<Arc<dyn TagRepository>> {
|
||||
match pool {
|
||||
#[cfg(feature = "sqlite")]
|
||||
DatabasePool::Sqlite(pool) => Ok(Arc::new(SqliteTagRepository::new(pool.clone()))),
|
||||
#[cfg(feature = "postgres")]
|
||||
DatabasePool::Postgres(_) => Err(FactoryError::NotImplemented(
|
||||
"Postgres TagRepository".to_string(),
|
||||
)),
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(FactoryError::NotImplemented(
|
||||
"No database feature enabled".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_user_repository(pool: &DatabasePool) -> FactoryResult<Arc<dyn UserRepository>> {
|
||||
match pool {
|
||||
#[cfg(feature = "sqlite")]
|
||||
DatabasePool::Sqlite(pool) => Ok(Arc::new(SqliteUserRepository::new(pool.clone()))),
|
||||
#[cfg(feature = "postgres")]
|
||||
DatabasePool::Postgres(_) => Err(FactoryError::NotImplemented(
|
||||
"Postgres UserRepository".to_string(),
|
||||
)),
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(FactoryError::NotImplemented(
|
||||
"No database feature enabled".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_session_store(
|
||||
pool: &DatabasePool,
|
||||
) -> FactoryResult<crate::session_store::InfraSessionStore> {
|
||||
match pool {
|
||||
#[cfg(feature = "sqlite")]
|
||||
DatabasePool::Sqlite(pool) => {
|
||||
let store = tower_sessions_sqlx_store::SqliteStore::new(pool.clone());
|
||||
Ok(crate::session_store::InfraSessionStore::Sqlite(store))
|
||||
}
|
||||
#[cfg(feature = "postgres")]
|
||||
DatabasePool::Postgres(pool) => {
|
||||
let store = tower_sessions_sqlx_store::PostgresStore::new(pool.clone());
|
||||
Ok(crate::session_store::InfraSessionStore::Postgres(store))
|
||||
}
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(FactoryError::NotImplemented(
|
||||
"No database feature enabled".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,22 @@
|
||||
//! - [`db::run_migrations`] - Run database migrations
|
||||
|
||||
pub mod db;
|
||||
pub mod factory;
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub mod note_repository;
|
||||
pub mod session_store;
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub mod tag_repository;
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub mod user_repository;
|
||||
|
||||
// Re-export for convenience
|
||||
pub use db::{DatabaseConfig, create_pool, run_migrations};
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub use db::create_pool;
|
||||
pub use db::{DatabaseConfig, run_migrations};
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub use note_repository::SqliteNoteRepository;
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub use tag_repository::SqliteTagRepository;
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub use user_repository::SqliteUserRepository;
|
||||
|
||||
73
notes-infra/src/session_store.rs
Normal file
73
notes-infra/src/session_store.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx;
|
||||
use tower_sessions::{
|
||||
SessionStore,
|
||||
session::{Id, Record},
|
||||
};
|
||||
#[cfg(feature = "postgres")]
|
||||
use tower_sessions_sqlx_store::PostgresStore;
|
||||
#[cfg(feature = "sqlite")]
|
||||
use tower_sessions_sqlx_store::SqliteStore;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum InfraSessionStore {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Sqlite(SqliteStore),
|
||||
#[cfg(feature = "postgres")]
|
||||
Postgres(PostgresStore),
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SessionStore for InfraSessionStore {
|
||||
async fn save(&self, session_record: &Record) -> tower_sessions::session_store::Result<()> {
|
||||
match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::Sqlite(store) => store.save(session_record).await,
|
||||
#[cfg(feature = "postgres")]
|
||||
Self::Postgres(store) => store.save(session_record).await,
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(tower_sessions::session_store::Error::Backend(
|
||||
"No backend enabled".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn load(&self, session_id: &Id) -> tower_sessions::session_store::Result<Option<Record>> {
|
||||
match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::Sqlite(store) => store.load(session_id).await,
|
||||
#[cfg(feature = "postgres")]
|
||||
Self::Postgres(store) => store.load(session_id).await,
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(tower_sessions::session_store::Error::Backend(
|
||||
"No backend enabled".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete(&self, session_id: &Id) -> tower_sessions::session_store::Result<()> {
|
||||
match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::Sqlite(store) => store.delete(session_id).await,
|
||||
#[cfg(feature = "postgres")]
|
||||
Self::Postgres(store) => store.delete(session_id).await,
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(tower_sessions::session_store::Error::Backend(
|
||||
"No backend enabled".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InfraSessionStore {
|
||||
pub async fn migrate(&self) -> Result<(), sqlx::Error> {
|
||||
match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::Sqlite(store) => store.migrate().await,
|
||||
#[cfg(feature = "postgres")]
|
||||
Self::Postgres(store) => store.migrate().await,
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(sqlx::Error::Configuration("No backend enabled".into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,14 +158,15 @@ impl TagRepository for SqliteTagRepository {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::{DatabaseConfig, create_pool, run_migrations};
|
||||
use crate::db::{DatabaseConfig, DatabasePool, create_pool, run_migrations};
|
||||
use crate::user_repository::SqliteUserRepository;
|
||||
use notes_domain::{User, UserRepository};
|
||||
|
||||
async fn setup_test_db() -> SqlitePool {
|
||||
let config = DatabaseConfig::in_memory();
|
||||
let pool = create_pool(&config).await.unwrap();
|
||||
run_migrations(&pool).await.unwrap();
|
||||
let db_pool = DatabasePool::Sqlite(pool.clone());
|
||||
run_migrations(&db_pool).await.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
|
||||
@@ -133,12 +133,13 @@ impl UserRepository for SqliteUserRepository {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::{DatabaseConfig, create_pool, run_migrations};
|
||||
use crate::db::{DatabaseConfig, DatabasePool, create_pool, run_migrations};
|
||||
|
||||
async fn setup_test_db() -> SqlitePool {
|
||||
let config = DatabaseConfig::in_memory();
|
||||
let pool = create_pool(&config).await.unwrap();
|
||||
run_migrations(&pool).await.unwrap();
|
||||
let db_pool = DatabasePool::Sqlite(pool.clone());
|
||||
run_migrations(&db_pool).await.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user