@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,1222 @@
1
+ import { getNormalisedString } from '../../shared/normalisation';
2
+ import type { QueryState } from '../../shared/query';
3
+
4
+ // @ts-strict-ignore
5
+ let _uid = 0;
6
+ function resetUid() {
7
+ _uid = 0;
8
+ }
9
+
10
+ function uid(tableName) {
11
+ _uid++;
12
+ return tableName + _uid;
13
+ }
14
+
15
+ class CompileError extends Error {}
16
+
17
+ function nativeDateToInt(date) {
18
+ const pad = x => (x < 10 ? '0' : '') + x;
19
+ return date.getFullYear() + pad(date.getMonth() + 1) + pad(date.getDate());
20
+ }
21
+
22
+ function dateToInt(date) {
23
+ return parseInt(date.replace(/-/g, ''));
24
+ }
25
+
26
+ function addTombstone(schema, tableName, tableId, whereStr) {
27
+ const hasTombstone = schema[tableName].tombstone != null;
28
+ return hasTombstone ? `${whereStr} AND ${tableId}.tombstone = 0` : whereStr;
29
+ }
30
+
31
+ function popPath(path) {
32
+ const parts = path.split('.');
33
+ return { path: parts.slice(0, -1).join('.'), field: parts[parts.length - 1] };
34
+ }
35
+
36
+ function isKeyword(str) {
37
+ return str === 'group';
38
+ }
39
+
40
+ export function quoteAlias(alias) {
41
+ return alias.indexOf('.') === -1 && !isKeyword(alias) ? alias : `"${alias}"`;
42
+ }
43
+
44
+ function typed(value, type, { literal = false } = {}) {
45
+ return { value, type, literal };
46
+ }
47
+
48
+ function getFieldDescription(schema, tableName, field) {
49
+ if (schema[tableName] == null) {
50
+ throw new CompileError(`Table "${tableName}" does not exist in the schema`);
51
+ }
52
+
53
+ const fieldDesc = schema[tableName][field];
54
+ if (fieldDesc == null) {
55
+ throw new CompileError(
56
+ `Field "${field}" does not exist in table "${tableName}"`,
57
+ );
58
+ }
59
+ return fieldDesc;
60
+ }
61
+
62
+ function makePath(state, path) {
63
+ const { schema, paths } = state;
64
+
65
+ const parts = path.split('.');
66
+ if (parts.length < 2) {
67
+ throw new CompileError('Invalid path: ' + path);
68
+ }
69
+
70
+ const initialTable = parts[0];
71
+
72
+ const tableName = parts.slice(1).reduce((tableName, field) => {
73
+ const table = schema[tableName];
74
+
75
+ if (table == null) {
76
+ throw new CompileError(`Path error: ${tableName} table does not exist`);
77
+ }
78
+
79
+ if (!table[field] || table[field].ref == null) {
80
+ throw new CompileError(
81
+ `Field not joinable on table ${tableName}: "${field}"`,
82
+ );
83
+ }
84
+
85
+ return table[field].ref;
86
+ }, initialTable);
87
+
88
+ let joinTable;
89
+ const parentParts = parts.slice(0, -1);
90
+ if (parentParts.length === 1) {
91
+ joinTable = parentParts[0];
92
+ } else {
93
+ const parentPath = parentParts.join('.');
94
+ const parentDesc = paths.get(parentPath);
95
+ if (!parentDesc) {
96
+ throw new CompileError('Path does not exist: ' + parentPath);
97
+ }
98
+ joinTable = parentDesc.tableId;
99
+ }
100
+
101
+ return {
102
+ tableName,
103
+ tableId: uid(tableName),
104
+ joinField: parts[parts.length - 1],
105
+ joinTable,
106
+ };
107
+ }
108
+
109
+ function resolvePath(state, path) {
110
+ let paths = path.split('.');
111
+
112
+ paths = paths.reduce(
113
+ (acc, name) => {
114
+ const fullName = acc.context + '.' + name;
115
+ return {
116
+ context: fullName,
117
+ path: [...acc.path, fullName],
118
+ };
119
+ },
120
+ { context: state.implicitTableName, path: [] },
121
+ ).path;
122
+
123
+ paths.forEach(path => {
124
+ if (!state.paths.get(path)) {
125
+ state.paths.set(path, makePath(state, path));
126
+ }
127
+ });
128
+
129
+ const pathInfo = state.paths.get(paths[paths.length - 1]);
130
+ return pathInfo;
131
+ }
132
+
133
+ function transformField(state, name) {
134
+ if (typeof name !== 'string') {
135
+ throw new CompileError('Invalid field name, must be a string');
136
+ }
137
+
138
+ const { path, field: originalField } = popPath(name);
139
+
140
+ let field = originalField;
141
+ let pathInfo;
142
+ if (path === '') {
143
+ pathInfo = {
144
+ tableName: state.implicitTableName,
145
+ tableId: state.implicitTableId,
146
+ };
147
+ } else {
148
+ pathInfo = resolvePath(state, path);
149
+ }
150
+
151
+ const fieldDesc = getFieldDescription(
152
+ state.schema,
153
+ pathInfo.tableName,
154
+ field,
155
+ );
156
+
157
+ // If this is a field that references an item in another table, that
158
+ // item could have been deleted. If that's the case, we want to
159
+ // return `null` instead of an id pointing to a deleted item. This
160
+ // converts an id reference into a path that pulls the id through a
161
+ // table join which will filter out dead items, resulting in a
162
+ // `null` id if the item is deleted
163
+ if (
164
+ state.validateRefs &&
165
+ fieldDesc.ref &&
166
+ fieldDesc.type === 'id' &&
167
+ field !== 'id'
168
+ ) {
169
+ const refPath = state.implicitTableName + '.' + name;
170
+ let refPathInfo = state.paths.get(refPath);
171
+
172
+ if (!refPathInfo) {
173
+ refPathInfo = makePath(state, refPath);
174
+ state.paths.set(refPath, refPathInfo);
175
+ }
176
+
177
+ field = 'id';
178
+ pathInfo = refPathInfo;
179
+ }
180
+
181
+ const fieldStr = pathInfo.tableId + '.' + field;
182
+ return typed(fieldStr, fieldDesc.type);
183
+ }
184
+
185
+ function parseDate(str) {
186
+ const m = str.match(/^(\d{4}-\d{2}-\d{2})$/);
187
+ if (m) {
188
+ return typed(dateToInt(m[1]), 'date', { literal: true });
189
+ }
190
+ return null;
191
+ }
192
+
193
+ function parseMonth(str) {
194
+ const m = str.match(/^(\d{4}-\d{2})$/);
195
+ if (m) {
196
+ return typed(dateToInt(m[1]), 'date', { literal: true });
197
+ }
198
+ return null;
199
+ }
200
+
201
+ function parseYear(str) {
202
+ const m = str.match(/^(\d{4})$/);
203
+ if (m) {
204
+ return typed(dateToInt(m[1]), 'date', { literal: true });
205
+ }
206
+ return null;
207
+ }
208
+
209
+ function badDateFormat(str, type) {
210
+ throw new CompileError(`Bad ${type} format: ${str}`);
211
+ }
212
+
213
+ function inferParam(param, type) {
214
+ const existingType = param.paramType;
215
+ if (existingType) {
216
+ const casts = {
217
+ date: ['string'],
218
+ 'date-month': ['date'],
219
+ 'date-year': ['date', 'date-month'],
220
+ id: ['string'],
221
+ float: ['integer'],
222
+ };
223
+
224
+ if (
225
+ existingType !== type &&
226
+ (!casts[type] || !casts[type].includes(existingType))
227
+ ) {
228
+ throw new Error(
229
+ `Parameter "${param.paramName}" can't convert to ${type} (already inferred as ${existingType})`,
230
+ );
231
+ }
232
+ } else {
233
+ param.paramType = type;
234
+ }
235
+ }
236
+
237
+ function castInput(state, expr, type) {
238
+ if (expr.type === type) {
239
+ return expr;
240
+ } else if (expr.type === 'param') {
241
+ inferParam(expr, type);
242
+ return typed(expr.value, type);
243
+ } else if (expr.type === 'null') {
244
+ if (!expr.literal) {
245
+ throw new CompileError("A non-literal null doesn't make sense");
246
+ }
247
+
248
+ if (type === 'boolean') {
249
+ return typed(0, 'boolean', { literal: true });
250
+ }
251
+ return expr;
252
+ }
253
+
254
+ // These are all things that can be safely casted automatically
255
+ if (type === 'date') {
256
+ if (expr.type === 'string') {
257
+ if (expr.literal) {
258
+ return parseDate(expr.value) || badDateFormat(expr.value, 'date');
259
+ } else {
260
+ throw new CompileError(
261
+ 'Casting string fields to dates is not supported',
262
+ );
263
+ }
264
+ }
265
+
266
+ throw new CompileError(`Can't cast ${expr.type} to date`);
267
+ } else if (type === 'date-month') {
268
+ let expr2;
269
+ if (expr.type === 'date') {
270
+ expr2 = expr;
271
+ } else if (expr.type === 'string' || expr.type === 'any') {
272
+ expr2 =
273
+ parseMonth(expr.value) ||
274
+ parseDate(expr.value) ||
275
+ badDateFormat(expr.value, 'date-month');
276
+ } else {
277
+ throw new CompileError(`Can't cast ${expr.type} to date-month`);
278
+ }
279
+
280
+ if (expr2.literal) {
281
+ return typed(
282
+ dateToInt(expr2.value.toString().slice(0, 6)),
283
+ 'date-month',
284
+ { literal: true },
285
+ );
286
+ } else {
287
+ return typed(
288
+ `CAST(SUBSTR(${expr2.value}, 1, 6) AS integer)`,
289
+ 'date-month',
290
+ );
291
+ }
292
+ } else if (type === 'date-year') {
293
+ let expr2;
294
+ if (expr.type === 'date' || expr.type === 'date-month') {
295
+ expr2 = expr;
296
+ } else if (expr.type === 'string') {
297
+ expr2 =
298
+ parseYear(expr.value) ||
299
+ parseMonth(expr.value) ||
300
+ parseDate(expr.value) ||
301
+ badDateFormat(expr.value, 'date-year');
302
+ } else {
303
+ throw new CompileError(`Can't cast ${expr.type} to date-year`);
304
+ }
305
+
306
+ if (expr2.literal) {
307
+ return typed(dateToInt(expr2.value.toString().slice(0, 4)), 'date-year', {
308
+ literal: true,
309
+ });
310
+ } else {
311
+ return typed(
312
+ `CAST(SUBSTR(${expr2.value}, 1, 4) AS integer)`,
313
+ 'date-year',
314
+ );
315
+ }
316
+ } else if (type === 'id') {
317
+ if (expr.type === 'string') {
318
+ return typed(expr.value, 'id', { literal: expr.literal });
319
+ }
320
+ } else if (type === 'float') {
321
+ if (expr.type === 'integer') {
322
+ return typed(expr.value, 'float', { literal: expr.literal });
323
+ }
324
+ }
325
+
326
+ if (expr.type === 'any') {
327
+ return typed(expr.value, type, { literal: expr.literal });
328
+ }
329
+
330
+ throw new CompileError(`Can't convert ${expr.type} to ${type}`);
331
+ }
332
+
333
+ // TODO: remove state from these functions
334
+ function val(state, expr, type?: string) {
335
+ let castedExpr = expr;
336
+
337
+ // Cast the type if necessary
338
+ if (type) {
339
+ castedExpr = castInput(state, expr, type);
340
+ }
341
+
342
+ if (castedExpr.literal) {
343
+ if (castedExpr.type === 'id') {
344
+ return `'${castedExpr.value}'`;
345
+ } else if (castedExpr.type === 'string') {
346
+ // Escape quotes
347
+ const value = castedExpr.value.replace(/'/g, "''");
348
+ return `'${value}'`;
349
+ }
350
+ }
351
+
352
+ return castedExpr.value;
353
+ }
354
+
355
+ function valArray(state, arr: unknown[], types?: string[]) {
356
+ return arr.map((value, idx) => val(state, value, types ? types[idx] : null));
357
+ }
358
+
359
+ function validateArgLength(arr: unknown[], min: number, max?: number) {
360
+ if (max == null) {
361
+ max = min;
362
+ }
363
+
364
+ if (min != null && arr.length < min) {
365
+ throw new CompileError('Too few arguments');
366
+ }
367
+ if (max != null && arr.length > max) {
368
+ throw new CompileError('Too many arguments');
369
+ }
370
+ }
371
+
372
+ //// Nice errors
373
+
374
+ function saveStack(type, func) {
375
+ return (state, ...args) => {
376
+ if (state == null || state.compileStack == null) {
377
+ throw new CompileError(
378
+ 'This function cannot track error data. ' +
379
+ 'It needs to accept the compiler state as the first argument.',
380
+ );
381
+ }
382
+
383
+ state.compileStack.push({ type, args });
384
+ const ret = func(state, ...args);
385
+ state.compileStack.pop();
386
+ return ret;
387
+ };
388
+ }
389
+
390
+ function prettyValue(value) {
391
+ if (typeof value === 'string') {
392
+ return value;
393
+ } else if (value === undefined) {
394
+ return 'undefined';
395
+ }
396
+
397
+ const str = JSON.stringify(value);
398
+ if (str.length > 70) {
399
+ const expanded = JSON.stringify(value, null, 2);
400
+ return expanded.split('\n').join('\n ');
401
+ }
402
+ return str;
403
+ }
404
+
405
+ function getCompileError(error, stack) {
406
+ if (stack.length === 0) {
407
+ return error;
408
+ }
409
+
410
+ let stackStr = stack
411
+ .slice(1)
412
+ .reverse()
413
+ .map(entry => {
414
+ switch (entry.type) {
415
+ case 'expr':
416
+ case 'function':
417
+ return prettyValue(entry.args[0]);
418
+ case 'op': {
419
+ const [fieldRef, opData] = entry.args;
420
+ return prettyValue({ [fieldRef]: opData });
421
+ }
422
+ case 'value':
423
+ return prettyValue(entry.value);
424
+ default:
425
+ return '';
426
+ }
427
+ })
428
+ .map(str => '\n ' + str)
429
+ .join('');
430
+
431
+ const rootMethod = stack[0].type;
432
+ const methodArgs = stack[0].args[0];
433
+ stackStr += `\n ${rootMethod}(${prettyValue(
434
+ methodArgs.length === 1 ? methodArgs[0] : methodArgs,
435
+ )})`;
436
+
437
+ // In production, hide internal stack traces
438
+ if (process.env.NODE_ENV === 'production') {
439
+ const err = new CompileError();
440
+ err.message = `${error.message}\n\nExpression stack:` + stackStr;
441
+ err.stack = null;
442
+ return err;
443
+ }
444
+
445
+ error.message = `${error.message}\n\nExpression stack:` + stackStr;
446
+ return error;
447
+ }
448
+
449
+ //// Compiler
450
+
451
+ function compileLiteral(value) {
452
+ if (value === undefined) {
453
+ throw new CompileError('`undefined` is not a valid query value');
454
+ } else if (value === null) {
455
+ return typed('NULL', 'null', { literal: true });
456
+ } else if (value instanceof Date) {
457
+ return typed(nativeDateToInt(value), 'date', { literal: true });
458
+ } else if (typeof value === 'string') {
459
+ // Allow user to escape $, and quote the string to make it a
460
+ // string literal in the output
461
+ value = value.replace(/\\\$/g, '$');
462
+ return typed(value, 'string', { literal: true });
463
+ } else if (typeof value === 'boolean') {
464
+ return typed(value ? 1 : 0, 'boolean', { literal: true });
465
+ } else if (typeof value === 'number') {
466
+ return typed(value, Number.isInteger(value) ? 'integer' : 'float', {
467
+ literal: true,
468
+ });
469
+ } else if (Array.isArray(value)) {
470
+ return typed(value, 'array', { literal: true });
471
+ } else {
472
+ throw new CompileError(
473
+ 'Unsupported type of expression: ' + JSON.stringify(value),
474
+ );
475
+ }
476
+ }
477
+
478
+ const compileExpr = saveStack('expr', (state, expr) => {
479
+ if (typeof expr === 'string') {
480
+ // Field reference
481
+ if (expr[0] === '$') {
482
+ const fieldRef = expr === '$' ? state.implicitField : expr.slice(1);
483
+
484
+ if (fieldRef == null || fieldRef === '') {
485
+ throw new CompileError('Invalid field reference: ' + expr);
486
+ }
487
+
488
+ return transformField(state, fieldRef);
489
+ }
490
+
491
+ // Named parameter
492
+ if (expr[0] === ':') {
493
+ const param = { value: '?', type: 'param', paramName: expr.slice(1) };
494
+ state.namedParameters.push(param);
495
+ return param;
496
+ }
497
+ }
498
+
499
+ if (expr !== null) {
500
+ if (Array.isArray(expr)) {
501
+ return compileLiteral(expr);
502
+ } else if (
503
+ typeof expr === 'object' &&
504
+ Object.keys(expr).find(k => k[0] === '$')
505
+ ) {
506
+ // It's a function call
507
+ return compileFunction(state, expr);
508
+ }
509
+ }
510
+
511
+ return compileLiteral(expr);
512
+ });
513
+
514
+ const compileFunction = saveStack('function', (state, func) => {
515
+ const [name] = Object.keys(func);
516
+ let argExprs = func[name];
517
+ if (!Array.isArray(argExprs)) {
518
+ argExprs = [argExprs];
519
+ }
520
+
521
+ if (name[0] !== '$') {
522
+ throw new CompileError(
523
+ `Unknown property "${name}." Did you mean to call a function? Try prefixing it with $`,
524
+ );
525
+ }
526
+
527
+ let args = argExprs;
528
+ // `$condition` is a special-case where it will be evaluated later
529
+ if (name !== '$condition') {
530
+ args = argExprs.map(arg => compileExpr(state, arg));
531
+ }
532
+
533
+ switch (name) {
534
+ // aggregate functions
535
+ case '$sum': {
536
+ validateArgLength(args, 1);
537
+ const [arg1] = valArray(state, args, ['float']);
538
+ return typed(`SUM(${arg1})`, args[0].type);
539
+ }
540
+
541
+ case '$sumOver': {
542
+ const [arg1] = valArray(state, args, ['float']);
543
+ const order = state.orders
544
+ ? 'ORDER BY ' + compileOrderBy(state, state.orders)
545
+ : '';
546
+
547
+ return typed(
548
+ `(SUM(${arg1}) OVER (${order} ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING))`,
549
+ args[0].type,
550
+ );
551
+ }
552
+
553
+ case '$count': {
554
+ validateArgLength(args, 1);
555
+ const [arg1] = valArray(state, args);
556
+ return typed(`COUNT(${arg1})`, 'integer');
557
+ }
558
+
559
+ // string functions
560
+ case '$substr': {
561
+ validateArgLength(args, 2, 3);
562
+ const [arg1, arg2, arg3] = valArray(state, args, [
563
+ 'string',
564
+ 'integer',
565
+ 'integer',
566
+ ]);
567
+ return typed(`SUBSTR(${arg1}, ${arg2}, ${arg3})`, 'string');
568
+ }
569
+ case '$lower': {
570
+ validateArgLength(args, 1);
571
+ const [arg1] = valArray(state, args, ['string']);
572
+ return typed(`UNICODE_LOWER(${arg1})`, 'string');
573
+ }
574
+
575
+ // integer/float functions
576
+ case '$neg': {
577
+ validateArgLength(args, 1);
578
+ valArray(state, args, ['float']);
579
+ return typed(`(-${val(state, args[0])})`, args[0].type);
580
+ }
581
+ case '$abs': {
582
+ validateArgLength(args, 1);
583
+ valArray(state, args, ['float']);
584
+ return typed(`ABS(${val(state, args[0])})`, args[0].type);
585
+ }
586
+ case '$idiv': {
587
+ validateArgLength(args, 2);
588
+ valArray(state, args, ['integer', 'integer']);
589
+ return typed(
590
+ `(${val(state, args[0])} / ${val(state, args[1])})`,
591
+ args[0].type,
592
+ );
593
+ }
594
+
595
+ // id functions
596
+ case '$id': {
597
+ validateArgLength(args, 1);
598
+ return typed(val(state, args[0]), args[0].type);
599
+ }
600
+
601
+ // date functions
602
+ case '$day': {
603
+ validateArgLength(args, 1);
604
+ return castInput(state, args[0], 'date');
605
+ }
606
+ case '$month': {
607
+ validateArgLength(args, 1);
608
+ return castInput(state, args[0], 'date-month');
609
+ }
610
+ case '$year': {
611
+ validateArgLength(args, 1);
612
+ return castInput(state, args[0], 'date-year');
613
+ }
614
+
615
+ // various functions
616
+ case '$condition':
617
+ validateArgLength(args, 1);
618
+ const conds = compileConditions(state, args[0]);
619
+ return typed(conds.join(' AND '), 'boolean');
620
+
621
+ case '$nocase':
622
+ validateArgLength(args, 1);
623
+ const [arg1] = valArray(state, args, ['string']);
624
+ return typed(`${arg1} COLLATE NOCASE`, args[0].type);
625
+
626
+ case '$literal': {
627
+ validateArgLength(args, 1);
628
+ if (!args[0].literal) {
629
+ throw new CompileError('Literal not passed to $literal');
630
+ }
631
+ return args[0];
632
+ }
633
+ default:
634
+ throw new CompileError(`Unknown function: ${name}`);
635
+ }
636
+ });
637
+
638
+ const compileOp = saveStack('op', (state, fieldRef, opData) => {
639
+ const { $transform, ...opExpr } = opData;
640
+ const [op] = Object.keys(opExpr);
641
+
642
+ const rhs = compileExpr(state, opData[op]);
643
+
644
+ let lhs;
645
+ if ($transform) {
646
+ lhs = compileFunction(
647
+ { ...state, implicitField: fieldRef },
648
+ typeof $transform === 'string' ? { [$transform]: '$' } : $transform,
649
+ );
650
+ } else {
651
+ lhs = compileExpr(state, '$' + fieldRef);
652
+ }
653
+
654
+ switch (op) {
655
+ case '$gte': {
656
+ const [left, right] = valArray(state, [lhs, rhs], [null, lhs.type]);
657
+ return `${left} >= ${right}`;
658
+ }
659
+ case '$lte': {
660
+ const [left, right] = valArray(state, [lhs, rhs], [null, lhs.type]);
661
+ return `${left} <= ${right}`;
662
+ }
663
+ case '$gt': {
664
+ const [left, right] = valArray(state, [lhs, rhs], [null, lhs.type]);
665
+ return `${left} > ${right}`;
666
+ }
667
+ case '$lt': {
668
+ const [left, right] = valArray(state, [lhs, rhs], [null, lhs.type]);
669
+ return `${left} < ${right}`;
670
+ }
671
+ case '$eq': {
672
+ if (castInput(state, rhs, lhs.type).type === 'null') {
673
+ return `${val(state, lhs)} IS NULL`;
674
+ }
675
+
676
+ const [left, right] = valArray(state, [lhs, rhs], [null, lhs.type]);
677
+
678
+ if (rhs.type === 'param') {
679
+ const orders = state.namedParameters.map(param => {
680
+ return param === rhs || param === lhs ? [param, { ...param }] : param;
681
+ });
682
+ state.namedParameters = [].concat.apply([], orders);
683
+
684
+ return `CASE
685
+ WHEN ${left} IS NULL THEN ${right} IS NULL
686
+ ELSE ${left} = ${right}
687
+ END`;
688
+ }
689
+
690
+ return `${left} = ${right}`;
691
+ }
692
+ case '$ne': {
693
+ if (castInput(state, rhs, lhs.type).type === 'null') {
694
+ return `${val(state, lhs)} IS NOT NULL`;
695
+ }
696
+
697
+ const [left, right] = valArray(state, [lhs, rhs], [null, lhs.type]);
698
+
699
+ if (rhs.type === 'param') {
700
+ const orders = state.namedParameters.map(param => {
701
+ return param === rhs || param === lhs ? [param, { ...param }] : param;
702
+ });
703
+ state.namedParameters = [].concat.apply([], orders);
704
+
705
+ return `CASE
706
+ WHEN ${left} IS NULL THEN ${right} IS NOT NULL
707
+ ELSE ${left} IS NOT ${right}
708
+ END`;
709
+ }
710
+
711
+ return `(${left} != ${right} OR ${left} IS NULL)`;
712
+ }
713
+ case '$oneof': {
714
+ const [left, right] = valArray(state, [lhs, rhs], [null, 'array']);
715
+ // Dedupe the ids
716
+ const ids = [...new Set(right)];
717
+
718
+ return (
719
+ `${String(left)} IN (` +
720
+ ids.map(id => `'${String(id)}'`).join(',') +
721
+ ')'
722
+ );
723
+ }
724
+ case '$like': {
725
+ const [left, right] = valArray(state, [lhs, rhs], ['string', 'string']);
726
+ return `UNICODE_LIKE(${getNormalisedString(right)}, NORMALISE(${left}))`;
727
+ }
728
+ case '$regexp': {
729
+ const [left, right] = valArray(state, [lhs, rhs], ['string', 'string']);
730
+ return `REGEXP(${right}, ${left})`;
731
+ }
732
+ case '$notlike': {
733
+ const [left, right] = valArray(state, [lhs, rhs], ['string', 'string']);
734
+ return `(NOT UNICODE_LIKE(${getNormalisedString(right)}, NORMALISE(${left}))\n OR ${left} IS NULL)`;
735
+ }
736
+ default:
737
+ throw new CompileError(`Unknown operator: ${op}`);
738
+ }
739
+ });
740
+
741
+ function compileConditions(state, conds) {
742
+ if (!Array.isArray(conds)) {
743
+ // Convert the object form `{foo: 1, bar:2}` into the array form
744
+ // `[{foo: 1}, {bar:2}]`
745
+ conds = Object.entries(conds).map(cond => {
746
+ return { [cond[0]]: cond[1] };
747
+ });
748
+ }
749
+
750
+ return conds.filter(Boolean).reduce((res, condsObj) => {
751
+ const compiled = Object.entries(condsObj)
752
+ .map(([field, cond]) => {
753
+ // Allow a falsy value in the lhs of $and and $or to allow for
754
+ // quick forms like `$or: amount != 0 && ...`
755
+ if (field === '$and') {
756
+ if (!cond) {
757
+ return null;
758
+ }
759
+ return compileAnd(state, cond);
760
+ } else if (field === '$or') {
761
+ if (!cond || (Array.isArray(cond) && cond.length === 0)) {
762
+ return null;
763
+ }
764
+ return compileOr(state, cond);
765
+ }
766
+
767
+ if (
768
+ typeof cond === 'string' ||
769
+ typeof cond === 'number' ||
770
+ typeof cond === 'boolean' ||
771
+ cond instanceof Date ||
772
+ cond == null
773
+ ) {
774
+ return compileOp(state, field, { $eq: cond });
775
+ }
776
+
777
+ if (Array.isArray(cond)) {
778
+ // An array of conditions for a field is implicitly an `and`
779
+ return cond.map(c => compileOp(state, field, c)).join(' AND ');
780
+ }
781
+ return compileOp(state, field, cond);
782
+ })
783
+ .filter(Boolean);
784
+
785
+ return [...res, ...compiled];
786
+ }, []);
787
+ }
788
+
789
+ function compileOr(state, conds) {
790
+ // Same as above
791
+ if (!conds) {
792
+ return '0';
793
+ }
794
+ const res = compileConditions(state, conds);
795
+ if (res.length === 0) {
796
+ return '0';
797
+ }
798
+ return '(' + res.join('\n OR ') + ')';
799
+ }
800
+
801
+ function compileAnd(state, conds) {
802
+ // Same as above
803
+ if (!conds) {
804
+ return '1';
805
+ }
806
+ const res = compileConditions(state, conds);
807
+ if (res.length === 0) {
808
+ return '1';
809
+ }
810
+ return '(' + res.join('\n AND ') + ')';
811
+ }
812
+
813
+ const compileWhere = saveStack('filter', (state, conds) => {
814
+ return compileAnd(state, conds);
815
+ });
816
+
817
+ function compileJoins(state, tableRef, internalTableFilters) {
818
+ const joins = [];
819
+ state.paths.forEach((desc, path) => {
820
+ const { tableName, tableId, joinField, joinTable } = state.paths.get(path);
821
+
822
+ let on = `${tableId}.id = ${tableRef(joinTable)}.${quoteAlias(joinField)}`;
823
+
824
+ const filters = internalTableFilters(tableName);
825
+ if (filters.length > 0) {
826
+ on +=
827
+ ' AND ' +
828
+ compileAnd(
829
+ { ...state, implicitTableName: tableName, implicitTableId: tableId },
830
+ filters,
831
+ );
832
+ }
833
+
834
+ joins.push(
835
+ `LEFT JOIN ${tableRef(
836
+ tableName,
837
+ true,
838
+ )} ${tableId} ON ${addTombstone(state.schema, tableName, tableId, on)}`,
839
+ );
840
+
841
+ if (state.dependencies.indexOf(tableName) === -1) {
842
+ state.dependencies.push(tableName);
843
+ }
844
+ });
845
+ return joins.join('\n');
846
+ }
847
+
848
+ function expandStar(state, expr) {
849
+ let path;
850
+ let pathInfo;
851
+ if (expr === '*') {
852
+ pathInfo = {
853
+ tableName: state.implicitTableName,
854
+ tableId: state.implicitTableId,
855
+ };
856
+ } else if (expr.match(/\.\*$/)) {
857
+ const result = popPath(expr);
858
+ path = result.path;
859
+ pathInfo = resolvePath(state, result.path);
860
+ }
861
+
862
+ const table = state.schema[pathInfo.tableName];
863
+ if (table == null) {
864
+ throw new Error(`Table "${pathInfo.tableName}" does not exist`);
865
+ }
866
+
867
+ return Object.keys(table).map(field => (path ? `${path}.${field}` : field));
868
+ }
869
+
870
+ const compileSelect = saveStack(
871
+ 'select',
872
+ (state, exprs, isAggregate, orders) => {
873
+ // Always include the id if it's not an aggregate
874
+ if (!isAggregate && !exprs.includes('id') && !exprs.includes('*')) {
875
+ exprs = exprs.concat(['id']);
876
+ }
877
+
878
+ const select = exprs.map(expr => {
879
+ if (typeof expr === 'string') {
880
+ if (expr.indexOf('*') !== -1) {
881
+ const fields = expandStar(state, expr);
882
+
883
+ return fields
884
+ .map(field => {
885
+ const compiled = compileExpr(state, '$' + field);
886
+ state.outputTypes.set(field, compiled.type);
887
+ return compiled.value + ' AS ' + quoteAlias(field);
888
+ })
889
+ .join(', ');
890
+ }
891
+
892
+ const compiled = compileExpr(state, '$' + expr);
893
+ state.outputTypes.set(expr, compiled.type);
894
+ return compiled.value + ' AS ' + quoteAlias(expr);
895
+ }
896
+
897
+ const [name, value] = Object.entries(expr)[0];
898
+ if (name[0] === '$') {
899
+ state.compileStack.push({ type: 'value', value: expr });
900
+ throw new CompileError(
901
+ `Invalid field "${name}", are you trying to select a function? You need to name the expression`,
902
+ );
903
+ }
904
+
905
+ if (typeof value === 'string') {
906
+ const compiled = compileExpr(state, '$' + value);
907
+ state.outputTypes.set(name, compiled.type);
908
+ return `${compiled.value} AS ${quoteAlias(name)}`;
909
+ }
910
+
911
+ const compiled = compileFunction({ ...state, orders }, value);
912
+ state.outputTypes.set(name, compiled.type);
913
+ return compiled.value + ` AS ${quoteAlias(name)}`;
914
+ });
915
+
916
+ return select.join(', ');
917
+ },
918
+ );
919
+
920
+ const compileGroupBy = saveStack('groupBy', (state, exprs) => {
921
+ const groupBy = exprs.map(expr => {
922
+ if (typeof expr === 'string') {
923
+ return compileExpr(state, '$' + expr).value;
924
+ }
925
+
926
+ return compileFunction(state, expr).value;
927
+ });
928
+
929
+ return groupBy.join(', ');
930
+ });
931
+
932
+ const compileOrderBy = saveStack('orderBy', (state, exprs) => {
933
+ const orderBy = exprs.map(expr => {
934
+ let compiled;
935
+ let dir = null;
936
+
937
+ if (typeof expr === 'string') {
938
+ compiled = compileExpr(state, '$' + expr).value;
939
+ } else {
940
+ const entries = Object.entries(expr);
941
+ const entry = entries[0];
942
+
943
+ // Check if this is a field reference
944
+ if (entries.length === 1 && entry[0][0] !== '$') {
945
+ dir = entry[1];
946
+ compiled = compileExpr(state, '$' + entry[0]).value;
947
+ } else {
948
+ // Otherwise it's a function
949
+ const { $dir, ...func } = expr;
950
+ dir = $dir;
951
+ compiled = compileFunction(state, func).value;
952
+ }
953
+ }
954
+
955
+ if (dir != null) {
956
+ if (dir !== 'desc' && dir !== 'asc') {
957
+ throw new CompileError('Invalid order direction: ' + dir);
958
+ }
959
+ return `${compiled} ${dir}`;
960
+ }
961
+ return compiled;
962
+ });
963
+
964
+ return orderBy.join(', ');
965
+ });
966
+
967
+ const AGGREGATE_FUNCTIONS = ['$sum', '$count'];
968
+ function isAggregateFunction(expr) {
969
+ if (typeof expr !== 'object' || Array.isArray(expr)) {
970
+ return false;
971
+ }
972
+
973
+ const [name, originalArgExprs] = Object.entries(expr)[0];
974
+ let argExprs = originalArgExprs;
975
+ if (!Array.isArray(argExprs)) {
976
+ argExprs = [argExprs];
977
+ }
978
+
979
+ if (AGGREGATE_FUNCTIONS.indexOf(name) !== -1) {
980
+ return true;
981
+ }
982
+
983
+ return !!(argExprs as unknown[]).find(ex => isAggregateFunction(ex));
984
+ }
985
+
986
+ export function isAggregateQuery(queryState) {
987
+ // it's aggregate if:
988
+ // either an aggregate function is used in `select`
989
+ // or a `groupBy` exists
990
+
991
+ if (queryState.groupExpressions.length > 0) {
992
+ return true;
993
+ }
994
+
995
+ return !!queryState.selectExpressions.find(expr => {
996
+ if (typeof expr !== 'string') {
997
+ const [_, value] = Object.entries(expr)[0];
998
+ return isAggregateFunction(value);
999
+ }
1000
+ return false;
1001
+ });
1002
+ }
1003
+
1004
+ // TODO: Type this based on schema/index
1005
+ type Schema = unknown;
1006
+
1007
+ export type SchemaConfig = {
1008
+ tableViews?:
1009
+ | Record<string, string>
1010
+ | ((name: string, config: { withDead; isJoin; tableOptions }) => string);
1011
+ tableFilters?: (name: string) => unknown[];
1012
+ customizeQuery?: (queryState: QueryState) => QueryState;
1013
+ views?: Record<
1014
+ string,
1015
+ {
1016
+ fields?: Record<string, string>;
1017
+ [key: `v_${string}`]: string | ((internalFields, publicFields) => string);
1018
+ }
1019
+ >;
1020
+ };
1021
+
1022
+ // Types per field. Should be based on the schema.
1023
+ export type OutputTypes = Map<string, string | number | null>;
1024
+
1025
+ type NamedParameter = {
1026
+ type: string;
1027
+ paramName: string;
1028
+ paramType?: string;
1029
+ value: string;
1030
+ };
1031
+
1032
+ // TODO: Type this
1033
+ type CompileStack = unknown[];
1034
+
1035
+ export type CompilerState = {
1036
+ schema: Schema;
1037
+ implicitTableName: string;
1038
+ implicitTableId: string;
1039
+ paths: Map<string, unknown>;
1040
+ dependencies: string[];
1041
+ compileStack: CompileStack;
1042
+ outputTypes: OutputTypes;
1043
+ validateRefs: boolean;
1044
+ namedParameters: NamedParameter[];
1045
+ };
1046
+
1047
+ export type SqlPieces = {
1048
+ select: string;
1049
+ from: string;
1050
+ joins: string;
1051
+ where: string;
1052
+ groupBy: string;
1053
+ orderBy: string;
1054
+ limit: number | null;
1055
+ offset: number | null;
1056
+ };
1057
+
1058
+ export function compileQuery(
1059
+ queryState: QueryState,
1060
+ schema: Schema,
1061
+ schemaConfig: SchemaConfig = {},
1062
+ ) {
1063
+ const { withDead, validateRefs = true, tableOptions, rawMode } = queryState;
1064
+
1065
+ const {
1066
+ tableViews = {},
1067
+ tableFilters = () => [],
1068
+ customizeQuery = queryState => queryState,
1069
+ } = schemaConfig;
1070
+
1071
+ const internalTableFilters = name => {
1072
+ const filters = tableFilters(name);
1073
+ // These filters cannot join tables and must be simple strings
1074
+ for (const filter of filters) {
1075
+ if (Array.isArray(filter)) {
1076
+ throw new CompileError(
1077
+ 'Invalid internal table filter: only object filters are supported',
1078
+ );
1079
+ }
1080
+ if (Object.keys(filter)[0].indexOf('.') !== -1) {
1081
+ throw new CompileError(
1082
+ 'Invalid internal table filter: field names cannot contain paths',
1083
+ );
1084
+ }
1085
+ }
1086
+ return filters;
1087
+ };
1088
+
1089
+ const tableRef = (name: string, isJoin?: boolean) => {
1090
+ const view =
1091
+ typeof tableViews === 'function'
1092
+ ? tableViews(name, { withDead, isJoin, tableOptions })
1093
+ : tableViews[name];
1094
+ return view || name;
1095
+ };
1096
+
1097
+ const tableName = queryState.table;
1098
+
1099
+ const {
1100
+ filterExpressions,
1101
+ selectExpressions,
1102
+ groupExpressions,
1103
+ orderExpressions,
1104
+ limit,
1105
+ offset,
1106
+ } = customizeQuery(queryState);
1107
+
1108
+ let select = '';
1109
+ let where = '';
1110
+ let joins = '';
1111
+ let groupBy = '';
1112
+ let orderBy = '';
1113
+ const state: CompilerState = {
1114
+ schema,
1115
+ implicitTableName: tableName,
1116
+ implicitTableId: tableRef(tableName),
1117
+ paths: new Map(),
1118
+ dependencies: [tableName],
1119
+ compileStack: [],
1120
+ outputTypes: new Map(),
1121
+ validateRefs,
1122
+ namedParameters: [],
1123
+ };
1124
+
1125
+ resetUid();
1126
+
1127
+ try {
1128
+ select = compileSelect(
1129
+ state,
1130
+ selectExpressions,
1131
+ isAggregateQuery(queryState),
1132
+ orderExpressions,
1133
+ );
1134
+
1135
+ if (filterExpressions.length > 0) {
1136
+ const result = compileWhere(state, filterExpressions);
1137
+ where = 'WHERE ' + result;
1138
+ } else {
1139
+ where = 'WHERE 1';
1140
+ }
1141
+
1142
+ if (!rawMode) {
1143
+ const filters = internalTableFilters(tableName);
1144
+ if (filters.length > 0) {
1145
+ where += ' AND ' + compileAnd(state, filters);
1146
+ }
1147
+ }
1148
+
1149
+ if (groupExpressions.length > 0) {
1150
+ const result = compileGroupBy(state, groupExpressions);
1151
+ groupBy = 'GROUP BY ' + result;
1152
+ }
1153
+
1154
+ // Orders don't matter if doing a single calculation
1155
+ if (orderExpressions.length > 0) {
1156
+ const result = compileOrderBy(state, orderExpressions);
1157
+ orderBy = 'ORDER BY ' + result;
1158
+ }
1159
+
1160
+ if (state.paths.size > 0) {
1161
+ joins = compileJoins(state, tableRef, internalTableFilters);
1162
+ }
1163
+ } catch (e) {
1164
+ if (e instanceof CompileError) {
1165
+ throw getCompileError(e, state.compileStack);
1166
+ }
1167
+
1168
+ throw e;
1169
+ }
1170
+
1171
+ const sqlPieces: SqlPieces = {
1172
+ select,
1173
+ from: tableRef(tableName),
1174
+ joins,
1175
+ where,
1176
+ groupBy,
1177
+ orderBy,
1178
+ limit,
1179
+ offset,
1180
+ };
1181
+
1182
+ return {
1183
+ sqlPieces,
1184
+ state,
1185
+ };
1186
+ }
1187
+
1188
+ export function defaultConstructQuery(
1189
+ queryState: QueryState,
1190
+ compilerState: CompilerState,
1191
+ sqlPieces: SqlPieces,
1192
+ ) {
1193
+ const s = sqlPieces;
1194
+
1195
+ const where = queryState.withDead
1196
+ ? s.where
1197
+ : addTombstone(
1198
+ compilerState.schema,
1199
+ compilerState.implicitTableName,
1200
+ compilerState.implicitTableId,
1201
+ s.where,
1202
+ );
1203
+
1204
+ return `
1205
+ SELECT ${s.select} FROM ${s.from}
1206
+ ${s.joins}
1207
+ ${where}
1208
+ ${s.groupBy}
1209
+ ${s.orderBy}
1210
+ ${s.limit != null ? `LIMIT ${s.limit}` : ''}
1211
+ ${s.offset != null ? `OFFSET ${s.offset}` : ''}
1212
+ `;
1213
+ }
1214
+
1215
+ export function generateSQLWithState(
1216
+ queryState: QueryState,
1217
+ schema?: Schema,
1218
+ schemaConfig?: SchemaConfig,
1219
+ ) {
1220
+ const { sqlPieces, state } = compileQuery(queryState, schema, schemaConfig);
1221
+ return { sql: defaultConstructQuery(queryState, state, sqlPieces), state };
1222
+ }