Fullfør oppgave 5.1: create_communication-intensjon

Ny intensjon POST /intentions/create_communication som oppretter en
kommunikasjonsnode (node_kind='communication') med:
- metadata.started_at satt til opprettelsestidspunkt
- owner-edge fra innlogget bruker til noden
- member_of-edges for alle angitte deltakere
- Validering av deltaker-noder og visibility
- Samme to-lags skriveflyt som andre intensjoner (STDB instant, PG async)

Testet med curl mot produksjonsserver — node, edges og node_access
opprettes korrekt i både STDB og PG.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
vegard 2026-03-17 15:44:02 +01:00
parent db98935182
commit 7189925d08
3 changed files with 191 additions and 2 deletions

View file

@ -503,6 +503,195 @@ pub async fn delete_node(
Ok(Json(DeleteNodeResponse { deleted: true })) Ok(Json(DeleteNodeResponse { deleted: true }))
} }
// =============================================================================
// create_communication
// =============================================================================
#[derive(Deserialize)]
pub struct CreateCommunicationRequest {
/// Visningstittel for kommunikasjonsnoden (f.eks. "Redaksjonsmøte").
pub title: Option<String>,
/// Deltakere — liste med node_id-er (person-noder).
/// Innlogget bruker legges automatisk til som owner.
pub participants: Vec<Uuid>,
/// Synlighet. Default: "hidden" (privat).
pub visibility: Option<String>,
}
#[derive(Serialize)]
pub struct CreateCommunicationResponse {
pub node_id: Uuid,
/// Edge-IDer for opprettede deltaker-edges (owner + member_of).
pub edge_ids: Vec<Uuid>,
}
/// POST /intentions/create_communication
///
/// Oppretter en kommunikasjonsnode med deltaker-edges.
/// Innlogget bruker blir automatisk owner. Andre deltakere får member_of-edge.
/// Metadata inneholder started_at-tidsstempel.
///
/// Ref: docs/primitiver/nodes.md (communication), docs/retninger/universell_input.md
pub async fn create_communication(
State(state): State<AppState>,
user: AuthUser,
Json(req): Json<CreateCommunicationRequest>,
) -> Result<Json<CreateCommunicationResponse>, (StatusCode, Json<ErrorResponse>)> {
let visibility = req.visibility.unwrap_or_else(|| "hidden".to_string());
if !VALID_VISIBILITIES.contains(&visibility.as_str()) {
return Err(bad_request(&format!(
"Ugyldig visibility: '{visibility}'. Gyldige verdier: {VALID_VISIBILITIES:?}"
)));
}
// Valider at alle deltakere eksisterer
for participant_id in &req.participants {
let exists = node_exists(&state.db, *participant_id)
.await
.map_err(|e| {
tracing::error!("PG-feil ved nodesjekk: {e}");
internal_error("Databasefeil ved validering")
})?;
if !exists {
return Err(bad_request(&format!(
"Deltaker-node {} finnes ikke",
participant_id
)));
}
}
let title = req.title.unwrap_or_default();
let now = chrono::Utc::now();
let metadata = serde_json::json!({ "started_at": now.to_rfc3339() });
let metadata_str = metadata.to_string();
// -- Opprett kommunikasjonsnoden --
let node_id = Uuid::now_v7();
let node_id_str = node_id.to_string();
let created_by_str = user.node_id.to_string();
state
.stdb
.create_node(
&node_id_str,
"communication",
&title,
"",
&visibility,
&metadata_str,
&created_by_str,
)
.await
.map_err(|e| stdb_error("create_node (communication)", e))?;
tracing::info!(
node_id = %node_id,
created_by = %user.node_id,
participants = ?req.participants,
"Kommunikasjonsnode opprettet i STDB"
);
// Spawn PG-skriving for noden
spawn_pg_insert_node(
state.db.clone(),
node_id,
"communication".to_string(),
title,
String::new(),
visibility,
metadata,
user.node_id,
);
// -- Opprett deltaker-edges --
let mut edge_ids = Vec::new();
// Owner-edge for innlogget bruker
let owner_edge_id = Uuid::now_v7();
edge_ids.push(owner_edge_id);
let owner_edge_id_str = owner_edge_id.to_string();
let owner_metadata = serde_json::json!({});
let owner_metadata_str = owner_metadata.to_string();
state
.stdb
.create_edge(
&owner_edge_id_str,
&created_by_str,
&node_id_str,
"owner",
&owner_metadata_str,
false,
&created_by_str,
)
.await
.map_err(|e| stdb_error("create_edge (owner)", e))?;
// Spawn PG-skriving for owner-edge (med access recompute)
spawn_pg_insert_edge(
state.db.clone(),
state.stdb.clone(),
owner_edge_id,
user.node_id,
node_id,
"owner".to_string(),
owner_metadata,
false,
user.node_id,
);
// member_of-edges for øvrige deltakere
for participant_id in &req.participants {
// Hopp over innlogget bruker — allerede owner
if *participant_id == user.node_id {
continue;
}
let edge_id = Uuid::now_v7();
edge_ids.push(edge_id);
let edge_id_str = edge_id.to_string();
let participant_id_str = participant_id.to_string();
let member_metadata = serde_json::json!({});
let member_metadata_str = member_metadata.to_string();
state
.stdb
.create_edge(
&edge_id_str,
&participant_id_str,
&node_id_str,
"member_of",
&member_metadata_str,
false,
&created_by_str,
)
.await
.map_err(|e| stdb_error("create_edge (member_of)", e))?;
spawn_pg_insert_edge(
state.db.clone(),
state.stdb.clone(),
edge_id,
*participant_id,
node_id,
"member_of".to_string(),
member_metadata,
false,
user.node_id,
);
}
tracing::info!(
node_id = %node_id,
edge_count = edge_ids.len(),
"Kommunikasjonsnode med deltaker-edges opprettet"
);
Ok(Json(CreateCommunicationResponse { node_id, edge_ids }))
}
// ============================================================================= // =============================================================================
// Bakgrunns-PG-operasjoner // Bakgrunns-PG-operasjoner
// ============================================================================= // =============================================================================

