// Synops SpacetimeDB-modul // // Speiler PG-skjema (nodes + edges) som sanntids-cache. // PG er autoritativ; SpacetimeDB er varm cache for frontend. // Maskinrommet gjør warmup (PG → ST) ved oppstart og toveissynk deretter. // // Ref: docs/retninger/datalaget.md, docs/primitiver/nodes.md, docs/primitiver/edges.md use spacetimedb::{reducer, Table, ReducerContext, Timestamp}; // ============================================================================= // Tabeller // ============================================================================= /// Speiler PG nodes-tabellen. UUID-er representeres som String. /// visibility og node_kind er strenger (PG-enums har ikke STDB-ekvivalent). /// metadata er JSON-streng (PG JSONB). #[spacetimedb::table(accessor = node, public)] pub struct Node { #[primary_key] pub id: String, pub node_kind: String, pub title: String, pub content: String, pub visibility: String, pub metadata: String, pub created_at: Timestamp, pub created_by: String, } /// Speiler PG edges-tabellen. #[spacetimedb::table(accessor = edge, public)] pub struct Edge { #[primary_key] pub id: String, #[index(btree)] pub source_id: String, #[index(btree)] pub target_id: String, pub edge_type: String, pub metadata: String, pub system: bool, pub created_at: Timestamp, pub created_by: String, } // ============================================================================= // Livssyklus // ============================================================================= #[reducer(init)] pub fn init(_ctx: &ReducerContext) { log::info!("Synops-modul initialisert"); } #[reducer(client_connected)] pub fn client_connected(ctx: &ReducerContext) { log::info!("Klient tilkoblet: {}", ctx.sender().to_hex()); } #[reducer(client_disconnected)] pub fn client_disconnected(ctx: &ReducerContext) { log::info!("Klient frakoblet: {}", ctx.sender().to_hex()); } // ============================================================================= // Node CRUD // ============================================================================= #[reducer] pub fn create_node( ctx: &ReducerContext, id: String, node_kind: String, title: String, content: String, visibility: String, metadata: String, created_by: String, ) -> Result<(), String> { if id.is_empty() { return Err("id kan ikke være tom".into()); } if ctx.db.node().id().find(&id).is_some() { return Err(format!("Node med id {} finnes allerede", id)); } ctx.db.node().insert(Node { id, node_kind, title, content, visibility, metadata, created_at: ctx.timestamp, created_by, }); Ok(()) } #[reducer] pub fn update_node( ctx: &ReducerContext, id: String, node_kind: String, title: String, content: String, visibility: String, metadata: String, ) -> Result<(), String> { let existing = ctx.db.node().id().find(&id) .ok_or_else(|| format!("Node {} ikke funnet", id))?; ctx.db.node().id().update(Node { node_kind, title, content, visibility, metadata, ..existing }); Ok(()) } #[reducer] pub fn delete_node(ctx: &ReducerContext, id: String) -> Result<(), String> { // Slett tilhørende edges først (speiler PG ON DELETE CASCADE) let source_edges: Vec<_> = ctx.db.edge().source_id().filter(&id).collect(); for e in source_edges { ctx.db.edge().id().delete(&e.id); } let target_edges: Vec<_> = ctx.db.edge().target_id().filter(&id).collect(); for e in target_edges { ctx.db.edge().id().delete(&e.id); } ctx.db.node().id().delete(&id); Ok(()) } // ============================================================================= // Edge CRUD // ============================================================================= #[reducer] pub fn create_edge( ctx: &ReducerContext, id: String, source_id: String, target_id: String, edge_type: String, metadata: String, system: bool, created_by: String, ) -> Result<(), String> { if id.is_empty() { return Err("id kan ikke være tom".into()); } if ctx.db.edge().id().find(&id).is_some() { return Err(format!("Edge med id {} finnes allerede", id)); } ctx.db.edge().insert(Edge { id, source_id, target_id, edge_type, metadata, system, created_at: ctx.timestamp, created_by, }); Ok(()) } #[reducer] pub fn update_edge( ctx: &ReducerContext, id: String, edge_type: String, metadata: String, ) -> Result<(), String> { let existing = ctx.db.edge().id().find(&id) .ok_or_else(|| format!("Edge {} ikke funnet", id))?; ctx.db.edge().id().update(Edge { edge_type, metadata, ..existing }); Ok(()) } #[reducer] pub fn delete_edge(ctx: &ReducerContext, id: String) -> Result<(), String> { ctx.db.edge().id().delete(&id); Ok(()) } // ============================================================================= // Warmup/vedlikehold // ============================================================================= /// Tøm alle noder og edges (brukes ved restart/warmup for å unngå duplikater) #[reducer] pub fn clear_all(ctx: &ReducerContext) -> Result<(), String> { let all_edges: Vec<_> = ctx.db.edge().iter().collect(); for e in all_edges { ctx.db.edge().id().delete(&e.id); } let all_nodes: Vec<_> = ctx.db.node().iter().collect(); for n in all_nodes { ctx.db.node().id().delete(&n.id); } log::info!("Alle noder og edges slettet (clear_all)"); Ok(()) }