Run rustfmt and move cache_report into sync_service.rs
This commit is contained in:
parent
73b3e2cb96
commit
889859dbae
@ -19,11 +19,23 @@ impl MigrationTrait for Migration {
|
|||||||
.primary_key(),
|
.primary_key(),
|
||||||
)
|
)
|
||||||
.col(
|
.col(
|
||||||
ColumnDef::new(TimeEntry::TogglId).big_unsigned().not_null().unique_key())
|
ColumnDef::new(TimeEntry::TogglId)
|
||||||
|
.big_unsigned()
|
||||||
|
.not_null()
|
||||||
|
.unique_key(),
|
||||||
|
)
|
||||||
.col(ColumnDef::new(TimeEntry::Description).string().not_null())
|
.col(ColumnDef::new(TimeEntry::Description).string().not_null())
|
||||||
.col(ColumnDef::new(TimeEntry::ProjectId).big_unsigned())
|
.col(ColumnDef::new(TimeEntry::ProjectId).big_unsigned())
|
||||||
.col(ColumnDef::new(TimeEntry::Start).timestamp_with_time_zone().not_null())
|
.col(
|
||||||
.col(ColumnDef::new(TimeEntry::Stop).timestamp_with_time_zone().not_null())
|
ColumnDef::new(TimeEntry::Start)
|
||||||
|
.timestamp_with_time_zone()
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(TimeEntry::Stop)
|
||||||
|
.timestamp_with_time_zone()
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
.col(ColumnDef::new(TimeEntry::RawJson).json_binary().not_null())
|
.col(ColumnDef::new(TimeEntry::RawJson).json_binary().not_null())
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
@ -46,5 +58,5 @@ enum TimeEntry {
|
|||||||
ProjectId,
|
ProjectId,
|
||||||
Start,
|
Start,
|
||||||
Stop,
|
Stop,
|
||||||
RawJson
|
RawJson,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,11 @@ impl MigrationTrait for Migration {
|
|||||||
.col(ColumnDef::new(Client::Name).string().not_null())
|
.col(ColumnDef::new(Client::Name).string().not_null())
|
||||||
.col(ColumnDef::new(Client::Archived).boolean().not_null())
|
.col(ColumnDef::new(Client::Archived).boolean().not_null())
|
||||||
.col(ColumnDef::new(Client::WorkspaceId).integer().not_null())
|
.col(ColumnDef::new(Client::WorkspaceId).integer().not_null())
|
||||||
.col(ColumnDef::new(Client::At).timestamp_with_time_zone().not_null())
|
.col(
|
||||||
|
ColumnDef::new(Client::At)
|
||||||
|
.timestamp_with_time_zone()
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
.col(ColumnDef::new(Client::ServerDeletedAt).timestamp_with_time_zone())
|
.col(ColumnDef::new(Client::ServerDeletedAt).timestamp_with_time_zone())
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
@ -42,5 +46,5 @@ enum Client {
|
|||||||
Archived,
|
Archived,
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
At,
|
At,
|
||||||
ServerDeletedAt
|
ServerDeletedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,19 @@ impl MigrationTrait for Migration {
|
|||||||
Table::create()
|
Table::create()
|
||||||
.table(Project::Table)
|
.table(Project::Table)
|
||||||
.if_not_exists()
|
.if_not_exists()
|
||||||
.col(ColumnDef::new(Project::Id).integer().primary_key().auto_increment().not_null())
|
.col(
|
||||||
.col(ColumnDef::new(Project::TogglId).big_unsigned().not_null().unique_key())
|
ColumnDef::new(Project::Id)
|
||||||
|
.integer()
|
||||||
|
.primary_key()
|
||||||
|
.auto_increment()
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Project::TogglId)
|
||||||
|
.big_unsigned()
|
||||||
|
.not_null()
|
||||||
|
.unique_key(),
|
||||||
|
)
|
||||||
.col(
|
.col(
|
||||||
ColumnDef::new(Project::WorkspaceId)
|
ColumnDef::new(Project::WorkspaceId)
|
||||||
.big_unsigned()
|
.big_unsigned()
|
||||||
@ -27,13 +38,15 @@ impl MigrationTrait for Migration {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Create foreign key
|
// Create foreign key
|
||||||
manager.create_foreign_key(
|
manager
|
||||||
ForeignKey::create()
|
.create_foreign_key(
|
||||||
.name("project_client_id")
|
ForeignKey::create()
|
||||||
.from(Project::Table, Project::ClientId)
|
.name("project_client_id")
|
||||||
.to(Client::Table, Client::Id)
|
.from(Project::Table, Project::ClientId)
|
||||||
.to_owned(),
|
.to(Client::Table, Client::Id)
|
||||||
).await?;
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,46 +6,55 @@ pub struct Migration;
|
|||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MigrationTrait for Migration {
|
impl MigrationTrait for Migration {
|
||||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
manager.alter_table(
|
manager
|
||||||
TableAlterStatement::new()
|
.alter_table(
|
||||||
.table(Project::Table)
|
TableAlterStatement::new()
|
||||||
.add_column(ColumnDef::new(Project::Color).text())
|
.table(Project::Table)
|
||||||
.add_column(ColumnDef::new(Project::ServerCreatedAt).timestamp_with_time_zone())
|
.add_column(ColumnDef::new(Project::Color).text())
|
||||||
.add_column(ColumnDef::new(Project::ServerUpdatedAt).timestamp_with_time_zone())
|
.add_column(ColumnDef::new(Project::ServerCreatedAt).timestamp_with_time_zone())
|
||||||
.add_column(ColumnDef::new(Project::ServerDeletedAt).timestamp_with_time_zone())
|
.add_column(ColumnDef::new(Project::ServerUpdatedAt).timestamp_with_time_zone())
|
||||||
.to_owned()
|
.add_column(ColumnDef::new(Project::ServerDeletedAt).timestamp_with_time_zone())
|
||||||
).await?;
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
manager.get_connection().execute_unprepared(
|
manager
|
||||||
r#"
|
.get_connection()
|
||||||
|
.execute_unprepared(
|
||||||
|
r#"
|
||||||
update "project"
|
update "project"
|
||||||
set "color" = raw_json ->> 'color',
|
set "color" = raw_json ->> 'color',
|
||||||
"server_created_at" = (raw_json ->> 'created_at') :: timestamptz,
|
"server_created_at" = (raw_json ->> 'created_at') :: timestamptz,
|
||||||
"server_updated_at" = (raw_json ->> 'at') :: timestamptz,
|
"server_updated_at" = (raw_json ->> 'at') :: timestamptz,
|
||||||
"server_deleted_at" = (raw_json ->> 'server_deleted_at') :: timestamptz
|
"server_deleted_at" = (raw_json ->> 'server_deleted_at') :: timestamptz
|
||||||
"#,
|
"#,
|
||||||
).await?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
manager.alter_table(
|
manager
|
||||||
TableAlterStatement::new()
|
.alter_table(
|
||||||
.table(Project::Table)
|
TableAlterStatement::new()
|
||||||
.modify_column(ColumnDef::new(Project::Color).not_null())
|
.table(Project::Table)
|
||||||
.modify_column(ColumnDef::new(Project::ServerCreatedAt).not_null())
|
.modify_column(ColumnDef::new(Project::Color).not_null())
|
||||||
.modify_column(ColumnDef::new(Project::ServerUpdatedAt).not_null())
|
.modify_column(ColumnDef::new(Project::ServerCreatedAt).not_null())
|
||||||
.to_owned()
|
.modify_column(ColumnDef::new(Project::ServerUpdatedAt).not_null())
|
||||||
).await
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
manager.alter_table(
|
manager
|
||||||
TableAlterStatement::new()
|
.alter_table(
|
||||||
.table(Project::Table)
|
TableAlterStatement::new()
|
||||||
.drop_column(Project::Color)
|
.table(Project::Table)
|
||||||
.drop_column(Project::ServerCreatedAt)
|
.drop_column(Project::Color)
|
||||||
.drop_column(Project::ServerUpdatedAt)
|
.drop_column(Project::ServerCreatedAt)
|
||||||
.drop_column(Project::ServerDeletedAt)
|
.drop_column(Project::ServerUpdatedAt)
|
||||||
.to_owned()
|
.drop_column(Project::ServerDeletedAt)
|
||||||
).await
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,44 +6,58 @@ pub struct Migration;
|
|||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MigrationTrait for Migration {
|
impl MigrationTrait for Migration {
|
||||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
manager.alter_table(TableAlterStatement::new()
|
manager
|
||||||
.table(TimeEntry::Table)
|
.alter_table(
|
||||||
.add_column(ColumnDef::new(TimeEntry::Tags)
|
TableAlterStatement::new()
|
||||||
.json_binary()
|
.table(TimeEntry::Table)
|
||||||
.default(serde_json::json!([]))
|
.add_column(
|
||||||
.not_null()
|
ColumnDef::new(TimeEntry::Tags)
|
||||||
|
.json_binary()
|
||||||
|
.default(serde_json::json!([]))
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.add_column(
|
||||||
|
ColumnDef::new(TimeEntry::ServerUpdatedAt).timestamp_with_time_zone(),
|
||||||
|
)
|
||||||
|
.add_column(
|
||||||
|
ColumnDef::new(TimeEntry::ServerDeletedAt).timestamp_with_time_zone(),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.add_column(ColumnDef::new(TimeEntry::ServerUpdatedAt)
|
.await?;
|
||||||
.timestamp_with_time_zone())
|
|
||||||
.add_column(ColumnDef::new(TimeEntry::ServerDeletedAt)
|
|
||||||
.timestamp_with_time_zone())
|
|
||||||
.to_owned()
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
manager.get_connection().execute_unprepared(
|
manager
|
||||||
r#"
|
.get_connection()
|
||||||
|
.execute_unprepared(
|
||||||
|
r#"
|
||||||
update "time_entry"
|
update "time_entry"
|
||||||
set "tags" = coalesce(raw_json -> 'tags', '[]' :: jsonb),
|
set "tags" = coalesce(raw_json -> 'tags', '[]' :: jsonb),
|
||||||
"server_updated_at" = (raw_json ->> 'at') :: timestamptz;
|
"server_updated_at" = (raw_json ->> 'at') :: timestamptz;
|
||||||
"#,
|
"#,
|
||||||
).await?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
manager.alter_table(
|
manager
|
||||||
TableAlterStatement::new()
|
.alter_table(
|
||||||
.table(TimeEntry::Table)
|
TableAlterStatement::new()
|
||||||
.modify_column(ColumnDef::new(TimeEntry::ServerUpdatedAt).not_null())
|
.table(TimeEntry::Table)
|
||||||
.to_owned()
|
.modify_column(ColumnDef::new(TimeEntry::ServerUpdatedAt).not_null())
|
||||||
).await
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
manager.alter_table(TableAlterStatement::new()
|
manager
|
||||||
.table(TimeEntry::Table)
|
.alter_table(
|
||||||
.drop_column(TimeEntry::Tags)
|
TableAlterStatement::new()
|
||||||
.drop_column(TimeEntry::ServerDeletedAt)
|
.table(TimeEntry::Table)
|
||||||
.drop_column(TimeEntry::ServerUpdatedAt)
|
.drop_column(TimeEntry::Tags)
|
||||||
.to_owned()
|
.drop_column(TimeEntry::ServerDeletedAt)
|
||||||
).await
|
.drop_column(TimeEntry::ServerUpdatedAt)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
use crate::utils::Result;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use chrono::{NaiveDate, NaiveTime};
|
use chrono::{NaiveDate, NaiveTime};
|
||||||
use csv::StringRecord;
|
use csv::StringRecord;
|
||||||
use crate::utils::Result;
|
|
||||||
|
|
||||||
mod headings {
|
mod headings {
|
||||||
pub const USER: usize = 1;
|
pub const USER: usize = 1;
|
||||||
@ -20,10 +20,18 @@ mod headings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_csv_row(row: StringRecord) -> Result<crate::entity::time_entry::Model> {
|
fn parse_csv_row(row: StringRecord) -> Result<crate::entity::time_entry::Model> {
|
||||||
let start_date = row.get(headings::START_DATE).ok_or(anyhow!("Missing start date in CSV"))?;
|
let start_date = row
|
||||||
let start_time = row.get(headings::START_TIME).ok_or(anyhow!("Missing start time in CSV"))?;
|
.get(headings::START_DATE)
|
||||||
let end_date = row.get(headings::END_DATE).ok_or(anyhow!("Missing end date in CSV"))?;
|
.ok_or(anyhow!("Missing start date in CSV"))?;
|
||||||
let end_time = row.get(headings::END_TIME).ok_or(anyhow!("Missing end time in CSV"))?;
|
let start_time = row
|
||||||
|
.get(headings::START_TIME)
|
||||||
|
.ok_or(anyhow!("Missing start time in CSV"))?;
|
||||||
|
let end_date = row
|
||||||
|
.get(headings::END_DATE)
|
||||||
|
.ok_or(anyhow!("Missing end date in CSV"))?;
|
||||||
|
let end_time = row
|
||||||
|
.get(headings::END_TIME)
|
||||||
|
.ok_or(anyhow!("Missing end time in CSV"))?;
|
||||||
|
|
||||||
let start_time = NaiveTime::parse_from_str(start_time, "%H:%M:%S")?;
|
let start_time = NaiveTime::parse_from_str(start_time, "%H:%M:%S")?;
|
||||||
let end_time = NaiveTime::parse_from_str(end_time, "%H:%M:%S")?;
|
let end_time = NaiveTime::parse_from_str(end_time, "%H:%M:%S")?;
|
||||||
@ -33,15 +41,28 @@ fn parse_csv_row(row: StringRecord) -> Result<crate::entity::time_entry::Model>
|
|||||||
let start = start_date.and_time(start_time);
|
let start = start_date.and_time(start_time);
|
||||||
let end = end_date.and_time(end_time);
|
let end = end_date.and_time(end_time);
|
||||||
|
|
||||||
let description = row.get(headings::DESCRIPTION).ok_or(anyhow!("Missing description in CSV"))?;
|
let description = row
|
||||||
let project_name = row.get(headings::PROJECT_NAME).ok_or(anyhow!("Missing project name in CSV"))?;
|
.get(headings::DESCRIPTION)
|
||||||
let client_name = row.get(headings::CLIENT_NAME).ok_or(anyhow!("Missing client name in CSV"))?;
|
.ok_or(anyhow!("Missing description in CSV"))?;
|
||||||
let tags = row.get(headings::TAGS).ok_or(anyhow!("Missing tags in CSV"))?;
|
let project_name = row
|
||||||
let task_name = row.get(headings::TASK_NAME).ok_or(anyhow!("Missing task name in CSV"))?;
|
.get(headings::PROJECT_NAME)
|
||||||
let billable = match row.get(headings::BILLABLE).ok_or(anyhow!("Missing billable in CSV"))? {
|
.ok_or(anyhow!("Missing project name in CSV"))?;
|
||||||
|
let client_name = row
|
||||||
|
.get(headings::CLIENT_NAME)
|
||||||
|
.ok_or(anyhow!("Missing client name in CSV"))?;
|
||||||
|
let tags = row
|
||||||
|
.get(headings::TAGS)
|
||||||
|
.ok_or(anyhow!("Missing tags in CSV"))?;
|
||||||
|
let task_name = row
|
||||||
|
.get(headings::TASK_NAME)
|
||||||
|
.ok_or(anyhow!("Missing task name in CSV"))?;
|
||||||
|
let billable = match row
|
||||||
|
.get(headings::BILLABLE)
|
||||||
|
.ok_or(anyhow!("Missing billable in CSV"))?
|
||||||
|
{
|
||||||
"Yes" => true,
|
"Yes" => true,
|
||||||
"No" => false,
|
"No" => false,
|
||||||
_ => unimplemented!("Unknown billable value")
|
_ => unimplemented!("Unknown billable value"),
|
||||||
};
|
};
|
||||||
|
|
||||||
unimplemented!("Refactor model to support non-json sources")
|
unimplemented!("Refactor model to support non-json sources")
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use crate::entity::{client, project, time_entry};
|
use crate::entity::{client, project, time_entry};
|
||||||
use crate::toggl_api::types::{Project, Client, ReportRow, TimeEntry};
|
use crate::toggl_api::types::{Client, Project, ReportRow, TimeEntry};
|
||||||
use sea_orm::sea_query::OnConflict;
|
use sea_orm::sea_query::OnConflict;
|
||||||
use sea_orm::{NotSet, Set};
|
use sea_orm::{NotSet, Set};
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ impl ReportRow {
|
|||||||
server_deleted_at: NotSet,
|
server_deleted_at: NotSet,
|
||||||
server_updated_at: Set(inner.at.fixed_offset()),
|
server_updated_at: Set(inner.at.fixed_offset()),
|
||||||
// TODO: tags on report row import, need to track in separate table
|
// TODO: tags on report row import, need to track in separate table
|
||||||
tags: NotSet
|
tags: NotSet,
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/main.rs
19
src/main.rs
@ -1,19 +1,20 @@
|
|||||||
use crate::toggl_api::TogglApiClient;
|
use crate::toggl_api::TogglApiClient;
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
use axum::{Extension, Router};
|
use axum::{Extension, Router};
|
||||||
use utils::{Result, shutdown_signal};
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use migration::{Migrator, MigratorTrait};
|
use migration::{Migrator, MigratorTrait};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
|
use utils::{shutdown_signal, Result};
|
||||||
|
|
||||||
|
mod csv_parser;
|
||||||
mod db;
|
mod db;
|
||||||
mod entity;
|
mod entity;
|
||||||
mod poll;
|
mod poll;
|
||||||
mod utils;
|
|
||||||
mod csv_parser;
|
|
||||||
mod toggl_api;
|
|
||||||
mod routes;
|
mod routes;
|
||||||
|
mod sync_service;
|
||||||
|
mod toggl_api;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Parser)]
|
#[derive(Debug, Clone, Parser)]
|
||||||
struct Config {
|
struct Config {
|
||||||
@ -40,18 +41,14 @@ async fn main() -> Result<()> {
|
|||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let config = Config::parse();
|
let config = Config::parse();
|
||||||
let toggl_client = TogglApiClient::new(
|
let toggl_client =
|
||||||
&config.workspace_id.to_string(),
|
TogglApiClient::new(&config.workspace_id.to_string(), &config.toggl_api_token);
|
||||||
&config.toggl_api_token,
|
|
||||||
);
|
|
||||||
|
|
||||||
let db = sea_orm::Database::connect(config.database_url)
|
let db = sea_orm::Database::connect(config.database_url)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Migrator::up(&db, None)
|
Migrator::up(&db, None).await.expect("Failed to migrate");
|
||||||
.await
|
|
||||||
.expect("Failed to migrate");
|
|
||||||
|
|
||||||
tokio::spawn(poll::poll_job(
|
tokio::spawn(poll::poll_job(
|
||||||
toggl_client.clone(),
|
toggl_client.clone(),
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
use tracing::{debug, instrument};
|
use crate::entity::time_entry;
|
||||||
use axum::{Extension, Json};
|
use crate::entity::time_entry::Entity as TimeEntry;
|
||||||
use std::collections::HashMap;
|
use crate::toggl_api::types::{self, Client, Project, ReportRow, TogglReportQuery};
|
||||||
use serde_json::Value;
|
use crate::toggl_api::TogglApiClient;
|
||||||
use axum::response::IntoResponse;
|
use crate::{entity, sync_service, utils};
|
||||||
use axum::http::StatusCode;
|
|
||||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use axum::extract::{Multipart, Query};
|
use axum::extract::{Multipart, Query};
|
||||||
use migration::{Condition};
|
use axum::http::StatusCode;
|
||||||
use chrono::{NaiveDate};
|
use axum::response::IntoResponse;
|
||||||
|
use axum::{Extension, Json};
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use migration::Condition;
|
||||||
|
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use crate::entity::time_entry::{Entity as TimeEntry};
|
use serde_json::Value;
|
||||||
use crate::toggl_api::TogglApiClient;
|
use std::collections::HashMap;
|
||||||
use crate::toggl_api::types::{self, Project, Client, ReportRow, TogglReportQuery};
|
use tracing::{debug, instrument};
|
||||||
use crate::{entity, utils};
|
|
||||||
use crate::entity::time_entry;
|
|
||||||
|
|
||||||
#[instrument(skip(db, toggl_client))]
|
#[instrument(skip(db, toggl_client))]
|
||||||
pub async fn report(
|
pub async fn report(
|
||||||
@ -27,49 +27,11 @@ pub async fn report(
|
|||||||
|
|
||||||
// We don't perform any deletes on report-fetched entries as they aren't necessarily exclusive
|
// We don't perform any deletes on report-fetched entries as they aren't necessarily exclusive
|
||||||
// on their time range.
|
// on their time range.
|
||||||
cache_report(&db, &report, None).await?;
|
sync_service::cache_report(&db, &report, None).await?;
|
||||||
|
|
||||||
Ok(Json(report))
|
Ok(Json(report))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
|
||||||
pub async fn cache_report(
|
|
||||||
db: &DatabaseConnection,
|
|
||||||
models: &Vec<ReportRow>,
|
|
||||||
exclusive_on: Option<Condition>,
|
|
||||||
) -> utils::Result<()> {
|
|
||||||
let models = models.iter().flat_map(|entry| entry.as_models());
|
|
||||||
let models = models.collect::<Vec<_>>();
|
|
||||||
let ids = models
|
|
||||||
.iter()
|
|
||||||
.map(|entry| entry.toggl_id.clone().unwrap())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
debug!("Caching report entries: {:?}", models);
|
|
||||||
|
|
||||||
// TODO: Why is this needed?
|
|
||||||
if models.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeEntry::insert_many(models)
|
|
||||||
.on_conflict(ReportRow::grafting_conflict_statement())
|
|
||||||
.exec(db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if let Some(exclusive_on) = exclusive_on {
|
|
||||||
TimeEntry::delete_many()
|
|
||||||
.filter(
|
|
||||||
Condition::all()
|
|
||||||
.add(exclusive_on)
|
|
||||||
.add(time_entry::Column::TogglId.is_in(ids).not()),
|
|
||||||
)
|
|
||||||
.exec(db)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(toggl_client))]
|
#[instrument(skip(toggl_client))]
|
||||||
pub async fn current(
|
pub async fn current(
|
||||||
Extension(toggl_client): Extension<TogglApiClient>,
|
Extension(toggl_client): Extension<TogglApiClient>,
|
||||||
@ -116,7 +78,9 @@ pub async fn clients(
|
|||||||
Ok(Json(clients))
|
Ok(Json(clients))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn health(Extension(toggl_client): Extension<TogglApiClient>) -> utils::Result<&'static str> {
|
pub async fn health(
|
||||||
|
Extension(toggl_client): Extension<TogglApiClient>,
|
||||||
|
) -> utils::Result<&'static str> {
|
||||||
return if toggl_client.check_health().await {
|
return if toggl_client.check_health().await {
|
||||||
Ok("Ok")
|
Ok("Ok")
|
||||||
} else {
|
} else {
|
||||||
@ -169,7 +133,7 @@ pub async fn refresh(
|
|||||||
|
|
||||||
let report = toggl_client.full_report(&query).await?;
|
let report = toggl_client.full_report(&query).await?;
|
||||||
let exclusivity_condition = utils::day_exclusivity_condition(start_date, end_date.date_naive());
|
let exclusivity_condition = utils::day_exclusivity_condition(start_date, end_date.date_naive());
|
||||||
cache_report(&db, &report, Some(exclusivity_condition)).await?;
|
sync_service::cache_report(&db, &report, Some(exclusivity_condition)).await?;
|
||||||
|
|
||||||
Ok("Ok")
|
Ok("Ok")
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/sync_service.rs
Normal file
45
src/sync_service.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
use crate::entity::time_entry;
|
||||||
|
use crate::entity::time_entry::Entity as TimeEntry;
|
||||||
|
use crate::toggl_api::types::ReportRow;
|
||||||
|
use crate::utils;
|
||||||
|
use migration::Condition;
|
||||||
|
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
||||||
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn cache_report(
|
||||||
|
db: &DatabaseConnection,
|
||||||
|
models: &Vec<ReportRow>,
|
||||||
|
exclusive_on: Option<Condition>,
|
||||||
|
) -> utils::Result<()> {
|
||||||
|
let models = models.iter().flat_map(|entry| entry.as_models());
|
||||||
|
let models = models.collect::<Vec<_>>();
|
||||||
|
let ids = models
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.toggl_id.clone().unwrap())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
debug!("Caching report entries: {:?}", models);
|
||||||
|
|
||||||
|
// TODO: Why is this needed?
|
||||||
|
if models.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeEntry::insert_many(models)
|
||||||
|
.on_conflict(ReportRow::grafting_conflict_statement())
|
||||||
|
.exec(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(exclusive_on) = exclusive_on {
|
||||||
|
TimeEntry::delete_many()
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(exclusive_on)
|
||||||
|
.add(time_entry::Column::TogglId.is_in(ids).not()),
|
||||||
|
)
|
||||||
|
.exec(db)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -1,17 +1,19 @@
|
|||||||
|
use crate::toggl_api::types::{
|
||||||
|
Client as ProjectClient, Project, ReportRow, TimeEntry, TogglReportQuery,
|
||||||
|
};
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use base64::engine::general_purpose::STANDARD;
|
||||||
|
use base64::Engine;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use hyper::HeaderMap;
|
||||||
|
use reqwest::header::HeaderValue;
|
||||||
use reqwest::{Client, RequestBuilder, Response};
|
use reqwest::{Client, RequestBuilder, Response};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use anyhow::anyhow;
|
|
||||||
use axum::http::StatusCode;
|
|
||||||
use base64::Engine;
|
|
||||||
use base64::engine::general_purpose::STANDARD;
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use hyper::HeaderMap;
|
|
||||||
use reqwest::header::HeaderValue;
|
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use tracing::log::debug;
|
use tracing::log::debug;
|
||||||
use crate::toggl_api::types::{TimeEntry, Project, Client as ProjectClient, ReportRow, TogglReportQuery};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TogglApiClient {
|
pub struct TogglApiClient {
|
||||||
@ -24,7 +26,9 @@ pub struct TogglApiClient {
|
|||||||
impl TogglApiClient {
|
impl TogglApiClient {
|
||||||
async fn make_request(&self, request_builder: RequestBuilder) -> crate::Result<Response> {
|
async fn make_request(&self, request_builder: RequestBuilder) -> crate::Result<Response> {
|
||||||
loop {
|
loop {
|
||||||
let builder = request_builder.try_clone().ok_or(anyhow!("Failed to clone request builder"))?;
|
let builder = request_builder
|
||||||
|
.try_clone()
|
||||||
|
.ok_or(anyhow!("Failed to clone request builder"))?;
|
||||||
let response = self.client.execute(builder.build()?).await?;
|
let response = self.client.execute(builder.build()?).await?;
|
||||||
|
|
||||||
// If we are rate limited, wait a bit and try again
|
// If we are rate limited, wait a bit and try again
|
||||||
@ -60,10 +64,7 @@ impl TogglApiClient {
|
|||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
let mut value = HeaderValue::from_str(&format!("Basic {}", toggl_auth)).unwrap();
|
let mut value = HeaderValue::from_str(&format!("Basic {}", toggl_auth)).unwrap();
|
||||||
value.set_sensitive(true);
|
value.set_sensitive(true);
|
||||||
headers.insert(
|
headers.insert("Authorization", value);
|
||||||
"Authorization",
|
|
||||||
value,
|
|
||||||
);
|
|
||||||
headers
|
headers
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,9 +75,8 @@ impl TogglApiClient {
|
|||||||
base_url = self.base_url,
|
base_url = self.base_url,
|
||||||
);
|
);
|
||||||
|
|
||||||
let projects = self.make_request(self
|
let projects = self
|
||||||
.client
|
.make_request(self.client.get(&url))
|
||||||
.get(&url))
|
|
||||||
.await?
|
.await?
|
||||||
.json::<Vec<Project>>()
|
.json::<Vec<Project>>()
|
||||||
.await?;
|
.await?;
|
||||||
@ -91,9 +91,8 @@ impl TogglApiClient {
|
|||||||
base_url = self.base_url,
|
base_url = self.base_url,
|
||||||
);
|
);
|
||||||
|
|
||||||
let clients = self.make_request(self
|
let clients = self
|
||||||
.client
|
.make_request(self.client.get(&url))
|
||||||
.get(&url))
|
|
||||||
.await?
|
.await?
|
||||||
.json::<Vec<ProjectClient>>()
|
.json::<Vec<ProjectClient>>()
|
||||||
.await?;
|
.await?;
|
||||||
@ -101,37 +100,37 @@ impl TogglApiClient {
|
|||||||
Ok(clients)
|
Ok(clients)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_time_entries_modified_since(&self, date_time: DateTime<Utc>) -> crate::Result<Vec<TimeEntry>> {
|
pub async fn fetch_time_entries_modified_since(
|
||||||
let url = format!(
|
&self,
|
||||||
"{base_url}/me/time_entries",
|
date_time: DateTime<Utc>,
|
||||||
base_url = self.base_url
|
) -> crate::Result<Vec<TimeEntry>> {
|
||||||
);
|
let url = format!("{base_url}/me/time_entries", base_url = self.base_url);
|
||||||
|
|
||||||
Ok(
|
Ok(self
|
||||||
self.make_request(self.client.get(url)
|
.make_request(
|
||||||
.query(&[("since", date_time.timestamp())]))
|
self.client
|
||||||
.await?
|
.get(url)
|
||||||
.json::<Vec<TimeEntry>>()
|
.query(&[("since", date_time.timestamp())]),
|
||||||
.await?
|
)
|
||||||
)
|
.await?
|
||||||
|
.json::<Vec<TimeEntry>>()
|
||||||
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_time_entries_in_range(&self, (start, end): (DateTime<Utc>, DateTime<Utc>)) -> crate::Result<Vec<TimeEntry>> {
|
pub async fn fetch_time_entries_in_range(
|
||||||
let url = format!(
|
&self,
|
||||||
"{base_url}/me/time_entries",
|
(start, end): (DateTime<Utc>, DateTime<Utc>),
|
||||||
base_url = self.base_url
|
) -> crate::Result<Vec<TimeEntry>> {
|
||||||
);
|
let url = format!("{base_url}/me/time_entries", base_url = self.base_url);
|
||||||
|
|
||||||
Ok(
|
Ok(self
|
||||||
self.make_request(self.client.get(url)
|
.make_request(self.client.get(url).query(&[
|
||||||
.query(&[
|
("start_date", start.to_rfc3339()),
|
||||||
("start_date", start.to_rfc3339()),
|
("end_date", end.to_rfc3339()),
|
||||||
("end_date", end.to_rfc3339())
|
]))
|
||||||
]))
|
.await?
|
||||||
.await?
|
.json::<Vec<TimeEntry>>()
|
||||||
.json::<Vec<TimeEntry>>()
|
.await?)
|
||||||
.await?
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_current_time_entry(&self) -> crate::Result<Option<TimeEntry>> {
|
pub async fn fetch_current_time_entry(&self) -> crate::Result<Option<TimeEntry>> {
|
||||||
@ -140,9 +139,8 @@ impl TogglApiClient {
|
|||||||
base_url = self.base_url
|
base_url = self.base_url
|
||||||
);
|
);
|
||||||
|
|
||||||
let res = self.make_request(self
|
let res = self
|
||||||
.client
|
.make_request(self.client.get(url))
|
||||||
.get(url))
|
|
||||||
.await?
|
.await?
|
||||||
.json::<Option<TimeEntry>>()
|
.json::<Option<TimeEntry>>()
|
||||||
.await?;
|
.await?;
|
||||||
@ -162,9 +160,7 @@ impl TogglApiClient {
|
|||||||
self.workspace_id.parse::<i32>()?.into(),
|
self.workspace_id.parse::<i32>()?.into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.make_request(self.client
|
self.make_request(self.client.post(url).json(&body)).await?;
|
||||||
.post(url)
|
|
||||||
.json(&body)).await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -180,10 +176,7 @@ impl TogglApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, filters))]
|
#[instrument(skip(self, filters))]
|
||||||
pub async fn full_report(
|
pub async fn full_report(&self, filters: &TogglReportQuery) -> crate::Result<Vec<ReportRow>> {
|
||||||
&self,
|
|
||||||
filters: &TogglReportQuery,
|
|
||||||
) -> crate::Result<Vec<ReportRow>> {
|
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{base_url}/workspace/{workspace_id}/search/time_entries",
|
"{base_url}/workspace/{workspace_id}/search/time_entries",
|
||||||
base_url = self.reports_base_url,
|
base_url = self.reports_base_url,
|
||||||
@ -201,15 +194,15 @@ impl TogglApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement rate limiting
|
// TODO: Implement rate limiting
|
||||||
let response = self.make_request(self
|
let response = self
|
||||||
.client
|
.make_request(
|
||||||
.post(&url)
|
self.client
|
||||||
.json(&Self::paginate_filters(&filters, last_row_number_n)))
|
.post(&url)
|
||||||
|
.json(&Self::paginate_filters(&filters, last_row_number_n)),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let data = response
|
let data = response.json::<Vec<ReportRow>>().await?;
|
||||||
.json::<Vec<ReportRow>>()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
last_row_number = data.last().map(|e| e.row_number as u64);
|
last_row_number = data.last().map(|e| e.row_number as u64);
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use serde_with::skip_serializing_none;
|
use serde_with::skip_serializing_none;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::option::Option;
|
use std::option::Option;
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
pub struct ReportRow {
|
pub struct ReportRow {
|
||||||
@ -46,7 +46,6 @@ pub struct TimeEntry {
|
|||||||
pub at: DateTime<Utc>,
|
pub at: DateTime<Utc>,
|
||||||
pub server_deleted_at: Option<DateTime<Utc>>,
|
pub server_deleted_at: Option<DateTime<Utc>>,
|
||||||
pub user_id: i64,
|
pub user_id: i64,
|
||||||
|
|
||||||
// Ignored fields
|
// Ignored fields
|
||||||
// duronly: bool,
|
// duronly: bool,
|
||||||
// uid: i64,
|
// uid: i64,
|
||||||
@ -65,7 +64,6 @@ pub struct Project {
|
|||||||
pub at: DateTime<Utc>,
|
pub at: DateTime<Utc>,
|
||||||
pub server_deleted_at: Option<DateTime<Utc>>,
|
pub server_deleted_at: Option<DateTime<Utc>>,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
|
|
||||||
// cid: Option<serde_json::Value>,
|
// cid: Option<serde_json::Value>,
|
||||||
// wid: i64,
|
// wid: i64,
|
||||||
// rate: Option<serde_json::Value>,
|
// rate: Option<serde_json::Value>,
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
|
use crate::entity::time_entry;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use tokio::signal;
|
|
||||||
use chrono::{NaiveDate, NaiveTime};
|
use chrono::{NaiveDate, NaiveTime};
|
||||||
use migration::{Condition, IntoCondition};
|
use migration::{Condition, IntoCondition};
|
||||||
use sea_orm::ColumnTrait;
|
use sea_orm::ColumnTrait;
|
||||||
use crate::entity::time_entry;
|
use tokio::signal;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AppError(anyhow::Error);
|
pub struct AppError(anyhow::Error);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user