rok-cli 0.3.2

Developer CLI for rok-based Axum applications
use super::{write_file, Scaffold, ScaffoldArgs, ScaffoldResult};
use anyhow::Result;

pub struct ExportScaffold;

impl Scaffold for ExportScaffold {
    fn name(&self) -> &'static str {
        "export"
    }
    fn description(&self) -> &'static str {
        "CSV/Excel export: streaming response, scheduled job, download links with expiry"
    }

    fn generate(&self, args: &ScaffoldArgs) -> Result<ScaffoldResult> {
        let mut r = ScaffoldResult::default();
        let d = args.dry_run;
        write_file(
            &mut r,
            "src/app/controllers/export_controller.rs",
            CONTROLLER,
            d,
        )?;
        write_file(&mut r, "src/app/jobs/export_job.rs", JOB, d)?;
        write_file(
            &mut r,
            "migrations/create_export_logs_table.sql",
            MIGRATION,
            d,
        )?;
        r.warnings
            .push("Register GET /exports and POST /exports routes".into());
        Ok(r)
    }
}

const CONTROLLER: &str = r#"use axum::{extract::{Path, Query, State}, response::IntoResponse};
use rok_auth::axum::{Ctx, Response};
use serde::Deserialize;

#[derive(Deserialize)]
pub struct ExportQuery { pub format: Option<String>, pub filter: Option<String> }

pub async fn export(ctx: Ctx, Query(q): Query<ExportQuery>, State(pool): State<sqlx::PgPool>) -> impl IntoResponse {
    let fmt = q.format.as_deref().unwrap_or("csv");
    // TODO: for small datasets: stream directly
    //       for large datasets: enqueue ExportJob, return job_id
    Response::json(serde_json::json!({ "job_id": null, "download_url": null, "format": fmt }))
}

pub async fn download(Path(token): Path<String>) -> impl IntoResponse {
    // TODO: verify signed token, serve file with Content-Disposition
    Response::not_found("Export not found or expired")
}
"#;

const JOB: &str = r#"use rok_queue::Job;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct ExportJob {
    pub user_id: i64,
    pub query: String,
    pub format: String,
    pub log_id: i64,
}

#[async_trait::async_trait]
impl Job for ExportJob {
    const QUEUE: &'static str = "exports";

    async fn handle(&self) -> anyhow::Result<()> {
        // TODO: run query, write CSV/Excel to storage, update export_logs with download URL
        Ok(())
    }
}
"#;

const MIGRATION: &str = r#"CREATE TABLE export_logs (
    id           BIGSERIAL PRIMARY KEY,
    user_id      BIGINT NOT NULL,
    format       TEXT NOT NULL DEFAULT 'csv',
    status       TEXT NOT NULL DEFAULT 'queued',
    download_url TEXT,
    expires_at   TIMESTAMPTZ,
    created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);
"#;