routa_server/api/
notes.rs1use 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, ¬e_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(¬e_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(¬e).await?;
116 Ok(Json(serde_json::json!({ "note": note })))
117}
118
119#[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
141async 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(¬e_id, &workspace_id).await?;
147 Ok(Json(
148 serde_json::json!({ "deleted": true, "noteId": note_id }),
149 ))
150}
151
152#[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}