diff --git a/kordophone-db/src/chat_database.rs b/kordophone-db/src/chat_database.rs index 1ca973b..65e2286 100644 --- a/kordophone-db/src/chat_database.rs +++ b/kordophone-db/src/chat_database.rs @@ -1,7 +1,14 @@ +use std::error::Error; use microrm::prelude::*; use microrm::Stored; -use crate::models::conversation::{self, Conversation, ConversationID}; -use std::error::Error; + +use crate::models::participant::ParticipantID; +use crate::models::{ + participant::Participant, + conversation::{ + self, Conversation, ConversationID, PendingConversation + } +}; pub struct ChatDatabase { db: DB, @@ -10,6 +17,7 @@ pub struct ChatDatabase { #[derive(Database)] struct DB { conversations: microrm::IDMap, + participants: microrm::IDMap, } impl ChatDatabase { @@ -20,20 +28,27 @@ impl ChatDatabase { }) } - pub fn insert_conversation(&self, conversation: Conversation) -> Result { + pub fn insert_conversation(&self, conversation: PendingConversation) -> Result { // First see if conversation guid already exists, update it if so - let guid = &conversation.guid; - let mut existing = self.db.conversations - .with(Conversation::Guid, guid) - .get()?; + let guid = conversation.guid(); + let mut existing = self.stored_conversation_by_guid(guid)?; - if let Some(existing) = existing.first_mut() { - existing.display_name = conversation.display_name; + if let Some(existing) = existing.as_mut() { + conversation.update(existing); existing.sync(); return Ok(existing.id()); } else { // Otherwise, insert. - return self.db.conversations.insert(conversation); + let inserted = self.db.conversations.insert_and_return(conversation.get_conversation())?; + + // Insert participants + let participants = conversation.get_participants(); + let inserted_participants = participants.iter() + .map(|p| self.db.participants.insert(p.clone()).unwrap()) + .collect::>(); + inserted.connect_participants(inserted_participants); + + return Ok(inserted.id()); } } @@ -66,6 +81,26 @@ impl ChatDatabase { ) } + fn upsert_participants(&self, participants: Vec) -> Vec { + // Filter existing participants and add to result + let existing_participants = participants.iter() + .filter_map(|p| self.db.participants + .with(Participant::DisplayName, &p.display_name) + .get() + .ok() + .and_then(|v| v + .into_iter() + .last() + .map(|p| p.id()) + ) + ) + .collect::>(); + + participants.iter() + .map(|p| self.db.participants.insert(p.clone()).unwrap()) + .collect() + } + fn stored_conversation_by_guid(&self, guid: &str) -> Result>, microrm::Error> { self.db.conversations .with(Conversation::Guid, guid) diff --git a/kordophone-db/src/lib.rs b/kordophone-db/src/lib.rs index 1eca944..6eb6f99 100644 --- a/kordophone-db/src/lib.rs +++ b/kordophone-db/src/lib.rs @@ -3,9 +3,13 @@ pub mod chat_database; #[cfg(test)] mod tests { - use microrm::prelude::Queryable; - - use crate::{chat_database::{self, ChatDatabase}, models::conversation::{Conversation, ConversationBuilder, Participant}}; + use crate::{ + chat_database::ChatDatabase, + models::{ + conversation::{Conversation, ConversationBuilder}, + participant::Participant + } + }; #[test] fn test_database_init() { @@ -48,7 +52,8 @@ mod tests { fn test_conversation_participants() { let db = ChatDatabase::new_in_memory().unwrap(); - let participants: Vec = vec!["one".into(), "two".into()]; + let participants: Vec = vec!["one".into(), "two".into()]; + let conversation = ConversationBuilder::new() .display_name("Test") .participant_display_names(participants.clone()) @@ -57,12 +62,20 @@ mod tests { let id = db.insert_conversation(conversation).unwrap(); let read_conversation = db.get_conversation_by_id(id).unwrap().unwrap(); - let read_participants: Vec = read_conversation.participant_display_names - .get() - .unwrap() - .into_iter() - .map(|p| p.wrapped().display_name) - .collect(); + let read_participants: Vec = read_conversation.get_participant_display_names(); + + assert_eq!(participants, read_participants); + + // Try making another conversation with the same participants + let conversation = ConversationBuilder::new() + .display_name("A Different Test") + .participant_display_names(participants.clone()) + .build(); + + let id = db.insert_conversation(conversation).unwrap(); + + let read_conversation = db.get_conversation_by_id(id).unwrap().unwrap(); + let read_participants: Vec = read_conversation.get_participant_display_names(); assert_eq!(participants, read_participants); } diff --git a/kordophone-db/src/models/conversation.rs b/kordophone-db/src/models/conversation.rs index 3807d54..4fd2ff5 100644 --- a/kordophone-db/src/models/conversation.rs +++ b/kordophone-db/src/models/conversation.rs @@ -1,9 +1,14 @@ use microrm::prelude::*; -use chrono::{DateTime, Local, Utc}; +use microrm::Stored; use time::OffsetDateTime; use uuid::Uuid; -use crate::models::date::Date; +use crate::models::{ + date::Date, + participant::Participant, +}; + +use super::participant::ParticipantID; #[derive(Entity, Clone)] pub struct Conversation { @@ -14,19 +19,8 @@ pub struct Conversation { pub display_name: Option, pub last_message_preview: Option, pub date: OffsetDateTime, - pub participant_display_names: microrm::RelationMap, -} - -#[derive(Entity, Clone)] -pub struct Participant { - #[unique] - pub display_name: String -} - -impl Into for String { - fn into(self) -> Participant { - Participant { display_name: self } - } + + participant_display_names: microrm::RelationMap, } impl Conversation { @@ -44,6 +38,60 @@ impl Conversation { display_name: self.display_name.clone(), } } + + pub fn get_participant_display_names(&self) -> Vec { + self.participant_display_names + .get() + .unwrap() + .into_iter() + .map(|p| p.wrapped()) + .collect() + } + + pub fn update(&self, stored_conversation: &mut Stored) { + *stored_conversation.as_mut() = self.clone(); + } + + pub fn connect_participants(&self, participant_ids: Vec) { + participant_ids.iter().for_each(|id| { + self.participant_display_names.connect_to(*id).unwrap(); + }); + } +} + +#[derive(Clone)] +pub struct PendingConversation { + conversation: Conversation, + participants: Vec, +} + +impl PendingConversation { + pub fn guid(&self) -> &String { + &self.conversation.guid + } + + pub fn into_builder(self) -> ConversationBuilder { + ConversationBuilder { + guid: Some(self.conversation.guid), + date: Date::new(self.conversation.date), + participant_display_names: Some(self.participants), + unread_count: Some(self.conversation.unread_count), + last_message_preview: self.conversation.last_message_preview, + display_name: self.conversation.display_name, + } + } + + pub fn update(&self, stored_conversation: &mut microrm::Stored) { + self.conversation.update(stored_conversation); + } + + pub fn get_participants(&self) -> &Vec { + &self.participants + } + + pub fn get_conversation(&self) -> Conversation { + self.conversation.clone() + } } #[derive(Default)] @@ -52,7 +100,7 @@ pub struct ConversationBuilder { date: Date, unread_count: Option, last_message_preview: Option, - participant_display_names: Option>, + participant_display_names: Option>, display_name: Option, } @@ -81,7 +129,7 @@ impl ConversationBuilder { self } - pub fn participant_display_names(mut self, participant_display_names: Vec) -> Self { + pub fn participant_display_names(mut self, participant_display_names: Vec) -> Self { self.participant_display_names = Some(participant_display_names); self } @@ -91,23 +139,21 @@ impl ConversationBuilder { self } - pub fn build(self) -> Conversation { - let result = Conversation { - guid: self.guid.unwrap_or(Uuid::new_v4().to_string()), + fn build_conversation(&self) -> Conversation { + Conversation { + guid: self.guid.clone().unwrap_or(Uuid::new_v4().to_string()), unread_count: self.unread_count.unwrap_or(0), - last_message_preview: self.last_message_preview, - display_name: self.display_name, + last_message_preview: self.last_message_preview.clone(), + display_name: self.display_name.clone(), date: self.date.dt, participant_display_names: Default::default(), - }; - - // TODO: this isn't right... this is crashing the test. - if let Some(participants) = self.participant_display_names { - for participant in participants { - result.participant_display_names.insert(participant.into()).unwrap(); - } } + } - result + pub fn build(self) -> PendingConversation { + PendingConversation { + conversation: self.build_conversation(), + participants: self.participant_display_names.unwrap_or_default(), + } } } diff --git a/kordophone-db/src/models/mod.rs b/kordophone-db/src/models/mod.rs index 3181e39..8323539 100644 --- a/kordophone-db/src/models/mod.rs +++ b/kordophone-db/src/models/mod.rs @@ -1,2 +1,3 @@ pub mod conversation; -pub mod date; \ No newline at end of file +pub mod date; +pub mod participant; \ No newline at end of file diff --git a/kordophone-db/src/models/participant.rs b/kordophone-db/src/models/participant.rs new file mode 100644 index 0000000..e5cabb4 --- /dev/null +++ b/kordophone-db/src/models/participant.rs @@ -0,0 +1,19 @@ +use microrm::prelude::*; + +#[derive(Entity, Clone, PartialEq)] +pub struct Participant { + #[unique] + pub display_name: String +} + +impl Into for String { + fn into(self) -> Participant { + Participant { display_name: self } + } +} + +impl From<&str> for Participant { + fn from(s: &str) -> Self { + Participant { display_name: s.into() } + } +}