lib

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

commit 2e0bc517499df7efebda9eaf6a25ca82038daec2
parent 53d95b42f1a66297d3ffb2e01aaeab53873b8a1b
Author: triesap <tyson@radroots.org>
Date:   Wed, 24 Dec 2025 23:08:31 +0000

sql: support rel-based location_gcs lookups


- Extend ILocationGcsFindOne to accept rel selector variants
- Implement rel_query and find_one_by_rel SQL helpers
- Update create/update/delete paths to use enum-based find/delete args
- Add trade_listing_dvm_tags helper with unit tests for tag assembly

Diffstat:
Mtangle-schema/bindings/ts/src/types.ts | 2+-
Mtangle-schema/src/models/location_gcs.rs | 20++++++++++++++------
Mtangle-sql/src/models/location_gcs.rs | 110++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mtrade/src/listing/tags.rs | 43++++++++++++++++++++++++++++++++++++++++++-
4 files changed, 129 insertions(+), 46 deletions(-)

diff --git a/tangle-schema/bindings/ts/src/types.ts b/tangle-schema/bindings/ts/src/types.ts @@ -54,7 +54,7 @@ export type ILocationGcsFindMany = { filter: ILocationGcsFieldsFilter | null, } export type ILocationGcsFindManyResolve = IResultList<LocationGcs>; -export type ILocationGcsFindOne = { on: LocationGcsQueryBindValues, }; +export type ILocationGcsFindOne = { on: LocationGcsQueryBindValues, } | { rel: LocationGcsFindManyRel, }; export type ILocationGcsFindOneResolve = IResult<LocationGcs>; diff --git a/tangle-schema/src/models/location_gcs.rs b/tangle-schema/src/models/location_gcs.rs @@ -209,16 +209,25 @@ pub type ILocationGcsCreate = ILocationGcsFields; )] pub struct ILocationGcsCreateResolveTs; pub type ILocationGcsCreateResolve = IResult<LocationGcs>; +#[derive(Deserialize, Serialize)] +pub struct ILocationGcsFindOneArgs { + pub on: LocationGcsQueryBindValues, +} #[cfg_attr(feature = "ts-rs", derive(TS))] #[cfg_attr( feature = "ts-rs", ts(export, export_to = "types.ts", rename = "ILocationGcsFindOne") )] #[derive(Deserialize, Serialize)] -pub struct ILocationGcsFindOneArgs { - pub on: LocationGcsQueryBindValues, +#[serde(untagged)] +pub enum ILocationGcsFindOne { + On { + on: LocationGcsQueryBindValues, + }, + Rel { + rel: LocationGcsFindManyRel, + }, } -pub type ILocationGcsFindOne = ILocationGcsFindOneArgs; #[cfg_attr(feature = "ts-rs", derive(TS))] #[cfg_attr( feature = "ts-rs", @@ -269,7 +278,7 @@ pub type ILocationGcsFindManyResolve = IResultList<LocationGcs>; ) )] pub struct ILocationGcsDeleteTs; -pub type ILocationGcsDelete = ILocationGcsFindOneArgs; +pub type ILocationGcsDelete = ILocationGcsFindOne; #[cfg_attr(feature = "ts-rs", derive(TS))] #[cfg_attr( feature = "ts-rs", @@ -304,4 +313,4 @@ pub type ILocationGcsUpdate = ILocationGcsUpdateArgs; ) )] pub struct ILocationGcsUpdateResolveTs; -pub type ILocationGcsUpdateResolve = IResult<LocationGcs>; -\ No newline at end of file +pub type ILocationGcsUpdateResolve = IResult<LocationGcs>; diff --git a/tangle-sql/src/models/location_gcs.rs b/tangle-sql/src/models/location_gcs.rs @@ -36,7 +36,7 @@ pub fn create<E: SqlExecutor>( let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); let params_json = utils::to_params_json(bind_values)?; let _ = exec.exec(&sql, &params_json)?; - let args = ILocationGcsFindOne { + let args = ILocationGcsFindOne::On { on: LocationGcsQueryBindValues::Id { id: id.clone() }, }; let found = find_one(exec, &args)?; @@ -50,12 +50,17 @@ pub fn find_one<E: SqlExecutor>( exec: &E, opts: &ILocationGcsFindOne, ) -> Result<ILocationGcsFindOneResolve, IError<SqlError>> { - let (column, value) = opts.on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value])?; - let json = exec.query_raw(&sql, &params_json)?; - let mut rows: Vec<LocationGcs> = utils::parse_json(&json)?; - let result = rows.pop(); + let result = match opts { + ILocationGcsFindOne::On { on } => { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value])?; + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<LocationGcs> = utils::parse_json(&json)?; + rows.pop() + } + ILocationGcsFindOne::Rel { rel } => find_one_by_rel(exec, rel)?, + }; Ok(IResult { result }) } @@ -81,33 +86,46 @@ fn find_many_filter<E: SqlExecutor>( Ok(rows) } +fn rel_query(rel: &LocationGcsFindManyRel) -> (&'static str, Vec<Value>) { + match rel { + LocationGcsFindManyRel::OnTradeProduct(args) => ( + "SELECT lg.* FROM location_gcs lg JOIN trade_product_location tp_lg ON lg.id = tp_lg.tb_lg WHERE tp_lg.tb_tp = ?", + vec![Value::from(args.id.clone())], + ), + LocationGcsFindManyRel::OffTradeProduct(args) => ( + "SELECT lg.* FROM location_gcs lg WHERE NOT EXISTS (SELECT 1 FROM trade_product_location tp_lg WHERE tp_lg.tb_lg = lg.id AND tp_lg.tb_tp = ?)", + vec![Value::from(args.id.clone())], + ), + LocationGcsFindManyRel::OnFarm(args) => ( + "SELECT lg.* FROM location_gcs lg JOIN farm_location farm_lg ON lg.id = farm_lg.tb_lg WHERE farm_lg.tb_farm = ?", + vec![Value::from(args.id.clone())], + ), + LocationGcsFindManyRel::OffFarm(args) => ( + "SELECT lg.* FROM location_gcs lg WHERE NOT EXISTS (SELECT 1 FROM farm_location farm_lg WHERE farm_lg.tb_lg = lg.id AND farm_lg.tb_farm = ?)", + vec![Value::from(args.id.clone())], + ), + } +} + +fn find_one_by_rel<E: SqlExecutor>( + exec: &E, + rel: &LocationGcsFindManyRel, +) -> Result<Option<LocationGcs>, IError<SqlError>> { + let (sql, bind_values) = rel_query(rel); + let params_json = utils::to_params_json(bind_values)?; + let sql = format!("{sql} LIMIT 1;"); + let json = exec.query_raw(&sql, &params_json)?; + let mut rows: Vec<LocationGcs> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + fn find_many_by_rel<E: SqlExecutor>( exec: &E, rel: &LocationGcsFindManyRel, ) -> Result<Vec<LocationGcs>, IError<SqlError>> { - let (sql, bind_values): (String, Vec<Value>) = match rel { - LocationGcsFindManyRel::OnTradeProduct(args) => { - let sql = String::from("SELECT lg.* FROM location_gcs lg JOIN trade_product_location tp_lg ON lg.id = tp_lg.tb_lg WHERE tp_lg.tb_tp = ?;"); - let binds = vec![Value::from(args.id.clone())]; - (sql, binds) - } - LocationGcsFindManyRel::OffTradeProduct(args) => { - let sql = String::from("SELECT lg.* FROM location_gcs lg WHERE NOT EXISTS (SELECT 1 FROM trade_product_location tp_lg WHERE tp_lg.tb_lg = lg.id AND tp_lg.tb_tp = ?);"); - let binds = vec![Value::from(args.id.clone())]; - (sql, binds) - } - LocationGcsFindManyRel::OnFarm(args) => { - let sql = String::from("SELECT lg.* FROM location_gcs lg JOIN farm_location farm_lg ON lg.id = farm_lg.tb_lg WHERE farm_lg.tb_farm = ?;"); - let binds = vec![Value::from(args.id.clone())]; - (sql, binds) - } - LocationGcsFindManyRel::OffFarm(args) => { - let sql = String::from("SELECT lg.* FROM location_gcs lg WHERE NOT EXISTS (SELECT 1 FROM farm_location farm_lg WHERE farm_lg.tb_lg = lg.id AND farm_lg.tb_farm = ?);"); - let binds = vec![Value::from(args.id.clone())]; - (sql, binds) - } - }; + let (sql, bind_values) = rel_query(rel); let params_json = utils::to_params_json(bind_values)?; + let sql = format!("{sql};"); let json = exec.query_raw(&sql, &params_json)?; let rows: Vec<LocationGcs> = utils::parse_json(&json)?; Ok(rows) @@ -143,7 +161,7 @@ pub fn update<E: SqlExecutor>( let id_for_lookup = match opts.on.primary_key() { Some(id) => id, None => { - let find_opts = ILocationGcsFindOne { + let find_opts = ILocationGcsFindOne::On { on: opts.on.clone(), }; let found = find_one(exec, &find_opts)?; @@ -166,14 +184,21 @@ pub fn delete<E: SqlExecutor>( exec: &E, opts: &ILocationGcsDelete, ) -> Result<ILocationGcsDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts.on.primary_key() { - Some(id) => id, - None => { - let find_opts = ILocationGcsFindOne { - on: opts.on.clone(), - }; - let found = find_one(exec, &find_opts)?; - let model = found.result.ok_or_else(|| IError::from(SqlError::NotFound(opts.on.lookup_key())))?; + let id_for_lookup = match opts { + ILocationGcsDelete::On { on } => match on.primary_key() { + Some(id) => id, + None => { + let find_opts = ILocationGcsFindOne::On { on: on.clone() }; + let found = find_one(exec, &find_opts)?; + let model = found + .result + .ok_or_else(|| IError::from(SqlError::NotFound(on.lookup_key())))?; + model.id + } + }, + ILocationGcsDelete::Rel { rel } => { + let found = find_one_by_rel(exec, rel)?; + let model = found.ok_or_else(|| IError::from(SqlError::NotFound(rel_lookup_key(rel))))?; model.id } }; @@ -185,3 +210,12 @@ pub fn delete<E: SqlExecutor>( } Ok(IResult { result: id_for_lookup }) } + +fn rel_lookup_key(rel: &LocationGcsFindManyRel) -> String { + match rel { + LocationGcsFindManyRel::OnTradeProduct(args) => format!("on_trade_product:{}", args.id.as_str()), + LocationGcsFindManyRel::OffTradeProduct(args) => format!("off_trade_product:{}", args.id.as_str()), + LocationGcsFindManyRel::OnFarm(args) => format!("on_farm:{}", args.id.as_str()), + LocationGcsFindManyRel::OffFarm(args) => format!("off_farm:{}", args.id.as_str()), + } +} diff --git a/trade/src/listing/tags.rs b/trade/src/listing/tags.rs @@ -13,6 +13,26 @@ fn push_tag(tags: &mut Vec<Vec<String>>, name: &'static str, value: impl Into<St } #[inline] +pub fn trade_listing_dvm_tags<P, A, D>( + recipient_pubkey: P, + listing_addr: A, + order_id: Option<D>, +) -> Vec<Vec<String>> +where + P: Into<String>, + A: Into<String>, + D: Into<String>, +{ + let mut tags = Vec::with_capacity(2 + usize::from(order_id.is_some())); + push_tag(&mut tags, "p", recipient_pubkey); + push_tag(&mut tags, "a", listing_addr); + if let Some(order_id) = order_id { + push_tag(&mut tags, TAG_D, order_id); + } + tags +} + +#[inline] pub fn push_trade_listing_chain_tags( tags: &mut Vec<Vec<String>>, e_root_id: impl Into<String>, @@ -81,7 +101,7 @@ pub fn validate_trade_listing_chain(tags: &[Vec<String>]) -> Result<(), JobParse #[cfg(test)] mod tests { - use super::validate_trade_listing_chain; + use super::{trade_listing_dvm_tags, validate_trade_listing_chain}; use radroots_events::tags::{TAG_D, TAG_E_ROOT}; use radroots_events_codec::job::error::JobParseError; @@ -118,4 +138,25 @@ mod tests { other => panic!("expected invalid root tag, got {other:?}"), } } + + #[test] + fn trade_listing_dvm_tags_builds_expected_tags() { + let tags = trade_listing_dvm_tags("pubkey", "30402:pubkey:listing", Some("order-1")); + let expected: Vec<Vec<String>> = vec![ + vec![String::from("p"), String::from("pubkey")], + vec![String::from("a"), String::from("30402:pubkey:listing")], + vec![String::from(TAG_D), String::from("order-1")], + ]; + assert_eq!(tags, expected); + } + + #[test] + fn trade_listing_dvm_tags_omit_order_id_when_missing() { + let tags = trade_listing_dvm_tags("pubkey", "30402:pubkey:listing", None::<String>); + let expected: Vec<Vec<String>> = vec![ + vec![String::from("p"), String::from("pubkey")], + vec![String::from("a"), String::from("30402:pubkey:listing")], + ]; + assert_eq!(tags, expected); + } }