2025-06-18 19:21:01 -04:00
use oiseau ::cache ::Cache ;
2025-06-19 16:19:57 -04:00
use crate ::database ::common ::NAME_REGEX ;
2025-06-18 19:21:01 -04:00
use crate ::model ::{ auth ::User , journals ::Note , permissions ::FinePermission , Error , Result } ;
use crate ::{ auto_method , DataManager } ;
2025-06-19 15:48:04 -04:00
use oiseau ::{ execute , get , params , query_row , query_rows , PostgresRow } ;
2025-06-18 19:21:01 -04:00
impl DataManager {
/// Get a [`Note`] from an SQL row.
pub ( crate ) fn get_note_from_row ( x : & PostgresRow ) -> Note {
Note {
id : get ! ( x ->0 ( i64 ) ) as usize ,
created : get ! ( x ->1 ( i64 ) ) as usize ,
owner : get ! ( x ->2 ( i64 ) ) as usize ,
title : get ! ( x ->3 ( String ) ) ,
journal : get ! ( x ->4 ( i64 ) ) as usize ,
content : get ! ( x ->5 ( String ) ) ,
edited : get ! ( x ->6 ( i64 ) ) as usize ,
}
}
auto_method! ( get_note_by_id ( usize as i64 ) @ get_note_from_row -> " SELECT * FROM notes WHERE id = $1 " - - name = " note " - - returns = Note - - cache - key - tmpl = " atto.note:{} " ) ;
2025-06-19 15:48:04 -04:00
/// Get a note by `journal` and `title`.
pub async fn get_note_by_journal_title ( & self , journal : usize , title : & str ) -> Result < Note > {
let conn = match self . 0. connect ( ) . await {
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = query_row! (
& conn ,
" SELECT * FROM notes WHERE journal = $1 AND title = $2 " ,
params! [ & ( journal as i64 ) , & title ] ,
| x | { Ok ( Self ::get_note_from_row ( x ) ) }
) ;
if res . is_err ( ) {
return Err ( Error ::GeneralNotFound ( " note " . to_string ( ) ) ) ;
}
Ok ( res . unwrap ( ) )
}
2025-06-18 19:21:01 -04:00
/// Get all notes by journal.
///
/// # Arguments
/// * `id` - the ID of the journal to fetch notes for
pub async fn get_notes_by_journal ( & self , id : usize ) -> Result < Vec < Note > > {
let conn = match self . 0. connect ( ) . await {
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = query_rows! (
& conn ,
2025-06-19 15:48:04 -04:00
" SELECT * FROM notes WHERE journal = $1 ORDER BY edited DESC " ,
2025-06-18 19:21:01 -04:00
& [ & ( id as i64 ) ] ,
| x | { Self ::get_note_from_row ( x ) }
) ;
if res . is_err ( ) {
return Err ( Error ::GeneralNotFound ( " note " . to_string ( ) ) ) ;
}
Ok ( res . unwrap ( ) )
}
/// Create a new note in the database.
///
/// # Arguments
/// * `data` - a mock [`Note`] object to insert
2025-06-19 15:48:04 -04:00
pub async fn create_note ( & self , mut data : Note ) -> Result < Note > {
2025-06-18 19:21:01 -04:00
// check values
if data . title . len ( ) < 2 {
return Err ( Error ::DataTooShort ( " title " . to_string ( ) ) ) ;
} else if data . title . len ( ) > 64 {
return Err ( Error ::DataTooLong ( " title " . to_string ( ) ) ) ;
}
if data . content . len ( ) < 2 {
return Err ( Error ::DataTooShort ( " content " . to_string ( ) ) ) ;
2025-06-19 15:52:46 -04:00
} else if data . content . len ( ) > 262144 {
2025-06-18 19:21:01 -04:00
return Err ( Error ::DataTooLong ( " content " . to_string ( ) ) ) ;
}
2025-06-19 15:48:04 -04:00
data . title = data . title . replace ( " " , " _ " ) ;
2025-06-19 16:19:57 -04:00
// check name
let regex = regex ::RegexBuilder ::new ( NAME_REGEX )
. multi_line ( true )
. build ( )
. unwrap ( ) ;
if regex . captures ( & data . title ) . is_some ( ) {
return Err ( Error ::MiscError (
" This title contains invalid characters " . to_string ( ) ,
) ) ;
}
2025-06-19 15:48:04 -04:00
// make sure this title isn't already in use
if self
. get_note_by_journal_title ( data . journal , & data . title )
. await
. is_ok ( )
{
return Err ( Error ::TitleInUse ) ;
}
// check permission
let journal = self . get_journal_by_id ( data . journal ) . await ? ;
if data . owner ! = journal . owner {
return Err ( Error ::NotAllowed ) ;
}
2025-06-18 19:21:01 -04:00
// ...
let conn = match self . 0. connect ( ) . await {
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = execute! (
& conn ,
" INSERT INTO notes VALUES ($1, $2, $3, $4, $5, $6, $7) " ,
params! [
& ( data . id as i64 ) ,
& ( data . created as i64 ) ,
& ( data . owner as i64 ) ,
& data . title ,
& ( data . journal as i64 ) ,
& data . content ,
& ( data . edited as i64 ) ,
]
) ;
if let Err ( e ) = res {
return Err ( Error ::DatabaseError ( e . to_string ( ) ) ) ;
}
Ok ( data )
}
pub async fn delete_note ( & self , id : usize , user : & User ) -> Result < ( ) > {
let note = self . get_note_by_id ( id ) . await ? ;
// check user permission
if user . id ! = note . owner & & ! user . permissions . check ( FinePermission ::MANAGE_NOTES ) {
return Err ( Error ::NotAllowed ) ;
}
// ...
let conn = match self . 0. connect ( ) . await {
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = execute! ( & conn , " DELETE FROM notes WHERE id = $1 " , & [ & ( id as i64 ) ] ) ;
if let Err ( e ) = res {
return Err ( Error ::DatabaseError ( e . to_string ( ) ) ) ;
}
// ...
self . 0. 1. remove ( format! ( " atto.note: {} " , id ) ) . await ;
Ok ( ( ) )
}
auto_method! ( update_note_title ( & str ) @ get_note_by_id :MANAGE_NOTES -> " UPDATE notes SET title = $1 WHERE id = $2 " - - cache - key - tmpl = " atto.note:{} " ) ;
2025-06-18 21:00:07 -04:00
auto_method! ( update_note_content ( & str ) @ get_note_by_id :MANAGE_NOTES -> " UPDATE notes SET content = $1 WHERE id = $2 " - - cache - key - tmpl = " atto.note:{} " ) ;
2025-06-19 15:48:04 -04:00
auto_method! ( update_note_edited ( i64 ) -> " UPDATE notes SET edited = $1 WHERE id = $2 " - - cache - key - tmpl = " atto.note:{} " ) ;
2025-06-18 19:21:01 -04:00
}