// SpacetimeDB HTTP-klient for maskinrommet. // // Kaller STDB-reducere via HTTP JSON API. // Maskinrommet eier all skriving — denne klienten er eneste vei inn. // // API-format: POST /v1/database/{db}/call/{reducer} // Body: JSON-objekt med navngitte parametre. // Auth: Bearer-token fra STDB-identitet. // // Ref: docs/retninger/datalaget.md, docs/infra/synkronisering.md use reqwest::Client; use serde::Serialize; /// SpacetimeDB-klient som kaller reducere via HTTP. #[derive(Clone)] pub struct StdbClient { client: Client, base_url: String, database: String, token: String, } impl StdbClient { /// Opprett ny klient. `base_url` er STDB-serverens URL (f.eks. "http://spacetimedb:3000"). pub fn new(base_url: &str, database: &str, token: &str) -> Self { Self { client: Client::new(), base_url: base_url.trim_end_matches('/').to_string(), database: database.to_string(), token: token.to_string(), } } /// Hent en ny identitet og token fra STDB-serveren. /// Brukes ved oppstart hvis ingen token er konfigurert. pub async fn create_identity(base_url: &str) -> Result<(String, String), StdbError> { let client = Client::new(); let url = format!("{}/v1/identity", base_url.trim_end_matches('/')); let resp = client.post(&url).send().await?; if !resp.status().is_success() { return Err(StdbError::Http(format!( "Kunne ikke opprette identitet: {}", resp.status() ))); } let body: serde_json::Value = resp.json().await?; let identity = body["identity"] .as_str() .ok_or_else(|| StdbError::Http("Mangler identity i respons".into()))? .to_string(); let token = body["token"] .as_str() .ok_or_else(|| StdbError::Http("Mangler token i respons".into()))? .to_string(); Ok((identity, token)) } /// Kall en reducer med navngitte parametre (JSON-objekt). async fn call_reducer(&self, reducer: &str, args: &T) -> Result<(), StdbError> { let url = format!( "{}/v1/database/{}/call/{}", self.base_url, self.database, reducer ); let resp = self .client .post(&url) .bearer_auth(&self.token) .json(args) .send() .await?; if resp.status().is_success() { Ok(()) } else { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); Err(StdbError::Reducer { reducer: reducer.to_string(), status: status.as_u16(), message: body, }) } } // ========================================================================= // Node-operasjoner // ========================================================================= pub async fn create_node( &self, id: &str, node_kind: &str, title: &str, content: &str, visibility: &str, metadata: &str, created_by: &str, ) -> Result<(), StdbError> { #[derive(Serialize)] struct Args<'a> { id: &'a str, node_kind: &'a str, title: &'a str, content: &'a str, visibility: &'a str, metadata: &'a str, created_by: &'a str, } self.call_reducer( "create_node", &Args { id, node_kind, title, content, visibility, metadata, created_by, }, ) .await } pub async fn update_node( &self, id: &str, node_kind: &str, title: &str, content: &str, visibility: &str, metadata: &str, ) -> Result<(), StdbError> { #[derive(Serialize)] struct Args<'a> { id: &'a str, node_kind: &'a str, title: &'a str, content: &'a str, visibility: &'a str, metadata: &'a str, } self.call_reducer( "update_node", &Args { id, node_kind, title, content, visibility, metadata, }, ) .await } pub async fn delete_node(&self, id: &str) -> Result<(), StdbError> { #[derive(Serialize)] struct Args<'a> { id: &'a str, } self.call_reducer("delete_node", &Args { id }).await } // ========================================================================= // Edge-operasjoner // ========================================================================= pub async fn create_edge( &self, id: &str, source_id: &str, target_id: &str, edge_type: &str, metadata: &str, system: bool, created_by: &str, ) -> Result<(), StdbError> { #[derive(Serialize)] struct Args<'a> { id: &'a str, source_id: &'a str, target_id: &'a str, edge_type: &'a str, metadata: &'a str, system: bool, created_by: &'a str, } self.call_reducer( "create_edge", &Args { id, source_id, target_id, edge_type, metadata, system, created_by, }, ) .await } pub async fn update_edge( &self, id: &str, edge_type: &str, metadata: &str, ) -> Result<(), StdbError> { #[derive(Serialize)] struct Args<'a> { id: &'a str, edge_type: &'a str, metadata: &'a str, } self.call_reducer("update_edge", &Args { id, edge_type, metadata }) .await } pub async fn delete_edge(&self, id: &str) -> Result<(), StdbError> { #[derive(Serialize)] struct Args<'a> { id: &'a str, } self.call_reducer("delete_edge", &Args { id }).await } // ========================================================================= // NodeAccess-operasjoner // ========================================================================= pub async fn upsert_node_access( &self, subject_id: &str, object_id: &str, access: &str, via_edge: &str, ) -> Result<(), StdbError> { #[derive(Serialize)] struct Args<'a> { subject_id: &'a str, object_id: &'a str, access: &'a str, via_edge: &'a str, } self.call_reducer( "upsert_node_access", &Args { subject_id, object_id, access, via_edge, }, ) .await } pub async fn delete_node_access( &self, subject_id: &str, object_id: &str, ) -> Result<(), StdbError> { #[derive(Serialize)] struct Args<'a> { subject_id: &'a str, object_id: &'a str, } self.call_reducer("delete_node_access", &Args { subject_id, object_id }) .await } // ========================================================================= // Live-rom (LiveKit) // ========================================================================= pub async fn create_live_room( &self, room_id: &str, communication_id: &str, ) -> Result<(), StdbError> { #[derive(Serialize)] struct Args<'a> { room_id: &'a str, communication_id: &'a str, } self.call_reducer("create_live_room", &Args { room_id, communication_id }) .await } pub async fn add_room_participant( &self, room_id: &str, user_id: &str, display_name: &str, role: &str, ) -> Result<(), StdbError> { #[derive(Serialize)] struct Args<'a> { room_id: &'a str, user_id: &'a str, display_name: &'a str, role: &'a str, } self.call_reducer( "add_room_participant", &Args { room_id, user_id, display_name, role }, ) .await } pub async fn remove_room_participant( &self, room_id: &str, user_id: &str, ) -> Result<(), StdbError> { #[derive(Serialize)] struct Args<'a> { room_id: &'a str, user_id: &'a str, } self.call_reducer("remove_room_participant", &Args { room_id, user_id }) .await } pub async fn close_live_room(&self, room_id: &str) -> Result<(), StdbError> { #[derive(Serialize)] struct Args<'a> { room_id: &'a str, } self.call_reducer("close_live_room", &Args { room_id }).await } // ========================================================================= // Placement-operasjoner (message_placements) // ========================================================================= /// Plasser en melding i en kontekst. Idempotent (upsert). pub async fn place_message( &self, id: &str, message_id: &str, context_type: &str, context_id: &str, position_json: &str, ) -> Result<(), StdbError> { #[derive(Serialize)] struct Args<'a> { id: &'a str, message_id: &'a str, context_type: &'a str, context_id: &'a str, position_json: &'a str, } self.call_reducer( "place_message", &Args { id, message_id, context_type, context_id, position_json }, ) .await } /// Fjern en meldings plassering fra en kontekst. pub async fn remove_placement( &self, message_id: &str, context_type: &str, context_id: &str, ) -> Result<(), StdbError> { #[derive(Serialize)] struct Args<'a> { message_id: &'a str, context_type: &'a str, context_id: &'a str, } self.call_reducer( "remove_placement", &Args { message_id, context_type, context_id }, ) .await } /// Flytt en plassering (oppdater posisjon). pub async fn move_on_canvas( &self, placement_id: &str, new_position_json: &str, ) -> Result<(), StdbError> { #[derive(Serialize)] struct Args<'a> { placement_id: &'a str, new_position_json: &'a str, } self.call_reducer( "move_on_canvas", &Args { placement_id, new_position_json }, ) .await } // ========================================================================= // Vedlikehold // ========================================================================= /// Tøm alle noder og edges. Brukes ved warmup for å unngå duplikater. pub async fn clear_all(&self) -> Result<(), StdbError> { #[derive(Serialize)] struct Empty {} self.call_reducer("clear_all", &Empty {}).await } } // ============================================================================= // Feilhåndtering // ============================================================================= #[derive(Debug)] pub enum StdbError { /// HTTP-transportfeil (nettverk, timeout) Http(String), /// Reducer returnerte feil (400, 500, etc.) Reducer { reducer: String, status: u16, message: String, }, } impl std::fmt::Display for StdbError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { StdbError::Http(msg) => write!(f, "STDB HTTP-feil: {msg}"), StdbError::Reducer { reducer, status, message, } => write!(f, "STDB reducer {reducer} feilet ({status}): {message}"), } } } impl std::error::Error for StdbError {} impl From for StdbError { fn from(e: reqwest::Error) -> Self { StdbError::Http(e.to_string()) } }