@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.
Files changed (358) hide show
  1. package/.swcrc +11 -0
  2. package/bin/build-browser +40 -0
  3. package/bin/copy-migrations +9 -0
  4. package/db.sqlite +0 -0
  5. package/default-db.sqlite +0 -0
  6. package/migrations/.force-copy-windows +0 -0
  7. package/migrations/1548957970627_remove-db-version.sql +5 -0
  8. package/migrations/1550601598648_payees.sql +23 -0
  9. package/migrations/1555786194328_remove_category_group_unique.sql +25 -0
  10. package/migrations/1561751833510_indexes.sql +7 -0
  11. package/migrations/1567699552727_budget.sql +38 -0
  12. package/migrations/1582384163573_cleared.sql +6 -0
  13. package/migrations/1597756566448_rules.sql +10 -0
  14. package/migrations/1608652596043_parent_field.sql +13 -0
  15. package/migrations/1608652596044_trans_views.sql +56 -0
  16. package/migrations/1612625548236_optimize.sql +7 -0
  17. package/migrations/1614782639336_trans_views2.sql +33 -0
  18. package/migrations/1615745967948_meta.sql +10 -0
  19. package/migrations/1616167010796_accounts_order.sql +5 -0
  20. package/migrations/1618975177358_schedules.sql +28 -0
  21. package/migrations/1632571489012_remove_cache.js +136 -0
  22. package/migrations/1679728867040_rules_conditions.sql +5 -0
  23. package/migrations/1681115033845_add_schedule_name.sql +5 -0
  24. package/migrations/1682974838138_remove_payee_rules.sql +5 -0
  25. package/migrations/1685007876842_add_category_hidden.sql +6 -0
  26. package/migrations/1686139660866_remove_account_type.sql +5 -0
  27. package/migrations/1688749527273_transaction_filters.sql +10 -0
  28. package/migrations/1688841238000_add_account_type.sql +5 -0
  29. package/migrations/1691233396000_add_schedule_next_date_tombstone.sql +5 -0
  30. package/migrations/1694438752000_add_goal_targets.sql +7 -0
  31. package/migrations/1697046240000_add_reconciled.sql +5 -0
  32. package/migrations/1704572023730_add_account_sync_source.sql +5 -0
  33. package/migrations/1704572023731_add_missing_goCardless_sync_source.sql +9 -0
  34. package/migrations/1707267033000_reports.sql +28 -0
  35. package/migrations/1712784523000_unhide_input_group.sql +8 -0
  36. package/migrations/1716359441000_include_current.sql +5 -0
  37. package/migrations/1720310586000_link_transfer_schedules.sql +19 -0
  38. package/migrations/1720664867241_add_payee_favorite.sql +5 -0
  39. package/migrations/1720665000000_goal_context.sql +6 -0
  40. package/migrations/1722717601000_reports_move_selected_categories.js +55 -0
  41. package/migrations/1722804019000_create_dashboard_table.js +69 -0
  42. package/migrations/1723665565000_prefs.js +59 -0
  43. package/migrations/1730744182000_fix_dashboard_table.sql +7 -0
  44. package/migrations/1736640000000_custom_report_sorting.sql +7 -0
  45. package/migrations/1737158400000_add_learn_categories_to_payees.sql +5 -0
  46. package/migrations/1738491452000_sorting_rename.sql +13 -0
  47. package/migrations/1739139550000_bank_sync_page.sql +7 -0
  48. package/migrations/1740506588539_add_last_reconciled_at.sql +5 -0
  49. package/migrations/1745425408000_update_budgetType_pref.sql +7 -0
  50. package/migrations/1749799110000_add_tags.sql +10 -0
  51. package/migrations/1749799110001_tags_tombstone.sql +5 -0
  52. package/migrations/1754611200000_add_category_template_settings.sql +5 -0
  53. package/migrations/1759260219000_add_trim_interval_report_setting.sql +6 -0
  54. package/migrations/1759842823172_add_isGlobal_to_preferences.sql +1 -0
  55. package/migrations/1762178745667_rename_csv_skip_lines_pref.sql +8 -0
  56. package/migrations/1765518577215_multiple_dashboards.js +30 -0
  57. package/migrations/1768872504000_add_payee_locations.sql +21 -0
  58. package/package.json +128 -0
  59. package/src/mocks/arbitrary-schema.ts +162 -0
  60. package/src/mocks/budget.ts +901 -0
  61. package/src/mocks/files/8859-1.qfx +63 -0
  62. package/src/mocks/files/best.data-ever$.QFX +124 -0
  63. package/src/mocks/files/big.data.QiF +91 -0
  64. package/src/mocks/files/budgets/.commit-to-git +0 -0
  65. package/src/mocks/files/camt/camt.053.payee-memo.xml +127 -0
  66. package/src/mocks/files/camt/camt.053.xml +463 -0
  67. package/src/mocks/files/credit-card.ofx +11 -0
  68. package/src/mocks/files/data-multi-decimal.ofx +64 -0
  69. package/src/mocks/files/data-payee-memo.ofx +75 -0
  70. package/src/mocks/files/data-payee-memo.qif +17 -0
  71. package/src/mocks/files/data.ofx +124 -0
  72. package/src/mocks/files/data.qfx +124 -0
  73. package/src/mocks/files/data.qif +91 -0
  74. package/src/mocks/files/default-budget-template/db.sqlite +0 -0
  75. package/src/mocks/files/default-budget-template/metadata.json +6 -0
  76. package/src/mocks/files/html-vals.qfx +17 -0
  77. package/src/mocks/index.ts +221 -0
  78. package/src/mocks/migrations/1508717984291_up_add-poop.sql +13 -0
  79. package/src/mocks/migrations/1508718036311_up_modify-poop.sql +2 -0
  80. package/src/mocks/migrations/1508727787513_remove-is_income.sql +15 -0
  81. package/src/mocks/random.ts +16 -0
  82. package/src/mocks/setup.ts +180 -0
  83. package/src/mocks/spreadsheet.ts +101 -0
  84. package/src/mocks/util.ts +82 -0
  85. package/src/platform/client/connection/README.md +3 -0
  86. package/src/platform/client/connection/__mocks__/index.ts +67 -0
  87. package/src/platform/client/connection/index-types.ts +95 -0
  88. package/src/platform/client/connection/index.browser.ts +213 -0
  89. package/src/platform/client/connection/index.ts +155 -0
  90. package/src/platform/client/undo/index.ts +59 -0
  91. package/src/platform/exceptions/__mocks__/index.ts +7 -0
  92. package/src/platform/exceptions/index.ts +9 -0
  93. package/src/platform/server/asyncStorage/__mocks__/index.ts +50 -0
  94. package/src/platform/server/asyncStorage/index-types.ts +35 -0
  95. package/src/platform/server/asyncStorage/index.api.ts +2 -0
  96. package/src/platform/server/asyncStorage/index.electron.ts +88 -0
  97. package/src/platform/server/asyncStorage/index.ts +126 -0
  98. package/src/platform/server/connection/README.md +3 -0
  99. package/src/platform/server/connection/__mocks__/index.ts +15 -0
  100. package/src/platform/server/connection/index-types.ts +20 -0
  101. package/src/platform/server/connection/index.api.ts +13 -0
  102. package/src/platform/server/connection/index.electron.ts +102 -0
  103. package/src/platform/server/connection/index.ts +154 -0
  104. package/src/platform/server/fetch/__mocks__/index.ts +3 -0
  105. package/src/platform/server/fetch/index.api.ts +1 -0
  106. package/src/platform/server/fetch/index.electron.ts +18 -0
  107. package/src/platform/server/fetch/index.ts +20 -0
  108. package/src/platform/server/fs/index.api.ts +198 -0
  109. package/src/platform/server/fs/index.electron.ts +208 -0
  110. package/src/platform/server/fs/index.test.ts +117 -0
  111. package/src/platform/server/fs/index.ts +416 -0
  112. package/src/platform/server/fs/path-join.api.ts +1 -0
  113. package/src/platform/server/fs/path-join.electron.ts +1 -0
  114. package/src/platform/server/fs/path-join.ts +97 -0
  115. package/src/platform/server/fs/shared.ts +33 -0
  116. package/src/platform/server/indexeddb/index.ts +115 -0
  117. package/src/platform/server/log/index.ts +43 -0
  118. package/src/platform/server/sqlite/index.api.ts +2 -0
  119. package/src/platform/server/sqlite/index.electron.ts +134 -0
  120. package/src/platform/server/sqlite/index.test.ts +108 -0
  121. package/src/platform/server/sqlite/index.ts +241 -0
  122. package/src/platform/server/sqlite/normalise.ts +9 -0
  123. package/src/platform/server/sqlite/unicodeLike.test.ts +58 -0
  124. package/src/platform/server/sqlite/unicodeLike.ts +31 -0
  125. package/src/server/__mocks__/post.ts +9 -0
  126. package/src/server/__snapshots__/main.test.ts.snap +199 -0
  127. package/src/server/__snapshots__/sheet.test.ts.snap +9 -0
  128. package/src/server/accounts/__snapshots__/sync.test.ts.snap +136 -0
  129. package/src/server/accounts/app-bank-sync.test.ts +136 -0
  130. package/src/server/accounts/app.ts +1294 -0
  131. package/src/server/accounts/link.ts +25 -0
  132. package/src/server/accounts/payees.ts +36 -0
  133. package/src/server/accounts/sync.test.ts +679 -0
  134. package/src/server/accounts/sync.ts +1168 -0
  135. package/src/server/accounts/title/index.ts +60 -0
  136. package/src/server/accounts/title/lower-case.ts +93 -0
  137. package/src/server/accounts/title/specials.ts +21 -0
  138. package/src/server/admin/app.ts +241 -0
  139. package/src/server/api-models.ts +244 -0
  140. package/src/server/api.test.ts +36 -0
  141. package/src/server/api.ts +1030 -0
  142. package/src/server/app.ts +91 -0
  143. package/src/server/aql/compiler.test.ts +966 -0
  144. package/src/server/aql/compiler.ts +1222 -0
  145. package/src/server/aql/exec.test.ts +289 -0
  146. package/src/server/aql/exec.ts +128 -0
  147. package/src/server/aql/index.ts +41 -0
  148. package/src/server/aql/schema/executors.test.ts +420 -0
  149. package/src/server/aql/schema/executors.ts +345 -0
  150. package/src/server/aql/schema/index.test.ts +67 -0
  151. package/src/server/aql/schema/index.ts +409 -0
  152. package/src/server/aql/schema-helpers.test.ts +242 -0
  153. package/src/server/aql/schema-helpers.ts +208 -0
  154. package/src/server/aql/views.test.ts +62 -0
  155. package/src/server/aql/views.ts +57 -0
  156. package/src/server/auth/app.ts +387 -0
  157. package/src/server/bench.ts +29 -0
  158. package/src/server/budget/actions.ts +686 -0
  159. package/src/server/budget/app.ts +469 -0
  160. package/src/server/budget/base.test.ts +340 -0
  161. package/src/server/budget/base.ts +339 -0
  162. package/src/server/budget/category-template-context.test.ts +1658 -0
  163. package/src/server/budget/category-template-context.ts +862 -0
  164. package/src/server/budget/cleanup-template.pegjs +27 -0
  165. package/src/server/budget/cleanup-template.ts +408 -0
  166. package/src/server/budget/envelope.ts +403 -0
  167. package/src/server/budget/goal-template.pegjs +110 -0
  168. package/src/server/budget/goal-template.ts +309 -0
  169. package/src/server/budget/report.ts +308 -0
  170. package/src/server/budget/schedule-template.test.ts +184 -0
  171. package/src/server/budget/schedule-template.ts +351 -0
  172. package/src/server/budget/statements.ts +60 -0
  173. package/src/server/budget/template-notes.test.ts +393 -0
  174. package/src/server/budget/template-notes.ts +323 -0
  175. package/src/server/budget/util.ts +25 -0
  176. package/src/server/budgetfiles/__snapshots__/backups.test.ts.snap +101 -0
  177. package/src/server/budgetfiles/app.ts +672 -0
  178. package/src/server/budgetfiles/backups.test.ts +79 -0
  179. package/src/server/budgetfiles/backups.ts +251 -0
  180. package/src/server/cloud-storage.ts +467 -0
  181. package/src/server/dashboard/app.ts +373 -0
  182. package/src/server/db/__snapshots__/index.test.ts.snap +271 -0
  183. package/src/server/db/index.test.ts +300 -0
  184. package/src/server/db/index.ts +855 -0
  185. package/src/server/db/mappings.ts +59 -0
  186. package/src/server/db/sort.ts +58 -0
  187. package/src/server/db/types/index.ts +342 -0
  188. package/src/server/db/util.ts +36 -0
  189. package/src/server/encryption/app.ts +133 -0
  190. package/src/server/encryption/encryption-internals.api.ts +2 -0
  191. package/src/server/encryption/encryption-internals.electron.ts +89 -0
  192. package/src/server/encryption/encryption-internals.ts +109 -0
  193. package/src/server/encryption/encryption.test.ts +19 -0
  194. package/src/server/encryption/index.test.ts +19 -0
  195. package/src/server/encryption/index.ts +89 -0
  196. package/src/server/errors.ts +110 -0
  197. package/src/server/filters/app.ts +191 -0
  198. package/src/server/importers/actual.ts +49 -0
  199. package/src/server/importers/index.ts +58 -0
  200. package/src/server/importers/ynab4-types.ts +163 -0
  201. package/src/server/importers/ynab4.ts +470 -0
  202. package/src/server/importers/ynab5-types.ts +290 -0
  203. package/src/server/importers/ynab5.ts +1193 -0
  204. package/src/server/main-app.ts +25 -0
  205. package/src/server/main.test.ts +392 -0
  206. package/src/server/main.ts +336 -0
  207. package/src/server/migrate/__snapshots__/migrations.test.ts.snap +17 -0
  208. package/src/server/migrate/cli.ts +100 -0
  209. package/src/server/migrate/migrations.test.ts +81 -0
  210. package/src/server/migrate/migrations.ts +192 -0
  211. package/src/server/models.ts +184 -0
  212. package/src/server/mutators.ts +139 -0
  213. package/src/server/notes/app.ts +18 -0
  214. package/src/server/payees/app.ts +351 -0
  215. package/src/server/polyfills.ts +26 -0
  216. package/src/server/post.ts +219 -0
  217. package/src/server/preferences/app.ts +249 -0
  218. package/src/server/prefs.ts +91 -0
  219. package/src/server/reports/app.ts +187 -0
  220. package/src/server/rules/action.ts +344 -0
  221. package/src/server/rules/app.ts +193 -0
  222. package/src/server/rules/condition.ts +436 -0
  223. package/src/server/rules/customFunctions.ts +61 -0
  224. package/src/server/rules/formula-action.test.ts +175 -0
  225. package/src/server/rules/handlebars-helpers.ts +131 -0
  226. package/src/server/rules/index.test.ts +1095 -0
  227. package/src/server/rules/index.ts +22 -0
  228. package/src/server/rules/rule-indexer.ts +89 -0
  229. package/src/server/rules/rule-utils.ts +274 -0
  230. package/src/server/rules/rule.ts +193 -0
  231. package/src/server/schedules/app.test.ts +502 -0
  232. package/src/server/schedules/app.ts +644 -0
  233. package/src/server/schedules/find-schedules.ts +391 -0
  234. package/src/server/server-config.ts +59 -0
  235. package/src/server/sheet.test.ts +101 -0
  236. package/src/server/sheet.ts +280 -0
  237. package/src/server/spreadsheet/__snapshots__/spreadsheet.test.ts.snap +5 -0
  238. package/src/server/spreadsheet/app.ts +54 -0
  239. package/src/server/spreadsheet/globals.ts +13 -0
  240. package/src/server/spreadsheet/graph-data-structure.ts +165 -0
  241. package/src/server/spreadsheet/scratch +60 -0
  242. package/src/server/spreadsheet/spreadsheet.test.ts +191 -0
  243. package/src/server/spreadsheet/spreadsheet.ts +523 -0
  244. package/src/server/spreadsheet/util.ts +15 -0
  245. package/src/server/sql/init.sql +88 -0
  246. package/src/server/sync/__snapshots__/sync.test.ts.snap +31 -0
  247. package/src/server/sync/app.ts +29 -0
  248. package/src/server/sync/encoder.ts +129 -0
  249. package/src/server/sync/index.ts +820 -0
  250. package/src/server/sync/make-test-message.ts +19 -0
  251. package/src/server/sync/migrate.test.ts +169 -0
  252. package/src/server/sync/migrate.ts +48 -0
  253. package/src/server/sync/repair.ts +39 -0
  254. package/src/server/sync/reset.ts +91 -0
  255. package/src/server/sync/sync.property.test.ts +385 -0
  256. package/src/server/sync/sync.test.ts +349 -0
  257. package/src/server/sync/utils.ts +3 -0
  258. package/src/server/tags/app.ts +101 -0
  259. package/src/server/tests/mockData.json +9352 -0
  260. package/src/server/tests/mockSyncServer.ts +119 -0
  261. package/src/server/tools/app.ts +152 -0
  262. package/src/server/transactions/__snapshots__/transaction-rules.test.ts.snap +173 -0
  263. package/src/server/transactions/__snapshots__/transfer.test.ts.snap +655 -0
  264. package/src/server/transactions/app.ts +136 -0
  265. package/src/server/transactions/export/export-to-csv.ts +132 -0
  266. package/src/server/transactions/import/__snapshots__/parse-file.test.ts.snap +1582 -0
  267. package/src/server/transactions/import/ofx2json.test.ts +33 -0
  268. package/src/server/transactions/import/ofx2json.ts +157 -0
  269. package/src/server/transactions/import/parse-file.test.ts +224 -0
  270. package/src/server/transactions/import/parse-file.ts +286 -0
  271. package/src/server/transactions/import/qif2json.ts +110 -0
  272. package/src/server/transactions/import/xmlcamt2json.ts +168 -0
  273. package/src/server/transactions/index.ts +196 -0
  274. package/src/server/transactions/merge.test.ts +370 -0
  275. package/src/server/transactions/merge.ts +139 -0
  276. package/src/server/transactions/transaction-rules.test.ts +994 -0
  277. package/src/server/transactions/transaction-rules.ts +1038 -0
  278. package/src/server/transactions/transfer.test.ts +221 -0
  279. package/src/server/transactions/transfer.ts +173 -0
  280. package/src/server/undo.ts +271 -0
  281. package/src/server/update.ts +37 -0
  282. package/src/server/util/budget-name.ts +61 -0
  283. package/src/server/util/custom-sync-mapping.ts +48 -0
  284. package/src/server/util/rschedule.ts +9 -0
  285. package/src/shared/__mocks__/platform.ts +7 -0
  286. package/src/shared/__snapshots__/months.test.ts.snap +21 -0
  287. package/src/shared/arithmetic.test.ts +112 -0
  288. package/src/shared/arithmetic.ts +170 -0
  289. package/src/shared/async.test.ts +135 -0
  290. package/src/shared/async.ts +76 -0
  291. package/src/shared/constants.ts +5 -0
  292. package/src/shared/currencies.ts +70 -0
  293. package/src/shared/dashboard.ts +260 -0
  294. package/src/shared/environment.ts +18 -0
  295. package/src/shared/errors.ts +195 -0
  296. package/src/shared/locale.ts +27 -0
  297. package/src/shared/location-utils.test.ts +69 -0
  298. package/src/shared/location-utils.ts +49 -0
  299. package/src/shared/months.test.ts +5 -0
  300. package/src/shared/months.ts +485 -0
  301. package/src/shared/normalisation.ts +6 -0
  302. package/src/shared/platform.electron.ts +21 -0
  303. package/src/shared/platform.ts +20 -0
  304. package/src/shared/query.ts +176 -0
  305. package/src/shared/rules.test.ts +56 -0
  306. package/src/shared/rules.ts +371 -0
  307. package/src/shared/schedules.test.ts +570 -0
  308. package/src/shared/schedules.ts +560 -0
  309. package/src/shared/test-helpers.ts +156 -0
  310. package/src/shared/transactions.test.ts +275 -0
  311. package/src/shared/transactions.ts +433 -0
  312. package/src/shared/transfer.test.ts +75 -0
  313. package/src/shared/transfer.ts +16 -0
  314. package/src/shared/user.ts +4 -0
  315. package/src/shared/util.test.ts +240 -0
  316. package/src/shared/util.ts +633 -0
  317. package/src/types/api-handlers.ts +287 -0
  318. package/src/types/budget.ts +8 -0
  319. package/src/types/file.ts +47 -0
  320. package/src/types/handlers.ts +46 -0
  321. package/src/types/models/account.ts +24 -0
  322. package/src/types/models/bank-sync.ts +23 -0
  323. package/src/types/models/bank.ts +6 -0
  324. package/src/types/models/category-group.ts +11 -0
  325. package/src/types/models/category.ts +13 -0
  326. package/src/types/models/dashboard.ts +199 -0
  327. package/src/types/models/gocardless.ts +84 -0
  328. package/src/types/models/import-transaction.ts +56 -0
  329. package/src/types/models/index.ts +23 -0
  330. package/src/types/models/nearby-payee.ts +7 -0
  331. package/src/types/models/note.ts +4 -0
  332. package/src/types/models/openid.ts +8 -0
  333. package/src/types/models/payee-location.ts +8 -0
  334. package/src/types/models/payee.ts +10 -0
  335. package/src/types/models/pluggyai.ts +19 -0
  336. package/src/types/models/reports.ts +144 -0
  337. package/src/types/models/rule.ts +174 -0
  338. package/src/types/models/schedule.ts +49 -0
  339. package/src/types/models/simplefin.ts +28 -0
  340. package/src/types/models/tags.ts +6 -0
  341. package/src/types/models/templates.ts +135 -0
  342. package/src/types/models/transaction-filter.ts +9 -0
  343. package/src/types/models/transaction.ts +39 -0
  344. package/src/types/models/user-access.ts +10 -0
  345. package/src/types/models/user.ts +25 -0
  346. package/src/types/prefs.ts +167 -0
  347. package/src/types/server-events.ts +86 -0
  348. package/src/types/server-handlers.ts +27 -0
  349. package/src/types/util.ts +26 -0
  350. package/tsconfig.json +34 -0
  351. package/typings/pegjs.ts +1 -0
  352. package/typings/process-worker.ts +12 -0
  353. package/typings/vite-plugin-peggy-loader.ts +1 -0
  354. package/typings/window.ts +62 -0
  355. package/vite.config.ts +109 -0
  356. package/vite.desktop.config.ts +59 -0
  357. package/vitest.config.ts +43 -0
  358. 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
+ }