lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit bc7ba824865cdd4589d15989bad3f9f18cf12221
parent 27344b0843f4166b78ca2cb631568dbd1a500a9b
Author: triesap <tyson@radroots.org>
Date:   Sun, 28 Dec 2025 19:41:30 +0000

tangle-events: add wasm build and harden ingest/update paths


- Add build-tangle-events-wasm target and pkg package.json exports
- Add base64/uuid deps and wasm d-tag factory using UUIDv7 URL-safe encoding
- Normalize nostr event parsing to accept author/pubkey envelope with validation
- Skip update zero-change and ignore NotFound during tag/location/member delete sweeps

Diffstat:
MCargo.lock | 2++
MMakefile | 9+++++++--
Mtangle-db/src/models/farm.rs | 5+----
Mtangle-db/src/models/farm_gcs_location.rs | 5+----
Mtangle-db/src/models/farm_member.rs | 5+----
Mtangle-db/src/models/farm_member_claim.rs | 5+----
Mtangle-db/src/models/farm_tag.rs | 5+----
Mtangle-db/src/models/gcs_location.rs | 5+----
Mtangle-db/src/models/log_error.rs | 5+----
Mtangle-db/src/models/media_image.rs | 5+----
Mtangle-db/src/models/nostr_event_state.rs | 5+----
Mtangle-db/src/models/nostr_profile.rs | 5+----
Mtangle-db/src/models/nostr_relay.rs | 5+----
Mtangle-db/src/models/plot.rs | 5+----
Mtangle-db/src/models/plot_gcs_location.rs | 5+----
Mtangle-db/src/models/plot_tag.rs | 5+----
Mtangle-db/src/models/trade_product.rs | 5+----
Mtangle-events-wasm/Cargo.toml | 2++
Atangle-events-wasm/pkg/package.json | 19+++++++++++++++++++
Mtangle-events-wasm/src/lib.rs | 52+++++++++++++++++++++++++++++++++++++++++++++++++---
Mtangle-events/src/emit.rs | 2+-
Mtangle-events/src/ingest.rs | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
22 files changed, 150 insertions(+), 78 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1944,6 +1944,7 @@ dependencies = [ name = "radroots-tangle-events-wasm" version = "0.1.0" dependencies = [ + "base64 0.22.1", "radroots-events", "radroots-sql-core", "radroots-sql-wasm-core", @@ -1951,6 +1952,7 @@ dependencies = [ "serde", "serde-wasm-bindgen", "serde_json", + "uuid", "wasm-bindgen", ] diff --git a/Makefile b/Makefile @@ -1,6 +1,6 @@ .PHONY: all bindings clean help \ bindings-events bindings-tangle-db-schema bindings-trade bindings-types \ - build build-events-codec-wasm build-tangle-db-wasm + build build-events-codec-wasm build-tangle-db-wasm build-tangle-events-wasm SHELL := /bin/bash .SHELLFLAGS := -e -o pipefail -c @@ -14,7 +14,8 @@ BINDINGS_TARGETS := \ BUILD_TARGETS := \ build-events-codec-wasm \ - build-tangle-db-wasm + build-tangle-db-wasm \ + build-tangle-events-wasm all: bindings build @@ -58,3 +59,7 @@ build-tangle-db-wasm: build-events-codec-wasm: wasm-pack build events-codec-wasm --release --target web \ --out-dir ../events-codec-wasm/pkg/dist --scope radroots + +build-tangle-events-wasm: + wasm-pack build tangle-events-wasm --release --target web \ + --out-dir ../tangle-events-wasm/pkg/dist --scope radroots diff --git a/tangle-db/src/models/farm.rs b/tangle-db/src/models/farm.rs @@ -138,10 +138,7 @@ pub fn update<E: SqlExecutor>( bind_values.push(Value::from(id_for_lookup.clone())); let sql = format!("UPDATE {TABLE_NAME} SET {} WHERE id = ?;", set_parts.join(", ")); let params_json = utils::to_params_json(bind_values)?; - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } + let _ = exec.exec(&sql, &params_json)?; let updated = select_by_id(exec, &id_for_lookup)?; Ok(IResult { result: updated }) } diff --git a/tangle-db/src/models/farm_gcs_location.rs b/tangle-db/src/models/farm_gcs_location.rs @@ -138,10 +138,7 @@ pub fn update<E: SqlExecutor>( bind_values.push(Value::from(id_for_lookup.clone())); let sql = format!("UPDATE {TABLE_NAME} SET {} WHERE id = ?;", set_parts.join(", ")); let params_json = utils::to_params_json(bind_values)?; - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } + let _ = exec.exec(&sql, &params_json)?; let updated = select_by_id(exec, &id_for_lookup)?; Ok(IResult { result: updated }) } diff --git a/tangle-db/src/models/farm_member.rs b/tangle-db/src/models/farm_member.rs @@ -138,10 +138,7 @@ pub fn update<E: SqlExecutor>( bind_values.push(Value::from(id_for_lookup.clone())); let sql = format!("UPDATE {TABLE_NAME} SET {} WHERE id = ?;", set_parts.join(", ")); let params_json = utils::to_params_json(bind_values)?; - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } + let _ = exec.exec(&sql, &params_json)?; let updated = select_by_id(exec, &id_for_lookup)?; Ok(IResult { result: updated }) } diff --git a/tangle-db/src/models/farm_member_claim.rs b/tangle-db/src/models/farm_member_claim.rs @@ -138,10 +138,7 @@ pub fn update<E: SqlExecutor>( bind_values.push(Value::from(id_for_lookup.clone())); let sql = format!("UPDATE {TABLE_NAME} SET {} WHERE id = ?;", set_parts.join(", ")); let params_json = utils::to_params_json(bind_values)?; - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } + let _ = exec.exec(&sql, &params_json)?; let updated = select_by_id(exec, &id_for_lookup)?; Ok(IResult { result: updated }) } diff --git a/tangle-db/src/models/farm_tag.rs b/tangle-db/src/models/farm_tag.rs @@ -138,10 +138,7 @@ pub fn update<E: SqlExecutor>( bind_values.push(Value::from(id_for_lookup.clone())); let sql = format!("UPDATE {TABLE_NAME} SET {} WHERE id = ?;", set_parts.join(", ")); let params_json = utils::to_params_json(bind_values)?; - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } + let _ = exec.exec(&sql, &params_json)?; let updated = select_by_id(exec, &id_for_lookup)?; Ok(IResult { result: updated }) } diff --git a/tangle-db/src/models/gcs_location.rs b/tangle-db/src/models/gcs_location.rs @@ -178,10 +178,7 @@ pub fn update<E: SqlExecutor>( bind_values.push(Value::from(id_for_lookup.clone())); let sql = format!("UPDATE {TABLE_NAME} SET {} WHERE id = ?;", set_parts.join(", ")); let params_json = utils::to_params_json(bind_values)?; - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } + let _ = exec.exec(&sql, &params_json)?; let updated = select_by_id(exec, &id_for_lookup)?; Ok(IResult { result: updated }) } diff --git a/tangle-db/src/models/log_error.rs b/tangle-db/src/models/log_error.rs @@ -138,10 +138,7 @@ pub fn update<E: SqlExecutor>( bind_values.push(Value::from(id_for_lookup.clone())); let sql = format!("UPDATE {TABLE_NAME} SET {} WHERE id = ?;", set_parts.join(", ")); let params_json = utils::to_params_json(bind_values)?; - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } + let _ = exec.exec(&sql, &params_json)?; let updated = select_by_id(exec, &id_for_lookup)?; Ok(IResult { result: updated }) } diff --git a/tangle-db/src/models/media_image.rs b/tangle-db/src/models/media_image.rs @@ -162,10 +162,7 @@ pub fn update<E: SqlExecutor>( bind_values.push(Value::from(id_for_lookup.clone())); let sql = format!("UPDATE {TABLE_NAME} SET {} WHERE id = ?;", set_parts.join(", ")); let params_json = utils::to_params_json(bind_values)?; - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } + let _ = exec.exec(&sql, &params_json)?; let updated = select_by_id(exec, &id_for_lookup)?; Ok(IResult { result: updated }) } diff --git a/tangle-db/src/models/nostr_event_state.rs b/tangle-db/src/models/nostr_event_state.rs @@ -138,10 +138,7 @@ pub fn update<E: SqlExecutor>( bind_values.push(Value::from(id_for_lookup.clone())); let sql = format!("UPDATE {TABLE_NAME} SET {} WHERE id = ?;", set_parts.join(", ")); let params_json = utils::to_params_json(bind_values)?; - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } + let _ = exec.exec(&sql, &params_json)?; let updated = select_by_id(exec, &id_for_lookup)?; Ok(IResult { result: updated }) } diff --git a/tangle-db/src/models/nostr_profile.rs b/tangle-db/src/models/nostr_profile.rs @@ -162,10 +162,7 @@ pub fn update<E: SqlExecutor>( bind_values.push(Value::from(id_for_lookup.clone())); let sql = format!("UPDATE {TABLE_NAME} SET {} WHERE id = ?;", set_parts.join(", ")); let params_json = utils::to_params_json(bind_values)?; - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } + let _ = exec.exec(&sql, &params_json)?; let updated = select_by_id(exec, &id_for_lookup)?; Ok(IResult { result: updated }) } diff --git a/tangle-db/src/models/nostr_relay.rs b/tangle-db/src/models/nostr_relay.rs @@ -162,10 +162,7 @@ pub fn update<E: SqlExecutor>( bind_values.push(Value::from(id_for_lookup.clone())); let sql = format!("UPDATE {TABLE_NAME} SET {} WHERE id = ?;", set_parts.join(", ")); let params_json = utils::to_params_json(bind_values)?; - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } + let _ = exec.exec(&sql, &params_json)?; let updated = select_by_id(exec, &id_for_lookup)?; Ok(IResult { result: updated }) } diff --git a/tangle-db/src/models/plot.rs b/tangle-db/src/models/plot.rs @@ -138,10 +138,7 @@ pub fn update<E: SqlExecutor>( bind_values.push(Value::from(id_for_lookup.clone())); let sql = format!("UPDATE {TABLE_NAME} SET {} WHERE id = ?;", set_parts.join(", ")); let params_json = utils::to_params_json(bind_values)?; - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } + let _ = exec.exec(&sql, &params_json)?; let updated = select_by_id(exec, &id_for_lookup)?; Ok(IResult { result: updated }) } diff --git a/tangle-db/src/models/plot_gcs_location.rs b/tangle-db/src/models/plot_gcs_location.rs @@ -138,10 +138,7 @@ pub fn update<E: SqlExecutor>( bind_values.push(Value::from(id_for_lookup.clone())); let sql = format!("UPDATE {TABLE_NAME} SET {} WHERE id = ?;", set_parts.join(", ")); let params_json = utils::to_params_json(bind_values)?; - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } + let _ = exec.exec(&sql, &params_json)?; let updated = select_by_id(exec, &id_for_lookup)?; Ok(IResult { result: updated }) } diff --git a/tangle-db/src/models/plot_tag.rs b/tangle-db/src/models/plot_tag.rs @@ -138,10 +138,7 @@ pub fn update<E: SqlExecutor>( bind_values.push(Value::from(id_for_lookup.clone())); let sql = format!("UPDATE {TABLE_NAME} SET {} WHERE id = ?;", set_parts.join(", ")); let params_json = utils::to_params_json(bind_values)?; - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } + let _ = exec.exec(&sql, &params_json)?; let updated = select_by_id(exec, &id_for_lookup)?; Ok(IResult { result: updated }) } diff --git a/tangle-db/src/models/trade_product.rs b/tangle-db/src/models/trade_product.rs @@ -138,10 +138,7 @@ pub fn update<E: SqlExecutor>( bind_values.push(Value::from(id_for_lookup.clone())); let sql = format!("UPDATE {TABLE_NAME} SET {} WHERE id = ?;", set_parts.join(", ")); let params_json = utils::to_params_json(bind_values)?; - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } + let _ = exec.exec(&sql, &params_json)?; let updated = select_by_id(exec, &id_for_lookup)?; Ok(IResult { result: updated }) } diff --git a/tangle-events-wasm/Cargo.toml b/tangle-events-wasm/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true crate-type = ["cdylib", "rlib"] [dependencies] +base64 = { workspace = true } radroots-events = { workspace = true, default-features = false, features = ["serde"] } radroots-sql-core = { workspace = true, features = ["web"] } radroots-sql-wasm-core = { workspace = true } @@ -17,4 +18,5 @@ radroots-tangle-events = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } serde-wasm-bindgen = { workspace = true } +uuid = { workspace = true, features = ["js"] } wasm-bindgen = { workspace = true } diff --git a/tangle-events-wasm/pkg/package.json b/tangle-events-wasm/pkg/package.json @@ -0,0 +1,19 @@ +{ + "name": "@radroots/tangle-events-wasm", + "version": "0.1.0", + "private": true, + "type": "module", + "files": [ + "dist" + ], + "main": "./dist/radroots_tangle_events_wasm.js", + "types": "./dist/radroots_tangle_events_wasm.d.ts", + "exports": { + ".": { + "types": "./dist/radroots_tangle_events_wasm.d.ts", + "import": "./dist/radroots_tangle_events_wasm.js", + "default": "./dist/radroots_tangle_events_wasm.js" + } + }, + "sideEffects": false +} diff --git a/tangle-events-wasm/src/lib.rs b/tangle-events-wasm/src/lib.rs @@ -4,23 +4,68 @@ use radroots_events::RadrootsNostrEvent; use radroots_sql_core::WasmSqlExecutor; use radroots_tangle_events::{ - radroots_tangle_ingest_event, + radroots_tangle_ingest_event_with_factory, radroots_tangle_sync_all, + RadrootsTangleIdFactory, RadrootsTangleIngestOutcome, RadrootsTangleSyncRequest, }; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::Engine; +use serde::Deserialize; +use uuid::Uuid; use wasm_bindgen::prelude::*; fn err_js<E: ToString>(err: E) -> JsValue { JsValue::from_str(&err.to_string()) } +struct WasmIdFactory; + +impl RadrootsTangleIdFactory for WasmIdFactory { + fn new_d_tag(&self) -> String { + let uuid = Uuid::now_v7(); + URL_SAFE_NO_PAD.encode(uuid.as_bytes()) + } +} + +#[derive(Deserialize)] +struct NostrEventEnvelope { + id: String, + #[serde(default)] + author: Option<String>, + #[serde(default)] + pubkey: Option<String>, + created_at: u32, + kind: u32, + tags: Vec<Vec<String>>, + content: String, + sig: String, +} + fn parse_request(request_json: &str) -> Result<RadrootsTangleSyncRequest, JsValue> { serde_json::from_str(request_json).map_err(err_js) } fn parse_event(event_json: &str) -> Result<RadrootsNostrEvent, JsValue> { - serde_json::from_str(event_json).map_err(err_js) + let envelope: NostrEventEnvelope = serde_json::from_str(event_json).map_err(err_js)?; + let author = match (envelope.author, envelope.pubkey) { + (Some(author), Some(pubkey)) if author != pubkey => { + return Err(JsValue::from_str("author/pubkey mismatch")); + } + (Some(author), _) => author, + (None, Some(pubkey)) => pubkey, + (None, None) => return Err(JsValue::from_str("missing author/pubkey")), + }; + Ok(RadrootsNostrEvent { + id: envelope.id, + author, + created_at: envelope.created_at, + kind: envelope.kind, + tags: envelope.tags, + content: envelope.content, + sig: envelope.sig, + }) } #[wasm_bindgen(js_name = tangle_events_sync_all)] @@ -35,7 +80,8 @@ pub fn tangle_events_sync_all(request_json: &str) -> Result<JsValue, JsValue> { pub fn tangle_events_ingest_event(event_json: &str) -> Result<JsValue, JsValue> { let event = parse_event(event_json)?; let exec = WasmSqlExecutor::new(); - let outcome = radroots_tangle_ingest_event(&exec, &event).map_err(err_js)?; + let factory = WasmIdFactory; + let outcome = radroots_tangle_ingest_event_with_factory(&exec, &event, &factory).map_err(err_js)?; let value = match outcome { RadrootsTangleIngestOutcome::Applied => "applied", RadrootsTangleIngestOutcome::Skipped => "skipped", diff --git a/tangle-events/src/emit.rs b/tangle-events/src/emit.rs @@ -654,7 +654,7 @@ fn profile_event( profile: radroots_tangle_db_schema::nostr_profile::NostrProfile, ) -> Result<RadrootsTangleEventDraft, RadrootsTangleEventsError> { let profile_type = match profile.profile_type.as_str() { - "individual" => Some(RadrootsProfileType::Individual), + "individual" | "farmer" => Some(RadrootsProfileType::Individual), "farm" => Some(RadrootsProfileType::Farm), other => radroots_profile_type_from_tag_value(other), }; diff --git a/tangle-events/src/ingest.rs b/tangle-events/src/ingest.rs @@ -20,6 +20,7 @@ use radroots_events_codec::list_set::decode as list_set_decode; use radroots_events_codec::plot::decode as plot_decode; use radroots_events_codec::profile::decode as profile_decode; use radroots_sql_core::SqlExecutor; +use radroots_sql_core::error::SqlError; use radroots_tangle_db_schema::farm::{ FarmQueryBindValues, IFarmFields, @@ -597,12 +598,19 @@ fn upsert_farm_tags<E: SqlExecutor>( }, )?; for row in existing.results { - let _ = farm_tag::delete( + match farm_tag::delete( exec, &IFarmTagDelete::On(IFarmTagFindOneArgs { on: FarmTagQueryBindValues::Id { id: row.id }, }), - )?; + ) { + Ok(_) => {} + Err(err) => { + if !matches!(err.err, SqlError::NotFound(_)) { + return Err(err.into()); + } + } + } } let mut tags = tags.unwrap_or_default(); @@ -639,12 +647,19 @@ fn upsert_plot_tags<E: SqlExecutor>( }, )?; for row in existing.results { - let _ = plot_tag::delete( + match plot_tag::delete( exec, &IPlotTagDelete::On(IPlotTagFindOneArgs { on: PlotTagQueryBindValues::Id { id: row.id }, }), - )?; + ) { + Ok(_) => {} + Err(err) => { + if !matches!(err.err, SqlError::NotFound(_)) { + return Err(err.into()); + } + } + } } let mut tags = tags.unwrap_or_default(); @@ -719,12 +734,19 @@ fn clear_farm_locations<E: SqlExecutor>( }, )?; for row in existing.results { - let _ = farm_gcs_location::delete( + match farm_gcs_location::delete( exec, &IFarmGcsLocationDelete::On(IFarmGcsLocationFindOneArgs { on: FarmGcsLocationQueryBindValues::Id { id: row.id }, }), - )?; + ) { + Ok(_) => {} + Err(err) => { + if !matches!(err.err, SqlError::NotFound(_)) { + return Err(err.into()); + } + } + } } Ok(()) } @@ -747,12 +769,19 @@ fn clear_plot_locations<E: SqlExecutor>( }, )?; for row in existing.results { - let _ = plot_gcs_location::delete( + match plot_gcs_location::delete( exec, &IPlotGcsLocationDelete::On(IPlotGcsLocationFindOneArgs { on: PlotGcsLocationQueryBindValues::Id { id: row.id }, }), - )?; + ) { + Ok(_) => {} + Err(err) => { + if !matches!(err.err, SqlError::NotFound(_)) { + return Err(err.into()); + } + } + } } Ok(()) } @@ -820,12 +849,19 @@ fn upsert_farm_members<E: SqlExecutor>( }, )?; for row in existing.results { - let _ = farm_member::delete( + match farm_member::delete( exec, &IFarmMemberDelete::On(IFarmMemberFindOneArgs { on: FarmMemberQueryBindValues::Id { id: row.id }, }), - )?; + ) { + Ok(_) => {} + Err(err) => { + if !matches!(err.err, SqlError::NotFound(_)) { + return Err(err.into()); + } + } + } } let mut entries = list_set @@ -867,12 +903,19 @@ fn upsert_member_claims<E: SqlExecutor>( }, )?; for row in existing.results { - let _ = farm_member_claim::delete( + match farm_member_claim::delete( exec, &IFarmMemberClaimDelete::On(IFarmMemberClaimFindOneArgs { on: FarmMemberClaimQueryBindValues::Id { id: row.id }, }), - )?; + ) { + Ok(_) => {} + Err(err) => { + if !matches!(err.err, SqlError::NotFound(_)) { + return Err(err.into()); + } + } + } } let mut entries = list_set