From bad0de9bc04412c5c2de91be5584a4e4a665a9cf Mon Sep 17 00:00:00 2001 From: "Elf M. Sternberg" Date: Fri, 6 Nov 2020 14:43:05 -0800 Subject: [PATCH] Mostly documentation changes, but I do want to emphasize that by the time we hit this layer, the distinction between an API note and a REST note have been made. --- server/Pipfile | 11 ++ server/nm-store/docs/TODO.md | 2 + server/nm-store/src/errors.rs | 3 + .../nm-store/src/sql/initialize_database.sql | 6 + server/nm-store/src/store.rs | 13 +- server/nm-store/src/store_private.rs | 28 ++- server/nm-trees/Makefile | 15 ++ server/nm-trees/src/lib.rs | 159 +++++++++--------- server/nm-trees/src/structs.rs | 4 +- 9 files changed, 151 insertions(+), 90 deletions(-) create mode 100644 server/Pipfile create mode 100644 server/nm-trees/Makefile diff --git a/server/Pipfile b/server/Pipfile new file mode 100644 index 0000000..8b36f8b --- /dev/null +++ b/server/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] + +[requires] +python_version = "2.7" diff --git a/server/nm-store/docs/TODO.md b/server/nm-store/docs/TODO.md index a96835b..71ce017 100644 --- a/server/nm-store/docs/TODO.md +++ b/server/nm-store/docs/TODO.md @@ -1,2 +1,4 @@ [ ] Add RelationshipKind to Notes passed out [ ] Add KastenKind to Backreferences passed out +[ ] Provide the array of notes references (the 'cycle' manager) to make + mapping from Vec->Tree easier. diff --git a/server/nm-store/src/errors.rs b/server/nm-store/src/errors.rs index 26031f2..b6913f7 100644 --- a/server/nm-store/src/errors.rs +++ b/server/nm-store/src/errors.rs @@ -9,6 +9,9 @@ pub enum NoteStoreError { #[error("Invalid Note Structure")] InvalidNoteStructure(String), + /// The requested kasten or note was not found. As much as + /// possible, this should be preferred to a + /// sqlx::Error::RowNotFound. #[error("Not found")] NotFound, diff --git a/server/nm-store/src/sql/initialize_database.sql b/server/nm-store/src/sql/initialize_database.sql index 38e6084..66ee2c2 100644 --- a/server/nm-store/src/sql/initialize_database.sql +++ b/server/nm-store/src/sql/initialize_database.sql @@ -50,3 +50,9 @@ CREATE TABLE note_kasten_relationships ( CHECK (note_id <> kasten_id) ); +-- A fabulous constraint. This index prevents us from saying that +-- if a note points to a kasten, the kasten may not point to a +-- note. Now, it's absolutely required that a kasten_id point to +-- a KastenType note; the content should be a title only. +CREATE UNIQUE INDEX note_kasten_unique_idx + ON note_kasten_relationships (MIN(note_id, kasten_id), MAX(note_id, kasten_id)); diff --git a/server/nm-store/src/store.rs b/server/nm-store/src/store.rs index bc7982b..228a2c1 100644 --- a/server/nm-store/src/store.rs +++ b/server/nm-store/src/store.rs @@ -137,14 +137,13 @@ impl NoteStore { Ok((vec![Note::from(zettlekasten)], vec![])) } - pub async fn add_note(&self, note: &NewNote) -> NoteResult { - self.insert_note( + pub async fn add_note(&self, note: &NewNote, parent_id: &str, location: Option) -> NoteResult { + let new_id = self.insert_note( note, &ParentId(parent_id.to_string()), location, - RelationshipKind::Direct, - ) - .await + RelationshipKind::Direct).await?; + Ok(new_id) } /// Move a note from one location to another. @@ -208,7 +207,9 @@ impl NoteStore { let note_id = NoteId(note_id.to_string()); let parent_id = ParentId(note_parent_id.to_string()); - let _ = delete_note_to_note_relationship(&mut tx, &parent_id, ¬e_id); + 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 { diff --git a/server/nm-store/src/store_private.rs b/server/nm-store/src/store_private.rs index c038d64..ce057fe 100644 --- a/server/nm-store/src/store_private.rs +++ b/server/nm-store/src/store_private.rs @@ -18,12 +18,6 @@ type SqlResult = sqlx::Result; // coherent and easily readable, and hides away the gnarliness of some // of the SQL queries. -// Important!!! Note_relationships are usually (parent_note -> note), -// but Note to Kasten relationships are always (note-as-parent -> -// kasten_note), so when looking for "all the notes referring to this -// kasten", you use the kasten's id as the TARGET note_id, and the -// note referring to the kasten in the parent_id. - lazy_static! { static ref SELECT_KASTEN_BY_TITLE_SQL: String = str::replace( include_str!("sql/select_notes_by_parameter.sql"), @@ -65,6 +59,15 @@ where // |_|\___|\__\__|_||_| |_|\_\__,_/__/\__\___|_||_| // +// Select the requested kasten via its id. This is fairly rare; +// kastens should usually be picked up via their title, but if you're +// navigating to an instance, this is how you specify the kasten in a +// URL. The return value is an array of Note objects; it is the +// responsibility of client code to restructure these into a tree-like +// object. +// +// Recommended: Clients should update the URL whenever changing +// kasten. pub(crate) async fn select_kasten_by_slug<'a, E>(executor: E, slug: &NoteId) -> SqlResult> where E: Executor<'a, Database = Sqlite>, @@ -76,6 +79,9 @@ where Ok(r.into_iter().map(|z| Note::from(z)).collect()) } +// Fetch the kasten by title. The return value is an array of Note +// objects; it is the responsibility of client code to restructure +// these into a tree-like object. pub(crate) async fn select_kasten_by_title<'a, E>(executor: E, title: &str) -> SqlResult> where E: Executor<'a, Database = Sqlite>, @@ -87,6 +93,10 @@ where Ok(r.into_iter().map(|z| Note::from(z)).collect()) } +// Fetch all backreferences to a kasten. The return value is an array +// of arrays, and inside each array is a list from a root kasten to +// the note that references the give kasten. Clients may choose how +// they want to display that collection. pub(crate) async fn select_backreferences_for_kasten<'a, E>(executor: E, kasten_id: &NoteId) -> SqlResult> where E: Executor<'a, Database = Sqlite>, @@ -104,6 +114,7 @@ where // |___|_||_/__/\___|_| \__| \___/|_||_\___| |_|\_\___/\__\___| // +// Inserts a single note into the notes table. That is all. pub(crate) async fn insert_note<'a, E>(executor: E, zettle: &NewNote) -> SqlResult where E: Executor<'a, Database = Sqlite>, @@ -154,7 +165,8 @@ pub(crate) fn find_maximal_slug_number(slugs: &[JustId]) -> Option { // Given an initial string and an existing collection of slugs, // generate a new slug that does not conflict with the current -// collection. +// collection. Right now we're using the slugify operation, which... +// isn't all that. pub(crate) async fn generate_slug<'a, E>(executor: E, title: &str) -> SqlResult where E: Executor<'a, Database = Sqlite>, @@ -180,6 +192,8 @@ where }) } +// A helper function: given a title and a slug, create a KastenType +// note. pub(crate) fn create_zettlekasten(title: &str, slug: &str) -> NewNote { NewNoteBuilder::default() .id(slug.to_string()) diff --git a/server/nm-trees/Makefile b/server/nm-trees/Makefile new file mode 100644 index 0000000..e0b2174 --- /dev/null +++ b/server/nm-trees/Makefile @@ -0,0 +1,15 @@ +.PHONY: all +all: help + +.PHONY: help +help: + @M=$$(perl -ne 'm/((\w|-)*):.*##/ && print length($$1)."\n"' Makefile | \ + sort -nr | head -1) && \ + perl -ne "m/^((\w|-)*):.*##\s*(.*)/ && print(sprintf(\"%s: %s\t%s\n\", \$$1, \" \"x($$M-length(\$$1)), \$$3))" Makefile + +# This is necessary because I'm trying hard not to use +# any `nightly` features. But rustfmt is likely to be +# a `nightly-only` feature for a long time to come, so +# this is my hack. +fmt: ## Format the code, using the most modern version of rustfmt + rustup run nightly cargo fmt diff --git a/server/nm-trees/src/lib.rs b/server/nm-trees/src/lib.rs index 617ec68..9f54146 100644 --- a/server/nm-trees/src/lib.rs +++ b/server/nm-trees/src/lib.rs @@ -12,10 +12,10 @@ mod make_tree; mod structs; +use crate::make_tree::{make_backreferences, make_note_tree}; +use crate::structs::{Note, Page}; use chrono::{DateTime, Utc}; -use nm_store::{NoteStore, NoteStoreError, NewNote}; -use crate::structs::{Page, Note}; -use crate::make_tree::{make_note_tree, make_backreferences}; +use nm_store::{NewNote, NoteStore, NoteStoreError}; #[derive(Debug)] pub struct Notesmachine(pub(crate) NoteStore); @@ -23,87 +23,97 @@ pub struct Notesmachine(pub(crate) NoteStore); type Result = core::result::Result; pub fn make_page(foundtree: &Note, backreferences: Vec>) -> Page { - Page { - slug: foundtree.id, - title: foundtree.content, - creation_date: foundtree.creation_date, - updated_date: foundtree.updated_date, - lastview_date: foundtree.lastview_date, - deleted_date: foundtree.deleted_date, - notes: foundtree.children, - backreferences: backreferences - } + Page { + slug: foundtree.id, + title: foundtree.content, + creation_date: foundtree.creation_date, + updated_date: foundtree.updated_date, + lastview_date: foundtree.lastview_date, + deleted_date: foundtree.deleted_date, + notes: foundtree.children, + backreferences: backreferences, + } } impl Notesmachine { - pub async fn new(url: &str) -> Result { - let notestore = NoteStore::new(url).await?; - Ok(Notesmachine(notestore)) - } + pub async fn new(url: &str) -> Result { + let notestore = NoteStore::new(url).await?; + Ok(Notesmachine(notestore)) + } - pub async fn get_page_via_slug(&self, slug: &str) -> Result { - let (rawtree, rawbackreferences) = self.0.get_kasten_by_slug(slug).await?; - Ok(make_page(&make_note_tree(&rawtree), make_backreferences(&rawbackreferences))) - } + pub async fn get_page_via_slug(&self, slug: &str) -> Result { + let (rawtree, rawbackreferences) = self.0.get_kasten_by_slug(slug).await?; + Ok(make_page( + &make_note_tree(&rawtree), + make_backreferences(&rawbackreferences), + )) + } - pub async fn get_page(&self, title: &str) -> Result { - let (rawtree, rawbackreferences) = self.0.get_kasten_by_title(title).await?; - Ok(make_page(&make_note_tree(&rawtree), make_backreferences(&rawbackreferences))) - } + pub async fn get_page(&self, title: &str) -> Result { + let (rawtree, rawbackreferences) = self.0.get_kasten_by_title(title).await?; + Ok(make_page( + &make_note_tree(&rawtree), + make_backreferences(&rawbackreferences), + )) + } - // TODO: - // You should be able to: - // Add a note that has no parent (gets added to "today") - // Add a note that specifies only the page (gets added to page/root) - // Add a note that has no location (gets tacked onto the end of the above) - // Add a note that specifies the date of creation. - pub async fn add_note(&self, note: &NewNote) -> Result { - let mut note = note.clone(); - if note.parent_id.is_none() { - let (parent, _) = self.get_today_page().await?; - note.parent_id = parent.id; - } - Ok(self.0.add_note(¬e)) - } + // TODO: + // You should be able to: + // Add a note that has no parent (gets added to "today") + // Add a note that specifies only the page (gets added to page/root) + // Add a note that has no location (gets tacked onto the end of the above) + // Add a note that specifies the date of creation. + pub async fn add_note(&self, note: &NewNote) -> Result { + let mut note = note.clone(); + if note.parent_id.is_none() { + note.parent_id = self.get_today_page().await?; + } + Ok(self.0.add_note(¬e)) + } -// pub async fn reference_note(&self, note_id: &str, new_parent_id: &str, new_location: i64) -> Result<()> { -// todo!(); -// } -// -// pub async fn embed_note(&self, note_id: &str, new_parent_id: &str, new_location: i64) -> Result<()> { -// todo!(); -// } + // pub async fn reference_note(&self, note_id: &str, new_parent_id: &str, new_location: i64) -> Result<()> { + // todo!(); + // } + // + // pub async fn embed_note(&self, note_id: &str, new_parent_id: &str, new_location: i64) -> Result<()> { + // todo!(); + // } - pub async fn move_note(&self, note_id: &str, old_parent_id: &str, new_parent_id: &str, location: i64) -> Result<()> { - self.0.move_note(note_id, old_parent_id, new_parent_id, location) - } + pub async fn move_note( + &self, + note_id: &str, + old_parent_id: &str, + new_parent_id: &str, + location: i64, + ) -> Result<()> { + self.0.move_note(note_id, old_parent_id, new_parent_id, location).await + } - pub async fn update_note(&self, note_id: &str, content: &str) -> Result<()> { - self.0.update_note(note_id, content) - } + pub async fn update_note(&self, note_id: &str, content: &str) -> Result<()> { + self.0.update_note_content(note_id, content).await + } - pub async fn delete_note(&self, note_id: &str) -> Result<()> { - self.0.delete_note(note_id) - } + pub async fn delete_note(&self, note_id: &str, parent_note_id: &str) -> Result<()> { + self.0.delete_note(note_id, parent_note_id).await + } } // Private stuff impl Notesmachine { - async fn get_today_page(&self) -> Result { - let title = chrono::Utc::now().format("%F").to_string(); - let (rawtree, _) = self.0.get_kasten_by_title(title).await?; - Ok(rawtree.id) - } + async fn get_today_page(&self) -> Result { + let title = chrono::Utc::now().format("%F").to_string(); + let (rawtree, _) = self.0.get_kasten_by_title(title).await?; + Ok(rawtree.id) + } } - - + #[cfg(test)] mod tests { - use super::*; - use tokio; + use super::*; + use tokio; - async fn fresh_inmemory_database() -> Notesmachine { + async fn fresh_inmemory_database() -> Notesmachine { let notesmachine = Notesmachine::new("sqlite://:memory:").await; assert!(notesmachine.is_ok(), "{:?}", notesmachine); let notesmachine = notesmachine.unwrap(); @@ -115,7 +125,7 @@ mod tests { #[tokio::test(threaded_scheduler)] async fn fetching_unfound_page_by_slug_works() { let notesmachine = fresh_inmemory_database().await; - let unfoundpage = notesmachine.navigate_via_slug("nonexistent-slug").await; + let unfoundpage = notesmachine.get_page_via_slug("nonexistent-slug").await; assert!(unfoundpage.is_err()); } @@ -123,15 +133,14 @@ mod tests { async fn fetching_unfound_page_by_title_works() { let title = "Nonexistent Page"; let notesmachine = fresh_inmemory_database().await; - let newpageresult = notesmachine.get_box(&title).await; + let newpageresult = notesmachine.get_page(&title).await; assert!(newpageresult.is_ok(), "{:?}", newpageresult); - let newpage = newpageresult.unwrap(); - assert_eq!(newpage.title, title, "{:?}", newpage.title); - assert_eq!(newpage.slug, "nonexistent-page", "{:?}", newpage.slug); - assert_eq!(newpage.root_note.content, "", "{:?}", newpage.root_note.content); - assert_eq!(newpage.root_note.notetype, "root", "{:?}", newpage.root_note.notetype); - assert_eq!(newpage.root_note.children.len(), 0, "{:?}", newpage.root_note.children); - } + let newpage = newpageresult.unwrap(); + assert_eq!(newpage.title, title, "{:?}", newpage.title); + assert_eq!(newpage.slug, "nonexistent-page", "{:?}", newpage.slug); + assert_eq!(newpage.root_note.content, "", "{:?}", newpage.root_note.content); + assert_eq!(newpage.root_note.notetype, "root", "{:?}", newpage.root_note.notetype); + assert_eq!(newpage.root_note.children.len(), 0, "{:?}", newpage.root_note.children); + } } - diff --git a/server/nm-trees/src/structs.rs b/server/nm-trees/src/structs.rs index 8278ad8..969938b 100644 --- a/server/nm-trees/src/structs.rs +++ b/server/nm-trees/src/structs.rs @@ -1,7 +1,6 @@ use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, Debug)] pub struct Note { pub id: String, pub parent_id: Option, @@ -15,6 +14,7 @@ pub struct Note { pub children: Vec, } +#[derive(Clone, Debug)] pub struct Page { pub slug: String, pub title: String,