From 8ee71c76a3989e65b13eb51d35475071f93c5384 Mon Sep 17 00:00:00 2001 From: "Elf M. Sternberg" Date: Mon, 28 Sep 2020 17:33:43 -0700 Subject: [PATCH] FEAT Can initialize the database and fail to retrieve a page. In the great tradition of TPP, this is a win. We've gone through the test driven development, and there is so much *learning* here: - tokio::test NEEDS the threaded_schedular feature to report errors correctly - thiserror can't do enum variants the way I expected - Different error types for different returns is not kosher - Serde's configuration NEEDS a type, such as JSON, to work, - Rust has include_str!(), to embed text in a Rust program from an external source - SQLX is still a pain, but it's managable. --- server/nm-store/Cargo.toml | 12 ++++- server/nm-store/src/errors.rs | 15 +++++++ server/nm-store/src/lib.rs | 22 +++++++-- .../nm-store/src/sql/initialize_database.sql | 45 +++++++++++++++++++ server/nm-store/src/sql/select_one_page.sql | 1 + server/nm-store/src/store.rs | 37 +++++++++++++++ server/nm-store/src/structs.rs | 10 +++++ 7 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 server/nm-store/src/errors.rs create mode 100644 server/nm-store/src/sql/initialize_database.sql create mode 100644 server/nm-store/src/sql/select_one_page.sql create mode 100644 server/nm-store/src/store.rs create mode 100644 server/nm-store/src/structs.rs diff --git a/server/nm-store/Cargo.toml b/server/nm-store/Cargo.toml index 3326a38..d2debe5 100644 --- a/server/nm-store/Cargo.toml +++ b/server/nm-store/Cargo.toml @@ -6,11 +6,19 @@ edition = "2018" description = "Datastore interface layer for notesmachine." readme = "./README.org" + [features] -cli = ["nm-store-cli"] [dependencies] -nm-store-cli = { path: "../nm-store-cli" } +thiserror = "1.0.20" +tokio = { version = "0.2.22", features = ["rt-threaded"] } +serde = { version = "1.0.116", features = ["derive"] } +serde_json = "1.0.56" +sqlx = { version = "0.4.0-beta.1", default-features = false, features = [ + "runtime-tokio", + "sqlite", + "macros", +] } diff --git a/server/nm-store/src/errors.rs b/server/nm-store/src/errors.rs new file mode 100644 index 0000000..1af8672 --- /dev/null +++ b/server/nm-store/src/errors.rs @@ -0,0 +1,15 @@ +use sqlx; +use thiserror::Error; + +/// All the ways looking up objects can fail +#[derive(Error, Debug)] +pub enum NoteStoreError { + /// When building a new note for the back-end, it failed to parse + /// in some critical way. + #[error("Invalid Note Structure")] + InvalidNoteStructure(String), + + /// All other errors from the database. + #[error(transparent)] + DBError(#[from] sqlx::Error), +} diff --git a/server/nm-store/src/lib.rs b/server/nm-store/src/lib.rs index 31e1bb2..3958321 100644 --- a/server/nm-store/src/lib.rs +++ b/server/nm-store/src/lib.rs @@ -1,7 +1,23 @@ +mod errors; +mod store; +mod structs; + +pub use crate::store::NoteStore; +pub use crate::errors::NoteStoreError; + + #[cfg(test)] mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); + use super::*; + use tokio; + + #[tokio::test(threaded_scheduler)] + async fn it_works() { + let storagepool = NoteStore::new("sqlite://:memory:").await; + assert!(storagepool.is_ok()); + let storagepool = storagepool.unwrap(); + assert!(storagepool.reset_database().await.is_ok()); + let unfoundpage = storagepool.fetch_page("nonexistent-page").await; + assert!(unfoundpage.is_err()); } } diff --git a/server/nm-store/src/sql/initialize_database.sql b/server/nm-store/src/sql/initialize_database.sql new file mode 100644 index 0000000..d6a07c7 --- /dev/null +++ b/server/nm-store/src/sql/initialize_database.sql @@ -0,0 +1,45 @@ +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; + +CREATE TABLE notes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uuid TEXT NOT NULL UNIQUE, + content TEXT NULL, + notetype TEXT +); + +CREATE INDEX notes_uuids ON notes (uuid); + +CREATE TABLE pages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title text NOT NULL UNIQUE, + slug text NOT NULL UNIQUE, + note_id INTEGER, + FOREIGN KEY (note_id) REFERENCES notes (id) ON DELETE NO ACTION ON UPDATE NO ACTION +); + +CREATE INDEX pages_slugs ON pages (slug); + +CREATE TABLE favorites ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + position INTEGER NOT NULL +); + +CREATE TABLE note_relationships ( + note_id INTEGER NOT NULL, + parent_id INTEGER NOT NULL, + position INTEGER NOT NULL, + nature TEXT NOT NULL, + FOREIGN KEY (note_id) REFERENCES notes (id) ON DELETE NO ACTION ON UPDATE NO ACTION, + FOREIGN KEY (parent_id) REFERENCES notes (id) ON DELETE NO ACTION ON UPDATE NO ACTION +); + +CREATE TABLE page_relationships ( + note_id INTEGER NOT NULL, + page_id INTEGER NOT NULL, + FOREIGN KEY (note_id) references notes (id) ON DELETE NO ACTION ON UPDATE NO ACTION, + FOREIGN KEY (page_id) references pages (id) ON DELETE NO ACTION ON UPDATE NO ACTION +); diff --git a/server/nm-store/src/sql/select_one_page.sql b/server/nm-store/src/sql/select_one_page.sql new file mode 100644 index 0000000..44840f9 --- /dev/null +++ b/server/nm-store/src/sql/select_one_page.sql @@ -0,0 +1 @@ +SELECT id, title, slug, note_id FROM pages WHERE slug=?; diff --git a/server/nm-store/src/store.rs b/server/nm-store/src/store.rs new file mode 100644 index 0000000..bdc282a --- /dev/null +++ b/server/nm-store/src/store.rs @@ -0,0 +1,37 @@ +use sqlx; +use sqlx::sqlite::SqlitePool; +use std::sync::Arc; +use crate::errors::NoteStoreError; +use crate::structs::RawPage; + +/// A handle to our Sqlite database. +#[derive(Clone)] +pub struct NoteStore(Arc); + +type NoteResult = core::result::Result; +type SqlResult = sqlx::Result; + +impl NoteStore { + pub async fn new(url: &str) -> NoteResult { + let pool = SqlitePool::connect(url).await?; + Ok(NoteStore(Arc::new(pool))) + } + + /// This will erase all the data in the database. Only use this + /// if you're sure that's what you want. + pub async fn reset_database(&self) -> NoteResult<()> { + let initialize_sql = include_str!("sql/initialize_database.sql"); + sqlx::query(initialize_sql) + .execute(&*self.0) + .await?; + Ok(()) + } + + pub async fn fetch_page(&self, id: &str) -> SqlResult { + let select_one_page_sql = include_str!("sql/select_one_page.sql"); + sqlx::query_as(select_one_page_sql) + .bind(&id) + .fetch_one(&*self.0) + .await + } +} diff --git a/server/nm-store/src/structs.rs b/server/nm-store/src/structs.rs new file mode 100644 index 0000000..1df84e0 --- /dev/null +++ b/server/nm-store/src/structs.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; +use sqlx::{self, FromRow}; + +#[derive(Clone, Serialize, Deserialize, Debug, FromRow)] +pub struct RawPage { + id: i64, + slug: String, + title: String, + note_id: i64, +}