notegraf_web/routes/
api_v1.rs

1use crate::NoteType;
2use actix_web::{delete, get, post, web, HttpResponse, Responder};
3use notegraf::errors::NoteStoreError;
4use notegraf::notemetadata::NoteMetadataEditable;
5use notegraf::notestore::BoxedNoteStore;
6use notegraf::{NoteLocator, NoteSerializable};
7use serde::Deserialize;
8use std::collections::HashSet;
9
10fn notestore_error_handler(e: &NoteStoreError) -> HttpResponse {
11    match e {
12        NoteStoreError::NoteNotExist(_) => HttpResponse::NotFound().body(e.to_string()),
13        NoteStoreError::NoteIDConflict(_) => HttpResponse::Conflict().body(e.to_string()),
14        NoteStoreError::RevisionNotExist(_, _) => HttpResponse::NotFound().body(e.to_string()),
15        NoteStoreError::IOError(_) => {
16            error!("Note store internal error {:?}", e);
17            HttpResponse::InternalServerError().finish()
18        }
19        NoteStoreError::SerdeError(_) => HttpResponse::BadRequest().body(e.to_string()),
20        NoteStoreError::UpdateOldRevision(_, _) => HttpResponse::Conflict().body(e.to_string()),
21        NoteStoreError::DeleteOldRevision(_, _) => HttpResponse::Conflict().body(e.to_string()),
22        NoteStoreError::NotAChild(_, _) => HttpResponse::Conflict().body(e.to_string()),
23        NoteStoreError::ExistingNext(_, _) => HttpResponse::Conflict().body(e.to_string()),
24        NoteStoreError::HasBranches(_) => HttpResponse::Conflict().body(e.to_string()),
25        NoteStoreError::HasReferences(_) => HttpResponse::Conflict().body(e.to_string()),
26        NoteStoreError::ParseError(_) => HttpResponse::BadRequest().body(e.to_string()),
27        NoteStoreError::PostgreSQLError(_) => {
28            error!("Note store internal error {:?}", e);
29            HttpResponse::InternalServerError().finish()
30        }
31        NoteStoreError::NoteInnerError(_) => HttpResponse::BadRequest().body(e.to_string()),
32        NoteStoreError::NotUuid(_) => HttpResponse::BadRequest().body(e.to_string()),
33    }
34}
35
36async fn get_note_by_locator(
37    store: web::Data<BoxedNoteStore<NoteType>>,
38    loc: &NoteLocator,
39) -> impl Responder {
40    let result = store.as_ref().get_note(loc).await;
41    match result {
42        Ok(note) => HttpResponse::Ok().json(NoteSerializable::all_fields(note)),
43        Err(e) => notestore_error_handler(&e),
44    }
45}
46
47#[delete("/note/{note_id}")]
48#[instrument(
49    skip(store, params),
50    fields(
51        note_id = %params.0
52    )
53)]
54async fn delete_note_current(
55    store: web::Data<BoxedNoteStore<NoteType>>,
56    params: web::Path<(String,)>,
57) -> impl Responder {
58    let (note_id,) = params.into_inner();
59    let loc = NoteLocator::Current(note_id.into());
60    let res = store.delete_note(&loc).await;
61    match res {
62        Ok(_) => HttpResponse::Ok().finish(),
63        Err(e) => notestore_error_handler(&e),
64    }
65}
66
67#[get("/note/{note_id}/revision/{revision_id}")]
68#[instrument(
69    skip(store, params),
70    fields(
71        note_id = %params.0,
72        revision_id = %params.1
73    )
74)]
75async fn get_note_specific(
76    store: web::Data<BoxedNoteStore<NoteType>>,
77    params: web::Path<(String, String)>,
78) -> impl Responder {
79    let (note_id, revision_id) = params.into_inner();
80    let loc = NoteLocator::Specific(note_id.into(), revision_id.into());
81    get_note_by_locator(store, &loc).await
82}
83
84#[derive(Deserialize)]
85struct NotePostData {
86    title: String,
87    note_inner: String,
88    metadata_tags: String,
89    metadata_custom_metadata: String,
90}
91
92struct NoteStoreEditArgument {
93    title: String,
94    note_inner: NoteType,
95    metadata: NoteMetadataEditable,
96}
97
98impl TryFrom<NotePostData> for NoteStoreEditArgument {
99    type Error = String;
100
101    fn try_from(note: NotePostData) -> Result<Self, Self::Error> {
102        let custom_metadata =
103            serde_json::from_str(&note.metadata_custom_metadata).map_err(|e| e.to_string())?;
104        let tags: HashSet<String> = HashSet::from_iter(
105            note.metadata_tags
106                .split(',')
107                .into_iter()
108                .map(|tag| tag.trim().to_owned())
109                .filter(|tag| !tag.is_empty()),
110        );
111        Ok(NoteStoreEditArgument {
112            title: note.title,
113            note_inner: NoteType::from(note.note_inner),
114            metadata: NoteMetadataEditable {
115                tags: Some(tags),
116                custom_metadata: Some(custom_metadata),
117            },
118        })
119    }
120}
121
122#[post("/note")]
123#[instrument(skip(store, note))]
124async fn new_note(
125    store: web::Data<BoxedNoteStore<NoteType>>,
126    note: web::Json<NotePostData>,
127) -> impl Responder {
128    let note: Result<NoteStoreEditArgument, String> = note.into_inner().try_into();
129    if let Err(e) = note {
130        return HttpResponse::BadRequest().body(e);
131    }
132    let note = note.unwrap();
133    let res = store
134        .new_note(note.title, note.note_inner, note.metadata)
135        .await;
136    match res {
137        Ok(loc) => HttpResponse::Ok().json(loc),
138        Err(e) => notestore_error_handler(&e),
139    }
140}
141
142#[get("/note/{note_id}/revision")]
143#[instrument(
144    skip(store, params),
145    fields(
146        note_id = %params.0
147    )
148)]
149async fn get_revisions(
150    store: web::Data<BoxedNoteStore<NoteType>>,
151    params: web::Path<(String,)>,
152) -> impl Responder {
153    let (note_id,) = params.into_inner();
154    let loc = NoteLocator::Current(note_id.into());
155    let res = store.get_revisions(&loc).await;
156    if let Err(e) = res {
157        return notestore_error_handler(&e);
158    }
159    let revisions: Vec<NoteSerializable<NoteType>> = res
160        .unwrap()
161        .into_iter()
162        .map(|x| NoteSerializable::all_fields(x))
163        .collect();
164    HttpResponse::Ok().json(revisions)
165}
166
167#[post("/note/{note_id}/revision")]
168#[instrument(
169    skip(store, params, note),
170    fields(
171        note_id = %params.0
172    )
173)]
174async fn update_note(
175    store: web::Data<BoxedNoteStore<NoteType>>,
176    params: web::Path<(String,)>,
177    note: web::Json<NotePostData>,
178) -> impl Responder {
179    let (note_id,) = params.into_inner();
180    let loc = NoteLocator::Current(note_id.into());
181    let note: Result<NoteStoreEditArgument, String> = note.into_inner().try_into();
182    if let Err(e) = note {
183        return HttpResponse::BadRequest().body(e);
184    }
185    let note = note.unwrap();
186    let res = store
187        .update_note(&loc, Some(note.title), Some(note.note_inner), note.metadata)
188        .await;
189    match res {
190        Ok(loc) => HttpResponse::Ok().json(loc),
191        Err(e) => notestore_error_handler(&e),
192    }
193}
194
195#[post("/note/{note_id}/branch")]
196#[instrument(
197    skip(store, params, note),
198    fields(
199      note_id = %params.0
200    )
201)]
202async fn new_branch(
203    store: web::Data<BoxedNoteStore<NoteType>>,
204    params: web::Path<(String,)>,
205    note: web::Json<NotePostData>,
206) -> impl Responder {
207    let (note_id,) = params.into_inner();
208    let loc = NoteLocator::Current(note_id.into());
209    let note: Result<NoteStoreEditArgument, String> = note.into_inner().try_into();
210    if let Err(e) = note {
211        return HttpResponse::BadRequest().body(e);
212    }
213    let note = note.unwrap();
214    let res = store
215        .add_branch(loc.get_id(), note.title, note.note_inner, note.metadata)
216        .await;
217    match res {
218        Ok(loc_child) => HttpResponse::Ok().json(loc_child),
219        Err(e) => notestore_error_handler(&e),
220    }
221}
222
223#[post("/note/{note_id}/next")]
224#[instrument(
225    skip(store, params, note),
226    fields(
227        note_id = %params.0
228    )
229)]
230async fn new_next(
231    store: web::Data<BoxedNoteStore<NoteType>>,
232    params: web::Path<(String,)>,
233    note: web::Json<NotePostData>,
234) -> impl Responder {
235    let (note_id,) = params.into_inner();
236    let loc = NoteLocator::Current(note_id.into());
237    let note: Result<NoteStoreEditArgument, String> = note.into_inner().try_into();
238    if let Err(e) = note {
239        return HttpResponse::BadRequest().body(e);
240    }
241    let note = note.unwrap();
242    let res = store
243        .append_note(loc.get_id(), note.title, note.note_inner, note.metadata)
244        .await;
245    match res {
246        Ok(loc_next) => HttpResponse::Ok().json(loc_next),
247        Err(e) => notestore_error_handler(&e),
248    }
249}
250
251#[get("/note/{note_id}")]
252#[instrument(
253    skip(store, params),
254    fields(
255        note_id = %params.0
256    )
257)]
258async fn get_note_current(
259    store: web::Data<BoxedNoteStore<NoteType>>,
260    params: web::Path<(String,)>,
261) -> impl Responder {
262    let (note_id,) = params.into_inner();
263    let loc = NoteLocator::Current(note_id.into());
264    get_note_by_locator(store, &loc).await
265}
266
267#[derive(Deserialize, Debug)]
268struct SearchQuery {
269    query: Option<String>,
270}
271
272#[get("/note")]
273#[instrument(skip(store, search))]
274async fn search(
275    store: web::Data<BoxedNoteStore<NoteType>>,
276    search: web::Query<SearchQuery>,
277) -> impl Responder {
278    let search = search.into_inner();
279    let query = search.query;
280    let res = if let Some(q) = query {
281        store.search(&q.into()).await
282    } else {
283        store.search(&"".to_owned().into()).await
284    };
285    if let Err(e) = res {
286        return notestore_error_handler(&e);
287    }
288    let revisions: Vec<NoteSerializable<NoteType>> = res
289        .unwrap()
290        .into_iter()
291        .map(|x| NoteSerializable::all_fields(x))
292        .collect();
293    HttpResponse::Ok().json(revisions)
294}
295
296#[get("/tags")]
297#[instrument(skip(store))]
298async fn get_tags(store: web::Data<BoxedNoteStore<NoteType>>) -> impl Responder {
299    let res = store.tags().await;
300
301    if let Err(e) = res {
302        return notestore_error_handler(&e);
303    }
304    HttpResponse::Ok().json(res.unwrap())
305}
306
307pub fn config(cfg: &mut web::ServiceConfig) {
308    cfg.service(get_note_current)
309        .service(get_note_specific)
310        .service(new_note)
311        .service(delete_note_current)
312        .service(update_note)
313        .service(get_revisions)
314        .service(search)
315        .service(new_branch)
316        .service(new_next)
317        .service(get_tags);
318}