activation.rs (7122B)
1 use radroots_app_view::{ 2 AccountSurfaceActivationProjection, ActiveSurface, FarmId, FarmerActivationProjection, 3 SelectedSurfaceProjection, 4 }; 5 use rusqlite::{Connection, OptionalExtension, params}; 6 7 use crate::AppSqliteError; 8 9 pub struct AppActivationRepository<'a> { 10 connection: &'a Connection, 11 } 12 13 impl<'a> AppActivationRepository<'a> { 14 pub const fn new(connection: &'a Connection) -> Self { 15 Self { connection } 16 } 17 18 pub fn load_surface_activation( 19 &self, 20 account_id: &str, 21 ) -> Result<Option<AccountSurfaceActivationProjection>, AppSqliteError> { 22 let row = self 23 .connection 24 .query_row( 25 "SELECT account_id, selected_surface, farmer_farm_id 26 FROM account_surface_activations 27 WHERE account_id = ?1 28 LIMIT 1", 29 [account_id], 30 |row| { 31 Ok(( 32 row.get::<_, String>(0)?, 33 row.get::<_, String>(1)?, 34 row.get::<_, Option<String>>(2)?, 35 )) 36 }, 37 ) 38 .optional() 39 .map_err(|source| AppSqliteError::Query { 40 operation: "load account surface activation", 41 source, 42 })?; 43 44 row.map(|(account_id, selected_surface, farmer_farm_id)| { 45 Ok(AccountSurfaceActivationProjection::new( 46 account_id, 47 SelectedSurfaceProjection::new(parse_active_surface( 48 "account_surface_activations.selected_surface", 49 selected_surface, 50 )?), 51 FarmerActivationProjection { 52 farm_id: parse_optional_farm_id( 53 "account_surface_activations.farmer_farm_id", 54 farmer_farm_id, 55 )?, 56 }, 57 )) 58 }) 59 .transpose() 60 } 61 62 pub fn save_surface_activation( 63 &self, 64 projection: &AccountSurfaceActivationProjection, 65 ) -> Result<(), AppSqliteError> { 66 self.connection 67 .execute( 68 "INSERT INTO account_surface_activations ( 69 account_id, 70 selected_surface, 71 farmer_farm_id, 72 updated_at 73 ) VALUES (?1, ?2, ?3, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) 74 ON CONFLICT(account_id) DO UPDATE SET 75 selected_surface = excluded.selected_surface, 76 farmer_farm_id = excluded.farmer_farm_id, 77 updated_at = excluded.updated_at", 78 params![ 79 projection.account_id, 80 projection.active_surface().storage_key(), 81 projection 82 .farmer_activation 83 .farm_id 84 .map(|farm_id| farm_id.to_string()), 85 ], 86 ) 87 .map_err(|source| AppSqliteError::Query { 88 operation: "save account surface activation", 89 source, 90 })?; 91 92 Ok(()) 93 } 94 95 pub fn clear_surface_activation(&self, account_id: &str) -> Result<(), AppSqliteError> { 96 self.connection 97 .execute( 98 "DELETE FROM account_surface_activations WHERE account_id = ?1", 99 [account_id], 100 ) 101 .map_err(|source| AppSqliteError::Query { 102 operation: "clear account surface activation", 103 source, 104 })?; 105 106 Ok(()) 107 } 108 } 109 110 fn parse_active_surface( 111 field: &'static str, 112 value: String, 113 ) -> Result<ActiveSurface, AppSqliteError> { 114 match value.as_str() { 115 "personal" => Ok(ActiveSurface::Personal), 116 "farmer" => Ok(ActiveSurface::Farmer), 117 _ => Err(AppSqliteError::DecodeEnum { field, value }), 118 } 119 } 120 121 fn parse_optional_farm_id( 122 field: &'static str, 123 value: Option<String>, 124 ) -> Result<Option<FarmId>, AppSqliteError> { 125 value 126 .map(|value| { 127 value 128 .parse() 129 .map_err(|_| AppSqliteError::DecodeId { field, value }) 130 }) 131 .transpose() 132 } 133 134 #[cfg(test)] 135 mod tests { 136 use radroots_app_view::{ 137 AccountSurfaceActivationProjection, ActiveSurface, FarmId, FarmerActivationProjection, 138 SelectedSurfaceProjection, 139 }; 140 141 use crate::{AppSqliteStore, DatabaseTarget}; 142 143 #[test] 144 fn load_surface_activation_returns_none_for_unknown_account() { 145 let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); 146 147 let projection = store 148 .load_surface_activation("acct_missing") 149 .expect("missing activation should load"); 150 151 assert_eq!(projection, None); 152 } 153 154 #[test] 155 fn surface_activation_round_trips_farmer_binding() { 156 let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); 157 let projection = AccountSurfaceActivationProjection::new( 158 "acct_farmer", 159 SelectedSurfaceProjection::new(ActiveSurface::Farmer), 160 FarmerActivationProjection::active(FarmId::new()), 161 ); 162 163 store 164 .save_surface_activation(&projection) 165 .expect("surface activation should save"); 166 167 let loaded = store 168 .load_surface_activation("acct_farmer") 169 .expect("surface activation should load") 170 .expect("surface activation should exist"); 171 172 assert_eq!(loaded, projection); 173 } 174 175 #[test] 176 fn surface_activation_upsert_and_clear_are_explicit() { 177 let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); 178 let first = AccountSurfaceActivationProjection::new( 179 "acct_surface", 180 SelectedSurfaceProjection::new(ActiveSurface::Farmer), 181 FarmerActivationProjection::active(FarmId::new()), 182 ); 183 let second = AccountSurfaceActivationProjection::new( 184 "acct_surface", 185 SelectedSurfaceProjection::new(ActiveSurface::Farmer), 186 FarmerActivationProjection::inactive(), 187 ); 188 189 store 190 .save_surface_activation(&first) 191 .expect("initial surface activation should save"); 192 store 193 .save_surface_activation(&second) 194 .expect("updated surface activation should save"); 195 196 let loaded = store 197 .load_surface_activation("acct_surface") 198 .expect("updated surface activation should load") 199 .expect("updated surface activation should exist"); 200 assert_eq!(loaded.active_surface(), ActiveSurface::Personal); 201 assert_eq!(loaded, second); 202 203 store 204 .clear_surface_activation("acct_surface") 205 .expect("surface activation should clear"); 206 assert_eq!( 207 store 208 .load_surface_activation("acct_surface") 209 .expect("cleared surface activation should load"), 210 None 211 ); 212 } 213 }