app

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

commit 759c5e6e80fe77106d071945cf3d003786378b70
parent 3ce26f6709e79c32b3c690ddff702e3213d05be6
Author: triesap <tyson@radroots.org>
Date:   Sat, 23 May 2026 20:57:11 +0000

sqlite: converge signed app listings

Diffstat:
Mcrates/shared/sqlite/src/local_interop.rs | 240+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 230 insertions(+), 10 deletions(-)

diff --git a/crates/shared/sqlite/src/local_interop.rs b/crates/shared/sqlite/src/local_interop.rs @@ -9,7 +9,7 @@ use radroots_local_events::{ SourceRuntime, }; use radroots_sql_core::{SqlExecutor, SqliteExecutor}; -use rusqlite::{Connection, params}; +use rusqlite::{Connection, OptionalExtension, params}; use serde_json::Value; use uuid::Uuid; @@ -470,17 +470,24 @@ impl<'a> AppLocalInteropRepository<'a> { .as_deref() .or(signed_farm_pubkey.as_deref()) .or(record.owner_pubkey.as_deref()); - let Some(farm_id) = - projected_farm_id(record.source_runtime, farm_pubkey, farm_key.as_str()) - else { - return Ok(None); + let existing_projection = + self.existing_listing_projection(record.listing_addr.as_deref())?; + let (farm_id, product_id) = if let Some(existing_projection) = existing_projection { + (existing_projection.farm_id, existing_projection.product_id) + } else { + let Some(farm_id) = + projected_farm_id(record.source_runtime, farm_pubkey, farm_key.as_str()) + else { + return Ok(None); + }; + let Some(product_id) = + projected_product_id(record.source_runtime, listing_pubkey, listing_key.as_str()) + else { + return Ok(None); + }; + (farm_id, product_id) }; self.ensure_farm_exists(farm_id)?; - let Some(product_id) = - projected_product_id(record.source_runtime, listing_pubkey, listing_key.as_str()) - else { - return Ok(None); - }; let title = content .as_ref() .and_then(|content| string_at(content, &["product", "title"])) @@ -644,6 +651,52 @@ impl<'a> AppLocalInteropRepository<'a> { Ok(()) } + fn existing_listing_projection( + &self, + listing_addr: Option<&str>, + ) -> Result<Option<ExistingListingProjection>, AppSqliteError> { + let Some(listing_addr) = listing_addr + .map(str::trim) + .filter(|listing_addr| !listing_addr.is_empty()) + else { + return Ok(None); + }; + let Some((product_id, farm_id)) = self + .connection + .query_row( + "SELECT products.id, products.farm_id + FROM local_interop_imports + JOIN products ON products.id = local_interop_imports.projected_id + WHERE local_interop_imports.projected_kind = 'listing' + AND local_interop_imports.projected_id IS NOT NULL + AND local_interop_imports.listing_addr = ?1 + ORDER BY local_interop_imports.local_seq DESC + LIMIT 1", + [listing_addr], + |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)), + ) + .optional() + .map_err(|source| AppSqliteError::Query { + operation: "load existing local interop listing projection", + source, + })? + else { + return Ok(None); + }; + Ok(Some(ExistingListingProjection { + product_id: product_id + .parse() + .map_err(|_| AppSqliteError::InvalidProjection { + reason: "existing listing projection product id must parse", + })?, + farm_id: farm_id + .parse() + .map_err(|_| AppSqliteError::InvalidProjection { + reason: "existing listing projection farm id must parse", + })?, + })) + } + fn record_import( &self, record: &LocalEventRecord, @@ -775,6 +828,12 @@ struct ProductProjection { stock_count: Option<u32>, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct ExistingListingProjection { + product_id: ProductId, + farm_id: FarmId, +} + fn deterministic_farm_id(owner_pubkey: Option<&str>, farm_key: &str) -> FarmId { FarmId::from(deterministic_uuid( "radroots-cli-farm", @@ -2137,4 +2196,165 @@ mod tests { assert_eq!(imported[0].projected_kind, "unsupported"); assert_eq!(farm_count, 0); } + + #[test] + fn signed_app_origin_listing_updates_existing_app_projection() { + let app_store = + AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); + let events = local_events_store(); + let farm_uuid = Uuid::from_u128(0x33333333333343338333333333333333); + let product_uuid = Uuid::from_u128(0x44444444444444448444444444444444); + let farm_key = app_d_tag_from_uuid(farm_uuid); + let listing_key = app_d_tag_from_uuid(product_uuid); + let listing_addr = format!("30402:app-seller-pubkey:{listing_key}"); + let app_farm_record = app_local_work_record( + "app:local_work:farm:signed-convergence", + farm_key.as_str(), + json!({ + "record_kind": "farm_config_v1", + "document": { + "selection": { + "account": "seller-account", + "farm_d_tag": farm_key + }, + "profile": { + "display_name": "App Farm" + }, + "farm": { + "d_tag": farm_key, + "name": "App Farm" + } + } + }), + ); + let mut app_listing_record = app_local_work_record( + "app:local_work:listing:signed-convergence", + farm_key.as_str(), + json!({ + "record_kind": "listing_draft_v1", + "document": { + "listing": { + "d_tag": listing_key, + "farm_d_tag": farm_key + }, + "seller_actor": { + "account_id": "seller-account", + "pubkey": "app-seller-pubkey" + }, + "product": { + "key": listing_key, + "title": "App Draft Eggs", + "summary": "Fresh app-origin eggs" + }, + "primary_bin": { + "quantity_unit": "each", + "price_amount": "7", + "price_currency": "USD" + }, + "inventory": { + "available": "12" + } + } + }), + ); + app_listing_record.listing_addr = Some(listing_addr.clone()); + events + .append_record(&app_farm_record) + .expect("append app farm local work"); + events + .append_record(&app_listing_record) + .expect("append app listing local work"); + + let local_report = app_store + .import_shared_local_events_from_store(&events) + .expect("import app local work"); + events + .append_record(&LocalEventRecordInput { + record_id: "cli:signed_event:listing:app-origin".to_owned(), + family: LocalRecordFamily::SignedEvent, + status: LocalRecordStatus::Published, + source_runtime: SourceRuntime::Cli, + created_at_ms: 1100, + inserted_at_ms: 1101, + owner_account_id: Some("seller-account".to_owned()), + owner_pubkey: Some("app-seller-pubkey".to_owned()), + farm_id: Some(farm_key.clone()), + listing_addr: Some(listing_addr.clone()), + local_work_json: None, + event_id: Some("event-app-origin".to_owned()), + event_kind: Some(KIND_LISTING), + event_pubkey: Some("app-seller-pubkey".to_owned()), + event_created_at: Some(1100), + event_tags_json: Some(json!([ + ["d", listing_key], + ["a", format!("30340:app-seller-pubkey:{farm_key}")], + ["title", "Relay App Eggs"], + ["summary", "Published app-origin eggs"], + ["radroots:bin", "bin-1", "1", "each"], + ["radroots:price", "bin-1", "8", "USD", "1", "each"], + ["inventory", "9"], + ["status", "active"] + ])), + event_content: Some("# Relay App Eggs\n\nPublished app-origin eggs".to_owned()), + event_sig: Some("signature".to_owned()), + raw_event_json: Some(json!({ + "id": "event-app-origin", + "kind": KIND_LISTING, + "pubkey": "app-seller-pubkey", + "content": "# Relay App Eggs\n\nPublished app-origin eggs" + })), + outbox_status: PublishOutboxStatus::Acknowledged, + relay_set_fingerprint: Some("relay-set".to_owned()), + relay_delivery_json: Some(json!({ + "state": "acknowledged", + "acknowledged_relays": ["ws://127.0.0.1:1234/"] + })), + }) + .expect("append signed app-origin listing"); + let signed_report = app_store + .import_shared_local_events_from_store(&events) + .expect("import signed app-origin listing"); + let imported = app_store + .load_local_interop_records() + .expect("load imported records"); + let listing_records = imported + .iter() + .filter(|record| record.projected_kind == "listing") + .collect::<Vec<_>>(); + let product_count: i64 = app_store + .connection() + .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) + .expect("product count"); + let product: (String, String, String, Option<i64>, Option<i64>) = app_store + .connection() + .query_row( + "SELECT id, farm_id, status, price_minor_units, stock_count FROM products", + [], + |row| { + Ok(( + row.get(0)?, + row.get(1)?, + row.get(2)?, + row.get(3)?, + row.get(4)?, + )) + }, + ) + .expect("load product"); + + assert_eq!(local_report.imported_records, 2); + assert_eq!(signed_report.scanned_records, 1); + assert_eq!(signed_report.imported_records, 1); + assert_eq!(listing_records.len(), 2); + assert_eq!( + listing_records[0].projected_id, + listing_records[1].projected_id + ); + assert_eq!(product_count, 1); + assert_eq!(product.0, product_uuid.to_string()); + assert_eq!(product.1, farm_uuid.to_string()); + assert_eq!(product.2, "published"); + assert_eq!(product.3, Some(800)); + assert_eq!(product.4, Some(9)); + } }