use super::{StoreError, Submission, SubmissionChanges, SubmissionFileChanges, Undo, Visibility}; use chrono::{DateTime, Utc}; use sled::{Db, Transactional, Tree}; use std::io::Cursor; use uuid::Uuid; #[derive(Clone, Debug)] pub struct Store { submission_tree: Tree, profile_tree: Tree, profile_created_tree: Tree, created_tree: Tree, count_tree: Tree, } #[derive(Debug, serde::Deserialize, serde::Serialize)] struct StoredSubmission<'a> { id: Uuid, profile_id: Uuid, title: &'a str, description: Option<&'a str>, files: Vec, published: Option>, visibility: Visibility, created_at: DateTime, updated_at: DateTime, } impl Store { pub fn build(db: &Db) -> Result { Ok(Store { submission_tree: db.open_tree("profiles/submissions")?, profile_tree: db.open_tree("profiles/submissions/profile")?, profile_created_tree: db.open_tree("/profiles/submissions/profile/created")?, created_tree: db.open_tree("/profiles/submissions/created")?, count_tree: db.open_tree("/profiles/submissions/count")?, }) } pub fn create( &self, profile_id: Uuid, title: &str, visibility: Visibility, ) -> Result { let mut id; let mut stored_submission; let now = Utc::now().into(); let mut stored_submission_vec = vec![]; while { stored_submission_vec.clear(); let writer = Cursor::new(&mut stored_submission_vec); id = Uuid::new_v4(); stored_submission = StoredSubmission { id, profile_id, title, description: None, files: vec![], published: None, visibility, created_at: now, updated_at: now, }; serde_json::to_writer(writer, &stored_submission)?; self.submission_tree .compare_and_swap( id_submission_key(id).as_bytes(), None as Option<&[u8]>, Some(stored_submission_vec.as_slice()), )? .is_err() } {} let res = [ &self.profile_tree, &self.created_tree, &self.profile_created_tree, &self.count_tree, ] .transaction(move |trees| { let profile_tree = &trees[0]; let created_tree = &trees[1]; let profile_created_tree = &trees[2]; let count_tree = &trees[3]; profile_tree.insert( profile_id_key(profile_id, id).as_bytes(), id.to_string().as_bytes(), )?; created_tree.insert( created_submission_key(now, id).as_bytes(), id.to_string().as_bytes(), )?; profile_created_tree.insert( profile_id_created_submission_key(profile_id, now, id).as_bytes(), id.to_string().as_bytes(), )?; super::count( count_tree, &profile_id_submission_count_key(profile_id), |c| c.saturating_add(1), )?; Ok(()) }); if let Err(e) = res { self.submission_tree .remove(id_submission_key(id).as_bytes())?; return Err(e.into()); } Ok(stored_submission.into()) } pub fn count(&self, profile_id: Uuid) -> Result { match self .count_tree .get(profile_id_submission_count_key(profile_id))? { Some(ivec) => Ok(String::from_utf8_lossy(&ivec) .parse::() .expect("Count is valid")), None => Ok(0), } } pub fn update(&self, changes: &SubmissionChanges) -> Result { let stored_submission_ivec = match self.submission_tree.get(id_submission_key(changes.id))? { Some(ivec) => ivec, None => return Err(StoreError::Missing), }; let mut stored_submission: StoredSubmission = serde_json::from_slice(&stored_submission_ivec)?; if let Some(title) = changes.title.as_ref() { stored_submission.title = &title; } if let Some(description) = changes.description.as_ref() { stored_submission.description = Some(&description); } stored_submission.published = changes.published; stored_submission.updated_at = Utc::now(); let stored_submission_vec = serde_json::to_vec(&stored_submission)?; if self .submission_tree .compare_and_swap( id_submission_key(changes.id), Some(&stored_submission_ivec), Some(stored_submission_vec.as_slice()), )? .is_err() { return Err(StoreError::DoubleStore); } Ok(stored_submission.into()) } pub fn update_files(&self, changes: &SubmissionFileChanges) -> Result { let stored_submission_ivec = match self.submission_tree.get(id_submission_key(changes.id))? { Some(ivec) => ivec, None => return Err(StoreError::Missing), }; let mut stored_submission: StoredSubmission = serde_json::from_slice(&stored_submission_ivec)?; stored_submission.files = changes.files.clone(); stored_submission.updated_at = Utc::now(); let stored_submission_vec = serde_json::to_vec(&stored_submission)?; if self .submission_tree .compare_and_swap( id_submission_key(changes.id), Some(&stored_submission_ivec), Some(stored_submission_vec.as_slice()), )? .is_err() { return Err(StoreError::DoubleStore); } Ok(stored_submission.into()) } pub fn for_profile(&self, profile_id: Uuid) -> impl DoubleEndedIterator { self.profile_tree .scan_prefix(profile_id_prefix(profile_id)) .values() .filter_map(|res| res.ok()) .filter_map(|ivec| { let id_str = String::from_utf8_lossy(&ivec); id_str.parse::().ok() }) } fn date_range( &self, range: impl std::ops::RangeBounds, ) -> impl DoubleEndedIterator where K: AsRef<[u8]>, { self.profile_created_tree .range(range) .values() .filter_map(|res| res.ok()) .filter_map(move |ivec| { let id_str = String::from_utf8_lossy(&ivec); id_str.parse::().ok() }) } pub fn newer_than(&self, id: Uuid) -> impl DoubleEndedIterator { let this = self.clone(); self.submission_tree .get(id_submission_key(id)) .ok() .and_then(|opt| opt) .and_then(|stored_submission_ivec| { let stored_submission: StoredSubmission = serde_json::from_slice(&stored_submission_ivec).ok()?; Some((stored_submission.profile_id, stored_submission.created_at)) }) .into_iter() .flat_map(move |(profile_id, created_at)| { let range_start = profile_id_created_submission_range_start(profile_id, created_at); let range_start = range_start.as_bytes().to_vec(); this.date_range(range_start..) }) } pub fn older_than(&self, id: Uuid) -> impl DoubleEndedIterator { let this = self.clone(); self.submission_tree .get(id_submission_key(id)) .ok() .and_then(|opt| opt) .and_then(|stored_submission_ivec| { let stored_submission: StoredSubmission = serde_json::from_slice(&stored_submission_ivec).ok()?; Some((stored_submission.profile_id, stored_submission.created_at)) }) .into_iter() .flat_map(move |(profile_id, created_at)| { let range_end = profile_id_created_submission_range_start(profile_id, created_at); let range_end = range_end.as_bytes().to_vec(); this.date_range(..range_end) }) } pub fn by_id(&self, id: Uuid) -> Result, StoreError> { let stored_submission_ivec = match self.submission_tree.get(id_submission_key(id))? { Some(ivec) => ivec, None => return Ok(None), }; let stored_submission: StoredSubmission = serde_json::from_slice(&stored_submission_ivec)?; Ok(Some(stored_submission.into())) } pub fn delete(&self, submission_id: Uuid) -> Result>, StoreError> { let stored_submission_ivec = match self.submission_tree.get(id_submission_key(submission_id))? { Some(ivec) => ivec, None => return Ok(None), }; let stored_submission: StoredSubmission = serde_json::from_slice(&stored_submission_ivec)?; let id = submission_id; let profile_id = stored_submission.profile_id; let created_at = stored_submission.created_at; [ &self.submission_tree, &self.profile_tree, &self.profile_created_tree, &self.created_tree, &self.count_tree, ] .transaction(move |trees| { let submission_tree = &trees[0]; let profile_tree = &trees[1]; let profile_created_tree = &trees[2]; let created_tree = &trees[3]; let count_tree = &trees[4]; submission_tree.remove(id_submission_key(id).as_bytes())?; profile_tree.remove(profile_id_key(profile_id, id).as_bytes())?; profile_created_tree .remove(profile_id_created_submission_key(profile_id, created_at, id).as_bytes())?; created_tree.remove(created_submission_key(created_at, id).as_bytes())?; super::count( count_tree, &profile_id_submission_count_key(profile_id), |c| c.saturating_sub(1), )?; Ok(()) })?; Ok(Some(Undo(stored_submission.into()))) } } // Used to map id -> Submission fn id_submission_key(id: Uuid) -> String { format!("/submission/{}/data", id) } // Used to map profile_id -> id fn profile_id_key(profile_id: Uuid, id: Uuid) -> String { format!("/profile/{}/submission/{}", profile_id, id) } fn profile_id_prefix(profile_id: Uuid) -> String { format!("/profile/{}/submission", profile_id) } // Used to fetch submissions for a given profile in a user-recognizalbe order fn profile_id_created_submission_key( profile_id: Uuid, created_at: DateTime, id: Uuid, ) -> String { format!( "/profile/{}/created/{}/submission/{}", profile_id, created_at.to_rfc3339(), id ) } fn profile_id_created_submission_range_start( profile_id: Uuid, created_at: DateTime, ) -> String { format!( "/profile/{}/created/{}", profile_id, created_at.to_rfc3339() ) } // Used to map created_at -> id fn created_submission_key(created_at: DateTime, id: Uuid) -> String { format!("/created/{}/submission/{}", created_at.to_rfc3339(), id) } fn profile_id_submission_count_key(profile_id: Uuid) -> String { format!("/profile/{}/count", profile_id) } impl<'a> From> for Submission { fn from(ss: StoredSubmission<'a>) -> Self { Submission { id: ss.id, profile_id: ss.profile_id, title: ss.title.to_owned(), description: ss.description.map(|d| d.to_owned()), files: ss.files.clone(), published: ss.published, visibility: ss.visibility, } } }