MERGE Shrinkwrap and Comrak updates.
This commit is contained in:
		
						commit
						0f98dc4523
					
				|  | @ -16,6 +16,7 @@ thiserror = "1.0.20" | ||||||
| derive_builder = "0.9.0" | derive_builder = "0.9.0" | ||||||
| lazy_static = "1.4.0" | lazy_static = "1.4.0" | ||||||
| comrak = "0.8.2" | comrak = "0.8.2" | ||||||
|  | shrinkwraprs = "0.3.0" | ||||||
| regex = "1.3.9" | regex = "1.3.9" | ||||||
| slug = "0.1.4" | slug = "0.1.4" | ||||||
| tokio = { version = "0.2.22", features = ["rt-threaded", "blocking"] } | tokio = { version = "0.2.22", features = ["rt-threaded", "blocking"] } | ||||||
|  |  | ||||||
|  | @ -1,20 +1,27 @@ | ||||||
| use crate::errors::NoteStoreError; | use crate::errors::NoteStoreError; | ||||||
| use crate::row_structs::{ | use crate::row_structs::{ | ||||||
|     JustId, JustSlugs, NewNote, NewNoteBuilder, NewPage, | 	JustId, JustSlugs, NewNote, NewNoteBuilder, NewPage, NewPageBuilder, NoteRelationship, RawNote, | ||||||
| 	NewPageBuilder, NoteRelationship, RawNote, RawPage, | 	RawPage, | ||||||
| }; | }; | ||||||
| use friendly_id; | use friendly_id; | ||||||
| use lazy_static::lazy_static; | use lazy_static::lazy_static; | ||||||
| use std::collections::HashMap; |  | ||||||
| use regex::Regex; | use regex::Regex; | ||||||
|  | use shrinkwraprs::Shrinkwrap; | ||||||
| use slug::slugify; | use slug::slugify; | ||||||
| use sqlx; | use sqlx; | ||||||
| use sqlx::{ | use sqlx::{ | ||||||
|     sqlite::{Sqlite, SqlitePool, SqliteRow}, | 	sqlite::{Sqlite, SqlitePool, SqliteRow}, | ||||||
|     Done, Executor, Row | 	Done, Executor, Row, | ||||||
| }; | }; | ||||||
|  | use std::collections::HashMap; | ||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
| 
 | 
 | ||||||
|  | #[derive(Shrinkwrap, Copy, Clone)] | ||||||
|  | struct ParentId(i64); | ||||||
|  | 
 | ||||||
|  | #[derive(Shrinkwrap, Copy, Clone)] | ||||||
|  | struct NoteId(i64); | ||||||
|  | 
 | ||||||
| /// A handle to our Sqlite database.
 | /// A handle to our Sqlite database.
 | ||||||
| #[derive(Clone, Debug)] | #[derive(Clone, Debug)] | ||||||
| pub struct NoteStore(Arc<SqlitePool>); | pub struct NoteStore(Arc<SqlitePool>); | ||||||
|  | @ -23,146 +30,150 @@ type NoteResult<T> = core::result::Result<T, NoteStoreError>; | ||||||
| type SqlResult<T> = sqlx::Result<T>; | type SqlResult<T> = sqlx::Result<T>; | ||||||
| 
 | 
 | ||||||
