FEAT Delete & Update note is now complete.

Well, as complete as it could be without proper automated testing.
I think there'll be some more testing soon, as it doesn't make sense
for it to hang out so blatantly like this.

Both a fmt and clippy pass have shaken all the lint off, and right
now it builds without warnings or lintings.  Wheee!
This commit is contained in:
Elf M. Sternberg 2020-10-26 18:54:56 -07:00
parent 739ff93427
commit 72fb3b11ee
8 changed files with 709 additions and 617 deletions

View File

@ -0,0 +1,46 @@
# Storage layer for Notesmachine
This library implements the core functionality of Notesmachine and
describes that functionality to a storage layer. There's a bit of
intermingling in here which can't be helped, although it may make sense
in the future to separate the decomposition of the note content into a
higher layer.
Notesmachine storage notes consist of two items: Zettle and Kasten,
which are German for "Note" and "Box". Here are the basic rules:
- Boxes have titles (and date metadata)
- Notes have content and a type (and date metadata)
- Notes are stored in boxes
- Notes are positioned with respect to other notes.
- There are two positions:
- Siblings, creating lists
- Children, creating trees like this one
- Notes may have references (pointers) to other boxes
- Notes may be moved around
- Notes may be deleted
- Boxes may be deleted
- When a box is renamed, every reference to that box is auto-edited to
reflect the change. If a box is renamed to match an existing box, the
notes in both boxes are merged.
Note-to-note relationships form trees, and are kept in a SQL database of
(`parent_id`, `child_id`, `position`, `relationship_type`). The
`position` is a monotonic index on the parent (that is, every pair
(`parent_id`, `position`) must be unique). The `relationship_type` is
an enum and can specify that the relationship is *original*,
*embedding*, or *referencing*. An embedded or referenced note may be
read/write or read-only with respect to the original, but there is only
one original note at any time.
Note-to-box relationships form a graph, and are kept in the SQL database
as a collection of *edges* from the note to the box (and naturally
vice-versa).
- Decision: When an original note is deleted, do all references and
embeddings also get deleted, or is the oldest one elevated to be a new
"original"? Or is that something the user may choose?
- Decision: Should the merging issue be handled at this layer, or would
it make sense to move this to a higher layer, and only provide the
hooks for it here?

View File

@ -1,4 +1,3 @@
use sqlx;
use thiserror::Error; use thiserror::Error;
/// All the ways looking up objects can fail /// All the ways looking up objects can fail

View File

@ -1,8 +1,7 @@
mod errors; mod errors;
mod row_structs; mod reference_parser;
mod store; mod store;
mod structs; mod structs;
mod reference_parser;
pub use crate::errors::NoteStoreError; pub use crate::errors::NoteStoreError;
pub use crate::store::NoteStore; pub use crate::store::NoteStore;

View File

