diff --git a/src/main.rs b/src/main.rs index 2d0f368..a380a35 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use crate::client::TogglClient; +use crate::entity::time_entry::{self, ActiveModel, Entity as TimeEntry}; use crate::types::{Current, ReportEntry, TogglQuery}; use anyhow::anyhow; use axum::http::StatusCode; @@ -9,19 +10,19 @@ use base64::engine::general_purpose::STANDARD; use base64::Engine; use beachhead::{shutdown_signal, Result}; use clap::Parser; +use migration::{Migrator, MigratorTrait}; +use sea_orm::sea_query::OnConflict; +use sea_orm::{DatabaseConnection, EntityTrait}; use serde_json::Value; use std::collections::HashMap; use std::net::SocketAddr; -use sea_orm::{DatabaseConnection, EntityTrait}; -use sea_orm::sea_query::OnConflict; use tower_http::trace::TraceLayer; -use migration::{Migrator, MigratorTrait}; -use crate::entity::time_entry::{self, Entity as TimeEntry}; mod client; -mod types; mod db; mod entity; +mod poll; +mod types; #[derive(Debug, Clone, Parser)] struct Config { @@ -44,7 +45,16 @@ pub async fn report( Json(query): Json, ) -> Result>> { let report = toggl_client.full_report(&query).await?; - let models = report.iter().flat_map(|entry| entry.as_models()); + cache_report(&db, &report).await?; + + Ok(Json(report)) +} + +async fn cache_report( + db: &DatabaseConnection, + models: &Vec, +) -> Result<()> { + let models = models.iter().flat_map(|entry| entry.as_models()); TimeEntry::insert_many(models) .on_conflict( @@ -55,10 +65,12 @@ pub async fn report( time_entry::Column::Start, time_entry::Column::Stop, time_entry::Column::RawJson, - ]).to_owned() - ).exec(&db).await?; - - Ok(Json(report)) + ]) + .to_owned(), + ) + .exec(db) + .await?; + Ok(()) } pub async fn current( @@ -99,9 +111,9 @@ async fn main() -> Result<()> { .await .unwrap(); - Migrator::up(&db, None) - .await - .expect("Failed to migrate"); + Migrator::up(&db, None).await.expect("Failed to migrate"); + + tokio::spawn(poll::poll_job(toggl_client.clone(), db.clone())); // build our application with a route let app = Router::new() diff --git a/src/poll.rs b/src/poll.rs new file mode 100644 index 0000000..951c922 --- /dev/null +++ b/src/poll.rs @@ -0,0 +1,52 @@ +use crate::client::TogglClient; +use crate::types::TogglQuery; +use sea_orm::DatabaseConnection; +use tracing::instrument; + +#[tracing::instrument(skip(client, db))] +pub async fn poll_job(client: TogglClient, db: DatabaseConnection) { + // Every 2h, poll the Toggl API for new time entries for today to cache them in the database + let period = tokio::time::Duration::from_secs(60 * 60 * 2); + + loop { + tracing::info!("Polling Toggl API"); + match perform_poll(&client, &db).await { + Ok(report_entries_count) => { + tracing::info!( + "Successfully polled Toggl API: {:?} entries retrieved", + report_entries_count + ); + } + + Err(error) => { + tracing::error!("Failed to poll Toggl API: {:?}", error); + } + } + + tokio::time::sleep(period).await; + } +} + +#[instrument(skip(client, db))] +async fn perform_poll(client: &TogglClient, db: &DatabaseConnection) -> beachhead::Result { + let report = client + .full_report(&TogglQuery { + start_date: Some( + chrono::Utc::now() + .date_naive() + .format("%Y-%m-%d") + .to_string(), + ), + end_date: Some( + chrono::Utc::now() + .date_naive() + .format("%Y-%m-%d") + .to_string(), + ), + ..Default::default() + }) + .await?; + + crate::cache_report(&db, &report).await?; + Ok(report.len()) +} diff --git a/src/types.rs b/src/types.rs index 2d6c5ff..bda0da2 100644 --- a/src/types.rs +++ b/src/types.rs @@ -58,7 +58,7 @@ pub struct Project { #[allow(non_snake_case)] #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct TogglQuery { pub billable: Option, pub client_ids: Option>,