Convert to proper CLI and add SQLX prepare statements

This commit is contained in:
Joshua Coles 2024-07-28 13:59:21 +01:00
parent b62d6db866
commit d78d82f8fa
15 changed files with 378 additions and 31 deletions

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

124
Cargo.lock generated
View File

@ -51,6 +51,55 @@ dependencies = [
"libc", "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]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.86" version = "1.0.86"
@ -246,6 +295,52 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.6" version = "0.9.6"
@ -398,12 +493,6 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "dotenv"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]] [[package]]
name = "dotenvy" name = "dotenvy"
version = "0.15.7" version = "0.15.7"
@ -716,6 +805,12 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.9" version = "0.3.9"
@ -954,6 +1049,12 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.13.0" version = "0.13.0"
@ -2054,7 +2155,7 @@ checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8"
dependencies = [ dependencies = [
"dotenvy", "dotenvy",
"either", "either",
"heck", "heck 0.4.1",
"hex", "hex",
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
@ -2351,7 +2452,8 @@ dependencies = [
"axum", "axum",
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
"dotenv", "clap",
"dotenvy",
"futures", "futures",
"governor", "governor",
"itertools", "itertools",
@ -2607,6 +2709,12 @@ version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"

View File

@ -20,9 +20,10 @@ serde_json_path_to_error = "0.1.4"
url = "2.5.2" url = "2.5.2"
serde_with = "3.9.0" serde_with = "3.9.0"
sqlx = { version = "0.7.4", features = ["postgres", "runtime-tokio", "macros", "chrono"] } sqlx = { version = "0.7.4", features = ["postgres", "runtime-tokio", "macros", "chrono"] }
dotenv = "0.15.0"
futures = "0.3.30" futures = "0.3.30"
tracing = "0.1.40" tracing = "0.1.40"
tracing-subscriber = "0.3.18" tracing-subscriber = "0.3.18"
itertools = "0.13.0" itertools = "0.13.0"
soa-rs = "0.6.1" soa-rs = "0.6.1"
clap = { version = "4.5.11", features = ["derive", "env"] }
dotenvy = "0.15.7"

2
Justfile Normal file
View File

@ -0,0 +1,2 @@
prepare:
cargo sqlx prepare

View File

@ -42,47 +42,69 @@ struct TableSummary {
tag_ids: Vec<u64>, tag_ids: Vec<u64>,
} }
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, database_url: String,
#[arg(long, env = "API_TOKEN")]
api_token: String, api_token: String,
#[arg(long, env = "DEFAULT_WORKSPACE_ID")]
default_workspace_id: u64, default_workspace_id: u64,
} }
impl Environment { #[derive(Subcommand)]
fn from_env() -> Self { enum Commands {
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); Server {
let api_token = std::env::var("API_TOKEN").expect("API_TOKEN must be set"); #[arg(long, env = "IP", default_value = "127.0.0.1")]
let default_workspace_id = std::env::var("DEFAULT_WORKSPACE_ID") ip: IpAddr,
.expect("DEFAULT_WORKSPACE_ID must be set")
.parse()
.expect("DEFAULT_WORKSPACE_ID must be a number");
Self { #[arg(long, env = "PORT", default_value = "3000")]
database_url, port: u16,
api_token, },
default_workspace_id,
} Migrate,
}
Sync,
} }
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
dotenv::dotenv().expect("Failed to load .env file"); dotenvy::dotenv().expect("Failed to load .env file");
// Init tracing // Init tracing
tracing_subscriber::fmt::init(); 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 toggl_api = TogglApi::new(&cli.api_token, cli.default_workspace_id);
let mut db = PgPool::connect(&env_config.database_url).await.unwrap(); let mut db = PgPool::connect(&cli.database_url).await.unwrap();
sqlx::migrate!("./migrations") sqlx::migrate!("./migrations")
.run(&db) .run(&db)
.await .await
.expect("Failed to run migrations"); .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 }; 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");
}
} }

View File

@ -1,3 +1,4 @@
use std::net::IpAddr;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::{ use axum::{
http::StatusCode, http::StatusCode,
@ -15,7 +16,7 @@ async fn sync(Extension(worker): Extension<Worker>) -> Result<impl IntoResponse,
Ok("Ok") Ok("Ok")
} }
pub async fn serve(worker: Worker) -> Result<(), AppError> { pub async fn serve(worker: Worker, ip: IpAddr, port: u16) -> Result<(), AppError> {
// build our application with a route // build our application with a route
let app = Router::new() let app = Router::new()
.route("/health", get(|| async { "Ok" })) .route("/health", get(|| async { "Ok" }))
@ -23,7 +24,7 @@ pub async fn serve(worker: Worker) -> Result<(), AppError> {
.layer(Extension(worker)); .layer(Extension(worker));
// run our app with hyper, listening globally on port 3000 // 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) axum::serve(listener, app)
.with_graceful_shutdown(async { .with_graceful_shutdown(async {