-
Couldn't load subscription status.
- Fork 0
api: enforce single default ledger, JWT env secret, export_stream scaffold #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| -- 028_add_unique_default_ledger_index.sql | ||
| -- Enforce at most one default ledger per family. | ||
| -- Safe to run multiple times (IF NOT EXISTS guard). | ||
|
|
||
| CREATE UNIQUE INDEX IF NOT EXISTS idx_ledgers_one_default | ||
| ON ledgers(family_id) | ||
| WHERE is_default = true; | ||
|
|
||
| -- Rationale: | ||
| -- Business rule: each family must have a single canonical default ledger used for | ||
| -- category and transaction fallbacks. Prior logic relies on code discipline; this | ||
| -- index guarantees integrity at the database layer and prevents race conditions | ||
| -- where two concurrent creations might both mark default. | ||
|
|
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -446,13 +446,102 @@ pub async fn export_transactions_csv_stream( | |||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| query.push(" ORDER BY t.transaction_date DESC, t.id DESC"); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Execute fully and build CSV body (simple, reliable) | ||||||||||||||||||||||||||||||||||||||
| // When export_stream feature enabled, stream rows instead of buffering entire CSV | ||||||||||||||||||||||||||||||||||||||
| #[cfg(feature = "export_stream")] | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| use futures::StreamExt; | ||||||||||||||||||||||||||||||||||||||
| use tokio::sync::mpsc; | ||||||||||||||||||||||||||||||||||||||
| use tokio_stream::wrappers::ReceiverStream; | ||||||||||||||||||||||||||||||||||||||
| let include_header = q.include_header.unwrap_or(true); | ||||||||||||||||||||||||||||||||||||||
| let (tx, rx) = mpsc::channel::<Result<bytes::Bytes, ApiError>>(8); | ||||||||||||||||||||||||||||||||||||||
| let built = query.build(); | ||||||||||||||||||||||||||||||||||||||
| let pool_clone = pool.clone(); | ||||||||||||||||||||||||||||||||||||||
| tokio::spawn(async move { | ||||||||||||||||||||||||||||||||||||||
| let mut stream = built.fetch_many(&pool_clone); | ||||||||||||||||||||||||||||||||||||||
| // Header | ||||||||||||||||||||||||||||||||||||||
| if include_header { | ||||||||||||||||||||||||||||||||||||||
| if tx | ||||||||||||||||||||||||||||||||||||||
| .send(Ok(bytes::Bytes::from_static( | ||||||||||||||||||||||||||||||||||||||
| b"Date,Description,Amount,Category,Account,Payee,Type\n", | ||||||||||||||||||||||||||||||||||||||
| ))) | ||||||||||||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||||||||||||
| .is_err() | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| while let Some(item) = stream.next().await { | ||||||||||||||||||||||||||||||||||||||
| match item { | ||||||||||||||||||||||||||||||||||||||
| Ok(sqlx::Either::Right(row)) => { | ||||||||||||||||||||||||||||||||||||||
| use sqlx::Row; | ||||||||||||||||||||||||||||||||||||||
| let date: NaiveDate = row.get("transaction_date"); | ||||||||||||||||||||||||||||||||||||||
| let desc: String = | ||||||||||||||||||||||||||||||||||||||
| row.try_get::<String, _>("description").unwrap_or_default(); | ||||||||||||||||||||||||||||||||||||||
| let amount: Decimal = row.get("amount"); | ||||||||||||||||||||||||||||||||||||||
| let category: Option<String> = row | ||||||||||||||||||||||||||||||||||||||
| .try_get::<String, _>("category_name") | ||||||||||||||||||||||||||||||||||||||
| .ok() | ||||||||||||||||||||||||||||||||||||||
| .filter(|s| !s.is_empty()); | ||||||||||||||||||||||||||||||||||||||
| let account_id: Uuid = row.get("account_id"); | ||||||||||||||||||||||||||||||||||||||
| let payee: Option<String> = row | ||||||||||||||||||||||||||||||||||||||
| .try_get::<String, _>("payee_name") | ||||||||||||||||||||||||||||||||||||||
| .ok() | ||||||||||||||||||||||||||||||||||||||
| .filter(|s| !s.is_empty()); | ||||||||||||||||||||||||||||||||||||||
| let ttype: String = row.get("transaction_type"); | ||||||||||||||||||||||||||||||||||||||
| let line = format!( | ||||||||||||||||||||||||||||||||||||||
| "{},{},{},{},{},{},{}\n", | ||||||||||||||||||||||||||||||||||||||
| date, | ||||||||||||||||||||||||||||||||||||||
| csv_escape_cell(desc, ','), | ||||||||||||||||||||||||||||||||||||||
| amount, | ||||||||||||||||||||||||||||||||||||||
| csv_escape_cell(category.clone().unwrap_or_default(), ','), | ||||||||||||||||||||||||||||||||||||||
| account_id, | ||||||||||||||||||||||||||||||||||||||
| csv_escape_cell(payee.clone().unwrap_or_default(), ','), | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+496
to
+498
|
||||||||||||||||||||||||||||||||||||||
| csv_escape_cell(category.clone().unwrap_or_default(), ','), | |
| account_id, | |
| csv_escape_cell(payee.clone().unwrap_or_default(), ','), | |
| csv_escape_cell(category.unwrap_or_default(), ','), | |
| account_id, | |
| csv_escape_cell(payee.unwrap_or_default(), ','), |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary cloning of category and payee values. Since these are Option<String> values already extracted, you can use category.unwrap_or_default() and payee.unwrap_or_default() directly without clone().
| csv_escape_cell(category.clone().unwrap_or_default(), ','), | |
| account_id, | |
| csv_escape_cell(payee.clone().unwrap_or_default(), ','), | |
| csv_escape_cell(category.unwrap_or_default(), ','), | |
| account_id, | |
| csv_escape_cell(payee.unwrap_or_default(), ','), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The .clone() calls on category and payee are unnecessary because the variables are not used again after being passed to csv_escape_cell. The unwrap_or_default() method consumes the Option, so you can pass ownership directly without cloning. This is slightly more efficient and idiomatic.
| csv_escape_cell(category.clone().unwrap_or_default(), ','), | |
| account_id, | |
| csv_escape_cell(payee.clone().unwrap_or_default(), ','), | |
| csv_escape_cell(category.unwrap_or_default(), ','), | |
| account_id, | |
| csv_escape_cell(payee.unwrap_or_default(), ','), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
eprintln!for warnings bypasses proper logging infrastructure. Consider using a proper logging framework (liketracingorlog) to ensure warnings are captured appropriately in production environments.