//! Deterministic local/demo sample data for appview read paths. //! //! This module writes projection-shaped SQLite rows only when explicitly enabled //! and the local/demo database has no Polymodel things. It lets e2e and local //! demo runs exercise the same `getFeed`, `getThing`, and `getModel` appview //! contracts as production reads without UI-only fixtures. use jacquard_common::types::blob::BlobRef; use polymodel_api::space_polymodel::{ actor::profile::Profile, library::{File, model::Model, part::Part, thing::Thing}, }; use serde::{Serialize, de::DeserializeOwned}; use serde_json::{Value, json}; use sqlx::{SqliteConnection, SqlitePool}; pub const SAMPLE_DID: &str = "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa"; pub const SAMPLE_HANDLE: &str = "ari.model.tools"; pub const SAMPLE_THING_RKEY: &str = "parametric-enclosure"; pub const SAMPLE_THING_URI: &str = "at://did:plc:aaaaaaaaaaaaaaaaaaaaaaaa/space.polymodel.library.thing/parametric-enclosure"; const SAMPLE_TIME: &str = "2026-06-21T12:00:00.000Z"; const SAMPLE_TIME_MILLIS: i64 = 1_750_507_200_000; // Additional demo makers so the discovery home shows a populated feed with // mixed media states: one thing with a cover, two cover-less things that // render the missing-media blueprint placeholder. const MIRA_DID: &str = "did:plc:bbbbbbbbbbbbbbbbbbbbbbbb"; const MIRA_HANDLE: &str = "mira.tools"; const FOUNDRY_DID: &str = "did:plc:cccccccccccccccccccccccc"; const FOUNDRY_HANDLE: &str = "foundrylab.tools"; pub async fn seed_if_empty(pool: &SqlitePool) -> anyhow::Result<()> { let count: i64 = sqlx::query_scalar!("SELECT COUNT(*) FROM things") .fetch_one(pool) .await?; if count > 0 { tracing::debug!( count, "sample data skipped because things table is populated" ); return Ok(()); } seed(pool).await?; tracing::info!( thing_uri = SAMPLE_THING_URI, "seeded local/demo Polymodel sample data" ); Ok(()) } async fn seed(pool: &SqlitePool) -> anyhow::Result<()> { let mut tx = pool.begin().await?; insert_identity(&mut tx, SAMPLE_DID, SAMPLE_HANDLE).await?; insert_profile(&mut tx).await?; let model_uris = [ model_uri("enclosure-v1"), model_uri("panel-variant"), model_uri("ruggedized-lid"), ]; let parts = sample_parts(); for part in &parts { insert_part(&mut tx, part).await?; } insert_model( &mut tx, "enclosure-v1", "Snap-fit electronics enclosure", "Main printable enclosure with lid, body, buttons, and mounting hardware.", &parts[0..6], ) .await?; insert_model( &mut tx, "panel-variant", "Panel-mount variant", "Alternate front panel for flush mounting sensors and displays.", &parts[6..12], ) .await?; insert_model( &mut tx, "ruggedized-lid", "Ruggedized outdoor lid", "Weather-resistant lid and gasket parts for workshop deployments.", &parts[12..18], ) .await?; let instructions = vec![ "Print the body and lid in PETG or PLA+ with 0.2 mm layers.".to_string(), "Use the model selector to choose the standard, panel-mount, or ruggedized variant." .to_string(), "Press brass inserts into the corner bosses before final assembly.".to_string(), ]; let tags = vec!["enclosure", "parametric", "print-in-place"]; let thing_record = json!({ "name": "Parametric enclosure kit", "summary": "A configurable electronics enclosure kit with three printable model variants and detailed part files.", "instructions": instructions, "license": "CC-BY-4.0", "tags": tags, "cover": [image("bafkreiathingcover", "Rendered preview of a parametric electronics enclosure")], "previews": [image("bafkreiathingpreview", "Exploded blueprint preview of the enclosure kit")], "models": model_uris.iter().map(|uri| strong_ref(uri)).collect::>(), "createdAt": SAMPLE_TIME }); let thing_record_json = typed_json::(thing_record.clone())?; sqlx::query!( r#"INSERT INTO things (did, rkey, uri, cid, name, summary, license, tags_json, tags_text, instructions_text, cover_json, derived_from_uri, created_at, indexed_at, record_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)"#, SAMPLE_DID, SAMPLE_THING_RKEY, SAMPLE_THING_URI, "bafyreipolymodelsamplething", "Parametric enclosure kit", "A configurable electronics enclosure kit with three printable model variants and detailed part files.", "CC-BY-4.0", serde_json::to_string(&tags)?, tags.join(" "), instructions.join("\n"), serde_json::to_string(&thing_record["cover"])? , SAMPLE_TIME_MILLIS, SAMPLE_TIME_MILLIS, thing_record_json, ) .execute(&mut *tx) .await?; for (position, model_uri) in model_uris.iter().enumerate() { sqlx::query!( "INSERT INTO thing_models (thing_uri, model_uri, position) VALUES (?, ?, ?)", SAMPLE_THING_URI, model_uri, position as i64, ) .execute(&mut *tx) .await?; } sqlx::query!( "INSERT INTO content_stats (uri, like_count, save_count, tag_count) VALUES (?, 42, 12, 3)", SAMPLE_THING_URI, ) .execute(&mut *tx) .await?; // Two more makers with cover-less things so the home feed is populated and // the missing-media placeholder path renders against real projected rows. insert_identity(&mut tx, MIRA_DID, MIRA_HANDLE).await?; insert_identity(&mut tx, FOUNDRY_DID, FOUNDRY_HANDLE).await?; insert_coverless_thing( &mut tx, MIRA_DID, "field-prototype", "Untitled thing", "A work-in-progress upload with no preview rendered yet.", 0, 0, ) .await?; insert_coverless_thing( &mut tx, FOUNDRY_DID, "modular-calibration-tower", "Modular calibration tower", "A calibration tower for dialing in printer tolerances before a full run.", 8, 5, ) .await?; tx.commit().await?; Ok(()) } async fn insert_identity( conn: &mut SqliteConnection, did: &str, handle: &str, ) -> anyhow::Result<()> { sqlx::query!( "INSERT INTO identities (did, handle, updated_at) VALUES (?, ?, ?)", did, handle, SAMPLE_TIME_MILLIS, ) .execute(&mut *conn) .await?; Ok(()) } async fn insert_profile(conn: &mut SqliteConnection) -> anyhow::Result<()> { let avatar = typed_json::(blob("bafkreiariavatar", "image/jpeg", 2048))?; let profile_record = typed_json::(json!({ "displayName": "Ari Chen", "description": "Maker of practical parametric fixtures and electronics housings.", "avatar": serde_json::from_str::(&avatar)?, "pronouns": "she/her", "defaultLicense": "CC-BY-4.0" }))?; sqlx::query!( r#"INSERT INTO profiles (did, display_name, description, avatar_json, default_license, pronouns, printers_json, links_json, record_json, indexed_at) VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?)"#, SAMPLE_DID, "Ari Chen", "Maker of practical parametric fixtures and electronics housings.", &avatar, "CC-BY-4.0", "she/her", profile_record, SAMPLE_TIME_MILLIS, ) .execute(&mut *conn) .await?; Ok(()) } async fn insert_model( conn: &mut SqliteConnection, rkey: &str, name: &str, summary: &str, parts: &[SamplePart], ) -> anyhow::Result<()> { let uri = model_uri(rkey); let record = typed_json::(json!({ "name": name, "summary": summary, "instructions": ["Review the ordered part list before printing.", "Print fit-critical clips slowly for best tolerances."], "license": "CC-BY-4.0", "tags": ["variant", "enclosure"], "cover": [image("bafkreimodelcover", &format!("Preview of {name}"))], "previews": [image("bafkreimodelpreview", &format!("Blueprint preview of {name}"))], "parts": parts.iter().map(|part| strong_ref(&part_uri(part.rkey))).collect::>(), "createdAt": SAMPLE_TIME }))?; sqlx::query!( r#"INSERT INTO models (did, rkey, uri, cid, name, summary, created_at, indexed_at, record_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"#, SAMPLE_DID, rkey, &uri, format!("bafyreimodel{rkey}"), name, summary, SAMPLE_TIME_MILLIS, SAMPLE_TIME_MILLIS, record, ) .execute(&mut *conn) .await?; for (position, part) in parts.iter().enumerate() { sqlx::query!( "INSERT INTO model_parts (model_uri, part_uri, position) VALUES (?, ?, ?)", &uri, part_uri(part.rkey), position as i64, ) .execute(&mut *conn) .await?; } Ok(()) } async fn insert_part(conn: &mut SqliteConnection, part: &SamplePart) -> anyhow::Result<()> { let file = file_manifest(part.file_size); let file_json = typed_json::(file.clone())?; let record = typed_json::(json!({ "name": part.name, "file": file, "format": "STL", "dimensions": { "x": part.dimensions.0, "y": part.dimensions.1, "z": part.dimensions.2, "unit": "mm" }, "units": "mm", "notes": part.notes, "printSettings": ["0.2 mm layer height", "15% gyroid infill", "No supports required unless noted"], "previews": [image("bafkreipartpreview", &format!("Preview of {}", part.name))], "createdAt": SAMPLE_TIME }))?; sqlx::query!( r#"INSERT INTO parts (did, rkey, uri, cid, name, format, file_json, created_at, indexed_at, record_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#, SAMPLE_DID, part.rkey, part_uri(part.rkey), format!("bafyreipart{}", part.rkey), part.name, "STL", file_json, SAMPLE_TIME_MILLIS, SAMPLE_TIME_MILLIS, record, ) .execute(&mut *conn) .await?; Ok(()) } #[derive(Clone, Copy)] struct SamplePart { rkey: &'static str, name: &'static str, file_size: i64, dimensions: (&'static str, &'static str, &'static str), notes: &'static str, } fn sample_parts() -> Vec { [ ("part-01", "Main enclosure body"), ("part-02", "Snap-fit lid"), ("part-03", "Button cap set"), ("part-04", "USB-C bezel"), ("part-05", "PCB mounting tray"), ("part-06", "Corner screw boss"), ("part-07", "Front sensor panel"), ("part-08", "Display window frame"), ("part-09", "Panel latch pair"), ("part-10", "Flush wall bracket"), ("part-11", "Cable strain relief"), ("part-12", "Label plate"), ("part-13", "Ruggedized lid shell"), ("part-14", "TPU gasket guide"), ("part-15", "Outdoor cable gland"), ("part-16", "Hinged dust cover"), ("part-17", "Drain slot insert"), ("part-18", "Mounting foot set"), ] .into_iter() .enumerate() .map(|(index, (rkey, name))| SamplePart { rkey, name, file_size: 48_000 + ((index + 1) as i64 * 2_048), dimensions: ("86.0", "54.0", "18.5"), notes: "Orient the visible face upward and verify first-layer adhesion around clips.", }) .collect() } async fn insert_coverless_thing( conn: &mut SqliteConnection, did: &str, rkey: &str, name: &str, summary: &str, like_count: i64, save_count: i64, ) -> anyhow::Result<()> { let uri = thing_uri(did, rkey); let record = typed_json::(json!({ "name": name, "summary": summary, "license": "CC-BY-4.0", "createdAt": SAMPLE_TIME }))?; sqlx::query!( r#"INSERT INTO things (did, rkey, uri, cid, name, summary, license, tags_json, tags_text, instructions_text, cover_json, derived_from_uri, created_at, indexed_at, record_json) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, NULL, NULL, ?, ?, ?)"#, did, rkey, &uri, format!("bafyreithing{}", rkey.replace('-', "")), name, summary, "CC-BY-4.0", SAMPLE_TIME_MILLIS, SAMPLE_TIME_MILLIS, record, ) .execute(&mut *conn) .await?; if like_count > 0 || save_count > 0 { sqlx::query!( "INSERT INTO content_stats (uri, like_count, save_count, tag_count) VALUES (?, ?, ?, 0)", &uri, like_count, save_count, ) .execute(&mut *conn) .await?; } Ok(()) } fn model_uri(rkey: &str) -> String { format!("at://{SAMPLE_DID}/space.polymodel.library.model/{rkey}") } fn part_uri(rkey: &str) -> String { format!("at://{SAMPLE_DID}/space.polymodel.library.part/{rkey}") } fn thing_uri(did: &str, rkey: &str) -> String { format!("at://{did}/space.polymodel.library.thing/{rkey}") } fn strong_ref(uri: &str) -> Value { json!({ "uri": uri, "cid": "bafyreicid" }) } fn image(cid: &str, alt: &str) -> Value { json!({ "alt": alt, "aspectRatio": { "width": 4, "height": 3 }, "image": blob(cid, "image/jpeg", 1234) }) } fn file_manifest(size: i64) -> Value { json!({ "mimeType": "model/stl", "size": size, "chunks": [{ "blob": blob("bafkreistlchunk", "model/stl", size), "offset": 0, "size": size }] }) } fn blob(cid: &str, mime_type: &str, size: i64) -> Value { json!({ "$type": "blob", "ref": { "$link": cid }, "mimeType": mime_type, "size": size }) } fn typed_json(value: Value) -> anyhow::Result where T: DeserializeOwned + Serialize, { let typed: T = serde_json::from_value(value)?; Ok(serde_json::to_string(&typed)?) } #[cfg(test)] mod tests { use std::str::FromStr; use std::sync::Arc; use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use super::*; use crate::appview::{state::AppState, views}; async fn test_pool() -> SqlitePool { let options = SqliteConnectOptions::from_str("sqlite::memory:").unwrap(); let pool = SqlitePoolOptions::new() .max_connections(1) .connect_with(options) .await .unwrap(); sqlx::migrate!("./migrations").run(&pool).await.unwrap(); pool } #[tokio::test] async fn seed_populates_feed_thing_and_model_read_paths() { let pool = test_pool().await; seed_if_empty(&pool).await.unwrap(); seed_if_empty(&pool).await.unwrap(); let bootstrap = crate::oauth::bootstrap_oauth(pool.clone(), Some("http://localhost")) .expect("ephemeral OAuth bootstrap for tests"); let state = Arc::new(AppState::new(pool, bootstrap)); let feed = views::feed_recent(&state, 10, None, None).await.unwrap(); // Three seeded things: the model-rich parametric kit plus two cover-less // demo things that exercise the missing-media placeholder path. assert_eq!(feed.items.len(), 3); // feed_recent is rkey DESC, so "parametric-enclosure" sorts first. assert_eq!( feed.items[0].thing.name.as_str(), "Parametric enclosure kit" ); assert_eq!(feed.items[0].thing.model_count, 3); assert_eq!(feed.items[0].thing.part_count, 18); let names: Vec<&str> = feed .items .iter() .map(|item| item.thing.name.as_str()) .collect(); assert!(names.contains(&"Untitled thing")); assert!(names.contains(&"Modular calibration tower")); let (thing, models) = views::get_thing(&state, SAMPLE_THING_URI, None) .await .unwrap(); assert_eq!( thing.author.display_name.as_ref().unwrap().as_str(), "Ari Chen" ); assert_eq!(models.len(), 3); let detail = views::get_model(&state, models[0].uri.as_ref(), None) .await .unwrap(); assert_eq!(detail.parts.len(), 6); assert_eq!(detail.parts[0].name.as_str(), "Main enclosure body"); } }