From b3dcefcd65355d4953866f21876029d521a6e016 Mon Sep 17 00:00:00 2001 From: Ruben Rosario Date: Sun, 21 Jun 2026 16:03:40 +0100 Subject: [PATCH] Add created_at/updated_at to Task model, DB, and API - Add created_at and updated_at fields to Task struct - Preserve existing created_at on upsert in insert_task - Parse updated field from Google Tasks API response - Add created_at column to DB schema with migration --- src/app.rs | 2 ++ src/domain/models.rs | 2 ++ src/infrastructure/api.rs | 28 +++++++++++++++++++++++++++ src/infrastructure/db.rs | 40 ++++++++++++++++++++++++++++++++++----- src/main.rs | 4 ++++ 5 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/app.rs b/src/app.rs index ff68612..9522dad 100644 --- a/src/app.rs +++ b/src/app.rs @@ -537,6 +537,8 @@ impl App { status: TaskStatus::NeedsAction, due: None, position: 0, + created_at: None, + updated_at: None, }; self.db.insert_task(&task).ok(); self.db.push_sync( diff --git a/src/domain/models.rs b/src/domain/models.rs index e205cf6..752f9bd 100644 --- a/src/domain/models.rs +++ b/src/domain/models.rs @@ -16,6 +16,8 @@ pub struct Task { pub status: TaskStatus, pub due: Option, pub position: i64, + pub created_at: Option, + pub updated_at: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/src/infrastructure/api.rs b/src/infrastructure/api.rs index 3467841..4b7e505 100644 --- a/src/infrastructure/api.rs +++ b/src/infrastructure/api.rs @@ -181,6 +181,18 @@ impl ApiClient { .ok() }); + let updated = item["updated"].as_str().and_then(|s| { + chrono::NaiveDateTime::parse_from_str( + &s.replace("T", " ") + .replace("Z", "") + .chars() + .take(19) + .collect::(), + "%Y-%m-%d %H:%M:%S", + ) + .ok() + }); + Task { id: item["id"].as_str().unwrap_or("").to_string(), list_id: list_id.to_string(), @@ -193,6 +205,8 @@ impl ApiClient { }, due: due_str, position: i as i64, + created_at: None, + updated_at: updated, } }) .collect(); @@ -261,6 +275,18 @@ impl ApiClient { .ok() }); + let updated = item["updated"].as_str().and_then(|s| { + chrono::NaiveDateTime::parse_from_str( + &s.replace("T", " ") + .replace("Z", "") + .chars() + .take(19) + .collect::(), + "%Y-%m-%d %H:%M:%S", + ) + .ok() + }); + Task { id: item["id"].as_str().unwrap_or("").to_string(), list_id: list_id.to_string(), @@ -273,6 +299,8 @@ impl ApiClient { }, due: due_str, position: i as i64, + created_at: None, + updated_at: updated, } }) .collect(); diff --git a/src/infrastructure/db.rs b/src/infrastructure/db.rs index 3ea5396..745283d 100644 --- a/src/infrastructure/db.rs +++ b/src/infrastructure/db.rs @@ -28,6 +28,7 @@ impl Db { due TEXT, position INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')), FOREIGN KEY (list_id) REFERENCES task_lists(id) ON DELETE CASCADE ); @@ -45,6 +46,10 @@ impl Db { "ALTER TABLE sync_queue ADD COLUMN retries INTEGER NOT NULL DEFAULT 0;", ) .ok(); + conn.execute_batch( + "ALTER TABLE tasks ADD COLUMN created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now'));", + ) + .ok(); Ok(Self { conn: Mutex::new(conn) }) } @@ -84,13 +89,15 @@ impl Db { let conn = self.conn.lock().unwrap(); let mut stmt = conn .prepare( - "SELECT id, list_id, title, notes, status, due, position + "SELECT id, list_id, title, notes, status, due, position, created_at, updated_at FROM tasks WHERE list_id = ?1 ORDER BY position ASC", ) .unwrap(); stmt.query_map(params![list_id], |row| { let due_str: Option = row.get(5)?; let due = due_str.and_then(|s| NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M").ok()); + let created_str: String = row.get(7)?; + let updated_str: String = row.get(8)?; Ok(Task { id: row.get(0)?, list_id: row.get(1)?, @@ -102,6 +109,8 @@ impl Db { }, due, position: row.get(6)?, + created_at: NaiveDateTime::parse_from_str(&created_str, "%Y-%m-%d %H:%M:%S").ok(), + updated_at: NaiveDateTime::parse_from_str(&updated_str, "%Y-%m-%d %H:%M:%S").ok(), }) }) .unwrap() @@ -126,9 +135,26 @@ impl Db { } else { task.position }; + + // Preserve existing created_at if the task already exists + let created_at = task.created_at.unwrap_or_else(|| { + conn.query_row( + "SELECT created_at FROM tasks WHERE id = ?1", + params![task.id], + |row| row.get::<_, String>(0), + ) + .ok() + .and_then(|s| NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S").ok()) + .unwrap_or_else(|| chrono::Utc::now().naive_utc()) + }); + + let updated_at = task + .updated_at + .unwrap_or_else(|| chrono::Utc::now().naive_utc()); + conn.execute( - "INSERT OR REPLACE INTO tasks (id, list_id, title, notes, status, due, position, updated_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + "INSERT OR REPLACE INTO tasks (id, list_id, title, notes, status, due, position, updated_at, created_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", params![ task.id, task.list_id, @@ -137,7 +163,8 @@ impl Db { status_str, due_str, position, - chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(), + updated_at.format("%Y-%m-%d %H:%M:%S").to_string(), + created_at.format("%Y-%m-%d %H:%M:%S").to_string(), ], )?; Ok(()) @@ -149,6 +176,9 @@ impl Db { TaskStatus::Completed => "completed", TaskStatus::NeedsAction => "needsAction", }; + let updated_at = task + .updated_at + .unwrap_or_else(|| chrono::Utc::now().naive_utc()); let conn = self.conn.lock().unwrap(); conn.execute( "UPDATE tasks SET title=?1, notes=?2, status=?3, due=?4, position=?5, updated_at=?6 @@ -159,7 +189,7 @@ impl Db { status_str, due_str, task.position, - chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(), + updated_at.format("%Y-%m-%d %H:%M:%S").to_string(), task.id, ], )?; diff --git a/src/main.rs b/src/main.rs index 384e576..b35893c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -255,6 +255,8 @@ async fn push_sync( status: TaskStatus::NeedsAction, due: None, position: 0, + created_at: None, + updated_at: None, }); api.create_task(&item.list_id, &task).await } @@ -267,6 +269,8 @@ async fn push_sync( status: TaskStatus::NeedsAction, due: None, position: 0, + created_at: None, + updated_at: None, }); api.update_task(&item.list_id, &task).await }