lib

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

commit 23ffac2afb2c7c85c79940468bbb32ed95443c5e
parent 4caff310a8c4814bf0551b09ec9733410e464157
Author: triesap <tyson@radroots.org>
Date:   Sat, 11 Jul 2026 23:40:12 +0000

crate-surface: rename replica and sp1 crates

- rename replica schema and store crate surfaces
- rename NostrDB crate, module, and bridge feature
- rename trade SP1 guest and host package keys
- update backup manifests, contracts, and direct consumers

Diffstat:
MCargo.lock | 166++++++++++++++++++++++++++++++++++++++++----------------------------------------
MCargo.toml | 20++++++++++----------
Mbuild/nix/common.nix | 2+-
Mcontracts/coverage.toml | 12++++++------
Mcontracts/manifest.toml | 14+++++++-------
Mcontracts/replica.toml | 4++--
Mcrates/authority/src/local_signer.rs | 4++--
Mcrates/nostr_accounts/Cargo.toml | 6+++---
Mcrates/nostr_accounts/src/lib.rs | 8++++----
Dcrates/nostr_accounts/src/ndb_bridge.rs | 67-------------------------------------------------------------------
Acrates/nostr_accounts/src/nostrdb_bridge.rs | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcrates/nostr_ndb/Cargo.toml | 40----------------------------------------
Dcrates/nostr_ndb/README | 23-----------------------
Dcrates/nostr_ndb/src/config.rs | 51---------------------------------------------------
Dcrates/nostr_ndb/src/error.rs | 58----------------------------------------------------------
Dcrates/nostr_ndb/src/filter.rs | 272-------------------------------------------------------------------------------
Dcrates/nostr_ndb/src/ingest.rs | 38--------------------------------------
Dcrates/nostr_ndb/src/lib.rs | 51---------------------------------------------------
Dcrates/nostr_ndb/src/ndb.rs | 1055-------------------------------------------------------------------------------
Dcrates/nostr_ndb/src/query.rs | 60------------------------------------------------------------
Dcrates/nostr_ndb/src/runtime_adapter.rs | 118-------------------------------------------------------------------------------
Dcrates/nostr_ndb/src/subscription.rs | 106-------------------------------------------------------------------------------
Mcrates/nostr_runtime/Cargo.toml | 2+-
Acrates/nostrdb/Cargo.toml | 40++++++++++++++++++++++++++++++++++++++++
Acrates/nostrdb/README | 23+++++++++++++++++++++++
Acrates/nostrdb/src/config.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/nostrdb/src/error.rs | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/nostrdb/src/filter.rs | 272+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/nostrdb/src/ingest.rs | 38++++++++++++++++++++++++++++++++++++++
Acrates/nostrdb/src/lib.rs | 48++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/nostrdb/src/nostrdb.rs | 1080+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/nostrdb/src/query.rs | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/nostrdb/src/runtime_adapter.rs | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/nostrdb/src/subscription.rs | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rcrates/nostr_ndb/src/test_fixtures.rs -> crates/nostrdb/src/test_fixtures.rs | 0
Dcrates/replica_db/Cargo.toml | 33---------------------------------
Dcrates/replica_db/README | 22----------------------
Dcrates/replica_db/src/backup.rs | 993-------------------------------------------------------------------------------
Dcrates/replica_db/src/export.rs | 283-------------------------------------------------------------------------------
Dcrates/replica_db/src/lib.rs | 784-------------------------------------------------------------------------------
Dcrates/replica_db/src/models/farm.rs | 161-------------------------------------------------------------------------------
Dcrates/replica_db/src/models/farm_gcs_location.rs | 162-------------------------------------------------------------------------------
Dcrates/replica_db/src/models/farm_member.rs | 161-------------------------------------------------------------------------------
Dcrates/replica_db/src/models/farm_member_claim.rs | 162-------------------------------------------------------------------------------
Dcrates/replica_db/src/models/farm_tag.rs | 157-------------------------------------------------------------------------------
Dcrates/replica_db/src/models/gcs_location.rs | 240-------------------------------------------------------------------------------
Dcrates/replica_db/src/models/log_error.rs | 161-------------------------------------------------------------------------------
Dcrates/replica_db/src/models/media_image.rs | 220-------------------------------------------------------------------------------
Dcrates/replica_db/src/models/nostr_event_head.rs | 162-------------------------------------------------------------------------------
Dcrates/replica_db/src/models/nostr_profile.rs | 217-------------------------------------------------------------------------------
Dcrates/replica_db/src/models/nostr_profile_relay.rs | 45---------------------------------------------
Dcrates/replica_db/src/models/nostr_relay.rs | 220-------------------------------------------------------------------------------
Dcrates/replica_db/src/models/plot.rs | 157-------------------------------------------------------------------------------
Dcrates/replica_db/src/models/plot_gcs_location.rs | 162-------------------------------------------------------------------------------
Dcrates/replica_db/src/models/plot_tag.rs | 157-------------------------------------------------------------------------------
Dcrates/replica_db/src/models/trade_product.rs | 147-------------------------------------------------------------------------------
Dcrates/replica_db/src/models/trade_product_location.rs | 45---------------------------------------------
Dcrates/replica_db/src/models/trade_product_media.rs | 45---------------------------------------------
Dcrates/replica_db/tests/backup_export_paths.rs | 305------------------------------------------------------------------------------
Dcrates/replica_db/tests/error_paths.rs | 977-------------------------------------------------------------------------------
Dcrates/replica_db/tests/full_mode.rs | 1350-------------------------------------------------------------------------------
Dcrates/replica_db/tests/migration_repairs.rs | 104-------------------------------------------------------------------------------
Dcrates/replica_db/tests/region_scripted_paths.rs | 786-------------------------------------------------------------------------------
Dcrates/replica_db_schema/Cargo.toml | 25-------------------------
Dcrates/replica_db_schema/README | 22----------------------
Dcrates/replica_db_schema/src/dto.rs | 2266-------------------------------------------------------------------------------
Dcrates/replica_db_schema/tests/query_bind_values.rs | 354-------------------------------------------------------------------------------
Acrates/replica_schema/Cargo.toml | 25+++++++++++++++++++++++++
Acrates/replica_schema/README | 22++++++++++++++++++++++
Acrates/replica_schema/src/dto.rs | 2266+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rcrates/replica_db_schema/src/lib.rs -> crates/replica_schema/src/lib.rs | 0
Rcrates/replica_db_schema/src/models/farm.rs -> crates/replica_schema/src/models/farm.rs | 0
Rcrates/replica_db_schema/src/models/farm_gcs_location.rs -> crates/replica_schema/src/models/farm_gcs_location.rs | 0
Rcrates/replica_db_schema/src/models/farm_member.rs -> crates/replica_schema/src/models/farm_member.rs | 0
Rcrates/replica_db_schema/src/models/farm_member_claim.rs -> crates/replica_schema/src/models/farm_member_claim.rs | 0
Rcrates/replica_db_schema/src/models/farm_tag.rs -> crates/replica_schema/src/models/farm_tag.rs | 0
Rcrates/replica_db_schema/src/models/gcs_location.rs -> crates/replica_schema/src/models/gcs_location.rs | 0
Rcrates/replica_db_schema/src/models/log_error.rs -> crates/replica_schema/src/models/log_error.rs | 0
Rcrates/replica_db_schema/src/models/media_image.rs -> crates/replica_schema/src/models/media_image.rs | 0
Rcrates/replica_db_schema/src/models/mod.rs -> crates/replica_schema/src/models/mod.rs | 0
Rcrates/replica_db_schema/src/models/nostr_event_head.rs -> crates/replica_schema/src/models/nostr_event_head.rs | 0
Rcrates/replica_db_schema/src/models/nostr_profile.rs -> crates/replica_schema/src/models/nostr_profile.rs | 0
Rcrates/replica_db_schema/src/models/nostr_profile_relay.rs -> crates/replica_schema/src/models/nostr_profile_relay.rs | 0
Rcrates/replica_db_schema/src/models/nostr_relay.rs -> crates/replica_schema/src/models/nostr_relay.rs | 0
Rcrates/replica_db_schema/src/models/plot.rs -> crates/replica_schema/src/models/plot.rs | 0
Rcrates/replica_db_schema/src/models/plot_gcs_location.rs -> crates/replica_schema/src/models/plot_gcs_location.rs | 0
Rcrates/replica_db_schema/src/models/plot_tag.rs -> crates/replica_schema/src/models/plot_tag.rs | 0
Rcrates/replica_db_schema/src/models/result.rs -> crates/replica_schema/src/models/result.rs | 0
Rcrates/replica_db_schema/src/models/trade_product.rs -> crates/replica_schema/src/models/trade_product.rs | 0
Rcrates/replica_db_schema/src/models/trade_product_location.rs -> crates/replica_schema/src/models/trade_product_location.rs | 0
Rcrates/replica_db_schema/src/models/trade_product_media.rs -> crates/replica_schema/src/models/trade_product_media.rs | 0
Acrates/replica_schema/tests/query_bind_values.rs | 354+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/Cargo.toml | 33+++++++++++++++++++++++++++++++++
Acrates/replica_store/README | 22++++++++++++++++++++++
Rcrates/replica_db/migrations/0000_init.down.sql -> crates/replica_store/migrations/0000_init.down.sql | 0
Rcrates/replica_db/migrations/0000_init.up.sql -> crates/replica_store/migrations/0000_init.up.sql | 0
Rcrates/replica_db/migrations/0001_log_error.down.sql -> crates/replica_store/migrations/0001_log_error.down.sql | 0
Rcrates/replica_db/migrations/0001_log_error.up.sql -> crates/replica_store/migrations/0001_log_error.up.sql | 0
Rcrates/replica_db/migrations/0002_farm.down.sql -> crates/replica_store/migrations/0002_farm.down.sql | 0
Rcrates/replica_db/migrations/0002_farm.up.sql -> crates/replica_store/migrations/0002_farm.up.sql | 0
Rcrates/replica_db/migrations/0003_gcs_location.down.sql -> crates/replica_store/migrations/0003_gcs_location.down.sql | 0
Rcrates/replica_db/migrations/0003_gcs_location.up.sql -> crates/replica_store/migrations/0003_gcs_location.up.sql | 0
Rcrates/replica_db/migrations/0004_trade_product.down.sql -> crates/replica_store/migrations/0004_trade_product.down.sql | 0
Rcrates/replica_db/migrations/0004_trade_product.up.sql -> crates/replica_store/migrations/0004_trade_product.up.sql | 0
Rcrates/replica_db/migrations/0005_nostr_profile.down.sql -> crates/replica_store/migrations/0005_nostr_profile.down.sql | 0
Rcrates/replica_db/migrations/0005_nostr_profile.up.sql -> crates/replica_store/migrations/0005_nostr_profile.up.sql | 0
Rcrates/replica_db/migrations/0006_nostr_relay.down.sql -> crates/replica_store/migrations/0006_nostr_relay.down.sql | 0
Rcrates/replica_db/migrations/0006_nostr_relay.up.sql -> crates/replica_store/migrations/0006_nostr_relay.up.sql | 0
Rcrates/replica_db/migrations/0007_media_image.down.sql -> crates/replica_store/migrations/0007_media_image.down.sql | 0
Rcrates/replica_db/migrations/0007_media_image.up.sql -> crates/replica_store/migrations/0007_media_image.up.sql | 0
Rcrates/replica_db/migrations/0008_farm_gcs_location.down.sql -> crates/replica_store/migrations/0008_farm_gcs_location.down.sql | 0
Rcrates/replica_db/migrations/0008_farm_gcs_location.up.sql -> crates/replica_store/migrations/0008_farm_gcs_location.up.sql | 0
Rcrates/replica_db/migrations/0009_nostr_profile_relay.down.sql -> crates/replica_store/migrations/0009_nostr_profile_relay.down.sql | 0
Rcrates/replica_db/migrations/0009_nostr_profile_relay.up.sql -> crates/replica_store/migrations/0009_nostr_profile_relay.up.sql | 0
Rcrates/replica_db/migrations/0010_trade_product_location.down.sql -> crates/replica_store/migrations/0010_trade_product_location.down.sql | 0
Rcrates/replica_db/migrations/0010_trade_product_location.up.sql -> crates/replica_store/migrations/0010_trade_product_location.up.sql | 0
Rcrates/replica_db/migrations/0011_trade_product_media.down.sql -> crates/replica_store/migrations/0011_trade_product_media.down.sql | 0
Rcrates/replica_db/migrations/0011_trade_product_media.up.sql -> crates/replica_store/migrations/0011_trade_product_media.up.sql | 0
Rcrates/replica_db/migrations/0012_plot.down.sql -> crates/replica_store/migrations/0012_plot.down.sql | 0
Rcrates/replica_db/migrations/0012_plot.up.sql -> crates/replica_store/migrations/0012_plot.up.sql | 0
Rcrates/replica_db/migrations/0013_plot_gcs_location.down.sql -> crates/replica_store/migrations/0013_plot_gcs_location.down.sql | 0
Rcrates/replica_db/migrations/0013_plot_gcs_location.up.sql -> crates/replica_store/migrations/0013_plot_gcs_location.up.sql | 0
Rcrates/replica_db/migrations/0014_farm_tag.down.sql -> crates/replica_store/migrations/0014_farm_tag.down.sql | 0
Rcrates/replica_db/migrations/0014_farm_tag.up.sql -> crates/replica_store/migrations/0014_farm_tag.up.sql | 0
Rcrates/replica_db/migrations/0015_plot_tag.down.sql -> crates/replica_store/migrations/0015_plot_tag.down.sql | 0
Rcrates/replica_db/migrations/0015_plot_tag.up.sql -> crates/replica_store/migrations/0015_plot_tag.up.sql | 0
Rcrates/replica_db/migrations/0016_farm_member.down.sql -> crates/replica_store/migrations/0016_farm_member.down.sql | 0
Rcrates/replica_db/migrations/0016_farm_member.up.sql -> crates/replica_store/migrations/0016_farm_member.up.sql | 0
Rcrates/replica_db/migrations/0017_farm_member_claim.down.sql -> crates/replica_store/migrations/0017_farm_member_claim.down.sql | 0
Rcrates/replica_db/migrations/0017_farm_member_claim.up.sql -> crates/replica_store/migrations/0017_farm_member_claim.up.sql | 0
Rcrates/replica_db/migrations/0018_nostr_event_head.down.sql -> crates/replica_store/migrations/0018_nostr_event_head.down.sql | 0
Rcrates/replica_db/migrations/0018_nostr_event_head.up.sql -> crates/replica_store/migrations/0018_nostr_event_head.up.sql | 0
Rcrates/replica_db/migrations/0019_repair_missing_indexes.down.sql -> crates/replica_store/migrations/0019_repair_missing_indexes.down.sql | 0
Rcrates/replica_db/migrations/0019_repair_missing_indexes.up.sql -> crates/replica_store/migrations/0019_repair_missing_indexes.up.sql | 0
Rcrates/replica_db/migrations/0020_trade_product_listing_addr.down.sql -> crates/replica_store/migrations/0020_trade_product_listing_addr.down.sql | 0
Rcrates/replica_db/migrations/0020_trade_product_listing_addr.up.sql -> crates/replica_store/migrations/0020_trade_product_listing_addr.up.sql | 0
Rcrates/replica_db/migrations/0021_trade_product_primary_bin_id.down.sql -> crates/replica_store/migrations/0021_trade_product_primary_bin_id.down.sql | 0
Rcrates/replica_db/migrations/0021_trade_product_primary_bin_id.up.sql -> crates/replica_store/migrations/0021_trade_product_primary_bin_id.up.sql | 0
Rcrates/replica_db/migrations/0022_trade_product_exact_economics.down.sql -> crates/replica_store/migrations/0022_trade_product_exact_economics.down.sql | 0
Rcrates/replica_db/migrations/0022_trade_product_exact_economics.up.sql -> crates/replica_store/migrations/0022_trade_product_exact_economics.up.sql | 0
Rcrates/replica_db/migrations/0023_trade_product_verified_primary_bin.down.sql -> crates/replica_store/migrations/0023_trade_product_verified_primary_bin.down.sql | 0
Rcrates/replica_db/migrations/0023_trade_product_verified_primary_bin.up.sql -> crates/replica_store/migrations/0023_trade_product_verified_primary_bin.up.sql | 0
Acrates/replica_store/src/backup.rs | 993+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/export.rs | 285+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/lib.rs | 784+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rcrates/replica_db/src/migrations.rs -> crates/replica_store/src/migrations.rs | 0
Acrates/replica_store/src/models/farm.rs | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/models/farm_gcs_location.rs | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/models/farm_member.rs | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/models/farm_member_claim.rs | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/models/farm_tag.rs | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/models/gcs_location.rs | 238+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/models/log_error.rs | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/models/media_image.rs | 218+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rcrates/replica_db/src/models/mod.rs -> crates/replica_store/src/models/mod.rs | 0
Acrates/replica_store/src/models/nostr_event_head.rs | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/models/nostr_profile.rs | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/models/nostr_profile_relay.rs | 45+++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/models/nostr_relay.rs | 218+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/models/plot.rs | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/models/plot_gcs_location.rs | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/models/plot_tag.rs | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/models/trade_product.rs | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/models/trade_product_location.rs | 45+++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/src/models/trade_product_media.rs | 45+++++++++++++++++++++++++++++++++++++++++++++
Rcrates/replica_db/src/query.rs -> crates/replica_store/src/query.rs | 0
Acrates/replica_store/tests/backup_export_paths.rs | 305++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/tests/error_paths.rs | 977+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/tests/full_mode.rs | 1350+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/tests/migration_repairs.rs | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store/tests/region_scripted_paths.rs | 786+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/replica_sync/Cargo.toml | 6+++---
Mcrates/replica_sync/src/emit.rs | 74++++++++++++++++++++++++++++++++++++--------------------------------------
Mcrates/replica_sync/src/error.rs | 4++--
Mcrates/replica_sync/src/ingest.rs | 60++++++++++++++++++++++++++++++------------------------------
Mcrates/replica_sync/src/sync_state.rs | 14+++++++-------
Mcrates/replica_sync/src/tests.rs | 24++++++++++++------------
Mcrates/replica_sync/tests/ingest_roundtrip.rs | 34+++++++++++++++-------------------
Dcrates/sp1_guest_trade/Cargo.toml | 36------------------------------------
Dcrates/sp1_guest_trade/src/bin/order_acceptance_guest.rs | 14--------------
Dcrates/sp1_host_trade/Cargo.toml | 43-------------------------------------------
Dcrates/sp1_host_trade/build.rs | 26--------------------------
Dcrates/sp1_host_trade/src/lib.rs | 3111-------------------------------------------------------------------------------
Acrates/trade_sp1_guest/Cargo.toml | 36++++++++++++++++++++++++++++++++++++
Acrates/trade_sp1_guest/src/bin/order_acceptance_guest.rs | 14++++++++++++++
Rcrates/sp1_guest_trade/src/lib.rs -> crates/trade_sp1_guest/src/lib.rs | 0
Acrates/trade_sp1_host/Cargo.toml | 43+++++++++++++++++++++++++++++++++++++++++++
Acrates/trade_sp1_host/build.rs | 26++++++++++++++++++++++++++
Acrates/trade_sp1_host/src/lib.rs | 3111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rcrates/sp1_host_trade/tests/fixtures/remote_returned_proof_order_acceptance_core_v1.json -> crates/trade_sp1_host/tests/fixtures/remote_returned_proof_order_acceptance_core_v1.json | 0
Mtools/xtask/src/contract.rs | 4++--
191 files changed, 16478 insertions(+), 16486 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -4282,21 +4282,6 @@ dependencies = [ ] [[package]] -name = "radroots_event_store" -version = "0.1.0-alpha.2" -dependencies = [ - "radroots_event", - "radroots_nostr", - "radroots_transport", - "serde", - "serde_json", - "sqlx", - "tempfile", - "thiserror 1.0.69", - "tokio", -] - -[[package]] name = "radroots_event" version = "0.1.0-alpha.2" dependencies = [ @@ -4333,6 +4318,21 @@ dependencies = [ ] [[package]] +name = "radroots_event_store" +version = "0.1.0-alpha.2" +dependencies = [ + "radroots_event", + "radroots_nostr", + "radroots_transport", + "serde", + "serde_json", + "sqlx", + "tempfile", + "thiserror 1.0.69", + "tokio", +] + +[[package]] name = "radroots_geocoder" version = "0.1.0-alpha.2" dependencies = [ @@ -4367,16 +4367,6 @@ dependencies = [ ] [[package]] -name = "radroots_runtime_store" -version = "0.1.0-alpha.2" -dependencies = [ - "radroots_sql_core", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] name = "radroots_log" version = "0.1.0-alpha.2" dependencies = [ @@ -4458,8 +4448,8 @@ name = "radroots_nostr_accounts" version = "0.1.0-alpha.2" dependencies = [ "radroots_identity", - "radroots_nostr_ndb", "radroots_nostr_signer", + "radroots_nostrdb", "radroots_protected_store", "radroots_runtime", "radroots_secret_vault", @@ -4483,21 +4473,6 @@ dependencies = [ ] [[package]] -name = "radroots_nostr_ndb" -version = "0.1.0-alpha.2" -dependencies = [ - "futures", - "hex", - "nostrdb", - "radroots_nostr", - "radroots_nostr_runtime", - "serde_json", - "tempfile", - "thiserror 1.0.69", - "tokio", -] - -[[package]] name = "radroots_nostr_runtime" version = "0.1.0-alpha.2" dependencies = [ @@ -4528,12 +4503,27 @@ dependencies = [ ] [[package]] +name = "radroots_nostrdb" +version = "0.1.0-alpha.2" +dependencies = [ + "futures", + "hex", + "nostrdb", + "radroots_nostr", + "radroots_nostr_runtime", + "serde_json", + "tempfile", + "thiserror 1.0.69", + "tokio", +] + +[[package]] name = "radroots_outbox" version = "0.1.0-alpha.2" dependencies = [ "hex", - "radroots_event_store", "radroots_event", + "radroots_event_store", "radroots_nostr", "radroots_transport", "serde", @@ -4559,24 +4549,24 @@ dependencies = [ ] [[package]] -name = "radroots_replica_db" +name = "radroots_replica_schema" version = "0.1.0-alpha.2" dependencies = [ - "hex", - "radroots_replica_db_schema", - "radroots_sql_core", + "dto_bindgen_core", "serde", "serde_json", - "sha2", ] [[package]] -name = "radroots_replica_db_schema" +name = "radroots_replica_store" version = "0.1.0-alpha.2" dependencies = [ - "dto_bindgen_core", + "hex", + "radroots_replica_schema", + "radroots_sql_core", "serde", "serde_json", + "sha2", ] [[package]] @@ -4588,8 +4578,8 @@ dependencies = [ "radroots_core", "radroots_event", "radroots_event_codec", - "radroots_replica_db", - "radroots_replica_db_schema", + "radroots_replica_schema", + "radroots_replica_store", "radroots_sql_core", "serde", "serde_json", @@ -4654,6 +4644,16 @@ dependencies = [ ] [[package]] +name = "radroots_runtime_store" +version = "0.1.0-alpha.2" +dependencies = [ + "radroots_sql_core", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] name = "radroots_secret_vault" version = "0.1.0-alpha.2" dependencies = [ @@ -4782,36 +4782,6 @@ dependencies = [ ] [[package]] -name = "radroots_sp1_guest_trade" -version = "0.1.0-alpha.2" -dependencies = [ - "serde", - "serde_json", - "sha2", - "sp1-zkvm", - "thiserror 1.0.69", -] - -[[package]] -name = "radroots_sp1_host_trade" -version = "0.1.0-alpha.2" -dependencies = [ - "base64 0.22.1", - "bincode", - "futures", - "radroots_event", - "radroots_sp1_guest_trade", - "radroots_trade", - "serde", - "serde_json", - "sha2", - "sp1-build", - "sp1-sdk", - "thiserror 1.0.69", - "tokio", -] - -[[package]] name = "radroots_sql_core" version = "0.1.0-alpha.2" dependencies = [ @@ -4839,9 +4809,9 @@ dependencies = [ "hex", "radroots_authority", "radroots_core", - "radroots_event_store", "radroots_event", "radroots_event_codec", + "radroots_event_store", "radroots_nostr", "radroots_transport", "serde", @@ -4853,6 +4823,36 @@ dependencies = [ ] [[package]] +name = "radroots_trade_sp1_guest" +version = "0.1.0-alpha.2" +dependencies = [ + "serde", + "serde_json", + "sha2", + "sp1-zkvm", + "thiserror 1.0.69", +] + +[[package]] +name = "radroots_trade_sp1_host" +version = "0.1.0-alpha.2" +dependencies = [ + "base64 0.22.1", + "bincode", + "futures", + "radroots_event", + "radroots_trade", + "radroots_trade_sp1_guest", + "serde", + "serde_json", + "sha2", + "sp1-build", + "sp1-sdk", + "thiserror 1.0.69", + "tokio", +] + +[[package]] name = "radroots_transport" version = "0.1.0-alpha.2" dependencies = [ @@ -4868,8 +4868,8 @@ version = "0.1.0-alpha.2" dependencies = [ "futures", "nostr", - "radroots_event_store", "radroots_event", + "radroots_event_store", "radroots_nostr", "radroots_outbox", "radroots_transport", diff --git a/Cargo.toml b/Cargo.toml @@ -15,7 +15,7 @@ members = [ "crates/nostr_accounts", "crates/nostr_connect", "crates/nostr_signer", - "crates/nostr_ndb", + "crates/nostrdb", "crates/nostr_runtime", "crates/outbox", "crates/transport_publish_protocol", @@ -34,14 +34,14 @@ members = [ "crates/simplex_smp_transport", "crates/sql_core", "crates/test_fixtures", - "crates/replica_db_schema", + "crates/replica_schema", "crates/replica_sync", - "crates/replica_db", + "crates/replica_store", "crates/runtime_paths", "crates/runtime_distribution", "crates/runtime_manager", - "crates/sp1_guest_trade", - "crates/sp1_host_trade", + "crates/trade_sp1_guest", + "crates/trade_sp1_host", "crates/trade", "crates/transport", "crates/mesh", @@ -77,7 +77,7 @@ radroots_nostr = { path = "crates/nostr", version = "0.1.0-alpha.2", default-fea radroots_nostr_accounts = { path = "crates/nostr_accounts", version = "0.1.0-alpha.2", default-features = false } radroots_nostr_connect = { path = "crates/nostr_connect", version = "0.1.0-alpha.2", default-features = false } radroots_nostr_signer = { path = "crates/nostr_signer", version = "0.1.0-alpha.2", default-features = false } -radroots_nostr_ndb = { path = "crates/nostr_ndb", version = "0.1.0-alpha.2", default-features = false } +radroots_nostrdb = { path = "crates/nostrdb", version = "0.1.0-alpha.2", default-features = false } radroots_runtime = { path = "crates/runtime", version = "0.1.0-alpha.2", default-features = false } radroots_runtime_paths = { path = "crates/runtime_paths", version = "0.1.0-alpha.2", default-features = false } radroots_runtime_distribution = { path = "crates/runtime_distribution", version = "0.1.0-alpha.2", default-features = false } @@ -99,9 +99,9 @@ radroots_simplex_smp_proto = { path = "crates/simplex_smp_proto", version = "0.1 radroots_simplex_smp_transport = { path = "crates/simplex_smp_transport", version = "0.1.0-alpha.2", default-features = false } radroots_sql_core = { path = "crates/sql_core", version = "0.1.0-alpha.2", default-features = false } radroots_test_fixtures = { path = "crates/test_fixtures", version = "0.1.0-alpha.2" } -radroots_replica_db_schema = { path = "crates/replica_db_schema", version = "0.1.0-alpha.2", default-features = false } +radroots_replica_schema = { path = "crates/replica_schema", version = "0.1.0-alpha.2", default-features = false } radroots_replica_sync = { path = "crates/replica_sync", version = "0.1.0-alpha.2", default-features = false } -radroots_replica_db = { path = "crates/replica_db", version = "0.1.0-alpha.2", default-features = false } +radroots_replica_store = { path = "crates/replica_store", version = "0.1.0-alpha.2", default-features = false } radroots_trade = { path = "crates/trade", version = "0.1.0-alpha.2", default-features = false } radroots_transport = { path = "crates/transport", version = "0.1.0-alpha.2", default-features = false } radroots_mesh = { path = "crates/mesh", version = "0.1.0-alpha.2", default-features = false } @@ -110,8 +110,8 @@ radroots_mesh_agent_proto = { path = "crates/mesh_agent_proto", version = "0.1.0 radroots_protected_store = { path = "crates/protected_store", version = "0.1.0-alpha.2", default-features = false } radroots_secret_vault = { path = "crates/secret_vault", version = "0.1.0-alpha.2", default-features = false } radroots_simplex_app_store = { path = "crates/simplex_app_store", version = "0.1.0-alpha.2", default-features = false } -radroots_sp1_guest_trade = { path = "crates/sp1_guest_trade", version = "0.1.0-alpha.2", default-features = false } -radroots_sp1_host_trade = { path = "crates/sp1_host_trade", version = "0.1.0-alpha.2", default-features = false } +radroots_trade_sp1_guest = { path = "crates/trade_sp1_guest", version = "0.1.0-alpha.2", default-features = false } +radroots_trade_sp1_host = { path = "crates/trade_sp1_host", version = "0.1.0-alpha.2", default-features = false } anyhow = { version = "1" } aes-gcm = { version = "0.10.3", default-features = false, features = [ diff --git a/build/nix/common.nix b/build/nix/common.nix @@ -91,7 +91,7 @@ let "radroots_event" "radroots_trade" "radroots_identity" - "radroots_replica_db_schema" + "radroots_replica_schema" "radroots_event_codec" "radroots_nostr_connect" "radroots_nostr_signer" diff --git a/contracts/coverage.toml b/contracts/coverage.toml @@ -19,7 +19,7 @@ require_branches = false temporary = true reason = "branch coverage is not applicable while the crate has no measured branch records" -[overrides.radroots_replica_db_schema] +[overrides.radroots_replica_schema] require_branches = false temporary = true reason = "branch coverage is not applicable while the crate has no measured branch records" @@ -48,7 +48,7 @@ crates = [ "radroots_nostr", "radroots_nostr_accounts", "radroots_nostr_connect", - "radroots_nostr_ndb", + "radroots_nostrdb", "radroots_nostr_runtime", "radroots_nostr_signer", "radroots_outbox", @@ -56,16 +56,16 @@ crates = [ "radroots_transport_publish_protocol", "radroots_transport_nostr", "radroots_transport_reticulum", - "radroots_replica_db", - "radroots_replica_db_schema", + "radroots_replica_store", + "radroots_replica_schema", "radroots_replica_sync", "radroots_runtime", "radroots_runtime_distribution", "radroots_runtime_manager", "radroots_runtime_paths", "radroots_secret_vault", - "radroots_sp1_guest_trade", - "radroots_sp1_host_trade", + "radroots_trade_sp1_guest", + "radroots_trade_sp1_host", "radroots_sql_core", "radroots_test_fixtures", "radroots_trade", diff --git a/contracts/manifest.toml b/contracts/manifest.toml @@ -35,8 +35,8 @@ published_support = [ "radroots_geocoder", "radroots_event_index", "radroots_sql_core", - "radroots_replica_db_schema", - "radroots_replica_db", + "radroots_replica_schema", + "radroots_replica_store", "radroots_replica_sync", ] deferred_publication = [ @@ -52,16 +52,16 @@ deferred_publication = [ "radroots_mesh_agent_proto", "radroots_net", "radroots_nostr_runtime", - "radroots_nostr_ndb", + "radroots_nostrdb", "radroots_simplex_chat_proto", "radroots_simplex_smp_proto", - "radroots_sp1_guest_trade", - "radroots_sp1_host_trade", + "radroots_trade_sp1_guest", + "radroots_trade_sp1_host", ] [surface.internal_replica_crates] -schema = "radroots_replica_db_schema" -storage = "radroots_replica_db" +schema = "radroots_replica_schema" +storage = "radroots_replica_store" sync = "radroots_replica_sync" [policy] diff --git a/contracts/replica.toml b/contracts/replica.toml @@ -4,8 +4,8 @@ version = "0.1.0" purpose = "offline-first local replica state and deterministic sync" [crate_family] -schema = "radroots_replica_db_schema" -storage = "radroots_replica_db" +schema = "radroots_replica_schema" +storage = "radroots_replica_store" sync = "radroots_replica_sync" [policy] diff --git a/crates/authority/src/local_signer.rs b/crates/authority/src/local_signer.rs @@ -41,7 +41,7 @@ mod tests { use radroots_event::RadrootsEventEnvelope; use radroots_event::kinds::KIND_POST; use radroots_nostr::prelude::{ - RadrootsEventEnvelopeVerification, RadrootsNostrSecretKey, radroots_nostr_verify_event, + RadrootsNostrEventVerification, RadrootsNostrSecretKey, radroots_nostr_verify_event, }; const FIXTURE_ALICE_SECRET_KEY_HEX: &str = @@ -97,7 +97,7 @@ mod tests { assert_eq!(signed.pubkey, draft.expected_pubkey); assert_eq!( radroots_nostr_verify_event(&verification_event(&signed)), - RadrootsEventEnvelopeVerification::Verified + RadrootsNostrEventVerification::Verified ); } } diff --git a/crates/nostr_accounts/Cargo.toml b/crates/nostr_accounts/Cargo.toml @@ -26,7 +26,7 @@ std = [ file-store = ["std"] memory-vault = ["std", "radroots_secret_vault/memory-vault"] os-keyring = ["std", "radroots_secret_vault/os-keyring"] -ndb-bridge = ["std", "dep:radroots_nostr_ndb"] +nostrdb-bridge = ["std", "dep:radroots_nostrdb"] [dependencies] radroots_identity = { workspace = true, optional = true, default-features = false, features = [ @@ -35,8 +35,8 @@ radroots_identity = { workspace = true, optional = true, default-features = fals "json-file", ] } radroots_nostr_signer = { workspace = true, optional = true } -radroots_nostr_ndb = { workspace = true, optional = true, default-features = false, features = [ - "ndb", +radroots_nostrdb = { workspace = true, optional = true, default-features = false, features = [ + "nostrdb", "giftwrap", "rt", ] } diff --git a/crates/nostr_accounts/src/lib.rs b/crates/nostr_accounts/src/lib.rs @@ -6,8 +6,8 @@ pub mod error; pub mod manager; #[cfg(feature = "std")] pub mod model; -#[cfg(feature = "ndb-bridge")] -pub mod ndb_bridge; +#[cfg(feature = "nostrdb-bridge")] +pub mod nostrdb_bridge; #[cfg(feature = "std")] pub mod store; #[cfg(feature = "std")] @@ -22,8 +22,8 @@ pub mod prelude { RADROOTS_NOSTR_ACCOUNTS_STORE_VERSION, RadrootsNostrAccountRecord, RadrootsNostrAccountStatus, RadrootsNostrAccountStoreState, }; - #[cfg(feature = "ndb-bridge")] - pub use crate::ndb_bridge::radroots_nostr_accounts_register_default_secret_with_ndb; + #[cfg(feature = "nostrdb-bridge")] + pub use crate::nostrdb_bridge::radroots_nostr_accounts_register_default_secret_with_nostrdb; #[cfg(feature = "std")] pub use crate::store::{ RadrootsNostrAccountStore, RadrootsNostrFileAccountStore, RadrootsNostrMemoryAccountStore, diff --git a/crates/nostr_accounts/src/ndb_bridge.rs b/crates/nostr_accounts/src/ndb_bridge.rs @@ -1,67 +0,0 @@ -use crate::error::RadrootsNostrAccountsError; -use crate::manager::RadrootsNostrAccountsManager; -use radroots_nostr_ndb::prelude::RadrootsNostrNdb; - -pub fn radroots_nostr_accounts_register_default_secret_with_ndb( - manager: &RadrootsNostrAccountsManager, - ndb: &RadrootsNostrNdb, -) -> Result<bool, RadrootsNostrAccountsError> { - let Some(signer) = manager.default_signer_capability()? else { - return Ok(false); - }; - let Some(identity) = manager.resolve_signing_identity_for_signer(&signer)? else { - return Ok(false); - }; - Ok(ndb.add_giftwrap_secret_key(identity.secret_key_bytes())) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::store::RadrootsNostrFileAccountStore; - use crate::vault::RadrootsNostrSecretVaultMemory; - use radroots_nostr_ndb::prelude::RadrootsNostrNdbConfig; - use std::sync::Arc; - - #[test] - fn register_default_secret_returns_true_for_signing_account() { - let temp = tempfile::tempdir().expect("tempdir"); - let store = Arc::new(RadrootsNostrFileAccountStore::new( - temp.path().join("accounts.json"), - )); - let vault = Arc::new(RadrootsNostrSecretVaultMemory::new()); - let manager = RadrootsNostrAccountsManager::new(store, vault).expect("manager"); - manager - .generate_identity(Some("primary".into()), true) - .expect("generate"); - - let ndb = RadrootsNostrNdb::open(RadrootsNostrNdbConfig::new(temp.path().join("ndb"))) - .expect("ndb"); - let added = radroots_nostr_accounts_register_default_secret_with_ndb(&manager, &ndb) - .expect("register"); - assert!(added); - } - - #[test] - fn register_default_secret_returns_false_for_watch_only_account() { - let temp = tempfile::tempdir().expect("tempdir"); - let store = Arc::new(RadrootsNostrFileAccountStore::new( - temp.path().join("accounts.json"), - )); - let vault = Arc::new(RadrootsNostrSecretVaultMemory::new()); - let manager = RadrootsNostrAccountsManager::new(store, vault).expect("manager"); - manager - .upsert_public_identity( - radroots_identity::RadrootsIdentity::generate().to_public(), - Some("watch".into()), - true, - ) - .expect("watch"); - - let ndb = RadrootsNostrNdb::open(RadrootsNostrNdbConfig::new(temp.path().join("ndb"))) - .expect("ndb"); - let added = radroots_nostr_accounts_register_default_secret_with_ndb(&manager, &ndb) - .expect("register"); - assert!(!added); - } -} diff --git a/crates/nostr_accounts/src/nostrdb_bridge.rs b/crates/nostr_accounts/src/nostrdb_bridge.rs @@ -0,0 +1,71 @@ +use crate::error::RadrootsNostrAccountsError; +use crate::manager::RadrootsNostrAccountsManager; +use radroots_nostrdb::prelude::RadrootsNostrdb; + +pub fn radroots_nostr_accounts_register_default_secret_with_nostrdb( + manager: &RadrootsNostrAccountsManager, + nostrdb: &RadrootsNostrdb, +) -> Result<bool, RadrootsNostrAccountsError> { + let Some(signer) = manager.default_signer_capability()? else { + return Ok(false); + }; + let Some(identity) = manager.resolve_signing_identity_for_signer(&signer)? else { + return Ok(false); + }; + Ok(nostrdb.add_giftwrap_secret_key(identity.secret_key_bytes())) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::store::RadrootsNostrFileAccountStore; + use crate::vault::RadrootsNostrSecretVaultMemory; + use radroots_nostrdb::prelude::RadrootsNostrdbConfig; + use std::sync::Arc; + + #[test] + fn register_default_secret_returns_true_for_signing_account() { + let temp = tempfile::tempdir().expect("tempdir"); + let store = Arc::new(RadrootsNostrFileAccountStore::new( + temp.path().join("accounts.json"), + )); + let vault = Arc::new(RadrootsNostrSecretVaultMemory::new()); + let manager = RadrootsNostrAccountsManager::new(store, vault).expect("manager"); + manager + .generate_identity(Some("primary".into()), true) + .expect("generate"); + + let nostrdb = + RadrootsNostrdb::open(RadrootsNostrdbConfig::new(temp.path().join("nostrdb"))) + .expect("nostrdb"); + let added = + radroots_nostr_accounts_register_default_secret_with_nostrdb(&manager, &nostrdb) + .expect("register"); + assert!(added); + } + + #[test] + fn register_default_secret_returns_false_for_watch_only_account() { + let temp = tempfile::tempdir().expect("tempdir"); + let store = Arc::new(RadrootsNostrFileAccountStore::new( + temp.path().join("accounts.json"), + )); + let vault = Arc::new(RadrootsNostrSecretVaultMemory::new()); + let manager = RadrootsNostrAccountsManager::new(store, vault).expect("manager"); + manager + .upsert_public_identity( + radroots_identity::RadrootsIdentity::generate().to_public(), + Some("watch".into()), + true, + ) + .expect("watch"); + + let nostrdb = + RadrootsNostrdb::open(RadrootsNostrdbConfig::new(temp.path().join("nostrdb"))) + .expect("nostrdb"); + let added = + radroots_nostr_accounts_register_default_secret_with_nostrdb(&manager, &nostrdb) + .expect("register"); + assert!(!added); + } +} diff --git a/crates/nostr_ndb/Cargo.toml b/crates/nostr_ndb/Cargo.toml @@ -1,40 +0,0 @@ -[package] -name = "radroots_nostr_ndb" -publish = false -version = "0.1.0-alpha.2" -edition.workspace = true -authors = ["Tyson Lupul <tyson@radroots.org>"] -rust-version.workspace = true -license.workspace = true -description = "NostrDB integration layer for Radroots" -repository.workspace = true -homepage.workspace = true -readme = "README" - -[features] -default = ["std", "rt", "ndb", "runtime-adapter"] -std = [] -rt = ["std", "dep:futures", "dep:tokio"] -ndb = ["std", "dep:nostrdb"] -runtime-adapter = ["std", "dep:radroots_nostr_runtime"] -giftwrap = ["std", "ndb"] - -[dependencies] -radroots_nostr = { workspace = true, default-features = false, features = [ - "std", -] } -radroots_nostr_runtime = { workspace = true, optional = true, default-features = false, features = [ - "std", - "rt", - "nostr-client", -] } -futures = { workspace = true, optional = true } -hex = { workspace = true } -nostrdb = { version = "0.9.0", optional = true } -serde_json = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true, optional = true, features = ["rt", "sync", "time"] } - -[dev-dependencies] -tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] } -tempfile = { workspace = true } diff --git a/crates/nostr_ndb/README b/crates/nostr_ndb/README @@ -1,23 +0,0 @@ -# radroots_nostr_ndb - -This is the README for `radroots_nostr_ndb`, which provides nostrdb adapter -primitives for the `radroots` core libraries. - -## Overview - - * configuration, filter, ingest, query, and subscription types for - nostrdb-backed workflows; - * runtime-adapter hooks that let higher layers drive nostrdb as an event sink; - * feature-gated support for the `nostrdb` crate, runtime glue, and gift-wrap - handling; - * a `std`-based integration crate with optional async runtime support. - -## Copyright - -Except as otherwise noted, all files in the `radroots_nostr_ndb` distribution -are - - Copyright (c) 2025 Tyson Lupul - -For information on usage and redistribution, and for a DISCLAIMER OF ALL -WARRANTIES, see LICENSE included in the `radroots_nostr_ndb` distribution. diff --git a/crates/nostr_ndb/src/config.rs b/crates/nostr_ndb/src/config.rs @@ -1,51 +0,0 @@ -use std::path::{Path, PathBuf}; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct RadrootsNostrNdbConfig { - db_dir: PathBuf, - mapsize_bytes: Option<usize>, - ingester_threads: Option<i32>, - skip_validation: bool, -} - -impl RadrootsNostrNdbConfig { - pub fn new(db_dir: impl Into<PathBuf>) -> Self { - Self { - db_dir: db_dir.into(), - mapsize_bytes: None, - ingester_threads: None, - skip_validation: false, - } - } - - pub fn db_dir(&self) -> &Path { - &self.db_dir - } - - pub fn mapsize_bytes(&self) -> Option<usize> { - self.mapsize_bytes - } - - pub fn ingester_threads(&self) -> Option<i32> { - self.ingester_threads - } - - pub fn skip_validation(&self) -> bool { - self.skip_validation - } - - pub fn with_mapsize_bytes(mut self, bytes: usize) -> Self { - self.mapsize_bytes = Some(bytes); - self - } - - pub fn with_ingester_threads(mut self, threads: i32) -> Self { - self.ingester_threads = Some(threads); - self - } - - pub fn with_skip_validation(mut self, skip_validation: bool) -> Self { - self.skip_validation = skip_validation; - self - } -} diff --git a/crates/nostr_ndb/src/error.rs b/crates/nostr_ndb/src/error.rs @@ -1,58 +0,0 @@ -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum RadrootsNostrNdbError { - #[error("database path must be utf-8")] - NonUtf8Path, - - #[error("invalid hex for {field}: {reason}")] - InvalidHex { field: &'static str, reason: String }, - - #[error("invalid hex length for {field}: expected {expected} bytes, got {actual}")] - InvalidHexLength { - field: &'static str, - expected: usize, - actual: usize, - }, - - #[error("event json encode failed: {0}")] - EventJsonEncode(String), - - #[error("nostrdb error: {0}")] - Ndb(String), -} - -#[cfg(feature = "ndb")] -impl From<nostrdb::Error> for RadrootsNostrNdbError { - fn from(value: nostrdb::Error) -> Self { - Self::Ndb(value.to_string()) - } -} - -impl From<serde_json::Error> for RadrootsNostrNdbError { - fn from(value: serde_json::Error) -> Self { - Self::EventJsonEncode(value.to_string()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn converts_nostrdb_error() { - let converted: RadrootsNostrNdbError = nostrdb::Error::NotFound.into(); - assert!(converted.to_string().starts_with("nostrdb error:")); - } - - #[test] - fn converts_serde_json_error() { - let source = serde_json::from_str::<serde_json::Value>("not json").expect_err("json error"); - let converted: RadrootsNostrNdbError = source.into(); - assert!( - converted - .to_string() - .starts_with("event json encode failed:") - ); - } -} diff --git a/crates/nostr_ndb/src/filter.rs b/crates/nostr_ndb/src/filter.rs @@ -1,272 +0,0 @@ -use crate::error::RadrootsNostrNdbError; - -#[derive(Debug, Clone, Eq, PartialEq, Default)] -pub struct RadrootsNostrNdbFilterSpec { - event_ids_hex: Vec<String>, - authors_hex: Vec<String>, - kinds: Vec<u16>, - since_unix: Option<u64>, - until_unix: Option<u64>, - limit: Option<u64>, - search: Option<String>, -} - -impl RadrootsNostrNdbFilterSpec { - pub fn new() -> Self { - Self::default() - } - - pub fn text_notes(limit: Option<u64>, since_unix: Option<u64>) -> Self { - let mut filter = Self::new().with_kind(1); - if let Some(limit) = limit { - filter = filter.with_limit(limit); - } - if let Some(since_unix) = since_unix { - filter = filter.with_since_unix(since_unix); - } - filter - } - - pub fn with_event_id_hex(mut self, id_hex: impl Into<String>) -> Self { - self.event_ids_hex.push(id_hex.into()); - self - } - - pub fn with_author_hex(mut self, author_hex: impl Into<String>) -> Self { - self.authors_hex.push(author_hex.into()); - self - } - - pub fn with_kind(mut self, kind: u16) -> Self { - self.kinds.push(kind); - self - } - - pub fn with_since_unix(mut self, since_unix: u64) -> Self { - self.since_unix = Some(since_unix); - self - } - - pub fn with_until_unix(mut self, until_unix: u64) -> Self { - self.until_unix = Some(until_unix); - self - } - - pub fn with_limit(mut self, limit: u64) -> Self { - self.limit = Some(limit); - self - } - - pub fn with_search(mut self, search: impl Into<String>) -> Self { - self.search = Some(search.into()); - self - } - - pub fn event_ids_hex(&self) -> &[String] { - &self.event_ids_hex - } - - pub fn authors_hex(&self) -> &[String] { - &self.authors_hex - } - - pub fn kinds(&self) -> &[u16] { - &self.kinds - } - - pub fn since_unix(&self) -> Option<u64> { - self.since_unix - } - - pub fn until_unix(&self) -> Option<u64> { - self.until_unix - } - - pub fn limit(&self) -> Option<u64> { - self.limit - } - - pub fn search(&self) -> Option<&str> { - self.search.as_deref() - } - - pub(crate) fn to_ndb_filter(&self) -> Result<nostrdb::Filter, RadrootsNostrNdbError> { - let mut builder = nostrdb::Filter::new(); - - if !self.event_ids_hex.is_empty() { - let event_ids = self - .event_ids_hex - .iter() - .map(|hex_value| parse_hex_32(hex_value, "event_id")) - .collect::<Result<Vec<_>, _>>()?; - builder = builder.ids(event_ids.iter()); - } - - if !self.authors_hex.is_empty() { - let authors = self - .authors_hex - .iter() - .map(|hex_value| parse_hex_32(hex_value, "author")) - .collect::<Result<Vec<_>, _>>()?; - builder = builder.authors(authors.iter()); - } - - if !self.kinds.is_empty() { - builder = builder.kinds(self.kinds.iter().map(|kind| *kind as u64)); - } - - if let Some(since_unix) = self.since_unix { - builder = builder.since(since_unix); - } - - if let Some(until_unix) = self.until_unix { - builder = builder.until(until_unix); - } - - if let Some(limit) = self.limit { - builder = builder.limit(limit); - } - - if let Some(search) = self.search() { - builder = builder.search(search); - } - - Ok(builder.build()) - } -} - -pub(crate) fn parse_hex_32( - value: &str, - field: &'static str, -) -> Result<[u8; 32], RadrootsNostrNdbError> { - let bytes = hex::decode(value).map_err(|source| RadrootsNostrNdbError::InvalidHex { - field, - reason: source.to_string(), - })?; - - if bytes.len() != 32 { - return Err(RadrootsNostrNdbError::InvalidHexLength { - field, - expected: 32, - actual: bytes.len(), - }); - } - - let mut out = [0u8; 32]; - out.copy_from_slice(bytes.as_slice()); - Ok(out) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn valid_hex_32(value: u8) -> String { - format!("{value:02x}").repeat(32) - } - - #[test] - fn filter_spec_builders_and_accessors_round_trip() { - let event_id = valid_hex_32(0x11); - let author = valid_hex_32(0x22); - - let empty_notes = RadrootsNostrNdbFilterSpec::text_notes(None, None); - assert_eq!(empty_notes.kinds(), &[1]); - assert_eq!(empty_notes.limit(), None); - assert_eq!(empty_notes.since_unix(), None); - - let spec = RadrootsNostrNdbFilterSpec::text_notes(Some(50), Some(100)) - .with_event_id_hex(event_id.clone()) - .with_author_hex(author.clone()) - .with_kind(30023) - .with_since_unix(200) - .with_until_unix(300) - .with_limit(10) - .with_search("coffee"); - - assert_eq!(spec.event_ids_hex(), std::slice::from_ref(&event_id)); - assert_eq!(spec.authors_hex(), std::slice::from_ref(&author)); - assert_eq!(spec.kinds(), &[1, 30023]); - assert_eq!(spec.since_unix(), Some(200)); - assert_eq!(spec.until_unix(), Some(300)); - assert_eq!(spec.limit(), Some(10)); - assert_eq!(spec.search(), Some("coffee")); - - let empty = RadrootsNostrNdbFilterSpec::new(); - let _ = empty.to_ndb_filter().expect("empty ndb filter"); - } - - #[test] - fn to_ndb_filter_builds_supported_success_paths() { - let event_id = valid_hex_32(0x11); - let author = valid_hex_32(0x22); - - let _ = RadrootsNostrNdbFilterSpec::new() - .with_event_id_hex(event_id) - .to_ndb_filter() - .expect("event id filter"); - - let _ = RadrootsNostrNdbFilterSpec::new() - .with_author_hex(author) - .to_ndb_filter() - .expect("author filter"); - - let _ = RadrootsNostrNdbFilterSpec::new() - .with_kind(1) - .with_since_unix(200) - .with_until_unix(300) - .with_limit(10) - .to_ndb_filter() - .expect("range filter"); - - let _ = RadrootsNostrNdbFilterSpec::new() - .with_search("coffee") - .to_ndb_filter() - .expect("search filter"); - } - - #[test] - fn parse_hex_32_validates_input() { - let valid = parse_hex_32(valid_hex_32(0xab).as_str(), "value").expect("valid"); - assert_eq!(valid, [0xab; 32]); - - let invalid_hex = parse_hex_32("zz", "value"); - assert!(matches!( - invalid_hex, - Err(RadrootsNostrNdbError::InvalidHex { field: "value", .. }) - )); - - let invalid_len = parse_hex_32("abcd", "value"); - assert!(matches!( - invalid_len, - Err(RadrootsNostrNdbError::InvalidHexLength { - field: "value", - expected: 32, - .. - }) - )); - } - - #[test] - fn to_ndb_filter_rejects_invalid_event_id_and_author_hex() { - let bad_event_id = RadrootsNostrNdbFilterSpec::new().with_event_id_hex("not-hex"); - let bad_event_result = bad_event_id.to_ndb_filter(); - assert!(matches!( - bad_event_result, - Err(RadrootsNostrNdbError::InvalidHex { - field: "event_id", - .. - }) - )); - - let bad_author = RadrootsNostrNdbFilterSpec::new().with_author_hex("not-hex"); - let bad_author_result = bad_author.to_ndb_filter(); - assert!(matches!( - bad_author_result, - Err(RadrootsNostrNdbError::InvalidHex { - field: "author", - .. - }) - )); - } -} diff --git a/crates/nostr_ndb/src/ingest.rs b/crates/nostr_ndb/src/ingest.rs @@ -1,38 +0,0 @@ -#[derive(Debug, Clone, Default, Eq, PartialEq)] -pub enum RadrootsNostrNdbIngestSource { - #[default] - Client, - Relay { - relay_url: Option<String>, - }, -} - -impl RadrootsNostrNdbIngestSource { - pub fn client() -> Self { - Self::Client - } - - pub fn relay(relay_url: impl Into<String>) -> Self { - Self::Relay { - relay_url: Some(relay_url.into()), - } - } - - pub fn relay_unknown() -> Self { - Self::Relay { relay_url: None } - } - - pub(crate) fn to_ndb_metadata(&self) -> nostrdb::IngestMetadata { - match self { - Self::Client => nostrdb::IngestMetadata::new().client(true), - Self::Relay { relay_url } => { - let meta = nostrdb::IngestMetadata::new().client(false); - if let Some(relay_url) = relay_url { - meta.relay(relay_url.as_str()) - } else { - meta - } - } - } - } -} diff --git a/crates/nostr_ndb/src/lib.rs b/crates/nostr_ndb/src/lib.rs @@ -1,51 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std)] -#![forbid(unsafe_code)] - -#[cfg(not(feature = "std"))] -compile_error!("radroots_nostr_ndb requires the std feature"); - -extern crate alloc; - -#[cfg(test)] -mod test_fixtures; - -#[cfg(feature = "ndb")] -pub mod config; -pub mod error; -#[cfg(feature = "ndb")] -pub mod filter; -#[cfg(feature = "ndb")] -pub mod ingest; -#[cfg(feature = "ndb")] -pub mod ndb; -#[cfg(feature = "ndb")] -pub mod query; -#[cfg(all(feature = "ndb", feature = "runtime-adapter"))] -pub mod runtime_adapter; -#[cfg(feature = "ndb")] -pub mod subscription; - -pub mod prelude { - #[cfg(feature = "ndb")] - pub use crate::config::RadrootsNostrNdbConfig; - pub use crate::error::RadrootsNostrNdbError; - #[cfg(feature = "ndb")] - pub use crate::filter::RadrootsNostrNdbFilterSpec; - #[cfg(feature = "ndb")] - pub use crate::ingest::RadrootsNostrNdbIngestSource; - #[cfg(feature = "ndb")] - pub use crate::ndb::RadrootsNostrNdb; - #[cfg(feature = "ndb")] - pub use crate::query::{ - RadrootsNostrNdbNote, RadrootsNostrNdbProfile, RadrootsNostrNdbQuerySpec, - }; - #[cfg(all(feature = "ndb", feature = "runtime-adapter"))] - pub use crate::runtime_adapter::RadrootsNostrNdbEventSinkAdapter; - #[cfg(all(feature = "ndb", feature = "rt"))] - pub use crate::subscription::RadrootsNostrNdbSubscriptionStream; - #[cfg(feature = "ndb")] - pub use crate::subscription::{ - RadrootsNostrNdbNoteKey, RadrootsNostrNdbSubscriptionHandle, - RadrootsNostrNdbSubscriptionSpec, - }; -} diff --git a/crates/nostr_ndb/src/ndb.rs b/crates/nostr_ndb/src/ndb.rs @@ -1,1055 +0,0 @@ -use crate::config::RadrootsNostrNdbConfig; -use crate::error::RadrootsNostrNdbError; -use crate::filter::parse_hex_32; -use crate::ingest::RadrootsNostrNdbIngestSource; -use crate::query::{RadrootsNostrNdbNote, RadrootsNostrNdbProfile, RadrootsNostrNdbQuerySpec}; -use crate::subscription::{ - RadrootsNostrNdbNoteKey, RadrootsNostrNdbSubscriptionHandle, RadrootsNostrNdbSubscriptionSpec, - RadrootsNostrNdbSubscriptionStream, -}; -use radroots_nostr::prelude::RadrootsNostrEvent; -use std::path::Path; - -#[derive(Debug, Clone)] -pub struct RadrootsNostrNdb { - db_dir: std::path::PathBuf, - pub(crate) inner: nostrdb::Ndb, -} - -#[cfg(test)] -mod test_hooks { - use std::sync::atomic::{AtomicBool, Ordering}; - - pub static FORCE_EVENT_JSON_ERROR: AtomicBool = AtomicBool::new(false); - pub static FORCE_PROCESS_EVENT_ERROR: AtomicBool = AtomicBool::new(false); - pub static FORCE_SUBSCRIBE_ERROR: AtomicBool = AtomicBool::new(false); - pub static FORCE_UNSUBSCRIBE_ERROR: AtomicBool = AtomicBool::new(false); - pub static FORCE_WAIT_ERROR: AtomicBool = AtomicBool::new(false); - pub static FORCE_TRANSACTION_ERROR: AtomicBool = AtomicBool::new(false); - pub static FORCE_QUERY_ERROR: AtomicBool = AtomicBool::new(false); - pub static FORCE_NOTE_JSON_ERROR: AtomicBool = AtomicBool::new(false); - pub static FORCE_PROFILE_QUERY_ERROR: AtomicBool = AtomicBool::new(false); - - pub fn take(flag: &AtomicBool) -> bool { - flag.swap(false, Ordering::SeqCst) - } -} - -fn map_profile_lookup_result<T>( - result: Result<T, nostrdb::Error>, -) -> Result<Option<T>, RadrootsNostrNdbError> { - match result { - Ok(value) => Ok(Some(value)), - Err(nostrdb::Error::NotFound) => Ok(None), - Err(source) => Err(source.into()), - } -} - -impl RadrootsNostrNdb { - fn serialize_event(event: &RadrootsNostrEvent) -> Result<String, RadrootsNostrNdbError> { - #[cfg(test)] - if test_hooks::take(&test_hooks::FORCE_EVENT_JSON_ERROR) { - return Err(RadrootsNostrNdbError::EventJsonEncode( - "forced event json error".into(), - )); - } - serde_json::to_string(event).map_err(Into::into) - } - - fn process_event_with_inner( - &self, - json: &str, - metadata: nostrdb::IngestMetadata, - ) -> Result<(), RadrootsNostrNdbError> { - #[cfg(test)] - if test_hooks::take(&test_hooks::FORCE_PROCESS_EVENT_ERROR) { - return Err(RadrootsNostrNdbError::Ndb( - "forced process event error".into(), - )); - } - self.inner - .process_event_with(json, metadata) - .map_err(Into::into) - } - - fn subscribe_inner( - &self, - filters: &[nostrdb::Filter], - ) -> Result<nostrdb::Subscription, RadrootsNostrNdbError> { - #[cfg(test)] - if test_hooks::take(&test_hooks::FORCE_SUBSCRIBE_ERROR) { - return Err(RadrootsNostrNdbError::Ndb("forced subscribe error".into())); - } - self.inner.subscribe(filters).map_err(Into::into) - } - - fn unsubscribe_inner( - &self, - subscription: nostrdb::Subscription, - ) -> Result<(), RadrootsNostrNdbError> { - #[cfg(test)] - if test_hooks::take(&test_hooks::FORCE_UNSUBSCRIBE_ERROR) { - return Err(RadrootsNostrNdbError::Ndb( - "forced unsubscribe error".into(), - )); - } - let mut inner = self.inner.clone(); - inner.unsubscribe(subscription).map_err(Into::into) - } - - #[cfg(feature = "rt")] - async fn wait_for_notes_inner( - &self, - subscription: nostrdb::Subscription, - max_notes: u32, - ) -> Result<Vec<nostrdb::NoteKey>, RadrootsNostrNdbError> { - #[cfg(test)] - if test_hooks::take(&test_hooks::FORCE_WAIT_ERROR) { - return Err(RadrootsNostrNdbError::Ndb("forced wait error".into())); - } - self.inner - .wait_for_notes(subscription, max_notes) - .await - .map_err(Into::into) - } - - fn open_txn(&self) -> Result<nostrdb::Transaction, RadrootsNostrNdbError> { - #[cfg(test)] - if test_hooks::take(&test_hooks::FORCE_TRANSACTION_ERROR) { - return Err(RadrootsNostrNdbError::Ndb( - "forced transaction error".into(), - )); - } - nostrdb::Transaction::new(&self.inner).map_err(Into::into) - } - - fn query_inner<'a>( - &self, - txn: &'a nostrdb::Transaction, - filters: &[nostrdb::Filter], - max_results: i32, - ) -> Result<Vec<nostrdb::QueryResult<'a>>, RadrootsNostrNdbError> { - #[cfg(test)] - if test_hooks::take(&test_hooks::FORCE_QUERY_ERROR) { - return Err(RadrootsNostrNdbError::Ndb("forced query error".into())); - } - self.inner - .query(txn, filters, max_results) - .map_err(Into::into) - } - - fn note_json_value(note: &nostrdb::Note) -> Result<String, RadrootsNostrNdbError> { - #[cfg(test)] - if test_hooks::take(&test_hooks::FORCE_NOTE_JSON_ERROR) { - return Err(RadrootsNostrNdbError::Ndb("forced note json error".into())); - } - note.json().map_err(Into::into) - } - - fn get_profile_record<'a>( - &self, - txn: &'a nostrdb::Transaction, - pubkey: &[u8; 32], - ) -> Result<Option<nostrdb::ProfileRecord<'a>>, RadrootsNostrNdbError> { - #[cfg(test)] - if test_hooks::take(&test_hooks::FORCE_PROFILE_QUERY_ERROR) { - return map_profile_lookup_result(Err(nostrdb::Error::QueryError)); - } - map_profile_lookup_result(self.inner.get_profile_by_pubkey(txn, pubkey)) - } - - pub fn open(config: RadrootsNostrNdbConfig) -> Result<Self, RadrootsNostrNdbError> { - let mut inner_config = nostrdb::Config::new().skip_validation(config.skip_validation()); - if let Some(mapsize_bytes) = config.mapsize_bytes() { - inner_config = inner_config.set_mapsize(mapsize_bytes); - } - if let Some(ingester_threads) = config.ingester_threads() { - inner_config = inner_config.set_ingester_threads(ingester_threads); - } - - let db_dir = config.db_dir().to_path_buf(); - let db_dir_str = db_dir.to_str().ok_or(RadrootsNostrNdbError::NonUtf8Path)?; - let inner = nostrdb::Ndb::new(db_dir_str, &inner_config)?; - - Ok(Self { db_dir, inner }) - } - - pub fn db_dir(&self) -> &Path { - &self.db_dir - } - - pub fn ingest_event_json_with_source( - &self, - json: &str, - source: RadrootsNostrNdbIngestSource, - ) -> Result<(), RadrootsNostrNdbError> { - let metadata = source.to_ndb_metadata(); - self.process_event_with_inner(json, metadata)?; - Ok(()) - } - - pub fn ingest_event_json(&self, json: &str) -> Result<(), RadrootsNostrNdbError> { - self.ingest_event_json_with_source(json, RadrootsNostrNdbIngestSource::default()) - } - - pub fn ingest_event( - &self, - event: &RadrootsNostrEvent, - source: RadrootsNostrNdbIngestSource, - ) -> Result<(), RadrootsNostrNdbError> { - let json = Self::serialize_event(event)?; - self.ingest_event_json_with_source(json.as_str(), source) - } - - #[cfg(feature = "giftwrap")] - pub fn add_giftwrap_secret_key(&self, secret_key: [u8; 32]) -> bool { - self.inner.add_key(&secret_key) - } - - #[cfg(feature = "giftwrap")] - pub fn add_giftwrap_secret_key_hex( - &self, - secret_key_hex: &str, - ) -> Result<bool, RadrootsNostrNdbError> { - let secret_key = parse_hex_32(secret_key_hex, "secret_key")?; - Ok(self.add_giftwrap_secret_key(secret_key)) - } - - #[cfg(feature = "giftwrap")] - pub fn process_giftwraps(&self) -> Result<(), RadrootsNostrNdbError> { - let txn = nostrdb::Transaction::new(&self.inner)?; - self.inner.process_giftwraps(&txn); - Ok(()) - } - - pub fn subscribe( - &self, - spec: &RadrootsNostrNdbSubscriptionSpec, - ) -> Result<RadrootsNostrNdbSubscriptionHandle, RadrootsNostrNdbError> { - let filters = spec - .filters() - .iter() - .map(|filter_spec| filter_spec.to_ndb_filter()) - .collect::<Result<Vec<_>, _>>()?; - let subscription = self.subscribe_inner(filters.as_slice())?; - Ok(RadrootsNostrNdbSubscriptionHandle::new(subscription.id())) - } - - pub fn unsubscribe( - &self, - handle: RadrootsNostrNdbSubscriptionHandle, - ) -> Result<(), RadrootsNostrNdbError> { - let subscription = nostrdb::Subscription::new(handle.id()); - self.unsubscribe_inner(subscription)?; - Ok(()) - } - - pub fn poll_for_note_keys( - &self, - handle: RadrootsNostrNdbSubscriptionHandle, - max_notes: u32, - ) -> Vec<RadrootsNostrNdbNoteKey> { - self.inner - .poll_for_notes(nostrdb::Subscription::new(handle.id()), max_notes) - .into_iter() - .map(|note_key| RadrootsNostrNdbNoteKey::new(note_key.as_u64())) - .collect() - } - - #[cfg(feature = "rt")] - pub async fn wait_for_note_keys( - &self, - handle: RadrootsNostrNdbSubscriptionHandle, - max_notes: u32, - ) -> Result<Vec<RadrootsNostrNdbNoteKey>, RadrootsNostrNdbError> { - let note_keys = self - .wait_for_notes_inner(nostrdb::Subscription::new(handle.id()), max_notes) - .await?; - Ok(note_keys - .into_iter() - .map(|note_key| RadrootsNostrNdbNoteKey::new(note_key.as_u64())) - .collect()) - } - - #[cfg(feature = "rt")] - pub fn subscription_stream( - &self, - handle: RadrootsNostrNdbSubscriptionHandle, - notes_per_await: u32, - ) -> RadrootsNostrNdbSubscriptionStream { - let stream = nostrdb::Subscription::new(handle.id()) - .stream(&self.inner) - .notes_per_await(notes_per_await.max(1)); - RadrootsNostrNdbSubscriptionStream { inner: stream } - } - - pub fn query_notes( - &self, - spec: &RadrootsNostrNdbQuerySpec, - ) -> Result<Vec<RadrootsNostrNdbNote>, RadrootsNostrNdbError> { - if spec.filters().is_empty() { - return Ok(Vec::new()); - } - - let filters = spec - .filters() - .iter() - .map(|filter_spec| filter_spec.to_ndb_filter()) - .collect::<Result<Vec<_>, _>>()?; - let txn = self.open_txn()?; - let query_results = - self.query_inner(&txn, filters.as_slice(), spec.max_results() as i32)?; - - query_results - .into_iter() - .map(|query_result| { - let note = query_result.note; - let json = Self::note_json_value(&note)?; - Ok(RadrootsNostrNdbNote { - note_key: query_result.note_key.as_u64(), - id_hex: hex::encode(note.id()), - author_hex: hex::encode(note.pubkey()), - kind: note.kind(), - created_at_unix: note.created_at(), - content: note.content().to_owned(), - json, - }) - }) - .collect::<Result<Vec<_>, RadrootsNostrNdbError>>() - } - - pub fn get_profile_by_pubkey_hex( - &self, - pubkey_hex: &str, - ) -> Result<Option<RadrootsNostrNdbProfile>, RadrootsNostrNdbError> { - let pubkey = parse_hex_32(pubkey_hex, "pubkey")?; - let txn = self.open_txn()?; - let Some(profile_record) = self.get_profile_record(&txn, &pubkey)? else { - return Ok(None); - }; - - let profile = profile_record.record().profile(); - let profile_key = profile_record.key().map(|key| key.as_u64()); - Ok(profile.map(|profile| RadrootsNostrNdbProfile { - profile_key, - pubkey_hex: pubkey_hex.to_owned(), - name: profile.name().map(ToOwned::to_owned), - display_name: profile.display_name().map(ToOwned::to_owned), - about: profile.about().map(ToOwned::to_owned), - picture: profile.picture().map(ToOwned::to_owned), - banner: profile.banner().map(ToOwned::to_owned), - website: profile.website().map(ToOwned::to_owned), - nip05: profile.nip05().map(ToOwned::to_owned), - lud16: profile.lud16().map(ToOwned::to_owned), - })) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::filter::RadrootsNostrNdbFilterSpec; - use crate::ingest::RadrootsNostrNdbIngestSource; - use crate::query::RadrootsNostrNdbQuerySpec; - use crate::test_fixtures::{FIXTURE_ALICE_EMAIL, FIXTURE_ALICE_USERNAME}; - use futures::StreamExt; - use radroots_nostr::prelude::{RadrootsNostrEventBuilder, RadrootsNostrKeys}; - use radroots_nostr::prelude::{RadrootsNostrMetadata, radroots_nostr_build_metadata_event}; - use std::sync::atomic::Ordering; - use std::sync::{Mutex, OnceLock}; - use std::time::Duration; - use tempfile::TempDir; - - fn test_hooks_lock() -> &'static Mutex<()> { - static TEST_HOOKS_LOCK: OnceLock<Mutex<()>> = OnceLock::new(); - TEST_HOOKS_LOCK.get_or_init(|| Mutex::new(())) - } - - fn test_hooks_guard() -> std::sync::MutexGuard<'static, ()> { - test_hooks_lock().lock().expect("test hooks lock") - } - - fn reset_test_flags() { - test_hooks::FORCE_EVENT_JSON_ERROR.store(false, Ordering::SeqCst); - test_hooks::FORCE_PROCESS_EVENT_ERROR.store(false, Ordering::SeqCst); - test_hooks::FORCE_SUBSCRIBE_ERROR.store(false, Ordering::SeqCst); - test_hooks::FORCE_UNSUBSCRIBE_ERROR.store(false, Ordering::SeqCst); - test_hooks::FORCE_WAIT_ERROR.store(false, Ordering::SeqCst); - test_hooks::FORCE_TRANSACTION_ERROR.store(false, Ordering::SeqCst); - test_hooks::FORCE_QUERY_ERROR.store(false, Ordering::SeqCst); - test_hooks::FORCE_NOTE_JSON_ERROR.store(false, Ordering::SeqCst); - test_hooks::FORCE_PROFILE_QUERY_ERROR.store(false, Ordering::SeqCst); - } - - #[test] - fn config_builder_tracks_values() { - let config = RadrootsNostrNdbConfig::new("target/testdbs/nostr_ndb_config") - .with_mapsize_bytes(1024 * 1024) - .with_ingester_threads(2) - .with_skip_validation(true); - - assert_eq!(config.mapsize_bytes(), Some(1024 * 1024)); - assert_eq!(config.ingester_threads(), Some(2)); - assert!(config.skip_validation()); - } - - #[test] - fn map_profile_lookup_result_handles_all_error_kinds() { - let success = map_profile_lookup_result::<u64>(Ok(7)).expect("ok"); - assert_eq!(success, Some(7)); - - let not_found = - map_profile_lookup_result::<u64>(Err(nostrdb::Error::NotFound)).expect("none"); - assert!(not_found.is_none()); - - let query_error = map_profile_lookup_result::<u64>(Err(nostrdb::Error::QueryError)) - .expect_err("query error"); - assert!(query_error.to_string().starts_with("nostrdb error:")); - } - - #[test] - fn open_creates_database() { - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir) - .with_mapsize_bytes(64 * 1024 * 1024) - .with_ingester_threads(1); - - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - assert_eq!(ndb.db_dir(), db_dir.as_path()); - assert!(db_dir.exists()); - } - - #[cfg(unix)] - #[test] - fn open_rejects_non_utf8_path() { - use std::os::unix::ffi::OsStrExt; - - let path = std::path::PathBuf::from(std::ffi::OsStr::from_bytes(b"ndb-\xFF")); - let config = RadrootsNostrNdbConfig::new(&path); - let err = RadrootsNostrNdb::open(config).expect_err("non utf8 path"); - assert!(err.to_string().contains("utf-8")); - } - - #[test] - fn open_reports_ndb_error_for_file_path() { - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - std::fs::write(&db_dir, "not a directory").expect("write db file"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let err = RadrootsNostrNdb::open(config).expect_err("file path should fail"); - assert!(err.to_string().starts_with("nostrdb error:")); - } - - #[test] - fn ingest_source_builders_track_origin() { - assert_eq!( - RadrootsNostrNdbIngestSource::default(), - RadrootsNostrNdbIngestSource::client() - ); - assert_eq!( - RadrootsNostrNdbIngestSource::relay("wss://radroots.org"), - RadrootsNostrNdbIngestSource::Relay { - relay_url: Some("wss://radroots.org".into()) - } - ); - assert_eq!( - RadrootsNostrNdbIngestSource::relay_unknown(), - RadrootsNostrNdbIngestSource::Relay { relay_url: None } - ); - } - - #[test] - fn ingest_event_accepts_signed_note() { - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - let keys = RadrootsNostrKeys::generate(); - let event = RadrootsNostrEventBuilder::text_note("hello from ndb") - .sign_with_keys(&keys) - .expect("event should sign"); - - ndb.ingest_event(&event, RadrootsNostrNdbIngestSource::client()) - .expect("ingest should succeed"); - } - - #[test] - fn ingest_event_json_accepts_signed_note() { - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - let keys = RadrootsNostrKeys::generate(); - let event = RadrootsNostrEventBuilder::text_note("hello from ndb json") - .sign_with_keys(&keys) - .expect("event should sign"); - let json = serde_json::to_string(&event).expect("event json"); - - ndb.ingest_event_json(&json) - .expect("json ingest should succeed"); - } - - #[test] - fn ingest_event_json_rejects_invalid_json() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - test_hooks::FORCE_PROCESS_EVENT_ERROR.store(true, Ordering::SeqCst); - let err = ndb - .ingest_event_json_with_source("not json", RadrootsNostrNdbIngestSource::client()) - .expect_err("process event error"); - assert!(err.to_string().starts_with("nostrdb error:")); - } - - #[test] - fn ingest_event_reports_event_json_error() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - let keys = RadrootsNostrKeys::generate(); - let event = RadrootsNostrEventBuilder::text_note("forced json error") - .sign_with_keys(&keys) - .expect("event should sign"); - test_hooks::FORCE_EVENT_JSON_ERROR.store(true, Ordering::SeqCst); - - let err = ndb - .ingest_event(&event, RadrootsNostrNdbIngestSource::client()) - .expect_err("forced json error"); - assert!(err.to_string().starts_with("event json encode failed:")); - } - - #[test] - fn subscribe_poll_and_unsubscribe_round_trip() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - let spec = RadrootsNostrNdbSubscriptionSpec::single( - RadrootsNostrNdbFilterSpec::new() - .with_kind(1) - .with_limit(10), - ); - let handle = ndb.subscribe(&spec).expect("subscribe should succeed"); - - let keys = RadrootsNostrKeys::generate(); - let event = RadrootsNostrEventBuilder::text_note("subscription test") - .sign_with_keys(&keys) - .expect("event should sign"); - ndb.ingest_event(&event, RadrootsNostrNdbIngestSource::relay_unknown()) - .expect("ingest should succeed"); - - let mut notes = Vec::new(); - for _ in 0..40 { - notes = ndb.poll_for_note_keys(handle, 32); - if !notes.is_empty() { - break; - } - std::thread::sleep(Duration::from_millis(25)); - } - - assert!(!notes.is_empty()); - ndb.unsubscribe(handle).expect("unsubscribe should succeed"); - } - - #[test] - fn subscribe_reports_ndb_error() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - let spec = RadrootsNostrNdbSubscriptionSpec::text_notes(Some(10), None); - test_hooks::FORCE_SUBSCRIBE_ERROR.store(true, Ordering::SeqCst); - - let err = ndb.subscribe(&spec).expect_err("forced subscribe error"); - assert!(err.to_string().starts_with("nostrdb error:")); - } - - #[test] - fn unsubscribe_reports_ndb_error() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - let spec = RadrootsNostrNdbSubscriptionSpec::text_notes(Some(10), None); - let handle = ndb.subscribe(&spec).expect("subscribe should succeed"); - test_hooks::FORCE_UNSUBSCRIBE_ERROR.store(true, Ordering::SeqCst); - - let err = ndb - .unsubscribe(handle) - .expect_err("forced unsubscribe error"); - assert!(err.to_string().starts_with("nostrdb error:")); - } - - #[tokio::test] - #[allow(clippy::await_holding_lock)] - async fn wait_for_note_keys_yields_results() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - let spec = RadrootsNostrNdbSubscriptionSpec::text_notes(Some(10), None); - let handle = ndb.subscribe(&spec).expect("subscribe should succeed"); - - let keys = RadrootsNostrKeys::generate(); - let event = RadrootsNostrEventBuilder::text_note("wait test") - .sign_with_keys(&keys) - .expect("event should sign"); - ndb.ingest_event(&event, RadrootsNostrNdbIngestSource::relay_unknown()) - .expect("ingest should succeed"); - - let notes = ndb - .wait_for_note_keys(handle, 32) - .await - .expect("wait should succeed"); - assert!(!notes.is_empty()); - } - - #[tokio::test] - #[allow(clippy::await_holding_lock)] - async fn wait_for_note_keys_reports_ndb_error() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - let spec = RadrootsNostrNdbSubscriptionSpec::text_notes(Some(10), None); - let handle = ndb.subscribe(&spec).expect("subscribe should succeed"); - test_hooks::FORCE_WAIT_ERROR.store(true, Ordering::SeqCst); - - let err = ndb - .wait_for_note_keys(handle, 1) - .await - .expect_err("forced wait error"); - assert!(err.to_string().starts_with("nostrdb error:")); - } - - #[test] - fn query_notes_returns_ingested_results() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - let keys = RadrootsNostrKeys::generate(); - let event = RadrootsNostrEventBuilder::text_note("query note") - .sign_with_keys(&keys) - .expect("event should sign"); - ndb.ingest_event(&event, RadrootsNostrNdbIngestSource::client()) - .expect("ingest should succeed"); - - let query_spec = RadrootsNostrNdbQuerySpec::text_notes(Some(50), None, 50); - let mut notes = Vec::new(); - for _ in 0..40 { - notes = ndb.query_notes(&query_spec).expect("query should succeed"); - if !notes.is_empty() { - break; - } - std::thread::sleep(Duration::from_millis(25)); - } - assert!(!notes.is_empty()); - let note_pairs = notes - .iter() - .map(|note| (note.id_hex.clone(), note.content.clone())) - .collect::<Vec<_>>(); - assert!(note_pairs.contains(&(event.id.to_hex(), "query note".to_string()))); - } - - #[test] - fn query_notes_empty_filters_returns_empty() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - let query_spec = RadrootsNostrNdbQuerySpec::new(Vec::new(), 10); - let notes = ndb.query_notes(&query_spec).expect("query should succeed"); - assert!(notes.is_empty()); - } - - #[test] - fn query_notes_rejects_invalid_filters() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - let spec = RadrootsNostrNdbQuerySpec::single( - RadrootsNostrNdbFilterSpec::new().with_author_hex("not-hex"), - 10, - ); - let err = ndb.query_notes(&spec).expect_err("invalid filter"); - assert!(err.to_string().contains("invalid hex")); - } - - #[test] - fn query_notes_reports_transaction_error() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - let spec = RadrootsNostrNdbQuerySpec::text_notes(Some(10), None, 10); - test_hooks::FORCE_TRANSACTION_ERROR.store(true, Ordering::SeqCst); - - let err = ndb - .query_notes(&spec) - .expect_err("forced transaction error"); - assert!(err.to_string().starts_with("nostrdb error:")); - } - - #[test] - fn query_notes_reports_query_error() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - let spec = RadrootsNostrNdbQuerySpec::text_notes(Some(10), None, 10); - test_hooks::FORCE_QUERY_ERROR.store(true, Ordering::SeqCst); - - let err = ndb.query_notes(&spec).expect_err("forced query error"); - assert!(err.to_string().starts_with("nostrdb error:")); - } - - #[test] - fn query_notes_reports_note_json_error() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - let keys = RadrootsNostrKeys::generate(); - let event = RadrootsNostrEventBuilder::text_note("note json error") - .sign_with_keys(&keys) - .expect("event should sign"); - ndb.ingest_event(&event, RadrootsNostrNdbIngestSource::client()) - .expect("ingest should succeed"); - - let query_spec = RadrootsNostrNdbQuerySpec::text_notes(Some(50), None, 50); - for _ in 0..40 { - let notes = ndb.query_notes(&query_spec).expect("query should succeed"); - if !notes.is_empty() { - break; - } - std::thread::sleep(Duration::from_millis(25)); - } - test_hooks::FORCE_NOTE_JSON_ERROR.store(true, Ordering::SeqCst); - - let err = ndb - .query_notes(&query_spec) - .expect_err("forced note json error"); - assert!(err.to_string().starts_with("nostrdb error:")); - } - - #[test] - fn profile_lookup_returns_metadata_fields() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - let keys = RadrootsNostrKeys::generate(); - let pubkey_hex = keys.public_key().to_hex(); - let metadata = RadrootsNostrMetadata::new() - .name(FIXTURE_ALICE_USERNAME) - .display_name(FIXTURE_ALICE_USERNAME) - .about("coffee operator") - .lud16(FIXTURE_ALICE_EMAIL); - let metadata_event = radroots_nostr_build_metadata_event(&metadata) - .sign_with_keys(&keys) - .expect("metadata event should sign"); - ndb.ingest_event(&metadata_event, RadrootsNostrNdbIngestSource::client()) - .expect("ingest should succeed"); - - let mut profile = None; - for _ in 0..40 { - profile = ndb - .get_profile_by_pubkey_hex(pubkey_hex.as_str()) - .expect("profile lookup should succeed"); - if profile.is_some() { - break; - } - std::thread::sleep(Duration::from_millis(25)); - } - let profile = profile.expect("profile should exist"); - assert_eq!(profile.pubkey_hex, pubkey_hex); - assert_eq!(profile.name.as_deref(), Some(FIXTURE_ALICE_USERNAME)); - assert_eq!( - profile.display_name.as_deref(), - Some(FIXTURE_ALICE_USERNAME) - ); - assert_eq!(profile.lud16.as_deref(), Some(FIXTURE_ALICE_EMAIL)); - } - - #[test] - fn profile_lookup_returns_none_when_missing() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - let pubkey_hex = RadrootsNostrKeys::generate().public_key().to_hex(); - let profile = ndb - .get_profile_by_pubkey_hex(pubkey_hex.as_str()) - .expect("profile lookup"); - assert!(profile.is_none()); - } - - #[test] - fn profile_lookup_reports_query_error() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - let pubkey_hex = RadrootsNostrKeys::generate().public_key().to_hex(); - test_hooks::FORCE_PROFILE_QUERY_ERROR.store(true, Ordering::SeqCst); - - let err = ndb - .get_profile_by_pubkey_hex(pubkey_hex.as_str()) - .expect_err("forced profile query error"); - assert!(err.to_string().starts_with("nostrdb error:")); - } - - #[test] - fn profile_lookup_reports_transaction_error() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - let pubkey_hex = RadrootsNostrKeys::generate().public_key().to_hex(); - test_hooks::FORCE_TRANSACTION_ERROR.store(true, Ordering::SeqCst); - - let err = ndb - .get_profile_by_pubkey_hex(pubkey_hex.as_str()) - .expect_err("forced transaction error"); - assert!(err.to_string().starts_with("nostrdb error:")); - } - - #[test] - fn profile_lookup_returns_none_without_metadata_record() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - let keys = RadrootsNostrKeys::generate(); - let pubkey_hex = keys.public_key().to_hex(); - let event = RadrootsNostrEventBuilder::text_note("non profile event") - .sign_with_keys(&keys) - .expect("event should sign"); - ndb.ingest_event(&event, RadrootsNostrNdbIngestSource::client()) - .expect("ingest should succeed"); - - let profile = ndb - .get_profile_by_pubkey_hex(pubkey_hex.as_str()) - .expect("profile lookup"); - assert!(profile.is_none()); - } - - #[test] - fn profile_lookup_invalid_metadata_content_returns_none() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - let keys = RadrootsNostrKeys::generate(); - let pubkey_hex = keys.public_key().to_hex(); - let event = RadrootsNostrEventBuilder::new( - radroots_nostr::prelude::RadrootsNostrKind::Metadata, - "not valid metadata json", - ) - .sign_with_keys(&keys) - .expect("event should sign"); - ndb.ingest_event(&event, RadrootsNostrNdbIngestSource::client()) - .expect("ingest should succeed"); - - let result = ndb.get_profile_by_pubkey_hex(pubkey_hex.as_str()); - assert!(result.expect("profile lookup").is_none()); - } - - #[test] - fn subscribe_rejects_invalid_author_hex() { - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - let spec = RadrootsNostrNdbSubscriptionSpec::single( - RadrootsNostrNdbFilterSpec::new().with_author_hex("not-hex"), - ); - let err = ndb.subscribe(&spec).expect_err("subscribe should fail"); - assert!(err.to_string().contains("invalid hex for author")); - } - - #[test] - fn profile_lookup_rejects_invalid_pubkey_length() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - let err = ndb - .get_profile_by_pubkey_hex("abcd") - .expect_err("lookup should fail"); - assert!(err.to_string().contains("invalid hex length for pubkey")); - } - - #[tokio::test] - async fn subscription_stream_yields_events() { - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - let spec = RadrootsNostrNdbSubscriptionSpec::text_notes(Some(10), None); - let handle = ndb.subscribe(&spec).expect("subscribe should succeed"); - let mut stream = ndb.subscription_stream(handle, 0); - - let pending = tokio::time::timeout(Duration::from_millis(20), stream.next()).await; - assert!(pending.is_err()); - - let keys = RadrootsNostrKeys::generate(); - let event = RadrootsNostrEventBuilder::text_note("stream note") - .sign_with_keys(&keys) - .expect("event should sign"); - ndb.ingest_event(&event, RadrootsNostrNdbIngestSource::client()) - .expect("ingest should succeed"); - - let note_keys = tokio::time::timeout(Duration::from_secs(2), stream.next()) - .await - .expect("stream should wake") - .expect("stream should yield note keys"); - assert!(!note_keys.is_empty()); - assert!(note_keys.iter().all(|key| key.as_u64() > 0)); - } - - #[test] - fn concurrent_ingest_handles_parallel_writers() { - let _guard = test_hooks_guard(); - reset_test_flags(); - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - let worker_count = 4usize; - let notes_per_worker = 20usize; - let mut handles = Vec::new(); - - for worker in 0..worker_count { - let db = ndb.clone(); - handles.push(std::thread::spawn(move || { - let keys = RadrootsNostrKeys::generate(); - for idx in 0..notes_per_worker { - let content = format!("parallel-{worker}-{idx}"); - let event = RadrootsNostrEventBuilder::text_note(content.as_str()) - .sign_with_keys(&keys) - .expect("event should sign"); - db.ingest_event(&event, RadrootsNostrNdbIngestSource::client()) - .expect("ingest should succeed"); - } - })); - } - - for handle in handles { - handle.join().expect("worker should complete"); - } - - let query_spec = RadrootsNostrNdbQuerySpec::text_notes(Some(512), None, 512); - let expected = worker_count * notes_per_worker; - let mut observed = 0usize; - let mut break_threshold = expected + 1; - - for _ in 0..80 { - let notes = ndb.query_notes(&query_spec).expect("query should succeed"); - observed = notes - .iter() - .filter(|note| note.content.starts_with("parallel-")) - .count(); - if observed >= break_threshold { - break; - } - break_threshold = expected; - std::thread::sleep(Duration::from_millis(25)); - } - - assert!( - observed >= expected, - "expected at least {expected} parallel notes, got {observed}" - ); - } - - #[cfg(feature = "giftwrap")] - #[test] - fn giftwrap_secret_key_hex_validates_length() { - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - let result = ndb.add_giftwrap_secret_key_hex("abcd"); - let err = result.expect_err("invalid giftwrap key"); - assert!( - err.to_string() - .contains("invalid hex length for secret_key") - ); - } - - #[cfg(feature = "giftwrap")] - #[test] - fn giftwrap_process_flow_executes() { - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - - let secret_key = [7u8; 32]; - let _ = ndb.add_giftwrap_secret_key(secret_key); - ndb.process_giftwraps() - .expect("giftwrap processing should run"); - } -} diff --git a/crates/nostr_ndb/src/query.rs b/crates/nostr_ndb/src/query.rs @@ -1,60 +0,0 @@ -use crate::filter::RadrootsNostrNdbFilterSpec; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct RadrootsNostrNdbQuerySpec { - filters: Vec<RadrootsNostrNdbFilterSpec>, - max_results: u32, -} - -impl RadrootsNostrNdbQuerySpec { - pub fn new(filters: Vec<RadrootsNostrNdbFilterSpec>, max_results: u32) -> Self { - Self { - filters, - max_results: max_results.max(1), - } - } - - pub fn single(filter: RadrootsNostrNdbFilterSpec, max_results: u32) -> Self { - Self::new(vec![filter], max_results) - } - - pub fn text_notes(limit: Option<u64>, since_unix: Option<u64>, max_results: u32) -> Self { - Self::single( - RadrootsNostrNdbFilterSpec::text_notes(limit, since_unix), - max_results, - ) - } - - pub fn filters(&self) -> &[RadrootsNostrNdbFilterSpec] { - &self.filters - } - - pub fn max_results(&self) -> u32 { - self.max_results - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct RadrootsNostrNdbNote { - pub note_key: u64, - pub id_hex: String, - pub author_hex: String, - pub kind: u32, - pub created_at_unix: u64, - pub content: String, - pub json: String, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct RadrootsNostrNdbProfile { - pub profile_key: Option<u64>, - pub pubkey_hex: String, - pub name: Option<String>, - pub display_name: Option<String>, - pub about: Option<String>, - pub picture: Option<String>, - pub banner: Option<String>, - pub website: Option<String>, - pub nip05: Option<String>, - pub lud16: Option<String>, -} diff --git a/crates/nostr_ndb/src/runtime_adapter.rs b/crates/nostr_ndb/src/runtime_adapter.rs @@ -1,118 +0,0 @@ -use crate::ingest::RadrootsNostrNdbIngestSource; -use crate::ndb::RadrootsNostrNdb; -use radroots_nostr::prelude::RadrootsNostrEvent; -use radroots_nostr_runtime::prelude::RadrootsNostrEventSink; -use std::sync::Arc; - -#[derive(Clone)] -pub struct RadrootsNostrNdbEventSinkAdapter { - ndb: RadrootsNostrNdb, - source: RadrootsNostrNdbIngestSource, -} - -fn ndb_error_to_string(source: crate::error::RadrootsNostrNdbError) -> String { - source.to_string() -} - -impl RadrootsNostrNdbEventSinkAdapter { - pub fn new(ndb: RadrootsNostrNdb) -> Self { - Self { - ndb, - source: RadrootsNostrNdbIngestSource::client(), - } - } - - pub fn with_source(mut self, source: RadrootsNostrNdbIngestSource) -> Self { - self.source = source; - self - } - - pub fn into_event_sink(self) -> Arc<dyn RadrootsNostrEventSink> { - Arc::new(self) - } -} - -impl RadrootsNostrEventSink for RadrootsNostrNdb { - fn ingest_event(&self, event: &RadrootsNostrEvent) -> Result<(), String> { - RadrootsNostrNdb::ingest_event(self, event, RadrootsNostrNdbIngestSource::client()) - .map_err(ndb_error_to_string) - } -} - -impl RadrootsNostrEventSink for RadrootsNostrNdbEventSinkAdapter { - fn ingest_event(&self, event: &RadrootsNostrEvent) -> Result<(), String> { - self.ndb - .ingest_event(event, self.source.clone()) - .map_err(ndb_error_to_string) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::config::RadrootsNostrNdbConfig; - use radroots_nostr::prelude::{RadrootsNostrEventBuilder, RadrootsNostrKeys}; - use tempfile::TempDir; - - #[test] - fn runtime_adapter_accepts_signed_events() { - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - let adapter = RadrootsNostrNdbEventSinkAdapter::new(ndb); - - let keys = RadrootsNostrKeys::generate(); - let event = RadrootsNostrEventBuilder::text_note("hello from runtime adapter") - .sign_with_keys(&keys) - .expect("event should sign"); - - adapter - .ingest_event(&event) - .expect("adapter should ingest event"); - } - - #[test] - fn runtime_adapter_can_be_boxed_as_sink_trait() { - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - let sink = RadrootsNostrNdbEventSinkAdapter::new(ndb) - .with_source(RadrootsNostrNdbIngestSource::relay("wss://radroots.org")) - .into_event_sink(); - - let keys = RadrootsNostrKeys::generate(); - let event = RadrootsNostrEventBuilder::text_note("hello trait object") - .sign_with_keys(&keys) - .expect("event should sign"); - - sink.ingest_event(&event) - .expect("boxed sink should ingest event"); - } - - #[test] - fn ndb_can_be_boxed_as_sink_trait() { - let tmp_dir = TempDir::new().expect("tempdir should open"); - let db_dir = tmp_dir.path().join("ndb"); - let config = RadrootsNostrNdbConfig::new(&db_dir); - let ndb = RadrootsNostrNdb::open(config).expect("database should open"); - let sink: Arc<dyn RadrootsNostrEventSink> = Arc::new(ndb.clone()); - - let keys = RadrootsNostrKeys::generate(); - let event = RadrootsNostrEventBuilder::text_note("hello ndb trait object") - .sign_with_keys(&keys) - .expect("event should sign"); - - sink.ingest_event(&event) - .expect("ndb trait object should ingest event"); - } - - #[test] - fn runtime_adapter_error_to_string_converts() { - let rendered = ndb_error_to_string(crate::error::RadrootsNostrNdbError::Ndb( - "ndb error".to_string(), - )); - assert_eq!(rendered, "nostrdb error: ndb error"); - } -} diff --git a/crates/nostr_ndb/src/subscription.rs b/crates/nostr_ndb/src/subscription.rs @@ -1,106 +0,0 @@ -use crate::filter::RadrootsNostrNdbFilterSpec; - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] -pub struct RadrootsNostrNdbSubscriptionHandle { - id: u64, -} - -impl RadrootsNostrNdbSubscriptionHandle { - pub(crate) fn new(id: u64) -> Self { - Self { id } - } - - pub fn id(self) -> u64 { - self.id - } -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] -pub struct RadrootsNostrNdbNoteKey { - key: u64, -} - -impl RadrootsNostrNdbNoteKey { - pub(crate) fn new(key: u64) -> Self { - Self { key } - } - - pub fn as_u64(self) -> u64 { - self.key - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct RadrootsNostrNdbSubscriptionSpec { - filters: Vec<RadrootsNostrNdbFilterSpec>, -} - -impl RadrootsNostrNdbSubscriptionSpec { - pub fn new(filters: Vec<RadrootsNostrNdbFilterSpec>) -> Self { - Self { filters } - } - - pub fn single(filter: RadrootsNostrNdbFilterSpec) -> Self { - Self { - filters: vec![filter], - } - } - - pub fn text_notes(limit: Option<u64>, since_unix: Option<u64>) -> Self { - Self::single(RadrootsNostrNdbFilterSpec::text_notes(limit, since_unix)) - } - - pub fn filters(&self) -> &[RadrootsNostrNdbFilterSpec] { - &self.filters - } -} - -#[cfg(feature = "rt")] -pub struct RadrootsNostrNdbSubscriptionStream { - pub(crate) inner: nostrdb::SubscriptionStream, -} - -#[cfg(feature = "rt")] -impl futures::Stream for RadrootsNostrNdbSubscriptionStream { - type Item = Vec<RadrootsNostrNdbNoteKey>; - - fn poll_next( - mut self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll<Option<Self::Item>> { - std::pin::Pin::new(&mut self.inner) - .poll_next(cx) - .map(|note_keys| { - note_keys.map(|keys| { - keys.into_iter() - .map(|note_key| RadrootsNostrNdbNoteKey::new(note_key.as_u64())) - .collect() - }) - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::filter::RadrootsNostrNdbFilterSpec; - - #[test] - fn subscription_types_expose_builders_and_accessors() { - let handle = RadrootsNostrNdbSubscriptionHandle::new(42); - assert_eq!(handle.id(), 42); - - let note_key = RadrootsNostrNdbNoteKey::new(7); - assert_eq!(note_key.as_u64(), 7); - - let filter = RadrootsNostrNdbFilterSpec::new().with_kind(1); - let from_new = RadrootsNostrNdbSubscriptionSpec::new(vec![filter.clone()]); - assert_eq!(from_new.filters(), std::slice::from_ref(&filter)); - - let from_single = RadrootsNostrNdbSubscriptionSpec::single(filter.clone()); - assert_eq!(from_single.filters(), std::slice::from_ref(&filter)); - - let text_notes = RadrootsNostrNdbSubscriptionSpec::text_notes(Some(10), Some(123)); - assert_eq!(text_notes.filters().len(), 1); - } -} diff --git a/crates/nostr_runtime/Cargo.toml b/crates/nostr_runtime/Cargo.toml @@ -16,7 +16,7 @@ default = ["std", "nostr-client", "rt"] std = [] rt = ["std", "dep:futures", "dep:tokio"] nostr-client = ["std", "dep:radroots_nostr"] -nostr-ndb = ["nostr-client"] +nostrdb = ["nostr-client"] [dependencies] radroots_nostr = { workspace = true, optional = true, default-features = true, features = [ diff --git a/crates/nostrdb/Cargo.toml b/crates/nostrdb/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "radroots_nostrdb" +publish = false +version = "0.1.0-alpha.2" +edition.workspace = true +authors = ["Tyson Lupul <tyson@radroots.org>"] +rust-version.workspace = true +license.workspace = true +description = "NostrDB integration layer for Radroots" +repository.workspace = true +homepage.workspace = true +readme = "README" + +[features] +default = ["std", "rt", "nostrdb", "runtime-adapter"] +std = [] +rt = ["std", "dep:futures", "dep:tokio"] +nostrdb = ["std", "dep:nostrdb"] +runtime-adapter = ["std", "dep:radroots_nostr_runtime"] +giftwrap = ["std", "nostrdb"] + +[dependencies] +radroots_nostr = { workspace = true, default-features = false, features = [ + "std", +] } +radroots_nostr_runtime = { workspace = true, optional = true, default-features = false, features = [ + "std", + "rt", + "nostr-client", +] } +futures = { workspace = true, optional = true } +hex = { workspace = true } +nostrdb = { version = "0.9.0", optional = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, optional = true, features = ["rt", "sync", "time"] } + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] } +tempfile = { workspace = true } diff --git a/crates/nostrdb/README b/crates/nostrdb/README @@ -0,0 +1,23 @@ +# radroots_nostrdb + +This is the README for `radroots_nostrdb`, which provides nostrdb adapter +primitives for the `radroots` core libraries. + +## Overview + + * configuration, filter, ingest, query, and subscription types for + nostrdb-backed workflows; + * runtime-adapter hooks that let higher layers drive nostrdb as an event sink; + * feature-gated support for the `nostrdb` crate, runtime glue, and gift-wrap + handling; + * a `std`-based integration crate with optional async runtime support. + +## Copyright + +Except as otherwise noted, all files in the `radroots_nostrdb` distribution +are + + Copyright (c) 2025 Tyson Lupul + +For information on usage and redistribution, and for a DISCLAIMER OF ALL +WARRANTIES, see LICENSE included in the `radroots_nostrdb` distribution. diff --git a/crates/nostrdb/src/config.rs b/crates/nostrdb/src/config.rs @@ -0,0 +1,51 @@ +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct RadrootsNostrdbConfig { + db_dir: PathBuf, + mapsize_bytes: Option<usize>, + ingester_threads: Option<i32>, + skip_validation: bool, +} + +impl RadrootsNostrdbConfig { + pub fn new(db_dir: impl Into<PathBuf>) -> Self { + Self { + db_dir: db_dir.into(), + mapsize_bytes: None, + ingester_threads: None, + skip_validation: false, + } + } + + pub fn db_dir(&self) -> &Path { + &self.db_dir + } + + pub fn mapsize_bytes(&self) -> Option<usize> { + self.mapsize_bytes + } + + pub fn ingester_threads(&self) -> Option<i32> { + self.ingester_threads + } + + pub fn skip_validation(&self) -> bool { + self.skip_validation + } + + pub fn with_mapsize_bytes(mut self, bytes: usize) -> Self { + self.mapsize_bytes = Some(bytes); + self + } + + pub fn with_ingester_threads(mut self, threads: i32) -> Self { + self.ingester_threads = Some(threads); + self + } + + pub fn with_skip_validation(mut self, skip_validation: bool) -> Self { + self.skip_validation = skip_validation; + self + } +} diff --git a/crates/nostrdb/src/error.rs b/crates/nostrdb/src/error.rs @@ -0,0 +1,58 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum RadrootsNostrdbError { + #[error("database path must be utf-8")] + NonUtf8Path, + + #[error("invalid hex for {field}: {reason}")] + InvalidHex { field: &'static str, reason: String }, + + #[error("invalid hex length for {field}: expected {expected} bytes, got {actual}")] + InvalidHexLength { + field: &'static str, + expected: usize, + actual: usize, + }, + + #[error("event json encode failed: {0}")] + EventJsonEncode(String), + + #[error("nostrdb error: {0}")] + Nostrdb(String), +} + +#[cfg(feature = "nostrdb")] +impl From<nostrdb::Error> for RadrootsNostrdbError { + fn from(value: nostrdb::Error) -> Self { + Self::Nostrdb(value.to_string()) + } +} + +impl From<serde_json::Error> for RadrootsNostrdbError { + fn from(value: serde_json::Error) -> Self { + Self::EventJsonEncode(value.to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn converts_nostrdb_error() { + let converted: RadrootsNostrdbError = nostrdb::Error::NotFound.into(); + assert!(converted.to_string().starts_with("nostrdb error:")); + } + + #[test] + fn converts_serde_json_error() { + let source = serde_json::from_str::<serde_json::Value>("not json").expect_err("json error"); + let converted: RadrootsNostrdbError = source.into(); + assert!( + converted + .to_string() + .starts_with("event json encode failed:") + ); + } +} diff --git a/crates/nostrdb/src/filter.rs b/crates/nostrdb/src/filter.rs @@ -0,0 +1,272 @@ +use crate::error::RadrootsNostrdbError; + +#[derive(Debug, Clone, Eq, PartialEq, Default)] +pub struct RadrootsNostrdbFilterSpec { + event_ids_hex: Vec<String>, + authors_hex: Vec<String>, + kinds: Vec<u16>, + since_unix: Option<u64>, + until_unix: Option<u64>, + limit: Option<u64>, + search: Option<String>, +} + +impl RadrootsNostrdbFilterSpec { + pub fn new() -> Self { + Self::default() + } + + pub fn text_notes(limit: Option<u64>, since_unix: Option<u64>) -> Self { + let mut filter = Self::new().with_kind(1); + if let Some(limit) = limit { + filter = filter.with_limit(limit); + } + if let Some(since_unix) = since_unix { + filter = filter.with_since_unix(since_unix); + } + filter + } + + pub fn with_event_id_hex(mut self, id_hex: impl Into<String>) -> Self { + self.event_ids_hex.push(id_hex.into()); + self + } + + pub fn with_author_hex(mut self, author_hex: impl Into<String>) -> Self { + self.authors_hex.push(author_hex.into()); + self + } + + pub fn with_kind(mut self, kind: u16) -> Self { + self.kinds.push(kind); + self + } + + pub fn with_since_unix(mut self, since_unix: u64) -> Self { + self.since_unix = Some(since_unix); + self + } + + pub fn with_until_unix(mut self, until_unix: u64) -> Self { + self.until_unix = Some(until_unix); + self + } + + pub fn with_limit(mut self, limit: u64) -> Self { + self.limit = Some(limit); + self + } + + pub fn with_search(mut self, search: impl Into<String>) -> Self { + self.search = Some(search.into()); + self + } + + pub fn event_ids_hex(&self) -> &[String] { + &self.event_ids_hex + } + + pub fn authors_hex(&self) -> &[String] { + &self.authors_hex + } + + pub fn kinds(&self) -> &[u16] { + &self.kinds + } + + pub fn since_unix(&self) -> Option<u64> { + self.since_unix + } + + pub fn until_unix(&self) -> Option<u64> { + self.until_unix + } + + pub fn limit(&self) -> Option<u64> { + self.limit + } + + pub fn search(&self) -> Option<&str> { + self.search.as_deref() + } + + pub(crate) fn to_nostrdb_filter(&self) -> Result<nostrdb::Filter, RadrootsNostrdbError> { + let mut builder = nostrdb::Filter::new(); + + if !self.event_ids_hex.is_empty() { + let event_ids = self + .event_ids_hex + .iter() + .map(|hex_value| parse_hex_32(hex_value, "event_id")) + .collect::<Result<Vec<_>, _>>()?; + builder = builder.ids(event_ids.iter()); + } + + if !self.authors_hex.is_empty() { + let authors = self + .authors_hex + .iter() + .map(|hex_value| parse_hex_32(hex_value, "author")) + .collect::<Result<Vec<_>, _>>()?; + builder = builder.authors(authors.iter()); + } + + if !self.kinds.is_empty() { + builder = builder.kinds(self.kinds.iter().map(|kind| *kind as u64)); + } + + if let Some(since_unix) = self.since_unix { + builder = builder.since(since_unix); + } + + if let Some(until_unix) = self.until_unix { + builder = builder.until(until_unix); + } + + if let Some(limit) = self.limit { + builder = builder.limit(limit); + } + + if let Some(search) = self.search() { + builder = builder.search(search); + } + + Ok(builder.build()) + } +} + +pub(crate) fn parse_hex_32( + value: &str, + field: &'static str, +) -> Result<[u8; 32], RadrootsNostrdbError> { + let bytes = hex::decode(value).map_err(|source| RadrootsNostrdbError::InvalidHex { + field, + reason: source.to_string(), + })?; + + if bytes.len() != 32 { + return Err(RadrootsNostrdbError::InvalidHexLength { + field, + expected: 32, + actual: bytes.len(), + }); + } + + let mut out = [0u8; 32]; + out.copy_from_slice(bytes.as_slice()); + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn valid_hex_32(value: u8) -> String { + format!("{value:02x}").repeat(32) + } + + #[test] + fn filter_spec_builders_and_accessors_round_trip() { + let event_id = valid_hex_32(0x11); + let author = valid_hex_32(0x22); + + let empty_notes = RadrootsNostrdbFilterSpec::text_notes(None, None); + assert_eq!(empty_notes.kinds(), &[1]); + assert_eq!(empty_notes.limit(), None); + assert_eq!(empty_notes.since_unix(), None); + + let spec = RadrootsNostrdbFilterSpec::text_notes(Some(50), Some(100)) + .with_event_id_hex(event_id.clone()) + .with_author_hex(author.clone()) + .with_kind(30023) + .with_since_unix(200) + .with_until_unix(300) + .with_limit(10) + .with_search("coffee"); + + assert_eq!(spec.event_ids_hex(), std::slice::from_ref(&event_id)); + assert_eq!(spec.authors_hex(), std::slice::from_ref(&author)); + assert_eq!(spec.kinds(), &[1, 30023]); + assert_eq!(spec.since_unix(), Some(200)); + assert_eq!(spec.until_unix(), Some(300)); + assert_eq!(spec.limit(), Some(10)); + assert_eq!(spec.search(), Some("coffee")); + + let empty = RadrootsNostrdbFilterSpec::new(); + let _ = empty.to_nostrdb_filter().expect("empty nostrdb filter"); + } + + #[test] + fn to_nostrdb_filter_builds_supported_success_paths() { + let event_id = valid_hex_32(0x11); + let author = valid_hex_32(0x22); + + let _ = RadrootsNostrdbFilterSpec::new() + .with_event_id_hex(event_id) + .to_nostrdb_filter() + .expect("event id filter"); + + let _ = RadrootsNostrdbFilterSpec::new() + .with_author_hex(author) + .to_nostrdb_filter() + .expect("author filter"); + + let _ = RadrootsNostrdbFilterSpec::new() + .with_kind(1) + .with_since_unix(200) + .with_until_unix(300) + .with_limit(10) + .to_nostrdb_filter() + .expect("range filter"); + + let _ = RadrootsNostrdbFilterSpec::new() + .with_search("coffee") + .to_nostrdb_filter() + .expect("search filter"); + } + + #[test] + fn parse_hex_32_validates_input() { + let valid = parse_hex_32(valid_hex_32(0xab).as_str(), "value").expect("valid"); + assert_eq!(valid, [0xab; 32]); + + let invalid_hex = parse_hex_32("zz", "value"); + assert!(matches!( + invalid_hex, + Err(RadrootsNostrdbError::InvalidHex { field: "value", .. }) + )); + + let invalid_len = parse_hex_32("abcd", "value"); + assert!(matches!( + invalid_len, + Err(RadrootsNostrdbError::InvalidHexLength { + field: "value", + expected: 32, + .. + }) + )); + } + + #[test] + fn to_nostrdb_filter_rejects_invalid_event_id_and_author_hex() { + let bad_event_id = RadrootsNostrdbFilterSpec::new().with_event_id_hex("not-hex"); + let bad_event_result = bad_event_id.to_nostrdb_filter(); + assert!(matches!( + bad_event_result, + Err(RadrootsNostrdbError::InvalidHex { + field: "event_id", + .. + }) + )); + + let bad_author = RadrootsNostrdbFilterSpec::new().with_author_hex("not-hex"); + let bad_author_result = bad_author.to_nostrdb_filter(); + assert!(matches!( + bad_author_result, + Err(RadrootsNostrdbError::InvalidHex { + field: "author", + .. + }) + )); + } +} diff --git a/crates/nostrdb/src/ingest.rs b/crates/nostrdb/src/ingest.rs @@ -0,0 +1,38 @@ +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub enum RadrootsNostrdbIngestSource { + #[default] + Client, + Relay { + relay_url: Option<String>, + }, +} + +impl RadrootsNostrdbIngestSource { + pub fn client() -> Self { + Self::Client + } + + pub fn relay(relay_url: impl Into<String>) -> Self { + Self::Relay { + relay_url: Some(relay_url.into()), + } + } + + pub fn relay_unknown() -> Self { + Self::Relay { relay_url: None } + } + + pub(crate) fn to_nostrdb_metadata(&self) -> nostrdb::IngestMetadata { + match self { + Self::Client => nostrdb::IngestMetadata::new().client(true), + Self::Relay { relay_url } => { + let meta = nostrdb::IngestMetadata::new().client(false); + if let Some(relay_url) = relay_url { + meta.relay(relay_url.as_str()) + } else { + meta + } + } + } + } +} diff --git a/crates/nostrdb/src/lib.rs b/crates/nostrdb/src/lib.rs @@ -0,0 +1,48 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![forbid(unsafe_code)] + +#[cfg(not(feature = "std"))] +compile_error!("radroots_nostrdb requires the std feature"); + +extern crate alloc; + +#[cfg(test)] +mod test_fixtures; + +#[cfg(feature = "nostrdb")] +pub mod config; +pub mod error; +#[cfg(feature = "nostrdb")] +pub mod filter; +#[cfg(feature = "nostrdb")] +pub mod ingest; +#[cfg(feature = "nostrdb")] +pub mod nostrdb; +#[cfg(feature = "nostrdb")] +pub mod query; +#[cfg(all(feature = "nostrdb", feature = "runtime-adapter"))] +pub mod runtime_adapter; +#[cfg(feature = "nostrdb")] +pub mod subscription; + +pub mod prelude { + #[cfg(feature = "nostrdb")] + pub use crate::config::RadrootsNostrdbConfig; + pub use crate::error::RadrootsNostrdbError; + #[cfg(feature = "nostrdb")] + pub use crate::filter::RadrootsNostrdbFilterSpec; + #[cfg(feature = "nostrdb")] + pub use crate::ingest::RadrootsNostrdbIngestSource; + #[cfg(feature = "nostrdb")] + pub use crate::nostrdb::RadrootsNostrdb; + #[cfg(feature = "nostrdb")] + pub use crate::query::{RadrootsNostrdbNote, RadrootsNostrdbProfile, RadrootsNostrdbQuerySpec}; + #[cfg(all(feature = "nostrdb", feature = "runtime-adapter"))] + pub use crate::runtime_adapter::RadrootsNostrdbEventSinkAdapter; + #[cfg(all(feature = "nostrdb", feature = "rt"))] + pub use crate::subscription::RadrootsNostrdbSubscriptionStream; + #[cfg(feature = "nostrdb")] + pub use crate::subscription::{ + RadrootsNostrdbNoteKey, RadrootsNostrdbSubscriptionHandle, RadrootsNostrdbSubscriptionSpec, + }; +} diff --git a/crates/nostrdb/src/nostrdb.rs b/crates/nostrdb/src/nostrdb.rs @@ -0,0 +1,1080 @@ +use crate::config::RadrootsNostrdbConfig; +use crate::error::RadrootsNostrdbError; +use crate::filter::parse_hex_32; +use crate::ingest::RadrootsNostrdbIngestSource; +use crate::query::{RadrootsNostrdbNote, RadrootsNostrdbProfile, RadrootsNostrdbQuerySpec}; +use crate::subscription::{ + RadrootsNostrdbNoteKey, RadrootsNostrdbSubscriptionHandle, RadrootsNostrdbSubscriptionSpec, + RadrootsNostrdbSubscriptionStream, +}; +use radroots_nostr::prelude::RadrootsNostrEvent; +use std::path::Path; + +#[derive(Debug, Clone)] +pub struct RadrootsNostrdb { + db_dir: std::path::PathBuf, + pub(crate) inner: nostrdb::Ndb, +} + +#[cfg(test)] +mod test_hooks { + use std::sync::atomic::{AtomicBool, Ordering}; + + pub static FORCE_EVENT_JSON_ERROR: AtomicBool = AtomicBool::new(false); + pub static FORCE_PROCESS_EVENT_ERROR: AtomicBool = AtomicBool::new(false); + pub static FORCE_SUBSCRIBE_ERROR: AtomicBool = AtomicBool::new(false); + pub static FORCE_UNSUBSCRIBE_ERROR: AtomicBool = AtomicBool::new(false); + pub static FORCE_WAIT_ERROR: AtomicBool = AtomicBool::new(false); + pub static FORCE_TRANSACTION_ERROR: AtomicBool = AtomicBool::new(false); + pub static FORCE_QUERY_ERROR: AtomicBool = AtomicBool::new(false); + pub static FORCE_NOTE_JSON_ERROR: AtomicBool = AtomicBool::new(false); + pub static FORCE_PROFILE_QUERY_ERROR: AtomicBool = AtomicBool::new(false); + + pub fn take(flag: &AtomicBool) -> bool { + flag.swap(false, Ordering::SeqCst) + } +} + +fn map_profile_lookup_result<T>( + result: Result<T, nostrdb::Error>, +) -> Result<Option<T>, RadrootsNostrdbError> { + match result { + Ok(value) => Ok(Some(value)), + Err(nostrdb::Error::NotFound) => Ok(None), + Err(source) => Err(source.into()), + } +} + +impl RadrootsNostrdb { + fn serialize_event(event: &RadrootsNostrEvent) -> Result<String, RadrootsNostrdbError> { + #[cfg(test)] + if test_hooks::take(&test_hooks::FORCE_EVENT_JSON_ERROR) { + return Err(RadrootsNostrdbError::EventJsonEncode( + "forced event json error".into(), + )); + } + serde_json::to_string(event).map_err(Into::into) + } + + fn process_event_with_inner( + &self, + json: &str, + metadata: nostrdb::IngestMetadata, + ) -> Result<(), RadrootsNostrdbError> { + #[cfg(test)] + if test_hooks::take(&test_hooks::FORCE_PROCESS_EVENT_ERROR) { + return Err(RadrootsNostrdbError::Nostrdb( + "forced process event error".into(), + )); + } + self.inner + .process_event_with(json, metadata) + .map_err(Into::into) + } + + fn subscribe_inner( + &self, + filters: &[nostrdb::Filter], + ) -> Result<nostrdb::Subscription, RadrootsNostrdbError> { + #[cfg(test)] + if test_hooks::take(&test_hooks::FORCE_SUBSCRIBE_ERROR) { + return Err(RadrootsNostrdbError::Nostrdb( + "forced subscribe error".into(), + )); + } + self.inner.subscribe(filters).map_err(Into::into) + } + + fn unsubscribe_inner( + &self, + subscription: nostrdb::Subscription, + ) -> Result<(), RadrootsNostrdbError> { + #[cfg(test)] + if test_hooks::take(&test_hooks::FORCE_UNSUBSCRIBE_ERROR) { + return Err(RadrootsNostrdbError::Nostrdb( + "forced unsubscribe error".into(), + )); + } + let mut inner = self.inner.clone(); + inner.unsubscribe(subscription).map_err(Into::into) + } + + #[cfg(feature = "rt")] + async fn wait_for_notes_inner( + &self, + subscription: nostrdb::Subscription, + max_notes: u32, + ) -> Result<Vec<nostrdb::NoteKey>, RadrootsNostrdbError> { + #[cfg(test)] + if test_hooks::take(&test_hooks::FORCE_WAIT_ERROR) { + return Err(RadrootsNostrdbError::Nostrdb("forced wait error".into())); + } + self.inner + .wait_for_notes(subscription, max_notes) + .await + .map_err(Into::into) + } + + fn open_txn(&self) -> Result<nostrdb::Transaction, RadrootsNostrdbError> { + #[cfg(test)] + if test_hooks::take(&test_hooks::FORCE_TRANSACTION_ERROR) { + return Err(RadrootsNostrdbError::Nostrdb( + "forced transaction error".into(), + )); + } + nostrdb::Transaction::new(&self.inner).map_err(Into::into) + } + + fn query_inner<'a>( + &self, + txn: &'a nostrdb::Transaction, + filters: &[nostrdb::Filter], + max_results: i32, + ) -> Result<Vec<nostrdb::QueryResult<'a>>, RadrootsNostrdbError> { + #[cfg(test)] + if test_hooks::take(&test_hooks::FORCE_QUERY_ERROR) { + return Err(RadrootsNostrdbError::Nostrdb("forced query error".into())); + } + self.inner + .query(txn, filters, max_results) + .map_err(Into::into) + } + + fn note_json_value(note: &nostrdb::Note) -> Result<String, RadrootsNostrdbError> { + #[cfg(test)] + if test_hooks::take(&test_hooks::FORCE_NOTE_JSON_ERROR) { + return Err(RadrootsNostrdbError::Nostrdb( + "forced note json error".into(), + )); + } + note.json().map_err(Into::into) + } + + fn get_profile_record<'a>( + &self, + txn: &'a nostrdb::Transaction, + pubkey: &[u8; 32], + ) -> Result<Option<nostrdb::ProfileRecord<'a>>, RadrootsNostrdbError> { + #[cfg(test)] + if test_hooks::take(&test_hooks::FORCE_PROFILE_QUERY_ERROR) { + return map_profile_lookup_result(Err(nostrdb::Error::QueryError)); + } + map_profile_lookup_result(self.inner.get_profile_by_pubkey(txn, pubkey)) + } + + pub fn open(config: RadrootsNostrdbConfig) -> Result<Self, RadrootsNostrdbError> { + let mut inner_config = nostrdb::Config::new().skip_validation(config.skip_validation()); + if let Some(mapsize_bytes) = config.mapsize_bytes() { + inner_config = inner_config.set_mapsize(mapsize_bytes); + } + if let Some(ingester_threads) = config.ingester_threads() { + inner_config = inner_config.set_ingester_threads(ingester_threads); + } + + let db_dir = config.db_dir().to_path_buf(); + let db_dir_str = db_dir.to_str().ok_or(RadrootsNostrdbError::NonUtf8Path)?; + let inner = nostrdb::Ndb::new(db_dir_str, &inner_config)?; + + Ok(Self { db_dir, inner }) + } + + pub fn db_dir(&self) -> &Path { + &self.db_dir + } + + pub fn ingest_event_json_with_source( + &self, + json: &str, + source: RadrootsNostrdbIngestSource, + ) -> Result<(), RadrootsNostrdbError> { + let metadata = source.to_nostrdb_metadata(); + self.process_event_with_inner(json, metadata)?; + Ok(()) + } + + pub fn ingest_event_json(&self, json: &str) -> Result<(), RadrootsNostrdbError> { + self.ingest_event_json_with_source(json, RadrootsNostrdbIngestSource::default()) + } + + pub fn ingest_event( + &self, + event: &RadrootsNostrEvent, + source: RadrootsNostrdbIngestSource, + ) -> Result<(), RadrootsNostrdbError> { + let json = Self::serialize_event(event)?; + self.ingest_event_json_with_source(json.as_str(), source) + } + + #[cfg(feature = "giftwrap")] + pub fn add_giftwrap_secret_key(&self, secret_key: [u8; 32]) -> bool { + self.inner.add_key(&secret_key) + } + + #[cfg(feature = "giftwrap")] + pub fn add_giftwrap_secret_key_hex( + &self, + secret_key_hex: &str, + ) -> Result<bool, RadrootsNostrdbError> { + let secret_key = parse_hex_32(secret_key_hex, "secret_key")?; + Ok(self.add_giftwrap_secret_key(secret_key)) + } + + #[cfg(feature = "giftwrap")] + pub fn process_giftwraps(&self) -> Result<(), RadrootsNostrdbError> { + let txn = nostrdb::Transaction::new(&self.inner)?; + self.inner.process_giftwraps(&txn); + Ok(()) + } + + pub fn subscribe( + &self, + spec: &RadrootsNostrdbSubscriptionSpec, + ) -> Result<RadrootsNostrdbSubscriptionHandle, RadrootsNostrdbError> { + let filters = spec + .filters() + .iter() + .map(|filter_spec| filter_spec.to_nostrdb_filter()) + .collect::<Result<Vec<_>, _>>()?; + let subscription = self.subscribe_inner(filters.as_slice())?; + Ok(RadrootsNostrdbSubscriptionHandle::new(subscription.id())) + } + + pub fn unsubscribe( + &self, + handle: RadrootsNostrdbSubscriptionHandle, + ) -> Result<(), RadrootsNostrdbError> { + let subscription = nostrdb::Subscription::new(handle.id()); + self.unsubscribe_inner(subscription)?; + Ok(()) + } + + pub fn poll_for_note_keys( + &self, + handle: RadrootsNostrdbSubscriptionHandle, + max_notes: u32, + ) -> Vec<RadrootsNostrdbNoteKey> { + self.inner + .poll_for_notes(nostrdb::Subscription::new(handle.id()), max_notes) + .into_iter() + .map(|note_key| RadrootsNostrdbNoteKey::new(note_key.as_u64())) + .collect() + } + + #[cfg(feature = "rt")] + pub async fn wait_for_note_keys( + &self, + handle: RadrootsNostrdbSubscriptionHandle, + max_notes: u32, + ) -> Result<Vec<RadrootsNostrdbNoteKey>, RadrootsNostrdbError> { + let note_keys = self + .wait_for_notes_inner(nostrdb::Subscription::new(handle.id()), max_notes) + .await?; + Ok(note_keys + .into_iter() + .map(|note_key| RadrootsNostrdbNoteKey::new(note_key.as_u64())) + .collect()) + } + + #[cfg(feature = "rt")] + pub fn subscription_stream( + &self, + handle: RadrootsNostrdbSubscriptionHandle, + notes_per_await: u32, + ) -> RadrootsNostrdbSubscriptionStream { + let stream = nostrdb::Subscription::new(handle.id()) + .stream(&self.inner) + .notes_per_await(notes_per_await.max(1)); + RadrootsNostrdbSubscriptionStream { inner: stream } + } + + pub fn query_notes( + &self, + spec: &RadrootsNostrdbQuerySpec, + ) -> Result<Vec<RadrootsNostrdbNote>, RadrootsNostrdbError> { + if spec.filters().is_empty() { + return Ok(Vec::new()); + } + + let filters = spec + .filters() + .iter() + .map(|filter_spec| filter_spec.to_nostrdb_filter()) + .collect::<Result<Vec<_>, _>>()?; + let txn = self.open_txn()?; + let query_results = + self.query_inner(&txn, filters.as_slice(), spec.max_results() as i32)?; + + query_results + .into_iter() + .map(|query_result| { + let note = query_result.note; + let json = Self::note_json_value(&note)?; + Ok(RadrootsNostrdbNote { + note_key: query_result.note_key.as_u64(), + id_hex: hex::encode(note.id()), + author_hex: hex::encode(note.pubkey()), + kind: note.kind(), + created_at_unix: note.created_at(), + content: note.content().to_owned(), + json, + }) + }) + .collect::<Result<Vec<_>, RadrootsNostrdbError>>() + } + + pub fn get_profile_by_pubkey_hex( + &self, + pubkey_hex: &str, + ) -> Result<Option<RadrootsNostrdbProfile>, RadrootsNostrdbError> { + let pubkey = parse_hex_32(pubkey_hex, "pubkey")?; + let txn = self.open_txn()?; + let Some(profile_record) = self.get_profile_record(&txn, &pubkey)? else { + return Ok(None); + }; + + let profile = profile_record.record().profile(); + let profile_key = profile_record.key().map(|key| key.as_u64()); + Ok(profile.map(|profile| RadrootsNostrdbProfile { + profile_key, + pubkey_hex: pubkey_hex.to_owned(), + name: profile.name().map(ToOwned::to_owned), + display_name: profile.display_name().map(ToOwned::to_owned), + about: profile.about().map(ToOwned::to_owned), + picture: profile.picture().map(ToOwned::to_owned), + banner: profile.banner().map(ToOwned::to_owned), + website: profile.website().map(ToOwned::to_owned), + nip05: profile.nip05().map(ToOwned::to_owned), + lud16: profile.lud16().map(ToOwned::to_owned), + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::filter::RadrootsNostrdbFilterSpec; + use crate::ingest::RadrootsNostrdbIngestSource; + use crate::query::RadrootsNostrdbQuerySpec; + use crate::test_fixtures::{FIXTURE_ALICE_EMAIL, FIXTURE_ALICE_USERNAME}; + use futures::StreamExt; + use radroots_nostr::prelude::{RadrootsNostrEventBuilder, RadrootsNostrKeys}; + use radroots_nostr::prelude::{RadrootsNostrMetadata, radroots_nostr_build_metadata_event}; + use std::sync::atomic::Ordering; + use std::sync::{Mutex, OnceLock}; + use std::time::Duration; + use tempfile::TempDir; + + fn test_hooks_lock() -> &'static Mutex<()> { + static TEST_HOOKS_LOCK: OnceLock<Mutex<()>> = OnceLock::new(); + TEST_HOOKS_LOCK.get_or_init(|| Mutex::new(())) + } + + fn test_hooks_guard() -> std::sync::MutexGuard<'static, ()> { + test_hooks_lock().lock().expect("test hooks lock") + } + + fn reset_test_flags() { + test_hooks::FORCE_EVENT_JSON_ERROR.store(false, Ordering::SeqCst); + test_hooks::FORCE_PROCESS_EVENT_ERROR.store(false, Ordering::SeqCst); + test_hooks::FORCE_SUBSCRIBE_ERROR.store(false, Ordering::SeqCst); + test_hooks::FORCE_UNSUBSCRIBE_ERROR.store(false, Ordering::SeqCst); + test_hooks::FORCE_WAIT_ERROR.store(false, Ordering::SeqCst); + test_hooks::FORCE_TRANSACTION_ERROR.store(false, Ordering::SeqCst); + test_hooks::FORCE_QUERY_ERROR.store(false, Ordering::SeqCst); + test_hooks::FORCE_NOTE_JSON_ERROR.store(false, Ordering::SeqCst); + test_hooks::FORCE_PROFILE_QUERY_ERROR.store(false, Ordering::SeqCst); + } + + #[test] + fn config_builder_tracks_values() { + let config = RadrootsNostrdbConfig::new("target/testdbs/nostrdb_config") + .with_mapsize_bytes(1024 * 1024) + .with_ingester_threads(2) + .with_skip_validation(true); + + assert_eq!(config.mapsize_bytes(), Some(1024 * 1024)); + assert_eq!(config.ingester_threads(), Some(2)); + assert!(config.skip_validation()); + } + + #[test] + fn map_profile_lookup_result_handles_all_error_kinds() { + let success = map_profile_lookup_result::<u64>(Ok(7)).expect("ok"); + assert_eq!(success, Some(7)); + + let not_found = + map_profile_lookup_result::<u64>(Err(nostrdb::Error::NotFound)).expect("none"); + assert!(not_found.is_none()); + + let query_error = map_profile_lookup_result::<u64>(Err(nostrdb::Error::QueryError)) + .expect_err("query error"); + assert!(query_error.to_string().starts_with("nostrdb error:")); + } + + #[test] + fn open_creates_database() { + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir) + .with_mapsize_bytes(64 * 1024 * 1024) + .with_ingester_threads(1); + + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + assert_eq!(nostrdb.db_dir(), db_dir.as_path()); + assert!(db_dir.exists()); + } + + #[cfg(unix)] + #[test] + fn open_rejects_non_utf8_path() { + use std::os::unix::ffi::OsStrExt; + + let path = std::path::PathBuf::from(std::ffi::OsStr::from_bytes(b"nostrdb-\xFF")); + let config = RadrootsNostrdbConfig::new(&path); + let err = RadrootsNostrdb::open(config).expect_err("non utf8 path"); + assert!(err.to_string().contains("utf-8")); + } + + #[test] + fn open_reports_nostrdb_error_for_file_path() { + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + std::fs::write(&db_dir, "not a directory").expect("write db file"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let err = RadrootsNostrdb::open(config).expect_err("file path should fail"); + assert!(err.to_string().starts_with("nostrdb error:")); + } + + #[test] + fn ingest_source_builders_track_origin() { + assert_eq!( + RadrootsNostrdbIngestSource::default(), + RadrootsNostrdbIngestSource::client() + ); + assert_eq!( + RadrootsNostrdbIngestSource::relay("wss://radroots.org"), + RadrootsNostrdbIngestSource::Relay { + relay_url: Some("wss://radroots.org".into()) + } + ); + assert_eq!( + RadrootsNostrdbIngestSource::relay_unknown(), + RadrootsNostrdbIngestSource::Relay { relay_url: None } + ); + } + + #[test] + fn ingest_event_accepts_signed_note() { + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + let keys = RadrootsNostrKeys::generate(); + let event = RadrootsNostrEventBuilder::text_note("hello from nostrdb") + .sign_with_keys(&keys) + .expect("event should sign"); + + nostrdb + .ingest_event(&event, RadrootsNostrdbIngestSource::client()) + .expect("ingest should succeed"); + } + + #[test] + fn ingest_event_json_accepts_signed_note() { + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + let keys = RadrootsNostrKeys::generate(); + let event = RadrootsNostrEventBuilder::text_note("hello from nostrdb json") + .sign_with_keys(&keys) + .expect("event should sign"); + let json = serde_json::to_string(&event).expect("event json"); + + nostrdb + .ingest_event_json(&json) + .expect("json ingest should succeed"); + } + + #[test] + fn ingest_event_json_rejects_invalid_json() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + test_hooks::FORCE_PROCESS_EVENT_ERROR.store(true, Ordering::SeqCst); + let err = nostrdb + .ingest_event_json_with_source("not json", RadrootsNostrdbIngestSource::client()) + .expect_err("process event error"); + assert!(err.to_string().starts_with("nostrdb error:")); + } + + #[test] + fn ingest_event_reports_event_json_error() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + let keys = RadrootsNostrKeys::generate(); + let event = RadrootsNostrEventBuilder::text_note("forced json error") + .sign_with_keys(&keys) + .expect("event should sign"); + test_hooks::FORCE_EVENT_JSON_ERROR.store(true, Ordering::SeqCst); + + let err = nostrdb + .ingest_event(&event, RadrootsNostrdbIngestSource::client()) + .expect_err("forced json error"); + assert!(err.to_string().starts_with("event json encode failed:")); + } + + #[test] + fn subscribe_poll_and_unsubscribe_round_trip() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + let spec = RadrootsNostrdbSubscriptionSpec::single( + RadrootsNostrdbFilterSpec::new().with_kind(1).with_limit(10), + ); + let handle = nostrdb.subscribe(&spec).expect("subscribe should succeed"); + + let keys = RadrootsNostrKeys::generate(); + let event = RadrootsNostrEventBuilder::text_note("subscription test") + .sign_with_keys(&keys) + .expect("event should sign"); + nostrdb + .ingest_event(&event, RadrootsNostrdbIngestSource::relay_unknown()) + .expect("ingest should succeed"); + + let mut notes = Vec::new(); + for _ in 0..40 { + notes = nostrdb.poll_for_note_keys(handle, 32); + if !notes.is_empty() { + break; + } + std::thread::sleep(Duration::from_millis(25)); + } + + assert!(!notes.is_empty()); + nostrdb + .unsubscribe(handle) + .expect("unsubscribe should succeed"); + } + + #[test] + fn subscribe_reports_nostrdb_error() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + let spec = RadrootsNostrdbSubscriptionSpec::text_notes(Some(10), None); + test_hooks::FORCE_SUBSCRIBE_ERROR.store(true, Ordering::SeqCst); + + let err = nostrdb + .subscribe(&spec) + .expect_err("forced subscribe error"); + assert!(err.to_string().starts_with("nostrdb error:")); + } + + #[test] + fn unsubscribe_reports_nostrdb_error() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + let spec = RadrootsNostrdbSubscriptionSpec::text_notes(Some(10), None); + let handle = nostrdb.subscribe(&spec).expect("subscribe should succeed"); + test_hooks::FORCE_UNSUBSCRIBE_ERROR.store(true, Ordering::SeqCst); + + let err = nostrdb + .unsubscribe(handle) + .expect_err("forced unsubscribe error"); + assert!(err.to_string().starts_with("nostrdb error:")); + } + + #[tokio::test] + #[allow(clippy::await_holding_lock)] + async fn wait_for_note_keys_yields_results() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + let spec = RadrootsNostrdbSubscriptionSpec::text_notes(Some(10), None); + let handle = nostrdb.subscribe(&spec).expect("subscribe should succeed"); + + let keys = RadrootsNostrKeys::generate(); + let event = RadrootsNostrEventBuilder::text_note("wait test") + .sign_with_keys(&keys) + .expect("event should sign"); + nostrdb + .ingest_event(&event, RadrootsNostrdbIngestSource::relay_unknown()) + .expect("ingest should succeed"); + + let notes = nostrdb + .wait_for_note_keys(handle, 32) + .await + .expect("wait should succeed"); + assert!(!notes.is_empty()); + } + + #[tokio::test] + #[allow(clippy::await_holding_lock)] + async fn wait_for_note_keys_reports_nostrdb_error() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + let spec = RadrootsNostrdbSubscriptionSpec::text_notes(Some(10), None); + let handle = nostrdb.subscribe(&spec).expect("subscribe should succeed"); + test_hooks::FORCE_WAIT_ERROR.store(true, Ordering::SeqCst); + + let err = nostrdb + .wait_for_note_keys(handle, 1) + .await + .expect_err("forced wait error"); + assert!(err.to_string().starts_with("nostrdb error:")); + } + + #[test] + fn query_notes_returns_ingested_results() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + let keys = RadrootsNostrKeys::generate(); + let event = RadrootsNostrEventBuilder::text_note("query note") + .sign_with_keys(&keys) + .expect("event should sign"); + nostrdb + .ingest_event(&event, RadrootsNostrdbIngestSource::client()) + .expect("ingest should succeed"); + + let query_spec = RadrootsNostrdbQuerySpec::text_notes(Some(50), None, 50); + let mut notes = Vec::new(); + for _ in 0..40 { + notes = nostrdb + .query_notes(&query_spec) + .expect("query should succeed"); + if !notes.is_empty() { + break; + } + std::thread::sleep(Duration::from_millis(25)); + } + assert!(!notes.is_empty()); + let note_pairs = notes + .iter() + .map(|note| (note.id_hex.clone(), note.content.clone())) + .collect::<Vec<_>>(); + assert!(note_pairs.contains(&(event.id.to_hex(), "query note".to_string()))); + } + + #[test] + fn query_notes_empty_filters_returns_empty() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + let query_spec = RadrootsNostrdbQuerySpec::new(Vec::new(), 10); + let notes = nostrdb + .query_notes(&query_spec) + .expect("query should succeed"); + assert!(notes.is_empty()); + } + + #[test] + fn query_notes_rejects_invalid_filters() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + let spec = RadrootsNostrdbQuerySpec::single( + RadrootsNostrdbFilterSpec::new().with_author_hex("not-hex"), + 10, + ); + let err = nostrdb.query_notes(&spec).expect_err("invalid filter"); + assert!(err.to_string().contains("invalid hex")); + } + + #[test] + fn query_notes_reports_transaction_error() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + let spec = RadrootsNostrdbQuerySpec::text_notes(Some(10), None, 10); + test_hooks::FORCE_TRANSACTION_ERROR.store(true, Ordering::SeqCst); + + let err = nostrdb + .query_notes(&spec) + .expect_err("forced transaction error"); + assert!(err.to_string().starts_with("nostrdb error:")); + } + + #[test] + fn query_notes_reports_query_error() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + let spec = RadrootsNostrdbQuerySpec::text_notes(Some(10), None, 10); + test_hooks::FORCE_QUERY_ERROR.store(true, Ordering::SeqCst); + + let err = nostrdb.query_notes(&spec).expect_err("forced query error"); + assert!(err.to_string().starts_with("nostrdb error:")); + } + + #[test] + fn query_notes_reports_note_json_error() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + let keys = RadrootsNostrKeys::generate(); + let event = RadrootsNostrEventBuilder::text_note("note json error") + .sign_with_keys(&keys) + .expect("event should sign"); + nostrdb + .ingest_event(&event, RadrootsNostrdbIngestSource::client()) + .expect("ingest should succeed"); + + let query_spec = RadrootsNostrdbQuerySpec::text_notes(Some(50), None, 50); + for _ in 0..40 { + let notes = nostrdb + .query_notes(&query_spec) + .expect("query should succeed"); + if !notes.is_empty() { + break; + } + std::thread::sleep(Duration::from_millis(25)); + } + test_hooks::FORCE_NOTE_JSON_ERROR.store(true, Ordering::SeqCst); + + let err = nostrdb + .query_notes(&query_spec) + .expect_err("forced note json error"); + assert!(err.to_string().starts_with("nostrdb error:")); + } + + #[test] + fn profile_lookup_returns_metadata_fields() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + let keys = RadrootsNostrKeys::generate(); + let pubkey_hex = keys.public_key().to_hex(); + let metadata = RadrootsNostrMetadata::new() + .name(FIXTURE_ALICE_USERNAME) + .display_name(FIXTURE_ALICE_USERNAME) + .about("coffee operator") + .lud16(FIXTURE_ALICE_EMAIL); + let metadata_event = radroots_nostr_build_metadata_event(&metadata) + .sign_with_keys(&keys) + .expect("metadata event should sign"); + nostrdb + .ingest_event(&metadata_event, RadrootsNostrdbIngestSource::client()) + .expect("ingest should succeed"); + + let mut profile = None; + for _ in 0..40 { + profile = nostrdb + .get_profile_by_pubkey_hex(pubkey_hex.as_str()) + .expect("profile lookup should succeed"); + if profile.is_some() { + break; + } + std::thread::sleep(Duration::from_millis(25)); + } + let profile = profile.expect("profile should exist"); + assert_eq!(profile.pubkey_hex, pubkey_hex); + assert_eq!(profile.name.as_deref(), Some(FIXTURE_ALICE_USERNAME)); + assert_eq!( + profile.display_name.as_deref(), + Some(FIXTURE_ALICE_USERNAME) + ); + assert_eq!(profile.lud16.as_deref(), Some(FIXTURE_ALICE_EMAIL)); + } + + #[test] + fn profile_lookup_returns_none_when_missing() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + let pubkey_hex = RadrootsNostrKeys::generate().public_key().to_hex(); + let profile = nostrdb + .get_profile_by_pubkey_hex(pubkey_hex.as_str()) + .expect("profile lookup"); + assert!(profile.is_none()); + } + + #[test] + fn profile_lookup_reports_query_error() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + let pubkey_hex = RadrootsNostrKeys::generate().public_key().to_hex(); + test_hooks::FORCE_PROFILE_QUERY_ERROR.store(true, Ordering::SeqCst); + + let err = nostrdb + .get_profile_by_pubkey_hex(pubkey_hex.as_str()) + .expect_err("forced profile query error"); + assert!(err.to_string().starts_with("nostrdb error:")); + } + + #[test] + fn profile_lookup_reports_transaction_error() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + let pubkey_hex = RadrootsNostrKeys::generate().public_key().to_hex(); + test_hooks::FORCE_TRANSACTION_ERROR.store(true, Ordering::SeqCst); + + let err = nostrdb + .get_profile_by_pubkey_hex(pubkey_hex.as_str()) + .expect_err("forced transaction error"); + assert!(err.to_string().starts_with("nostrdb error:")); + } + + #[test] + fn profile_lookup_returns_none_without_metadata_record() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + let keys = RadrootsNostrKeys::generate(); + let pubkey_hex = keys.public_key().to_hex(); + let event = RadrootsNostrEventBuilder::text_note("non profile event") + .sign_with_keys(&keys) + .expect("event should sign"); + nostrdb + .ingest_event(&event, RadrootsNostrdbIngestSource::client()) + .expect("ingest should succeed"); + + let profile = nostrdb + .get_profile_by_pubkey_hex(pubkey_hex.as_str()) + .expect("profile lookup"); + assert!(profile.is_none()); + } + + #[test] + fn profile_lookup_invalid_metadata_content_returns_none() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + let keys = RadrootsNostrKeys::generate(); + let pubkey_hex = keys.public_key().to_hex(); + let event = RadrootsNostrEventBuilder::new( + radroots_nostr::prelude::RadrootsNostrKind::Metadata, + "not valid metadata json", + ) + .sign_with_keys(&keys) + .expect("event should sign"); + nostrdb + .ingest_event(&event, RadrootsNostrdbIngestSource::client()) + .expect("ingest should succeed"); + + let result = nostrdb.get_profile_by_pubkey_hex(pubkey_hex.as_str()); + assert!(result.expect("profile lookup").is_none()); + } + + #[test] + fn subscribe_rejects_invalid_author_hex() { + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + let spec = RadrootsNostrdbSubscriptionSpec::single( + RadrootsNostrdbFilterSpec::new().with_author_hex("not-hex"), + ); + let err = nostrdb.subscribe(&spec).expect_err("subscribe should fail"); + assert!(err.to_string().contains("invalid hex for author")); + } + + #[test] + fn profile_lookup_rejects_invalid_pubkey_length() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + let err = nostrdb + .get_profile_by_pubkey_hex("abcd") + .expect_err("lookup should fail"); + assert!(err.to_string().contains("invalid hex length for pubkey")); + } + + #[tokio::test] + async fn subscription_stream_yields_events() { + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + let spec = RadrootsNostrdbSubscriptionSpec::text_notes(Some(10), None); + let handle = nostrdb.subscribe(&spec).expect("subscribe should succeed"); + let mut stream = nostrdb.subscription_stream(handle, 0); + + let pending = tokio::time::timeout(Duration::from_millis(20), stream.next()).await; + assert!(pending.is_err()); + + let keys = RadrootsNostrKeys::generate(); + let event = RadrootsNostrEventBuilder::text_note("stream note") + .sign_with_keys(&keys) + .expect("event should sign"); + nostrdb + .ingest_event(&event, RadrootsNostrdbIngestSource::client()) + .expect("ingest should succeed"); + + let note_keys = tokio::time::timeout(Duration::from_secs(2), stream.next()) + .await + .expect("stream should wake") + .expect("stream should yield note keys"); + assert!(!note_keys.is_empty()); + assert!(note_keys.iter().all(|key| key.as_u64() > 0)); + } + + #[test] + fn concurrent_ingest_handles_parallel_writers() { + let _guard = test_hooks_guard(); + reset_test_flags(); + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + let worker_count = 4usize; + let notes_per_worker = 20usize; + let mut handles = Vec::new(); + + for worker in 0..worker_count { + let db = nostrdb.clone(); + handles.push(std::thread::spawn(move || { + let keys = RadrootsNostrKeys::generate(); + for idx in 0..notes_per_worker { + let content = format!("parallel-{worker}-{idx}"); + let event = RadrootsNostrEventBuilder::text_note(content.as_str()) + .sign_with_keys(&keys) + .expect("event should sign"); + db.ingest_event(&event, RadrootsNostrdbIngestSource::client()) + .expect("ingest should succeed"); + } + })); + } + + for handle in handles { + handle.join().expect("worker should complete"); + } + + let query_spec = RadrootsNostrdbQuerySpec::text_notes(Some(512), None, 512); + let expected = worker_count * notes_per_worker; + let mut observed = 0usize; + let mut break_threshold = expected + 1; + + for _ in 0..80 { + let notes = nostrdb + .query_notes(&query_spec) + .expect("query should succeed"); + observed = notes + .iter() + .filter(|note| note.content.starts_with("parallel-")) + .count(); + if observed >= break_threshold { + break; + } + break_threshold = expected; + std::thread::sleep(Duration::from_millis(25)); + } + + assert!( + observed >= expected, + "expected at least {expected} parallel notes, got {observed}" + ); + } + + #[cfg(feature = "giftwrap")] + #[test] + fn giftwrap_secret_key_hex_validates_length() { + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + let result = nostrdb.add_giftwrap_secret_key_hex("abcd"); + let err = result.expect_err("invalid giftwrap key"); + assert!( + err.to_string() + .contains("invalid hex length for secret_key") + ); + } + + #[cfg(feature = "giftwrap")] + #[test] + fn giftwrap_process_flow_executes() { + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + + let secret_key = [7u8; 32]; + let _ = nostrdb.add_giftwrap_secret_key(secret_key); + nostrdb + .process_giftwraps() + .expect("giftwrap processing should run"); + } +} diff --git a/crates/nostrdb/src/query.rs b/crates/nostrdb/src/query.rs @@ -0,0 +1,60 @@ +use crate::filter::RadrootsNostrdbFilterSpec; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct RadrootsNostrdbQuerySpec { + filters: Vec<RadrootsNostrdbFilterSpec>, + max_results: u32, +} + +impl RadrootsNostrdbQuerySpec { + pub fn new(filters: Vec<RadrootsNostrdbFilterSpec>, max_results: u32) -> Self { + Self { + filters, + max_results: max_results.max(1), + } + } + + pub fn single(filter: RadrootsNostrdbFilterSpec, max_results: u32) -> Self { + Self::new(vec![filter], max_results) + } + + pub fn text_notes(limit: Option<u64>, since_unix: Option<u64>, max_results: u32) -> Self { + Self::single( + RadrootsNostrdbFilterSpec::text_notes(limit, since_unix), + max_results, + ) + } + + pub fn filters(&self) -> &[RadrootsNostrdbFilterSpec] { + &self.filters + } + + pub fn max_results(&self) -> u32 { + self.max_results + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct RadrootsNostrdbNote { + pub note_key: u64, + pub id_hex: String, + pub author_hex: String, + pub kind: u32, + pub created_at_unix: u64, + pub content: String, + pub json: String, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct RadrootsNostrdbProfile { + pub profile_key: Option<u64>, + pub pubkey_hex: String, + pub name: Option<String>, + pub display_name: Option<String>, + pub about: Option<String>, + pub picture: Option<String>, + pub banner: Option<String>, + pub website: Option<String>, + pub nip05: Option<String>, + pub lud16: Option<String>, +} diff --git a/crates/nostrdb/src/runtime_adapter.rs b/crates/nostrdb/src/runtime_adapter.rs @@ -0,0 +1,118 @@ +use crate::ingest::RadrootsNostrdbIngestSource; +use crate::nostrdb::RadrootsNostrdb; +use radroots_nostr::prelude::RadrootsNostrEvent; +use radroots_nostr_runtime::prelude::RadrootsNostrEventSink; +use std::sync::Arc; + +#[derive(Clone)] +pub struct RadrootsNostrdbEventSinkAdapter { + nostrdb: RadrootsNostrdb, + source: RadrootsNostrdbIngestSource, +} + +fn nostrdb_error_to_string(source: crate::error::RadrootsNostrdbError) -> String { + source.to_string() +} + +impl RadrootsNostrdbEventSinkAdapter { + pub fn new(nostrdb: RadrootsNostrdb) -> Self { + Self { + nostrdb, + source: RadrootsNostrdbIngestSource::client(), + } + } + + pub fn with_source(mut self, source: RadrootsNostrdbIngestSource) -> Self { + self.source = source; + self + } + + pub fn into_event_sink(self) -> Arc<dyn RadrootsNostrEventSink> { + Arc::new(self) + } +} + +impl RadrootsNostrEventSink for RadrootsNostrdb { + fn ingest_event(&self, event: &RadrootsNostrEvent) -> Result<(), String> { + RadrootsNostrdb::ingest_event(self, event, RadrootsNostrdbIngestSource::client()) + .map_err(nostrdb_error_to_string) + } +} + +impl RadrootsNostrEventSink for RadrootsNostrdbEventSinkAdapter { + fn ingest_event(&self, event: &RadrootsNostrEvent) -> Result<(), String> { + self.nostrdb + .ingest_event(event, self.source.clone()) + .map_err(nostrdb_error_to_string) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::RadrootsNostrdbConfig; + use radroots_nostr::prelude::{RadrootsNostrEventBuilder, RadrootsNostrKeys}; + use tempfile::TempDir; + + #[test] + fn runtime_adapter_accepts_signed_events() { + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + let adapter = RadrootsNostrdbEventSinkAdapter::new(nostrdb); + + let keys = RadrootsNostrKeys::generate(); + let event = RadrootsNostrEventBuilder::text_note("hello from runtime adapter") + .sign_with_keys(&keys) + .expect("event should sign"); + + adapter + .ingest_event(&event) + .expect("adapter should ingest event"); + } + + #[test] + fn runtime_adapter_can_be_boxed_as_sink_trait() { + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + let sink = RadrootsNostrdbEventSinkAdapter::new(nostrdb) + .with_source(RadrootsNostrdbIngestSource::relay("wss://radroots.org")) + .into_event_sink(); + + let keys = RadrootsNostrKeys::generate(); + let event = RadrootsNostrEventBuilder::text_note("hello trait object") + .sign_with_keys(&keys) + .expect("event should sign"); + + sink.ingest_event(&event) + .expect("boxed sink should ingest event"); + } + + #[test] + fn nostrdb_can_be_boxed_as_sink_trait() { + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("nostrdb"); + let config = RadrootsNostrdbConfig::new(&db_dir); + let nostrdb = RadrootsNostrdb::open(config).expect("database should open"); + let sink: Arc<dyn RadrootsNostrEventSink> = Arc::new(nostrdb.clone()); + + let keys = RadrootsNostrKeys::generate(); + let event = RadrootsNostrEventBuilder::text_note("hello nostrdb trait object") + .sign_with_keys(&keys) + .expect("event should sign"); + + sink.ingest_event(&event) + .expect("nostrdb trait object should ingest event"); + } + + #[test] + fn runtime_adapter_error_to_string_converts() { + let rendered = nostrdb_error_to_string(crate::error::RadrootsNostrdbError::Nostrdb( + "nostrdb error".to_string(), + )); + assert_eq!(rendered, "nostrdb error: nostrdb error"); + } +} diff --git a/crates/nostrdb/src/subscription.rs b/crates/nostrdb/src/subscription.rs @@ -0,0 +1,106 @@ +use crate::filter::RadrootsNostrdbFilterSpec; + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub struct RadrootsNostrdbSubscriptionHandle { + id: u64, +} + +impl RadrootsNostrdbSubscriptionHandle { + pub(crate) fn new(id: u64) -> Self { + Self { id } + } + + pub fn id(self) -> u64 { + self.id + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub struct RadrootsNostrdbNoteKey { + key: u64, +} + +impl RadrootsNostrdbNoteKey { + pub(crate) fn new(key: u64) -> Self { + Self { key } + } + + pub fn as_u64(self) -> u64 { + self.key + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct RadrootsNostrdbSubscriptionSpec { + filters: Vec<RadrootsNostrdbFilterSpec>, +} + +impl RadrootsNostrdbSubscriptionSpec { + pub fn new(filters: Vec<RadrootsNostrdbFilterSpec>) -> Self { + Self { filters } + } + + pub fn single(filter: RadrootsNostrdbFilterSpec) -> Self { + Self { + filters: vec![filter], + } + } + + pub fn text_notes(limit: Option<u64>, since_unix: Option<u64>) -> Self { + Self::single(RadrootsNostrdbFilterSpec::text_notes(limit, since_unix)) + } + + pub fn filters(&self) -> &[RadrootsNostrdbFilterSpec] { + &self.filters + } +} + +#[cfg(feature = "rt")] +pub struct RadrootsNostrdbSubscriptionStream { + pub(crate) inner: nostrdb::SubscriptionStream, +} + +#[cfg(feature = "rt")] +impl futures::Stream for RadrootsNostrdbSubscriptionStream { + type Item = Vec<RadrootsNostrdbNoteKey>; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll<Option<Self::Item>> { + std::pin::Pin::new(&mut self.inner) + .poll_next(cx) + .map(|note_keys| { + note_keys.map(|keys| { + keys.into_iter() + .map(|note_key| RadrootsNostrdbNoteKey::new(note_key.as_u64())) + .collect() + }) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::filter::RadrootsNostrdbFilterSpec; + + #[test] + fn subscription_types_expose_builders_and_accessors() { + let handle = RadrootsNostrdbSubscriptionHandle::new(42); + assert_eq!(handle.id(), 42); + + let note_key = RadrootsNostrdbNoteKey::new(7); + assert_eq!(note_key.as_u64(), 7); + + let filter = RadrootsNostrdbFilterSpec::new().with_kind(1); + let from_new = RadrootsNostrdbSubscriptionSpec::new(vec![filter.clone()]); + assert_eq!(from_new.filters(), std::slice::from_ref(&filter)); + + let from_single = RadrootsNostrdbSubscriptionSpec::single(filter.clone()); + assert_eq!(from_single.filters(), std::slice::from_ref(&filter)); + + let text_notes = RadrootsNostrdbSubscriptionSpec::text_notes(Some(10), Some(123)); + assert_eq!(text_notes.filters().len(), 1); + } +} diff --git a/crates/nostr_ndb/src/test_fixtures.rs b/crates/nostrdb/src/test_fixtures.rs diff --git a/crates/replica_db/Cargo.toml b/crates/replica_db/Cargo.toml @@ -1,33 +0,0 @@ -[package] -name = "radroots_replica_db" -publish = ["crates-io"] -version = "0.1.0-alpha.2" -edition.workspace = true -authors = ["Tyson Lupul <tyson@radroots.org>"] -rust-version.workspace = true -license.workspace = true -description = "Replica database layer for Radroots" -repository.workspace = true -homepage.workspace = true -documentation = "https://docs.rs/radroots_replica_db" -readme = "README" - -[lib] -crate-type = ["rlib"] - -[features] -default = ["std"] -std = ["radroots_sql_core/std"] -web = ["std", "radroots_sql_core/web"] -native = ["std", "radroots_sql_core/native"] - -[dependencies] -radroots_sql_core = { workspace = true } -radroots_replica_db_schema = { 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/replica_db/README b/crates/replica_db/README @@ -1,22 +0,0 @@ -# radroots_replica_db - -This is the README for `radroots_replica_db`, which provides replica SQL -facades and shaped query helpers for the `radroots` core libraries. - -## Overview - - * a generic `ReplicaSql<E>` facade over any `SqlExecutor` implementation; - * migration, backup, restore, and export helpers for replica database state; - * typed CRUD helpers over the shared replica schema models; - * shaped query helpers for common trade, farm, and event freshness lookups; - * feature-gated web and native backend support through `radroots_sql_core`. - -## Copyright - -Except as otherwise noted, all files in the `radroots_replica_db` distribution -are - - Copyright (c) 2025 Tyson Lupul - -For information on usage and redistribution, and for a DISCLAIMER OF ALL -WARRANTIES, see LICENSE included in the `radroots_replica_db` distribution. diff --git a/crates/replica_db/src/backup.rs b/crates/replica_db/src/backup.rs @@ -1,993 +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 REPLICA_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(executor: &dyn SqlExecutor) -> 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: REPLICA_DB_VERSION.to_string(), - schema, - migrations, - data, - }) -} - -pub fn export_database_backup_json(executor: &dyn SqlExecutor) -> Result<String, SqlError> { - let backup = export_database_backup(executor)?; - serde_json::to_string(&backup).map_err(SqlError::from) -} - -pub fn restore_database_backup( - executor: &dyn SqlExecutor, - 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( - executor: &dyn SqlExecutor, - 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(executor: &dyn SqlExecutor) -> 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( - executor: &dyn SqlExecutor, - 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( - executor: &dyn SqlExecutor, - 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( - executor: &dyn SqlExecutor, - 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 = Value::Array(binds).to_string(); - executor.exec(&sql, &params_json)?; - Ok(()) -} - -pub(crate) fn load_schema(executor: &dyn SqlExecutor) -> 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( - executor: &dyn SqlExecutor, - 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 != REPLICA_DB_VERSION { - return Err(SqlError::InvalidArgument(format!( - "unsupported replica-db version {}, expected {}", - backup.replica_db_version, REPLICA_DB_VERSION - ))); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_sql_core::ExecOutcome; - use std::sync::Mutex; - use std::sync::atomic::{AtomicUsize, Ordering}; - - fn assert_sql_error_code<T: core::fmt::Debug>(result: Result<T, SqlError>, code: &str) { - let err = result.unwrap_err(); - assert_eq!(err.code(), code); - } - - struct MockExecutor { - query_rules: Vec<(String, String)>, - fail_exec_contains: Option<String>, - fail_query_contains: Option<String>, - fail_begin: bool, - fail_commit: bool, - 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, - fail_query_contains: None, - fail_begin: false, - fail_commit: false, - exec_calls: Mutex::new(Vec::new()), - begin_calls: AtomicUsize::new(0), - commit_calls: AtomicUsize::new(0), - rollback_calls: AtomicUsize::new(0), - } - } - - fn with_query_failure(mut self, needle: &str) -> Self { - self.fail_query_contains = Some(needle.to_string()); - self - } - - fn with_begin_failure(mut self) -> Self { - self.fail_begin = true; - self - } - - fn with_commit_failure(mut self) -> Self { - self.fail_commit = true; - self - } - - 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 - && 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> { - if let Some(needle) = &self.fail_query_contains - && sql.contains(needle) - { - return Err(SqlError::InvalidQuery(String::from("forced query failure"))); - } - 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); - if self.fail_begin { - return Err(SqlError::InvalidQuery(String::from("forced begin failure"))); - } - Ok(()) - } - - fn commit(&self) -> Result<(), SqlError> { - self.commit_calls.fetch_add(1, Ordering::SeqCst); - if self.fail_commit { - return Err(SqlError::InvalidQuery(String::from( - "forced commit failure", - ))); - } - 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: REPLICA_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(), - }; - - assert_sql_error_code( - restore_database_backup(&executor, &backup), - "ERR_INVALID_QUERY", - ); - 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: REPLICA_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 load_schema_rejects_invalid_json() { - let executor = MockExecutor::new( - vec![( - String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), - String::from("{"), - )], - None, - ); - assert_sql_error_code(load_schema(&executor), "ERR_SERIALIZATION"); - } - - #[test] - fn validate_backup_version_rejects_invalid_versions() { - let wrong_format = backup_with_versions("0.0.1", REPLICA_DB_VERSION); - assert_sql_error_code( - validate_backup_version(&wrong_format), - "ERR_INVALID_ARGUMENT", - ); - - let wrong_db_version = backup_with_versions(DATABASE_BACKUP_VERSION, "0.0.0"); - assert_sql_error_code( - validate_backup_version(&wrong_db_version), - "ERR_INVALID_ARGUMENT", - ); - } - - #[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, REPLICA_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); - assert_sql_error_code( - restore_database_backup_json(&executor, "{"), - "ERR_SERIALIZATION", - ); - } - - #[test] - fn restore_database_backup_json_accepts_valid_json() { - let executor = MockExecutor::new( - vec![( - String::from("select type, name from sqlite_master"), - String::from("[]"), - )], - None, - ); - let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_DB_VERSION); - let backup_json = serde_json::to_string(&backup).expect("serialize backup"); - - restore_database_backup_json(&executor, &backup_json).expect("restore should succeed"); - assert_eq!(executor.begin_count(), 1); - assert_eq!(executor.commit_count(), 1); - assert_eq!(executor.rollback_count(), 0); - } - - #[test] - fn export_database_backup_propagates_schema_query_errors() { - let executor = MockExecutor::new(Vec::new(), None).with_query_failure( - "select type, name, tbl_name as table_name, sql from sqlite_master", - ); - assert_sql_error_code(export_database_backup(&executor), "ERR_INVALID_QUERY"); - } - - #[test] - fn export_database_backup_propagates_table_query_errors() { - let schema_rows = serde_json::json!([ - { - "type": "table", - "name": "tb_a", - "table_name": "tb_a", - "sql": "CREATE TABLE tb_a (id TEXT);" - } - ]) - .to_string(); - let executor = MockExecutor::new( - vec![( - String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), - schema_rows, - )], - None, - ) - .with_query_failure("SELECT * FROM \"tb_a\";"); - assert_sql_error_code(export_database_backup(&executor), "ERR_INVALID_QUERY"); - } - - #[test] - fn export_database_backup_json_propagates_export_errors() { - let executor = MockExecutor::new(Vec::new(), None).with_query_failure( - "select type, name, tbl_name as table_name, sql from sqlite_master", - ); - assert_sql_error_code(export_database_backup_json(&executor), "ERR_INVALID_QUERY"); - } - - #[test] - fn export_database_backup_succeeds_with_empty_schema() { - let executor = MockExecutor::new( - vec![( - String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), - String::from("[]"), - )], - None, - ); - let backup = export_database_backup(&executor).expect("backup success"); - assert!(backup.schema.is_empty()); - assert!(backup.data.is_empty()); - } - - #[test] - fn export_database_backup_json_succeeds_with_empty_schema() { - let executor = MockExecutor::new( - vec![( - String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), - String::from("[]"), - )], - None, - ); - let backup_json = export_database_backup_json(&executor).expect("backup json success"); - assert!(backup_json.contains("\"schema\":[]")); - } - - #[test] - fn drop_existing_objects_rejects_invalid_master_json() { - let executor = MockExecutor::new( - vec![( - String::from("select type, name from sqlite_master"), - String::from("{"), - )], - None, - ); - assert_sql_error_code(drop_existing_objects(&executor), "ERR_SERIALIZATION"); - } - - #[test] - fn drop_existing_objects_propagates_drop_exec_errors() { - let master_rows = serde_json::json!([{ "type": "table", "name": "tb_a" }]).to_string(); - let executor = MockExecutor::new( - vec![( - String::from("select type, name from sqlite_master"), - master_rows, - )], - Some(String::from("DROP TABLE IF EXISTS")), - ); - assert_sql_error_code(drop_existing_objects(&executor), "ERR_INVALID_QUERY"); - } - - #[test] - fn create_schema_from_backup_propagates_non_table_exec_errors() { - let executor = MockExecutor::new(Vec::new(), Some(String::from("CREATE VIEW"))); - let schema = vec![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;")), - }]; - assert_sql_error_code( - create_schema_from_backup(&executor, &schema), - "ERR_INVALID_QUERY", - ); - } - - #[test] - fn read_tables_for_backup_propagates_query_errors() { - let executor = - MockExecutor::new(Vec::new(), None).with_query_failure("SELECT * FROM \"tb_a\";"); - 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);")), - }]; - assert_sql_error_code( - read_tables_for_backup(&executor, &schema), - "ERR_INVALID_QUERY", - ); - } - - #[test] - fn read_tables_for_backup_propagates_parse_errors() { - let executor = MockExecutor::new( - vec![(String::from("SELECT * FROM \"tb_a\";"), String::from("{"))], - 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);")), - }]; - assert_sql_error_code( - read_tables_for_backup(&executor, &schema), - "ERR_SERIALIZATION", - ); - } - - #[test] - fn restore_database_backup_rejects_invalid_versions_before_transaction() { - let executor = MockExecutor::new(Vec::new(), None); - let backup = backup_with_versions("0.0.1", REPLICA_DB_VERSION); - assert_sql_error_code( - restore_database_backup(&executor, &backup), - "ERR_INVALID_ARGUMENT", - ); - assert_eq!(executor.begin_count(), 0); - } - - #[test] - fn restore_database_backup_fails_when_foreign_keys_disable_fails() { - let executor = MockExecutor::new( - vec![( - String::from("select type, name from sqlite_master"), - String::from("[]"), - )], - Some(String::from("PRAGMA foreign_keys = OFF;")), - ); - let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_DB_VERSION); - assert_sql_error_code( - restore_database_backup(&executor, &backup), - "ERR_INVALID_QUERY", - ); - } - - #[test] - fn restore_database_backup_fails_when_begin_fails() { - let executor = MockExecutor::new( - vec![( - String::from("select type, name from sqlite_master"), - String::from("[]"), - )], - None, - ) - .with_begin_failure(); - let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_DB_VERSION); - assert_sql_error_code( - restore_database_backup(&executor, &backup), - "ERR_INVALID_QUERY", - ); - } - - #[test] - fn restore_database_backup_fails_when_drop_query_fails() { - let executor = MockExecutor::new(Vec::new(), None) - .with_query_failure("select type, name from sqlite_master"); - let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_DB_VERSION); - assert_sql_error_code( - restore_database_backup(&executor, &backup), - "ERR_INVALID_QUERY", - ); - } - - #[test] - fn restore_database_backup_fails_when_create_schema_fails() { - let executor = MockExecutor::new( - vec![( - String::from("select type, name from sqlite_master"), - String::from("[]"), - )], - Some(String::from("CREATE TABLE tb_a")), - ); - let backup = DatabaseBackup { - format_version: DATABASE_BACKUP_VERSION.to_string(), - replica_db_version: REPLICA_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);")), - }], - migrations: Vec::new(), - data: Vec::new(), - }; - - assert_sql_error_code( - restore_database_backup(&executor, &backup), - "ERR_INVALID_QUERY", - ); - assert_eq!(executor.begin_count(), 1); - assert_eq!(executor.commit_count(), 0); - assert_eq!(executor.rollback_count(), 1); - } - - #[test] - fn restore_database_backup_fails_when_insert_rows_fail() { - let executor = MockExecutor::new( - vec![( - String::from("select type, name from sqlite_master"), - String::from("[]"), - )], - Some(String::from("INSERT INTO \"tb_a\"")), - ); - let mut row = Map::new(); - row.insert(String::from("id"), Value::from("1")); - let backup = DatabaseBackup { - format_version: DATABASE_BACKUP_VERSION.to_string(), - replica_db_version: REPLICA_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);")), - }], - migrations: Vec::new(), - data: vec![TableData { - name: String::from("tb_a"), - rows: vec![row], - }], - }; - assert_sql_error_code( - restore_database_backup(&executor, &backup), - "ERR_INVALID_QUERY", - ); - } - - #[test] - fn restore_database_backup_fails_when_commit_fails() { - let executor = MockExecutor::new( - vec![( - String::from("select type, name from sqlite_master"), - String::from("[]"), - )], - None, - ) - .with_commit_failure(); - let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_DB_VERSION); - assert_sql_error_code( - restore_database_backup(&executor, &backup), - "ERR_INVALID_QUERY", - ); - } - - #[test] - fn restore_database_backup_fails_when_foreign_keys_enable_fails_after_commit() { - let executor = MockExecutor::new( - vec![( - String::from("select type, name from sqlite_master"), - String::from("[]"), - )], - Some(String::from("PRAGMA foreign_keys = ON;")), - ); - let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_DB_VERSION); - assert_sql_error_code( - restore_database_backup(&executor, &backup), - "ERR_INVALID_QUERY", - ); - } -} diff --git a/crates/replica_db/src/export.rs b/crates/replica_db/src/export.rs @@ -1,283 +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, REPLICA_DB_VERSION, SchemaEntry, escape_identifier, - export_migrations, load_schema, -}; - -pub const REPLICA_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 ReplicaDbExportManifestRs { - 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(executor: &dyn SqlExecutor) -> Result<ReplicaDbExportManifestRs, 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(ReplicaDbExportManifestRs { - export_version: REPLICA_DB_EXPORT_VERSION.to_string(), - replica_db_version: REPLICA_DB_VERSION.to_string(), - backup_format_version: DATABASE_BACKUP_VERSION.to_string(), - schema_hash, - schema, - migrations, - table_counts, - }) -} - -fn load_table_counts( - executor: &dyn SqlExecutor, - 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]) -> String { - let mut hasher = Sha256::new(); - for entry in schema { - hasher.update(entry.object_type.as_bytes()); - hasher.update([0]); - hasher.update(entry.name.as_bytes()); - hasher.update([0]); - if let Some(table_name) = &entry.table_name { - hasher.update(table_name.as_bytes()); - } - hasher.update([0]); - if let Some(sql) = &entry.sql { - hasher.update(sql.as_bytes()); - } - hasher.update([255]); - } - hex::encode(hasher.finalize()) -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_sql_core::ExecOutcome; - - fn assert_sql_error_code<T: core::fmt::Debug>(result: Result<T, SqlError>, code: &str) { - let err = result.unwrap_err(); - assert_eq!(err.code(), code); - } - - struct MockExecutor { - query_rules: Vec<(String, String)>, - fail_query_contains: Option<String>, - } - - impl MockExecutor { - fn new(query_rules: Vec<(String, String)>, fail_query_contains: Option<String>) -> Self { - Self { - query_rules, - fail_query_contains, - } - } - } - - impl SqlExecutor for MockExecutor { - fn exec(&self, _sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> { - Ok(ExecOutcome { - changes: 1, - last_insert_id: 1, - }) - } - - fn query_raw(&self, sql: &str, _params_json: &str) -> Result<String, SqlError> { - if let Some(needle) = &self.fail_query_contains - && sql.contains(needle) - { - return Err(SqlError::InvalidQuery(String::from("forced query failure"))); - } - for (needle, response) in &self.query_rules { - if sql.contains(needle) { - return Ok(response.clone()); - } - } - Ok(String::from("[]")) - } - - fn begin(&self) -> Result<(), SqlError> { - Ok(()) - } - - fn commit(&self) -> Result<(), SqlError> { - Ok(()) - } - - fn rollback(&self) -> Result<(), SqlError> { - Ok(()) - } - } - - #[test] - fn export_manifest_propagates_schema_query_errors() { - let executor = MockExecutor::new( - Vec::new(), - Some(String::from( - "select type, name, tbl_name as table_name, sql from sqlite_master", - )), - ); - assert_sql_error_code(export_manifest(&executor), "ERR_INVALID_QUERY"); - } - - #[test] - fn export_manifest_propagates_table_count_query_errors() { - let schema_rows = serde_json::json!([ - { - "type": "table", - "name": "tb_a", - "table_name": "tb_a", - "sql": "CREATE TABLE tb_a (id TEXT);" - } - ]) - .to_string(); - let executor = MockExecutor::new( - vec![( - String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), - schema_rows, - )], - Some(String::from("select count(1) as count from \"tb_a\"")), - ); - assert_sql_error_code(export_manifest(&executor), "ERR_INVALID_QUERY"); - } - - #[test] - fn export_manifest_propagates_table_count_parse_errors() { - let schema_rows = serde_json::json!([ - { - "type": "table", - "name": "tb_a", - "table_name": "tb_a", - "sql": "CREATE TABLE tb_a (id TEXT);" - } - ]) - .to_string(); - let executor = MockExecutor::new( - vec![ - ( - String::from( - "select type, name, tbl_name as table_name, sql from sqlite_master", - ), - schema_rows, - ), - ( - String::from("select count(1) as count from \"tb_a\""), - String::from("{"), - ), - ], - None, - ); - assert_sql_error_code(export_manifest(&executor), "ERR_SERIALIZATION"); - } - - #[test] - fn export_manifest_defaults_missing_count_row_to_zero() { - let schema_rows = serde_json::json!([ - { - "type": "table", - "name": "tb_a", - "table_name": "tb_a", - "sql": "CREATE TABLE tb_a (id TEXT);" - } - ]) - .to_string(); - let executor = MockExecutor::new( - vec![ - ( - String::from( - "select type, name, tbl_name as table_name, sql from sqlite_master", - ), - schema_rows, - ), - ( - String::from("select count(1) as count from \"tb_a\""), - String::from("[]"), - ), - ], - None, - ); - let manifest = export_manifest(&executor).expect("export should succeed"); - assert_eq!(manifest.table_counts.len(), 1); - assert_eq!(manifest.table_counts[0].name, "tb_a"); - assert_eq!(manifest.table_counts[0].row_count, 0); - } - - #[test] - fn schema_hash_handles_optional_fields() { - let with_all = 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);")), - }; - let without_optional = SchemaEntry { - object_type: String::from("index"), - name: String::from("ix_a"), - table_name: None, - sql: None, - }; - let hash = schema_hash(&[with_all, without_optional]); - assert_eq!(hash.len(), 64); - } - - #[test] - fn mock_executor_trait_and_query_paths_are_covered() { - let executor = MockExecutor::new( - vec![(String::from("select 1"), String::from("[{\"count\":1}]"))], - None, - ); - let outcome = executor.exec("select 1", "[]").expect("exec"); - assert_eq!(outcome.changes, 1); - assert_eq!(outcome.last_insert_id, 1); - - executor.begin().expect("begin"); - executor.commit().expect("commit"); - executor.rollback().expect("rollback"); - - let matched = executor.query_raw("select 1", "[]").expect("matched query"); - assert_eq!(matched, "[{\"count\":1}]"); - let fallback = executor - .query_raw("select 2", "[]") - .expect("fallback query"); - assert_eq!(fallback, "[]"); - - let failing = MockExecutor::new(Vec::new(), Some(String::from("select fail"))); - assert_sql_error_code(failing.query_raw("select fail", "[]"), "ERR_INVALID_QUERY"); - } -} diff --git a/crates/replica_db/src/lib.rs b/crates/replica_db/src/lib.rs @@ -1,784 +0,0 @@ -use radroots_replica_db_schema::ReplicaSchemaError; -pub use radroots_sql_core::error::SqlError; -pub use radroots_sql_core::{ExecOutcome, SqlExecutor}; - -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_head::{ - INostrEventHeadCreate, INostrEventHeadCreateResolve, INostrEventHeadDelete, - INostrEventHeadDeleteResolve, INostrEventHeadFindMany, INostrEventHeadFindManyResolve, - INostrEventHeadFindOne, INostrEventHeadFindOneResolve, INostrEventHeadUpdate, - INostrEventHeadUpdateResolve, -}; - -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, -}; - -pub mod backup; -pub mod export; -pub mod migrations; -pub mod models; -pub mod query; -pub use backup::{DatabaseBackup, MigrationBackup, SchemaEntry}; -pub use export::{ - REPLICA_DB_EXPORT_VERSION, ReplicaDbExportManifestRs, TableCount, export_manifest, -}; -pub use models::*; -pub use query::ReplicaTradeProductSummaryRow; - -pub struct ReplicaSql<E: SqlExecutor> { - executor: E, -} - -impl<E: SqlExecutor> ReplicaSql<E> { - pub fn coverage_branch_probe(enabled: bool) -> &'static str { - if enabled { "enabled" } else { "disabled" } - } -} - -impl<E: SqlExecutor> ReplicaSql<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, ReplicaSchemaError<SqlError>> { - models::farm::create(self.executor(), opts) - } - - pub fn farm_find_many( - &self, - opts: &IFarmFindMany, - ) -> Result<IFarmFindManyResolve, ReplicaSchemaError<SqlError>> { - models::farm::find_many(self.executor(), opts) - } - - pub fn farm_find_one( - &self, - opts: &IFarmFindOne, - ) -> Result<IFarmFindOneResolve, ReplicaSchemaError<SqlError>> { - models::farm::find_one(self.executor(), opts) - } - - pub fn farm_update( - &self, - opts: &IFarmUpdate, - ) -> Result<IFarmUpdateResolve, ReplicaSchemaError<SqlError>> { - models::farm::update(self.executor(), opts) - } - - pub fn farm_delete( - &self, - opts: &IFarmDelete, - ) -> Result<IFarmDeleteResolve, ReplicaSchemaError<SqlError>> { - models::farm::delete(self.executor(), opts) - } - - pub fn plot_create( - &self, - opts: &IPlotCreate, - ) -> Result<IPlotCreateResolve, ReplicaSchemaError<SqlError>> { - models::plot::create(self.executor(), opts) - } - - pub fn plot_find_many( - &self, - opts: &IPlotFindMany, - ) -> Result<IPlotFindManyResolve, ReplicaSchemaError<SqlError>> { - models::plot::find_many(self.executor(), opts) - } - - pub fn plot_find_one( - &self, - opts: &IPlotFindOne, - ) -> Result<IPlotFindOneResolve, ReplicaSchemaError<SqlError>> { - models::plot::find_one(self.executor(), opts) - } - - pub fn plot_update( - &self, - opts: &IPlotUpdate, - ) -> Result<IPlotUpdateResolve, ReplicaSchemaError<SqlError>> { - models::plot::update(self.executor(), opts) - } - - pub fn plot_delete( - &self, - opts: &IPlotDelete, - ) -> Result<IPlotDeleteResolve, ReplicaSchemaError<SqlError>> { - models::plot::delete(self.executor(), opts) - } - - pub fn gcs_location_create( - &self, - opts: &IGcsLocationCreate, - ) -> Result<IGcsLocationCreateResolve, ReplicaSchemaError<SqlError>> { - models::gcs_location::create(self.executor(), opts) - } - - pub fn gcs_location_find_many( - &self, - opts: &IGcsLocationFindMany, - ) -> Result<IGcsLocationFindManyResolve, ReplicaSchemaError<SqlError>> { - models::gcs_location::find_many(self.executor(), opts) - } - - pub fn gcs_location_find_one( - &self, - opts: &IGcsLocationFindOne, - ) -> Result<IGcsLocationFindOneResolve, ReplicaSchemaError<SqlError>> { - models::gcs_location::find_one(self.executor(), opts) - } - - pub fn gcs_location_update( - &self, - opts: &IGcsLocationUpdate, - ) -> Result<IGcsLocationUpdateResolve, ReplicaSchemaError<SqlError>> { - models::gcs_location::update(self.executor(), opts) - } - - pub fn gcs_location_delete( - &self, - opts: &IGcsLocationDelete, - ) -> Result<IGcsLocationDeleteResolve, ReplicaSchemaError<SqlError>> { - models::gcs_location::delete(self.executor(), opts) - } - - pub fn farm_gcs_location_create( - &self, - opts: &IFarmGcsLocationCreate, - ) -> Result<IFarmGcsLocationCreateResolve, ReplicaSchemaError<SqlError>> { - models::farm_gcs_location::create(self.executor(), opts) - } - - pub fn farm_gcs_location_find_many( - &self, - opts: &IFarmGcsLocationFindMany, - ) -> Result<IFarmGcsLocationFindManyResolve, ReplicaSchemaError<SqlError>> { - models::farm_gcs_location::find_many(self.executor(), opts) - } - - pub fn farm_gcs_location_find_one( - &self, - opts: &IFarmGcsLocationFindOne, - ) -> Result<IFarmGcsLocationFindOneResolve, ReplicaSchemaError<SqlError>> { - models::farm_gcs_location::find_one(self.executor(), opts) - } - - pub fn farm_gcs_location_update( - &self, - opts: &IFarmGcsLocationUpdate, - ) -> Result<IFarmGcsLocationUpdateResolve, ReplicaSchemaError<SqlError>> { - models::farm_gcs_location::update(self.executor(), opts) - } - - pub fn farm_gcs_location_delete( - &self, - opts: &IFarmGcsLocationDelete, - ) -> Result<IFarmGcsLocationDeleteResolve, ReplicaSchemaError<SqlError>> { - models::farm_gcs_location::delete(self.executor(), opts) - } - - pub fn plot_gcs_location_create( - &self, - opts: &IPlotGcsLocationCreate, - ) -> Result<IPlotGcsLocationCreateResolve, ReplicaSchemaError<SqlError>> { - models::plot_gcs_location::create(self.executor(), opts) - } - - pub fn plot_gcs_location_find_many( - &self, - opts: &IPlotGcsLocationFindMany, - ) -> Result<IPlotGcsLocationFindManyResolve, ReplicaSchemaError<SqlError>> { - models::plot_gcs_location::find_many(self.executor(), opts) - } - - pub fn plot_gcs_location_find_one( - &self, - opts: &IPlotGcsLocationFindOne, - ) -> Result<IPlotGcsLocationFindOneResolve, ReplicaSchemaError<SqlError>> { - models::plot_gcs_location::find_one(self.executor(), opts) - } - - pub fn plot_gcs_location_update( - &self, - opts: &IPlotGcsLocationUpdate, - ) -> Result<IPlotGcsLocationUpdateResolve, ReplicaSchemaError<SqlError>> { - models::plot_gcs_location::update(self.executor(), opts) - } - - pub fn plot_gcs_location_delete( - &self, - opts: &IPlotGcsLocationDelete, - ) -> Result<IPlotGcsLocationDeleteResolve, ReplicaSchemaError<SqlError>> { - models::plot_gcs_location::delete(self.executor(), opts) - } - - pub fn farm_tag_create( - &self, - opts: &IFarmTagCreate, - ) -> Result<IFarmTagCreateResolve, ReplicaSchemaError<SqlError>> { - models::farm_tag::create(self.executor(), opts) - } - - pub fn farm_tag_find_many( - &self, - opts: &IFarmTagFindMany, - ) -> Result<IFarmTagFindManyResolve, ReplicaSchemaError<SqlError>> { - models::farm_tag::find_many(self.executor(), opts) - } - - pub fn farm_tag_find_one( - &self, - opts: &IFarmTagFindOne, - ) -> Result<IFarmTagFindOneResolve, ReplicaSchemaError<SqlError>> { - models::farm_tag::find_one(self.executor(), opts) - } - - pub fn farm_tag_update( - &self, - opts: &IFarmTagUpdate, - ) -> Result<IFarmTagUpdateResolve, ReplicaSchemaError<SqlError>> { - models::farm_tag::update(self.executor(), opts) - } - - pub fn farm_tag_delete( - &self, - opts: &IFarmTagDelete, - ) -> Result<IFarmTagDeleteResolve, ReplicaSchemaError<SqlError>> { - models::farm_tag::delete(self.executor(), opts) - } - - pub fn plot_tag_create( - &self, - opts: &IPlotTagCreate, - ) -> Result<IPlotTagCreateResolve, ReplicaSchemaError<SqlError>> { - models::plot_tag::create(self.executor(), opts) - } - - pub fn plot_tag_find_many( - &self, - opts: &IPlotTagFindMany, - ) -> Result<IPlotTagFindManyResolve, ReplicaSchemaError<SqlError>> { - models::plot_tag::find_many(self.executor(), opts) - } - - pub fn plot_tag_find_one( - &self, - opts: &IPlotTagFindOne, - ) -> Result<IPlotTagFindOneResolve, ReplicaSchemaError<SqlError>> { - models::plot_tag::find_one(self.executor(), opts) - } - - pub fn plot_tag_update( - &self, - opts: &IPlotTagUpdate, - ) -> Result<IPlotTagUpdateResolve, ReplicaSchemaError<SqlError>> { - models::plot_tag::update(self.executor(), opts) - } - - pub fn plot_tag_delete( - &self, - opts: &IPlotTagDelete, - ) -> Result<IPlotTagDeleteResolve, ReplicaSchemaError<SqlError>> { - models::plot_tag::delete(self.executor(), opts) - } - - pub fn farm_member_create( - &self, - opts: &IFarmMemberCreate, - ) -> Result<IFarmMemberCreateResolve, ReplicaSchemaError<SqlError>> { - models::farm_member::create(self.executor(), opts) - } - - pub fn farm_member_find_many( - &self, - opts: &IFarmMemberFindMany, - ) -> Result<IFarmMemberFindManyResolve, ReplicaSchemaError<SqlError>> { - models::farm_member::find_many(self.executor(), opts) - } - - pub fn farm_member_find_one( - &self, - opts: &IFarmMemberFindOne, - ) -> Result<IFarmMemberFindOneResolve, ReplicaSchemaError<SqlError>> { - models::farm_member::find_one(self.executor(), opts) - } - - pub fn farm_member_update( - &self, - opts: &IFarmMemberUpdate, - ) -> Result<IFarmMemberUpdateResolve, ReplicaSchemaError<SqlError>> { - models::farm_member::update(self.executor(), opts) - } - - pub fn farm_member_delete( - &self, - opts: &IFarmMemberDelete, - ) -> Result<IFarmMemberDeleteResolve, ReplicaSchemaError<SqlError>> { - models::farm_member::delete(self.executor(), opts) - } - - pub fn farm_member_claim_create( - &self, - opts: &IFarmMemberClaimCreate, - ) -> Result<IFarmMemberClaimCreateResolve, ReplicaSchemaError<SqlError>> { - models::farm_member_claim::create(self.executor(), opts) - } - - pub fn farm_member_claim_find_many( - &self, - opts: &IFarmMemberClaimFindMany, - ) -> Result<IFarmMemberClaimFindManyResolve, ReplicaSchemaError<SqlError>> { - models::farm_member_claim::find_many(self.executor(), opts) - } - - pub fn farm_member_claim_find_one( - &self, - opts: &IFarmMemberClaimFindOne, - ) -> Result<IFarmMemberClaimFindOneResolve, ReplicaSchemaError<SqlError>> { - models::farm_member_claim::find_one(self.executor(), opts) - } - - pub fn farm_member_claim_update( - &self, - opts: &IFarmMemberClaimUpdate, - ) -> Result<IFarmMemberClaimUpdateResolve, ReplicaSchemaError<SqlError>> { - models::farm_member_claim::update(self.executor(), opts) - } - - pub fn farm_member_claim_delete( - &self, - opts: &IFarmMemberClaimDelete, - ) -> Result<IFarmMemberClaimDeleteResolve, ReplicaSchemaError<SqlError>> { - models::farm_member_claim::delete(self.executor(), opts) - } - - pub fn log_error_create( - &self, - opts: &ILogErrorCreate, - ) -> Result<ILogErrorCreateResolve, ReplicaSchemaError<SqlError>> { - models::log_error::create(self.executor(), opts) - } - - pub fn log_error_find_many( - &self, - opts: &ILogErrorFindMany, - ) -> Result<ILogErrorFindManyResolve, ReplicaSchemaError<SqlError>> { - models::log_error::find_many(self.executor(), opts) - } - - pub fn log_error_find_one( - &self, - opts: &ILogErrorFindOne, - ) -> Result<ILogErrorFindOneResolve, ReplicaSchemaError<SqlError>> { - models::log_error::find_one(self.executor(), opts) - } - - pub fn log_error_update( - &self, - opts: &ILogErrorUpdate, - ) -> Result<ILogErrorUpdateResolve, ReplicaSchemaError<SqlError>> { - models::log_error::update(self.executor(), opts) - } - - pub fn log_error_delete( - &self, - opts: &ILogErrorDelete, - ) -> Result<ILogErrorDeleteResolve, ReplicaSchemaError<SqlError>> { - models::log_error::delete(self.executor(), opts) - } - - pub fn media_image_create( - &self, - opts: &IMediaImageCreate, - ) -> Result<IMediaImageCreateResolve, ReplicaSchemaError<SqlError>> { - models::media_image::create(self.executor(), opts) - } - - pub fn media_image_find_many( - &self, - opts: &IMediaImageFindMany, - ) -> Result<IMediaImageFindManyResolve, ReplicaSchemaError<SqlError>> { - models::media_image::find_many(self.executor(), opts) - } - - pub fn media_image_find_one( - &self, - opts: &IMediaImageFindOne, - ) -> Result<IMediaImageFindOneResolve, ReplicaSchemaError<SqlError>> { - models::media_image::find_one(self.executor(), opts) - } - - pub fn media_image_update( - &self, - opts: &IMediaImageUpdate, - ) -> Result<IMediaImageUpdateResolve, ReplicaSchemaError<SqlError>> { - models::media_image::update(self.executor(), opts) - } - - pub fn media_image_delete( - &self, - opts: &IMediaImageDelete, - ) -> Result<IMediaImageDeleteResolve, ReplicaSchemaError<SqlError>> { - models::media_image::delete(self.executor(), opts) - } - - pub fn nostr_profile_create( - &self, - opts: &INostrProfileCreate, - ) -> Result<INostrProfileCreateResolve, ReplicaSchemaError<SqlError>> { - models::nostr_profile::create(self.executor(), opts) - } - - pub fn nostr_profile_find_many( - &self, - opts: &INostrProfileFindMany, - ) -> Result<INostrProfileFindManyResolve, ReplicaSchemaError<SqlError>> { - models::nostr_profile::find_many(self.executor(), opts) - } - - pub fn nostr_profile_find_one( - &self, - opts: &INostrProfileFindOne, - ) -> Result<INostrProfileFindOneResolve, ReplicaSchemaError<SqlError>> { - models::nostr_profile::find_one(self.executor(), opts) - } - - pub fn nostr_profile_update( - &self, - opts: &INostrProfileUpdate, - ) -> Result<INostrProfileUpdateResolve, ReplicaSchemaError<SqlError>> { - models::nostr_profile::update(self.executor(), opts) - } - - pub fn nostr_profile_delete( - &self, - opts: &INostrProfileDelete, - ) -> Result<INostrProfileDeleteResolve, ReplicaSchemaError<SqlError>> { - models::nostr_profile::delete(self.executor(), opts) - } - - pub fn nostr_event_head_create( - &self, - opts: &INostrEventHeadCreate, - ) -> Result<INostrEventHeadCreateResolve, ReplicaSchemaError<SqlError>> { - models::nostr_event_head::create(self.executor(), opts) - } - - pub fn nostr_event_head_find_many( - &self, - opts: &INostrEventHeadFindMany, - ) -> Result<INostrEventHeadFindManyResolve, ReplicaSchemaError<SqlError>> { - models::nostr_event_head::find_many(self.executor(), opts) - } - - pub fn nostr_event_head_find_one( - &self, - opts: &INostrEventHeadFindOne, - ) -> Result<INostrEventHeadFindOneResolve, ReplicaSchemaError<SqlError>> { - models::nostr_event_head::find_one(self.executor(), opts) - } - - pub fn nostr_event_head_update( - &self, - opts: &INostrEventHeadUpdate, - ) -> Result<INostrEventHeadUpdateResolve, ReplicaSchemaError<SqlError>> { - models::nostr_event_head::update(self.executor(), opts) - } - - pub fn nostr_event_head_delete( - &self, - opts: &INostrEventHeadDelete, - ) -> Result<INostrEventHeadDeleteResolve, ReplicaSchemaError<SqlError>> { - models::nostr_event_head::delete(self.executor(), opts) - } - - pub fn nostr_relay_create( - &self, - opts: &INostrRelayCreate, - ) -> Result<INostrRelayCreateResolve, ReplicaSchemaError<SqlError>> { - models::nostr_relay::create(self.executor(), opts) - } - - pub fn nostr_relay_find_many( - &self, - opts: &INostrRelayFindMany, - ) -> Result<INostrRelayFindManyResolve, ReplicaSchemaError<SqlError>> { - models::nostr_relay::find_many(self.executor(), opts) - } - - pub fn nostr_relay_find_one( - &self, - opts: &INostrRelayFindOne, - ) -> Result<INostrRelayFindOneResolve, ReplicaSchemaError<SqlError>> { - models::nostr_relay::find_one(self.executor(), opts) - } - - pub fn nostr_relay_update( - &self, - opts: &INostrRelayUpdate, - ) -> Result<INostrRelayUpdateResolve, ReplicaSchemaError<SqlError>> { - models::nostr_relay::update(self.executor(), opts) - } - - pub fn nostr_relay_delete( - &self, - opts: &INostrRelayDelete, - ) -> Result<INostrRelayDeleteResolve, ReplicaSchemaError<SqlError>> { - models::nostr_relay::delete(self.executor(), opts) - } - - pub fn trade_product_create( - &self, - opts: &ITradeProductCreate, - ) -> Result<ITradeProductCreateResolve, ReplicaSchemaError<SqlError>> { - models::trade_product::create(self.executor(), opts) - } - - pub fn trade_product_find_many( - &self, - opts: &ITradeProductFindMany, - ) -> Result<ITradeProductFindManyResolve, ReplicaSchemaError<SqlError>> { - models::trade_product::find_many(self.executor(), opts) - } - - pub fn trade_product_find_one( - &self, - opts: &ITradeProductFindOne, - ) -> Result<ITradeProductFindOneResolve, ReplicaSchemaError<SqlError>> { - models::trade_product::find_one(self.executor(), opts) - } - - pub fn trade_product_update( - &self, - opts: &ITradeProductUpdate, - ) -> Result<ITradeProductUpdateResolve, ReplicaSchemaError<SqlError>> { - models::trade_product::update(self.executor(), opts) - } - - pub fn trade_product_delete( - &self, - opts: &ITradeProductDelete, - ) -> Result<ITradeProductDeleteResolve, ReplicaSchemaError<SqlError>> { - models::trade_product::delete(self.executor(), opts) - } - - pub fn nostr_profile_relay_set( - &self, - opts: &INostrProfileRelayRelation, - ) -> Result<INostrProfileRelayResolve, ReplicaSchemaError<SqlError>> { - models::nostr_profile_relay::set(self.executor(), opts) - } - - pub fn nostr_profile_relay_unset( - &self, - opts: &INostrProfileRelayRelation, - ) -> Result<INostrProfileRelayResolve, ReplicaSchemaError<SqlError>> { - models::nostr_profile_relay::unset(self.executor(), opts) - } - - pub fn trade_product_location_set( - &self, - opts: &ITradeProductLocationRelation, - ) -> Result<ITradeProductLocationResolve, ReplicaSchemaError<SqlError>> { - models::trade_product_location::set(self.executor(), opts) - } - - pub fn trade_product_location_unset( - &self, - opts: &ITradeProductLocationRelation, - ) -> Result<ITradeProductLocationResolve, ReplicaSchemaError<SqlError>> { - models::trade_product_location::unset(self.executor(), opts) - } - - pub fn trade_product_media_set( - &self, - opts: &ITradeProductMediaRelation, - ) -> Result<ITradeProductMediaResolve, ReplicaSchemaError<SqlError>> { - models::trade_product_media::set(self.executor(), opts) - } - - pub fn trade_product_media_unset( - &self, - opts: &ITradeProductMediaRelation, - ) -> Result<ITradeProductMediaResolve, ReplicaSchemaError<SqlError>> { - models::trade_product_media::unset(self.executor(), opts) - } -} - -#[cfg(test)] -mod tests { - use super::ReplicaSql; - 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 replica_sql_constructor_and_executor_access_are_supported() { - let db = ReplicaSql::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!( - ReplicaSql::<ProbeExecutor>::coverage_branch_probe(true), - "enabled" - ); - assert_eq!( - ReplicaSql::<ProbeExecutor>::coverage_branch_probe(false), - "disabled" - ); - } -} diff --git a/crates/replica_db/src/models/farm.rs b/crates/replica_db/src/models/farm.rs @@ -1,161 +0,0 @@ -use radroots_replica_db_schema::farm::{ - Farm, FarmQueryBindValues, IFarmCreate, IFarmCreateResolve, IFarmDelete, IFarmDeleteResolve, - IFarmFieldsFilter, IFarmFindMany, IFarmFindManyResolve, IFarmFindOne, IFarmFindOneResolve, - IFarmUpdate, IFarmUpdateResolve, -}; -use radroots_replica_db_schema::{ - ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList, -}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "farm"; - -pub fn create( - exec: &dyn SqlExecutor, - opts: &IFarmCreate, -) -> Result<IFarmCreateResolve, ReplicaSchemaError<SqlError>> { - let field_map = utils::to_object_map(opts).expect("farm create fields serialize"); - 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).expect("farm create bind params serialize"); - let _ = exec.exec(&sql, &params_json)?; - let on = FarmQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)? - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_one( - exec: &dyn SqlExecutor, - opts: &IFarmFindOne, -) -> Result<IFarmFindOneResolve, ReplicaSchemaError<SqlError>> { - let result = match opts { - IFarmFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_many( - exec: &dyn SqlExecutor, - opts: &IFarmFindMany, -) -> Result<IFarmFindManyResolve, ReplicaSchemaError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(ReplicaSchemaResultList { results }) -} - -fn find_many_filter( - exec: &dyn SqlExecutor, - filter: &Option<IFarmFieldsFilter>, -) -> Result<Vec<Farm>, ReplicaSchemaError<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).expect("farm find_many bind params serialize"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<Farm> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on( - exec: &dyn SqlExecutor, - on: &FarmQueryBindValues, -) -> Result<Option<Farm>, ReplicaSchemaError<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]).expect("farm find_one bind params serialize"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<Farm> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id(exec: &dyn SqlExecutor, id: &str) -> Result<Farm, ReplicaSchemaError<SqlError>> { - let params_json = utils::to_params_json(vec![Value::from(id.to_owned())]) - .expect("farm select_by_id bind params serialize"); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<Farm> = utils::parse_json(&json)?; - rows.pop() - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update( - exec: &dyn SqlExecutor, - opts: &IFarmUpdate, -) -> Result<IFarmUpdateResolve, ReplicaSchemaError<SqlError>> { - let mut updates = - utils::to_partial_object_map(&opts.fields).expect("farm update fields serialize"); - if updates.is_empty() { - return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("farm update bind params serialize"); - let _ = exec.exec(&sql, &params_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(ReplicaSchemaResult { result: updated }) -} - -pub fn delete( - exec: &dyn SqlExecutor, - opts: &IFarmDelete, -) -> Result<IFarmDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( - args.on.lookup_key(), - )))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) - .expect("farm delete bind params serialize"); - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(ReplicaSchemaError::from(SqlError::NotFound( - id_for_lookup.clone(), - ))); - } - Ok(ReplicaSchemaResult { - 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 @@ -1,162 +0,0 @@ -use radroots_replica_db_schema::farm_gcs_location::{ - FarmGcsLocation, FarmGcsLocationQueryBindValues, IFarmGcsLocationCreate, - IFarmGcsLocationCreateResolve, IFarmGcsLocationDelete, IFarmGcsLocationDeleteResolve, - IFarmGcsLocationFieldsFilter, IFarmGcsLocationFindMany, IFarmGcsLocationFindManyResolve, - IFarmGcsLocationFindOne, IFarmGcsLocationFindOneResolve, IFarmGcsLocationUpdate, - IFarmGcsLocationUpdateResolve, -}; -use radroots_replica_db_schema::{ - ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList, -}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "farm_gcs_location"; - -pub fn create( - exec: &dyn SqlExecutor, - opts: &IFarmGcsLocationCreate, -) -> Result<IFarmGcsLocationCreateResolve, ReplicaSchemaError<SqlError>> { - let field_map = utils::to_object_map(opts).expect("serialize object map"); - 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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let on = FarmGcsLocationQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)? - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_one( - exec: &dyn SqlExecutor, - opts: &IFarmGcsLocationFindOne, -) -> Result<IFarmGcsLocationFindOneResolve, ReplicaSchemaError<SqlError>> { - let result = match opts { - IFarmGcsLocationFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_many( - exec: &dyn SqlExecutor, - opts: &IFarmGcsLocationFindMany, -) -> Result<IFarmGcsLocationFindManyResolve, ReplicaSchemaError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(ReplicaSchemaResultList { results }) -} - -fn find_many_filter( - exec: &dyn SqlExecutor, - filter: &Option<IFarmGcsLocationFieldsFilter>, -) -> Result<Vec<FarmGcsLocation>, ReplicaSchemaError<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).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<FarmGcsLocation> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on( - exec: &dyn SqlExecutor, - on: &FarmGcsLocationQueryBindValues, -) -> Result<Option<FarmGcsLocation>, ReplicaSchemaError<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]).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<FarmGcsLocation> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id( - exec: &dyn SqlExecutor, - id: &str, -) -> Result<FarmGcsLocation, ReplicaSchemaError<SqlError>> { - let params_json = - utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<FarmGcsLocation> = utils::parse_json(&json)?; - rows.pop() - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update( - exec: &dyn SqlExecutor, - opts: &IFarmGcsLocationUpdate, -) -> Result<IFarmGcsLocationUpdateResolve, ReplicaSchemaError<SqlError>> { - let mut updates = - utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); - if updates.is_empty() { - return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(ReplicaSchemaResult { result: updated }) -} - -pub fn delete( - exec: &dyn SqlExecutor, - opts: &IFarmGcsLocationDelete, -) -> Result<IFarmGcsLocationDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( - args.on.lookup_key(), - )))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) - .expect("serialize bind params"); - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(ReplicaSchemaError::from(SqlError::NotFound( - id_for_lookup.clone(), - ))); - } - Ok(ReplicaSchemaResult { - result: id_for_lookup, - }) -} diff --git a/crates/replica_db/src/models/farm_member.rs b/crates/replica_db/src/models/farm_member.rs @@ -1,161 +0,0 @@ -use radroots_replica_db_schema::farm_member::{ - FarmMember, FarmMemberQueryBindValues, IFarmMemberCreate, IFarmMemberCreateResolve, - IFarmMemberDelete, IFarmMemberDeleteResolve, IFarmMemberFieldsFilter, IFarmMemberFindMany, - IFarmMemberFindManyResolve, IFarmMemberFindOne, IFarmMemberFindOneResolve, IFarmMemberUpdate, - IFarmMemberUpdateResolve, -}; -use radroots_replica_db_schema::{ - ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList, -}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "farm_member"; - -pub fn create( - exec: &dyn SqlExecutor, - opts: &IFarmMemberCreate, -) -> Result<IFarmMemberCreateResolve, ReplicaSchemaError<SqlError>> { - let field_map = utils::to_object_map(opts).expect("serialize object map"); - 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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let on = FarmMemberQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)? - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_one( - exec: &dyn SqlExecutor, - opts: &IFarmMemberFindOne, -) -> Result<IFarmMemberFindOneResolve, ReplicaSchemaError<SqlError>> { - let result = match opts { - IFarmMemberFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_many( - exec: &dyn SqlExecutor, - opts: &IFarmMemberFindMany, -) -> Result<IFarmMemberFindManyResolve, ReplicaSchemaError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(ReplicaSchemaResultList { results }) -} - -fn find_many_filter( - exec: &dyn SqlExecutor, - filter: &Option<IFarmMemberFieldsFilter>, -) -> Result<Vec<FarmMember>, ReplicaSchemaError<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).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<FarmMember> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on( - exec: &dyn SqlExecutor, - on: &FarmMemberQueryBindValues, -) -> Result<Option<FarmMember>, ReplicaSchemaError<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]).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<FarmMember> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id( - exec: &dyn SqlExecutor, - id: &str, -) -> Result<FarmMember, ReplicaSchemaError<SqlError>> { - let params_json = - utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<FarmMember> = utils::parse_json(&json)?; - rows.pop() - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update( - exec: &dyn SqlExecutor, - opts: &IFarmMemberUpdate, -) -> Result<IFarmMemberUpdateResolve, ReplicaSchemaError<SqlError>> { - let mut updates = - utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); - if updates.is_empty() { - return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(ReplicaSchemaResult { result: updated }) -} - -pub fn delete( - exec: &dyn SqlExecutor, - opts: &IFarmMemberDelete, -) -> Result<IFarmMemberDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( - args.on.lookup_key(), - )))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) - .expect("serialize bind params"); - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(ReplicaSchemaError::from(SqlError::NotFound( - id_for_lookup.clone(), - ))); - } - Ok(ReplicaSchemaResult { - 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 @@ -1,162 +0,0 @@ -use radroots_replica_db_schema::farm_member_claim::{ - FarmMemberClaim, FarmMemberClaimQueryBindValues, IFarmMemberClaimCreate, - IFarmMemberClaimCreateResolve, IFarmMemberClaimDelete, IFarmMemberClaimDeleteResolve, - IFarmMemberClaimFieldsFilter, IFarmMemberClaimFindMany, IFarmMemberClaimFindManyResolve, - IFarmMemberClaimFindOne, IFarmMemberClaimFindOneResolve, IFarmMemberClaimUpdate, - IFarmMemberClaimUpdateResolve, -}; -use radroots_replica_db_schema::{ - ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList, -}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "farm_member_claim"; - -pub fn create( - exec: &dyn SqlExecutor, - opts: &IFarmMemberClaimCreate, -) -> Result<IFarmMemberClaimCreateResolve, ReplicaSchemaError<SqlError>> { - let field_map = utils::to_object_map(opts).expect("serialize object map"); - 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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let on = FarmMemberClaimQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)? - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_one( - exec: &dyn SqlExecutor, - opts: &IFarmMemberClaimFindOne, -) -> Result<IFarmMemberClaimFindOneResolve, ReplicaSchemaError<SqlError>> { - let result = match opts { - IFarmMemberClaimFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_many( - exec: &dyn SqlExecutor, - opts: &IFarmMemberClaimFindMany, -) -> Result<IFarmMemberClaimFindManyResolve, ReplicaSchemaError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(ReplicaSchemaResultList { results }) -} - -fn find_many_filter( - exec: &dyn SqlExecutor, - filter: &Option<IFarmMemberClaimFieldsFilter>, -) -> Result<Vec<FarmMemberClaim>, ReplicaSchemaError<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).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<FarmMemberClaim> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on( - exec: &dyn SqlExecutor, - on: &FarmMemberClaimQueryBindValues, -) -> Result<Option<FarmMemberClaim>, ReplicaSchemaError<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]).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<FarmMemberClaim> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id( - exec: &dyn SqlExecutor, - id: &str, -) -> Result<FarmMemberClaim, ReplicaSchemaError<SqlError>> { - let params_json = - utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<FarmMemberClaim> = utils::parse_json(&json)?; - rows.pop() - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update( - exec: &dyn SqlExecutor, - opts: &IFarmMemberClaimUpdate, -) -> Result<IFarmMemberClaimUpdateResolve, ReplicaSchemaError<SqlError>> { - let mut updates = - utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); - if updates.is_empty() { - return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(ReplicaSchemaResult { result: updated }) -} - -pub fn delete( - exec: &dyn SqlExecutor, - opts: &IFarmMemberClaimDelete, -) -> Result<IFarmMemberClaimDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( - args.on.lookup_key(), - )))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) - .expect("serialize bind params"); - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(ReplicaSchemaError::from(SqlError::NotFound( - id_for_lookup.clone(), - ))); - } - Ok(ReplicaSchemaResult { - result: id_for_lookup, - }) -} diff --git a/crates/replica_db/src/models/farm_tag.rs b/crates/replica_db/src/models/farm_tag.rs @@ -1,157 +0,0 @@ -use radroots_replica_db_schema::farm_tag::{ - FarmTag, FarmTagQueryBindValues, IFarmTagCreate, IFarmTagCreateResolve, IFarmTagDelete, - IFarmTagDeleteResolve, IFarmTagFieldsFilter, IFarmTagFindMany, IFarmTagFindManyResolve, - IFarmTagFindOne, IFarmTagFindOneResolve, IFarmTagUpdate, IFarmTagUpdateResolve, -}; -use radroots_replica_db_schema::{ - ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList, -}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "farm_tag"; - -pub fn create( - exec: &dyn SqlExecutor, - opts: &IFarmTagCreate, -) -> Result<IFarmTagCreateResolve, ReplicaSchemaError<SqlError>> { - let field_map = utils::to_object_map(opts).expect("serialize object map"); - 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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let on = FarmTagQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)? - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_one( - exec: &dyn SqlExecutor, - opts: &IFarmTagFindOne, -) -> Result<IFarmTagFindOneResolve, ReplicaSchemaError<SqlError>> { - let result = match opts { - IFarmTagFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_many( - exec: &dyn SqlExecutor, - opts: &IFarmTagFindMany, -) -> Result<IFarmTagFindManyResolve, ReplicaSchemaError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(ReplicaSchemaResultList { results }) -} - -fn find_many_filter( - exec: &dyn SqlExecutor, - filter: &Option<IFarmTagFieldsFilter>, -) -> Result<Vec<FarmTag>, ReplicaSchemaError<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).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<FarmTag> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on( - exec: &dyn SqlExecutor, - on: &FarmTagQueryBindValues, -) -> Result<Option<FarmTag>, ReplicaSchemaError<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]).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<FarmTag> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id(exec: &dyn SqlExecutor, id: &str) -> Result<FarmTag, ReplicaSchemaError<SqlError>> { - let params_json = - utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<FarmTag> = utils::parse_json(&json)?; - rows.pop() - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update( - exec: &dyn SqlExecutor, - opts: &IFarmTagUpdate, -) -> Result<IFarmTagUpdateResolve, ReplicaSchemaError<SqlError>> { - let mut updates = - utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); - if updates.is_empty() { - return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(ReplicaSchemaResult { result: updated }) -} - -pub fn delete( - exec: &dyn SqlExecutor, - opts: &IFarmTagDelete, -) -> Result<IFarmTagDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( - args.on.lookup_key(), - )))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) - .expect("serialize bind params"); - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(ReplicaSchemaError::from(SqlError::NotFound( - id_for_lookup.clone(), - ))); - } - Ok(ReplicaSchemaResult { - result: id_for_lookup, - }) -} diff --git a/crates/replica_db/src/models/gcs_location.rs b/crates/replica_db/src/models/gcs_location.rs @@ -1,240 +0,0 @@ -use radroots_replica_db_schema::gcs_location::{ - GcsLocation, GcsLocationFindManyRel, GcsLocationQueryBindValues, IGcsLocationCreate, - IGcsLocationCreateResolve, IGcsLocationDelete, IGcsLocationDeleteResolve, - IGcsLocationFieldsFilter, IGcsLocationFindMany, IGcsLocationFindManyResolve, - IGcsLocationFindOne, IGcsLocationFindOneResolve, IGcsLocationUpdate, IGcsLocationUpdateResolve, -}; -use radroots_replica_db_schema::{ - ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList, -}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "gcs_location"; - -pub fn create( - exec: &dyn SqlExecutor, - opts: &IGcsLocationCreate, -) -> Result<IGcsLocationCreateResolve, ReplicaSchemaError<SqlError>> { - let field_map = utils::to_object_map(opts).expect("serialize object map"); - 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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let on = GcsLocationQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)? - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_one( - exec: &dyn SqlExecutor, - opts: &IGcsLocationFindOne, -) -> Result<IGcsLocationFindOneResolve, ReplicaSchemaError<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(ReplicaSchemaResult { result }) -} - -pub fn find_many( - exec: &dyn SqlExecutor, - opts: &IGcsLocationFindMany, -) -> Result<IGcsLocationFindManyResolve, ReplicaSchemaError<SqlError>> { - let results = match opts { - IGcsLocationFindMany::Filter { filter } => find_many_filter(exec, filter)?, - IGcsLocationFindMany::Rel { rel } => find_many_by_rel(exec, rel)?, - }; - Ok(ReplicaSchemaResultList { results }) -} - -fn find_many_filter( - exec: &dyn SqlExecutor, - filter: &Option<IGcsLocationFieldsFilter>, -) -> Result<Vec<GcsLocation>, ReplicaSchemaError<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).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<GcsLocation> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on( - exec: &dyn SqlExecutor, - on: &GcsLocationQueryBindValues, -) -> Result<Option<GcsLocation>, ReplicaSchemaError<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]).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_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( - exec: &dyn SqlExecutor, - rel: &GcsLocationFindManyRel, -) -> Result<Option<GcsLocation>, ReplicaSchemaError<SqlError>> { - let (sql, bind_values) = rel_query(rel); - let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); - let sql = format!("{sql} LIMIT 1;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<GcsLocation> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn find_many_by_rel( - exec: &dyn SqlExecutor, - rel: &GcsLocationFindManyRel, -) -> Result<Vec<GcsLocation>, ReplicaSchemaError<SqlError>> { - let (sql, bind_values) = rel_query(rel); - let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); - let sql = format!("{sql};"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<GcsLocation> = utils::parse_json(&json)?; - Ok(rows) -} - -fn select_by_id( - exec: &dyn SqlExecutor, - id: &str, -) -> Result<GcsLocation, ReplicaSchemaError<SqlError>> { - let params_json = - utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<GcsLocation> = utils::parse_json(&json)?; - rows.pop() - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update( - exec: &dyn SqlExecutor, - opts: &IGcsLocationUpdate, -) -> Result<IGcsLocationUpdateResolve, ReplicaSchemaError<SqlError>> { - let mut updates = - utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); - if updates.is_empty() { - return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(ReplicaSchemaResult { result: updated }) -} - -pub fn delete( - exec: &dyn SqlExecutor, - opts: &IGcsLocationDelete, -) -> Result<IGcsLocationDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::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(ReplicaSchemaError::from(SqlError::NotFound( - rel_lookup_key(&args.rel), - )))?; - model.id - } - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) - .expect("serialize bind params"); - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(ReplicaSchemaError::from(SqlError::NotFound( - id_for_lookup.clone(), - ))); - } - Ok(ReplicaSchemaResult { - 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 @@ -1,161 +0,0 @@ -use radroots_replica_db_schema::log_error::{ - ILogErrorCreate, ILogErrorCreateResolve, ILogErrorDelete, ILogErrorDeleteResolve, - ILogErrorFieldsFilter, ILogErrorFindMany, ILogErrorFindManyResolve, ILogErrorFindOne, - ILogErrorFindOneResolve, ILogErrorUpdate, ILogErrorUpdateResolve, LogError, - LogErrorQueryBindValues, -}; -use radroots_replica_db_schema::{ - ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList, -}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "log_error"; - -pub fn create( - exec: &dyn SqlExecutor, - opts: &ILogErrorCreate, -) -> Result<ILogErrorCreateResolve, ReplicaSchemaError<SqlError>> { - let field_map = utils::to_object_map(opts).expect("serialize object map"); - 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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let on = LogErrorQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)? - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_one( - exec: &dyn SqlExecutor, - opts: &ILogErrorFindOne, -) -> Result<ILogErrorFindOneResolve, ReplicaSchemaError<SqlError>> { - let result = match opts { - ILogErrorFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_many( - exec: &dyn SqlExecutor, - opts: &ILogErrorFindMany, -) -> Result<ILogErrorFindManyResolve, ReplicaSchemaError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(ReplicaSchemaResultList { results }) -} - -fn find_many_filter( - exec: &dyn SqlExecutor, - filter: &Option<ILogErrorFieldsFilter>, -) -> Result<Vec<LogError>, ReplicaSchemaError<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).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<LogError> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on( - exec: &dyn SqlExecutor, - on: &LogErrorQueryBindValues, -) -> Result<Option<LogError>, ReplicaSchemaError<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]).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<LogError> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id( - exec: &dyn SqlExecutor, - id: &str, -) -> Result<LogError, ReplicaSchemaError<SqlError>> { - let params_json = - utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<LogError> = utils::parse_json(&json)?; - rows.pop() - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update( - exec: &dyn SqlExecutor, - opts: &ILogErrorUpdate, -) -> Result<ILogErrorUpdateResolve, ReplicaSchemaError<SqlError>> { - let mut updates = - utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); - if updates.is_empty() { - return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(ReplicaSchemaResult { result: updated }) -} - -pub fn delete( - exec: &dyn SqlExecutor, - opts: &ILogErrorDelete, -) -> Result<ILogErrorDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( - args.on.lookup_key(), - )))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) - .expect("serialize bind params"); - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(ReplicaSchemaError::from(SqlError::NotFound( - id_for_lookup.clone(), - ))); - } - Ok(ReplicaSchemaResult { - result: id_for_lookup, - }) -} diff --git a/crates/replica_db/src/models/media_image.rs b/crates/replica_db/src/models/media_image.rs @@ -1,220 +0,0 @@ -use radroots_replica_db_schema::media_image::{ - IMediaImageCreate, IMediaImageCreateResolve, IMediaImageDelete, IMediaImageDeleteResolve, - IMediaImageFieldsFilter, IMediaImageFindMany, IMediaImageFindManyResolve, IMediaImageFindOne, - IMediaImageFindOneResolve, IMediaImageUpdate, IMediaImageUpdateResolve, MediaImage, - MediaImageFindManyRel, MediaImageQueryBindValues, -}; -use radroots_replica_db_schema::{ - ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList, -}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "media_image"; - -pub fn create( - exec: &dyn SqlExecutor, - opts: &IMediaImageCreate, -) -> Result<IMediaImageCreateResolve, ReplicaSchemaError<SqlError>> { - let field_map = utils::to_object_map(opts).expect("serialize object map"); - 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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let on = MediaImageQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)? - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_one( - exec: &dyn SqlExecutor, - opts: &IMediaImageFindOne, -) -> Result<IMediaImageFindOneResolve, ReplicaSchemaError<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(ReplicaSchemaResult { result }) -} - -pub fn find_many( - exec: &dyn SqlExecutor, - opts: &IMediaImageFindMany, -) -> Result<IMediaImageFindManyResolve, ReplicaSchemaError<SqlError>> { - let results = match opts { - IMediaImageFindMany::Filter { filter } => find_many_filter(exec, filter)?, - IMediaImageFindMany::Rel { rel } => find_many_by_rel(exec, rel)?, - }; - Ok(ReplicaSchemaResultList { results }) -} - -fn find_many_filter( - exec: &dyn SqlExecutor, - filter: &Option<IMediaImageFieldsFilter>, -) -> Result<Vec<MediaImage>, ReplicaSchemaError<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).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<MediaImage> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on( - exec: &dyn SqlExecutor, - on: &MediaImageQueryBindValues, -) -> Result<Option<MediaImage>, ReplicaSchemaError<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]).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_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( - exec: &dyn SqlExecutor, - rel: &MediaImageFindManyRel, -) -> Result<Option<MediaImage>, ReplicaSchemaError<SqlError>> { - let (sql, bind_values) = rel_query(rel); - let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); - let sql = format!("{sql} LIMIT 1;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<MediaImage> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn find_many_by_rel( - exec: &dyn SqlExecutor, - rel: &MediaImageFindManyRel, -) -> Result<Vec<MediaImage>, ReplicaSchemaError<SqlError>> { - let (sql, bind_values) = rel_query(rel); - let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); - let sql = format!("{sql};"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<MediaImage> = utils::parse_json(&json)?; - Ok(rows) -} - -fn select_by_id( - exec: &dyn SqlExecutor, - id: &str, -) -> Result<MediaImage, ReplicaSchemaError<SqlError>> { - let params_json = - utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<MediaImage> = utils::parse_json(&json)?; - rows.pop() - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update( - exec: &dyn SqlExecutor, - opts: &IMediaImageUpdate, -) -> Result<IMediaImageUpdateResolve, ReplicaSchemaError<SqlError>> { - let mut updates = - utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); - if updates.is_empty() { - return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(ReplicaSchemaResult { result: updated }) -} - -pub fn delete( - exec: &dyn SqlExecutor, - opts: &IMediaImageDelete, -) -> Result<IMediaImageDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::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(ReplicaSchemaError::from(SqlError::NotFound( - rel_lookup_key(&args.rel), - )))?; - model.id - } - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) - .expect("serialize bind params"); - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(ReplicaSchemaError::from(SqlError::NotFound( - id_for_lookup.clone(), - ))); - } - Ok(ReplicaSchemaResult { - 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/replica_db/src/models/nostr_event_head.rs b/crates/replica_db/src/models/nostr_event_head.rs @@ -1,162 +0,0 @@ -use radroots_replica_db_schema::nostr_event_head::{ - INostrEventHeadCreate, INostrEventHeadCreateResolve, INostrEventHeadDelete, - INostrEventHeadDeleteResolve, INostrEventHeadFieldsFilter, INostrEventHeadFindMany, - INostrEventHeadFindManyResolve, INostrEventHeadFindOne, INostrEventHeadFindOneResolve, - INostrEventHeadUpdate, INostrEventHeadUpdateResolve, NostrEventHead, - NostrEventHeadQueryBindValues, -}; -use radroots_replica_db_schema::{ - ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList, -}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "nostr_event_head"; - -pub fn create( - exec: &dyn SqlExecutor, - opts: &INostrEventHeadCreate, -) -> Result<INostrEventHeadCreateResolve, ReplicaSchemaError<SqlError>> { - let field_map = utils::to_object_map(opts).expect("serialize object map"); - 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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let on = NostrEventHeadQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)? - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_one( - exec: &dyn SqlExecutor, - opts: &INostrEventHeadFindOne, -) -> Result<INostrEventHeadFindOneResolve, ReplicaSchemaError<SqlError>> { - let result = match opts { - INostrEventHeadFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_many( - exec: &dyn SqlExecutor, - opts: &INostrEventHeadFindMany, -) -> Result<INostrEventHeadFindManyResolve, ReplicaSchemaError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(ReplicaSchemaResultList { results }) -} - -fn find_many_filter( - exec: &dyn SqlExecutor, - filter: &Option<INostrEventHeadFieldsFilter>, -) -> Result<Vec<NostrEventHead>, ReplicaSchemaError<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).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<NostrEventHead> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on( - exec: &dyn SqlExecutor, - on: &NostrEventHeadQueryBindValues, -) -> Result<Option<NostrEventHead>, ReplicaSchemaError<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]).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<NostrEventHead> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id( - exec: &dyn SqlExecutor, - id: &str, -) -> Result<NostrEventHead, ReplicaSchemaError<SqlError>> { - let params_json = - utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<NostrEventHead> = utils::parse_json(&json)?; - rows.pop() - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update( - exec: &dyn SqlExecutor, - opts: &INostrEventHeadUpdate, -) -> Result<INostrEventHeadUpdateResolve, ReplicaSchemaError<SqlError>> { - let mut updates = - utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); - if updates.is_empty() { - return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(ReplicaSchemaResult { result: updated }) -} - -pub fn delete( - exec: &dyn SqlExecutor, - opts: &INostrEventHeadDelete, -) -> Result<INostrEventHeadDeleteResolve, ReplicaSchemaError<SqlError>> { - let id_for_lookup = match opts { - INostrEventHeadDelete::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(ReplicaSchemaError::from(SqlError::NotFound( - args.on.lookup_key(), - )))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) - .expect("serialize bind params"); - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(ReplicaSchemaError::from(SqlError::NotFound( - id_for_lookup.clone(), - ))); - } - Ok(ReplicaSchemaResult { - result: id_for_lookup, - }) -} diff --git a/crates/replica_db/src/models/nostr_profile.rs b/crates/replica_db/src/models/nostr_profile.rs @@ -1,217 +0,0 @@ -use radroots_replica_db_schema::nostr_profile::{ - INostrProfileCreate, INostrProfileCreateResolve, INostrProfileDelete, - INostrProfileDeleteResolve, INostrProfileFieldsFilter, INostrProfileFindMany, - INostrProfileFindManyResolve, INostrProfileFindOne, INostrProfileFindOneResolve, - INostrProfileUpdate, INostrProfileUpdateResolve, NostrProfile, NostrProfileFindManyRel, - NostrProfileQueryBindValues, -}; -use radroots_replica_db_schema::{ - ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList, -}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "nostr_profile"; - -pub fn create( - exec: &dyn SqlExecutor, - opts: &INostrProfileCreate, -) -> Result<INostrProfileCreateResolve, ReplicaSchemaError<SqlError>> { - let field_map = utils::to_object_map(opts).expect("serialize object map"); - 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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let on = NostrProfileQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)? - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_one( - exec: &dyn SqlExecutor, - opts: &INostrProfileFindOne, -) -> Result<INostrProfileFindOneResolve, ReplicaSchemaError<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(ReplicaSchemaResult { result }) -} - -pub fn find_many( - exec: &dyn SqlExecutor, - opts: &INostrProfileFindMany, -) -> Result<INostrProfileFindManyResolve, ReplicaSchemaError<SqlError>> { - let results = match opts { - INostrProfileFindMany::Filter { filter } => find_many_filter(exec, filter)?, - INostrProfileFindMany::Rel { rel } => find_many_by_rel(exec, rel)?, - }; - Ok(ReplicaSchemaResultList { results }) -} - -fn find_many_filter( - exec: &dyn SqlExecutor, - filter: &Option<INostrProfileFieldsFilter>, -) -> Result<Vec<NostrProfile>, ReplicaSchemaError<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).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<NostrProfile> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on( - exec: &dyn SqlExecutor, - on: &NostrProfileQueryBindValues, -) -> Result<Option<NostrProfile>, ReplicaSchemaError<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]).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_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( - exec: &dyn SqlExecutor, - rel: &NostrProfileFindManyRel, -) -> Result<Option<NostrProfile>, ReplicaSchemaError<SqlError>> { - let (sql, bind_values) = rel_query(rel); - let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); - let sql = format!("{sql} LIMIT 1;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<NostrProfile> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn find_many_by_rel( - exec: &dyn SqlExecutor, - rel: &NostrProfileFindManyRel, -) -> Result<Vec<NostrProfile>, ReplicaSchemaError<SqlError>> { - let (sql, bind_values) = rel_query(rel); - let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); - let sql = format!("{sql};"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<NostrProfile> = utils::parse_json(&json)?; - Ok(rows) -} - -fn select_by_id( - exec: &dyn SqlExecutor, - id: &str, -) -> Result<NostrProfile, ReplicaSchemaError<SqlError>> { - let params_json = - utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<NostrProfile> = utils::parse_json(&json)?; - rows.pop() - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update( - exec: &dyn SqlExecutor, - opts: &INostrProfileUpdate, -) -> Result<INostrProfileUpdateResolve, ReplicaSchemaError<SqlError>> { - let mut updates = - utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); - if updates.is_empty() { - return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(ReplicaSchemaResult { result: updated }) -} - -pub fn delete( - exec: &dyn SqlExecutor, - opts: &INostrProfileDelete, -) -> Result<INostrProfileDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::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(ReplicaSchemaError::from(SqlError::NotFound( - rel_lookup_key(&args.rel), - )))?; - model.id - } - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) - .expect("serialize bind params"); - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(ReplicaSchemaError::from(SqlError::NotFound( - id_for_lookup.clone(), - ))); - } - Ok(ReplicaSchemaResult { - 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 @@ -1,45 +0,0 @@ -use radroots_replica_db_schema::nostr_profile_relay::{ - INostrProfileRelayRelation, INostrProfileRelayResolve, -}; -use radroots_replica_db_schema::{ReplicaSchemaError, ReplicaSchemaResultPass}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "nostr_profile_relay"; - -pub fn set( - exec: &dyn SqlExecutor, - opts: &INostrProfileRelayRelation, -) -> Result<INostrProfileRelayResolve, ReplicaSchemaError<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).expect("serialize bind params"); - let _ = exec.exec(&query, &params_json)?; - Ok(ReplicaSchemaResultPass { pass: true }) -} - -pub fn unset( - exec: &dyn SqlExecutor, - opts: &INostrProfileRelayRelation, -) -> Result<INostrProfileRelayResolve, ReplicaSchemaError<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).expect("serialize bind params"); - let _ = exec.exec(&query, &params_json)?; - Ok(ReplicaSchemaResultPass { pass: true }) -} diff --git a/crates/replica_db/src/models/nostr_relay.rs b/crates/replica_db/src/models/nostr_relay.rs @@ -1,220 +0,0 @@ -use radroots_replica_db_schema::nostr_relay::{ - INostrRelayCreate, INostrRelayCreateResolve, INostrRelayDelete, INostrRelayDeleteResolve, - INostrRelayFieldsFilter, INostrRelayFindMany, INostrRelayFindManyResolve, INostrRelayFindOne, - INostrRelayFindOneResolve, INostrRelayUpdate, INostrRelayUpdateResolve, NostrRelay, - NostrRelayFindManyRel, NostrRelayQueryBindValues, -}; -use radroots_replica_db_schema::{ - ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList, -}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "nostr_relay"; - -pub fn create( - exec: &dyn SqlExecutor, - opts: &INostrRelayCreate, -) -> Result<INostrRelayCreateResolve, ReplicaSchemaError<SqlError>> { - let field_map = utils::to_object_map(opts).expect("serialize object map"); - 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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let on = NostrRelayQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)? - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_one( - exec: &dyn SqlExecutor, - opts: &INostrRelayFindOne, -) -> Result<INostrRelayFindOneResolve, ReplicaSchemaError<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(ReplicaSchemaResult { result }) -} - -pub fn find_many( - exec: &dyn SqlExecutor, - opts: &INostrRelayFindMany, -) -> Result<INostrRelayFindManyResolve, ReplicaSchemaError<SqlError>> { - let results = match opts { - INostrRelayFindMany::Filter { filter } => find_many_filter(exec, filter)?, - INostrRelayFindMany::Rel { rel } => find_many_by_rel(exec, rel)?, - }; - Ok(ReplicaSchemaResultList { results }) -} - -fn find_many_filter( - exec: &dyn SqlExecutor, - filter: &Option<INostrRelayFieldsFilter>, -) -> Result<Vec<NostrRelay>, ReplicaSchemaError<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).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<NostrRelay> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on( - exec: &dyn SqlExecutor, - on: &NostrRelayQueryBindValues, -) -> Result<Option<NostrRelay>, ReplicaSchemaError<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]).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_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( - exec: &dyn SqlExecutor, - rel: &NostrRelayFindManyRel, -) -> Result<Option<NostrRelay>, ReplicaSchemaError<SqlError>> { - let (sql, bind_values) = rel_query(rel); - let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); - let sql = format!("{sql} LIMIT 1;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<NostrRelay> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn find_many_by_rel( - exec: &dyn SqlExecutor, - rel: &NostrRelayFindManyRel, -) -> Result<Vec<NostrRelay>, ReplicaSchemaError<SqlError>> { - let (sql, bind_values) = rel_query(rel); - let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); - let sql = format!("{sql};"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<NostrRelay> = utils::parse_json(&json)?; - Ok(rows) -} - -fn select_by_id( - exec: &dyn SqlExecutor, - id: &str, -) -> Result<NostrRelay, ReplicaSchemaError<SqlError>> { - let params_json = - utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<NostrRelay> = utils::parse_json(&json)?; - rows.pop() - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update( - exec: &dyn SqlExecutor, - opts: &INostrRelayUpdate, -) -> Result<INostrRelayUpdateResolve, ReplicaSchemaError<SqlError>> { - let mut updates = - utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); - if updates.is_empty() { - return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(ReplicaSchemaResult { result: updated }) -} - -pub fn delete( - exec: &dyn SqlExecutor, - opts: &INostrRelayDelete, -) -> Result<INostrRelayDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::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(ReplicaSchemaError::from(SqlError::NotFound( - rel_lookup_key(&args.rel), - )))?; - model.id - } - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) - .expect("serialize bind params"); - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(ReplicaSchemaError::from(SqlError::NotFound( - id_for_lookup.clone(), - ))); - } - Ok(ReplicaSchemaResult { - 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 @@ -1,157 +0,0 @@ -use radroots_replica_db_schema::plot::{ - IPlotCreate, IPlotCreateResolve, IPlotDelete, IPlotDeleteResolve, IPlotFieldsFilter, - IPlotFindMany, IPlotFindManyResolve, IPlotFindOne, IPlotFindOneResolve, IPlotUpdate, - IPlotUpdateResolve, Plot, PlotQueryBindValues, -}; -use radroots_replica_db_schema::{ - ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList, -}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "plot"; - -pub fn create( - exec: &dyn SqlExecutor, - opts: &IPlotCreate, -) -> Result<IPlotCreateResolve, ReplicaSchemaError<SqlError>> { - let field_map = utils::to_object_map(opts).expect("serialize object map"); - 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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let on = PlotQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)? - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_one( - exec: &dyn SqlExecutor, - opts: &IPlotFindOne, -) -> Result<IPlotFindOneResolve, ReplicaSchemaError<SqlError>> { - let result = match opts { - IPlotFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_many( - exec: &dyn SqlExecutor, - opts: &IPlotFindMany, -) -> Result<IPlotFindManyResolve, ReplicaSchemaError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(ReplicaSchemaResultList { results }) -} - -fn find_many_filter( - exec: &dyn SqlExecutor, - filter: &Option<IPlotFieldsFilter>, -) -> Result<Vec<Plot>, ReplicaSchemaError<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).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<Plot> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on( - exec: &dyn SqlExecutor, - on: &PlotQueryBindValues, -) -> Result<Option<Plot>, ReplicaSchemaError<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]).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<Plot> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id(exec: &dyn SqlExecutor, id: &str) -> Result<Plot, ReplicaSchemaError<SqlError>> { - let params_json = - utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<Plot> = utils::parse_json(&json)?; - rows.pop() - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update( - exec: &dyn SqlExecutor, - opts: &IPlotUpdate, -) -> Result<IPlotUpdateResolve, ReplicaSchemaError<SqlError>> { - let mut updates = - utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); - if updates.is_empty() { - return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(ReplicaSchemaResult { result: updated }) -} - -pub fn delete( - exec: &dyn SqlExecutor, - opts: &IPlotDelete, -) -> Result<IPlotDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( - args.on.lookup_key(), - )))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) - .expect("serialize bind params"); - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(ReplicaSchemaError::from(SqlError::NotFound( - id_for_lookup.clone(), - ))); - } - Ok(ReplicaSchemaResult { - 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 @@ -1,162 +0,0 @@ -use radroots_replica_db_schema::plot_gcs_location::{ - IPlotGcsLocationCreate, IPlotGcsLocationCreateResolve, IPlotGcsLocationDelete, - IPlotGcsLocationDeleteResolve, IPlotGcsLocationFieldsFilter, IPlotGcsLocationFindMany, - IPlotGcsLocationFindManyResolve, IPlotGcsLocationFindOne, IPlotGcsLocationFindOneResolve, - IPlotGcsLocationUpdate, IPlotGcsLocationUpdateResolve, PlotGcsLocation, - PlotGcsLocationQueryBindValues, -}; -use radroots_replica_db_schema::{ - ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList, -}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "plot_gcs_location"; - -pub fn create( - exec: &dyn SqlExecutor, - opts: &IPlotGcsLocationCreate, -) -> Result<IPlotGcsLocationCreateResolve, ReplicaSchemaError<SqlError>> { - let field_map = utils::to_object_map(opts).expect("serialize object map"); - 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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let on = PlotGcsLocationQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)? - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_one( - exec: &dyn SqlExecutor, - opts: &IPlotGcsLocationFindOne, -) -> Result<IPlotGcsLocationFindOneResolve, ReplicaSchemaError<SqlError>> { - let result = match opts { - IPlotGcsLocationFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_many( - exec: &dyn SqlExecutor, - opts: &IPlotGcsLocationFindMany, -) -> Result<IPlotGcsLocationFindManyResolve, ReplicaSchemaError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(ReplicaSchemaResultList { results }) -} - -fn find_many_filter( - exec: &dyn SqlExecutor, - filter: &Option<IPlotGcsLocationFieldsFilter>, -) -> Result<Vec<PlotGcsLocation>, ReplicaSchemaError<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).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<PlotGcsLocation> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on( - exec: &dyn SqlExecutor, - on: &PlotGcsLocationQueryBindValues, -) -> Result<Option<PlotGcsLocation>, ReplicaSchemaError<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]).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<PlotGcsLocation> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id( - exec: &dyn SqlExecutor, - id: &str, -) -> Result<PlotGcsLocation, ReplicaSchemaError<SqlError>> { - let params_json = - utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<PlotGcsLocation> = utils::parse_json(&json)?; - rows.pop() - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update( - exec: &dyn SqlExecutor, - opts: &IPlotGcsLocationUpdate, -) -> Result<IPlotGcsLocationUpdateResolve, ReplicaSchemaError<SqlError>> { - let mut updates = - utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); - if updates.is_empty() { - return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(ReplicaSchemaResult { result: updated }) -} - -pub fn delete( - exec: &dyn SqlExecutor, - opts: &IPlotGcsLocationDelete, -) -> Result<IPlotGcsLocationDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( - args.on.lookup_key(), - )))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) - .expect("serialize bind params"); - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(ReplicaSchemaError::from(SqlError::NotFound( - id_for_lookup.clone(), - ))); - } - Ok(ReplicaSchemaResult { - result: id_for_lookup, - }) -} diff --git a/crates/replica_db/src/models/plot_tag.rs b/crates/replica_db/src/models/plot_tag.rs @@ -1,157 +0,0 @@ -use radroots_replica_db_schema::plot_tag::{ - IPlotTagCreate, IPlotTagCreateResolve, IPlotTagDelete, IPlotTagDeleteResolve, - IPlotTagFieldsFilter, IPlotTagFindMany, IPlotTagFindManyResolve, IPlotTagFindOne, - IPlotTagFindOneResolve, IPlotTagUpdate, IPlotTagUpdateResolve, PlotTag, PlotTagQueryBindValues, -}; -use radroots_replica_db_schema::{ - ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList, -}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "plot_tag"; - -pub fn create( - exec: &dyn SqlExecutor, - opts: &IPlotTagCreate, -) -> Result<IPlotTagCreateResolve, ReplicaSchemaError<SqlError>> { - let field_map = utils::to_object_map(opts).expect("serialize object map"); - 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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let on = PlotTagQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)? - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_one( - exec: &dyn SqlExecutor, - opts: &IPlotTagFindOne, -) -> Result<IPlotTagFindOneResolve, ReplicaSchemaError<SqlError>> { - let result = match opts { - IPlotTagFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_many( - exec: &dyn SqlExecutor, - opts: &IPlotTagFindMany, -) -> Result<IPlotTagFindManyResolve, ReplicaSchemaError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(ReplicaSchemaResultList { results }) -} - -fn find_many_filter( - exec: &dyn SqlExecutor, - filter: &Option<IPlotTagFieldsFilter>, -) -> Result<Vec<PlotTag>, ReplicaSchemaError<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).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<PlotTag> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on( - exec: &dyn SqlExecutor, - on: &PlotTagQueryBindValues, -) -> Result<Option<PlotTag>, ReplicaSchemaError<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]).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<PlotTag> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id(exec: &dyn SqlExecutor, id: &str) -> Result<PlotTag, ReplicaSchemaError<SqlError>> { - let params_json = - utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<PlotTag> = utils::parse_json(&json)?; - rows.pop() - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update( - exec: &dyn SqlExecutor, - opts: &IPlotTagUpdate, -) -> Result<IPlotTagUpdateResolve, ReplicaSchemaError<SqlError>> { - let mut updates = - utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); - if updates.is_empty() { - return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(ReplicaSchemaResult { result: updated }) -} - -pub fn delete( - exec: &dyn SqlExecutor, - opts: &IPlotTagDelete, -) -> Result<IPlotTagDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( - args.on.lookup_key(), - )))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) - .expect("serialize bind params"); - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(ReplicaSchemaError::from(SqlError::NotFound( - id_for_lookup.clone(), - ))); - } - Ok(ReplicaSchemaResult { - result: id_for_lookup, - }) -} diff --git a/crates/replica_db/src/models/trade_product.rs b/crates/replica_db/src/models/trade_product.rs @@ -1,147 +0,0 @@ -use radroots_replica_db_schema::trade_product::{ - ITradeProductCreate, ITradeProductCreateResolve, ITradeProductDelete, - ITradeProductDeleteResolve, ITradeProductFieldsFilter, ITradeProductFindMany, - ITradeProductFindManyResolve, ITradeProductFindOne, ITradeProductFindOneResolve, - ITradeProductUpdate, ITradeProductUpdateResolve, TradeProduct, TradeProductQueryBindValues, -}; -use radroots_replica_db_schema::{ - ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList, -}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "trade_product"; - -pub fn create( - exec: &dyn SqlExecutor, - opts: &ITradeProductCreate, -) -> Result<ITradeProductCreateResolve, ReplicaSchemaError<SqlError>> { - let field_map = utils::to_object_map(opts).expect("serialize object map"); - 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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let on = TradeProductQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)? - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_one( - exec: &dyn SqlExecutor, - opts: &ITradeProductFindOne, -) -> Result<ITradeProductFindOneResolve, ReplicaSchemaError<SqlError>> { - let result = match opts { - ITradeProductFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(ReplicaSchemaResult { result }) -} - -pub fn find_many( - exec: &dyn SqlExecutor, - opts: &ITradeProductFindMany, -) -> Result<ITradeProductFindManyResolve, ReplicaSchemaError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(ReplicaSchemaResultList { results }) -} - -fn find_many_filter( - exec: &dyn SqlExecutor, - filter: &Option<ITradeProductFieldsFilter>, -) -> Result<Vec<TradeProduct>, ReplicaSchemaError<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).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let rows: Vec<TradeProduct> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on( - exec: &dyn SqlExecutor, - on: &TradeProductQueryBindValues, -) -> Result<Option<TradeProduct>, ReplicaSchemaError<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]).expect("serialize bind params"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<TradeProduct> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id( - exec: &dyn SqlExecutor, - id: &str, -) -> Result<TradeProduct, ReplicaSchemaError<SqlError>> { - let params_json = - utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<TradeProduct> = utils::parse_json(&json)?; - rows.pop() - .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update( - exec: &dyn SqlExecutor, - opts: &ITradeProductUpdate, -) -> Result<ITradeProductUpdateResolve, ReplicaSchemaError<SqlError>> { - let mut updates = - utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); - if updates.is_empty() { - return Err(ReplicaSchemaError::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).expect("serialize bind params"); - let _ = exec.exec(&sql, &params_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(ReplicaSchemaResult { result: updated }) -} - -pub fn delete( - exec: &dyn SqlExecutor, - opts: &ITradeProductDelete, -) -> Result<ITradeProductDeleteResolve, ReplicaSchemaError<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())]) - .expect("serialize bind params"); - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, &params_json)?; - if outcome.changes == 0 { - return Err(ReplicaSchemaError::from(SqlError::NotFound( - id_for_lookup.clone(), - ))); - } - Ok(ReplicaSchemaResult { - 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 @@ -1,45 +0,0 @@ -use radroots_replica_db_schema::trade_product_location::{ - ITradeProductLocationRelation, ITradeProductLocationResolve, -}; -use radroots_replica_db_schema::{ReplicaSchemaError, ReplicaSchemaResultPass}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "trade_product_location"; - -pub fn set( - exec: &dyn SqlExecutor, - opts: &ITradeProductLocationRelation, -) -> Result<ITradeProductLocationResolve, ReplicaSchemaError<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).expect("serialize bind params"); - let _ = exec.exec(&query, &params_json)?; - Ok(ReplicaSchemaResultPass { pass: true }) -} - -pub fn unset( - exec: &dyn SqlExecutor, - opts: &ITradeProductLocationRelation, -) -> Result<ITradeProductLocationResolve, ReplicaSchemaError<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).expect("serialize bind params"); - let _ = exec.exec(&query, &params_json)?; - Ok(ReplicaSchemaResultPass { pass: true }) -} diff --git a/crates/replica_db/src/models/trade_product_media.rs b/crates/replica_db/src/models/trade_product_media.rs @@ -1,45 +0,0 @@ -use radroots_replica_db_schema::trade_product_media::{ - ITradeProductMediaRelation, ITradeProductMediaResolve, -}; -use radroots_replica_db_schema::{ReplicaSchemaError, ReplicaSchemaResultPass}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use serde_json::Value; - -const TABLE_NAME: &str = "trade_product_media"; - -pub fn set( - exec: &dyn SqlExecutor, - opts: &ITradeProductMediaRelation, -) -> Result<ITradeProductMediaResolve, ReplicaSchemaError<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).expect("serialize bind params"); - let _ = exec.exec(&query, &params_json)?; - Ok(ReplicaSchemaResultPass { pass: true }) -} - -pub fn unset( - exec: &dyn SqlExecutor, - opts: &ITradeProductMediaRelation, -) -> Result<ITradeProductMediaResolve, ReplicaSchemaError<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).expect("serialize bind params"); - let _ = exec.exec(&query, &params_json)?; - Ok(ReplicaSchemaResultPass { pass: true }) -} diff --git a/crates/replica_db/tests/backup_export_paths.rs b/crates/replica_db/tests/backup_export_paths.rs @@ -1,305 +0,0 @@ -use std::collections::VecDeque; -use std::sync::Mutex; - -use radroots_replica_db::backup::{ - DATABASE_BACKUP_VERSION, DatabaseBackup, REPLICA_DB_VERSION, SchemaEntry, TableData, - export_database_backup, export_database_backup_json, restore_database_backup, -}; -use radroots_replica_db::export::export_manifest; -use radroots_replica_db::{ExecOutcome, SqlError, SqlExecutor}; -use serde_json::{Map, Value, json}; - -struct PatternExecutor { - query_rules: Vec<(String, String)>, - fail_query_contains: Option<String>, - fail_exec_contains: Option<String>, - fail_begin: bool, - fail_commit: bool, - begin_count: Mutex<usize>, - commit_count: Mutex<usize>, - rollback_count: Mutex<usize>, - query_queue: Mutex<VecDeque<Result<String, SqlError>>>, - exec_queue: Mutex<VecDeque<Result<ExecOutcome, SqlError>>>, -} - -impl PatternExecutor { - fn new() -> Self { - Self { - query_rules: Vec::new(), - fail_query_contains: None, - fail_exec_contains: None, - fail_begin: false, - fail_commit: false, - begin_count: Mutex::new(0), - commit_count: Mutex::new(0), - rollback_count: Mutex::new(0), - query_queue: Mutex::new(VecDeque::new()), - exec_queue: Mutex::new(VecDeque::new()), - } - } - - fn with_query_rule(mut self, needle: &str, response: &str) -> Self { - self.query_rules - .push((needle.to_string(), response.to_string())); - self - } - - fn with_query_failure(mut self, needle: &str) -> Self { - self.fail_query_contains = Some(needle.to_string()); - self - } - - fn with_exec_failure(mut self, needle: &str) -> Self { - self.fail_exec_contains = Some(needle.to_string()); - self - } - - fn with_begin_failure(mut self) -> Self { - self.fail_begin = true; - self - } - - fn with_commit_failure(mut self) -> Self { - self.fail_commit = true; - self - } -} - -impl SqlExecutor for PatternExecutor { - fn exec(&self, sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> { - if let Some(result) = self.exec_queue.lock().expect("exec queue lock").pop_front() { - return result; - } - if let Some(needle) = &self.fail_exec_contains - && 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> { - if let Some(result) = self - .query_queue - .lock() - .expect("query queue lock") - .pop_front() - { - return result; - } - if let Some(needle) = &self.fail_query_contains - && sql.contains(needle) - { - return Err(SqlError::InvalidQuery(String::from("forced query failure"))); - } - 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_count.lock().expect("begin count lock") += 1; - if self.fail_begin { - return Err(SqlError::InvalidQuery(String::from("forced begin failure"))); - } - Ok(()) - } - - fn commit(&self) -> Result<(), SqlError> { - *self.commit_count.lock().expect("commit count lock") += 1; - if self.fail_commit { - return Err(SqlError::InvalidQuery(String::from( - "forced commit failure", - ))); - } - Ok(()) - } - - fn rollback(&self) -> Result<(), SqlError> { - *self.rollback_count.lock().expect("rollback count lock") += 1; - Ok(()) - } -} - -fn assert_sql_error_code<T: core::fmt::Debug>(result: Result<T, SqlError>, code: &str) { - let err = result.unwrap_err(); - assert_eq!(err.code(), code); -} - -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 backup_public_api_error_paths_cover_library_instantiations() { - let schema_query = "select type, name, tbl_name as table_name, sql from sqlite_master"; - - let executor = PatternExecutor::new().with_query_failure(schema_query); - assert_sql_error_code(export_database_backup(&executor), "ERR_INVALID_QUERY"); - - let schema_rows = json!([ - { - "type": "table", - "name": "tb_a", - "table_name": "tb_a", - "sql": "CREATE TABLE tb_a (id TEXT);" - } - ]) - .to_string(); - let executor = PatternExecutor::new() - .with_query_rule(schema_query, &schema_rows) - .with_query_failure("SELECT * FROM \"tb_a\";"); - assert_sql_error_code(export_database_backup(&executor), "ERR_INVALID_QUERY"); - - let executor = PatternExecutor::new().with_query_failure(schema_query); - assert_sql_error_code(export_database_backup_json(&executor), "ERR_INVALID_QUERY"); - - let executor = PatternExecutor::new().with_query_rule(schema_query, "[]"); - let backup_json = export_database_backup_json(&executor).expect("backup json success"); - assert!(backup_json.contains("\"schema\":[]")); - - let executor = PatternExecutor::new(); - let backup = backup_with_versions("0.0.1", REPLICA_DB_VERSION); - assert_sql_error_code( - restore_database_backup(&executor, &backup), - "ERR_INVALID_ARGUMENT", - ); - - let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_DB_VERSION); - let executor = PatternExecutor::new().with_exec_failure("PRAGMA foreign_keys = OFF;"); - assert_sql_error_code( - restore_database_backup(&executor, &backup), - "ERR_INVALID_QUERY", - ); - - let executor = PatternExecutor::new().with_begin_failure(); - assert_sql_error_code( - restore_database_backup(&executor, &backup), - "ERR_INVALID_QUERY", - ); - - let executor = - PatternExecutor::new().with_query_failure("select type, name from sqlite_master"); - assert_sql_error_code( - restore_database_backup(&executor, &backup), - "ERR_INVALID_QUERY", - ); - - let executor = PatternExecutor::new().with_commit_failure(); - assert_sql_error_code( - restore_database_backup(&executor, &backup), - "ERR_INVALID_QUERY", - ); - - let executor = PatternExecutor::new().with_exec_failure("PRAGMA foreign_keys = ON;"); - assert_sql_error_code( - restore_database_backup(&executor, &backup), - "ERR_INVALID_QUERY", - ); -} - -#[test] -fn backup_public_api_insert_and_parse_failures_cover_library_instantiations() { - let backup = DatabaseBackup { - format_version: DATABASE_BACKUP_VERSION.to_string(), - replica_db_version: REPLICA_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);")), - }], - migrations: Vec::new(), - data: vec![TableData { - name: String::from("tb_a"), - rows: vec![{ - let mut row = Map::new(); - row.insert(String::from("id"), Value::from("1")); - row - }], - }], - }; - - let executor = PatternExecutor::new().with_exec_failure("INSERT INTO \"tb_a\""); - assert_sql_error_code( - restore_database_backup(&executor, &backup), - "ERR_INVALID_QUERY", - ); - - let schema_query = "select type, name, tbl_name as table_name, sql from sqlite_master"; - let executor = PatternExecutor::new().with_query_rule(schema_query, "{"); - assert_sql_error_code(export_database_backup(&executor), "ERR_SERIALIZATION"); - - let schema_rows = json!([ - { - "type": "table", - "name": "tb_a", - "table_name": "tb_a", - "sql": "CREATE TABLE tb_a (id TEXT);" - } - ]) - .to_string(); - let executor = PatternExecutor::new() - .with_query_rule(schema_query, &schema_rows) - .with_query_rule("SELECT * FROM \"tb_a\";", "{"); - assert_sql_error_code(export_database_backup(&executor), "ERR_SERIALIZATION"); - - let null_name_rows = json!([ - { - "type": "table", - "name": null, - "table_name": "tb_a", - "sql": "CREATE TABLE tb_a (id TEXT);" - } - ]) - .to_string(); - let executor = PatternExecutor::new().with_query_rule(schema_query, &null_name_rows); - let backup = export_database_backup(&executor).expect("backup with null-name schema rows"); - assert!(backup.schema.is_empty()); -} - -#[test] -fn export_manifest_public_api_error_paths_cover_library_instantiations() { - let schema_query = "select type, name, tbl_name as table_name, sql from sqlite_master"; - - let executor = PatternExecutor::new().with_query_failure(schema_query); - assert_sql_error_code(export_manifest(&executor), "ERR_INVALID_QUERY"); - - let schema_rows = json!([ - { - "type": "table", - "name": "tb_a", - "table_name": "tb_a", - "sql": "CREATE TABLE tb_a (id TEXT);" - } - ]) - .to_string(); - - let executor = PatternExecutor::new() - .with_query_rule(schema_query, &schema_rows) - .with_query_failure("select count(1) as count from \"tb_a\""); - assert_sql_error_code(export_manifest(&executor), "ERR_INVALID_QUERY"); - - let executor = PatternExecutor::new() - .with_query_rule(schema_query, &schema_rows) - .with_query_rule("select count(1) as count from \"tb_a\"", "{"); - assert_sql_error_code(export_manifest(&executor), "ERR_SERIALIZATION"); - - let executor = PatternExecutor::new() - .with_query_rule(schema_query, &schema_rows) - .with_query_rule("select count(1) as count from \"tb_a\"", "[]"); - let manifest = export_manifest(&executor).expect("manifest success"); - assert_eq!(manifest.table_counts.len(), 1); -} diff --git a/crates/replica_db/tests/error_paths.rs b/crates/replica_db/tests/error_paths.rs @@ -1,977 +0,0 @@ -use radroots_replica_db::ReplicaSql; -use radroots_replica_db_schema::ReplicaSchemaError; -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_event_head::{ - INostrEventHeadCreate, INostrEventHeadDelete, INostrEventHeadFindMany, INostrEventHeadFindOne, - INostrEventHeadUpdate, -}; -use radroots_replica_db_schema::nostr_profile::{ - INostrProfileCreate, INostrProfileDelete, INostrProfileFindMany, INostrProfileFindOne, - INostrProfileUpdate, -}; -use radroots_replica_db_schema::nostr_profile_relay::INostrProfileRelayRelation; -use radroots_replica_db_schema::nostr_relay::{ - INostrRelayCreate, INostrRelayDelete, INostrRelayFindMany, INostrRelayFindOne, - INostrRelayUpdate, -}; -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_sql_core::{SqlError, SqlExecutor, SqliteExecutor}; -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 open_db() -> ReplicaSql<SqliteExecutor> { - let exec = SqliteExecutor::open_memory().expect("open sqlite memory"); - let db = ReplicaSql::new(exec); - db.migrate_up().expect("migrate up"); - db -} - -fn drop_table(db: &ReplicaSql<SqliteExecutor>, table_name: &str) { - let sql = format!("DROP TABLE {table_name};"); - db.executor().exec(&sql, "[]").expect("drop table"); -} - -fn assert_invalid_query<T>(result: Result<T, ReplicaSchemaError<SqlError>>) { - let err = match result { - Ok(_) => panic!("invalid query expected"), - Err(err) => err, - }; - assert!(matches!(err.err, SqlError::InvalidQuery(_))); -} - -fn assert_not_found<T>(result: Result<T, ReplicaSchemaError<SqlError>>) { - let err = match result { - Ok(_) => panic!("not found expected"), - Err(err) => err, - }; - assert!(matches!(err.err, SqlError::NotFound(_))); -} - -#[test] -fn gcs_location_error_paths_cover_regions() { - let db = open_db(); - - let update_missing: IGcsLocationUpdate = parse_json(json!({ - "on": { "d_tag": "missing-gcs" }, - "fields": { "label": "x" } - })); - assert_not_found(db.gcs_location_update(&update_missing)); - - let delete_missing_on: IGcsLocationDelete = parse_json(json!({ - "on": { "d_tag": "missing-gcs" } - })); - assert_not_found(db.gcs_location_delete(&delete_missing_on)); - - let delete_missing_rel: IGcsLocationDelete = parse_json(json!({ - "rel": { "off_plot": { "id": "missing-plot" } } - })); - assert_not_found(db.gcs_location_delete(&delete_missing_rel)); - - drop_table(&db, "gcs_location"); - - let create_opts: 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))" - })); - assert_invalid_query(db.gcs_location_create(&create_opts)); - - let find_many_filter: IGcsLocationFindMany = parse_json(json!({ - "filter": { "id": "id-1" } - })); - assert_invalid_query(db.gcs_location_find_many(&find_many_filter)); - - let find_many_rel: IGcsLocationFindMany = parse_json(json!({ - "rel": { "on_farm": { "id": "farm-1" } } - })); - assert_invalid_query(db.gcs_location_find_many(&find_many_rel)); - - let find_one_on: IGcsLocationFindOne = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.gcs_location_find_one(&find_one_on)); - - let find_one_rel: IGcsLocationFindOne = parse_json(json!({ - "rel": { "off_trade_product": { "id": "tp-1" } } - })); - assert_invalid_query(db.gcs_location_find_one(&find_one_rel)); - - let update_id: IGcsLocationUpdate = parse_json(json!({ - "on": { "id": "id-1" }, - "fields": { "label": "x" } - })); - assert_invalid_query(db.gcs_location_update(&update_id)); - - let delete_id: IGcsLocationDelete = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.gcs_location_delete(&delete_id)); - - let delete_rel: IGcsLocationDelete = parse_json(json!({ - "rel": { "on_plot": { "id": "plot-1" } } - })); - assert_invalid_query(db.gcs_location_delete(&delete_rel)); -} - -#[test] -fn media_image_error_paths_cover_regions() { - let db = open_db(); - - let update_missing: IMediaImageUpdate = parse_json(json!({ - "on": { "file_path": "/missing.jpg" }, - "fields": { "label": "x" } - })); - assert_not_found(db.media_image_update(&update_missing)); - - let delete_missing_on: IMediaImageDelete = parse_json(json!({ - "on": { "file_path": "/missing.jpg" } - })); - assert_not_found(db.media_image_delete(&delete_missing_on)); - - let delete_missing_rel: IMediaImageDelete = parse_json(json!({ - "rel": { "off_trade_product": { "id": "missing-tp" } } - })); - assert_not_found(db.media_image_delete(&delete_missing_rel)); - - drop_table(&db, "media_image"); - - let create_opts: IMediaImageCreate = parse_json(json!({ - "file_path": "/img/a.jpg", - "mime_type": "image/jpeg", - "res_base": "https://cdn.example.com", - "res_path": "img/a.jpg" - })); - assert_invalid_query(db.media_image_create(&create_opts)); - - let find_many_filter: IMediaImageFindMany = parse_json(json!({ - "filter": { "id": "id-1" } - })); - assert_invalid_query(db.media_image_find_many(&find_many_filter)); - - let find_many_rel: IMediaImageFindMany = parse_json(json!({ - "rel": { "on_trade_product": { "id": "tp-1" } } - })); - assert_invalid_query(db.media_image_find_many(&find_many_rel)); - - let find_one_on: IMediaImageFindOne = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.media_image_find_one(&find_one_on)); - - let find_one_rel: IMediaImageFindOne = parse_json(json!({ - "rel": { "off_trade_product": { "id": "tp-1" } } - })); - assert_invalid_query(db.media_image_find_one(&find_one_rel)); - - let update_id: IMediaImageUpdate = parse_json(json!({ - "on": { "id": "id-1" }, - "fields": { "label": "x" } - })); - assert_invalid_query(db.media_image_update(&update_id)); - - let delete_id: IMediaImageDelete = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.media_image_delete(&delete_id)); - - let delete_rel: IMediaImageDelete = parse_json(json!({ - "rel": { "on_trade_product": { "id": "tp-1" } } - })); - assert_invalid_query(db.media_image_delete(&delete_rel)); -} - -#[test] -fn nostr_profile_error_paths_cover_regions() { - let db = open_db(); - - let update_missing: INostrProfileUpdate = parse_json(json!({ - "on": { "public_key": hex64('a') }, - "fields": { "name": "x" } - })); - assert_not_found(db.nostr_profile_update(&update_missing)); - - let delete_missing_on: INostrProfileDelete = parse_json(json!({ - "on": { "public_key": hex64('a') } - })); - assert_not_found(db.nostr_profile_delete(&delete_missing_on)); - - let delete_missing_rel: INostrProfileDelete = parse_json(json!({ - "rel": { "off_relay": { "id": "missing-relay" } } - })); - assert_not_found(db.nostr_profile_delete(&delete_missing_rel)); - - drop_table(&db, "nostr_profile"); - - let create_opts: INostrProfileCreate = parse_json(json!({ - "public_key": hex64('d'), - "profile_type": "farm", - "name": "profile a" - })); - assert_invalid_query(db.nostr_profile_create(&create_opts)); - - let find_many_filter: INostrProfileFindMany = parse_json(json!({ - "filter": { "id": "id-1" } - })); - assert_invalid_query(db.nostr_profile_find_many(&find_many_filter)); - - let find_many_rel: INostrProfileFindMany = parse_json(json!({ - "rel": { "on_relay": { "id": "relay-1" } } - })); - assert_invalid_query(db.nostr_profile_find_many(&find_many_rel)); - - let find_one_on: INostrProfileFindOne = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.nostr_profile_find_one(&find_one_on)); - - let find_one_rel: INostrProfileFindOne = parse_json(json!({ - "rel": { "off_relay": { "id": "relay-1" } } - })); - assert_invalid_query(db.nostr_profile_find_one(&find_one_rel)); - - let update_id: INostrProfileUpdate = parse_json(json!({ - "on": { "id": "id-1" }, - "fields": { "name": "x" } - })); - assert_invalid_query(db.nostr_profile_update(&update_id)); - - let delete_id: INostrProfileDelete = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.nostr_profile_delete(&delete_id)); - - let delete_rel: INostrProfileDelete = parse_json(json!({ - "rel": { "on_relay": { "id": "relay-1" } } - })); - assert_invalid_query(db.nostr_profile_delete(&delete_rel)); -} - -#[test] -fn nostr_relay_error_paths_cover_regions() { - let db = open_db(); - - let update_missing: INostrRelayUpdate = parse_json(json!({ - "on": { "url": "wss://missing.example.com" }, - "fields": { "name": "x" } - })); - assert_not_found(db.nostr_relay_update(&update_missing)); - - let delete_missing_on: INostrRelayDelete = parse_json(json!({ - "on": { "url": "wss://missing.example.com" } - })); - assert_not_found(db.nostr_relay_delete(&delete_missing_on)); - - let delete_missing_rel: INostrRelayDelete = parse_json(json!({ - "rel": { "off_profile": { "public_key": hex64('b') } } - })); - assert_not_found(db.nostr_relay_delete(&delete_missing_rel)); - - drop_table(&db, "nostr_relay"); - - let create_opts: INostrRelayCreate = parse_json(json!({ - "url": "wss://relay.example.com" - })); - assert_invalid_query(db.nostr_relay_create(&create_opts)); - - let find_many_filter: INostrRelayFindMany = parse_json(json!({ - "filter": { "id": "id-1" } - })); - assert_invalid_query(db.nostr_relay_find_many(&find_many_filter)); - - let find_many_rel: INostrRelayFindMany = parse_json(json!({ - "rel": { "on_profile": { "public_key": hex64('d') } } - })); - assert_invalid_query(db.nostr_relay_find_many(&find_many_rel)); - - let find_one_on: INostrRelayFindOne = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.nostr_relay_find_one(&find_one_on)); - - let find_one_rel: INostrRelayFindOne = parse_json(json!({ - "rel": { "off_profile": { "public_key": hex64('d') } } - })); - assert_invalid_query(db.nostr_relay_find_one(&find_one_rel)); - - let update_id: INostrRelayUpdate = parse_json(json!({ - "on": { "id": "id-1" }, - "fields": { "name": "x" } - })); - assert_invalid_query(db.nostr_relay_update(&update_id)); - - let delete_id: INostrRelayDelete = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.nostr_relay_delete(&delete_id)); - - let delete_rel: INostrRelayDelete = parse_json(json!({ - "rel": { "on_profile": { "public_key": hex64('d') } } - })); - assert_invalid_query(db.nostr_relay_delete(&delete_rel)); -} - -#[test] -fn farm_error_paths_cover_regions() { - let db = open_db(); - - let update_missing: IFarmUpdate = parse_json(json!({ - "on": { "d_tag": "missing-farm" }, - "fields": { "name": "farm x" } - })); - assert_not_found(db.farm_update(&update_missing)); - - let update_missing_id: IFarmUpdate = parse_json(json!({ - "on": { "id": "missing-id" }, - "fields": { "name": "farm y" } - })); - assert_not_found(db.farm_update(&update_missing_id)); - - let delete_missing_on: IFarmDelete = parse_json(json!({ - "on": { "d_tag": "missing-farm" } - })); - assert_not_found(db.farm_delete(&delete_missing_on)); - - drop_table(&db, "farm"); - - let create_opts: IFarmCreate = parse_json(json!({ - "d_tag": "farm-a", - "pubkey": hex64('a'), - "name": "farm a" - })); - assert_invalid_query(db.farm_create(&create_opts)); - - let find_many_filter: IFarmFindMany = parse_json(json!({ - "filter": { "id": "id-1" } - })); - assert_invalid_query(db.farm_find_many(&find_many_filter)); - - let find_one_on: IFarmFindOne = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.farm_find_one(&find_one_on)); - - let update_id: IFarmUpdate = parse_json(json!({ - "on": { "id": "id-1" }, - "fields": { "name": "farm z" } - })); - assert_invalid_query(db.farm_update(&update_id)); - - let delete_id: IFarmDelete = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.farm_delete(&delete_id)); -} - -#[test] -fn plot_error_paths_cover_regions() { - let db = open_db(); - - let update_missing: IPlotUpdate = parse_json(json!({ - "on": { "d_tag": "missing-plot" }, - "fields": { "name": "plot x" } - })); - assert_not_found(db.plot_update(&update_missing)); - - let update_missing_id: IPlotUpdate = parse_json(json!({ - "on": { "id": "missing-id" }, - "fields": { "name": "plot y" } - })); - assert_not_found(db.plot_update(&update_missing_id)); - - let delete_missing_on: IPlotDelete = parse_json(json!({ - "on": { "d_tag": "missing-plot" } - })); - assert_not_found(db.plot_delete(&delete_missing_on)); - - drop_table(&db, "plot"); - - let create_opts: IPlotCreate = parse_json(json!({ - "d_tag": "plot-a", - "farm_id": "farm-1", - "name": "plot a" - })); - assert_invalid_query(db.plot_create(&create_opts)); - - let find_many_filter: IPlotFindMany = parse_json(json!({ - "filter": { "id": "id-1" } - })); - assert_invalid_query(db.plot_find_many(&find_many_filter)); - - let find_one_on: IPlotFindOne = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.plot_find_one(&find_one_on)); - - let update_id: IPlotUpdate = parse_json(json!({ - "on": { "id": "id-1" }, - "fields": { "name": "plot z" } - })); - assert_invalid_query(db.plot_update(&update_id)); - - let delete_id: IPlotDelete = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.plot_delete(&delete_id)); -} - -#[test] -fn farm_gcs_location_error_paths_cover_regions() { - let db = open_db(); - - let update_missing: IFarmGcsLocationUpdate = parse_json(json!({ - "on": { "farm_id": "farm-1" }, - "fields": { "role": "x" } - })); - assert_not_found(db.farm_gcs_location_update(&update_missing)); - - let update_missing_id: IFarmGcsLocationUpdate = parse_json(json!({ - "on": { "id": "missing-id" }, - "fields": { "role": "y" } - })); - assert_not_found(db.farm_gcs_location_update(&update_missing_id)); - - let delete_missing_on: IFarmGcsLocationDelete = parse_json(json!({ - "on": { "farm_id": "farm-1" } - })); - assert_not_found(db.farm_gcs_location_delete(&delete_missing_on)); - - drop_table(&db, "farm_gcs_location"); - - let create_opts: IFarmGcsLocationCreate = parse_json(json!({ - "farm_id": "farm-1", - "gcs_location_id": "gcs-1", - "role": "primary" - })); - assert_invalid_query(db.farm_gcs_location_create(&create_opts)); - - let find_many_filter: IFarmGcsLocationFindMany = parse_json(json!({ - "filter": { "id": "id-1" } - })); - assert_invalid_query(db.farm_gcs_location_find_many(&find_many_filter)); - - let find_one_on: IFarmGcsLocationFindOne = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.farm_gcs_location_find_one(&find_one_on)); - - let update_id: IFarmGcsLocationUpdate = parse_json(json!({ - "on": { "id": "id-1" }, - "fields": { "role": "z" } - })); - assert_invalid_query(db.farm_gcs_location_update(&update_id)); - - let delete_id: IFarmGcsLocationDelete = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.farm_gcs_location_delete(&delete_id)); -} - -#[test] -fn plot_gcs_location_error_paths_cover_regions() { - let db = open_db(); - - let update_missing: IPlotGcsLocationUpdate = parse_json(json!({ - "on": { "plot_id": "plot-1" }, - "fields": { "role": "x" } - })); - assert_not_found(db.plot_gcs_location_update(&update_missing)); - - let update_missing_id: IPlotGcsLocationUpdate = parse_json(json!({ - "on": { "id": "missing-id" }, - "fields": { "role": "y" } - })); - assert_not_found(db.plot_gcs_location_update(&update_missing_id)); - - let delete_missing_on: IPlotGcsLocationDelete = parse_json(json!({ - "on": { "plot_id": "plot-1" } - })); - assert_not_found(db.plot_gcs_location_delete(&delete_missing_on)); - - drop_table(&db, "plot_gcs_location"); - - let create_opts: IPlotGcsLocationCreate = parse_json(json!({ - "plot_id": "plot-1", - "gcs_location_id": "gcs-1", - "role": "primary" - })); - assert_invalid_query(db.plot_gcs_location_create(&create_opts)); - - let find_many_filter: IPlotGcsLocationFindMany = parse_json(json!({ - "filter": { "id": "id-1" } - })); - assert_invalid_query(db.plot_gcs_location_find_many(&find_many_filter)); - - let find_one_on: IPlotGcsLocationFindOne = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.plot_gcs_location_find_one(&find_one_on)); - - let update_id: IPlotGcsLocationUpdate = parse_json(json!({ - "on": { "id": "id-1" }, - "fields": { "role": "z" } - })); - assert_invalid_query(db.plot_gcs_location_update(&update_id)); - - let delete_id: IPlotGcsLocationDelete = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.plot_gcs_location_delete(&delete_id)); -} - -#[test] -fn farm_tag_error_paths_cover_regions() { - let db = open_db(); - - let update_missing: IFarmTagUpdate = parse_json(json!({ - "on": { "farm_id": "farm-1" }, - "fields": { "tag": "x" } - })); - assert_not_found(db.farm_tag_update(&update_missing)); - - let update_missing_id: IFarmTagUpdate = parse_json(json!({ - "on": { "id": "missing-id" }, - "fields": { "tag": "y" } - })); - assert_not_found(db.farm_tag_update(&update_missing_id)); - - let delete_missing_on: IFarmTagDelete = parse_json(json!({ - "on": { "farm_id": "farm-1" } - })); - assert_not_found(db.farm_tag_delete(&delete_missing_on)); - - drop_table(&db, "farm_tag"); - - let create_opts: IFarmTagCreate = parse_json(json!({ - "farm_id": "farm-1", - "tag": "organic" - })); - assert_invalid_query(db.farm_tag_create(&create_opts)); - - let find_many_filter: IFarmTagFindMany = parse_json(json!({ - "filter": { "id": "id-1" } - })); - assert_invalid_query(db.farm_tag_find_many(&find_many_filter)); - - let find_one_on: IFarmTagFindOne = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.farm_tag_find_one(&find_one_on)); - - let update_id: IFarmTagUpdate = parse_json(json!({ - "on": { "id": "id-1" }, - "fields": { "tag": "z" } - })); - assert_invalid_query(db.farm_tag_update(&update_id)); - - let delete_id: IFarmTagDelete = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.farm_tag_delete(&delete_id)); -} - -#[test] -fn plot_tag_error_paths_cover_regions() { - let db = open_db(); - - let update_missing: IPlotTagUpdate = parse_json(json!({ - "on": { "plot_id": "plot-1" }, - "fields": { "tag": "x" } - })); - assert_not_found(db.plot_tag_update(&update_missing)); - - let update_missing_id: IPlotTagUpdate = parse_json(json!({ - "on": { "id": "missing-id" }, - "fields": { "tag": "y" } - })); - assert_not_found(db.plot_tag_update(&update_missing_id)); - - let delete_missing_on: IPlotTagDelete = parse_json(json!({ - "on": { "plot_id": "plot-1" } - })); - assert_not_found(db.plot_tag_delete(&delete_missing_on)); - - drop_table(&db, "plot_tag"); - - let create_opts: IPlotTagCreate = parse_json(json!({ - "plot_id": "plot-1", - "tag": "north" - })); - assert_invalid_query(db.plot_tag_create(&create_opts)); - - let find_many_filter: IPlotTagFindMany = parse_json(json!({ - "filter": { "id": "id-1" } - })); - assert_invalid_query(db.plot_tag_find_many(&find_many_filter)); - - let find_one_on: IPlotTagFindOne = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.plot_tag_find_one(&find_one_on)); - - let update_id: IPlotTagUpdate = parse_json(json!({ - "on": { "id": "id-1" }, - "fields": { "tag": "z" } - })); - assert_invalid_query(db.plot_tag_update(&update_id)); - - let delete_id: IPlotTagDelete = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.plot_tag_delete(&delete_id)); -} - -#[test] -fn farm_member_error_paths_cover_regions() { - let db = open_db(); - - let update_missing: IFarmMemberUpdate = parse_json(json!({ - "on": { "member_pubkey": hex64('a') }, - "fields": { "role": "x" } - })); - assert_not_found(db.farm_member_update(&update_missing)); - - let update_missing_id: IFarmMemberUpdate = parse_json(json!({ - "on": { "id": "missing-id" }, - "fields": { "role": "y" } - })); - assert_not_found(db.farm_member_update(&update_missing_id)); - - let delete_missing_on: IFarmMemberDelete = parse_json(json!({ - "on": { "member_pubkey": hex64('a') } - })); - assert_not_found(db.farm_member_delete(&delete_missing_on)); - - drop_table(&db, "farm_member"); - - let create_opts: IFarmMemberCreate = parse_json(json!({ - "farm_id": "farm-1", - "member_pubkey": hex64('a'), - "role": "owner" - })); - assert_invalid_query(db.farm_member_create(&create_opts)); - - let find_many_filter: IFarmMemberFindMany = parse_json(json!({ - "filter": { "id": "id-1" } - })); - assert_invalid_query(db.farm_member_find_many(&find_many_filter)); - - let find_one_on: IFarmMemberFindOne = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.farm_member_find_one(&find_one_on)); - - let update_id: IFarmMemberUpdate = parse_json(json!({ - "on": { "id": "id-1" }, - "fields": { "role": "z" } - })); - assert_invalid_query(db.farm_member_update(&update_id)); - - let delete_id: IFarmMemberDelete = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.farm_member_delete(&delete_id)); -} - -#[test] -fn farm_member_claim_error_paths_cover_regions() { - let db = open_db(); - - let update_missing: IFarmMemberClaimUpdate = parse_json(json!({ - "on": { "member_pubkey": hex64('a') }, - "fields": { "farm_pubkey": hex64('b') } - })); - assert_not_found(db.farm_member_claim_update(&update_missing)); - - let update_missing_id: IFarmMemberClaimUpdate = parse_json(json!({ - "on": { "id": "missing-id" }, - "fields": { "farm_pubkey": hex64('c') } - })); - assert_not_found(db.farm_member_claim_update(&update_missing_id)); - - let delete_missing_on: IFarmMemberClaimDelete = parse_json(json!({ - "on": { "member_pubkey": hex64('a') } - })); - assert_not_found(db.farm_member_claim_delete(&delete_missing_on)); - - drop_table(&db, "farm_member_claim"); - - let create_opts: IFarmMemberClaimCreate = parse_json(json!({ - "member_pubkey": hex64('a'), - "farm_pubkey": hex64('b') - })); - assert_invalid_query(db.farm_member_claim_create(&create_opts)); - - let find_many_filter: IFarmMemberClaimFindMany = parse_json(json!({ - "filter": { "id": "id-1" } - })); - assert_invalid_query(db.farm_member_claim_find_many(&find_many_filter)); - - let find_one_on: IFarmMemberClaimFindOne = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.farm_member_claim_find_one(&find_one_on)); - - let update_id: IFarmMemberClaimUpdate = parse_json(json!({ - "on": { "id": "id-1" }, - "fields": { "farm_pubkey": hex64('c') } - })); - assert_invalid_query(db.farm_member_claim_update(&update_id)); - - let delete_id: IFarmMemberClaimDelete = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.farm_member_claim_delete(&delete_id)); -} - -#[test] -fn log_error_error_paths_cover_regions() { - let db = open_db(); - - let update_missing: ILogErrorUpdate = parse_json(json!({ - "on": { "nostr_pubkey": hex64('a') }, - "fields": { "message": "x" } - })); - assert_not_found(db.log_error_update(&update_missing)); - - let update_missing_id: ILogErrorUpdate = parse_json(json!({ - "on": { "id": "missing-id" }, - "fields": { "message": "y" } - })); - assert_not_found(db.log_error_update(&update_missing_id)); - - let delete_missing_on: ILogErrorDelete = parse_json(json!({ - "on": { "nostr_pubkey": hex64('a') } - })); - assert_not_found(db.log_error_delete(&delete_missing_on)); - - drop_table(&db, "log_error"); - - let create_opts: ILogErrorCreate = parse_json(json!({ - "error": "panic", - "message": "boom", - "app_system": "studio", - "app_version": "1.0.0", - "nostr_pubkey": hex64('a') - })); - assert_invalid_query(db.log_error_create(&create_opts)); - - let find_many_filter: ILogErrorFindMany = parse_json(json!({ - "filter": { "id": "id-1" } - })); - assert_invalid_query(db.log_error_find_many(&find_many_filter)); - - let find_one_on: ILogErrorFindOne = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.log_error_find_one(&find_one_on)); - - let update_id: ILogErrorUpdate = parse_json(json!({ - "on": { "id": "id-1" }, - "fields": { "message": "z" } - })); - assert_invalid_query(db.log_error_update(&update_id)); - - let delete_id: ILogErrorDelete = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.log_error_delete(&delete_id)); -} - -#[test] -fn nostr_event_head_error_paths_cover_regions() { - let db = open_db(); - - let update_missing: INostrEventHeadUpdate = parse_json(json!({ - "on": { "key": "state-a" }, - "fields": { "content_hash": "hash-x" } - })); - assert_not_found(db.nostr_event_head_update(&update_missing)); - - let update_missing_id: INostrEventHeadUpdate = parse_json(json!({ - "on": { "id": "missing-id" }, - "fields": { "content_hash": "hash-y" } - })); - assert_not_found(db.nostr_event_head_update(&update_missing_id)); - - let delete_missing_on: INostrEventHeadDelete = parse_json(json!({ - "on": { "key": "state-a" } - })); - assert_not_found(db.nostr_event_head_delete(&delete_missing_on)); - - drop_table(&db, "nostr_event_head"); - - let create_opts: INostrEventHeadCreate = parse_json(json!({ - "key": "state-a", - "kind": 30023, - "pubkey": hex64('a'), - "d_tag": "listing-a", - "last_event_id": hex64('b'), - "last_created_at": 1, - "content_hash": "hash-a" - })); - assert_invalid_query(db.nostr_event_head_create(&create_opts)); - - let find_many_filter: INostrEventHeadFindMany = parse_json(json!({ - "filter": { "id": "id-1" } - })); - assert_invalid_query(db.nostr_event_head_find_many(&find_many_filter)); - - let find_one_on: INostrEventHeadFindOne = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.nostr_event_head_find_one(&find_one_on)); - - let update_id: INostrEventHeadUpdate = parse_json(json!({ - "on": { "id": "id-1" }, - "fields": { "content_hash": "hash-z" } - })); - assert_invalid_query(db.nostr_event_head_update(&update_id)); - - let delete_id: INostrEventHeadDelete = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.nostr_event_head_delete(&delete_id)); -} - -#[test] -fn trade_product_error_paths_cover_regions() { - let db = open_db(); - - let update_missing: ITradeProductUpdate = parse_json(json!({ - "on": { "id": "missing-id" }, - "fields": { "title": "x" } - })); - assert_not_found(db.trade_product_update(&update_missing)); - - let delete_missing_on: ITradeProductDelete = parse_json(json!({ - "on": { "id": "missing-id" } - })); - assert_not_found(db.trade_product_delete(&delete_missing_on)); - - drop_table(&db, "trade_product"); - - let create_opts: 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_amt_exact": "100", - "qty_unit": "kg", - "price_amt": 7.5, - "price_amt_exact": "7.5", - "price_currency": "USD", - "price_qty_amt": 1, - "price_qty_amt_exact": "1", - "price_qty_unit": "kg" - })); - assert_invalid_query(db.trade_product_create(&create_opts)); - - let find_many_filter: ITradeProductFindMany = parse_json(json!({ - "filter": { "id": "id-1" } - })); - assert_invalid_query(db.trade_product_find_many(&find_many_filter)); - - let find_one_on: ITradeProductFindOne = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.trade_product_find_one(&find_one_on)); - - let update_id: ITradeProductUpdate = parse_json(json!({ - "on": { "id": "id-1" }, - "fields": { "title": "z" } - })); - assert_invalid_query(db.trade_product_update(&update_id)); - - let delete_id: ITradeProductDelete = parse_json(json!({ - "on": { "id": "id-1" } - })); - assert_invalid_query(db.trade_product_delete(&delete_id)); -} - -#[test] -fn relation_set_unset_error_paths_cover_regions() { - let db = open_db(); - - drop_table(&db, "nostr_profile_relay"); - let profile_relay_rel: INostrProfileRelayRelation = parse_json(json!({ - "nostr_profile": { "id": "profile-1" }, - "nostr_relay": { "id": "relay-1" } - })); - assert_invalid_query(db.nostr_profile_relay_set(&profile_relay_rel)); - assert_invalid_query(db.nostr_profile_relay_unset(&profile_relay_rel)); - - drop_table(&db, "trade_product_location"); - let product_location_rel: ITradeProductLocationRelation = parse_json(json!({ - "trade_product": { "id": "product-1" }, - "gcs_location": { "id": "gcs-1" } - })); - assert_invalid_query(db.trade_product_location_set(&product_location_rel)); - assert_invalid_query(db.trade_product_location_unset(&product_location_rel)); - - drop_table(&db, "trade_product_media"); - let product_media_rel: ITradeProductMediaRelation = parse_json(json!({ - "trade_product": { "id": "product-1" }, - "media_image": { "id": "media-1" } - })); - assert_invalid_query(db.trade_product_media_set(&product_media_rel)); - assert_invalid_query(db.trade_product_media_unset(&product_media_rel)); -} diff --git a/crates/replica_db/tests/full_mode.rs b/crates/replica_db/tests/full_mode.rs @@ -1,1350 +0,0 @@ -use radroots_replica_db::{ReplicaSql, export_manifest}; -use radroots_replica_db_schema::ReplicaSchemaError; -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_head::{ - INostrEventHeadCreate, INostrEventHeadDelete, INostrEventHeadFindMany, INostrEventHeadFindOne, - INostrEventHeadUpdate, -}; -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_sql_core::{SqlError, SqliteExecutor}; -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, ReplicaSchemaError<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, ReplicaSchemaError<SqlError>>) { - let err = match result { - Ok(_) => panic!("not found expected"), - Err(err) => err, - }; - assert!(matches!(err.err, SqlError::NotFound(_))); -} - -fn open_db() -> ReplicaSql<SqliteExecutor> { - let exec = SqliteExecutor::open_memory().expect("open sqlite memory"); - let db = ReplicaSql::new(exec); - db.migrate_up().expect("migrate up"); - db -} - -#[test] -fn full_mode_shaped_query_helpers_cover_cli_reads() { - let db = open_db(); - - let farm: IFarmCreate = parse_json(json!({ - "d_tag": "farm-a", - "pubkey": hex64('a'), - "name": "farm a" - })); - db.farm_create(&farm).expect("farm create"); - - 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))", - "label": "stockholm" - })); - let gcs_created = db - .gcs_location_create(&gcs_location) - .expect("gcs create") - .result; - let listing_addr = format!("30402:{}:listing-a", hex64('d')); - - let trade_product: ITradeProductCreate = parse_json(json!({ - "key": "product-a", - "category": "coffee", - "title": "coffee a", - "summary": "washed lot", - "process": "washed", - "lot": "lot-a", - "profile": "floral", - "year": 2024, - "qty_amt": 100, - "qty_amt_exact": "100", - "qty_unit": "kg", - "qty_label": "bags", - "qty_avail": 2, - "price_amt": 7.5, - "price_amt_exact": "7.5", - "price_currency": "USD", - "price_qty_amt": 1, - "price_qty_amt_exact": "1", - "price_qty_unit": "kg", - "listing_addr": listing_addr.clone(), - "primary_bin_id": "bin-a", - "verified_primary_bin_id": "bin-a", - "notes": "fresh coffee" - })); - let trade_product_created = db - .trade_product_create(&trade_product) - .expect("trade product create") - .result; - - 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 nostr_event_head: INostrEventHeadCreate = parse_json(json!({ - "key": "state-a", - "kind": 30023, - "pubkey": hex64('d'), - "d_tag": "listing-a", - "last_event_id": hex64('e'), - "last_created_at": 42, - "content_hash": "hash-a" - })); - db.nostr_event_head_create(&nostr_event_head) - .expect("nostr event state create"); - - let rows = db - .trade_product_search(&["coffee".to_owned()]) - .expect("trade product search"); - assert_eq!(rows.len(), 1); - assert_eq!(rows[0].key, "product-a"); - assert_eq!(rows[0].listing_addr.as_deref(), Some(listing_addr.as_str())); - assert_eq!(rows[0].primary_bin_id.as_deref(), Some("bin-a")); - assert_eq!(rows[0].verified_primary_bin_id.as_deref(), Some("bin-a")); - assert_eq!(rows[0].location_primary.as_deref(), Some("stockholm")); - - let lookup_rows = db - .trade_product_lookup("product-a") - .expect("trade product lookup"); - assert_eq!(lookup_rows.len(), 1); - assert_eq!(lookup_rows[0].id, trade_product_created.id); - assert_eq!( - lookup_rows[0].listing_addr.as_deref(), - Some(listing_addr.as_str()) - ); - assert_eq!(lookup_rows[0].primary_bin_id.as_deref(), Some("bin-a")); - assert_eq!( - lookup_rows[0].verified_primary_bin_id.as_deref(), - Some("bin-a") - ); - - assert_eq!( - db.trade_product_search(&[]).expect("empty search"), - Vec::new() - ); - assert_eq!( - db.farm_unique_d_tag_by_pubkey(hex64('a').as_str()) - .expect("farm unique d tag"), - Some("farm-a".to_owned()) - ); - assert_eq!( - db.nostr_event_last_created_at() - .expect("nostr event freshness"), - Some(42) - ); -} - -#[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_head: INostrEventHeadCreate = 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_head_created = db - .nostr_event_head_create(&nostr_event_head) - .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_amt_exact": "100", - "qty_unit": "kg", - "price_amt": 7.5, - "price_amt_exact": "7.5", - "price_currency": "USD", - "price_qty_amt": 1, - "price_qty_amt_exact": "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_head_find_many: INostrEventHeadFindMany = - parse_json(json!({ "filter": { "id": nostr_event_head_created.id } })); - assert_eq!( - db.nostr_event_head_find_many(&nostr_event_head_find_many) - .expect("nostr event state find many") - .results - .len(), - 1 - ); - let nostr_event_head_find_one: INostrEventHeadFindOne = - parse_json(json!({ "on": { "id": nostr_event_head_created.id } })); - assert!( - db.nostr_event_head_find_one(&nostr_event_head_find_one) - .expect("nostr event state find one") - .result - .is_some() - ); - let nostr_event_head_update_alt: INostrEventHeadUpdate = - parse_json(json!({ "on": { "key": "state-a" }, "fields": { "content_hash": "hash-b" } })); - assert_eq!( - db.nostr_event_head_update(&nostr_event_head_update_alt) - .expect("nostr event state update") - .result - .content_hash, - "hash-b" - ); - let nostr_event_head_update_id: INostrEventHeadUpdate = parse_json( - json!({ "on": { "id": nostr_event_head_created.id }, "fields": { "content_hash": "hash-c" } }), - ); - assert_eq!( - db.nostr_event_head_update(&nostr_event_head_update_id) - .expect("nostr event state update id") - .result - .content_hash, - "hash-c" - ); - let nostr_event_head_update_empty: INostrEventHeadUpdate = - parse_json(json!({ "on": { "id": nostr_event_head_created.id }, "fields": {} })); - assert_invalid_argument(db.nostr_event_head_update(&nostr_event_head_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 } })); - let trade_product_results = db - .trade_product_find_many(&trade_product_find_many) - .expect("trade product find many") - .results; - assert_eq!(trade_product_results.len(), 1); - assert_eq!(trade_product_results[0].listing_addr, None); - 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_head_delete: INostrEventHeadDelete = - parse_json(json!({ "on": { "key": "state-a" } })); - db.nostr_event_head_delete(&nostr_event_head_delete) - .expect("nostr event state delete"); - let nostr_event_head_delete_missing: INostrEventHeadDelete = - parse_json(json!({ "on": { "id": nostr_event_head_created.id } })); - assert_not_found(db.nostr_event_head_delete(&nostr_event_head_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_db/tests/migration_repairs.rs b/crates/replica_db/tests/migration_repairs.rs @@ -1,104 +0,0 @@ -use radroots_replica_db::migrations; -use radroots_sql_core::{SqlExecutor, SqliteExecutor}; -use serde_json::Value; - -fn query_rows(exec: &SqliteExecutor, sql: &str) -> Vec<Value> { - serde_json::from_str(&exec.query_raw(sql, "[]").expect("query should succeed")) - .expect("query should decode") -} - -fn create_legacy_schema_without_secondary_indexes(exec: &SqliteExecutor) { - let schema = [ - "CREATE TABLE __migrations (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, applied_at TEXT NOT NULL DEFAULT (datetime('now')))", - "CREATE TABLE farm (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), d_tag TEXT NOT NULL, pubkey TEXT NOT NULL, name TEXT NOT NULL, about TEXT, website TEXT, picture TEXT, banner TEXT, location_primary TEXT, location_city TEXT, location_region TEXT, location_country TEXT)", - "CREATE TABLE gcs_location (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), d_tag TEXT NOT NULL, lat REAL NOT NULL, lng REAL NOT NULL, geohash TEXT NOT NULL, point TEXT NOT NULL, polygon TEXT NOT NULL, accuracy REAL, altitude REAL, tag_0 TEXT, label TEXT, area REAL, elevation INTEGER, soil TEXT, climate TEXT, gc_id TEXT, gc_name TEXT, gc_admin1_id TEXT, gc_admin1_name TEXT, gc_country_id TEXT, gc_country_name TEXT)", - "CREATE TABLE trade_product (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), key TEXT NOT NULL, category TEXT NOT NULL, title TEXT NOT NULL, summary TEXT NOT NULL, process TEXT NOT NULL, lot TEXT NOT NULL, profile TEXT NOT NULL, year INTEGER NOT NULL, qty_amt INTEGER NOT NULL, qty_unit CHAR(4) NOT NULL, qty_label TEXT, qty_avail INTEGER, price_amt REAL NOT NULL, price_currency CHAR(3) NOT NULL, price_qty_amt INTEGER NOT NULL, price_qty_unit CHAR(4) NOT NULL, notes TEXT)", - "CREATE TABLE plot (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), d_tag TEXT NOT NULL, farm_id CHAR(36) NOT NULL, name TEXT NOT NULL, about TEXT, location_primary TEXT, location_city TEXT, location_region TEXT, location_country TEXT, FOREIGN KEY (farm_id) REFERENCES farm(id) ON DELETE CASCADE)", - "CREATE TABLE nostr_event_head (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), key TEXT NOT NULL UNIQUE, kind INTEGER NOT NULL, pubkey CHAR(64) NOT NULL CHECK(length(pubkey) = 64), d_tag TEXT NOT NULL, last_event_id CHAR(64) NOT NULL CHECK(length(last_event_id) = 64), last_created_at INTEGER NOT NULL, content_hash TEXT NOT NULL)", - ]; - - for sql in schema { - exec.exec(sql, "[]") - .expect("schema statement should succeed"); - } - - for name in [ - "0000_init", - "0001_log_error", - "0002_farm", - "0003_gcs_location", - "0004_trade_product", - "0005_nostr_profile", - "0006_nostr_relay", - "0007_media_image", - "0008_farm_gcs_location", - "0009_nostr_profile_relay", - "0010_trade_product_location", - "0011_trade_product_media", - "0012_plot", - "0013_plot_gcs_location", - "0014_farm_tag", - "0015_plot_tag", - "0016_farm_member", - "0017_farm_member_claim", - "0018_nostr_event_head", - ] { - let sql = format!("INSERT INTO __migrations(name) VALUES ('{name}')"); - exec.exec(&sql, "[]") - .expect("legacy migration marker should succeed"); - } -} - -#[test] -fn run_all_up_repairs_missing_indexes_in_legacy_sqlite_dbs() { - let exec = SqliteExecutor::open_memory().expect("open sqlite memory"); - create_legacy_schema_without_secondary_indexes(&exec); - - let before = query_rows( - &exec, - "SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('farm_pubkey_d_tag_idx', 'gcs_location_geohash_idx', 'plot_farm_d_tag_idx', 'nostr_event_head_kind_idx') ORDER BY name", - ); - assert!(before.is_empty()); - - migrations::run_all_up(&exec).expect("repair migration should succeed"); - - let after = query_rows( - &exec, - "SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('farm_pubkey_d_tag_idx', 'gcs_location_geohash_idx', 'plot_farm_d_tag_idx', 'nostr_event_head_kind_idx') ORDER BY name", - ); - let names = after - .iter() - .map(|row| { - row.get("name") - .and_then(Value::as_str) - .expect("index name should exist") - .to_string() - }) - .collect::<Vec<_>>(); - assert_eq!( - names, - vec![ - "farm_pubkey_d_tag_idx".to_string(), - "gcs_location_geohash_idx".to_string(), - "nostr_event_head_kind_idx".to_string(), - "plot_farm_d_tag_idx".to_string(), - ] - ); - - let trade_product_columns = query_rows(&exec, "PRAGMA table_info(trade_product)"); - assert!(trade_product_columns.iter().any(|row| { - row.get("name") - .and_then(Value::as_str) - .is_some_and(|name| name == "listing_addr") - })); - assert!(trade_product_columns.iter().any(|row| { - row.get("name") - .and_then(Value::as_str) - .is_some_and(|name| name == "primary_bin_id") - })); - assert!(trade_product_columns.iter().any(|row| { - row.get("name") - .and_then(Value::as_str) - .is_some_and(|name| name == "verified_primary_bin_id") - })); -} diff --git a/crates/replica_db/tests/region_scripted_paths.rs b/crates/replica_db/tests/region_scripted_paths.rs @@ -1,786 +0,0 @@ -use std::collections::VecDeque; -use std::sync::Mutex; - -use radroots_replica_db::{ExecOutcome, ReplicaSql, SqlError, SqlExecutor}; -use radroots_replica_db_schema::ReplicaSchemaError; -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, - IGcsLocationFindOneRelArgs, IGcsLocationUpdate, -}; -use radroots_replica_db_schema::log_error::{ - ILogErrorCreate, ILogErrorDelete, ILogErrorFindMany, ILogErrorFindOne, ILogErrorUpdate, -}; -use radroots_replica_db_schema::media_image::{ - IMediaImageCreate, IMediaImageDelete, IMediaImageFindMany, IMediaImageFindOne, - IMediaImageFindOneRelArgs, IMediaImageUpdate, MediaImageFindManyRel, - MediaImageTradeProductArgs, -}; -use radroots_replica_db_schema::nostr_event_head::{ - INostrEventHeadCreate, INostrEventHeadDelete, INostrEventHeadFindMany, INostrEventHeadFindOne, - INostrEventHeadUpdate, -}; -use radroots_replica_db_schema::nostr_profile::{ - INostrProfileCreate, INostrProfileDelete, INostrProfileFindMany, INostrProfileFindOne, - INostrProfileFindOneRelArgs, INostrProfileUpdate, NostrProfileFindManyRel, - NostrProfileRelayArgs, -}; -use radroots_replica_db_schema::nostr_relay::{ - INostrRelayCreate, INostrRelayDelete, INostrRelayFindMany, INostrRelayFindOne, - INostrRelayFindOneRelArgs, 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, ITradeProductFindMany, ITradeProductFindOne, ITradeProductUpdate, -}; -use serde::de::DeserializeOwned; -use serde_json::json; - -struct ScriptedExecutor { - exec_results: Mutex<VecDeque<Result<ExecOutcome, SqlError>>>, - query_results: Mutex<VecDeque<Result<String, SqlError>>>, - begin_results: Mutex<VecDeque<Result<(), SqlError>>>, - commit_results: Mutex<VecDeque<Result<(), SqlError>>>, - rollback_results: Mutex<VecDeque<Result<(), SqlError>>>, -} - -impl ScriptedExecutor { - fn new( - exec_results: Vec<Result<ExecOutcome, SqlError>>, - query_results: Vec<Result<String, SqlError>>, - ) -> Self { - Self { - exec_results: Mutex::new(VecDeque::from(exec_results)), - query_results: Mutex::new(VecDeque::from(query_results)), - begin_results: Mutex::new(VecDeque::new()), - commit_results: Mutex::new(VecDeque::new()), - rollback_results: Mutex::new(VecDeque::new()), - } - } -} - -impl SqlExecutor for ScriptedExecutor { - fn exec(&self, _sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> { - if let Some(result) = self - .exec_results - .lock() - .expect("lock exec queue") - .pop_front() - { - result - } else { - Ok(ExecOutcome { - changes: 1, - last_insert_id: 1, - }) - } - } - - fn query_raw(&self, _sql: &str, _params_json: &str) -> Result<String, SqlError> { - if let Some(result) = self - .query_results - .lock() - .expect("lock query queue") - .pop_front() - { - result - } else { - Ok(String::from("[]")) - } - } - - fn begin(&self) -> Result<(), SqlError> { - if let Some(result) = self - .begin_results - .lock() - .expect("lock begin queue") - .pop_front() - { - result - } else { - Ok(()) - } - } - - fn commit(&self) -> Result<(), SqlError> { - if let Some(result) = self - .commit_results - .lock() - .expect("lock commit queue") - .pop_front() - { - result - } else { - Ok(()) - } - } - - fn rollback(&self) -> Result<(), SqlError> { - if let Some(result) = self - .rollback_results - .lock() - .expect("lock rollback queue") - .pop_front() - { - result - } else { - Ok(()) - } - } -} - -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 err_query() -> Result<String, SqlError> { - Err(SqlError::InvalidQuery(String::from("forced query error"))) -} - -fn bad_json() -> Result<String, SqlError> { - Ok(String::from("{")) -} - -fn ok_rows() -> Result<String, SqlError> { - Ok(String::from("[]")) -} - -fn ok_exec() -> Result<ExecOutcome, SqlError> { - Ok(ExecOutcome { - changes: 1, - last_insert_id: 1, - }) -} - -fn db_with_scripts( - exec_results: Vec<Result<ExecOutcome, SqlError>>, - query_results: Vec<Result<String, SqlError>>, -) -> ReplicaSql<ScriptedExecutor> { - ReplicaSql::new(ScriptedExecutor::new(exec_results, query_results)) -} - -fn assert_replica_schema_error_code<T>( - result: Result<T, ReplicaSchemaError<SqlError>>, - code: &str, -) { - let err = match result { - Ok(_) => panic!("expected ierror"), - Err(err) => err, - }; - assert_eq!(err.err.code(), code); -} - -macro_rules! assert_secondary_model_paths { - ( - $test_name:ident, - $create_ty:ty, $create_json:expr, $create_call:ident, - $find_many_ty:ty, $find_many_json:expr, $find_many_call:ident, - $find_one_ty:ty, $find_one_json:expr, $find_one_call:ident, - $update_ty:ty, $update_id_json:expr, $update_lookup_json:expr, $update_call:ident, - $delete_ty:ty, $delete_lookup_json:expr, $delete_call:ident - ) => { - #[test] - fn $test_name() { - let create_opts: $create_ty = parse_json($create_json); - - let db = db_with_scripts(vec![ok_exec()], vec![err_query()]); - assert_replica_schema_error_code(db.$create_call(&create_opts), "ERR_INVALID_QUERY"); - - let db = db_with_scripts(vec![ok_exec()], vec![ok_rows()]); - assert_replica_schema_error_code(db.$create_call(&create_opts), "ERR_NOT_FOUND"); - - let find_many_opts: $find_many_ty = parse_json($find_many_json); - let db = db_with_scripts(vec![], vec![bad_json()]); - assert_replica_schema_error_code( - db.$find_many_call(&find_many_opts), - "ERR_SERIALIZATION", - ); - - let find_one_opts: $find_one_ty = parse_json($find_one_json); - let db = db_with_scripts(vec![], vec![bad_json()]); - assert_replica_schema_error_code( - db.$find_one_call(&find_one_opts), - "ERR_SERIALIZATION", - ); - - let update_id_opts: $update_ty = parse_json($update_id_json); - let db = db_with_scripts(vec![ok_exec()], vec![err_query()]); - assert_replica_schema_error_code(db.$update_call(&update_id_opts), "ERR_INVALID_QUERY"); - - let db = db_with_scripts(vec![ok_exec()], vec![bad_json()]); - assert_replica_schema_error_code(db.$update_call(&update_id_opts), "ERR_SERIALIZATION"); - - let update_lookup_opts: $update_ty = parse_json($update_lookup_json); - let db = db_with_scripts(vec![], vec![err_query()]); - assert_replica_schema_error_code( - db.$update_call(&update_lookup_opts), - "ERR_INVALID_QUERY", - ); - - let delete_lookup_opts: $delete_ty = parse_json($delete_lookup_json); - let db = db_with_scripts(vec![], vec![err_query()]); - assert_replica_schema_error_code( - db.$delete_call(&delete_lookup_opts), - "ERR_INVALID_QUERY", - ); - } - }; -} - -macro_rules! assert_rel_model_paths { - ( - $test_name:ident, - $create_ty:ty, $create_json:expr, $create_call:ident, - $find_many_ty:ty, $find_many_filter_json:expr, $find_many_rel_expr:expr, $find_many_call:ident, - $find_one_ty:ty, $find_one_on_json:expr, $find_one_rel_expr:expr, $find_one_call:ident, - $update_ty:ty, $update_id_json:expr, $update_lookup_json:expr, $update_call:ident, - $delete_ty:ty, $delete_lookup_json:expr, $delete_rel_expr:expr, $delete_call:ident - ) => { - #[test] - fn $test_name() { - let create_opts: $create_ty = parse_json($create_json); - - let db = db_with_scripts(vec![ok_exec()], vec![err_query()]); - assert_replica_schema_error_code(db.$create_call(&create_opts), "ERR_INVALID_QUERY"); - - let db = db_with_scripts(vec![ok_exec()], vec![ok_rows()]); - assert_replica_schema_error_code(db.$create_call(&create_opts), "ERR_NOT_FOUND"); - - let find_many_filter_opts: $find_many_ty = parse_json($find_many_filter_json); - let db = db_with_scripts(vec![], vec![bad_json()]); - assert_replica_schema_error_code( - db.$find_many_call(&find_many_filter_opts), - "ERR_SERIALIZATION", - ); - - let find_many_rel_opts: $find_many_ty = $find_many_rel_expr; - let db = db_with_scripts(vec![], vec![bad_json()]); - assert_replica_schema_error_code( - db.$find_many_call(&find_many_rel_opts), - "ERR_SERIALIZATION", - ); - - let find_many_rel_opts: $find_many_ty = $find_many_rel_expr; - let db = db_with_scripts(vec![], vec![err_query()]); - assert_replica_schema_error_code( - db.$find_many_call(&find_many_rel_opts), - "ERR_INVALID_QUERY", - ); - - let find_one_on_opts: $find_one_ty = parse_json($find_one_on_json); - let db = db_with_scripts(vec![], vec![bad_json()]); - assert_replica_schema_error_code( - db.$find_one_call(&find_one_on_opts), - "ERR_SERIALIZATION", - ); - - let find_one_rel_opts: $find_one_ty = $find_one_rel_expr; - let db = db_with_scripts(vec![], vec![bad_json()]); - assert_replica_schema_error_code( - db.$find_one_call(&find_one_rel_opts), - "ERR_SERIALIZATION", - ); - - let update_id_opts: $update_ty = parse_json($update_id_json); - let db = db_with_scripts(vec![ok_exec()], vec![err_query()]); - assert_replica_schema_error_code(db.$update_call(&update_id_opts), "ERR_INVALID_QUERY"); - - let db = db_with_scripts(vec![ok_exec()], vec![bad_json()]); - assert_replica_schema_error_code(db.$update_call(&update_id_opts), "ERR_SERIALIZATION"); - - let update_lookup_opts: $update_ty = parse_json($update_lookup_json); - let db = db_with_scripts(vec![], vec![err_query()]); - assert_replica_schema_error_code( - db.$update_call(&update_lookup_opts), - "ERR_INVALID_QUERY", - ); - - let delete_lookup_opts: $delete_ty = parse_json($delete_lookup_json); - let db = db_with_scripts(vec![], vec![err_query()]); - assert_replica_schema_error_code( - db.$delete_call(&delete_lookup_opts), - "ERR_INVALID_QUERY", - ); - - let delete_rel_opts: $delete_ty = $delete_rel_expr; - let db = db_with_scripts(vec![], vec![err_query()]); - assert_replica_schema_error_code( - db.$delete_call(&delete_rel_opts), - "ERR_INVALID_QUERY", - ); - } - }; -} - -macro_rules! assert_trade_product_paths { - ( - $test_name:ident, - $create_ty:ty, $create_json:expr, $create_call:ident, - $find_many_ty:ty, $find_many_json:expr, $find_many_call:ident, - $find_one_ty:ty, $find_one_json:expr, $find_one_call:ident, - $update_ty:ty, $update_json:expr, $update_call:ident - ) => { - #[test] - fn $test_name() { - let create_opts: $create_ty = parse_json($create_json); - - let db = db_with_scripts(vec![ok_exec()], vec![err_query()]); - assert_replica_schema_error_code(db.$create_call(&create_opts), "ERR_INVALID_QUERY"); - - let db = db_with_scripts(vec![ok_exec()], vec![ok_rows()]); - assert_replica_schema_error_code(db.$create_call(&create_opts), "ERR_NOT_FOUND"); - - let find_many_opts: $find_many_ty = parse_json($find_many_json); - let db = db_with_scripts(vec![], vec![bad_json()]); - assert_replica_schema_error_code( - db.$find_many_call(&find_many_opts), - "ERR_SERIALIZATION", - ); - - let find_one_opts: $find_one_ty = parse_json($find_one_json); - let db = db_with_scripts(vec![], vec![bad_json()]); - assert_replica_schema_error_code( - db.$find_one_call(&find_one_opts), - "ERR_SERIALIZATION", - ); - - let update_opts: $update_ty = parse_json($update_json); - let db = db_with_scripts(vec![ok_exec()], vec![err_query()]); - assert_replica_schema_error_code(db.$update_call(&update_opts), "ERR_INVALID_QUERY"); - - let db = db_with_scripts(vec![ok_exec()], vec![bad_json()]); - assert_replica_schema_error_code(db.$update_call(&update_opts), "ERR_SERIALIZATION"); - } - }; -} - -assert_secondary_model_paths!( - farm_scripted_region_paths, - IFarmCreate, - json!({ "d_tag": "farm-a", "pubkey": hex64('a'), "name": "farm a" }), - farm_create, - IFarmFindMany, - json!({ "filter": { "id": "id-1" } }), - farm_find_many, - IFarmFindOne, - json!({ "on": { "id": "id-1" } }), - farm_find_one, - IFarmUpdate, - json!({ "on": { "id": "id-1" }, "fields": { "name": "farm z" } }), - json!({ "on": { "d_tag": "farm-a" }, "fields": { "name": "farm y" } }), - farm_update, - IFarmDelete, - json!({ "on": { "d_tag": "farm-a" } }), - farm_delete -); - -assert_secondary_model_paths!( - plot_scripted_region_paths, - IPlotCreate, - json!({ "d_tag": "plot-a", "farm_id": "farm-1", "name": "plot a" }), - plot_create, - IPlotFindMany, - json!({ "filter": { "id": "id-1" } }), - plot_find_many, - IPlotFindOne, - json!({ "on": { "id": "id-1" } }), - plot_find_one, - IPlotUpdate, - json!({ "on": { "id": "id-1" }, "fields": { "name": "plot z" } }), - json!({ "on": { "d_tag": "plot-a" }, "fields": { "name": "plot y" } }), - plot_update, - IPlotDelete, - json!({ "on": { "d_tag": "plot-a" } }), - plot_delete -); - -assert_secondary_model_paths!( - farm_gcs_location_scripted_region_paths, - IFarmGcsLocationCreate, - json!({ "farm_id": "farm-1", "gcs_location_id": "gcs-1", "role": "primary" }), - farm_gcs_location_create, - IFarmGcsLocationFindMany, - json!({ "filter": { "id": "id-1" } }), - farm_gcs_location_find_many, - IFarmGcsLocationFindOne, - json!({ "on": { "id": "id-1" } }), - farm_gcs_location_find_one, - IFarmGcsLocationUpdate, - json!({ "on": { "id": "id-1" }, "fields": { "role": "z" } }), - json!({ "on": { "farm_id": "farm-1" }, "fields": { "role": "y" } }), - farm_gcs_location_update, - IFarmGcsLocationDelete, - json!({ "on": { "farm_id": "farm-1" } }), - farm_gcs_location_delete -); - -assert_secondary_model_paths!( - plot_gcs_location_scripted_region_paths, - IPlotGcsLocationCreate, - json!({ "plot_id": "plot-1", "gcs_location_id": "gcs-1", "role": "primary" }), - plot_gcs_location_create, - IPlotGcsLocationFindMany, - json!({ "filter": { "id": "id-1" } }), - plot_gcs_location_find_many, - IPlotGcsLocationFindOne, - json!({ "on": { "id": "id-1" } }), - plot_gcs_location_find_one, - IPlotGcsLocationUpdate, - json!({ "on": { "id": "id-1" }, "fields": { "role": "z" } }), - json!({ "on": { "plot_id": "plot-1" }, "fields": { "role": "y" } }), - plot_gcs_location_update, - IPlotGcsLocationDelete, - json!({ "on": { "plot_id": "plot-1" } }), - plot_gcs_location_delete -); - -assert_secondary_model_paths!( - farm_tag_scripted_region_paths, - IFarmTagCreate, - json!({ "farm_id": "farm-1", "tag": "organic" }), - farm_tag_create, - IFarmTagFindMany, - json!({ "filter": { "id": "id-1" } }), - farm_tag_find_many, - IFarmTagFindOne, - json!({ "on": { "id": "id-1" } }), - farm_tag_find_one, - IFarmTagUpdate, - json!({ "on": { "id": "id-1" }, "fields": { "tag": "z" } }), - json!({ "on": { "farm_id": "farm-1" }, "fields": { "tag": "y" } }), - farm_tag_update, - IFarmTagDelete, - json!({ "on": { "farm_id": "farm-1" } }), - farm_tag_delete -); - -assert_secondary_model_paths!( - plot_tag_scripted_region_paths, - IPlotTagCreate, - json!({ "plot_id": "plot-1", "tag": "north" }), - plot_tag_create, - IPlotTagFindMany, - json!({ "filter": { "id": "id-1" } }), - plot_tag_find_many, - IPlotTagFindOne, - json!({ "on": { "id": "id-1" } }), - plot_tag_find_one, - IPlotTagUpdate, - json!({ "on": { "id": "id-1" }, "fields": { "tag": "z" } }), - json!({ "on": { "plot_id": "plot-1" }, "fields": { "tag": "y" } }), - plot_tag_update, - IPlotTagDelete, - json!({ "on": { "plot_id": "plot-1" } }), - plot_tag_delete -); - -assert_secondary_model_paths!( - farm_member_scripted_region_paths, - IFarmMemberCreate, - json!({ "farm_id": "farm-1", "member_pubkey": hex64('b'), "role": "owner" }), - farm_member_create, - IFarmMemberFindMany, - json!({ "filter": { "id": "id-1" } }), - farm_member_find_many, - IFarmMemberFindOne, - json!({ "on": { "id": "id-1" } }), - farm_member_find_one, - IFarmMemberUpdate, - json!({ "on": { "id": "id-1" }, "fields": { "role": "z" } }), - json!({ "on": { "member_pubkey": hex64('b') }, "fields": { "role": "y" } }), - farm_member_update, - IFarmMemberDelete, - json!({ "on": { "member_pubkey": hex64('b') } }), - farm_member_delete -); - -assert_secondary_model_paths!( - farm_member_claim_scripted_region_paths, - IFarmMemberClaimCreate, - json!({ "member_pubkey": hex64('b'), "farm_pubkey": hex64('a') }), - farm_member_claim_create, - IFarmMemberClaimFindMany, - json!({ "filter": { "id": "id-1" } }), - farm_member_claim_find_many, - IFarmMemberClaimFindOne, - json!({ "on": { "id": "id-1" } }), - farm_member_claim_find_one, - IFarmMemberClaimUpdate, - json!({ "on": { "id": "id-1" }, "fields": { "farm_pubkey": hex64('c') } }), - json!({ "on": { "member_pubkey": hex64('b') }, "fields": { "farm_pubkey": hex64('d') } }), - farm_member_claim_update, - IFarmMemberClaimDelete, - json!({ "on": { "member_pubkey": hex64('b') } }), - farm_member_claim_delete -); - -assert_secondary_model_paths!( - log_error_scripted_region_paths, - ILogErrorCreate, - json!({ - "error": "panic", - "message": "boom", - "app_system": "studio", - "app_version": "1.0.0", - "nostr_pubkey": hex64('c') - }), - log_error_create, - ILogErrorFindMany, - json!({ "filter": { "id": "id-1" } }), - log_error_find_many, - ILogErrorFindOne, - json!({ "on": { "id": "id-1" } }), - log_error_find_one, - ILogErrorUpdate, - json!({ "on": { "id": "id-1" }, "fields": { "message": "z" } }), - json!({ "on": { "nostr_pubkey": hex64('c') }, "fields": { "message": "y" } }), - log_error_update, - ILogErrorDelete, - json!({ "on": { "nostr_pubkey": hex64('c') } }), - log_error_delete -); - -assert_secondary_model_paths!( - nostr_event_head_scripted_region_paths, - INostrEventHeadCreate, - 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" - }), - nostr_event_head_create, - INostrEventHeadFindMany, - json!({ "filter": { "id": "id-1" } }), - nostr_event_head_find_many, - INostrEventHeadFindOne, - json!({ "on": { "id": "id-1" } }), - nostr_event_head_find_one, - INostrEventHeadUpdate, - json!({ "on": { "id": "id-1" }, "fields": { "content_hash": "hash-z" } }), - json!({ "on": { "key": "state-a" }, "fields": { "content_hash": "hash-y" } }), - nostr_event_head_update, - INostrEventHeadDelete, - json!({ "on": { "key": "state-a" } }), - nostr_event_head_delete -); - -assert_rel_model_paths!( - gcs_location_scripted_region_paths, - IGcsLocationCreate, - 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))" - }), - gcs_location_create, - IGcsLocationFindMany, - json!({ "filter": { "id": "id-1" } }), - IGcsLocationFindMany::Rel { - rel: GcsLocationFindManyRel::OnFarm(GcsLocationFarmArgs { - id: String::from("farm-1") - }) - }, - gcs_location_find_many, - IGcsLocationFindOne, - json!({ "on": { "id": "id-1" } }), - IGcsLocationFindOne::Rel(IGcsLocationFindOneRelArgs { - rel: GcsLocationFindManyRel::OffTradeProduct(GcsLocationTradeProductArgs { - id: String::from("tp-1") - }) - }), - gcs_location_find_one, - IGcsLocationUpdate, - json!({ "on": { "id": "id-1" }, "fields": { "label": "z" } }), - json!({ "on": { "d_tag": "gcs-a" }, "fields": { "label": "y" } }), - gcs_location_update, - IGcsLocationDelete, - json!({ "on": { "d_tag": "gcs-a" } }), - IGcsLocationDelete::Rel(IGcsLocationFindOneRelArgs { - rel: GcsLocationFindManyRel::OnPlot(GcsLocationPlotArgs { - id: String::from("plot-1") - }) - }), - gcs_location_delete -); - -assert_rel_model_paths!( - media_image_scripted_region_paths, - IMediaImageCreate, - json!({ - "file_path": "/img/a.jpg", - "mime_type": "image/jpeg", - "res_base": "https://cdn.example.com", - "res_path": "img/a.jpg" - }), - media_image_create, - IMediaImageFindMany, - json!({ "filter": { "id": "id-1" } }), - IMediaImageFindMany::Rel { - rel: MediaImageFindManyRel::OnTradeProduct(MediaImageTradeProductArgs { - id: String::from("tp-1") - }) - }, - media_image_find_many, - IMediaImageFindOne, - json!({ "on": { "id": "id-1" } }), - IMediaImageFindOne::Rel(IMediaImageFindOneRelArgs { - rel: MediaImageFindManyRel::OffTradeProduct(MediaImageTradeProductArgs { - id: String::from("tp-1") - }) - }), - media_image_find_one, - IMediaImageUpdate, - json!({ "on": { "id": "id-1" }, "fields": { "label": "z" } }), - json!({ "on": { "file_path": "/img/a.jpg" }, "fields": { "label": "y" } }), - media_image_update, - IMediaImageDelete, - json!({ "on": { "file_path": "/img/a.jpg" } }), - IMediaImageDelete::Rel(IMediaImageFindOneRelArgs { - rel: MediaImageFindManyRel::OnTradeProduct(MediaImageTradeProductArgs { - id: String::from("tp-1") - }) - }), - media_image_delete -); - -assert_rel_model_paths!( - nostr_profile_scripted_region_paths, - INostrProfileCreate, - json!({ "public_key": hex64('d'), "profile_type": "farm", "name": "profile a" }), - nostr_profile_create, - INostrProfileFindMany, - json!({ "filter": { "id": "id-1" } }), - INostrProfileFindMany::Rel { - rel: NostrProfileFindManyRel::OnRelay(NostrProfileRelayArgs { - id: String::from("relay-1") - }) - }, - nostr_profile_find_many, - INostrProfileFindOne, - json!({ "on": { "id": "id-1" } }), - INostrProfileFindOne::Rel(INostrProfileFindOneRelArgs { - rel: NostrProfileFindManyRel::OffRelay(NostrProfileRelayArgs { - id: String::from("relay-1") - }) - }), - nostr_profile_find_one, - INostrProfileUpdate, - json!({ "on": { "id": "id-1" }, "fields": { "name": "z" } }), - json!({ "on": { "public_key": hex64('d') }, "fields": { "name": "y" } }), - nostr_profile_update, - INostrProfileDelete, - json!({ "on": { "public_key": hex64('d') } }), - INostrProfileDelete::Rel(INostrProfileFindOneRelArgs { - rel: NostrProfileFindManyRel::OnRelay(NostrProfileRelayArgs { - id: String::from("relay-1") - }) - }), - nostr_profile_delete -); - -assert_rel_model_paths!( - nostr_relay_scripted_region_paths, - INostrRelayCreate, - json!({ "url": "wss://relay.example.com" }), - nostr_relay_create, - INostrRelayFindMany, - json!({ "filter": { "id": "id-1" } }), - INostrRelayFindMany::Rel { - rel: NostrRelayFindManyRel::OnProfile(NostrRelayProfileArgs { - public_key: hex64('d') - }) - }, - nostr_relay_find_many, - INostrRelayFindOne, - json!({ "on": { "id": "id-1" } }), - INostrRelayFindOne::Rel(INostrRelayFindOneRelArgs { - rel: NostrRelayFindManyRel::OffProfile(NostrRelayProfileArgs { - public_key: hex64('d') - }) - }), - nostr_relay_find_one, - INostrRelayUpdate, - json!({ "on": { "id": "id-1" }, "fields": { "name": "z" } }), - json!({ "on": { "url": "wss://relay.example.com" }, "fields": { "name": "y" } }), - nostr_relay_update, - INostrRelayDelete, - json!({ "on": { "url": "wss://relay.example.com" } }), - INostrRelayDelete::Rel(INostrRelayFindOneRelArgs { - rel: NostrRelayFindManyRel::OnProfile(NostrRelayProfileArgs { - public_key: hex64('d') - }) - }), - nostr_relay_delete -); - -assert_trade_product_paths!( - trade_product_scripted_region_paths, - ITradeProductCreate, - json!({ - "key": "product-a", - "category": "coffee", - "title": "coffee a", - "summary": "summary", - "process": "washed", - "lot": "lot-a", - "profile": "floral", - "year": 2024, - "qty_amt": 100, - "qty_amt_exact": "100", - "qty_unit": "kg", - "price_amt": 7.5, - "price_amt_exact": "7.5", - "price_currency": "USD", - "price_qty_amt": 1, - "price_qty_amt_exact": "1", - "price_qty_unit": "kg" - }), - trade_product_create, - ITradeProductFindMany, - json!({ "filter": { "id": "id-1" } }), - trade_product_find_many, - ITradeProductFindOne, - json!({ "on": { "id": "id-1" } }), - trade_product_find_one, - ITradeProductUpdate, - json!({ "on": { "id": "id-1" }, "fields": { "title": "z" } }), - trade_product_update -); diff --git a/crates/replica_db_schema/Cargo.toml b/crates/replica_db_schema/Cargo.toml @@ -1,25 +0,0 @@ -[package] -name = "radroots_replica_db_schema" -publish = ["crates-io"] -version = "0.1.0-alpha.2" -edition.workspace = true -authors = ["Tyson Lupul <tyson@radroots.org>"] -rust-version.workspace = true -license.workspace = true -description = "Replica schema model for Radroots" -repository.workspace = true -homepage.workspace = true -documentation = "https://docs.rs/radroots_replica_db_schema" -readme = "README" - -[lib] -crate-type = ["rlib"] - -[features] -default = [] -dto-bindgen = ["dep:dto_bindgen_core"] - -[dependencies] -dto_bindgen_core = { workspace = true, optional = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } diff --git a/crates/replica_db_schema/README b/crates/replica_db_schema/README @@ -1,22 +0,0 @@ -# radroots_replica_db_schema - -This is the README for `radroots_replica_db_schema`, which provides replica -schema models for the `radroots` core libraries. - -## Overview - - * schema model types reexported from the `models` module; - * table- and record-oriented value types shared by replica database code; - * optional serialization support through `serde`; - * no runtime backend logic beyond the shared schema model layer. - -## Copyright - -Except as otherwise noted, all files in the `radroots_replica_db_schema` -distribution are - - Copyright (c) 2025 Tyson Lupul - -For information on usage and redistribution, and for a DISCLAIMER OF ALL -WARRANTIES, see LICENSE included in the `radroots_replica_db_schema` -distribution. diff --git a/crates/replica_db_schema/src/dto.rs b/crates/replica_db_schema/src/dto.rs @@ -1,2266 +0,0 @@ -use dto_bindgen_core::{ - BackendId, DefaultKind, EnumDef, EnumRepr, FieldDef, FieldPresence, GenericParam, IdentName, - Primitive, Registry, RustTypeId, SerializePresence, SourceSpan, StructDef, TargetFieldNames, - TargetOverride, TypeDef, TypeRef, VariantDef, VariantShape, WireFieldNames, -}; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum TypeSpec { - Object { - name: &'static str, - fields: &'static [FieldSpec], - }, - Union { - name: &'static str, - variants: &'static [VariantSpec], - }, - Alias { - name: &'static str, - target: &'static str, - }, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum VariantSpec { - Object(&'static [FieldSpec]), - Ref(&'static str), -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -struct FieldSpec { - name: &'static str, - target: &'static str, - optional: bool, - nullable: bool, -} - -impl FieldSpec { - const fn required(name: &'static str, target: &'static str) -> Self { - Self { - name, - target, - optional: false, - nullable: false, - } - } - - const fn nullable(name: &'static str, target: &'static str) -> Self { - Self { - name, - target, - optional: false, - nullable: true, - } - } - - const fn optional(name: &'static str, target: &'static str) -> Self { - Self { - name, - target, - optional: true, - nullable: false, - } - } - - const fn optional_nullable(name: &'static str, target: &'static str) -> Self { - Self { - name, - target, - optional: true, - nullable: true, - } - } -} - -pub fn dto_registry() -> Registry { - let mut registry = Registry::new(); - register_result_contracts(&mut registry); - for spec in TYPE_SPECS { - let name = spec.name(); - let type_id = registry.register_type( - RustTypeId::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_NAME"), name), - spec.type_def(), - ); - registry.mark_root(type_id); - } - registry -} - -pub fn type_inventory() -> Vec<&'static str> { - RESULT_CONTRACT_NAMES - .iter() - .copied() - .chain(TYPE_SPECS.iter().map(TypeSpec::name)) - .collect() -} - -impl TypeSpec { - fn name(&self) -> &'static str { - match self { - Self::Object { name, .. } | Self::Union { name, .. } | Self::Alias { name, .. } => name, - } - } - - fn type_def(&self) -> TypeDef { - match self { - Self::Object { name, fields } => TypeDef::Struct(object_def(name, fields)), - Self::Union { name, variants } => TypeDef::Enum(union_def(name, variants)), - Self::Alias { name, target } => TypeDef::Enum(alias_def(name, target)), - } - } -} - -fn object_def(name: &str, fields: &[FieldSpec]) -> StructDef { - let mut def = StructDef::new(name, name, source_span()); - for field in fields { - def = def.with_field(field_def(field)); - } - def -} - -fn register_result_contracts(registry: &mut Registry) { - for (name, field_name, field_ty) in [ - ( - "ReplicaSchemaError", - "err", - TypeRef::GenericParam("T".to_owned()), - ), - ( - "ReplicaSchemaResult", - "result", - TypeRef::GenericParam("T".to_owned()), - ), - ( - "ReplicaSchemaResultList", - "results", - TypeRef::vec(TypeRef::GenericParam("T".to_owned())), - ), - ] { - let mut def = StructDef::new(name, name, source_span()) - .with_field(typed_field_def(field_name, field_ty)); - def.generics.push(GenericParam::new("T")); - let type_id = registry.register_type( - RustTypeId::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_NAME"), name), - TypeDef::Struct(def), - ); - registry.mark_root(type_id); - } - - let type_id = registry.register_type( - RustTypeId::new( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - "ReplicaSchemaResultPass", - ), - TypeDef::Struct( - StructDef::new( - "ReplicaSchemaResultPass", - "ReplicaSchemaResultPass", - source_span(), - ) - .with_field(typed_field_def("pass", TypeRef::Primitive(Primitive::Bool))), - ), - ); - registry.mark_root(type_id); -} - -fn union_def(name: &str, variants: &[VariantSpec]) -> EnumDef { - let mut def = EnumDef::new(name, name, EnumRepr::Untagged, source_span()); - for (index, variant) in variants.iter().enumerate() { - def = def.with_variant(match variant { - VariantSpec::Object(fields) => VariantDef::new( - format!("Variant{index}"), - format!("variant{index}"), - VariantShape::Struct(fields.iter().map(field_def).collect()), - source_span(), - ), - VariantSpec::Ref(target) => VariantDef::new( - format!("Variant{index}"), - format!("variant{index}"), - VariantShape::Newtype(ts_ref(target)), - source_span(), - ), - }); - } - def -} - -fn alias_def(name: &str, target: &str) -> EnumDef { - EnumDef::new(name, name, EnumRepr::Untagged, source_span()).with_variant(VariantDef::new( - "Alias", - "alias", - VariantShape::Newtype(ts_ref(target)), - source_span(), - )) -} - -fn field_def(field: &FieldSpec) -> FieldDef { - typed_field_def(field.name, ts_ref(field.target)) - .with_presence(field_presence(field.optional, field.nullable)) -} - -fn typed_field_def(name: &str, ty: TypeRef) -> FieldDef { - FieldDef::new( - IdentName::new(name), - WireFieldNames::same(name), - TargetFieldNames::new(name, name), - ty, - source_span(), - ) -} - -fn field_presence(optional: bool, nullable: bool) -> FieldPresence { - match (optional, nullable) { - (false, false) => FieldPresence::required(), - (false, true) => FieldPresence::nullable_required(), - (true, true) => FieldPresence::optional_nullable(), - (true, false) => FieldPresence { - nullable: false, - required_on_deserialize: false, - default: Some(DefaultKind::NoneValue), - serialize_presence: SerializePresence::Always, - }, - } -} - -fn ts_ref(target: &str) -> TypeRef { - TypeRef::Override(TargetOverride::new(BackendId::TypeScript, target)) -} - -fn source_span() -> SourceSpan { - SourceSpan::new(file!(), line!(), column!()) -} - -const RESULT_CONTRACT_NAMES: &[&str] = &[ - "ReplicaSchemaError", - "ReplicaSchemaResult", - "ReplicaSchemaResultList", - "ReplicaSchemaResultPass", -]; - -const TYPE_SPECS: &[TypeSpec] = &[ - TypeSpec::Object { - name: "Farm", - fields: &[ - FieldSpec::required("id", "string"), - FieldSpec::required("created_at", "string"), - FieldSpec::required("updated_at", "string"), - FieldSpec::required("d_tag", "string"), - FieldSpec::required("pubkey", "string"), - FieldSpec::required("name", "string"), - FieldSpec::nullable("about", "string"), - FieldSpec::nullable("website", "string"), - FieldSpec::nullable("picture", "string"), - FieldSpec::nullable("banner", "string"), - FieldSpec::nullable("location_primary", "string"), - FieldSpec::nullable("location_city", "string"), - FieldSpec::nullable("location_region", "string"), - FieldSpec::nullable("location_country", "string"), - ], - }, - TypeSpec::Object { - name: "FarmGcsLocation", - fields: &[ - FieldSpec::required("id", "string"), - FieldSpec::required("created_at", "string"), - FieldSpec::required("updated_at", "string"), - FieldSpec::required("farm_id", "string"), - FieldSpec::required("gcs_location_id", "string"), - FieldSpec::required("role", "string"), - ], - }, - TypeSpec::Union { - name: "FarmGcsLocationQueryBindValues", - variants: &[ - VariantSpec::Object(&[FieldSpec::required("id", "string")]), - VariantSpec::Object(&[FieldSpec::required("farm_id", "string")]), - VariantSpec::Object(&[FieldSpec::required("gcs_location_id", "string")]), - ], - }, - TypeSpec::Object { - name: "FarmMember", - fields: &[ - FieldSpec::required("id", "string"), - FieldSpec::required("created_at", "string"), - FieldSpec::required("updated_at", "string"), - FieldSpec::required("farm_id", "string"), - FieldSpec::required("member_pubkey", "string"), - FieldSpec::required("role", "string"), - ], - }, - TypeSpec::Object { - name: "FarmMemberClaim", - fields: &[ - FieldSpec::required("id", "string"), - FieldSpec::required("created_at", "string"), - FieldSpec::required("updated_at", "string"), - FieldSpec::required("member_pubkey", "string"), - FieldSpec::required("farm_pubkey", "string"), - ], - }, - TypeSpec::Union { - name: "FarmMemberClaimQueryBindValues", - variants: &[ - VariantSpec::Object(&[FieldSpec::required("id", "string")]), - VariantSpec::Object(&[FieldSpec::required("member_pubkey", "string")]), - VariantSpec::Object(&[FieldSpec::required("farm_pubkey", "string")]), - ], - }, - TypeSpec::Union { - name: "FarmMemberQueryBindValues", - variants: &[ - VariantSpec::Object(&[FieldSpec::required("id", "string")]), - VariantSpec::Object(&[FieldSpec::required("farm_id", "string")]), - VariantSpec::Object(&[FieldSpec::required("member_pubkey", "string")]), - ], - }, - TypeSpec::Union { - name: "FarmQueryBindValues", - variants: &[ - VariantSpec::Object(&[FieldSpec::required("id", "string")]), - VariantSpec::Object(&[FieldSpec::required("d_tag", "string")]), - VariantSpec::Object(&[FieldSpec::required("pubkey", "string")]), - ], - }, - TypeSpec::Object { - name: "FarmTag", - fields: &[ - FieldSpec::required("id", "string"), - FieldSpec::required("created_at", "string"), - FieldSpec::required("updated_at", "string"), - FieldSpec::required("farm_id", "string"), - FieldSpec::required("tag", "string"), - ], - }, - TypeSpec::Union { - name: "FarmTagQueryBindValues", - variants: &[ - VariantSpec::Object(&[FieldSpec::required("id", "string")]), - VariantSpec::Object(&[FieldSpec::required("farm_id", "string")]), - VariantSpec::Object(&[FieldSpec::required("tag", "string")]), - ], - }, - TypeSpec::Object { - name: "GcsLocation", - fields: &[ - FieldSpec::required("id", "string"), - FieldSpec::required("created_at", "string"), - FieldSpec::required("updated_at", "string"), - FieldSpec::required("d_tag", "string"), - FieldSpec::required("lat", "number"), - FieldSpec::required("lng", "number"), - FieldSpec::required("geohash", "string"), - FieldSpec::required("point", "string"), - FieldSpec::required("polygon", "string"), - FieldSpec::nullable("accuracy", "number"), - FieldSpec::nullable("altitude", "number"), - FieldSpec::nullable("tag_0", "string"), - FieldSpec::nullable("label", "string"), - FieldSpec::nullable("area", "number"), - FieldSpec::nullable("elevation", "number"), - FieldSpec::nullable("soil", "string"), - FieldSpec::nullable("climate", "string"), - FieldSpec::nullable("gc_id", "string"), - FieldSpec::nullable("gc_name", "string"), - FieldSpec::nullable("gc_admin1_id", "string"), - FieldSpec::nullable("gc_admin1_name", "string"), - FieldSpec::nullable("gc_country_id", "string"), - FieldSpec::nullable("gc_country_name", "string"), - ], - }, - TypeSpec::Object { - name: "GcsLocationFarmArgs", - fields: &[FieldSpec::required("id", "string")], - }, - TypeSpec::Union { - name: "GcsLocationFindManyRel", - variants: &[ - VariantSpec::Object(&[FieldSpec::required( - "on_trade_product", - "GcsLocationTradeProductArgs", - )]), - VariantSpec::Object(&[FieldSpec::required( - "off_trade_product", - "GcsLocationTradeProductArgs", - )]), - VariantSpec::Object(&[FieldSpec::required("on_farm", "GcsLocationFarmArgs")]), - VariantSpec::Object(&[FieldSpec::required("off_farm", "GcsLocationFarmArgs")]), - VariantSpec::Object(&[FieldSpec::required("on_plot", "GcsLocationPlotArgs")]), - VariantSpec::Object(&[FieldSpec::required("off_plot", "GcsLocationPlotArgs")]), - ], - }, - TypeSpec::Object { - name: "GcsLocationPlotArgs", - fields: &[FieldSpec::required("id", "string")], - }, - TypeSpec::Union { - name: "GcsLocationQueryBindValues", - variants: &[ - VariantSpec::Object(&[FieldSpec::required("id", "string")]), - VariantSpec::Object(&[FieldSpec::required("d_tag", "string")]), - VariantSpec::Object(&[FieldSpec::required("geohash", "string")]), - ], - }, - TypeSpec::Object { - name: "GcsLocationTradeProductArgs", - fields: &[FieldSpec::required("id", "string")], - }, - TypeSpec::Alias { - name: "IFarmCreate", - target: "IFarmFields", - }, - TypeSpec::Alias { - name: "IFarmCreateResolve", - target: "ReplicaSchemaResult<Farm>", - }, - TypeSpec::Alias { - name: "IFarmDelete", - target: "IFarmFindOne", - }, - TypeSpec::Alias { - name: "IFarmDeleteResolve", - target: "ReplicaSchemaResult<string>", - }, - TypeSpec::Object { - name: "IFarmFields", - fields: &[ - FieldSpec::required("d_tag", "string"), - FieldSpec::required("pubkey", "string"), - FieldSpec::required("name", "string"), - FieldSpec::optional_nullable("about", "string"), - FieldSpec::optional_nullable("website", "string"), - FieldSpec::optional_nullable("picture", "string"), - FieldSpec::optional_nullable("banner", "string"), - FieldSpec::optional_nullable("location_primary", "string"), - FieldSpec::optional_nullable("location_city", "string"), - FieldSpec::optional_nullable("location_region", "string"), - FieldSpec::optional_nullable("location_country", "string"), - ], - }, - TypeSpec::Object { - name: "IFarmFieldsFilter", - fields: &[ - FieldSpec::optional("id", "string"), - FieldSpec::optional("created_at", "string"), - FieldSpec::optional("updated_at", "string"), - FieldSpec::optional("d_tag", "string"), - FieldSpec::optional("pubkey", "string"), - FieldSpec::optional("name", "string"), - FieldSpec::optional("about", "string"), - FieldSpec::optional("website", "string"), - FieldSpec::optional("picture", "string"), - FieldSpec::optional("banner", "string"), - FieldSpec::optional("location_primary", "string"), - FieldSpec::optional("location_city", "string"), - FieldSpec::optional("location_region", "string"), - FieldSpec::optional("location_country", "string"), - ], - }, - TypeSpec::Object { - name: "IFarmFieldsPartial", - fields: &[ - FieldSpec::optional_nullable("d_tag", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("pubkey", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("name", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("about", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("website", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("picture", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("banner", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("location_primary", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("location_city", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("location_region", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("location_country", "ReplicaDbJsonValue"), - ], - }, - TypeSpec::Alias { - name: "IFarmFindMany", - target: "IFarmFindManyArgs", - }, - TypeSpec::Object { - name: "IFarmFindManyArgs", - fields: &[FieldSpec::nullable("filter", "IFarmFieldsFilter")], - }, - TypeSpec::Alias { - name: "IFarmFindManyResolve", - target: "ReplicaSchemaResultList<Farm>", - }, - TypeSpec::Union { - name: "IFarmFindOne", - variants: &[VariantSpec::Ref("IFarmFindOneArgs")], - }, - TypeSpec::Object { - name: "IFarmFindOneArgs", - fields: &[FieldSpec::required("on", "FarmQueryBindValues")], - }, - TypeSpec::Alias { - name: "IFarmFindOneResolve", - target: "ReplicaSchemaResult<Farm | null>", - }, - TypeSpec::Alias { - name: "IFarmGcsLocationCreate", - target: "IFarmGcsLocationFields", - }, - TypeSpec::Alias { - name: "IFarmGcsLocationCreateResolve", - target: "ReplicaSchemaResult<FarmGcsLocation>", - }, - TypeSpec::Alias { - name: "IFarmGcsLocationDelete", - target: "IFarmGcsLocationFindOne", - }, - TypeSpec::Alias { - name: "IFarmGcsLocationDeleteResolve", - target: "ReplicaSchemaResult<string>", - }, - TypeSpec::Object { - name: "IFarmGcsLocationFields", - fields: &[ - FieldSpec::required("farm_id", "string"), - FieldSpec::required("gcs_location_id", "string"), - FieldSpec::required("role", "string"), - ], - }, - TypeSpec::Object { - name: "IFarmGcsLocationFieldsFilter", - fields: &[ - FieldSpec::optional("id", "string"), - FieldSpec::optional("created_at", "string"), - FieldSpec::optional("updated_at", "string"), - FieldSpec::optional("farm_id", "string"), - FieldSpec::optional("gcs_location_id", "string"), - FieldSpec::optional("role", "string"), - ], - }, - TypeSpec::Object { - name: "IFarmGcsLocationFieldsPartial", - fields: &[ - FieldSpec::optional_nullable("farm_id", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("gcs_location_id", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("role", "ReplicaDbJsonValue"), - ], - }, - TypeSpec::Alias { - name: "IFarmGcsLocationFindMany", - target: "IFarmGcsLocationFindManyArgs", - }, - TypeSpec::Object { - name: "IFarmGcsLocationFindManyArgs", - fields: &[FieldSpec::nullable( - "filter", - "IFarmGcsLocationFieldsFilter", - )], - }, - TypeSpec::Alias { - name: "IFarmGcsLocationFindManyResolve", - target: "ReplicaSchemaResultList<FarmGcsLocation>", - }, - TypeSpec::Union { - name: "IFarmGcsLocationFindOne", - variants: &[VariantSpec::Ref("IFarmGcsLocationFindOneArgs")], - }, - TypeSpec::Object { - name: "IFarmGcsLocationFindOneArgs", - fields: &[FieldSpec::required("on", "FarmGcsLocationQueryBindValues")], - }, - TypeSpec::Alias { - name: "IFarmGcsLocationFindOneResolve", - target: "ReplicaSchemaResult<FarmGcsLocation | null>", - }, - TypeSpec::Alias { - name: "IFarmGcsLocationUpdate", - target: "IFarmGcsLocationUpdateArgs", - }, - TypeSpec::Object { - name: "IFarmGcsLocationUpdateArgs", - fields: &[ - FieldSpec::required("on", "FarmGcsLocationQueryBindValues"), - FieldSpec::required("fields", "IFarmGcsLocationFieldsPartial"), - ], - }, - TypeSpec::Alias { - name: "IFarmGcsLocationUpdateResolve", - target: "ReplicaSchemaResult<FarmGcsLocation>", - }, - TypeSpec::Alias { - name: "IFarmMemberClaimCreate", - target: "IFarmMemberClaimFields", - }, - TypeSpec::Alias { - name: "IFarmMemberClaimCreateResolve", - target: "ReplicaSchemaResult<FarmMemberClaim>", - }, - TypeSpec::Alias { - name: "IFarmMemberClaimDelete", - target: "IFarmMemberClaimFindOne", - }, - TypeSpec::Alias { - name: "IFarmMemberClaimDeleteResolve", - target: "ReplicaSchemaResult<string>", - }, - TypeSpec::Object { - name: "IFarmMemberClaimFields", - fields: &[ - FieldSpec::required("member_pubkey", "string"), - FieldSpec::required("farm_pubkey", "string"), - ], - }, - TypeSpec::Object { - name: "IFarmMemberClaimFieldsFilter", - fields: &[ - FieldSpec::optional("id", "string"), - FieldSpec::optional("created_at", "string"), - FieldSpec::optional("updated_at", "string"), - FieldSpec::optional("member_pubkey", "string"), - FieldSpec::optional("farm_pubkey", "string"), - ], - }, - TypeSpec::Object { - name: "IFarmMemberClaimFieldsPartial", - fields: &[ - FieldSpec::optional_nullable("member_pubkey", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("farm_pubkey", "ReplicaDbJsonValue"), - ], - }, - TypeSpec::Alias { - name: "IFarmMemberClaimFindMany", - target: "IFarmMemberClaimFindManyArgs", - }, - TypeSpec::Object { - name: "IFarmMemberClaimFindManyArgs", - fields: &[FieldSpec::nullable( - "filter", - "IFarmMemberClaimFieldsFilter", - )], - }, - TypeSpec::Alias { - name: "IFarmMemberClaimFindManyResolve", - target: "ReplicaSchemaResultList<FarmMemberClaim>", - }, - TypeSpec::Union { - name: "IFarmMemberClaimFindOne", - variants: &[VariantSpec::Ref("IFarmMemberClaimFindOneArgs")], - }, - TypeSpec::Object { - name: "IFarmMemberClaimFindOneArgs", - fields: &[FieldSpec::required("on", "FarmMemberClaimQueryBindValues")], - }, - TypeSpec::Alias { - name: "IFarmMemberClaimFindOneResolve", - target: "ReplicaSchemaResult<FarmMemberClaim | null>", - }, - TypeSpec::Alias { - name: "IFarmMemberClaimUpdate", - target: "IFarmMemberClaimUpdateArgs", - }, - TypeSpec::Object { - name: "IFarmMemberClaimUpdateArgs", - fields: &[ - FieldSpec::required("on", "FarmMemberClaimQueryBindValues"), - FieldSpec::required("fields", "IFarmMemberClaimFieldsPartial"), - ], - }, - TypeSpec::Alias { - name: "IFarmMemberClaimUpdateResolve", - target: "ReplicaSchemaResult<FarmMemberClaim>", - }, - TypeSpec::Alias { - name: "IFarmMemberCreate", - target: "IFarmMemberFields", - }, - TypeSpec::Alias { - name: "IFarmMemberCreateResolve", - target: "ReplicaSchemaResult<FarmMember>", - }, - TypeSpec::Alias { - name: "IFarmMemberDelete", - target: "IFarmMemberFindOne", - }, - TypeSpec::Alias { - name: "IFarmMemberDeleteResolve", - target: "ReplicaSchemaResult<string>", - }, - TypeSpec::Object { - name: "IFarmMemberFields", - fields: &[ - FieldSpec::required("farm_id", "string"), - FieldSpec::required("member_pubkey", "string"), - FieldSpec::required("role", "string"), - ], - }, - TypeSpec::Object { - name: "IFarmMemberFieldsFilter", - fields: &[ - FieldSpec::optional("id", "string"), - FieldSpec::optional("created_at", "string"), - FieldSpec::optional("updated_at", "string"), - FieldSpec::optional("farm_id", "string"), - FieldSpec::optional("member_pubkey", "string"), - FieldSpec::optional("role", "string"), - ], - }, - TypeSpec::Object { - name: "IFarmMemberFieldsPartial", - fields: &[ - FieldSpec::optional_nullable("farm_id", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("member_pubkey", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("role", "ReplicaDbJsonValue"), - ], - }, - TypeSpec::Alias { - name: "IFarmMemberFindMany", - target: "IFarmMemberFindManyArgs", - }, - TypeSpec::Object { - name: "IFarmMemberFindManyArgs", - fields: &[FieldSpec::nullable("filter", "IFarmMemberFieldsFilter")], - }, - TypeSpec::Alias { - name: "IFarmMemberFindManyResolve", - target: "ReplicaSchemaResultList<FarmMember>", - }, - TypeSpec::Union { - name: "IFarmMemberFindOne", - variants: &[VariantSpec::Ref("IFarmMemberFindOneArgs")], - }, - TypeSpec::Object { - name: "IFarmMemberFindOneArgs", - fields: &[FieldSpec::required("on", "FarmMemberQueryBindValues")], - }, - TypeSpec::Alias { - name: "IFarmMemberFindOneResolve", - target: "ReplicaSchemaResult<FarmMember | null>", - }, - TypeSpec::Alias { - name: "IFarmMemberUpdate", - target: "IFarmMemberUpdateArgs", - }, - TypeSpec::Object { - name: "IFarmMemberUpdateArgs", - fields: &[ - FieldSpec::required("on", "FarmMemberQueryBindValues"), - FieldSpec::required("fields", "IFarmMemberFieldsPartial"), - ], - }, - TypeSpec::Alias { - name: "IFarmMemberUpdateResolve", - target: "ReplicaSchemaResult<FarmMember>", - }, - TypeSpec::Alias { - name: "IFarmTagCreate", - target: "IFarmTagFields", - }, - TypeSpec::Alias { - name: "IFarmTagCreateResolve", - target: "ReplicaSchemaResult<FarmTag>", - }, - TypeSpec::Alias { - name: "IFarmTagDelete", - target: "IFarmTagFindOne", - }, - TypeSpec::Alias { - name: "IFarmTagDeleteResolve", - target: "ReplicaSchemaResult<string>", - }, - TypeSpec::Object { - name: "IFarmTagFields", - fields: &[ - FieldSpec::required("farm_id", "string"), - FieldSpec::required("tag", "string"), - ], - }, - TypeSpec::Object { - name: "IFarmTagFieldsFilter", - fields: &[ - FieldSpec::optional("id", "string"), - FieldSpec::optional("created_at", "string"), - FieldSpec::optional("updated_at", "string"), - FieldSpec::optional("farm_id", "string"), - FieldSpec::optional("tag", "string"), - ], - }, - TypeSpec::Object { - name: "IFarmTagFieldsPartial", - fields: &[ - FieldSpec::optional_nullable("farm_id", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("tag", "ReplicaDbJsonValue"), - ], - }, - TypeSpec::Alias { - name: "IFarmTagFindMany", - target: "IFarmTagFindManyArgs", - }, - TypeSpec::Object { - name: "IFarmTagFindManyArgs", - fields: &[FieldSpec::nullable("filter", "IFarmTagFieldsFilter")], - }, - TypeSpec::Alias { - name: "IFarmTagFindManyResolve", - target: "ReplicaSchemaResultList<FarmTag>", - }, - TypeSpec::Union { - name: "IFarmTagFindOne", - variants: &[VariantSpec::Ref("IFarmTagFindOneArgs")], - }, - TypeSpec::Object { - name: "IFarmTagFindOneArgs", - fields: &[FieldSpec::required("on", "FarmTagQueryBindValues")], - }, - TypeSpec::Alias { - name: "IFarmTagFindOneResolve", - target: "ReplicaSchemaResult<FarmTag | null>", - }, - TypeSpec::Alias { - name: "IFarmTagUpdate", - target: "IFarmTagUpdateArgs", - }, - TypeSpec::Object { - name: "IFarmTagUpdateArgs", - fields: &[ - FieldSpec::required("on", "FarmTagQueryBindValues"), - FieldSpec::required("fields", "IFarmTagFieldsPartial"), - ], - }, - TypeSpec::Alias { - name: "IFarmTagUpdateResolve", - target: "ReplicaSchemaResult<FarmTag>", - }, - TypeSpec::Alias { - name: "IFarmUpdate", - target: "IFarmUpdateArgs", - }, - TypeSpec::Object { - name: "IFarmUpdateArgs", - fields: &[ - FieldSpec::required("on", "FarmQueryBindValues"), - FieldSpec::required("fields", "IFarmFieldsPartial"), - ], - }, - TypeSpec::Alias { - name: "IFarmUpdateResolve", - target: "ReplicaSchemaResult<Farm>", - }, - TypeSpec::Alias { - name: "IGcsLocationCreate", - target: "IGcsLocationFields", - }, - TypeSpec::Alias { - name: "IGcsLocationCreateResolve", - target: "ReplicaSchemaResult<GcsLocation>", - }, - TypeSpec::Alias { - name: "IGcsLocationDelete", - target: "IGcsLocationFindOne", - }, - TypeSpec::Alias { - name: "IGcsLocationDeleteResolve", - target: "ReplicaSchemaResult<string>", - }, - TypeSpec::Object { - name: "IGcsLocationFields", - fields: &[ - FieldSpec::required("d_tag", "string"), - FieldSpec::required("lat", "number"), - FieldSpec::required("lng", "number"), - FieldSpec::required("geohash", "string"), - FieldSpec::required("point", "string"), - FieldSpec::required("polygon", "string"), - FieldSpec::optional_nullable("accuracy", "number"), - FieldSpec::optional_nullable("altitude", "number"), - FieldSpec::optional_nullable("tag_0", "string"), - FieldSpec::optional_nullable("label", "string"), - FieldSpec::optional_nullable("area", "number"), - FieldSpec::optional_nullable("elevation", "number"), - FieldSpec::optional_nullable("soil", "string"), - FieldSpec::optional_nullable("climate", "string"), - FieldSpec::optional_nullable("gc_id", "string"), - FieldSpec::optional_nullable("gc_name", "string"), - FieldSpec::optional_nullable("gc_admin1_id", "string"), - FieldSpec::optional_nullable("gc_admin1_name", "string"), - FieldSpec::optional_nullable("gc_country_id", "string"), - FieldSpec::optional_nullable("gc_country_name", "string"), - ], - }, - TypeSpec::Object { - name: "IGcsLocationFieldsFilter", - fields: &[ - FieldSpec::optional("id", "string"), - FieldSpec::optional("created_at", "string"), - FieldSpec::optional("updated_at", "string"), - FieldSpec::optional("d_tag", "string"), - FieldSpec::optional("lat", "number"), - FieldSpec::optional("lng", "number"), - FieldSpec::optional("geohash", "string"), - FieldSpec::optional("point", "string"), - FieldSpec::optional("polygon", "string"), - FieldSpec::optional("accuracy", "number"), - FieldSpec::optional("altitude", "number"), - FieldSpec::optional("tag_0", "string"), - FieldSpec::optional("label", "string"), - FieldSpec::optional("area", "number"), - FieldSpec::optional("elevation", "number"), - FieldSpec::optional("soil", "string"), - FieldSpec::optional("climate", "string"), - FieldSpec::optional("gc_id", "string"), - FieldSpec::optional("gc_name", "string"), - FieldSpec::optional("gc_admin1_id", "string"), - FieldSpec::optional("gc_admin1_name", "string"), - FieldSpec::optional("gc_country_id", "string"), - FieldSpec::optional("gc_country_name", "string"), - ], - }, - TypeSpec::Object { - name: "IGcsLocationFieldsPartial", - fields: &[ - FieldSpec::optional_nullable("d_tag", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("lat", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("lng", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("geohash", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("point", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("polygon", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("accuracy", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("altitude", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("tag_0", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("label", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("area", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("elevation", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("soil", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("climate", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("gc_id", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("gc_name", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("gc_admin1_id", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("gc_admin1_name", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("gc_country_id", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("gc_country_name", "ReplicaDbJsonValue"), - ], - }, - TypeSpec::Union { - name: "IGcsLocationFindMany", - variants: &[ - VariantSpec::Object(&[FieldSpec::nullable("filter", "IGcsLocationFieldsFilter")]), - VariantSpec::Object(&[FieldSpec::required("rel", "GcsLocationFindManyRel")]), - ], - }, - TypeSpec::Alias { - name: "IGcsLocationFindManyResolve", - target: "ReplicaSchemaResultList<GcsLocation>", - }, - TypeSpec::Union { - name: "IGcsLocationFindOne", - variants: &[ - VariantSpec::Ref("IGcsLocationFindOneArgs"), - VariantSpec::Ref("IGcsLocationFindOneRelArgs"), - ], - }, - TypeSpec::Object { - name: "IGcsLocationFindOneArgs", - fields: &[FieldSpec::required("on", "GcsLocationQueryBindValues")], - }, - TypeSpec::Object { - name: "IGcsLocationFindOneRelArgs", - fields: &[FieldSpec::required("rel", "GcsLocationFindManyRel")], - }, - TypeSpec::Alias { - name: "IGcsLocationFindOneResolve", - target: "ReplicaSchemaResult<GcsLocation | null>", - }, - TypeSpec::Alias { - name: "IGcsLocationUpdate", - target: "IGcsLocationUpdateArgs", - }, - TypeSpec::Object { - name: "IGcsLocationUpdateArgs", - fields: &[ - FieldSpec::required("on", "GcsLocationQueryBindValues"), - FieldSpec::required("fields", "IGcsLocationFieldsPartial"), - ], - }, - TypeSpec::Alias { - name: "IGcsLocationUpdateResolve", - target: "ReplicaSchemaResult<GcsLocation>", - }, - TypeSpec::Alias { - name: "ILogErrorCreate", - target: "ILogErrorFields", - }, - TypeSpec::Alias { - name: "ILogErrorCreateResolve", - target: "ReplicaSchemaResult<LogError>", - }, - TypeSpec::Alias { - name: "ILogErrorDelete", - target: "ILogErrorFindOne", - }, - TypeSpec::Alias { - name: "ILogErrorDeleteResolve", - target: "ReplicaSchemaResult<string>", - }, - TypeSpec::Object { - name: "ILogErrorFields", - fields: &[ - FieldSpec::required("error", "string"), - FieldSpec::required("message", "string"), - FieldSpec::optional_nullable("stack_trace", "string"), - FieldSpec::optional_nullable("cause", "string"), - FieldSpec::required("app_system", "string"), - FieldSpec::required("app_version", "string"), - FieldSpec::required("nostr_pubkey", "string"), - FieldSpec::optional_nullable("data", "string"), - ], - }, - TypeSpec::Object { - name: "ILogErrorFieldsFilter", - fields: &[ - FieldSpec::optional("id", "string"), - FieldSpec::optional("created_at", "string"), - FieldSpec::optional("updated_at", "string"), - FieldSpec::optional("error", "string"), - FieldSpec::optional("message", "string"), - FieldSpec::optional("stack_trace", "string"), - FieldSpec::optional("cause", "string"), - FieldSpec::optional("app_system", "string"), - FieldSpec::optional("app_version", "string"), - FieldSpec::optional("nostr_pubkey", "string"), - FieldSpec::optional("data", "string"), - ], - }, - TypeSpec::Object { - name: "ILogErrorFieldsPartial", - fields: &[ - FieldSpec::optional_nullable("error", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("message", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("stack_trace", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("cause", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("app_system", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("app_version", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("nostr_pubkey", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("data", "ReplicaDbJsonValue"), - ], - }, - TypeSpec::Alias { - name: "ILogErrorFindMany", - target: "ILogErrorFindManyArgs", - }, - TypeSpec::Object { - name: "ILogErrorFindManyArgs", - fields: &[FieldSpec::nullable("filter", "ILogErrorFieldsFilter")], - }, - TypeSpec::Alias { - name: "ILogErrorFindManyResolve", - target: "ReplicaSchemaResultList<LogError>", - }, - TypeSpec::Union { - name: "ILogErrorFindOne", - variants: &[VariantSpec::Ref("ILogErrorFindOneArgs")], - }, - TypeSpec::Object { - name: "ILogErrorFindOneArgs", - fields: &[FieldSpec::required("on", "LogErrorQueryBindValues")], - }, - TypeSpec::Alias { - name: "ILogErrorFindOneResolve", - target: "ReplicaSchemaResult<LogError | null>", - }, - TypeSpec::Alias { - name: "ILogErrorUpdate", - target: "ILogErrorUpdateArgs", - }, - TypeSpec::Object { - name: "ILogErrorUpdateArgs", - fields: &[ - FieldSpec::required("on", "LogErrorQueryBindValues"), - FieldSpec::required("fields", "ILogErrorFieldsPartial"), - ], - }, - TypeSpec::Alias { - name: "ILogErrorUpdateResolve", - target: "ReplicaSchemaResult<LogError>", - }, - TypeSpec::Alias { - name: "IMediaImageCreate", - target: "IMediaImageFields", - }, - TypeSpec::Alias { - name: "IMediaImageCreateResolve", - target: "ReplicaSchemaResult<MediaImage>", - }, - TypeSpec::Alias { - name: "IMediaImageDelete", - target: "IMediaImageFindOne", - }, - TypeSpec::Alias { - name: "IMediaImageDeleteResolve", - target: "ReplicaSchemaResult<string>", - }, - TypeSpec::Object { - name: "IMediaImageFields", - fields: &[ - FieldSpec::required("file_path", "string"), - FieldSpec::required("mime_type", "string"), - FieldSpec::required("res_base", "string"), - FieldSpec::required("res_path", "string"), - FieldSpec::optional_nullable("label", "string"), - FieldSpec::optional_nullable("description", "string"), - ], - }, - TypeSpec::Object { - name: "IMediaImageFieldsFilter", - fields: &[ - FieldSpec::optional("id", "string"), - FieldSpec::optional("created_at", "string"), - FieldSpec::optional("updated_at", "string"), - FieldSpec::optional("file_path", "string"), - FieldSpec::optional("mime_type", "string"), - FieldSpec::optional("res_base", "string"), - FieldSpec::optional("res_path", "string"), - FieldSpec::optional("label", "string"), - FieldSpec::optional("description", "string"), - ], - }, - TypeSpec::Object { - name: "IMediaImageFieldsPartial", - fields: &[ - FieldSpec::optional_nullable("file_path", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("mime_type", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("res_base", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("res_path", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("label", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("description", "ReplicaDbJsonValue"), - ], - }, - TypeSpec::Union { - name: "IMediaImageFindMany", - variants: &[ - VariantSpec::Object(&[FieldSpec::nullable("filter", "IMediaImageFieldsFilter")]), - VariantSpec::Object(&[FieldSpec::required("rel", "MediaImageFindManyRel")]), - ], - }, - TypeSpec::Alias { - name: "IMediaImageFindManyResolve", - target: "ReplicaSchemaResultList<MediaImage>", - }, - TypeSpec::Union { - name: "IMediaImageFindOne", - variants: &[ - VariantSpec::Ref("IMediaImageFindOneArgs"), - VariantSpec::Ref("IMediaImageFindOneRelArgs"), - ], - }, - TypeSpec::Object { - name: "IMediaImageFindOneArgs", - fields: &[FieldSpec::required("on", "MediaImageQueryBindValues")], - }, - TypeSpec::Object { - name: "IMediaImageFindOneRelArgs", - fields: &[FieldSpec::required("rel", "MediaImageFindManyRel")], - }, - TypeSpec::Alias { - name: "IMediaImageFindOneResolve", - target: "ReplicaSchemaResult<MediaImage | null>", - }, - TypeSpec::Alias { - name: "IMediaImageUpdate", - target: "IMediaImageUpdateArgs", - }, - TypeSpec::Object { - name: "IMediaImageUpdateArgs", - fields: &[ - FieldSpec::required("on", "MediaImageQueryBindValues"), - FieldSpec::required("fields", "IMediaImageFieldsPartial"), - ], - }, - TypeSpec::Alias { - name: "IMediaImageUpdateResolve", - target: "ReplicaSchemaResult<MediaImage>", - }, - TypeSpec::Alias { - name: "INostrEventHeadCreate", - target: "INostrEventHeadFields", - }, - TypeSpec::Alias { - name: "INostrEventHeadCreateResolve", - target: "ReplicaSchemaResult<NostrEventHead>", - }, - TypeSpec::Alias { - name: "INostrEventHeadDelete", - target: "INostrEventHeadFindOne", - }, - TypeSpec::Alias { - name: "INostrEventHeadDeleteResolve", - target: "ReplicaSchemaResult<string>", - }, - TypeSpec::Object { - name: "INostrEventHeadFields", - fields: &[ - FieldSpec::required("key", "string"), - FieldSpec::required("kind", "number"), - FieldSpec::required("pubkey", "string"), - FieldSpec::required("d_tag", "string"), - FieldSpec::required("last_event_id", "string"), - FieldSpec::required("last_created_at", "number"), - FieldSpec::required("content_hash", "string"), - ], - }, - TypeSpec::Object { - name: "INostrEventHeadFieldsFilter", - fields: &[ - FieldSpec::optional("id", "string"), - FieldSpec::optional("created_at", "string"), - FieldSpec::optional("updated_at", "string"), - FieldSpec::optional("key", "string"), - FieldSpec::optional("kind", "number"), - FieldSpec::optional("pubkey", "string"), - FieldSpec::optional("d_tag", "string"), - FieldSpec::optional("last_event_id", "string"), - FieldSpec::optional("last_created_at", "number"), - FieldSpec::optional("content_hash", "string"), - ], - }, - TypeSpec::Object { - name: "INostrEventHeadFieldsPartial", - fields: &[ - FieldSpec::optional_nullable("key", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("kind", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("pubkey", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("d_tag", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("last_event_id", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("last_created_at", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("content_hash", "ReplicaDbJsonValue"), - ], - }, - TypeSpec::Alias { - name: "INostrEventHeadFindMany", - target: "INostrEventHeadFindManyArgs", - }, - TypeSpec::Object { - name: "INostrEventHeadFindManyArgs", - fields: &[FieldSpec::nullable("filter", "INostrEventHeadFieldsFilter")], - }, - TypeSpec::Alias { - name: "INostrEventHeadFindManyResolve", - target: "ReplicaSchemaResultList<NostrEventHead>", - }, - TypeSpec::Union { - name: "INostrEventHeadFindOne", - variants: &[VariantSpec::Ref("INostrEventHeadFindOneArgs")], - }, - TypeSpec::Object { - name: "INostrEventHeadFindOneArgs", - fields: &[FieldSpec::required("on", "NostrEventHeadQueryBindValues")], - }, - TypeSpec::Alias { - name: "INostrEventHeadFindOneResolve", - target: "ReplicaSchemaResult<NostrEventHead | null>", - }, - TypeSpec::Alias { - name: "INostrEventHeadUpdate", - target: "INostrEventHeadUpdateArgs", - }, - TypeSpec::Object { - name: "INostrEventHeadUpdateArgs", - fields: &[ - FieldSpec::required("on", "NostrEventHeadQueryBindValues"), - FieldSpec::required("fields", "INostrEventHeadFieldsPartial"), - ], - }, - TypeSpec::Alias { - name: "INostrEventHeadUpdateResolve", - target: "ReplicaSchemaResult<NostrEventHead>", - }, - TypeSpec::Alias { - name: "INostrProfileCreate", - target: "INostrProfileFields", - }, - TypeSpec::Alias { - name: "INostrProfileCreateResolve", - target: "ReplicaSchemaResult<NostrProfile>", - }, - TypeSpec::Alias { - name: "INostrProfileDelete", - target: "INostrProfileFindOne", - }, - TypeSpec::Alias { - name: "INostrProfileDeleteResolve", - target: "ReplicaSchemaResult<string>", - }, - TypeSpec::Object { - name: "INostrProfileFields", - fields: &[ - FieldSpec::required("public_key", "string"), - FieldSpec::required("profile_type", "string"), - FieldSpec::required("name", "string"), - FieldSpec::optional_nullable("display_name", "string"), - FieldSpec::optional_nullable("about", "string"), - FieldSpec::optional_nullable("website", "string"), - FieldSpec::optional_nullable("picture", "string"), - FieldSpec::optional_nullable("banner", "string"), - FieldSpec::optional_nullable("nip05", "string"), - FieldSpec::optional_nullable("lud06", "string"), - FieldSpec::optional_nullable("lud16", "string"), - ], - }, - TypeSpec::Object { - name: "INostrProfileFieldsFilter", - fields: &[ - FieldSpec::optional("id", "string"), - FieldSpec::optional("created_at", "string"), - FieldSpec::optional("updated_at", "string"), - FieldSpec::optional("public_key", "string"), - FieldSpec::optional("profile_type", "string"), - FieldSpec::optional("name", "string"), - FieldSpec::optional("display_name", "string"), - FieldSpec::optional("about", "string"), - FieldSpec::optional("website", "string"), - FieldSpec::optional("picture", "string"), - FieldSpec::optional("banner", "string"), - FieldSpec::optional("nip05", "string"), - FieldSpec::optional("lud06", "string"), - FieldSpec::optional("lud16", "string"), - ], - }, - TypeSpec::Object { - name: "INostrProfileFieldsPartial", - fields: &[ - FieldSpec::optional_nullable("public_key", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("profile_type", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("name", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("display_name", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("about", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("website", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("picture", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("banner", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("nip05", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("lud06", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("lud16", "ReplicaDbJsonValue"), - ], - }, - TypeSpec::Union { - name: "INostrProfileFindMany", - variants: &[ - VariantSpec::Object(&[FieldSpec::nullable("filter", "INostrProfileFieldsFilter")]), - VariantSpec::Object(&[FieldSpec::required("rel", "NostrProfileFindManyRel")]), - ], - }, - TypeSpec::Alias { - name: "INostrProfileFindManyResolve", - target: "ReplicaSchemaResultList<NostrProfile>", - }, - TypeSpec::Union { - name: "INostrProfileFindOne", - variants: &[ - VariantSpec::Ref("INostrProfileFindOneArgs"), - VariantSpec::Ref("INostrProfileFindOneRelArgs"), - ], - }, - TypeSpec::Object { - name: "INostrProfileFindOneArgs", - fields: &[FieldSpec::required("on", "NostrProfileQueryBindValues")], - }, - TypeSpec::Object { - name: "INostrProfileFindOneRelArgs", - fields: &[FieldSpec::required("rel", "NostrProfileFindManyRel")], - }, - TypeSpec::Alias { - name: "INostrProfileFindOneResolve", - target: "ReplicaSchemaResult<NostrProfile | null>", - }, - TypeSpec::Object { - name: "INostrProfileRelayRelation", - fields: &[ - FieldSpec::required("nostr_profile", "NostrProfileQueryBindValues"), - FieldSpec::required("nostr_relay", "NostrRelayQueryBindValues"), - ], - }, - TypeSpec::Alias { - name: "INostrProfileRelayResolve", - target: "ReplicaSchemaResultPass", - }, - TypeSpec::Alias { - name: "INostrProfileUpdate", - target: "INostrProfileUpdateArgs", - }, - TypeSpec::Object { - name: "INostrProfileUpdateArgs", - fields: &[ - FieldSpec::required("on", "NostrProfileQueryBindValues"), - FieldSpec::required("fields", "INostrProfileFieldsPartial"), - ], - }, - TypeSpec::Alias { - name: "INostrProfileUpdateResolve", - target: "ReplicaSchemaResult<NostrProfile>", - }, - TypeSpec::Alias { - name: "INostrRelayCreate", - target: "INostrRelayFields", - }, - TypeSpec::Alias { - name: "INostrRelayCreateResolve", - target: "ReplicaSchemaResult<NostrRelay>", - }, - TypeSpec::Alias { - name: "INostrRelayDelete", - target: "INostrRelayFindOne", - }, - TypeSpec::Alias { - name: "INostrRelayDeleteResolve", - target: "ReplicaSchemaResult<string>", - }, - TypeSpec::Object { - name: "INostrRelayFields", - fields: &[ - FieldSpec::required("url", "string"), - FieldSpec::optional_nullable("relay_id", "string"), - FieldSpec::optional_nullable("name", "string"), - FieldSpec::optional_nullable("description", "string"), - FieldSpec::optional_nullable("pubkey", "string"), - FieldSpec::optional_nullable("contact", "string"), - FieldSpec::optional_nullable("supported_nips", "string"), - FieldSpec::optional_nullable("software", "string"), - FieldSpec::optional_nullable("version", "string"), - FieldSpec::optional_nullable("data", "string"), - ], - }, - TypeSpec::Object { - name: "INostrRelayFieldsFilter", - fields: &[ - FieldSpec::optional("id", "string"), - FieldSpec::optional("created_at", "string"), - FieldSpec::optional("updated_at", "string"), - FieldSpec::optional("url", "string"), - FieldSpec::optional("relay_id", "string"), - FieldSpec::optional("name", "string"), - FieldSpec::optional("description", "string"), - FieldSpec::optional("pubkey", "string"), - FieldSpec::optional("contact", "string"), - FieldSpec::optional("supported_nips", "string"), - FieldSpec::optional("software", "string"), - FieldSpec::optional("version", "string"), - FieldSpec::optional("data", "string"), - ], - }, - TypeSpec::Object { - name: "INostrRelayFieldsPartial", - fields: &[ - FieldSpec::optional_nullable("url", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("relay_id", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("name", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("description", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("pubkey", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("contact", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("supported_nips", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("software", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("version", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("data", "ReplicaDbJsonValue"), - ], - }, - TypeSpec::Union { - name: "INostrRelayFindMany", - variants: &[ - VariantSpec::Object(&[FieldSpec::nullable("filter", "INostrRelayFieldsFilter")]), - VariantSpec::Object(&[FieldSpec::required("rel", "NostrRelayFindManyRel")]), - ], - }, - TypeSpec::Alias { - name: "INostrRelayFindManyResolve", - target: "ReplicaSchemaResultList<NostrRelay>", - }, - TypeSpec::Union { - name: "INostrRelayFindOne", - variants: &[ - VariantSpec::Ref("INostrRelayFindOneArgs"), - VariantSpec::Ref("INostrRelayFindOneRelArgs"), - ], - }, - TypeSpec::Object { - name: "INostrRelayFindOneArgs", - fields: &[FieldSpec::required("on", "NostrRelayQueryBindValues")], - }, - TypeSpec::Object { - name: "INostrRelayFindOneRelArgs", - fields: &[FieldSpec::required("rel", "NostrRelayFindManyRel")], - }, - TypeSpec::Alias { - name: "INostrRelayFindOneResolve", - target: "ReplicaSchemaResult<NostrRelay | null>", - }, - TypeSpec::Alias { - name: "INostrRelayUpdate", - target: "INostrRelayUpdateArgs", - }, - TypeSpec::Object { - name: "INostrRelayUpdateArgs", - fields: &[ - FieldSpec::required("on", "NostrRelayQueryBindValues"), - FieldSpec::required("fields", "INostrRelayFieldsPartial"), - ], - }, - TypeSpec::Alias { - name: "INostrRelayUpdateResolve", - target: "ReplicaSchemaResult<NostrRelay>", - }, - TypeSpec::Alias { - name: "IPlotCreate", - target: "IPlotFields", - }, - TypeSpec::Alias { - name: "IPlotCreateResolve", - target: "ReplicaSchemaResult<Plot>", - }, - TypeSpec::Alias { - name: "IPlotDelete", - target: "IPlotFindOne", - }, - TypeSpec::Alias { - name: "IPlotDeleteResolve", - target: "ReplicaSchemaResult<string>", - }, - TypeSpec::Object { - name: "IPlotFields", - fields: &[ - FieldSpec::required("d_tag", "string"), - FieldSpec::required("farm_id", "string"), - FieldSpec::required("name", "string"), - FieldSpec::optional_nullable("about", "string"), - FieldSpec::optional_nullable("location_primary", "string"), - FieldSpec::optional_nullable("location_city", "string"), - FieldSpec::optional_nullable("location_region", "string"), - FieldSpec::optional_nullable("location_country", "string"), - ], - }, - TypeSpec::Object { - name: "IPlotFieldsFilter", - fields: &[ - FieldSpec::optional("id", "string"), - FieldSpec::optional("created_at", "string"), - FieldSpec::optional("updated_at", "string"), - FieldSpec::optional("d_tag", "string"), - FieldSpec::optional("farm_id", "string"), - FieldSpec::optional("name", "string"), - FieldSpec::optional("about", "string"), - FieldSpec::optional("location_primary", "string"), - FieldSpec::optional("location_city", "string"), - FieldSpec::optional("location_region", "string"), - FieldSpec::optional("location_country", "string"), - ], - }, - TypeSpec::Object { - name: "IPlotFieldsPartial", - fields: &[ - FieldSpec::optional_nullable("d_tag", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("farm_id", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("name", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("about", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("location_primary", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("location_city", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("location_region", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("location_country", "ReplicaDbJsonValue"), - ], - }, - TypeSpec::Alias { - name: "IPlotFindMany", - target: "IPlotFindManyArgs", - }, - TypeSpec::Object { - name: "IPlotFindManyArgs", - fields: &[FieldSpec::nullable("filter", "IPlotFieldsFilter")], - }, - TypeSpec::Alias { - name: "IPlotFindManyResolve", - target: "ReplicaSchemaResultList<Plot>", - }, - TypeSpec::Union { - name: "IPlotFindOne", - variants: &[VariantSpec::Ref("IPlotFindOneArgs")], - }, - TypeSpec::Object { - name: "IPlotFindOneArgs", - fields: &[FieldSpec::required("on", "PlotQueryBindValues")], - }, - TypeSpec::Alias { - name: "IPlotFindOneResolve", - target: "ReplicaSchemaResult<Plot | null>", - }, - TypeSpec::Alias { - name: "IPlotGcsLocationCreate", - target: "IPlotGcsLocationFields", - }, - TypeSpec::Alias { - name: "IPlotGcsLocationCreateResolve", - target: "ReplicaSchemaResult<PlotGcsLocation>", - }, - TypeSpec::Alias { - name: "IPlotGcsLocationDelete", - target: "IPlotGcsLocationFindOne", - }, - TypeSpec::Alias { - name: "IPlotGcsLocationDeleteResolve", - target: "ReplicaSchemaResult<string>", - }, - TypeSpec::Object { - name: "IPlotGcsLocationFields", - fields: &[ - FieldSpec::required("plot_id", "string"), - FieldSpec::required("gcs_location_id", "string"), - FieldSpec::required("role", "string"), - ], - }, - TypeSpec::Object { - name: "IPlotGcsLocationFieldsFilter", - fields: &[ - FieldSpec::optional("id", "string"), - FieldSpec::optional("created_at", "string"), - FieldSpec::optional("updated_at", "string"), - FieldSpec::optional("plot_id", "string"), - FieldSpec::optional("gcs_location_id", "string"), - FieldSpec::optional("role", "string"), - ], - }, - TypeSpec::Object { - name: "IPlotGcsLocationFieldsPartial", - fields: &[ - FieldSpec::optional_nullable("plot_id", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("gcs_location_id", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("role", "ReplicaDbJsonValue"), - ], - }, - TypeSpec::Alias { - name: "IPlotGcsLocationFindMany", - target: "IPlotGcsLocationFindManyArgs", - }, - TypeSpec::Object { - name: "IPlotGcsLocationFindManyArgs", - fields: &[FieldSpec::nullable( - "filter", - "IPlotGcsLocationFieldsFilter", - )], - }, - TypeSpec::Alias { - name: "IPlotGcsLocationFindManyResolve", - target: "ReplicaSchemaResultList<PlotGcsLocation>", - }, - TypeSpec::Union { - name: "IPlotGcsLocationFindOne", - variants: &[VariantSpec::Ref("IPlotGcsLocationFindOneArgs")], - }, - TypeSpec::Object { - name: "IPlotGcsLocationFindOneArgs", - fields: &[FieldSpec::required("on", "PlotGcsLocationQueryBindValues")], - }, - TypeSpec::Alias { - name: "IPlotGcsLocationFindOneResolve", - target: "ReplicaSchemaResult<PlotGcsLocation | null>", - }, - TypeSpec::Alias { - name: "IPlotGcsLocationUpdate", - target: "IPlotGcsLocationUpdateArgs", - }, - TypeSpec::Object { - name: "IPlotGcsLocationUpdateArgs", - fields: &[ - FieldSpec::required("on", "PlotGcsLocationQueryBindValues"), - FieldSpec::required("fields", "IPlotGcsLocationFieldsPartial"), - ], - }, - TypeSpec::Alias { - name: "IPlotGcsLocationUpdateResolve", - target: "ReplicaSchemaResult<PlotGcsLocation>", - }, - TypeSpec::Alias { - name: "IPlotTagCreate", - target: "IPlotTagFields", - }, - TypeSpec::Alias { - name: "IPlotTagCreateResolve", - target: "ReplicaSchemaResult<PlotTag>", - }, - TypeSpec::Alias { - name: "IPlotTagDelete", - target: "IPlotTagFindOne", - }, - TypeSpec::Alias { - name: "IPlotTagDeleteResolve", - target: "ReplicaSchemaResult<string>", - }, - TypeSpec::Object { - name: "IPlotTagFields", - fields: &[ - FieldSpec::required("plot_id", "string"), - FieldSpec::required("tag", "string"), - ], - }, - TypeSpec::Object { - name: "IPlotTagFieldsFilter", - fields: &[ - FieldSpec::optional("id", "string"), - FieldSpec::optional("created_at", "string"), - FieldSpec::optional("updated_at", "string"), - FieldSpec::optional("plot_id", "string"), - FieldSpec::optional("tag", "string"), - ], - }, - TypeSpec::Object { - name: "IPlotTagFieldsPartial", - fields: &[ - FieldSpec::optional_nullable("plot_id", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("tag", "ReplicaDbJsonValue"), - ], - }, - TypeSpec::Alias { - name: "IPlotTagFindMany", - target: "IPlotTagFindManyArgs", - }, - TypeSpec::Object { - name: "IPlotTagFindManyArgs", - fields: &[FieldSpec::nullable("filter", "IPlotTagFieldsFilter")], - }, - TypeSpec::Alias { - name: "IPlotTagFindManyResolve", - target: "ReplicaSchemaResultList<PlotTag>", - }, - TypeSpec::Union { - name: "IPlotTagFindOne", - variants: &[VariantSpec::Ref("IPlotTagFindOneArgs")], - }, - TypeSpec::Object { - name: "IPlotTagFindOneArgs", - fields: &[FieldSpec::required("on", "PlotTagQueryBindValues")], - }, - TypeSpec::Alias { - name: "IPlotTagFindOneResolve", - target: "ReplicaSchemaResult<PlotTag | null>", - }, - TypeSpec::Alias { - name: "IPlotTagUpdate", - target: "IPlotTagUpdateArgs", - }, - TypeSpec::Object { - name: "IPlotTagUpdateArgs", - fields: &[ - FieldSpec::required("on", "PlotTagQueryBindValues"), - FieldSpec::required("fields", "IPlotTagFieldsPartial"), - ], - }, - TypeSpec::Alias { - name: "IPlotTagUpdateResolve", - target: "ReplicaSchemaResult<PlotTag>", - }, - TypeSpec::Alias { - name: "IPlotUpdate", - target: "IPlotUpdateArgs", - }, - TypeSpec::Object { - name: "IPlotUpdateArgs", - fields: &[ - FieldSpec::required("on", "PlotQueryBindValues"), - FieldSpec::required("fields", "IPlotFieldsPartial"), - ], - }, - TypeSpec::Alias { - name: "IPlotUpdateResolve", - target: "ReplicaSchemaResult<Plot>", - }, - TypeSpec::Alias { - name: "ITradeProductCreate", - target: "ITradeProductFields", - }, - TypeSpec::Alias { - name: "ITradeProductCreateResolve", - target: "ReplicaSchemaResult<TradeProduct>", - }, - TypeSpec::Alias { - name: "ITradeProductDelete", - target: "ITradeProductFindOne", - }, - TypeSpec::Alias { - name: "ITradeProductDeleteResolve", - target: "ReplicaSchemaResult<string>", - }, - TypeSpec::Object { - name: "ITradeProductFields", - fields: &[ - FieldSpec::required("key", "string"), - FieldSpec::required("category", "string"), - FieldSpec::required("title", "string"), - FieldSpec::required("summary", "string"), - FieldSpec::required("process", "string"), - FieldSpec::required("lot", "string"), - FieldSpec::required("profile", "string"), - FieldSpec::required("year", "bigint"), - FieldSpec::required("qty_amt", "number"), - FieldSpec::required("qty_amt_exact", "string"), - FieldSpec::required("qty_unit", "string"), - FieldSpec::optional_nullable("qty_label", "string"), - FieldSpec::optional_nullable("qty_avail", "bigint"), - FieldSpec::required("price_amt", "number"), - FieldSpec::required("price_amt_exact", "string"), - FieldSpec::required("price_currency", "string"), - FieldSpec::required("price_qty_amt", "number"), - FieldSpec::required("price_qty_amt_exact", "string"), - FieldSpec::required("price_qty_unit", "string"), - FieldSpec::optional_nullable("listing_addr", "string"), - FieldSpec::optional_nullable("primary_bin_id", "string"), - FieldSpec::optional_nullable("verified_primary_bin_id", "string"), - FieldSpec::optional_nullable("notes", "string"), - ], - }, - TypeSpec::Object { - name: "ITradeProductFieldsFilter", - fields: &[ - FieldSpec::optional("id", "string"), - FieldSpec::optional("created_at", "string"), - FieldSpec::optional("updated_at", "string"), - FieldSpec::optional("key", "string"), - FieldSpec::optional("category", "string"), - FieldSpec::optional("title", "string"), - FieldSpec::optional("summary", "string"), - FieldSpec::optional("process", "string"), - FieldSpec::optional("lot", "string"), - FieldSpec::optional("profile", "string"), - FieldSpec::optional("year", "bigint"), - FieldSpec::optional("qty_amt", "number"), - FieldSpec::optional("qty_amt_exact", "string"), - FieldSpec::optional("qty_unit", "string"), - FieldSpec::optional("qty_label", "string"), - FieldSpec::optional("qty_avail", "bigint"), - FieldSpec::optional("price_amt", "number"), - FieldSpec::optional("price_amt_exact", "string"), - FieldSpec::optional("price_currency", "string"), - FieldSpec::optional("price_qty_amt", "number"), - FieldSpec::optional("price_qty_amt_exact", "string"), - FieldSpec::optional("price_qty_unit", "string"), - FieldSpec::optional("listing_addr", "string"), - FieldSpec::optional("primary_bin_id", "string"), - FieldSpec::optional("verified_primary_bin_id", "string"), - FieldSpec::optional("notes", "string"), - ], - }, - TypeSpec::Object { - name: "ITradeProductFieldsPartial", - fields: &[ - FieldSpec::optional_nullable("key", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("category", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("title", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("summary", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("process", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("lot", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("profile", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("year", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("qty_amt", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("qty_amt_exact", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("qty_unit", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("qty_label", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("qty_avail", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("price_amt", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("price_amt_exact", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("price_currency", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("price_qty_amt", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("price_qty_amt_exact", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("price_qty_unit", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("listing_addr", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("primary_bin_id", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("verified_primary_bin_id", "ReplicaDbJsonValue"), - FieldSpec::optional_nullable("notes", "ReplicaDbJsonValue"), - ], - }, - TypeSpec::Alias { - name: "ITradeProductFindMany", - target: "ITradeProductFindManyArgs", - }, - TypeSpec::Object { - name: "ITradeProductFindManyArgs", - fields: &[FieldSpec::nullable("filter", "ITradeProductFieldsFilter")], - }, - TypeSpec::Alias { - name: "ITradeProductFindManyResolve", - target: "ReplicaSchemaResultList<TradeProduct>", - }, - TypeSpec::Union { - name: "ITradeProductFindOne", - variants: &[VariantSpec::Ref("ITradeProductFindOneArgs")], - }, - TypeSpec::Object { - name: "ITradeProductFindOneArgs", - fields: &[FieldSpec::required("on", "TradeProductQueryBindValues")], - }, - TypeSpec::Alias { - name: "ITradeProductFindOneResolve", - target: "ReplicaSchemaResult<TradeProduct | null>", - }, - TypeSpec::Object { - name: "ITradeProductLocationRelation", - fields: &[ - FieldSpec::required("trade_product", "TradeProductQueryBindValues"), - FieldSpec::required("gcs_location", "GcsLocationQueryBindValues"), - ], - }, - TypeSpec::Alias { - name: "ITradeProductLocationResolve", - target: "ReplicaSchemaResultPass", - }, - TypeSpec::Object { - name: "ITradeProductMediaRelation", - fields: &[ - FieldSpec::required("trade_product", "TradeProductQueryBindValues"), - FieldSpec::required("media_image", "MediaImageQueryBindValues"), - ], - }, - TypeSpec::Alias { - name: "ITradeProductMediaResolve", - target: "ReplicaSchemaResultPass", - }, - TypeSpec::Alias { - name: "ITradeProductUpdate", - target: "ITradeProductUpdateArgs", - }, - TypeSpec::Object { - name: "ITradeProductUpdateArgs", - fields: &[ - FieldSpec::required("on", "TradeProductQueryBindValues"), - FieldSpec::required("fields", "ITradeProductFieldsPartial"), - ], - }, - TypeSpec::Alias { - name: "ITradeProductUpdateResolve", - target: "ReplicaSchemaResult<TradeProduct>", - }, - TypeSpec::Object { - name: "LogError", - fields: &[ - FieldSpec::required("id", "string"), - FieldSpec::required("created_at", "string"), - FieldSpec::required("updated_at", "string"), - FieldSpec::required("error", "string"), - FieldSpec::required("message", "string"), - FieldSpec::nullable("stack_trace", "string"), - FieldSpec::nullable("cause", "string"), - FieldSpec::required("app_system", "string"), - FieldSpec::required("app_version", "string"), - FieldSpec::required("nostr_pubkey", "string"), - FieldSpec::nullable("data", "string"), - ], - }, - TypeSpec::Union { - name: "LogErrorQueryBindValues", - variants: &[ - VariantSpec::Object(&[FieldSpec::required("id", "string")]), - VariantSpec::Object(&[FieldSpec::required("nostr_pubkey", "string")]), - ], - }, - TypeSpec::Object { - name: "MediaImage", - fields: &[ - FieldSpec::required("id", "string"), - FieldSpec::required("created_at", "string"), - FieldSpec::required("updated_at", "string"), - FieldSpec::required("file_path", "string"), - FieldSpec::required("mime_type", "string"), - FieldSpec::required("res_base", "string"), - FieldSpec::required("res_path", "string"), - FieldSpec::nullable("label", "string"), - FieldSpec::nullable("description", "string"), - ], - }, - TypeSpec::Union { - name: "MediaImageFindManyRel", - variants: &[ - VariantSpec::Object(&[FieldSpec::required( - "on_trade_product", - "MediaImageTradeProductArgs", - )]), - VariantSpec::Object(&[FieldSpec::required( - "off_trade_product", - "MediaImageTradeProductArgs", - )]), - ], - }, - TypeSpec::Union { - name: "MediaImageQueryBindValues", - variants: &[ - VariantSpec::Object(&[FieldSpec::required("id", "string")]), - VariantSpec::Object(&[FieldSpec::required("file_path", "string")]), - ], - }, - TypeSpec::Object { - name: "MediaImageTradeProductArgs", - fields: &[FieldSpec::required("id", "string")], - }, - TypeSpec::Object { - name: "NostrEventHead", - fields: &[ - FieldSpec::required("id", "string"), - FieldSpec::required("created_at", "string"), - FieldSpec::required("updated_at", "string"), - FieldSpec::required("key", "string"), - FieldSpec::required("kind", "number"), - FieldSpec::required("pubkey", "string"), - FieldSpec::required("d_tag", "string"), - FieldSpec::required("last_event_id", "string"), - FieldSpec::required("last_created_at", "number"), - FieldSpec::required("content_hash", "string"), - ], - }, - TypeSpec::Union { - name: "NostrEventHeadQueryBindValues", - variants: &[ - VariantSpec::Object(&[FieldSpec::required("id", "string")]), - VariantSpec::Object(&[FieldSpec::required("key", "string")]), - ], - }, - TypeSpec::Object { - name: "NostrProfile", - fields: &[ - FieldSpec::required("id", "string"), - FieldSpec::required("created_at", "string"), - FieldSpec::required("updated_at", "string"), - FieldSpec::required("public_key", "string"), - FieldSpec::required("profile_type", "string"), - FieldSpec::required("name", "string"), - FieldSpec::nullable("display_name", "string"), - FieldSpec::nullable("about", "string"), - FieldSpec::nullable("website", "string"), - FieldSpec::nullable("picture", "string"), - FieldSpec::nullable("banner", "string"), - FieldSpec::nullable("nip05", "string"), - FieldSpec::nullable("lud06", "string"), - FieldSpec::nullable("lud16", "string"), - ], - }, - TypeSpec::Union { - name: "NostrProfileFindManyRel", - variants: &[ - VariantSpec::Object(&[FieldSpec::required("on_relay", "NostrProfileRelayArgs")]), - VariantSpec::Object(&[FieldSpec::required("off_relay", "NostrProfileRelayArgs")]), - ], - }, - TypeSpec::Union { - name: "NostrProfileQueryBindValues", - variants: &[ - VariantSpec::Object(&[FieldSpec::required("id", "string")]), - VariantSpec::Object(&[FieldSpec::required("public_key", "string")]), - ], - }, - TypeSpec::Object { - name: "NostrProfileRelayArgs", - fields: &[FieldSpec::required("id", "string")], - }, - TypeSpec::Object { - name: "NostrRelay", - fields: &[ - FieldSpec::required("id", "string"), - FieldSpec::required("created_at", "string"), - FieldSpec::required("updated_at", "string"), - FieldSpec::required("url", "string"), - FieldSpec::nullable("relay_id", "string"), - FieldSpec::nullable("name", "string"), - FieldSpec::nullable("description", "string"), - FieldSpec::nullable("pubkey", "string"), - FieldSpec::nullable("contact", "string"), - FieldSpec::nullable("supported_nips", "string"), - FieldSpec::nullable("software", "string"), - FieldSpec::nullable("version", "string"), - FieldSpec::nullable("data", "string"), - ], - }, - TypeSpec::Union { - name: "NostrRelayFindManyRel", - variants: &[ - VariantSpec::Object(&[FieldSpec::required("on_profile", "NostrRelayProfileArgs")]), - VariantSpec::Object(&[FieldSpec::required("off_profile", "NostrRelayProfileArgs")]), - ], - }, - TypeSpec::Object { - name: "NostrRelayProfileArgs", - fields: &[FieldSpec::required("public_key", "string")], - }, - TypeSpec::Union { - name: "NostrRelayQueryBindValues", - variants: &[ - VariantSpec::Object(&[FieldSpec::required("id", "string")]), - VariantSpec::Object(&[FieldSpec::required("url", "string")]), - ], - }, - TypeSpec::Object { - name: "Plot", - fields: &[ - FieldSpec::required("id", "string"), - FieldSpec::required("created_at", "string"), - FieldSpec::required("updated_at", "string"), - FieldSpec::required("d_tag", "string"), - FieldSpec::required("farm_id", "string"), - FieldSpec::required("name", "string"), - FieldSpec::nullable("about", "string"), - FieldSpec::nullable("location_primary", "string"), - FieldSpec::nullable("location_city", "string"), - FieldSpec::nullable("location_region", "string"), - FieldSpec::nullable("location_country", "string"), - ], - }, - TypeSpec::Object { - name: "PlotGcsLocation", - fields: &[ - FieldSpec::required("id", "string"), - FieldSpec::required("created_at", "string"), - FieldSpec::required("updated_at", "string"), - FieldSpec::required("plot_id", "string"), - FieldSpec::required("gcs_location_id", "string"), - FieldSpec::required("role", "string"), - ], - }, - TypeSpec::Union { - name: "PlotGcsLocationQueryBindValues", - variants: &[ - VariantSpec::Object(&[FieldSpec::required("id", "string")]), - VariantSpec::Object(&[FieldSpec::required("plot_id", "string")]), - VariantSpec::Object(&[FieldSpec::required("gcs_location_id", "string")]), - ], - }, - TypeSpec::Union { - name: "PlotQueryBindValues", - variants: &[ - VariantSpec::Object(&[FieldSpec::required("id", "string")]), - VariantSpec::Object(&[FieldSpec::required("d_tag", "string")]), - VariantSpec::Object(&[FieldSpec::required("farm_id", "string")]), - ], - }, - TypeSpec::Object { - name: "PlotTag", - fields: &[ - FieldSpec::required("id", "string"), - FieldSpec::required("created_at", "string"), - FieldSpec::required("updated_at", "string"), - FieldSpec::required("plot_id", "string"), - FieldSpec::required("tag", "string"), - ], - }, - TypeSpec::Union { - name: "PlotTagQueryBindValues", - variants: &[ - VariantSpec::Object(&[FieldSpec::required("id", "string")]), - VariantSpec::Object(&[FieldSpec::required("plot_id", "string")]), - VariantSpec::Object(&[FieldSpec::required("tag", "string")]), - ], - }, - TypeSpec::Alias { - name: "ReplicaDbJsonValue", - target: "null | boolean | number | string | Array<ReplicaDbJsonValue> | { [key: string]: ReplicaDbJsonValue }", - }, - TypeSpec::Object { - name: "TradeProduct", - fields: &[ - FieldSpec::required("id", "string"), - FieldSpec::required("created_at", "string"), - FieldSpec::required("updated_at", "string"), - FieldSpec::required("key", "string"), - FieldSpec::required("category", "string"), - FieldSpec::required("title", "string"), - FieldSpec::required("summary", "string"), - FieldSpec::required("process", "string"), - FieldSpec::required("lot", "string"), - FieldSpec::required("profile", "string"), - FieldSpec::required("year", "bigint"), - FieldSpec::required("qty_amt", "number"), - FieldSpec::nullable("qty_amt_exact", "string"), - FieldSpec::required("qty_unit", "string"), - FieldSpec::nullable("qty_label", "string"), - FieldSpec::nullable("qty_avail", "bigint"), - FieldSpec::required("price_amt", "number"), - FieldSpec::nullable("price_amt_exact", "string"), - FieldSpec::required("price_currency", "string"), - FieldSpec::required("price_qty_amt", "number"), - FieldSpec::nullable("price_qty_amt_exact", "string"), - FieldSpec::required("price_qty_unit", "string"), - FieldSpec::nullable("listing_addr", "string"), - FieldSpec::nullable("primary_bin_id", "string"), - FieldSpec::nullable("verified_primary_bin_id", "string"), - FieldSpec::nullable("notes", "string"), - ], - }, - TypeSpec::Union { - name: "TradeProductQueryBindValues", - variants: &[VariantSpec::Object(&[FieldSpec::required("id", "string")])], - }, -]; - -#[cfg(test)] -mod tests { - use super::{FieldSpec, TYPE_SPECS, TypeSpec, VariantSpec, dto_registry, type_inventory}; - - #[test] - fn registry_exports_known_schema_types() { - let registry = dto_registry(); - assert!(!registry.has_errors(), "{:?}", registry.diagnostics); - assert!(type_inventory().contains(&"Farm")); - assert!(type_inventory().contains(&"NostrEventHead")); - assert!(type_inventory().contains(&"ReplicaDbJsonValue")); - } - - #[test] - fn source_find_one_resolves_preserve_nullable_result() { - assert!(TYPE_SPECS.iter().any(|spec| matches!( - spec, - TypeSpec::Alias { - name: "IFarmFindOneResolve", - target: "ReplicaSchemaResult<Farm | null>" - } - ))); - } - - #[test] - fn relation_find_many_inputs_preserve_filter_and_rel_variants() { - for (name, filter, rel) in [ - ( - "IGcsLocationFindMany", - "IGcsLocationFieldsFilter", - "GcsLocationFindManyRel", - ), - ( - "IMediaImageFindMany", - "IMediaImageFieldsFilter", - "MediaImageFindManyRel", - ), - ( - "INostrProfileFindMany", - "INostrProfileFieldsFilter", - "NostrProfileFindManyRel", - ), - ( - "INostrRelayFindMany", - "INostrRelayFieldsFilter", - "NostrRelayFindManyRel", - ), - ] { - assert!(TYPE_SPECS.iter().any(|spec| matches!( - spec, - TypeSpec::Union { name: actual_name, variants } - if *actual_name == name - && variants.len() == 2 - && matches!(variants[0], VariantSpec::Object(fields) if fields.len() == 1 && fields[0] == FieldSpec::nullable("filter", filter)) - && matches!(variants[1], VariantSpec::Object(fields) if fields.len() == 1 && fields[0] == FieldSpec::required("rel", rel)) - ))); - } - } - - #[test] - fn serde_json_value_policy_is_explicit() { - assert!(TYPE_SPECS.iter().any(|spec| matches!( - spec, - TypeSpec::Alias { name: "ReplicaDbJsonValue", target } if target.contains("[key: string]: ReplicaDbJsonValue") - ))); - assert!(TYPE_SPECS.iter().any(|spec| matches!( - spec, - TypeSpec::Object { name: "ITradeProductFieldsPartial", fields } if fields.iter().any(|field| field.name == "year" && field.target == "ReplicaDbJsonValue" && field.optional && field.nullable) - ))); - } - - #[test] - fn trade_product_large_integer_policy_is_explicit() { - assert!(TYPE_SPECS.iter().any(|spec| matches!( - spec, - TypeSpec::Object { name: "TradeProduct", fields } if fields.iter().any(|field| field.name == "year" && field.target == "bigint") - ))); - assert!(TYPE_SPECS.iter().any(|spec| matches!( - spec, - TypeSpec::Object { name: "ITradeProductFieldsFilter", fields } if fields.iter().any(|field| field.name == "qty_avail" && field.target == "bigint" && field.optional) - ))); - } -} diff --git a/crates/replica_db_schema/tests/query_bind_values.rs b/crates/replica_db_schema/tests/query_bind_values.rs @@ -1,354 +0,0 @@ -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_head::NostrEventHeadQueryBindValues; -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_head_query_bind_values_cover_all_variants, - NostrEventHeadQueryBindValues::Id { - id: "nostr-event-state-id".to_string() - }, - "id", - "nostr-event-state-id", - [( - NostrEventHeadQueryBindValues::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_schema/Cargo.toml b/crates/replica_schema/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "radroots_replica_schema" +publish = ["crates-io"] +version = "0.1.0-alpha.2" +edition.workspace = true +authors = ["Tyson Lupul <tyson@radroots.org>"] +rust-version.workspace = true +license.workspace = true +description = "Replica schema model for Radroots" +repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/radroots_replica_schema" +readme = "README" + +[lib] +crate-type = ["rlib"] + +[features] +default = [] +dto-bindgen = ["dep:dto_bindgen_core"] + +[dependencies] +dto_bindgen_core = { workspace = true, optional = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } diff --git a/crates/replica_schema/README b/crates/replica_schema/README @@ -0,0 +1,22 @@ +# radroots_replica_schema + +This is the README for `radroots_replica_schema`, which provides replica +schema models for the `radroots` core libraries. + +## Overview + + * schema model types reexported from the `models` module; + * table- and record-oriented value types shared by replica database code; + * optional serialization support through `serde`; + * no runtime backend logic beyond the shared schema model layer. + +## Copyright + +Except as otherwise noted, all files in the `radroots_replica_schema` +distribution are + + Copyright (c) 2025 Tyson Lupul + +For information on usage and redistribution, and for a DISCLAIMER OF ALL +WARRANTIES, see LICENSE included in the `radroots_replica_schema` +distribution. diff --git a/crates/replica_schema/src/dto.rs b/crates/replica_schema/src/dto.rs @@ -0,0 +1,2266 @@ +use dto_bindgen_core::{ + BackendId, DefaultKind, EnumDef, EnumRepr, FieldDef, FieldPresence, GenericParam, IdentName, + Primitive, Registry, RustTypeId, SerializePresence, SourceSpan, StructDef, TargetFieldNames, + TargetOverride, TypeDef, TypeRef, VariantDef, VariantShape, WireFieldNames, +}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum TypeSpec { + Object { + name: &'static str, + fields: &'static [FieldSpec], + }, + Union { + name: &'static str, + variants: &'static [VariantSpec], + }, + Alias { + name: &'static str, + target: &'static str, + }, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum VariantSpec { + Object(&'static [FieldSpec]), + Ref(&'static str), +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct FieldSpec { + name: &'static str, + target: &'static str, + optional: bool, + nullable: bool, +} + +impl FieldSpec { + const fn required(name: &'static str, target: &'static str) -> Self { + Self { + name, + target, + optional: false, + nullable: false, + } + } + + const fn nullable(name: &'static str, target: &'static str) -> Self { + Self { + name, + target, + optional: false, + nullable: true, + } + } + + const fn optional(name: &'static str, target: &'static str) -> Self { + Self { + name, + target, + optional: true, + nullable: false, + } + } + + const fn optional_nullable(name: &'static str, target: &'static str) -> Self { + Self { + name, + target, + optional: true, + nullable: true, + } + } +} + +pub fn dto_registry() -> Registry { + let mut registry = Registry::new(); + register_result_contracts(&mut registry); + for spec in TYPE_SPECS { + let name = spec.name(); + let type_id = registry.register_type( + RustTypeId::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_NAME"), name), + spec.type_def(), + ); + registry.mark_root(type_id); + } + registry +} + +pub fn type_inventory() -> Vec<&'static str> { + RESULT_CONTRACT_NAMES + .iter() + .copied() + .chain(TYPE_SPECS.iter().map(TypeSpec::name)) + .collect() +} + +impl TypeSpec { + fn name(&self) -> &'static str { + match self { + Self::Object { name, .. } | Self::Union { name, .. } | Self::Alias { name, .. } => name, + } + } + + fn type_def(&self) -> TypeDef { + match self { + Self::Object { name, fields } => TypeDef::Struct(object_def(name, fields)), + Self::Union { name, variants } => TypeDef::Enum(union_def(name, variants)), + Self::Alias { name, target } => TypeDef::Enum(alias_def(name, target)), + } + } +} + +fn object_def(name: &str, fields: &[FieldSpec]) -> StructDef { + let mut def = StructDef::new(name, name, source_span()); + for field in fields { + def = def.with_field(field_def(field)); + } + def +} + +fn register_result_contracts(registry: &mut Registry) { + for (name, field_name, field_ty) in [ + ( + "ReplicaSchemaError", + "err", + TypeRef::GenericParam("T".to_owned()), + ), + ( + "ReplicaSchemaResult", + "result", + TypeRef::GenericParam("T".to_owned()), + ), + ( + "ReplicaSchemaResultList", + "results", + TypeRef::vec(TypeRef::GenericParam("T".to_owned())), + ), + ] { + let mut def = StructDef::new(name, name, source_span()) + .with_field(typed_field_def(field_name, field_ty)); + def.generics.push(GenericParam::new("T")); + let type_id = registry.register_type( + RustTypeId::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_NAME"), name), + TypeDef::Struct(def), + ); + registry.mark_root(type_id); + } + + let type_id = registry.register_type( + RustTypeId::new( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + "ReplicaSchemaResultPass", + ), + TypeDef::Struct( + StructDef::new( + "ReplicaSchemaResultPass", + "ReplicaSchemaResultPass", + source_span(), + ) + .with_field(typed_field_def("pass", TypeRef::Primitive(Primitive::Bool))), + ), + ); + registry.mark_root(type_id); +} + +fn union_def(name: &str, variants: &[VariantSpec]) -> EnumDef { + let mut def = EnumDef::new(name, name, EnumRepr::Untagged, source_span()); + for (index, variant) in variants.iter().enumerate() { + def = def.with_variant(match variant { + VariantSpec::Object(fields) => VariantDef::new( + format!("Variant{index}"), + format!("variant{index}"), + VariantShape::Struct(fields.iter().map(field_def).collect()), + source_span(), + ), + VariantSpec::Ref(target) => VariantDef::new( + format!("Variant{index}"), + format!("variant{index}"), + VariantShape::Newtype(ts_ref(target)), + source_span(), + ), + }); + } + def +} + +fn alias_def(name: &str, target: &str) -> EnumDef { + EnumDef::new(name, name, EnumRepr::Untagged, source_span()).with_variant(VariantDef::new( + "Alias", + "alias", + VariantShape::Newtype(ts_ref(target)), + source_span(), + )) +} + +fn field_def(field: &FieldSpec) -> FieldDef { + typed_field_def(field.name, ts_ref(field.target)) + .with_presence(field_presence(field.optional, field.nullable)) +} + +fn typed_field_def(name: &str, ty: TypeRef) -> FieldDef { + FieldDef::new( + IdentName::new(name), + WireFieldNames::same(name), + TargetFieldNames::new(name, name), + ty, + source_span(), + ) +} + +fn field_presence(optional: bool, nullable: bool) -> FieldPresence { + match (optional, nullable) { + (false, false) => FieldPresence::required(), + (false, true) => FieldPresence::nullable_required(), + (true, true) => FieldPresence::optional_nullable(), + (true, false) => FieldPresence { + nullable: false, + required_on_deserialize: false, + default: Some(DefaultKind::NoneValue), + serialize_presence: SerializePresence::Always, + }, + } +} + +fn ts_ref(target: &str) -> TypeRef { + TypeRef::Override(TargetOverride::new(BackendId::TypeScript, target)) +} + +fn source_span() -> SourceSpan { + SourceSpan::new(file!(), line!(), column!()) +} + +const RESULT_CONTRACT_NAMES: &[&str] = &[ + "ReplicaSchemaError", + "ReplicaSchemaResult", + "ReplicaSchemaResultList", + "ReplicaSchemaResultPass", +]; + +const TYPE_SPECS: &[TypeSpec] = &[ + TypeSpec::Object { + name: "Farm", + fields: &[ + FieldSpec::required("id", "string"), + FieldSpec::required("created_at", "string"), + FieldSpec::required("updated_at", "string"), + FieldSpec::required("d_tag", "string"), + FieldSpec::required("pubkey", "string"), + FieldSpec::required("name", "string"), + FieldSpec::nullable("about", "string"), + FieldSpec::nullable("website", "string"), + FieldSpec::nullable("picture", "string"), + FieldSpec::nullable("banner", "string"), + FieldSpec::nullable("location_primary", "string"), + FieldSpec::nullable("location_city", "string"), + FieldSpec::nullable("location_region", "string"), + FieldSpec::nullable("location_country", "string"), + ], + }, + TypeSpec::Object { + name: "FarmGcsLocation", + fields: &[ + FieldSpec::required("id", "string"), + FieldSpec::required("created_at", "string"), + FieldSpec::required("updated_at", "string"), + FieldSpec::required("farm_id", "string"), + FieldSpec::required("gcs_location_id", "string"), + FieldSpec::required("role", "string"), + ], + }, + TypeSpec::Union { + name: "FarmGcsLocationQueryBindValues", + variants: &[ + VariantSpec::Object(&[FieldSpec::required("id", "string")]), + VariantSpec::Object(&[FieldSpec::required("farm_id", "string")]), + VariantSpec::Object(&[FieldSpec::required("gcs_location_id", "string")]), + ], + }, + TypeSpec::Object { + name: "FarmMember", + fields: &[ + FieldSpec::required("id", "string"), + FieldSpec::required("created_at", "string"), + FieldSpec::required("updated_at", "string"), + FieldSpec::required("farm_id", "string"), + FieldSpec::required("member_pubkey", "string"), + FieldSpec::required("role", "string"), + ], + }, + TypeSpec::Object { + name: "FarmMemberClaim", + fields: &[ + FieldSpec::required("id", "string"), + FieldSpec::required("created_at", "string"), + FieldSpec::required("updated_at", "string"), + FieldSpec::required("member_pubkey", "string"), + FieldSpec::required("farm_pubkey", "string"), + ], + }, + TypeSpec::Union { + name: "FarmMemberClaimQueryBindValues", + variants: &[ + VariantSpec::Object(&[FieldSpec::required("id", "string")]), + VariantSpec::Object(&[FieldSpec::required("member_pubkey", "string")]), + VariantSpec::Object(&[FieldSpec::required("farm_pubkey", "string")]), + ], + }, + TypeSpec::Union { + name: "FarmMemberQueryBindValues", + variants: &[ + VariantSpec::Object(&[FieldSpec::required("id", "string")]), + VariantSpec::Object(&[FieldSpec::required("farm_id", "string")]), + VariantSpec::Object(&[FieldSpec::required("member_pubkey", "string")]), + ], + }, + TypeSpec::Union { + name: "FarmQueryBindValues", + variants: &[ + VariantSpec::Object(&[FieldSpec::required("id", "string")]), + VariantSpec::Object(&[FieldSpec::required("d_tag", "string")]), + VariantSpec::Object(&[FieldSpec::required("pubkey", "string")]), + ], + }, + TypeSpec::Object { + name: "FarmTag", + fields: &[ + FieldSpec::required("id", "string"), + FieldSpec::required("created_at", "string"), + FieldSpec::required("updated_at", "string"), + FieldSpec::required("farm_id", "string"), + FieldSpec::required("tag", "string"), + ], + }, + TypeSpec::Union { + name: "FarmTagQueryBindValues", + variants: &[ + VariantSpec::Object(&[FieldSpec::required("id", "string")]), + VariantSpec::Object(&[FieldSpec::required("farm_id", "string")]), + VariantSpec::Object(&[FieldSpec::required("tag", "string")]), + ], + }, + TypeSpec::Object { + name: "GcsLocation", + fields: &[ + FieldSpec::required("id", "string"), + FieldSpec::required("created_at", "string"), + FieldSpec::required("updated_at", "string"), + FieldSpec::required("d_tag", "string"), + FieldSpec::required("lat", "number"), + FieldSpec::required("lng", "number"), + FieldSpec::required("geohash", "string"), + FieldSpec::required("point", "string"), + FieldSpec::required("polygon", "string"), + FieldSpec::nullable("accuracy", "number"), + FieldSpec::nullable("altitude", "number"), + FieldSpec::nullable("tag_0", "string"), + FieldSpec::nullable("label", "string"), + FieldSpec::nullable("area", "number"), + FieldSpec::nullable("elevation", "number"), + FieldSpec::nullable("soil", "string"), + FieldSpec::nullable("climate", "string"), + FieldSpec::nullable("gc_id", "string"), + FieldSpec::nullable("gc_name", "string"), + FieldSpec::nullable("gc_admin1_id", "string"), + FieldSpec::nullable("gc_admin1_name", "string"), + FieldSpec::nullable("gc_country_id", "string"), + FieldSpec::nullable("gc_country_name", "string"), + ], + }, + TypeSpec::Object { + name: "GcsLocationFarmArgs", + fields: &[FieldSpec::required("id", "string")], + }, + TypeSpec::Union { + name: "GcsLocationFindManyRel", + variants: &[ + VariantSpec::Object(&[FieldSpec::required( + "on_trade_product", + "GcsLocationTradeProductArgs", + )]), + VariantSpec::Object(&[FieldSpec::required( + "off_trade_product", + "GcsLocationTradeProductArgs", + )]), + VariantSpec::Object(&[FieldSpec::required("on_farm", "GcsLocationFarmArgs")]), + VariantSpec::Object(&[FieldSpec::required("off_farm", "GcsLocationFarmArgs")]), + VariantSpec::Object(&[FieldSpec::required("on_plot", "GcsLocationPlotArgs")]), + VariantSpec::Object(&[FieldSpec::required("off_plot", "GcsLocationPlotArgs")]), + ], + }, + TypeSpec::Object { + name: "GcsLocationPlotArgs", + fields: &[FieldSpec::required("id", "string")], + }, + TypeSpec::Union { + name: "GcsLocationQueryBindValues", + variants: &[ + VariantSpec::Object(&[FieldSpec::required("id", "string")]), + VariantSpec::Object(&[FieldSpec::required("d_tag", "string")]), + VariantSpec::Object(&[FieldSpec::required("geohash", "string")]), + ], + }, + TypeSpec::Object { + name: "GcsLocationTradeProductArgs", + fields: &[FieldSpec::required("id", "string")], + }, + TypeSpec::Alias { + name: "IFarmCreate", + target: "IFarmFields", + }, + TypeSpec::Alias { + name: "IFarmCreateResolve", + target: "ReplicaSchemaResult<Farm>", + }, + TypeSpec::Alias { + name: "IFarmDelete", + target: "IFarmFindOne", + }, + TypeSpec::Alias { + name: "IFarmDeleteResolve", + target: "ReplicaSchemaResult<string>", + }, + TypeSpec::Object { + name: "IFarmFields", + fields: &[ + FieldSpec::required("d_tag", "string"), + FieldSpec::required("pubkey", "string"), + FieldSpec::required("name", "string"), + FieldSpec::optional_nullable("about", "string"), + FieldSpec::optional_nullable("website", "string"), + FieldSpec::optional_nullable("picture", "string"), + FieldSpec::optional_nullable("banner", "string"), + FieldSpec::optional_nullable("location_primary", "string"), + FieldSpec::optional_nullable("location_city", "string"), + FieldSpec::optional_nullable("location_region", "string"), + FieldSpec::optional_nullable("location_country", "string"), + ], + }, + TypeSpec::Object { + name: "IFarmFieldsFilter", + fields: &[ + FieldSpec::optional("id", "string"), + FieldSpec::optional("created_at", "string"), + FieldSpec::optional("updated_at", "string"), + FieldSpec::optional("d_tag", "string"), + FieldSpec::optional("pubkey", "string"), + FieldSpec::optional("name", "string"), + FieldSpec::optional("about", "string"), + FieldSpec::optional("website", "string"), + FieldSpec::optional("picture", "string"), + FieldSpec::optional("banner", "string"), + FieldSpec::optional("location_primary", "string"), + FieldSpec::optional("location_city", "string"), + FieldSpec::optional("location_region", "string"), + FieldSpec::optional("location_country", "string"), + ], + }, + TypeSpec::Object { + name: "IFarmFieldsPartial", + fields: &[ + FieldSpec::optional_nullable("d_tag", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("pubkey", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("name", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("about", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("website", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("picture", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("banner", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("location_primary", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("location_city", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("location_region", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("location_country", "ReplicaStoreJsonValue"), + ], + }, + TypeSpec::Alias { + name: "IFarmFindMany", + target: "IFarmFindManyArgs", + }, + TypeSpec::Object { + name: "IFarmFindManyArgs", + fields: &[FieldSpec::nullable("filter", "IFarmFieldsFilter")], + }, + TypeSpec::Alias { + name: "IFarmFindManyResolve", + target: "ReplicaSchemaResultList<Farm>", + }, + TypeSpec::Union { + name: "IFarmFindOne", + variants: &[VariantSpec::Ref("IFarmFindOneArgs")], + }, + TypeSpec::Object { + name: "IFarmFindOneArgs", + fields: &[FieldSpec::required("on", "FarmQueryBindValues")], + }, + TypeSpec::Alias { + name: "IFarmFindOneResolve", + target: "ReplicaSchemaResult<Farm | null>", + }, + TypeSpec::Alias { + name: "IFarmGcsLocationCreate", + target: "IFarmGcsLocationFields", + }, + TypeSpec::Alias { + name: "IFarmGcsLocationCreateResolve", + target: "ReplicaSchemaResult<FarmGcsLocation>", + }, + TypeSpec::Alias { + name: "IFarmGcsLocationDelete", + target: "IFarmGcsLocationFindOne", + }, + TypeSpec::Alias { + name: "IFarmGcsLocationDeleteResolve", + target: "ReplicaSchemaResult<string>", + }, + TypeSpec::Object { + name: "IFarmGcsLocationFields", + fields: &[ + FieldSpec::required("farm_id", "string"), + FieldSpec::required("gcs_location_id", "string"), + FieldSpec::required("role", "string"), + ], + }, + TypeSpec::Object { + name: "IFarmGcsLocationFieldsFilter", + fields: &[ + FieldSpec::optional("id", "string"), + FieldSpec::optional("created_at", "string"), + FieldSpec::optional("updated_at", "string"), + FieldSpec::optional("farm_id", "string"), + FieldSpec::optional("gcs_location_id", "string"), + FieldSpec::optional("role", "string"), + ], + }, + TypeSpec::Object { + name: "IFarmGcsLocationFieldsPartial", + fields: &[ + FieldSpec::optional_nullable("farm_id", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("gcs_location_id", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("role", "ReplicaStoreJsonValue"), + ], + }, + TypeSpec::Alias { + name: "IFarmGcsLocationFindMany", + target: "IFarmGcsLocationFindManyArgs", + }, + TypeSpec::Object { + name: "IFarmGcsLocationFindManyArgs", + fields: &[FieldSpec::nullable( + "filter", + "IFarmGcsLocationFieldsFilter", + )], + }, + TypeSpec::Alias { + name: "IFarmGcsLocationFindManyResolve", + target: "ReplicaSchemaResultList<FarmGcsLocation>", + }, + TypeSpec::Union { + name: "IFarmGcsLocationFindOne", + variants: &[VariantSpec::Ref("IFarmGcsLocationFindOneArgs")], + }, + TypeSpec::Object { + name: "IFarmGcsLocationFindOneArgs", + fields: &[FieldSpec::required("on", "FarmGcsLocationQueryBindValues")], + }, + TypeSpec::Alias { + name: "IFarmGcsLocationFindOneResolve", + target: "ReplicaSchemaResult<FarmGcsLocation | null>", + }, + TypeSpec::Alias { + name: "IFarmGcsLocationUpdate", + target: "IFarmGcsLocationUpdateArgs", + }, + TypeSpec::Object { + name: "IFarmGcsLocationUpdateArgs", + fields: &[ + FieldSpec::required("on", "FarmGcsLocationQueryBindValues"), + FieldSpec::required("fields", "IFarmGcsLocationFieldsPartial"), + ], + }, + TypeSpec::Alias { + name: "IFarmGcsLocationUpdateResolve", + target: "ReplicaSchemaResult<FarmGcsLocation>", + }, + TypeSpec::Alias { + name: "IFarmMemberClaimCreate", + target: "IFarmMemberClaimFields", + }, + TypeSpec::Alias { + name: "IFarmMemberClaimCreateResolve", + target: "ReplicaSchemaResult<FarmMemberClaim>", + }, + TypeSpec::Alias { + name: "IFarmMemberClaimDelete", + target: "IFarmMemberClaimFindOne", + }, + TypeSpec::Alias { + name: "IFarmMemberClaimDeleteResolve", + target: "ReplicaSchemaResult<string>", + }, + TypeSpec::Object { + name: "IFarmMemberClaimFields", + fields: &[ + FieldSpec::required("member_pubkey", "string"), + FieldSpec::required("farm_pubkey", "string"), + ], + }, + TypeSpec::Object { + name: "IFarmMemberClaimFieldsFilter", + fields: &[ + FieldSpec::optional("id", "string"), + FieldSpec::optional("created_at", "string"), + FieldSpec::optional("updated_at", "string"), + FieldSpec::optional("member_pubkey", "string"), + FieldSpec::optional("farm_pubkey", "string"), + ], + }, + TypeSpec::Object { + name: "IFarmMemberClaimFieldsPartial", + fields: &[ + FieldSpec::optional_nullable("member_pubkey", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("farm_pubkey", "ReplicaStoreJsonValue"), + ], + }, + TypeSpec::Alias { + name: "IFarmMemberClaimFindMany", + target: "IFarmMemberClaimFindManyArgs", + }, + TypeSpec::Object { + name: "IFarmMemberClaimFindManyArgs", + fields: &[FieldSpec::nullable( + "filter", + "IFarmMemberClaimFieldsFilter", + )], + }, + TypeSpec::Alias { + name: "IFarmMemberClaimFindManyResolve", + target: "ReplicaSchemaResultList<FarmMemberClaim>", + }, + TypeSpec::Union { + name: "IFarmMemberClaimFindOne", + variants: &[VariantSpec::Ref("IFarmMemberClaimFindOneArgs")], + }, + TypeSpec::Object { + name: "IFarmMemberClaimFindOneArgs", + fields: &[FieldSpec::required("on", "FarmMemberClaimQueryBindValues")], + }, + TypeSpec::Alias { + name: "IFarmMemberClaimFindOneResolve", + target: "ReplicaSchemaResult<FarmMemberClaim | null>", + }, + TypeSpec::Alias { + name: "IFarmMemberClaimUpdate", + target: "IFarmMemberClaimUpdateArgs", + }, + TypeSpec::Object { + name: "IFarmMemberClaimUpdateArgs", + fields: &[ + FieldSpec::required("on", "FarmMemberClaimQueryBindValues"), + FieldSpec::required("fields", "IFarmMemberClaimFieldsPartial"), + ], + }, + TypeSpec::Alias { + name: "IFarmMemberClaimUpdateResolve", + target: "ReplicaSchemaResult<FarmMemberClaim>", + }, + TypeSpec::Alias { + name: "IFarmMemberCreate", + target: "IFarmMemberFields", + }, + TypeSpec::Alias { + name: "IFarmMemberCreateResolve", + target: "ReplicaSchemaResult<FarmMember>", + }, + TypeSpec::Alias { + name: "IFarmMemberDelete", + target: "IFarmMemberFindOne", + }, + TypeSpec::Alias { + name: "IFarmMemberDeleteResolve", + target: "ReplicaSchemaResult<string>", + }, + TypeSpec::Object { + name: "IFarmMemberFields", + fields: &[ + FieldSpec::required("farm_id", "string"), + FieldSpec::required("member_pubkey", "string"), + FieldSpec::required("role", "string"), + ], + }, + TypeSpec::Object { + name: "IFarmMemberFieldsFilter", + fields: &[ + FieldSpec::optional("id", "string"), + FieldSpec::optional("created_at", "string"), + FieldSpec::optional("updated_at", "string"), + FieldSpec::optional("farm_id", "string"), + FieldSpec::optional("member_pubkey", "string"), + FieldSpec::optional("role", "string"), + ], + }, + TypeSpec::Object { + name: "IFarmMemberFieldsPartial", + fields: &[ + FieldSpec::optional_nullable("farm_id", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("member_pubkey", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("role", "ReplicaStoreJsonValue"), + ], + }, + TypeSpec::Alias { + name: "IFarmMemberFindMany", + target: "IFarmMemberFindManyArgs", + }, + TypeSpec::Object { + name: "IFarmMemberFindManyArgs", + fields: &[FieldSpec::nullable("filter", "IFarmMemberFieldsFilter")], + }, + TypeSpec::Alias { + name: "IFarmMemberFindManyResolve", + target: "ReplicaSchemaResultList<FarmMember>", + }, + TypeSpec::Union { + name: "IFarmMemberFindOne", + variants: &[VariantSpec::Ref("IFarmMemberFindOneArgs")], + }, + TypeSpec::Object { + name: "IFarmMemberFindOneArgs", + fields: &[FieldSpec::required("on", "FarmMemberQueryBindValues")], + }, + TypeSpec::Alias { + name: "IFarmMemberFindOneResolve", + target: "ReplicaSchemaResult<FarmMember | null>", + }, + TypeSpec::Alias { + name: "IFarmMemberUpdate", + target: "IFarmMemberUpdateArgs", + }, + TypeSpec::Object { + name: "IFarmMemberUpdateArgs", + fields: &[ + FieldSpec::required("on", "FarmMemberQueryBindValues"), + FieldSpec::required("fields", "IFarmMemberFieldsPartial"), + ], + }, + TypeSpec::Alias { + name: "IFarmMemberUpdateResolve", + target: "ReplicaSchemaResult<FarmMember>", + }, + TypeSpec::Alias { + name: "IFarmTagCreate", + target: "IFarmTagFields", + }, + TypeSpec::Alias { + name: "IFarmTagCreateResolve", + target: "ReplicaSchemaResult<FarmTag>", + }, + TypeSpec::Alias { + name: "IFarmTagDelete", + target: "IFarmTagFindOne", + }, + TypeSpec::Alias { + name: "IFarmTagDeleteResolve", + target: "ReplicaSchemaResult<string>", + }, + TypeSpec::Object { + name: "IFarmTagFields", + fields: &[ + FieldSpec::required("farm_id", "string"), + FieldSpec::required("tag", "string"), + ], + }, + TypeSpec::Object { + name: "IFarmTagFieldsFilter", + fields: &[ + FieldSpec::optional("id", "string"), + FieldSpec::optional("created_at", "string"), + FieldSpec::optional("updated_at", "string"), + FieldSpec::optional("farm_id", "string"), + FieldSpec::optional("tag", "string"), + ], + }, + TypeSpec::Object { + name: "IFarmTagFieldsPartial", + fields: &[ + FieldSpec::optional_nullable("farm_id", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("tag", "ReplicaStoreJsonValue"), + ], + }, + TypeSpec::Alias { + name: "IFarmTagFindMany", + target: "IFarmTagFindManyArgs", + }, + TypeSpec::Object { + name: "IFarmTagFindManyArgs", + fields: &[FieldSpec::nullable("filter", "IFarmTagFieldsFilter")], + }, + TypeSpec::Alias { + name: "IFarmTagFindManyResolve", + target: "ReplicaSchemaResultList<FarmTag>", + }, + TypeSpec::Union { + name: "IFarmTagFindOne", + variants: &[VariantSpec::Ref("IFarmTagFindOneArgs")], + }, + TypeSpec::Object { + name: "IFarmTagFindOneArgs", + fields: &[FieldSpec::required("on", "FarmTagQueryBindValues")], + }, + TypeSpec::Alias { + name: "IFarmTagFindOneResolve", + target: "ReplicaSchemaResult<FarmTag | null>", + }, + TypeSpec::Alias { + name: "IFarmTagUpdate", + target: "IFarmTagUpdateArgs", + }, + TypeSpec::Object { + name: "IFarmTagUpdateArgs", + fields: &[ + FieldSpec::required("on", "FarmTagQueryBindValues"), + FieldSpec::required("fields", "IFarmTagFieldsPartial"), + ], + }, + TypeSpec::Alias { + name: "IFarmTagUpdateResolve", + target: "ReplicaSchemaResult<FarmTag>", + }, + TypeSpec::Alias { + name: "IFarmUpdate", + target: "IFarmUpdateArgs", + }, + TypeSpec::Object { + name: "IFarmUpdateArgs", + fields: &[ + FieldSpec::required("on", "FarmQueryBindValues"), + FieldSpec::required("fields", "IFarmFieldsPartial"), + ], + }, + TypeSpec::Alias { + name: "IFarmUpdateResolve", + target: "ReplicaSchemaResult<Farm>", + }, + TypeSpec::Alias { + name: "IGcsLocationCreate", + target: "IGcsLocationFields", + }, + TypeSpec::Alias { + name: "IGcsLocationCreateResolve", + target: "ReplicaSchemaResult<GcsLocation>", + }, + TypeSpec::Alias { + name: "IGcsLocationDelete", + target: "IGcsLocationFindOne", + }, + TypeSpec::Alias { + name: "IGcsLocationDeleteResolve", + target: "ReplicaSchemaResult<string>", + }, + TypeSpec::Object { + name: "IGcsLocationFields", + fields: &[ + FieldSpec::required("d_tag", "string"), + FieldSpec::required("lat", "number"), + FieldSpec::required("lng", "number"), + FieldSpec::required("geohash", "string"), + FieldSpec::required("point", "string"), + FieldSpec::required("polygon", "string"), + FieldSpec::optional_nullable("accuracy", "number"), + FieldSpec::optional_nullable("altitude", "number"), + FieldSpec::optional_nullable("tag_0", "string"), + FieldSpec::optional_nullable("label", "string"), + FieldSpec::optional_nullable("area", "number"), + FieldSpec::optional_nullable("elevation", "number"), + FieldSpec::optional_nullable("soil", "string"), + FieldSpec::optional_nullable("climate", "string"), + FieldSpec::optional_nullable("gc_id", "string"), + FieldSpec::optional_nullable("gc_name", "string"), + FieldSpec::optional_nullable("gc_admin1_id", "string"), + FieldSpec::optional_nullable("gc_admin1_name", "string"), + FieldSpec::optional_nullable("gc_country_id", "string"), + FieldSpec::optional_nullable("gc_country_name", "string"), + ], + }, + TypeSpec::Object { + name: "IGcsLocationFieldsFilter", + fields: &[ + FieldSpec::optional("id", "string"), + FieldSpec::optional("created_at", "string"), + FieldSpec::optional("updated_at", "string"), + FieldSpec::optional("d_tag", "string"), + FieldSpec::optional("lat", "number"), + FieldSpec::optional("lng", "number"), + FieldSpec::optional("geohash", "string"), + FieldSpec::optional("point", "string"), + FieldSpec::optional("polygon", "string"), + FieldSpec::optional("accuracy", "number"), + FieldSpec::optional("altitude", "number"), + FieldSpec::optional("tag_0", "string"), + FieldSpec::optional("label", "string"), + FieldSpec::optional("area", "number"), + FieldSpec::optional("elevation", "number"), + FieldSpec::optional("soil", "string"), + FieldSpec::optional("climate", "string"), + FieldSpec::optional("gc_id", "string"), + FieldSpec::optional("gc_name", "string"), + FieldSpec::optional("gc_admin1_id", "string"), + FieldSpec::optional("gc_admin1_name", "string"), + FieldSpec::optional("gc_country_id", "string"), + FieldSpec::optional("gc_country_name", "string"), + ], + }, + TypeSpec::Object { + name: "IGcsLocationFieldsPartial", + fields: &[ + FieldSpec::optional_nullable("d_tag", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("lat", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("lng", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("geohash", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("point", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("polygon", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("accuracy", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("altitude", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("tag_0", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("label", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("area", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("elevation", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("soil", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("climate", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("gc_id", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("gc_name", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("gc_admin1_id", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("gc_admin1_name", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("gc_country_id", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("gc_country_name", "ReplicaStoreJsonValue"), + ], + }, + TypeSpec::Union { + name: "IGcsLocationFindMany", + variants: &[ + VariantSpec::Object(&[FieldSpec::nullable("filter", "IGcsLocationFieldsFilter")]), + VariantSpec::Object(&[FieldSpec::required("rel", "GcsLocationFindManyRel")]), + ], + }, + TypeSpec::Alias { + name: "IGcsLocationFindManyResolve", + target: "ReplicaSchemaResultList<GcsLocation>", + }, + TypeSpec::Union { + name: "IGcsLocationFindOne", + variants: &[ + VariantSpec::Ref("IGcsLocationFindOneArgs"), + VariantSpec::Ref("IGcsLocationFindOneRelArgs"), + ], + }, + TypeSpec::Object { + name: "IGcsLocationFindOneArgs", + fields: &[FieldSpec::required("on", "GcsLocationQueryBindValues")], + }, + TypeSpec::Object { + name: "IGcsLocationFindOneRelArgs", + fields: &[FieldSpec::required("rel", "GcsLocationFindManyRel")], + }, + TypeSpec::Alias { + name: "IGcsLocationFindOneResolve", + target: "ReplicaSchemaResult<GcsLocation | null>", + }, + TypeSpec::Alias { + name: "IGcsLocationUpdate", + target: "IGcsLocationUpdateArgs", + }, + TypeSpec::Object { + name: "IGcsLocationUpdateArgs", + fields: &[ + FieldSpec::required("on", "GcsLocationQueryBindValues"), + FieldSpec::required("fields", "IGcsLocationFieldsPartial"), + ], + }, + TypeSpec::Alias { + name: "IGcsLocationUpdateResolve", + target: "ReplicaSchemaResult<GcsLocation>", + }, + TypeSpec::Alias { + name: "ILogErrorCreate", + target: "ILogErrorFields", + }, + TypeSpec::Alias { + name: "ILogErrorCreateResolve", + target: "ReplicaSchemaResult<LogError>", + }, + TypeSpec::Alias { + name: "ILogErrorDelete", + target: "ILogErrorFindOne", + }, + TypeSpec::Alias { + name: "ILogErrorDeleteResolve", + target: "ReplicaSchemaResult<string>", + }, + TypeSpec::Object { + name: "ILogErrorFields", + fields: &[ + FieldSpec::required("error", "string"), + FieldSpec::required("message", "string"), + FieldSpec::optional_nullable("stack_trace", "string"), + FieldSpec::optional_nullable("cause", "string"), + FieldSpec::required("app_system", "string"), + FieldSpec::required("app_version", "string"), + FieldSpec::required("nostr_pubkey", "string"), + FieldSpec::optional_nullable("data", "string"), + ], + }, + TypeSpec::Object { + name: "ILogErrorFieldsFilter", + fields: &[ + FieldSpec::optional("id", "string"), + FieldSpec::optional("created_at", "string"), + FieldSpec::optional("updated_at", "string"), + FieldSpec::optional("error", "string"), + FieldSpec::optional("message", "string"), + FieldSpec::optional("stack_trace", "string"), + FieldSpec::optional("cause", "string"), + FieldSpec::optional("app_system", "string"), + FieldSpec::optional("app_version", "string"), + FieldSpec::optional("nostr_pubkey", "string"), + FieldSpec::optional("data", "string"), + ], + }, + TypeSpec::Object { + name: "ILogErrorFieldsPartial", + fields: &[ + FieldSpec::optional_nullable("error", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("message", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("stack_trace", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("cause", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("app_system", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("app_version", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("nostr_pubkey", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("data", "ReplicaStoreJsonValue"), + ], + }, + TypeSpec::Alias { + name: "ILogErrorFindMany", + target: "ILogErrorFindManyArgs", + }, + TypeSpec::Object { + name: "ILogErrorFindManyArgs", + fields: &[FieldSpec::nullable("filter", "ILogErrorFieldsFilter")], + }, + TypeSpec::Alias { + name: "ILogErrorFindManyResolve", + target: "ReplicaSchemaResultList<LogError>", + }, + TypeSpec::Union { + name: "ILogErrorFindOne", + variants: &[VariantSpec::Ref("ILogErrorFindOneArgs")], + }, + TypeSpec::Object { + name: "ILogErrorFindOneArgs", + fields: &[FieldSpec::required("on", "LogErrorQueryBindValues")], + }, + TypeSpec::Alias { + name: "ILogErrorFindOneResolve", + target: "ReplicaSchemaResult<LogError | null>", + }, + TypeSpec::Alias { + name: "ILogErrorUpdate", + target: "ILogErrorUpdateArgs", + }, + TypeSpec::Object { + name: "ILogErrorUpdateArgs", + fields: &[ + FieldSpec::required("on", "LogErrorQueryBindValues"), + FieldSpec::required("fields", "ILogErrorFieldsPartial"), + ], + }, + TypeSpec::Alias { + name: "ILogErrorUpdateResolve", + target: "ReplicaSchemaResult<LogError>", + }, + TypeSpec::Alias { + name: "IMediaImageCreate", + target: "IMediaImageFields", + }, + TypeSpec::Alias { + name: "IMediaImageCreateResolve", + target: "ReplicaSchemaResult<MediaImage>", + }, + TypeSpec::Alias { + name: "IMediaImageDelete", + target: "IMediaImageFindOne", + }, + TypeSpec::Alias { + name: "IMediaImageDeleteResolve", + target: "ReplicaSchemaResult<string>", + }, + TypeSpec::Object { + name: "IMediaImageFields", + fields: &[ + FieldSpec::required("file_path", "string"), + FieldSpec::required("mime_type", "string"), + FieldSpec::required("res_base", "string"), + FieldSpec::required("res_path", "string"), + FieldSpec::optional_nullable("label", "string"), + FieldSpec::optional_nullable("description", "string"), + ], + }, + TypeSpec::Object { + name: "IMediaImageFieldsFilter", + fields: &[ + FieldSpec::optional("id", "string"), + FieldSpec::optional("created_at", "string"), + FieldSpec::optional("updated_at", "string"), + FieldSpec::optional("file_path", "string"), + FieldSpec::optional("mime_type", "string"), + FieldSpec::optional("res_base", "string"), + FieldSpec::optional("res_path", "string"), + FieldSpec::optional("label", "string"), + FieldSpec::optional("description", "string"), + ], + }, + TypeSpec::Object { + name: "IMediaImageFieldsPartial", + fields: &[ + FieldSpec::optional_nullable("file_path", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("mime_type", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("res_base", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("res_path", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("label", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("description", "ReplicaStoreJsonValue"), + ], + }, + TypeSpec::Union { + name: "IMediaImageFindMany", + variants: &[ + VariantSpec::Object(&[FieldSpec::nullable("filter", "IMediaImageFieldsFilter")]), + VariantSpec::Object(&[FieldSpec::required("rel", "MediaImageFindManyRel")]), + ], + }, + TypeSpec::Alias { + name: "IMediaImageFindManyResolve", + target: "ReplicaSchemaResultList<MediaImage>", + }, + TypeSpec::Union { + name: "IMediaImageFindOne", + variants: &[ + VariantSpec::Ref("IMediaImageFindOneArgs"), + VariantSpec::Ref("IMediaImageFindOneRelArgs"), + ], + }, + TypeSpec::Object { + name: "IMediaImageFindOneArgs", + fields: &[FieldSpec::required("on", "MediaImageQueryBindValues")], + }, + TypeSpec::Object { + name: "IMediaImageFindOneRelArgs", + fields: &[FieldSpec::required("rel", "MediaImageFindManyRel")], + }, + TypeSpec::Alias { + name: "IMediaImageFindOneResolve", + target: "ReplicaSchemaResult<MediaImage | null>", + }, + TypeSpec::Alias { + name: "IMediaImageUpdate", + target: "IMediaImageUpdateArgs", + }, + TypeSpec::Object { + name: "IMediaImageUpdateArgs", + fields: &[ + FieldSpec::required("on", "MediaImageQueryBindValues"), + FieldSpec::required("fields", "IMediaImageFieldsPartial"), + ], + }, + TypeSpec::Alias { + name: "IMediaImageUpdateResolve", + target: "ReplicaSchemaResult<MediaImage>", + }, + TypeSpec::Alias { + name: "INostrEventHeadCreate", + target: "INostrEventHeadFields", + }, + TypeSpec::Alias { + name: "INostrEventHeadCreateResolve", + target: "ReplicaSchemaResult<NostrEventHead>", + }, + TypeSpec::Alias { + name: "INostrEventHeadDelete", + target: "INostrEventHeadFindOne", + }, + TypeSpec::Alias { + name: "INostrEventHeadDeleteResolve", + target: "ReplicaSchemaResult<string>", + }, + TypeSpec::Object { + name: "INostrEventHeadFields", + fields: &[ + FieldSpec::required("key", "string"), + FieldSpec::required("kind", "number"), + FieldSpec::required("pubkey", "string"), + FieldSpec::required("d_tag", "string"), + FieldSpec::required("last_event_id", "string"), + FieldSpec::required("last_created_at", "number"), + FieldSpec::required("content_hash", "string"), + ], + }, + TypeSpec::Object { + name: "INostrEventHeadFieldsFilter", + fields: &[ + FieldSpec::optional("id", "string"), + FieldSpec::optional("created_at", "string"), + FieldSpec::optional("updated_at", "string"), + FieldSpec::optional("key", "string"), + FieldSpec::optional("kind", "number"), + FieldSpec::optional("pubkey", "string"), + FieldSpec::optional("d_tag", "string"), + FieldSpec::optional("last_event_id", "string"), + FieldSpec::optional("last_created_at", "number"), + FieldSpec::optional("content_hash", "string"), + ], + }, + TypeSpec::Object { + name: "INostrEventHeadFieldsPartial", + fields: &[ + FieldSpec::optional_nullable("key", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("kind", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("pubkey", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("d_tag", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("last_event_id", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("last_created_at", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("content_hash", "ReplicaStoreJsonValue"), + ], + }, + TypeSpec::Alias { + name: "INostrEventHeadFindMany", + target: "INostrEventHeadFindManyArgs", + }, + TypeSpec::Object { + name: "INostrEventHeadFindManyArgs", + fields: &[FieldSpec::nullable("filter", "INostrEventHeadFieldsFilter")], + }, + TypeSpec::Alias { + name: "INostrEventHeadFindManyResolve", + target: "ReplicaSchemaResultList<NostrEventHead>", + }, + TypeSpec::Union { + name: "INostrEventHeadFindOne", + variants: &[VariantSpec::Ref("INostrEventHeadFindOneArgs")], + }, + TypeSpec::Object { + name: "INostrEventHeadFindOneArgs", + fields: &[FieldSpec::required("on", "NostrEventHeadQueryBindValues")], + }, + TypeSpec::Alias { + name: "INostrEventHeadFindOneResolve", + target: "ReplicaSchemaResult<NostrEventHead | null>", + }, + TypeSpec::Alias { + name: "INostrEventHeadUpdate", + target: "INostrEventHeadUpdateArgs", + }, + TypeSpec::Object { + name: "INostrEventHeadUpdateArgs", + fields: &[ + FieldSpec::required("on", "NostrEventHeadQueryBindValues"), + FieldSpec::required("fields", "INostrEventHeadFieldsPartial"), + ], + }, + TypeSpec::Alias { + name: "INostrEventHeadUpdateResolve", + target: "ReplicaSchemaResult<NostrEventHead>", + }, + TypeSpec::Alias { + name: "INostrProfileCreate", + target: "INostrProfileFields", + }, + TypeSpec::Alias { + name: "INostrProfileCreateResolve", + target: "ReplicaSchemaResult<NostrProfile>", + }, + TypeSpec::Alias { + name: "INostrProfileDelete", + target: "INostrProfileFindOne", + }, + TypeSpec::Alias { + name: "INostrProfileDeleteResolve", + target: "ReplicaSchemaResult<string>", + }, + TypeSpec::Object { + name: "INostrProfileFields", + fields: &[ + FieldSpec::required("public_key", "string"), + FieldSpec::required("profile_type", "string"), + FieldSpec::required("name", "string"), + FieldSpec::optional_nullable("display_name", "string"), + FieldSpec::optional_nullable("about", "string"), + FieldSpec::optional_nullable("website", "string"), + FieldSpec::optional_nullable("picture", "string"), + FieldSpec::optional_nullable("banner", "string"), + FieldSpec::optional_nullable("nip05", "string"), + FieldSpec::optional_nullable("lud06", "string"), + FieldSpec::optional_nullable("lud16", "string"), + ], + }, + TypeSpec::Object { + name: "INostrProfileFieldsFilter", + fields: &[ + FieldSpec::optional("id", "string"), + FieldSpec::optional("created_at", "string"), + FieldSpec::optional("updated_at", "string"), + FieldSpec::optional("public_key", "string"), + FieldSpec::optional("profile_type", "string"), + FieldSpec::optional("name", "string"), + FieldSpec::optional("display_name", "string"), + FieldSpec::optional("about", "string"), + FieldSpec::optional("website", "string"), + FieldSpec::optional("picture", "string"), + FieldSpec::optional("banner", "string"), + FieldSpec::optional("nip05", "string"), + FieldSpec::optional("lud06", "string"), + FieldSpec::optional("lud16", "string"), + ], + }, + TypeSpec::Object { + name: "INostrProfileFieldsPartial", + fields: &[ + FieldSpec::optional_nullable("public_key", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("profile_type", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("name", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("display_name", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("about", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("website", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("picture", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("banner", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("nip05", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("lud06", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("lud16", "ReplicaStoreJsonValue"), + ], + }, + TypeSpec::Union { + name: "INostrProfileFindMany", + variants: &[ + VariantSpec::Object(&[FieldSpec::nullable("filter", "INostrProfileFieldsFilter")]), + VariantSpec::Object(&[FieldSpec::required("rel", "NostrProfileFindManyRel")]), + ], + }, + TypeSpec::Alias { + name: "INostrProfileFindManyResolve", + target: "ReplicaSchemaResultList<NostrProfile>", + }, + TypeSpec::Union { + name: "INostrProfileFindOne", + variants: &[ + VariantSpec::Ref("INostrProfileFindOneArgs"), + VariantSpec::Ref("INostrProfileFindOneRelArgs"), + ], + }, + TypeSpec::Object { + name: "INostrProfileFindOneArgs", + fields: &[FieldSpec::required("on", "NostrProfileQueryBindValues")], + }, + TypeSpec::Object { + name: "INostrProfileFindOneRelArgs", + fields: &[FieldSpec::required("rel", "NostrProfileFindManyRel")], + }, + TypeSpec::Alias { + name: "INostrProfileFindOneResolve", + target: "ReplicaSchemaResult<NostrProfile | null>", + }, + TypeSpec::Object { + name: "INostrProfileRelayRelation", + fields: &[ + FieldSpec::required("nostr_profile", "NostrProfileQueryBindValues"), + FieldSpec::required("nostr_relay", "NostrRelayQueryBindValues"), + ], + }, + TypeSpec::Alias { + name: "INostrProfileRelayResolve", + target: "ReplicaSchemaResultPass", + }, + TypeSpec::Alias { + name: "INostrProfileUpdate", + target: "INostrProfileUpdateArgs", + }, + TypeSpec::Object { + name: "INostrProfileUpdateArgs", + fields: &[ + FieldSpec::required("on", "NostrProfileQueryBindValues"), + FieldSpec::required("fields", "INostrProfileFieldsPartial"), + ], + }, + TypeSpec::Alias { + name: "INostrProfileUpdateResolve", + target: "ReplicaSchemaResult<NostrProfile>", + }, + TypeSpec::Alias { + name: "INostrRelayCreate", + target: "INostrRelayFields", + }, + TypeSpec::Alias { + name: "INostrRelayCreateResolve", + target: "ReplicaSchemaResult<NostrRelay>", + }, + TypeSpec::Alias { + name: "INostrRelayDelete", + target: "INostrRelayFindOne", + }, + TypeSpec::Alias { + name: "INostrRelayDeleteResolve", + target: "ReplicaSchemaResult<string>", + }, + TypeSpec::Object { + name: "INostrRelayFields", + fields: &[ + FieldSpec::required("url", "string"), + FieldSpec::optional_nullable("relay_id", "string"), + FieldSpec::optional_nullable("name", "string"), + FieldSpec::optional_nullable("description", "string"), + FieldSpec::optional_nullable("pubkey", "string"), + FieldSpec::optional_nullable("contact", "string"), + FieldSpec::optional_nullable("supported_nips", "string"), + FieldSpec::optional_nullable("software", "string"), + FieldSpec::optional_nullable("version", "string"), + FieldSpec::optional_nullable("data", "string"), + ], + }, + TypeSpec::Object { + name: "INostrRelayFieldsFilter", + fields: &[ + FieldSpec::optional("id", "string"), + FieldSpec::optional("created_at", "string"), + FieldSpec::optional("updated_at", "string"), + FieldSpec::optional("url", "string"), + FieldSpec::optional("relay_id", "string"), + FieldSpec::optional("name", "string"), + FieldSpec::optional("description", "string"), + FieldSpec::optional("pubkey", "string"), + FieldSpec::optional("contact", "string"), + FieldSpec::optional("supported_nips", "string"), + FieldSpec::optional("software", "string"), + FieldSpec::optional("version", "string"), + FieldSpec::optional("data", "string"), + ], + }, + TypeSpec::Object { + name: "INostrRelayFieldsPartial", + fields: &[ + FieldSpec::optional_nullable("url", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("relay_id", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("name", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("description", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("pubkey", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("contact", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("supported_nips", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("software", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("version", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("data", "ReplicaStoreJsonValue"), + ], + }, + TypeSpec::Union { + name: "INostrRelayFindMany", + variants: &[ + VariantSpec::Object(&[FieldSpec::nullable("filter", "INostrRelayFieldsFilter")]), + VariantSpec::Object(&[FieldSpec::required("rel", "NostrRelayFindManyRel")]), + ], + }, + TypeSpec::Alias { + name: "INostrRelayFindManyResolve", + target: "ReplicaSchemaResultList<NostrRelay>", + }, + TypeSpec::Union { + name: "INostrRelayFindOne", + variants: &[ + VariantSpec::Ref("INostrRelayFindOneArgs"), + VariantSpec::Ref("INostrRelayFindOneRelArgs"), + ], + }, + TypeSpec::Object { + name: "INostrRelayFindOneArgs", + fields: &[FieldSpec::required("on", "NostrRelayQueryBindValues")], + }, + TypeSpec::Object { + name: "INostrRelayFindOneRelArgs", + fields: &[FieldSpec::required("rel", "NostrRelayFindManyRel")], + }, + TypeSpec::Alias { + name: "INostrRelayFindOneResolve", + target: "ReplicaSchemaResult<NostrRelay | null>", + }, + TypeSpec::Alias { + name: "INostrRelayUpdate", + target: "INostrRelayUpdateArgs", + }, + TypeSpec::Object { + name: "INostrRelayUpdateArgs", + fields: &[ + FieldSpec::required("on", "NostrRelayQueryBindValues"), + FieldSpec::required("fields", "INostrRelayFieldsPartial"), + ], + }, + TypeSpec::Alias { + name: "INostrRelayUpdateResolve", + target: "ReplicaSchemaResult<NostrRelay>", + }, + TypeSpec::Alias { + name: "IPlotCreate", + target: "IPlotFields", + }, + TypeSpec::Alias { + name: "IPlotCreateResolve", + target: "ReplicaSchemaResult<Plot>", + }, + TypeSpec::Alias { + name: "IPlotDelete", + target: "IPlotFindOne", + }, + TypeSpec::Alias { + name: "IPlotDeleteResolve", + target: "ReplicaSchemaResult<string>", + }, + TypeSpec::Object { + name: "IPlotFields", + fields: &[ + FieldSpec::required("d_tag", "string"), + FieldSpec::required("farm_id", "string"), + FieldSpec::required("name", "string"), + FieldSpec::optional_nullable("about", "string"), + FieldSpec::optional_nullable("location_primary", "string"), + FieldSpec::optional_nullable("location_city", "string"), + FieldSpec::optional_nullable("location_region", "string"), + FieldSpec::optional_nullable("location_country", "string"), + ], + }, + TypeSpec::Object { + name: "IPlotFieldsFilter", + fields: &[ + FieldSpec::optional("id", "string"), + FieldSpec::optional("created_at", "string"), + FieldSpec::optional("updated_at", "string"), + FieldSpec::optional("d_tag", "string"), + FieldSpec::optional("farm_id", "string"), + FieldSpec::optional("name", "string"), + FieldSpec::optional("about", "string"), + FieldSpec::optional("location_primary", "string"), + FieldSpec::optional("location_city", "string"), + FieldSpec::optional("location_region", "string"), + FieldSpec::optional("location_country", "string"), + ], + }, + TypeSpec::Object { + name: "IPlotFieldsPartial", + fields: &[ + FieldSpec::optional_nullable("d_tag", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("farm_id", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("name", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("about", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("location_primary", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("location_city", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("location_region", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("location_country", "ReplicaStoreJsonValue"), + ], + }, + TypeSpec::Alias { + name: "IPlotFindMany", + target: "IPlotFindManyArgs", + }, + TypeSpec::Object { + name: "IPlotFindManyArgs", + fields: &[FieldSpec::nullable("filter", "IPlotFieldsFilter")], + }, + TypeSpec::Alias { + name: "IPlotFindManyResolve", + target: "ReplicaSchemaResultList<Plot>", + }, + TypeSpec::Union { + name: "IPlotFindOne", + variants: &[VariantSpec::Ref("IPlotFindOneArgs")], + }, + TypeSpec::Object { + name: "IPlotFindOneArgs", + fields: &[FieldSpec::required("on", "PlotQueryBindValues")], + }, + TypeSpec::Alias { + name: "IPlotFindOneResolve", + target: "ReplicaSchemaResult<Plot | null>", + }, + TypeSpec::Alias { + name: "IPlotGcsLocationCreate", + target: "IPlotGcsLocationFields", + }, + TypeSpec::Alias { + name: "IPlotGcsLocationCreateResolve", + target: "ReplicaSchemaResult<PlotGcsLocation>", + }, + TypeSpec::Alias { + name: "IPlotGcsLocationDelete", + target: "IPlotGcsLocationFindOne", + }, + TypeSpec::Alias { + name: "IPlotGcsLocationDeleteResolve", + target: "ReplicaSchemaResult<string>", + }, + TypeSpec::Object { + name: "IPlotGcsLocationFields", + fields: &[ + FieldSpec::required("plot_id", "string"), + FieldSpec::required("gcs_location_id", "string"), + FieldSpec::required("role", "string"), + ], + }, + TypeSpec::Object { + name: "IPlotGcsLocationFieldsFilter", + fields: &[ + FieldSpec::optional("id", "string"), + FieldSpec::optional("created_at", "string"), + FieldSpec::optional("updated_at", "string"), + FieldSpec::optional("plot_id", "string"), + FieldSpec::optional("gcs_location_id", "string"), + FieldSpec::optional("role", "string"), + ], + }, + TypeSpec::Object { + name: "IPlotGcsLocationFieldsPartial", + fields: &[ + FieldSpec::optional_nullable("plot_id", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("gcs_location_id", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("role", "ReplicaStoreJsonValue"), + ], + }, + TypeSpec::Alias { + name: "IPlotGcsLocationFindMany", + target: "IPlotGcsLocationFindManyArgs", + }, + TypeSpec::Object { + name: "IPlotGcsLocationFindManyArgs", + fields: &[FieldSpec::nullable( + "filter", + "IPlotGcsLocationFieldsFilter", + )], + }, + TypeSpec::Alias { + name: "IPlotGcsLocationFindManyResolve", + target: "ReplicaSchemaResultList<PlotGcsLocation>", + }, + TypeSpec::Union { + name: "IPlotGcsLocationFindOne", + variants: &[VariantSpec::Ref("IPlotGcsLocationFindOneArgs")], + }, + TypeSpec::Object { + name: "IPlotGcsLocationFindOneArgs", + fields: &[FieldSpec::required("on", "PlotGcsLocationQueryBindValues")], + }, + TypeSpec::Alias { + name: "IPlotGcsLocationFindOneResolve", + target: "ReplicaSchemaResult<PlotGcsLocation | null>", + }, + TypeSpec::Alias { + name: "IPlotGcsLocationUpdate", + target: "IPlotGcsLocationUpdateArgs", + }, + TypeSpec::Object { + name: "IPlotGcsLocationUpdateArgs", + fields: &[ + FieldSpec::required("on", "PlotGcsLocationQueryBindValues"), + FieldSpec::required("fields", "IPlotGcsLocationFieldsPartial"), + ], + }, + TypeSpec::Alias { + name: "IPlotGcsLocationUpdateResolve", + target: "ReplicaSchemaResult<PlotGcsLocation>", + }, + TypeSpec::Alias { + name: "IPlotTagCreate", + target: "IPlotTagFields", + }, + TypeSpec::Alias { + name: "IPlotTagCreateResolve", + target: "ReplicaSchemaResult<PlotTag>", + }, + TypeSpec::Alias { + name: "IPlotTagDelete", + target: "IPlotTagFindOne", + }, + TypeSpec::Alias { + name: "IPlotTagDeleteResolve", + target: "ReplicaSchemaResult<string>", + }, + TypeSpec::Object { + name: "IPlotTagFields", + fields: &[ + FieldSpec::required("plot_id", "string"), + FieldSpec::required("tag", "string"), + ], + }, + TypeSpec::Object { + name: "IPlotTagFieldsFilter", + fields: &[ + FieldSpec::optional("id", "string"), + FieldSpec::optional("created_at", "string"), + FieldSpec::optional("updated_at", "string"), + FieldSpec::optional("plot_id", "string"), + FieldSpec::optional("tag", "string"), + ], + }, + TypeSpec::Object { + name: "IPlotTagFieldsPartial", + fields: &[ + FieldSpec::optional_nullable("plot_id", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("tag", "ReplicaStoreJsonValue"), + ], + }, + TypeSpec::Alias { + name: "IPlotTagFindMany", + target: "IPlotTagFindManyArgs", + }, + TypeSpec::Object { + name: "IPlotTagFindManyArgs", + fields: &[FieldSpec::nullable("filter", "IPlotTagFieldsFilter")], + }, + TypeSpec::Alias { + name: "IPlotTagFindManyResolve", + target: "ReplicaSchemaResultList<PlotTag>", + }, + TypeSpec::Union { + name: "IPlotTagFindOne", + variants: &[VariantSpec::Ref("IPlotTagFindOneArgs")], + }, + TypeSpec::Object { + name: "IPlotTagFindOneArgs", + fields: &[FieldSpec::required("on", "PlotTagQueryBindValues")], + }, + TypeSpec::Alias { + name: "IPlotTagFindOneResolve", + target: "ReplicaSchemaResult<PlotTag | null>", + }, + TypeSpec::Alias { + name: "IPlotTagUpdate", + target: "IPlotTagUpdateArgs", + }, + TypeSpec::Object { + name: "IPlotTagUpdateArgs", + fields: &[ + FieldSpec::required("on", "PlotTagQueryBindValues"), + FieldSpec::required("fields", "IPlotTagFieldsPartial"), + ], + }, + TypeSpec::Alias { + name: "IPlotTagUpdateResolve", + target: "ReplicaSchemaResult<PlotTag>", + }, + TypeSpec::Alias { + name: "IPlotUpdate", + target: "IPlotUpdateArgs", + }, + TypeSpec::Object { + name: "IPlotUpdateArgs", + fields: &[ + FieldSpec::required("on", "PlotQueryBindValues"), + FieldSpec::required("fields", "IPlotFieldsPartial"), + ], + }, + TypeSpec::Alias { + name: "IPlotUpdateResolve", + target: "ReplicaSchemaResult<Plot>", + }, + TypeSpec::Alias { + name: "ITradeProductCreate", + target: "ITradeProductFields", + }, + TypeSpec::Alias { + name: "ITradeProductCreateResolve", + target: "ReplicaSchemaResult<TradeProduct>", + }, + TypeSpec::Alias { + name: "ITradeProductDelete", + target: "ITradeProductFindOne", + }, + TypeSpec::Alias { + name: "ITradeProductDeleteResolve", + target: "ReplicaSchemaResult<string>", + }, + TypeSpec::Object { + name: "ITradeProductFields", + fields: &[ + FieldSpec::required("key", "string"), + FieldSpec::required("category", "string"), + FieldSpec::required("title", "string"), + FieldSpec::required("summary", "string"), + FieldSpec::required("process", "string"), + FieldSpec::required("lot", "string"), + FieldSpec::required("profile", "string"), + FieldSpec::required("year", "bigint"), + FieldSpec::required("qty_amt", "number"), + FieldSpec::required("qty_amt_exact", "string"), + FieldSpec::required("qty_unit", "string"), + FieldSpec::optional_nullable("qty_label", "string"), + FieldSpec::optional_nullable("qty_avail", "bigint"), + FieldSpec::required("price_amt", "number"), + FieldSpec::required("price_amt_exact", "string"), + FieldSpec::required("price_currency", "string"), + FieldSpec::required("price_qty_amt", "number"), + FieldSpec::required("price_qty_amt_exact", "string"), + FieldSpec::required("price_qty_unit", "string"), + FieldSpec::optional_nullable("listing_addr", "string"), + FieldSpec::optional_nullable("primary_bin_id", "string"), + FieldSpec::optional_nullable("verified_primary_bin_id", "string"), + FieldSpec::optional_nullable("notes", "string"), + ], + }, + TypeSpec::Object { + name: "ITradeProductFieldsFilter", + fields: &[ + FieldSpec::optional("id", "string"), + FieldSpec::optional("created_at", "string"), + FieldSpec::optional("updated_at", "string"), + FieldSpec::optional("key", "string"), + FieldSpec::optional("category", "string"), + FieldSpec::optional("title", "string"), + FieldSpec::optional("summary", "string"), + FieldSpec::optional("process", "string"), + FieldSpec::optional("lot", "string"), + FieldSpec::optional("profile", "string"), + FieldSpec::optional("year", "bigint"), + FieldSpec::optional("qty_amt", "number"), + FieldSpec::optional("qty_amt_exact", "string"), + FieldSpec::optional("qty_unit", "string"), + FieldSpec::optional("qty_label", "string"), + FieldSpec::optional("qty_avail", "bigint"), + FieldSpec::optional("price_amt", "number"), + FieldSpec::optional("price_amt_exact", "string"), + FieldSpec::optional("price_currency", "string"), + FieldSpec::optional("price_qty_amt", "number"), + FieldSpec::optional("price_qty_amt_exact", "string"), + FieldSpec::optional("price_qty_unit", "string"), + FieldSpec::optional("listing_addr", "string"), + FieldSpec::optional("primary_bin_id", "string"), + FieldSpec::optional("verified_primary_bin_id", "string"), + FieldSpec::optional("notes", "string"), + ], + }, + TypeSpec::Object { + name: "ITradeProductFieldsPartial", + fields: &[ + FieldSpec::optional_nullable("key", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("category", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("title", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("summary", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("process", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("lot", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("profile", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("year", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("qty_amt", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("qty_amt_exact", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("qty_unit", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("qty_label", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("qty_avail", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("price_amt", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("price_amt_exact", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("price_currency", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("price_qty_amt", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("price_qty_amt_exact", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("price_qty_unit", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("listing_addr", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("primary_bin_id", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("verified_primary_bin_id", "ReplicaStoreJsonValue"), + FieldSpec::optional_nullable("notes", "ReplicaStoreJsonValue"), + ], + }, + TypeSpec::Alias { + name: "ITradeProductFindMany", + target: "ITradeProductFindManyArgs", + }, + TypeSpec::Object { + name: "ITradeProductFindManyArgs", + fields: &[FieldSpec::nullable("filter", "ITradeProductFieldsFilter")], + }, + TypeSpec::Alias { + name: "ITradeProductFindManyResolve", + target: "ReplicaSchemaResultList<TradeProduct>", + }, + TypeSpec::Union { + name: "ITradeProductFindOne", + variants: &[VariantSpec::Ref("ITradeProductFindOneArgs")], + }, + TypeSpec::Object { + name: "ITradeProductFindOneArgs", + fields: &[FieldSpec::required("on", "TradeProductQueryBindValues")], + }, + TypeSpec::Alias { + name: "ITradeProductFindOneResolve", + target: "ReplicaSchemaResult<TradeProduct | null>", + }, + TypeSpec::Object { + name: "ITradeProductLocationRelation", + fields: &[ + FieldSpec::required("trade_product", "TradeProductQueryBindValues"), + FieldSpec::required("gcs_location", "GcsLocationQueryBindValues"), + ], + }, + TypeSpec::Alias { + name: "ITradeProductLocationResolve", + target: "ReplicaSchemaResultPass", + }, + TypeSpec::Object { + name: "ITradeProductMediaRelation", + fields: &[ + FieldSpec::required("trade_product", "TradeProductQueryBindValues"), + FieldSpec::required("media_image", "MediaImageQueryBindValues"), + ], + }, + TypeSpec::Alias { + name: "ITradeProductMediaResolve", + target: "ReplicaSchemaResultPass", + }, + TypeSpec::Alias { + name: "ITradeProductUpdate", + target: "ITradeProductUpdateArgs", + }, + TypeSpec::Object { + name: "ITradeProductUpdateArgs", + fields: &[ + FieldSpec::required("on", "TradeProductQueryBindValues"), + FieldSpec::required("fields", "ITradeProductFieldsPartial"), + ], + }, + TypeSpec::Alias { + name: "ITradeProductUpdateResolve", + target: "ReplicaSchemaResult<TradeProduct>", + }, + TypeSpec::Object { + name: "LogError", + fields: &[ + FieldSpec::required("id", "string"), + FieldSpec::required("created_at", "string"), + FieldSpec::required("updated_at", "string"), + FieldSpec::required("error", "string"), + FieldSpec::required("message", "string"), + FieldSpec::nullable("stack_trace", "string"), + FieldSpec::nullable("cause", "string"), + FieldSpec::required("app_system", "string"), + FieldSpec::required("app_version", "string"), + FieldSpec::required("nostr_pubkey", "string"), + FieldSpec::nullable("data", "string"), + ], + }, + TypeSpec::Union { + name: "LogErrorQueryBindValues", + variants: &[ + VariantSpec::Object(&[FieldSpec::required("id", "string")]), + VariantSpec::Object(&[FieldSpec::required("nostr_pubkey", "string")]), + ], + }, + TypeSpec::Object { + name: "MediaImage", + fields: &[ + FieldSpec::required("id", "string"), + FieldSpec::required("created_at", "string"), + FieldSpec::required("updated_at", "string"), + FieldSpec::required("file_path", "string"), + FieldSpec::required("mime_type", "string"), + FieldSpec::required("res_base", "string"), + FieldSpec::required("res_path", "string"), + FieldSpec::nullable("label", "string"), + FieldSpec::nullable("description", "string"), + ], + }, + TypeSpec::Union { + name: "MediaImageFindManyRel", + variants: &[ + VariantSpec::Object(&[FieldSpec::required( + "on_trade_product", + "MediaImageTradeProductArgs", + )]), + VariantSpec::Object(&[FieldSpec::required( + "off_trade_product", + "MediaImageTradeProductArgs", + )]), + ], + }, + TypeSpec::Union { + name: "MediaImageQueryBindValues", + variants: &[ + VariantSpec::Object(&[FieldSpec::required("id", "string")]), + VariantSpec::Object(&[FieldSpec::required("file_path", "string")]), + ], + }, + TypeSpec::Object { + name: "MediaImageTradeProductArgs", + fields: &[FieldSpec::required("id", "string")], + }, + TypeSpec::Object { + name: "NostrEventHead", + fields: &[ + FieldSpec::required("id", "string"), + FieldSpec::required("created_at", "string"), + FieldSpec::required("updated_at", "string"), + FieldSpec::required("key", "string"), + FieldSpec::required("kind", "number"), + FieldSpec::required("pubkey", "string"), + FieldSpec::required("d_tag", "string"), + FieldSpec::required("last_event_id", "string"), + FieldSpec::required("last_created_at", "number"), + FieldSpec::required("content_hash", "string"), + ], + }, + TypeSpec::Union { + name: "NostrEventHeadQueryBindValues", + variants: &[ + VariantSpec::Object(&[FieldSpec::required("id", "string")]), + VariantSpec::Object(&[FieldSpec::required("key", "string")]), + ], + }, + TypeSpec::Object { + name: "NostrProfile", + fields: &[ + FieldSpec::required("id", "string"), + FieldSpec::required("created_at", "string"), + FieldSpec::required("updated_at", "string"), + FieldSpec::required("public_key", "string"), + FieldSpec::required("profile_type", "string"), + FieldSpec::required("name", "string"), + FieldSpec::nullable("display_name", "string"), + FieldSpec::nullable("about", "string"), + FieldSpec::nullable("website", "string"), + FieldSpec::nullable("picture", "string"), + FieldSpec::nullable("banner", "string"), + FieldSpec::nullable("nip05", "string"), + FieldSpec::nullable("lud06", "string"), + FieldSpec::nullable("lud16", "string"), + ], + }, + TypeSpec::Union { + name: "NostrProfileFindManyRel", + variants: &[ + VariantSpec::Object(&[FieldSpec::required("on_relay", "NostrProfileRelayArgs")]), + VariantSpec::Object(&[FieldSpec::required("off_relay", "NostrProfileRelayArgs")]), + ], + }, + TypeSpec::Union { + name: "NostrProfileQueryBindValues", + variants: &[ + VariantSpec::Object(&[FieldSpec::required("id", "string")]), + VariantSpec::Object(&[FieldSpec::required("public_key", "string")]), + ], + }, + TypeSpec::Object { + name: "NostrProfileRelayArgs", + fields: &[FieldSpec::required("id", "string")], + }, + TypeSpec::Object { + name: "NostrRelay", + fields: &[ + FieldSpec::required("id", "string"), + FieldSpec::required("created_at", "string"), + FieldSpec::required("updated_at", "string"), + FieldSpec::required("url", "string"), + FieldSpec::nullable("relay_id", "string"), + FieldSpec::nullable("name", "string"), + FieldSpec::nullable("description", "string"), + FieldSpec::nullable("pubkey", "string"), + FieldSpec::nullable("contact", "string"), + FieldSpec::nullable("supported_nips", "string"), + FieldSpec::nullable("software", "string"), + FieldSpec::nullable("version", "string"), + FieldSpec::nullable("data", "string"), + ], + }, + TypeSpec::Union { + name: "NostrRelayFindManyRel", + variants: &[ + VariantSpec::Object(&[FieldSpec::required("on_profile", "NostrRelayProfileArgs")]), + VariantSpec::Object(&[FieldSpec::required("off_profile", "NostrRelayProfileArgs")]), + ], + }, + TypeSpec::Object { + name: "NostrRelayProfileArgs", + fields: &[FieldSpec::required("public_key", "string")], + }, + TypeSpec::Union { + name: "NostrRelayQueryBindValues", + variants: &[ + VariantSpec::Object(&[FieldSpec::required("id", "string")]), + VariantSpec::Object(&[FieldSpec::required("url", "string")]), + ], + }, + TypeSpec::Object { + name: "Plot", + fields: &[ + FieldSpec::required("id", "string"), + FieldSpec::required("created_at", "string"), + FieldSpec::required("updated_at", "string"), + FieldSpec::required("d_tag", "string"), + FieldSpec::required("farm_id", "string"), + FieldSpec::required("name", "string"), + FieldSpec::nullable("about", "string"), + FieldSpec::nullable("location_primary", "string"), + FieldSpec::nullable("location_city", "string"), + FieldSpec::nullable("location_region", "string"), + FieldSpec::nullable("location_country", "string"), + ], + }, + TypeSpec::Object { + name: "PlotGcsLocation", + fields: &[ + FieldSpec::required("id", "string"), + FieldSpec::required("created_at", "string"), + FieldSpec::required("updated_at", "string"), + FieldSpec::required("plot_id", "string"), + FieldSpec::required("gcs_location_id", "string"), + FieldSpec::required("role", "string"), + ], + }, + TypeSpec::Union { + name: "PlotGcsLocationQueryBindValues", + variants: &[ + VariantSpec::Object(&[FieldSpec::required("id", "string")]), + VariantSpec::Object(&[FieldSpec::required("plot_id", "string")]), + VariantSpec::Object(&[FieldSpec::required("gcs_location_id", "string")]), + ], + }, + TypeSpec::Union { + name: "PlotQueryBindValues", + variants: &[ + VariantSpec::Object(&[FieldSpec::required("id", "string")]), + VariantSpec::Object(&[FieldSpec::required("d_tag", "string")]), + VariantSpec::Object(&[FieldSpec::required("farm_id", "string")]), + ], + }, + TypeSpec::Object { + name: "PlotTag", + fields: &[ + FieldSpec::required("id", "string"), + FieldSpec::required("created_at", "string"), + FieldSpec::required("updated_at", "string"), + FieldSpec::required("plot_id", "string"), + FieldSpec::required("tag", "string"), + ], + }, + TypeSpec::Union { + name: "PlotTagQueryBindValues", + variants: &[ + VariantSpec::Object(&[FieldSpec::required("id", "string")]), + VariantSpec::Object(&[FieldSpec::required("plot_id", "string")]), + VariantSpec::Object(&[FieldSpec::required("tag", "string")]), + ], + }, + TypeSpec::Alias { + name: "ReplicaStoreJsonValue", + target: "null | boolean | number | string | Array<ReplicaStoreJsonValue> | { [key: string]: ReplicaStoreJsonValue }", + }, + TypeSpec::Object { + name: "TradeProduct", + fields: &[ + FieldSpec::required("id", "string"), + FieldSpec::required("created_at", "string"), + FieldSpec::required("updated_at", "string"), + FieldSpec::required("key", "string"), + FieldSpec::required("category", "string"), + FieldSpec::required("title", "string"), + FieldSpec::required("summary", "string"), + FieldSpec::required("process", "string"), + FieldSpec::required("lot", "string"), + FieldSpec::required("profile", "string"), + FieldSpec::required("year", "bigint"), + FieldSpec::required("qty_amt", "number"), + FieldSpec::nullable("qty_amt_exact", "string"), + FieldSpec::required("qty_unit", "string"), + FieldSpec::nullable("qty_label", "string"), + FieldSpec::nullable("qty_avail", "bigint"), + FieldSpec::required("price_amt", "number"), + FieldSpec::nullable("price_amt_exact", "string"), + FieldSpec::required("price_currency", "string"), + FieldSpec::required("price_qty_amt", "number"), + FieldSpec::nullable("price_qty_amt_exact", "string"), + FieldSpec::required("price_qty_unit", "string"), + FieldSpec::nullable("listing_addr", "string"), + FieldSpec::nullable("primary_bin_id", "string"), + FieldSpec::nullable("verified_primary_bin_id", "string"), + FieldSpec::nullable("notes", "string"), + ], + }, + TypeSpec::Union { + name: "TradeProductQueryBindValues", + variants: &[VariantSpec::Object(&[FieldSpec::required("id", "string")])], + }, +]; + +#[cfg(test)] +mod tests { + use super::{FieldSpec, TYPE_SPECS, TypeSpec, VariantSpec, dto_registry, type_inventory}; + + #[test] + fn registry_exports_known_schema_types() { + let registry = dto_registry(); + assert!(!registry.has_errors(), "{:?}", registry.diagnostics); + assert!(type_inventory().contains(&"Farm")); + assert!(type_inventory().contains(&"NostrEventHead")); + assert!(type_inventory().contains(&"ReplicaStoreJsonValue")); + } + + #[test] + fn source_find_one_resolves_preserve_nullable_result() { + assert!(TYPE_SPECS.iter().any(|spec| matches!( + spec, + TypeSpec::Alias { + name: "IFarmFindOneResolve", + target: "ReplicaSchemaResult<Farm | null>" + } + ))); + } + + #[test] + fn relation_find_many_inputs_preserve_filter_and_rel_variants() { + for (name, filter, rel) in [ + ( + "IGcsLocationFindMany", + "IGcsLocationFieldsFilter", + "GcsLocationFindManyRel", + ), + ( + "IMediaImageFindMany", + "IMediaImageFieldsFilter", + "MediaImageFindManyRel", + ), + ( + "INostrProfileFindMany", + "INostrProfileFieldsFilter", + "NostrProfileFindManyRel", + ), + ( + "INostrRelayFindMany", + "INostrRelayFieldsFilter", + "NostrRelayFindManyRel", + ), + ] { + assert!(TYPE_SPECS.iter().any(|spec| matches!( + spec, + TypeSpec::Union { name: actual_name, variants } + if *actual_name == name + && variants.len() == 2 + && matches!(variants[0], VariantSpec::Object(fields) if fields.len() == 1 && fields[0] == FieldSpec::nullable("filter", filter)) + && matches!(variants[1], VariantSpec::Object(fields) if fields.len() == 1 && fields[0] == FieldSpec::required("rel", rel)) + ))); + } + } + + #[test] + fn serde_json_value_policy_is_explicit() { + assert!(TYPE_SPECS.iter().any(|spec| matches!( + spec, + TypeSpec::Alias { name: "ReplicaStoreJsonValue", target } if target.contains("[key: string]: ReplicaStoreJsonValue") + ))); + assert!(TYPE_SPECS.iter().any(|spec| matches!( + spec, + TypeSpec::Object { name: "ITradeProductFieldsPartial", fields } if fields.iter().any(|field| field.name == "year" && field.target == "ReplicaStoreJsonValue" && field.optional && field.nullable) + ))); + } + + #[test] + fn trade_product_large_integer_policy_is_explicit() { + assert!(TYPE_SPECS.iter().any(|spec| matches!( + spec, + TypeSpec::Object { name: "TradeProduct", fields } if fields.iter().any(|field| field.name == "year" && field.target == "bigint") + ))); + assert!(TYPE_SPECS.iter().any(|spec| matches!( + spec, + TypeSpec::Object { name: "ITradeProductFieldsFilter", fields } if fields.iter().any(|field| field.name == "qty_avail" && field.target == "bigint" && field.optional) + ))); + } +} diff --git a/crates/replica_db_schema/src/lib.rs b/crates/replica_schema/src/lib.rs diff --git a/crates/replica_db_schema/src/models/farm.rs b/crates/replica_schema/src/models/farm.rs diff --git a/crates/replica_db_schema/src/models/farm_gcs_location.rs b/crates/replica_schema/src/models/farm_gcs_location.rs diff --git a/crates/replica_db_schema/src/models/farm_member.rs b/crates/replica_schema/src/models/farm_member.rs diff --git a/crates/replica_db_schema/src/models/farm_member_claim.rs b/crates/replica_schema/src/models/farm_member_claim.rs diff --git a/crates/replica_db_schema/src/models/farm_tag.rs b/crates/replica_schema/src/models/farm_tag.rs diff --git a/crates/replica_db_schema/src/models/gcs_location.rs b/crates/replica_schema/src/models/gcs_location.rs diff --git a/crates/replica_db_schema/src/models/log_error.rs b/crates/replica_schema/src/models/log_error.rs diff --git a/crates/replica_db_schema/src/models/media_image.rs b/crates/replica_schema/src/models/media_image.rs diff --git a/crates/replica_db_schema/src/models/mod.rs b/crates/replica_schema/src/models/mod.rs diff --git a/crates/replica_db_schema/src/models/nostr_event_head.rs b/crates/replica_schema/src/models/nostr_event_head.rs diff --git a/crates/replica_db_schema/src/models/nostr_profile.rs b/crates/replica_schema/src/models/nostr_profile.rs diff --git a/crates/replica_db_schema/src/models/nostr_profile_relay.rs b/crates/replica_schema/src/models/nostr_profile_relay.rs diff --git a/crates/replica_db_schema/src/models/nostr_relay.rs b/crates/replica_schema/src/models/nostr_relay.rs diff --git a/crates/replica_db_schema/src/models/plot.rs b/crates/replica_schema/src/models/plot.rs diff --git a/crates/replica_db_schema/src/models/plot_gcs_location.rs b/crates/replica_schema/src/models/plot_gcs_location.rs diff --git a/crates/replica_db_schema/src/models/plot_tag.rs b/crates/replica_schema/src/models/plot_tag.rs diff --git a/crates/replica_db_schema/src/models/result.rs b/crates/replica_schema/src/models/result.rs diff --git a/crates/replica_db_schema/src/models/trade_product.rs b/crates/replica_schema/src/models/trade_product.rs diff --git a/crates/replica_db_schema/src/models/trade_product_location.rs b/crates/replica_schema/src/models/trade_product_location.rs diff --git a/crates/replica_db_schema/src/models/trade_product_media.rs b/crates/replica_schema/src/models/trade_product_media.rs diff --git a/crates/replica_schema/tests/query_bind_values.rs b/crates/replica_schema/tests/query_bind_values.rs @@ -0,0 +1,354 @@ +use radroots_replica_schema::farm::FarmQueryBindValues; +use radroots_replica_schema::farm_gcs_location::FarmGcsLocationQueryBindValues; +use radroots_replica_schema::farm_member::FarmMemberQueryBindValues; +use radroots_replica_schema::farm_member_claim::FarmMemberClaimQueryBindValues; +use radroots_replica_schema::farm_tag::FarmTagQueryBindValues; +use radroots_replica_schema::gcs_location::GcsLocationQueryBindValues; +use radroots_replica_schema::log_error::LogErrorQueryBindValues; +use radroots_replica_schema::media_image::MediaImageQueryBindValues; +use radroots_replica_schema::nostr_event_head::NostrEventHeadQueryBindValues; +use radroots_replica_schema::nostr_profile::NostrProfileQueryBindValues; +use radroots_replica_schema::nostr_relay::NostrRelayQueryBindValues; +use radroots_replica_schema::plot::PlotQueryBindValues; +use radroots_replica_schema::plot_gcs_location::PlotGcsLocationQueryBindValues; +use radroots_replica_schema::plot_tag::PlotTagQueryBindValues; +use radroots_replica_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_head_query_bind_values_cover_all_variants, + NostrEventHeadQueryBindValues::Id { + id: "nostr-event-state-id".to_string() + }, + "id", + "nostr-event-state-id", + [( + NostrEventHeadQueryBindValues::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_store/Cargo.toml b/crates/replica_store/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "radroots_replica_store" +publish = ["crates-io"] +version = "0.1.0-alpha.2" +edition.workspace = true +authors = ["Tyson Lupul <tyson@radroots.org>"] +rust-version.workspace = true +license.workspace = true +description = "Replica database layer for Radroots" +repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/radroots_replica_store" +readme = "README" + +[lib] +crate-type = ["rlib"] + +[features] +default = ["std"] +std = ["radroots_sql_core/std"] +web = ["std", "radroots_sql_core/web"] +native = ["std", "radroots_sql_core/native"] + +[dependencies] +radroots_sql_core = { workspace = true } +radroots_replica_schema = { 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/replica_store/README b/crates/replica_store/README @@ -0,0 +1,22 @@ +# radroots_replica_store + +This is the README for `radroots_replica_store`, which provides replica SQL +facades and shaped query helpers for the `radroots` core libraries. + +## Overview + + * a generic `ReplicaSql<E>` facade over any `SqlExecutor` implementation; + * migration, backup, restore, and export helpers for replica database state; + * typed CRUD helpers over the shared replica schema models; + * shaped query helpers for common trade, farm, and event freshness lookups; + * feature-gated web and native backend support through `radroots_sql_core`. + +## Copyright + +Except as otherwise noted, all files in the `radroots_replica_store` distribution +are + + Copyright (c) 2025 Tyson Lupul + +For information on usage and redistribution, and for a DISCLAIMER OF ALL +WARRANTIES, see LICENSE included in the `radroots_replica_store` distribution. diff --git a/crates/replica_db/migrations/0000_init.down.sql b/crates/replica_store/migrations/0000_init.down.sql diff --git a/crates/replica_db/migrations/0000_init.up.sql b/crates/replica_store/migrations/0000_init.up.sql diff --git a/crates/replica_db/migrations/0001_log_error.down.sql b/crates/replica_store/migrations/0001_log_error.down.sql diff --git a/crates/replica_db/migrations/0001_log_error.up.sql b/crates/replica_store/migrations/0001_log_error.up.sql diff --git a/crates/replica_db/migrations/0002_farm.down.sql b/crates/replica_store/migrations/0002_farm.down.sql diff --git a/crates/replica_db/migrations/0002_farm.up.sql b/crates/replica_store/migrations/0002_farm.up.sql diff --git a/crates/replica_db/migrations/0003_gcs_location.down.sql b/crates/replica_store/migrations/0003_gcs_location.down.sql diff --git a/crates/replica_db/migrations/0003_gcs_location.up.sql b/crates/replica_store/migrations/0003_gcs_location.up.sql diff --git a/crates/replica_db/migrations/0004_trade_product.down.sql b/crates/replica_store/migrations/0004_trade_product.down.sql diff --git a/crates/replica_db/migrations/0004_trade_product.up.sql b/crates/replica_store/migrations/0004_trade_product.up.sql diff --git a/crates/replica_db/migrations/0005_nostr_profile.down.sql b/crates/replica_store/migrations/0005_nostr_profile.down.sql diff --git a/crates/replica_db/migrations/0005_nostr_profile.up.sql b/crates/replica_store/migrations/0005_nostr_profile.up.sql diff --git a/crates/replica_db/migrations/0006_nostr_relay.down.sql b/crates/replica_store/migrations/0006_nostr_relay.down.sql diff --git a/crates/replica_db/migrations/0006_nostr_relay.up.sql b/crates/replica_store/migrations/0006_nostr_relay.up.sql diff --git a/crates/replica_db/migrations/0007_media_image.down.sql b/crates/replica_store/migrations/0007_media_image.down.sql diff --git a/crates/replica_db/migrations/0007_media_image.up.sql b/crates/replica_store/migrations/0007_media_image.up.sql diff --git a/crates/replica_db/migrations/0008_farm_gcs_location.down.sql b/crates/replica_store/migrations/0008_farm_gcs_location.down.sql diff --git a/crates/replica_db/migrations/0008_farm_gcs_location.up.sql b/crates/replica_store/migrations/0008_farm_gcs_location.up.sql diff --git a/crates/replica_db/migrations/0009_nostr_profile_relay.down.sql b/crates/replica_store/migrations/0009_nostr_profile_relay.down.sql diff --git a/crates/replica_db/migrations/0009_nostr_profile_relay.up.sql b/crates/replica_store/migrations/0009_nostr_profile_relay.up.sql diff --git a/crates/replica_db/migrations/0010_trade_product_location.down.sql b/crates/replica_store/migrations/0010_trade_product_location.down.sql diff --git a/crates/replica_db/migrations/0010_trade_product_location.up.sql b/crates/replica_store/migrations/0010_trade_product_location.up.sql diff --git a/crates/replica_db/migrations/0011_trade_product_media.down.sql b/crates/replica_store/migrations/0011_trade_product_media.down.sql diff --git a/crates/replica_db/migrations/0011_trade_product_media.up.sql b/crates/replica_store/migrations/0011_trade_product_media.up.sql diff --git a/crates/replica_db/migrations/0012_plot.down.sql b/crates/replica_store/migrations/0012_plot.down.sql diff --git a/crates/replica_db/migrations/0012_plot.up.sql b/crates/replica_store/migrations/0012_plot.up.sql diff --git a/crates/replica_db/migrations/0013_plot_gcs_location.down.sql b/crates/replica_store/migrations/0013_plot_gcs_location.down.sql diff --git a/crates/replica_db/migrations/0013_plot_gcs_location.up.sql b/crates/replica_store/migrations/0013_plot_gcs_location.up.sql diff --git a/crates/replica_db/migrations/0014_farm_tag.down.sql b/crates/replica_store/migrations/0014_farm_tag.down.sql diff --git a/crates/replica_db/migrations/0014_farm_tag.up.sql b/crates/replica_store/migrations/0014_farm_tag.up.sql diff --git a/crates/replica_db/migrations/0015_plot_tag.down.sql b/crates/replica_store/migrations/0015_plot_tag.down.sql diff --git a/crates/replica_db/migrations/0015_plot_tag.up.sql b/crates/replica_store/migrations/0015_plot_tag.up.sql diff --git a/crates/replica_db/migrations/0016_farm_member.down.sql b/crates/replica_store/migrations/0016_farm_member.down.sql diff --git a/crates/replica_db/migrations/0016_farm_member.up.sql b/crates/replica_store/migrations/0016_farm_member.up.sql diff --git a/crates/replica_db/migrations/0017_farm_member_claim.down.sql b/crates/replica_store/migrations/0017_farm_member_claim.down.sql diff --git a/crates/replica_db/migrations/0017_farm_member_claim.up.sql b/crates/replica_store/migrations/0017_farm_member_claim.up.sql diff --git a/crates/replica_db/migrations/0018_nostr_event_head.down.sql b/crates/replica_store/migrations/0018_nostr_event_head.down.sql diff --git a/crates/replica_db/migrations/0018_nostr_event_head.up.sql b/crates/replica_store/migrations/0018_nostr_event_head.up.sql diff --git a/crates/replica_db/migrations/0019_repair_missing_indexes.down.sql b/crates/replica_store/migrations/0019_repair_missing_indexes.down.sql diff --git a/crates/replica_db/migrations/0019_repair_missing_indexes.up.sql b/crates/replica_store/migrations/0019_repair_missing_indexes.up.sql diff --git a/crates/replica_db/migrations/0020_trade_product_listing_addr.down.sql b/crates/replica_store/migrations/0020_trade_product_listing_addr.down.sql diff --git a/crates/replica_db/migrations/0020_trade_product_listing_addr.up.sql b/crates/replica_store/migrations/0020_trade_product_listing_addr.up.sql diff --git a/crates/replica_db/migrations/0021_trade_product_primary_bin_id.down.sql b/crates/replica_store/migrations/0021_trade_product_primary_bin_id.down.sql diff --git a/crates/replica_db/migrations/0021_trade_product_primary_bin_id.up.sql b/crates/replica_store/migrations/0021_trade_product_primary_bin_id.up.sql diff --git a/crates/replica_db/migrations/0022_trade_product_exact_economics.down.sql b/crates/replica_store/migrations/0022_trade_product_exact_economics.down.sql diff --git a/crates/replica_db/migrations/0022_trade_product_exact_economics.up.sql b/crates/replica_store/migrations/0022_trade_product_exact_economics.up.sql diff --git a/crates/replica_db/migrations/0023_trade_product_verified_primary_bin.down.sql b/crates/replica_store/migrations/0023_trade_product_verified_primary_bin.down.sql diff --git a/crates/replica_db/migrations/0023_trade_product_verified_primary_bin.up.sql b/crates/replica_store/migrations/0023_trade_product_verified_primary_bin.up.sql diff --git a/crates/replica_store/src/backup.rs b/crates/replica_store/src/backup.rs @@ -0,0 +1,993 @@ +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 REPLICA_STORE_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_store_version: String, + pub schema: Vec<SchemaEntry>, + pub migrations: Vec<MigrationBackup>, + pub data: Vec<TableData>, +} + +pub fn export_database_backup(executor: &dyn SqlExecutor) -> 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_store_version: REPLICA_STORE_VERSION.to_string(), + schema, + migrations, + data, + }) +} + +pub fn export_database_backup_json(executor: &dyn SqlExecutor) -> Result<String, SqlError> { + let backup = export_database_backup(executor)?; + serde_json::to_string(&backup).map_err(SqlError::from) +} + +pub fn restore_database_backup( + executor: &dyn SqlExecutor, + 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( + executor: &dyn SqlExecutor, + 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(executor: &dyn SqlExecutor) -> 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( + executor: &dyn SqlExecutor, + 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( + executor: &dyn SqlExecutor, + 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( + executor: &dyn SqlExecutor, + 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 = Value::Array(binds).to_string(); + executor.exec(&sql, &params_json)?; + Ok(()) +} + +pub(crate) fn load_schema(executor: &dyn SqlExecutor) -> 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( + executor: &dyn SqlExecutor, + 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_store_version != REPLICA_STORE_VERSION { + return Err(SqlError::InvalidArgument(format!( + "unsupported replica-store version {}, expected {}", + backup.replica_store_version, REPLICA_STORE_VERSION + ))); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use radroots_sql_core::ExecOutcome; + use std::sync::Mutex; + use std::sync::atomic::{AtomicUsize, Ordering}; + + fn assert_sql_error_code<T: core::fmt::Debug>(result: Result<T, SqlError>, code: &str) { + let err = result.unwrap_err(); + assert_eq!(err.code(), code); + } + + struct MockExecutor { + query_rules: Vec<(String, String)>, + fail_exec_contains: Option<String>, + fail_query_contains: Option<String>, + fail_begin: bool, + fail_commit: bool, + 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, + fail_query_contains: None, + fail_begin: false, + fail_commit: false, + exec_calls: Mutex::new(Vec::new()), + begin_calls: AtomicUsize::new(0), + commit_calls: AtomicUsize::new(0), + rollback_calls: AtomicUsize::new(0), + } + } + + fn with_query_failure(mut self, needle: &str) -> Self { + self.fail_query_contains = Some(needle.to_string()); + self + } + + fn with_begin_failure(mut self) -> Self { + self.fail_begin = true; + self + } + + fn with_commit_failure(mut self) -> Self { + self.fail_commit = true; + self + } + + 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 + && 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> { + if let Some(needle) = &self.fail_query_contains + && sql.contains(needle) + { + return Err(SqlError::InvalidQuery(String::from("forced query failure"))); + } + 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); + if self.fail_begin { + return Err(SqlError::InvalidQuery(String::from("forced begin failure"))); + } + Ok(()) + } + + fn commit(&self) -> Result<(), SqlError> { + self.commit_calls.fetch_add(1, Ordering::SeqCst); + if self.fail_commit { + return Err(SqlError::InvalidQuery(String::from( + "forced commit failure", + ))); + } + Ok(()) + } + + fn rollback(&self) -> Result<(), SqlError> { + self.rollback_calls.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + } + + fn backup_with_versions(format_version: &str, replica_store_version: &str) -> DatabaseBackup { + DatabaseBackup { + format_version: format_version.to_string(), + replica_store_version: replica_store_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_store_version: REPLICA_STORE_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(), + }; + + assert_sql_error_code( + restore_database_backup(&executor, &backup), + "ERR_INVALID_QUERY", + ); + 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_store_version: REPLICA_STORE_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 load_schema_rejects_invalid_json() { + let executor = MockExecutor::new( + vec![( + String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), + String::from("{"), + )], + None, + ); + assert_sql_error_code(load_schema(&executor), "ERR_SERIALIZATION"); + } + + #[test] + fn validate_backup_version_rejects_invalid_versions() { + let wrong_format = backup_with_versions("0.0.1", REPLICA_STORE_VERSION); + assert_sql_error_code( + validate_backup_version(&wrong_format), + "ERR_INVALID_ARGUMENT", + ); + + let wrong_db_version = backup_with_versions(DATABASE_BACKUP_VERSION, "0.0.0"); + assert_sql_error_code( + validate_backup_version(&wrong_db_version), + "ERR_INVALID_ARGUMENT", + ); + } + + #[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, REPLICA_STORE_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); + assert_sql_error_code( + restore_database_backup_json(&executor, "{"), + "ERR_SERIALIZATION", + ); + } + + #[test] + fn restore_database_backup_json_accepts_valid_json() { + let executor = MockExecutor::new( + vec![( + String::from("select type, name from sqlite_master"), + String::from("[]"), + )], + None, + ); + let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_VERSION); + let backup_json = serde_json::to_string(&backup).expect("serialize backup"); + + restore_database_backup_json(&executor, &backup_json).expect("restore should succeed"); + assert_eq!(executor.begin_count(), 1); + assert_eq!(executor.commit_count(), 1); + assert_eq!(executor.rollback_count(), 0); + } + + #[test] + fn export_database_backup_propagates_schema_query_errors() { + let executor = MockExecutor::new(Vec::new(), None).with_query_failure( + "select type, name, tbl_name as table_name, sql from sqlite_master", + ); + assert_sql_error_code(export_database_backup(&executor), "ERR_INVALID_QUERY"); + } + + #[test] + fn export_database_backup_propagates_table_query_errors() { + let schema_rows = serde_json::json!([ + { + "type": "table", + "name": "tb_a", + "table_name": "tb_a", + "sql": "CREATE TABLE tb_a (id TEXT);" + } + ]) + .to_string(); + let executor = MockExecutor::new( + vec![( + String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), + schema_rows, + )], + None, + ) + .with_query_failure("SELECT * FROM \"tb_a\";"); + assert_sql_error_code(export_database_backup(&executor), "ERR_INVALID_QUERY"); + } + + #[test] + fn export_database_backup_json_propagates_export_errors() { + let executor = MockExecutor::new(Vec::new(), None).with_query_failure( + "select type, name, tbl_name as table_name, sql from sqlite_master", + ); + assert_sql_error_code(export_database_backup_json(&executor), "ERR_INVALID_QUERY"); + } + + #[test] + fn export_database_backup_succeeds_with_empty_schema() { + let executor = MockExecutor::new( + vec![( + String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), + String::from("[]"), + )], + None, + ); + let backup = export_database_backup(&executor).expect("backup success"); + assert!(backup.schema.is_empty()); + assert!(backup.data.is_empty()); + } + + #[test] + fn export_database_backup_json_succeeds_with_empty_schema() { + let executor = MockExecutor::new( + vec![( + String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), + String::from("[]"), + )], + None, + ); + let backup_json = export_database_backup_json(&executor).expect("backup json success"); + assert!(backup_json.contains("\"schema\":[]")); + } + + #[test] + fn drop_existing_objects_rejects_invalid_master_json() { + let executor = MockExecutor::new( + vec![( + String::from("select type, name from sqlite_master"), + String::from("{"), + )], + None, + ); + assert_sql_error_code(drop_existing_objects(&executor), "ERR_SERIALIZATION"); + } + + #[test] + fn drop_existing_objects_propagates_drop_exec_errors() { + let master_rows = serde_json::json!([{ "type": "table", "name": "tb_a" }]).to_string(); + let executor = MockExecutor::new( + vec![( + String::from("select type, name from sqlite_master"), + master_rows, + )], + Some(String::from("DROP TABLE IF EXISTS")), + ); + assert_sql_error_code(drop_existing_objects(&executor), "ERR_INVALID_QUERY"); + } + + #[test] + fn create_schema_from_backup_propagates_non_table_exec_errors() { + let executor = MockExecutor::new(Vec::new(), Some(String::from("CREATE VIEW"))); + let schema = vec![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;")), + }]; + assert_sql_error_code( + create_schema_from_backup(&executor, &schema), + "ERR_INVALID_QUERY", + ); + } + + #[test] + fn read_tables_for_backup_propagates_query_errors() { + let executor = + MockExecutor::new(Vec::new(), None).with_query_failure("SELECT * FROM \"tb_a\";"); + 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);")), + }]; + assert_sql_error_code( + read_tables_for_backup(&executor, &schema), + "ERR_INVALID_QUERY", + ); + } + + #[test] + fn read_tables_for_backup_propagates_parse_errors() { + let executor = MockExecutor::new( + vec![(String::from("SELECT * FROM \"tb_a\";"), String::from("{"))], + 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);")), + }]; + assert_sql_error_code( + read_tables_for_backup(&executor, &schema), + "ERR_SERIALIZATION", + ); + } + + #[test] + fn restore_database_backup_rejects_invalid_versions_before_transaction() { + let executor = MockExecutor::new(Vec::new(), None); + let backup = backup_with_versions("0.0.1", REPLICA_STORE_VERSION); + assert_sql_error_code( + restore_database_backup(&executor, &backup), + "ERR_INVALID_ARGUMENT", + ); + assert_eq!(executor.begin_count(), 0); + } + + #[test] + fn restore_database_backup_fails_when_foreign_keys_disable_fails() { + let executor = MockExecutor::new( + vec![( + String::from("select type, name from sqlite_master"), + String::from("[]"), + )], + Some(String::from("PRAGMA foreign_keys = OFF;")), + ); + let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_VERSION); + assert_sql_error_code( + restore_database_backup(&executor, &backup), + "ERR_INVALID_QUERY", + ); + } + + #[test] + fn restore_database_backup_fails_when_begin_fails() { + let executor = MockExecutor::new( + vec![( + String::from("select type, name from sqlite_master"), + String::from("[]"), + )], + None, + ) + .with_begin_failure(); + let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_VERSION); + assert_sql_error_code( + restore_database_backup(&executor, &backup), + "ERR_INVALID_QUERY", + ); + } + + #[test] + fn restore_database_backup_fails_when_drop_query_fails() { + let executor = MockExecutor::new(Vec::new(), None) + .with_query_failure("select type, name from sqlite_master"); + let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_VERSION); + assert_sql_error_code( + restore_database_backup(&executor, &backup), + "ERR_INVALID_QUERY", + ); + } + + #[test] + fn restore_database_backup_fails_when_create_schema_fails() { + let executor = MockExecutor::new( + vec![( + String::from("select type, name from sqlite_master"), + String::from("[]"), + )], + Some(String::from("CREATE TABLE tb_a")), + ); + let backup = DatabaseBackup { + format_version: DATABASE_BACKUP_VERSION.to_string(), + replica_store_version: REPLICA_STORE_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);")), + }], + migrations: Vec::new(), + data: Vec::new(), + }; + + assert_sql_error_code( + restore_database_backup(&executor, &backup), + "ERR_INVALID_QUERY", + ); + assert_eq!(executor.begin_count(), 1); + assert_eq!(executor.commit_count(), 0); + assert_eq!(executor.rollback_count(), 1); + } + + #[test] + fn restore_database_backup_fails_when_insert_rows_fail() { + let executor = MockExecutor::new( + vec![( + String::from("select type, name from sqlite_master"), + String::from("[]"), + )], + Some(String::from("INSERT INTO \"tb_a\"")), + ); + let mut row = Map::new(); + row.insert(String::from("id"), Value::from("1")); + let backup = DatabaseBackup { + format_version: DATABASE_BACKUP_VERSION.to_string(), + replica_store_version: REPLICA_STORE_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);")), + }], + migrations: Vec::new(), + data: vec![TableData { + name: String::from("tb_a"), + rows: vec![row], + }], + }; + assert_sql_error_code( + restore_database_backup(&executor, &backup), + "ERR_INVALID_QUERY", + ); + } + + #[test] + fn restore_database_backup_fails_when_commit_fails() { + let executor = MockExecutor::new( + vec![( + String::from("select type, name from sqlite_master"), + String::from("[]"), + )], + None, + ) + .with_commit_failure(); + let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_VERSION); + assert_sql_error_code( + restore_database_backup(&executor, &backup), + "ERR_INVALID_QUERY", + ); + } + + #[test] + fn restore_database_backup_fails_when_foreign_keys_enable_fails_after_commit() { + let executor = MockExecutor::new( + vec![( + String::from("select type, name from sqlite_master"), + String::from("[]"), + )], + Some(String::from("PRAGMA foreign_keys = ON;")), + ); + let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_VERSION); + assert_sql_error_code( + restore_database_backup(&executor, &backup), + "ERR_INVALID_QUERY", + ); + } +} diff --git a/crates/replica_store/src/export.rs b/crates/replica_store/src/export.rs @@ -0,0 +1,285 @@ +use radroots_sql_core::{SqlExecutor, error::SqlError, utils}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::backup::{ + DATABASE_BACKUP_VERSION, MigrationBackup, REPLICA_STORE_VERSION, SchemaEntry, + escape_identifier, export_migrations, load_schema, +}; + +pub const REPLICA_STORE_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 ReplicaStoreExportManifestRs { + pub export_version: String, + pub replica_store_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( + executor: &dyn SqlExecutor, +) -> Result<ReplicaStoreExportManifestRs, 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(ReplicaStoreExportManifestRs { + export_version: REPLICA_STORE_EXPORT_VERSION.to_string(), + replica_store_version: REPLICA_STORE_VERSION.to_string(), + backup_format_version: DATABASE_BACKUP_VERSION.to_string(), + schema_hash, + schema, + migrations, + table_counts, + }) +} + +fn load_table_counts( + executor: &dyn SqlExecutor, + 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]) -> String { + let mut hasher = Sha256::new(); + for entry in schema { + hasher.update(entry.object_type.as_bytes()); + hasher.update([0]); + hasher.update(entry.name.as_bytes()); + hasher.update([0]); + if let Some(table_name) = &entry.table_name { + hasher.update(table_name.as_bytes()); + } + hasher.update([0]); + if let Some(sql) = &entry.sql { + hasher.update(sql.as_bytes()); + } + hasher.update([255]); + } + hex::encode(hasher.finalize()) +} + +#[cfg(test)] +mod tests { + use super::*; + use radroots_sql_core::ExecOutcome; + + fn assert_sql_error_code<T: core::fmt::Debug>(result: Result<T, SqlError>, code: &str) { + let err = result.unwrap_err(); + assert_eq!(err.code(), code); + } + + struct MockExecutor { + query_rules: Vec<(String, String)>, + fail_query_contains: Option<String>, + } + + impl MockExecutor { + fn new(query_rules: Vec<(String, String)>, fail_query_contains: Option<String>) -> Self { + Self { + query_rules, + fail_query_contains, + } + } + } + + impl SqlExecutor for MockExecutor { + fn exec(&self, _sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> { + Ok(ExecOutcome { + changes: 1, + last_insert_id: 1, + }) + } + + fn query_raw(&self, sql: &str, _params_json: &str) -> Result<String, SqlError> { + if let Some(needle) = &self.fail_query_contains + && sql.contains(needle) + { + return Err(SqlError::InvalidQuery(String::from("forced query failure"))); + } + for (needle, response) in &self.query_rules { + if sql.contains(needle) { + return Ok(response.clone()); + } + } + Ok(String::from("[]")) + } + + fn begin(&self) -> Result<(), SqlError> { + Ok(()) + } + + fn commit(&self) -> Result<(), SqlError> { + Ok(()) + } + + fn rollback(&self) -> Result<(), SqlError> { + Ok(()) + } + } + + #[test] + fn export_manifest_propagates_schema_query_errors() { + let executor = MockExecutor::new( + Vec::new(), + Some(String::from( + "select type, name, tbl_name as table_name, sql from sqlite_master", + )), + ); + assert_sql_error_code(export_manifest(&executor), "ERR_INVALID_QUERY"); + } + + #[test] + fn export_manifest_propagates_table_count_query_errors() { + let schema_rows = serde_json::json!([ + { + "type": "table", + "name": "tb_a", + "table_name": "tb_a", + "sql": "CREATE TABLE tb_a (id TEXT);" + } + ]) + .to_string(); + let executor = MockExecutor::new( + vec![( + String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), + schema_rows, + )], + Some(String::from("select count(1) as count from \"tb_a\"")), + ); + assert_sql_error_code(export_manifest(&executor), "ERR_INVALID_QUERY"); + } + + #[test] + fn export_manifest_propagates_table_count_parse_errors() { + let schema_rows = serde_json::json!([ + { + "type": "table", + "name": "tb_a", + "table_name": "tb_a", + "sql": "CREATE TABLE tb_a (id TEXT);" + } + ]) + .to_string(); + let executor = MockExecutor::new( + vec![ + ( + String::from( + "select type, name, tbl_name as table_name, sql from sqlite_master", + ), + schema_rows, + ), + ( + String::from("select count(1) as count from \"tb_a\""), + String::from("{"), + ), + ], + None, + ); + assert_sql_error_code(export_manifest(&executor), "ERR_SERIALIZATION"); + } + + #[test] + fn export_manifest_defaults_missing_count_row_to_zero() { + let schema_rows = serde_json::json!([ + { + "type": "table", + "name": "tb_a", + "table_name": "tb_a", + "sql": "CREATE TABLE tb_a (id TEXT);" + } + ]) + .to_string(); + let executor = MockExecutor::new( + vec![ + ( + String::from( + "select type, name, tbl_name as table_name, sql from sqlite_master", + ), + schema_rows, + ), + ( + String::from("select count(1) as count from \"tb_a\""), + String::from("[]"), + ), + ], + None, + ); + let manifest = export_manifest(&executor).expect("export should succeed"); + assert_eq!(manifest.table_counts.len(), 1); + assert_eq!(manifest.table_counts[0].name, "tb_a"); + assert_eq!(manifest.table_counts[0].row_count, 0); + } + + #[test] + fn schema_hash_handles_optional_fields() { + let with_all = 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);")), + }; + let without_optional = SchemaEntry { + object_type: String::from("index"), + name: String::from("ix_a"), + table_name: None, + sql: None, + }; + let hash = schema_hash(&[with_all, without_optional]); + assert_eq!(hash.len(), 64); + } + + #[test] + fn mock_executor_trait_and_query_paths_are_covered() { + let executor = MockExecutor::new( + vec![(String::from("select 1"), String::from("[{\"count\":1}]"))], + None, + ); + let outcome = executor.exec("select 1", "[]").expect("exec"); + assert_eq!(outcome.changes, 1); + assert_eq!(outcome.last_insert_id, 1); + + executor.begin().expect("begin"); + executor.commit().expect("commit"); + executor.rollback().expect("rollback"); + + let matched = executor.query_raw("select 1", "[]").expect("matched query"); + assert_eq!(matched, "[{\"count\":1}]"); + let fallback = executor + .query_raw("select 2", "[]") + .expect("fallback query"); + assert_eq!(fallback, "[]"); + + let failing = MockExecutor::new(Vec::new(), Some(String::from("select fail"))); + assert_sql_error_code(failing.query_raw("select fail", "[]"), "ERR_INVALID_QUERY"); + } +} diff --git a/crates/replica_store/src/lib.rs b/crates/replica_store/src/lib.rs @@ -0,0 +1,784 @@ +use radroots_replica_schema::ReplicaSchemaError; +pub use radroots_sql_core::error::SqlError; +pub use radroots_sql_core::{ExecOutcome, SqlExecutor}; + +use radroots_replica_schema::farm::{ + IFarmCreate, IFarmCreateResolve, IFarmDelete, IFarmDeleteResolve, IFarmFindMany, + IFarmFindManyResolve, IFarmFindOne, IFarmFindOneResolve, IFarmUpdate, IFarmUpdateResolve, +}; + +use radroots_replica_schema::farm_gcs_location::{ + IFarmGcsLocationCreate, IFarmGcsLocationCreateResolve, IFarmGcsLocationDelete, + IFarmGcsLocationDeleteResolve, IFarmGcsLocationFindMany, IFarmGcsLocationFindManyResolve, + IFarmGcsLocationFindOne, IFarmGcsLocationFindOneResolve, IFarmGcsLocationUpdate, + IFarmGcsLocationUpdateResolve, +}; + +use radroots_replica_schema::farm_member::{ + IFarmMemberCreate, IFarmMemberCreateResolve, IFarmMemberDelete, IFarmMemberDeleteResolve, + IFarmMemberFindMany, IFarmMemberFindManyResolve, IFarmMemberFindOne, IFarmMemberFindOneResolve, + IFarmMemberUpdate, IFarmMemberUpdateResolve, +}; + +use radroots_replica_schema::farm_member_claim::{ + IFarmMemberClaimCreate, IFarmMemberClaimCreateResolve, IFarmMemberClaimDelete, + IFarmMemberClaimDeleteResolve, IFarmMemberClaimFindMany, IFarmMemberClaimFindManyResolve, + IFarmMemberClaimFindOne, IFarmMemberClaimFindOneResolve, IFarmMemberClaimUpdate, + IFarmMemberClaimUpdateResolve, +}; + +use radroots_replica_schema::farm_tag::{ + IFarmTagCreate, IFarmTagCreateResolve, IFarmTagDelete, IFarmTagDeleteResolve, IFarmTagFindMany, + IFarmTagFindManyResolve, IFarmTagFindOne, IFarmTagFindOneResolve, IFarmTagUpdate, + IFarmTagUpdateResolve, +}; + +use radroots_replica_schema::gcs_location::{ + IGcsLocationCreate, IGcsLocationCreateResolve, IGcsLocationDelete, IGcsLocationDeleteResolve, + IGcsLocationFindMany, IGcsLocationFindManyResolve, IGcsLocationFindOne, + IGcsLocationFindOneResolve, IGcsLocationUpdate, IGcsLocationUpdateResolve, +}; + +use radroots_replica_schema::log_error::{ + ILogErrorCreate, ILogErrorCreateResolve, ILogErrorDelete, ILogErrorDeleteResolve, + ILogErrorFindMany, ILogErrorFindManyResolve, ILogErrorFindOne, ILogErrorFindOneResolve, + ILogErrorUpdate, ILogErrorUpdateResolve, +}; + +use radroots_replica_schema::media_image::{ + IMediaImageCreate, IMediaImageCreateResolve, IMediaImageDelete, IMediaImageDeleteResolve, + IMediaImageFindMany, IMediaImageFindManyResolve, IMediaImageFindOne, IMediaImageFindOneResolve, + IMediaImageUpdate, IMediaImageUpdateResolve, +}; + +use radroots_replica_schema::nostr_profile::{ + INostrProfileCreate, INostrProfileCreateResolve, INostrProfileDelete, + INostrProfileDeleteResolve, INostrProfileFindMany, INostrProfileFindManyResolve, + INostrProfileFindOne, INostrProfileFindOneResolve, INostrProfileUpdate, + INostrProfileUpdateResolve, +}; + +use radroots_replica_schema::nostr_event_head::{ + INostrEventHeadCreate, INostrEventHeadCreateResolve, INostrEventHeadDelete, + INostrEventHeadDeleteResolve, INostrEventHeadFindMany, INostrEventHeadFindManyResolve, + INostrEventHeadFindOne, INostrEventHeadFindOneResolve, INostrEventHeadUpdate, + INostrEventHeadUpdateResolve, +}; + +use radroots_replica_schema::nostr_relay::{ + INostrRelayCreate, INostrRelayCreateResolve, INostrRelayDelete, INostrRelayDeleteResolve, + INostrRelayFindMany, INostrRelayFindManyResolve, INostrRelayFindOne, INostrRelayFindOneResolve, + INostrRelayUpdate, INostrRelayUpdateResolve, +}; + +use radroots_replica_schema::trade_product::{ + ITradeProductCreate, ITradeProductCreateResolve, ITradeProductDelete, + ITradeProductDeleteResolve, ITradeProductFindMany, ITradeProductFindManyResolve, + ITradeProductFindOne, ITradeProductFindOneResolve, ITradeProductUpdate, + ITradeProductUpdateResolve, +}; + +use radroots_replica_schema::plot::{ + IPlotCreate, IPlotCreateResolve, IPlotDelete, IPlotDeleteResolve, IPlotFindMany, + IPlotFindManyResolve, IPlotFindOne, IPlotFindOneResolve, IPlotUpdate, IPlotUpdateResolve, +}; + +use radroots_replica_schema::plot_gcs_location::{ + IPlotGcsLocationCreate, IPlotGcsLocationCreateResolve, IPlotGcsLocationDelete, + IPlotGcsLocationDeleteResolve, IPlotGcsLocationFindMany, IPlotGcsLocationFindManyResolve, + IPlotGcsLocationFindOne, IPlotGcsLocationFindOneResolve, IPlotGcsLocationUpdate, + IPlotGcsLocationUpdateResolve, +}; + +use radroots_replica_schema::plot_tag::{ + IPlotTagCreate, IPlotTagCreateResolve, IPlotTagDelete, IPlotTagDeleteResolve, IPlotTagFindMany, + IPlotTagFindManyResolve, IPlotTagFindOne, IPlotTagFindOneResolve, IPlotTagUpdate, + IPlotTagUpdateResolve, +}; + +use radroots_replica_schema::nostr_profile_relay::{ + INostrProfileRelayRelation, INostrProfileRelayResolve, +}; + +use radroots_replica_schema::trade_product_location::{ + ITradeProductLocationRelation, ITradeProductLocationResolve, +}; + +use radroots_replica_schema::trade_product_media::{ + ITradeProductMediaRelation, ITradeProductMediaResolve, +}; + +pub mod backup; +pub mod export; +pub mod migrations; +pub mod models; +pub mod query; +pub use backup::{DatabaseBackup, MigrationBackup, SchemaEntry}; +pub use export::{ + REPLICA_STORE_EXPORT_VERSION, ReplicaStoreExportManifestRs, TableCount, export_manifest, +}; +pub use models::*; +pub use query::ReplicaTradeProductSummaryRow; + +pub struct ReplicaSql<E: SqlExecutor> { + executor: E, +} + +impl<E: SqlExecutor> ReplicaSql<E> { + pub fn coverage_branch_probe(enabled: bool) -> &'static str { + if enabled { "enabled" } else { "disabled" } + } +} + +impl<E: SqlExecutor> ReplicaSql<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, ReplicaSchemaError<SqlError>> { + models::farm::create(self.executor(), opts) + } + + pub fn farm_find_many( + &self, + opts: &IFarmFindMany, + ) -> Result<IFarmFindManyResolve, ReplicaSchemaError<SqlError>> { + models::farm::find_many(self.executor(), opts) + } + + pub fn farm_find_one( + &self, + opts: &IFarmFindOne, + ) -> Result<IFarmFindOneResolve, ReplicaSchemaError<SqlError>> { + models::farm::find_one(self.executor(), opts) + } + + pub fn farm_update( + &self, + opts: &IFarmUpdate, + ) -> Result<IFarmUpdateResolve, ReplicaSchemaError<SqlError>> { + models::farm::update(self.executor(), opts) + } + + pub fn farm_delete( + &self, + opts: &IFarmDelete, + ) -> Result<IFarmDeleteResolve, ReplicaSchemaError<SqlError>> { + models::farm::delete(self.executor(), opts) + } + + pub fn plot_create( + &self, + opts: &IPlotCreate, + ) -> Result<IPlotCreateResolve, ReplicaSchemaError<SqlError>> { + models::plot::create(self.executor(), opts) + } + + pub fn plot_find_many( + &self, + opts: &IPlotFindMany, + ) -> Result<IPlotFindManyResolve, ReplicaSchemaError<SqlError>> { + models::plot::find_many(self.executor(), opts) + } + + pub fn plot_find_one( + &self, + opts: &IPlotFindOne, + ) -> Result<IPlotFindOneResolve, ReplicaSchemaError<SqlError>> { + models::plot::find_one(self.executor(), opts) + } + + pub fn plot_update( + &self, + opts: &IPlotUpdate, + ) -> Result<IPlotUpdateResolve, ReplicaSchemaError<SqlError>> { + models::plot::update(self.executor(), opts) + } + + pub fn plot_delete( + &self, + opts: &IPlotDelete, + ) -> Result<IPlotDeleteResolve, ReplicaSchemaError<SqlError>> { + models::plot::delete(self.executor(), opts) + } + + pub fn gcs_location_create( + &self, + opts: &IGcsLocationCreate, + ) -> Result<IGcsLocationCreateResolve, ReplicaSchemaError<SqlError>> { + models::gcs_location::create(self.executor(), opts) + } + + pub fn gcs_location_find_many( + &self, + opts: &IGcsLocationFindMany, + ) -> Result<IGcsLocationFindManyResolve, ReplicaSchemaError<SqlError>> { + models::gcs_location::find_many(self.executor(), opts) + } + + pub fn gcs_location_find_one( + &self, + opts: &IGcsLocationFindOne, + ) -> Result<IGcsLocationFindOneResolve, ReplicaSchemaError<SqlError>> { + models::gcs_location::find_one(self.executor(), opts) + } + + pub fn gcs_location_update( + &self, + opts: &IGcsLocationUpdate, + ) -> Result<IGcsLocationUpdateResolve, ReplicaSchemaError<SqlError>> { + models::gcs_location::update(self.executor(), opts) + } + + pub fn gcs_location_delete( + &self, + opts: &IGcsLocationDelete, + ) -> Result<IGcsLocationDeleteResolve, ReplicaSchemaError<SqlError>> { + models::gcs_location::delete(self.executor(), opts) + } + + pub fn farm_gcs_location_create( + &self, + opts: &IFarmGcsLocationCreate, + ) -> Result<IFarmGcsLocationCreateResolve, ReplicaSchemaError<SqlError>> { + models::farm_gcs_location::create(self.executor(), opts) + } + + pub fn farm_gcs_location_find_many( + &self, + opts: &IFarmGcsLocationFindMany, + ) -> Result<IFarmGcsLocationFindManyResolve, ReplicaSchemaError<SqlError>> { + models::farm_gcs_location::find_many(self.executor(), opts) + } + + pub fn farm_gcs_location_find_one( + &self, + opts: &IFarmGcsLocationFindOne, + ) -> Result<IFarmGcsLocationFindOneResolve, ReplicaSchemaError<SqlError>> { + models::farm_gcs_location::find_one(self.executor(), opts) + } + + pub fn farm_gcs_location_update( + &self, + opts: &IFarmGcsLocationUpdate, + ) -> Result<IFarmGcsLocationUpdateResolve, ReplicaSchemaError<SqlError>> { + models::farm_gcs_location::update(self.executor(), opts) + } + + pub fn farm_gcs_location_delete( + &self, + opts: &IFarmGcsLocationDelete, + ) -> Result<IFarmGcsLocationDeleteResolve, ReplicaSchemaError<SqlError>> { + models::farm_gcs_location::delete(self.executor(), opts) + } + + pub fn plot_gcs_location_create( + &self, + opts: &IPlotGcsLocationCreate, + ) -> Result<IPlotGcsLocationCreateResolve, ReplicaSchemaError<SqlError>> { + models::plot_gcs_location::create(self.executor(), opts) + } + + pub fn plot_gcs_location_find_many( + &self, + opts: &IPlotGcsLocationFindMany, + ) -> Result<IPlotGcsLocationFindManyResolve, ReplicaSchemaError<SqlError>> { + models::plot_gcs_location::find_many(self.executor(), opts) + } + + pub fn plot_gcs_location_find_one( + &self, + opts: &IPlotGcsLocationFindOne, + ) -> Result<IPlotGcsLocationFindOneResolve, ReplicaSchemaError<SqlError>> { + models::plot_gcs_location::find_one(self.executor(), opts) + } + + pub fn plot_gcs_location_update( + &self, + opts: &IPlotGcsLocationUpdate, + ) -> Result<IPlotGcsLocationUpdateResolve, ReplicaSchemaError<SqlError>> { + models::plot_gcs_location::update(self.executor(), opts) + } + + pub fn plot_gcs_location_delete( + &self, + opts: &IPlotGcsLocationDelete, + ) -> Result<IPlotGcsLocationDeleteResolve, ReplicaSchemaError<SqlError>> { + models::plot_gcs_location::delete(self.executor(), opts) + } + + pub fn farm_tag_create( + &self, + opts: &IFarmTagCreate, + ) -> Result<IFarmTagCreateResolve, ReplicaSchemaError<SqlError>> { + models::farm_tag::create(self.executor(), opts) + } + + pub fn farm_tag_find_many( + &self, + opts: &IFarmTagFindMany, + ) -> Result<IFarmTagFindManyResolve, ReplicaSchemaError<SqlError>> { + models::farm_tag::find_many(self.executor(), opts) + } + + pub fn farm_tag_find_one( + &self, + opts: &IFarmTagFindOne, + ) -> Result<IFarmTagFindOneResolve, ReplicaSchemaError<SqlError>> { + models::farm_tag::find_one(self.executor(), opts) + } + + pub fn farm_tag_update( + &self, + opts: &IFarmTagUpdate, + ) -> Result<IFarmTagUpdateResolve, ReplicaSchemaError<SqlError>> { + models::farm_tag::update(self.executor(), opts) + } + + pub fn farm_tag_delete( + &self, + opts: &IFarmTagDelete, + ) -> Result<IFarmTagDeleteResolve, ReplicaSchemaError<SqlError>> { + models::farm_tag::delete(self.executor(), opts) + } + + pub fn plot_tag_create( + &self, + opts: &IPlotTagCreate, + ) -> Result<IPlotTagCreateResolve, ReplicaSchemaError<SqlError>> { + models::plot_tag::create(self.executor(), opts) + } + + pub fn plot_tag_find_many( + &self, + opts: &IPlotTagFindMany, + ) -> Result<IPlotTagFindManyResolve, ReplicaSchemaError<SqlError>> { + models::plot_tag::find_many(self.executor(), opts) + } + + pub fn plot_tag_find_one( + &self, + opts: &IPlotTagFindOne, + ) -> Result<IPlotTagFindOneResolve, ReplicaSchemaError<SqlError>> { + models::plot_tag::find_one(self.executor(), opts) + } + + pub fn plot_tag_update( + &self, + opts: &IPlotTagUpdate, + ) -> Result<IPlotTagUpdateResolve, ReplicaSchemaError<SqlError>> { + models::plot_tag::update(self.executor(), opts) + } + + pub fn plot_tag_delete( + &self, + opts: &IPlotTagDelete, + ) -> Result<IPlotTagDeleteResolve, ReplicaSchemaError<SqlError>> { + models::plot_tag::delete(self.executor(), opts) + } + + pub fn farm_member_create( + &self, + opts: &IFarmMemberCreate, + ) -> Result<IFarmMemberCreateResolve, ReplicaSchemaError<SqlError>> { + models::farm_member::create(self.executor(), opts) + } + + pub fn farm_member_find_many( + &self, + opts: &IFarmMemberFindMany, + ) -> Result<IFarmMemberFindManyResolve, ReplicaSchemaError<SqlError>> { + models::farm_member::find_many(self.executor(), opts) + } + + pub fn farm_member_find_one( + &self, + opts: &IFarmMemberFindOne, + ) -> Result<IFarmMemberFindOneResolve, ReplicaSchemaError<SqlError>> { + models::farm_member::find_one(self.executor(), opts) + } + + pub fn farm_member_update( + &self, + opts: &IFarmMemberUpdate, + ) -> Result<IFarmMemberUpdateResolve, ReplicaSchemaError<SqlError>> { + models::farm_member::update(self.executor(), opts) + } + + pub fn farm_member_delete( + &self, + opts: &IFarmMemberDelete, + ) -> Result<IFarmMemberDeleteResolve, ReplicaSchemaError<SqlError>> { + models::farm_member::delete(self.executor(), opts) + } + + pub fn farm_member_claim_create( + &self, + opts: &IFarmMemberClaimCreate, + ) -> Result<IFarmMemberClaimCreateResolve, ReplicaSchemaError<SqlError>> { + models::farm_member_claim::create(self.executor(), opts) + } + + pub fn farm_member_claim_find_many( + &self, + opts: &IFarmMemberClaimFindMany, + ) -> Result<IFarmMemberClaimFindManyResolve, ReplicaSchemaError<SqlError>> { + models::farm_member_claim::find_many(self.executor(), opts) + } + + pub fn farm_member_claim_find_one( + &self, + opts: &IFarmMemberClaimFindOne, + ) -> Result<IFarmMemberClaimFindOneResolve, ReplicaSchemaError<SqlError>> { + models::farm_member_claim::find_one(self.executor(), opts) + } + + pub fn farm_member_claim_update( + &self, + opts: &IFarmMemberClaimUpdate, + ) -> Result<IFarmMemberClaimUpdateResolve, ReplicaSchemaError<SqlError>> { + models::farm_member_claim::update(self.executor(), opts) + } + + pub fn farm_member_claim_delete( + &self, + opts: &IFarmMemberClaimDelete, + ) -> Result<IFarmMemberClaimDeleteResolve, ReplicaSchemaError<SqlError>> { + models::farm_member_claim::delete(self.executor(), opts) + } + + pub fn log_error_create( + &self, + opts: &ILogErrorCreate, + ) -> Result<ILogErrorCreateResolve, ReplicaSchemaError<SqlError>> { + models::log_error::create(self.executor(), opts) + } + + pub fn log_error_find_many( + &self, + opts: &ILogErrorFindMany, + ) -> Result<ILogErrorFindManyResolve, ReplicaSchemaError<SqlError>> { + models::log_error::find_many(self.executor(), opts) + } + + pub fn log_error_find_one( + &self, + opts: &ILogErrorFindOne, + ) -> Result<ILogErrorFindOneResolve, ReplicaSchemaError<SqlError>> { + models::log_error::find_one(self.executor(), opts) + } + + pub fn log_error_update( + &self, + opts: &ILogErrorUpdate, + ) -> Result<ILogErrorUpdateResolve, ReplicaSchemaError<SqlError>> { + models::log_error::update(self.executor(), opts) + } + + pub fn log_error_delete( + &self, + opts: &ILogErrorDelete, + ) -> Result<ILogErrorDeleteResolve, ReplicaSchemaError<SqlError>> { + models::log_error::delete(self.executor(), opts) + } + + pub fn media_image_create( + &self, + opts: &IMediaImageCreate, + ) -> Result<IMediaImageCreateResolve, ReplicaSchemaError<SqlError>> { + models::media_image::create(self.executor(), opts) + } + + pub fn media_image_find_many( + &self, + opts: &IMediaImageFindMany, + ) -> Result<IMediaImageFindManyResolve, ReplicaSchemaError<SqlError>> { + models::media_image::find_many(self.executor(), opts) + } + + pub fn media_image_find_one( + &self, + opts: &IMediaImageFindOne, + ) -> Result<IMediaImageFindOneResolve, ReplicaSchemaError<SqlError>> { + models::media_image::find_one(self.executor(), opts) + } + + pub fn media_image_update( + &self, + opts: &IMediaImageUpdate, + ) -> Result<IMediaImageUpdateResolve, ReplicaSchemaError<SqlError>> { + models::media_image::update(self.executor(), opts) + } + + pub fn media_image_delete( + &self, + opts: &IMediaImageDelete, + ) -> Result<IMediaImageDeleteResolve, ReplicaSchemaError<SqlError>> { + models::media_image::delete(self.executor(), opts) + } + + pub fn nostr_profile_create( + &self, + opts: &INostrProfileCreate, + ) -> Result<INostrProfileCreateResolve, ReplicaSchemaError<SqlError>> { + models::nostr_profile::create(self.executor(), opts) + } + + pub fn nostr_profile_find_many( + &self, + opts: &INostrProfileFindMany, + ) -> Result<INostrProfileFindManyResolve, ReplicaSchemaError<SqlError>> { + models::nostr_profile::find_many(self.executor(), opts) + } + + pub fn nostr_profile_find_one( + &self, + opts: &INostrProfileFindOne, + ) -> Result<INostrProfileFindOneResolve, ReplicaSchemaError<SqlError>> { + models::nostr_profile::find_one(self.executor(), opts) + } + + pub fn nostr_profile_update( + &self, + opts: &INostrProfileUpdate, + ) -> Result<INostrProfileUpdateResolve, ReplicaSchemaError<SqlError>> { + models::nostr_profile::update(self.executor(), opts) + } + + pub fn nostr_profile_delete( + &self, + opts: &INostrProfileDelete, + ) -> Result<INostrProfileDeleteResolve, ReplicaSchemaError<SqlError>> { + models::nostr_profile::delete(self.executor(), opts) + } + + pub fn nostr_event_head_create( + &self, + opts: &INostrEventHeadCreate, + ) -> Result<INostrEventHeadCreateResolve, ReplicaSchemaError<SqlError>> { + models::nostr_event_head::create(self.executor(), opts) + } + + pub fn nostr_event_head_find_many( + &self, + opts: &INostrEventHeadFindMany, + ) -> Result<INostrEventHeadFindManyResolve, ReplicaSchemaError<SqlError>> { + models::nostr_event_head::find_many(self.executor(), opts) + } + + pub fn nostr_event_head_find_one( + &self, + opts: &INostrEventHeadFindOne, + ) -> Result<INostrEventHeadFindOneResolve, ReplicaSchemaError<SqlError>> { + models::nostr_event_head::find_one(self.executor(), opts) + } + + pub fn nostr_event_head_update( + &self, + opts: &INostrEventHeadUpdate, + ) -> Result<INostrEventHeadUpdateResolve, ReplicaSchemaError<SqlError>> { + models::nostr_event_head::update(self.executor(), opts) + } + + pub fn nostr_event_head_delete( + &self, + opts: &INostrEventHeadDelete, + ) -> Result<INostrEventHeadDeleteResolve, ReplicaSchemaError<SqlError>> { + models::nostr_event_head::delete(self.executor(), opts) + } + + pub fn nostr_relay_create( + &self, + opts: &INostrRelayCreate, + ) -> Result<INostrRelayCreateResolve, ReplicaSchemaError<SqlError>> { + models::nostr_relay::create(self.executor(), opts) + } + + pub fn nostr_relay_find_many( + &self, + opts: &INostrRelayFindMany, + ) -> Result<INostrRelayFindManyResolve, ReplicaSchemaError<SqlError>> { + models::nostr_relay::find_many(self.executor(), opts) + } + + pub fn nostr_relay_find_one( + &self, + opts: &INostrRelayFindOne, + ) -> Result<INostrRelayFindOneResolve, ReplicaSchemaError<SqlError>> { + models::nostr_relay::find_one(self.executor(), opts) + } + + pub fn nostr_relay_update( + &self, + opts: &INostrRelayUpdate, + ) -> Result<INostrRelayUpdateResolve, ReplicaSchemaError<SqlError>> { + models::nostr_relay::update(self.executor(), opts) + } + + pub fn nostr_relay_delete( + &self, + opts: &INostrRelayDelete, + ) -> Result<INostrRelayDeleteResolve, ReplicaSchemaError<SqlError>> { + models::nostr_relay::delete(self.executor(), opts) + } + + pub fn trade_product_create( + &self, + opts: &ITradeProductCreate, + ) -> Result<ITradeProductCreateResolve, ReplicaSchemaError<SqlError>> { + models::trade_product::create(self.executor(), opts) + } + + pub fn trade_product_find_many( + &self, + opts: &ITradeProductFindMany, + ) -> Result<ITradeProductFindManyResolve, ReplicaSchemaError<SqlError>> { + models::trade_product::find_many(self.executor(), opts) + } + + pub fn trade_product_find_one( + &self, + opts: &ITradeProductFindOne, + ) -> Result<ITradeProductFindOneResolve, ReplicaSchemaError<SqlError>> { + models::trade_product::find_one(self.executor(), opts) + } + + pub fn trade_product_update( + &self, + opts: &ITradeProductUpdate, + ) -> Result<ITradeProductUpdateResolve, ReplicaSchemaError<SqlError>> { + models::trade_product::update(self.executor(), opts) + } + + pub fn trade_product_delete( + &self, + opts: &ITradeProductDelete, + ) -> Result<ITradeProductDeleteResolve, ReplicaSchemaError<SqlError>> { + models::trade_product::delete(self.executor(), opts) + } + + pub fn nostr_profile_relay_set( + &self, + opts: &INostrProfileRelayRelation, + ) -> Result<INostrProfileRelayResolve, ReplicaSchemaError<SqlError>> { + models::nostr_profile_relay::set(self.executor(), opts) + } + + pub fn nostr_profile_relay_unset( + &self, + opts: &INostrProfileRelayRelation, + ) -> Result<INostrProfileRelayResolve, ReplicaSchemaError<SqlError>> { + models::nostr_profile_relay::unset(self.executor(), opts) + } + + pub fn trade_product_location_set( + &self, + opts: &ITradeProductLocationRelation, + ) -> Result<ITradeProductLocationResolve, ReplicaSchemaError<SqlError>> { + models::trade_product_location::set(self.executor(), opts) + } + + pub fn trade_product_location_unset( + &self, + opts: &ITradeProductLocationRelation, + ) -> Result<ITradeProductLocationResolve, ReplicaSchemaError<SqlError>> { + models::trade_product_location::unset(self.executor(), opts) + } + + pub fn trade_product_media_set( + &self, + opts: &ITradeProductMediaRelation, + ) -> Result<ITradeProductMediaResolve, ReplicaSchemaError<SqlError>> { + models::trade_product_media::set(self.executor(), opts) + } + + pub fn trade_product_media_unset( + &self, + opts: &ITradeProductMediaRelation, + ) -> Result<ITradeProductMediaResolve, ReplicaSchemaError<SqlError>> { + models::trade_product_media::unset(self.executor(), opts) + } +} + +#[cfg(test)] +mod tests { + use super::ReplicaSql; + 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 replica_sql_constructor_and_executor_access_are_supported() { + let db = ReplicaSql::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!( + ReplicaSql::<ProbeExecutor>::coverage_branch_probe(true), + "enabled" + ); + assert_eq!( + ReplicaSql::<ProbeExecutor>::coverage_branch_probe(false), + "disabled" + ); + } +} diff --git a/crates/replica_db/src/migrations.rs b/crates/replica_store/src/migrations.rs diff --git a/crates/replica_store/src/models/farm.rs b/crates/replica_store/src/models/farm.rs @@ -0,0 +1,159 @@ +use radroots_replica_schema::farm::{ + Farm, FarmQueryBindValues, IFarmCreate, IFarmCreateResolve, IFarmDelete, IFarmDeleteResolve, + IFarmFieldsFilter, IFarmFindMany, IFarmFindManyResolve, IFarmFindOne, IFarmFindOneResolve, + IFarmUpdate, IFarmUpdateResolve, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "farm"; + +pub fn create( + exec: &dyn SqlExecutor, + opts: &IFarmCreate, +) -> Result<IFarmCreateResolve, ReplicaSchemaError<SqlError>> { + let field_map = utils::to_object_map(opts).expect("farm create fields serialize"); + 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).expect("farm create bind params serialize"); + let _ = exec.exec(&sql, &params_json)?; + let on = FarmQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)? + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_one( + exec: &dyn SqlExecutor, + opts: &IFarmFindOne, +) -> Result<IFarmFindOneResolve, ReplicaSchemaError<SqlError>> { + let result = match opts { + IFarmFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_many( + exec: &dyn SqlExecutor, + opts: &IFarmFindMany, +) -> Result<IFarmFindManyResolve, ReplicaSchemaError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(ReplicaSchemaResultList { results }) +} + +fn find_many_filter( + exec: &dyn SqlExecutor, + filter: &Option<IFarmFieldsFilter>, +) -> Result<Vec<Farm>, ReplicaSchemaError<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).expect("farm find_many bind params serialize"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<Farm> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on( + exec: &dyn SqlExecutor, + on: &FarmQueryBindValues, +) -> Result<Option<Farm>, ReplicaSchemaError<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]).expect("farm find_one bind params serialize"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<Farm> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id(exec: &dyn SqlExecutor, id: &str) -> Result<Farm, ReplicaSchemaError<SqlError>> { + let params_json = utils::to_params_json(vec![Value::from(id.to_owned())]) + .expect("farm select_by_id bind params serialize"); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<Farm> = utils::parse_json(&json)?; + rows.pop() + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update( + exec: &dyn SqlExecutor, + opts: &IFarmUpdate, +) -> Result<IFarmUpdateResolve, ReplicaSchemaError<SqlError>> { + let mut updates = + utils::to_partial_object_map(&opts.fields).expect("farm update fields serialize"); + if updates.is_empty() { + return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("farm update bind params serialize"); + let _ = exec.exec(&sql, &params_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(ReplicaSchemaResult { result: updated }) +} + +pub fn delete( + exec: &dyn SqlExecutor, + opts: &IFarmDelete, +) -> Result<IFarmDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( + args.on.lookup_key(), + )))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) + .expect("farm delete bind params serialize"); + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, &params_json)?; + if outcome.changes == 0 { + return Err(ReplicaSchemaError::from(SqlError::NotFound( + id_for_lookup.clone(), + ))); + } + Ok(ReplicaSchemaResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica_store/src/models/farm_gcs_location.rs b/crates/replica_store/src/models/farm_gcs_location.rs @@ -0,0 +1,160 @@ +use radroots_replica_schema::farm_gcs_location::{ + FarmGcsLocation, FarmGcsLocationQueryBindValues, IFarmGcsLocationCreate, + IFarmGcsLocationCreateResolve, IFarmGcsLocationDelete, IFarmGcsLocationDeleteResolve, + IFarmGcsLocationFieldsFilter, IFarmGcsLocationFindMany, IFarmGcsLocationFindManyResolve, + IFarmGcsLocationFindOne, IFarmGcsLocationFindOneResolve, IFarmGcsLocationUpdate, + IFarmGcsLocationUpdateResolve, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "farm_gcs_location"; + +pub fn create( + exec: &dyn SqlExecutor, + opts: &IFarmGcsLocationCreate, +) -> Result<IFarmGcsLocationCreateResolve, ReplicaSchemaError<SqlError>> { + let field_map = utils::to_object_map(opts).expect("serialize object map"); + 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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let on = FarmGcsLocationQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)? + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_one( + exec: &dyn SqlExecutor, + opts: &IFarmGcsLocationFindOne, +) -> Result<IFarmGcsLocationFindOneResolve, ReplicaSchemaError<SqlError>> { + let result = match opts { + IFarmGcsLocationFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_many( + exec: &dyn SqlExecutor, + opts: &IFarmGcsLocationFindMany, +) -> Result<IFarmGcsLocationFindManyResolve, ReplicaSchemaError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(ReplicaSchemaResultList { results }) +} + +fn find_many_filter( + exec: &dyn SqlExecutor, + filter: &Option<IFarmGcsLocationFieldsFilter>, +) -> Result<Vec<FarmGcsLocation>, ReplicaSchemaError<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).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<FarmGcsLocation> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on( + exec: &dyn SqlExecutor, + on: &FarmGcsLocationQueryBindValues, +) -> Result<Option<FarmGcsLocation>, ReplicaSchemaError<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]).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<FarmGcsLocation> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id( + exec: &dyn SqlExecutor, + id: &str, +) -> Result<FarmGcsLocation, ReplicaSchemaError<SqlError>> { + let params_json = + utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<FarmGcsLocation> = utils::parse_json(&json)?; + rows.pop() + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update( + exec: &dyn SqlExecutor, + opts: &IFarmGcsLocationUpdate, +) -> Result<IFarmGcsLocationUpdateResolve, ReplicaSchemaError<SqlError>> { + let mut updates = + utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); + if updates.is_empty() { + return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(ReplicaSchemaResult { result: updated }) +} + +pub fn delete( + exec: &dyn SqlExecutor, + opts: &IFarmGcsLocationDelete, +) -> Result<IFarmGcsLocationDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( + args.on.lookup_key(), + )))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) + .expect("serialize bind params"); + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, &params_json)?; + if outcome.changes == 0 { + return Err(ReplicaSchemaError::from(SqlError::NotFound( + id_for_lookup.clone(), + ))); + } + Ok(ReplicaSchemaResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica_store/src/models/farm_member.rs b/crates/replica_store/src/models/farm_member.rs @@ -0,0 +1,159 @@ +use radroots_replica_schema::farm_member::{ + FarmMember, FarmMemberQueryBindValues, IFarmMemberCreate, IFarmMemberCreateResolve, + IFarmMemberDelete, IFarmMemberDeleteResolve, IFarmMemberFieldsFilter, IFarmMemberFindMany, + IFarmMemberFindManyResolve, IFarmMemberFindOne, IFarmMemberFindOneResolve, IFarmMemberUpdate, + IFarmMemberUpdateResolve, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "farm_member"; + +pub fn create( + exec: &dyn SqlExecutor, + opts: &IFarmMemberCreate, +) -> Result<IFarmMemberCreateResolve, ReplicaSchemaError<SqlError>> { + let field_map = utils::to_object_map(opts).expect("serialize object map"); + 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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let on = FarmMemberQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)? + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_one( + exec: &dyn SqlExecutor, + opts: &IFarmMemberFindOne, +) -> Result<IFarmMemberFindOneResolve, ReplicaSchemaError<SqlError>> { + let result = match opts { + IFarmMemberFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_many( + exec: &dyn SqlExecutor, + opts: &IFarmMemberFindMany, +) -> Result<IFarmMemberFindManyResolve, ReplicaSchemaError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(ReplicaSchemaResultList { results }) +} + +fn find_many_filter( + exec: &dyn SqlExecutor, + filter: &Option<IFarmMemberFieldsFilter>, +) -> Result<Vec<FarmMember>, ReplicaSchemaError<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).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<FarmMember> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on( + exec: &dyn SqlExecutor, + on: &FarmMemberQueryBindValues, +) -> Result<Option<FarmMember>, ReplicaSchemaError<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]).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<FarmMember> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id( + exec: &dyn SqlExecutor, + id: &str, +) -> Result<FarmMember, ReplicaSchemaError<SqlError>> { + let params_json = + utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<FarmMember> = utils::parse_json(&json)?; + rows.pop() + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update( + exec: &dyn SqlExecutor, + opts: &IFarmMemberUpdate, +) -> Result<IFarmMemberUpdateResolve, ReplicaSchemaError<SqlError>> { + let mut updates = + utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); + if updates.is_empty() { + return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(ReplicaSchemaResult { result: updated }) +} + +pub fn delete( + exec: &dyn SqlExecutor, + opts: &IFarmMemberDelete, +) -> Result<IFarmMemberDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( + args.on.lookup_key(), + )))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) + .expect("serialize bind params"); + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, &params_json)?; + if outcome.changes == 0 { + return Err(ReplicaSchemaError::from(SqlError::NotFound( + id_for_lookup.clone(), + ))); + } + Ok(ReplicaSchemaResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica_store/src/models/farm_member_claim.rs b/crates/replica_store/src/models/farm_member_claim.rs @@ -0,0 +1,160 @@ +use radroots_replica_schema::farm_member_claim::{ + FarmMemberClaim, FarmMemberClaimQueryBindValues, IFarmMemberClaimCreate, + IFarmMemberClaimCreateResolve, IFarmMemberClaimDelete, IFarmMemberClaimDeleteResolve, + IFarmMemberClaimFieldsFilter, IFarmMemberClaimFindMany, IFarmMemberClaimFindManyResolve, + IFarmMemberClaimFindOne, IFarmMemberClaimFindOneResolve, IFarmMemberClaimUpdate, + IFarmMemberClaimUpdateResolve, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "farm_member_claim"; + +pub fn create( + exec: &dyn SqlExecutor, + opts: &IFarmMemberClaimCreate, +) -> Result<IFarmMemberClaimCreateResolve, ReplicaSchemaError<SqlError>> { + let field_map = utils::to_object_map(opts).expect("serialize object map"); + 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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let on = FarmMemberClaimQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)? + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_one( + exec: &dyn SqlExecutor, + opts: &IFarmMemberClaimFindOne, +) -> Result<IFarmMemberClaimFindOneResolve, ReplicaSchemaError<SqlError>> { + let result = match opts { + IFarmMemberClaimFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_many( + exec: &dyn SqlExecutor, + opts: &IFarmMemberClaimFindMany, +) -> Result<IFarmMemberClaimFindManyResolve, ReplicaSchemaError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(ReplicaSchemaResultList { results }) +} + +fn find_many_filter( + exec: &dyn SqlExecutor, + filter: &Option<IFarmMemberClaimFieldsFilter>, +) -> Result<Vec<FarmMemberClaim>, ReplicaSchemaError<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).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<FarmMemberClaim> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on( + exec: &dyn SqlExecutor, + on: &FarmMemberClaimQueryBindValues, +) -> Result<Option<FarmMemberClaim>, ReplicaSchemaError<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]).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<FarmMemberClaim> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id( + exec: &dyn SqlExecutor, + id: &str, +) -> Result<FarmMemberClaim, ReplicaSchemaError<SqlError>> { + let params_json = + utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<FarmMemberClaim> = utils::parse_json(&json)?; + rows.pop() + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update( + exec: &dyn SqlExecutor, + opts: &IFarmMemberClaimUpdate, +) -> Result<IFarmMemberClaimUpdateResolve, ReplicaSchemaError<SqlError>> { + let mut updates = + utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); + if updates.is_empty() { + return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(ReplicaSchemaResult { result: updated }) +} + +pub fn delete( + exec: &dyn SqlExecutor, + opts: &IFarmMemberClaimDelete, +) -> Result<IFarmMemberClaimDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( + args.on.lookup_key(), + )))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) + .expect("serialize bind params"); + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, &params_json)?; + if outcome.changes == 0 { + return Err(ReplicaSchemaError::from(SqlError::NotFound( + id_for_lookup.clone(), + ))); + } + Ok(ReplicaSchemaResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica_store/src/models/farm_tag.rs b/crates/replica_store/src/models/farm_tag.rs @@ -0,0 +1,155 @@ +use radroots_replica_schema::farm_tag::{ + FarmTag, FarmTagQueryBindValues, IFarmTagCreate, IFarmTagCreateResolve, IFarmTagDelete, + IFarmTagDeleteResolve, IFarmTagFieldsFilter, IFarmTagFindMany, IFarmTagFindManyResolve, + IFarmTagFindOne, IFarmTagFindOneResolve, IFarmTagUpdate, IFarmTagUpdateResolve, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "farm_tag"; + +pub fn create( + exec: &dyn SqlExecutor, + opts: &IFarmTagCreate, +) -> Result<IFarmTagCreateResolve, ReplicaSchemaError<SqlError>> { + let field_map = utils::to_object_map(opts).expect("serialize object map"); + 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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let on = FarmTagQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)? + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_one( + exec: &dyn SqlExecutor, + opts: &IFarmTagFindOne, +) -> Result<IFarmTagFindOneResolve, ReplicaSchemaError<SqlError>> { + let result = match opts { + IFarmTagFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_many( + exec: &dyn SqlExecutor, + opts: &IFarmTagFindMany, +) -> Result<IFarmTagFindManyResolve, ReplicaSchemaError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(ReplicaSchemaResultList { results }) +} + +fn find_many_filter( + exec: &dyn SqlExecutor, + filter: &Option<IFarmTagFieldsFilter>, +) -> Result<Vec<FarmTag>, ReplicaSchemaError<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).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<FarmTag> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on( + exec: &dyn SqlExecutor, + on: &FarmTagQueryBindValues, +) -> Result<Option<FarmTag>, ReplicaSchemaError<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]).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<FarmTag> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id(exec: &dyn SqlExecutor, id: &str) -> Result<FarmTag, ReplicaSchemaError<SqlError>> { + let params_json = + utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<FarmTag> = utils::parse_json(&json)?; + rows.pop() + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update( + exec: &dyn SqlExecutor, + opts: &IFarmTagUpdate, +) -> Result<IFarmTagUpdateResolve, ReplicaSchemaError<SqlError>> { + let mut updates = + utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); + if updates.is_empty() { + return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(ReplicaSchemaResult { result: updated }) +} + +pub fn delete( + exec: &dyn SqlExecutor, + opts: &IFarmTagDelete, +) -> Result<IFarmTagDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( + args.on.lookup_key(), + )))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) + .expect("serialize bind params"); + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, &params_json)?; + if outcome.changes == 0 { + return Err(ReplicaSchemaError::from(SqlError::NotFound( + id_for_lookup.clone(), + ))); + } + Ok(ReplicaSchemaResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica_store/src/models/gcs_location.rs b/crates/replica_store/src/models/gcs_location.rs @@ -0,0 +1,238 @@ +use radroots_replica_schema::gcs_location::{ + GcsLocation, GcsLocationFindManyRel, GcsLocationQueryBindValues, IGcsLocationCreate, + IGcsLocationCreateResolve, IGcsLocationDelete, IGcsLocationDeleteResolve, + IGcsLocationFieldsFilter, IGcsLocationFindMany, IGcsLocationFindManyResolve, + IGcsLocationFindOne, IGcsLocationFindOneResolve, IGcsLocationUpdate, IGcsLocationUpdateResolve, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "gcs_location"; + +pub fn create( + exec: &dyn SqlExecutor, + opts: &IGcsLocationCreate, +) -> Result<IGcsLocationCreateResolve, ReplicaSchemaError<SqlError>> { + let field_map = utils::to_object_map(opts).expect("serialize object map"); + 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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let on = GcsLocationQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)? + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_one( + exec: &dyn SqlExecutor, + opts: &IGcsLocationFindOne, +) -> Result<IGcsLocationFindOneResolve, ReplicaSchemaError<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(ReplicaSchemaResult { result }) +} + +pub fn find_many( + exec: &dyn SqlExecutor, + opts: &IGcsLocationFindMany, +) -> Result<IGcsLocationFindManyResolve, ReplicaSchemaError<SqlError>> { + let results = match opts { + IGcsLocationFindMany::Filter { filter } => find_many_filter(exec, filter)?, + IGcsLocationFindMany::Rel { rel } => find_many_by_rel(exec, rel)?, + }; + Ok(ReplicaSchemaResultList { results }) +} + +fn find_many_filter( + exec: &dyn SqlExecutor, + filter: &Option<IGcsLocationFieldsFilter>, +) -> Result<Vec<GcsLocation>, ReplicaSchemaError<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).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<GcsLocation> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on( + exec: &dyn SqlExecutor, + on: &GcsLocationQueryBindValues, +) -> Result<Option<GcsLocation>, ReplicaSchemaError<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]).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_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( + exec: &dyn SqlExecutor, + rel: &GcsLocationFindManyRel, +) -> Result<Option<GcsLocation>, ReplicaSchemaError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); + let sql = format!("{sql} LIMIT 1;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<GcsLocation> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn find_many_by_rel( + exec: &dyn SqlExecutor, + rel: &GcsLocationFindManyRel, +) -> Result<Vec<GcsLocation>, ReplicaSchemaError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); + let sql = format!("{sql};"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<GcsLocation> = utils::parse_json(&json)?; + Ok(rows) +} + +fn select_by_id( + exec: &dyn SqlExecutor, + id: &str, +) -> Result<GcsLocation, ReplicaSchemaError<SqlError>> { + let params_json = + utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<GcsLocation> = utils::parse_json(&json)?; + rows.pop() + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update( + exec: &dyn SqlExecutor, + opts: &IGcsLocationUpdate, +) -> Result<IGcsLocationUpdateResolve, ReplicaSchemaError<SqlError>> { + let mut updates = + utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); + if updates.is_empty() { + return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(ReplicaSchemaResult { result: updated }) +} + +pub fn delete( + exec: &dyn SqlExecutor, + opts: &IGcsLocationDelete, +) -> Result<IGcsLocationDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::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(ReplicaSchemaError::from(SqlError::NotFound( + rel_lookup_key(&args.rel), + )))?; + model.id + } + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) + .expect("serialize bind params"); + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, &params_json)?; + if outcome.changes == 0 { + return Err(ReplicaSchemaError::from(SqlError::NotFound( + id_for_lookup.clone(), + ))); + } + Ok(ReplicaSchemaResult { + 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_store/src/models/log_error.rs b/crates/replica_store/src/models/log_error.rs @@ -0,0 +1,159 @@ +use radroots_replica_schema::log_error::{ + ILogErrorCreate, ILogErrorCreateResolve, ILogErrorDelete, ILogErrorDeleteResolve, + ILogErrorFieldsFilter, ILogErrorFindMany, ILogErrorFindManyResolve, ILogErrorFindOne, + ILogErrorFindOneResolve, ILogErrorUpdate, ILogErrorUpdateResolve, LogError, + LogErrorQueryBindValues, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "log_error"; + +pub fn create( + exec: &dyn SqlExecutor, + opts: &ILogErrorCreate, +) -> Result<ILogErrorCreateResolve, ReplicaSchemaError<SqlError>> { + let field_map = utils::to_object_map(opts).expect("serialize object map"); + 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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let on = LogErrorQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)? + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_one( + exec: &dyn SqlExecutor, + opts: &ILogErrorFindOne, +) -> Result<ILogErrorFindOneResolve, ReplicaSchemaError<SqlError>> { + let result = match opts { + ILogErrorFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_many( + exec: &dyn SqlExecutor, + opts: &ILogErrorFindMany, +) -> Result<ILogErrorFindManyResolve, ReplicaSchemaError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(ReplicaSchemaResultList { results }) +} + +fn find_many_filter( + exec: &dyn SqlExecutor, + filter: &Option<ILogErrorFieldsFilter>, +) -> Result<Vec<LogError>, ReplicaSchemaError<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).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<LogError> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on( + exec: &dyn SqlExecutor, + on: &LogErrorQueryBindValues, +) -> Result<Option<LogError>, ReplicaSchemaError<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]).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<LogError> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id( + exec: &dyn SqlExecutor, + id: &str, +) -> Result<LogError, ReplicaSchemaError<SqlError>> { + let params_json = + utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<LogError> = utils::parse_json(&json)?; + rows.pop() + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update( + exec: &dyn SqlExecutor, + opts: &ILogErrorUpdate, +) -> Result<ILogErrorUpdateResolve, ReplicaSchemaError<SqlError>> { + let mut updates = + utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); + if updates.is_empty() { + return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(ReplicaSchemaResult { result: updated }) +} + +pub fn delete( + exec: &dyn SqlExecutor, + opts: &ILogErrorDelete, +) -> Result<ILogErrorDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( + args.on.lookup_key(), + )))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) + .expect("serialize bind params"); + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, &params_json)?; + if outcome.changes == 0 { + return Err(ReplicaSchemaError::from(SqlError::NotFound( + id_for_lookup.clone(), + ))); + } + Ok(ReplicaSchemaResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica_store/src/models/media_image.rs b/crates/replica_store/src/models/media_image.rs @@ -0,0 +1,218 @@ +use radroots_replica_schema::media_image::{ + IMediaImageCreate, IMediaImageCreateResolve, IMediaImageDelete, IMediaImageDeleteResolve, + IMediaImageFieldsFilter, IMediaImageFindMany, IMediaImageFindManyResolve, IMediaImageFindOne, + IMediaImageFindOneResolve, IMediaImageUpdate, IMediaImageUpdateResolve, MediaImage, + MediaImageFindManyRel, MediaImageQueryBindValues, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "media_image"; + +pub fn create( + exec: &dyn SqlExecutor, + opts: &IMediaImageCreate, +) -> Result<IMediaImageCreateResolve, ReplicaSchemaError<SqlError>> { + let field_map = utils::to_object_map(opts).expect("serialize object map"); + 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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let on = MediaImageQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)? + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_one( + exec: &dyn SqlExecutor, + opts: &IMediaImageFindOne, +) -> Result<IMediaImageFindOneResolve, ReplicaSchemaError<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(ReplicaSchemaResult { result }) +} + +pub fn find_many( + exec: &dyn SqlExecutor, + opts: &IMediaImageFindMany, +) -> Result<IMediaImageFindManyResolve, ReplicaSchemaError<SqlError>> { + let results = match opts { + IMediaImageFindMany::Filter { filter } => find_many_filter(exec, filter)?, + IMediaImageFindMany::Rel { rel } => find_many_by_rel(exec, rel)?, + }; + Ok(ReplicaSchemaResultList { results }) +} + +fn find_many_filter( + exec: &dyn SqlExecutor, + filter: &Option<IMediaImageFieldsFilter>, +) -> Result<Vec<MediaImage>, ReplicaSchemaError<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).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<MediaImage> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on( + exec: &dyn SqlExecutor, + on: &MediaImageQueryBindValues, +) -> Result<Option<MediaImage>, ReplicaSchemaError<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]).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_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( + exec: &dyn SqlExecutor, + rel: &MediaImageFindManyRel, +) -> Result<Option<MediaImage>, ReplicaSchemaError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); + let sql = format!("{sql} LIMIT 1;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<MediaImage> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn find_many_by_rel( + exec: &dyn SqlExecutor, + rel: &MediaImageFindManyRel, +) -> Result<Vec<MediaImage>, ReplicaSchemaError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); + let sql = format!("{sql};"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<MediaImage> = utils::parse_json(&json)?; + Ok(rows) +} + +fn select_by_id( + exec: &dyn SqlExecutor, + id: &str, +) -> Result<MediaImage, ReplicaSchemaError<SqlError>> { + let params_json = + utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<MediaImage> = utils::parse_json(&json)?; + rows.pop() + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update( + exec: &dyn SqlExecutor, + opts: &IMediaImageUpdate, +) -> Result<IMediaImageUpdateResolve, ReplicaSchemaError<SqlError>> { + let mut updates = + utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); + if updates.is_empty() { + return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(ReplicaSchemaResult { result: updated }) +} + +pub fn delete( + exec: &dyn SqlExecutor, + opts: &IMediaImageDelete, +) -> Result<IMediaImageDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::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(ReplicaSchemaError::from(SqlError::NotFound( + rel_lookup_key(&args.rel), + )))?; + model.id + } + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) + .expect("serialize bind params"); + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, &params_json)?; + if outcome.changes == 0 { + return Err(ReplicaSchemaError::from(SqlError::NotFound( + id_for_lookup.clone(), + ))); + } + Ok(ReplicaSchemaResult { + 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/replica_db/src/models/mod.rs b/crates/replica_store/src/models/mod.rs diff --git a/crates/replica_store/src/models/nostr_event_head.rs b/crates/replica_store/src/models/nostr_event_head.rs @@ -0,0 +1,160 @@ +use radroots_replica_schema::nostr_event_head::{ + INostrEventHeadCreate, INostrEventHeadCreateResolve, INostrEventHeadDelete, + INostrEventHeadDeleteResolve, INostrEventHeadFieldsFilter, INostrEventHeadFindMany, + INostrEventHeadFindManyResolve, INostrEventHeadFindOne, INostrEventHeadFindOneResolve, + INostrEventHeadUpdate, INostrEventHeadUpdateResolve, NostrEventHead, + NostrEventHeadQueryBindValues, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "nostr_event_head"; + +pub fn create( + exec: &dyn SqlExecutor, + opts: &INostrEventHeadCreate, +) -> Result<INostrEventHeadCreateResolve, ReplicaSchemaError<SqlError>> { + let field_map = utils::to_object_map(opts).expect("serialize object map"); + 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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let on = NostrEventHeadQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)? + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_one( + exec: &dyn SqlExecutor, + opts: &INostrEventHeadFindOne, +) -> Result<INostrEventHeadFindOneResolve, ReplicaSchemaError<SqlError>> { + let result = match opts { + INostrEventHeadFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_many( + exec: &dyn SqlExecutor, + opts: &INostrEventHeadFindMany, +) -> Result<INostrEventHeadFindManyResolve, ReplicaSchemaError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(ReplicaSchemaResultList { results }) +} + +fn find_many_filter( + exec: &dyn SqlExecutor, + filter: &Option<INostrEventHeadFieldsFilter>, +) -> Result<Vec<NostrEventHead>, ReplicaSchemaError<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).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<NostrEventHead> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on( + exec: &dyn SqlExecutor, + on: &NostrEventHeadQueryBindValues, +) -> Result<Option<NostrEventHead>, ReplicaSchemaError<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]).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<NostrEventHead> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id( + exec: &dyn SqlExecutor, + id: &str, +) -> Result<NostrEventHead, ReplicaSchemaError<SqlError>> { + let params_json = + utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<NostrEventHead> = utils::parse_json(&json)?; + rows.pop() + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update( + exec: &dyn SqlExecutor, + opts: &INostrEventHeadUpdate, +) -> Result<INostrEventHeadUpdateResolve, ReplicaSchemaError<SqlError>> { + let mut updates = + utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); + if updates.is_empty() { + return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(ReplicaSchemaResult { result: updated }) +} + +pub fn delete( + exec: &dyn SqlExecutor, + opts: &INostrEventHeadDelete, +) -> Result<INostrEventHeadDeleteResolve, ReplicaSchemaError<SqlError>> { + let id_for_lookup = match opts { + INostrEventHeadDelete::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(ReplicaSchemaError::from(SqlError::NotFound( + args.on.lookup_key(), + )))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) + .expect("serialize bind params"); + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, &params_json)?; + if outcome.changes == 0 { + return Err(ReplicaSchemaError::from(SqlError::NotFound( + id_for_lookup.clone(), + ))); + } + Ok(ReplicaSchemaResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica_store/src/models/nostr_profile.rs b/crates/replica_store/src/models/nostr_profile.rs @@ -0,0 +1,215 @@ +use radroots_replica_schema::nostr_profile::{ + INostrProfileCreate, INostrProfileCreateResolve, INostrProfileDelete, + INostrProfileDeleteResolve, INostrProfileFieldsFilter, INostrProfileFindMany, + INostrProfileFindManyResolve, INostrProfileFindOne, INostrProfileFindOneResolve, + INostrProfileUpdate, INostrProfileUpdateResolve, NostrProfile, NostrProfileFindManyRel, + NostrProfileQueryBindValues, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "nostr_profile"; + +pub fn create( + exec: &dyn SqlExecutor, + opts: &INostrProfileCreate, +) -> Result<INostrProfileCreateResolve, ReplicaSchemaError<SqlError>> { + let field_map = utils::to_object_map(opts).expect("serialize object map"); + 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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let on = NostrProfileQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)? + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_one( + exec: &dyn SqlExecutor, + opts: &INostrProfileFindOne, +) -> Result<INostrProfileFindOneResolve, ReplicaSchemaError<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(ReplicaSchemaResult { result }) +} + +pub fn find_many( + exec: &dyn SqlExecutor, + opts: &INostrProfileFindMany, +) -> Result<INostrProfileFindManyResolve, ReplicaSchemaError<SqlError>> { + let results = match opts { + INostrProfileFindMany::Filter { filter } => find_many_filter(exec, filter)?, + INostrProfileFindMany::Rel { rel } => find_many_by_rel(exec, rel)?, + }; + Ok(ReplicaSchemaResultList { results }) +} + +fn find_many_filter( + exec: &dyn SqlExecutor, + filter: &Option<INostrProfileFieldsFilter>, +) -> Result<Vec<NostrProfile>, ReplicaSchemaError<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).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<NostrProfile> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on( + exec: &dyn SqlExecutor, + on: &NostrProfileQueryBindValues, +) -> Result<Option<NostrProfile>, ReplicaSchemaError<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]).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_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( + exec: &dyn SqlExecutor, + rel: &NostrProfileFindManyRel, +) -> Result<Option<NostrProfile>, ReplicaSchemaError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); + let sql = format!("{sql} LIMIT 1;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<NostrProfile> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn find_many_by_rel( + exec: &dyn SqlExecutor, + rel: &NostrProfileFindManyRel, +) -> Result<Vec<NostrProfile>, ReplicaSchemaError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); + let sql = format!("{sql};"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<NostrProfile> = utils::parse_json(&json)?; + Ok(rows) +} + +fn select_by_id( + exec: &dyn SqlExecutor, + id: &str, +) -> Result<NostrProfile, ReplicaSchemaError<SqlError>> { + let params_json = + utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<NostrProfile> = utils::parse_json(&json)?; + rows.pop() + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update( + exec: &dyn SqlExecutor, + opts: &INostrProfileUpdate, +) -> Result<INostrProfileUpdateResolve, ReplicaSchemaError<SqlError>> { + let mut updates = + utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); + if updates.is_empty() { + return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(ReplicaSchemaResult { result: updated }) +} + +pub fn delete( + exec: &dyn SqlExecutor, + opts: &INostrProfileDelete, +) -> Result<INostrProfileDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::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(ReplicaSchemaError::from(SqlError::NotFound( + rel_lookup_key(&args.rel), + )))?; + model.id + } + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) + .expect("serialize bind params"); + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, &params_json)?; + if outcome.changes == 0 { + return Err(ReplicaSchemaError::from(SqlError::NotFound( + id_for_lookup.clone(), + ))); + } + Ok(ReplicaSchemaResult { + 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_store/src/models/nostr_profile_relay.rs b/crates/replica_store/src/models/nostr_profile_relay.rs @@ -0,0 +1,45 @@ +use radroots_replica_schema::nostr_profile_relay::{ + INostrProfileRelayRelation, INostrProfileRelayResolve, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResultPass}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "nostr_profile_relay"; + +pub fn set( + exec: &dyn SqlExecutor, + opts: &INostrProfileRelayRelation, +) -> Result<INostrProfileRelayResolve, ReplicaSchemaError<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).expect("serialize bind params"); + let _ = exec.exec(&query, &params_json)?; + Ok(ReplicaSchemaResultPass { pass: true }) +} + +pub fn unset( + exec: &dyn SqlExecutor, + opts: &INostrProfileRelayRelation, +) -> Result<INostrProfileRelayResolve, ReplicaSchemaError<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).expect("serialize bind params"); + let _ = exec.exec(&query, &params_json)?; + Ok(ReplicaSchemaResultPass { pass: true }) +} diff --git a/crates/replica_store/src/models/nostr_relay.rs b/crates/replica_store/src/models/nostr_relay.rs @@ -0,0 +1,218 @@ +use radroots_replica_schema::nostr_relay::{ + INostrRelayCreate, INostrRelayCreateResolve, INostrRelayDelete, INostrRelayDeleteResolve, + INostrRelayFieldsFilter, INostrRelayFindMany, INostrRelayFindManyResolve, INostrRelayFindOne, + INostrRelayFindOneResolve, INostrRelayUpdate, INostrRelayUpdateResolve, NostrRelay, + NostrRelayFindManyRel, NostrRelayQueryBindValues, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "nostr_relay"; + +pub fn create( + exec: &dyn SqlExecutor, + opts: &INostrRelayCreate, +) -> Result<INostrRelayCreateResolve, ReplicaSchemaError<SqlError>> { + let field_map = utils::to_object_map(opts).expect("serialize object map"); + 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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let on = NostrRelayQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)? + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_one( + exec: &dyn SqlExecutor, + opts: &INostrRelayFindOne, +) -> Result<INostrRelayFindOneResolve, ReplicaSchemaError<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(ReplicaSchemaResult { result }) +} + +pub fn find_many( + exec: &dyn SqlExecutor, + opts: &INostrRelayFindMany, +) -> Result<INostrRelayFindManyResolve, ReplicaSchemaError<SqlError>> { + let results = match opts { + INostrRelayFindMany::Filter { filter } => find_many_filter(exec, filter)?, + INostrRelayFindMany::Rel { rel } => find_many_by_rel(exec, rel)?, + }; + Ok(ReplicaSchemaResultList { results }) +} + +fn find_many_filter( + exec: &dyn SqlExecutor, + filter: &Option<INostrRelayFieldsFilter>, +) -> Result<Vec<NostrRelay>, ReplicaSchemaError<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).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<NostrRelay> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on( + exec: &dyn SqlExecutor, + on: &NostrRelayQueryBindValues, +) -> Result<Option<NostrRelay>, ReplicaSchemaError<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]).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_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( + exec: &dyn SqlExecutor, + rel: &NostrRelayFindManyRel, +) -> Result<Option<NostrRelay>, ReplicaSchemaError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); + let sql = format!("{sql} LIMIT 1;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<NostrRelay> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn find_many_by_rel( + exec: &dyn SqlExecutor, + rel: &NostrRelayFindManyRel, +) -> Result<Vec<NostrRelay>, ReplicaSchemaError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); + let sql = format!("{sql};"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<NostrRelay> = utils::parse_json(&json)?; + Ok(rows) +} + +fn select_by_id( + exec: &dyn SqlExecutor, + id: &str, +) -> Result<NostrRelay, ReplicaSchemaError<SqlError>> { + let params_json = + utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<NostrRelay> = utils::parse_json(&json)?; + rows.pop() + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update( + exec: &dyn SqlExecutor, + opts: &INostrRelayUpdate, +) -> Result<INostrRelayUpdateResolve, ReplicaSchemaError<SqlError>> { + let mut updates = + utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); + if updates.is_empty() { + return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(ReplicaSchemaResult { result: updated }) +} + +pub fn delete( + exec: &dyn SqlExecutor, + opts: &INostrRelayDelete, +) -> Result<INostrRelayDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::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(ReplicaSchemaError::from(SqlError::NotFound( + rel_lookup_key(&args.rel), + )))?; + model.id + } + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) + .expect("serialize bind params"); + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, &params_json)?; + if outcome.changes == 0 { + return Err(ReplicaSchemaError::from(SqlError::NotFound( + id_for_lookup.clone(), + ))); + } + Ok(ReplicaSchemaResult { + 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_store/src/models/plot.rs b/crates/replica_store/src/models/plot.rs @@ -0,0 +1,155 @@ +use radroots_replica_schema::plot::{ + IPlotCreate, IPlotCreateResolve, IPlotDelete, IPlotDeleteResolve, IPlotFieldsFilter, + IPlotFindMany, IPlotFindManyResolve, IPlotFindOne, IPlotFindOneResolve, IPlotUpdate, + IPlotUpdateResolve, Plot, PlotQueryBindValues, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "plot"; + +pub fn create( + exec: &dyn SqlExecutor, + opts: &IPlotCreate, +) -> Result<IPlotCreateResolve, ReplicaSchemaError<SqlError>> { + let field_map = utils::to_object_map(opts).expect("serialize object map"); + 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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let on = PlotQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)? + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_one( + exec: &dyn SqlExecutor, + opts: &IPlotFindOne, +) -> Result<IPlotFindOneResolve, ReplicaSchemaError<SqlError>> { + let result = match opts { + IPlotFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_many( + exec: &dyn SqlExecutor, + opts: &IPlotFindMany, +) -> Result<IPlotFindManyResolve, ReplicaSchemaError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(ReplicaSchemaResultList { results }) +} + +fn find_many_filter( + exec: &dyn SqlExecutor, + filter: &Option<IPlotFieldsFilter>, +) -> Result<Vec<Plot>, ReplicaSchemaError<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).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<Plot> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on( + exec: &dyn SqlExecutor, + on: &PlotQueryBindValues, +) -> Result<Option<Plot>, ReplicaSchemaError<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]).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<Plot> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id(exec: &dyn SqlExecutor, id: &str) -> Result<Plot, ReplicaSchemaError<SqlError>> { + let params_json = + utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<Plot> = utils::parse_json(&json)?; + rows.pop() + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update( + exec: &dyn SqlExecutor, + opts: &IPlotUpdate, +) -> Result<IPlotUpdateResolve, ReplicaSchemaError<SqlError>> { + let mut updates = + utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); + if updates.is_empty() { + return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(ReplicaSchemaResult { result: updated }) +} + +pub fn delete( + exec: &dyn SqlExecutor, + opts: &IPlotDelete, +) -> Result<IPlotDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( + args.on.lookup_key(), + )))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) + .expect("serialize bind params"); + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, &params_json)?; + if outcome.changes == 0 { + return Err(ReplicaSchemaError::from(SqlError::NotFound( + id_for_lookup.clone(), + ))); + } + Ok(ReplicaSchemaResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica_store/src/models/plot_gcs_location.rs b/crates/replica_store/src/models/plot_gcs_location.rs @@ -0,0 +1,160 @@ +use radroots_replica_schema::plot_gcs_location::{ + IPlotGcsLocationCreate, IPlotGcsLocationCreateResolve, IPlotGcsLocationDelete, + IPlotGcsLocationDeleteResolve, IPlotGcsLocationFieldsFilter, IPlotGcsLocationFindMany, + IPlotGcsLocationFindManyResolve, IPlotGcsLocationFindOne, IPlotGcsLocationFindOneResolve, + IPlotGcsLocationUpdate, IPlotGcsLocationUpdateResolve, PlotGcsLocation, + PlotGcsLocationQueryBindValues, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "plot_gcs_location"; + +pub fn create( + exec: &dyn SqlExecutor, + opts: &IPlotGcsLocationCreate, +) -> Result<IPlotGcsLocationCreateResolve, ReplicaSchemaError<SqlError>> { + let field_map = utils::to_object_map(opts).expect("serialize object map"); + 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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let on = PlotGcsLocationQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)? + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_one( + exec: &dyn SqlExecutor, + opts: &IPlotGcsLocationFindOne, +) -> Result<IPlotGcsLocationFindOneResolve, ReplicaSchemaError<SqlError>> { + let result = match opts { + IPlotGcsLocationFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_many( + exec: &dyn SqlExecutor, + opts: &IPlotGcsLocationFindMany, +) -> Result<IPlotGcsLocationFindManyResolve, ReplicaSchemaError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(ReplicaSchemaResultList { results }) +} + +fn find_many_filter( + exec: &dyn SqlExecutor, + filter: &Option<IPlotGcsLocationFieldsFilter>, +) -> Result<Vec<PlotGcsLocation>, ReplicaSchemaError<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).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<PlotGcsLocation> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on( + exec: &dyn SqlExecutor, + on: &PlotGcsLocationQueryBindValues, +) -> Result<Option<PlotGcsLocation>, ReplicaSchemaError<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]).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<PlotGcsLocation> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id( + exec: &dyn SqlExecutor, + id: &str, +) -> Result<PlotGcsLocation, ReplicaSchemaError<SqlError>> { + let params_json = + utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<PlotGcsLocation> = utils::parse_json(&json)?; + rows.pop() + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update( + exec: &dyn SqlExecutor, + opts: &IPlotGcsLocationUpdate, +) -> Result<IPlotGcsLocationUpdateResolve, ReplicaSchemaError<SqlError>> { + let mut updates = + utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); + if updates.is_empty() { + return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(ReplicaSchemaResult { result: updated }) +} + +pub fn delete( + exec: &dyn SqlExecutor, + opts: &IPlotGcsLocationDelete, +) -> Result<IPlotGcsLocationDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( + args.on.lookup_key(), + )))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) + .expect("serialize bind params"); + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, &params_json)?; + if outcome.changes == 0 { + return Err(ReplicaSchemaError::from(SqlError::NotFound( + id_for_lookup.clone(), + ))); + } + Ok(ReplicaSchemaResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica_store/src/models/plot_tag.rs b/crates/replica_store/src/models/plot_tag.rs @@ -0,0 +1,155 @@ +use radroots_replica_schema::plot_tag::{ + IPlotTagCreate, IPlotTagCreateResolve, IPlotTagDelete, IPlotTagDeleteResolve, + IPlotTagFieldsFilter, IPlotTagFindMany, IPlotTagFindManyResolve, IPlotTagFindOne, + IPlotTagFindOneResolve, IPlotTagUpdate, IPlotTagUpdateResolve, PlotTag, PlotTagQueryBindValues, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "plot_tag"; + +pub fn create( + exec: &dyn SqlExecutor, + opts: &IPlotTagCreate, +) -> Result<IPlotTagCreateResolve, ReplicaSchemaError<SqlError>> { + let field_map = utils::to_object_map(opts).expect("serialize object map"); + 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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let on = PlotTagQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)? + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_one( + exec: &dyn SqlExecutor, + opts: &IPlotTagFindOne, +) -> Result<IPlotTagFindOneResolve, ReplicaSchemaError<SqlError>> { + let result = match opts { + IPlotTagFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_many( + exec: &dyn SqlExecutor, + opts: &IPlotTagFindMany, +) -> Result<IPlotTagFindManyResolve, ReplicaSchemaError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(ReplicaSchemaResultList { results }) +} + +fn find_many_filter( + exec: &dyn SqlExecutor, + filter: &Option<IPlotTagFieldsFilter>, +) -> Result<Vec<PlotTag>, ReplicaSchemaError<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).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<PlotTag> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on( + exec: &dyn SqlExecutor, + on: &PlotTagQueryBindValues, +) -> Result<Option<PlotTag>, ReplicaSchemaError<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]).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<PlotTag> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id(exec: &dyn SqlExecutor, id: &str) -> Result<PlotTag, ReplicaSchemaError<SqlError>> { + let params_json = + utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<PlotTag> = utils::parse_json(&json)?; + rows.pop() + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update( + exec: &dyn SqlExecutor, + opts: &IPlotTagUpdate, +) -> Result<IPlotTagUpdateResolve, ReplicaSchemaError<SqlError>> { + let mut updates = + utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); + if updates.is_empty() { + return Err(ReplicaSchemaError::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(ReplicaSchemaError::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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(ReplicaSchemaResult { result: updated }) +} + +pub fn delete( + exec: &dyn SqlExecutor, + opts: &IPlotTagDelete, +) -> Result<IPlotTagDeleteResolve, ReplicaSchemaError<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(ReplicaSchemaError::from(SqlError::NotFound( + args.on.lookup_key(), + )))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) + .expect("serialize bind params"); + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, &params_json)?; + if outcome.changes == 0 { + return Err(ReplicaSchemaError::from(SqlError::NotFound( + id_for_lookup.clone(), + ))); + } + Ok(ReplicaSchemaResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica_store/src/models/trade_product.rs b/crates/replica_store/src/models/trade_product.rs @@ -0,0 +1,145 @@ +use radroots_replica_schema::trade_product::{ + ITradeProductCreate, ITradeProductCreateResolve, ITradeProductDelete, + ITradeProductDeleteResolve, ITradeProductFieldsFilter, ITradeProductFindMany, + ITradeProductFindManyResolve, ITradeProductFindOne, ITradeProductFindOneResolve, + ITradeProductUpdate, ITradeProductUpdateResolve, TradeProduct, TradeProductQueryBindValues, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "trade_product"; + +pub fn create( + exec: &dyn SqlExecutor, + opts: &ITradeProductCreate, +) -> Result<ITradeProductCreateResolve, ReplicaSchemaError<SqlError>> { + let field_map = utils::to_object_map(opts).expect("serialize object map"); + 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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let on = TradeProductQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)? + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.clone())))?; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_one( + exec: &dyn SqlExecutor, + opts: &ITradeProductFindOne, +) -> Result<ITradeProductFindOneResolve, ReplicaSchemaError<SqlError>> { + let result = match opts { + ITradeProductFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(ReplicaSchemaResult { result }) +} + +pub fn find_many( + exec: &dyn SqlExecutor, + opts: &ITradeProductFindMany, +) -> Result<ITradeProductFindManyResolve, ReplicaSchemaError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(ReplicaSchemaResultList { results }) +} + +fn find_many_filter( + exec: &dyn SqlExecutor, + filter: &Option<ITradeProductFieldsFilter>, +) -> Result<Vec<TradeProduct>, ReplicaSchemaError<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).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let rows: Vec<TradeProduct> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on( + exec: &dyn SqlExecutor, + on: &TradeProductQueryBindValues, +) -> Result<Option<TradeProduct>, ReplicaSchemaError<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]).expect("serialize bind params"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<TradeProduct> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id( + exec: &dyn SqlExecutor, + id: &str, +) -> Result<TradeProduct, ReplicaSchemaError<SqlError>> { + let params_json = + utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<TradeProduct> = utils::parse_json(&json)?; + rows.pop() + .ok_or(ReplicaSchemaError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update( + exec: &dyn SqlExecutor, + opts: &ITradeProductUpdate, +) -> Result<ITradeProductUpdateResolve, ReplicaSchemaError<SqlError>> { + let mut updates = + utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); + if updates.is_empty() { + return Err(ReplicaSchemaError::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).expect("serialize bind params"); + let _ = exec.exec(&sql, &params_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(ReplicaSchemaResult { result: updated }) +} + +pub fn delete( + exec: &dyn SqlExecutor, + opts: &ITradeProductDelete, +) -> Result<ITradeProductDeleteResolve, ReplicaSchemaError<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())]) + .expect("serialize bind params"); + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, &params_json)?; + if outcome.changes == 0 { + return Err(ReplicaSchemaError::from(SqlError::NotFound( + id_for_lookup.clone(), + ))); + } + Ok(ReplicaSchemaResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica_store/src/models/trade_product_location.rs b/crates/replica_store/src/models/trade_product_location.rs @@ -0,0 +1,45 @@ +use radroots_replica_schema::trade_product_location::{ + ITradeProductLocationRelation, ITradeProductLocationResolve, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResultPass}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "trade_product_location"; + +pub fn set( + exec: &dyn SqlExecutor, + opts: &ITradeProductLocationRelation, +) -> Result<ITradeProductLocationResolve, ReplicaSchemaError<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).expect("serialize bind params"); + let _ = exec.exec(&query, &params_json)?; + Ok(ReplicaSchemaResultPass { pass: true }) +} + +pub fn unset( + exec: &dyn SqlExecutor, + opts: &ITradeProductLocationRelation, +) -> Result<ITradeProductLocationResolve, ReplicaSchemaError<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).expect("serialize bind params"); + let _ = exec.exec(&query, &params_json)?; + Ok(ReplicaSchemaResultPass { pass: true }) +} diff --git a/crates/replica_store/src/models/trade_product_media.rs b/crates/replica_store/src/models/trade_product_media.rs @@ -0,0 +1,45 @@ +use radroots_replica_schema::trade_product_media::{ + ITradeProductMediaRelation, ITradeProductMediaResolve, +}; +use radroots_replica_schema::{ReplicaSchemaError, ReplicaSchemaResultPass}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use serde_json::Value; + +const TABLE_NAME: &str = "trade_product_media"; + +pub fn set( + exec: &dyn SqlExecutor, + opts: &ITradeProductMediaRelation, +) -> Result<ITradeProductMediaResolve, ReplicaSchemaError<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).expect("serialize bind params"); + let _ = exec.exec(&query, &params_json)?; + Ok(ReplicaSchemaResultPass { pass: true }) +} + +pub fn unset( + exec: &dyn SqlExecutor, + opts: &ITradeProductMediaRelation, +) -> Result<ITradeProductMediaResolve, ReplicaSchemaError<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).expect("serialize bind params"); + let _ = exec.exec(&query, &params_json)?; + Ok(ReplicaSchemaResultPass { pass: true }) +} diff --git a/crates/replica_db/src/query.rs b/crates/replica_store/src/query.rs diff --git a/crates/replica_store/tests/backup_export_paths.rs b/crates/replica_store/tests/backup_export_paths.rs @@ -0,0 +1,305 @@ +use std::collections::VecDeque; +use std::sync::Mutex; + +use radroots_replica_store::backup::{ + DATABASE_BACKUP_VERSION, DatabaseBackup, REPLICA_STORE_VERSION, SchemaEntry, TableData, + export_database_backup, export_database_backup_json, restore_database_backup, +}; +use radroots_replica_store::export::export_manifest; +use radroots_replica_store::{ExecOutcome, SqlError, SqlExecutor}; +use serde_json::{Map, Value, json}; + +struct PatternExecutor { + query_rules: Vec<(String, String)>, + fail_query_contains: Option<String>, + fail_exec_contains: Option<String>, + fail_begin: bool, + fail_commit: bool, + begin_count: Mutex<usize>, + commit_count: Mutex<usize>, + rollback_count: Mutex<usize>, + query_queue: Mutex<VecDeque<Result<String, SqlError>>>, + exec_queue: Mutex<VecDeque<Result<ExecOutcome, SqlError>>>, +} + +impl PatternExecutor { + fn new() -> Self { + Self { + query_rules: Vec::new(), + fail_query_contains: None, + fail_exec_contains: None, + fail_begin: false, + fail_commit: false, + begin_count: Mutex::new(0), + commit_count: Mutex::new(0), + rollback_count: Mutex::new(0), + query_queue: Mutex::new(VecDeque::new()), + exec_queue: Mutex::new(VecDeque::new()), + } + } + + fn with_query_rule(mut self, needle: &str, response: &str) -> Self { + self.query_rules + .push((needle.to_string(), response.to_string())); + self + } + + fn with_query_failure(mut self, needle: &str) -> Self { + self.fail_query_contains = Some(needle.to_string()); + self + } + + fn with_exec_failure(mut self, needle: &str) -> Self { + self.fail_exec_contains = Some(needle.to_string()); + self + } + + fn with_begin_failure(mut self) -> Self { + self.fail_begin = true; + self + } + + fn with_commit_failure(mut self) -> Self { + self.fail_commit = true; + self + } +} + +impl SqlExecutor for PatternExecutor { + fn exec(&self, sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> { + if let Some(result) = self.exec_queue.lock().expect("exec queue lock").pop_front() { + return result; + } + if let Some(needle) = &self.fail_exec_contains + && 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> { + if let Some(result) = self + .query_queue + .lock() + .expect("query queue lock") + .pop_front() + { + return result; + } + if let Some(needle) = &self.fail_query_contains + && sql.contains(needle) + { + return Err(SqlError::InvalidQuery(String::from("forced query failure"))); + } + 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_count.lock().expect("begin count lock") += 1; + if self.fail_begin { + return Err(SqlError::InvalidQuery(String::from("forced begin failure"))); + } + Ok(()) + } + + fn commit(&self) -> Result<(), SqlError> { + *self.commit_count.lock().expect("commit count lock") += 1; + if self.fail_commit { + return Err(SqlError::InvalidQuery(String::from( + "forced commit failure", + ))); + } + Ok(()) + } + + fn rollback(&self) -> Result<(), SqlError> { + *self.rollback_count.lock().expect("rollback count lock") += 1; + Ok(()) + } +} + +fn assert_sql_error_code<T: core::fmt::Debug>(result: Result<T, SqlError>, code: &str) { + let err = result.unwrap_err(); + assert_eq!(err.code(), code); +} + +fn backup_with_versions(format_version: &str, replica_store_version: &str) -> DatabaseBackup { + DatabaseBackup { + format_version: format_version.to_string(), + replica_store_version: replica_store_version.to_string(), + schema: Vec::new(), + migrations: Vec::new(), + data: Vec::new(), + } +} + +#[test] +fn backup_public_api_error_paths_cover_library_instantiations() { + let schema_query = "select type, name, tbl_name as table_name, sql from sqlite_master"; + + let executor = PatternExecutor::new().with_query_failure(schema_query); + assert_sql_error_code(export_database_backup(&executor), "ERR_INVALID_QUERY"); + + let schema_rows = json!([ + { + "type": "table", + "name": "tb_a", + "table_name": "tb_a", + "sql": "CREATE TABLE tb_a (id TEXT);" + } + ]) + .to_string(); + let executor = PatternExecutor::new() + .with_query_rule(schema_query, &schema_rows) + .with_query_failure("SELECT * FROM \"tb_a\";"); + assert_sql_error_code(export_database_backup(&executor), "ERR_INVALID_QUERY"); + + let executor = PatternExecutor::new().with_query_failure(schema_query); + assert_sql_error_code(export_database_backup_json(&executor), "ERR_INVALID_QUERY"); + + let executor = PatternExecutor::new().with_query_rule(schema_query, "[]"); + let backup_json = export_database_backup_json(&executor).expect("backup json success"); + assert!(backup_json.contains("\"schema\":[]")); + + let executor = PatternExecutor::new(); + let backup = backup_with_versions("0.0.1", REPLICA_STORE_VERSION); + assert_sql_error_code( + restore_database_backup(&executor, &backup), + "ERR_INVALID_ARGUMENT", + ); + + let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_VERSION); + let executor = PatternExecutor::new().with_exec_failure("PRAGMA foreign_keys = OFF;"); + assert_sql_error_code( + restore_database_backup(&executor, &backup), + "ERR_INVALID_QUERY", + ); + + let executor = PatternExecutor::new().with_begin_failure(); + assert_sql_error_code( + restore_database_backup(&executor, &backup), + "ERR_INVALID_QUERY", + ); + + let executor = + PatternExecutor::new().with_query_failure("select type, name from sqlite_master"); + assert_sql_error_code( + restore_database_backup(&executor, &backup), + "ERR_INVALID_QUERY", + ); + + let executor = PatternExecutor::new().with_commit_failure(); + assert_sql_error_code( + restore_database_backup(&executor, &backup), + "ERR_INVALID_QUERY", + ); + + let executor = PatternExecutor::new().with_exec_failure("PRAGMA foreign_keys = ON;"); + assert_sql_error_code( + restore_database_backup(&executor, &backup), + "ERR_INVALID_QUERY", + ); +} + +#[test] +fn backup_public_api_insert_and_parse_failures_cover_library_instantiations() { + let backup = DatabaseBackup { + format_version: DATABASE_BACKUP_VERSION.to_string(), + replica_store_version: REPLICA_STORE_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);")), + }], + migrations: Vec::new(), + data: vec![TableData { + name: String::from("tb_a"), + rows: vec![{ + let mut row = Map::new(); + row.insert(String::from("id"), Value::from("1")); + row + }], + }], + }; + + let executor = PatternExecutor::new().with_exec_failure("INSERT INTO \"tb_a\""); + assert_sql_error_code( + restore_database_backup(&executor, &backup), + "ERR_INVALID_QUERY", + ); + + let schema_query = "select type, name, tbl_name as table_name, sql from sqlite_master"; + let executor = PatternExecutor::new().with_query_rule(schema_query, "{"); + assert_sql_error_code(export_database_backup(&executor), "ERR_SERIALIZATION"); + + let schema_rows = json!([ + { + "type": "table", + "name": "tb_a", + "table_name": "tb_a", + "sql": "CREATE TABLE tb_a (id TEXT);" + } + ]) + .to_string(); + let executor = PatternExecutor::new() + .with_query_rule(schema_query, &schema_rows) + .with_query_rule("SELECT * FROM \"tb_a\";", "{"); + assert_sql_error_code(export_database_backup(&executor), "ERR_SERIALIZATION"); + + let null_name_rows = json!([ + { + "type": "table", + "name": null, + "table_name": "tb_a", + "sql": "CREATE TABLE tb_a (id TEXT);" + } + ]) + .to_string(); + let executor = PatternExecutor::new().with_query_rule(schema_query, &null_name_rows); + let backup = export_database_backup(&executor).expect("backup with null-name schema rows"); + assert!(backup.schema.is_empty()); +} + +#[test] +fn export_manifest_public_api_error_paths_cover_library_instantiations() { + let schema_query = "select type, name, tbl_name as table_name, sql from sqlite_master"; + + let executor = PatternExecutor::new().with_query_failure(schema_query); + assert_sql_error_code(export_manifest(&executor), "ERR_INVALID_QUERY"); + + let schema_rows = json!([ + { + "type": "table", + "name": "tb_a", + "table_name": "tb_a", + "sql": "CREATE TABLE tb_a (id TEXT);" + } + ]) + .to_string(); + + let executor = PatternExecutor::new() + .with_query_rule(schema_query, &schema_rows) + .with_query_failure("select count(1) as count from \"tb_a\""); + assert_sql_error_code(export_manifest(&executor), "ERR_INVALID_QUERY"); + + let executor = PatternExecutor::new() + .with_query_rule(schema_query, &schema_rows) + .with_query_rule("select count(1) as count from \"tb_a\"", "{"); + assert_sql_error_code(export_manifest(&executor), "ERR_SERIALIZATION"); + + let executor = PatternExecutor::new() + .with_query_rule(schema_query, &schema_rows) + .with_query_rule("select count(1) as count from \"tb_a\"", "[]"); + let manifest = export_manifest(&executor).expect("manifest success"); + assert_eq!(manifest.table_counts.len(), 1); +} diff --git a/crates/replica_store/tests/error_paths.rs b/crates/replica_store/tests/error_paths.rs @@ -0,0 +1,977 @@ +use radroots_replica_schema::ReplicaSchemaError; +use radroots_replica_schema::farm::{ + IFarmCreate, IFarmDelete, IFarmFindMany, IFarmFindOne, IFarmUpdate, +}; +use radroots_replica_schema::farm_gcs_location::{ + IFarmGcsLocationCreate, IFarmGcsLocationDelete, IFarmGcsLocationFindMany, + IFarmGcsLocationFindOne, IFarmGcsLocationUpdate, +}; +use radroots_replica_schema::farm_member::{ + IFarmMemberCreate, IFarmMemberDelete, IFarmMemberFindMany, IFarmMemberFindOne, + IFarmMemberUpdate, +}; +use radroots_replica_schema::farm_member_claim::{ + IFarmMemberClaimCreate, IFarmMemberClaimDelete, IFarmMemberClaimFindMany, + IFarmMemberClaimFindOne, IFarmMemberClaimUpdate, +}; +use radroots_replica_schema::farm_tag::{ + IFarmTagCreate, IFarmTagDelete, IFarmTagFindMany, IFarmTagFindOne, IFarmTagUpdate, +}; +use radroots_replica_schema::gcs_location::{ + IGcsLocationCreate, IGcsLocationDelete, IGcsLocationFindMany, IGcsLocationFindOne, + IGcsLocationUpdate, +}; +use radroots_replica_schema::log_error::{ + ILogErrorCreate, ILogErrorDelete, ILogErrorFindMany, ILogErrorFindOne, ILogErrorUpdate, +}; +use radroots_replica_schema::media_image::{ + IMediaImageCreate, IMediaImageDelete, IMediaImageFindMany, IMediaImageFindOne, + IMediaImageUpdate, +}; +use radroots_replica_schema::nostr_event_head::{ + INostrEventHeadCreate, INostrEventHeadDelete, INostrEventHeadFindMany, INostrEventHeadFindOne, + INostrEventHeadUpdate, +}; +use radroots_replica_schema::nostr_profile::{ + INostrProfileCreate, INostrProfileDelete, INostrProfileFindMany, INostrProfileFindOne, + INostrProfileUpdate, +}; +use radroots_replica_schema::nostr_profile_relay::INostrProfileRelayRelation; +use radroots_replica_schema::nostr_relay::{ + INostrRelayCreate, INostrRelayDelete, INostrRelayFindMany, INostrRelayFindOne, + INostrRelayUpdate, +}; +use radroots_replica_schema::plot::{ + IPlotCreate, IPlotDelete, IPlotFindMany, IPlotFindOne, IPlotUpdate, +}; +use radroots_replica_schema::plot_gcs_location::{ + IPlotGcsLocationCreate, IPlotGcsLocationDelete, IPlotGcsLocationFindMany, + IPlotGcsLocationFindOne, IPlotGcsLocationUpdate, +}; +use radroots_replica_schema::plot_tag::{ + IPlotTagCreate, IPlotTagDelete, IPlotTagFindMany, IPlotTagFindOne, IPlotTagUpdate, +}; +use radroots_replica_schema::trade_product::{ + ITradeProductCreate, ITradeProductDelete, ITradeProductFindMany, ITradeProductFindOne, + ITradeProductUpdate, +}; +use radroots_replica_schema::trade_product_location::ITradeProductLocationRelation; +use radroots_replica_schema::trade_product_media::ITradeProductMediaRelation; +use radroots_replica_store::ReplicaSql; +use radroots_sql_core::{SqlError, SqlExecutor, SqliteExecutor}; +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 open_db() -> ReplicaSql<SqliteExecutor> { + let exec = SqliteExecutor::open_memory().expect("open sqlite memory"); + let db = ReplicaSql::new(exec); + db.migrate_up().expect("migrate up"); + db +} + +fn drop_table(db: &ReplicaSql<SqliteExecutor>, table_name: &str) { + let sql = format!("DROP TABLE {table_name};"); + db.executor().exec(&sql, "[]").expect("drop table"); +} + +fn assert_invalid_query<T>(result: Result<T, ReplicaSchemaError<SqlError>>) { + let err = match result { + Ok(_) => panic!("invalid query expected"), + Err(err) => err, + }; + assert!(matches!(err.err, SqlError::InvalidQuery(_))); +} + +fn assert_not_found<T>(result: Result<T, ReplicaSchemaError<SqlError>>) { + let err = match result { + Ok(_) => panic!("not found expected"), + Err(err) => err, + }; + assert!(matches!(err.err, SqlError::NotFound(_))); +} + +#[test] +fn gcs_location_error_paths_cover_regions() { + let db = open_db(); + + let update_missing: IGcsLocationUpdate = parse_json(json!({ + "on": { "d_tag": "missing-gcs" }, + "fields": { "label": "x" } + })); + assert_not_found(db.gcs_location_update(&update_missing)); + + let delete_missing_on: IGcsLocationDelete = parse_json(json!({ + "on": { "d_tag": "missing-gcs" } + })); + assert_not_found(db.gcs_location_delete(&delete_missing_on)); + + let delete_missing_rel: IGcsLocationDelete = parse_json(json!({ + "rel": { "off_plot": { "id": "missing-plot" } } + })); + assert_not_found(db.gcs_location_delete(&delete_missing_rel)); + + drop_table(&db, "gcs_location"); + + let create_opts: 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))" + })); + assert_invalid_query(db.gcs_location_create(&create_opts)); + + let find_many_filter: IGcsLocationFindMany = parse_json(json!({ + "filter": { "id": "id-1" } + })); + assert_invalid_query(db.gcs_location_find_many(&find_many_filter)); + + let find_many_rel: IGcsLocationFindMany = parse_json(json!({ + "rel": { "on_farm": { "id": "farm-1" } } + })); + assert_invalid_query(db.gcs_location_find_many(&find_many_rel)); + + let find_one_on: IGcsLocationFindOne = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.gcs_location_find_one(&find_one_on)); + + let find_one_rel: IGcsLocationFindOne = parse_json(json!({ + "rel": { "off_trade_product": { "id": "tp-1" } } + })); + assert_invalid_query(db.gcs_location_find_one(&find_one_rel)); + + let update_id: IGcsLocationUpdate = parse_json(json!({ + "on": { "id": "id-1" }, + "fields": { "label": "x" } + })); + assert_invalid_query(db.gcs_location_update(&update_id)); + + let delete_id: IGcsLocationDelete = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.gcs_location_delete(&delete_id)); + + let delete_rel: IGcsLocationDelete = parse_json(json!({ + "rel": { "on_plot": { "id": "plot-1" } } + })); + assert_invalid_query(db.gcs_location_delete(&delete_rel)); +} + +#[test] +fn media_image_error_paths_cover_regions() { + let db = open_db(); + + let update_missing: IMediaImageUpdate = parse_json(json!({ + "on": { "file_path": "/missing.jpg" }, + "fields": { "label": "x" } + })); + assert_not_found(db.media_image_update(&update_missing)); + + let delete_missing_on: IMediaImageDelete = parse_json(json!({ + "on": { "file_path": "/missing.jpg" } + })); + assert_not_found(db.media_image_delete(&delete_missing_on)); + + let delete_missing_rel: IMediaImageDelete = parse_json(json!({ + "rel": { "off_trade_product": { "id": "missing-tp" } } + })); + assert_not_found(db.media_image_delete(&delete_missing_rel)); + + drop_table(&db, "media_image"); + + let create_opts: IMediaImageCreate = parse_json(json!({ + "file_path": "/img/a.jpg", + "mime_type": "image/jpeg", + "res_base": "https://cdn.example.com", + "res_path": "img/a.jpg" + })); + assert_invalid_query(db.media_image_create(&create_opts)); + + let find_many_filter: IMediaImageFindMany = parse_json(json!({ + "filter": { "id": "id-1" } + })); + assert_invalid_query(db.media_image_find_many(&find_many_filter)); + + let find_many_rel: IMediaImageFindMany = parse_json(json!({ + "rel": { "on_trade_product": { "id": "tp-1" } } + })); + assert_invalid_query(db.media_image_find_many(&find_many_rel)); + + let find_one_on: IMediaImageFindOne = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.media_image_find_one(&find_one_on)); + + let find_one_rel: IMediaImageFindOne = parse_json(json!({ + "rel": { "off_trade_product": { "id": "tp-1" } } + })); + assert_invalid_query(db.media_image_find_one(&find_one_rel)); + + let update_id: IMediaImageUpdate = parse_json(json!({ + "on": { "id": "id-1" }, + "fields": { "label": "x" } + })); + assert_invalid_query(db.media_image_update(&update_id)); + + let delete_id: IMediaImageDelete = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.media_image_delete(&delete_id)); + + let delete_rel: IMediaImageDelete = parse_json(json!({ + "rel": { "on_trade_product": { "id": "tp-1" } } + })); + assert_invalid_query(db.media_image_delete(&delete_rel)); +} + +#[test] +fn nostr_profile_error_paths_cover_regions() { + let db = open_db(); + + let update_missing: INostrProfileUpdate = parse_json(json!({ + "on": { "public_key": hex64('a') }, + "fields": { "name": "x" } + })); + assert_not_found(db.nostr_profile_update(&update_missing)); + + let delete_missing_on: INostrProfileDelete = parse_json(json!({ + "on": { "public_key": hex64('a') } + })); + assert_not_found(db.nostr_profile_delete(&delete_missing_on)); + + let delete_missing_rel: INostrProfileDelete = parse_json(json!({ + "rel": { "off_relay": { "id": "missing-relay" } } + })); + assert_not_found(db.nostr_profile_delete(&delete_missing_rel)); + + drop_table(&db, "nostr_profile"); + + let create_opts: INostrProfileCreate = parse_json(json!({ + "public_key": hex64('d'), + "profile_type": "farm", + "name": "profile a" + })); + assert_invalid_query(db.nostr_profile_create(&create_opts)); + + let find_many_filter: INostrProfileFindMany = parse_json(json!({ + "filter": { "id": "id-1" } + })); + assert_invalid_query(db.nostr_profile_find_many(&find_many_filter)); + + let find_many_rel: INostrProfileFindMany = parse_json(json!({ + "rel": { "on_relay": { "id": "relay-1" } } + })); + assert_invalid_query(db.nostr_profile_find_many(&find_many_rel)); + + let find_one_on: INostrProfileFindOne = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.nostr_profile_find_one(&find_one_on)); + + let find_one_rel: INostrProfileFindOne = parse_json(json!({ + "rel": { "off_relay": { "id": "relay-1" } } + })); + assert_invalid_query(db.nostr_profile_find_one(&find_one_rel)); + + let update_id: INostrProfileUpdate = parse_json(json!({ + "on": { "id": "id-1" }, + "fields": { "name": "x" } + })); + assert_invalid_query(db.nostr_profile_update(&update_id)); + + let delete_id: INostrProfileDelete = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.nostr_profile_delete(&delete_id)); + + let delete_rel: INostrProfileDelete = parse_json(json!({ + "rel": { "on_relay": { "id": "relay-1" } } + })); + assert_invalid_query(db.nostr_profile_delete(&delete_rel)); +} + +#[test] +fn nostr_relay_error_paths_cover_regions() { + let db = open_db(); + + let update_missing: INostrRelayUpdate = parse_json(json!({ + "on": { "url": "wss://missing.example.com" }, + "fields": { "name": "x" } + })); + assert_not_found(db.nostr_relay_update(&update_missing)); + + let delete_missing_on: INostrRelayDelete = parse_json(json!({ + "on": { "url": "wss://missing.example.com" } + })); + assert_not_found(db.nostr_relay_delete(&delete_missing_on)); + + let delete_missing_rel: INostrRelayDelete = parse_json(json!({ + "rel": { "off_profile": { "public_key": hex64('b') } } + })); + assert_not_found(db.nostr_relay_delete(&delete_missing_rel)); + + drop_table(&db, "nostr_relay"); + + let create_opts: INostrRelayCreate = parse_json(json!({ + "url": "wss://relay.example.com" + })); + assert_invalid_query(db.nostr_relay_create(&create_opts)); + + let find_many_filter: INostrRelayFindMany = parse_json(json!({ + "filter": { "id": "id-1" } + })); + assert_invalid_query(db.nostr_relay_find_many(&find_many_filter)); + + let find_many_rel: INostrRelayFindMany = parse_json(json!({ + "rel": { "on_profile": { "public_key": hex64('d') } } + })); + assert_invalid_query(db.nostr_relay_find_many(&find_many_rel)); + + let find_one_on: INostrRelayFindOne = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.nostr_relay_find_one(&find_one_on)); + + let find_one_rel: INostrRelayFindOne = parse_json(json!({ + "rel": { "off_profile": { "public_key": hex64('d') } } + })); + assert_invalid_query(db.nostr_relay_find_one(&find_one_rel)); + + let update_id: INostrRelayUpdate = parse_json(json!({ + "on": { "id": "id-1" }, + "fields": { "name": "x" } + })); + assert_invalid_query(db.nostr_relay_update(&update_id)); + + let delete_id: INostrRelayDelete = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.nostr_relay_delete(&delete_id)); + + let delete_rel: INostrRelayDelete = parse_json(json!({ + "rel": { "on_profile": { "public_key": hex64('d') } } + })); + assert_invalid_query(db.nostr_relay_delete(&delete_rel)); +} + +#[test] +fn farm_error_paths_cover_regions() { + let db = open_db(); + + let update_missing: IFarmUpdate = parse_json(json!({ + "on": { "d_tag": "missing-farm" }, + "fields": { "name": "farm x" } + })); + assert_not_found(db.farm_update(&update_missing)); + + let update_missing_id: IFarmUpdate = parse_json(json!({ + "on": { "id": "missing-id" }, + "fields": { "name": "farm y" } + })); + assert_not_found(db.farm_update(&update_missing_id)); + + let delete_missing_on: IFarmDelete = parse_json(json!({ + "on": { "d_tag": "missing-farm" } + })); + assert_not_found(db.farm_delete(&delete_missing_on)); + + drop_table(&db, "farm"); + + let create_opts: IFarmCreate = parse_json(json!({ + "d_tag": "farm-a", + "pubkey": hex64('a'), + "name": "farm a" + })); + assert_invalid_query(db.farm_create(&create_opts)); + + let find_many_filter: IFarmFindMany = parse_json(json!({ + "filter": { "id": "id-1" } + })); + assert_invalid_query(db.farm_find_many(&find_many_filter)); + + let find_one_on: IFarmFindOne = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.farm_find_one(&find_one_on)); + + let update_id: IFarmUpdate = parse_json(json!({ + "on": { "id": "id-1" }, + "fields": { "name": "farm z" } + })); + assert_invalid_query(db.farm_update(&update_id)); + + let delete_id: IFarmDelete = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.farm_delete(&delete_id)); +} + +#[test] +fn plot_error_paths_cover_regions() { + let db = open_db(); + + let update_missing: IPlotUpdate = parse_json(json!({ + "on": { "d_tag": "missing-plot" }, + "fields": { "name": "plot x" } + })); + assert_not_found(db.plot_update(&update_missing)); + + let update_missing_id: IPlotUpdate = parse_json(json!({ + "on": { "id": "missing-id" }, + "fields": { "name": "plot y" } + })); + assert_not_found(db.plot_update(&update_missing_id)); + + let delete_missing_on: IPlotDelete = parse_json(json!({ + "on": { "d_tag": "missing-plot" } + })); + assert_not_found(db.plot_delete(&delete_missing_on)); + + drop_table(&db, "plot"); + + let create_opts: IPlotCreate = parse_json(json!({ + "d_tag": "plot-a", + "farm_id": "farm-1", + "name": "plot a" + })); + assert_invalid_query(db.plot_create(&create_opts)); + + let find_many_filter: IPlotFindMany = parse_json(json!({ + "filter": { "id": "id-1" } + })); + assert_invalid_query(db.plot_find_many(&find_many_filter)); + + let find_one_on: IPlotFindOne = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.plot_find_one(&find_one_on)); + + let update_id: IPlotUpdate = parse_json(json!({ + "on": { "id": "id-1" }, + "fields": { "name": "plot z" } + })); + assert_invalid_query(db.plot_update(&update_id)); + + let delete_id: IPlotDelete = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.plot_delete(&delete_id)); +} + +#[test] +fn farm_gcs_location_error_paths_cover_regions() { + let db = open_db(); + + let update_missing: IFarmGcsLocationUpdate = parse_json(json!({ + "on": { "farm_id": "farm-1" }, + "fields": { "role": "x" } + })); + assert_not_found(db.farm_gcs_location_update(&update_missing)); + + let update_missing_id: IFarmGcsLocationUpdate = parse_json(json!({ + "on": { "id": "missing-id" }, + "fields": { "role": "y" } + })); + assert_not_found(db.farm_gcs_location_update(&update_missing_id)); + + let delete_missing_on: IFarmGcsLocationDelete = parse_json(json!({ + "on": { "farm_id": "farm-1" } + })); + assert_not_found(db.farm_gcs_location_delete(&delete_missing_on)); + + drop_table(&db, "farm_gcs_location"); + + let create_opts: IFarmGcsLocationCreate = parse_json(json!({ + "farm_id": "farm-1", + "gcs_location_id": "gcs-1", + "role": "primary" + })); + assert_invalid_query(db.farm_gcs_location_create(&create_opts)); + + let find_many_filter: IFarmGcsLocationFindMany = parse_json(json!({ + "filter": { "id": "id-1" } + })); + assert_invalid_query(db.farm_gcs_location_find_many(&find_many_filter)); + + let find_one_on: IFarmGcsLocationFindOne = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.farm_gcs_location_find_one(&find_one_on)); + + let update_id: IFarmGcsLocationUpdate = parse_json(json!({ + "on": { "id": "id-1" }, + "fields": { "role": "z" } + })); + assert_invalid_query(db.farm_gcs_location_update(&update_id)); + + let delete_id: IFarmGcsLocationDelete = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.farm_gcs_location_delete(&delete_id)); +} + +#[test] +fn plot_gcs_location_error_paths_cover_regions() { + let db = open_db(); + + let update_missing: IPlotGcsLocationUpdate = parse_json(json!({ + "on": { "plot_id": "plot-1" }, + "fields": { "role": "x" } + })); + assert_not_found(db.plot_gcs_location_update(&update_missing)); + + let update_missing_id: IPlotGcsLocationUpdate = parse_json(json!({ + "on": { "id": "missing-id" }, + "fields": { "role": "y" } + })); + assert_not_found(db.plot_gcs_location_update(&update_missing_id)); + + let delete_missing_on: IPlotGcsLocationDelete = parse_json(json!({ + "on": { "plot_id": "plot-1" } + })); + assert_not_found(db.plot_gcs_location_delete(&delete_missing_on)); + + drop_table(&db, "plot_gcs_location"); + + let create_opts: IPlotGcsLocationCreate = parse_json(json!({ + "plot_id": "plot-1", + "gcs_location_id": "gcs-1", + "role": "primary" + })); + assert_invalid_query(db.plot_gcs_location_create(&create_opts)); + + let find_many_filter: IPlotGcsLocationFindMany = parse_json(json!({ + "filter": { "id": "id-1" } + })); + assert_invalid_query(db.plot_gcs_location_find_many(&find_many_filter)); + + let find_one_on: IPlotGcsLocationFindOne = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.plot_gcs_location_find_one(&find_one_on)); + + let update_id: IPlotGcsLocationUpdate = parse_json(json!({ + "on": { "id": "id-1" }, + "fields": { "role": "z" } + })); + assert_invalid_query(db.plot_gcs_location_update(&update_id)); + + let delete_id: IPlotGcsLocationDelete = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.plot_gcs_location_delete(&delete_id)); +} + +#[test] +fn farm_tag_error_paths_cover_regions() { + let db = open_db(); + + let update_missing: IFarmTagUpdate = parse_json(json!({ + "on": { "farm_id": "farm-1" }, + "fields": { "tag": "x" } + })); + assert_not_found(db.farm_tag_update(&update_missing)); + + let update_missing_id: IFarmTagUpdate = parse_json(json!({ + "on": { "id": "missing-id" }, + "fields": { "tag": "y" } + })); + assert_not_found(db.farm_tag_update(&update_missing_id)); + + let delete_missing_on: IFarmTagDelete = parse_json(json!({ + "on": { "farm_id": "farm-1" } + })); + assert_not_found(db.farm_tag_delete(&delete_missing_on)); + + drop_table(&db, "farm_tag"); + + let create_opts: IFarmTagCreate = parse_json(json!({ + "farm_id": "farm-1", + "tag": "organic" + })); + assert_invalid_query(db.farm_tag_create(&create_opts)); + + let find_many_filter: IFarmTagFindMany = parse_json(json!({ + "filter": { "id": "id-1" } + })); + assert_invalid_query(db.farm_tag_find_many(&find_many_filter)); + + let find_one_on: IFarmTagFindOne = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.farm_tag_find_one(&find_one_on)); + + let update_id: IFarmTagUpdate = parse_json(json!({ + "on": { "id": "id-1" }, + "fields": { "tag": "z" } + })); + assert_invalid_query(db.farm_tag_update(&update_id)); + + let delete_id: IFarmTagDelete = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.farm_tag_delete(&delete_id)); +} + +#[test] +fn plot_tag_error_paths_cover_regions() { + let db = open_db(); + + let update_missing: IPlotTagUpdate = parse_json(json!({ + "on": { "plot_id": "plot-1" }, + "fields": { "tag": "x" } + })); + assert_not_found(db.plot_tag_update(&update_missing)); + + let update_missing_id: IPlotTagUpdate = parse_json(json!({ + "on": { "id": "missing-id" }, + "fields": { "tag": "y" } + })); + assert_not_found(db.plot_tag_update(&update_missing_id)); + + let delete_missing_on: IPlotTagDelete = parse_json(json!({ + "on": { "plot_id": "plot-1" } + })); + assert_not_found(db.plot_tag_delete(&delete_missing_on)); + + drop_table(&db, "plot_tag"); + + let create_opts: IPlotTagCreate = parse_json(json!({ + "plot_id": "plot-1", + "tag": "north" + })); + assert_invalid_query(db.plot_tag_create(&create_opts)); + + let find_many_filter: IPlotTagFindMany = parse_json(json!({ + "filter": { "id": "id-1" } + })); + assert_invalid_query(db.plot_tag_find_many(&find_many_filter)); + + let find_one_on: IPlotTagFindOne = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.plot_tag_find_one(&find_one_on)); + + let update_id: IPlotTagUpdate = parse_json(json!({ + "on": { "id": "id-1" }, + "fields": { "tag": "z" } + })); + assert_invalid_query(db.plot_tag_update(&update_id)); + + let delete_id: IPlotTagDelete = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.plot_tag_delete(&delete_id)); +} + +#[test] +fn farm_member_error_paths_cover_regions() { + let db = open_db(); + + let update_missing: IFarmMemberUpdate = parse_json(json!({ + "on": { "member_pubkey": hex64('a') }, + "fields": { "role": "x" } + })); + assert_not_found(db.farm_member_update(&update_missing)); + + let update_missing_id: IFarmMemberUpdate = parse_json(json!({ + "on": { "id": "missing-id" }, + "fields": { "role": "y" } + })); + assert_not_found(db.farm_member_update(&update_missing_id)); + + let delete_missing_on: IFarmMemberDelete = parse_json(json!({ + "on": { "member_pubkey": hex64('a') } + })); + assert_not_found(db.farm_member_delete(&delete_missing_on)); + + drop_table(&db, "farm_member"); + + let create_opts: IFarmMemberCreate = parse_json(json!({ + "farm_id": "farm-1", + "member_pubkey": hex64('a'), + "role": "owner" + })); + assert_invalid_query(db.farm_member_create(&create_opts)); + + let find_many_filter: IFarmMemberFindMany = parse_json(json!({ + "filter": { "id": "id-1" } + })); + assert_invalid_query(db.farm_member_find_many(&find_many_filter)); + + let find_one_on: IFarmMemberFindOne = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.farm_member_find_one(&find_one_on)); + + let update_id: IFarmMemberUpdate = parse_json(json!({ + "on": { "id": "id-1" }, + "fields": { "role": "z" } + })); + assert_invalid_query(db.farm_member_update(&update_id)); + + let delete_id: IFarmMemberDelete = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.farm_member_delete(&delete_id)); +} + +#[test] +fn farm_member_claim_error_paths_cover_regions() { + let db = open_db(); + + let update_missing: IFarmMemberClaimUpdate = parse_json(json!({ + "on": { "member_pubkey": hex64('a') }, + "fields": { "farm_pubkey": hex64('b') } + })); + assert_not_found(db.farm_member_claim_update(&update_missing)); + + let update_missing_id: IFarmMemberClaimUpdate = parse_json(json!({ + "on": { "id": "missing-id" }, + "fields": { "farm_pubkey": hex64('c') } + })); + assert_not_found(db.farm_member_claim_update(&update_missing_id)); + + let delete_missing_on: IFarmMemberClaimDelete = parse_json(json!({ + "on": { "member_pubkey": hex64('a') } + })); + assert_not_found(db.farm_member_claim_delete(&delete_missing_on)); + + drop_table(&db, "farm_member_claim"); + + let create_opts: IFarmMemberClaimCreate = parse_json(json!({ + "member_pubkey": hex64('a'), + "farm_pubkey": hex64('b') + })); + assert_invalid_query(db.farm_member_claim_create(&create_opts)); + + let find_many_filter: IFarmMemberClaimFindMany = parse_json(json!({ + "filter": { "id": "id-1" } + })); + assert_invalid_query(db.farm_member_claim_find_many(&find_many_filter)); + + let find_one_on: IFarmMemberClaimFindOne = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.farm_member_claim_find_one(&find_one_on)); + + let update_id: IFarmMemberClaimUpdate = parse_json(json!({ + "on": { "id": "id-1" }, + "fields": { "farm_pubkey": hex64('c') } + })); + assert_invalid_query(db.farm_member_claim_update(&update_id)); + + let delete_id: IFarmMemberClaimDelete = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.farm_member_claim_delete(&delete_id)); +} + +#[test] +fn log_error_error_paths_cover_regions() { + let db = open_db(); + + let update_missing: ILogErrorUpdate = parse_json(json!({ + "on": { "nostr_pubkey": hex64('a') }, + "fields": { "message": "x" } + })); + assert_not_found(db.log_error_update(&update_missing)); + + let update_missing_id: ILogErrorUpdate = parse_json(json!({ + "on": { "id": "missing-id" }, + "fields": { "message": "y" } + })); + assert_not_found(db.log_error_update(&update_missing_id)); + + let delete_missing_on: ILogErrorDelete = parse_json(json!({ + "on": { "nostr_pubkey": hex64('a') } + })); + assert_not_found(db.log_error_delete(&delete_missing_on)); + + drop_table(&db, "log_error"); + + let create_opts: ILogErrorCreate = parse_json(json!({ + "error": "panic", + "message": "boom", + "app_system": "studio", + "app_version": "1.0.0", + "nostr_pubkey": hex64('a') + })); + assert_invalid_query(db.log_error_create(&create_opts)); + + let find_many_filter: ILogErrorFindMany = parse_json(json!({ + "filter": { "id": "id-1" } + })); + assert_invalid_query(db.log_error_find_many(&find_many_filter)); + + let find_one_on: ILogErrorFindOne = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.log_error_find_one(&find_one_on)); + + let update_id: ILogErrorUpdate = parse_json(json!({ + "on": { "id": "id-1" }, + "fields": { "message": "z" } + })); + assert_invalid_query(db.log_error_update(&update_id)); + + let delete_id: ILogErrorDelete = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.log_error_delete(&delete_id)); +} + +#[test] +fn nostr_event_head_error_paths_cover_regions() { + let db = open_db(); + + let update_missing: INostrEventHeadUpdate = parse_json(json!({ + "on": { "key": "state-a" }, + "fields": { "content_hash": "hash-x" } + })); + assert_not_found(db.nostr_event_head_update(&update_missing)); + + let update_missing_id: INostrEventHeadUpdate = parse_json(json!({ + "on": { "id": "missing-id" }, + "fields": { "content_hash": "hash-y" } + })); + assert_not_found(db.nostr_event_head_update(&update_missing_id)); + + let delete_missing_on: INostrEventHeadDelete = parse_json(json!({ + "on": { "key": "state-a" } + })); + assert_not_found(db.nostr_event_head_delete(&delete_missing_on)); + + drop_table(&db, "nostr_event_head"); + + let create_opts: INostrEventHeadCreate = parse_json(json!({ + "key": "state-a", + "kind": 30023, + "pubkey": hex64('a'), + "d_tag": "listing-a", + "last_event_id": hex64('b'), + "last_created_at": 1, + "content_hash": "hash-a" + })); + assert_invalid_query(db.nostr_event_head_create(&create_opts)); + + let find_many_filter: INostrEventHeadFindMany = parse_json(json!({ + "filter": { "id": "id-1" } + })); + assert_invalid_query(db.nostr_event_head_find_many(&find_many_filter)); + + let find_one_on: INostrEventHeadFindOne = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.nostr_event_head_find_one(&find_one_on)); + + let update_id: INostrEventHeadUpdate = parse_json(json!({ + "on": { "id": "id-1" }, + "fields": { "content_hash": "hash-z" } + })); + assert_invalid_query(db.nostr_event_head_update(&update_id)); + + let delete_id: INostrEventHeadDelete = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.nostr_event_head_delete(&delete_id)); +} + +#[test] +fn trade_product_error_paths_cover_regions() { + let db = open_db(); + + let update_missing: ITradeProductUpdate = parse_json(json!({ + "on": { "id": "missing-id" }, + "fields": { "title": "x" } + })); + assert_not_found(db.trade_product_update(&update_missing)); + + let delete_missing_on: ITradeProductDelete = parse_json(json!({ + "on": { "id": "missing-id" } + })); + assert_not_found(db.trade_product_delete(&delete_missing_on)); + + drop_table(&db, "trade_product"); + + let create_opts: 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_amt_exact": "100", + "qty_unit": "kg", + "price_amt": 7.5, + "price_amt_exact": "7.5", + "price_currency": "USD", + "price_qty_amt": 1, + "price_qty_amt_exact": "1", + "price_qty_unit": "kg" + })); + assert_invalid_query(db.trade_product_create(&create_opts)); + + let find_many_filter: ITradeProductFindMany = parse_json(json!({ + "filter": { "id": "id-1" } + })); + assert_invalid_query(db.trade_product_find_many(&find_many_filter)); + + let find_one_on: ITradeProductFindOne = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.trade_product_find_one(&find_one_on)); + + let update_id: ITradeProductUpdate = parse_json(json!({ + "on": { "id": "id-1" }, + "fields": { "title": "z" } + })); + assert_invalid_query(db.trade_product_update(&update_id)); + + let delete_id: ITradeProductDelete = parse_json(json!({ + "on": { "id": "id-1" } + })); + assert_invalid_query(db.trade_product_delete(&delete_id)); +} + +#[test] +fn relation_set_unset_error_paths_cover_regions() { + let db = open_db(); + + drop_table(&db, "nostr_profile_relay"); + let profile_relay_rel: INostrProfileRelayRelation = parse_json(json!({ + "nostr_profile": { "id": "profile-1" }, + "nostr_relay": { "id": "relay-1" } + })); + assert_invalid_query(db.nostr_profile_relay_set(&profile_relay_rel)); + assert_invalid_query(db.nostr_profile_relay_unset(&profile_relay_rel)); + + drop_table(&db, "trade_product_location"); + let product_location_rel: ITradeProductLocationRelation = parse_json(json!({ + "trade_product": { "id": "product-1" }, + "gcs_location": { "id": "gcs-1" } + })); + assert_invalid_query(db.trade_product_location_set(&product_location_rel)); + assert_invalid_query(db.trade_product_location_unset(&product_location_rel)); + + drop_table(&db, "trade_product_media"); + let product_media_rel: ITradeProductMediaRelation = parse_json(json!({ + "trade_product": { "id": "product-1" }, + "media_image": { "id": "media-1" } + })); + assert_invalid_query(db.trade_product_media_set(&product_media_rel)); + assert_invalid_query(db.trade_product_media_unset(&product_media_rel)); +} diff --git a/crates/replica_store/tests/full_mode.rs b/crates/replica_store/tests/full_mode.rs @@ -0,0 +1,1350 @@ +use radroots_replica_schema::ReplicaSchemaError; +use radroots_replica_schema::farm::{ + IFarmCreate, IFarmDelete, IFarmFindMany, IFarmFindOne, IFarmUpdate, +}; +use radroots_replica_schema::farm_gcs_location::{ + IFarmGcsLocationCreate, IFarmGcsLocationDelete, IFarmGcsLocationFindMany, + IFarmGcsLocationFindOne, IFarmGcsLocationUpdate, +}; +use radroots_replica_schema::farm_member::{ + IFarmMemberCreate, IFarmMemberDelete, IFarmMemberFindMany, IFarmMemberFindOne, + IFarmMemberUpdate, +}; +use radroots_replica_schema::farm_member_claim::{ + IFarmMemberClaimCreate, IFarmMemberClaimDelete, IFarmMemberClaimFindMany, + IFarmMemberClaimFindOne, IFarmMemberClaimUpdate, +}; +use radroots_replica_schema::farm_tag::{ + IFarmTagCreate, IFarmTagDelete, IFarmTagFindMany, IFarmTagFindOne, IFarmTagUpdate, +}; +use radroots_replica_schema::gcs_location::{ + GcsLocationFarmArgs, GcsLocationFindManyRel, GcsLocationPlotArgs, GcsLocationTradeProductArgs, + IGcsLocationCreate, IGcsLocationDelete, IGcsLocationFindMany, IGcsLocationFindOne, + IGcsLocationUpdate, +}; +use radroots_replica_schema::log_error::{ + ILogErrorCreate, ILogErrorDelete, ILogErrorFindMany, ILogErrorFindOne, ILogErrorUpdate, +}; +use radroots_replica_schema::media_image::{ + IMediaImageCreate, IMediaImageDelete, IMediaImageFindMany, IMediaImageFindOne, + IMediaImageUpdate, MediaImageFindManyRel, MediaImageTradeProductArgs, +}; +use radroots_replica_schema::nostr_event_head::{ + INostrEventHeadCreate, INostrEventHeadDelete, INostrEventHeadFindMany, INostrEventHeadFindOne, + INostrEventHeadUpdate, +}; +use radroots_replica_schema::nostr_profile::{ + INostrProfileCreate, INostrProfileDelete, INostrProfileFindMany, INostrProfileFindOne, + INostrProfileUpdate, NostrProfileFindManyRel, NostrProfileRelayArgs, +}; +use radroots_replica_schema::nostr_profile_relay::INostrProfileRelayRelation; +use radroots_replica_schema::nostr_relay::{ + INostrRelayCreate, INostrRelayDelete, INostrRelayFindMany, INostrRelayFindOne, + INostrRelayUpdate, NostrRelayFindManyRel, NostrRelayProfileArgs, +}; +use radroots_replica_schema::plot::{ + IPlotCreate, IPlotDelete, IPlotFindMany, IPlotFindOne, IPlotUpdate, +}; +use radroots_replica_schema::plot_gcs_location::{ + IPlotGcsLocationCreate, IPlotGcsLocationDelete, IPlotGcsLocationFindMany, + IPlotGcsLocationFindOne, IPlotGcsLocationUpdate, +}; +use radroots_replica_schema::plot_tag::{ + IPlotTagCreate, IPlotTagDelete, IPlotTagFindMany, IPlotTagFindOne, IPlotTagUpdate, +}; +use radroots_replica_schema::trade_product::{ + ITradeProductCreate, ITradeProductDelete, ITradeProductFindMany, ITradeProductFindOne, + ITradeProductUpdate, +}; +use radroots_replica_schema::trade_product_location::ITradeProductLocationRelation; +use radroots_replica_schema::trade_product_media::ITradeProductMediaRelation; +use radroots_replica_store::{ReplicaSql, export_manifest}; +use radroots_sql_core::{SqlError, SqliteExecutor}; +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, ReplicaSchemaError<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, ReplicaSchemaError<SqlError>>) { + let err = match result { + Ok(_) => panic!("not found expected"), + Err(err) => err, + }; + assert!(matches!(err.err, SqlError::NotFound(_))); +} + +fn open_db() -> ReplicaSql<SqliteExecutor> { + let exec = SqliteExecutor::open_memory().expect("open sqlite memory"); + let db = ReplicaSql::new(exec); + db.migrate_up().expect("migrate up"); + db +} + +#[test] +fn full_mode_shaped_query_helpers_cover_cli_reads() { + let db = open_db(); + + let farm: IFarmCreate = parse_json(json!({ + "d_tag": "farm-a", + "pubkey": hex64('a'), + "name": "farm a" + })); + db.farm_create(&farm).expect("farm create"); + + 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))", + "label": "stockholm" + })); + let gcs_created = db + .gcs_location_create(&gcs_location) + .expect("gcs create") + .result; + let listing_addr = format!("30402:{}:listing-a", hex64('d')); + + let trade_product: ITradeProductCreate = parse_json(json!({ + "key": "product-a", + "category": "coffee", + "title": "coffee a", + "summary": "washed lot", + "process": "washed", + "lot": "lot-a", + "profile": "floral", + "year": 2024, + "qty_amt": 100, + "qty_amt_exact": "100", + "qty_unit": "kg", + "qty_label": "bags", + "qty_avail": 2, + "price_amt": 7.5, + "price_amt_exact": "7.5", + "price_currency": "USD", + "price_qty_amt": 1, + "price_qty_amt_exact": "1", + "price_qty_unit": "kg", + "listing_addr": listing_addr.clone(), + "primary_bin_id": "bin-a", + "verified_primary_bin_id": "bin-a", + "notes": "fresh coffee" + })); + let trade_product_created = db + .trade_product_create(&trade_product) + .expect("trade product create") + .result; + + 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 nostr_event_head: INostrEventHeadCreate = parse_json(json!({ + "key": "state-a", + "kind": 30023, + "pubkey": hex64('d'), + "d_tag": "listing-a", + "last_event_id": hex64('e'), + "last_created_at": 42, + "content_hash": "hash-a" + })); + db.nostr_event_head_create(&nostr_event_head) + .expect("nostr event state create"); + + let rows = db + .trade_product_search(&["coffee".to_owned()]) + .expect("trade product search"); + assert_eq!(rows.len(), 1); + assert_eq!(rows[0].key, "product-a"); + assert_eq!(rows[0].listing_addr.as_deref(), Some(listing_addr.as_str())); + assert_eq!(rows[0].primary_bin_id.as_deref(), Some("bin-a")); + assert_eq!(rows[0].verified_primary_bin_id.as_deref(), Some("bin-a")); + assert_eq!(rows[0].location_primary.as_deref(), Some("stockholm")); + + let lookup_rows = db + .trade_product_lookup("product-a") + .expect("trade product lookup"); + assert_eq!(lookup_rows.len(), 1); + assert_eq!(lookup_rows[0].id, trade_product_created.id); + assert_eq!( + lookup_rows[0].listing_addr.as_deref(), + Some(listing_addr.as_str()) + ); + assert_eq!(lookup_rows[0].primary_bin_id.as_deref(), Some("bin-a")); + assert_eq!( + lookup_rows[0].verified_primary_bin_id.as_deref(), + Some("bin-a") + ); + + assert_eq!( + db.trade_product_search(&[]).expect("empty search"), + Vec::new() + ); + assert_eq!( + db.farm_unique_d_tag_by_pubkey(hex64('a').as_str()) + .expect("farm unique d tag"), + Some("farm-a".to_owned()) + ); + assert_eq!( + db.nostr_event_last_created_at() + .expect("nostr event freshness"), + Some(42) + ); +} + +#[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_head: INostrEventHeadCreate = 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_head_created = db + .nostr_event_head_create(&nostr_event_head) + .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_amt_exact": "100", + "qty_unit": "kg", + "price_amt": 7.5, + "price_amt_exact": "7.5", + "price_currency": "USD", + "price_qty_amt": 1, + "price_qty_amt_exact": "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_head_find_many: INostrEventHeadFindMany = + parse_json(json!({ "filter": { "id": nostr_event_head_created.id } })); + assert_eq!( + db.nostr_event_head_find_many(&nostr_event_head_find_many) + .expect("nostr event state find many") + .results + .len(), + 1 + ); + let nostr_event_head_find_one: INostrEventHeadFindOne = + parse_json(json!({ "on": { "id": nostr_event_head_created.id } })); + assert!( + db.nostr_event_head_find_one(&nostr_event_head_find_one) + .expect("nostr event state find one") + .result + .is_some() + ); + let nostr_event_head_update_alt: INostrEventHeadUpdate = + parse_json(json!({ "on": { "key": "state-a" }, "fields": { "content_hash": "hash-b" } })); + assert_eq!( + db.nostr_event_head_update(&nostr_event_head_update_alt) + .expect("nostr event state update") + .result + .content_hash, + "hash-b" + ); + let nostr_event_head_update_id: INostrEventHeadUpdate = parse_json( + json!({ "on": { "id": nostr_event_head_created.id }, "fields": { "content_hash": "hash-c" } }), + ); + assert_eq!( + db.nostr_event_head_update(&nostr_event_head_update_id) + .expect("nostr event state update id") + .result + .content_hash, + "hash-c" + ); + let nostr_event_head_update_empty: INostrEventHeadUpdate = + parse_json(json!({ "on": { "id": nostr_event_head_created.id }, "fields": {} })); + assert_invalid_argument(db.nostr_event_head_update(&nostr_event_head_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 } })); + let trade_product_results = db + .trade_product_find_many(&trade_product_find_many) + .expect("trade product find many") + .results; + assert_eq!(trade_product_results.len(), 1); + assert_eq!(trade_product_results[0].listing_addr, None); + 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_head_delete: INostrEventHeadDelete = + parse_json(json!({ "on": { "key": "state-a" } })); + db.nostr_event_head_delete(&nostr_event_head_delete) + .expect("nostr event state delete"); + let nostr_event_head_delete_missing: INostrEventHeadDelete = + parse_json(json!({ "on": { "id": nostr_event_head_created.id } })); + assert_not_found(db.nostr_event_head_delete(&nostr_event_head_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_store/tests/migration_repairs.rs b/crates/replica_store/tests/migration_repairs.rs @@ -0,0 +1,104 @@ +use radroots_replica_store::migrations; +use radroots_sql_core::{SqlExecutor, SqliteExecutor}; +use serde_json::Value; + +fn query_rows(exec: &SqliteExecutor, sql: &str) -> Vec<Value> { + serde_json::from_str(&exec.query_raw(sql, "[]").expect("query should succeed")) + .expect("query should decode") +} + +fn create_legacy_schema_without_secondary_indexes(exec: &SqliteExecutor) { + let schema = [ + "CREATE TABLE __migrations (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, applied_at TEXT NOT NULL DEFAULT (datetime('now')))", + "CREATE TABLE farm (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), d_tag TEXT NOT NULL, pubkey TEXT NOT NULL, name TEXT NOT NULL, about TEXT, website TEXT, picture TEXT, banner TEXT, location_primary TEXT, location_city TEXT, location_region TEXT, location_country TEXT)", + "CREATE TABLE gcs_location (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), d_tag TEXT NOT NULL, lat REAL NOT NULL, lng REAL NOT NULL, geohash TEXT NOT NULL, point TEXT NOT NULL, polygon TEXT NOT NULL, accuracy REAL, altitude REAL, tag_0 TEXT, label TEXT, area REAL, elevation INTEGER, soil TEXT, climate TEXT, gc_id TEXT, gc_name TEXT, gc_admin1_id TEXT, gc_admin1_name TEXT, gc_country_id TEXT, gc_country_name TEXT)", + "CREATE TABLE trade_product (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), key TEXT NOT NULL, category TEXT NOT NULL, title TEXT NOT NULL, summary TEXT NOT NULL, process TEXT NOT NULL, lot TEXT NOT NULL, profile TEXT NOT NULL, year INTEGER NOT NULL, qty_amt INTEGER NOT NULL, qty_unit CHAR(4) NOT NULL, qty_label TEXT, qty_avail INTEGER, price_amt REAL NOT NULL, price_currency CHAR(3) NOT NULL, price_qty_amt INTEGER NOT NULL, price_qty_unit CHAR(4) NOT NULL, notes TEXT)", + "CREATE TABLE plot (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), d_tag TEXT NOT NULL, farm_id CHAR(36) NOT NULL, name TEXT NOT NULL, about TEXT, location_primary TEXT, location_city TEXT, location_region TEXT, location_country TEXT, FOREIGN KEY (farm_id) REFERENCES farm(id) ON DELETE CASCADE)", + "CREATE TABLE nostr_event_head (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), key TEXT NOT NULL UNIQUE, kind INTEGER NOT NULL, pubkey CHAR(64) NOT NULL CHECK(length(pubkey) = 64), d_tag TEXT NOT NULL, last_event_id CHAR(64) NOT NULL CHECK(length(last_event_id) = 64), last_created_at INTEGER NOT NULL, content_hash TEXT NOT NULL)", + ]; + + for sql in schema { + exec.exec(sql, "[]") + .expect("schema statement should succeed"); + } + + for name in [ + "0000_init", + "0001_log_error", + "0002_farm", + "0003_gcs_location", + "0004_trade_product", + "0005_nostr_profile", + "0006_nostr_relay", + "0007_media_image", + "0008_farm_gcs_location", + "0009_nostr_profile_relay", + "0010_trade_product_location", + "0011_trade_product_media", + "0012_plot", + "0013_plot_gcs_location", + "0014_farm_tag", + "0015_plot_tag", + "0016_farm_member", + "0017_farm_member_claim", + "0018_nostr_event_head", + ] { + let sql = format!("INSERT INTO __migrations(name) VALUES ('{name}')"); + exec.exec(&sql, "[]") + .expect("legacy migration marker should succeed"); + } +} + +#[test] +fn run_all_up_repairs_missing_indexes_in_legacy_sqlite_dbs() { + let exec = SqliteExecutor::open_memory().expect("open sqlite memory"); + create_legacy_schema_without_secondary_indexes(&exec); + + let before = query_rows( + &exec, + "SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('farm_pubkey_d_tag_idx', 'gcs_location_geohash_idx', 'plot_farm_d_tag_idx', 'nostr_event_head_kind_idx') ORDER BY name", + ); + assert!(before.is_empty()); + + migrations::run_all_up(&exec).expect("repair migration should succeed"); + + let after = query_rows( + &exec, + "SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('farm_pubkey_d_tag_idx', 'gcs_location_geohash_idx', 'plot_farm_d_tag_idx', 'nostr_event_head_kind_idx') ORDER BY name", + ); + let names = after + .iter() + .map(|row| { + row.get("name") + .and_then(Value::as_str) + .expect("index name should exist") + .to_string() + }) + .collect::<Vec<_>>(); + assert_eq!( + names, + vec![ + "farm_pubkey_d_tag_idx".to_string(), + "gcs_location_geohash_idx".to_string(), + "nostr_event_head_kind_idx".to_string(), + "plot_farm_d_tag_idx".to_string(), + ] + ); + + let trade_product_columns = query_rows(&exec, "PRAGMA table_info(trade_product)"); + assert!(trade_product_columns.iter().any(|row| { + row.get("name") + .and_then(Value::as_str) + .is_some_and(|name| name == "listing_addr") + })); + assert!(trade_product_columns.iter().any(|row| { + row.get("name") + .and_then(Value::as_str) + .is_some_and(|name| name == "primary_bin_id") + })); + assert!(trade_product_columns.iter().any(|row| { + row.get("name") + .and_then(Value::as_str) + .is_some_and(|name| name == "verified_primary_bin_id") + })); +} diff --git a/crates/replica_store/tests/region_scripted_paths.rs b/crates/replica_store/tests/region_scripted_paths.rs @@ -0,0 +1,786 @@ +use std::collections::VecDeque; +use std::sync::Mutex; + +use radroots_replica_schema::ReplicaSchemaError; +use radroots_replica_schema::farm::{ + IFarmCreate, IFarmDelete, IFarmFindMany, IFarmFindOne, IFarmUpdate, +}; +use radroots_replica_schema::farm_gcs_location::{ + IFarmGcsLocationCreate, IFarmGcsLocationDelete, IFarmGcsLocationFindMany, + IFarmGcsLocationFindOne, IFarmGcsLocationUpdate, +}; +use radroots_replica_schema::farm_member::{ + IFarmMemberCreate, IFarmMemberDelete, IFarmMemberFindMany, IFarmMemberFindOne, + IFarmMemberUpdate, +}; +use radroots_replica_schema::farm_member_claim::{ + IFarmMemberClaimCreate, IFarmMemberClaimDelete, IFarmMemberClaimFindMany, + IFarmMemberClaimFindOne, IFarmMemberClaimUpdate, +}; +use radroots_replica_schema::farm_tag::{ + IFarmTagCreate, IFarmTagDelete, IFarmTagFindMany, IFarmTagFindOne, IFarmTagUpdate, +}; +use radroots_replica_schema::gcs_location::{ + GcsLocationFarmArgs, GcsLocationFindManyRel, GcsLocationPlotArgs, GcsLocationTradeProductArgs, + IGcsLocationCreate, IGcsLocationDelete, IGcsLocationFindMany, IGcsLocationFindOne, + IGcsLocationFindOneRelArgs, IGcsLocationUpdate, +}; +use radroots_replica_schema::log_error::{ + ILogErrorCreate, ILogErrorDelete, ILogErrorFindMany, ILogErrorFindOne, ILogErrorUpdate, +}; +use radroots_replica_schema::media_image::{ + IMediaImageCreate, IMediaImageDelete, IMediaImageFindMany, IMediaImageFindOne, + IMediaImageFindOneRelArgs, IMediaImageUpdate, MediaImageFindManyRel, + MediaImageTradeProductArgs, +}; +use radroots_replica_schema::nostr_event_head::{ + INostrEventHeadCreate, INostrEventHeadDelete, INostrEventHeadFindMany, INostrEventHeadFindOne, + INostrEventHeadUpdate, +}; +use radroots_replica_schema::nostr_profile::{ + INostrProfileCreate, INostrProfileDelete, INostrProfileFindMany, INostrProfileFindOne, + INostrProfileFindOneRelArgs, INostrProfileUpdate, NostrProfileFindManyRel, + NostrProfileRelayArgs, +}; +use radroots_replica_schema::nostr_relay::{ + INostrRelayCreate, INostrRelayDelete, INostrRelayFindMany, INostrRelayFindOne, + INostrRelayFindOneRelArgs, INostrRelayUpdate, NostrRelayFindManyRel, NostrRelayProfileArgs, +}; +use radroots_replica_schema::plot::{ + IPlotCreate, IPlotDelete, IPlotFindMany, IPlotFindOne, IPlotUpdate, +}; +use radroots_replica_schema::plot_gcs_location::{ + IPlotGcsLocationCreate, IPlotGcsLocationDelete, IPlotGcsLocationFindMany, + IPlotGcsLocationFindOne, IPlotGcsLocationUpdate, +}; +use radroots_replica_schema::plot_tag::{ + IPlotTagCreate, IPlotTagDelete, IPlotTagFindMany, IPlotTagFindOne, IPlotTagUpdate, +}; +use radroots_replica_schema::trade_product::{ + ITradeProductCreate, ITradeProductFindMany, ITradeProductFindOne, ITradeProductUpdate, +}; +use radroots_replica_store::{ExecOutcome, ReplicaSql, SqlError, SqlExecutor}; +use serde::de::DeserializeOwned; +use serde_json::json; + +struct ScriptedExecutor { + exec_results: Mutex<VecDeque<Result<ExecOutcome, SqlError>>>, + query_results: Mutex<VecDeque<Result<String, SqlError>>>, + begin_results: Mutex<VecDeque<Result<(), SqlError>>>, + commit_results: Mutex<VecDeque<Result<(), SqlError>>>, + rollback_results: Mutex<VecDeque<Result<(), SqlError>>>, +} + +impl ScriptedExecutor { + fn new( + exec_results: Vec<Result<ExecOutcome, SqlError>>, + query_results: Vec<Result<String, SqlError>>, + ) -> Self { + Self { + exec_results: Mutex::new(VecDeque::from(exec_results)), + query_results: Mutex::new(VecDeque::from(query_results)), + begin_results: Mutex::new(VecDeque::new()), + commit_results: Mutex::new(VecDeque::new()), + rollback_results: Mutex::new(VecDeque::new()), + } + } +} + +impl SqlExecutor for ScriptedExecutor { + fn exec(&self, _sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> { + if let Some(result) = self + .exec_results + .lock() + .expect("lock exec queue") + .pop_front() + { + result + } else { + Ok(ExecOutcome { + changes: 1, + last_insert_id: 1, + }) + } + } + + fn query_raw(&self, _sql: &str, _params_json: &str) -> Result<String, SqlError> { + if let Some(result) = self + .query_results + .lock() + .expect("lock query queue") + .pop_front() + { + result + } else { + Ok(String::from("[]")) + } + } + + fn begin(&self) -> Result<(), SqlError> { + if let Some(result) = self + .begin_results + .lock() + .expect("lock begin queue") + .pop_front() + { + result + } else { + Ok(()) + } + } + + fn commit(&self) -> Result<(), SqlError> { + if let Some(result) = self + .commit_results + .lock() + .expect("lock commit queue") + .pop_front() + { + result + } else { + Ok(()) + } + } + + fn rollback(&self) -> Result<(), SqlError> { + if let Some(result) = self + .rollback_results + .lock() + .expect("lock rollback queue") + .pop_front() + { + result + } else { + Ok(()) + } + } +} + +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 err_query() -> Result<String, SqlError> { + Err(SqlError::InvalidQuery(String::from("forced query error"))) +} + +fn bad_json() -> Result<String, SqlError> { + Ok(String::from("{")) +} + +fn ok_rows() -> Result<String, SqlError> { + Ok(String::from("[]")) +} + +fn ok_exec() -> Result<ExecOutcome, SqlError> { + Ok(ExecOutcome { + changes: 1, + last_insert_id: 1, + }) +} + +fn db_with_scripts( + exec_results: Vec<Result<ExecOutcome, SqlError>>, + query_results: Vec<Result<String, SqlError>>, +) -> ReplicaSql<ScriptedExecutor> { + ReplicaSql::new(ScriptedExecutor::new(exec_results, query_results)) +} + +fn assert_replica_schema_error_code<T>( + result: Result<T, ReplicaSchemaError<SqlError>>, + code: &str, +) { + let err = match result { + Ok(_) => panic!("expected ierror"), + Err(err) => err, + }; + assert_eq!(err.err.code(), code); +} + +macro_rules! assert_secondary_model_paths { + ( + $test_name:ident, + $create_ty:ty, $create_json:expr, $create_call:ident, + $find_many_ty:ty, $find_many_json:expr, $find_many_call:ident, + $find_one_ty:ty, $find_one_json:expr, $find_one_call:ident, + $update_ty:ty, $update_id_json:expr, $update_lookup_json:expr, $update_call:ident, + $delete_ty:ty, $delete_lookup_json:expr, $delete_call:ident + ) => { + #[test] + fn $test_name() { + let create_opts: $create_ty = parse_json($create_json); + + let db = db_with_scripts(vec![ok_exec()], vec![err_query()]); + assert_replica_schema_error_code(db.$create_call(&create_opts), "ERR_INVALID_QUERY"); + + let db = db_with_scripts(vec![ok_exec()], vec![ok_rows()]); + assert_replica_schema_error_code(db.$create_call(&create_opts), "ERR_NOT_FOUND"); + + let find_many_opts: $find_many_ty = parse_json($find_many_json); + let db = db_with_scripts(vec![], vec![bad_json()]); + assert_replica_schema_error_code( + db.$find_many_call(&find_many_opts), + "ERR_SERIALIZATION", + ); + + let find_one_opts: $find_one_ty = parse_json($find_one_json); + let db = db_with_scripts(vec![], vec![bad_json()]); + assert_replica_schema_error_code( + db.$find_one_call(&find_one_opts), + "ERR_SERIALIZATION", + ); + + let update_id_opts: $update_ty = parse_json($update_id_json); + let db = db_with_scripts(vec![ok_exec()], vec![err_query()]); + assert_replica_schema_error_code(db.$update_call(&update_id_opts), "ERR_INVALID_QUERY"); + + let db = db_with_scripts(vec![ok_exec()], vec![bad_json()]); + assert_replica_schema_error_code(db.$update_call(&update_id_opts), "ERR_SERIALIZATION"); + + let update_lookup_opts: $update_ty = parse_json($update_lookup_json); + let db = db_with_scripts(vec![], vec![err_query()]); + assert_replica_schema_error_code( + db.$update_call(&update_lookup_opts), + "ERR_INVALID_QUERY", + ); + + let delete_lookup_opts: $delete_ty = parse_json($delete_lookup_json); + let db = db_with_scripts(vec![], vec![err_query()]); + assert_replica_schema_error_code( + db.$delete_call(&delete_lookup_opts), + "ERR_INVALID_QUERY", + ); + } + }; +} + +macro_rules! assert_rel_model_paths { + ( + $test_name:ident, + $create_ty:ty, $create_json:expr, $create_call:ident, + $find_many_ty:ty, $find_many_filter_json:expr, $find_many_rel_expr:expr, $find_many_call:ident, + $find_one_ty:ty, $find_one_on_json:expr, $find_one_rel_expr:expr, $find_one_call:ident, + $update_ty:ty, $update_id_json:expr, $update_lookup_json:expr, $update_call:ident, + $delete_ty:ty, $delete_lookup_json:expr, $delete_rel_expr:expr, $delete_call:ident + ) => { + #[test] + fn $test_name() { + let create_opts: $create_ty = parse_json($create_json); + + let db = db_with_scripts(vec![ok_exec()], vec![err_query()]); + assert_replica_schema_error_code(db.$create_call(&create_opts), "ERR_INVALID_QUERY"); + + let db = db_with_scripts(vec![ok_exec()], vec![ok_rows()]); + assert_replica_schema_error_code(db.$create_call(&create_opts), "ERR_NOT_FOUND"); + + let find_many_filter_opts: $find_many_ty = parse_json($find_many_filter_json); + let db = db_with_scripts(vec![], vec![bad_json()]); + assert_replica_schema_error_code( + db.$find_many_call(&find_many_filter_opts), + "ERR_SERIALIZATION", + ); + + let find_many_rel_opts: $find_many_ty = $find_many_rel_expr; + let db = db_with_scripts(vec![], vec![bad_json()]); + assert_replica_schema_error_code( + db.$find_many_call(&find_many_rel_opts), + "ERR_SERIALIZATION", + ); + + let find_many_rel_opts: $find_many_ty = $find_many_rel_expr; + let db = db_with_scripts(vec![], vec![err_query()]); + assert_replica_schema_error_code( + db.$find_many_call(&find_many_rel_opts), + "ERR_INVALID_QUERY", + ); + + let find_one_on_opts: $find_one_ty = parse_json($find_one_on_json); + let db = db_with_scripts(vec![], vec![bad_json()]); + assert_replica_schema_error_code( + db.$find_one_call(&find_one_on_opts), + "ERR_SERIALIZATION", + ); + + let find_one_rel_opts: $find_one_ty = $find_one_rel_expr; + let db = db_with_scripts(vec![], vec![bad_json()]); + assert_replica_schema_error_code( + db.$find_one_call(&find_one_rel_opts), + "ERR_SERIALIZATION", + ); + + let update_id_opts: $update_ty = parse_json($update_id_json); + let db = db_with_scripts(vec![ok_exec()], vec![err_query()]); + assert_replica_schema_error_code(db.$update_call(&update_id_opts), "ERR_INVALID_QUERY"); + + let db = db_with_scripts(vec![ok_exec()], vec![bad_json()]); + assert_replica_schema_error_code(db.$update_call(&update_id_opts), "ERR_SERIALIZATION"); + + let update_lookup_opts: $update_ty = parse_json($update_lookup_json); + let db = db_with_scripts(vec![], vec![err_query()]); + assert_replica_schema_error_code( + db.$update_call(&update_lookup_opts), + "ERR_INVALID_QUERY", + ); + + let delete_lookup_opts: $delete_ty = parse_json($delete_lookup_json); + let db = db_with_scripts(vec![], vec![err_query()]); + assert_replica_schema_error_code( + db.$delete_call(&delete_lookup_opts), + "ERR_INVALID_QUERY", + ); + + let delete_rel_opts: $delete_ty = $delete_rel_expr; + let db = db_with_scripts(vec![], vec![err_query()]); + assert_replica_schema_error_code( + db.$delete_call(&delete_rel_opts), + "ERR_INVALID_QUERY", + ); + } + }; +} + +macro_rules! assert_trade_product_paths { + ( + $test_name:ident, + $create_ty:ty, $create_json:expr, $create_call:ident, + $find_many_ty:ty, $find_many_json:expr, $find_many_call:ident, + $find_one_ty:ty, $find_one_json:expr, $find_one_call:ident, + $update_ty:ty, $update_json:expr, $update_call:ident + ) => { + #[test] + fn $test_name() { + let create_opts: $create_ty = parse_json($create_json); + + let db = db_with_scripts(vec![ok_exec()], vec![err_query()]); + assert_replica_schema_error_code(db.$create_call(&create_opts), "ERR_INVALID_QUERY"); + + let db = db_with_scripts(vec![ok_exec()], vec![ok_rows()]); + assert_replica_schema_error_code(db.$create_call(&create_opts), "ERR_NOT_FOUND"); + + let find_many_opts: $find_many_ty = parse_json($find_many_json); + let db = db_with_scripts(vec![], vec![bad_json()]); + assert_replica_schema_error_code( + db.$find_many_call(&find_many_opts), + "ERR_SERIALIZATION", + ); + + let find_one_opts: $find_one_ty = parse_json($find_one_json); + let db = db_with_scripts(vec![], vec![bad_json()]); + assert_replica_schema_error_code( + db.$find_one_call(&find_one_opts), + "ERR_SERIALIZATION", + ); + + let update_opts: $update_ty = parse_json($update_json); + let db = db_with_scripts(vec![ok_exec()], vec![err_query()]); + assert_replica_schema_error_code(db.$update_call(&update_opts), "ERR_INVALID_QUERY"); + + let db = db_with_scripts(vec![ok_exec()], vec![bad_json()]); + assert_replica_schema_error_code(db.$update_call(&update_opts), "ERR_SERIALIZATION"); + } + }; +} + +assert_secondary_model_paths!( + farm_scripted_region_paths, + IFarmCreate, + json!({ "d_tag": "farm-a", "pubkey": hex64('a'), "name": "farm a" }), + farm_create, + IFarmFindMany, + json!({ "filter": { "id": "id-1" } }), + farm_find_many, + IFarmFindOne, + json!({ "on": { "id": "id-1" } }), + farm_find_one, + IFarmUpdate, + json!({ "on": { "id": "id-1" }, "fields": { "name": "farm z" } }), + json!({ "on": { "d_tag": "farm-a" }, "fields": { "name": "farm y" } }), + farm_update, + IFarmDelete, + json!({ "on": { "d_tag": "farm-a" } }), + farm_delete +); + +assert_secondary_model_paths!( + plot_scripted_region_paths, + IPlotCreate, + json!({ "d_tag": "plot-a", "farm_id": "farm-1", "name": "plot a" }), + plot_create, + IPlotFindMany, + json!({ "filter": { "id": "id-1" } }), + plot_find_many, + IPlotFindOne, + json!({ "on": { "id": "id-1" } }), + plot_find_one, + IPlotUpdate, + json!({ "on": { "id": "id-1" }, "fields": { "name": "plot z" } }), + json!({ "on": { "d_tag": "plot-a" }, "fields": { "name": "plot y" } }), + plot_update, + IPlotDelete, + json!({ "on": { "d_tag": "plot-a" } }), + plot_delete +); + +assert_secondary_model_paths!( + farm_gcs_location_scripted_region_paths, + IFarmGcsLocationCreate, + json!({ "farm_id": "farm-1", "gcs_location_id": "gcs-1", "role": "primary" }), + farm_gcs_location_create, + IFarmGcsLocationFindMany, + json!({ "filter": { "id": "id-1" } }), + farm_gcs_location_find_many, + IFarmGcsLocationFindOne, + json!({ "on": { "id": "id-1" } }), + farm_gcs_location_find_one, + IFarmGcsLocationUpdate, + json!({ "on": { "id": "id-1" }, "fields": { "role": "z" } }), + json!({ "on": { "farm_id": "farm-1" }, "fields": { "role": "y" } }), + farm_gcs_location_update, + IFarmGcsLocationDelete, + json!({ "on": { "farm_id": "farm-1" } }), + farm_gcs_location_delete +); + +assert_secondary_model_paths!( + plot_gcs_location_scripted_region_paths, + IPlotGcsLocationCreate, + json!({ "plot_id": "plot-1", "gcs_location_id": "gcs-1", "role": "primary" }), + plot_gcs_location_create, + IPlotGcsLocationFindMany, + json!({ "filter": { "id": "id-1" } }), + plot_gcs_location_find_many, + IPlotGcsLocationFindOne, + json!({ "on": { "id": "id-1" } }), + plot_gcs_location_find_one, + IPlotGcsLocationUpdate, + json!({ "on": { "id": "id-1" }, "fields": { "role": "z" } }), + json!({ "on": { "plot_id": "plot-1" }, "fields": { "role": "y" } }), + plot_gcs_location_update, + IPlotGcsLocationDelete, + json!({ "on": { "plot_id": "plot-1" } }), + plot_gcs_location_delete +); + +assert_secondary_model_paths!( + farm_tag_scripted_region_paths, + IFarmTagCreate, + json!({ "farm_id": "farm-1", "tag": "organic" }), + farm_tag_create, + IFarmTagFindMany, + json!({ "filter": { "id": "id-1" } }), + farm_tag_find_many, + IFarmTagFindOne, + json!({ "on": { "id": "id-1" } }), + farm_tag_find_one, + IFarmTagUpdate, + json!({ "on": { "id": "id-1" }, "fields": { "tag": "z" } }), + json!({ "on": { "farm_id": "farm-1" }, "fields": { "tag": "y" } }), + farm_tag_update, + IFarmTagDelete, + json!({ "on": { "farm_id": "farm-1" } }), + farm_tag_delete +); + +assert_secondary_model_paths!( + plot_tag_scripted_region_paths, + IPlotTagCreate, + json!({ "plot_id": "plot-1", "tag": "north" }), + plot_tag_create, + IPlotTagFindMany, + json!({ "filter": { "id": "id-1" } }), + plot_tag_find_many, + IPlotTagFindOne, + json!({ "on": { "id": "id-1" } }), + plot_tag_find_one, + IPlotTagUpdate, + json!({ "on": { "id": "id-1" }, "fields": { "tag": "z" } }), + json!({ "on": { "plot_id": "plot-1" }, "fields": { "tag": "y" } }), + plot_tag_update, + IPlotTagDelete, + json!({ "on": { "plot_id": "plot-1" } }), + plot_tag_delete +); + +assert_secondary_model_paths!( + farm_member_scripted_region_paths, + IFarmMemberCreate, + json!({ "farm_id": "farm-1", "member_pubkey": hex64('b'), "role": "owner" }), + farm_member_create, + IFarmMemberFindMany, + json!({ "filter": { "id": "id-1" } }), + farm_member_find_many, + IFarmMemberFindOne, + json!({ "on": { "id": "id-1" } }), + farm_member_find_one, + IFarmMemberUpdate, + json!({ "on": { "id": "id-1" }, "fields": { "role": "z" } }), + json!({ "on": { "member_pubkey": hex64('b') }, "fields": { "role": "y" } }), + farm_member_update, + IFarmMemberDelete, + json!({ "on": { "member_pubkey": hex64('b') } }), + farm_member_delete +); + +assert_secondary_model_paths!( + farm_member_claim_scripted_region_paths, + IFarmMemberClaimCreate, + json!({ "member_pubkey": hex64('b'), "farm_pubkey": hex64('a') }), + farm_member_claim_create, + IFarmMemberClaimFindMany, + json!({ "filter": { "id": "id-1" } }), + farm_member_claim_find_many, + IFarmMemberClaimFindOne, + json!({ "on": { "id": "id-1" } }), + farm_member_claim_find_one, + IFarmMemberClaimUpdate, + json!({ "on": { "id": "id-1" }, "fields": { "farm_pubkey": hex64('c') } }), + json!({ "on": { "member_pubkey": hex64('b') }, "fields": { "farm_pubkey": hex64('d') } }), + farm_member_claim_update, + IFarmMemberClaimDelete, + json!({ "on": { "member_pubkey": hex64('b') } }), + farm_member_claim_delete +); + +assert_secondary_model_paths!( + log_error_scripted_region_paths, + ILogErrorCreate, + json!({ + "error": "panic", + "message": "boom", + "app_system": "studio", + "app_version": "1.0.0", + "nostr_pubkey": hex64('c') + }), + log_error_create, + ILogErrorFindMany, + json!({ "filter": { "id": "id-1" } }), + log_error_find_many, + ILogErrorFindOne, + json!({ "on": { "id": "id-1" } }), + log_error_find_one, + ILogErrorUpdate, + json!({ "on": { "id": "id-1" }, "fields": { "message": "z" } }), + json!({ "on": { "nostr_pubkey": hex64('c') }, "fields": { "message": "y" } }), + log_error_update, + ILogErrorDelete, + json!({ "on": { "nostr_pubkey": hex64('c') } }), + log_error_delete +); + +assert_secondary_model_paths!( + nostr_event_head_scripted_region_paths, + INostrEventHeadCreate, + 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" + }), + nostr_event_head_create, + INostrEventHeadFindMany, + json!({ "filter": { "id": "id-1" } }), + nostr_event_head_find_many, + INostrEventHeadFindOne, + json!({ "on": { "id": "id-1" } }), + nostr_event_head_find_one, + INostrEventHeadUpdate, + json!({ "on": { "id": "id-1" }, "fields": { "content_hash": "hash-z" } }), + json!({ "on": { "key": "state-a" }, "fields": { "content_hash": "hash-y" } }), + nostr_event_head_update, + INostrEventHeadDelete, + json!({ "on": { "key": "state-a" } }), + nostr_event_head_delete +); + +assert_rel_model_paths!( + gcs_location_scripted_region_paths, + IGcsLocationCreate, + 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))" + }), + gcs_location_create, + IGcsLocationFindMany, + json!({ "filter": { "id": "id-1" } }), + IGcsLocationFindMany::Rel { + rel: GcsLocationFindManyRel::OnFarm(GcsLocationFarmArgs { + id: String::from("farm-1") + }) + }, + gcs_location_find_many, + IGcsLocationFindOne, + json!({ "on": { "id": "id-1" } }), + IGcsLocationFindOne::Rel(IGcsLocationFindOneRelArgs { + rel: GcsLocationFindManyRel::OffTradeProduct(GcsLocationTradeProductArgs { + id: String::from("tp-1") + }) + }), + gcs_location_find_one, + IGcsLocationUpdate, + json!({ "on": { "id": "id-1" }, "fields": { "label": "z" } }), + json!({ "on": { "d_tag": "gcs-a" }, "fields": { "label": "y" } }), + gcs_location_update, + IGcsLocationDelete, + json!({ "on": { "d_tag": "gcs-a" } }), + IGcsLocationDelete::Rel(IGcsLocationFindOneRelArgs { + rel: GcsLocationFindManyRel::OnPlot(GcsLocationPlotArgs { + id: String::from("plot-1") + }) + }), + gcs_location_delete +); + +assert_rel_model_paths!( + media_image_scripted_region_paths, + IMediaImageCreate, + json!({ + "file_path": "/img/a.jpg", + "mime_type": "image/jpeg", + "res_base": "https://cdn.example.com", + "res_path": "img/a.jpg" + }), + media_image_create, + IMediaImageFindMany, + json!({ "filter": { "id": "id-1" } }), + IMediaImageFindMany::Rel { + rel: MediaImageFindManyRel::OnTradeProduct(MediaImageTradeProductArgs { + id: String::from("tp-1") + }) + }, + media_image_find_many, + IMediaImageFindOne, + json!({ "on": { "id": "id-1" } }), + IMediaImageFindOne::Rel(IMediaImageFindOneRelArgs { + rel: MediaImageFindManyRel::OffTradeProduct(MediaImageTradeProductArgs { + id: String::from("tp-1") + }) + }), + media_image_find_one, + IMediaImageUpdate, + json!({ "on": { "id": "id-1" }, "fields": { "label": "z" } }), + json!({ "on": { "file_path": "/img/a.jpg" }, "fields": { "label": "y" } }), + media_image_update, + IMediaImageDelete, + json!({ "on": { "file_path": "/img/a.jpg" } }), + IMediaImageDelete::Rel(IMediaImageFindOneRelArgs { + rel: MediaImageFindManyRel::OnTradeProduct(MediaImageTradeProductArgs { + id: String::from("tp-1") + }) + }), + media_image_delete +); + +assert_rel_model_paths!( + nostr_profile_scripted_region_paths, + INostrProfileCreate, + json!({ "public_key": hex64('d'), "profile_type": "farm", "name": "profile a" }), + nostr_profile_create, + INostrProfileFindMany, + json!({ "filter": { "id": "id-1" } }), + INostrProfileFindMany::Rel { + rel: NostrProfileFindManyRel::OnRelay(NostrProfileRelayArgs { + id: String::from("relay-1") + }) + }, + nostr_profile_find_many, + INostrProfileFindOne, + json!({ "on": { "id": "id-1" } }), + INostrProfileFindOne::Rel(INostrProfileFindOneRelArgs { + rel: NostrProfileFindManyRel::OffRelay(NostrProfileRelayArgs { + id: String::from("relay-1") + }) + }), + nostr_profile_find_one, + INostrProfileUpdate, + json!({ "on": { "id": "id-1" }, "fields": { "name": "z" } }), + json!({ "on": { "public_key": hex64('d') }, "fields": { "name": "y" } }), + nostr_profile_update, + INostrProfileDelete, + json!({ "on": { "public_key": hex64('d') } }), + INostrProfileDelete::Rel(INostrProfileFindOneRelArgs { + rel: NostrProfileFindManyRel::OnRelay(NostrProfileRelayArgs { + id: String::from("relay-1") + }) + }), + nostr_profile_delete +); + +assert_rel_model_paths!( + nostr_relay_scripted_region_paths, + INostrRelayCreate, + json!({ "url": "wss://relay.example.com" }), + nostr_relay_create, + INostrRelayFindMany, + json!({ "filter": { "id": "id-1" } }), + INostrRelayFindMany::Rel { + rel: NostrRelayFindManyRel::OnProfile(NostrRelayProfileArgs { + public_key: hex64('d') + }) + }, + nostr_relay_find_many, + INostrRelayFindOne, + json!({ "on": { "id": "id-1" } }), + INostrRelayFindOne::Rel(INostrRelayFindOneRelArgs { + rel: NostrRelayFindManyRel::OffProfile(NostrRelayProfileArgs { + public_key: hex64('d') + }) + }), + nostr_relay_find_one, + INostrRelayUpdate, + json!({ "on": { "id": "id-1" }, "fields": { "name": "z" } }), + json!({ "on": { "url": "wss://relay.example.com" }, "fields": { "name": "y" } }), + nostr_relay_update, + INostrRelayDelete, + json!({ "on": { "url": "wss://relay.example.com" } }), + INostrRelayDelete::Rel(INostrRelayFindOneRelArgs { + rel: NostrRelayFindManyRel::OnProfile(NostrRelayProfileArgs { + public_key: hex64('d') + }) + }), + nostr_relay_delete +); + +assert_trade_product_paths!( + trade_product_scripted_region_paths, + ITradeProductCreate, + json!({ + "key": "product-a", + "category": "coffee", + "title": "coffee a", + "summary": "summary", + "process": "washed", + "lot": "lot-a", + "profile": "floral", + "year": 2024, + "qty_amt": 100, + "qty_amt_exact": "100", + "qty_unit": "kg", + "price_amt": 7.5, + "price_amt_exact": "7.5", + "price_currency": "USD", + "price_qty_amt": 1, + "price_qty_amt_exact": "1", + "price_qty_unit": "kg" + }), + trade_product_create, + ITradeProductFindMany, + json!({ "filter": { "id": "id-1" } }), + trade_product_find_many, + ITradeProductFindOne, + json!({ "on": { "id": "id-1" } }), + trade_product_find_one, + ITradeProductUpdate, + json!({ "on": { "id": "id-1" }, "fields": { "title": "z" } }), + trade_product_update +); diff --git a/crates/replica_sync/Cargo.toml b/crates/replica_sync/Cargo.toml @@ -34,8 +34,8 @@ radroots_event_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_replica_schema = { workspace = true } +radroots_replica_store = { workspace = true } hex = { workspace = true } serde = { workspace = true, default-features = false, features = [ "alloc", @@ -50,7 +50,7 @@ uuid = { workspace = true, optional = true } [dev-dependencies] radroots_sql_core = { workspace = true, features = ["native"] } -radroots_replica_db = { workspace = true } +radroots_replica_store = { workspace = true } serde_json = { workspace = true } [lints.rust] diff --git a/crates/replica_sync/src/emit.rs b/crates/replica_sync/src/emit.rs @@ -23,34 +23,34 @@ use radroots_event_codec::farm::list_sets as farm_list_sets; use radroots_event_codec::list_set::encode as list_set_encode; use radroots_event_codec::plot::encode as plot_encode; use radroots_event_codec::wire::WireEventParts; -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::{ +use radroots_replica_schema::farm::{ Farm, IFarmFieldsFilter, IFarmFindMany, IFarmFindOne, IFarmFindOneArgs, }; -use radroots_replica_db_schema::farm_gcs_location::{ +use radroots_replica_schema::farm_gcs_location::{ FarmGcsLocation, IFarmGcsLocationFieldsFilter, IFarmGcsLocationFindMany, }; -use radroots_replica_db_schema::farm_member::{ +use radroots_replica_schema::farm_member::{ FarmMember, IFarmMemberFieldsFilter, IFarmMemberFindMany, }; -use radroots_replica_db_schema::farm_member_claim::{ +use radroots_replica_schema::farm_member_claim::{ FarmMemberClaim, IFarmMemberClaimFieldsFilter, IFarmMemberClaimFindMany, }; -use radroots_replica_db_schema::farm_tag::{IFarmTagFieldsFilter, IFarmTagFindMany}; -use radroots_replica_db_schema::gcs_location::{ +use radroots_replica_schema::farm_tag::{IFarmTagFieldsFilter, IFarmTagFindMany}; +use radroots_replica_schema::gcs_location::{ GcsLocation, GcsLocationQueryBindValues, IGcsLocationFindOne, IGcsLocationFindOneArgs, }; -use radroots_replica_db_schema::nostr_profile::{ +use radroots_replica_schema::nostr_profile::{ INostrProfileFindOne, INostrProfileFindOneArgs, NostrProfileQueryBindValues, }; -use radroots_replica_db_schema::plot::{IPlotFieldsFilter, IPlotFindMany, Plot}; -use radroots_replica_db_schema::plot_gcs_location::{ +use radroots_replica_schema::plot::{IPlotFieldsFilter, IPlotFindMany, Plot}; +use radroots_replica_schema::plot_gcs_location::{ IPlotGcsLocationFieldsFilter, IPlotGcsLocationFindMany, PlotGcsLocation, }; -use radroots_replica_db_schema::plot_tag::{IPlotTagFieldsFilter, IPlotTagFindMany}; +use radroots_replica_schema::plot_tag::{IPlotTagFieldsFilter, IPlotTagFindMany}; +use radroots_replica_store::{ + farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, nostr_profile, + plot, plot_gcs_location, plot_tag, +}; use radroots_sql_core::SqlExecutor; use serde_json::Value; @@ -289,7 +289,7 @@ fn resolve_farm( let result_query = farm::find_one( exec, &IFarmFindOne::On(IFarmFindOneArgs { - on: radroots_replica_db_schema::farm::FarmQueryBindValues::Id { id: id.clone() }, + on: radroots_replica_schema::farm::FarmQueryBindValues::Id { id: id.clone() }, }), ); let result = result_query?; @@ -754,10 +754,8 @@ fn parse_polygon(value: &str, lat: f64, lng: f64) -> RadrootsGeoJsonPolygon { fn load_profile( exec: &dyn SqlExecutor, pubkey: &str, -) -> Result< - Option<radroots_replica_db_schema::nostr_profile::NostrProfile>, - RadrootsReplicaEventsError, -> { +) -> Result<Option<radroots_replica_schema::nostr_profile::NostrProfile>, RadrootsReplicaEventsError> +{ let result_query = nostr_profile::find_one( exec, &INostrProfileFindOne::On(INostrProfileFindOneArgs { @@ -772,7 +770,7 @@ fn load_profile( fn profile_event( pubkey: &str, - profile: radroots_replica_db_schema::nostr_profile::NostrProfile, + profile: radroots_replica_schema::nostr_profile::NostrProfile, ) -> Result<RadrootsReplicaEventDraft, RadrootsReplicaEventsError> { let profile_type = match profile.profile_type.as_str() { "individual" | "farmer" => Some(RadrootsProfileType::Individual), @@ -921,24 +919,24 @@ fn parts_to_draft(author: &str, parts: WireEventParts) -> RadrootsReplicaEventDr #[cfg(test)] mod tests { use super::*; - 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::{ + use radroots_replica_schema::farm::{IFarmFields, IFarmFieldsFilter, IFarmFindMany}; + use radroots_replica_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::{ + use radroots_replica_schema::farm_member::IFarmMemberFields; + use radroots_replica_schema::farm_member_claim::IFarmMemberClaimFields; + use radroots_replica_schema::farm_tag::IFarmTagFields; + use radroots_replica_schema::gcs_location::IGcsLocationFields; + use radroots_replica_schema::nostr_profile::INostrProfileFields; + use radroots_replica_schema::plot::{IPlotFields, IPlotFindMany}; + use radroots_replica_schema::plot_gcs_location::{ IPlotGcsLocationFields, IPlotGcsLocationFindMany, }; - use radroots_replica_db_schema::plot_tag::IPlotTagFields; + use radroots_replica_schema::plot_tag::IPlotTagFields; + use radroots_replica_store::{ + farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, + migrations, nostr_profile, plot, plot_gcs_location, plot_tag, + }; use radroots_sql_core::{ExecOutcome, SqlError, SqlExecutor, SqliteExecutor}; struct ErrorExecutor; @@ -1507,7 +1505,7 @@ mod tests { let profile_event_farm = profile_event( &farm_row.pubkey, - radroots_replica_db_schema::nostr_profile::NostrProfile { + radroots_replica_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(), @@ -1528,7 +1526,7 @@ mod tests { assert!(!profile_event_farm.tags.is_empty()); let profile_event_unknown = profile_event( &"6".repeat(64), - radroots_replica_db_schema::nostr_profile::NostrProfile { + radroots_replica_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(), @@ -1754,7 +1752,7 @@ mod tests { let profile_coop = profile_event( &"c".repeat(64), - radroots_replica_db_schema::nostr_profile::NostrProfile { + radroots_replica_schema::nostr_profile::NostrProfile { id: "00000000-0000-0000-0000-0000000000c0".to_string(), created_at: "2024-01-01T00:00:00.000Z".to_string(), updated_at: "2024-01-01T00:00:00.000Z".to_string(), @@ -1776,7 +1774,7 @@ mod tests { let profile_any = profile_event( &"a".repeat(64), - radroots_replica_db_schema::nostr_profile::NostrProfile { + radroots_replica_schema::nostr_profile::NostrProfile { id: "00000000-0000-0000-0000-0000000000a0".to_string(), created_at: "2024-01-01T00:00:00.000Z".to_string(), updated_at: "2024-01-01T00:00:00.000Z".to_string(), diff --git a/crates/replica_sync/src/error.rs b/crates/replica_sync/src/error.rs @@ -4,7 +4,7 @@ use alloc::string::{String, ToString}; use core::fmt; use radroots_event_codec::error::{EventEncodeError, EventParseError}; -use radroots_replica_db_schema::ReplicaSchemaError; +use radroots_replica_schema::ReplicaSchemaError; use radroots_sql_core::error::SqlError; #[derive(Debug)] @@ -53,7 +53,7 @@ impl From<EventParseError> for RadrootsReplicaEventsError { mod tests { use super::RadrootsReplicaEventsError; use radroots_event_codec::error::{EventEncodeError, EventParseError}; - use radroots_replica_db_schema::ReplicaSchemaError; + use radroots_replica_schema::ReplicaSchemaError; use radroots_sql_core::error::SqlError; #[test] diff --git a/crates/replica_sync/src/ingest.rs b/crates/replica_sync/src/ingest.rs @@ -30,61 +30,61 @@ use radroots_event_codec::list_set::decode as list_set_decode; use radroots_event_codec::listing::decode as listing_decode; use radroots_event_codec::plot::decode as plot_decode; use radroots_event_codec::profile::decode as profile_decode; -#[cfg(test)] -use radroots_replica_db::farm_gcs_location; -use radroots_replica_db::{ - farm, farm_member, farm_member_claim, farm_tag, gcs_location, nostr_event_head, nostr_profile, - plot, plot_gcs_location, plot_tag, trade_product, -}; -use radroots_replica_db_schema::ReplicaSchemaError; -use radroots_replica_db_schema::farm::{ +use radroots_replica_schema::ReplicaSchemaError; +use radroots_replica_schema::farm::{ FarmQueryBindValues, IFarmFields, IFarmFieldsFilter, IFarmFieldsPartial, IFarmFindMany, IFarmUpdate, }; #[cfg(test)] -use radroots_replica_db_schema::farm_gcs_location::{ +use radroots_replica_schema::farm_gcs_location::{ FarmGcsLocationQueryBindValues, IFarmGcsLocationDelete, IFarmGcsLocationFieldsFilter, IFarmGcsLocationFindMany, IFarmGcsLocationFindOneArgs, }; -use radroots_replica_db_schema::farm_member::{ +use radroots_replica_schema::farm_member::{ FarmMemberQueryBindValues, IFarmMemberDelete, IFarmMemberFields, IFarmMemberFieldsFilter, IFarmMemberFindMany, IFarmMemberFindOneArgs, }; -use radroots_replica_db_schema::farm_member_claim::{ +use radroots_replica_schema::farm_member_claim::{ FarmMemberClaimQueryBindValues, IFarmMemberClaimDelete, IFarmMemberClaimFields, IFarmMemberClaimFieldsFilter, IFarmMemberClaimFindMany, IFarmMemberClaimFindOneArgs, }; -use radroots_replica_db_schema::farm_tag::{ +use radroots_replica_schema::farm_tag::{ FarmTagQueryBindValues, IFarmTagDelete, IFarmTagFields, IFarmTagFieldsFilter, IFarmTagFindMany, IFarmTagFindOneArgs, }; -use radroots_replica_db_schema::gcs_location::IGcsLocationFields; -use radroots_replica_db_schema::nostr_event_head::{ +use radroots_replica_schema::gcs_location::IGcsLocationFields; +use radroots_replica_schema::nostr_event_head::{ INostrEventHeadFields, INostrEventHeadFieldsPartial, INostrEventHeadFindOne, INostrEventHeadFindOneArgs, INostrEventHeadUpdate, NostrEventHead, NostrEventHeadQueryBindValues, }; -use radroots_replica_db_schema::nostr_profile::{ +use radroots_replica_schema::nostr_profile::{ INostrProfileFields, INostrProfileFieldsPartial, INostrProfileFindOne, INostrProfileFindOneArgs, INostrProfileUpdate, NostrProfileQueryBindValues, }; -use radroots_replica_db_schema::plot::{ +use radroots_replica_schema::plot::{ IPlotFields, IPlotFieldsFilter, IPlotFieldsPartial, IPlotFindMany, IPlotUpdate, PlotQueryBindValues, }; -use radroots_replica_db_schema::plot_gcs_location::{ +use radroots_replica_schema::plot_gcs_location::{ IPlotGcsLocationDelete, IPlotGcsLocationFields, IPlotGcsLocationFieldsFilter, IPlotGcsLocationFindMany, IPlotGcsLocationFindOneArgs, PlotGcsLocationQueryBindValues, }; -use radroots_replica_db_schema::plot_tag::{ +use radroots_replica_schema::plot_tag::{ IPlotTagDelete, IPlotTagFields, IPlotTagFieldsFilter, IPlotTagFindMany, IPlotTagFindOneArgs, PlotTagQueryBindValues, }; -use radroots_replica_db_schema::trade_product::{ +use radroots_replica_schema::trade_product::{ ITradeProductFields, ITradeProductFieldsFilter, ITradeProductFieldsPartial, ITradeProductFindMany, ITradeProductFindOne, ITradeProductFindOneArgs, ITradeProductUpdate, TradeProductQueryBindValues, }; +#[cfg(test)] +use radroots_replica_store::farm_gcs_location; +use radroots_replica_store::{ + farm, farm_member, farm_member_claim, farm_tag, gcs_location, nostr_event_head, nostr_profile, + plot, plot_gcs_location, plot_tag, trade_product, +}; use radroots_sql_core::SqlExecutor; use radroots_sql_core::error::SqlError; use serde_json::{Value, json}; @@ -1005,7 +1005,7 @@ fn find_farm_by_ref( exec: &dyn SqlExecutor, pubkey: &str, d_tag: &str, -) -> Result<radroots_replica_db_schema::farm::Farm, RadrootsReplicaEventsError> { +) -> Result<radroots_replica_schema::farm::Farm, RadrootsReplicaEventsError> { let filter = IFarmFieldsFilter { id: None, created_at: None, @@ -1526,20 +1526,20 @@ mod tests { use radroots_event_codec::farm::list_sets as farm_list_sets; use radroots_event_codec::list_set::encode as list_set_encode; use radroots_event_codec::plot::encode as plot_encode; - use radroots_replica_db::{ + use radroots_replica_schema::farm::IFarmFields; + use radroots_replica_schema::farm_gcs_location::IFarmGcsLocationFields; + use radroots_replica_schema::farm_member::IFarmMemberFields; + use radroots_replica_schema::farm_member_claim::IFarmMemberClaimFields; + use radroots_replica_schema::farm_tag::IFarmTagFields; + use radroots_replica_schema::gcs_location::IGcsLocationFields; + use radroots_replica_schema::plot::IPlotFields; + use radroots_replica_schema::plot_gcs_location::IPlotGcsLocationFields; + use radroots_replica_schema::plot_tag::IPlotTagFields; + use radroots_replica_store::{ ReplicaSql, farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, migrations, nostr_event_head, plot, plot_gcs_location, plot_tag, trade_product, }; - 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; use radroots_sql_core::{ExecOutcome, SqlExecutor, SqliteExecutor}; struct FixedFactory; diff --git a/crates/replica_sync/src/sync_state.rs b/crates/replica_sync/src/sync_state.rs @@ -3,8 +3,8 @@ use alloc::{collections::BTreeMap, string::String, string::ToString, vec::Vec}; #[cfg(feature = "std")] use std::collections::BTreeMap; -use radroots_replica_db_schema::farm::IFarmFindMany; -use radroots_replica_db_schema::nostr_event_head::INostrEventHeadFindMany; +use radroots_replica_schema::farm::IFarmFindMany; +use radroots_replica_schema::nostr_event_head::INostrEventHeadFindMany; use radroots_sql_core::SqlExecutor; use crate::error::RadrootsReplicaEventsError; @@ -48,7 +48,7 @@ pub fn radroots_replica_pending_publish_batch<E: SqlExecutor>( exec: &E, ) -> Result<RadrootsReplicaPendingPublishBatch, RadrootsReplicaEventsError> { let farms = - radroots_replica_db::farm::find_many(exec, &IFarmFindMany { filter: None })?.results; + radroots_replica_store::farm::find_many(exec, &IFarmFindMany { filter: None })?.results; let mut expected: BTreeMap<String, RadrootsReplicaPendingPublishEvent> = BTreeMap::new(); for farm in farms { @@ -75,7 +75,7 @@ pub fn radroots_replica_pending_publish_batch<E: SqlExecutor>( } } - let states_query = radroots_replica_db::nostr_event_head::find_many( + let states_query = radroots_replica_store::nostr_event_head::find_many( exec, &INostrEventHeadFindMany { filter: None }, ); @@ -123,9 +123,9 @@ mod tests { event_content_hash, event_content_hash_fail_next, event_head_key, tag_value, }; use crate::types::RadrootsReplicaFarmSelector; - use radroots_replica_db::{farm, migrations, nostr_event_head}; - use radroots_replica_db_schema::farm::IFarmFields; - use radroots_replica_db_schema::nostr_event_head::INostrEventHeadFields; + use radroots_replica_schema::farm::IFarmFields; + use radroots_replica_schema::nostr_event_head::INostrEventHeadFields; + use radroots_replica_store::{farm, migrations, nostr_event_head}; use radroots_sql_core::{SqlExecutor, SqliteExecutor}; #[test] diff --git a/crates/replica_sync/src/tests.rs b/crates/replica_sync/src/tests.rs @@ -4,21 +4,21 @@ use crate::{ }; use radroots_event::gcs::{RadrootsGeoJsonPoint, RadrootsGeoJsonPolygon}; use radroots_event::kinds::{KIND_FARM, KIND_LIST_SET_GENERIC, KIND_PLOT, KIND_PROFILE}; -use radroots_replica_db::{ +use radroots_replica_schema::ReplicaSchemaError; +use radroots_replica_schema::farm::IFarmFields; +use radroots_replica_schema::farm_gcs_location::IFarmGcsLocationFields; +use radroots_replica_schema::farm_member::IFarmMemberFields; +use radroots_replica_schema::farm_member_claim::IFarmMemberClaimFields; +use radroots_replica_schema::farm_tag::IFarmTagFields; +use radroots_replica_schema::gcs_location::IGcsLocationFields; +use radroots_replica_schema::nostr_profile::INostrProfileFields; +use radroots_replica_schema::plot::IPlotFields; +use radroots_replica_schema::plot_gcs_location::IPlotGcsLocationFields; +use radroots_replica_schema::plot_tag::IPlotTagFields; +use radroots_replica_store::{ 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::ReplicaSchemaError; -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_sql_core::SqliteExecutor; use radroots_sql_core::error::SqlError; use std::panic; diff --git a/crates/replica_sync/tests/ingest_roundtrip.rs b/crates/replica_sync/tests/ingest_roundtrip.rs @@ -16,28 +16,24 @@ use radroots_event_codec::farm::encode as farm_encode; use radroots_event_codec::farm::list_sets as farm_list_sets; use radroots_event_codec::list_set::encode as list_set_encode; use radroots_event_codec::plot::encode as plot_encode; -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::ReplicaSchemaError; -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::{ +use radroots_replica_schema::ReplicaSchemaError; +use radroots_replica_schema::farm::{IFarmFields, IFarmFieldsFilter, IFarmFindMany}; +use radroots_replica_schema::farm_gcs_location::IFarmGcsLocationFields; +use radroots_replica_schema::farm_member::{ IFarmMemberFields, IFarmMemberFieldsFilter, IFarmMemberFindMany, }; -use radroots_replica_db_schema::farm_member_claim::{ +use radroots_replica_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_schema::farm_tag::{IFarmTagFields, IFarmTagFieldsFilter, IFarmTagFindMany}; +use radroots_replica_schema::gcs_location::IGcsLocationFields; +use radroots_replica_schema::nostr_profile::INostrProfileFields; +use radroots_replica_schema::plot::IPlotFields; +use radroots_replica_schema::plot_gcs_location::IPlotGcsLocationFields; +use radroots_replica_schema::plot_tag::{IPlotTagFields, IPlotTagFieldsFilter, IPlotTagFindMany}; +use radroots_replica_store::{ + 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_sync::{ RADROOTS_REPLICA_TRANSFER_VERSION, RadrootsReplicaEventDraft, RadrootsReplicaEventsError, @@ -1396,7 +1392,7 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() { let plot_rows = unwrap_sql( plot::find_many( &exec, - &radroots_replica_db_schema::plot::IPlotFindMany { filter: None }, + &radroots_replica_schema::plot::IPlotFindMany { filter: None }, ), "plot rows", ) diff --git a/crates/sp1_guest_trade/Cargo.toml b/crates/sp1_guest_trade/Cargo.toml @@ -1,36 +0,0 @@ -[package] -name = "radroots_sp1_guest_trade" -publish = false -version = "0.1.0-alpha.2" -edition.workspace = true -authors = ["Tyson Lupul <tyson@radroots.org>"] -rust-version.workspace = true -license.workspace = true -description = "SP1 guest model for Radroots trade verification" -repository.workspace = true -homepage.workspace = true - -[[bin]] -name = "radroots_sp1_trade_order_acceptance_guest" -path = "src/bin/order_acceptance_guest.rs" -required-features = ["sp1_guest"] -test = false - -[features] -default = [] -sp1_guest = ["dep:sp1-zkvm"] - -[dependencies] -serde = { workspace = true, default-features = false, features = [ - "alloc", - "derive", -] } -serde_json = { workspace = true, default-features = false, features = [ - "alloc", -] } -sha2 = { workspace = true, default-features = false } -sp1-zkvm = { workspace = true, optional = true } -thiserror = { workspace = true } - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/sp1_guest_trade/src/bin/order_acceptance_guest.rs b/crates/sp1_guest_trade/src/bin/order_acceptance_guest.rs @@ -1,14 +0,0 @@ -#![cfg_attr(target_os = "zkvm", no_main)] - -use radroots_sp1_guest_trade::{ - RadrootsSp1TradeOrderAcceptanceWitness, reduce_order_acceptance_canonical_public_values, -}; - -sp1_zkvm::entrypoint!(main); - -fn main() { - let witness = sp1_zkvm::io::read::<RadrootsSp1TradeOrderAcceptanceWitness>(); - let public_values = reduce_order_acceptance_canonical_public_values(&witness) - .expect("valid radroots order acceptance witness"); - sp1_zkvm::io::commit(&public_values); -} diff --git a/crates/sp1_host_trade/Cargo.toml b/crates/sp1_host_trade/Cargo.toml @@ -1,43 +0,0 @@ -[package] -name = "radroots_sp1_host_trade" -publish = false -version = "0.1.0-alpha.2" -edition.workspace = true -build = "build.rs" -authors = ["Tyson Lupul <tyson@radroots.org>"] -rust-version.workspace = true -license.workspace = true -description = "SP1 host model for Radroots trade verification" -repository.workspace = true -homepage.workspace = true - -[features] -default = [] -sp1_verify = ["dep:bincode", "dep:sp1-build", "dep:sp1-sdk"] -sp1_proving = ["sp1_verify"] -sp1_cuda = ["dep:futures", "sp1_proving", "sp1-sdk/cuda"] - -[dependencies] -base64 = { workspace = true } -bincode = { workspace = true, optional = true } -futures = { workspace = true, optional = true } -radroots_sp1_guest_trade = { workspace = true } -radroots_trade = { workspace = true, default-features = false, features = [ - "serde_json", - "std", -] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -sha2 = { workspace = true } -sp1-sdk = { workspace = true, optional = true } -thiserror = { workspace = true } - -[build-dependencies] -sp1-build = { workspace = true, optional = true } - -[dev-dependencies] -radroots_event = { workspace = true, features = ["serde"] } -tokio = { workspace = true, features = ["rt-multi-thread"] } - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/sp1_host_trade/build.rs b/crates/sp1_host_trade/build.rs @@ -1,26 +0,0 @@ -fn main() { - let build_guest_elf_env = "RADROOTS_SP1_HOST_TRADE_BUILD_GUEST_ELF"; - let run_real_proof_tests_env = "RADROOTS_SP1_HOST_TRADE_RUN_REAL_PROOF_TESTS"; - println!("cargo:rerun-if-env-changed=CARGO_FEATURE_SP1_VERIFY"); - println!("cargo:rerun-if-env-changed={build_guest_elf_env}"); - println!("cargo:rerun-if-env-changed={run_real_proof_tests_env}"); - println!("cargo:rustc-check-cfg=cfg(radroots_sp1_guest_elf)"); - println!("cargo:rustc-check-cfg=cfg(radroots_sp1_real_proof_tests)"); - if std::env::var(run_real_proof_tests_env).as_deref() == Ok("1") { - println!("cargo:rustc-cfg=radroots_sp1_real_proof_tests"); - } - if std::env::var(build_guest_elf_env).as_deref() != Ok("1") { - return; - } - #[cfg(feature = "sp1_verify")] - { - let args = sp1_build::BuildArgs { - binaries: vec!["radroots_sp1_trade_order_acceptance_guest".to_string()], - features: vec!["sp1_guest".to_string()], - locked: true, - ..sp1_build::BuildArgs::default() - }; - sp1_build::build_program_with_args("../sp1_guest_trade", args); - println!("cargo:rustc-cfg=radroots_sp1_guest_elf"); - } -} diff --git a/crates/sp1_host_trade/src/lib.rs b/crates/sp1_host_trade/src/lib.rs @@ -1,3111 +0,0 @@ -#![cfg_attr(coverage_nightly, feature(coverage_attribute))] -#![forbid(unsafe_code)] - -use base64::Engine; -use radroots_sp1_guest_trade::{ - RadrootsSp1TradeGuestError, RadrootsSp1TradeOrderAcceptanceWitness, - RadrootsSp1TradeProofResult, RadrootsSp1TradePublicValuesExecution, - reduce_order_acceptance_public_values, -}; -#[cfg(feature = "sp1_verify")] -use radroots_sp1_guest_trade::{ - RadrootsSp1TradeProofPublicValues, RadrootsSp1TradeProofStatementType, -}; -use radroots_trade::validation_receipt::{ - RadrootsTradeValidationReceipt, RadrootsValidationReceiptProof, - RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, - RadrootsValidationReceiptStatement, RadrootsValidationReceiptType, VALIDATION_RECEIPT_DOMAIN, - VALIDATION_RECEIPT_PROOF_REFERENCE_SHA256_PREFIX, VALIDATION_RECEIPT_VERSION, -}; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use thiserror::Error; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RadrootsSp1TradeProofMode { - None, - Core, - Compressed, - Groth16, - Plonk, -} - -impl RadrootsSp1TradeProofMode { - pub const fn proof_system(self) -> RadrootsValidationReceiptProofSystem { - match self { - Self::None => RadrootsValidationReceiptProofSystem::None, - Self::Core => RadrootsValidationReceiptProofSystem::Sp1Core, - Self::Compressed => RadrootsValidationReceiptProofSystem::Sp1Compressed, - Self::Groth16 => RadrootsValidationReceiptProofSystem::Sp1Groth16, - Self::Plonk => RadrootsValidationReceiptProofSystem::Sp1Plonk, - } - } - - pub const fn mode_label(self) -> Option<&'static str> { - match self { - Self::None => None, - Self::Core => Some("core"), - Self::Compressed => Some("compressed"), - Self::Groth16 => Some("groth16"), - Self::Plonk => Some("plonk"), - } - } - - pub fn from_label(value: &str) -> Option<Self> { - match value { - "none" => Some(Self::None), - "core" => Some(Self::Core), - "compressed" => Some(Self::Compressed), - "groth16" => Some(Self::Groth16), - "plonk" => Some(Self::Plonk), - _ => None, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct RadrootsSp1TradeProofArtifact { - pub inline_proof_base64: Option<String>, - pub mode: Option<String>, - pub program_hash: Option<String>, - pub proof_digest: String, - pub proof_reference: Option<String>, - pub public_values_hash: String, - pub system: RadrootsValidationReceiptProofSystem, - pub verifying_key_hash: Option<String>, -} - -pub const RADROOTS_SP1_TRADE_PROOF_ARTIFACT_SCHEMA_VERSION: u32 = 1; -pub const RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION: u32 = 1; -pub const RADROOTS_SP1_TRADE_SP1_VERSION_LINE: &str = "sp1-sdk-6.2.1"; -pub const RADROOTS_SP1_TRADE_PROOF_CODEC: &str = "sp1-proof-with-public-values-bincode"; -pub const RADROOTS_SP1_TRADE_VERIFYING_KEY_CODEC: &str = "sp1-verifying-key-bincode"; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct RadrootsSp1TradeProofEnvelope { - pub schema_version: u32, - pub sp1_version_line: String, - pub proof_system: String, - pub proof_mode: String, - pub proof_codec: String, - pub proof_content_hash: String, - pub proof_digest: String, - pub public_values_hash: String, - pub canonical_public_values_hash: String, - pub sp1_program_hash: String, - pub sp1_verifying_key_hash: String, - pub sp1_verifying_key_codec: String, - pub sp1_verifying_key_base64: String, - pub receipt_type: String, - pub receipt_result: String, - pub listing_event_id: String, - pub root_event_id: String, - pub target_event_id: String, - pub event_set_root: String, - pub previous_state_root: String, - pub new_state_root: String, - pub changed_records_root: String, - pub error_bitmap: String, - pub proof_content_base64: String, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct RadrootsSp1TradeResolvedProofArtifact { - pub artifact: RadrootsSp1TradeProofArtifact, - #[serde(default)] - pub resolved_proof_envelope_base64: Option<String>, -} - -impl RadrootsSp1TradeResolvedProofArtifact { - pub fn inline(artifact: RadrootsSp1TradeProofArtifact) -> Self { - Self { - artifact, - resolved_proof_envelope_base64: None, - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RadrootsSp1TradeProverBackend { - Disabled, - DeterministicNone, - LocalExecute, - LocalCpuProve, - LocalCudaProve, - RemoteHttpProve, -} - -impl RadrootsSp1TradeProverBackend { - pub const fn as_str(self) -> &'static str { - match self { - Self::Disabled => "disabled", - Self::DeterministicNone => "deterministic_none", - Self::LocalExecute => "local_execute", - Self::LocalCpuProve => "local_cpu_prove", - Self::LocalCudaProve => "local_cuda_prove", - Self::RemoteHttpProve => "remote_http_prove", - } - } - - pub fn from_label(value: &str) -> Option<Self> { - match value { - "disabled" => Some(Self::Disabled), - "deterministic_none" => Some(Self::DeterministicNone), - "local_execute" => Some(Self::LocalExecute), - "local_cpu_prove" => Some(Self::LocalCpuProve), - "local_cuda_prove" => Some(Self::LocalCudaProve), - "remote_http_prove" => Some(Self::RemoteHttpProve), - _ => None, - } - } -} - -impl core::fmt::Display for RadrootsSp1TradeProverBackend { - fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - formatter.write_str(self.as_str()) - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RadrootsSp1TradeProofEngine { - Cpu, - Cuda, -} - -impl RadrootsSp1TradeProofEngine { - pub const fn as_str(self) -> &'static str { - match self { - Self::Cpu => "cpu", - Self::Cuda => "cuda", - } - } - - pub fn from_label(value: &str) -> Option<Self> { - match value { - "cpu" => Some(Self::Cpu), - "cuda" => Some(Self::Cuda), - _ => None, - } - } -} - -impl core::fmt::Display for RadrootsSp1TradeProofEngine { - fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - formatter.write_str(self.as_str()) - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RadrootsSp1TradeRemoteProverStatus { - Accepted, - Running, - Completed, - Failed, - Rejected, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RadrootsSp1TradeWorkerResultStatus { - Succeeded, -} - -impl RadrootsSp1TradeWorkerResultStatus { - pub const fn as_str(self) -> &'static str { - match self { - Self::Succeeded => "succeeded", - } - } -} - -impl core::fmt::Display for RadrootsSp1TradeWorkerResultStatus { - fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - formatter.write_str(self.as_str()) - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RadrootsSp1TradeWorkerRole { - NonAuthoritativeProver, -} - -impl RadrootsSp1TradeWorkerRole { - pub const fn as_str(self) -> &'static str { - match self { - Self::NonAuthoritativeProver => "non_authoritative_prover", - } - } -} - -impl core::fmt::Display for RadrootsSp1TradeWorkerRole { - fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - formatter.write_str(self.as_str()) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct RadrootsSp1TradeRemoteProverRequest { - pub schema_version: u32, - pub request_id: String, - pub proof_target: String, - pub proof_mode: RadrootsSp1TradeProofMode, - pub sp1_version_line: String, - pub witness: RadrootsSp1TradeOrderAcceptanceWitness, - pub expected_sp1_program_hash: String, - pub expected_sp1_verifying_key_hash: String, - pub expected_public_values_hash: String, - pub expected_reducer_program_hash: String, - pub expected_protocol_version: String, - pub expected_witness_version: u32, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct RadrootsSp1TradeRemoteProverResponse { - pub schema_version: u32, - pub request_id: String, - pub status: RadrootsSp1TradeRemoteProverStatus, - #[serde(default)] - pub status_url: Option<String>, - #[serde(default)] - pub status_path: Option<String>, - #[serde(default)] - pub proof_system: Option<RadrootsValidationReceiptProofSystem>, - #[serde(default)] - pub proof_mode: Option<RadrootsSp1TradeProofMode>, - #[serde(default)] - pub public_values_hash: Option<String>, - #[serde(default)] - pub sp1_program_hash: Option<String>, - #[serde(default)] - pub sp1_verifying_key_hash: Option<String>, - #[serde(default)] - pub proof_artifact: Option<RadrootsSp1TradeProofArtifact>, - #[serde(default)] - pub resolved_proof_envelope_base64: Option<String>, - #[serde(default)] - pub reason_code: Option<String>, - #[serde(default)] - pub message: Option<String>, - #[serde(default)] - pub detail: Option<String>, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct RadrootsSp1TradeWorkerResultPayload { - pub cryptographic_proof_verified: bool, - pub decision_event_id: Option<String>, - pub event_set_root: Option<String>, - pub listing_event_id: Option<String>, - pub order_id: Option<String>, - pub proof_generated: bool, - pub proof_mode: RadrootsSp1TradeProofMode, - pub proof_system: RadrootsValidationReceiptProofSystem, - pub public_values_hash: String, - pub prover_backend: RadrootsSp1TradeProverBackend, - pub receipt_event_id: String, - pub receipt_kind: Option<u32>, - pub reducer_output_root: Option<String>, - pub request_event_id: Option<String>, - pub sp1_execute_checked: bool, - pub sp1_execute_public_values_hash: Option<String>, - pub status: RadrootsSp1TradeWorkerResultStatus, - pub worker_role: Option<RadrootsSp1TradeWorkerRole>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsSp1TradeProofBundle { - pub execution: RadrootsSp1TradePublicValuesExecution, - pub proof: RadrootsSp1TradeProofArtifact, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsSp1TradeValidationReceiptVerification { - pub canonical_public_values_len: usize, - pub proof_mode: RadrootsSp1TradeProofMode, - pub proof_system: RadrootsValidationReceiptProofSystem, - pub public_values_hash: String, - pub sp1_program_hash: String, - pub sp1_verifying_key_hash: String, -} - -#[derive(Debug, Error, PartialEq, Eq)] -pub enum RadrootsSp1TradeHostError { - #[error("guest execution failed")] - Guest, - #[error("proof mode requires an SP1 verifying key hash")] - MissingVerifyingKeyHash, - #[error("proof public values hash does not match execution")] - PublicValuesHashMismatch, - #[error("proof digest does not match execution")] - ProofDigestMismatch, - #[error("proof material is missing")] - MissingProofMaterial, - #[error("proof material has conflicting inline and reference sources")] - ProofMaterialConflict, - #[error("receipt binding field {0} is missing")] - MissingReceiptBinding(&'static str), - #[error("SP1 execution failed: {0}")] - Sp1ExecuteFailed(String), - #[error("SP1 execution returned exit code {0}")] - Sp1ExitCode(u64), - #[error("SP1 public values failed to decode: {0}")] - Sp1PublicValuesDecode(String), - #[error("SP1 public values do not match deterministic reducer output")] - Sp1PublicValuesMismatch, - #[error("SP1 proof generation requires the sp1_proving feature")] - Sp1ProofGenerationRequired, - #[error("SP1 CUDA proof generation is unavailable: {0}")] - Sp1CudaProofEngineUnavailable(String), - #[error("SP1 proof mode is required")] - Sp1ProofModeRequired, - #[error("SP1 setup failed: {0}")] - Sp1SetupFailed(String), - #[error("SP1 proof generation failed: {0}")] - Sp1ProofFailed(String), - #[error("SP1 proof verifier is unavailable in this build")] - Sp1ProofVerifierUnavailable, - #[error("SP1 proof verification failed: {0}")] - Sp1ProofVerificationFailed(String), - #[error("SP1 proof material failed to decode: {0}")] - Sp1ProofMaterialDecode(String), - #[error("SP1 proof material is synthetic")] - Sp1SyntheticProofMaterial, - #[error("SP1 proof reference is unresolved")] - Sp1ProofReferenceUnresolved, - #[error("SP1 proof reference is invalid")] - InvalidSp1ProofReference, - #[error("SP1 proof reference digest does not match resolved envelope")] - Sp1ProofReferenceDigestMismatch, - #[error("SP1 proof mode does not match the proof artifact")] - Sp1ProofModeMismatch, - #[error("SP1 verifying key hash mismatch")] - Sp1VerifyingKeyHashMismatch, - #[error("SP1 program hash mismatch")] - Sp1ProgramHashMismatch, - #[error("SP1 program hash is missing")] - MissingSp1ProgramHash, - #[error("validation receipt field {0} does not match SP1 public values")] - ValidationReceiptBindingMismatch(&'static str), - #[error("proof artifact encoding failed")] - ProofEncoding, -} - -impl From<RadrootsSp1TradeGuestError> for RadrootsSp1TradeHostError { - fn from(_: RadrootsSp1TradeGuestError) -> Self { - Self::Guest - } -} - -pub fn execute_order_acceptance_public_values( - witness: &RadrootsSp1TradeOrderAcceptanceWitness, -) -> Result<RadrootsSp1TradePublicValuesExecution, RadrootsSp1TradeHostError> { - Ok(reduce_order_acceptance_public_values(witness)?) -} - -#[cfg(feature = "sp1_proving")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsSp1TradeExecuteReport { - pub exit_code: u64, - pub gas: Option<u64>, - pub total_instruction_count: u64, - pub total_syscall_count: u64, -} - -#[cfg(feature = "sp1_proving")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsSp1TradeExecuteBundle { - pub committed_public_values: Vec<u8>, - pub execution: RadrootsSp1TradePublicValuesExecution, - pub report: RadrootsSp1TradeExecuteReport, -} - -#[cfg(all(feature = "sp1_verify", radroots_sp1_guest_elf))] -pub fn order_acceptance_guest_elf() -> sp1_sdk::Elf { - sp1_sdk::include_elf!("radroots_sp1_trade_order_acceptance_guest") -} - -#[cfg(all(feature = "sp1_verify", radroots_sp1_guest_elf))] -pub fn sp1_program_hash_for_order_acceptance_guest() -> String { - sp1_program_hash_for_elf(&order_acceptance_guest_elf()) -} - -#[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] -pub async fn execute_order_acceptance_sp1_public_values( - witness: &RadrootsSp1TradeOrderAcceptanceWitness, -) -> Result<RadrootsSp1TradeExecuteBundle, RadrootsSp1TradeHostError> { - execute_order_acceptance_sp1_public_values_with_elf(order_acceptance_guest_elf(), witness).await -} - -#[cfg(all(feature = "sp1_proving", not(radroots_sp1_guest_elf)))] -pub async fn execute_order_acceptance_sp1_public_values( - _witness: &RadrootsSp1TradeOrderAcceptanceWitness, -) -> Result<RadrootsSp1TradeExecuteBundle, RadrootsSp1TradeHostError> { - Err(RadrootsSp1TradeHostError::Sp1SetupFailed( - "SP1 guest ELF build is not enabled".to_owned(), - )) -} - -#[cfg(feature = "sp1_proving")] -pub async fn execute_order_acceptance_sp1_public_values_with_elf( - elf: sp1_sdk::Elf, - witness: &RadrootsSp1TradeOrderAcceptanceWitness, -) -> Result<RadrootsSp1TradeExecuteBundle, RadrootsSp1TradeHostError> { - use sp1_sdk::{HashableKey, Prover, ProverClient, ProvingKey, SP1Stdin, StatusCode}; - - let client = ProverClient::builder().light().build().await; - let pk = client - .setup(elf.clone()) - .await - .map_err(|error| RadrootsSp1TradeHostError::Sp1SetupFailed(error.to_string()))?; - let verifying_key_hash = pk.verifying_key().bytes32(); - let witness = witness_with_sp1_identity( - witness, - Some(sp1_program_hash_for_elf(&elf)), - Some(verifying_key_hash), - )?; - let expected = execute_order_acceptance_public_values(&witness)?; - let mut stdin = SP1Stdin::new(); - stdin.write(&witness); - let (public_values, report) = client - .execute(elf, stdin) - .calculate_gas(true) - .expected_exit_code(StatusCode::SUCCESS) - .await - .map_err(|error| RadrootsSp1TradeHostError::Sp1ExecuteFailed(error.to_string()))?; - if report.exit_code != 0 { - return Err(RadrootsSp1TradeHostError::Sp1ExitCode(report.exit_code)); - } - - let (committed_public_values, execution) = execution_from_sp1_public_values(public_values)?; - if execution != expected { - return Err(RadrootsSp1TradeHostError::Sp1PublicValuesMismatch); - } - - Ok(RadrootsSp1TradeExecuteBundle { - committed_public_values, - execution, - report: RadrootsSp1TradeExecuteReport { - exit_code: report.exit_code, - gas: report.gas(), - total_instruction_count: report.total_instruction_count(), - total_syscall_count: report.total_syscall_count(), - }, - }) -} - -#[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] -pub async fn generate_order_acceptance_sp1_proof( - witness: &RadrootsSp1TradeOrderAcceptanceWitness, - mode: RadrootsSp1TradeProofMode, -) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> { - generate_order_acceptance_sp1_proof_with_engine(witness, mode, RadrootsSp1TradeProofEngine::Cpu) - .await -} - -#[cfg(all(feature = "sp1_proving", not(radroots_sp1_guest_elf)))] -pub async fn generate_order_acceptance_sp1_proof( - _witness: &RadrootsSp1TradeOrderAcceptanceWitness, - _mode: RadrootsSp1TradeProofMode, -) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> { - Err(RadrootsSp1TradeHostError::Sp1SetupFailed( - "SP1 guest ELF build is not enabled".to_owned(), - )) -} - -#[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] -pub async fn generate_order_acceptance_sp1_proof_with_engine( - witness: &RadrootsSp1TradeOrderAcceptanceWitness, - mode: RadrootsSp1TradeProofMode, - engine: RadrootsSp1TradeProofEngine, -) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> { - match engine { - RadrootsSp1TradeProofEngine::Cpu => { - let client = sp1_sdk::ProverClient::builder().cpu().build().await; - generate_order_acceptance_sp1_proof_with_client(&client, witness, mode).await - } - RadrootsSp1TradeProofEngine::Cuda => { - generate_order_acceptance_sp1_cuda_proof(witness, mode).await - } - } -} - -#[cfg(all(feature = "sp1_proving", not(radroots_sp1_guest_elf)))] -pub async fn generate_order_acceptance_sp1_proof_with_engine( - _witness: &RadrootsSp1TradeOrderAcceptanceWitness, - _mode: RadrootsSp1TradeProofMode, - _engine: RadrootsSp1TradeProofEngine, -) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> { - Err(RadrootsSp1TradeHostError::Sp1SetupFailed( - "SP1 guest ELF build is not enabled".to_owned(), - )) -} - -#[cfg(all(feature = "sp1_proving", feature = "sp1_cuda", radroots_sp1_guest_elf))] -async fn generate_order_acceptance_sp1_cuda_proof( - witness: &RadrootsSp1TradeOrderAcceptanceWitness, - mode: RadrootsSp1TradeProofMode, -) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> { - use futures::FutureExt; - let client = std::panic::AssertUnwindSafe(sp1_sdk::ProverClient::builder().cuda().build()) - .catch_unwind() - .await - .map_err(cuda_panic_to_host_error)?; - generate_order_acceptance_sp1_proof_with_client(&client, witness, mode).await -} - -#[cfg(all(feature = "sp1_proving", feature = "sp1_cuda", radroots_sp1_guest_elf))] -fn cuda_panic_to_host_error(payload: Box<dyn std::any::Any + Send>) -> RadrootsSp1TradeHostError { - let message = payload - .downcast_ref::<&str>() - .map(|value| (*value).to_owned()) - .or_else(|| payload.downcast_ref::<String>().cloned()) - .unwrap_or_else(|| "CUDA prover initialization failed".to_owned()); - RadrootsSp1TradeHostError::Sp1CudaProofEngineUnavailable(message) -} - -#[cfg(all( - feature = "sp1_proving", - not(feature = "sp1_cuda"), - radroots_sp1_guest_elf -))] -async fn generate_order_acceptance_sp1_cuda_proof( - _witness: &RadrootsSp1TradeOrderAcceptanceWitness, - _mode: RadrootsSp1TradeProofMode, -) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> { - Err(RadrootsSp1TradeHostError::Sp1CudaProofEngineUnavailable( - "build without sp1_cuda feature".to_owned(), - )) -} - -#[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] -async fn generate_order_acceptance_sp1_proof_with_client<P>( - client: &P, - witness: &RadrootsSp1TradeOrderAcceptanceWitness, - mode: RadrootsSp1TradeProofMode, -) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> -where - P: sp1_sdk::Prover, -{ - use sp1_sdk::{HashableKey, ProveRequest, ProvingKey, SP1Stdin, StatusCode}; - - let sp1_mode = sp1_proof_mode(mode)?; - let elf = order_acceptance_guest_elf(); - let sp1_program_hash = sp1_program_hash_for_elf(&elf); - let pk = client - .setup(elf) - .await - .map_err(|error| RadrootsSp1TradeHostError::Sp1SetupFailed(error.to_string()))?; - let verifying_key_hash = pk.verifying_key().bytes32(); - let witness = - witness_with_sp1_identity(witness, Some(sp1_program_hash), Some(verifying_key_hash))?; - let expected = execute_order_acceptance_public_values(&witness)?; - let mut stdin = SP1Stdin::new(); - stdin.write(&witness); - let proof = client - .prove(&pk, stdin) - .mode(sp1_mode) - .expected_exit_code(StatusCode::SUCCESS) - .await - .map_err(|error| RadrootsSp1TradeHostError::Sp1ProofFailed(error.to_string()))?; - if !sp1_proof_material_is_real(&proof.proof) { - return Err(RadrootsSp1TradeHostError::Sp1SyntheticProofMaterial); - } - client - .verify(&proof, pk.verifying_key(), Some(StatusCode::SUCCESS)) - .map_err(|error| { - RadrootsSp1TradeHostError::Sp1ProofVerificationFailed(error.to_string()) - })?; - let (_, execution) = execution_from_sp1_public_values(proof.public_values.clone())?; - if execution != expected { - return Err(RadrootsSp1TradeHostError::Sp1PublicValuesMismatch); - } - let proof_bytes = - bincode::serialize(&proof).map_err(|_| RadrootsSp1TradeHostError::ProofEncoding)?; - let verifying_key_bytes = bincode::serialize(pk.verifying_key()) - .map_err(|_| RadrootsSp1TradeHostError::ProofEncoding)?; - let proof = proof_artifact_for_real_sp1_execution( - &execution, - mode, - &proof_bytes, - &verifying_key_bytes, - )?; - verify_order_acceptance_proof_artifact_structure(&execution, &proof)?; - Ok(RadrootsSp1TradeProofBundle { execution, proof }) -} - -#[cfg(feature = "sp1_verify")] -pub async fn verify_order_acceptance_resolved_sp1_proof_artifact( - execution: &RadrootsSp1TradePublicValuesExecution, - resolved: &RadrootsSp1TradeResolvedProofArtifact, -) -> Result<(), RadrootsSp1TradeHostError> { - use sp1_sdk::{HashableKey, Prover, ProverClient, StatusCode}; - - let artifact = &resolved.artifact; - verify_order_acceptance_proof_artifact_structure(execution, artifact)?; - let envelope_base64 = resolved_proof_envelope_base64(resolved)?; - let envelope = decode_proof_envelope_base64(envelope_base64)?; - verify_proof_envelope(execution, artifact, &envelope)?; - let mode = artifact_proof_mode(artifact)?; - let proof = decode_sp1_proof_envelope(&envelope)?; - if !sp1_proof_material_is_real(&proof.proof) { - return Err(RadrootsSp1TradeHostError::Sp1SyntheticProofMaterial); - } - if !sp1_proof_matches_mode(&proof.proof, mode) { - return Err(RadrootsSp1TradeHostError::Sp1ProofModeMismatch); - } - let client = ProverClient::builder().cpu().build().await; - let verifying_key = decode_sp1_verifying_key_envelope(&envelope)?; - let verifying_key_hash = verifying_key.bytes32(); - if artifact.verifying_key_hash.as_deref() != Some(verifying_key_hash.as_str()) { - return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch); - } - let sp1_program_hash = artifact - .program_hash - .as_deref() - .ok_or(RadrootsSp1TradeHostError::MissingSp1ProgramHash)?; - client - .verify(&proof, &verifying_key, Some(StatusCode::SUCCESS)) - .map_err(|error| { - RadrootsSp1TradeHostError::Sp1ProofVerificationFailed(error.to_string()) - })?; - let (_, proof_execution) = execution_from_sp1_public_values(proof.public_values)?; - require_public_values_sp1_identity( - &proof_execution.public_values, - sp1_program_hash, - verifying_key_hash.as_str(), - )?; - if &proof_execution != execution { - return Err(RadrootsSp1TradeHostError::Sp1PublicValuesMismatch); - } - Ok(()) -} - -#[cfg(not(feature = "sp1_verify"))] -pub async fn verify_order_acceptance_resolved_sp1_proof_artifact( - _execution: &RadrootsSp1TradePublicValuesExecution, - _resolved: &RadrootsSp1TradeResolvedProofArtifact, -) -> Result<(), RadrootsSp1TradeHostError> { - Err(RadrootsSp1TradeHostError::Sp1ProofVerifierUnavailable) -} - -#[cfg(feature = "sp1_verify")] -pub async fn verify_order_acceptance_validation_receipt_inline_sp1_proof( - receipt: &RadrootsTradeValidationReceipt, -) -> Result<RadrootsSp1TradeValidationReceiptVerification, RadrootsSp1TradeHostError> { - use sp1_sdk::{HashableKey, Prover, ProverClient, StatusCode}; - - if receipt.proof.system == RadrootsValidationReceiptProofSystem::None { - return Err(RadrootsSp1TradeHostError::Sp1ProofModeRequired); - } - if receipt.proof.proof_reference.is_some() { - return Err(RadrootsSp1TradeHostError::Sp1ProofReferenceUnresolved); - } - - let artifact = RadrootsSp1TradeProofArtifact { - inline_proof_base64: Some( - receipt - .proof - .inline_proof_base64 - .clone() - .ok_or(RadrootsSp1TradeHostError::MissingProofMaterial)?, - ), - mode: receipt.proof.mode.clone(), - program_hash: receipt.proof.program_hash.clone(), - proof_digest: String::new(), - proof_reference: None, - public_values_hash: receipt.public_values_hash.clone(), - system: receipt.proof.system, - verifying_key_hash: receipt.proof.verifying_key_hash.clone(), - }; - let mode = artifact_proof_mode(&artifact)?; - let proof = decode_sp1_proof_artifact(&artifact)?; - if !sp1_proof_material_is_real(&proof.proof) { - return Err(RadrootsSp1TradeHostError::Sp1SyntheticProofMaterial); - } - if !sp1_proof_matches_mode(&proof.proof, mode) { - return Err(RadrootsSp1TradeHostError::Sp1ProofModeMismatch); - } - - let envelope = decode_proof_envelope(&artifact)?; - let verifying_key = decode_sp1_verifying_key_envelope(&envelope)?; - let verifying_key_hash = verifying_key.bytes32(); - if artifact.verifying_key_hash.as_deref() != Some(verifying_key_hash.as_str()) { - return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch); - } - let sp1_program_hash = artifact - .program_hash - .as_deref() - .ok_or(RadrootsSp1TradeHostError::MissingSp1ProgramHash)?; - - let client = ProverClient::builder().cpu().build().await; - client - .verify(&proof, &verifying_key, Some(StatusCode::SUCCESS)) - .map_err(|error| { - RadrootsSp1TradeHostError::Sp1ProofVerificationFailed(error.to_string()) - })?; - let (_, execution) = execution_from_sp1_public_values(proof.public_values)?; - require_public_values_sp1_identity( - &execution.public_values, - sp1_program_hash, - verifying_key_hash.as_str(), - )?; - verify_proof_envelope(&execution, &artifact, &envelope)?; - verify_validation_receipt_matches_public_values(receipt, &execution.public_values)?; - if execution.public_values_hash != receipt.public_values_hash { - return Err(RadrootsSp1TradeHostError::PublicValuesHashMismatch); - } - - Ok(RadrootsSp1TradeValidationReceiptVerification { - canonical_public_values_len: execution.canonical_public_values.len(), - proof_mode: mode, - proof_system: receipt.proof.system, - public_values_hash: execution.public_values_hash, - sp1_program_hash: sp1_program_hash.to_owned(), - sp1_verifying_key_hash: verifying_key_hash, - }) -} - -#[cfg(not(feature = "sp1_verify"))] -pub async fn verify_order_acceptance_validation_receipt_inline_sp1_proof( - _receipt: &RadrootsTradeValidationReceipt, -) -> Result<RadrootsSp1TradeValidationReceiptVerification, RadrootsSp1TradeHostError> { - Err(RadrootsSp1TradeHostError::Sp1ProofVerifierUnavailable) -} - -#[cfg(feature = "sp1_verify")] -pub async fn verify_order_acceptance_validation_receipt_resolved_sp1_proof( - receipt: &RadrootsTradeValidationReceipt, - resolved: &RadrootsSp1TradeResolvedProofArtifact, -) -> Result<RadrootsSp1TradeValidationReceiptVerification, RadrootsSp1TradeHostError> { - use sp1_sdk::{HashableKey, Prover, ProverClient, StatusCode}; - - if receipt.proof.system == RadrootsValidationReceiptProofSystem::None { - return Err(RadrootsSp1TradeHostError::Sp1ProofModeRequired); - } - verify_receipt_proof_matches_artifact(receipt, &resolved.artifact)?; - let envelope_base64 = resolved_proof_envelope_base64(resolved)?; - let envelope = decode_proof_envelope_base64(envelope_base64)?; - let mode = artifact_proof_mode(&resolved.artifact)?; - let proof = decode_sp1_proof_envelope(&envelope)?; - if !sp1_proof_material_is_real(&proof.proof) { - return Err(RadrootsSp1TradeHostError::Sp1SyntheticProofMaterial); - } - if !sp1_proof_matches_mode(&proof.proof, mode) { - return Err(RadrootsSp1TradeHostError::Sp1ProofModeMismatch); - } - - let verifying_key = decode_sp1_verifying_key_envelope(&envelope)?; - let verifying_key_hash = verifying_key.bytes32(); - if resolved.artifact.verifying_key_hash.as_deref() != Some(verifying_key_hash.as_str()) { - return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch); - } - let sp1_program_hash = resolved - .artifact - .program_hash - .as_deref() - .ok_or(RadrootsSp1TradeHostError::MissingSp1ProgramHash)?; - - let client = ProverClient::builder().cpu().build().await; - client - .verify(&proof, &verifying_key, Some(StatusCode::SUCCESS)) - .map_err(|error| { - RadrootsSp1TradeHostError::Sp1ProofVerificationFailed(error.to_string()) - })?; - let (_, execution) = execution_from_sp1_public_values(proof.public_values)?; - require_public_values_sp1_identity( - &execution.public_values, - sp1_program_hash, - verifying_key_hash.as_str(), - )?; - verify_order_acceptance_proof_artifact_structure(&execution, &resolved.artifact)?; - verify_proof_envelope(&execution, &resolved.artifact, &envelope)?; - verify_validation_receipt_matches_public_values(receipt, &execution.public_values)?; - if execution.public_values_hash != receipt.public_values_hash { - return Err(RadrootsSp1TradeHostError::PublicValuesHashMismatch); - } - - Ok(RadrootsSp1TradeValidationReceiptVerification { - canonical_public_values_len: execution.canonical_public_values.len(), - proof_mode: mode, - proof_system: receipt.proof.system, - public_values_hash: execution.public_values_hash, - sp1_program_hash: sp1_program_hash.to_owned(), - sp1_verifying_key_hash: verifying_key_hash, - }) -} - -#[cfg(not(feature = "sp1_verify"))] -pub async fn verify_order_acceptance_validation_receipt_resolved_sp1_proof( - _receipt: &RadrootsTradeValidationReceipt, - _resolved: &RadrootsSp1TradeResolvedProofArtifact, -) -> Result<RadrootsSp1TradeValidationReceiptVerification, RadrootsSp1TradeHostError> { - Err(RadrootsSp1TradeHostError::Sp1ProofVerifierUnavailable) -} - -pub fn generate_order_acceptance_proof( - witness: &RadrootsSp1TradeOrderAcceptanceWitness, - mode: RadrootsSp1TradeProofMode, -) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> { - let execution = execute_order_acceptance_public_values(witness)?; - let proof = proof_artifact_for_execution(&execution, mode)?; - verify_order_acceptance_proof_artifact_structure(&execution, &proof)?; - Ok(RadrootsSp1TradeProofBundle { execution, proof }) -} - -#[cfg(feature = "sp1_verify")] -fn verify_validation_receipt_matches_public_values( - receipt: &RadrootsTradeValidationReceipt, - public_values: &RadrootsSp1TradeProofPublicValues, -) -> Result<(), RadrootsSp1TradeHostError> { - let program_hash = public_values - .sp1_program_hash - .as_deref() - .ok_or(RadrootsSp1TradeHostError::MissingSp1ProgramHash)?; - let verifying_key_hash = public_values - .sp1_verifying_key_hash - .as_deref() - .ok_or(RadrootsSp1TradeHostError::MissingVerifyingKeyHash)?; - if receipt.proof.program_hash.as_deref() != Some(program_hash) { - return Err(RadrootsSp1TradeHostError::Sp1ProgramHashMismatch); - } - if receipt.proof.verifying_key_hash.as_deref() != Some(verifying_key_hash) { - return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch); - } - if public_values.statement_type != RadrootsSp1TradeProofStatementType::TradeTransition { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "statement_type", - )); - } - if receipt.receipt_type != RadrootsValidationReceiptType::TradeTransition - || receipt.statement.statement_type != RadrootsValidationReceiptType::TradeTransition - { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "receipt_type", - )); - } - if public_values.event_set_root != receipt.event_set_root { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "event_set_root", - )); - } - if public_values.previous_state_root != receipt.previous_state_root { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "previous_state_root", - )); - } - if public_values.new_state_root != receipt.new_state_root { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "new_state_root", - )); - } - if public_values.changed_records_root != receipt.changed_records_root { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "changed_records_root", - )); - } - if public_values.error_bitmap != receipt.error_bitmap { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "error_bitmap", - )); - } - if public_values.listing_event_id.as_deref() - != Some(receipt.statement.listing_event_id.as_str()) - { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "listing_event_id", - )); - } - if public_values.root_event_id.as_deref() != Some(receipt.statement.root_event_id.as_str()) { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "root_event_id", - )); - } - if public_values.target_event_id.as_deref() != Some(receipt.statement.target_event_id.as_str()) - { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "target_event_id", - )); - } - if !receipt_result_matches_public_values(receipt.result, public_values.result) { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "result", - )); - } - Ok(()) -} - -#[cfg(feature = "sp1_verify")] -fn require_public_values_sp1_identity( - public_values: &RadrootsSp1TradeProofPublicValues, - expected_program_hash: &str, - expected_verifying_key_hash: &str, -) -> Result<(), RadrootsSp1TradeHostError> { - match public_values.sp1_program_hash.as_deref() { - Some(value) if value == expected_program_hash => {} - Some(_) => return Err(RadrootsSp1TradeHostError::Sp1ProgramHashMismatch), - None => return Err(RadrootsSp1TradeHostError::MissingSp1ProgramHash), - } - match public_values.sp1_verifying_key_hash.as_deref() { - Some(value) if value == expected_verifying_key_hash => {} - Some(_) => return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch), - None => return Err(RadrootsSp1TradeHostError::MissingVerifyingKeyHash), - } - Ok(()) -} - -#[cfg(feature = "sp1_verify")] -fn receipt_result_matches_public_values( - receipt_result: RadrootsValidationReceiptResult, - public_values_result: RadrootsSp1TradeProofResult, -) -> bool { - matches!( - (receipt_result, public_values_result), - ( - RadrootsValidationReceiptResult::Valid, - RadrootsSp1TradeProofResult::Valid - ) | ( - RadrootsValidationReceiptResult::Invalid, - RadrootsSp1TradeProofResult::Invalid - ) - ) -} - -pub fn verify_order_acceptance_proof_artifact_structure( - execution: &RadrootsSp1TradePublicValuesExecution, - artifact: &RadrootsSp1TradeProofArtifact, -) -> Result<(), RadrootsSp1TradeHostError> { - if artifact.public_values_hash != execution.public_values_hash { - return Err(RadrootsSp1TradeHostError::PublicValuesHashMismatch); - } - let expected = proof_digest_for_execution(execution, artifact)?; - if artifact.proof_digest != expected { - return Err(RadrootsSp1TradeHostError::ProofDigestMismatch); - } - match artifact.system { - RadrootsValidationReceiptProofSystem::None => { - if artifact.inline_proof_base64.is_some() - || artifact.mode.is_some() - || artifact.program_hash.is_some() - || artifact.proof_reference.is_some() - || artifact.verifying_key_hash.is_some() - { - return Err(RadrootsSp1TradeHostError::MissingProofMaterial); - } - } - _ => { - match (&artifact.inline_proof_base64, &artifact.proof_reference) { - (None, None) => return Err(RadrootsSp1TradeHostError::MissingProofMaterial), - (Some(_), Some(_)) => { - return Err(RadrootsSp1TradeHostError::ProofMaterialConflict); - } - (None, Some(reference)) => { - proof_reference_digest(reference)?; - } - _ => {} - } - let public_values_program_hash = execution - .public_values - .sp1_program_hash - .as_deref() - .ok_or(RadrootsSp1TradeHostError::MissingSp1ProgramHash)?; - let public_values_verifying_key_hash = execution - .public_values - .sp1_verifying_key_hash - .as_deref() - .ok_or(RadrootsSp1TradeHostError::MissingVerifyingKeyHash)?; - if artifact.program_hash.as_deref() != Some(public_values_program_hash) { - return Err(RadrootsSp1TradeHostError::Sp1ProgramHashMismatch); - } - if artifact.verifying_key_hash.as_deref() != Some(public_values_verifying_key_hash) { - return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch); - } - if artifact.inline_proof_base64.is_some() { - let envelope = decode_proof_envelope(artifact)?; - verify_proof_envelope(execution, artifact, &envelope)?; - } - } - } - Ok(()) -} - -pub fn validation_receipt_for_order_acceptance_proof( - bundle: &RadrootsSp1TradeProofBundle, -) -> Result<RadrootsTradeValidationReceipt, RadrootsSp1TradeHostError> { - let public_values = &bundle.execution.public_values; - let listing_event_id = public_values.listing_event_id.clone().ok_or( - RadrootsSp1TradeHostError::MissingReceiptBinding("listing_event_id"), - )?; - let root_event_id = public_values.root_event_id.clone().ok_or( - RadrootsSp1TradeHostError::MissingReceiptBinding("root_event_id"), - )?; - let target_event_id = public_values.target_event_id.clone().ok_or( - RadrootsSp1TradeHostError::MissingReceiptBinding("target_event_id"), - )?; - Ok(RadrootsTradeValidationReceipt { - changed_records_root: public_values.changed_records_root.clone(), - domain: VALIDATION_RECEIPT_DOMAIN.to_string(), - error_bitmap: public_values.error_bitmap.clone(), - event_set_root: public_values.event_set_root.clone(), - new_state_root: public_values.new_state_root.clone(), - previous_state_root: public_values.previous_state_root.clone(), - proof: RadrootsValidationReceiptProof { - inline_proof_base64: bundle.proof.inline_proof_base64.clone(), - mode: bundle.proof.mode.clone(), - program_hash: bundle.proof.program_hash.clone(), - proof_reference: bundle.proof.proof_reference.clone(), - system: bundle.proof.system, - verifying_key_hash: bundle.proof.verifying_key_hash.clone(), - }, - public_values_hash: bundle.execution.public_values_hash.clone(), - receipt_type: RadrootsValidationReceiptType::TradeTransition, - result: validation_receipt_result_from_public_values(public_values.result), - statement: RadrootsValidationReceiptStatement { - listing_event_id, - root_event_id, - target_event_id, - statement_type: RadrootsValidationReceiptType::TradeTransition, - }, - version: VALIDATION_RECEIPT_VERSION, - }) -} - -fn validation_receipt_result_from_public_values( - result: RadrootsSp1TradeProofResult, -) -> RadrootsValidationReceiptResult { - match result { - RadrootsSp1TradeProofResult::Valid => RadrootsValidationReceiptResult::Valid, - RadrootsSp1TradeProofResult::Invalid => RadrootsValidationReceiptResult::Invalid, - } -} - -#[cfg(feature = "sp1_verify")] -fn verify_receipt_proof_matches_artifact( - receipt: &RadrootsTradeValidationReceipt, - artifact: &RadrootsSp1TradeProofArtifact, -) -> Result<(), RadrootsSp1TradeHostError> { - if receipt.public_values_hash != artifact.public_values_hash { - return Err(RadrootsSp1TradeHostError::PublicValuesHashMismatch); - } - if receipt.proof.inline_proof_base64.as_deref() != artifact.inline_proof_base64.as_deref() - || receipt.proof.mode.as_deref() != artifact.mode.as_deref() - || receipt.proof.program_hash.as_deref() != artifact.program_hash.as_deref() - || receipt.proof.proof_reference.as_deref() != artifact.proof_reference.as_deref() - || receipt.proof.system != artifact.system - || receipt.proof.verifying_key_hash.as_deref() != artifact.verifying_key_hash.as_deref() - { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "proof", - )); - } - Ok(()) -} - -fn validation_receipt_result_label(result: RadrootsValidationReceiptResult) -> &'static str { - match result { - RadrootsValidationReceiptResult::Valid => "valid", - RadrootsValidationReceiptResult::Invalid => "invalid", - } -} - -fn proof_artifact_for_execution( - execution: &RadrootsSp1TradePublicValuesExecution, - mode: RadrootsSp1TradeProofMode, -) -> Result<RadrootsSp1TradeProofArtifact, RadrootsSp1TradeHostError> { - let system = mode.proof_system(); - let mut artifact = RadrootsSp1TradeProofArtifact { - inline_proof_base64: None, - mode: mode.mode_label().map(str::to_string), - program_hash: None, - proof_digest: String::new(), - proof_reference: None, - public_values_hash: execution.public_values_hash.clone(), - system, - verifying_key_hash: None, - }; - if system == RadrootsValidationReceiptProofSystem::None { - artifact.proof_digest = proof_digest_for_execution(execution, &artifact)?; - return Ok(artifact); - } - - Err(RadrootsSp1TradeHostError::Sp1ProofGenerationRequired) -} - -pub fn referenced_order_acceptance_proof_artifact_for_execution( - execution: &RadrootsSp1TradePublicValuesExecution, - mode: RadrootsSp1TradeProofMode, - proof_reference: String, -) -> Result<RadrootsSp1TradeProofArtifact, RadrootsSp1TradeHostError> { - proof_reference_digest(proof_reference.as_str())?; - let system = mode.proof_system(); - if system == RadrootsValidationReceiptProofSystem::None { - return Err(RadrootsSp1TradeHostError::Sp1ProofModeRequired); - } - let mut artifact = RadrootsSp1TradeProofArtifact { - inline_proof_base64: None, - mode: mode.mode_label().map(str::to_string), - program_hash: execution.public_values.sp1_program_hash.clone(), - proof_digest: String::new(), - proof_reference: Some(proof_reference), - public_values_hash: execution.public_values_hash.clone(), - system, - verifying_key_hash: execution.public_values.sp1_verifying_key_hash.clone(), - }; - artifact.proof_digest = proof_digest_for_execution(execution, &artifact)?; - verify_order_acceptance_proof_artifact_structure(execution, &artifact)?; - Ok(artifact) -} - -#[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] -fn proof_artifact_for_real_sp1_execution( - execution: &RadrootsSp1TradePublicValuesExecution, - mode: RadrootsSp1TradeProofMode, - proof_bytes: &[u8], - verifying_key_bytes: &[u8], -) -> Result<RadrootsSp1TradeProofArtifact, RadrootsSp1TradeHostError> { - let system = mode.proof_system(); - if system == RadrootsValidationReceiptProofSystem::None { - return Err(RadrootsSp1TradeHostError::Sp1ProofModeRequired); - } - let program_hash = execution - .public_values - .sp1_program_hash - .clone() - .ok_or(RadrootsSp1TradeHostError::MissingSp1ProgramHash)?; - let verifying_key_hash = execution - .public_values - .sp1_verifying_key_hash - .clone() - .ok_or(RadrootsSp1TradeHostError::MissingVerifyingKeyHash)?; - let mut envelope = proof_envelope_for_real_sp1_execution( - execution, - system, - mode, - program_hash.as_str(), - verifying_key_hash.as_str(), - proof_bytes, - verifying_key_bytes, - )?; - envelope.proof_digest = proof_digest_for_envelope(&envelope)?; - let envelope_json = - serde_json::to_vec(&envelope).map_err(|_| RadrootsSp1TradeHostError::ProofEncoding)?; - Ok(RadrootsSp1TradeProofArtifact { - inline_proof_base64: Some(base64::engine::general_purpose::STANDARD.encode(envelope_json)), - mode: mode.mode_label().map(str::to_string), - program_hash: Some(program_hash), - proof_digest: envelope.proof_digest, - proof_reference: None, - public_values_hash: execution.public_values_hash.clone(), - system, - verifying_key_hash: Some(verifying_key_hash), - }) -} - -fn proof_digest_for_execution( - execution: &RadrootsSp1TradePublicValuesExecution, - artifact: &RadrootsSp1TradeProofArtifact, -) -> Result<String, RadrootsSp1TradeHostError> { - if artifact.inline_proof_base64.is_some() { - return proof_digest_for_envelope(&decode_proof_envelope(artifact)?); - } - let material = ProofDigestMaterial { - canonical_public_values: &execution.canonical_public_values, - mode: artifact.mode.as_deref(), - program_hash: artifact.program_hash.as_deref(), - proof_reference: artifact.proof_reference.as_deref(), - public_values_hash: &artifact.public_values_hash, - system: artifact.system.as_str(), - verifying_key_hash: artifact.verifying_key_hash.as_deref(), - }; - let bytes = - serde_json::to_vec(&material).map_err(|_| RadrootsSp1TradeHostError::ProofEncoding)?; - let mut hasher = Sha256::new(); - hasher.update(b"radroots:sp1-trade-proof-artifact:v1"); - hasher.update(bytes); - Ok(format!("0x{}", hex_lower(hasher.finalize().as_slice()))) -} - -#[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] -fn proof_envelope_for_real_sp1_execution( - execution: &RadrootsSp1TradePublicValuesExecution, - system: RadrootsValidationReceiptProofSystem, - mode: RadrootsSp1TradeProofMode, - program_hash: &str, - verifying_key_hash: &str, - proof_bytes: &[u8], - verifying_key_bytes: &[u8], -) -> Result<RadrootsSp1TradeProofEnvelope, RadrootsSp1TradeHostError> { - Ok(RadrootsSp1TradeProofEnvelope { - schema_version: RADROOTS_SP1_TRADE_PROOF_ARTIFACT_SCHEMA_VERSION, - sp1_version_line: RADROOTS_SP1_TRADE_SP1_VERSION_LINE.to_owned(), - proof_system: system.as_str().to_owned(), - proof_mode: mode - .mode_label() - .ok_or(RadrootsSp1TradeHostError::Sp1ProofModeRequired)? - .to_owned(), - proof_codec: RADROOTS_SP1_TRADE_PROOF_CODEC.to_owned(), - proof_content_hash: hash_bytes("radroots:sp1-proof-content:v1", proof_bytes), - proof_digest: String::new(), - public_values_hash: execution.public_values_hash.clone(), - canonical_public_values_hash: hash_bytes( - "radroots:sp1-canonical-public-values:v1", - &execution.canonical_public_values, - ), - sp1_program_hash: program_hash.to_owned(), - sp1_verifying_key_hash: verifying_key_hash.to_owned(), - sp1_verifying_key_codec: RADROOTS_SP1_TRADE_VERIFYING_KEY_CODEC.to_owned(), - sp1_verifying_key_base64: base64::engine::general_purpose::STANDARD - .encode(verifying_key_bytes), - receipt_type: RadrootsValidationReceiptType::TradeTransition - .as_str() - .to_owned(), - receipt_result: validation_receipt_result_label( - validation_receipt_result_from_public_values(execution.public_values.result), - ) - .to_owned(), - listing_event_id: execution.public_values.listing_event_id.clone().ok_or( - RadrootsSp1TradeHostError::MissingReceiptBinding("listing_event_id"), - )?, - root_event_id: execution.public_values.root_event_id.clone().ok_or( - RadrootsSp1TradeHostError::MissingReceiptBinding("root_event_id"), - )?, - target_event_id: execution.public_values.target_event_id.clone().ok_or( - RadrootsSp1TradeHostError::MissingReceiptBinding("target_event_id"), - )?, - event_set_root: execution.public_values.event_set_root.clone(), - previous_state_root: execution.public_values.previous_state_root.clone(), - new_state_root: execution.public_values.new_state_root.clone(), - changed_records_root: execution.public_values.changed_records_root.clone(), - error_bitmap: execution.public_values.error_bitmap.clone(), - proof_content_base64: base64::engine::general_purpose::STANDARD.encode(proof_bytes), - }) -} - -fn decode_proof_envelope( - artifact: &RadrootsSp1TradeProofArtifact, -) -> Result<RadrootsSp1TradeProofEnvelope, RadrootsSp1TradeHostError> { - let inline = artifact - .inline_proof_base64 - .as_deref() - .ok_or(RadrootsSp1TradeHostError::MissingProofMaterial)?; - decode_proof_envelope_base64(inline) -} - -fn decode_proof_envelope_base64( - value: &str, -) -> Result<RadrootsSp1TradeProofEnvelope, RadrootsSp1TradeHostError> { - let envelope_bytes = proof_envelope_bytes_from_base64(value)?; - serde_json::from_slice::<RadrootsSp1TradeProofEnvelope>(&envelope_bytes) - .map_err(|error| RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(error.to_string())) -} - -fn proof_envelope_bytes_from_base64(value: &str) -> Result<Vec<u8>, RadrootsSp1TradeHostError> { - base64::engine::general_purpose::STANDARD - .decode(value) - .map_err(|error| RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(error.to_string())) -} - -pub fn proof_reference_for_proof_envelope_base64( - value: &str, -) -> Result<String, RadrootsSp1TradeHostError> { - let envelope_bytes = proof_envelope_bytes_from_base64(value)?; - let mut hasher = Sha256::new(); - hasher.update(envelope_bytes); - Ok(format!( - "{VALIDATION_RECEIPT_PROOF_REFERENCE_SHA256_PREFIX}{}", - hex_lower(hasher.finalize().as_slice()) - )) -} - -fn proof_reference_digest(value: &str) -> Result<&str, RadrootsSp1TradeHostError> { - let Some(digest) = value.strip_prefix(VALIDATION_RECEIPT_PROOF_REFERENCE_SHA256_PREFIX) else { - return Err(RadrootsSp1TradeHostError::InvalidSp1ProofReference); - }; - if digest.len() != 64 || !is_lower_hex(digest) { - return Err(RadrootsSp1TradeHostError::InvalidSp1ProofReference); - } - Ok(digest) -} - -#[cfg(feature = "sp1_verify")] -fn resolved_proof_envelope_base64( - resolved: &RadrootsSp1TradeResolvedProofArtifact, -) -> Result<&str, RadrootsSp1TradeHostError> { - match ( - resolved.artifact.inline_proof_base64.as_deref(), - resolved.artifact.proof_reference.as_deref(), - resolved.resolved_proof_envelope_base64.as_deref(), - ) { - (Some(inline), None, None) => Ok(inline), - (Some(_), None, Some(_)) => Err(RadrootsSp1TradeHostError::ProofMaterialConflict), - (None, Some(reference), Some(envelope)) => { - let expected = proof_reference_digest(reference)?; - let actual = proof_reference_for_proof_envelope_base64(envelope)?; - let actual = proof_reference_digest(actual.as_str())?; - if actual != expected { - return Err(RadrootsSp1TradeHostError::Sp1ProofReferenceDigestMismatch); - } - Ok(envelope) - } - (None, Some(_), None) => Err(RadrootsSp1TradeHostError::Sp1ProofReferenceUnresolved), - (Some(_), Some(_), _) => Err(RadrootsSp1TradeHostError::ProofMaterialConflict), - (None, None, _) => Err(RadrootsSp1TradeHostError::MissingProofMaterial), - } -} - -fn proof_content_bytes_from_envelope( - envelope: &RadrootsSp1TradeProofEnvelope, -) -> Result<Vec<u8>, RadrootsSp1TradeHostError> { - base64::engine::general_purpose::STANDARD - .decode(envelope.proof_content_base64.as_str()) - .map_err(|error| RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(error.to_string())) -} - -fn proof_digest_for_envelope( - envelope: &RadrootsSp1TradeProofEnvelope, -) -> Result<String, RadrootsSp1TradeHostError> { - let material = ProofEnvelopeDigestMaterial { - schema_version: envelope.schema_version, - sp1_version_line: envelope.sp1_version_line.as_str(), - proof_system: envelope.proof_system.as_str(), - proof_mode: envelope.proof_mode.as_str(), - proof_codec: envelope.proof_codec.as_str(), - proof_content_hash: envelope.proof_content_hash.as_str(), - public_values_hash: envelope.public_values_hash.as_str(), - canonical_public_values_hash: envelope.canonical_public_values_hash.as_str(), - sp1_program_hash: envelope.sp1_program_hash.as_str(), - sp1_verifying_key_hash: envelope.sp1_verifying_key_hash.as_str(), - sp1_verifying_key_codec: envelope.sp1_verifying_key_codec.as_str(), - sp1_verifying_key_base64: envelope.sp1_verifying_key_base64.as_str(), - receipt_type: envelope.receipt_type.as_str(), - receipt_result: envelope.receipt_result.as_str(), - listing_event_id: envelope.listing_event_id.as_str(), - root_event_id: envelope.root_event_id.as_str(), - target_event_id: envelope.target_event_id.as_str(), - event_set_root: envelope.event_set_root.as_str(), - previous_state_root: envelope.previous_state_root.as_str(), - new_state_root: envelope.new_state_root.as_str(), - changed_records_root: envelope.changed_records_root.as_str(), - error_bitmap: envelope.error_bitmap.as_str(), - }; - let bytes = - serde_json::to_vec(&material).map_err(|_| RadrootsSp1TradeHostError::ProofEncoding)?; - Ok(hash_bytes( - "radroots:sp1-trade-proof-envelope-digest:v1", - &bytes, - )) -} - -fn verify_proof_envelope( - execution: &RadrootsSp1TradePublicValuesExecution, - artifact: &RadrootsSp1TradeProofArtifact, - envelope: &RadrootsSp1TradeProofEnvelope, -) -> Result<(), RadrootsSp1TradeHostError> { - if envelope.schema_version != RADROOTS_SP1_TRADE_PROOF_ARTIFACT_SCHEMA_VERSION - || envelope.sp1_version_line != RADROOTS_SP1_TRADE_SP1_VERSION_LINE - || envelope.proof_codec != RADROOTS_SP1_TRADE_PROOF_CODEC - || envelope.proof_system != artifact.system.as_str() - || Some(envelope.proof_mode.as_str()) != artifact.mode.as_deref() - || envelope.public_values_hash != artifact.public_values_hash - || envelope.sp1_program_hash.as_str() != artifact.program_hash.as_deref().unwrap_or("") - || envelope.sp1_verifying_key_hash.as_str() - != artifact.verifying_key_hash.as_deref().unwrap_or("") - || envelope.sp1_verifying_key_codec != RADROOTS_SP1_TRADE_VERIFYING_KEY_CODEC - || envelope.sp1_verifying_key_base64.is_empty() - { - return Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode( - "proof envelope metadata mismatch".to_owned(), - )); - } - if envelope.proof_digest != proof_digest_for_envelope(envelope)? { - return Err(RadrootsSp1TradeHostError::ProofDigestMismatch); - } - let proof_bytes = proof_content_bytes_from_envelope(envelope)?; - if envelope.proof_content_hash != hash_bytes("radroots:sp1-proof-content:v1", &proof_bytes) { - return Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode( - "proof envelope content hash mismatch".to_owned(), - )); - } - let expected_canonical_public_values_hash = hash_bytes( - "radroots:sp1-canonical-public-values:v1", - &execution.canonical_public_values, - ); - if envelope.canonical_public_values_hash != expected_canonical_public_values_hash { - return Err(RadrootsSp1TradeHostError::PublicValuesHashMismatch); - } - if envelope.receipt_type != RadrootsValidationReceiptType::TradeTransition.as_str() { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "receipt_type", - )); - } - if envelope.receipt_result - != validation_receipt_result_label(validation_receipt_result_from_public_values( - execution.public_values.result, - )) - { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "result", - )); - } - if envelope.listing_event_id.as_str() - != execution - .public_values - .listing_event_id - .as_deref() - .unwrap_or("") - { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "listing_event_id", - )); - } - if envelope.root_event_id.as_str() - != execution - .public_values - .root_event_id - .as_deref() - .unwrap_or("") - { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "root_event_id", - )); - } - if envelope.target_event_id.as_str() - != execution - .public_values - .target_event_id - .as_deref() - .unwrap_or("") - { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "target_event_id", - )); - } - if envelope.event_set_root != execution.public_values.event_set_root - || envelope.previous_state_root != execution.public_values.previous_state_root - || envelope.new_state_root != execution.public_values.new_state_root - || envelope.changed_records_root != execution.public_values.changed_records_root - || envelope.error_bitmap != execution.public_values.error_bitmap - { - return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( - "state_roots", - )); - } - Ok(()) -} - -fn hex_lower(bytes: &[u8]) -> String { - const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut out = String::with_capacity(bytes.len() * 2); - for byte in bytes { - out.push(HEX[(byte >> 4) as usize] as char); - out.push(HEX[(byte & 0x0f) as usize] as char); - } - out -} - -fn is_lower_hex(value: &str) -> bool { - value - .bytes() - .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) -} - -#[cfg(any( - feature = "sp1_proving", - all(feature = "sp1_verify", radroots_sp1_guest_elf) -))] -fn sp1_program_hash_for_elf(elf: &sp1_sdk::Elf) -> String { - let bytes: &[u8] = match elf { - sp1_sdk::Elf::Static(bytes) => bytes, - sp1_sdk::Elf::Dynamic(bytes) => bytes, - }; - hash_bytes("radroots:sp1-guest-elf:v1", bytes) -} - -fn hash_bytes(domain: &'static str, bytes: &[u8]) -> String { - let mut hasher = Sha256::new(); - hasher.update(domain.as_bytes()); - hasher.update(bytes); - format!("0x{}", hex_lower(hasher.finalize().as_slice())) -} - -#[cfg(feature = "sp1_proving")] -fn witness_with_sp1_identity( - witness: &RadrootsSp1TradeOrderAcceptanceWitness, - sp1_program_hash: Option<String>, - sp1_verifying_key_hash: Option<String>, -) -> Result<RadrootsSp1TradeOrderAcceptanceWitness, RadrootsSp1TradeHostError> { - if let (Some(existing), Some(actual)) = ( - witness.sp1_program_hash.as_deref(), - sp1_program_hash.as_deref(), - ) && existing != actual - { - return Err(RadrootsSp1TradeHostError::Sp1ProgramHashMismatch); - } - if let (Some(existing), Some(actual)) = ( - witness.sp1_verifying_key_hash.as_deref(), - sp1_verifying_key_hash.as_deref(), - ) && existing != actual - { - return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch); - } - - let mut bound = witness.clone(); - if let Some(hash) = sp1_program_hash { - bound.sp1_program_hash = Some(hash); - } - if let Some(hash) = sp1_verifying_key_hash { - bound.sp1_verifying_key_hash = Some(hash); - } - Ok(bound) -} - -#[cfg(feature = "sp1_verify")] -fn public_values_prefix(bytes: &[u8]) -> String { - const PREFIX_LEN: usize = 32; - hex_lower(&bytes[..bytes.len().min(PREFIX_LEN)]) -} - -#[cfg(feature = "sp1_verify")] -fn execution_from_sp1_public_values( - public_values: sp1_sdk::SP1PublicValues, -) -> Result<(Vec<u8>, RadrootsSp1TradePublicValuesExecution), RadrootsSp1TradeHostError> { - let committed_public_values = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let mut public_values = public_values; - public_values.read::<Vec<u8>>() - })) - .map_err(|_| { - RadrootsSp1TradeHostError::Sp1PublicValuesDecode( - "SP1 public values stream did not contain canonical bytes".to_string(), - ) - })?; - let decoded: radroots_sp1_guest_trade::RadrootsSp1TradeProofPublicValues = - serde_json::from_slice(&committed_public_values).map_err(|error| { - RadrootsSp1TradeHostError::Sp1PublicValuesDecode(format!( - "{error}; public values bytes={}; prefix={}", - committed_public_values.len(), - public_values_prefix(&committed_public_values) - )) - })?; - let canonical_public_values = radroots_sp1_guest_trade::canonical_public_values_bytes(&decoded) - .map_err(|error| RadrootsSp1TradeHostError::Sp1PublicValuesDecode(error.to_string()))?; - let public_values_hash = radroots_sp1_guest_trade::public_values_hash_hex(&decoded)?; - Ok(( - committed_public_values, - RadrootsSp1TradePublicValuesExecution { - public_values: decoded, - public_values_hash, - canonical_public_values, - }, - )) -} - -#[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] -fn sp1_proof_mode( - mode: RadrootsSp1TradeProofMode, -) -> Result<sp1_sdk::SP1ProofMode, RadrootsSp1TradeHostError> { - match mode { - RadrootsSp1TradeProofMode::None => Err(RadrootsSp1TradeHostError::Sp1ProofModeRequired), - RadrootsSp1TradeProofMode::Core => Ok(sp1_sdk::SP1ProofMode::Core), - RadrootsSp1TradeProofMode::Compressed => Ok(sp1_sdk::SP1ProofMode::Compressed), - RadrootsSp1TradeProofMode::Groth16 => Ok(sp1_sdk::SP1ProofMode::Groth16), - RadrootsSp1TradeProofMode::Plonk => Ok(sp1_sdk::SP1ProofMode::Plonk), - } -} - -#[cfg(feature = "sp1_verify")] -fn artifact_proof_mode( - artifact: &RadrootsSp1TradeProofArtifact, -) -> Result<RadrootsSp1TradeProofMode, RadrootsSp1TradeHostError> { - match artifact.system { - RadrootsValidationReceiptProofSystem::None => { - Err(RadrootsSp1TradeHostError::Sp1ProofModeRequired) - } - RadrootsValidationReceiptProofSystem::Sp1Core => Ok(RadrootsSp1TradeProofMode::Core), - RadrootsValidationReceiptProofSystem::Sp1Compressed => { - Ok(RadrootsSp1TradeProofMode::Compressed) - } - RadrootsValidationReceiptProofSystem::Sp1Groth16 => Ok(RadrootsSp1TradeProofMode::Groth16), - RadrootsValidationReceiptProofSystem::Sp1Plonk => Ok(RadrootsSp1TradeProofMode::Plonk), - } -} - -#[cfg(feature = "sp1_verify")] -fn decode_sp1_proof_artifact( - artifact: &RadrootsSp1TradeProofArtifact, -) -> Result<sp1_sdk::SP1ProofWithPublicValues, RadrootsSp1TradeHostError> { - let envelope = decode_proof_envelope(artifact)?; - decode_sp1_proof_envelope(&envelope) -} - -#[cfg(feature = "sp1_verify")] -fn decode_sp1_proof_envelope( - envelope: &RadrootsSp1TradeProofEnvelope, -) -> Result<sp1_sdk::SP1ProofWithPublicValues, RadrootsSp1TradeHostError> { - let proof_bytes = proof_content_bytes_from_envelope(envelope)?; - bincode::deserialize::<sp1_sdk::SP1ProofWithPublicValues>(&proof_bytes) - .map_err(|error| RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(error.to_string())) -} - -#[cfg(feature = "sp1_verify")] -fn decode_sp1_verifying_key_envelope( - envelope: &RadrootsSp1TradeProofEnvelope, -) -> Result<sp1_sdk::SP1VerifyingKey, RadrootsSp1TradeHostError> { - if envelope.sp1_verifying_key_codec != RADROOTS_SP1_TRADE_VERIFYING_KEY_CODEC { - return Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode( - "proof envelope verifying key codec mismatch".to_owned(), - )); - } - let verifying_key_bytes = base64::engine::general_purpose::STANDARD - .decode(envelope.sp1_verifying_key_base64.as_str()) - .map_err(|error| RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(error.to_string()))?; - bincode::deserialize::<sp1_sdk::SP1VerifyingKey>(&verifying_key_bytes) - .map_err(|error| RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(error.to_string())) -} - -#[cfg(feature = "sp1_verify")] -fn sp1_proof_material_is_real(proof: &sp1_sdk::SP1Proof) -> bool { - match proof { - sp1_sdk::SP1Proof::Core(chunks) => !chunks.is_empty(), - sp1_sdk::SP1Proof::Compressed(_) => true, - sp1_sdk::SP1Proof::Groth16(proof) => !proof.encoded_proof.is_empty(), - sp1_sdk::SP1Proof::Plonk(proof) => !proof.encoded_proof.is_empty(), - } -} - -#[cfg(feature = "sp1_verify")] -fn sp1_proof_matches_mode(proof: &sp1_sdk::SP1Proof, mode: RadrootsSp1TradeProofMode) -> bool { - matches!( - (proof, mode), - (sp1_sdk::SP1Proof::Core(_), RadrootsSp1TradeProofMode::Core) - | ( - sp1_sdk::SP1Proof::Compressed(_), - RadrootsSp1TradeProofMode::Compressed - ) - | ( - sp1_sdk::SP1Proof::Groth16(_), - RadrootsSp1TradeProofMode::Groth16 - ) - | ( - sp1_sdk::SP1Proof::Plonk(_), - RadrootsSp1TradeProofMode::Plonk - ) - ) -} - -#[derive(Serialize)] -struct ProofDigestMaterial<'a> { - canonical_public_values: &'a [u8], - mode: Option<&'a str>, - program_hash: Option<&'a str>, - proof_reference: Option<&'a str>, - public_values_hash: &'a str, - system: &'a str, - verifying_key_hash: Option<&'a str>, -} - -#[derive(Serialize)] -struct ProofEnvelopeDigestMaterial<'a> { - schema_version: u32, - sp1_version_line: &'a str, - proof_system: &'a str, - proof_mode: &'a str, - proof_codec: &'a str, - proof_content_hash: &'a str, - public_values_hash: &'a str, - canonical_public_values_hash: &'a str, - sp1_program_hash: &'a str, - sp1_verifying_key_hash: &'a str, - sp1_verifying_key_codec: &'a str, - sp1_verifying_key_base64: &'a str, - receipt_type: &'a str, - receipt_result: &'a str, - listing_event_id: &'a str, - root_event_id: &'a str, - target_event_id: &'a str, - event_set_root: &'a str, - previous_state_root: &'a str, - new_state_root: &'a str, - changed_records_root: &'a str, - error_bitmap: &'a str, -} - -#[cfg(test)] -#[cfg_attr(coverage_nightly, coverage(off))] -mod tests { - use super::{ - RadrootsSp1TradeHostError, RadrootsSp1TradeProofMode, generate_order_acceptance_proof, - validation_receipt_for_order_acceptance_proof, validation_receipt_result_label, - verify_order_acceptance_proof_artifact_structure, - }; - use base64::Engine; - use radroots_event::{RadrootsEventEnvelope, kinds::KIND_TRADE_VALIDATION_RECEIPT}; - use radroots_sp1_guest_trade::{ - RADROOTS_SP1_TRADE_KIND_LISTING, RADROOTS_SP1_TRADE_KIND_ORDER_DECISION, - RADROOTS_SP1_TRADE_KIND_ORDER_REQUEST, RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET, - RADROOTS_SP1_TRADE_PROTOCOL_VERSION, RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH, - RADROOTS_SP1_TRADE_WITNESS_VERSION, RadrootsSp1TradeCanonicalEventEvidence, - RadrootsSp1TradeEventEvidenceRole, RadrootsSp1TradeEventWorkflowPosition, - RadrootsSp1TradeInventoryBinWitness, RadrootsSp1TradeInventoryCommitmentWitness, - RadrootsSp1TradeOrderAcceptanceWitness, RadrootsSp1TradeOrderDecisionEventWitness, - RadrootsSp1TradeOrderDecisionWitness, RadrootsSp1TradeOrderItemWitness, - RadrootsSp1TradeOrderRequestWitness, RadrootsSp1TradeProofResult, - RadrootsSp1TradePublicValuesExecution, - }; - use radroots_trade::validation_receipt::{ - RadrootsValidationReceiptExpectedBinding, RadrootsValidationReceiptProof, - RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, - RadrootsValidationReceiptType, validation_receipt_event_build, - verify_validation_receipt_event, - }; - #[cfg(feature = "sp1_verify")] - use serde::Deserialize; - - type ProofEnvelopeMutation = (&'static str, fn(&mut super::RadrootsSp1TradeProofEnvelope)); - type PublicValuesExecutionMutation = - (&'static str, fn(&mut RadrootsSp1TradePublicValuesExecution)); - - fn witness() -> RadrootsSp1TradeOrderAcceptanceWitness { - RadrootsSp1TradeOrderAcceptanceWitness { - witness_version: RADROOTS_SP1_TRADE_WITNESS_VERSION, - proof_target: RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET.to_string(), - listing_event_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), - request_event_id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - .to_string(), - decision_event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), - event_evidence: event_evidence(), - request: request(2), - decision: decision(2), - inventory_bins: vec![RadrootsSp1TradeInventoryBinWitness { - bin_id: "bin-1".to_string(), - listing_capacity: 5, - previous_reserved: 1, - }], - inventory_sequence: 7, - previous_state_root: None, - reducer_program_hash: RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH.to_string(), - radroots_protocol_version: RADROOTS_SP1_TRADE_PROTOCOL_VERSION.to_string(), - sp1_program_hash: Some( - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), - ), - sp1_verifying_key_hash: Some( - "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), - ), - } - } - - #[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] - fn witness_without_sp1_identity() -> RadrootsSp1TradeOrderAcceptanceWitness { - let mut input = witness(); - input.sp1_program_hash = None; - input.sp1_verifying_key_hash = None; - input - } - - fn event_evidence() -> Vec<RadrootsSp1TradeCanonicalEventEvidence> { - vec![ - RadrootsSp1TradeCanonicalEventEvidence { - event_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), - signer_pubkey: "1111111111111111111111111111111111111111111111111111111111111111" - .to_string(), - kind: RADROOTS_SP1_TRADE_KIND_LISTING, - canonical_event_hash: - "0x1010101010101010101010101010101010101010101010101010101010101010".to_string(), - signature_hash: - "0x1111111111111111111111111111111111111111111111111111111111111111".to_string(), - preverified_signature: true, - role: RadrootsSp1TradeEventEvidenceRole::Seller, - workflow_position: RadrootsSp1TradeEventWorkflowPosition::Listing, - content_hash: "0x1212121212121212121212121212121212121212121212121212121212121212" - .to_string(), - tags_hash: "0x1313131313131313131313131313131313131313131313131313131313131313" - .to_string(), - ordering_key: "001:listing".to_string(), - }, - RadrootsSp1TradeCanonicalEventEvidence { - event_id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - .to_string(), - signer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222" - .to_string(), - kind: RADROOTS_SP1_TRADE_KIND_ORDER_REQUEST, - canonical_event_hash: - "0x2020202020202020202020202020202020202020202020202020202020202020".to_string(), - signature_hash: - "0x2121212121212121212121212121212121212121212121212121212121212121".to_string(), - preverified_signature: true, - role: RadrootsSp1TradeEventEvidenceRole::Buyer, - workflow_position: RadrootsSp1TradeEventWorkflowPosition::OrderRequest, - content_hash: "0x2222222222222222222222222222222222222222222222222222222222222222" - .to_string(), - tags_hash: "0x2323232323232323232323232323232323232323232323232323232323232323" - .to_string(), - ordering_key: "002:order_request".to_string(), - }, - RadrootsSp1TradeCanonicalEventEvidence { - event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), - signer_pubkey: "1111111111111111111111111111111111111111111111111111111111111111" - .to_string(), - kind: RADROOTS_SP1_TRADE_KIND_ORDER_DECISION, - canonical_event_hash: - "0x3030303030303030303030303030303030303030303030303030303030303030".to_string(), - signature_hash: - "0x3131313131313131313131313131313131313131313131313131313131313131".to_string(), - preverified_signature: true, - role: RadrootsSp1TradeEventEvidenceRole::Seller, - workflow_position: RadrootsSp1TradeEventWorkflowPosition::OrderDecision, - content_hash: "0x3232323232323232323232323232323232323232323232323232323232323232" - .to_string(), - tags_hash: "0x3333333333333333333333333333333333333333333333333333333333333333" - .to_string(), - ordering_key: "003:order_decision".to_string(), - }, - ] - } - - fn request(bin_count: u32) -> RadrootsSp1TradeOrderRequestWitness { - RadrootsSp1TradeOrderRequestWitness { - order_id: "order-1".to_string(), - listing_addr: - "30402:1111111111111111111111111111111111111111111111111111111111111111:listing-1" - .to_string(), - buyer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222" - .to_string(), - seller_pubkey: "1111111111111111111111111111111111111111111111111111111111111111" - .to_string(), - items: vec![RadrootsSp1TradeOrderItemWitness { - bin_id: "bin-1".to_string(), - bin_count, - }], - } - } - - fn decision(bin_count: u32) -> RadrootsSp1TradeOrderDecisionEventWitness { - RadrootsSp1TradeOrderDecisionEventWitness { - order_id: "order-1".to_string(), - listing_addr: - "30402:1111111111111111111111111111111111111111111111111111111111111111:listing-1" - .to_string(), - buyer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222" - .to_string(), - seller_pubkey: "1111111111111111111111111111111111111111111111111111111111111111" - .to_string(), - decision: RadrootsSp1TradeOrderDecisionWitness::Accepted { - inventory_commitments: vec![RadrootsSp1TradeInventoryCommitmentWitness { - bin_id: "bin-1".to_string(), - bin_count, - }], - }, - } - } - - fn inline_sp1_artifact_fixture() -> ( - super::RadrootsSp1TradeProofBundle, - super::RadrootsSp1TradeProofEnvelope, - ) { - let bundle = generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) - .expect("proof bundle"); - let execution = &bundle.execution; - let proof_bytes = b"deterministic proof bytes"; - let verifying_key_bytes = b"deterministic verifying key bytes"; - let public_values_hash = execution.public_values_hash.clone(); - let canonical_public_values = execution.canonical_public_values.clone(); - let result = execution.public_values.result; - let program_hash = execution - .public_values - .sp1_program_hash - .clone() - .expect("program hash"); - let verifying_key_hash = execution - .public_values - .sp1_verifying_key_hash - .clone() - .expect("verifying key hash"); - let listing_event_id = execution - .public_values - .listing_event_id - .clone() - .expect("listing event id"); - let root_event_id = execution - .public_values - .root_event_id - .clone() - .expect("root event id"); - let target_event_id = execution - .public_values - .target_event_id - .clone() - .expect("target event id"); - let event_set_root = execution.public_values.event_set_root.clone(); - let previous_state_root = execution.public_values.previous_state_root.clone(); - let new_state_root = execution.public_values.new_state_root.clone(); - let changed_records_root = execution.public_values.changed_records_root.clone(); - let error_bitmap = execution.public_values.error_bitmap.clone(); - let mut envelope = super::RadrootsSp1TradeProofEnvelope { - schema_version: super::RADROOTS_SP1_TRADE_PROOF_ARTIFACT_SCHEMA_VERSION, - sp1_version_line: super::RADROOTS_SP1_TRADE_SP1_VERSION_LINE.to_string(), - proof_system: RadrootsValidationReceiptProofSystem::Sp1Core - .as_str() - .to_string(), - proof_mode: "core".to_string(), - proof_codec: super::RADROOTS_SP1_TRADE_PROOF_CODEC.to_string(), - proof_content_hash: super::hash_bytes("radroots:sp1-proof-content:v1", proof_bytes), - proof_digest: String::new(), - public_values_hash: public_values_hash.clone(), - canonical_public_values_hash: super::hash_bytes( - "radroots:sp1-canonical-public-values:v1", - &canonical_public_values, - ), - sp1_program_hash: program_hash.clone(), - sp1_verifying_key_hash: verifying_key_hash.clone(), - sp1_verifying_key_codec: super::RADROOTS_SP1_TRADE_VERIFYING_KEY_CODEC.to_string(), - sp1_verifying_key_base64: base64::engine::general_purpose::STANDARD - .encode(verifying_key_bytes), - receipt_type: RadrootsValidationReceiptType::TradeTransition - .as_str() - .to_string(), - receipt_result: super::validation_receipt_result_label( - super::validation_receipt_result_from_public_values(result), - ) - .to_string(), - listing_event_id, - root_event_id, - target_event_id, - event_set_root, - previous_state_root, - new_state_root, - changed_records_root, - error_bitmap, - proof_content_base64: base64::engine::general_purpose::STANDARD.encode(proof_bytes), - }; - envelope.proof_digest = super::proof_digest_for_envelope(&envelope).expect("digest"); - let envelope_json = serde_json::to_vec(&envelope).expect("envelope json"); - let envelope_base64 = base64::engine::general_purpose::STANDARD.encode(envelope_json); - let mut bundle = bundle; - bundle.proof = super::RadrootsSp1TradeProofArtifact { - inline_proof_base64: Some(envelope_base64), - mode: Some("core".to_string()), - program_hash: Some(program_hash), - proof_digest: envelope.proof_digest.clone(), - proof_reference: None, - public_values_hash, - system: RadrootsValidationReceiptProofSystem::Sp1Core, - verifying_key_hash: Some(verifying_key_hash), - }; - (bundle, envelope) - } - - fn refresh_proof_envelope_digest(envelope: &mut super::RadrootsSp1TradeProofEnvelope) { - envelope.proof_digest = super::proof_digest_for_envelope(envelope).expect("digest"); - } - - fn assert_missing_none_proof_material( - execution: &RadrootsSp1TradePublicValuesExecution, - mut artifact: super::RadrootsSp1TradeProofArtifact, - ) { - artifact.proof_digest = - super::proof_digest_for_execution(execution, &artifact).expect("digest"); - assert_eq!( - verify_order_acceptance_proof_artifact_structure(execution, &artifact) - .expect_err("none material"), - RadrootsSp1TradeHostError::MissingProofMaterial - ); - } - - #[test] - fn enum_labels_and_display_surfaces_are_complete() { - let modes = [ - ( - RadrootsSp1TradeProofMode::None, - None, - RadrootsValidationReceiptProofSystem::None, - ), - ( - RadrootsSp1TradeProofMode::Core, - Some("core"), - RadrootsValidationReceiptProofSystem::Sp1Core, - ), - ( - RadrootsSp1TradeProofMode::Compressed, - Some("compressed"), - RadrootsValidationReceiptProofSystem::Sp1Compressed, - ), - ( - RadrootsSp1TradeProofMode::Groth16, - Some("groth16"), - RadrootsValidationReceiptProofSystem::Sp1Groth16, - ), - ( - RadrootsSp1TradeProofMode::Plonk, - Some("plonk"), - RadrootsValidationReceiptProofSystem::Sp1Plonk, - ), - ]; - for (mode, label, system) in modes { - assert_eq!(mode.proof_system(), system); - assert_eq!(mode.mode_label(), label); - if let Some(label) = label { - assert_eq!(RadrootsSp1TradeProofMode::from_label(label), Some(mode)); - } - } - assert_eq!( - RadrootsSp1TradeProofMode::from_label("none"), - Some(RadrootsSp1TradeProofMode::None) - ); - assert_eq!(RadrootsSp1TradeProofMode::from_label("legacy"), None); - - for backend in [ - super::RadrootsSp1TradeProverBackend::Disabled, - super::RadrootsSp1TradeProverBackend::DeterministicNone, - super::RadrootsSp1TradeProverBackend::LocalExecute, - super::RadrootsSp1TradeProverBackend::LocalCpuProve, - super::RadrootsSp1TradeProverBackend::LocalCudaProve, - super::RadrootsSp1TradeProverBackend::RemoteHttpProve, - ] { - assert_eq!( - super::RadrootsSp1TradeProverBackend::from_label(backend.as_str()), - Some(backend) - ); - assert_eq!(backend.to_string(), backend.as_str()); - } - assert_eq!( - super::RadrootsSp1TradeProverBackend::from_label("legacy"), - None - ); - - for engine in [ - super::RadrootsSp1TradeProofEngine::Cpu, - super::RadrootsSp1TradeProofEngine::Cuda, - ] { - assert_eq!( - super::RadrootsSp1TradeProofEngine::from_label(engine.as_str()), - Some(engine) - ); - assert_eq!(engine.to_string(), engine.as_str()); - } - assert_eq!(super::RadrootsSp1TradeProofEngine::from_label("tpu"), None); - assert_eq!( - super::RadrootsSp1TradeWorkerResultStatus::Succeeded.to_string(), - "succeeded" - ); - assert_eq!( - super::RadrootsSp1TradeWorkerRole::NonAuthoritativeProver.to_string(), - "non_authoritative_prover" - ); - let artifact = generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) - .expect("proof bundle") - .proof; - assert_eq!( - super::RadrootsSp1TradeResolvedProofArtifact::inline(artifact.clone()).artifact, - artifact - ); - assert_eq!( - RadrootsSp1TradeHostError::from( - radroots_sp1_guest_trade::RadrootsSp1TradeGuestError::UnsupportedProofTarget - ), - RadrootsSp1TradeHostError::Guest - ); - } - - #[test] - fn inline_proof_envelope_helpers_validate_digest_and_references() { - let (bundle, envelope) = inline_sp1_artifact_fixture(); - verify_order_acceptance_proof_artifact_structure(&bundle.execution, &bundle.proof) - .expect("inline artifact structure"); - let envelope_base64 = bundle - .proof - .inline_proof_base64 - .as_deref() - .expect("inline envelope"); - let decoded = super::decode_proof_envelope_base64(envelope_base64).expect("decode"); - assert_eq!(decoded, envelope); - assert_eq!( - super::proof_content_bytes_from_envelope(&decoded).expect("content"), - b"deterministic proof bytes" - ); - let reference = - super::proof_reference_for_proof_envelope_base64(envelope_base64).expect("reference"); - assert_eq!( - super::proof_reference_digest(reference.as_str()) - .expect("digest") - .len(), - 64 - ); - assert!(matches!( - super::proof_reference_for_proof_envelope_base64("not-base64"), - Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) - )); - assert!(matches!( - super::decode_proof_envelope_base64( - base64::engine::general_purpose::STANDARD - .encode(b"not json") - .as_str() - ), - Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) - )); - assert_eq!( - super::proof_reference_digest("legacy-reference").expect_err("prefix"), - RadrootsSp1TradeHostError::InvalidSp1ProofReference - ); - } - - #[test] - fn proof_envelope_metadata_mismatches_are_reported_by_field_family() { - let (bundle, envelope) = inline_sp1_artifact_fixture(); - let execution = &bundle.execution; - let artifact = &bundle.proof; - - let mut bad = envelope.clone(); - bad.schema_version += 1; - assert!(matches!( - super::verify_proof_envelope(execution, artifact, &bad), - Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) - )); - - let metadata_cases: [fn(&mut super::RadrootsSp1TradeProofEnvelope); 9] = [ - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.sp1_version_line = "legacy".to_string(); - }, - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.proof_codec = "legacy".to_string(); - }, - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.proof_system = "legacy".to_string(); - }, - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.proof_mode = "compressed".to_string(); - }, - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.public_values_hash = - "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" - .to_string(); - }, - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.sp1_program_hash = - "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" - .to_string(); - }, - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.sp1_verifying_key_hash = - "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" - .to_string(); - }, - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.sp1_verifying_key_codec = "legacy".to_string(); - }, - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.sp1_verifying_key_base64.clear(); - }, - ]; - for apply in metadata_cases { - let mut bad = envelope.clone(); - apply(&mut bad); - assert!(matches!( - super::verify_proof_envelope(execution, artifact, &bad), - Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) - )); - } - - let mut bad = envelope.clone(); - bad.proof_digest = - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); - assert_eq!( - super::verify_proof_envelope(execution, artifact, &bad).expect_err("digest"), - RadrootsSp1TradeHostError::ProofDigestMismatch - ); - - let mut bad = envelope.clone(); - bad.proof_content_hash = - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); - refresh_proof_envelope_digest(&mut bad); - assert!(matches!( - super::verify_proof_envelope(execution, artifact, &bad), - Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) - )); - - let mut bad = envelope.clone(); - bad.proof_content_base64 = "not-base64".to_string(); - refresh_proof_envelope_digest(&mut bad); - assert!(matches!( - super::verify_proof_envelope(execution, artifact, &bad), - Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) - )); - - let mut bad = envelope.clone(); - bad.canonical_public_values_hash = - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); - refresh_proof_envelope_digest(&mut bad); - assert_eq!( - super::verify_proof_envelope(execution, artifact, &bad).expect_err("public values"), - RadrootsSp1TradeHostError::PublicValuesHashMismatch - ); - - let envelope_cases: [ProofEnvelopeMutation; 10] = [ - ( - "receipt_type", - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.receipt_type = "legacy".to_string(); - }, - ), - ( - "result", - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.receipt_result = "invalid".to_string(); - }, - ), - ( - "listing_event_id", - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.listing_event_id = - "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" - .to_string(); - }, - ), - ( - "root_event_id", - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.root_event_id = - "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" - .to_string(); - }, - ), - ( - "target_event_id", - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.target_event_id = - "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" - .to_string(); - }, - ), - ( - "state_roots", - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.event_set_root = - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(); - }, - ), - ( - "state_roots", - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.previous_state_root = - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(); - }, - ), - ( - "state_roots", - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.new_state_root = - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(); - }, - ), - ( - "state_roots", - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.changed_records_root = - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(); - }, - ), - ( - "state_roots", - |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { - envelope.error_bitmap = - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(); - }, - ), - ]; - for (field, apply) in envelope_cases { - let mut bad = envelope.clone(); - apply(&mut bad); - refresh_proof_envelope_digest(&mut bad); - assert_eq!( - super::verify_proof_envelope(execution, artifact, &bad) - .expect_err("binding mismatch"), - RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch(field) - ); - } - } - - #[test] - fn proof_artifact_structure_rejects_material_edge_cases() { - let (bundle, _) = inline_sp1_artifact_fixture(); - let execution = &bundle.execution; - let inline_proof_base64 = bundle - .proof - .inline_proof_base64 - .clone() - .expect("inline envelope"); - - let base_none_artifact = - generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) - .expect("proof bundle") - .proof; - - let mut artifact = base_none_artifact.clone(); - artifact.proof_digest = - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); - assert_eq!( - verify_order_acceptance_proof_artifact_structure(execution, &artifact) - .expect_err("proof digest"), - RadrootsSp1TradeHostError::ProofDigestMismatch - ); - - let mut artifact = base_none_artifact.clone(); - artifact.inline_proof_base64 = Some(inline_proof_base64); - assert_missing_none_proof_material(execution, artifact); - - let mut artifact = base_none_artifact.clone(); - artifact.mode = Some("core".to_string()); - assert_missing_none_proof_material(execution, artifact); - - let mut artifact = base_none_artifact.clone(); - artifact.program_hash = - Some("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()); - assert_missing_none_proof_material(execution, artifact); - - let mut artifact = base_none_artifact.clone(); - artifact.proof_reference = Some(format!("radroots-proof://sha256/{}", "1".repeat(64))); - assert_missing_none_proof_material(execution, artifact); - - let mut artifact = base_none_artifact; - artifact.verifying_key_hash = - Some("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()); - assert_missing_none_proof_material(execution, artifact); - - let mut artifact = bundle.proof.clone(); - artifact.inline_proof_base64 = None; - artifact.proof_reference = None; - artifact.proof_digest = - super::proof_digest_for_execution(execution, &artifact).expect("digest"); - assert_eq!( - verify_order_acceptance_proof_artifact_structure(execution, &artifact) - .expect_err("missing sp1 material"), - RadrootsSp1TradeHostError::MissingProofMaterial - ); - - let mut artifact = bundle.proof.clone(); - artifact.proof_reference = Some(format!("radroots-proof://sha256/{}", "1".repeat(64))); - artifact.proof_digest = - super::proof_digest_for_execution(execution, &artifact).expect("digest"); - assert_eq!( - verify_order_acceptance_proof_artifact_structure(execution, &artifact) - .expect_err("conflict"), - RadrootsSp1TradeHostError::ProofMaterialConflict - ); - - let mut artifact = bundle.proof.clone(); - artifact.verifying_key_hash = - Some("0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string()); - artifact.proof_digest = - super::proof_digest_for_execution(execution, &artifact).expect("digest"); - assert_eq!( - verify_order_acceptance_proof_artifact_structure(execution, &artifact) - .expect_err("verifying key mismatch"), - RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch - ); - - let invalid_reference = format!("radroots-proof://sha256/{}", "A".repeat(64)); - assert_eq!( - super::proof_reference_digest(invalid_reference.as_str()) - .expect_err("uppercase digest"), - RadrootsSp1TradeHostError::InvalidSp1ProofReference - ); - let err = super::referenced_order_acceptance_proof_artifact_for_execution( - execution, - RadrootsSp1TradeProofMode::None, - format!("radroots-proof://sha256/{}", "1".repeat(64)), - ) - .expect_err("none reference"); - assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProofModeRequired); - } - - #[test] - fn validation_receipt_requires_event_bindings() { - let bundle = generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) - .expect("proof bundle"); - let cases: [PublicValuesExecutionMutation; 3] = [ - ("listing_event_id", |execution| { - execution.public_values.listing_event_id = None; - }), - ("root_event_id", |execution| { - execution.public_values.root_event_id = None; - }), - ("target_event_id", |execution| { - execution.public_values.target_event_id = None; - }), - ]; - for (field, clear) in cases { - let mut bundle = bundle.clone(); - clear(&mut bundle.execution); - assert_eq!( - validation_receipt_for_order_acceptance_proof(&bundle) - .expect_err("missing binding"), - RadrootsSp1TradeHostError::MissingReceiptBinding(field) - ); - } - } - - #[test] - fn execute_public_values_and_bind_validation_receipt() { - let bundle = generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) - .expect("proof bundle"); - assert_eq!( - bundle.proof.system, - RadrootsValidationReceiptProofSystem::None - ); - verify_order_acceptance_proof_artifact_structure(&bundle.execution, &bundle.proof) - .expect("proof verifies"); - - let receipt = - validation_receipt_for_order_acceptance_proof(&bundle).expect("validation receipt"); - assert_eq!( - receipt.public_values_hash, - bundle.execution.public_values_hash - ); - assert_eq!( - receipt.event_set_root, - bundle.execution.public_values.event_set_root - ); - assert_eq!( - receipt.new_state_root, - bundle.execution.public_values.new_state_root - ); - - let parts = validation_receipt_event_build("order-1", &receipt).expect("event parts"); - let event = RadrootsEventEnvelope { - id: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string(), - author: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" - .to_string(), - created_at: 1, - kind: KIND_TRADE_VALIDATION_RECEIPT, - tags: parts.tags, - content: parts.content, - sig: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_string(), - }; - verify_validation_receipt_event( - &event, - RadrootsValidationReceiptExpectedBinding { - event_set_root: Some(&receipt.event_set_root), - order_id: Some("order-1"), - proof_system: Some(RadrootsValidationReceiptProofSystem::None), - public_values_hash: Some(&receipt.public_values_hash), - reducer_output_root: Some(&receipt.new_state_root), - ..RadrootsValidationReceiptExpectedBinding::default() - }, - ) - .expect("receipt verifies"); - } - - #[test] - fn proof_verifier_rejects_tampered_public_values_hash() { - let mut bundle = - generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) - .expect("proof bundle"); - bundle.proof.public_values_hash = - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); - let err = - verify_order_acceptance_proof_artifact_structure(&bundle.execution, &bundle.proof) - .expect_err("tamper"); - assert_eq!(err, RadrootsSp1TradeHostError::PublicValuesHashMismatch); - } - - #[test] - fn validation_receipt_result_tracks_public_values_result() { - let mut bundle = - generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) - .expect("proof bundle"); - bundle.execution.public_values.result = RadrootsSp1TradeProofResult::Invalid; - let receipt = - validation_receipt_for_order_acceptance_proof(&bundle).expect("validation receipt"); - assert_eq!(receipt.result, RadrootsValidationReceiptResult::Invalid); - assert_eq!(validation_receipt_result_label(receipt.result), "invalid"); - } - - #[test] - fn sp1_modes_require_the_sp1_proving_lane() { - let err = - generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::Compressed) - .expect_err("synthetic sp1 proof"); - assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProofGenerationRequired); - } - - #[test] - fn sp1_artifact_program_hash_must_match_public_values_identity() { - let mut input = witness(); - input.sp1_program_hash = - Some("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()); - let execution = - super::execute_order_acceptance_public_values(&input).expect("deterministic execution"); - let mut artifact = super::RadrootsSp1TradeProofArtifact { - inline_proof_base64: None, - mode: Some("core".to_string()), - program_hash: Some( - "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string(), - ), - proof_digest: String::new(), - proof_reference: Some(format!("radroots-proof://sha256/{}", "1".repeat(64))), - public_values_hash: execution.public_values_hash.clone(), - system: RadrootsValidationReceiptProofSystem::Sp1Core, - verifying_key_hash: execution.public_values.sp1_verifying_key_hash.clone(), - }; - artifact.proof_digest = - super::proof_digest_for_execution(&execution, &artifact).expect("proof digest"); - let err = verify_order_acceptance_proof_artifact_structure(&execution, &artifact) - .expect_err("program hash mismatch"); - assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProgramHashMismatch); - } - - #[test] - fn sp1_artifact_requires_public_values_sp1_identity() { - let mut input = witness(); - input.sp1_program_hash = None; - let execution = - super::execute_order_acceptance_public_values(&input).expect("deterministic execution"); - let mut artifact = super::RadrootsSp1TradeProofArtifact { - inline_proof_base64: None, - mode: Some("core".to_string()), - program_hash: Some( - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), - ), - proof_digest: String::new(), - proof_reference: Some(format!("radroots-proof://sha256/{}", "1".repeat(64))), - public_values_hash: execution.public_values_hash.clone(), - system: RadrootsValidationReceiptProofSystem::Sp1Core, - verifying_key_hash: execution.public_values.sp1_verifying_key_hash.clone(), - }; - artifact.proof_digest = - super::proof_digest_for_execution(&execution, &artifact).expect("proof digest"); - let err = verify_order_acceptance_proof_artifact_structure(&execution, &artifact) - .expect_err("missing program hash"); - assert_eq!(err, RadrootsSp1TradeHostError::MissingSp1ProgramHash); - } - - #[cfg(feature = "sp1_verify")] - #[tokio::test] - async fn verify_order_acceptance_validation_receipt_inline_sp1_proof_rejects_raw_material() { - let bundle = generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) - .expect("proof bundle"); - let mut receipt = - validation_receipt_for_order_acceptance_proof(&bundle).expect("validation receipt"); - receipt.proof = RadrootsValidationReceiptProof { - inline_proof_base64: Some("cHJvb2Y=".to_string()), - mode: Some("core".to_string()), - program_hash: Some( - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), - ), - proof_reference: None, - system: RadrootsValidationReceiptProofSystem::Sp1Core, - verifying_key_hash: Some( - "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), - ), - }; - let error = super::verify_order_acceptance_validation_receipt_inline_sp1_proof(&receipt) - .await - .expect_err("raw material"); - assert!(matches!( - error, - RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_) - )); - } - - #[test] - fn sp1_artifact_program_hash_is_distinct_from_reducer_hash() { - let mut input = witness(); - input.sp1_program_hash = - Some("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()); - let execution = - super::execute_order_acceptance_public_values(&input).expect("deterministic execution"); - let mut artifact = super::RadrootsSp1TradeProofArtifact { - inline_proof_base64: None, - mode: Some("core".to_string()), - program_hash: execution.public_values.sp1_program_hash.clone(), - proof_digest: String::new(), - proof_reference: Some(format!("radroots-proof://sha256/{}", "1".repeat(64))), - public_values_hash: execution.public_values_hash.clone(), - system: RadrootsValidationReceiptProofSystem::Sp1Core, - verifying_key_hash: execution.public_values.sp1_verifying_key_hash.clone(), - }; - artifact.proof_digest = - super::proof_digest_for_execution(&execution, &artifact).expect("proof digest"); - verify_order_acceptance_proof_artifact_structure(&execution, &artifact) - .expect("artifact verifies"); - assert_ne!( - artifact.program_hash.as_deref(), - Some(execution.public_values.reducer_program_hash.as_str()) - ); - } - - #[test] - fn proof_reference_requires_canonical_sha256_uri() { - let execution = - super::execute_order_acceptance_public_values(&witness()).expect("execution"); - let err = super::referenced_order_acceptance_proof_artifact_for_execution( - &execution, - RadrootsSp1TradeProofMode::Core, - "radroots-proof://sha256/xyz".to_string(), - ) - .expect_err("invalid reference"); - assert_eq!(err, RadrootsSp1TradeHostError::InvalidSp1ProofReference); - - let artifact = super::referenced_order_acceptance_proof_artifact_for_execution( - &execution, - RadrootsSp1TradeProofMode::Core, - format!("radroots-proof://sha256/{}", "1".repeat(64)), - ) - .expect("referenced artifact"); - verify_order_acceptance_proof_artifact_structure(&execution, &artifact) - .expect("referenced artifact is structurally valid"); - } - - #[cfg(not(feature = "sp1_verify"))] - #[test] - fn sp1_verification_apis_report_unavailable_without_sp1_verify_feature() { - let runtime = tokio::runtime::Runtime::new().expect("runtime"); - runtime.block_on(async { - let bundle = - generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) - .expect("proof bundle"); - let mut inline_receipt = - validation_receipt_for_order_acceptance_proof(&bundle).expect("validation receipt"); - inline_receipt.proof = RadrootsValidationReceiptProof { - inline_proof_base64: Some("cHJvb2Y=".to_string()), - mode: Some("core".to_string()), - program_hash: Some( - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), - ), - proof_reference: None, - system: RadrootsValidationReceiptProofSystem::Sp1Core, - verifying_key_hash: Some( - "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - .to_string(), - ), - }; - let err = - super::verify_order_acceptance_validation_receipt_inline_sp1_proof(&inline_receipt) - .await - .expect_err("verifier unavailable"); - assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProofVerifierUnavailable); - - let execution = - super::execute_order_acceptance_public_values(&witness()).expect("execution"); - let artifact = super::referenced_order_acceptance_proof_artifact_for_execution( - &execution, - RadrootsSp1TradeProofMode::Core, - format!("radroots-proof://sha256/{}", "1".repeat(64)), - ) - .expect("referenced artifact"); - let resolved = super::RadrootsSp1TradeResolvedProofArtifact { - artifact: artifact.clone(), - resolved_proof_envelope_base64: None, - }; - let err = - super::verify_order_acceptance_resolved_sp1_proof_artifact(&execution, &resolved) - .await - .expect_err("verifier unavailable"); - assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProofVerifierUnavailable); - - let mut referenced_receipt = - validation_receipt_for_order_acceptance_proof(&bundle).expect("validation receipt"); - referenced_receipt.proof = RadrootsValidationReceiptProof { - inline_proof_base64: artifact.inline_proof_base64.clone(), - mode: artifact.mode.clone(), - program_hash: artifact.program_hash.clone(), - proof_reference: artifact.proof_reference.clone(), - system: artifact.system, - verifying_key_hash: artifact.verifying_key_hash.clone(), - }; - let err = super::verify_order_acceptance_validation_receipt_resolved_sp1_proof( - &referenced_receipt, - &resolved, - ) - .await - .expect_err("verifier unavailable"); - assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProofVerifierUnavailable); - }); - } - - #[test] - fn remote_prover_contract_round_trips_provider_neutral_payloads() { - let request = super::RadrootsSp1TradeRemoteProverRequest { - schema_version: super::RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION, - request_id: "request-1".to_string(), - proof_target: RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET.to_string(), - proof_mode: RadrootsSp1TradeProofMode::Core, - sp1_version_line: super::RADROOTS_SP1_TRADE_SP1_VERSION_LINE.to_string(), - witness: witness(), - expected_sp1_program_hash: - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), - expected_sp1_verifying_key_hash: - "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), - expected_public_values_hash: - "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), - expected_reducer_program_hash: RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH.to_string(), - expected_protocol_version: RADROOTS_SP1_TRADE_PROTOCOL_VERSION.to_string(), - expected_witness_version: RADROOTS_SP1_TRADE_WITNESS_VERSION, - }; - let request_json = serde_json::to_string(&request).expect("request json"); - let decoded_request: super::RadrootsSp1TradeRemoteProverRequest = - serde_json::from_str(&request_json).expect("decoded request"); - assert_eq!(decoded_request, request); - - let response = super::RadrootsSp1TradeRemoteProverResponse { - schema_version: super::RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION, - request_id: "request-1".to_string(), - status: super::RadrootsSp1TradeRemoteProverStatus::Accepted, - status_url: None, - status_path: Some("/proofs/request-1".to_string()), - proof_system: None, - proof_mode: None, - public_values_hash: None, - sp1_program_hash: None, - sp1_verifying_key_hash: None, - proof_artifact: None, - resolved_proof_envelope_base64: None, - reason_code: None, - message: None, - detail: None, - }; - let response_json = serde_json::to_string(&response).expect("response json"); - let decoded_response: super::RadrootsSp1TradeRemoteProverResponse = - serde_json::from_str(&response_json).expect("decoded response"); - assert_eq!(decoded_response, response); - assert_eq!( - super::RadrootsSp1TradeProverBackend::from_label("remote_http_prove"), - Some(super::RadrootsSp1TradeProverBackend::RemoteHttpProve) - ); - } - - #[cfg(feature = "sp1_verify")] - #[derive(Deserialize)] - #[serde(deny_unknown_fields)] - struct RemoteReturnedProofFixture { - schema_version: u32, - fixture_id: String, - proof_target: String, - proof_mode: RadrootsSp1TradeProofMode, - proof_system: RadrootsValidationReceiptProofSystem, - sp1_version_line: String, - remote_prover_request: super::RadrootsSp1TradeRemoteProverRequest, - remote_prover_response: super::RadrootsSp1TradeRemoteProverResponse, - } - - #[cfg(feature = "sp1_verify")] - fn remote_returned_proof_fixture() -> RemoteReturnedProofFixture { - serde_json::from_str(include_str!( - "../tests/fixtures/remote_returned_proof_order_acceptance_core_v1.json" - )) - .expect("remote returned proof fixture") - } - - #[cfg(feature = "sp1_verify")] - fn verified_remote_returned_proof_fixture() -> ( - RadrootsSp1TradePublicValuesExecution, - super::RadrootsSp1TradeProofArtifact, - ) { - let fixture = remote_returned_proof_fixture(); - assert_eq!(fixture.schema_version, 1); - assert_eq!( - fixture.fixture_id, - "remote_returned_proof_order_acceptance_core_v1" - ); - assert_eq!( - fixture.proof_target, - RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET - ); - assert_eq!(fixture.proof_mode, RadrootsSp1TradeProofMode::Core); - assert_eq!( - fixture.proof_system, - RadrootsValidationReceiptProofSystem::Sp1Core - ); - assert_eq!( - fixture.sp1_version_line, - super::RADROOTS_SP1_TRADE_SP1_VERSION_LINE - ); - - let request = fixture.remote_prover_request; - let response = fixture.remote_prover_response; - let execution = super::execute_order_acceptance_public_values(&request.witness) - .expect("deterministic execution"); - assert_eq!( - execution.public_values_hash, - request.expected_public_values_hash - ); - assert_eq!( - execution.public_values.sp1_program_hash.as_deref(), - Some(request.expected_sp1_program_hash.as_str()) - ); - assert_eq!( - execution.public_values.sp1_verifying_key_hash.as_deref(), - Some(request.expected_sp1_verifying_key_hash.as_str()) - ); - assert_eq!( - response.status, - super::RadrootsSp1TradeRemoteProverStatus::Completed - ); - assert_eq!(response.request_id, request.request_id); - assert_eq!( - response.public_values_hash.as_deref(), - Some(request.expected_public_values_hash.as_str()) - ); - assert_eq!( - response.sp1_program_hash.as_deref(), - Some(request.expected_sp1_program_hash.as_str()) - ); - assert_eq!( - response.sp1_verifying_key_hash.as_deref(), - Some(request.expected_sp1_verifying_key_hash.as_str()) - ); - - let artifact = response.proof_artifact.expect("proof artifact"); - verify_order_acceptance_proof_artifact_structure(&execution, &artifact) - .expect("artifact structure"); - (execution, artifact) - } - - #[cfg(feature = "sp1_verify")] - #[test] - fn remote_returned_proof_artifact_fixture_is_structurally_valid() { - verified_remote_returned_proof_fixture(); - } - - #[cfg(all(feature = "sp1_verify", radroots_sp1_real_proof_tests))] - #[tokio::test] - async fn remote_returned_proof_artifact_real_sp1_verifies() { - let (execution, artifact) = verified_remote_returned_proof_fixture(); - super::verify_order_acceptance_resolved_sp1_proof_artifact( - &execution, - &super::RadrootsSp1TradeResolvedProofArtifact::inline(artifact.clone()), - ) - .await - .expect("remote proof verifies"); - - let mut digest_mismatch = artifact.clone(); - digest_mismatch.proof_digest = - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); - let err = verify_order_acceptance_proof_artifact_structure(&execution, &digest_mismatch) - .expect_err("digest mismatch"); - assert_eq!(err, RadrootsSp1TradeHostError::ProofDigestMismatch); - - let mut identity_mismatch = execution.clone(); - identity_mismatch.public_values.sp1_program_hash = - Some("0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string()); - let err = verify_order_acceptance_proof_artifact_structure(&identity_mismatch, &artifact) - .expect_err("identity mismatch"); - assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProgramHashMismatch); - - let mut public_values_mismatch = request.witness; - public_values_mismatch.inventory_sequence += 1; - let mismatch_execution = - super::execute_order_acceptance_public_values(&public_values_mismatch) - .expect("mismatch execution"); - let err = verify_order_acceptance_proof_artifact_structure(&mismatch_execution, &artifact) - .expect_err("public values mismatch"); - assert_eq!(err, RadrootsSp1TradeHostError::PublicValuesHashMismatch); - } - - #[cfg(feature = "sp1_verify")] - #[tokio::test] - async fn resolved_reference_rejects_unresolved_and_digest_mismatch() { - let execution = - super::execute_order_acceptance_public_values(&witness()).expect("execution"); - let artifact = super::referenced_order_acceptance_proof_artifact_for_execution( - &execution, - RadrootsSp1TradeProofMode::Core, - format!("radroots-proof://sha256/{}", "1".repeat(64)), - ) - .expect("referenced artifact"); - - let err = super::verify_order_acceptance_resolved_sp1_proof_artifact( - &execution, - &super::RadrootsSp1TradeResolvedProofArtifact { - artifact: artifact.clone(), - resolved_proof_envelope_base64: None, - }, - ) - .await - .expect_err("unresolved reference"); - assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProofReferenceUnresolved); - - let err = super::verify_order_acceptance_resolved_sp1_proof_artifact( - &execution, - &super::RadrootsSp1TradeResolvedProofArtifact { - artifact, - resolved_proof_envelope_base64: Some("cHJvb2Y=".to_string()), - }, - ) - .await - .expect_err("digest mismatch"); - assert_eq!( - err, - RadrootsSp1TradeHostError::Sp1ProofReferenceDigestMismatch - ); - } - - #[test] - fn none_proof_mode_builds_deterministic_reducer_receipt() { - let mut input = witness(); - input.sp1_verifying_key_hash = None; - let bundle = generate_order_acceptance_proof(&input, RadrootsSp1TradeProofMode::None) - .expect("none proof"); - assert_eq!( - bundle.proof.system, - RadrootsValidationReceiptProofSystem::None - ); - let receipt = - validation_receipt_for_order_acceptance_proof(&bundle).expect("validation receipt"); - assert_eq!( - receipt.proof.system, - RadrootsValidationReceiptProofSystem::None - ); - assert!(receipt.proof.inline_proof_base64.is_none()); - } - - #[test] - fn deterministic_crates_do_not_depend_on_sp1_sdk() { - let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); - let crates_dir = manifest_dir.parent().expect("crates dir"); - for crate_dir in ["events", "trade", "sp1_guest_trade"] { - let manifest = std::fs::read_to_string(crates_dir.join(crate_dir).join("Cargo.toml")) - .expect("manifest"); - assert!(!manifest.contains("sp1-sdk")); - assert!(!manifest.contains("sp1_sdk")); - } - } - - #[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] - #[tokio::test] - async fn sp1_execute_public_values_match_deterministic_reducer() { - let input = witness_without_sp1_identity(); - let execution = super::execute_order_acceptance_sp1_public_values(&input) - .await - .expect("sp1 execute"); - let mut expected_input = input; - expected_input.sp1_program_hash = - execution.execution.public_values.sp1_program_hash.clone(); - expected_input.sp1_verifying_key_hash = execution - .execution - .public_values - .sp1_verifying_key_hash - .clone(); - let expected = super::execute_order_acceptance_public_values(&expected_input) - .expect("deterministic execution"); - assert_eq!(execution.execution, expected); - assert_eq!( - execution.committed_public_values, - expected.canonical_public_values - ); - assert_eq!(execution.report.exit_code, 0); - assert!(execution.report.total_instruction_count > 0); - } - - #[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] - #[tokio::test] - async fn expensive_proof_generation_and_verification_is_runnable() { - let bundle = super::generate_order_acceptance_sp1_proof( - &witness_without_sp1_identity(), - RadrootsSp1TradeProofMode::Core, - ) - .await - .expect("proof bundle"); - super::verify_order_acceptance_resolved_sp1_proof_artifact( - &bundle.execution, - &super::RadrootsSp1TradeResolvedProofArtifact::inline(bundle.proof.clone()), - ) - .await - .expect("proof verifies"); - let receipt = - validation_receipt_for_order_acceptance_proof(&bundle).expect("validation receipt"); - let verification = - super::verify_order_acceptance_validation_receipt_inline_sp1_proof(&receipt) - .await - .expect("receipt proof verifies"); - assert_eq!( - bundle.proof.system, - RadrootsValidationReceiptProofSystem::Sp1Core - ); - assert!(bundle.proof.inline_proof_base64.is_some()); - assert_eq!(verification.proof_mode, RadrootsSp1TradeProofMode::Core); - assert_eq!(verification.public_values_hash, receipt.public_values_hash); - assert_eq!( - verification.sp1_program_hash, - receipt.proof.program_hash.expect("receipt program hash") - ); - assert_eq!( - verification.sp1_verifying_key_hash, - receipt - .proof - .verifying_key_hash - .expect("receipt verifying key hash") - ); - } - - #[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] - #[tokio::test] - async fn real_sp1_verifier_rejects_missing_and_synthetic_material() { - let execution = - super::execute_order_acceptance_sp1_public_values(&witness_without_sp1_identity()) - .await - .expect("sp1 execute") - .execution; - let mut missing = super::RadrootsSp1TradeProofArtifact { - inline_proof_base64: None, - mode: Some("core".to_string()), - program_hash: execution.public_values.sp1_program_hash.clone(), - proof_digest: "0x00".to_string(), - proof_reference: None, - public_values_hash: execution.public_values_hash.clone(), - system: RadrootsValidationReceiptProofSystem::Sp1Core, - verifying_key_hash: execution.public_values.sp1_verifying_key_hash.clone(), - }; - let err = super::verify_order_acceptance_resolved_sp1_proof_artifact( - &execution, - &super::RadrootsSp1TradeResolvedProofArtifact::inline(missing.clone()), - ) - .await - .expect_err("missing proof material"); - assert_eq!(err, RadrootsSp1TradeHostError::ProofDigestMismatch); - - missing.proof_digest = - super::proof_digest_for_execution(&execution, &missing).expect("missing proof digest"); - let err = super::verify_order_acceptance_resolved_sp1_proof_artifact( - &execution, - &super::RadrootsSp1TradeResolvedProofArtifact::inline(missing.clone()), - ) - .await - .expect_err("missing proof material"); - assert_eq!(err, RadrootsSp1TradeHostError::MissingProofMaterial); - - let mut synthetic = missing; - synthetic.inline_proof_base64 = - Some(base64::engine::general_purpose::STANDARD.encode(b"synthetic proof material")); - synthetic.proof_digest = super::proof_digest_for_execution(&execution, &synthetic) - .expect("synthetic proof digest"); - let err = super::verify_order_acceptance_resolved_sp1_proof_artifact( - &execution, - &super::RadrootsSp1TradeResolvedProofArtifact::inline(synthetic), - ) - .await - .expect_err("synthetic proof material"); - assert!(matches!( - err, - RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_) - )); - } -} diff --git a/crates/trade_sp1_guest/Cargo.toml b/crates/trade_sp1_guest/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "radroots_trade_sp1_guest" +publish = false +version = "0.1.0-alpha.2" +edition.workspace = true +authors = ["Tyson Lupul <tyson@radroots.org>"] +rust-version.workspace = true +license.workspace = true +description = "SP1 guest model for Radroots trade verification" +repository.workspace = true +homepage.workspace = true + +[[bin]] +name = "radroots_sp1_trade_order_acceptance_guest" +path = "src/bin/order_acceptance_guest.rs" +required-features = ["sp1_guest"] +test = false + +[features] +default = [] +sp1_guest = ["dep:sp1-zkvm"] + +[dependencies] +serde = { workspace = true, default-features = false, features = [ + "alloc", + "derive", +] } +serde_json = { workspace = true, default-features = false, features = [ + "alloc", +] } +sha2 = { workspace = true, default-features = false } +sp1-zkvm = { workspace = true, optional = true } +thiserror = { workspace = true } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/trade_sp1_guest/src/bin/order_acceptance_guest.rs b/crates/trade_sp1_guest/src/bin/order_acceptance_guest.rs @@ -0,0 +1,14 @@ +#![cfg_attr(target_os = "zkvm", no_main)] + +use radroots_trade_sp1_guest::{ + RadrootsSp1TradeOrderAcceptanceWitness, reduce_order_acceptance_canonical_public_values, +}; + +sp1_zkvm::entrypoint!(main); + +fn main() { + let witness = sp1_zkvm::io::read::<RadrootsSp1TradeOrderAcceptanceWitness>(); + let public_values = reduce_order_acceptance_canonical_public_values(&witness) + .expect("valid radroots order acceptance witness"); + sp1_zkvm::io::commit(&public_values); +} diff --git a/crates/sp1_guest_trade/src/lib.rs b/crates/trade_sp1_guest/src/lib.rs diff --git a/crates/trade_sp1_host/Cargo.toml b/crates/trade_sp1_host/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "radroots_trade_sp1_host" +publish = false +version = "0.1.0-alpha.2" +edition.workspace = true +build = "build.rs" +authors = ["Tyson Lupul <tyson@radroots.org>"] +rust-version.workspace = true +license.workspace = true +description = "SP1 host model for Radroots trade verification" +repository.workspace = true +homepage.workspace = true + +[features] +default = [] +sp1_verify = ["dep:bincode", "dep:sp1-build", "dep:sp1-sdk"] +sp1_proving = ["sp1_verify"] +sp1_cuda = ["dep:futures", "sp1_proving", "sp1-sdk/cuda"] + +[dependencies] +base64 = { workspace = true } +bincode = { workspace = true, optional = true } +futures = { workspace = true, optional = true } +radroots_trade_sp1_guest = { workspace = true } +radroots_trade = { workspace = true, default-features = false, features = [ + "serde_json", + "std", +] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +sha2 = { workspace = true } +sp1-sdk = { workspace = true, optional = true } +thiserror = { workspace = true } + +[build-dependencies] +sp1-build = { workspace = true, optional = true } + +[dev-dependencies] +radroots_event = { workspace = true, features = ["serde"] } +tokio = { workspace = true, features = ["rt-multi-thread"] } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/trade_sp1_host/build.rs b/crates/trade_sp1_host/build.rs @@ -0,0 +1,26 @@ +fn main() { + let build_guest_elf_env = "RADROOTS_SP1_HOST_TRADE_BUILD_GUEST_ELF"; + let run_real_proof_tests_env = "RADROOTS_SP1_HOST_TRADE_RUN_REAL_PROOF_TESTS"; + println!("cargo:rerun-if-env-changed=CARGO_FEATURE_SP1_VERIFY"); + println!("cargo:rerun-if-env-changed={build_guest_elf_env}"); + println!("cargo:rerun-if-env-changed={run_real_proof_tests_env}"); + println!("cargo:rustc-check-cfg=cfg(radroots_sp1_guest_elf)"); + println!("cargo:rustc-check-cfg=cfg(radroots_sp1_real_proof_tests)"); + if std::env::var(run_real_proof_tests_env).as_deref() == Ok("1") { + println!("cargo:rustc-cfg=radroots_sp1_real_proof_tests"); + } + if std::env::var(build_guest_elf_env).as_deref() != Ok("1") { + return; + } + #[cfg(feature = "sp1_verify")] + { + let args = sp1_build::BuildArgs { + binaries: vec!["radroots_sp1_trade_order_acceptance_guest".to_string()], + features: vec!["sp1_guest".to_string()], + locked: true, + ..sp1_build::BuildArgs::default() + }; + sp1_build::build_program_with_args("../trade_sp1_guest", args); + println!("cargo:rustc-cfg=radroots_sp1_guest_elf"); + } +} diff --git a/crates/trade_sp1_host/src/lib.rs b/crates/trade_sp1_host/src/lib.rs @@ -0,0 +1,3111 @@ +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] +#![forbid(unsafe_code)] + +use base64::Engine; +use radroots_trade::validation_receipt::{ + RadrootsTradeValidationReceipt, RadrootsValidationReceiptProof, + RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, + RadrootsValidationReceiptStatement, RadrootsValidationReceiptType, VALIDATION_RECEIPT_DOMAIN, + VALIDATION_RECEIPT_PROOF_REFERENCE_SHA256_PREFIX, VALIDATION_RECEIPT_VERSION, +}; +use radroots_trade_sp1_guest::{ + RadrootsSp1TradeGuestError, RadrootsSp1TradeOrderAcceptanceWitness, + RadrootsSp1TradeProofResult, RadrootsSp1TradePublicValuesExecution, + reduce_order_acceptance_public_values, +}; +#[cfg(feature = "sp1_verify")] +use radroots_trade_sp1_guest::{ + RadrootsSp1TradeProofPublicValues, RadrootsSp1TradeProofStatementType, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use thiserror::Error; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RadrootsSp1TradeProofMode { + None, + Core, + Compressed, + Groth16, + Plonk, +} + +impl RadrootsSp1TradeProofMode { + pub const fn proof_system(self) -> RadrootsValidationReceiptProofSystem { + match self { + Self::None => RadrootsValidationReceiptProofSystem::None, + Self::Core => RadrootsValidationReceiptProofSystem::Sp1Core, + Self::Compressed => RadrootsValidationReceiptProofSystem::Sp1Compressed, + Self::Groth16 => RadrootsValidationReceiptProofSystem::Sp1Groth16, + Self::Plonk => RadrootsValidationReceiptProofSystem::Sp1Plonk, + } + } + + pub const fn mode_label(self) -> Option<&'static str> { + match self { + Self::None => None, + Self::Core => Some("core"), + Self::Compressed => Some("compressed"), + Self::Groth16 => Some("groth16"), + Self::Plonk => Some("plonk"), + } + } + + pub fn from_label(value: &str) -> Option<Self> { + match value { + "none" => Some(Self::None), + "core" => Some(Self::Core), + "compressed" => Some(Self::Compressed), + "groth16" => Some(Self::Groth16), + "plonk" => Some(Self::Plonk), + _ => None, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsSp1TradeProofArtifact { + pub inline_proof_base64: Option<String>, + pub mode: Option<String>, + pub program_hash: Option<String>, + pub proof_digest: String, + pub proof_reference: Option<String>, + pub public_values_hash: String, + pub system: RadrootsValidationReceiptProofSystem, + pub verifying_key_hash: Option<String>, +} + +pub const RADROOTS_SP1_TRADE_PROOF_ARTIFACT_SCHEMA_VERSION: u32 = 1; +pub const RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION: u32 = 1; +pub const RADROOTS_SP1_TRADE_SP1_VERSION_LINE: &str = "sp1-sdk-6.2.1"; +pub const RADROOTS_SP1_TRADE_PROOF_CODEC: &str = "sp1-proof-with-public-values-bincode"; +pub const RADROOTS_SP1_TRADE_VERIFYING_KEY_CODEC: &str = "sp1-verifying-key-bincode"; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsSp1TradeProofEnvelope { + pub schema_version: u32, + pub sp1_version_line: String, + pub proof_system: String, + pub proof_mode: String, + pub proof_codec: String, + pub proof_content_hash: String, + pub proof_digest: String, + pub public_values_hash: String, + pub canonical_public_values_hash: String, + pub sp1_program_hash: String, + pub sp1_verifying_key_hash: String, + pub sp1_verifying_key_codec: String, + pub sp1_verifying_key_base64: String, + pub receipt_type: String, + pub receipt_result: String, + pub listing_event_id: String, + pub root_event_id: String, + pub target_event_id: String, + pub event_set_root: String, + pub previous_state_root: String, + pub new_state_root: String, + pub changed_records_root: String, + pub error_bitmap: String, + pub proof_content_base64: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsSp1TradeResolvedProofArtifact { + pub artifact: RadrootsSp1TradeProofArtifact, + #[serde(default)] + pub resolved_proof_envelope_base64: Option<String>, +} + +impl RadrootsSp1TradeResolvedProofArtifact { + pub fn inline(artifact: RadrootsSp1TradeProofArtifact) -> Self { + Self { + artifact, + resolved_proof_envelope_base64: None, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RadrootsSp1TradeProverBackend { + Disabled, + DeterministicNone, + LocalExecute, + LocalCpuProve, + LocalCudaProve, + RemoteHttpProve, +} + +impl RadrootsSp1TradeProverBackend { + pub const fn as_str(self) -> &'static str { + match self { + Self::Disabled => "disabled", + Self::DeterministicNone => "deterministic_none", + Self::LocalExecute => "local_execute", + Self::LocalCpuProve => "local_cpu_prove", + Self::LocalCudaProve => "local_cuda_prove", + Self::RemoteHttpProve => "remote_http_prove", + } + } + + pub fn from_label(value: &str) -> Option<Self> { + match value { + "disabled" => Some(Self::Disabled), + "deterministic_none" => Some(Self::DeterministicNone), + "local_execute" => Some(Self::LocalExecute), + "local_cpu_prove" => Some(Self::LocalCpuProve), + "local_cuda_prove" => Some(Self::LocalCudaProve), + "remote_http_prove" => Some(Self::RemoteHttpProve), + _ => None, + } + } +} + +impl core::fmt::Display for RadrootsSp1TradeProverBackend { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str(self.as_str()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RadrootsSp1TradeProofEngine { + Cpu, + Cuda, +} + +impl RadrootsSp1TradeProofEngine { + pub const fn as_str(self) -> &'static str { + match self { + Self::Cpu => "cpu", + Self::Cuda => "cuda", + } + } + + pub fn from_label(value: &str) -> Option<Self> { + match value { + "cpu" => Some(Self::Cpu), + "cuda" => Some(Self::Cuda), + _ => None, + } + } +} + +impl core::fmt::Display for RadrootsSp1TradeProofEngine { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str(self.as_str()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RadrootsSp1TradeRemoteProverStatus { + Accepted, + Running, + Completed, + Failed, + Rejected, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RadrootsSp1TradeWorkerResultStatus { + Succeeded, +} + +impl RadrootsSp1TradeWorkerResultStatus { + pub const fn as_str(self) -> &'static str { + match self { + Self::Succeeded => "succeeded", + } + } +} + +impl core::fmt::Display for RadrootsSp1TradeWorkerResultStatus { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str(self.as_str()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RadrootsSp1TradeWorkerRole { + NonAuthoritativeProver, +} + +impl RadrootsSp1TradeWorkerRole { + pub const fn as_str(self) -> &'static str { + match self { + Self::NonAuthoritativeProver => "non_authoritative_prover", + } + } +} + +impl core::fmt::Display for RadrootsSp1TradeWorkerRole { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str(self.as_str()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsSp1TradeRemoteProverRequest { + pub schema_version: u32, + pub request_id: String, + pub proof_target: String, + pub proof_mode: RadrootsSp1TradeProofMode, + pub sp1_version_line: String, + pub witness: RadrootsSp1TradeOrderAcceptanceWitness, + pub expected_sp1_program_hash: String, + pub expected_sp1_verifying_key_hash: String, + pub expected_public_values_hash: String, + pub expected_reducer_program_hash: String, + pub expected_protocol_version: String, + pub expected_witness_version: u32, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsSp1TradeRemoteProverResponse { + pub schema_version: u32, + pub request_id: String, + pub status: RadrootsSp1TradeRemoteProverStatus, + #[serde(default)] + pub status_url: Option<String>, + #[serde(default)] + pub status_path: Option<String>, + #[serde(default)] + pub proof_system: Option<RadrootsValidationReceiptProofSystem>, + #[serde(default)] + pub proof_mode: Option<RadrootsSp1TradeProofMode>, + #[serde(default)] + pub public_values_hash: Option<String>, + #[serde(default)] + pub sp1_program_hash: Option<String>, + #[serde(default)] + pub sp1_verifying_key_hash: Option<String>, + #[serde(default)] + pub proof_artifact: Option<RadrootsSp1TradeProofArtifact>, + #[serde(default)] + pub resolved_proof_envelope_base64: Option<String>, + #[serde(default)] + pub reason_code: Option<String>, + #[serde(default)] + pub message: Option<String>, + #[serde(default)] + pub detail: Option<String>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsSp1TradeWorkerResultPayload { + pub cryptographic_proof_verified: bool, + pub decision_event_id: Option<String>, + pub event_set_root: Option<String>, + pub listing_event_id: Option<String>, + pub order_id: Option<String>, + pub proof_generated: bool, + pub proof_mode: RadrootsSp1TradeProofMode, + pub proof_system: RadrootsValidationReceiptProofSystem, + pub public_values_hash: String, + pub prover_backend: RadrootsSp1TradeProverBackend, + pub receipt_event_id: String, + pub receipt_kind: Option<u32>, + pub reducer_output_root: Option<String>, + pub request_event_id: Option<String>, + pub sp1_execute_checked: bool, + pub sp1_execute_public_values_hash: Option<String>, + pub status: RadrootsSp1TradeWorkerResultStatus, + pub worker_role: Option<RadrootsSp1TradeWorkerRole>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsSp1TradeProofBundle { + pub execution: RadrootsSp1TradePublicValuesExecution, + pub proof: RadrootsSp1TradeProofArtifact, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsSp1TradeValidationReceiptVerification { + pub canonical_public_values_len: usize, + pub proof_mode: RadrootsSp1TradeProofMode, + pub proof_system: RadrootsValidationReceiptProofSystem, + pub public_values_hash: String, + pub sp1_program_hash: String, + pub sp1_verifying_key_hash: String, +} + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum RadrootsSp1TradeHostError { + #[error("guest execution failed")] + Guest, + #[error("proof mode requires an SP1 verifying key hash")] + MissingVerifyingKeyHash, + #[error("proof public values hash does not match execution")] + PublicValuesHashMismatch, + #[error("proof digest does not match execution")] + ProofDigestMismatch, + #[error("proof material is missing")] + MissingProofMaterial, + #[error("proof material has conflicting inline and reference sources")] + ProofMaterialConflict, + #[error("receipt binding field {0} is missing")] + MissingReceiptBinding(&'static str), + #[error("SP1 execution failed: {0}")] + Sp1ExecuteFailed(String), + #[error("SP1 execution returned exit code {0}")] + Sp1ExitCode(u64), + #[error("SP1 public values failed to decode: {0}")] + Sp1PublicValuesDecode(String), + #[error("SP1 public values do not match deterministic reducer output")] + Sp1PublicValuesMismatch, + #[error("SP1 proof generation requires the sp1_proving feature")] + Sp1ProofGenerationRequired, + #[error("SP1 CUDA proof generation is unavailable: {0}")] + Sp1CudaProofEngineUnavailable(String), + #[error("SP1 proof mode is required")] + Sp1ProofModeRequired, + #[error("SP1 setup failed: {0}")] + Sp1SetupFailed(String), + #[error("SP1 proof generation failed: {0}")] + Sp1ProofFailed(String), + #[error("SP1 proof verifier is unavailable in this build")] + Sp1ProofVerifierUnavailable, + #[error("SP1 proof verification failed: {0}")] + Sp1ProofVerificationFailed(String), + #[error("SP1 proof material failed to decode: {0}")] + Sp1ProofMaterialDecode(String), + #[error("SP1 proof material is synthetic")] + Sp1SyntheticProofMaterial, + #[error("SP1 proof reference is unresolved")] + Sp1ProofReferenceUnresolved, + #[error("SP1 proof reference is invalid")] + InvalidSp1ProofReference, + #[error("SP1 proof reference digest does not match resolved envelope")] + Sp1ProofReferenceDigestMismatch, + #[error("SP1 proof mode does not match the proof artifact")] + Sp1ProofModeMismatch, + #[error("SP1 verifying key hash mismatch")] + Sp1VerifyingKeyHashMismatch, + #[error("SP1 program hash mismatch")] + Sp1ProgramHashMismatch, + #[error("SP1 program hash is missing")] + MissingSp1ProgramHash, + #[error("validation receipt field {0} does not match SP1 public values")] + ValidationReceiptBindingMismatch(&'static str), + #[error("proof artifact encoding failed")] + ProofEncoding, +} + +impl From<RadrootsSp1TradeGuestError> for RadrootsSp1TradeHostError { + fn from(_: RadrootsSp1TradeGuestError) -> Self { + Self::Guest + } +} + +pub fn execute_order_acceptance_public_values( + witness: &RadrootsSp1TradeOrderAcceptanceWitness, +) -> Result<RadrootsSp1TradePublicValuesExecution, RadrootsSp1TradeHostError> { + Ok(reduce_order_acceptance_public_values(witness)?) +} + +#[cfg(feature = "sp1_proving")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsSp1TradeExecuteReport { + pub exit_code: u64, + pub gas: Option<u64>, + pub total_instruction_count: u64, + pub total_syscall_count: u64, +} + +#[cfg(feature = "sp1_proving")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsSp1TradeExecuteBundle { + pub committed_public_values: Vec<u8>, + pub execution: RadrootsSp1TradePublicValuesExecution, + pub report: RadrootsSp1TradeExecuteReport, +} + +#[cfg(all(feature = "sp1_verify", radroots_sp1_guest_elf))] +pub fn order_acceptance_guest_elf() -> sp1_sdk::Elf { + sp1_sdk::include_elf!("radroots_sp1_trade_order_acceptance_guest") +} + +#[cfg(all(feature = "sp1_verify", radroots_sp1_guest_elf))] +pub fn sp1_program_hash_for_order_acceptance_guest() -> String { + sp1_program_hash_for_elf(&order_acceptance_guest_elf()) +} + +#[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] +pub async fn execute_order_acceptance_sp1_public_values( + witness: &RadrootsSp1TradeOrderAcceptanceWitness, +) -> Result<RadrootsSp1TradeExecuteBundle, RadrootsSp1TradeHostError> { + execute_order_acceptance_sp1_public_values_with_elf(order_acceptance_guest_elf(), witness).await +} + +#[cfg(all(feature = "sp1_proving", not(radroots_sp1_guest_elf)))] +pub async fn execute_order_acceptance_sp1_public_values( + _witness: &RadrootsSp1TradeOrderAcceptanceWitness, +) -> Result<RadrootsSp1TradeExecuteBundle, RadrootsSp1TradeHostError> { + Err(RadrootsSp1TradeHostError::Sp1SetupFailed( + "SP1 guest ELF build is not enabled".to_owned(), + )) +} + +#[cfg(feature = "sp1_proving")] +pub async fn execute_order_acceptance_sp1_public_values_with_elf( + elf: sp1_sdk::Elf, + witness: &RadrootsSp1TradeOrderAcceptanceWitness, +) -> Result<RadrootsSp1TradeExecuteBundle, RadrootsSp1TradeHostError> { + use sp1_sdk::{HashableKey, Prover, ProverClient, ProvingKey, SP1Stdin, StatusCode}; + + let client = ProverClient::builder().light().build().await; + let pk = client + .setup(elf.clone()) + .await + .map_err(|error| RadrootsSp1TradeHostError::Sp1SetupFailed(error.to_string()))?; + let verifying_key_hash = pk.verifying_key().bytes32(); + let witness = witness_with_sp1_identity( + witness, + Some(sp1_program_hash_for_elf(&elf)), + Some(verifying_key_hash), + )?; + let expected = execute_order_acceptance_public_values(&witness)?; + let mut stdin = SP1Stdin::new(); + stdin.write(&witness); + let (public_values, report) = client + .execute(elf, stdin) + .calculate_gas(true) + .expected_exit_code(StatusCode::SUCCESS) + .await + .map_err(|error| RadrootsSp1TradeHostError::Sp1ExecuteFailed(error.to_string()))?; + if report.exit_code != 0 { + return Err(RadrootsSp1TradeHostError::Sp1ExitCode(report.exit_code)); + } + + let (committed_public_values, execution) = execution_from_sp1_public_values(public_values)?; + if execution != expected { + return Err(RadrootsSp1TradeHostError::Sp1PublicValuesMismatch); + } + + Ok(RadrootsSp1TradeExecuteBundle { + committed_public_values, + execution, + report: RadrootsSp1TradeExecuteReport { + exit_code: report.exit_code, + gas: report.gas(), + total_instruction_count: report.total_instruction_count(), + total_syscall_count: report.total_syscall_count(), + }, + }) +} + +#[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] +pub async fn generate_order_acceptance_sp1_proof( + witness: &RadrootsSp1TradeOrderAcceptanceWitness, + mode: RadrootsSp1TradeProofMode, +) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> { + generate_order_acceptance_sp1_proof_with_engine(witness, mode, RadrootsSp1TradeProofEngine::Cpu) + .await +} + +#[cfg(all(feature = "sp1_proving", not(radroots_sp1_guest_elf)))] +pub async fn generate_order_acceptance_sp1_proof( + _witness: &RadrootsSp1TradeOrderAcceptanceWitness, + _mode: RadrootsSp1TradeProofMode, +) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> { + Err(RadrootsSp1TradeHostError::Sp1SetupFailed( + "SP1 guest ELF build is not enabled".to_owned(), + )) +} + +#[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] +pub async fn generate_order_acceptance_sp1_proof_with_engine( + witness: &RadrootsSp1TradeOrderAcceptanceWitness, + mode: RadrootsSp1TradeProofMode, + engine: RadrootsSp1TradeProofEngine, +) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> { + match engine { + RadrootsSp1TradeProofEngine::Cpu => { + let client = sp1_sdk::ProverClient::builder().cpu().build().await; + generate_order_acceptance_sp1_proof_with_client(&client, witness, mode).await + } + RadrootsSp1TradeProofEngine::Cuda => { + generate_order_acceptance_sp1_cuda_proof(witness, mode).await + } + } +} + +#[cfg(all(feature = "sp1_proving", not(radroots_sp1_guest_elf)))] +pub async fn generate_order_acceptance_sp1_proof_with_engine( + _witness: &RadrootsSp1TradeOrderAcceptanceWitness, + _mode: RadrootsSp1TradeProofMode, + _engine: RadrootsSp1TradeProofEngine, +) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> { + Err(RadrootsSp1TradeHostError::Sp1SetupFailed( + "SP1 guest ELF build is not enabled".to_owned(), + )) +} + +#[cfg(all(feature = "sp1_proving", feature = "sp1_cuda", radroots_sp1_guest_elf))] +async fn generate_order_acceptance_sp1_cuda_proof( + witness: &RadrootsSp1TradeOrderAcceptanceWitness, + mode: RadrootsSp1TradeProofMode, +) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> { + use futures::FutureExt; + let client = std::panic::AssertUnwindSafe(sp1_sdk::ProverClient::builder().cuda().build()) + .catch_unwind() + .await + .map_err(cuda_panic_to_host_error)?; + generate_order_acceptance_sp1_proof_with_client(&client, witness, mode).await +} + +#[cfg(all(feature = "sp1_proving", feature = "sp1_cuda", radroots_sp1_guest_elf))] +fn cuda_panic_to_host_error(payload: Box<dyn std::any::Any + Send>) -> RadrootsSp1TradeHostError { + let message = payload + .downcast_ref::<&str>() + .map(|value| (*value).to_owned()) + .or_else(|| payload.downcast_ref::<String>().cloned()) + .unwrap_or_else(|| "CUDA prover initialization failed".to_owned()); + RadrootsSp1TradeHostError::Sp1CudaProofEngineUnavailable(message) +} + +#[cfg(all( + feature = "sp1_proving", + not(feature = "sp1_cuda"), + radroots_sp1_guest_elf +))] +async fn generate_order_acceptance_sp1_cuda_proof( + _witness: &RadrootsSp1TradeOrderAcceptanceWitness, + _mode: RadrootsSp1TradeProofMode, +) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> { + Err(RadrootsSp1TradeHostError::Sp1CudaProofEngineUnavailable( + "build without sp1_cuda feature".to_owned(), + )) +} + +#[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] +async fn generate_order_acceptance_sp1_proof_with_client<P>( + client: &P, + witness: &RadrootsSp1TradeOrderAcceptanceWitness, + mode: RadrootsSp1TradeProofMode, +) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> +where + P: sp1_sdk::Prover, +{ + use sp1_sdk::{HashableKey, ProveRequest, ProvingKey, SP1Stdin, StatusCode}; + + let sp1_mode = sp1_proof_mode(mode)?; + let elf = order_acceptance_guest_elf(); + let sp1_program_hash = sp1_program_hash_for_elf(&elf); + let pk = client + .setup(elf) + .await + .map_err(|error| RadrootsSp1TradeHostError::Sp1SetupFailed(error.to_string()))?; + let verifying_key_hash = pk.verifying_key().bytes32(); + let witness = + witness_with_sp1_identity(witness, Some(sp1_program_hash), Some(verifying_key_hash))?; + let expected = execute_order_acceptance_public_values(&witness)?; + let mut stdin = SP1Stdin::new(); + stdin.write(&witness); + let proof = client + .prove(&pk, stdin) + .mode(sp1_mode) + .expected_exit_code(StatusCode::SUCCESS) + .await + .map_err(|error| RadrootsSp1TradeHostError::Sp1ProofFailed(error.to_string()))?; + if !sp1_proof_material_is_real(&proof.proof) { + return Err(RadrootsSp1TradeHostError::Sp1SyntheticProofMaterial); + } + client + .verify(&proof, pk.verifying_key(), Some(StatusCode::SUCCESS)) + .map_err(|error| { + RadrootsSp1TradeHostError::Sp1ProofVerificationFailed(error.to_string()) + })?; + let (_, execution) = execution_from_sp1_public_values(proof.public_values.clone())?; + if execution != expected { + return Err(RadrootsSp1TradeHostError::Sp1PublicValuesMismatch); + } + let proof_bytes = + bincode::serialize(&proof).map_err(|_| RadrootsSp1TradeHostError::ProofEncoding)?; + let verifying_key_bytes = bincode::serialize(pk.verifying_key()) + .map_err(|_| RadrootsSp1TradeHostError::ProofEncoding)?; + let proof = proof_artifact_for_real_sp1_execution( + &execution, + mode, + &proof_bytes, + &verifying_key_bytes, + )?; + verify_order_acceptance_proof_artifact_structure(&execution, &proof)?; + Ok(RadrootsSp1TradeProofBundle { execution, proof }) +} + +#[cfg(feature = "sp1_verify")] +pub async fn verify_order_acceptance_resolved_sp1_proof_artifact( + execution: &RadrootsSp1TradePublicValuesExecution, + resolved: &RadrootsSp1TradeResolvedProofArtifact, +) -> Result<(), RadrootsSp1TradeHostError> { + use sp1_sdk::{HashableKey, Prover, ProverClient, StatusCode}; + + let artifact = &resolved.artifact; + verify_order_acceptance_proof_artifact_structure(execution, artifact)?; + let envelope_base64 = resolved_proof_envelope_base64(resolved)?; + let envelope = decode_proof_envelope_base64(envelope_base64)?; + verify_proof_envelope(execution, artifact, &envelope)?; + let mode = artifact_proof_mode(artifact)?; + let proof = decode_sp1_proof_envelope(&envelope)?; + if !sp1_proof_material_is_real(&proof.proof) { + return Err(RadrootsSp1TradeHostError::Sp1SyntheticProofMaterial); + } + if !sp1_proof_matches_mode(&proof.proof, mode) { + return Err(RadrootsSp1TradeHostError::Sp1ProofModeMismatch); + } + let client = ProverClient::builder().cpu().build().await; + let verifying_key = decode_sp1_verifying_key_envelope(&envelope)?; + let verifying_key_hash = verifying_key.bytes32(); + if artifact.verifying_key_hash.as_deref() != Some(verifying_key_hash.as_str()) { + return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch); + } + let sp1_program_hash = artifact + .program_hash + .as_deref() + .ok_or(RadrootsSp1TradeHostError::MissingSp1ProgramHash)?; + client + .verify(&proof, &verifying_key, Some(StatusCode::SUCCESS)) + .map_err(|error| { + RadrootsSp1TradeHostError::Sp1ProofVerificationFailed(error.to_string()) + })?; + let (_, proof_execution) = execution_from_sp1_public_values(proof.public_values)?; + require_public_values_sp1_identity( + &proof_execution.public_values, + sp1_program_hash, + verifying_key_hash.as_str(), + )?; + if &proof_execution != execution { + return Err(RadrootsSp1TradeHostError::Sp1PublicValuesMismatch); + } + Ok(()) +} + +#[cfg(not(feature = "sp1_verify"))] +pub async fn verify_order_acceptance_resolved_sp1_proof_artifact( + _execution: &RadrootsSp1TradePublicValuesExecution, + _resolved: &RadrootsSp1TradeResolvedProofArtifact, +) -> Result<(), RadrootsSp1TradeHostError> { + Err(RadrootsSp1TradeHostError::Sp1ProofVerifierUnavailable) +} + +#[cfg(feature = "sp1_verify")] +pub async fn verify_order_acceptance_validation_receipt_inline_sp1_proof( + receipt: &RadrootsTradeValidationReceipt, +) -> Result<RadrootsSp1TradeValidationReceiptVerification, RadrootsSp1TradeHostError> { + use sp1_sdk::{HashableKey, Prover, ProverClient, StatusCode}; + + if receipt.proof.system == RadrootsValidationReceiptProofSystem::None { + return Err(RadrootsSp1TradeHostError::Sp1ProofModeRequired); + } + if receipt.proof.proof_reference.is_some() { + return Err(RadrootsSp1TradeHostError::Sp1ProofReferenceUnresolved); + } + + let artifact = RadrootsSp1TradeProofArtifact { + inline_proof_base64: Some( + receipt + .proof + .inline_proof_base64 + .clone() + .ok_or(RadrootsSp1TradeHostError::MissingProofMaterial)?, + ), + mode: receipt.proof.mode.clone(), + program_hash: receipt.proof.program_hash.clone(), + proof_digest: String::new(), + proof_reference: None, + public_values_hash: receipt.public_values_hash.clone(), + system: receipt.proof.system, + verifying_key_hash: receipt.proof.verifying_key_hash.clone(), + }; + let mode = artifact_proof_mode(&artifact)?; + let proof = decode_sp1_proof_artifact(&artifact)?; + if !sp1_proof_material_is_real(&proof.proof) { + return Err(RadrootsSp1TradeHostError::Sp1SyntheticProofMaterial); + } + if !sp1_proof_matches_mode(&proof.proof, mode) { + return Err(RadrootsSp1TradeHostError::Sp1ProofModeMismatch); + } + + let envelope = decode_proof_envelope(&artifact)?; + let verifying_key = decode_sp1_verifying_key_envelope(&envelope)?; + let verifying_key_hash = verifying_key.bytes32(); + if artifact.verifying_key_hash.as_deref() != Some(verifying_key_hash.as_str()) { + return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch); + } + let sp1_program_hash = artifact + .program_hash + .as_deref() + .ok_or(RadrootsSp1TradeHostError::MissingSp1ProgramHash)?; + + let client = ProverClient::builder().cpu().build().await; + client + .verify(&proof, &verifying_key, Some(StatusCode::SUCCESS)) + .map_err(|error| { + RadrootsSp1TradeHostError::Sp1ProofVerificationFailed(error.to_string()) + })?; + let (_, execution) = execution_from_sp1_public_values(proof.public_values)?; + require_public_values_sp1_identity( + &execution.public_values, + sp1_program_hash, + verifying_key_hash.as_str(), + )?; + verify_proof_envelope(&execution, &artifact, &envelope)?; + verify_validation_receipt_matches_public_values(receipt, &execution.public_values)?; + if execution.public_values_hash != receipt.public_values_hash { + return Err(RadrootsSp1TradeHostError::PublicValuesHashMismatch); + } + + Ok(RadrootsSp1TradeValidationReceiptVerification { + canonical_public_values_len: execution.canonical_public_values.len(), + proof_mode: mode, + proof_system: receipt.proof.system, + public_values_hash: execution.public_values_hash, + sp1_program_hash: sp1_program_hash.to_owned(), + sp1_verifying_key_hash: verifying_key_hash, + }) +} + +#[cfg(not(feature = "sp1_verify"))] +pub async fn verify_order_acceptance_validation_receipt_inline_sp1_proof( + _receipt: &RadrootsTradeValidationReceipt, +) -> Result<RadrootsSp1TradeValidationReceiptVerification, RadrootsSp1TradeHostError> { + Err(RadrootsSp1TradeHostError::Sp1ProofVerifierUnavailable) +} + +#[cfg(feature = "sp1_verify")] +pub async fn verify_order_acceptance_validation_receipt_resolved_sp1_proof( + receipt: &RadrootsTradeValidationReceipt, + resolved: &RadrootsSp1TradeResolvedProofArtifact, +) -> Result<RadrootsSp1TradeValidationReceiptVerification, RadrootsSp1TradeHostError> { + use sp1_sdk::{HashableKey, Prover, ProverClient, StatusCode}; + + if receipt.proof.system == RadrootsValidationReceiptProofSystem::None { + return Err(RadrootsSp1TradeHostError::Sp1ProofModeRequired); + } + verify_receipt_proof_matches_artifact(receipt, &resolved.artifact)?; + let envelope_base64 = resolved_proof_envelope_base64(resolved)?; + let envelope = decode_proof_envelope_base64(envelope_base64)?; + let mode = artifact_proof_mode(&resolved.artifact)?; + let proof = decode_sp1_proof_envelope(&envelope)?; + if !sp1_proof_material_is_real(&proof.proof) { + return Err(RadrootsSp1TradeHostError::Sp1SyntheticProofMaterial); + } + if !sp1_proof_matches_mode(&proof.proof, mode) { + return Err(RadrootsSp1TradeHostError::Sp1ProofModeMismatch); + } + + let verifying_key = decode_sp1_verifying_key_envelope(&envelope)?; + let verifying_key_hash = verifying_key.bytes32(); + if resolved.artifact.verifying_key_hash.as_deref() != Some(verifying_key_hash.as_str()) { + return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch); + } + let sp1_program_hash = resolved + .artifact + .program_hash + .as_deref() + .ok_or(RadrootsSp1TradeHostError::MissingSp1ProgramHash)?; + + let client = ProverClient::builder().cpu().build().await; + client + .verify(&proof, &verifying_key, Some(StatusCode::SUCCESS)) + .map_err(|error| { + RadrootsSp1TradeHostError::Sp1ProofVerificationFailed(error.to_string()) + })?; + let (_, execution) = execution_from_sp1_public_values(proof.public_values)?; + require_public_values_sp1_identity( + &execution.public_values, + sp1_program_hash, + verifying_key_hash.as_str(), + )?; + verify_order_acceptance_proof_artifact_structure(&execution, &resolved.artifact)?; + verify_proof_envelope(&execution, &resolved.artifact, &envelope)?; + verify_validation_receipt_matches_public_values(receipt, &execution.public_values)?; + if execution.public_values_hash != receipt.public_values_hash { + return Err(RadrootsSp1TradeHostError::PublicValuesHashMismatch); + } + + Ok(RadrootsSp1TradeValidationReceiptVerification { + canonical_public_values_len: execution.canonical_public_values.len(), + proof_mode: mode, + proof_system: receipt.proof.system, + public_values_hash: execution.public_values_hash, + sp1_program_hash: sp1_program_hash.to_owned(), + sp1_verifying_key_hash: verifying_key_hash, + }) +} + +#[cfg(not(feature = "sp1_verify"))] +pub async fn verify_order_acceptance_validation_receipt_resolved_sp1_proof( + _receipt: &RadrootsTradeValidationReceipt, + _resolved: &RadrootsSp1TradeResolvedProofArtifact, +) -> Result<RadrootsSp1TradeValidationReceiptVerification, RadrootsSp1TradeHostError> { + Err(RadrootsSp1TradeHostError::Sp1ProofVerifierUnavailable) +} + +pub fn generate_order_acceptance_proof( + witness: &RadrootsSp1TradeOrderAcceptanceWitness, + mode: RadrootsSp1TradeProofMode, +) -> Result<RadrootsSp1TradeProofBundle, RadrootsSp1TradeHostError> { + let execution = execute_order_acceptance_public_values(witness)?; + let proof = proof_artifact_for_execution(&execution, mode)?; + verify_order_acceptance_proof_artifact_structure(&execution, &proof)?; + Ok(RadrootsSp1TradeProofBundle { execution, proof }) +} + +#[cfg(feature = "sp1_verify")] +fn verify_validation_receipt_matches_public_values( + receipt: &RadrootsTradeValidationReceipt, + public_values: &RadrootsSp1TradeProofPublicValues, +) -> Result<(), RadrootsSp1TradeHostError> { + let program_hash = public_values + .sp1_program_hash + .as_deref() + .ok_or(RadrootsSp1TradeHostError::MissingSp1ProgramHash)?; + let verifying_key_hash = public_values + .sp1_verifying_key_hash + .as_deref() + .ok_or(RadrootsSp1TradeHostError::MissingVerifyingKeyHash)?; + if receipt.proof.program_hash.as_deref() != Some(program_hash) { + return Err(RadrootsSp1TradeHostError::Sp1ProgramHashMismatch); + } + if receipt.proof.verifying_key_hash.as_deref() != Some(verifying_key_hash) { + return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch); + } + if public_values.statement_type != RadrootsSp1TradeProofStatementType::TradeTransition { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "statement_type", + )); + } + if receipt.receipt_type != RadrootsValidationReceiptType::TradeTransition + || receipt.statement.statement_type != RadrootsValidationReceiptType::TradeTransition + { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "receipt_type", + )); + } + if public_values.event_set_root != receipt.event_set_root { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "event_set_root", + )); + } + if public_values.previous_state_root != receipt.previous_state_root { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "previous_state_root", + )); + } + if public_values.new_state_root != receipt.new_state_root { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "new_state_root", + )); + } + if public_values.changed_records_root != receipt.changed_records_root { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "changed_records_root", + )); + } + if public_values.error_bitmap != receipt.error_bitmap { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "error_bitmap", + )); + } + if public_values.listing_event_id.as_deref() + != Some(receipt.statement.listing_event_id.as_str()) + { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "listing_event_id", + )); + } + if public_values.root_event_id.as_deref() != Some(receipt.statement.root_event_id.as_str()) { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "root_event_id", + )); + } + if public_values.target_event_id.as_deref() != Some(receipt.statement.target_event_id.as_str()) + { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "target_event_id", + )); + } + if !receipt_result_matches_public_values(receipt.result, public_values.result) { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "result", + )); + } + Ok(()) +} + +#[cfg(feature = "sp1_verify")] +fn require_public_values_sp1_identity( + public_values: &RadrootsSp1TradeProofPublicValues, + expected_program_hash: &str, + expected_verifying_key_hash: &str, +) -> Result<(), RadrootsSp1TradeHostError> { + match public_values.sp1_program_hash.as_deref() { + Some(value) if value == expected_program_hash => {} + Some(_) => return Err(RadrootsSp1TradeHostError::Sp1ProgramHashMismatch), + None => return Err(RadrootsSp1TradeHostError::MissingSp1ProgramHash), + } + match public_values.sp1_verifying_key_hash.as_deref() { + Some(value) if value == expected_verifying_key_hash => {} + Some(_) => return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch), + None => return Err(RadrootsSp1TradeHostError::MissingVerifyingKeyHash), + } + Ok(()) +} + +#[cfg(feature = "sp1_verify")] +fn receipt_result_matches_public_values( + receipt_result: RadrootsValidationReceiptResult, + public_values_result: RadrootsSp1TradeProofResult, +) -> bool { + matches!( + (receipt_result, public_values_result), + ( + RadrootsValidationReceiptResult::Valid, + RadrootsSp1TradeProofResult::Valid + ) | ( + RadrootsValidationReceiptResult::Invalid, + RadrootsSp1TradeProofResult::Invalid + ) + ) +} + +pub fn verify_order_acceptance_proof_artifact_structure( + execution: &RadrootsSp1TradePublicValuesExecution, + artifact: &RadrootsSp1TradeProofArtifact, +) -> Result<(), RadrootsSp1TradeHostError> { + if artifact.public_values_hash != execution.public_values_hash { + return Err(RadrootsSp1TradeHostError::PublicValuesHashMismatch); + } + let expected = proof_digest_for_execution(execution, artifact)?; + if artifact.proof_digest != expected { + return Err(RadrootsSp1TradeHostError::ProofDigestMismatch); + } + match artifact.system { + RadrootsValidationReceiptProofSystem::None => { + if artifact.inline_proof_base64.is_some() + || artifact.mode.is_some() + || artifact.program_hash.is_some() + || artifact.proof_reference.is_some() + || artifact.verifying_key_hash.is_some() + { + return Err(RadrootsSp1TradeHostError::MissingProofMaterial); + } + } + _ => { + match (&artifact.inline_proof_base64, &artifact.proof_reference) { + (None, None) => return Err(RadrootsSp1TradeHostError::MissingProofMaterial), + (Some(_), Some(_)) => { + return Err(RadrootsSp1TradeHostError::ProofMaterialConflict); + } + (None, Some(reference)) => { + proof_reference_digest(reference)?; + } + _ => {} + } + let public_values_program_hash = execution + .public_values + .sp1_program_hash + .as_deref() + .ok_or(RadrootsSp1TradeHostError::MissingSp1ProgramHash)?; + let public_values_verifying_key_hash = execution + .public_values + .sp1_verifying_key_hash + .as_deref() + .ok_or(RadrootsSp1TradeHostError::MissingVerifyingKeyHash)?; + if artifact.program_hash.as_deref() != Some(public_values_program_hash) { + return Err(RadrootsSp1TradeHostError::Sp1ProgramHashMismatch); + } + if artifact.verifying_key_hash.as_deref() != Some(public_values_verifying_key_hash) { + return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch); + } + if artifact.inline_proof_base64.is_some() { + let envelope = decode_proof_envelope(artifact)?; + verify_proof_envelope(execution, artifact, &envelope)?; + } + } + } + Ok(()) +} + +pub fn validation_receipt_for_order_acceptance_proof( + bundle: &RadrootsSp1TradeProofBundle, +) -> Result<RadrootsTradeValidationReceipt, RadrootsSp1TradeHostError> { + let public_values = &bundle.execution.public_values; + let listing_event_id = public_values.listing_event_id.clone().ok_or( + RadrootsSp1TradeHostError::MissingReceiptBinding("listing_event_id"), + )?; + let root_event_id = public_values.root_event_id.clone().ok_or( + RadrootsSp1TradeHostError::MissingReceiptBinding("root_event_id"), + )?; + let target_event_id = public_values.target_event_id.clone().ok_or( + RadrootsSp1TradeHostError::MissingReceiptBinding("target_event_id"), + )?; + Ok(RadrootsTradeValidationReceipt { + changed_records_root: public_values.changed_records_root.clone(), + domain: VALIDATION_RECEIPT_DOMAIN.to_string(), + error_bitmap: public_values.error_bitmap.clone(), + event_set_root: public_values.event_set_root.clone(), + new_state_root: public_values.new_state_root.clone(), + previous_state_root: public_values.previous_state_root.clone(), + proof: RadrootsValidationReceiptProof { + inline_proof_base64: bundle.proof.inline_proof_base64.clone(), + mode: bundle.proof.mode.clone(), + program_hash: bundle.proof.program_hash.clone(), + proof_reference: bundle.proof.proof_reference.clone(), + system: bundle.proof.system, + verifying_key_hash: bundle.proof.verifying_key_hash.clone(), + }, + public_values_hash: bundle.execution.public_values_hash.clone(), + receipt_type: RadrootsValidationReceiptType::TradeTransition, + result: validation_receipt_result_from_public_values(public_values.result), + statement: RadrootsValidationReceiptStatement { + listing_event_id, + root_event_id, + target_event_id, + statement_type: RadrootsValidationReceiptType::TradeTransition, + }, + version: VALIDATION_RECEIPT_VERSION, + }) +} + +fn validation_receipt_result_from_public_values( + result: RadrootsSp1TradeProofResult, +) -> RadrootsValidationReceiptResult { + match result { + RadrootsSp1TradeProofResult::Valid => RadrootsValidationReceiptResult::Valid, + RadrootsSp1TradeProofResult::Invalid => RadrootsValidationReceiptResult::Invalid, + } +} + +#[cfg(feature = "sp1_verify")] +fn verify_receipt_proof_matches_artifact( + receipt: &RadrootsTradeValidationReceipt, + artifact: &RadrootsSp1TradeProofArtifact, +) -> Result<(), RadrootsSp1TradeHostError> { + if receipt.public_values_hash != artifact.public_values_hash { + return Err(RadrootsSp1TradeHostError::PublicValuesHashMismatch); + } + if receipt.proof.inline_proof_base64.as_deref() != artifact.inline_proof_base64.as_deref() + || receipt.proof.mode.as_deref() != artifact.mode.as_deref() + || receipt.proof.program_hash.as_deref() != artifact.program_hash.as_deref() + || receipt.proof.proof_reference.as_deref() != artifact.proof_reference.as_deref() + || receipt.proof.system != artifact.system + || receipt.proof.verifying_key_hash.as_deref() != artifact.verifying_key_hash.as_deref() + { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "proof", + )); + } + Ok(()) +} + +fn validation_receipt_result_label(result: RadrootsValidationReceiptResult) -> &'static str { + match result { + RadrootsValidationReceiptResult::Valid => "valid", + RadrootsValidationReceiptResult::Invalid => "invalid", + } +} + +fn proof_artifact_for_execution( + execution: &RadrootsSp1TradePublicValuesExecution, + mode: RadrootsSp1TradeProofMode, +) -> Result<RadrootsSp1TradeProofArtifact, RadrootsSp1TradeHostError> { + let system = mode.proof_system(); + let mut artifact = RadrootsSp1TradeProofArtifact { + inline_proof_base64: None, + mode: mode.mode_label().map(str::to_string), + program_hash: None, + proof_digest: String::new(), + proof_reference: None, + public_values_hash: execution.public_values_hash.clone(), + system, + verifying_key_hash: None, + }; + if system == RadrootsValidationReceiptProofSystem::None { + artifact.proof_digest = proof_digest_for_execution(execution, &artifact)?; + return Ok(artifact); + } + + Err(RadrootsSp1TradeHostError::Sp1ProofGenerationRequired) +} + +pub fn referenced_order_acceptance_proof_artifact_for_execution( + execution: &RadrootsSp1TradePublicValuesExecution, + mode: RadrootsSp1TradeProofMode, + proof_reference: String, +) -> Result<RadrootsSp1TradeProofArtifact, RadrootsSp1TradeHostError> { + proof_reference_digest(proof_reference.as_str())?; + let system = mode.proof_system(); + if system == RadrootsValidationReceiptProofSystem::None { + return Err(RadrootsSp1TradeHostError::Sp1ProofModeRequired); + } + let mut artifact = RadrootsSp1TradeProofArtifact { + inline_proof_base64: None, + mode: mode.mode_label().map(str::to_string), + program_hash: execution.public_values.sp1_program_hash.clone(), + proof_digest: String::new(), + proof_reference: Some(proof_reference), + public_values_hash: execution.public_values_hash.clone(), + system, + verifying_key_hash: execution.public_values.sp1_verifying_key_hash.clone(), + }; + artifact.proof_digest = proof_digest_for_execution(execution, &artifact)?; + verify_order_acceptance_proof_artifact_structure(execution, &artifact)?; + Ok(artifact) +} + +#[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] +fn proof_artifact_for_real_sp1_execution( + execution: &RadrootsSp1TradePublicValuesExecution, + mode: RadrootsSp1TradeProofMode, + proof_bytes: &[u8], + verifying_key_bytes: &[u8], +) -> Result<RadrootsSp1TradeProofArtifact, RadrootsSp1TradeHostError> { + let system = mode.proof_system(); + if system == RadrootsValidationReceiptProofSystem::None { + return Err(RadrootsSp1TradeHostError::Sp1ProofModeRequired); + } + let program_hash = execution + .public_values + .sp1_program_hash + .clone() + .ok_or(RadrootsSp1TradeHostError::MissingSp1ProgramHash)?; + let verifying_key_hash = execution + .public_values + .sp1_verifying_key_hash + .clone() + .ok_or(RadrootsSp1TradeHostError::MissingVerifyingKeyHash)?; + let mut envelope = proof_envelope_for_real_sp1_execution( + execution, + system, + mode, + program_hash.as_str(), + verifying_key_hash.as_str(), + proof_bytes, + verifying_key_bytes, + )?; + envelope.proof_digest = proof_digest_for_envelope(&envelope)?; + let envelope_json = + serde_json::to_vec(&envelope).map_err(|_| RadrootsSp1TradeHostError::ProofEncoding)?; + Ok(RadrootsSp1TradeProofArtifact { + inline_proof_base64: Some(base64::engine::general_purpose::STANDARD.encode(envelope_json)), + mode: mode.mode_label().map(str::to_string), + program_hash: Some(program_hash), + proof_digest: envelope.proof_digest, + proof_reference: None, + public_values_hash: execution.public_values_hash.clone(), + system, + verifying_key_hash: Some(verifying_key_hash), + }) +} + +fn proof_digest_for_execution( + execution: &RadrootsSp1TradePublicValuesExecution, + artifact: &RadrootsSp1TradeProofArtifact, +) -> Result<String, RadrootsSp1TradeHostError> { + if artifact.inline_proof_base64.is_some() { + return proof_digest_for_envelope(&decode_proof_envelope(artifact)?); + } + let material = ProofDigestMaterial { + canonical_public_values: &execution.canonical_public_values, + mode: artifact.mode.as_deref(), + program_hash: artifact.program_hash.as_deref(), + proof_reference: artifact.proof_reference.as_deref(), + public_values_hash: &artifact.public_values_hash, + system: artifact.system.as_str(), + verifying_key_hash: artifact.verifying_key_hash.as_deref(), + }; + let bytes = + serde_json::to_vec(&material).map_err(|_| RadrootsSp1TradeHostError::ProofEncoding)?; + let mut hasher = Sha256::new(); + hasher.update(b"radroots:sp1-trade-proof-artifact:v1"); + hasher.update(bytes); + Ok(format!("0x{}", hex_lower(hasher.finalize().as_slice()))) +} + +#[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] +fn proof_envelope_for_real_sp1_execution( + execution: &RadrootsSp1TradePublicValuesExecution, + system: RadrootsValidationReceiptProofSystem, + mode: RadrootsSp1TradeProofMode, + program_hash: &str, + verifying_key_hash: &str, + proof_bytes: &[u8], + verifying_key_bytes: &[u8], +) -> Result<RadrootsSp1TradeProofEnvelope, RadrootsSp1TradeHostError> { + Ok(RadrootsSp1TradeProofEnvelope { + schema_version: RADROOTS_SP1_TRADE_PROOF_ARTIFACT_SCHEMA_VERSION, + sp1_version_line: RADROOTS_SP1_TRADE_SP1_VERSION_LINE.to_owned(), + proof_system: system.as_str().to_owned(), + proof_mode: mode + .mode_label() + .ok_or(RadrootsSp1TradeHostError::Sp1ProofModeRequired)? + .to_owned(), + proof_codec: RADROOTS_SP1_TRADE_PROOF_CODEC.to_owned(), + proof_content_hash: hash_bytes("radroots:sp1-proof-content:v1", proof_bytes), + proof_digest: String::new(), + public_values_hash: execution.public_values_hash.clone(), + canonical_public_values_hash: hash_bytes( + "radroots:sp1-canonical-public-values:v1", + &execution.canonical_public_values, + ), + sp1_program_hash: program_hash.to_owned(), + sp1_verifying_key_hash: verifying_key_hash.to_owned(), + sp1_verifying_key_codec: RADROOTS_SP1_TRADE_VERIFYING_KEY_CODEC.to_owned(), + sp1_verifying_key_base64: base64::engine::general_purpose::STANDARD + .encode(verifying_key_bytes), + receipt_type: RadrootsValidationReceiptType::TradeTransition + .as_str() + .to_owned(), + receipt_result: validation_receipt_result_label( + validation_receipt_result_from_public_values(execution.public_values.result), + ) + .to_owned(), + listing_event_id: execution.public_values.listing_event_id.clone().ok_or( + RadrootsSp1TradeHostError::MissingReceiptBinding("listing_event_id"), + )?, + root_event_id: execution.public_values.root_event_id.clone().ok_or( + RadrootsSp1TradeHostError::MissingReceiptBinding("root_event_id"), + )?, + target_event_id: execution.public_values.target_event_id.clone().ok_or( + RadrootsSp1TradeHostError::MissingReceiptBinding("target_event_id"), + )?, + event_set_root: execution.public_values.event_set_root.clone(), + previous_state_root: execution.public_values.previous_state_root.clone(), + new_state_root: execution.public_values.new_state_root.clone(), + changed_records_root: execution.public_values.changed_records_root.clone(), + error_bitmap: execution.public_values.error_bitmap.clone(), + proof_content_base64: base64::engine::general_purpose::STANDARD.encode(proof_bytes), + }) +} + +fn decode_proof_envelope( + artifact: &RadrootsSp1TradeProofArtifact, +) -> Result<RadrootsSp1TradeProofEnvelope, RadrootsSp1TradeHostError> { + let inline = artifact + .inline_proof_base64 + .as_deref() + .ok_or(RadrootsSp1TradeHostError::MissingProofMaterial)?; + decode_proof_envelope_base64(inline) +} + +fn decode_proof_envelope_base64( + value: &str, +) -> Result<RadrootsSp1TradeProofEnvelope, RadrootsSp1TradeHostError> { + let envelope_bytes = proof_envelope_bytes_from_base64(value)?; + serde_json::from_slice::<RadrootsSp1TradeProofEnvelope>(&envelope_bytes) + .map_err(|error| RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(error.to_string())) +} + +fn proof_envelope_bytes_from_base64(value: &str) -> Result<Vec<u8>, RadrootsSp1TradeHostError> { + base64::engine::general_purpose::STANDARD + .decode(value) + .map_err(|error| RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(error.to_string())) +} + +pub fn proof_reference_for_proof_envelope_base64( + value: &str, +) -> Result<String, RadrootsSp1TradeHostError> { + let envelope_bytes = proof_envelope_bytes_from_base64(value)?; + let mut hasher = Sha256::new(); + hasher.update(envelope_bytes); + Ok(format!( + "{VALIDATION_RECEIPT_PROOF_REFERENCE_SHA256_PREFIX}{}", + hex_lower(hasher.finalize().as_slice()) + )) +} + +fn proof_reference_digest(value: &str) -> Result<&str, RadrootsSp1TradeHostError> { + let Some(digest) = value.strip_prefix(VALIDATION_RECEIPT_PROOF_REFERENCE_SHA256_PREFIX) else { + return Err(RadrootsSp1TradeHostError::InvalidSp1ProofReference); + }; + if digest.len() != 64 || !is_lower_hex(digest) { + return Err(RadrootsSp1TradeHostError::InvalidSp1ProofReference); + } + Ok(digest) +} + +#[cfg(feature = "sp1_verify")] +fn resolved_proof_envelope_base64( + resolved: &RadrootsSp1TradeResolvedProofArtifact, +) -> Result<&str, RadrootsSp1TradeHostError> { + match ( + resolved.artifact.inline_proof_base64.as_deref(), + resolved.artifact.proof_reference.as_deref(), + resolved.resolved_proof_envelope_base64.as_deref(), + ) { + (Some(inline), None, None) => Ok(inline), + (Some(_), None, Some(_)) => Err(RadrootsSp1TradeHostError::ProofMaterialConflict), + (None, Some(reference), Some(envelope)) => { + let expected = proof_reference_digest(reference)?; + let actual = proof_reference_for_proof_envelope_base64(envelope)?; + let actual = proof_reference_digest(actual.as_str())?; + if actual != expected { + return Err(RadrootsSp1TradeHostError::Sp1ProofReferenceDigestMismatch); + } + Ok(envelope) + } + (None, Some(_), None) => Err(RadrootsSp1TradeHostError::Sp1ProofReferenceUnresolved), + (Some(_), Some(_), _) => Err(RadrootsSp1TradeHostError::ProofMaterialConflict), + (None, None, _) => Err(RadrootsSp1TradeHostError::MissingProofMaterial), + } +} + +fn proof_content_bytes_from_envelope( + envelope: &RadrootsSp1TradeProofEnvelope, +) -> Result<Vec<u8>, RadrootsSp1TradeHostError> { + base64::engine::general_purpose::STANDARD + .decode(envelope.proof_content_base64.as_str()) + .map_err(|error| RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(error.to_string())) +} + +fn proof_digest_for_envelope( + envelope: &RadrootsSp1TradeProofEnvelope, +) -> Result<String, RadrootsSp1TradeHostError> { + let material = ProofEnvelopeDigestMaterial { + schema_version: envelope.schema_version, + sp1_version_line: envelope.sp1_version_line.as_str(), + proof_system: envelope.proof_system.as_str(), + proof_mode: envelope.proof_mode.as_str(), + proof_codec: envelope.proof_codec.as_str(), + proof_content_hash: envelope.proof_content_hash.as_str(), + public_values_hash: envelope.public_values_hash.as_str(), + canonical_public_values_hash: envelope.canonical_public_values_hash.as_str(), + sp1_program_hash: envelope.sp1_program_hash.as_str(), + sp1_verifying_key_hash: envelope.sp1_verifying_key_hash.as_str(), + sp1_verifying_key_codec: envelope.sp1_verifying_key_codec.as_str(), + sp1_verifying_key_base64: envelope.sp1_verifying_key_base64.as_str(), + receipt_type: envelope.receipt_type.as_str(), + receipt_result: envelope.receipt_result.as_str(), + listing_event_id: envelope.listing_event_id.as_str(), + root_event_id: envelope.root_event_id.as_str(), + target_event_id: envelope.target_event_id.as_str(), + event_set_root: envelope.event_set_root.as_str(), + previous_state_root: envelope.previous_state_root.as_str(), + new_state_root: envelope.new_state_root.as_str(), + changed_records_root: envelope.changed_records_root.as_str(), + error_bitmap: envelope.error_bitmap.as_str(), + }; + let bytes = + serde_json::to_vec(&material).map_err(|_| RadrootsSp1TradeHostError::ProofEncoding)?; + Ok(hash_bytes( + "radroots:sp1-trade-proof-envelope-digest:v1", + &bytes, + )) +} + +fn verify_proof_envelope( + execution: &RadrootsSp1TradePublicValuesExecution, + artifact: &RadrootsSp1TradeProofArtifact, + envelope: &RadrootsSp1TradeProofEnvelope, +) -> Result<(), RadrootsSp1TradeHostError> { + if envelope.schema_version != RADROOTS_SP1_TRADE_PROOF_ARTIFACT_SCHEMA_VERSION + || envelope.sp1_version_line != RADROOTS_SP1_TRADE_SP1_VERSION_LINE + || envelope.proof_codec != RADROOTS_SP1_TRADE_PROOF_CODEC + || envelope.proof_system != artifact.system.as_str() + || Some(envelope.proof_mode.as_str()) != artifact.mode.as_deref() + || envelope.public_values_hash != artifact.public_values_hash + || envelope.sp1_program_hash.as_str() != artifact.program_hash.as_deref().unwrap_or("") + || envelope.sp1_verifying_key_hash.as_str() + != artifact.verifying_key_hash.as_deref().unwrap_or("") + || envelope.sp1_verifying_key_codec != RADROOTS_SP1_TRADE_VERIFYING_KEY_CODEC + || envelope.sp1_verifying_key_base64.is_empty() + { + return Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode( + "proof envelope metadata mismatch".to_owned(), + )); + } + if envelope.proof_digest != proof_digest_for_envelope(envelope)? { + return Err(RadrootsSp1TradeHostError::ProofDigestMismatch); + } + let proof_bytes = proof_content_bytes_from_envelope(envelope)?; + if envelope.proof_content_hash != hash_bytes("radroots:sp1-proof-content:v1", &proof_bytes) { + return Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode( + "proof envelope content hash mismatch".to_owned(), + )); + } + let expected_canonical_public_values_hash = hash_bytes( + "radroots:sp1-canonical-public-values:v1", + &execution.canonical_public_values, + ); + if envelope.canonical_public_values_hash != expected_canonical_public_values_hash { + return Err(RadrootsSp1TradeHostError::PublicValuesHashMismatch); + } + if envelope.receipt_type != RadrootsValidationReceiptType::TradeTransition.as_str() { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "receipt_type", + )); + } + if envelope.receipt_result + != validation_receipt_result_label(validation_receipt_result_from_public_values( + execution.public_values.result, + )) + { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "result", + )); + } + if envelope.listing_event_id.as_str() + != execution + .public_values + .listing_event_id + .as_deref() + .unwrap_or("") + { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "listing_event_id", + )); + } + if envelope.root_event_id.as_str() + != execution + .public_values + .root_event_id + .as_deref() + .unwrap_or("") + { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "root_event_id", + )); + } + if envelope.target_event_id.as_str() + != execution + .public_values + .target_event_id + .as_deref() + .unwrap_or("") + { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "target_event_id", + )); + } + if envelope.event_set_root != execution.public_values.event_set_root + || envelope.previous_state_root != execution.public_values.previous_state_root + || envelope.new_state_root != execution.public_values.new_state_root + || envelope.changed_records_root != execution.public_values.changed_records_root + || envelope.error_bitmap != execution.public_values.error_bitmap + { + return Err(RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch( + "state_roots", + )); + } + Ok(()) +} + +fn hex_lower(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len() * 2); + for byte in bytes { + out.push(HEX[(byte >> 4) as usize] as char); + out.push(HEX[(byte & 0x0f) as usize] as char); + } + out +} + +fn is_lower_hex(value: &str) -> bool { + value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +#[cfg(any( + feature = "sp1_proving", + all(feature = "sp1_verify", radroots_sp1_guest_elf) +))] +fn sp1_program_hash_for_elf(elf: &sp1_sdk::Elf) -> String { + let bytes: &[u8] = match elf { + sp1_sdk::Elf::Static(bytes) => bytes, + sp1_sdk::Elf::Dynamic(bytes) => bytes, + }; + hash_bytes("radroots:sp1-guest-elf:v1", bytes) +} + +fn hash_bytes(domain: &'static str, bytes: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(domain.as_bytes()); + hasher.update(bytes); + format!("0x{}", hex_lower(hasher.finalize().as_slice())) +} + +#[cfg(feature = "sp1_proving")] +fn witness_with_sp1_identity( + witness: &RadrootsSp1TradeOrderAcceptanceWitness, + sp1_program_hash: Option<String>, + sp1_verifying_key_hash: Option<String>, +) -> Result<RadrootsSp1TradeOrderAcceptanceWitness, RadrootsSp1TradeHostError> { + if let (Some(existing), Some(actual)) = ( + witness.sp1_program_hash.as_deref(), + sp1_program_hash.as_deref(), + ) && existing != actual + { + return Err(RadrootsSp1TradeHostError::Sp1ProgramHashMismatch); + } + if let (Some(existing), Some(actual)) = ( + witness.sp1_verifying_key_hash.as_deref(), + sp1_verifying_key_hash.as_deref(), + ) && existing != actual + { + return Err(RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch); + } + + let mut bound = witness.clone(); + if let Some(hash) = sp1_program_hash { + bound.sp1_program_hash = Some(hash); + } + if let Some(hash) = sp1_verifying_key_hash { + bound.sp1_verifying_key_hash = Some(hash); + } + Ok(bound) +} + +#[cfg(feature = "sp1_verify")] +fn public_values_prefix(bytes: &[u8]) -> String { + const PREFIX_LEN: usize = 32; + hex_lower(&bytes[..bytes.len().min(PREFIX_LEN)]) +} + +#[cfg(feature = "sp1_verify")] +fn execution_from_sp1_public_values( + public_values: sp1_sdk::SP1PublicValues, +) -> Result<(Vec<u8>, RadrootsSp1TradePublicValuesExecution), RadrootsSp1TradeHostError> { + let committed_public_values = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let mut public_values = public_values; + public_values.read::<Vec<u8>>() + })) + .map_err(|_| { + RadrootsSp1TradeHostError::Sp1PublicValuesDecode( + "SP1 public values stream did not contain canonical bytes".to_string(), + ) + })?; + let decoded: radroots_trade_sp1_guest::RadrootsSp1TradeProofPublicValues = + serde_json::from_slice(&committed_public_values).map_err(|error| { + RadrootsSp1TradeHostError::Sp1PublicValuesDecode(format!( + "{error}; public values bytes={}; prefix={}", + committed_public_values.len(), + public_values_prefix(&committed_public_values) + )) + })?; + let canonical_public_values = radroots_trade_sp1_guest::canonical_public_values_bytes(&decoded) + .map_err(|error| RadrootsSp1TradeHostError::Sp1PublicValuesDecode(error.to_string()))?; + let public_values_hash = radroots_trade_sp1_guest::public_values_hash_hex(&decoded)?; + Ok(( + committed_public_values, + RadrootsSp1TradePublicValuesExecution { + public_values: decoded, + public_values_hash, + canonical_public_values, + }, + )) +} + +#[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] +fn sp1_proof_mode( + mode: RadrootsSp1TradeProofMode, +) -> Result<sp1_sdk::SP1ProofMode, RadrootsSp1TradeHostError> { + match mode { + RadrootsSp1TradeProofMode::None => Err(RadrootsSp1TradeHostError::Sp1ProofModeRequired), + RadrootsSp1TradeProofMode::Core => Ok(sp1_sdk::SP1ProofMode::Core), + RadrootsSp1TradeProofMode::Compressed => Ok(sp1_sdk::SP1ProofMode::Compressed), + RadrootsSp1TradeProofMode::Groth16 => Ok(sp1_sdk::SP1ProofMode::Groth16), + RadrootsSp1TradeProofMode::Plonk => Ok(sp1_sdk::SP1ProofMode::Plonk), + } +} + +#[cfg(feature = "sp1_verify")] +fn artifact_proof_mode( + artifact: &RadrootsSp1TradeProofArtifact, +) -> Result<RadrootsSp1TradeProofMode, RadrootsSp1TradeHostError> { + match artifact.system { + RadrootsValidationReceiptProofSystem::None => { + Err(RadrootsSp1TradeHostError::Sp1ProofModeRequired) + } + RadrootsValidationReceiptProofSystem::Sp1Core => Ok(RadrootsSp1TradeProofMode::Core), + RadrootsValidationReceiptProofSystem::Sp1Compressed => { + Ok(RadrootsSp1TradeProofMode::Compressed) + } + RadrootsValidationReceiptProofSystem::Sp1Groth16 => Ok(RadrootsSp1TradeProofMode::Groth16), + RadrootsValidationReceiptProofSystem::Sp1Plonk => Ok(RadrootsSp1TradeProofMode::Plonk), + } +} + +#[cfg(feature = "sp1_verify")] +fn decode_sp1_proof_artifact( + artifact: &RadrootsSp1TradeProofArtifact, +) -> Result<sp1_sdk::SP1ProofWithPublicValues, RadrootsSp1TradeHostError> { + let envelope = decode_proof_envelope(artifact)?; + decode_sp1_proof_envelope(&envelope) +} + +#[cfg(feature = "sp1_verify")] +fn decode_sp1_proof_envelope( + envelope: &RadrootsSp1TradeProofEnvelope, +) -> Result<sp1_sdk::SP1ProofWithPublicValues, RadrootsSp1TradeHostError> { + let proof_bytes = proof_content_bytes_from_envelope(envelope)?; + bincode::deserialize::<sp1_sdk::SP1ProofWithPublicValues>(&proof_bytes) + .map_err(|error| RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(error.to_string())) +} + +#[cfg(feature = "sp1_verify")] +fn decode_sp1_verifying_key_envelope( + envelope: &RadrootsSp1TradeProofEnvelope, +) -> Result<sp1_sdk::SP1VerifyingKey, RadrootsSp1TradeHostError> { + if envelope.sp1_verifying_key_codec != RADROOTS_SP1_TRADE_VERIFYING_KEY_CODEC { + return Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode( + "proof envelope verifying key codec mismatch".to_owned(), + )); + } + let verifying_key_bytes = base64::engine::general_purpose::STANDARD + .decode(envelope.sp1_verifying_key_base64.as_str()) + .map_err(|error| RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(error.to_string()))?; + bincode::deserialize::<sp1_sdk::SP1VerifyingKey>(&verifying_key_bytes) + .map_err(|error| RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(error.to_string())) +} + +#[cfg(feature = "sp1_verify")] +fn sp1_proof_material_is_real(proof: &sp1_sdk::SP1Proof) -> bool { + match proof { + sp1_sdk::SP1Proof::Core(chunks) => !chunks.is_empty(), + sp1_sdk::SP1Proof::Compressed(_) => true, + sp1_sdk::SP1Proof::Groth16(proof) => !proof.encoded_proof.is_empty(), + sp1_sdk::SP1Proof::Plonk(proof) => !proof.encoded_proof.is_empty(), + } +} + +#[cfg(feature = "sp1_verify")] +fn sp1_proof_matches_mode(proof: &sp1_sdk::SP1Proof, mode: RadrootsSp1TradeProofMode) -> bool { + matches!( + (proof, mode), + (sp1_sdk::SP1Proof::Core(_), RadrootsSp1TradeProofMode::Core) + | ( + sp1_sdk::SP1Proof::Compressed(_), + RadrootsSp1TradeProofMode::Compressed + ) + | ( + sp1_sdk::SP1Proof::Groth16(_), + RadrootsSp1TradeProofMode::Groth16 + ) + | ( + sp1_sdk::SP1Proof::Plonk(_), + RadrootsSp1TradeProofMode::Plonk + ) + ) +} + +#[derive(Serialize)] +struct ProofDigestMaterial<'a> { + canonical_public_values: &'a [u8], + mode: Option<&'a str>, + program_hash: Option<&'a str>, + proof_reference: Option<&'a str>, + public_values_hash: &'a str, + system: &'a str, + verifying_key_hash: Option<&'a str>, +} + +#[derive(Serialize)] +struct ProofEnvelopeDigestMaterial<'a> { + schema_version: u32, + sp1_version_line: &'a str, + proof_system: &'a str, + proof_mode: &'a str, + proof_codec: &'a str, + proof_content_hash: &'a str, + public_values_hash: &'a str, + canonical_public_values_hash: &'a str, + sp1_program_hash: &'a str, + sp1_verifying_key_hash: &'a str, + sp1_verifying_key_codec: &'a str, + sp1_verifying_key_base64: &'a str, + receipt_type: &'a str, + receipt_result: &'a str, + listing_event_id: &'a str, + root_event_id: &'a str, + target_event_id: &'a str, + event_set_root: &'a str, + previous_state_root: &'a str, + new_state_root: &'a str, + changed_records_root: &'a str, + error_bitmap: &'a str, +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::{ + RadrootsSp1TradeHostError, RadrootsSp1TradeProofMode, generate_order_acceptance_proof, + validation_receipt_for_order_acceptance_proof, validation_receipt_result_label, + verify_order_acceptance_proof_artifact_structure, + }; + use base64::Engine; + use radroots_event::{RadrootsEventEnvelope, kinds::KIND_TRADE_VALIDATION_RECEIPT}; + use radroots_trade::validation_receipt::{ + RadrootsValidationReceiptExpectedBinding, RadrootsValidationReceiptProof, + RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, + RadrootsValidationReceiptType, validation_receipt_event_build, + verify_validation_receipt_event, + }; + use radroots_trade_sp1_guest::{ + RADROOTS_SP1_TRADE_KIND_LISTING, RADROOTS_SP1_TRADE_KIND_ORDER_DECISION, + RADROOTS_SP1_TRADE_KIND_ORDER_REQUEST, RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET, + RADROOTS_SP1_TRADE_PROTOCOL_VERSION, RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH, + RADROOTS_SP1_TRADE_WITNESS_VERSION, RadrootsSp1TradeCanonicalEventEvidence, + RadrootsSp1TradeEventEvidenceRole, RadrootsSp1TradeEventWorkflowPosition, + RadrootsSp1TradeInventoryBinWitness, RadrootsSp1TradeInventoryCommitmentWitness, + RadrootsSp1TradeOrderAcceptanceWitness, RadrootsSp1TradeOrderDecisionEventWitness, + RadrootsSp1TradeOrderDecisionWitness, RadrootsSp1TradeOrderItemWitness, + RadrootsSp1TradeOrderRequestWitness, RadrootsSp1TradeProofResult, + RadrootsSp1TradePublicValuesExecution, + }; + #[cfg(feature = "sp1_verify")] + use serde::Deserialize; + + type ProofEnvelopeMutation = (&'static str, fn(&mut super::RadrootsSp1TradeProofEnvelope)); + type PublicValuesExecutionMutation = + (&'static str, fn(&mut RadrootsSp1TradePublicValuesExecution)); + + fn witness() -> RadrootsSp1TradeOrderAcceptanceWitness { + RadrootsSp1TradeOrderAcceptanceWitness { + witness_version: RADROOTS_SP1_TRADE_WITNESS_VERSION, + proof_target: RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET.to_string(), + listing_event_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + request_event_id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + .to_string(), + decision_event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + event_evidence: event_evidence(), + request: request(2), + decision: decision(2), + inventory_bins: vec![RadrootsSp1TradeInventoryBinWitness { + bin_id: "bin-1".to_string(), + listing_capacity: 5, + previous_reserved: 1, + }], + inventory_sequence: 7, + previous_state_root: None, + reducer_program_hash: RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH.to_string(), + radroots_protocol_version: RADROOTS_SP1_TRADE_PROTOCOL_VERSION.to_string(), + sp1_program_hash: Some( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + ), + sp1_verifying_key_hash: Some( + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), + ), + } + } + + #[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] + fn witness_without_sp1_identity() -> RadrootsSp1TradeOrderAcceptanceWitness { + let mut input = witness(); + input.sp1_program_hash = None; + input.sp1_verifying_key_hash = None; + input + } + + fn event_evidence() -> Vec<RadrootsSp1TradeCanonicalEventEvidence> { + vec![ + RadrootsSp1TradeCanonicalEventEvidence { + event_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + signer_pubkey: "1111111111111111111111111111111111111111111111111111111111111111" + .to_string(), + kind: RADROOTS_SP1_TRADE_KIND_LISTING, + canonical_event_hash: + "0x1010101010101010101010101010101010101010101010101010101010101010".to_string(), + signature_hash: + "0x1111111111111111111111111111111111111111111111111111111111111111".to_string(), + preverified_signature: true, + role: RadrootsSp1TradeEventEvidenceRole::Seller, + workflow_position: RadrootsSp1TradeEventWorkflowPosition::Listing, + content_hash: "0x1212121212121212121212121212121212121212121212121212121212121212" + .to_string(), + tags_hash: "0x1313131313131313131313131313131313131313131313131313131313131313" + .to_string(), + ordering_key: "001:listing".to_string(), + }, + RadrootsSp1TradeCanonicalEventEvidence { + event_id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + .to_string(), + signer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222" + .to_string(), + kind: RADROOTS_SP1_TRADE_KIND_ORDER_REQUEST, + canonical_event_hash: + "0x2020202020202020202020202020202020202020202020202020202020202020".to_string(), + signature_hash: + "0x2121212121212121212121212121212121212121212121212121212121212121".to_string(), + preverified_signature: true, + role: RadrootsSp1TradeEventEvidenceRole::Buyer, + workflow_position: RadrootsSp1TradeEventWorkflowPosition::OrderRequest, + content_hash: "0x2222222222222222222222222222222222222222222222222222222222222222" + .to_string(), + tags_hash: "0x2323232323232323232323232323232323232323232323232323232323232323" + .to_string(), + ordering_key: "002:order_request".to_string(), + }, + RadrootsSp1TradeCanonicalEventEvidence { + event_id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + signer_pubkey: "1111111111111111111111111111111111111111111111111111111111111111" + .to_string(), + kind: RADROOTS_SP1_TRADE_KIND_ORDER_DECISION, + canonical_event_hash: + "0x3030303030303030303030303030303030303030303030303030303030303030".to_string(), + signature_hash: + "0x3131313131313131313131313131313131313131313131313131313131313131".to_string(), + preverified_signature: true, + role: RadrootsSp1TradeEventEvidenceRole::Seller, + workflow_position: RadrootsSp1TradeEventWorkflowPosition::OrderDecision, + content_hash: "0x3232323232323232323232323232323232323232323232323232323232323232" + .to_string(), + tags_hash: "0x3333333333333333333333333333333333333333333333333333333333333333" + .to_string(), + ordering_key: "003:order_decision".to_string(), + }, + ] + } + + fn request(bin_count: u32) -> RadrootsSp1TradeOrderRequestWitness { + RadrootsSp1TradeOrderRequestWitness { + order_id: "order-1".to_string(), + listing_addr: + "30402:1111111111111111111111111111111111111111111111111111111111111111:listing-1" + .to_string(), + buyer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222" + .to_string(), + seller_pubkey: "1111111111111111111111111111111111111111111111111111111111111111" + .to_string(), + items: vec![RadrootsSp1TradeOrderItemWitness { + bin_id: "bin-1".to_string(), + bin_count, + }], + } + } + + fn decision(bin_count: u32) -> RadrootsSp1TradeOrderDecisionEventWitness { + RadrootsSp1TradeOrderDecisionEventWitness { + order_id: "order-1".to_string(), + listing_addr: + "30402:1111111111111111111111111111111111111111111111111111111111111111:listing-1" + .to_string(), + buyer_pubkey: "2222222222222222222222222222222222222222222222222222222222222222" + .to_string(), + seller_pubkey: "1111111111111111111111111111111111111111111111111111111111111111" + .to_string(), + decision: RadrootsSp1TradeOrderDecisionWitness::Accepted { + inventory_commitments: vec![RadrootsSp1TradeInventoryCommitmentWitness { + bin_id: "bin-1".to_string(), + bin_count, + }], + }, + } + } + + fn inline_sp1_artifact_fixture() -> ( + super::RadrootsSp1TradeProofBundle, + super::RadrootsSp1TradeProofEnvelope, + ) { + let bundle = generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) + .expect("proof bundle"); + let execution = &bundle.execution; + let proof_bytes = b"deterministic proof bytes"; + let verifying_key_bytes = b"deterministic verifying key bytes"; + let public_values_hash = execution.public_values_hash.clone(); + let canonical_public_values = execution.canonical_public_values.clone(); + let result = execution.public_values.result; + let program_hash = execution + .public_values + .sp1_program_hash + .clone() + .expect("program hash"); + let verifying_key_hash = execution + .public_values + .sp1_verifying_key_hash + .clone() + .expect("verifying key hash"); + let listing_event_id = execution + .public_values + .listing_event_id + .clone() + .expect("listing event id"); + let root_event_id = execution + .public_values + .root_event_id + .clone() + .expect("root event id"); + let target_event_id = execution + .public_values + .target_event_id + .clone() + .expect("target event id"); + let event_set_root = execution.public_values.event_set_root.clone(); + let previous_state_root = execution.public_values.previous_state_root.clone(); + let new_state_root = execution.public_values.new_state_root.clone(); + let changed_records_root = execution.public_values.changed_records_root.clone(); + let error_bitmap = execution.public_values.error_bitmap.clone(); + let mut envelope = super::RadrootsSp1TradeProofEnvelope { + schema_version: super::RADROOTS_SP1_TRADE_PROOF_ARTIFACT_SCHEMA_VERSION, + sp1_version_line: super::RADROOTS_SP1_TRADE_SP1_VERSION_LINE.to_string(), + proof_system: RadrootsValidationReceiptProofSystem::Sp1Core + .as_str() + .to_string(), + proof_mode: "core".to_string(), + proof_codec: super::RADROOTS_SP1_TRADE_PROOF_CODEC.to_string(), + proof_content_hash: super::hash_bytes("radroots:sp1-proof-content:v1", proof_bytes), + proof_digest: String::new(), + public_values_hash: public_values_hash.clone(), + canonical_public_values_hash: super::hash_bytes( + "radroots:sp1-canonical-public-values:v1", + &canonical_public_values, + ), + sp1_program_hash: program_hash.clone(), + sp1_verifying_key_hash: verifying_key_hash.clone(), + sp1_verifying_key_codec: super::RADROOTS_SP1_TRADE_VERIFYING_KEY_CODEC.to_string(), + sp1_verifying_key_base64: base64::engine::general_purpose::STANDARD + .encode(verifying_key_bytes), + receipt_type: RadrootsValidationReceiptType::TradeTransition + .as_str() + .to_string(), + receipt_result: super::validation_receipt_result_label( + super::validation_receipt_result_from_public_values(result), + ) + .to_string(), + listing_event_id, + root_event_id, + target_event_id, + event_set_root, + previous_state_root, + new_state_root, + changed_records_root, + error_bitmap, + proof_content_base64: base64::engine::general_purpose::STANDARD.encode(proof_bytes), + }; + envelope.proof_digest = super::proof_digest_for_envelope(&envelope).expect("digest"); + let envelope_json = serde_json::to_vec(&envelope).expect("envelope json"); + let envelope_base64 = base64::engine::general_purpose::STANDARD.encode(envelope_json); + let mut bundle = bundle; + bundle.proof = super::RadrootsSp1TradeProofArtifact { + inline_proof_base64: Some(envelope_base64), + mode: Some("core".to_string()), + program_hash: Some(program_hash), + proof_digest: envelope.proof_digest.clone(), + proof_reference: None, + public_values_hash, + system: RadrootsValidationReceiptProofSystem::Sp1Core, + verifying_key_hash: Some(verifying_key_hash), + }; + (bundle, envelope) + } + + fn refresh_proof_envelope_digest(envelope: &mut super::RadrootsSp1TradeProofEnvelope) { + envelope.proof_digest = super::proof_digest_for_envelope(envelope).expect("digest"); + } + + fn assert_missing_none_proof_material( + execution: &RadrootsSp1TradePublicValuesExecution, + mut artifact: super::RadrootsSp1TradeProofArtifact, + ) { + artifact.proof_digest = + super::proof_digest_for_execution(execution, &artifact).expect("digest"); + assert_eq!( + verify_order_acceptance_proof_artifact_structure(execution, &artifact) + .expect_err("none material"), + RadrootsSp1TradeHostError::MissingProofMaterial + ); + } + + #[test] + fn enum_labels_and_display_surfaces_are_complete() { + let modes = [ + ( + RadrootsSp1TradeProofMode::None, + None, + RadrootsValidationReceiptProofSystem::None, + ), + ( + RadrootsSp1TradeProofMode::Core, + Some("core"), + RadrootsValidationReceiptProofSystem::Sp1Core, + ), + ( + RadrootsSp1TradeProofMode::Compressed, + Some("compressed"), + RadrootsValidationReceiptProofSystem::Sp1Compressed, + ), + ( + RadrootsSp1TradeProofMode::Groth16, + Some("groth16"), + RadrootsValidationReceiptProofSystem::Sp1Groth16, + ), + ( + RadrootsSp1TradeProofMode::Plonk, + Some("plonk"), + RadrootsValidationReceiptProofSystem::Sp1Plonk, + ), + ]; + for (mode, label, system) in modes { + assert_eq!(mode.proof_system(), system); + assert_eq!(mode.mode_label(), label); + if let Some(label) = label { + assert_eq!(RadrootsSp1TradeProofMode::from_label(label), Some(mode)); + } + } + assert_eq!( + RadrootsSp1TradeProofMode::from_label("none"), + Some(RadrootsSp1TradeProofMode::None) + ); + assert_eq!(RadrootsSp1TradeProofMode::from_label("legacy"), None); + + for backend in [ + super::RadrootsSp1TradeProverBackend::Disabled, + super::RadrootsSp1TradeProverBackend::DeterministicNone, + super::RadrootsSp1TradeProverBackend::LocalExecute, + super::RadrootsSp1TradeProverBackend::LocalCpuProve, + super::RadrootsSp1TradeProverBackend::LocalCudaProve, + super::RadrootsSp1TradeProverBackend::RemoteHttpProve, + ] { + assert_eq!( + super::RadrootsSp1TradeProverBackend::from_label(backend.as_str()), + Some(backend) + ); + assert_eq!(backend.to_string(), backend.as_str()); + } + assert_eq!( + super::RadrootsSp1TradeProverBackend::from_label("legacy"), + None + ); + + for engine in [ + super::RadrootsSp1TradeProofEngine::Cpu, + super::RadrootsSp1TradeProofEngine::Cuda, + ] { + assert_eq!( + super::RadrootsSp1TradeProofEngine::from_label(engine.as_str()), + Some(engine) + ); + assert_eq!(engine.to_string(), engine.as_str()); + } + assert_eq!(super::RadrootsSp1TradeProofEngine::from_label("tpu"), None); + assert_eq!( + super::RadrootsSp1TradeWorkerResultStatus::Succeeded.to_string(), + "succeeded" + ); + assert_eq!( + super::RadrootsSp1TradeWorkerRole::NonAuthoritativeProver.to_string(), + "non_authoritative_prover" + ); + let artifact = generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) + .expect("proof bundle") + .proof; + assert_eq!( + super::RadrootsSp1TradeResolvedProofArtifact::inline(artifact.clone()).artifact, + artifact + ); + assert_eq!( + RadrootsSp1TradeHostError::from( + radroots_trade_sp1_guest::RadrootsSp1TradeGuestError::UnsupportedProofTarget + ), + RadrootsSp1TradeHostError::Guest + ); + } + + #[test] + fn inline_proof_envelope_helpers_validate_digest_and_references() { + let (bundle, envelope) = inline_sp1_artifact_fixture(); + verify_order_acceptance_proof_artifact_structure(&bundle.execution, &bundle.proof) + .expect("inline artifact structure"); + let envelope_base64 = bundle + .proof + .inline_proof_base64 + .as_deref() + .expect("inline envelope"); + let decoded = super::decode_proof_envelope_base64(envelope_base64).expect("decode"); + assert_eq!(decoded, envelope); + assert_eq!( + super::proof_content_bytes_from_envelope(&decoded).expect("content"), + b"deterministic proof bytes" + ); + let reference = + super::proof_reference_for_proof_envelope_base64(envelope_base64).expect("reference"); + assert_eq!( + super::proof_reference_digest(reference.as_str()) + .expect("digest") + .len(), + 64 + ); + assert!(matches!( + super::proof_reference_for_proof_envelope_base64("not-base64"), + Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) + )); + assert!(matches!( + super::decode_proof_envelope_base64( + base64::engine::general_purpose::STANDARD + .encode(b"not json") + .as_str() + ), + Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) + )); + assert_eq!( + super::proof_reference_digest("legacy-reference").expect_err("prefix"), + RadrootsSp1TradeHostError::InvalidSp1ProofReference + ); + } + + #[test] + fn proof_envelope_metadata_mismatches_are_reported_by_field_family() { + let (bundle, envelope) = inline_sp1_artifact_fixture(); + let execution = &bundle.execution; + let artifact = &bundle.proof; + + let mut bad = envelope.clone(); + bad.schema_version += 1; + assert!(matches!( + super::verify_proof_envelope(execution, artifact, &bad), + Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) + )); + + let metadata_cases: [fn(&mut super::RadrootsSp1TradeProofEnvelope); 9] = [ + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.sp1_version_line = "legacy".to_string(); + }, + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.proof_codec = "legacy".to_string(); + }, + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.proof_system = "legacy".to_string(); + }, + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.proof_mode = "compressed".to_string(); + }, + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.public_values_hash = + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + .to_string(); + }, + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.sp1_program_hash = + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + .to_string(); + }, + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.sp1_verifying_key_hash = + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + .to_string(); + }, + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.sp1_verifying_key_codec = "legacy".to_string(); + }, + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.sp1_verifying_key_base64.clear(); + }, + ]; + for apply in metadata_cases { + let mut bad = envelope.clone(); + apply(&mut bad); + assert!(matches!( + super::verify_proof_envelope(execution, artifact, &bad), + Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) + )); + } + + let mut bad = envelope.clone(); + bad.proof_digest = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); + assert_eq!( + super::verify_proof_envelope(execution, artifact, &bad).expect_err("digest"), + RadrootsSp1TradeHostError::ProofDigestMismatch + ); + + let mut bad = envelope.clone(); + bad.proof_content_hash = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); + refresh_proof_envelope_digest(&mut bad); + assert!(matches!( + super::verify_proof_envelope(execution, artifact, &bad), + Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) + )); + + let mut bad = envelope.clone(); + bad.proof_content_base64 = "not-base64".to_string(); + refresh_proof_envelope_digest(&mut bad); + assert!(matches!( + super::verify_proof_envelope(execution, artifact, &bad), + Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) + )); + + let mut bad = envelope.clone(); + bad.canonical_public_values_hash = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); + refresh_proof_envelope_digest(&mut bad); + assert_eq!( + super::verify_proof_envelope(execution, artifact, &bad).expect_err("public values"), + RadrootsSp1TradeHostError::PublicValuesHashMismatch + ); + + let envelope_cases: [ProofEnvelopeMutation; 10] = [ + ( + "receipt_type", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.receipt_type = "legacy".to_string(); + }, + ), + ( + "result", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.receipt_result = "invalid".to_string(); + }, + ), + ( + "listing_event_id", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.listing_event_id = + "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + .to_string(); + }, + ), + ( + "root_event_id", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.root_event_id = + "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + .to_string(); + }, + ), + ( + "target_event_id", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.target_event_id = + "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + .to_string(); + }, + ), + ( + "state_roots", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.event_set_root = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(); + }, + ), + ( + "state_roots", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.previous_state_root = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(); + }, + ), + ( + "state_roots", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.new_state_root = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(); + }, + ), + ( + "state_roots", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.changed_records_root = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(); + }, + ), + ( + "state_roots", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.error_bitmap = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(); + }, + ), + ]; + for (field, apply) in envelope_cases { + let mut bad = envelope.clone(); + apply(&mut bad); + refresh_proof_envelope_digest(&mut bad); + assert_eq!( + super::verify_proof_envelope(execution, artifact, &bad) + .expect_err("binding mismatch"), + RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch(field) + ); + } + } + + #[test] + fn proof_artifact_structure_rejects_material_edge_cases() { + let (bundle, _) = inline_sp1_artifact_fixture(); + let execution = &bundle.execution; + let inline_proof_base64 = bundle + .proof + .inline_proof_base64 + .clone() + .expect("inline envelope"); + + let base_none_artifact = + generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) + .expect("proof bundle") + .proof; + + let mut artifact = base_none_artifact.clone(); + artifact.proof_digest = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); + assert_eq!( + verify_order_acceptance_proof_artifact_structure(execution, &artifact) + .expect_err("proof digest"), + RadrootsSp1TradeHostError::ProofDigestMismatch + ); + + let mut artifact = base_none_artifact.clone(); + artifact.inline_proof_base64 = Some(inline_proof_base64); + assert_missing_none_proof_material(execution, artifact); + + let mut artifact = base_none_artifact.clone(); + artifact.mode = Some("core".to_string()); + assert_missing_none_proof_material(execution, artifact); + + let mut artifact = base_none_artifact.clone(); + artifact.program_hash = + Some("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()); + assert_missing_none_proof_material(execution, artifact); + + let mut artifact = base_none_artifact.clone(); + artifact.proof_reference = Some(format!("radroots-proof://sha256/{}", "1".repeat(64))); + assert_missing_none_proof_material(execution, artifact); + + let mut artifact = base_none_artifact; + artifact.verifying_key_hash = + Some("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()); + assert_missing_none_proof_material(execution, artifact); + + let mut artifact = bundle.proof.clone(); + artifact.inline_proof_base64 = None; + artifact.proof_reference = None; + artifact.proof_digest = + super::proof_digest_for_execution(execution, &artifact).expect("digest"); + assert_eq!( + verify_order_acceptance_proof_artifact_structure(execution, &artifact) + .expect_err("missing sp1 material"), + RadrootsSp1TradeHostError::MissingProofMaterial + ); + + let mut artifact = bundle.proof.clone(); + artifact.proof_reference = Some(format!("radroots-proof://sha256/{}", "1".repeat(64))); + artifact.proof_digest = + super::proof_digest_for_execution(execution, &artifact).expect("digest"); + assert_eq!( + verify_order_acceptance_proof_artifact_structure(execution, &artifact) + .expect_err("conflict"), + RadrootsSp1TradeHostError::ProofMaterialConflict + ); + + let mut artifact = bundle.proof.clone(); + artifact.verifying_key_hash = + Some("0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string()); + artifact.proof_digest = + super::proof_digest_for_execution(execution, &artifact).expect("digest"); + assert_eq!( + verify_order_acceptance_proof_artifact_structure(execution, &artifact) + .expect_err("verifying key mismatch"), + RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch + ); + + let invalid_reference = format!("radroots-proof://sha256/{}", "A".repeat(64)); + assert_eq!( + super::proof_reference_digest(invalid_reference.as_str()) + .expect_err("uppercase digest"), + RadrootsSp1TradeHostError::InvalidSp1ProofReference + ); + let err = super::referenced_order_acceptance_proof_artifact_for_execution( + execution, + RadrootsSp1TradeProofMode::None, + format!("radroots-proof://sha256/{}", "1".repeat(64)), + ) + .expect_err("none reference"); + assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProofModeRequired); + } + + #[test] + fn validation_receipt_requires_event_bindings() { + let bundle = generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) + .expect("proof bundle"); + let cases: [PublicValuesExecutionMutation; 3] = [ + ("listing_event_id", |execution| { + execution.public_values.listing_event_id = None; + }), + ("root_event_id", |execution| { + execution.public_values.root_event_id = None; + }), + ("target_event_id", |execution| { + execution.public_values.target_event_id = None; + }), + ]; + for (field, clear) in cases { + let mut bundle = bundle.clone(); + clear(&mut bundle.execution); + assert_eq!( + validation_receipt_for_order_acceptance_proof(&bundle) + .expect_err("missing binding"), + RadrootsSp1TradeHostError::MissingReceiptBinding(field) + ); + } + } + + #[test] + fn execute_public_values_and_bind_validation_receipt() { + let bundle = generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) + .expect("proof bundle"); + assert_eq!( + bundle.proof.system, + RadrootsValidationReceiptProofSystem::None + ); + verify_order_acceptance_proof_artifact_structure(&bundle.execution, &bundle.proof) + .expect("proof verifies"); + + let receipt = + validation_receipt_for_order_acceptance_proof(&bundle).expect("validation receipt"); + assert_eq!( + receipt.public_values_hash, + bundle.execution.public_values_hash + ); + assert_eq!( + receipt.event_set_root, + bundle.execution.public_values.event_set_root + ); + assert_eq!( + receipt.new_state_root, + bundle.execution.public_values.new_state_root + ); + + let parts = validation_receipt_event_build("order-1", &receipt).expect("event parts"); + let event = RadrootsEventEnvelope { + id: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string(), + author: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + .to_string(), + created_at: 1, + kind: KIND_TRADE_VALIDATION_RECEIPT, + tags: parts.tags, + content: parts.content, + sig: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_string(), + }; + verify_validation_receipt_event( + &event, + RadrootsValidationReceiptExpectedBinding { + event_set_root: Some(&receipt.event_set_root), + order_id: Some("order-1"), + proof_system: Some(RadrootsValidationReceiptProofSystem::None), + public_values_hash: Some(&receipt.public_values_hash), + reducer_output_root: Some(&receipt.new_state_root), + ..RadrootsValidationReceiptExpectedBinding::default() + }, + ) + .expect("receipt verifies"); + } + + #[test] + fn proof_verifier_rejects_tampered_public_values_hash() { + let mut bundle = + generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) + .expect("proof bundle"); + bundle.proof.public_values_hash = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); + let err = + verify_order_acceptance_proof_artifact_structure(&bundle.execution, &bundle.proof) + .expect_err("tamper"); + assert_eq!(err, RadrootsSp1TradeHostError::PublicValuesHashMismatch); + } + + #[test] + fn validation_receipt_result_tracks_public_values_result() { + let mut bundle = + generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) + .expect("proof bundle"); + bundle.execution.public_values.result = RadrootsSp1TradeProofResult::Invalid; + let receipt = + validation_receipt_for_order_acceptance_proof(&bundle).expect("validation receipt"); + assert_eq!(receipt.result, RadrootsValidationReceiptResult::Invalid); + assert_eq!(validation_receipt_result_label(receipt.result), "invalid"); + } + + #[test] + fn sp1_modes_require_the_sp1_proving_lane() { + let err = + generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::Compressed) + .expect_err("synthetic sp1 proof"); + assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProofGenerationRequired); + } + + #[test] + fn sp1_artifact_program_hash_must_match_public_values_identity() { + let mut input = witness(); + input.sp1_program_hash = + Some("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()); + let execution = + super::execute_order_acceptance_public_values(&input).expect("deterministic execution"); + let mut artifact = super::RadrootsSp1TradeProofArtifact { + inline_proof_base64: None, + mode: Some("core".to_string()), + program_hash: Some( + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string(), + ), + proof_digest: String::new(), + proof_reference: Some(format!("radroots-proof://sha256/{}", "1".repeat(64))), + public_values_hash: execution.public_values_hash.clone(), + system: RadrootsValidationReceiptProofSystem::Sp1Core, + verifying_key_hash: execution.public_values.sp1_verifying_key_hash.clone(), + }; + artifact.proof_digest = + super::proof_digest_for_execution(&execution, &artifact).expect("proof digest"); + let err = verify_order_acceptance_proof_artifact_structure(&execution, &artifact) + .expect_err("program hash mismatch"); + assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProgramHashMismatch); + } + + #[test] + fn sp1_artifact_requires_public_values_sp1_identity() { + let mut input = witness(); + input.sp1_program_hash = None; + let execution = + super::execute_order_acceptance_public_values(&input).expect("deterministic execution"); + let mut artifact = super::RadrootsSp1TradeProofArtifact { + inline_proof_base64: None, + mode: Some("core".to_string()), + program_hash: Some( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + ), + proof_digest: String::new(), + proof_reference: Some(format!("radroots-proof://sha256/{}", "1".repeat(64))), + public_values_hash: execution.public_values_hash.clone(), + system: RadrootsValidationReceiptProofSystem::Sp1Core, + verifying_key_hash: execution.public_values.sp1_verifying_key_hash.clone(), + }; + artifact.proof_digest = + super::proof_digest_for_execution(&execution, &artifact).expect("proof digest"); + let err = verify_order_acceptance_proof_artifact_structure(&execution, &artifact) + .expect_err("missing program hash"); + assert_eq!(err, RadrootsSp1TradeHostError::MissingSp1ProgramHash); + } + + #[cfg(feature = "sp1_verify")] + #[tokio::test] + async fn verify_order_acceptance_validation_receipt_inline_sp1_proof_rejects_raw_material() { + let bundle = generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) + .expect("proof bundle"); + let mut receipt = + validation_receipt_for_order_acceptance_proof(&bundle).expect("validation receipt"); + receipt.proof = RadrootsValidationReceiptProof { + inline_proof_base64: Some("cHJvb2Y=".to_string()), + mode: Some("core".to_string()), + program_hash: Some( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + ), + proof_reference: None, + system: RadrootsValidationReceiptProofSystem::Sp1Core, + verifying_key_hash: Some( + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), + ), + }; + let error = super::verify_order_acceptance_validation_receipt_inline_sp1_proof(&receipt) + .await + .expect_err("raw material"); + assert!(matches!( + error, + RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_) + )); + } + + #[test] + fn sp1_artifact_program_hash_is_distinct_from_reducer_hash() { + let mut input = witness(); + input.sp1_program_hash = + Some("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()); + let execution = + super::execute_order_acceptance_public_values(&input).expect("deterministic execution"); + let mut artifact = super::RadrootsSp1TradeProofArtifact { + inline_proof_base64: None, + mode: Some("core".to_string()), + program_hash: execution.public_values.sp1_program_hash.clone(), + proof_digest: String::new(), + proof_reference: Some(format!("radroots-proof://sha256/{}", "1".repeat(64))), + public_values_hash: execution.public_values_hash.clone(), + system: RadrootsValidationReceiptProofSystem::Sp1Core, + verifying_key_hash: execution.public_values.sp1_verifying_key_hash.clone(), + }; + artifact.proof_digest = + super::proof_digest_for_execution(&execution, &artifact).expect("proof digest"); + verify_order_acceptance_proof_artifact_structure(&execution, &artifact) + .expect("artifact verifies"); + assert_ne!( + artifact.program_hash.as_deref(), + Some(execution.public_values.reducer_program_hash.as_str()) + ); + } + + #[test] + fn proof_reference_requires_canonical_sha256_uri() { + let execution = + super::execute_order_acceptance_public_values(&witness()).expect("execution"); + let err = super::referenced_order_acceptance_proof_artifact_for_execution( + &execution, + RadrootsSp1TradeProofMode::Core, + "radroots-proof://sha256/xyz".to_string(), + ) + .expect_err("invalid reference"); + assert_eq!(err, RadrootsSp1TradeHostError::InvalidSp1ProofReference); + + let artifact = super::referenced_order_acceptance_proof_artifact_for_execution( + &execution, + RadrootsSp1TradeProofMode::Core, + format!("radroots-proof://sha256/{}", "1".repeat(64)), + ) + .expect("referenced artifact"); + verify_order_acceptance_proof_artifact_structure(&execution, &artifact) + .expect("referenced artifact is structurally valid"); + } + + #[cfg(not(feature = "sp1_verify"))] + #[test] + fn sp1_verification_apis_report_unavailable_without_sp1_verify_feature() { + let runtime = tokio::runtime::Runtime::new().expect("runtime"); + runtime.block_on(async { + let bundle = + generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) + .expect("proof bundle"); + let mut inline_receipt = + validation_receipt_for_order_acceptance_proof(&bundle).expect("validation receipt"); + inline_receipt.proof = RadrootsValidationReceiptProof { + inline_proof_base64: Some("cHJvb2Y=".to_string()), + mode: Some("core".to_string()), + program_hash: Some( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + ), + proof_reference: None, + system: RadrootsValidationReceiptProofSystem::Sp1Core, + verifying_key_hash: Some( + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + .to_string(), + ), + }; + let err = + super::verify_order_acceptance_validation_receipt_inline_sp1_proof(&inline_receipt) + .await + .expect_err("verifier unavailable"); + assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProofVerifierUnavailable); + + let execution = + super::execute_order_acceptance_public_values(&witness()).expect("execution"); + let artifact = super::referenced_order_acceptance_proof_artifact_for_execution( + &execution, + RadrootsSp1TradeProofMode::Core, + format!("radroots-proof://sha256/{}", "1".repeat(64)), + ) + .expect("referenced artifact"); + let resolved = super::RadrootsSp1TradeResolvedProofArtifact { + artifact: artifact.clone(), + resolved_proof_envelope_base64: None, + }; + let err = + super::verify_order_acceptance_resolved_sp1_proof_artifact(&execution, &resolved) + .await + .expect_err("verifier unavailable"); + assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProofVerifierUnavailable); + + let mut referenced_receipt = + validation_receipt_for_order_acceptance_proof(&bundle).expect("validation receipt"); + referenced_receipt.proof = RadrootsValidationReceiptProof { + inline_proof_base64: artifact.inline_proof_base64.clone(), + mode: artifact.mode.clone(), + program_hash: artifact.program_hash.clone(), + proof_reference: artifact.proof_reference.clone(), + system: artifact.system, + verifying_key_hash: artifact.verifying_key_hash.clone(), + }; + let err = super::verify_order_acceptance_validation_receipt_resolved_sp1_proof( + &referenced_receipt, + &resolved, + ) + .await + .expect_err("verifier unavailable"); + assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProofVerifierUnavailable); + }); + } + + #[test] + fn remote_prover_contract_round_trips_provider_neutral_payloads() { + let request = super::RadrootsSp1TradeRemoteProverRequest { + schema_version: super::RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION, + request_id: "request-1".to_string(), + proof_target: RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET.to_string(), + proof_mode: RadrootsSp1TradeProofMode::Core, + sp1_version_line: super::RADROOTS_SP1_TRADE_SP1_VERSION_LINE.to_string(), + witness: witness(), + expected_sp1_program_hash: + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + expected_sp1_verifying_key_hash: + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), + expected_public_values_hash: + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + expected_reducer_program_hash: RADROOTS_SP1_TRADE_REDUCER_PROGRAM_HASH.to_string(), + expected_protocol_version: RADROOTS_SP1_TRADE_PROTOCOL_VERSION.to_string(), + expected_witness_version: RADROOTS_SP1_TRADE_WITNESS_VERSION, + }; + let request_json = serde_json::to_string(&request).expect("request json"); + let decoded_request: super::RadrootsSp1TradeRemoteProverRequest = + serde_json::from_str(&request_json).expect("decoded request"); + assert_eq!(decoded_request, request); + + let response = super::RadrootsSp1TradeRemoteProverResponse { + schema_version: super::RADROOTS_SP1_TRADE_REMOTE_PROVER_SCHEMA_VERSION, + request_id: "request-1".to_string(), + status: super::RadrootsSp1TradeRemoteProverStatus::Accepted, + status_url: None, + status_path: Some("/proofs/request-1".to_string()), + proof_system: None, + proof_mode: None, + public_values_hash: None, + sp1_program_hash: None, + sp1_verifying_key_hash: None, + proof_artifact: None, + resolved_proof_envelope_base64: None, + reason_code: None, + message: None, + detail: None, + }; + let response_json = serde_json::to_string(&response).expect("response json"); + let decoded_response: super::RadrootsSp1TradeRemoteProverResponse = + serde_json::from_str(&response_json).expect("decoded response"); + assert_eq!(decoded_response, response); + assert_eq!( + super::RadrootsSp1TradeProverBackend::from_label("remote_http_prove"), + Some(super::RadrootsSp1TradeProverBackend::RemoteHttpProve) + ); + } + + #[cfg(feature = "sp1_verify")] + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct RemoteReturnedProofFixture { + schema_version: u32, + fixture_id: String, + proof_target: String, + proof_mode: RadrootsSp1TradeProofMode, + proof_system: RadrootsValidationReceiptProofSystem, + sp1_version_line: String, + remote_prover_request: super::RadrootsSp1TradeRemoteProverRequest, + remote_prover_response: super::RadrootsSp1TradeRemoteProverResponse, + } + + #[cfg(feature = "sp1_verify")] + fn remote_returned_proof_fixture() -> RemoteReturnedProofFixture { + serde_json::from_str(include_str!( + "../tests/fixtures/remote_returned_proof_order_acceptance_core_v1.json" + )) + .expect("remote returned proof fixture") + } + + #[cfg(feature = "sp1_verify")] + fn verified_remote_returned_proof_fixture() -> ( + RadrootsSp1TradePublicValuesExecution, + super::RadrootsSp1TradeProofArtifact, + ) { + let fixture = remote_returned_proof_fixture(); + assert_eq!(fixture.schema_version, 1); + assert_eq!( + fixture.fixture_id, + "remote_returned_proof_order_acceptance_core_v1" + ); + assert_eq!( + fixture.proof_target, + RADROOTS_SP1_TRADE_ORDER_ACCEPTANCE_PROOF_TARGET + ); + assert_eq!(fixture.proof_mode, RadrootsSp1TradeProofMode::Core); + assert_eq!( + fixture.proof_system, + RadrootsValidationReceiptProofSystem::Sp1Core + ); + assert_eq!( + fixture.sp1_version_line, + super::RADROOTS_SP1_TRADE_SP1_VERSION_LINE + ); + + let request = fixture.remote_prover_request; + let response = fixture.remote_prover_response; + let execution = super::execute_order_acceptance_public_values(&request.witness) + .expect("deterministic execution"); + assert_eq!( + execution.public_values_hash, + request.expected_public_values_hash + ); + assert_eq!( + execution.public_values.sp1_program_hash.as_deref(), + Some(request.expected_sp1_program_hash.as_str()) + ); + assert_eq!( + execution.public_values.sp1_verifying_key_hash.as_deref(), + Some(request.expected_sp1_verifying_key_hash.as_str()) + ); + assert_eq!( + response.status, + super::RadrootsSp1TradeRemoteProverStatus::Completed + ); + assert_eq!(response.request_id, request.request_id); + assert_eq!( + response.public_values_hash.as_deref(), + Some(request.expected_public_values_hash.as_str()) + ); + assert_eq!( + response.sp1_program_hash.as_deref(), + Some(request.expected_sp1_program_hash.as_str()) + ); + assert_eq!( + response.sp1_verifying_key_hash.as_deref(), + Some(request.expected_sp1_verifying_key_hash.as_str()) + ); + + let artifact = response.proof_artifact.expect("proof artifact"); + verify_order_acceptance_proof_artifact_structure(&execution, &artifact) + .expect("artifact structure"); + (execution, artifact) + } + + #[cfg(feature = "sp1_verify")] + #[test] + fn remote_returned_proof_artifact_fixture_is_structurally_valid() { + verified_remote_returned_proof_fixture(); + } + + #[cfg(all(feature = "sp1_verify", radroots_sp1_real_proof_tests))] + #[tokio::test] + async fn remote_returned_proof_artifact_real_sp1_verifies() { + let (execution, artifact) = verified_remote_returned_proof_fixture(); + super::verify_order_acceptance_resolved_sp1_proof_artifact( + &execution, + &super::RadrootsSp1TradeResolvedProofArtifact::inline(artifact.clone()), + ) + .await + .expect("remote proof verifies"); + + let mut digest_mismatch = artifact.clone(); + digest_mismatch.proof_digest = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); + let err = verify_order_acceptance_proof_artifact_structure(&execution, &digest_mismatch) + .expect_err("digest mismatch"); + assert_eq!(err, RadrootsSp1TradeHostError::ProofDigestMismatch); + + let mut identity_mismatch = execution.clone(); + identity_mismatch.public_values.sp1_program_hash = + Some("0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string()); + let err = verify_order_acceptance_proof_artifact_structure(&identity_mismatch, &artifact) + .expect_err("identity mismatch"); + assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProgramHashMismatch); + + let mut public_values_mismatch = request.witness; + public_values_mismatch.inventory_sequence += 1; + let mismatch_execution = + super::execute_order_acceptance_public_values(&public_values_mismatch) + .expect("mismatch execution"); + let err = verify_order_acceptance_proof_artifact_structure(&mismatch_execution, &artifact) + .expect_err("public values mismatch"); + assert_eq!(err, RadrootsSp1TradeHostError::PublicValuesHashMismatch); + } + + #[cfg(feature = "sp1_verify")] + #[tokio::test] + async fn resolved_reference_rejects_unresolved_and_digest_mismatch() { + let execution = + super::execute_order_acceptance_public_values(&witness()).expect("execution"); + let artifact = super::referenced_order_acceptance_proof_artifact_for_execution( + &execution, + RadrootsSp1TradeProofMode::Core, + format!("radroots-proof://sha256/{}", "1".repeat(64)), + ) + .expect("referenced artifact"); + + let err = super::verify_order_acceptance_resolved_sp1_proof_artifact( + &execution, + &super::RadrootsSp1TradeResolvedProofArtifact { + artifact: artifact.clone(), + resolved_proof_envelope_base64: None, + }, + ) + .await + .expect_err("unresolved reference"); + assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProofReferenceUnresolved); + + let err = super::verify_order_acceptance_resolved_sp1_proof_artifact( + &execution, + &super::RadrootsSp1TradeResolvedProofArtifact { + artifact, + resolved_proof_envelope_base64: Some("cHJvb2Y=".to_string()), + }, + ) + .await + .expect_err("digest mismatch"); + assert_eq!( + err, + RadrootsSp1TradeHostError::Sp1ProofReferenceDigestMismatch + ); + } + + #[test] + fn none_proof_mode_builds_deterministic_reducer_receipt() { + let mut input = witness(); + input.sp1_verifying_key_hash = None; + let bundle = generate_order_acceptance_proof(&input, RadrootsSp1TradeProofMode::None) + .expect("none proof"); + assert_eq!( + bundle.proof.system, + RadrootsValidationReceiptProofSystem::None + ); + let receipt = + validation_receipt_for_order_acceptance_proof(&bundle).expect("validation receipt"); + assert_eq!( + receipt.proof.system, + RadrootsValidationReceiptProofSystem::None + ); + assert!(receipt.proof.inline_proof_base64.is_none()); + } + + #[test] + fn deterministic_crates_do_not_depend_on_sp1_sdk() { + let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + let crates_dir = manifest_dir.parent().expect("crates dir"); + for crate_dir in ["event", "trade", "trade_sp1_guest"] { + let manifest = std::fs::read_to_string(crates_dir.join(crate_dir).join("Cargo.toml")) + .expect("manifest"); + assert!(!manifest.contains("sp1-sdk")); + assert!(!manifest.contains("sp1_sdk")); + } + } + + #[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] + #[tokio::test] + async fn sp1_execute_public_values_match_deterministic_reducer() { + let input = witness_without_sp1_identity(); + let execution = super::execute_order_acceptance_sp1_public_values(&input) + .await + .expect("sp1 execute"); + let mut expected_input = input; + expected_input.sp1_program_hash = + execution.execution.public_values.sp1_program_hash.clone(); + expected_input.sp1_verifying_key_hash = execution + .execution + .public_values + .sp1_verifying_key_hash + .clone(); + let expected = super::execute_order_acceptance_public_values(&expected_input) + .expect("deterministic execution"); + assert_eq!(execution.execution, expected); + assert_eq!( + execution.committed_public_values, + expected.canonical_public_values + ); + assert_eq!(execution.report.exit_code, 0); + assert!(execution.report.total_instruction_count > 0); + } + + #[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] + #[tokio::test] + async fn expensive_proof_generation_and_verification_is_runnable() { + let bundle = super::generate_order_acceptance_sp1_proof( + &witness_without_sp1_identity(), + RadrootsSp1TradeProofMode::Core, + ) + .await + .expect("proof bundle"); + super::verify_order_acceptance_resolved_sp1_proof_artifact( + &bundle.execution, + &super::RadrootsSp1TradeResolvedProofArtifact::inline(bundle.proof.clone()), + ) + .await + .expect("proof verifies"); + let receipt = + validation_receipt_for_order_acceptance_proof(&bundle).expect("validation receipt"); + let verification = + super::verify_order_acceptance_validation_receipt_inline_sp1_proof(&receipt) + .await + .expect("receipt proof verifies"); + assert_eq!( + bundle.proof.system, + RadrootsValidationReceiptProofSystem::Sp1Core + ); + assert!(bundle.proof.inline_proof_base64.is_some()); + assert_eq!(verification.proof_mode, RadrootsSp1TradeProofMode::Core); + assert_eq!(verification.public_values_hash, receipt.public_values_hash); + assert_eq!( + verification.sp1_program_hash, + receipt.proof.program_hash.expect("receipt program hash") + ); + assert_eq!( + verification.sp1_verifying_key_hash, + receipt + .proof + .verifying_key_hash + .expect("receipt verifying key hash") + ); + } + + #[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] + #[tokio::test] + async fn real_sp1_verifier_rejects_missing_and_synthetic_material() { + let execution = + super::execute_order_acceptance_sp1_public_values(&witness_without_sp1_identity()) + .await + .expect("sp1 execute") + .execution; + let mut missing = super::RadrootsSp1TradeProofArtifact { + inline_proof_base64: None, + mode: Some("core".to_string()), + program_hash: execution.public_values.sp1_program_hash.clone(), + proof_digest: "0x00".to_string(), + proof_reference: None, + public_values_hash: execution.public_values_hash.clone(), + system: RadrootsValidationReceiptProofSystem::Sp1Core, + verifying_key_hash: execution.public_values.sp1_verifying_key_hash.clone(), + }; + let err = super::verify_order_acceptance_resolved_sp1_proof_artifact( + &execution, + &super::RadrootsSp1TradeResolvedProofArtifact::inline(missing.clone()), + ) + .await + .expect_err("missing proof material"); + assert_eq!(err, RadrootsSp1TradeHostError::ProofDigestMismatch); + + missing.proof_digest = + super::proof_digest_for_execution(&execution, &missing).expect("missing proof digest"); + let err = super::verify_order_acceptance_resolved_sp1_proof_artifact( + &execution, + &super::RadrootsSp1TradeResolvedProofArtifact::inline(missing.clone()), + ) + .await + .expect_err("missing proof material"); + assert_eq!(err, RadrootsSp1TradeHostError::MissingProofMaterial); + + let mut synthetic = missing; + synthetic.inline_proof_base64 = + Some(base64::engine::general_purpose::STANDARD.encode(b"synthetic proof material")); + synthetic.proof_digest = super::proof_digest_for_execution(&execution, &synthetic) + .expect("synthetic proof digest"); + let err = super::verify_order_acceptance_resolved_sp1_proof_artifact( + &execution, + &super::RadrootsSp1TradeResolvedProofArtifact::inline(synthetic), + ) + .await + .expect_err("synthetic proof material"); + assert!(matches!( + err, + RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_) + )); + } +} diff --git a/crates/sp1_host_trade/tests/fixtures/remote_returned_proof_order_acceptance_core_v1.json b/crates/trade_sp1_host/tests/fixtures/remote_returned_proof_order_acceptance_core_v1.json diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs @@ -5664,8 +5664,8 @@ edition = "2024" "surface.internal_replica_crates.storage must be a crate identifier", |bundle| { bundle.manifest.surface.internal_replica_crates = Some(InternalReplicaCrates { - schema: "radroots_replica_db_schema".to_string(), - storage: "crates/replica_db".to_string(), + schema: "radroots_replica_schema".to_string(), + storage: "crates/replica_store".to_string(), sync: "radroots_replica_sync".to_string(), }); },