diff --git a/Cargo.lock b/Cargo.lock index 1747467..360010e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1116,6 +1116,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1238,6 +1248,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.11.2" @@ -1844,6 +1860,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -2234,6 +2259,16 @@ dependencies = [ "syn 2.0.71", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.36" @@ -2288,6 +2323,7 @@ dependencies = [ "base64 0.22.1", "chrono", "dotenv", + "futures", "governor", "reqwest", "reqwest-middleware", @@ -2300,6 +2336,8 @@ dependencies = [ "sqlx", "thiserror", "tokio", + "tracing", + "tracing-subscriber", "url", ] @@ -2436,6 +2474,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -2512,6 +2576,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 7ace8a2..d40bc79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,6 @@ url = "2.5.2" serde_with = "3.9.0" sqlx = { version = "0.7.4", features = ["postgres", "runtime-tokio", "macros", "chrono"] } dotenv = "0.15.0" +futures = "0.3.30" +tracing = "0.1.40" +tracing-subscriber = "0.3.18" diff --git a/src/main.rs b/src/main.rs index 7781d3e..0a49f38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use chrono::{NaiveDate, TimeDelta, Utc}; +use chrono::{TimeDelta, Utc}; use sqlx::{Connection, PgConnection}; use toggl::TogglApi; @@ -65,21 +65,75 @@ impl Worker { let time_entries = self.toggl_api .get_time_entries_for_user_modified_since(fetch_since).await?; - let refetch_projects = time_entries.iter() + let fetch_workspaces = time_entries.iter() + .map(|entry| entry.workspace_id) + .filter(|workspace_id| !existing_ids.workspace_ids.contains(&workspace_id)) + .collect::>(); + + let fetch_projects = time_entries.iter() .map(|entry| entry.project_id) .filter_map(|project_id| project_id) .any(|project_id| !existing_ids.project_ids.contains(&project_id)); - let refetch_tags = time_entries.iter() + let fetch_tags = time_entries.iter() .flat_map(|entry| entry.tag_ids.iter()) .any(|tag| !existing_ids.tag_ids.contains(&tag)); + if !fetch_workspaces.is_empty() { + self.update_workspaces(&fetch_workspaces).await?; + } + + if fetch_projects { + self.update_projects(&existing_ids).await?; + } + + if fetch_tags { + self.update_tags().await?; + } + Ok(()) } - async fn update_projects(&mut self) -> Result<(), AppError> { + async fn update_workspaces(&mut self, workspace_ids: &[u64]) -> Result<(), AppError> { + let workspaces = workspace_ids.iter() + .map(|id| self.toggl_api.get_workspace(*id)); + + let workspaces = futures::future::join_all(workspaces).await + .into_iter() + .collect::, _>>()?; + + for workspace in workspaces { + sqlx::query!( + r#" + INSERT INTO workspaces (id, organization_id, name) + VALUES ($1, $2, $3) + ON CONFLICT (id) DO UPDATE SET + organization_id = excluded.organization_id, + name = excluded.name + "#, + workspace.id as i64, + workspace.organization_id as i64, + workspace.name, + ) + .execute(&mut self.db) + .await?; + } + + Ok(()) + } + + async fn update_projects(&mut self, existing_ids: &TableSummary) -> Result<(), AppError> { let projects = self.toggl_api.get_projects().await?; + let fetch_clients = projects.iter() + .map(|project| project.client_id) + .filter_map(|client_id| client_id) + .any(|client_id| !existing_ids.client_ids.contains(&(client_id as u64))); + + if fetch_clients { + self.update_clients().await?; + } + for project in projects { sqlx::query!( r#" @@ -123,6 +177,74 @@ impl Worker { Ok(()) } + + async fn update_tags(&mut self) -> Result<(), AppError> { + let tags = self.toggl_api.get_tags().await?; + + for tag in tags { + sqlx::query!( + r#" + INSERT INTO tags (id, name, workspace_id, creator_id, updated_at, deleted_at, permissions) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (id) DO UPDATE SET + name = excluded.name, + workspace_id = excluded.workspace_id, + creator_id = excluded.creator_id, + updated_at = excluded.updated_at, + deleted_at = excluded.deleted_at, + permissions = excluded.permissions + "#, + tag.id as i64, + tag.name, + tag.workspace_id as i64, + tag.creator_id as i64, + tag.updated_at, + tag.deleted_at, + tag.permissions, + ) + .execute(&mut self.db) + .await?; + } + + Ok(()) + } + + async fn update_clients(&mut self) -> Result<(), AppError> { + let clients = self.toggl_api.get_clients().await?; + + for client in clients { + sqlx::query!( + r#" + INSERT INTO tracking_clients (id, updated_at, archived, creator_id, integration_provider, notes, name, server_deleted_at, workspace_id, permissions) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ON CONFLICT (id) DO UPDATE SET + updated_at = excluded.updated_at, + archived = excluded.archived, + creator_id = excluded.creator_id, + integration_provider = excluded.integration_provider, + notes = excluded.notes, + name = excluded.name, + server_deleted_at = excluded.server_deleted_at, + workspace_id = excluded.workspace_id, + permissions = excluded.permissions + "#, + client.id, + client.updated_at, + client.archived, + client.creator_id, + client.integration_provider, + client.notes, + client.name, + client.server_deleted_at, + client.workspace_id, + client.permissions, + ) + .execute(&mut self.db) + .await?; + } + + Ok(()) + } } #[tokio::main] @@ -143,7 +265,7 @@ async fn main() { toggl_api: api, }; - worker.update_projects() + worker.update(TimeDelta::days(7)) .await .unwrap(); } diff --git a/src/toggl/mod.rs b/src/toggl/mod.rs index 7983483..44f5f9d 100644 --- a/src/toggl/mod.rs +++ b/src/toggl/mod.rs @@ -362,21 +362,21 @@ pub mod types { /// The Workspace ID associated with the client. #[serde(rename = "wid")] - pub workspace_id: i32, + pub workspace_id: i64, - permissions: Option, + pub permissions: Option, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Tag { - id: u64, - name: String, - workspace_id: u64, - creator_id: u64, + pub id: u64, + pub name: String, + pub workspace_id: u64, + pub creator_id: u64, #[serde(rename = "at")] - updated_at: DateTime, - deleted_at: Option>, - permissions: Option, + pub updated_at: DateTime, + pub deleted_at: Option>, + pub permissions: Option, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -482,7 +482,6 @@ pub mod types { } use std::fmt; - use std::path::Display; impl fmt::Debug for TogglReportFilters { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {