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.
This commit is contained in:
parent
2bd7c0aaad
commit
8ee71c76a3
|
@ -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",
|
||||
] }
|
||||
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
SELECT id, title, slug, note_id FROM pages WHERE slug=?;
|
|
@ -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<SqlitePool>);
|
||||
|
||||
type NoteResult<T> = core::result::Result<T, NoteStoreError>;
|
||||
type SqlResult<T> = sqlx::Result<T>;
|
||||
|
||||
impl NoteStore {
|
||||
pub async fn new(url: &str) -> NoteResult<Self> {
|
||||
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<RawPage> {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
Loading…
Reference in New Issue