@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,345 @@
1
+ // @ts-strict-ignore
2
+
3
+ import { aqlQuery } from '..';
4
+ import { q } from '../../../shared/query';
5
+ import type { QueryState } from '../../../shared/query';
6
+ import type { CategoryEntity } from '../../../types/models';
7
+ import * as db from '../../db';
8
+ import { whereIn } from '../../db/util';
9
+ import { isAggregateQuery } from '../compiler';
10
+ import type { CompilerState, OutputTypes, SqlPieces } from '../compiler';
11
+ import { execQuery } from '../exec';
12
+ import type { AqlQueryExecutor } from '../exec';
13
+ import { convertOutputType } from '../schema-helpers';
14
+
15
+ // Transactions executor
16
+
17
+ type SplitsOption = 'all' | 'inline' | 'none' | 'grouped';
18
+
19
+ function toGroup(parents, children, mapper = x => x) {
20
+ return parents.reduce((list, parent) => {
21
+ const childs = children.get(parent.id) || [];
22
+ list.push({
23
+ ...mapper(parent),
24
+ subtransactions: childs.map(mapper),
25
+ });
26
+ return list;
27
+ }, []);
28
+ }
29
+
30
+ // These two queries will return very different things:
31
+ //
32
+ // q('transactions').select({ $count: 'id' })
33
+ // q('transactions', { splits: "grouped" }).select({ $count: 'id' })
34
+ //
35
+ // The first will return the count of non-split and child
36
+ // transactions, and the second will return the count of all parent
37
+ // (or non-split) transactions
38
+
39
+ function execTransactions(
40
+ compilerState: CompilerState,
41
+ queryState: QueryState,
42
+ sqlPieces: SqlPieces,
43
+ params: (string | number)[],
44
+ outputTypes: OutputTypes,
45
+ ) {
46
+ const tableOptions = queryState.tableOptions || {};
47
+ const splitType = tableOptions.splits
48
+ ? (tableOptions.splits as string)
49
+ : 'inline';
50
+ if (!isValidSplitsOption(splitType)) {
51
+ throw new Error(`Invalid "splits" option for transactions: "${splitType}"`);
52
+ }
53
+
54
+ if (splitType === 'all' || splitType === 'inline' || splitType === 'none') {
55
+ return execTransactionsBasic(
56
+ compilerState,
57
+ queryState,
58
+ sqlPieces,
59
+ params,
60
+ splitType,
61
+ outputTypes,
62
+ );
63
+ } else if (splitType === 'grouped') {
64
+ return execTransactionsGrouped(
65
+ compilerState,
66
+ queryState,
67
+ sqlPieces,
68
+ params,
69
+ outputTypes,
70
+ );
71
+ }
72
+ }
73
+
74
+ function _isUnhappy(filter) {
75
+ // These fields can be filtered - all split transactions will
76
+ // still be returned regardless
77
+ for (const key of Object.keys(filter)) {
78
+ if (key === '$or' || key === '$and') {
79
+ if (filter[key] && _isUnhappy(filter[key])) {
80
+ return true;
81
+ }
82
+ } else if (!(key.indexOf('account') === 0 || key === 'date')) {
83
+ return true;
84
+ }
85
+ }
86
+ return false;
87
+ }
88
+
89
+ export function isHappyPathQuery(queryState) {
90
+ return queryState.filterExpressions.find(_isUnhappy) == null;
91
+ }
92
+
93
+ async function execTransactionsGrouped(
94
+ compilerState: CompilerState,
95
+ queryState: QueryState,
96
+ sqlPieces: SqlPieces,
97
+ params: (string | number)[],
98
+ outputTypes: OutputTypes,
99
+ ) {
100
+ const { withDead } = queryState;
101
+ const whereDead = withDead ? '' : `AND ${sqlPieces.from}.tombstone = 0`;
102
+
103
+ // Aggregate queries don't make sense for a grouped transactions
104
+ // query. We never should include both parent and children
105
+ // transactions as it would duplicate amounts and the final number
106
+ // would never make sense. In this case, switch back to the "inline"
107
+ // type where only non-parent transactions are considered
108
+ if (isAggregateQuery(queryState)) {
109
+ const s = { ...sqlPieces };
110
+
111
+ // Modify the where to only include non-parents
112
+ s.where = `${s.where} AND ${s.from}.is_parent = 0`;
113
+
114
+ // We also want to exclude deleted transactions. Normally we
115
+ // handle this manually down below, but now that we are doing a
116
+ // normal query we want to rely on the view. Unfortunately, SQL
117
+ // has already been generated so we can't easily change the view
118
+ // name here; instead, we change it and map it back to the name
119
+ // used elsewhere in the query. Ideally we'd improve this
120
+ if (!withDead) {
121
+ s.from = 'v_transactions_internal_alive v_transactions_internal';
122
+ }
123
+
124
+ return execQuery(queryState, compilerState, s, params, outputTypes);
125
+ }
126
+
127
+ let rows;
128
+ let matched = null;
129
+
130
+ if (isHappyPathQuery(queryState)) {
131
+ // This is just an optimization - we can just filter out children
132
+ // directly and only list parents
133
+ const rowSql = `
134
+ SELECT ${sqlPieces.from}.id as group_id
135
+ FROM ${sqlPieces.from}
136
+ ${sqlPieces.joins}
137
+ ${sqlPieces.where} AND is_child = 0 ${whereDead}
138
+ ${sqlPieces.orderBy}
139
+ ${sqlPieces.limit != null ? `LIMIT ${sqlPieces.limit}` : ''}
140
+ ${sqlPieces.offset != null ? `OFFSET ${sqlPieces.offset}` : ''}
141
+ `;
142
+ rows = await db.all<db.DbViewTransactionInternal>(rowSql, params);
143
+ } else {
144
+ // TODO: phew, what a doozy. write docs why it works this way
145
+ const rowSql = `
146
+ SELECT group_id, matched FROM (
147
+ SELECT
148
+ group_id,
149
+ GROUP_CONCAT(id) as matched
150
+ FROM (
151
+ SELECT ${sqlPieces.from}.id, IFNULL(${sqlPieces.from}.parent_id, ${sqlPieces.from}.id) as group_id
152
+ FROM ${sqlPieces.from}
153
+ LEFT JOIN transactions _t2 ON ${sqlPieces.from}.is_child = 1 AND _t2.id = ${sqlPieces.from}.parent_id
154
+ ${sqlPieces.joins}
155
+ ${sqlPieces.where} AND ${sqlPieces.from}.tombstone = 0 AND IFNULL(_t2.tombstone, 0) = 0
156
+ )
157
+ GROUP BY group_id
158
+ )
159
+ LEFT JOIN ${sqlPieces.from} ON ${sqlPieces.from}.id = group_id
160
+ ${sqlPieces.joins}
161
+ ${sqlPieces.orderBy}
162
+ ${sqlPieces.limit != null ? `LIMIT ${sqlPieces.limit}` : ''}
163
+ ${sqlPieces.offset != null ? `OFFSET ${sqlPieces.offset}` : ''}
164
+ `;
165
+
166
+ rows = await db.all<db.DbViewTransactionInternal>(rowSql, params);
167
+ matched = new Set(
168
+ [].concat.apply(
169
+ [],
170
+ rows.map(row => row.matched.split(',')),
171
+ ),
172
+ );
173
+ }
174
+
175
+ const where = whereIn(
176
+ rows.map(row => row.group_id),
177
+ `IFNULL(${sqlPieces.from}.parent_id, ${sqlPieces.from}.id)`,
178
+ );
179
+ const finalSql = `
180
+ SELECT ${sqlPieces.select}, parent_id AS _parent_id FROM ${sqlPieces.from}
181
+ ${sqlPieces.joins}
182
+ WHERE ${where} ${whereDead}
183
+ ${sqlPieces.orderBy}
184
+ `;
185
+
186
+ const allRows = await db.all<
187
+ db.DbViewTransactionInternal & {
188
+ _parent_id: db.DbViewTransactionInternal['parent_id'];
189
+ }
190
+ >(finalSql);
191
+
192
+ // Group the parents and children up
193
+ const { parents, children } = allRows.reduce(
194
+ (acc, trans) => {
195
+ const pid = trans._parent_id;
196
+ delete trans._parent_id;
197
+
198
+ if (pid == null) {
199
+ acc.parents.push(trans);
200
+ } else {
201
+ const arr = acc.children.get(pid) || [];
202
+ arr.push(trans);
203
+ acc.children.set(pid, arr);
204
+ }
205
+ return acc;
206
+ },
207
+ { parents: [], children: new Map() },
208
+ );
209
+
210
+ const mapper = trans => {
211
+ Object.keys(trans).forEach(name => {
212
+ trans[name] = convertOutputType(trans[name], outputTypes.get(name));
213
+ });
214
+
215
+ if (matched && !matched.has(trans.id)) {
216
+ trans._unmatched = true;
217
+ }
218
+ return trans;
219
+ };
220
+
221
+ return toGroup(parents, children, mapper);
222
+ }
223
+
224
+ async function execTransactionsBasic(
225
+ compilerState: CompilerState,
226
+ queryState: QueryState,
227
+ sqlPieces: SqlPieces,
228
+ params: (string | number)[],
229
+ splitType: SplitsOption,
230
+ outputTypes: OutputTypes,
231
+ ) {
232
+ const s = { ...sqlPieces };
233
+
234
+ if (splitType !== 'all') {
235
+ if (splitType === 'none') {
236
+ s.where = `${s.where} AND ${s.from}.parent_id IS NULL`;
237
+ } else {
238
+ s.where = `${s.where} AND ${s.from}.is_parent = 0`;
239
+ }
240
+ }
241
+
242
+ return execQuery(queryState, compilerState, s, params, outputTypes);
243
+ }
244
+
245
+ function isValidSplitsOption(splits: string): splits is SplitsOption {
246
+ return ['all', 'inline', 'none', 'grouped'].includes(splits);
247
+ }
248
+
249
+ // Category groups executor
250
+
251
+ type CategoriesOption = 'all' | 'none';
252
+
253
+ async function execCategoryGroups(
254
+ compilerState: CompilerState,
255
+ queryState: QueryState,
256
+ sqlPieces: SqlPieces,
257
+ params: (string | number)[],
258
+ outputTypes: OutputTypes,
259
+ ) {
260
+ const tableOptions = queryState.tableOptions || {};
261
+ const categoriesOption = tableOptions.categories
262
+ ? (tableOptions.categories as string)
263
+ : 'all';
264
+ if (!isValidCategoriesOption(categoriesOption)) {
265
+ throw new Error(
266
+ `Invalid "categories" option for category_groups: "${categoriesOption}"`,
267
+ );
268
+ }
269
+
270
+ if (categoriesOption !== 'none') {
271
+ return execCategoryGroupsWithCategories(
272
+ compilerState,
273
+ queryState,
274
+ sqlPieces,
275
+ params,
276
+ categoriesOption,
277
+ outputTypes,
278
+ );
279
+ }
280
+ return execCategoryGroupsBasic(
281
+ compilerState,
282
+ queryState,
283
+ sqlPieces,
284
+ params,
285
+ outputTypes,
286
+ );
287
+ }
288
+
289
+ async function execCategoryGroupsWithCategories(
290
+ compilerState: CompilerState,
291
+ queryState: QueryState,
292
+ sqlPieces: SqlPieces,
293
+ params: (string | number)[],
294
+ categoriesOption: CategoriesOption,
295
+ outputTypes: OutputTypes,
296
+ ) {
297
+ const categoryGroups = await execCategoryGroupsBasic(
298
+ compilerState,
299
+ queryState,
300
+ sqlPieces,
301
+ params,
302
+ outputTypes,
303
+ );
304
+
305
+ if (categoriesOption === 'none') {
306
+ return categoryGroups;
307
+ }
308
+
309
+ const { data: categories }: { data: CategoryEntity[] } = await aqlQuery(
310
+ q('categories')
311
+ .filter({
312
+ group: { $oneof: categoryGroups.map(cg => cg.id) },
313
+ })
314
+ .select('*'),
315
+ );
316
+
317
+ return categoryGroups.map(group => {
318
+ const cats = categories.filter(cat => cat.group === group.id);
319
+ return {
320
+ ...group,
321
+ categories: cats,
322
+ };
323
+ });
324
+ }
325
+
326
+ async function execCategoryGroupsBasic(
327
+ compilerState: CompilerState,
328
+ queryState: QueryState,
329
+ sqlPieces: SqlPieces,
330
+ params: (string | number)[],
331
+ outputTypes: OutputTypes,
332
+ ) {
333
+ return execQuery(queryState, compilerState, sqlPieces, params, outputTypes);
334
+ }
335
+
336
+ function isValidCategoriesOption(
337
+ categories: string,
338
+ ): categories is CategoriesOption {
339
+ return ['all', 'none'].includes(categories);
340
+ }
341
+
342
+ export const schemaExecutors: Record<string, AqlQueryExecutor> = {
343
+ transactions: execTransactions,
344
+ category_groups: execCategoryGroups,
345
+ };
@@ -0,0 +1,67 @@
1
+ // @ts-strict-ignore
2
+ import * as db from '../../db';
3
+
4
+ // This file doesn't test the schema code directly, it tests that
5
+ // views return data as expected and various constraints on the
6
+ // sqlite3 schema itself.
7
+
8
+ beforeEach(global.emptyDatabase());
9
+
10
+ describe('schema', () => {
11
+ test('never returns transactions without a date', async () => {
12
+ expect(
13
+ (await db.all<db.DbTransaction>('SELECT * FROM transactions')).length,
14
+ ).toBe(0);
15
+ expect(
16
+ (await db.all<db.DbViewTransaction>('SELECT * FROM v_transactions'))
17
+ .length,
18
+ ).toBe(0);
19
+ db.runQuery('INSERT INTO transactions (acct) VALUES (?)', ['foo']);
20
+ expect(
21
+ (await db.all<db.DbTransaction>('SELECT * FROM transactions')).length,
22
+ ).toBe(1);
23
+ expect(
24
+ (await db.all<db.DbViewTransaction>('SELECT * FROM v_transactions'))
25
+ .length,
26
+ ).toBe(0);
27
+ });
28
+
29
+ test('never returns transactions without an account', async () => {
30
+ expect(
31
+ (await db.all<db.DbTransaction>('SELECT * FROM transactions')).length,
32
+ ).toBe(0);
33
+ expect(
34
+ (await db.all<db.DbViewTransaction>('SELECT * FROM v_transactions'))
35
+ .length,
36
+ ).toBe(0);
37
+ db.runQuery('INSERT INTO transactions (date) VALUES (?)', [20200101]);
38
+ expect(
39
+ (await db.all<db.DbTransaction>('SELECT * FROM transactions')).length,
40
+ ).toBe(1);
41
+ expect(
42
+ (await db.all<db.DbViewTransaction>('SELECT * FROM v_transactions'))
43
+ .length,
44
+ ).toBe(0);
45
+ });
46
+
47
+ test('never returns child transactions without a parent', async () => {
48
+ expect(
49
+ (await db.all<db.DbTransaction>('SELECT * FROM transactions')).length,
50
+ ).toBe(0);
51
+ expect(
52
+ (await db.all<db.DbViewTransaction>('SELECT * FROM v_transactions'))
53
+ .length,
54
+ ).toBe(0);
55
+ db.runQuery(
56
+ 'INSERT INTO transactions (date, acct, isChild) VALUES (?, ?, ?)',
57
+ [20200101, 'foo', 1],
58
+ );
59
+ expect(
60
+ (await db.all<db.DbTransaction>('SELECT * FROM transactions')).length,
61
+ ).toBe(1);
62
+ expect(
63
+ (await db.all<db.DbViewTransaction>('SELECT * FROM v_transactions'))
64
+ .length,
65
+ ).toBe(0);
66
+ });
67
+ });