//! Same-origin client calls for the publish flow. //! //! Thin typed wrappers over [`PolymodelClient`]'s raw app-request helpers for //! the draft store, raw-byte file staging, and image upload. Every call rides //! the browser cookie session (same-origin), so these are authenticated without //! any explicit token handling. //! //! [`resolve_strong_ref`] is the exception: an unauthenticated, identity-resolving //! `get_record` that reads a thing record directly from the target repo's own //! PDS, so a cross-repo "Works with" link resolves. use http::Method; use jacquard::client::{AgentSessionExt, BasicClient}; use jacquard::common::types::string::AtUri; use polymodel_api::com_atproto::repo::strong_ref::StrongRef; use polymodel_api::space_polymodel::library::Image; use polymodel_api::space_polymodel::library::publish_thing::PublishThingOutput; use polymodel_api::space_polymodel::library::stage_file::StageFileOutput; use polymodel_api::space_polymodel::library::thing::Thing; use crate::client::{PolymodelClient, RawError}; use crate::publish::draft::{ DeleteDraftResponse, DraftIdResponse, DraftSummary, DraftThingInput, ImageUploadResponse, }; /// A resolved compatibility target: the `StrongRef` to persist plus the thing's /// display name, shown on the chip. pub struct ResolvedRef { pub strong_ref: StrongRef, pub name: Option, } /// Resolve a thing `AtUri` to a current `StrongRef` (uri + cid) by reading the /// record directly from the target repo's own PDS. /// /// Uses an unauthenticated, identity-resolving jacquard agent: `get_record` /// resolves the URI's authority (DID or handle) to its PDS and reads from there, /// so a thing in *another* maker's repo resolves (unlike the same-origin proxy, /// which is pinned to the signed-in user's PDS). A record with no `cid` is an /// error — a `StrongRef` requires one. pub async fn resolve_strong_ref(uri: &AtUri) -> Result { let agent = BasicClient::unauthenticated(); let response = agent .get_record::(uri) .await .map_err(|error| RawError::Transport(error.to_string()))?; // `into_output()` fails for typed XRPC errors — a `RecordNotFound` for a // missing record, or a decode/validation error otherwise. Both are surfaced // as a decode failure carrying the real cause rather than a fake HTTP status. let output = response .into_output() .map_err(|error| RawError::Decode(error.to_string()))?; let cid = output .cid .ok_or_else(|| RawError::Decode("record has no cid".to_string()))?; let name = { let n = output.value.name.as_str().trim(); (!n.is_empty()).then(|| n.to_string()) }; Ok(ResolvedRef { strong_ref: StrongRef { cid, uri: output.uri, extra_data: None, }, name, }) } /// stageFile XRPC path (raw-body upload), reused from the existing write path. const STAGE_FILE_PATH: &str = "/xrpc/space.polymodel.library.stageFile"; const DRAFTS_PATH: &str = "/app/drafts"; const IMAGES_PATH: &str = "/app/images"; #[cfg(all(target_family = "wasm", target_os = "unknown"))] const STAGED_PATH: &str = "/app/staged"; /// Create a new server-side draft; returns the server-assigned opaque id. pub async fn create_draft( client: &PolymodelClient, draft: &DraftThingInput, ) -> Result { let response: DraftIdResponse = client .app_json(Method::POST, DRAFTS_PATH, Some(draft)) .await?; Ok(response.draft_id) } /// Upsert an existing draft by id (autosave). pub async fn put_draft( client: &PolymodelClient, draft_id: &str, draft: &DraftThingInput, ) -> Result<(), RawError> { let _: DraftIdResponse = client .app_json(Method::PUT, &draft_path(draft_id), Some(draft)) .await?; Ok(()) } /// Fetch a single draft payload (resume). pub async fn get_draft( client: &PolymodelClient, draft_id: &str, ) -> Result { client .app_json::<(), _>(Method::GET, &draft_path(draft_id), None) .await } /// List the signed-in actor's draft summaries. pub async fn list_drafts(client: &PolymodelClient) -> Result, RawError> { client .app_json::<(), _>(Method::GET, DRAFTS_PATH, None) .await } /// Delete a draft; returns whether a row existed. pub async fn delete_draft(client: &PolymodelClient, draft_id: &str) -> Result { let response: DeleteDraftResponse = client .app_json::<(), _>(Method::DELETE, &draft_path(draft_id), None) .await?; Ok(response.deleted) } /// Assemble + publish a draft to the actor's PDS; the server deletes the draft /// only on success. pub async fn publish_draft( client: &PolymodelClient, draft_id: &str, ) -> Result { let path = format!("{DRAFTS_PATH}/{draft_id}/publish"); client.app_json::<(), _>(Method::POST, &path, None).await } /// Stage a geometry file: raw bytes → blobs on the actor's PDS + reassembly /// metadata. Returns the `uploadId`/`file` the part references. pub async fn stage_file( client: &PolymodelClient, filename: &str, content_type: &str, bytes: Vec, ) -> Result { client .app_post_bytes( STAGE_FILE_PATH, content_type, &[("x-polymodel-filename", filename.to_string())], bytes, ) .await } #[cfg(all(target_family = "wasm", target_os = "unknown"))] pub async fn get_staged_resource( client: &PolymodelClient, upload_id: &str, ) -> Result, RawError> { let path = format!("{STAGED_PATH}/{}", urlencoding::encode(upload_id)); client.app_bytes::<()>(Method::GET, &path, None).await } /// Upload an image and receive a fully-formed `#image` (blobref + server-decoded /// aspect ratio + the given alt text). pub async fn upload_image( client: &PolymodelClient, alt: &str, content_type: &str, bytes: Vec, ) -> Result { let response: ImageUploadResponse = client .app_post_bytes( IMAGES_PATH, content_type, &[("x-polymodel-alt", alt.to_string())], bytes, ) .await?; Ok(response.image) } fn draft_path(draft_id: &str) -> String { format!("{DRAFTS_PATH}/{draft_id}") }