@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,126 @@
1
+ // @ts-strict-ignore
2
+ import type { GlobalPrefsJson } from '../../../types/prefs';
3
+ import { getDatabase } from '../indexeddb';
4
+
5
+ import type * as T from './index-types';
6
+
7
+ export const init: T.Init = function () {
8
+ // No need to initialise in the browser
9
+ };
10
+
11
+ export const getItem: T.GetItem = async function (key) {
12
+ const db = await getDatabase();
13
+
14
+ const transaction = db.transaction(['asyncStorage'], 'readonly');
15
+ const objectStore = transaction.objectStore('asyncStorage');
16
+
17
+ return new Promise((resolve, reject) => {
18
+ const req = objectStore.get(key);
19
+ req.onerror = e => reject(e);
20
+ // @ts-expect-error fix me
21
+ req.onsuccess = e => resolve(e.target.result);
22
+ });
23
+ };
24
+
25
+ export const setItem: T.SetItem = async function (key, value) {
26
+ const db = await getDatabase();
27
+
28
+ const transaction = db.transaction(['asyncStorage'], 'readwrite');
29
+ const objectStore = transaction.objectStore('asyncStorage');
30
+
31
+ void new Promise((resolve, reject) => {
32
+ const req = objectStore.put(value, key);
33
+ req.onerror = e => reject(e);
34
+ req.onsuccess = () => resolve(undefined);
35
+ transaction.commit();
36
+ });
37
+ };
38
+
39
+ export const removeItem: T.RemoveItem = async function (key) {
40
+ const db = await getDatabase();
41
+
42
+ const transaction = db.transaction(['asyncStorage'], 'readwrite');
43
+ const objectStore = transaction.objectStore('asyncStorage');
44
+
45
+ return new Promise((resolve, reject) => {
46
+ const req = objectStore.delete(key);
47
+ req.onerror = e => reject(e);
48
+ req.onsuccess = () => resolve(undefined);
49
+ transaction.commit();
50
+ });
51
+ };
52
+
53
+ export async function multiGet<K extends readonly (keyof GlobalPrefsJson)[]>(
54
+ keys: K,
55
+ ): Promise<{ [P in K[number]]: GlobalPrefsJson[P] }> {
56
+ const db = await getDatabase();
57
+
58
+ const transaction = db.transaction(['asyncStorage'], 'readonly');
59
+ const objectStore = transaction.objectStore('asyncStorage');
60
+
61
+ const results = await Promise.all(
62
+ keys.map(key => {
63
+ return new Promise<[K[number], GlobalPrefsJson[K[number]]]>(
64
+ (resolve, reject) => {
65
+ const req = objectStore.get(key);
66
+ req.onerror = e => reject(e);
67
+ req.onsuccess = e => {
68
+ const target = e.target as IDBRequest<GlobalPrefsJson[K[number]]>;
69
+ resolve([key, target.result]);
70
+ };
71
+ },
72
+ );
73
+ }),
74
+ );
75
+
76
+ transaction.commit();
77
+
78
+ // Convert the array of tuples to an object with properly typed properties
79
+ return results.reduce(
80
+ (acc, [key, value]) => {
81
+ acc[key] = value;
82
+ return acc;
83
+ },
84
+ {} as { [P in K[number]]: GlobalPrefsJson[P] },
85
+ );
86
+ }
87
+
88
+ export const multiSet: T.MultiSet = async function (keyValues) {
89
+ const db = await getDatabase();
90
+
91
+ const transaction = db.transaction(['asyncStorage'], 'readwrite');
92
+ const objectStore = transaction.objectStore('asyncStorage');
93
+
94
+ const promise = Promise.all(
95
+ keyValues.map(([key, value]) => {
96
+ return new Promise((resolve, reject) => {
97
+ const req = objectStore.put(value, key);
98
+ req.onerror = e => reject(e);
99
+ req.onsuccess = () => resolve(undefined);
100
+ });
101
+ }),
102
+ );
103
+
104
+ transaction.commit();
105
+ await promise;
106
+ };
107
+
108
+ export const multiRemove: T.MultiRemove = async function (keys) {
109
+ const db = await getDatabase();
110
+
111
+ const transaction = db.transaction(['asyncStorage'], 'readwrite');
112
+ const objectStore = transaction.objectStore('asyncStorage');
113
+
114
+ const promise = Promise.all(
115
+ keys.map(key => {
116
+ return new Promise((resolve, reject) => {
117
+ const req = objectStore.delete(key);
118
+ req.onerror = e => reject(e);
119
+ req.onsuccess = () => resolve(undefined);
120
+ });
121
+ }),
122
+ );
123
+
124
+ transaction.commit();
125
+ await promise;
126
+ };
@@ -0,0 +1,3 @@
1
+ # Server-side connection package
2
+
3
+ This is the server counterpart of the `connection` client-side package (packages/loot-core/src/platform/client/connection). This contains methods to receive, reply, and push messages from/to the client web application.
@@ -0,0 +1,15 @@
1
+ import type * as T from '../index-types';
2
+
3
+ let events = [];
4
+
5
+ export const init: T.Init = function () {
6
+ // No need to initialise in tests
7
+ };
8
+
9
+ export const send: T.Send = function (type, args) {
10
+ events.push([type, args]);
11
+ };
12
+
13
+ export const resetEvents: T.ResetEvents = function () {
14
+ events = [];
15
+ };
@@ -0,0 +1,20 @@
1
+ import type { Handlers } from '../../../types/handlers';
2
+ import type { ServerEvents } from '../../../types/server-events';
3
+
4
+ export declare function init(
5
+ channel: Window | number, // in electron the port number, in web the worker
6
+ handlers: Handlers,
7
+ ): void;
8
+ export type Init = typeof init;
9
+
10
+ export declare function send<K extends keyof ServerEvents>(
11
+ type: K,
12
+ args?: ServerEvents[K],
13
+ ): void;
14
+ export type Send = typeof send;
15
+
16
+ export declare function getNumClients(): number;
17
+ export type GetNumClients = typeof getNumClients;
18
+
19
+ export declare function resetEvents(): void;
20
+ export type ResetEvents = typeof resetEvents;
@@ -0,0 +1,13 @@
1
+ import type * as T from './index-types';
2
+
3
+ export const init: T.Init = function () {
4
+ // Nothing
5
+ };
6
+
7
+ export const send: T.Send = function () {
8
+ // Nothing
9
+ };
10
+
11
+ export const getNumClients: T.GetNumClients = function () {
12
+ return 1;
13
+ };
@@ -0,0 +1,102 @@
1
+ // @ts-strict-ignore
2
+ import { APIError } from '../../../server/errors';
3
+ import { isMutating, runHandler } from '../../../server/mutators';
4
+ import { captureException } from '../../exceptions';
5
+ import { logger } from '../log';
6
+
7
+ import type * as T from './index-types';
8
+
9
+ function coerceError(error) {
10
+ if (error.type && error.type === 'APIError') {
11
+ return error;
12
+ }
13
+
14
+ return { type: 'ServerError', message: error.message, cause: error };
15
+ }
16
+
17
+ export const init: T.Init = function (_socketName, handlers) {
18
+ process.parentPort.on('message', ({ data }) => {
19
+ const { id, name, args, undoTag, catchErrors } = data;
20
+
21
+ if (handlers[name]) {
22
+ runHandler(handlers[name], args, { undoTag, name }).then(
23
+ result => {
24
+ if (catchErrors) {
25
+ result = { data: result, error: null };
26
+ }
27
+
28
+ process.parentPort.postMessage({
29
+ type: 'reply',
30
+ id,
31
+ result,
32
+ mutated:
33
+ isMutating(handlers[name]) && name !== 'undo' && name !== 'redo',
34
+ undoTag,
35
+ });
36
+ },
37
+ nativeError => {
38
+ const error = coerceError(nativeError);
39
+
40
+ if (name.startsWith('api/')) {
41
+ // The API is newer and does automatically forward
42
+ // errors
43
+ process.parentPort.postMessage({
44
+ type: 'reply',
45
+ id,
46
+ error,
47
+ });
48
+ } else if (catchErrors) {
49
+ process.parentPort.postMessage({
50
+ type: 'reply',
51
+ id,
52
+ result: { error, data: null },
53
+ });
54
+ } else {
55
+ process.parentPort.postMessage({ type: 'error', id, error });
56
+ }
57
+
58
+ if (error.type === 'ServerError' && name !== 'api/load-budget') {
59
+ captureException(nativeError);
60
+ }
61
+
62
+ if (!catchErrors) {
63
+ // Notify the frontend that something bad happend
64
+ send('server-error');
65
+ }
66
+ },
67
+ );
68
+ } else {
69
+ logger.error('Unknown server method: ' + name);
70
+ captureException(new Error('Unknown server method: ' + name));
71
+ const unknownMethodError = APIError('Unknown server method: ' + name);
72
+
73
+ if (catchErrors) {
74
+ process.parentPort.postMessage({
75
+ type: 'reply',
76
+ id,
77
+ result: catchErrors
78
+ ? { error: unknownMethodError, data: null }
79
+ : null,
80
+ });
81
+ } else {
82
+ process.parentPort.postMessage({
83
+ type: 'error',
84
+ id,
85
+ error: unknownMethodError,
86
+ });
87
+ }
88
+ }
89
+ });
90
+ };
91
+
92
+ export const getNumClients: T.GetNumClients = function () {
93
+ return 0;
94
+ };
95
+
96
+ export const send: T.Send = function (name, args) {
97
+ process.parentPort.postMessage({ type: 'push', name, args });
98
+ };
99
+
100
+ export const resetEvents: T.ResetEvents = function () {
101
+ // resetEvents is used in tests to mock the server
102
+ };
@@ -0,0 +1,154 @@
1
+ // @ts-strict-ignore
2
+ import { APIError } from '../../../server/errors';
3
+ import { isMutating, runHandler } from '../../../server/mutators';
4
+ import { captureException } from '../../exceptions';
5
+ import { logger } from '../log';
6
+
7
+ import type * as T from './index-types';
8
+
9
+ function getGlobalObject() {
10
+ const obj =
11
+ typeof window !== 'undefined'
12
+ ? window
13
+ : typeof self !== 'undefined'
14
+ ? self
15
+ : null;
16
+ if (!obj) {
17
+ throw new Error('Cannot get global object');
18
+ }
19
+ return obj as unknown as typeof globalThis & {
20
+ __globalServerChannel: Window | null;
21
+ };
22
+ }
23
+
24
+ getGlobalObject().__globalServerChannel = null;
25
+
26
+ function coerceError(error) {
27
+ if (error.type && error.type === 'APIError') {
28
+ return error;
29
+ }
30
+
31
+ return { type: 'ServerError', message: error.message, cause: error };
32
+ }
33
+
34
+ export const init: T.Init = function (serverChn, handlers) {
35
+ const serverChannel = serverChn as Window;
36
+ getGlobalObject().__globalServerChannel = serverChannel;
37
+
38
+ serverChannel.addEventListener(
39
+ 'message',
40
+ e => {
41
+ const data = e.data;
42
+ const msg = typeof data === 'string' ? JSON.parse(data) : data;
43
+
44
+ if (msg.type && (msg.type === 'init' || msg.type.startsWith('__'))) {
45
+ return;
46
+ }
47
+
48
+ if (msg.name === 'client-connected-to-backend') {
49
+ // the client is indicating that it is connected to this backend. Stop attempting to connect
50
+ logger.info('Backend: Client connected');
51
+ clearInterval(reconnectToClientInterval);
52
+ return;
53
+ }
54
+
55
+ const { id, name, args, undoTag, catchErrors } = msg;
56
+
57
+ if (handlers[name]) {
58
+ runHandler(handlers[name], args, { undoTag, name }).then(
59
+ result => {
60
+ serverChannel.postMessage({
61
+ type: 'reply',
62
+ id,
63
+ result: catchErrors ? { data: result, error: null } : result,
64
+ mutated: isMutating(handlers[name]),
65
+ undoTag,
66
+ });
67
+ },
68
+ nativeError => {
69
+ const error = coerceError(nativeError);
70
+
71
+ if (name.startsWith('api/')) {
72
+ // The API is newer and does automatically forward
73
+ // errors
74
+ serverChannel.postMessage({ type: 'reply', id, error });
75
+ } else if (catchErrors) {
76
+ serverChannel.postMessage({
77
+ type: 'reply',
78
+ id,
79
+ result: { error, data: null },
80
+ });
81
+ } else {
82
+ serverChannel.postMessage({ type: 'error', id, error });
83
+ }
84
+
85
+ // Only report internal errors
86
+ if (error.type === 'ServerError') {
87
+ captureException(nativeError);
88
+ }
89
+
90
+ if (!catchErrors) {
91
+ // Notify the frontend that something bad happend
92
+ send('server-error');
93
+ }
94
+ },
95
+ );
96
+ } else {
97
+ logger.error('Unknown server method: ' + name);
98
+ captureException(new Error('Unknown server method: ' + name));
99
+ const unknownMethodError = APIError('Unknown server method: ' + name);
100
+
101
+ if (catchErrors) {
102
+ serverChannel.postMessage({
103
+ type: 'reply',
104
+ id,
105
+ result: catchErrors
106
+ ? { error: unknownMethodError, data: null }
107
+ : null,
108
+ });
109
+ } else {
110
+ serverChannel.postMessage({
111
+ type: 'error',
112
+ id,
113
+ error: unknownMethodError,
114
+ });
115
+ }
116
+ }
117
+ },
118
+ false,
119
+ );
120
+
121
+ const RECONNECT_INTERVAL_MS = 200;
122
+ const MAX_RECONNECT_ATTEMPTS = 500;
123
+ let reconnectAttempts = 0;
124
+
125
+ const reconnectToClientInterval = setInterval(() => {
126
+ logger.info('Backend: Trying to connect to client');
127
+ serverChannel.postMessage({ type: 'connect' });
128
+ reconnectAttempts++;
129
+ if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
130
+ // Failed to connect to client - signal server error
131
+ send('server-error');
132
+ clearInterval(reconnectToClientInterval);
133
+ }
134
+ }, RECONNECT_INTERVAL_MS);
135
+ };
136
+
137
+ export const send: T.Send = function (name, args) {
138
+ const { __globalServerChannel } = getGlobalObject();
139
+ if (__globalServerChannel) {
140
+ __globalServerChannel.postMessage({
141
+ type: 'push',
142
+ name,
143
+ args,
144
+ });
145
+ }
146
+ };
147
+
148
+ export const getNumClients = function () {
149
+ return 1;
150
+ };
151
+
152
+ export const resetEvents: T.ResetEvents = function () {
153
+ // resetEvents is used in tests to mock the server
154
+ };
@@ -0,0 +1,3 @@
1
+ export const fetch = function () {
2
+ throw new Error('fetch not implemented');
3
+ };
@@ -0,0 +1 @@
1
+ export const fetch = globalThis.fetch;
@@ -0,0 +1,18 @@
1
+ import { logger } from '../log';
2
+
3
+ import type * as T from './index';
4
+
5
+ export const fetch: typeof T.fetch = async (input, options) => {
6
+ try {
7
+ return await globalThis.fetch(input, {
8
+ ...options,
9
+ headers: {
10
+ ...options?.headers,
11
+ origin: 'app://actual',
12
+ },
13
+ });
14
+ } catch (error) {
15
+ logger.error(error); // log error
16
+ throw error;
17
+ }
18
+ };
@@ -0,0 +1,20 @@
1
+ import * as connection from '../connection';
2
+
3
+ export const fetch = async (
4
+ input: RequestInfo | URL,
5
+ options: RequestInit = {},
6
+ ): Promise<Response> => {
7
+ // Set redirect to manual so that we can detect and respond to redirects.
8
+ if (!options.redirect) options.redirect = 'manual';
9
+
10
+ const response = await globalThis.fetch(input, options);
11
+
12
+ // Authentication proxies redirect when authentication has expired. In this case,
13
+ // we want to fully reload and yeild control from the service worker back to the server.
14
+ if (response.type === 'opaqueredirect') {
15
+ connection.send('api-fetch-redirected');
16
+ throw new Error(`API request redirected`);
17
+ }
18
+
19
+ return response;
20
+ };
@@ -0,0 +1,198 @@
1
+ // @ts-strict-ignore
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ import promiseRetry from 'promise-retry';
6
+
7
+ import { logger } from '../log';
8
+
9
+ import type * as T from './index';
10
+
11
+ export { getDocumentDir, getBudgetDir, _setDocumentDir } from './shared';
12
+
13
+ export const init: typeof T.init = async () => {
14
+ // Nothing to do
15
+ };
16
+
17
+ export const getDataDir: typeof T.getDataDir = () => {
18
+ if (!process.env.ACTUAL_DATA_DIR) {
19
+ throw new Error('ACTUAL_DATA_DIR env variable is required');
20
+ }
21
+ return process.env.ACTUAL_DATA_DIR;
22
+ };
23
+
24
+ export const bundledDatabasePath: typeof T.bundledDatabasePath = path.join(
25
+ __dirname,
26
+ 'default-db.sqlite',
27
+ );
28
+
29
+ export const migrationsPath: typeof T.migrationsPath = path.join(
30
+ __dirname,
31
+ 'migrations',
32
+ );
33
+
34
+ export const demoBudgetPath: typeof T.demoBudgetPath = path.join(
35
+ __dirname,
36
+ 'demo-budget',
37
+ );
38
+
39
+ export const join: typeof T.join = (...args: Parameters<typeof path.join>) =>
40
+ path.join(...args);
41
+
42
+ export const basename: typeof T.basename = filepath => path.basename(filepath);
43
+
44
+ export const listDir: typeof T.listDir = filepath =>
45
+ new Promise((resolve, reject) => {
46
+ fs.readdir(filepath, (err, files) => {
47
+ if (err) {
48
+ reject(err);
49
+ } else {
50
+ resolve(files);
51
+ }
52
+ });
53
+ });
54
+
55
+ export const exists: typeof T.exists = filepath =>
56
+ new Promise(resolve => {
57
+ fs.access(filepath, fs.constants.F_OK, err => {
58
+ return resolve(!err);
59
+ });
60
+ });
61
+
62
+ export const mkdir: typeof T.mkdir = filepath =>
63
+ new Promise((resolve, reject) => {
64
+ fs.mkdir(filepath, err => {
65
+ if (err) {
66
+ reject(err);
67
+ } else {
68
+ resolve(undefined);
69
+ }
70
+ });
71
+ });
72
+
73
+ export const size: typeof T.size = filepath =>
74
+ new Promise((resolve, reject) => {
75
+ fs.stat(filepath, (err, stats) => {
76
+ if (err) {
77
+ reject(err);
78
+ } else {
79
+ resolve(stats.size);
80
+ }
81
+ });
82
+ });
83
+
84
+ export const copyFile: typeof T.copyFile = (frompath, topath) => {
85
+ return new Promise<boolean>((resolve, reject) => {
86
+ const readStream = fs.createReadStream(frompath);
87
+ const writeStream = fs.createWriteStream(topath);
88
+
89
+ readStream.on('error', reject);
90
+ writeStream.on('error', reject);
91
+
92
+ writeStream.on('open', () => readStream.pipe(writeStream));
93
+ writeStream.once('close', () => resolve(true));
94
+ });
95
+ };
96
+
97
+ export const readFile: typeof T.readFile = (
98
+ filepath: string,
99
+ encoding: 'utf8' | 'binary' | null = 'utf8',
100
+ ) => {
101
+ if (encoding === 'binary') {
102
+ // `binary` is not actually a valid encoding, you pass `null` into node if
103
+ // you want a buffer
104
+ encoding = null;
105
+ }
106
+ // `any` as cannot refine return with two function overrides
107
+ // oxlint-disable-next-line typescript/no-explicit-any
108
+ return new Promise<any>((resolve, reject) => {
109
+ fs.readFile(filepath, encoding, (err, data) => {
110
+ if (err) {
111
+ reject(err);
112
+ } else {
113
+ resolve(data);
114
+ }
115
+ });
116
+ });
117
+ };
118
+
119
+ export const writeFile: typeof T.writeFile = async (filepath, contents) => {
120
+ try {
121
+ await promiseRetry(
122
+ (retry, attempt) => {
123
+ return new Promise((resolve, reject) => {
124
+ fs.writeFile(filepath, contents, 'utf8', err => {
125
+ if (err) {
126
+ logger.error(
127
+ `Failed to write to ${filepath}. Attempted ${attempt} times. Something is locking the file - potentially a virus scanner or backup software.`,
128
+ );
129
+ reject(err);
130
+ } else {
131
+ if (attempt > 1) {
132
+ logger.info(
133
+ `Successfully recovered from file lock. It took ${attempt} retries`,
134
+ );
135
+ }
136
+ resolve(undefined);
137
+ }
138
+ });
139
+ }).catch(retry);
140
+ },
141
+ {
142
+ retries: 20,
143
+ minTimeout: 100,
144
+ maxTimeout: 500,
145
+ factor: 1.5,
146
+ },
147
+ );
148
+
149
+ return undefined;
150
+ } catch (err) {
151
+ logger.error(`Unable to recover from file lock on file ${filepath}`);
152
+ throw err;
153
+ }
154
+ };
155
+
156
+ export const removeFile: typeof T.removeFile = filepath => {
157
+ return new Promise(function (resolve, reject) {
158
+ fs.unlink(filepath, err => {
159
+ return err ? reject(err) : resolve(undefined);
160
+ });
161
+ });
162
+ };
163
+
164
+ export const removeDir: typeof T.removeDir = dirpath => {
165
+ return new Promise(function (resolve, reject) {
166
+ fs.rmdir(dirpath, err => {
167
+ return err ? reject(err) : resolve(undefined);
168
+ });
169
+ });
170
+ };
171
+
172
+ export const removeDirRecursively: typeof T.removeDirRecursively =
173
+ async dirpath => {
174
+ if (await exists(dirpath)) {
175
+ for (const file of await listDir(dirpath)) {
176
+ const fullpath = join(dirpath, file);
177
+ if (fs.statSync(fullpath).isDirectory()) {
178
+ await removeDirRecursively(fullpath);
179
+ } else {
180
+ await removeFile(fullpath);
181
+ }
182
+ }
183
+
184
+ await removeDir(dirpath);
185
+ }
186
+ };
187
+
188
+ export const getModifiedTime: typeof T.getModifiedTime = filepath => {
189
+ return new Promise(function (resolve, reject) {
190
+ fs.stat(filepath, (err, stats) => {
191
+ if (err) {
192
+ reject(err);
193
+ } else {
194
+ resolve(new Date(stats.mtime));
195
+ }
196
+ });
197
+ });
198
+ };