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