Legg til create_alias-endepunkt og alias-spørring

Oppgave 8.1: Alias-noder med system-edge. Nytt endepunkt
POST /intentions/create_alias oppretter en person-node og
en alias-edge (system=true) fra brukerens hovednode.
GET /query/aliases returnerer brukerens alias-noder.

Alias-edgen er usynlig for traversering via eksisterende
RLS-policy som filtrerer system-edges.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
vegard 2026-03-17 19:00:08 +01:00
parent b687c717c6
commit 89abd5eee4
2 changed files with 152 additions and 0 deletions

View file

@ -1066,6 +1066,156 @@ async fn find_collection_for_node(db: &PgPool, node_id: Uuid) -> Result<Option<U
Ok(row)
}
// =============================================================================
// create_alias
// =============================================================================
#[derive(Deserialize)]
pub struct CreateAliasRequest {
/// Visningsnavn for aliaset (f.eks. "Bjørn" for en podcastvert-identitet).
pub title: String,
/// Valgfri metadata (f.eks. display_name, bio, avatar).
pub metadata: Option<serde_json::Value>,
}
#[derive(Serialize)]
pub struct CreateAliasResponse {
/// ID til den nye alias-noden.
pub alias_node_id: Uuid,
/// ID til alias-edgen (system=true, usynlig for traversering).
pub alias_edge_id: Uuid,
}
/// POST /intentions/create_alias
///
/// Oppretter en alias-node (node_kind='person') og en `alias`-edge
/// (system=true) fra brukerens hovednode til aliasnoden. Alias-edgen
/// er usynlig for traversering — RLS-policyen filtrerer system-edges.
///
/// Bruksområde: en bruker kan ha flere identiteter (f.eks. Vegard
/// som seg selv og "Bjørn" som podcastvert). Oppgave 8.2 vil bruke
/// aliaset til å sette created_by kontekstbasert.
///
/// Ref: docs/primitiver/edges.md (systemedges), docs/primitiver/nodes.md
pub async fn create_alias(
State(state): State<AppState>,
user: AuthUser,
Json(req): Json<CreateAliasRequest>,
) -> Result<Json<CreateAliasResponse>, (StatusCode, Json<ErrorResponse>)> {
let title = req.title.trim().to_string();
if title.is_empty() {
return Err(bad_request("Alias-tittel kan ikke være tom"));
}
let metadata = req.metadata.unwrap_or_else(|| serde_json::json!({}));
// -- Generer IDer --
let alias_node_id = Uuid::now_v7();
let alias_edge_id = Uuid::now_v7();
let alias_node_id_str = alias_node_id.to_string();
let alias_edge_id_str = alias_edge_id.to_string();
let user_node_id_str = user.node_id.to_string();
let metadata_str = metadata.to_string();
// -- Skriv alias-node til STDB --
state
.stdb
.create_node(
&alias_node_id_str,
"person",
&title,
"",
"hidden",
&metadata_str,
&user_node_id_str,
)
.await
.map_err(|e| stdb_error("create_node (alias)", e))?;
// -- Skriv alias-edge til STDB (system=true) --
state
.stdb
.create_edge(
&alias_edge_id_str,
&user_node_id_str,
&alias_node_id_str,
"alias",
"{}",
true,
&user_node_id_str,
)
.await
.map_err(|e| stdb_error("create_edge (alias)", e))?;
tracing::info!(
alias_node_id = %alias_node_id,
alias_edge_id = %alias_edge_id,
user_node_id = %user.node_id,
title = %title,
"Alias opprettet i STDB"
);
// -- Spawn async PG-skriving: node + edge --
let pg_db = state.db.clone();
let pg_title = title.clone();
let pg_metadata = metadata.clone();
let pg_user_node_id = user.node_id;
tokio::spawn(async move {
// 1. Skriv alias-noden
let node_result = sqlx::query(
r#"
INSERT INTO nodes (id, node_kind, title, visibility, metadata, created_by)
VALUES ($1, 'person', $2, 'hidden'::visibility, $3, $4)
"#,
)
.bind(alias_node_id)
.bind(&pg_title)
.bind(&pg_metadata)
.bind(pg_user_node_id)
.execute(&pg_db)
.await;
match node_result {
Ok(_) => {
tracing::info!(alias_node_id = %alias_node_id, "Alias-node persistert til PostgreSQL");
}
Err(e) => {
tracing::error!(alias_node_id = %alias_node_id, error = %e, "Kunne ikke persistere alias-node til PostgreSQL");
return;
}
}
// 2. Skriv alias-edge (system=true)
let edge_result = sqlx::query(
r#"
INSERT INTO edges (id, source_id, target_id, edge_type, metadata, system, created_by)
VALUES ($1, $2, $3, 'alias', '{}', true, $4)
"#,
)
.bind(alias_edge_id)
.bind(pg_user_node_id)
.bind(alias_node_id)
.bind(pg_user_node_id)
.execute(&pg_db)
.await;
match edge_result {
Ok(_) => {
tracing::info!(alias_edge_id = %alias_edge_id, "Alias-edge persistert til PostgreSQL");
}
Err(e) => {
tracing::error!(alias_edge_id = %alias_edge_id, error = %e, "Kunne ikke persistere alias-edge til PostgreSQL");
}
}
});
Ok(Json(CreateAliasResponse {
alias_node_id,
alias_edge_id,
}))
}
// =============================================================================
// Bakgrunns-PG-operasjoner
// =============================================================================

View file

@ -141,9 +141,11 @@ async fn main() {
.route("/query/nodes", get(queries::query_nodes))
.route("/query/segments", get(queries::query_segments))
.route("/query/segments/srt", get(queries::export_srt))
.route("/intentions/create_alias", post(intentions::create_alias))
.route("/intentions/update_segment", post(intentions::update_segment))
.route("/intentions/retranscribe", post(intentions::retranscribe))
.route("/intentions/resolve_retranscription", post(intentions::resolve_retranscription))
.route("/query/aliases", get(queries::query_aliases))
.route("/query/transcription_versions", get(queries::query_transcription_versions))
.route("/query/segments_version", get(queries::query_segments_version))
.layer(TraceLayer::new_for_http())