diff --git a/server/nm-store/src/lib.rs b/server/nm-store/src/lib.rs index 4b0ec21..1ec2e1a 100644 --- a/server/nm-store/src/lib.rs +++ b/server/nm-store/src/lib.rs @@ -7,7 +7,6 @@ pub use crate::errors::NoteStoreError; pub use crate::store::NoteStore; pub use crate::structs::{Note, NoteKind, NoteRelationship, PageRelationship}; - #[cfg(test)] mod tests { use super::*; @@ -113,6 +112,4 @@ mod tests { assert_eq!(newpages[1].parent_id, Some(newroot.id.clone())); assert_eq!(newpages[2].parent_id, Some(newpages[1].id.clone())); } - - } diff --git a/server/nm-store/src/store/private.rs b/server/nm-store/src/store/private.rs index 559eb38..10a9d06 100644 --- a/server/nm-store/src/store/private.rs +++ b/server/nm-store/src/store/private.rs @@ -3,8 +3,8 @@ use lazy_static::lazy_static; use regex::Regex; use slug::slugify; use sqlx::{sqlite::Sqlite, Acquire, Done, Executor, Transaction}; -use std::collections::HashSet; use std::cmp; +use std::collections::HashSet; type SqlResult = sqlx::Result; @@ -20,25 +20,24 @@ type SqlResult = sqlx::Result; // of the SQL queries. lazy_static! { - static ref SELECT_PAGE_BY_TITLE_SQL: String = str::replace( - include_str!("sql/select_notes_by_parameter.sql"), - "QUERYPARAMETER", - "notes.content" - ); + static ref SELECT_PAGE_BY_TITLE_SQL: String = str::replace( + include_str!("sql/select_notes_by_parameter.sql"), + "QUERYPARAMETER", + "notes.content" + ); } lazy_static! { - static ref SELECT_PAGE_BY_ID_SQL: String = str::replace( - include_str!("sql/select_notes_by_parameter.sql"), - "QUERYPARAMETER", - "notes.id" - ); + static ref SELECT_PAGE_BY_ID_SQL: String = str::replace( + include_str!("sql/select_notes_by_parameter.sql"), + "QUERYPARAMETER", + "notes.id" + ); } - lazy_static! { - static ref SELECT_NOTES_BACKREFERENCING_PAGE_SQL: &'static str = - include_str!("sql/select_notes_backreferencing_page.sql"); + static ref SELECT_NOTES_BACKREFERENCING_PAGE_SQL: &'static str = + include_str!("sql/select_notes_backreferencing_page.sql"); } // ___ _ @@ -49,13 +48,10 @@ lazy_static! { pub(crate) async fn reset_database<'a, E>(executor: E) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let initialize_sql = include_str!("sql/initialize_database.sql"); - sqlx::query(initialize_sql) - .execute(executor) - .await - .map(|_| ()) + let initialize_sql = include_str!("sql/initialize_database.sql"); + sqlx::query(initialize_sql).execute(executor).await.map(|_| ()) } // ___ _ _ _ __ _ @@ -68,16 +64,12 @@ where // SQL operations are quite different between the first two and the last. async fn select_object_by_query<'a, E>(executor: E, query: &str, field: &str) -> SqlResult> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let r: Vec = sqlx::query_as(query) - .bind(field) - .fetch_all(executor) - .await?; - Ok(r.into_iter().map(|z| Note::from(z)).collect()) + let r: Vec = sqlx::query_as(query).bind(field).fetch_all(executor).await?; + Ok(r.into_iter().map(|z| Note::from(z)).collect()) } - // Select the requested page via its id. This is fairly rare; // pages should usually be picked up via their title, but if you're // navigating to an instance, this is how you specify the page in a @@ -89,9 +81,9 @@ where // page. pub(crate) async fn select_page_by_slug<'a, E>(executor: E, slug: &str) -> SqlResult> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - select_object_by_query(executor, &SELECT_PAGE_BY_ID_SQL, &slug).await + select_object_by_query(executor, &SELECT_PAGE_BY_ID_SQL, &slug).await } // Fetch the page by title. The return value is an array of Note @@ -99,23 +91,20 @@ where // these into a tree-like object. pub(crate) async fn select_page_by_title<'a, E>(executor: E, title: &str) -> SqlResult> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - select_object_by_query(executor, &SELECT_PAGE_BY_TITLE_SQL, &title).await + select_object_by_query(executor, &SELECT_PAGE_BY_TITLE_SQL, &title).await } // Fetch all backreferences to a page. The return value is an array // of arrays, and inside each array is a list from a root page to // the note that references the give page. Clients may choose how // they want to display that collection. -pub(crate) async fn select_backreferences_for_page<'a, E>( - executor: E, - page_id: &str, -) -> SqlResult> +pub(crate) async fn select_backreferences_for_page<'a, E>(executor: E, page_id: &str) -> SqlResult> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - select_object_by_query(executor, &SELECT_NOTES_BACKREFERENCING_PAGE_SQL, &page_id).await + select_object_by_query(executor, &SELECT_NOTES_BACKREFERENCING_PAGE_SQL, &page_id).await } // ___ _ ___ _ _ _ @@ -127,53 +116,52 @@ where // Inserts a single note into the notes table. That is all. pub(crate) async fn insert_note<'a, E>(executor: E, note: &NewNote) -> SqlResult where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let insert_one_note_sql = concat!( - "INSERT INTO notes (id, content, kind, ", - " creation_date, updated_date, lastview_date) ", - "VALUES (?, ?, ?, ?, ?, ?);" - ); + let insert_one_note_sql = concat!( + "INSERT INTO notes (id, content, kind, ", + " creation_date, updated_date, lastview_date) ", + "VALUES (?, ?, ?, ?, ?, ?);" + ); - let _ = sqlx::query(insert_one_note_sql) - .bind(¬e.id) - .bind(¬e.content) - .bind(note.kind.to_string()) - .bind(¬e.creation_date) - .bind(¬e.updated_date) - .bind(¬e.lastview_date) - .execute(executor) - .await?; - Ok(note.id.clone()) + let _ = sqlx::query(insert_one_note_sql) + .bind(¬e.id) + .bind(¬e.content) + .bind(note.kind.to_string()) + .bind(¬e.creation_date) + .bind(¬e.updated_date) + .bind(¬e.lastview_date) + .execute(executor) + .await?; + Ok(note.id.clone()) } // Inserts a single note into the notes table. That is all. -pub(crate) async fn bulk_insert_notes<'a, E>(executor: E, notes: &[NewNote]) -> SqlResult<()> +pub(crate) async fn insert_bulk_notes<'a, E>(executor: E, notes: &[NewNote]) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - if notes.is_empty() { - return Ok(()); - } - - let insert_pattern = "VALUES (?, ?, ?, ?, ?, ?)".to_string(); - let insert_bulk_notes_sql = - "INSERT INTO notes (id, content, kind, creation_date, updated_date, lastview_date) ".to_string() - + &[insert_pattern.as_str()] - .repeat(notes.len()) - .join(", ") + &";".to_string(); + if notes.is_empty() { + return Ok(()); + } - let mut request = sqlx::query(&insert_bulk_notes_sql); - for note in notes { - request = request - .bind(¬e.id) - .bind(¬e.content) - .bind(note.kind.to_string()) - .bind(¬e.creation_date) - .bind(¬e.updated_date) - .bind(¬e.lastview_date); - } - request.execute(executor).await.map(|_| ()) + let insert_pattern = "VALUES (?, ?, ?, ?, ?, ?)".to_string(); + let insert_bulk_notes_sql = "INSERT INTO notes (id, content, kind, creation_date, updated_date, lastview_date) " + .to_string() + + &[insert_pattern.as_str()].repeat(notes.len()).join(", ") + + &";".to_string(); + + let mut request = sqlx::query(&insert_bulk_notes_sql); + for note in notes { + request = request + .bind(¬e.id) + .bind(¬e.content) + .bind(note.kind.to_string()) + .bind(¬e.creation_date) + .bind(¬e.updated_date) + .bind(¬e.lastview_date); + } + request.execute(executor).await.map(|_| ()) } // ___ _ _ _ _ __ _ @@ -185,21 +173,21 @@ where // Given a possible slug, find the slug with the highest // uniquification number, and return that number, if any. pub(crate) fn find_maximal_slug_number(slugs: &[JustId]) -> Option { - lazy_static! { - static ref RE_CAP_NUM: Regex = Regex::new(r"-(\d+)$").unwrap(); - } + lazy_static! { + static ref RE_CAP_NUM: Regex = Regex::new(r"-(\d+)$").unwrap(); + } - if slugs.is_empty() { - return None; - } + if slugs.is_empty() { + return None; + } - let mut slug_counters: Vec = slugs - .iter() - .filter_map(|slug| RE_CAP_NUM.captures(&slug.id)) - .map(|cap| cap.get(1).unwrap().as_str().parse::().unwrap()) - .collect(); - slug_counters.sort_unstable(); - slug_counters.pop() + let mut slug_counters: Vec = slugs + .iter() + .filter_map(|slug| RE_CAP_NUM.captures(&slug.id)) + .map(|cap| cap.get(1).unwrap().as_str().parse::().unwrap()) + .collect(); + slug_counters.sort_unstable(); + slug_counters.pop() } // Given an initial string and an existing collection of slugs, @@ -208,38 +196,38 @@ pub(crate) fn find_maximal_slug_number(slugs: &[JustId]) -> Option { // isn't all that. pub(crate) async fn generate_slug<'a, E>(executor: E, title: &str) -> SqlResult where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - lazy_static! { - static ref RE_STRIP_NUM: Regex = Regex::new(r"-\d+$").unwrap(); - static ref SLUG_FINDER_SQL: String = format!( - "SELECT id FROM notes WHERE kind = '{}' AND id LIKE '?%';", - NoteKind::Page.to_string() - ); - } + lazy_static! { + static ref RE_STRIP_NUM: Regex = Regex::new(r"-\d+$").unwrap(); + static ref SLUG_FINDER_SQL: String = format!( + "SELECT id FROM notes WHERE kind = '{}' AND id LIKE '?%';", + NoteKind::Page.to_string() + ); + } - let initial_slug = slugify(title); - let sample_slug = RE_STRIP_NUM.replace_all(&initial_slug, ""); - let similar_slugs: Vec = sqlx::query_as(&SLUG_FINDER_SQL) - .bind(&*sample_slug) - .fetch_all(executor) - .await?; - let maximal_slug_number = find_maximal_slug_number(&similar_slugs); - Ok(match maximal_slug_number { - None => initial_slug, - Some(slug_number) => format!("{}-{}", initial_slug, slug_number + 1), - }) + let initial_slug = slugify(title); + let sample_slug = RE_STRIP_NUM.replace_all(&initial_slug, ""); + let similar_slugs: Vec = sqlx::query_as(&SLUG_FINDER_SQL) + .bind(&*sample_slug) + .fetch_all(executor) + .await?; + let maximal_slug_number = find_maximal_slug_number(&similar_slugs); + Ok(match maximal_slug_number { + None => initial_slug, + Some(slug_number) => format!("{}-{}", initial_slug, slug_number + 1), + }) } // A helper function: given a title and a slug, create a PageType // note. pub(crate) fn create_page(title: &str, slug: &str) -> NewNote { - NewNoteBuilder::default() - .id(slug.to_string()) - .content(title.to_string()) - .kind(NoteKind::Page) - .build() - .unwrap() + NewNoteBuilder::default() + .id(slug.to_string()) + .content(title.to_string()) + .kind(NoteKind::Page) + .build() + .unwrap() } // _ _ _ _ ___ _ _ _ @@ -248,26 +236,22 @@ pub(crate) fn create_page(title: &str, slug: &str) -> NewNote { // \___/| .__/\__,_\__,_|\__\___| \___/|_||_\___| |_|\_\___/\__\___| // |_| -pub(crate) async fn update_note_content<'a, E>( - executor: E, - note_id: &NoteId, - content: &str, -) -> SqlResult<()> +pub(crate) async fn update_note_content<'a, E>(executor: E, note_id: &str, content: &str) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let update_note_content_sql = "UPDATE notes SET content = ? WHERE note_id = ?"; - let count = sqlx::query(update_note_content_sql) - .bind(content) - .bind(&**note_id) - .execute(executor) - .await? - .rows_affected(); + let update_note_content_sql = "UPDATE notes SET content = ? WHERE note_id = ?"; + let count = sqlx::query(update_note_content_sql) + .bind(content) + .bind(&**note_id) + .execute(executor) + .await? + .rows_affected(); - match count { - 1 => Ok(()), - _ => Err(sqlx::Error::RowNotFound), - } + match count { + 1 => Ok(()), + _ => Err(sqlx::Error::RowNotFound), + } } // ___ _ _ ___ _ _ _ ___ _ _ _ _ _ @@ -277,25 +261,25 @@ where // |_| pub(crate) async fn select_note_to_note_relationship<'a, E>( - executor: E, - parent_id: &str, - note_id: &str, + executor: E, + parent_id: &str, + note_id: &str, ) -> SqlResult where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let get_note_to_note_relationship_sql = concat!( - "SELECT parent_id, note_id, location, kind ", - "FROM note_relationships ", - "WHERE parent_id = ? and note_id = ? ", - "LIMIT 1" - ); - let s: NoteRelationshipRow = sqlx::query_as(get_note_to_note_relationship_sql) - .bind(parent_id) - .bind(note_id) - .fetch_one(executor) - .await?; - Ok(NoteRelationship::from(s)) + let get_note_to_note_relationship_sql = concat!( + "SELECT parent_id, note_id, location, kind ", + "FROM note_relationships ", + "WHERE parent_id = ? and note_id = ? ", + "LIMIT 1" + ); + let s: NoteRelationshipRow = sqlx::query_as(get_note_to_note_relationship_sql) + .bind(parent_id) + .bind(note_id) + .fetch_one(executor) + .await?; + Ok(NoteRelationship::from(s)) } // _ _ _ _ _ _ _ ___ _ _ _ _ _ @@ -305,83 +289,80 @@ where // |_| pub(crate) async fn insert_note_to_note_relationship<'a, E>( - executor: E, - parent_id: &str, - note_id: &str, - location: i64, - kind: &RelationshipKind, + executor: E, + parent_id: &str, + note_id: &str, + location: i64, + kind: &RelationshipKind, ) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let insert_note_to_note_relationship_sql = concat!( - "INSERT INTO note_relationships (parent_id, note_id, location, kind) ", - "values (?, ?, ?, ?)" - ); + let insert_note_to_note_relationship_sql = concat!( + "INSERT INTO note_relationships (parent_id, note_id, location, kind) ", + "values (?, ?, ?, ?)" + ); - let _ = sqlx::query(insert_note_to_note_relationship_sql) - .bind(parent_id) - .bind(note_id) - .bind(&location) - .bind(kind.to_string()) - .execute(executor) - .await?; - Ok(()) + let _ = sqlx::query(insert_note_to_note_relationship_sql) + .bind(parent_id) + .bind(note_id) + .bind(&location) + .bind(kind.to_string()) + .execute(executor) + .await?; + Ok(()) } -pub(crate) async fn make_room_for_new_note<'a, E>( - executor: E, - parent_id: &str, - location: i64, +pub(crate) async fn make_room_for_new_note_relationship<'a, E>( + executor: E, + parent_id: &str, + location: i64, ) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let make_room_for_new_note_sql = concat!( - "UPDATE note_relationships ", - "SET location = location + 1 ", - "WHERE location >= ? and parent_id = ?;" - ); + let make_room_for_new_note_sql = concat!( + "UPDATE note_relationships ", + "SET location = location + 1 ", + "WHERE location >= ? and parent_id = ?;" + ); - let _ = sqlx::query(make_room_for_new_note_sql) - .bind(&location) - .bind(parent_id) - .execute(executor) - .await?; - Ok(()) + let _ = sqlx::query(make_room_for_new_note_sql) + .bind(&location) + .bind(parent_id) + .execute(executor) + .await?; + Ok(()) } pub(crate) async fn determine_max_child_location_for_note<'a, E>( - executor: E, - note_id: &str, - comp_loc: Option, + executor: E, + note_id: &str, + comp_loc: Option, ) -> SqlResult where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let row_count = assert_max_child_location_for_note(executor, note_id).await? + 1; - Ok(match comp_loc { - Some(location) => cmp::min(row_count, location), - None => row_count - }) + let row_count = assert_max_child_location_for_note(executor, note_id).await? + 1; + Ok(match comp_loc { + Some(location) => cmp::min(row_count, location), + None => row_count, + }) } -pub(crate) async fn assert_max_child_location_for_note<'a, E>( - executor: E, - note_id: &str, -) -> SqlResult +pub(crate) async fn assert_max_child_location_for_note<'a, E>(executor: E, note_id: &str) -> SqlResult where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let assert_max_child_location_for_note_sql = - "SELECT MAX(location) AS count FROM note_relationships WHERE parent_id = ?;"; + let assert_max_child_location_for_note_sql = + "SELECT MAX(location) AS count FROM note_relationships WHERE parent_id = ?;"; - let count: RowCount = sqlx::query_as(assert_max_child_location_for_note_sql) - .bind(note_id) - .fetch_one(executor) - .await?; + let count: RowCount = sqlx::query_as(assert_max_child_location_for_note_sql) + .bind(note_id) + .fetch_one(executor) + .await?; - Ok(count.count) + Ok(count.count) } // _ _ _ _ _ __ _ ___ _ _ _ _ _ @@ -391,57 +372,49 @@ where // |_| pub(crate) async fn insert_bulk_note_to_page_relationships<'a, E>( - executor: E, - note_id: &str, - references: &[String], + executor: E, + note_id: &str, + references: &[String], ) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - if references.is_empty() { - return Ok(()); - } + if references.is_empty() { + return Ok(()); + } - let insert_pattern = format!("(?, ?, '{}')", PageRelationshipKind::Page.to_string()); - let insert_note_page_references_sql = - "INSERT INTO note_page_relationships (note_id, page_id, kind) VALUES ".to_string() - + &[insert_pattern.as_str()] - .repeat(references.len()) - .join(", ") + &";".to_string(); + let insert_pattern = format!("(?, ?, '{}')", PageRelationshipKind::Page.to_string()); + let insert_note_page_references_sql = "INSERT INTO note_page_relationships (note_id, page_id, kind) VALUES " + .to_string() + + &[insert_pattern.as_str()].repeat(references.len()).join(", ") + + &";".to_string(); - let mut request = sqlx::query(&insert_note_page_references_sql); - for reference in references { - request = request.bind(note_id).bind(reference); - } + let mut request = sqlx::query(&insert_note_page_references_sql); + for reference in references { + request = request.bind(note_id).bind(reference); + } - request.execute(executor).await.map(|_| ()) + request.execute(executor).await.map(|_| ()) } -pub(crate) async fn delete_bulk_note_to_page_relationships<'a, E>( - executor: E, - note_id: &str, -) -> SqlResult<()> +pub(crate) async fn delete_bulk_note_to_page_relationships<'a, E>(executor: E, note_id: &str) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let delete_note_to_page_relationship_sql = - "DELETE FROM note_page_relationships WHERE and note_id = ?;"; - let _ = sqlx::query(delete_note_to_page_relationship_sql) - .bind(note_id) - .execute(executor) - .await?; - Ok(()) + let delete_note_to_page_relationship_sql = "DELETE FROM note_page_relationships WHERE and note_id = ?;"; + let _ = sqlx::query(delete_note_to_page_relationship_sql) + .bind(note_id) + .execute(executor) + .await?; + Ok(()) } // Given the references supplied, and the references found in the datastore, // return a list of the references not found in the datastore. -pub(crate) fn diff_references( - references: &[String], - found_references: &[PageTitle], -) -> Vec { - let all: HashSet = references.iter().cloned().collect(); - let found: HashSet = found_references.iter().map(|r| r.content.clone()).collect(); - all.difference(&found).cloned().collect() +pub(crate) fn diff_references(references: &[String], found_references: &[PageTitle]) -> Vec { + let all: HashSet = references.iter().cloned().collect(); + let found: HashSet = found_references.iter().map(|r| r.content.clone()).collect(); + all.difference(&found).cloned().collect() } // ___ _ _ _ _ __ _ ___ _ _ _ _ _ @@ -454,32 +427,31 @@ pub(crate) fn diff_references( // list of titles. Used by insert_note and update_note_content to // find the ids of all the references in a given document. pub(crate) async fn find_all_page_from_list_of_references<'a, E>( - executor: E, - references: &[String], + executor: E, + references: &[String], ) -> SqlResult> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - if references.is_empty() { - return Ok(vec![]); - } + if references.is_empty() { + return Ok(vec![]); + } - lazy_static! { - static ref SELECT_ALL_REFERENCES_FOR_SQL_BASE: String = format!( - "SELECT id, content FROM notes WHERE kind = '{}' AND content IN (", - NoteKind::Page.to_string() - ); - } + lazy_static! { + static ref SELECT_ALL_REFERENCES_FOR_SQL_BASE: String = format!( + "SELECT id, content FROM notes WHERE kind = '{}' AND content IN (", + NoteKind::Page.to_string() + ); + } - let find_all_references_for_sql = SELECT_ALL_REFERENCES_FOR_SQL_BASE.to_string() - + &["?"].repeat(references.len()).join(",") - + &");".to_string(); + let find_all_references_for_sql = + SELECT_ALL_REFERENCES_FOR_SQL_BASE.to_string() + &["?"].repeat(references.len()).join(",") + &");".to_string(); - let mut request = sqlx::query_as(&find_all_references_for_sql); - for id in references.iter() { - request = request.bind(id); - } - request.fetch_all(executor).await + let mut request = sqlx::query_as(&find_all_references_for_sql); + for id in references.iter() { + request = request.bind(id); + } + request.fetch_all(executor).await } // ___ _ _ @@ -489,121 +461,116 @@ where // pub(crate) async fn delete_note_to_note_relationship<'a, E>( - executor: E, - parent_id: &str, - note_id: &str, + executor: E, + parent_id: &str, + note_id: &str, ) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let delete_note_to_note_relationship_sql = concat!( - "DELETE FROM note_relationships ", - "WHERE parent_id = ? and note_id = ? " - ); + let delete_note_to_note_relationship_sql = concat!( + "DELETE FROM note_relationships ", + "WHERE parent_id = ? and note_id = ? " + ); - let count = sqlx::query(delete_note_to_note_relationship_sql) - .bind(parent_id) - .bind(note_id) - .execute(executor) - .await? - .rows_affected(); + let count = sqlx::query(delete_note_to_note_relationship_sql) + .bind(parent_id) + .bind(note_id) + .execute(executor) + .await? + .rows_affected(); - match count { - 1 => Ok(()), - _ => Err(sqlx::Error::RowNotFound), - } + match count { + 1 => Ok(()), + _ => Err(sqlx::Error::RowNotFound), + } } -pub(crate) async fn delete_note_to_page_relationships<'a, E>( - executor: E, - note_id: &str, -) -> SqlResult<()> +pub(crate) async fn delete_note_to_page_relationships<'a, E>(executor: E, note_id: &str) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - lazy_static! { - static ref DELETE_NOTE_TO_PAGE_RELATIONSHIPS_SQL: String = format!( - "DELETE FROM note_relationships WHERE kind in ('{}', '{}') AND parent_id = ?;", - PageRelationshipKind::Page.to_string(), - PageRelationshipKind::Unacked.to_string() - ); - } + lazy_static! { + static ref DELETE_NOTE_TO_PAGE_RELATIONSHIPS_SQL: String = format!( + "DELETE FROM note_relationships WHERE kind in ('{}', '{}') AND parent_id = ?;", + PageRelationshipKind::Page.to_string(), + PageRelationshipKind::Unacked.to_string() + ); + } - let _ = sqlx::query(&DELETE_NOTE_TO_PAGE_RELATIONSHIPS_SQL) - .bind(note_id) - .execute(executor) - .await?; - Ok(()) + let _ = sqlx::query(&DELETE_NOTE_TO_PAGE_RELATIONSHIPS_SQL) + .bind(note_id) + .execute(executor) + .await?; + Ok(()) } pub(crate) async fn delete_note<'a, E>(executor: E, note_id: &str) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let delete_note_sql = "DELETE FROM notes WHERE note_id = ?"; + let delete_note_sql = "DELETE FROM notes WHERE note_id = ?"; - let count = sqlx::query(delete_note_sql) - .bind(note_id) - .execute(executor) - .await? - .rows_affected(); + let count = sqlx::query(delete_note_sql) + .bind(note_id) + .execute(executor) + .await? + .rows_affected(); - match count { - 1 => Ok(()), - _ => Err(sqlx::Error::RowNotFound), - } + match count { + 1 => Ok(()), + _ => Err(sqlx::Error::RowNotFound), + } } // After removing a note, recalculate the position of all notes under // the parent note, such that there order is now completely // sequential. -pub(crate) async fn close_hole_for_deleted_note<'a, E>( - executor: E, - parent_id: &str, - location: i64, +pub(crate) async fn close_hole_for_deleted_note_relationship<'a, E>( + executor: E, + parent_id: &str, + location: i64, ) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let close_hole_for_deleted_note_sql = concat!( - "UPDATE note_relationships ", - "SET location = location - 1 ", - "WHERE location > ? and parent_id = ?;" - ); + let close_hole_for_deleted_note_sql = concat!( + "UPDATE note_relationships ", + "SET location = location - 1 ", + "WHERE location > ? and parent_id = ?;" + ); - let _ = sqlx::query(close_hole_for_deleted_note_sql) - .bind(&location) - .bind(parent_id) - .execute(executor) - .await?; - Ok(()) + let _ = sqlx::query(close_hole_for_deleted_note_sql) + .bind(&location) + .bind(parent_id) + .execute(executor) + .await?; + Ok(()) } - // Given a list of references found in the content, generate the // references that do not previously exist, returning all found // references. NOTE: The function signature for this is for a // transaction, and uses a nested transaction. pub(crate) async fn validate_or_generate_all_found_references( - txi: &mut Transaction<'_, Sqlite>, - references: &[String] + txi: &mut Transaction<'_, Sqlite>, + references: &[String], ) -> SqlResult> { - let mut tx = txi.begin().await?; - - let found_references = - find_all_page_from_list_of_references(&mut tx, &references).await?; - let new_references = diff_references(&references, &found_references); - let mut new_page: Vec = vec![]; - for one_reference in new_references.iter() { - let slug = generate_slug(&mut tx, one_reference).await?; - new_page.push(create_page(&one_reference, &slug)); - } - let _ = bulk_insert_notes(&mut tx, &new_page).await?; + let mut tx = txi.begin().await?; - let mut all_reference_ids: Vec = found_references.iter().map(|r| r.id.clone()).collect(); - all_reference_ids.append(&mut new_page.iter().map(|r| r.id.clone()).collect()); - tx.commit().await?; - Ok(all_reference_ids) + let found_references = find_all_page_from_list_of_references(&mut tx, &references).await?; + let new_references = diff_references(&references, &found_references); + let mut new_page: Vec = vec![]; + for one_reference in new_references.iter() { + let slug = generate_slug(&mut tx, one_reference).await?; + new_page.push(create_page(&one_reference, &slug)); + } + let _ = insert_bulk_notes(&mut tx, &new_page).await?; + + let mut all_reference_ids: Vec = found_references.iter().map(|r| r.id.clone()).collect(); + all_reference_ids.append(&mut new_page.iter().map(|r| r.id.clone()).collect()); + tx.commit().await?; + Ok(all_reference_ids) } // __ __ _ @@ -616,13 +583,12 @@ pub(crate) async fn validate_or_generate_all_found_references( pub(crate) async fn count_existing_note_relationships<'a, E>(executor: E, note_id: &str) -> SqlResult where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let count_existing_note_relationships_sql = - "SELECT COUNT(*) as count FROM note_relationships WHERE note_id = ?;"; - let count: RowCount = sqlx::query_as(&count_existing_note_relationships_sql) - .bind(note_id) - .fetch_one(executor) - .await?; - Ok(count.count) + let count_existing_note_relationships_sql = "SELECT COUNT(*) as count FROM note_relationships WHERE note_id = ?;"; + let count: RowCount = sqlx::query_as(&count_existing_note_relationships_sql) + .bind(note_id) + .fetch_one(executor) + .await?; + Ok(count.count) } diff --git a/server/nm-store/src/store/store.rs b/server/nm-store/src/store/store.rs index d936f00..9ecf17f 100644 --- a/server/nm-store/src/store/store.rs +++ b/server/nm-store/src/store/store.rs @@ -69,204 +69,187 @@ pub type NoteResult = core::result::Result; // (box). impl NoteStore { - /// Initializes a new instance of the note store. Note that the - /// note store holds an Arc internally; this code is (I think) - /// safe to Send. - pub async fn new(url: &str) -> NoteResult { - let pool = SqlitePool::connect(url).await?; - Ok(NoteStore(Arc::new(pool))) - } - /// Erase all the data in the database and restore it - /// to its original empty form. Do not use unless you - /// really, really want that to happen. - pub async fn reset_database(&self) -> NoteResult<()> { - reset_database(&*self.0) - .await - .map_err(NoteStoreError::DBError) - } + /// Initializes a new instance of the note store. Note that the + /// note store holds an Arc internally; this code is (I think) + /// safe to Send. + pub async fn new(url: &str) -> NoteResult { + let pool = SqlitePool::connect(url).await?; + Ok(NoteStore(Arc::new(pool))) + } + /// Erase all the data in the database and restore it + /// to its original empty form. Do not use unless you + /// really, really want that to happen. + pub async fn reset_database(&self) -> NoteResult<()> { + reset_database(&*self.0).await.map_err(NoteStoreError::DBError) + } - /// Fetch page by slug - /// - /// Supports the use case of the user navigating to a known place - /// via a bookmark or other URL. Since the title isn't clear from - /// the slug, the slug is insufficient to generate a new page, so - /// this use case says that in the event of a failure to find the - /// requested page, return a basic NotFound. - pub async fn get_page_by_slug(&self, slug: &str) -> NoteResult<(Vec, Vec)> { - let page = select_page_by_slug(&*self.0, slug).await?; - if page.is_empty() { - return Err(NoteStoreError::NotFound); - } + /// Fetch page by slug + /// + /// Supports the use case of the user navigating to a known place + /// via a bookmark or other URL. Since the title isn't clear from + /// the slug, the slug is insufficient to generate a new page, so + /// this use case says that in the event of a failure to find the + /// requested page, return a basic NotFound. + pub async fn get_page_by_slug(&self, slug: &str) -> NoteResult<(Vec, Vec)> { + let page = select_page_by_slug(&*self.0, slug).await?; + if page.is_empty() { + return Err(NoteStoreError::NotFound); + } - let note_id = &page[0].id; - let backreferences = select_backreferences_for_page(&*self.0, ¬e_id).await?; - Ok((page, backreferences)) - } + let note_id = &page[0].id; + let backreferences = select_backreferences_for_page(&*self.0, ¬e_id).await?; + Ok((page, backreferences)) + } - /// Fetch page by title - /// - /// The most common use case: the user is navigating by requesting - /// a page. The page either exists or it doesn't. If it - /// doesn't, we go out and make it. Since we know it doesn't exist, - /// we also know no backreferences to it exist, so in that case you - /// get back two empty vecs. - pub async fn get_page_by_title(&self, title: &str) -> NoteResult<(Vec, Vec)> { - if title.len() == 0 { - return Err(NoteStoreError::NotFound); - } + /// Fetch page by title + /// + /// The most common use case: the user is navigating by requesting + /// a page. The page either exists or it doesn't. If it + /// doesn't, we go out and make it. Since we know it doesn't exist, + /// we also know no backreferences to it exist, so in that case you + /// get back two empty vecs. + pub async fn get_page_by_title(&self, title: &str) -> NoteResult<(Vec, Vec)> { + if title.len() == 0 { + return Err(NoteStoreError::NotFound); + } - let page = select_page_by_title(&*self.0, title).await?; - if page.len() > 0 { - let note_id = &page[0].id; - let backreferences = select_backreferences_for_page(&*self.0, ¬e_id).await?; - return Ok((page, backreferences)); - } + let page = select_page_by_title(&*self.0, title).await?; + if page.len() > 0 { + let note_id = &page[0].id; + let backreferences = select_backreferences_for_page(&*self.0, ¬e_id).await?; + return Ok((page, backreferences)); + } - // Sanity check! - let references = build_references(&title); - if references.len() > 0 { - return Err(NoteStoreError::InvalidNoteStructure( - "Titles may not contain nested references.".to_string(), - )); - } + // Sanity check! + let references = build_references(&title); + if references.len() > 0 { + return Err(NoteStoreError::InvalidNoteStructure( + "Titles may not contain nested references.".to_string(), + )); + } - let mut tx = self.0.begin().await?; - let slug = generate_slug(&mut tx, title).await?; - let page = create_page(&title, &slug); - let _ = insert_note(&mut tx, &page).await?; - tx.commit().await?; + let mut tx = self.0.begin().await?; + let slug = generate_slug(&mut tx, title).await?; + let page = create_page(&title, &slug); + let _ = insert_note(&mut tx, &page).await?; + tx.commit().await?; - Ok((vec![Note::from(page)], vec![])) - } + Ok((vec![Note::from(page)], vec![])) + } - pub async fn add_note( - &self, - note: &NewNote, - parent_id: &str, - location: Option, - ) -> NoteResult { - let kind = RelationshipKind::Direct; - let new_id = self.insert_note(note, parent_id, location, kind).await?; - Ok(new_id) - } + pub async fn add_note(&self, note: &NewNote, parent_id: &str, location: Option) -> NoteResult { + let kind = RelationshipKind::Direct; + let new_id = self.insert_note(note, parent_id, location, kind).await?; + Ok(new_id) + } - /// Move a note from one location to another. - pub async fn move_note( - &self, - note_id: &str, - old_parent_id: &str, - new_parent_id: &str, - new_location: i64, - ) -> NoteResult<()> { - let mut tx = self.0.begin().await?; + /// Move a note from one location to another. + pub async fn move_note( + &self, + note_id: &str, + old_parent_id: &str, + new_parent_id: &str, + new_location: i64, + ) -> NoteResult<()> { + let mut tx = self.0.begin().await?; - let old_note = select_note_to_note_relationship(&mut tx, &old_parent_id, ¬e_id).await?; - let old_note_location = old_note.location; - let old_note_kind = old_note.kind; + let old_note = select_note_to_note_relationship(&mut tx, &old_parent_id, ¬e_id).await?; + let old_note_location = old_note.location; + let old_note_kind = old_note.kind; - let _ = delete_note_to_note_relationship(&mut tx, &old_parent_id, ¬e_id).await?; - let _ = close_hole_for_deleted_note(&mut tx, &old_parent_id, old_note_location).await?; - let new_location = determine_max_child_location_for_note(&mut tx, &new_parent_id, Some(new_location)).await?; - let _ = make_room_for_new_note(&mut tx, &new_parent_id, new_location).await?; - let _ = insert_note_to_note_relationship( - &mut tx, - &new_parent_id, - ¬e_id, - new_location, - &old_note_kind, - ) - .await?; - tx.commit().await?; - Ok(()) - } + let _ = delete_note_to_note_relationship(&mut tx, &old_parent_id, ¬e_id).await?; + let _ = close_hole_for_deleted_note_relationship(&mut tx, &old_parent_id, old_note_location).await?; + let new_location = determine_max_child_location_for_note(&mut tx, &new_parent_id, Some(new_location)).await?; + let _ = make_room_for_new_note_relationship(&mut tx, &new_parent_id, new_location).await?; + let _ = + insert_note_to_note_relationship(&mut tx, &new_parent_id, ¬e_id, new_location, &old_note_kind).await?; + tx.commit().await?; + Ok(()) + } - /// Updates a note's content. Completely rebuilds the note's - /// outgoing edge reference list every time. - pub async fn update_note_content(&self, note_id: &str, content: &str) -> NoteResult<()> { - let references = build_references(&content); - let note_id = NoteId(note_id.to_string()); + /// Updates a note's content. Completely rebuilds the note's + /// outgoing edge reference list every time. + pub async fn update_note_content(&self, note_id: &str, content: &str) -> NoteResult<()> { + let references = build_references(&content); + let mut tx = self.0.begin().await?; + let _ = update_note_content(&mut tx, ¬e_id, &content).await?; + let _ = delete_bulk_note_to_page_relationships(&mut tx, ¬e_id).await?; + let known_reference_ids = validate_or_generate_all_found_references(&mut tx, &references).await?; + let _ = insert_bulk_note_to_page_relationships(&mut tx, ¬e_id, &known_reference_ids).await?; + tx.commit().await?; + Ok(()) + } - let mut tx = self.0.begin().await?; - let _ = update_note_content(&mut tx, ¬e_id, &content).await?; - let _ = delete_bulk_note_to_page_relationships(&mut tx, ¬e_id).await?; - let known_reference_ids = validate_or_generate_all_found_references(&mut tx, &references).await?; - let _ = insert_bulk_note_to_page_relationships(&mut tx, ¬e_id, &known_reference_ids) - .await?; - tx.commit().await?; - Ok(()) - } + /// Deletes a note. If the note's relationship drops to zero, all + /// references from that note to pages are also deleted. + pub async fn delete_note(&self, note_id: &str, note_parent_id: &str) -> NoteResult<()> { + let mut tx = self.0.begin().await?; - /// Deletes a note. If the note's relationship drops to zero, all - /// references from that note to pages are also deleted. - pub async fn delete_note(&self, note_id: &str, note_parent_id: &str) -> NoteResult<()> { - let mut tx = self.0.begin().await?; - let note_id = NoteId(note_id.to_string()); - let parent_id = ParentId(note_parent_id.to_string()); + let note_id = note_id.to_string(); + let parent_id = note_parent_id.to_string(); - if *parent_id != *note_id { - let _ = delete_note_to_note_relationship(&mut tx, &parent_id, ¬e_id); - } - // The big one: if zero parents report having an interest in this note, then it, - // *and any sub-relationships*, go away. - if count_existing_note_relationships(&mut tx, ¬e_id).await? == 0 { - let _ = delete_note_to_page_relationships(&mut tx, ¬e_id).await?; - let _ = delete_note(&mut tx, ¬e_id).await?; - } - tx.commit().await?; - Ok(()) - } + if parent_id != note_id { + let _ = delete_note_to_note_relationship(&mut tx, &parent_id, ¬e_id); + } + // The big one: if zero parents report having an interest in this note, then it, + // *and any sub-relationships*, go away. + if count_existing_note_relationships(&mut tx, ¬e_id).await? == 0 { + let _ = delete_note_to_page_relationships(&mut tx, ¬e_id).await?; + let _ = delete_note(&mut tx, ¬e_id).await?; + } + tx.commit().await?; + Ok(()) + } } // The Private stuff impl NoteStore { - // Pretty much the most dangerous function in our system. Has to - // have ALL the error checking. - async fn insert_note( - &self, - note: &NewNote, - parent_id: &str, - location: Option, - kind: RelationshipKind, - ) -> NoteResult { - if let Some(location) = location { - if location < 0 { - return Err(NoteStoreError::InvalidNoteStructure( - "Add note: A negative location is not valid.".to_string(), - )); - } - } + // Pretty much the most dangerous function in our system. Has to + // have ALL the error checking. + async fn insert_note( + &self, + note: &NewNote, + parent_id: &str, + location: Option, + kind: RelationshipKind, + ) -> NoteResult { + if let Some(location) = location { + if location < 0 { + return Err(NoteStoreError::InvalidNoteStructure( + "Add note: A negative location is not valid.".to_string(), + )); + } + } - if parent_id.is_empty() { - return Err(NoteStoreError::InvalidNoteStructure( - "Add note: A parent note ID is required.".to_string(), - )); - } + if parent_id.is_empty() { + return Err(NoteStoreError::InvalidNoteStructure( + "Add note: A parent note ID is required.".to_string(), + )); + } - if note.id.is_empty() { - return Err(NoteStoreError::InvalidNoteStructure( - "Add note: Your note should have an id already".to_string(), - )); - } + if note.id.is_empty() { + return Err(NoteStoreError::InvalidNoteStructure( + "Add note: Your note should have an id already".to_string(), + )); + } - if note.content.is_empty() { - return Err(NoteStoreError::InvalidNoteStructure( - "Add note: Empty notes are not supported.".to_string(), - )); - } + if note.content.is_empty() { + return Err(NoteStoreError::InvalidNoteStructure( + "Add note: Empty notes are not supported.".to_string(), + )); + } - let references = build_references(¬e.content); + let references = build_references(¬e.content); - let mut tx = self.0.begin().await?; - let location = determine_max_child_location_for_note(&mut tx, parent_id, location).await?; - let note_id = NoteId(note.id.clone()); - insert_note(&mut tx, ¬e).await?; - make_room_for_new_note(&mut tx, &parent_id, location).await?; - insert_note_to_note_relationship(&mut tx, &parent_id, ¬e_id, location, &kind).await?; - let known_reference_ids = validate_or_generate_all_found_references(&mut tx, &references).await?; - let _ = insert_bulk_note_to_page_relationships(&mut tx, ¬e_id, &known_reference_ids) - .await?; - tx.commit().await?; - Ok(note_id.to_string()) - } + let mut tx = self.0.begin().await?; + let location = determine_max_child_location_for_note(&mut tx, parent_id, location).await?; + insert_note(&mut tx, ¬e).await?; + make_room_for_new_note_relationship(&mut tx, &parent_id, location).await?; + insert_note_to_note_relationship(&mut tx, &parent_id, ¬e.id, location, &kind).await?; + let known_reference_ids = validate_or_generate_all_found_references(&mut tx, &references).await?; + let _ = insert_bulk_note_to_page_relationships(&mut tx, ¬e.id, &known_reference_ids).await?; + tx.commit().await?; + Ok(note.id.to_string()) + } }