//! `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, } } } /// Operations a docs trigger can fire on. v1.1.2. Stored as a /// lowercase string in `docs_trigger_details.ops` (Postgres `text[]`). /// Distinct from `KvEventOp` because docs has CRUD verbs (`create`) /// instead of KV's set/upsert flavour (`insert`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum DocsEventOp { Create, Update, Delete, } impl DocsEventOp { #[must_use] pub const fn as_str(self) -> &'static str { match self { Self::Create => "create", Self::Update => "update", Self::Delete => "delete", } } #[must_use] pub fn from_wire(s: &str) -> Option { match s { "create" => Some(Self::Create), "update" => Some(Self::Update), "delete" => Some(Self::Delete), _ => None, } } } /// Operations a files trigger can fire on. v1.1.5. Stored as a /// lowercase string in `files_trigger_details.ops` (Postgres `text[]`). /// CRUD verbs (`create`) mirror `DocsEventOp`, distinct from KV's /// set/upsert flavour. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum FilesEventOp { Create, Update, Delete, } impl FilesEventOp { #[must_use] pub const fn as_str(self) -> &'static str { match self { Self::Create => "create", Self::Update => "update", Self::Delete => "delete", } } #[must_use] pub fn from_wire(s: &str) -> Option { match s { "create" => Some(Self::Create), "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 docs create / update / delete fired this handler. v1.1.2. /// `data` is the current document state (absent on delete); /// `prev_data` is the prior state (absent on create). For update /// and delete handlers, `prev_data` is the load-bearing /// change-data-capture surface (the repo reads the old row in the /// same statement as the write). Docs { op: DocsEventOp, collection: String, /// UUID as string — Rhai sees it as a string. id: String, #[serde(default, skip_serializing_if = "Option::is_none")] data: Option, #[serde(default, skip_serializing_if = "Option::is_none")] prev_data: Option, }, /// A cron schedule fired this handler. v1.1.4. Carries the /// schedule + timezone the trigger was configured with, the /// canonical cron moment (`scheduled_at`, the instant the /// expression *meant*), and when the scheduler actually enqueued /// the fire (`fired_at`). Surfaced to scripts as `ctx.event.cron`. Cron { schedule: String, timezone: String, scheduled_at: DateTime, fired_at: DateTime, }, /// A files create / update / delete fired this handler. v1.1.5. /// Carries the affected file's **metadata only** — never the blob /// bytes (files are too big to ship through trigger payloads). A /// handler that wants the bytes calls /// `files::collection(c).get(id)` itself. `prev` is the prior /// metadata for update (and the deleted-row metadata for delete); /// absent on create. Surfaced to scripts as `ctx.event.files`. Files { op: FilesEventOp, collection: String, /// UUID as string — Rhai sees it as a string. id: String, name: String, content_type: String, size: u64, /// Lowercase hex SHA-256. checksum: String, #[serde(default, skip_serializing_if = "Option::is_none")] prev: Option, }, /// A durable pub/sub publish fired this handler. v1.1.5. Carries /// the topic, the JSON-decoded message, and the publish instant. /// Surfaced to scripts as `ctx.event.pubsub`. Pubsub { topic: String, message: serde_json::Value, published_at: DateTime, }, /// 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::Docs { .. } => "docs", Self::Cron { .. } => "cron", Self::Files { .. } => "files", Self::Pubsub { .. } => "pubsub", 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, }