commit da49136dc0f2b34720ffc3db349b8e63e2f62fd5 parent e4ab42f9e07cf5716aba70dc42d20fbe2bf161d5 Author: triesap <tyson@radroots.org> Date: Sun, 1 Mar 2026 16:05:34 +0000 build: rename tangle crate family to replica Diffstat:
157 files changed, 12427 insertions(+), 12427 deletions(-)
diff --git a/.github/workflows/sdk-contract-ci.yml b/.github/workflows/sdk-contract-ci.yml @@ -30,7 +30,7 @@ jobs: -p radroots-events \ -p radroots-trade \ -p radroots-identity \ - -p radroots-tangle-db-schema \ + -p radroots-replica-db-schema \ -p radroots-events-codec \ -p radroots-events-codec-wasm @@ -43,7 +43,7 @@ jobs: -p radroots-events \ -p radroots-trade \ -p radroots-identity \ - -p radroots-tangle-db-schema \ + -p radroots-replica-db-schema \ -p radroots-events-codec \ -p radroots-events-codec-wasm diff --git a/Cargo.lock b/Cargo.lock @@ -2797,133 +2797,133 @@ dependencies = [ ] [[package]] -name = "radroots-runtime" +name = "radroots-replica-db" version = "0.1.0" dependencies = [ - "anyhow", - "clap", - "config", - "radroots-log", + "hex", + "radroots-replica-db-schema", + "radroots-sql-core", + "radroots-types", "serde", "serde_json", - "tempfile", - "thiserror 1.0.69", - "tokio", - "toml 0.8.23", - "tracing", + "sha2", ] [[package]] -name = "radroots-sql-core" +name = "radroots-replica-db-schema" version = "0.1.0" dependencies = [ - "chrono", - "radroots-sql-wasm-bridge", - "rusqlite", + "radroots-types", "serde", - "serde-wasm-bindgen", "serde_json", - "thiserror 1.0.69", - "uuid", - "wasm-bindgen", + "ts-rs", ] [[package]] -name = "radroots-sql-wasm-bridge" +name = "radroots-replica-db-wasm" version = "0.1.0" dependencies = [ "js-sys", + "radroots-replica-db", + "radroots-replica-db-schema", + "radroots-replica-sync", + "radroots-sql-core", + "radroots-sql-wasm-core", + "serde", + "serde-wasm-bindgen", + "serde_json", "wasm-bindgen", + "wasm-bindgen-test", ] [[package]] -name = "radroots-sql-wasm-core" +name = "radroots-replica-sync" version = "0.1.0" dependencies = [ - "chrono", - "js-sys", + "base64 0.22.1", + "hex", + "radroots-events", + "radroots-events-codec", + "radroots-replica-db", + "radroots-replica-db-schema", "radroots-sql-core", - "radroots-sql-wasm-bridge", - "rusqlite", + "radroots-types", "serde", - "serde-wasm-bindgen", "serde_json", - "thiserror 1.0.69", - "ts-rs", + "sha2", "uuid", - "wasm-bindgen", ] [[package]] -name = "radroots-tangle-db" +name = "radroots-replica-sync-wasm" version = "0.1.0" dependencies = [ - "hex", + "base64 0.22.1", + "radroots-events", + "radroots-replica-sync", "radroots-sql-core", - "radroots-tangle-db-schema", - "radroots-types", + "radroots-sql-wasm-core", "serde", + "serde-wasm-bindgen", "serde_json", - "sha2", + "uuid", + "wasm-bindgen", ] [[package]] -name = "radroots-tangle-db-schema" +name = "radroots-runtime" version = "0.1.0" dependencies = [ - "radroots-types", + "anyhow", + "clap", + "config", + "radroots-log", "serde", "serde_json", - "ts-rs", + "tempfile", + "thiserror 1.0.69", + "tokio", + "toml 0.8.23", + "tracing", ] [[package]] -name = "radroots-tangle-db-wasm" +name = "radroots-sql-core" version = "0.1.0" dependencies = [ - "js-sys", - "radroots-sql-core", - "radroots-sql-wasm-core", - "radroots-tangle-db", - "radroots-tangle-db-schema", - "radroots-tangle-events", + "chrono", + "radroots-sql-wasm-bridge", + "rusqlite", "serde", "serde-wasm-bindgen", "serde_json", + "thiserror 1.0.69", + "uuid", "wasm-bindgen", - "wasm-bindgen-test", ] [[package]] -name = "radroots-tangle-events" +name = "radroots-sql-wasm-bridge" version = "0.1.0" dependencies = [ - "base64 0.22.1", - "hex", - "radroots-events", - "radroots-events-codec", - "radroots-sql-core", - "radroots-tangle-db", - "radroots-tangle-db-schema", - "radroots-types", - "serde", - "serde_json", - "sha2", - "uuid", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "radroots-tangle-events-wasm" +name = "radroots-sql-wasm-core" version = "0.1.0" dependencies = [ - "base64 0.22.1", - "radroots-events", + "chrono", + "js-sys", "radroots-sql-core", - "radroots-sql-wasm-core", - "radroots-tangle-events", + "radroots-sql-wasm-bridge", + "rusqlite", "serde", "serde-wasm-bindgen", "serde_json", + "thiserror 1.0.69", + "ts-rs", "uuid", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml @@ -19,11 +19,11 @@ members = [ "crates/sql-wasm-bridge", "crates/sql-wasm-core", "crates/sql-core", - "crates/tangle-db-schema", - "crates/tangle-events", - "crates/tangle-events-wasm", - "crates/tangle-db", - "crates/tangle-db-wasm", + "crates/replica-db-schema", + "crates/replica-sync", + "crates/replica-sync-wasm", + "crates/replica-db", + "crates/replica-db-wasm", "crates/trade", "crates/types", "crates/xtask", @@ -58,11 +58,11 @@ radroots-nostr-runtime = { path = "crates/nostr-runtime", version = "0.1.0", def radroots-sql-wasm-bridge = { path = "crates/sql-wasm-bridge", version = "0.1.0" } radroots-sql-wasm-core = { path = "crates/sql-wasm-core", version = "0.1.0", default-features = false } radroots-sql-core = { path = "crates/sql-core", version = "0.1.0" } -radroots-tangle-db-schema = { path = "crates/tangle-db-schema", version = "0.1.0", default-features = false } -radroots-tangle-events = { path = "crates/tangle-events", version = "0.1.0", default-features = false } -radroots-tangle-db = { path = "crates/tangle-db", version = "0.1.0", default-features = false } -radroots-tangle-db-wasm = { path = "crates/tangle-db-wasm", version = "0.1.0" } -radroots-tangle-events-wasm = { path = "crates/tangle-events-wasm", version = "0.1.0" } +radroots-replica-db-schema = { path = "crates/replica-db-schema", version = "0.1.0", default-features = false } +radroots-replica-sync = { path = "crates/replica-sync", version = "0.1.0", default-features = false } +radroots-replica-db = { path = "crates/replica-db", version = "0.1.0", default-features = false } +radroots-replica-db-wasm = { path = "crates/replica-db-wasm", version = "0.1.0" } +radroots-replica-sync-wasm = { path = "crates/replica-sync-wasm", version = "0.1.0" } radroots-trade = { path = "crates/trade", version = "0.1.0", default-features = false } radroots-types = { path = "crates/types", version = "0.1.0", default-features = false } diff --git a/Makefile b/Makefile @@ -1,13 +1,13 @@ .PHONY: all build clean help export-ts-sdk-bindings \ - build-events-codec-wasm build-tangle-db-wasm build-tangle-events-wasm + build-events-codec-wasm build-replica-db-wasm build-replica-sync-wasm SHELL := /bin/bash .SHELLFLAGS := -e -o pipefail -c BUILD_TARGETS := \ build-events-codec-wasm \ - build-tangle-db-wasm \ - build-tangle-events-wasm + build-replica-db-wasm \ + build-replica-sync-wasm all: build @@ -28,14 +28,14 @@ help: export-ts-sdk-bindings: cargo run -q -p xtask -- sdk export-ts -build-tangle-db-wasm: - wasm-pack build crates/tangle-db-wasm --release --target web \ - --out-dir ../tangle-db-wasm/pkg/dist --scope radroots +build-replica-db-wasm: + wasm-pack build crates/replica-db-wasm --release --target web \ + --out-dir ../replica-db-wasm/pkg/dist --scope radroots build-events-codec-wasm: wasm-pack build crates/events-codec-wasm --release --target web \ --out-dir ../events-codec-wasm/pkg/dist --scope radroots -build-tangle-events-wasm: - wasm-pack build crates/tangle-events-wasm --release --target web \ - --out-dir ../tangle-events-wasm/pkg/dist --scope radroots +build-replica-sync-wasm: + wasm-pack build crates/replica-sync-wasm --release --target web \ + --out-dir ../replica-sync-wasm/pkg/dist --scope radroots diff --git a/contract/coverage/required-crates.toml b/contract/coverage/required-crates.toml @@ -7,7 +7,7 @@ crates = [ "radroots-trade", "radroots-events-codec", "radroots-events-codec-wasm", - "radroots-tangle-db-schema", + "radroots-replica-db-schema", "radroots-app-core", "radroots-app-wasm", "radroots-events-indexed", @@ -22,10 +22,10 @@ crates = [ "radroots-sql-core", "radroots-sql-wasm-core", "radroots-sql-wasm-bridge", - "radroots-tangle-events", - "radroots-tangle-db", - "radroots-tangle-events-wasm", - "radroots-tangle-db-wasm", + "radroots-replica-sync", + "radroots-replica-db", + "radroots-replica-sync-wasm", + "radroots-replica-db-wasm", ] [policy] diff --git a/contract/coverage/rollout.toml b/contract/coverage/rollout.toml @@ -44,7 +44,7 @@ status = "required" order = 7 [[rollout.crates]] -name = "radroots-tangle-db-schema" +name = "radroots-replica-db-schema" status = "required" order = 8 @@ -124,21 +124,21 @@ status = "required" order = 23 [[rollout.crates]] -name = "radroots-tangle-events" +name = "radroots-replica-sync" status = "required" order = 24 [[rollout.crates]] -name = "radroots-tangle-db" +name = "radroots-replica-db" status = "required" order = 25 [[rollout.crates]] -name = "radroots-tangle-events-wasm" +name = "radroots-replica-sync-wasm" status = "required" order = 26 [[rollout.crates]] -name = "radroots-tangle-db-wasm" +name = "radroots-replica-db-wasm" status = "required" order = 27 diff --git a/contract/release/publish-set.toml b/contract/release/publish-set.toml @@ -28,11 +28,11 @@ crates = [ "radroots-sql-core", "radroots-sql-wasm-bridge", "radroots-sql-wasm-core", - "radroots-tangle-db", - "radroots-tangle-db-schema", - "radroots-tangle-db-wasm", - "radroots-tangle-events", - "radroots-tangle-events-wasm", + "radroots-replica-db", + "radroots-replica-db-schema", + "radroots-replica-db-wasm", + "radroots-replica-sync", + "radroots-replica-sync-wasm", "xtask", ] diff --git a/crates/replica-db-schema/Cargo.toml b/crates/replica-db-schema/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "radroots-replica-db-schema" +version.workspace = true +edition.workspace = true +authors = ["Radroots Authors"] +rust-version.workspace = true +license.workspace = true +publish = false +build = "build.rs" + +[lib] +crate-type = ["rlib"] + +[features] +default = [] +ts-rs = ["dep:ts-rs"] + +[dependencies] +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +ts-rs = { workspace = true, optional = true } +radroots-types = { workspace = true } diff --git a/crates/replica-db-schema/build.rs b/crates/replica-db-schema/build.rs @@ -0,0 +1,39 @@ +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +fn workspace_root(manifest_dir: &Path) -> PathBuf { + let parent = manifest_dir.parent().unwrap_or(manifest_dir); + if parent.file_name().and_then(|name| name.to_str()) == Some("crates") { + parent.parent().unwrap_or(parent).to_path_buf() + } else { + parent.to_path_buf() + } +} + +fn export_dir(crate_name: &str) -> PathBuf { + if let Some(export_dir) = env::var_os("RADROOTS_TS_RS_EXPORT_DIR") { + return PathBuf::from(export_dir); + } + let manifest_dir = PathBuf::from( + env::var("CARGO_MANIFEST_DIR").expect("missing required env var CARGO_MANIFEST_DIR"), + ); + workspace_root(&manifest_dir) + .join("target") + .join("ts-rs") + .join(crate_name) +} + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + if env::var_os("CARGO_FEATURE_TS_RS").is_some() { + let out_dir = export_dir("replica-db-schema"); + println!("cargo:rustc-env=TS_RS_EXPORT_DIR={}", out_dir.display()); + println!("cargo:rerun-if-env-changed=RADROOTS_TS_RS_EXPORT_DIR"); + if !out_dir.exists() { + fs::create_dir_all(&out_dir).expect("create TS export dir"); + } + println!("cargo:rerun-if-changed=src"); + } +} diff --git a/crates/tangle-db-schema/src/lib.rs b/crates/replica-db-schema/src/lib.rs diff --git a/crates/tangle-db-schema/src/models/farm.rs b/crates/replica-db-schema/src/models/farm.rs diff --git a/crates/tangle-db-schema/src/models/farm_gcs_location.rs b/crates/replica-db-schema/src/models/farm_gcs_location.rs diff --git a/crates/tangle-db-schema/src/models/farm_member.rs b/crates/replica-db-schema/src/models/farm_member.rs diff --git a/crates/tangle-db-schema/src/models/farm_member_claim.rs b/crates/replica-db-schema/src/models/farm_member_claim.rs diff --git a/crates/tangle-db-schema/src/models/farm_tag.rs b/crates/replica-db-schema/src/models/farm_tag.rs diff --git a/crates/tangle-db-schema/src/models/gcs_location.rs b/crates/replica-db-schema/src/models/gcs_location.rs diff --git a/crates/tangle-db-schema/src/models/log_error.rs b/crates/replica-db-schema/src/models/log_error.rs diff --git a/crates/tangle-db-schema/src/models/media_image.rs b/crates/replica-db-schema/src/models/media_image.rs diff --git a/crates/tangle-db-schema/src/models/mod.rs b/crates/replica-db-schema/src/models/mod.rs diff --git a/crates/tangle-db-schema/src/models/nostr_event_state.rs b/crates/replica-db-schema/src/models/nostr_event_state.rs diff --git a/crates/tangle-db-schema/src/models/nostr_profile.rs b/crates/replica-db-schema/src/models/nostr_profile.rs diff --git a/crates/tangle-db-schema/src/models/nostr_profile_relay.rs b/crates/replica-db-schema/src/models/nostr_profile_relay.rs diff --git a/crates/tangle-db-schema/src/models/nostr_relay.rs b/crates/replica-db-schema/src/models/nostr_relay.rs diff --git a/crates/tangle-db-schema/src/models/plot.rs b/crates/replica-db-schema/src/models/plot.rs diff --git a/crates/tangle-db-schema/src/models/plot_gcs_location.rs b/crates/replica-db-schema/src/models/plot_gcs_location.rs diff --git a/crates/tangle-db-schema/src/models/plot_tag.rs b/crates/replica-db-schema/src/models/plot_tag.rs diff --git a/crates/tangle-db-schema/src/models/trade_product.rs b/crates/replica-db-schema/src/models/trade_product.rs diff --git a/crates/tangle-db-schema/src/models/trade_product_location.rs b/crates/replica-db-schema/src/models/trade_product_location.rs diff --git a/crates/tangle-db-schema/src/models/trade_product_media.rs b/crates/replica-db-schema/src/models/trade_product_media.rs diff --git a/crates/replica-db-schema/tests/query_bind_values.rs b/crates/replica-db-schema/tests/query_bind_values.rs @@ -0,0 +1,354 @@ +use radroots_replica_db_schema::farm::FarmQueryBindValues; +use radroots_replica_db_schema::farm_gcs_location::FarmGcsLocationQueryBindValues; +use radroots_replica_db_schema::farm_member::FarmMemberQueryBindValues; +use radroots_replica_db_schema::farm_member_claim::FarmMemberClaimQueryBindValues; +use radroots_replica_db_schema::farm_tag::FarmTagQueryBindValues; +use radroots_replica_db_schema::gcs_location::GcsLocationQueryBindValues; +use radroots_replica_db_schema::log_error::LogErrorQueryBindValues; +use radroots_replica_db_schema::media_image::MediaImageQueryBindValues; +use radroots_replica_db_schema::nostr_event_state::NostrEventStateQueryBindValues; +use radroots_replica_db_schema::nostr_profile::NostrProfileQueryBindValues; +use radroots_replica_db_schema::nostr_relay::NostrRelayQueryBindValues; +use radroots_replica_db_schema::plot::PlotQueryBindValues; +use radroots_replica_db_schema::plot_gcs_location::PlotGcsLocationQueryBindValues; +use radroots_replica_db_schema::plot_tag::PlotTagQueryBindValues; +use radroots_replica_db_schema::trade_product::TradeProductQueryBindValues; +use serde_json::Value; + +macro_rules! assert_query_bind_values { + ($test_name:ident, $id_expr:expr, $id_param:literal, $id_lookup:literal, [$(($expr:expr, $param:literal, $lookup:literal)),* $(,)?]) => { + #[test] + fn $test_name() { + let id_case = $id_expr; + let (id_param, id_value) = id_case.to_filter_param(); + assert_eq!(id_param, $id_param); + assert_eq!(id_value, Value::from($id_lookup.to_string())); + assert_eq!(id_case.primary_key(), Some($id_lookup.to_string())); + assert_eq!(id_case.lookup_key(), $id_lookup.to_string()); + + $( + let alt_case = $expr; + let (alt_param, alt_value) = alt_case.to_filter_param(); + assert_eq!(alt_param, $param); + assert_eq!(alt_value, Value::from($lookup.to_string())); + assert_eq!(alt_case.primary_key(), None); + assert_eq!(alt_case.lookup_key(), $lookup.to_string()); + )* + } + }; +} + +assert_query_bind_values!( + farm_query_bind_values_cover_all_variants, + FarmQueryBindValues::Id { + id: "farm-id".to_string() + }, + "id", + "farm-id", + [ + ( + FarmQueryBindValues::DTag { + d_tag: "farm-d".to_string() + }, + "d_tag", + "farm-d" + ), + ( + FarmQueryBindValues::Pubkey { + pubkey: "farm-pk".to_string() + }, + "pubkey", + "farm-pk" + ), + ] +); + +assert_query_bind_values!( + farm_gcs_location_query_bind_values_cover_all_variants, + FarmGcsLocationQueryBindValues::Id { + id: "farm-gcs-id".to_string() + }, + "id", + "farm-gcs-id", + [ + ( + FarmGcsLocationQueryBindValues::FarmId { + farm_id: "farm-id".to_string() + }, + "farm_id", + "farm-id" + ), + ( + FarmGcsLocationQueryBindValues::GcsLocationId { + gcs_location_id: "gcs-id".to_string() + }, + "gcs_location_id", + "gcs-id" + ), + ] +); + +assert_query_bind_values!( + farm_member_query_bind_values_cover_all_variants, + FarmMemberQueryBindValues::Id { + id: "farm-member-id".to_string() + }, + "id", + "farm-member-id", + [ + ( + FarmMemberQueryBindValues::FarmId { + farm_id: "farm-id".to_string() + }, + "farm_id", + "farm-id" + ), + ( + FarmMemberQueryBindValues::MemberPubkey { + member_pubkey: "member-pk".to_string() + }, + "member_pubkey", + "member-pk" + ), + ] +); + +assert_query_bind_values!( + farm_member_claim_query_bind_values_cover_all_variants, + FarmMemberClaimQueryBindValues::Id { + id: "farm-member-claim-id".to_string() + }, + "id", + "farm-member-claim-id", + [ + ( + FarmMemberClaimQueryBindValues::MemberPubkey { + member_pubkey: "member-pk".to_string() + }, + "member_pubkey", + "member-pk" + ), + ( + FarmMemberClaimQueryBindValues::FarmPubkey { + farm_pubkey: "farm-pk".to_string() + }, + "farm_pubkey", + "farm-pk" + ), + ] +); + +assert_query_bind_values!( + farm_tag_query_bind_values_cover_all_variants, + FarmTagQueryBindValues::Id { + id: "farm-tag-id".to_string() + }, + "id", + "farm-tag-id", + [ + ( + FarmTagQueryBindValues::FarmId { + farm_id: "farm-id".to_string() + }, + "farm_id", + "farm-id" + ), + ( + FarmTagQueryBindValues::Tag { + tag: "organic".to_string() + }, + "tag", + "organic" + ), + ] +); + +assert_query_bind_values!( + gcs_location_query_bind_values_cover_all_variants, + GcsLocationQueryBindValues::Id { + id: "gcs-location-id".to_string() + }, + "id", + "gcs-location-id", + [ + ( + GcsLocationQueryBindValues::DTag { + d_tag: "gcs-d".to_string() + }, + "d_tag", + "gcs-d" + ), + ( + GcsLocationQueryBindValues::Geohash { + geohash: "9q8yy".to_string() + }, + "geohash", + "9q8yy" + ), + ] +); + +assert_query_bind_values!( + log_error_query_bind_values_cover_all_variants, + LogErrorQueryBindValues::Id { + id: "log-error-id".to_string() + }, + "id", + "log-error-id", + [( + LogErrorQueryBindValues::NostrPubkey { + nostr_pubkey: "nostr-pk".to_string() + }, + "nostr_pubkey", + "nostr-pk" + ),] +); + +assert_query_bind_values!( + media_image_query_bind_values_cover_all_variants, + MediaImageQueryBindValues::Id { + id: "media-image-id".to_string() + }, + "id", + "media-image-id", + [( + MediaImageQueryBindValues::FilePath { + file_path: "/tmp/a.jpg".to_string() + }, + "file_path", + "/tmp/a.jpg" + ),] +); + +assert_query_bind_values!( + nostr_event_state_query_bind_values_cover_all_variants, + NostrEventStateQueryBindValues::Id { + id: "nostr-event-state-id".to_string() + }, + "id", + "nostr-event-state-id", + [( + NostrEventStateQueryBindValues::Key { + key: "event-key".to_string() + }, + "key", + "event-key" + ),] +); + +assert_query_bind_values!( + nostr_profile_query_bind_values_cover_all_variants, + NostrProfileQueryBindValues::Id { + id: "nostr-profile-id".to_string() + }, + "id", + "nostr-profile-id", + [( + NostrProfileQueryBindValues::PublicKey { + public_key: "nostr-public-key".to_string() + }, + "public_key", + "nostr-public-key" + ),] +); + +assert_query_bind_values!( + nostr_relay_query_bind_values_cover_all_variants, + NostrRelayQueryBindValues::Id { + id: "nostr-relay-id".to_string() + }, + "id", + "nostr-relay-id", + [( + NostrRelayQueryBindValues::Url { + url: "wss://relay.example.com".to_string() + }, + "url", + "wss://relay.example.com" + ),] +); + +assert_query_bind_values!( + plot_query_bind_values_cover_all_variants, + PlotQueryBindValues::Id { + id: "plot-id".to_string() + }, + "id", + "plot-id", + [ + ( + PlotQueryBindValues::DTag { + d_tag: "plot-d".to_string() + }, + "d_tag", + "plot-d" + ), + ( + PlotQueryBindValues::FarmId { + farm_id: "farm-id".to_string() + }, + "farm_id", + "farm-id" + ), + ] +); + +assert_query_bind_values!( + plot_gcs_location_query_bind_values_cover_all_variants, + PlotGcsLocationQueryBindValues::Id { + id: "plot-gcs-id".to_string() + }, + "id", + "plot-gcs-id", + [ + ( + PlotGcsLocationQueryBindValues::PlotId { + plot_id: "plot-id".to_string() + }, + "plot_id", + "plot-id" + ), + ( + PlotGcsLocationQueryBindValues::GcsLocationId { + gcs_location_id: "gcs-id".to_string() + }, + "gcs_location_id", + "gcs-id" + ), + ] +); + +assert_query_bind_values!( + plot_tag_query_bind_values_cover_all_variants, + PlotTagQueryBindValues::Id { + id: "plot-tag-id".to_string() + }, + "id", + "plot-tag-id", + [ + ( + PlotTagQueryBindValues::PlotId { + plot_id: "plot-id".to_string() + }, + "plot_id", + "plot-id" + ), + ( + PlotTagQueryBindValues::Tag { + tag: "steep".to_string() + }, + "tag", + "steep" + ), + ] +); + +assert_query_bind_values!( + trade_product_query_bind_values_cover_all_variants, + TradeProductQueryBindValues::Id { + id: "trade-product-id".to_string() + }, + "id", + "trade-product-id", + [] +); diff --git a/crates/replica-db-wasm/Cargo.toml b/crates/replica-db-wasm/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "radroots-replica-db-wasm" +version.workspace = true +edition.workspace = true +authors = ["Radroots Authors"] +rust-version.workspace = true +license.workspace = true +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +radroots-sql-core = { workspace = true, features = ["bridge"] } +radroots-sql-wasm-core = { workspace = true, default-features = false, features = ["bridge"] } +radroots-replica-db = { workspace = true } +radroots-replica-db-schema = { workspace = true } +radroots-replica-sync = { workspace = true } +js-sys = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +serde-wasm-bindgen = { workspace = true } +wasm-bindgen = { workspace = true } + +[dev-dependencies] +wasm-bindgen-test = { workspace = true } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/replica-db-wasm/pkg/package.json b/crates/replica-db-wasm/pkg/package.json @@ -0,0 +1,19 @@ +{ + "name": "@radroots/replica-db-wasm", + "version": "0.1.0", + "private": true, + "type": "module", + "files": [ + "dist" + ], + "main": "./dist/radroots_replica_db_wasm.js", + "types": "./dist/radroots_replica_db_wasm.d.ts", + "exports": { + ".": { + "types": "./dist/radroots_replica_db_wasm.d.ts", + "import": "./dist/radroots_replica_db_wasm.js", + "default": "./dist/radroots_replica_db_wasm.js" + } + }, + "sideEffects": false +} +\ No newline at end of file diff --git a/crates/replica-db-wasm/src/lib.rs b/crates/replica-db-wasm/src/lib.rs @@ -0,0 +1,27 @@ +#![cfg(any(target_arch = "wasm32", coverage_nightly))] +#![forbid(unsafe_code)] + +#[cfg(target_arch = "wasm32")] +mod wasm_impl; +#[cfg(target_arch = "wasm32")] +pub use wasm_impl::*; + +#[cfg(coverage_nightly)] +pub fn coverage_branch_probe(input: bool) -> &'static str { + if input { + "replica-db-wasm" + } else { + "replica-db-wasm" + } +} + +#[cfg(all(test, coverage_nightly))] +mod tests { + use super::coverage_branch_probe; + + #[test] + fn coverage_branch_probe_hits_both_paths() { + assert_eq!(coverage_branch_probe(true), "replica-db-wasm"); + assert_eq!(coverage_branch_probe(false), "replica-db-wasm"); + } +} diff --git a/crates/tangle-db-wasm/src/utils.rs b/crates/replica-db-wasm/src/utils.rs diff --git a/crates/replica-db-wasm/src/wasm_impl.rs b/crates/replica-db-wasm/src/wasm_impl.rs @@ -0,0 +1,884 @@ +use radroots_sql_core::{ + WasmSqlExecutor, export_lock_begin, export_lock_end, with_export_lock_bypass, +}; +use radroots_sql_wasm_core::{err_js, parse_json}; +use radroots_replica_db::migrations; +use radroots_replica_db::{TangleDbExportManifestRs, export_manifest}; +use radroots_replica_sync::radroots_replica_sync_status; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::*; + +use radroots_replica_db_schema::farm::{ + IFarmCreate, IFarmDelete, IFarmFindMany, IFarmFindOne, IFarmUpdate, +}; + +use radroots_replica_db_schema::farm_gcs_location::{ + IFarmGcsLocationCreate, IFarmGcsLocationDelete, IFarmGcsLocationFindMany, + IFarmGcsLocationFindOne, IFarmGcsLocationUpdate, +}; + +use radroots_replica_db_schema::farm_member::{ + IFarmMemberCreate, IFarmMemberDelete, IFarmMemberFindMany, IFarmMemberFindOne, + IFarmMemberUpdate, +}; + +use radroots_replica_db_schema::farm_member_claim::{ + IFarmMemberClaimCreate, IFarmMemberClaimDelete, IFarmMemberClaimFindMany, + IFarmMemberClaimFindOne, IFarmMemberClaimUpdate, +}; + +use radroots_replica_db_schema::farm_tag::{ + IFarmTagCreate, IFarmTagDelete, IFarmTagFindMany, IFarmTagFindOne, IFarmTagUpdate, +}; + +use radroots_replica_db_schema::gcs_location::{ + IGcsLocationCreate, IGcsLocationDelete, IGcsLocationFindMany, IGcsLocationFindOne, + IGcsLocationUpdate, +}; + +use radroots_replica_db_schema::log_error::{ + ILogErrorCreate, ILogErrorDelete, ILogErrorFindMany, ILogErrorFindOne, ILogErrorUpdate, +}; + +use radroots_replica_db_schema::media_image::{ + IMediaImageCreate, IMediaImageDelete, IMediaImageFindMany, IMediaImageFindOne, + IMediaImageUpdate, +}; + +use radroots_replica_db_schema::nostr_profile::{ + INostrProfileCreate, INostrProfileDelete, INostrProfileFindMany, INostrProfileFindOne, + INostrProfileUpdate, +}; + +use radroots_replica_db_schema::nostr_event_state::{ + INostrEventStateCreate, INostrEventStateDelete, INostrEventStateFindMany, + INostrEventStateFindOne, INostrEventStateUpdate, +}; + +use radroots_replica_db_schema::nostr_relay::{ + INostrRelayCreate, INostrRelayDelete, INostrRelayFindMany, INostrRelayFindOne, + INostrRelayUpdate, +}; + +use radroots_replica_db_schema::trade_product::{ + ITradeProductCreate, ITradeProductDelete, ITradeProductFindMany, ITradeProductFindOne, + ITradeProductUpdate, +}; + +use radroots_replica_db_schema::plot::{ + IPlotCreate, IPlotDelete, IPlotFindMany, IPlotFindOne, IPlotUpdate, +}; + +use radroots_replica_db_schema::plot_gcs_location::{ + IPlotGcsLocationCreate, IPlotGcsLocationDelete, IPlotGcsLocationFindMany, + IPlotGcsLocationFindOne, IPlotGcsLocationUpdate, +}; + +use radroots_replica_db_schema::plot_tag::{ + IPlotTagCreate, IPlotTagDelete, IPlotTagFindMany, IPlotTagFindOne, IPlotTagUpdate, +}; + +use radroots_replica_db_schema::nostr_profile_relay::INostrProfileRelayRelation; + +use radroots_replica_db_schema::trade_product_location::ITradeProductLocationRelation; + +use radroots_replica_db_schema::trade_product_media::ITradeProductMediaRelation; + +pub mod utils; +pub use utils::*; + +#[wasm_bindgen(js_name = replica_db_run_migrations)] +pub fn replica_db_run_migrations() -> Result<(), JsValue> { + let exec = WasmSqlExecutor::new(); + migrations::run_all_up(&exec).map_err(err_js) +} + +#[wasm_bindgen(js_name = replica_db_reset_database)] +pub fn replica_db_reset_database() -> Result<(), JsValue> { + let exec = WasmSqlExecutor::new(); + migrations::run_all_down(&exec).map_err(err_js) +} + +#[wasm_bindgen(js_name = replica_db_export_json)] +pub fn replica_db_export_json() -> Result<JsValue, JsValue> { + let exec = WasmSqlExecutor::new(); + let dump = radroots_replica_db::backup::export_database_backup(&exec).map_err(err_js)?; + value_to_js(dump) +} + +#[wasm_bindgen(js_name = replica_db_import_json)] +pub fn replica_db_import_json(dump_json: &str) -> Result<(), JsValue> { + let exec = WasmSqlExecutor::new(); + radroots_replica_db::backup::restore_database_backup_json(&exec, dump_json).map_err(err_js) +} + +#[wasm_bindgen(js_name = replica_db_export_begin)] +pub fn replica_db_export_begin() -> Result<JsValue, JsValue> { + export_lock_begin().map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let result = with_export_lock_bypass(|| export_snapshot(&exec)); + match result { + Ok(value) => Ok(value), + Err(err) => { + export_lock_end(); + Err(err) + } + } +} + +#[wasm_bindgen(js_name = replica_db_export_finish)] +pub fn replica_db_export_finish() -> Result<(), JsValue> { + export_lock_end(); + Ok(()) +} + +fn export_snapshot(exec: &WasmSqlExecutor) -> Result<JsValue, JsValue> { + let status = radroots_replica_sync_status(exec).map_err(|err| { + err_js(radroots_sql_core::SqlError::InvalidArgument( + err.to_string(), + )) + })?; + if status.pending_count > 0 { + return Err(err_js(radroots_sql_core::SqlError::InvalidArgument( + format!( + "tangle db export requires synced state (pending {}/{})", + status.pending_count, status.expected_count + ), + ))); + } + let manifest = export_manifest(exec).map_err(err_js)?; + export_snapshot_value(manifest) +} + +fn export_snapshot_value(manifest: TangleDbExportManifestRs) -> Result<JsValue, JsValue> { + let bytes_js = radroots_sql_wasm_core::export_bytes(); + export_snapshot_value_with_bytes(manifest, bytes_js) +} + +fn export_snapshot_value_with_bytes( + manifest: TangleDbExportManifestRs, + bytes_js: JsValue, +) -> Result<JsValue, JsValue> { + let manifest_js = serde_wasm_bindgen::to_value(&manifest).map_err(|err| { + err_js(radroots_sql_core::SqlError::SerializationError( + err.to_string(), + )) + })?; + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &JsValue::from_str("manifest_rs"), &manifest_js) + .map_err(|_| err_js(radroots_sql_core::SqlError::Internal))?; + js_sys::Reflect::set(&obj, &JsValue::from_str("db_bytes"), &bytes_js) + .map_err(|_| err_js(radroots_sql_core::SqlError::Internal))?; + Ok(JsValue::from(obj)) +} + +#[cfg(all(test, target_arch = "wasm32"))] +mod tests { + use super::export_snapshot_value_with_bytes; + use js_sys::{Reflect, Uint8Array}; + use wasm_bindgen::JsValue; + + #[wasm_bindgen_test::wasm_bindgen_test] + fn export_snapshot_value_includes_fields() { + let manifest = radroots_replica_db::TangleDbExportManifestRs { + export_version: "1".to_string(), + replica_db_version: "0.0.0".to_string(), + backup_format_version: "0.0.0".to_string(), + schema_hash: "hash".to_string(), + schema: Vec::new(), + migrations: Vec::new(), + table_counts: Vec::new(), + }; + let bytes = Uint8Array::new_with_length(2); + let js = + export_snapshot_value_with_bytes(manifest, JsValue::from(bytes)).expect("snapshot"); + let manifest_rs = + Reflect::get(&js, &JsValue::from_str("manifest_rs")).expect("manifest_rs"); + let db_bytes = Reflect::get(&js, &JsValue::from_str("db_bytes")).expect("db_bytes"); + assert!(manifest_rs.is_object()); + assert!(db_bytes.is_object()); + } +} + +#[wasm_bindgen(js_name = replica_db_farm_create)] +pub fn replica_db_farm_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::farm::create(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_find_one)] +pub fn replica_db_farm_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::farm::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_find_many)] +pub fn replica_db_farm_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::farm::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_update)] +pub fn replica_db_farm_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::farm::update(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_delete)] +pub fn replica_db_farm_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::farm::delete(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_plot_create)] +pub fn replica_db_plot_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::plot::create(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_plot_find_one)] +pub fn replica_db_plot_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::plot::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_plot_find_many)] +pub fn replica_db_plot_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::plot::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_plot_update)] +pub fn replica_db_plot_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::plot::update(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_plot_delete)] +pub fn replica_db_plot_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::plot::delete(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_gcs_location_create)] +pub fn replica_db_gcs_location_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IGcsLocationCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::gcs_location::create(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_gcs_location_find_one)] +pub fn replica_db_gcs_location_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IGcsLocationFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::gcs_location::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_gcs_location_find_many)] +pub fn replica_db_gcs_location_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IGcsLocationFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::gcs_location::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_gcs_location_update)] +pub fn replica_db_gcs_location_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IGcsLocationUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::gcs_location::update(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_gcs_location_delete)] +pub fn replica_db_gcs_location_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IGcsLocationDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::gcs_location::delete(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_gcs_location_create)] +pub fn replica_db_farm_gcs_location_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmGcsLocationCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::farm_gcs_location::create(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_gcs_location_find_one)] +pub fn replica_db_farm_gcs_location_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmGcsLocationFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::farm_gcs_location::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_gcs_location_find_many)] +pub fn replica_db_farm_gcs_location_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmGcsLocationFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::farm_gcs_location::find_many(&exec, &opts) + .map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_gcs_location_update)] +pub fn replica_db_farm_gcs_location_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmGcsLocationUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::farm_gcs_location::update(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_gcs_location_delete)] +pub fn replica_db_farm_gcs_location_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmGcsLocationDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::farm_gcs_location::delete(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_plot_gcs_location_create)] +pub fn replica_db_plot_gcs_location_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotGcsLocationCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::plot_gcs_location::create(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_plot_gcs_location_find_one)] +pub fn replica_db_plot_gcs_location_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotGcsLocationFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::plot_gcs_location::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_plot_gcs_location_find_many)] +pub fn replica_db_plot_gcs_location_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotGcsLocationFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::plot_gcs_location::find_many(&exec, &opts) + .map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_plot_gcs_location_update)] +pub fn replica_db_plot_gcs_location_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotGcsLocationUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::plot_gcs_location::update(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_plot_gcs_location_delete)] +pub fn replica_db_plot_gcs_location_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotGcsLocationDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::plot_gcs_location::delete(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_tag_create)] +pub fn replica_db_farm_tag_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmTagCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::farm_tag::create(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_tag_find_one)] +pub fn replica_db_farm_tag_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmTagFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::farm_tag::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_tag_find_many)] +pub fn replica_db_farm_tag_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmTagFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::farm_tag::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_tag_update)] +pub fn replica_db_farm_tag_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmTagUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::farm_tag::update(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_tag_delete)] +pub fn replica_db_farm_tag_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmTagDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::farm_tag::delete(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_plot_tag_create)] +pub fn replica_db_plot_tag_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotTagCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::plot_tag::create(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_plot_tag_find_one)] +pub fn replica_db_plot_tag_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotTagFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::plot_tag::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_plot_tag_find_many)] +pub fn replica_db_plot_tag_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotTagFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::plot_tag::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_plot_tag_update)] +pub fn replica_db_plot_tag_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotTagUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::plot_tag::update(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_plot_tag_delete)] +pub fn replica_db_plot_tag_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotTagDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::plot_tag::delete(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_member_create)] +pub fn replica_db_farm_member_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::farm_member::create(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_member_find_one)] +pub fn replica_db_farm_member_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::farm_member::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_member_find_many)] +pub fn replica_db_farm_member_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::farm_member::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_member_update)] +pub fn replica_db_farm_member_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::farm_member::update(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_member_delete)] +pub fn replica_db_farm_member_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::farm_member::delete(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_member_claim_create)] +pub fn replica_db_farm_member_claim_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberClaimCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::farm_member_claim::create(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_member_claim_find_one)] +pub fn replica_db_farm_member_claim_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberClaimFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::farm_member_claim::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_member_claim_find_many)] +pub fn replica_db_farm_member_claim_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberClaimFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::farm_member_claim::find_many(&exec, &opts) + .map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_member_claim_update)] +pub fn replica_db_farm_member_claim_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberClaimUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::farm_member_claim::update(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_farm_member_claim_delete)] +pub fn replica_db_farm_member_claim_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberClaimDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::farm_member_claim::delete(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_log_error_create)] +pub fn replica_db_log_error_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ILogErrorCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::log_error::create(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_log_error_find_one)] +pub fn replica_db_log_error_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ILogErrorFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::log_error::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_log_error_find_many)] +pub fn replica_db_log_error_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ILogErrorFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::log_error::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_log_error_update)] +pub fn replica_db_log_error_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ILogErrorUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::log_error::update(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_log_error_delete)] +pub fn replica_db_log_error_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ILogErrorDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::log_error::delete(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_media_image_create)] +pub fn replica_db_media_image_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IMediaImageCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::media_image::create(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_media_image_find_one)] +pub fn replica_db_media_image_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IMediaImageFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::media_image::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_media_image_find_many)] +pub fn replica_db_media_image_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IMediaImageFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::media_image::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_media_image_update)] +pub fn replica_db_media_image_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IMediaImageUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::media_image::update(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_media_image_delete)] +pub fn replica_db_media_image_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IMediaImageDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::media_image::delete(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_profile_create)] +pub fn replica_db_nostr_profile_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrProfileCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::nostr_profile::create(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_profile_find_one)] +pub fn replica_db_nostr_profile_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrProfileFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::nostr_profile::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_profile_find_many)] +pub fn replica_db_nostr_profile_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrProfileFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::nostr_profile::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_profile_update)] +pub fn replica_db_nostr_profile_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrProfileUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::nostr_profile::update(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_profile_delete)] +pub fn replica_db_nostr_profile_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrProfileDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::nostr_profile::delete(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_event_state_create)] +pub fn replica_db_nostr_event_state_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrEventStateCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::nostr_event_state::create(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_event_state_find_one)] +pub fn replica_db_nostr_event_state_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrEventStateFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::nostr_event_state::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_event_state_find_many)] +pub fn replica_db_nostr_event_state_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrEventStateFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::nostr_event_state::find_many(&exec, &opts) + .map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_event_state_update)] +pub fn replica_db_nostr_event_state_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrEventStateUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::nostr_event_state::update(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_event_state_delete)] +pub fn replica_db_nostr_event_state_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrEventStateDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::nostr_event_state::delete(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_relay_create)] +pub fn replica_db_nostr_relay_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrRelayCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::nostr_relay::create(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_relay_find_one)] +pub fn replica_db_nostr_relay_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrRelayFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::nostr_relay::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_relay_find_many)] +pub fn replica_db_nostr_relay_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrRelayFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::nostr_relay::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_relay_update)] +pub fn replica_db_nostr_relay_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrRelayUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::nostr_relay::update(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_relay_delete)] +pub fn replica_db_nostr_relay_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrRelayDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::nostr_relay::delete(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_trade_product_create)] +pub fn replica_db_trade_product_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::trade_product::create(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_trade_product_find_one)] +pub fn replica_db_trade_product_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::trade_product::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_trade_product_find_many)] +pub fn replica_db_trade_product_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::trade_product::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_trade_product_update)] +pub fn replica_db_trade_product_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::trade_product::update(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_trade_product_delete)] +pub fn replica_db_trade_product_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::trade_product::delete(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_profile_relay_set)] +pub fn replica_db_nostr_profile_relay_set(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrProfileRelayRelation = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::nostr_profile_relay::set(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_nostr_profile_relay_unset)] +pub fn replica_db_nostr_profile_relay_unset(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrProfileRelayRelation = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::nostr_profile_relay::unset(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_trade_product_location_set)] +pub fn replica_db_trade_product_location_set(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductLocationRelation = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::trade_product_location::set(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_trade_product_location_unset)] +pub fn replica_db_trade_product_location_unset(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductLocationRelation = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_db::trade_product_location::unset(&exec, &opts) + .map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_trade_product_media_set)] +pub fn replica_db_trade_product_media_set(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductMediaRelation = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::trade_product_media::set(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_db_trade_product_media_unset)] +pub fn replica_db_trade_product_media_unset(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductMediaRelation = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_db::trade_product_media::unset(&exec, &opts).map_err(|e| err_js(e.err))?; + value_to_js(out) +} diff --git a/crates/replica-db/Cargo.toml b/crates/replica-db/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "radroots-replica-db" +version.workspace = true +edition.workspace = true +authors = ["Radroots Authors"] +rust-version.workspace = true +license.workspace = true +publish = false + +[lib] +crate-type = ["rlib"] + +[features] +default = [] +web = ["radroots-sql-core/web"] +native = ["radroots-sql-core/native"] +embedded = ["radroots-sql-core/embedded"] +coverage-minimal = [] + +[dependencies] +radroots-sql-core = { workspace = true } +radroots-replica-db-schema = { workspace = true } +radroots-types = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +hex = { workspace = true } +sha2 = { workspace = true } + +[dev-dependencies] +radroots-sql-core = { workspace = true, features = ["native"] } diff --git a/crates/tangle-db/migrations/0000_init.down.sql b/crates/replica-db/migrations/0000_init.down.sql diff --git a/crates/tangle-db/migrations/0000_init.up.sql b/crates/replica-db/migrations/0000_init.up.sql diff --git a/crates/tangle-db/migrations/0001_log_error.down.sql b/crates/replica-db/migrations/0001_log_error.down.sql diff --git a/crates/tangle-db/migrations/0001_log_error.up.sql b/crates/replica-db/migrations/0001_log_error.up.sql diff --git a/crates/tangle-db/migrations/0002_farm.down.sql b/crates/replica-db/migrations/0002_farm.down.sql diff --git a/crates/tangle-db/migrations/0002_farm.up.sql b/crates/replica-db/migrations/0002_farm.up.sql diff --git a/crates/tangle-db/migrations/0003_gcs_location.down.sql b/crates/replica-db/migrations/0003_gcs_location.down.sql diff --git a/crates/tangle-db/migrations/0003_gcs_location.up.sql b/crates/replica-db/migrations/0003_gcs_location.up.sql diff --git a/crates/tangle-db/migrations/0004_trade_product.down.sql b/crates/replica-db/migrations/0004_trade_product.down.sql diff --git a/crates/tangle-db/migrations/0004_trade_product.up.sql b/crates/replica-db/migrations/0004_trade_product.up.sql diff --git a/crates/tangle-db/migrations/0005_nostr_profile.down.sql b/crates/replica-db/migrations/0005_nostr_profile.down.sql diff --git a/crates/tangle-db/migrations/0005_nostr_profile.up.sql b/crates/replica-db/migrations/0005_nostr_profile.up.sql diff --git a/crates/tangle-db/migrations/0006_nostr_relay.down.sql b/crates/replica-db/migrations/0006_nostr_relay.down.sql diff --git a/crates/tangle-db/migrations/0006_nostr_relay.up.sql b/crates/replica-db/migrations/0006_nostr_relay.up.sql diff --git a/crates/tangle-db/migrations/0007_media_image.down.sql b/crates/replica-db/migrations/0007_media_image.down.sql diff --git a/crates/tangle-db/migrations/0007_media_image.up.sql b/crates/replica-db/migrations/0007_media_image.up.sql diff --git a/crates/tangle-db/migrations/0008_farm_gcs_location.down.sql b/crates/replica-db/migrations/0008_farm_gcs_location.down.sql diff --git a/crates/tangle-db/migrations/0008_farm_gcs_location.up.sql b/crates/replica-db/migrations/0008_farm_gcs_location.up.sql diff --git a/crates/tangle-db/migrations/0009_nostr_profile_relay.down.sql b/crates/replica-db/migrations/0009_nostr_profile_relay.down.sql diff --git a/crates/tangle-db/migrations/0009_nostr_profile_relay.up.sql b/crates/replica-db/migrations/0009_nostr_profile_relay.up.sql diff --git a/crates/tangle-db/migrations/0010_trade_product_location.down.sql b/crates/replica-db/migrations/0010_trade_product_location.down.sql diff --git a/crates/tangle-db/migrations/0010_trade_product_location.up.sql b/crates/replica-db/migrations/0010_trade_product_location.up.sql diff --git a/crates/tangle-db/migrations/0011_trade_product_media.down.sql b/crates/replica-db/migrations/0011_trade_product_media.down.sql diff --git a/crates/tangle-db/migrations/0011_trade_product_media.up.sql b/crates/replica-db/migrations/0011_trade_product_media.up.sql diff --git a/crates/tangle-db/migrations/0012_plot.down.sql b/crates/replica-db/migrations/0012_plot.down.sql diff --git a/crates/tangle-db/migrations/0012_plot.up.sql b/crates/replica-db/migrations/0012_plot.up.sql diff --git a/crates/tangle-db/migrations/0013_plot_gcs_location.down.sql b/crates/replica-db/migrations/0013_plot_gcs_location.down.sql diff --git a/crates/tangle-db/migrations/0013_plot_gcs_location.up.sql b/crates/replica-db/migrations/0013_plot_gcs_location.up.sql diff --git a/crates/tangle-db/migrations/0014_farm_tag.down.sql b/crates/replica-db/migrations/0014_farm_tag.down.sql diff --git a/crates/tangle-db/migrations/0014_farm_tag.up.sql b/crates/replica-db/migrations/0014_farm_tag.up.sql diff --git a/crates/tangle-db/migrations/0015_plot_tag.down.sql b/crates/replica-db/migrations/0015_plot_tag.down.sql diff --git a/crates/tangle-db/migrations/0015_plot_tag.up.sql b/crates/replica-db/migrations/0015_plot_tag.up.sql diff --git a/crates/tangle-db/migrations/0016_farm_member.down.sql b/crates/replica-db/migrations/0016_farm_member.down.sql diff --git a/crates/tangle-db/migrations/0016_farm_member.up.sql b/crates/replica-db/migrations/0016_farm_member.up.sql diff --git a/crates/tangle-db/migrations/0017_farm_member_claim.down.sql b/crates/replica-db/migrations/0017_farm_member_claim.down.sql diff --git a/crates/tangle-db/migrations/0017_farm_member_claim.up.sql b/crates/replica-db/migrations/0017_farm_member_claim.up.sql diff --git a/crates/tangle-db/migrations/0018_nostr_event_state.down.sql b/crates/replica-db/migrations/0018_nostr_event_state.down.sql diff --git a/crates/tangle-db/migrations/0018_nostr_event_state.up.sql b/crates/replica-db/migrations/0018_nostr_event_state.up.sql diff --git a/crates/replica-db/src/backup.rs b/crates/replica-db/src/backup.rs @@ -0,0 +1,627 @@ +use radroots_sql_core::{SqlExecutor, error::SqlError, utils}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use std::collections::{BTreeMap, HashMap}; + +pub const DATABASE_BACKUP_VERSION: &str = "1.0.0"; +pub const TANGLE_DB_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SchemaEntry { + pub object_type: String, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub table_name: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub sql: Option<String>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TableData { + pub name: String, + pub rows: Vec<Map<String, Value>>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MigrationBackup { + pub name: String, + pub up_sql: String, + pub down_sql: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DatabaseBackup { + pub format_version: String, + pub replica_db_version: String, + pub schema: Vec<SchemaEntry>, + pub migrations: Vec<MigrationBackup>, + pub data: Vec<TableData>, +} + +pub fn export_database_backup<E: SqlExecutor>(executor: &E) -> Result<DatabaseBackup, SqlError> { + let schema = load_schema(executor)?; + let data = read_tables_for_backup(executor, &schema)?; + let migrations = export_migrations(); + Ok(DatabaseBackup { + format_version: DATABASE_BACKUP_VERSION.to_string(), + replica_db_version: TANGLE_DB_VERSION.to_string(), + schema, + migrations, + data, + }) +} + +pub fn export_database_backup_json<E: SqlExecutor>(executor: &E) -> Result<String, SqlError> { + let backup = export_database_backup(executor)?; + serde_json::to_string(&backup).map_err(SqlError::from) +} + +pub fn restore_database_backup<E: SqlExecutor>( + executor: &E, + backup: &DatabaseBackup, +) -> Result<(), SqlError> { + validate_backup_version(backup)?; + executor.exec("PRAGMA foreign_keys = OFF;", "[]")?; + executor.begin()?; + let result = (|| { + drop_existing_objects(executor)?; + create_schema_from_backup(executor, &backup.schema)?; + insert_rows_from_backup(executor, backup)?; + Ok(()) + })(); + + match result { + Ok(()) => { + executor.commit()?; + let _ = executor.exec("PRAGMA foreign_keys = ON;", "[]")?; + Ok(()) + } + Err(err) => { + let _ = executor.rollback(); + let _ = executor.exec("PRAGMA foreign_keys = ON;", "[]"); + Err(err) + } + } +} + +pub fn restore_database_backup_json<E: SqlExecutor>( + executor: &E, + backup_json: &str, +) -> Result<(), SqlError> { + let backup: DatabaseBackup = serde_json::from_str(backup_json).map_err(SqlError::from)?; + restore_database_backup(executor, &backup) +} + +fn drop_existing_objects<E: SqlExecutor>(executor: &E) -> Result<(), SqlError> { + #[derive(Deserialize)] + struct MasterRow { + #[serde(rename = "type")] + object_type: Option<String>, + name: Option<String>, + } + let query = "select type, name from sqlite_master where name not like 'sqlite_%'"; + let json = executor.query_raw(query, "[]")?; + let rows: Vec<MasterRow> = utils::parse_json(&json)?; + + let mut groups: HashMap<String, Vec<String>> = HashMap::new(); + for row in rows.into_iter() { + let obj_type = row.object_type.unwrap_or_default(); + let name = match row.name { + Some(n) => n, + None => continue, + }; + groups.entry(obj_type).or_default().push(name); + } + + for object_type in ["trigger", "view", "index", "table"] { + if let Some(names) = groups.get(object_type) { + for name in names { + let stmt = match object_type { + "trigger" => format!("DROP TRIGGER IF EXISTS {};", escape_identifier(name)), + "view" => format!("DROP VIEW IF EXISTS {};", escape_identifier(name)), + "index" => format!("DROP INDEX IF EXISTS {};", escape_identifier(name)), + _ => format!("DROP TABLE IF EXISTS {};", escape_identifier(name)), + }; + let _ = executor.exec(&stmt, "[]")?; + } + } + } + Ok(()) +} + +fn create_schema_from_backup<E: SqlExecutor>( + executor: &E, + schema: &[SchemaEntry], +) -> Result<(), SqlError> { + for entry in schema.iter().filter(|s| s.object_type == "table") { + if let Some(sql) = &entry.sql { + executor.exec(sql, "[]")?; + } + } + for entry in schema.iter().filter(|s| s.object_type != "table") { + if let Some(sql) = &entry.sql { + executor.exec(sql, "[]")?; + } + } + Ok(()) +} + +fn insert_rows_from_backup<E: SqlExecutor>( + executor: &E, + backup: &DatabaseBackup, +) -> Result<(), SqlError> { + let mut row_sources: HashMap<&str, &Vec<Map<String, Value>>> = HashMap::new(); + for table in &backup.data { + row_sources.insert(table.name.as_str(), &table.rows); + } + for entry in backup.schema.iter().filter(|s| s.object_type == "table") { + let rows = match row_sources.get(entry.name.as_str()) { + Some(r) => *r, + None => continue, + }; + for row in rows { + insert_row(executor, &entry.name, row)?; + } + } + Ok(()) +} + +fn insert_row<E: SqlExecutor>( + executor: &E, + table: &str, + row: &Map<String, Value>, +) -> Result<(), SqlError> { + if row.is_empty() { + return Ok(()); + } + + let mut cols: BTreeMap<String, &Value> = BTreeMap::new(); + for (k, v) in row { + cols.insert(k.clone(), v); + } + + let column_names: Vec<String> = cols.keys().cloned().collect(); + let placeholders = (0..column_names.len()) + .map(|_| "?") + .collect::<Vec<_>>() + .join(","); + let sql = format!( + "INSERT INTO {} ({}) VALUES ({});", + escape_identifier(table), + column_names + .iter() + .map(|c| escape_identifier(c)) + .collect::<Vec<_>>() + .join(","), + placeholders + ); + + let binds: Vec<Value> = cols.values().map(|v| utils::to_db_bind_value(*v)).collect(); + let params_json = serde_json::to_string(&binds).map_err(SqlError::from)?; + executor.exec(&sql, ¶ms_json)?; + Ok(()) +} + +pub(crate) fn load_schema<E: SqlExecutor>(executor: &E) -> Result<Vec<SchemaEntry>, SqlError> { + let query = "select type, name, tbl_name as table_name, sql from sqlite_master where name not like 'sqlite_%' order by type, name"; + let json = executor.query_raw(query, "[]")?; + #[derive(Deserialize)] + struct RawSchema { + #[serde(rename = "type")] + object_type: Option<String>, + name: Option<String>, + table_name: Option<String>, + sql: Option<String>, + } + let rows: Vec<RawSchema> = utils::parse_json(&json)?; + Ok(rows + .into_iter() + .filter_map(|row| { + let name = row.name?; + let object_type = row.object_type.unwrap_or_default(); + Some(SchemaEntry { + object_type, + name, + table_name: row.table_name, + sql: row.sql, + }) + }) + .collect()) +} + +pub(crate) fn export_migrations() -> Vec<MigrationBackup> { + crate::migrations::MIGRATIONS + .iter() + .map(|m| MigrationBackup { + name: m.name.to_string(), + up_sql: m.up_sql.to_string(), + down_sql: m.down_sql.to_string(), + }) + .collect() +} + +fn read_tables_for_backup<E: SqlExecutor>( + executor: &E, + schema: &[SchemaEntry], +) -> Result<Vec<TableData>, SqlError> { + let mut data = Vec::new(); + for entry in schema.iter().filter(|s| s.object_type == "table") { + let select_sql = format!("SELECT * FROM {};", escape_identifier(&entry.name)); + let json = executor.query_raw(&select_sql, "[]")?; + let rows: Vec<Map<String, Value>> = utils::parse_json(&json)?; + data.push(TableData { + name: entry.name.clone(), + rows, + }); + } + Ok(data) +} + +pub(crate) fn escape_identifier(name: &str) -> String { + let mut escaped = String::with_capacity(name.len() + 2); + escaped.push('"'); + for c in name.chars() { + if c == '"' { + escaped.push('"'); + } + escaped.push(c); + } + escaped.push('"'); + escaped +} + +fn validate_backup_version(backup: &DatabaseBackup) -> Result<(), SqlError> { + if backup.format_version != DATABASE_BACKUP_VERSION { + return Err(SqlError::InvalidArgument(format!( + "unsupported backup format {}, expected {}", + backup.format_version, DATABASE_BACKUP_VERSION + ))); + } + if backup.replica_db_version != TANGLE_DB_VERSION { + return Err(SqlError::InvalidArgument(format!( + "unsupported replica-db version {}, expected {}", + backup.replica_db_version, TANGLE_DB_VERSION + ))); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use radroots_sql_core::ExecOutcome; + use std::sync::Mutex; + use std::sync::atomic::{AtomicUsize, Ordering}; + + struct MockExecutor { + query_rules: Vec<(String, String)>, + fail_exec_contains: Option<String>, + exec_calls: Mutex<Vec<String>>, + begin_calls: AtomicUsize, + commit_calls: AtomicUsize, + rollback_calls: AtomicUsize, + } + + impl MockExecutor { + fn new(query_rules: Vec<(String, String)>, fail_exec_contains: Option<String>) -> Self { + Self { + query_rules, + fail_exec_contains, + exec_calls: Mutex::new(Vec::new()), + begin_calls: AtomicUsize::new(0), + commit_calls: AtomicUsize::new(0), + rollback_calls: AtomicUsize::new(0), + } + } + + fn exec_calls(&self) -> Vec<String> { + self.exec_calls.lock().expect("exec calls lock").clone() + } + + fn begin_count(&self) -> usize { + self.begin_calls.load(Ordering::SeqCst) + } + + fn commit_count(&self) -> usize { + self.commit_calls.load(Ordering::SeqCst) + } + + fn rollback_count(&self) -> usize { + self.rollback_calls.load(Ordering::SeqCst) + } + } + + impl SqlExecutor for MockExecutor { + fn exec(&self, sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> { + self.exec_calls + .lock() + .expect("exec calls lock") + .push(sql.to_string()); + if let Some(needle) = &self.fail_exec_contains { + if sql.contains(needle) { + return Err(SqlError::InvalidQuery(String::from("forced exec failure"))); + } + } + Ok(ExecOutcome { + changes: 1, + last_insert_id: 1, + }) + } + + fn query_raw(&self, sql: &str, _params_json: &str) -> Result<String, SqlError> { + for (needle, response) in &self.query_rules { + if sql.contains(needle) { + return Ok(response.clone()); + } + } + Ok(String::from("[]")) + } + + fn begin(&self) -> Result<(), SqlError> { + self.begin_calls.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + + fn commit(&self) -> Result<(), SqlError> { + self.commit_calls.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + + fn rollback(&self) -> Result<(), SqlError> { + self.rollback_calls.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + } + + fn backup_with_versions(format_version: &str, replica_db_version: &str) -> DatabaseBackup { + DatabaseBackup { + format_version: format_version.to_string(), + replica_db_version: replica_db_version.to_string(), + schema: Vec::new(), + migrations: Vec::new(), + data: Vec::new(), + } + } + + #[test] + fn restore_database_backup_rolls_back_when_exec_fails() { + let executor = MockExecutor::new( + vec![( + String::from("select type, name from sqlite_master"), + String::from("[]"), + )], + Some(String::from("CREATE TABLE fail_table")), + ); + let backup = DatabaseBackup { + format_version: DATABASE_BACKUP_VERSION.to_string(), + replica_db_version: TANGLE_DB_VERSION.to_string(), + schema: vec![SchemaEntry { + object_type: String::from("table"), + name: String::from("fail_table"), + table_name: Some(String::from("fail_table")), + sql: Some(String::from("CREATE TABLE fail_table (id TEXT);")), + }], + migrations: Vec::new(), + data: Vec::new(), + }; + + let err = restore_database_backup(&executor, &backup).expect_err("restore should fail"); + assert!(matches!(err, SqlError::InvalidQuery(_))); + assert_eq!(executor.begin_count(), 1); + assert_eq!(executor.commit_count(), 0); + assert_eq!(executor.rollback_count(), 1); + let calls = executor.exec_calls(); + assert!( + calls + .iter() + .any(|sql| sql.contains("PRAGMA foreign_keys = OFF")) + ); + assert!( + calls + .iter() + .any(|sql| sql.contains("PRAGMA foreign_keys = ON")) + ); + } + + #[test] + fn drop_existing_objects_skips_rows_without_name() { + let master_rows = serde_json::json!([ + { "type": "trigger", "name": "tg_a" }, + { "type": "view", "name": "vw_a" }, + { "type": "index", "name": "ix_a" }, + { "type": "table", "name": "tb_a" }, + { "type": "table", "name": null } + ]) + .to_string(); + let executor = MockExecutor::new( + vec![( + String::from("select type, name from sqlite_master"), + master_rows, + )], + None, + ); + + drop_existing_objects(&executor).expect("drop existing objects"); + let calls = executor.exec_calls(); + assert!( + calls + .iter() + .any(|sql| sql.contains("DROP TRIGGER IF EXISTS \"tg_a\";")) + ); + assert!( + calls + .iter() + .any(|sql| sql.contains("DROP VIEW IF EXISTS \"vw_a\";")) + ); + assert!( + calls + .iter() + .any(|sql| sql.contains("DROP INDEX IF EXISTS \"ix_a\";")) + ); + assert!( + calls + .iter() + .any(|sql| sql.contains("DROP TABLE IF EXISTS \"tb_a\";")) + ); + } + + #[test] + fn create_schema_from_backup_executes_table_and_non_table_sql() { + let executor = MockExecutor::new(Vec::new(), None); + let schema = vec![ + SchemaEntry { + object_type: String::from("table"), + name: String::from("tb_a"), + table_name: Some(String::from("tb_a")), + sql: Some(String::from("CREATE TABLE tb_a (id TEXT);")), + }, + SchemaEntry { + object_type: String::from("table"), + name: String::from("tb_b"), + table_name: Some(String::from("tb_b")), + sql: None, + }, + SchemaEntry { + object_type: String::from("view"), + name: String::from("vw_a"), + table_name: Some(String::from("vw_a")), + sql: Some(String::from("CREATE VIEW vw_a AS SELECT 1;")), + }, + SchemaEntry { + object_type: String::from("index"), + name: String::from("ix_a"), + table_name: Some(String::from("ix_a")), + sql: None, + }, + ]; + + create_schema_from_backup(&executor, &schema).expect("create schema from backup"); + let calls = executor.exec_calls(); + assert!( + calls + .iter() + .any(|sql| sql == "CREATE TABLE tb_a (id TEXT);") + ); + assert!( + calls + .iter() + .any(|sql| sql == "CREATE VIEW vw_a AS SELECT 1;") + ); + assert_eq!(calls.len(), 2); + } + + #[test] + fn insert_rows_from_backup_skips_missing_data_and_empty_rows() { + let executor = MockExecutor::new(Vec::new(), None); + let mut row = Map::new(); + row.insert(String::from("co\"l"), Value::from(7)); + let backup = DatabaseBackup { + format_version: DATABASE_BACKUP_VERSION.to_string(), + replica_db_version: TANGLE_DB_VERSION.to_string(), + schema: vec![ + SchemaEntry { + object_type: String::from("table"), + name: String::from("tb_a"), + table_name: Some(String::from("tb_a")), + sql: Some(String::from("CREATE TABLE tb_a (id TEXT);")), + }, + SchemaEntry { + object_type: String::from("table"), + name: String::from("tb_b"), + table_name: Some(String::from("tb_b")), + sql: Some(String::from("CREATE TABLE tb_b (id TEXT);")), + }, + ], + migrations: Vec::new(), + data: vec![TableData { + name: String::from("tb_a"), + rows: vec![row], + }], + }; + + insert_rows_from_backup(&executor, &backup).expect("insert rows from backup"); + let calls_after_insert = executor.exec_calls(); + assert!( + calls_after_insert + .iter() + .any(|sql| sql.contains("INSERT INTO \"tb_a\" (\"co\"\"l\") VALUES (?);")) + ); + assert!( + !calls_after_insert + .iter() + .any(|sql| sql.contains("\"tb_b\"")) + ); + + let empty_row = Map::new(); + insert_row(&executor, "tb_a", &empty_row).expect("insert empty row"); + assert_eq!(executor.exec_calls().len(), calls_after_insert.len()); + assert_eq!(escape_identifier("a\"b"), "\"a\"\"b\""); + } + + #[test] + fn load_schema_filters_rows_without_name() { + let schema_rows = serde_json::json!([ + { "type": "table", "name": null, "table_name": "tb_a", "sql": "CREATE TABLE tb_a (id TEXT);" }, + { "type": "view", "name": "vw_a", "table_name": "vw_a", "sql": "CREATE VIEW vw_a AS SELECT 1;" } + ]) + .to_string(); + let executor = MockExecutor::new( + vec![( + String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), + schema_rows, + )], + None, + ); + + let rows = load_schema(&executor).expect("load schema"); + assert_eq!(rows.len(), 1); + assert_eq!(rows[0].name, "vw_a"); + assert_eq!(rows[0].object_type, "view"); + } + + #[test] + fn validate_backup_version_rejects_invalid_versions() { + let wrong_format = backup_with_versions("0.0.1", TANGLE_DB_VERSION); + let err = validate_backup_version(&wrong_format).expect_err("format version must fail"); + assert!(matches!(err, SqlError::InvalidArgument(_))); + + let wrong_db_version = backup_with_versions(DATABASE_BACKUP_VERSION, "0.0.0"); + let err = validate_backup_version(&wrong_db_version).expect_err("db version must fail"); + assert!(matches!(err, SqlError::InvalidArgument(_))); + } + + #[test] + fn restore_database_backup_commits_on_success_and_query_fallback_works() { + let executor = MockExecutor::new( + vec![( + String::from("select type, name from sqlite_master"), + String::from("[]"), + )], + None, + ); + let backup = backup_with_versions(DATABASE_BACKUP_VERSION, TANGLE_DB_VERSION); + + let matched = executor + .query_raw("select type, name from sqlite_master", "[]") + .expect("query match"); + assert_eq!(matched, "[]"); + + let fallback = executor + .query_raw("select 1", "[]") + .expect("query fallback"); + assert_eq!(fallback, "[]"); + + restore_database_backup(&executor, &backup).expect("restore should succeed"); + assert_eq!(executor.begin_count(), 1); + assert_eq!(executor.commit_count(), 1); + assert_eq!(executor.rollback_count(), 0); + } + + #[test] + fn restore_database_backup_json_rejects_invalid_json() { + let executor = MockExecutor::new(Vec::new(), None); + let err = restore_database_backup_json(&executor, "{") + .expect_err("invalid backup json should fail"); + assert!(matches!(err, SqlError::SerializationError(_))); + } +} diff --git a/crates/replica-db/src/export.rs b/crates/replica-db/src/export.rs @@ -0,0 +1,75 @@ +use radroots_sql_core::{SqlExecutor, error::SqlError, utils}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::backup::{ + DATABASE_BACKUP_VERSION, MigrationBackup, SchemaEntry, TANGLE_DB_VERSION, escape_identifier, + export_migrations, load_schema, +}; + +pub const TANGLE_DB_EXPORT_VERSION: &str = "1"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TableCount { + pub name: String, + pub row_count: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TangleDbExportManifestRs { + pub export_version: String, + pub replica_db_version: String, + pub backup_format_version: String, + pub schema_hash: String, + pub schema: Vec<SchemaEntry>, + pub migrations: Vec<MigrationBackup>, + pub table_counts: Vec<TableCount>, +} + +pub fn export_manifest<E: SqlExecutor>(executor: &E) -> Result<TangleDbExportManifestRs, SqlError> { + let schema = load_schema(executor)?; + let migrations = export_migrations(); + let table_counts = load_table_counts(executor, &schema)?; + let schema_hash = schema_hash(&schema)?; + Ok(TangleDbExportManifestRs { + export_version: TANGLE_DB_EXPORT_VERSION.to_string(), + replica_db_version: TANGLE_DB_VERSION.to_string(), + backup_format_version: DATABASE_BACKUP_VERSION.to_string(), + schema_hash, + schema, + migrations, + table_counts, + }) +} + +fn load_table_counts<E: SqlExecutor>( + executor: &E, + schema: &[SchemaEntry], +) -> Result<Vec<TableCount>, SqlError> { + #[derive(Deserialize)] + struct CountRow { + count: u64, + } + let mut counts = Vec::new(); + for entry in schema.iter().filter(|s| s.object_type == "table") { + let sql = format!( + "select count(1) as count from {}", + escape_identifier(&entry.name) + ); + let json = executor.query_raw(&sql, "[]")?; + let rows: Vec<CountRow> = utils::parse_json(&json)?; + let row_count = rows.first().map(|row| row.count).unwrap_or(0); + counts.push(TableCount { + name: entry.name.clone(), + row_count, + }); + } + Ok(counts) +} + +fn schema_hash(schema: &[SchemaEntry]) -> Result<String, SqlError> { + let json = serde_json::to_string(schema).map_err(SqlError::from)?; + let mut hasher = Sha256::new(); + hasher.update(json.as_bytes()); + Ok(hex::encode(hasher.finalize())) +} diff --git a/crates/replica-db/src/lib.rs b/crates/replica-db/src/lib.rs @@ -0,0 +1,783 @@ +#![cfg_attr(feature = "coverage-minimal", allow(unused_imports))] + +pub use radroots_sql_core::error::SqlError; +pub use radroots_sql_core::{ExecOutcome, SqlExecutor}; +use radroots_types::types::IError; + +use radroots_replica_db_schema::farm::{ + IFarmCreate, IFarmCreateResolve, IFarmDelete, IFarmDeleteResolve, IFarmFindMany, + IFarmFindManyResolve, IFarmFindOne, IFarmFindOneResolve, IFarmUpdate, IFarmUpdateResolve, +}; + +use radroots_replica_db_schema::farm_gcs_location::{ + IFarmGcsLocationCreate, IFarmGcsLocationCreateResolve, IFarmGcsLocationDelete, + IFarmGcsLocationDeleteResolve, IFarmGcsLocationFindMany, IFarmGcsLocationFindManyResolve, + IFarmGcsLocationFindOne, IFarmGcsLocationFindOneResolve, IFarmGcsLocationUpdate, + IFarmGcsLocationUpdateResolve, +}; + +use radroots_replica_db_schema::farm_member::{ + IFarmMemberCreate, IFarmMemberCreateResolve, IFarmMemberDelete, IFarmMemberDeleteResolve, + IFarmMemberFindMany, IFarmMemberFindManyResolve, IFarmMemberFindOne, IFarmMemberFindOneResolve, + IFarmMemberUpdate, IFarmMemberUpdateResolve, +}; + +use radroots_replica_db_schema::farm_member_claim::{ + IFarmMemberClaimCreate, IFarmMemberClaimCreateResolve, IFarmMemberClaimDelete, + IFarmMemberClaimDeleteResolve, IFarmMemberClaimFindMany, IFarmMemberClaimFindManyResolve, + IFarmMemberClaimFindOne, IFarmMemberClaimFindOneResolve, IFarmMemberClaimUpdate, + IFarmMemberClaimUpdateResolve, +}; + +use radroots_replica_db_schema::farm_tag::{ + IFarmTagCreate, IFarmTagCreateResolve, IFarmTagDelete, IFarmTagDeleteResolve, IFarmTagFindMany, + IFarmTagFindManyResolve, IFarmTagFindOne, IFarmTagFindOneResolve, IFarmTagUpdate, + IFarmTagUpdateResolve, +}; + +use radroots_replica_db_schema::gcs_location::{ + IGcsLocationCreate, IGcsLocationCreateResolve, IGcsLocationDelete, IGcsLocationDeleteResolve, + IGcsLocationFindMany, IGcsLocationFindManyResolve, IGcsLocationFindOne, + IGcsLocationFindOneResolve, IGcsLocationUpdate, IGcsLocationUpdateResolve, +}; + +use radroots_replica_db_schema::log_error::{ + ILogErrorCreate, ILogErrorCreateResolve, ILogErrorDelete, ILogErrorDeleteResolve, + ILogErrorFindMany, ILogErrorFindManyResolve, ILogErrorFindOne, ILogErrorFindOneResolve, + ILogErrorUpdate, ILogErrorUpdateResolve, +}; + +use radroots_replica_db_schema::media_image::{ + IMediaImageCreate, IMediaImageCreateResolve, IMediaImageDelete, IMediaImageDeleteResolve, + IMediaImageFindMany, IMediaImageFindManyResolve, IMediaImageFindOne, IMediaImageFindOneResolve, + IMediaImageUpdate, IMediaImageUpdateResolve, +}; + +use radroots_replica_db_schema::nostr_profile::{ + INostrProfileCreate, INostrProfileCreateResolve, INostrProfileDelete, + INostrProfileDeleteResolve, INostrProfileFindMany, INostrProfileFindManyResolve, + INostrProfileFindOne, INostrProfileFindOneResolve, INostrProfileUpdate, + INostrProfileUpdateResolve, +}; + +use radroots_replica_db_schema::nostr_event_state::{ + INostrEventStateCreate, INostrEventStateCreateResolve, INostrEventStateDelete, + INostrEventStateDeleteResolve, INostrEventStateFindMany, INostrEventStateFindManyResolve, + INostrEventStateFindOne, INostrEventStateFindOneResolve, INostrEventStateUpdate, + INostrEventStateUpdateResolve, +}; + +use radroots_replica_db_schema::nostr_relay::{ + INostrRelayCreate, INostrRelayCreateResolve, INostrRelayDelete, INostrRelayDeleteResolve, + INostrRelayFindMany, INostrRelayFindManyResolve, INostrRelayFindOne, INostrRelayFindOneResolve, + INostrRelayUpdate, INostrRelayUpdateResolve, +}; + +use radroots_replica_db_schema::trade_product::{ + ITradeProductCreate, ITradeProductCreateResolve, ITradeProductDelete, + ITradeProductDeleteResolve, ITradeProductFindMany, ITradeProductFindManyResolve, + ITradeProductFindOne, ITradeProductFindOneResolve, ITradeProductUpdate, + ITradeProductUpdateResolve, +}; + +use radroots_replica_db_schema::plot::{ + IPlotCreate, IPlotCreateResolve, IPlotDelete, IPlotDeleteResolve, IPlotFindMany, + IPlotFindManyResolve, IPlotFindOne, IPlotFindOneResolve, IPlotUpdate, IPlotUpdateResolve, +}; + +use radroots_replica_db_schema::plot_gcs_location::{ + IPlotGcsLocationCreate, IPlotGcsLocationCreateResolve, IPlotGcsLocationDelete, + IPlotGcsLocationDeleteResolve, IPlotGcsLocationFindMany, IPlotGcsLocationFindManyResolve, + IPlotGcsLocationFindOne, IPlotGcsLocationFindOneResolve, IPlotGcsLocationUpdate, + IPlotGcsLocationUpdateResolve, +}; + +use radroots_replica_db_schema::plot_tag::{ + IPlotTagCreate, IPlotTagCreateResolve, IPlotTagDelete, IPlotTagDeleteResolve, IPlotTagFindMany, + IPlotTagFindManyResolve, IPlotTagFindOne, IPlotTagFindOneResolve, IPlotTagUpdate, + IPlotTagUpdateResolve, +}; + +use radroots_replica_db_schema::nostr_profile_relay::{ + INostrProfileRelayRelation, INostrProfileRelayResolve, +}; + +use radroots_replica_db_schema::trade_product_location::{ + ITradeProductLocationRelation, ITradeProductLocationResolve, +}; + +use radroots_replica_db_schema::trade_product_media::{ + ITradeProductMediaRelation, ITradeProductMediaResolve, +}; + +#[cfg(not(feature = "coverage-minimal"))] +pub mod backup; +#[cfg(not(feature = "coverage-minimal"))] +pub mod export; +#[cfg(not(feature = "coverage-minimal"))] +pub mod migrations; +#[cfg(not(feature = "coverage-minimal"))] +pub mod models; +#[cfg(not(feature = "coverage-minimal"))] +pub use backup::{DatabaseBackup, MigrationBackup, SchemaEntry}; +#[cfg(not(feature = "coverage-minimal"))] +pub use export::{TANGLE_DB_EXPORT_VERSION, TableCount, TangleDbExportManifestRs, export_manifest}; +#[cfg(not(feature = "coverage-minimal"))] +pub use models::*; + +pub struct TangleSql<E: SqlExecutor> { + executor: E, +} + +impl<E: SqlExecutor> TangleSql<E> { + pub fn coverage_branch_probe(enabled: bool) -> &'static str { + if enabled { "enabled" } else { "disabled" } + } +} + +#[cfg(not(feature = "coverage-minimal"))] +impl<E: SqlExecutor> TangleSql<E> { + pub fn new(executor: E) -> Self { + Self { executor } + } + + pub fn executor(&self) -> &E { + &self.executor + } + + pub fn migrate_up(&self) -> Result<(), SqlError> { + crate::migrations::run_all_up(self.executor()) + } + + pub fn migrate_down(&self) -> Result<(), SqlError> { + crate::migrations::run_all_down(self.executor()) + } + + pub fn backup_database(&self) -> Result<DatabaseBackup, SqlError> { + crate::backup::export_database_backup(self.executor()) + } + + pub fn backup_database_json(&self) -> Result<String, SqlError> { + crate::backup::export_database_backup_json(self.executor()) + } + + pub fn restore_database(&self, backup: &DatabaseBackup) -> Result<(), SqlError> { + crate::backup::restore_database_backup(self.executor(), backup) + } + + pub fn restore_database_json(&self, backup_json: &str) -> Result<(), SqlError> { + crate::backup::restore_database_backup_json(self.executor(), backup_json) + } + + pub fn farm_create(&self, opts: &IFarmCreate) -> Result<IFarmCreateResolve, IError<SqlError>> { + models::farm::create(self.executor(), opts) + } + + pub fn farm_find_many( + &self, + opts: &IFarmFindMany, + ) -> Result<IFarmFindManyResolve, IError<SqlError>> { + models::farm::find_many(self.executor(), opts) + } + + pub fn farm_find_one( + &self, + opts: &IFarmFindOne, + ) -> Result<IFarmFindOneResolve, IError<SqlError>> { + models::farm::find_one(self.executor(), opts) + } + + pub fn farm_update(&self, opts: &IFarmUpdate) -> Result<IFarmUpdateResolve, IError<SqlError>> { + models::farm::update(self.executor(), opts) + } + + pub fn farm_delete(&self, opts: &IFarmDelete) -> Result<IFarmDeleteResolve, IError<SqlError>> { + models::farm::delete(self.executor(), opts) + } + + pub fn plot_create(&self, opts: &IPlotCreate) -> Result<IPlotCreateResolve, IError<SqlError>> { + models::plot::create(self.executor(), opts) + } + + pub fn plot_find_many( + &self, + opts: &IPlotFindMany, + ) -> Result<IPlotFindManyResolve, IError<SqlError>> { + models::plot::find_many(self.executor(), opts) + } + + pub fn plot_find_one( + &self, + opts: &IPlotFindOne, + ) -> Result<IPlotFindOneResolve, IError<SqlError>> { + models::plot::find_one(self.executor(), opts) + } + + pub fn plot_update(&self, opts: &IPlotUpdate) -> Result<IPlotUpdateResolve, IError<SqlError>> { + models::plot::update(self.executor(), opts) + } + + pub fn plot_delete(&self, opts: &IPlotDelete) -> Result<IPlotDeleteResolve, IError<SqlError>> { + models::plot::delete(self.executor(), opts) + } + + pub fn gcs_location_create( + &self, + opts: &IGcsLocationCreate, + ) -> Result<IGcsLocationCreateResolve, IError<SqlError>> { + models::gcs_location::create(self.executor(), opts) + } + + pub fn gcs_location_find_many( + &self, + opts: &IGcsLocationFindMany, + ) -> Result<IGcsLocationFindManyResolve, IError<SqlError>> { + models::gcs_location::find_many(self.executor(), opts) + } + + pub fn gcs_location_find_one( + &self, + opts: &IGcsLocationFindOne, + ) -> Result<IGcsLocationFindOneResolve, IError<SqlError>> { + models::gcs_location::find_one(self.executor(), opts) + } + + pub fn gcs_location_update( + &self, + opts: &IGcsLocationUpdate, + ) -> Result<IGcsLocationUpdateResolve, IError<SqlError>> { + models::gcs_location::update(self.executor(), opts) + } + + pub fn gcs_location_delete( + &self, + opts: &IGcsLocationDelete, + ) -> Result<IGcsLocationDeleteResolve, IError<SqlError>> { + models::gcs_location::delete(self.executor(), opts) + } + + pub fn farm_gcs_location_create( + &self, + opts: &IFarmGcsLocationCreate, + ) -> Result<IFarmGcsLocationCreateResolve, IError<SqlError>> { + models::farm_gcs_location::create(self.executor(), opts) + } + + pub fn farm_gcs_location_find_many( + &self, + opts: &IFarmGcsLocationFindMany, + ) -> Result<IFarmGcsLocationFindManyResolve, IError<SqlError>> { + models::farm_gcs_location::find_many(self.executor(), opts) + } + + pub fn farm_gcs_location_find_one( + &self, + opts: &IFarmGcsLocationFindOne, + ) -> Result<IFarmGcsLocationFindOneResolve, IError<SqlError>> { + models::farm_gcs_location::find_one(self.executor(), opts) + } + + pub fn farm_gcs_location_update( + &self, + opts: &IFarmGcsLocationUpdate, + ) -> Result<IFarmGcsLocationUpdateResolve, IError<SqlError>> { + models::farm_gcs_location::update(self.executor(), opts) + } + + pub fn farm_gcs_location_delete( + &self, + opts: &IFarmGcsLocationDelete, + ) -> Result<IFarmGcsLocationDeleteResolve, IError<SqlError>> { + models::farm_gcs_location::delete(self.executor(), opts) + } + + pub fn plot_gcs_location_create( + &self, + opts: &IPlotGcsLocationCreate, + ) -> Result<IPlotGcsLocationCreateResolve, IError<SqlError>> { + models::plot_gcs_location::create(self.executor(), opts) + } + + pub fn plot_gcs_location_find_many( + &self, + opts: &IPlotGcsLocationFindMany, + ) -> Result<IPlotGcsLocationFindManyResolve, IError<SqlError>> { + models::plot_gcs_location::find_many(self.executor(), opts) + } + + pub fn plot_gcs_location_find_one( + &self, + opts: &IPlotGcsLocationFindOne, + ) -> Result<IPlotGcsLocationFindOneResolve, IError<SqlError>> { + models::plot_gcs_location::find_one(self.executor(), opts) + } + + pub fn plot_gcs_location_update( + &self, + opts: &IPlotGcsLocationUpdate, + ) -> Result<IPlotGcsLocationUpdateResolve, IError<SqlError>> { + models::plot_gcs_location::update(self.executor(), opts) + } + + pub fn plot_gcs_location_delete( + &self, + opts: &IPlotGcsLocationDelete, + ) -> Result<IPlotGcsLocationDeleteResolve, IError<SqlError>> { + models::plot_gcs_location::delete(self.executor(), opts) + } + + pub fn farm_tag_create( + &self, + opts: &IFarmTagCreate, + ) -> Result<IFarmTagCreateResolve, IError<SqlError>> { + models::farm_tag::create(self.executor(), opts) + } + + pub fn farm_tag_find_many( + &self, + opts: &IFarmTagFindMany, + ) -> Result<IFarmTagFindManyResolve, IError<SqlError>> { + models::farm_tag::find_many(self.executor(), opts) + } + + pub fn farm_tag_find_one( + &self, + opts: &IFarmTagFindOne, + ) -> Result<IFarmTagFindOneResolve, IError<SqlError>> { + models::farm_tag::find_one(self.executor(), opts) + } + + pub fn farm_tag_update( + &self, + opts: &IFarmTagUpdate, + ) -> Result<IFarmTagUpdateResolve, IError<SqlError>> { + models::farm_tag::update(self.executor(), opts) + } + + pub fn farm_tag_delete( + &self, + opts: &IFarmTagDelete, + ) -> Result<IFarmTagDeleteResolve, IError<SqlError>> { + models::farm_tag::delete(self.executor(), opts) + } + + pub fn plot_tag_create( + &self, + opts: &IPlotTagCreate, + ) -> Result<IPlotTagCreateResolve, IError<SqlError>> { + models::plot_tag::create(self.executor(), opts) + } + + pub fn plot_tag_find_many( + &self, + opts: &IPlotTagFindMany, + ) -> Result<IPlotTagFindManyResolve, IError<SqlError>> { + models::plot_tag::find_many(self.executor(), opts) + } + + pub fn plot_tag_find_one( + &self, + opts: &IPlotTagFindOne, + ) -> Result<IPlotTagFindOneResolve, IError<SqlError>> { + models::plot_tag::find_one(self.executor(), opts) + } + + pub fn plot_tag_update( + &self, + opts: &IPlotTagUpdate, + ) -> Result<IPlotTagUpdateResolve, IError<SqlError>> { + models::plot_tag::update(self.executor(), opts) + } + + pub fn plot_tag_delete( + &self, + opts: &IPlotTagDelete, + ) -> Result<IPlotTagDeleteResolve, IError<SqlError>> { + models::plot_tag::delete(self.executor(), opts) + } + + pub fn farm_member_create( + &self, + opts: &IFarmMemberCreate, + ) -> Result<IFarmMemberCreateResolve, IError<SqlError>> { + models::farm_member::create(self.executor(), opts) + } + + pub fn farm_member_find_many( + &self, + opts: &IFarmMemberFindMany, + ) -> Result<IFarmMemberFindManyResolve, IError<SqlError>> { + models::farm_member::find_many(self.executor(), opts) + } + + pub fn farm_member_find_one( + &self, + opts: &IFarmMemberFindOne, + ) -> Result<IFarmMemberFindOneResolve, IError<SqlError>> { + models::farm_member::find_one(self.executor(), opts) + } + + pub fn farm_member_update( + &self, + opts: &IFarmMemberUpdate, + ) -> Result<IFarmMemberUpdateResolve, IError<SqlError>> { + models::farm_member::update(self.executor(), opts) + } + + pub fn farm_member_delete( + &self, + opts: &IFarmMemberDelete, + ) -> Result<IFarmMemberDeleteResolve, IError<SqlError>> { + models::farm_member::delete(self.executor(), opts) + } + + pub fn farm_member_claim_create( + &self, + opts: &IFarmMemberClaimCreate, + ) -> Result<IFarmMemberClaimCreateResolve, IError<SqlError>> { + models::farm_member_claim::create(self.executor(), opts) + } + + pub fn farm_member_claim_find_many( + &self, + opts: &IFarmMemberClaimFindMany, + ) -> Result<IFarmMemberClaimFindManyResolve, IError<SqlError>> { + models::farm_member_claim::find_many(self.executor(), opts) + } + + pub fn farm_member_claim_find_one( + &self, + opts: &IFarmMemberClaimFindOne, + ) -> Result<IFarmMemberClaimFindOneResolve, IError<SqlError>> { + models::farm_member_claim::find_one(self.executor(), opts) + } + + pub fn farm_member_claim_update( + &self, + opts: &IFarmMemberClaimUpdate, + ) -> Result<IFarmMemberClaimUpdateResolve, IError<SqlError>> { + models::farm_member_claim::update(self.executor(), opts) + } + + pub fn farm_member_claim_delete( + &self, + opts: &IFarmMemberClaimDelete, + ) -> Result<IFarmMemberClaimDeleteResolve, IError<SqlError>> { + models::farm_member_claim::delete(self.executor(), opts) + } + + pub fn log_error_create( + &self, + opts: &ILogErrorCreate, + ) -> Result<ILogErrorCreateResolve, IError<SqlError>> { + models::log_error::create(self.executor(), opts) + } + + pub fn log_error_find_many( + &self, + opts: &ILogErrorFindMany, + ) -> Result<ILogErrorFindManyResolve, IError<SqlError>> { + models::log_error::find_many(self.executor(), opts) + } + + pub fn log_error_find_one( + &self, + opts: &ILogErrorFindOne, + ) -> Result<ILogErrorFindOneResolve, IError<SqlError>> { + models::log_error::find_one(self.executor(), opts) + } + + pub fn log_error_update( + &self, + opts: &ILogErrorUpdate, + ) -> Result<ILogErrorUpdateResolve, IError<SqlError>> { + models::log_error::update(self.executor(), opts) + } + + pub fn log_error_delete( + &self, + opts: &ILogErrorDelete, + ) -> Result<ILogErrorDeleteResolve, IError<SqlError>> { + models::log_error::delete(self.executor(), opts) + } + + pub fn media_image_create( + &self, + opts: &IMediaImageCreate, + ) -> Result<IMediaImageCreateResolve, IError<SqlError>> { + models::media_image::create(self.executor(), opts) + } + + pub fn media_image_find_many( + &self, + opts: &IMediaImageFindMany, + ) -> Result<IMediaImageFindManyResolve, IError<SqlError>> { + models::media_image::find_many(self.executor(), opts) + } + + pub fn media_image_find_one( + &self, + opts: &IMediaImageFindOne, + ) -> Result<IMediaImageFindOneResolve, IError<SqlError>> { + models::media_image::find_one(self.executor(), opts) + } + + pub fn media_image_update( + &self, + opts: &IMediaImageUpdate, + ) -> Result<IMediaImageUpdateResolve, IError<SqlError>> { + models::media_image::update(self.executor(), opts) + } + + pub fn media_image_delete( + &self, + opts: &IMediaImageDelete, + ) -> Result<IMediaImageDeleteResolve, IError<SqlError>> { + models::media_image::delete(self.executor(), opts) + } + + pub fn nostr_profile_create( + &self, + opts: &INostrProfileCreate, + ) -> Result<INostrProfileCreateResolve, IError<SqlError>> { + models::nostr_profile::create(self.executor(), opts) + } + + pub fn nostr_profile_find_many( + &self, + opts: &INostrProfileFindMany, + ) -> Result<INostrProfileFindManyResolve, IError<SqlError>> { + models::nostr_profile::find_many(self.executor(), opts) + } + + pub fn nostr_profile_find_one( + &self, + opts: &INostrProfileFindOne, + ) -> Result<INostrProfileFindOneResolve, IError<SqlError>> { + models::nostr_profile::find_one(self.executor(), opts) + } + + pub fn nostr_profile_update( + &self, + opts: &INostrProfileUpdate, + ) -> Result<INostrProfileUpdateResolve, IError<SqlError>> { + models::nostr_profile::update(self.executor(), opts) + } + + pub fn nostr_profile_delete( + &self, + opts: &INostrProfileDelete, + ) -> Result<INostrProfileDeleteResolve, IError<SqlError>> { + models::nostr_profile::delete(self.executor(), opts) + } + + pub fn nostr_event_state_create( + &self, + opts: &INostrEventStateCreate, + ) -> Result<INostrEventStateCreateResolve, IError<SqlError>> { + models::nostr_event_state::create(self.executor(), opts) + } + + pub fn nostr_event_state_find_many( + &self, + opts: &INostrEventStateFindMany, + ) -> Result<INostrEventStateFindManyResolve, IError<SqlError>> { + models::nostr_event_state::find_many(self.executor(), opts) + } + + pub fn nostr_event_state_find_one( + &self, + opts: &INostrEventStateFindOne, + ) -> Result<INostrEventStateFindOneResolve, IError<SqlError>> { + models::nostr_event_state::find_one(self.executor(), opts) + } + + pub fn nostr_event_state_update( + &self, + opts: &INostrEventStateUpdate, + ) -> Result<INostrEventStateUpdateResolve, IError<SqlError>> { + models::nostr_event_state::update(self.executor(), opts) + } + + pub fn nostr_event_state_delete( + &self, + opts: &INostrEventStateDelete, + ) -> Result<INostrEventStateDeleteResolve, IError<SqlError>> { + models::nostr_event_state::delete(self.executor(), opts) + } + + pub fn nostr_relay_create( + &self, + opts: &INostrRelayCreate, + ) -> Result<INostrRelayCreateResolve, IError<SqlError>> { + models::nostr_relay::create(self.executor(), opts) + } + + pub fn nostr_relay_find_many( + &self, + opts: &INostrRelayFindMany, + ) -> Result<INostrRelayFindManyResolve, IError<SqlError>> { + models::nostr_relay::find_many(self.executor(), opts) + } + + pub fn nostr_relay_find_one( + &self, + opts: &INostrRelayFindOne, + ) -> Result<INostrRelayFindOneResolve, IError<SqlError>> { + models::nostr_relay::find_one(self.executor(), opts) + } + + pub fn nostr_relay_update( + &self, + opts: &INostrRelayUpdate, + ) -> Result<INostrRelayUpdateResolve, IError<SqlError>> { + models::nostr_relay::update(self.executor(), opts) + } + + pub fn nostr_relay_delete( + &self, + opts: &INostrRelayDelete, + ) -> Result<INostrRelayDeleteResolve, IError<SqlError>> { + models::nostr_relay::delete(self.executor(), opts) + } + + pub fn trade_product_create( + &self, + opts: &ITradeProductCreate, + ) -> Result<ITradeProductCreateResolve, IError<SqlError>> { + models::trade_product::create(self.executor(), opts) + } + + pub fn trade_product_find_many( + &self, + opts: &ITradeProductFindMany, + ) -> Result<ITradeProductFindManyResolve, IError<SqlError>> { + models::trade_product::find_many(self.executor(), opts) + } + + pub fn trade_product_find_one( + &self, + opts: &ITradeProductFindOne, + ) -> Result<ITradeProductFindOneResolve, IError<SqlError>> { + models::trade_product::find_one(self.executor(), opts) + } + + pub fn trade_product_update( + &self, + opts: &ITradeProductUpdate, + ) -> Result<ITradeProductUpdateResolve, IError<SqlError>> { + models::trade_product::update(self.executor(), opts) + } + + pub fn trade_product_delete( + &self, + opts: &ITradeProductDelete, + ) -> Result<ITradeProductDeleteResolve, IError<SqlError>> { + models::trade_product::delete(self.executor(), opts) + } + + pub fn nostr_profile_relay_set( + &self, + opts: &INostrProfileRelayRelation, + ) -> Result<INostrProfileRelayResolve, IError<SqlError>> { + models::nostr_profile_relay::set(self.executor(), opts) + } + + pub fn nostr_profile_relay_unset( + &self, + opts: &INostrProfileRelayRelation, + ) -> Result<INostrProfileRelayResolve, IError<SqlError>> { + models::nostr_profile_relay::unset(self.executor(), opts) + } + + pub fn trade_product_location_set( + &self, + opts: &ITradeProductLocationRelation, + ) -> Result<ITradeProductLocationResolve, IError<SqlError>> { + models::trade_product_location::set(self.executor(), opts) + } + + pub fn trade_product_location_unset( + &self, + opts: &ITradeProductLocationRelation, + ) -> Result<ITradeProductLocationResolve, IError<SqlError>> { + models::trade_product_location::unset(self.executor(), opts) + } + + pub fn trade_product_media_set( + &self, + opts: &ITradeProductMediaRelation, + ) -> Result<ITradeProductMediaResolve, IError<SqlError>> { + models::trade_product_media::set(self.executor(), opts) + } + + pub fn trade_product_media_unset( + &self, + opts: &ITradeProductMediaRelation, + ) -> Result<ITradeProductMediaResolve, IError<SqlError>> { + models::trade_product_media::unset(self.executor(), opts) + } +} + +#[cfg(feature = "coverage-minimal")] +impl<E: SqlExecutor> TangleSql<E> { + pub fn new(executor: E) -> Self { + Self { executor } + } + + pub fn executor(&self) -> &E { + &self.executor + } +} + +#[cfg(test)] +mod tests { + use super::TangleSql; + use radroots_sql_core::{ExecOutcome, SqlError, SqlExecutor}; + + struct ProbeExecutor; + + impl SqlExecutor for ProbeExecutor { + fn exec(&self, _sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> { + Ok(ExecOutcome { + changes: 0, + last_insert_id: 0, + }) + } + + fn query_raw(&self, _sql: &str, _params_json: &str) -> Result<String, SqlError> { + Ok("[]".to_string()) + } + + fn begin(&self) -> Result<(), SqlError> { + Ok(()) + } + + fn commit(&self) -> Result<(), SqlError> { + Ok(()) + } + + fn rollback(&self) -> Result<(), SqlError> { + Ok(()) + } + } + + #[test] + fn tangle_sql_constructor_and_executor_access_are_supported() { + let db = TangleSql::new(ProbeExecutor); + let exec = db.executor(); + assert!(exec.exec("select 1", "[]").is_ok()); + assert!(exec.query_raw("select 1", "[]").is_ok()); + assert!(exec.begin().is_ok()); + assert!(exec.commit().is_ok()); + assert!(exec.rollback().is_ok()); + assert_eq!( + TangleSql::<ProbeExecutor>::coverage_branch_probe(true), + "enabled" + ); + assert_eq!( + TangleSql::<ProbeExecutor>::coverage_branch_probe(false), + "disabled" + ); + } +} diff --git a/crates/tangle-db/src/migrations.rs b/crates/replica-db/src/migrations.rs diff --git a/crates/replica-db/src/models/farm.rs b/crates/replica-db/src/models/farm.rs @@ -0,0 +1,145 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::farm::{ + Farm, FarmQueryBindValues, IFarmCreate, IFarmCreateResolve, IFarmDelete, IFarmDeleteResolve, + IFarmFieldsFilter, IFarmFindMany, IFarmFindManyResolve, IFarmFindOne, IFarmFindOneResolve, + IFarmUpdate, IFarmUpdateResolve, +}; +use radroots_types::types::{IError, IResult, IResultList}; +use serde_json::Value; + +const TABLE_NAME: &str = "farm"; + +pub fn create<E: SqlExecutor>( + exec: &E, + opts: &IFarmCreate, +) -> Result<IFarmCreateResolve, IError<SqlError>> { + let field_map = utils::to_object_map(opts)?; + let id = utils::uuidv4(); + let now = utils::time_created_on(); + let meta: [(&str, Value); 3] = [ + ("id", Value::from(id.clone())), + ("created_at", Value::from(now.clone())), + ("updated_at", Value::from(now.clone())), + ]; + let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); + let params_json = utils::to_params_json(bind_values)?; + let _ = exec.exec(&sql, ¶ms_json)?; + let on = FarmQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; + Ok(IResult { result }) +} + +pub fn find_one<E: SqlExecutor>( + exec: &E, + opts: &IFarmFindOne, +) -> Result<IFarmFindOneResolve, IError<SqlError>> { + let result = match opts { + IFarmFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(IResult { result }) +} + +pub fn find_many<E: SqlExecutor>( + exec: &E, + opts: &IFarmFindMany, +) -> Result<IFarmFindManyResolve, IError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(IResultList { results }) +} + +fn find_many_filter<E: SqlExecutor>( + exec: &E, + filter: &Option<IFarmFieldsFilter>, +) -> Result<Vec<Farm>, IError<SqlError>> { + let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); + let params_json = utils::to_params_json(bind_values)?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<Farm> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on<E: SqlExecutor>( + exec: &E, + on: &FarmQueryBindValues, +) -> Result<Option<Farm>, IError<SqlError>> { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value])?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<Farm> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<Farm, IError<SqlError>> { + let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<Farm> = utils::parse_json(&json)?; + rows.pop() + .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update<E: SqlExecutor>( + exec: &E, + opts: &IFarmUpdate, +) -> Result<IFarmUpdateResolve, IError<SqlError>> { + let mut updates = utils::to_partial_object_map(&opts.fields)?; + if updates.is_empty() { + return Err(IError::from(SqlError::InvalidArgument(String::from( + "no fields to update", + )))); + } + updates.insert( + String::from("updated_at"), + Value::from(utils::time_created_on()), + ); + let mut set_parts = Vec::with_capacity(updates.len()); + let mut bind_values = Vec::with_capacity(updates.len() + 1); + for (column, value) in updates { + set_parts.push(format!("{column} = ?")); + bind_values.push(utils::to_db_bind_value(&value)); + } + let id_for_lookup = match opts.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &opts.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; + model.id + } + }; + 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 _ = exec.exec(&sql, ¶ms_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(IResult { result: updated }) +} + +pub fn delete<E: SqlExecutor>( + exec: &E, + opts: &IFarmDelete, +) -> Result<IFarmDeleteResolve, IError<SqlError>> { + let id_for_lookup = match opts { + IFarmDelete::On(args) => match args.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &args.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, ¶ms_json)?; + if outcome.changes == 0 { + return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); + } + Ok(IResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica-db/src/models/farm_gcs_location.rs b/crates/replica-db/src/models/farm_gcs_location.rs @@ -0,0 +1,147 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::farm_gcs_location::{ + FarmGcsLocation, FarmGcsLocationQueryBindValues, IFarmGcsLocationCreate, + IFarmGcsLocationCreateResolve, IFarmGcsLocationDelete, IFarmGcsLocationDeleteResolve, + IFarmGcsLocationFieldsFilter, IFarmGcsLocationFindMany, IFarmGcsLocationFindManyResolve, + IFarmGcsLocationFindOne, IFarmGcsLocationFindOneResolve, IFarmGcsLocationUpdate, + IFarmGcsLocationUpdateResolve, +}; +use radroots_types::types::{IError, IResult, IResultList}; +use serde_json::Value; + +const TABLE_NAME: &str = "farm_gcs_location"; + +pub fn create<E: SqlExecutor>( + exec: &E, + opts: &IFarmGcsLocationCreate, +) -> Result<IFarmGcsLocationCreateResolve, IError<SqlError>> { + let field_map = utils::to_object_map(opts)?; + let id = utils::uuidv4(); + let now = utils::time_created_on(); + let meta: [(&str, Value); 3] = [ + ("id", Value::from(id.clone())), + ("created_at", Value::from(now.clone())), + ("updated_at", Value::from(now.clone())), + ]; + let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); + let params_json = utils::to_params_json(bind_values)?; + let _ = exec.exec(&sql, ¶ms_json)?; + let on = FarmGcsLocationQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; + Ok(IResult { result }) +} + +pub fn find_one<E: SqlExecutor>( + exec: &E, + opts: &IFarmGcsLocationFindOne, +) -> Result<IFarmGcsLocationFindOneResolve, IError<SqlError>> { + let result = match opts { + IFarmGcsLocationFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(IResult { result }) +} + +pub fn find_many<E: SqlExecutor>( + exec: &E, + opts: &IFarmGcsLocationFindMany, +) -> Result<IFarmGcsLocationFindManyResolve, IError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(IResultList { results }) +} + +fn find_many_filter<E: SqlExecutor>( + exec: &E, + filter: &Option<IFarmGcsLocationFieldsFilter>, +) -> Result<Vec<FarmGcsLocation>, IError<SqlError>> { + let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); + let params_json = utils::to_params_json(bind_values)?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<FarmGcsLocation> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on<E: SqlExecutor>( + exec: &E, + on: &FarmGcsLocationQueryBindValues, +) -> Result<Option<FarmGcsLocation>, IError<SqlError>> { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value])?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<FarmGcsLocation> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<FarmGcsLocation, IError<SqlError>> { + let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<FarmGcsLocation> = utils::parse_json(&json)?; + rows.pop() + .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update<E: SqlExecutor>( + exec: &E, + opts: &IFarmGcsLocationUpdate, +) -> Result<IFarmGcsLocationUpdateResolve, IError<SqlError>> { + let mut updates = utils::to_partial_object_map(&opts.fields)?; + if updates.is_empty() { + return Err(IError::from(SqlError::InvalidArgument(String::from( + "no fields to update", + )))); + } + updates.insert( + String::from("updated_at"), + Value::from(utils::time_created_on()), + ); + let mut set_parts = Vec::with_capacity(updates.len()); + let mut bind_values = Vec::with_capacity(updates.len() + 1); + for (column, value) in updates { + set_parts.push(format!("{column} = ?")); + bind_values.push(utils::to_db_bind_value(&value)); + } + let id_for_lookup = match opts.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &opts.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; + model.id + } + }; + 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 _ = exec.exec(&sql, ¶ms_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(IResult { result: updated }) +} + +pub fn delete<E: SqlExecutor>( + exec: &E, + opts: &IFarmGcsLocationDelete, +) -> Result<IFarmGcsLocationDeleteResolve, IError<SqlError>> { + let id_for_lookup = match opts { + IFarmGcsLocationDelete::On(args) => match args.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &args.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, ¶ms_json)?; + if outcome.changes == 0 { + return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); + } + Ok(IResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica-db/src/models/farm_member.rs b/crates/replica-db/src/models/farm_member.rs @@ -0,0 +1,146 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::farm_member::{ + FarmMember, FarmMemberQueryBindValues, IFarmMemberCreate, IFarmMemberCreateResolve, + IFarmMemberDelete, IFarmMemberDeleteResolve, IFarmMemberFieldsFilter, IFarmMemberFindMany, + IFarmMemberFindManyResolve, IFarmMemberFindOne, IFarmMemberFindOneResolve, IFarmMemberUpdate, + IFarmMemberUpdateResolve, +}; +use radroots_types::types::{IError, IResult, IResultList}; +use serde_json::Value; + +const TABLE_NAME: &str = "farm_member"; + +pub fn create<E: SqlExecutor>( + exec: &E, + opts: &IFarmMemberCreate, +) -> Result<IFarmMemberCreateResolve, IError<SqlError>> { + let field_map = utils::to_object_map(opts)?; + let id = utils::uuidv4(); + let now = utils::time_created_on(); + let meta: [(&str, Value); 3] = [ + ("id", Value::from(id.clone())), + ("created_at", Value::from(now.clone())), + ("updated_at", Value::from(now.clone())), + ]; + let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); + let params_json = utils::to_params_json(bind_values)?; + let _ = exec.exec(&sql, ¶ms_json)?; + let on = FarmMemberQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; + Ok(IResult { result }) +} + +pub fn find_one<E: SqlExecutor>( + exec: &E, + opts: &IFarmMemberFindOne, +) -> Result<IFarmMemberFindOneResolve, IError<SqlError>> { + let result = match opts { + IFarmMemberFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(IResult { result }) +} + +pub fn find_many<E: SqlExecutor>( + exec: &E, + opts: &IFarmMemberFindMany, +) -> Result<IFarmMemberFindManyResolve, IError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(IResultList { results }) +} + +fn find_many_filter<E: SqlExecutor>( + exec: &E, + filter: &Option<IFarmMemberFieldsFilter>, +) -> Result<Vec<FarmMember>, IError<SqlError>> { + let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); + let params_json = utils::to_params_json(bind_values)?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<FarmMember> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on<E: SqlExecutor>( + exec: &E, + on: &FarmMemberQueryBindValues, +) -> Result<Option<FarmMember>, IError<SqlError>> { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value])?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<FarmMember> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<FarmMember, IError<SqlError>> { + let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<FarmMember> = utils::parse_json(&json)?; + rows.pop() + .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update<E: SqlExecutor>( + exec: &E, + opts: &IFarmMemberUpdate, +) -> Result<IFarmMemberUpdateResolve, IError<SqlError>> { + let mut updates = utils::to_partial_object_map(&opts.fields)?; + if updates.is_empty() { + return Err(IError::from(SqlError::InvalidArgument(String::from( + "no fields to update", + )))); + } + updates.insert( + String::from("updated_at"), + Value::from(utils::time_created_on()), + ); + let mut set_parts = Vec::with_capacity(updates.len()); + let mut bind_values = Vec::with_capacity(updates.len() + 1); + for (column, value) in updates { + set_parts.push(format!("{column} = ?")); + bind_values.push(utils::to_db_bind_value(&value)); + } + let id_for_lookup = match opts.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &opts.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; + model.id + } + }; + 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 _ = exec.exec(&sql, ¶ms_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(IResult { result: updated }) +} + +pub fn delete<E: SqlExecutor>( + exec: &E, + opts: &IFarmMemberDelete, +) -> Result<IFarmMemberDeleteResolve, IError<SqlError>> { + let id_for_lookup = match opts { + IFarmMemberDelete::On(args) => match args.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &args.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, ¶ms_json)?; + if outcome.changes == 0 { + return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); + } + Ok(IResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica-db/src/models/farm_member_claim.rs b/crates/replica-db/src/models/farm_member_claim.rs @@ -0,0 +1,147 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::farm_member_claim::{ + FarmMemberClaim, FarmMemberClaimQueryBindValues, IFarmMemberClaimCreate, + IFarmMemberClaimCreateResolve, IFarmMemberClaimDelete, IFarmMemberClaimDeleteResolve, + IFarmMemberClaimFieldsFilter, IFarmMemberClaimFindMany, IFarmMemberClaimFindManyResolve, + IFarmMemberClaimFindOne, IFarmMemberClaimFindOneResolve, IFarmMemberClaimUpdate, + IFarmMemberClaimUpdateResolve, +}; +use radroots_types::types::{IError, IResult, IResultList}; +use serde_json::Value; + +const TABLE_NAME: &str = "farm_member_claim"; + +pub fn create<E: SqlExecutor>( + exec: &E, + opts: &IFarmMemberClaimCreate, +) -> Result<IFarmMemberClaimCreateResolve, IError<SqlError>> { + let field_map = utils::to_object_map(opts)?; + let id = utils::uuidv4(); + let now = utils::time_created_on(); + let meta: [(&str, Value); 3] = [ + ("id", Value::from(id.clone())), + ("created_at", Value::from(now.clone())), + ("updated_at", Value::from(now.clone())), + ]; + let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); + let params_json = utils::to_params_json(bind_values)?; + let _ = exec.exec(&sql, ¶ms_json)?; + let on = FarmMemberClaimQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; + Ok(IResult { result }) +} + +pub fn find_one<E: SqlExecutor>( + exec: &E, + opts: &IFarmMemberClaimFindOne, +) -> Result<IFarmMemberClaimFindOneResolve, IError<SqlError>> { + let result = match opts { + IFarmMemberClaimFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(IResult { result }) +} + +pub fn find_many<E: SqlExecutor>( + exec: &E, + opts: &IFarmMemberClaimFindMany, +) -> Result<IFarmMemberClaimFindManyResolve, IError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(IResultList { results }) +} + +fn find_many_filter<E: SqlExecutor>( + exec: &E, + filter: &Option<IFarmMemberClaimFieldsFilter>, +) -> Result<Vec<FarmMemberClaim>, IError<SqlError>> { + let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); + let params_json = utils::to_params_json(bind_values)?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<FarmMemberClaim> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on<E: SqlExecutor>( + exec: &E, + on: &FarmMemberClaimQueryBindValues, +) -> Result<Option<FarmMemberClaim>, IError<SqlError>> { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value])?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<FarmMemberClaim> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<FarmMemberClaim, IError<SqlError>> { + let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<FarmMemberClaim> = utils::parse_json(&json)?; + rows.pop() + .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update<E: SqlExecutor>( + exec: &E, + opts: &IFarmMemberClaimUpdate, +) -> Result<IFarmMemberClaimUpdateResolve, IError<SqlError>> { + let mut updates = utils::to_partial_object_map(&opts.fields)?; + if updates.is_empty() { + return Err(IError::from(SqlError::InvalidArgument(String::from( + "no fields to update", + )))); + } + updates.insert( + String::from("updated_at"), + Value::from(utils::time_created_on()), + ); + let mut set_parts = Vec::with_capacity(updates.len()); + let mut bind_values = Vec::with_capacity(updates.len() + 1); + for (column, value) in updates { + set_parts.push(format!("{column} = ?")); + bind_values.push(utils::to_db_bind_value(&value)); + } + let id_for_lookup = match opts.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &opts.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; + model.id + } + }; + 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 _ = exec.exec(&sql, ¶ms_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(IResult { result: updated }) +} + +pub fn delete<E: SqlExecutor>( + exec: &E, + opts: &IFarmMemberClaimDelete, +) -> Result<IFarmMemberClaimDeleteResolve, IError<SqlError>> { + let id_for_lookup = match opts { + IFarmMemberClaimDelete::On(args) => match args.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &args.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, ¶ms_json)?; + if outcome.changes == 0 { + return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); + } + Ok(IResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica-db/src/models/farm_tag.rs b/crates/replica-db/src/models/farm_tag.rs @@ -0,0 +1,145 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::farm_tag::{ + FarmTag, FarmTagQueryBindValues, IFarmTagCreate, IFarmTagCreateResolve, IFarmTagDelete, + IFarmTagDeleteResolve, IFarmTagFieldsFilter, IFarmTagFindMany, IFarmTagFindManyResolve, + IFarmTagFindOne, IFarmTagFindOneResolve, IFarmTagUpdate, IFarmTagUpdateResolve, +}; +use radroots_types::types::{IError, IResult, IResultList}; +use serde_json::Value; + +const TABLE_NAME: &str = "farm_tag"; + +pub fn create<E: SqlExecutor>( + exec: &E, + opts: &IFarmTagCreate, +) -> Result<IFarmTagCreateResolve, IError<SqlError>> { + let field_map = utils::to_object_map(opts)?; + let id = utils::uuidv4(); + let now = utils::time_created_on(); + let meta: [(&str, Value); 3] = [ + ("id", Value::from(id.clone())), + ("created_at", Value::from(now.clone())), + ("updated_at", Value::from(now.clone())), + ]; + let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); + let params_json = utils::to_params_json(bind_values)?; + let _ = exec.exec(&sql, ¶ms_json)?; + let on = FarmTagQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; + Ok(IResult { result }) +} + +pub fn find_one<E: SqlExecutor>( + exec: &E, + opts: &IFarmTagFindOne, +) -> Result<IFarmTagFindOneResolve, IError<SqlError>> { + let result = match opts { + IFarmTagFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(IResult { result }) +} + +pub fn find_many<E: SqlExecutor>( + exec: &E, + opts: &IFarmTagFindMany, +) -> Result<IFarmTagFindManyResolve, IError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(IResultList { results }) +} + +fn find_many_filter<E: SqlExecutor>( + exec: &E, + filter: &Option<IFarmTagFieldsFilter>, +) -> Result<Vec<FarmTag>, IError<SqlError>> { + let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); + let params_json = utils::to_params_json(bind_values)?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<FarmTag> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on<E: SqlExecutor>( + exec: &E, + on: &FarmTagQueryBindValues, +) -> Result<Option<FarmTag>, IError<SqlError>> { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value])?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<FarmTag> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<FarmTag, IError<SqlError>> { + let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<FarmTag> = utils::parse_json(&json)?; + rows.pop() + .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update<E: SqlExecutor>( + exec: &E, + opts: &IFarmTagUpdate, +) -> Result<IFarmTagUpdateResolve, IError<SqlError>> { + let mut updates = utils::to_partial_object_map(&opts.fields)?; + if updates.is_empty() { + return Err(IError::from(SqlError::InvalidArgument(String::from( + "no fields to update", + )))); + } + updates.insert( + String::from("updated_at"), + Value::from(utils::time_created_on()), + ); + let mut set_parts = Vec::with_capacity(updates.len()); + let mut bind_values = Vec::with_capacity(updates.len() + 1); + for (column, value) in updates { + set_parts.push(format!("{column} = ?")); + bind_values.push(utils::to_db_bind_value(&value)); + } + let id_for_lookup = match opts.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &opts.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; + model.id + } + }; + 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 _ = exec.exec(&sql, ¶ms_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(IResult { result: updated }) +} + +pub fn delete<E: SqlExecutor>( + exec: &E, + opts: &IFarmTagDelete, +) -> Result<IFarmTagDeleteResolve, IError<SqlError>> { + let id_for_lookup = match opts { + IFarmTagDelete::On(args) => match args.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &args.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, ¶ms_json)?; + if outcome.changes == 0 { + return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); + } + Ok(IResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica-db/src/models/gcs_location.rs b/crates/replica-db/src/models/gcs_location.rs @@ -0,0 +1,223 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::gcs_location::{ + GcsLocation, GcsLocationFindManyRel, GcsLocationQueryBindValues, IGcsLocationCreate, + IGcsLocationCreateResolve, IGcsLocationDelete, IGcsLocationDeleteResolve, + IGcsLocationFieldsFilter, IGcsLocationFindMany, IGcsLocationFindManyResolve, + IGcsLocationFindOne, IGcsLocationFindOneResolve, IGcsLocationUpdate, IGcsLocationUpdateResolve, +}; +use radroots_types::types::{IError, IResult, IResultList}; +use serde_json::Value; + +const TABLE_NAME: &str = "gcs_location"; + +pub fn create<E: SqlExecutor>( + exec: &E, + opts: &IGcsLocationCreate, +) -> Result<IGcsLocationCreateResolve, IError<SqlError>> { + let field_map = utils::to_object_map(opts)?; + let id = utils::uuidv4(); + let now = utils::time_created_on(); + let meta: [(&str, Value); 3] = [ + ("id", Value::from(id.clone())), + ("created_at", Value::from(now.clone())), + ("updated_at", Value::from(now.clone())), + ]; + let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); + let params_json = utils::to_params_json(bind_values)?; + let _ = exec.exec(&sql, ¶ms_json)?; + let on = GcsLocationQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; + Ok(IResult { result }) +} + +pub fn find_one<E: SqlExecutor>( + exec: &E, + opts: &IGcsLocationFindOne, +) -> Result<IGcsLocationFindOneResolve, IError<SqlError>> { + let result = match opts { + IGcsLocationFindOne::On(args) => find_one_by_on(exec, &args.on)?, + IGcsLocationFindOne::Rel(args) => find_one_by_rel(exec, &args.rel)?, + }; + Ok(IResult { result }) +} + +pub fn find_many<E: SqlExecutor>( + exec: &E, + opts: &IGcsLocationFindMany, +) -> Result<IGcsLocationFindManyResolve, IError<SqlError>> { + let results = match opts { + IGcsLocationFindMany::Filter { filter } => find_many_filter(exec, filter)?, + IGcsLocationFindMany::Rel { rel } => find_many_by_rel(exec, rel)?, + }; + Ok(IResultList { results }) +} + +fn find_many_filter<E: SqlExecutor>( + exec: &E, + filter: &Option<IGcsLocationFieldsFilter>, +) -> Result<Vec<GcsLocation>, IError<SqlError>> { + let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); + let params_json = utils::to_params_json(bind_values)?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<GcsLocation> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on<E: SqlExecutor>( + exec: &E, + on: &GcsLocationQueryBindValues, +) -> Result<Option<GcsLocation>, IError<SqlError>> { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value])?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<GcsLocation> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn rel_query(rel: &GcsLocationFindManyRel) -> (&'static str, Vec<Value>) { + match rel { + GcsLocationFindManyRel::OnTradeProduct(args) => ( + "SELECT gl.* FROM gcs_location gl JOIN trade_product_location tp_gl ON gl.id = tp_gl.tb_gl WHERE tp_gl.tb_tp = ?", + vec![Value::from(args.id.clone())], + ), + GcsLocationFindManyRel::OffTradeProduct(args) => ( + "SELECT gl.* FROM gcs_location gl WHERE NOT EXISTS (SELECT 1 FROM trade_product_location tp_gl WHERE tp_gl.tb_gl = gl.id AND tp_gl.tb_tp = ?)", + vec![Value::from(args.id.clone())], + ), + GcsLocationFindManyRel::OnFarm(args) => ( + "SELECT gl.* FROM gcs_location gl JOIN farm_gcs_location fgcs ON gl.id = fgcs.gcs_location_id WHERE fgcs.farm_id = ?", + vec![Value::from(args.id.clone())], + ), + GcsLocationFindManyRel::OffFarm(args) => ( + "SELECT gl.* FROM gcs_location gl WHERE NOT EXISTS (SELECT 1 FROM farm_gcs_location fgcs WHERE fgcs.gcs_location_id = gl.id AND fgcs.farm_id = ?)", + vec![Value::from(args.id.clone())], + ), + GcsLocationFindManyRel::OnPlot(args) => ( + "SELECT gl.* FROM gcs_location gl JOIN plot_gcs_location pgcs ON gl.id = pgcs.gcs_location_id WHERE pgcs.plot_id = ?", + vec![Value::from(args.id.clone())], + ), + GcsLocationFindManyRel::OffPlot(args) => ( + "SELECT gl.* FROM gcs_location gl WHERE NOT EXISTS (SELECT 1 FROM plot_gcs_location pgcs WHERE pgcs.gcs_location_id = gl.id AND pgcs.plot_id = ?)", + vec![Value::from(args.id.clone())], + ), + } +} + +fn find_one_by_rel<E: SqlExecutor>( + exec: &E, + rel: &GcsLocationFindManyRel, +) -> Result<Option<GcsLocation>, IError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values)?; + let sql = format!("{sql} LIMIT 1;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<GcsLocation> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn find_many_by_rel<E: SqlExecutor>( + exec: &E, + rel: &GcsLocationFindManyRel, +) -> Result<Vec<GcsLocation>, IError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values)?; + let sql = format!("{sql};"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<GcsLocation> = utils::parse_json(&json)?; + Ok(rows) +} + +fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<GcsLocation, IError<SqlError>> { + let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<GcsLocation> = utils::parse_json(&json)?; + rows.pop() + .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update<E: SqlExecutor>( + exec: &E, + opts: &IGcsLocationUpdate, +) -> Result<IGcsLocationUpdateResolve, IError<SqlError>> { + let mut updates = utils::to_partial_object_map(&opts.fields)?; + if updates.is_empty() { + return Err(IError::from(SqlError::InvalidArgument(String::from( + "no fields to update", + )))); + } + updates.insert( + String::from("updated_at"), + Value::from(utils::time_created_on()), + ); + let mut set_parts = Vec::with_capacity(updates.len()); + let mut bind_values = Vec::with_capacity(updates.len() + 1); + for (column, value) in updates { + set_parts.push(format!("{column} = ?")); + bind_values.push(utils::to_db_bind_value(&value)); + } + let id_for_lookup = match opts.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &opts.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; + model.id + } + }; + 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 _ = exec.exec(&sql, ¶ms_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(IResult { result: updated }) +} + +pub fn delete<E: SqlExecutor>( + exec: &E, + opts: &IGcsLocationDelete, +) -> Result<IGcsLocationDeleteResolve, IError<SqlError>> { + let id_for_lookup = match opts { + IGcsLocationDelete::On(args) => match args.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &args.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; + model.id + } + }, + IGcsLocationDelete::Rel(args) => { + let found = find_one_by_rel(exec, &args.rel)?; + let model = found.ok_or(IError::from(SqlError::NotFound(rel_lookup_key(&args.rel))))?; + model.id + } + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, ¶ms_json)?; + if outcome.changes == 0 { + return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); + } + Ok(IResult { + result: id_for_lookup, + }) +} + +fn rel_lookup_key(rel: &GcsLocationFindManyRel) -> String { + match rel { + GcsLocationFindManyRel::OnTradeProduct(args) => { + format!("on_trade_product:{}", args.id.as_str()) + } + GcsLocationFindManyRel::OffTradeProduct(args) => { + format!("off_trade_product:{}", args.id.as_str()) + } + GcsLocationFindManyRel::OnFarm(args) => format!("on_farm:{}", args.id.as_str()), + GcsLocationFindManyRel::OffFarm(args) => format!("off_farm:{}", args.id.as_str()), + GcsLocationFindManyRel::OnPlot(args) => format!("on_plot:{}", args.id.as_str()), + GcsLocationFindManyRel::OffPlot(args) => format!("off_plot:{}", args.id.as_str()), + } +} diff --git a/crates/replica-db/src/models/log_error.rs b/crates/replica-db/src/models/log_error.rs @@ -0,0 +1,146 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::log_error::{ + ILogErrorCreate, ILogErrorCreateResolve, ILogErrorDelete, ILogErrorDeleteResolve, + ILogErrorFieldsFilter, ILogErrorFindMany, ILogErrorFindManyResolve, ILogErrorFindOne, + ILogErrorFindOneResolve, ILogErrorUpdate, ILogErrorUpdateResolve, LogError, + LogErrorQueryBindValues, +}; +use radroots_types::types::{IError, IResult, IResultList}; +use serde_json::Value; + +const TABLE_NAME: &str = "log_error"; + +pub fn create<E: SqlExecutor>( + exec: &E, + opts: &ILogErrorCreate, +) -> Result<ILogErrorCreateResolve, IError<SqlError>> { + let field_map = utils::to_object_map(opts)?; + let id = utils::uuidv4(); + let now = utils::time_created_on(); + let meta: [(&str, Value); 3] = [ + ("id", Value::from(id.clone())), + ("created_at", Value::from(now.clone())), + ("updated_at", Value::from(now.clone())), + ]; + let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); + let params_json = utils::to_params_json(bind_values)?; + let _ = exec.exec(&sql, ¶ms_json)?; + let on = LogErrorQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; + Ok(IResult { result }) +} + +pub fn find_one<E: SqlExecutor>( + exec: &E, + opts: &ILogErrorFindOne, +) -> Result<ILogErrorFindOneResolve, IError<SqlError>> { + let result = match opts { + ILogErrorFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(IResult { result }) +} + +pub fn find_many<E: SqlExecutor>( + exec: &E, + opts: &ILogErrorFindMany, +) -> Result<ILogErrorFindManyResolve, IError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(IResultList { results }) +} + +fn find_many_filter<E: SqlExecutor>( + exec: &E, + filter: &Option<ILogErrorFieldsFilter>, +) -> Result<Vec<LogError>, IError<SqlError>> { + let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); + let params_json = utils::to_params_json(bind_values)?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<LogError> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on<E: SqlExecutor>( + exec: &E, + on: &LogErrorQueryBindValues, +) -> Result<Option<LogError>, IError<SqlError>> { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value])?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<LogError> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<LogError, IError<SqlError>> { + let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<LogError> = utils::parse_json(&json)?; + rows.pop() + .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update<E: SqlExecutor>( + exec: &E, + opts: &ILogErrorUpdate, +) -> Result<ILogErrorUpdateResolve, IError<SqlError>> { + let mut updates = utils::to_partial_object_map(&opts.fields)?; + if updates.is_empty() { + return Err(IError::from(SqlError::InvalidArgument(String::from( + "no fields to update", + )))); + } + updates.insert( + String::from("updated_at"), + Value::from(utils::time_created_on()), + ); + let mut set_parts = Vec::with_capacity(updates.len()); + let mut bind_values = Vec::with_capacity(updates.len() + 1); + for (column, value) in updates { + set_parts.push(format!("{column} = ?")); + bind_values.push(utils::to_db_bind_value(&value)); + } + let id_for_lookup = match opts.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &opts.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; + model.id + } + }; + 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 _ = exec.exec(&sql, ¶ms_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(IResult { result: updated }) +} + +pub fn delete<E: SqlExecutor>( + exec: &E, + opts: &ILogErrorDelete, +) -> Result<ILogErrorDeleteResolve, IError<SqlError>> { + let id_for_lookup = match opts { + ILogErrorDelete::On(args) => match args.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &args.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, ¶ms_json)?; + if outcome.changes == 0 { + return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); + } + Ok(IResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica-db/src/models/media_image.rs b/crates/replica-db/src/models/media_image.rs @@ -0,0 +1,203 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::media_image::{ + IMediaImageCreate, IMediaImageCreateResolve, IMediaImageDelete, IMediaImageDeleteResolve, + IMediaImageFieldsFilter, IMediaImageFindMany, IMediaImageFindManyResolve, IMediaImageFindOne, + IMediaImageFindOneResolve, IMediaImageUpdate, IMediaImageUpdateResolve, MediaImage, + MediaImageFindManyRel, MediaImageQueryBindValues, +}; +use radroots_types::types::{IError, IResult, IResultList}; +use serde_json::Value; + +const TABLE_NAME: &str = "media_image"; + +pub fn create<E: SqlExecutor>( + exec: &E, + opts: &IMediaImageCreate, +) -> Result<IMediaImageCreateResolve, IError<SqlError>> { + let field_map = utils::to_object_map(opts)?; + let id = utils::uuidv4(); + let now = utils::time_created_on(); + let meta: [(&str, Value); 3] = [ + ("id", Value::from(id.clone())), + ("created_at", Value::from(now.clone())), + ("updated_at", Value::from(now.clone())), + ]; + let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); + let params_json = utils::to_params_json(bind_values)?; + let _ = exec.exec(&sql, ¶ms_json)?; + let on = MediaImageQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; + Ok(IResult { result }) +} + +pub fn find_one<E: SqlExecutor>( + exec: &E, + opts: &IMediaImageFindOne, +) -> Result<IMediaImageFindOneResolve, IError<SqlError>> { + let result = match opts { + IMediaImageFindOne::On(args) => find_one_by_on(exec, &args.on)?, + IMediaImageFindOne::Rel(args) => find_one_by_rel(exec, &args.rel)?, + }; + Ok(IResult { result }) +} + +pub fn find_many<E: SqlExecutor>( + exec: &E, + opts: &IMediaImageFindMany, +) -> Result<IMediaImageFindManyResolve, IError<SqlError>> { + let results = match opts { + IMediaImageFindMany::Filter { filter } => find_many_filter(exec, filter)?, + IMediaImageFindMany::Rel { rel } => find_many_by_rel(exec, rel)?, + }; + Ok(IResultList { results }) +} + +fn find_many_filter<E: SqlExecutor>( + exec: &E, + filter: &Option<IMediaImageFieldsFilter>, +) -> Result<Vec<MediaImage>, IError<SqlError>> { + let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); + let params_json = utils::to_params_json(bind_values)?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<MediaImage> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on<E: SqlExecutor>( + exec: &E, + on: &MediaImageQueryBindValues, +) -> Result<Option<MediaImage>, IError<SqlError>> { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value])?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<MediaImage> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn rel_query(rel: &MediaImageFindManyRel) -> (&'static str, Vec<Value>) { + match rel { + MediaImageFindManyRel::OnTradeProduct(args) => ( + "SELECT mu.* FROM media_image mu JOIN trade_product_media tp_lg ON mu.id = tp_lg.tb_mu WHERE tp_lg.tb_tp = ?", + vec![Value::from(args.id.clone())], + ), + MediaImageFindManyRel::OffTradeProduct(args) => ( + "SELECT mu.* FROM media_image mu WHERE NOT EXISTS (SELECT 1 FROM trade_product_media tp_lg WHERE tp_lg.tb_mu = mu.id AND tp_lg.tb_tp = ?)", + vec![Value::from(args.id.clone())], + ), + } +} + +fn find_one_by_rel<E: SqlExecutor>( + exec: &E, + rel: &MediaImageFindManyRel, +) -> Result<Option<MediaImage>, IError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values)?; + let sql = format!("{sql} LIMIT 1;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<MediaImage> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn find_many_by_rel<E: SqlExecutor>( + exec: &E, + rel: &MediaImageFindManyRel, +) -> Result<Vec<MediaImage>, IError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values)?; + let sql = format!("{sql};"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<MediaImage> = utils::parse_json(&json)?; + Ok(rows) +} + +fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<MediaImage, IError<SqlError>> { + let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<MediaImage> = utils::parse_json(&json)?; + rows.pop() + .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update<E: SqlExecutor>( + exec: &E, + opts: &IMediaImageUpdate, +) -> Result<IMediaImageUpdateResolve, IError<SqlError>> { + let mut updates = utils::to_partial_object_map(&opts.fields)?; + if updates.is_empty() { + return Err(IError::from(SqlError::InvalidArgument(String::from( + "no fields to update", + )))); + } + updates.insert( + String::from("updated_at"), + Value::from(utils::time_created_on()), + ); + let mut set_parts = Vec::with_capacity(updates.len()); + let mut bind_values = Vec::with_capacity(updates.len() + 1); + for (column, value) in updates { + set_parts.push(format!("{column} = ?")); + bind_values.push(utils::to_db_bind_value(&value)); + } + let id_for_lookup = match opts.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &opts.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; + model.id + } + }; + 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 _ = exec.exec(&sql, ¶ms_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(IResult { result: updated }) +} + +pub fn delete<E: SqlExecutor>( + exec: &E, + opts: &IMediaImageDelete, +) -> Result<IMediaImageDeleteResolve, IError<SqlError>> { + let id_for_lookup = match opts { + IMediaImageDelete::On(args) => match args.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &args.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; + model.id + } + }, + IMediaImageDelete::Rel(args) => { + let found = find_one_by_rel(exec, &args.rel)?; + let model = found.ok_or(IError::from(SqlError::NotFound(rel_lookup_key(&args.rel))))?; + model.id + } + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, ¶ms_json)?; + if outcome.changes == 0 { + return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); + } + Ok(IResult { + result: id_for_lookup, + }) +} + +fn rel_lookup_key(rel: &MediaImageFindManyRel) -> String { + match rel { + MediaImageFindManyRel::OnTradeProduct(args) => { + format!("on_trade_product:{}", args.id.as_str()) + } + MediaImageFindManyRel::OffTradeProduct(args) => { + format!("off_trade_product:{}", args.id.as_str()) + } + } +} diff --git a/crates/tangle-db/src/models/mod.rs b/crates/replica-db/src/models/mod.rs diff --git a/crates/replica-db/src/models/nostr_event_state.rs b/crates/replica-db/src/models/nostr_event_state.rs @@ -0,0 +1,147 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::nostr_event_state::{ + INostrEventStateCreate, INostrEventStateCreateResolve, INostrEventStateDelete, + INostrEventStateDeleteResolve, INostrEventStateFieldsFilter, INostrEventStateFindMany, + INostrEventStateFindManyResolve, INostrEventStateFindOne, INostrEventStateFindOneResolve, + INostrEventStateUpdate, INostrEventStateUpdateResolve, NostrEventState, + NostrEventStateQueryBindValues, +}; +use radroots_types::types::{IError, IResult, IResultList}; +use serde_json::Value; + +const TABLE_NAME: &str = "nostr_event_state"; + +pub fn create<E: SqlExecutor>( + exec: &E, + opts: &INostrEventStateCreate, +) -> Result<INostrEventStateCreateResolve, IError<SqlError>> { + let field_map = utils::to_object_map(opts)?; + let id = utils::uuidv4(); + let now = utils::time_created_on(); + let meta: [(&str, Value); 3] = [ + ("id", Value::from(id.clone())), + ("created_at", Value::from(now.clone())), + ("updated_at", Value::from(now.clone())), + ]; + let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); + let params_json = utils::to_params_json(bind_values)?; + let _ = exec.exec(&sql, ¶ms_json)?; + let on = NostrEventStateQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; + Ok(IResult { result }) +} + +pub fn find_one<E: SqlExecutor>( + exec: &E, + opts: &INostrEventStateFindOne, +) -> Result<INostrEventStateFindOneResolve, IError<SqlError>> { + let result = match opts { + INostrEventStateFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(IResult { result }) +} + +pub fn find_many<E: SqlExecutor>( + exec: &E, + opts: &INostrEventStateFindMany, +) -> Result<INostrEventStateFindManyResolve, IError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(IResultList { results }) +} + +fn find_many_filter<E: SqlExecutor>( + exec: &E, + filter: &Option<INostrEventStateFieldsFilter>, +) -> Result<Vec<NostrEventState>, IError<SqlError>> { + let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); + let params_json = utils::to_params_json(bind_values)?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<NostrEventState> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on<E: SqlExecutor>( + exec: &E, + on: &NostrEventStateQueryBindValues, +) -> Result<Option<NostrEventState>, IError<SqlError>> { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value])?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<NostrEventState> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<NostrEventState, IError<SqlError>> { + let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<NostrEventState> = utils::parse_json(&json)?; + rows.pop() + .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update<E: SqlExecutor>( + exec: &E, + opts: &INostrEventStateUpdate, +) -> Result<INostrEventStateUpdateResolve, IError<SqlError>> { + let mut updates = utils::to_partial_object_map(&opts.fields)?; + if updates.is_empty() { + return Err(IError::from(SqlError::InvalidArgument(String::from( + "no fields to update", + )))); + } + updates.insert( + String::from("updated_at"), + Value::from(utils::time_created_on()), + ); + let mut set_parts = Vec::with_capacity(updates.len()); + let mut bind_values = Vec::with_capacity(updates.len() + 1); + for (column, value) in updates { + set_parts.push(format!("{column} = ?")); + bind_values.push(utils::to_db_bind_value(&value)); + } + let id_for_lookup = match opts.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &opts.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; + model.id + } + }; + 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 _ = exec.exec(&sql, ¶ms_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(IResult { result: updated }) +} + +pub fn delete<E: SqlExecutor>( + exec: &E, + opts: &INostrEventStateDelete, +) -> Result<INostrEventStateDeleteResolve, IError<SqlError>> { + let id_for_lookup = match opts { + INostrEventStateDelete::On(args) => match args.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &args.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, ¶ms_json)?; + if outcome.changes == 0 { + return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); + } + Ok(IResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica-db/src/models/nostr_profile.rs b/crates/replica-db/src/models/nostr_profile.rs @@ -0,0 +1,200 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::nostr_profile::{ + INostrProfileCreate, INostrProfileCreateResolve, INostrProfileDelete, + INostrProfileDeleteResolve, INostrProfileFieldsFilter, INostrProfileFindMany, + INostrProfileFindManyResolve, INostrProfileFindOne, INostrProfileFindOneResolve, + INostrProfileUpdate, INostrProfileUpdateResolve, NostrProfile, NostrProfileFindManyRel, + NostrProfileQueryBindValues, +}; +use radroots_types::types::{IError, IResult, IResultList}; +use serde_json::Value; + +const TABLE_NAME: &str = "nostr_profile"; + +pub fn create<E: SqlExecutor>( + exec: &E, + opts: &INostrProfileCreate, +) -> Result<INostrProfileCreateResolve, IError<SqlError>> { + let field_map = utils::to_object_map(opts)?; + let id = utils::uuidv4(); + let now = utils::time_created_on(); + let meta: [(&str, Value); 3] = [ + ("id", Value::from(id.clone())), + ("created_at", Value::from(now.clone())), + ("updated_at", Value::from(now.clone())), + ]; + let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); + let params_json = utils::to_params_json(bind_values)?; + let _ = exec.exec(&sql, ¶ms_json)?; + let on = NostrProfileQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; + Ok(IResult { result }) +} + +pub fn find_one<E: SqlExecutor>( + exec: &E, + opts: &INostrProfileFindOne, +) -> Result<INostrProfileFindOneResolve, IError<SqlError>> { + let result = match opts { + INostrProfileFindOne::On(args) => find_one_by_on(exec, &args.on)?, + INostrProfileFindOne::Rel(args) => find_one_by_rel(exec, &args.rel)?, + }; + Ok(IResult { result }) +} + +pub fn find_many<E: SqlExecutor>( + exec: &E, + opts: &INostrProfileFindMany, +) -> Result<INostrProfileFindManyResolve, IError<SqlError>> { + let results = match opts { + INostrProfileFindMany::Filter { filter } => find_many_filter(exec, filter)?, + INostrProfileFindMany::Rel { rel } => find_many_by_rel(exec, rel)?, + }; + Ok(IResultList { results }) +} + +fn find_many_filter<E: SqlExecutor>( + exec: &E, + filter: &Option<INostrProfileFieldsFilter>, +) -> Result<Vec<NostrProfile>, IError<SqlError>> { + let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); + let params_json = utils::to_params_json(bind_values)?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<NostrProfile> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on<E: SqlExecutor>( + exec: &E, + on: &NostrProfileQueryBindValues, +) -> Result<Option<NostrProfile>, IError<SqlError>> { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value])?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<NostrProfile> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn rel_query(rel: &NostrProfileFindManyRel) -> (&'static str, Vec<Value>) { + match rel { + NostrProfileFindManyRel::OnRelay(args) => ( + "SELECT pr.* FROM nostr_profile pr JOIN nostr_profile_relay pr_rl ON pr.id = pr_rl.tb_pr WHERE pr_rl.tb_rl = ?", + vec![Value::from(args.id.clone())], + ), + NostrProfileFindManyRel::OffRelay(args) => ( + "SELECT pr.* FROM nostr_profile pr WHERE NOT EXISTS (SELECT 1 FROM nostr_profile_relay pr_rl WHERE pr_rl.tb_pr = pr.id AND pr_rl.tb_rl = ?)", + vec![Value::from(args.id.clone())], + ), + } +} + +fn find_one_by_rel<E: SqlExecutor>( + exec: &E, + rel: &NostrProfileFindManyRel, +) -> Result<Option<NostrProfile>, IError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values)?; + let sql = format!("{sql} LIMIT 1;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<NostrProfile> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn find_many_by_rel<E: SqlExecutor>( + exec: &E, + rel: &NostrProfileFindManyRel, +) -> Result<Vec<NostrProfile>, IError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values)?; + let sql = format!("{sql};"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<NostrProfile> = utils::parse_json(&json)?; + Ok(rows) +} + +fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<NostrProfile, IError<SqlError>> { + let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<NostrProfile> = utils::parse_json(&json)?; + rows.pop() + .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update<E: SqlExecutor>( + exec: &E, + opts: &INostrProfileUpdate, +) -> Result<INostrProfileUpdateResolve, IError<SqlError>> { + let mut updates = utils::to_partial_object_map(&opts.fields)?; + if updates.is_empty() { + return Err(IError::from(SqlError::InvalidArgument(String::from( + "no fields to update", + )))); + } + updates.insert( + String::from("updated_at"), + Value::from(utils::time_created_on()), + ); + let mut set_parts = Vec::with_capacity(updates.len()); + let mut bind_values = Vec::with_capacity(updates.len() + 1); + for (column, value) in updates { + set_parts.push(format!("{column} = ?")); + bind_values.push(utils::to_db_bind_value(&value)); + } + let id_for_lookup = match opts.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &opts.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; + model.id + } + }; + 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 _ = exec.exec(&sql, ¶ms_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(IResult { result: updated }) +} + +pub fn delete<E: SqlExecutor>( + exec: &E, + opts: &INostrProfileDelete, +) -> Result<INostrProfileDeleteResolve, IError<SqlError>> { + let id_for_lookup = match opts { + INostrProfileDelete::On(args) => match args.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &args.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; + model.id + } + }, + INostrProfileDelete::Rel(args) => { + let found = find_one_by_rel(exec, &args.rel)?; + let model = found.ok_or(IError::from(SqlError::NotFound(rel_lookup_key(&args.rel))))?; + model.id + } + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, ¶ms_json)?; + if outcome.changes == 0 { + return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); + } + Ok(IResult { + result: id_for_lookup, + }) +} + +fn rel_lookup_key(rel: &NostrProfileFindManyRel) -> String { + match rel { + NostrProfileFindManyRel::OnRelay(args) => format!("on_relay:{}", args.id.as_str()), + NostrProfileFindManyRel::OffRelay(args) => format!("off_relay:{}", args.id.as_str()), + } +} diff --git a/crates/replica-db/src/models/nostr_profile_relay.rs b/crates/replica-db/src/models/nostr_profile_relay.rs @@ -0,0 +1,45 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::nostr_profile_relay::{ + INostrProfileRelayRelation, INostrProfileRelayResolve, +}; +use radroots_types::types::{IError, IResultPass}; +use serde_json::Value; + +const TABLE_NAME: &str = "nostr_profile_relay"; + +pub fn set<E: SqlExecutor>( + exec: &E, + opts: &INostrProfileRelayRelation, +) -> Result<INostrProfileRelayResolve, IError<SqlError>> { + let mut query_vals: Vec<Value> = Vec::with_capacity(2); + let (nostr_profile_column, nostr_profile_value) = opts.nostr_profile.to_filter_param(); + query_vals.push(nostr_profile_value); + let (nostr_relay_column, nostr_relay_value) = opts.nostr_relay.to_filter_param(); + query_vals.push(nostr_relay_value); + let query = format!( + "INSERT INTO {} (tb_pr, tb_rl) VALUES ((SELECT id FROM nostr_profile WHERE {} = ?), (SELECT id FROM nostr_relay WHERE {} = ?));", + TABLE_NAME, nostr_profile_column, nostr_relay_column + ); + let params_json = utils::to_params_json(query_vals)?; + let _ = exec.exec(&query, ¶ms_json)?; + Ok(IResultPass { pass: true }) +} + +pub fn unset<E: SqlExecutor>( + exec: &E, + opts: &INostrProfileRelayRelation, +) -> Result<INostrProfileRelayResolve, IError<SqlError>> { + let mut query_vals: Vec<Value> = Vec::with_capacity(2); + let (nostr_profile_column, nostr_profile_value) = opts.nostr_profile.to_filter_param(); + query_vals.push(nostr_profile_value); + let (nostr_relay_column, nostr_relay_value) = opts.nostr_relay.to_filter_param(); + query_vals.push(nostr_relay_value); + let query = format!( + "DELETE FROM {} WHERE tb_pr = (SELECT id FROM nostr_profile WHERE {} = ?) AND tb_rl = (SELECT id FROM nostr_relay WHERE {} = ?);", + TABLE_NAME, nostr_profile_column, nostr_relay_column + ); + let params_json = utils::to_params_json(query_vals)?; + let _ = exec.exec(&query, ¶ms_json)?; + Ok(IResultPass { pass: true }) +} diff --git a/crates/replica-db/src/models/nostr_relay.rs b/crates/replica-db/src/models/nostr_relay.rs @@ -0,0 +1,203 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::nostr_relay::{ + INostrRelayCreate, INostrRelayCreateResolve, INostrRelayDelete, INostrRelayDeleteResolve, + INostrRelayFieldsFilter, INostrRelayFindMany, INostrRelayFindManyResolve, INostrRelayFindOne, + INostrRelayFindOneResolve, INostrRelayUpdate, INostrRelayUpdateResolve, NostrRelay, + NostrRelayFindManyRel, NostrRelayQueryBindValues, +}; +use radroots_types::types::{IError, IResult, IResultList}; +use serde_json::Value; + +const TABLE_NAME: &str = "nostr_relay"; + +pub fn create<E: SqlExecutor>( + exec: &E, + opts: &INostrRelayCreate, +) -> Result<INostrRelayCreateResolve, IError<SqlError>> { + let field_map = utils::to_object_map(opts)?; + let id = utils::uuidv4(); + let now = utils::time_created_on(); + let meta: [(&str, Value); 3] = [ + ("id", Value::from(id.clone())), + ("created_at", Value::from(now.clone())), + ("updated_at", Value::from(now.clone())), + ]; + let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); + let params_json = utils::to_params_json(bind_values)?; + let _ = exec.exec(&sql, ¶ms_json)?; + let on = NostrRelayQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; + Ok(IResult { result }) +} + +pub fn find_one<E: SqlExecutor>( + exec: &E, + opts: &INostrRelayFindOne, +) -> Result<INostrRelayFindOneResolve, IError<SqlError>> { + let result = match opts { + INostrRelayFindOne::On(args) => find_one_by_on(exec, &args.on)?, + INostrRelayFindOne::Rel(args) => find_one_by_rel(exec, &args.rel)?, + }; + Ok(IResult { result }) +} + +pub fn find_many<E: SqlExecutor>( + exec: &E, + opts: &INostrRelayFindMany, +) -> Result<INostrRelayFindManyResolve, IError<SqlError>> { + let results = match opts { + INostrRelayFindMany::Filter { filter } => find_many_filter(exec, filter)?, + INostrRelayFindMany::Rel { rel } => find_many_by_rel(exec, rel)?, + }; + Ok(IResultList { results }) +} + +fn find_many_filter<E: SqlExecutor>( + exec: &E, + filter: &Option<INostrRelayFieldsFilter>, +) -> Result<Vec<NostrRelay>, IError<SqlError>> { + let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); + let params_json = utils::to_params_json(bind_values)?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<NostrRelay> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on<E: SqlExecutor>( + exec: &E, + on: &NostrRelayQueryBindValues, +) -> Result<Option<NostrRelay>, IError<SqlError>> { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value])?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<NostrRelay> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn rel_query(rel: &NostrRelayFindManyRel) -> (&'static str, Vec<Value>) { + match rel { + NostrRelayFindManyRel::OnProfile(args) => ( + "SELECT rl.* FROM nostr_relay rl JOIN nostr_profile_relay pr_rl ON rl.id = pr_rl.tb_rl JOIN nostr_profile pr ON pr.id = pr_rl.tb_pr WHERE pr.public_key = ?", + vec![Value::from(args.public_key.clone())], + ), + NostrRelayFindManyRel::OffProfile(args) => ( + "SELECT rl.* FROM nostr_relay rl LEFT JOIN nostr_profile_relay pr_rl ON rl.id = pr_rl.tb_rl LEFT JOIN nostr_profile pr ON pr.id = pr_rl.tb_pr WHERE pr.public_key <> ?", + vec![Value::from(args.public_key.clone())], + ), + } +} + +fn find_one_by_rel<E: SqlExecutor>( + exec: &E, + rel: &NostrRelayFindManyRel, +) -> Result<Option<NostrRelay>, IError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values)?; + let sql = format!("{sql} LIMIT 1;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<NostrRelay> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn find_many_by_rel<E: SqlExecutor>( + exec: &E, + rel: &NostrRelayFindManyRel, +) -> Result<Vec<NostrRelay>, IError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values)?; + let sql = format!("{sql};"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<NostrRelay> = utils::parse_json(&json)?; + Ok(rows) +} + +fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<NostrRelay, IError<SqlError>> { + let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<NostrRelay> = utils::parse_json(&json)?; + rows.pop() + .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update<E: SqlExecutor>( + exec: &E, + opts: &INostrRelayUpdate, +) -> Result<INostrRelayUpdateResolve, IError<SqlError>> { + let mut updates = utils::to_partial_object_map(&opts.fields)?; + if updates.is_empty() { + return Err(IError::from(SqlError::InvalidArgument(String::from( + "no fields to update", + )))); + } + updates.insert( + String::from("updated_at"), + Value::from(utils::time_created_on()), + ); + let mut set_parts = Vec::with_capacity(updates.len()); + let mut bind_values = Vec::with_capacity(updates.len() + 1); + for (column, value) in updates { + set_parts.push(format!("{column} = ?")); + bind_values.push(utils::to_db_bind_value(&value)); + } + let id_for_lookup = match opts.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &opts.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; + model.id + } + }; + 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 _ = exec.exec(&sql, ¶ms_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(IResult { result: updated }) +} + +pub fn delete<E: SqlExecutor>( + exec: &E, + opts: &INostrRelayDelete, +) -> Result<INostrRelayDeleteResolve, IError<SqlError>> { + let id_for_lookup = match opts { + INostrRelayDelete::On(args) => match args.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &args.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; + model.id + } + }, + INostrRelayDelete::Rel(args) => { + let found = find_one_by_rel(exec, &args.rel)?; + let model = found.ok_or(IError::from(SqlError::NotFound(rel_lookup_key(&args.rel))))?; + model.id + } + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, ¶ms_json)?; + if outcome.changes == 0 { + return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); + } + Ok(IResult { + result: id_for_lookup, + }) +} + +fn rel_lookup_key(rel: &NostrRelayFindManyRel) -> String { + match rel { + NostrRelayFindManyRel::OnProfile(args) => { + format!("on_profile:{}", args.public_key.as_str()) + } + NostrRelayFindManyRel::OffProfile(args) => { + format!("off_profile:{}", args.public_key.as_str()) + } + } +} diff --git a/crates/replica-db/src/models/plot.rs b/crates/replica-db/src/models/plot.rs @@ -0,0 +1,145 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::plot::{ + IPlotCreate, IPlotCreateResolve, IPlotDelete, IPlotDeleteResolve, IPlotFieldsFilter, + IPlotFindMany, IPlotFindManyResolve, IPlotFindOne, IPlotFindOneResolve, IPlotUpdate, + IPlotUpdateResolve, Plot, PlotQueryBindValues, +}; +use radroots_types::types::{IError, IResult, IResultList}; +use serde_json::Value; + +const TABLE_NAME: &str = "plot"; + +pub fn create<E: SqlExecutor>( + exec: &E, + opts: &IPlotCreate, +) -> Result<IPlotCreateResolve, IError<SqlError>> { + let field_map = utils::to_object_map(opts)?; + let id = utils::uuidv4(); + let now = utils::time_created_on(); + let meta: [(&str, Value); 3] = [ + ("id", Value::from(id.clone())), + ("created_at", Value::from(now.clone())), + ("updated_at", Value::from(now.clone())), + ]; + let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); + let params_json = utils::to_params_json(bind_values)?; + let _ = exec.exec(&sql, ¶ms_json)?; + let on = PlotQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; + Ok(IResult { result }) +} + +pub fn find_one<E: SqlExecutor>( + exec: &E, + opts: &IPlotFindOne, +) -> Result<IPlotFindOneResolve, IError<SqlError>> { + let result = match opts { + IPlotFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(IResult { result }) +} + +pub fn find_many<E: SqlExecutor>( + exec: &E, + opts: &IPlotFindMany, +) -> Result<IPlotFindManyResolve, IError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(IResultList { results }) +} + +fn find_many_filter<E: SqlExecutor>( + exec: &E, + filter: &Option<IPlotFieldsFilter>, +) -> Result<Vec<Plot>, IError<SqlError>> { + let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); + let params_json = utils::to_params_json(bind_values)?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<Plot> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on<E: SqlExecutor>( + exec: &E, + on: &PlotQueryBindValues, +) -> Result<Option<Plot>, IError<SqlError>> { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value])?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<Plot> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<Plot, IError<SqlError>> { + let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<Plot> = utils::parse_json(&json)?; + rows.pop() + .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update<E: SqlExecutor>( + exec: &E, + opts: &IPlotUpdate, +) -> Result<IPlotUpdateResolve, IError<SqlError>> { + let mut updates = utils::to_partial_object_map(&opts.fields)?; + if updates.is_empty() { + return Err(IError::from(SqlError::InvalidArgument(String::from( + "no fields to update", + )))); + } + updates.insert( + String::from("updated_at"), + Value::from(utils::time_created_on()), + ); + let mut set_parts = Vec::with_capacity(updates.len()); + let mut bind_values = Vec::with_capacity(updates.len() + 1); + for (column, value) in updates { + set_parts.push(format!("{column} = ?")); + bind_values.push(utils::to_db_bind_value(&value)); + } + let id_for_lookup = match opts.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &opts.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; + model.id + } + }; + 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 _ = exec.exec(&sql, ¶ms_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(IResult { result: updated }) +} + +pub fn delete<E: SqlExecutor>( + exec: &E, + opts: &IPlotDelete, +) -> Result<IPlotDeleteResolve, IError<SqlError>> { + let id_for_lookup = match opts { + IPlotDelete::On(args) => match args.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &args.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, ¶ms_json)?; + if outcome.changes == 0 { + return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); + } + Ok(IResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica-db/src/models/plot_gcs_location.rs b/crates/replica-db/src/models/plot_gcs_location.rs @@ -0,0 +1,147 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::plot_gcs_location::{ + IPlotGcsLocationCreate, IPlotGcsLocationCreateResolve, IPlotGcsLocationDelete, + IPlotGcsLocationDeleteResolve, IPlotGcsLocationFieldsFilter, IPlotGcsLocationFindMany, + IPlotGcsLocationFindManyResolve, IPlotGcsLocationFindOne, IPlotGcsLocationFindOneResolve, + IPlotGcsLocationUpdate, IPlotGcsLocationUpdateResolve, PlotGcsLocation, + PlotGcsLocationQueryBindValues, +}; +use radroots_types::types::{IError, IResult, IResultList}; +use serde_json::Value; + +const TABLE_NAME: &str = "plot_gcs_location"; + +pub fn create<E: SqlExecutor>( + exec: &E, + opts: &IPlotGcsLocationCreate, +) -> Result<IPlotGcsLocationCreateResolve, IError<SqlError>> { + let field_map = utils::to_object_map(opts)?; + let id = utils::uuidv4(); + let now = utils::time_created_on(); + let meta: [(&str, Value); 3] = [ + ("id", Value::from(id.clone())), + ("created_at", Value::from(now.clone())), + ("updated_at", Value::from(now.clone())), + ]; + let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); + let params_json = utils::to_params_json(bind_values)?; + let _ = exec.exec(&sql, ¶ms_json)?; + let on = PlotGcsLocationQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; + Ok(IResult { result }) +} + +pub fn find_one<E: SqlExecutor>( + exec: &E, + opts: &IPlotGcsLocationFindOne, +) -> Result<IPlotGcsLocationFindOneResolve, IError<SqlError>> { + let result = match opts { + IPlotGcsLocationFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(IResult { result }) +} + +pub fn find_many<E: SqlExecutor>( + exec: &E, + opts: &IPlotGcsLocationFindMany, +) -> Result<IPlotGcsLocationFindManyResolve, IError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(IResultList { results }) +} + +fn find_many_filter<E: SqlExecutor>( + exec: &E, + filter: &Option<IPlotGcsLocationFieldsFilter>, +) -> Result<Vec<PlotGcsLocation>, IError<SqlError>> { + let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); + let params_json = utils::to_params_json(bind_values)?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<PlotGcsLocation> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on<E: SqlExecutor>( + exec: &E, + on: &PlotGcsLocationQueryBindValues, +) -> Result<Option<PlotGcsLocation>, IError<SqlError>> { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value])?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<PlotGcsLocation> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<PlotGcsLocation, IError<SqlError>> { + let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<PlotGcsLocation> = utils::parse_json(&json)?; + rows.pop() + .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update<E: SqlExecutor>( + exec: &E, + opts: &IPlotGcsLocationUpdate, +) -> Result<IPlotGcsLocationUpdateResolve, IError<SqlError>> { + let mut updates = utils::to_partial_object_map(&opts.fields)?; + if updates.is_empty() { + return Err(IError::from(SqlError::InvalidArgument(String::from( + "no fields to update", + )))); + } + updates.insert( + String::from("updated_at"), + Value::from(utils::time_created_on()), + ); + let mut set_parts = Vec::with_capacity(updates.len()); + let mut bind_values = Vec::with_capacity(updates.len() + 1); + for (column, value) in updates { + set_parts.push(format!("{column} = ?")); + bind_values.push(utils::to_db_bind_value(&value)); + } + let id_for_lookup = match opts.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &opts.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; + model.id + } + }; + 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 _ = exec.exec(&sql, ¶ms_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(IResult { result: updated }) +} + +pub fn delete<E: SqlExecutor>( + exec: &E, + opts: &IPlotGcsLocationDelete, +) -> Result<IPlotGcsLocationDeleteResolve, IError<SqlError>> { + let id_for_lookup = match opts { + IPlotGcsLocationDelete::On(args) => match args.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &args.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, ¶ms_json)?; + if outcome.changes == 0 { + return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); + } + Ok(IResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica-db/src/models/plot_tag.rs b/crates/replica-db/src/models/plot_tag.rs @@ -0,0 +1,145 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::plot_tag::{ + IPlotTagCreate, IPlotTagCreateResolve, IPlotTagDelete, IPlotTagDeleteResolve, + IPlotTagFieldsFilter, IPlotTagFindMany, IPlotTagFindManyResolve, IPlotTagFindOne, + IPlotTagFindOneResolve, IPlotTagUpdate, IPlotTagUpdateResolve, PlotTag, PlotTagQueryBindValues, +}; +use radroots_types::types::{IError, IResult, IResultList}; +use serde_json::Value; + +const TABLE_NAME: &str = "plot_tag"; + +pub fn create<E: SqlExecutor>( + exec: &E, + opts: &IPlotTagCreate, +) -> Result<IPlotTagCreateResolve, IError<SqlError>> { + let field_map = utils::to_object_map(opts)?; + let id = utils::uuidv4(); + let now = utils::time_created_on(); + let meta: [(&str, Value); 3] = [ + ("id", Value::from(id.clone())), + ("created_at", Value::from(now.clone())), + ("updated_at", Value::from(now.clone())), + ]; + let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); + let params_json = utils::to_params_json(bind_values)?; + let _ = exec.exec(&sql, ¶ms_json)?; + let on = PlotTagQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; + Ok(IResult { result }) +} + +pub fn find_one<E: SqlExecutor>( + exec: &E, + opts: &IPlotTagFindOne, +) -> Result<IPlotTagFindOneResolve, IError<SqlError>> { + let result = match opts { + IPlotTagFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(IResult { result }) +} + +pub fn find_many<E: SqlExecutor>( + exec: &E, + opts: &IPlotTagFindMany, +) -> Result<IPlotTagFindManyResolve, IError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(IResultList { results }) +} + +fn find_many_filter<E: SqlExecutor>( + exec: &E, + filter: &Option<IPlotTagFieldsFilter>, +) -> Result<Vec<PlotTag>, IError<SqlError>> { + let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); + let params_json = utils::to_params_json(bind_values)?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<PlotTag> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on<E: SqlExecutor>( + exec: &E, + on: &PlotTagQueryBindValues, +) -> Result<Option<PlotTag>, IError<SqlError>> { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value])?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<PlotTag> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<PlotTag, IError<SqlError>> { + let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<PlotTag> = utils::parse_json(&json)?; + rows.pop() + .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update<E: SqlExecutor>( + exec: &E, + opts: &IPlotTagUpdate, +) -> Result<IPlotTagUpdateResolve, IError<SqlError>> { + let mut updates = utils::to_partial_object_map(&opts.fields)?; + if updates.is_empty() { + return Err(IError::from(SqlError::InvalidArgument(String::from( + "no fields to update", + )))); + } + updates.insert( + String::from("updated_at"), + Value::from(utils::time_created_on()), + ); + let mut set_parts = Vec::with_capacity(updates.len()); + let mut bind_values = Vec::with_capacity(updates.len() + 1); + for (column, value) in updates { + set_parts.push(format!("{column} = ?")); + bind_values.push(utils::to_db_bind_value(&value)); + } + let id_for_lookup = match opts.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &opts.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; + model.id + } + }; + 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 _ = exec.exec(&sql, ¶ms_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(IResult { result: updated }) +} + +pub fn delete<E: SqlExecutor>( + exec: &E, + opts: &IPlotTagDelete, +) -> Result<IPlotTagDeleteResolve, IError<SqlError>> { + let id_for_lookup = match opts { + IPlotTagDelete::On(args) => match args.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &args.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, ¶ms_json)?; + if outcome.changes == 0 { + return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); + } + Ok(IResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica-db/src/models/trade_product.rs b/crates/replica-db/src/models/trade_product.rs @@ -0,0 +1,136 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::trade_product::{ + ITradeProductCreate, ITradeProductCreateResolve, ITradeProductDelete, + ITradeProductDeleteResolve, ITradeProductFieldsFilter, ITradeProductFindMany, + ITradeProductFindManyResolve, ITradeProductFindOne, ITradeProductFindOneResolve, + ITradeProductUpdate, ITradeProductUpdateResolve, TradeProduct, TradeProductQueryBindValues, +}; +use radroots_types::types::{IError, IResult, IResultList}; +use serde_json::Value; + +const TABLE_NAME: &str = "trade_product"; + +pub fn create<E: SqlExecutor>( + exec: &E, + opts: &ITradeProductCreate, +) -> Result<ITradeProductCreateResolve, IError<SqlError>> { + let field_map = utils::to_object_map(opts)?; + let id = utils::uuidv4(); + let now = utils::time_created_on(); + let meta: [(&str, Value); 3] = [ + ("id", Value::from(id.clone())), + ("created_at", Value::from(now.clone())), + ("updated_at", Value::from(now.clone())), + ]; + let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); + let params_json = utils::to_params_json(bind_values)?; + let _ = exec.exec(&sql, ¶ms_json)?; + let on = TradeProductQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; + Ok(IResult { result }) +} + +pub fn find_one<E: SqlExecutor>( + exec: &E, + opts: &ITradeProductFindOne, +) -> Result<ITradeProductFindOneResolve, IError<SqlError>> { + let result = match opts { + ITradeProductFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(IResult { result }) +} + +pub fn find_many<E: SqlExecutor>( + exec: &E, + opts: &ITradeProductFindMany, +) -> Result<ITradeProductFindManyResolve, IError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(IResultList { results }) +} + +fn find_many_filter<E: SqlExecutor>( + exec: &E, + filter: &Option<ITradeProductFieldsFilter>, +) -> Result<Vec<TradeProduct>, IError<SqlError>> { + let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); + let params_json = utils::to_params_json(bind_values)?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<TradeProduct> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on<E: SqlExecutor>( + exec: &E, + on: &TradeProductQueryBindValues, +) -> Result<Option<TradeProduct>, IError<SqlError>> { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value])?; + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<TradeProduct> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<TradeProduct, IError<SqlError>> { + let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<TradeProduct> = utils::parse_json(&json)?; + rows.pop() + .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update<E: SqlExecutor>( + exec: &E, + opts: &ITradeProductUpdate, +) -> Result<ITradeProductUpdateResolve, IError<SqlError>> { + let mut updates = utils::to_partial_object_map(&opts.fields)?; + if updates.is_empty() { + return Err(IError::from(SqlError::InvalidArgument(String::from( + "no fields to update", + )))); + } + updates.insert( + String::from("updated_at"), + Value::from(utils::time_created_on()), + ); + let mut set_parts = Vec::with_capacity(updates.len()); + let mut bind_values = Vec::with_capacity(updates.len() + 1); + for (column, value) in updates { + set_parts.push(format!("{column} = ?")); + bind_values.push(utils::to_db_bind_value(&value)); + } + let id_for_lookup = match &opts.on { + TradeProductQueryBindValues::Id { id } => id.clone(), + }; + 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 _ = exec.exec(&sql, ¶ms_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(IResult { result: updated }) +} + +pub fn delete<E: SqlExecutor>( + exec: &E, + opts: &ITradeProductDelete, +) -> Result<ITradeProductDeleteResolve, IError<SqlError>> { + let id_for_lookup = match opts { + ITradeProductDelete::On(args) => match &args.on { + TradeProductQueryBindValues::Id { id } => id.clone(), + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, ¶ms_json)?; + if outcome.changes == 0 { + return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); + } + Ok(IResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica-db/src/models/trade_product_location.rs b/crates/replica-db/src/models/trade_product_location.rs @@ -0,0 +1,45 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::trade_product_location::{ + ITradeProductLocationRelation, ITradeProductLocationResolve, +}; +use radroots_types::types::{IError, IResultPass}; +use serde_json::Value; + +const TABLE_NAME: &str = "trade_product_location"; + +pub fn set<E: SqlExecutor>( + exec: &E, + opts: &ITradeProductLocationRelation, +) -> Result<ITradeProductLocationResolve, IError<SqlError>> { + let mut query_vals: Vec<Value> = Vec::with_capacity(2); + let (trade_product_column, trade_product_value) = opts.trade_product.to_filter_param(); + query_vals.push(trade_product_value); + let (gcs_location_column, gcs_location_value) = opts.gcs_location.to_filter_param(); + query_vals.push(gcs_location_value); + let query = format!( + "INSERT INTO {} (tb_tp, tb_gl) VALUES ((SELECT id FROM trade_product WHERE {} = ?), (SELECT id FROM gcs_location WHERE {} = ?));", + TABLE_NAME, trade_product_column, gcs_location_column + ); + let params_json = utils::to_params_json(query_vals)?; + let _ = exec.exec(&query, ¶ms_json)?; + Ok(IResultPass { pass: true }) +} + +pub fn unset<E: SqlExecutor>( + exec: &E, + opts: &ITradeProductLocationRelation, +) -> Result<ITradeProductLocationResolve, IError<SqlError>> { + let mut query_vals: Vec<Value> = Vec::with_capacity(2); + let (trade_product_column, trade_product_value) = opts.trade_product.to_filter_param(); + query_vals.push(trade_product_value); + let (gcs_location_column, gcs_location_value) = opts.gcs_location.to_filter_param(); + query_vals.push(gcs_location_value); + let query = format!( + "DELETE FROM {} WHERE tb_tp = (SELECT id FROM trade_product WHERE {} = ?) AND tb_gl = (SELECT id FROM gcs_location WHERE {} = ?);", + TABLE_NAME, trade_product_column, gcs_location_column + ); + let params_json = utils::to_params_json(query_vals)?; + let _ = exec.exec(&query, ¶ms_json)?; + Ok(IResultPass { pass: true }) +} diff --git a/crates/replica-db/src/models/trade_product_media.rs b/crates/replica-db/src/models/trade_product_media.rs @@ -0,0 +1,45 @@ +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_replica_db_schema::trade_product_media::{ + ITradeProductMediaRelation, ITradeProductMediaResolve, +}; +use radroots_types::types::{IError, IResultPass}; +use serde_json::Value; + +const TABLE_NAME: &str = "trade_product_media"; + +pub fn set<E: SqlExecutor>( + exec: &E, + opts: &ITradeProductMediaRelation, +) -> Result<ITradeProductMediaResolve, IError<SqlError>> { + let mut query_vals: Vec<Value> = Vec::with_capacity(2); + let (trade_product_column, trade_product_value) = opts.trade_product.to_filter_param(); + query_vals.push(trade_product_value); + let (media_image_column, media_image_value) = opts.media_image.to_filter_param(); + query_vals.push(media_image_value); + let query = format!( + "INSERT INTO {} (tb_tp, tb_mu) VALUES ((SELECT id FROM trade_product WHERE {} = ?), (SELECT id FROM media_image WHERE {} = ?));", + TABLE_NAME, trade_product_column, media_image_column + ); + let params_json = utils::to_params_json(query_vals)?; + let _ = exec.exec(&query, ¶ms_json)?; + Ok(IResultPass { pass: true }) +} + +pub fn unset<E: SqlExecutor>( + exec: &E, + opts: &ITradeProductMediaRelation, +) -> Result<ITradeProductMediaResolve, IError<SqlError>> { + let mut query_vals: Vec<Value> = Vec::with_capacity(2); + let (trade_product_column, trade_product_value) = opts.trade_product.to_filter_param(); + query_vals.push(trade_product_value); + let (media_image_column, media_image_value) = opts.media_image.to_filter_param(); + query_vals.push(media_image_value); + let query = format!( + "DELETE FROM {} WHERE tb_tp = (SELECT id FROM trade_product WHERE {} = ?) AND tb_mu = (SELECT id FROM media_image WHERE {} = ?);", + TABLE_NAME, trade_product_column, media_image_column + ); + let params_json = utils::to_params_json(query_vals)?; + let _ = exec.exec(&query, ¶ms_json)?; + Ok(IResultPass { pass: true }) +} diff --git a/crates/replica-db/tests/full_mode.rs b/crates/replica-db/tests/full_mode.rs @@ -0,0 +1,1232 @@ +use radroots_sql_core::{SqlError, SqliteExecutor}; +use radroots_replica_db::{TangleSql, export_manifest}; +use radroots_replica_db_schema::farm::{ + IFarmCreate, IFarmDelete, IFarmFindMany, IFarmFindOne, IFarmUpdate, +}; +use radroots_replica_db_schema::farm_gcs_location::{ + IFarmGcsLocationCreate, IFarmGcsLocationDelete, IFarmGcsLocationFindMany, + IFarmGcsLocationFindOne, IFarmGcsLocationUpdate, +}; +use radroots_replica_db_schema::farm_member::{ + IFarmMemberCreate, IFarmMemberDelete, IFarmMemberFindMany, IFarmMemberFindOne, + IFarmMemberUpdate, +}; +use radroots_replica_db_schema::farm_member_claim::{ + IFarmMemberClaimCreate, IFarmMemberClaimDelete, IFarmMemberClaimFindMany, + IFarmMemberClaimFindOne, IFarmMemberClaimUpdate, +}; +use radroots_replica_db_schema::farm_tag::{ + IFarmTagCreate, IFarmTagDelete, IFarmTagFindMany, IFarmTagFindOne, IFarmTagUpdate, +}; +use radroots_replica_db_schema::gcs_location::{ + GcsLocationFarmArgs, GcsLocationFindManyRel, GcsLocationPlotArgs, GcsLocationTradeProductArgs, + IGcsLocationCreate, IGcsLocationDelete, IGcsLocationFindMany, IGcsLocationFindOne, + IGcsLocationUpdate, +}; +use radroots_replica_db_schema::log_error::{ + ILogErrorCreate, ILogErrorDelete, ILogErrorFindMany, ILogErrorFindOne, ILogErrorUpdate, +}; +use radroots_replica_db_schema::media_image::{ + IMediaImageCreate, IMediaImageDelete, IMediaImageFindMany, IMediaImageFindOne, + IMediaImageUpdate, MediaImageFindManyRel, MediaImageTradeProductArgs, +}; +use radroots_replica_db_schema::nostr_event_state::{ + INostrEventStateCreate, INostrEventStateDelete, INostrEventStateFindMany, + INostrEventStateFindOne, INostrEventStateUpdate, +}; +use radroots_replica_db_schema::nostr_profile::{ + INostrProfileCreate, INostrProfileDelete, INostrProfileFindMany, INostrProfileFindOne, + INostrProfileUpdate, NostrProfileFindManyRel, NostrProfileRelayArgs, +}; +use radroots_replica_db_schema::nostr_profile_relay::INostrProfileRelayRelation; +use radroots_replica_db_schema::nostr_relay::{ + INostrRelayCreate, INostrRelayDelete, INostrRelayFindMany, INostrRelayFindOne, + INostrRelayUpdate, NostrRelayFindManyRel, NostrRelayProfileArgs, +}; +use radroots_replica_db_schema::plot::{ + IPlotCreate, IPlotDelete, IPlotFindMany, IPlotFindOne, IPlotUpdate, +}; +use radroots_replica_db_schema::plot_gcs_location::{ + IPlotGcsLocationCreate, IPlotGcsLocationDelete, IPlotGcsLocationFindMany, + IPlotGcsLocationFindOne, IPlotGcsLocationUpdate, +}; +use radroots_replica_db_schema::plot_tag::{ + IPlotTagCreate, IPlotTagDelete, IPlotTagFindMany, IPlotTagFindOne, IPlotTagUpdate, +}; +use radroots_replica_db_schema::trade_product::{ + ITradeProductCreate, ITradeProductDelete, ITradeProductFindMany, ITradeProductFindOne, + ITradeProductUpdate, +}; +use radroots_replica_db_schema::trade_product_location::ITradeProductLocationRelation; +use radroots_replica_db_schema::trade_product_media::ITradeProductMediaRelation; +use radroots_types::types::IError; +use serde::de::DeserializeOwned; +use serde_json::json; + +fn parse_json<T: DeserializeOwned>(value: serde_json::Value) -> T { + serde_json::from_value(value).expect("valid test payload") +} + +fn hex64(ch: char) -> String { + std::iter::repeat_n(ch, 64).collect() +} + +fn assert_invalid_argument<T>(result: Result<T, IError<SqlError>>) { + let err = match result { + Ok(_) => panic!("invalid argument expected"), + Err(err) => err, + }; + assert!(matches!(err.err, SqlError::InvalidArgument(_))); +} + +fn assert_not_found<T>(result: Result<T, IError<SqlError>>) { + let err = match result { + Ok(_) => panic!("not found expected"), + Err(err) => err, + }; + assert!(matches!(err.err, SqlError::NotFound(_))); +} + +fn open_db() -> TangleSql<SqliteExecutor> { + let exec = SqliteExecutor::open_memory().expect("open sqlite memory"); + let db = TangleSql::new(exec); + db.migrate_up().expect("migrate up"); + db +} + +#[test] +fn full_mode_crud_and_relation_paths() { + let db = open_db(); + + db.migrate_down().expect("migrate down"); + db.migrate_up().expect("migrate up again"); + + let farm: IFarmCreate = parse_json(json!({ + "d_tag": "farm-a", + "pubkey": hex64('a'), + "name": "farm a" + })); + let farm_created = db.farm_create(&farm).expect("farm create").result; + + let gcs_location: IGcsLocationCreate = parse_json(json!({ + "d_tag": "gcs-a", + "lat": 59.33, + "lng": 18.06, + "geohash": "u6sce4f", + "point": "POINT(18.06 59.33)", + "polygon": "POLYGON((18.06 59.33,18.07 59.33,18.07 59.34,18.06 59.34,18.06 59.33))" + })); + let gcs_created = db + .gcs_location_create(&gcs_location) + .expect("gcs create") + .result; + + let plot: IPlotCreate = parse_json(json!({ + "d_tag": "plot-a", + "farm_id": farm_created.id, + "name": "plot a" + })); + let plot_created = db.plot_create(&plot).expect("plot create").result; + + let farm_gcs: IFarmGcsLocationCreate = parse_json(json!({ + "farm_id": farm_created.id, + "gcs_location_id": gcs_created.id, + "role": "primary" + })); + let farm_gcs_created = db + .farm_gcs_location_create(&farm_gcs) + .expect("farm gcs create") + .result; + + let plot_gcs: IPlotGcsLocationCreate = parse_json(json!({ + "plot_id": plot_created.id, + "gcs_location_id": gcs_created.id, + "role": "primary" + })); + let plot_gcs_created = db + .plot_gcs_location_create(&plot_gcs) + .expect("plot gcs create") + .result; + + let farm_tag: IFarmTagCreate = parse_json(json!({ + "farm_id": farm_created.id, + "tag": "organic" + })); + let farm_tag_created = db + .farm_tag_create(&farm_tag) + .expect("farm tag create") + .result; + + let plot_tag: IPlotTagCreate = parse_json(json!({ + "plot_id": plot_created.id, + "tag": "north" + })); + let plot_tag_created = db + .plot_tag_create(&plot_tag) + .expect("plot tag create") + .result; + + let farm_member: IFarmMemberCreate = parse_json(json!({ + "farm_id": farm_created.id, + "member_pubkey": hex64('b'), + "role": "owner" + })); + let farm_member_created = db + .farm_member_create(&farm_member) + .expect("farm member create") + .result; + + let farm_member_claim: IFarmMemberClaimCreate = parse_json(json!({ + "member_pubkey": hex64('b'), + "farm_pubkey": hex64('a') + })); + let farm_member_claim_created = db + .farm_member_claim_create(&farm_member_claim) + .expect("farm member claim create") + .result; + + let log_error: ILogErrorCreate = parse_json(json!({ + "error": "panic", + "message": "boom", + "app_system": "studio", + "app_version": "1.0.0", + "nostr_pubkey": hex64('c') + })); + let log_error_created = db + .log_error_create(&log_error) + .expect("log error create") + .result; + + let media_image: IMediaImageCreate = parse_json(json!({ + "file_path": "/img/a.jpg", + "mime_type": "image/jpeg", + "res_base": "https://cdn.example.com", + "res_path": "img/a.jpg" + })); + let media_image_created = db + .media_image_create(&media_image) + .expect("media image create") + .result; + + let nostr_profile: INostrProfileCreate = parse_json(json!({ + "public_key": hex64('d'), + "profile_type": "farm", + "name": "profile a" + })); + let nostr_profile_created = db + .nostr_profile_create(&nostr_profile) + .expect("nostr profile create") + .result; + + let nostr_relay: INostrRelayCreate = parse_json(json!({ + "url": "wss://relay.example.com" + })); + let nostr_relay_created = db + .nostr_relay_create(&nostr_relay) + .expect("nostr relay create") + .result; + + let nostr_event_state: INostrEventStateCreate = parse_json(json!({ + "key": "state-a", + "kind": 30023, + "pubkey": hex64('d'), + "d_tag": "listing-a", + "last_event_id": hex64('e'), + "last_created_at": 1, + "content_hash": "hash-a" + })); + let nostr_event_state_created = db + .nostr_event_state_create(&nostr_event_state) + .expect("nostr event state create") + .result; + + let trade_product: ITradeProductCreate = parse_json(json!({ + "key": "product-a", + "category": "coffee", + "title": "coffee a", + "summary": "summary", + "process": "washed", + "lot": "lot-a", + "profile": "floral", + "year": 2024, + "qty_amt": 100, + "qty_unit": "kg", + "price_amt": 7.5, + "price_currency": "USD", + "price_qty_amt": 1, + "price_qty_unit": "kg" + })); + let trade_product_created = db + .trade_product_create(&trade_product) + .expect("trade product create") + .result; + + let gcs_extra: IGcsLocationCreate = parse_json(json!({ + "d_tag": "gcs-b", + "lat": 59.34, + "lng": 18.07, + "geohash": "u6sce4g", + "point": "POINT(18.07 59.34)", + "polygon": "POLYGON((18.07 59.34,18.08 59.34,18.08 59.35,18.07 59.35,18.07 59.34))" + })); + let _gcs_extra_created = db + .gcs_location_create(&gcs_extra) + .expect("gcs extra create") + .result; + + let media_image_extra: IMediaImageCreate = parse_json(json!({ + "file_path": "/img/b.jpg", + "mime_type": "image/jpeg", + "res_base": "https://cdn.example.com", + "res_path": "img/b.jpg" + })); + let _media_image_extra_created = db + .media_image_create(&media_image_extra) + .expect("media image extra create") + .result; + + let nostr_profile_extra: INostrProfileCreate = parse_json(json!({ + "public_key": hex64('f'), + "profile_type": "farm", + "name": "profile c" + })); + let nostr_profile_extra_created = db + .nostr_profile_create(&nostr_profile_extra) + .expect("nostr profile extra create") + .result; + + let nostr_relay_extra: INostrRelayCreate = parse_json(json!({ + "url": "wss://relay2.example.com" + })); + let nostr_relay_extra_created = db + .nostr_relay_create(&nostr_relay_extra) + .expect("nostr relay extra create") + .result; + + let profile_relay_rel: INostrProfileRelayRelation = parse_json(json!({ + "nostr_profile": { "id": nostr_profile_created.id }, + "nostr_relay": { "id": nostr_relay_created.id } + })); + db.nostr_profile_relay_set(&profile_relay_rel) + .expect("profile relay set"); + + let profile_relay_rel_extra: INostrProfileRelayRelation = parse_json(json!({ + "nostr_profile": { "id": nostr_profile_extra_created.id }, + "nostr_relay": { "id": nostr_relay_extra_created.id } + })); + db.nostr_profile_relay_set(&profile_relay_rel_extra) + .expect("profile relay extra set"); + + let product_location_rel: ITradeProductLocationRelation = parse_json(json!({ + "trade_product": { "id": trade_product_created.id }, + "gcs_location": { "id": gcs_created.id } + })); + db.trade_product_location_set(&product_location_rel) + .expect("product location set"); + + let product_media_rel: ITradeProductMediaRelation = parse_json(json!({ + "trade_product": { "id": trade_product_created.id }, + "media_image": { "id": media_image_created.id } + })); + db.trade_product_media_set(&product_media_rel) + .expect("product media set"); + + let _: IFarmFindMany = parse_json(json!({ "filter": { "id": farm_created.id } })); + let farm_find_many: IFarmFindMany = parse_json(json!({ "filter": { "id": farm_created.id } })); + assert_eq!( + db.farm_find_many(&farm_find_many) + .expect("farm find many") + .results + .len(), + 1 + ); + + let farm_find_one: IFarmFindOne = parse_json(json!({ "on": { "id": farm_created.id } })); + assert!( + db.farm_find_one(&farm_find_one) + .expect("farm find one") + .result + .is_some() + ); + + let farm_update_on_alt: IFarmUpdate = + parse_json(json!({ "on": { "d_tag": "farm-a" }, "fields": { "name": "farm a+" } })); + assert_eq!( + db.farm_update(&farm_update_on_alt) + .expect("farm update alt") + .result + .name, + "farm a+" + ); + let farm_update_on_id: IFarmUpdate = + parse_json(json!({ "on": { "id": farm_created.id }, "fields": { "name": "farm a++" } })); + assert_eq!( + db.farm_update(&farm_update_on_id) + .expect("farm update id") + .result + .name, + "farm a++" + ); + let farm_update_empty: IFarmUpdate = + parse_json(json!({ "on": { "id": farm_created.id }, "fields": {} })); + assert_invalid_argument(db.farm_update(&farm_update_empty)); + + let plot_find_many: IPlotFindMany = parse_json(json!({ "filter": { "id": plot_created.id } })); + assert_eq!( + db.plot_find_many(&plot_find_many) + .expect("plot find many") + .results + .len(), + 1 + ); + let plot_find_one: IPlotFindOne = parse_json(json!({ "on": { "id": plot_created.id } })); + assert!( + db.plot_find_one(&plot_find_one) + .expect("plot find one") + .result + .is_some() + ); + let plot_update_alt: IPlotUpdate = + parse_json(json!({ "on": { "d_tag": "plot-a" }, "fields": { "name": "plot a+" } })); + assert_eq!( + db.plot_update(&plot_update_alt) + .expect("plot update alt") + .result + .name, + "plot a+" + ); + let plot_update_id: IPlotUpdate = + parse_json(json!({ "on": { "id": plot_created.id }, "fields": { "name": "plot a++" } })); + assert_eq!( + db.plot_update(&plot_update_id) + .expect("plot update id") + .result + .name, + "plot a++" + ); + let plot_update_empty: IPlotUpdate = + parse_json(json!({ "on": { "id": plot_created.id }, "fields": {} })); + assert_invalid_argument(db.plot_update(&plot_update_empty)); + + for opts in [ + IGcsLocationFindMany::Rel { + rel: GcsLocationFindManyRel::OnTradeProduct(GcsLocationTradeProductArgs { + id: trade_product_created.id.clone(), + }), + }, + IGcsLocationFindMany::Rel { + rel: GcsLocationFindManyRel::OffTradeProduct(GcsLocationTradeProductArgs { + id: trade_product_created.id.clone(), + }), + }, + IGcsLocationFindMany::Rel { + rel: GcsLocationFindManyRel::OnFarm(GcsLocationFarmArgs { + id: farm_created.id.clone(), + }), + }, + IGcsLocationFindMany::Rel { + rel: GcsLocationFindManyRel::OffFarm(GcsLocationFarmArgs { + id: farm_created.id.clone(), + }), + }, + IGcsLocationFindMany::Rel { + rel: GcsLocationFindManyRel::OnPlot(GcsLocationPlotArgs { + id: plot_created.id.clone(), + }), + }, + IGcsLocationFindMany::Rel { + rel: GcsLocationFindManyRel::OffPlot(GcsLocationPlotArgs { + id: plot_created.id.clone(), + }), + }, + ] { + let _ = db.gcs_location_find_many(&opts).expect("gcs rel find many"); + } + let gcs_find_many_filter: IGcsLocationFindMany = + parse_json(json!({ "filter": { "id": gcs_created.id } })); + assert_eq!( + db.gcs_location_find_many(&gcs_find_many_filter) + .expect("gcs find many filter") + .results + .len(), + 1 + ); + let gcs_find_one_on: IGcsLocationFindOne = + parse_json(json!({ "on": { "id": gcs_created.id } })); + assert!( + db.gcs_location_find_one(&gcs_find_one_on) + .expect("gcs find one on") + .result + .is_some() + ); + let gcs_find_one_rel: IGcsLocationFindOne = + parse_json(json!({ "rel": { "on_farm": { "id": farm_created.id } } })); + assert!( + db.gcs_location_find_one(&gcs_find_one_rel) + .expect("gcs find one rel") + .result + .is_some() + ); + let gcs_update_alt: IGcsLocationUpdate = + parse_json(json!({ "on": { "d_tag": "gcs-a" }, "fields": { "label": "gcs a+" } })); + assert_eq!( + db.gcs_location_update(&gcs_update_alt) + .expect("gcs update alt") + .result + .label + .as_deref(), + Some("gcs a+") + ); + let gcs_update_id: IGcsLocationUpdate = + parse_json(json!({ "on": { "id": gcs_created.id }, "fields": { "label": "gcs a++" } })); + assert_eq!( + db.gcs_location_update(&gcs_update_id) + .expect("gcs update id") + .result + .label + .as_deref(), + Some("gcs a++") + ); + let gcs_update_empty: IGcsLocationUpdate = + parse_json(json!({ "on": { "id": gcs_created.id }, "fields": {} })); + assert_invalid_argument(db.gcs_location_update(&gcs_update_empty)); + + let farm_gcs_find_many: IFarmGcsLocationFindMany = + parse_json(json!({ "filter": { "id": farm_gcs_created.id } })); + assert_eq!( + db.farm_gcs_location_find_many(&farm_gcs_find_many) + .expect("farm gcs find many") + .results + .len(), + 1 + ); + let farm_gcs_find_one: IFarmGcsLocationFindOne = + parse_json(json!({ "on": { "id": farm_gcs_created.id } })); + assert!( + db.farm_gcs_location_find_one(&farm_gcs_find_one) + .expect("farm gcs find one") + .result + .is_some() + ); + let farm_gcs_update_alt: IFarmGcsLocationUpdate = parse_json(json!({ + "on": { "farm_id": farm_created.id }, + "fields": { "role": "secondary" } + })); + assert_eq!( + db.farm_gcs_location_update(&farm_gcs_update_alt) + .expect("farm gcs update") + .result + .role, + "secondary" + ); + let farm_gcs_update_id: IFarmGcsLocationUpdate = parse_json( + json!({ "on": { "id": farm_gcs_created.id }, "fields": { "role": "tertiary" } }), + ); + assert_eq!( + db.farm_gcs_location_update(&farm_gcs_update_id) + .expect("farm gcs update id") + .result + .role, + "tertiary" + ); + let farm_gcs_update_empty: IFarmGcsLocationUpdate = + parse_json(json!({ "on": { "id": farm_gcs_created.id }, "fields": {} })); + assert_invalid_argument(db.farm_gcs_location_update(&farm_gcs_update_empty)); + + let plot_gcs_find_many: IPlotGcsLocationFindMany = + parse_json(json!({ "filter": { "id": plot_gcs_created.id } })); + assert_eq!( + db.plot_gcs_location_find_many(&plot_gcs_find_many) + .expect("plot gcs find many") + .results + .len(), + 1 + ); + let plot_gcs_find_one: IPlotGcsLocationFindOne = + parse_json(json!({ "on": { "id": plot_gcs_created.id } })); + assert!( + db.plot_gcs_location_find_one(&plot_gcs_find_one) + .expect("plot gcs find one") + .result + .is_some() + ); + let plot_gcs_update_alt: IPlotGcsLocationUpdate = parse_json(json!({ + "on": { "plot_id": plot_created.id }, + "fields": { "role": "secondary" } + })); + assert_eq!( + db.plot_gcs_location_update(&plot_gcs_update_alt) + .expect("plot gcs update") + .result + .role, + "secondary" + ); + let plot_gcs_update_id: IPlotGcsLocationUpdate = parse_json( + json!({ "on": { "id": plot_gcs_created.id }, "fields": { "role": "tertiary" } }), + ); + assert_eq!( + db.plot_gcs_location_update(&plot_gcs_update_id) + .expect("plot gcs update id") + .result + .role, + "tertiary" + ); + let plot_gcs_update_empty: IPlotGcsLocationUpdate = + parse_json(json!({ "on": { "id": plot_gcs_created.id }, "fields": {} })); + assert_invalid_argument(db.plot_gcs_location_update(&plot_gcs_update_empty)); + + let farm_tag_find_many: IFarmTagFindMany = + parse_json(json!({ "filter": { "id": farm_tag_created.id } })); + assert_eq!( + db.farm_tag_find_many(&farm_tag_find_many) + .expect("farm tag find many") + .results + .len(), + 1 + ); + let farm_tag_find_one: IFarmTagFindOne = + parse_json(json!({ "on": { "id": farm_tag_created.id } })); + assert!( + db.farm_tag_find_one(&farm_tag_find_one) + .expect("farm tag find one") + .result + .is_some() + ); + let farm_tag_update_alt: IFarmTagUpdate = parse_json( + json!({ "on": { "farm_id": farm_created.id }, "fields": { "tag": "biodynamic" } }), + ); + assert_eq!( + db.farm_tag_update(&farm_tag_update_alt) + .expect("farm tag update") + .result + .tag, + "biodynamic" + ); + let farm_tag_update_id: IFarmTagUpdate = parse_json( + json!({ "on": { "id": farm_tag_created.id }, "fields": { "tag": "regenerative" } }), + ); + assert_eq!( + db.farm_tag_update(&farm_tag_update_id) + .expect("farm tag update id") + .result + .tag, + "regenerative" + ); + let farm_tag_update_empty: IFarmTagUpdate = + parse_json(json!({ "on": { "id": farm_tag_created.id }, "fields": {} })); + assert_invalid_argument(db.farm_tag_update(&farm_tag_update_empty)); + + let plot_tag_find_many: IPlotTagFindMany = + parse_json(json!({ "filter": { "id": plot_tag_created.id } })); + assert_eq!( + db.plot_tag_find_many(&plot_tag_find_many) + .expect("plot tag find many") + .results + .len(), + 1 + ); + let plot_tag_find_one: IPlotTagFindOne = + parse_json(json!({ "on": { "id": plot_tag_created.id } })); + assert!( + db.plot_tag_find_one(&plot_tag_find_one) + .expect("plot tag find one") + .result + .is_some() + ); + let plot_tag_update_alt: IPlotTagUpdate = + parse_json(json!({ "on": { "plot_id": plot_created.id }, "fields": { "tag": "south" } })); + assert_eq!( + db.plot_tag_update(&plot_tag_update_alt) + .expect("plot tag update") + .result + .tag, + "south" + ); + let plot_tag_update_id: IPlotTagUpdate = + parse_json(json!({ "on": { "id": plot_tag_created.id }, "fields": { "tag": "east" } })); + assert_eq!( + db.plot_tag_update(&plot_tag_update_id) + .expect("plot tag update id") + .result + .tag, + "east" + ); + let plot_tag_update_empty: IPlotTagUpdate = + parse_json(json!({ "on": { "id": plot_tag_created.id }, "fields": {} })); + assert_invalid_argument(db.plot_tag_update(&plot_tag_update_empty)); + + let farm_member_find_many: IFarmMemberFindMany = + parse_json(json!({ "filter": { "id": farm_member_created.id } })); + assert_eq!( + db.farm_member_find_many(&farm_member_find_many) + .expect("farm member find many") + .results + .len(), + 1 + ); + let farm_member_find_one: IFarmMemberFindOne = + parse_json(json!({ "on": { "id": farm_member_created.id } })); + assert!( + db.farm_member_find_one(&farm_member_find_one) + .expect("farm member find one") + .result + .is_some() + ); + let farm_member_update_alt: IFarmMemberUpdate = parse_json(json!({ + "on": { "member_pubkey": hex64('b') }, + "fields": { "role": "editor" } + })); + assert_eq!( + db.farm_member_update(&farm_member_update_alt) + .expect("farm member update") + .result + .role, + "editor" + ); + let farm_member_update_id: IFarmMemberUpdate = parse_json( + json!({ "on": { "id": farm_member_created.id }, "fields": { "role": "admin" } }), + ); + assert_eq!( + db.farm_member_update(&farm_member_update_id) + .expect("farm member update id") + .result + .role, + "admin" + ); + let farm_member_update_empty: IFarmMemberUpdate = + parse_json(json!({ "on": { "id": farm_member_created.id }, "fields": {} })); + assert_invalid_argument(db.farm_member_update(&farm_member_update_empty)); + + let farm_member_claim_find_many: IFarmMemberClaimFindMany = + parse_json(json!({ "filter": { "id": farm_member_claim_created.id } })); + assert_eq!( + db.farm_member_claim_find_many(&farm_member_claim_find_many) + .expect("farm member claim find many") + .results + .len(), + 1 + ); + let farm_member_claim_find_one: IFarmMemberClaimFindOne = + parse_json(json!({ "on": { "id": farm_member_claim_created.id } })); + assert!( + db.farm_member_claim_find_one(&farm_member_claim_find_one) + .expect("farm member claim find one") + .result + .is_some() + ); + let farm_member_claim_update_alt: IFarmMemberClaimUpdate = parse_json(json!({ + "on": { "member_pubkey": hex64('b') }, + "fields": { "farm_pubkey": hex64('f') } + })); + assert_eq!( + db.farm_member_claim_update(&farm_member_claim_update_alt) + .expect("farm member claim update") + .result + .farm_pubkey, + hex64('f') + ); + let farm_member_claim_update_id: IFarmMemberClaimUpdate = parse_json(json!({ + "on": { "id": farm_member_claim_created.id }, + "fields": { "farm_pubkey": hex64('g') } + })); + assert_eq!( + db.farm_member_claim_update(&farm_member_claim_update_id) + .expect("farm member claim update id") + .result + .farm_pubkey, + hex64('g') + ); + let farm_member_claim_update_empty: IFarmMemberClaimUpdate = + parse_json(json!({ "on": { "id": farm_member_claim_created.id }, "fields": {} })); + assert_invalid_argument(db.farm_member_claim_update(&farm_member_claim_update_empty)); + + let log_error_find_many: ILogErrorFindMany = + parse_json(json!({ "filter": { "id": log_error_created.id } })); + assert_eq!( + db.log_error_find_many(&log_error_find_many) + .expect("log error find many") + .results + .len(), + 1 + ); + let log_error_find_one: ILogErrorFindOne = + parse_json(json!({ "on": { "id": log_error_created.id } })); + assert!( + db.log_error_find_one(&log_error_find_one) + .expect("log error find one") + .result + .is_some() + ); + let log_error_update_alt: ILogErrorUpdate = parse_json(json!({ + "on": { "nostr_pubkey": hex64('c') }, + "fields": { "message": "boom+" } + })); + assert_eq!( + db.log_error_update(&log_error_update_alt) + .expect("log error update") + .result + .message, + "boom+" + ); + let log_error_update_id: ILogErrorUpdate = parse_json( + json!({ "on": { "id": log_error_created.id }, "fields": { "message": "boom++" } }), + ); + assert_eq!( + db.log_error_update(&log_error_update_id) + .expect("log error update id") + .result + .message, + "boom++" + ); + let log_error_update_empty: ILogErrorUpdate = + parse_json(json!({ "on": { "id": log_error_created.id }, "fields": {} })); + assert_invalid_argument(db.log_error_update(&log_error_update_empty)); + + for opts in [ + IMediaImageFindMany::Rel { + rel: MediaImageFindManyRel::OnTradeProduct(MediaImageTradeProductArgs { + id: trade_product_created.id.clone(), + }), + }, + IMediaImageFindMany::Rel { + rel: MediaImageFindManyRel::OffTradeProduct(MediaImageTradeProductArgs { + id: trade_product_created.id.clone(), + }), + }, + ] { + let _ = db + .media_image_find_many(&opts) + .expect("media image rel find many"); + } + let media_image_find_many_filter: IMediaImageFindMany = + parse_json(json!({ "filter": { "id": media_image_created.id } })); + assert_eq!( + db.media_image_find_many(&media_image_find_many_filter) + .expect("media image find many filter") + .results + .len(), + 1 + ); + let media_image_find_one_on: IMediaImageFindOne = + parse_json(json!({ "on": { "id": media_image_created.id } })); + assert!( + db.media_image_find_one(&media_image_find_one_on) + .expect("media image find one") + .result + .is_some() + ); + let media_image_find_one_rel: IMediaImageFindOne = + parse_json(json!({ "rel": { "on_trade_product": { "id": trade_product_created.id } } })); + assert!( + db.media_image_find_one(&media_image_find_one_rel) + .expect("media image find one rel") + .result + .is_some() + ); + let media_image_update_alt: IMediaImageUpdate = + parse_json(json!({ "on": { "file_path": "/img/a.jpg" }, "fields": { "label": "hero" } })); + assert_eq!( + db.media_image_update(&media_image_update_alt) + .expect("media image update") + .result + .label + .as_deref(), + Some("hero") + ); + let media_image_update_id: IMediaImageUpdate = parse_json( + json!({ "on": { "id": media_image_created.id }, "fields": { "label": "hero+" } }), + ); + assert_eq!( + db.media_image_update(&media_image_update_id) + .expect("media image update id") + .result + .label + .as_deref(), + Some("hero+") + ); + let media_image_update_empty: IMediaImageUpdate = + parse_json(json!({ "on": { "id": media_image_created.id }, "fields": {} })); + assert_invalid_argument(db.media_image_update(&media_image_update_empty)); + + for opts in [ + INostrProfileFindMany::Rel { + rel: NostrProfileFindManyRel::OnRelay(NostrProfileRelayArgs { + id: nostr_relay_created.id.clone(), + }), + }, + INostrProfileFindMany::Rel { + rel: NostrProfileFindManyRel::OffRelay(NostrProfileRelayArgs { + id: nostr_relay_created.id.clone(), + }), + }, + ] { + let _ = db + .nostr_profile_find_many(&opts) + .expect("nostr profile rel find many"); + } + let nostr_profile_find_many_filter: INostrProfileFindMany = + parse_json(json!({ "filter": { "id": nostr_profile_created.id } })); + assert_eq!( + db.nostr_profile_find_many(&nostr_profile_find_many_filter) + .expect("nostr profile find many filter") + .results + .len(), + 1 + ); + let nostr_profile_find_one_on: INostrProfileFindOne = + parse_json(json!({ "on": { "id": nostr_profile_created.id } })); + assert!( + db.nostr_profile_find_one(&nostr_profile_find_one_on) + .expect("nostr profile find one") + .result + .is_some() + ); + let nostr_profile_find_one_rel: INostrProfileFindOne = + parse_json(json!({ "rel": { "on_relay": { "id": nostr_relay_created.id } } })); + assert!( + db.nostr_profile_find_one(&nostr_profile_find_one_rel) + .expect("nostr profile find one rel") + .result + .is_some() + ); + let nostr_profile_update_alt: INostrProfileUpdate = parse_json( + json!({ "on": { "public_key": hex64('d') }, "fields": { "name": "profile b" } }), + ); + assert_eq!( + db.nostr_profile_update(&nostr_profile_update_alt) + .expect("nostr profile update") + .result + .name, + "profile b" + ); + let nostr_profile_update_id: INostrProfileUpdate = parse_json( + json!({ "on": { "id": nostr_profile_created.id }, "fields": { "name": "profile b+" } }), + ); + assert_eq!( + db.nostr_profile_update(&nostr_profile_update_id) + .expect("nostr profile update id") + .result + .name, + "profile b+" + ); + let nostr_profile_update_empty: INostrProfileUpdate = + parse_json(json!({ "on": { "id": nostr_profile_created.id }, "fields": {} })); + assert_invalid_argument(db.nostr_profile_update(&nostr_profile_update_empty)); + + let nostr_event_state_find_many: INostrEventStateFindMany = + parse_json(json!({ "filter": { "id": nostr_event_state_created.id } })); + assert_eq!( + db.nostr_event_state_find_many(&nostr_event_state_find_many) + .expect("nostr event state find many") + .results + .len(), + 1 + ); + let nostr_event_state_find_one: INostrEventStateFindOne = + parse_json(json!({ "on": { "id": nostr_event_state_created.id } })); + assert!( + db.nostr_event_state_find_one(&nostr_event_state_find_one) + .expect("nostr event state find one") + .result + .is_some() + ); + let nostr_event_state_update_alt: INostrEventStateUpdate = + parse_json(json!({ "on": { "key": "state-a" }, "fields": { "content_hash": "hash-b" } })); + assert_eq!( + db.nostr_event_state_update(&nostr_event_state_update_alt) + .expect("nostr event state update") + .result + .content_hash, + "hash-b" + ); + let nostr_event_state_update_id: INostrEventStateUpdate = parse_json( + json!({ "on": { "id": nostr_event_state_created.id }, "fields": { "content_hash": "hash-c" } }), + ); + assert_eq!( + db.nostr_event_state_update(&nostr_event_state_update_id) + .expect("nostr event state update id") + .result + .content_hash, + "hash-c" + ); + let nostr_event_state_update_empty: INostrEventStateUpdate = + parse_json(json!({ "on": { "id": nostr_event_state_created.id }, "fields": {} })); + assert_invalid_argument(db.nostr_event_state_update(&nostr_event_state_update_empty)); + + for opts in [ + INostrRelayFindMany::Rel { + rel: NostrRelayFindManyRel::OnProfile(NostrRelayProfileArgs { + public_key: hex64('d'), + }), + }, + INostrRelayFindMany::Rel { + rel: NostrRelayFindManyRel::OffProfile(NostrRelayProfileArgs { + public_key: hex64('d'), + }), + }, + ] { + let _ = db + .nostr_relay_find_many(&opts) + .expect("nostr relay rel find many"); + } + let nostr_relay_find_many_filter: INostrRelayFindMany = + parse_json(json!({ "filter": { "id": nostr_relay_created.id } })); + assert_eq!( + db.nostr_relay_find_many(&nostr_relay_find_many_filter) + .expect("nostr relay find many filter") + .results + .len(), + 1 + ); + let nostr_relay_find_one_on: INostrRelayFindOne = + parse_json(json!({ "on": { "id": nostr_relay_created.id } })); + assert!( + db.nostr_relay_find_one(&nostr_relay_find_one_on) + .expect("nostr relay find one") + .result + .is_some() + ); + let nostr_relay_find_one_rel: INostrRelayFindOne = + parse_json(json!({ "rel": { "on_profile": { "public_key": hex64('d') } } })); + assert!( + db.nostr_relay_find_one(&nostr_relay_find_one_rel) + .expect("nostr relay find one rel") + .result + .is_some() + ); + let nostr_relay_update_alt: INostrRelayUpdate = parse_json(json!({ + "on": { "url": "wss://relay.example.com" }, + "fields": { "name": "relay a" } + })); + assert_eq!( + db.nostr_relay_update(&nostr_relay_update_alt) + .expect("nostr relay update") + .result + .name + .as_deref(), + Some("relay a") + ); + let nostr_relay_update_id: INostrRelayUpdate = parse_json( + json!({ "on": { "id": nostr_relay_created.id }, "fields": { "name": "relay a+" } }), + ); + assert_eq!( + db.nostr_relay_update(&nostr_relay_update_id) + .expect("nostr relay update id") + .result + .name + .as_deref(), + Some("relay a+") + ); + let nostr_relay_update_empty: INostrRelayUpdate = + parse_json(json!({ "on": { "id": nostr_relay_created.id }, "fields": {} })); + assert_invalid_argument(db.nostr_relay_update(&nostr_relay_update_empty)); + + let trade_product_find_many: ITradeProductFindMany = + parse_json(json!({ "filter": { "id": trade_product_created.id } })); + assert_eq!( + db.trade_product_find_many(&trade_product_find_many) + .expect("trade product find many") + .results + .len(), + 1 + ); + let trade_product_find_one: ITradeProductFindOne = + parse_json(json!({ "on": { "id": trade_product_created.id } })); + assert!( + db.trade_product_find_one(&trade_product_find_one) + .expect("trade product find one") + .result + .is_some() + ); + let trade_product_update: ITradeProductUpdate = parse_json( + json!({ "on": { "id": trade_product_created.id }, "fields": { "title": "coffee b" } }), + ); + assert_eq!( + db.trade_product_update(&trade_product_update) + .expect("trade product update") + .result + .title, + "coffee b" + ); + let trade_product_update_empty: ITradeProductUpdate = + parse_json(json!({ "on": { "id": trade_product_created.id }, "fields": {} })); + assert_invalid_argument(db.trade_product_update(&trade_product_update_empty)); + + let backup = db.backup_database().expect("backup"); + let backup_json = db.backup_database_json().expect("backup json"); + let _manifest = export_manifest(db.executor()).expect("export manifest"); + db.restore_database(&backup).expect("restore backup"); + db.restore_database_json(&backup_json) + .expect("restore backup json"); + + let gcs_delete_rel_found: IGcsLocationDelete = + parse_json(json!({ "rel": { "off_trade_product": { "id": trade_product_created.id } } })); + db.gcs_location_delete(&gcs_delete_rel_found) + .expect("gcs rel delete found"); + + let media_image_rel_delete_found: IMediaImageDelete = + parse_json(json!({ "rel": { "off_trade_product": { "id": trade_product_created.id } } })); + db.media_image_delete(&media_image_rel_delete_found) + .expect("media image rel delete found"); + + let nostr_relay_rel_delete_found: INostrRelayDelete = + parse_json(json!({ "rel": { "on_profile": { "public_key": hex64('f') } } })); + db.nostr_relay_delete(&nostr_relay_rel_delete_found) + .expect("nostr relay rel delete found"); + + let nostr_profile_rel_delete_found: INostrProfileDelete = + parse_json(json!({ "rel": { "off_relay": { "id": nostr_relay_created.id } } })); + db.nostr_profile_delete(&nostr_profile_rel_delete_found) + .expect("nostr profile rel delete found"); + + db.trade_product_media_unset(&product_media_rel) + .expect("product media unset"); + db.trade_product_location_unset(&product_location_rel) + .expect("product location unset"); + db.nostr_profile_relay_unset(&profile_relay_rel) + .expect("profile relay unset"); + + let trade_product_delete: ITradeProductDelete = + parse_json(json!({ "on": { "id": trade_product_created.id } })); + db.trade_product_delete(&trade_product_delete) + .expect("trade product delete"); + let trade_product_delete_missing: ITradeProductDelete = + parse_json(json!({ "on": { "id": trade_product_created.id } })); + assert_not_found(db.trade_product_delete(&trade_product_delete_missing)); + + let plot_gcs_delete: IPlotGcsLocationDelete = + parse_json(json!({ "on": { "plot_id": plot_created.id } })); + db.plot_gcs_location_delete(&plot_gcs_delete) + .expect("plot gcs delete"); + let plot_gcs_delete_missing: IPlotGcsLocationDelete = + parse_json(json!({ "on": { "id": plot_gcs_created.id } })); + assert_not_found(db.plot_gcs_location_delete(&plot_gcs_delete_missing)); + + let farm_gcs_delete: IFarmGcsLocationDelete = + parse_json(json!({ "on": { "farm_id": farm_created.id } })); + db.farm_gcs_location_delete(&farm_gcs_delete) + .expect("farm gcs delete"); + let farm_gcs_delete_missing: IFarmGcsLocationDelete = + parse_json(json!({ "on": { "id": farm_gcs_created.id } })); + assert_not_found(db.farm_gcs_location_delete(&farm_gcs_delete_missing)); + + let gcs_delete_on_non_primary: IGcsLocationDelete = + parse_json(json!({ "on": { "d_tag": "gcs-a" } })); + let _ = db.gcs_location_delete(&gcs_delete_on_non_primary); + + for payload in [ + json!({ "rel": { "on_trade_product": { "id": trade_product_created.id } } }), + json!({ "rel": { "off_trade_product": { "id": trade_product_created.id } } }), + json!({ "rel": { "on_farm": { "id": farm_created.id } } }), + json!({ "rel": { "off_farm": { "id": farm_created.id } } }), + json!({ "rel": { "on_plot": { "id": plot_created.id } } }), + json!({ "rel": { "off_plot": { "id": plot_created.id } } }), + ] { + let opts: IGcsLocationDelete = parse_json(payload); + let _ = db.gcs_location_delete(&opts); + } + let gcs_delete_missing: IGcsLocationDelete = + parse_json(json!({ "on": { "id": gcs_created.id } })); + assert_not_found(db.gcs_location_delete(&gcs_delete_missing)); + + let media_image_delete: IMediaImageDelete = + parse_json(json!({ "on": { "file_path": "/img/a.jpg" } })); + db.media_image_delete(&media_image_delete) + .expect("media image delete"); + let media_image_delete_missing: IMediaImageDelete = + parse_json(json!({ "on": { "id": media_image_created.id } })); + assert_not_found(db.media_image_delete(&media_image_delete_missing)); + for payload in [ + json!({ "rel": { "on_trade_product": { "id": trade_product_created.id } } }), + json!({ "rel": { "off_trade_product": { "id": trade_product_created.id } } }), + ] { + let opts: IMediaImageDelete = parse_json(payload); + let _ = db.media_image_delete(&opts); + } + + let nostr_profile_delete: INostrProfileDelete = + parse_json(json!({ "on": { "public_key": hex64('d') } })); + db.nostr_profile_delete(&nostr_profile_delete) + .expect("nostr profile delete"); + let nostr_profile_delete_missing: INostrProfileDelete = + parse_json(json!({ "on": { "id": nostr_profile_created.id } })); + assert_not_found(db.nostr_profile_delete(&nostr_profile_delete_missing)); + for payload in [ + json!({ "rel": { "on_relay": { "id": nostr_relay_created.id } } }), + json!({ "rel": { "off_relay": { "id": nostr_relay_created.id } } }), + ] { + let opts: INostrProfileDelete = parse_json(payload); + let _ = db.nostr_profile_delete(&opts); + } + + let nostr_relay_delete: INostrRelayDelete = + parse_json(json!({ "on": { "url": "wss://relay.example.com" } })); + db.nostr_relay_delete(&nostr_relay_delete) + .expect("nostr relay delete"); + let nostr_relay_delete_missing: INostrRelayDelete = + parse_json(json!({ "on": { "id": nostr_relay_created.id } })); + assert_not_found(db.nostr_relay_delete(&nostr_relay_delete_missing)); + for payload in [ + json!({ "rel": { "on_profile": { "public_key": hex64('d') } } }), + json!({ "rel": { "off_profile": { "public_key": hex64('d') } } }), + ] { + let opts: INostrRelayDelete = parse_json(payload); + let _ = db.nostr_relay_delete(&opts); + } + + let nostr_event_state_delete: INostrEventStateDelete = + parse_json(json!({ "on": { "key": "state-a" } })); + db.nostr_event_state_delete(&nostr_event_state_delete) + .expect("nostr event state delete"); + let nostr_event_state_delete_missing: INostrEventStateDelete = + parse_json(json!({ "on": { "id": nostr_event_state_created.id } })); + assert_not_found(db.nostr_event_state_delete(&nostr_event_state_delete_missing)); + + let log_error_delete: ILogErrorDelete = + parse_json(json!({ "on": { "nostr_pubkey": hex64('c') } })); + db.log_error_delete(&log_error_delete) + .expect("log error delete"); + let log_error_delete_missing: ILogErrorDelete = + parse_json(json!({ "on": { "id": log_error_created.id } })); + assert_not_found(db.log_error_delete(&log_error_delete_missing)); + + let farm_member_claim_delete: IFarmMemberClaimDelete = + parse_json(json!({ "on": { "member_pubkey": hex64('b') } })); + db.farm_member_claim_delete(&farm_member_claim_delete) + .expect("farm member claim delete"); + let farm_member_claim_delete_missing: IFarmMemberClaimDelete = + parse_json(json!({ "on": { "id": farm_member_claim_created.id } })); + assert_not_found(db.farm_member_claim_delete(&farm_member_claim_delete_missing)); + + let farm_member_delete: IFarmMemberDelete = + parse_json(json!({ "on": { "member_pubkey": hex64('b') } })); + db.farm_member_delete(&farm_member_delete) + .expect("farm member delete"); + let farm_member_delete_missing: IFarmMemberDelete = + parse_json(json!({ "on": { "id": farm_member_created.id } })); + assert_not_found(db.farm_member_delete(&farm_member_delete_missing)); + + let plot_tag_delete: IPlotTagDelete = parse_json(json!({ "on": { "tag": "east" } })); + db.plot_tag_delete(&plot_tag_delete) + .expect("plot tag delete"); + let plot_tag_delete_missing: IPlotTagDelete = + parse_json(json!({ "on": { "id": plot_tag_created.id } })); + assert_not_found(db.plot_tag_delete(&plot_tag_delete_missing)); + + let farm_tag_delete: IFarmTagDelete = parse_json(json!({ "on": { "tag": "regenerative" } })); + db.farm_tag_delete(&farm_tag_delete) + .expect("farm tag delete"); + let farm_tag_delete_missing: IFarmTagDelete = + parse_json(json!({ "on": { "id": farm_tag_created.id } })); + assert_not_found(db.farm_tag_delete(&farm_tag_delete_missing)); + + let plot_delete: IPlotDelete = parse_json(json!({ "on": { "d_tag": "plot-a" } })); + db.plot_delete(&plot_delete).expect("plot delete"); + let plot_delete_missing: IPlotDelete = parse_json(json!({ "on": { "id": plot_created.id } })); + assert_not_found(db.plot_delete(&plot_delete_missing)); + + let farm_delete: IFarmDelete = parse_json(json!({ "on": { "d_tag": "farm-a" } })); + db.farm_delete(&farm_delete).expect("farm delete"); + let farm_delete_missing: IFarmDelete = parse_json(json!({ "on": { "id": farm_created.id } })); + assert_not_found(db.farm_delete(&farm_delete_missing)); +} diff --git a/crates/replica-sync-wasm/Cargo.toml b/crates/replica-sync-wasm/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "radroots-replica-sync-wasm" +version.workspace = true +edition.workspace = true +authors = ["Radroots Authors"] +rust-version.workspace = true +license.workspace = true +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +base64 = { workspace = true } +radroots-events = { workspace = true, default-features = false, features = ["serde"] } +radroots-sql-core = { workspace = true, features = ["bridge"] } +radroots-sql-wasm-core = { workspace = true, default-features = false, features = ["bridge"] } +radroots-replica-sync = { 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 } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/replica-sync-wasm/pkg/package.json b/crates/replica-sync-wasm/pkg/package.json @@ -0,0 +1,19 @@ +{ + "name": "@radroots/replica-sync-wasm", + "version": "0.1.0", + "private": true, + "type": "module", + "files": [ + "dist" + ], + "main": "./dist/radroots_replica_sync_wasm.js", + "types": "./dist/radroots_replica_sync_wasm.d.ts", + "exports": { + ".": { + "types": "./dist/radroots_replica_sync_wasm.d.ts", + "import": "./dist/radroots_replica_sync_wasm.js", + "default": "./dist/radroots_replica_sync_wasm.js" + } + }, + "sideEffects": false +} diff --git a/crates/replica-sync-wasm/src/lib.rs b/crates/replica-sync-wasm/src/lib.rs @@ -0,0 +1,124 @@ +#![cfg(any(target_arch = "wasm32", coverage_nightly))] +#![forbid(unsafe_code)] + +#[cfg(target_arch = "wasm32")] +use base64::Engine; +#[cfg(target_arch = "wasm32")] +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +#[cfg(target_arch = "wasm32")] +use radroots_events::RadrootsNostrEvent; +#[cfg(target_arch = "wasm32")] +use radroots_sql_core::WasmSqlExecutor; +#[cfg(target_arch = "wasm32")] +use radroots_replica_sync::{ + RadrootsReplicaIdFactory, RadrootsReplicaIngestOutcome, RadrootsReplicaSyncRequest, + radroots_replica_ingest_event_with_factory, radroots_replica_sync_all, +}; +#[cfg(target_arch = "wasm32")] +use serde::Deserialize; +#[cfg(target_arch = "wasm32")] +use uuid::Uuid; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[cfg(target_arch = "wasm32")] +fn err_js<E: ToString>(err: E) -> JsValue { + JsValue::from_str(&err.to_string()) +} + +#[cfg(target_arch = "wasm32")] +struct WasmIdFactory; + +#[cfg(target_arch = "wasm32")] +impl RadrootsReplicaIdFactory for WasmIdFactory { + fn new_d_tag(&self) -> String { + let uuid = Uuid::now_v7(); + URL_SAFE_NO_PAD.encode(uuid.as_bytes()) + } +} + +#[cfg(target_arch = "wasm32")] +#[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, +} + +#[cfg(target_arch = "wasm32")] +fn parse_request(request_json: &str) -> Result<RadrootsReplicaSyncRequest, JsValue> { + serde_json::from_str(request_json).map_err(err_js) +} + +#[cfg(target_arch = "wasm32")] +fn parse_event(event_json: &str) -> Result<RadrootsNostrEvent, JsValue> { + 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, + }) +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen(js_name = replica_sync_sync_all)] +pub fn replica_sync_sync_all(request_json: &str) -> Result<JsValue, JsValue> { + let request = parse_request(request_json)?; + let exec = WasmSqlExecutor::new(); + let bundle = radroots_replica_sync_all(&exec, &request).map_err(err_js)?; + serde_wasm_bindgen::to_value(&bundle).map_err(err_js) +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen(js_name = replica_sync_ingest_event)] +pub fn replica_sync_ingest_event(event_json: &str) -> Result<JsValue, JsValue> { + let event = parse_event(event_json)?; + let exec = WasmSqlExecutor::new(); + let factory = WasmIdFactory; + let outcome = + radroots_replica_ingest_event_with_factory(&exec, &event, &factory).map_err(err_js)?; + let value = match outcome { + RadrootsReplicaIngestOutcome::Applied => "applied", + RadrootsReplicaIngestOutcome::Skipped => "skipped", + }; + Ok(JsValue::from_str(value)) +} + +#[cfg(coverage_nightly)] +pub fn coverage_branch_probe(input: bool) -> &'static str { + if input { + "replica-sync-wasm" + } else { + "replica-sync-wasm" + } +} + +#[cfg(all(test, coverage_nightly))] +mod tests { + use super::coverage_branch_probe; + + #[test] + fn coverage_branch_probe_hits_both_paths() { + assert_eq!(coverage_branch_probe(true), "replica-sync-wasm"); + assert_eq!(coverage_branch_probe(false), "replica-sync-wasm"); + } +} diff --git a/crates/replica-sync/Cargo.toml b/crates/replica-sync/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "radroots-replica-sync" +version.workspace = true +edition.workspace = true +authors = ["Radroots Authors"] +rust-version.workspace = true +license.workspace = true +publish = false + +[lib] +crate-type = ["rlib"] + +[features] +default = ["std"] +std = [ + "radroots-events/std", + "radroots-events-codec/std", + "dep:base64", + "dep:uuid", +] + +[dependencies] +radroots-events = { workspace = true, default-features = false, features = ["serde"] } +radroots-events-codec = { workspace = true, default-features = false, features = ["serde_json"] } +radroots-sql-core = { workspace = true } +radroots-replica-db-schema = { workspace = true } +radroots-replica-db = { workspace = true } +radroots-types = { workspace = true } +hex = { workspace = true } +serde = { workspace = true, default-features = false, features = ["alloc", "derive"] } +serde_json = { workspace = true, default-features = false, features = ["alloc"] } +sha2 = { workspace = true, default-features = false } +base64 = { workspace = true, optional = true } +uuid = { workspace = true, optional = true } + +[dev-dependencies] +radroots-sql-core = { workspace = true, features = ["native"] } +radroots-replica-db = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/replica-sync/src/canonical.rs b/crates/replica-sync/src/canonical.rs @@ -0,0 +1,95 @@ +#[cfg(not(feature = "std"))] +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; + +use serde::Serialize; +use serde_json::{Map, Value}; + +use crate::error::RadrootsReplicaEventsError; + +pub fn canonical_json_string<T: Serialize>(value: &T) -> Result<String, RadrootsReplicaEventsError> { + let value = serde_json::to_value(value).map_err(|_| { + RadrootsReplicaEventsError::InvalidData("canonical json serialization failed".to_string()) + })?; + Ok(canonicalize_value(value).to_string()) +} + +fn canonicalize_value(value: Value) -> Value { + match value { + Value::Object(map) => canonicalize_object(map), + Value::Array(values) => { + let values = values + .into_iter() + .map(canonicalize_value) + .collect::<Vec<_>>(); + Value::Array(values) + } + other => other, + } +} + +fn canonicalize_object(map: Map<String, Value>) -> Value { + let mut entries = map.into_iter().collect::<Vec<_>>(); + entries.sort_by(|a, b| a.0.cmp(&b.0)); + let mut ordered = Map::new(); + for (key, value) in entries { + ordered.insert(key, canonicalize_value(value)); + } + Value::Object(ordered) +} + +#[cfg(test)] +mod tests { + use super::canonical_json_string; + use serde::Serialize; + + #[derive(Serialize)] + struct CanonicalFixture { + z: u32, + a: NestedFixture, + } + + #[derive(Serialize)] + struct NestedFixture { + b: u32, + a: u32, + } + + #[test] + fn canonical_json_string_sorts_object_keys_recursively() { + let value = CanonicalFixture { + z: 2, + a: NestedFixture { b: 3, a: 1 }, + }; + let json = canonical_json_string(&value).expect("json"); + assert_eq!(json, r#"{"a":{"a":1,"b":3},"z":2}"#); + } + + #[test] + fn canonical_json_string_handles_arrays() { + let json = canonical_json_string(&serde_json::json!([{"b": 2, "a": 1}])).expect("json"); + assert_eq!(json, r#"[{"a":1,"b":2}]"#); + } + + struct AlwaysErr; + + impl Serialize for AlwaysErr { + fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + Err(serde::ser::Error::custom("always fail")) + } + } + + #[test] + fn canonical_json_string_propagates_serialization_errors() { + let err = canonical_json_string(&AlwaysErr).expect_err("serialize fail"); + assert!( + err.to_string() + .contains("canonical json serialization failed") + ); + } +} diff --git a/crates/replica-sync/src/emit.rs b/crates/replica-sync/src/emit.rs @@ -0,0 +1,1415 @@ +#[cfg(not(feature = "std"))] +use alloc::format; +#[cfg(not(feature = "std"))] +use alloc::{ + collections::BTreeMap, + string::{String, ToString}, + vec::Vec, +}; +#[cfg(feature = "std")] +use std::collections::BTreeMap; + +use radroots_events::farm::{ + RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef, RadrootsGcsLocation, RadrootsGeoJsonPoint, + RadrootsGeoJsonPolygon, +}; +use radroots_events::kinds::{KIND_FARM, KIND_LIST_SET_GENERIC, KIND_PLOT}; +use radroots_events::plot::RadrootsPlot; +use radroots_events::profile::{ + RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, RadrootsProfileType, + radroots_profile_type_from_tag_value, radroots_profile_type_tag_value, +}; +use radroots_events_codec::farm::encode as farm_encode; +use radroots_events_codec::farm::list_sets as farm_list_sets; +use radroots_events_codec::list_set::encode as list_set_encode; +use radroots_events_codec::plot::encode as plot_encode; +use radroots_events_codec::wire::WireEventParts; +use radroots_sql_core::SqlExecutor; +use radroots_replica_db::{ + farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, nostr_profile, + plot, plot_gcs_location, plot_tag, +}; +use radroots_replica_db_schema::farm::{ + Farm, IFarmFieldsFilter, IFarmFindMany, IFarmFindOne, IFarmFindOneArgs, +}; +use radroots_replica_db_schema::farm_gcs_location::{ + FarmGcsLocation, IFarmGcsLocationFieldsFilter, IFarmGcsLocationFindMany, +}; +use radroots_replica_db_schema::farm_member::{ + FarmMember, IFarmMemberFieldsFilter, IFarmMemberFindMany, +}; +use radroots_replica_db_schema::farm_member_claim::{ + FarmMemberClaim, IFarmMemberClaimFieldsFilter, IFarmMemberClaimFindMany, +}; +use radroots_replica_db_schema::farm_tag::{IFarmTagFieldsFilter, IFarmTagFindMany}; +use radroots_replica_db_schema::gcs_location::{ + GcsLocation, GcsLocationQueryBindValues, IGcsLocationFindOne, IGcsLocationFindOneArgs, +}; +use radroots_replica_db_schema::nostr_profile::{ + INostrProfileFindOne, INostrProfileFindOneArgs, NostrProfileQueryBindValues, +}; +use radroots_replica_db_schema::plot::{IPlotFieldsFilter, IPlotFindMany, Plot}; +use radroots_replica_db_schema::plot_gcs_location::{ + IPlotGcsLocationFieldsFilter, IPlotGcsLocationFindMany, PlotGcsLocation, +}; +use radroots_replica_db_schema::plot_tag::{IPlotTagFieldsFilter, IPlotTagFindMany}; +use serde_json::Value; + +use crate::canonical::canonical_json_string; +use crate::error::RadrootsReplicaEventsError; +use crate::geo::{geojson_point_from_lat_lng, geojson_polygon_circle_wgs84}; +use crate::types::{ + RADROOTS_REPLICA_TRANSFER_VERSION, RadrootsReplicaEventDraft, RadrootsReplicaFarmSelector, + RadrootsReplicaSyncBundle, RadrootsReplicaSyncOptions, RadrootsReplicaSyncRequest, +}; + +const ROLE_PRIMARY: &str = "primary"; +const ROLE_MEMBER: &str = "member"; +const ROLE_OWNER: &str = "owner"; +const ROLE_WORKER: &str = "worker"; + +pub fn radroots_replica_sync_all<E: SqlExecutor>( + exec: &E, + request: &RadrootsReplicaSyncRequest, +) -> Result<RadrootsReplicaSyncBundle, RadrootsReplicaEventsError> { + radroots_replica_sync_all_with_options(exec, &request.farm, request.options.as_ref()) +} + +pub fn radroots_replica_sync_all_with_options<E: SqlExecutor>( + exec: &E, + farm_selector: &RadrootsReplicaFarmSelector, + options: Option<&RadrootsReplicaSyncOptions>, +) -> Result<RadrootsReplicaSyncBundle, RadrootsReplicaEventsError> { + let farm = resolve_farm(exec, farm_selector)?; + let include_profiles = options.and_then(|opt| opt.include_profiles).unwrap_or(true); + let include_list_sets = options + .and_then(|opt| opt.include_list_sets) + .unwrap_or(true); + let include_claims = options + .and_then(|opt| opt.include_membership_claims) + .unwrap_or(true); + + let mut events = Vec::new(); + + if include_profiles { + let profiles = radroots_replica_profile_events(exec, &farm)?; + events.extend(profiles); + } + + events.push(radroots_replica_farm_event(exec, &farm)?); + + let plots = radroots_replica_plot_events(exec, &farm)?; + events.extend(plots); + + if include_list_sets { + let list_sets = radroots_replica_list_set_events(exec, &farm)?; + events.extend(list_sets); + } + + if include_claims { + let claims = radroots_replica_membership_claim_events(exec, &farm.pubkey)?; + events.extend(claims); + } + + Ok(RadrootsReplicaSyncBundle { + version: RADROOTS_REPLICA_TRANSFER_VERSION, + events, + }) +} + +pub fn radroots_replica_profile_events<E: SqlExecutor>( + exec: &E, + farm: &Farm, +) -> Result<Vec<RadrootsReplicaEventDraft>, RadrootsReplicaEventsError> { + let mut pubkeys = collect_profile_pubkeys(exec, farm)?; + pubkeys.sort(); + pubkeys.dedup(); + + let mut events = Vec::new(); + for pubkey in pubkeys { + if let Some(profile) = load_profile(exec, &pubkey)? { + events.push(profile_event(&pubkey, profile)?); + } + } + Ok(events) +} + +pub fn radroots_replica_farm_event<E: SqlExecutor>( + exec: &E, + farm: &Farm, +) -> Result<RadrootsReplicaEventDraft, RadrootsReplicaEventsError> { + let tags = collect_farm_tags(exec, &farm.id)?; + let location = load_farm_location(exec, farm)?; + let farm_event = RadrootsFarm { + d_tag: farm.d_tag.clone(), + name: farm.name.clone(), + about: farm.about.clone(), + website: farm.website.clone(), + picture: farm.picture.clone(), + banner: farm.banner.clone(), + location, + tags: if tags.is_empty() { None } else { Some(tags) }, + }; + let tags = farm_encode::farm_build_tags(&farm_event)?; + let content = canonical_json_string(&farm_event)?; + let parts = WireEventParts { + kind: KIND_FARM, + content, + tags, + }; + Ok(parts_to_draft(&farm.pubkey, parts)) +} + +pub fn radroots_replica_plot_events<E: SqlExecutor>( + exec: &E, + farm: &Farm, +) -> Result<Vec<RadrootsReplicaEventDraft>, RadrootsReplicaEventsError> { + let plots = load_plots(exec, &farm.id)?; + let mut events = Vec::new(); + for plot_row in plots { + let tags = collect_plot_tags(exec, &plot_row.id)?; + let location = load_plot_location(exec, &plot_row)?; + let plot_event = RadrootsPlot { + d_tag: plot_row.d_tag.clone(), + farm: RadrootsFarmRef { + pubkey: farm.pubkey.clone(), + d_tag: farm.d_tag.clone(), + }, + name: plot_row.name.clone(), + about: plot_row.about.clone(), + location, + tags: if tags.is_empty() { None } else { Some(tags) }, + }; + let tags = plot_encode::plot_build_tags(&plot_event)?; + let content = canonical_json_string(&plot_event)?; + let parts = WireEventParts { + kind: KIND_PLOT, + content, + tags, + }; + events.push(parts_to_draft(&farm.pubkey, parts)); + } + Ok(events) +} + +pub fn radroots_replica_list_set_events<E: SqlExecutor>( + exec: &E, + farm: &Farm, +) -> Result<Vec<RadrootsReplicaEventDraft>, RadrootsReplicaEventsError> { + let members = load_farm_members(exec, &farm.id)?; + let plots = load_plots(exec, &farm.id)?; + + let members_list = + farm_list_sets::farm_members_list_set(&farm.d_tag, role_pubkeys(&members, ROLE_MEMBER))?; + let owners_list = + farm_list_sets::farm_owners_list_set(&farm.d_tag, role_pubkeys(&members, ROLE_OWNER))?; + let workers_list = + farm_list_sets::farm_workers_list_set(&farm.d_tag, role_pubkeys(&members, ROLE_WORKER))?; + + let plot_ids = sorted_plot_ids(&plots); + let plots_list = farm_list_sets::farm_plots_list_set(&farm.d_tag, &farm.pubkey, plot_ids)?; + + let list_sets = [members_list, owners_list, workers_list, plots_list]; + let mut events = Vec::new(); + for list_set in list_sets { + let parts = list_set_encode::to_wire_parts_with_kind(&list_set, KIND_LIST_SET_GENERIC)?; + events.push(parts_to_draft(&farm.pubkey, parts)); + } + Ok(events) +} + +pub fn radroots_replica_membership_claim_events<E: SqlExecutor>( + exec: &E, + farm_pubkey: &str, +) -> Result<Vec<RadrootsReplicaEventDraft>, RadrootsReplicaEventsError> { + let claims = load_member_claims(exec, farm_pubkey)?; + let mut by_member: BTreeMap<String, Vec<String>> = BTreeMap::new(); + for claim in claims { + by_member + .entry(claim.member_pubkey.clone()) + .or_default() + .push(claim.farm_pubkey.clone()); + } + + let mut events = Vec::new(); + for (member_pubkey, _) in by_member.iter() { + let all_claims = load_member_claims_for_member(exec, member_pubkey)?; + let mut farm_pubkeys = all_claims + .into_iter() + .map(|claim| claim.farm_pubkey) + .collect::<Vec<String>>(); + farm_pubkeys.sort(); + farm_pubkeys.dedup(); + let list_set = farm_list_sets::member_of_farms_list_set(farm_pubkeys)?; + let parts = list_set_encode::to_wire_parts_with_kind(&list_set, KIND_LIST_SET_GENERIC)?; + events.push(parts_to_draft(member_pubkey, parts)); + } + + Ok(events) +} + +fn resolve_farm<E: SqlExecutor>( + exec: &E, + selector: &RadrootsReplicaFarmSelector, +) -> Result<Farm, RadrootsReplicaEventsError> { + if let Some(id) = selector.id.as_ref().filter(|v| !v.trim().is_empty()) { + let result_query = farm::find_one( + exec, + &IFarmFindOne::On(IFarmFindOneArgs { + on: radroots_replica_db_schema::farm::FarmQueryBindValues::Id { id: id.clone() }, + }), + ); + let result = result_query?; + return result.result.ok_or_else(|| { + RadrootsReplicaEventsError::InvalidSelector(format!("farm not found: {id}")) + }); + } + + let d_tag = selector + .d_tag + .as_ref() + .map(|v| v.trim()) + .filter(|v| !v.is_empty()); + let pubkey = selector + .pubkey + .as_ref() + .map(|v| v.trim()) + .filter(|v| !v.is_empty()); + + let (d_tag, pubkey) = match (d_tag, pubkey) { + (Some(d_tag), Some(pubkey)) => (d_tag, pubkey), + _ => { + return Err(RadrootsReplicaEventsError::InvalidSelector( + "farm selector requires id or (d_tag + pubkey)".to_string(), + )); + } + }; + + let filter = IFarmFieldsFilter { + id: None, + created_at: None, + updated_at: None, + d_tag: Some(d_tag.to_string()), + pubkey: Some(pubkey.to_string()), + name: None, + about: None, + website: None, + picture: None, + banner: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, + }; + let result_query = farm::find_many( + exec, + &IFarmFindMany { + filter: Some(filter), + }, + ); + let result = result_query?; + if result.results.len() == 1 { + return Ok(result.results.into_iter().next().expect("farm result")); + } + Err(RadrootsReplicaEventsError::InvalidSelector( + "farm selector did not resolve to a single farm".to_string(), + )) +} + +fn collect_farm_tags<E: SqlExecutor>( + exec: &E, + farm_id: &str, +) -> Result<Vec<String>, RadrootsReplicaEventsError> { + let filter = IFarmTagFieldsFilter { + id: None, + created_at: None, + updated_at: None, + farm_id: Some(farm_id.to_string()), + tag: None, + }; + let result_query = farm_tag::find_many( + exec, + &IFarmTagFindMany { + filter: Some(filter), + }, + ); + let result = result_query?; + let mut tags = result + .results + .into_iter() + .map(|row| row.tag) + .collect::<Vec<_>>(); + tags.sort(); + tags.dedup(); + Ok(tags) +} + +fn collect_plot_tags<E: SqlExecutor>( + exec: &E, + plot_id: &str, +) -> Result<Vec<String>, RadrootsReplicaEventsError> { + let filter = IPlotTagFieldsFilter { + id: None, + created_at: None, + updated_at: None, + plot_id: Some(plot_id.to_string()), + tag: None, + }; + let result_query = plot_tag::find_many( + exec, + &IPlotTagFindMany { + filter: Some(filter), + }, + ); + let result = result_query?; + let mut tags = result + .results + .into_iter() + .map(|row| row.tag) + .collect::<Vec<_>>(); + tags.sort(); + tags.dedup(); + Ok(tags) +} + +fn load_farm_members<E: SqlExecutor>( + exec: &E, + farm_id: &str, +) -> Result<Vec<FarmMember>, RadrootsReplicaEventsError> { + let filter = IFarmMemberFieldsFilter { + id: None, + created_at: None, + updated_at: None, + farm_id: Some(farm_id.to_string()), + member_pubkey: None, + role: None, + }; + let result_query = farm_member::find_many( + exec, + &IFarmMemberFindMany { + filter: Some(filter), + }, + ); + let result = result_query?; + Ok(result.results) +} + +fn role_pubkeys(members: &[FarmMember], role: &str) -> Vec<String> { + let mut values = members + .iter() + .filter(|member| member.role == role) + .map(|member| member.member_pubkey.clone()) + .collect::<Vec<_>>(); + values.sort(); + values.dedup(); + values +} + +fn sorted_plot_ids(plots: &[Plot]) -> Vec<String> { + let mut ids = plots + .iter() + .map(|plot| plot.d_tag.clone()) + .collect::<Vec<_>>(); + ids.sort(); + ids.dedup(); + ids +} + +fn load_plots<E: SqlExecutor>( + exec: &E, + farm_id: &str, +) -> Result<Vec<Plot>, RadrootsReplicaEventsError> { + let filter = IPlotFieldsFilter { + id: None, + created_at: None, + updated_at: None, + d_tag: None, + farm_id: Some(farm_id.to_string()), + name: None, + about: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, + }; + let result_query = plot::find_many( + exec, + &IPlotFindMany { + filter: Some(filter), + }, + ); + let result = result_query?; + let mut plots = result.results; + plots.sort_by(|a, b| a.d_tag.cmp(&b.d_tag)); + Ok(plots) +} + +fn load_farm_location<E: SqlExecutor>( + exec: &E, + farm: &Farm, +) -> Result<Option<RadrootsFarmLocation>, RadrootsReplicaEventsError> { + let location = load_gcs_location_for_farm(exec, &farm.id)?; + Ok(location.map(|gcs| RadrootsFarmLocation { + primary: farm.location_primary.clone(), + city: farm.location_city.clone(), + region: farm.location_region.clone(), + country: farm.location_country.clone(), + gcs, + })) +} + +fn load_plot_location<E: SqlExecutor>( + exec: &E, + plot: &Plot, +) -> Result<Option<radroots_events::plot::RadrootsPlotLocation>, RadrootsReplicaEventsError> { + let location = load_gcs_location_for_plot(exec, &plot.id)?; + Ok( + location.map(|gcs| radroots_events::plot::RadrootsPlotLocation { + primary: plot.location_primary.clone(), + city: plot.location_city.clone(), + region: plot.location_region.clone(), + country: plot.location_country.clone(), + gcs, + }), + ) +} + +fn load_gcs_location_for_farm<E: SqlExecutor>( + exec: &E, + farm_id: &str, +) -> Result<Option<RadrootsGcsLocation>, RadrootsReplicaEventsError> { + let primary = load_relation_by_role(exec, farm_id, ROLE_PRIMARY, RelationType::Farm)?; + match primary { + Some(gcs) => Ok(Some(gcs)), + None => load_relation_by_role(exec, farm_id, "", RelationType::Farm), + } +} + +fn load_gcs_location_for_plot<E: SqlExecutor>( + exec: &E, + plot_id: &str, +) -> Result<Option<RadrootsGcsLocation>, RadrootsReplicaEventsError> { + let primary = load_relation_by_role(exec, plot_id, ROLE_PRIMARY, RelationType::Plot)?; + match primary { + Some(gcs) => Ok(Some(gcs)), + None => load_relation_by_role(exec, plot_id, "", RelationType::Plot), + } +} + +enum RelationType { + Farm, + Plot, +} + +fn load_relation_by_role<E: SqlExecutor>( + exec: &E, + id: &str, + role: &str, + relation: RelationType, +) -> Result<Option<RadrootsGcsLocation>, RadrootsReplicaEventsError> { + let mut rels = match relation { + RelationType::Farm => { + let filter = IFarmGcsLocationFieldsFilter { + id: None, + created_at: None, + updated_at: None, + farm_id: Some(id.to_string()), + gcs_location_id: None, + role: if role.is_empty() { + None + } else { + Some(role.to_string()) + }, + }; + let result_query = farm_gcs_location::find_many( + exec, + &IFarmGcsLocationFindMany { + filter: Some(filter), + }, + ); + let result = result_query?; + result + .results + .into_iter() + .map(RelationRow::Farm) + .collect::<Vec<_>>() + } + RelationType::Plot => { + let filter = IPlotGcsLocationFieldsFilter { + id: None, + created_at: None, + updated_at: None, + plot_id: Some(id.to_string()), + gcs_location_id: None, + role: if role.is_empty() { + None + } else { + Some(role.to_string()) + }, + }; + let result_query = plot_gcs_location::find_many( + exec, + &IPlotGcsLocationFindMany { + filter: Some(filter), + }, + ); + let result = result_query?; + result + .results + .into_iter() + .map(RelationRow::Plot) + .collect::<Vec<_>>() + } + }; + + if rels.is_empty() { + return Ok(None); + } + + rels.sort_by(|a, b| { + let rank = location_role_rank(a.role()).cmp(&location_role_rank(b.role())); + rank.then_with(|| a.gcs_location_id().cmp(b.gcs_location_id())) + }); + let gcs_id = rels[0].gcs_location_id().to_string(); + let gcs_result = gcs_location::find_one( + exec, + &IGcsLocationFindOne::On(IGcsLocationFindOneArgs { + on: GcsLocationQueryBindValues::Id { id: gcs_id }, + }), + ); + let gcs = gcs_result?.result.ok_or_else(|| { + RadrootsReplicaEventsError::InvalidData("gcs_location not found".to_string()) + })?; + Ok(Some(gcs_location_to_event(&gcs)?)) +} + +enum RelationRow { + Farm(FarmGcsLocation), + Plot(PlotGcsLocation), +} + +impl RelationRow { + fn gcs_location_id(&self) -> &str { + match self { + Self::Farm(row) => row.gcs_location_id.as_str(), + Self::Plot(row) => row.gcs_location_id.as_str(), + } + } + + fn role(&self) -> &str { + match self { + Self::Farm(row) => row.role.as_str(), + Self::Plot(row) => row.role.as_str(), + } + } +} + +fn location_role_rank(role: &str) -> u8 { + if role == ROLE_PRIMARY { 0 } else { 1 } +} + +fn gcs_location_to_event( + gcs: &GcsLocation, +) -> Result<RadrootsGcsLocation, RadrootsReplicaEventsError> { + let point = parse_point(&gcs.point, gcs.lat, gcs.lng); + let polygon = parse_polygon(&gcs.polygon, gcs.lat, gcs.lng); + Ok(RadrootsGcsLocation { + lat: gcs.lat, + lng: gcs.lng, + geohash: gcs.geohash.clone(), + point, + polygon, + accuracy: gcs.accuracy, + altitude: gcs.altitude, + tag_0: gcs.tag_0.clone(), + label: gcs.label.clone(), + area: gcs.area, + elevation: gcs.elevation, + soil: gcs.soil.clone(), + climate: gcs.climate.clone(), + gc_id: gcs.gc_id.clone(), + gc_name: gcs.gc_name.clone(), + gc_admin1_id: gcs.gc_admin1_id.clone(), + gc_admin1_name: gcs.gc_admin1_name.clone(), + gc_country_id: gcs.gc_country_id.clone(), + gc_country_name: gcs.gc_country_name.clone(), + }) +} + +fn parse_point(value: &str, lat: f64, lng: f64) -> RadrootsGeoJsonPoint { + if !value.trim().is_empty() { + if let Ok(parsed) = serde_json::from_str::<RadrootsGeoJsonPoint>(value) { + return parsed; + } + } + geojson_point_from_lat_lng(lat, lng) +} + +fn parse_polygon(value: &str, lat: f64, lng: f64) -> RadrootsGeoJsonPolygon { + if !value.trim().is_empty() { + if let Ok(parsed) = serde_json::from_str::<RadrootsGeoJsonPolygon>(value) { + if !parsed.coordinates.is_empty() && !parsed.coordinates[0].is_empty() { + return parsed; + } + } + } + geojson_polygon_circle_wgs84(lat, lng, 100.0, 64) +} + +fn load_profile<E: SqlExecutor>( + exec: &E, + pubkey: &str, +) -> Result<Option<radroots_replica_db_schema::nostr_profile::NostrProfile>, RadrootsReplicaEventsError> +{ + let result_query = nostr_profile::find_one( + exec, + &INostrProfileFindOne::On(INostrProfileFindOneArgs { + on: NostrProfileQueryBindValues::PublicKey { + public_key: pubkey.to_string(), + }, + }), + ); + let result = result_query?; + Ok(result.result) +} + +fn profile_event( + pubkey: &str, + profile: radroots_replica_db_schema::nostr_profile::NostrProfile, +) -> Result<RadrootsReplicaEventDraft, RadrootsReplicaEventsError> { + let profile_type = match profile.profile_type.as_str() { + "individual" | "farmer" => Some(RadrootsProfileType::Individual), + "farm" => Some(RadrootsProfileType::Farm), + "coop" => Some(RadrootsProfileType::Coop), + "any" => Some(RadrootsProfileType::Any), + other => radroots_profile_type_from_tag_value(other), + }; + let profile_event = RadrootsProfile { + name: profile.name, + display_name: profile.display_name, + nip05: profile.nip05, + about: profile.about, + website: profile.website, + picture: profile.picture, + banner: profile.banner, + lud06: profile.lud06, + lud16: profile.lud16, + bot: None, + }; + let content = serialize_profile_content(&profile_event)?; + let mut tags = Vec::new(); + if let Some(profile_type) = profile_type { + let mut tag = Vec::with_capacity(2); + tag.push(RADROOTS_PROFILE_TYPE_TAG_KEY.to_string()); + tag.push(radroots_profile_type_tag_value(profile_type).to_string()); + tags.push(tag); + } + Ok(RadrootsReplicaEventDraft { + kind: radroots_events::kinds::KIND_PROFILE, + author: pubkey.to_string(), + content, + tags, + }) +} + +fn serialize_profile_content( + profile: &RadrootsProfile, +) -> Result<String, RadrootsReplicaEventsError> { + let mut obj = serde_json::Map::new(); + obj.insert("name".to_string(), Value::from(profile.name.clone())); + if let Some(value) = profile.display_name.as_ref() { + obj.insert("display_name".to_string(), Value::from(value.clone())); + } + if let Some(value) = profile.nip05.as_ref() { + obj.insert("nip05".to_string(), Value::from(value.clone())); + } + if let Some(value) = profile.about.as_ref() { + obj.insert("about".to_string(), Value::from(value.clone())); + } + if let Some(value) = profile.website.as_ref() { + obj.insert("website".to_string(), Value::from(value.clone())); + } + if let Some(value) = profile.picture.as_ref() { + obj.insert("picture".to_string(), Value::from(value.clone())); + } + if let Some(value) = profile.banner.as_ref() { + obj.insert("banner".to_string(), Value::from(value.clone())); + } + if let Some(value) = profile.lud06.as_ref() { + obj.insert("lud06".to_string(), Value::from(value.clone())); + } + if let Some(value) = profile.lud16.as_ref() { + obj.insert("lud16".to_string(), Value::from(value.clone())); + } + canonical_json_string(&Value::Object(obj)) +} + +fn collect_member_pubkeys<E: SqlExecutor>( + exec: &E, + farm_id: &str, +) -> Result<Vec<String>, RadrootsReplicaEventsError> { + let members = load_farm_members(exec, farm_id)?; + let mut pubkeys = members + .into_iter() + .map(|row| row.member_pubkey) + .collect::<Vec<_>>(); + pubkeys.sort(); + pubkeys.dedup(); + Ok(pubkeys) +} + +fn collect_profile_pubkeys<E: SqlExecutor>( + exec: &E, + farm: &Farm, +) -> Result<Vec<String>, RadrootsReplicaEventsError> { + let mut pubkeys = collect_member_pubkeys(exec, &farm.id)?; + let claims = load_member_claims(exec, &farm.pubkey)?; + pubkeys.extend(claims.into_iter().map(|claim| claim.member_pubkey)); + pubkeys.push(farm.pubkey.clone()); + Ok(pubkeys) +} + +fn load_member_claims<E: SqlExecutor>( + exec: &E, + farm_pubkey: &str, +) -> Result<Vec<FarmMemberClaim>, RadrootsReplicaEventsError> { + let filter = IFarmMemberClaimFieldsFilter { + id: None, + created_at: None, + updated_at: None, + member_pubkey: None, + farm_pubkey: Some(farm_pubkey.to_string()), + }; + let result_query = farm_member_claim::find_many( + exec, + &IFarmMemberClaimFindMany { + filter: Some(filter), + }, + ); + let result = result_query?; + Ok(result.results) +} + +fn load_member_claims_for_member<E: SqlExecutor>( + exec: &E, + member_pubkey: &str, +) -> Result<Vec<FarmMemberClaim>, RadrootsReplicaEventsError> { + let filter = IFarmMemberClaimFieldsFilter { + id: None, + created_at: None, + updated_at: None, + member_pubkey: Some(member_pubkey.to_string()), + farm_pubkey: None, + }; + let result_query = farm_member_claim::find_many( + exec, + &IFarmMemberClaimFindMany { + filter: Some(filter), + }, + ); + let result = result_query?; + Ok(result.results) +} + +fn parts_to_draft(author: &str, parts: WireEventParts) -> RadrootsReplicaEventDraft { + RadrootsReplicaEventDraft { + kind: parts.kind, + author: author.to_string(), + content: parts.content, + tags: parts.tags, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use radroots_sql_core::SqliteExecutor; + use radroots_replica_db::{ + farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, + migrations, nostr_profile, plot, plot_gcs_location, plot_tag, + }; + use radroots_replica_db_schema::farm::{IFarmFields, IFarmFieldsFilter, IFarmFindMany}; + use radroots_replica_db_schema::farm_gcs_location::{ + IFarmGcsLocationFields, IFarmGcsLocationFindMany, + }; + use radroots_replica_db_schema::farm_member::IFarmMemberFields; + use radroots_replica_db_schema::farm_member_claim::IFarmMemberClaimFields; + use radroots_replica_db_schema::farm_tag::IFarmTagFields; + use radroots_replica_db_schema::gcs_location::IGcsLocationFields; + use radroots_replica_db_schema::nostr_profile::INostrProfileFields; + use radroots_replica_db_schema::plot::{IPlotFields, IPlotFindMany}; + use radroots_replica_db_schema::plot_gcs_location::{ + IPlotGcsLocationFields, IPlotGcsLocationFindMany, + }; + use radroots_replica_db_schema::plot_tag::IPlotTagFields; + + fn seed(exec: &SqliteExecutor) -> (Farm, Plot, Plot) { + migrations::run_all_up(exec).expect("migrations"); + let farm = farm::create( + exec, + &IFarmFields { + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + pubkey: "f".repeat(64), + name: "farm".to_string(), + about: Some("about".to_string()), + website: Some("https://farm.example.com".to_string()), + picture: Some("https://farm.example.com/p.png".to_string()), + banner: Some("https://farm.example.com/b.png".to_string()), + location_primary: Some("primary".to_string()), + location_city: Some("city".to_string()), + location_region: Some("region".to_string()), + location_country: Some("country".to_string()), + }, + ) + .expect("farm") + .result; + + let gcs_primary = gcs_location::create( + exec, + &IGcsLocationFields { + d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), + lat: 10.0, + lng: 20.0, + geohash: "s0".to_string(), + point: "{\"type\":\"Point\",\"coordinates\":[20.0,10.0]}".to_string(), + polygon: + "{\"type\":\"Polygon\",\"coordinates\":[[[20.0,10.0],[20.1,10.1],[19.9,10.1],[20.0,10.0]]]}".to_string(), + accuracy: None, + altitude: None, + tag_0: None, + label: None, + area: None, + elevation: None, + soil: None, + climate: None, + gc_id: None, + gc_name: None, + gc_admin1_id: None, + gc_admin1_name: None, + gc_country_id: None, + gc_country_name: None, + }, + ) + .expect("gcs primary") + .result; + let gcs_secondary = gcs_location::create( + exec, + &IGcsLocationFields { + d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), + lat: 11.0, + lng: 21.0, + geohash: "s1".to_string(), + point: "{".to_string(), + polygon: "{\"type\":\"Polygon\",\"coordinates\":[[]]}".to_string(), + accuracy: None, + altitude: None, + tag_0: None, + label: None, + area: None, + elevation: None, + soil: None, + climate: None, + gc_id: None, + gc_name: None, + gc_admin1_id: None, + gc_admin1_name: None, + gc_country_id: None, + gc_country_name: None, + }, + ) + .expect("gcs secondary") + .result; + + let _ = farm_gcs_location::create( + exec, + &IFarmGcsLocationFields { + farm_id: farm.id.clone(), + gcs_location_id: gcs_secondary.id.clone(), + role: "".to_string(), + }, + ) + .expect("farm gcs secondary"); + let _ = farm_gcs_location::create( + exec, + &IFarmGcsLocationFields { + farm_id: farm.id.clone(), + gcs_location_id: gcs_primary.id.clone(), + role: "primary".to_string(), + }, + ) + .expect("farm gcs primary"); + + let plot_primary = plot::create( + exec, + &IPlotFields { + d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), + farm_id: farm.id.clone(), + name: "plot-primary".to_string(), + about: Some("plot about".to_string()), + location_primary: Some("plot primary".to_string()), + location_city: Some("plot city".to_string()), + location_region: Some("plot region".to_string()), + location_country: Some("plot country".to_string()), + }, + ) + .expect("plot primary") + .result; + let plot_secondary = plot::create( + exec, + &IPlotFields { + d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), + farm_id: farm.id.clone(), + name: "plot-secondary".to_string(), + about: Some("plot secondary about".to_string()), + location_primary: Some("plot secondary primary".to_string()), + location_city: None, + location_region: None, + location_country: None, + }, + ) + .expect("plot secondary") + .result; + + let _ = plot_gcs_location::create( + exec, + &IPlotGcsLocationFields { + plot_id: plot_primary.id.clone(), + gcs_location_id: gcs_secondary.id.clone(), + role: "secondary".to_string(), + }, + ) + .expect("plot primary secondary relation"); + let _ = plot_gcs_location::create( + exec, + &IPlotGcsLocationFields { + plot_id: plot_primary.id.clone(), + gcs_location_id: gcs_primary.id.clone(), + role: "primary".to_string(), + }, + ) + .expect("plot primary relation"); + let _ = plot_gcs_location::create( + exec, + &IPlotGcsLocationFields { + plot_id: plot_secondary.id.clone(), + gcs_location_id: gcs_secondary.id.clone(), + role: "secondary".to_string(), + }, + ) + .expect("plot secondary relation"); + + let _ = farm_tag::create( + exec, + &IFarmTagFields { + farm_id: farm.id.clone(), + tag: "coffee".to_string(), + }, + ) + .expect("farm tag"); + let _ = plot_tag::create( + exec, + &IPlotTagFields { + plot_id: plot_primary.id.clone(), + tag: "orchard".to_string(), + }, + ) + .expect("plot tag"); + + let _ = farm_member::create( + exec, + &IFarmMemberFields { + farm_id: farm.id.clone(), + member_pubkey: "m".repeat(64), + role: "member".to_string(), + }, + ) + .expect("member"); + let _ = farm_member::create( + exec, + &IFarmMemberFields { + farm_id: farm.id.clone(), + member_pubkey: "o".repeat(64), + role: "owner".to_string(), + }, + ) + .expect("owner"); + let _ = farm_member::create( + exec, + &IFarmMemberFields { + farm_id: farm.id.clone(), + member_pubkey: "u".repeat(64), + role: "worker".to_string(), + }, + ) + .expect("worker"); + let _ = farm_member::create( + exec, + &IFarmMemberFields { + farm_id: farm.id.clone(), + member_pubkey: "x".repeat(64), + role: "member".to_string(), + }, + ) + .expect("member no profile"); + + let _ = farm_member_claim::create( + exec, + &IFarmMemberClaimFields { + member_pubkey: "m".repeat(64), + farm_pubkey: farm.pubkey.clone(), + }, + ) + .expect("claim member"); + let _ = farm_member_claim::create( + exec, + &IFarmMemberClaimFields { + member_pubkey: "x".repeat(64), + farm_pubkey: farm.pubkey.clone(), + }, + ) + .expect("claim member no profile"); + + let _ = nostr_profile::create( + exec, + &INostrProfileFields { + public_key: farm.pubkey.clone(), + profile_type: "farm".to_string(), + name: "farm profile".to_string(), + display_name: None, + about: None, + website: None, + picture: None, + banner: None, + nip05: None, + lud06: None, + lud16: None, + }, + ) + .expect("farm profile"); + let _ = nostr_profile::create( + exec, + &INostrProfileFields { + public_key: "m".repeat(64), + profile_type: "legacy".to_string(), + name: "member profile".to_string(), + display_name: Some("member".to_string()), + about: Some("about".to_string()), + website: Some("https://member.example.com".to_string()), + picture: Some("https://member.example.com/p.png".to_string()), + banner: Some("https://member.example.com/b.png".to_string()), + nip05: Some("member@example.com".to_string()), + lud06: Some("lud06".to_string()), + lud16: Some("lud16".to_string()), + }, + ) + .expect("member profile"); + + (farm, plot_primary, plot_secondary) + } + + #[test] + fn emit_paths_cover_private_and_public_helpers() { + let exec = SqliteExecutor::open_memory().expect("db"); + let (farm_row, plot_primary, plot_secondary) = seed(&exec); + + let by_id = resolve_farm( + &exec, + &RadrootsReplicaFarmSelector { + id: Some(farm_row.id.clone()), + d_tag: None, + pubkey: None, + }, + ) + .expect("resolve by id"); + assert_eq!(by_id.id, farm_row.id); + + assert!( + resolve_farm( + &exec, + &RadrootsReplicaFarmSelector { + id: Some("00000000-0000-0000-0000-000000000000".to_string()), + d_tag: None, + pubkey: None, + }, + ) + .is_err() + ); + assert!( + resolve_farm( + &exec, + &RadrootsReplicaFarmSelector { + id: None, + d_tag: None, + pubkey: None, + }, + ) + .is_err() + ); + + let _ = farm::create( + &exec, + &IFarmFields { + d_tag: farm_row.d_tag.clone(), + pubkey: farm_row.pubkey.clone(), + name: "duplicate".to_string(), + about: None, + website: None, + picture: None, + banner: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, + }, + ) + .expect("duplicate farm"); + assert!( + resolve_farm( + &exec, + &RadrootsReplicaFarmSelector { + id: None, + d_tag: Some(farm_row.d_tag.clone()), + pubkey: Some(farm_row.pubkey.clone()), + }, + ) + .is_err() + ); + + let tags = collect_farm_tags(&exec, &farm_row.id).expect("farm tags"); + assert_eq!(tags, vec!["coffee".to_string()]); + let plot_tags = collect_plot_tags(&exec, &plot_primary.id).expect("plot tags"); + assert_eq!(plot_tags, vec!["orchard".to_string()]); + + let members = load_farm_members(&exec, &farm_row.id).expect("members"); + assert_eq!(role_pubkeys(&members, ROLE_MEMBER).len(), 2); + assert_eq!(role_pubkeys(&members, ROLE_OWNER).len(), 1); + assert_eq!(role_pubkeys(&members, ROLE_WORKER).len(), 1); + let plots = load_plots(&exec, &farm_row.id).expect("plots"); + assert_eq!(sorted_plot_ids(&plots).len(), 2); + + let farm_location = load_farm_location(&exec, &farm_row).expect("farm location"); + assert!(farm_location.is_some()); + let plot_location_primary = load_plot_location(&exec, &plot_primary).expect("plot primary"); + assert!(plot_location_primary.is_some()); + let plot_location_secondary = + load_plot_location(&exec, &plot_secondary).expect("plot secondary"); + assert!(plot_location_secondary.is_some()); + + assert!( + load_relation_by_role(&exec, &farm_row.id, "primary", RelationType::Farm) + .expect("farm primary") + .is_some() + ); + assert!( + load_relation_by_role(&exec, &farm_row.id, "", RelationType::Farm) + .expect("farm fallback") + .is_some() + ); + assert!( + load_relation_by_role(&exec, &plot_secondary.id, "", RelationType::Plot) + .expect("plot fallback") + .is_some() + ); + + let mut farm_rel = + farm_gcs_location::find_many(&exec, &IFarmGcsLocationFindMany { filter: None }) + .expect("farm rels") + .results; + let mut plot_rel = + plot_gcs_location::find_many(&exec, &IPlotGcsLocationFindMany { filter: None }) + .expect("plot rels") + .results; + let farm_row_role = RelationRow::Farm(farm_rel.remove(0)).role().to_string(); + let plot_row_role = RelationRow::Plot(plot_rel.remove(0)).role().to_string(); + let _ = farm_row_role; + let _ = plot_row_role; + assert_eq!(location_role_rank(ROLE_PRIMARY), 0); + assert_eq!(location_role_rank("secondary"), 1); + + let point_valid = parse_point("{\"type\":\"Point\",\"coordinates\":[1.0,2.0]}", 3.0, 4.0); + assert_eq!(point_valid.coordinates, [1.0, 2.0]); + let point_invalid = parse_point("{", 3.0, 4.0); + assert_eq!(point_invalid.coordinates, [4.0, 3.0]); + let point_empty = parse_point("", 3.0, 4.0); + assert_eq!(point_empty.coordinates, [4.0, 3.0]); + + let polygon_valid = parse_polygon( + "{\"type\":\"Polygon\",\"coordinates\":[[[1.0,2.0],[1.1,2.1],[1.0,2.0]]]}", + 3.0, + 4.0, + ); + assert!(!polygon_valid.coordinates[0].is_empty()); + let polygon_empty_outer = + parse_polygon("{\"type\":\"Polygon\",\"coordinates\":[]}", 3.0, 4.0); + assert!(!polygon_empty_outer.coordinates[0].is_empty()); + let polygon_empty_inner = + parse_polygon("{\"type\":\"Polygon\",\"coordinates\":[[]]}", 3.0, 4.0); + assert!(!polygon_empty_inner.coordinates[0].is_empty()); + let polygon_invalid = parse_polygon("{", 3.0, 4.0); + assert!(!polygon_invalid.coordinates[0].is_empty()); + let polygon_blank = parse_polygon("", 3.0, 4.0); + assert!(!polygon_blank.coordinates[0].is_empty()); + + assert!( + load_profile(&exec, &farm_row.pubkey) + .expect("farm profile") + .is_some() + ); + assert!( + load_profile(&exec, &"z".repeat(64)) + .expect("missing profile") + .is_none() + ); + + let profile_event_farm = profile_event( + &farm_row.pubkey, + radroots_replica_db_schema::nostr_profile::NostrProfile { + id: "00000000-0000-0000-0000-000000000001".to_string(), + created_at: "2024-01-01T00:00:00.000Z".to_string(), + updated_at: "2024-01-01T00:00:00.000Z".to_string(), + public_key: farm_row.pubkey.clone(), + profile_type: "farm".to_string(), + name: "farm".to_string(), + display_name: None, + about: None, + website: None, + picture: None, + banner: None, + nip05: None, + lud06: None, + lud16: None, + }, + ) + .expect("profile farm"); + assert!(!profile_event_farm.tags.is_empty()); + let profile_event_unknown = profile_event( + &"m".repeat(64), + radroots_replica_db_schema::nostr_profile::NostrProfile { + id: "00000000-0000-0000-0000-000000000002".to_string(), + created_at: "2024-01-01T00:00:00.000Z".to_string(), + updated_at: "2024-01-01T00:00:00.000Z".to_string(), + public_key: "m".repeat(64), + profile_type: "legacy".to_string(), + name: "legacy".to_string(), + display_name: None, + about: None, + website: None, + picture: None, + banner: None, + nip05: None, + lud06: None, + lud16: None, + }, + ) + .expect("profile legacy"); + assert!(profile_event_unknown.tags.is_empty()); + + let profile_content = serialize_profile_content(&RadrootsProfile { + name: "name".to_string(), + display_name: Some("display".to_string()), + nip05: Some("nip05".to_string()), + about: Some("about".to_string()), + website: Some("website".to_string()), + picture: Some("picture".to_string()), + banner: Some("banner".to_string()), + lud06: Some("lud06".to_string()), + lud16: Some("lud16".to_string()), + bot: None, + }) + .expect("serialize profile"); + assert!(profile_content.contains("\"name\":\"name\"")); + + let member_pubkeys = collect_member_pubkeys(&exec, &farm_row.id).expect("member pubkeys"); + assert!(!member_pubkeys.is_empty()); + let profile_pubkeys = collect_profile_pubkeys(&exec, &farm_row).expect("profile pubkeys"); + assert!(!profile_pubkeys.is_empty()); + let claims = load_member_claims(&exec, &farm_row.pubkey).expect("claims"); + assert!(!claims.is_empty()); + let member_claims = + load_member_claims_for_member(&exec, &"m".repeat(64)).expect("claims by member"); + assert!(!member_claims.is_empty()); + + let profile_events = radroots_replica_profile_events(&exec, &farm_row).expect("profiles"); + assert!(!profile_events.is_empty()); + let farm_event = radroots_replica_farm_event(&exec, &farm_row).expect("farm event"); + assert_eq!(farm_event.kind, KIND_FARM); + let plot_events = radroots_replica_plot_events(&exec, &farm_row).expect("plot events"); + assert_eq!(plot_events.len(), 2); + let list_sets = radroots_replica_list_set_events(&exec, &farm_row).expect("list sets"); + assert_eq!(list_sets.len(), 4); + let membership_claims = + radroots_replica_membership_claim_events(&exec, &farm_row.pubkey).expect("membership"); + assert!(!membership_claims.is_empty()); + let bundle = radroots_replica_sync_all_with_options( + &exec, + &RadrootsReplicaFarmSelector { + id: Some(farm_row.id.clone()), + d_tag: None, + pubkey: None, + }, + Some(&RadrootsReplicaSyncOptions { + include_profiles: Some(true), + include_list_sets: Some(true), + include_membership_claims: Some(true), + }), + ) + .expect("sync all"); + assert!(!bundle.events.is_empty()); + + let _ = exec.exec("PRAGMA foreign_keys = OFF", "[]"); + let _ = plot_gcs_location::create( + &exec, + &IPlotGcsLocationFields { + plot_id: plot_secondary.id.clone(), + gcs_location_id: "00000000-0000-0000-0000-000000000000".to_string(), + role: "".to_string(), + }, + ); + assert!(load_relation_by_role(&exec, &plot_secondary.id, "", RelationType::Plot).is_err()); + + let by_pair = farm::find_many( + &exec, + &IFarmFindMany { + filter: Some(IFarmFieldsFilter { + id: None, + created_at: None, + updated_at: None, + d_tag: Some("AAAAAAAAAAAAAAAAAAAAAA".to_string()), + pubkey: Some("f".repeat(64)), + name: None, + about: None, + website: None, + picture: None, + banner: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, + }), + }, + ) + .expect("by pair"); + assert!(!by_pair.results.is_empty()); + + let plots_lookup = plot::find_many( + &exec, + &IPlotFindMany { + filter: Some(IPlotFieldsFilter { + id: None, + created_at: None, + updated_at: None, + d_tag: None, + farm_id: Some(farm_row.id), + name: None, + about: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, + }), + }, + ) + .expect("plots lookup"); + assert_eq!(plots_lookup.results.len(), 2); + } +} diff --git a/crates/replica-sync/src/error.rs b/crates/replica-sync/src/error.rs @@ -0,0 +1,91 @@ +#[cfg(not(feature = "std"))] +use alloc::string::{String, ToString}; + +use core::fmt; + +use radroots_events_codec::error::{EventEncodeError, EventParseError}; +use radroots_sql_core::error::SqlError; +use radroots_types::types::IError; + +#[derive(Debug)] +pub enum RadrootsReplicaEventsError { + Sql(IError<SqlError>), + Encode(EventEncodeError), + Parse(EventParseError), + InvalidSelector(String), + InvalidData(String), +} + +impl fmt::Display for RadrootsReplicaEventsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Sql(err) => write!(f, "tangle_events.sql: {}", err.err.to_string()), + Self::Encode(err) => write!(f, "tangle_events.encode: {err}"), + Self::Parse(err) => write!(f, "tangle_events.parse: {err}"), + Self::InvalidSelector(msg) => write!(f, "tangle_events.selector: {msg}"), + Self::InvalidData(msg) => write!(f, "tangle_events.data: {msg}"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RadrootsReplicaEventsError {} + +impl From<IError<SqlError>> for RadrootsReplicaEventsError { + fn from(err: IError<SqlError>) -> Self { + Self::Sql(err) + } +} + +impl From<EventEncodeError> for RadrootsReplicaEventsError { + fn from(err: EventEncodeError) -> Self { + Self::Encode(err) + } +} + +impl From<EventParseError> for RadrootsReplicaEventsError { + fn from(err: EventParseError) -> Self { + Self::Parse(err) + } +} + +#[cfg(test)] +mod tests { + use super::RadrootsReplicaEventsError; + use radroots_events_codec::error::{EventEncodeError, EventParseError}; + use radroots_sql_core::error::SqlError; + use radroots_types::types::IError; + + #[test] + fn display_formats_all_error_variants() { + let sql_err = RadrootsReplicaEventsError::Sql(IError::from(SqlError::Internal)); + assert!(sql_err.to_string().contains("tangle_events.sql")); + + let encode_err = RadrootsReplicaEventsError::Encode(EventEncodeError::InvalidField("name")); + assert!(encode_err.to_string().contains("tangle_events.encode")); + + let parse_err = RadrootsReplicaEventsError::Parse(EventParseError::InvalidTag("d")); + assert!(parse_err.to_string().contains("tangle_events.parse")); + + let selector_err = + RadrootsReplicaEventsError::InvalidSelector("selector missing".to_string()); + assert!(selector_err.to_string().contains("tangle_events.selector")); + + let data_err = RadrootsReplicaEventsError::InvalidData("bad data".to_string()); + assert!(data_err.to_string().contains("tangle_events.data")); + } + + #[test] + fn from_impls_map_into_expected_variants() { + let sql_from: RadrootsReplicaEventsError = IError::from(SqlError::Internal).into(); + assert!(matches!(sql_from, RadrootsReplicaEventsError::Sql(_))); + + let encode_from: RadrootsReplicaEventsError = EventEncodeError::Json.into(); + assert!(matches!(encode_from, RadrootsReplicaEventsError::Encode(_))); + + let parse_number_err = "invalid".parse::<u32>().expect_err("parse int should fail"); + let parse_from: RadrootsReplicaEventsError = + EventParseError::InvalidNumber("k", parse_number_err).into(); + assert!(matches!(parse_from, RadrootsReplicaEventsError::Parse(_))); + } +} diff --git a/crates/replica-sync/src/event_state.rs b/crates/replica-sync/src/event_state.rs @@ -0,0 +1,74 @@ +#[cfg(not(feature = "std"))] +use alloc::format; +#[cfg(not(feature = "std"))] +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; +#[cfg(feature = "std")] +use std::{string::String, vec::Vec}; + +use serde_json::Value; +use sha2::{Digest, Sha256}; + +use crate::error::RadrootsReplicaEventsError; + +pub fn event_state_key(kind: u32, pubkey: &str, d_tag: &str) -> String { + format!("{kind}:{pubkey}:{d_tag}") +} + +pub fn event_content_hash( + content: &str, + tags: &[Vec<String>], +) -> Result<String, RadrootsReplicaEventsError> { + let tags_json = Value::Array( + tags.iter() + .map(|tag| Value::Array(tag.iter().cloned().map(Value::String).collect())) + .collect(), + ) + .to_string(); + let mut hasher = Sha256::new(); + hasher.update(content.as_bytes()); + hasher.update(tags_json.as_bytes()); + Ok(hex::encode(hasher.finalize())) +} + +pub fn tag_value<'a>(tags: &'a [Vec<String>], key: &str) -> Option<&'a str> { + tags.iter() + .find(|tag| tag.get(0).map(|v| v.as_str()) == Some(key)) + .and_then(|tag| tag.get(1)) + .map(|value| value.as_str()) +} + +#[cfg(test)] +mod tests { + use super::{event_content_hash, event_state_key, tag_value}; + + #[test] + fn event_state_key_formats_consistently() { + let key = event_state_key(30000, "author", "d-tag"); + assert_eq!(key, "30000:author:d-tag"); + } + + #[test] + fn event_content_hash_is_stable_for_same_inputs() { + let tags = vec![vec!["d".to_string(), "tag".to_string()]]; + let first = event_content_hash("content", &tags).expect("hash first"); + let second = event_content_hash("content", &tags).expect("hash second"); + assert_eq!(first, second); + assert_eq!(first.len(), 64); + } + + #[test] + fn tag_value_finds_and_misses_keys() { + let tags = vec![ + vec!["p".to_string(), "member".to_string()], + vec!["d".to_string(), "farm".to_string()], + vec!["x".to_string()], + ]; + assert_eq!(tag_value(&tags, "p"), Some("member")); + assert_eq!(tag_value(&tags, "d"), Some("farm")); + assert_eq!(tag_value(&tags, "x"), None); + assert_eq!(tag_value(&tags, "missing"), None); + } +} diff --git a/crates/tangle-events/src/geo.rs b/crates/replica-sync/src/geo.rs diff --git a/crates/replica-sync/src/ingest.rs b/crates/replica-sync/src/ingest.rs @@ -0,0 +1,1957 @@ +#[cfg(not(feature = "std"))] +use alloc::format; +#[cfg(not(feature = "std"))] +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; + +#[cfg(feature = "std")] +use base64::Engine; +#[cfg(feature = "std")] +use base64::engine::general_purpose::URL_SAFE_NO_PAD; + +use radroots_events::RadrootsNostrEvent; +use radroots_events::kinds::{KIND_FARM, KIND_PLOT, KIND_PROFILE, is_nip51_list_set_kind}; +use radroots_events_codec::farm::decode as farm_decode; +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_replica_db::{ + farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, + nostr_event_state, nostr_profile, plot, plot_gcs_location, plot_tag, +}; +use radroots_replica_db_schema::farm::{ + FarmQueryBindValues, IFarmFields, IFarmFieldsFilter, IFarmFieldsPartial, IFarmFindMany, + IFarmUpdate, +}; +use radroots_replica_db_schema::farm_gcs_location::{ + FarmGcsLocationQueryBindValues, IFarmGcsLocationDelete, IFarmGcsLocationFields, + IFarmGcsLocationFieldsFilter, IFarmGcsLocationFindMany, IFarmGcsLocationFindOneArgs, +}; +use radroots_replica_db_schema::farm_member::{ + FarmMemberQueryBindValues, IFarmMemberDelete, IFarmMemberFields, IFarmMemberFieldsFilter, + IFarmMemberFindMany, IFarmMemberFindOneArgs, +}; +use radroots_replica_db_schema::farm_member_claim::{ + FarmMemberClaimQueryBindValues, IFarmMemberClaimDelete, IFarmMemberClaimFields, + IFarmMemberClaimFieldsFilter, IFarmMemberClaimFindMany, IFarmMemberClaimFindOneArgs, +}; +use radroots_replica_db_schema::farm_tag::{ + FarmTagQueryBindValues, IFarmTagDelete, IFarmTagFields, IFarmTagFieldsFilter, IFarmTagFindMany, + IFarmTagFindOneArgs, +}; +use radroots_replica_db_schema::gcs_location::IGcsLocationFields; +use radroots_replica_db_schema::nostr_event_state::{ + INostrEventStateFields, INostrEventStateFieldsPartial, INostrEventStateFindOne, + INostrEventStateFindOneArgs, INostrEventStateUpdate, NostrEventStateQueryBindValues, +}; +use radroots_replica_db_schema::nostr_profile::{ + INostrProfileFields, INostrProfileFieldsPartial, INostrProfileFindOne, + INostrProfileFindOneArgs, INostrProfileUpdate, NostrProfileQueryBindValues, +}; +use radroots_replica_db_schema::plot::{ + IPlotFields, IPlotFieldsFilter, IPlotFieldsPartial, IPlotFindMany, IPlotUpdate, + PlotQueryBindValues, +}; +use radroots_replica_db_schema::plot_gcs_location::{ + IPlotGcsLocationDelete, IPlotGcsLocationFields, IPlotGcsLocationFieldsFilter, + IPlotGcsLocationFindMany, IPlotGcsLocationFindOneArgs, PlotGcsLocationQueryBindValues, +}; +use radroots_replica_db_schema::plot_tag::{ + IPlotTagDelete, IPlotTagFields, IPlotTagFieldsFilter, IPlotTagFindMany, IPlotTagFindOneArgs, + PlotTagQueryBindValues, +}; +use serde_json::Value; + +use crate::error::RadrootsReplicaEventsError; +use crate::event_state::{event_content_hash, event_state_key}; +const ROLE_PRIMARY: &str = "primary"; +const ROLE_MEMBER: &str = "member"; +const ROLE_OWNER: &str = "owner"; +const ROLE_WORKER: &str = "worker"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsReplicaIngestOutcome { + Applied, + Skipped, +} + +pub trait RadrootsReplicaIdFactory { + fn new_d_tag(&self) -> String; +} + +#[cfg(feature = "std")] +pub struct RadrootsReplicaDefaultIdFactory; + +#[cfg(feature = "std")] +impl RadrootsReplicaIdFactory for RadrootsReplicaDefaultIdFactory { + fn new_d_tag(&self) -> String { + let uuid = uuid::Uuid::now_v7(); + let bytes = uuid.as_bytes(); + URL_SAFE_NO_PAD.encode(bytes) + } +} + +#[cfg(feature = "std")] +pub fn radroots_replica_ingest_event<E: SqlExecutor>( + exec: &E, + event: &RadrootsNostrEvent, +) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { + radroots_replica_ingest_event_with_factory(exec, event, &RadrootsReplicaDefaultIdFactory) +} + +pub fn radroots_replica_ingest_event_with_factory<E: SqlExecutor, F: RadrootsReplicaIdFactory>( + exec: &E, + event: &RadrootsNostrEvent, + factory: &F, +) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { + exec.begin() + .map_err(|e| RadrootsReplicaEventsError::from(radroots_types::types::IError::from(e)))?; + + let outcome = match ingest_event_inner(exec, event, factory) { + Ok(outcome) => { + exec.commit().map_err(|e| { + RadrootsReplicaEventsError::from(radroots_types::types::IError::from(e)) + })?; + Ok(outcome) + } + Err(err) => { + let _ = exec.rollback(); + Err(err) + } + }; + + outcome +} + +fn ingest_event_inner<E: SqlExecutor, F: RadrootsReplicaIdFactory>( + exec: &E, + event: &RadrootsNostrEvent, + factory: &F, +) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { + match event.kind { + KIND_PROFILE => ingest_profile_event(exec, event), + KIND_FARM => ingest_farm_event(exec, event, factory), + KIND_PLOT => ingest_plot_event(exec, event, factory), + kind if is_nip51_list_set_kind(kind) => ingest_list_set_event(exec, event), + _ => Err(RadrootsReplicaEventsError::InvalidData(format!( + "unsupported kind {}", + event.kind + ))), + } +} + +fn ingest_profile_event<E: SqlExecutor>( + exec: &E, + event: &RadrootsNostrEvent, +) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { + let metadata_result = profile_decode::metadata_from_event( + event.id.clone(), + event.author.clone(), + event.created_at, + event.kind, + event.content.clone(), + event.tags.clone(), + ); + let metadata = metadata_result?; + let profile_type = metadata.profile_type.ok_or_else(|| { + RadrootsReplicaEventsError::InvalidData("profile_type required".to_string()) + })?; + + let d_tag = "".to_string(); + let decision = event_state_decision(exec, event, &d_tag)?; + if !decision.apply { + return Ok(RadrootsReplicaIngestOutcome::Skipped); + } + + let profile_type = match profile_type { + radroots_events::profile::RadrootsProfileType::Individual => "individual", + radroots_events::profile::RadrootsProfileType::Farm => "farm", + radroots_events::profile::RadrootsProfileType::Coop => "coop", + radroots_events::profile::RadrootsProfileType::Any => "any", + radroots_events::profile::RadrootsProfileType::Radrootsd => "radrootsd", + }; + + let existing_result = nostr_profile::find_one( + exec, + &INostrProfileFindOne::On(INostrProfileFindOneArgs { + on: NostrProfileQueryBindValues::PublicKey { + public_key: metadata.author.clone(), + }, + }), + ); + let existing = existing_result?.result; + + match existing { + Some(profile) => { + let fields = INostrProfileFieldsPartial { + public_key: None, + profile_type: Some(Value::from(profile_type)), + name: Some(Value::from(metadata.profile.name)), + display_name: to_value_opt(metadata.profile.display_name), + about: to_value_opt(metadata.profile.about), + website: to_value_opt(metadata.profile.website), + picture: to_value_opt(metadata.profile.picture), + banner: to_value_opt(metadata.profile.banner), + nip05: to_value_opt(metadata.profile.nip05), + lud06: to_value_opt(metadata.profile.lud06), + lud16: to_value_opt(metadata.profile.lud16), + }; + let update_result = nostr_profile::update( + exec, + &INostrProfileUpdate { + on: NostrProfileQueryBindValues::Id { id: profile.id }, + fields, + }, + ); + let _updated = update_result?; + } + None => { + let fields = INostrProfileFields { + public_key: metadata.author.clone(), + profile_type: profile_type.to_string(), + name: metadata.profile.name, + display_name: metadata.profile.display_name, + about: metadata.profile.about, + website: metadata.profile.website, + picture: metadata.profile.picture, + banner: metadata.profile.banner, + nip05: metadata.profile.nip05, + lud06: metadata.profile.lud06, + lud16: metadata.profile.lud16, + }; + let _ = nostr_profile::create(exec, &fields)?; + } + } + + radroots_replica_ingest_event_state(exec, event, &d_tag, &decision.content_hash)?; + Ok(RadrootsReplicaIngestOutcome::Applied) +} + +fn ingest_farm_event<E: SqlExecutor, F: RadrootsReplicaIdFactory>( + exec: &E, + event: &RadrootsNostrEvent, + factory: &F, +) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { + let farm = farm_decode::farm_from_event(event.kind, &event.tags, &event.content)?; + let decision = event_state_decision(exec, event, &farm.d_tag)?; + if !decision.apply { + return Ok(RadrootsReplicaIngestOutcome::Skipped); + } + + let filter = IFarmFieldsFilter { + id: None, + created_at: None, + updated_at: None, + d_tag: Some(farm.d_tag.clone()), + pubkey: Some(event.author.clone()), + name: None, + about: None, + website: None, + picture: None, + banner: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, + }; + let existing_result = farm::find_many( + exec, + &IFarmFindMany { + filter: Some(filter), + }, + ); + let existing = existing_result?; + let location = farm.location.clone(); + let (location_primary, location_city, location_region, location_country) = + unpack_farm_location_strings(location.as_ref()); + let farm_id = if let Some(row) = existing.results.get(0) { + let fields = IFarmFieldsPartial { + d_tag: Some(Value::from(farm.d_tag.clone())), + pubkey: Some(Value::from(event.author.clone())), + name: Some(Value::from(farm.name.clone())), + about: to_value_opt(farm.about.clone()), + website: to_value_opt(farm.website.clone()), + picture: to_value_opt(farm.picture.clone()), + banner: to_value_opt(farm.banner.clone()), + location_primary: to_value_opt(location_primary), + location_city: to_value_opt(location_city), + location_region: to_value_opt(location_region), + location_country: to_value_opt(location_country), + }; + let update_result = farm::update( + exec, + &IFarmUpdate { + on: FarmQueryBindValues::Id { id: row.id.clone() }, + fields, + }, + ); + let _updated = update_result?; + row.id.clone() + } else { + let fields = IFarmFields { + d_tag: farm.d_tag.clone(), + pubkey: event.author.clone(), + name: farm.name.clone(), + about: farm.about.clone(), + website: farm.website.clone(), + picture: farm.picture.clone(), + banner: farm.banner.clone(), + location_primary, + location_city, + location_region, + location_country, + }; + farm::create(exec, &fields)?.result.id + }; + + upsert_farm_tags(exec, &farm_id, farm.tags)?; + upsert_farm_location(exec, &farm_id, location, factory)?; + + radroots_replica_ingest_event_state(exec, event, &farm.d_tag, &decision.content_hash)?; + Ok(RadrootsReplicaIngestOutcome::Applied) +} + +fn ingest_plot_event<E: SqlExecutor, F: RadrootsReplicaIdFactory>( + exec: &E, + event: &RadrootsNostrEvent, + factory: &F, +) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { + let plot = plot_decode::plot_from_event(event.kind, &event.tags, &event.content)?; + let decision = event_state_decision(exec, event, &plot.d_tag)?; + if !decision.apply { + return Ok(RadrootsReplicaIngestOutcome::Skipped); + } + + let farm = find_farm_by_ref(exec, &plot.farm.pubkey, &plot.farm.d_tag)?; + let filter = IPlotFieldsFilter { + id: None, + created_at: None, + updated_at: None, + d_tag: Some(plot.d_tag.clone()), + farm_id: Some(farm.id.clone()), + name: None, + about: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, + }; + let existing_result = plot::find_many( + exec, + &IPlotFindMany { + filter: Some(filter), + }, + ); + let existing = existing_result?; + let location = plot.location.clone(); + let (location_primary, location_city, location_region, location_country) = + unpack_plot_location_strings(location.as_ref()); + let plot_id = if let Some(row) = existing.results.get(0) { + let fields = IPlotFieldsPartial { + d_tag: Some(Value::from(plot.d_tag.clone())), + farm_id: Some(Value::from(farm.id.clone())), + name: Some(Value::from(plot.name.clone())), + about: to_value_opt(plot.about.clone()), + location_primary: to_value_opt(location_primary), + location_city: to_value_opt(location_city), + location_region: to_value_opt(location_region), + location_country: to_value_opt(location_country), + }; + let update_result = plot::update( + exec, + &IPlotUpdate { + on: PlotQueryBindValues::Id { id: row.id.clone() }, + fields, + }, + ); + let _updated = update_result?; + row.id.clone() + } else { + let fields = IPlotFields { + d_tag: plot.d_tag.clone(), + farm_id: farm.id.clone(), + name: plot.name.clone(), + about: plot.about.clone(), + location_primary, + location_city, + location_region, + location_country, + }; + plot::create(exec, &fields)?.result.id + }; + + upsert_plot_tags(exec, &plot_id, plot.tags)?; + upsert_plot_location(exec, &plot_id, location, factory)?; + + radroots_replica_ingest_event_state(exec, event, &plot.d_tag, &decision.content_hash)?; + Ok(RadrootsReplicaIngestOutcome::Applied) +} + +fn ingest_list_set_event<E: SqlExecutor>( + exec: &E, + event: &RadrootsNostrEvent, +) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { + if event.kind != radroots_events::kinds::KIND_LIST_SET_GENERIC { + return Ok(RadrootsReplicaIngestOutcome::Skipped); + } + let list_set = + list_set_decode::list_set_from_tags(event.kind, event.content.clone(), &event.tags)?; + + if list_set.title.is_some() || list_set.description.is_some() || list_set.image.is_some() { + return Err(RadrootsReplicaEventsError::InvalidData( + "domain:farm list sets must omit metadata".to_string(), + )); + } + if !list_set.content.is_empty() { + return Err(RadrootsReplicaEventsError::InvalidData( + "domain:farm list sets must not include content".to_string(), + )); + } + + let d_tag = list_set.d_tag.clone(); + let decision = event_state_decision(exec, event, &d_tag)?; + if !decision.apply { + return Ok(RadrootsReplicaIngestOutcome::Skipped); + } + + if d_tag == "member_of.farms" { + ensure_list_set_entries_tag(&list_set, "p", "member_of.farms")?; + upsert_member_claims(exec, &event.author, &list_set)?; + radroots_replica_ingest_event_state(exec, event, &d_tag, &decision.content_hash)?; + return Ok(RadrootsReplicaIngestOutcome::Applied); + } + + if let Some((farm_d_tag, role)) = parse_farm_list_set_d_tag(&d_tag) { + if role == ListSetRole::Plots { + ensure_list_set_entries_tag(&list_set, "a", "farm plots")?; + radroots_replica_ingest_event_state(exec, event, &d_tag, &decision.content_hash)?; + return Ok(RadrootsReplicaIngestOutcome::Applied); + } + ensure_list_set_entries_tag(&list_set, "p", "farm members")?; + let farm = find_farm_by_ref(exec, &event.author, &farm_d_tag)?; + upsert_farm_members(exec, &farm.id, role, &list_set)?; + radroots_replica_ingest_event_state(exec, event, &d_tag, &decision.content_hash)?; + return Ok(RadrootsReplicaIngestOutcome::Applied); + } + + Err(RadrootsReplicaEventsError::InvalidData( + "unsupported list set d_tag".to_string(), + )) +} + +pub fn radroots_replica_ingest_event_state<E: SqlExecutor>( + exec: &E, + event: &RadrootsNostrEvent, + d_tag: &str, + content_hash: &str, +) -> Result<(), RadrootsReplicaEventsError> { + let key = event_state_key(event.kind, &event.author, d_tag); + let existing_result = nostr_event_state::find_one( + exec, + &INostrEventStateFindOne::On(INostrEventStateFindOneArgs { + on: NostrEventStateQueryBindValues::Key { key: key.clone() }, + }), + ); + let existing = existing_result?.result; + + match existing { + Some(state) => { + let fields = INostrEventStateFieldsPartial { + key: None, + kind: None, + pubkey: None, + d_tag: None, + last_event_id: Some(Value::from(event.id.clone())), + last_created_at: Some(Value::from(event.created_at)), + content_hash: Some(Value::from(content_hash.to_string())), + }; + let update_result = nostr_event_state::update( + exec, + &INostrEventStateUpdate { + on: NostrEventStateQueryBindValues::Id { id: state.id }, + fields, + }, + ); + let _updated = update_result?; + } + None => { + let fields = INostrEventStateFields { + key, + kind: event.kind, + pubkey: event.author.clone(), + d_tag: d_tag.to_string(), + last_event_id: event.id.clone(), + last_created_at: event.created_at, + content_hash: content_hash.to_string(), + }; + let _ = nostr_event_state::create(exec, &fields)?; + } + } + + Ok(()) +} + +fn event_state_decision<E: SqlExecutor>( + exec: &E, + event: &RadrootsNostrEvent, + d_tag: &str, +) -> Result<EventStateDecision, RadrootsReplicaEventsError> { + let key = event_state_key(event.kind, &event.author, d_tag); + let content_hash = event_content_hash(&event.content, &event.tags)?; + let existing_result = nostr_event_state::find_one( + exec, + &INostrEventStateFindOne::On(INostrEventStateFindOneArgs { + on: NostrEventStateQueryBindValues::Key { key }, + }), + ); + let existing = existing_result?.result; + + if let Some(state) = existing { + if event.created_at < state.last_created_at { + return Ok(EventStateDecision { + apply: false, + content_hash, + }); + } + if event.created_at == state.last_created_at && content_hash == state.content_hash { + return Ok(EventStateDecision { + apply: false, + content_hash, + }); + } + } + + Ok(EventStateDecision { + apply: true, + content_hash, + }) +} + +fn find_farm_by_ref<E: SqlExecutor>( + exec: &E, + pubkey: &str, + d_tag: &str, +) -> Result<radroots_replica_db_schema::farm::Farm, RadrootsReplicaEventsError> { + let filter = IFarmFieldsFilter { + id: None, + created_at: None, + updated_at: None, + d_tag: Some(d_tag.to_string()), + pubkey: Some(pubkey.to_string()), + name: None, + about: None, + website: None, + picture: None, + banner: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, + }; + let result_query = farm::find_many( + exec, + &IFarmFindMany { + filter: Some(filter), + }, + ); + let result = result_query?; + result + .results + .into_iter() + .next() + .ok_or_else(|| RadrootsReplicaEventsError::InvalidData("farm not found".to_string())) +} + +fn upsert_farm_tags<E: SqlExecutor>( + exec: &E, + farm_id: &str, + tags: Option<Vec<String>>, +) -> Result<(), RadrootsReplicaEventsError> { + let existing_query = farm_tag::find_many( + exec, + &IFarmTagFindMany { + filter: Some(IFarmTagFieldsFilter { + id: None, + created_at: None, + updated_at: None, + farm_id: Some(farm_id.to_string()), + tag: None, + }), + }, + ); + let existing = existing_query?; + for row in existing.results { + 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(); + tags.sort(); + tags.dedup(); + for tag in tags { + if tag.trim().is_empty() { + continue; + } + let fields = IFarmTagFields { + farm_id: farm_id.to_string(), + tag, + }; + let _ = farm_tag::create(exec, &fields)?; + } + Ok(()) +} + +fn upsert_plot_tags<E: SqlExecutor>( + exec: &E, + plot_id: &str, + tags: Option<Vec<String>>, +) -> Result<(), RadrootsReplicaEventsError> { + let existing_query = plot_tag::find_many( + exec, + &IPlotTagFindMany { + filter: Some(IPlotTagFieldsFilter { + id: None, + created_at: None, + updated_at: None, + plot_id: Some(plot_id.to_string()), + tag: None, + }), + }, + ); + let existing = existing_query?; + for row in existing.results { + 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(); + tags.sort(); + tags.dedup(); + for tag in tags { + if tag.trim().is_empty() { + continue; + } + let fields = IPlotTagFields { + plot_id: plot_id.to_string(), + tag, + }; + let _ = plot_tag::create(exec, &fields)?; + } + Ok(()) +} + +fn upsert_farm_location<E: SqlExecutor, F: RadrootsReplicaIdFactory>( + exec: &E, + farm_id: &str, + location: Option<radroots_events::farm::RadrootsFarmLocation>, + factory: &F, +) -> Result<(), RadrootsReplicaEventsError> { + clear_farm_locations(exec, farm_id)?; + if let Some(location) = location { + let gcs_id = create_gcs_location(exec, location.gcs, factory)?; + let fields = IFarmGcsLocationFields { + farm_id: farm_id.to_string(), + gcs_location_id: gcs_id, + role: ROLE_PRIMARY.to_string(), + }; + let _ = farm_gcs_location::create(exec, &fields)?; + } + Ok(()) +} + +fn upsert_plot_location<E: SqlExecutor, F: RadrootsReplicaIdFactory>( + exec: &E, + plot_id: &str, + location: Option<radroots_events::plot::RadrootsPlotLocation>, + factory: &F, +) -> Result<(), RadrootsReplicaEventsError> { + clear_plot_locations(exec, plot_id)?; + if let Some(location) = location { + let gcs_id = create_gcs_location(exec, location.gcs, factory)?; + let fields = IPlotGcsLocationFields { + plot_id: plot_id.to_string(), + gcs_location_id: gcs_id, + role: ROLE_PRIMARY.to_string(), + }; + let _ = plot_gcs_location::create(exec, &fields)?; + } + Ok(()) +} + +fn clear_farm_locations<E: SqlExecutor>( + exec: &E, + farm_id: &str, +) -> Result<(), RadrootsReplicaEventsError> { + let existing_query = farm_gcs_location::find_many( + exec, + &IFarmGcsLocationFindMany { + filter: Some(IFarmGcsLocationFieldsFilter { + id: None, + created_at: None, + updated_at: None, + farm_id: Some(farm_id.to_string()), + gcs_location_id: None, + role: None, + }), + }, + ); + let existing = existing_query?; + for row in existing.results { + 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(()) +} + +fn clear_plot_locations<E: SqlExecutor>( + exec: &E, + plot_id: &str, +) -> Result<(), RadrootsReplicaEventsError> { + let existing_query = plot_gcs_location::find_many( + exec, + &IPlotGcsLocationFindMany { + filter: Some(IPlotGcsLocationFieldsFilter { + id: None, + created_at: None, + updated_at: None, + plot_id: Some(plot_id.to_string()), + gcs_location_id: None, + role: None, + }), + }, + ); + let existing = existing_query?; + for row in existing.results { + 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(()) +} + +fn create_gcs_location<E: SqlExecutor, F: RadrootsReplicaIdFactory>( + exec: &E, + gcs: radroots_events::farm::RadrootsGcsLocation, + factory: &F, +) -> Result<String, RadrootsReplicaEventsError> { + let d_tag = factory.new_d_tag(); + let point = serde_json::to_string(&gcs.point).map_err(map_gcs_point_serialize_error)?; + let polygon = serde_json::to_string(&gcs.polygon).map_err(map_gcs_polygon_serialize_error)?; + + let fields = IGcsLocationFields { + d_tag, + lat: gcs.lat, + lng: gcs.lng, + geohash: gcs.geohash, + point, + polygon, + accuracy: gcs.accuracy, + altitude: gcs.altitude, + tag_0: gcs.tag_0, + label: gcs.label, + area: gcs.area, + elevation: gcs.elevation, + soil: gcs.soil, + climate: gcs.climate, + gc_id: gcs.gc_id, + gc_name: gcs.gc_name, + gc_admin1_id: gcs.gc_admin1_id, + gc_admin1_name: gcs.gc_admin1_name, + gc_country_id: gcs.gc_country_id, + gc_country_name: gcs.gc_country_name, + }; + let result = gcs_location::create(exec, &fields)?; + Ok(result.result.id) +} + +fn map_gcs_point_serialize_error(_err: serde_json::Error) -> RadrootsReplicaEventsError { + RadrootsReplicaEventsError::InvalidData("gcs.point".to_string()) +} + +fn map_gcs_polygon_serialize_error(_err: serde_json::Error) -> RadrootsReplicaEventsError { + RadrootsReplicaEventsError::InvalidData("gcs.polygon".to_string()) +} + +fn upsert_farm_members<E: SqlExecutor>( + exec: &E, + farm_id: &str, + role: ListSetRole, + list_set: &radroots_events::list_set::RadrootsListSet, +) -> Result<(), RadrootsReplicaEventsError> { + let role_value = match role { + ListSetRole::Members => ROLE_MEMBER, + ListSetRole::Owners => ROLE_OWNER, + ListSetRole::Workers => ROLE_WORKER, + ListSetRole::Plots => return Ok(()), + }; + let existing_query = farm_member::find_many( + exec, + &IFarmMemberFindMany { + filter: Some(IFarmMemberFieldsFilter { + id: None, + created_at: None, + updated_at: None, + farm_id: Some(farm_id.to_string()), + member_pubkey: None, + role: Some(role_value.to_string()), + }), + }, + ); + let existing = existing_query?; + for row in existing.results { + 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 + .entries + .iter() + .filter(|entry| entry.tag == "p") + .filter_map(|entry| entry.values.get(0)) + .map(|value| value.to_string()) + .collect::<Vec<_>>(); + entries.sort(); + entries.dedup(); + + for pubkey in entries { + let fields = IFarmMemberFields { + farm_id: farm_id.to_string(), + member_pubkey: pubkey, + role: role_value.to_string(), + }; + let _ = farm_member::create(exec, &fields)?; + } + Ok(()) +} + +fn upsert_member_claims<E: SqlExecutor>( + exec: &E, + member_pubkey: &str, + list_set: &radroots_events::list_set::RadrootsListSet, +) -> Result<(), RadrootsReplicaEventsError> { + let existing_query = farm_member_claim::find_many( + exec, + &IFarmMemberClaimFindMany { + filter: Some(IFarmMemberClaimFieldsFilter { + id: None, + created_at: None, + updated_at: None, + member_pubkey: Some(member_pubkey.to_string()), + farm_pubkey: None, + }), + }, + ); + let existing = existing_query?; + for row in existing.results { + 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 + .entries + .iter() + .filter(|entry| entry.tag == "p") + .filter_map(|entry| entry.values.get(0)) + .map(|value| value.to_string()) + .collect::<Vec<_>>(); + entries.sort(); + entries.dedup(); + + for farm_pubkey in entries { + let fields = IFarmMemberClaimFields { + member_pubkey: member_pubkey.to_string(), + farm_pubkey, + }; + let _ = farm_member_claim::create(exec, &fields)?; + } + Ok(()) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ListSetRole { + Members, + Owners, + Workers, + Plots, +} + +fn unpack_farm_location_strings( + location: Option<&radroots_events::farm::RadrootsFarmLocation>, +) -> ( + Option<String>, + Option<String>, + Option<String>, + Option<String>, +) { + match location { + Some(location) => ( + location.primary.clone(), + location.city.clone(), + location.region.clone(), + location.country.clone(), + ), + None => (None, None, None, None), + } +} + +fn unpack_plot_location_strings( + location: Option<&radroots_events::plot::RadrootsPlotLocation>, +) -> ( + Option<String>, + Option<String>, + Option<String>, + Option<String>, +) { + match location { + Some(location) => ( + location.primary.clone(), + location.city.clone(), + location.region.clone(), + location.country.clone(), + ), + None => (None, None, None, None), + } +} + +fn ensure_list_set_entries_tag( + list_set: &radroots_events::list_set::RadrootsListSet, + expected: &str, + label: &str, +) -> Result<(), RadrootsReplicaEventsError> { + for entry in list_set.entries.iter() { + if entry.tag != expected { + return Err(RadrootsReplicaEventsError::InvalidData(format!( + "domain:farm list set {label} must only include {expected} tags" + ))); + } + } + Ok(()) +} + +fn parse_farm_list_set_d_tag(d_tag: &str) -> Option<(String, ListSetRole)> { + let mut parts = d_tag.splitn(3, ':'); + if parts.next()? != "farm" { + return None; + } + let farm_d_tag = parts.next()?.to_string(); + let suffix = parts.next()?; + let role = match suffix { + "members" => ListSetRole::Members, + "members.owners" => ListSetRole::Owners, + "members.workers" => ListSetRole::Workers, + "plots" => ListSetRole::Plots, + _ => return None, + }; + Some((farm_d_tag, role)) +} + +fn to_value_opt(value: Option<String>) -> Option<Value> { + Some(match value { + Some(value) => Value::from(value), + None => Value::Null, + }) +} + +struct EventStateDecision { + apply: bool, + content_hash: String, +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + use radroots_events::farm::{ + RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef, RadrootsGcsLocation, + RadrootsGeoJsonPoint, RadrootsGeoJsonPolygon, + }; + use radroots_events::kinds::{KIND_LIST_SET_FOLLOW, KIND_LIST_SET_GENERIC}; + use radroots_events::list::RadrootsListEntry; + use radroots_events::list_set::RadrootsListSet; + use radroots_events::plot::{RadrootsPlot, RadrootsPlotLocation}; + use radroots_events::profile::{ + RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, RadrootsProfileType, + radroots_profile_type_tag_value, + }; + use radroots_events_codec::farm::encode as farm_encode; + use radroots_events_codec::farm::list_sets as farm_list_sets; + use radroots_events_codec::list_set::encode as list_set_encode; + use radroots_events_codec::plot::encode as plot_encode; + use radroots_sql_core::{ExecOutcome, SqlExecutor, SqliteExecutor}; + use radroots_replica_db::{ + farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, + migrations, plot, plot_gcs_location, plot_tag, + }; + use radroots_replica_db_schema::farm::IFarmFields; + use radroots_replica_db_schema::farm_gcs_location::IFarmGcsLocationFields; + use radroots_replica_db_schema::farm_member::IFarmMemberFields; + use radroots_replica_db_schema::farm_member_claim::IFarmMemberClaimFields; + use radroots_replica_db_schema::farm_tag::IFarmTagFields; + use radroots_replica_db_schema::gcs_location::IGcsLocationFields; + use radroots_replica_db_schema::plot::IPlotFields; + use radroots_replica_db_schema::plot_gcs_location::IPlotGcsLocationFields; + use radroots_replica_db_schema::plot_tag::IPlotTagFields; + + struct FixedFactory; + + impl RadrootsReplicaIdFactory for FixedFactory { + fn new_d_tag(&self) -> String { + "AAAAAAAAAAAAAAAAAAAAAZ".to_string() + } + } + + struct TxnExecutor { + begin_err: Option<SqlError>, + commit_err: Option<SqlError>, + rollback_count: Arc<AtomicUsize>, + } + + impl SqlExecutor for TxnExecutor { + fn exec(&self, _sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> { + Err(SqlError::UnsupportedPlatform) + } + + fn query_raw(&self, _sql: &str, _params_json: &str) -> Result<String, SqlError> { + Err(SqlError::UnsupportedPlatform) + } + + fn begin(&self) -> Result<(), SqlError> { + match self.begin_err.clone() { + Some(err) => Err(err), + None => Ok(()), + } + } + + fn commit(&self) -> Result<(), SqlError> { + match self.commit_err.clone() { + Some(err) => Err(err), + None => Ok(()), + } + } + + fn rollback(&self) -> Result<(), SqlError> { + self.rollback_count.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + } + + struct DeleteErrorExecutor<'a> { + inner: &'a SqliteExecutor, + table_name: &'static str, + err: SqlError, + } + + impl SqlExecutor for DeleteErrorExecutor<'_> { + fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { + let normalized = sql.to_ascii_lowercase(); + if normalized.contains("delete from") && normalized.contains(self.table_name) { + return Err(self.err.clone()); + } + self.inner.exec(sql, params_json) + } + + fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> { + self.inner.query_raw(sql, params_json) + } + + fn begin(&self) -> Result<(), SqlError> { + self.inner.begin() + } + + fn commit(&self) -> Result<(), SqlError> { + self.inner.commit() + } + + fn rollback(&self) -> Result<(), SqlError> { + self.inner.rollback() + } + } + + fn sample_gcs(lat: f64, lng: f64, geohash: &str) -> RadrootsGcsLocation { + RadrootsGcsLocation { + lat, + lng, + geohash: geohash.to_string(), + point: RadrootsGeoJsonPoint { + r#type: "Point".to_string(), + coordinates: [lng, lat], + }, + polygon: RadrootsGeoJsonPolygon { + r#type: "Polygon".to_string(), + coordinates: vec![vec![ + [lng, lat], + [lng, lat + 0.001], + [lng - 0.001, lat + 0.001], + [lng, lat], + ]], + }, + accuracy: Some(1.0), + altitude: Some(2.0), + tag_0: Some("tag".to_string()), + label: Some("label".to_string()), + area: Some(3.0), + elevation: Some(4), + soil: Some("soil".to_string()), + climate: Some("climate".to_string()), + gc_id: Some("gc_id".to_string()), + gc_name: Some("gc_name".to_string()), + gc_admin1_id: Some("gc_admin1_id".to_string()), + gc_admin1_name: Some("gc_admin1_name".to_string()), + gc_country_id: Some("gc_country_id".to_string()), + gc_country_name: Some("gc_country_name".to_string()), + } + } + + fn profile_event( + id: u64, + author: &str, + created_at: u32, + profile_type: Option<RadrootsProfileType>, + name: &str, + ) -> RadrootsNostrEvent { + let profile = RadrootsProfile { + name: name.to_string(), + display_name: Some(format!("{name}-display")), + nip05: Some(format!("{name}@example.com")), + about: Some(format!("{name}-about")), + website: Some("https://example.com".to_string()), + picture: Some("https://example.com/p.png".to_string()), + banner: Some("https://example.com/b.png".to_string()), + lud06: Some("lud06".to_string()), + lud16: Some("lud16".to_string()), + bot: None, + }; + let mut tags = Vec::new(); + if let Some(profile_type) = profile_type { + tags.push(vec![ + RADROOTS_PROFILE_TYPE_TAG_KEY.to_string(), + radroots_profile_type_tag_value(profile_type).to_string(), + ]); + } + RadrootsNostrEvent { + id: format!("{id:064x}"), + author: author.to_string(), + created_at, + kind: KIND_PROFILE, + tags, + content: serde_json::to_string(&profile).expect("profile json"), + sig: "f".repeat(128), + } + } + + fn farm_event( + id: u64, + author: &str, + created_at: u32, + d_tag: &str, + name: &str, + location: Option<RadrootsFarmLocation>, + tags: Option<Vec<String>>, + ) -> RadrootsNostrEvent { + let farm = RadrootsFarm { + d_tag: d_tag.to_string(), + name: name.to_string(), + about: Some("about".to_string()), + website: Some("https://farm.example.com".to_string()), + picture: Some("https://farm.example.com/p.png".to_string()), + banner: Some("https://farm.example.com/b.png".to_string()), + location, + tags, + }; + let tags = farm_encode::farm_build_tags(&farm).expect("farm tags"); + RadrootsNostrEvent { + id: format!("{id:064x}"), + author: author.to_string(), + created_at, + kind: KIND_FARM, + tags, + content: serde_json::to_string(&farm).expect("farm json"), + sig: "f".repeat(128), + } + } + + fn plot_event( + id: u64, + author: &str, + created_at: u32, + d_tag: &str, + farm_ref: RadrootsFarmRef, + name: &str, + location: Option<RadrootsPlotLocation>, + tags: Option<Vec<String>>, + ) -> RadrootsNostrEvent { + let plot = RadrootsPlot { + d_tag: d_tag.to_string(), + farm: farm_ref, + name: name.to_string(), + about: Some("plot-about".to_string()), + location, + tags, + }; + let tags = plot_encode::plot_build_tags(&plot).expect("plot tags"); + RadrootsNostrEvent { + id: format!("{id:064x}"), + author: author.to_string(), + created_at, + kind: KIND_PLOT, + tags, + content: serde_json::to_string(&plot).expect("plot json"), + sig: "f".repeat(128), + } + } + + fn list_set_event( + id: u64, + author: &str, + created_at: u32, + kind: u32, + list_set: &RadrootsListSet, + ) -> RadrootsNostrEvent { + let parts = list_set_encode::to_wire_parts_with_kind(list_set, kind).expect("list set"); + RadrootsNostrEvent { + id: format!("{id:064x}"), + author: author.to_string(), + created_at, + kind, + tags: parts.tags, + content: parts.content, + sig: "f".repeat(128), + } + } + + fn seed_rows(exec: &SqliteExecutor) -> (String, String, String, String) { + migrations::run_all_up(exec).expect("migrations"); + let farm_row = farm::create( + exec, + &IFarmFields { + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + pubkey: "f".repeat(64), + name: "farm".to_string(), + about: None, + website: None, + picture: None, + banner: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, + }, + ) + .expect("farm") + .result; + let plot_row = plot::create( + exec, + &IPlotFields { + d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), + farm_id: farm_row.id.clone(), + name: "plot".to_string(), + about: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, + }, + ) + .expect("plot") + .result; + let gcs_row = gcs_location::create( + exec, + &IGcsLocationFields { + d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), + lat: 1.0, + lng: 2.0, + geohash: "s0".to_string(), + point: "{\"type\":\"Point\",\"coordinates\":[2.0,1.0]}".to_string(), + polygon: + "{\"type\":\"Polygon\",\"coordinates\":[[[2.0,1.0],[2.1,1.1],[1.9,1.1],[2.0,1.0]]]}".to_string(), + accuracy: None, + altitude: None, + tag_0: None, + label: None, + area: None, + elevation: None, + soil: None, + climate: None, + gc_id: None, + gc_name: None, + gc_admin1_id: None, + gc_admin1_name: None, + gc_country_id: None, + gc_country_name: None, + }, + ) + .expect("gcs") + .result; + + let _ = farm_tag::create( + exec, + &IFarmTagFields { + farm_id: farm_row.id.clone(), + tag: "alpha".to_string(), + }, + ) + .expect("farm tag"); + let _ = plot_tag::create( + exec, + &IPlotTagFields { + plot_id: plot_row.id.clone(), + tag: "beta".to_string(), + }, + ) + .expect("plot tag"); + let _ = farm_gcs_location::create( + exec, + &IFarmGcsLocationFields { + farm_id: farm_row.id.clone(), + gcs_location_id: gcs_row.id.clone(), + role: "primary".to_string(), + }, + ) + .expect("farm gcs"); + let _ = plot_gcs_location::create( + exec, + &IPlotGcsLocationFields { + plot_id: plot_row.id.clone(), + gcs_location_id: gcs_row.id.clone(), + role: "primary".to_string(), + }, + ) + .expect("plot gcs"); + let _ = farm_member::create( + exec, + &IFarmMemberFields { + farm_id: farm_row.id.clone(), + member_pubkey: "m".repeat(64), + role: "member".to_string(), + }, + ) + .expect("member"); + let _ = farm_member_claim::create( + exec, + &IFarmMemberClaimFields { + member_pubkey: "m".repeat(64), + farm_pubkey: farm_row.pubkey.clone(), + }, + ) + .expect("claim"); + ( + farm_row.id, + farm_row.pubkey, + farm_row.d_tag, + plot_row.d_tag.clone(), + ) + } + + #[test] + fn ingest_transaction_paths_are_covered() { + let begin_executor = TxnExecutor { + begin_err: Some(SqlError::Internal), + commit_err: None, + rollback_count: Arc::new(AtomicUsize::new(0)), + }; + let event = RadrootsNostrEvent { + id: format!("{:064x}", 1u64), + author: "a".repeat(64), + created_at: 1, + kind: KIND_LIST_SET_FOLLOW, + tags: Vec::new(), + content: String::new(), + sig: "f".repeat(128), + }; + let begin_err = + radroots_replica_ingest_event_with_factory(&begin_executor, &event, &FixedFactory) + .expect_err("begin"); + assert!(matches!(begin_err, RadrootsReplicaEventsError::Sql(_))); + assert!(begin_executor.commit().is_ok()); + assert!(matches!( + begin_executor.exec("select 1", "[]").expect_err("exec"), + SqlError::UnsupportedPlatform + )); + assert!(matches!( + begin_executor + .query_raw("select 1", "[]") + .expect_err("query"), + SqlError::UnsupportedPlatform + )); + + let rollback_count = Arc::new(AtomicUsize::new(0)); + let commit_executor = TxnExecutor { + begin_err: None, + commit_err: Some(SqlError::Internal), + rollback_count: rollback_count.clone(), + }; + let commit_err = + radroots_replica_ingest_event_with_factory(&commit_executor, &event, &FixedFactory) + .expect_err("commit"); + assert!(matches!(commit_err, RadrootsReplicaEventsError::Sql(_))); + assert_eq!(rollback_count.load(Ordering::SeqCst), 0); + + let rollback_executor = TxnExecutor { + begin_err: None, + commit_err: None, + rollback_count: Arc::new(AtomicUsize::new(0)), + }; + let unsupported = RadrootsNostrEvent { + id: format!("{:064x}", 2u64), + author: "a".repeat(64), + created_at: 2, + kind: 42, + tags: Vec::new(), + content: String::new(), + sig: "f".repeat(128), + }; + let err = radroots_replica_ingest_event_with_factory( + &rollback_executor, + &unsupported, + &FixedFactory, + ) + .expect_err("rollback"); + assert!(err.to_string().contains("unsupported kind")); + assert_eq!(rollback_executor.rollback_count.load(Ordering::SeqCst), 1); + } + + #[test] + fn ingest_core_paths_cover_helpers_and_decisions() { + let exec = SqliteExecutor::open_memory().expect("db"); + migrations::run_all_up(&exec).expect("migrations"); + + let factory = RadrootsReplicaDefaultIdFactory; + assert_eq!(factory.new_d_tag().len(), 22); + + let profile_pubkey = "p".repeat(64); + let profile = profile_event( + 10, + &profile_pubkey, + 1, + Some(RadrootsProfileType::Individual), + "alice", + ); + let profile_no_type = profile_event(9, &profile_pubkey, 0, None, "alice-none"); + assert!(ingest_profile_event(&exec, &profile_no_type).is_err()); + assert_eq!( + radroots_replica_ingest_event(&exec, &profile).expect("ingest wrapper"), + RadrootsReplicaIngestOutcome::Applied + ); + let profile_update = profile_event( + 11, + &profile_pubkey, + 2, + Some(RadrootsProfileType::Individual), + "alice-2", + ); + assert_eq!( + ingest_profile_event(&exec, &profile_update).expect("profile update"), + RadrootsReplicaIngestOutcome::Applied + ); + let profile_same_time_diff_hash = profile_event( + 12, + &profile_pubkey, + 2, + Some(RadrootsProfileType::Individual), + "alice-3", + ); + let decision = + event_state_decision(&exec, &profile_same_time_diff_hash, "").expect("decision"); + assert!(decision.apply); + + let farm_pubkey = "f".repeat(64); + let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; + let farm = farm_event( + 20, + &farm_pubkey, + 10, + farm_d_tag, + "farm-a", + Some(RadrootsFarmLocation { + primary: Some("primary".to_string()), + city: Some("city".to_string()), + region: Some("region".to_string()), + country: Some("country".to_string()), + gcs: sample_gcs(10.0, 20.0, "s0"), + }), + Some(vec![ + "coffee".to_string(), + "coffee".to_string(), + " ".to_string(), + ]), + ); + assert_eq!( + ingest_farm_event(&exec, &farm, &FixedFactory).expect("farm"), + RadrootsReplicaIngestOutcome::Applied + ); + let farm_update = farm_event( + 21, + &farm_pubkey, + 11, + farm_d_tag, + "farm-b", + None, + Some(vec!["market".to_string()]), + ); + assert_eq!( + ingest_farm_event(&exec, &farm_update, &FixedFactory).expect("farm update"), + RadrootsReplicaIngestOutcome::Applied + ); + + let plot_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ"; + let plot = plot_event( + 30, + &farm_pubkey, + 20, + plot_d_tag, + RadrootsFarmRef { + pubkey: farm_pubkey.clone(), + d_tag: farm_d_tag.to_string(), + }, + "plot-a", + Some(RadrootsPlotLocation { + primary: Some("p".to_string()), + city: Some("c".to_string()), + region: Some("r".to_string()), + country: Some("k".to_string()), + gcs: sample_gcs(11.0, 21.0, "s1"), + }), + Some(vec!["tag".to_string()]), + ); + assert_eq!( + ingest_plot_event(&exec, &plot, &FixedFactory).expect("plot"), + RadrootsReplicaIngestOutcome::Applied + ); + let plot_update = plot_event( + 31, + &farm_pubkey, + 21, + plot_d_tag, + RadrootsFarmRef { + pubkey: farm_pubkey.clone(), + d_tag: farm_d_tag.to_string(), + }, + "plot-b", + None, + Some(vec!["tag2".to_string()]), + ); + assert_eq!( + ingest_plot_event(&exec, &plot_update, &FixedFactory).expect("plot update"), + RadrootsReplicaIngestOutcome::Applied + ); + + let members = farm_list_sets::farm_members_list_set(farm_d_tag, vec!["m".repeat(64)]) + .expect("members"); + let owners = + farm_list_sets::farm_owners_list_set(farm_d_tag, vec!["o".repeat(64)]).expect("owners"); + let workers = farm_list_sets::farm_workers_list_set(farm_d_tag, vec!["w".repeat(64)]) + .expect("workers"); + let plots = farm_list_sets::farm_plots_list_set( + farm_d_tag, + &farm_pubkey, + vec![plot_d_tag.to_string()], + ) + .expect("plots"); + let member_of = + farm_list_sets::member_of_farms_list_set(vec![farm_pubkey.clone()]).expect("member_of"); + + for (idx, list_set) in [members, owners, workers, plots, member_of] + .iter() + .enumerate() + { + let event = list_set_event( + 40 + idx as u64, + if list_set.d_tag == "member_of.farms" { + &profile_pubkey + } else { + &farm_pubkey + }, + 30 + idx as u32, + KIND_LIST_SET_GENERIC, + list_set, + ); + assert_eq!( + ingest_list_set_event(&exec, &event).expect("list set"), + RadrootsReplicaIngestOutcome::Applied + ); + } + + let bad_description = RadrootsListSet { + d_tag: "member_of.farms".to_string(), + content: String::new(), + entries: vec![RadrootsListEntry { + tag: "p".to_string(), + values: vec![farm_pubkey.clone()], + }], + title: None, + description: Some("bad".to_string()), + image: None, + }; + let bad_description_event = list_set_event( + 90, + &profile_pubkey, + 100, + KIND_LIST_SET_GENERIC, + &bad_description, + ); + assert!(ingest_list_set_event(&exec, &bad_description_event).is_err()); + + let bad_image = RadrootsListSet { + d_tag: "member_of.farms".to_string(), + content: String::new(), + entries: vec![RadrootsListEntry { + tag: "p".to_string(), + values: vec![farm_pubkey.clone()], + }], + title: None, + description: None, + image: Some("bad".to_string()), + }; + let bad_image_event = + list_set_event(91, &profile_pubkey, 101, KIND_LIST_SET_GENERIC, &bad_image); + assert!(ingest_list_set_event(&exec, &bad_image_event).is_err()); + + assert!(parse_farm_list_set_d_tag("farm:AAAAAAAAAAAAAAAAAAAAAA:unknown").is_none()); + assert!(parse_farm_list_set_d_tag("farm:AAAAAAAAAAAAAAAAAAAAAA:plots").is_some()); + assert_eq!(to_value_opt(Some("x".to_string())), Some(Value::from("x"))); + assert_eq!(to_value_opt(None), Some(Value::Null)); + let location = RadrootsFarmLocation { + primary: Some("p".to_string()), + city: Some("c".to_string()), + region: Some("r".to_string()), + country: Some("k".to_string()), + gcs: sample_gcs(12.0, 22.0, "s2"), + }; + assert_eq!( + unpack_farm_location_strings(Some(&location)).0, + Some("p".to_string()) + ); + assert_eq!( + unpack_plot_location_strings(Some(&RadrootsPlotLocation { + primary: Some("p".to_string()), + city: None, + region: None, + country: None, + gcs: sample_gcs(13.0, 23.0, "s3"), + })) + .0, + Some("p".to_string()) + ); + assert!(ensure_list_set_entries_tag(&bad_image, "p", "x").is_ok()); + assert!( + ensure_list_set_entries_tag( + &RadrootsListSet { + d_tag: "x".to_string(), + content: String::new(), + entries: vec![RadrootsListEntry { + tag: "a".to_string(), + values: vec!["x".to_string()], + }], + title: None, + description: None, + image: None, + }, + "p", + "x", + ) + .is_err() + ); + } + + #[test] + fn ingest_delete_error_paths_are_covered() { + let exec = SqliteExecutor::open_memory().expect("db"); + let (farm_id, _farm_pubkey, farm_d_tag, _plot_d_tag) = seed_rows(&exec); + + let not_found_farm_tags = DeleteErrorExecutor { + inner: &exec, + table_name: "farm_tag", + err: SqlError::NotFound("farm_tag".to_string()), + }; + assert!( + upsert_farm_tags( + ¬_found_farm_tags, + &farm_id, + Some(vec!["next".to_string()]) + ) + .is_ok() + ); + + let not_found_plot_tags = DeleteErrorExecutor { + inner: &exec, + table_name: "plot_tag", + err: SqlError::NotFound("plot_tag".to_string()), + }; + let plot_id = plot::find_many(&exec, &IPlotFindMany { filter: None }) + .expect("plots") + .results[0] + .id + .clone(); + assert!( + upsert_plot_tags( + ¬_found_plot_tags, + &plot_id, + Some(vec!["next".to_string()]) + ) + .is_ok() + ); + + let not_found_farm_locations = DeleteErrorExecutor { + inner: &exec, + table_name: "farm_gcs_location", + err: SqlError::NotFound("farm_gcs_location".to_string()), + }; + assert!( + upsert_farm_location( + ¬_found_farm_locations, + &farm_id, + Some(RadrootsFarmLocation { + primary: None, + city: None, + region: None, + country: None, + gcs: sample_gcs(1.0, 2.0, "s4"), + }), + &FixedFactory, + ) + .is_ok() + ); + + let not_found_plot_locations = DeleteErrorExecutor { + inner: &exec, + table_name: "plot_gcs_location", + err: SqlError::NotFound("plot_gcs_location".to_string()), + }; + assert!( + upsert_plot_location( + ¬_found_plot_locations, + &plot_id, + Some(RadrootsPlotLocation { + primary: None, + city: None, + region: None, + country: None, + gcs: sample_gcs(1.1, 2.1, "s5"), + }), + &FixedFactory, + ) + .is_ok() + ); + + let members_list_set = + farm_list_sets::farm_members_list_set(&farm_d_tag, vec!["n".repeat(64)]) + .expect("members"); + assert!( + upsert_farm_members(&exec, &farm_id, ListSetRole::Members, &members_list_set).is_ok() + ); + let not_found_members = DeleteErrorExecutor { + inner: &exec, + table_name: "farm_member", + err: SqlError::NotFound("farm_member".to_string()), + }; + let not_found_members_list_set = + farm_list_sets::farm_members_list_set(&farm_d_tag, vec!["q".repeat(64)]) + .expect("not found members"); + assert!( + upsert_farm_members( + ¬_found_members, + &farm_id, + ListSetRole::Members, + ¬_found_members_list_set, + ) + .is_ok() + ); + assert!( + upsert_farm_members( + ¬_found_members, + &farm_id, + ListSetRole::Plots, + ¬_found_members_list_set, + ) + .is_ok() + ); + + let member_claims = + farm_list_sets::member_of_farms_list_set(vec!["z".repeat(64)]).expect("claims"); + assert!(upsert_member_claims(&exec, &"m".repeat(64), &member_claims).is_ok()); + let not_found_claims = DeleteErrorExecutor { + inner: &exec, + table_name: "farm_member_claim", + err: SqlError::NotFound("farm_member_claim".to_string()), + }; + let not_found_member_claims = + farm_list_sets::member_of_farms_list_set(vec!["y".repeat(64)]).expect("claims nf"); + assert!( + upsert_member_claims(¬_found_claims, &"m".repeat(64), ¬_found_member_claims) + .is_ok() + ); + assert!(not_found_claims.begin().is_ok()); + assert!(not_found_claims.commit().is_ok()); + let _ = not_found_claims.rollback(); + assert!(not_found_claims.query_raw("SELECT 1", "[]").is_ok()); + assert!(matches!( + not_found_claims.exec("DELETE FROM farm_member_claim WHERE id = 1", "[]"), + Err(SqlError::NotFound(_)) + )); + let _ = not_found_claims.exec("DELETE FROM other_table WHERE id = 1", "[]"); + + let internal_farm_tags = DeleteErrorExecutor { + inner: &exec, + table_name: "farm_tag", + err: SqlError::Internal, + }; + assert!( + upsert_farm_tags(&internal_farm_tags, &farm_id, Some(vec!["x".to_string()])).is_err() + ); + + let internal_plot_tags = DeleteErrorExecutor { + inner: &exec, + table_name: "plot_tag", + err: SqlError::Internal, + }; + assert!( + upsert_plot_tags(&internal_plot_tags, &plot_id, Some(vec!["x".to_string()])).is_err() + ); + + let internal_farm_locations = DeleteErrorExecutor { + inner: &exec, + table_name: "farm_gcs_location", + err: SqlError::Internal, + }; + assert!( + upsert_farm_location( + &internal_farm_locations, + &farm_id, + Some(RadrootsFarmLocation { + primary: None, + city: None, + region: None, + country: None, + gcs: sample_gcs(2.0, 3.0, "s6"), + }), + &FixedFactory, + ) + .is_err() + ); + + let internal_plot_locations = DeleteErrorExecutor { + inner: &exec, + table_name: "plot_gcs_location", + err: SqlError::Internal, + }; + assert!( + upsert_plot_location( + &internal_plot_locations, + &plot_id, + Some(RadrootsPlotLocation { + primary: None, + city: None, + region: None, + country: None, + gcs: sample_gcs(2.1, 3.1, "s7"), + }), + &FixedFactory, + ) + .is_err() + ); + + let internal_members = DeleteErrorExecutor { + inner: &exec, + table_name: "farm_member", + err: SqlError::Internal, + }; + assert!( + upsert_farm_members( + &internal_members, + &farm_id, + ListSetRole::Members, + &members_list_set, + ) + .is_err() + ); + + let internal_claims = DeleteErrorExecutor { + inner: &exec, + table_name: "farm_member_claim", + err: SqlError::Internal, + }; + assert!(upsert_member_claims(&internal_claims, &"m".repeat(64), &member_claims).is_err()); + } + + #[test] + fn create_gcs_location_error_mapping_helpers_are_covered() { + let point_json_err = serde_json::from_str::<Value>("{").expect_err("invalid json"); + let point_err = map_gcs_point_serialize_error(point_json_err); + assert_eq!(point_err.to_string(), "tangle_events.data: gcs.point"); + + let polygon_json_err = serde_json::from_str::<Value>("{").expect_err("invalid json"); + let polygon_err = map_gcs_polygon_serialize_error(polygon_json_err); + assert_eq!(polygon_err.to_string(), "tangle_events.data: gcs.polygon"); + } +} diff --git a/crates/replica-sync/src/lib.rs b/crates/replica-sync/src/lib.rs @@ -0,0 +1,37 @@ +#![forbid(unsafe_code)] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(not(feature = "std"))] +extern crate alloc; + +mod canonical; +pub mod emit; +pub mod error; +mod event_state; +mod geo; +pub mod ingest; +pub mod sync_state; +pub mod types; + +pub use emit::{ + radroots_replica_farm_event, radroots_replica_list_set_events, + radroots_replica_membership_claim_events, radroots_replica_plot_events, + radroots_replica_profile_events, radroots_replica_sync_all, + radroots_replica_sync_all_with_options, +}; +pub use error::RadrootsReplicaEventsError; +pub use ingest::{ + RadrootsReplicaIdFactory, RadrootsReplicaIngestOutcome, radroots_replica_ingest_event_state, + radroots_replica_ingest_event_with_factory, +}; +pub use sync_state::{RadrootsReplicaSyncStatus, radroots_replica_sync_status}; +pub use types::{ + RADROOTS_REPLICA_TRANSFER_VERSION, RadrootsReplicaEventDraft, RadrootsReplicaFarmSelector, + RadrootsReplicaSyncBundle, RadrootsReplicaSyncOptions, RadrootsReplicaSyncRequest, +}; + +#[cfg(feature = "std")] +pub use ingest::{RadrootsReplicaDefaultIdFactory, radroots_replica_ingest_event}; + +#[cfg(test)] +mod tests; diff --git a/crates/replica-sync/src/sync_state.rs b/crates/replica-sync/src/sync_state.rs @@ -0,0 +1,68 @@ +#[cfg(not(feature = "std"))] +use alloc::{ + collections::BTreeMap, + string::{String, ToString}, +}; +#[cfg(feature = "std")] +use std::collections::BTreeMap; + +use radroots_sql_core::SqlExecutor; +use radroots_replica_db_schema::farm::IFarmFindMany; +use radroots_replica_db_schema::nostr_event_state::INostrEventStateFindMany; + +use crate::error::RadrootsReplicaEventsError; +use crate::event_state::{event_content_hash, event_state_key, tag_value}; +use crate::types::RadrootsReplicaFarmSelector; + +#[derive(Clone, Debug)] +pub struct RadrootsReplicaSyncStatus { + pub expected_count: usize, + pub pending_count: usize, +} + +pub fn radroots_replica_sync_status<E: SqlExecutor>( + exec: &E, +) -> Result<RadrootsReplicaSyncStatus, RadrootsReplicaEventsError> { + let farms = radroots_replica_db::farm::find_many(exec, &IFarmFindMany { filter: None })?.results; + let mut expected: BTreeMap<String, String> = BTreeMap::new(); + + for farm in farms { + let selector = RadrootsReplicaFarmSelector { + id: Some(farm.id), + d_tag: None, + pubkey: None, + }; + let bundle = crate::emit::radroots_replica_sync_all_with_options(exec, &selector, None)?; + for event in bundle.events { + let d_tag = tag_value(&event.tags, "d").unwrap_or(""); + let key = event_state_key(event.kind, &event.author, d_tag); + let content_hash = event_content_hash(&event.content, &event.tags)?; + expected.entry(key).or_insert(content_hash); + } + } + + let states_query = radroots_replica_db::nostr_event_state::find_many( + exec, + &INostrEventStateFindMany { filter: None }, + ); + let states_result = states_query?; + let states = states_result.results; + + let mut state_map: BTreeMap<String, String> = BTreeMap::new(); + for state in states { + state_map.insert(state.key, state.content_hash); + } + + let mut pending = 0; + for (key, content_hash) in expected.iter() { + match state_map.get(key) { + Some(existing) if existing == content_hash => {} + _ => pending += 1, + } + } + + Ok(RadrootsReplicaSyncStatus { + expected_count: expected.len(), + pending_count: pending, + }) +} diff --git a/crates/replica-sync/src/tests.rs b/crates/replica-sync/src/tests.rs @@ -0,0 +1,235 @@ +use crate::{ + RADROOTS_REPLICA_TRANSFER_VERSION, RadrootsReplicaFarmSelector, RadrootsReplicaSyncRequest, + radroots_replica_sync_all, +}; +use radroots_events::farm::{RadrootsGeoJsonPoint, RadrootsGeoJsonPolygon}; +use radroots_events::kinds::{KIND_FARM, KIND_LIST_SET_GENERIC, KIND_PLOT, KIND_PROFILE}; +use radroots_sql_core::SqliteExecutor; +use radroots_sql_core::error::SqlError; +use radroots_replica_db::{ + farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, migrations, + nostr_profile, plot, plot_gcs_location, plot_tag, +}; +use radroots_replica_db_schema::farm::IFarmFields; +use radroots_replica_db_schema::farm_gcs_location::IFarmGcsLocationFields; +use radroots_replica_db_schema::farm_member::IFarmMemberFields; +use radroots_replica_db_schema::farm_member_claim::IFarmMemberClaimFields; +use radroots_replica_db_schema::farm_tag::IFarmTagFields; +use radroots_replica_db_schema::gcs_location::IGcsLocationFields; +use radroots_replica_db_schema::nostr_profile::INostrProfileFields; +use radroots_replica_db_schema::plot::IPlotFields; +use radroots_replica_db_schema::plot_gcs_location::IPlotGcsLocationFields; +use radroots_replica_db_schema::plot_tag::IPlotTagFields; +use radroots_types::types::IError; + +fn unwrap_sql<T>(result: Result<T, IError<SqlError>>, label: &str) -> T { + match result { + Ok(value) => value, + Err(err) => panic!("{label}: {}", err.err), + } +} + +#[test] +fn sync_all_emits_expected_order() { + let exec = SqliteExecutor::open_memory().expect("exec"); + migrations::run_all_up(&exec).expect("migrations"); + + let farm_pubkey = "f".repeat(64); + let farm_fields = IFarmFields { + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + pubkey: farm_pubkey.clone(), + name: "Green Farm".to_string(), + about: Some("About".to_string()), + website: None, + picture: None, + banner: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, + }; + let farm_row = unwrap_sql(farm::create(&exec, &farm_fields), "farm").result; + + let gcs_point = RadrootsGeoJsonPoint { + r#type: "Point".to_string(), + coordinates: [-122.4, 37.7], + }; + let gcs_polygon = RadrootsGeoJsonPolygon { + r#type: "Polygon".to_string(), + coordinates: vec![vec![ + [-122.4, 37.7], + [-122.4, 37.701], + [-122.401, 37.701], + [-122.4, 37.7], + ]], + }; + let gcs_fields = IGcsLocationFields { + d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), + lat: 37.7, + lng: -122.4, + geohash: "9q8yy".to_string(), + point: serde_json::to_string(&gcs_point).expect("point"), + polygon: serde_json::to_string(&gcs_polygon).expect("polygon"), + accuracy: None, + altitude: None, + tag_0: None, + label: None, + area: None, + elevation: None, + soil: None, + climate: None, + gc_id: None, + gc_name: None, + gc_admin1_id: None, + gc_admin1_name: None, + gc_country_id: None, + gc_country_name: None, + }; + let gcs_row = unwrap_sql(gcs_location::create(&exec, &gcs_fields), "gcs").result; + + let farm_gcs_fields = IFarmGcsLocationFields { + farm_id: farm_row.id.clone(), + gcs_location_id: gcs_row.id.clone(), + role: "primary".to_string(), + }; + let _ = unwrap_sql( + farm_gcs_location::create(&exec, &farm_gcs_fields), + "farm_gcs", + ); + + let plot_fields = IPlotFields { + d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), + farm_id: farm_row.id.clone(), + name: "Plot A".to_string(), + about: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, + }; + let plot_row = unwrap_sql(plot::create(&exec, &plot_fields), "plot").result; + + let plot_gcs_fields = IPlotGcsLocationFields { + plot_id: plot_row.id.clone(), + gcs_location_id: gcs_row.id.clone(), + role: "primary".to_string(), + }; + let _ = unwrap_sql( + plot_gcs_location::create(&exec, &plot_gcs_fields), + "plot_gcs", + ); + + let _ = unwrap_sql( + farm_tag::create( + &exec, + &IFarmTagFields { + farm_id: farm_row.id.clone(), + tag: "coffee".to_string(), + }, + ), + "farm_tag", + ); + + let _ = unwrap_sql( + plot_tag::create( + &exec, + &IPlotTagFields { + plot_id: plot_row.id.clone(), + tag: "orchard".to_string(), + }, + ), + "plot_tag", + ); + + let owner_pubkey = "o".repeat(64); + let _ = unwrap_sql( + farm_member::create( + &exec, + &IFarmMemberFields { + farm_id: farm_row.id.clone(), + member_pubkey: owner_pubkey.clone(), + role: "owner".to_string(), + }, + ), + "farm_member", + ); + + let _ = unwrap_sql( + farm_member_claim::create( + &exec, + &IFarmMemberClaimFields { + member_pubkey: owner_pubkey.clone(), + farm_pubkey: farm_pubkey.clone(), + }, + ), + "farm_member_claim", + ); + + let _ = unwrap_sql( + nostr_profile::create( + &exec, + &INostrProfileFields { + public_key: farm_pubkey.clone(), + profile_type: "farm".to_string(), + name: "Farm Profile".to_string(), + display_name: None, + about: None, + website: None, + picture: None, + banner: None, + nip05: None, + lud06: None, + lud16: None, + }, + ), + "farm_profile", + ); + + let _ = unwrap_sql( + nostr_profile::create( + &exec, + &INostrProfileFields { + public_key: owner_pubkey.clone(), + profile_type: "individual".to_string(), + name: "Owner".to_string(), + display_name: None, + about: None, + website: None, + picture: None, + banner: None, + nip05: None, + lud06: None, + lud16: None, + }, + ), + "owner_profile", + ); + + let request = RadrootsReplicaSyncRequest { + farm: RadrootsReplicaFarmSelector { + id: Some(farm_row.id.clone()), + d_tag: None, + pubkey: None, + }, + options: None, + }; + let bundle = radroots_replica_sync_all(&exec, &request).expect("sync"); + + assert_eq!(bundle.version, RADROOTS_REPLICA_TRANSFER_VERSION); + assert_eq!(bundle.events.len(), 9); + let kinds = bundle + .events + .iter() + .map(|event| event.kind) + .collect::<Vec<_>>(); + assert_eq!(kinds[0], KIND_PROFILE); + assert_eq!(kinds[1], KIND_PROFILE); + assert_eq!(kinds[2], KIND_FARM); + assert_eq!(kinds[3], KIND_PLOT); + assert!( + kinds[4..8] + .iter() + .all(|kind| *kind == KIND_LIST_SET_GENERIC) + ); + assert_eq!(kinds[8], KIND_LIST_SET_GENERIC); +} diff --git a/crates/replica-sync/src/types.rs b/crates/replica-sync/src/types.rs @@ -0,0 +1,40 @@ +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; + +use serde::{Deserialize, Serialize}; + +pub const RADROOTS_REPLICA_TRANSFER_VERSION: u32 = 1; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RadrootsReplicaEventDraft { + pub kind: u32, + pub author: String, + pub content: String, + pub tags: Vec<Vec<String>>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RadrootsReplicaSyncBundle { + pub version: u32, + pub events: Vec<RadrootsReplicaEventDraft>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RadrootsReplicaFarmSelector { + pub id: Option<String>, + pub d_tag: Option<String>, + pub pubkey: Option<String>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RadrootsReplicaSyncOptions { + pub include_profiles: Option<bool>, + pub include_list_sets: Option<bool>, + pub include_membership_claims: Option<bool>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RadrootsReplicaSyncRequest { + pub farm: RadrootsReplicaFarmSelector, + pub options: Option<RadrootsReplicaSyncOptions>, +} diff --git a/crates/replica-sync/tests/ingest_roundtrip.rs b/crates/replica-sync/tests/ingest_roundtrip.rs @@ -0,0 +1,1427 @@ +use radroots_events::RadrootsNostrEvent; +use radroots_events::farm::{ + RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef, RadrootsGcsLocation, RadrootsGeoJsonPoint, + RadrootsGeoJsonPolygon, +}; +use radroots_events::kinds::{ + KIND_FARM, KIND_LIST_SET_FOLLOW, KIND_LIST_SET_GENERIC, KIND_PLOT, KIND_PROFILE, +}; +use radroots_events::list::RadrootsListEntry; +use radroots_events::list_set::RadrootsListSet; +use radroots_events::plot::{RadrootsPlot, RadrootsPlotLocation}; +use radroots_events::profile::{ + RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, RadrootsProfileType, + radroots_profile_type_tag_value, +}; +use radroots_events_codec::error::{EventEncodeError, EventParseError}; +use radroots_events_codec::farm::encode as farm_encode; +use radroots_events_codec::farm::list_sets as farm_list_sets; +use radroots_events_codec::list_set::encode as list_set_encode; +use radroots_events_codec::plot::encode as plot_encode; +use radroots_sql_core::SqlExecutor; +use radroots_sql_core::SqliteExecutor; +use radroots_sql_core::error::SqlError; +use radroots_replica_db::{ + farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, migrations, + nostr_profile, plot, plot_gcs_location, plot_tag, +}; +use radroots_replica_db_schema::farm::{IFarmFields, IFarmFieldsFilter, IFarmFindMany}; +use radroots_replica_db_schema::farm_gcs_location::IFarmGcsLocationFields; +use radroots_replica_db_schema::farm_member::{ + IFarmMemberFields, IFarmMemberFieldsFilter, IFarmMemberFindMany, +}; +use radroots_replica_db_schema::farm_member_claim::{ + IFarmMemberClaimFields, IFarmMemberClaimFieldsFilter, IFarmMemberClaimFindMany, +}; +use radroots_replica_db_schema::farm_tag::{IFarmTagFields, IFarmTagFieldsFilter, IFarmTagFindMany}; +use radroots_replica_db_schema::gcs_location::IGcsLocationFields; +use radroots_replica_db_schema::nostr_profile::INostrProfileFields; +use radroots_replica_db_schema::plot::IPlotFields; +use radroots_replica_db_schema::plot_gcs_location::IPlotGcsLocationFields; +use radroots_replica_db_schema::plot_tag::{IPlotTagFields, IPlotTagFieldsFilter, IPlotTagFindMany}; +use radroots_replica_sync::{ + RADROOTS_REPLICA_TRANSFER_VERSION, RadrootsReplicaEventDraft, RadrootsReplicaEventsError, + RadrootsReplicaFarmSelector, RadrootsReplicaIngestOutcome, RadrootsReplicaSyncOptions, + RadrootsReplicaSyncRequest, radroots_replica_ingest_event, radroots_replica_sync_all, + radroots_replica_sync_status, +}; +use radroots_types::types::IError; + +fn unwrap_sql<T>(result: Result<T, IError<SqlError>>, label: &str) -> T { + match result { + Ok(value) => value, + Err(err) => panic!("{label}: {}", err.err), + } +} + +fn draft_to_event(draft: &RadrootsReplicaEventDraft, index: u32) -> RadrootsNostrEvent { + RadrootsNostrEvent { + id: format!("{:064x}", index as u64 + 1), + author: draft.author.clone(), + created_at: 1_720_000_000 + index, + kind: draft.kind, + tags: draft.tags.clone(), + content: draft.content.clone(), + sig: "f".repeat(128), + } +} + +fn seed_source( + exec: &SqliteExecutor, +) -> ( + RadrootsReplicaSyncRequest, + String, + String, + Vec<RadrootsReplicaEventDraft>, +) { + migrations::run_all_up(exec).expect("migrations"); + + let farm_pubkey = "f".repeat(64); + let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA".to_string(); + let farm_fields = IFarmFields { + d_tag: farm_d_tag.clone(), + pubkey: farm_pubkey.clone(), + name: "Green Farm".to_string(), + about: Some("About".to_string()), + website: None, + picture: None, + banner: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, + }; + let farm_row = unwrap_sql(farm::create(exec, &farm_fields), "farm").result; + + let point = radroots_events::farm::RadrootsGeoJsonPoint { + r#type: "Point".to_string(), + coordinates: [-122.4, 37.7], + }; + let polygon = radroots_events::farm::RadrootsGeoJsonPolygon { + r#type: "Polygon".to_string(), + coordinates: vec![vec![ + [-122.4, 37.7], + [-122.4, 37.701], + [-122.401, 37.701], + [-122.4, 37.7], + ]], + }; + let gcs_fields = IGcsLocationFields { + d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), + lat: 37.7, + lng: -122.4, + geohash: "9q8yy".to_string(), + point: serde_json::to_string(&point).expect("point"), + polygon: serde_json::to_string(&polygon).expect("polygon"), + accuracy: None, + altitude: None, + tag_0: None, + label: None, + area: None, + elevation: None, + soil: None, + climate: None, + gc_id: None, + gc_name: None, + gc_admin1_id: None, + gc_admin1_name: None, + gc_country_id: None, + gc_country_name: None, + }; + let gcs_row = unwrap_sql(gcs_location::create(exec, &gcs_fields), "gcs").result; + + let _ = unwrap_sql( + farm_gcs_location::create( + exec, + &IFarmGcsLocationFields { + farm_id: farm_row.id.clone(), + gcs_location_id: gcs_row.id.clone(), + role: "primary".to_string(), + }, + ), + "farm_gcs", + ); + + let plot_row = unwrap_sql( + plot::create( + exec, + &IPlotFields { + d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), + farm_id: farm_row.id.clone(), + name: "Plot A".to_string(), + about: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, + }, + ), + "plot", + ) + .result; + + let _ = unwrap_sql( + plot_gcs_location::create( + exec, + &IPlotGcsLocationFields { + plot_id: plot_row.id.clone(), + gcs_location_id: gcs_row.id.clone(), + role: "primary".to_string(), + }, + ), + "plot_gcs", + ); + + let _ = unwrap_sql( + farm_tag::create( + exec, + &IFarmTagFields { + farm_id: farm_row.id.clone(), + tag: "coffee".to_string(), + }, + ), + "farm_tag", + ); + + let _ = unwrap_sql( + plot_tag::create( + exec, + &IPlotTagFields { + plot_id: plot_row.id.clone(), + tag: "orchard".to_string(), + }, + ), + "plot_tag", + ); + + let owner_pubkey = "o".repeat(64); + let _ = unwrap_sql( + farm_member::create( + exec, + &IFarmMemberFields { + farm_id: farm_row.id.clone(), + member_pubkey: owner_pubkey.clone(), + role: "owner".to_string(), + }, + ), + "farm_member", + ); + let _ = unwrap_sql( + farm_member_claim::create( + exec, + &IFarmMemberClaimFields { + member_pubkey: owner_pubkey.clone(), + farm_pubkey: farm_pubkey.clone(), + }, + ), + "farm_member_claim", + ); + + let _ = unwrap_sql( + nostr_profile::create( + exec, + &INostrProfileFields { + public_key: farm_pubkey.clone(), + profile_type: "farm".to_string(), + name: "Farm Profile".to_string(), + display_name: None, + about: None, + website: None, + picture: None, + banner: None, + nip05: None, + lud06: None, + lud16: None, + }, + ), + "farm_profile", + ); + let _ = unwrap_sql( + nostr_profile::create( + exec, + &INostrProfileFields { + public_key: owner_pubkey.clone(), + profile_type: "individual".to_string(), + name: "Owner".to_string(), + display_name: None, + about: None, + website: None, + picture: None, + banner: None, + nip05: None, + lud06: None, + lud16: None, + }, + ), + "owner_profile", + ); + + let request = RadrootsReplicaSyncRequest { + farm: RadrootsReplicaFarmSelector { + id: Some(farm_row.id), + d_tag: None, + pubkey: None, + }, + options: None, + }; + let bundle = radroots_replica_sync_all(exec, &request).expect("sync"); + (request, farm_d_tag, farm_pubkey, bundle.events) +} + +#[test] +fn ingest_roundtrip_yields_zero_pending_sync() { + let source = SqliteExecutor::open_memory().expect("source db"); + let (_source_request, farm_d_tag, farm_pubkey, drafts) = seed_source(&source); + assert_eq!(drafts.len(), 9); + + let target = SqliteExecutor::open_memory().expect("target db"); + migrations::run_all_up(&target).expect("target migrations"); + + let mut skipped = 0usize; + for (index, draft) in drafts.iter().enumerate() { + let event = draft_to_event(draft, index as u32); + let first = radroots_replica_ingest_event(&target, &event).expect("first ingest"); + assert_eq!(first, RadrootsReplicaIngestOutcome::Applied); + let second = radroots_replica_ingest_event(&target, &event).expect("second ingest"); + if second == RadrootsReplicaIngestOutcome::Skipped { + skipped += 1; + } + } + assert!(skipped > 0); + + let status = radroots_replica_sync_status(&target).expect("sync status"); + assert_eq!(status.expected_count, drafts.len()); + assert_eq!(status.pending_count, 0); + + let replay = radroots_replica_sync_all( + &target, + &RadrootsReplicaSyncRequest { + farm: RadrootsReplicaFarmSelector { + id: None, + d_tag: Some(farm_d_tag), + pubkey: Some(farm_pubkey), + }, + options: None, + }, + ) + .expect("replay sync"); + assert_eq!(replay.version, RADROOTS_REPLICA_TRANSFER_VERSION); + assert_eq!(replay.events.len(), drafts.len()); +} + +#[test] +fn sync_status_empty_db_is_zero() { + let exec = SqliteExecutor::open_memory().expect("db"); + migrations::run_all_up(&exec).expect("migrations"); + let status = radroots_replica_sync_status(&exec).expect("status"); + assert_eq!(status.expected_count, 0); + assert_eq!(status.pending_count, 0); +} + +#[test] +fn sync_all_selector_and_options_paths_are_supported() { + let source = SqliteExecutor::open_memory().expect("source db"); + let (request, farm_d_tag, farm_pubkey, full_events) = seed_source(&source); + + let by_pair = radroots_replica_sync_all( + &source, + &RadrootsReplicaSyncRequest { + farm: RadrootsReplicaFarmSelector { + id: None, + d_tag: Some(farm_d_tag.clone()), + pubkey: Some(farm_pubkey.clone()), + }, + options: None, + }, + ) + .expect("selector by d_tag + pubkey"); + assert_eq!(by_pair.events.len(), full_events.len()); + + let reduced = radroots_replica_sync_all( + &source, + &RadrootsReplicaSyncRequest { + farm: request.farm, + options: Some(RadrootsReplicaSyncOptions { + include_profiles: Some(false), + include_list_sets: Some(false), + include_membership_claims: Some(false), + }), + }, + ) + .expect("reduced sync"); + assert_eq!(reduced.events.len(), 2); +} + +#[test] +fn ingest_rejects_unsupported_kind() { + let exec = SqliteExecutor::open_memory().expect("db"); + migrations::run_all_up(&exec).expect("migrations"); + let event = RadrootsNostrEvent { + id: format!("{:064x}", 1u64), + author: "a".repeat(64), + created_at: 1_720_000_001, + kind: 42, + tags: Vec::new(), + content: String::new(), + sig: "f".repeat(128), + }; + let err = radroots_replica_ingest_event(&exec, &event).expect_err("unsupported kind"); + assert!(err.to_string().contains("unsupported kind")); +} + +fn event_with_parts( + id: u64, + author: &str, + created_at: u32, + kind: u32, + content: String, + tags: Vec<Vec<String>>, +) -> RadrootsNostrEvent { + RadrootsNostrEvent { + id: format!("{id:064x}"), + author: author.to_string(), + created_at, + kind, + tags, + content, + sig: "f".repeat(128), + } +} + +fn sample_point(lat: f64, lng: f64) -> RadrootsGeoJsonPoint { + RadrootsGeoJsonPoint { + r#type: "Point".to_string(), + coordinates: [lng, lat], + } +} + +fn sample_polygon(lat: f64, lng: f64) -> RadrootsGeoJsonPolygon { + RadrootsGeoJsonPolygon { + r#type: "Polygon".to_string(), + coordinates: vec![vec![ + [lng, lat], + [lng, lat + 0.001], + [lng - 0.001, lat + 0.001], + [lng, lat], + ]], + } +} + +fn sample_gcs(lat: f64, lng: f64, geohash: &str) -> RadrootsGcsLocation { + RadrootsGcsLocation { + lat, + lng, + geohash: geohash.to_string(), + point: sample_point(lat, lng), + polygon: sample_polygon(lat, lng), + accuracy: Some(2.0), + altitude: Some(10.0), + tag_0: Some("soil".to_string()), + label: Some("north".to_string()), + area: Some(1_000.0), + elevation: Some(5), + soil: Some("loam".to_string()), + climate: Some("temperate".to_string()), + gc_id: Some("gc".to_string()), + gc_name: Some("name".to_string()), + gc_admin1_id: Some("admin1".to_string()), + gc_admin1_name: Some("admin1_name".to_string()), + gc_country_id: Some("country".to_string()), + gc_country_name: Some("country_name".to_string()), + } +} + +fn profile_event( + id: u64, + author: &str, + created_at: u32, + profile_type: Option<RadrootsProfileType>, + name: &str, +) -> RadrootsNostrEvent { + let profile = RadrootsProfile { + name: name.to_string(), + display_name: Some(format!("{name}_display")), + nip05: Some(format!("{name}@example.com")), + about: Some(format!("{name} about")), + website: Some("https://example.com".to_string()), + picture: Some("https://example.com/p.png".to_string()), + banner: Some("https://example.com/b.png".to_string()), + lud06: Some("lud06".to_string()), + lud16: Some("lud16".to_string()), + bot: None, + }; + let mut tags = Vec::new(); + if let Some(kind) = profile_type { + tags.push(vec![ + RADROOTS_PROFILE_TYPE_TAG_KEY.to_string(), + radroots_profile_type_tag_value(kind).to_string(), + ]); + } + event_with_parts( + id, + author, + created_at, + KIND_PROFILE, + serde_json::to_string(&profile).expect("profile json"), + tags, + ) +} + +fn farm_event( + id: u64, + author: &str, + created_at: u32, + d_tag: &str, + name: &str, + location: Option<RadrootsFarmLocation>, + tags: Option<Vec<String>>, +) -> RadrootsNostrEvent { + let farm = RadrootsFarm { + d_tag: d_tag.to_string(), + name: name.to_string(), + about: Some(format!("{name} about")), + website: Some("https://farm.example.com".to_string()), + picture: Some("https://farm.example.com/p.png".to_string()), + banner: Some("https://farm.example.com/b.png".to_string()), + location, + tags, + }; + let event_tags = farm_encode::farm_build_tags(&farm).expect("farm tags"); + event_with_parts( + id, + author, + created_at, + KIND_FARM, + serde_json::to_string(&farm).expect("farm json"), + event_tags, + ) +} + +fn plot_event( + id: u64, + author: &str, + created_at: u32, + d_tag: &str, + farm_ref: RadrootsFarmRef, + name: &str, + location: Option<RadrootsPlotLocation>, + tags: Option<Vec<String>>, +) -> RadrootsNostrEvent { + let plot = RadrootsPlot { + d_tag: d_tag.to_string(), + farm: farm_ref, + name: name.to_string(), + about: Some(format!("{name} about")), + location, + tags, + }; + let event_tags = plot_encode::plot_build_tags(&plot).expect("plot tags"); + event_with_parts( + id, + author, + created_at, + KIND_PLOT, + serde_json::to_string(&plot).expect("plot json"), + event_tags, + ) +} + +fn list_set_event( + id: u64, + author: &str, + created_at: u32, + kind: u32, + list_set: &RadrootsListSet, +) -> RadrootsNostrEvent { + let parts = list_set_encode::to_wire_parts_with_kind(list_set, kind).expect("list set parts"); + event_with_parts(id, author, created_at, kind, parts.content, parts.tags) +} + +#[test] +fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() { + let exec = SqliteExecutor::open_memory().expect("db"); + migrations::run_all_up(&exec).expect("migrations"); + + let profile_pubkey = "p".repeat(64); + let profile_create = profile_event( + 101, + &profile_pubkey, + 10, + Some(RadrootsProfileType::Individual), + "alice", + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &profile_create).expect("profile create"), + RadrootsReplicaIngestOutcome::Applied + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &profile_create).expect("profile skip same"), + RadrootsReplicaIngestOutcome::Skipped + ); + let profile_older = profile_event( + 102, + &profile_pubkey, + 9, + Some(RadrootsProfileType::Individual), + "alice-older", + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &profile_older).expect("profile skip older"), + RadrootsReplicaIngestOutcome::Skipped + ); + let profile_same_time_new_hash = profile_event( + 103, + &profile_pubkey, + 10, + Some(RadrootsProfileType::Individual), + "alice-updated", + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &profile_same_time_new_hash) + .expect("profile apply same timestamp different hash"), + RadrootsReplicaIngestOutcome::Applied + ); + let profile_missing_type = profile_event(104, &profile_pubkey, 11, None, "missing-type"); + let err = radroots_replica_ingest_event(&exec, &profile_missing_type) + .expect_err("profile type is required"); + assert!(err.to_string().contains("profile_type required")); + + let profile_types = [ + (RadrootsProfileType::Farm, "f".repeat(64), "farm-profile"), + (RadrootsProfileType::Coop, "c".repeat(64), "coop-profile"), + (RadrootsProfileType::Any, "a".repeat(64), "any-profile"), + ( + RadrootsProfileType::Radrootsd, + "d".repeat(64), + "radrootsd-profile", + ), + ]; + for (index, (profile_type, pubkey, name)) in profile_types.iter().enumerate() { + let event = profile_event( + 110 + index as u64, + pubkey, + 20 + index as u32, + Some(*profile_type), + name, + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &event).expect("profile variant"), + RadrootsReplicaIngestOutcome::Applied + ); + } + + let farm_pubkey = "e".repeat(64); + let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; + let farm_location = RadrootsFarmLocation { + primary: Some("farm-primary".to_string()), + city: Some("city".to_string()), + region: Some("region".to_string()), + country: Some("country".to_string()), + gcs: sample_gcs(37.7, -122.4, "9q8yy"), + }; + let farm_create = farm_event( + 200, + &farm_pubkey, + 100, + farm_d_tag, + "farm-a", + Some(farm_location.clone()), + Some(vec![ + "coffee".to_string(), + " ".to_string(), + "coffee".to_string(), + "grain".to_string(), + ]), + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &farm_create).expect("farm create"), + RadrootsReplicaIngestOutcome::Applied + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &farm_create).expect("farm skip same"), + RadrootsReplicaIngestOutcome::Skipped + ); + let farm_older = farm_event( + 201, + &farm_pubkey, + 99, + farm_d_tag, + "farm-older", + Some(farm_location.clone()), + None, + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &farm_older).expect("farm skip older"), + RadrootsReplicaIngestOutcome::Skipped + ); + let farm_update_same_time = farm_event( + 202, + &farm_pubkey, + 100, + farm_d_tag, + "farm-a-updated", + None, + Some(vec!["market".to_string()]), + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &farm_update_same_time).expect("farm update"), + RadrootsReplicaIngestOutcome::Applied + ); + + let farm_rows = unwrap_sql( + farm::find_many( + &exec, + &IFarmFindMany { + filter: Some(IFarmFieldsFilter { + id: None, + created_at: None, + updated_at: None, + d_tag: Some(farm_d_tag.to_string()), + pubkey: Some(farm_pubkey.clone()), + name: None, + about: None, + website: None, + picture: None, + banner: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, + }), + }, + ), + "farm find_many", + ) + .results; + assert_eq!(farm_rows.len(), 1); + let farm_id = farm_rows[0].id.clone(); + + let farm_tags = unwrap_sql( + farm_tag::find_many( + &exec, + &IFarmTagFindMany { + filter: Some(IFarmTagFieldsFilter { + id: None, + created_at: None, + updated_at: None, + farm_id: Some(farm_id.clone()), + tag: None, + }), + }, + ), + "farm tags", + ) + .results; + assert_eq!(farm_tags.len(), 1); + assert_eq!(farm_tags[0].tag, "market"); + + let plot_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ"; + let plot_location = RadrootsPlotLocation { + primary: Some("plot-primary".to_string()), + city: Some("plot-city".to_string()), + region: Some("plot-region".to_string()), + country: Some("plot-country".to_string()), + gcs: sample_gcs(37.8, -122.5, "9q8yz"), + }; + let plot_create = plot_event( + 300, + &farm_pubkey, + 200, + plot_d_tag, + RadrootsFarmRef { + pubkey: farm_pubkey.clone(), + d_tag: farm_d_tag.to_string(), + }, + "plot-a", + Some(plot_location.clone()), + Some(vec![ + "orchard".to_string(), + " ".to_string(), + "orchard".to_string(), + "shade".to_string(), + ]), + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &plot_create).expect("plot create"), + RadrootsReplicaIngestOutcome::Applied + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &plot_create).expect("plot skip same"), + RadrootsReplicaIngestOutcome::Skipped + ); + let plot_older = plot_event( + 301, + &farm_pubkey, + 199, + plot_d_tag, + RadrootsFarmRef { + pubkey: farm_pubkey.clone(), + d_tag: farm_d_tag.to_string(), + }, + "plot-older", + Some(plot_location.clone()), + None, + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &plot_older).expect("plot skip older"), + RadrootsReplicaIngestOutcome::Skipped + ); + let plot_update = plot_event( + 302, + &farm_pubkey, + 200, + plot_d_tag, + RadrootsFarmRef { + pubkey: farm_pubkey.clone(), + d_tag: farm_d_tag.to_string(), + }, + "plot-a-updated", + None, + Some(vec!["updated".to_string()]), + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &plot_update).expect("plot update"), + RadrootsReplicaIngestOutcome::Applied + ); + let plot_missing_farm = plot_event( + 303, + &farm_pubkey, + 201, + "AAAAAAAAAAAAAAAAAAAAAg", + RadrootsFarmRef { + pubkey: "z".repeat(64), + d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), + }, + "plot-missing-farm", + None, + None, + ); + let missing_farm_err = radroots_replica_ingest_event(&exec, &plot_missing_farm) + .expect_err("plot requires existing farm"); + assert!(missing_farm_err.to_string().contains("farm not found")); + + let plot_rows = unwrap_sql( + plot::find_many( + &exec, + &radroots_replica_db_schema::plot::IPlotFindMany { filter: None }, + ), + "plot rows", + ) + .results; + assert_eq!(plot_rows.len(), 1); + let plot_id = plot_rows[0].id.clone(); + let plot_tags = unwrap_sql( + plot_tag::find_many( + &exec, + &IPlotTagFindMany { + filter: Some(IPlotTagFieldsFilter { + id: None, + created_at: None, + updated_at: None, + plot_id: Some(plot_id), + tag: None, + }), + }, + ), + "plot tags", + ) + .results; + assert_eq!(plot_tags.len(), 1); + assert_eq!(plot_tags[0].tag, "updated"); + + let non_generic_list_set = RadrootsListSet { + d_tag: "member_of.farms".to_string(), + content: String::new(), + entries: vec![RadrootsListEntry { + tag: "p".to_string(), + values: vec![farm_pubkey.clone()], + }], + title: None, + description: None, + image: None, + }; + let non_generic_event = list_set_event( + 400, + &profile_pubkey, + 300, + KIND_LIST_SET_FOLLOW, + &non_generic_list_set, + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &non_generic_event).expect("non-generic list set"), + RadrootsReplicaIngestOutcome::Skipped + ); + + let metadata_list_set = RadrootsListSet { + d_tag: "member_of.farms".to_string(), + content: String::new(), + entries: vec![RadrootsListEntry { + tag: "p".to_string(), + values: vec![farm_pubkey.clone()], + }], + title: Some("title".to_string()), + description: None, + image: None, + }; + let metadata_event = list_set_event( + 401, + &profile_pubkey, + 301, + KIND_LIST_SET_GENERIC, + &metadata_list_set, + ); + let metadata_err = radroots_replica_ingest_event(&exec, &metadata_event) + .expect_err("metadata must be rejected"); + assert!(metadata_err.to_string().contains("must omit metadata")); + + let content_list_set = RadrootsListSet { + d_tag: "member_of.farms".to_string(), + content: "not-empty".to_string(), + entries: vec![RadrootsListEntry { + tag: "p".to_string(), + values: vec![farm_pubkey.clone()], + }], + title: None, + description: None, + image: None, + }; + let content_event = list_set_event( + 402, + &profile_pubkey, + 302, + KIND_LIST_SET_GENERIC, + &content_list_set, + ); + let content_err = + radroots_replica_ingest_event(&exec, &content_event).expect_err("content must be rejected"); + assert!(content_err.to_string().contains("must not include content")); + + let invalid_member_of = RadrootsListSet { + d_tag: "member_of.farms".to_string(), + content: String::new(), + entries: vec![RadrootsListEntry { + tag: "a".to_string(), + values: vec![farm_pubkey.clone()], + }], + title: None, + description: None, + image: None, + }; + let invalid_member_of_event = list_set_event( + 403, + &profile_pubkey, + 303, + KIND_LIST_SET_GENERIC, + &invalid_member_of, + ); + let invalid_member_of_err = radroots_replica_ingest_event(&exec, &invalid_member_of_event) + .expect_err("member_of requires p tags"); + assert!( + invalid_member_of_err + .to_string() + .contains("must only include p tags") + ); + + let member_of_valid = RadrootsListSet { + d_tag: "member_of.farms".to_string(), + content: String::new(), + entries: vec![ + RadrootsListEntry { + tag: "p".to_string(), + values: vec![farm_pubkey.clone()], + }, + RadrootsListEntry { + tag: "p".to_string(), + values: vec![farm_pubkey.clone()], + }, + ], + title: None, + description: None, + image: None, + }; + let member_of_event = list_set_event( + 404, + &profile_pubkey, + 304, + KIND_LIST_SET_GENERIC, + &member_of_valid, + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &member_of_event).expect("member_of apply"), + RadrootsReplicaIngestOutcome::Applied + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &member_of_event).expect("member_of skip"), + RadrootsReplicaIngestOutcome::Skipped + ); + + let claims = unwrap_sql( + farm_member_claim::find_many( + &exec, + &IFarmMemberClaimFindMany { + filter: Some(IFarmMemberClaimFieldsFilter { + id: None, + created_at: None, + updated_at: None, + member_pubkey: Some(profile_pubkey.clone()), + farm_pubkey: None, + }), + }, + ), + "claims", + ) + .results; + assert_eq!(claims.len(), 1); + assert_eq!(claims[0].farm_pubkey, farm_pubkey); + + let invalid_members = RadrootsListSet { + d_tag: format!("farm:{farm_d_tag}:members"), + content: String::new(), + entries: vec![RadrootsListEntry { + tag: "a".to_string(), + values: vec!["x".to_string()], + }], + title: None, + description: None, + image: None, + }; + let invalid_members_event = list_set_event( + 405, + &farm_pubkey, + 305, + KIND_LIST_SET_GENERIC, + &invalid_members, + ); + let invalid_members_err = radroots_replica_ingest_event(&exec, &invalid_members_event) + .expect_err("members list requires p entries"); + assert!( + invalid_members_err + .to_string() + .contains("must only include p tags") + ); + + let members_valid = + farm_list_sets::farm_members_list_set(farm_d_tag, vec!["m".repeat(64), "m".repeat(64)]) + .expect("members list"); + let members_event = list_set_event( + 406, + &farm_pubkey, + 306, + KIND_LIST_SET_GENERIC, + &members_valid, + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &members_event).expect("members apply"), + RadrootsReplicaIngestOutcome::Applied + ); + let owners_valid = + farm_list_sets::farm_owners_list_set(farm_d_tag, vec!["o".repeat(64)]).expect("owners"); + let owners_event = list_set_event(407, &farm_pubkey, 307, KIND_LIST_SET_GENERIC, &owners_valid); + assert_eq!( + radroots_replica_ingest_event(&exec, &owners_event).expect("owners apply"), + RadrootsReplicaIngestOutcome::Applied + ); + let workers_valid = + farm_list_sets::farm_workers_list_set(farm_d_tag, vec!["w".repeat(64)]).expect("workers"); + let workers_event = list_set_event( + 408, + &farm_pubkey, + 308, + KIND_LIST_SET_GENERIC, + &workers_valid, + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &workers_event).expect("workers apply"), + RadrootsReplicaIngestOutcome::Applied + ); + + let members = unwrap_sql( + farm_member::find_many( + &exec, + &IFarmMemberFindMany { + filter: Some(IFarmMemberFieldsFilter { + id: None, + created_at: None, + updated_at: None, + farm_id: Some(farm_id), + member_pubkey: None, + role: None, + }), + }, + ), + "members", + ) + .results; + assert_eq!(members.len(), 3); + + let invalid_plots = RadrootsListSet { + d_tag: format!("farm:{farm_d_tag}:plots"), + content: String::new(), + entries: vec![RadrootsListEntry { + tag: "p".to_string(), + values: vec!["x".to_string()], + }], + title: None, + description: None, + image: None, + }; + let invalid_plots_event = list_set_event( + 409, + &farm_pubkey, + 309, + KIND_LIST_SET_GENERIC, + &invalid_plots, + ); + let invalid_plots_err = radroots_replica_ingest_event(&exec, &invalid_plots_event) + .expect_err("plots list requires a entries"); + assert!( + invalid_plots_err + .to_string() + .contains("must only include a tags") + ); + + let plot_address = plot_encode::plot_address(&farm_pubkey, plot_d_tag).expect("plot address"); + let plots_valid = RadrootsListSet { + d_tag: format!("farm:{farm_d_tag}:plots"), + content: String::new(), + entries: vec![RadrootsListEntry { + tag: "a".to_string(), + values: vec![plot_address], + }], + title: None, + description: None, + image: None, + }; + let plots_event = list_set_event(410, &farm_pubkey, 310, KIND_LIST_SET_GENERIC, &plots_valid); + assert_eq!( + radroots_replica_ingest_event(&exec, &plots_event).expect("plots apply"), + RadrootsReplicaIngestOutcome::Applied + ); + + let unsupported_list_set = RadrootsListSet { + d_tag: "unsupported.list".to_string(), + content: String::new(), + entries: vec![RadrootsListEntry { + tag: "p".to_string(), + values: vec![farm_pubkey], + }], + title: None, + description: None, + image: None, + }; + let unsupported_event = list_set_event( + 411, + &profile_pubkey, + 311, + KIND_LIST_SET_GENERIC, + &unsupported_list_set, + ); + let unsupported_err = radroots_replica_ingest_event(&exec, &unsupported_event) + .expect_err("unsupported list set d_tag"); + assert!( + unsupported_err + .to_string() + .contains("unsupported list set d_tag") + ); +} + +#[test] +fn sync_status_reports_pending_when_not_all_events_are_ingested() { + let source = SqliteExecutor::open_memory().expect("source"); + let (_request, _farm_d_tag, _farm_pubkey, drafts) = seed_source(&source); + let target = SqliteExecutor::open_memory().expect("target"); + migrations::run_all_up(&target).expect("migrations"); + + for (index, draft) in drafts.iter().enumerate() { + let event = draft_to_event(draft, index as u32); + let _ = radroots_replica_ingest_event(&target, &event).expect("ingest"); + } + target + .exec( + "UPDATE nostr_event_state SET content_hash = ? WHERE id = (SELECT id FROM nostr_event_state LIMIT 1)", + "[\"invalid_hash\"]", + ) + .expect("mutate state hash"); + + let status = radroots_replica_sync_status(&target).expect("status pending"); + assert_eq!(status.expected_count, drafts.len()); + assert!(status.pending_count > 0); +} + +#[test] +fn sync_all_rejects_invalid_selectors_and_non_unique_pair() { + let exec = SqliteExecutor::open_memory().expect("db"); + migrations::run_all_up(&exec).expect("migrations"); + + let missing_selector_err = radroots_replica_sync_all( + &exec, + &RadrootsReplicaSyncRequest { + farm: RadrootsReplicaFarmSelector { + id: None, + d_tag: None, + pubkey: None, + }, + options: None, + }, + ) + .expect_err("selector validation"); + assert!( + missing_selector_err + .to_string() + .contains("requires id or (d_tag + pubkey)") + ); + + let missing_id_err = radroots_replica_sync_all( + &exec, + &RadrootsReplicaSyncRequest { + farm: RadrootsReplicaFarmSelector { + id: Some("00000000-0000-0000-0000-000000000000".to_string()), + d_tag: None, + pubkey: None, + }, + options: None, + }, + ) + .expect_err("missing farm id"); + assert!(missing_id_err.to_string().contains("farm not found")); + + let duplicate_d_tag = "AAAAAAAAAAAAAAAAAAAAAA".to_string(); + let duplicate_pubkey = "u".repeat(64); + let fields = IFarmFields { + d_tag: duplicate_d_tag.clone(), + pubkey: duplicate_pubkey.clone(), + name: "one".to_string(), + about: None, + website: None, + picture: None, + banner: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, + }; + let _ = unwrap_sql(farm::create(&exec, &fields), "farm one"); + let _ = unwrap_sql(farm::create(&exec, &fields), "farm two"); + + let non_unique_err = radroots_replica_sync_all( + &exec, + &RadrootsReplicaSyncRequest { + farm: RadrootsReplicaFarmSelector { + id: None, + d_tag: Some(duplicate_d_tag), + pubkey: Some(duplicate_pubkey), + }, + options: None, + }, + ) + .expect_err("non unique selector"); + assert!( + non_unique_err + .to_string() + .contains("did not resolve to a single farm") + ); +} + +#[test] +fn sync_emit_handles_invalid_geojson_and_unknown_profile_type() { + let exec = SqliteExecutor::open_memory().expect("db"); + migrations::run_all_up(&exec).expect("migrations"); + + let farm_pubkey = "g".repeat(64); + let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA".to_string(); + let farm_row = unwrap_sql( + farm::create( + &exec, + &IFarmFields { + d_tag: farm_d_tag.clone(), + pubkey: farm_pubkey.clone(), + name: "farm".to_string(), + about: Some("about".to_string()), + website: None, + picture: None, + banner: None, + location_primary: Some("primary".to_string()), + location_city: Some("city".to_string()), + location_region: Some("region".to_string()), + location_country: Some("country".to_string()), + }, + ), + "farm", + ) + .result; + + let bad_gcs = unwrap_sql( + gcs_location::create( + &exec, + &IGcsLocationFields { + d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), + lat: 10.0, + lng: 20.0, + geohash: "s0".to_string(), + point: "{".to_string(), + polygon: "{\"type\":\"Polygon\",\"coordinates\":[[]]}".to_string(), + accuracy: None, + altitude: None, + tag_0: None, + label: None, + area: None, + elevation: None, + soil: None, + climate: None, + gc_id: None, + gc_name: None, + gc_admin1_id: None, + gc_admin1_name: None, + gc_country_id: None, + gc_country_name: None, + }, + ), + "bad gcs", + ) + .result; + let _ = unwrap_sql( + farm_gcs_location::create( + &exec, + &IFarmGcsLocationFields { + farm_id: farm_row.id.clone(), + gcs_location_id: bad_gcs.id.clone(), + role: "".to_string(), + }, + ), + "farm gcs", + ); + + let plot_row = unwrap_sql( + plot::create( + &exec, + &IPlotFields { + d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), + farm_id: farm_row.id.clone(), + name: "plot".to_string(), + about: Some("plot about".to_string()), + location_primary: Some("plot primary".to_string()), + location_city: None, + location_region: None, + location_country: None, + }, + ), + "plot", + ) + .result; + let _ = unwrap_sql( + plot_gcs_location::create( + &exec, + &IPlotGcsLocationFields { + plot_id: plot_row.id.clone(), + gcs_location_id: bad_gcs.id, + role: "primary".to_string(), + }, + ), + "plot gcs", + ); + + let member_pubkey = "m".repeat(64); + let _ = unwrap_sql( + farm_member::create( + &exec, + &IFarmMemberFields { + farm_id: farm_row.id.clone(), + member_pubkey: member_pubkey.clone(), + role: "owner".to_string(), + }, + ), + "member", + ); + let _ = unwrap_sql( + farm_member_claim::create( + &exec, + &IFarmMemberClaimFields { + member_pubkey: member_pubkey.clone(), + farm_pubkey: farm_pubkey.clone(), + }, + ), + "claim", + ); + let _ = unwrap_sql( + nostr_profile::create( + &exec, + &INostrProfileFields { + public_key: farm_pubkey.clone(), + profile_type: "farm".to_string(), + name: "farm profile".to_string(), + display_name: None, + about: None, + website: None, + picture: None, + banner: None, + nip05: None, + lud06: None, + lud16: None, + }, + ), + "farm profile", + ); + let _ = unwrap_sql( + nostr_profile::create( + &exec, + &INostrProfileFields { + public_key: member_pubkey.clone(), + profile_type: "legacy".to_string(), + name: "legacy profile".to_string(), + display_name: Some("legacy".to_string()), + about: Some("about".to_string()), + website: Some("https://example.com".to_string()), + picture: Some("https://example.com/p.png".to_string()), + banner: Some("https://example.com/b.png".to_string()), + nip05: Some("legacy@example.com".to_string()), + lud06: Some("lud06".to_string()), + lud16: Some("lud16".to_string()), + }, + ), + "legacy profile", + ); + + let bundle = radroots_replica_sync_all( + &exec, + &RadrootsReplicaSyncRequest { + farm: RadrootsReplicaFarmSelector { + id: Some(farm_row.id), + d_tag: None, + pubkey: None, + }, + options: None, + }, + ) + .expect("sync"); + assert_eq!(bundle.version, RADROOTS_REPLICA_TRANSFER_VERSION); + assert!(bundle.events.iter().any(|event| event.kind == KIND_FARM)); + assert!(bundle.events.iter().any(|event| event.kind == KIND_PLOT)); + assert!( + bundle + .events + .iter() + .any(|event| event.kind == KIND_LIST_SET_GENERIC) + ); + assert!(bundle.events.iter().any(|event| { + event.kind == KIND_PROFILE + && event.author == member_pubkey + && event + .tags + .iter() + .all(|tag| tag[0] != RADROOTS_PROFILE_TYPE_TAG_KEY) + })); +} + +#[test] +fn error_conversion_paths_are_exercised() { + let sql: RadrootsReplicaEventsError = IError::from(SqlError::Internal).into(); + assert!(matches!(sql, RadrootsReplicaEventsError::Sql(_))); + + let encode: RadrootsReplicaEventsError = EventEncodeError::Json.into(); + assert!(matches!(encode, RadrootsReplicaEventsError::Encode(_))); + + let parse_number_err = "x".parse::<u32>().expect_err("parse should fail"); + let parse: RadrootsReplicaEventsError = + EventParseError::InvalidNumber("k", parse_number_err).into(); + assert!(matches!(parse, RadrootsReplicaEventsError::Parse(_))); +} diff --git a/crates/tangle-db-schema/Cargo.toml b/crates/tangle-db-schema/Cargo.toml @@ -1,22 +0,0 @@ -[package] -name = "radroots-tangle-db-schema" -version.workspace = true -edition.workspace = true -authors = ["Radroots Authors"] -rust-version.workspace = true -license.workspace = true -publish = false -build = "build.rs" - -[lib] -crate-type = ["rlib"] - -[features] -default = [] -ts-rs = ["dep:ts-rs"] - -[dependencies] -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -ts-rs = { workspace = true, optional = true } -radroots-types = { workspace = true } diff --git a/crates/tangle-db-schema/build.rs b/crates/tangle-db-schema/build.rs @@ -1,39 +0,0 @@ -use std::{ - env, fs, - path::{Path, PathBuf}, -}; - -fn workspace_root(manifest_dir: &Path) -> PathBuf { - let parent = manifest_dir.parent().unwrap_or(manifest_dir); - if parent.file_name().and_then(|name| name.to_str()) == Some("crates") { - parent.parent().unwrap_or(parent).to_path_buf() - } else { - parent.to_path_buf() - } -} - -fn export_dir(crate_name: &str) -> PathBuf { - if let Some(export_dir) = env::var_os("RADROOTS_TS_RS_EXPORT_DIR") { - return PathBuf::from(export_dir); - } - let manifest_dir = PathBuf::from( - env::var("CARGO_MANIFEST_DIR").expect("missing required env var CARGO_MANIFEST_DIR"), - ); - workspace_root(&manifest_dir) - .join("target") - .join("ts-rs") - .join(crate_name) -} - -fn main() { - println!("cargo:rerun-if-changed=build.rs"); - if env::var_os("CARGO_FEATURE_TS_RS").is_some() { - let out_dir = export_dir("tangle-db-schema"); - println!("cargo:rustc-env=TS_RS_EXPORT_DIR={}", out_dir.display()); - println!("cargo:rerun-if-env-changed=RADROOTS_TS_RS_EXPORT_DIR"); - if !out_dir.exists() { - fs::create_dir_all(&out_dir).expect("create TS export dir"); - } - println!("cargo:rerun-if-changed=src"); - } -} diff --git a/crates/tangle-db-schema/tests/query_bind_values.rs b/crates/tangle-db-schema/tests/query_bind_values.rs @@ -1,354 +0,0 @@ -use radroots_tangle_db_schema::farm::FarmQueryBindValues; -use radroots_tangle_db_schema::farm_gcs_location::FarmGcsLocationQueryBindValues; -use radroots_tangle_db_schema::farm_member::FarmMemberQueryBindValues; -use radroots_tangle_db_schema::farm_member_claim::FarmMemberClaimQueryBindValues; -use radroots_tangle_db_schema::farm_tag::FarmTagQueryBindValues; -use radroots_tangle_db_schema::gcs_location::GcsLocationQueryBindValues; -use radroots_tangle_db_schema::log_error::LogErrorQueryBindValues; -use radroots_tangle_db_schema::media_image::MediaImageQueryBindValues; -use radroots_tangle_db_schema::nostr_event_state::NostrEventStateQueryBindValues; -use radroots_tangle_db_schema::nostr_profile::NostrProfileQueryBindValues; -use radroots_tangle_db_schema::nostr_relay::NostrRelayQueryBindValues; -use radroots_tangle_db_schema::plot::PlotQueryBindValues; -use radroots_tangle_db_schema::plot_gcs_location::PlotGcsLocationQueryBindValues; -use radroots_tangle_db_schema::plot_tag::PlotTagQueryBindValues; -use radroots_tangle_db_schema::trade_product::TradeProductQueryBindValues; -use serde_json::Value; - -macro_rules! assert_query_bind_values { - ($test_name:ident, $id_expr:expr, $id_param:literal, $id_lookup:literal, [$(($expr:expr, $param:literal, $lookup:literal)),* $(,)?]) => { - #[test] - fn $test_name() { - let id_case = $id_expr; - let (id_param, id_value) = id_case.to_filter_param(); - assert_eq!(id_param, $id_param); - assert_eq!(id_value, Value::from($id_lookup.to_string())); - assert_eq!(id_case.primary_key(), Some($id_lookup.to_string())); - assert_eq!(id_case.lookup_key(), $id_lookup.to_string()); - - $( - let alt_case = $expr; - let (alt_param, alt_value) = alt_case.to_filter_param(); - assert_eq!(alt_param, $param); - assert_eq!(alt_value, Value::from($lookup.to_string())); - assert_eq!(alt_case.primary_key(), None); - assert_eq!(alt_case.lookup_key(), $lookup.to_string()); - )* - } - }; -} - -assert_query_bind_values!( - farm_query_bind_values_cover_all_variants, - FarmQueryBindValues::Id { - id: "farm-id".to_string() - }, - "id", - "farm-id", - [ - ( - FarmQueryBindValues::DTag { - d_tag: "farm-d".to_string() - }, - "d_tag", - "farm-d" - ), - ( - FarmQueryBindValues::Pubkey { - pubkey: "farm-pk".to_string() - }, - "pubkey", - "farm-pk" - ), - ] -); - -assert_query_bind_values!( - farm_gcs_location_query_bind_values_cover_all_variants, - FarmGcsLocationQueryBindValues::Id { - id: "farm-gcs-id".to_string() - }, - "id", - "farm-gcs-id", - [ - ( - FarmGcsLocationQueryBindValues::FarmId { - farm_id: "farm-id".to_string() - }, - "farm_id", - "farm-id" - ), - ( - FarmGcsLocationQueryBindValues::GcsLocationId { - gcs_location_id: "gcs-id".to_string() - }, - "gcs_location_id", - "gcs-id" - ), - ] -); - -assert_query_bind_values!( - farm_member_query_bind_values_cover_all_variants, - FarmMemberQueryBindValues::Id { - id: "farm-member-id".to_string() - }, - "id", - "farm-member-id", - [ - ( - FarmMemberQueryBindValues::FarmId { - farm_id: "farm-id".to_string() - }, - "farm_id", - "farm-id" - ), - ( - FarmMemberQueryBindValues::MemberPubkey { - member_pubkey: "member-pk".to_string() - }, - "member_pubkey", - "member-pk" - ), - ] -); - -assert_query_bind_values!( - farm_member_claim_query_bind_values_cover_all_variants, - FarmMemberClaimQueryBindValues::Id { - id: "farm-member-claim-id".to_string() - }, - "id", - "farm-member-claim-id", - [ - ( - FarmMemberClaimQueryBindValues::MemberPubkey { - member_pubkey: "member-pk".to_string() - }, - "member_pubkey", - "member-pk" - ), - ( - FarmMemberClaimQueryBindValues::FarmPubkey { - farm_pubkey: "farm-pk".to_string() - }, - "farm_pubkey", - "farm-pk" - ), - ] -); - -assert_query_bind_values!( - farm_tag_query_bind_values_cover_all_variants, - FarmTagQueryBindValues::Id { - id: "farm-tag-id".to_string() - }, - "id", - "farm-tag-id", - [ - ( - FarmTagQueryBindValues::FarmId { - farm_id: "farm-id".to_string() - }, - "farm_id", - "farm-id" - ), - ( - FarmTagQueryBindValues::Tag { - tag: "organic".to_string() - }, - "tag", - "organic" - ), - ] -); - -assert_query_bind_values!( - gcs_location_query_bind_values_cover_all_variants, - GcsLocationQueryBindValues::Id { - id: "gcs-location-id".to_string() - }, - "id", - "gcs-location-id", - [ - ( - GcsLocationQueryBindValues::DTag { - d_tag: "gcs-d".to_string() - }, - "d_tag", - "gcs-d" - ), - ( - GcsLocationQueryBindValues::Geohash { - geohash: "9q8yy".to_string() - }, - "geohash", - "9q8yy" - ), - ] -); - -assert_query_bind_values!( - log_error_query_bind_values_cover_all_variants, - LogErrorQueryBindValues::Id { - id: "log-error-id".to_string() - }, - "id", - "log-error-id", - [( - LogErrorQueryBindValues::NostrPubkey { - nostr_pubkey: "nostr-pk".to_string() - }, - "nostr_pubkey", - "nostr-pk" - ),] -); - -assert_query_bind_values!( - media_image_query_bind_values_cover_all_variants, - MediaImageQueryBindValues::Id { - id: "media-image-id".to_string() - }, - "id", - "media-image-id", - [( - MediaImageQueryBindValues::FilePath { - file_path: "/tmp/a.jpg".to_string() - }, - "file_path", - "/tmp/a.jpg" - ),] -); - -assert_query_bind_values!( - nostr_event_state_query_bind_values_cover_all_variants, - NostrEventStateQueryBindValues::Id { - id: "nostr-event-state-id".to_string() - }, - "id", - "nostr-event-state-id", - [( - NostrEventStateQueryBindValues::Key { - key: "event-key".to_string() - }, - "key", - "event-key" - ),] -); - -assert_query_bind_values!( - nostr_profile_query_bind_values_cover_all_variants, - NostrProfileQueryBindValues::Id { - id: "nostr-profile-id".to_string() - }, - "id", - "nostr-profile-id", - [( - NostrProfileQueryBindValues::PublicKey { - public_key: "nostr-public-key".to_string() - }, - "public_key", - "nostr-public-key" - ),] -); - -assert_query_bind_values!( - nostr_relay_query_bind_values_cover_all_variants, - NostrRelayQueryBindValues::Id { - id: "nostr-relay-id".to_string() - }, - "id", - "nostr-relay-id", - [( - NostrRelayQueryBindValues::Url { - url: "wss://relay.example.com".to_string() - }, - "url", - "wss://relay.example.com" - ),] -); - -assert_query_bind_values!( - plot_query_bind_values_cover_all_variants, - PlotQueryBindValues::Id { - id: "plot-id".to_string() - }, - "id", - "plot-id", - [ - ( - PlotQueryBindValues::DTag { - d_tag: "plot-d".to_string() - }, - "d_tag", - "plot-d" - ), - ( - PlotQueryBindValues::FarmId { - farm_id: "farm-id".to_string() - }, - "farm_id", - "farm-id" - ), - ] -); - -assert_query_bind_values!( - plot_gcs_location_query_bind_values_cover_all_variants, - PlotGcsLocationQueryBindValues::Id { - id: "plot-gcs-id".to_string() - }, - "id", - "plot-gcs-id", - [ - ( - PlotGcsLocationQueryBindValues::PlotId { - plot_id: "plot-id".to_string() - }, - "plot_id", - "plot-id" - ), - ( - PlotGcsLocationQueryBindValues::GcsLocationId { - gcs_location_id: "gcs-id".to_string() - }, - "gcs_location_id", - "gcs-id" - ), - ] -); - -assert_query_bind_values!( - plot_tag_query_bind_values_cover_all_variants, - PlotTagQueryBindValues::Id { - id: "plot-tag-id".to_string() - }, - "id", - "plot-tag-id", - [ - ( - PlotTagQueryBindValues::PlotId { - plot_id: "plot-id".to_string() - }, - "plot_id", - "plot-id" - ), - ( - PlotTagQueryBindValues::Tag { - tag: "steep".to_string() - }, - "tag", - "steep" - ), - ] -); - -assert_query_bind_values!( - trade_product_query_bind_values_cover_all_variants, - TradeProductQueryBindValues::Id { - id: "trade-product-id".to_string() - }, - "id", - "trade-product-id", - [] -); diff --git a/crates/tangle-db-wasm/Cargo.toml b/crates/tangle-db-wasm/Cargo.toml @@ -1,29 +0,0 @@ -[package] -name = "radroots-tangle-db-wasm" -version.workspace = true -edition.workspace = true -authors = ["Radroots Authors"] -rust-version.workspace = true -license.workspace = true -publish = false - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -radroots-sql-core = { workspace = true, features = ["bridge"] } -radroots-sql-wasm-core = { workspace = true, default-features = false, features = ["bridge"] } -radroots-tangle-db = { workspace = true } -radroots-tangle-db-schema = { workspace = true } -radroots-tangle-events = { workspace = true } -js-sys = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -serde-wasm-bindgen = { workspace = true } -wasm-bindgen = { workspace = true } - -[dev-dependencies] -wasm-bindgen-test = { workspace = true } - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/tangle-db-wasm/pkg/package.json b/crates/tangle-db-wasm/pkg/package.json @@ -1,19 +0,0 @@ -{ - "name": "@radroots/tangle-db-wasm", - "version": "0.1.0", - "private": true, - "type": "module", - "files": [ - "dist" - ], - "main": "./dist/radroots_tangle_db_wasm.js", - "types": "./dist/radroots_tangle_db_wasm.d.ts", - "exports": { - ".": { - "types": "./dist/radroots_tangle_db_wasm.d.ts", - "import": "./dist/radroots_tangle_db_wasm.js", - "default": "./dist/radroots_tangle_db_wasm.js" - } - }, - "sideEffects": false -} -\ No newline at end of file diff --git a/crates/tangle-db-wasm/src/lib.rs b/crates/tangle-db-wasm/src/lib.rs @@ -1,27 +0,0 @@ -#![cfg(any(target_arch = "wasm32", coverage_nightly))] -#![forbid(unsafe_code)] - -#[cfg(target_arch = "wasm32")] -mod wasm_impl; -#[cfg(target_arch = "wasm32")] -pub use wasm_impl::*; - -#[cfg(coverage_nightly)] -pub fn coverage_branch_probe(input: bool) -> &'static str { - if input { - "tangle-db-wasm" - } else { - "tangle-db-wasm" - } -} - -#[cfg(all(test, coverage_nightly))] -mod tests { - use super::coverage_branch_probe; - - #[test] - fn coverage_branch_probe_hits_both_paths() { - assert_eq!(coverage_branch_probe(true), "tangle-db-wasm"); - assert_eq!(coverage_branch_probe(false), "tangle-db-wasm"); - } -} diff --git a/crates/tangle-db-wasm/src/wasm_impl.rs b/crates/tangle-db-wasm/src/wasm_impl.rs @@ -1,884 +0,0 @@ -use radroots_sql_core::{ - WasmSqlExecutor, export_lock_begin, export_lock_end, with_export_lock_bypass, -}; -use radroots_sql_wasm_core::{err_js, parse_json}; -use radroots_tangle_db::migrations; -use radroots_tangle_db::{TangleDbExportManifestRs, export_manifest}; -use radroots_tangle_events::radroots_tangle_sync_status; -use wasm_bindgen::JsValue; -use wasm_bindgen::prelude::*; - -use radroots_tangle_db_schema::farm::{ - IFarmCreate, IFarmDelete, IFarmFindMany, IFarmFindOne, IFarmUpdate, -}; - -use radroots_tangle_db_schema::farm_gcs_location::{ - IFarmGcsLocationCreate, IFarmGcsLocationDelete, IFarmGcsLocationFindMany, - IFarmGcsLocationFindOne, IFarmGcsLocationUpdate, -}; - -use radroots_tangle_db_schema::farm_member::{ - IFarmMemberCreate, IFarmMemberDelete, IFarmMemberFindMany, IFarmMemberFindOne, - IFarmMemberUpdate, -}; - -use radroots_tangle_db_schema::farm_member_claim::{ - IFarmMemberClaimCreate, IFarmMemberClaimDelete, IFarmMemberClaimFindMany, - IFarmMemberClaimFindOne, IFarmMemberClaimUpdate, -}; - -use radroots_tangle_db_schema::farm_tag::{ - IFarmTagCreate, IFarmTagDelete, IFarmTagFindMany, IFarmTagFindOne, IFarmTagUpdate, -}; - -use radroots_tangle_db_schema::gcs_location::{ - IGcsLocationCreate, IGcsLocationDelete, IGcsLocationFindMany, IGcsLocationFindOne, - IGcsLocationUpdate, -}; - -use radroots_tangle_db_schema::log_error::{ - ILogErrorCreate, ILogErrorDelete, ILogErrorFindMany, ILogErrorFindOne, ILogErrorUpdate, -}; - -use radroots_tangle_db_schema::media_image::{ - IMediaImageCreate, IMediaImageDelete, IMediaImageFindMany, IMediaImageFindOne, - IMediaImageUpdate, -}; - -use radroots_tangle_db_schema::nostr_profile::{ - INostrProfileCreate, INostrProfileDelete, INostrProfileFindMany, INostrProfileFindOne, - INostrProfileUpdate, -}; - -use radroots_tangle_db_schema::nostr_event_state::{ - INostrEventStateCreate, INostrEventStateDelete, INostrEventStateFindMany, - INostrEventStateFindOne, INostrEventStateUpdate, -}; - -use radroots_tangle_db_schema::nostr_relay::{ - INostrRelayCreate, INostrRelayDelete, INostrRelayFindMany, INostrRelayFindOne, - INostrRelayUpdate, -}; - -use radroots_tangle_db_schema::trade_product::{ - ITradeProductCreate, ITradeProductDelete, ITradeProductFindMany, ITradeProductFindOne, - ITradeProductUpdate, -}; - -use radroots_tangle_db_schema::plot::{ - IPlotCreate, IPlotDelete, IPlotFindMany, IPlotFindOne, IPlotUpdate, -}; - -use radroots_tangle_db_schema::plot_gcs_location::{ - IPlotGcsLocationCreate, IPlotGcsLocationDelete, IPlotGcsLocationFindMany, - IPlotGcsLocationFindOne, IPlotGcsLocationUpdate, -}; - -use radroots_tangle_db_schema::plot_tag::{ - IPlotTagCreate, IPlotTagDelete, IPlotTagFindMany, IPlotTagFindOne, IPlotTagUpdate, -}; - -use radroots_tangle_db_schema::nostr_profile_relay::INostrProfileRelayRelation; - -use radroots_tangle_db_schema::trade_product_location::ITradeProductLocationRelation; - -use radroots_tangle_db_schema::trade_product_media::ITradeProductMediaRelation; - -pub mod utils; -pub use utils::*; - -#[wasm_bindgen(js_name = tangle_db_run_migrations)] -pub fn tangle_db_run_migrations() -> Result<(), JsValue> { - let exec = WasmSqlExecutor::new(); - migrations::run_all_up(&exec).map_err(err_js) -} - -#[wasm_bindgen(js_name = tangle_db_reset_database)] -pub fn tangle_db_reset_database() -> Result<(), JsValue> { - let exec = WasmSqlExecutor::new(); - migrations::run_all_down(&exec).map_err(err_js) -} - -#[wasm_bindgen(js_name = tangle_db_export_json)] -pub fn tangle_db_export_json() -> Result<JsValue, JsValue> { - let exec = WasmSqlExecutor::new(); - let dump = radroots_tangle_db::backup::export_database_backup(&exec).map_err(err_js)?; - value_to_js(dump) -} - -#[wasm_bindgen(js_name = tangle_db_import_json)] -pub fn tangle_db_import_json(dump_json: &str) -> Result<(), JsValue> { - let exec = WasmSqlExecutor::new(); - radroots_tangle_db::backup::restore_database_backup_json(&exec, dump_json).map_err(err_js) -} - -#[wasm_bindgen(js_name = tangle_db_export_begin)] -pub fn tangle_db_export_begin() -> Result<JsValue, JsValue> { - export_lock_begin().map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let result = with_export_lock_bypass(|| export_snapshot(&exec)); - match result { - Ok(value) => Ok(value), - Err(err) => { - export_lock_end(); - Err(err) - } - } -} - -#[wasm_bindgen(js_name = tangle_db_export_finish)] -pub fn tangle_db_export_finish() -> Result<(), JsValue> { - export_lock_end(); - Ok(()) -} - -fn export_snapshot(exec: &WasmSqlExecutor) -> Result<JsValue, JsValue> { - let status = radroots_tangle_sync_status(exec).map_err(|err| { - err_js(radroots_sql_core::SqlError::InvalidArgument( - err.to_string(), - )) - })?; - if status.pending_count > 0 { - return Err(err_js(radroots_sql_core::SqlError::InvalidArgument( - format!( - "tangle db export requires synced state (pending {}/{})", - status.pending_count, status.expected_count - ), - ))); - } - let manifest = export_manifest(exec).map_err(err_js)?; - export_snapshot_value(manifest) -} - -fn export_snapshot_value(manifest: TangleDbExportManifestRs) -> Result<JsValue, JsValue> { - let bytes_js = radroots_sql_wasm_core::export_bytes(); - export_snapshot_value_with_bytes(manifest, bytes_js) -} - -fn export_snapshot_value_with_bytes( - manifest: TangleDbExportManifestRs, - bytes_js: JsValue, -) -> Result<JsValue, JsValue> { - let manifest_js = serde_wasm_bindgen::to_value(&manifest).map_err(|err| { - err_js(radroots_sql_core::SqlError::SerializationError( - err.to_string(), - )) - })?; - let obj = js_sys::Object::new(); - js_sys::Reflect::set(&obj, &JsValue::from_str("manifest_rs"), &manifest_js) - .map_err(|_| err_js(radroots_sql_core::SqlError::Internal))?; - js_sys::Reflect::set(&obj, &JsValue::from_str("db_bytes"), &bytes_js) - .map_err(|_| err_js(radroots_sql_core::SqlError::Internal))?; - Ok(JsValue::from(obj)) -} - -#[cfg(all(test, target_arch = "wasm32"))] -mod tests { - use super::export_snapshot_value_with_bytes; - use js_sys::{Reflect, Uint8Array}; - use wasm_bindgen::JsValue; - - #[wasm_bindgen_test::wasm_bindgen_test] - fn export_snapshot_value_includes_fields() { - let manifest = radroots_tangle_db::TangleDbExportManifestRs { - export_version: "1".to_string(), - tangle_db_version: "0.0.0".to_string(), - backup_format_version: "0.0.0".to_string(), - schema_hash: "hash".to_string(), - schema: Vec::new(), - migrations: Vec::new(), - table_counts: Vec::new(), - }; - let bytes = Uint8Array::new_with_length(2); - let js = - export_snapshot_value_with_bytes(manifest, JsValue::from(bytes)).expect("snapshot"); - let manifest_rs = - Reflect::get(&js, &JsValue::from_str("manifest_rs")).expect("manifest_rs"); - let db_bytes = Reflect::get(&js, &JsValue::from_str("db_bytes")).expect("db_bytes"); - assert!(manifest_rs.is_object()); - assert!(db_bytes.is_object()); - } -} - -#[wasm_bindgen(js_name = tangle_db_farm_create)] -pub fn tangle_db_farm_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::farm::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_find_one)] -pub fn tangle_db_farm_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::farm::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_find_many)] -pub fn tangle_db_farm_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::farm::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_update)] -pub fn tangle_db_farm_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::farm::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_delete)] -pub fn tangle_db_farm_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::farm::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_plot_create)] -pub fn tangle_db_plot_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::plot::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_plot_find_one)] -pub fn tangle_db_plot_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::plot::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_plot_find_many)] -pub fn tangle_db_plot_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::plot::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_plot_update)] -pub fn tangle_db_plot_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::plot::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_plot_delete)] -pub fn tangle_db_plot_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::plot::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_gcs_location_create)] -pub fn tangle_db_gcs_location_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IGcsLocationCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::gcs_location::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_gcs_location_find_one)] -pub fn tangle_db_gcs_location_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IGcsLocationFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::gcs_location::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_gcs_location_find_many)] -pub fn tangle_db_gcs_location_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IGcsLocationFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::gcs_location::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_gcs_location_update)] -pub fn tangle_db_gcs_location_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IGcsLocationUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::gcs_location::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_gcs_location_delete)] -pub fn tangle_db_gcs_location_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IGcsLocationDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::gcs_location::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_gcs_location_create)] -pub fn tangle_db_farm_gcs_location_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmGcsLocationCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::farm_gcs_location::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_gcs_location_find_one)] -pub fn tangle_db_farm_gcs_location_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmGcsLocationFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::farm_gcs_location::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_gcs_location_find_many)] -pub fn tangle_db_farm_gcs_location_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmGcsLocationFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::farm_gcs_location::find_many(&exec, &opts) - .map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_gcs_location_update)] -pub fn tangle_db_farm_gcs_location_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmGcsLocationUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::farm_gcs_location::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_gcs_location_delete)] -pub fn tangle_db_farm_gcs_location_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmGcsLocationDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::farm_gcs_location::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_plot_gcs_location_create)] -pub fn tangle_db_plot_gcs_location_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotGcsLocationCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::plot_gcs_location::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_plot_gcs_location_find_one)] -pub fn tangle_db_plot_gcs_location_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotGcsLocationFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::plot_gcs_location::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_plot_gcs_location_find_many)] -pub fn tangle_db_plot_gcs_location_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotGcsLocationFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::plot_gcs_location::find_many(&exec, &opts) - .map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_plot_gcs_location_update)] -pub fn tangle_db_plot_gcs_location_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotGcsLocationUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::plot_gcs_location::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_plot_gcs_location_delete)] -pub fn tangle_db_plot_gcs_location_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotGcsLocationDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::plot_gcs_location::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_tag_create)] -pub fn tangle_db_farm_tag_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmTagCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::farm_tag::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_tag_find_one)] -pub fn tangle_db_farm_tag_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmTagFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::farm_tag::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_tag_find_many)] -pub fn tangle_db_farm_tag_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmTagFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::farm_tag::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_tag_update)] -pub fn tangle_db_farm_tag_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmTagUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::farm_tag::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_tag_delete)] -pub fn tangle_db_farm_tag_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmTagDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::farm_tag::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_plot_tag_create)] -pub fn tangle_db_plot_tag_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotTagCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::plot_tag::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_plot_tag_find_one)] -pub fn tangle_db_plot_tag_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotTagFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::plot_tag::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_plot_tag_find_many)] -pub fn tangle_db_plot_tag_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotTagFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::plot_tag::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_plot_tag_update)] -pub fn tangle_db_plot_tag_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotTagUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::plot_tag::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_plot_tag_delete)] -pub fn tangle_db_plot_tag_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IPlotTagDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::plot_tag::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_member_create)] -pub fn tangle_db_farm_member_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::farm_member::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_member_find_one)] -pub fn tangle_db_farm_member_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::farm_member::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_member_find_many)] -pub fn tangle_db_farm_member_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::farm_member::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_member_update)] -pub fn tangle_db_farm_member_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::farm_member::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_member_delete)] -pub fn tangle_db_farm_member_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::farm_member::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_member_claim_create)] -pub fn tangle_db_farm_member_claim_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberClaimCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::farm_member_claim::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_member_claim_find_one)] -pub fn tangle_db_farm_member_claim_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberClaimFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::farm_member_claim::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_member_claim_find_many)] -pub fn tangle_db_farm_member_claim_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberClaimFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::farm_member_claim::find_many(&exec, &opts) - .map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_member_claim_update)] -pub fn tangle_db_farm_member_claim_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberClaimUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::farm_member_claim::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_farm_member_claim_delete)] -pub fn tangle_db_farm_member_claim_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IFarmMemberClaimDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::farm_member_claim::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_log_error_create)] -pub fn tangle_db_log_error_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ILogErrorCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::log_error::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_log_error_find_one)] -pub fn tangle_db_log_error_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ILogErrorFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::log_error::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_log_error_find_many)] -pub fn tangle_db_log_error_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ILogErrorFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::log_error::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_log_error_update)] -pub fn tangle_db_log_error_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ILogErrorUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::log_error::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_log_error_delete)] -pub fn tangle_db_log_error_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ILogErrorDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::log_error::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_media_image_create)] -pub fn tangle_db_media_image_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IMediaImageCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::media_image::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_media_image_find_one)] -pub fn tangle_db_media_image_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IMediaImageFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::media_image::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_media_image_find_many)] -pub fn tangle_db_media_image_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IMediaImageFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::media_image::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_media_image_update)] -pub fn tangle_db_media_image_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IMediaImageUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::media_image::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_media_image_delete)] -pub fn tangle_db_media_image_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: IMediaImageDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::media_image::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_profile_create)] -pub fn tangle_db_nostr_profile_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrProfileCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::nostr_profile::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_profile_find_one)] -pub fn tangle_db_nostr_profile_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrProfileFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::nostr_profile::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_profile_find_many)] -pub fn tangle_db_nostr_profile_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrProfileFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::nostr_profile::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_profile_update)] -pub fn tangle_db_nostr_profile_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrProfileUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::nostr_profile::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_profile_delete)] -pub fn tangle_db_nostr_profile_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrProfileDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::nostr_profile::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_event_state_create)] -pub fn tangle_db_nostr_event_state_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrEventStateCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::nostr_event_state::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_event_state_find_one)] -pub fn tangle_db_nostr_event_state_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrEventStateFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::nostr_event_state::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_event_state_find_many)] -pub fn tangle_db_nostr_event_state_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrEventStateFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::nostr_event_state::find_many(&exec, &opts) - .map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_event_state_update)] -pub fn tangle_db_nostr_event_state_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrEventStateUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::nostr_event_state::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_event_state_delete)] -pub fn tangle_db_nostr_event_state_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrEventStateDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::nostr_event_state::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_relay_create)] -pub fn tangle_db_nostr_relay_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrRelayCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::nostr_relay::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_relay_find_one)] -pub fn tangle_db_nostr_relay_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrRelayFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::nostr_relay::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_relay_find_many)] -pub fn tangle_db_nostr_relay_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrRelayFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::nostr_relay::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_relay_update)] -pub fn tangle_db_nostr_relay_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrRelayUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::nostr_relay::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_relay_delete)] -pub fn tangle_db_nostr_relay_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrRelayDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::nostr_relay::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_trade_product_create)] -pub fn tangle_db_trade_product_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductCreate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::trade_product::create(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_trade_product_find_one)] -pub fn tangle_db_trade_product_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductFindOne = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::trade_product::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_trade_product_find_many)] -pub fn tangle_db_trade_product_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductFindMany = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::trade_product::find_many(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_trade_product_update)] -pub fn tangle_db_trade_product_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductUpdate = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::trade_product::update(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_trade_product_delete)] -pub fn tangle_db_trade_product_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductDelete = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::trade_product::delete(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_profile_relay_set)] -pub fn tangle_db_nostr_profile_relay_set(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrProfileRelayRelation = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::nostr_profile_relay::set(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_nostr_profile_relay_unset)] -pub fn tangle_db_nostr_profile_relay_unset(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrProfileRelayRelation = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::nostr_profile_relay::unset(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_trade_product_location_set)] -pub fn tangle_db_trade_product_location_set(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductLocationRelation = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::trade_product_location::set(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_trade_product_location_unset)] -pub fn tangle_db_trade_product_location_unset(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductLocationRelation = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = radroots_tangle_db::trade_product_location::unset(&exec, &opts) - .map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_trade_product_media_set)] -pub fn tangle_db_trade_product_media_set(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductMediaRelation = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::trade_product_media::set(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} - -#[wasm_bindgen(js_name = tangle_db_trade_product_media_unset)] -pub fn tangle_db_trade_product_media_unset(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: ITradeProductMediaRelation = parse_json(opts_json).map_err(err_js)?; - let exec = WasmSqlExecutor::new(); - let out = - radroots_tangle_db::trade_product_media::unset(&exec, &opts).map_err(|e| err_js(e.err))?; - value_to_js(out) -} diff --git a/crates/tangle-db/Cargo.toml b/crates/tangle-db/Cargo.toml @@ -1,30 +0,0 @@ -[package] -name = "radroots-tangle-db" -version.workspace = true -edition.workspace = true -authors = ["Radroots Authors"] -rust-version.workspace = true -license.workspace = true -publish = false - -[lib] -crate-type = ["rlib"] - -[features] -default = [] -web = ["radroots-sql-core/web"] -native = ["radroots-sql-core/native"] -embedded = ["radroots-sql-core/embedded"] -coverage-minimal = [] - -[dependencies] -radroots-sql-core = { workspace = true } -radroots-tangle-db-schema = { workspace = true } -radroots-types = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -hex = { workspace = true } -sha2 = { workspace = true } - -[dev-dependencies] -radroots-sql-core = { workspace = true, features = ["native"] } diff --git a/crates/tangle-db/src/backup.rs b/crates/tangle-db/src/backup.rs @@ -1,627 +0,0 @@ -use radroots_sql_core::{SqlExecutor, error::SqlError, utils}; -use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value}; -use std::collections::{BTreeMap, HashMap}; - -pub const DATABASE_BACKUP_VERSION: &str = "1.0.0"; -pub const TANGLE_DB_VERSION: &str = env!("CARGO_PKG_VERSION"); - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SchemaEntry { - pub object_type: String, - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub table_name: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub sql: Option<String>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TableData { - pub name: String, - pub rows: Vec<Map<String, Value>>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MigrationBackup { - pub name: String, - pub up_sql: String, - pub down_sql: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DatabaseBackup { - pub format_version: String, - pub tangle_db_version: String, - pub schema: Vec<SchemaEntry>, - pub migrations: Vec<MigrationBackup>, - pub data: Vec<TableData>, -} - -pub fn export_database_backup<E: SqlExecutor>(executor: &E) -> Result<DatabaseBackup, SqlError> { - let schema = load_schema(executor)?; - let data = read_tables_for_backup(executor, &schema)?; - let migrations = export_migrations(); - Ok(DatabaseBackup { - format_version: DATABASE_BACKUP_VERSION.to_string(), - tangle_db_version: TANGLE_DB_VERSION.to_string(), - schema, - migrations, - data, - }) -} - -pub fn export_database_backup_json<E: SqlExecutor>(executor: &E) -> Result<String, SqlError> { - let backup = export_database_backup(executor)?; - serde_json::to_string(&backup).map_err(SqlError::from) -} - -pub fn restore_database_backup<E: SqlExecutor>( - executor: &E, - backup: &DatabaseBackup, -) -> Result<(), SqlError> { - validate_backup_version(backup)?; - executor.exec("PRAGMA foreign_keys = OFF;", "[]")?; - executor.begin()?; - let result = (|| { - drop_existing_objects(executor)?; - create_schema_from_backup(executor, &backup.schema)?; - insert_rows_from_backup(executor, backup)?; - Ok(()) - })(); - - match result { - Ok(()) => { - executor.commit()?; - let _ = executor.exec("PRAGMA foreign_keys = ON;", "[]")?; - Ok(()) - } - Err(err) => { - let _ = executor.rollback(); - let _ = executor.exec("PRAGMA foreign_keys = ON;", "[]"); - Err(err) - } - } -} - -pub fn restore_database_backup_json<E: SqlExecutor>( - executor: &E, - backup_json: &str, -) -> Result<(), SqlError> { - let backup: DatabaseBackup = serde_json::from_str(backup_json).map_err(SqlError::from)?; - restore_database_backup(executor, &backup) -} - -fn drop_existing_objects<E: SqlExecutor>(executor: &E) -> Result<(), SqlError> { - #[derive(Deserialize)] - struct MasterRow { - #[serde(rename = "type")] - object_type: Option<String>, - name: Option<String>, - } - let query = "select type, name from sqlite_master where name not like 'sqlite_%'"; - let json = executor.query_raw(query, "[]")?; - let rows: Vec<MasterRow> = utils::parse_json(&json)?; - - let mut groups: HashMap<String, Vec<String>> = HashMap::new(); - for row in rows.into_iter() { - let obj_type = row.object_type.unwrap_or_default(); - let name = match row.name { - Some(n) => n, - None => continue, - }; - groups.entry(obj_type).or_default().push(name); - } - - for object_type in ["trigger", "view", "index", "table"] { - if let Some(names) = groups.get(object_type) { - for name in names { - let stmt = match object_type { - "trigger" => format!("DROP TRIGGER IF EXISTS {};", escape_identifier(name)), - "view" => format!("DROP VIEW IF EXISTS {};", escape_identifier(name)), - "index" => format!("DROP INDEX IF EXISTS {};", escape_identifier(name)), - _ => format!("DROP TABLE IF EXISTS {};", escape_identifier(name)), - }; - let _ = executor.exec(&stmt, "[]")?; - } - } - } - Ok(()) -} - -fn create_schema_from_backup<E: SqlExecutor>( - executor: &E, - schema: &[SchemaEntry], -) -> Result<(), SqlError> { - for entry in schema.iter().filter(|s| s.object_type == "table") { - if let Some(sql) = &entry.sql { - executor.exec(sql, "[]")?; - } - } - for entry in schema.iter().filter(|s| s.object_type != "table") { - if let Some(sql) = &entry.sql { - executor.exec(sql, "[]")?; - } - } - Ok(()) -} - -fn insert_rows_from_backup<E: SqlExecutor>( - executor: &E, - backup: &DatabaseBackup, -) -> Result<(), SqlError> { - let mut row_sources: HashMap<&str, &Vec<Map<String, Value>>> = HashMap::new(); - for table in &backup.data { - row_sources.insert(table.name.as_str(), &table.rows); - } - for entry in backup.schema.iter().filter(|s| s.object_type == "table") { - let rows = match row_sources.get(entry.name.as_str()) { - Some(r) => *r, - None => continue, - }; - for row in rows { - insert_row(executor, &entry.name, row)?; - } - } - Ok(()) -} - -fn insert_row<E: SqlExecutor>( - executor: &E, - table: &str, - row: &Map<String, Value>, -) -> Result<(), SqlError> { - if row.is_empty() { - return Ok(()); - } - - let mut cols: BTreeMap<String, &Value> = BTreeMap::new(); - for (k, v) in row { - cols.insert(k.clone(), v); - } - - let column_names: Vec<String> = cols.keys().cloned().collect(); - let placeholders = (0..column_names.len()) - .map(|_| "?") - .collect::<Vec<_>>() - .join(","); - let sql = format!( - "INSERT INTO {} ({}) VALUES ({});", - escape_identifier(table), - column_names - .iter() - .map(|c| escape_identifier(c)) - .collect::<Vec<_>>() - .join(","), - placeholders - ); - - let binds: Vec<Value> = cols.values().map(|v| utils::to_db_bind_value(*v)).collect(); - let params_json = serde_json::to_string(&binds).map_err(SqlError::from)?; - executor.exec(&sql, ¶ms_json)?; - Ok(()) -} - -pub(crate) fn load_schema<E: SqlExecutor>(executor: &E) -> Result<Vec<SchemaEntry>, SqlError> { - let query = "select type, name, tbl_name as table_name, sql from sqlite_master where name not like 'sqlite_%' order by type, name"; - let json = executor.query_raw(query, "[]")?; - #[derive(Deserialize)] - struct RawSchema { - #[serde(rename = "type")] - object_type: Option<String>, - name: Option<String>, - table_name: Option<String>, - sql: Option<String>, - } - let rows: Vec<RawSchema> = utils::parse_json(&json)?; - Ok(rows - .into_iter() - .filter_map(|row| { - let name = row.name?; - let object_type = row.object_type.unwrap_or_default(); - Some(SchemaEntry { - object_type, - name, - table_name: row.table_name, - sql: row.sql, - }) - }) - .collect()) -} - -pub(crate) fn export_migrations() -> Vec<MigrationBackup> { - crate::migrations::MIGRATIONS - .iter() - .map(|m| MigrationBackup { - name: m.name.to_string(), - up_sql: m.up_sql.to_string(), - down_sql: m.down_sql.to_string(), - }) - .collect() -} - -fn read_tables_for_backup<E: SqlExecutor>( - executor: &E, - schema: &[SchemaEntry], -) -> Result<Vec<TableData>, SqlError> { - let mut data = Vec::new(); - for entry in schema.iter().filter(|s| s.object_type == "table") { - let select_sql = format!("SELECT * FROM {};", escape_identifier(&entry.name)); - let json = executor.query_raw(&select_sql, "[]")?; - let rows: Vec<Map<String, Value>> = utils::parse_json(&json)?; - data.push(TableData { - name: entry.name.clone(), - rows, - }); - } - Ok(data) -} - -pub(crate) fn escape_identifier(name: &str) -> String { - let mut escaped = String::with_capacity(name.len() + 2); - escaped.push('"'); - for c in name.chars() { - if c == '"' { - escaped.push('"'); - } - escaped.push(c); - } - escaped.push('"'); - escaped -} - -fn validate_backup_version(backup: &DatabaseBackup) -> Result<(), SqlError> { - if backup.format_version != DATABASE_BACKUP_VERSION { - return Err(SqlError::InvalidArgument(format!( - "unsupported backup format {}, expected {}", - backup.format_version, DATABASE_BACKUP_VERSION - ))); - } - if backup.tangle_db_version != TANGLE_DB_VERSION { - return Err(SqlError::InvalidArgument(format!( - "unsupported tangle-db version {}, expected {}", - backup.tangle_db_version, TANGLE_DB_VERSION - ))); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_sql_core::ExecOutcome; - use std::sync::Mutex; - use std::sync::atomic::{AtomicUsize, Ordering}; - - struct MockExecutor { - query_rules: Vec<(String, String)>, - fail_exec_contains: Option<String>, - exec_calls: Mutex<Vec<String>>, - begin_calls: AtomicUsize, - commit_calls: AtomicUsize, - rollback_calls: AtomicUsize, - } - - impl MockExecutor { - fn new(query_rules: Vec<(String, String)>, fail_exec_contains: Option<String>) -> Self { - Self { - query_rules, - fail_exec_contains, - exec_calls: Mutex::new(Vec::new()), - begin_calls: AtomicUsize::new(0), - commit_calls: AtomicUsize::new(0), - rollback_calls: AtomicUsize::new(0), - } - } - - fn exec_calls(&self) -> Vec<String> { - self.exec_calls.lock().expect("exec calls lock").clone() - } - - fn begin_count(&self) -> usize { - self.begin_calls.load(Ordering::SeqCst) - } - - fn commit_count(&self) -> usize { - self.commit_calls.load(Ordering::SeqCst) - } - - fn rollback_count(&self) -> usize { - self.rollback_calls.load(Ordering::SeqCst) - } - } - - impl SqlExecutor for MockExecutor { - fn exec(&self, sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> { - self.exec_calls - .lock() - .expect("exec calls lock") - .push(sql.to_string()); - if let Some(needle) = &self.fail_exec_contains { - if sql.contains(needle) { - return Err(SqlError::InvalidQuery(String::from("forced exec failure"))); - } - } - Ok(ExecOutcome { - changes: 1, - last_insert_id: 1, - }) - } - - fn query_raw(&self, sql: &str, _params_json: &str) -> Result<String, SqlError> { - for (needle, response) in &self.query_rules { - if sql.contains(needle) { - return Ok(response.clone()); - } - } - Ok(String::from("[]")) - } - - fn begin(&self) -> Result<(), SqlError> { - self.begin_calls.fetch_add(1, Ordering::SeqCst); - Ok(()) - } - - fn commit(&self) -> Result<(), SqlError> { - self.commit_calls.fetch_add(1, Ordering::SeqCst); - Ok(()) - } - - fn rollback(&self) -> Result<(), SqlError> { - self.rollback_calls.fetch_add(1, Ordering::SeqCst); - Ok(()) - } - } - - fn backup_with_versions(format_version: &str, tangle_db_version: &str) -> DatabaseBackup { - DatabaseBackup { - format_version: format_version.to_string(), - tangle_db_version: tangle_db_version.to_string(), - schema: Vec::new(), - migrations: Vec::new(), - data: Vec::new(), - } - } - - #[test] - fn restore_database_backup_rolls_back_when_exec_fails() { - let executor = MockExecutor::new( - vec![( - String::from("select type, name from sqlite_master"), - String::from("[]"), - )], - Some(String::from("CREATE TABLE fail_table")), - ); - let backup = DatabaseBackup { - format_version: DATABASE_BACKUP_VERSION.to_string(), - tangle_db_version: TANGLE_DB_VERSION.to_string(), - schema: vec![SchemaEntry { - object_type: String::from("table"), - name: String::from("fail_table"), - table_name: Some(String::from("fail_table")), - sql: Some(String::from("CREATE TABLE fail_table (id TEXT);")), - }], - migrations: Vec::new(), - data: Vec::new(), - }; - - let err = restore_database_backup(&executor, &backup).expect_err("restore should fail"); - assert!(matches!(err, SqlError::InvalidQuery(_))); - assert_eq!(executor.begin_count(), 1); - assert_eq!(executor.commit_count(), 0); - assert_eq!(executor.rollback_count(), 1); - let calls = executor.exec_calls(); - assert!( - calls - .iter() - .any(|sql| sql.contains("PRAGMA foreign_keys = OFF")) - ); - assert!( - calls - .iter() - .any(|sql| sql.contains("PRAGMA foreign_keys = ON")) - ); - } - - #[test] - fn drop_existing_objects_skips_rows_without_name() { - let master_rows = serde_json::json!([ - { "type": "trigger", "name": "tg_a" }, - { "type": "view", "name": "vw_a" }, - { "type": "index", "name": "ix_a" }, - { "type": "table", "name": "tb_a" }, - { "type": "table", "name": null } - ]) - .to_string(); - let executor = MockExecutor::new( - vec![( - String::from("select type, name from sqlite_master"), - master_rows, - )], - None, - ); - - drop_existing_objects(&executor).expect("drop existing objects"); - let calls = executor.exec_calls(); - assert!( - calls - .iter() - .any(|sql| sql.contains("DROP TRIGGER IF EXISTS \"tg_a\";")) - ); - assert!( - calls - .iter() - .any(|sql| sql.contains("DROP VIEW IF EXISTS \"vw_a\";")) - ); - assert!( - calls - .iter() - .any(|sql| sql.contains("DROP INDEX IF EXISTS \"ix_a\";")) - ); - assert!( - calls - .iter() - .any(|sql| sql.contains("DROP TABLE IF EXISTS \"tb_a\";")) - ); - } - - #[test] - fn create_schema_from_backup_executes_table_and_non_table_sql() { - let executor = MockExecutor::new(Vec::new(), None); - let schema = vec![ - SchemaEntry { - object_type: String::from("table"), - name: String::from("tb_a"), - table_name: Some(String::from("tb_a")), - sql: Some(String::from("CREATE TABLE tb_a (id TEXT);")), - }, - SchemaEntry { - object_type: String::from("table"), - name: String::from("tb_b"), - table_name: Some(String::from("tb_b")), - sql: None, - }, - SchemaEntry { - object_type: String::from("view"), - name: String::from("vw_a"), - table_name: Some(String::from("vw_a")), - sql: Some(String::from("CREATE VIEW vw_a AS SELECT 1;")), - }, - SchemaEntry { - object_type: String::from("index"), - name: String::from("ix_a"), - table_name: Some(String::from("ix_a")), - sql: None, - }, - ]; - - create_schema_from_backup(&executor, &schema).expect("create schema from backup"); - let calls = executor.exec_calls(); - assert!( - calls - .iter() - .any(|sql| sql == "CREATE TABLE tb_a (id TEXT);") - ); - assert!( - calls - .iter() - .any(|sql| sql == "CREATE VIEW vw_a AS SELECT 1;") - ); - assert_eq!(calls.len(), 2); - } - - #[test] - fn insert_rows_from_backup_skips_missing_data_and_empty_rows() { - let executor = MockExecutor::new(Vec::new(), None); - let mut row = Map::new(); - row.insert(String::from("co\"l"), Value::from(7)); - let backup = DatabaseBackup { - format_version: DATABASE_BACKUP_VERSION.to_string(), - tangle_db_version: TANGLE_DB_VERSION.to_string(), - schema: vec![ - SchemaEntry { - object_type: String::from("table"), - name: String::from("tb_a"), - table_name: Some(String::from("tb_a")), - sql: Some(String::from("CREATE TABLE tb_a (id TEXT);")), - }, - SchemaEntry { - object_type: String::from("table"), - name: String::from("tb_b"), - table_name: Some(String::from("tb_b")), - sql: Some(String::from("CREATE TABLE tb_b (id TEXT);")), - }, - ], - migrations: Vec::new(), - data: vec![TableData { - name: String::from("tb_a"), - rows: vec![row], - }], - }; - - insert_rows_from_backup(&executor, &backup).expect("insert rows from backup"); - let calls_after_insert = executor.exec_calls(); - assert!( - calls_after_insert - .iter() - .any(|sql| sql.contains("INSERT INTO \"tb_a\" (\"co\"\"l\") VALUES (?);")) - ); - assert!( - !calls_after_insert - .iter() - .any(|sql| sql.contains("\"tb_b\"")) - ); - - let empty_row = Map::new(); - insert_row(&executor, "tb_a", &empty_row).expect("insert empty row"); - assert_eq!(executor.exec_calls().len(), calls_after_insert.len()); - assert_eq!(escape_identifier("a\"b"), "\"a\"\"b\""); - } - - #[test] - fn load_schema_filters_rows_without_name() { - let schema_rows = serde_json::json!([ - { "type": "table", "name": null, "table_name": "tb_a", "sql": "CREATE TABLE tb_a (id TEXT);" }, - { "type": "view", "name": "vw_a", "table_name": "vw_a", "sql": "CREATE VIEW vw_a AS SELECT 1;" } - ]) - .to_string(); - let executor = MockExecutor::new( - vec![( - String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), - schema_rows, - )], - None, - ); - - let rows = load_schema(&executor).expect("load schema"); - assert_eq!(rows.len(), 1); - assert_eq!(rows[0].name, "vw_a"); - assert_eq!(rows[0].object_type, "view"); - } - - #[test] - fn validate_backup_version_rejects_invalid_versions() { - let wrong_format = backup_with_versions("0.0.1", TANGLE_DB_VERSION); - let err = validate_backup_version(&wrong_format).expect_err("format version must fail"); - assert!(matches!(err, SqlError::InvalidArgument(_))); - - let wrong_db_version = backup_with_versions(DATABASE_BACKUP_VERSION, "0.0.0"); - let err = validate_backup_version(&wrong_db_version).expect_err("db version must fail"); - assert!(matches!(err, SqlError::InvalidArgument(_))); - } - - #[test] - fn restore_database_backup_commits_on_success_and_query_fallback_works() { - let executor = MockExecutor::new( - vec![( - String::from("select type, name from sqlite_master"), - String::from("[]"), - )], - None, - ); - let backup = backup_with_versions(DATABASE_BACKUP_VERSION, TANGLE_DB_VERSION); - - let matched = executor - .query_raw("select type, name from sqlite_master", "[]") - .expect("query match"); - assert_eq!(matched, "[]"); - - let fallback = executor - .query_raw("select 1", "[]") - .expect("query fallback"); - assert_eq!(fallback, "[]"); - - restore_database_backup(&executor, &backup).expect("restore should succeed"); - assert_eq!(executor.begin_count(), 1); - assert_eq!(executor.commit_count(), 1); - assert_eq!(executor.rollback_count(), 0); - } - - #[test] - fn restore_database_backup_json_rejects_invalid_json() { - let executor = MockExecutor::new(Vec::new(), None); - let err = restore_database_backup_json(&executor, "{") - .expect_err("invalid backup json should fail"); - assert!(matches!(err, SqlError::SerializationError(_))); - } -} diff --git a/crates/tangle-db/src/export.rs b/crates/tangle-db/src/export.rs @@ -1,75 +0,0 @@ -use radroots_sql_core::{SqlExecutor, error::SqlError, utils}; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; - -use crate::backup::{ - DATABASE_BACKUP_VERSION, MigrationBackup, SchemaEntry, TANGLE_DB_VERSION, escape_identifier, - export_migrations, load_schema, -}; - -pub const TANGLE_DB_EXPORT_VERSION: &str = "1"; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TableCount { - pub name: String, - pub row_count: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TangleDbExportManifestRs { - pub export_version: String, - pub tangle_db_version: String, - pub backup_format_version: String, - pub schema_hash: String, - pub schema: Vec<SchemaEntry>, - pub migrations: Vec<MigrationBackup>, - pub table_counts: Vec<TableCount>, -} - -pub fn export_manifest<E: SqlExecutor>(executor: &E) -> Result<TangleDbExportManifestRs, SqlError> { - let schema = load_schema(executor)?; - let migrations = export_migrations(); - let table_counts = load_table_counts(executor, &schema)?; - let schema_hash = schema_hash(&schema)?; - Ok(TangleDbExportManifestRs { - export_version: TANGLE_DB_EXPORT_VERSION.to_string(), - tangle_db_version: TANGLE_DB_VERSION.to_string(), - backup_format_version: DATABASE_BACKUP_VERSION.to_string(), - schema_hash, - schema, - migrations, - table_counts, - }) -} - -fn load_table_counts<E: SqlExecutor>( - executor: &E, - schema: &[SchemaEntry], -) -> Result<Vec<TableCount>, SqlError> { - #[derive(Deserialize)] - struct CountRow { - count: u64, - } - let mut counts = Vec::new(); - for entry in schema.iter().filter(|s| s.object_type == "table") { - let sql = format!( - "select count(1) as count from {}", - escape_identifier(&entry.name) - ); - let json = executor.query_raw(&sql, "[]")?; - let rows: Vec<CountRow> = utils::parse_json(&json)?; - let row_count = rows.first().map(|row| row.count).unwrap_or(0); - counts.push(TableCount { - name: entry.name.clone(), - row_count, - }); - } - Ok(counts) -} - -fn schema_hash(schema: &[SchemaEntry]) -> Result<String, SqlError> { - let json = serde_json::to_string(schema).map_err(SqlError::from)?; - let mut hasher = Sha256::new(); - hasher.update(json.as_bytes()); - Ok(hex::encode(hasher.finalize())) -} diff --git a/crates/tangle-db/src/lib.rs b/crates/tangle-db/src/lib.rs @@ -1,783 +0,0 @@ -#![cfg_attr(feature = "coverage-minimal", allow(unused_imports))] - -pub use radroots_sql_core::error::SqlError; -pub use radroots_sql_core::{ExecOutcome, SqlExecutor}; -use radroots_types::types::IError; - -use radroots_tangle_db_schema::farm::{ - IFarmCreate, IFarmCreateResolve, IFarmDelete, IFarmDeleteResolve, IFarmFindMany, - IFarmFindManyResolve, IFarmFindOne, IFarmFindOneResolve, IFarmUpdate, IFarmUpdateResolve, -}; - -use radroots_tangle_db_schema::farm_gcs_location::{ - IFarmGcsLocationCreate, IFarmGcsLocationCreateResolve, IFarmGcsLocationDelete, - IFarmGcsLocationDeleteResolve, IFarmGcsLocationFindMany, IFarmGcsLocationFindManyResolve, - IFarmGcsLocationFindOne, IFarmGcsLocationFindOneResolve, IFarmGcsLocationUpdate, - IFarmGcsLocationUpdateResolve, -}; - -use radroots_tangle_db_schema::farm_member::{ - IFarmMemberCreate, IFarmMemberCreateResolve, IFarmMemberDelete, IFarmMemberDeleteResolve, - IFarmMemberFindMany, IFarmMemberFindManyResolve, IFarmMemberFindOne, IFarmMemberFindOneResolve, - IFarmMemberUpdate, IFarmMemberUpdateResolve, -}; - -use radroots_tangle_db_schema::farm_member_claim::{ - IFarmMemberClaimCreate, IFarmMemberClaimCreateResolve, IFarmMemberClaimDelete, - IFarmMemberClaimDeleteResolve, IFarmMemberClaimFindMany, IFarmMemberClaimFindManyResolve, - IFarmMemberClaimFindOne, IFarmMemberClaimFindOneResolve, IFarmMemberClaimUpdate, - IFarmMemberClaimUpdateResolve, -}; - -use radroots_tangle_db_schema::farm_tag::{ - IFarmTagCreate, IFarmTagCreateResolve, IFarmTagDelete, IFarmTagDeleteResolve, IFarmTagFindMany, - IFarmTagFindManyResolve, IFarmTagFindOne, IFarmTagFindOneResolve, IFarmTagUpdate, - IFarmTagUpdateResolve, -}; - -use radroots_tangle_db_schema::gcs_location::{ - IGcsLocationCreate, IGcsLocationCreateResolve, IGcsLocationDelete, IGcsLocationDeleteResolve, - IGcsLocationFindMany, IGcsLocationFindManyResolve, IGcsLocationFindOne, - IGcsLocationFindOneResolve, IGcsLocationUpdate, IGcsLocationUpdateResolve, -}; - -use radroots_tangle_db_schema::log_error::{ - ILogErrorCreate, ILogErrorCreateResolve, ILogErrorDelete, ILogErrorDeleteResolve, - ILogErrorFindMany, ILogErrorFindManyResolve, ILogErrorFindOne, ILogErrorFindOneResolve, - ILogErrorUpdate, ILogErrorUpdateResolve, -}; - -use radroots_tangle_db_schema::media_image::{ - IMediaImageCreate, IMediaImageCreateResolve, IMediaImageDelete, IMediaImageDeleteResolve, - IMediaImageFindMany, IMediaImageFindManyResolve, IMediaImageFindOne, IMediaImageFindOneResolve, - IMediaImageUpdate, IMediaImageUpdateResolve, -}; - -use radroots_tangle_db_schema::nostr_profile::{ - INostrProfileCreate, INostrProfileCreateResolve, INostrProfileDelete, - INostrProfileDeleteResolve, INostrProfileFindMany, INostrProfileFindManyResolve, - INostrProfileFindOne, INostrProfileFindOneResolve, INostrProfileUpdate, - INostrProfileUpdateResolve, -}; - -use radroots_tangle_db_schema::nostr_event_state::{ - INostrEventStateCreate, INostrEventStateCreateResolve, INostrEventStateDelete, - INostrEventStateDeleteResolve, INostrEventStateFindMany, INostrEventStateFindManyResolve, - INostrEventStateFindOne, INostrEventStateFindOneResolve, INostrEventStateUpdate, - INostrEventStateUpdateResolve, -}; - -use radroots_tangle_db_schema::nostr_relay::{ - INostrRelayCreate, INostrRelayCreateResolve, INostrRelayDelete, INostrRelayDeleteResolve, - INostrRelayFindMany, INostrRelayFindManyResolve, INostrRelayFindOne, INostrRelayFindOneResolve, - INostrRelayUpdate, INostrRelayUpdateResolve, -}; - -use radroots_tangle_db_schema::trade_product::{ - ITradeProductCreate, ITradeProductCreateResolve, ITradeProductDelete, - ITradeProductDeleteResolve, ITradeProductFindMany, ITradeProductFindManyResolve, - ITradeProductFindOne, ITradeProductFindOneResolve, ITradeProductUpdate, - ITradeProductUpdateResolve, -}; - -use radroots_tangle_db_schema::plot::{ - IPlotCreate, IPlotCreateResolve, IPlotDelete, IPlotDeleteResolve, IPlotFindMany, - IPlotFindManyResolve, IPlotFindOne, IPlotFindOneResolve, IPlotUpdate, IPlotUpdateResolve, -}; - -use radroots_tangle_db_schema::plot_gcs_location::{ - IPlotGcsLocationCreate, IPlotGcsLocationCreateResolve, IPlotGcsLocationDelete, - IPlotGcsLocationDeleteResolve, IPlotGcsLocationFindMany, IPlotGcsLocationFindManyResolve, - IPlotGcsLocationFindOne, IPlotGcsLocationFindOneResolve, IPlotGcsLocationUpdate, - IPlotGcsLocationUpdateResolve, -}; - -use radroots_tangle_db_schema::plot_tag::{ - IPlotTagCreate, IPlotTagCreateResolve, IPlotTagDelete, IPlotTagDeleteResolve, IPlotTagFindMany, - IPlotTagFindManyResolve, IPlotTagFindOne, IPlotTagFindOneResolve, IPlotTagUpdate, - IPlotTagUpdateResolve, -}; - -use radroots_tangle_db_schema::nostr_profile_relay::{ - INostrProfileRelayRelation, INostrProfileRelayResolve, -}; - -use radroots_tangle_db_schema::trade_product_location::{ - ITradeProductLocationRelation, ITradeProductLocationResolve, -}; - -use radroots_tangle_db_schema::trade_product_media::{ - ITradeProductMediaRelation, ITradeProductMediaResolve, -}; - -#[cfg(not(feature = "coverage-minimal"))] -pub mod backup; -#[cfg(not(feature = "coverage-minimal"))] -pub mod export; -#[cfg(not(feature = "coverage-minimal"))] -pub mod migrations; -#[cfg(not(feature = "coverage-minimal"))] -pub mod models; -#[cfg(not(feature = "coverage-minimal"))] -pub use backup::{DatabaseBackup, MigrationBackup, SchemaEntry}; -#[cfg(not(feature = "coverage-minimal"))] -pub use export::{TANGLE_DB_EXPORT_VERSION, TableCount, TangleDbExportManifestRs, export_manifest}; -#[cfg(not(feature = "coverage-minimal"))] -pub use models::*; - -pub struct TangleSql<E: SqlExecutor> { - executor: E, -} - -impl<E: SqlExecutor> TangleSql<E> { - pub fn coverage_branch_probe(enabled: bool) -> &'static str { - if enabled { "enabled" } else { "disabled" } - } -} - -#[cfg(not(feature = "coverage-minimal"))] -impl<E: SqlExecutor> TangleSql<E> { - pub fn new(executor: E) -> Self { - Self { executor } - } - - pub fn executor(&self) -> &E { - &self.executor - } - - pub fn migrate_up(&self) -> Result<(), SqlError> { - crate::migrations::run_all_up(self.executor()) - } - - pub fn migrate_down(&self) -> Result<(), SqlError> { - crate::migrations::run_all_down(self.executor()) - } - - pub fn backup_database(&self) -> Result<DatabaseBackup, SqlError> { - crate::backup::export_database_backup(self.executor()) - } - - pub fn backup_database_json(&self) -> Result<String, SqlError> { - crate::backup::export_database_backup_json(self.executor()) - } - - pub fn restore_database(&self, backup: &DatabaseBackup) -> Result<(), SqlError> { - crate::backup::restore_database_backup(self.executor(), backup) - } - - pub fn restore_database_json(&self, backup_json: &str) -> Result<(), SqlError> { - crate::backup::restore_database_backup_json(self.executor(), backup_json) - } - - pub fn farm_create(&self, opts: &IFarmCreate) -> Result<IFarmCreateResolve, IError<SqlError>> { - models::farm::create(self.executor(), opts) - } - - pub fn farm_find_many( - &self, - opts: &IFarmFindMany, - ) -> Result<IFarmFindManyResolve, IError<SqlError>> { - models::farm::find_many(self.executor(), opts) - } - - pub fn farm_find_one( - &self, - opts: &IFarmFindOne, - ) -> Result<IFarmFindOneResolve, IError<SqlError>> { - models::farm::find_one(self.executor(), opts) - } - - pub fn farm_update(&self, opts: &IFarmUpdate) -> Result<IFarmUpdateResolve, IError<SqlError>> { - models::farm::update(self.executor(), opts) - } - - pub fn farm_delete(&self, opts: &IFarmDelete) -> Result<IFarmDeleteResolve, IError<SqlError>> { - models::farm::delete(self.executor(), opts) - } - - pub fn plot_create(&self, opts: &IPlotCreate) -> Result<IPlotCreateResolve, IError<SqlError>> { - models::plot::create(self.executor(), opts) - } - - pub fn plot_find_many( - &self, - opts: &IPlotFindMany, - ) -> Result<IPlotFindManyResolve, IError<SqlError>> { - models::plot::find_many(self.executor(), opts) - } - - pub fn plot_find_one( - &self, - opts: &IPlotFindOne, - ) -> Result<IPlotFindOneResolve, IError<SqlError>> { - models::plot::find_one(self.executor(), opts) - } - - pub fn plot_update(&self, opts: &IPlotUpdate) -> Result<IPlotUpdateResolve, IError<SqlError>> { - models::plot::update(self.executor(), opts) - } - - pub fn plot_delete(&self, opts: &IPlotDelete) -> Result<IPlotDeleteResolve, IError<SqlError>> { - models::plot::delete(self.executor(), opts) - } - - pub fn gcs_location_create( - &self, - opts: &IGcsLocationCreate, - ) -> Result<IGcsLocationCreateResolve, IError<SqlError>> { - models::gcs_location::create(self.executor(), opts) - } - - pub fn gcs_location_find_many( - &self, - opts: &IGcsLocationFindMany, - ) -> Result<IGcsLocationFindManyResolve, IError<SqlError>> { - models::gcs_location::find_many(self.executor(), opts) - } - - pub fn gcs_location_find_one( - &self, - opts: &IGcsLocationFindOne, - ) -> Result<IGcsLocationFindOneResolve, IError<SqlError>> { - models::gcs_location::find_one(self.executor(), opts) - } - - pub fn gcs_location_update( - &self, - opts: &IGcsLocationUpdate, - ) -> Result<IGcsLocationUpdateResolve, IError<SqlError>> { - models::gcs_location::update(self.executor(), opts) - } - - pub fn gcs_location_delete( - &self, - opts: &IGcsLocationDelete, - ) -> Result<IGcsLocationDeleteResolve, IError<SqlError>> { - models::gcs_location::delete(self.executor(), opts) - } - - pub fn farm_gcs_location_create( - &self, - opts: &IFarmGcsLocationCreate, - ) -> Result<IFarmGcsLocationCreateResolve, IError<SqlError>> { - models::farm_gcs_location::create(self.executor(), opts) - } - - pub fn farm_gcs_location_find_many( - &self, - opts: &IFarmGcsLocationFindMany, - ) -> Result<IFarmGcsLocationFindManyResolve, IError<SqlError>> { - models::farm_gcs_location::find_many(self.executor(), opts) - } - - pub fn farm_gcs_location_find_one( - &self, - opts: &IFarmGcsLocationFindOne, - ) -> Result<IFarmGcsLocationFindOneResolve, IError<SqlError>> { - models::farm_gcs_location::find_one(self.executor(), opts) - } - - pub fn farm_gcs_location_update( - &self, - opts: &IFarmGcsLocationUpdate, - ) -> Result<IFarmGcsLocationUpdateResolve, IError<SqlError>> { - models::farm_gcs_location::update(self.executor(), opts) - } - - pub fn farm_gcs_location_delete( - &self, - opts: &IFarmGcsLocationDelete, - ) -> Result<IFarmGcsLocationDeleteResolve, IError<SqlError>> { - models::farm_gcs_location::delete(self.executor(), opts) - } - - pub fn plot_gcs_location_create( - &self, - opts: &IPlotGcsLocationCreate, - ) -> Result<IPlotGcsLocationCreateResolve, IError<SqlError>> { - models::plot_gcs_location::create(self.executor(), opts) - } - - pub fn plot_gcs_location_find_many( - &self, - opts: &IPlotGcsLocationFindMany, - ) -> Result<IPlotGcsLocationFindManyResolve, IError<SqlError>> { - models::plot_gcs_location::find_many(self.executor(), opts) - } - - pub fn plot_gcs_location_find_one( - &self, - opts: &IPlotGcsLocationFindOne, - ) -> Result<IPlotGcsLocationFindOneResolve, IError<SqlError>> { - models::plot_gcs_location::find_one(self.executor(), opts) - } - - pub fn plot_gcs_location_update( - &self, - opts: &IPlotGcsLocationUpdate, - ) -> Result<IPlotGcsLocationUpdateResolve, IError<SqlError>> { - models::plot_gcs_location::update(self.executor(), opts) - } - - pub fn plot_gcs_location_delete( - &self, - opts: &IPlotGcsLocationDelete, - ) -> Result<IPlotGcsLocationDeleteResolve, IError<SqlError>> { - models::plot_gcs_location::delete(self.executor(), opts) - } - - pub fn farm_tag_create( - &self, - opts: &IFarmTagCreate, - ) -> Result<IFarmTagCreateResolve, IError<SqlError>> { - models::farm_tag::create(self.executor(), opts) - } - - pub fn farm_tag_find_many( - &self, - opts: &IFarmTagFindMany, - ) -> Result<IFarmTagFindManyResolve, IError<SqlError>> { - models::farm_tag::find_many(self.executor(), opts) - } - - pub fn farm_tag_find_one( - &self, - opts: &IFarmTagFindOne, - ) -> Result<IFarmTagFindOneResolve, IError<SqlError>> { - models::farm_tag::find_one(self.executor(), opts) - } - - pub fn farm_tag_update( - &self, - opts: &IFarmTagUpdate, - ) -> Result<IFarmTagUpdateResolve, IError<SqlError>> { - models::farm_tag::update(self.executor(), opts) - } - - pub fn farm_tag_delete( - &self, - opts: &IFarmTagDelete, - ) -> Result<IFarmTagDeleteResolve, IError<SqlError>> { - models::farm_tag::delete(self.executor(), opts) - } - - pub fn plot_tag_create( - &self, - opts: &IPlotTagCreate, - ) -> Result<IPlotTagCreateResolve, IError<SqlError>> { - models::plot_tag::create(self.executor(), opts) - } - - pub fn plot_tag_find_many( - &self, - opts: &IPlotTagFindMany, - ) -> Result<IPlotTagFindManyResolve, IError<SqlError>> { - models::plot_tag::find_many(self.executor(), opts) - } - - pub fn plot_tag_find_one( - &self, - opts: &IPlotTagFindOne, - ) -> Result<IPlotTagFindOneResolve, IError<SqlError>> { - models::plot_tag::find_one(self.executor(), opts) - } - - pub fn plot_tag_update( - &self, - opts: &IPlotTagUpdate, - ) -> Result<IPlotTagUpdateResolve, IError<SqlError>> { - models::plot_tag::update(self.executor(), opts) - } - - pub fn plot_tag_delete( - &self, - opts: &IPlotTagDelete, - ) -> Result<IPlotTagDeleteResolve, IError<SqlError>> { - models::plot_tag::delete(self.executor(), opts) - } - - pub fn farm_member_create( - &self, - opts: &IFarmMemberCreate, - ) -> Result<IFarmMemberCreateResolve, IError<SqlError>> { - models::farm_member::create(self.executor(), opts) - } - - pub fn farm_member_find_many( - &self, - opts: &IFarmMemberFindMany, - ) -> Result<IFarmMemberFindManyResolve, IError<SqlError>> { - models::farm_member::find_many(self.executor(), opts) - } - - pub fn farm_member_find_one( - &self, - opts: &IFarmMemberFindOne, - ) -> Result<IFarmMemberFindOneResolve, IError<SqlError>> { - models::farm_member::find_one(self.executor(), opts) - } - - pub fn farm_member_update( - &self, - opts: &IFarmMemberUpdate, - ) -> Result<IFarmMemberUpdateResolve, IError<SqlError>> { - models::farm_member::update(self.executor(), opts) - } - - pub fn farm_member_delete( - &self, - opts: &IFarmMemberDelete, - ) -> Result<IFarmMemberDeleteResolve, IError<SqlError>> { - models::farm_member::delete(self.executor(), opts) - } - - pub fn farm_member_claim_create( - &self, - opts: &IFarmMemberClaimCreate, - ) -> Result<IFarmMemberClaimCreateResolve, IError<SqlError>> { - models::farm_member_claim::create(self.executor(), opts) - } - - pub fn farm_member_claim_find_many( - &self, - opts: &IFarmMemberClaimFindMany, - ) -> Result<IFarmMemberClaimFindManyResolve, IError<SqlError>> { - models::farm_member_claim::find_many(self.executor(), opts) - } - - pub fn farm_member_claim_find_one( - &self, - opts: &IFarmMemberClaimFindOne, - ) -> Result<IFarmMemberClaimFindOneResolve, IError<SqlError>> { - models::farm_member_claim::find_one(self.executor(), opts) - } - - pub fn farm_member_claim_update( - &self, - opts: &IFarmMemberClaimUpdate, - ) -> Result<IFarmMemberClaimUpdateResolve, IError<SqlError>> { - models::farm_member_claim::update(self.executor(), opts) - } - - pub fn farm_member_claim_delete( - &self, - opts: &IFarmMemberClaimDelete, - ) -> Result<IFarmMemberClaimDeleteResolve, IError<SqlError>> { - models::farm_member_claim::delete(self.executor(), opts) - } - - pub fn log_error_create( - &self, - opts: &ILogErrorCreate, - ) -> Result<ILogErrorCreateResolve, IError<SqlError>> { - models::log_error::create(self.executor(), opts) - } - - pub fn log_error_find_many( - &self, - opts: &ILogErrorFindMany, - ) -> Result<ILogErrorFindManyResolve, IError<SqlError>> { - models::log_error::find_many(self.executor(), opts) - } - - pub fn log_error_find_one( - &self, - opts: &ILogErrorFindOne, - ) -> Result<ILogErrorFindOneResolve, IError<SqlError>> { - models::log_error::find_one(self.executor(), opts) - } - - pub fn log_error_update( - &self, - opts: &ILogErrorUpdate, - ) -> Result<ILogErrorUpdateResolve, IError<SqlError>> { - models::log_error::update(self.executor(), opts) - } - - pub fn log_error_delete( - &self, - opts: &ILogErrorDelete, - ) -> Result<ILogErrorDeleteResolve, IError<SqlError>> { - models::log_error::delete(self.executor(), opts) - } - - pub fn media_image_create( - &self, - opts: &IMediaImageCreate, - ) -> Result<IMediaImageCreateResolve, IError<SqlError>> { - models::media_image::create(self.executor(), opts) - } - - pub fn media_image_find_many( - &self, - opts: &IMediaImageFindMany, - ) -> Result<IMediaImageFindManyResolve, IError<SqlError>> { - models::media_image::find_many(self.executor(), opts) - } - - pub fn media_image_find_one( - &self, - opts: &IMediaImageFindOne, - ) -> Result<IMediaImageFindOneResolve, IError<SqlError>> { - models::media_image::find_one(self.executor(), opts) - } - - pub fn media_image_update( - &self, - opts: &IMediaImageUpdate, - ) -> Result<IMediaImageUpdateResolve, IError<SqlError>> { - models::media_image::update(self.executor(), opts) - } - - pub fn media_image_delete( - &self, - opts: &IMediaImageDelete, - ) -> Result<IMediaImageDeleteResolve, IError<SqlError>> { - models::media_image::delete(self.executor(), opts) - } - - pub fn nostr_profile_create( - &self, - opts: &INostrProfileCreate, - ) -> Result<INostrProfileCreateResolve, IError<SqlError>> { - models::nostr_profile::create(self.executor(), opts) - } - - pub fn nostr_profile_find_many( - &self, - opts: &INostrProfileFindMany, - ) -> Result<INostrProfileFindManyResolve, IError<SqlError>> { - models::nostr_profile::find_many(self.executor(), opts) - } - - pub fn nostr_profile_find_one( - &self, - opts: &INostrProfileFindOne, - ) -> Result<INostrProfileFindOneResolve, IError<SqlError>> { - models::nostr_profile::find_one(self.executor(), opts) - } - - pub fn nostr_profile_update( - &self, - opts: &INostrProfileUpdate, - ) -> Result<INostrProfileUpdateResolve, IError<SqlError>> { - models::nostr_profile::update(self.executor(), opts) - } - - pub fn nostr_profile_delete( - &self, - opts: &INostrProfileDelete, - ) -> Result<INostrProfileDeleteResolve, IError<SqlError>> { - models::nostr_profile::delete(self.executor(), opts) - } - - pub fn nostr_event_state_create( - &self, - opts: &INostrEventStateCreate, - ) -> Result<INostrEventStateCreateResolve, IError<SqlError>> { - models::nostr_event_state::create(self.executor(), opts) - } - - pub fn nostr_event_state_find_many( - &self, - opts: &INostrEventStateFindMany, - ) -> Result<INostrEventStateFindManyResolve, IError<SqlError>> { - models::nostr_event_state::find_many(self.executor(), opts) - } - - pub fn nostr_event_state_find_one( - &self, - opts: &INostrEventStateFindOne, - ) -> Result<INostrEventStateFindOneResolve, IError<SqlError>> { - models::nostr_event_state::find_one(self.executor(), opts) - } - - pub fn nostr_event_state_update( - &self, - opts: &INostrEventStateUpdate, - ) -> Result<INostrEventStateUpdateResolve, IError<SqlError>> { - models::nostr_event_state::update(self.executor(), opts) - } - - pub fn nostr_event_state_delete( - &self, - opts: &INostrEventStateDelete, - ) -> Result<INostrEventStateDeleteResolve, IError<SqlError>> { - models::nostr_event_state::delete(self.executor(), opts) - } - - pub fn nostr_relay_create( - &self, - opts: &INostrRelayCreate, - ) -> Result<INostrRelayCreateResolve, IError<SqlError>> { - models::nostr_relay::create(self.executor(), opts) - } - - pub fn nostr_relay_find_many( - &self, - opts: &INostrRelayFindMany, - ) -> Result<INostrRelayFindManyResolve, IError<SqlError>> { - models::nostr_relay::find_many(self.executor(), opts) - } - - pub fn nostr_relay_find_one( - &self, - opts: &INostrRelayFindOne, - ) -> Result<INostrRelayFindOneResolve, IError<SqlError>> { - models::nostr_relay::find_one(self.executor(), opts) - } - - pub fn nostr_relay_update( - &self, - opts: &INostrRelayUpdate, - ) -> Result<INostrRelayUpdateResolve, IError<SqlError>> { - models::nostr_relay::update(self.executor(), opts) - } - - pub fn nostr_relay_delete( - &self, - opts: &INostrRelayDelete, - ) -> Result<INostrRelayDeleteResolve, IError<SqlError>> { - models::nostr_relay::delete(self.executor(), opts) - } - - pub fn trade_product_create( - &self, - opts: &ITradeProductCreate, - ) -> Result<ITradeProductCreateResolve, IError<SqlError>> { - models::trade_product::create(self.executor(), opts) - } - - pub fn trade_product_find_many( - &self, - opts: &ITradeProductFindMany, - ) -> Result<ITradeProductFindManyResolve, IError<SqlError>> { - models::trade_product::find_many(self.executor(), opts) - } - - pub fn trade_product_find_one( - &self, - opts: &ITradeProductFindOne, - ) -> Result<ITradeProductFindOneResolve, IError<SqlError>> { - models::trade_product::find_one(self.executor(), opts) - } - - pub fn trade_product_update( - &self, - opts: &ITradeProductUpdate, - ) -> Result<ITradeProductUpdateResolve, IError<SqlError>> { - models::trade_product::update(self.executor(), opts) - } - - pub fn trade_product_delete( - &self, - opts: &ITradeProductDelete, - ) -> Result<ITradeProductDeleteResolve, IError<SqlError>> { - models::trade_product::delete(self.executor(), opts) - } - - pub fn nostr_profile_relay_set( - &self, - opts: &INostrProfileRelayRelation, - ) -> Result<INostrProfileRelayResolve, IError<SqlError>> { - models::nostr_profile_relay::set(self.executor(), opts) - } - - pub fn nostr_profile_relay_unset( - &self, - opts: &INostrProfileRelayRelation, - ) -> Result<INostrProfileRelayResolve, IError<SqlError>> { - models::nostr_profile_relay::unset(self.executor(), opts) - } - - pub fn trade_product_location_set( - &self, - opts: &ITradeProductLocationRelation, - ) -> Result<ITradeProductLocationResolve, IError<SqlError>> { - models::trade_product_location::set(self.executor(), opts) - } - - pub fn trade_product_location_unset( - &self, - opts: &ITradeProductLocationRelation, - ) -> Result<ITradeProductLocationResolve, IError<SqlError>> { - models::trade_product_location::unset(self.executor(), opts) - } - - pub fn trade_product_media_set( - &self, - opts: &ITradeProductMediaRelation, - ) -> Result<ITradeProductMediaResolve, IError<SqlError>> { - models::trade_product_media::set(self.executor(), opts) - } - - pub fn trade_product_media_unset( - &self, - opts: &ITradeProductMediaRelation, - ) -> Result<ITradeProductMediaResolve, IError<SqlError>> { - models::trade_product_media::unset(self.executor(), opts) - } -} - -#[cfg(feature = "coverage-minimal")] -impl<E: SqlExecutor> TangleSql<E> { - pub fn new(executor: E) -> Self { - Self { executor } - } - - pub fn executor(&self) -> &E { - &self.executor - } -} - -#[cfg(test)] -mod tests { - use super::TangleSql; - use radroots_sql_core::{ExecOutcome, SqlError, SqlExecutor}; - - struct ProbeExecutor; - - impl SqlExecutor for ProbeExecutor { - fn exec(&self, _sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> { - Ok(ExecOutcome { - changes: 0, - last_insert_id: 0, - }) - } - - fn query_raw(&self, _sql: &str, _params_json: &str) -> Result<String, SqlError> { - Ok("[]".to_string()) - } - - fn begin(&self) -> Result<(), SqlError> { - Ok(()) - } - - fn commit(&self) -> Result<(), SqlError> { - Ok(()) - } - - fn rollback(&self) -> Result<(), SqlError> { - Ok(()) - } - } - - #[test] - fn tangle_sql_constructor_and_executor_access_are_supported() { - let db = TangleSql::new(ProbeExecutor); - let exec = db.executor(); - assert!(exec.exec("select 1", "[]").is_ok()); - assert!(exec.query_raw("select 1", "[]").is_ok()); - assert!(exec.begin().is_ok()); - assert!(exec.commit().is_ok()); - assert!(exec.rollback().is_ok()); - assert_eq!( - TangleSql::<ProbeExecutor>::coverage_branch_probe(true), - "enabled" - ); - assert_eq!( - TangleSql::<ProbeExecutor>::coverage_branch_probe(false), - "disabled" - ); - } -} diff --git a/crates/tangle-db/src/models/farm.rs b/crates/tangle-db/src/models/farm.rs @@ -1,145 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::farm::{ - Farm, FarmQueryBindValues, IFarmCreate, IFarmCreateResolve, IFarmDelete, IFarmDeleteResolve, - IFarmFieldsFilter, IFarmFindMany, IFarmFindManyResolve, IFarmFindOne, IFarmFindOneResolve, - IFarmUpdate, IFarmUpdateResolve, -}; -use radroots_types::types::{IError, IResult, IResultList}; -use serde_json::Value; - -const TABLE_NAME: &str = "farm"; - -pub fn create<E: SqlExecutor>( - exec: &E, - opts: &IFarmCreate, -) -> Result<IFarmCreateResolve, IError<SqlError>> { - let field_map = utils::to_object_map(opts)?; - let id = utils::uuidv4(); - let now = utils::time_created_on(); - let meta: [(&str, Value); 3] = [ - ("id", Value::from(id.clone())), - ("created_at", Value::from(now.clone())), - ("updated_at", Value::from(now.clone())), - ]; - let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); - let params_json = utils::to_params_json(bind_values)?; - let _ = exec.exec(&sql, ¶ms_json)?; - let on = FarmQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; - Ok(IResult { result }) -} - -pub fn find_one<E: SqlExecutor>( - exec: &E, - opts: &IFarmFindOne, -) -> Result<IFarmFindOneResolve, IError<SqlError>> { - let result = match opts { - IFarmFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(IResult { result }) -} - -pub fn find_many<E: SqlExecutor>( - exec: &E, - opts: &IFarmFindMany, -) -> Result<IFarmFindManyResolve, IError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(IResultList { results }) -} - -fn find_many_filter<E: SqlExecutor>( - exec: &E, - filter: &Option<IFarmFieldsFilter>, -) -> Result<Vec<Farm>, IError<SqlError>> { - let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); - let params_json = utils::to_params_json(bind_values)?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<Farm> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on<E: SqlExecutor>( - exec: &E, - on: &FarmQueryBindValues, -) -> Result<Option<Farm>, IError<SqlError>> { - let (column, value) = on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value])?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<Farm> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<Farm, IError<SqlError>> { - let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<Farm> = utils::parse_json(&json)?; - rows.pop() - .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update<E: SqlExecutor>( - exec: &E, - opts: &IFarmUpdate, -) -> Result<IFarmUpdateResolve, IError<SqlError>> { - let mut updates = utils::to_partial_object_map(&opts.fields)?; - if updates.is_empty() { - return Err(IError::from(SqlError::InvalidArgument(String::from( - "no fields to update", - )))); - } - updates.insert( - String::from("updated_at"), - Value::from(utils::time_created_on()), - ); - let mut set_parts = Vec::with_capacity(updates.len()); - let mut bind_values = Vec::with_capacity(updates.len() + 1); - for (column, value) in updates { - set_parts.push(format!("{column} = ?")); - bind_values.push(utils::to_db_bind_value(&value)); - } - let id_for_lookup = match opts.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &opts.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; - model.id - } - }; - 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 _ = exec.exec(&sql, ¶ms_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(IResult { result: updated }) -} - -pub fn delete<E: SqlExecutor>( - exec: &E, - opts: &IFarmDelete, -) -> Result<IFarmDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts { - IFarmDelete::On(args) => match args.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &args.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, ¶ms_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } - Ok(IResult { - result: id_for_lookup, - }) -} diff --git a/crates/tangle-db/src/models/farm_gcs_location.rs b/crates/tangle-db/src/models/farm_gcs_location.rs @@ -1,147 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::farm_gcs_location::{ - FarmGcsLocation, FarmGcsLocationQueryBindValues, IFarmGcsLocationCreate, - IFarmGcsLocationCreateResolve, IFarmGcsLocationDelete, IFarmGcsLocationDeleteResolve, - IFarmGcsLocationFieldsFilter, IFarmGcsLocationFindMany, IFarmGcsLocationFindManyResolve, - IFarmGcsLocationFindOne, IFarmGcsLocationFindOneResolve, IFarmGcsLocationUpdate, - IFarmGcsLocationUpdateResolve, -}; -use radroots_types::types::{IError, IResult, IResultList}; -use serde_json::Value; - -const TABLE_NAME: &str = "farm_gcs_location"; - -pub fn create<E: SqlExecutor>( - exec: &E, - opts: &IFarmGcsLocationCreate, -) -> Result<IFarmGcsLocationCreateResolve, IError<SqlError>> { - let field_map = utils::to_object_map(opts)?; - let id = utils::uuidv4(); - let now = utils::time_created_on(); - let meta: [(&str, Value); 3] = [ - ("id", Value::from(id.clone())), - ("created_at", Value::from(now.clone())), - ("updated_at", Value::from(now.clone())), - ]; - let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); - let params_json = utils::to_params_json(bind_values)?; - let _ = exec.exec(&sql, ¶ms_json)?; - let on = FarmGcsLocationQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; - Ok(IResult { result }) -} - -pub fn find_one<E: SqlExecutor>( - exec: &E, - opts: &IFarmGcsLocationFindOne, -) -> Result<IFarmGcsLocationFindOneResolve, IError<SqlError>> { - let result = match opts { - IFarmGcsLocationFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(IResult { result }) -} - -pub fn find_many<E: SqlExecutor>( - exec: &E, - opts: &IFarmGcsLocationFindMany, -) -> Result<IFarmGcsLocationFindManyResolve, IError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(IResultList { results }) -} - -fn find_many_filter<E: SqlExecutor>( - exec: &E, - filter: &Option<IFarmGcsLocationFieldsFilter>, -) -> Result<Vec<FarmGcsLocation>, IError<SqlError>> { - let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); - let params_json = utils::to_params_json(bind_values)?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<FarmGcsLocation> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on<E: SqlExecutor>( - exec: &E, - on: &FarmGcsLocationQueryBindValues, -) -> Result<Option<FarmGcsLocation>, IError<SqlError>> { - let (column, value) = on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value])?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<FarmGcsLocation> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<FarmGcsLocation, IError<SqlError>> { - let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<FarmGcsLocation> = utils::parse_json(&json)?; - rows.pop() - .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update<E: SqlExecutor>( - exec: &E, - opts: &IFarmGcsLocationUpdate, -) -> Result<IFarmGcsLocationUpdateResolve, IError<SqlError>> { - let mut updates = utils::to_partial_object_map(&opts.fields)?; - if updates.is_empty() { - return Err(IError::from(SqlError::InvalidArgument(String::from( - "no fields to update", - )))); - } - updates.insert( - String::from("updated_at"), - Value::from(utils::time_created_on()), - ); - let mut set_parts = Vec::with_capacity(updates.len()); - let mut bind_values = Vec::with_capacity(updates.len() + 1); - for (column, value) in updates { - set_parts.push(format!("{column} = ?")); - bind_values.push(utils::to_db_bind_value(&value)); - } - let id_for_lookup = match opts.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &opts.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; - model.id - } - }; - 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 _ = exec.exec(&sql, ¶ms_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(IResult { result: updated }) -} - -pub fn delete<E: SqlExecutor>( - exec: &E, - opts: &IFarmGcsLocationDelete, -) -> Result<IFarmGcsLocationDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts { - IFarmGcsLocationDelete::On(args) => match args.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &args.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, ¶ms_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } - Ok(IResult { - result: id_for_lookup, - }) -} diff --git a/crates/tangle-db/src/models/farm_member.rs b/crates/tangle-db/src/models/farm_member.rs @@ -1,146 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::farm_member::{ - FarmMember, FarmMemberQueryBindValues, IFarmMemberCreate, IFarmMemberCreateResolve, - IFarmMemberDelete, IFarmMemberDeleteResolve, IFarmMemberFieldsFilter, IFarmMemberFindMany, - IFarmMemberFindManyResolve, IFarmMemberFindOne, IFarmMemberFindOneResolve, IFarmMemberUpdate, - IFarmMemberUpdateResolve, -}; -use radroots_types::types::{IError, IResult, IResultList}; -use serde_json::Value; - -const TABLE_NAME: &str = "farm_member"; - -pub fn create<E: SqlExecutor>( - exec: &E, - opts: &IFarmMemberCreate, -) -> Result<IFarmMemberCreateResolve, IError<SqlError>> { - let field_map = utils::to_object_map(opts)?; - let id = utils::uuidv4(); - let now = utils::time_created_on(); - let meta: [(&str, Value); 3] = [ - ("id", Value::from(id.clone())), - ("created_at", Value::from(now.clone())), - ("updated_at", Value::from(now.clone())), - ]; - let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); - let params_json = utils::to_params_json(bind_values)?; - let _ = exec.exec(&sql, ¶ms_json)?; - let on = FarmMemberQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; - Ok(IResult { result }) -} - -pub fn find_one<E: SqlExecutor>( - exec: &E, - opts: &IFarmMemberFindOne, -) -> Result<IFarmMemberFindOneResolve, IError<SqlError>> { - let result = match opts { - IFarmMemberFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(IResult { result }) -} - -pub fn find_many<E: SqlExecutor>( - exec: &E, - opts: &IFarmMemberFindMany, -) -> Result<IFarmMemberFindManyResolve, IError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(IResultList { results }) -} - -fn find_many_filter<E: SqlExecutor>( - exec: &E, - filter: &Option<IFarmMemberFieldsFilter>, -) -> Result<Vec<FarmMember>, IError<SqlError>> { - let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); - let params_json = utils::to_params_json(bind_values)?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<FarmMember> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on<E: SqlExecutor>( - exec: &E, - on: &FarmMemberQueryBindValues, -) -> Result<Option<FarmMember>, IError<SqlError>> { - let (column, value) = on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value])?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<FarmMember> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<FarmMember, IError<SqlError>> { - let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<FarmMember> = utils::parse_json(&json)?; - rows.pop() - .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update<E: SqlExecutor>( - exec: &E, - opts: &IFarmMemberUpdate, -) -> Result<IFarmMemberUpdateResolve, IError<SqlError>> { - let mut updates = utils::to_partial_object_map(&opts.fields)?; - if updates.is_empty() { - return Err(IError::from(SqlError::InvalidArgument(String::from( - "no fields to update", - )))); - } - updates.insert( - String::from("updated_at"), - Value::from(utils::time_created_on()), - ); - let mut set_parts = Vec::with_capacity(updates.len()); - let mut bind_values = Vec::with_capacity(updates.len() + 1); - for (column, value) in updates { - set_parts.push(format!("{column} = ?")); - bind_values.push(utils::to_db_bind_value(&value)); - } - let id_for_lookup = match opts.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &opts.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; - model.id - } - }; - 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 _ = exec.exec(&sql, ¶ms_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(IResult { result: updated }) -} - -pub fn delete<E: SqlExecutor>( - exec: &E, - opts: &IFarmMemberDelete, -) -> Result<IFarmMemberDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts { - IFarmMemberDelete::On(args) => match args.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &args.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, ¶ms_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } - Ok(IResult { - result: id_for_lookup, - }) -} diff --git a/crates/tangle-db/src/models/farm_member_claim.rs b/crates/tangle-db/src/models/farm_member_claim.rs @@ -1,147 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::farm_member_claim::{ - FarmMemberClaim, FarmMemberClaimQueryBindValues, IFarmMemberClaimCreate, - IFarmMemberClaimCreateResolve, IFarmMemberClaimDelete, IFarmMemberClaimDeleteResolve, - IFarmMemberClaimFieldsFilter, IFarmMemberClaimFindMany, IFarmMemberClaimFindManyResolve, - IFarmMemberClaimFindOne, IFarmMemberClaimFindOneResolve, IFarmMemberClaimUpdate, - IFarmMemberClaimUpdateResolve, -}; -use radroots_types::types::{IError, IResult, IResultList}; -use serde_json::Value; - -const TABLE_NAME: &str = "farm_member_claim"; - -pub fn create<E: SqlExecutor>( - exec: &E, - opts: &IFarmMemberClaimCreate, -) -> Result<IFarmMemberClaimCreateResolve, IError<SqlError>> { - let field_map = utils::to_object_map(opts)?; - let id = utils::uuidv4(); - let now = utils::time_created_on(); - let meta: [(&str, Value); 3] = [ - ("id", Value::from(id.clone())), - ("created_at", Value::from(now.clone())), - ("updated_at", Value::from(now.clone())), - ]; - let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); - let params_json = utils::to_params_json(bind_values)?; - let _ = exec.exec(&sql, ¶ms_json)?; - let on = FarmMemberClaimQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; - Ok(IResult { result }) -} - -pub fn find_one<E: SqlExecutor>( - exec: &E, - opts: &IFarmMemberClaimFindOne, -) -> Result<IFarmMemberClaimFindOneResolve, IError<SqlError>> { - let result = match opts { - IFarmMemberClaimFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(IResult { result }) -} - -pub fn find_many<E: SqlExecutor>( - exec: &E, - opts: &IFarmMemberClaimFindMany, -) -> Result<IFarmMemberClaimFindManyResolve, IError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(IResultList { results }) -} - -fn find_many_filter<E: SqlExecutor>( - exec: &E, - filter: &Option<IFarmMemberClaimFieldsFilter>, -) -> Result<Vec<FarmMemberClaim>, IError<SqlError>> { - let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); - let params_json = utils::to_params_json(bind_values)?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<FarmMemberClaim> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on<E: SqlExecutor>( - exec: &E, - on: &FarmMemberClaimQueryBindValues, -) -> Result<Option<FarmMemberClaim>, IError<SqlError>> { - let (column, value) = on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value])?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<FarmMemberClaim> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<FarmMemberClaim, IError<SqlError>> { - let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<FarmMemberClaim> = utils::parse_json(&json)?; - rows.pop() - .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update<E: SqlExecutor>( - exec: &E, - opts: &IFarmMemberClaimUpdate, -) -> Result<IFarmMemberClaimUpdateResolve, IError<SqlError>> { - let mut updates = utils::to_partial_object_map(&opts.fields)?; - if updates.is_empty() { - return Err(IError::from(SqlError::InvalidArgument(String::from( - "no fields to update", - )))); - } - updates.insert( - String::from("updated_at"), - Value::from(utils::time_created_on()), - ); - let mut set_parts = Vec::with_capacity(updates.len()); - let mut bind_values = Vec::with_capacity(updates.len() + 1); - for (column, value) in updates { - set_parts.push(format!("{column} = ?")); - bind_values.push(utils::to_db_bind_value(&value)); - } - let id_for_lookup = match opts.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &opts.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; - model.id - } - }; - 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 _ = exec.exec(&sql, ¶ms_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(IResult { result: updated }) -} - -pub fn delete<E: SqlExecutor>( - exec: &E, - opts: &IFarmMemberClaimDelete, -) -> Result<IFarmMemberClaimDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts { - IFarmMemberClaimDelete::On(args) => match args.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &args.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, ¶ms_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } - Ok(IResult { - result: id_for_lookup, - }) -} diff --git a/crates/tangle-db/src/models/farm_tag.rs b/crates/tangle-db/src/models/farm_tag.rs @@ -1,145 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::farm_tag::{ - FarmTag, FarmTagQueryBindValues, IFarmTagCreate, IFarmTagCreateResolve, IFarmTagDelete, - IFarmTagDeleteResolve, IFarmTagFieldsFilter, IFarmTagFindMany, IFarmTagFindManyResolve, - IFarmTagFindOne, IFarmTagFindOneResolve, IFarmTagUpdate, IFarmTagUpdateResolve, -}; -use radroots_types::types::{IError, IResult, IResultList}; -use serde_json::Value; - -const TABLE_NAME: &str = "farm_tag"; - -pub fn create<E: SqlExecutor>( - exec: &E, - opts: &IFarmTagCreate, -) -> Result<IFarmTagCreateResolve, IError<SqlError>> { - let field_map = utils::to_object_map(opts)?; - let id = utils::uuidv4(); - let now = utils::time_created_on(); - let meta: [(&str, Value); 3] = [ - ("id", Value::from(id.clone())), - ("created_at", Value::from(now.clone())), - ("updated_at", Value::from(now.clone())), - ]; - let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); - let params_json = utils::to_params_json(bind_values)?; - let _ = exec.exec(&sql, ¶ms_json)?; - let on = FarmTagQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; - Ok(IResult { result }) -} - -pub fn find_one<E: SqlExecutor>( - exec: &E, - opts: &IFarmTagFindOne, -) -> Result<IFarmTagFindOneResolve, IError<SqlError>> { - let result = match opts { - IFarmTagFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(IResult { result }) -} - -pub fn find_many<E: SqlExecutor>( - exec: &E, - opts: &IFarmTagFindMany, -) -> Result<IFarmTagFindManyResolve, IError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(IResultList { results }) -} - -fn find_many_filter<E: SqlExecutor>( - exec: &E, - filter: &Option<IFarmTagFieldsFilter>, -) -> Result<Vec<FarmTag>, IError<SqlError>> { - let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); - let params_json = utils::to_params_json(bind_values)?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<FarmTag> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on<E: SqlExecutor>( - exec: &E, - on: &FarmTagQueryBindValues, -) -> Result<Option<FarmTag>, IError<SqlError>> { - let (column, value) = on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value])?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<FarmTag> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<FarmTag, IError<SqlError>> { - let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<FarmTag> = utils::parse_json(&json)?; - rows.pop() - .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update<E: SqlExecutor>( - exec: &E, - opts: &IFarmTagUpdate, -) -> Result<IFarmTagUpdateResolve, IError<SqlError>> { - let mut updates = utils::to_partial_object_map(&opts.fields)?; - if updates.is_empty() { - return Err(IError::from(SqlError::InvalidArgument(String::from( - "no fields to update", - )))); - } - updates.insert( - String::from("updated_at"), - Value::from(utils::time_created_on()), - ); - let mut set_parts = Vec::with_capacity(updates.len()); - let mut bind_values = Vec::with_capacity(updates.len() + 1); - for (column, value) in updates { - set_parts.push(format!("{column} = ?")); - bind_values.push(utils::to_db_bind_value(&value)); - } - let id_for_lookup = match opts.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &opts.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; - model.id - } - }; - 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 _ = exec.exec(&sql, ¶ms_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(IResult { result: updated }) -} - -pub fn delete<E: SqlExecutor>( - exec: &E, - opts: &IFarmTagDelete, -) -> Result<IFarmTagDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts { - IFarmTagDelete::On(args) => match args.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &args.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, ¶ms_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } - Ok(IResult { - result: id_for_lookup, - }) -} diff --git a/crates/tangle-db/src/models/gcs_location.rs b/crates/tangle-db/src/models/gcs_location.rs @@ -1,223 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::gcs_location::{ - GcsLocation, GcsLocationFindManyRel, GcsLocationQueryBindValues, IGcsLocationCreate, - IGcsLocationCreateResolve, IGcsLocationDelete, IGcsLocationDeleteResolve, - IGcsLocationFieldsFilter, IGcsLocationFindMany, IGcsLocationFindManyResolve, - IGcsLocationFindOne, IGcsLocationFindOneResolve, IGcsLocationUpdate, IGcsLocationUpdateResolve, -}; -use radroots_types::types::{IError, IResult, IResultList}; -use serde_json::Value; - -const TABLE_NAME: &str = "gcs_location"; - -pub fn create<E: SqlExecutor>( - exec: &E, - opts: &IGcsLocationCreate, -) -> Result<IGcsLocationCreateResolve, IError<SqlError>> { - let field_map = utils::to_object_map(opts)?; - let id = utils::uuidv4(); - let now = utils::time_created_on(); - let meta: [(&str, Value); 3] = [ - ("id", Value::from(id.clone())), - ("created_at", Value::from(now.clone())), - ("updated_at", Value::from(now.clone())), - ]; - let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); - let params_json = utils::to_params_json(bind_values)?; - let _ = exec.exec(&sql, ¶ms_json)?; - let on = GcsLocationQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; - Ok(IResult { result }) -} - -pub fn find_one<E: SqlExecutor>( - exec: &E, - opts: &IGcsLocationFindOne, -) -> Result<IGcsLocationFindOneResolve, IError<SqlError>> { - let result = match opts { - IGcsLocationFindOne::On(args) => find_one_by_on(exec, &args.on)?, - IGcsLocationFindOne::Rel(args) => find_one_by_rel(exec, &args.rel)?, - }; - Ok(IResult { result }) -} - -pub fn find_many<E: SqlExecutor>( - exec: &E, - opts: &IGcsLocationFindMany, -) -> Result<IGcsLocationFindManyResolve, IError<SqlError>> { - let results = match opts { - IGcsLocationFindMany::Filter { filter } => find_many_filter(exec, filter)?, - IGcsLocationFindMany::Rel { rel } => find_many_by_rel(exec, rel)?, - }; - Ok(IResultList { results }) -} - -fn find_many_filter<E: SqlExecutor>( - exec: &E, - filter: &Option<IGcsLocationFieldsFilter>, -) -> Result<Vec<GcsLocation>, IError<SqlError>> { - let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); - let params_json = utils::to_params_json(bind_values)?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<GcsLocation> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on<E: SqlExecutor>( - exec: &E, - on: &GcsLocationQueryBindValues, -) -> Result<Option<GcsLocation>, IError<SqlError>> { - let (column, value) = on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value])?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<GcsLocation> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn rel_query(rel: &GcsLocationFindManyRel) -> (&'static str, Vec<Value>) { - match rel { - GcsLocationFindManyRel::OnTradeProduct(args) => ( - "SELECT gl.* FROM gcs_location gl JOIN trade_product_location tp_gl ON gl.id = tp_gl.tb_gl WHERE tp_gl.tb_tp = ?", - vec![Value::from(args.id.clone())], - ), - GcsLocationFindManyRel::OffTradeProduct(args) => ( - "SELECT gl.* FROM gcs_location gl WHERE NOT EXISTS (SELECT 1 FROM trade_product_location tp_gl WHERE tp_gl.tb_gl = gl.id AND tp_gl.tb_tp = ?)", - vec![Value::from(args.id.clone())], - ), - GcsLocationFindManyRel::OnFarm(args) => ( - "SELECT gl.* FROM gcs_location gl JOIN farm_gcs_location fgcs ON gl.id = fgcs.gcs_location_id WHERE fgcs.farm_id = ?", - vec![Value::from(args.id.clone())], - ), - GcsLocationFindManyRel::OffFarm(args) => ( - "SELECT gl.* FROM gcs_location gl WHERE NOT EXISTS (SELECT 1 FROM farm_gcs_location fgcs WHERE fgcs.gcs_location_id = gl.id AND fgcs.farm_id = ?)", - vec![Value::from(args.id.clone())], - ), - GcsLocationFindManyRel::OnPlot(args) => ( - "SELECT gl.* FROM gcs_location gl JOIN plot_gcs_location pgcs ON gl.id = pgcs.gcs_location_id WHERE pgcs.plot_id = ?", - vec![Value::from(args.id.clone())], - ), - GcsLocationFindManyRel::OffPlot(args) => ( - "SELECT gl.* FROM gcs_location gl WHERE NOT EXISTS (SELECT 1 FROM plot_gcs_location pgcs WHERE pgcs.gcs_location_id = gl.id AND pgcs.plot_id = ?)", - vec![Value::from(args.id.clone())], - ), - } -} - -fn find_one_by_rel<E: SqlExecutor>( - exec: &E, - rel: &GcsLocationFindManyRel, -) -> Result<Option<GcsLocation>, IError<SqlError>> { - let (sql, bind_values) = rel_query(rel); - let params_json = utils::to_params_json(bind_values)?; - let sql = format!("{sql} LIMIT 1;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<GcsLocation> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn find_many_by_rel<E: SqlExecutor>( - exec: &E, - rel: &GcsLocationFindManyRel, -) -> Result<Vec<GcsLocation>, IError<SqlError>> { - let (sql, bind_values) = rel_query(rel); - let params_json = utils::to_params_json(bind_values)?; - let sql = format!("{sql};"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<GcsLocation> = utils::parse_json(&json)?; - Ok(rows) -} - -fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<GcsLocation, IError<SqlError>> { - let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<GcsLocation> = utils::parse_json(&json)?; - rows.pop() - .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update<E: SqlExecutor>( - exec: &E, - opts: &IGcsLocationUpdate, -) -> Result<IGcsLocationUpdateResolve, IError<SqlError>> { - let mut updates = utils::to_partial_object_map(&opts.fields)?; - if updates.is_empty() { - return Err(IError::from(SqlError::InvalidArgument(String::from( - "no fields to update", - )))); - } - updates.insert( - String::from("updated_at"), - Value::from(utils::time_created_on()), - ); - let mut set_parts = Vec::with_capacity(updates.len()); - let mut bind_values = Vec::with_capacity(updates.len() + 1); - for (column, value) in updates { - set_parts.push(format!("{column} = ?")); - bind_values.push(utils::to_db_bind_value(&value)); - } - let id_for_lookup = match opts.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &opts.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; - model.id - } - }; - 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 _ = exec.exec(&sql, ¶ms_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(IResult { result: updated }) -} - -pub fn delete<E: SqlExecutor>( - exec: &E, - opts: &IGcsLocationDelete, -) -> Result<IGcsLocationDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts { - IGcsLocationDelete::On(args) => match args.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &args.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; - model.id - } - }, - IGcsLocationDelete::Rel(args) => { - let found = find_one_by_rel(exec, &args.rel)?; - let model = found.ok_or(IError::from(SqlError::NotFound(rel_lookup_key(&args.rel))))?; - model.id - } - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, ¶ms_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } - Ok(IResult { - result: id_for_lookup, - }) -} - -fn rel_lookup_key(rel: &GcsLocationFindManyRel) -> String { - match rel { - GcsLocationFindManyRel::OnTradeProduct(args) => { - format!("on_trade_product:{}", args.id.as_str()) - } - GcsLocationFindManyRel::OffTradeProduct(args) => { - format!("off_trade_product:{}", args.id.as_str()) - } - GcsLocationFindManyRel::OnFarm(args) => format!("on_farm:{}", args.id.as_str()), - GcsLocationFindManyRel::OffFarm(args) => format!("off_farm:{}", args.id.as_str()), - GcsLocationFindManyRel::OnPlot(args) => format!("on_plot:{}", args.id.as_str()), - GcsLocationFindManyRel::OffPlot(args) => format!("off_plot:{}", args.id.as_str()), - } -} diff --git a/crates/tangle-db/src/models/log_error.rs b/crates/tangle-db/src/models/log_error.rs @@ -1,146 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::log_error::{ - ILogErrorCreate, ILogErrorCreateResolve, ILogErrorDelete, ILogErrorDeleteResolve, - ILogErrorFieldsFilter, ILogErrorFindMany, ILogErrorFindManyResolve, ILogErrorFindOne, - ILogErrorFindOneResolve, ILogErrorUpdate, ILogErrorUpdateResolve, LogError, - LogErrorQueryBindValues, -}; -use radroots_types::types::{IError, IResult, IResultList}; -use serde_json::Value; - -const TABLE_NAME: &str = "log_error"; - -pub fn create<E: SqlExecutor>( - exec: &E, - opts: &ILogErrorCreate, -) -> Result<ILogErrorCreateResolve, IError<SqlError>> { - let field_map = utils::to_object_map(opts)?; - let id = utils::uuidv4(); - let now = utils::time_created_on(); - let meta: [(&str, Value); 3] = [ - ("id", Value::from(id.clone())), - ("created_at", Value::from(now.clone())), - ("updated_at", Value::from(now.clone())), - ]; - let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); - let params_json = utils::to_params_json(bind_values)?; - let _ = exec.exec(&sql, ¶ms_json)?; - let on = LogErrorQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; - Ok(IResult { result }) -} - -pub fn find_one<E: SqlExecutor>( - exec: &E, - opts: &ILogErrorFindOne, -) -> Result<ILogErrorFindOneResolve, IError<SqlError>> { - let result = match opts { - ILogErrorFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(IResult { result }) -} - -pub fn find_many<E: SqlExecutor>( - exec: &E, - opts: &ILogErrorFindMany, -) -> Result<ILogErrorFindManyResolve, IError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(IResultList { results }) -} - -fn find_many_filter<E: SqlExecutor>( - exec: &E, - filter: &Option<ILogErrorFieldsFilter>, -) -> Result<Vec<LogError>, IError<SqlError>> { - let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); - let params_json = utils::to_params_json(bind_values)?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<LogError> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on<E: SqlExecutor>( - exec: &E, - on: &LogErrorQueryBindValues, -) -> Result<Option<LogError>, IError<SqlError>> { - let (column, value) = on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value])?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<LogError> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<LogError, IError<SqlError>> { - let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<LogError> = utils::parse_json(&json)?; - rows.pop() - .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update<E: SqlExecutor>( - exec: &E, - opts: &ILogErrorUpdate, -) -> Result<ILogErrorUpdateResolve, IError<SqlError>> { - let mut updates = utils::to_partial_object_map(&opts.fields)?; - if updates.is_empty() { - return Err(IError::from(SqlError::InvalidArgument(String::from( - "no fields to update", - )))); - } - updates.insert( - String::from("updated_at"), - Value::from(utils::time_created_on()), - ); - let mut set_parts = Vec::with_capacity(updates.len()); - let mut bind_values = Vec::with_capacity(updates.len() + 1); - for (column, value) in updates { - set_parts.push(format!("{column} = ?")); - bind_values.push(utils::to_db_bind_value(&value)); - } - let id_for_lookup = match opts.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &opts.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; - model.id - } - }; - 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 _ = exec.exec(&sql, ¶ms_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(IResult { result: updated }) -} - -pub fn delete<E: SqlExecutor>( - exec: &E, - opts: &ILogErrorDelete, -) -> Result<ILogErrorDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts { - ILogErrorDelete::On(args) => match args.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &args.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, ¶ms_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } - Ok(IResult { - result: id_for_lookup, - }) -} diff --git a/crates/tangle-db/src/models/media_image.rs b/crates/tangle-db/src/models/media_image.rs @@ -1,203 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::media_image::{ - IMediaImageCreate, IMediaImageCreateResolve, IMediaImageDelete, IMediaImageDeleteResolve, - IMediaImageFieldsFilter, IMediaImageFindMany, IMediaImageFindManyResolve, IMediaImageFindOne, - IMediaImageFindOneResolve, IMediaImageUpdate, IMediaImageUpdateResolve, MediaImage, - MediaImageFindManyRel, MediaImageQueryBindValues, -}; -use radroots_types::types::{IError, IResult, IResultList}; -use serde_json::Value; - -const TABLE_NAME: &str = "media_image"; - -pub fn create<E: SqlExecutor>( - exec: &E, - opts: &IMediaImageCreate, -) -> Result<IMediaImageCreateResolve, IError<SqlError>> { - let field_map = utils::to_object_map(opts)?; - let id = utils::uuidv4(); - let now = utils::time_created_on(); - let meta: [(&str, Value); 3] = [ - ("id", Value::from(id.clone())), - ("created_at", Value::from(now.clone())), - ("updated_at", Value::from(now.clone())), - ]; - let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); - let params_json = utils::to_params_json(bind_values)?; - let _ = exec.exec(&sql, ¶ms_json)?; - let on = MediaImageQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; - Ok(IResult { result }) -} - -pub fn find_one<E: SqlExecutor>( - exec: &E, - opts: &IMediaImageFindOne, -) -> Result<IMediaImageFindOneResolve, IError<SqlError>> { - let result = match opts { - IMediaImageFindOne::On(args) => find_one_by_on(exec, &args.on)?, - IMediaImageFindOne::Rel(args) => find_one_by_rel(exec, &args.rel)?, - }; - Ok(IResult { result }) -} - -pub fn find_many<E: SqlExecutor>( - exec: &E, - opts: &IMediaImageFindMany, -) -> Result<IMediaImageFindManyResolve, IError<SqlError>> { - let results = match opts { - IMediaImageFindMany::Filter { filter } => find_many_filter(exec, filter)?, - IMediaImageFindMany::Rel { rel } => find_many_by_rel(exec, rel)?, - }; - Ok(IResultList { results }) -} - -fn find_many_filter<E: SqlExecutor>( - exec: &E, - filter: &Option<IMediaImageFieldsFilter>, -) -> Result<Vec<MediaImage>, IError<SqlError>> { - let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); - let params_json = utils::to_params_json(bind_values)?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<MediaImage> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on<E: SqlExecutor>( - exec: &E, - on: &MediaImageQueryBindValues, -) -> Result<Option<MediaImage>, IError<SqlError>> { - let (column, value) = on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value])?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<MediaImage> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn rel_query(rel: &MediaImageFindManyRel) -> (&'static str, Vec<Value>) { - match rel { - MediaImageFindManyRel::OnTradeProduct(args) => ( - "SELECT mu.* FROM media_image mu JOIN trade_product_media tp_lg ON mu.id = tp_lg.tb_mu WHERE tp_lg.tb_tp = ?", - vec![Value::from(args.id.clone())], - ), - MediaImageFindManyRel::OffTradeProduct(args) => ( - "SELECT mu.* FROM media_image mu WHERE NOT EXISTS (SELECT 1 FROM trade_product_media tp_lg WHERE tp_lg.tb_mu = mu.id AND tp_lg.tb_tp = ?)", - vec![Value::from(args.id.clone())], - ), - } -} - -fn find_one_by_rel<E: SqlExecutor>( - exec: &E, - rel: &MediaImageFindManyRel, -) -> Result<Option<MediaImage>, IError<SqlError>> { - let (sql, bind_values) = rel_query(rel); - let params_json = utils::to_params_json(bind_values)?; - let sql = format!("{sql} LIMIT 1;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<MediaImage> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn find_many_by_rel<E: SqlExecutor>( - exec: &E, - rel: &MediaImageFindManyRel, -) -> Result<Vec<MediaImage>, IError<SqlError>> { - let (sql, bind_values) = rel_query(rel); - let params_json = utils::to_params_json(bind_values)?; - let sql = format!("{sql};"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<MediaImage> = utils::parse_json(&json)?; - Ok(rows) -} - -fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<MediaImage, IError<SqlError>> { - let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<MediaImage> = utils::parse_json(&json)?; - rows.pop() - .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update<E: SqlExecutor>( - exec: &E, - opts: &IMediaImageUpdate, -) -> Result<IMediaImageUpdateResolve, IError<SqlError>> { - let mut updates = utils::to_partial_object_map(&opts.fields)?; - if updates.is_empty() { - return Err(IError::from(SqlError::InvalidArgument(String::from( - "no fields to update", - )))); - } - updates.insert( - String::from("updated_at"), - Value::from(utils::time_created_on()), - ); - let mut set_parts = Vec::with_capacity(updates.len()); - let mut bind_values = Vec::with_capacity(updates.len() + 1); - for (column, value) in updates { - set_parts.push(format!("{column} = ?")); - bind_values.push(utils::to_db_bind_value(&value)); - } - let id_for_lookup = match opts.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &opts.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; - model.id - } - }; - 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 _ = exec.exec(&sql, ¶ms_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(IResult { result: updated }) -} - -pub fn delete<E: SqlExecutor>( - exec: &E, - opts: &IMediaImageDelete, -) -> Result<IMediaImageDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts { - IMediaImageDelete::On(args) => match args.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &args.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; - model.id - } - }, - IMediaImageDelete::Rel(args) => { - let found = find_one_by_rel(exec, &args.rel)?; - let model = found.ok_or(IError::from(SqlError::NotFound(rel_lookup_key(&args.rel))))?; - model.id - } - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, ¶ms_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } - Ok(IResult { - result: id_for_lookup, - }) -} - -fn rel_lookup_key(rel: &MediaImageFindManyRel) -> String { - match rel { - MediaImageFindManyRel::OnTradeProduct(args) => { - format!("on_trade_product:{}", args.id.as_str()) - } - MediaImageFindManyRel::OffTradeProduct(args) => { - format!("off_trade_product:{}", args.id.as_str()) - } - } -} diff --git a/crates/tangle-db/src/models/nostr_event_state.rs b/crates/tangle-db/src/models/nostr_event_state.rs @@ -1,147 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::nostr_event_state::{ - INostrEventStateCreate, INostrEventStateCreateResolve, INostrEventStateDelete, - INostrEventStateDeleteResolve, INostrEventStateFieldsFilter, INostrEventStateFindMany, - INostrEventStateFindManyResolve, INostrEventStateFindOne, INostrEventStateFindOneResolve, - INostrEventStateUpdate, INostrEventStateUpdateResolve, NostrEventState, - NostrEventStateQueryBindValues, -}; -use radroots_types::types::{IError, IResult, IResultList}; -use serde_json::Value; - -const TABLE_NAME: &str = "nostr_event_state"; - -pub fn create<E: SqlExecutor>( - exec: &E, - opts: &INostrEventStateCreate, -) -> Result<INostrEventStateCreateResolve, IError<SqlError>> { - let field_map = utils::to_object_map(opts)?; - let id = utils::uuidv4(); - let now = utils::time_created_on(); - let meta: [(&str, Value); 3] = [ - ("id", Value::from(id.clone())), - ("created_at", Value::from(now.clone())), - ("updated_at", Value::from(now.clone())), - ]; - let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); - let params_json = utils::to_params_json(bind_values)?; - let _ = exec.exec(&sql, ¶ms_json)?; - let on = NostrEventStateQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; - Ok(IResult { result }) -} - -pub fn find_one<E: SqlExecutor>( - exec: &E, - opts: &INostrEventStateFindOne, -) -> Result<INostrEventStateFindOneResolve, IError<SqlError>> { - let result = match opts { - INostrEventStateFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(IResult { result }) -} - -pub fn find_many<E: SqlExecutor>( - exec: &E, - opts: &INostrEventStateFindMany, -) -> Result<INostrEventStateFindManyResolve, IError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(IResultList { results }) -} - -fn find_many_filter<E: SqlExecutor>( - exec: &E, - filter: &Option<INostrEventStateFieldsFilter>, -) -> Result<Vec<NostrEventState>, IError<SqlError>> { - let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); - let params_json = utils::to_params_json(bind_values)?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<NostrEventState> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on<E: SqlExecutor>( - exec: &E, - on: &NostrEventStateQueryBindValues, -) -> Result<Option<NostrEventState>, IError<SqlError>> { - let (column, value) = on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value])?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<NostrEventState> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<NostrEventState, IError<SqlError>> { - let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<NostrEventState> = utils::parse_json(&json)?; - rows.pop() - .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update<E: SqlExecutor>( - exec: &E, - opts: &INostrEventStateUpdate, -) -> Result<INostrEventStateUpdateResolve, IError<SqlError>> { - let mut updates = utils::to_partial_object_map(&opts.fields)?; - if updates.is_empty() { - return Err(IError::from(SqlError::InvalidArgument(String::from( - "no fields to update", - )))); - } - updates.insert( - String::from("updated_at"), - Value::from(utils::time_created_on()), - ); - let mut set_parts = Vec::with_capacity(updates.len()); - let mut bind_values = Vec::with_capacity(updates.len() + 1); - for (column, value) in updates { - set_parts.push(format!("{column} = ?")); - bind_values.push(utils::to_db_bind_value(&value)); - } - let id_for_lookup = match opts.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &opts.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; - model.id - } - }; - 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 _ = exec.exec(&sql, ¶ms_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(IResult { result: updated }) -} - -pub fn delete<E: SqlExecutor>( - exec: &E, - opts: &INostrEventStateDelete, -) -> Result<INostrEventStateDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts { - INostrEventStateDelete::On(args) => match args.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &args.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, ¶ms_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } - Ok(IResult { - result: id_for_lookup, - }) -} diff --git a/crates/tangle-db/src/models/nostr_profile.rs b/crates/tangle-db/src/models/nostr_profile.rs @@ -1,200 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::nostr_profile::{ - INostrProfileCreate, INostrProfileCreateResolve, INostrProfileDelete, - INostrProfileDeleteResolve, INostrProfileFieldsFilter, INostrProfileFindMany, - INostrProfileFindManyResolve, INostrProfileFindOne, INostrProfileFindOneResolve, - INostrProfileUpdate, INostrProfileUpdateResolve, NostrProfile, NostrProfileFindManyRel, - NostrProfileQueryBindValues, -}; -use radroots_types::types::{IError, IResult, IResultList}; -use serde_json::Value; - -const TABLE_NAME: &str = "nostr_profile"; - -pub fn create<E: SqlExecutor>( - exec: &E, - opts: &INostrProfileCreate, -) -> Result<INostrProfileCreateResolve, IError<SqlError>> { - let field_map = utils::to_object_map(opts)?; - let id = utils::uuidv4(); - let now = utils::time_created_on(); - let meta: [(&str, Value); 3] = [ - ("id", Value::from(id.clone())), - ("created_at", Value::from(now.clone())), - ("updated_at", Value::from(now.clone())), - ]; - let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); - let params_json = utils::to_params_json(bind_values)?; - let _ = exec.exec(&sql, ¶ms_json)?; - let on = NostrProfileQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; - Ok(IResult { result }) -} - -pub fn find_one<E: SqlExecutor>( - exec: &E, - opts: &INostrProfileFindOne, -) -> Result<INostrProfileFindOneResolve, IError<SqlError>> { - let result = match opts { - INostrProfileFindOne::On(args) => find_one_by_on(exec, &args.on)?, - INostrProfileFindOne::Rel(args) => find_one_by_rel(exec, &args.rel)?, - }; - Ok(IResult { result }) -} - -pub fn find_many<E: SqlExecutor>( - exec: &E, - opts: &INostrProfileFindMany, -) -> Result<INostrProfileFindManyResolve, IError<SqlError>> { - let results = match opts { - INostrProfileFindMany::Filter { filter } => find_many_filter(exec, filter)?, - INostrProfileFindMany::Rel { rel } => find_many_by_rel(exec, rel)?, - }; - Ok(IResultList { results }) -} - -fn find_many_filter<E: SqlExecutor>( - exec: &E, - filter: &Option<INostrProfileFieldsFilter>, -) -> Result<Vec<NostrProfile>, IError<SqlError>> { - let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); - let params_json = utils::to_params_json(bind_values)?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<NostrProfile> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on<E: SqlExecutor>( - exec: &E, - on: &NostrProfileQueryBindValues, -) -> Result<Option<NostrProfile>, IError<SqlError>> { - let (column, value) = on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value])?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<NostrProfile> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn rel_query(rel: &NostrProfileFindManyRel) -> (&'static str, Vec<Value>) { - match rel { - NostrProfileFindManyRel::OnRelay(args) => ( - "SELECT pr.* FROM nostr_profile pr JOIN nostr_profile_relay pr_rl ON pr.id = pr_rl.tb_pr WHERE pr_rl.tb_rl = ?", - vec![Value::from(args.id.clone())], - ), - NostrProfileFindManyRel::OffRelay(args) => ( - "SELECT pr.* FROM nostr_profile pr WHERE NOT EXISTS (SELECT 1 FROM nostr_profile_relay pr_rl WHERE pr_rl.tb_pr = pr.id AND pr_rl.tb_rl = ?)", - vec![Value::from(args.id.clone())], - ), - } -} - -fn find_one_by_rel<E: SqlExecutor>( - exec: &E, - rel: &NostrProfileFindManyRel, -) -> Result<Option<NostrProfile>, IError<SqlError>> { - let (sql, bind_values) = rel_query(rel); - let params_json = utils::to_params_json(bind_values)?; - let sql = format!("{sql} LIMIT 1;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<NostrProfile> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn find_many_by_rel<E: SqlExecutor>( - exec: &E, - rel: &NostrProfileFindManyRel, -) -> Result<Vec<NostrProfile>, IError<SqlError>> { - let (sql, bind_values) = rel_query(rel); - let params_json = utils::to_params_json(bind_values)?; - let sql = format!("{sql};"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<NostrProfile> = utils::parse_json(&json)?; - Ok(rows) -} - -fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<NostrProfile, IError<SqlError>> { - let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<NostrProfile> = utils::parse_json(&json)?; - rows.pop() - .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update<E: SqlExecutor>( - exec: &E, - opts: &INostrProfileUpdate, -) -> Result<INostrProfileUpdateResolve, IError<SqlError>> { - let mut updates = utils::to_partial_object_map(&opts.fields)?; - if updates.is_empty() { - return Err(IError::from(SqlError::InvalidArgument(String::from( - "no fields to update", - )))); - } - updates.insert( - String::from("updated_at"), - Value::from(utils::time_created_on()), - ); - let mut set_parts = Vec::with_capacity(updates.len()); - let mut bind_values = Vec::with_capacity(updates.len() + 1); - for (column, value) in updates { - set_parts.push(format!("{column} = ?")); - bind_values.push(utils::to_db_bind_value(&value)); - } - let id_for_lookup = match opts.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &opts.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; - model.id - } - }; - 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 _ = exec.exec(&sql, ¶ms_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(IResult { result: updated }) -} - -pub fn delete<E: SqlExecutor>( - exec: &E, - opts: &INostrProfileDelete, -) -> Result<INostrProfileDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts { - INostrProfileDelete::On(args) => match args.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &args.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; - model.id - } - }, - INostrProfileDelete::Rel(args) => { - let found = find_one_by_rel(exec, &args.rel)?; - let model = found.ok_or(IError::from(SqlError::NotFound(rel_lookup_key(&args.rel))))?; - model.id - } - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, ¶ms_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } - Ok(IResult { - result: id_for_lookup, - }) -} - -fn rel_lookup_key(rel: &NostrProfileFindManyRel) -> String { - match rel { - NostrProfileFindManyRel::OnRelay(args) => format!("on_relay:{}", args.id.as_str()), - NostrProfileFindManyRel::OffRelay(args) => format!("off_relay:{}", args.id.as_str()), - } -} diff --git a/crates/tangle-db/src/models/nostr_profile_relay.rs b/crates/tangle-db/src/models/nostr_profile_relay.rs @@ -1,45 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::nostr_profile_relay::{ - INostrProfileRelayRelation, INostrProfileRelayResolve, -}; -use radroots_types::types::{IError, IResultPass}; -use serde_json::Value; - -const TABLE_NAME: &str = "nostr_profile_relay"; - -pub fn set<E: SqlExecutor>( - exec: &E, - opts: &INostrProfileRelayRelation, -) -> Result<INostrProfileRelayResolve, IError<SqlError>> { - let mut query_vals: Vec<Value> = Vec::with_capacity(2); - let (nostr_profile_column, nostr_profile_value) = opts.nostr_profile.to_filter_param(); - query_vals.push(nostr_profile_value); - let (nostr_relay_column, nostr_relay_value) = opts.nostr_relay.to_filter_param(); - query_vals.push(nostr_relay_value); - let query = format!( - "INSERT INTO {} (tb_pr, tb_rl) VALUES ((SELECT id FROM nostr_profile WHERE {} = ?), (SELECT id FROM nostr_relay WHERE {} = ?));", - TABLE_NAME, nostr_profile_column, nostr_relay_column - ); - let params_json = utils::to_params_json(query_vals)?; - let _ = exec.exec(&query, ¶ms_json)?; - Ok(IResultPass { pass: true }) -} - -pub fn unset<E: SqlExecutor>( - exec: &E, - opts: &INostrProfileRelayRelation, -) -> Result<INostrProfileRelayResolve, IError<SqlError>> { - let mut query_vals: Vec<Value> = Vec::with_capacity(2); - let (nostr_profile_column, nostr_profile_value) = opts.nostr_profile.to_filter_param(); - query_vals.push(nostr_profile_value); - let (nostr_relay_column, nostr_relay_value) = opts.nostr_relay.to_filter_param(); - query_vals.push(nostr_relay_value); - let query = format!( - "DELETE FROM {} WHERE tb_pr = (SELECT id FROM nostr_profile WHERE {} = ?) AND tb_rl = (SELECT id FROM nostr_relay WHERE {} = ?);", - TABLE_NAME, nostr_profile_column, nostr_relay_column - ); - let params_json = utils::to_params_json(query_vals)?; - let _ = exec.exec(&query, ¶ms_json)?; - Ok(IResultPass { pass: true }) -} diff --git a/crates/tangle-db/src/models/nostr_relay.rs b/crates/tangle-db/src/models/nostr_relay.rs @@ -1,203 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::nostr_relay::{ - INostrRelayCreate, INostrRelayCreateResolve, INostrRelayDelete, INostrRelayDeleteResolve, - INostrRelayFieldsFilter, INostrRelayFindMany, INostrRelayFindManyResolve, INostrRelayFindOne, - INostrRelayFindOneResolve, INostrRelayUpdate, INostrRelayUpdateResolve, NostrRelay, - NostrRelayFindManyRel, NostrRelayQueryBindValues, -}; -use radroots_types::types::{IError, IResult, IResultList}; -use serde_json::Value; - -const TABLE_NAME: &str = "nostr_relay"; - -pub fn create<E: SqlExecutor>( - exec: &E, - opts: &INostrRelayCreate, -) -> Result<INostrRelayCreateResolve, IError<SqlError>> { - let field_map = utils::to_object_map(opts)?; - let id = utils::uuidv4(); - let now = utils::time_created_on(); - let meta: [(&str, Value); 3] = [ - ("id", Value::from(id.clone())), - ("created_at", Value::from(now.clone())), - ("updated_at", Value::from(now.clone())), - ]; - let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); - let params_json = utils::to_params_json(bind_values)?; - let _ = exec.exec(&sql, ¶ms_json)?; - let on = NostrRelayQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; - Ok(IResult { result }) -} - -pub fn find_one<E: SqlExecutor>( - exec: &E, - opts: &INostrRelayFindOne, -) -> Result<INostrRelayFindOneResolve, IError<SqlError>> { - let result = match opts { - INostrRelayFindOne::On(args) => find_one_by_on(exec, &args.on)?, - INostrRelayFindOne::Rel(args) => find_one_by_rel(exec, &args.rel)?, - }; - Ok(IResult { result }) -} - -pub fn find_many<E: SqlExecutor>( - exec: &E, - opts: &INostrRelayFindMany, -) -> Result<INostrRelayFindManyResolve, IError<SqlError>> { - let results = match opts { - INostrRelayFindMany::Filter { filter } => find_many_filter(exec, filter)?, - INostrRelayFindMany::Rel { rel } => find_many_by_rel(exec, rel)?, - }; - Ok(IResultList { results }) -} - -fn find_many_filter<E: SqlExecutor>( - exec: &E, - filter: &Option<INostrRelayFieldsFilter>, -) -> Result<Vec<NostrRelay>, IError<SqlError>> { - let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); - let params_json = utils::to_params_json(bind_values)?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<NostrRelay> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on<E: SqlExecutor>( - exec: &E, - on: &NostrRelayQueryBindValues, -) -> Result<Option<NostrRelay>, IError<SqlError>> { - let (column, value) = on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value])?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<NostrRelay> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn rel_query(rel: &NostrRelayFindManyRel) -> (&'static str, Vec<Value>) { - match rel { - NostrRelayFindManyRel::OnProfile(args) => ( - "SELECT rl.* FROM nostr_relay rl JOIN nostr_profile_relay pr_rl ON rl.id = pr_rl.tb_rl JOIN nostr_profile pr ON pr.id = pr_rl.tb_pr WHERE pr.public_key = ?", - vec![Value::from(args.public_key.clone())], - ), - NostrRelayFindManyRel::OffProfile(args) => ( - "SELECT rl.* FROM nostr_relay rl LEFT JOIN nostr_profile_relay pr_rl ON rl.id = pr_rl.tb_rl LEFT JOIN nostr_profile pr ON pr.id = pr_rl.tb_pr WHERE pr.public_key <> ?", - vec![Value::from(args.public_key.clone())], - ), - } -} - -fn find_one_by_rel<E: SqlExecutor>( - exec: &E, - rel: &NostrRelayFindManyRel, -) -> Result<Option<NostrRelay>, IError<SqlError>> { - let (sql, bind_values) = rel_query(rel); - let params_json = utils::to_params_json(bind_values)?; - let sql = format!("{sql} LIMIT 1;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<NostrRelay> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn find_many_by_rel<E: SqlExecutor>( - exec: &E, - rel: &NostrRelayFindManyRel, -) -> Result<Vec<NostrRelay>, IError<SqlError>> { - let (sql, bind_values) = rel_query(rel); - let params_json = utils::to_params_json(bind_values)?; - let sql = format!("{sql};"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<NostrRelay> = utils::parse_json(&json)?; - Ok(rows) -} - -fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<NostrRelay, IError<SqlError>> { - let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<NostrRelay> = utils::parse_json(&json)?; - rows.pop() - .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update<E: SqlExecutor>( - exec: &E, - opts: &INostrRelayUpdate, -) -> Result<INostrRelayUpdateResolve, IError<SqlError>> { - let mut updates = utils::to_partial_object_map(&opts.fields)?; - if updates.is_empty() { - return Err(IError::from(SqlError::InvalidArgument(String::from( - "no fields to update", - )))); - } - updates.insert( - String::from("updated_at"), - Value::from(utils::time_created_on()), - ); - let mut set_parts = Vec::with_capacity(updates.len()); - let mut bind_values = Vec::with_capacity(updates.len() + 1); - for (column, value) in updates { - set_parts.push(format!("{column} = ?")); - bind_values.push(utils::to_db_bind_value(&value)); - } - let id_for_lookup = match opts.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &opts.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; - model.id - } - }; - 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 _ = exec.exec(&sql, ¶ms_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(IResult { result: updated }) -} - -pub fn delete<E: SqlExecutor>( - exec: &E, - opts: &INostrRelayDelete, -) -> Result<INostrRelayDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts { - INostrRelayDelete::On(args) => match args.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &args.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; - model.id - } - }, - INostrRelayDelete::Rel(args) => { - let found = find_one_by_rel(exec, &args.rel)?; - let model = found.ok_or(IError::from(SqlError::NotFound(rel_lookup_key(&args.rel))))?; - model.id - } - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, ¶ms_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } - Ok(IResult { - result: id_for_lookup, - }) -} - -fn rel_lookup_key(rel: &NostrRelayFindManyRel) -> String { - match rel { - NostrRelayFindManyRel::OnProfile(args) => { - format!("on_profile:{}", args.public_key.as_str()) - } - NostrRelayFindManyRel::OffProfile(args) => { - format!("off_profile:{}", args.public_key.as_str()) - } - } -} diff --git a/crates/tangle-db/src/models/plot.rs b/crates/tangle-db/src/models/plot.rs @@ -1,145 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::plot::{ - IPlotCreate, IPlotCreateResolve, IPlotDelete, IPlotDeleteResolve, IPlotFieldsFilter, - IPlotFindMany, IPlotFindManyResolve, IPlotFindOne, IPlotFindOneResolve, IPlotUpdate, - IPlotUpdateResolve, Plot, PlotQueryBindValues, -}; -use radroots_types::types::{IError, IResult, IResultList}; -use serde_json::Value; - -const TABLE_NAME: &str = "plot"; - -pub fn create<E: SqlExecutor>( - exec: &E, - opts: &IPlotCreate, -) -> Result<IPlotCreateResolve, IError<SqlError>> { - let field_map = utils::to_object_map(opts)?; - let id = utils::uuidv4(); - let now = utils::time_created_on(); - let meta: [(&str, Value); 3] = [ - ("id", Value::from(id.clone())), - ("created_at", Value::from(now.clone())), - ("updated_at", Value::from(now.clone())), - ]; - let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); - let params_json = utils::to_params_json(bind_values)?; - let _ = exec.exec(&sql, ¶ms_json)?; - let on = PlotQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; - Ok(IResult { result }) -} - -pub fn find_one<E: SqlExecutor>( - exec: &E, - opts: &IPlotFindOne, -) -> Result<IPlotFindOneResolve, IError<SqlError>> { - let result = match opts { - IPlotFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(IResult { result }) -} - -pub fn find_many<E: SqlExecutor>( - exec: &E, - opts: &IPlotFindMany, -) -> Result<IPlotFindManyResolve, IError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(IResultList { results }) -} - -fn find_many_filter<E: SqlExecutor>( - exec: &E, - filter: &Option<IPlotFieldsFilter>, -) -> Result<Vec<Plot>, IError<SqlError>> { - let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); - let params_json = utils::to_params_json(bind_values)?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<Plot> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on<E: SqlExecutor>( - exec: &E, - on: &PlotQueryBindValues, -) -> Result<Option<Plot>, IError<SqlError>> { - let (column, value) = on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value])?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<Plot> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<Plot, IError<SqlError>> { - let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<Plot> = utils::parse_json(&json)?; - rows.pop() - .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update<E: SqlExecutor>( - exec: &E, - opts: &IPlotUpdate, -) -> Result<IPlotUpdateResolve, IError<SqlError>> { - let mut updates = utils::to_partial_object_map(&opts.fields)?; - if updates.is_empty() { - return Err(IError::from(SqlError::InvalidArgument(String::from( - "no fields to update", - )))); - } - updates.insert( - String::from("updated_at"), - Value::from(utils::time_created_on()), - ); - let mut set_parts = Vec::with_capacity(updates.len()); - let mut bind_values = Vec::with_capacity(updates.len() + 1); - for (column, value) in updates { - set_parts.push(format!("{column} = ?")); - bind_values.push(utils::to_db_bind_value(&value)); - } - let id_for_lookup = match opts.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &opts.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; - model.id - } - }; - 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 _ = exec.exec(&sql, ¶ms_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(IResult { result: updated }) -} - -pub fn delete<E: SqlExecutor>( - exec: &E, - opts: &IPlotDelete, -) -> Result<IPlotDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts { - IPlotDelete::On(args) => match args.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &args.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, ¶ms_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } - Ok(IResult { - result: id_for_lookup, - }) -} diff --git a/crates/tangle-db/src/models/plot_gcs_location.rs b/crates/tangle-db/src/models/plot_gcs_location.rs @@ -1,147 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::plot_gcs_location::{ - IPlotGcsLocationCreate, IPlotGcsLocationCreateResolve, IPlotGcsLocationDelete, - IPlotGcsLocationDeleteResolve, IPlotGcsLocationFieldsFilter, IPlotGcsLocationFindMany, - IPlotGcsLocationFindManyResolve, IPlotGcsLocationFindOne, IPlotGcsLocationFindOneResolve, - IPlotGcsLocationUpdate, IPlotGcsLocationUpdateResolve, PlotGcsLocation, - PlotGcsLocationQueryBindValues, -}; -use radroots_types::types::{IError, IResult, IResultList}; -use serde_json::Value; - -const TABLE_NAME: &str = "plot_gcs_location"; - -pub fn create<E: SqlExecutor>( - exec: &E, - opts: &IPlotGcsLocationCreate, -) -> Result<IPlotGcsLocationCreateResolve, IError<SqlError>> { - let field_map = utils::to_object_map(opts)?; - let id = utils::uuidv4(); - let now = utils::time_created_on(); - let meta: [(&str, Value); 3] = [ - ("id", Value::from(id.clone())), - ("created_at", Value::from(now.clone())), - ("updated_at", Value::from(now.clone())), - ]; - let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); - let params_json = utils::to_params_json(bind_values)?; - let _ = exec.exec(&sql, ¶ms_json)?; - let on = PlotGcsLocationQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; - Ok(IResult { result }) -} - -pub fn find_one<E: SqlExecutor>( - exec: &E, - opts: &IPlotGcsLocationFindOne, -) -> Result<IPlotGcsLocationFindOneResolve, IError<SqlError>> { - let result = match opts { - IPlotGcsLocationFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(IResult { result }) -} - -pub fn find_many<E: SqlExecutor>( - exec: &E, - opts: &IPlotGcsLocationFindMany, -) -> Result<IPlotGcsLocationFindManyResolve, IError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(IResultList { results }) -} - -fn find_many_filter<E: SqlExecutor>( - exec: &E, - filter: &Option<IPlotGcsLocationFieldsFilter>, -) -> Result<Vec<PlotGcsLocation>, IError<SqlError>> { - let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); - let params_json = utils::to_params_json(bind_values)?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<PlotGcsLocation> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on<E: SqlExecutor>( - exec: &E, - on: &PlotGcsLocationQueryBindValues, -) -> Result<Option<PlotGcsLocation>, IError<SqlError>> { - let (column, value) = on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value])?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<PlotGcsLocation> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<PlotGcsLocation, IError<SqlError>> { - let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<PlotGcsLocation> = utils::parse_json(&json)?; - rows.pop() - .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update<E: SqlExecutor>( - exec: &E, - opts: &IPlotGcsLocationUpdate, -) -> Result<IPlotGcsLocationUpdateResolve, IError<SqlError>> { - let mut updates = utils::to_partial_object_map(&opts.fields)?; - if updates.is_empty() { - return Err(IError::from(SqlError::InvalidArgument(String::from( - "no fields to update", - )))); - } - updates.insert( - String::from("updated_at"), - Value::from(utils::time_created_on()), - ); - let mut set_parts = Vec::with_capacity(updates.len()); - let mut bind_values = Vec::with_capacity(updates.len() + 1); - for (column, value) in updates { - set_parts.push(format!("{column} = ?")); - bind_values.push(utils::to_db_bind_value(&value)); - } - let id_for_lookup = match opts.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &opts.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; - model.id - } - }; - 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 _ = exec.exec(&sql, ¶ms_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(IResult { result: updated }) -} - -pub fn delete<E: SqlExecutor>( - exec: &E, - opts: &IPlotGcsLocationDelete, -) -> Result<IPlotGcsLocationDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts { - IPlotGcsLocationDelete::On(args) => match args.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &args.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, ¶ms_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } - Ok(IResult { - result: id_for_lookup, - }) -} diff --git a/crates/tangle-db/src/models/plot_tag.rs b/crates/tangle-db/src/models/plot_tag.rs @@ -1,145 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::plot_tag::{ - IPlotTagCreate, IPlotTagCreateResolve, IPlotTagDelete, IPlotTagDeleteResolve, - IPlotTagFieldsFilter, IPlotTagFindMany, IPlotTagFindManyResolve, IPlotTagFindOne, - IPlotTagFindOneResolve, IPlotTagUpdate, IPlotTagUpdateResolve, PlotTag, PlotTagQueryBindValues, -}; -use radroots_types::types::{IError, IResult, IResultList}; -use serde_json::Value; - -const TABLE_NAME: &str = "plot_tag"; - -pub fn create<E: SqlExecutor>( - exec: &E, - opts: &IPlotTagCreate, -) -> Result<IPlotTagCreateResolve, IError<SqlError>> { - let field_map = utils::to_object_map(opts)?; - let id = utils::uuidv4(); - let now = utils::time_created_on(); - let meta: [(&str, Value); 3] = [ - ("id", Value::from(id.clone())), - ("created_at", Value::from(now.clone())), - ("updated_at", Value::from(now.clone())), - ]; - let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); - let params_json = utils::to_params_json(bind_values)?; - let _ = exec.exec(&sql, ¶ms_json)?; - let on = PlotTagQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; - Ok(IResult { result }) -} - -pub fn find_one<E: SqlExecutor>( - exec: &E, - opts: &IPlotTagFindOne, -) -> Result<IPlotTagFindOneResolve, IError<SqlError>> { - let result = match opts { - IPlotTagFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(IResult { result }) -} - -pub fn find_many<E: SqlExecutor>( - exec: &E, - opts: &IPlotTagFindMany, -) -> Result<IPlotTagFindManyResolve, IError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(IResultList { results }) -} - -fn find_many_filter<E: SqlExecutor>( - exec: &E, - filter: &Option<IPlotTagFieldsFilter>, -) -> Result<Vec<PlotTag>, IError<SqlError>> { - let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); - let params_json = utils::to_params_json(bind_values)?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<PlotTag> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on<E: SqlExecutor>( - exec: &E, - on: &PlotTagQueryBindValues, -) -> Result<Option<PlotTag>, IError<SqlError>> { - let (column, value) = on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value])?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<PlotTag> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<PlotTag, IError<SqlError>> { - let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<PlotTag> = utils::parse_json(&json)?; - rows.pop() - .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update<E: SqlExecutor>( - exec: &E, - opts: &IPlotTagUpdate, -) -> Result<IPlotTagUpdateResolve, IError<SqlError>> { - let mut updates = utils::to_partial_object_map(&opts.fields)?; - if updates.is_empty() { - return Err(IError::from(SqlError::InvalidArgument(String::from( - "no fields to update", - )))); - } - updates.insert( - String::from("updated_at"), - Value::from(utils::time_created_on()), - ); - let mut set_parts = Vec::with_capacity(updates.len()); - let mut bind_values = Vec::with_capacity(updates.len() + 1); - for (column, value) in updates { - set_parts.push(format!("{column} = ?")); - bind_values.push(utils::to_db_bind_value(&value)); - } - let id_for_lookup = match opts.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &opts.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; - model.id - } - }; - 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 _ = exec.exec(&sql, ¶ms_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(IResult { result: updated }) -} - -pub fn delete<E: SqlExecutor>( - exec: &E, - opts: &IPlotTagDelete, -) -> Result<IPlotTagDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts { - IPlotTagDelete::On(args) => match args.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &args.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, ¶ms_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } - Ok(IResult { - result: id_for_lookup, - }) -} diff --git a/crates/tangle-db/src/models/trade_product.rs b/crates/tangle-db/src/models/trade_product.rs @@ -1,136 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::trade_product::{ - ITradeProductCreate, ITradeProductCreateResolve, ITradeProductDelete, - ITradeProductDeleteResolve, ITradeProductFieldsFilter, ITradeProductFindMany, - ITradeProductFindManyResolve, ITradeProductFindOne, ITradeProductFindOneResolve, - ITradeProductUpdate, ITradeProductUpdateResolve, TradeProduct, TradeProductQueryBindValues, -}; -use radroots_types::types::{IError, IResult, IResultList}; -use serde_json::Value; - -const TABLE_NAME: &str = "trade_product"; - -pub fn create<E: SqlExecutor>( - exec: &E, - opts: &ITradeProductCreate, -) -> Result<ITradeProductCreateResolve, IError<SqlError>> { - let field_map = utils::to_object_map(opts)?; - let id = utils::uuidv4(); - let now = utils::time_created_on(); - let meta: [(&str, Value); 3] = [ - ("id", Value::from(id.clone())), - ("created_at", Value::from(now.clone())), - ("updated_at", Value::from(now.clone())), - ]; - let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); - let params_json = utils::to_params_json(bind_values)?; - let _ = exec.exec(&sql, ¶ms_json)?; - let on = TradeProductQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; - Ok(IResult { result }) -} - -pub fn find_one<E: SqlExecutor>( - exec: &E, - opts: &ITradeProductFindOne, -) -> Result<ITradeProductFindOneResolve, IError<SqlError>> { - let result = match opts { - ITradeProductFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(IResult { result }) -} - -pub fn find_many<E: SqlExecutor>( - exec: &E, - opts: &ITradeProductFindMany, -) -> Result<ITradeProductFindManyResolve, IError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(IResultList { results }) -} - -fn find_many_filter<E: SqlExecutor>( - exec: &E, - filter: &Option<ITradeProductFieldsFilter>, -) -> Result<Vec<TradeProduct>, IError<SqlError>> { - let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); - let params_json = utils::to_params_json(bind_values)?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<TradeProduct> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on<E: SqlExecutor>( - exec: &E, - on: &TradeProductQueryBindValues, -) -> Result<Option<TradeProduct>, IError<SqlError>> { - let (column, value) = on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value])?; - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<TradeProduct> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id<E: SqlExecutor>(exec: &E, id: &str) -> Result<TradeProduct, IError<SqlError>> { - let params_json = utils::to_params_json(vec![Value::from(id.to_owned())])?; - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<TradeProduct> = utils::parse_json(&json)?; - rows.pop() - .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update<E: SqlExecutor>( - exec: &E, - opts: &ITradeProductUpdate, -) -> Result<ITradeProductUpdateResolve, IError<SqlError>> { - let mut updates = utils::to_partial_object_map(&opts.fields)?; - if updates.is_empty() { - return Err(IError::from(SqlError::InvalidArgument(String::from( - "no fields to update", - )))); - } - updates.insert( - String::from("updated_at"), - Value::from(utils::time_created_on()), - ); - let mut set_parts = Vec::with_capacity(updates.len()); - let mut bind_values = Vec::with_capacity(updates.len() + 1); - for (column, value) in updates { - set_parts.push(format!("{column} = ?")); - bind_values.push(utils::to_db_bind_value(&value)); - } - let id_for_lookup = match &opts.on { - TradeProductQueryBindValues::Id { id } => id.clone(), - }; - 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 _ = exec.exec(&sql, ¶ms_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(IResult { result: updated }) -} - -pub fn delete<E: SqlExecutor>( - exec: &E, - opts: &ITradeProductDelete, -) -> Result<ITradeProductDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts { - ITradeProductDelete::On(args) => match &args.on { - TradeProductQueryBindValues::Id { id } => id.clone(), - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())])?; - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, ¶ms_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } - Ok(IResult { - result: id_for_lookup, - }) -} diff --git a/crates/tangle-db/src/models/trade_product_location.rs b/crates/tangle-db/src/models/trade_product_location.rs @@ -1,45 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::trade_product_location::{ - ITradeProductLocationRelation, ITradeProductLocationResolve, -}; -use radroots_types::types::{IError, IResultPass}; -use serde_json::Value; - -const TABLE_NAME: &str = "trade_product_location"; - -pub fn set<E: SqlExecutor>( - exec: &E, - opts: &ITradeProductLocationRelation, -) -> Result<ITradeProductLocationResolve, IError<SqlError>> { - let mut query_vals: Vec<Value> = Vec::with_capacity(2); - let (trade_product_column, trade_product_value) = opts.trade_product.to_filter_param(); - query_vals.push(trade_product_value); - let (gcs_location_column, gcs_location_value) = opts.gcs_location.to_filter_param(); - query_vals.push(gcs_location_value); - let query = format!( - "INSERT INTO {} (tb_tp, tb_gl) VALUES ((SELECT id FROM trade_product WHERE {} = ?), (SELECT id FROM gcs_location WHERE {} = ?));", - TABLE_NAME, trade_product_column, gcs_location_column - ); - let params_json = utils::to_params_json(query_vals)?; - let _ = exec.exec(&query, ¶ms_json)?; - Ok(IResultPass { pass: true }) -} - -pub fn unset<E: SqlExecutor>( - exec: &E, - opts: &ITradeProductLocationRelation, -) -> Result<ITradeProductLocationResolve, IError<SqlError>> { - let mut query_vals: Vec<Value> = Vec::with_capacity(2); - let (trade_product_column, trade_product_value) = opts.trade_product.to_filter_param(); - query_vals.push(trade_product_value); - let (gcs_location_column, gcs_location_value) = opts.gcs_location.to_filter_param(); - query_vals.push(gcs_location_value); - let query = format!( - "DELETE FROM {} WHERE tb_tp = (SELECT id FROM trade_product WHERE {} = ?) AND tb_gl = (SELECT id FROM gcs_location WHERE {} = ?);", - TABLE_NAME, trade_product_column, gcs_location_column - ); - let params_json = utils::to_params_json(query_vals)?; - let _ = exec.exec(&query, ¶ms_json)?; - Ok(IResultPass { pass: true }) -} diff --git a/crates/tangle-db/src/models/trade_product_media.rs b/crates/tangle-db/src/models/trade_product_media.rs @@ -1,45 +0,0 @@ -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_tangle_db_schema::trade_product_media::{ - ITradeProductMediaRelation, ITradeProductMediaResolve, -}; -use radroots_types::types::{IError, IResultPass}; -use serde_json::Value; - -const TABLE_NAME: &str = "trade_product_media"; - -pub fn set<E: SqlExecutor>( - exec: &E, - opts: &ITradeProductMediaRelation, -) -> Result<ITradeProductMediaResolve, IError<SqlError>> { - let mut query_vals: Vec<Value> = Vec::with_capacity(2); - let (trade_product_column, trade_product_value) = opts.trade_product.to_filter_param(); - query_vals.push(trade_product_value); - let (media_image_column, media_image_value) = opts.media_image.to_filter_param(); - query_vals.push(media_image_value); - let query = format!( - "INSERT INTO {} (tb_tp, tb_mu) VALUES ((SELECT id FROM trade_product WHERE {} = ?), (SELECT id FROM media_image WHERE {} = ?));", - TABLE_NAME, trade_product_column, media_image_column - ); - let params_json = utils::to_params_json(query_vals)?; - let _ = exec.exec(&query, ¶ms_json)?; - Ok(IResultPass { pass: true }) -} - -pub fn unset<E: SqlExecutor>( - exec: &E, - opts: &ITradeProductMediaRelation, -) -> Result<ITradeProductMediaResolve, IError<SqlError>> { - let mut query_vals: Vec<Value> = Vec::with_capacity(2); - let (trade_product_column, trade_product_value) = opts.trade_product.to_filter_param(); - query_vals.push(trade_product_value); - let (media_image_column, media_image_value) = opts.media_image.to_filter_param(); - query_vals.push(media_image_value); - let query = format!( - "DELETE FROM {} WHERE tb_tp = (SELECT id FROM trade_product WHERE {} = ?) AND tb_mu = (SELECT id FROM media_image WHERE {} = ?);", - TABLE_NAME, trade_product_column, media_image_column - ); - let params_json = utils::to_params_json(query_vals)?; - let _ = exec.exec(&query, ¶ms_json)?; - Ok(IResultPass { pass: true }) -} diff --git a/crates/tangle-db/tests/full_mode.rs b/crates/tangle-db/tests/full_mode.rs @@ -1,1232 +0,0 @@ -use radroots_sql_core::{SqlError, SqliteExecutor}; -use radroots_tangle_db::{TangleSql, export_manifest}; -use radroots_tangle_db_schema::farm::{ - IFarmCreate, IFarmDelete, IFarmFindMany, IFarmFindOne, IFarmUpdate, -}; -use radroots_tangle_db_schema::farm_gcs_location::{ - IFarmGcsLocationCreate, IFarmGcsLocationDelete, IFarmGcsLocationFindMany, - IFarmGcsLocationFindOne, IFarmGcsLocationUpdate, -}; -use radroots_tangle_db_schema::farm_member::{ - IFarmMemberCreate, IFarmMemberDelete, IFarmMemberFindMany, IFarmMemberFindOne, - IFarmMemberUpdate, -}; -use radroots_tangle_db_schema::farm_member_claim::{ - IFarmMemberClaimCreate, IFarmMemberClaimDelete, IFarmMemberClaimFindMany, - IFarmMemberClaimFindOne, IFarmMemberClaimUpdate, -}; -use radroots_tangle_db_schema::farm_tag::{ - IFarmTagCreate, IFarmTagDelete, IFarmTagFindMany, IFarmTagFindOne, IFarmTagUpdate, -}; -use radroots_tangle_db_schema::gcs_location::{ - GcsLocationFarmArgs, GcsLocationFindManyRel, GcsLocationPlotArgs, GcsLocationTradeProductArgs, - IGcsLocationCreate, IGcsLocationDelete, IGcsLocationFindMany, IGcsLocationFindOne, - IGcsLocationUpdate, -}; -use radroots_tangle_db_schema::log_error::{ - ILogErrorCreate, ILogErrorDelete, ILogErrorFindMany, ILogErrorFindOne, ILogErrorUpdate, -}; -use radroots_tangle_db_schema::media_image::{ - IMediaImageCreate, IMediaImageDelete, IMediaImageFindMany, IMediaImageFindOne, - IMediaImageUpdate, MediaImageFindManyRel, MediaImageTradeProductArgs, -}; -use radroots_tangle_db_schema::nostr_event_state::{ - INostrEventStateCreate, INostrEventStateDelete, INostrEventStateFindMany, - INostrEventStateFindOne, INostrEventStateUpdate, -}; -use radroots_tangle_db_schema::nostr_profile::{ - INostrProfileCreate, INostrProfileDelete, INostrProfileFindMany, INostrProfileFindOne, - INostrProfileUpdate, NostrProfileFindManyRel, NostrProfileRelayArgs, -}; -use radroots_tangle_db_schema::nostr_profile_relay::INostrProfileRelayRelation; -use radroots_tangle_db_schema::nostr_relay::{ - INostrRelayCreate, INostrRelayDelete, INostrRelayFindMany, INostrRelayFindOne, - INostrRelayUpdate, NostrRelayFindManyRel, NostrRelayProfileArgs, -}; -use radroots_tangle_db_schema::plot::{ - IPlotCreate, IPlotDelete, IPlotFindMany, IPlotFindOne, IPlotUpdate, -}; -use radroots_tangle_db_schema::plot_gcs_location::{ - IPlotGcsLocationCreate, IPlotGcsLocationDelete, IPlotGcsLocationFindMany, - IPlotGcsLocationFindOne, IPlotGcsLocationUpdate, -}; -use radroots_tangle_db_schema::plot_tag::{ - IPlotTagCreate, IPlotTagDelete, IPlotTagFindMany, IPlotTagFindOne, IPlotTagUpdate, -}; -use radroots_tangle_db_schema::trade_product::{ - ITradeProductCreate, ITradeProductDelete, ITradeProductFindMany, ITradeProductFindOne, - ITradeProductUpdate, -}; -use radroots_tangle_db_schema::trade_product_location::ITradeProductLocationRelation; -use radroots_tangle_db_schema::trade_product_media::ITradeProductMediaRelation; -use radroots_types::types::IError; -use serde::de::DeserializeOwned; -use serde_json::json; - -fn parse_json<T: DeserializeOwned>(value: serde_json::Value) -> T { - serde_json::from_value(value).expect("valid test payload") -} - -fn hex64(ch: char) -> String { - std::iter::repeat_n(ch, 64).collect() -} - -fn assert_invalid_argument<T>(result: Result<T, IError<SqlError>>) { - let err = match result { - Ok(_) => panic!("invalid argument expected"), - Err(err) => err, - }; - assert!(matches!(err.err, SqlError::InvalidArgument(_))); -} - -fn assert_not_found<T>(result: Result<T, IError<SqlError>>) { - let err = match result { - Ok(_) => panic!("not found expected"), - Err(err) => err, - }; - assert!(matches!(err.err, SqlError::NotFound(_))); -} - -fn open_db() -> TangleSql<SqliteExecutor> { - let exec = SqliteExecutor::open_memory().expect("open sqlite memory"); - let db = TangleSql::new(exec); - db.migrate_up().expect("migrate up"); - db -} - -#[test] -fn full_mode_crud_and_relation_paths() { - let db = open_db(); - - db.migrate_down().expect("migrate down"); - db.migrate_up().expect("migrate up again"); - - let farm: IFarmCreate = parse_json(json!({ - "d_tag": "farm-a", - "pubkey": hex64('a'), - "name": "farm a" - })); - let farm_created = db.farm_create(&farm).expect("farm create").result; - - let gcs_location: IGcsLocationCreate = parse_json(json!({ - "d_tag": "gcs-a", - "lat": 59.33, - "lng": 18.06, - "geohash": "u6sce4f", - "point": "POINT(18.06 59.33)", - "polygon": "POLYGON((18.06 59.33,18.07 59.33,18.07 59.34,18.06 59.34,18.06 59.33))" - })); - let gcs_created = db - .gcs_location_create(&gcs_location) - .expect("gcs create") - .result; - - let plot: IPlotCreate = parse_json(json!({ - "d_tag": "plot-a", - "farm_id": farm_created.id, - "name": "plot a" - })); - let plot_created = db.plot_create(&plot).expect("plot create").result; - - let farm_gcs: IFarmGcsLocationCreate = parse_json(json!({ - "farm_id": farm_created.id, - "gcs_location_id": gcs_created.id, - "role": "primary" - })); - let farm_gcs_created = db - .farm_gcs_location_create(&farm_gcs) - .expect("farm gcs create") - .result; - - let plot_gcs: IPlotGcsLocationCreate = parse_json(json!({ - "plot_id": plot_created.id, - "gcs_location_id": gcs_created.id, - "role": "primary" - })); - let plot_gcs_created = db - .plot_gcs_location_create(&plot_gcs) - .expect("plot gcs create") - .result; - - let farm_tag: IFarmTagCreate = parse_json(json!({ - "farm_id": farm_created.id, - "tag": "organic" - })); - let farm_tag_created = db - .farm_tag_create(&farm_tag) - .expect("farm tag create") - .result; - - let plot_tag: IPlotTagCreate = parse_json(json!({ - "plot_id": plot_created.id, - "tag": "north" - })); - let plot_tag_created = db - .plot_tag_create(&plot_tag) - .expect("plot tag create") - .result; - - let farm_member: IFarmMemberCreate = parse_json(json!({ - "farm_id": farm_created.id, - "member_pubkey": hex64('b'), - "role": "owner" - })); - let farm_member_created = db - .farm_member_create(&farm_member) - .expect("farm member create") - .result; - - let farm_member_claim: IFarmMemberClaimCreate = parse_json(json!({ - "member_pubkey": hex64('b'), - "farm_pubkey": hex64('a') - })); - let farm_member_claim_created = db - .farm_member_claim_create(&farm_member_claim) - .expect("farm member claim create") - .result; - - let log_error: ILogErrorCreate = parse_json(json!({ - "error": "panic", - "message": "boom", - "app_system": "studio", - "app_version": "1.0.0", - "nostr_pubkey": hex64('c') - })); - let log_error_created = db - .log_error_create(&log_error) - .expect("log error create") - .result; - - let media_image: IMediaImageCreate = parse_json(json!({ - "file_path": "/img/a.jpg", - "mime_type": "image/jpeg", - "res_base": "https://cdn.example.com", - "res_path": "img/a.jpg" - })); - let media_image_created = db - .media_image_create(&media_image) - .expect("media image create") - .result; - - let nostr_profile: INostrProfileCreate = parse_json(json!({ - "public_key": hex64('d'), - "profile_type": "farm", - "name": "profile a" - })); - let nostr_profile_created = db - .nostr_profile_create(&nostr_profile) - .expect("nostr profile create") - .result; - - let nostr_relay: INostrRelayCreate = parse_json(json!({ - "url": "wss://relay.example.com" - })); - let nostr_relay_created = db - .nostr_relay_create(&nostr_relay) - .expect("nostr relay create") - .result; - - let nostr_event_state: INostrEventStateCreate = parse_json(json!({ - "key": "state-a", - "kind": 30023, - "pubkey": hex64('d'), - "d_tag": "listing-a", - "last_event_id": hex64('e'), - "last_created_at": 1, - "content_hash": "hash-a" - })); - let nostr_event_state_created = db - .nostr_event_state_create(&nostr_event_state) - .expect("nostr event state create") - .result; - - let trade_product: ITradeProductCreate = parse_json(json!({ - "key": "product-a", - "category": "coffee", - "title": "coffee a", - "summary": "summary", - "process": "washed", - "lot": "lot-a", - "profile": "floral", - "year": 2024, - "qty_amt": 100, - "qty_unit": "kg", - "price_amt": 7.5, - "price_currency": "USD", - "price_qty_amt": 1, - "price_qty_unit": "kg" - })); - let trade_product_created = db - .trade_product_create(&trade_product) - .expect("trade product create") - .result; - - let gcs_extra: IGcsLocationCreate = parse_json(json!({ - "d_tag": "gcs-b", - "lat": 59.34, - "lng": 18.07, - "geohash": "u6sce4g", - "point": "POINT(18.07 59.34)", - "polygon": "POLYGON((18.07 59.34,18.08 59.34,18.08 59.35,18.07 59.35,18.07 59.34))" - })); - let _gcs_extra_created = db - .gcs_location_create(&gcs_extra) - .expect("gcs extra create") - .result; - - let media_image_extra: IMediaImageCreate = parse_json(json!({ - "file_path": "/img/b.jpg", - "mime_type": "image/jpeg", - "res_base": "https://cdn.example.com", - "res_path": "img/b.jpg" - })); - let _media_image_extra_created = db - .media_image_create(&media_image_extra) - .expect("media image extra create") - .result; - - let nostr_profile_extra: INostrProfileCreate = parse_json(json!({ - "public_key": hex64('f'), - "profile_type": "farm", - "name": "profile c" - })); - let nostr_profile_extra_created = db - .nostr_profile_create(&nostr_profile_extra) - .expect("nostr profile extra create") - .result; - - let nostr_relay_extra: INostrRelayCreate = parse_json(json!({ - "url": "wss://relay2.example.com" - })); - let nostr_relay_extra_created = db - .nostr_relay_create(&nostr_relay_extra) - .expect("nostr relay extra create") - .result; - - let profile_relay_rel: INostrProfileRelayRelation = parse_json(json!({ - "nostr_profile": { "id": nostr_profile_created.id }, - "nostr_relay": { "id": nostr_relay_created.id } - })); - db.nostr_profile_relay_set(&profile_relay_rel) - .expect("profile relay set"); - - let profile_relay_rel_extra: INostrProfileRelayRelation = parse_json(json!({ - "nostr_profile": { "id": nostr_profile_extra_created.id }, - "nostr_relay": { "id": nostr_relay_extra_created.id } - })); - db.nostr_profile_relay_set(&profile_relay_rel_extra) - .expect("profile relay extra set"); - - let product_location_rel: ITradeProductLocationRelation = parse_json(json!({ - "trade_product": { "id": trade_product_created.id }, - "gcs_location": { "id": gcs_created.id } - })); - db.trade_product_location_set(&product_location_rel) - .expect("product location set"); - - let product_media_rel: ITradeProductMediaRelation = parse_json(json!({ - "trade_product": { "id": trade_product_created.id }, - "media_image": { "id": media_image_created.id } - })); - db.trade_product_media_set(&product_media_rel) - .expect("product media set"); - - let _: IFarmFindMany = parse_json(json!({ "filter": { "id": farm_created.id } })); - let farm_find_many: IFarmFindMany = parse_json(json!({ "filter": { "id": farm_created.id } })); - assert_eq!( - db.farm_find_many(&farm_find_many) - .expect("farm find many") - .results - .len(), - 1 - ); - - let farm_find_one: IFarmFindOne = parse_json(json!({ "on": { "id": farm_created.id } })); - assert!( - db.farm_find_one(&farm_find_one) - .expect("farm find one") - .result - .is_some() - ); - - let farm_update_on_alt: IFarmUpdate = - parse_json(json!({ "on": { "d_tag": "farm-a" }, "fields": { "name": "farm a+" } })); - assert_eq!( - db.farm_update(&farm_update_on_alt) - .expect("farm update alt") - .result - .name, - "farm a+" - ); - let farm_update_on_id: IFarmUpdate = - parse_json(json!({ "on": { "id": farm_created.id }, "fields": { "name": "farm a++" } })); - assert_eq!( - db.farm_update(&farm_update_on_id) - .expect("farm update id") - .result - .name, - "farm a++" - ); - let farm_update_empty: IFarmUpdate = - parse_json(json!({ "on": { "id": farm_created.id }, "fields": {} })); - assert_invalid_argument(db.farm_update(&farm_update_empty)); - - let plot_find_many: IPlotFindMany = parse_json(json!({ "filter": { "id": plot_created.id } })); - assert_eq!( - db.plot_find_many(&plot_find_many) - .expect("plot find many") - .results - .len(), - 1 - ); - let plot_find_one: IPlotFindOne = parse_json(json!({ "on": { "id": plot_created.id } })); - assert!( - db.plot_find_one(&plot_find_one) - .expect("plot find one") - .result - .is_some() - ); - let plot_update_alt: IPlotUpdate = - parse_json(json!({ "on": { "d_tag": "plot-a" }, "fields": { "name": "plot a+" } })); - assert_eq!( - db.plot_update(&plot_update_alt) - .expect("plot update alt") - .result - .name, - "plot a+" - ); - let plot_update_id: IPlotUpdate = - parse_json(json!({ "on": { "id": plot_created.id }, "fields": { "name": "plot a++" } })); - assert_eq!( - db.plot_update(&plot_update_id) - .expect("plot update id") - .result - .name, - "plot a++" - ); - let plot_update_empty: IPlotUpdate = - parse_json(json!({ "on": { "id": plot_created.id }, "fields": {} })); - assert_invalid_argument(db.plot_update(&plot_update_empty)); - - for opts in [ - IGcsLocationFindMany::Rel { - rel: GcsLocationFindManyRel::OnTradeProduct(GcsLocationTradeProductArgs { - id: trade_product_created.id.clone(), - }), - }, - IGcsLocationFindMany::Rel { - rel: GcsLocationFindManyRel::OffTradeProduct(GcsLocationTradeProductArgs { - id: trade_product_created.id.clone(), - }), - }, - IGcsLocationFindMany::Rel { - rel: GcsLocationFindManyRel::OnFarm(GcsLocationFarmArgs { - id: farm_created.id.clone(), - }), - }, - IGcsLocationFindMany::Rel { - rel: GcsLocationFindManyRel::OffFarm(GcsLocationFarmArgs { - id: farm_created.id.clone(), - }), - }, - IGcsLocationFindMany::Rel { - rel: GcsLocationFindManyRel::OnPlot(GcsLocationPlotArgs { - id: plot_created.id.clone(), - }), - }, - IGcsLocationFindMany::Rel { - rel: GcsLocationFindManyRel::OffPlot(GcsLocationPlotArgs { - id: plot_created.id.clone(), - }), - }, - ] { - let _ = db.gcs_location_find_many(&opts).expect("gcs rel find many"); - } - let gcs_find_many_filter: IGcsLocationFindMany = - parse_json(json!({ "filter": { "id": gcs_created.id } })); - assert_eq!( - db.gcs_location_find_many(&gcs_find_many_filter) - .expect("gcs find many filter") - .results - .len(), - 1 - ); - let gcs_find_one_on: IGcsLocationFindOne = - parse_json(json!({ "on": { "id": gcs_created.id } })); - assert!( - db.gcs_location_find_one(&gcs_find_one_on) - .expect("gcs find one on") - .result - .is_some() - ); - let gcs_find_one_rel: IGcsLocationFindOne = - parse_json(json!({ "rel": { "on_farm": { "id": farm_created.id } } })); - assert!( - db.gcs_location_find_one(&gcs_find_one_rel) - .expect("gcs find one rel") - .result - .is_some() - ); - let gcs_update_alt: IGcsLocationUpdate = - parse_json(json!({ "on": { "d_tag": "gcs-a" }, "fields": { "label": "gcs a+" } })); - assert_eq!( - db.gcs_location_update(&gcs_update_alt) - .expect("gcs update alt") - .result - .label - .as_deref(), - Some("gcs a+") - ); - let gcs_update_id: IGcsLocationUpdate = - parse_json(json!({ "on": { "id": gcs_created.id }, "fields": { "label": "gcs a++" } })); - assert_eq!( - db.gcs_location_update(&gcs_update_id) - .expect("gcs update id") - .result - .label - .as_deref(), - Some("gcs a++") - ); - let gcs_update_empty: IGcsLocationUpdate = - parse_json(json!({ "on": { "id": gcs_created.id }, "fields": {} })); - assert_invalid_argument(db.gcs_location_update(&gcs_update_empty)); - - let farm_gcs_find_many: IFarmGcsLocationFindMany = - parse_json(json!({ "filter": { "id": farm_gcs_created.id } })); - assert_eq!( - db.farm_gcs_location_find_many(&farm_gcs_find_many) - .expect("farm gcs find many") - .results - .len(), - 1 - ); - let farm_gcs_find_one: IFarmGcsLocationFindOne = - parse_json(json!({ "on": { "id": farm_gcs_created.id } })); - assert!( - db.farm_gcs_location_find_one(&farm_gcs_find_one) - .expect("farm gcs find one") - .result - .is_some() - ); - let farm_gcs_update_alt: IFarmGcsLocationUpdate = parse_json(json!({ - "on": { "farm_id": farm_created.id }, - "fields": { "role": "secondary" } - })); - assert_eq!( - db.farm_gcs_location_update(&farm_gcs_update_alt) - .expect("farm gcs update") - .result - .role, - "secondary" - ); - let farm_gcs_update_id: IFarmGcsLocationUpdate = parse_json( - json!({ "on": { "id": farm_gcs_created.id }, "fields": { "role": "tertiary" } }), - ); - assert_eq!( - db.farm_gcs_location_update(&farm_gcs_update_id) - .expect("farm gcs update id") - .result - .role, - "tertiary" - ); - let farm_gcs_update_empty: IFarmGcsLocationUpdate = - parse_json(json!({ "on": { "id": farm_gcs_created.id }, "fields": {} })); - assert_invalid_argument(db.farm_gcs_location_update(&farm_gcs_update_empty)); - - let plot_gcs_find_many: IPlotGcsLocationFindMany = - parse_json(json!({ "filter": { "id": plot_gcs_created.id } })); - assert_eq!( - db.plot_gcs_location_find_many(&plot_gcs_find_many) - .expect("plot gcs find many") - .results - .len(), - 1 - ); - let plot_gcs_find_one: IPlotGcsLocationFindOne = - parse_json(json!({ "on": { "id": plot_gcs_created.id } })); - assert!( - db.plot_gcs_location_find_one(&plot_gcs_find_one) - .expect("plot gcs find one") - .result - .is_some() - ); - let plot_gcs_update_alt: IPlotGcsLocationUpdate = parse_json(json!({ - "on": { "plot_id": plot_created.id }, - "fields": { "role": "secondary" } - })); - assert_eq!( - db.plot_gcs_location_update(&plot_gcs_update_alt) - .expect("plot gcs update") - .result - .role, - "secondary" - ); - let plot_gcs_update_id: IPlotGcsLocationUpdate = parse_json( - json!({ "on": { "id": plot_gcs_created.id }, "fields": { "role": "tertiary" } }), - ); - assert_eq!( - db.plot_gcs_location_update(&plot_gcs_update_id) - .expect("plot gcs update id") - .result - .role, - "tertiary" - ); - let plot_gcs_update_empty: IPlotGcsLocationUpdate = - parse_json(json!({ "on": { "id": plot_gcs_created.id }, "fields": {} })); - assert_invalid_argument(db.plot_gcs_location_update(&plot_gcs_update_empty)); - - let farm_tag_find_many: IFarmTagFindMany = - parse_json(json!({ "filter": { "id": farm_tag_created.id } })); - assert_eq!( - db.farm_tag_find_many(&farm_tag_find_many) - .expect("farm tag find many") - .results - .len(), - 1 - ); - let farm_tag_find_one: IFarmTagFindOne = - parse_json(json!({ "on": { "id": farm_tag_created.id } })); - assert!( - db.farm_tag_find_one(&farm_tag_find_one) - .expect("farm tag find one") - .result - .is_some() - ); - let farm_tag_update_alt: IFarmTagUpdate = parse_json( - json!({ "on": { "farm_id": farm_created.id }, "fields": { "tag": "biodynamic" } }), - ); - assert_eq!( - db.farm_tag_update(&farm_tag_update_alt) - .expect("farm tag update") - .result - .tag, - "biodynamic" - ); - let farm_tag_update_id: IFarmTagUpdate = parse_json( - json!({ "on": { "id": farm_tag_created.id }, "fields": { "tag": "regenerative" } }), - ); - assert_eq!( - db.farm_tag_update(&farm_tag_update_id) - .expect("farm tag update id") - .result - .tag, - "regenerative" - ); - let farm_tag_update_empty: IFarmTagUpdate = - parse_json(json!({ "on": { "id": farm_tag_created.id }, "fields": {} })); - assert_invalid_argument(db.farm_tag_update(&farm_tag_update_empty)); - - let plot_tag_find_many: IPlotTagFindMany = - parse_json(json!({ "filter": { "id": plot_tag_created.id } })); - assert_eq!( - db.plot_tag_find_many(&plot_tag_find_many) - .expect("plot tag find many") - .results - .len(), - 1 - ); - let plot_tag_find_one: IPlotTagFindOne = - parse_json(json!({ "on": { "id": plot_tag_created.id } })); - assert!( - db.plot_tag_find_one(&plot_tag_find_one) - .expect("plot tag find one") - .result - .is_some() - ); - let plot_tag_update_alt: IPlotTagUpdate = - parse_json(json!({ "on": { "plot_id": plot_created.id }, "fields": { "tag": "south" } })); - assert_eq!( - db.plot_tag_update(&plot_tag_update_alt) - .expect("plot tag update") - .result - .tag, - "south" - ); - let plot_tag_update_id: IPlotTagUpdate = - parse_json(json!({ "on": { "id": plot_tag_created.id }, "fields": { "tag": "east" } })); - assert_eq!( - db.plot_tag_update(&plot_tag_update_id) - .expect("plot tag update id") - .result - .tag, - "east" - ); - let plot_tag_update_empty: IPlotTagUpdate = - parse_json(json!({ "on": { "id": plot_tag_created.id }, "fields": {} })); - assert_invalid_argument(db.plot_tag_update(&plot_tag_update_empty)); - - let farm_member_find_many: IFarmMemberFindMany = - parse_json(json!({ "filter": { "id": farm_member_created.id } })); - assert_eq!( - db.farm_member_find_many(&farm_member_find_many) - .expect("farm member find many") - .results - .len(), - 1 - ); - let farm_member_find_one: IFarmMemberFindOne = - parse_json(json!({ "on": { "id": farm_member_created.id } })); - assert!( - db.farm_member_find_one(&farm_member_find_one) - .expect("farm member find one") - .result - .is_some() - ); - let farm_member_update_alt: IFarmMemberUpdate = parse_json(json!({ - "on": { "member_pubkey": hex64('b') }, - "fields": { "role": "editor" } - })); - assert_eq!( - db.farm_member_update(&farm_member_update_alt) - .expect("farm member update") - .result - .role, - "editor" - ); - let farm_member_update_id: IFarmMemberUpdate = parse_json( - json!({ "on": { "id": farm_member_created.id }, "fields": { "role": "admin" } }), - ); - assert_eq!( - db.farm_member_update(&farm_member_update_id) - .expect("farm member update id") - .result - .role, - "admin" - ); - let farm_member_update_empty: IFarmMemberUpdate = - parse_json(json!({ "on": { "id": farm_member_created.id }, "fields": {} })); - assert_invalid_argument(db.farm_member_update(&farm_member_update_empty)); - - let farm_member_claim_find_many: IFarmMemberClaimFindMany = - parse_json(json!({ "filter": { "id": farm_member_claim_created.id } })); - assert_eq!( - db.farm_member_claim_find_many(&farm_member_claim_find_many) - .expect("farm member claim find many") - .results - .len(), - 1 - ); - let farm_member_claim_find_one: IFarmMemberClaimFindOne = - parse_json(json!({ "on": { "id": farm_member_claim_created.id } })); - assert!( - db.farm_member_claim_find_one(&farm_member_claim_find_one) - .expect("farm member claim find one") - .result - .is_some() - ); - let farm_member_claim_update_alt: IFarmMemberClaimUpdate = parse_json(json!({ - "on": { "member_pubkey": hex64('b') }, - "fields": { "farm_pubkey": hex64('f') } - })); - assert_eq!( - db.farm_member_claim_update(&farm_member_claim_update_alt) - .expect("farm member claim update") - .result - .farm_pubkey, - hex64('f') - ); - let farm_member_claim_update_id: IFarmMemberClaimUpdate = parse_json(json!({ - "on": { "id": farm_member_claim_created.id }, - "fields": { "farm_pubkey": hex64('g') } - })); - assert_eq!( - db.farm_member_claim_update(&farm_member_claim_update_id) - .expect("farm member claim update id") - .result - .farm_pubkey, - hex64('g') - ); - let farm_member_claim_update_empty: IFarmMemberClaimUpdate = - parse_json(json!({ "on": { "id": farm_member_claim_created.id }, "fields": {} })); - assert_invalid_argument(db.farm_member_claim_update(&farm_member_claim_update_empty)); - - let log_error_find_many: ILogErrorFindMany = - parse_json(json!({ "filter": { "id": log_error_created.id } })); - assert_eq!( - db.log_error_find_many(&log_error_find_many) - .expect("log error find many") - .results - .len(), - 1 - ); - let log_error_find_one: ILogErrorFindOne = - parse_json(json!({ "on": { "id": log_error_created.id } })); - assert!( - db.log_error_find_one(&log_error_find_one) - .expect("log error find one") - .result - .is_some() - ); - let log_error_update_alt: ILogErrorUpdate = parse_json(json!({ - "on": { "nostr_pubkey": hex64('c') }, - "fields": { "message": "boom+" } - })); - assert_eq!( - db.log_error_update(&log_error_update_alt) - .expect("log error update") - .result - .message, - "boom+" - ); - let log_error_update_id: ILogErrorUpdate = parse_json( - json!({ "on": { "id": log_error_created.id }, "fields": { "message": "boom++" } }), - ); - assert_eq!( - db.log_error_update(&log_error_update_id) - .expect("log error update id") - .result - .message, - "boom++" - ); - let log_error_update_empty: ILogErrorUpdate = - parse_json(json!({ "on": { "id": log_error_created.id }, "fields": {} })); - assert_invalid_argument(db.log_error_update(&log_error_update_empty)); - - for opts in [ - IMediaImageFindMany::Rel { - rel: MediaImageFindManyRel::OnTradeProduct(MediaImageTradeProductArgs { - id: trade_product_created.id.clone(), - }), - }, - IMediaImageFindMany::Rel { - rel: MediaImageFindManyRel::OffTradeProduct(MediaImageTradeProductArgs { - id: trade_product_created.id.clone(), - }), - }, - ] { - let _ = db - .media_image_find_many(&opts) - .expect("media image rel find many"); - } - let media_image_find_many_filter: IMediaImageFindMany = - parse_json(json!({ "filter": { "id": media_image_created.id } })); - assert_eq!( - db.media_image_find_many(&media_image_find_many_filter) - .expect("media image find many filter") - .results - .len(), - 1 - ); - let media_image_find_one_on: IMediaImageFindOne = - parse_json(json!({ "on": { "id": media_image_created.id } })); - assert!( - db.media_image_find_one(&media_image_find_one_on) - .expect("media image find one") - .result - .is_some() - ); - let media_image_find_one_rel: IMediaImageFindOne = - parse_json(json!({ "rel": { "on_trade_product": { "id": trade_product_created.id } } })); - assert!( - db.media_image_find_one(&media_image_find_one_rel) - .expect("media image find one rel") - .result - .is_some() - ); - let media_image_update_alt: IMediaImageUpdate = - parse_json(json!({ "on": { "file_path": "/img/a.jpg" }, "fields": { "label": "hero" } })); - assert_eq!( - db.media_image_update(&media_image_update_alt) - .expect("media image update") - .result - .label - .as_deref(), - Some("hero") - ); - let media_image_update_id: IMediaImageUpdate = parse_json( - json!({ "on": { "id": media_image_created.id }, "fields": { "label": "hero+" } }), - ); - assert_eq!( - db.media_image_update(&media_image_update_id) - .expect("media image update id") - .result - .label - .as_deref(), - Some("hero+") - ); - let media_image_update_empty: IMediaImageUpdate = - parse_json(json!({ "on": { "id": media_image_created.id }, "fields": {} })); - assert_invalid_argument(db.media_image_update(&media_image_update_empty)); - - for opts in [ - INostrProfileFindMany::Rel { - rel: NostrProfileFindManyRel::OnRelay(NostrProfileRelayArgs { - id: nostr_relay_created.id.clone(), - }), - }, - INostrProfileFindMany::Rel { - rel: NostrProfileFindManyRel::OffRelay(NostrProfileRelayArgs { - id: nostr_relay_created.id.clone(), - }), - }, - ] { - let _ = db - .nostr_profile_find_many(&opts) - .expect("nostr profile rel find many"); - } - let nostr_profile_find_many_filter: INostrProfileFindMany = - parse_json(json!({ "filter": { "id": nostr_profile_created.id } })); - assert_eq!( - db.nostr_profile_find_many(&nostr_profile_find_many_filter) - .expect("nostr profile find many filter") - .results - .len(), - 1 - ); - let nostr_profile_find_one_on: INostrProfileFindOne = - parse_json(json!({ "on": { "id": nostr_profile_created.id } })); - assert!( - db.nostr_profile_find_one(&nostr_profile_find_one_on) - .expect("nostr profile find one") - .result - .is_some() - ); - let nostr_profile_find_one_rel: INostrProfileFindOne = - parse_json(json!({ "rel": { "on_relay": { "id": nostr_relay_created.id } } })); - assert!( - db.nostr_profile_find_one(&nostr_profile_find_one_rel) - .expect("nostr profile find one rel") - .result - .is_some() - ); - let nostr_profile_update_alt: INostrProfileUpdate = parse_json( - json!({ "on": { "public_key": hex64('d') }, "fields": { "name": "profile b" } }), - ); - assert_eq!( - db.nostr_profile_update(&nostr_profile_update_alt) - .expect("nostr profile update") - .result - .name, - "profile b" - ); - let nostr_profile_update_id: INostrProfileUpdate = parse_json( - json!({ "on": { "id": nostr_profile_created.id }, "fields": { "name": "profile b+" } }), - ); - assert_eq!( - db.nostr_profile_update(&nostr_profile_update_id) - .expect("nostr profile update id") - .result - .name, - "profile b+" - ); - let nostr_profile_update_empty: INostrProfileUpdate = - parse_json(json!({ "on": { "id": nostr_profile_created.id }, "fields": {} })); - assert_invalid_argument(db.nostr_profile_update(&nostr_profile_update_empty)); - - let nostr_event_state_find_many: INostrEventStateFindMany = - parse_json(json!({ "filter": { "id": nostr_event_state_created.id } })); - assert_eq!( - db.nostr_event_state_find_many(&nostr_event_state_find_many) - .expect("nostr event state find many") - .results - .len(), - 1 - ); - let nostr_event_state_find_one: INostrEventStateFindOne = - parse_json(json!({ "on": { "id": nostr_event_state_created.id } })); - assert!( - db.nostr_event_state_find_one(&nostr_event_state_find_one) - .expect("nostr event state find one") - .result - .is_some() - ); - let nostr_event_state_update_alt: INostrEventStateUpdate = - parse_json(json!({ "on": { "key": "state-a" }, "fields": { "content_hash": "hash-b" } })); - assert_eq!( - db.nostr_event_state_update(&nostr_event_state_update_alt) - .expect("nostr event state update") - .result - .content_hash, - "hash-b" - ); - let nostr_event_state_update_id: INostrEventStateUpdate = parse_json( - json!({ "on": { "id": nostr_event_state_created.id }, "fields": { "content_hash": "hash-c" } }), - ); - assert_eq!( - db.nostr_event_state_update(&nostr_event_state_update_id) - .expect("nostr event state update id") - .result - .content_hash, - "hash-c" - ); - let nostr_event_state_update_empty: INostrEventStateUpdate = - parse_json(json!({ "on": { "id": nostr_event_state_created.id }, "fields": {} })); - assert_invalid_argument(db.nostr_event_state_update(&nostr_event_state_update_empty)); - - for opts in [ - INostrRelayFindMany::Rel { - rel: NostrRelayFindManyRel::OnProfile(NostrRelayProfileArgs { - public_key: hex64('d'), - }), - }, - INostrRelayFindMany::Rel { - rel: NostrRelayFindManyRel::OffProfile(NostrRelayProfileArgs { - public_key: hex64('d'), - }), - }, - ] { - let _ = db - .nostr_relay_find_many(&opts) - .expect("nostr relay rel find many"); - } - let nostr_relay_find_many_filter: INostrRelayFindMany = - parse_json(json!({ "filter": { "id": nostr_relay_created.id } })); - assert_eq!( - db.nostr_relay_find_many(&nostr_relay_find_many_filter) - .expect("nostr relay find many filter") - .results - .len(), - 1 - ); - let nostr_relay_find_one_on: INostrRelayFindOne = - parse_json(json!({ "on": { "id": nostr_relay_created.id } })); - assert!( - db.nostr_relay_find_one(&nostr_relay_find_one_on) - .expect("nostr relay find one") - .result - .is_some() - ); - let nostr_relay_find_one_rel: INostrRelayFindOne = - parse_json(json!({ "rel": { "on_profile": { "public_key": hex64('d') } } })); - assert!( - db.nostr_relay_find_one(&nostr_relay_find_one_rel) - .expect("nostr relay find one rel") - .result - .is_some() - ); - let nostr_relay_update_alt: INostrRelayUpdate = parse_json(json!({ - "on": { "url": "wss://relay.example.com" }, - "fields": { "name": "relay a" } - })); - assert_eq!( - db.nostr_relay_update(&nostr_relay_update_alt) - .expect("nostr relay update") - .result - .name - .as_deref(), - Some("relay a") - ); - let nostr_relay_update_id: INostrRelayUpdate = parse_json( - json!({ "on": { "id": nostr_relay_created.id }, "fields": { "name": "relay a+" } }), - ); - assert_eq!( - db.nostr_relay_update(&nostr_relay_update_id) - .expect("nostr relay update id") - .result - .name - .as_deref(), - Some("relay a+") - ); - let nostr_relay_update_empty: INostrRelayUpdate = - parse_json(json!({ "on": { "id": nostr_relay_created.id }, "fields": {} })); - assert_invalid_argument(db.nostr_relay_update(&nostr_relay_update_empty)); - - let trade_product_find_many: ITradeProductFindMany = - parse_json(json!({ "filter": { "id": trade_product_created.id } })); - assert_eq!( - db.trade_product_find_many(&trade_product_find_many) - .expect("trade product find many") - .results - .len(), - 1 - ); - let trade_product_find_one: ITradeProductFindOne = - parse_json(json!({ "on": { "id": trade_product_created.id } })); - assert!( - db.trade_product_find_one(&trade_product_find_one) - .expect("trade product find one") - .result - .is_some() - ); - let trade_product_update: ITradeProductUpdate = parse_json( - json!({ "on": { "id": trade_product_created.id }, "fields": { "title": "coffee b" } }), - ); - assert_eq!( - db.trade_product_update(&trade_product_update) - .expect("trade product update") - .result - .title, - "coffee b" - ); - let trade_product_update_empty: ITradeProductUpdate = - parse_json(json!({ "on": { "id": trade_product_created.id }, "fields": {} })); - assert_invalid_argument(db.trade_product_update(&trade_product_update_empty)); - - let backup = db.backup_database().expect("backup"); - let backup_json = db.backup_database_json().expect("backup json"); - let _manifest = export_manifest(db.executor()).expect("export manifest"); - db.restore_database(&backup).expect("restore backup"); - db.restore_database_json(&backup_json) - .expect("restore backup json"); - - let gcs_delete_rel_found: IGcsLocationDelete = - parse_json(json!({ "rel": { "off_trade_product": { "id": trade_product_created.id } } })); - db.gcs_location_delete(&gcs_delete_rel_found) - .expect("gcs rel delete found"); - - let media_image_rel_delete_found: IMediaImageDelete = - parse_json(json!({ "rel": { "off_trade_product": { "id": trade_product_created.id } } })); - db.media_image_delete(&media_image_rel_delete_found) - .expect("media image rel delete found"); - - let nostr_relay_rel_delete_found: INostrRelayDelete = - parse_json(json!({ "rel": { "on_profile": { "public_key": hex64('f') } } })); - db.nostr_relay_delete(&nostr_relay_rel_delete_found) - .expect("nostr relay rel delete found"); - - let nostr_profile_rel_delete_found: INostrProfileDelete = - parse_json(json!({ "rel": { "off_relay": { "id": nostr_relay_created.id } } })); - db.nostr_profile_delete(&nostr_profile_rel_delete_found) - .expect("nostr profile rel delete found"); - - db.trade_product_media_unset(&product_media_rel) - .expect("product media unset"); - db.trade_product_location_unset(&product_location_rel) - .expect("product location unset"); - db.nostr_profile_relay_unset(&profile_relay_rel) - .expect("profile relay unset"); - - let trade_product_delete: ITradeProductDelete = - parse_json(json!({ "on": { "id": trade_product_created.id } })); - db.trade_product_delete(&trade_product_delete) - .expect("trade product delete"); - let trade_product_delete_missing: ITradeProductDelete = - parse_json(json!({ "on": { "id": trade_product_created.id } })); - assert_not_found(db.trade_product_delete(&trade_product_delete_missing)); - - let plot_gcs_delete: IPlotGcsLocationDelete = - parse_json(json!({ "on": { "plot_id": plot_created.id } })); - db.plot_gcs_location_delete(&plot_gcs_delete) - .expect("plot gcs delete"); - let plot_gcs_delete_missing: IPlotGcsLocationDelete = - parse_json(json!({ "on": { "id": plot_gcs_created.id } })); - assert_not_found(db.plot_gcs_location_delete(&plot_gcs_delete_missing)); - - let farm_gcs_delete: IFarmGcsLocationDelete = - parse_json(json!({ "on": { "farm_id": farm_created.id } })); - db.farm_gcs_location_delete(&farm_gcs_delete) - .expect("farm gcs delete"); - let farm_gcs_delete_missing: IFarmGcsLocationDelete = - parse_json(json!({ "on": { "id": farm_gcs_created.id } })); - assert_not_found(db.farm_gcs_location_delete(&farm_gcs_delete_missing)); - - let gcs_delete_on_non_primary: IGcsLocationDelete = - parse_json(json!({ "on": { "d_tag": "gcs-a" } })); - let _ = db.gcs_location_delete(&gcs_delete_on_non_primary); - - for payload in [ - json!({ "rel": { "on_trade_product": { "id": trade_product_created.id } } }), - json!({ "rel": { "off_trade_product": { "id": trade_product_created.id } } }), - json!({ "rel": { "on_farm": { "id": farm_created.id } } }), - json!({ "rel": { "off_farm": { "id": farm_created.id } } }), - json!({ "rel": { "on_plot": { "id": plot_created.id } } }), - json!({ "rel": { "off_plot": { "id": plot_created.id } } }), - ] { - let opts: IGcsLocationDelete = parse_json(payload); - let _ = db.gcs_location_delete(&opts); - } - let gcs_delete_missing: IGcsLocationDelete = - parse_json(json!({ "on": { "id": gcs_created.id } })); - assert_not_found(db.gcs_location_delete(&gcs_delete_missing)); - - let media_image_delete: IMediaImageDelete = - parse_json(json!({ "on": { "file_path": "/img/a.jpg" } })); - db.media_image_delete(&media_image_delete) - .expect("media image delete"); - let media_image_delete_missing: IMediaImageDelete = - parse_json(json!({ "on": { "id": media_image_created.id } })); - assert_not_found(db.media_image_delete(&media_image_delete_missing)); - for payload in [ - json!({ "rel": { "on_trade_product": { "id": trade_product_created.id } } }), - json!({ "rel": { "off_trade_product": { "id": trade_product_created.id } } }), - ] { - let opts: IMediaImageDelete = parse_json(payload); - let _ = db.media_image_delete(&opts); - } - - let nostr_profile_delete: INostrProfileDelete = - parse_json(json!({ "on": { "public_key": hex64('d') } })); - db.nostr_profile_delete(&nostr_profile_delete) - .expect("nostr profile delete"); - let nostr_profile_delete_missing: INostrProfileDelete = - parse_json(json!({ "on": { "id": nostr_profile_created.id } })); - assert_not_found(db.nostr_profile_delete(&nostr_profile_delete_missing)); - for payload in [ - json!({ "rel": { "on_relay": { "id": nostr_relay_created.id } } }), - json!({ "rel": { "off_relay": { "id": nostr_relay_created.id } } }), - ] { - let opts: INostrProfileDelete = parse_json(payload); - let _ = db.nostr_profile_delete(&opts); - } - - let nostr_relay_delete: INostrRelayDelete = - parse_json(json!({ "on": { "url": "wss://relay.example.com" } })); - db.nostr_relay_delete(&nostr_relay_delete) - .expect("nostr relay delete"); - let nostr_relay_delete_missing: INostrRelayDelete = - parse_json(json!({ "on": { "id": nostr_relay_created.id } })); - assert_not_found(db.nostr_relay_delete(&nostr_relay_delete_missing)); - for payload in [ - json!({ "rel": { "on_profile": { "public_key": hex64('d') } } }), - json!({ "rel": { "off_profile": { "public_key": hex64('d') } } }), - ] { - let opts: INostrRelayDelete = parse_json(payload); - let _ = db.nostr_relay_delete(&opts); - } - - let nostr_event_state_delete: INostrEventStateDelete = - parse_json(json!({ "on": { "key": "state-a" } })); - db.nostr_event_state_delete(&nostr_event_state_delete) - .expect("nostr event state delete"); - let nostr_event_state_delete_missing: INostrEventStateDelete = - parse_json(json!({ "on": { "id": nostr_event_state_created.id } })); - assert_not_found(db.nostr_event_state_delete(&nostr_event_state_delete_missing)); - - let log_error_delete: ILogErrorDelete = - parse_json(json!({ "on": { "nostr_pubkey": hex64('c') } })); - db.log_error_delete(&log_error_delete) - .expect("log error delete"); - let log_error_delete_missing: ILogErrorDelete = - parse_json(json!({ "on": { "id": log_error_created.id } })); - assert_not_found(db.log_error_delete(&log_error_delete_missing)); - - let farm_member_claim_delete: IFarmMemberClaimDelete = - parse_json(json!({ "on": { "member_pubkey": hex64('b') } })); - db.farm_member_claim_delete(&farm_member_claim_delete) - .expect("farm member claim delete"); - let farm_member_claim_delete_missing: IFarmMemberClaimDelete = - parse_json(json!({ "on": { "id": farm_member_claim_created.id } })); - assert_not_found(db.farm_member_claim_delete(&farm_member_claim_delete_missing)); - - let farm_member_delete: IFarmMemberDelete = - parse_json(json!({ "on": { "member_pubkey": hex64('b') } })); - db.farm_member_delete(&farm_member_delete) - .expect("farm member delete"); - let farm_member_delete_missing: IFarmMemberDelete = - parse_json(json!({ "on": { "id": farm_member_created.id } })); - assert_not_found(db.farm_member_delete(&farm_member_delete_missing)); - - let plot_tag_delete: IPlotTagDelete = parse_json(json!({ "on": { "tag": "east" } })); - db.plot_tag_delete(&plot_tag_delete) - .expect("plot tag delete"); - let plot_tag_delete_missing: IPlotTagDelete = - parse_json(json!({ "on": { "id": plot_tag_created.id } })); - assert_not_found(db.plot_tag_delete(&plot_tag_delete_missing)); - - let farm_tag_delete: IFarmTagDelete = parse_json(json!({ "on": { "tag": "regenerative" } })); - db.farm_tag_delete(&farm_tag_delete) - .expect("farm tag delete"); - let farm_tag_delete_missing: IFarmTagDelete = - parse_json(json!({ "on": { "id": farm_tag_created.id } })); - assert_not_found(db.farm_tag_delete(&farm_tag_delete_missing)); - - let plot_delete: IPlotDelete = parse_json(json!({ "on": { "d_tag": "plot-a" } })); - db.plot_delete(&plot_delete).expect("plot delete"); - let plot_delete_missing: IPlotDelete = parse_json(json!({ "on": { "id": plot_created.id } })); - assert_not_found(db.plot_delete(&plot_delete_missing)); - - let farm_delete: IFarmDelete = parse_json(json!({ "on": { "d_tag": "farm-a" } })); - db.farm_delete(&farm_delete).expect("farm delete"); - let farm_delete_missing: IFarmDelete = parse_json(json!({ "on": { "id": farm_created.id } })); - assert_not_found(db.farm_delete(&farm_delete_missing)); -} diff --git a/crates/tangle-events-wasm/Cargo.toml b/crates/tangle-events-wasm/Cargo.toml @@ -1,26 +0,0 @@ -[package] -name = "radroots-tangle-events-wasm" -version.workspace = true -edition.workspace = true -authors = ["Radroots Authors"] -rust-version.workspace = true -license.workspace = true -publish = false - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -base64 = { workspace = true } -radroots-events = { workspace = true, default-features = false, features = ["serde"] } -radroots-sql-core = { workspace = true, features = ["bridge"] } -radroots-sql-wasm-core = { workspace = true, default-features = false, features = ["bridge"] } -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 } - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/tangle-events-wasm/pkg/package.json b/crates/tangle-events-wasm/pkg/package.json @@ -1,19 +0,0 @@ -{ - "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/crates/tangle-events-wasm/src/lib.rs b/crates/tangle-events-wasm/src/lib.rs @@ -1,124 +0,0 @@ -#![cfg(any(target_arch = "wasm32", coverage_nightly))] -#![forbid(unsafe_code)] - -#[cfg(target_arch = "wasm32")] -use base64::Engine; -#[cfg(target_arch = "wasm32")] -use base64::engine::general_purpose::URL_SAFE_NO_PAD; -#[cfg(target_arch = "wasm32")] -use radroots_events::RadrootsNostrEvent; -#[cfg(target_arch = "wasm32")] -use radroots_sql_core::WasmSqlExecutor; -#[cfg(target_arch = "wasm32")] -use radroots_tangle_events::{ - RadrootsTangleIdFactory, RadrootsTangleIngestOutcome, RadrootsTangleSyncRequest, - radroots_tangle_ingest_event_with_factory, radroots_tangle_sync_all, -}; -#[cfg(target_arch = "wasm32")] -use serde::Deserialize; -#[cfg(target_arch = "wasm32")] -use uuid::Uuid; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::prelude::*; - -#[cfg(target_arch = "wasm32")] -fn err_js<E: ToString>(err: E) -> JsValue { - JsValue::from_str(&err.to_string()) -} - -#[cfg(target_arch = "wasm32")] -struct WasmIdFactory; - -#[cfg(target_arch = "wasm32")] -impl RadrootsTangleIdFactory for WasmIdFactory { - fn new_d_tag(&self) -> String { - let uuid = Uuid::now_v7(); - URL_SAFE_NO_PAD.encode(uuid.as_bytes()) - } -} - -#[cfg(target_arch = "wasm32")] -#[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, -} - -#[cfg(target_arch = "wasm32")] -fn parse_request(request_json: &str) -> Result<RadrootsTangleSyncRequest, JsValue> { - serde_json::from_str(request_json).map_err(err_js) -} - -#[cfg(target_arch = "wasm32")] -fn parse_event(event_json: &str) -> Result<RadrootsNostrEvent, JsValue> { - 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, - }) -} - -#[cfg(target_arch = "wasm32")] -#[wasm_bindgen(js_name = tangle_events_sync_all)] -pub fn tangle_events_sync_all(request_json: &str) -> Result<JsValue, JsValue> { - let request = parse_request(request_json)?; - let exec = WasmSqlExecutor::new(); - let bundle = radroots_tangle_sync_all(&exec, &request).map_err(err_js)?; - serde_wasm_bindgen::to_value(&bundle).map_err(err_js) -} - -#[cfg(target_arch = "wasm32")] -#[wasm_bindgen(js_name = tangle_events_ingest_event)] -pub fn tangle_events_ingest_event(event_json: &str) -> Result<JsValue, JsValue> { - let event = parse_event(event_json)?; - let exec = WasmSqlExecutor::new(); - 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", - }; - Ok(JsValue::from_str(value)) -} - -#[cfg(coverage_nightly)] -pub fn coverage_branch_probe(input: bool) -> &'static str { - if input { - "tangle-events-wasm" - } else { - "tangle-events-wasm" - } -} - -#[cfg(all(test, coverage_nightly))] -mod tests { - use super::coverage_branch_probe; - - #[test] - fn coverage_branch_probe_hits_both_paths() { - assert_eq!(coverage_branch_probe(true), "tangle-events-wasm"); - assert_eq!(coverage_branch_probe(false), "tangle-events-wasm"); - } -} diff --git a/crates/tangle-events/Cargo.toml b/crates/tangle-events/Cargo.toml @@ -1,39 +0,0 @@ -[package] -name = "radroots-tangle-events" -version.workspace = true -edition.workspace = true -authors = ["Radroots Authors"] -rust-version.workspace = true -license.workspace = true -publish = false - -[lib] -crate-type = ["rlib"] - -[features] -default = ["std"] -std = [ - "radroots-events/std", - "radroots-events-codec/std", - "dep:base64", - "dep:uuid", -] - -[dependencies] -radroots-events = { workspace = true, default-features = false, features = ["serde"] } -radroots-events-codec = { workspace = true, default-features = false, features = ["serde_json"] } -radroots-sql-core = { workspace = true } -radroots-tangle-db-schema = { workspace = true } -radroots-tangle-db = { workspace = true } -radroots-types = { workspace = true } -hex = { workspace = true } -serde = { workspace = true, default-features = false, features = ["alloc", "derive"] } -serde_json = { workspace = true, default-features = false, features = ["alloc"] } -sha2 = { workspace = true, default-features = false } -base64 = { workspace = true, optional = true } -uuid = { workspace = true, optional = true } - -[dev-dependencies] -radroots-sql-core = { workspace = true, features = ["native"] } -radroots-tangle-db = { workspace = true } -serde_json = { workspace = true } diff --git a/crates/tangle-events/src/canonical.rs b/crates/tangle-events/src/canonical.rs @@ -1,95 +0,0 @@ -#[cfg(not(feature = "std"))] -use alloc::{ - string::{String, ToString}, - vec::Vec, -}; - -use serde::Serialize; -use serde_json::{Map, Value}; - -use crate::error::RadrootsTangleEventsError; - -pub fn canonical_json_string<T: Serialize>(value: &T) -> Result<String, RadrootsTangleEventsError> { - let value = serde_json::to_value(value).map_err(|_| { - RadrootsTangleEventsError::InvalidData("canonical json serialization failed".to_string()) - })?; - Ok(canonicalize_value(value).to_string()) -} - -fn canonicalize_value(value: Value) -> Value { - match value { - Value::Object(map) => canonicalize_object(map), - Value::Array(values) => { - let values = values - .into_iter() - .map(canonicalize_value) - .collect::<Vec<_>>(); - Value::Array(values) - } - other => other, - } -} - -fn canonicalize_object(map: Map<String, Value>) -> Value { - let mut entries = map.into_iter().collect::<Vec<_>>(); - entries.sort_by(|a, b| a.0.cmp(&b.0)); - let mut ordered = Map::new(); - for (key, value) in entries { - ordered.insert(key, canonicalize_value(value)); - } - Value::Object(ordered) -} - -#[cfg(test)] -mod tests { - use super::canonical_json_string; - use serde::Serialize; - - #[derive(Serialize)] - struct CanonicalFixture { - z: u32, - a: NestedFixture, - } - - #[derive(Serialize)] - struct NestedFixture { - b: u32, - a: u32, - } - - #[test] - fn canonical_json_string_sorts_object_keys_recursively() { - let value = CanonicalFixture { - z: 2, - a: NestedFixture { b: 3, a: 1 }, - }; - let json = canonical_json_string(&value).expect("json"); - assert_eq!(json, r#"{"a":{"a":1,"b":3},"z":2}"#); - } - - #[test] - fn canonical_json_string_handles_arrays() { - let json = canonical_json_string(&serde_json::json!([{"b": 2, "a": 1}])).expect("json"); - assert_eq!(json, r#"[{"a":1,"b":2}]"#); - } - - struct AlwaysErr; - - impl Serialize for AlwaysErr { - fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error> - where - S: serde::Serializer, - { - Err(serde::ser::Error::custom("always fail")) - } - } - - #[test] - fn canonical_json_string_propagates_serialization_errors() { - let err = canonical_json_string(&AlwaysErr).expect_err("serialize fail"); - assert!( - err.to_string() - .contains("canonical json serialization failed") - ); - } -} diff --git a/crates/tangle-events/src/emit.rs b/crates/tangle-events/src/emit.rs @@ -1,1415 +0,0 @@ -#[cfg(not(feature = "std"))] -use alloc::format; -#[cfg(not(feature = "std"))] -use alloc::{ - collections::BTreeMap, - string::{String, ToString}, - vec::Vec, -}; -#[cfg(feature = "std")] -use std::collections::BTreeMap; - -use radroots_events::farm::{ - RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef, RadrootsGcsLocation, RadrootsGeoJsonPoint, - RadrootsGeoJsonPolygon, -}; -use radroots_events::kinds::{KIND_FARM, KIND_LIST_SET_GENERIC, KIND_PLOT}; -use radroots_events::plot::RadrootsPlot; -use radroots_events::profile::{ - RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, RadrootsProfileType, - radroots_profile_type_from_tag_value, radroots_profile_type_tag_value, -}; -use radroots_events_codec::farm::encode as farm_encode; -use radroots_events_codec::farm::list_sets as farm_list_sets; -use radroots_events_codec::list_set::encode as list_set_encode; -use radroots_events_codec::plot::encode as plot_encode; -use radroots_events_codec::wire::WireEventParts; -use radroots_sql_core::SqlExecutor; -use radroots_tangle_db::{ - farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, nostr_profile, - plot, plot_gcs_location, plot_tag, -}; -use radroots_tangle_db_schema::farm::{ - Farm, IFarmFieldsFilter, IFarmFindMany, IFarmFindOne, IFarmFindOneArgs, -}; -use radroots_tangle_db_schema::farm_gcs_location::{ - FarmGcsLocation, IFarmGcsLocationFieldsFilter, IFarmGcsLocationFindMany, -}; -use radroots_tangle_db_schema::farm_member::{ - FarmMember, IFarmMemberFieldsFilter, IFarmMemberFindMany, -}; -use radroots_tangle_db_schema::farm_member_claim::{ - FarmMemberClaim, IFarmMemberClaimFieldsFilter, IFarmMemberClaimFindMany, -}; -use radroots_tangle_db_schema::farm_tag::{IFarmTagFieldsFilter, IFarmTagFindMany}; -use radroots_tangle_db_schema::gcs_location::{ - GcsLocation, GcsLocationQueryBindValues, IGcsLocationFindOne, IGcsLocationFindOneArgs, -}; -use radroots_tangle_db_schema::nostr_profile::{ - INostrProfileFindOne, INostrProfileFindOneArgs, NostrProfileQueryBindValues, -}; -use radroots_tangle_db_schema::plot::{IPlotFieldsFilter, IPlotFindMany, Plot}; -use radroots_tangle_db_schema::plot_gcs_location::{ - IPlotGcsLocationFieldsFilter, IPlotGcsLocationFindMany, PlotGcsLocation, -}; -use radroots_tangle_db_schema::plot_tag::{IPlotTagFieldsFilter, IPlotTagFindMany}; -use serde_json::Value; - -use crate::canonical::canonical_json_string; -use crate::error::RadrootsTangleEventsError; -use crate::geo::{geojson_point_from_lat_lng, geojson_polygon_circle_wgs84}; -use crate::types::{ - RADROOTS_TANGLE_TRANSFER_VERSION, RadrootsTangleEventDraft, RadrootsTangleFarmSelector, - RadrootsTangleSyncBundle, RadrootsTangleSyncOptions, RadrootsTangleSyncRequest, -}; - -const ROLE_PRIMARY: &str = "primary"; -const ROLE_MEMBER: &str = "member"; -const ROLE_OWNER: &str = "owner"; -const ROLE_WORKER: &str = "worker"; - -pub fn radroots_tangle_sync_all<E: SqlExecutor>( - exec: &E, - request: &RadrootsTangleSyncRequest, -) -> Result<RadrootsTangleSyncBundle, RadrootsTangleEventsError> { - radroots_tangle_sync_all_with_options(exec, &request.farm, request.options.as_ref()) -} - -pub fn radroots_tangle_sync_all_with_options<E: SqlExecutor>( - exec: &E, - farm_selector: &RadrootsTangleFarmSelector, - options: Option<&RadrootsTangleSyncOptions>, -) -> Result<RadrootsTangleSyncBundle, RadrootsTangleEventsError> { - let farm = resolve_farm(exec, farm_selector)?; - let include_profiles = options.and_then(|opt| opt.include_profiles).unwrap_or(true); - let include_list_sets = options - .and_then(|opt| opt.include_list_sets) - .unwrap_or(true); - let include_claims = options - .and_then(|opt| opt.include_membership_claims) - .unwrap_or(true); - - let mut events = Vec::new(); - - if include_profiles { - let profiles = radroots_tangle_profile_events(exec, &farm)?; - events.extend(profiles); - } - - events.push(radroots_tangle_farm_event(exec, &farm)?); - - let plots = radroots_tangle_plot_events(exec, &farm)?; - events.extend(plots); - - if include_list_sets { - let list_sets = radroots_tangle_list_set_events(exec, &farm)?; - events.extend(list_sets); - } - - if include_claims { - let claims = radroots_tangle_membership_claim_events(exec, &farm.pubkey)?; - events.extend(claims); - } - - Ok(RadrootsTangleSyncBundle { - version: RADROOTS_TANGLE_TRANSFER_VERSION, - events, - }) -} - -pub fn radroots_tangle_profile_events<E: SqlExecutor>( - exec: &E, - farm: &Farm, -) -> Result<Vec<RadrootsTangleEventDraft>, RadrootsTangleEventsError> { - let mut pubkeys = collect_profile_pubkeys(exec, farm)?; - pubkeys.sort(); - pubkeys.dedup(); - - let mut events = Vec::new(); - for pubkey in pubkeys { - if let Some(profile) = load_profile(exec, &pubkey)? { - events.push(profile_event(&pubkey, profile)?); - } - } - Ok(events) -} - -pub fn radroots_tangle_farm_event<E: SqlExecutor>( - exec: &E, - farm: &Farm, -) -> Result<RadrootsTangleEventDraft, RadrootsTangleEventsError> { - let tags = collect_farm_tags(exec, &farm.id)?; - let location = load_farm_location(exec, farm)?; - let farm_event = RadrootsFarm { - d_tag: farm.d_tag.clone(), - name: farm.name.clone(), - about: farm.about.clone(), - website: farm.website.clone(), - picture: farm.picture.clone(), - banner: farm.banner.clone(), - location, - tags: if tags.is_empty() { None } else { Some(tags) }, - }; - let tags = farm_encode::farm_build_tags(&farm_event)?; - let content = canonical_json_string(&farm_event)?; - let parts = WireEventParts { - kind: KIND_FARM, - content, - tags, - }; - Ok(parts_to_draft(&farm.pubkey, parts)) -} - -pub fn radroots_tangle_plot_events<E: SqlExecutor>( - exec: &E, - farm: &Farm, -) -> Result<Vec<RadrootsTangleEventDraft>, RadrootsTangleEventsError> { - let plots = load_plots(exec, &farm.id)?; - let mut events = Vec::new(); - for plot_row in plots { - let tags = collect_plot_tags(exec, &plot_row.id)?; - let location = load_plot_location(exec, &plot_row)?; - let plot_event = RadrootsPlot { - d_tag: plot_row.d_tag.clone(), - farm: RadrootsFarmRef { - pubkey: farm.pubkey.clone(), - d_tag: farm.d_tag.clone(), - }, - name: plot_row.name.clone(), - about: plot_row.about.clone(), - location, - tags: if tags.is_empty() { None } else { Some(tags) }, - }; - let tags = plot_encode::plot_build_tags(&plot_event)?; - let content = canonical_json_string(&plot_event)?; - let parts = WireEventParts { - kind: KIND_PLOT, - content, - tags, - }; - events.push(parts_to_draft(&farm.pubkey, parts)); - } - Ok(events) -} - -pub fn radroots_tangle_list_set_events<E: SqlExecutor>( - exec: &E, - farm: &Farm, -) -> Result<Vec<RadrootsTangleEventDraft>, RadrootsTangleEventsError> { - let members = load_farm_members(exec, &farm.id)?; - let plots = load_plots(exec, &farm.id)?; - - let members_list = - farm_list_sets::farm_members_list_set(&farm.d_tag, role_pubkeys(&members, ROLE_MEMBER))?; - let owners_list = - farm_list_sets::farm_owners_list_set(&farm.d_tag, role_pubkeys(&members, ROLE_OWNER))?; - let workers_list = - farm_list_sets::farm_workers_list_set(&farm.d_tag, role_pubkeys(&members, ROLE_WORKER))?; - - let plot_ids = sorted_plot_ids(&plots); - let plots_list = farm_list_sets::farm_plots_list_set(&farm.d_tag, &farm.pubkey, plot_ids)?; - - let list_sets = [members_list, owners_list, workers_list, plots_list]; - let mut events = Vec::new(); - for list_set in list_sets { - let parts = list_set_encode::to_wire_parts_with_kind(&list_set, KIND_LIST_SET_GENERIC)?; - events.push(parts_to_draft(&farm.pubkey, parts)); - } - Ok(events) -} - -pub fn radroots_tangle_membership_claim_events<E: SqlExecutor>( - exec: &E, - farm_pubkey: &str, -) -> Result<Vec<RadrootsTangleEventDraft>, RadrootsTangleEventsError> { - let claims = load_member_claims(exec, farm_pubkey)?; - let mut by_member: BTreeMap<String, Vec<String>> = BTreeMap::new(); - for claim in claims { - by_member - .entry(claim.member_pubkey.clone()) - .or_default() - .push(claim.farm_pubkey.clone()); - } - - let mut events = Vec::new(); - for (member_pubkey, _) in by_member.iter() { - let all_claims = load_member_claims_for_member(exec, member_pubkey)?; - let mut farm_pubkeys = all_claims - .into_iter() - .map(|claim| claim.farm_pubkey) - .collect::<Vec<String>>(); - farm_pubkeys.sort(); - farm_pubkeys.dedup(); - let list_set = farm_list_sets::member_of_farms_list_set(farm_pubkeys)?; - let parts = list_set_encode::to_wire_parts_with_kind(&list_set, KIND_LIST_SET_GENERIC)?; - events.push(parts_to_draft(member_pubkey, parts)); - } - - Ok(events) -} - -fn resolve_farm<E: SqlExecutor>( - exec: &E, - selector: &RadrootsTangleFarmSelector, -) -> Result<Farm, RadrootsTangleEventsError> { - if let Some(id) = selector.id.as_ref().filter(|v| !v.trim().is_empty()) { - let result_query = farm::find_one( - exec, - &IFarmFindOne::On(IFarmFindOneArgs { - on: radroots_tangle_db_schema::farm::FarmQueryBindValues::Id { id: id.clone() }, - }), - ); - let result = result_query?; - return result.result.ok_or_else(|| { - RadrootsTangleEventsError::InvalidSelector(format!("farm not found: {id}")) - }); - } - - let d_tag = selector - .d_tag - .as_ref() - .map(|v| v.trim()) - .filter(|v| !v.is_empty()); - let pubkey = selector - .pubkey - .as_ref() - .map(|v| v.trim()) - .filter(|v| !v.is_empty()); - - let (d_tag, pubkey) = match (d_tag, pubkey) { - (Some(d_tag), Some(pubkey)) => (d_tag, pubkey), - _ => { - return Err(RadrootsTangleEventsError::InvalidSelector( - "farm selector requires id or (d_tag + pubkey)".to_string(), - )); - } - }; - - let filter = IFarmFieldsFilter { - id: None, - created_at: None, - updated_at: None, - d_tag: Some(d_tag.to_string()), - pubkey: Some(pubkey.to_string()), - name: None, - about: None, - website: None, - picture: None, - banner: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }; - let result_query = farm::find_many( - exec, - &IFarmFindMany { - filter: Some(filter), - }, - ); - let result = result_query?; - if result.results.len() == 1 { - return Ok(result.results.into_iter().next().expect("farm result")); - } - Err(RadrootsTangleEventsError::InvalidSelector( - "farm selector did not resolve to a single farm".to_string(), - )) -} - -fn collect_farm_tags<E: SqlExecutor>( - exec: &E, - farm_id: &str, -) -> Result<Vec<String>, RadrootsTangleEventsError> { - let filter = IFarmTagFieldsFilter { - id: None, - created_at: None, - updated_at: None, - farm_id: Some(farm_id.to_string()), - tag: None, - }; - let result_query = farm_tag::find_many( - exec, - &IFarmTagFindMany { - filter: Some(filter), - }, - ); - let result = result_query?; - let mut tags = result - .results - .into_iter() - .map(|row| row.tag) - .collect::<Vec<_>>(); - tags.sort(); - tags.dedup(); - Ok(tags) -} - -fn collect_plot_tags<E: SqlExecutor>( - exec: &E, - plot_id: &str, -) -> Result<Vec<String>, RadrootsTangleEventsError> { - let filter = IPlotTagFieldsFilter { - id: None, - created_at: None, - updated_at: None, - plot_id: Some(plot_id.to_string()), - tag: None, - }; - let result_query = plot_tag::find_many( - exec, - &IPlotTagFindMany { - filter: Some(filter), - }, - ); - let result = result_query?; - let mut tags = result - .results - .into_iter() - .map(|row| row.tag) - .collect::<Vec<_>>(); - tags.sort(); - tags.dedup(); - Ok(tags) -} - -fn load_farm_members<E: SqlExecutor>( - exec: &E, - farm_id: &str, -) -> Result<Vec<FarmMember>, RadrootsTangleEventsError> { - let filter = IFarmMemberFieldsFilter { - id: None, - created_at: None, - updated_at: None, - farm_id: Some(farm_id.to_string()), - member_pubkey: None, - role: None, - }; - let result_query = farm_member::find_many( - exec, - &IFarmMemberFindMany { - filter: Some(filter), - }, - ); - let result = result_query?; - Ok(result.results) -} - -fn role_pubkeys(members: &[FarmMember], role: &str) -> Vec<String> { - let mut values = members - .iter() - .filter(|member| member.role == role) - .map(|member| member.member_pubkey.clone()) - .collect::<Vec<_>>(); - values.sort(); - values.dedup(); - values -} - -fn sorted_plot_ids(plots: &[Plot]) -> Vec<String> { - let mut ids = plots - .iter() - .map(|plot| plot.d_tag.clone()) - .collect::<Vec<_>>(); - ids.sort(); - ids.dedup(); - ids -} - -fn load_plots<E: SqlExecutor>( - exec: &E, - farm_id: &str, -) -> Result<Vec<Plot>, RadrootsTangleEventsError> { - let filter = IPlotFieldsFilter { - id: None, - created_at: None, - updated_at: None, - d_tag: None, - farm_id: Some(farm_id.to_string()), - name: None, - about: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }; - let result_query = plot::find_many( - exec, - &IPlotFindMany { - filter: Some(filter), - }, - ); - let result = result_query?; - let mut plots = result.results; - plots.sort_by(|a, b| a.d_tag.cmp(&b.d_tag)); - Ok(plots) -} - -fn load_farm_location<E: SqlExecutor>( - exec: &E, - farm: &Farm, -) -> Result<Option<RadrootsFarmLocation>, RadrootsTangleEventsError> { - let location = load_gcs_location_for_farm(exec, &farm.id)?; - Ok(location.map(|gcs| RadrootsFarmLocation { - primary: farm.location_primary.clone(), - city: farm.location_city.clone(), - region: farm.location_region.clone(), - country: farm.location_country.clone(), - gcs, - })) -} - -fn load_plot_location<E: SqlExecutor>( - exec: &E, - plot: &Plot, -) -> Result<Option<radroots_events::plot::RadrootsPlotLocation>, RadrootsTangleEventsError> { - let location = load_gcs_location_for_plot(exec, &plot.id)?; - Ok( - location.map(|gcs| radroots_events::plot::RadrootsPlotLocation { - primary: plot.location_primary.clone(), - city: plot.location_city.clone(), - region: plot.location_region.clone(), - country: plot.location_country.clone(), - gcs, - }), - ) -} - -fn load_gcs_location_for_farm<E: SqlExecutor>( - exec: &E, - farm_id: &str, -) -> Result<Option<RadrootsGcsLocation>, RadrootsTangleEventsError> { - let primary = load_relation_by_role(exec, farm_id, ROLE_PRIMARY, RelationType::Farm)?; - match primary { - Some(gcs) => Ok(Some(gcs)), - None => load_relation_by_role(exec, farm_id, "", RelationType::Farm), - } -} - -fn load_gcs_location_for_plot<E: SqlExecutor>( - exec: &E, - plot_id: &str, -) -> Result<Option<RadrootsGcsLocation>, RadrootsTangleEventsError> { - let primary = load_relation_by_role(exec, plot_id, ROLE_PRIMARY, RelationType::Plot)?; - match primary { - Some(gcs) => Ok(Some(gcs)), - None => load_relation_by_role(exec, plot_id, "", RelationType::Plot), - } -} - -enum RelationType { - Farm, - Plot, -} - -fn load_relation_by_role<E: SqlExecutor>( - exec: &E, - id: &str, - role: &str, - relation: RelationType, -) -> Result<Option<RadrootsGcsLocation>, RadrootsTangleEventsError> { - let mut rels = match relation { - RelationType::Farm => { - let filter = IFarmGcsLocationFieldsFilter { - id: None, - created_at: None, - updated_at: None, - farm_id: Some(id.to_string()), - gcs_location_id: None, - role: if role.is_empty() { - None - } else { - Some(role.to_string()) - }, - }; - let result_query = farm_gcs_location::find_many( - exec, - &IFarmGcsLocationFindMany { - filter: Some(filter), - }, - ); - let result = result_query?; - result - .results - .into_iter() - .map(RelationRow::Farm) - .collect::<Vec<_>>() - } - RelationType::Plot => { - let filter = IPlotGcsLocationFieldsFilter { - id: None, - created_at: None, - updated_at: None, - plot_id: Some(id.to_string()), - gcs_location_id: None, - role: if role.is_empty() { - None - } else { - Some(role.to_string()) - }, - }; - let result_query = plot_gcs_location::find_many( - exec, - &IPlotGcsLocationFindMany { - filter: Some(filter), - }, - ); - let result = result_query?; - result - .results - .into_iter() - .map(RelationRow::Plot) - .collect::<Vec<_>>() - } - }; - - if rels.is_empty() { - return Ok(None); - } - - rels.sort_by(|a, b| { - let rank = location_role_rank(a.role()).cmp(&location_role_rank(b.role())); - rank.then_with(|| a.gcs_location_id().cmp(b.gcs_location_id())) - }); - let gcs_id = rels[0].gcs_location_id().to_string(); - let gcs_result = gcs_location::find_one( - exec, - &IGcsLocationFindOne::On(IGcsLocationFindOneArgs { - on: GcsLocationQueryBindValues::Id { id: gcs_id }, - }), - ); - let gcs = gcs_result?.result.ok_or_else(|| { - RadrootsTangleEventsError::InvalidData("gcs_location not found".to_string()) - })?; - Ok(Some(gcs_location_to_event(&gcs)?)) -} - -enum RelationRow { - Farm(FarmGcsLocation), - Plot(PlotGcsLocation), -} - -impl RelationRow { - fn gcs_location_id(&self) -> &str { - match self { - Self::Farm(row) => row.gcs_location_id.as_str(), - Self::Plot(row) => row.gcs_location_id.as_str(), - } - } - - fn role(&self) -> &str { - match self { - Self::Farm(row) => row.role.as_str(), - Self::Plot(row) => row.role.as_str(), - } - } -} - -fn location_role_rank(role: &str) -> u8 { - if role == ROLE_PRIMARY { 0 } else { 1 } -} - -fn gcs_location_to_event( - gcs: &GcsLocation, -) -> Result<RadrootsGcsLocation, RadrootsTangleEventsError> { - let point = parse_point(&gcs.point, gcs.lat, gcs.lng); - let polygon = parse_polygon(&gcs.polygon, gcs.lat, gcs.lng); - Ok(RadrootsGcsLocation { - lat: gcs.lat, - lng: gcs.lng, - geohash: gcs.geohash.clone(), - point, - polygon, - accuracy: gcs.accuracy, - altitude: gcs.altitude, - tag_0: gcs.tag_0.clone(), - label: gcs.label.clone(), - area: gcs.area, - elevation: gcs.elevation, - soil: gcs.soil.clone(), - climate: gcs.climate.clone(), - gc_id: gcs.gc_id.clone(), - gc_name: gcs.gc_name.clone(), - gc_admin1_id: gcs.gc_admin1_id.clone(), - gc_admin1_name: gcs.gc_admin1_name.clone(), - gc_country_id: gcs.gc_country_id.clone(), - gc_country_name: gcs.gc_country_name.clone(), - }) -} - -fn parse_point(value: &str, lat: f64, lng: f64) -> RadrootsGeoJsonPoint { - if !value.trim().is_empty() { - if let Ok(parsed) = serde_json::from_str::<RadrootsGeoJsonPoint>(value) { - return parsed; - } - } - geojson_point_from_lat_lng(lat, lng) -} - -fn parse_polygon(value: &str, lat: f64, lng: f64) -> RadrootsGeoJsonPolygon { - if !value.trim().is_empty() { - if let Ok(parsed) = serde_json::from_str::<RadrootsGeoJsonPolygon>(value) { - if !parsed.coordinates.is_empty() && !parsed.coordinates[0].is_empty() { - return parsed; - } - } - } - geojson_polygon_circle_wgs84(lat, lng, 100.0, 64) -} - -fn load_profile<E: SqlExecutor>( - exec: &E, - pubkey: &str, -) -> Result<Option<radroots_tangle_db_schema::nostr_profile::NostrProfile>, RadrootsTangleEventsError> -{ - let result_query = nostr_profile::find_one( - exec, - &INostrProfileFindOne::On(INostrProfileFindOneArgs { - on: NostrProfileQueryBindValues::PublicKey { - public_key: pubkey.to_string(), - }, - }), - ); - let result = result_query?; - Ok(result.result) -} - -fn profile_event( - pubkey: &str, - profile: radroots_tangle_db_schema::nostr_profile::NostrProfile, -) -> Result<RadrootsTangleEventDraft, RadrootsTangleEventsError> { - let profile_type = match profile.profile_type.as_str() { - "individual" | "farmer" => Some(RadrootsProfileType::Individual), - "farm" => Some(RadrootsProfileType::Farm), - "coop" => Some(RadrootsProfileType::Coop), - "any" => Some(RadrootsProfileType::Any), - other => radroots_profile_type_from_tag_value(other), - }; - let profile_event = RadrootsProfile { - name: profile.name, - display_name: profile.display_name, - nip05: profile.nip05, - about: profile.about, - website: profile.website, - picture: profile.picture, - banner: profile.banner, - lud06: profile.lud06, - lud16: profile.lud16, - bot: None, - }; - let content = serialize_profile_content(&profile_event)?; - let mut tags = Vec::new(); - if let Some(profile_type) = profile_type { - let mut tag = Vec::with_capacity(2); - tag.push(RADROOTS_PROFILE_TYPE_TAG_KEY.to_string()); - tag.push(radroots_profile_type_tag_value(profile_type).to_string()); - tags.push(tag); - } - Ok(RadrootsTangleEventDraft { - kind: radroots_events::kinds::KIND_PROFILE, - author: pubkey.to_string(), - content, - tags, - }) -} - -fn serialize_profile_content( - profile: &RadrootsProfile, -) -> Result<String, RadrootsTangleEventsError> { - let mut obj = serde_json::Map::new(); - obj.insert("name".to_string(), Value::from(profile.name.clone())); - if let Some(value) = profile.display_name.as_ref() { - obj.insert("display_name".to_string(), Value::from(value.clone())); - } - if let Some(value) = profile.nip05.as_ref() { - obj.insert("nip05".to_string(), Value::from(value.clone())); - } - if let Some(value) = profile.about.as_ref() { - obj.insert("about".to_string(), Value::from(value.clone())); - } - if let Some(value) = profile.website.as_ref() { - obj.insert("website".to_string(), Value::from(value.clone())); - } - if let Some(value) = profile.picture.as_ref() { - obj.insert("picture".to_string(), Value::from(value.clone())); - } - if let Some(value) = profile.banner.as_ref() { - obj.insert("banner".to_string(), Value::from(value.clone())); - } - if let Some(value) = profile.lud06.as_ref() { - obj.insert("lud06".to_string(), Value::from(value.clone())); - } - if let Some(value) = profile.lud16.as_ref() { - obj.insert("lud16".to_string(), Value::from(value.clone())); - } - canonical_json_string(&Value::Object(obj)) -} - -fn collect_member_pubkeys<E: SqlExecutor>( - exec: &E, - farm_id: &str, -) -> Result<Vec<String>, RadrootsTangleEventsError> { - let members = load_farm_members(exec, farm_id)?; - let mut pubkeys = members - .into_iter() - .map(|row| row.member_pubkey) - .collect::<Vec<_>>(); - pubkeys.sort(); - pubkeys.dedup(); - Ok(pubkeys) -} - -fn collect_profile_pubkeys<E: SqlExecutor>( - exec: &E, - farm: &Farm, -) -> Result<Vec<String>, RadrootsTangleEventsError> { - let mut pubkeys = collect_member_pubkeys(exec, &farm.id)?; - let claims = load_member_claims(exec, &farm.pubkey)?; - pubkeys.extend(claims.into_iter().map(|claim| claim.member_pubkey)); - pubkeys.push(farm.pubkey.clone()); - Ok(pubkeys) -} - -fn load_member_claims<E: SqlExecutor>( - exec: &E, - farm_pubkey: &str, -) -> Result<Vec<FarmMemberClaim>, RadrootsTangleEventsError> { - let filter = IFarmMemberClaimFieldsFilter { - id: None, - created_at: None, - updated_at: None, - member_pubkey: None, - farm_pubkey: Some(farm_pubkey.to_string()), - }; - let result_query = farm_member_claim::find_many( - exec, - &IFarmMemberClaimFindMany { - filter: Some(filter), - }, - ); - let result = result_query?; - Ok(result.results) -} - -fn load_member_claims_for_member<E: SqlExecutor>( - exec: &E, - member_pubkey: &str, -) -> Result<Vec<FarmMemberClaim>, RadrootsTangleEventsError> { - let filter = IFarmMemberClaimFieldsFilter { - id: None, - created_at: None, - updated_at: None, - member_pubkey: Some(member_pubkey.to_string()), - farm_pubkey: None, - }; - let result_query = farm_member_claim::find_many( - exec, - &IFarmMemberClaimFindMany { - filter: Some(filter), - }, - ); - let result = result_query?; - Ok(result.results) -} - -fn parts_to_draft(author: &str, parts: WireEventParts) -> RadrootsTangleEventDraft { - RadrootsTangleEventDraft { - kind: parts.kind, - author: author.to_string(), - content: parts.content, - tags: parts.tags, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_sql_core::SqliteExecutor; - use radroots_tangle_db::{ - farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, - migrations, nostr_profile, plot, plot_gcs_location, plot_tag, - }; - use radroots_tangle_db_schema::farm::{IFarmFields, IFarmFieldsFilter, IFarmFindMany}; - use radroots_tangle_db_schema::farm_gcs_location::{ - IFarmGcsLocationFields, IFarmGcsLocationFindMany, - }; - use radroots_tangle_db_schema::farm_member::IFarmMemberFields; - use radroots_tangle_db_schema::farm_member_claim::IFarmMemberClaimFields; - use radroots_tangle_db_schema::farm_tag::IFarmTagFields; - use radroots_tangle_db_schema::gcs_location::IGcsLocationFields; - use radroots_tangle_db_schema::nostr_profile::INostrProfileFields; - use radroots_tangle_db_schema::plot::{IPlotFields, IPlotFindMany}; - use radroots_tangle_db_schema::plot_gcs_location::{ - IPlotGcsLocationFields, IPlotGcsLocationFindMany, - }; - use radroots_tangle_db_schema::plot_tag::IPlotTagFields; - - fn seed(exec: &SqliteExecutor) -> (Farm, Plot, Plot) { - migrations::run_all_up(exec).expect("migrations"); - let farm = farm::create( - exec, - &IFarmFields { - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), - pubkey: "f".repeat(64), - name: "farm".to_string(), - about: Some("about".to_string()), - website: Some("https://farm.example.com".to_string()), - picture: Some("https://farm.example.com/p.png".to_string()), - banner: Some("https://farm.example.com/b.png".to_string()), - location_primary: Some("primary".to_string()), - location_city: Some("city".to_string()), - location_region: Some("region".to_string()), - location_country: Some("country".to_string()), - }, - ) - .expect("farm") - .result; - - let gcs_primary = gcs_location::create( - exec, - &IGcsLocationFields { - d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), - lat: 10.0, - lng: 20.0, - geohash: "s0".to_string(), - point: "{\"type\":\"Point\",\"coordinates\":[20.0,10.0]}".to_string(), - polygon: - "{\"type\":\"Polygon\",\"coordinates\":[[[20.0,10.0],[20.1,10.1],[19.9,10.1],[20.0,10.0]]]}".to_string(), - accuracy: None, - altitude: None, - tag_0: None, - label: None, - area: None, - elevation: None, - soil: None, - climate: None, - gc_id: None, - gc_name: None, - gc_admin1_id: None, - gc_admin1_name: None, - gc_country_id: None, - gc_country_name: None, - }, - ) - .expect("gcs primary") - .result; - let gcs_secondary = gcs_location::create( - exec, - &IGcsLocationFields { - d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), - lat: 11.0, - lng: 21.0, - geohash: "s1".to_string(), - point: "{".to_string(), - polygon: "{\"type\":\"Polygon\",\"coordinates\":[[]]}".to_string(), - accuracy: None, - altitude: None, - tag_0: None, - label: None, - area: None, - elevation: None, - soil: None, - climate: None, - gc_id: None, - gc_name: None, - gc_admin1_id: None, - gc_admin1_name: None, - gc_country_id: None, - gc_country_name: None, - }, - ) - .expect("gcs secondary") - .result; - - let _ = farm_gcs_location::create( - exec, - &IFarmGcsLocationFields { - farm_id: farm.id.clone(), - gcs_location_id: gcs_secondary.id.clone(), - role: "".to_string(), - }, - ) - .expect("farm gcs secondary"); - let _ = farm_gcs_location::create( - exec, - &IFarmGcsLocationFields { - farm_id: farm.id.clone(), - gcs_location_id: gcs_primary.id.clone(), - role: "primary".to_string(), - }, - ) - .expect("farm gcs primary"); - - let plot_primary = plot::create( - exec, - &IPlotFields { - d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), - farm_id: farm.id.clone(), - name: "plot-primary".to_string(), - about: Some("plot about".to_string()), - location_primary: Some("plot primary".to_string()), - location_city: Some("plot city".to_string()), - location_region: Some("plot region".to_string()), - location_country: Some("plot country".to_string()), - }, - ) - .expect("plot primary") - .result; - let plot_secondary = plot::create( - exec, - &IPlotFields { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), - farm_id: farm.id.clone(), - name: "plot-secondary".to_string(), - about: Some("plot secondary about".to_string()), - location_primary: Some("plot secondary primary".to_string()), - location_city: None, - location_region: None, - location_country: None, - }, - ) - .expect("plot secondary") - .result; - - let _ = plot_gcs_location::create( - exec, - &IPlotGcsLocationFields { - plot_id: plot_primary.id.clone(), - gcs_location_id: gcs_secondary.id.clone(), - role: "secondary".to_string(), - }, - ) - .expect("plot primary secondary relation"); - let _ = plot_gcs_location::create( - exec, - &IPlotGcsLocationFields { - plot_id: plot_primary.id.clone(), - gcs_location_id: gcs_primary.id.clone(), - role: "primary".to_string(), - }, - ) - .expect("plot primary relation"); - let _ = plot_gcs_location::create( - exec, - &IPlotGcsLocationFields { - plot_id: plot_secondary.id.clone(), - gcs_location_id: gcs_secondary.id.clone(), - role: "secondary".to_string(), - }, - ) - .expect("plot secondary relation"); - - let _ = farm_tag::create( - exec, - &IFarmTagFields { - farm_id: farm.id.clone(), - tag: "coffee".to_string(), - }, - ) - .expect("farm tag"); - let _ = plot_tag::create( - exec, - &IPlotTagFields { - plot_id: plot_primary.id.clone(), - tag: "orchard".to_string(), - }, - ) - .expect("plot tag"); - - let _ = farm_member::create( - exec, - &IFarmMemberFields { - farm_id: farm.id.clone(), - member_pubkey: "m".repeat(64), - role: "member".to_string(), - }, - ) - .expect("member"); - let _ = farm_member::create( - exec, - &IFarmMemberFields { - farm_id: farm.id.clone(), - member_pubkey: "o".repeat(64), - role: "owner".to_string(), - }, - ) - .expect("owner"); - let _ = farm_member::create( - exec, - &IFarmMemberFields { - farm_id: farm.id.clone(), - member_pubkey: "u".repeat(64), - role: "worker".to_string(), - }, - ) - .expect("worker"); - let _ = farm_member::create( - exec, - &IFarmMemberFields { - farm_id: farm.id.clone(), - member_pubkey: "x".repeat(64), - role: "member".to_string(), - }, - ) - .expect("member no profile"); - - let _ = farm_member_claim::create( - exec, - &IFarmMemberClaimFields { - member_pubkey: "m".repeat(64), - farm_pubkey: farm.pubkey.clone(), - }, - ) - .expect("claim member"); - let _ = farm_member_claim::create( - exec, - &IFarmMemberClaimFields { - member_pubkey: "x".repeat(64), - farm_pubkey: farm.pubkey.clone(), - }, - ) - .expect("claim member no profile"); - - let _ = nostr_profile::create( - exec, - &INostrProfileFields { - public_key: farm.pubkey.clone(), - profile_type: "farm".to_string(), - name: "farm profile".to_string(), - display_name: None, - about: None, - website: None, - picture: None, - banner: None, - nip05: None, - lud06: None, - lud16: None, - }, - ) - .expect("farm profile"); - let _ = nostr_profile::create( - exec, - &INostrProfileFields { - public_key: "m".repeat(64), - profile_type: "legacy".to_string(), - name: "member profile".to_string(), - display_name: Some("member".to_string()), - about: Some("about".to_string()), - website: Some("https://member.example.com".to_string()), - picture: Some("https://member.example.com/p.png".to_string()), - banner: Some("https://member.example.com/b.png".to_string()), - nip05: Some("member@example.com".to_string()), - lud06: Some("lud06".to_string()), - lud16: Some("lud16".to_string()), - }, - ) - .expect("member profile"); - - (farm, plot_primary, plot_secondary) - } - - #[test] - fn emit_paths_cover_private_and_public_helpers() { - let exec = SqliteExecutor::open_memory().expect("db"); - let (farm_row, plot_primary, plot_secondary) = seed(&exec); - - let by_id = resolve_farm( - &exec, - &RadrootsTangleFarmSelector { - id: Some(farm_row.id.clone()), - d_tag: None, - pubkey: None, - }, - ) - .expect("resolve by id"); - assert_eq!(by_id.id, farm_row.id); - - assert!( - resolve_farm( - &exec, - &RadrootsTangleFarmSelector { - id: Some("00000000-0000-0000-0000-000000000000".to_string()), - d_tag: None, - pubkey: None, - }, - ) - .is_err() - ); - assert!( - resolve_farm( - &exec, - &RadrootsTangleFarmSelector { - id: None, - d_tag: None, - pubkey: None, - }, - ) - .is_err() - ); - - let _ = farm::create( - &exec, - &IFarmFields { - d_tag: farm_row.d_tag.clone(), - pubkey: farm_row.pubkey.clone(), - name: "duplicate".to_string(), - about: None, - website: None, - picture: None, - banner: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }, - ) - .expect("duplicate farm"); - assert!( - resolve_farm( - &exec, - &RadrootsTangleFarmSelector { - id: None, - d_tag: Some(farm_row.d_tag.clone()), - pubkey: Some(farm_row.pubkey.clone()), - }, - ) - .is_err() - ); - - let tags = collect_farm_tags(&exec, &farm_row.id).expect("farm tags"); - assert_eq!(tags, vec!["coffee".to_string()]); - let plot_tags = collect_plot_tags(&exec, &plot_primary.id).expect("plot tags"); - assert_eq!(plot_tags, vec!["orchard".to_string()]); - - let members = load_farm_members(&exec, &farm_row.id).expect("members"); - assert_eq!(role_pubkeys(&members, ROLE_MEMBER).len(), 2); - assert_eq!(role_pubkeys(&members, ROLE_OWNER).len(), 1); - assert_eq!(role_pubkeys(&members, ROLE_WORKER).len(), 1); - let plots = load_plots(&exec, &farm_row.id).expect("plots"); - assert_eq!(sorted_plot_ids(&plots).len(), 2); - - let farm_location = load_farm_location(&exec, &farm_row).expect("farm location"); - assert!(farm_location.is_some()); - let plot_location_primary = load_plot_location(&exec, &plot_primary).expect("plot primary"); - assert!(plot_location_primary.is_some()); - let plot_location_secondary = - load_plot_location(&exec, &plot_secondary).expect("plot secondary"); - assert!(plot_location_secondary.is_some()); - - assert!( - load_relation_by_role(&exec, &farm_row.id, "primary", RelationType::Farm) - .expect("farm primary") - .is_some() - ); - assert!( - load_relation_by_role(&exec, &farm_row.id, "", RelationType::Farm) - .expect("farm fallback") - .is_some() - ); - assert!( - load_relation_by_role(&exec, &plot_secondary.id, "", RelationType::Plot) - .expect("plot fallback") - .is_some() - ); - - let mut farm_rel = - farm_gcs_location::find_many(&exec, &IFarmGcsLocationFindMany { filter: None }) - .expect("farm rels") - .results; - let mut plot_rel = - plot_gcs_location::find_many(&exec, &IPlotGcsLocationFindMany { filter: None }) - .expect("plot rels") - .results; - let farm_row_role = RelationRow::Farm(farm_rel.remove(0)).role().to_string(); - let plot_row_role = RelationRow::Plot(plot_rel.remove(0)).role().to_string(); - let _ = farm_row_role; - let _ = plot_row_role; - assert_eq!(location_role_rank(ROLE_PRIMARY), 0); - assert_eq!(location_role_rank("secondary"), 1); - - let point_valid = parse_point("{\"type\":\"Point\",\"coordinates\":[1.0,2.0]}", 3.0, 4.0); - assert_eq!(point_valid.coordinates, [1.0, 2.0]); - let point_invalid = parse_point("{", 3.0, 4.0); - assert_eq!(point_invalid.coordinates, [4.0, 3.0]); - let point_empty = parse_point("", 3.0, 4.0); - assert_eq!(point_empty.coordinates, [4.0, 3.0]); - - let polygon_valid = parse_polygon( - "{\"type\":\"Polygon\",\"coordinates\":[[[1.0,2.0],[1.1,2.1],[1.0,2.0]]]}", - 3.0, - 4.0, - ); - assert!(!polygon_valid.coordinates[0].is_empty()); - let polygon_empty_outer = - parse_polygon("{\"type\":\"Polygon\",\"coordinates\":[]}", 3.0, 4.0); - assert!(!polygon_empty_outer.coordinates[0].is_empty()); - let polygon_empty_inner = - parse_polygon("{\"type\":\"Polygon\",\"coordinates\":[[]]}", 3.0, 4.0); - assert!(!polygon_empty_inner.coordinates[0].is_empty()); - let polygon_invalid = parse_polygon("{", 3.0, 4.0); - assert!(!polygon_invalid.coordinates[0].is_empty()); - let polygon_blank = parse_polygon("", 3.0, 4.0); - assert!(!polygon_blank.coordinates[0].is_empty()); - - assert!( - load_profile(&exec, &farm_row.pubkey) - .expect("farm profile") - .is_some() - ); - assert!( - load_profile(&exec, &"z".repeat(64)) - .expect("missing profile") - .is_none() - ); - - let profile_event_farm = profile_event( - &farm_row.pubkey, - radroots_tangle_db_schema::nostr_profile::NostrProfile { - id: "00000000-0000-0000-0000-000000000001".to_string(), - created_at: "2024-01-01T00:00:00.000Z".to_string(), - updated_at: "2024-01-01T00:00:00.000Z".to_string(), - public_key: farm_row.pubkey.clone(), - profile_type: "farm".to_string(), - name: "farm".to_string(), - display_name: None, - about: None, - website: None, - picture: None, - banner: None, - nip05: None, - lud06: None, - lud16: None, - }, - ) - .expect("profile farm"); - assert!(!profile_event_farm.tags.is_empty()); - let profile_event_unknown = profile_event( - &"m".repeat(64), - radroots_tangle_db_schema::nostr_profile::NostrProfile { - id: "00000000-0000-0000-0000-000000000002".to_string(), - created_at: "2024-01-01T00:00:00.000Z".to_string(), - updated_at: "2024-01-01T00:00:00.000Z".to_string(), - public_key: "m".repeat(64), - profile_type: "legacy".to_string(), - name: "legacy".to_string(), - display_name: None, - about: None, - website: None, - picture: None, - banner: None, - nip05: None, - lud06: None, - lud16: None, - }, - ) - .expect("profile legacy"); - assert!(profile_event_unknown.tags.is_empty()); - - let profile_content = serialize_profile_content(&RadrootsProfile { - name: "name".to_string(), - display_name: Some("display".to_string()), - nip05: Some("nip05".to_string()), - about: Some("about".to_string()), - website: Some("website".to_string()), - picture: Some("picture".to_string()), - banner: Some("banner".to_string()), - lud06: Some("lud06".to_string()), - lud16: Some("lud16".to_string()), - bot: None, - }) - .expect("serialize profile"); - assert!(profile_content.contains("\"name\":\"name\"")); - - let member_pubkeys = collect_member_pubkeys(&exec, &farm_row.id).expect("member pubkeys"); - assert!(!member_pubkeys.is_empty()); - let profile_pubkeys = collect_profile_pubkeys(&exec, &farm_row).expect("profile pubkeys"); - assert!(!profile_pubkeys.is_empty()); - let claims = load_member_claims(&exec, &farm_row.pubkey).expect("claims"); - assert!(!claims.is_empty()); - let member_claims = - load_member_claims_for_member(&exec, &"m".repeat(64)).expect("claims by member"); - assert!(!member_claims.is_empty()); - - let profile_events = radroots_tangle_profile_events(&exec, &farm_row).expect("profiles"); - assert!(!profile_events.is_empty()); - let farm_event = radroots_tangle_farm_event(&exec, &farm_row).expect("farm event"); - assert_eq!(farm_event.kind, KIND_FARM); - let plot_events = radroots_tangle_plot_events(&exec, &farm_row).expect("plot events"); - assert_eq!(plot_events.len(), 2); - let list_sets = radroots_tangle_list_set_events(&exec, &farm_row).expect("list sets"); - assert_eq!(list_sets.len(), 4); - let membership_claims = - radroots_tangle_membership_claim_events(&exec, &farm_row.pubkey).expect("membership"); - assert!(!membership_claims.is_empty()); - let bundle = radroots_tangle_sync_all_with_options( - &exec, - &RadrootsTangleFarmSelector { - id: Some(farm_row.id.clone()), - d_tag: None, - pubkey: None, - }, - Some(&RadrootsTangleSyncOptions { - include_profiles: Some(true), - include_list_sets: Some(true), - include_membership_claims: Some(true), - }), - ) - .expect("sync all"); - assert!(!bundle.events.is_empty()); - - let _ = exec.exec("PRAGMA foreign_keys = OFF", "[]"); - let _ = plot_gcs_location::create( - &exec, - &IPlotGcsLocationFields { - plot_id: plot_secondary.id.clone(), - gcs_location_id: "00000000-0000-0000-0000-000000000000".to_string(), - role: "".to_string(), - }, - ); - assert!(load_relation_by_role(&exec, &plot_secondary.id, "", RelationType::Plot).is_err()); - - let by_pair = farm::find_many( - &exec, - &IFarmFindMany { - filter: Some(IFarmFieldsFilter { - id: None, - created_at: None, - updated_at: None, - d_tag: Some("AAAAAAAAAAAAAAAAAAAAAA".to_string()), - pubkey: Some("f".repeat(64)), - name: None, - about: None, - website: None, - picture: None, - banner: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }), - }, - ) - .expect("by pair"); - assert!(!by_pair.results.is_empty()); - - let plots_lookup = plot::find_many( - &exec, - &IPlotFindMany { - filter: Some(IPlotFieldsFilter { - id: None, - created_at: None, - updated_at: None, - d_tag: None, - farm_id: Some(farm_row.id), - name: None, - about: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }), - }, - ) - .expect("plots lookup"); - assert_eq!(plots_lookup.results.len(), 2); - } -} diff --git a/crates/tangle-events/src/error.rs b/crates/tangle-events/src/error.rs @@ -1,91 +0,0 @@ -#[cfg(not(feature = "std"))] -use alloc::string::{String, ToString}; - -use core::fmt; - -use radroots_events_codec::error::{EventEncodeError, EventParseError}; -use radroots_sql_core::error::SqlError; -use radroots_types::types::IError; - -#[derive(Debug)] -pub enum RadrootsTangleEventsError { - Sql(IError<SqlError>), - Encode(EventEncodeError), - Parse(EventParseError), - InvalidSelector(String), - InvalidData(String), -} - -impl fmt::Display for RadrootsTangleEventsError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Sql(err) => write!(f, "tangle_events.sql: {}", err.err.to_string()), - Self::Encode(err) => write!(f, "tangle_events.encode: {err}"), - Self::Parse(err) => write!(f, "tangle_events.parse: {err}"), - Self::InvalidSelector(msg) => write!(f, "tangle_events.selector: {msg}"), - Self::InvalidData(msg) => write!(f, "tangle_events.data: {msg}"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for RadrootsTangleEventsError {} - -impl From<IError<SqlError>> for RadrootsTangleEventsError { - fn from(err: IError<SqlError>) -> Self { - Self::Sql(err) - } -} - -impl From<EventEncodeError> for RadrootsTangleEventsError { - fn from(err: EventEncodeError) -> Self { - Self::Encode(err) - } -} - -impl From<EventParseError> for RadrootsTangleEventsError { - fn from(err: EventParseError) -> Self { - Self::Parse(err) - } -} - -#[cfg(test)] -mod tests { - use super::RadrootsTangleEventsError; - use radroots_events_codec::error::{EventEncodeError, EventParseError}; - use radroots_sql_core::error::SqlError; - use radroots_types::types::IError; - - #[test] - fn display_formats_all_error_variants() { - let sql_err = RadrootsTangleEventsError::Sql(IError::from(SqlError::Internal)); - assert!(sql_err.to_string().contains("tangle_events.sql")); - - let encode_err = RadrootsTangleEventsError::Encode(EventEncodeError::InvalidField("name")); - assert!(encode_err.to_string().contains("tangle_events.encode")); - - let parse_err = RadrootsTangleEventsError::Parse(EventParseError::InvalidTag("d")); - assert!(parse_err.to_string().contains("tangle_events.parse")); - - let selector_err = - RadrootsTangleEventsError::InvalidSelector("selector missing".to_string()); - assert!(selector_err.to_string().contains("tangle_events.selector")); - - let data_err = RadrootsTangleEventsError::InvalidData("bad data".to_string()); - assert!(data_err.to_string().contains("tangle_events.data")); - } - - #[test] - fn from_impls_map_into_expected_variants() { - let sql_from: RadrootsTangleEventsError = IError::from(SqlError::Internal).into(); - assert!(matches!(sql_from, RadrootsTangleEventsError::Sql(_))); - - let encode_from: RadrootsTangleEventsError = EventEncodeError::Json.into(); - assert!(matches!(encode_from, RadrootsTangleEventsError::Encode(_))); - - let parse_number_err = "invalid".parse::<u32>().expect_err("parse int should fail"); - let parse_from: RadrootsTangleEventsError = - EventParseError::InvalidNumber("k", parse_number_err).into(); - assert!(matches!(parse_from, RadrootsTangleEventsError::Parse(_))); - } -} diff --git a/crates/tangle-events/src/event_state.rs b/crates/tangle-events/src/event_state.rs @@ -1,74 +0,0 @@ -#[cfg(not(feature = "std"))] -use alloc::format; -#[cfg(not(feature = "std"))] -use alloc::{ - string::{String, ToString}, - vec::Vec, -}; -#[cfg(feature = "std")] -use std::{string::String, vec::Vec}; - -use serde_json::Value; -use sha2::{Digest, Sha256}; - -use crate::error::RadrootsTangleEventsError; - -pub fn event_state_key(kind: u32, pubkey: &str, d_tag: &str) -> String { - format!("{kind}:{pubkey}:{d_tag}") -} - -pub fn event_content_hash( - content: &str, - tags: &[Vec<String>], -) -> Result<String, RadrootsTangleEventsError> { - let tags_json = Value::Array( - tags.iter() - .map(|tag| Value::Array(tag.iter().cloned().map(Value::String).collect())) - .collect(), - ) - .to_string(); - let mut hasher = Sha256::new(); - hasher.update(content.as_bytes()); - hasher.update(tags_json.as_bytes()); - Ok(hex::encode(hasher.finalize())) -} - -pub fn tag_value<'a>(tags: &'a [Vec<String>], key: &str) -> Option<&'a str> { - tags.iter() - .find(|tag| tag.get(0).map(|v| v.as_str()) == Some(key)) - .and_then(|tag| tag.get(1)) - .map(|value| value.as_str()) -} - -#[cfg(test)] -mod tests { - use super::{event_content_hash, event_state_key, tag_value}; - - #[test] - fn event_state_key_formats_consistently() { - let key = event_state_key(30000, "author", "d-tag"); - assert_eq!(key, "30000:author:d-tag"); - } - - #[test] - fn event_content_hash_is_stable_for_same_inputs() { - let tags = vec![vec!["d".to_string(), "tag".to_string()]]; - let first = event_content_hash("content", &tags).expect("hash first"); - let second = event_content_hash("content", &tags).expect("hash second"); - assert_eq!(first, second); - assert_eq!(first.len(), 64); - } - - #[test] - fn tag_value_finds_and_misses_keys() { - let tags = vec![ - vec!["p".to_string(), "member".to_string()], - vec!["d".to_string(), "farm".to_string()], - vec!["x".to_string()], - ]; - assert_eq!(tag_value(&tags, "p"), Some("member")); - assert_eq!(tag_value(&tags, "d"), Some("farm")); - assert_eq!(tag_value(&tags, "x"), None); - assert_eq!(tag_value(&tags, "missing"), None); - } -} diff --git a/crates/tangle-events/src/ingest.rs b/crates/tangle-events/src/ingest.rs @@ -1,1957 +0,0 @@ -#[cfg(not(feature = "std"))] -use alloc::format; -#[cfg(not(feature = "std"))] -use alloc::{ - string::{String, ToString}, - vec::Vec, -}; - -#[cfg(feature = "std")] -use base64::Engine; -#[cfg(feature = "std")] -use base64::engine::general_purpose::URL_SAFE_NO_PAD; - -use radroots_events::RadrootsNostrEvent; -use radroots_events::kinds::{KIND_FARM, KIND_PLOT, KIND_PROFILE, is_nip51_list_set_kind}; -use radroots_events_codec::farm::decode as farm_decode; -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::{ - farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, - nostr_event_state, nostr_profile, plot, plot_gcs_location, plot_tag, -}; -use radroots_tangle_db_schema::farm::{ - FarmQueryBindValues, IFarmFields, IFarmFieldsFilter, IFarmFieldsPartial, IFarmFindMany, - IFarmUpdate, -}; -use radroots_tangle_db_schema::farm_gcs_location::{ - FarmGcsLocationQueryBindValues, IFarmGcsLocationDelete, IFarmGcsLocationFields, - IFarmGcsLocationFieldsFilter, IFarmGcsLocationFindMany, IFarmGcsLocationFindOneArgs, -}; -use radroots_tangle_db_schema::farm_member::{ - FarmMemberQueryBindValues, IFarmMemberDelete, IFarmMemberFields, IFarmMemberFieldsFilter, - IFarmMemberFindMany, IFarmMemberFindOneArgs, -}; -use radroots_tangle_db_schema::farm_member_claim::{ - FarmMemberClaimQueryBindValues, IFarmMemberClaimDelete, IFarmMemberClaimFields, - IFarmMemberClaimFieldsFilter, IFarmMemberClaimFindMany, IFarmMemberClaimFindOneArgs, -}; -use radroots_tangle_db_schema::farm_tag::{ - FarmTagQueryBindValues, IFarmTagDelete, IFarmTagFields, IFarmTagFieldsFilter, IFarmTagFindMany, - IFarmTagFindOneArgs, -}; -use radroots_tangle_db_schema::gcs_location::IGcsLocationFields; -use radroots_tangle_db_schema::nostr_event_state::{ - INostrEventStateFields, INostrEventStateFieldsPartial, INostrEventStateFindOne, - INostrEventStateFindOneArgs, INostrEventStateUpdate, NostrEventStateQueryBindValues, -}; -use radroots_tangle_db_schema::nostr_profile::{ - INostrProfileFields, INostrProfileFieldsPartial, INostrProfileFindOne, - INostrProfileFindOneArgs, INostrProfileUpdate, NostrProfileQueryBindValues, -}; -use radroots_tangle_db_schema::plot::{ - IPlotFields, IPlotFieldsFilter, IPlotFieldsPartial, IPlotFindMany, IPlotUpdate, - PlotQueryBindValues, -}; -use radroots_tangle_db_schema::plot_gcs_location::{ - IPlotGcsLocationDelete, IPlotGcsLocationFields, IPlotGcsLocationFieldsFilter, - IPlotGcsLocationFindMany, IPlotGcsLocationFindOneArgs, PlotGcsLocationQueryBindValues, -}; -use radroots_tangle_db_schema::plot_tag::{ - IPlotTagDelete, IPlotTagFields, IPlotTagFieldsFilter, IPlotTagFindMany, IPlotTagFindOneArgs, - PlotTagQueryBindValues, -}; -use serde_json::Value; - -use crate::error::RadrootsTangleEventsError; -use crate::event_state::{event_content_hash, event_state_key}; -const ROLE_PRIMARY: &str = "primary"; -const ROLE_MEMBER: &str = "member"; -const ROLE_OWNER: &str = "owner"; -const ROLE_WORKER: &str = "worker"; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsTangleIngestOutcome { - Applied, - Skipped, -} - -pub trait RadrootsTangleIdFactory { - fn new_d_tag(&self) -> String; -} - -#[cfg(feature = "std")] -pub struct RadrootsTangleDefaultIdFactory; - -#[cfg(feature = "std")] -impl RadrootsTangleIdFactory for RadrootsTangleDefaultIdFactory { - fn new_d_tag(&self) -> String { - let uuid = uuid::Uuid::now_v7(); - let bytes = uuid.as_bytes(); - URL_SAFE_NO_PAD.encode(bytes) - } -} - -#[cfg(feature = "std")] -pub fn radroots_tangle_ingest_event<E: SqlExecutor>( - exec: &E, - event: &RadrootsNostrEvent, -) -> Result<RadrootsTangleIngestOutcome, RadrootsTangleEventsError> { - radroots_tangle_ingest_event_with_factory(exec, event, &RadrootsTangleDefaultIdFactory) -} - -pub fn radroots_tangle_ingest_event_with_factory<E: SqlExecutor, F: RadrootsTangleIdFactory>( - exec: &E, - event: &RadrootsNostrEvent, - factory: &F, -) -> Result<RadrootsTangleIngestOutcome, RadrootsTangleEventsError> { - exec.begin() - .map_err(|e| RadrootsTangleEventsError::from(radroots_types::types::IError::from(e)))?; - - let outcome = match ingest_event_inner(exec, event, factory) { - Ok(outcome) => { - exec.commit().map_err(|e| { - RadrootsTangleEventsError::from(radroots_types::types::IError::from(e)) - })?; - Ok(outcome) - } - Err(err) => { - let _ = exec.rollback(); - Err(err) - } - }; - - outcome -} - -fn ingest_event_inner<E: SqlExecutor, F: RadrootsTangleIdFactory>( - exec: &E, - event: &RadrootsNostrEvent, - factory: &F, -) -> Result<RadrootsTangleIngestOutcome, RadrootsTangleEventsError> { - match event.kind { - KIND_PROFILE => ingest_profile_event(exec, event), - KIND_FARM => ingest_farm_event(exec, event, factory), - KIND_PLOT => ingest_plot_event(exec, event, factory), - kind if is_nip51_list_set_kind(kind) => ingest_list_set_event(exec, event), - _ => Err(RadrootsTangleEventsError::InvalidData(format!( - "unsupported kind {}", - event.kind - ))), - } -} - -fn ingest_profile_event<E: SqlExecutor>( - exec: &E, - event: &RadrootsNostrEvent, -) -> Result<RadrootsTangleIngestOutcome, RadrootsTangleEventsError> { - let metadata_result = profile_decode::metadata_from_event( - event.id.clone(), - event.author.clone(), - event.created_at, - event.kind, - event.content.clone(), - event.tags.clone(), - ); - let metadata = metadata_result?; - let profile_type = metadata.profile_type.ok_or_else(|| { - RadrootsTangleEventsError::InvalidData("profile_type required".to_string()) - })?; - - let d_tag = "".to_string(); - let decision = event_state_decision(exec, event, &d_tag)?; - if !decision.apply { - return Ok(RadrootsTangleIngestOutcome::Skipped); - } - - let profile_type = match profile_type { - radroots_events::profile::RadrootsProfileType::Individual => "individual", - radroots_events::profile::RadrootsProfileType::Farm => "farm", - radroots_events::profile::RadrootsProfileType::Coop => "coop", - radroots_events::profile::RadrootsProfileType::Any => "any", - radroots_events::profile::RadrootsProfileType::Radrootsd => "radrootsd", - }; - - let existing_result = nostr_profile::find_one( - exec, - &INostrProfileFindOne::On(INostrProfileFindOneArgs { - on: NostrProfileQueryBindValues::PublicKey { - public_key: metadata.author.clone(), - }, - }), - ); - let existing = existing_result?.result; - - match existing { - Some(profile) => { - let fields = INostrProfileFieldsPartial { - public_key: None, - profile_type: Some(Value::from(profile_type)), - name: Some(Value::from(metadata.profile.name)), - display_name: to_value_opt(metadata.profile.display_name), - about: to_value_opt(metadata.profile.about), - website: to_value_opt(metadata.profile.website), - picture: to_value_opt(metadata.profile.picture), - banner: to_value_opt(metadata.profile.banner), - nip05: to_value_opt(metadata.profile.nip05), - lud06: to_value_opt(metadata.profile.lud06), - lud16: to_value_opt(metadata.profile.lud16), - }; - let update_result = nostr_profile::update( - exec, - &INostrProfileUpdate { - on: NostrProfileQueryBindValues::Id { id: profile.id }, - fields, - }, - ); - let _updated = update_result?; - } - None => { - let fields = INostrProfileFields { - public_key: metadata.author.clone(), - profile_type: profile_type.to_string(), - name: metadata.profile.name, - display_name: metadata.profile.display_name, - about: metadata.profile.about, - website: metadata.profile.website, - picture: metadata.profile.picture, - banner: metadata.profile.banner, - nip05: metadata.profile.nip05, - lud06: metadata.profile.lud06, - lud16: metadata.profile.lud16, - }; - let _ = nostr_profile::create(exec, &fields)?; - } - } - - radroots_tangle_ingest_event_state(exec, event, &d_tag, &decision.content_hash)?; - Ok(RadrootsTangleIngestOutcome::Applied) -} - -fn ingest_farm_event<E: SqlExecutor, F: RadrootsTangleIdFactory>( - exec: &E, - event: &RadrootsNostrEvent, - factory: &F, -) -> Result<RadrootsTangleIngestOutcome, RadrootsTangleEventsError> { - let farm = farm_decode::farm_from_event(event.kind, &event.tags, &event.content)?; - let decision = event_state_decision(exec, event, &farm.d_tag)?; - if !decision.apply { - return Ok(RadrootsTangleIngestOutcome::Skipped); - } - - let filter = IFarmFieldsFilter { - id: None, - created_at: None, - updated_at: None, - d_tag: Some(farm.d_tag.clone()), - pubkey: Some(event.author.clone()), - name: None, - about: None, - website: None, - picture: None, - banner: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }; - let existing_result = farm::find_many( - exec, - &IFarmFindMany { - filter: Some(filter), - }, - ); - let existing = existing_result?; - let location = farm.location.clone(); - let (location_primary, location_city, location_region, location_country) = - unpack_farm_location_strings(location.as_ref()); - let farm_id = if let Some(row) = existing.results.get(0) { - let fields = IFarmFieldsPartial { - d_tag: Some(Value::from(farm.d_tag.clone())), - pubkey: Some(Value::from(event.author.clone())), - name: Some(Value::from(farm.name.clone())), - about: to_value_opt(farm.about.clone()), - website: to_value_opt(farm.website.clone()), - picture: to_value_opt(farm.picture.clone()), - banner: to_value_opt(farm.banner.clone()), - location_primary: to_value_opt(location_primary), - location_city: to_value_opt(location_city), - location_region: to_value_opt(location_region), - location_country: to_value_opt(location_country), - }; - let update_result = farm::update( - exec, - &IFarmUpdate { - on: FarmQueryBindValues::Id { id: row.id.clone() }, - fields, - }, - ); - let _updated = update_result?; - row.id.clone() - } else { - let fields = IFarmFields { - d_tag: farm.d_tag.clone(), - pubkey: event.author.clone(), - name: farm.name.clone(), - about: farm.about.clone(), - website: farm.website.clone(), - picture: farm.picture.clone(), - banner: farm.banner.clone(), - location_primary, - location_city, - location_region, - location_country, - }; - farm::create(exec, &fields)?.result.id - }; - - upsert_farm_tags(exec, &farm_id, farm.tags)?; - upsert_farm_location(exec, &farm_id, location, factory)?; - - radroots_tangle_ingest_event_state(exec, event, &farm.d_tag, &decision.content_hash)?; - Ok(RadrootsTangleIngestOutcome::Applied) -} - -fn ingest_plot_event<E: SqlExecutor, F: RadrootsTangleIdFactory>( - exec: &E, - event: &RadrootsNostrEvent, - factory: &F, -) -> Result<RadrootsTangleIngestOutcome, RadrootsTangleEventsError> { - let plot = plot_decode::plot_from_event(event.kind, &event.tags, &event.content)?; - let decision = event_state_decision(exec, event, &plot.d_tag)?; - if !decision.apply { - return Ok(RadrootsTangleIngestOutcome::Skipped); - } - - let farm = find_farm_by_ref(exec, &plot.farm.pubkey, &plot.farm.d_tag)?; - let filter = IPlotFieldsFilter { - id: None, - created_at: None, - updated_at: None, - d_tag: Some(plot.d_tag.clone()), - farm_id: Some(farm.id.clone()), - name: None, - about: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }; - let existing_result = plot::find_many( - exec, - &IPlotFindMany { - filter: Some(filter), - }, - ); - let existing = existing_result?; - let location = plot.location.clone(); - let (location_primary, location_city, location_region, location_country) = - unpack_plot_location_strings(location.as_ref()); - let plot_id = if let Some(row) = existing.results.get(0) { - let fields = IPlotFieldsPartial { - d_tag: Some(Value::from(plot.d_tag.clone())), - farm_id: Some(Value::from(farm.id.clone())), - name: Some(Value::from(plot.name.clone())), - about: to_value_opt(plot.about.clone()), - location_primary: to_value_opt(location_primary), - location_city: to_value_opt(location_city), - location_region: to_value_opt(location_region), - location_country: to_value_opt(location_country), - }; - let update_result = plot::update( - exec, - &IPlotUpdate { - on: PlotQueryBindValues::Id { id: row.id.clone() }, - fields, - }, - ); - let _updated = update_result?; - row.id.clone() - } else { - let fields = IPlotFields { - d_tag: plot.d_tag.clone(), - farm_id: farm.id.clone(), - name: plot.name.clone(), - about: plot.about.clone(), - location_primary, - location_city, - location_region, - location_country, - }; - plot::create(exec, &fields)?.result.id - }; - - upsert_plot_tags(exec, &plot_id, plot.tags)?; - upsert_plot_location(exec, &plot_id, location, factory)?; - - radroots_tangle_ingest_event_state(exec, event, &plot.d_tag, &decision.content_hash)?; - Ok(RadrootsTangleIngestOutcome::Applied) -} - -fn ingest_list_set_event<E: SqlExecutor>( - exec: &E, - event: &RadrootsNostrEvent, -) -> Result<RadrootsTangleIngestOutcome, RadrootsTangleEventsError> { - if event.kind != radroots_events::kinds::KIND_LIST_SET_GENERIC { - return Ok(RadrootsTangleIngestOutcome::Skipped); - } - let list_set = - list_set_decode::list_set_from_tags(event.kind, event.content.clone(), &event.tags)?; - - if list_set.title.is_some() || list_set.description.is_some() || list_set.image.is_some() { - return Err(RadrootsTangleEventsError::InvalidData( - "domain:farm list sets must omit metadata".to_string(), - )); - } - if !list_set.content.is_empty() { - return Err(RadrootsTangleEventsError::InvalidData( - "domain:farm list sets must not include content".to_string(), - )); - } - - let d_tag = list_set.d_tag.clone(); - let decision = event_state_decision(exec, event, &d_tag)?; - if !decision.apply { - return Ok(RadrootsTangleIngestOutcome::Skipped); - } - - if d_tag == "member_of.farms" { - ensure_list_set_entries_tag(&list_set, "p", "member_of.farms")?; - upsert_member_claims(exec, &event.author, &list_set)?; - radroots_tangle_ingest_event_state(exec, event, &d_tag, &decision.content_hash)?; - return Ok(RadrootsTangleIngestOutcome::Applied); - } - - if let Some((farm_d_tag, role)) = parse_farm_list_set_d_tag(&d_tag) { - if role == ListSetRole::Plots { - ensure_list_set_entries_tag(&list_set, "a", "farm plots")?; - radroots_tangle_ingest_event_state(exec, event, &d_tag, &decision.content_hash)?; - return Ok(RadrootsTangleIngestOutcome::Applied); - } - ensure_list_set_entries_tag(&list_set, "p", "farm members")?; - let farm = find_farm_by_ref(exec, &event.author, &farm_d_tag)?; - upsert_farm_members(exec, &farm.id, role, &list_set)?; - radroots_tangle_ingest_event_state(exec, event, &d_tag, &decision.content_hash)?; - return Ok(RadrootsTangleIngestOutcome::Applied); - } - - Err(RadrootsTangleEventsError::InvalidData( - "unsupported list set d_tag".to_string(), - )) -} - -pub fn radroots_tangle_ingest_event_state<E: SqlExecutor>( - exec: &E, - event: &RadrootsNostrEvent, - d_tag: &str, - content_hash: &str, -) -> Result<(), RadrootsTangleEventsError> { - let key = event_state_key(event.kind, &event.author, d_tag); - let existing_result = nostr_event_state::find_one( - exec, - &INostrEventStateFindOne::On(INostrEventStateFindOneArgs { - on: NostrEventStateQueryBindValues::Key { key: key.clone() }, - }), - ); - let existing = existing_result?.result; - - match existing { - Some(state) => { - let fields = INostrEventStateFieldsPartial { - key: None, - kind: None, - pubkey: None, - d_tag: None, - last_event_id: Some(Value::from(event.id.clone())), - last_created_at: Some(Value::from(event.created_at)), - content_hash: Some(Value::from(content_hash.to_string())), - }; - let update_result = nostr_event_state::update( - exec, - &INostrEventStateUpdate { - on: NostrEventStateQueryBindValues::Id { id: state.id }, - fields, - }, - ); - let _updated = update_result?; - } - None => { - let fields = INostrEventStateFields { - key, - kind: event.kind, - pubkey: event.author.clone(), - d_tag: d_tag.to_string(), - last_event_id: event.id.clone(), - last_created_at: event.created_at, - content_hash: content_hash.to_string(), - }; - let _ = nostr_event_state::create(exec, &fields)?; - } - } - - Ok(()) -} - -fn event_state_decision<E: SqlExecutor>( - exec: &E, - event: &RadrootsNostrEvent, - d_tag: &str, -) -> Result<EventStateDecision, RadrootsTangleEventsError> { - let key = event_state_key(event.kind, &event.author, d_tag); - let content_hash = event_content_hash(&event.content, &event.tags)?; - let existing_result = nostr_event_state::find_one( - exec, - &INostrEventStateFindOne::On(INostrEventStateFindOneArgs { - on: NostrEventStateQueryBindValues::Key { key }, - }), - ); - let existing = existing_result?.result; - - if let Some(state) = existing { - if event.created_at < state.last_created_at { - return Ok(EventStateDecision { - apply: false, - content_hash, - }); - } - if event.created_at == state.last_created_at && content_hash == state.content_hash { - return Ok(EventStateDecision { - apply: false, - content_hash, - }); - } - } - - Ok(EventStateDecision { - apply: true, - content_hash, - }) -} - -fn find_farm_by_ref<E: SqlExecutor>( - exec: &E, - pubkey: &str, - d_tag: &str, -) -> Result<radroots_tangle_db_schema::farm::Farm, RadrootsTangleEventsError> { - let filter = IFarmFieldsFilter { - id: None, - created_at: None, - updated_at: None, - d_tag: Some(d_tag.to_string()), - pubkey: Some(pubkey.to_string()), - name: None, - about: None, - website: None, - picture: None, - banner: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }; - let result_query = farm::find_many( - exec, - &IFarmFindMany { - filter: Some(filter), - }, - ); - let result = result_query?; - result - .results - .into_iter() - .next() - .ok_or_else(|| RadrootsTangleEventsError::InvalidData("farm not found".to_string())) -} - -fn upsert_farm_tags<E: SqlExecutor>( - exec: &E, - farm_id: &str, - tags: Option<Vec<String>>, -) -> Result<(), RadrootsTangleEventsError> { - let existing_query = farm_tag::find_many( - exec, - &IFarmTagFindMany { - filter: Some(IFarmTagFieldsFilter { - id: None, - created_at: None, - updated_at: None, - farm_id: Some(farm_id.to_string()), - tag: None, - }), - }, - ); - let existing = existing_query?; - for row in existing.results { - 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(); - tags.sort(); - tags.dedup(); - for tag in tags { - if tag.trim().is_empty() { - continue; - } - let fields = IFarmTagFields { - farm_id: farm_id.to_string(), - tag, - }; - let _ = farm_tag::create(exec, &fields)?; - } - Ok(()) -} - -fn upsert_plot_tags<E: SqlExecutor>( - exec: &E, - plot_id: &str, - tags: Option<Vec<String>>, -) -> Result<(), RadrootsTangleEventsError> { - let existing_query = plot_tag::find_many( - exec, - &IPlotTagFindMany { - filter: Some(IPlotTagFieldsFilter { - id: None, - created_at: None, - updated_at: None, - plot_id: Some(plot_id.to_string()), - tag: None, - }), - }, - ); - let existing = existing_query?; - for row in existing.results { - 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(); - tags.sort(); - tags.dedup(); - for tag in tags { - if tag.trim().is_empty() { - continue; - } - let fields = IPlotTagFields { - plot_id: plot_id.to_string(), - tag, - }; - let _ = plot_tag::create(exec, &fields)?; - } - Ok(()) -} - -fn upsert_farm_location<E: SqlExecutor, F: RadrootsTangleIdFactory>( - exec: &E, - farm_id: &str, - location: Option<radroots_events::farm::RadrootsFarmLocation>, - factory: &F, -) -> Result<(), RadrootsTangleEventsError> { - clear_farm_locations(exec, farm_id)?; - if let Some(location) = location { - let gcs_id = create_gcs_location(exec, location.gcs, factory)?; - let fields = IFarmGcsLocationFields { - farm_id: farm_id.to_string(), - gcs_location_id: gcs_id, - role: ROLE_PRIMARY.to_string(), - }; - let _ = farm_gcs_location::create(exec, &fields)?; - } - Ok(()) -} - -fn upsert_plot_location<E: SqlExecutor, F: RadrootsTangleIdFactory>( - exec: &E, - plot_id: &str, - location: Option<radroots_events::plot::RadrootsPlotLocation>, - factory: &F, -) -> Result<(), RadrootsTangleEventsError> { - clear_plot_locations(exec, plot_id)?; - if let Some(location) = location { - let gcs_id = create_gcs_location(exec, location.gcs, factory)?; - let fields = IPlotGcsLocationFields { - plot_id: plot_id.to_string(), - gcs_location_id: gcs_id, - role: ROLE_PRIMARY.to_string(), - }; - let _ = plot_gcs_location::create(exec, &fields)?; - } - Ok(()) -} - -fn clear_farm_locations<E: SqlExecutor>( - exec: &E, - farm_id: &str, -) -> Result<(), RadrootsTangleEventsError> { - let existing_query = farm_gcs_location::find_many( - exec, - &IFarmGcsLocationFindMany { - filter: Some(IFarmGcsLocationFieldsFilter { - id: None, - created_at: None, - updated_at: None, - farm_id: Some(farm_id.to_string()), - gcs_location_id: None, - role: None, - }), - }, - ); - let existing = existing_query?; - for row in existing.results { - 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(()) -} - -fn clear_plot_locations<E: SqlExecutor>( - exec: &E, - plot_id: &str, -) -> Result<(), RadrootsTangleEventsError> { - let existing_query = plot_gcs_location::find_many( - exec, - &IPlotGcsLocationFindMany { - filter: Some(IPlotGcsLocationFieldsFilter { - id: None, - created_at: None, - updated_at: None, - plot_id: Some(plot_id.to_string()), - gcs_location_id: None, - role: None, - }), - }, - ); - let existing = existing_query?; - for row in existing.results { - 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(()) -} - -fn create_gcs_location<E: SqlExecutor, F: RadrootsTangleIdFactory>( - exec: &E, - gcs: radroots_events::farm::RadrootsGcsLocation, - factory: &F, -) -> Result<String, RadrootsTangleEventsError> { - let d_tag = factory.new_d_tag(); - let point = serde_json::to_string(&gcs.point).map_err(map_gcs_point_serialize_error)?; - let polygon = serde_json::to_string(&gcs.polygon).map_err(map_gcs_polygon_serialize_error)?; - - let fields = IGcsLocationFields { - d_tag, - lat: gcs.lat, - lng: gcs.lng, - geohash: gcs.geohash, - point, - polygon, - accuracy: gcs.accuracy, - altitude: gcs.altitude, - tag_0: gcs.tag_0, - label: gcs.label, - area: gcs.area, - elevation: gcs.elevation, - soil: gcs.soil, - climate: gcs.climate, - gc_id: gcs.gc_id, - gc_name: gcs.gc_name, - gc_admin1_id: gcs.gc_admin1_id, - gc_admin1_name: gcs.gc_admin1_name, - gc_country_id: gcs.gc_country_id, - gc_country_name: gcs.gc_country_name, - }; - let result = gcs_location::create(exec, &fields)?; - Ok(result.result.id) -} - -fn map_gcs_point_serialize_error(_err: serde_json::Error) -> RadrootsTangleEventsError { - RadrootsTangleEventsError::InvalidData("gcs.point".to_string()) -} - -fn map_gcs_polygon_serialize_error(_err: serde_json::Error) -> RadrootsTangleEventsError { - RadrootsTangleEventsError::InvalidData("gcs.polygon".to_string()) -} - -fn upsert_farm_members<E: SqlExecutor>( - exec: &E, - farm_id: &str, - role: ListSetRole, - list_set: &radroots_events::list_set::RadrootsListSet, -) -> Result<(), RadrootsTangleEventsError> { - let role_value = match role { - ListSetRole::Members => ROLE_MEMBER, - ListSetRole::Owners => ROLE_OWNER, - ListSetRole::Workers => ROLE_WORKER, - ListSetRole::Plots => return Ok(()), - }; - let existing_query = farm_member::find_many( - exec, - &IFarmMemberFindMany { - filter: Some(IFarmMemberFieldsFilter { - id: None, - created_at: None, - updated_at: None, - farm_id: Some(farm_id.to_string()), - member_pubkey: None, - role: Some(role_value.to_string()), - }), - }, - ); - let existing = existing_query?; - for row in existing.results { - 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 - .entries - .iter() - .filter(|entry| entry.tag == "p") - .filter_map(|entry| entry.values.get(0)) - .map(|value| value.to_string()) - .collect::<Vec<_>>(); - entries.sort(); - entries.dedup(); - - for pubkey in entries { - let fields = IFarmMemberFields { - farm_id: farm_id.to_string(), - member_pubkey: pubkey, - role: role_value.to_string(), - }; - let _ = farm_member::create(exec, &fields)?; - } - Ok(()) -} - -fn upsert_member_claims<E: SqlExecutor>( - exec: &E, - member_pubkey: &str, - list_set: &radroots_events::list_set::RadrootsListSet, -) -> Result<(), RadrootsTangleEventsError> { - let existing_query = farm_member_claim::find_many( - exec, - &IFarmMemberClaimFindMany { - filter: Some(IFarmMemberClaimFieldsFilter { - id: None, - created_at: None, - updated_at: None, - member_pubkey: Some(member_pubkey.to_string()), - farm_pubkey: None, - }), - }, - ); - let existing = existing_query?; - for row in existing.results { - 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 - .entries - .iter() - .filter(|entry| entry.tag == "p") - .filter_map(|entry| entry.values.get(0)) - .map(|value| value.to_string()) - .collect::<Vec<_>>(); - entries.sort(); - entries.dedup(); - - for farm_pubkey in entries { - let fields = IFarmMemberClaimFields { - member_pubkey: member_pubkey.to_string(), - farm_pubkey, - }; - let _ = farm_member_claim::create(exec, &fields)?; - } - Ok(()) -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum ListSetRole { - Members, - Owners, - Workers, - Plots, -} - -fn unpack_farm_location_strings( - location: Option<&radroots_events::farm::RadrootsFarmLocation>, -) -> ( - Option<String>, - Option<String>, - Option<String>, - Option<String>, -) { - match location { - Some(location) => ( - location.primary.clone(), - location.city.clone(), - location.region.clone(), - location.country.clone(), - ), - None => (None, None, None, None), - } -} - -fn unpack_plot_location_strings( - location: Option<&radroots_events::plot::RadrootsPlotLocation>, -) -> ( - Option<String>, - Option<String>, - Option<String>, - Option<String>, -) { - match location { - Some(location) => ( - location.primary.clone(), - location.city.clone(), - location.region.clone(), - location.country.clone(), - ), - None => (None, None, None, None), - } -} - -fn ensure_list_set_entries_tag( - list_set: &radroots_events::list_set::RadrootsListSet, - expected: &str, - label: &str, -) -> Result<(), RadrootsTangleEventsError> { - for entry in list_set.entries.iter() { - if entry.tag != expected { - return Err(RadrootsTangleEventsError::InvalidData(format!( - "domain:farm list set {label} must only include {expected} tags" - ))); - } - } - Ok(()) -} - -fn parse_farm_list_set_d_tag(d_tag: &str) -> Option<(String, ListSetRole)> { - let mut parts = d_tag.splitn(3, ':'); - if parts.next()? != "farm" { - return None; - } - let farm_d_tag = parts.next()?.to_string(); - let suffix = parts.next()?; - let role = match suffix { - "members" => ListSetRole::Members, - "members.owners" => ListSetRole::Owners, - "members.workers" => ListSetRole::Workers, - "plots" => ListSetRole::Plots, - _ => return None, - }; - Some((farm_d_tag, role)) -} - -fn to_value_opt(value: Option<String>) -> Option<Value> { - Some(match value { - Some(value) => Value::from(value), - None => Value::Null, - }) -} - -struct EventStateDecision { - apply: bool, - content_hash: String, -} - -#[cfg(test)] -mod tests { - use super::*; - use std::sync::Arc; - use std::sync::atomic::{AtomicUsize, Ordering}; - - use radroots_events::farm::{ - RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef, RadrootsGcsLocation, - RadrootsGeoJsonPoint, RadrootsGeoJsonPolygon, - }; - use radroots_events::kinds::{KIND_LIST_SET_FOLLOW, KIND_LIST_SET_GENERIC}; - use radroots_events::list::RadrootsListEntry; - use radroots_events::list_set::RadrootsListSet; - use radroots_events::plot::{RadrootsPlot, RadrootsPlotLocation}; - use radroots_events::profile::{ - RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, RadrootsProfileType, - radroots_profile_type_tag_value, - }; - use radroots_events_codec::farm::encode as farm_encode; - use radroots_events_codec::farm::list_sets as farm_list_sets; - use radroots_events_codec::list_set::encode as list_set_encode; - use radroots_events_codec::plot::encode as plot_encode; - use radroots_sql_core::{ExecOutcome, SqlExecutor, SqliteExecutor}; - use radroots_tangle_db::{ - farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, - migrations, plot, plot_gcs_location, plot_tag, - }; - use radroots_tangle_db_schema::farm::IFarmFields; - use radroots_tangle_db_schema::farm_gcs_location::IFarmGcsLocationFields; - use radroots_tangle_db_schema::farm_member::IFarmMemberFields; - use radroots_tangle_db_schema::farm_member_claim::IFarmMemberClaimFields; - use radroots_tangle_db_schema::farm_tag::IFarmTagFields; - use radroots_tangle_db_schema::gcs_location::IGcsLocationFields; - use radroots_tangle_db_schema::plot::IPlotFields; - use radroots_tangle_db_schema::plot_gcs_location::IPlotGcsLocationFields; - use radroots_tangle_db_schema::plot_tag::IPlotTagFields; - - struct FixedFactory; - - impl RadrootsTangleIdFactory for FixedFactory { - fn new_d_tag(&self) -> String { - "AAAAAAAAAAAAAAAAAAAAAZ".to_string() - } - } - - struct TxnExecutor { - begin_err: Option<SqlError>, - commit_err: Option<SqlError>, - rollback_count: Arc<AtomicUsize>, - } - - impl SqlExecutor for TxnExecutor { - fn exec(&self, _sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> { - Err(SqlError::UnsupportedPlatform) - } - - fn query_raw(&self, _sql: &str, _params_json: &str) -> Result<String, SqlError> { - Err(SqlError::UnsupportedPlatform) - } - - fn begin(&self) -> Result<(), SqlError> { - match self.begin_err.clone() { - Some(err) => Err(err), - None => Ok(()), - } - } - - fn commit(&self) -> Result<(), SqlError> { - match self.commit_err.clone() { - Some(err) => Err(err), - None => Ok(()), - } - } - - fn rollback(&self) -> Result<(), SqlError> { - self.rollback_count.fetch_add(1, Ordering::SeqCst); - Ok(()) - } - } - - struct DeleteErrorExecutor<'a> { - inner: &'a SqliteExecutor, - table_name: &'static str, - err: SqlError, - } - - impl SqlExecutor for DeleteErrorExecutor<'_> { - fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { - let normalized = sql.to_ascii_lowercase(); - if normalized.contains("delete from") && normalized.contains(self.table_name) { - return Err(self.err.clone()); - } - self.inner.exec(sql, params_json) - } - - fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> { - self.inner.query_raw(sql, params_json) - } - - fn begin(&self) -> Result<(), SqlError> { - self.inner.begin() - } - - fn commit(&self) -> Result<(), SqlError> { - self.inner.commit() - } - - fn rollback(&self) -> Result<(), SqlError> { - self.inner.rollback() - } - } - - fn sample_gcs(lat: f64, lng: f64, geohash: &str) -> RadrootsGcsLocation { - RadrootsGcsLocation { - lat, - lng, - geohash: geohash.to_string(), - point: RadrootsGeoJsonPoint { - r#type: "Point".to_string(), - coordinates: [lng, lat], - }, - polygon: RadrootsGeoJsonPolygon { - r#type: "Polygon".to_string(), - coordinates: vec![vec![ - [lng, lat], - [lng, lat + 0.001], - [lng - 0.001, lat + 0.001], - [lng, lat], - ]], - }, - accuracy: Some(1.0), - altitude: Some(2.0), - tag_0: Some("tag".to_string()), - label: Some("label".to_string()), - area: Some(3.0), - elevation: Some(4), - soil: Some("soil".to_string()), - climate: Some("climate".to_string()), - gc_id: Some("gc_id".to_string()), - gc_name: Some("gc_name".to_string()), - gc_admin1_id: Some("gc_admin1_id".to_string()), - gc_admin1_name: Some("gc_admin1_name".to_string()), - gc_country_id: Some("gc_country_id".to_string()), - gc_country_name: Some("gc_country_name".to_string()), - } - } - - fn profile_event( - id: u64, - author: &str, - created_at: u32, - profile_type: Option<RadrootsProfileType>, - name: &str, - ) -> RadrootsNostrEvent { - let profile = RadrootsProfile { - name: name.to_string(), - display_name: Some(format!("{name}-display")), - nip05: Some(format!("{name}@example.com")), - about: Some(format!("{name}-about")), - website: Some("https://example.com".to_string()), - picture: Some("https://example.com/p.png".to_string()), - banner: Some("https://example.com/b.png".to_string()), - lud06: Some("lud06".to_string()), - lud16: Some("lud16".to_string()), - bot: None, - }; - let mut tags = Vec::new(); - if let Some(profile_type) = profile_type { - tags.push(vec![ - RADROOTS_PROFILE_TYPE_TAG_KEY.to_string(), - radroots_profile_type_tag_value(profile_type).to_string(), - ]); - } - RadrootsNostrEvent { - id: format!("{id:064x}"), - author: author.to_string(), - created_at, - kind: KIND_PROFILE, - tags, - content: serde_json::to_string(&profile).expect("profile json"), - sig: "f".repeat(128), - } - } - - fn farm_event( - id: u64, - author: &str, - created_at: u32, - d_tag: &str, - name: &str, - location: Option<RadrootsFarmLocation>, - tags: Option<Vec<String>>, - ) -> RadrootsNostrEvent { - let farm = RadrootsFarm { - d_tag: d_tag.to_string(), - name: name.to_string(), - about: Some("about".to_string()), - website: Some("https://farm.example.com".to_string()), - picture: Some("https://farm.example.com/p.png".to_string()), - banner: Some("https://farm.example.com/b.png".to_string()), - location, - tags, - }; - let tags = farm_encode::farm_build_tags(&farm).expect("farm tags"); - RadrootsNostrEvent { - id: format!("{id:064x}"), - author: author.to_string(), - created_at, - kind: KIND_FARM, - tags, - content: serde_json::to_string(&farm).expect("farm json"), - sig: "f".repeat(128), - } - } - - fn plot_event( - id: u64, - author: &str, - created_at: u32, - d_tag: &str, - farm_ref: RadrootsFarmRef, - name: &str, - location: Option<RadrootsPlotLocation>, - tags: Option<Vec<String>>, - ) -> RadrootsNostrEvent { - let plot = RadrootsPlot { - d_tag: d_tag.to_string(), - farm: farm_ref, - name: name.to_string(), - about: Some("plot-about".to_string()), - location, - tags, - }; - let tags = plot_encode::plot_build_tags(&plot).expect("plot tags"); - RadrootsNostrEvent { - id: format!("{id:064x}"), - author: author.to_string(), - created_at, - kind: KIND_PLOT, - tags, - content: serde_json::to_string(&plot).expect("plot json"), - sig: "f".repeat(128), - } - } - - fn list_set_event( - id: u64, - author: &str, - created_at: u32, - kind: u32, - list_set: &RadrootsListSet, - ) -> RadrootsNostrEvent { - let parts = list_set_encode::to_wire_parts_with_kind(list_set, kind).expect("list set"); - RadrootsNostrEvent { - id: format!("{id:064x}"), - author: author.to_string(), - created_at, - kind, - tags: parts.tags, - content: parts.content, - sig: "f".repeat(128), - } - } - - fn seed_rows(exec: &SqliteExecutor) -> (String, String, String, String) { - migrations::run_all_up(exec).expect("migrations"); - let farm_row = farm::create( - exec, - &IFarmFields { - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), - pubkey: "f".repeat(64), - name: "farm".to_string(), - about: None, - website: None, - picture: None, - banner: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }, - ) - .expect("farm") - .result; - let plot_row = plot::create( - exec, - &IPlotFields { - d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), - farm_id: farm_row.id.clone(), - name: "plot".to_string(), - about: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }, - ) - .expect("plot") - .result; - let gcs_row = gcs_location::create( - exec, - &IGcsLocationFields { - d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), - lat: 1.0, - lng: 2.0, - geohash: "s0".to_string(), - point: "{\"type\":\"Point\",\"coordinates\":[2.0,1.0]}".to_string(), - polygon: - "{\"type\":\"Polygon\",\"coordinates\":[[[2.0,1.0],[2.1,1.1],[1.9,1.1],[2.0,1.0]]]}".to_string(), - accuracy: None, - altitude: None, - tag_0: None, - label: None, - area: None, - elevation: None, - soil: None, - climate: None, - gc_id: None, - gc_name: None, - gc_admin1_id: None, - gc_admin1_name: None, - gc_country_id: None, - gc_country_name: None, - }, - ) - .expect("gcs") - .result; - - let _ = farm_tag::create( - exec, - &IFarmTagFields { - farm_id: farm_row.id.clone(), - tag: "alpha".to_string(), - }, - ) - .expect("farm tag"); - let _ = plot_tag::create( - exec, - &IPlotTagFields { - plot_id: plot_row.id.clone(), - tag: "beta".to_string(), - }, - ) - .expect("plot tag"); - let _ = farm_gcs_location::create( - exec, - &IFarmGcsLocationFields { - farm_id: farm_row.id.clone(), - gcs_location_id: gcs_row.id.clone(), - role: "primary".to_string(), - }, - ) - .expect("farm gcs"); - let _ = plot_gcs_location::create( - exec, - &IPlotGcsLocationFields { - plot_id: plot_row.id.clone(), - gcs_location_id: gcs_row.id.clone(), - role: "primary".to_string(), - }, - ) - .expect("plot gcs"); - let _ = farm_member::create( - exec, - &IFarmMemberFields { - farm_id: farm_row.id.clone(), - member_pubkey: "m".repeat(64), - role: "member".to_string(), - }, - ) - .expect("member"); - let _ = farm_member_claim::create( - exec, - &IFarmMemberClaimFields { - member_pubkey: "m".repeat(64), - farm_pubkey: farm_row.pubkey.clone(), - }, - ) - .expect("claim"); - ( - farm_row.id, - farm_row.pubkey, - farm_row.d_tag, - plot_row.d_tag.clone(), - ) - } - - #[test] - fn ingest_transaction_paths_are_covered() { - let begin_executor = TxnExecutor { - begin_err: Some(SqlError::Internal), - commit_err: None, - rollback_count: Arc::new(AtomicUsize::new(0)), - }; - let event = RadrootsNostrEvent { - id: format!("{:064x}", 1u64), - author: "a".repeat(64), - created_at: 1, - kind: KIND_LIST_SET_FOLLOW, - tags: Vec::new(), - content: String::new(), - sig: "f".repeat(128), - }; - let begin_err = - radroots_tangle_ingest_event_with_factory(&begin_executor, &event, &FixedFactory) - .expect_err("begin"); - assert!(matches!(begin_err, RadrootsTangleEventsError::Sql(_))); - assert!(begin_executor.commit().is_ok()); - assert!(matches!( - begin_executor.exec("select 1", "[]").expect_err("exec"), - SqlError::UnsupportedPlatform - )); - assert!(matches!( - begin_executor - .query_raw("select 1", "[]") - .expect_err("query"), - SqlError::UnsupportedPlatform - )); - - let rollback_count = Arc::new(AtomicUsize::new(0)); - let commit_executor = TxnExecutor { - begin_err: None, - commit_err: Some(SqlError::Internal), - rollback_count: rollback_count.clone(), - }; - let commit_err = - radroots_tangle_ingest_event_with_factory(&commit_executor, &event, &FixedFactory) - .expect_err("commit"); - assert!(matches!(commit_err, RadrootsTangleEventsError::Sql(_))); - assert_eq!(rollback_count.load(Ordering::SeqCst), 0); - - let rollback_executor = TxnExecutor { - begin_err: None, - commit_err: None, - rollback_count: Arc::new(AtomicUsize::new(0)), - }; - let unsupported = RadrootsNostrEvent { - id: format!("{:064x}", 2u64), - author: "a".repeat(64), - created_at: 2, - kind: 42, - tags: Vec::new(), - content: String::new(), - sig: "f".repeat(128), - }; - let err = radroots_tangle_ingest_event_with_factory( - &rollback_executor, - &unsupported, - &FixedFactory, - ) - .expect_err("rollback"); - assert!(err.to_string().contains("unsupported kind")); - assert_eq!(rollback_executor.rollback_count.load(Ordering::SeqCst), 1); - } - - #[test] - fn ingest_core_paths_cover_helpers_and_decisions() { - let exec = SqliteExecutor::open_memory().expect("db"); - migrations::run_all_up(&exec).expect("migrations"); - - let factory = RadrootsTangleDefaultIdFactory; - assert_eq!(factory.new_d_tag().len(), 22); - - let profile_pubkey = "p".repeat(64); - let profile = profile_event( - 10, - &profile_pubkey, - 1, - Some(RadrootsProfileType::Individual), - "alice", - ); - let profile_no_type = profile_event(9, &profile_pubkey, 0, None, "alice-none"); - assert!(ingest_profile_event(&exec, &profile_no_type).is_err()); - assert_eq!( - radroots_tangle_ingest_event(&exec, &profile).expect("ingest wrapper"), - RadrootsTangleIngestOutcome::Applied - ); - let profile_update = profile_event( - 11, - &profile_pubkey, - 2, - Some(RadrootsProfileType::Individual), - "alice-2", - ); - assert_eq!( - ingest_profile_event(&exec, &profile_update).expect("profile update"), - RadrootsTangleIngestOutcome::Applied - ); - let profile_same_time_diff_hash = profile_event( - 12, - &profile_pubkey, - 2, - Some(RadrootsProfileType::Individual), - "alice-3", - ); - let decision = - event_state_decision(&exec, &profile_same_time_diff_hash, "").expect("decision"); - assert!(decision.apply); - - let farm_pubkey = "f".repeat(64); - let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; - let farm = farm_event( - 20, - &farm_pubkey, - 10, - farm_d_tag, - "farm-a", - Some(RadrootsFarmLocation { - primary: Some("primary".to_string()), - city: Some("city".to_string()), - region: Some("region".to_string()), - country: Some("country".to_string()), - gcs: sample_gcs(10.0, 20.0, "s0"), - }), - Some(vec![ - "coffee".to_string(), - "coffee".to_string(), - " ".to_string(), - ]), - ); - assert_eq!( - ingest_farm_event(&exec, &farm, &FixedFactory).expect("farm"), - RadrootsTangleIngestOutcome::Applied - ); - let farm_update = farm_event( - 21, - &farm_pubkey, - 11, - farm_d_tag, - "farm-b", - None, - Some(vec!["market".to_string()]), - ); - assert_eq!( - ingest_farm_event(&exec, &farm_update, &FixedFactory).expect("farm update"), - RadrootsTangleIngestOutcome::Applied - ); - - let plot_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ"; - let plot = plot_event( - 30, - &farm_pubkey, - 20, - plot_d_tag, - RadrootsFarmRef { - pubkey: farm_pubkey.clone(), - d_tag: farm_d_tag.to_string(), - }, - "plot-a", - Some(RadrootsPlotLocation { - primary: Some("p".to_string()), - city: Some("c".to_string()), - region: Some("r".to_string()), - country: Some("k".to_string()), - gcs: sample_gcs(11.0, 21.0, "s1"), - }), - Some(vec!["tag".to_string()]), - ); - assert_eq!( - ingest_plot_event(&exec, &plot, &FixedFactory).expect("plot"), - RadrootsTangleIngestOutcome::Applied - ); - let plot_update = plot_event( - 31, - &farm_pubkey, - 21, - plot_d_tag, - RadrootsFarmRef { - pubkey: farm_pubkey.clone(), - d_tag: farm_d_tag.to_string(), - }, - "plot-b", - None, - Some(vec!["tag2".to_string()]), - ); - assert_eq!( - ingest_plot_event(&exec, &plot_update, &FixedFactory).expect("plot update"), - RadrootsTangleIngestOutcome::Applied - ); - - let members = farm_list_sets::farm_members_list_set(farm_d_tag, vec!["m".repeat(64)]) - .expect("members"); - let owners = - farm_list_sets::farm_owners_list_set(farm_d_tag, vec!["o".repeat(64)]).expect("owners"); - let workers = farm_list_sets::farm_workers_list_set(farm_d_tag, vec!["w".repeat(64)]) - .expect("workers"); - let plots = farm_list_sets::farm_plots_list_set( - farm_d_tag, - &farm_pubkey, - vec![plot_d_tag.to_string()], - ) - .expect("plots"); - let member_of = - farm_list_sets::member_of_farms_list_set(vec![farm_pubkey.clone()]).expect("member_of"); - - for (idx, list_set) in [members, owners, workers, plots, member_of] - .iter() - .enumerate() - { - let event = list_set_event( - 40 + idx as u64, - if list_set.d_tag == "member_of.farms" { - &profile_pubkey - } else { - &farm_pubkey - }, - 30 + idx as u32, - KIND_LIST_SET_GENERIC, - list_set, - ); - assert_eq!( - ingest_list_set_event(&exec, &event).expect("list set"), - RadrootsTangleIngestOutcome::Applied - ); - } - - let bad_description = RadrootsListSet { - d_tag: "member_of.farms".to_string(), - content: String::new(), - entries: vec![RadrootsListEntry { - tag: "p".to_string(), - values: vec![farm_pubkey.clone()], - }], - title: None, - description: Some("bad".to_string()), - image: None, - }; - let bad_description_event = list_set_event( - 90, - &profile_pubkey, - 100, - KIND_LIST_SET_GENERIC, - &bad_description, - ); - assert!(ingest_list_set_event(&exec, &bad_description_event).is_err()); - - let bad_image = RadrootsListSet { - d_tag: "member_of.farms".to_string(), - content: String::new(), - entries: vec![RadrootsListEntry { - tag: "p".to_string(), - values: vec![farm_pubkey.clone()], - }], - title: None, - description: None, - image: Some("bad".to_string()), - }; - let bad_image_event = - list_set_event(91, &profile_pubkey, 101, KIND_LIST_SET_GENERIC, &bad_image); - assert!(ingest_list_set_event(&exec, &bad_image_event).is_err()); - - assert!(parse_farm_list_set_d_tag("farm:AAAAAAAAAAAAAAAAAAAAAA:unknown").is_none()); - assert!(parse_farm_list_set_d_tag("farm:AAAAAAAAAAAAAAAAAAAAAA:plots").is_some()); - assert_eq!(to_value_opt(Some("x".to_string())), Some(Value::from("x"))); - assert_eq!(to_value_opt(None), Some(Value::Null)); - let location = RadrootsFarmLocation { - primary: Some("p".to_string()), - city: Some("c".to_string()), - region: Some("r".to_string()), - country: Some("k".to_string()), - gcs: sample_gcs(12.0, 22.0, "s2"), - }; - assert_eq!( - unpack_farm_location_strings(Some(&location)).0, - Some("p".to_string()) - ); - assert_eq!( - unpack_plot_location_strings(Some(&RadrootsPlotLocation { - primary: Some("p".to_string()), - city: None, - region: None, - country: None, - gcs: sample_gcs(13.0, 23.0, "s3"), - })) - .0, - Some("p".to_string()) - ); - assert!(ensure_list_set_entries_tag(&bad_image, "p", "x").is_ok()); - assert!( - ensure_list_set_entries_tag( - &RadrootsListSet { - d_tag: "x".to_string(), - content: String::new(), - entries: vec![RadrootsListEntry { - tag: "a".to_string(), - values: vec!["x".to_string()], - }], - title: None, - description: None, - image: None, - }, - "p", - "x", - ) - .is_err() - ); - } - - #[test] - fn ingest_delete_error_paths_are_covered() { - let exec = SqliteExecutor::open_memory().expect("db"); - let (farm_id, _farm_pubkey, farm_d_tag, _plot_d_tag) = seed_rows(&exec); - - let not_found_farm_tags = DeleteErrorExecutor { - inner: &exec, - table_name: "farm_tag", - err: SqlError::NotFound("farm_tag".to_string()), - }; - assert!( - upsert_farm_tags( - ¬_found_farm_tags, - &farm_id, - Some(vec!["next".to_string()]) - ) - .is_ok() - ); - - let not_found_plot_tags = DeleteErrorExecutor { - inner: &exec, - table_name: "plot_tag", - err: SqlError::NotFound("plot_tag".to_string()), - }; - let plot_id = plot::find_many(&exec, &IPlotFindMany { filter: None }) - .expect("plots") - .results[0] - .id - .clone(); - assert!( - upsert_plot_tags( - ¬_found_plot_tags, - &plot_id, - Some(vec!["next".to_string()]) - ) - .is_ok() - ); - - let not_found_farm_locations = DeleteErrorExecutor { - inner: &exec, - table_name: "farm_gcs_location", - err: SqlError::NotFound("farm_gcs_location".to_string()), - }; - assert!( - upsert_farm_location( - ¬_found_farm_locations, - &farm_id, - Some(RadrootsFarmLocation { - primary: None, - city: None, - region: None, - country: None, - gcs: sample_gcs(1.0, 2.0, "s4"), - }), - &FixedFactory, - ) - .is_ok() - ); - - let not_found_plot_locations = DeleteErrorExecutor { - inner: &exec, - table_name: "plot_gcs_location", - err: SqlError::NotFound("plot_gcs_location".to_string()), - }; - assert!( - upsert_plot_location( - ¬_found_plot_locations, - &plot_id, - Some(RadrootsPlotLocation { - primary: None, - city: None, - region: None, - country: None, - gcs: sample_gcs(1.1, 2.1, "s5"), - }), - &FixedFactory, - ) - .is_ok() - ); - - let members_list_set = - farm_list_sets::farm_members_list_set(&farm_d_tag, vec!["n".repeat(64)]) - .expect("members"); - assert!( - upsert_farm_members(&exec, &farm_id, ListSetRole::Members, &members_list_set).is_ok() - ); - let not_found_members = DeleteErrorExecutor { - inner: &exec, - table_name: "farm_member", - err: SqlError::NotFound("farm_member".to_string()), - }; - let not_found_members_list_set = - farm_list_sets::farm_members_list_set(&farm_d_tag, vec!["q".repeat(64)]) - .expect("not found members"); - assert!( - upsert_farm_members( - ¬_found_members, - &farm_id, - ListSetRole::Members, - ¬_found_members_list_set, - ) - .is_ok() - ); - assert!( - upsert_farm_members( - ¬_found_members, - &farm_id, - ListSetRole::Plots, - ¬_found_members_list_set, - ) - .is_ok() - ); - - let member_claims = - farm_list_sets::member_of_farms_list_set(vec!["z".repeat(64)]).expect("claims"); - assert!(upsert_member_claims(&exec, &"m".repeat(64), &member_claims).is_ok()); - let not_found_claims = DeleteErrorExecutor { - inner: &exec, - table_name: "farm_member_claim", - err: SqlError::NotFound("farm_member_claim".to_string()), - }; - let not_found_member_claims = - farm_list_sets::member_of_farms_list_set(vec!["y".repeat(64)]).expect("claims nf"); - assert!( - upsert_member_claims(¬_found_claims, &"m".repeat(64), ¬_found_member_claims) - .is_ok() - ); - assert!(not_found_claims.begin().is_ok()); - assert!(not_found_claims.commit().is_ok()); - let _ = not_found_claims.rollback(); - assert!(not_found_claims.query_raw("SELECT 1", "[]").is_ok()); - assert!(matches!( - not_found_claims.exec("DELETE FROM farm_member_claim WHERE id = 1", "[]"), - Err(SqlError::NotFound(_)) - )); - let _ = not_found_claims.exec("DELETE FROM other_table WHERE id = 1", "[]"); - - let internal_farm_tags = DeleteErrorExecutor { - inner: &exec, - table_name: "farm_tag", - err: SqlError::Internal, - }; - assert!( - upsert_farm_tags(&internal_farm_tags, &farm_id, Some(vec!["x".to_string()])).is_err() - ); - - let internal_plot_tags = DeleteErrorExecutor { - inner: &exec, - table_name: "plot_tag", - err: SqlError::Internal, - }; - assert!( - upsert_plot_tags(&internal_plot_tags, &plot_id, Some(vec!["x".to_string()])).is_err() - ); - - let internal_farm_locations = DeleteErrorExecutor { - inner: &exec, - table_name: "farm_gcs_location", - err: SqlError::Internal, - }; - assert!( - upsert_farm_location( - &internal_farm_locations, - &farm_id, - Some(RadrootsFarmLocation { - primary: None, - city: None, - region: None, - country: None, - gcs: sample_gcs(2.0, 3.0, "s6"), - }), - &FixedFactory, - ) - .is_err() - ); - - let internal_plot_locations = DeleteErrorExecutor { - inner: &exec, - table_name: "plot_gcs_location", - err: SqlError::Internal, - }; - assert!( - upsert_plot_location( - &internal_plot_locations, - &plot_id, - Some(RadrootsPlotLocation { - primary: None, - city: None, - region: None, - country: None, - gcs: sample_gcs(2.1, 3.1, "s7"), - }), - &FixedFactory, - ) - .is_err() - ); - - let internal_members = DeleteErrorExecutor { - inner: &exec, - table_name: "farm_member", - err: SqlError::Internal, - }; - assert!( - upsert_farm_members( - &internal_members, - &farm_id, - ListSetRole::Members, - &members_list_set, - ) - .is_err() - ); - - let internal_claims = DeleteErrorExecutor { - inner: &exec, - table_name: "farm_member_claim", - err: SqlError::Internal, - }; - assert!(upsert_member_claims(&internal_claims, &"m".repeat(64), &member_claims).is_err()); - } - - #[test] - fn create_gcs_location_error_mapping_helpers_are_covered() { - let point_json_err = serde_json::from_str::<Value>("{").expect_err("invalid json"); - let point_err = map_gcs_point_serialize_error(point_json_err); - assert_eq!(point_err.to_string(), "tangle_events.data: gcs.point"); - - let polygon_json_err = serde_json::from_str::<Value>("{").expect_err("invalid json"); - let polygon_err = map_gcs_polygon_serialize_error(polygon_json_err); - assert_eq!(polygon_err.to_string(), "tangle_events.data: gcs.polygon"); - } -} diff --git a/crates/tangle-events/src/lib.rs b/crates/tangle-events/src/lib.rs @@ -1,37 +0,0 @@ -#![forbid(unsafe_code)] -#![cfg_attr(not(feature = "std"), no_std)] - -#[cfg(not(feature = "std"))] -extern crate alloc; - -mod canonical; -pub mod emit; -pub mod error; -mod event_state; -mod geo; -pub mod ingest; -pub mod sync_state; -pub mod types; - -pub use emit::{ - radroots_tangle_farm_event, radroots_tangle_list_set_events, - radroots_tangle_membership_claim_events, radroots_tangle_plot_events, - radroots_tangle_profile_events, radroots_tangle_sync_all, - radroots_tangle_sync_all_with_options, -}; -pub use error::RadrootsTangleEventsError; -pub use ingest::{ - RadrootsTangleIdFactory, RadrootsTangleIngestOutcome, radroots_tangle_ingest_event_state, - radroots_tangle_ingest_event_with_factory, -}; -pub use sync_state::{RadrootsTangleSyncStatus, radroots_tangle_sync_status}; -pub use types::{ - RADROOTS_TANGLE_TRANSFER_VERSION, RadrootsTangleEventDraft, RadrootsTangleFarmSelector, - RadrootsTangleSyncBundle, RadrootsTangleSyncOptions, RadrootsTangleSyncRequest, -}; - -#[cfg(feature = "std")] -pub use ingest::{RadrootsTangleDefaultIdFactory, radroots_tangle_ingest_event}; - -#[cfg(test)] -mod tests; diff --git a/crates/tangle-events/src/sync_state.rs b/crates/tangle-events/src/sync_state.rs @@ -1,68 +0,0 @@ -#[cfg(not(feature = "std"))] -use alloc::{ - collections::BTreeMap, - string::{String, ToString}, -}; -#[cfg(feature = "std")] -use std::collections::BTreeMap; - -use radroots_sql_core::SqlExecutor; -use radroots_tangle_db_schema::farm::IFarmFindMany; -use radroots_tangle_db_schema::nostr_event_state::INostrEventStateFindMany; - -use crate::error::RadrootsTangleEventsError; -use crate::event_state::{event_content_hash, event_state_key, tag_value}; -use crate::types::RadrootsTangleFarmSelector; - -#[derive(Clone, Debug)] -pub struct RadrootsTangleSyncStatus { - pub expected_count: usize, - pub pending_count: usize, -} - -pub fn radroots_tangle_sync_status<E: SqlExecutor>( - exec: &E, -) -> Result<RadrootsTangleSyncStatus, RadrootsTangleEventsError> { - let farms = radroots_tangle_db::farm::find_many(exec, &IFarmFindMany { filter: None })?.results; - let mut expected: BTreeMap<String, String> = BTreeMap::new(); - - for farm in farms { - let selector = RadrootsTangleFarmSelector { - id: Some(farm.id), - d_tag: None, - pubkey: None, - }; - let bundle = crate::emit::radroots_tangle_sync_all_with_options(exec, &selector, None)?; - for event in bundle.events { - let d_tag = tag_value(&event.tags, "d").unwrap_or(""); - let key = event_state_key(event.kind, &event.author, d_tag); - let content_hash = event_content_hash(&event.content, &event.tags)?; - expected.entry(key).or_insert(content_hash); - } - } - - let states_query = radroots_tangle_db::nostr_event_state::find_many( - exec, - &INostrEventStateFindMany { filter: None }, - ); - let states_result = states_query?; - let states = states_result.results; - - let mut state_map: BTreeMap<String, String> = BTreeMap::new(); - for state in states { - state_map.insert(state.key, state.content_hash); - } - - let mut pending = 0; - for (key, content_hash) in expected.iter() { - match state_map.get(key) { - Some(existing) if existing == content_hash => {} - _ => pending += 1, - } - } - - Ok(RadrootsTangleSyncStatus { - expected_count: expected.len(), - pending_count: pending, - }) -} diff --git a/crates/tangle-events/src/tests.rs b/crates/tangle-events/src/tests.rs @@ -1,235 +0,0 @@ -use crate::{ - RADROOTS_TANGLE_TRANSFER_VERSION, RadrootsTangleFarmSelector, RadrootsTangleSyncRequest, - radroots_tangle_sync_all, -}; -use radroots_events::farm::{RadrootsGeoJsonPoint, RadrootsGeoJsonPolygon}; -use radroots_events::kinds::{KIND_FARM, KIND_LIST_SET_GENERIC, KIND_PLOT, KIND_PROFILE}; -use radroots_sql_core::SqliteExecutor; -use radroots_sql_core::error::SqlError; -use radroots_tangle_db::{ - farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, migrations, - nostr_profile, plot, plot_gcs_location, plot_tag, -}; -use radroots_tangle_db_schema::farm::IFarmFields; -use radroots_tangle_db_schema::farm_gcs_location::IFarmGcsLocationFields; -use radroots_tangle_db_schema::farm_member::IFarmMemberFields; -use radroots_tangle_db_schema::farm_member_claim::IFarmMemberClaimFields; -use radroots_tangle_db_schema::farm_tag::IFarmTagFields; -use radroots_tangle_db_schema::gcs_location::IGcsLocationFields; -use radroots_tangle_db_schema::nostr_profile::INostrProfileFields; -use radroots_tangle_db_schema::plot::IPlotFields; -use radroots_tangle_db_schema::plot_gcs_location::IPlotGcsLocationFields; -use radroots_tangle_db_schema::plot_tag::IPlotTagFields; -use radroots_types::types::IError; - -fn unwrap_sql<T>(result: Result<T, IError<SqlError>>, label: &str) -> T { - match result { - Ok(value) => value, - Err(err) => panic!("{label}: {}", err.err), - } -} - -#[test] -fn sync_all_emits_expected_order() { - let exec = SqliteExecutor::open_memory().expect("exec"); - migrations::run_all_up(&exec).expect("migrations"); - - let farm_pubkey = "f".repeat(64); - let farm_fields = IFarmFields { - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), - pubkey: farm_pubkey.clone(), - name: "Green Farm".to_string(), - about: Some("About".to_string()), - website: None, - picture: None, - banner: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }; - let farm_row = unwrap_sql(farm::create(&exec, &farm_fields), "farm").result; - - let gcs_point = RadrootsGeoJsonPoint { - r#type: "Point".to_string(), - coordinates: [-122.4, 37.7], - }; - let gcs_polygon = RadrootsGeoJsonPolygon { - r#type: "Polygon".to_string(), - coordinates: vec![vec![ - [-122.4, 37.7], - [-122.4, 37.701], - [-122.401, 37.701], - [-122.4, 37.7], - ]], - }; - let gcs_fields = IGcsLocationFields { - d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), - lat: 37.7, - lng: -122.4, - geohash: "9q8yy".to_string(), - point: serde_json::to_string(&gcs_point).expect("point"), - polygon: serde_json::to_string(&gcs_polygon).expect("polygon"), - accuracy: None, - altitude: None, - tag_0: None, - label: None, - area: None, - elevation: None, - soil: None, - climate: None, - gc_id: None, - gc_name: None, - gc_admin1_id: None, - gc_admin1_name: None, - gc_country_id: None, - gc_country_name: None, - }; - let gcs_row = unwrap_sql(gcs_location::create(&exec, &gcs_fields), "gcs").result; - - let farm_gcs_fields = IFarmGcsLocationFields { - farm_id: farm_row.id.clone(), - gcs_location_id: gcs_row.id.clone(), - role: "primary".to_string(), - }; - let _ = unwrap_sql( - farm_gcs_location::create(&exec, &farm_gcs_fields), - "farm_gcs", - ); - - let plot_fields = IPlotFields { - d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), - farm_id: farm_row.id.clone(), - name: "Plot A".to_string(), - about: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }; - let plot_row = unwrap_sql(plot::create(&exec, &plot_fields), "plot").result; - - let plot_gcs_fields = IPlotGcsLocationFields { - plot_id: plot_row.id.clone(), - gcs_location_id: gcs_row.id.clone(), - role: "primary".to_string(), - }; - let _ = unwrap_sql( - plot_gcs_location::create(&exec, &plot_gcs_fields), - "plot_gcs", - ); - - let _ = unwrap_sql( - farm_tag::create( - &exec, - &IFarmTagFields { - farm_id: farm_row.id.clone(), - tag: "coffee".to_string(), - }, - ), - "farm_tag", - ); - - let _ = unwrap_sql( - plot_tag::create( - &exec, - &IPlotTagFields { - plot_id: plot_row.id.clone(), - tag: "orchard".to_string(), - }, - ), - "plot_tag", - ); - - let owner_pubkey = "o".repeat(64); - let _ = unwrap_sql( - farm_member::create( - &exec, - &IFarmMemberFields { - farm_id: farm_row.id.clone(), - member_pubkey: owner_pubkey.clone(), - role: "owner".to_string(), - }, - ), - "farm_member", - ); - - let _ = unwrap_sql( - farm_member_claim::create( - &exec, - &IFarmMemberClaimFields { - member_pubkey: owner_pubkey.clone(), - farm_pubkey: farm_pubkey.clone(), - }, - ), - "farm_member_claim", - ); - - let _ = unwrap_sql( - nostr_profile::create( - &exec, - &INostrProfileFields { - public_key: farm_pubkey.clone(), - profile_type: "farm".to_string(), - name: "Farm Profile".to_string(), - display_name: None, - about: None, - website: None, - picture: None, - banner: None, - nip05: None, - lud06: None, - lud16: None, - }, - ), - "farm_profile", - ); - - let _ = unwrap_sql( - nostr_profile::create( - &exec, - &INostrProfileFields { - public_key: owner_pubkey.clone(), - profile_type: "individual".to_string(), - name: "Owner".to_string(), - display_name: None, - about: None, - website: None, - picture: None, - banner: None, - nip05: None, - lud06: None, - lud16: None, - }, - ), - "owner_profile", - ); - - let request = RadrootsTangleSyncRequest { - farm: RadrootsTangleFarmSelector { - id: Some(farm_row.id.clone()), - d_tag: None, - pubkey: None, - }, - options: None, - }; - let bundle = radroots_tangle_sync_all(&exec, &request).expect("sync"); - - assert_eq!(bundle.version, RADROOTS_TANGLE_TRANSFER_VERSION); - assert_eq!(bundle.events.len(), 9); - let kinds = bundle - .events - .iter() - .map(|event| event.kind) - .collect::<Vec<_>>(); - assert_eq!(kinds[0], KIND_PROFILE); - assert_eq!(kinds[1], KIND_PROFILE); - assert_eq!(kinds[2], KIND_FARM); - assert_eq!(kinds[3], KIND_PLOT); - assert!( - kinds[4..8] - .iter() - .all(|kind| *kind == KIND_LIST_SET_GENERIC) - ); - assert_eq!(kinds[8], KIND_LIST_SET_GENERIC); -} diff --git a/crates/tangle-events/src/types.rs b/crates/tangle-events/src/types.rs @@ -1,40 +0,0 @@ -#[cfg(not(feature = "std"))] -use alloc::{string::String, vec::Vec}; - -use serde::{Deserialize, Serialize}; - -pub const RADROOTS_TANGLE_TRANSFER_VERSION: u32 = 1; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RadrootsTangleEventDraft { - pub kind: u32, - pub author: String, - pub content: String, - pub tags: Vec<Vec<String>>, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RadrootsTangleSyncBundle { - pub version: u32, - pub events: Vec<RadrootsTangleEventDraft>, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RadrootsTangleFarmSelector { - pub id: Option<String>, - pub d_tag: Option<String>, - pub pubkey: Option<String>, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RadrootsTangleSyncOptions { - pub include_profiles: Option<bool>, - pub include_list_sets: Option<bool>, - pub include_membership_claims: Option<bool>, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RadrootsTangleSyncRequest { - pub farm: RadrootsTangleFarmSelector, - pub options: Option<RadrootsTangleSyncOptions>, -} diff --git a/crates/tangle-events/tests/ingest_roundtrip.rs b/crates/tangle-events/tests/ingest_roundtrip.rs @@ -1,1427 +0,0 @@ -use radroots_events::RadrootsNostrEvent; -use radroots_events::farm::{ - RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef, RadrootsGcsLocation, RadrootsGeoJsonPoint, - RadrootsGeoJsonPolygon, -}; -use radroots_events::kinds::{ - KIND_FARM, KIND_LIST_SET_FOLLOW, KIND_LIST_SET_GENERIC, KIND_PLOT, KIND_PROFILE, -}; -use radroots_events::list::RadrootsListEntry; -use radroots_events::list_set::RadrootsListSet; -use radroots_events::plot::{RadrootsPlot, RadrootsPlotLocation}; -use radroots_events::profile::{ - RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, RadrootsProfileType, - radroots_profile_type_tag_value, -}; -use radroots_events_codec::error::{EventEncodeError, EventParseError}; -use radroots_events_codec::farm::encode as farm_encode; -use radroots_events_codec::farm::list_sets as farm_list_sets; -use radroots_events_codec::list_set::encode as list_set_encode; -use radroots_events_codec::plot::encode as plot_encode; -use radroots_sql_core::SqlExecutor; -use radroots_sql_core::SqliteExecutor; -use radroots_sql_core::error::SqlError; -use radroots_tangle_db::{ - farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, migrations, - nostr_profile, plot, plot_gcs_location, plot_tag, -}; -use radroots_tangle_db_schema::farm::{IFarmFields, IFarmFieldsFilter, IFarmFindMany}; -use radroots_tangle_db_schema::farm_gcs_location::IFarmGcsLocationFields; -use radroots_tangle_db_schema::farm_member::{ - IFarmMemberFields, IFarmMemberFieldsFilter, IFarmMemberFindMany, -}; -use radroots_tangle_db_schema::farm_member_claim::{ - IFarmMemberClaimFields, IFarmMemberClaimFieldsFilter, IFarmMemberClaimFindMany, -}; -use radroots_tangle_db_schema::farm_tag::{IFarmTagFields, IFarmTagFieldsFilter, IFarmTagFindMany}; -use radroots_tangle_db_schema::gcs_location::IGcsLocationFields; -use radroots_tangle_db_schema::nostr_profile::INostrProfileFields; -use radroots_tangle_db_schema::plot::IPlotFields; -use radroots_tangle_db_schema::plot_gcs_location::IPlotGcsLocationFields; -use radroots_tangle_db_schema::plot_tag::{IPlotTagFields, IPlotTagFieldsFilter, IPlotTagFindMany}; -use radroots_tangle_events::{ - RADROOTS_TANGLE_TRANSFER_VERSION, RadrootsTangleEventDraft, RadrootsTangleEventsError, - RadrootsTangleFarmSelector, RadrootsTangleIngestOutcome, RadrootsTangleSyncOptions, - RadrootsTangleSyncRequest, radroots_tangle_ingest_event, radroots_tangle_sync_all, - radroots_tangle_sync_status, -}; -use radroots_types::types::IError; - -fn unwrap_sql<T>(result: Result<T, IError<SqlError>>, label: &str) -> T { - match result { - Ok(value) => value, - Err(err) => panic!("{label}: {}", err.err), - } -} - -fn draft_to_event(draft: &RadrootsTangleEventDraft, index: u32) -> RadrootsNostrEvent { - RadrootsNostrEvent { - id: format!("{:064x}", index as u64 + 1), - author: draft.author.clone(), - created_at: 1_720_000_000 + index, - kind: draft.kind, - tags: draft.tags.clone(), - content: draft.content.clone(), - sig: "f".repeat(128), - } -} - -fn seed_source( - exec: &SqliteExecutor, -) -> ( - RadrootsTangleSyncRequest, - String, - String, - Vec<RadrootsTangleEventDraft>, -) { - migrations::run_all_up(exec).expect("migrations"); - - let farm_pubkey = "f".repeat(64); - let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA".to_string(); - let farm_fields = IFarmFields { - d_tag: farm_d_tag.clone(), - pubkey: farm_pubkey.clone(), - name: "Green Farm".to_string(), - about: Some("About".to_string()), - website: None, - picture: None, - banner: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }; - let farm_row = unwrap_sql(farm::create(exec, &farm_fields), "farm").result; - - let point = radroots_events::farm::RadrootsGeoJsonPoint { - r#type: "Point".to_string(), - coordinates: [-122.4, 37.7], - }; - let polygon = radroots_events::farm::RadrootsGeoJsonPolygon { - r#type: "Polygon".to_string(), - coordinates: vec![vec![ - [-122.4, 37.7], - [-122.4, 37.701], - [-122.401, 37.701], - [-122.4, 37.7], - ]], - }; - let gcs_fields = IGcsLocationFields { - d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), - lat: 37.7, - lng: -122.4, - geohash: "9q8yy".to_string(), - point: serde_json::to_string(&point).expect("point"), - polygon: serde_json::to_string(&polygon).expect("polygon"), - accuracy: None, - altitude: None, - tag_0: None, - label: None, - area: None, - elevation: None, - soil: None, - climate: None, - gc_id: None, - gc_name: None, - gc_admin1_id: None, - gc_admin1_name: None, - gc_country_id: None, - gc_country_name: None, - }; - let gcs_row = unwrap_sql(gcs_location::create(exec, &gcs_fields), "gcs").result; - - let _ = unwrap_sql( - farm_gcs_location::create( - exec, - &IFarmGcsLocationFields { - farm_id: farm_row.id.clone(), - gcs_location_id: gcs_row.id.clone(), - role: "primary".to_string(), - }, - ), - "farm_gcs", - ); - - let plot_row = unwrap_sql( - plot::create( - exec, - &IPlotFields { - d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), - farm_id: farm_row.id.clone(), - name: "Plot A".to_string(), - about: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }, - ), - "plot", - ) - .result; - - let _ = unwrap_sql( - plot_gcs_location::create( - exec, - &IPlotGcsLocationFields { - plot_id: plot_row.id.clone(), - gcs_location_id: gcs_row.id.clone(), - role: "primary".to_string(), - }, - ), - "plot_gcs", - ); - - let _ = unwrap_sql( - farm_tag::create( - exec, - &IFarmTagFields { - farm_id: farm_row.id.clone(), - tag: "coffee".to_string(), - }, - ), - "farm_tag", - ); - - let _ = unwrap_sql( - plot_tag::create( - exec, - &IPlotTagFields { - plot_id: plot_row.id.clone(), - tag: "orchard".to_string(), - }, - ), - "plot_tag", - ); - - let owner_pubkey = "o".repeat(64); - let _ = unwrap_sql( - farm_member::create( - exec, - &IFarmMemberFields { - farm_id: farm_row.id.clone(), - member_pubkey: owner_pubkey.clone(), - role: "owner".to_string(), - }, - ), - "farm_member", - ); - let _ = unwrap_sql( - farm_member_claim::create( - exec, - &IFarmMemberClaimFields { - member_pubkey: owner_pubkey.clone(), - farm_pubkey: farm_pubkey.clone(), - }, - ), - "farm_member_claim", - ); - - let _ = unwrap_sql( - nostr_profile::create( - exec, - &INostrProfileFields { - public_key: farm_pubkey.clone(), - profile_type: "farm".to_string(), - name: "Farm Profile".to_string(), - display_name: None, - about: None, - website: None, - picture: None, - banner: None, - nip05: None, - lud06: None, - lud16: None, - }, - ), - "farm_profile", - ); - let _ = unwrap_sql( - nostr_profile::create( - exec, - &INostrProfileFields { - public_key: owner_pubkey.clone(), - profile_type: "individual".to_string(), - name: "Owner".to_string(), - display_name: None, - about: None, - website: None, - picture: None, - banner: None, - nip05: None, - lud06: None, - lud16: None, - }, - ), - "owner_profile", - ); - - let request = RadrootsTangleSyncRequest { - farm: RadrootsTangleFarmSelector { - id: Some(farm_row.id), - d_tag: None, - pubkey: None, - }, - options: None, - }; - let bundle = radroots_tangle_sync_all(exec, &request).expect("sync"); - (request, farm_d_tag, farm_pubkey, bundle.events) -} - -#[test] -fn ingest_roundtrip_yields_zero_pending_sync() { - let source = SqliteExecutor::open_memory().expect("source db"); - let (_source_request, farm_d_tag, farm_pubkey, drafts) = seed_source(&source); - assert_eq!(drafts.len(), 9); - - let target = SqliteExecutor::open_memory().expect("target db"); - migrations::run_all_up(&target).expect("target migrations"); - - let mut skipped = 0usize; - for (index, draft) in drafts.iter().enumerate() { - let event = draft_to_event(draft, index as u32); - let first = radroots_tangle_ingest_event(&target, &event).expect("first ingest"); - assert_eq!(first, RadrootsTangleIngestOutcome::Applied); - let second = radroots_tangle_ingest_event(&target, &event).expect("second ingest"); - if second == RadrootsTangleIngestOutcome::Skipped { - skipped += 1; - } - } - assert!(skipped > 0); - - let status = radroots_tangle_sync_status(&target).expect("sync status"); - assert_eq!(status.expected_count, drafts.len()); - assert_eq!(status.pending_count, 0); - - let replay = radroots_tangle_sync_all( - &target, - &RadrootsTangleSyncRequest { - farm: RadrootsTangleFarmSelector { - id: None, - d_tag: Some(farm_d_tag), - pubkey: Some(farm_pubkey), - }, - options: None, - }, - ) - .expect("replay sync"); - assert_eq!(replay.version, RADROOTS_TANGLE_TRANSFER_VERSION); - assert_eq!(replay.events.len(), drafts.len()); -} - -#[test] -fn sync_status_empty_db_is_zero() { - let exec = SqliteExecutor::open_memory().expect("db"); - migrations::run_all_up(&exec).expect("migrations"); - let status = radroots_tangle_sync_status(&exec).expect("status"); - assert_eq!(status.expected_count, 0); - assert_eq!(status.pending_count, 0); -} - -#[test] -fn sync_all_selector_and_options_paths_are_supported() { - let source = SqliteExecutor::open_memory().expect("source db"); - let (request, farm_d_tag, farm_pubkey, full_events) = seed_source(&source); - - let by_pair = radroots_tangle_sync_all( - &source, - &RadrootsTangleSyncRequest { - farm: RadrootsTangleFarmSelector { - id: None, - d_tag: Some(farm_d_tag.clone()), - pubkey: Some(farm_pubkey.clone()), - }, - options: None, - }, - ) - .expect("selector by d_tag + pubkey"); - assert_eq!(by_pair.events.len(), full_events.len()); - - let reduced = radroots_tangle_sync_all( - &source, - &RadrootsTangleSyncRequest { - farm: request.farm, - options: Some(RadrootsTangleSyncOptions { - include_profiles: Some(false), - include_list_sets: Some(false), - include_membership_claims: Some(false), - }), - }, - ) - .expect("reduced sync"); - assert_eq!(reduced.events.len(), 2); -} - -#[test] -fn ingest_rejects_unsupported_kind() { - let exec = SqliteExecutor::open_memory().expect("db"); - migrations::run_all_up(&exec).expect("migrations"); - let event = RadrootsNostrEvent { - id: format!("{:064x}", 1u64), - author: "a".repeat(64), - created_at: 1_720_000_001, - kind: 42, - tags: Vec::new(), - content: String::new(), - sig: "f".repeat(128), - }; - let err = radroots_tangle_ingest_event(&exec, &event).expect_err("unsupported kind"); - assert!(err.to_string().contains("unsupported kind")); -} - -fn event_with_parts( - id: u64, - author: &str, - created_at: u32, - kind: u32, - content: String, - tags: Vec<Vec<String>>, -) -> RadrootsNostrEvent { - RadrootsNostrEvent { - id: format!("{id:064x}"), - author: author.to_string(), - created_at, - kind, - tags, - content, - sig: "f".repeat(128), - } -} - -fn sample_point(lat: f64, lng: f64) -> RadrootsGeoJsonPoint { - RadrootsGeoJsonPoint { - r#type: "Point".to_string(), - coordinates: [lng, lat], - } -} - -fn sample_polygon(lat: f64, lng: f64) -> RadrootsGeoJsonPolygon { - RadrootsGeoJsonPolygon { - r#type: "Polygon".to_string(), - coordinates: vec![vec![ - [lng, lat], - [lng, lat + 0.001], - [lng - 0.001, lat + 0.001], - [lng, lat], - ]], - } -} - -fn sample_gcs(lat: f64, lng: f64, geohash: &str) -> RadrootsGcsLocation { - RadrootsGcsLocation { - lat, - lng, - geohash: geohash.to_string(), - point: sample_point(lat, lng), - polygon: sample_polygon(lat, lng), - accuracy: Some(2.0), - altitude: Some(10.0), - tag_0: Some("soil".to_string()), - label: Some("north".to_string()), - area: Some(1_000.0), - elevation: Some(5), - soil: Some("loam".to_string()), - climate: Some("temperate".to_string()), - gc_id: Some("gc".to_string()), - gc_name: Some("name".to_string()), - gc_admin1_id: Some("admin1".to_string()), - gc_admin1_name: Some("admin1_name".to_string()), - gc_country_id: Some("country".to_string()), - gc_country_name: Some("country_name".to_string()), - } -} - -fn profile_event( - id: u64, - author: &str, - created_at: u32, - profile_type: Option<RadrootsProfileType>, - name: &str, -) -> RadrootsNostrEvent { - let profile = RadrootsProfile { - name: name.to_string(), - display_name: Some(format!("{name}_display")), - nip05: Some(format!("{name}@example.com")), - about: Some(format!("{name} about")), - website: Some("https://example.com".to_string()), - picture: Some("https://example.com/p.png".to_string()), - banner: Some("https://example.com/b.png".to_string()), - lud06: Some("lud06".to_string()), - lud16: Some("lud16".to_string()), - bot: None, - }; - let mut tags = Vec::new(); - if let Some(kind) = profile_type { - tags.push(vec![ - RADROOTS_PROFILE_TYPE_TAG_KEY.to_string(), - radroots_profile_type_tag_value(kind).to_string(), - ]); - } - event_with_parts( - id, - author, - created_at, - KIND_PROFILE, - serde_json::to_string(&profile).expect("profile json"), - tags, - ) -} - -fn farm_event( - id: u64, - author: &str, - created_at: u32, - d_tag: &str, - name: &str, - location: Option<RadrootsFarmLocation>, - tags: Option<Vec<String>>, -) -> RadrootsNostrEvent { - let farm = RadrootsFarm { - d_tag: d_tag.to_string(), - name: name.to_string(), - about: Some(format!("{name} about")), - website: Some("https://farm.example.com".to_string()), - picture: Some("https://farm.example.com/p.png".to_string()), - banner: Some("https://farm.example.com/b.png".to_string()), - location, - tags, - }; - let event_tags = farm_encode::farm_build_tags(&farm).expect("farm tags"); - event_with_parts( - id, - author, - created_at, - KIND_FARM, - serde_json::to_string(&farm).expect("farm json"), - event_tags, - ) -} - -fn plot_event( - id: u64, - author: &str, - created_at: u32, - d_tag: &str, - farm_ref: RadrootsFarmRef, - name: &str, - location: Option<RadrootsPlotLocation>, - tags: Option<Vec<String>>, -) -> RadrootsNostrEvent { - let plot = RadrootsPlot { - d_tag: d_tag.to_string(), - farm: farm_ref, - name: name.to_string(), - about: Some(format!("{name} about")), - location, - tags, - }; - let event_tags = plot_encode::plot_build_tags(&plot).expect("plot tags"); - event_with_parts( - id, - author, - created_at, - KIND_PLOT, - serde_json::to_string(&plot).expect("plot json"), - event_tags, - ) -} - -fn list_set_event( - id: u64, - author: &str, - created_at: u32, - kind: u32, - list_set: &RadrootsListSet, -) -> RadrootsNostrEvent { - let parts = list_set_encode::to_wire_parts_with_kind(list_set, kind).expect("list set parts"); - event_with_parts(id, author, created_at, kind, parts.content, parts.tags) -} - -#[test] -fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() { - let exec = SqliteExecutor::open_memory().expect("db"); - migrations::run_all_up(&exec).expect("migrations"); - - let profile_pubkey = "p".repeat(64); - let profile_create = profile_event( - 101, - &profile_pubkey, - 10, - Some(RadrootsProfileType::Individual), - "alice", - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &profile_create).expect("profile create"), - RadrootsTangleIngestOutcome::Applied - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &profile_create).expect("profile skip same"), - RadrootsTangleIngestOutcome::Skipped - ); - let profile_older = profile_event( - 102, - &profile_pubkey, - 9, - Some(RadrootsProfileType::Individual), - "alice-older", - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &profile_older).expect("profile skip older"), - RadrootsTangleIngestOutcome::Skipped - ); - let profile_same_time_new_hash = profile_event( - 103, - &profile_pubkey, - 10, - Some(RadrootsProfileType::Individual), - "alice-updated", - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &profile_same_time_new_hash) - .expect("profile apply same timestamp different hash"), - RadrootsTangleIngestOutcome::Applied - ); - let profile_missing_type = profile_event(104, &profile_pubkey, 11, None, "missing-type"); - let err = radroots_tangle_ingest_event(&exec, &profile_missing_type) - .expect_err("profile type is required"); - assert!(err.to_string().contains("profile_type required")); - - let profile_types = [ - (RadrootsProfileType::Farm, "f".repeat(64), "farm-profile"), - (RadrootsProfileType::Coop, "c".repeat(64), "coop-profile"), - (RadrootsProfileType::Any, "a".repeat(64), "any-profile"), - ( - RadrootsProfileType::Radrootsd, - "d".repeat(64), - "radrootsd-profile", - ), - ]; - for (index, (profile_type, pubkey, name)) in profile_types.iter().enumerate() { - let event = profile_event( - 110 + index as u64, - pubkey, - 20 + index as u32, - Some(*profile_type), - name, - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &event).expect("profile variant"), - RadrootsTangleIngestOutcome::Applied - ); - } - - let farm_pubkey = "e".repeat(64); - let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; - let farm_location = RadrootsFarmLocation { - primary: Some("farm-primary".to_string()), - city: Some("city".to_string()), - region: Some("region".to_string()), - country: Some("country".to_string()), - gcs: sample_gcs(37.7, -122.4, "9q8yy"), - }; - let farm_create = farm_event( - 200, - &farm_pubkey, - 100, - farm_d_tag, - "farm-a", - Some(farm_location.clone()), - Some(vec![ - "coffee".to_string(), - " ".to_string(), - "coffee".to_string(), - "grain".to_string(), - ]), - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &farm_create).expect("farm create"), - RadrootsTangleIngestOutcome::Applied - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &farm_create).expect("farm skip same"), - RadrootsTangleIngestOutcome::Skipped - ); - let farm_older = farm_event( - 201, - &farm_pubkey, - 99, - farm_d_tag, - "farm-older", - Some(farm_location.clone()), - None, - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &farm_older).expect("farm skip older"), - RadrootsTangleIngestOutcome::Skipped - ); - let farm_update_same_time = farm_event( - 202, - &farm_pubkey, - 100, - farm_d_tag, - "farm-a-updated", - None, - Some(vec!["market".to_string()]), - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &farm_update_same_time).expect("farm update"), - RadrootsTangleIngestOutcome::Applied - ); - - let farm_rows = unwrap_sql( - farm::find_many( - &exec, - &IFarmFindMany { - filter: Some(IFarmFieldsFilter { - id: None, - created_at: None, - updated_at: None, - d_tag: Some(farm_d_tag.to_string()), - pubkey: Some(farm_pubkey.clone()), - name: None, - about: None, - website: None, - picture: None, - banner: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }), - }, - ), - "farm find_many", - ) - .results; - assert_eq!(farm_rows.len(), 1); - let farm_id = farm_rows[0].id.clone(); - - let farm_tags = unwrap_sql( - farm_tag::find_many( - &exec, - &IFarmTagFindMany { - filter: Some(IFarmTagFieldsFilter { - id: None, - created_at: None, - updated_at: None, - farm_id: Some(farm_id.clone()), - tag: None, - }), - }, - ), - "farm tags", - ) - .results; - assert_eq!(farm_tags.len(), 1); - assert_eq!(farm_tags[0].tag, "market"); - - let plot_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ"; - let plot_location = RadrootsPlotLocation { - primary: Some("plot-primary".to_string()), - city: Some("plot-city".to_string()), - region: Some("plot-region".to_string()), - country: Some("plot-country".to_string()), - gcs: sample_gcs(37.8, -122.5, "9q8yz"), - }; - let plot_create = plot_event( - 300, - &farm_pubkey, - 200, - plot_d_tag, - RadrootsFarmRef { - pubkey: farm_pubkey.clone(), - d_tag: farm_d_tag.to_string(), - }, - "plot-a", - Some(plot_location.clone()), - Some(vec![ - "orchard".to_string(), - " ".to_string(), - "orchard".to_string(), - "shade".to_string(), - ]), - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &plot_create).expect("plot create"), - RadrootsTangleIngestOutcome::Applied - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &plot_create).expect("plot skip same"), - RadrootsTangleIngestOutcome::Skipped - ); - let plot_older = plot_event( - 301, - &farm_pubkey, - 199, - plot_d_tag, - RadrootsFarmRef { - pubkey: farm_pubkey.clone(), - d_tag: farm_d_tag.to_string(), - }, - "plot-older", - Some(plot_location.clone()), - None, - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &plot_older).expect("plot skip older"), - RadrootsTangleIngestOutcome::Skipped - ); - let plot_update = plot_event( - 302, - &farm_pubkey, - 200, - plot_d_tag, - RadrootsFarmRef { - pubkey: farm_pubkey.clone(), - d_tag: farm_d_tag.to_string(), - }, - "plot-a-updated", - None, - Some(vec!["updated".to_string()]), - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &plot_update).expect("plot update"), - RadrootsTangleIngestOutcome::Applied - ); - let plot_missing_farm = plot_event( - 303, - &farm_pubkey, - 201, - "AAAAAAAAAAAAAAAAAAAAAg", - RadrootsFarmRef { - pubkey: "z".repeat(64), - d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), - }, - "plot-missing-farm", - None, - None, - ); - let missing_farm_err = radroots_tangle_ingest_event(&exec, &plot_missing_farm) - .expect_err("plot requires existing farm"); - assert!(missing_farm_err.to_string().contains("farm not found")); - - let plot_rows = unwrap_sql( - plot::find_many( - &exec, - &radroots_tangle_db_schema::plot::IPlotFindMany { filter: None }, - ), - "plot rows", - ) - .results; - assert_eq!(plot_rows.len(), 1); - let plot_id = plot_rows[0].id.clone(); - let plot_tags = unwrap_sql( - plot_tag::find_many( - &exec, - &IPlotTagFindMany { - filter: Some(IPlotTagFieldsFilter { - id: None, - created_at: None, - updated_at: None, - plot_id: Some(plot_id), - tag: None, - }), - }, - ), - "plot tags", - ) - .results; - assert_eq!(plot_tags.len(), 1); - assert_eq!(plot_tags[0].tag, "updated"); - - let non_generic_list_set = RadrootsListSet { - d_tag: "member_of.farms".to_string(), - content: String::new(), - entries: vec![RadrootsListEntry { - tag: "p".to_string(), - values: vec![farm_pubkey.clone()], - }], - title: None, - description: None, - image: None, - }; - let non_generic_event = list_set_event( - 400, - &profile_pubkey, - 300, - KIND_LIST_SET_FOLLOW, - &non_generic_list_set, - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &non_generic_event).expect("non-generic list set"), - RadrootsTangleIngestOutcome::Skipped - ); - - let metadata_list_set = RadrootsListSet { - d_tag: "member_of.farms".to_string(), - content: String::new(), - entries: vec![RadrootsListEntry { - tag: "p".to_string(), - values: vec![farm_pubkey.clone()], - }], - title: Some("title".to_string()), - description: None, - image: None, - }; - let metadata_event = list_set_event( - 401, - &profile_pubkey, - 301, - KIND_LIST_SET_GENERIC, - &metadata_list_set, - ); - let metadata_err = radroots_tangle_ingest_event(&exec, &metadata_event) - .expect_err("metadata must be rejected"); - assert!(metadata_err.to_string().contains("must omit metadata")); - - let content_list_set = RadrootsListSet { - d_tag: "member_of.farms".to_string(), - content: "not-empty".to_string(), - entries: vec![RadrootsListEntry { - tag: "p".to_string(), - values: vec![farm_pubkey.clone()], - }], - title: None, - description: None, - image: None, - }; - let content_event = list_set_event( - 402, - &profile_pubkey, - 302, - KIND_LIST_SET_GENERIC, - &content_list_set, - ); - let content_err = - radroots_tangle_ingest_event(&exec, &content_event).expect_err("content must be rejected"); - assert!(content_err.to_string().contains("must not include content")); - - let invalid_member_of = RadrootsListSet { - d_tag: "member_of.farms".to_string(), - content: String::new(), - entries: vec![RadrootsListEntry { - tag: "a".to_string(), - values: vec![farm_pubkey.clone()], - }], - title: None, - description: None, - image: None, - }; - let invalid_member_of_event = list_set_event( - 403, - &profile_pubkey, - 303, - KIND_LIST_SET_GENERIC, - &invalid_member_of, - ); - let invalid_member_of_err = radroots_tangle_ingest_event(&exec, &invalid_member_of_event) - .expect_err("member_of requires p tags"); - assert!( - invalid_member_of_err - .to_string() - .contains("must only include p tags") - ); - - let member_of_valid = RadrootsListSet { - d_tag: "member_of.farms".to_string(), - content: String::new(), - entries: vec![ - RadrootsListEntry { - tag: "p".to_string(), - values: vec![farm_pubkey.clone()], - }, - RadrootsListEntry { - tag: "p".to_string(), - values: vec![farm_pubkey.clone()], - }, - ], - title: None, - description: None, - image: None, - }; - let member_of_event = list_set_event( - 404, - &profile_pubkey, - 304, - KIND_LIST_SET_GENERIC, - &member_of_valid, - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &member_of_event).expect("member_of apply"), - RadrootsTangleIngestOutcome::Applied - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &member_of_event).expect("member_of skip"), - RadrootsTangleIngestOutcome::Skipped - ); - - let claims = unwrap_sql( - farm_member_claim::find_many( - &exec, - &IFarmMemberClaimFindMany { - filter: Some(IFarmMemberClaimFieldsFilter { - id: None, - created_at: None, - updated_at: None, - member_pubkey: Some(profile_pubkey.clone()), - farm_pubkey: None, - }), - }, - ), - "claims", - ) - .results; - assert_eq!(claims.len(), 1); - assert_eq!(claims[0].farm_pubkey, farm_pubkey); - - let invalid_members = RadrootsListSet { - d_tag: format!("farm:{farm_d_tag}:members"), - content: String::new(), - entries: vec![RadrootsListEntry { - tag: "a".to_string(), - values: vec!["x".to_string()], - }], - title: None, - description: None, - image: None, - }; - let invalid_members_event = list_set_event( - 405, - &farm_pubkey, - 305, - KIND_LIST_SET_GENERIC, - &invalid_members, - ); - let invalid_members_err = radroots_tangle_ingest_event(&exec, &invalid_members_event) - .expect_err("members list requires p entries"); - assert!( - invalid_members_err - .to_string() - .contains("must only include p tags") - ); - - let members_valid = - farm_list_sets::farm_members_list_set(farm_d_tag, vec!["m".repeat(64), "m".repeat(64)]) - .expect("members list"); - let members_event = list_set_event( - 406, - &farm_pubkey, - 306, - KIND_LIST_SET_GENERIC, - &members_valid, - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &members_event).expect("members apply"), - RadrootsTangleIngestOutcome::Applied - ); - let owners_valid = - farm_list_sets::farm_owners_list_set(farm_d_tag, vec!["o".repeat(64)]).expect("owners"); - let owners_event = list_set_event(407, &farm_pubkey, 307, KIND_LIST_SET_GENERIC, &owners_valid); - assert_eq!( - radroots_tangle_ingest_event(&exec, &owners_event).expect("owners apply"), - RadrootsTangleIngestOutcome::Applied - ); - let workers_valid = - farm_list_sets::farm_workers_list_set(farm_d_tag, vec!["w".repeat(64)]).expect("workers"); - let workers_event = list_set_event( - 408, - &farm_pubkey, - 308, - KIND_LIST_SET_GENERIC, - &workers_valid, - ); - assert_eq!( - radroots_tangle_ingest_event(&exec, &workers_event).expect("workers apply"), - RadrootsTangleIngestOutcome::Applied - ); - - let members = unwrap_sql( - farm_member::find_many( - &exec, - &IFarmMemberFindMany { - filter: Some(IFarmMemberFieldsFilter { - id: None, - created_at: None, - updated_at: None, - farm_id: Some(farm_id), - member_pubkey: None, - role: None, - }), - }, - ), - "members", - ) - .results; - assert_eq!(members.len(), 3); - - let invalid_plots = RadrootsListSet { - d_tag: format!("farm:{farm_d_tag}:plots"), - content: String::new(), - entries: vec![RadrootsListEntry { - tag: "p".to_string(), - values: vec!["x".to_string()], - }], - title: None, - description: None, - image: None, - }; - let invalid_plots_event = list_set_event( - 409, - &farm_pubkey, - 309, - KIND_LIST_SET_GENERIC, - &invalid_plots, - ); - let invalid_plots_err = radroots_tangle_ingest_event(&exec, &invalid_plots_event) - .expect_err("plots list requires a entries"); - assert!( - invalid_plots_err - .to_string() - .contains("must only include a tags") - ); - - let plot_address = plot_encode::plot_address(&farm_pubkey, plot_d_tag).expect("plot address"); - let plots_valid = RadrootsListSet { - d_tag: format!("farm:{farm_d_tag}:plots"), - content: String::new(), - entries: vec![RadrootsListEntry { - tag: "a".to_string(), - values: vec![plot_address], - }], - title: None, - description: None, - image: None, - }; - let plots_event = list_set_event(410, &farm_pubkey, 310, KIND_LIST_SET_GENERIC, &plots_valid); - assert_eq!( - radroots_tangle_ingest_event(&exec, &plots_event).expect("plots apply"), - RadrootsTangleIngestOutcome::Applied - ); - - let unsupported_list_set = RadrootsListSet { - d_tag: "unsupported.list".to_string(), - content: String::new(), - entries: vec![RadrootsListEntry { - tag: "p".to_string(), - values: vec![farm_pubkey], - }], - title: None, - description: None, - image: None, - }; - let unsupported_event = list_set_event( - 411, - &profile_pubkey, - 311, - KIND_LIST_SET_GENERIC, - &unsupported_list_set, - ); - let unsupported_err = radroots_tangle_ingest_event(&exec, &unsupported_event) - .expect_err("unsupported list set d_tag"); - assert!( - unsupported_err - .to_string() - .contains("unsupported list set d_tag") - ); -} - -#[test] -fn sync_status_reports_pending_when_not_all_events_are_ingested() { - let source = SqliteExecutor::open_memory().expect("source"); - let (_request, _farm_d_tag, _farm_pubkey, drafts) = seed_source(&source); - let target = SqliteExecutor::open_memory().expect("target"); - migrations::run_all_up(&target).expect("migrations"); - - for (index, draft) in drafts.iter().enumerate() { - let event = draft_to_event(draft, index as u32); - let _ = radroots_tangle_ingest_event(&target, &event).expect("ingest"); - } - target - .exec( - "UPDATE nostr_event_state SET content_hash = ? WHERE id = (SELECT id FROM nostr_event_state LIMIT 1)", - "[\"invalid_hash\"]", - ) - .expect("mutate state hash"); - - let status = radroots_tangle_sync_status(&target).expect("status pending"); - assert_eq!(status.expected_count, drafts.len()); - assert!(status.pending_count > 0); -} - -#[test] -fn sync_all_rejects_invalid_selectors_and_non_unique_pair() { - let exec = SqliteExecutor::open_memory().expect("db"); - migrations::run_all_up(&exec).expect("migrations"); - - let missing_selector_err = radroots_tangle_sync_all( - &exec, - &RadrootsTangleSyncRequest { - farm: RadrootsTangleFarmSelector { - id: None, - d_tag: None, - pubkey: None, - }, - options: None, - }, - ) - .expect_err("selector validation"); - assert!( - missing_selector_err - .to_string() - .contains("requires id or (d_tag + pubkey)") - ); - - let missing_id_err = radroots_tangle_sync_all( - &exec, - &RadrootsTangleSyncRequest { - farm: RadrootsTangleFarmSelector { - id: Some("00000000-0000-0000-0000-000000000000".to_string()), - d_tag: None, - pubkey: None, - }, - options: None, - }, - ) - .expect_err("missing farm id"); - assert!(missing_id_err.to_string().contains("farm not found")); - - let duplicate_d_tag = "AAAAAAAAAAAAAAAAAAAAAA".to_string(); - let duplicate_pubkey = "u".repeat(64); - let fields = IFarmFields { - d_tag: duplicate_d_tag.clone(), - pubkey: duplicate_pubkey.clone(), - name: "one".to_string(), - about: None, - website: None, - picture: None, - banner: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }; - let _ = unwrap_sql(farm::create(&exec, &fields), "farm one"); - let _ = unwrap_sql(farm::create(&exec, &fields), "farm two"); - - let non_unique_err = radroots_tangle_sync_all( - &exec, - &RadrootsTangleSyncRequest { - farm: RadrootsTangleFarmSelector { - id: None, - d_tag: Some(duplicate_d_tag), - pubkey: Some(duplicate_pubkey), - }, - options: None, - }, - ) - .expect_err("non unique selector"); - assert!( - non_unique_err - .to_string() - .contains("did not resolve to a single farm") - ); -} - -#[test] -fn sync_emit_handles_invalid_geojson_and_unknown_profile_type() { - let exec = SqliteExecutor::open_memory().expect("db"); - migrations::run_all_up(&exec).expect("migrations"); - - let farm_pubkey = "g".repeat(64); - let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA".to_string(); - let farm_row = unwrap_sql( - farm::create( - &exec, - &IFarmFields { - d_tag: farm_d_tag.clone(), - pubkey: farm_pubkey.clone(), - name: "farm".to_string(), - about: Some("about".to_string()), - website: None, - picture: None, - banner: None, - location_primary: Some("primary".to_string()), - location_city: Some("city".to_string()), - location_region: Some("region".to_string()), - location_country: Some("country".to_string()), - }, - ), - "farm", - ) - .result; - - let bad_gcs = unwrap_sql( - gcs_location::create( - &exec, - &IGcsLocationFields { - d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), - lat: 10.0, - lng: 20.0, - geohash: "s0".to_string(), - point: "{".to_string(), - polygon: "{\"type\":\"Polygon\",\"coordinates\":[[]]}".to_string(), - accuracy: None, - altitude: None, - tag_0: None, - label: None, - area: None, - elevation: None, - soil: None, - climate: None, - gc_id: None, - gc_name: None, - gc_admin1_id: None, - gc_admin1_name: None, - gc_country_id: None, - gc_country_name: None, - }, - ), - "bad gcs", - ) - .result; - let _ = unwrap_sql( - farm_gcs_location::create( - &exec, - &IFarmGcsLocationFields { - farm_id: farm_row.id.clone(), - gcs_location_id: bad_gcs.id.clone(), - role: "".to_string(), - }, - ), - "farm gcs", - ); - - let plot_row = unwrap_sql( - plot::create( - &exec, - &IPlotFields { - d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), - farm_id: farm_row.id.clone(), - name: "plot".to_string(), - about: Some("plot about".to_string()), - location_primary: Some("plot primary".to_string()), - location_city: None, - location_region: None, - location_country: None, - }, - ), - "plot", - ) - .result; - let _ = unwrap_sql( - plot_gcs_location::create( - &exec, - &IPlotGcsLocationFields { - plot_id: plot_row.id.clone(), - gcs_location_id: bad_gcs.id, - role: "primary".to_string(), - }, - ), - "plot gcs", - ); - - let member_pubkey = "m".repeat(64); - let _ = unwrap_sql( - farm_member::create( - &exec, - &IFarmMemberFields { - farm_id: farm_row.id.clone(), - member_pubkey: member_pubkey.clone(), - role: "owner".to_string(), - }, - ), - "member", - ); - let _ = unwrap_sql( - farm_member_claim::create( - &exec, - &IFarmMemberClaimFields { - member_pubkey: member_pubkey.clone(), - farm_pubkey: farm_pubkey.clone(), - }, - ), - "claim", - ); - let _ = unwrap_sql( - nostr_profile::create( - &exec, - &INostrProfileFields { - public_key: farm_pubkey.clone(), - profile_type: "farm".to_string(), - name: "farm profile".to_string(), - display_name: None, - about: None, - website: None, - picture: None, - banner: None, - nip05: None, - lud06: None, - lud16: None, - }, - ), - "farm profile", - ); - let _ = unwrap_sql( - nostr_profile::create( - &exec, - &INostrProfileFields { - public_key: member_pubkey.clone(), - profile_type: "legacy".to_string(), - name: "legacy profile".to_string(), - display_name: Some("legacy".to_string()), - about: Some("about".to_string()), - website: Some("https://example.com".to_string()), - picture: Some("https://example.com/p.png".to_string()), - banner: Some("https://example.com/b.png".to_string()), - nip05: Some("legacy@example.com".to_string()), - lud06: Some("lud06".to_string()), - lud16: Some("lud16".to_string()), - }, - ), - "legacy profile", - ); - - let bundle = radroots_tangle_sync_all( - &exec, - &RadrootsTangleSyncRequest { - farm: RadrootsTangleFarmSelector { - id: Some(farm_row.id), - d_tag: None, - pubkey: None, - }, - options: None, - }, - ) - .expect("sync"); - assert_eq!(bundle.version, RADROOTS_TANGLE_TRANSFER_VERSION); - assert!(bundle.events.iter().any(|event| event.kind == KIND_FARM)); - assert!(bundle.events.iter().any(|event| event.kind == KIND_PLOT)); - assert!( - bundle - .events - .iter() - .any(|event| event.kind == KIND_LIST_SET_GENERIC) - ); - assert!(bundle.events.iter().any(|event| { - event.kind == KIND_PROFILE - && event.author == member_pubkey - && event - .tags - .iter() - .all(|tag| tag[0] != RADROOTS_PROFILE_TYPE_TAG_KEY) - })); -} - -#[test] -fn error_conversion_paths_are_exercised() { - let sql: RadrootsTangleEventsError = IError::from(SqlError::Internal).into(); - assert!(matches!(sql, RadrootsTangleEventsError::Sql(_))); - - let encode: RadrootsTangleEventsError = EventEncodeError::Json.into(); - assert!(matches!(encode, RadrootsTangleEventsError::Encode(_))); - - let parse_number_err = "x".parse::<u32>().expect_err("parse should fail"); - let parse: RadrootsTangleEventsError = - EventParseError::InvalidNumber("k", parse_number_err).into(); - assert!(matches!(parse, RadrootsTangleEventsError::Parse(_))); -}