Add JSON export for full data dump

This commit is contained in:
2026-03-02 00:43:23 +02:00
parent 987ab925ef
commit e53301421e
2 changed files with 165 additions and 0 deletions

View File

@@ -469,6 +469,21 @@ impl Database {
rows.collect() rows.collect()
} }
pub fn list_all_budgets(&self) -> SqlResult<Vec<Budget>> {
let mut stmt = self.conn.prepare(
"SELECT id, category_id, amount, month FROM budgets ORDER BY month ASC, id ASC"
)?;
let rows = stmt.query_map([], |row| {
Ok(Budget {
id: row.get(0)?,
category_id: row.get(1)?,
amount: row.get(2)?,
month: row.get(3)?,
})
})?;
rows.collect()
}
pub fn delete_budget(&self, id: i64) -> SqlResult<()> { pub fn delete_budget(&self, id: i64) -> SqlResult<()> {
self.conn.execute("DELETE FROM budgets WHERE id = ?1", params![id])?; self.conn.execute("DELETE FROM budgets WHERE id = ?1", params![id])?;
Ok(()) Ok(())

View File

@@ -0,0 +1,150 @@
use crate::db::Database;
use crate::models::{Budget, Category, RecurringTransaction, Transaction};
use serde::Serialize;
use std::io::Write;
#[derive(Debug, Serialize)]
pub struct ExportData {
pub transactions: Vec<Transaction>,
pub categories: Vec<Category>,
pub budgets: Vec<Budget>,
pub recurring: Vec<RecurringTransaction>,
}
#[derive(Debug)]
pub enum ExportError {
Db(rusqlite::Error),
Json(serde_json::Error),
Io(std::io::Error),
}
impl From<rusqlite::Error> for ExportError {
fn from(e: rusqlite::Error) -> Self {
ExportError::Db(e)
}
}
impl From<serde_json::Error> for ExportError {
fn from(e: serde_json::Error) -> Self {
ExportError::Json(e)
}
}
impl From<std::io::Error> for ExportError {
fn from(e: std::io::Error) -> Self {
ExportError::Io(e)
}
}
impl std::fmt::Display for ExportError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExportError::Db(e) => write!(f, "Database error: {}", e),
ExportError::Json(e) => write!(f, "JSON error: {}", e),
ExportError::Io(e) => write!(f, "IO error: {}", e),
}
}
}
pub fn export_json<W: Write>(db: &Database, writer: W) -> Result<ExportData, ExportError> {
let transactions = db.list_all_transactions(None, None)?;
let categories = db.list_categories(None)?;
let budgets = db.list_all_budgets()?;
let recurring = db.list_recurring(false)?;
let data = ExportData {
transactions,
categories,
budgets,
recurring,
};
serde_json::to_writer_pretty(writer, &data)?;
Ok(data)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{NewTransaction, TransactionType};
use chrono::NaiveDate;
fn setup_db() -> Database {
Database::open_in_memory().unwrap()
}
#[test]
fn test_json_export_produces_valid_json() {
let db = setup_db();
let cats = db.list_categories(Some(TransactionType::Expense)).unwrap();
let txn = NewTransaction {
amount: 25.0,
transaction_type: TransactionType::Expense,
category_id: cats[0].id,
currency: "USD".to_string(),
exchange_rate: 1.0,
note: Some("Test".to_string()),
date: NaiveDate::from_ymd_opt(2026, 3, 1).unwrap(),
recurring_id: None,
};
db.insert_transaction(&txn).unwrap();
let mut buf = Vec::new();
let data = export_json(&db, &mut buf).unwrap();
assert_eq!(data.transactions.len(), 1);
assert!(!data.categories.is_empty());
assert!(data.budgets.is_empty());
assert!(data.recurring.is_empty());
// Verify it's valid JSON by parsing it back
let output = String::from_utf8(buf).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert!(parsed["transactions"].is_array());
assert!(parsed["categories"].is_array());
}
#[test]
fn test_json_export_includes_all_sections() {
let db = setup_db();
let mut buf = Vec::new();
let data = export_json(&db, &mut buf).unwrap();
// Even with no transactions, we should have default categories
assert!(!data.categories.is_empty());
let output = String::from_utf8(buf).unwrap();
assert!(output.contains("\"transactions\""));
assert!(output.contains("\"categories\""));
assert!(output.contains("\"budgets\""));
assert!(output.contains("\"recurring\""));
}
#[test]
fn test_json_export_transaction_fields() {
let db = setup_db();
let cats = db.list_categories(Some(TransactionType::Income)).unwrap();
let txn = NewTransaction {
amount: 500.0,
transaction_type: TransactionType::Income,
category_id: cats[0].id,
currency: "EUR".to_string(),
exchange_rate: 0.92,
note: Some("Freelance".to_string()),
date: NaiveDate::from_ymd_opt(2026, 2, 15).unwrap(),
recurring_id: None,
};
db.insert_transaction(&txn).unwrap();
let mut buf = Vec::new();
export_json(&db, &mut buf).unwrap();
let output = String::from_utf8(buf).unwrap();
assert!(output.contains("500.0"));
assert!(output.contains("EUR"));
assert!(output.contains("Freelance"));
}
}