feat: Add initial PostgreSQL migration, refactor database connection to k_core, and simplify session store setup.
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -1154,8 +1154,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "k-core"
|
name = "k-core"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
source = "git+https://git.gabrielkaszewski.dev/GKaszewski/k-core#0057613308e43d9ffe47105ba29a6baaf21c9787"
|
source = "git+https://git.gabrielkaszewski.dev/GKaszewski/k-core#bda288362ade1cd3508bbc985a0aebd7714d9a18"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
@@ -4,9 +4,21 @@ ignore = [".git", "target", ".idea", ".vscode"]
|
|||||||
|
|
||||||
[placeholders]
|
[placeholders]
|
||||||
project_name = { type = "string", prompt = "Project name" }
|
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]
|
||||||
# Conditional dependencies based on database choice
|
# Conditional dependencies based on database choice
|
||||||
sqlite = { condition = "database == 'sqlite'", ignore = ["template-infra/src/user_repository_postgres.rs"] }
|
sqlite = { condition = "database == 'sqlite'", ignore = [
|
||||||
postgres = { condition = "database == 'postgres'", ignore = ["template-infra/src/user_repository_sqlite.rs"] }
|
"template-infra/src/user_repository_postgres.rs",
|
||||||
|
"migrations_postgres",
|
||||||
|
] }
|
||||||
|
postgres = { condition = "database == 'postgres'", ignore = [
|
||||||
|
"template-infra/src/user_repository_sqlite.rs",
|
||||||
|
"migrations",
|
||||||
|
] }
|
||||||
|
|||||||
11
migrations_postgres/20240101000000_init_users.sql
Normal file
11
migrations_postgres/20240101000000_init_users.sql
Normal file
@@ -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);
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::time::Duration as StdDuration;
|
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 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 tokio::net::TcpListener;
|
||||||
use tower_sessions::{Expiry, SessionManagerLayer};
|
use tower_sessions::{Expiry, SessionManagerLayer};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@@ -32,42 +32,26 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
info!("Starting server on {}:{}", config.host, config.port);
|
info!("Starting server on {}:{}", config.host, config.port);
|
||||||
|
|
||||||
// 3. Connect to database
|
// 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(),
|
url: config.database_url.clone(),
|
||||||
max_connections: 5,
|
max_connections: 5,
|
||||||
min_connections: 1,
|
|
||||||
acquire_timeout: StdDuration::from_secs(30),
|
acquire_timeout: StdDuration::from_secs(30),
|
||||||
};
|
};
|
||||||
|
|
||||||
// We assume generic connection logic in k-core/template-infra
|
// Returns k_core::db::DatabasePool
|
||||||
// But here we use k-core via template-infra
|
let db_pool = k_core::db::connect(&db_config).await?;
|
||||||
#[cfg(feature = "sqlite")]
|
|
||||||
let pool = k_core::db::connect_sqlite(&db_config.url).await?;
|
|
||||||
|
|
||||||
#[cfg(feature = "postgres")]
|
// 4. Run migrations (using the re-export if you kept it, or direct k_core)
|
||||||
let pool = k_core::db::connect_postgres(&db_config.url).await?;
|
template_infra::db::run_migrations(&db_pool).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());
|
|
||||||
|
|
||||||
// 4. Run migrations
|
|
||||||
db::run_migrations(&db_pool).await?;
|
|
||||||
|
|
||||||
// 5. Initialize Services
|
// 5. Initialize Services
|
||||||
let user_repo = build_user_repository(&db_pool).await?;
|
let user_repo = build_user_repository(&db_pool).await?;
|
||||||
let user_service = UserService::new(user_repo.clone());
|
let user_service = UserService::new(user_repo.clone());
|
||||||
|
|
||||||
// 6. Setup Session Store
|
// 6. Setup Session Store
|
||||||
#[cfg(feature = "sqlite")]
|
let session_store = build_session_store(&db_pool).await?;
|
||||||
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_layer = SessionManagerLayer::new(session_store)
|
let session_layer = SessionManagerLayer::new(session_store)
|
||||||
.with_secure(false) // Set to true in production with HTTPS
|
.with_secure(false) // Set to true in production with HTTPS
|
||||||
@@ -80,9 +64,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let state = AppState::new(user_service, config.clone());
|
let state = AppState::new(user_service, config.clone());
|
||||||
|
|
||||||
// 9. Build Router
|
// 9. Build Router
|
||||||
let app = routes::api_v1_router()
|
let app = routes::api_v1_router().layer(auth_layer).with_state(state);
|
||||||
.layer(auth_layer)
|
|
||||||
.with_state(state);
|
|
||||||
|
|
||||||
// 10. Start Server
|
// 10. Start Server
|
||||||
let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?;
|
let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?;
|
||||||
|
|||||||
@@ -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<String>) -> 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<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)
|
|
||||||
.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> {
|
pub async fn run_migrations(pool: &DatabasePool) -> Result<(), sqlx::Error> {
|
||||||
match pool {
|
match pool {
|
||||||
#[cfg(feature = "sqlite")]
|
|
||||||
DatabasePool::Sqlite(pool) => {
|
DatabasePool::Sqlite(pool) => {
|
||||||
sqlx::migrate!("../migrations").run(pool).await?;
|
sqlx::migrate!("../migrations_sqlite").run(pool).await?;
|
||||||
}
|
}
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
DatabasePool::Postgres(_pool) => {
|
DatabasePool::Postgres(_) => {
|
||||||
// Placeholder for Postgres migrations
|
// Postgres migrations would go here
|
||||||
// sqlx::migrate!("../migrations/postgres").run(_pool).await?;
|
tracing::warn!("Postgres migrations not implemented in template yet");
|
||||||
tracing::warn!("Postgres migrations not yet implemented");
|
// Pass through the types from the core library
|
||||||
return Err(sqlx::Error::Configuration(
|
// This allows you to change k-core later without breaking imports in template-infra
|
||||||
"Postgres migrations not yet implemented".into(),
|
// The `pub use` statement cannot be placed inside a match arm.
|
||||||
));
|
// It is already present at the top of the file.
|
||||||
}
|
|
||||||
#[allow(unreachable_patterns)]
|
|
||||||
_ => {
|
|
||||||
return Err(sqlx::Error::Configuration(
|
|
||||||
"No database feature enabled".into(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!("Database migrations completed successfully");
|
|
||||||
Ok(())
|
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::db::DatabasePool;
|
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
use crate::SqliteUserRepository;
|
use crate::SqliteUserRepository;
|
||||||
|
use crate::db::DatabasePool;
|
||||||
use template_domain::UserRepository;
|
use template_domain::UserRepository;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
@@ -22,7 +22,9 @@ pub async fn build_user_repository(pool: &DatabasePool) -> FactoryResult<Arc<dyn
|
|||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
DatabasePool::Sqlite(pool) => Ok(Arc::new(SqliteUserRepository::new(pool.clone()))),
|
DatabasePool::Sqlite(pool) => Ok(Arc::new(SqliteUserRepository::new(pool.clone()))),
|
||||||
#[cfg(feature = "postgres")]
|
#[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)]
|
#[allow(unreachable_patterns)]
|
||||||
_ => Err(FactoryError::NotImplemented(
|
_ => Err(FactoryError::NotImplemented(
|
||||||
"No database feature enabled".to_string(),
|
"No database feature enabled".to_string(),
|
||||||
|
|||||||
@@ -141,14 +141,19 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
#[cfg(all(test, feature = "sqlite"))]
|
#[cfg(all(test, feature = "sqlite"))]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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 {
|
async fn setup_test_db() -> SqlitePool {
|
||||||
let config = DatabaseConfig::in_memory();
|
let config = DatabaseConfig::in_memory();
|
||||||
let pool = create_pool(&config).await.unwrap();
|
// connect returns DatabasePool directly now
|
||||||
let db_pool = DatabasePool::Sqlite(pool.clone());
|
let db_pool = connect(&config).await.expect("Failed to create pool");
|
||||||
run_migrations(&db_pool).await.unwrap();
|
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]
|
#[tokio::test]
|
||||||
|
|||||||
Reference in New Issue
Block a user