commit ac01d23e01513a9c36e350477a71c3f81cd03422
parent 643040091375a56717dbd0200d8af6b9209640b8
Author: triesap <triesap@radroots.dev>
Date: Sun, 26 Oct 2025 20:36:30 +0000
Integrate `radroots-core` types and refactor `indexer` listing process to parse structured product, quantity, price, location, and images from tags, replacing JSON parsing and refining error semantics.
Diffstat:
13 files changed, 239 insertions(+), 26 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,5 +1,6 @@
/target
node_modules
+.turbo
# Debug
logs/
diff --git a/Cargo.lock b/Cargo.lock
@@ -1334,6 +1334,7 @@ dependencies = [
"config",
"indexer-utils",
"once_cell",
+ "radroots-core",
"radroots-events",
"radroots-events-indexed",
"regex",
diff --git a/Cargo.toml b/Cargo.toml
@@ -11,6 +11,7 @@ rust-version = "1.86.0"
license = "AGPL-3.0"
[workspace.dependencies]
+radroots-core = { path = "../../crates/crates/core" }
radroots-events = { path = "../../crates/crates/events" }
radroots-events-indexed = { path = "../../crates/crates/events-indexed" }
diff --git a/app/src/routes/(market)/(listing)/[0=country]/+page.ts b/app/src/routes/(market)/(listing)/[0=country]/+page.ts
@@ -5,7 +5,7 @@ import type { RadrootsEventsIndexedManifest } from "@radroots/events-indexed-bin
import { error } from "@sveltejs/kit";
import type { EntryGenerator, PageLoad } from "./$types";
-const { RADROOTS_MARKET_RELAY_INDEXES_URL: indexes_url } = _env;
+const { RADROOTS_MARKET_RELAY_INDEXES_URL: idx_url } = _env;
export const entries: EntryGenerator = async () => {
const [
@@ -13,7 +13,7 @@ export const entries: EntryGenerator = async () => {
]: [
string[]
] = await Promise.all([
- fetch(`${indexes_url}/events/30402/country/indexes.json`).then(r => r.json())
+ fetch(`${idx_url}/events/30402/country/indexes.json`).then(r => r.json())
]);
return events_0_country_indexes.map(i => ({ 0: i }))
};
@@ -30,7 +30,7 @@ export const load: PageLoad<PageLoadData> = async ({ fetch, params }) => {
const [
res_country_manifest,
] = await Promise.all([
- fetch(`${indexes_url}/events/30402/country/${country}/manifest.json`)
+ fetch(`${idx_url}/events/30402/country/${country}/manifest.json`)
]);
if (!res_country_manifest.ok) error(404, { message: `country:${country}` });
@@ -40,7 +40,7 @@ export const load: PageLoad<PageLoadData> = async ({ fetch, params }) => {
let events: RadrootsListingEventMetadata[] = [];
if (manifest.shards.length > 0) {
const shard = manifest.shards[0];
- const res_country_shard = await fetch(`${indexes_url}/events/30402/country/${country}/${shard.file}?v=${shard.sha256}`);
+ const res_country_shard = await fetch(`${idx_url}/events/30402/country/${country}/${shard.file}?v=${shard.sha256}`);
if (!res_country_shard.ok) error(500, { message: `load:${country}:${shard.file}` });
events = await res_country_shard.json();
}
diff --git a/app/src/routes/(market)/(profile)/[0=nip05]/+error.svelte b/app/src/routes/(market)/(profile)/[0=nip05]/+error.svelte
@@ -0,0 +1,9 @@
+<script lang="ts">
+ import { page } from "$app/state";
+</script>
+
+<div class={`flex flex-col w-full justify-start items-start`}>
+ <p class={`font-sans font-[400] text-base text-ly0-gl`}>
+ {`error: ${page.error?.message}`}
+ </p>
+</div>
diff --git a/app/src/routes/(market)/(profile)/[0=nip05]/+page.svelte b/app/src/routes/(market)/(profile)/[0=nip05]/+page.svelte
@@ -3,6 +3,10 @@
import type { PageProps } from "./$types";
let { data }: PageProps = $props();
+
+ $effect(() => {
+ console.log(JSON.stringify(data, null, 4), `data`);
+ });
</script>
<Profile basis={{ indexed: data }} />
diff --git a/app/src/routes/(market)/(profile)/[0=nip05]/+page.ts b/app/src/routes/(market)/(profile)/[0=nip05]/+page.ts
@@ -6,7 +6,7 @@ import { lib_nostr_npub_encode } from "@radroots/utils-nostr";
import { error } from "@sveltejs/kit";
import type { EntryGenerator, PageLoad } from "./$types";
-const { RADROOTS_MARKET_RELAY_INDEXES_URL: indexes_url } = _env;
+const { RADROOTS_MARKET_RELAY_INDEXES_URL: idx_url } = _env;
export const entries: EntryGenerator = async () => {
const [
@@ -14,7 +14,7 @@ export const entries: EntryGenerator = async () => {
]: [
string[]
] = await Promise.all([
- fetch(`${indexes_url}/events/0/nip05/indexes.json`).then(r => r.json())
+ fetch(`${idx_url}/events/0/nip05/indexes.json`).then(r => r.json())
]);
return events_0_author_indexes.map(i => ({ 0: i }))
};
@@ -28,8 +28,8 @@ export const load: PageLoad<PageLoadData> = async ({ fetch, params }) => {
res_nip05_metadata,
res_nip05_listings_manifest,
] = await Promise.all([
- fetch(`${indexes_url}/events/0/nip05/${nip05}/metadata.json`),
- fetch(`${indexes_url}/events/30402/nip05/${nip05}/manifest.json`)
+ fetch(`${idx_url}/events/0/nip05/${nip05}/metadata.json`),
+ fetch(`${idx_url}/events/30402/nip05/${nip05}/manifest.json`)
]);
if (!res_nip05_metadata.ok) error(404, { message: `nip05:${nip05}`, });
@@ -41,7 +41,7 @@ export const load: PageLoad<PageLoadData> = async ({ fetch, params }) => {
let listings_events: RadrootsListingEventMetadata[] = [];
if (listings_manifest.shards.length > 0) {
const shard = listings_manifest.shards[0];
- const res_country_shard = await fetch(`${indexes_url}/events/30402/nip05/${nip05}/${shard.file}?v=${shard.sha256}`);
+ const res_country_shard = await fetch(`${idx_url}/events/30402/nip05/${nip05}/${shard.file}?v=${shard.sha256}`);
if (!res_country_shard.ok) error(500, { message: `nip05:listing:shard:${nip05}:${shard.file}` });
listings_events = await res_country_shard.json();
}
diff --git a/app/src/routes/(market)/(profile)/profile/[0=npub]/+page.ts b/app/src/routes/(market)/(profile)/profile/[0=npub]/+page.ts
@@ -4,7 +4,7 @@ import type { RadrootsProfileEventMetadata } from "@radroots/events-bindings";
import { error } from "@sveltejs/kit";
import type { EntryGenerator, PageLoad } from "./$types";
-const { RADROOTS_MARKET_RELAY_INDEXES_URL: indexes_url } = _env;
+const { RADROOTS_MARKET_RELAY_INDEXES_URL: idx_url } = _env;
export const entries: EntryGenerator = async () => {
const [
@@ -12,7 +12,7 @@ export const entries: EntryGenerator = async () => {
]: [
string[]
] = await Promise.all([
- fetch(`${indexes_url}/events/0/npub/indexes.json`).then(r => r.json())
+ fetch(`${idx_url}/events/0/npub/indexes.json`).then(r => r.json())
]);
return events_0_author_indexes.map(i => ({ 0: i }))
};
@@ -25,7 +25,7 @@ export const load: PageLoad<PageLoadData> = async ({ fetch, params }) => {
const [
res_npub_metadata,
] = await Promise.all([
- fetch(`${indexes_url}/events/0/npub/${npub}/metadata.json`)
+ fetch(`${idx_url}/events/0/npub/${npub}/metadata.json`)
]);
if (!res_npub_metadata.ok) error(404, { message: `npub:${npub}` });
diff --git a/app/src/routes/(market)/(profile)/profile/[0=public_key]/+page.ts b/app/src/routes/(market)/(profile)/profile/[0=public_key]/+page.ts
@@ -5,7 +5,7 @@ import { lib_nostr_npub_encode } from "@radroots/utils-nostr";
import { error } from "@sveltejs/kit";
import type { EntryGenerator, PageLoad } from "./$types";
-const { RADROOTS_MARKET_RELAY_INDEXES_URL: indexes_url } = _env;
+const { RADROOTS_MARKET_RELAY_INDEXES_URL: idx_url } = _env;
export const entries: EntryGenerator = async () => {
const [
@@ -13,7 +13,7 @@ export const entries: EntryGenerator = async () => {
]: [
string[]
] = await Promise.all([
- fetch(`${indexes_url}/events/0/author/indexes.json`).then(r => r.json())
+ fetch(`${idx_url}/events/0/author/indexes.json`).then(r => r.json())
]);
return events_0_author_indexes.map(i => ({ 0: i }))
};
@@ -26,7 +26,7 @@ export const load: PageLoad<PageLoadData> = async ({ fetch, params }) => {
const [
res_author_metadata,
] = await Promise.all([
- fetch(`${indexes_url}/events/0/author/${public_key}/metadata.json`),
+ fetch(`${idx_url}/events/0/author/${public_key}/metadata.json`),
]);
if (!res_author_metadata.ok) error(404, { message: `public_key:${public_key}` });
diff --git a/app/src/routes/+page.ts b/app/src/routes/+page.ts
@@ -2,7 +2,7 @@ import { _env } from "$lib/utils/_env";
import { error } from "@sveltejs/kit";
import type { PageLoad } from "./$types";
-const { RADROOTS_MARKET_RELAY_INDEXES_URL: indexes_url } = _env;
+const { RADROOTS_MARKET_RELAY_INDEXES_URL: idx_url } = _env;
type PageLoadData = {
profiles: string[];
@@ -10,13 +10,12 @@ type PageLoadData = {
};
export const load: PageLoad<PageLoadData> = async ({ fetch, params }) => {
-
const [
res_nip05_indexes,
res_country_indexes,
] = await Promise.all([
- fetch(`${indexes_url}/events/30402/nip05/indexes.json`),
- fetch(`${indexes_url}/events/30402/country/indexes.json`),
+ fetch(`${idx_url}/events/30402/nip05/indexes.json`),
+ fetch(`${idx_url}/events/30402/country/indexes.json`),
]);
if (!res_nip05_indexes.ok) error(404, { message: `nip05:indexes` });
diff --git a/crates/indexer/Cargo.toml b/crates/indexer/Cargo.toml
@@ -11,6 +11,7 @@ audit = []
[dependencies]
indexer-utils = { path = "../indexer-utils" }
+radroots-core = { workspace = true }
radroots-events = { workspace = true }
radroots-events-indexed = { workspace = true }
diff --git a/crates/indexer/src/domain/events/listing.rs b/crates/indexer/src/domain/events/listing.rs
@@ -1,7 +1,11 @@
use thiserror::Error;
use radroots_events::{
- listing::models::{RadrootsListing, RadrootsListingEventIndex, RadrootsListingEventMetadata},
+ listing::models::{
+ RadrootsListing, RadrootsListingEventIndex, RadrootsListingEventMetadata,
+ RadrootsListingImage, RadrootsListingImageSize, RadrootsListingLocation,
+ RadrootsListingPrice, RadrootsListingProduct, RadrootsListingQuantity,
+ },
RadrootsNostrEvent,
};
@@ -9,8 +13,200 @@ use crate::relay::event::RelayIndexerEvent;
#[derive(Debug, Error)]
pub enum RadrootsListingEventIndexError {
- #[error("Failed to parse listing JSON: {0}")]
- ParseError(#[from] serde_json::Error),
+ #[error("Failed to parse listing from tags")]
+ ParseError,
+}
+
+fn parse_listing_from_tags(
+ tags: &[Vec<String>],
+) -> Result<RadrootsListing, RadrootsListingEventIndexError> {
+ let get_first = |key: &str| -> Option<String> {
+ tags.iter()
+ .find(|t| {
+ t.get(0)
+ .map(|s| s.eq_ignore_ascii_case(key))
+ .unwrap_or(false)
+ })
+ .and_then(|t| t.get(1).cloned())
+ };
+
+ let required = |v: Option<String>| v.ok_or(RadrootsListingEventIndexError::ParseError);
+
+ let d_tag = required(get_first("d"))?;
+
+ let product = RadrootsListingProduct {
+ key: required(get_first("key"))?,
+ title: required(get_first("title"))?,
+ category: required(get_first("category"))?,
+ summary: get_first("summary"),
+ process: get_first("process"),
+ lot: get_first("lot"),
+ location: get_first("location"),
+ profile: get_first("profile"),
+ year: get_first("year"),
+ };
+
+ let mut quantities: Vec<RadrootsListingQuantity> = Vec::new();
+ for t in tags
+ .iter()
+ .filter(|t| t.first().map(|k| k == "quantity").unwrap_or(false))
+ {
+ if t.len() >= 3 {
+ let amount = match t[1].parse::<radroots_core::RadrootsCoreDecimal>() {
+ Ok(v) => v,
+ Err(_) => continue,
+ };
+ let unit = match t[2].parse::<radroots_core::RadrootsCoreUnit>() {
+ Ok(v) => v,
+ Err(_) => continue,
+ };
+ let label = t.get(3).cloned();
+ quantities.push(RadrootsListingQuantity {
+ value: radroots_core::RadrootsCoreQuantity {
+ amount,
+ unit,
+ label: label.clone(),
+ },
+ label,
+ count: None,
+ });
+ }
+ }
+
+ let mut prices: Vec<RadrootsListingPrice> = Vec::new();
+ for t in tags
+ .iter()
+ .filter(|t| t.first().map(|k| k == "price").unwrap_or(false))
+ {
+ if t.len() >= 5 {
+ let money_amount = match t[1].parse::<radroots_core::RadrootsCoreDecimal>() {
+ Ok(v) => v,
+ Err(_) => continue,
+ };
+ let money_currency = match t[2].parse::<radroots_core::RadrootsCoreCurrency>() {
+ Ok(v) => v,
+ Err(_) => continue,
+ };
+ let qty_amount = match t[3].parse::<radroots_core::RadrootsCoreDecimal>() {
+ Ok(v) => v,
+ Err(_) => continue,
+ };
+ let qty_unit = match t[4].parse::<radroots_core::RadrootsCoreUnit>() {
+ Ok(v) => v,
+ Err(_) => continue,
+ };
+
+ let price = radroots_core::RadrootsCoreQuantityPrice {
+ amount: radroots_core::RadrootsCoreMoney {
+ amount: money_amount,
+ currency: money_currency,
+ },
+ quantity: radroots_core::RadrootsCoreQuantity {
+ amount: qty_amount,
+ unit: qty_unit,
+ label: None,
+ },
+ };
+ prices.push(price);
+ }
+ }
+
+ let mut primary: Option<String> = None;
+ let mut city: Option<String> = None;
+ let mut region: Option<String> = None;
+ let mut country: Option<String> = None;
+ if let Some(t) = tags
+ .iter()
+ .find(|t| t.first().map(|k| k == "location").unwrap_or(false))
+ {
+ if t.len() >= 2 {
+ primary = Some(t[1].clone());
+ }
+ if t.len() >= 3 {
+ city = Some(t[2].clone());
+ }
+ if t.len() >= 4 {
+ region = Some(t[3].clone());
+ }
+ if t.len() >= 5 {
+ country = Some(t[4].clone());
+ }
+ }
+
+ let geohash = tags
+ .iter()
+ .filter(|t| t.first().map(|k| k == "g").unwrap_or(false))
+ .filter_map(|t| t.get(1).cloned())
+ .max_by_key(|s| s.len());
+
+ let mut lat: Option<f64> = None;
+ let mut lng: Option<f64> = None;
+ for t in tags.iter().filter(|t| {
+ t.first()
+ .map(|k| k.eq_ignore_ascii_case("l"))
+ .unwrap_or(false)
+ }) {
+ if t.len() >= 3 {
+ let val = t[1].parse::<f64>().ok();
+ let label = t[2].as_str();
+ match label {
+ "dd.lat" => lat = val,
+ "dd.lon" => lng = val,
+ _ => {}
+ }
+ }
+ }
+
+ let location = if primary.is_some()
+ || city.is_some()
+ || region.is_some()
+ || country.is_some()
+ || lat.is_some()
+ || lng.is_some()
+ || geohash.is_some()
+ {
+ Some(RadrootsListingLocation {
+ primary: primary.unwrap_or_default(),
+ city,
+ region,
+ country,
+ lat,
+ lng,
+ geohash,
+ })
+ } else {
+ None
+ };
+
+ let images: Option<Vec<RadrootsListingImage>> = tags
+ .iter()
+ .filter(|t| t.first().map(|k| k == "img").unwrap_or(false))
+ .map(|t| {
+ let url = t.get(1).cloned().unwrap_or_default();
+ let size = if t.len() >= 4 {
+ let w = t[2].parse::<u32>().ok();
+ let h = t[3].parse::<u32>().ok();
+ match (w, h) {
+ (Some(w), Some(h)) => Some(RadrootsListingImageSize { w, h }),
+ _ => None,
+ }
+ } else {
+ None
+ };
+ RadrootsListingImage { url, size }
+ })
+ .collect::<Vec<_>>()
+ .into();
+
+ Ok(RadrootsListing {
+ d_tag,
+ product,
+ quantities,
+ prices,
+ discounts: None,
+ location,
+ images,
+ })
}
fn create_radroots_listing_event_metadata(
@@ -18,10 +214,10 @@ fn create_radroots_listing_event_metadata(
author: String,
published_at: u32,
kind: u32,
- content: String,
- _tags: Vec<Vec<String>>,
+ _content: String,
+ tags: Vec<Vec<String>>,
) -> Result<RadrootsListingEventMetadata, RadrootsListingEventIndexError> {
- let listing: RadrootsListing = serde_json::from_str(&content)?;
+ let listing = parse_listing_from_tags(&tags)?;
Ok(RadrootsListingEventMetadata {
id,
author,
diff --git a/package.json b/package.json
@@ -5,7 +5,8 @@
"scripts": {
"build": "turbo build",
"build:app": "turbo build --filter=app --filter=@radroots/*",
- "dev": "turbo dev --filter=@radroots/*"
+ "dev:lib": "turbo dev --filter=@radroots/* --concurrency 16",
+ "dev:app": "cd app && yarn dev"
},
"devDependencies": {
"turbo": "2.5.3",