@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,409 @@
1
+ // @ts-strict-ignore
2
+ import type { SchemaConfig } from '../compiler';
3
+
4
+ function f(type: string, opts?: Record<string, unknown>) {
5
+ return { type, ...opts };
6
+ }
7
+
8
+ // This is the table schema!
9
+ //
10
+ // The schema itself is the public API for querying and updating data.
11
+ // The schema config provides a lot of control over how queries are
12
+ // constructed other metadata used by the executors. The goal is to
13
+ // provide a simple interface to the app, but allow the backend to
14
+ // highly optimize how queries are run.
15
+ //
16
+ // A few notes:
17
+ //
18
+ // * `transactions` is a special table and is has a highly customized
19
+ // executor (see `executors.js`) for performace. It should mostly be
20
+ // transparent to you, but it's worth reading the comments in there if
21
+ // requirements change, especially regarding split transactions.
22
+ //
23
+ // * You should rarely change the schema and only add to it, but it is
24
+ // relatively safe to change if needed. The only problem is changing a
25
+ // public API, but that could be mitigated with an option in schema
26
+ // config to map field name for backwards compatibility.
27
+ //
28
+ // * It's important that the table names here in the schema map to the
29
+ // real tables in sqlite that are updated. Otherwise subscriptions
30
+ // might not work because the dependencies are wrong. If needed in the
31
+ // future, we can provide another option in the schema config to map a
32
+ // table name the internal table that is updated.
33
+
34
+ export const schema = {
35
+ transactions: {
36
+ id: f('id'),
37
+ is_parent: f('boolean'),
38
+ is_child: f('boolean'),
39
+ parent_id: f('id'),
40
+ account: f('id', { ref: 'accounts', required: true }),
41
+ category: f('id', { ref: 'categories' }),
42
+ amount: f('integer', { default: 0, required: true }),
43
+ payee: f('id', { ref: 'payees' }),
44
+ notes: f('string'),
45
+ date: f('date', { required: true }),
46
+ imported_id: f('string'),
47
+ error: f('json'),
48
+ imported_payee: f('string'),
49
+ starting_balance_flag: f('boolean'),
50
+ transfer_id: f('id'),
51
+ sort_order: f('float', { default: () => Date.now() }),
52
+ cleared: f('boolean', { default: true }),
53
+ reconciled: f('boolean', { default: false }),
54
+ tombstone: f('boolean'),
55
+ schedule: f('id', { ref: 'schedules' }),
56
+ raw_synced_data: f('string'),
57
+ // subtransactions is a special field added if the table has the
58
+ // `splits: grouped` option
59
+ },
60
+ payees: {
61
+ id: f('id'),
62
+ name: f('string', { required: true }),
63
+ transfer_acct: f('id', { ref: 'accounts' }),
64
+ tombstone: f('boolean'),
65
+ favorite: f('boolean'),
66
+ learn_categories: f('boolean'),
67
+ },
68
+ accounts: {
69
+ id: f('id'),
70
+ name: f('string', { required: true }),
71
+ offbudget: f('boolean'),
72
+ closed: f('boolean'),
73
+ sort_order: f('float'),
74
+ tombstone: f('boolean'),
75
+ account_id: f('string'),
76
+ official_name: f('string'),
77
+ account_sync_source: f('string'),
78
+ last_reconciled: f('string'),
79
+ last_sync: f('string'),
80
+ },
81
+ categories: {
82
+ id: f('id'),
83
+ name: f('string'),
84
+ is_income: f('boolean'),
85
+ hidden: f('boolean'),
86
+ group: f('id', { ref: 'category_groups' }),
87
+ goal_def: f('string'),
88
+ template_settings: f('json', { default: { source: 'notes' } }),
89
+ sort_order: f('float'),
90
+ tombstone: f('boolean'),
91
+ },
92
+ category_groups: {
93
+ id: f('id'),
94
+ name: f('string'),
95
+ is_income: f('boolean'),
96
+ hidden: f('boolean'),
97
+ sort_order: f('float'),
98
+ tombstone: f('boolean'),
99
+ },
100
+ schedules: {
101
+ id: f('id'),
102
+ name: f('string'),
103
+ rule: f('id', { ref: 'rules', required: true }),
104
+ next_date: f('date'),
105
+ completed: f('boolean'),
106
+ posts_transaction: f('boolean'),
107
+ tombstone: f('boolean'),
108
+
109
+ // These are special fields that are actually pulled from the
110
+ // underlying rule
111
+ _payee: f('id', { ref: 'payees' }),
112
+ _account: f('id', { ref: 'accounts' }),
113
+ _amount: f('json/fallback'),
114
+ _amountOp: f('string'),
115
+ _date: f('json/fallback'),
116
+ _conditions: f('json'),
117
+ _actions: f('json'),
118
+ },
119
+ rules: {
120
+ id: f('id'),
121
+ stage: f('string'),
122
+ conditions_op: f('string'),
123
+ conditions: f('json'),
124
+ actions: f('json'),
125
+ tombstone: f('boolean'),
126
+ },
127
+ notes: {
128
+ id: f('id'),
129
+ note: f('string'),
130
+ },
131
+ preferences: {
132
+ id: f('id'),
133
+ value: f('string'),
134
+ },
135
+ transaction_filters: {
136
+ id: f('id'),
137
+ name: f('string'),
138
+ conditions_op: f('string'),
139
+ conditions: f('json'),
140
+ tombstone: f('boolean'),
141
+ },
142
+ custom_reports: {
143
+ id: f('id'),
144
+ name: f('string'),
145
+ start_date: f('string', { default: '2023-06' }),
146
+ end_date: f('string', { default: '2023-09' }),
147
+ date_static: f('integer', { default: 0 }),
148
+ date_range: f('string'),
149
+ mode: f('string', { default: 'total' }),
150
+ group_by: f('string', { default: 'Category' }),
151
+ sort_by: f('string', { default: 'desc' }),
152
+ balance_type: f('string', { default: 'Expense' }),
153
+ show_empty: f('integer', { default: 0 }),
154
+ show_offbudget: f('integer', { default: 0 }),
155
+ show_hidden: f('integer', { default: 0 }),
156
+ show_uncategorized: f('integer', { default: 0 }),
157
+ trim_intervals: f('integer', { default: 0 }),
158
+ include_current: f('integer', { default: 0 }),
159
+ graph_type: f('string', { default: 'BarGraph' }),
160
+ conditions: f('json'),
161
+ conditions_op: f('string'),
162
+ metadata: f('json'),
163
+ interval: f('string', { default: 'Monthly' }),
164
+ color_scheme: f('json'),
165
+ tombstone: f('boolean'),
166
+ },
167
+ reflect_budgets: {
168
+ id: f('id'),
169
+ month: f('integer'),
170
+ category: f('string'),
171
+ amount: f('integer'),
172
+ carryover: f('integer'),
173
+ goal: f('integer'),
174
+ long_goal: f('integer'),
175
+ },
176
+ zero_budgets: {
177
+ id: f('id'),
178
+ month: f('integer'),
179
+ category: f('string', { ref: 'categories' }),
180
+ amount: f('integer'),
181
+ carryover: f('integer'),
182
+ goal: f('integer'),
183
+ long_goal: f('integer'),
184
+ },
185
+ dashboard_pages: {
186
+ id: f('id'),
187
+ name: f('string'),
188
+ tombstone: f('boolean'),
189
+ },
190
+ dashboard: {
191
+ id: f('id'),
192
+ dashboard_page_id: f('id', { ref: 'dashboard_pages' }),
193
+ type: f('string', { required: true }),
194
+ width: f('integer', { required: true }),
195
+ height: f('integer', { required: true }),
196
+ x: f('integer', { required: true }),
197
+ y: f('integer', { required: true }),
198
+ meta: f('json'),
199
+ tombstone: f('boolean'),
200
+ },
201
+ payee_locations: {
202
+ id: f('id'),
203
+ payee_id: f('id', { ref: 'payees', required: true }),
204
+ latitude: f('float', { required: true }),
205
+ longitude: f('float', { required: true }),
206
+ created_at: f('integer', { required: true }),
207
+ tombstone: f('boolean'),
208
+ },
209
+ };
210
+
211
+ export const schemaConfig: SchemaConfig = {
212
+ // Note: these views *must* represent the underlying table that we
213
+ // are mapping here. The compiler makes optimizations with this
214
+ // assumption
215
+ tableViews(name, { isJoin, withDead, tableOptions = { splits: undefined } }) {
216
+ switch (name) {
217
+ case 'transactions': {
218
+ // If joining, we always only show alive transactions. There's
219
+ // no way to configure join behavior yet
220
+ if (isJoin) {
221
+ return 'v_transactions_internal_alive';
222
+ }
223
+
224
+ const splitType = tableOptions.splits || 'inline';
225
+ // Use the view to exclude dead transactions if using `inline` or `none`
226
+ if (!withDead && (splitType === 'inline' || splitType === 'none')) {
227
+ return 'v_transactions_internal_alive';
228
+ }
229
+
230
+ // Otherwse we disregard the `withDead` option here and handle
231
+ // that in the executors to improve performance
232
+ return 'v_transactions_internal';
233
+ }
234
+
235
+ case 'schedules':
236
+ return 'v_schedules';
237
+
238
+ case 'categories':
239
+ return 'v_categories';
240
+
241
+ case 'payees':
242
+ return 'v_payees';
243
+
244
+ default:
245
+ }
246
+ return name;
247
+ },
248
+
249
+ customizeQuery(queryState) {
250
+ const { table: tableName } = queryState;
251
+
252
+ function orderBy(orders) {
253
+ // If order was specified, always add id as the last sort to make
254
+ // it deterministic
255
+ if (orders.length > 0) {
256
+ return orders.concat(['id']);
257
+ }
258
+
259
+ // Otherwise, these are the default orders for each table
260
+ switch (tableName) {
261
+ case 'transactions':
262
+ return [
263
+ { date: 'desc' },
264
+ 'starting_balance_flag',
265
+ { sort_order: 'desc' },
266
+ 'id',
267
+ ];
268
+ case 'category_groups':
269
+ return ['is_income', 'sort_order', 'id'];
270
+ case 'categories':
271
+ return ['sort_order', 'id'];
272
+ case 'payees':
273
+ return [
274
+ { $condition: { transfer_acct: null }, $dir: 'desc' },
275
+ { $nocase: '$name' },
276
+ ];
277
+ case 'accounts':
278
+ return ['sort_order', 'name'];
279
+ case 'schedules':
280
+ return [{ $condition: { completed: true } }, 'next_date'];
281
+ default:
282
+ }
283
+
284
+ return [];
285
+ }
286
+
287
+ return {
288
+ ...queryState,
289
+ orderExpressions: orderBy(queryState.orderExpressions),
290
+ };
291
+ },
292
+
293
+ views: {
294
+ payees: {
295
+ v_payees: internalFields => {
296
+ const fields = internalFields({
297
+ name: 'COALESCE(__accounts.name, _.name)',
298
+ });
299
+
300
+ return `
301
+ SELECT ${fields} FROM payees _
302
+ LEFT JOIN accounts __accounts ON (_.transfer_acct = __accounts.id AND __accounts.tombstone = 0)
303
+ -- We never want to show transfer payees that are pointing to deleted accounts.
304
+ -- Either this is not a transfer payee, if the account exists
305
+ WHERE _.transfer_acct IS NULL OR __accounts.id IS NOT NULL
306
+ `;
307
+ },
308
+ },
309
+
310
+ categories: {
311
+ fields: {
312
+ group: 'cat_group',
313
+ },
314
+
315
+ v_categories: internalFields => {
316
+ const fields = internalFields({ group: 'cat_group' });
317
+ return `SELECT ${fields} FROM categories _`;
318
+ },
319
+ },
320
+
321
+ schedules: {
322
+ v_schedules: internalFields => {
323
+ const fields = internalFields({
324
+ next_date: `
325
+ CASE
326
+ WHEN _nd.local_next_date_ts = _nd.base_next_date_ts THEN _nd.local_next_date
327
+ ELSE _nd.base_next_date
328
+ END
329
+ `,
330
+ _payee: `pm.targetId`,
331
+ _account: `json_extract(_rules.conditions, _paths.account || '.value')`,
332
+ _amount: `json_extract(_rules.conditions, _paths.amount || '.value')`,
333
+ _amountOp: `json_extract(_rules.conditions, _paths.amount || '.op')`,
334
+ _date: `json_extract(_rules.conditions, _paths.date || '.value')`,
335
+ _conditions: '_rules.conditions',
336
+ _actions: '_rules.actions',
337
+ });
338
+
339
+ return `
340
+ SELECT ${fields} FROM schedules _
341
+ LEFT JOIN schedules_next_date _nd ON _nd.schedule_id = _.id
342
+ LEFT JOIN schedules_json_paths _paths ON _paths.schedule_id = _.id
343
+ LEFT JOIN rules _rules ON _rules.id = _.rule
344
+ LEFT JOIN payee_mapping pm ON pm.id = json_extract(_rules.conditions, _paths.payee || '.value')
345
+ `;
346
+ },
347
+ },
348
+
349
+ transactions: {
350
+ fields: {
351
+ is_parent: 'isParent',
352
+ is_child: 'isChild',
353
+ account: 'acct',
354
+ imported_id: 'financial_id',
355
+ imported_payee: 'imported_description',
356
+ transfer_id: 'transferred_id',
357
+ payee: 'description',
358
+ },
359
+
360
+ v_transactions_internal: internalFields => {
361
+ // Override some fields to make custom stuff
362
+ const fields = internalFields({
363
+ payee: 'pm.targetId',
364
+ category: `CASE WHEN _.isParent = 1 THEN NULL ELSE cm.transferId END`,
365
+ amount: `IFNULL(_.amount, 0)`,
366
+ parent_id: 'CASE WHEN _.isChild = 0 THEN NULL ELSE _.parent_id END',
367
+ });
368
+
369
+ return `
370
+ SELECT ${fields} FROM transactions _
371
+ LEFT JOIN category_mapping cm ON cm.id = _.category
372
+ LEFT JOIN payee_mapping pm ON pm.id = _.description
373
+ WHERE
374
+ _.date IS NOT NULL AND
375
+ _.acct IS NOT NULL AND
376
+ (_.isChild = 0 OR _.parent_id IS NOT NULL)
377
+ `;
378
+ },
379
+
380
+ // We join on t2 to only include valid child transactions. We
381
+ // want to only include ones with valid parents, which is when
382
+ // an alive parent transaction exists
383
+ v_transactions_internal_alive: `
384
+ SELECT _.* FROM v_transactions_internal _
385
+ LEFT JOIN transactions t2 ON (_.is_child = 1 AND t2.id = _.parent_id)
386
+ WHERE IFNULL(_.tombstone, 0) = 0 AND (_.is_child = 0 OR t2.tombstone = 0)
387
+ `,
388
+
389
+ v_transactions: (_, publicFields) => {
390
+ const fields = publicFields({
391
+ payee: 'p.id',
392
+ category: 'c.id',
393
+ account: 'a.id',
394
+ });
395
+
396
+ // This adds an order, and also validates any id references by
397
+ // selecting the ids through a join which return null if they
398
+ // are dead
399
+ return `
400
+ SELECT ${fields} FROM v_transactions_internal_alive _
401
+ LEFT JOIN payees p ON (p.id = _.payee AND p.tombstone = 0)
402
+ LEFT JOIN categories c ON (c.id = _.category AND c.tombstone = 0)
403
+ LEFT JOIN accounts a ON (a.id = _.account AND a.tombstone = 0)
404
+ ORDER BY _.date desc, _.starting_balance_flag, _.sort_order desc, _.id;
405
+ `;
406
+ },
407
+ },
408
+ },
409
+ };
@@ -0,0 +1,242 @@
1
+ import {
2
+ conform,
3
+ convertForInsert,
4
+ convertForUpdate,
5
+ convertFromSelect,
6
+ } from './schema-helpers';
7
+
8
+ const basicSchema = {
9
+ transactions: {
10
+ id: { type: 'id' },
11
+ date: { type: 'date', required: true },
12
+ account: { type: 'id', required: true },
13
+ amount: { type: 'integer', default: 0, required: true },
14
+ cleared: { type: 'boolean', default: true },
15
+ notes: { type: 'text' },
16
+ sort_order: { type: 'float', default: () => Date.now() },
17
+ },
18
+ };
19
+
20
+ describe('schema-helpers', () => {
21
+ test('select converts field types', () => {
22
+ const trans = convertFromSelect(basicSchema, {}, 'transactions', {
23
+ amount: 5,
24
+ cleared: 0,
25
+ date: 20200101,
26
+ });
27
+ expect(trans).toEqual({
28
+ amount: 5,
29
+ cleared: false,
30
+ date: '2020-01-01',
31
+ });
32
+ });
33
+
34
+ test('a basic insert works', () => {
35
+ const trans = convertForInsert(basicSchema, {}, 'transactions', {
36
+ id: 't1',
37
+ account: 'foo',
38
+ amount: 5,
39
+ date: '2020-01-01',
40
+ });
41
+ expect(trans).toEqual({
42
+ id: 't1',
43
+ account: 'foo',
44
+ amount: 5,
45
+ cleared: 1,
46
+ date: 20200101,
47
+ sort_order: 123456789,
48
+ });
49
+ });
50
+
51
+ test('a basic update works', () => {
52
+ const trans = convertForUpdate(basicSchema, {}, 'transactions', {
53
+ id: 'foo',
54
+ amount: 5001,
55
+ });
56
+ expect(trans).toEqual({ id: 'foo', amount: 5001 });
57
+ });
58
+
59
+ test('insert forces required fields to exist and be non-null', () => {
60
+ expect(() => {
61
+ convertForInsert(basicSchema, {}, 'transactions2', {
62
+ id: 't1',
63
+ account: 'foo',
64
+ amount: 5,
65
+ });
66
+ }).toThrow(/"transactions2" does not exist/);
67
+
68
+ expect(() => {
69
+ convertForInsert(basicSchema, {}, 'transactions', {
70
+ id: 't1',
71
+ account: 'foo',
72
+ amount: 5,
73
+ });
74
+ }).toThrow(/"date" is required/);
75
+
76
+ expect(() => {
77
+ convertForInsert(basicSchema, {}, 'transactions', {
78
+ id: 't1',
79
+ account: 'foo',
80
+ amount: 5,
81
+ date: null,
82
+ });
83
+ }).toThrow(/"date" is required/);
84
+ });
85
+
86
+ test('update forces required fields be non-null', () => {
87
+ expect(() => {
88
+ convertForUpdate(basicSchema, {}, 'transactions2', {
89
+ id: 'trans',
90
+ account: 'acct',
91
+ amount: 5,
92
+ });
93
+ }).toThrow(/"transactions2" does not exist/);
94
+
95
+ expect(() => {
96
+ convertForUpdate(basicSchema, {}, 'transactions', {
97
+ id: 'trans',
98
+ account: 'acct',
99
+ amount: 5,
100
+ });
101
+ }).not.toThrow(/"date" is required/);
102
+
103
+ expect(() => {
104
+ convertForUpdate(basicSchema, {}, 'transactions', {
105
+ id: 'trans',
106
+ account: 'acct',
107
+ amount: 5,
108
+ date: null,
109
+ });
110
+ }).toThrow(/"date" is required/);
111
+
112
+ // It should enforce fields that have a `default` too
113
+ expect(() => {
114
+ convertForUpdate(basicSchema, {}, 'transactions', {
115
+ id: 'trans',
116
+ account: 'acct',
117
+ amount: null,
118
+ });
119
+ }).toThrow(/"amount" is required/);
120
+ });
121
+
122
+ test('conform converts types to db representations', () => {
123
+ const obj = conform(basicSchema, {}, 'transactions', {
124
+ date: '2020-01-01',
125
+ cleared: false,
126
+ });
127
+ expect(obj.date).toBe(20200101);
128
+ expect(obj.cleared).toBe(0);
129
+ });
130
+
131
+ test('conform renames fields', () => {
132
+ const obj = conform(
133
+ basicSchema,
134
+ {
135
+ views: {
136
+ transactions: { fields: { amount: 'amount2', cleared: 'cleared2' } },
137
+ },
138
+ },
139
+ 'transactions',
140
+ { amount: 100, cleared: false, date: '2020-01-01' },
141
+ );
142
+ expect(obj.amount2).toBe(100);
143
+ expect(obj.cleared2).toBe(0);
144
+ expect(obj.date).toBe(20200101);
145
+ });
146
+
147
+ test('default values are handled properly', () => {
148
+ // Amount isn't specified, but should default to 0. cleared is
149
+ // specified but is null and should default to 1
150
+ let trans = convertForInsert(basicSchema, {}, 'transactions', {
151
+ id: 't1',
152
+ account: 'foo',
153
+ date: '2020-01-01',
154
+ cleared: null,
155
+ });
156
+ expect(trans).toEqual({
157
+ id: 't1',
158
+ account: 'foo',
159
+ amount: 0,
160
+ date: 20200101,
161
+ cleared: 1,
162
+ sort_order: 123456789,
163
+ });
164
+
165
+ // Updates should never apply defaults
166
+ trans = convertForUpdate(basicSchema, {}, 'transactions', {
167
+ id: 'id',
168
+ cleared: null,
169
+ });
170
+ expect(trans).toEqual({
171
+ id: 'id',
172
+ cleared: 0,
173
+ });
174
+ });
175
+
176
+ test('null values are skipped when inserting, but not when updating', () => {
177
+ // Note how `notes` is not a part of the final object
178
+ let trans = convertForInsert(basicSchema, {}, 'transactions', {
179
+ id: 't1',
180
+ account: 'foo',
181
+ date: '2020-01-01',
182
+ notes: null,
183
+ });
184
+ expect(trans).toEqual({
185
+ id: 't1',
186
+ account: 'foo',
187
+ date: 20200101,
188
+ amount: 0,
189
+ cleared: 1,
190
+ sort_order: 123456789,
191
+ });
192
+
193
+ // `notes` is null and must be included in an update
194
+ trans = convertForUpdate(basicSchema, {}, 'transactions', {
195
+ id: 'id',
196
+ notes: null,
197
+ });
198
+ expect(trans).toEqual({
199
+ id: 'id',
200
+ notes: null,
201
+ });
202
+ });
203
+
204
+ test('floats are not allowed as input types to integers', () => {
205
+ expect(() => {
206
+ convertForUpdate(basicSchema, {}, 'transactions', {
207
+ id: 'id',
208
+ amount: 45.5,
209
+ });
210
+ }).toThrow("Can't convert to integer");
211
+ });
212
+
213
+ test('dates before 1995-01-01 are rejected', () => {
214
+ expect(() => {
215
+ convertForInsert(basicSchema, {}, 'transactions', {
216
+ id: 't1',
217
+ account: 'foo',
218
+ amount: 5,
219
+ date: '1994-12-31',
220
+ });
221
+ }).toThrow('Invalid date: 1994-12-31');
222
+
223
+ expect(() => {
224
+ convertForInsert(basicSchema, {}, 'transactions', {
225
+ id: 't1',
226
+ account: 'foo',
227
+ amount: 5,
228
+ date: '1900-01-01',
229
+ });
230
+ }).toThrow('Invalid date: 1900-01-01');
231
+ });
232
+
233
+ test('dates on or after 1995-01-01 are accepted', () => {
234
+ const trans = convertForInsert(basicSchema, {}, 'transactions', {
235
+ id: 't1',
236
+ account: 'foo',
237
+ amount: 5,
238
+ date: '1995-01-01',
239
+ });
240
+ expect(trans.date).toBe(19950101);
241
+ });
242
+ });