Kontekst-arv: automatisk belongs_to-edge ved input i kommunikasjonsnode

Legger til context_id-parameter på create_node-intensjonen. Når context_id
er satt (og peker på en kommunikasjonsnode), opprettes automatisk en
belongs_to-edge fra den nye noden til kontekstnoden. Dette er kjernen i
kontekst-arv: si noe i et møte → noden tilhører møtet automatisk.

Backend: Validerer at context_id eksisterer og er communication-node,
oppretter belongs_to-edge i STDB+PG etter node-opprettelse.
Frontend: Oppdaterer API-typer med context_id og belongs_to_edge_id.

Ref: docs/retninger/universell_input.md (kontekst arves automatisk)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
vegard 2026-03-17 15:57:25 +01:00
parent 4ef0b31b3c
commit b55705d12c
2 changed files with 95 additions and 1 deletions

View file

@ -12,10 +12,14 @@ export interface CreateNodeRequest {
content?: string;
visibility?: string;
metadata?: Record<string, unknown>;
/** Kontekst-node (kommunikasjonsnode). Gir automatisk belongs_to-edge. */
context_id?: string;
}
export interface CreateNodeResponse {
node_id: string;
/** Edge-ID for automatisk belongs_to-edge (kun ved context_id). */
belongs_to_edge_id?: string;
}
export interface CreateEdgeRequest {

View file

@ -136,17 +136,28 @@ pub struct CreateNodeRequest {
pub visibility: Option<String>,
/// Typespesifikk metadata (JSON-objekt).
pub metadata: Option<serde_json::Value>,
/// Kontekst-node (f.eks. kommunikasjonsnode). Hvis satt, opprettes
/// automatisk en `belongs_to`-edge fra den nye noden til kontekstnoden.
/// Ref: docs/retninger/universell_input.md (kontekst-arv).
pub context_id: Option<Uuid>,
}
#[derive(Serialize)]
pub struct CreateNodeResponse {
pub node_id: Uuid,
/// Edge-ID for automatisk opprettet `belongs_to`-edge (kun ved context_id).
#[serde(skip_serializing_if = "Option::is_none")]
pub belongs_to_edge_id: Option<Uuid>,
}
/// POST /intentions/create_node
///
/// Validerer input, skriver til STDB (instant), spawner async PG-skriving.
/// Returnerer node_id umiddelbart.
///
/// Hvis `context_id` er satt, opprettes automatisk en `belongs_to`-edge
/// fra den nye noden til kontekstnoden. Kontekstnoden må eksistere og
/// være en kommunikasjonsnode. Ref: docs/retninger/universell_input.md
pub async fn create_node(
State(state): State<AppState>,
user: AuthUser,
@ -165,6 +176,31 @@ pub async fn create_node(
)));
}
// -- Valider context_id hvis satt --
if let Some(ctx_id) = req.context_id {
let ctx_node = sqlx::query_as::<_, NodeKindRow>(
"SELECT node_kind FROM nodes WHERE id = $1",
)
.bind(ctx_id)
.fetch_optional(&state.db)
.await
.map_err(|e| {
tracing::error!("PG-feil ved context_id-sjekk: {e}");
internal_error("Databasefeil ved validering av context_id")
})?;
match ctx_node {
None => return Err(bad_request(&format!("context_id {} finnes ikke", ctx_id))),
Some(row) if row.node_kind != "communication" => {
return Err(bad_request(&format!(
"context_id {} er en '{}'-node, ikke en kommunikasjonsnode",
ctx_id, row.node_kind
)));
}
_ => {} // OK — kommunikasjonsnode
}
}
let title = req.title.unwrap_or_default();
let content = req.content.unwrap_or_default();
let metadata = req
@ -196,6 +232,7 @@ pub async fn create_node(
node_id = %node_id,
node_kind = %node_kind,
created_by = %user.node_id,
context_id = ?req.context_id,
"Node opprettet i STDB"
);
@ -211,7 +248,54 @@ pub async fn create_node(
user.node_id,
);
Ok(Json(CreateNodeResponse { node_id }))
// -- Kontekst-arv: automatisk belongs_to-edge --
let belongs_to_edge_id = if let Some(ctx_id) = req.context_id {
let edge_id = Uuid::now_v7();
let edge_id_str = edge_id.to_string();
let ctx_id_str = ctx_id.to_string();
let bt_metadata = serde_json::json!({});
let bt_metadata_str = bt_metadata.to_string();
state
.stdb
.create_edge(
&edge_id_str,
&node_id_str, // source = ny node
&ctx_id_str, // target = kommunikasjonsnoden
"belongs_to",
&bt_metadata_str,
false,
&created_by_str,
)
.await
.map_err(|e| stdb_error("create_edge (belongs_to)", e))?;
tracing::info!(
edge_id = %edge_id,
node_id = %node_id,
context_id = %ctx_id,
"belongs_to-edge opprettet i STDB (kontekst-arv)"
);
// belongs_to er ikke tilgangsgivende — enkel PG-insert
spawn_pg_insert_edge(
state.db.clone(),
state.stdb.clone(),
edge_id,
node_id,
ctx_id,
"belongs_to".to_string(),
bt_metadata,
false,
user.node_id,
);
Some(edge_id)
} else {
None
};
Ok(Json(CreateNodeResponse { node_id, belongs_to_edge_id }))
}
// =============================================================================
@ -705,6 +789,12 @@ struct NodeRow {
metadata: serde_json::Value,
}
/// Enkel rad for å sjekke node_kind (brukes ved context_id-validering).
#[derive(sqlx::FromRow)]
struct NodeKindRow {
node_kind: String,
}
/// Spawner en tokio-task som skriver noden til PostgreSQL i bakgrunnen.
fn spawn_pg_insert_node(
db: PgPool,