View file

@ -118,6 +118,7 @@ async fn main() {
.route("/intentions/create_edge", post(intentions::create_edge)) .route("/intentions/create_edge", post(intentions::create_edge))
.route("/intentions/update_node", post(intentions::update_node)) .route("/intentions/update_node", post(intentions::update_node))
.route("/intentions/delete_node", post(intentions::delete_node)) .route("/intentions/delete_node", post(intentions::delete_node))
.route("/intentions/create_communication", post(intentions::create_communication))
.route("/query/nodes", get(queries::query_nodes)) .route("/query/nodes", get(queries::query_nodes))
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())
.with_state(state); .with_state(state);

View file

@ -78,8 +78,7 @@ Uavhengige faser kan fortsatt plukkes.
## Fase 5: Kommunikasjonsnoder ## Fase 5: Kommunikasjonsnoder
- [~] 5.1 Opprett kommunikasjonsnode: intensjon `create_communication` → node med `node_kind='communication'`, deltaker-edges, metadata (started_at). - [x] 5.1 Opprett kommunikasjonsnode: intensjon `create_communication` → node med `node_kind='communication'`, deltaker-edges, metadata (started_at).
> Påbegynt: 2026-03-17T15:35
- [ ] 5.2 Kontekst-arv: input i kommunikasjonsnode → automatisk `belongs_to`-edge. - [ ] 5.2 Kontekst-arv: input i kommunikasjonsnode → automatisk `belongs_to`-edge.
- [ ] 5.3 Chat-visning i frontend: noder med `belongs_to`-edge til kommunikasjonsnode, sortert på tid, sanntid via STDB. - [ ] 5.3 Chat-visning i frontend: noder med `belongs_to`-edge til kommunikasjonsnode, sortert på tid, sanntid via STDB.
- [ ] 5.4 Én-til-én chat: opprett kommunikasjonsnode med to deltakere. Full loop: skriv melding → vis i sanntid hos begge. - [ ] 5.4 Én-til-én chat: opprett kommunikasjonsnode med to deltakere. Full loop: skriv melding → vis i sanntid hos begge.