// Personlig arbeidsflate — oppgave 19.6 // // GET /my/workspace — finn eller opprett brukerens personlige workspace-node. // // Hvert bruker-node har én workspace-node (node_kind='workspace') koblet via // en owner-edge. Ved første kall opprettes noden automatisk. // // Ref: docs/retninger/arbeidsflaten.md § "Tre lag" use axum::extract::State; use axum::http::StatusCode; use axum::Json; use serde::Serialize; use uuid::Uuid; use crate::auth::AuthUser; use crate::AppState; #[derive(Serialize)] pub struct WorkspaceResponse { pub node_id: Uuid, pub title: String, pub metadata: serde_json::Value, pub created: bool, } #[derive(sqlx::FromRow)] struct WorkspaceRow { id: Uuid, title: String, metadata: String, } /// GET /my/workspace — finn eller opprett brukerens personlige workspace. pub async fn my_workspace( State(state): State, user: AuthUser, ) -> Result, (StatusCode, String)> { // Søk etter eksisterende workspace-node der brukeren er eier let existing = sqlx::query_as::<_, WorkspaceRow>( r#" SELECT n.id, n.title, n.metadata FROM nodes n INNER JOIN edges e ON e.target_id = n.id WHERE e.source_id = $1 AND e.edge_type = 'owner' AND n.node_kind = 'workspace' LIMIT 1 "#, ) .bind(user.node_id) .fetch_optional(&state.db) .await .map_err(|e| { tracing::error!("PG-feil ved workspace-oppslag: {e}"); (StatusCode::INTERNAL_SERVER_ERROR, "Databasefeil".to_string()) })?; if let Some(row) = existing { let metadata: serde_json::Value = serde_json::from_str(&row.metadata).unwrap_or(serde_json::json!({})); return Ok(Json(WorkspaceResponse { node_id: row.id, title: row.title, metadata, created: false, })); } // Ingen workspace funnet — opprett ny let node_id = Uuid::now_v7(); let title = "Min arbeidsflate".to_string(); let metadata = serde_json::json!({ "workspace_layout": { "panels": [] } }); let metadata_str = metadata.to_string(); let node_id_str = node_id.to_string(); let created_by_str = user.node_id.to_string(); // Skriv til SpacetimeDB (instant) state .stdb .create_node( &node_id_str, "workspace", &title, "", "hidden", &metadata_str, &created_by_str, ) .await .map_err(|e| { tracing::error!("STDB-feil ved workspace-opprettelse: {e}"); ( StatusCode::INTERNAL_SERVER_ERROR, "SpacetimeDB-feil".to_string(), ) })?; // Async PG-skriving (for persistens) let db = state.db.clone(); let title_clone = title.clone(); let metadata_str_clone = metadata_str.clone(); tokio::spawn(async move { if let Err(e) = sqlx::query( r#" INSERT INTO nodes (id, node_kind, title, content, visibility, metadata, created_by) VALUES ($1, 'workspace', $2, '', 'hidden', $3, $4) ON CONFLICT (id) DO NOTHING "#, ) .bind(node_id) .bind(&title_clone) .bind(&metadata_str_clone) .bind(user.node_id) .execute(&db) .await { tracing::error!("PG-feil ved workspace-skriving: {e}"); } }); // Opprett owner-edge let edge_id = Uuid::now_v7(); let edge_id_str = edge_id.to_string(); state .stdb .create_edge( &edge_id_str, &created_by_str, &node_id_str, "owner", "{}", false, &created_by_str, ) .await .map_err(|e| { tracing::error!("STDB-feil ved workspace owner-edge: {e}"); ( StatusCode::INTERNAL_SERVER_ERROR, "SpacetimeDB-feil".to_string(), ) })?; // Async PG edge-skriving let db2 = state.db.clone(); tokio::spawn(async move { if let Err(e) = sqlx::query( r#" INSERT INTO edges (id, source_id, target_id, edge_type, metadata, system, created_by) VALUES ($1, $2, $3, 'owner', '{}', false, $2) ON CONFLICT (id) DO NOTHING "#, ) .bind(edge_id) .bind(user.node_id) .bind(node_id) .execute(&db2) .await { tracing::error!("PG-feil ved workspace owner-edge skriving: {e}"); } }); tracing::info!( node_id = %node_id, user_id = %user.node_id, "Personlig workspace opprettet" ); Ok(Json(WorkspaceResponse { node_id, title, metadata, created: true, })) }