Skip to main content

ironflow_store/entities/
run.rs

1//! [`Run`] entity and related request/update types.
2
3use chrono::{DateTime, Utc};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use uuid::Uuid;
8
9use super::{FsmState, RunStatus, TriggerKind};
10
11/// A workflow execution record.
12///
13/// Represents a single invocation of a workflow, tracking its status through
14/// the [`RunStatus`] FSM (SQL-side via [`lib_fsm`](crate::postgres::helpers::lib_fsm)),
15/// aggregated metrics, and timestamps.
16///
17/// # Examples
18///
19/// ```
20/// use ironflow_store::entities::Run;
21///
22/// // Runs are created by RunStore::create_run, not directly.
23/// ```
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[non_exhaustive]
26pub struct Run {
27    /// Unique identifier (UUIDv7, sortable by creation time).
28    pub id: Uuid,
29    /// Name of the workflow that was executed.
30    pub workflow_name: String,
31    /// Current FSM status — embeds state + state_machine_id for SQL-side transitions.
32    pub status: FsmState<RunStatus>,
33    /// How this run was triggered.
34    pub trigger: TriggerKind,
35    /// Trigger-specific payload (e.g. webhook body).
36    pub payload: Value,
37    /// Error message if the run failed.
38    pub error: Option<String>,
39    /// Number of times this run has been retried.
40    pub retry_count: u32,
41    /// Maximum number of retries allowed.
42    pub max_retries: u32,
43    /// Aggregated cost across all agent steps, in USD.
44    pub cost_usd: Decimal,
45    /// Aggregated wall-clock duration across all steps, in milliseconds.
46    pub duration_ms: u64,
47    /// When the run was created (enqueued).
48    pub created_at: DateTime<Utc>,
49    /// When the run record was last updated.
50    pub updated_at: DateTime<Utc>,
51    /// When execution started (transitioned to Running).
52    pub started_at: Option<DateTime<Utc>>,
53    /// When execution finished (transitioned to a terminal state).
54    pub completed_at: Option<DateTime<Utc>>,
55}
56
57/// Request to create a new run.
58///
59/// # Examples
60///
61/// ```
62/// use ironflow_store::entities::{NewRun, TriggerKind};
63/// use serde_json::json;
64///
65/// let req = NewRun {
66///     workflow_name: "deploy".to_string(),
67///     trigger: TriggerKind::Manual,
68///     payload: json!({}),
69///     max_retries: 3,
70/// };
71/// ```
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct NewRun {
74    /// Workflow name.
75    pub workflow_name: String,
76    /// How the run was triggered.
77    pub trigger: TriggerKind,
78    /// Trigger-specific payload.
79    pub payload: Value,
80    /// Maximum retry attempts.
81    pub max_retries: u32,
82}
83
84/// Filters for listing runs.
85///
86/// All fields are optional; `None` means "no filter" for that field.
87///
88/// # Examples
89///
90/// ```
91/// use ironflow_store::entities::{RunFilter, RunStatus};
92///
93/// let filter = RunFilter {
94///     workflow_name: Some("deploy".to_string()),
95///     status: Some(RunStatus::Completed),
96///     ..RunFilter::default()
97/// };
98/// ```
99#[derive(Debug, Clone, Default)]
100pub struct RunFilter {
101    /// Filter by workflow name (exact match).
102    pub workflow_name: Option<String>,
103    /// Filter by run status.
104    pub status: Option<RunStatus>,
105    /// Only include runs created after this timestamp.
106    pub created_after: Option<DateTime<Utc>>,
107    /// Only include runs created before this timestamp.
108    pub created_before: Option<DateTime<Utc>>,
109}
110
111/// Partial update for a run.
112///
113/// # Examples
114///
115/// ```
116/// use ironflow_store::entities::{RunUpdate, RunStatus};
117///
118/// let update = RunUpdate {
119///     status: Some(RunStatus::Completed),
120///     ..RunUpdate::default()
121/// };
122/// ```
123#[derive(Debug, Clone, Default, Serialize, Deserialize)]
124pub struct RunUpdate {
125    /// New status.
126    pub status: Option<RunStatus>,
127    /// Error message.
128    pub error: Option<String>,
129    /// Increment retry count.
130    pub increment_retry: bool,
131    /// Aggregated cost.
132    pub cost_usd: Option<Decimal>,
133    /// Aggregated duration.
134    pub duration_ms: Option<u64>,
135    /// When execution started.
136    pub started_at: Option<DateTime<Utc>>,
137    /// When execution completed.
138    pub completed_at: Option<DateTime<Utc>>,
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use serde_json::json;
145
146    #[test]
147    fn newrun_serde_roundtrip() {
148        let new_run = NewRun {
149            workflow_name: "deploy".to_string(),
150            trigger: TriggerKind::Manual,
151            payload: json!({"key": "value"}),
152            max_retries: 3,
153        };
154
155        let json = serde_json::to_string(&new_run).expect("serialize");
156        let back: NewRun = serde_json::from_str(&json).expect("deserialize");
157        assert_eq!(back.workflow_name, new_run.workflow_name);
158        assert_eq!(back.trigger, new_run.trigger);
159        assert_eq!(back.payload, new_run.payload);
160        assert_eq!(back.max_retries, new_run.max_retries);
161    }
162
163    #[test]
164    fn run_serde_preserves_all_fields() {
165        use crate::entities::FsmState;
166        use chrono::Utc;
167        use uuid::Uuid;
168
169        let now = Utc::now();
170        let run = Run {
171            id: Uuid::now_v7(),
172            workflow_name: "test-wf".to_string(),
173            status: FsmState::new(RunStatus::Running, Uuid::now_v7()),
174            trigger: TriggerKind::Webhook {
175                path: "/hooks/test".to_string(),
176            },
177            payload: json!({"data": 123}),
178            error: Some("test error".to_string()),
179            retry_count: 2,
180            max_retries: 5,
181            cost_usd: Decimal::new(1234, 2),
182            duration_ms: 5000,
183            created_at: now,
184            updated_at: now,
185            started_at: Some(now),
186            completed_at: Some(now),
187        };
188
189        let json = serde_json::to_string(&run).expect("serialize");
190        let back: Run = serde_json::from_str(&json).expect("deserialize");
191
192        assert_eq!(back.id, run.id);
193        assert_eq!(back.workflow_name, run.workflow_name);
194        assert_eq!(back.status.state, run.status.state);
195        assert_eq!(back.trigger, run.trigger);
196        assert_eq!(back.payload, run.payload);
197        assert_eq!(back.error, run.error);
198        assert_eq!(back.retry_count, run.retry_count);
199        assert_eq!(back.max_retries, run.max_retries);
200        assert_eq!(back.cost_usd, run.cost_usd);
201        assert_eq!(back.duration_ms, run.duration_ms);
202        assert_eq!(back.started_at, run.started_at);
203        assert_eq!(back.completed_at, run.completed_at);
204    }
205
206    #[test]
207    fn runupdate_serde_roundtrip() {
208        let update = RunUpdate {
209            status: Some(RunStatus::Completed),
210            error: Some("test error".to_string()),
211            increment_retry: true,
212            cost_usd: Some(Decimal::new(5000, 2)),
213            duration_ms: Some(3000),
214            started_at: None,
215            completed_at: None,
216        };
217
218        let json = serde_json::to_string(&update).expect("serialize");
219        let back: RunUpdate = serde_json::from_str(&json).expect("deserialize");
220
221        assert_eq!(back.status, update.status);
222        assert_eq!(back.error, update.error);
223        assert_eq!(back.increment_retry, update.increment_retry);
224        assert_eq!(back.cost_usd, update.cost_usd);
225        assert_eq!(back.duration_ms, update.duration_ms);
226    }
227
228    #[test]
229    fn runfilter_default_is_no_filters() {
230        let filter = RunFilter::default();
231        assert!(filter.workflow_name.is_none());
232        assert!(filter.status.is_none());
233        assert!(filter.created_after.is_none());
234        assert!(filter.created_before.is_none());
235    }
236
237    #[test]
238    fn runfilter_with_multiple_criteria() {
239        let filter = RunFilter {
240            workflow_name: Some("deploy".to_string()),
241            status: Some(RunStatus::Running),
242            ..RunFilter::default()
243        };
244
245        assert_eq!(filter.workflow_name, Some("deploy".to_string()));
246        assert_eq!(filter.status, Some(RunStatus::Running));
247        assert!(filter.created_after.is_none());
248        assert!(filter.created_before.is_none());
249    }
250}