Skip to main content

ironflow_store/memory/
mod.rs

1//! In-memory [`Store`](crate::store::Store) implementation for development and testing.
2//!
3//! [`InMemoryStore`] uses `Arc<RwLock<..>>` internally, making it safe to share
4//! across tasks. Data is lost when the process exits.
5//!
6//! # Examples
7//!
8//! ```no_run
9//! use std::collections::HashMap;
10//! use ironflow_store::prelude::*;
11//! use serde_json::json;
12//!
13//! # async fn example() -> Result<(), ironflow_store::error::StoreError> {
14//! let store = InMemoryStore::new();
15//!
16//! let run = store.create_run(NewRun {
17//!     workflow_name: "test".to_string(),
18//!     trigger: TriggerKind::Manual,
19//!     payload: json!({}),
20//!     max_retries: 3,
21//!     handler_version: None,
22//!     labels: HashMap::new(),
23//!     scheduled_at: None,
24//!     created_by: None,
25//!     idempotency_key: None,
26//!     max_cost_usd: None,
27//! }).await?.into_run();
28//!
29//! assert_eq!(run.status.state, RunStatus::Pending);
30//! # Ok(())
31//! # }
32//! ```
33
34use std::collections::HashMap;
35use std::sync::Arc;
36
37use tokio::sync::RwLock;
38use uuid::Uuid;
39
40use crate::entities::User;
41
42mod api_key_store;
43mod artifact_store;
44mod audit_log_store;
45mod log_store;
46mod run_store;
47mod schedule_store;
48mod secret_store;
49mod user_store;
50
51#[derive(Debug, Default)]
52pub(super) struct State {
53    pub(super) runs: HashMap<Uuid, crate::entities::Run>,
54    /// Idempotency key -> run holding it. Guarded by the same lock as `runs`,
55    /// so check-then-insert is atomic.
56    pub(super) idempotency_keys: HashMap<String, Uuid>,
57    pub(super) steps: HashMap<Uuid, crate::entities::Step>,
58    pub(super) step_dependencies: Vec<crate::entities::StepDependency>,
59    pub(super) artifacts: HashMap<Uuid, crate::entities::Artifact>,
60    pub(super) users: HashMap<Uuid, User>,
61    pub(super) api_keys: HashMap<Uuid, crate::entities::ApiKey>,
62    pub(super) secrets: HashMap<String, EncryptedSecret>,
63    pub(super) schedules: HashMap<Uuid, crate::entities::Schedule>,
64    pub(super) audit_logs: Vec<crate::entities::AuditLogEntry>,
65    pub(super) log_entries: Vec<crate::entities::LogEntry>,
66}
67
68#[derive(Debug, Clone)]
69pub(super) struct EncryptedSecret {
70    pub(super) id: Uuid,
71    pub(super) key: String,
72    #[cfg(feature = "secret-store")]
73    pub(super) encrypted_value: Vec<u8>,
74    #[cfg(feature = "secret-store")]
75    pub(super) nonce: Vec<u8>,
76    #[cfg(feature = "secret-store")]
77    pub(super) key_version: i32,
78    pub(super) created_at: chrono::DateTime<chrono::Utc>,
79    pub(super) updated_at: chrono::DateTime<chrono::Utc>,
80}
81
82/// In-memory store backed by `Arc<RwLock<..>>`.
83///
84/// Thread-safe and cheap to clone. All data is held in memory and lost on drop.
85/// Implements [`Store`](crate::store::Store) so a single `Arc<InMemoryStore>`
86/// covers runs, users, API keys, and secrets.
87///
88/// # Examples
89///
90/// ```
91/// use ironflow_store::memory::InMemoryStore;
92///
93/// let store = InMemoryStore::new();
94/// let store2 = store.clone(); // cheap Arc clone
95/// ```
96#[derive(Debug, Clone)]
97pub struct InMemoryStore {
98    pub(super) state: Arc<RwLock<State>>,
99    #[cfg(feature = "secret-store")]
100    pub(super) key_ring: Option<Arc<crate::crypto::KeyRing>>,
101}
102
103impl InMemoryStore {
104    /// Create a new empty in-memory store.
105    ///
106    /// # Examples
107    ///
108    /// ```
109    /// use ironflow_store::memory::InMemoryStore;
110    ///
111    /// let store = InMemoryStore::new();
112    /// ```
113    pub fn new() -> Self {
114        Self {
115            state: Arc::new(RwLock::new(State::default())),
116            #[cfg(feature = "secret-store")]
117            key_ring: None,
118        }
119    }
120
121    /// Set a single, unversioned master key for secret encryption.
122    ///
123    /// Shorthand for a key ring holding this key alone at
124    /// [`LEGACY_KEY_VERSION`](crate::crypto::LEGACY_KEY_VERSION).
125    ///
126    /// Required before using [`SecretStore`](crate::secret_store::SecretStore)
127    /// methods that read/write secret values. Without a key, those methods
128    /// return [`StoreError::Crypto`](crate::error::StoreError::Crypto).
129    ///
130    /// Listing and deleting secrets works without a key.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use ironflow_store::memory::InMemoryStore;
136    /// use ironflow_store::crypto::MasterKey;
137    ///
138    /// # fn example() -> Result<(), ironflow_store::crypto::CryptoError> {
139    /// let mut store = InMemoryStore::new();
140    /// let key = MasterKey::from_hex(
141    ///     "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
142    /// )?;
143    /// store.set_master_key(key);
144    /// # Ok(())
145    /// # }
146    /// ```
147    #[cfg(feature = "secret-store")]
148    pub fn set_master_key(&mut self, key: crate::crypto::MasterKey) {
149        self.set_key_ring(crate::crypto::KeyRing::single(key));
150    }
151
152    /// Set the versioned key ring for secret encryption.
153    ///
154    /// New secrets are encrypted with the ring's active version; existing ones
155    /// are decrypted with whichever version they were written with.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use ironflow_store::memory::InMemoryStore;
161    /// use ironflow_store::crypto::KeyRing;
162    ///
163    /// # fn example() -> Result<(), ironflow_store::crypto::CryptoError> {
164    /// let mut store = InMemoryStore::new();
165    /// let spec = format!("1:{},2:{}", "aa".repeat(32), "bb".repeat(32));
166    /// store.set_key_ring(KeyRing::from_spec(&spec, Some(2))?);
167    /// # Ok(())
168    /// # }
169    /// ```
170    #[cfg(feature = "secret-store")]
171    pub fn set_key_ring(&mut self, ring: crate::crypto::KeyRing) {
172        self.key_ring = Some(Arc::new(ring));
173    }
174
175    /// Override a run's `created_at` timestamp for testing retention policies.
176    ///
177    /// # Examples
178    ///
179    /// ```no_run
180    /// use chrono::{Utc, TimeDelta};
181    /// use ironflow_store::memory::InMemoryStore;
182    /// use uuid::Uuid;
183    ///
184    /// # async fn example(store: &InMemoryStore, run_id: Uuid) {
185    /// let old = Utc::now() - TimeDelta::days(100);
186    /// store.set_run_created_at(run_id, old).await;
187    /// # }
188    /// ```
189    pub async fn set_run_created_at(
190        &self,
191        run_id: Uuid,
192        created_at: chrono::DateTime<chrono::Utc>,
193    ) {
194        let mut state = self.state.write().await;
195        if let Some(run) = state.runs.get_mut(&run_id) {
196            run.created_at = created_at;
197        }
198    }
199}
200
201impl Default for InMemoryStore {
202    fn default() -> Self {
203        Self::new()
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use std::collections::HashMap;
210
211    use serde_json::json;
212
213    use crate::entities::{NewRun, TriggerKind};
214
215    use super::InMemoryStore;
216
217    pub(crate) fn new_run_req(name: &str) -> NewRun {
218        NewRun {
219            created_by: None,
220            workflow_name: name.to_string(),
221            trigger: TriggerKind::Manual,
222            payload: json!({}),
223            max_retries: 3,
224            handler_version: None,
225            labels: HashMap::new(),
226            scheduled_at: None,
227            idempotency_key: None,
228            max_cost_usd: None,
229        }
230    }
231
232    pub(crate) async fn create_terminal_run(
233        store: &InMemoryStore,
234        name: &str,
235        status: crate::entities::RunStatus,
236    ) -> crate::entities::Run {
237        use crate::store::RunStore;
238
239        let run = store
240            .create_run(new_run_req(name))
241            .await
242            .unwrap()
243            .into_run();
244        store
245            .update_run_status(run.id, crate::entities::RunStatus::Running)
246            .await
247            .unwrap();
248        store.update_run_status(run.id, status).await.unwrap();
249        store.get_run(run.id).await.unwrap().unwrap()
250    }
251}