use actix_web::{web, HttpRequest, HttpResponse}; use serde_json::json; use sqlx::PgPool; use uuid::Uuid; use chrono::{DateTime, Utc}; use crate::auth::AuthGuard; use mem_store::versioning::{EntityVersioningService, EdgeVersioningService}; /// GET /memory/entities/{id}/versions /// List all versions of an entity pub async fn get_entity_versions( req: HttpRequest, path: web::Path, pool: web::Data, ) -> HttpResponse { // Verify auth if let Err(e) = AuthGuard::extract_token(&req) { return HttpResponse::Unauthorized().json(json!({ "error": e.to_string() })); } let entity_id = path.into_inner(); let service = EntityVersioningService::new(pool.get_ref().clone()); match service.get_versions(&entity_id).await { Ok(versions) => HttpResponse::Ok().json(json!({ "entity_id": entity_id, "versions": versions, "total": versions.len() })), Err(e) => { tracing::error!("Failed to fetch entity versions: {}", e); HttpResponse::InternalServerError().json(json!({ "error": "Failed to fetch versions" })) } } } /// GET /memory/entities/{id}/versions/{num} /// Get specific version of an entity pub async fn get_entity_version( req: HttpRequest, path: web::Path<(String, i32)>, pool: web::Data, ) -> HttpResponse { if let Err(e) = AuthGuard::extract_token(&req) { return HttpResponse::Unauthorized().json(json!({ "error": e.to_string() })); } let (entity_id, version_num) = path.into_inner(); let service = EntityVersioningService::new(pool.get_ref().clone()); match service.get_version(&entity_id, version_num).await { Ok(Some(version)) => HttpResponse::Ok().json(json!({ "entity_id": entity_id, "version": version })), Ok(None) => HttpResponse::NotFound().json(json!({ "error": format!("Version {} not found for entity {}", version_num, entity_id) })), Err(e) => { tracing::error!("Failed to fetch entity version: {}", e); HttpResponse::InternalServerError().json(json!({ "error": "Failed to fetch version" })) } } } /// GET /memory/entities/{id}/diff?from={v1}&to={v2} /// Get diff between two versions pub async fn get_entity_diff( req: HttpRequest, path: web::Path, query: web::Query, pool: web::Data, ) -> HttpResponse { if let Err(e) = AuthGuard::extract_token(&req) { return HttpResponse::Unauthorized().json(json!({ "error": e.to_string() })); } let entity_id = path.into_inner(); let from_v = query.from; let to_v = query.to; if from_v >= to_v { return HttpResponse::BadRequest().json(json!({ "error": "from version must be < to version" })); } let service = EntityVersioningService::new(pool.get_ref().clone()); match service.diff_versions(&entity_id, from_v, to_v).await { Ok(diff) => HttpResponse::Ok().json(json!({ "entity_id": entity_id, "diff": diff })), Err(e) => { tracing::error!("Failed to compute diff: {}", e); HttpResponse::InternalServerError().json(json!({ "error": "Failed to compute diff" })) } } } /// GET /memory/entities/{id}/at?as_of={timestamp} /// Get entity state at point in time pub async fn get_entity_at_time( req: HttpRequest, path: web::Path, query: web::Query, pool: web::Data, ) -> HttpResponse { if let Err(e) = AuthGuard::extract_token(&req) { return HttpResponse::Unauthorized().json(json!({ "error": e.to_string() })); } let entity_id = path.into_inner(); let as_of = match DateTime::parse_from_rfc3339(&query.as_of) { Ok(dt) => dt.with_timezone(&Utc), Err(_) => { return HttpResponse::BadRequest().json(json!({ "error": "Invalid RFC3339 timestamp format" })) } }; let service = EntityVersioningService::new(pool.get_ref().clone()); match service.get_entity_at_time(&entity_id, as_of).await { Ok(Some(snapshot)) => HttpResponse::Ok().json(json!({ "entity_id": entity_id, "as_of": as_of.to_rfc3339(), "snapshot": snapshot })), Ok(None) => HttpResponse::NotFound().json(json!({ "error": format!("No version of {} existed before {}", entity_id, as_of) })), Err(e) => { tracing::error!("Failed to fetch entity at time: {}", e); HttpResponse::InternalServerError().json(json!({ "error": "Failed to fetch historical state" })) } } } /// GET /memory/edges/{id}/versions /// List all versions of an edge pub async fn get_edge_versions( req: HttpRequest, path: web::Path, pool: web::Data, ) -> HttpResponse { if let Err(e) = AuthGuard::extract_token(&req) { return HttpResponse::Unauthorized().json(json!({ "error": e.to_string() })); } let edge_id = path.into_inner(); let service = EdgeVersioningService::new(pool.get_ref().clone()); match service.get_versions(edge_id).await { Ok(versions) => HttpResponse::Ok().json(json!({ "edge_id": edge_id.to_string(), "versions": versions, "total": versions.len() })), Err(e) => { tracing::error!("Failed to fetch edge versions: {}", e); HttpResponse::InternalServerError().json(json!({ "error": "Failed to fetch versions" })) } } } /// GET /memory/edges/{id}/diff?from={v1}&to={v2} /// Get diff between two edge versions pub async fn get_edge_diff( req: HttpRequest, path: web::Path, query: web::Query, pool: web::Data, ) -> HttpResponse { if let Err(e) = AuthGuard::extract_token(&req) { return HttpResponse::Unauthorized().json(json!({ "error": e.to_string() })); } let edge_id = path.into_inner(); let from_v = query.from; let to_v = query.to; if from_v >= to_v { return HttpResponse::BadRequest().json(json!({ "error": "from version must be < to version" })); } let service = EdgeVersioningService::new(pool.get_ref().clone()); match service.diff_versions(edge_id, from_v, to_v).await { Ok(diff) => HttpResponse::Ok().json(json!({ "edge_id": edge_id.to_string(), "diff": diff })), Err(e) => { tracing::error!("Failed to compute edge diff: {}", e); HttpResponse::InternalServerError().json(json!({ "error": "Failed to compute diff" })) } } } // Query types #[derive(serde::Deserialize)] pub struct DiffQuery { pub from: i32, pub to: i32, } #[derive(serde::Deserialize)] pub struct TimeQuery { pub as_of: String, } #[cfg(test)] mod tests { use super::*; #[test] fn test_diff_query_validation() { let query = DiffQuery { from: 5, to: 3 }; assert!(query.from >= query.to); // Invalid } #[test] fn test_time_query_rfc3339() { let ts = "2025-01-30T10:00:00Z"; let result = DateTime::parse_from_rfc3339(ts); assert!(result.is_ok()); } }