@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,208 @@
1
+ // @ts-strict-ignore
2
+ import { dayFromDate } from '../../shared/months';
3
+ import { fromDateRepr, toDateRepr } from '../models';
4
+
5
+ function isRequired(name, fieldDesc) {
6
+ return fieldDesc.required || name === 'id';
7
+ }
8
+
9
+ // TODO: All of the data type needs to check the input value. This
10
+ // doesn't just convert, it casts. See integer handling.
11
+ export function convertInputType(value, type) {
12
+ if (value === undefined) {
13
+ throw new Error('Query value cannot be undefined');
14
+ } else if (value === null) {
15
+ if (type === 'boolean') {
16
+ return 0;
17
+ }
18
+
19
+ return null;
20
+ }
21
+
22
+ switch (type) {
23
+ case 'date':
24
+ if (value instanceof Date) {
25
+ return toDateRepr(dayFromDate(value));
26
+ } else if (
27
+ value.match(/^\d{4}-\d{2}-\d{2}$/) == null ||
28
+ value < '1995-01-01'
29
+ ) {
30
+ throw new Error('Invalid date: ' + value);
31
+ }
32
+
33
+ return toDateRepr(value);
34
+ case 'date-month':
35
+ return toDateRepr(value.slice(0, 7));
36
+ case 'date-year':
37
+ return toDateRepr(value.slice(0, 4));
38
+ case 'boolean':
39
+ return value ? 1 : 0;
40
+ case 'id':
41
+ if (typeof value !== 'string' && value !== null) {
42
+ throw new Error('Invalid id, must be string: ' + value);
43
+ }
44
+ return value;
45
+ case 'integer':
46
+ if (typeof value === 'number' && Number.isInteger(value)) {
47
+ return value;
48
+ } else {
49
+ throw new Error("Can't convert to integer: " + JSON.stringify(value));
50
+ }
51
+ case 'json':
52
+ return JSON.stringify(value);
53
+ default:
54
+ }
55
+ return value;
56
+ }
57
+
58
+ export function convertOutputType(value, type) {
59
+ if (value === null) {
60
+ if (type === 'boolean') {
61
+ return false;
62
+ }
63
+ return null;
64
+ }
65
+
66
+ switch (type) {
67
+ case 'date':
68
+ return fromDateRepr(value);
69
+ case 'date-month':
70
+ return fromDateRepr(value).slice(0, 7);
71
+ case 'date-year':
72
+ return fromDateRepr(value).slice(0, 4);
73
+ case 'boolean':
74
+ return value === 1;
75
+ case 'json':
76
+ case 'json/fallback':
77
+ try {
78
+ return JSON.parse(value);
79
+ } catch {
80
+ return type === 'json/fallback' ? value : null;
81
+ }
82
+ default:
83
+ }
84
+
85
+ return value;
86
+ }
87
+
88
+ export function conform(
89
+ schema,
90
+ schemaConfig,
91
+ table,
92
+ obj,
93
+ { skipNull = false } = {},
94
+ ) {
95
+ const tableSchema = schema[table];
96
+ if (tableSchema == null) {
97
+ throw new Error(`Table "${table}" does not exist`);
98
+ }
99
+
100
+ const views = schemaConfig.views || {};
101
+
102
+ // Rename fields if necessary
103
+ const fieldRef = field => {
104
+ if (views[table] && views[table].fields) {
105
+ return views[table].fields[field] || field;
106
+ }
107
+ return field;
108
+ };
109
+
110
+ return Object.fromEntries(
111
+ Object.keys(obj)
112
+ .map(field => {
113
+ // Fields that start with an underscore are ignored
114
+ if (field[0] === '_') {
115
+ return null;
116
+ }
117
+
118
+ const fieldDesc = tableSchema[field];
119
+ if (fieldDesc == null) {
120
+ throw new Error(
121
+ `Field "${field}" does not exist on table ${table}: ${JSON.stringify(
122
+ obj,
123
+ )}`,
124
+ );
125
+ }
126
+
127
+ if (isRequired(field, fieldDesc) && obj[field] == null) {
128
+ throw new Error(
129
+ `"${field}" is required for table "${table}": ${JSON.stringify(
130
+ obj,
131
+ )}`,
132
+ );
133
+ }
134
+
135
+ // This option removes null values (see `convertForInsert`)
136
+ if (skipNull && obj[field] == null) {
137
+ return null;
138
+ }
139
+
140
+ return [fieldRef(field), convertInputType(obj[field], fieldDesc.type)];
141
+ })
142
+ .filter(Boolean),
143
+ );
144
+ }
145
+
146
+ export function convertForInsert(schema, schemaConfig, table, rawObj) {
147
+ const obj = { ...rawObj };
148
+
149
+ const tableSchema = schema[table];
150
+ if (tableSchema == null) {
151
+ throw new Error(`Error inserting: table "${table}" does not exist`);
152
+ }
153
+
154
+ // Inserting checks all the fields in the table and adds any default
155
+ // values necessary
156
+ Object.keys(tableSchema).forEach(field => {
157
+ const fieldDesc = tableSchema[field];
158
+
159
+ if (obj[field] == null) {
160
+ if (fieldDesc.default !== undefined) {
161
+ obj[field] =
162
+ typeof fieldDesc.default === 'function'
163
+ ? fieldDesc.default()
164
+ : fieldDesc.default;
165
+ } else if (isRequired(field, fieldDesc)) {
166
+ // Although this check is also done in `conform`, it only
167
+ // checks the fields in `obj`. For insert, we need to do it
168
+ // here to check that all required fields in the table exist
169
+ throw new Error(
170
+ `"${field}" is required for table "${table}": ${JSON.stringify(obj)}`,
171
+ );
172
+ }
173
+ }
174
+ });
175
+
176
+ // We use `skipNull` to remove any null values. There's no need to
177
+ // set those when inserting, that will be the default and it reduces
178
+ // the amount of messages generated to sync
179
+ return conform(schema, schemaConfig, table, obj, { skipNull: true });
180
+ }
181
+
182
+ export function convertForUpdate(schema, schemaConfig, table, rawObj) {
183
+ const obj = { ...rawObj };
184
+
185
+ const tableSchema = schema[table];
186
+ if (tableSchema == null) {
187
+ throw new Error(`Error updating: table "${table}" does not exist`);
188
+ }
189
+
190
+ return conform(schema, schemaConfig, table, obj);
191
+ }
192
+
193
+ export function convertFromSelect(schema, schemaConfig, table, obj) {
194
+ const tableSchema = schema[table];
195
+ if (tableSchema == null) {
196
+ throw new Error(`Table "${table}" does not exist`);
197
+ }
198
+
199
+ const fields = Object.keys(tableSchema);
200
+ const result = {};
201
+ for (let i = 0; i < fields.length; i++) {
202
+ const fieldName = fields[i];
203
+ const fieldDesc = tableSchema[fieldName];
204
+
205
+ result[fieldName] = convertOutputType(obj[fieldName], fieldDesc.type);
206
+ }
207
+ return result;
208
+ }
@@ -0,0 +1,62 @@
1
+ // @ts-strict-ignore
2
+ import * as db from '../db';
3
+
4
+ import { makeViews } from './views';
5
+
6
+ beforeEach(global.emptyDatabase());
7
+
8
+ const schema = {
9
+ transactions: {
10
+ id: { type: 'id' },
11
+ amount: { type: 'integer' },
12
+ transfer_id: { type: 'integer' },
13
+ },
14
+ };
15
+
16
+ const schemaConfig = {
17
+ views: {
18
+ transactions: {
19
+ fields: {
20
+ amount: 'a_mo_unt',
21
+ },
22
+
23
+ v_transactions1: internalFields => {
24
+ const fields = internalFields({
25
+ transfer_id: 'CASE WHEN amount < 4 THEN null ELSE transfer_id END',
26
+ });
27
+
28
+ return `SELECT ${fields} FROM transactions`;
29
+ },
30
+
31
+ v_transactions2: (_, publicFields) => {
32
+ const fields = publicFields({
33
+ transfer_id: 'COERCE(transfer_id, "foo")',
34
+ });
35
+
36
+ return `SELECT ${fields} FROM v_transactions1`;
37
+ },
38
+ },
39
+ },
40
+ };
41
+
42
+ describe('schema views', () => {
43
+ test('generates views with all the right fields', () => {
44
+ const str = makeViews(schema, schemaConfig);
45
+ expect(str).toMatch('DROP VIEW IF EXISTS v_transactions1;');
46
+ expect(str).toMatch(
47
+ 'CREATE VIEW v_transactions1 AS SELECT _.id, _.a_mo_unt AS amount, CASE WHEN amount < 4 THEN null ELSE transfer_id END AS transfer_id FROM transactions;',
48
+ );
49
+ expect(str).toMatch('DROP VIEW IF EXISTS v_transactions2;');
50
+ expect(str).toMatch(
51
+ 'CREATE VIEW v_transactions2 AS SELECT _.id, _.amount, COERCE(transfer_id, "foo") AS transfer_id FROM v_transactions1;',
52
+ );
53
+
54
+ db.execQuery('DROP TABLE transactions');
55
+ db.execQuery(
56
+ 'CREATE TABLE transactions (id TEXT PRIMARY KEY, a_mo_unt INTEGER, transfer_id TEXT)',
57
+ );
58
+
59
+ // Make sure the string is valid SQL
60
+ db.execQuery(str);
61
+ });
62
+ });
@@ -0,0 +1,57 @@
1
+ // @ts-strict-ignore
2
+ import { quoteAlias } from './compiler';
3
+
4
+ function selectFields(fields) {
5
+ return Object.keys(fields)
6
+ .map(as => {
7
+ let field = fields[as];
8
+ const needsAs = field !== as;
9
+ // If it's just an identifier, we automatically prefix it with
10
+ // `_.` which makes sure it references the root table
11
+ if (!field.match(/[ .]/)) {
12
+ field = `_.${field}`;
13
+ }
14
+ return needsAs ? `${field} AS ${quoteAlias(as)}` : `${field}`;
15
+ })
16
+ .join(', ');
17
+ }
18
+
19
+ export function makeViews(schema, schemaConfig) {
20
+ const views = schemaConfig.views;
21
+ const viewStrs = [];
22
+
23
+ Object.keys(views).forEach(table => {
24
+ const { fields: fieldMappings = {}, ...tableViews } = views[table];
25
+
26
+ const publicFields = Object.fromEntries(
27
+ Object.keys(schema[table]).map(name => [name, name]),
28
+ );
29
+ const internalFields = { ...publicFields, ...fieldMappings };
30
+
31
+ Object.keys(tableViews).forEach(viewName => {
32
+ const publicMaker = overrides => {
33
+ const fields = { ...publicFields, ...overrides };
34
+ return selectFields(fields);
35
+ };
36
+ const internalMaker = overrides => {
37
+ const fields = { ...internalFields, ...overrides };
38
+ return selectFields(fields);
39
+ };
40
+
41
+ let sql;
42
+ if (typeof tableViews[viewName] === 'function') {
43
+ sql = tableViews[viewName](internalMaker, publicMaker);
44
+ } else {
45
+ sql = tableViews[viewName];
46
+ }
47
+ sql = sql.trim().replace(/;$/, '');
48
+
49
+ viewStrs.push(`
50
+ DROP VIEW IF EXISTS ${viewName};
51
+ CREATE VIEW ${viewName} AS ${sql};
52
+ `);
53
+ });
54
+ });
55
+
56
+ return viewStrs.join('\n');
57
+ }
@@ -0,0 +1,387 @@
1
+ import * as asyncStorage from '../../platform/server/asyncStorage';
2
+ import { logger } from '../../platform/server/log';
3
+ import type { OpenIdConfig } from '../../types/models';
4
+ import { createApp } from '../app';
5
+ import * as encryption from '../encryption';
6
+ import { PostError } from '../errors';
7
+ import { get, post } from '../post';
8
+ import { getServer, isValidBaseURL } from '../server-config';
9
+
10
+ export type AuthHandlers = {
11
+ 'get-did-bootstrap': typeof didBootstrap;
12
+ 'subscribe-needs-bootstrap': typeof needsBootstrap;
13
+ 'subscribe-bootstrap': typeof bootstrap;
14
+ 'subscribe-get-login-methods': typeof getLoginMethods;
15
+ 'subscribe-get-user': typeof getUser;
16
+ 'subscribe-change-password': typeof changePassword;
17
+ 'subscribe-sign-in': typeof signIn;
18
+ 'subscribe-sign-out': typeof signOut;
19
+ 'subscribe-set-token': typeof setToken;
20
+ 'enable-openid': typeof enableOpenId;
21
+ 'get-openid-config': typeof getOpenIdConfig;
22
+ 'enable-password': typeof enablePassword;
23
+ };
24
+
25
+ export const app = createApp<AuthHandlers>();
26
+ app.method('get-did-bootstrap', didBootstrap);
27
+ app.method('subscribe-needs-bootstrap', needsBootstrap);
28
+ app.method('subscribe-bootstrap', bootstrap);
29
+ app.method('subscribe-get-login-methods', getLoginMethods);
30
+ app.method('subscribe-get-user', getUser);
31
+ app.method('subscribe-change-password', changePassword);
32
+ app.method('subscribe-sign-in', signIn);
33
+ app.method('subscribe-sign-out', signOut);
34
+ app.method('subscribe-set-token', setToken);
35
+ app.method('enable-openid', enableOpenId);
36
+ app.method('get-openid-config', getOpenIdConfig);
37
+ app.method('enable-password', enablePassword);
38
+
39
+ async function didBootstrap() {
40
+ return Boolean(await asyncStorage.getItem('did-bootstrap'));
41
+ }
42
+
43
+ async function needsBootstrap({ url }: { url?: string } = {}) {
44
+ if (url && !isValidBaseURL(url)) {
45
+ return { error: 'get-server-failure' };
46
+ }
47
+
48
+ let serverConfig: ReturnType<typeof getServer>;
49
+
50
+ try {
51
+ serverConfig = getServer(url);
52
+ if (!serverConfig) {
53
+ return { bootstrapped: true, hasServer: false };
54
+ }
55
+ } catch {
56
+ return { error: 'get-server-failure' };
57
+ }
58
+
59
+ let resText: string;
60
+ try {
61
+ resText = await get(serverConfig.SIGNUP_SERVER + '/needs-bootstrap');
62
+ } catch {
63
+ return { error: 'network-failure' };
64
+ }
65
+
66
+ let res: {
67
+ status: 'ok';
68
+ data: {
69
+ bootstrapped: boolean;
70
+ loginMethod: 'password' | 'openid' | string;
71
+ availableLoginMethods: Array<{
72
+ method: string;
73
+ displayName: string;
74
+ active: boolean;
75
+ }>;
76
+ multiuser: boolean;
77
+ };
78
+ };
79
+
80
+ try {
81
+ res = JSON.parse(resText);
82
+ } catch {
83
+ return { error: 'parse-failure' };
84
+ }
85
+
86
+ return {
87
+ bootstrapped: res.data.bootstrapped,
88
+ availableLoginMethods: res.data.availableLoginMethods || [
89
+ { method: 'password', active: true, displayName: 'Password' },
90
+ ],
91
+ multiuser: res.data.multiuser || false,
92
+ hasServer: true,
93
+ };
94
+ }
95
+
96
+ async function bootstrap(loginConfig: {
97
+ password?: string;
98
+ openId?: OpenIdConfig;
99
+ }) {
100
+ try {
101
+ const serverConfig = getServer();
102
+ if (!serverConfig) {
103
+ throw new Error('No sync server configured.');
104
+ }
105
+ await post(serverConfig.SIGNUP_SERVER + '/bootstrap', loginConfig);
106
+ } catch (err) {
107
+ if (err instanceof PostError) {
108
+ return {
109
+ error: err.reason || 'network-failure',
110
+ };
111
+ }
112
+
113
+ throw err;
114
+ }
115
+ return {};
116
+ }
117
+
118
+ async function getLoginMethods() {
119
+ let res: {
120
+ methods?: Array<{ method: string; displayName: string; active: boolean }>;
121
+ };
122
+ try {
123
+ const serverConfig = getServer();
124
+ if (!serverConfig) {
125
+ throw new Error('No sync server configured.');
126
+ }
127
+ res = await fetch(serverConfig.SIGNUP_SERVER + '/login-methods').then(res =>
128
+ res.json(),
129
+ );
130
+ } catch (err) {
131
+ if (err instanceof PostError) {
132
+ return {
133
+ error: err.reason || 'network-failure',
134
+ };
135
+ }
136
+
137
+ throw err;
138
+ }
139
+
140
+ if (res.methods) {
141
+ return { methods: res.methods };
142
+ }
143
+ return { error: 'internal' };
144
+ }
145
+
146
+ async function getUser() {
147
+ const serverConfig = getServer();
148
+ if (!serverConfig) {
149
+ if (!(await asyncStorage.getItem('did-bootstrap'))) {
150
+ return null;
151
+ }
152
+ return { offline: false };
153
+ }
154
+
155
+ const userToken = await asyncStorage.getItem('user-token');
156
+
157
+ if (!userToken) {
158
+ return null;
159
+ }
160
+
161
+ try {
162
+ const res = await get(serverConfig.SIGNUP_SERVER + '/validate', {
163
+ headers: {
164
+ 'X-ACTUAL-TOKEN': userToken,
165
+ },
166
+ });
167
+ let tokenExpired = false;
168
+ const {
169
+ status,
170
+ reason,
171
+ data: {
172
+ userName = null,
173
+ permission = '',
174
+ userId = null,
175
+ displayName = null,
176
+ loginMethod = null,
177
+ prefs: serverPrefs,
178
+ } = {},
179
+ } = JSON.parse(res) || {};
180
+
181
+ if (status === 'error') {
182
+ if (reason === 'unauthorized') {
183
+ return null;
184
+ } else if (reason === 'token-expired') {
185
+ tokenExpired = true;
186
+ } else {
187
+ return { offline: true };
188
+ }
189
+ }
190
+
191
+ return {
192
+ offline: false,
193
+ userName,
194
+ permission,
195
+ userId,
196
+ displayName,
197
+ loginMethod,
198
+ tokenExpired,
199
+ serverPrefs,
200
+ };
201
+ } catch (e) {
202
+ logger.log(e);
203
+ return { offline: true };
204
+ }
205
+ }
206
+
207
+ async function changePassword({ password }: { password: string }) {
208
+ const userToken = await asyncStorage.getItem('user-token');
209
+ if (!userToken) {
210
+ return { error: 'not-logged-in' };
211
+ }
212
+
213
+ try {
214
+ const serverConfig = getServer();
215
+ if (!serverConfig) {
216
+ throw new Error('No sync server configured.');
217
+ }
218
+ await post(serverConfig.SIGNUP_SERVER + '/change-password', {
219
+ token: userToken,
220
+ password,
221
+ });
222
+ } catch (err) {
223
+ if (err instanceof PostError) {
224
+ return {
225
+ error: err.reason || 'network-failure',
226
+ };
227
+ }
228
+
229
+ throw err;
230
+ }
231
+
232
+ return {};
233
+ }
234
+
235
+ async function signIn(
236
+ loginInfo:
237
+ | {
238
+ password: string;
239
+ loginMethod?: string;
240
+ }
241
+ | {
242
+ returnUrl: string;
243
+ loginMethod?: 'openid';
244
+ },
245
+ ) {
246
+ if (
247
+ typeof loginInfo.loginMethod !== 'string' ||
248
+ loginInfo.loginMethod == null
249
+ ) {
250
+ loginInfo.loginMethod = 'password';
251
+ }
252
+ let res: {
253
+ token?: string;
254
+ returnUrl?: string;
255
+ };
256
+
257
+ try {
258
+ const serverConfig = getServer();
259
+ if (!serverConfig) {
260
+ throw new Error('No sync server configured.');
261
+ }
262
+ res = await post(serverConfig.SIGNUP_SERVER + '/login', loginInfo);
263
+ } catch (err) {
264
+ if (err instanceof PostError) {
265
+ return {
266
+ error: err.reason || 'network-failure',
267
+ };
268
+ }
269
+
270
+ throw err;
271
+ }
272
+
273
+ if (res.returnUrl) {
274
+ return { redirectUrl: res.returnUrl };
275
+ }
276
+
277
+ if (!res.token) {
278
+ throw new Error('login: User token not set');
279
+ }
280
+
281
+ await asyncStorage.setItem('user-token', res.token);
282
+ return {};
283
+ }
284
+
285
+ async function signOut() {
286
+ encryption.unloadAllKeys();
287
+ await asyncStorage.multiRemove([
288
+ 'user-token',
289
+ 'encrypt-keys',
290
+ 'lastBudget',
291
+ 'readOnly',
292
+ ]);
293
+ return 'ok';
294
+ }
295
+
296
+ async function setToken({ token }: { token: string }) {
297
+ await asyncStorage.setItem('user-token', token);
298
+ }
299
+
300
+ async function enableOpenId(openIdConfig: { openId: OpenIdConfig }) {
301
+ try {
302
+ const userToken = await asyncStorage.getItem('user-token');
303
+
304
+ if (!userToken) {
305
+ return { error: 'unauthorized' };
306
+ }
307
+
308
+ const serverConfig = getServer();
309
+ if (!serverConfig) {
310
+ throw new Error('No sync server configured.');
311
+ }
312
+
313
+ await post(serverConfig.BASE_SERVER + '/openid/enable', openIdConfig, {
314
+ 'X-ACTUAL-TOKEN': userToken,
315
+ });
316
+ } catch (err) {
317
+ if (err instanceof PostError) {
318
+ return {
319
+ error: err.reason || 'network-failure',
320
+ };
321
+ }
322
+
323
+ throw err;
324
+ }
325
+ return {};
326
+ }
327
+
328
+ async function getOpenIdConfig({ password }: { password: string }) {
329
+ try {
330
+ const userToken = await asyncStorage.getItem('user-token');
331
+
332
+ const serverConfig = getServer();
333
+ if (!serverConfig) {
334
+ throw new Error('No sync server configured.');
335
+ }
336
+
337
+ const res = await post(
338
+ serverConfig.BASE_SERVER + '/openid/config',
339
+ { password },
340
+ {
341
+ 'X-ACTUAL-TOKEN': userToken,
342
+ },
343
+ );
344
+
345
+ if (res) {
346
+ return res as { openId: OpenIdConfig };
347
+ }
348
+
349
+ return null;
350
+ } catch (err) {
351
+ if (err instanceof PostError) {
352
+ return {
353
+ error: err.reason || 'network-failure',
354
+ };
355
+ }
356
+
357
+ throw err;
358
+ }
359
+ }
360
+
361
+ async function enablePassword(passwordConfig: { password: string }) {
362
+ try {
363
+ const userToken = await asyncStorage.getItem('user-token');
364
+
365
+ if (!userToken) {
366
+ return { error: 'unauthorized' };
367
+ }
368
+
369
+ const serverConfig = getServer();
370
+ if (!serverConfig) {
371
+ throw new Error('No sync server configured.');
372
+ }
373
+
374
+ await post(serverConfig.BASE_SERVER + '/openid/disable', passwordConfig, {
375
+ 'X-ACTUAL-TOKEN': userToken,
376
+ });
377
+ } catch (err) {
378
+ if (err instanceof PostError) {
379
+ return {
380
+ error: err.reason || 'network-failure',
381
+ };
382
+ }
383
+
384
+ throw err;
385
+ }
386
+ return {};
387
+ }