Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510//! 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::<Vec<_>>(), "createdAt": SAMPLE_TIME });
let thing_record_json = typed_json::<Thing>(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::<BlobRef>(blob("bafkreiariavatar", "image/jpeg", 2048))?; let profile_record = typed_json::<Profile>(json!({ "displayName": "Ari Chen", "description": "Maker of practical parametric fixtures and electronics housings.", "avatar": serde_json::from_str::<Value>(&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::<Model>(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::<Vec<_>>(), "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>(file.clone())?; let record = typed_json::<Part>(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<SamplePart> { [ ("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::<Thing>(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<T>(value: Value) -> anyhow::Result<String>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"); }}