use sqlx::SqlitePool;
#[derive(Debug, Clone)]
pub struct Resources {
pool: SqlitePool,
}
impl Resources {
pub fn new(pool: SqlitePool) -> Self {
Self { pool }
}
pub async fn insert(
&self,
uid: &str,
calendar_id: &str,
resource_id: &str,
metadata: Option<&str>,
) -> Result<(), sqlx::Error> {
const SQL: &str = "
INSERT INTO resources (uid, calendar_id, resource_id, metadata)
VALUES (?, ?, ?, ?)
ON CONFLICT(uid, calendar_id) DO UPDATE SET
resource_id = excluded.resource_id,
metadata = excluded.metadata;
";
sqlx::query(SQL)
.bind(uid)
.bind(calendar_id)
.bind(resource_id)
.bind(metadata)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn get(
&self,
uid: &str,
calendar_id: &str,
) -> Result<Option<ResourceRecord>, sqlx::Error> {
const SQL: &str = "
SELECT uid, calendar_id, resource_id, metadata
FROM resources
WHERE uid = ? AND calendar_id = ?;
";
sqlx::query_as(SQL)
.bind(uid)
.bind(calendar_id)
.fetch_optional(&self.pool)
.await
}
#[allow(dead_code)]
pub async fn delete(&self, uid: &str, calendar_id: &str) -> Result<(), sqlx::Error> {
const SQL: &str = "DELETE FROM resources WHERE uid = ? AND calendar_id = ?;";
sqlx::query(SQL)
.bind(uid)
.bind(calendar_id)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn list_uids_by_calendar(
&self,
calendar_id: &str,
) -> Result<Vec<String>, sqlx::Error> {
const SQL: &str = "SELECT uid FROM resources WHERE calendar_id = ?;";
let rows: Vec<(String,)> = sqlx::query_as(SQL)
.bind(calendar_id)
.fetch_all(&self.pool)
.await?;
Ok(rows.into_iter().map(|r| r.0).collect())
}
}
#[derive(Debug, sqlx::FromRow)]
#[allow(dead_code)]
pub struct ResourceRecord {
pub uid: String,
pub calendar_id: String,
pub resource_id: String,
pub metadata: Option<String>,
}
impl ResourceRecord {
#[allow(dead_code)]
pub fn metadata_json<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
let metadata = self.metadata.as_ref()?;
serde_json::from_str(metadata).ok()
}
#[allow(dead_code)]
pub fn calendar_id(&self) -> &str {
&self.calendar_id
}
}
#[cfg(test)]
mod tests {
async fn setup_test_db() -> crate::db::Db {
crate::db::Db::open(None)
.await
.expect("Failed to create test database")
}
#[tokio::test]
async fn resources_insert_inserts_new_resource() {
let db = setup_test_db().await;
let todo = crate::db::tests_utils::test_todo("test-uid", "Test Todo");
let todo_record = crate::db::todos::TodoRecord::from_todo("test-uid", &todo, "default");
let upsert_result = db.todos.upsert(&todo_record).await;
if let Err(e) = upsert_result {
panic!("Failed to upsert todo: {e:?}");
}
let event = crate::db::tests_utils::test_event("test-uid", "Test Event");
let event_record =
crate::db::events::EventRecord::from_event("test-uid", &event, "default");
db.events.upsert(event_record).await.unwrap();
let insert_result = db
.resources
.insert("test-uid", "default", "file:///path/test.ics", None)
.await;
if let Err(e) = insert_result {
panic!("Failed to insert resource: {e:?}");
}
let retrieved = db.resources.get("test-uid", "default").await.unwrap();
assert!(retrieved.is_some());
let resource = retrieved.unwrap();
assert_eq!(resource.uid, "test-uid");
assert_eq!(resource.calendar_id, "default");
assert_eq!(resource.resource_id, "file:///path/test.ics");
assert!(resource.metadata.is_none());
}
#[tokio::test]
async fn resources_get_returns_none_for_nonexistent() {
let db = setup_test_db().await;
let retrieved = db.resources.get("nonexistent", "default").await.unwrap();
assert!(retrieved.is_none());
}
#[tokio::test]
async fn resources_delete_removes_resource() {
let db = setup_test_db().await;
let todo = crate::db::tests_utils::test_todo("test-uid", "Test Todo");
let todo_record = crate::db::todos::TodoRecord::from_todo("test-uid", &todo, "default");
db.todos.upsert(&todo_record).await.unwrap();
let event = crate::db::tests_utils::test_event("test-uid", "Test Event");
let event_record =
crate::db::events::EventRecord::from_event("test-uid", &event, "default");
db.events.upsert(event_record).await.unwrap();
db.resources
.insert("test-uid", "default", "file:///path/test.ics", None)
.await
.unwrap();
db.resources.delete("test-uid", "default").await.unwrap();
let retrieved = db.resources.get("test-uid", "default").await.unwrap();
assert!(retrieved.is_none());
}
#[tokio::test]
async fn resources_insert_updates_existing_resource() {
let db = setup_test_db().await;
let todo = crate::db::tests_utils::test_todo("test-uid", "Test Todo");
let todo_record = crate::db::todos::TodoRecord::from_todo("test-uid", &todo, "default");
db.todos.upsert(&todo_record).await.unwrap();
let event = crate::db::tests_utils::test_event("test-uid", "Test Event");
let event_record =
crate::db::events::EventRecord::from_event("test-uid", &event, "default");
db.events.upsert(event_record).await.unwrap();
db.resources
.insert("test-uid", "default", "file:///path/test.ics", None)
.await
.unwrap();
db.resources
.insert("test-uid", "default", "file:///path/new.ics", Some("{}"))
.await
.unwrap();
let retrieved = db.resources.get("test-uid", "default").await.unwrap();
assert!(retrieved.is_some());
let resource = retrieved.unwrap();
assert_eq!(resource.resource_id, "file:///path/new.ics");
assert_eq!(resource.metadata, Some("{}".to_string()));
}
#[tokio::test]
async fn resources_metadata_json_parses_json() {
#[derive(serde::Deserialize)]
struct TestMetadata {
etag: String,
version: i32,
}
let db = setup_test_db().await;
let todo = crate::db::tests_utils::test_todo("test-uid", "Test Todo");
let todo_record = crate::db::todos::TodoRecord::from_todo("test-uid", &todo, "default");
db.todos.upsert(&todo_record).await.unwrap();
let event = crate::db::tests_utils::test_event("test-uid", "Test Event");
let event_record =
crate::db::events::EventRecord::from_event("test-uid", &event, "default");
db.events.upsert(event_record).await.unwrap();
let json = r#"{"etag":"\"abc123\"","version":1}"#;
db.resources
.insert("test-uid", "default", "/dav/test.ics", Some(json))
.await
.unwrap();
let retrieved = db.resources.get("test-uid", "default").await.unwrap();
assert!(retrieved.is_some());
let resource = retrieved.unwrap();
let metadata: TestMetadata = resource.metadata_json().unwrap();
assert_eq!(metadata.etag, "\"abc123\"");
assert_eq!(metadata.version, 1);
}
}