# Git Commit Message

refactor: add production features and improve architecture

- Add structured configuration with validation
- Implement Redis connection pooling
- Add database migrations system
- Change API methods: GET /set → POST /set, GET /delete → DELETE /delete
- Add health check endpoint
- Add graceful shutdown and structured logging
- Update frontend for new API methods
- Add open source attribution
This commit is contained in:
creations 2025-06-04 07:56:15 -04:00
parent ad6c9b7095
commit 6bfd298455
Signed by: creations
GPG key ID: 8F553AA4320FC711
21 changed files with 842 additions and 3124 deletions

View file

@ -1,27 +1,122 @@
use crate::config::DatabaseConfig;
use sqlx::{postgres::PgPoolOptions, PgPool};
use std::env;
pub async fn connect() -> PgPool {
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is required");
use std::fs;
use std::path::Path;
use std::time::Duration;
use tracing::{error, info, warn};
pub async fn connect(config: &DatabaseConfig) -> Result<PgPool, sqlx::Error> {
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(&db_url)
.await
.expect("Failed to connect to Postgres");
.max_connections(config.max_connections)
.acquire_timeout(Duration::from_secs(config.connect_timeout_seconds))
.idle_timeout(Some(Duration::from_secs(600)))
.max_lifetime(Some(Duration::from_secs(1800)))
.connect(&config.url)
.await?;
create_migrations_table(&pool).await?;
run_migrations(&pool).await?;
Ok(pool)
}
async fn create_migrations_table(pool: &PgPool) -> Result<(), sqlx::Error> {
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS timezones (
user_id TEXT PRIMARY KEY,
username TEXT NOT NULL,
timezone TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ DEFAULT NOW()
)
"#,
)
.execute(&pool)
.await
.expect("Failed to create timezones table");
.execute(pool)
.await?;
pool
Ok(())
}
async fn run_migrations(pool: &PgPool) -> Result<(), sqlx::Error> {
let migrations_dir = Path::new("migrations");
if !migrations_dir.exists() {
warn!("Migrations directory not found, skipping migrations");
return Ok(());
}
let mut migration_files = Vec::new();
match fs::read_dir(migrations_dir) {
Ok(entries) => {
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("sql") {
if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
migration_files.push(file_name.to_string());
}
}
}
}
}
Err(e) => {
error!("Failed to read migrations directory: {}", e);
return Err(sqlx::Error::Io(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to read migrations directory: {}", e),
)));
}
}
migration_files.sort();
for migration_file in migration_files {
let applied = sqlx::query_scalar::<_, bool>(
"SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version = $1)",
)
.bind(&migration_file)
.fetch_one(pool)
.await?;
if applied {
info!("Migration {} already applied, skipping", migration_file);
continue;
}
let migration_path = migrations_dir.join(&migration_file);
let migration_sql = match fs::read_to_string(&migration_path) {
Ok(content) => content,
Err(e) => {
error!("Failed to read migration file {}: {}", migration_file, e);
return Err(sqlx::Error::Io(e));
}
};
info!("Running migration: {}", migration_file);
let mut tx = pool.begin().await?;
let statements: Vec<&str> = migration_sql
.split(';')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
for statement in statements {
if let Err(e) = sqlx::query(statement).execute(&mut *tx).await {
error!("Failed to execute migration {}: {}", migration_file, e);
return Err(e);
}
}
sqlx::query("INSERT INTO schema_migrations (version) VALUES ($1)")
.bind(&migration_file)
.execute(&mut *tx)
.await?;
tx.commit().await?;
info!("Successfully applied migration: {}", migration_file);
}
Ok(())
}