@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,2 @@
1
+ // oxlint-disable-next-line no-restricted-imports
2
+ export * from './index.electron';
@@ -0,0 +1,134 @@
1
+ // @ts-strict-ignore
2
+ import SQL from 'better-sqlite3';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+
5
+ import { readFile, removeFile } from '../fs';
6
+ import { logger } from '../log';
7
+
8
+ import { normalise } from './normalise';
9
+ import { unicodeLike } from './unicodeLike';
10
+
11
+ function verifyParamTypes(sql, arr) {
12
+ arr.forEach(val => {
13
+ if (typeof val !== 'string' && typeof val !== 'number' && val !== null) {
14
+ logger.log(sql, arr);
15
+ throw new Error('Invalid field type ' + val + ' for sql ' + sql);
16
+ }
17
+ });
18
+ }
19
+
20
+ export async function init() {
21
+ // No need to initialise on electron
22
+ }
23
+
24
+ export function prepare(db, sql) {
25
+ return db.prepare(sql);
26
+ }
27
+
28
+ export function runQuery(
29
+ db: SQL.Database,
30
+ sql: string | SQL.Statement,
31
+ params: (string | number)[] = [],
32
+ fetchAll = false,
33
+ ) {
34
+ if (params) {
35
+ verifyParamTypes(sql, params);
36
+ }
37
+
38
+ let stmt: SQL.Statement;
39
+ try {
40
+ stmt = typeof sql === 'string' ? db.prepare(sql) : sql;
41
+ } catch (e) {
42
+ logger.log('error', sql);
43
+ throw e;
44
+ }
45
+
46
+ if (fetchAll) {
47
+ try {
48
+ const result = stmt.all(...params);
49
+ return result;
50
+ } catch (e) {
51
+ logger.log('error', sql);
52
+ throw e;
53
+ }
54
+ } else {
55
+ const info = stmt.run(...params);
56
+ return { changes: info.changes, insertId: info.lastInsertRowid };
57
+ }
58
+ }
59
+
60
+ export function execQuery(db: SQL.Database, sql: string) {
61
+ db.exec(sql);
62
+ }
63
+
64
+ export function transaction(db: SQL.Database, fn: () => void) {
65
+ db.transaction(fn)();
66
+ }
67
+
68
+ // **Important**: this is an unsafe function since sqlite executes
69
+ // executes statements sequentially. It would be easy for other code
70
+ // to run statements in between our transaction and get caught up in
71
+ // it. This is rarely used, and only needed for specific cases (like
72
+ // batch importing a bunch of data). Don't use this.
73
+ let transactionDepth = 0;
74
+ export async function asyncTransaction(
75
+ db: SQL.Database,
76
+ fn: () => Promise<void>,
77
+ ) {
78
+ // Support nested transactions by "coalescing" them into the parent
79
+ // one if one is already started
80
+ if (transactionDepth === 0) {
81
+ db.exec('BEGIN TRANSACTION');
82
+ }
83
+ transactionDepth++;
84
+
85
+ try {
86
+ await fn();
87
+ } finally {
88
+ transactionDepth--;
89
+ // We always commit because rollback is more dangerous - any
90
+ // queries that ran *in-between* this async function would be
91
+ // lost. Right now we are only using transactions for speed
92
+ // purposes unfortunately
93
+ if (transactionDepth === 0) {
94
+ db.exec('COMMIT');
95
+ }
96
+ }
97
+ }
98
+
99
+ function regexp(regex: string, text: string | null) {
100
+ return new RegExp(regex).test(text || '') ? 1 : 0;
101
+ }
102
+
103
+ export function openDatabase(pathOrBuffer: string | Buffer): SQL.Database {
104
+ const db = new SQL(pathOrBuffer);
105
+ // Define Unicode-aware LOWER, UPPER, and LIKE implementation.
106
+ // This is necessary because better-sqlite3 uses SQLite build without ICU support.
107
+ db.function('UNICODE_LOWER', { deterministic: true }, (arg: string | null) =>
108
+ arg?.toLowerCase(),
109
+ );
110
+ db.function('UNICODE_UPPER', { deterministic: true }, (arg: string | null) =>
111
+ arg?.toUpperCase(),
112
+ );
113
+ db.function('UNICODE_LIKE', { deterministic: true }, unicodeLike);
114
+ db.function('REGEXP', { deterministic: true }, regexp);
115
+ db.function('NORMALISE', { deterministic: true }, normalise);
116
+ return db;
117
+ }
118
+
119
+ export function closeDatabase(db: SQL.Database) {
120
+ db.close();
121
+ }
122
+
123
+ export async function exportDatabase(db: SQL.Database) {
124
+ // electron does not support better-sqlite serialize since v21
125
+ // save to file and read in the raw data.
126
+ const name = `${process.env.ACTUAL_DATA_DIR}/backup-for-export-${uuidv4()}.db`;
127
+
128
+ await db.backup(name);
129
+
130
+ const data = await readFile(name, 'binary');
131
+ await removeFile(name);
132
+
133
+ return data;
134
+ }
@@ -0,0 +1,108 @@
1
+ // @ts-strict-ignore
2
+ import { patchFetchForSqlJS } from '../../../mocks/util';
3
+
4
+ import { execQuery, init, openDatabase, runQuery, transaction } from './index';
5
+
6
+ beforeAll(async () => {
7
+ const baseURL = `${__dirname}/../../../../../../node_modules/@jlongster/sql.js/dist/`;
8
+ patchFetchForSqlJS(baseURL);
9
+
10
+ return init({ baseURL });
11
+ });
12
+
13
+ const initSQL = `
14
+ CREATE TABLE numbers (id TEXT PRIMARY KEY, number INTEGER);
15
+ CREATE TABLE textstrings (id TEXT PRIMARY KEY, string TEXT);
16
+ `;
17
+
18
+ describe('Web sqlite', () => {
19
+ it('should rollback transactions', async () => {
20
+ const db = await openDatabase();
21
+ execQuery(db, initSQL);
22
+
23
+ runQuery(db, "INSERT INTO numbers (id, number) VALUES ('id1', 4)");
24
+
25
+ let rows = runQuery(db, 'SELECT * FROM numbers', null, true);
26
+ expect(rows.length).toBe(1);
27
+ // @ts-expect-error Property 'number' does not exist on type 'unknown'
28
+ expect(rows[0].number).toBe(4);
29
+
30
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => null);
31
+ expect(() => {
32
+ transaction(db, () => {
33
+ runQuery(db, "INSERT INTO numbers (id, number) VALUES ('id2', 5)");
34
+ runQuery(db, "INSERT INTO numbers (id, number) VALUES ('id3', 6)");
35
+ // Insert an invalid one that will error
36
+ runQuery(db, "INSERT INTO numbers (id, number) VALUES ('id1', 1)");
37
+ });
38
+ }).toThrow(/constraint failed/);
39
+ consoleSpy.mockRestore();
40
+
41
+ // Nothing should have changed in the db
42
+ rows = runQuery(db, 'SELECT * FROM numbers', null, true);
43
+ expect(rows.length).toBe(1);
44
+ // @ts-expect-error Property 'number' does not exist on type 'unknown'
45
+ expect(rows[0].number).toBe(4);
46
+ });
47
+
48
+ it('should support nested transactions', async () => {
49
+ const db = await openDatabase();
50
+ execQuery(db, initSQL);
51
+
52
+ runQuery(db, "INSERT INTO numbers (id, number) VALUES ('id1', 4)");
53
+
54
+ let rows = runQuery(db, 'SELECT * FROM numbers', null, true);
55
+ expect(rows.length).toBe(1);
56
+ // @ts-expect-error Property 'number' does not exist on type 'unknown'
57
+ expect(rows[0].number).toBe(4);
58
+
59
+ transaction(db, () => {
60
+ runQuery(db, "INSERT INTO numbers (id, number) VALUES ('id2', 5)");
61
+ runQuery(db, "INSERT INTO numbers (id, number) VALUES ('id3', 6)");
62
+
63
+ // Only this transaction should fail
64
+ const consoleSpy = vi
65
+ .spyOn(console, 'log')
66
+ .mockImplementation(() => null);
67
+ expect(() => {
68
+ transaction(db, () => {
69
+ runQuery(db, "INSERT INTO numbers (id, number) VALUES ('id4', 7)");
70
+ // Insert an invalid one that will error
71
+ runQuery(db, "INSERT INTO numbers (id, number) VALUES ('id1', 1)");
72
+ });
73
+ }).toThrow(/constraint failed/);
74
+ consoleSpy.mockRestore();
75
+ });
76
+
77
+ // Nothing should have changed in the db
78
+ rows = runQuery(db, 'SELECT * FROM numbers', null, true);
79
+ expect(rows.length).toBe(3);
80
+ // @ts-expect-error Property 'number' does not exist on type 'unknown'
81
+ expect(rows[0].number).toBe(4);
82
+ // @ts-expect-error Property 'number' does not exist on type 'unknown'
83
+ expect(rows[1].number).toBe(5);
84
+ // @ts-expect-error Property 'number' does not exist on type 'unknown'
85
+ expect(rows[2].number).toBe(6);
86
+ });
87
+
88
+ it('should match regex on text fields', async () => {
89
+ const db = await openDatabase();
90
+ execQuery(db, initSQL);
91
+
92
+ runQuery(
93
+ db,
94
+ "INSERT INTO textstrings (id, string) VALUES ('id1', 'not empty string')",
95
+ );
96
+ runQuery(db, "INSERT INTO textstrings (id) VALUES ('id2')");
97
+
98
+ const rows = runQuery(
99
+ db,
100
+ 'SELECT id FROM textstrings where REGEXP("n.", string)',
101
+ null,
102
+ true,
103
+ );
104
+ expect(rows.length).toBe(1);
105
+ // @ts-expect-error Property 'id' does not exist on type 'unknown'
106
+ expect(rows[0].id).toBe('id1');
107
+ });
108
+ });
@@ -0,0 +1,241 @@
1
+ // @ts-strict-ignore
2
+ import initSqlJS from '@jlongster/sql.js';
3
+ import type { Database, SqlJsStatic, Statement } from '@jlongster/sql.js';
4
+
5
+ import { logger } from '../log';
6
+
7
+ import { normalise } from './normalise';
8
+ import { unicodeLike } from './unicodeLike';
9
+
10
+ // Types exported from sql.js (and Emscripten) are incomplete, so we need to redefine them here
11
+ type FSStream = (typeof FS)['FSStream'] & {
12
+ node: (typeof FS)['FSNode'] & {
13
+ contents: {
14
+ readIfFallback: () => Promise<unknown>;
15
+ };
16
+ };
17
+ };
18
+ type FS = Omit<typeof FS, 'lookupPath' | 'open' | 'close'> & {
19
+ lookupPath: (
20
+ path: string,
21
+ opts?: { follow?: boolean },
22
+ ) => { node: (typeof FS)['FSNode'] & { link?: string } };
23
+ open: (path: string, flags: string, mode?: number) => FSStream;
24
+ close: (stream: FSStream) => void;
25
+ };
26
+ export type SqlJsModule = SqlJsStatic & {
27
+ FS: FS;
28
+ reset_filesystem: () => void;
29
+ register_for_idb: (idb: IDBDatabase) => void;
30
+ };
31
+
32
+ let SQL: SqlJsModule | null = null;
33
+
34
+ export async function init({
35
+ baseURL = process.env.PUBLIC_URL,
36
+ }: { baseURL?: string } = {}) {
37
+ // `initSqlJS` doesn't actually return a real promise, so make sure
38
+ // we're returning a real one for correct semantics
39
+ return new Promise((resolve, reject) => {
40
+ initSqlJS({
41
+ locateFile: file => baseURL + file,
42
+ }).then(
43
+ sql => {
44
+ SQL = sql as SqlJsModule;
45
+ resolve(undefined);
46
+ },
47
+ err => {
48
+ reject(err);
49
+ },
50
+ );
51
+ });
52
+ }
53
+
54
+ export function _getModule() {
55
+ if (SQL == null) {
56
+ throw new Error('_getModule: sql.js must be initialized first');
57
+ }
58
+ return SQL;
59
+ }
60
+
61
+ function verifyParamTypes(
62
+ sql: string | Statement,
63
+ arr: (string | number)[] = [],
64
+ ) {
65
+ arr.forEach(val => {
66
+ if (typeof val !== 'string' && typeof val !== 'number' && val !== null) {
67
+ throw new Error('Invalid field type ' + val + ' for sql ' + sql);
68
+ }
69
+ });
70
+ }
71
+
72
+ export function prepare(db: Database, sql: string) {
73
+ return db.prepare(sql);
74
+ }
75
+
76
+ export function runQuery(
77
+ db: Database,
78
+ sql: string | Statement,
79
+ params?: (string | number)[],
80
+ fetchAll?: false,
81
+ ): { changes: unknown };
82
+ export function runQuery<T>(
83
+ db: Database,
84
+ sql: string | Statement,
85
+ params: (string | number)[],
86
+ fetchAll: true,
87
+ ): T[];
88
+ export function runQuery<T>(
89
+ db: Database,
90
+ sql: string | Statement,
91
+ params: (string | number)[] = [],
92
+ fetchAll = false,
93
+ ): T[] | { changes: unknown } {
94
+ if (params) {
95
+ verifyParamTypes(sql, params);
96
+ }
97
+
98
+ const stmt = typeof sql === 'string' ? db.prepare(sql) : sql;
99
+
100
+ if (fetchAll) {
101
+ try {
102
+ stmt.bind(params);
103
+ const rows = [];
104
+
105
+ while (stmt.step()) {
106
+ rows.push(stmt.getAsObject());
107
+ }
108
+
109
+ if (typeof sql === 'string') {
110
+ stmt.free();
111
+ } else {
112
+ stmt.reset();
113
+ }
114
+ return rows;
115
+ } catch (e) {
116
+ logger.log(sql);
117
+ throw e;
118
+ }
119
+ } else {
120
+ stmt.run(params);
121
+ return { changes: db.getRowsModified() };
122
+ }
123
+ }
124
+
125
+ export function execQuery(db: Database, sql: string) {
126
+ db.exec(sql);
127
+ }
128
+
129
+ let transactionDepth = 0;
130
+
131
+ export function transaction(db: Database, fn: () => void) {
132
+ let before, after, undo;
133
+ if (transactionDepth > 0) {
134
+ before = 'SAVEPOINT __actual_sp';
135
+ after = 'RELEASE __actual_sp';
136
+ undo = 'ROLLBACK TO __actual_sp';
137
+ } else {
138
+ before = 'BEGIN';
139
+ after = 'COMMIT';
140
+ undo = 'ROLLBACK';
141
+ }
142
+
143
+ execQuery(db, before);
144
+ transactionDepth++;
145
+
146
+ try {
147
+ fn();
148
+ execQuery(db, after);
149
+ } catch (ex) {
150
+ execQuery(db, undo);
151
+
152
+ if (undo !== 'ROLLBACK') {
153
+ execQuery(db, after);
154
+ }
155
+
156
+ throw ex;
157
+ } finally {
158
+ transactionDepth--;
159
+ }
160
+ }
161
+
162
+ // See the comment about this function in index.electron.js. You
163
+ // shouldn't normally use this. I'd like to get rid of it.
164
+ export async function asyncTransaction(db: Database, fn: () => Promise<void>) {
165
+ // Support nested transactions by "coalescing" them into the parent
166
+ // one if one is already started
167
+ if (transactionDepth === 0) {
168
+ db.exec('BEGIN TRANSACTION');
169
+ }
170
+ transactionDepth++;
171
+
172
+ try {
173
+ await fn();
174
+ } finally {
175
+ transactionDepth--;
176
+ // We always commit because rollback is more dangerous - any
177
+ // queries that ran *in-between* this async function would be
178
+ // lost. Right now we are only using transactions for speed
179
+ // purposes unfortunately
180
+ if (transactionDepth === 0) {
181
+ db.exec('COMMIT');
182
+ }
183
+ }
184
+ }
185
+
186
+ function regexp(regex: string, text: string) {
187
+ return new RegExp(regex).test(text || '') ? 1 : 0;
188
+ }
189
+
190
+ export async function openDatabase(pathOrBuffer?: string | Uint8Array) {
191
+ let db = null;
192
+ if (pathOrBuffer) {
193
+ if (typeof pathOrBuffer !== 'string') {
194
+ db = new SQL.Database(pathOrBuffer);
195
+ } else {
196
+ const path = pathOrBuffer;
197
+ if (path !== ':memory:') {
198
+ if (typeof SharedArrayBuffer === 'undefined') {
199
+ const stream = SQL.FS.open(SQL.FS.readlink(path), 'a+');
200
+ await stream.node.contents.readIfFallback();
201
+ SQL.FS.close(stream);
202
+ }
203
+
204
+ db = new SQL.Database(
205
+ path.includes('/blocked') ? path : SQL.FS.readlink(path),
206
+ // @ts-expect-error 2nd argument missed in sql.js types
207
+ { filename: true },
208
+ );
209
+ db.exec(`
210
+ PRAGMA journal_mode=MEMORY;
211
+ PRAGMA cache_size=-10000;
212
+ `);
213
+ }
214
+ }
215
+ }
216
+
217
+ if (db === null) {
218
+ db = new SQL.Database();
219
+ }
220
+
221
+ // Define Unicode-aware LOWER, UPPER, and LIKE implementation.
222
+ // This is necessary because sql.js uses SQLite build without ICU support.
223
+ //
224
+ // Note that this function should ideally be created with a deterministic flag
225
+ // to allow SQLite to better optimize calls to it by factoring them out of inner loops
226
+ // but SQL.js does not support this: https://github.com/sql-js/sql.js/issues/551
227
+ db.create_function('UNICODE_LOWER', arg => arg?.toLowerCase());
228
+ db.create_function('UNICODE_UPPER', arg => arg?.toUpperCase());
229
+ db.create_function('UNICODE_LIKE', unicodeLike);
230
+ db.create_function('REGEXP', regexp);
231
+ db.create_function('NORMALISE', normalise);
232
+ return db;
233
+ }
234
+
235
+ export function closeDatabase(db: Database) {
236
+ db.close();
237
+ }
238
+
239
+ export async function exportDatabase(db: Database) {
240
+ return db.export();
241
+ }
@@ -0,0 +1,9 @@
1
+ import { getNormalisedString } from '../../../shared/normalisation';
2
+
3
+ export function normalise(value: string | null): string | null {
4
+ if (!value) {
5
+ return null;
6
+ }
7
+
8
+ return getNormalisedString(value);
9
+ }
@@ -0,0 +1,58 @@
1
+ import { unicodeLike } from './unicodeLike';
2
+
3
+ describe('unicode LIKE functionality', () => {
4
+ it('empty pattern should not match to a value', () => {
5
+ const result = unicodeLike(null, 'value');
6
+
7
+ expect(result).toBe(0);
8
+ });
9
+
10
+ it('empty pattern should not match to null', () => {
11
+ const result = unicodeLike(null, null);
12
+
13
+ expect(result).toBe(0);
14
+ });
15
+
16
+ it('should match special characters', () => {
17
+ // oxlint-disable-next-line no-template-curly-in-string
18
+ const result = unicodeLike('.*+^${}()|[]\\', '.*+^${}()|[]\\');
19
+
20
+ expect(result).toBe(1);
21
+ });
22
+
23
+ it('should use ? as the single character placeholder', () => {
24
+ const result = unicodeLike('t?st', 'test');
25
+
26
+ expect(result).toBe(1);
27
+ });
28
+
29
+ it('should use % as the zero-or-more characters placeholder', () => {
30
+ const result = unicodeLike('t%st', 'te123st');
31
+
32
+ expect(result).toBe(1);
33
+ });
34
+
35
+ it('should ignore case for unicode', () => {
36
+ const result = unicodeLike('á', 'Ábcdefg');
37
+
38
+ expect(result).toBe(1);
39
+ });
40
+
41
+ it('should ignore case for ascii', () => {
42
+ const result = unicodeLike('a', 'Abcdefg');
43
+
44
+ expect(result).toBe(1);
45
+ });
46
+
47
+ it('should treat null value as empty string', () => {
48
+ const result = unicodeLike('%', null);
49
+
50
+ expect(result).toBe(1);
51
+ });
52
+
53
+ it('should not match null value to the string "null"', () => {
54
+ const result = unicodeLike('null', null);
55
+
56
+ expect(result).toBe(0);
57
+ });
58
+ });
@@ -0,0 +1,31 @@
1
+ import { LRUCache } from 'lru-cache';
2
+
3
+ const likePatternCache = new LRUCache<string, RegExp>({ max: 500 });
4
+
5
+ export function unicodeLike(
6
+ pattern: string | null,
7
+ value: string | null,
8
+ ): number {
9
+ if (!pattern) {
10
+ return 0;
11
+ }
12
+
13
+ if (!value) {
14
+ value = '';
15
+ }
16
+
17
+ let cachedRegExp = likePatternCache.get(pattern);
18
+ if (!cachedRegExp) {
19
+ // we don't escape ? and % because we don't know
20
+ // whether they originate from the user input or from our query compiler.
21
+ // Maybe improve the query compiler to correctly process these characters?
22
+ const processedPattern = pattern
23
+ .replace(/[.*+^${}()|[\]\\]/g, '\\$&')
24
+ .replaceAll('?', '.')
25
+ .replaceAll('%', '.*');
26
+ cachedRegExp = new RegExp(processedPattern, 'i');
27
+ likePatternCache.set(pattern, cachedRegExp);
28
+ }
29
+
30
+ return cachedRegExp.test(value) ? 1 : 0;
31
+ }
@@ -0,0 +1,9 @@
1
+ // @ts-strict-ignore
2
+ export {
3
+ handleRequest as post,
4
+ handleRequestBinary as postBinary,
5
+ } from '../tests/mockSyncServer';
6
+
7
+ export const get = function () {
8
+ throw new Error('get unimplemented');
9
+ };