Skip to main content

tuitbot_server/routes/approval/
export.rs

1//! Export and history endpoints for the approval queue.
2
3use std::sync::Arc;
4
5use axum::extract::{Path, Query, State};
6use axum::Json;
7use serde::Deserialize;
8use serde_json::{json, Value};
9use tuitbot_core::storage::approval_queue;
10
11use crate::account::AccountContext;
12use crate::error::ApiError;
13use crate::state::AppState;
14
15/// Query parameters for the approval export endpoint.
16#[derive(Deserialize)]
17pub struct ExportQuery {
18    /// Export format: "csv" or "json" (default: "csv").
19    #[serde(default = "default_csv")]
20    pub format: String,
21    /// Comma-separated status values (default: all).
22    #[serde(default = "default_export_status")]
23    pub status: String,
24    /// Filter by action type.
25    #[serde(rename = "type")]
26    pub action_type: Option<String>,
27}
28
29fn default_csv() -> String {
30    "csv".to_string()
31}
32
33fn default_export_status() -> String {
34    "pending,approved,rejected,posted".to_string()
35}
36
37/// `GET /api/approval/export` — export approval items as CSV or JSON.
38pub async fn export_items(
39    State(state): State<Arc<AppState>>,
40    ctx: AccountContext,
41    Query(params): Query<ExportQuery>,
42) -> Result<axum::response::Response, ApiError> {
43    use axum::response::IntoResponse;
44
45    let statuses: Vec<&str> = params.status.split(',').map(|s| s.trim()).collect();
46    let action_type = params.action_type.as_deref();
47
48    let items =
49        approval_queue::get_by_statuses_for(&state.db, &ctx.account_id, &statuses, action_type)
50            .await?;
51
52    if params.format == "json" {
53        let body = serde_json::to_string(&items).unwrap_or_else(|_| "[]".to_string());
54        Ok((
55            [
56                (
57                    axum::http::header::CONTENT_TYPE,
58                    "application/json; charset=utf-8",
59                ),
60                (
61                    axum::http::header::CONTENT_DISPOSITION,
62                    "attachment; filename=\"approval_export.json\"",
63                ),
64            ],
65            body,
66        )
67            .into_response())
68    } else {
69        let mut csv = String::from(
70            "id,action_type,target_author,generated_content,topic,score,status,reviewed_by,review_notes,created_at\n",
71        );
72        for item in &items {
73            csv.push_str(&format!(
74                "{},{},{},{},{},{},{},{},{},{}\n",
75                item.id,
76                escape_csv(&item.action_type),
77                escape_csv(&item.target_author),
78                escape_csv(&item.generated_content),
79                escape_csv(&item.topic),
80                item.score,
81                escape_csv(&item.status),
82                escape_csv(item.reviewed_by.as_deref().unwrap_or("")),
83                escape_csv(item.review_notes.as_deref().unwrap_or("")),
84                escape_csv(&item.created_at),
85            ));
86        }
87        Ok((
88            [
89                (axum::http::header::CONTENT_TYPE, "text/csv; charset=utf-8"),
90                (
91                    axum::http::header::CONTENT_DISPOSITION,
92                    "attachment; filename=\"approval_export.csv\"",
93                ),
94            ],
95            csv,
96        )
97            .into_response())
98    }
99}
100
101/// Escape a value for CSV output.
102pub(super) fn escape_csv(value: &str) -> String {
103    if value.contains(',') || value.contains('"') || value.contains('\n') {
104        format!("\"{}\"", value.replace('"', "\"\""))
105    } else {
106        value.to_string()
107    }
108}
109
110/// `GET /api/approval/:id/history` — get edit history for an item.
111pub async fn get_edit_history(
112    State(state): State<Arc<AppState>>,
113    _ctx: AccountContext,
114    Path(id): Path<i64>,
115) -> Result<Json<Value>, ApiError> {
116    // Query by approval_id PK is already implicitly scoped.
117    let history = approval_queue::get_edit_history(&state.db, id).await?;
118    Ok(Json(json!(history)))
119}