Skrivestien: POST /intentions/create_node (oppgave 2.4)
Implementerer den første intensjonen i maskinrommet — skrivestien som gjør at frontend kan opprette noder via maskinrommet. Flyten: 1. Valider input (node_kind, visibility, metadata) 2. Generer UUIDv7 (tidssortert) 3. Skriv til SpacetimeDB (instant — frontend ser noden umiddelbart) 4. Spawn async tokio-task for PG-persistering 5. Returner node_id uten å vente på PG Verifisert på server med fire testcaser: 1. Uten auth → 401 2. Ugyldig visibility → 400 med feilmelding 3. Minimal request (tomt body) → 200, node opprettet med defaults 4. Full request → 200, node verifisert i både STDB og PG Også: Dockerfile oppdatert til Rust 1.88 (avhengigheter krevde >1.86), og api_grensesnitt.md oppdatert med endepunktdokumentasjon. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e7e8e8a10d
commit
61b35d3c01
5 changed files with 202 additions and 5 deletions
|
|
@ -55,7 +55,20 @@ Tunge spørringer (søk, statistikk, graftraversering) går via maskinrommet →
|
||||||
- **SvelteKit er et rent frontend-prosjekt.** Ingen server-side PG-tilgang.
|
- **SvelteKit er et rent frontend-prosjekt.** Ingen server-side PG-tilgang.
|
||||||
- **Bakgrunnsjobber** (Whisper, LLM, TTS) orkestreres av maskinrommet, aldri direkte fra frontend.
|
- **Bakgrunnsjobber** (Whisper, LLM, TTS) orkestreres av maskinrommet, aldri direkte fra frontend.
|
||||||
|
|
||||||
## 5. Instruks for Claude Code
|
## 5. Implementerte endepunkter
|
||||||
|
|
||||||
|
### Offentlige
|
||||||
|
- `GET /health` — Helsesjekk. Verifiserer PG- og STDB-tilkobling.
|
||||||
|
|
||||||
|
### Autentiserte (krever `Authorization: Bearer <JWT>`)
|
||||||
|
- `GET /me` — Returnerer autentisert brukers `node_id` og `authentik_sub`.
|
||||||
|
- `POST /intentions/create_node` — Opprett node. Skriv til STDB (instant),
|
||||||
|
spawn async PG-skriving, returner `node_id` umiddelbart.
|
||||||
|
- Body (JSON): `{ node_kind?, title?, content?, visibility?, metadata? }`
|
||||||
|
- Defaults: `node_kind="content"`, `visibility="hidden"`, andre felter tomme
|
||||||
|
- Respons: `{ node_id: "<uuid>" }`
|
||||||
|
|
||||||
|
## 6. Instruks for Claude Code
|
||||||
|
|
||||||
- Maskinrommet (`maskinrommet/`) er Rust-prosjektet med axum, tokio, sqlx.
|
- Maskinrommet (`maskinrommet/`) er Rust-prosjektet med axum, tokio, sqlx.
|
||||||
- Intensjoner fra frontend → `POST /intentions/*` endepunkter i maskinrommet.
|
- Intensjoner fra frontend → `POST /intentions/*` endepunkter i maskinrommet.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Maskinrommet — flertrinns Docker-bygg
|
# Maskinrommet — flertrinns Docker-bygg
|
||||||
# Bygger Rust-binæren i et kompileringssteg, kopierer til minimal runtime.
|
# Bygger Rust-binæren i et kompileringssteg, kopierer til minimal runtime.
|
||||||
|
|
||||||
FROM rust:1.86 AS builder
|
FROM rust:1.88 AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY Cargo.toml Cargo.lock* ./
|
COPY Cargo.toml Cargo.lock* ./
|
||||||
|
|
|
||||||
183
maskinrommet/src/intentions.rs
Normal file
183
maskinrommet/src/intentions.rs
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
// Intensjoner — skrivestien i maskinrommet.
|
||||||
|
//
|
||||||
|
// Frontend sender intensjoner (ikke data). Maskinrommet validerer,
|
||||||
|
// skriver til SpacetimeDB først (instant feedback via WebSocket),
|
||||||
|
// deretter persisterer til PostgreSQL asynkront.
|
||||||
|
//
|
||||||
|
// Ref: docs/retninger/maskinrommet.md, docs/retninger/datalaget.md
|
||||||
|
|
||||||
|
use axum::{extract::State, http::StatusCode, Json};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::auth::AuthUser;
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// create_node
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Gyldige visibility-verdier (speiler PG enum).
|
||||||
|
const VALID_VISIBILITIES: &[&str] = &["hidden", "discoverable", "readable", "open"];
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateNodeRequest {
|
||||||
|
/// Hint om hva noden er. Default: "content".
|
||||||
|
pub node_kind: Option<String>,
|
||||||
|
/// Visningstittel. Kan være null (f.eks. chatmeldinger).
|
||||||
|
pub title: Option<String>,
|
||||||
|
/// Ren tekst-innhold.
|
||||||
|
pub content: Option<String>,
|
||||||
|
/// Synlighet. Default: "hidden" (privat).
|
||||||
|
pub visibility: Option<String>,
|
||||||
|
/// Typespesifikk metadata (JSON-objekt).
|
||||||
|
pub metadata: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
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.
|
||||||
|
/// Returnerer node_id umiddelbart.
|
||||||
|
pub async fn create_node(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
user: AuthUser,
|
||||||
|
Json(req): Json<CreateNodeRequest>,
|
||||||
|
) -> Result<Json<CreateNodeResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||||
|
// -- Valider input --
|
||||||
|
let node_kind = req.node_kind.unwrap_or_else(|| "content".to_string());
|
||||||
|
if node_kind.is_empty() {
|
||||||
|
return Err(bad_request("node_kind kan ikke være tom"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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:?}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = req.title.unwrap_or_default();
|
||||||
|
let content = req.content.unwrap_or_default();
|
||||||
|
let metadata = req
|
||||||
|
.metadata
|
||||||
|
.unwrap_or_else(|| serde_json::json!({}));
|
||||||
|
let metadata_str = metadata.to_string();
|
||||||
|
|
||||||
|
// -- Generer UUIDv7 (tidssortert) --
|
||||||
|
let node_id = Uuid::now_v7();
|
||||||
|
let node_id_str = node_id.to_string();
|
||||||
|
let created_by_str = user.node_id.to_string();
|
||||||
|
|
||||||
|
// -- Skriv til SpacetimeDB (instant) --
|
||||||
|
state
|
||||||
|
.stdb
|
||||||
|
.create_node(
|
||||||
|
&node_id_str,
|
||||||
|
&node_kind,
|
||||||
|
&title,
|
||||||
|
&content,
|
||||||
|
&visibility,
|
||||||
|
&metadata_str,
|
||||||
|
&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}"),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
node_id = %node_id,
|
||||||
|
node_kind = %node_kind,
|
||||||
|
created_by = %user.node_id,
|
||||||
|
"Node opprettet i STDB"
|
||||||
|
);
|
||||||
|
|
||||||
|
// -- Spawn async PG-skriving --
|
||||||
|
spawn_pg_insert(
|
||||||
|
state.db.clone(),
|
||||||
|
node_id,
|
||||||
|
node_kind,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
visibility,
|
||||||
|
metadata,
|
||||||
|
user.node_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
// -- Returner node_id umiddelbart --
|
||||||
|
Ok(Json(CreateNodeResponse { node_id }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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(
|
||||||
|
db: PgPool,
|
||||||
|
node_id: Uuid,
|
||||||
|
node_kind: String,
|
||||||
|
title: String,
|
||||||
|
content: String,
|
||||||
|
visibility: String,
|
||||||
|
metadata: serde_json::Value,
|
||||||
|
created_by: Uuid,
|
||||||
|
) {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO nodes (id, node_kind, title, content, visibility, metadata, created_by)
|
||||||
|
VALUES ($1, $2, NULLIF($3, ''), NULLIF($4, ''), $5::visibility, $6, $7)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(node_id)
|
||||||
|
.bind(&node_kind)
|
||||||
|
.bind(&title)
|
||||||
|
.bind(&content)
|
||||||
|
.bind(&visibility)
|
||||||
|
.bind(&metadata)
|
||||||
|
.bind(created_by)
|
||||||
|
.execute(&db)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => {
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bad_request(msg: &str) -> (StatusCode, Json<ErrorResponse>) {
|
||||||
|
(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(ErrorResponse {
|
||||||
|
error: msg.to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
mod auth;
|
mod auth;
|
||||||
|
mod intentions;
|
||||||
mod stdb;
|
mod stdb;
|
||||||
mod warmup;
|
mod warmup;
|
||||||
|
|
||||||
use axum::{extract::State, http::StatusCode, routing::get, Json, Router};
|
use axum::{extract::State, http::StatusCode, routing::{get, post}, Json, Router};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sqlx::postgres::PgPoolOptions;
|
use sqlx::postgres::PgPoolOptions;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
@ -111,6 +112,7 @@ async fn main() {
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/health", get(health))
|
.route("/health", get(health))
|
||||||
.route("/me", get(me))
|
.route("/me", get(me))
|
||||||
|
.route("/intentions/create_node", post(intentions::create_node))
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
|
|
|
||||||
3
tasks.md
3
tasks.md
|
|
@ -54,8 +54,7 @@ Uavhengige faser kan fortsatt plukkes.
|
||||||
- [x] 2.1 Rust-prosjekt: opprett `maskinrommet/` med axum, tokio, sqlx (PG), serde. Dockerfile. Kompilerer og starter. Ref: `docs/retninger/maskinrommet.md`.
|
- [x] 2.1 Rust-prosjekt: opprett `maskinrommet/` med axum, tokio, sqlx (PG), serde. Dockerfile. Kompilerer og starter. Ref: `docs/retninger/maskinrommet.md`.
|
||||||
- [x] 2.2 Auth-middleware: valider Authentik JWT-tokens, slå opp `auth_identities` → node_id. Returner 401 for ugyldige tokens.
|
- [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.3 SpacetimeDB-klient i maskinrommet: koble til STDB, skriv noder og edges via reducers.
|
||||||
- [~] 2.4 Skrivestien: `POST /intentions/create_node` — valider, skriv STDB (instant), spawn async PG-skriving. Returner node_id umiddelbart.
|
- [x] 2.4 Skrivestien: `POST /intentions/create_node` — valider, skriv STDB (instant), spawn async PG-skriving. Returner node_id umiddelbart.
|
||||||
> Påbegynt: 2026-03-17T12:52
|
|
||||||
- [ ] 2.5 Flere intensjoner: `create_edge`, `update_node`, `delete_node`. Validering av tilgang (created_by eller owner/admin-edge).
|
- [ ] 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.
|
- [ ] 2.6 Docker Compose: legg maskinrommet inn i server-stacken. Intern nettverkstilgang til PG og STDB.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue