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:
parent
4ef0b31b3c
commit
b55705d12c
2 changed files with 95 additions and 1 deletions
|
|
@ -12,10 +12,14 @@ export interface CreateNodeRequest {
|
||||||
content?: string;
|
content?: string;
|
||||||
visibility?: string;
|
visibility?: string;
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
|
/** Kontekst-node (kommunikasjonsnode). Gir automatisk belongs_to-edge. */
|
||||||
|
context_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateNodeResponse {
|
export interface CreateNodeResponse {
|
||||||
node_id: string;
|
node_id: string;
|
||||||
|
/** Edge-ID for automatisk belongs_to-edge (kun ved context_id). */
|
||||||
|
belongs_to_edge_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateEdgeRequest {
|
export interface CreateEdgeRequest {
|
||||||
|
|
|
||||||
|
|
@ -136,17 +136,28 @@ pub struct CreateNodeRequest {
|
||||||
pub visibility: Option<String>,
|
pub visibility: Option<String>,
|
||||||
/// Typespesifikk metadata (JSON-objekt).
|
/// Typespesifikk metadata (JSON-objekt).
|
||||||
pub metadata: Option<serde_json::Value>,
|
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)]
|
#[derive(Serialize)]
|
||||||
pub struct CreateNodeResponse {
|
pub struct CreateNodeResponse {
|
||||||
pub node_id: Uuid,
|
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
|
/// POST /intentions/create_node
|
||||||
///
|
///
|
||||||
/// Validerer input, skriver til STDB (instant), spawner async PG-skriving.
|
/// Validerer input, skriver til STDB (instant), spawner async PG-skriving.
|
||||||
/// Returnerer node_id umiddelbart.
|
/// 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(
|
pub async fn create_node(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
user: AuthUser,
|
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 title = req.title.unwrap_or_default();
|
||||||
let content = req.content.unwrap_or_default();
|
let content = req.content.unwrap_or_default();
|
||||||
let metadata = req
|
let metadata = req
|
||||||
|
|
@ -196,6 +232,7 @@ pub async fn create_node(
|
||||||
node_id = %node_id,
|
node_id = %node_id,
|
||||||
node_kind = %node_kind,
|
node_kind = %node_kind,
|
||||||
created_by = %user.node_id,
|
created_by = %user.node_id,
|
||||||
|
context_id = ?req.context_id,
|
||||||
"Node opprettet i STDB"
|
"Node opprettet i STDB"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -211,7 +248,54 @@ pub async fn create_node(
|
||||||
user.node_id,
|
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,
|
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.
|
/// Spawner en tokio-task som skriver noden til PostgreSQL i bakgrunnen.
|
||||||
fn spawn_pg_insert_node(
|
fn spawn_pg_insert_node(
|
||||||
db: PgPool,
|
db: PgPool,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue