Flere intensjoner: create_edge, update_node, delete_node (oppgave 2.5)
Implementerer tre nye skrivestier i maskinrommet med tilgangskontroll: - POST /intentions/create_edge — opprett retningsbestemt edge mellom to noder. Validerer at begge noder eksisterer og edge_type er satt. - POST /intentions/update_node — partial update av eksisterende node. Kun oppgitte felter endres, resten beholdes fra PG. - POST /intentions/delete_node — slett node med cascade av edges. Tilgangskontroll for update/delete: brukeren må enten være created_by på noden, eller ha en owner/admin-edge til den. Sjekkes mot PG som autoritativ kilde. Alle endepunkter følger samme mønster som create_node: STDB-skriving (instant) → async PG-persistering → umiddelbar respons. Verifisert på server med 10 testcaser: 1. /me med gyldig token → 200 2. create_node → 200 med node_id 3. create_edge (gyldig) → 200 med edge_id 4. create_edge (ugyldig source) → 400 5. create_edge (tom edge_type) → 400 6. update_node (partial, eier) → 200 7. update_node (ingen tilgang) → 403 8. delete_node (eier) → 200 9. delete_node (ingen tilgang) → 403 10. update via owner-edge (Sidelinja) → 200 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f7a1a36824
commit
1e9a4c83c3
4 changed files with 524 additions and 38 deletions
|
|
@ -67,6 +67,20 @@ Tunge spørringer (søk, statistikk, graftraversering) går via maskinrommet →
|
|||
- Body (JSON): `{ node_kind?, title?, content?, visibility?, metadata? }`
|
||||
- Defaults: `node_kind="content"`, `visibility="hidden"`, andre felter tomme
|
||||
- Respons: `{ node_id: "<uuid>" }`
|
||||
- `POST /intentions/create_edge` — Opprett edge mellom to noder.
|
||||
Validerer at begge nodene eksisterer og at edge_type ikke er tom.
|
||||
- Body (JSON): `{ source_id, target_id, edge_type, metadata?, system? }`
|
||||
- Defaults: `metadata={}`, `system=false`
|
||||
- Respons: `{ edge_id: "<uuid>" }`
|
||||
- `POST /intentions/update_node` — Oppdater eksisterende node (partial update).
|
||||
Krever tilgang: created_by eller owner/admin-edge.
|
||||
- Body (JSON): `{ node_id, node_kind?, title?, content?, visibility?, metadata? }`
|
||||
- Kun oppgitte felter endres, resten beholdes
|
||||
- Respons: `{ node_id: "<uuid>" }`
|
||||
- `POST /intentions/delete_node` — Slett node og tilhørende edges.
|
||||
Krever tilgang: created_by eller owner/admin-edge.
|
||||
- Body (JSON): `{ node_id }`
|
||||
- Respons: `{ deleted: true }`
|
||||
|
||||
## 6. Instruks for Claude Code
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
// skriver til SpacetimeDB først (instant feedback via WebSocket),
|
||||
// deretter persisterer til PostgreSQL asynkront.
|
||||
//
|
||||
// Tilgangskontroll: Muterende operasjoner (update, delete) krever at
|
||||
// brukeren er created_by på noden, eller har owner/admin-edge til den.
|
||||
//
|
||||
// Ref: docs/retninger/maskinrommet.md, docs/retninger/datalaget.md
|
||||
|
||||
use axum::{extract::State, http::StatusCode, Json};
|
||||
|
|
@ -15,12 +18,112 @@ use crate::auth::AuthUser;
|
|||
use crate::AppState;
|
||||
|
||||
// =============================================================================
|
||||
// create_node
|
||||
// Felles
|
||||
// =============================================================================
|
||||
|
||||
/// Gyldige visibility-verdier (speiler PG enum).
|
||||
const VALID_VISIBILITIES: &[&str] = &["hidden", "discoverable", "readable", "open"];
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ErrorResponse {
|
||||
error: String,
|
||||
}
|
||||
|
||||
fn bad_request(msg: &str) -> (StatusCode, Json<ErrorResponse>) {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse {
|
||||
error: msg.to_string(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn forbidden(msg: &str) -> (StatusCode, Json<ErrorResponse>) {
|
||||
(
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(ErrorResponse {
|
||||
error: msg.to_string(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn internal_error(msg: &str) -> (StatusCode, Json<ErrorResponse>) {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
error: msg.to_string(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn stdb_error(op: &str, e: crate::stdb::StdbError) -> (StatusCode, Json<ErrorResponse>) {
|
||||
tracing::error!("STDB {op} feilet: {e}");
|
||||
internal_error(&format!("Kunne ikke skrive til SpacetimeDB: {e}"))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tilgangskontroll
|
||||
// =============================================================================
|
||||
|
||||
/// Sjekker om brukeren har skrivetilgang til en node.
|
||||
/// Returnerer true hvis brukeren er created_by, eller har owner/admin-edge.
|
||||
async fn user_can_modify_node(db: &PgPool, user_id: Uuid, node_id: Uuid) -> Result<bool, sqlx::Error> {
|
||||
let row = sqlx::query_scalar::<_, bool>(
|
||||
r#"
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM nodes WHERE id = $1 AND created_by = $2
|
||||
) OR EXISTS(
|
||||
SELECT 1 FROM edges
|
||||
WHERE source_id = $2 AND target_id = $1
|
||||
AND edge_type IN ('owner', 'admin')
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(node_id)
|
||||
.bind(user_id)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
/// Sjekker om brukeren har skrivetilgang til en edge.
|
||||
/// Brukeren må ha opprettet edgen, eller ha owner/admin-edge til source-noden.
|
||||
#[allow(dead_code)]
|
||||
async fn user_can_modify_edge(db: &PgPool, user_id: Uuid, edge_id: Uuid) -> Result<bool, sqlx::Error> {
|
||||
let row = sqlx::query_scalar::<_, bool>(
|
||||
r#"
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM edges WHERE id = $1 AND created_by = $2
|
||||
) OR EXISTS(
|
||||
SELECT 1 FROM edges e
|
||||
JOIN edges access_edge ON access_edge.source_id = $2
|
||||
AND access_edge.target_id = e.source_id
|
||||
AND access_edge.edge_type IN ('owner', 'admin')
|
||||
WHERE e.id = $1
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(edge_id)
|
||||
.bind(user_id)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
/// Sjekker om en node eksisterer i PG.
|
||||
async fn node_exists(db: &PgPool, node_id: Uuid) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM nodes WHERE id = $1)")
|
||||
.bind(node_id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// create_node
|
||||
// =============================================================================
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateNodeRequest {
|
||||
/// Hint om hva noden er. Default: "content".
|
||||
|
|
@ -40,11 +143,6 @@ pub struct CreateNodeResponse {
|
|||
pub node_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ErrorResponse {
|
||||
error: String,
|
||||
}
|
||||
|
||||
/// POST /intentions/create_node
|
||||
///
|
||||
/// Validerer input, skriver til STDB (instant), spawner async PG-skriving.
|
||||
|
|
@ -92,15 +190,7 @@ pub async fn create_node(
|
|||
&created_by_str,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("STDB create_node feilet: {e}");
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
error: format!("Kunne ikke skrive til SpacetimeDB: {e}"),
|
||||
}),
|
||||
)
|
||||
})?;
|
||||
.map_err(|e| stdb_error("create_node", e))?;
|
||||
|
||||
tracing::info!(
|
||||
node_id = %node_id,
|
||||
|
|
@ -110,7 +200,7 @@ pub async fn create_node(
|
|||
);
|
||||
|
||||
// -- Spawn async PG-skriving --
|
||||
spawn_pg_insert(
|
||||
spawn_pg_insert_node(
|
||||
state.db.clone(),
|
||||
node_id,
|
||||
node_kind,
|
||||
|
|
@ -121,13 +211,312 @@ pub async fn create_node(
|
|||
user.node_id,
|
||||
);
|
||||
|
||||
// -- Returner node_id umiddelbart --
|
||||
Ok(Json(CreateNodeResponse { node_id }))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// create_edge
|
||||
// =============================================================================
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateEdgeRequest {
|
||||
/// Kilde-node (fra).
|
||||
pub source_id: Uuid,
|
||||
/// Mål-node (til).
|
||||
pub target_id: Uuid,
|
||||
/// Relasjontype (freeform streng). Ref: docs/primitiver/edges.md
|
||||
pub edge_type: String,
|
||||
/// Typespesifikk metadata (JSON-objekt).
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
/// Systemedge — usynlig ved traversering. Default: false.
|
||||
pub system: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CreateEdgeResponse {
|
||||
pub edge_id: Uuid,
|
||||
}
|
||||
|
||||
/// POST /intentions/create_edge
|
||||
///
|
||||
/// Oppretter en retningsbestemt edge mellom to noder.
|
||||
/// Krever at begge nodene eksisterer. Brukeren trenger ikke spesiell
|
||||
/// tilgang for å opprette edges — tilgangskontroll på edges håndheves
|
||||
/// ved lesing (node_access-matrisen, fase 4).
|
||||
pub async fn create_edge(
|
||||
State(state): State<AppState>,
|
||||
user: AuthUser,
|
||||
Json(req): Json<CreateEdgeRequest>,
|
||||
) -> Result<Json<CreateEdgeResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
// -- Valider input --
|
||||
if req.edge_type.is_empty() {
|
||||
return Err(bad_request("edge_type kan ikke være tom"));
|
||||
}
|
||||
|
||||
// Sjekk at begge nodene eksisterer
|
||||
let (source_exists, target_exists) = tokio::try_join!(
|
||||
node_exists(&state.db, req.source_id),
|
||||
node_exists(&state.db, req.target_id),
|
||||
)
|
||||
.map_err(|e| {
|
||||
tracing::error!("PG-feil ved nodesjekk: {e}");
|
||||
internal_error("Databasefeil ved validering")
|
||||
})?;
|
||||
|
||||
if !source_exists {
|
||||
return Err(bad_request(&format!("source_id {} finnes ikke", req.source_id)));
|
||||
}
|
||||
if !target_exists {
|
||||
return Err(bad_request(&format!("target_id {} finnes ikke", req.target_id)));
|
||||
}
|
||||
|
||||
let metadata = req.metadata.unwrap_or_else(|| serde_json::json!({}));
|
||||
let metadata_str = metadata.to_string();
|
||||
let system = req.system.unwrap_or(false);
|
||||
|
||||
// -- Generer UUIDv7 --
|
||||
let edge_id = Uuid::now_v7();
|
||||
let edge_id_str = edge_id.to_string();
|
||||
let source_id_str = req.source_id.to_string();
|
||||
let target_id_str = req.target_id.to_string();
|
||||
let created_by_str = user.node_id.to_string();
|
||||
|
||||
// -- Skriv til SpacetimeDB (instant) --
|
||||
state
|
||||
.stdb
|
||||
.create_edge(
|
||||
&edge_id_str,
|
||||
&source_id_str,
|
||||
&target_id_str,
|
||||
&req.edge_type,
|
||||
&metadata_str,
|
||||
system,
|
||||
&created_by_str,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| stdb_error("create_edge", e))?;
|
||||
|
||||
tracing::info!(
|
||||
edge_id = %edge_id,
|
||||
source_id = %req.source_id,
|
||||
target_id = %req.target_id,
|
||||
edge_type = %req.edge_type,
|
||||
created_by = %user.node_id,
|
||||
"Edge opprettet i STDB"
|
||||
);
|
||||
|
||||
// -- Spawn async PG-skriving --
|
||||
let edge_type = req.edge_type.clone();
|
||||
spawn_pg_insert_edge(
|
||||
state.db.clone(),
|
||||
edge_id,
|
||||
req.source_id,
|
||||
req.target_id,
|
||||
edge_type,
|
||||
metadata,
|
||||
system,
|
||||
user.node_id,
|
||||
);
|
||||
|
||||
Ok(Json(CreateEdgeResponse { edge_id }))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// update_node
|
||||
// =============================================================================
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateNodeRequest {
|
||||
/// ID til noden som skal oppdateres.
|
||||
pub node_id: Uuid,
|
||||
/// Ny node_kind. Beholder eksisterende hvis None.
|
||||
pub node_kind: Option<String>,
|
||||
/// Ny tittel. Beholder eksisterende hvis None.
|
||||
pub title: Option<String>,
|
||||
/// Nytt innhold. Beholder eksisterende hvis None.
|
||||
pub content: Option<String>,
|
||||
/// Ny synlighet. Beholder eksisterende hvis None.
|
||||
pub visibility: Option<String>,
|
||||
/// Ny metadata. Beholder eksisterende hvis None.
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct UpdateNodeResponse {
|
||||
pub node_id: Uuid,
|
||||
}
|
||||
|
||||
/// POST /intentions/update_node
|
||||
///
|
||||
/// Oppdaterer en eksisterende node. Krever at brukeren er created_by
|
||||
/// eller har owner/admin-edge til noden.
|
||||
pub async fn update_node(
|
||||
State(state): State<AppState>,
|
||||
user: AuthUser,
|
||||
Json(req): Json<UpdateNodeRequest>,
|
||||
) -> Result<Json<UpdateNodeResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
// -- Tilgangskontroll --
|
||||
let can_modify = user_can_modify_node(&state.db, user.node_id, req.node_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("PG-feil ved tilgangssjekk: {e}");
|
||||
internal_error("Databasefeil ved tilgangssjekk")
|
||||
})?;
|
||||
|
||||
if !can_modify {
|
||||
return Err(forbidden("Ingen tilgang til å endre denne noden"));
|
||||
}
|
||||
|
||||
// -- Hent eksisterende node fra PG for å fylle inn manglende felt --
|
||||
let existing = sqlx::query_as::<_, NodeRow>(
|
||||
"SELECT node_kind, title, content, visibility::text, metadata FROM nodes WHERE id = $1",
|
||||
)
|
||||
.bind(req.node_id)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("PG-feil ved henting av node: {e}");
|
||||
internal_error("Databasefeil ved henting av node")
|
||||
})?
|
||||
.ok_or_else(|| bad_request(&format!("Node {} finnes ikke", req.node_id)))?;
|
||||
|
||||
let node_kind = req.node_kind.unwrap_or(existing.node_kind);
|
||||
if node_kind.is_empty() {
|
||||
return Err(bad_request("node_kind kan ikke være tom"));
|
||||
}
|
||||
|
||||
let visibility = req.visibility.unwrap_or(existing.visibility);
|
||||
if !VALID_VISIBILITIES.contains(&visibility.as_str()) {
|
||||
return Err(bad_request(&format!(
|
||||
"Ugyldig visibility: '{visibility}'. Gyldige verdier: {VALID_VISIBILITIES:?}"
|
||||
)));
|
||||
}
|
||||
|
||||
let title = req.title.unwrap_or(existing.title.unwrap_or_default());
|
||||
let content = req.content.unwrap_or(existing.content.unwrap_or_default());
|
||||
let metadata = req.metadata.unwrap_or(existing.metadata);
|
||||
let metadata_str = metadata.to_string();
|
||||
|
||||
let node_id_str = req.node_id.to_string();
|
||||
|
||||
// -- Skriv til SpacetimeDB (instant) --
|
||||
state
|
||||
.stdb
|
||||
.update_node(
|
||||
&node_id_str,
|
||||
&node_kind,
|
||||
&title,
|
||||
&content,
|
||||
&visibility,
|
||||
&metadata_str,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| stdb_error("update_node", e))?;
|
||||
|
||||
tracing::info!(
|
||||
node_id = %req.node_id,
|
||||
updated_by = %user.node_id,
|
||||
"Node oppdatert i STDB"
|
||||
);
|
||||
|
||||
// -- Spawn async PG-skriving --
|
||||
spawn_pg_update_node(
|
||||
state.db.clone(),
|
||||
req.node_id,
|
||||
node_kind,
|
||||
title,
|
||||
content,
|
||||
visibility,
|
||||
metadata,
|
||||
);
|
||||
|
||||
Ok(Json(UpdateNodeResponse { node_id: req.node_id }))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// delete_node
|
||||
// =============================================================================
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteNodeRequest {
|
||||
/// ID til noden som skal slettes.
|
||||
pub node_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DeleteNodeResponse {
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
/// POST /intentions/delete_node
|
||||
///
|
||||
/// Sletter en node og alle dens edges (CASCADE i PG, eksplisitt i STDB).
|
||||
/// Krever at brukeren er created_by eller har owner/admin-edge til noden.
|
||||
pub async fn delete_node(
|
||||
State(state): State<AppState>,
|
||||
user: AuthUser,
|
||||
Json(req): Json<DeleteNodeRequest>,
|
||||
) -> Result<Json<DeleteNodeResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
// -- Tilgangskontroll --
|
||||
let can_modify = user_can_modify_node(&state.db, user.node_id, req.node_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("PG-feil ved tilgangssjekk: {e}");
|
||||
internal_error("Databasefeil ved tilgangssjekk")
|
||||
})?;
|
||||
|
||||
if !can_modify {
|
||||
return Err(forbidden("Ingen tilgang til å slette denne noden"));
|
||||
}
|
||||
|
||||
// Sjekk at noden eksisterer
|
||||
let exists = node_exists(&state.db, req.node_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("PG-feil ved nodesjekk: {e}");
|
||||
internal_error("Databasefeil ved validering")
|
||||
})?;
|
||||
|
||||
if !exists {
|
||||
return Err(bad_request(&format!("Node {} finnes ikke", req.node_id)));
|
||||
}
|
||||
|
||||
let node_id_str = req.node_id.to_string();
|
||||
|
||||
// -- Slett fra SpacetimeDB (instant) --
|
||||
state
|
||||
.stdb
|
||||
.delete_node(&node_id_str)
|
||||
.await
|
||||
.map_err(|e| stdb_error("delete_node", e))?;
|
||||
|
||||
tracing::info!(
|
||||
node_id = %req.node_id,
|
||||
deleted_by = %user.node_id,
|
||||
"Node slettet fra STDB"
|
||||
);
|
||||
|
||||
// -- Spawn async PG-sletting --
|
||||
spawn_pg_delete_node(state.db.clone(), req.node_id);
|
||||
|
||||
Ok(Json(DeleteNodeResponse { deleted: true }))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Bakgrunns-PG-operasjoner
|
||||
// =============================================================================
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct NodeRow {
|
||||
node_kind: String,
|
||||
title: Option<String>,
|
||||
content: Option<String>,
|
||||
visibility: String,
|
||||
metadata: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Spawner en tokio-task som skriver noden til PostgreSQL i bakgrunnen.
|
||||
/// Frontend får oppdatering via STDB WebSocket uavhengig av denne.
|
||||
fn spawn_pg_insert(
|
||||
fn spawn_pg_insert_node(
|
||||
db: PgPool,
|
||||
node_id: Uuid,
|
||||
node_kind: String,
|
||||
|
|
@ -156,28 +545,109 @@ fn spawn_pg_insert(
|
|||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
tracing::info!(
|
||||
node_id = %node_id,
|
||||
"Node persistert til PostgreSQL"
|
||||
);
|
||||
tracing::info!(node_id = %node_id, "Node persistert til PostgreSQL");
|
||||
}
|
||||
Err(e) => {
|
||||
// Logg feilen. I fremtiden: dead letter queue (fase 12.3).
|
||||
tracing::error!(
|
||||
node_id = %node_id,
|
||||
error = %e,
|
||||
"Kunne ikke persistere node til PostgreSQL"
|
||||
);
|
||||
tracing::error!(node_id = %node_id, error = %e, "Kunne ikke persistere node til PostgreSQL");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn bad_request(msg: &str) -> (StatusCode, Json<ErrorResponse>) {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse {
|
||||
error: msg.to_string(),
|
||||
}),
|
||||
/// Spawner en tokio-task som skriver edgen til PostgreSQL i bakgrunnen.
|
||||
fn spawn_pg_insert_edge(
|
||||
db: PgPool,
|
||||
edge_id: Uuid,
|
||||
source_id: Uuid,
|
||||
target_id: Uuid,
|
||||
edge_type: String,
|
||||
metadata: serde_json::Value,
|
||||
system: bool,
|
||||
created_by: Uuid,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO edges (id, source_id, target_id, edge_type, metadata, system, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
"#,
|
||||
)
|
||||
.bind(edge_id)
|
||||
.bind(source_id)
|
||||
.bind(target_id)
|
||||
.bind(&edge_type)
|
||||
.bind(&metadata)
|
||||
.bind(system)
|
||||
.bind(created_by)
|
||||
.execute(&db)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
tracing::info!(edge_id = %edge_id, "Edge persistert til PostgreSQL");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(edge_id = %edge_id, error = %e, "Kunne ikke persistere edge til PostgreSQL");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Spawner en tokio-task som oppdaterer noden i PostgreSQL.
|
||||
fn spawn_pg_update_node(
|
||||
db: PgPool,
|
||||
node_id: Uuid,
|
||||
node_kind: String,
|
||||
title: String,
|
||||
content: String,
|
||||
visibility: String,
|
||||
metadata: serde_json::Value,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
UPDATE nodes
|
||||
SET node_kind = $2, title = NULLIF($3, ''), content = NULLIF($4, ''),
|
||||
visibility = $5::visibility, metadata = $6
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(node_id)
|
||||
.bind(&node_kind)
|
||||
.bind(&title)
|
||||
.bind(&content)
|
||||
.bind(&visibility)
|
||||
.bind(&metadata)
|
||||
.execute(&db)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
tracing::info!(node_id = %node_id, "Node oppdatert i PostgreSQL");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(node_id = %node_id, error = %e, "Kunne ikke oppdatere node i PostgreSQL");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Spawner en tokio-task som sletter noden fra PostgreSQL.
|
||||
/// Edges slettes automatisk via ON DELETE CASCADE.
|
||||
fn spawn_pg_delete_node(db: PgPool, node_id: Uuid) {
|
||||
tokio::spawn(async move {
|
||||
let result = sqlx::query("DELETE FROM nodes WHERE id = $1")
|
||||
.bind(node_id)
|
||||
.execute(&db)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
tracing::info!(node_id = %node_id, "Node slettet fra PostgreSQL");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(node_id = %node_id, error = %e, "Kunne ikke slette node fra PostgreSQL");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,9 @@ async fn main() {
|
|||
.route("/health", get(health))
|
||||
.route("/me", get(me))
|
||||
.route("/intentions/create_node", post(intentions::create_node))
|
||||
.route("/intentions/create_edge", post(intentions::create_edge))
|
||||
.route("/intentions/update_node", post(intentions::update_node))
|
||||
.route("/intentions/delete_node", post(intentions::delete_node))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(state);
|
||||
|
||||
|
|
|
|||
3
tasks.md
3
tasks.md
|
|
@ -55,8 +55,7 @@ Uavhengige faser kan fortsatt plukkes.
|
|||
- [x] 2.2 Auth-middleware: valider Authentik JWT-tokens, slå opp `auth_identities` → node_id. Returner 401 for ugyldige tokens.
|
||||
- [x] 2.3 SpacetimeDB-klient i maskinrommet: koble til STDB, skriv noder og edges via reducers.
|
||||
- [x] 2.4 Skrivestien: `POST /intentions/create_node` — valider, skriv STDB (instant), spawn async PG-skriving. Returner node_id umiddelbart.
|
||||
- [~] 2.5 Flere intensjoner: `create_edge`, `update_node`, `delete_node`. Validering av tilgang (created_by eller owner/admin-edge).
|
||||
> Påbegynt: 2026-03-17T13:11
|
||||
- [x] 2.5 Flere intensjoner: `create_edge`, `update_node`, `delete_node`. Validering av tilgang (created_by eller owner/admin-edge).
|
||||
- [ ] 2.6 Docker Compose: legg maskinrommet inn i server-stacken. Intern nettverkstilgang til PG og STDB.
|
||||
|
||||
## Fase 3: Frontend — skjelett
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue