@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,79 @@
1
+ // @ts-strict-ignore
2
+ import * as dateFns from 'date-fns';
3
+
4
+ import { updateBackups } from './backups';
5
+
6
+ describe('Backups', () => {
7
+ test('backups work', async () => {
8
+ async function getUpdatedBackups(backups) {
9
+ const toRemove = await updateBackups(backups);
10
+ return backups.filter(b => !toRemove.includes(b.id));
11
+ }
12
+
13
+ function cleanDates(backups) {
14
+ return backups.map(backup => ({
15
+ id: backup.id,
16
+ date: dateFns.format(backup.date, 'yyyy-MM-dd'),
17
+ }));
18
+ }
19
+
20
+ // Should keep 3 backups on the current day
21
+ expect(
22
+ cleanDates(
23
+ await getUpdatedBackups([
24
+ { id: 'backup1', date: dateFns.parseISO('2017-01-01') },
25
+ { id: 'backup2', date: dateFns.parseISO('2017-01-01') },
26
+ { id: 'backup3', date: dateFns.parseISO('2017-01-01') },
27
+ { id: 'backup4', date: dateFns.parseISO('2017-01-01') },
28
+ ]),
29
+ ),
30
+ ).toMatchSnapshot();
31
+
32
+ // Should not delete any since up to 3 are allowed on the current
33
+ // day
34
+ expect(
35
+ cleanDates(
36
+ await getUpdatedBackups([
37
+ { id: 'backup1', date: dateFns.parseISO('2017-01-01') },
38
+ { id: 'backup2', date: dateFns.parseISO('2017-01-01') },
39
+ { id: 'backup3', date: dateFns.parseISO('2016-12-30') },
40
+ { id: 'backup4', date: dateFns.parseISO('2016-12-29') },
41
+ ]),
42
+ ),
43
+ ).toMatchSnapshot();
44
+
45
+ // Should delete any additional backups on other days (keep the
46
+ // two on the current day but delete copies on other days)
47
+ expect(
48
+ cleanDates(
49
+ await getUpdatedBackups([
50
+ { id: 'backup1', date: dateFns.parseISO('2017-01-01') },
51
+ { id: 'backup2', date: dateFns.parseISO('2017-01-01') },
52
+ { id: 'backup3', date: dateFns.parseISO('2016-12-29') },
53
+ { id: 'backup4', date: dateFns.parseISO('2016-12-29') },
54
+ { id: 'backup5', date: dateFns.parseISO('2016-12-29') },
55
+ ]),
56
+ ),
57
+ ).toMatchSnapshot();
58
+
59
+ // Should only keep up to 10 backups
60
+ expect(
61
+ cleanDates(
62
+ await getUpdatedBackups([
63
+ { id: 'backup1', date: dateFns.parseISO('2017-01-01') },
64
+ { id: 'backup2', date: dateFns.parseISO('2017-01-01') },
65
+ { id: 'backup3', date: dateFns.parseISO('2016-12-29') },
66
+ { id: 'backup4', date: dateFns.parseISO('2016-12-28') },
67
+ { id: 'backup5', date: dateFns.parseISO('2016-12-27') },
68
+ { id: 'backup6', date: dateFns.parseISO('2016-12-26') },
69
+ { id: 'backup7', date: dateFns.parseISO('2016-12-25') },
70
+ { id: 'backup8', date: dateFns.parseISO('2016-12-24') },
71
+ { id: 'backup9', date: dateFns.parseISO('2016-12-23') },
72
+ { id: 'backup10', date: dateFns.parseISO('2016-12-22') },
73
+ { id: 'backup11', date: dateFns.parseISO('2016-12-21') },
74
+ { id: 'backup12', date: dateFns.parseISO('2016-12-20') },
75
+ ]),
76
+ ),
77
+ ).toMatchSnapshot();
78
+ });
79
+ });
@@ -0,0 +1,251 @@
1
+ import type { Database } from '@jlongster/sql.js';
2
+ // @ts-strict-ignore
3
+ import AdmZip from 'adm-zip';
4
+ import * as dateFns from 'date-fns';
5
+
6
+ import * as connection from '../../platform/server/connection';
7
+ import * as fs from '../../platform/server/fs';
8
+ import { logger } from '../../platform/server/log';
9
+ import * as sqlite from '../../platform/server/sqlite';
10
+ import * as monthUtils from '../../shared/months';
11
+ import * as cloudStorage from '../cloud-storage';
12
+ import * as prefs from '../prefs';
13
+
14
+ // A special backup that represents the latest version of the db that
15
+ // can be reverted to after loading a backup
16
+ const LATEST_BACKUP_FILENAME = 'db.latest.sqlite';
17
+ let serviceInterval = null;
18
+
19
+ export type Backup = { id: string; date: string } | LatestBackup;
20
+ type LatestBackup = { id: string; date: null; isLatest: true };
21
+ type BackupWithDate = { id: string; date: Date };
22
+
23
+ async function getBackups(id: string): Promise<BackupWithDate[]> {
24
+ const budgetDir = fs.getBudgetDir(id);
25
+ const backupDir = fs.join(budgetDir, 'backups');
26
+
27
+ let paths = [];
28
+ if (await fs.exists(backupDir)) {
29
+ paths = await fs.listDir(backupDir);
30
+ paths = paths.filter(file => file.match(/\.zip$/));
31
+ }
32
+
33
+ const backups = await Promise.all(
34
+ paths.map(async path => {
35
+ const mtime = await fs.getModifiedTime(fs.join(backupDir, path));
36
+ return {
37
+ id: path,
38
+ date: new Date(mtime),
39
+ };
40
+ }),
41
+ );
42
+
43
+ backups.sort((b1, b2) => {
44
+ if (b1.date < b2.date) {
45
+ return 1;
46
+ } else if (b1.date > b2.date) {
47
+ return -1;
48
+ }
49
+ return 0;
50
+ });
51
+
52
+ return backups;
53
+ }
54
+
55
+ async function getLatestBackup(id: string): Promise<LatestBackup | null> {
56
+ const budgetDir = fs.getBudgetDir(id);
57
+ if (await fs.exists(fs.join(budgetDir, LATEST_BACKUP_FILENAME))) {
58
+ return {
59
+ id: LATEST_BACKUP_FILENAME,
60
+ date: null,
61
+ isLatest: true,
62
+ };
63
+ }
64
+ return null;
65
+ }
66
+
67
+ export async function getAvailableBackups(id: string): Promise<Backup[]> {
68
+ const backups = await getBackups(id);
69
+
70
+ const latestBackup = await getLatestBackup(id);
71
+ if (latestBackup) {
72
+ backups.unshift(latestBackup);
73
+ }
74
+
75
+ return backups.map(backup => ({
76
+ ...backup,
77
+ date: backup.date ? dateFns.format(backup.date, 'yyyy-MM-dd H:mm') : null,
78
+ }));
79
+ }
80
+
81
+ export async function updateBackups(backups) {
82
+ const byDay = backups.reduce((groups, backup) => {
83
+ const day = dateFns.format(backup.date, 'yyyy-MM-dd');
84
+ groups[day] = groups[day] || [];
85
+ groups[day].push(backup);
86
+ return groups;
87
+ }, {});
88
+
89
+ const removed = [];
90
+ for (const day of Object.keys(byDay)) {
91
+ const dayBackups = byDay[day];
92
+ const isToday = day === monthUtils.currentDay();
93
+ // Allow 3 backups of the current day (so fine-grained edits are
94
+ // kept around). Otherwise only keep around one backup per day.
95
+ // And only keep a total of 10 backups.
96
+ for (const backup of dayBackups.slice(isToday ? 3 : 1)) {
97
+ removed.push(backup.id);
98
+ }
99
+ }
100
+
101
+ // Get the list of remaining backups and only keep the latest 10
102
+ const currentBackups = backups.filter(backup => !removed.includes(backup.id));
103
+ return removed.concat(currentBackups.slice(10).map(backup => backup.id));
104
+ }
105
+
106
+ export async function makeBackup(id: string) {
107
+ const budgetDir = fs.getBudgetDir(id);
108
+
109
+ // When making a backup, we no longer consider the user to be
110
+ // viewing any backups. If there exists a "latest backup" we should
111
+ // delete it and consider whatever is current as the latest
112
+ if (await fs.exists(fs.join(budgetDir, LATEST_BACKUP_FILENAME))) {
113
+ await fs.removeFile(fs.join(fs.getBudgetDir(id), LATEST_BACKUP_FILENAME));
114
+ }
115
+
116
+ const backupId = `${dateFns.format(new Date(), 'yyyy-MM-dd_HH-mm-ss')}.zip`;
117
+ const backupPath = fs.join(budgetDir, 'backups', backupId);
118
+
119
+ if (!(await fs.exists(fs.join(budgetDir, 'backups')))) {
120
+ await fs.mkdir(fs.join(budgetDir, 'backups'));
121
+ }
122
+
123
+ // Copy db to a temp path so we can clean CRDT messages before zipping
124
+ const tempDbPath = fs.join(
125
+ budgetDir,
126
+ 'backups',
127
+ `db.${Date.now()}.sqlite.tmp`,
128
+ );
129
+
130
+ await fs.copyFile(fs.join(budgetDir, 'db.sqlite'), tempDbPath);
131
+
132
+ let db: Database | undefined;
133
+
134
+ try {
135
+ // Remove all the messages from the backup
136
+ db = await sqlite.openDatabase(tempDbPath);
137
+ sqlite.runQuery(db, 'DELETE FROM messages_crdt');
138
+ sqlite.runQuery(db, 'DELETE FROM messages_clock');
139
+ // Zip up the cleaned db and metadata into a single backup file
140
+ const zip = new AdmZip();
141
+ zip.addLocalFile(tempDbPath, '', 'db.sqlite');
142
+ zip.addLocalFile(fs.join(budgetDir, 'metadata.json'));
143
+ zip.writeZip(backupPath);
144
+ } finally {
145
+ if (db) {
146
+ sqlite.closeDatabase(db);
147
+ }
148
+ if (await fs.exists(tempDbPath)) {
149
+ await fs.removeFile(tempDbPath);
150
+ }
151
+ }
152
+
153
+ const toRemove = await updateBackups(await getBackups(id));
154
+ for (const id of toRemove) {
155
+ await fs.removeFile(fs.join(budgetDir, 'backups', id));
156
+ }
157
+
158
+ connection.send('backups-updated', await getAvailableBackups(id));
159
+ }
160
+
161
+ export async function loadBackup(id: string, backupId: string) {
162
+ const budgetDir = fs.getBudgetDir(id);
163
+
164
+ if (!(await fs.exists(fs.join(budgetDir, LATEST_BACKUP_FILENAME)))) {
165
+ // If this is the first time we're loading a backup, save the
166
+ // current version so the user can easily revert back to it
167
+ await fs.copyFile(
168
+ fs.join(budgetDir, 'db.sqlite'),
169
+ fs.join(budgetDir, LATEST_BACKUP_FILENAME),
170
+ );
171
+
172
+ await fs.copyFile(
173
+ fs.join(budgetDir, 'metadata.json'),
174
+ fs.join(budgetDir, 'metadata.latest.json'),
175
+ );
176
+
177
+ // Restart the backup service to make sure the user has the full
178
+ // amount of time to figure out which one they want
179
+ stopBackupService();
180
+ startBackupService(id);
181
+
182
+ await prefs.loadPrefs(id);
183
+ }
184
+
185
+ if (backupId === LATEST_BACKUP_FILENAME) {
186
+ logger.log('Reverting backup');
187
+
188
+ // If reverting back to the latest, copy and delete the latest
189
+ // backup
190
+ await fs.copyFile(
191
+ fs.join(budgetDir, LATEST_BACKUP_FILENAME),
192
+ fs.join(budgetDir, 'db.sqlite'),
193
+ );
194
+ await fs.copyFile(
195
+ fs.join(budgetDir, 'metadata.latest.json'),
196
+ fs.join(budgetDir, 'metadata.json'),
197
+ );
198
+ await fs.removeFile(fs.join(budgetDir, LATEST_BACKUP_FILENAME));
199
+ await fs.removeFile(fs.join(budgetDir, 'metadata.latest.json'));
200
+
201
+ // Re-upload the new file
202
+ try {
203
+ await cloudStorage.upload();
204
+ } catch {}
205
+ prefs.unloadPrefs();
206
+ } else {
207
+ logger.log('Loading backup', backupId);
208
+
209
+ // This function is only ever called when a budget isn't loaded,
210
+ // so it's safe to load our prefs in. We need to forget about any
211
+ // syncing data if we are loading a backup (the current sync data
212
+ // will be restored if the user reverts to the original version)
213
+ await prefs.loadPrefs(id);
214
+ await prefs.savePrefs({
215
+ groupId: null,
216
+ lastSyncedTimestamp: null,
217
+ lastUploaded: null,
218
+ });
219
+
220
+ // Re-upload the new file
221
+ try {
222
+ await cloudStorage.upload();
223
+ } catch {}
224
+
225
+ prefs.unloadPrefs();
226
+
227
+ const zip = new AdmZip(fs.join(budgetDir, 'backups', backupId));
228
+ zip.extractEntryTo('db.sqlite', budgetDir, false, true);
229
+ zip.extractEntryTo('metadata.json', budgetDir, false, true);
230
+ }
231
+ }
232
+
233
+ export function startBackupService(id: string) {
234
+ if (serviceInterval) {
235
+ clearInterval(serviceInterval);
236
+ }
237
+
238
+ // Make a backup every 15 minutes
239
+ serviceInterval = setInterval(
240
+ async () => {
241
+ logger.log('Making backup');
242
+ await makeBackup(id);
243
+ },
244
+ 1000 * 60 * 15,
245
+ );
246
+ }
247
+
248
+ export function stopBackupService() {
249
+ clearInterval(serviceInterval);
250
+ serviceInterval = null;
251
+ }