@ -37,7 +37,7 @@ pub(crate) fn find_links(document: &str) -> Vec<String> {
} }
match &node.data.borrow().value { match &node.data.borrow().value {
&NodeValue::Text(ref text) => Some( NodeValue::Text(ref text) => Some(
RE_REFERENCES RE_REFERENCES
.captures_iter(text) .captures_iter(text)
.map(|t| String::from_utf8_lossy(&t.get(1).unwrap().as_bytes()).to_string()) .map(|t| String::from_utf8_lossy(&t.get(1).unwrap().as_bytes()).to_string())
@ -68,19 +68,16 @@ fn recase(title: &str) -> String {
RE_PASS3.replace_all(&pass, " ").trim().to_string() RE_PASS3.replace_all(&pass, " ").trim().to_string()
} }
fn build_page_titles(references: &Vec<String>) -> Vec<String> { fn build_page_titles(references: &[String]) -> Vec<String> {
references references
.iter() .iter()
.map(|s| { .map(|s| match s.chars().next() {
let c = s.chars().nth(0); Some('#') => recase(s),
match c { Some('[') => s.strip_prefix("[[").unwrap().strip_suffix("]]").unwrap().to_string(),
Some('#') => recase(s), Some(_) => s.clone(),
Some('[') => s.strip_prefix("[[").unwrap().strip_suffix("]]").unwrap().to_string(), _ => "".to_string(),
Some(_) => s.clone(),
_ => "".to_string(),
}
}) })
.filter(|s| s.len() > 0) .filter(|s| s.is_empty())
.collect() .collect()
} }

View File

@ -1,111 +0,0 @@
use chrono::{DateTime, Utc};
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use sqlx::{self, FromRow};
#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
pub struct RawPage {
pub id: i64,
pub slug: String,
pub title: String,
pub note_id: i64,
pub creation_date: DateTime<Utc>,
pub updated_date: DateTime<Utc>,
pub lastview_date: DateTime<Utc>,
pub deleted_date: Option<DateTime<Utc>>,
}
#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
pub struct RawNote {
pub id: i64,
pub uuid: String,
pub parent_id: i64,
pub parent_uuid: String,
pub content: String,
pub position: i64,
pub notetype: String,
pub creation_date: DateTime<Utc>,
pub updated_date: DateTime<Utc>,
pub lastview_date: DateTime<Utc>,
pub deleted_date: Option<DateTime<Utc>>,
}
#[derive(Clone, Serialize, Deserialize, Debug, Builder)]
pub struct NewPage {
pub slug: String,
pub title: String,
pub note_id: i64,
#[builder(default = r#"chrono::Utc::now()"#)]
pub creation_date: DateTime<Utc>,
#[builder(default = r#"chrono::Utc::now()"#)]
pub updated_date: DateTime<Utc>,
#[builder(default = r#"chrono::Utc::now()"#)]
pub lastview_date: DateTime<Utc>,
#[builder(default = r#"None"#)]
pub deleted_date: Option<DateTime<Utc>>,
}
#[derive(Clone, Serialize, Deserialize, Debug, Builder)]
pub struct NewNote {
#[builder(default = r#""".to_string()"#)]
pub uuid: String,
pub content: String,
#[builder(default = r#""note".to_string()"#)]
pub notetype: String,
#[builder(default = r#"chrono::Utc::now()"#)]
pub creation_date: DateTime<Utc>,
#[builder(default = r#"chrono::Utc::now()"#)]
pub updated_date: DateTime<Utc>,
#[builder(default = r#"chrono::Utc::now()"#)]
pub lastview_date: DateTime<Utc>,
#[builder(default = r#"None"#)]
pub deleted_date: Option<DateTime<Utc>>,
}
#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
pub(crate) struct JustSlugs {
pub slug: String,
}
#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
pub(crate) struct JustTitles {
title: String,
}
#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
pub(crate) struct JustId {
pub id: i64,
}
#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
pub(crate) struct PageTitles {
pub id: i64,
pub title: String,
}
#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
pub(crate) struct NoteRelationship {
pub parent_id: i64,
pub note_id: i64,
pub position: i64,
pub nature: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_build_new_note() {
let now = chrono::Utc::now();
let newnote = NewNoteBuilder::default()
.uuid("foo".to_string())
.content("bar".to_string())
.build()
.unwrap();
assert!((newnote.creation_date - now).num_minutes() < 1);
assert!((newnote.updated_date - now).num_minutes() < 1);
assert!((newnote.lastview_date - now).num_minutes() < 1);
assert!(newnote.deleted_date.is_none());
}
}

View File

@ -1,8 +1,8 @@
DROP TABLE IF EXISTS notes; DROP TABLE IF EXISTS notes;
DROP TABLE IF EXISTS note_relationships; DROP TABLE IF EXISTS note_relationships;
DROP TABLE IF EXISTS pages; DROP TABLE IF EXISTS pages;
DROP TABLE IF EXISTS favorites;
DROP TABLE IF EXISTS page_relationships; DROP TABLE IF EXISTS page_relationships;
DROP TABLE IF EXISTS favorites;
CREATE TABLE notes ( CREATE TABLE notes (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,

File diff suppressed because it is too large Load Diff

View File

@ -1,58 +1,116 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use sqlx::{self, FromRow}; use sqlx::{self, FromRow};
// // A Resource is either content or a URL to content that the #[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
// // user embeds in a note. TODO: I have no idea how to do this yet, pub struct RawPage {
// // but I'll figure it out. pub id: i64,
// #[derive(Clone, Serialize, Deserialize, Debug)]
// pub struct Resource {
// pub id: String,
// pub content: String,
// }
//
// // A Breadcrumb is a component of a reference. Every element should
// // be clickable, although in practice what's going to happen is that
// // the user will be sent to the *page* with that note, then *scrolled*
// // to that note via anchor.
// #[derive(Clone, Debug)]
// pub struct Breadcrumb {
// pub note_id: String,
// pub summary: String,
// }
//
// // A Note is the heart of our system. It is a single object that has
// // a place in our system; it has a parent, but it also has embedded
// // references that allow it to navigate through a web of related
// // objects. It may have children. *AT THIS LAYER*, though, it is
// // returned as an array. It is up to the
// #[derive(Clone, Debug)]
// pub struct Note {
// pub id: String,
// pub parent_id: String,
// pub content: String,
// pub resources: Vec<Resource>,
// pub note_type: String, // Describes the relationship to the parent note.
// pub created: DateTime<Utc>,
// pub updated: DateTime<Utc>,
// pub viewed: DateTime<Utc>,
// pub deleted: Option<DateTime<Utc>>,
// }
//
// pub struct Reference {
// pub page_id: String,
// pub page_title: String,
// pub reference_summary_titles: Vec<Breadcrumbs>,
// pub reference_summary: String,
// }
pub struct Page {
pub slug: String, pub slug: String,
pub title: String, pub title: String,
// pub notes: Vec<Notes>, // The actual notes on this page. pub note_id: i64,
// pub references: Vec<Reference>, // All other notes that reference this page. pub creation_date: DateTime<Utc>,
// pub unlinked_references: Vec<Reference>, pub updated_date: DateTime<Utc>,
pub created: DateTime<Utc>, pub lastview_date: DateTime<Utc>,
pub updated: DateTime<Utc>, pub deleted_date: Option<DateTime<Utc>>,
pub viewed: DateTime<Utc>, }
pub deleted: Option<DateTime<Utc>>,
#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
pub struct RawNote {
pub id: i64,
pub uuid: String,
pub parent_id: i64,
pub parent_uuid: String,
pub content: String,
pub position: i64,
pub notetype: String,
pub creation_date: DateTime<Utc>,
pub updated_date: DateTime<Utc>,
pub lastview_date: DateTime<Utc>,
pub deleted_date: Option<DateTime<Utc>>,
}
#[derive(Clone, Serialize, Deserialize, Debug, Builder)]
pub struct NewPage {
pub slug: String,
pub title: String,
pub note_id: i64,
#[builder(default = r#"chrono::Utc::now()"#)]
pub creation_date: DateTime<Utc>,
#[builder(default = r#"chrono::Utc::now()"#)]
pub updated_date: DateTime<Utc>,
#[builder(default = r#"chrono::Utc::now()"#)]
pub lastview_date: DateTime<Utc>,
#[builder(default = r#"None"#)]
pub deleted_date: Option<DateTime<Utc>>,
}
#[derive(Clone, Serialize, Deserialize, Debug, Builder)]
pub struct NewNote {
#[builder(default = r#""".to_string()"#)]
pub uuid: String,
pub content: String,
#[builder(default = r#""note".to_string()"#)]
pub notetype: String,
#[builder(default = r#"chrono::Utc::now()"#)]
pub creation_date: DateTime<Utc>,
#[builder(default = r#"chrono::Utc::now()"#)]
pub updated_date: DateTime<Utc>,
#[builder(default = r#"chrono::Utc::now()"#)]
pub lastview_date: DateTime<Utc>,
#[builder(default = r#"None"#)]
pub deleted_date: Option<DateTime<Utc>>,
}
#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
pub(crate) struct JustSlugs {
pub slug: String,
}
#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
pub(crate) struct JustTitles {
title: String,
}
#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
pub(crate) struct JustId {
pub id: i64,
}
#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
pub(crate) struct PageTitles {
pub id: i64,
pub title: String,
}
#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
pub(crate) struct NoteRelationship {
pub parent_id: i64,
pub note_id: i64,
pub position: i64,
pub nature: String,
}
#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
pub(crate) struct RowCount {
pub count: i64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_build_new_note() {
let now = chrono::Utc::now();
let newnote = NewNoteBuilder::default()
.uuid("foo".to_string())
.content("bar".to_string())
.build()
.unwrap();
assert!((newnote.creation_date - now).num_minutes() < 1);
assert!((newnote.updated_date - now).num_minutes() < 1);
assert!((newnote.lastview_date - now).num_minutes() < 1);
assert!(newnote.deleted_date.is_none());
}
} }