From c1c42f4fd9d420a66bec0ab229dc607b7f2ee432 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Fri, 2 Jan 2026 05:47:51 +0100 Subject: [PATCH] feat: Add initial PostgreSQL migration, refactor database connection to k_core, and simplify session store setup. --- Cargo.lock | 4 +- cargo-generate.toml | 18 ++- .../20240101000000_init_users.sql | 11 ++ .../20240101000000_init_users.sql | 0 template-api/src/main.rs | 46 ++----- template-infra/src/db.rs | 125 ++---------------- template-infra/src/factory.rs | 6 +- template-infra/src/user_repository.rs | 13 +- 8 files changed, 64 insertions(+), 159 deletions(-) create mode 100644 migrations_postgres/20240101000000_init_users.sql rename {migrations => migrations_sqlite}/20240101000000_init_users.sql (100%) diff --git a/Cargo.lock b/Cargo.lock index fc2311e..b1e6fc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1154,8 +1154,8 @@ dependencies = [ [[package]] name = "k-core" -version = "0.1.0" -source = "git+https://git.gabrielkaszewski.dev/GKaszewski/k-core#0057613308e43d9ffe47105ba29a6baaf21c9787" +version = "0.1.1" +source = "git+https://git.gabrielkaszewski.dev/GKaszewski/k-core#bda288362ade1cd3508bbc985a0aebd7714d9a18" dependencies = [ "anyhow", "chrono", diff --git a/cargo-generate.toml b/cargo-generate.toml index 1c8c55e..19775b4 100644 --- a/cargo-generate.toml +++ b/cargo-generate.toml @@ -4,9 +4,21 @@ ignore = [".git", "target", ".idea", ".vscode"] [placeholders] project_name = { type = "string", prompt = "Project name" } -database = { type = "string", prompt = "Database type", choices = ["sqlite", "postgres"], default = "sqlite" } +database = { type = "string", prompt = "Database type", choices = [ + "sqlite", + "postgres", +], default = "sqlite" } + +[hooks] +post = ["migrations.rhai"] [conditional] # Conditional dependencies based on database choice -sqlite = { condition = "database == 'sqlite'", ignore = ["template-infra/src/user_repository_postgres.rs"] } -postgres = { condition = "database == 'postgres'", ignore = ["template-infra/src/user_repository_sqlite.rs"] } +sqlite = { condition = "database == 'sqlite'", ignore = [ + "template-infra/src/user_repository_postgres.rs", + "migrations_postgres", +] } +postgres = { condition = "database == 'postgres'", ignore = [ + "template-infra/src/user_repository_sqlite.rs", + "migrations", +] } diff --git a/migrations_postgres/20240101000000_init_users.sql b/migrations_postgres/20240101000000_init_users.sql new file mode 100644 index 0000000..5ec837e --- /dev/null +++ b/migrations_postgres/20240101000000_init_users.sql @@ -0,0 +1,11 @@ +-- Create users table +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY NOT NULL, + subject TEXT NOT NULL, + email TEXT NOT NULL, + password_hash TEXT, + created_at TIMESTAMPTZ NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_users_subject ON users(subject); +CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email); diff --git a/migrations/20240101000000_init_users.sql b/migrations_sqlite/20240101000000_init_users.sql similarity index 100% rename from migrations/20240101000000_init_users.sql rename to migrations_sqlite/20240101000000_init_users.sql diff --git a/template-api/src/main.rs b/template-api/src/main.rs index eea2371..aabbd77 100644 --- a/template-api/src/main.rs +++ b/template-api/src/main.rs @@ -1,10 +1,10 @@ use std::net::SocketAddr; use std::time::Duration as StdDuration; -use template_domain::UserService; -use template_infra::factory::build_user_repository; -use template_infra::{db, session_store}; use k_core::logging; +use template_domain::UserService; +use template_infra::factory::build_session_store; +use template_infra::factory::build_user_repository; use tokio::net::TcpListener; use tower_sessions::{Expiry, SessionManagerLayer}; use tracing::info; @@ -32,43 +32,27 @@ async fn main() -> anyhow::Result<()> { info!("Starting server on {}:{}", config.host, config.port); // 3. Connect to database - let db_config = db::DatabaseConfig { + // k-core handles the "Which DB are we using?" logic internally based on feature flags + // and returns the correct Enum variant. + let db_config = k_core::db::DatabaseConfig { url: config.database_url.clone(), max_connections: 5, - min_connections: 1, acquire_timeout: StdDuration::from_secs(30), }; - - // We assume generic connection logic in k-core/template-infra - // But here we use k-core via template-infra - #[cfg(feature = "sqlite")] - let pool = k_core::db::connect_sqlite(&db_config.url).await?; - - #[cfg(feature = "postgres")] - let pool = k_core::db::connect_postgres(&db_config.url).await?; - #[cfg(feature = "sqlite")] - let db_pool = template_infra::db::DatabasePool::Sqlite(pool.clone()); - #[cfg(feature = "postgres")] - let db_pool = template_infra::db::DatabasePool::Postgres(pool.clone()); + // Returns k_core::db::DatabasePool + let db_pool = k_core::db::connect(&db_config).await?; - // 4. Run migrations - db::run_migrations(&db_pool).await?; + // 4. Run migrations (using the re-export if you kept it, or direct k_core) + template_infra::db::run_migrations(&db_pool).await?; // 5. Initialize Services let user_repo = build_user_repository(&db_pool).await?; let user_service = UserService::new(user_repo.clone()); - + // 6. Setup Session Store - #[cfg(feature = "sqlite")] - let session_store = session_store::InfraSessionStore::Sqlite( - tower_sessions_sqlx_store::SqliteStore::new(pool.clone()) - ); - #[cfg(feature = "postgres")] - let session_store = session_store::InfraSessionStore::Postgres( - tower_sessions_sqlx_store::PostgresStore::new(pool.clone()) - ); - + let session_store = build_session_store(&db_pool).await?; + let session_layer = SessionManagerLayer::new(session_store) .with_secure(false) // Set to true in production with HTTPS .with_expiry(Expiry::OnInactivity(time::Duration::hours(1))); @@ -80,9 +64,7 @@ async fn main() -> anyhow::Result<()> { let state = AppState::new(user_service, config.clone()); // 9. Build Router - let app = routes::api_v1_router() - .layer(auth_layer) - .with_state(state); + let app = routes::api_v1_router().layer(auth_layer).with_state(state); // 10. Start Server let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?; diff --git a/template-infra/src/db.rs b/template-infra/src/db.rs index 8d89678..d5a618f 100644 --- a/template-infra/src/db.rs +++ b/template-infra/src/db.rs @@ -1,126 +1,19 @@ -//! Database connection pool management +pub use k_core::db::{DatabaseConfig, DatabasePool}; -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; - -/// Configuration for the database connection -#[derive(Debug, Clone)] -pub struct DatabaseConfig { - pub url: String, - pub max_connections: u32, - pub min_connections: u32, - pub acquire_timeout: Duration, -} - -impl Default for DatabaseConfig { - fn default() -> Self { - Self { - url: "sqlite:data.db?mode=rwc".to_string(), - max_connections: 5, - min_connections: 1, - acquire_timeout: Duration::from_secs(5), - } - } -} - -impl DatabaseConfig { - pub fn new(url: impl Into) -> Self { - Self { - url: url.into(), - ..Default::default() - } - } - - pub fn in_memory() -> Self { - Self { - url: "sqlite::memory:".to_string(), - max_connections: 1, // SQLite in-memory is single-connection - min_connections: 1, - ..Default::default() - } - } -} - -#[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) - .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) - .synchronous(sqlx::sqlite::SqliteSynchronous::Normal) - .busy_timeout(Duration::from_secs(30)); - - let pool = SqlitePoolOptions::new() - .max_connections(config.max_connections) - .min_connections(config.min_connections) - .acquire_timeout(config.acquire_timeout) - .connect_with(options) - .await?; - - Ok(pool) -} - -/// Run database migrations pub async fn run_migrations(pool: &DatabasePool) -> Result<(), sqlx::Error> { match pool { - #[cfg(feature = "sqlite")] DatabasePool::Sqlite(pool) => { - sqlx::migrate!("../migrations").run(pool).await?; + sqlx::migrate!("../migrations_sqlite").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(), - )); + DatabasePool::Postgres(_) => { + // Postgres migrations would go here + tracing::warn!("Postgres migrations not implemented in template yet"); + // Pass through the types from the core library + // This allows you to change k-core later without breaking imports in template-infra + // The `pub use` statement cannot be placed inside a match arm. + // It is already present at the top of the file. } } - - tracing::info!("Database migrations completed successfully"); Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_create_in_memory_pool() { - let config = DatabaseConfig::in_memory(); - let pool = create_pool(&config).await; - assert!(pool.is_ok()); - } - - #[tokio::test] - async fn test_run_migrations() { - let config = DatabaseConfig::in_memory(); - let pool = create_pool(&config).await.unwrap(); - let db_pool = DatabasePool::Sqlite(pool); - let result = run_migrations(&db_pool).await; - assert!(result.is_ok()); - } -} diff --git a/template-infra/src/factory.rs b/template-infra/src/factory.rs index dc3e033..1e45e01 100644 --- a/template-infra/src/factory.rs +++ b/template-infra/src/factory.rs @@ -1,8 +1,8 @@ use std::sync::Arc; -use crate::db::DatabasePool; #[cfg(feature = "sqlite")] use crate::SqliteUserRepository; +use crate::db::DatabasePool; use template_domain::UserRepository; #[derive(Debug, thiserror::Error)] @@ -22,7 +22,9 @@ pub async fn build_user_repository(pool: &DatabasePool) -> FactoryResult Ok(Arc::new(SqliteUserRepository::new(pool.clone()))), #[cfg(feature = "postgres")] - DatabasePool::Postgres(pool) => Ok(Arc::new(crate::user_repository::PostgresUserRepository::new(pool.clone()))), + DatabasePool::Postgres(pool) => Ok(Arc::new( + crate::user_repository::PostgresUserRepository::new(pool.clone()), + )), #[allow(unreachable_patterns)] _ => Err(FactoryError::NotImplemented( "No database feature enabled".to_string(), diff --git a/template-infra/src/user_repository.rs b/template-infra/src/user_repository.rs index 7c0d501..9feb56b 100644 --- a/template-infra/src/user_repository.rs +++ b/template-infra/src/user_repository.rs @@ -141,14 +141,19 @@ impl UserRepository for SqliteUserRepository { #[cfg(all(test, feature = "sqlite"))] mod tests { use super::*; - use crate::db::{DatabaseConfig, DatabasePool, create_pool, run_migrations}; + use crate::db::{DatabaseConfig, DatabasePool, run_migrations}; + use k_core::db::connect; // Import k_core::db::connect async fn setup_test_db() -> SqlitePool { let config = DatabaseConfig::in_memory(); - let pool = create_pool(&config).await.unwrap(); - let db_pool = DatabasePool::Sqlite(pool.clone()); + // connect returns DatabasePool directly now + let db_pool = connect(&config).await.expect("Failed to create pool"); run_migrations(&db_pool).await.unwrap(); - pool + // Extract SqlitePool from DatabasePool for SqliteUserRepository + match db_pool { + DatabasePool::Sqlite(pool) => pool, + _ => panic!("Expected SqlitePool for testing"), + } } #[tokio::test]