//! `pic triggers` subcommands. //! //! Three per-kind create helpers cover the most common trigger shapes //! (KV, cron, dead_letter). For docs/files/pubsub/email/queue the //! generic `create-from-json` accepts a raw JSON body so headless //! operators don't get blocked waiting for per-kind wrappers. use anyhow::{anyhow, Context, Result}; use serde_json::json; use crate::client::Client; use crate::config; use crate::output::{KvBlock, OutputMode, Table}; pub async fn ls(app: &str, mode: OutputMode) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let resp = client.triggers_list(app).await?; let mut table = Table::new([ "id", "kind", "script_id", "enabled", "dispatch", "retry_max", "materialized", "created_at", ]); for t in resp.triggers { table.row([ t.id, t.kind, t.script_id.to_string(), t.enabled.to_string(), t.dispatch_mode, t.retry_max_attempts.to_string(), t.materialized.to_string(), t.created_at.to_rfc3339(), ]); } table.print(mode); Ok(()) } /// `pic triggers ls --group ` — read-only view of a group's §11 tail trigger /// TEMPLATES (event kinds that fan out live to descendant apps). Authoring is /// declarative via the `[group]` manifest `[[triggers.*]]`. pub async fn ls_group(group: &str, mode: OutputMode) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let rows = client.group_triggers_list(group).await?; let mut table = Table::new(["kind", "target", "script", "enabled", "sealed", "shared"]); for t in rows { table.row([ t.kind, t.target, t.script, t.enabled.to_string(), t.sealed.to_string(), t.shared.to_string(), ]); } table.print(mode); Ok(()) } pub async fn rm(app: &str, trigger_id: &str) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; client.triggers_delete(app, trigger_id).await?; println!("Deleted trigger {trigger_id}"); Ok(()) } /// Common create body shape — every kind shares `script_id`, /// `dispatch_mode`, and the retry knobs. Per-kind fields layer on /// top via `serde_json::json!`. fn base_body(script_id: &str, dispatch: &str) -> serde_json::Value { json!({ "script_id": script_id, "dispatch_mode": dispatch, }) } pub async fn create_kv( app: &str, script_id: &str, collection_glob: &str, ops: &[String], dispatch: &str, mode: OutputMode, ) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let mut body = base_body(script_id, dispatch); body["collection_glob"] = json!(collection_glob); if !ops.is_empty() { body["ops"] = json!(ops); } let created = client.triggers_create(app, "kv", &body).await?; print_created(&created, mode); Ok(()) } pub async fn create_cron( app: &str, script_id: &str, schedule: &str, timezone: Option<&str>, dispatch: &str, mode: OutputMode, ) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let mut body = base_body(script_id, dispatch); body["schedule"] = json!(schedule); if let Some(tz) = timezone { body["timezone"] = json!(tz); } let created = client.triggers_create(app, "cron", &body).await?; print_created(&created, mode); Ok(()) } pub async fn create_dead_letter( app: &str, script_id: &str, source_filter: Option<&str>, trigger_id_filter: Option<&str>, script_id_filter: Option<&str>, dispatch: &str, mode: OutputMode, ) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let mut body = base_body(script_id, dispatch); if let Some(s) = source_filter { body["source_filter"] = json!(s); } if let Some(t) = trigger_id_filter { body["trigger_id_filter"] = json!(t); } if let Some(s) = script_id_filter { body["script_id_filter"] = json!(s); } let created = client.triggers_create(app, "dead_letter", &body).await?; print_created(&created, mode); Ok(()) } /// docs/files share KV's `{collection_glob, ops?}` shape. async fn create_collection_trigger( kind: &str, app: &str, script_id: &str, collection_glob: &str, ops: &[String], dispatch: &str, mode: OutputMode, ) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let mut body = base_body(script_id, dispatch); body["collection_glob"] = json!(collection_glob); if !ops.is_empty() { body["ops"] = json!(ops); } let created = client.triggers_create(app, kind, &body).await?; print_created(&created, mode); Ok(()) } pub async fn create_docs( app: &str, script_id: &str, collection_glob: &str, ops: &[String], dispatch: &str, mode: OutputMode, ) -> Result<()> { create_collection_trigger("docs", app, script_id, collection_glob, ops, dispatch, mode).await } pub async fn create_files( app: &str, script_id: &str, collection_glob: &str, ops: &[String], dispatch: &str, mode: OutputMode, ) -> Result<()> { create_collection_trigger( "files", app, script_id, collection_glob, ops, dispatch, mode, ) .await } pub async fn create_pubsub( app: &str, script_id: &str, topic_pattern: &str, dispatch: &str, mode: OutputMode, ) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let mut body = base_body(script_id, dispatch); body["topic_pattern"] = json!(topic_pattern); let created = client.triggers_create(app, "pubsub", &body).await?; print_created(&created, mode); Ok(()) } pub async fn create_queue( app: &str, script_id: &str, queue_name: &str, visibility_timeout_secs: Option, dispatch: &str, mode: OutputMode, ) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let mut body = base_body(script_id, dispatch); body["queue_name"] = json!(queue_name); if let Some(v) = visibility_timeout_secs { body["visibility_timeout_secs"] = json!(v); } let created = client.triggers_create(app, "queue", &body).await?; print_created(&created, mode); Ok(()) } pub async fn create_email( app: &str, script_id: &str, inbound_secret: Option<&str>, mode: OutputMode, ) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; // Email triggers take no dispatch_mode (inbound webhook only) and an // optional shared HMAC secret the provider signs POSTs with. let mut body = json!({ "script_id": script_id }); if let Some(secret) = inbound_secret { body["inbound_secret"] = json!(secret); } let created = client.triggers_create(app, "email", &body).await?; print_created(&created, mode); Ok(()) } pub async fn create_from_json(app: &str, kind: &str, body: &str, mode: OutputMode) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let body_json: serde_json::Value = if body == "-" { let mut s = String::new(); std::io::Read::read_to_string(&mut std::io::stdin(), &mut s) .context("read body JSON from stdin")?; serde_json::from_str(&s).map_err(|e| anyhow!("parse JSON from stdin: {e}"))? } else if let Some(path) = body.strip_prefix('@') { let raw = std::fs::read_to_string(path).with_context(|| format!("read JSON body from {path}"))?; serde_json::from_str(&raw).map_err(|e| anyhow!("parse JSON body in {path}: {e}"))? } else { serde_json::from_str(body).map_err(|e| anyhow!("parse inline JSON body: {e}"))? }; let created = client.triggers_create(app, kind, &body_json).await?; print_created(&created, mode); Ok(()) } fn print_created(t: &crate::client::TriggerDto, mode: OutputMode) { let mut block = KvBlock::new(); block .field("id", t.id.clone()) .field("kind", t.kind.clone()) .field("script_id", t.script_id.to_string()) .field("enabled", t.enabled.to_string()) .field("dispatch_mode", t.dispatch_mode.clone()) .field("retry_max_attempts", t.retry_max_attempts.to_string()) .field("created_at", t.created_at.to_rfc3339()); block.print(mode); }