303 lines
14 KiB
Rust
303 lines
14 KiB
Rust
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
//! # 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?
|
|
//!
|
|
|
|
use crate::errors::NoteStoreError;
|
|
use crate::reference_parser::build_references;
|
|
use crate::store_private::*;
|
|
use crate::structs::*;
|
|
use sqlx::sqlite::SqlitePool;
|
|
use std::cmp;
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
/// A handle to our Sqlite database.
|
|
#[derive(Clone, Debug)]
|
|
pub struct NoteStore(Arc<SqlitePool>);
|
|
|
|
type NoteResult<T> = core::result::Result<T, NoteStoreError>;
|
|
|
|
// One thing that's pretty terrible about this code is that the
|
|
// Executor type in Sqlx is move-only, so it can only be used once per
|
|
// outgoing function call. That means that a lot of this code is
|
|
// internally duplicated, which sucks. I tried using the Acquire()
|
|
// trait, but its interaction with Executor was not very
|
|
// deterministic.
|
|
|
|
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<Self> {
|
|
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<(RawPage, Vec<RawNote>)> {
|
|
// let select_note_collection_for_root = include_str!("sql/select_note_collection_for_root.sql");
|
|
let mut tx = self.0.begin().await?;
|
|
let page = select_page_by_slug(&mut tx, slug).await?;
|
|
let note_id = page.note_id;
|
|
let notes = select_note_collection_from_root(&mut tx, note_id).await?;
|
|
tx.commit().await?;
|
|
Ok((page, notes))
|
|
}
|
|
|
|
/// Fetch page by title
|
|
///
|
|
/// Supports the use case of the user navigating to a page via
|
|
/// the page's formal title. Since the title is the key reference
|
|
/// of the system, if no page with that title is found, a page with
|
|
/// that title is generated automatically.
|
|
pub async fn get_page_by_title(&self, title: &str) -> NoteResult<(RawPage, Vec<RawNote>)> {
|
|
let mut tx = self.0.begin().await?;
|
|
let (page, notes) = match select_page_by_title(&mut tx, title).await {
|
|
Ok(page) => {
|
|
let note_id = page.note_id;
|
|
(page, select_note_collection_from_root(&mut tx, note_id).await?)
|
|
}
|
|
Err(sqlx::Error::RowNotFound) => {
|
|
let page = {
|
|
let new_root_note = create_unique_root_note();
|
|
let new_root_note_id = insert_one_new_note(&mut tx, &new_root_note).await?;
|
|
let new_page_slug = generate_slug(&mut tx, title).await?;
|
|
let new_page = create_new_page_for(&title, &new_page_slug, new_root_note_id);
|
|
let _ = insert_one_new_page(&mut tx, &new_page).await?;
|
|
select_page_by_title(&mut tx, &title).await?
|
|
};
|
|
let note_id = page.note_id;
|
|
(page, select_note_collection_from_root(&mut tx, note_id).await?)
|
|
}
|
|
Err(e) => return Err(NoteStoreError::DBError(e)),
|
|
};
|
|
tx.commit().await?;
|
|
Ok((page, notes))
|
|
}
|
|
|
|
/// Insert a note as the child of an existing note, at a set position.
|
|
pub async fn insert_nested_note(
|
|
&self,
|
|
note: &NewNote,
|
|
parent_note_uuid: &str,
|
|
position: i64,
|
|
) -> NoteResult<String> {
|
|
let mut new_note = note.clone();
|
|
new_note.uuid = friendly_id::create();
|
|
let references = build_references(¬e.content);
|
|
let mut tx = self.0.begin().await?;
|
|
|
|
// Start by building the note and putting it into its relationship.
|
|
println!("Select_note_id_for_uuid");
|
|
let parent_id: ParentId = select_note_id_for_uuid(&mut tx, parent_note_uuid).await?;
|
|
|
|
// Ensure new position is sane
|
|
println!("Assert Max Child Position");
|
|
let parent_max_position = assert_max_child_position_for_note(&mut tx, parent_id).await?;
|
|
let position = cmp::min(parent_max_position + 1, position);
|
|
|
|
println!("Insert_one_new_note");
|
|
let new_note_id = insert_one_new_note(&mut tx, &new_note).await?;
|
|
println!("make_room_for_new_note");
|
|
let _ = make_room_for_new_note(&mut tx, parent_id, position).await?;
|
|
println!("Insert_note_to_note_relationship");
|
|
let _ = insert_note_to_note_relationship(&mut tx, parent_id, new_note_id, position, "note").await?;
|
|
|
|
// From the references, make lists of pages that exist, and pages
|
|
// that do not.
|
|
println!("Find_all_page_references");
|
|
let found_references = find_all_page_references_for(&mut tx, &references).await?;
|
|
let new_references = diff_references(&references, &found_references);
|
|
let mut known_reference_ids: Vec<PageId> = Vec::new();
|
|
|
|
// Create the pages that don't exist
|
|
for one_reference in new_references.iter() {
|
|
let new_root_note = create_unique_root_note();
|
|
println!("Insert_one_new_root_note");
|
|
let new_root_note_id = insert_one_new_note(&mut tx, &new_root_note).await?;
|
|
println!("Generate_slug");
|
|
let new_page_slug = generate_slug(&mut tx, &one_reference).await?;
|
|
let new_page = create_new_page_for(&one_reference, &new_page_slug, new_root_note_id);
|
|
println!("insert_one_new_page");
|
|
known_reference_ids.push(insert_one_new_page(&mut tx, &new_page).await?)
|
|
}
|
|
|
|
// And associate the note with all the pages.
|
|
known_reference_ids.append(&mut found_references.iter().map(|r| PageId(r.id)).collect());
|
|
println!("insert_note_to_page_relationships");
|
|
let _ = insert_note_to_page_relationships(&mut tx, new_note_id, &known_reference_ids).await?;
|
|
|
|
tx.commit().await?;
|
|
Ok(new_note.uuid)
|
|
}
|
|
|
|
// This doesn't do anything with the references, as those are
|
|
// dependent entirely on the *content*, and not the *position*, of
|
|
// the note and the referenced page.
|
|
//
|
|
/// Move a note from one location to another.
|
|
pub async fn move_note(
|
|
&self,
|
|
note_uuid: &str,
|
|
old_parent_uuid: &str,
|
|
new_parent_uuid: &str,
|
|
new_position: i64,
|
|
) -> NoteResult<()> {
|
|
let all_uuids = vec![note_uuid, old_parent_uuid, new_parent_uuid];
|
|
let mut tx = self.0.begin().await?;
|
|
|
|
// This is one of the few cases where we we're getting IDs for
|
|
// notes, but the nature of the ID isn't known at this time.
|
|
// This has to be handled manually, in the next paragraph
|
|
// below.
|
|
let found_id_vec = bulk_select_ids_for_note_uuids(&mut tx, &all_uuids).await?;
|
|
let found_ids: HashMap<String, i64> = found_id_vec.into_iter().collect();
|
|
if found_ids.len() != 3 {
|
|
return Err(NoteStoreError::NotFound);
|
|
}
|
|
|
|
let old_parent_id = ParentId(*found_ids.get(old_parent_uuid).unwrap());
|
|
let new_parent_id = ParentId(*found_ids.get(new_parent_uuid).unwrap());
|
|
let note_id = NoteId(*found_ids.get(note_uuid).unwrap());
|
|
|
|
let old_note = get_note_to_note_relationship(&mut tx, old_parent_id, note_id).await?;
|
|
let old_note_position = old_note.position;
|
|
let old_note_nature = &old_note.nature;
|
|
|
|
let _ = delete_note_to_note_relationship(&mut tx, old_parent_id, note_id).await?;
|
|
let _ = close_hole_for_deleted_note(&mut tx, old_parent_id, old_note_position).await?;
|
|
let parent_max_position = assert_max_child_position_for_note(&mut tx, new_parent_id).await?;
|
|
let new_position = cmp::min(parent_max_position + 1, new_position);
|
|
let _ = make_room_for_new_note(&mut tx, new_parent_id, new_position).await?;
|
|
let _ =
|
|
insert_note_to_note_relationship(&mut tx, new_parent_id, note_id, new_position, old_note_nature).await?;
|
|
tx.commit().await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Embed or reference a note from a different location.
|
|
pub async fn reference_or_embed_note(
|
|
&self,
|
|
note_uuid: &str,
|
|
new_parent_uuid: &str,
|
|
new_position: i64,
|
|
new_nature: &str,
|
|
) -> NoteResult<()> {
|
|
let mut tx = self.0.begin().await?;
|
|
let existing_note_id: NoteId = NoteId(select_note_id_for_uuid(&mut tx, note_uuid).await?.0);
|
|
let new_parent_id: ParentId = select_note_id_for_uuid(&mut tx, new_parent_uuid).await?;
|
|
let _ = make_room_for_new_note(&mut tx, new_parent_id, new_position).await?;
|
|
let _ = insert_note_to_note_relationship(&mut tx, new_parent_id, existing_note_id, new_position, new_nature)
|
|
.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_uuid: &str, note_parent_uuid: &str) -> NoteResult<()> {
|
|
let mut tx = self.0.begin().await?;
|
|
let condemned_note_id: NoteId = NoteId(select_note_id_for_uuid(&mut tx, note_uuid).await?.0);
|
|
let note_parent_id: ParentId = select_note_id_for_uuid(&mut tx, note_parent_uuid).await?;
|
|
let _ = delete_note_to_note_relationship(&mut tx, note_parent_id, condemned_note_id);
|
|
if count_existing_note_relationships(&mut tx, condemned_note_id).await? == 0 {
|
|
let _ = delete_note_to_page_relationships(&mut tx, condemned_note_id).await?;
|
|
let _ = delete_note(&mut tx, condemned_note_id).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_uuid: &str, content: &str) -> NoteResult<()> {
|
|
let references = build_references(&content);
|
|
|
|
let mut tx = self.0.begin().await?;
|
|
|
|
let note_id: NoteId = NoteId(select_note_id_for_uuid(&mut tx, note_uuid).await?.0);
|
|
let _ = update_note_content(&mut tx, note_id, &content).await?;
|
|
|
|
let found_references = find_all_page_references_for(&mut tx, &references).await?;
|
|
let new_references = diff_references(&references, &found_references);
|
|
let mut known_reference_ids: Vec<PageId> = Vec::new();
|
|
|
|
// Create the pages that don't exist
|
|
for one_reference in new_references.iter() {
|
|
let new_root_note = create_unique_root_note();
|
|
let new_root_note_id = insert_one_new_note(&mut tx, &new_root_note).await?;
|
|
let new_page_slug = generate_slug(&mut tx, &one_reference).await?;
|
|
let new_page = create_new_page_for(&one_reference, &new_page_slug, new_root_note_id);
|
|
known_reference_ids.push(insert_one_new_page(&mut tx, &new_page).await?)
|
|
}
|
|
|
|
// And associate the note with all the pages.
|
|
known_reference_ids.append(&mut found_references.iter().map(|r| PageId(r.id)).collect());
|
|
let _ = insert_note_to_page_relationships(&mut tx, note_id, &known_reference_ids).await?;
|
|
|
|
tx.commit().await?;
|
|
Ok(())
|
|
}
|
|
}
|