notesmachine/server/nm-store/src/store.rs

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(&note.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(())
}
}