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}