@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,192 @@
1
+ // @ts-strict-ignore
2
+ // We have to bundle in JS migrations manually to avoid having to `eval`
3
+ // them which doesn't play well with CSP. There isn't great, and eventually
4
+ // we can remove this migration.
5
+ import type { Database } from '@jlongster/sql.js';
6
+
7
+ import m1632571489012 from '../../../migrations/1632571489012_remove_cache';
8
+ import m1722717601000 from '../../../migrations/1722717601000_reports_move_selected_categories';
9
+ import m1722804019000 from '../../../migrations/1722804019000_create_dashboard_table';
10
+ import m1723665565000 from '../../../migrations/1723665565000_prefs';
11
+ import m1765518577215 from '../../../migrations/1765518577215_multiple_dashboards';
12
+ import * as fs from '../../platform/server/fs';
13
+ import { logger } from '../../platform/server/log';
14
+ import * as sqlite from '../../platform/server/sqlite';
15
+ import * as prefs from '../prefs';
16
+
17
+ let MIGRATIONS_DIR = fs.migrationsPath;
18
+
19
+ const javascriptMigrations = {
20
+ 1632571489012: m1632571489012,
21
+ 1722717601000: m1722717601000,
22
+ 1722804019000: m1722804019000,
23
+ 1723665565000: m1723665565000,
24
+ 1765518577215: m1765518577215,
25
+ };
26
+
27
+ export async function withMigrationsDir(
28
+ dir: string,
29
+ func: () => Promise<void>,
30
+ ): Promise<void> {
31
+ const oldDir = MIGRATIONS_DIR;
32
+ MIGRATIONS_DIR = dir;
33
+ await func();
34
+ MIGRATIONS_DIR = oldDir;
35
+ }
36
+
37
+ export function getMigrationsDir(): string {
38
+ return MIGRATIONS_DIR;
39
+ }
40
+
41
+ function getMigrationId(name: string): number {
42
+ return parseInt(name.match(/^(\d)+/)[0]);
43
+ }
44
+
45
+ export function getUpMigration(id, names) {
46
+ for (const m of names) {
47
+ if (getMigrationId(m) === id) {
48
+ return m;
49
+ }
50
+ }
51
+ }
52
+
53
+ async function patchBadMigrations(db: Database) {
54
+ const badFiltersMigration = 1685375406832;
55
+ const newFiltersMigration = 1688749527273;
56
+ const appliedIds = await getAppliedMigrations(db);
57
+ if (appliedIds.includes(badFiltersMigration)) {
58
+ sqlite.runQuery(db, 'DELETE FROM __migrations__ WHERE id = ?', [
59
+ badFiltersMigration,
60
+ ]);
61
+ sqlite.runQuery(db, 'INSERT INTO __migrations__ (id) VALUES (?)', [
62
+ newFiltersMigration,
63
+ ]);
64
+ }
65
+ }
66
+
67
+ export async function getAppliedMigrations(db: Database): Promise<number[]> {
68
+ const rows = sqlite.runQuery<{ id: number }>(
69
+ db,
70
+ 'SELECT * FROM __migrations__ ORDER BY id ASC',
71
+ [],
72
+ true,
73
+ );
74
+ return rows.map(row => row.id);
75
+ }
76
+
77
+ export async function getMigrationList(
78
+ migrationsDir: string,
79
+ ): Promise<string[]> {
80
+ const files = await fs.listDir(migrationsDir);
81
+ return files
82
+ .filter(name => name.match(/(\.sql|\.js)$/))
83
+ .sort((m1, m2) => {
84
+ const id1 = getMigrationId(m1);
85
+ const id2 = getMigrationId(m2);
86
+ if (id1 < id2) {
87
+ return -1;
88
+ } else if (id1 > id2) {
89
+ return 1;
90
+ }
91
+ return 0;
92
+ });
93
+ }
94
+
95
+ export function getPending(appliedIds: number[], all: string[]): string[] {
96
+ return all.filter(name => {
97
+ const id = getMigrationId(name);
98
+ return appliedIds.indexOf(id) === -1;
99
+ });
100
+ }
101
+
102
+ async function applyJavaScript(db, id) {
103
+ const dbInterface = {
104
+ runQuery: (query, params, fetchAll) =>
105
+ sqlite.runQuery(db, query, params, fetchAll),
106
+ execQuery: query => sqlite.execQuery(db, query),
107
+ transaction: func => sqlite.transaction(db, func),
108
+ };
109
+
110
+ if (javascriptMigrations[id] == null) {
111
+ throw new Error('Could not find JS migration code to run for ' + id);
112
+ }
113
+
114
+ const run = javascriptMigrations[id];
115
+ return run(dbInterface, {
116
+ fs,
117
+ fileId: prefs.getPrefs()?.id,
118
+ });
119
+ }
120
+
121
+ async function applySql(db, sql) {
122
+ try {
123
+ sqlite.execQuery(db, sql);
124
+ } catch (e) {
125
+ logger.log('Error applying sql:', sql);
126
+ throw e;
127
+ }
128
+ }
129
+
130
+ export async function applyMigration(
131
+ db: Database,
132
+ name: string,
133
+ migrationsDir: string,
134
+ ): Promise<void> {
135
+ const code = await fs.readFile(fs.join(migrationsDir, name));
136
+ if (name.match(/\.js$/)) {
137
+ await applyJavaScript(db, getMigrationId(name));
138
+ } else {
139
+ await applySql(db, code);
140
+ }
141
+ sqlite.runQuery(db, 'INSERT INTO __migrations__ (id) VALUES (?)', [
142
+ getMigrationId(name),
143
+ ]);
144
+ }
145
+
146
+ function checkDatabaseValidity(
147
+ appliedIds: number[],
148
+ available: string[],
149
+ ): void {
150
+ if (appliedIds.length > available.length) {
151
+ logger.error(
152
+ 'Database is out of sync with migrations (index past available):',
153
+ {
154
+ appliedIds,
155
+ available,
156
+ },
157
+ );
158
+ throw new Error('out-of-sync-migrations');
159
+ }
160
+
161
+ for (let i = 0; i < appliedIds.length; i++) {
162
+ if (appliedIds[i] !== getMigrationId(available[i])) {
163
+ logger.error(
164
+ 'Database is out of sync with migrations (migration id mismatch):',
165
+ {
166
+ appliedIds,
167
+ available,
168
+ missing: available.filter(
169
+ m => !appliedIds.includes(getMigrationId(m)),
170
+ ),
171
+ },
172
+ );
173
+ throw new Error('out-of-sync-migrations');
174
+ }
175
+ }
176
+ }
177
+
178
+ export async function migrate(db: Database): Promise<string[]> {
179
+ await patchBadMigrations(db);
180
+ const appliedIds = await getAppliedMigrations(db);
181
+ const available = await getMigrationList(MIGRATIONS_DIR);
182
+
183
+ checkDatabaseValidity(appliedIds, available);
184
+
185
+ const pending = getPending(appliedIds, available);
186
+
187
+ for (const migration of pending) {
188
+ await applyMigration(db, migration, MIGRATIONS_DIR);
189
+ }
190
+
191
+ return pending;
192
+ }
@@ -0,0 +1,184 @@
1
+ import type {
2
+ CategoryEntity,
3
+ CategoryGroupEntity,
4
+ PayeeEntity,
5
+ } from '../types/models';
6
+
7
+ import {
8
+ convertForInsert,
9
+ convertForUpdate,
10
+ convertFromSelect,
11
+ schema,
12
+ schemaConfig,
13
+ } from './aql';
14
+ import type { DbAccount, DbCategory, DbCategoryGroup, DbPayee } from './db';
15
+ import { ValidationError } from './errors';
16
+
17
+ export function requiredFields<T extends object, K extends keyof T>(
18
+ name: string,
19
+ row: T,
20
+ fields: K[],
21
+ update?: boolean,
22
+ ) {
23
+ fields.forEach(field => {
24
+ if (update) {
25
+ if (row.hasOwnProperty(field) && row[field] == null) {
26
+ throw new ValidationError(`${name} is missing field ${String(field)}`);
27
+ }
28
+ } else {
29
+ if (!row.hasOwnProperty(field) || row[field] == null) {
30
+ throw new ValidationError(`${name} is missing field ${String(field)}`);
31
+ }
32
+ }
33
+ });
34
+ }
35
+
36
+ export function toDateRepr(str: string) {
37
+ if (typeof str !== 'string') {
38
+ throw new Error('toDateRepr not passed a string: ' + str);
39
+ }
40
+
41
+ return parseInt(str.replace(/-/g, ''));
42
+ }
43
+
44
+ export function fromDateRepr(number: number) {
45
+ if (typeof number !== 'number') {
46
+ throw new Error('fromDateRepr not passed a number: ' + number);
47
+ }
48
+
49
+ const dateString = number.toString();
50
+ return (
51
+ dateString.slice(0, 4) +
52
+ '-' +
53
+ dateString.slice(4, 6) +
54
+ '-' +
55
+ dateString.slice(6)
56
+ );
57
+ }
58
+
59
+ export const accountModel = {
60
+ validate(account: Partial<DbAccount>, { update }: { update?: boolean } = {}) {
61
+ requiredFields(
62
+ 'account',
63
+ account,
64
+ update ? ['name', 'offbudget', 'closed'] : ['name'],
65
+ update,
66
+ );
67
+
68
+ return account as DbAccount;
69
+ },
70
+ };
71
+
72
+ export const categoryModel = {
73
+ validate(
74
+ category: Partial<DbCategory>,
75
+ { update }: { update?: boolean } = {},
76
+ ): DbCategory {
77
+ requiredFields(
78
+ 'category',
79
+ category,
80
+ update ? ['name', 'is_income', 'cat_group'] : ['name', 'cat_group'],
81
+ update,
82
+ );
83
+
84
+ const { sort_order: _sort_order, ...rest } = category;
85
+ return { ...rest } as DbCategory;
86
+ },
87
+ toDb(
88
+ category: CategoryEntity,
89
+ { update }: { update?: boolean } = {},
90
+ ): DbCategory {
91
+ return (
92
+ update
93
+ ? convertForUpdate(schema, schemaConfig, 'categories', category)
94
+ : convertForInsert(schema, schemaConfig, 'categories', category)
95
+ ) as DbCategory;
96
+ },
97
+ fromDb(category: DbCategory): CategoryEntity {
98
+ return convertFromSelect(
99
+ schema,
100
+ schemaConfig,
101
+ 'categories',
102
+ category,
103
+ ) as CategoryEntity;
104
+ },
105
+ };
106
+
107
+ export const categoryGroupModel = {
108
+ validate(
109
+ categoryGroup: Partial<DbCategoryGroup>,
110
+ { update }: { update?: boolean } = {},
111
+ ): DbCategoryGroup {
112
+ requiredFields(
113
+ 'categoryGroup',
114
+ categoryGroup,
115
+ update ? ['name', 'is_income'] : ['name'],
116
+ update,
117
+ );
118
+
119
+ const { sort_order: _sort_order, ...rest } = categoryGroup;
120
+ return { ...rest } as DbCategoryGroup;
121
+ },
122
+ toDb(
123
+ categoryGroup: CategoryGroupEntity,
124
+ { update }: { update?: boolean } = {},
125
+ ): DbCategoryGroup {
126
+ return (
127
+ update
128
+ ? convertForUpdate(
129
+ schema,
130
+ schemaConfig,
131
+ 'category_groups',
132
+ categoryGroup,
133
+ )
134
+ : convertForInsert(
135
+ schema,
136
+ schemaConfig,
137
+ 'category_groups',
138
+ categoryGroup,
139
+ )
140
+ ) as DbCategoryGroup;
141
+ },
142
+ fromDb(
143
+ categoryGroup: DbCategoryGroup & {
144
+ categories: DbCategory[];
145
+ },
146
+ ): CategoryGroupEntity {
147
+ const { categories, ...rest } = categoryGroup;
148
+ const categoryGroupEntity = convertFromSelect(
149
+ schema,
150
+ schemaConfig,
151
+ 'category_groups',
152
+ rest,
153
+ ) as CategoryGroupEntity;
154
+
155
+ return {
156
+ ...categoryGroupEntity,
157
+ categories: categories
158
+ .filter(category => category.cat_group === categoryGroup.id)
159
+ .map(c => categoryModel.fromDb(c)),
160
+ };
161
+ },
162
+ };
163
+
164
+ export const payeeModel = {
165
+ validate(payee: Partial<DbPayee>, { update }: { update?: boolean } = {}) {
166
+ requiredFields('payee', payee, update ? [] : ['name'], update);
167
+ return payee as DbPayee;
168
+ },
169
+ toDb(payee: PayeeEntity, { update }: { update?: boolean } = {}): DbPayee {
170
+ return (
171
+ update
172
+ ? convertForUpdate(schema, schemaConfig, 'payees', payee)
173
+ : convertForInsert(schema, schemaConfig, 'payees', payee)
174
+ ) as DbPayee;
175
+ },
176
+ fromDb(payee: DbPayee): PayeeEntity {
177
+ return convertFromSelect(
178
+ schema,
179
+ schemaConfig,
180
+ 'payees',
181
+ payee,
182
+ ) as PayeeEntity;
183
+ },
184
+ };
@@ -0,0 +1,139 @@
1
+ // @ts-strict-ignore
2
+ import { captureBreadcrumb, captureException } from '../platform/exceptions';
3
+ import { sequential } from '../shared/async';
4
+ import type { HandlerFunctions, Handlers } from '../types/handlers';
5
+
6
+ const runningMethods = new Set();
7
+
8
+ let currentContext = null;
9
+ const mutatingMethods = new WeakMap();
10
+ let globalMutationsEnabled = false;
11
+
12
+ let _latestHandlerNames = [];
13
+
14
+ export function mutator<T extends HandlerFunctions>(handler: T): T {
15
+ mutatingMethods.set(handler, true);
16
+ return handler;
17
+ }
18
+
19
+ export function isMutating(handler) {
20
+ return mutatingMethods.has(handler);
21
+ }
22
+
23
+ async function flushRunningMethods() {
24
+ // Give the client some time to invoke new requests
25
+ await wait(200);
26
+
27
+ while (runningMethods.size > 0) {
28
+ // Wait for all of them
29
+ await Promise.all([...runningMethods.values()]);
30
+
31
+ // We give clients more time to make other requests. This lets them continue
32
+ // to do an async workflow
33
+ await wait(100);
34
+ }
35
+ }
36
+
37
+ function wait(time) {
38
+ return new Promise(resolve => setTimeout(resolve, time));
39
+ }
40
+
41
+ export async function runHandler<T extends Handlers[keyof Handlers]>(
42
+ handler: T,
43
+ args?: Parameters<T>[0],
44
+ { undoTag, name }: { undoTag?; name? } = {},
45
+ ): Promise<ReturnType<T>> {
46
+ // For debug reasons, track the latest handlers that have been
47
+ // called
48
+ _latestHandlerNames.push(name);
49
+ if (_latestHandlerNames.length > 5) {
50
+ _latestHandlerNames = _latestHandlerNames.slice(-5);
51
+ }
52
+
53
+ if (mutatingMethods.has(handler)) {
54
+ return runMutator(() => handler(args), { undoTag }) as Promise<
55
+ ReturnType<T>
56
+ >;
57
+ }
58
+
59
+ // When closing a file, it clears out all global state for the file. That
60
+ // means any async workflows currently executed would be cut off. We handle
61
+ // this by letting all async workflows finish executing before closing the
62
+ // file
63
+ if (name === 'close-budget') {
64
+ await flushRunningMethods();
65
+ }
66
+
67
+ const promise = handler(args);
68
+ runningMethods.add(promise);
69
+ void promise.then(() => {
70
+ runningMethods.delete(promise);
71
+ });
72
+ return promise as Promise<ReturnType<T>>;
73
+ }
74
+
75
+ // These are useful for tests. Only use them in tests.
76
+ export function enableGlobalMutations() {
77
+ if (process.env.NODE_ENV === 'test') {
78
+ globalMutationsEnabled = true;
79
+ }
80
+ }
81
+
82
+ export function disableGlobalMutations() {
83
+ if (process.env.NODE_ENV === 'test') {
84
+ globalMutationsEnabled = false;
85
+ }
86
+ }
87
+
88
+ function _runMutator<T extends () => Promise<unknown>>(
89
+ func: T,
90
+ initialContext = {},
91
+ ): Promise<Awaited<ReturnType<T>>> {
92
+ currentContext = initialContext;
93
+ return func().finally(() => {
94
+ currentContext = null;
95
+ }) as Promise<Awaited<ReturnType<T>>>;
96
+ }
97
+ // Type cast needed as TS looses types over nested generic returns
98
+ export const runMutator = sequential(_runMutator) as typeof _runMutator;
99
+
100
+ export function withMutatorContext<T>(
101
+ context: { undoListening: boolean; undoTag?: unknown },
102
+ func: () => Promise<T>,
103
+ ): Promise<T> {
104
+ if (currentContext == null && !globalMutationsEnabled) {
105
+ captureBreadcrumb('Recent methods: ' + _latestHandlerNames.join(', '));
106
+ captureException(new Error('withMutatorContext: mutator not running'));
107
+
108
+ // See comment below. This is not an error right now, but it will
109
+ // be in the future.
110
+ return func();
111
+ }
112
+
113
+ const prevContext = currentContext;
114
+ currentContext = { ...currentContext, ...context };
115
+ return func().finally(() => {
116
+ currentContext = prevContext;
117
+ });
118
+ }
119
+
120
+ export function getMutatorContext() {
121
+ if (currentContext == null) {
122
+ captureBreadcrumb({
123
+ category: 'server',
124
+ message: 'Recent methods: ' + _latestHandlerNames.join(', '),
125
+ });
126
+ // captureException(new Error('getMutatorContext: mutator not running'));
127
+
128
+ // For now, this is a non-fatal error. It will be in the future,
129
+ // but this is relatively non-critical (undo just won't work) so
130
+ // return an empty context. When we have more confidence that
131
+ // everything is running inside a mutator, throw an error.
132
+ return {};
133
+ }
134
+
135
+ if (currentContext == null && globalMutationsEnabled) {
136
+ return {};
137
+ }
138
+ return currentContext;
139
+ }
@@ -0,0 +1,18 @@
1
+ import type { NoteEntity } from '../../types/models';
2
+ import { createApp } from '../app';
3
+ import * as db from '../db';
4
+ import { mutator } from '../mutators';
5
+ import { undoable } from '../undo';
6
+
7
+ export type NotesHandlers = {
8
+ 'notes-save': typeof updateNotes;
9
+ 'notes-save-undoable': typeof updateNotes;
10
+ };
11
+
12
+ export const app = createApp<NotesHandlers>();
13
+ app.method('notes-save', updateNotes);
14
+ app.method('notes-save-undoable', mutator(undoable(updateNotes)));
15
+
16
+ async function updateNotes({ id, note }: NoteEntity) {
17
+ await db.update('notes', { id, note });
18
+ }