Skip to main content

routa_server/api/
notes.rs

1use axum::{
2    extract::{Query, State},
3    response::sse::{Event, KeepAlive, Sse},
4    routing::get,
5    Json, Router,
6};
7use serde::Deserialize;
8use std::convert::Infallible;
9use tokio_stream::StreamExt as _;
10
11use crate::error::ServerError;
12use crate::models::note::{Note, NoteMetadata, NoteType};
13use crate::state::AppState;
14
15pub fn router() -> Router<AppState> {
16    Router::new()
17        .route(
18            "/",
19            get(list_notes)
20                .post(create_or_update_note)
21                .delete(delete_note_query),
22        )
23        .route("/events", get(note_events_sse))
24        .route(
25            "/{workspace_id}/{note_id}",
26            get(get_note).delete(delete_note_path),
27        )
28}
29
30#[derive(Debug, Deserialize)]
31#[serde(rename_all = "camelCase")]
32struct ListNotesQuery {
33    workspace_id: Option<String>,
34    #[serde(rename = "type")]
35    note_type: Option<String>,
36    note_id: Option<String>,
37}
38
39async fn list_notes(
40    State(state): State<AppState>,
41    Query(query): Query<ListNotesQuery>,
42) -> Result<Json<serde_json::Value>, ServerError> {
43    let workspace_id = query.workspace_id.as_deref().unwrap_or("default");
44
45    if let Some(note_id) = &query.note_id {
46        let note = state.note_store.get(note_id, workspace_id).await?;
47        return Ok(Json(serde_json::json!({ "note": note })));
48    }
49
50    let notes = if let Some(type_str) = &query.note_type {
51        let note_type = NoteType::from_str(type_str);
52        state
53            .note_store
54            .list_by_type(workspace_id, &note_type)
55            .await?
56    } else {
57        state.note_store.list_by_workspace(workspace_id).await?
58    };
59
60    Ok(Json(serde_json::json!({ "notes": notes })))
61}
62
63async fn get_note(
64    State(state): State<AppState>,
65    axum::extract::Path((workspace_id, note_id)): axum::extract::Path<(String, String)>,
66) -> Result<Json<serde_json::Value>, ServerError> {
67    let note = state
68        .note_store
69        .get(&note_id, &workspace_id)
70        .await?
71        .ok_or_else(|| ServerError::NotFound(format!("Note {note_id} not found")))?;
72    Ok(Json(serde_json::json!({ "note": note })))
73}
74
75#[derive(Debug, Deserialize)]
76#[serde(rename_all = "camelCase")]
77struct CreateNoteRequest {
78    note_id: Option<String>,
79    title: String,
80    content: Option<String>,
81    workspace_id: Option<String>,
82    #[serde(rename = "type")]
83    note_type: Option<String>,
84    metadata: Option<NoteMetadata>,
85    #[allow(dead_code)]
86    source: Option<String>,
87}
88
89async fn create_or_update_note(
90    State(state): State<AppState>,
91    Json(body): Json<CreateNoteRequest>,
92) -> Result<Json<serde_json::Value>, ServerError> {
93    let workspace_id = body.workspace_id.unwrap_or_else(|| "default".to_string());
94    let note_id = body
95        .note_id
96        .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
97
98    let metadata = body.metadata.unwrap_or(NoteMetadata {
99        note_type: body
100            .note_type
101            .as_deref()
102            .map(NoteType::from_str)
103            .unwrap_or(NoteType::General),
104        ..Default::default()
105    });
106
107    let note = Note::new(
108        note_id,
109        body.title,
110        body.content.unwrap_or_default(),
111        workspace_id,
112        Some(metadata),
113    );
114
115    state.note_store.save(&note).await?;
116    Ok(Json(serde_json::json!({ "note": note })))
117}
118
119/// DELETE /api/notes?noteId=xxx&workspaceId=xxx  (Next.js compatible)
120#[derive(Debug, Deserialize)]
121#[serde(rename_all = "camelCase")]
122struct DeleteNoteQuery {
123    note_id: String,
124    workspace_id: Option<String>,
125}
126
127async fn delete_note_query(
128    State(state): State<AppState>,
129    Query(query): Query<DeleteNoteQuery>,
130) -> Result<Json<serde_json::Value>, ServerError> {
131    let workspace_id = query.workspace_id.as_deref().unwrap_or("default");
132    state
133        .note_store
134        .delete(&query.note_id, workspace_id)
135        .await?;
136    Ok(Json(
137        serde_json::json!({ "deleted": true, "noteId": query.note_id }),
138    ))
139}
140
141/// DELETE /api/notes/{workspace_id}/{note_id}  (REST-style)
142async fn delete_note_path(
143    State(state): State<AppState>,
144    axum::extract::Path((workspace_id, note_id)): axum::extract::Path<(String, String)>,
145) -> Result<Json<serde_json::Value>, ServerError> {
146    state.note_store.delete(&note_id, &workspace_id).await?;
147    Ok(Json(
148        serde_json::json!({ "deleted": true, "noteId": note_id }),
149    ))
150}
151
152/// GET /api/notes/events?workspaceId=xxx — SSE stream for note change events.
153///
154/// Currently sends a heartbeat every 15 seconds.
155/// TODO: Integrate with a real event bus (broadcast channel) when notes are modified.
156#[derive(Debug, Deserialize)]
157#[serde(rename_all = "camelCase")]
158struct NoteEventsQuery {
159    #[allow(dead_code)]
160    workspace_id: Option<String>,
161}
162
163async fn note_events_sse(
164    Query(_query): Query<NoteEventsQuery>,
165) -> Sse<impl tokio_stream::Stream<Item = Result<Event, Infallible>>> {
166    let stream = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
167        std::time::Duration::from_secs(15),
168    ))
169    .map(|_| Ok(Event::default().comment("heartbeat")));
170
171    Sse::new(stream).keep_alive(KeepAlive::default())
172}