@actual-app/core 26.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.swcrc +11 -0
- package/bin/build-browser +40 -0
- package/bin/copy-migrations +9 -0
- package/db.sqlite +0 -0
- package/default-db.sqlite +0 -0
- package/migrations/.force-copy-windows +0 -0
- package/migrations/1548957970627_remove-db-version.sql +5 -0
- package/migrations/1550601598648_payees.sql +23 -0
- package/migrations/1555786194328_remove_category_group_unique.sql +25 -0
- package/migrations/1561751833510_indexes.sql +7 -0
- package/migrations/1567699552727_budget.sql +38 -0
- package/migrations/1582384163573_cleared.sql +6 -0
- package/migrations/1597756566448_rules.sql +10 -0
- package/migrations/1608652596043_parent_field.sql +13 -0
- package/migrations/1608652596044_trans_views.sql +56 -0
- package/migrations/1612625548236_optimize.sql +7 -0
- package/migrations/1614782639336_trans_views2.sql +33 -0
- package/migrations/1615745967948_meta.sql +10 -0
- package/migrations/1616167010796_accounts_order.sql +5 -0
- package/migrations/1618975177358_schedules.sql +28 -0
- package/migrations/1632571489012_remove_cache.js +136 -0
- package/migrations/1679728867040_rules_conditions.sql +5 -0
- package/migrations/1681115033845_add_schedule_name.sql +5 -0
- package/migrations/1682974838138_remove_payee_rules.sql +5 -0
- package/migrations/1685007876842_add_category_hidden.sql +6 -0
- package/migrations/1686139660866_remove_account_type.sql +5 -0
- package/migrations/1688749527273_transaction_filters.sql +10 -0
- package/migrations/1688841238000_add_account_type.sql +5 -0
- package/migrations/1691233396000_add_schedule_next_date_tombstone.sql +5 -0
- package/migrations/1694438752000_add_goal_targets.sql +7 -0
- package/migrations/1697046240000_add_reconciled.sql +5 -0
- package/migrations/1704572023730_add_account_sync_source.sql +5 -0
- package/migrations/1704572023731_add_missing_goCardless_sync_source.sql +9 -0
- package/migrations/1707267033000_reports.sql +28 -0
- package/migrations/1712784523000_unhide_input_group.sql +8 -0
- package/migrations/1716359441000_include_current.sql +5 -0
- package/migrations/1720310586000_link_transfer_schedules.sql +19 -0
- package/migrations/1720664867241_add_payee_favorite.sql +5 -0
- package/migrations/1720665000000_goal_context.sql +6 -0
- package/migrations/1722717601000_reports_move_selected_categories.js +55 -0
- package/migrations/1722804019000_create_dashboard_table.js +69 -0
- package/migrations/1723665565000_prefs.js +59 -0
- package/migrations/1730744182000_fix_dashboard_table.sql +7 -0
- package/migrations/1736640000000_custom_report_sorting.sql +7 -0
- package/migrations/1737158400000_add_learn_categories_to_payees.sql +5 -0
- package/migrations/1738491452000_sorting_rename.sql +13 -0
- package/migrations/1739139550000_bank_sync_page.sql +7 -0
- package/migrations/1740506588539_add_last_reconciled_at.sql +5 -0
- package/migrations/1745425408000_update_budgetType_pref.sql +7 -0
- package/migrations/1749799110000_add_tags.sql +10 -0
- package/migrations/1749799110001_tags_tombstone.sql +5 -0
- package/migrations/1754611200000_add_category_template_settings.sql +5 -0
- package/migrations/1759260219000_add_trim_interval_report_setting.sql +6 -0
- package/migrations/1759842823172_add_isGlobal_to_preferences.sql +1 -0
- package/migrations/1762178745667_rename_csv_skip_lines_pref.sql +8 -0
- package/migrations/1765518577215_multiple_dashboards.js +30 -0
- package/migrations/1768872504000_add_payee_locations.sql +21 -0
- package/package.json +128 -0
- package/src/mocks/arbitrary-schema.ts +162 -0
- package/src/mocks/budget.ts +901 -0
- package/src/mocks/files/8859-1.qfx +63 -0
- package/src/mocks/files/best.data-ever$.QFX +124 -0
- package/src/mocks/files/big.data.QiF +91 -0
- package/src/mocks/files/budgets/.commit-to-git +0 -0
- package/src/mocks/files/camt/camt.053.payee-memo.xml +127 -0
- package/src/mocks/files/camt/camt.053.xml +463 -0
- package/src/mocks/files/credit-card.ofx +11 -0
- package/src/mocks/files/data-multi-decimal.ofx +64 -0
- package/src/mocks/files/data-payee-memo.ofx +75 -0
- package/src/mocks/files/data-payee-memo.qif +17 -0
- package/src/mocks/files/data.ofx +124 -0
- package/src/mocks/files/data.qfx +124 -0
- package/src/mocks/files/data.qif +91 -0
- package/src/mocks/files/default-budget-template/db.sqlite +0 -0
- package/src/mocks/files/default-budget-template/metadata.json +6 -0
- package/src/mocks/files/html-vals.qfx +17 -0
- package/src/mocks/index.ts +221 -0
- package/src/mocks/migrations/1508717984291_up_add-poop.sql +13 -0
- package/src/mocks/migrations/1508718036311_up_modify-poop.sql +2 -0
- package/src/mocks/migrations/1508727787513_remove-is_income.sql +15 -0
- package/src/mocks/random.ts +16 -0
- package/src/mocks/setup.ts +180 -0
- package/src/mocks/spreadsheet.ts +101 -0
- package/src/mocks/util.ts +82 -0
- package/src/platform/client/connection/README.md +3 -0
- package/src/platform/client/connection/__mocks__/index.ts +67 -0
- package/src/platform/client/connection/index-types.ts +95 -0
- package/src/platform/client/connection/index.browser.ts +213 -0
- package/src/platform/client/connection/index.ts +155 -0
- package/src/platform/client/undo/index.ts +59 -0
- package/src/platform/exceptions/__mocks__/index.ts +7 -0
- package/src/platform/exceptions/index.ts +9 -0
- package/src/platform/server/asyncStorage/__mocks__/index.ts +50 -0
- package/src/platform/server/asyncStorage/index-types.ts +35 -0
- package/src/platform/server/asyncStorage/index.api.ts +2 -0
- package/src/platform/server/asyncStorage/index.electron.ts +88 -0
- package/src/platform/server/asyncStorage/index.ts +126 -0
- package/src/platform/server/connection/README.md +3 -0
- package/src/platform/server/connection/__mocks__/index.ts +15 -0
- package/src/platform/server/connection/index-types.ts +20 -0
- package/src/platform/server/connection/index.api.ts +13 -0
- package/src/platform/server/connection/index.electron.ts +102 -0
- package/src/platform/server/connection/index.ts +154 -0
- package/src/platform/server/fetch/__mocks__/index.ts +3 -0
- package/src/platform/server/fetch/index.api.ts +1 -0
- package/src/platform/server/fetch/index.electron.ts +18 -0
- package/src/platform/server/fetch/index.ts +20 -0
- package/src/platform/server/fs/index.api.ts +198 -0
- package/src/platform/server/fs/index.electron.ts +208 -0
- package/src/platform/server/fs/index.test.ts +117 -0
- package/src/platform/server/fs/index.ts +416 -0
- package/src/platform/server/fs/path-join.api.ts +1 -0
- package/src/platform/server/fs/path-join.electron.ts +1 -0
- package/src/platform/server/fs/path-join.ts +97 -0
- package/src/platform/server/fs/shared.ts +33 -0
- package/src/platform/server/indexeddb/index.ts +115 -0
- package/src/platform/server/log/index.ts +43 -0
- package/src/platform/server/sqlite/index.api.ts +2 -0
- package/src/platform/server/sqlite/index.electron.ts +134 -0
- package/src/platform/server/sqlite/index.test.ts +108 -0
- package/src/platform/server/sqlite/index.ts +241 -0
- package/src/platform/server/sqlite/normalise.ts +9 -0
- package/src/platform/server/sqlite/unicodeLike.test.ts +58 -0
- package/src/platform/server/sqlite/unicodeLike.ts +31 -0
- package/src/server/__mocks__/post.ts +9 -0
- package/src/server/__snapshots__/main.test.ts.snap +199 -0
- package/src/server/__snapshots__/sheet.test.ts.snap +9 -0
- package/src/server/accounts/__snapshots__/sync.test.ts.snap +136 -0
- package/src/server/accounts/app-bank-sync.test.ts +136 -0
- package/src/server/accounts/app.ts +1294 -0
- package/src/server/accounts/link.ts +25 -0
- package/src/server/accounts/payees.ts +36 -0
- package/src/server/accounts/sync.test.ts +679 -0
- package/src/server/accounts/sync.ts +1168 -0
- package/src/server/accounts/title/index.ts +60 -0
- package/src/server/accounts/title/lower-case.ts +93 -0
- package/src/server/accounts/title/specials.ts +21 -0
- package/src/server/admin/app.ts +241 -0
- package/src/server/api-models.ts +244 -0
- package/src/server/api.test.ts +36 -0
- package/src/server/api.ts +1030 -0
- package/src/server/app.ts +91 -0
- package/src/server/aql/compiler.test.ts +966 -0
- package/src/server/aql/compiler.ts +1222 -0
- package/src/server/aql/exec.test.ts +289 -0
- package/src/server/aql/exec.ts +128 -0
- package/src/server/aql/index.ts +41 -0
- package/src/server/aql/schema/executors.test.ts +420 -0
- package/src/server/aql/schema/executors.ts +345 -0
- package/src/server/aql/schema/index.test.ts +67 -0
- package/src/server/aql/schema/index.ts +409 -0
- package/src/server/aql/schema-helpers.test.ts +242 -0
- package/src/server/aql/schema-helpers.ts +208 -0
- package/src/server/aql/views.test.ts +62 -0
- package/src/server/aql/views.ts +57 -0
- package/src/server/auth/app.ts +387 -0
- package/src/server/bench.ts +29 -0
- package/src/server/budget/actions.ts +686 -0
- package/src/server/budget/app.ts +469 -0
- package/src/server/budget/base.test.ts +340 -0
- package/src/server/budget/base.ts +339 -0
- package/src/server/budget/category-template-context.test.ts +1658 -0
- package/src/server/budget/category-template-context.ts +862 -0
- package/src/server/budget/cleanup-template.pegjs +27 -0
- package/src/server/budget/cleanup-template.ts +408 -0
- package/src/server/budget/envelope.ts +403 -0
- package/src/server/budget/goal-template.pegjs +110 -0
- package/src/server/budget/goal-template.ts +309 -0
- package/src/server/budget/report.ts +308 -0
- package/src/server/budget/schedule-template.test.ts +184 -0
- package/src/server/budget/schedule-template.ts +351 -0
- package/src/server/budget/statements.ts +60 -0
- package/src/server/budget/template-notes.test.ts +393 -0
- package/src/server/budget/template-notes.ts +323 -0
- package/src/server/budget/util.ts +25 -0
- package/src/server/budgetfiles/__snapshots__/backups.test.ts.snap +101 -0
- package/src/server/budgetfiles/app.ts +672 -0
- package/src/server/budgetfiles/backups.test.ts +79 -0
- package/src/server/budgetfiles/backups.ts +251 -0
- package/src/server/cloud-storage.ts +467 -0
- package/src/server/dashboard/app.ts +373 -0
- package/src/server/db/__snapshots__/index.test.ts.snap +271 -0
- package/src/server/db/index.test.ts +300 -0
- package/src/server/db/index.ts +855 -0
- package/src/server/db/mappings.ts +59 -0
- package/src/server/db/sort.ts +58 -0
- package/src/server/db/types/index.ts +342 -0
- package/src/server/db/util.ts +36 -0
- package/src/server/encryption/app.ts +133 -0
- package/src/server/encryption/encryption-internals.api.ts +2 -0
- package/src/server/encryption/encryption-internals.electron.ts +89 -0
- package/src/server/encryption/encryption-internals.ts +109 -0
- package/src/server/encryption/encryption.test.ts +19 -0
- package/src/server/encryption/index.test.ts +19 -0
- package/src/server/encryption/index.ts +89 -0
- package/src/server/errors.ts +110 -0
- package/src/server/filters/app.ts +191 -0
- package/src/server/importers/actual.ts +49 -0
- package/src/server/importers/index.ts +58 -0
- package/src/server/importers/ynab4-types.ts +163 -0
- package/src/server/importers/ynab4.ts +470 -0
- package/src/server/importers/ynab5-types.ts +290 -0
- package/src/server/importers/ynab5.ts +1193 -0
- package/src/server/main-app.ts +25 -0
- package/src/server/main.test.ts +392 -0
- package/src/server/main.ts +336 -0
- package/src/server/migrate/__snapshots__/migrations.test.ts.snap +17 -0
- package/src/server/migrate/cli.ts +100 -0
- package/src/server/migrate/migrations.test.ts +81 -0
- package/src/server/migrate/migrations.ts +192 -0
- package/src/server/models.ts +184 -0
- package/src/server/mutators.ts +139 -0
- package/src/server/notes/app.ts +18 -0
- package/src/server/payees/app.ts +351 -0
- package/src/server/polyfills.ts +26 -0
- package/src/server/post.ts +219 -0
- package/src/server/preferences/app.ts +249 -0
- package/src/server/prefs.ts +91 -0
- package/src/server/reports/app.ts +187 -0
- package/src/server/rules/action.ts +344 -0
- package/src/server/rules/app.ts +193 -0
- package/src/server/rules/condition.ts +436 -0
- package/src/server/rules/customFunctions.ts +61 -0
- package/src/server/rules/formula-action.test.ts +175 -0
- package/src/server/rules/handlebars-helpers.ts +131 -0
- package/src/server/rules/index.test.ts +1095 -0
- package/src/server/rules/index.ts +22 -0
- package/src/server/rules/rule-indexer.ts +89 -0
- package/src/server/rules/rule-utils.ts +274 -0
- package/src/server/rules/rule.ts +193 -0
- package/src/server/schedules/app.test.ts +502 -0
- package/src/server/schedules/app.ts +644 -0
- package/src/server/schedules/find-schedules.ts +391 -0
- package/src/server/server-config.ts +59 -0
- package/src/server/sheet.test.ts +101 -0
- package/src/server/sheet.ts +280 -0
- package/src/server/spreadsheet/__snapshots__/spreadsheet.test.ts.snap +5 -0
- package/src/server/spreadsheet/app.ts +54 -0
- package/src/server/spreadsheet/globals.ts +13 -0
- package/src/server/spreadsheet/graph-data-structure.ts +165 -0
- package/src/server/spreadsheet/scratch +60 -0
- package/src/server/spreadsheet/spreadsheet.test.ts +191 -0
- package/src/server/spreadsheet/spreadsheet.ts +523 -0
- package/src/server/spreadsheet/util.ts +15 -0
- package/src/server/sql/init.sql +88 -0
- package/src/server/sync/__snapshots__/sync.test.ts.snap +31 -0
- package/src/server/sync/app.ts +29 -0
- package/src/server/sync/encoder.ts +129 -0
- package/src/server/sync/index.ts +820 -0
- package/src/server/sync/make-test-message.ts +19 -0
- package/src/server/sync/migrate.test.ts +169 -0
- package/src/server/sync/migrate.ts +48 -0
- package/src/server/sync/repair.ts +39 -0
- package/src/server/sync/reset.ts +91 -0
- package/src/server/sync/sync.property.test.ts +385 -0
- package/src/server/sync/sync.test.ts +349 -0
- package/src/server/sync/utils.ts +3 -0
- package/src/server/tags/app.ts +101 -0
- package/src/server/tests/mockData.json +9352 -0
- package/src/server/tests/mockSyncServer.ts +119 -0
- package/src/server/tools/app.ts +152 -0
- package/src/server/transactions/__snapshots__/transaction-rules.test.ts.snap +173 -0
- package/src/server/transactions/__snapshots__/transfer.test.ts.snap +655 -0
- package/src/server/transactions/app.ts +136 -0
- package/src/server/transactions/export/export-to-csv.ts +132 -0
- package/src/server/transactions/import/__snapshots__/parse-file.test.ts.snap +1582 -0
- package/src/server/transactions/import/ofx2json.test.ts +33 -0
- package/src/server/transactions/import/ofx2json.ts +157 -0
- package/src/server/transactions/import/parse-file.test.ts +224 -0
- package/src/server/transactions/import/parse-file.ts +286 -0
- package/src/server/transactions/import/qif2json.ts +110 -0
- package/src/server/transactions/import/xmlcamt2json.ts +168 -0
- package/src/server/transactions/index.ts +196 -0
- package/src/server/transactions/merge.test.ts +370 -0
- package/src/server/transactions/merge.ts +139 -0
- package/src/server/transactions/transaction-rules.test.ts +994 -0
- package/src/server/transactions/transaction-rules.ts +1038 -0
- package/src/server/transactions/transfer.test.ts +221 -0
- package/src/server/transactions/transfer.ts +173 -0
- package/src/server/undo.ts +271 -0
- package/src/server/update.ts +37 -0
- package/src/server/util/budget-name.ts +61 -0
- package/src/server/util/custom-sync-mapping.ts +48 -0
- package/src/server/util/rschedule.ts +9 -0
- package/src/shared/__mocks__/platform.ts +7 -0
- package/src/shared/__snapshots__/months.test.ts.snap +21 -0
- package/src/shared/arithmetic.test.ts +112 -0
- package/src/shared/arithmetic.ts +170 -0
- package/src/shared/async.test.ts +135 -0
- package/src/shared/async.ts +76 -0
- package/src/shared/constants.ts +5 -0
- package/src/shared/currencies.ts +70 -0
- package/src/shared/dashboard.ts +260 -0
- package/src/shared/environment.ts +18 -0
- package/src/shared/errors.ts +195 -0
- package/src/shared/locale.ts +27 -0
- package/src/shared/location-utils.test.ts +69 -0
- package/src/shared/location-utils.ts +49 -0
- package/src/shared/months.test.ts +5 -0
- package/src/shared/months.ts +485 -0
- package/src/shared/normalisation.ts +6 -0
- package/src/shared/platform.electron.ts +21 -0
- package/src/shared/platform.ts +20 -0
- package/src/shared/query.ts +176 -0
- package/src/shared/rules.test.ts +56 -0
- package/src/shared/rules.ts +371 -0
- package/src/shared/schedules.test.ts +570 -0
- package/src/shared/schedules.ts +560 -0
- package/src/shared/test-helpers.ts +156 -0
- package/src/shared/transactions.test.ts +275 -0
- package/src/shared/transactions.ts +433 -0
- package/src/shared/transfer.test.ts +75 -0
- package/src/shared/transfer.ts +16 -0
- package/src/shared/user.ts +4 -0
- package/src/shared/util.test.ts +240 -0
- package/src/shared/util.ts +633 -0
- package/src/types/api-handlers.ts +287 -0
- package/src/types/budget.ts +8 -0
- package/src/types/file.ts +47 -0
- package/src/types/handlers.ts +46 -0
- package/src/types/models/account.ts +24 -0
- package/src/types/models/bank-sync.ts +23 -0
- package/src/types/models/bank.ts +6 -0
- package/src/types/models/category-group.ts +11 -0
- package/src/types/models/category.ts +13 -0
- package/src/types/models/dashboard.ts +199 -0
- package/src/types/models/gocardless.ts +84 -0
- package/src/types/models/import-transaction.ts +56 -0
- package/src/types/models/index.ts +23 -0
- package/src/types/models/nearby-payee.ts +7 -0
- package/src/types/models/note.ts +4 -0
- package/src/types/models/openid.ts +8 -0
- package/src/types/models/payee-location.ts +8 -0
- package/src/types/models/payee.ts +10 -0
- package/src/types/models/pluggyai.ts +19 -0
- package/src/types/models/reports.ts +144 -0
- package/src/types/models/rule.ts +174 -0
- package/src/types/models/schedule.ts +49 -0
- package/src/types/models/simplefin.ts +28 -0
- package/src/types/models/tags.ts +6 -0
- package/src/types/models/templates.ts +135 -0
- package/src/types/models/transaction-filter.ts +9 -0
- package/src/types/models/transaction.ts +39 -0
- package/src/types/models/user-access.ts +10 -0
- package/src/types/models/user.ts +25 -0
- package/src/types/prefs.ts +167 -0
- package/src/types/server-events.ts +86 -0
- package/src/types/server-handlers.ts +27 -0
- package/src/types/util.ts +26 -0
- package/tsconfig.json +34 -0
- package/typings/pegjs.ts +1 -0
- package/typings/process-worker.ts +12 -0
- package/typings/vite-plugin-peggy-loader.ts +1 -0
- package/typings/window.ts +62 -0
- package/vite.config.ts +109 -0
- package/vite.desktop.config.ts +59 -0
- package/vitest.config.ts +43 -0
- package/vitest.web.config.ts +38 -0
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
// @ts-strict-ignore
|
|
2
|
+
import {
|
|
3
|
+
deserializeClock,
|
|
4
|
+
makeClientId,
|
|
5
|
+
makeClock,
|
|
6
|
+
serializeClock,
|
|
7
|
+
setClock,
|
|
8
|
+
Timestamp,
|
|
9
|
+
} from '@actual-app/crdt';
|
|
10
|
+
import type { Database, Statement } from '@jlongster/sql.js';
|
|
11
|
+
import { LRUCache } from 'lru-cache';
|
|
12
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
13
|
+
|
|
14
|
+
import * as fs from '../../platform/server/fs';
|
|
15
|
+
import * as sqlite from '../../platform/server/sqlite';
|
|
16
|
+
import * as monthUtils from '../../shared/months';
|
|
17
|
+
import { groupById } from '../../shared/util';
|
|
18
|
+
import type { TransactionEntity } from '../../types/models';
|
|
19
|
+
import type { WithRequired } from '../../types/util';
|
|
20
|
+
import {
|
|
21
|
+
convertForInsert,
|
|
22
|
+
convertForUpdate,
|
|
23
|
+
convertFromSelect,
|
|
24
|
+
schema,
|
|
25
|
+
schemaConfig,
|
|
26
|
+
} from '../aql';
|
|
27
|
+
import {
|
|
28
|
+
accountModel,
|
|
29
|
+
categoryGroupModel,
|
|
30
|
+
categoryModel,
|
|
31
|
+
payeeModel,
|
|
32
|
+
toDateRepr,
|
|
33
|
+
} from '../models';
|
|
34
|
+
import { batchMessages, sendMessages } from '../sync';
|
|
35
|
+
|
|
36
|
+
import { shoveSortOrders, SORT_INCREMENT } from './sort';
|
|
37
|
+
import type {
|
|
38
|
+
DbAccount,
|
|
39
|
+
DbBank,
|
|
40
|
+
DbCategory,
|
|
41
|
+
DbCategoryGroup,
|
|
42
|
+
DbCategoryMapping,
|
|
43
|
+
DbClockMessage,
|
|
44
|
+
DbPayee,
|
|
45
|
+
DbPayeeMapping,
|
|
46
|
+
DbTag,
|
|
47
|
+
DbTransaction,
|
|
48
|
+
DbViewTransaction,
|
|
49
|
+
DbViewTransactionInternalAlive,
|
|
50
|
+
} from './types';
|
|
51
|
+
|
|
52
|
+
export * from './types';
|
|
53
|
+
|
|
54
|
+
export { toDateRepr, fromDateRepr } from '../models';
|
|
55
|
+
|
|
56
|
+
let dbPath: string | null = null;
|
|
57
|
+
let db: Database | null = null;
|
|
58
|
+
|
|
59
|
+
// Util
|
|
60
|
+
|
|
61
|
+
export function getDatabasePath() {
|
|
62
|
+
return dbPath;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function openDatabase(id?: string) {
|
|
66
|
+
if (db) {
|
|
67
|
+
sqlite.closeDatabase(db);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
dbPath = fs.join(fs.getBudgetDir(id), 'db.sqlite');
|
|
71
|
+
setDatabase(await sqlite.openDatabase(dbPath));
|
|
72
|
+
|
|
73
|
+
// await execQuery('PRAGMA journal_mode = WAL');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function closeDatabase() {
|
|
77
|
+
if (db) {
|
|
78
|
+
sqlite.closeDatabase(db);
|
|
79
|
+
setDatabase(null);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function setDatabase(db_: Database) {
|
|
84
|
+
db = db_;
|
|
85
|
+
resetQueryCache();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getDatabase() {
|
|
89
|
+
return db;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function loadClock() {
|
|
93
|
+
const row = await first<DbClockMessage>('SELECT * FROM messages_clock');
|
|
94
|
+
if (row) {
|
|
95
|
+
const clock = deserializeClock(row.clock);
|
|
96
|
+
setClock(clock);
|
|
97
|
+
} else {
|
|
98
|
+
// No clock exists yet (first run of the app), so create a default
|
|
99
|
+
// one.
|
|
100
|
+
const timestamp = new Timestamp(0, 0, makeClientId());
|
|
101
|
+
const clock = makeClock(timestamp);
|
|
102
|
+
setClock(clock);
|
|
103
|
+
|
|
104
|
+
runQuery('INSERT INTO messages_clock (id, clock) VALUES (?, ?)', [
|
|
105
|
+
1,
|
|
106
|
+
serializeClock(clock),
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Functions
|
|
112
|
+
export function runQuery(
|
|
113
|
+
sql: string | Statement,
|
|
114
|
+
params?: Array<string | number>,
|
|
115
|
+
fetchAll?: false,
|
|
116
|
+
): { changes: unknown };
|
|
117
|
+
|
|
118
|
+
export function runQuery<T>(
|
|
119
|
+
sql: string | Statement,
|
|
120
|
+
params: Array<string | number> | undefined,
|
|
121
|
+
fetchAll: true,
|
|
122
|
+
): T[];
|
|
123
|
+
|
|
124
|
+
export function runQuery<T>(
|
|
125
|
+
sql: string | Statement,
|
|
126
|
+
params: (string | number)[],
|
|
127
|
+
fetchAll: boolean,
|
|
128
|
+
) {
|
|
129
|
+
if (fetchAll) {
|
|
130
|
+
return sqlite.runQuery<T>(db, sql, params, true);
|
|
131
|
+
} else {
|
|
132
|
+
return sqlite.runQuery(db, sql, params, false);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function execQuery(sql: string) {
|
|
137
|
+
sqlite.execQuery(db, sql);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// This manages an LRU cache of prepared query statements. This is
|
|
141
|
+
// only needed in hot spots when you are running lots of queries.
|
|
142
|
+
let _queryCache = new LRUCache<string, Statement>({ max: 100 });
|
|
143
|
+
export function cache(sql: string) {
|
|
144
|
+
const cached = _queryCache.get(sql);
|
|
145
|
+
if (cached) {
|
|
146
|
+
return cached;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const prepared = sqlite.prepare(db, sql);
|
|
150
|
+
_queryCache.set(sql, prepared);
|
|
151
|
+
return prepared;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function resetQueryCache() {
|
|
155
|
+
_queryCache = new LRUCache<string, Statement>({ max: 100 });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function transaction(fn: () => void) {
|
|
159
|
+
return sqlite.transaction(db, fn);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function asyncTransaction(fn: () => Promise<void>) {
|
|
163
|
+
return sqlite.asyncTransaction(db, fn);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// This function is marked as async because `runQuery` is no longer
|
|
167
|
+
// async. We return a promise here until we've audited all the code to
|
|
168
|
+
// make sure nothing calls `.then` on this.
|
|
169
|
+
export async function all<T>(sql: string, params?: (string | number)[]) {
|
|
170
|
+
return runQuery<T>(sql, params, true);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function first<T>(sql, params?: (string | number)[]) {
|
|
174
|
+
const arr = runQuery<T>(sql, params, true);
|
|
175
|
+
return arr.length === 0 ? null : arr[0];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// The underlying sql system is now sync, but we can't update `first` yet
|
|
179
|
+
// without auditing all uses of it
|
|
180
|
+
export function firstSync<T>(sql, params?: (string | number)[]) {
|
|
181
|
+
const arr = runQuery<T>(sql, params, true);
|
|
182
|
+
return arr.length === 0 ? null : arr[0];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// This function is marked as async because `runQuery` is no longer
|
|
186
|
+
// async. We return a promise here until we've audited all the code to
|
|
187
|
+
// make sure nothing calls `.then` on this.
|
|
188
|
+
export async function run(sql, params?: (string | number)[]) {
|
|
189
|
+
return runQuery(sql, params);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function select(table, id) {
|
|
193
|
+
const rows = runQuery('SELECT * FROM ' + table + ' WHERE id = ?', [id], true);
|
|
194
|
+
// TODO: In the next phase, we will make this function generic
|
|
195
|
+
// and pass the type of the return type to `runQuery`.
|
|
196
|
+
// oxlint-disable-next-line typescript/no-explicit-any
|
|
197
|
+
return rows[0] as any;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function update(table, params) {
|
|
201
|
+
const fields = Object.keys(params).filter(k => k !== 'id');
|
|
202
|
+
|
|
203
|
+
if (params.id == null) {
|
|
204
|
+
throw new Error('update: id is required');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await sendMessages(
|
|
208
|
+
fields.map(k => {
|
|
209
|
+
return {
|
|
210
|
+
dataset: table,
|
|
211
|
+
row: params.id,
|
|
212
|
+
column: k,
|
|
213
|
+
value: params[k],
|
|
214
|
+
timestamp: Timestamp.send(),
|
|
215
|
+
};
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function insertWithUUID(table, row) {
|
|
221
|
+
if (!row.id) {
|
|
222
|
+
row = { ...row, id: uuidv4() };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
await insert(table, row);
|
|
226
|
+
|
|
227
|
+
// We can't rely on the return value of insert because if the
|
|
228
|
+
// primary key is text, sqlite returns the internal row id which we
|
|
229
|
+
// don't care about. We want to return the generated UUID.
|
|
230
|
+
return row.id;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function insert(table, row) {
|
|
234
|
+
const fields = Object.keys(row).filter(k => k !== 'id');
|
|
235
|
+
|
|
236
|
+
if (row.id == null) {
|
|
237
|
+
throw new Error('insert: id is required');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await sendMessages(
|
|
241
|
+
fields.map(k => {
|
|
242
|
+
return {
|
|
243
|
+
dataset: table,
|
|
244
|
+
row: row.id,
|
|
245
|
+
column: k,
|
|
246
|
+
value: row[k],
|
|
247
|
+
timestamp: Timestamp.send(),
|
|
248
|
+
};
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function delete_(table, id) {
|
|
254
|
+
await sendMessages([
|
|
255
|
+
{
|
|
256
|
+
dataset: table,
|
|
257
|
+
row: id,
|
|
258
|
+
column: 'tombstone',
|
|
259
|
+
value: 1,
|
|
260
|
+
timestamp: Timestamp.send(),
|
|
261
|
+
},
|
|
262
|
+
]);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function deleteAll(table: string) {
|
|
266
|
+
const rows = await all<{ id: string }>(`
|
|
267
|
+
SELECT id FROM ${table} WHERE tombstone = 0
|
|
268
|
+
`);
|
|
269
|
+
await Promise.all(rows.map(({ id }) => delete_(table, id)));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function selectWithSchema(table, sql, params) {
|
|
273
|
+
const rows = runQuery(sql, params, true);
|
|
274
|
+
const convertedRows = rows
|
|
275
|
+
.map(row => convertFromSelect(schema, schemaConfig, table, row))
|
|
276
|
+
.filter(Boolean);
|
|
277
|
+
// TODO: Make convertFromSelect generic so we don't need this cast
|
|
278
|
+
// oxlint-disable-next-line typescript/no-explicit-any
|
|
279
|
+
return convertedRows as any[];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function selectFirstWithSchema(table, sql, params) {
|
|
283
|
+
const rows = await selectWithSchema(table, sql, params);
|
|
284
|
+
return rows.length > 0 ? rows[0] : null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function insertWithSchema(table, row) {
|
|
288
|
+
// Even though `insertWithUUID` does this, we need to do it here so
|
|
289
|
+
// the schema validation passes
|
|
290
|
+
if (!row.id) {
|
|
291
|
+
row = { ...row, id: uuidv4() };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return insertWithUUID(
|
|
295
|
+
table,
|
|
296
|
+
convertForInsert(schema, schemaConfig, table, row),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function updateWithSchema(table, fields) {
|
|
301
|
+
return update(table, convertForUpdate(schema, schemaConfig, table, fields));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Data-specific functions. Ideally this would be split up into
|
|
305
|
+
// different files
|
|
306
|
+
|
|
307
|
+
export async function getCategories(
|
|
308
|
+
ids?: Array<DbCategory['id']>,
|
|
309
|
+
): Promise<DbCategory[]> {
|
|
310
|
+
const whereIn = ids ? `c.id IN (${toSqlQueryParameters(ids)}) AND` : '';
|
|
311
|
+
const query = `SELECT c.* FROM categories c WHERE ${whereIn} c.tombstone = 0 ORDER BY c.sort_order, c.id`;
|
|
312
|
+
return ids
|
|
313
|
+
? await all<DbCategory>(query, [...ids])
|
|
314
|
+
: await all<DbCategory>(query);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function getCategoriesGrouped(
|
|
318
|
+
ids?: Array<DbCategoryGroup['id']>,
|
|
319
|
+
): Promise<
|
|
320
|
+
Array<
|
|
321
|
+
DbCategoryGroup & {
|
|
322
|
+
categories: DbCategory[];
|
|
323
|
+
}
|
|
324
|
+
>
|
|
325
|
+
> {
|
|
326
|
+
const categoryGroupWhereIn = ids
|
|
327
|
+
? `cg.id IN (${toSqlQueryParameters(ids)}) AND`
|
|
328
|
+
: '';
|
|
329
|
+
const categoryGroupQuery = `SELECT cg.* FROM category_groups cg WHERE ${categoryGroupWhereIn} cg.tombstone = 0
|
|
330
|
+
ORDER BY cg.is_income, cg.sort_order, cg.id`;
|
|
331
|
+
|
|
332
|
+
const categoryWhereIn = ids
|
|
333
|
+
? `c.cat_group IN (${toSqlQueryParameters(ids)}) AND`
|
|
334
|
+
: '';
|
|
335
|
+
const categoryQuery = `SELECT c.* FROM categories c WHERE ${categoryWhereIn} c.tombstone = 0
|
|
336
|
+
ORDER BY c.sort_order, c.id`;
|
|
337
|
+
|
|
338
|
+
const groups = ids
|
|
339
|
+
? await all<DbCategoryGroup>(categoryGroupQuery, [...ids])
|
|
340
|
+
: await all<DbCategoryGroup>(categoryGroupQuery);
|
|
341
|
+
|
|
342
|
+
const categories = ids
|
|
343
|
+
? await all<DbCategory>(categoryQuery, [...ids])
|
|
344
|
+
: await all<DbCategory>(categoryQuery);
|
|
345
|
+
|
|
346
|
+
return groups.map(group => ({
|
|
347
|
+
...group,
|
|
348
|
+
categories: categories.filter(c => c.cat_group === group.id),
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export async function insertCategoryGroup(
|
|
353
|
+
group: WithRequired<Partial<DbCategoryGroup>, 'name'>,
|
|
354
|
+
): Promise<DbCategoryGroup['id']> {
|
|
355
|
+
// Don't allow duplicate group
|
|
356
|
+
const existingGroup = await first<
|
|
357
|
+
Pick<DbCategoryGroup, 'id' | 'name' | 'hidden'>
|
|
358
|
+
>(
|
|
359
|
+
`SELECT id, name, hidden FROM category_groups WHERE UPPER(name) = ? and tombstone = 0 LIMIT 1`,
|
|
360
|
+
[group.name.toUpperCase()],
|
|
361
|
+
);
|
|
362
|
+
if (existingGroup) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`A ${existingGroup.hidden ? 'hidden ' : ''}'${existingGroup.name}' category group already exists.`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const lastGroup = await first<Pick<DbCategoryGroup, 'sort_order'>>(`
|
|
369
|
+
SELECT sort_order FROM category_groups WHERE tombstone = 0 ORDER BY sort_order DESC, id DESC LIMIT 1
|
|
370
|
+
`);
|
|
371
|
+
const sort_order = (lastGroup ? lastGroup.sort_order : 0) + SORT_INCREMENT;
|
|
372
|
+
|
|
373
|
+
group = {
|
|
374
|
+
...categoryGroupModel.validate(group),
|
|
375
|
+
sort_order,
|
|
376
|
+
};
|
|
377
|
+
const id: DbCategoryGroup['id'] = await insertWithUUID(
|
|
378
|
+
'category_groups',
|
|
379
|
+
group,
|
|
380
|
+
);
|
|
381
|
+
return id;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function updateCategoryGroup(
|
|
385
|
+
group: WithRequired<Partial<DbCategoryGroup>, 'id' | 'name' | 'is_income'>,
|
|
386
|
+
) {
|
|
387
|
+
const existingGroup = await first<
|
|
388
|
+
Pick<DbCategoryGroup, 'id' | 'name' | 'hidden'>
|
|
389
|
+
>(
|
|
390
|
+
`SELECT id, name, hidden FROM category_groups WHERE UPPER(name) = ? AND id != ? AND tombstone = 0 LIMIT 1`,
|
|
391
|
+
[group.name.toUpperCase(), group.id],
|
|
392
|
+
);
|
|
393
|
+
if (existingGroup) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
`A ${existingGroup.hidden ? 'hidden ' : ''}'${existingGroup.name}' category group already exists.`,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
group = categoryGroupModel.validate(group, { update: true });
|
|
399
|
+
return update('category_groups', group);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function moveCategoryGroup(
|
|
403
|
+
id: DbCategoryGroup['id'],
|
|
404
|
+
targetId?: DbCategoryGroup['id'] | null,
|
|
405
|
+
) {
|
|
406
|
+
const groups = await all<Pick<DbCategoryGroup, 'id' | 'sort_order'>>(
|
|
407
|
+
`SELECT id, sort_order FROM category_groups WHERE tombstone = 0 ORDER BY sort_order, id`,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const { updates, sort_order } = shoveSortOrders(groups, targetId);
|
|
411
|
+
for (const info of updates) {
|
|
412
|
+
await update('category_groups', info);
|
|
413
|
+
}
|
|
414
|
+
await update('category_groups', { id, sort_order });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export async function deleteCategoryGroup(
|
|
418
|
+
group: Pick<DbCategoryGroup, 'id'>,
|
|
419
|
+
transferId?: DbCategory['id'] | null,
|
|
420
|
+
) {
|
|
421
|
+
const categories = await all<DbCategory>(
|
|
422
|
+
'SELECT * FROM categories WHERE cat_group = ?',
|
|
423
|
+
[group.id],
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
// Delete all the categories within a group
|
|
427
|
+
await Promise.all(categories.map(cat => deleteCategory(cat, transferId)));
|
|
428
|
+
await delete_('category_groups', group.id);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export async function insertCategory(
|
|
432
|
+
category: WithRequired<Partial<DbCategory>, 'name' | 'cat_group'>,
|
|
433
|
+
{ atEnd }: { atEnd?: boolean | undefined } = { atEnd: undefined },
|
|
434
|
+
): Promise<DbCategory['id']> {
|
|
435
|
+
let sort_order;
|
|
436
|
+
|
|
437
|
+
let id_: DbCategory['id'];
|
|
438
|
+
await batchMessages(async () => {
|
|
439
|
+
// Dont allow duplicated names in groups
|
|
440
|
+
const existingCatInGroup = await first<Pick<DbCategory, 'id'>>(
|
|
441
|
+
`SELECT id FROM categories WHERE cat_group = ? and UPPER(name) = ? and tombstone = 0 LIMIT 1`,
|
|
442
|
+
[category.cat_group, category.name.toUpperCase()],
|
|
443
|
+
);
|
|
444
|
+
if (existingCatInGroup) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
`Category '${category.name}' already exists in group '${category.cat_group}'`,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (atEnd) {
|
|
451
|
+
const lastCat = await first<Pick<DbCategory, 'sort_order'>>(`
|
|
452
|
+
SELECT sort_order FROM categories WHERE tombstone = 0 ORDER BY sort_order DESC, id DESC LIMIT 1
|
|
453
|
+
`);
|
|
454
|
+
sort_order = (lastCat ? lastCat.sort_order : 0) + SORT_INCREMENT;
|
|
455
|
+
} else {
|
|
456
|
+
// Unfortunately since we insert at the beginning, we need to shove
|
|
457
|
+
// the sort orders to make sure there's room for it
|
|
458
|
+
const categories = await all<Pick<DbCategory, 'id' | 'sort_order'>>(
|
|
459
|
+
`SELECT id, sort_order FROM categories WHERE cat_group = ? AND tombstone = 0 ORDER BY sort_order, id`,
|
|
460
|
+
[category.cat_group],
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
const { updates, sort_order: order } = shoveSortOrders(
|
|
464
|
+
categories,
|
|
465
|
+
categories.length > 0 ? categories[0].id : null,
|
|
466
|
+
);
|
|
467
|
+
for (const info of updates) {
|
|
468
|
+
await update('categories', info);
|
|
469
|
+
}
|
|
470
|
+
sort_order = order;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
category = {
|
|
474
|
+
...categoryModel.validate(category),
|
|
475
|
+
sort_order,
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const id = await insertWithUUID('categories', category);
|
|
479
|
+
// Create an entry in the mapping table that points it to itself
|
|
480
|
+
await insert('category_mapping', { id, transferId: id });
|
|
481
|
+
id_ = id;
|
|
482
|
+
});
|
|
483
|
+
return id_;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export function updateCategory(
|
|
487
|
+
category: WithRequired<
|
|
488
|
+
Partial<DbCategory>,
|
|
489
|
+
'name' | 'is_income' | 'cat_group'
|
|
490
|
+
>,
|
|
491
|
+
) {
|
|
492
|
+
category = categoryModel.validate(category, { update: true });
|
|
493
|
+
// Change from cat_group to group because category AQL schema named it group.
|
|
494
|
+
// const { cat_group: group, ...rest } = category;
|
|
495
|
+
return update('categories', category);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export async function moveCategory(
|
|
499
|
+
id: DbCategory['id'],
|
|
500
|
+
groupId: DbCategoryGroup['id'],
|
|
501
|
+
targetId?: DbCategory['id'] | null,
|
|
502
|
+
) {
|
|
503
|
+
if (!groupId) {
|
|
504
|
+
throw new Error('moveCategory: groupId is required');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const categories = await all<Pick<DbCategory, 'id' | 'sort_order'>>(
|
|
508
|
+
`SELECT id, sort_order FROM categories WHERE cat_group = ? AND tombstone = 0 ORDER BY sort_order, id`,
|
|
509
|
+
[groupId],
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
const { updates, sort_order } = shoveSortOrders(categories, targetId);
|
|
513
|
+
for (const info of updates) {
|
|
514
|
+
await update('categories', info);
|
|
515
|
+
}
|
|
516
|
+
await update('categories', { id, sort_order, cat_group: groupId });
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export async function deleteCategory(
|
|
520
|
+
category: Pick<DbCategory, 'id'>,
|
|
521
|
+
transferId?: DbCategory['id'] | null,
|
|
522
|
+
) {
|
|
523
|
+
if (transferId) {
|
|
524
|
+
// We need to update all the deleted categories that currently
|
|
525
|
+
// point to the one we're about to delete so they all are
|
|
526
|
+
// "forwarded" to the new transferred category.
|
|
527
|
+
const existingTransfers = await all<DbCategoryMapping>(
|
|
528
|
+
'SELECT * FROM category_mapping WHERE transferId = ?',
|
|
529
|
+
[category.id],
|
|
530
|
+
);
|
|
531
|
+
for (const mapping of existingTransfers) {
|
|
532
|
+
await update('category_mapping', {
|
|
533
|
+
id: mapping.id,
|
|
534
|
+
transferId,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Finally, map the category we're about to delete to the new one
|
|
539
|
+
await update('category_mapping', { id: category.id, transferId });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return delete_('categories', category.id);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export async function getPayee(id: DbPayee['id']) {
|
|
546
|
+
return first<DbPayee>(`SELECT * FROM payees WHERE id = ?`, [id]);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export async function getAccount(id: DbAccount['id']) {
|
|
550
|
+
return first<DbAccount>(`SELECT * FROM accounts WHERE id = ?`, [id]);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export async function getCategory(id: DbCategory['id']) {
|
|
554
|
+
return first<DbCategory>(`SELECT * FROM categories WHERE id = ?`, [id]);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export async function insertPayee(
|
|
558
|
+
payee: WithRequired<Partial<DbPayee>, 'name'>,
|
|
559
|
+
) {
|
|
560
|
+
payee = payeeModel.validate(payee);
|
|
561
|
+
let id: DbPayee['id'];
|
|
562
|
+
await batchMessages(async () => {
|
|
563
|
+
id = await insertWithUUID('payees', payee);
|
|
564
|
+
await insert('payee_mapping', { id, targetId: id });
|
|
565
|
+
});
|
|
566
|
+
return id;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export async function deletePayee(payee: Pick<DbPayee, 'id'>) {
|
|
570
|
+
const { transfer_acct } = await first<DbPayee>(
|
|
571
|
+
'SELECT * FROM payees WHERE id = ?',
|
|
572
|
+
[payee.id],
|
|
573
|
+
);
|
|
574
|
+
if (transfer_acct) {
|
|
575
|
+
// You should never be able to delete transfer payees
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// let mappings = await all('SELECT id FROM payee_mapping WHERE targetId = ?', [
|
|
580
|
+
// payee.id
|
|
581
|
+
// ]);
|
|
582
|
+
// await Promise.all(
|
|
583
|
+
// mappings.map(m => update('payee_mapping', { id: m.id, targetId: null }))
|
|
584
|
+
// );
|
|
585
|
+
|
|
586
|
+
return delete_('payees', payee.id);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export async function deleteTransferPayee(payee: Pick<DbPayee, 'id'>) {
|
|
590
|
+
// This allows deleting transfer payees
|
|
591
|
+
return delete_('payees', payee.id);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export function updatePayee(payee: WithRequired<Partial<DbPayee>, 'id'>) {
|
|
595
|
+
payee = payeeModel.validate(payee, { update: true });
|
|
596
|
+
return update('payees', payee);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export async function mergePayees(
|
|
600
|
+
target: DbPayee['id'],
|
|
601
|
+
ids: Array<DbPayee['id']>,
|
|
602
|
+
) {
|
|
603
|
+
// Load in payees so we can check some stuff
|
|
604
|
+
const dbPayees: DbPayee[] = await all<DbPayee>('SELECT * FROM payees');
|
|
605
|
+
const payees = groupById(dbPayees);
|
|
606
|
+
|
|
607
|
+
// Filter out any transfer payees
|
|
608
|
+
if (payees[target].transfer_acct != null) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
ids = ids.filter(id => payees[id].transfer_acct == null);
|
|
612
|
+
|
|
613
|
+
await batchMessages(async () => {
|
|
614
|
+
await Promise.all(
|
|
615
|
+
ids.map(async id => {
|
|
616
|
+
const mappings = await all<DbPayeeMapping>(
|
|
617
|
+
'SELECT id FROM payee_mapping WHERE targetId = ?',
|
|
618
|
+
[id],
|
|
619
|
+
);
|
|
620
|
+
await Promise.all(
|
|
621
|
+
mappings.map(m =>
|
|
622
|
+
update('payee_mapping', { id: m.id, targetId: target }),
|
|
623
|
+
),
|
|
624
|
+
);
|
|
625
|
+
}),
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
await Promise.all(
|
|
629
|
+
ids.map(id =>
|
|
630
|
+
Promise.all([
|
|
631
|
+
update('payee_mapping', { id, targetId: target }),
|
|
632
|
+
delete_('payees', id),
|
|
633
|
+
]),
|
|
634
|
+
),
|
|
635
|
+
);
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export function getPayees() {
|
|
640
|
+
return all<DbPayee & { name: DbAccount['name'] | DbPayee['name'] }>(`
|
|
641
|
+
SELECT p.*, COALESCE(a.name, p.name) AS name FROM payees p
|
|
642
|
+
LEFT JOIN accounts a ON (p.transfer_acct = a.id AND a.tombstone = 0)
|
|
643
|
+
WHERE p.tombstone = 0 AND (p.transfer_acct IS NULL OR a.id IS NOT NULL)
|
|
644
|
+
ORDER BY p.transfer_acct IS NULL DESC, p.name COLLATE NOCASE, a.offbudget, a.sort_order
|
|
645
|
+
`);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
export function getCommonPayees() {
|
|
649
|
+
const twelveWeeksAgo = toDateRepr(
|
|
650
|
+
monthUtils.subWeeks(monthUtils.currentDate(), 12),
|
|
651
|
+
);
|
|
652
|
+
const limit = 10;
|
|
653
|
+
return all<
|
|
654
|
+
DbPayee & {
|
|
655
|
+
common: true;
|
|
656
|
+
transfer_acct: null;
|
|
657
|
+
c: number;
|
|
658
|
+
latest: DbViewTransactionInternalAlive['date'];
|
|
659
|
+
}
|
|
660
|
+
>(`
|
|
661
|
+
SELECT p.id as id, p.name as name, p.favorite as favorite,
|
|
662
|
+
p.category as category, TRUE as common, NULL as transfer_acct,
|
|
663
|
+
count(*) as c,
|
|
664
|
+
max(t.date) as latest
|
|
665
|
+
FROM payees p
|
|
666
|
+
LEFT JOIN v_transactions_internal_alive t on t.payee == p.id
|
|
667
|
+
WHERE LENGTH(p.name) > 0
|
|
668
|
+
AND p.tombstone = 0
|
|
669
|
+
AND t.date > ${twelveWeeksAgo}
|
|
670
|
+
GROUP BY p.id
|
|
671
|
+
ORDER BY c DESC ,p.transfer_acct IS NULL DESC, p.name
|
|
672
|
+
COLLATE NOCASE
|
|
673
|
+
LIMIT ${limit}
|
|
674
|
+
`);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const orphanedPayeesQuery = `
|
|
678
|
+
SELECT p.id
|
|
679
|
+
FROM payees p
|
|
680
|
+
LEFT JOIN payee_mapping pm ON pm.id = p.id
|
|
681
|
+
LEFT JOIN v_transactions_internal_alive t ON t.payee = pm.targetId
|
|
682
|
+
WHERE p.tombstone = 0
|
|
683
|
+
AND p.transfer_acct IS NULL
|
|
684
|
+
AND t.id IS NULL
|
|
685
|
+
AND NOT EXISTS (
|
|
686
|
+
SELECT 1
|
|
687
|
+
FROM rules r,
|
|
688
|
+
json_each(r.conditions) as cond
|
|
689
|
+
WHERE r.tombstone = 0
|
|
690
|
+
AND json_extract(cond.value, '$.field') = 'description'
|
|
691
|
+
AND json_extract(cond.value, '$.value') = pm.targetId
|
|
692
|
+
);
|
|
693
|
+
`;
|
|
694
|
+
|
|
695
|
+
export function syncGetOrphanedPayees() {
|
|
696
|
+
return all<Pick<DbPayee, 'id'>>(orphanedPayeesQuery);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export async function getOrphanedPayees() {
|
|
700
|
+
const rows = await all<Pick<DbPayee, 'id'>>(orphanedPayeesQuery);
|
|
701
|
+
return rows.map(row => row.id);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export async function getPayeeByName(name: DbPayee['name']) {
|
|
705
|
+
return first<DbPayee>(
|
|
706
|
+
`SELECT * FROM payees WHERE UNICODE_LOWER(name) = ? AND tombstone = 0`,
|
|
707
|
+
[name.toLowerCase()],
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export function getAccounts() {
|
|
712
|
+
return all<
|
|
713
|
+
DbAccount & {
|
|
714
|
+
bankName: DbBank['name'];
|
|
715
|
+
bankId: DbBank['id'];
|
|
716
|
+
}
|
|
717
|
+
>(
|
|
718
|
+
`SELECT a.*, b.name as bankName, b.id as bankId FROM accounts a
|
|
719
|
+
LEFT JOIN banks b ON a.bank = b.id
|
|
720
|
+
WHERE a.tombstone = 0
|
|
721
|
+
ORDER BY sort_order, name`,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export async function insertAccount(account) {
|
|
726
|
+
const accounts = await all<DbAccount>(
|
|
727
|
+
'SELECT * FROM accounts WHERE offbudget = ? ORDER BY sort_order, name',
|
|
728
|
+
[account.offbudget ? 1 : 0],
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
// Don't pass a target in, it will default to appending at the end
|
|
732
|
+
const { sort_order } = shoveSortOrders(accounts);
|
|
733
|
+
|
|
734
|
+
account = accountModel.validate({ ...account, sort_order });
|
|
735
|
+
return insertWithUUID('accounts', account);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
export function updateAccount(account) {
|
|
739
|
+
account = accountModel.validate(account, { update: true });
|
|
740
|
+
return update('accounts', account);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export function deleteAccount(account) {
|
|
744
|
+
return delete_('accounts', account.id);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export async function moveAccount(
|
|
748
|
+
id: DbAccount['id'],
|
|
749
|
+
targetId: DbAccount['id'] | null,
|
|
750
|
+
) {
|
|
751
|
+
const account = await first<DbAccount>(
|
|
752
|
+
'SELECT * FROM accounts WHERE id = ?',
|
|
753
|
+
[id],
|
|
754
|
+
);
|
|
755
|
+
let accounts;
|
|
756
|
+
if (account.closed) {
|
|
757
|
+
accounts = await all<Pick<DbAccount, 'id' | 'sort_order'>>(
|
|
758
|
+
`SELECT id, sort_order FROM accounts WHERE closed = 1 ORDER BY sort_order, name`,
|
|
759
|
+
);
|
|
760
|
+
} else {
|
|
761
|
+
accounts = await all<Pick<DbAccount, 'id' | 'sort_order'>>(
|
|
762
|
+
`SELECT id, sort_order FROM accounts WHERE tombstone = 0 AND offbudget = ? ORDER BY sort_order, name`,
|
|
763
|
+
[account.offbudget ? 1 : 0],
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const { updates, sort_order } = shoveSortOrders(accounts, targetId);
|
|
768
|
+
await batchMessages(async () => {
|
|
769
|
+
for (const info of updates) {
|
|
770
|
+
void update('accounts', info);
|
|
771
|
+
}
|
|
772
|
+
void update('accounts', { id, sort_order });
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export async function getTransaction(id: DbViewTransaction['id']) {
|
|
777
|
+
const rows = await selectWithSchema(
|
|
778
|
+
'transactions',
|
|
779
|
+
'SELECT * FROM v_transactions WHERE id = ?',
|
|
780
|
+
[id],
|
|
781
|
+
);
|
|
782
|
+
return rows[0];
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export async function getTransactions(accountId: DbTransaction['acct']) {
|
|
786
|
+
if (arguments.length > 1) {
|
|
787
|
+
throw new Error(
|
|
788
|
+
'`getTransactions` was given a second argument, it now only takes a single argument `accountId`',
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return selectWithSchema(
|
|
793
|
+
'transactions',
|
|
794
|
+
'SELECT * FROM v_transactions WHERE account = ?',
|
|
795
|
+
[accountId],
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
export function insertTransaction(
|
|
800
|
+
transaction,
|
|
801
|
+
): Promise<TransactionEntity['id']> {
|
|
802
|
+
return insertWithSchema('transactions', transaction);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export function updateTransaction(transaction) {
|
|
806
|
+
return updateWithSchema('transactions', transaction);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
export async function deleteTransaction(transaction) {
|
|
810
|
+
return delete_('transactions', transaction.id);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function toSqlQueryParameters(params: unknown[]) {
|
|
814
|
+
return params.map(() => '?').join(',');
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export function getTags() {
|
|
818
|
+
return all<DbTag>(`
|
|
819
|
+
SELECT id, tag, color, description
|
|
820
|
+
FROM tags
|
|
821
|
+
WHERE tombstone = 0
|
|
822
|
+
ORDER BY tag
|
|
823
|
+
`);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
export function getAllTags() {
|
|
827
|
+
return all<DbTag>(`
|
|
828
|
+
SELECT id, tag, color, description
|
|
829
|
+
FROM tags
|
|
830
|
+
ORDER BY tag
|
|
831
|
+
`);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export function insertTag(tag): Promise<DbTag['id']> {
|
|
835
|
+
return insertWithUUID('tags', tag);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
export async function deleteTag(tag) {
|
|
839
|
+
return delete_('tags', tag.id);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export function updateTag(tag) {
|
|
843
|
+
return update('tags', tag);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export function findTags() {
|
|
847
|
+
return all<{ notes: string }>(
|
|
848
|
+
`
|
|
849
|
+
SELECT notes
|
|
850
|
+
FROM transactions
|
|
851
|
+
WHERE tombstone = 0 AND notes LIKE ?
|
|
852
|
+
`,
|
|
853
|
+
['%#%'],
|
|
854
|
+
);
|
|
855
|
+
}
|