2020-09-29 21:27:11 +00:00
|
|
|
use chrono::{DateTime, Utc};
|
2020-11-04 20:54:17 +00:00
|
|
|
use derive_builder::Builder;
|
|
|
|
use friendly_id;
|
|
|
|
use shrinkwraprs::Shrinkwrap;
|
2020-09-29 00:33:43 +00:00
|
|
|
use sqlx::{self, FromRow};
|
|
|
|
|
2020-11-04 20:54:17 +00:00
|
|
|
// Kasten is German for "Box," and is used both because this is
|
|
|
|
// supposed to be a Zettlekasten, and because "Box" is a heavily
|
|
|
|
// reserved word in Rust. So, for that matter, are "crate" and
|
|
|
|
// "cargo," "cell," and so forth. If I'd wanted to go the Full
|
|
|
|
// Noguchi, I guess I could have used "envelope."
|
|
|
|
|
2020-11-05 01:53:25 +00:00
|
|
|
// In order to prevent arbitrary enumeration tokens from getting into
|
|
|
|
// the database, the private layer takes a very hard line on insisting
|
|
|
|
// that everything sent TO the datastore come in the enumerated
|
|
|
|
// format, and everything coming OUT of the database be converted back
|
|
|
|
// into an enumeration. These macros instantiate those objects
|
|
|
|
// and their conversions to/from strings.
|
2020-11-04 20:54:17 +00:00
|
|
|
|
2020-11-05 01:53:25 +00:00
|
|
|
macro_rules! build_conversion_enums {
|
|
|
|
( $ty:ident, $( $s:literal => $x:ident, )*) => {
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
|
|
pub enum $ty {
|
|
|
|
$( $x ), *
|
|
|
|
}
|
2020-11-04 20:54:17 +00:00
|
|
|
|
2020-11-05 01:53:25 +00:00
|
|
|
impl From<String> for $ty {
|
|
|
|
fn from(kind: String) -> Self {
|
|
|
|
match &kind[..] {
|
|
|
|
$( $s => $ty::$x, )*
|
|
|
|
_ => panic!("Illegal value in $ty database: {}", kind),
|
|
|
|
}
|
|
|
|
}
|
2020-11-04 20:54:17 +00:00
|
|
|
}
|
|
|
|
|
2020-11-05 01:53:25 +00:00
|
|
|
impl From<$ty> for String {
|
|
|
|
fn from(kind: $ty) -> Self {
|
|
|
|
match kind {
|
|
|
|
$( $ty::$x => $s ),*
|
|
|
|
}
|
|
|
|
.to_string()
|
|
|
|
}
|
2020-11-04 20:54:17 +00:00
|
|
|
}
|
|
|
|
|
2020-11-05 01:53:25 +00:00
|
|
|
impl $ty {
|
|
|
|
pub fn to_string(&self) -> String {
|
|
|
|
String::from(self.clone())
|
|
|
|
}
|
2020-11-04 20:54:17 +00:00
|
|
|
}
|
2020-11-05 01:53:25 +00:00
|
|
|
};
|
2020-11-04 20:54:17 +00:00
|
|
|
}
|
|
|
|
|
2020-11-05 01:53:25 +00:00
|
|
|
#[derive(Shrinkwrap, Clone)]
|
|
|
|
pub(crate) struct NoteId(pub String);
|
2020-11-04 20:54:17 +00:00
|
|
|
|
2020-11-05 01:53:25 +00:00
|
|
|
#[derive(Shrinkwrap, Clone)]
|
|
|
|
pub(crate) struct ParentId(pub String);
|
|
|
|
|
|
|
|
// The different kinds of objects we support.
|
|
|
|
|
|
|
|
build_conversion_enums!(
|
|
|
|
NoteKind,
|
|
|
|
"box" => Kasten,
|
|
|
|
"note" => Note,
|
|
|
|
"resource" => Resource,
|
|
|
|
);
|
|
|
|
|
|
|
|
// The different kinds of relationships we support. I do not yet
|
|
|
|
// know how to ensure that there is a maximum of one (a ->
|
|
|
|
// b)::Direct, and that for any (a -> b) there is no (b <- a), that
|
|
|
|
// is, nor, for that matter, do I know how to prevent cycles.
|
|
|
|
|
|
|
|
build_conversion_enums!(
|
|
|
|
RelationshipKind,
|
|
|
|
"direct" => Direct,
|
|
|
|
"reference" => Reference,
|
|
|
|
"embed" => Embed,
|
|
|
|
);
|
|
|
|
|
|
|
|
build_conversion_enums!(
|
|
|
|
KastenRelationshipKind,
|
|
|
|
"kasten" => Kasten,
|
|
|
|
"unacked" => Unacked,
|
|
|
|
"cancelled" => Cancelled,
|
|
|
|
);
|
2020-11-04 20:54:17 +00:00
|
|
|
|
|
|
|
// A Note is the base construct of our system. It represents a
|
|
|
|
// single note and contains information about its parent and location.
|
|
|
|
// This is the object *retrieved* from the database.
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, FromRow)]
|
|
|
|
pub(crate) struct RowNote {
|
2020-11-03 02:32:01 +00:00
|
|
|
pub id: String,
|
2020-11-04 20:54:17 +00:00
|
|
|
pub parent_id: Option<String>,
|
2020-10-27 01:54:56 +00:00
|
|
|
pub content: String,
|
2020-11-03 02:32:01 +00:00
|
|
|
pub kind: String,
|
2020-11-04 20:54:17 +00:00
|
|
|
pub location: i64,
|
2020-10-27 01:54:56 +00:00
|
|
|
pub creation_date: DateTime<Utc>,
|
|
|
|
pub updated_date: DateTime<Utc>,
|
|
|
|
pub lastview_date: DateTime<Utc>,
|
|
|
|
pub deleted_date: Option<DateTime<Utc>>,
|
|
|
|
}
|
2020-11-04 20:54:17 +00:00
|
|
|
|
|
|
|
/// A Note as it's returned from the private layer. This is
|
|
|
|
/// provided to ensure that the NoteKind is an enum, and that we
|
|
|
|
/// control the list of possible values stored in the database.
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct Note {
|
|
|
|
pub id: String,
|
|
|
|
pub parent_id: Option<String>,
|
|
|
|
pub content: String,
|
|
|
|
pub kind: NoteKind,
|
|
|
|
pub location: i64,
|
|
|
|
pub creation_date: DateTime<Utc>,
|
|
|
|
pub updated_date: DateTime<Utc>,
|
|
|
|
pub lastview_date: DateTime<Utc>,
|
|
|
|
pub deleted_date: Option<DateTime<Utc>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<RowNote> for Note {
|
|
|
|
fn from(note: RowNote) -> Self {
|
|
|
|
Self {
|
|
|
|
id: note.id,
|
|
|
|
parent_id: note.parent_id,
|
|
|
|
content: note.content,
|
|
|
|
kind: NoteKind::from(note.kind),
|
|
|
|
location: note.location,
|
|
|
|
creation_date: note.creation_date,
|
|
|
|
updated_date: note.updated_date,
|
|
|
|
lastview_date: note.lastview_date,
|
|
|
|
deleted_date: note.deleted_date,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A new Note object as it's inserted into the system. It has no
|
|
|
|
/// parent or location information; those are data relative to the
|
|
|
|
/// parent, and must be provided by the client. In the case of a
|
|
|
|
/// Kasten, no location or parent is necessary.
|
|
|
|
#[derive(Clone, Debug, Builder)]
|
|
|
|
pub struct NewNote {
|
|
|
|
#[builder(default = r#"friendly_id::create()"#)]
|
|
|
|
pub id: String,
|
|
|
|
pub content: String,
|
|
|
|
#[builder(default = r#"NoteKind::Note"#)]
|
|
|
|
pub kind: NoteKind,
|
|
|
|
#[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>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<NewNote> for Note {
|
|
|
|
/// Only used for building new kastens, so the decision- making is
|
|
|
|
/// limited to kasten-level things, like pointing to self and
|
|
|
|
/// having a location of zero.
|
|
|
|
fn from(note: NewNote) -> Self {
|
|
|
|
Self {
|
|
|
|
id: note.id,
|
|
|
|
parent_id: None,
|
|
|
|
content: note.content,
|
|
|
|
kind: note.kind,
|
|
|
|
location: 0,
|
|
|
|
creation_date: note.creation_date,
|
|
|
|
updated_date: note.updated_date,
|
|
|
|
lastview_date: note.lastview_date,
|
|
|
|
deleted_date: note.deleted_date,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, FromRow)]
|
|
|
|
pub(crate) struct JustId {
|
|
|
|
pub id: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, FromRow)]
|
|
|
|
pub(crate) struct PageTitle {
|
|
|
|
pub id: String,
|
|
|
|
pub content: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, FromRow)]
|
|
|
|
pub(crate) struct RowCount {
|
|
|
|
pub count: i64,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, FromRow)]
|
|
|
|
pub(crate) struct NoteRelationshipRow {
|
|
|
|
pub parent_id: String,
|
|
|
|
pub note_id: String,
|
|
|
|
pub location: i64,
|
|
|
|
pub kind: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
2020-11-05 16:30:02 +00:00
|
|
|
pub struct NoteRelationship {
|
2020-11-04 20:54:17 +00:00
|
|
|
pub parent_id: String,
|
|
|
|
pub note_id: String,
|
|
|
|
pub location: i64,
|
|
|
|
pub kind: RelationshipKind,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<NoteRelationshipRow> for NoteRelationship {
|
|
|
|
fn from(rel: NoteRelationshipRow) -> Self {
|
|
|
|
Self {
|
|
|
|
parent_id: rel.parent_id,
|
|
|
|
note_id: rel.note_id,
|
|
|
|
location: rel.location,
|
|
|
|
kind: RelationshipKind::from(rel.kind),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-05 01:53:25 +00:00
|
|
|
#[derive(Clone, Debug, FromRow)]
|
|
|
|
pub(crate) struct KastenRelationshipRow {
|
|
|
|
pub note_id: String,
|
|
|
|
pub kasten_id: String,
|
|
|
|
pub kind: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
2020-11-05 16:30:02 +00:00
|
|
|
pub struct KastenRelationship {
|
2020-11-05 01:53:25 +00:00
|
|
|
pub note_id: String,
|
|
|
|
pub kasten_id: String,
|
|
|
|
pub kind: KastenRelationshipKind,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<KastenRelationshipRow> for KastenRelationship {
|
|
|
|
fn from(rel: KastenRelationshipRow) -> Self {
|
|
|
|
Self {
|
|
|
|
kasten_id: rel.kasten_id,
|
|
|
|
note_id: rel.note_id,
|
|
|
|
kind: KastenRelationshipKind::from(rel.kind),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-04 20:54:17 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn can_build_new_note() {
|
|
|
|
let now = chrono::Utc::now();
|
|
|
|
let newnote = NewNoteBuilder::default().content("bar".to_string()).build().unwrap();
|
|
|
|
assert!(newnote.id.len() > 4);
|
|
|
|
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());
|
|
|
|
}
|
|
|
|
}
|