@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,416 @@
1
+ // @ts-strict-ignore
2
+ import { SQLiteFS } from 'absurd-sql';
3
+ import IndexedDBBackend from 'absurd-sql/dist/indexeddb-backend';
4
+
5
+ import * as connection from '../connection';
6
+ import * as idb from '../indexeddb';
7
+ import { logger } from '../log';
8
+ import { _getModule } from '../sqlite';
9
+ import type { SqlJsModule } from '../sqlite';
10
+
11
+ import { join } from './path-join';
12
+
13
+ let FS: SqlJsModule['FS'] = null;
14
+ let BFS = null;
15
+ const NO_PERSIST = false;
16
+
17
+ export const bundledDatabasePath: string = '/default-db.sqlite';
18
+ export const migrationsPath: string = '/migrations';
19
+ export const demoBudgetPath: string = '/demo-budget';
20
+ export { join };
21
+ export { getDocumentDir, getBudgetDir, _setDocumentDir } from './shared';
22
+ export const getDataDir = () => process.env.ACTUAL_DATA_DIR;
23
+
24
+ export const pathToId = function (filepath: string): string {
25
+ return filepath.replace(/^\//, '').replace(/\//g, '-');
26
+ };
27
+
28
+ function _exists(filepath: string): boolean {
29
+ try {
30
+ FS.readlink(filepath);
31
+ return true;
32
+ } catch {}
33
+
34
+ try {
35
+ FS.stat(filepath);
36
+ return true;
37
+ } catch {}
38
+ return false;
39
+ }
40
+
41
+ function _mkdirRecursively(dir) {
42
+ const parts = dir.split('/').filter(str => str !== '');
43
+ let path = '';
44
+ for (const part of parts) {
45
+ path += '/' + part;
46
+ if (!_exists(path)) {
47
+ FS.mkdir(path);
48
+ }
49
+ }
50
+ }
51
+
52
+ function _createFile(filepath: string) {
53
+ // This can create the file. Check if it exists, if not create a
54
+ // symlink if it's a sqlite file. Otherwise store in idb
55
+
56
+ if (!NO_PERSIST && filepath.startsWith('/documents')) {
57
+ if (filepath.endsWith('.sqlite')) {
58
+ // If it doesn't exist, we need to create a symlink
59
+ if (!_exists(filepath)) {
60
+ FS.symlink('/blocked/' + pathToId(filepath), filepath);
61
+ }
62
+ } else {
63
+ // The contents are actually stored in IndexedDB. We only write to
64
+ // the in-memory fs to take advantage of the file hierarchy
65
+ FS.writeFile(filepath, '!$@) this should never read !$@)');
66
+ }
67
+ }
68
+
69
+ return filepath;
70
+ }
71
+
72
+ async function _readFile(
73
+ filepath: string,
74
+ opts: { encoding: 'utf8' },
75
+ ): Promise<string>;
76
+ async function _readFile(
77
+ filepath: string,
78
+ opts?: { encoding: 'binary' },
79
+ ): Promise<Uint8Array>;
80
+ async function _readFile(
81
+ filepath: string,
82
+ opts?: { encoding: 'utf8' } | { encoding: 'binary' },
83
+ ): Promise<string | Uint8Array> {
84
+ // We persist stuff in /documents, but don't need to handle sqlite
85
+ // file specifically because those are symlinked to a separate
86
+ // filesystem and will be handled in the BlockedFS
87
+ if (
88
+ !NO_PERSIST &&
89
+ filepath.startsWith('/documents') &&
90
+ !filepath.endsWith('.sqlite')
91
+ ) {
92
+ if (!_exists(filepath)) {
93
+ throw new Error('File does not exist: ' + filepath);
94
+ }
95
+
96
+ // Grab contents from IDB
97
+ const { store } = idb.getStore(await idb.getDatabase(), 'files');
98
+ const item = await idb.get(store, filepath);
99
+
100
+ if (item == null) {
101
+ throw new Error('File does not exist: ' + filepath);
102
+ }
103
+
104
+ if (opts?.encoding === 'utf8' && ArrayBuffer.isView(item.contents)) {
105
+ return String.fromCharCode.apply(
106
+ null,
107
+ new Uint16Array(item.contents.buffer),
108
+ );
109
+ }
110
+
111
+ return item.contents;
112
+ } else {
113
+ if (opts?.encoding === 'utf8') {
114
+ return FS.readFile(resolveLink(filepath), { encoding: 'utf8' });
115
+ } else if (opts?.encoding === 'binary') {
116
+ return FS.readFile(resolveLink(filepath), { encoding: 'binary' });
117
+ } else {
118
+ return FS.readFile(resolveLink(filepath));
119
+ }
120
+ }
121
+ }
122
+
123
+ function resolveLink(path: string): string {
124
+ try {
125
+ const { node } = FS.lookupPath(path, { follow: false });
126
+ return node.link ? FS.readlink(path) : path;
127
+ } catch {
128
+ return path;
129
+ }
130
+ }
131
+
132
+ async function _writeFile(filepath: string, contents): Promise<boolean> {
133
+ if (contents instanceof ArrayBuffer) {
134
+ contents = new Uint8Array(contents);
135
+ } else if (ArrayBuffer.isView(contents)) {
136
+ contents = new Uint8Array(contents.buffer);
137
+ }
138
+
139
+ // We always create the file if it doesn't exist, and this function
140
+ // setups up the file depending on its type
141
+ _createFile(filepath);
142
+
143
+ if (!NO_PERSIST && filepath.startsWith('/documents')) {
144
+ const isDb = filepath.endsWith('.sqlite');
145
+
146
+ // Write to IDB
147
+ const { store } = idb.getStore(await idb.getDatabase(), 'files');
148
+
149
+ if (isDb) {
150
+ // We never write the contents of the database to idb ourselves.
151
+ // It gets handled via a symlink to the blocked fs (created by
152
+ // `_createFile` above). However, we still need to record an
153
+ // entry for the db file so the fs gets properly constructed on
154
+ // startup
155
+ await idb.set(store, { filepath, contents: '' });
156
+
157
+ // Actually persist the data by going the FS, which will pass
158
+ // the data through the symlink to the blocked fs. For some
159
+ // reason we need to resolve symlinks ourselves.
160
+ await Promise.resolve();
161
+ FS.writeFile(resolveLink(filepath), contents);
162
+ } else {
163
+ await idb.set(store, { filepath, contents });
164
+ }
165
+ } else {
166
+ FS.writeFile(resolveLink(filepath), contents);
167
+ }
168
+ return true;
169
+ }
170
+
171
+ async function _copySqlFile(
172
+ frompath: string,
173
+ topath: string,
174
+ ): Promise<boolean> {
175
+ _createFile(topath);
176
+
177
+ const { store } = idb.getStore(await idb.getDatabase(), 'files');
178
+ await idb.set(store, { filepath: topath, contents: '' });
179
+ const fromitem = await idb.get(store, frompath);
180
+ const fromDbPath = pathToId(fromitem.filepath);
181
+ const toDbPath = pathToId(topath);
182
+
183
+ const fromfile = BFS.backend.createFile(fromDbPath);
184
+ const tofile = BFS.backend.createFile(toDbPath);
185
+
186
+ try {
187
+ fromfile.open();
188
+ tofile.open();
189
+ const fileSize = fromfile.meta.size;
190
+ const blockSize = fromfile.meta.blockSize;
191
+
192
+ const buffer = new ArrayBuffer(blockSize);
193
+ const bufferView = new Uint8Array(buffer);
194
+
195
+ for (let i = 0; i < fileSize; i += blockSize) {
196
+ const bytesToRead = Math.min(blockSize, fileSize - i);
197
+ fromfile.read(bufferView, 0, bytesToRead, i);
198
+ tofile.write(bufferView, 0, bytesToRead, i);
199
+ }
200
+ } catch (error) {
201
+ tofile.close();
202
+ fromfile.close();
203
+ await _removeFile(toDbPath);
204
+ logger.error('Failed to copy database file', error);
205
+ return false;
206
+ } finally {
207
+ tofile.close();
208
+ fromfile.close();
209
+ }
210
+
211
+ return true;
212
+ }
213
+
214
+ async function _removeFile(filepath: string) {
215
+ if (!NO_PERSIST && filepath.startsWith('/documents')) {
216
+ const isDb = filepath.endsWith('.sqlite');
217
+
218
+ // Remove from IDB
219
+ const { store } = idb.getStore(await idb.getDatabase(), 'files');
220
+ await idb.del(store, filepath);
221
+
222
+ // If this is the database, is has been symlinked and we want to
223
+ // remove the actual contents
224
+ if (isDb) {
225
+ const linked = resolveLink(filepath);
226
+ // Be resilient to fs corruption: don't throw an error by trying
227
+ // to remove a file that doesn't exist. For some reason the db
228
+ // file is gone? It's ok, just ignore it
229
+ if (_exists(linked)) {
230
+ FS.unlink(linked);
231
+ }
232
+ }
233
+ }
234
+
235
+ // Finally, remove any in-memory instance
236
+ FS.unlink(filepath);
237
+ }
238
+
239
+ // Load files from the server that should exist by default
240
+ async function populateDefaultFilesystem() {
241
+ const index = await (
242
+ await fetch(process.env.PUBLIC_URL + 'data-file-index.txt')
243
+ ).text();
244
+ const files = index
245
+ .split('\n')
246
+ .map(name => name.trim())
247
+ .filter(name => name !== '');
248
+ const fetchFile = url => fetch(url).then(res => res.arrayBuffer());
249
+
250
+ // This is hardcoded. We know we must create the migrations
251
+ // directory, it's not worth complicating the index to support
252
+ // creating arbitrary folders.
253
+ await mkdir('/migrations');
254
+ await mkdir('/demo-budget');
255
+
256
+ await Promise.all(
257
+ files.map(async file => {
258
+ const contents = await fetchFile(process.env.PUBLIC_URL + 'data/' + file);
259
+ await _writeFile('/' + file, contents);
260
+ }),
261
+ );
262
+ }
263
+
264
+ const populateFileHierarchy = async function () {
265
+ const { store } = idb.getStore(await idb.getDatabase(), 'files');
266
+ const req = store.getAllKeys();
267
+ const paths: string[] = await new Promise((resolve, reject) => {
268
+ // @ts-expect-error fix me
269
+ req.onsuccess = e => resolve(e.target.result);
270
+ req.onerror = e => reject(e);
271
+ });
272
+
273
+ for (const path of paths) {
274
+ _mkdirRecursively(basename(path));
275
+ _createFile(path);
276
+ }
277
+ };
278
+
279
+ export const init = async function () {
280
+ const Module = _getModule();
281
+ FS = Module.FS;
282
+
283
+ // When a user "uploads" a file, we just put it in memory in this
284
+ // dir and the backend takes it from there
285
+ FS.mkdir('/uploads');
286
+
287
+ // Files in /documents are actually read/written from idb.
288
+ // Everything in there is automatically persisted
289
+ FS.mkdir('/documents');
290
+
291
+ // Files in /blocked are handled by the BlockedFS, which is a
292
+ // special fs that persists files in blocks. This is necessary
293
+ // for sqlite3
294
+ FS.mkdir('/blocked');
295
+
296
+ // Jest doesn't support workers. Right now we disable the blocked fs
297
+ // backend under testing and just test that the directory structure
298
+ // is created correctly. We assume the the absurd-sql project tests
299
+ // the blocked fs enough. Additionally, we don't populate the
300
+ // default files in testing.
301
+ if (process.env.NODE_ENV !== 'test') {
302
+ const backend = new IndexedDBBackend(() => {
303
+ connection.send('fallback-write-error');
304
+ });
305
+ BFS = new SQLiteFS(FS, backend);
306
+ Module.register_for_idb(BFS);
307
+
308
+ FS.mount(BFS, {}, '/blocked');
309
+
310
+ await populateDefaultFilesystem();
311
+ }
312
+
313
+ await populateFileHierarchy();
314
+ };
315
+
316
+ export const basename = function (filepath) {
317
+ const parts = filepath.split('/');
318
+ return parts.slice(0, -1).join('/');
319
+ };
320
+
321
+ export const listDir = async function (filepath) {
322
+ const paths = FS.readdir(filepath);
323
+ return paths.filter(p => p !== '.' && p !== '..');
324
+ };
325
+
326
+ export const exists = async function (filepath) {
327
+ return _exists(filepath);
328
+ };
329
+
330
+ export const mkdir = async function (filepath) {
331
+ FS.mkdir(filepath);
332
+ };
333
+
334
+ export const size = async function (filepath) {
335
+ const attrs = FS.stat(resolveLink(filepath));
336
+ return attrs.size;
337
+ };
338
+
339
+ export const copyFile = async function (
340
+ frompath: string,
341
+ topath: string,
342
+ ): Promise<boolean> {
343
+ let result = false;
344
+ try {
345
+ const contents = await _readFile(frompath);
346
+ result = await _writeFile(topath, contents);
347
+ } catch (error) {
348
+ if (frompath.endsWith('.sqlite') || topath.endsWith('.sqlite')) {
349
+ try {
350
+ result = await _copySqlFile(frompath, topath);
351
+ } catch (secondError) {
352
+ throw new Error(
353
+ `Failed to copy SQL file from ${frompath} to ${topath}: ${secondError.message}`,
354
+ );
355
+ }
356
+ } else {
357
+ throw error;
358
+ }
359
+ }
360
+ return result;
361
+ };
362
+
363
+ export async function readFile(
364
+ filepath: string,
365
+ encoding?: 'utf8',
366
+ ): Promise<string>;
367
+ export async function readFile(
368
+ filepath: string,
369
+ encoding: 'binary',
370
+ ): Promise<Uint8Array>;
371
+ export async function readFile(
372
+ filepath: string,
373
+ encoding: 'binary' | 'utf8' = 'utf8',
374
+ ) {
375
+ if (encoding === 'utf8') {
376
+ return _readFile(filepath, { encoding });
377
+ }
378
+
379
+ return _readFile(filepath, { encoding });
380
+ }
381
+
382
+ export const writeFile = async function (filepath: string, contents) {
383
+ return _writeFile(filepath, contents);
384
+ };
385
+
386
+ export const removeFile = async function (filepath: string) {
387
+ return _removeFile(filepath);
388
+ };
389
+
390
+ export const removeDir = async function (filepath) {
391
+ FS.rmdir(filepath);
392
+ };
393
+
394
+ export const removeDirRecursively = async function (dirpath) {
395
+ if (await exists(dirpath)) {
396
+ for (const file of await listDir(dirpath)) {
397
+ const fullpath = join(dirpath, file);
398
+ // `true` here means to not follow symlinks
399
+ const attr = FS.stat(fullpath, true);
400
+
401
+ if (FS.isDir(attr.mode)) {
402
+ await removeDirRecursively(fullpath);
403
+ } else {
404
+ await removeFile(fullpath);
405
+ }
406
+ }
407
+
408
+ await removeDir(dirpath);
409
+ }
410
+ };
411
+
412
+ export const getModifiedTime = async (filepath: string): Promise<Date> => {
413
+ throw new Error(
414
+ 'getModifiedTime not supported on the web (only used for backups)',
415
+ );
416
+ };
@@ -0,0 +1 @@
1
+ export { join } from 'path';
@@ -0,0 +1 @@
1
+ export { join } from 'path';
@@ -0,0 +1,97 @@
1
+ // @ts-strict-ignore
2
+ // This code is pulled from
3
+ // https://github.com/browserify/path-browserify/blob/master/index.js#L33
4
+
5
+ // Resolves . and .. elements in a path with directory names
6
+ function normalizeStringPosix(path, allowAboveRoot) {
7
+ let res = '';
8
+ let lastSegmentLength = 0;
9
+ let lastSlash = -1;
10
+ let dots = 0;
11
+ let code;
12
+ for (let i = 0; i <= path.length; ++i) {
13
+ if (i < path.length) code = path.charCodeAt(i);
14
+ else if (code === 47 /*/*/) break;
15
+ else code = 47 /*/*/;
16
+ if (code === 47 /*/*/) {
17
+ if (lastSlash === i - 1 || dots === 1) {
18
+ // NOOP
19
+ } else if (lastSlash !== i - 1 && dots === 2) {
20
+ if (
21
+ res.length < 2 ||
22
+ lastSegmentLength !== 2 ||
23
+ res.charCodeAt(res.length - 1) !== 46 /*.*/ ||
24
+ res.charCodeAt(res.length - 2) !== 46 /*.*/
25
+ ) {
26
+ if (res.length > 2) {
27
+ const lastSlashIndex = res.lastIndexOf('/');
28
+ if (lastSlashIndex !== res.length - 1) {
29
+ if (lastSlashIndex === -1) {
30
+ res = '';
31
+ lastSegmentLength = 0;
32
+ } else {
33
+ res = res.slice(0, lastSlashIndex);
34
+ lastSegmentLength = res.length - 1 - res.lastIndexOf('/');
35
+ }
36
+ lastSlash = i;
37
+ dots = 0;
38
+ continue;
39
+ }
40
+ } else if (res.length === 2 || res.length === 1) {
41
+ res = '';
42
+ lastSegmentLength = 0;
43
+ lastSlash = i;
44
+ dots = 0;
45
+ continue;
46
+ }
47
+ }
48
+ if (allowAboveRoot) {
49
+ if (res.length > 0) res += '/..';
50
+ else res = '..';
51
+ lastSegmentLength = 2;
52
+ }
53
+ } else {
54
+ if (res.length > 0) res += '/' + path.slice(lastSlash + 1, i);
55
+ else res = path.slice(lastSlash + 1, i);
56
+ lastSegmentLength = i - lastSlash - 1;
57
+ }
58
+ lastSlash = i;
59
+ dots = 0;
60
+ } else if (code === 46 /*.*/ && dots !== -1) {
61
+ ++dots;
62
+ } else {
63
+ dots = -1;
64
+ }
65
+ }
66
+ return res;
67
+ }
68
+
69
+ function normalizePath(path) {
70
+ if (path.length === 0) return '.';
71
+
72
+ const isAbsolute = path.charCodeAt(0) === 47; /*/*/
73
+ const trailingSeparator = path.charCodeAt(path.length - 1) === 47; /*/*/
74
+
75
+ // Normalize the path
76
+ path = normalizeStringPosix(path, !isAbsolute);
77
+
78
+ if (path.length === 0 && !isAbsolute) path = '.';
79
+ if (path.length > 0 && trailingSeparator) path += '/';
80
+
81
+ if (isAbsolute) return '/' + path;
82
+ return path;
83
+ }
84
+
85
+ export const join = (...args: string[]) => {
86
+ if (args.length === 0) return '.';
87
+ let joined;
88
+ for (let i = 0; i < args.length; ++i) {
89
+ const arg = args[i];
90
+ if (arg.length > 0) {
91
+ if (joined === undefined) joined = arg;
92
+ else joined += '/' + arg;
93
+ }
94
+ }
95
+ if (joined === undefined) return '.';
96
+ return normalizePath(joined);
97
+ };
@@ -0,0 +1,33 @@
1
+ // @ts-strict-ignore
2
+ import { join } from './path-join';
3
+
4
+ let documentDir;
5
+ export const _setDocumentDir = dir => (documentDir = dir);
6
+
7
+ export const getDocumentDir = () => {
8
+ if (!documentDir) {
9
+ throw new Error('Document directory is not set');
10
+ }
11
+ return documentDir;
12
+ };
13
+
14
+ export const getBudgetDir = id => {
15
+ if (!id) {
16
+ throw new Error('getDocumentDir: id is falsy: ' + id);
17
+ }
18
+
19
+ // TODO: This should be better
20
+ //
21
+ // A cheesy safe guard. The id is generated from the budget name,
22
+ // so it provides an entry point for the user to accidentally (or
23
+ // intentionally) access other parts of the system. Always
24
+ // restrict it to only access files within the budget directory by
25
+ // never allowing slashes.
26
+ if (id.match(/[^A-Za-z0-9\-_]/)) {
27
+ throw new Error(
28
+ `Invalid budget id "${id}". Check the id of your budget in the Advanced section of the settings page.`,
29
+ );
30
+ }
31
+
32
+ return join(getDocumentDir(), id);
33
+ };
@@ -0,0 +1,115 @@
1
+ import { logger } from '../log';
2
+
3
+ let openedDb: null | ReturnType<typeof _openDatabase> = _openDatabase();
4
+
5
+ // The web version uses IndexedDB to store data
6
+ function _openDatabase() {
7
+ return new Promise<IDBDatabase>((resolve, reject) => {
8
+ const dbVersion = 9;
9
+ const openRequest = indexedDB.open('actual', dbVersion);
10
+
11
+ openRequest.onupgradeneeded = function (e) {
12
+ const db = (e.target as IDBOpenDBRequest).result;
13
+
14
+ // Remove old stores
15
+ if (db.objectStoreNames.contains('filesystem')) {
16
+ db.deleteObjectStore('filesystem');
17
+ }
18
+ if (db.objectStoreNames.contains('messages')) {
19
+ db.deleteObjectStore('messages');
20
+ }
21
+
22
+ // Create new stores
23
+ if (!db.objectStoreNames.contains('asyncStorage')) {
24
+ db.createObjectStore('asyncStorage');
25
+ }
26
+ if (!db.objectStoreNames.contains('files')) {
27
+ db.createObjectStore('files', { keyPath: 'filepath' });
28
+ }
29
+ };
30
+
31
+ openRequest.onblocked = e => logger.log('blocked', e);
32
+
33
+ openRequest.onerror = () => {
34
+ logger.log('openRequest error');
35
+ reject(new Error('indexeddb-failure: Could not open IndexedDB'));
36
+ };
37
+
38
+ openRequest.onsuccess = function (e) {
39
+ const db = (e.target as IDBOpenDBRequest).result;
40
+
41
+ db.onversionchange = () => {
42
+ // TODO: Notify the user somehow
43
+ db.close();
44
+ };
45
+
46
+ db.onerror = function (event) {
47
+ const error = (event.target as IDBOpenDBRequest)?.error;
48
+ logger.log('Database error:', error);
49
+
50
+ if (event.target && error) {
51
+ if (error.name === 'QuotaExceededError') {
52
+ throw new Error('indexeddb-quota-error');
53
+ }
54
+ }
55
+ };
56
+ resolve(db);
57
+ };
58
+ });
59
+ }
60
+
61
+ type Data = { filepath: string; contents: string };
62
+
63
+ export const getStore = function (db: IDBDatabase, name: string) {
64
+ const trans = db.transaction([name], 'readwrite');
65
+ return { trans, store: trans.objectStore(name) };
66
+ };
67
+
68
+ export const get = async function (
69
+ store: IDBObjectStore,
70
+ key: IDBValidKey | IDBKeyRange,
71
+ ) {
72
+ return new Promise<Data>((resolve, reject) => {
73
+ const req = store.get(key);
74
+ req.onsuccess = () => {
75
+ resolve(req.result);
76
+ };
77
+ req.onerror = e => reject(e);
78
+ });
79
+ };
80
+
81
+ export const set = async function (store: IDBObjectStore, item: Data) {
82
+ return new Promise((resolve, reject) => {
83
+ const req = store.put(item);
84
+ req.onsuccess = () => resolve(undefined);
85
+ req.onerror = e => reject(e);
86
+ });
87
+ };
88
+
89
+ export const del = async function (store: IDBObjectStore, key: string) {
90
+ return new Promise((resolve, reject) => {
91
+ const req = store.delete(key);
92
+ req.onsuccess = () => resolve(undefined);
93
+ req.onerror = e => reject(e);
94
+ });
95
+ };
96
+
97
+ export const getDatabase = function () {
98
+ return openedDb;
99
+ };
100
+
101
+ export const openDatabase = function () {
102
+ if (openedDb == null) {
103
+ openedDb = _openDatabase();
104
+ }
105
+ return openedDb;
106
+ };
107
+
108
+ export const closeDatabase = async function () {
109
+ if (openedDb) {
110
+ await openedDb.then(db => {
111
+ db.close();
112
+ });
113
+ openedDb = null;
114
+ }
115
+ };
@@ -0,0 +1,43 @@
1
+ let verboseMode = true;
2
+
3
+ export function setVerboseMode(verbose: boolean) {
4
+ verboseMode = verbose;
5
+ }
6
+
7
+ export function isVerboseMode(): boolean {
8
+ return verboseMode;
9
+ }
10
+
11
+ export const logger = {
12
+ info: (...args: unknown[]) => {
13
+ if (verboseMode) {
14
+ console.log(...args);
15
+ }
16
+ },
17
+ warn: (...args: unknown[]) => {
18
+ console.warn(...args);
19
+ },
20
+ log: (...args: unknown[]) => {
21
+ if (verboseMode) {
22
+ console.log(...args);
23
+ }
24
+ },
25
+ error: (...args: unknown[]) => {
26
+ console.error(...args);
27
+ },
28
+ debug: (...args: unknown[]) => {
29
+ if (verboseMode) {
30
+ console.debug(...args);
31
+ }
32
+ },
33
+ group: (...args: unknown[]) => {
34
+ if (verboseMode) {
35
+ console.group(...args);
36
+ }
37
+ },
38
+ groupEnd: () => {
39
+ if (verboseMode) {
40
+ console.groupEnd();
41
+ }
42
+ },
43
+ };