//! `TriggerEvent` — the description of the event that fired a script. //! //! Built by the dispatcher (in `manager-core`) from the outbox row and //! attached to the `ExecRequest` that's handed to `executor-core`. The //! Rhai bridge in `executor-core::engine::build_ctx_map` flattens this //! into `ctx.event` for the script. //! //! Living in `picloud-shared` so the dispatcher and the executor agree //! on the wire shape. Serializable so cluster mode (v1.3+) can ship //! ExecRequests over HTTP without rewriting this type. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::{DeadLetterId, ScriptId, TriggerId}; /// Operations a KV trigger can fire on. Stored as a lowercase string /// in `kv_trigger_details.ops` (Postgres `text[]`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum KvEventOp { Insert, Update, Delete, } impl KvEventOp { #[must_use] pub const fn as_str(self) -> &'static str { match self { Self::Insert => "insert", Self::Update => "update", Self::Delete => "delete", } } #[must_use] pub fn from_wire(s: &str) -> Option { match s { "insert" => Some(Self::Insert), "update" => Some(Self::Update), "delete" => Some(Self::Delete), _ => None, } } } /// Discriminated description of a triggering event. Lifted from the /// outbox row's payload at dispatch time. Each variant carries the /// fields the corresponding `ctx.event` shape exposes to the script. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "source", rename_all = "snake_case")] pub enum TriggerEvent { /// A KV insert / update / delete fired this handler. Kv { op: KvEventOp, collection: String, key: String, /// Present on `insert` and `update`. Absent on `delete`. #[serde(default, skip_serializing_if = "Option::is_none")] value: Option, }, /// A dead-letter row fired this handler. The original event is /// nested verbatim plus the dead-letter metadata the design notes /// §4 require. DeadLetter { dead_letter_id: DeadLetterId, original: Box, attempts: u32, last_error: String, #[serde(default, skip_serializing_if = "Option::is_none")] trigger_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] script_id: Option, first_attempt_at: DateTime, last_attempt_at: DateTime, }, } impl TriggerEvent { /// The `source` discriminant the script sees on `ctx.event.source`. #[must_use] pub const fn source(&self) -> &'static str { match self { Self::Kv { .. } => "kv", Self::DeadLetter { .. } => "dead_letter", } } } /// Convenience accessor on the dead-letter variant for places that /// already know they're handling a DL event. Pulled out so the /// dispatcher and the dashboard don't have to repeat the match. #[derive(Debug, Clone)] pub struct DeadLetterEventDetail { pub dead_letter_id: DeadLetterId, pub original: TriggerEvent, pub attempts: u32, pub last_error: String, pub trigger_id: Option, pub script_id: Option, pub first_attempt_at: DateTime, pub last_attempt_at: DateTime, }