diff --git a/Cargo.lock b/Cargo.lock index 0e38698..1adc925 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1486,6 +1486,7 @@ dependencies = [ "atoi", "byteorder", "bytes", + "chrono", "crc", "crossbeam-queue", "either", @@ -1547,6 +1548,7 @@ dependencies = [ "sha2", "sqlx-core", "sqlx-mysql", + "sqlx-postgres", "sqlx-sqlite", "syn 1.0.109", "tempfile", @@ -1565,6 +1567,7 @@ dependencies = [ "bitflags 2.6.0", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -1606,6 +1609,7 @@ dependencies = [ "base64", "bitflags 2.6.0", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -1641,6 +1645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index 5b7aba9..d32b257 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ chrono = "0.4.38" clap = { version = "4.5.9", features = ["derive"] } dotenvy = "0.15.0" regex = "1.5.4" -sqlx = { version = "0.7.4", features = ["runtime-tokio-native-tls", "sqlite"] } +sqlx = { version = "0.7.4", features = ["runtime-tokio-native-tls", "sqlite", "chrono"] } tokio = { version = "1.28.0", features = ["full", "test-util"] } [dev-dependencies] diff --git a/src/db/metadata_repo.rs b/src/db/metadata_repo.rs index bbe1ee6..af99ef7 100644 --- a/src/db/metadata_repo.rs +++ b/src/db/metadata_repo.rs @@ -9,6 +9,7 @@ use sqlx::SqlitePool; #[async_trait] pub trait MetadataRepo { async fn create(&self, metadata: models::Metadata) -> anyhow::Result; + async fn get_by_id(&self, contact_id: i64) -> anyhow::Result; } pub struct Connection { @@ -25,8 +26,8 @@ impl Connection { #[async_trait] impl MetadataRepo for Connection { - async fn create(&self, metadata: models::Metadata) -> anyhow::Result { - let query = "INSERT INTO contact_metadata + async fn create(&self, metadata: models::Metadata) -> anyhow::Result { + let query = "INSERT INTO contact_metadata (contact_id, starred, is_archived, @@ -38,39 +39,66 @@ impl MetadataRepo for Connection { last_reminder_at) VALUES (?,?,?,?,?,?,?,?,?)"; - let result = sqlx::query(query) - .bind(metadata.contact_id) - .bind(metadata.starred) - .bind(metadata.is_archived) - .bind(metadata.frequency) - - .bind(metadata.created_at.to_rfc3339_opts(SecondsFormat::Millis, true)) - .bind(metadata.updated_at.to_rfc3339_opts(SecondsFormat::Millis, true)) + let result = sqlx::query(query) + .bind(metadata.contact_id) + .bind(metadata.starred) + .bind(metadata.is_archived) + .bind(metadata.frequency) + .bind( + metadata + .created_at + .to_rfc3339_opts(SecondsFormat::Millis, true), + ) + .bind( + metadata + .updated_at + .to_rfc3339_opts(SecondsFormat::Millis, true), + ) + .bind( + metadata + .last_seen_at + .map(|dt| dt.to_rfc3339_opts(SecondsFormat::Millis, true)), + ) + .bind( + metadata + .next_reminder_at + .map(|dt| dt.to_rfc3339_opts(SecondsFormat::Millis, true)), + ) + .bind( + metadata + .last_reminder_at + .map(|dt| dt.to_rfc3339_opts(SecondsFormat::Millis, true)), + ) + .execute(&*self.sqlite_pool) + .await?; + + Ok(result.last_insert_rowid()) + } + async fn get_by_id(&self, contact_id: i64) -> anyhow::Result { + let query_get_by_id = "SELECT * FROM metadata WHERE contact_id=$1"; - .bind(metadata.last_seen_at.map(|dt| dt.to_rfc3339_opts(SecondsFormat::Millis, true))) - .bind(metadata.next_reminder_at.map(|dt| dt.to_rfc3339_opts(SecondsFormat::Millis, true))) - .bind(metadata.last_reminder_at.map(|dt| dt.to_rfc3339_opts(SecondsFormat::Millis, true))) - .execute(&*self.sqlite_pool) - .await?; + let metadata: models::Metadata = sqlx::query_as::<_, models::Metadata>(query_get_by_id) + .bind(contact_id) + .fetch_one(&*self.sqlite_pool) + .await?; - Ok(result.last_insert_rowid()) - } + Ok(metadata) + } } #[cfg(test)] mod tests { use super::*; - use mockall::predicate::*; use crate::models; + use mockall::predicate::*; use sqlx::{sqlite::SqlitePoolOptions, SqlitePool}; - async fn setup_test_db() -> SqlitePool { let pool = SqlitePoolOptions::new() .connect("sqlite::memory:") .await .expect("Failed to create in-memory SQLite database"); - + sqlx::query( "CREATE TABLE IF NOT EXISTS contact_metadata ( contact_id INTEGER NOT NULL, @@ -82,25 +110,24 @@ mod tests { next_reminder_at TEXT, frequency INTEGER, last_reminder_at TEXT - )" + )", ) .execute(&pool) .await .expect("Failed to create contact_metadata table"); - + pool } #[tokio::test] async fn test_create_metadata_sqlite() { - let pool = setup_test_db().await; - let repo = Connection::new(pool); + let pool = setup_test_db().await; + let repo = Connection::new(pool); - let test_metadata = models::Metadata::default(); + let test_metadata = models::Metadata::default(); - let result = repo.create(test_metadata.clone()).await.unwrap(); - assert!(result > 0); -} - + let result = repo.create(test_metadata.clone()).await.unwrap(); + assert!(result > 0); + } #[tokio::test] async fn test_create_metadata() { @@ -108,7 +135,6 @@ mod tests { let test_metadata = models::Metadata::default(); - mock_metadata_repo .expect_create() .times(1) @@ -121,6 +147,31 @@ mod tests { assert_eq!(result, 1); } -} + #[tokio::test] + async fn test_get_metadata() { + let mut mock_metadata_repo = MockMetadataRepo::new(); + + let test_metadata = models::Metadata { + contact_id: 1, + ..models::Metadata::default() + }; + + // Clone test_metadata before using it in the closure + let test_metadata_clone = test_metadata.clone(); + + mock_metadata_repo + .expect_get_by_id() + .times(1) + .with(eq(1)) + .returning(move |_| Ok(test_metadata_clone.clone())); + + let result = mock_metadata_repo.get_by_id(1).await; + + assert!(result.is_ok()); + let expected_metadata = result.unwrap(); + + assert_eq!(expected_metadata, test_metadata); + } +} diff --git a/src/models/metadata.rs b/src/models/metadata.rs index b249daa..fe4c27c 100644 --- a/src/models/metadata.rs +++ b/src/models/metadata.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Utc}; -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, sqlx::FromRow)] pub struct Metadata { pub contact_id: i64, pub starred: bool,