use chrono::{DateTime, NaiveDate, Utc}; use sqlx::sqlite::SqlitePool; use std::str::FromStr; use super::Birthday; use crate::AppError; use crate::db::DbId; use crate::switchboard::MentionHostType; struct RawContact { id: DbId, birthday: Option, manually_freshened_at: Option, lives_with: String, } #[derive(Clone, Debug)] pub struct Contact { pub id: DbId, pub birthday: Option, pub manually_freshened_at: Option>, pub lives_with: String, } impl Into for RawContact { fn into(self) -> Contact { Contact { id: self.id, birthday: self .birthday .and_then(|s| Birthday::from_str(s.as_ref()).ok()), manually_freshened_at: self .manually_freshened_at .and_then(|str| DateTime::parse_from_str(str.as_ref(), "%+").ok()) .map(|d| d.to_utc()), lives_with: self.lives_with, } } } struct RawHydratedContact { id: DbId, birthday: Option, manually_freshened_at: Option, lives_with: String, last_mention_date: Option, names: Option, } #[derive(Clone, Debug)] pub struct HydratedContact { pub contact: Contact, pub last_mention_date: Option, pub names: Vec, } impl Into for RawHydratedContact { fn into(self) -> HydratedContact { HydratedContact { contact: Into::::into(RawContact { id: self.id, birthday: self.birthday, manually_freshened_at: self.manually_freshened_at, lives_with: self.lives_with, }), names: self .names .unwrap_or(String::new()) .split('\x1c') .map(|s| s.to_string()) .collect::>(), last_mention_date: self .last_mention_date .and_then(|str| NaiveDate::from_str(str.as_ref()).ok()), } } } impl std::ops::Deref for HydratedContact { type Target = Contact; fn deref(&self) -> &Self::Target { &self.contact } } impl HydratedContact { pub fn display_name(&self) -> String { if let Some(name) = self.names.first() { name.clone() } else { "(unnamed)".to_string() } } pub async fn load(id: DbId, pool: &SqlitePool) -> Result { // copy-paste the query from 'all', then add "where c.id = $2" to the last line let raw = sqlx::query_as!( RawHydratedContact, r#"select id, birthday, lives_with, manually_freshened_at as "manually_freshened_at: String", ( select string_agg(name,'\x1c' order by sort) from names where contact_id = c.id ) as names, ( select jes.date from journal_entries jes join mentions ms on ms.entity_id = jes.id where ms.entity_type = $1 and ms.url = '/contact/'||c.id or ms.url in ( select '/group/'||slug from groups where contact_id = c.id ) order by jes.date desc limit 1 ) as last_mention_date from contacts c where c.id = $2"#, MentionHostType::JournalEntry as DbId, id ) .fetch_one(pool) .await?; Ok(Into::::into(raw)) } pub async fn all(pool: &SqlitePool) -> Result, AppError> { let contacts = sqlx::query_as!( RawHydratedContact, r#"select id, birthday, lives_with, manually_freshened_at as "manually_freshened_at: String", ( select string_agg(name,'\x1c' order by sort) from names where contact_id = c.id ) as names, ( select jes.date from journal_entries jes join mentions ms on ms.entity_id = jes.id where ms.entity_type = $1 and ms.url = '/contact/'||c.id or ms.url in ( select '/group/'||slug from groups where contact_id = c.id ) order by jes.date desc limit 1 ) as last_mention_date from contacts c"#, MentionHostType::JournalEntry as DbId ) .fetch_all(pool) .await?; Ok(contacts .into_iter() .map(|raw| Into::::into(raw)) .collect()) } }