260 lines
7.5 KiB
Rust
260 lines
7.5 KiB
Rust
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<String>,
|
||
|
|
pool: web::Data<PgPool>,
|
||
|
|
) -> 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<PgPool>,
|
||
|
|
) -> 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<String>,
|
||
|
|
query: web::Query<DiffQuery>,
|
||
|
|
pool: web::Data<PgPool>,
|
||
|
|
) -> 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<String>,
|
||
|
|
query: web::Query<TimeQuery>,
|
||
|
|
pool: web::Data<PgPool>,
|
||
|
|
) -> 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<Uuid>,
|
||
|
|
pool: web::Data<PgPool>,
|
||
|
|
) -> 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<Uuid>,
|
||
|
|
query: web::Query<DiffQuery>,
|
||
|
|
pool: web::Data<PgPool>,
|
||
|
|
) -> 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());
|
||
|
|
}
|
||
|
|
}
|