SQLite-backed Key / Value Store
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: environments now working

You can set specific environments that store the KV pairs under a
specific name.
That way, when you list them all / search, all you get is those in your
environment.
Should declutter the list view

+141 -49
+1
.gitignore
··· 1 1 /target 2 2 safir/target 3 3 safir-mem/target 4 + *.json
+2 -1
migrations/20240620173629_create_db.sql
··· 1 1 -- Add migration script here 2 2 create table if not exists safir ( 3 3 key text not null primary key, 4 - value text not null 4 + value text not null, 5 + environment text not null 5 6 ); 6 7 7 8 create index if not exists idx_key on safir(key);
+7
src/cli.rs
··· 63 63 64 64 /// Purges the .safirstore directory, removing it and its contents 65 65 Purge, 66 + 67 + /// Use / create an environment to store key / value pairs 68 + Use { 69 + /// Name of the environment to use / create 70 + #[arg(default_value_t = String::from("default"))] 71 + environment: String, 72 + }, 66 73 }
+6
src/main.rs
··· 41 41 cfg.mode 42 42 ); 43 43 } 44 + Commands::Use { environment } => { 45 + let mut cfg = safir.get_config(); 46 + cfg.environment = environment.clone(); 47 + cfg.write().context("writing config out")?; 48 + println!("Using environment '{}'", environment); 49 + } 44 50 } 45 51 46 52 Ok(())
+3
src/store/config.rs
··· 17 17 #[serde(skip)] 18 18 pub filepath: PathBuf, 19 19 20 + pub environment: String, 21 + 20 22 pub mode: SafirMode, 21 23 } 22 24 ··· 26 28 if !fp.exists() { 27 29 let cfg = Self { 28 30 filepath: fp, 31 + environment: "default".to_string(), 29 32 mode: SafirMode::File, 30 33 }; 31 34 cfg.write().context("writing config out")?;
+22 -5
src/store/db_store.rs
··· 49 49 #[async_trait] 50 50 impl SafirStore for SqliteStore { 51 51 async fn add(&mut self, key: String, value: String) -> Result<()> { 52 - sqlx::query("insert into safir(key, value) values(?1, ?2)") 52 + sqlx::query("insert into safir(key, value, environment) values(?1, ?2, ?3)") 53 53 .bind(&key) 54 54 .bind(&value) 55 + .bind(&self.config.environment) 55 56 .execute(&self.pool) 56 57 .await 57 58 .with_context(|| format!("insert {key} - {value} into database"))?; ··· 65 66 .map(|k| format!("'{k}'")) 66 67 .collect::<Vec<String>>(); 67 68 68 - let query = format!("select * from safir where key in ({})", keys.join(", ")); 69 + let query = format!( 70 + "select * from safir where environment = '{}' and key in ({})", 71 + &self.config.environment, 72 + keys.join(", ") 73 + ); 69 74 let results: Vec<KVPair> = sqlx::query_as::<_, KVPair>(&query) 70 75 .fetch_all(&self.pool) 71 76 .await?; ··· 74 79 } 75 80 76 81 async fn list(&self) -> Result<Vec<KVPair>> { 77 - let results: Vec<KVPair> = sqlx::query_as::<_, KVPair>("select * from safir") 82 + let query = format!( 83 + "select * from safir where environment = '{}'", 84 + self.config.environment 85 + ); 86 + let results: Vec<KVPair> = sqlx::query_as::<_, KVPair>(&query) 78 87 .fetch_all(&self.pool) 79 88 .await?; 80 89 ··· 87 96 .map(|k| format!("'{k}'")) 88 97 .collect::<Vec<String>>(); 89 98 90 - let query = format!("delete from safir where key in ({})", keys.join(", ")); 99 + let query = format!( 100 + "delete from safir where environment = '{}' and key in ({})", 101 + self.config.environment, 102 + keys.join(", ") 103 + ); 91 104 let _ = sqlx::query_as::<_, KVPair>(&query) 92 105 .fetch_all(&self.pool) 93 106 .await?; ··· 97 110 98 111 async fn clear(&mut self) -> Result<()> { 99 112 if confirm_entry("Are you sure you want to clear the safirstore?") { 100 - let _ = sqlx::query_as::<_, KVPair>("delete from safir") 113 + let query = format!( 114 + "delete from safir where environment = '{}'", 115 + self.config.environment 116 + ); 117 + let _ = sqlx::query_as::<_, KVPair>(&query) 101 118 .fetch_all(&self.pool) 102 119 .await?; 103 120 }
+100 -23
src/store/file_store.rs
··· 5 5 6 6 use anyhow::Result; 7 7 use async_trait::async_trait; 8 + use serde_json::Value; 8 9 9 - use std::{collections::HashMap, path::PathBuf}; 10 + use std::{ 11 + collections::HashMap, 12 + path::{Path, PathBuf}, 13 + }; 10 14 11 15 #[derive(Debug, Clone)] 12 16 pub struct KVStore { 13 17 loc: PathBuf, 14 - store: HashMap<String, String>, 18 + environment: String, 19 + store: HashMap<String, HashMap<String, String>>, 15 20 config: SafirConfig, 16 21 } 17 22 18 23 impl KVStore { 19 24 pub fn load(ws: PathBuf, config: SafirConfig) -> Self { 20 25 let store_path = ws.join("safirstore.json"); 21 - let store = if store_path.exists() { 22 - utils::load_store(&store_path) 26 + let safir = if store_path.exists() { 27 + let store = Self::load_store(&store_path, &config); 28 + Self { 29 + loc: store_path, 30 + environment: config.environment.clone(), 31 + config, 32 + store, 33 + } 23 34 } else { 24 - let store = HashMap::new(); 25 - utils::write_store(&store, &store_path); 26 - store 35 + let mut store = HashMap::new(); 36 + store.insert(config.environment.clone(), HashMap::new()); 37 + 38 + Self { 39 + loc: store_path, 40 + store, 41 + environment: config.environment.clone(), 42 + config, 43 + } 27 44 }; 28 45 29 - Self { 30 - loc: store_path, 31 - config, 32 - store, 46 + safir.write_store(); 47 + safir 48 + } 49 + 50 + /// Loads the store from disk 51 + /// This is stupid having to reload the map but it will allow users that had 52 + /// the old format to port over to the new format seamlessly 53 + pub fn load_store( 54 + path: impl AsRef<Path>, 55 + config: &SafirConfig, 56 + ) -> HashMap<String, HashMap<String, String>> { 57 + let contents = std::fs::read_to_string(path.as_ref()).expect("unable to store contents"); 58 + 59 + let store: HashMap<String, Value> = 60 + serde_json::from_str(&contents).expect("unable to deserialize contents"); 61 + 62 + // If they're all objects then this is the new format, load appropriately 63 + if store.values().all(|v| v.is_object()) { 64 + return serde_json::from_str::<HashMap<String, HashMap<String, String>>>(&contents) 65 + .unwrap(); 66 + } 67 + 68 + // Old map (no environment) - Load and create new environment 69 + let old_map = serde_json::from_str::<HashMap<String, String>>(&contents).unwrap(); 70 + let mut new_map = HashMap::new(); 71 + new_map.insert(config.environment.clone(), HashMap::new()); 72 + 73 + let env = new_map.get_mut(config.environment.as_str()).unwrap(); 74 + for (key, value) in old_map.into_iter() { 75 + env.insert(key, value); 33 76 } 77 + 78 + new_map 79 + } 80 + 81 + /// Writes the store to disk 82 + pub fn write_store(&self) { 83 + use std::io::Write; 84 + let str_store = 85 + serde_json::to_string_pretty(&self.store).expect("unable to serialize store contents"); 86 + 87 + let mut file = std::fs::File::create(&self.loc).expect("unable to get file handle"); 88 + 89 + file.write_all(str_store.as_bytes()) 90 + .expect("unable to write store out to disk"); 91 + } 92 + 93 + pub fn get_environment(&mut self) -> &mut HashMap<String, String> { 94 + self.store 95 + .entry(self.environment.clone()) 96 + .or_insert(HashMap::new()); 97 + 98 + self.store.get_mut(&self.environment).unwrap() 34 99 } 35 100 } 36 101 37 102 #[async_trait] 38 103 impl SafirStore for KVStore { 39 104 async fn add(&mut self, key: String, value: String) -> Result<()> { 40 - if let Some(v) = self.store.get(&key) { 105 + let env = self.get_environment(); 106 + if let Some(v) = env.get(&key) { 41 107 let confirm_msg = format!("Key {key} already exists ({v}), Replace?"); 42 108 if utils::confirm_entry(&confirm_msg) { 43 - self.store.insert(key, value); 109 + env.insert(key, value); 44 110 } 45 111 } else { 46 - self.store.insert(key, value); 112 + env.insert(key, value); 47 113 } 48 114 49 - utils::write_store(&self.store, &self.loc); 115 + self.write_store(); 50 116 51 117 Ok(()) 52 118 } 53 119 54 120 async fn get(&self, keys: Vec<String>) -> Result<Vec<KVPair>> { 121 + let inner = match self.store.get(&self.environment) { 122 + Some(inner) => inner, 123 + None => return Ok(vec![]), 124 + }; 125 + 55 126 let kvs: Vec<KVPair> = keys 56 127 .into_iter() 57 - .filter_map(|key| match self.store.get(&key) { 128 + .filter_map(|key| match inner.get(&key) { 58 129 Some(value) => Some((key, value.clone())), 59 130 None => None, 60 131 }) ··· 64 135 } 65 136 66 137 async fn list(&self) -> Result<Vec<KVPair>> { 67 - let kvs: Vec<KVPair> = self 68 - .store 138 + let inner = match self.store.get(&self.environment) { 139 + Some(inner) => inner, 140 + None => return Ok(vec![]), 141 + }; 142 + 143 + let kvs: Vec<KVPair> = inner 69 144 .iter() 70 145 .map(|(key, value)| (key.clone(), value.clone())) 71 146 .collect(); ··· 74 149 } 75 150 76 151 async fn remove(&mut self, keys: Vec<String>) -> Result<()> { 152 + let inner = self.get_environment(); 77 153 for key in keys.iter() { 78 - if let Some(v) = self.store.get(key) { 154 + if let Some(v) = inner.get(key) { 79 155 let confirm_msg = format!("Remove {key} ({v}) from the store?"); 80 156 if utils::confirm_entry(&confirm_msg) { 81 - self.store.remove(key); 157 + inner.remove(key); 82 158 } 83 159 } 84 160 } 85 161 86 - utils::write_store(&self.store, &self.loc); 162 + self.write_store(); 87 163 88 164 Ok(()) 89 165 } 90 166 async fn clear(&mut self) -> Result<()> { 167 + let inner = self.get_environment(); 91 168 let confirm_msg = "Are you sure you want to clear the cache of all contents?"; 92 169 if utils::confirm_entry(&confirm_msg) { 93 - self.store.clear(); 170 + inner.clear(); 94 171 } 95 172 96 - utils::write_store(&self.store, &self.loc); 173 + self.write_store(); 97 174 98 175 Ok(()) 99 176 }
-20
src/utils.rs
··· 1 1 use std::{ 2 - collections::HashMap, 3 2 fs, 4 3 io::Write, 5 4 path::{Path, PathBuf}, ··· 37 36 let (key, value) = kv; 38 37 println!("{display_cmd} {key}=\"{value}\""); 39 38 } 40 - } 41 - 42 - /// Loads the store from disk 43 - pub fn load_store(path: impl AsRef<Path>) -> HashMap<String, String> { 44 - let contents = std::fs::read_to_string(path.as_ref()).expect("unable to store contents"); 45 - 46 - return serde_json::from_str::<HashMap<String, String>>(&contents) 47 - .expect("unable to deserialize store contents"); 48 - } 49 - 50 - /// Writes the store to disk 51 - pub fn write_store(store: &HashMap<String, String>, path: impl AsRef<Path>) { 52 - let str_store = 53 - serde_json::to_string_pretty(store).expect("unable to serialize store contents"); 54 - 55 - let mut file = std::fs::File::create(&path).expect("unable to get file handle"); 56 - 57 - file.write_all(str_store.as_bytes()) 58 - .expect("unable to write store out to disk"); 59 39 } 60 40 61 41 /// Remove the .safirstore directory