rok-cli 0.3.6

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

pub struct ActivityLogScaffold;

impl Scaffold for ActivityLogScaffold {
    fn name(&self) -> &'static str {
        "activity-log"
    }
    fn description(&self) -> &'static str {
        "Activity log: event-driven logging, feed endpoint, actor/target filtering, retention"
    }

    fn generate(&self, args: &ScaffoldArgs) -> Result<ScaffoldResult> {
        let mut r = ScaffoldResult::default();
        let d = args.dry_run;
        write_file(
            &mut r,
            "src/app/controllers/activity_controller.rs",
            CONTROLLER,
            d,
        )?;
        write_file(&mut r, "src/app/listeners/activity_logger.rs", LISTENER, d)?;
        write_file(
            &mut r,
            "migrations/create_activities_table.sql",
            MIGRATION,
            d,
        )?;
        r.warnings
            .push("Register the ActivityLogger listener in your EventDispatcher".into());
        r.warnings.push("Register GET /activities route".into());
        Ok(r)
    }
}

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

#[derive(Deserialize)]
pub struct ActivityQuery {
    pub actor_id: Option<i64>,
    pub target_type: Option<String>,
    pub target_id: Option<i64>,
    pub page: Option<i64>,
    pub per_page: Option<i64>,
}

pub async fn index(Query(q): Query<ActivityQuery>, State(pool): State<sqlx::PgPool>) -> impl IntoResponse {
    // TODO: paginated activity feed with actor/target filters
    Response::json(serde_json::json!({ "data": [], "meta": { "total": 0 } }))
}
"#;

const LISTENER: &str = r#"use rok_events::Listener;

pub struct ActivityLogger {
    pub pool: sqlx::PgPool,
}

#[async_trait::async_trait]
impl Listener for ActivityLogger {
    async fn handle(&self, event: &rok_events::Event) -> anyhow::Result<()> {
        // TODO: INSERT INTO activities (actor_id, action, target_type, target_id, metadata)
        Ok(())
    }
}
"#;

const MIGRATION: &str = r#"CREATE TABLE activities (
    id          BIGSERIAL PRIMARY KEY,
    actor_id    BIGINT,
    action      TEXT NOT NULL,
    target_type TEXT,
    target_id   BIGINT,
    metadata    JSONB,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ON activities (actor_id, created_at DESC);
CREATE INDEX ON activities (target_type, target_id, created_at DESC);

-- Auto-prune job: delete activities older than 90 days
-- Add to your cron scheduler: DELETE FROM activities WHERE created_at < now() - interval '90 days'
"#;