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