Compare commits

...

2 Commits

Author SHA1 Message Date
Elf M. Sternberg 72fb3b11ee 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!
2020-10-26 18:54:56 -07:00
Elf M. Sternberg 739ff93427 Note/Page reference relationships now built. 2020-10-16 07:16:57 -07:00
10 changed files with 727 additions and 559 deletions

View File

@ -3,7 +3,10 @@ name = "nm-store-cli"
version = "0.1.0"
authors = ["Elf M. Sternberg <elf.sternberg@gmail.com>"]
edition = "2018"
description = "Command-line direct access to the notesmachine store."
readme = "./README.org"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -14,14 +14,10 @@ representations:
** Plans
*** TODO Make it possible to save a note
*** TODO Make it possible to retrieve a note
*** TODO Read how others use SQLX to initialize the database
*** TODO Implement CLI features
*** TODO Make it possible to connect two notes
*** TODO Make it possible to save a page
*** TODO Make it possible to connect a note to a page
*** TODO Make it possible to retrieve a collection of notes
*** TODO Make it possible to retrieve a page

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;
/// All the ways looking up objects can fail

View File

@ -1,8 +1,7 @@
mod errors;
mod row_structs;
mod reference_parser;
mod store;
mod structs;
mod reference_parser;
pub use crate::errors::NoteStoreError;
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 {
&NodeValue::Text(ref text) => Some(
NodeValue::Text(ref text) => Some(
RE_REFERENCES
.captures_iter(text)
.map(|t| String::from_utf8_lossy(&t.get(1).unwrap().as_bytes()).to_string())
@ -68,22 +68,23 @@ fn recase(title: &str) -> 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
.iter()
.map(|s| {
let c = s.chars().nth(0);
match c {
Some('#') => recase(s),
Some('[') => s.strip_prefix("[[").unwrap().strip_suffix("]]").unwrap().to_string(),
Some(_) => s.clone(),
_ => "".to_string(),
}
.map(|s| match s.chars().next() {
Some('#') => recase(s),
Some('[') => s.strip_prefix("[[").unwrap().strip_suffix("]]").unwrap().to_string(),
Some(_) => s.clone(),
_ => "".to_string(),
})
.filter(|s| s.len() > 0)
.filter(|s| s.is_empty())
.collect()
}
pub(crate) fn build_references(content: &str) -> Vec<String> {
build_page_titles(&find_links(content))
}
#[cfg(test)]
mod tests {
use super::*;
@ -122,4 +123,12 @@ Right? [[
];
assert!(res.iter().eq(expected.iter()), "{:?}", res);
}
#[test]
fn doesnt_crash_on_empty() {
let sample = "";
let res = build_page_titles(&find_links(sample));
let expected: Vec<String> = vec![];
assert!(res.iter().eq(expected.iter()), "{:?}", res);
}
}

View File

@ -1,105 +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 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 note_relationships;
DROP TABLE IF EXISTS pages;
DROP TABLE IF EXISTS favorites;
DROP TABLE IF EXISTS page_relationships;
DROP TABLE IF EXISTS favorites;
CREATE TABLE notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,10 @@
use chrono::{DateTime, Utc};
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use sqlx::{self, FromRow};
#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
pub(crate) struct RawPage {
pub struct RawPage {
pub id: i64,
pub slug: String,
pub title: String,
@ -15,10 +16,13 @@ pub(crate) struct RawPage {
}
#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
pub(crate) struct RawNote {
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>,
@ -26,58 +30,87 @@ pub(crate) struct RawNote {
pub deleted_date: Option<DateTime<Utc>>,
}
// // A Resource is either content or a URL to content that the
// // user embeds in a note. TODO: I have no idea how to do this yet,
// // but I'll figure it out.
// #[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 {
#[derive(Clone, Serialize, Deserialize, Debug, Builder)]
pub struct NewPage {
pub slug: String,
pub title: String,
// pub notes: Vec<Notes>, // The actual notes on this page.
// pub references: Vec<Reference>, // All other notes that reference this page.
// pub unlinked_references: Vec<Reference>,
pub created: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub viewed: DateTime<Utc>,
pub deleted: Option<DateTime<Utc>>,
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());
}
}