app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit e860e5033560e1dbf03430a7b31d4d7fcdc6c796
parent b0b194d8ca663be54e2f9a1d6a20a34624dec2c4
Author: triesap <tyson@radroots.org>
Date:   Mon,  2 Feb 2026 03:56:49 +0000

app: migrate legacy setup keys

- add legacy key migration for state records
- seed state from nostr/eula key entries
- clear legacy keys after successful migration
- cover migration path in datastore tests

Diffstat:
Mapp/src/bootstrap.rs | 195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 195 insertions(+), 0 deletions(-)

diff --git a/app/src/bootstrap.rs b/app/src/bootstrap.rs @@ -7,7 +7,10 @@ use crate::{ app_datastore_obj_key_state, app_datastore_obj_key_setup_draft, app_datastore_param_key, + app_datastore_key_eula_date, + app_datastore_key_nostr_key, app_log_debug_emit, + app_setup_state_new, app_state_record_new, app_state_record_validate, app_state_timestamp_ms, @@ -56,6 +59,9 @@ pub async fn app_datastore_read_state_record<T: RadrootsClientDatastore>( Ok(value) } Err(RadrootsClientDatastoreError::NoResult) => { + if let Some(record) = app_datastore_migrate_legacy_state(datastore, key_maps).await? { + return Ok(record); + } Err(RadrootsAppInitError::State(RadrootsAppStateError::Missing)) } Err(err) => Err(RadrootsAppInitError::Datastore(err)), @@ -65,6 +71,28 @@ pub async fn app_datastore_read_state_record<T: RadrootsClientDatastore>( } } +async fn app_datastore_migrate_legacy_state<T: RadrootsClientDatastore>( + datastore: &T, + key_maps: &RadrootsAppKeyMapConfig, +) -> RadrootsAppInitResult<Option<RadrootsAppStateRecord>> { + let key_nostr = app_datastore_key_nostr_key(key_maps).map_err(RadrootsAppInitError::Config)?; + let key_eula = app_datastore_key_eula_date(key_maps).map_err(RadrootsAppInitError::Config)?; + let active_key = match datastore.get(key_nostr).await { + Ok(value) => value, + Err(_) => return Ok(None), + }; + let eula_date = match datastore.get(key_eula).await { + Ok(value) => value, + Err(_) => return Ok(None), + }; + let state = app_setup_state_new(active_key.clone(), eula_date); + let record = app_state_record_new(state, 1, app_state_timestamp_ms()); + let stored = app_datastore_write_state_record(datastore, key_maps, &record).await?; + let _ = datastore.del(key_nostr).await; + let _ = datastore.del(key_eula).await; + Ok(Some(stored)) +} + pub async fn app_datastore_write_state<T: RadrootsClientDatastore>( datastore: &T, key_maps: &RadrootsAppKeyMapConfig, @@ -226,6 +254,7 @@ mod tests { app_datastore_clear_setup_draft, app_datastore_create_state, app_datastore_has_state, + app_datastore_read_state_record, app_datastore_read_state, app_datastore_read_setup_draft, app_datastore_update_state, @@ -237,6 +266,8 @@ mod tests { app_datastore_write_state, }; use crate::{ + app_datastore_key_eula_date, + app_datastore_key_nostr_key, app_key_maps_default, RadrootsAppInitError, RadrootsAppProfileSeed, @@ -260,6 +291,7 @@ mod tests { use serde::de::DeserializeOwned; use serde::Serialize; use std::cell::RefCell; + use std::collections::BTreeMap; struct SetupDraftDatastore { draft: RefCell<Option<RadrootsAppSetupDraft>>, @@ -526,6 +558,11 @@ mod tests { record: RefCell<Option<RadrootsAppStateRecord>>, } + struct LegacyKeyDatastore { + record: RefCell<Option<RadrootsAppStateRecord>>, + values: RefCell<BTreeMap<String, String>>, + } + #[async_trait(?Send)] impl RadrootsClientDatastore for TestDatastore { fn get_config(&self) -> RadrootsClientIdbConfig { @@ -657,6 +694,141 @@ mod tests { } } + #[async_trait(?Send)] + impl RadrootsClientDatastore for LegacyKeyDatastore { + fn get_config(&self) -> RadrootsClientIdbConfig { + IDB_CONFIG_DATASTORE + } + + fn get_store_id(&self) -> &str { + "test" + } + + async fn init(&self) -> RadrootsClientDatastoreResult<()> { + Ok(()) + } + + async fn set(&self, key: &str, value: &str) -> RadrootsClientDatastoreResult<String> { + self.values + .borrow_mut() + .insert(key.to_string(), value.to_string()); + Ok(value.to_string()) + } + + async fn get(&self, key: &str) -> RadrootsClientDatastoreResult<String> { + self.values + .borrow() + .get(key) + .cloned() + .ok_or(RadrootsClientDatastoreError::NoResult) + } + + async fn set_obj<T>( + &self, + _key: &str, + value: &T, + ) -> RadrootsClientDatastoreResult<T> + where + T: Serialize + DeserializeOwned + Clone, + { + let encoded = serde_json::to_string(value) + .map_err(|_| RadrootsClientDatastoreError::IdbUndefined)?; + if let Ok(parsed) = serde_json::from_str::<RadrootsAppStateRecord>(&encoded) { + *self.record.borrow_mut() = Some(parsed); + return Ok(value.clone()); + } + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn update_obj<T>( + &self, + _key: &str, + _value: &T, + ) -> RadrootsClientDatastoreResult<T> + where + T: Serialize + DeserializeOwned + Clone, + { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn get_obj<T>(&self, _key: &str) -> RadrootsClientDatastoreResult<T> + where + T: DeserializeOwned, + { + if let Some(record) = self.record.borrow().as_ref() { + let encoded = serde_json::to_string(record) + .map_err(|_| RadrootsClientDatastoreError::NoResult)?; + if let Ok(parsed) = serde_json::from_str(&encoded) { + return Ok(parsed); + } + } + Err(RadrootsClientDatastoreError::NoResult) + } + + async fn del_obj(&self, _key: &str) -> RadrootsClientDatastoreResult<String> { + *self.record.borrow_mut() = None; + Ok("cleared".to_string()) + } + + async fn del(&self, key: &str) -> RadrootsClientDatastoreResult<String> { + self.values.borrow_mut().remove(key); + Ok(key.to_string()) + } + + async fn del_pref(&self, _key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn set_param( + &self, + _key: &str, + _key_param: &str, + _value: &str, + ) -> RadrootsClientDatastoreResult<String> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn get_param( + &self, + _key: &str, + _key_param: &str, + ) -> RadrootsClientDatastoreResult<String> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn entries_pref( + &self, + _key_prefix: &str, + ) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn reset(&self) -> RadrootsClientDatastoreResult<()> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn export_backup( + &self, + ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn import_backup( + &self, + _payload: RadrootsClientBackupDatastorePayload, + ) -> RadrootsClientDatastoreResult<()> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + } + #[test] fn state_write_maps_idb_errors() { let datastore = RadrootsClientWebDatastore::new(None); @@ -899,4 +1071,27 @@ mod tests { let stored_profile = datastore.profile.borrow().clone(); assert_eq!(stored_profile, Some(profile)); } + + #[test] + fn state_record_migrates_legacy_keys() { + let key_maps = app_key_maps_default(); + let key_nostr = app_datastore_key_nostr_key(&key_maps).expect("nostr key"); + let key_eula = app_datastore_key_eula_date(&key_maps).expect("eula key"); + let mut values = BTreeMap::new(); + values.insert(key_nostr.to_string(), "pub".to_string()); + values.insert(key_eula.to_string(), "2025-01-01T00:00:00Z".to_string()); + let datastore = LegacyKeyDatastore { + record: RefCell::new(None), + values: RefCell::new(values), + }; + let record = futures::executor::block_on(app_datastore_read_state_record( + &datastore, + &key_maps, + )) + .expect("record"); + assert_eq!(record.state.active_key, "pub"); + assert_eq!(record.state.eula_date, "2025-01-01T00:00:00Z"); + assert!(datastore.values.borrow().get(key_nostr).is_none()); + assert!(datastore.values.borrow().get(key_eula).is_none()); + } }