From aba86b6979514c0be79358c975406b8888c3726e Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Sun, 31 Aug 2025 18:20:32 -0400 Subject: [PATCH] Fixed cascading history item deletion, added login disclaimer. --- finnow-api/source/account/data_impl_sqlite.d | 51 +++++------- .../source/transaction/data_impl_sqlite.d | 7 ++ finnow-api/sql/schema.sql | 22 ++--- .../history/ValueRecordHistoryItem.vue | 11 ++- web-app/src/pages/AccountPage.vue | 2 +- web-app/src/pages/LoginPage.vue | 45 ++++++++-- web-app/src/pages/MyUserPage.vue | 9 +- web-app/src/pages/home/AccountsModule.vue | 5 +- web-app/src/pages/home/TransactionsModule.vue | 83 ++++++++++--------- 9 files changed, 138 insertions(+), 97 deletions(-) diff --git a/finnow-api/source/account/data_impl_sqlite.d b/finnow-api/source/account/data_impl_sqlite.d index c73741c..1e46eed 100644 --- a/finnow-api/source/account/data_impl_sqlite.d +++ b/finnow-api/source/account/data_impl_sqlite.d @@ -4,6 +4,7 @@ import std.datetime; import d2sqlite3; import handy_http_primitives : Optional; +import slf4d; import account.data; import account.model; @@ -84,16 +85,6 @@ SQL", void deleteById(ulong id) { doTransaction(db, () { - // Delete associated history. - util.sqlite.update( - db, - "DELETE FROM history_item - WHERE id IN ( - SELECT history_item_id FROM account_history_item - WHERE account_id = ? - )", - id - ); // Delete all associated transactions. util.sqlite.update( db, @@ -101,8 +92,19 @@ SQL", "(SELECT transaction_id FROM account_journal_entry WHERE account_id = ?)", id ); - // Finally delete the account itself (and all cascaded entities, like journal entries). + // Delete the account itself (and all cascaded entities, like journal entries). util.sqlite.update(db, "DELETE FROM account WHERE id = ?", id); + /* Delete all orphaned history entries for journal entries referencing deleted transactions. + This is needed because even though the above `DELETE` cascades to + delete all history items for this account, there are some history + items linked to other accounts which aren't deleted automatically. + */ + util.sqlite.update( + db, + "DELETE FROM account_history_item WHERE type LIKE 'JOURNAL_ENTRY' AND id NOT IN ( + SELECT item_id FROM history_item_linked_journal_entry + )" + ); }); } @@ -159,13 +161,12 @@ SQL", Page!AccountHistoryItemResponse getHistory(ulong accountId, in PageRequest pagination) { ulong count = util.sqlite.count( db, - "SELECT COUNT(history_item_id) FROM account_history_item WHERE account_id = ?", + "SELECT COUNT(id) FROM account_history_item WHERE account_id = ?", accountId ); - const baseQuery = "SELECT hi.id, hi.timestamp, hi.type " ~ - "FROM account_history_item ahi " ~ - "LEFT JOIN history_item hi ON ahi.history_item_id = hi.id " ~ - "WHERE ahi.account_id = ?"; + const baseQuery = "SELECT id, timestamp, type " ~ + "FROM account_history_item " ~ + "WHERE account_id = ?"; string query = baseQuery ~ " " ~ pagination.toSql(); // First fetch the basic information about each item. BaseHistoryItem[] items = util.sqlite.findAll( @@ -310,7 +311,7 @@ class SqliteAccountJournalEntryRepository : AccountJournalEntryRepository { void deleteById(ulong id) { util.sqlite.update( db, - "DELETE FROM history_item WHERE id IN " ~ + "DELETE FROM account_history_item WHERE id IN " ~ "(SELECT item_id FROM history_item_linked_journal_entry WHERE journal_entry_id = ?)", id ); @@ -320,7 +321,7 @@ class SqliteAccountJournalEntryRepository : AccountJournalEntryRepository { void deleteByAccountIdAndTransactionId(ulong accountId, ulong transactionId) { util.sqlite.update( db, - "DELETE FROM history_item WHERE id IN " ~ + "DELETE FROM account_history_item WHERE id IN " ~ "(SELECT j.item_id FROM history_item_linked_journal_entry j " ~ " LEFT JOIN account_journal_entry je ON je.id = j.journal_entry_id " ~ "WHERE je.account_id = ? AND je.transaction_id = ?)", @@ -443,7 +444,7 @@ class SqliteAccountValueRecordRepository : AccountValueRecordRepository { // First delete any associated history items: util.sqlite.update( db, - "DELETE FROM history_item WHERE id IN " ~ + "DELETE FROM account_history_item WHERE id IN " ~ "(SELECT item_id FROM history_item_linked_value_record WHERE value_record_id = ?)", id ); @@ -505,16 +506,10 @@ private ulong insertNewAccountHistoryItem( ) { util.sqlite.update( db, - "INSERT INTO history_item (timestamp, type) VALUES (?, ?)", + "INSERT INTO account_history_item (account_id, timestamp, type) VALUES (?, ?, ?)", + accountId, timestamp.toISOExtString(), type ); - ulong historyItemId = db.lastInsertRowid(); - util.sqlite.update( - db, - "INSERT INTO account_history_item (account_id, history_item_id) VALUES (?, ?)", - accountId, - historyItemId - ); - return historyItemId; + return db.lastInsertRowid(); } diff --git a/finnow-api/source/transaction/data_impl_sqlite.d b/finnow-api/source/transaction/data_impl_sqlite.d index 60c61ee..3cc7ccb 100644 --- a/finnow-api/source/transaction/data_impl_sqlite.d +++ b/finnow-api/source/transaction/data_impl_sqlite.d @@ -403,6 +403,13 @@ class SqliteTransactionRepository : TransactionRepository { void deleteById(ulong id) { util.sqlite.deleteById(db, TABLE_NAME, id); + // Delete all history items for journal entries that have been removed by the deletion. + util.sqlite.update( + db, + "DELETE FROM account_history_item WHERE type LIKE 'JOURNAL_ENTRY' AND id NOT IN ( + SELECT item_id FROM history_item_linked_journal_entry + )" + ); } static Transaction parseTransaction(Row row) { diff --git a/finnow-api/sql/schema.sql b/finnow-api/sql/schema.sql index 9e1c6d2..a54d1f0 100644 --- a/finnow-api/sql/schema.sql +++ b/finnow-api/sql/schema.sql @@ -161,21 +161,13 @@ CREATE TABLE account_value_record_attachment ( -- History Entities -CREATE TABLE history_item ( - id INTEGER PRIMARY KEY, - timestamp TEXT NOT NULL, - type TEXT NOT NULL -); - CREATE TABLE account_history_item ( + id INTEGER PRIMARY KEY, account_id INTEGER NOT NULL, - history_item_id INTEGER NOT NULL, - PRIMARY KEY (account_id, history_item_id), + timestamp TEXT NOT NULL, + type TEXT NOT NULL, CONSTRAINT fk_account_history_item_account FOREIGN KEY (account_id) REFERENCES account(id) - ON UPDATE CASCADE ON DELETE CASCADE, - CONSTRAINT fk_account_history_item_history_item - FOREIGN KEY (history_item_id) REFERENCES history_item(id) ON UPDATE CASCADE ON DELETE CASCADE ); @@ -184,7 +176,7 @@ CREATE TABLE history_item_text ( item_id INTEGER PRIMARY KEY, content TEXT NOT NULL, CONSTRAINT fk_history_item_text_item - FOREIGN KEY (item_id) REFERENCES history_item(id) + FOREIGN KEY (item_id) REFERENCES account_history_item(id) ON UPDATE CASCADE ON DELETE CASCADE ); @@ -196,7 +188,7 @@ CREATE TABLE history_item_property_change ( new_value TEXT, PRIMARY KEY (item_id, property_name), CONSTRAINT fk_history_item_property_change_item - FOREIGN KEY (item_id) REFERENCES history_item(id) + FOREIGN KEY (item_id) REFERENCES account_history_item(id) ON UPDATE CASCADE ON DELETE CASCADE ); @@ -206,7 +198,7 @@ CREATE TABLE history_item_linked_value_record ( value_record_id INTEGER NOT NULL, PRIMARY KEY (item_id, value_record_id), CONSTRAINT fk_history_item_linked_value_record_item - FOREIGN KEY (item_id) REFERENCES history_item(id) + FOREIGN KEY (item_id) REFERENCES account_history_item(id) ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT fk_history_item_linked_value_record_value_record FOREIGN KEY (value_record_id) REFERENCES account_value_record(id) @@ -219,7 +211,7 @@ CREATE TABLE history_item_linked_journal_entry ( journal_entry_id INTEGER NOT NULL, PRIMARY KEY (item_id, journal_entry_id), CONSTRAINT fk_history_item_linked_journal_entry_item - FOREIGN KEY (item_id) REFERENCES history_item(id) + FOREIGN KEY (item_id) REFERENCES account_history_item(id) ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT fk_history_item_linked_journal_entry_journal_entry FOREIGN KEY (journal_entry_id) REFERENCES account_journal_entry(id) diff --git a/web-app/src/components/history/ValueRecordHistoryItem.vue b/web-app/src/components/history/ValueRecordHistoryItem.vue index 04a5d13..6c27a90 100644 --- a/web-app/src/components/history/ValueRecordHistoryItem.vue +++ b/web-app/src/components/history/ValueRecordHistoryItem.vue @@ -4,13 +4,14 @@ import { formatMoney } from '@/api/data'; import AppButton from '../AppButton.vue'; import { showConfirm } from '@/util/alert'; import { useRoute } from 'vue-router'; +import ButtonBar from '../ButtonBar.vue'; const route = useRoute() const props = defineProps<{ item: AccountHistoryValueRecordItem, accountId: number }>() async function deleteValueRecord(id: number) { - const confirm = await showConfirm('Are you sure you want to delete this value record?') + const confirm = await showConfirm("Are you sure you want to delete this value record? This may affect how your account's balance is calculated.") if (!confirm) return const api = new AccountApiClient(route) try { @@ -25,8 +26,10 @@ async function deleteValueRecord(id: number) {
Value recorded for this account at {{ formatMoney(item.value, item.currency) }}
-
- -
+ + + Delete this record + + diff --git a/web-app/src/pages/AccountPage.vue b/web-app/src/pages/AccountPage.vue index 247b334..f18acdb 100644 --- a/web-app/src/pages/AccountPage.vue +++ b/web-app/src/pages/AccountPage.vue @@ -84,7 +84,7 @@ async function addValueRecord() { Description {{ account.description }} - + Current Balance {{ formatMoney(account.currentBalance, account.currency) }} diff --git a/web-app/src/pages/LoginPage.vue b/web-app/src/pages/LoginPage.vue index 0341b3c..c3fc422 100644 --- a/web-app/src/pages/LoginPage.vue +++ b/web-app/src/pages/LoginPage.vue @@ -75,24 +75,43 @@ function generateSampleData() {