Add JSON export for full data dump
This commit is contained in:
@@ -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(())
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user