| impl NoteStore { | impl NoteStore { | ||||||
|     pub async fn new(url: &str) -> NoteResult<Self> { | 	pub async fn new(url: &str) -> NoteResult<Self> { | ||||||
|         let pool = SqlitePool::connect(url).await?; | 		let pool = SqlitePool::connect(url).await?; | ||||||
|         Ok(NoteStore(Arc::new(pool))) | 		Ok(NoteStore(Arc::new(pool))) | ||||||
|     } | 	} | ||||||
| 
 | 
 | ||||||
|     // Erase all the data in the database and restore it
 | 	// Erase all the data in the database and restore it
 | ||||||
|     // to its original empty form.  Do not use unless you
 | 	// to its original empty form.  Do not use unless you
 | ||||||
|     // really, really want that to happen.
 | 	// really, really want that to happen.
 | ||||||
|     pub async fn reset_database(&self) -> NoteResult<()> { | 	pub async fn reset_database(&self) -> NoteResult<()> { | ||||||
|         reset_database(&*self.0).await.map_err(NoteStoreError::DBError) | 		reset_database(&*self.0) | ||||||
|     } | 			.await | ||||||
|  | 			.map_err(NoteStoreError::DBError) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
|     /// Fetch page by slug
 | 	/// Fetch page by slug
 | ||||||
|     ///
 | 	///
 | ||||||
|     /// Supports the use case of the user navigating to a known place
 | 	/// 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
 | 	/// 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
 | 	/// 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
 | 	/// this use case says that in the event of a failure to find the
 | ||||||
|     /// requested page, return a basic NotFound.
 | 	/// requested page, return a basic NotFound.
 | ||||||
|     pub async fn get_page_by_slug(&self, slug: &str) -> NoteResult<(RawPage, Vec<RawNote>)> { | 	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 select_note_collection_for_root = include_str!("sql/select_note_collection_for_root.sql");
 | ||||||
|         let mut tx = self.0.begin().await?; | 		let mut tx = self.0.begin().await?; | ||||||
|         let page = select_page_by_slug(&mut tx, slug).await?; | 		let page = select_page_by_slug(&mut tx, slug).await?; | ||||||
| 		let note_id = page.note_id; | 		let note_id = page.note_id; | ||||||
| 		let notes = select_note_collection_from_root(&mut tx, note_id).await?; | 		let notes = select_note_collection_from_root(&mut tx, note_id).await?; | ||||||
|         tx.commit().await?; | 		tx.commit().await?; | ||||||
|         Ok((page, notes)) | 		Ok((page, notes)) | ||||||
|     } | 	} | ||||||
| 
 | 
 | ||||||
|     /// Fetch page by title
 | 	/// Fetch page by title
 | ||||||
|     ///
 | 	///
 | ||||||
|     /// Supports the use case of the user navigating to a page via
 | 	/// Supports the use case of the user navigating to a page via
 | ||||||
|     /// the page's formal title.  Since the title is the key reference
 | 	/// 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
 | 	/// of the system, if no page with that title is found, a page with
 | ||||||
|     /// that title is generated automatically.
 | 	/// that title is generated automatically.
 | ||||||
|     pub async fn get_page_by_title(&self, title: &str) -> NoteResult<(RawPage, Vec<RawNote>)> { | 	pub async fn get_page_by_title(&self, title: &str) -> NoteResult<(RawPage, Vec<RawNote>)> { | ||||||
|         let mut tx = self.0.begin().await?; | 		let mut tx = self.0.begin().await?; | ||||||
|         let (page, notes) = match select_page_by_title(&mut tx, title).await { | 		let (page, notes) = match select_page_by_title(&mut tx, title).await { | ||||||
|             Ok(page) => { | 			Ok(page) => { | ||||||
|                 let note_id = page.note_id; | 				let note_id = page.note_id; | ||||||
|                 (page, select_note_collection_from_root(&mut tx, note_id).await?) | 				( | ||||||
|             } | 					page, | ||||||
|             Err(sqlx::Error::RowNotFound) => { | 					select_note_collection_from_root(&mut tx, note_id).await?, | ||||||
|                 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?; | 			Err(sqlx::Error::RowNotFound) => { | ||||||
|                     let new_page_slug = generate_slug(&mut tx, title).await?; | 				let page = { | ||||||
|                     let new_page = create_new_page_for(&title, &new_page_slug, new_root_note_id); | 					let new_root_note = create_unique_root_note(); | ||||||
|                     let _ = insert_one_new_page(&mut tx, &new_page).await?; | 					let new_root_note_id = insert_one_new_note(&mut tx, &new_root_note).await?; | ||||||
|                     select_page_by_title(&mut tx, &title).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 note_id = page.note_id; | 					let _ = insert_one_new_page(&mut tx, &new_page).await?; | ||||||
|                 (page, select_note_collection_from_root(&mut tx, note_id).await?) | 					select_page_by_title(&mut tx, &title).await? | ||||||
|             } | 				}; | ||||||
|             Err(e) => return Err(NoteStoreError::DBError(e)), | 				let note_id = page.note_id; | ||||||
|         }; | 				( | ||||||
|         tx.commit().await?; | 					page, | ||||||
|         Ok((page, notes)) | 					select_note_collection_from_root(&mut tx, note_id).await?, | ||||||
|     } | 				) | ||||||
|  | 			} | ||||||
|  | 			Err(e) => return Err(NoteStoreError::DBError(e)), | ||||||
|  | 		}; | ||||||
|  | 		tx.commit().await?; | ||||||
|  | 		Ok((page, notes)) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	// TODO: Make sure the position is sane.
 | 	// TODO: Make sure the position is sane.
 | ||||||
| 	/// Insert a note as the child of an existing note, at a set position.
 | 	/// Insert a note as the child of an existing note, at a set position.
 | ||||||
|     pub async fn insert_nested_note( | 	pub async fn insert_nested_note( | ||||||
|         &self, | 		&self, | ||||||
|         note: &NewNote, | 		note: &NewNote, | ||||||
|         parent_note_uuid: &str, | 		parent_note_uuid: &str, | ||||||
|         position: i64, | 		position: i64, | ||||||
|     ) -> NoteResult<String> { | 	) -> NoteResult<String> { | ||||||
|         let mut new_note = note.clone(); | 		let mut new_note = note.clone(); | ||||||
|         new_note.uuid = friendly_id::create(); | 		new_note.uuid = friendly_id::create(); | ||||||
|         let mut tx = self.0.begin().await?; | 		let mut tx = self.0.begin().await?; | ||||||
|         let parent_id = select_note_id_for_uuid(&mut tx, parent_note_uuid).await?; | 		let parent_id = select_note_id_for_uuid(&mut tx, parent_note_uuid).await?; | ||||||
|         let new_note_id = insert_one_new_note(&mut tx, &new_note).await?; | 		let new_note_id = insert_one_new_note(&mut tx, &new_note).await?; | ||||||
|         let _ = make_room_for_new_note(&mut tx, parent_id, position).await?; | 		let _ = make_room_for_new_note(&mut tx, parent_id, position).await?; | ||||||
|         let _ = insert_note_note_relationship(&mut tx, parent_id, new_note_id, position).await?; | 		let _ = insert_note_note_relationship(&mut tx, parent_id, new_note_id, position).await?; | ||||||
|         tx.commit().await?; | 		tx.commit().await?; | ||||||
|         Ok(new_note.uuid) | 		Ok(new_note.uuid) | ||||||
|     } | 	} | ||||||
| 
 | 
 | ||||||
| 	// TODO: Make sure the new position is sane.
 | 	// TODO: Make sure the new position is sane.
 | ||||||
| 	/// Move a note from one location to another.
 | 	/// Move a note from one location to another.
 | ||||||
|     pub async fn move_note( | 	pub async fn move_note( | ||||||
|         &self, | 		&self, | ||||||
|         note_uuid: &str, | 		note_uuid: &str, | ||||||
|         old_parent_uuid: &str, | 		old_parent_uuid: &str, | ||||||
|         new_parent_uuid: &str, | 		new_parent_uuid: &str, | ||||||
|         new_position: i64, | 		new_position: i64, | ||||||
|     ) -> NoteResult<()> { | 	) -> NoteResult<()> { | ||||||
|         let sample = vec![note_uuid, old_parent_uuid, new_parent_uuid]; | 		let sample = vec![note_uuid, old_parent_uuid, new_parent_uuid]; | ||||||
|         let mut tx = self.0.begin().await?; | 		let mut tx = self.0.begin().await?; | ||||||
|         let found_id_vec = bulk_select_ids_for_note_uuids(&mut tx, &sample) |  | ||||||
| 			.await?; |  | ||||||
| 
 | 
 | ||||||
| 		let found_ids: HashMap<String, i64> = found_id_vec | 		// This is one of the few cases where we we're getting IDs for
 | ||||||
| 			.into_iter() | 		// notes, but the nature of the ID isn't known at this time.
 | ||||||
| 			.collect(); | 		// This has to be handled manually, in the next paragraph
 | ||||||
|  | 		// below.
 | ||||||
|  | 		let found_id_vec = bulk_select_ids_for_note_uuids(&mut tx, &sample).await?; | ||||||
|  | 		let found_ids: HashMap<String, i64> = found_id_vec.into_iter().collect(); | ||||||
|  | 		if found_ids.len() != 3 { | ||||||
|  | 			return Err(NoteStoreError::NotFound); | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
|         if found_ids.len() != 3 { | 		let old_parent_id = ParentId(*found_ids.get(old_parent_uuid).unwrap()); | ||||||
|             return Err(NoteStoreError::NotFound); | 		let new_parent_id = ParentId(*found_ids.get(new_parent_uuid).unwrap()); | ||||||
|         } | 		let note_id = NoteId(*found_ids.get(note_uuid).unwrap()); | ||||||
| 
 | 
 | ||||||
|         let old_parent_id = *found_ids.get(old_parent_uuid).unwrap(); | 		let old_note_position = get_note_note_relationship(&mut tx, old_parent_id, note_id) | ||||||
|         let new_parent_id = *found_ids.get(new_parent_uuid).unwrap(); | 			.await? | ||||||
|         let note_id = *found_ids.get(note_uuid).unwrap(); | 			.position; | ||||||
| 
 | 
 | ||||||
|         let old_note_position = get_note_note_relationship(&mut tx, old_parent_id, note_id) | 		let _ = delete_note_note_relationship(&mut tx, old_parent_id, note_id).await?; | ||||||
|             .await? | 		let _ = close_hole_for_deleted_note(&mut tx, old_parent_id, old_note_position).await?; | ||||||
|             .position; | 		let _ = make_room_for_new_note(&mut tx, new_parent_id, new_position).await?; | ||||||
| 
 | 		let _ = | ||||||
|         let _ = delete_note_note_relationship(&mut tx, old_parent_id, note_id).await?; | 			insert_note_note_relationship(&mut tx, new_parent_id, note_id, new_position).await?; | ||||||
|         let _ = close_hole_for_deleted_note(&mut tx, old_parent_id, old_note_position).await?; |  | ||||||
|         let _ = make_room_for_new_note(&mut tx, new_parent_id, new_position).await?; |  | ||||||
|         let _ = insert_note_note_relationship(&mut tx, new_parent_id, note_id, new_position).await?; |  | ||||||
| 		tx.commit().await?; | 		tx.commit().await?; | ||||||
| 		Ok(()) | 		Ok(()) | ||||||
|     } | 	} | ||||||
| 
 | 
 | ||||||
| 	/// Embed or reference a note from a different location.
 | 	/// Embed or reference a note from a different location.
 | ||||||
| 	pub async fn reference_or_embed_note( | 	pub async fn reference_or_embed_note( | ||||||
| 		&self, | 		&self, | ||||||
|         note_uuid: &str, | 		note_uuid: &str, | ||||||
|         new_parent_uuid: &str, | 		new_parent_uuid: &str, | ||||||
|         new_position: i64, | 		new_position: i64, | ||||||
| 		new_nature: &str) -> NoteResult<()> { | 		new_nature: &str, | ||||||
|  | 	) -> NoteResult<()> { | ||||||
| 		todo!() | 		todo!() | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	/// Delete a note
 | 	/// Delete a note
 | ||||||
| 	pub async fn delete_note( | 	pub async fn delete_note(&self, note_uuid: &str, note_parent_uuid: &str) -> NoteResult<()> { | ||||||
| 		&self, |  | ||||||
| 		note_uuid: &str, |  | ||||||
| 		note_parent_uuid: &str) -> NoteResult<()> { |  | ||||||
| 		todo!() | 		todo!() | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	/// Update a note's content
 | 	/// Update a note's content
 | ||||||
| 	pub async fn update_note_content( | 	pub async fn update_note_content(&self, note_uuid: &str, content: &str) -> NoteResult<()> { | ||||||
| 		&self, |  | ||||||
| 		note_uuid: &str, |  | ||||||
| 		content: &str) -> NoteResult<()> { |  | ||||||
| 		todo!() | 		todo!() | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -179,143 +190,158 @@ impl NoteStore { | ||||||
| 
 | 
 | ||||||
| async fn reset_database<'a, E>(executor: E) -> SqlResult<()> | async fn reset_database<'a, E>(executor: E) -> SqlResult<()> | ||||||
| where | where | ||||||
|     E: Executor<'a, Database = Sqlite>, | 	E: Executor<'a, Database = Sqlite>, | ||||||
| { | { | ||||||
|     let initialize_sql = include_str!("sql/initialize_database.sql"); | 	let initialize_sql = include_str!("sql/initialize_database.sql"); | ||||||
|     sqlx::query(initialize_sql).execute(executor).await.map(|_| ()) | 	sqlx::query(initialize_sql) | ||||||
|  | 		.execute(executor) | ||||||
|  | 		.await | ||||||
|  | 		.map(|_| ()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async fn select_page_by_slug<'a, E>(executor: E, slug: &str) -> SqlResult<RawPage> | async fn select_page_by_slug<'a, E>(executor: E, slug: &str) -> SqlResult<RawPage> | ||||||
| where | where | ||||||
|     E: Executor<'a, Database = Sqlite>, | 	E: Executor<'a, Database = Sqlite>, | ||||||
| { | { | ||||||
|     let select_one_page_by_slug_sql = concat!( | 	let select_one_page_by_slug_sql = concat!( | ||||||
|         "SELECT id, title, slug, note_id, creation_date, updated_date, ", | 		"SELECT id, title, slug, note_id, creation_date, updated_date, ", | ||||||
|         "lastview_date, deleted_date FROM pages WHERE slug=?;" | 		"lastview_date, deleted_date FROM pages WHERE slug=?;" | ||||||
|     ); | 	); | ||||||
|     Ok(sqlx::query_as(&select_one_page_by_slug_sql) | 	Ok(sqlx::query_as(&select_one_page_by_slug_sql) | ||||||
|         .bind(&slug) | 		.bind(&slug) | ||||||
|         .fetch_one(executor) | 		.fetch_one(executor) | ||||||
|         .await?) | 		.await?) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async fn select_page_by_title<'a, E>(executor: E, title: &str) -> SqlResult<RawPage> | async fn select_page_by_title<'a, E>(executor: E, title: &str) -> SqlResult<RawPage> | ||||||
| where | where | ||||||
|     E: Executor<'a, Database = Sqlite>, | 	E: Executor<'a, Database = Sqlite>, | ||||||
| { | { | ||||||
|     let select_one_page_by_title_sql = concat!( | 	let select_one_page_by_title_sql = concat!( | ||||||
|         "SELECT id, title, slug, note_id, creation_date, updated_date, ", | 		"SELECT id, title, slug, note_id, creation_date, updated_date, ", | ||||||
|         "lastview_date, deleted_date FROM pages WHERE title=?;" | 		"lastview_date, deleted_date FROM pages WHERE title=?;" | ||||||
|     ); | 	); | ||||||
|     Ok(sqlx::query_as(&select_one_page_by_title_sql) | 	Ok(sqlx::query_as(&select_one_page_by_title_sql) | ||||||
|         .bind(&title) | 		.bind(&title) | ||||||
|         .fetch_one(executor) | 		.fetch_one(executor) | ||||||
|         .await?) | 		.await?) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async fn select_note_id_for_uuid<'a, E>(executor: E, uuid: &str) -> SqlResult<i64> | async fn select_note_id_for_uuid<'a, E>(executor: E, uuid: &str) -> SqlResult<ParentId> | ||||||
| where | where | ||||||
|     E: Executor<'a, Database = Sqlite>, | 	E: Executor<'a, Database = Sqlite>, | ||||||
| { | { | ||||||
|     let select_note_id_for_uuid_sql = "SELECT id FROM notes WHERE uuid = ?"; | 	let select_note_id_for_uuid_sql = "SELECT id FROM notes WHERE uuid = ?"; | ||||||
|     let id: JustId = sqlx::query_as(&select_note_id_for_uuid_sql) | 	let id: JustId = sqlx::query_as(&select_note_id_for_uuid_sql) | ||||||
|         .bind(&uuid) | 		.bind(&uuid) | ||||||
|         .fetch_one(executor) | 		.fetch_one(executor) | ||||||
|         .await?; | 		.await?; | ||||||
|     Ok(id.id) | 	Ok(ParentId(id.id)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async fn make_room_for_new_note<'a, E>(executor: E, parent_id: i64, position: i64) -> SqlResult<()> | async fn make_room_for_new_note<'a, E>( | ||||||
|  | 	executor: E, | ||||||
|  | 	parent_id: ParentId, | ||||||
|  | 	position: i64, | ||||||
|  | ) -> SqlResult<()> | ||||||
| where | where | ||||||
|     E: Executor<'a, Database = Sqlite>, | 	E: Executor<'a, Database = Sqlite>, | ||||||
| { | { | ||||||
|     let make_room_for_new_note_sql = concat!( | 	let make_room_for_new_note_sql = concat!( | ||||||
|         "UPDATE note_relationships ", | 		"UPDATE note_relationships ", | ||||||
|         "SET position = position + 1 ", | 		"SET position = position + 1 ", | ||||||
|         "WHERE position >= ? and parent_id = ?;" | 		"WHERE position >= ? and parent_id = ?;" | ||||||
|     ); | 	); | ||||||
| 
 | 
 | ||||||
|     sqlx::query(make_room_for_new_note_sql) | 	sqlx::query(make_room_for_new_note_sql) | ||||||
|         .bind(&position) | 		.bind(&position) | ||||||
|         .bind(&parent_id) | 		.bind(&*parent_id) | ||||||
|         .execute(executor) | 		.execute(executor) | ||||||
|         .await | 		.await | ||||||
|         .map(|_| ()) | 		.map(|_| ()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async fn insert_note_note_relationship<'a, E>(executor: E, parent_id: i64, note_id: i64, position: i64) -> SqlResult<()> | async fn insert_note_note_relationship<'a, E>( | ||||||
|  | 	executor: E, | ||||||
|  | 	parent_id: ParentId, | ||||||
|  | 	note_id: NoteId, | ||||||
|  | 	position: i64, | ||||||
|  | ) -> SqlResult<()> | ||||||
| where | where | ||||||
|     E: Executor<'a, Database = Sqlite>, | 	E: Executor<'a, Database = Sqlite>, | ||||||
| { | { | ||||||
|     let insert_note_note_relationship_sql = concat!( | 	let insert_note_note_relationship_sql = concat!( | ||||||
|         "INSERT INTO note_relationships (parent_id, note_id, position, nature) ", | 		"INSERT INTO note_relationships (parent_id, note_id, position, nature) ", | ||||||
|         "values (?, ?, ?, ?)" | 		"values (?, ?, ?, ?)" | ||||||
|     ); | 	); | ||||||
| 
 | 
 | ||||||
|     sqlx::query(insert_note_note_relationship_sql) | 	sqlx::query(insert_note_note_relationship_sql) | ||||||
|         .bind(&parent_id) | 		.bind(&*parent_id) | ||||||
|         .bind(¬e_id) | 		.bind(&*note_id) | ||||||
|         .bind(&position) | 		.bind(&position) | ||||||
|         .bind("note") | 		.bind("note") | ||||||
|         .execute(executor) | 		.execute(executor) | ||||||
|         .await | 		.await | ||||||
|         .map(|_| ()) | 		.map(|_| ()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async fn select_note_collection_from_root<'a, E>(executor: E, root: i64) -> SqlResult<Vec<RawNote>> | async fn select_note_collection_from_root<'a, E>(executor: E, root: i64) -> SqlResult<Vec<RawNote>> | ||||||
| where | where | ||||||
|     E: Executor<'a, Database = Sqlite>, | 	E: Executor<'a, Database = Sqlite>, | ||||||
| { | { | ||||||
|     let select_note_collection_from_root_sql = include_str!("sql/select_note_collection_from_root.sql"); | 	let select_note_collection_from_root_sql = | ||||||
|     Ok(sqlx::query_as(&select_note_collection_from_root_sql) | 		include_str!("sql/select_note_collection_from_root.sql"); | ||||||
|         .bind(&root) | 	Ok(sqlx::query_as(&select_note_collection_from_root_sql) | ||||||
|         .fetch_all(executor) | 		.bind(&root) | ||||||
|         .await?) | 		.fetch_all(executor) | ||||||
|  | 		.await?) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async fn insert_one_new_note<'a, E>(executor: E, note: &NewNote) -> SqlResult<i64> | async fn insert_one_new_note<'a, E>(executor: E, note: &NewNote) -> SqlResult<NoteId> | ||||||
| where | where | ||||||
|     E: Executor<'a, Database = Sqlite>, | 	E: Executor<'a, Database = Sqlite>, | ||||||
| { | { | ||||||
|     let insert_one_note_sql = concat!( | 	let insert_one_note_sql = concat!( | ||||||
|         "INSERT INTO notes ( ", | 		"INSERT INTO notes ( ", | ||||||
|         "	   uuid, ", | 		"	   uuid, ", | ||||||
|         "	   content, ", | 		"	   content, ", | ||||||
|         "	   notetype, ", | 		"	   notetype, ", | ||||||
|         "	   creation_date, ", | 		"	   creation_date, ", | ||||||
|         "	   updated_date, ", | 		"	   updated_date, ", | ||||||
|         "	   lastview_date) ", | 		"	   lastview_date) ", | ||||||
|         "VALUES (?, ?, ?, ?, ?, ?);" | 		"VALUES (?, ?, ?, ?, ?, ?);" | ||||||
|     ); | 	); | ||||||
| 
 | 
 | ||||||
|     Ok(sqlx::query(insert_one_note_sql) | 	Ok(NoteId( | ||||||
|         .bind(¬e.uuid) | 		sqlx::query(insert_one_note_sql) | ||||||
|         .bind(¬e.content) | 			.bind(¬e.uuid) | ||||||
|         .bind(¬e.notetype) | 			.bind(¬e.content) | ||||||
|         .bind(¬e.creation_date) | 			.bind(¬e.notetype) | ||||||
|         .bind(¬e.updated_date) | 			.bind(¬e.creation_date) | ||||||
|         .bind(¬e.lastview_date) | 			.bind(¬e.updated_date) | ||||||
|         .execute(executor) | 			.bind(¬e.lastview_date) | ||||||
|         .await? | 			.execute(executor) | ||||||
|         .last_insert_rowid()) | 			.await? | ||||||
|  | 			.last_insert_rowid(), | ||||||
|  | 	)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn find_maximal_slug(slugs: &Vec<JustSlugs>) -> Option<u32> { | fn find_maximal_slug(slugs: &Vec<JustSlugs>) -> Option<u32> { | ||||||
|     lazy_static! { | 	lazy_static! { | ||||||
|         static ref RE_CAP_NUM: Regex = Regex::new(r"-(\d+)$").unwrap(); | 		static ref RE_CAP_NUM: Regex = Regex::new(r"-(\d+)$").unwrap(); | ||||||
|     } | 	} | ||||||
| 
 | 
 | ||||||
|     if slugs.len() == 0 { | 	if slugs.len() == 0 { | ||||||
|         return None; | 		return None; | ||||||
|     } | 	} | ||||||
| 
 | 
 | ||||||
|     let mut slug_counters: Vec<u32> = slugs | 	let mut slug_counters: Vec<u32> = slugs | ||||||
|         .iter() | 		.iter() | ||||||
|         .filter_map(|slug| RE_CAP_NUM.captures(&slug.slug)) | 		.filter_map(|slug| RE_CAP_NUM.captures(&slug.slug)) | ||||||
|         .map(|cap| cap.get(1).unwrap().as_str().parse::<u32>().unwrap()) | 		.map(|cap| cap.get(1).unwrap().as_str().parse::<u32>().unwrap()) | ||||||
|         .collect(); | 		.collect(); | ||||||
|     slug_counters.sort_unstable(); | 	slug_counters.sort_unstable(); | ||||||
|     slug_counters.pop() | 	slug_counters.pop() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Given an initial string and an existing collection of slugs,
 | // Given an initial string and an existing collection of slugs,
 | ||||||
|  | @ -323,148 +349,166 @@ fn find_maximal_slug(slugs: &Vec<JustSlugs>) -> Option<u32> { | ||||||
| // collection.
 | // collection.
 | ||||||
| async fn generate_slug<'a, E>(executor: E, title: &str) -> SqlResult<String> | async fn generate_slug<'a, E>(executor: E, title: &str) -> SqlResult<String> | ||||||
| where | where | ||||||
|     E: Executor<'a, Database = Sqlite>, | 	E: Executor<'a, Database = Sqlite>, | ||||||
| { | { | ||||||
|     lazy_static! { | 	lazy_static! { | ||||||
|         static ref RE_STRIP_NUM: Regex = Regex::new(r"-\d+$").unwrap(); | 		static ref RE_STRIP_NUM: Regex = Regex::new(r"-\d+$").unwrap(); | ||||||
|     } | 	} | ||||||
| 
 | 
 | ||||||
|     let initial_slug = slugify(title); | 	let initial_slug = slugify(title); | ||||||
|     let sample_slug = RE_STRIP_NUM.replace_all(&initial_slug, ""); | 	let sample_slug = RE_STRIP_NUM.replace_all(&initial_slug, ""); | ||||||
|     let slug_finder_sql = "SELECT slug FROM pages WHERE slug LIKE '?%';"; | 	let slug_finder_sql = "SELECT slug FROM pages WHERE slug LIKE '?%';"; | ||||||
|     let similar_slugs: Vec<JustSlugs> = sqlx::query_as(&slug_finder_sql) | 	let similar_slugs: Vec<JustSlugs> = sqlx::query_as(&slug_finder_sql) | ||||||
|         .bind(&*sample_slug) | 		.bind(&*sample_slug) | ||||||
|         .fetch_all(executor) | 		.fetch_all(executor) | ||||||
|         .await?; | 		.await?; | ||||||
|     let maximal_slug = find_maximal_slug(&similar_slugs); | 	let maximal_slug = find_maximal_slug(&similar_slugs); | ||||||
|     match maximal_slug { | 	match maximal_slug { | ||||||
|         None => Ok(initial_slug), | 		None => Ok(initial_slug), | ||||||
|         Some(max_slug) => Ok(format!("{}-{}", initial_slug, max_slug + 1)), | 		Some(max_slug) => Ok(format!("{}-{}", initial_slug, max_slug + 1)), | ||||||
|     } | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async fn insert_one_new_page<'a, E>(executor: E, page: &NewPage) -> SqlResult<i64> | async fn insert_one_new_page<'a, E>(executor: E, page: &NewPage) -> SqlResult<NoteId> | ||||||
| where | where | ||||||
|     E: Executor<'a, Database = Sqlite>, | 	E: Executor<'a, Database = Sqlite>, | ||||||
| { | { | ||||||
|     let insert_one_page_sql = concat!( | 	let insert_one_page_sql = concat!( | ||||||
|         "INSERT INTO pages ( ", | 		"INSERT INTO pages ( ", | ||||||
|         "	   slug, ", | 		"	   slug, ", | ||||||
|         "	   title, ", | 		"	   title, ", | ||||||
|         "	   note_id, ", | 		"	   note_id, ", | ||||||
|         "	   creation_date, ", | 		"	   creation_date, ", | ||||||
|         "	   updated_date, ", | 		"	   updated_date, ", | ||||||
|         "	   lastview_date) ", | 		"	   lastview_date) ", | ||||||
|         "VALUES (?, ?, ?, ?, ?, ?);" | 		"VALUES (?, ?, ?, ?, ?, ?);" | ||||||
|     ); | 	); | ||||||
| 
 | 
 | ||||||
|     Ok(sqlx::query(insert_one_page_sql) | 	Ok(NoteId( | ||||||
|         .bind(&page.slug) | 		sqlx::query(insert_one_page_sql) | ||||||
|         .bind(&page.title) | 			.bind(&page.slug) | ||||||
|         .bind(&page.note_id) | 			.bind(&page.title) | ||||||
|         .bind(&page.creation_date) | 			.bind(&page.note_id) | ||||||
|         .bind(&page.updated_date) | 			.bind(&page.creation_date) | ||||||
|         .bind(&page.lastview_date) | 			.bind(&page.updated_date) | ||||||
|         .execute(executor) | 			.bind(&page.lastview_date) | ||||||
|         .await? | 			.execute(executor) | ||||||
|         .last_insert_rowid()) | 			.await? | ||||||
|  | 			.last_insert_rowid(), | ||||||
|  | 	)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async fn bulk_select_ids_for_note_uuids<'a, E>(executor: E, ids: &Vec<&str>) -> SqlResult<Vec<(String, i64)>> | async fn bulk_select_ids_for_note_uuids<'a, E>( | ||||||
|  | 	executor: E, | ||||||
|  | 	ids: &Vec<&str>, | ||||||
|  | ) -> SqlResult<Vec<(String, i64)>> | ||||||
| where | where | ||||||
|     E: Executor<'a, Database = Sqlite>, | 	E: Executor<'a, Database = Sqlite>, | ||||||
| { | { | ||||||
|     let bulk_select_ids_for_note_uuids_sql = 
 | 	let bulk_select_ids_for_note_uuids_sql = "SELECT uuid, id FROM notes WHERE uuid IN (" | ||||||
|         "SELECT uuid, id FROM notes WHERE uuid IN (".to_string() + | 		.to_string() | ||||||
|         &["?",].repeat(ids.len()).join(",") + | 		+ &["?"].repeat(ids.len()).join(",") | ||||||
|         &");".to_string(); | 		+ &");".to_string(); | ||||||
| 
 | 
 | ||||||
|     let mut request = sqlx::query(&bulk_select_ids_for_note_uuids_sql); | 	let mut request = sqlx::query(&bulk_select_ids_for_note_uuids_sql); | ||||||
|     for id in ids.iter() { | 	for id in ids.iter() { | ||||||
|         request = request.bind(id); | 		request = request.bind(id); | ||||||
|     } | 	} | ||||||
|     Ok(request | 	Ok(request | ||||||
|        .try_map(|row: SqliteRow| { | 		.try_map(|row: SqliteRow| { | ||||||
| 		   let l = row.try_get::<String, _>(0)?; | 			let l = row.try_get::<String, _>(0)?; | ||||||
| 		   let r = row.try_get::<i64, _>(1)?; | 			let r = row.try_get::<i64, _>(1)?; | ||||||
| 		   Ok((l, r)) }) | 			Ok((l, r)) | ||||||
|        .fetch_all(executor) | 		}) | ||||||
|        .await? | 		.fetch_all(executor) | ||||||
|        .into_iter() | 		.await? | ||||||
|        .collect()) | 		.into_iter() | ||||||
|  | 		.collect()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async fn get_note_note_relationship<'a, E>(executor: E, parent_id: i64, note_id: i64) -> SqlResult<NoteRelationship> | async fn get_note_note_relationship<'a, E>( | ||||||
|  | 	executor: E, | ||||||
|  | 	parent_id: ParentId, | ||||||
|  | 	note_id: NoteId, | ||||||
|  | ) -> SqlResult<NoteRelationship> | ||||||
| where | where | ||||||
|     E: Executor<'a, Database = Sqlite>, | 	E: Executor<'a, Database = Sqlite>, | ||||||
| { | { | ||||||
|     let get_note_note_relationship_sql = concat!( | 	let get_note_note_relationship_sql = concat!( | ||||||
|         "SELECT parent_id, note_id, position, nature ", | 		"SELECT parent_id, note_id, position, nature ", | ||||||
|         "FROM note_relationships ", | 		"FROM note_relationships ", | ||||||
|         "WHERE parent_id = ? and note_id = ? ", | 		"WHERE parent_id = ? and note_id = ? ", | ||||||
|         "LIMIT 1" | 		"LIMIT 1" | ||||||
|     ); | 	); | ||||||
|     sqlx::query_as(get_note_note_relationship_sql) | 	sqlx::query_as(get_note_note_relationship_sql) | ||||||
|         .bind(&parent_id) | 		.bind(&*parent_id) | ||||||
|         .bind(¬e_id) | 		.bind(&*note_id) | ||||||
|         .fetch_one(executor) | 		.fetch_one(executor) | ||||||
|         .await | 		.await | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async fn delete_note_note_relationship<'a, E>(executor: E, parent_id: i64, note_id: i64) -> SqlResult<()> | async fn delete_note_note_relationship<'a, E>( | ||||||
|  | 	executor: E, | ||||||
|  | 	parent_id: ParentId, | ||||||
|  | 	note_id: NoteId, | ||||||
|  | ) -> SqlResult<()> | ||||||
| where | where | ||||||
|     E: Executor<'a, Database = Sqlite>, | 	E: Executor<'a, Database = Sqlite>, | ||||||
| { | { | ||||||
|     let delete_note_note_relationship_sql = concat!( | 	let delete_note_note_relationship_sql = concat!( | ||||||
|         "DELETE FROM note_relationships ", | 		"DELETE FROM note_relationships ", | ||||||
|         "WHERE parent_id = ? and note_id = ? " | 		"WHERE parent_id = ? and note_id = ? " | ||||||
|     ); | 	); | ||||||
| 
 | 
 | ||||||
|     let count = sqlx::query(delete_note_note_relationship_sql) | 	let count = sqlx::query(delete_note_note_relationship_sql) | ||||||
|         .bind(&parent_id) | 		.bind(&*parent_id) | ||||||
|         .bind(¬e_id) | 		.bind(&*note_id) | ||||||
|         .execute(executor) | 		.execute(executor) | ||||||
|         .await? | 		.await? | ||||||
|         .rows_affected(); | 		.rows_affected(); | ||||||
| 
 | 
 | ||||||
|     match count { | 	match count { | ||||||
|         1 => Ok(()), | 		1 => Ok(()), | ||||||
|         _ => Err(sqlx::Error::RowNotFound), | 		_ => Err(sqlx::Error::RowNotFound), | ||||||
|     } | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async fn close_hole_for_deleted_note<'a, E>(executor: E, parent_id: i64, position: i64) -> SqlResult<()> | async fn close_hole_for_deleted_note<'a, E>( | ||||||
|  | 	executor: E, | ||||||
|  | 	parent_id: ParentId, | ||||||
|  | 	position: i64, | ||||||
|  | ) -> SqlResult<()> | ||||||
| where | where | ||||||
|     E: Executor<'a, Database = Sqlite>, | 	E: Executor<'a, Database = Sqlite>, | ||||||
| { | { | ||||||
|     let close_hole_for_deleted_note_sql = concat!( | 	let close_hole_for_deleted_note_sql = concat!( | ||||||
|         "UPDATE note_relationships ", | 		"UPDATE note_relationships ", | ||||||
|         "SET position = position - 1 ", | 		"SET position = position - 1 ", | ||||||
|         "WHERE position > ? and parent_id = ?;" | 		"WHERE position > ? and parent_id = ?;" | ||||||
|     ); | 	); | ||||||
| 
 | 
 | ||||||
|     sqlx::query(close_hole_for_deleted_note_sql) | 	sqlx::query(close_hole_for_deleted_note_sql) | ||||||
|         .bind(&position) | 		.bind(&position) | ||||||
|         .bind(&parent_id) | 		.bind(&*parent_id) | ||||||
|         .execute(executor) | 		.execute(executor) | ||||||
|         .await | 		.await | ||||||
|         .map(|_| ()) | 		.map(|_| ()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn create_unique_root_note() -> NewNote { | fn create_unique_root_note() -> NewNote { | ||||||
|     NewNoteBuilder::default() | 	NewNoteBuilder::default() | ||||||
|         .uuid(friendly_id::create()) | 		.uuid(friendly_id::create()) | ||||||
|         .content("".to_string()) | 		.content("".to_string()) | ||||||
|         .notetype("root".to_string()) | 		.notetype("root".to_string()) | ||||||
|         .build() | 		.build() | ||||||
|         .unwrap() | 		.unwrap() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn create_new_page_for(title: &str, slug: &str, note_id: i64) -> NewPage { | fn create_new_page_for(title: &str, slug: &str, note_id: NoteId) -> NewPage { | ||||||
|     NewPageBuilder::default() | 	NewPageBuilder::default() | ||||||
|         .slug(slug.to_string()) | 		.slug(slug.to_string()) | ||||||
|         .title(title.to_string()) | 		.title(title.to_string()) | ||||||
|         .note_id(note_id) | 		.note_id(*note_id) | ||||||
|         .build() | 		.build() | ||||||
|         .unwrap() | 		.unwrap() | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue