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
This commit is contained in:
Ruben Rosario
2026-06-21 16:03:40 +01:00
parent e45631b235
commit b3dcefcd65
5 changed files with 71 additions and 5 deletions
+2
View File
@@ -537,6 +537,8 @@ impl App {
status: TaskStatus::NeedsAction, status: TaskStatus::NeedsAction,
due: None, due: None,
position: 0, position: 0,
created_at: None,
updated_at: None,
}; };
self.db.insert_task(&task).ok(); self.db.insert_task(&task).ok();
self.db.push_sync( self.db.push_sync(
+2
View File
@@ -16,6 +16,8 @@ pub struct Task {
pub status: TaskStatus, pub status: TaskStatus,
pub due: Option<NaiveDateTime>, pub due: Option<NaiveDateTime>,
pub position: i64, pub position: i64,
pub created_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+28
View File
@@ -181,6 +181,18 @@ impl ApiClient {
.ok() .ok()
}); });
let updated = item["updated"].as_str().and_then(|s| {
chrono::NaiveDateTime::parse_from_str(
&s.replace("T", " ")
.replace("Z", "")
.chars()
.take(19)
.collect::<String>(),
"%Y-%m-%d %H:%M:%S",
)
.ok()
});
Task { Task {
id: item["id"].as_str().unwrap_or("").to_string(), id: item["id"].as_str().unwrap_or("").to_string(),
list_id: list_id.to_string(), list_id: list_id.to_string(),
@@ -193,6 +205,8 @@ impl ApiClient {
}, },
due: due_str, due: due_str,
position: i as i64, position: i as i64,
created_at: None,
updated_at: updated,
} }
}) })
.collect(); .collect();
@@ -261,6 +275,18 @@ impl ApiClient {
.ok() .ok()
}); });
let updated = item["updated"].as_str().and_then(|s| {
chrono::NaiveDateTime::parse_from_str(
&s.replace("T", " ")
.replace("Z", "")
.chars()
.take(19)
.collect::<String>(),
"%Y-%m-%d %H:%M:%S",
)
.ok()
});
Task { Task {
id: item["id"].as_str().unwrap_or("").to_string(), id: item["id"].as_str().unwrap_or("").to_string(),
list_id: list_id.to_string(), list_id: list_id.to_string(),
@@ -273,6 +299,8 @@ impl ApiClient {
}, },
due: due_str, due: due_str,
position: i as i64, position: i as i64,
created_at: None,
updated_at: updated,
} }
}) })
.collect(); .collect();
+35 -5
View File
@@ -28,6 +28,7 @@ impl Db {
due TEXT, due TEXT,
position INTEGER NOT NULL DEFAULT 0, position INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL, 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 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;", "ALTER TABLE sync_queue ADD COLUMN retries INTEGER NOT NULL DEFAULT 0;",
) )
.ok(); .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) }) Ok(Self { conn: Mutex::new(conn) })
} }
@@ -84,13 +89,15 @@ impl Db {
let conn = self.conn.lock().unwrap(); let conn = self.conn.lock().unwrap();
let mut stmt = conn let mut stmt = conn
.prepare( .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", FROM tasks WHERE list_id = ?1 ORDER BY position ASC",
) )
.unwrap(); .unwrap();
stmt.query_map(params![list_id], |row| { stmt.query_map(params![list_id], |row| {
let due_str: Option<String> = row.get(5)?; let due_str: Option<String> = row.get(5)?;
let due = due_str.and_then(|s| NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M").ok()); 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 { Ok(Task {
id: row.get(0)?, id: row.get(0)?,
list_id: row.get(1)?, list_id: row.get(1)?,
@@ -102,6 +109,8 @@ impl Db {
}, },
due, due,
position: row.get(6)?, 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() .unwrap()
@@ -126,9 +135,26 @@ impl Db {
} else { } else {
task.position 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( conn.execute(
"INSERT OR REPLACE INTO tasks (id, list_id, title, notes, status, due, position, updated_at) "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)", VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
params![ params![
task.id, task.id,
task.list_id, task.list_id,
@@ -137,7 +163,8 @@ impl Db {
status_str, status_str,
due_str, due_str,
position, 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(()) Ok(())
@@ -149,6 +176,9 @@ impl Db {
TaskStatus::Completed => "completed", TaskStatus::Completed => "completed",
TaskStatus::NeedsAction => "needsAction", TaskStatus::NeedsAction => "needsAction",
}; };
let updated_at = task
.updated_at
.unwrap_or_else(|| chrono::Utc::now().naive_utc());
let conn = self.conn.lock().unwrap(); let conn = self.conn.lock().unwrap();
conn.execute( conn.execute(
"UPDATE tasks SET title=?1, notes=?2, status=?3, due=?4, position=?5, updated_at=?6 "UPDATE tasks SET title=?1, notes=?2, status=?3, due=?4, position=?5, updated_at=?6
@@ -159,7 +189,7 @@ impl Db {
status_str, status_str,
due_str, due_str,
task.position, 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, task.id,
], ],
)?; )?;
+4
View File
@@ -255,6 +255,8 @@ async fn push_sync(
status: TaskStatus::NeedsAction, status: TaskStatus::NeedsAction,
due: None, due: None,
position: 0, position: 0,
created_at: None,
updated_at: None,
}); });
api.create_task(&item.list_id, &task).await api.create_task(&item.list_id, &task).await
} }
@@ -267,6 +269,8 @@ async fn push_sync(
status: TaskStatus::NeedsAction, status: TaskStatus::NeedsAction,
due: None, due: None,
position: 0, position: 0,
created_at: None,
updated_at: None,
}); });
api.update_task(&item.list_id, &task).await api.update_task(&item.list_id, &task).await
} }