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