@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,672 @@
1
+ // @ts-strict-ignore
2
+ import * as CRDT from '@actual-app/crdt';
3
+
4
+ import { createTestBudget } from '../../mocks/budget';
5
+ import { captureBreadcrumb, captureException } from '../../platform/exceptions';
6
+ import * as asyncStorage from '../../platform/server/asyncStorage';
7
+ import * as connection from '../../platform/server/connection';
8
+ import * as fs from '../../platform/server/fs';
9
+ import { logger } from '../../platform/server/log';
10
+ import * as Platform from '../../shared/platform';
11
+ import type { Budget } from '../../types/budget';
12
+ import { createApp } from '../app';
13
+ import * as budget from '../budget/base';
14
+ import * as cloudStorage from '../cloud-storage';
15
+ import * as db from '../db';
16
+ import * as mappings from '../db/mappings';
17
+ import { handleBudgetImport } from '../importers';
18
+ import type { ImportableBudgetType } from '../importers';
19
+ import { app as mainApp } from '../main-app';
20
+ import { mutator } from '../mutators';
21
+ import * as prefs from '../prefs';
22
+ import { getServer } from '../server-config';
23
+ import * as sheet from '../sheet';
24
+ import { clearFullSyncTimeout, initialFullSync, setSyncingMode } from '../sync';
25
+ import * as syncMigrations from '../sync/migrate';
26
+ import * as rules from '../transactions/transaction-rules';
27
+ import { clearUndo } from '../undo';
28
+ import { updateVersion } from '../update';
29
+ import {
30
+ idFromBudgetName,
31
+ uniqueBudgetName,
32
+ validateBudgetName,
33
+ } from '../util/budget-name';
34
+
35
+ import {
36
+ loadBackup as _loadBackup,
37
+ makeBackup as _makeBackup,
38
+ getAvailableBackups,
39
+ startBackupService,
40
+ stopBackupService,
41
+ } from './backups';
42
+
43
+ const DEMO_BUDGET_ID = '_demo-budget';
44
+ const TEST_BUDGET_ID = '_test-budget';
45
+
46
+ export type BudgetFileHandlers = {
47
+ 'validate-budget-name': typeof handleValidateBudgetName;
48
+ 'unique-budget-name': typeof handleUniqueBudgetName;
49
+ 'get-budgets': typeof getBudgets;
50
+ 'get-remote-files': typeof getRemoteFiles;
51
+ 'reset-budget-cache': typeof resetBudgetCache;
52
+ 'upload-budget': typeof uploadBudget;
53
+ 'download-budget': typeof downloadBudget;
54
+ 'sync-budget': typeof syncBudget;
55
+ 'load-budget': typeof loadBudget;
56
+ 'create-demo-budget': typeof createDemoBudget;
57
+ 'close-budget': typeof closeBudget;
58
+ 'delete-budget': typeof deleteBudget;
59
+ 'duplicate-budget': typeof duplicateBudget;
60
+ 'create-budget': typeof createBudget;
61
+ 'import-budget': typeof importBudget;
62
+ 'export-budget': typeof exportBudget;
63
+ 'upload-file-web': typeof uploadFileWeb;
64
+ 'backups-get': typeof getBackups;
65
+ 'backup-load': typeof loadBackup;
66
+ 'backup-make': typeof makeBackup;
67
+ 'get-last-opened-backup': typeof getLastOpenedBackup;
68
+ };
69
+
70
+ export const app = createApp<BudgetFileHandlers>();
71
+ app.method('validate-budget-name', handleValidateBudgetName);
72
+ app.method('unique-budget-name', handleUniqueBudgetName);
73
+ app.method('get-budgets', getBudgets);
74
+ app.method('get-remote-files', getRemoteFiles);
75
+ app.method('reset-budget-cache', mutator(resetBudgetCache));
76
+ app.method('upload-budget', uploadBudget);
77
+ app.method('download-budget', downloadBudget);
78
+ app.method('sync-budget', syncBudget);
79
+ app.method('load-budget', loadBudget);
80
+ app.method('create-demo-budget', createDemoBudget);
81
+ app.method('close-budget', closeBudget);
82
+ app.method('delete-budget', deleteBudget);
83
+ app.method('duplicate-budget', duplicateBudget);
84
+ app.method('create-budget', createBudget);
85
+ app.method('import-budget', importBudget);
86
+ app.method('export-budget', exportBudget);
87
+ app.method('upload-file-web', uploadFileWeb);
88
+ app.method('backups-get', getBackups);
89
+ app.method('backup-load', loadBackup);
90
+ app.method('backup-make', makeBackup);
91
+ app.method('get-last-opened-backup', getLastOpenedBackup);
92
+
93
+ async function handleValidateBudgetName({ name }: { name: string }) {
94
+ return validateBudgetName(name);
95
+ }
96
+
97
+ async function handleUniqueBudgetName({ name }: { name: string }) {
98
+ return uniqueBudgetName(name);
99
+ }
100
+
101
+ async function getBudgets() {
102
+ const paths = await fs.listDir(fs.getDocumentDir());
103
+ const budgets: (Budget | null)[] = await Promise.all(
104
+ paths.map(async name => {
105
+ const prefsPath = fs.join(fs.getDocumentDir(), name, 'metadata.json');
106
+ if (await fs.exists(prefsPath)) {
107
+ let prefs;
108
+ try {
109
+ prefs = JSON.parse(await fs.readFile(prefsPath));
110
+ } catch (e) {
111
+ logger.log('Error parsing metadata:', e.stack);
112
+ return null;
113
+ }
114
+
115
+ // We treat the directory name as the canonical id so that if
116
+ // the user moves it around/renames/etc, nothing breaks. The
117
+ // id is stored in prefs just for convenience (and the prefs
118
+ // will always update to the latest given id)
119
+ if (name !== DEMO_BUDGET_ID) {
120
+ return {
121
+ id: name,
122
+ ...(prefs.cloudFileId ? { cloudFileId: prefs.cloudFileId } : {}),
123
+ ...(prefs.encryptKeyId ? { encryptKeyId: prefs.encryptKeyId } : {}),
124
+ ...(prefs.groupId ? { groupId: prefs.groupId } : {}),
125
+ ...(prefs.owner ? { owner: prefs.owner } : {}),
126
+ name: prefs.budgetName || '(no name)',
127
+ } satisfies Budget;
128
+ }
129
+ }
130
+
131
+ return null;
132
+ }),
133
+ );
134
+
135
+ return budgets.filter(Boolean) as Budget[];
136
+ }
137
+
138
+ async function getRemoteFiles() {
139
+ return cloudStorage.listRemoteFiles();
140
+ }
141
+
142
+ async function resetBudgetCache() {
143
+ // Recomputing everything will update the cache
144
+ await sheet.loadUserBudgets(db);
145
+ sheet.get().recomputeAll();
146
+ await sheet.waitOnSpreadsheet();
147
+ }
148
+
149
+ async function uploadBudget({ id }: { id?: Budget['id'] } = {}): Promise<{
150
+ error?: { reason: string };
151
+ }> {
152
+ if (id) {
153
+ if (prefs.getPrefs()) {
154
+ throw new Error('upload-budget: id given but prefs already loaded');
155
+ }
156
+
157
+ await prefs.loadPrefs(id);
158
+ }
159
+
160
+ try {
161
+ await cloudStorage.upload();
162
+ } catch (e) {
163
+ logger.log(e);
164
+ if (e.type === 'FileUploadError') {
165
+ return { error: e };
166
+ }
167
+ captureException(e);
168
+ return { error: { reason: 'internal' } };
169
+ } finally {
170
+ if (id) {
171
+ prefs.unloadPrefs();
172
+ }
173
+ }
174
+
175
+ return {};
176
+ }
177
+
178
+ async function downloadBudget({
179
+ cloudFileId,
180
+ }: {
181
+ cloudFileId: Budget['cloudFileId'];
182
+ }): Promise<{ id?: Budget['id']; error?: { reason: string; meta?: unknown } }> {
183
+ let result;
184
+ try {
185
+ result = await cloudStorage.download(cloudFileId);
186
+ } catch (e) {
187
+ if (e.type === 'FileDownloadError') {
188
+ if (e.reason === 'file-exists' && e.meta.id) {
189
+ await prefs.loadPrefs(e.meta.id);
190
+ const name = prefs.getPrefs().budgetName;
191
+ prefs.unloadPrefs();
192
+
193
+ e.meta = { ...e.meta, name };
194
+ }
195
+
196
+ return { error: e };
197
+ } else {
198
+ captureException(e);
199
+ return { error: { reason: 'internal' } };
200
+ }
201
+ }
202
+
203
+ const id = result.id;
204
+ await closeBudget();
205
+ await loadBudget({ id });
206
+ result = await syncBudget();
207
+
208
+ if (result.error) {
209
+ return result;
210
+ }
211
+ return { id };
212
+ }
213
+
214
+ // open and sync, but don't close
215
+ async function syncBudget() {
216
+ setSyncingMode('enabled');
217
+ const result = await initialFullSync();
218
+
219
+ return result;
220
+ }
221
+
222
+ async function loadBudget({ id }: { id: Budget['id'] }) {
223
+ const currentPrefs = prefs.getPrefs();
224
+
225
+ if (currentPrefs) {
226
+ if (currentPrefs.id === id) {
227
+ // If it's already loaded, do nothing
228
+ return {};
229
+ } else {
230
+ // Otherwise, close the currently loaded budget
231
+ await closeBudget();
232
+ }
233
+ }
234
+
235
+ const res = await _loadBudget(id);
236
+
237
+ return res;
238
+ }
239
+
240
+ async function createDemoBudget() {
241
+ // Make sure the read only flag isn't leftover (normally it's
242
+ // reset when signing in, but you don't have to sign in for the
243
+ // demo budget)
244
+ await asyncStorage.setItem('readOnly', '');
245
+
246
+ return createBudget({
247
+ budgetName: 'Demo Budget',
248
+ testMode: true,
249
+ testBudgetId: DEMO_BUDGET_ID,
250
+ });
251
+ }
252
+
253
+ async function closeBudget() {
254
+ captureBreadcrumb({ message: 'Closing budget' });
255
+
256
+ // The spreadsheet may be running, wait for it to complete
257
+ await sheet.waitOnSpreadsheet();
258
+ sheet.unloadSpreadsheet();
259
+
260
+ clearFullSyncTimeout();
261
+ await mainApp.stopServices();
262
+
263
+ db.closeDatabase();
264
+
265
+ try {
266
+ await asyncStorage.setItem('lastBudget', '');
267
+ } catch {
268
+ // This might fail if we are shutting down after failing to load a
269
+ // budget. We want to unload whatever has already been loaded but
270
+ // be resilient to anything failing
271
+ }
272
+
273
+ prefs.unloadPrefs();
274
+ stopBackupService();
275
+ return 'ok';
276
+ }
277
+
278
+ async function deleteBudget({
279
+ id,
280
+ cloudFileId,
281
+ }: {
282
+ id?: Budget['id'];
283
+ cloudFileId?: Budget['cloudFileId'];
284
+ }) {
285
+ // If it's a cloud file, you can delete it from the server by
286
+ // passing its cloud id
287
+ if (cloudFileId) {
288
+ await cloudStorage.removeFile(cloudFileId).catch(() => {
289
+ // Ignore errors
290
+ });
291
+ }
292
+
293
+ // If a local file exists, you can delete it by passing its local id
294
+ if (id) {
295
+ // opening and then closing the database is a hack to be able to delete
296
+ // the budget file if it hasn't been opened yet. This needs a better
297
+ // way, but works for now.
298
+ try {
299
+ await db.openDatabase(id);
300
+ db.closeDatabase();
301
+ const budgetDir = fs.getBudgetDir(id);
302
+ await fs.removeDirRecursively(budgetDir);
303
+ } catch {
304
+ return 'fail';
305
+ }
306
+ }
307
+
308
+ return 'ok';
309
+ }
310
+
311
+ async function duplicateBudget({
312
+ id,
313
+ newName,
314
+ cloudSync,
315
+ open,
316
+ }: {
317
+ id: Budget['id'];
318
+ newName: Budget['name'];
319
+ cloudSync: boolean;
320
+ open: 'none' | 'original' | 'copy';
321
+ }): Promise<Budget['id']> {
322
+ const { valid, message } = await validateBudgetName(newName);
323
+ if (!valid) throw new Error(message);
324
+
325
+ const budgetDir = fs.getBudgetDir(id);
326
+
327
+ const newId = await idFromBudgetName(newName);
328
+
329
+ // copy metadata from current budget
330
+ // replace id with new budget id and budgetName with new budget name
331
+ const metadataText = await fs.readFile(fs.join(budgetDir, 'metadata.json'));
332
+ const metadata = JSON.parse(metadataText);
333
+ metadata.id = newId;
334
+ metadata.budgetName = newName;
335
+ [
336
+ 'cloudFileId',
337
+ 'groupId',
338
+ 'lastUploaded',
339
+ 'encryptKeyId',
340
+ 'lastSyncedTimestamp',
341
+ ].forEach(item => {
342
+ if (metadata[item]) delete metadata[item];
343
+ });
344
+
345
+ try {
346
+ const newBudgetDir = fs.getBudgetDir(newId);
347
+ await fs.mkdir(newBudgetDir);
348
+
349
+ // write metadata for new budget
350
+ await fs.writeFile(
351
+ fs.join(newBudgetDir, 'metadata.json'),
352
+ JSON.stringify(metadata),
353
+ );
354
+
355
+ await fs.copyFile(
356
+ fs.join(budgetDir, 'db.sqlite'),
357
+ fs.join(newBudgetDir, 'db.sqlite'),
358
+ );
359
+ } catch (error) {
360
+ // Clean up any partially created files
361
+ try {
362
+ const newBudgetDir = fs.getBudgetDir(newId);
363
+ if (await fs.exists(newBudgetDir)) {
364
+ await fs.removeDirRecursively(newBudgetDir);
365
+ }
366
+ } catch {} // Ignore cleanup errors
367
+ throw new Error(`Failed to duplicate budget file: ${error.message}`);
368
+ }
369
+
370
+ // load in and validate
371
+ const { error } = await _loadBudget(newId);
372
+ if (error) {
373
+ logger.log('Error duplicating budget: ' + error);
374
+ return error;
375
+ }
376
+
377
+ if (cloudSync) {
378
+ try {
379
+ await cloudStorage.upload();
380
+ } catch (error) {
381
+ logger.warn('Failed to sync duplicated budget to cloud:', error);
382
+ // Ignore any errors uploading. If they are offline they should
383
+ // still be able to create files.
384
+ }
385
+ }
386
+
387
+ await closeBudget();
388
+ if (open === 'original') await _loadBudget(id);
389
+ if (open === 'copy') await _loadBudget(newId);
390
+
391
+ return newId;
392
+ }
393
+
394
+ async function createBudget({
395
+ budgetName,
396
+ avoidUpload,
397
+ testMode,
398
+ testBudgetId,
399
+ }: {
400
+ budgetName?: Budget['name'];
401
+ avoidUpload?: boolean;
402
+ testMode?: boolean;
403
+ testBudgetId?: Budget['name'];
404
+ } = {}) {
405
+ let id;
406
+ if (testMode) {
407
+ budgetName = budgetName || 'Test Budget';
408
+ id = testBudgetId || TEST_BUDGET_ID;
409
+
410
+ if (await fs.exists(fs.getBudgetDir(id))) {
411
+ await fs.removeDirRecursively(fs.getBudgetDir(id));
412
+ }
413
+ } else {
414
+ // Generate budget name if not given
415
+ if (!budgetName) {
416
+ budgetName = await uniqueBudgetName();
417
+ }
418
+
419
+ id = await idFromBudgetName(budgetName);
420
+ }
421
+
422
+ const budgetDir = fs.getBudgetDir(id);
423
+ await fs.mkdir(budgetDir);
424
+
425
+ // Create the initial database
426
+ await fs.copyFile(fs.bundledDatabasePath, fs.join(budgetDir, 'db.sqlite'));
427
+
428
+ // Create the initial prefs file
429
+ await fs.writeFile(
430
+ fs.join(budgetDir, 'metadata.json'),
431
+ JSON.stringify(prefs.getDefaultPrefs(id, budgetName)),
432
+ );
433
+
434
+ // Load it in
435
+ const { error } = await _loadBudget(id);
436
+ if (error) {
437
+ logger.log('Error creating budget: ' + error);
438
+ return { error };
439
+ }
440
+
441
+ if (!avoidUpload && !testMode) {
442
+ try {
443
+ await cloudStorage.upload();
444
+ } catch {
445
+ // Ignore any errors uploading. If they are offline they should
446
+ // still be able to create files.
447
+ }
448
+ }
449
+
450
+ if (testMode) {
451
+ await createTestBudget(mainApp.handlers);
452
+ }
453
+
454
+ return {};
455
+ }
456
+
457
+ async function importBudget({
458
+ filepath,
459
+ type,
460
+ }: {
461
+ filepath: string;
462
+ type: ImportableBudgetType;
463
+ }): Promise<{ error?: string }> {
464
+ try {
465
+ if (!(await fs.exists(filepath))) {
466
+ throw new Error(`File not found at the provided path: ${filepath}`);
467
+ }
468
+
469
+ const buffer = Buffer.from(await fs.readFile(filepath, 'binary'));
470
+ const results = await handleBudgetImport(type, filepath, buffer);
471
+ return results || {};
472
+ } catch (err) {
473
+ err.message = 'Error importing budget: ' + err.message;
474
+ captureException(err);
475
+ return { error: 'internal-error' };
476
+ }
477
+ }
478
+
479
+ async function exportBudget() {
480
+ try {
481
+ return {
482
+ data: await cloudStorage.exportBuffer(),
483
+ };
484
+ } catch (err) {
485
+ err.message = 'Error exporting budget: ' + err.message;
486
+ captureException(err);
487
+ return { error: 'internal-error' };
488
+ }
489
+ }
490
+
491
+ function onSheetChange({ names }: { names: string[] }) {
492
+ const nodes = names.map(name => {
493
+ const node = sheet.get()._getNode(name);
494
+ return { name: node.name, value: node.value };
495
+ });
496
+ connection.send('cells-changed', nodes);
497
+ }
498
+
499
+ async function _loadBudget(id: Budget['id']): Promise<{
500
+ error?:
501
+ | 'budget-not-found'
502
+ | 'loading-budget'
503
+ | 'out-of-sync-migrations'
504
+ | 'out-of-sync-data'
505
+ | 'opening-budget';
506
+ }> {
507
+ let dir: string;
508
+ try {
509
+ dir = fs.getBudgetDir(id);
510
+ } catch (e) {
511
+ captureException(
512
+ new Error('`getBudgetDir` failed in `loadBudget`: ' + e.message),
513
+ );
514
+ return { error: 'budget-not-found' };
515
+ }
516
+
517
+ captureBreadcrumb({ message: 'Loading budget ' + dir });
518
+
519
+ if (!(await fs.exists(dir))) {
520
+ captureException(new Error('budget directory does not exist'));
521
+ return { error: 'budget-not-found' };
522
+ }
523
+
524
+ try {
525
+ await prefs.loadPrefs(id);
526
+ await db.openDatabase(id);
527
+ } catch (e) {
528
+ captureBreadcrumb({ message: 'Error loading budget ' + id });
529
+ captureException(e);
530
+ await closeBudget();
531
+ return { error: 'opening-budget' };
532
+ }
533
+
534
+ // Older versions didn't tag the file with the current user, so do
535
+ // so now
536
+ if (!prefs.getPrefs().userId) {
537
+ const userId = await asyncStorage.getItem('user-token');
538
+ await prefs.savePrefs({ userId });
539
+ }
540
+
541
+ try {
542
+ await updateVersion();
543
+ } catch (e) {
544
+ logger.warn('Error updating', e);
545
+ let result;
546
+ if (e.message.includes('out-of-sync-migrations')) {
547
+ result = { error: 'out-of-sync-migrations' };
548
+ } else if (e.message.includes('out-of-sync-data')) {
549
+ result = { error: 'out-of-sync-data' };
550
+ } else {
551
+ captureException(e);
552
+ logger.info('Error updating budget ' + id, e);
553
+ logger.log('Error updating budget', e);
554
+ result = { error: 'loading-budget' };
555
+ }
556
+
557
+ await closeBudget();
558
+ return result;
559
+ }
560
+
561
+ await db.loadClock();
562
+
563
+ if (prefs.getPrefs().resetClock) {
564
+ // If we need to generate a fresh clock, we need to generate a new
565
+ // client id. This happens when the database is transferred to a
566
+ // new device.
567
+ //
568
+ // TODO: The client id should be stored elsewhere. It shouldn't
569
+ // work this way, but it's fine for now.
570
+ CRDT.getClock().timestamp.setNode(CRDT.makeClientId());
571
+ db.runQuery(
572
+ 'INSERT OR REPLACE INTO messages_clock (id, clock) VALUES (1, ?)',
573
+ [CRDT.serializeClock(CRDT.getClock())],
574
+ );
575
+
576
+ await prefs.savePrefs({ resetClock: false });
577
+ }
578
+
579
+ if (!Platform.isBrowser && process.env.NODE_ENV !== 'test') {
580
+ startBackupService(id);
581
+ }
582
+
583
+ try {
584
+ await sheet.loadSpreadsheet(db, onSheetChange);
585
+ } catch (e) {
586
+ captureException(e);
587
+ await closeBudget();
588
+ return { error: 'opening-budget' };
589
+ }
590
+
591
+ // This is a bit leaky, but we need to set the initial budget type
592
+ const { value: budgetType = 'envelope' } =
593
+ (await db.first<Pick<db.DbPreference, 'value'>>(
594
+ 'SELECT value from preferences WHERE id = ?',
595
+ ['budgetType'],
596
+ )) ?? {};
597
+ sheet.get().meta().budgetType = budgetType as prefs.BudgetType;
598
+ await budget.createAllBudgets();
599
+
600
+ // Load all the in-memory state
601
+ await mappings.loadMappings();
602
+ await rules.loadRules();
603
+ syncMigrations.listen();
604
+ mainApp.startServices();
605
+
606
+ clearUndo();
607
+
608
+ // Ensure that syncing is enabled
609
+ if (process.env.NODE_ENV !== 'test') {
610
+ if (id === DEMO_BUDGET_ID) {
611
+ setSyncingMode('disabled');
612
+ } else {
613
+ if (getServer()) {
614
+ setSyncingMode('enabled');
615
+ } else {
616
+ setSyncingMode('disabled');
617
+ }
618
+
619
+ await asyncStorage.setItem('lastBudget', id);
620
+
621
+ await cloudStorage.possiblyUpload();
622
+ }
623
+ } else {
624
+ // we're in a test - disable the sync
625
+ setSyncingMode('disabled');
626
+ }
627
+
628
+ app.events.emit('load-budget', { id });
629
+
630
+ return {};
631
+ }
632
+
633
+ async function uploadFileWeb({
634
+ filename,
635
+ contents,
636
+ }: {
637
+ filename: string;
638
+ contents: ArrayBuffer;
639
+ }) {
640
+ if (!Platform.isBrowser) {
641
+ return null;
642
+ }
643
+
644
+ await fs.writeFile('/uploads/' + filename, contents);
645
+ return {};
646
+ }
647
+
648
+ async function getBackups({ id }) {
649
+ return getAvailableBackups(id);
650
+ }
651
+
652
+ async function loadBackup({ id, backupId }) {
653
+ await _loadBackup(id, backupId);
654
+ }
655
+
656
+ async function makeBackup({ id }) {
657
+ await _makeBackup(id);
658
+ }
659
+
660
+ async function getLastOpenedBackup() {
661
+ const id = await asyncStorage.getItem('lastBudget');
662
+ if (id && id !== '') {
663
+ const budgetDir = fs.getBudgetDir(id);
664
+
665
+ // We never want to give back a budget that does not exist on the
666
+ // filesystem anymore, so first check that it exists
667
+ if (await fs.exists(budgetDir)) {
668
+ return id;
669
+ }
670
+ }
671
+ return null;
672
+ }