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:
parent
db98935182
commit
7189925d08
3 changed files with 191 additions and 2 deletions
|
|
@ -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
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
3
tasks.md
3
tasks.md
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue