wip:milestone 0 fixes
Some checks failed
CI/CD Pipeline / unit-tests (push) Failing after 1m16s
CI/CD Pipeline / integration-tests (push) Failing after 2m32s
CI/CD Pipeline / lint (push) Successful in 5m22s
CI/CD Pipeline / e2e-tests (push) Has been skipped
CI/CD Pipeline / build (push) Has been skipped

This commit is contained in:
2026-03-15 12:35:42 +02:00
parent 6708cf28a7
commit cffdf8af86
61266 changed files with 4511646 additions and 1938 deletions

165
common/src/cache.rs Normal file
View File

@@ -0,0 +1,165 @@
//! Multi-tier caching layer for MadBase
use redis::{AsyncCommands, Client};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;
use chrono::{DateTime, Utc};
#[derive(Error, Debug)]
pub enum CacheError {
#[error("Redis connection error: {0}")]
ConnectionError(#[from] redis::RedisError),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
#[error("Key not found: {0}")]
NotFound(String),
#[error("Lock acquisition failed")]
LockError,
}
pub type CacheResult<T> = Result<T, CacheError>;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SessionData {
pub user_id: Uuid,
pub email: String,
pub role: String,
pub created_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
}
#[derive(Clone)]
pub struct DistributedLock {
key: String,
cache: CacheLayer,
}
impl DistributedLock {
pub async fn acquire(cache: CacheLayer, key: &str, ttl_seconds: u64) -> CacheResult<Option<Self>> {
if cache.acquire(key, ttl_seconds).await? {
Ok(Some(Self { key: key.to_string(), cache }))
} else {
Ok(None)
}
}
pub async fn release(self) -> CacheResult<()> {
self.cache.release(&self.key).await
}
}
#[derive(Clone)]
pub struct RedisClient {
client: Client,
}
impl RedisClient {
pub fn new(redis_url: &str) -> CacheResult<Self> {
let client = Client::open(redis_url)?;
Ok(Self { client })
}
pub fn get_connection(&self) -> CacheResult<redis::Connection> {
Ok(self.client.get_connection()?)
}
pub async fn get_async_connection(&self) -> CacheResult<redis::aio::MultiplexedConnection> {
Ok(self.client.get_multiplexed_async_connection().await?)
}
pub async fn ping(&self) -> CacheResult<String> {
let mut conn = self.get_async_connection().await?;
let response: String = redis::cmd("PING").query_async(&mut conn).await?;
Ok(response)
}
}
#[derive(Clone)]
pub struct CacheLayer {
pub redis: Option<RedisClient>,
ttl_seconds: u64,
}
impl CacheLayer {
pub fn new(redis_url: Option<String>, ttl_seconds: u64) -> Self {
let redis = redis_url.and_then(|url| RedisClient::new(&url).ok());
Self { redis, ttl_seconds }
}
pub async fn get<T>(&self, key: &str) -> CacheResult<Option<T>>
where T: for<'de> Deserialize<'de> {
if let Some(redis) = &self.redis {
let mut conn = redis.get_async_connection().await?;
let value: Option<String> = redis::cmd("GET").arg(key).query_async(&mut conn).await?;
if let Some(v) = value {
return Ok(serde_json::from_str(&v)?);
}
}
Ok(None)
}
pub async fn get_session(&self, session_token: String) -> CacheResult<Option<SessionData>> {
self.get(&format!("session:{}", session_token)).await
}
pub async fn delete_session(&self, session_token: String) -> CacheResult<()> {
self.delete(&format!("session:{}", session_token)).await
}
pub async fn set<T>(&self, key: &str, value: &T) -> CacheResult<()>
where T: Serialize {
if let Some(redis) = &self.redis {
let mut conn = redis.get_async_connection().await?;
redis::cmd("SET").arg(key).arg(serde_json::to_string(value)?).arg("EX").arg(self.ttl_seconds).query_async::<_, ()>(&mut conn).await?;
}
Ok(())
}
pub async fn delete(&self, key: &str) -> CacheResult<()> {
if let Some(redis) = &self.redis {
let mut conn = redis.get_async_connection().await?;
redis::cmd("DEL").arg(key).query_async::<_, ()>(&mut conn).await?;
}
Ok(())
}
pub async fn mset<T>(&self, pairs: Vec<(String, T)>) -> CacheResult<()>
where T: Serialize {
if let Some(redis) = &self.redis {
let mut conn = redis.get_async_connection().await?;
let kv_pairs: Vec<(String, String)> = pairs.into_iter().map(|(k, v)| (k, serde_json::to_string(&v).unwrap())).collect();
conn.mset::<_, _, ()>(&kv_pairs).await?;
}
Ok(())
}
pub async fn mget<T>(&self, keys: &[String]) -> CacheResult<Vec<Option<T>>>
where T: for<'de> Deserialize<'de> {
if let Some(redis) = &self.redis {
let mut conn = redis.get_async_connection().await?;
let values: Vec<Option<String>> = redis::cmd("MGET").arg(keys.len()).query_async(&mut conn).await?;
return Ok(values.into_iter().map(|v| v.and_then(|s| serde_json::from_str(&s).ok())).collect());
}
Ok(keys.iter().map(|_| None).collect())
}
pub async fn acquire(&self, key: &str, ttl_seconds: u64) -> CacheResult<bool> {
if let Some(redis) = &self.redis {
let mut conn = redis.get_async_connection().await?;
let result: Option<String> = redis::cmd("SET").arg(&format!("lock:{}", key)).arg(Uuid::new_v4().to_string()).arg("NX").arg("EX").arg(ttl_seconds).query_async(&mut conn).await?;
return Ok(result.is_some());
}
Ok(true)
}
pub async fn release(&self, key: &str) -> CacheResult<()> {
if let Some(redis) = &self.redis {
let mut conn = redis.get_async_connection().await?;
let script = r#"if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end"#;
redis::Script::new(script).key(&format!("lock:{}", key)).arg(Uuid::new_v4().to_string()).invoke_async::<_, ()>(&mut conn).await?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_error_display() {
let err = CacheError::NotFound("test_key".to_string());
assert_eq!(err.to_string(), "Key not found: test_key");
}
#[tokio::test]
async fn test_cache_layer_new() {
let cache = CacheLayer::new(None, 3600);
assert!(cache.redis.is_none());
}
}

View File

@@ -1,9 +1,10 @@
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use std::env;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize)]
pub struct Config {
pub database_url: String,
pub redis_url: Option<String>,
pub jwt_secret: String,
pub port: u16,
pub google_client_id: Option<String>,
@@ -25,8 +26,14 @@ pub struct Config {
impl Config {
pub fn new() -> Result<Self, config::ConfigError> {
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let jwt_secret =
env::var("JWT_SECRET").unwrap_or_else(|_| "super-secret-key-please-change".to_string());
let redis_url = env::var("REDIS_URL").ok();
let jwt_secret = env::var("JWT_SECRET")
.expect("JWT_SECRET must be set. Generate one with: openssl rand -hex 32");
if jwt_secret.len() < 32 {
panic!("JWT_SECRET must be at least 32 characters long");
}
let port = env::var("PORT")
.unwrap_or_else(|_| "8000".to_string())
.parse()
@@ -53,6 +60,7 @@ impl Config {
Ok(Config {
database_url,
redis_url,
jwt_secret,
port,
google_client_id,
@@ -73,11 +81,11 @@ impl Config {
}
}
// New struct for Project Context
#[derive(Clone, Debug)]
pub struct ProjectContext {
pub project_ref: String,
pub db_url: String,
pub redis_url: Option<String>,
pub jwt_secret: String,
pub anon_key: Option<String>,
pub service_role_key: Option<String>,