diff --git a/.sqlx/query-0b4b51420a53deb3c37cea4c85c6dc5203cf7200dee8ed8cc6326ef243f716b0.json b/.sqlx/query-0b4b51420a53deb3c37cea4c85c6dc5203cf7200dee8ed8cc6326ef243f716b0.json new file mode 100644 index 0000000..583cd76 --- /dev/null +++ b/.sqlx/query-0b4b51420a53deb3c37cea4c85c6dc5203cf7200dee8ed8cc6326ef243f716b0.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO tags (id, name, workspace_id, creator_id, updated_at, deleted_at, permissions)\n SELECT * FROM UNNEST($1::bigint[], $2::text[], $3::bigint[], $4::bigint[], $5::timestamptz[], $6::timestamptz[], $7::text[])\n ON CONFLICT (id) DO UPDATE SET\n name = excluded.name,\n workspace_id = excluded.workspace_id,\n creator_id = excluded.creator_id,\n updated_at = excluded.updated_at,\n deleted_at = excluded.deleted_at,\n permissions = excluded.permissions\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8Array", + "TextArray", + "Int8Array", + "Int8Array", + "TimestamptzArray", + "TimestamptzArray", + "TextArray" + ] + }, + "nullable": [] + }, + "hash": "0b4b51420a53deb3c37cea4c85c6dc5203cf7200dee8ed8cc6326ef243f716b0" +} diff --git a/.sqlx/query-107be36abd0886a0a6179c1c589ab1ceb3a43851c0b5a4e0efda0fcef7072b6e.json b/.sqlx/query-107be36abd0886a0a6179c1c589ab1ceb3a43851c0b5a4e0efda0fcef7072b6e.json new file mode 100644 index 0000000..bc8d424 --- /dev/null +++ b/.sqlx/query-107be36abd0886a0a6179c1c589ab1ceb3a43851c0b5a4e0efda0fcef7072b6e.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO tracking_clients\n (id, updated_at, archived, creator_id, integration_provider, notes, name, server_deleted_at, workspace_id, permissions)\n SELECT * FROM UNNEST($1::bigint[], $2::timestamptz[], $3::bool[], $4::bigint[], $5::text[], $6::text[], $7::text[], $8::timestamptz[], $9::bigint[], $10::text[])\n ON CONFLICT (id) DO UPDATE SET\n updated_at = excluded.updated_at,\n archived = excluded.archived,\n creator_id = excluded.creator_id,\n integration_provider = excluded.integration_provider,\n notes = excluded.notes,\n name = excluded.name,\n server_deleted_at = excluded.server_deleted_at,\n workspace_id = excluded.workspace_id,\n permissions = excluded.permissions\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8Array", + "TimestamptzArray", + "BoolArray", + "Int8Array", + "TextArray", + "TextArray", + "TextArray", + "TimestamptzArray", + "Int8Array", + "TextArray" + ] + }, + "nullable": [] + }, + "hash": "107be36abd0886a0a6179c1c589ab1ceb3a43851c0b5a4e0efda0fcef7072b6e" +} diff --git a/.sqlx/query-1681e2e9011c799d91f79b45ab712963093766c6c8b86a0a46fa6e1b3e1dca7b.json b/.sqlx/query-1681e2e9011c799d91f79b45ab712963093766c6c8b86a0a46fa6e1b3e1dca7b.json new file mode 100644 index 0000000..210b735 --- /dev/null +++ b/.sqlx/query-1681e2e9011c799d91f79b45ab712963093766c6c8b86a0a46fa6e1b3e1dca7b.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "select max(updated_at) as last_updated_at from time_entries", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "last_updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "1681e2e9011c799d91f79b45ab712963093766c6c8b86a0a46fa6e1b3e1dca7b" +} diff --git a/.sqlx/query-19019001661f62a6c91ef13cd5903df954eb6c3316bce5146388d3066fd1d410.json b/.sqlx/query-19019001661f62a6c91ef13cd5903df954eb6c3316bce5146388d3066fd1d410.json new file mode 100644 index 0000000..b75ff17 --- /dev/null +++ b/.sqlx/query-19019001661f62a6c91ef13cd5903df954eb6c3316bce5146388d3066fd1d410.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "select id from tags", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "19019001661f62a6c91ef13cd5903df954eb6c3316bce5146388d3066fd1d410" +} diff --git a/.sqlx/query-2699f6f1991bae9b83566276daa4ed6b0a8984e46b112fc7e4ce17e07d444367.json b/.sqlx/query-2699f6f1991bae9b83566276daa4ed6b0a8984e46b112fc7e4ce17e07d444367.json new file mode 100644 index 0000000..22ec7cc --- /dev/null +++ b/.sqlx/query-2699f6f1991bae9b83566276daa4ed6b0a8984e46b112fc7e4ce17e07d444367.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO time_entries (id, workspace_id, user_id, project_id, task_id, start, stop, duration, updated_at, description, billable, server_deleted_at, permissions)\n SELECT * FROM UNNEST($1::bigint[], $2::bigint[], $3::bigint[], $4::bigint[], $5::bigint[], $6::timestamptz[], $7::timestamptz[], $8::int[], $9::timestamptz[], $10::text[], $11::bool[], $12::timestamptz[], $13::text[])\n ON CONFLICT (id) DO UPDATE SET\n workspace_id = excluded.workspace_id,\n user_id = excluded.user_id,\n project_id = excluded.project_id,\n task_id = excluded.task_id,\n start = excluded.start,\n stop = excluded.stop,\n duration = excluded.duration,\n updated_at = excluded.updated_at,\n description = excluded.description,\n billable = excluded.billable,\n server_deleted_at = excluded.server_deleted_at,\n permissions = excluded.permissions\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8Array", + "Int8Array", + "Int8Array", + "Int8Array", + "Int8Array", + "TimestamptzArray", + "TimestamptzArray", + "Int4Array", + "TimestamptzArray", + "TextArray", + "BoolArray", + "TimestamptzArray", + "TextArray" + ] + }, + "nullable": [] + }, + "hash": "2699f6f1991bae9b83566276daa4ed6b0a8984e46b112fc7e4ce17e07d444367" +} diff --git a/.sqlx/query-3c9f3d19e00757a1b6b61b42bf34ea6099b4e4c16be2c345bbc7fe9604b89938.json b/.sqlx/query-3c9f3d19e00757a1b6b61b42bf34ea6099b4e4c16be2c345bbc7fe9604b89938.json new file mode 100644 index 0000000..e1e0c8b --- /dev/null +++ b/.sqlx/query-3c9f3d19e00757a1b6b61b42bf34ea6099b4e4c16be2c345bbc7fe9604b89938.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "select id from tracking_clients", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "3c9f3d19e00757a1b6b61b42bf34ea6099b4e4c16be2c345bbc7fe9604b89938" +} diff --git a/.sqlx/query-6fe6248362270f0c9c522b3e27436972a4b56f7ea6a4cdb0b7a3e07d969f39de.json b/.sqlx/query-6fe6248362270f0c9c522b3e27436972a4b56f7ea6a4cdb0b7a3e07d969f39de.json new file mode 100644 index 0000000..3665908 --- /dev/null +++ b/.sqlx/query-6fe6248362270f0c9c522b3e27436972a4b56f7ea6a4cdb0b7a3e07d969f39de.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO workspaces (id, organization_id, name)\n SELECT * FROM UNNEST($1::bigint[], $2::bigint[], $3::text[])\n ON CONFLICT (id) DO UPDATE SET\n organization_id = excluded.organization_id,\n name = excluded.name\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8Array", + "Int8Array", + "TextArray" + ] + }, + "nullable": [] + }, + "hash": "6fe6248362270f0c9c522b3e27436972a4b56f7ea6a4cdb0b7a3e07d969f39de" +} diff --git a/.sqlx/query-a6ba6775ce708715abcbf946cb816fdba2b068f1645e7941f54a43a2b6639d31.json b/.sqlx/query-a6ba6775ce708715abcbf946cb816fdba2b068f1645e7941f54a43a2b6639d31.json new file mode 100644 index 0000000..dcf56cf --- /dev/null +++ b/.sqlx/query-a6ba6775ce708715abcbf946cb816fdba2b068f1645e7941f54a43a2b6639d31.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "select id from projects", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "a6ba6775ce708715abcbf946cb816fdba2b068f1645e7941f54a43a2b6639d31" +} diff --git a/.sqlx/query-e76966ebbfd08291c10ce64fb947bfcf0096ae4f16e3f91ffd3f512a9c949c45.json b/.sqlx/query-e76966ebbfd08291c10ce64fb947bfcf0096ae4f16e3f91ffd3f512a9c949c45.json new file mode 100644 index 0000000..12a0d38 --- /dev/null +++ b/.sqlx/query-e76966ebbfd08291c10ce64fb947bfcf0096ae4f16e3f91ffd3f512a9c949c45.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "select id from workspaces", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "e76966ebbfd08291c10ce64fb947bfcf0096ae4f16e3f91ffd3f512a9c949c45" +} diff --git a/.sqlx/query-eb4f7048dcaa8186c11bc20961e941f02c599088471b3515b39c308f551d6a7e.json b/.sqlx/query-eb4f7048dcaa8186c11bc20961e941f02c599088471b3515b39c308f551d6a7e.json new file mode 100644 index 0000000..2c32687 --- /dev/null +++ b/.sqlx/query-eb4f7048dcaa8186c11bc20961e941f02c599088471b3515b39c308f551d6a7e.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO projects (id, workspace_id, client_id, name, color, status, active, updated_at, start_date, created_at, server_deleted_at, actual_hours, actual_seconds, can_track_time, permissions)\n SELECT * FROM UNNEST($1::bigint[], $2::bigint[], $3::bigint[], $4::text[], $5::text[], $6::text[], $7::bool[], $8::timestamptz[], $9::date[], $10::timestamptz[], $11::timestamptz[], $12::int[], $13::int[], $14::bool[], $15::text[])\n ON CONFLICT (id) DO UPDATE SET\n workspace_id = excluded.workspace_id,\n client_id = excluded.client_id,\n name = excluded.name,\n color = excluded.color,\n status = excluded.status,\n active = excluded.active,\n updated_at = excluded.updated_at,\n start_date = excluded.start_date,\n created_at = excluded.created_at,\n server_deleted_at = excluded.server_deleted_at,\n actual_hours = excluded.actual_hours,\n actual_seconds = excluded.actual_seconds,\n can_track_time = excluded.can_track_time,\n permissions = excluded.permissions\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8Array", + "Int8Array", + "Int8Array", + "TextArray", + "TextArray", + "TextArray", + "BoolArray", + "TimestamptzArray", + "DateArray", + "TimestamptzArray", + "TimestamptzArray", + "Int4Array", + "Int4Array", + "BoolArray", + "TextArray" + ] + }, + "nullable": [] + }, + "hash": "eb4f7048dcaa8186c11bc20961e941f02c599088471b3515b39c308f551d6a7e" +} diff --git a/Cargo.lock b/Cargo.lock index ed0b505..5f6f8d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -246,6 +295,52 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "clap" +version = "4.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + [[package]] name = "const-oid" version = "0.9.6" @@ -398,12 +493,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "dotenv" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" - [[package]] name = "dotenvy" version = "0.15.7" @@ -716,6 +805,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -954,6 +1049,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.13.0" @@ -2054,7 +2155,7 @@ checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" dependencies = [ "dotenvy", "either", - "heck", + "heck 0.4.1", "hex", "once_cell", "proc-macro2", @@ -2351,7 +2452,8 @@ dependencies = [ "axum", "base64 0.22.1", "chrono", - "dotenv", + "clap", + "dotenvy", "futures", "governor", "itertools", @@ -2607,6 +2709,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 66ef1d4..f20da1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,9 +20,10 @@ serde_json_path_to_error = "0.1.4" 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" itertools = "0.13.0" soa-rs = "0.6.1" +clap = { version = "4.5.11", features = ["derive", "env"] } +dotenvy = "0.15.7" diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..53f64ed --- /dev/null +++ b/Justfile @@ -0,0 +1,2 @@ +prepare: + cargo sqlx prepare diff --git a/src/main.rs b/src/main.rs index 6a5cde2..61854fa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,47 +42,69 @@ struct TableSummary { tag_ids: Vec, } -struct Environment { +use clap::{Parser, Subcommand}; +use std::net::IpAddr; +use chrono::TimeDelta; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, + + #[arg(long, env = "DATABASE_URL")] database_url: String, + + #[arg(long, env = "API_TOKEN")] api_token: String, + + #[arg(long, env = "DEFAULT_WORKSPACE_ID")] default_workspace_id: u64, } -impl Environment { - fn from_env() -> Self { - let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); - let api_token = std::env::var("API_TOKEN").expect("API_TOKEN must be set"); - let default_workspace_id = std::env::var("DEFAULT_WORKSPACE_ID") - .expect("DEFAULT_WORKSPACE_ID must be set") - .parse() - .expect("DEFAULT_WORKSPACE_ID must be a number"); +#[derive(Subcommand)] +enum Commands { + Server { + #[arg(long, env = "IP", default_value = "127.0.0.1")] + ip: IpAddr, - Self { - database_url, - api_token, - default_workspace_id, - } - } + #[arg(long, env = "PORT", default_value = "3000")] + port: u16, + }, + + Migrate, + + Sync, } #[tokio::main] async fn main() { - dotenv::dotenv().expect("Failed to load .env file"); + dotenvy::dotenv().expect("Failed to load .env file"); // Init tracing tracing_subscriber::fmt::init(); - let env_config = Environment::from_env(); + let cli = Cli::parse(); - let toggl_api = TogglApi::new(&env_config.api_token, env_config.default_workspace_id); - let mut db = PgPool::connect(&env_config.database_url).await.unwrap(); + let toggl_api = TogglApi::new(&cli.api_token, cli.default_workspace_id); + let mut db = PgPool::connect(&cli.database_url).await.unwrap(); sqlx::migrate!("./migrations") .run(&db) .await .expect("Failed to run migrations"); + // Return early if we are just migrating + if let Commands::Migrate = cli.command { + return; + } + let worker = Worker { db, toggl_api }; - server::serve(worker).await.expect("Failed to start server") + if let Commands::Server { ip, port } = cli.command { + server::serve(worker, ip, port).await.expect("Failed to start server"); + } else { + worker.update(TimeDelta::days(30)) + .await.expect("Failed to update worker"); + } } diff --git a/src/server.rs b/src/server.rs index 60f1907..5be6e0f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,3 +1,4 @@ +use std::net::IpAddr; use axum::response::IntoResponse; use axum::{ http::StatusCode, @@ -15,7 +16,7 @@ async fn sync(Extension(worker): Extension) -> Result Result<(), AppError> { +pub async fn serve(worker: Worker, ip: IpAddr, port: u16) -> Result<(), AppError> { // build our application with a route let app = Router::new() .route("/health", get(|| async { "Ok" })) @@ -23,7 +24,7 @@ pub async fn serve(worker: Worker) -> Result<(), AppError> { .layer(Extension(worker)); // run our app with hyper, listening globally on port 3000 - let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; + let listener = tokio::net::TcpListener::bind((ip, port)).await?; axum::serve(listener, app) .with_graceful_shutdown(async {