use axum::{ Json, Router, extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, routing::{get, post}, }; use serde::Deserialize; use crate::journal::{Event, Journal}; pub fn router(journal: Journal) -> Router { Router::new() .route("/health", get(health)) .route("/event", post(record_event)) .route("/update", post(record_update)) .route("/flush", post(flush)) .route("/history/{entity_id}", get(entity_history)) .route("/recent", get(recent_events)) .route("/stats", get(stats)) .with_state(journal) } async fn health() -> &'static str { "journald ok" } // --- Record events --- #[derive(Deserialize)] struct RecordEventRequest { entity_type: String, entity_id: String, action: String, field: Option, old_value: Option, new_value: Option, actor: String, source: String, workspace_id: Option, } async fn record_event( State(journal): State, Json(req): Json, ) -> impl IntoResponse { let event = Event { event_id: String::new(), // assigned by journal timestamp: chrono::Utc::now(), entity_type: req.entity_type, entity_id: req.entity_id, field: req.field.unwrap_or_else(|| "*".to_string()), action: req.action, old_value: req.old_value.unwrap_or_default(), new_value: req.new_value.unwrap_or_default(), actor: req.actor, source: req.source, workspace_id: req.workspace_id.unwrap_or_default(), }; match journal.record(event).await { Ok(()) => Ok((StatusCode::CREATED, "event recorded")), Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)), } } #[derive(Deserialize)] struct UpdateRequest { entity_type: String, entity_id: String, field: String, old_value: String, new_value: String, actor: String, workspace_id: Option, } async fn record_update( State(journal): State, Json(req): Json, ) -> impl IntoResponse { match journal.record_update( &req.entity_type, &req.entity_id, &req.field, &req.old_value, &req.new_value, &req.actor, "api", &req.workspace_id.unwrap_or_default(), ).await { Ok(()) => Ok((StatusCode::CREATED, "update recorded")), Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)), } } // --- Flush --- async fn flush(State(journal): State) -> impl IntoResponse { match journal.flush().await { Ok(n) => Ok(format!("flushed {n} events")), Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)), } } // --- Query --- async fn entity_history( State(journal): State, Path(entity_id): Path, ) -> impl IntoResponse { match journal.get_entity_history(&entity_id).await { Ok(events) => Ok(Json(events)), Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)), } } #[derive(Deserialize)] struct RecentQuery { limit: Option, } async fn recent_events( State(journal): State, Query(q): Query, ) -> impl IntoResponse { let limit = q.limit.unwrap_or(50); match journal.get_recent(limit).await { Ok(events) => Ok(Json(events)), Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)), } } async fn stats(State(journal): State) -> impl IntoResponse { Json(journal.stats().await) }