diff --git a/notes-api/Cargo.toml b/notes-api/Cargo.toml index 529a9f1..15c2855 100644 --- a/notes-api/Cargo.toml +++ b/notes-api/Cargo.toml @@ -6,7 +6,7 @@ default-run = "notes-api" [dependencies] notes-domain = { path = "../notes-domain" } -notes-infra = { path = "../notes-infra" } +notes-infra = { path = "../notes-infra", features = ["sqlite"] } # Web framework axum = { version = "0.8.8", features = ["macros"] } diff --git a/notes-infra/Cargo.toml b/notes-infra/Cargo.toml index f3d9b74..e44cdff 100644 --- a/notes-infra/Cargo.toml +++ b/notes-infra/Cargo.toml @@ -3,14 +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", "postgres"] } +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", features = ["sqlite", "postgres"] } +tower-sessions-sqlx-store = { version = "0.15.0", default-features = false } diff --git a/notes-infra/src/db.rs b/notes-infra/src/db.rs index 05287f5..8d89678 100644 --- a/notes-infra/src/db.rs +++ b/notes-infra/src/db.rs @@ -1,7 +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}; -use sqlx::{Pool, Postgres, Sqlite}; +#[cfg(feature = "sqlite")] use std::str::FromStr; use std::time::Duration; @@ -45,11 +51,14 @@ impl DatabaseConfig { #[derive(Clone, Debug)] pub enum DatabasePool { + #[cfg(feature = "sqlite")] Sqlite(Pool), + #[cfg(feature = "postgres")] Postgres(Pool), } /// Create a database connection pool +#[cfg(feature = "sqlite")] pub async fn create_pool(config: &DatabaseConfig) -> Result { let options = SqliteConnectOptions::from_str(&config.url)? .create_if_missing(true) @@ -70,9 +79,11 @@ pub async fn create_pool(config: &DatabaseConfig) -> Result 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?; @@ -81,6 +92,12 @@ pub async fn run_migrations(pool: &DatabasePool) -> Result<(), sqlx::Error> { "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"); diff --git a/notes-infra/src/factory.rs b/notes-infra/src/factory.rs index f5d190a..4a8807f 100644 --- a/notes-infra/src/factory.rs +++ b/notes-infra/src/factory.rs @@ -1,10 +1,10 @@ use std::sync::Arc; +use crate::{DatabaseConfig, db::DatabasePool}; +#[cfg(feature = "sqlite")] +use crate::{SqliteNoteRepository, SqliteTagRepository, SqliteUserRepository}; use notes_domain::{NoteRepository, TagRepository, UserRepository}; -pub use crate::db::DatabasePool; -use crate::{DatabaseConfig, SqliteNoteRepository, SqliteTagRepository, SqliteUserRepository}; - #[derive(Debug, thiserror::Error)] pub enum FactoryError { #[error("Database error: {0}")] @@ -17,17 +17,31 @@ pub type FactoryResult = Result; pub async fn build_database_pool(db_config: &DatabaseConfig) -> FactoryResult { if db_config.url.starts_with("sqlite:") { - let pool = sqlx::sqlite::SqlitePoolOptions::new() - .max_connections(5) - .connect(&db_config.url) - .await?; - Ok(DatabasePool::Sqlite(pool)) + #[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:") { - let pool = sqlx::postgres::PgPoolOptions::new() - .max_connections(5) - .connect(&db_config.url) - .await?; - Ok(DatabasePool::Postgres(pool)) + #[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: {}", @@ -38,28 +52,46 @@ pub async fn build_database_pool(db_config: &DatabaseConfig) -> FactoryResult FactoryResult> { 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> { 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> { 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(), + )), } } @@ -67,13 +99,19 @@ pub async fn build_session_store( pool: &DatabasePool, ) -> FactoryResult { 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(), + )), } } diff --git a/notes-infra/src/lib.rs b/notes-infra/src/lib.rs index b108745..bc9a051 100644 --- a/notes-infra/src/lib.rs +++ b/notes-infra/src/lib.rs @@ -16,13 +16,21 @@ 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; diff --git a/notes-infra/src/session_store.rs b/notes-infra/src/session_store.rs index 727bfed..462aa85 100644 --- a/notes-infra/src/session_store.rs +++ b/notes-infra/src/session_store.rs @@ -4,11 +4,16 @@ use tower_sessions::{ SessionStore, session::{Id, Record}, }; -use tower_sessions_sqlx_store::{PostgresStore, SqliteStore}; +#[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), } @@ -16,22 +21,40 @@ pub enum InfraSessionStore { 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> { 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(), + )), } } } @@ -39,8 +62,12 @@ impl SessionStore for InfraSessionStore { 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())), } } }