diff --git a/migration/src/m20231106_201029_add_time_entry_project_fk.rs b/migration/src/m20231106_201029_add_time_entry_project_fk.rs index cb90ab2..8616287 100644 --- a/migration/src/m20231106_201029_add_time_entry_project_fk.rs +++ b/migration/src/m20231106_201029_add_time_entry_project_fk.rs @@ -29,13 +29,11 @@ impl MigrationTrait for Migration { #[derive(DeriveIden)] enum TimeEntry { Table, - Id, ProjectId, } #[derive(DeriveIden)] enum Project { Table, - Id, TogglId, } diff --git a/src/client.rs b/src/client.rs index eba7231..527f4aa 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,7 +2,6 @@ use reqwest::Client; use serde_json::Value; use std::collections::HashMap; use std::time::Duration; -use axum::http::StatusCode; use hyper::HeaderMap; use tracing::instrument; use tracing::log::debug; diff --git a/src/db.rs b/src/db.rs index f2a27ff..8eb16ba 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,18 +1,34 @@ -use sea_orm::{NotSet, Set}; -use crate::entity::{time_entry, client, project}; +use crate::entity::{client, project, time_entry}; use crate::types::{Project, ProjectClient, ReportEntry}; +use sea_orm::sea_query::OnConflict; +use sea_orm::{NotSet, Set}; impl ReportEntry { pub(crate) fn as_models(&self) -> Vec { - self.time_entries.iter().map(|inner| time_entry::ActiveModel { - id: NotSet, - toggl_id: Set(inner.id as i64), - description: Set(self.description.clone()), - project_id: Set(self.project_id.map(|id| id as i64)), - start: Set(chrono::DateTime::parse_from_rfc3339(&inner.start).unwrap()), - stop: Set(chrono::DateTime::parse_from_rfc3339(&inner.start).unwrap()), - raw_json: Set(serde_json::to_value(inner).unwrap()), - }).collect() + self.time_entries + .iter() + .map(|inner| time_entry::ActiveModel { + id: NotSet, + toggl_id: Set(inner.id as i64), + description: Set(self.description.clone()), + project_id: Set(self.project_id.map(|id| id as i64)), + start: Set(chrono::DateTime::parse_from_rfc3339(&inner.start).unwrap()), + stop: Set(chrono::DateTime::parse_from_rfc3339(&inner.start).unwrap()), + raw_json: Set(serde_json::to_value(inner).unwrap()), + }) + .collect() + } + + pub fn grafting_conflict_statement() -> OnConflict { + OnConflict::column(time_entry::Column::TogglId) + .update_columns(vec![ + time_entry::Column::Description, + time_entry::Column::ProjectId, + time_entry::Column::Start, + time_entry::Column::Stop, + time_entry::Column::RawJson, + ]) + .to_owned() } } @@ -27,6 +43,18 @@ impl ProjectClient { server_deleted_at: Set(self.server_deleted_at.clone().map(|dt| dt.fixed_offset())), } } + + pub fn grafting_conflict_statement() -> OnConflict { + OnConflict::column(client::Column::Id) + .update_columns(vec![ + client::Column::Name, + client::Column::Archived, + client::Column::WorkspaceId, + client::Column::At, + client::Column::ServerDeletedAt, + ]) + .to_owned() + } } impl Project { @@ -41,4 +69,16 @@ impl Project { raw_json: Set(serde_json::to_value(self).unwrap()), } } + + pub fn grafting_conflict_statement() -> OnConflict { + OnConflict::column(project::Column::TogglId) + .update_columns(vec![ + project::Column::Name, + project::Column::Active, + project::Column::ClientId, + project::Column::WorkspaceId, + project::Column::RawJson, + ]) + .to_owned() + } } diff --git a/src/main.rs b/src/main.rs index ff168b0..ae8a364 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use crate::client::TogglClient; -use crate::entity::time_entry::{self, Entity as TimeEntry}; +use crate::entity::prelude::TimeEntry; use crate::types::{Current, Project, ProjectClient, ReportEntry, TogglQuery}; use anyhow::anyhow; use axum::http::StatusCode; @@ -11,7 +11,6 @@ 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; @@ -54,17 +53,7 @@ async fn cache_report(db: &DatabaseConnection, models: &Vec) -> Res let models = models.iter().flat_map(|entry| entry.as_models()); TimeEntry::insert_many(models) - .on_conflict( - OnConflict::column(time_entry::Column::TogglId) - .update_columns(vec![ - time_entry::Column::Description, - time_entry::Column::ProjectId, - time_entry::Column::Start, - time_entry::Column::Stop, - time_entry::Column::RawJson, - ]) - .to_owned(), - ) + .on_conflict(ReportEntry::grafting_conflict_statement()) .exec(db) .await?; Ok(()) @@ -87,45 +76,27 @@ pub async fn start_time_entry( async fn projects( Extension(db): Extension, - Extension(toggl_client): Extension + Extension(toggl_client): Extension, ) -> Result>> { let projects = toggl_client.fetch_projects().await?; -entity::project::Entity::insert_many(projects.iter().map(Project::as_model)) - .on_conflict( - OnConflict::column(entity::project::Column::TogglId) - .update_columns(vec![ - entity::project::Column::Name, - entity::project::Column::Active, - entity::project::Column::ClientId, - entity::project::Column::WorkspaceId, - entity::project::Column::RawJson, - ]) - .to_owned(), - ) - .exec(&db).await?; + entity::project::Entity::insert_many(projects.iter().map(Project::as_model)) + .on_conflict(Project::grafting_conflict_statement()) + .exec(&db) + .await?; Ok(Json(projects)) } async fn clients( Extension(db): Extension, - Extension(toggl_client): Extension + Extension(toggl_client): Extension, ) -> Result>> { let clients = toggl_client.fetch_clients().await?; entity::client::Entity::insert_many(clients.iter().map(ProjectClient::as_model)) - .on_conflict( - OnConflict::column(entity::client::Column::Id) - .update_columns(vec![ - entity::client::Column::Name, - entity::client::Column::Archived, - entity::client::Column::At, - entity::client::Column::ServerDeletedAt, - entity::client::Column::WorkspaceId, - ]) - .to_owned(), - ) - .exec(&db).await?; + .on_conflict(ProjectClient::grafting_conflict_statement()) + .exec(&db) + .await?; Ok(Json(clients)) } diff --git a/src/poll.rs b/src/poll.rs index cc11c44..931fce4 100644 --- a/src/poll.rs +++ b/src/poll.rs @@ -1,7 +1,8 @@ use crate::client::TogglClient; -use crate::types::TogglQuery; -use sea_orm::DatabaseConnection; +use crate::types::{Project, ProjectClient, TogglQuery}; +use sea_orm::{DatabaseConnection, EntityTrait, QuerySelect}; use tracing::instrument; +use crate::entity::{project, client}; #[tracing::instrument(skip(client, db))] pub async fn poll_job(client: TogglClient, db: DatabaseConnection) { @@ -50,6 +51,33 @@ pub async fn perform_poll( }) .await?; + let existing_project_ids = project::Entity::find() + .select_only() + .column(project::Column::TogglId) + .into_tuple::() + .all(db) + .await?; + + let new_projects = report.iter() + .filter_map(|entry| entry.project_id) + .any(|project_id| !existing_project_ids.contains(&(project_id as i64))); + + if new_projects { + let clients = client.fetch_clients().await?; + + client::Entity::insert_many(clients.iter().map(ProjectClient::as_model)) + .on_conflict(ProjectClient::grafting_conflict_statement()) + .exec(db) + .await?; + + let projects = client.fetch_projects().await?; + + project::Entity::insert_many(projects.iter().map(Project::as_model)) + .on_conflict(Project::grafting_conflict_statement()) + .exec(db) + .await?; + } + crate::cache_report(&db, &report).await?; Ok(report.len()) }